随机森林,比你想象的更简单

2010年11月,微软在六十天内将一根黑色塑料条放在了八百万台电视下面。这仍然是历史上销售最快的消费电子产品的记录,领先于iPhone和iPad在其生命周期中的同一阶段。

你穿着袜子站在它面前挥手。它画出了你的骨架。肘部、手腕、膝盖、臀部,实时绘制,而你在客厅里手舞足蹈。

每个人都以为里面有什么巨大的东西。

其实只有三棵决策树。

不是三千棵。是三棵。每棵都问一些关于像素对的简单问题,大致类似于"这个点的东西是否比那边的东西离相机更近",大约二十个问题深度,然后三棵都投票。这就是Kinect身体追踪器,由微软研究团队于2011年发布,在2010年游戏机硬件上每帧运行时间不到五毫秒。

这是接下来十分钟要记住的事情。这些树单独来看都很差,这不是妥协或意外。这是设计。有人坐下来故意让每棵树变得更差,而整个组合因此变得更好。

到结束时你会确切知道为什么这种权衡有效。你也会知道为什么几乎每个人后来用来解释森林的图表悄悄地在说谎。

1、首先,什么是决策树

让我不要假设任何事情。

模型是接收关于某个事物的一些事实并返回关于该事物的猜测的机器。这些事实称为特征,它们是电子表格中的列。猜测是目标,即你希望拥有的列。

决策树是最诚实的模型,因为你可以阅读它。它玩二十个问题。它查看你的所有列,尝试每个列中每个合理的截止点,并选择单个是/否问题来最好地分离你想要的答案。类似于"这个肿块是否宽于16毫米"。回答是的行向左,回答否的行向右,每一侧都会被问一个新的问题。

"最好"这个词需要一个数字作为支撑。树通过两个结果组变得多么偏斜来评估每个候选问题。一个50个恶性50个良性的组是无用的。一个48个和2个的组是极好的。有几个公式可以测量这个,名字像基尼和熵,它们在实践中足够接近,我将不再提及它们。

当一个组无法进一步有效分割时,它就变成了叶子,一个最终的桶,包含少量训练行。要预测新东西,你将它向下传递问题,直到它落在叶子里,然后报告该叶子包含的内容。分类最常见的答案,数字直接取平均值。

这就是整棵树。现在看它崩溃。

我使用了scikit-learn附带的真实医疗数据集,569个乳腺肿瘤活检,每个有30个测量值,并训练了一棵树,同时让它每次加深一个问题。三分之二用于训练,其余作为它从未见过的患者隐藏起来。

在五个问题深度时,它正确预测了95%的新患者。推到六个时,它在训练行上达到了完美的100%,然后在新的行上立即变得更差,稳定在93.5%,无论我让它运行多深都保持在那里。

这就是过拟合,本文中的其他内容都是对此的回应。树停止学习肿瘤的样子,开始学习那369个特定肿瘤的样子。它记住了答案。

说起来容易做起来难。这里是一棵树一次一刀地切割二维空间。

每一刀都是一个是/否问题,因为树一次只能问一个列,每一刀都是直线水平或垂直线。到最后,它在单个离群点周围建立了一个私有的小盒子。

下面有一个更狡猾的问题。取那369个患者,交换二十个为其他二十个,重新训练。你经常会得到一棵在第一个问题上就不同意的树,因为第一个分割改变了下面的一切,少量的行就足以决定哪个列获胜。

这种跳跃性的词是方差。它并不意味着树平均而言是错误的。它意味着它是不可重复的,而不可重复本身就是一种无用。

2、一组房产经纪人,每人一个房间

比喻时间,这个比喻承载了文章的大部分内容。

你想知道一套公寓值多少钱。你可以雇佣一个优秀的测量员,带他们走遍整个地方,然后取他们的数字。那就是你单棵深树。仔细、彻底,如果他们那天对潮湿有不良情绪,你完全继承了这种不良情绪。

相反,你雇佣九个平庸的房产经纪人,让每人看一个房间。一个看厨房。一个看后面的卧室。一个只能站在走廊里嗅嗅。他们每个人都单独来看是个笑话。走廊那个尤其如此。

然后你取九个数字的平均值。

