Series · ML Math Derivations · Chapter 11

机器学习数学推导(十一):集成学习

推导一群平庸分类器为何能压过单个高手。涵盖偏差-方差分解、Bagging 与随机森林的方差缩减、AdaBoost 的指数损失、以及 GBDT 在函数空间中的梯度下降。

为什么"三个臭皮匠顶个诸葛亮"在机器学习里几乎是字面意义上成立的?答案不浪漫,但精确:平均能压方差,串行重加权能压偏差,再加一点随机化打破相关性——否则前两件事都白干。本文把这条线索的数学推到底:从偏差-方差分解出发,到 Bagging/随机森林如何利用 Bootstrap,再到 AdaBoost 如何被解读为指数损失下的前向分步加性建模,最后是 GBDT 把所有这些抽象成函数空间里的梯度下降。

读完之后,你应该能拿到任何一个集成方法都能立刻判断:它在压什么、为什么有效、什么时候会翻车

你将学到

  • 为什么 $T$ 个不相关模型的平均能让方差减为 $1/T$;如果它们相关,会发生什么。
  • Bagging 和随机森林是怎么把树之间的相关性砍下去的。
  • AdaBoost 不是启发式:它就是指数损失下的前向分步算法。
  • GBDT 如何在函数空间做梯度下降。
  • Bagging、Boosting、Stacking 各自适合什么场景。

预备知识

  • 偏差-方差权衡(本系列第 1-2 篇)。
  • 决策树基础:分裂、纯度。
  • 梯度下降。
  • 概率论:随机变量和的期望与方差。

1. 集成为什么有效

1.1 那个核心方差恒等式

考虑一个回归问题,基学习器在训练集 $\mathcal{D}_t$ 上得到 $h_t(\mathbf{x})$。最朴素的集成就是平均:

$$ H(\mathbf{x}) = \frac{1}{T}\sum_{t=1}^T h_t(\mathbf{x}). $$

由于训练集是随机的,每个 $h_t(\mathbf{x})$ 也是随机变量。设它们均值都是 $\mu$、方差都是 $\sigma^2$,两两相关系数为 $\rho$。简单算一下:

$$ \mathbb{E}[H(\mathbf{x})] = \mu, \qquad \operatorname{Var}[H(\mathbf{x})] = \rho\,\sigma^2 + \frac{1-\rho}{T}\,\sigma^2. $$

集成学习的全部理由就压缩在这一行公式里,值得逐字读:

  • 偏差不变:$\mathbb{E}[H] = \mu$。平均改不掉基学习器的系统性错误。如果所有树都欠拟合,集成照样欠拟合。
  • 方差有两块:一块是 $\rho\sigma^2$,加再多模型也消不掉;另一块 $\sigma^2/T$ 随 $T \to \infty$ 衰减——但只有当 $\rho = 0$ 时这一块才足够大
  • 相关性是大敌。哪怕 $T = \infty$,方差下界仍是 $\rho\sigma^2$。这就是随机森林为什么不仅仅做样本随机化,还要做特征随机化——后者是直接打 $\rho$ 的。

所以每一个集成方法回答的其实是同一个问题:怎么让基学习器之间足够不相关(小 $\rho$),同时又不至于太弱(大偏差)?

1.2 偏差-方差分解

平方损失下,任何预测器的泛化误差都可以分解为:

$$ \mathbb{E}[(y - \hat f(\mathbf{x}))^2] = \underbrace{(\mathbb{E}[\hat f] - f)^2}_{\text{偏差}^2}

  • \underbrace{\mathbb{E}[(\hat f - \mathbb{E}[\hat f])^2]}_{\text{方差}}
  • \underbrace{\sigma_\epsilon^2}_{\text{不可约噪声}}. $$

复杂模型(深树、大网络)偏差小但方差大;简单模型(决策桩、线性回归)方差小但偏差大。两类集成方法各打一头:

  • Bagging / 随机森林:保留低偏差高方差的基学习器,用平均压方差
  • Boosting:从高偏差低方差的基学习器出发,逐步加容量压偏差

整个集成学习的分类法,就这两句话。

1.3 一群平庸投票者的力量

二分类场景下,$T$ 个相互独立的分类器,每个错误率 $\epsilon < 1/2$,多数投票出错的概率服从二项分布:

