200 个树桩串起来: 训练误差从 25.83% 一路压到第 150 轮的 0.00%,而测试误差最低只有 18.83%(第 3 到 20 轮), 之后回升到 21.33%。 最大权重除以最小权重从 2.9 涨到 933.3——每轮都在把注意力往分错的点上挪。 和 bagging 对照:树桩从 63.17% 到 81.17%(boost 20 轮),bagging 对它一点用都没有; 深树 bag 50 是 81.50%,两者差不多。
9.2 留下了一个明确的失败:bagging 救不了树桩。原因是树桩太稳,没有方差可减。 可树桩的问题不在「抖」,在「能力不足」——它一次只能切一刀,很多规律它压根表达不出来。 这一节的办法是换拼法:不并行地平均,而是串行地加权。 每个新模型专门去补前面那些模型还没分对的点。 结果是一个「很弱」的模型,串起来能比「很强」的单个模型还准。
一、从弱到强:加权的思路
Boosting 的三步,每轮重复: 一是按当前权重找一个错得最少的弱学习器;二是按它的错误率给它一个权重(错得越少权重越大); 三是把分错的点权重调大、分对的调小,进入下一轮。 最后预测的时候不是投票,是「加权求和再取符号」。 权重 alpha 的公式是二分之一乘 ln((1 减 错误率) 除以 错误率)。 错误率 0.5 时 alpha 是零(等于乱猜,没有发言权);错误率越小 alpha 越大。 实测第 1 轮错误率 0.2583,alpha 0.527;第 200 轮错误率 0.4473,alpha 0.106。 「调权重」这一步是整个方法的核心,它和第 8 章的样本加权是同一套机制。 分错的点权重变大,下一个弱学习器在最小化加权错误率时,就不得不先照顾它们。 于是每个新模型都被逼着去啃「前面没啃下来的硬骨头」。 实测里最大权重除以最小权重,从第 1 轮的 2.9 涨到第 200 轮的 933.3。 这个比值就是「注意力集中程度」的直接读数。
二、实测:训练误差压到零
| 轮次 | 该轮加权错误率 | alpha | 累计训练误差 | 测试误差 | 权极比 |
|---|---|---|---|---|---|
| 1 | 0.2583 | 0.527 | 25.83% | 36.83% | 2.9 |
| 3 | 0.2526 | 0.542 | 8.33% | 18.83% | 3.4 |
| 10 | 0.3521 | 0.305 | 8.33% | 18.83% | 17.8 |
| 20 | 0.4229 | 0.155 | 7.50% | 18.83% | 55.1 |
| 60 | 0.4333 | 0.134 | 4.17% | 19.50% | 121.5 |
| 100 | 0.4380 | 0.125 | 1.67% | 19.17% | 177.0 |
| 150 | 0.4463 | 0.108 | 0.00% | 21.00% | 503.6 |
| 200 | 0.4473 | 0.106 | 0.00% | 21.33% | 933.3 |
| 累计训练误差这一列:第 1 轮 25.83%,第 3 轮就掉到 8.33%,第 150 轮精确到 0.00%。 | |||||
| 一个只会切一刀、单独用只有 63.17% 准确率的模型,串两百个就能把训练集全分对。 | |||||
| 这是 boosting 最惊人的地方。 | |||||
| 「该轮加权错误率」那一列是往上涨的(0.2583 到 0.4473),看着像越学越差。 | |||||
| 其实不是:每一轮的权重都变了,这个数是在「新的、更难的题目」上算的,前后不可比。 | |||||
| 它能一直待在 0.5 以下,就说明每个树桩还是比乱猜强——这是 boosting 能收敛的前提。 | |||||
| alpha 那一列一路走低(0.527 到 0.106),和上面那件事是一回事:题目越难,新模型的错误率越高, | |||||
| 它能拿到的权重就越小。 | |||||
| 到最后每轮的贡献只有第一轮的十分之一,加进去也改不了大局。 |
三、实测:测试误差先降后升
测试误差最低是 18.83%,出现在第 3 到第 20 轮;之后一路回升到 21.33%。 训练误差在这段时间里还在往零掉(8.33% 掉到 0.00%)。 一个降、一个升——这就是过拟合的标准形态,和 9.1 里那棵树是一模一样的病。 为什么会这样?因为「分不对的点」里有一大批本来就是标错的。 数据翻了 8% 的标签,这 8% 是纯噪声,任何模型都不可能分对。 可 boosting 不管这些,它只看「谁还没分对」,然后把权重压上去。 于是最后那几十轮,它做的事就是「专门去拟合噪声」。 这也解释了为什么 alpha 越来越小、权极比越来越大。 注意力越集中,能学到的东西越少,每一轮的增量就越小——同时也越针对那几个噪声点。 所以 boosting 必须早停。 挑轮数的办法和 9.1 挑 alpha 一样:留一份验证集,看它什么时候开始变差。 「训练误差到零」绝对不能当停止条件——实测到零的时候,测试误差已经从 18.83% 涨到 21.00% 了。
四、bagging 与 boosting 的分工
| 做法 | 训练误差 | 测试误差 | 说明 |
|---|---|---|---|
| 单个深树 | 5.00% | 24.50% | 方差大、偏差小 |
| 单个树桩 | 25.83% | 36.83% | 方差小、偏差大 |
| 深树 bag 50 | 5.00% | 18.50% | 降方差,偏差不动 |
| 树桩 boost 20 轮 | 7.50% | 18.83% | 降偏差,方差会涨 |
| 第一行到第三行:bagging 把深树的测试误差从 24.50% 降到 18.50%。 | |||
| 第二行到第四行:boosting 把树桩的测试误差从 36.83% 降到 18.83%,降了 18 个百分点。 | |||
| 看第二行和第三行:树桩差得远(36.83%), | |||
| 可 bagging 对它毫无办法(9.2 实测 63.17% 到 63.00%)。 | |||
| 因为它的病是偏差,而 bagging 只会治方差。 | |||
| 换成 boosting,同一个树桩立刻就能打——这才是对症下药。 | |||
| 这张表里还有个值得注意的地方:两个办法最后的成绩几乎一样(18.50% 对 18.83%)。 | |||
| 可它们走的路完全不同:一个把 15 个叶子的复杂模型变稳,一个把 1 次切分的简单模型变强。 | |||
| 这也说明「哪个更好」没有统一答案,取决于你手上有什么基学习器、有多少计算预算。 | |||
| 顺带说清一个常被混淆的点:boosting 的「方差会涨」是什么意思。 | |||
| 串行的模型之间不独立,第 t 个模型是看着前 t 减 1 个的结果学的, | |||
| 所以「平均降方差」那套推导完全不适用。 | |||
| 轮数越多,模型越贴着训练集,方差只会跟着涨——这也是必须早停的另一个理由。 |
五、动手 Demo:Boosting:200 个树桩把训练误差压到 0.00%,测试误差最低 18.83%
还是 9.1、9.2 那份数据(120 个训练点、600 个测试点、8% 标签噪声)。 基学习器换成树桩:只切一刀,二维里就是「x1 或 x2 大于某值」。 跑 200 轮 AdaBoost,每轮记录加权错误率、alpha、累计训练误差、测试误差, 以及「当前最大权重除以最小权重」这个注意力指标。 最后把单个深树、单个树桩、深树 bag 50、树桩 boost 20 轮四种做法摆在一起比。
w = np.ones(n) / n # 初始权重:均匀
S, Ste = np.zeros(n), np.zeros(len(Xte))
for t in range(200):
st = fit_stump(Xtr, ytr, w) # 加权错误率最小的树桩
pr = stump_predict(st, Xtr)
err = (w * (pr != ytr)).sum()
al = 0.5 * np.log((1 - err) / err) # 错误率越小,权重越大
S += al * pr # 加权累加
Ste += al * stump_predict(st, Xte)
w = w * np.exp(-al * ytr * pr) # 分错的权重大、分对的小
w /= w.sum()
实跑输出:
--------------------------------------------------------------------
Boosting:200 个树桩把训练误差压到 0.00%,测试误差最低 18.83%
--------------------------------------------------------------------
数据 和 D031、D032 同一份(120 个训练点,600 个测试点)
基学习器 树桩:只切一刀,二维里就是一个「x1 或 x2 大于某值」的判定
单个树桩 测试准确率 63.17%——比随便猜好不了多少
每轮 按当前权重找错得最少的树桩,给它一个权重 alpha,再把分错的点权重调大
逐轮记录(alpha 等于二分之一乘 ln((1 减 加权错误率) 除以 加权错误率)):
轮次 该轮加权错误率 alpha 累计训练误差 测试误差 最大最小权重比
-----------------------------------------------------------------------
1 0.2583 0.527 25.83% 36.83% 2.9
3 0.2526 0.542 8.33% 18.83% 3.4
10 0.3521 0.305 8.33% 18.83% 17.8
20 0.4229 0.155 7.50% 18.83% 55.1
60 0.4333 0.134 4.17% 19.50% 121.5
100 0.4380 0.125 1.67% 19.17% 177.0
150 0.4463 0.108 0.00% 21.00% 503.6
200 0.4473 0.106 0.00% 21.33% 933.3
**先看最后一列:最大权重除以最小权重,从几倍涨到上千倍。**
**这就是 boosting 在干的事——每轮都把注意力往「还没分对的点」上挪。**
**权重一集中,下一个树桩就必须去啃那些硬骨头,不然它的加权错误率降不下来。**
**再看「该轮加权错误率」这一列:它是往上涨的(0.2583 到 0.4473)。**
**这看着像「越学越差」,其实不是:每一轮的权重都变了,这个数是在「新的、更难的题目」上算的,前后不可比。**
**它能一直待在 0.5 以下,就说明每个树桩还是比乱猜强。**
**累计训练误差:第 1 轮 25.83%,第 3 轮就到 8.33%,第 150 轮到 0.00%。**
**一个只会切一刀的模型,拼两百个就能把训练集全分对——这是 boosting 最惊人的地方,也是它最容易骗人的地方。**
**可测试误差不跟着降。最低是 18.83%,出现在第 3 到 20 轮;之后一路回升到 21.33%。**
**这就是 boosting 的过拟合:轮数越多,它越专注那些「分不对的点」,而那些点里有一大批本来就是标错的。**
**它把噪声也当成必须攻克的难题,最后被噪声带着走。**
**所以 boosting 必须早停,靠验证集挑轮数——和 D031 里挑 alpha 是同一件事。**
三者摆在一起看:
做法 训练误差 测试误差 说明
--------------------------------------------------------
单个深树 5.00% 24.50% 方差大、偏差小
单个树桩 25.83% 36.83% 方差小、偏差大
深树 bag 50 5.00% 18.50% 降方差,偏差不动
树桩 boost 20 轮 7.50% 18.83% 降偏差,方差会涨
**第一行到第三行:bagging 把深树的测试误差从 24.50% 降到 18.50%。**
**第二行到第四行:boosting 把树桩的测试误差从 36.83% 降到 18.83%,降了 18.00 个百分点。**
**bagging 救不了树桩(D032 里已经看到,63.17% 到 63.00%),boosting 能救。因为前者降方差、后者降偏差,而树桩缺的是偏差。**
**这一条是第 9 章最该记住的结论:集成方法不是「多几个模型平均一下」这么简单,平均治的是抖动,加权治的是能力不足,两个病得吃两种药。**
要说明:**这份数据的「真规则」是三条并排的带子,而树桩一次只能切一条线、boost 出来的决策边界是这些线的加权和。**
**如果真规则是斜的,树桩 boosting 会更吃力;这也是它在这份数据上并没有压倒深树的原因。**
w = w * np.exp(-al * ytr * pr) 这一行同时处理了两种情形。
当预测正确时 ytr 乘 pr 等于正一,指数上是负 alpha,权重乘一个小于一的数——变小。
预测错误时乘积是负一,指数上是正 alpha,权重乘一个大于一的数——变大。
不用写 if,一个乘式搞定,这是 AdaBoost 公式的漂亮之处。
w /= w.sum() 不能省。
不归一化的话权重会一路膨胀或缩小到浮点极限,后面 err 就不是「比例」了。
而且 err 必须是比例,因为 alpha 的公式要算 (1 减 err) 除以 err。
0.5 * np.log((1 - err) / err) 里那个 0.5 容易被漏。
漏了它整个算法还是能跑,只是每轮的权重都大一倍,效果大致相当于把轮数减半——不会报错,
只是收敛曲线对不上。
这类「错得不明显」的常数是复现别人结果时最常见的坑。
fit_stump 每轮都要把所有阈值扫一遍,复杂度是 O(样本数 乘 特征数) 每次。
200 轮、120 个点,跑起来是秒级。
真数据上会用「按特征预排序加前缀和」把每轮降到 O(样本数),这就是各种 boosting 库的核心优化。
注意 Ste 是「测试集上的加权和」,用的是每轮的 alpha 和那个树桩,
但权重 w 只在训练集上更新。
把 w 也用到测试集上是错的——那等于看了测试集的标签去调权重。
这个错误在小 Demo 里不容易被发现,在真实项目里就是数据泄漏。
要说明:这份数据的真规则是三条并排的带子,而树桩只能切竖线或横线。
所以 boost 出来的决策边界是这些线的加权和,能逼近阶梯形状。
如果真规则是斜的,树桩 boosting 会更吃力——那不是 boosting 的问题,是基学习器表达力不够。
30 秒实验
- 把翻转率从 8% 降到 0,重跑 200 轮。 测试误差还会先降后升吗?如果一直降,说明过拟合的根源是什么?
- 把基学习器从树桩换成深度 3 的树,重跑。 需要多少轮就能把训练误差压到零?测试误差最低能到多少?
- 把 alpha 的公式改成常数(比如全取 1,相当于等权投票),重跑。 训练误差还能压到零吗?这说明 alpha 在起什么作用?
常见误区
- 「boosting 就是另一种 bagging,都是把很多模型加起来」。 拼法完全不同:bagging 是并行、独立、等权;boosting 是串行、依赖、加权。 更关键的是,它们的方差性质相反:bagging 靠「模型之间独立」来降方差, boosting 的模型之间高度相关,平均降方差那套推导根本不适用。
- 「训练误差到零就说明模型学好了」。 实测到第 150 轮训练误差归零,而那时测试误差已经涨到 21.00%, 比最低点 18.83% 差了 2 个多百分点。 训练误差到零只说明「训练集被记住了」,不代表任何泛化能力。
- 「boosting 不会过拟合,因为它是在优化间隔」。 这个说法在早期文献里有过,但实测不支持。 这里轮数从 20 加到 200,测试误差从 18.83% 涨到 21.33%,就是明确的过拟合。 boosting 在一段时间内对过拟合比较宽容,但绝不等于不会过拟合。
- 「弱学习器的错误率必须小于 0.5,否则算法会崩」。 确实要求小于 0.5,否则 alpha 会变成负数。 但代码里不会崩——它照样跑,只是那一轮「负权重」等于在添乱。 所以实践里要加一个保护:错误率超过 0.5 就提前停,或者把那个树桩的方向取反。
自测三问(点开对答案)
1. 为什么 AdaBoost 里错误率越低的弱学习器,权重 alpha 越大? —— **因为 alpha 是二分之一乘 ln((1 减 err) 除以 err)。** **err 越小,这个比值越大,ln 之后越大。** **直觉上也说得通:一个判断很准的模型,在最终投票里应该更有分量。** **err 等于 0.5 时 alpha 恰好是零——那是乱猜,没有发言权。** 2. 为什么每一轮的「加权错误率」会往上涨,这不矛盾吗? —— **不矛盾,因为每一轮的权重不一样。** **权重被调过之后,整个数据集变「难」了:容易的点权重小,难的权重大。** **所以 0.4473 和 0.2583 不是在同一个题目上算的,不能直接比大小。** **真正该看的是「它是否一直小于 0.5」——只要小于,每个树桩就还在贡献正向信息。** 3. 为什么 boosting 必须早停,而 bagging 不用? —— **因为 boosting 的轮数直接等于模型复杂度。** **每多一轮就多一个树桩参与加权求和,决策边界就多一个折点,轮数越多越贴训练集,** **方差一直在涨。** **bagging 相反:多一棵树只是多一个样本参与平均,方差只会降(虽然会到底),** **所以 B 大一点没有过拟合风险,只是浪费算力。**第 9 章把「拼模型」的两种基本拼法讲完了:并行平均(降方差)和串行加权(降偏差)。 下一章换一个角度问问题:不问「属于哪一类」,而问「属于这一类的概率是多少」。 这个转向会带来一个新东西——输出的不再是硬邦邦的正负一,而是一个 0 到 1 之间的数。
- ☐ (本章完,下一节 10.1 10.1)