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

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

为什么一群平庸的分类器组合起来能胜过一个超级厉害的分类器?答案很简单:取平均能降低方差,逐步调整权重能减少偏差,再加上一点随机性,就能打破相关性——否则前面的努力都会白费。本文将深入推导背后的数学原理,包括偏差-方差分解、Bagging 和随机森林如何利用 Bootstrap、AdaBoost 在指数损失下的前向分步优化,以及 GBDT 如何将这些方法统一为函数空间中的梯度下降。

读完之后,你应该能一眼看穿任何集成方法:它在降什么、为什么有效、何时会失效。

机器学习数学推导(十一):集成学习 — 章节概览图


你将学到什么#

  • 平均 $T$ 个不相关的模型,为什么能把方差降到原来的 $1/T$ ;如果这些模型相关,又会发生什么?
  • Bagging 和随机森林如何通过随机化使树之间几乎不相关。
  • AdaBoost 并非凭空设计,而是指数损失下前向分步加法建模的一个特例。
  • GBDT 如何在函数空间中执行梯度下降。
  • 面对具体问题时,如何选择 Bagging、Boosting 或 Stacking。

前置知识#

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

集成为什么有效#

核心方差公式#

$$H(\mathbf{x}) = \frac{1}{T}\sum_{t=1}^T h_t(\mathbf{x}).$$ $$ \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$ )。
  • 相关性是敌人:即使有无穷多个模型,方差的下限仍然是 $\rho\sigma^2$ 。这就是为什么随机森林不仅对样本做随机化,还要对特征做随机化——后者直接降低了 $\rho$

因此,所有集成方法的核心都在回答一个问题:如何生成多样化的基学习器(小 $\rho$ ),同时不让它们太弱(大偏差)?

偏差-方差分解#

$$ \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:从高偏差低方差的基学习器出发,逐步增加容量以降低偏差

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

平庸投票者的力量#

$$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%,效果非常出色。关键在于“独立”——这再次指向去相关性的核心问题。

Bagging 与随机森林#

Bagging:并行训练法#

Bagging 架构:在 Bootstrap 样本上并行训练弱学习器,然后取平均

Bagging(Breiman, 1996)是将 §1.1 中的方差恒等式直接应用的最佳方式。

算法步骤

  1. Bootstrap 抽样:从大小为 $N$ 的训练集 $\mathcal{D}$ 中,有放回地抽取 $T$ 个子集,每个子集大小仍为 $N$ 。记这些子集为 $\mathcal{D}_1,\dots,\mathcal{D}_T$
  2. 独立训练:在每个子集 $\mathcal{D}_t$ 上单独训练一个基学习器 $h_t$ 。通常树会生长得很深——低偏差、高方差,正好适合通过平均来降低方差。
  3. 结果聚合:回归任务取平均 $H(\mathbf{x}) = \tfrac{1}{T}\sum_t h_t(\mathbf{x})$ ;分类任务采用多数投票。
$$\left(1 - \tfrac{1}{N}\right)^N \xrightarrow{N \to \infty} e^{-1} \approx 0.368.$$

这些未被采样的点称为树 $t$袋外样本(OOB)

$$ \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\}. $$

这是泛化误差的一个几乎无偏估计,作为训练的副产品直接得到,无需额外划分验证集或运行交叉验证。

随机森林:让树彼此去相关#

单棵深树 vs 随机森林:后者通过平均去相关的树平滑了决策边界

仅靠 Bagging 还不够:Bootstrap 子集之间高度重叠,导致每棵树总是在相同的主导特征上做分裂,预测结果高度相关,$\rho$ 偏大,使得 §1.1 中的方差缩减效果难以实现。

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

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

强制每棵树在不同节点使用不同的特征,打破了“主导特征陷阱”,显著降低了 $\rho$ 。上图清楚展示了这一效果:单棵深度较大的树将输入空间分割成矩形块,容易过拟合噪声;而一棵 $m=1$ 的随机森林通过平均 80 棵这样的树,生成了一个平滑且泛化能力更强的决策边界。

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

这其实是 §1.1 中方差恒等式的另一种表达形式。调优随机森林只有两个关键点:

  1. 提升单棵树的能力(增大 $s$ ):让树更深、每次分裂考虑更多特征、使用更多样本。
  2. 减少树之间的相关性(降低 $\bar\rho$ ):减小 $m$ 、混合不同深度、增加树的数量。

这两个因素相互制约,因此 $m$ 是一个需要调整的参数,而非固定值。

特征重要性#

有两种主流方法可以评估特征的重要性:

  • 不纯度均值减少(MDI):累加所有基于特征 $j$ 分裂的节点的 $\Delta\text{Gini}$ 值,并对树取平均。计算速度快,但偏向高基数特征。
  • 置换重要性:用 OOB 样本,打乱第 $j$ 列特征值,观察 OOB 准确率下降多少。计算较慢,但无偏,且能正确处理相关特征。

当两个特征携带相似信息时,MDI 会将重要性分摊给两者;而置换重要性会给两者分配相同的下降值——这通常是解释模型时更希望看到的结果。

Bagging 和 Boosting:从偏差-方差角度看#

Bagging 在保留均值(偏差)的同时缩小了方差范围

上图用 120 个独立采样的数据集跑同样的拟合流程,画出了预测结果。看那条 $\pm 1$ 标准差的灰色带:

  • 左边单棵树:平均预测贴近真实值,偏差低,但每条曲线抖得厉害,方差高,误差带很宽。
  • 右边 25 棵树的 Bagging:平均值几乎没变,但误差带明显收窄,方差降了大约 $T$ 倍,和 §1.1 的理论预测完全一致。

Bagging 解决不了系统性偏差的问题。如果基学习器本身太弱,再怎么平均也没用——这事得靠 Boosting 来解决。


Boosting:逐步降低偏差#

Boosting:串行加权学习器

Boosting 完全改变了思路。它不是并行训练 $T$ 个学习器再取平均,而是按顺序一个个训练,每个学习器专注于前一个学习器犯的错误。每个学习器都故意设计得很弱(比如深度为 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}$ 。下一轮训练时,算法无法忽略那些困难样本。

训练误差的指数上界#

$$ \frac{1}{N}\sum_{i=1}^N \mathbf{1}[H(\mathbf{x}_i) \neq y_i] \;\le\; \prod_{t=1}^T Z_t. $$ $$ \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 将注意力集中在困难样本上;训练误差迅速下降

左图热力图展示了每个样本的权重随迭代的变化:大多数样本的权重逐渐趋于零,而边界上的困难样本权重却越来越高——算法确实把所有注意力都放在了之前学习器无法解决的样本上。右图对比了累积分类器的训练误差和理论上限 $\exp(-2\sum_t \gamma_t^2)$ ,虽然上限较宽松,但始终高于实际误差。

不过,这张图也揭示了 AdaBoost 的一个缺陷:当样本标签本身有误时,同样的机制会让模型将所有容量浪费在噪声上。相比之下,GBDT 使用鲁棒损失函数(如 Huber)能更优雅地处理这种情况。

为什么用指数权重?前向分步视角#

$$F(\mathbf{x}) = \sum_{t=1}^T \alpha_t\, h_t(\mathbf{x}),$$ $$(\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).$$ $$\min_{\alpha, h}\sum_{i=1}^N w_t(i)\exp(-\alpha\, y_i\, h(\mathbf{x}_i)).$$

先求解最优的 $h_t$ (它最小化加权错误率),再通过一维微积分求解 $\alpha_t$ ,最终得到的公式与 AdaBoost 的更新规则完全一致。这并不是什么启发式方法,而是函数空间中的坐标下降,每轮增加一个新的基函数。

梯度提升决策树(GBDT)#

Boosting 就是函数空间中的梯度下降#

指数损失太敏感了。Friedman 在 2001 年的核心洞见是:只要把问题重新理解为梯度下降,前向分步的思想就可以推广到任意可微的损失函数

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

这些 $r_{ti}$ 就是伪残差。用一棵回归树拟合 $\{(\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]$ 是学习率。

常见损失函数的伪残差形式#

损失$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 最初的直观想法。对于其他损失函数,只有从“函数空间梯度下降”的视角才能清晰地理解算法的本质。

正则化:三个关键参数#

如果不加以控制,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(下一篇会讲)在 GBDT 的基础上增加了显式的叶权重 $L_2$ 正则项和叶子数惩罚项,并将叶子最优解写成闭式解。不过,先搞清楚朴素 GBDT 的原理,再学 XGBoost 会更容易理解。

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 能带来收益?当基学习器犯的错误类型不同时。把深度模型、树模型和线性模型组合起来,往往能超越任何一个单独的模型,因为它们的错误会互相抵消。

集成规模:到底多少个学习器就够了?#

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

这张图展示了三种集成方法在同一个问题上的表现,并记录了测试误差随 $T$ 增加的变化趋势。有几个关键点需要注意:

  • 三种方法都在前十几棵树内轻松超越单棵深树的基线。
  • Bagging 和随机森林的表现平稳收敛:增加树的数量不会带来坏处,只是收益会逐渐趋于零。这种平稳的尾部曲线直接反映了 §1.1 中提到的方差下界 $\rho\sigma^2$
  • AdaBoost 收敛速度最快,但在某些情况下,一旦开始过拟合噪声,测试误差可能会反弹。这一点需要特别留意。
  • 随机森林的收敛平台比 Bagging 更低,因为特征随机化进一步降低了 $\bar\rho$

实际操作建议:对于 Bagging 和随机森林,$T$ 可以尽量大,只要算力允许就行;对于 AdaBoost,则需要用验证集配合 early stopping 来选择合适的 $T$


参考实现#

下面的代码特意写得简短,不依赖任何外部库。虽然性能不如 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
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

常见问题#

Q1:Bagging 和 Boosting 哪个更好?
问题本身不对。Bagging 降方差,Boosting 降偏差。如果基学习器过拟合(比如小数据集上的深树),就用 Bagging;如果基学习器欠拟合(比如决策桩),就用 Boosting。它们分别解决偏差-方差权衡的两个极端问题。

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,但测试误差可能上升。解决办法是用验证集做早停、缩小学习率、子采样,或者限制树的复杂度。

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


练习题#

练习 1 — 偏差-方差计算#

有 3 个独立的回归模型,每个模型的 $\text{bias}^2 = 4$$\text{Var} = 9$ 。分别计算以下两种情况的期望 MSE:(i)单个模型;(ii)简单平均集成模型。忽略噪声。

解答:单个模型的 MSE 是 $4 + 9 = 13$ 。集成模型的偏差保持不变,仍为 $4$ ,但方差降到 $9/3 = 3$ ,总 MSE 为 $7$ 。性能提升了约 $46\%$

练习 2 — 多数投票错误率#

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

$$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$ 。因为样本 $i$ 被正确分类,所以 $w_{t+1}(i) = 0.05 \cdot e^{-0.693} = 0.025$ (归一化前)。如果样本被错分,则权重会乘以 $e^{0.693} = 2$

练习 4 — AdaBoost 和 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$ 也会变得更紧。最终,整个森林的泛化能力反而更好。


下一步#

Bagging 和 Boosting 的代数骨架到此为止。但工业界真正在表格数据上跑赢一切的,是 Boosting 这条路线的两个极致工程实现——XGBoost 和 LightGBM。下一章我把它们当作"Boosting + 系统优化"来读,而不是当作两个新算法。

XGBoost 的二阶泰勒展开把"找最优叶子权重"化简成一个闭式解,正则化项直接进入分裂增益公式;LightGBM 用直方图算法、GOSS 采样、Leaf-wise 生长把训练时间砍掉一个量级。这两个库背后的数学并不复杂——核心都是"加一棵树最大化二阶损失下降"——但它们把这个简单想法做到了系统、内存、并发的极限。读完后你会发现,从"梯度提升的数学"到"XGBoost 的工程"之间,差的不是新理论,而是把每一行循环都打磨过。

参考文献#

  • 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.
本系列

机器学习数学推导 20 篇

  1. 01 机器学习数学推导(一):绪论与数学基础
  2. 02 机器学习数学推导(二):线性代数与矩阵论
  3. 03 机器学习数学推导(三):概率论与统计推断
  4. 04 机器学习数学推导(四):凸优化理论
  5. 05 机器学习数学推导(五):线性回归
  6. 06 机器学习数学推导(六):逻辑回归与分类
  7. 07 机器学习数学推导(七):决策树
  8. 08 机器学习数学推导(八):支持向量机
  9. 09 机器学习数学推导(九):朴素贝叶斯
  10. 10 机器学习数学推导(十):半朴素贝叶斯与贝叶斯网络
  11. 11 机器学习数学推导(十一):集成学习 当前
  12. 12 机器学习数学推导(十二):XGBoost 与 LightGBM
  13. 13 机器学习数学推导(十三):EM 算法与 GMM
  14. 14 机器学习数学推导(十四):变分推断与变分 EM
  15. 15 机器学习数学推导(十五):隐马尔可夫模型
  16. 16 机器学习数学推导(十六):条件随机场
  17. 17 机器学习数学推导(十七):降维与主成分分析
  18. 18 机器学习数学推导(十八):聚类算法
  19. 19 机器学习数学推导(十九):神经网络与反向传播
  20. 20 机器学习数学推导(二十):正则化与模型选择

读有所得?

GitHub 关注我 → 新文周更

GitHub