$$ P_{\text{ensemble}} = \sum_{k > T/2} \binom{T}{k}\,\epsilon^k(1-\epsilon)^{T-k}. $$

代入 $T = 21$、$\epsilon = 0.30$,得到大约 $0.026$。30% 的错误率很平庸,2.6% 的集成错误率已经很出色。唯一的前提是"独立"——又一次指向那个核心的去相关问题。


2. Bagging 与随机森林

2.1 Bagging:并行配方

Bagging:并行的弱学习器在 Bootstrap 样本上独立训练,再做平均

Bagging(Breiman, 1996)是把 §1.1 那个方差恒等式直接落地最干净的方式。

算法

  1. Bootstrap:从大小为 $N$ 的训练集 $\mathcal{D}$ 中有放回抽取 $T$ 个子集 $\mathcal{D}_1,\dots,\mathcal{D}_T$,每个大小都是 $N$。
  2. 训练:在 $\mathcal{D}_t$ 上独立训练 $h_t$。树通常长得很深——低偏差、高方差,正是要靠平均压下去的形态。
  3. 聚合:回归取平均 $H(\mathbf{x}) = \tfrac{1}{T}\sum_t h_t(\mathbf{x})$;分类多数投票。

Bootstrap 大约会漏掉 36.8% 的样本。某个样本在 $N$ 次有放回抽样中没被抽到的概率:

$$ \left(1 - \tfrac{1}{N}\right)^N \xrightarrow{N \to \infty} e^{-1} \approx 0.368. $$

这些没被采到的点,就是树 $t$ 的袋外样本(OOB)

OOB 误差是免费的验证集。对训练点 $(\mathbf{x}_i, y_i)$,只用没见过它的那些树来预测:

$$ \widehat{\text{Err}}_{\text{OOB}} = \frac{1}{N}\sum_{i=1}^N L\!\left(y_i,\; \frac{1}{|\mathcal{S}_i|}\sum_{t \in \mathcal{S}_i} h_t(\mathbf{x}_i)\right),\qquad \mathcal{S}_i = \{t : (\mathbf{x}_i, y_i) \notin \mathcal{D}_t\}. $$

这是泛化误差几乎无偏的估计,作为训练副产品免费拿到。不需要专门划验证集,也不需要跑交叉验证循环

2.2 随机森林:把树之间去相关

单棵深树 vs 随机森林:后者把决策边界平滑了

光做 Bagging 还不够:Bootstrap 子集之间高度重合,每棵树最后都会在同一批主导特征上做相同的分裂。预测彼此相关,$\rho$ 居高不下,§1.1 里那个方差缩减就卡住了。

随机森林(Breiman, 2001)加了第二层随机性:每次分裂节点时,从 $d$ 个特征中随机抽 $m$ 个,只在这 $m$ 个里找最优分裂。常用默认值:

  • 分类:$m = \lfloor\sqrt{d}\rfloor$。
  • 回归:$m = \lfloor d/3 \rfloor$。

强迫每棵树在不同节点用不同特征,就打破了"主导特征陷阱",把 $\rho$ 压下来。上图把这个效果画得很直白:单棵深度 12 的树把空间切成大块矩形、过拟合掉了噪声;80 棵 $m=1$ 的随机树平均之后,边界平滑得多。

泛化误差上界(Breiman, 2001)。设 $s$ 是各树的平均间隔(margin),$\bar\rho$ 是树之间的平均相关系数:

$$ \text{泛化误差} \;\le\; \frac{\bar\rho\,(1 - s^2)}{s^2}. $$

这其实就是把 §1.1 的方差恒等式重新打扮了一下。调一个森林只有两个旋钮

  1. 让每棵树更强(提高 $s$):长更深、每次看更多特征、用更多样本。
  2. 让树之间更不相关(降低 $\bar\rho$):减小 $m$、混合不同深度、加更多树。

两个旋钮互相牵制,所以 $m$ 才需要调,不能写死。

2.3 特征重要性

两种主流算法:

  • 不纯度均值减少(MDI):把所有用特征 $j$ 分裂的节点的 $\Delta\text{Gini}$ 累加,再对树做平均。便宜,但偏向高基数特征
  • 置换重要性:拿 OOB 样本,把第 $j$ 列打乱,看 OOB 准确率掉多少。慢,但无偏,对相关特征的处理也更合理。