平均值可靠地击败了测量员,这是决定其他一切的部分。它之所以有效,仅仅是因为九个在不同方向上犯错。厨房那个因为台面漂亮而高估。走廊那个因为闻起来像垃圾桶而低估。他们的错误相互抵消。

现在打破它。让九个一起在整个公寓里走动。他们都会发现同一个大理石岛台,都对同一件事感到兴奋,都以同样的方式犯错。平均九个相同的错误,你仍然有错误。

这就是整个主题。平均的弱模型击败一个强模型,但只有当它们保持不同意时才有效。

3、数学原理,只有一行长

平均不是民间智慧,它是算术,一个世纪前有人在牲畜身上验证过。

1906年,弗朗西斯·高尔顿去了普利茅斯的一个肥牛展,游客付费猜测一头牛的宰杀重量。之后他收集了投票并自己做了计算。787个可用的猜测。中间猜测是1,207磅。这头动物重1,198磅。人群的中间值差了九磅,不到百分之一,来自一个大多数人都毫无头绪的房间。

底层的规则很短。如果每个猜测都因相同的量而波动,写成σ²因为统计学家喜欢希腊字母,并且如果猜测是独立的,意味着没有人的错误能告诉你关于任何其他人的任何信息,那么B个猜测的平均值波动为σ²/B。

四个猜测者,四分之一的波动。一百个,百分之一。

那里没有任何东西使任何个体变得更聪明。平均值并不比人群更有知识。它更稳定,而更稳定原来是我们缺少的大部分东西。

4、那么另外499个数据集从哪里来

一个问题。你有一个数据集,收集另外四百个是昂贵、缓慢或非法的。

解决方法是引导采样,听起来像作弊。如果你有500行,你通过从相同的500行中抽取500行来构建一个新的训练集,有放回地。选一行,写下来,放回去以便可以再次被选中。第12行可能出现三次。第340行可能永远不会出现。

相同大小,相同材料,不同。

为什么允许这样做?因为你的样本是你拥有的更广泛世界的最佳图像,所以从它重采样大致模仿收集更多。粗糙但足够有用,这描述了大量令人惊讶的统计学。

这样做B次,平均树,你就有了bagging,即bootstrap aggregating的缩写,这个名字听起来像配送中心的工作。Leo Breiman在1996年发表了它。

这里有一个数字在大约四分钟后会非常重要。我从2,000行中运行了400次引导抽取,并计算了任何给定行出现的频率。

每次都有略多于三分之一的行完全错过。记住这一点。

import numpy as np
from sklearn.tree import DecisionTreeRegressor
rng = np.random.default_rng(0)
def bag_fit(X, y, n_trees):
    trees = []
    n = len(X)
    for _ in range(n_trees):
        idx = rng.integers(0, n, size=n)
        t = DecisionTreeRegressor()
        t.fit(X[idx], y[idx])
        trees.append(t)
    return trees
def bag_predict(trees, X):
    P = np.array([t.predict(X) for t in trees])
    return P.mean(axis=0)

从上到下,因为学习的一半是学习阅读它。

  • import numpy as np 引入数组和随机数生成器。np是昵称,这样你就不用输入numpy四十次。— from sklearn.tree import DecisionTreeRegressor 获取一棵树,预测数字而不是类别的那种。— rng = np.random.default_rng(0) 构建numpy的随机生成器。0种子,它固定随机数序列,这样你的运行与我的匹配。— def bag_fit(X, y, n_trees) 定义一个函数。X是特征表,y是你要预测的列,n_trees是你想要的数量。— trees = [] 是收集它们的空列表。— n = len(X) 计数行。— for _ in range(n_trees) 重复主体那么多次。下划线意味着"有一个计数器但我不关心它"。— idx = rng.integers(0, n, size=n) 从0到n-1绘制n个整数,每个单独绘制,所以重复几乎可以肯定。这一行就是引导采样。— t = DecisionTreeRegressor() 使用默认值创建一棵树,这意味着没有深度限制。故意过度生长。— t.fit(X[idx], y[idx]) 训练它。向numpy传递行号列表会按该顺序拉取这些行,所以列出两次的行会出现两次。— trees.append(t) 将其归档。— P = np.array([t.predict(X) for t in trees]) 构建一个网格,每棵树一行,每个预测一列。— P.mean(axis=0) 向下平均。axis=0折叠树方向,每个预测留下一个数字。

