AI 工程基础体系 · 第 45/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

集成学习:Bagging、Random Forest、Boosting、Stacking 与误差来源

集成学习(ensemble learning)不是某一个模型,而是一类组合多个学习器的方法。设有 MM 个基学习器:

f1(x),f2(x),,fM(x)f_1(x), f_2(x), \ldots, f_M(x)

集成模型通过某种规则得到最终预测:

F(x)=A(f1(x),f2(x),,fM(x))F(x)=A(f_1(x),f_2(x),\ldots,f_M(x))

其中 AA 可以是平均、投票、加权求和,或者另一个学习器。Bagging、Random Forest、Boosting 和 Stacking 的主要差异,就在于:

  1. 基学习器使用什么数据;
  2. 基学习器之间是并行训练还是串行训练;
  3. 最终预测如何组合;
  4. 组合主要针对偏差、方差,还是模型之间的结构差异。

集成学习有效的前提不是“模型越多越好”,而是基学习器既要有一定预测能力,又要保留足够的错误差异。若所有模型犯完全相同的错,增加模型数量几乎没有收益。


1. 先理解误差:集成学习到底在降低什么

1.1 预测误差的三个来源

监督学习中,常把误差拆成:

  • 不可约噪声(irreducible noise):数据本身的随机性、测量误差、标签错误;
  • 偏差(bias):模型假设过于简单,无法表达真实规律;
  • 方差(variance):模型对训练数据的偶然变化过于敏感。

以回归为例,真实标签满足:

Y=f(x)+ϵY=f(x)+\epsilon

其中 f(x)f(x) 是真实规律,ϵ\epsilon 是均值为 0、方差为 σϵ2\sigma_\epsilon^2 的噪声。若在不同训练集 DD 上训练模型,得到 f^D(x)\hat f_D(x),则在固定输入 xx 处:

ED,Y[(f^D(x)Y)2]=(ED[f^D(x)]f(x))2偏差平方+ED[(f^D(x)ED[f^D(x)])2]方差+σϵ2不可约噪声\mathbb{E}_{D,Y}\left[(\hat f_D(x)-Y)^2\right] = \underbrace{\left(\mathbb{E}_D[\hat f_D(x)]-f(x)\right)^2}_{\text{偏差平方}} + \underbrace{\mathbb{E}_D\left[(\hat f_D(x)-\mathbb{E}_D[\hat f_D(x)])^2\right]}_{\text{方差}} + \underbrace{\sigma_\epsilon^2}_{\text{不可约噪声}}

推导的关键是令:

μ(x)=ED[f^D(x)]\mu(x)=\mathbb{E}_D[\hat f_D(x)]

然后将误差写成:

f^D(x)Y=(f^D(x)μ(x))+(μ(x)f(x))+(f(x)Y)\hat f_D(x)-Y = (\hat f_D(x)-\mu(x)) + (\mu(x)-f(x)) + (f(x)-Y)

展开平方并取期望后,交叉项在相应的零均值条件下消失,得到上述分解。

这个分解说明:

  • 更复杂的模型通常降低偏差,但可能增加方差;
  • 更强的正则化通常降低方差,但可能增加偏差;
  • 集成方法最擅长降低的是方差,或者通过逐步修正降低偏差;
  • 任何方法都无法从根本上消除标签噪声。

分类问题没有一个在所有损失函数下都同样适用的简单偏差—方差公式,但同样存在模型不稳定性、表示能力不足和数据噪声等误差来源。


1.2 平均为什么能降低方差

先考虑 MM 个回归模型,它们每个模型的误差为:

ei=fi(x)f(x)e_i=f_i(x)-f(x)

假设每个模型误差的方差为 σ2\sigma^2,任意两个模型误差的相关系数都为 ρ\rho。平均预测:

fˉ(x)=1Mi=1Mfi(x)\bar f(x)=\frac{1}{M}\sum_{i=1}^{M}f_i(x)

其误差为:

eˉ(x)=1Mi=1Mei\bar e(x)=\frac{1}{M}\sum_{i=1}^{M}e_i

于是:

Var(eˉ)=Var(1Mi=1Mei)=1M2(Mσ2+M(M1)ρσ2)=σ2(ρ+1ρM)\begin{aligned} \operatorname{Var}(\bar e) &= \operatorname{Var}\left(\frac{1}{M}\sum_{i=1}^{M}e_i\right)\\ &= \frac{1}{M^2} \left( M\sigma^2+M(M-1)\rho\sigma^2 \right)\\ &= \sigma^2\left(\rho+\frac{1-\rho}{M}\right) \end{aligned}

这个公式揭示了集成的两个条件:

  • ρ=0\rho=0 时,方差约为 σ2/M\sigma^2/M,模型越多越有帮助;
  • ρ=1\rho=1 时,方差仍是 σ2\sigma^2,平均完全没有收益;
  • MM 很大时,方差下限趋近于 ρσ2\rho\sigma^2

因此,集成学习需要同时控制:

  1. 单个模型的质量;
  2. 模型误差之间的相关性。