当两个特征携带相似信号时,MDI 会把重要性拆给两个特征;置换重要性会给两个特征同样的下降——这通常才是你做可解释性时想要的结果。


3. Bagging vs Boosting:偏差-方差视角

Bagging 在保留均值(偏差)的同时把方差带压窄

上图把同一套拟合流程在 120 个独立采样的训练集上跑了一遍,画出预测分布。看那个 $\pm 1$ 标准差的灰色带:

  • 左边单棵树:均值贴近真实函数(低偏差),但每条曲线乱抖(高方差)。误差带很宽。
  • 右边 25 棵树的 Bagging:均值差不多,但带宽显著收窄。方差数值降了大约 $T$ 倍,跟 §1.1 的预测一致。

Bagging 对系统性偏差无能为力——基学习器要是真的太弱,再怎么平均也救不回来。这件事得交给 Boosting。


4. Boosting:串行压偏差

Boosting:串行训练的加权弱学习器(AdaBoost 视角)

Boosting 把局面整个翻转:不是并行训练 $T$ 个再平均,而是串行训练,每一轮专攻前一轮没搞定的样本。每个基学习器都故意留得很弱(如深度 1 的桩),高偏差、低方差。整个序列把偏差一步步压下去。

4.1 AdaBoost 算法

二分类,$y_i \in \{-1, +1\}$。初始化权重 $w_1(i) = 1/N$。对 $t = 1,\dots,T$:

  1. 在加权数据上训练弱学习器 $h_t$。
  2. 计算加权错误率 $\epsilon_t = \sum_i w_t(i)\,\mathbf{1}[h_t(\mathbf{x}_i) \neq y_i]$。
  3. 计算学习器权重 $\alpha_t = \tfrac{1}{2}\ln\tfrac{1-\epsilon_t}{\epsilon_t}$。
  4. 更新样本权重 $w_{t+1}(i) = w_t(i)\exp(-\alpha_t y_i h_t(\mathbf{x}_i)) / Z_t$,$Z_t$ 是归一化常数。

最终模型:$H(\mathbf{x}) = \operatorname{sign}\bigl(\sum_t \alpha_t h_t(\mathbf{x})\bigr)$。

公式里有三处值得反复琢磨:

  • $\alpha_t$ 是对数几率。$\epsilon_t = 0.1$ 的学习器拿 $\alpha_t \approx 1.10$,$\epsilon_t = 0.49$ 的只拿 $\alpha_t \approx 0.02$。好学习器的票更重
  • $\alpha_t > 0 \iff \epsilon_t < 1/2$。比随机还差的学习器会拿到负权重——AdaBoost 直接把它的预测翻过来用,没有浪费。
  • 乘性更新。预测对了的 $y_i h_t = +1$ 的样本权重乘 $e^{-\alpha_t}$ 缩小,错了的乘 $e^{+\alpha_t}$ 放大。下一轮想忽略困难样本都不可能

4.2 训练误差的指数上界

短短几行就能证明训练误差被归一化常数的乘积夹住:

$$ \frac{1}{N}\sum_{i=1}^N \mathbf{1}[H(\mathbf{x}_i) \neq y_i] \;\le\; \prod_{t=1}^T Z_t. $$

如果每一轮都满足弱学习条件 $\epsilon_t \le \tfrac{1}{2} - \gamma_t$(每个学习器至少比随机好 $\gamma_t$),则 $Z_t \le \sqrt{1 - 4\gamma_t^2}$,于是:

$$ \text{训练误差}(H) \;\le\; \prod_{t=1}^T \sqrt{1 - 4\gamma_t^2} \;\le\; \exp\!\left(-2\sum_{t=1}^T \gamma_t^2\right). $$

训练误差随 $T$ 指数衰减。下图在一个故意构造了几个困难边界样本的合成问题上把这件事画出来:

AdaBoost 把注意力转移到困难样本上;训练误差近乎指数衰减

左边热图是每个样本的权重随轮数演化:绝大多数样本褪到接近 0,边界上的困难样本越烧越亮——算法字面意义上把全部注意力都倒在前面的弱学习器搞不定的点上。右边把累积分类器的训练误差和理论上界 $\exp(-2\sum_t \gamma_t^2)$ 画在一起,上界宽松但始终在真实误差之上。