那里没有任何东西能改善单棵树。每棵树仍然是一个过度生长的杂乱,记住了自己的样本。整个收益是通过平均购买的,这不需要建模技能,只需要计算,而计算是廉价的成分。

5、当它回答时实际看起来是什么样子

到目前为止很抽象。这里是一片真实森林中的五棵真实树,为一个真实的保留点评分。

三棵说是恶性,两棵说是良性,真相是恶性。你的五个模型中有两个是错误的,这一点也不重要。没有树必须是好的。它们只需要在不同的地方犯错。

一个值得知道的细节。对于分类,森林可以直接举手投票,但scikit-learn做了更好的事情,平均每棵树的叶子比例,所以你得到概率而不是裸标签。当有人问你有多确定时,这很重要。

6、破坏派对的数字

如果σ²/B是事实,500棵树应该基本上没有波动。让我们检查一下。

我重新训练了各种大小的森林,每个大小三十次,并测量它们在相同测试行上的预测波动了多少。

一棵树,9.54。五十五棵树,0.68。四百棵树,0.54。

所以前五十五棵树完成了98%的工作,剩下的三百四十五棵几乎什么都不做。如果你观看了森林十一分钟并想知道是否值得,这就是你的答案。

罪魁祸首是那个词独立,它在公式中做了所有繁重的工作,而我们的树显然不是独立的。每个引导样本都来自相同的数据,所以任何两棵树共享大约三分之二的行。更糟糕的是,如果一个列确实是最强的信号,基本上每棵树都会找到它并首先在它上面分割。相同的骨架,相同的盲点,相同的行上相同的错误。

这就是房产经纪人一起走动。

相同的九个猜测者,相同大小的错误,两个完全不同的结果。左侧面板平均偏差0.13。右侧面板偏差1.66,添加第十个、第一百个或第一千个猜测者也不会改变它。

当猜测相关而不是独立时,公式增长了第二部分。

Var = ρσ² + (1 − ρ)σ²/B

那个ρ,读作"rho",是树之间错误的平均相关性。零意味着不相关的错误。一意味着相同的错误。现在看看随着B增加会发生什么。

右边的项下面有B,所以它蒸发了。左边的项没有任何B。它不会移动,也永远不会移动。那是你的下限,只有ρ可以降低它。

7、酒吧问答规则

这是策略。它是随机森林中唯一真正聪明的想法,而且有点荒谬。

既然树总是同意,就别让它们同意了。

在每次分割之前,在树被允许选择问题之前,你对它隐藏大多数列。它从p个可用列中获得m个列的随机候选名单,它必须从该候选名单中选择最佳问题,即使数据集中真正最好的列就在那里,被排除,挥手。

不是每棵树一次。在每次分割时,新抽取,每棵树数百次。

Tin Kam Ho在1995年发表了隐藏部分列的想法,Breiman将其附加到bagging上并于2001年发表了Random Forests,此后几乎没有变化。一个小插曲我比应该的更喜欢:Breiman和Adele Cutler将"Random Forests"注册为实际商标。该方法任何人都可以免费使用。但名称在法律上是受保护的。

为此有一个代价,这是开头循环闭合的地方。被剥夺了最佳列,每棵树确实变得更差。Breiman证明森林的错误上限随着树之间的相关性增加而上升,随着每棵树的强度增加而下降,所以你在权衡这两者,最佳答案在中间。

我去找了中间。四十个列,十二个携带真实信号,三百棵树,其他都不动。

m       单棵树      错误重叠      森林
-----------------------------------------------------------
   1        60.9%            0.055            82.4%
   2        64.1%            0.082            84.1%
   4        67.4%            0.115            85.1%
   6        68.6%            0.126            85.5%   <- sqrt(40)
  12        70.3%            0.152            85.4%
  20        71.0%            0.163            85.0%
  40        71.2%            0.173            84.5%

向下阅读列。随着m缩小,单棵树逐渐变差,从71.2%下降到60.9%。错误重叠也一直在下降。而森林,唯一重要的列,在中间m = 6时达到峰值,这是40的平方根,是scikit-learn的默认值。

