随机森林 = 很多棵决策树 + 两处随机 + 投票。 单棵树会一路切到底、把噪声也背下来(实测训练 0.9929、测试只有 0.7917); 八十棵树各看一份重采样的训练集、每次只准在一个随机特征上切,投票之后测试升到 0.8417。 代价是模型变成一片森林,再也画不成一张图。
前面六个算法都是「一个模型」。 这一节引入一个新思路:把很多不完美的模型凑起来。 它属于「集成学习」, 是表格数据上最常用的方法之一。 想系统看集成学习的其他分支(AdaBoost、GBDT), 可以去 40 课 第 9 章。
一、决策树:一路问问题,直到分出结论
决策树解决问题的办法很像医生问诊:先问一个最能把人分开的问题,根据回答分成两拨, 再对每一拨问下一个问题,直到能下结论。 在数据上,「问题」就是「某个特征是不是小于某个值」。每问一次就把数据切成两半,这叫一次切分。 怎么选问题?教材用的是基尼不纯度。 它衡量一批标签有多杂:两类各占一半时最大(0.5),全是一类时最小(0)。 每次切分都选「切完不纯度降得最多」的那个特征和那个阈值。 这个选法有个特点:它贪心。 每一步只管当下降得最多,不管后面会不会更好。 所以决策树容易切出一个「局部最优但整体别扭」的结构。这是它不稳定、方差大的根源。 树还有个好处是别的算法比不了的: 它的结果能画成一张图给人看。 「x2 小于 1.2 且 x1 大于 0.3 判为 A 类」—— 这种规则业务方一看就懂,这也是它在风控、医疗里长期占一席之地的原因。
二、树为什么会背答案
如果不限制深度,树会一直切下去,直到每个叶子节点里只剩一个样本。 这时训练集准确率必然是 1.0——每个点都有自己专属的一条路径。 但这条路径只对那个点有效。新来的点稍微挪一点,就走到别的叶子里去了。 这就是过拟合:模型记住了样本,没学到规律。 Demo 里量得很清楚:深度从 1 加到 16,训练准确率从 0.8250 爬到 0.9929, 测试却从 0.7833 掉到 0.7917,中间最高只到 0.8083。训练涨了 17 个点,测试只涨了不到 1 个点。 为什么涨不上去?因为数据里有 12% 的标签是故意翻错的。 这部分错误无法被任何模型学到,只能被平均掉。 而单棵树不会平均,它会把每条错误都当真。 常规的解法是剪枝:限制最大深度、限制叶子最少样本数。但剪枝很考验经验——剪多了欠拟合,剪少了没用。 随机森林给了另一条路:不剪,而是种很多棵,让它们互相抵消。
三、Bagging:让很多棵树投票
思路很朴素:如果一棵树容易犯错,那就种一百棵,让它们投票。 前提是这些树的错误不能一样——如果一百棵树错得一模一样,投票等于没投。 怎么让它们不一样? 每棵树看一份不同的训练集。 具体做法是有放回地抽样(bootstrap):从 280 个样本里随机抽 280 次, 有些样本被抽中多次,有些一次没抽中。每棵树的训练集都不一样。 这个做法叫 Bagging(Bootstrap Aggregating)。 它降低的是方差:单棵树的预测随训练集波动很大,平均之后波动互相抵消,整体就稳了。 注意它降不了偏差。如果所有树都系统性地偏(比如真实边界是曲线,而树只学到直线),投票也救不回来。 Bagging 治的是「不稳定」,不是「不准」。
四、随机森林:再加一处随机,让树更不像
光靠重采样还不够。 如果数据里有一个特别强的特征,每棵树都会优先在它上面切,结果一百棵树长得差不多,投票的意义就小了。 随机森林加了第二处随机: 每次切分时,只准在随机挑的一小部分特征里找切点。 分类问题通常取「特征数的平方根」, 回归取「特征数的三分之一」。 Demo 里只有两个特征, 所以每次只随机挑 1 个——这等于强制每棵树一半的切分用 x1、一半用 x2,树与树之间差异被拉到最大。 两处随机叠加,效果是树的预测之间相关性变低。 数学上可以证明: 集成的误差上限取决于单棵树的误差和树之间的相关性,相关性越低,上限越低。 这就是随机森林比单纯 Bagging 更好的原因。 实测:单棵 0.8083,五棵 0.8250,二十棵 0.8167,八十棵 0.8417。 中间那一下回落是随机波动——树的棵数不是越多越好,但方向是对的。
五、特征重要性:森林顺手送的礼物
森林没法画成一张图,但它能回答另一个问题:哪个特征最要紧? 做法是统计每个特征在所有树的所有切分里,一共贡献了多少不纯度下降。 贡献多的排前面,就是「重要特征」。 Demo 里深度 4 的树第一刀切在 x2 上,切点 −0.8904。 这题的生成规则是「x2 和 sin(1.6 x1) 比大小」—— x2 直接参与比较,当然更值得先切。树自己发现了这一点。 这个指标有两个坑要说清。 第一,它偏向「取值多的特征」:连续特征有很多可切的点,随便切都能降一点不纯度; 而只有两个取值的特征一共就一个切点。所以连续特征会显得更重要。 第二,共线的特征会互相分票。两个高度相关的特征,重要性会各分一半,看起来都不重要。 所以看重要性要成组看,不要只看单个排名。
六、动手 Demo:一棵树会背答案,一百棵树投票就不会
400 个点撒在 3 乘 3 的方格里,真实边界是曲线 x2 = sin(1.6 x1)。 然后把 12% 的标签故意翻错——这一步是关键,没有噪声的话,单棵树也能做到接近满分, 就看不出森林的价值。 训练 280 个、测试 120 个。决策树是手写的,只有七十行:基尼不纯度、扫所有切点、递归建树。
import numpy as np
def gini(y): # 不纯度:两类各半时最大
p = float(y.mean())
return 2.0 * p * (1.0 - p)
def best_split(X, y): # 扫所有特征、所有切点
base = gini(y)
best = (None, 0.0)
for j in range(X.shape[1]):
for t in np.unique(X[:, j]):
m = X[:, j] <= t
if m.sum() == 0 or (~m).sum() == 0:
continue
g = base - (m.sum() * gini(y[m]) + (~m).sum() * gini(y[~m])) / len(y)
if g > best[1]:
best = ((j, float(t)), g)
return best
# 森林:每次重采样训练集、每次只准看一个随机特征
for k in range(80):
idx = rg.choice(len(Xtr), len(Xtr), replace=True)
t = Tree(max_depth=16, n_feat=1).fit(Xtr[idx], ytr[idx])
votes += t.predict(Xte)
实跑输出:
--------------------------------------------------------------------
随机森林:一棵树会背答案,一百棵树投票就不会
--------------------------------------------------------------------
400 个点撒在 3 乘 3 的方格里,真实边界是曲线 x2 = sin(1.6 x1)。然后
**故意把 12% 的标签翻错**——真实数据里总有标错的、模棱两可的。训练 280 个、测试 120 个。
【一】单棵树:越深越会背
最大深度 实际深度 训练准确率 测试准确率
--------------------------------------------------
1 2 0.8250 0.7833
2 3 0.8286 0.7750
4 5 0.8893 0.7833
8 9 0.9786 0.8083
16 13 0.9929 0.7917
深度 1 是一条横线(只能按一个特征切一刀),欠拟合,训练和测试都只有 0.8 出头。
深度放开之后,训练准确率一路爬到 0.9929,
**测试却在 0.79 附近打转,深到 16 时还掉回 0.7917。**
训练几乎满分、测试反而降,这就是过拟合的样子。树会一路切下去,
直到把每一个翻错的标签都单独隔出来——**它记的是噪音,不是规律。**
【二】森林:让一百棵树吵架,然后投票
树的棵数 测试准确率
------------------------
1 0.8083
5 0.8250
20 0.8167
80 0.8417
每棵树都只看到**一份重采样出来的训练集**(bootstrap),每次切分只准在**随机挑的一个特征**
上找切点。这两处「随机」让每棵树的错法不一样,投票之后错误互相抵消。单棵 0.8083,
凑到 80 棵是 0.8417,涨了三个多百分点。
**别小看这三个点:测试集里本来就有 12% 的标签是错的,天花板大概在 0.88,**
**森林已经顶到很近了。**这就是集成学习最朴素的经验:**多个不完美的判断,比一个好。**
【三】代价:收益递减,而且再也画不出图
表里 20 棵到 80 棵的涨幅明显变小了——**森林的收益是递减的。**
到某个数量之后,再加树的边际收益接近零,只是白烧算力。另一个代价是模型变成一整片森林,
**再也没法画成一张图给人看**。森林换来的是准确率,牺牲的是可解释性。
【四】特征重要性:哪个特征在干活
看深度 4 那棵树的第一刀切在哪个特征上:**x2**,
切点是 -0.8904。这题的规则是「x2 和 sin(1.6 x1) 比大小」,x2 直接参与比较,
所以它比 x1 更值得先切——树自己发现了这一点。
这就是「特征重要性」的来源:**统计每个特征在所有树里贡献了多少不纯度下降。**
它不用人来指定权重,是从切分过程中自然长出来的——这也是随机森林在业务里受欢迎的原因之一,
报告里能说清「哪个变量最要紧」。
单棵树那张表要看两列的走势。训练从 0.8250 一路爬到 0.9929,测试从 0.7833 波动到 0.7917,
中间最好的时候才 0.8083。训练涨了 17 个点,测试涨了不到 1 个点——这就是过拟合的量化样子。
森林那四行:1 棵 0.8083,5 棵 0.8250,20 棵 0.8167,80 棵 0.8417。
20 棵那一下回落不是算法的问题,是随机波动——换了随机种子,20 棵也可能比 5 棵高。
为什么只涨三个多点?因为天花板就在那儿。测试集里本来有 12% 的标签是错的,
任何模型的上限大概 0.88。森林从 0.8083 顶到 0.8417,已经吃掉一大半能吃的空间。
「两处随机」值得单独说。重采样让每棵树看的样本不同,随机特征让每棵树切的维度不同。
Demo 里只有两个特征,所以 n_feat=1 等于强制一半的树先看 x1、一半先看 x2——差异被拉到了最大。
最后那段特征重要性不是算出来的,是「看树长什么样」看出来的。第一刀切 x2 而不是 x1,
是因为 x2 直接参与比较。真实的随机森林要统计所有树的所有切分,但道理就是这个道理。
30 秒实验
- 把标签翻错的比例从 12% 改成 0,单棵树的测试准确率会到多少?森林呢?
- 把
n_feat从 1 改成 2(每棵树都能看全部特征),森林的准确率会变高还是变低? - 把树的数量加到 300 棵,准确率还能涨吗?为什么?
常见误区
- 以为「树越多越准」。收益是递减的,而且树之间相关性高时(比如特征很少、
n_feat设成全部)加树几乎没用。先保证树的差异性,再谈数量。 - 用随机森林做外推。树的预测永远是训练集里出现过的标签值的平均, 它不可能预测出训练集里没有的数。线性回归至少还敢往外 extrapolate,树连试都不试。
- 把特征重要性当因果。它只说「这个特征在切分时有用」,不说明「改这个特征会改变结果」。 相关不等于因果,这一点在业务解释里尤其要小心。
自测三问(点开对答案)
1. Bagging 降的是偏差还是方差?为什么? —— 降方差。它把很多棵大致无偏但各自抖动的树平均起来,抖动互相抵消。 但如果所有树都系统性地偏,平均也救不回来——所以它降不了偏差。 2. 随机森林的两处随机分别是什么?各自有什么用? —— 一是对训练集有放回重采样(bootstrap),让每棵树看不同的样本; 二是每次切分只准看随机的一小部分特征,让每棵树在不同维度上切。 两者都是为了让树的预测之间「不那么像」,降低相关性。 3. 为什么单棵树的训练准确率能到 0.99,测试却只有 0.79? —— 因为树一路切到底,把每个样本(包括标错的)都单独隔离出来了。 训练集里那 12% 的错误标签它全记住了,而这些错误对新数据毫无帮助。随机森林是「很多简单模型凑起来」。 2.8 节要讲另一种「凑」——把简单的加权和一层层摞起来,这就是神经网络。
- ☐ (本节完,下一节 2.8 算法 8:神经网络)