但这张图也暴露了 AdaBoost 的命门:当样本是真的标错时,同样的机制会把全部容量倒在噪声上。GBDT 配上鲁棒损失(如 Huber)就温和得多。

4.3 为什么是指数权重?前向分步视角

AdaBoost 看起来像个聪明的启发式,直到你意识到它等价于指数损失下的前向分步加性建模。模型是:

$$ F(\mathbf{x}) = \sum_{t=1}^T \alpha_t\, h_t(\mathbf{x}), $$

损失是 $\mathcal{L}(F) = \sum_i \exp(-y_i F(\mathbf{x}_i))$。我们不联合优化 $T$ 项,而是固定住 $F_{t-1}$,第 $t$ 轮解:

$$ (\alpha_t, h_t) \;=\; \arg\min_{\alpha, h}\sum_{i=1}^N \exp\bigl(-y_i\,(F_{t-1}(\mathbf{x}_i) + \alpha\, h(\mathbf{x}_i))\bigr). $$

定义 $w_t(i) = \exp(-y_i F_{t-1}(\mathbf{x}_i))$,把 $w_t(i)$ 提到新指数外面,问题变成:

$$ \min_{\alpha, h}\sum_{i=1}^N w_t(i)\exp(-\alpha\, y_i\, h(\mathbf{x}_i)). $$

先解最优的 $h_t$(它最小化加权错误率),再解 $\alpha_t$(一维微积分),逐字逐句就是 AdaBoost 的更新公式。一切都不是启发式,而是函数空间里的坐标下降,每轮加一个新基函数。


5. 梯度提升决策树(GBDT)

5.1 把 Boosting 看作函数空间的梯度下降

指数损失太脆弱。Friedman(2001)的关键洞察是:前向分步的思路对任何可微损失都成立——只要把它重新表述为梯度下降。

设 $F(\mathbf{x}) = \sum_{t=0}^T h_t(\mathbf{x})$ 是加性模型,$\mathcal{L}(F) = \sum_i L(y_i, F(\mathbf{x}_i))$ 是损失。第 $t$ 轮我们想找一个 $h_t$ 让 $\mathcal{L}$ 下降。在当前迭代点 $F_{t-1}$ 处的负函数梯度,落在训练点上是个 $N$ 维向量:

$$ r_{ti} \;=\; -\!\left[\frac{\partial L(y_i, F)}{\partial F}\right]_{F = F_{t-1}(\mathbf{x}_i)},\qquad i = 1,\dots,N. $$

这就是伪残差。一棵回归树拟合 $\{(\mathbf{x}_i, r_{ti})\}$,相当于在函数空间里给最速下降方向做一个有限维的近似。

算法(Friedman, 2001)

  1. 初始化 $F_0(\mathbf{x}) = \arg\min_c \sum_i L(y_i, c)$(平方损失下就是均值)。
  2. 对 $t = 1,\dots,T$:
    • 计算伪残差 $r_{ti}$。
    • 用回归树 $h_t$ 拟合 $\{(\mathbf{x}_i, r_{ti})\}$。
    • 线搜索步长 $\rho_t = \arg\min_\rho \sum_i L(y_i, F_{t-1}(\mathbf{x}_i) + \rho\, h_t(\mathbf{x}_i))$。
    • 更新 $F_t = F_{t-1} + \eta\,\rho_t\, h_t$,$\eta \in (0, 1]$ 是学习率。

5.2 常用损失对应的伪残差

损失$L(y, F)$伪残差 $r_i$备注
平方损失(回归)$\tfrac{1}{2}(y - F)^2$$y_i - F_{t-1}(\mathbf{x}_i)$退化为拟合残差。
绝对值损失(鲁棒)$\lvert y - F \rvert$$\operatorname{sign}(y_i - F_{t-1}(\mathbf{x}_i))$对离群点鲁棒,但只看符号、忽略幅度。
Huber 损失分段截断残差两全其美:小残差平滑,大残差鲁棒。
Logistic 损失(二分类)$\log(1 + e^{-yF})$$y_i / (1 + e^{y_i F_{t-1}(\mathbf{x}_i)})$$F$ 是对数几率,输出 $\sigma(F)$。