默认值不是随意的。有人测量过。

8、完整配方

五个动作,然后一个循环。

步骤一到四在隔离中构建一棵树,对任何其他树一无所知。步骤五是它们互动的唯一时刻,它所做的就是取平均值。这个算法中没有任何协调,这就是为什么它能很好地分散在CPU核心上,以及为什么它如此难以打破。

记住在Kinect规模下的成本。该团队训练了三棵树,二十个问题深度,在一百万张合成深度图像上,每个节点从2,000个随机生成的候选问题中选择。训练消耗了大约24,000 CPU小时,他们通过投入数百台机器在一天内完成。构建森林是昂贵的。使用一个几乎是免费的,对于一个投放到八百万客厅的_console来说,这是重要的权衡。

你的只需要四秒钟。

from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(
    n_estimators=500,
    max_features="sqrt",
    min_samples_leaf=1,
    oob_score=True,
    n_jobs=-1,
    random_state=7,
)
forest.fit(X_train, y_train)
print(forest.oob_score_)
print(forest.predict_proba(X_test)[:5])
  • n_estimators=500 是B,树的数量。更多是安全的,正如我们即将看到的。— max_features="sqrt" 是m。字符串告诉scikit-learn在每次分割时重新计算列数的平方根。— min_samples_leaf=1 允许叶子包含单行,步骤四中的故意过度生长。已经是默认值,写出来让你可以看到。— oob_score=True 开启免费验证分数,这是两个部分之后的内容,也是我在这里最喜欢的东西。— n_jobs=-1 使用你拥有的每个CPU核心。树从不互相交谈,所以这几乎是免费的速度。— random_state=7 固定种子,这样引导和列抽取会重复。没有它,你的分数每次运行都会变化,你会浪费一个上午。— .fit(X_train, y_train) 生长所有500棵树。— .predict_proba(X_test)[:5] 返回每个类的概率,[:5] 查看五行,这样你就不会淹没屏幕。

那么所有这些平均实际上购买了什么?相同数据,相同代码,只有树的数量在变化。

八个半点的准确率,而且没有一行代码改变,除了一个数字。那条柔和的曲线不是由任何东西绘制的。它是数百条锯齿状楼梯平均后的样子。

9、你已经支付的测试集

还记得三分之一的行从未进入袋中的条形图吗?

每棵树都有一组从未引入的行。大约36.8%的行,这个数字不是任意的。一行躲避一次抽取的概率是(1 − 1/n)。有n次抽取,所以它躲避所有抽取的概率是(1 − 1/n)ⁿ,对于任何值得讨论的n,这落在1/e上,即0.3679。

这些是袋外行。

回报是美妙的。要诚实地为第41行评分,只使用从未见过第41行的树。对每一行都这样做,你就有了对未见数据的真实误差估计,基于你已经训练的模型,不花费任何东西,也不保留任何东西。

n = 10_000
draws = rng.integers(0, n, size=n)
left_out = n - len(np.unique(draws))
print(left_out / n, (1 - 1 / n) ** n)

np.unique(draws) 丢弃重复项并返回选取的不同行,所以n减去其长度是从未被触及的行数。在一个真实的300棵树的森林中,平均每行是110.3棵树的袋外样本,这是森林的0.3677。理论说是0.3679。

在同一次运行中,五折交叉验证给出了84.33%,花了五倍的时间。袋外给出了84.87%。正确保留的测试集给出了85.47%。免费估计落在昂贵估计和真相之间。

在丢弃验证集之前有两个警告。树很少时袋外估计是悲观的,因为每行由已经很小的森林的三分之一来评分。在5棵树时,它悲观了9.4个百分点。到600棵树时,0.3个百分点。它假设你的行是独立的,如果你有时间序列、同一客户的重复访问或重复项,它们就不是独立的。那样它会误导你,不过公平地说普通交叉验证也会。

10、快速问答,我实际上应该调整什么

哪个设置最重要? max_features。没有比这更接近的了。那是三个部分之前的表格,也是它存在的原因。

多少棵树? 更多,直到分数停止移动。五十到1,600棵树将准确率从84.47%提高到85.67%,而且从未倒退,因为向平均值添加另一个数字不会使平均值跳跃。额外的树花费时间和内存,但永远不会影响准确率。

