同一份数据、同一个算法: 深树从 75.50% 提到 81.50%(50 棵),树桩从 63.17% 变成 63.00%——一点没动。 换 20 份训练集重复整个实验:深树的标准差 2.6213 掉到 1.7713, 平均准确率 74.20% 抬到 78.99%;树桩的标准差本来就只有 0.6726。 从 50 棵加到 100 棵,准确率反而掉了 0.50 个百分点(600 个测试点上就是 3 个样本)。
9.1 把树的复杂度按住了,但没解决另一个问题:训练集换一批点,长出来的树就变一个样。 这种「一碰就变」的性质叫方差大,是 bagging 的用武之地。 这一节的结论有点反直觉:bagging 不是万能的,它对某些模型几乎完全无效。 为什么无效,比「它有效」更值得弄明白——因为它直接指向下一节的 boosting。
一、自助采样:为什么要放回
Bagging 的第一步是造出很多份「不一样的训练集」。 可手上只有一份,怎么办?有放回地抽——每次从 n 个样本里随机抽一个、记下来、放回去,抽 n 次。 这样得到的新训练集大小不变,但有些点出现好几次,有些点一次都没出现。 没出现的比例可以算:每次抽不到某个点的概率是 1 减 1 除以 n,抽 n 次都抽不到就是它的 n 次方, 这个数趋近 e 的负一次方,约 0.368。 所以每份自助样本大约包含六成三的不同样本,剩下三成七是重复的。 这个「重复加缺席」就是随机性的来源。 每份样本训出来的模型都不太一样,而这些差异是纯随机的、不带系统偏差的。 把它们平均起来,随机的部分互相抵消,共同的部分留下来。
二、为什么平均能降方差
这件事可以算。 假设 B 个模型的方差都是 sigma 平方,两两之间的相关系数是 rho,那么它们平均之后的方差是: rho 乘 sigma 平方,加上 (1 减 rho) 乘 sigma 平方除以 B。 这个式子说明了三件事。 第一,B 越大方差越小,但只会把后面那一项压到零, 前面那一项压不掉——所以 B 的收益是有天花板的。 第二,真正的瓶颈是 rho:模型之间越像,平均越没用。 第三,如果 rho 接近 1(所有模型完全一样),那平均多少次都没用,方差一点不降。 现在把实测的数代进去。 深树单个的标准差是 2.6213,bag 50 之后是 1.7713,比值 0.6757,平方是 0.4566。 代入 B 等于 50:rho 乘 49 除以 50 加 1 除以 50 等于 0.4566,解出 rho 约等于 0.45。 也就是说这些树两两之间的相关系数只有 0.45 左右——它们确实不一样,但也不算独立。 这个 0.45 解释了为什么「随机森林」还要再加一招。 因为光是「自助采样」只能把 rho 压到 0.45; 如果在每次切分时还随机限制候选特征,树之间会差得更多,rho 更低,方差的下降空间就更大。 所以随机森林不是「bagging 加树」,是「bagging 加特征随机」——加的那一半才是关键。
三、实测:深树有效,树桩无效
| 基学习器 | 单个 | 10 棵 | 50 棵 | 100 棵 |
|---|---|---|---|---|
| 深树(15 叶) | 75.50% | 77.50% | 81.50% | 81.00% |
| 树桩(1 次切分) | 63.17% | 63.00% | 63.17% | 63.00% |
| 深树那一行涨了 6 个百分点,树桩那一行一动不动。 | ||||
| 同一个算法、同一份数据,差别只在基学习器。 | ||||
| 再看换 20 份训练集的波动: |
| 做法 | 平均准确率 | 标准差 | 最小 | 最大 |
|---|---|---|---|---|
| 深树,单个 | 74.20% | 2.6213 | 69.33% | 78.00% |
| 深树,bag 50 | 78.99% | 1.7713 | 75.33% | 81.83% |
| 树桩,单个 | 62.71% | 0.6726 | 61.00% | 63.33% |
| 深树的标准差 2.6213,bag 之后 1.7713,抖动量减了三分之一多;平均值也抬了 4.79 个百分点。 | ||||
| 树桩的标准差只有 0.6726——它一开始就很稳,本来就没有多少方差可以减。 | ||||
| 这就是「bagging 要求基学习器不稳定」这句话的实测含义。 | ||||
| 稳定就是「换个训练集结果差不多」,就是方差小,就是平均不解决问题。 | ||||
| 所以拿到一个新模型,先问一句「它稳定吗」,比问「bagging 能不能提分」有用得多。 |
四、bagging 的边界
第一个边界:B 加到一定程度就没用了,甚至会小幅回落。 实测 50 棵 81.50%、100 棵 81.00%,少了 0.50 个百分点。 在 600 个测试点上,0.5 个百分点就是 3 个样本——这是抽样噪声,不该当成趋势。 但它至少说明「更多不更好」,B 过了几十就没有明显收益了。 第二个边界:bagging 不降偏差。 树再多,它们犯的错也是同一类错——都来自「用轴对齐的方盒子去逼近」这个结构性限制。 平均只能把这些错误互相抵消掉一部分随机的部分,系统性偏掉的那部分一分都消不掉。 第三个边界:单看训练误差,bagging 一点没改善。 9.3 那张对照表里,「单个深树」和「深树 bag 50」的训练误差都是 5.00%。 因为每棵树都还是长到底的,都还是把训练集记住了; 只是它们记的方式不一样,投票之后错的地方被抵消了一部分。 这三条合起来,bagging 的定位就很清楚了: 它是一个「把好模型变稳」的工具,不是一个「把差模型变好」的工具。 要救差模型,得靠 boosting。
五、动手 Demo:Bagging:深树 75.50% 提到 81.50%,树桩 63.17% 反而掉到 63.00%
用 9.1 那份数据(120 个训练点、600 个测试点、8% 标签噪声)。 做两个基学习器:一个是长到底的深树(15 个叶子),一个是只切一刀的树桩。 分别做 bagging,B 取 10、50、100,记录测试准确率。 然后换 20 份不同的训练集,把整个实验重复 20 次, 看准确率的平均值和标准差——这一步是为了看清 bagging 到底在减什么。
def bag(X, y, B, depth, seed=7):
rg = np.random.default_rng(seed)
n = len(y)
S = np.zeros(len(Xte))
for _ in range(B):
idx = rg.integers(0, n, n) # 有放回抽 n 个
nd = grow(X[idx], y[idx], max_depth=depth)
S += predict(nd, Xte) # 投票累加
return np.where(S >= 0, 1.0, -1.0)
print(bag(Xtr, ytr, 50, depth=12)) # 深树
print(bag(Xtr, ytr, 50, depth=1)) # 树桩
实跑输出:
--------------------------------------------------------------------
Bagging:深树 75.50% 提到 81.50%,树桩 63.17% 反而掉到 63.00%
--------------------------------------------------------------------
数据 和 D031 同一份训练集(120 个点)与测试集(600 个点)
做法 每次有放回抽 120 个点训一棵树,重复 B 次,把 B 个预测求和取符号
两个基学习器 一个是长到底的深树(15 个叶子),一个只切一刀的树桩
基学习器 单个 B 等于 10 B 等于 50 B 等于 100
-------------------------------------------------------------
深树(15 叶) 75.50% 77.50% 81.50% 81.00%
树桩(1 次切分) 63.17% 63.00% 63.17% 63.00%
**深树那一行涨了 6.00 个百分点(75.50% 到 81.50%)。**
**树桩那一行一动不动,还略微掉了(63.17% 到 63.00%)。**
**同一个算法(都是 bagging)、同一份数据,为什么一个有用一个没用?**
**因为 bagging 治的是「方差」:每次抽样不同,训出来的模型就在抖。抖得厉害,平均一下就稳了;根本不抖,平均多少次还是那个数。**
验证这个说法:换 20 份训练集,各自训练、各自评估,看准确率的波动。
做法 平均准确率 标准差 最小 最大
---------------------------------------------------
深树,单个 74.20% 2.6213 69.33% 78.00%
深树,bag 50 78.99% 1.7713 75.33% 81.83%
树桩,单个 62.71% 0.6726 61.00% 63.33%
**深树的标准差是 2.6213,bag 50 之后掉到 1.7713——抖动量减了一半以上,平均值也从 74.20% 抬到 78.99%。**
**树桩的标准差只有 0.6726 个百分点,一开始就很稳,所以没有可减的方差。**
**这就是「bagging 要求基学习器不稳定」这句话的实测含义。**
**稳定 = 换个训练集结果差不多 = 方差小 = 平均不解决问题。**
最后看 B 继续加有没有用:
深树,10 棵 77.50%
深树,50 棵 81.50%
深树,100 棵 81.00%
**从 50 棵到 100 棵,准确率不升反降了 0.50 个百分点。**
**这不是 bug:bagging 只能消掉「抽样带来的抖动」,消不掉「这个模型本身就偏」的部分。**
**树再多,它们犯的错也是同一类错——都来自「用方盒子去近似斜边界」这个结构性限制。**
**要减小偏差,得换模型或者换训练方式,那是下一节 boosting 的事。**
要说明:**100 棵比 50 棵低这 0.50 个百分点,在 600 个测试点上就是 3 个样本的差别,属于抽样噪声,不该当成趋势。**
rg.integers(0, n, n) 就是「有放回抽 n 个」的全部代码。
它从 0 到 n 减 1 里独立均匀地抽 n 个下标,抽到重复的是正常的。
不要写成 rg.permutation(n)——那是无放回,得到的是原训练集的一个重排,一点随机性都没有。
S += predict(nd, Xte) 是「硬投票」:把每个模型的加减一累加,最后看和的符号。
这等价于「多数票」,因为加减一的和的符号就是多数方向。
用求和而不是「数票数」,好处是代码短;如果要软投票(把各类概率平均),得让基学习器输出概率。
注意两次调用 bag 传的 depth 不同,这就是「同一个算法、不同基学习器」的全部差别。
结果差了 18 个百分点,可见「bagging 有没有用」几乎完全由基学习器决定。
20 份训练集那一段,每次都要重新 make(120, 100 + k) 造数据,而且要给 bagging 传不同的种子。
如果两次实验共用同一批自助样本,
测量出来的「方差」会偏小——因为你把「抽样随机性」这部分人为固定住了。
要说明:100 棵比 50 棵低 0.50 个百分点,这个差别在统计上不可靠。
600 个测试点上 0.5 个百分点是 3 个样本,随便换个种子就会变。
所以我把它写成「没有明显收益」,而不是「变差了」。
30 秒实验
- 把训练集加到 500 个点,重跑。深树的方差会变小吗?bagging 的收益会变大还是变小?
- 把 B 从 100 加到 500,只跑深树。准确率还能涨吗?耗时增加了多少?这个交换划算吗?
- 改成「每次切分时随机只用 1 个特征」(这就是随机森林的做法),重跑深树。 支持向量的标准差会降到多少?反推出来的 rho 比 0.45 小了还是大了?
常见误区
- 「bagging 就是多训几个模型取平均,所以任何模型都能提分」。 实测反例就在这一节:树桩从 63.17% 变成 63.00%,完全没动。 bagging 只能减方差,减不了偏差。基学习器本身很稳,就没有方差可减。
- 「B 越大越好」。 方差公式里 B 只在 (1 减 rho) 那一项的分母上,所以 B 趋于无穷时, 方差趋于 rho 乘 sigma 平方,到顶了。 实测 50 棵和 100 棵的差别就在噪声范围内。 多训只会多花时间。
- 「有放回抽样会丢掉三成多数据,所以效果会变差」。 丢数据确实会变差,但「丢」这件事本身带来了随机性,而随机性正是要买的东西。 每棵树只看六成多的数据,所以每棵都差一点;可它们差的地方不一样,平均之后就补回来了。 这是一个用「单个变差」换「整体变稳」的交易。
- 「bagging 和随机森林是一回事」。 随机森林是 bagging 加「特征随机」。 只做自助采样,树之间的相关系数还有 0.45; 加了特征随机之后,树之间差得更多,rho 更低,方差能压得更低。 那一半才是随机森林比纯 bagging 强的原因。
自测三问(点开对答案)
1. 为什么 bagging 对深树有效,对树桩几乎无效? —— **因为一个是高方差、一个是低方差。** **深树换个训练集就换个样子(标准差 2.6213),平均起来能把这些随机差异抵消掉。** **树桩换个训练集几乎不变(标准差 0.6726),平均多少次还是那个数。** **bagging 减的是方差,不是偏差——这就是判据。** 2. 为什么 B 从 50 加到 100 收益就几乎没有了? —— **因为平均后剩下的方差是「rho 乘 sigma 平方 加 (1 减 rho) 乘 sigma 平方除以 B」。** **B 只出现在第二项的分母上,B 足够大时这一项趋于零,方差停在 rho 乘 sigma 平方。** **实测反推出来的 rho 约 0.45,所以方差最多只能降到原来的 45% 左右,已经到底了。** 3. 如果要让 bagging 效果更好,应该改哪里? —— **降低树之间的相关系数 rho。** **因为方差的下限就是 rho 乘 sigma 平方,rho 不降,B 再大也没用。** **做法就是随机森林那一招:每次切分时只从随机挑出的一部分特征里选。** **树被迫用不同的特征,自然就长得不一样,rho 就下来了。** **另一条路是换一个偏差更小的基学习器——但那就不是 bagging 的职责了。**bagging 的极限已经摸到了:它救不了树桩。 下一节换个思路——不是「各自独立地学、然后平均」,而是「一个接一个地学, 每个专门补前一个的错」。 这种「串行」的拼法能把一个只会切一刀的模型,拼到和深树一样准。
- ☐ (本节完,下一节 9.3 9.3)