平方损失下整个算法退化成"拟合残差",这就是 Boosting 最初的直觉。其他损失下,只有"函数空间梯度下降"这个视角能干净地解释发生了什么

5.3 正则化:真正起作用的三个旋钮

GBDT 不防一手就会过拟合到飞起。标准防御措施:

  • 学习率收缩:$F_t = F_{t-1} + \eta\,h_t$,小 $\eta$(典型值 $0.01$ 到 $0.1$)。$\eta$ 越小需要越多树,但泛化更好——跟 SGD 小步长避免步长过大同理。小 $\eta$ 永远要配大 $T$
  • 随机梯度提升:每轮在随机子样本(如 50%-80% 行)上拟合 $h_t$。相当于函数空间目标的 SGD,去相关、又便宜。
  • 树的复杂度上限:限深度(典型 3-6)、限叶子最少样本数、限叶子总数。让每棵树保持"弱"——这是 Boosting 的全部前提

XGBoost(下一篇)在此基础上加了显式的叶权重 $L_2$ 罚项和叶子数罚项,并把叶子最优解写成闭式。但先把朴素 GBDT 吃透,再读 XGBoost 才会顺。


6. Stacking:用元学习器组合异构模型

Stacking:元学习器消费基学习器的输出

Bagging 和 Boosting 用的是同一族基学习器。Stacking 用的是不同族:

  1. 第一层:训练 $K$ 个基模型(如逻辑回归、随机森林、GBDT、k-NN)。为了避免泄漏,用交叉验证生成袋外预测:每个训练样本由没见过它的模型给出预测。
  2. 第二层:把 $K$ 个袋外预测当作新特征向量 $\mathbf{z} \in \mathbb{R}^K$,训练元学习器 $g(\mathbf{z}) \to \hat y$。

元学习器通常很简单(逻辑回归或浅层 GBDT),因为脏活累活已经被基学习器干完了。真正麻烦的是交叉验证管道——只要有任何一个基预测是 in-sample 的,元学习器就会盲目相信它,灾难性过拟合。

什么时候 Stacking 真的赚?当基学习器犯的错误类型不同。把深度模型、树模型、线性模型堆在一起,常常能打过其中任何单一模型,因为它们的错误是去相关的。


7. 集成规模:到底多少棵就够了

测试误差随集成规模的变化:Bagging、随机森林、AdaBoost

上图把三个集成方法在同一个问题上跑出来,画测试误差随 $T$ 的曲线。几个值得记住的规律:

  • 三种方法都在前十几棵树之内就把单棵深树的基线甩掉了。
  • Bagging 和随机森林的曲线优雅地趋平:树越多越好,最多就是不再变好。这条平台是 §1.1 那个方差下界 $\rho\sigma^2$ 的直接体现。
  • AdaBoost 收敛最快,但有些场景下 $T$ 太大反而过拟合噪声、测试误差会抬头。要警惕。
  • 随机森林的平台比 Bagging 更低,因为特征随机化把 $\bar\rho$ 进一步压下去了。

实战提示:Bagging/RF 的 $T$ 顶着算力上限选;Boosting 的 $T$ 用验证集 early stopping 选。


8. 参考实现

下面这些代码故意写得短、零依赖。性能比不上 scikit-learn,但每一步都直接对应上面的公式

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import numpy as np


class DecisionStump:
    """单分裂决策树,AdaBoost 的弱学习器。"""

    def __init__(self):
        self.feature_idx = None
        self.threshold = None
        self.left_value = None
        self.right_value = None

    def fit(self, X, y, weights):
        N, d = X.shape
        best_error = float("inf")
        for j in range(d):
            for threshold in np.unique(X[:, j]):
                left = X[:, j] <= threshold
                right = ~left
                if not left.any() or not right.any():
                    continue
                # 两侧分别加权多数投票
                lv = np.sign(np.sum(weights[left] * y[left])) or 1.0
                rv = np.sign(np.sum(weights[right] * y[right])) or 1.0
                pred = np.where(left, lv, rv)
                err = np.sum(weights[pred != y])
                if err < best_error:
                    best_error = err
                    self.feature_idx, self.threshold = j, threshold
                    self.left_value, self.right_value = lv, rv
        return self

    def predict(self, X):
        left = X[:, self.feature_idx] <= self.threshold
        return np.where(left, self.left_value, self.right_value)