我应该限制深度吗? 通常不需要。过度生长是有意的。如果你的标签有噪声,将min_samples_leaf提高到5或10,这会阻止树在每个错误标记的行周围雕刻私有叶子。

基尼还是熵? 真的,抛硬币。

我的少数类被忽略了。 是的。森林可以通过从不预测少数类并感觉良好来达到97%。class_weight="balanced"是你的第一步。

11、说谎的图表

现在是我在开头打开的循环。

当你调用.feature_importances_时,scikit-learn返回平均不纯度减少。每次一个列用于分割时,它使组更偏斜,该改进由通过的行数加权。按列求和,归一化,打印。免费、快速,它有两个结合得很糟糕的缺陷。

缺陷一,它完全在训练数据上计算。帮助树记忆的列因记忆而获得报酬。

缺陷二更狡猾。一个有数千个不同值的列提供数千个可能的切割点。一个是/否列只提供一个。给任何东西数千张彩票,仅运气就会给它一个稍微分离训练行的切割,而树无法区分运气和信号,将其收入囊中并支付信用。统计学家在2007年正式记录了这一点,此后它一直在悄悄地破坏重要性图表。

我知道的最生动的演示来自一项关于纽约公寓租赁数据的研究。作者插入了一列纯随机数字作为对照。在预测列表会吸引多少兴趣的模型中,该随机列排名中间,得分比卧室数量和浴室数量更具预测性。

我曾经发布过一个模型,其最重要的特征是一个按注册顺序生成的内部计费参考。它对任何人都毫无意义。它有数千个不同的值,这就足够了。

修复方法是排列重要性,它几乎粗鲁地简单。获取你的保留数据,打乱一列使其值落在错误的行上,然后在不重新训练任何内容的情况下重新评估模型。

分数崩溃,那一列正在工作。什么都没发生,它没有工作。所以我构建了一个我知道真相的表:一个预测性连续列,一个预测性是/否标志,一个纯噪声列,以及一个名为account_id的六位随机整数。

不纯度重要性给account_id 0.23,真正有用的标志0.03。垃圾以七倍半的优势击败了真正的东西。同时模型在训练数据上得分完美的1.000,在保留数据上得分0.737,这正是图表所奖励的记忆化的指纹。

import pandas as pd
from sklearn.inspection import permutation_importance
mdi = pd.Series(forest.feature_importances_, index=cols)
pi = permutation_importance(
    forest, X_test, y_test, n_repeats=20, random_state=3, n_jobs=-1
)
out = pd.DataFrame({"mdi": mdi, "perm": pi.importances_mean,
                    "sd": pi.importances_std})
print(out.sort_values("mdi", ascending=False).round(4))
  • pd.Series(forest.feature_importances_, index=cols) 将不纯度数字与列名粘合,使输出可读而不是裸数组。— permutation_importance(forest, X_test, y_test, …) 执行打乱。注意它接受测试集。这就是重点。— n_repeats=20 每列打乱二十次,因为一次打乱本身是抛硬币,你想要平均值。— pi.importances_mean 是平均分数下降,pi.importances_std 是变化程度。如果平均值小于标准差,你就没有东西。— .sort_values("mdi", ascending=False) 按骗子排序,这样你可以逐行观看两个排名不同意。

为什么这很重要。 它每列每次重复花费一次额外的评分过程,这是解释你的模型与编造故事之间的区别。它有自己的弱点。当两列携带相同信息时,模型依赖于你没有打乱的那一列,所以两者看起来都毫无价值。如果两列几乎是双胞胎,将它们作为一对打乱。

12、模型如何处理列

重要性说明哪些列重要。它没有说明方向或形状,"收入重要"不是任何人可以采取的行动。

部分依赖图解决了这个问题。选择一列,选择一系列值,对于每个值覆盖每一行的该列,预测它们全部,然后平均。ICE图是相同的计算,但平均值关闭,每行一条淡线。

你两者都想要。

那不是凭空担心。部分依赖曲线可以是完全平坦的,而你的一半行急剧上升,另一半同样急剧下降,完美抵消。平坦的线说什么都没发生。ICE线说两件相反的事情发生在两个不同的群体上,这是带到会议上更有趣的事情。

