📘 第8章 强化学习和分布式人工智能
📄 一句话概括

上一节刚讲完「一个模型不够用」,这一节的答案是:那就养一群,然后投票。但投票不是万能药—— 这一节的重点恰恰是它什么时候不管用。 结论先放这儿: 投票能消掉「各自乱猜」的误差,消不掉「大家都错得一样」的误差。所以一堆模型要真比一个好, 前提是它们错得不重合。

📝 本节和前面几节的关系

这一节是第 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 秒实验

  1. 把 bag 里那句「有放回抽」改成「不放回抽」。 每棵树变成完全一样,投票彻底失效——这一改就证明了「有放回」才是 bagging 的全部来源。
  2. 把 AdaBoost 的轮数从 60 加到 300,看测试集准确率还上不上。 它会先升后降,这就是 boosting 的过拟合:轮数本身就是一个要调的超参。
  3. 把第 3 段的 maxd 从 12 改成 3,重跑。 单棵和投票的差距会缩小——树越浅越稳,方差本来就小,能省下的也就不多。

常见误区

  1. 以为「集成一定比单个好」。Demo 第 2 段就是反例:101 棵一层树投出来,跟 1 棵差 1 个点。 关键不是数量,是多样性。
  2. 以为 bagging 能救「模型太简单」。它只能平均掉随机性, 平均不掉系统性偏差——一堆只会切半平面的模型,投票还是半平面。
  3. 以为 boosting 的轮数越多越好。Demo 里第 40 轮 0.9071,第 60 轮退回 0.9000。 轮数是超参,得用验证集挑。
  4. 以为随机森林「随机」所以不精确。它随机的是「每棵树看哪几个特征」,不是「随便猜」; 单棵变差是代价,换来的是这一群错得不一样。
自测三问(点开对答案) 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 倍。** **相关性越高,投票越像「抄同一份答案」。**

这一节管的是「一个任务、一堆模型」。 下一节换个更难的局面:没有人告诉你答案,只有一个迟到很久的分数。那时候连「训练集」都没有, 只能自己走、自己记。

相关笔记