class AdaBoost:
    """决策桩版 AdaBoost。标签取值 {-1, +1}。"""

    def __init__(self, n_estimators=50):
        self.n_estimators = n_estimators
        self.estimators, self.alphas = [], []

    def fit(self, X, y):
        N = X.shape[0]
        w = np.ones(N) / N
        for _ in range(self.n_estimators):
            stump = DecisionStump().fit(X, y, w)
            pred = stump.predict(X)
            eps = np.clip(np.sum(w[pred != y]), 1e-10, 1 - 1e-10)
            alpha = 0.5 * np.log((1 - eps) / eps)
            w = w * np.exp(-alpha * y * pred)
            w /= w.sum()
            self.estimators.append(stump)
            self.alphas.append(alpha)
        return self

    def predict(self, X):
        agg = sum(a * h.predict(X) for a, h in zip(self.alphas, self.estimators))
        return np.sign(agg)


class GradientBoostingRegressor:
    """平方损失下的 GBDT。"""

    def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):
        self.n_estimators = n_estimators
        self.learning_rate = learning_rate
        self.max_depth = max_depth
        self.trees, self.init_prediction = [], None

    def fit(self, X, y):
        self.init_prediction = np.mean(y)
        F = np.full(len(y), self.init_prediction)
        for _ in range(self.n_estimators):
            residuals = y - F                       # 负梯度
            tree = self._build_tree(X, residuals, depth=0)
            self.trees.append(tree)
            F += self.learning_rate * self._predict_tree(tree, X)
        return self

    def _build_tree(self, X, y, depth):
        if depth >= self.max_depth or len(y) < 2:
            return {"value": np.mean(y)}
        best = (None, None, np.inf)
        for j in range(X.shape[1]):
            for t in np.unique(X[:, j]):
                left = X[:, j] <= t
                if left.sum() < 1 or (~left).sum() < 1:
                    continue
                mse = np.var(y[left]) * left.sum() + np.var(y[~left]) * (~left).sum()
                if mse < best[2]:
                    best = ((j, t, left), best[1], mse)
        if best[0] is None:
            return {"value": np.mean(y)}
        j, t, left = best[0]
        return {
            "feature": j, "threshold": t,
            "left": self._build_tree(X[left], y[left], depth + 1),
            "right": self._build_tree(X[~left], y[~left], depth + 1),
        }

    def _predict_tree(self, tree, X):
        if "value" in tree:
            return np.full(len(X), tree["value"])
        left = X[:, tree["feature"]] <= tree["threshold"]
        out = np.empty(len(X))
        out[left] = self._predict_tree(tree["left"], X[left])
        out[~left] = self._predict_tree(tree["right"], X[~left])
        return out

    def predict(self, X):
        F = np.full(len(X), self.init_prediction)
        for tree in self.trees:
            F += self.learning_rate * self._predict_tree(tree, X)
        return F

9. Q&A

Q1:Bagging 和 Boosting 哪个"更强"? 问错了。Bagging 压方差,Boosting 压偏差。基学习器过拟合(小数据集上的深树),就 Bag;基学习器欠拟合(决策桩),就 Boost。两者站在偏差-方差天平的两端。

Q2:AdaBoost 为什么偏偏是指数权重更新? 那是指数损失下前向分步算法的闭式解,没什么神秘的。换损失就换更新规则:logistic 损失给出 LogitBoost,平方损失给出残差拟合。

Q3:为什么 GBDT 喜欢小学习率? 和 SGD 喜欢小步长是一个原因:每棵树都是真实下降方向的噪声估计。$\eta$ 小,意味着不把任何一棵树看得太重,单棵树犯的错会被稀释。代价是更多树,收益是更好泛化。$\eta = 0.05$ + $T = 1000$ 几乎总是赢 $\eta = 0.5$ + $T = 100$

Q4:随机森林的 $m$ 怎么选? 分类 $\sqrt{d}$、回归 $d/3$ 是非常好的起点。如果 OOB 误差高、且各树看起来太像,减小 $m$ 注入多样性。如果 OOB 误差高、且单棵树看起来太弱,增大 $m$

Q5:Boosting 能并行化吗? 外层循环($t = 1, 2, \dots$)不行——每轮依赖上一轮。内层循环(树内找最优分裂)可以爆并行:按特征并行、按样本切片并行。XGBoost 和 LightGBM 把这块榨得很彻底。