from sklearn.inspection import PartialDependenceDisplay
PartialDependenceDisplay.from_estimator(
    forest, X_test, features=["signal", "useful_flag"],
    kind="both", subsample=60, random_state=0,
)

from_estimator接受一个拟合模型并执行扫描。features命名列,每个面板一个。kind="both"绘制淡ICE线和粗平均线,这是唯一值得费心的原因。subsample=60绘制六十条线而不是每行一条,因为五千条淡线是污迹,不是图表。random_state=0固定哪六十个。

一个警告。将列设置为每行的固定值可以制造不存在的人,比如一个有三十年服务的二十二岁的人,模型会愉快地为他们评分。当你的列强烈相关时,该曲线的部分是虚构的。

13、森林失败的三个地方

一、它们无法看到自己数据边缘之外。

每个叶子持有训练目标的平均值。每棵树返回一个叶子。森林平均这些树。你已经拥有的数字的平均值的平均值不能超过你已经拥有的最大数字,所以输出永久锁定在训练标签的范围内。

我在t从0到99的死直线上训练。在t = 110时真相是270,线性回归说267.9,森林说243.6。在t = 139时真相是328,线性回归说325.7,森林说243.6 again。不是漂移。相同的数字,因为它已经用完数据,正在返回它拥有的最大平均值。

t = np.arange(100).reshape(-1, 1)
y = 2.0 * t.ravel() + 50 + rng.normal(0, 8, 100)
rf = RandomForestRegressor(n_estimators=300).fit(t, y)
print(rf.predict([[110]]), rf.predict([[500]]))

所以永远不要将原始森林指向趋势时间序列。预测从一个时期到下一个时期的变化而不是水平,或者先拟合简单趋势线,让森林处理剩余部分。

二、非常宽、非常稀疏的数据。 比如20,000列,只有10个携带信号,对文本来说很正常。使用max_features="sqrt",树每次分割看到141列,所以至少有一个有用列在候选名单中的概率是6.8%。十分之九以上的分割是从纯噪声中选取的。

三、平滑关系。 树只能在直角处切割,所以一条温和的对角线变成楼梯。平均数百棵树可以很好地打磨它,正如你所看到的,但永远不会完全平坦。

14、那么什么时候森林是正确答案

诚实比较,两个数据集,一切默认。

在乳腺癌数据上,普通逻辑回归得到97.0%,森林得到94.5%。森林输给了1950年代的技术,因为那个问题接近直线,而直线是正确的工具。

在每类三个集群的混乱问题上,逻辑回归达到73.7%,森林85.3%。十一个半点。

这就是规则。当关系是颠簸的、充满交互的,并且没有人能提前告诉你它的形状时,森林就赚到了它们的钱。这也是为什么树集成在普通电子表格数据上持续击败神经网络。2022年一项跨45个数据集的大型基准测试发现,基于树的模型在中型表格问题上仍然领先,部分原因是它们以神经网络无法做到的方式忽略无用的列。

这就剩下了提升,具有相反策略的兄弟。

森林独立构建每棵树并平均它们,攻击波动。提升一次构建一棵树,每棵都拟合前一棵的错误,攻击系统误差。相同材料,相反计划。未调优时,提升得到86.4%,森林85.3%,大约是通常的差距,并且随着调优而扩大。

但它有真实成本。提升可以持续进行直到记住你的训练集,所以它需要提前停止和监督。森林不会那样对你。额外的树总是安全的。

这就是为什么森林仍然是我拟合的第一件事。四行,无需监督,附带免费误差估计,以及诚实的下限来衡量其他一切,然后决定问题是否值得更聪明的东西。

15、简短版本

树不稳定。平均解决不稳定性。平均不能完全解决问题的唯一原因是树之间彼此太同意,所以我们隐藏随机列在每次分割时强制它们分开。每棵树变得更差,平均变得更好,这种权衡是整个方法中唯一真正令人惊讶的想法。

三棵树在五毫秒内找到了你的肘部。

然后用打乱检查你的重要性图表。否则一个六位数账户号码会出现在你的幻灯片顶部,房间里有人会问它是什么意思。


原文链接:Random Forests — Why Averaging Many Weak Models Works (With Animated Examples)

汇智网翻译整理,转载请标明出处