随机抽样、随机特征选择、不同模型结构和不同训练目标,都是为了制造“有用的不一致”,而不是为了无目的地制造随机性。


1.3 偏差、方差与噪声的一个具体算例

假设有三个回归模型,在某个输入 xx 处的预测误差分别为:

e1=2,e2=4,e3=0e_1=2,\quad e_2=4,\quad e_3=0

它们的平均误差是:

eˉ=2+4+03=2\bar e=\frac{2+4+0}{3}=2

如果真实函数值为 f(x)f(x),那么这个集成的预测仍然整体偏高 2。平均操作降低了模型之间的波动,却没有消除共同方向上的偏差。

反例是三个模型误差分别为:

e1=2,e2=2,e3=0e_1=2,\quad e_2=-2,\quad e_3=0

平均误差为 0,集成效果很好。但如果三个模型误差都是 22,平均误差仍为 22。这就是“低相关错误”与“共同错误”的区别。


2. Bagging:通过重采样并行降低方差

2.1 定义与基本流程

Bagging 是 Bootstrap Aggregating 的缩写,核心流程是:

  1. 从原始训练集 DD 中进行有放回抽样,得到多个 bootstrap 数据集;
  2. 在每个数据集上独立训练一个基学习器;
  3. 回归时平均预测;
  4. 分类时投票,或平均类别概率后取最大类别。

设原始数据有 nn 条样本。一次大小为 nn 的有放回抽样中,一条固定样本未被抽中的概率为:

(11n)n\left(1-\frac{1}{n}\right)^n

nn 较大时:

(11n)ne10.368\left(1-\frac{1}{n}\right)^n\approx e^{-1}\approx 0.368

因此一个 bootstrap 数据集平均包含约 63.2%63.2\% 的不同原始样本,约 36.8%36.8\% 的原始样本没有被抽中。这些未被抽中的样本称为该基学习器的 out-of-bag(OOB)样本

flowchart LR
    D[原始训练集] --> B1[Bootstrap 样本集 1]
    D --> B2[Bootstrap 样本集 2]
    D --> B3[Bootstrap 样本集 3]
    B1 --> M1[基学习器 1]
    B2 --> M2[基学习器 2]
    B3 --> M3[基学习器 3]
    M1 --> A[平均或投票]
    M2 --> A
    M3 --> A
    A --> P[最终预测]

Bagging 的并行性来自一个事实:第 ii 个基学习器不需要等待第 jj 个基学习器完成。训练集固定后,各个 bootstrap 数据集和模型可以独立计算。


2.2 Bagging 为什么主要降低方差

假设基学习器是高方差模型,例如没有强剪枝的决策树。单棵树可能因为少量样本变化而产生完全不同的切分结构。

Bagging 通过不同 bootstrap 数据集得到:

f1,f2,,fMf_1,f_2,\ldots,f_M

最终回归预测为:

y^(x)=1Mm=1Mfm(x)\hat y(x)=\frac{1}{M}\sum_{m=1}^M f_m(x)

若每棵树的偏差相近,平均能够抵消一部分由训练集偶然性造成的偏差方向,从而降低方差。

但 Bagging 并不能保证降低偏差:

  • 如果基学习器本身表达能力不足,所有模型都会欠拟合;
  • 如果特征中缺少预测信息,重采样不能凭空产生信息;
  • 如果标签存在系统性错误,模型可能共同学习错误规律。

一个简单反例是:用深度为 1 的决策树学习复杂的 XOR 关系。即使训练数千棵树,如果每棵树的结构都过于简单,Bagging 仍可能保留较高偏差。


2.3 bootstrap 不是独立同分布的“新世界”

Bagging 常被误解为“训练了很多完全独立的模型”。实际上:

  • 所有 bootstrap 数据都来自同一个原始数据集;
  • 不同 bootstrap 数据之间高度重叠;
  • 如果原始数据存在分布偏移、标签泄漏或采样偏差,所有模型可能共同继承;
  • bootstrap 只能改变样本权重,不能修复错误的数据生成过程。

对于时间序列、用户级数据和组间高度相关的数据,简单按行 bootstrap 还可能破坏原有依赖结构。例如同一用户的多条记录被同时拆到训练与验证集合,模型看似泛化良好,实际只是记住了用户特征。


2.4 OOB 评估的含义与边界

对于某个训练样本 xix_i,只有没有抽到它的基学习器才能用于 OOB 预测。令这些模型构成集合 Mi\mathcal{M}_i,则:

y^iOOB=A({fm(xi):mMi})\hat y_i^{\mathrm{OOB}} = A\left(\{f_m(x_i):m\in\mathcal{M}_i\}\right)

再将 y^iOOB\hat y_i^{\mathrm{OOB}} 与真实标签比较,就得到 OOB 误差。

OOB 评估的优点是无需额外划出验证集,且每个样本的预测来自没有使用该样本训练的模型。但它不是无条件可靠的测试集替代品:

  • 如果反复根据 OOB 误差调参,OOB 结果也会被间接过拟合;
  • 时间序列中的未来信息不能通过普通 bootstrap 处理;
  • 分组数据需要按组而不是按行构造独立性;
  • 类别极不平衡时,OOB 中某些类别的有效预测数量可能不足。