Q6:集成能防止过拟合吗?

  • Bagging/RF:基本能——加树几乎没坏处,因为只在压方差。
  • Boosting:不能——可以训练误差降到 0、测试误差却涨。对策:验证集 early stopping、学习率收缩、子采样、树复杂度上限。

Q7:为什么 GBDT 是 XGBoost / LightGBM / CatBoost 的基础? 因为函数空间梯度下降的视角是可推广的:任何可微损失、任何树学习器、任何二阶技巧都能套进来。XGBoost 加了牛顿步近似和显式正则;LightGBM 加了直方图分桶和 leaf-wise 生长;CatBoost 加了针对类别特征的 ordered boosting。三者都是 GBDT 加工程


10. 练习题

练习 1:偏差-方差心算

3 个独立的回归模型,每个 $\text{偏差}^2 = 4$、$\text{方差} = 9$。计算(i)单个模型和(ii)简单平均集成的期望 MSE。忽略噪声。

解答:单个 $4 + 9 = 13$。集成偏差不变 $4$、方差降至 $9/3 = 3$,合计 $7$。降幅约 $46\%$。

练习 2:多数投票错误率

21 个独立二分类器,每个错误率 $\epsilon = 0.30$,求多数投票错误率。

解答:多数投票出错当且仅当 $> 10$ 个出错:

$$ P_{\text{ensemble}} = \sum_{k=11}^{21} \binom{21}{k}(0.3)^k(0.7)^{21-k} \approx 0.026. $$

30% 的单个错误率被压到 2.6%。

练习 3:手算 AdaBoost 权重更新

第 $t$ 轮训练后 $h_t$ 的错误率 $\epsilon_t = 0.2$。样本 $i$ 被正确分类,当前权重 $w_t(i) = 0.05$。求(i)学习器权重 $\alpha_t$,(ii)归一化前的下一轮权重 $w_{t+1}(i)$。

解答:$\alpha_t = \tfrac{1}{2}\ln(0.8/0.2) = \tfrac{1}{2}\ln 4 \approx 0.693$。正确分类,$w_{t+1}(i) = 0.05 \cdot e^{-0.693} = 0.025$(归一化前)。错分样本会被乘 $e^{0.693} = 2$。

练习 4:AdaBoost vs GBDT 对照表

维度AdaBoostGBDT
损失函数指数损失(固定)任意可微
基学习器决策桩CART 回归树
更新方式重加权样本拟合负梯度
噪声鲁棒性差(指数惩罚)可调(Huber 等)
原生支持回归不支持支持

一句话:AdaBoost 是漂亮的特例,GBDT 是工业的框架。

练习 5:特征随机化为什么重要

随机森林取 $m = d$(每次都看全部特征)就退化为 Bagging。请解释为什么这通常比小 $m$ 表现更差——尽管单棵树更强。

解答:$m = d$ 时每棵树都贪心地在树根附近选同样的主导特征。两两相关 $\rho$ 居高不下,§1.1 的方差下界 $\rho\sigma^2$ 依然很大。减小 $m$ 让每棵树稍微变弱($s$ 下降),但 $\rho$ 下降得更快,Breiman 上界 $\bar\rho(1 - s^2)/s^2$ 反而更紧。结果是整片森林的泛化更好。


参考文献

  • Breiman, L. (1996). Bagging predictors. Machine Learning, 24(2), 123–140.
  • Breiman, L. (2001). Random forests. Machine Learning, 45(1), 5–32.
  • Freund, Y., & Schapire, R. E. (1997). A decision-theoretic generalization of on-line learning and an application to boosting. Journal of Computer and System Sciences, 55(1), 119–139.
  • Friedman, J. H. (2001). Greedy function approximation: A gradient boosting machine. Annals of Statistics, 29(5), 1189–1232.
  • Friedman, J. H. (2002). Stochastic gradient boosting. Computational Statistics & Data Analysis, 38(4), 367–378.
  • Schapire, R. E., Freund, Y., Bartlett, P., & Lee, W. S. (1998). Boosting the margin: A new explanation for the effectiveness of voting methods. Annals of Statistics, 26(5), 1651–1686.
  • Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (2nd ed.). Springer. Chapter 10.

系列导航

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub