上一节刚讲完「一个模型不够用」,这一节的答案是:那就养一群,然后投票。但投票不是万能药—— 这一节的重点恰恰是它什么时候不管用。 结论先放这儿: 投票能消掉「各自乱猜」的误差,消不掉「大家都错得一样」的误差。所以一堆模型要真比一个好, 前提是它们错得不重合。
这一节是第 8 章的开头,也是第 7 章的自然延伸。上一节的结论是「弱模型靠换」; 这里给出另一条路:不换模型,换「一群模型怎么合」。 它跟 7.2 的过拟合是一条线: bagging 治的是「不稳」(方差),boosting 治的是「太简单」(偏差)。 这两个毛病在 7.2 的深度表里都出现过—— 训练集 1.0000 而测试集掉下去,就是方差;两边一起差,就是偏差。
一、一群笨模型凭什么比一个强的强
先说清这句话的条件。三个条件缺一不可:
| 条件 | 意思 | 不满足会怎样 |
|---|---|---|
| 单个别太差 | 至少比瞎猜好 | 一群瞎猜的投票还是瞎猜 |
| 彼此要不一样 | 错的地方不能重合 | 一百个人抄同一份错答案 |
| 能合起来 | 投票或加权有一套规则 | 意见散着,合不出结论 |
| 第二条是最容易被忽略的,也是这一节的主线。想象一百个模型, | ||
| 每个都对同样的 30% 样本犯错——投票结果跟单个一模一样。反过来,如果它们的错彼此独立, | ||
| 多数投票的出错率会随人数指数下降。 | ||
| 这个「指数下降」是可以算的。假设每个模型独立、出错概率 p, | ||
| B 个模型多数投票出错的概率就是「B 次里错超过一半」的概率。 | ||
| p = 0.3 时,B = 1/3/11/51 对应的出错率是 0.3000/0.2160/0.0782/0.0014。 | ||
| 五十一个模型,出错的概率不到千分之二。 | ||
| 但这只是数学上的天花板。Demo 里拿真实的树去量这个数, | ||
| 实测比理论差了四个数量级——因为真实模型不独立。这一点第 5 段会算给你看。 |
二、Bagging:抽样本、投票
做法两步:有放回地抽 n 个样本训一棵(自助采样),重复 B 次,最后投票。「有放回」是全部的关键: 它让每棵树看到的训练集都不一样。 每棵树大概看到多少不同的样本?一个样本一次抽不中的概率是 1 − 1/n, n 次都没抽中的概率是 (1 − 1/n) 的 n 次方, n 大时趋近 1/e ≈ 0.368。所以每棵树大约看到 63.2% 的不同样本。 Demo 在 260 个点上跑二十次,平均抽到 161 个不同的(0.6225),理论值 0.6328。 剩下那 36.8% 没被抽到的样本,天然就是「这棵树没见过」的数据。拿它们给这棵树打分, 就得到一个几乎免费的验证集——这叫袋外样本(OOB),随机森林的「免费验证」就是靠它。 然后是最反直觉的一段:拿一层树来做 base,bagging 几乎不起作用。
| 做法 | 训练集准确率 | 测试集准确率 |
|---|---|---|
| 一层树(单棵) | 0.7346 | 0.7214 |
| 101 棵一层树投票 | 0.7269 | 0.7286 |
| 差 1 个点,而测试集 140 个点里一个点就是 0.0071——这点差别说明不了什么。更直接的证据是: | ||
| 101 棵树里只有 81 种不同的规则。 | ||
| 为什么?因为「在哪个坐标的哪个位置切一刀最好」这个判断,对抽样的那点小扰动根本不敏感。 | ||
| 同一批数据抽走几个点,最好的那一刀还切在同一个位置。方差本来就很小,平均一百次还是那个数。 | ||
| 这就是 bagging 的第一条边界:它治的是「不稳」,不是「笨」。 | ||
| 模型本身表达力不够(一层树只能说半平面),投票只会把同一个错答案重复一百遍。 |
三、随机森林:再抽一次,抽特征
Bagging 只随机了「样本」。随机森林又加了第二层随机:每次分裂时,只从所有特征里随机挑几个来比。 为什么要多这一层?因为如果某个特征特别强, 所有树都会用它、都在同一个位置切,树与树之间就高度相似——投票又退化成「抄同一份答案」。 抽特征是在人为制造分歧。 代价很直接:单棵会变差。它可能压根没看到最好的那个特征。 这是拿偏差换方差:单棵变笨,但一群的平均更稳。 这就是随机森林的全部秘密,没有第三层。它常常「开箱就能用」,原因不在算法多聪明, 而在它把「多样性」写进了训练过程,不靠运气。代价是可解释性变差—— 一棵树你能画出规则,五百棵树你只能看「哪个特征用得最多」。
四、AdaBoost:让后面的树专门盯前面的错
思路上跟 bagging 相反:不是各自独立抽数据,而是一轮一轮改样本权重。每轮训完, 分错的样本权重变大、分对的变小;每棵树还有一个「发言权」alpha。
| 量 | 怎么算 | 作用 |
|---|---|---|
| 错误率 | 被分错的样本权重之和 | 衡量这棵树有多差 |
| alpha | 0.5 × ln((1 − 错误率) ÷ 错误率) | 差的对就小声说话 |
| 权重的更新 | 乘 e 的「−alpha × 真值 × 预测」次方 | 错的变重、对的变轻 |
| Demo 拿一层树当 base,在这张圆盘数据集上跑了 60 轮: |
| 轮数 | 这一轮错误率 | alpha | 集成后测试集准确率 |
|---|---|---|---|
| 1 | 0.2654 | 0.5091 | 0.7214 |
| 3 | 0.3516 | 0.3059 | 0.7286 |
| 5 | 0.3121 | 0.3952 | 0.7786 |
| 10 | 0.3839 | 0.2365 | 0.8714 |
| 20 | 0.4224 | 0.1564 | 0.8929 |
| 40 | 0.4528 | 0.0948 | 0.9071 |
| 60 | 0.4500 | 0.1003 | 0.9000 |
| 从 0.7214 爬到 0.9071(第 40 轮),再加到 60 轮反而退回 0.9000。 | |||
| 六十棵树每一棵都还是「横一刀或竖一刀」,可它们合起来能圈出一个圆。 | |||
| 注意 alpha 的变化:第一轮 0.5091,最后十轮平均只有 0.1047。 | |||
| 后面的树错误率一直在 0.43 到 0.46 之间打转,它们拿不到多少发言权。 | |||
| 这不是它们没用,是它们只负责修一小块边角。 | |||
| 这一节要记住的分工在这里:bagging 降方差,boosting 降偏差。 | |||
| 模型「笨」的时候,只有后者管用。 | |||
| boosting 的短板也得说清:它对噪声敏感。因为每轮都在加权那些「还没分对」的点, | |||
| 标错的样本会被越加越重,后面的树全都围着几个错误标签打转。 | |||
| 这也是为什么 AdaBoost 之前要先清数据。 |
五、偏差和方差,分开量一次
光看一个测试集上的准确率,分不清「笨」和「不稳」。 换个办法:从一个大池子里反复抽不同的训练集,看同一个算法的成绩怎么抖。
| 做法 | 平均准确率 | 标准差 | 最好/最差 |
|---|---|---|---|
| 单棵深树 | 0.9219 | 0.0233 | 0.9500/0.8714 |
| 15 棵投票 | 0.9329 | 0.0158 | 0.9643/0.9143 |
| 平均分抬了一点(0.0110),标准差几乎砍了一半。 | |||
| 这就是「bagging 降方差」的量化版:抖动变小,是它的主要功劳。 | |||
| 那为什么「一百个人投票」在现实里没那么神?把实测的单棵错误率代进独立假设算:p = 0.0781、 | |||
| B = 15 时,理论出错率只有 0.000005。实测是 0.0671——差四个数量级。 | |||
| 差别就在「独立」这两个字。量一下:两棵树各自错 0.0857 和 0.0929, | |||
| 同时都错 0.0500;如果独立,同时都错应该只有 0.0080。 | |||
| 实测是它的 6.3 倍——它们的错误是相关的。 | |||
| 所以集成真正稀缺的东西不是「模型多」,是「错得不重合」。 | |||
| 随机森林那句「每次分裂只随机看几个特征」,就是在人为制造这种不重合。 | |||
| 代价是单棵更差,换来的是这一群错得不一样。 |
六、动手 Demo:一堆笨模型,怎么凑出一个聪明的
五段:一个笨模型、bagging 对它无效、换成深树才有效、AdaBoost、以及为什么会差这么多。 这五段合起来是一句话:投票治「不稳」,补课治「笨」。 注意第 2 段那个「101 棵树里只有 81 种规则」的诊断。它比看准确率有用得多—— 准确率几乎没动的时候,看「这群模型是不是同一张嘴」立刻就清楚了。
def stump(X, y, w=None): # 一层树:问一个问题,两片叶子各给一个答案
if w is None:
w = np.ones(len(y))
f, t = best_split(X, y, w, 0.15) # 基尼不纯度扫阈值,两边至少 15% 的点
m = X[:, f] <= t
a = int((w[m] * y[m]).sum() / w[m].sum() >= 0.5) # 左边的答案
c = int((w[~m] * y[~m]).sum() / w[~m].sum() >= 0.5)
return (f, t, a, c)
def bag(X, y, B, seed, deep): # bagging:有放回抽 n 个,抽 B 次
r = np.random.default_rng(seed)
ms = []
for _ in range(B):
idx = r.integers(0, len(y), len(y)) # 关键:允许重复
ms.append(build(X[idx], y[idx], 0) if deep else stump(X[idx], y[idx]))
return ms
def bag_pred(ms, X, deep): # 投票:过半数为准
v = np.zeros(len(X))
for m in ms:
v += pred_tree(m, X) if deep else pred_stump(m, X)
return (v / len(ms) >= 0.5).astype(int)
# AdaBoost:分错的样本权重变大,每棵树还有一个发言权 alpha
w = np.ones(len(ys)) / len(ys)
for it in range(1, 61):
m = stump(XA, YA, w)
pr = np.where(pred_stump(m, XA) == 1, 1.0, -1.0)
err = float(w[pr != ys].sum())
a = 0.5 * np.log((1.0 - err) / err) # 错得越少,说话越响
w = w * np.exp(-a * ys * pr) # 分对的降权,分错的升权
w = w / w.sum()
ms.append(m)
alphas.append(a)
实跑输出:
--------------------------------------------------------------------
一堆笨模型,怎么凑出一个聪明的
--------------------------------------------------------------------
==============================================================
[1] 先看一个笨模型能有多笨
造一批「圆盘」形状的点:半径 1.10 以内的算一类,外面的算另一类。
**它的边界是个圆,而一层树只会横切一刀或竖切一刀。**
一层树的规则:坐标 1 是不是 <= 0.733 ?左边判第 1 类,右边判第 0 类。
训练集准确率 0.7346,测试集准确率 0.7214。
**两边都只有 0.72 上下,这不是过拟合,是模型不够用。**
过拟合是「训练集很好、测试集差」;这里是**两边一起差**——
**它的表达力只到「一个半平面」,而正确答案是一个圆。这叫偏差大。**
正例占了 66.9%,所以连「全判正例」都能有 0.6692;一层树只比这多一点点。
[2] Bagging:抽样本、投票,先看它对笨模型管不管用
做法两步:**有放回抽 n 个样本训一棵(自助采样),重复 B 次,最后投票。**
有放回抽,每个样本没被抽中的概率是 (1 − 1/n) 的 n 次方。真跑二十次:
训练集 260 个点,抽二十次,平均每次抽到 161 个不同(占 0.6225)
理论极限 1 - (1 - 1/260) 的 260 次方 = 0.6328
**大约六成三。**剩下三成七每次都没被抽到。
**它们天然是「没见过的数据」,可以拿来估误差——这就是袋外样本。**
做法 训练集准确率 测试集准确率
--------------------------------------------------------------
一层树(单棵) 0.7346 0.7214
101 棵一层树投票 0.7269 0.7286
101 棵树里只有 81 种不同的规则。**投票投的基本是同一批意见。**
结果从 0.7214 挪到 0.7286,差 1.0 个点。
**测试集 140 个点,一个点就是 0.0071,这点差别说明不了什么。**
**为什么?因为「在哪个坐标的哪个位置切一刀最好」这个判断,对抽样的那点小扰动不敏感。**
**方差本来就很小,平均一百次还是那个数。**
这就是 bagging 的第一条边界:**它治的是「不稳」,不是「笨」。**
[3] 换成一棵长到底的树,bagging 才开始起作用
一棵长到底的树:24 个叶子,训练集 1.0000,测试集 0.8929
这棵树已经不错了。**但它不稳**:训练集里几个点的差别,就可能让某次切分换个位置。
从一个大池子里反复抽 15 份不同的训练集,每份长一棵、再长 15 棵投票:
做法 平均准确率 标准差 最好/最差
------------------------------------------------------------------
单棵深树 0.9219 0.0233 0.9500/0.8714
15 棵投票 0.9329 0.0158 0.9643/0.9143
**平均分从 0.9219 抬到 0.9329,标准差从 0.0233 压到 0.0158。**
同一个 base 模型、同一种投票,只是把「一层」换成「到底」。
**同一个工具,从「几乎没动」变成了「明显有用」。**
**所以「bagging 有没有用」这个问题,答案不在 bagging 里,在「你的模型稳不稳」里。**
[4] AdaBoost:让后面的树专门盯前面的错
思路上跟 bagging 相反:**不是各自独立抽数据,而是一轮轮改样本权重。**
每轮训完,**分错的样本权重变大、分对的变小**;每棵树还有个「发言权」alpha,错得少的树说话更响。
轮数 这一轮错误率 alpha 集成后测试集准确率
------------------------------------------------------------------
1 0.2654 0.5091 0.7214
2 0.3334 0.3465 0.7214
3 0.3516 0.3059 0.7286
5 0.3121 0.3952 0.7786
10 0.3839 0.2365 0.8714
20 0.4224 0.1564 0.8929
40 0.4528 0.0948 0.9071
60 0.4500 0.1003 0.9000
**从 0.7214 爬到 0.9071(第 40 轮),再加到 60 轮反而退回 0.9000。**
六十棵树每一棵都还是「横一刀或竖一刀」。
**可它们合起来能圈出一个圆——因为每一棵都在补前面没管到的那一块。**
alpha 这一列:**第一轮 0.5091,最后十轮平均只有 0.1047。**
最后二十轮的错误率在 0.43 到 0.46 之间打转,**发言权自然小——它们只负责修一小块边角。**
**一句话对比:bagging 是「很多棵各自猜,然后平均」;boosting 是「后面的树专门补前面的错」。**
**前者降方差,后者降偏差。模型「笨」的时候,只有后者管用。**
[5] 投票的数学:独立假设为什么靠不住
教科书里有个漂亮的算式:**B 个分类器各自独立、各自出错概率 p。**
**多数投票出错的概率,就是「B 次里错超过一半」的概率。**
拿实测的单棵错误率代进去:
量 数值
----------------------------------------------------
单棵深树的平均错误率 p 0.0781
15 棵投票:实测错误率 0.0671
15 棵投票:独立假设下应该多低 0.000005
**独立假设说「几乎不可能错」,实测却是 0.0671——差了四个数量级。**
**差别就在「独立」这两个字:这 15 棵树看的是同一批数据,错的地方本来就重合。**
量一下:两棵树各自错 0.0857 和 0.0929,**同时都错 0.0500**;
如果独立,同时都错应该只有 0.0080——**实测是它的 6.3 倍。**
**所以集成真正稀缺的东西不是「模型多」,是「错得不重合」。**
随机森林那句「每次分裂只随机看几个特征」,就是在人为制造这种不重合。
**代价是单棵更差,换来的是这一群错得不一样。**
整节连起来:**弱模型 → 抽样本投票(治不稳)→ 换深模型再投票 → 改权重补课(治笨)。**
**每一步都在做同一件事:让这一群模型的错误尽量不重合。**
第 2 段是这一节的核心实验,比任何比喻都清楚。同一个 bagging, 配一层树完全无效(0.7214 → 0.7286),配深树就有效(标准差从 0.0233 掉到 0.0158)。 所以「bagging 好不好」这个问题本身问错了,得问「我的模型稳不稳」。 第 3 段那个标准差 0.0233 是「重复抽 15 份训练集」量出来的,不是理论值。 这就是「方差」这个词的实测含义:换一批数据,成绩会跳多少。 第 4 段最值得看的是 alpha 那一列往下掉。第一轮 0.5091, 最后十轮平均 0.1047—— 后面的树是在「抠边角」,不是在「给出主要判断」。AdaBoost 最后那个 0.9000 比 0.9071 低, 也是「加太多轮会过拟合」的实证。 第 5 段那个 0.000005 对 0.0671 是整节课最该记住的一对数。 它说明「理论上的集成」和「现实里的集成」差在哪里——差在相关性。两个模型看同一批数据、 用同一类假设,它们错的地方必然重合。
30 秒实验
- 把
bag里那句「有放回抽」改成「不放回抽」。 每棵树变成完全一样,投票彻底失效——这一改就证明了「有放回」才是 bagging 的全部来源。 - 把 AdaBoost 的轮数从 60 加到 300,看测试集准确率还上不上。 它会先升后降,这就是 boosting 的过拟合:轮数本身就是一个要调的超参。
- 把第 3 段的
maxd从 12 改成 3,重跑。 单棵和投票的差距会缩小——树越浅越稳,方差本来就小,能省下的也就不多。
常见误区
- 以为「集成一定比单个好」。Demo 第 2 段就是反例:101 棵一层树投出来,跟 1 棵差 1 个点。 关键不是数量,是多样性。
- 以为 bagging 能救「模型太简单」。它只能平均掉随机性, 平均不掉系统性偏差——一堆只会切半平面的模型,投票还是半平面。
- 以为 boosting 的轮数越多越好。Demo 里第 40 轮 0.9071,第 60 轮退回 0.9000。 轮数是超参,得用验证集挑。
- 以为随机森林「随机」所以不精确。它随机的是「每棵树看哪几个特征」,不是「随便猜」; 单棵变差是代价,换来的是这一群错得不一样。
自测三问(点开对答案)
1. 为什么自助采样每次都只能拿到大约 63.2% 的不同样本? —— 因为**每个样本一次没被抽中的概率是 1 − 1/n,n 次都没抽中的概率是 (1 − 1/n) 的 n 次方,** **n 大时趋近 1/e ≈ 0.368。**所以被抽到过的不同样本约占 63.2%。 Demo 在 260 个点上测到 0.6225,理论值 0.6328。 **这个「剩下三成七」不是浪费,它正好是免费的验证集(袋外样本)。** 2. 同一个 bagging,为什么配一层树无效、配深树有效? —— 因为**bagging 平均掉的是「换一批数据就变」的那部分(方差)。** 一层树的最优切分对抽样扰动不敏感(101 棵只有 81 种规则), **方差本来就小,没有可平均的东西;它的毛病是偏差——只能说半平面,这个投票消不掉。**深树相反, **换一批数据切分就变,方差大,一平均就稳了(标准差 0.0233 → 0.0158)。** 3. 为什么「15 个模型投票,出错率应该趋近于零」在现实中不成立? —— 因为那个推导里有个「各自独立」的前提, **而现实中的模型看的是同一批数据、用的是同一类假设,错误高度相关。**Demo 里实测: 两棵树各自错 0.0857 和 0.0929, **独立的话同时都错应该只有 0.0080,实测 0.0500,是它的 6.3 倍。** **相关性越高,投票越像「抄同一份答案」。**这一节管的是「一个任务、一堆模型」。 下一节换个更难的局面:没有人告诉你答案,只有一个迟到很久的分数。那时候连「训练集」都没有, 只能自己走、自己记。
- ☐ (本节完,下一节 8.2 强化学习)