3. Random Forest:Bagging 加上随机特征选择

3.1 Random Forest 的定义

Random Forest(随机森林)通常由许多决策树组成。它保留了 Bagging 的样本随机性,并在每个节点分裂时再随机选择一部分特征候选。

训练一棵树时:

  1. 从训练数据中进行 bootstrap 抽样;
  2. 从根节点开始构造决策树;
  3. 在每个节点,只从随机选出的特征子集里寻找最佳切分;
  4. 通常让树充分生长,而不是像单棵树那样强剪枝;
  5. 重复训练多棵树;
  6. 对树的预测进行平均或投票。

因此,Random Forest 不是简单地“多棵树投票”,而是同时使用:

  • 样本级随机性;
  • 特征级随机性;
  • 多树聚合。

3.2 为什么特征随机化很重要

假设数据中有一个极强特征,决策树每次都优先按这个特征切分。即便样本 bootstrap 不同,各棵树的根节点和后续结构也可能非常相似,树之间的误差相关性 ρ\rho 较高。

随机森林在每个节点限制候选特征,使不同树看到的局部候选空间不同。即使强特征存在,某棵树在某个节点也可能暂时不能使用它,从而形成不同结构。

回到:

Var(eˉ)=σ2(ρ+1ρM)\operatorname{Var}(\bar e) = \sigma^2\left(\rho+\frac{1-\rho}{M}\right)

特征随机化的主要价值不是让单棵树更强,而是降低 ρ\rho。但随机化过强也会让每棵树变得过弱,增加单模型偏差。因此特征子集大小是偏差与相关性的折中。


3.3 随机森林的概率输出不是天然校准概率

分类随机森林常将树的类别预测或类别概率聚合。例如二分类时:

p^(x)=1Mm=1Mp^m(x)\hat p(x)=\frac{1}{M}\sum_{m=1}^{M}\hat p_m(x)

这个数值可以用于排序和阈值决策,但不自动意味着:

所有预测为 0.8 的样本中,恰好 80% 属于正类。

这要求概率校准。树数量、叶节点大小、类别权重和训练分布都会影响校准。若业务需要可靠概率,例如风险定价或人工审核分流,应在独立校准数据上使用校准方法,并重新评估校准误差,而不能仅看 ROC-AUC。


3.4 随机森林的适用边界

随机森林通常适合:

  • 表格数据;
  • 非线性关系和特征交互;
  • 需要较少特征预处理的任务;
  • 希望获得稳定基线和特征重要性线索的场景。

它并非对所有问题都占优:

  • 高维稀疏文本通常需要线性模型或专门的文本模型;
  • 图像、语音和复杂序列通常需要深度表示学习;
  • 需要精确外推的回归任务中,树模型容易在训练范围外给出近似叶节点常数;
  • 类别极不平衡时,默认 bootstrap 和多数投票可能忽略少数类。

“特征重要性高”也不等于“该特征具有因果作用”。相关特征、连续变量的取值数量和高基数类别都可能扭曲基于分裂增益的特征重要性。


4. Boosting:串行修正前面模型的不足

4.1 Boosting 与 Bagging 的根本差异

Bagging 的基学习器通常独立训练,重点是降低方差;Boosting 则按顺序训练:

f1f2fMf_1 \rightarrow f_2 \rightarrow \cdots \rightarrow f_M

mm 个模型依赖前面模型的结果,重点是逐步降低训练损失,常常能够降低偏差。

Boosting 的一般形式是加法模型:

FM(x)=F0(x)+m=1Mηhm(x)F_M(x)=F_0(x)+\sum_{m=1}^{M}\eta\,h_m(x)

其中:

  • F0(x)F_0(x) 是初始模型;
  • hm(x)h_m(x) 是第 mm 个弱学习器;
  • η\eta 是学习率;
  • MM 是迭代次数。

弱学习器不一定必须“很弱”,但通常使用浅树、低复杂度模型或带强正则化的模型,以便控制过拟合和计算成本。


4.2 AdaBoost:按错误样本重新加权

AdaBoost 是分类 Boosting 的经典形式。以二分类为例,令标签:

yi{1,+1}y_i\in\{-1,+1\}

初始时为每个样本赋相同权重:

wi(1)=1nw_i^{(1)}=\frac{1}{n}

mm 轮:

  1. 使用样本权重训练弱分类器 hm(x)h_m(x)
  2. 计算加权错误率:

ϵm=i=1nwi(m)1[hm(xi)yi]\epsilon_m= \sum_{i=1}^{n} w_i^{(m)} \mathbf{1}[h_m(x_i)\neq y_i]

  1. 计算该模型的权重:

αm=12log1ϵmϵm\alpha_m= \frac{1}{2}\log\frac{1-\epsilon_m}{\epsilon_m}

  1. 更新样本权重:

wi(m+1)wi(m)exp(αmyihm(xi))w_i^{(m+1)} \propto w_i^{(m)} \exp(-\alpha_m y_i h_m(x_i))

如果样本被正确分类,yihm(xi)=1y_i h_m(x_i)=1,权重乘以 eαme^{-\alpha_m};如果分类错误,yihm(xi)=1y_i h_m(x_i)=-1,权重乘以 eαme^{\alpha_m}。因此下一轮会更加关注前一轮犯错的样本。

最终分类器为:

H(x)=sign(m=1Mαmhm(x))H(x)=\operatorname{sign} \left( \sum_{m=1}^{M}\alpha_m h_m(x) \right)

ϵm<0.5\epsilon_m<0.5 时,αm>0\alpha_m>0,说明该弱学习器至少优于随机猜测。若 ϵm>0.5\epsilon_m>0.5,通常需要调整模型或反转其预测。


4.3 AdaBoost 的完整小算例

有四个样本,初始权重均为 0.250.25。第一棵树错分第 1 个样本,因此:

ϵ1=0.25\epsilon_1=0.25

该树的权重为:

α1=12log0.750.25=12log30.549\alpha_1 = \frac12\log\frac{0.75}{0.25} = \frac12\log 3 \approx 0.549

更新前:

  • 错分样本权重乘以 e0.5491.732e^{0.549}\approx 1.732
  • 正确样本权重乘以 e0.5490.577e^{-0.549}\approx 0.577

未归一化权重为:

(0.25×1.732, 0.25×0.577, 0.25×0.577, 0.25×0.577)(0.25\times1.732,\ 0.25\times0.577,\ 0.25\times0.577,\ 0.25\times0.577)

即约为:

(0.433, 0.144, 0.144, 0.144)(0.433,\ 0.144,\ 0.144,\ 0.144)

总和约为 0.8650.865,归一化后:

(0.5, 0.167, 0.167, 0.167)(0.5,\ 0.167,\ 0.167,\ 0.167)

下一棵树训练时,第 1 个样本拥有一半权重。它即使只改善这个困难样本,也可能显著降低加权错误率。

这个机制解释了 Boosting 的优势和风险:

  • 优势:能集中处理当前模型的困难区域;
  • 风险:如果困难样本是标签错误或异常点,模型会不断追逐噪声。

4.4 Gradient Boosting:拟合损失函数的负梯度

现代 Gradient Boosting 的核心不是特定的“错误样本加权”,而是在函数空间中做梯度下降。

给定损失函数 L(y,F(x))L(y,F(x)),第 mm 轮先计算每个样本的负梯度:

rim=[L(yi,F(xi))F(xi)]F=Fm1r_{im} = -\left[ \frac{\partial L(y_i,F(x_i))} {\partial F(x_i)} \right]_{F=F_{m-1}}

再训练一个回归树 hm(x)h_m(x) 去拟合 rimr_{im},最后更新:

Fm(x)=Fm1(x)+ηhm(x)F_m(x)=F_{m-1}(x)+\eta h_m(x)

对平方误差损失:

L(y,F)=12(yF)2L(y,F)=\frac12(y-F)^2

有:

LF=Fy\frac{\partial L}{\partial F}=F-y

所以:

rim=yiFm1(xi)r_{im}=y_i-F_{m-1}(x_i)

这正是当前模型的残差。也就是说,平方损失下的 Gradient Boosting 等价于“不断用新树拟合残差”。

对分类任务,可以使用对数损失;此时负梯度不再简单等于标签减预测值,但仍然表示“降低当前损失最有效的局部方向”。


4.5 学习率、树数量与树深度的关系

Gradient Boosting 中,学习率 η\eta 越小,每棵树的修正幅度越小,通常需要更多树:

FM=F0+ηm=1MhmF_M=F_0+\eta\sum_{m=1}^{M}h_m

这不是简单的“学习率越小越好”:

  • 学习率过大,训练损失下降快,但更容易过拟合或产生不稳定更新;
  • 学习率过小且树数量不足,会欠拟合;
  • 树太深,单棵树能表达复杂交互,可能降低偏差但增加方差;
  • 树太浅,模型稳定但可能无法捕捉必要交互。

Boosting 不能靠“增加更多树”无限改善。训练误差继续下降时,验证误差可能已经上升,这通常是过拟合,尤其在噪声标签、异常点和小数据集上明显。


4.6 Boosting 的失败反例

标签噪声反例:某些样本标签被随机翻转。AdaBoost 会反复提高这些样本权重,后续弱学习器逐渐围绕错误标签构造复杂边界。训练误差可能下降,但测试误差恶化。

分布偏移反例:训练数据中的某个局部模式只在历史期间成立。Boosting 会集中拟合这些高损失区域,但如果线上分布发生改变,修正项可能放大错误。

极端异常值反例:平方损失对大残差惩罚较强。某个离群点会持续产生巨大残差,Boosting 可能牺牲大多数正常样本去拟合它。稳健损失、样本裁剪或数据修正可能比继续增加树更有效。


5. Stacking:学习“什么时候相信哪个模型”

5.1 Stacking 的结构

Stacking(堆叠)使用多种不同基模型产生预测,再训练一个元模型(meta-model)组合这些预测。

设有 KK 个基模型:

f1(x),f2(x),,fK(x)f_1(x),f_2(x),\ldots,f_K(x)

元特征为:

z(x)=[f1(x),f2(x),,fK(x)]z(x)= \left[ f_1(x),f_2(x),\ldots,f_K(x) \right]

元模型 gg 输出最终结果:

F(x)=g(z(x))F(x)=g(z(x))

分类时,元特征可以是:

  • 每个模型的类别概率;
  • 每个模型的决策分数;
  • 经过约束的类别预测。

回归时通常使用连续预测值。

Stacking 与简单加权平均的差别在于,权重或组合规则由数据学习,而且可以是非线性的。但元模型越复杂,越容易过拟合。


5.2 为什么 Stacking 必须使用折外预测

这是 Stacking 中最重要的数据隔离条件。

错误做法是:

  1. 在全部训练集上训练基模型;
  2. 用这些基模型预测同一批训练样本;
  3. 用这些预测训练元模型。

因为基模型已经看过这些样本,预测值包含训练记忆。元模型会学习“基模型在已见样本上的表现”,而不是它们在未见样本上的泛化表现。

正确做法是使用 out-of-fold(OOF,折外)预测

  1. 将训练集分成 KK 折;
  2. 对第 jj 折:
    • 用其余 K1K-1 折训练基模型;
    • 只预测第 jj 折;
  3. 拼接所有折的预测,形成每个训练样本都来自未见过它的基模型预测;
  4. 用 OOF 预测训练元模型;
  5. 在最终训练阶段,使用全部训练数据重新训练每个基模型;
  6. 对新样本分别预测,再交给元模型。
flowchart TD
    D[训练数据] --> CV[K 折划分]
    CV --> F1[训练折外基模型]
    CV --> F2[生成每折 OOF 预测]
    F2 --> Z[OOF 元特征]
    Z --> G[训练元模型]
    D --> R[用全部训练数据重训基模型]
    X[新样本] --> R
    R --> P[基模型预测]
    P --> G
    G --> Y[最终预测]

“元模型训练数据必须近似模拟线上基模型输出”是 OOF 的本质。如果生产中基模型会漂移、版本不一致或使用不同预处理,离线 OOF 也可能与线上输入分布不一致。


5.3 Stacking 的信息增益来自互补性

假设:

  • 线性模型擅长处理近似线性趋势;
  • 树模型擅长处理阈值和特征交互;
  • 神经网络擅长从高维表示中提取模式。

当它们的错误具有差异时,元模型可以根据预测组合判断:

  • 当线性模型与树模型意见一致时提高置信度;
  • 当某个模型只在某类样本上可靠时调整其贡献;
  • 当模型预测冲突时利用额外上下文选择结果。

但如果所有基模型使用同一数据泄漏、同一错误特征和相同结构,它们可能高度相关,Stacking 只是在增加复杂度。


5.4 Stacking、Blending 与投票的区别

  • 硬投票:直接组合类别预测,例如多数票;
  • 软投票:平均类别概率;
  • 加权平均:权重通常通过验证集或优化方法确定;
  • Blending:使用一份留出验证集生成基模型预测,再训练元模型;
  • Stacking:通常使用 K 折 OOF 预测,能更充分利用训练数据;
  • Stacking 的元模型:可以是线性模型,也可以是树模型或神经网络。

Blending 实现简单,但留出集较小会降低基模型训练数据量;Stacking 数据利用率更高,但训练流程和版本管理更复杂。


6. 四类方法的统一比较

方法 基模型关系 随机性来源 主要目标 典型组合方式 主要风险
Bagging 并行、相互独立 bootstrap 样本 降低方差 平均或投票 不能明显降低偏差;模型错误可能相关
Random Forest 并行树模型 bootstrap + 特征随机子集 降低树之间相关性和方差 平均或投票 高维稀疏、精确外推和概率校准受限
Boosting 串行、后者依赖前者 残差、梯度或错误权重 逐步降低损失、降低偏差 加法模型 对噪声、异常值和参数敏感
Stacking 基模型后接元模型 模型结构、特征或训练过程差异 学习模型组合规则 元模型 OOF 泄漏、元模型过拟合、部署复杂

一个重要边界是:Random Forest 属于 Bagging 思想的具体扩展,但所有 Bagging 模型都不是 Random Forest。对任意基学习器进行 bootstrap 聚合可以称为 Bagging;Random Forest 还要求树节点的特征随机化。


7. 一个可运行的 scikit-learn 示例

下面示例在人工分类数据上比较单棵树、Bagging、Random Forest、Gradient Boosting 和 Stacking。它使用独立测试集,避免将测试集参与训练或调参。

前置条件:

python -m pip install -U scikit-learn

完整代码:

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import (
    BaggingClassifier,
    RandomForestClassifier,
    GradientBoostingClassifier,
    StackingClassifier,
)
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, log_loss, classification_report

# 1. 构造数据
X, y = make_classification(
    n_samples=3000,
    n_features=20,
    n_informative=8,
    n_redundant=4,
    n_classes=2,
    weights=[0.65, 0.35],
    class_sep=0.9,
    random_state=42,
)

# 2. 只在训练集上训练;测试集只用于最终评估
X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    stratify=y,
    random_state=42,
)

models = {
    "single_tree": DecisionTreeClassifier(
        max_depth=None,
        random_state=42,
    ),
    "bagging": BaggingClassifier(
        estimator=DecisionTreeClassifier(random_state=42),
        n_estimators=100,
        max_samples=1.0,
        bootstrap=True,
        n_jobs=-1,
        random_state=42,
    ),
    "random_forest": RandomForestClassifier(
        n_estimators=200,
        max_features="sqrt",
        n_jobs=-1,
        random_state=42,
    ),
    "gradient_boosting": GradientBoostingClassifier(
        n_estimators=150,
        learning_rate=0.05,
        max_depth=2,
        random_state=42,
    ),
}

# 3. Stacking 的元模型使用逻辑回归
stacking = StackingClassifier(
    estimators=[
        (
            "tree",
            DecisionTreeClassifier(max_depth=5, random_state=42),
        ),
        (
            "forest",
            RandomForestClassifier(
                n_estimators=100,
                max_features="sqrt",
                n_jobs=-1,
                random_state=42,
            ),
        ),
        (
            "gb",
            GradientBoostingClassifier(
                n_estimators=100,
                learning_rate=0.05,
                max_depth=2,
                random_state=42,
            ),
        ),
    ],
    final_estimator=LogisticRegression(max_iter=1000),
    cv=5,                 # 内部生成 OOF 预测
    stack_method="predict_proba",
    n_jobs=-1,
)
models["stacking"] = stacking

# 4. 训练并评估
for name, model in models.items():
    model.fit(X_train, y_train)

    pred = model.predict(X_test)
    proba = model.predict_proba(X_test)

    print(
        f"{name:18s} "
        f"accuracy={accuracy_score(y_test, pred):.4f} "
        f"log_loss={log_loss(y_test, proba):.4f}"
    )

# 5. 查看一个模型的详细分类结果
print("\nRandom Forest report:")
print(classification_report(
    y_test,
    models["random_forest"].predict(X_test),
))

代码中的几个关键点如下。

BaggingClassifier

BaggingClassifier(
    estimator=DecisionTreeClassifier(...),
    n_estimators=100,
    bootstrap=True,
)

estimator 是基学习器,n_estimators 是基学习器数量,bootstrap=True 表示对样本进行有放回抽样。n_jobs=-1 表示使用可用 CPU 并行训练;实际生产中应根据容器 CPU 配额设置,不能假设机器拥有无限并发。

RandomForestClassifier

RandomForestClassifier(
    n_estimators=200,
    max_features="sqrt",
)

max_features="sqrt" 让每个节点只在随机特征子集中寻找切分。这里的参数语义属于 scikit-learn 的公开 API,但具体默认值和可选值可能随版本变化,生产代码应固定依赖版本并查看对应版本文档。

GradientBoostingClassifier

GradientBoostingClassifier(
    n_estimators=150,
    learning_rate=0.05,
    max_depth=2,
)

树较浅,学习率较小,表示用更多次小步更新拟合分类损失。n_estimatorslearning_rate 和树复杂度需要联合调节,不能只单独增大其中一个参数。

StackingClassifier

StackingClassifier(
    estimators=[...],
    final_estimator=LogisticRegression(...),
    cv=5,
    stack_method="predict_proba",
)

cv=5 使 Stacking 内部使用交叉验证产生元模型训练所需的折外预测。stack_method="predict_proba" 表示将基模型的概率输出作为元特征;如果基模型没有 predict_proba,该配置可能无法工作,需要改用支持的方法。

运行结果中的具体数值会因 scikit-learn 版本、硬件和参数而变化。应关注相对关系和验证流程,而不是把某次随机数据上的准确率当作普遍结论。


8. 用 OOB 检查随机森林的训练外表现

随机森林可以开启 OOB 评估:

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(
    n_estimators=300,
    bootstrap=True,
    oob_score=True,
    n_jobs=-1,
    random_state=42,
)

rf.fit(X_train, y_train)

print("OOB score:", rf.oob_score_)
print("Test score:", rf.score(X_test, y_test))

这里的 oob_score_ 是在训练样本的 OOB 预测上计算的分数,score(X_test, y_test) 则是在从未参与训练的测试集上计算的分数。两者接近并不证明模型一定没有偏差,只能说明在当前数据划分和指标下,它们给出了相近结果。

风险包括:

  • oob_score=True 只在使用 bootstrap 时有意义;
  • OOB 不是时间序列的未来验证;
  • 不能用测试集结果反复选择模型后再声称测试集是未触碰的;
  • 若需要 F1、召回率、分层指标等,不能只看默认 score

9. 数据、评测与泄漏:集成模型最常见的真实失败原因

9.1 训练集与验证集泄漏

集成学习常需要更多层次的预测,因此更容易发生泄漏:

  • 先用全量数据做标准化、特征选择或目标编码,再划分训练集;
  • 用全量训练集训练基模型,再将其训练内预测交给 Stacking 元模型;
  • 在时间序列中随机打乱未来记录;
  • 同一用户、设备、病例或文档同时出现在训练和验证集合;
  • 调参多轮后仍把验证集当作最终测试集。

对于需要拟合参数的预处理,应将其放入交叉验证管道中。对于用户级泛化,应使用按用户分组的切分;对于时间依赖,应使用时间顺序切分。


9.2 评测指标必须匹配损失与业务决策

不同集成方法可能在不同指标上表现不同:

  • 类别不平衡时,准确率可能掩盖少数类失败;
  • 排序场景更关心 ROC-AUC、PR-AUC 或收益曲线;
  • 概率决策需要 Log Loss、Brier 分数和校准曲线;
  • 预测金额需要 MAE、RMSE,二者对异常值敏感程度不同;
  • 生成式 AI 的多个候选输出不能只用单一相似度指标,需要同时评估事实性、安全性、拒答质量、延迟和成本。

如果最终系统按阈值触发人工审核,那么阈值本身属于模型决策的一部分。应在验证集上选择阈值,并在测试集上一次性报告固定策略的结果。


9.3 用误差相关性判断“是否值得集成”

可以先分别得到多个模型在同一验证集上的错误指示变量:

Emi=1[y^miyi]E_{mi}= \mathbf{1}[\hat y_{mi}\neq y_i]

再计算模型间的错误相关性。如果模型 A 和模型 B 的准确率相近,但错误高度重合,组合收益有限;如果一个模型整体较弱但在另一个模型失败的样本上经常正确,它仍可能对 Stacking 有价值。

对于回归,可分析残差:

rmi=y^miyir_{mi}=\hat y_{mi}-y_i

并观察残差相关矩阵。不能仅根据模型名称判断互补性;“一个随机森林加一个梯度提升树”不一定比两个结构不同、但错误高度相关的模型更有价值。


10. 深度学习中的集成学习

深度学习也使用同样的集成原理,但训练成本通常更高。

常见方式包括:

  1. 深度模型独立训练多个随机种子:不同初始化、数据顺序和优化轨迹产生多个模型;
  2. 不同数据切分或交叉验证模型:每个模型看到不同训练子集;
  3. 模型结构集成:组合不同架构或不同输入模态;
  4. 概率平均或 Logit 平均:分类时聚合多个模型输出;
  5. 深度集成用于不确定性估计:模型之间的预测分歧可作为认知不确定性的线索。

深度集成通常能改善稳定性,但不能直接等同于严格的概率不确定性。它反映的是所训练模型集合的分歧,不一定覆盖数据分布外风险或标签噪声。

深度模型的集成还带来:

  • 多倍显存或 CPU 推理成本;
  • 多倍模型加载和发布复杂度;
  • 版本、权重、预处理和 tokenizer 必须成套管理;
  • 若多个模型共享同一错误数据或预训练偏差,集成并不能消除共同错误。

11. 生成式 AI 中的集成:候选、路由与评估器

在生成式 AI 系统中,集成不一定表现为“多个分类器投票”,常见形式是:

多候选生成与重排序

同一个生成模型采样多个候选答案,再由一个评分器、规则系统或另一个模型选择结果:

x1,,xKpθ(xc)x_1,\ldots,x_K\sim p_\theta(x\mid c)

x=argmaxxkS(xk,c)x^*=\arg\max_{x_k} S(x_k,c)

这里的 SS 可以综合:

  • 任务相关性;
  • 事实性或检索证据支持;
  • 安全策略;
  • 格式合法性;
  • 预算和延迟。

这更接近“生成器 + 评估器”的 Stacking 或 reranking,而不是简单平均文本。生成文本不能逐 token 直接做多数投票,除非设计了特定的序列级组合算法。

模型路由

路由器根据请求类型、风险、延迟预算和成本选择不同模型:

m=argminm[λ1Cm+λ2Tm+λ3Rm(x)]m^*=\arg\min_m \left[ \lambda_1 C_m+\lambda_2 T_m+\lambda_3 R_m(x) \right]

其中 CmC_m 是成本,TmT_m 是延迟,Rm(x)R_m(x) 是在当前请求上的风险或预估错误。路由器本身也可能过拟合离线流量,必须在真实流量分布上验证。

生成式 AI 的共同错误

多个模型都可能:

  • 使用同一过时检索索引;
  • 看到同样的恶意提示;
  • 继承同一错误知识;
  • 在相同上下文截断位置丢失关键证据;
  • 输出违反权限的数据。

因此,集成模型不能替代权限检查、数据访问控制和内容安全策略。权限必须在数据检索、工具调用和最终输出等边界分别执行,而不能把“模型大概率不会泄露”当作授权机制。


12. 生产系统中的成本、并发与故障路径

集成模型的总成本通常不只是训练成本:

Ctotal=Ctrain+Cstorage+Cinference+Cevaluation+CoperationC_{\text{total}} = C_{\text{train}} + C_{\text{storage}} + C_{\text{inference}} + C_{\text{evaluation}} + C_{\text{operation}}

若有 MM 个模型,在线推理通常需要:

  • 并行发送 MM 个请求,受最慢模型影响尾延迟;
  • 或串行调用,延迟近似累加;
  • 维护 MM 份权重、特征处理和监控;
  • 处理部分模型超时、失败或版本不一致。

一个完整的在线路径可以是:

sequenceDiagram
    participant C as 客户端
    participant R as 路由/编排层
    participant A as 模型A
    participant B as 模型B
    participant G as 元模型或重排序器
    participant P as 权限与安全策略

    C->>R: 请求与身份上下文
    R->>P: 校验数据和工具权限
    P-->>R: 允许的上下文范围
    par 并行调用
        R->>A: 预测请求
        R->>B: 预测请求
    end
    A-->>R: 结果或超时
    B-->>R: 结果或错误
    R->>G: 可用结果及状态
    G->>P: 输出安全与权限复核
    P-->>C: 最终结果、降级结果或拒绝

故障处理不能只假设“所有模型都会成功”:

  • 一个基模型超时,元模型是否能处理缺失输入;
  • 元模型是否训练过这种缺失模式;
  • 降级到单模型时是否改变风险等级;
  • 某个模型返回格式错误时是否隔离;
  • 部分结果是否可能导致不完整答案被误认为高置信答案;
  • 超时重试是否造成重复计费或重复写操作。

对于 Stacking,线上特征顺序、概率列顺序、类别编码和预处理版本必须固定。仅加载模型权重而遗漏元模型或特征变换,会产生静默错误,往往比直接报错更危险。


13. 诊断:如何判断是偏差、方差、噪声还是泄漏

可以按以下逻辑进行诊断,但每一步都要基于同一切分策略和同一业务指标。

训练误差高,验证误差也高

可能是:

  • 模型表达能力不足,偏差高;
  • 特征缺少信息;
  • 标签定义与目标不一致;
  • 训练过程没有收敛;
  • 训练和验证的预处理不一致。

此时直接增加 Bagging 模型数量通常无效,因为它主要降低方差。可以检查特征、模型容量、损失函数和标签质量。

训练误差低,验证误差高

可能是:

  • 单模型或 Boosting 过拟合;
  • 样本量不足;
  • 时间或用户泄漏导致验证策略失真;
  • 训练分布与验证分布不一致;
  • 异常点和噪声被过度拟合。

可以减少树深、降低迭代次数、增大叶节点、增加正则化,或重新设计切分方式。但不能把所有验证误差高都归因于模型复杂度。

单模型表现一般,Bagging 明显改善

通常说明模型方差较高,且不同 bootstrap 模型的错误不完全相同。决策树是典型情况。

Bagging 几乎没有改善

可能是:

  • 基模型偏差高;
  • 模型之间相关性高;
  • bootstrap 改变不足;
  • 数据噪声占主导;
  • 评测方差太大,收益未超过随机波动。

应查看不同模型的预测相关性和学习曲线,而不是只增加模型数量。

Boosting 训练指标持续改善,验证指标先升后降

这是典型的迭代过拟合信号。应使用独立验证数据进行早停或选择迭代轮数,并保留最终测试集用于一次性评估。早停的具体 API 和默认行为依赖所用实现,不能把某个库的参数语义泛化到所有 Boosting 框架。

Stacking 验证结果异常好,线上却下降

首先排查:

  1. 元模型是否使用了训练内预测而不是 OOF 预测;
  2. 预处理是否在切分前拟合;
  3. 用户、时间或实体是否跨集合;
  4. 线上基模型版本是否与生成 OOF 数据的版本一致;
  5. 线上是否存在模型超时、缺失输入或类别顺序变化。

Stacking 的异常高分首先应被视为泄漏嫌疑,而不是模型能力的证明。


14. 选择方法时的因果判断

可以用“误差结构”而不是模型流行程度来选择方法:

  • 如果基模型对训练数据变化很敏感,优先考虑 Bagging 或 Random Forest;
  • 如果模型具有较高偏差,且损失可以通过逐步修正降低,考虑 Boosting;
  • 如果已有多个结构明显不同、错误互补的模型,考虑 Stacking;
  • 如果所有模型共享同一数据问题,先修复数据和切分,集成本身不是补丁;
  • 如果线上成本和延迟严格受限,比较集成带来的收益是否超过额外推理;
  • 如果需要概率决策,单独评估校准,而不是从准确率推断概率可靠性;
  • 如果任务是图像、语音、长文本或生成式 AI,表格模型集成方法的具体实现不能直接照搬,但“降低相关错误、隔离评测数据、控制成本和故障”的原理仍然成立。

集成学习的核心不是把模型数量堆起来,而是通过数据扰动、特征扰动、训练路径差异或元模型学习,让多个模型在保持基本能力的同时犯出不完全相同的错误。Bagging 和 Random Forest 主要利用这种差异降低方差,Boosting 通过串行修正降低损失,Stacking 则学习不同模型在什么条件下更值得信任。最终效果取决于误差来源、数据独立性、评测设计以及生产系统是否能正确承载这些模型。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。