第八章 机器学习 -- 集成学习

一、Bagging

1.1 随机森林

Bagging(Bootstrap Aggregating)是 1996 年 Breiman 提出的方差缩减 集成策略。其思想是:对训练集做有放回抽样训练多棵"基学习器",再把它们的预测聚合(分类投票 / 回归平均)。降低方差、保留偏差------尤其适合高方差、低偏差的基学习器(如决策树、神经网络)。

Bagging 的核心思想

单棵决策树方差大、对训练集敏感。Bagging 通过多样性降低方差:

  • 每个基学习器只看到训练集的一部分(约 63.2% 的样本,因为有放回抽样)。
  • 不同基学习器在不同数据子集上学到不同模式。
  • 聚合让"个体错误"被"群体正确"覆盖。

Bagging 算法

text 复制代码
输入: 训练集 D = {(x_i, y_i)}, 基学习器算法 L, 学习器数 T, 抽样率 p
1. for t = 1, ..., T:
2.     D_t = bootstrap(D, p)          # 有放回抽样,样本数 = p * |D|
3.     h_t = L(D_t)                  # 训练基学习器
4. 输出: H(x) = aggregate({h_1(x), ..., h_T(x)})
   - 分类: 多数投票
   - 回归: 算术平均

方差缩减的数学

设各基学习器独立、期望 Eht=fEh_t = fEht=f,方差 Var(ht)=σ2\text{Var}(h_t) = \sigma^2Var(ht)=σ2。聚合 hˉ=1T∑ht\bar h = \frac{1}{T} \sum h_thˉ=T1∑ht:

Var(hˉ)=σ2T+(1−1T)Cov(h1,h2)\text{Var}(\bar h) = \frac{\sigma^2}{T} + (1 - \frac{1}{T}) \text{Cov}(h_1, h_2)Var(hˉ)=Tσ2+(1−T1)Cov(h1,h2)

独立时 Cov=0\text{Cov} = 0Cov=0,Var(hˉ)=σ2/T\text{Var}(\bar h) = \sigma^2 / TVar(hˉ)=σ2/T。但实际基学习器不独立(共享部分训练数据),Cov>0\text{Cov} > 0Cov>0,缩减效果打折。随机森林通过特征随机化进一步降低相关性。

随机森林(Random Forest, RF)

Breiman 2001 提出,在 Bagging 基础上对特征维度也做随机化:

  • 样本随机化:每棵树用有放回抽样(bootstrap)。
  • 特征随机化 :每次划分只考虑 mtry=d\text{mtry} = \sqrt{d}mtry=d (分类)或 d/3d/3d/3(回归)个特征(R 语言 randomForest 的约定;sklearn 分类默认 max_features="sqrt",回归默认 1.0 即全部特征,见下)。
  • 无剪枝:每棵 CART 完全生长(高方差、低偏差)。

为何特征随机化有效

  • 限制单棵树能用的特征,强制不同树学不同的"组合"。
  • 降低树间的相关性 → 进一步降方差。
  • mtry=d\text{mtry} = dmtry=d(不随机)退化为普通 Bagging。

scikit-learn 实现

python 复制代码
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor

clf = RandomForestClassifier(
    n_estimators=300,         # 树数量,越多越稳(边际收益递减)
    max_features="sqrt",      # 分类默认 "sqrt";回归器默认 1.0(全部特征,与 R 的 d/3 不同)
    max_depth=None,           # 完全生长(不剪枝)
    min_samples_split=2,
    min_samples_leaf=1,
    bootstrap=True,           # 样本有放回抽样
    oob_score=True,           # 用 OOB 样本做无偏评估
    n_jobs=-1,
    random_state=0,
)
clf.fit(X_train, y_train)
print(clf.oob_score_, clf.feature_importances_)

OOB(Out-of-Bag)评估

每棵树训练时,约 36.8% 的样本未被抽中(袋外样本)。用这些样本对单棵树做预测,全部树的 OOB 预测合并就是无偏的泛化误差估计------相当于免费的交叉验证。

P(样本∉某次 bootstrap)=(1−1/N)N→1/e≈0.368P(\text{样本} \notin \text{某次 bootstrap}) = (1 - 1/N)^N \to 1/e \approx 0.368P(样本∈/某次 bootstrap)=(1−1/N)N→1/e≈0.368

随机森林的预测

  • 分类:predict 返回多数票,predict_proba 返回各类投票比例(= 平均各树概率)。
  • 回归:predict 返回各树均值。

为何 RF 对类别不平衡鲁棒

每棵树的 bootstrap 抽样自然平衡了不同类别的局部密度。但整体仍偏向多数类 ------业务上仍需 class_weight="balanced" 或采样处理。

特征重要性

  • MDI(Mean Decrease Impurity) :每棵树的特征带来的不纯度下降总和(默认 feature_importances_)。
  • MDA / Permutation Importance:打乱单列后看 OOB 或验证集性能下降(更稳健)。

随机森林的优缺点

维度 评价
准确率 强基线,常超过单棵 CART 几个百分点
方差 大幅降低(500+ 棵树可接近无方差)
偏差 与单棵树相近
训练速度 可并行(n_jobs=-1)
预测速度 比单棵树慢 TTT 倍
可解释 弱(500 棵树难以可视化),可用 SHAP
内存 大(存 TTT 棵树)
对异常值 比单棵树鲁棒(聚合)但仍敏感
对类别不平衡 默认偏向多数类

与 Boosting 的对比

维度 Bagging / RF Boosting / GBDT
基学习器 独立并行 序贯依赖
主要作用 降方差 降偏差
对基学习器要求 高方差、低偏差 低方差、高偏差
树深度 深(完全生长) 浅(典型 3--8)
集成规模 数百到数千 几十到几百
训练时间 短(可并行) 长(序贯)
过拟合 难(更多树更稳) 易(迭代次数过多会过拟合)

Extra Trees(Extremely Randomized Trees)

Geurts et al. 2006:在 RF 基础上对分裂阈值也做随机化(不再扫描最优阈值)。单树 variance 增大、树间相关性降低 → 集成 variance 反而可能更低;训练更快。

sklearn.ensemble.ExtraTreesClassifier。

RF 的常见误区

  • 认为 RF 不会过拟合 :错。RF 在 TTT 增大时收敛到某个偏差-方差折衷;过拟合主要来自树深度过大、特征噪声大。
  • mtry 越小越好:太小(mtry=1)→ 树间相关性低但单树弱;太大 → 单树强但相关性高。需交叉验证。
  • 未做 OOB 评估浪费了 1/3 的数据 :开 oob_score=True 几乎无成本。
  • 特征重要性偏向高基数特征:MDI 对独热后的多列类别过度重视;用 Permutation Importance 或先目标编码。

易错点

  • 对量级不敏感但对噪声敏感:RF 对异常值仍敏感(每棵树的叶节点均值会被拉偏);先做异常值处理。
  • 树太多训练慢:500 棵树是常用起点;增加到 5000+ 边际收益小。
  • 预测时内存与延迟 :1000 棵树预测一个样本需遍历 1000 棵树;用 n_jobs 并行预测可缓解。
  • 类别不平衡 :RF 投票仍偏向多数类;用 class_weight="balanced"、阈值调整或重采样。

二、Boosting

2.1 AdaBoost

Boosting 是一族序贯集成 方法:每个基学习器在前一阶段的"残差/错误"上训练,把"难样本"逐渐学好。它与 Bagging 的关键区别是基学习器之间不是独立的 ,而是相互依赖。

Boosting 的核心思想

  • 起始:所有样本权重相等。
  • 每步:训练一个弱学习器(如决策树桩),看错哪些样本 → 增加这些样本的权重。
  • 加权组合:每个弱学习器根据其训练误差获得投票权重。
  • 终止:达到预定学习器数或训练误差足够小。

Boosting 把"弱学习器"(略好于随机)组合成"强学习器"。Kearns & Valiant 1989 首次提出"弱可学习 = 强可学习"的等价猜想,Schapire 1990 用构造性算法证明。

AdaBoost(Adaptive Boosting, Freund & Schapire 1997)

经典 Boosting 算法,针对二分类 y∈{−1,+1}y \in \{-1, +1\}y∈{−1,+1}:

text 复制代码
输入: D = {(x_i, y_i)}, 学习器数 T, 基学习器算法 L
1. 初始化样本权重 w_i = 1/N
2. for t = 1, ..., T:
3.     用 D 及权重 w 训练基学习器 h_t
4.     计算带权训练误差: ε_t = Σ_{i: h_t(x_i) ≠ y_i} w_i
5.     计算学习器权重: α_t = 0.5 * ln((1 - ε_t) / ε_t)
6.     更新样本权重: w_i ← w_i * exp(-α_t y_i h_t(x_i))
7.     归一化: w_i ← w_i / Σ_j w_j
8. 输出: H(x) = sign(Σ_{t=1}^T α_t h_t(x))

关键机制

  • αt>0\alpha_t > 0αt>0:训练误差 ϵt<0.5\epsilon_t < 0.5ϵt<0.5(弱学习器略好于随机)。
  • wiw_iwi 更新:被错分样本的权重以 exp⁡(αt)\exp(\alpha_t)exp(αt) 增长;正确分样本的权重以 exp⁡(−αt)\exp(-\alpha_t)exp(−αt) 下降。
  • 后续学习器被迫关注难样本。

AdaBoost 的损失解释

AdaBoost 等价于带指数损失的顺序梯度下降:

L(y,H)=∑iexp⁡(−yiH(xi))L(y, H) = \sum_i \exp(-y_i H(x_i))L(y,H)=i∑exp(−yiH(xi))

其中 H(x)=∑tαtht(x)H(x) = \sum_t \alpha_t h_t(x)H(x)=∑tαtht(x)。每步 AdaBoost 在指数损失上做最速下降近似,hth_tht 选负梯度方向。

训练误差的指数下降

设每步 ϵt≤1/2−γ\epsilon_t \le 1/2 - \gammaϵt≤1/2−γ(γ>0\gamma > 0γ>0 为学习器优势),则训练误差:

errtrain(T)≤exp⁡(−2γ2T)\text{err}_{\text{train}}(T) \le \exp(-2 \gamma^2 T)errtrain(T)≤exp(−2γ2T)

指数下降!这是 AdaBoost "看起来不会过拟合" 的原因。

但 AdaBoost 仍会过拟合

实际中 AdaBoost 也会过拟合,特别是数据噪声大时:

  • 噪声样本永远被错分 → 权重指数增长 → 后续学习器被迫拟合噪声。
  • 解决方案:GentleBoost (用更小步长)、Regularized AdaBoost (限制样本权重上界)、BrownBoost(考虑噪声率)。

scikit-learn 实现

python 复制代码
from sklearn.ensemble import AdaBoostClassifier

clf = AdaBoostClassifier(
    estimator=DecisionTreeClassifier(max_depth=1),  # 弱学习器:树桩
    n_estimators=50,
    learning_rate=0.5,    # 缩放 α_t,控制每步步长
    random_state=0,
)
clf.fit(X_train, y_train)
  • estimator 默认 DecisionTreeClassifier(max_depth=1)(决策树桩,1 层的 CART)。
  • learning_rate 小 → 需更多学习器;learning_rate=1.0 是经典 AdaBoost 步长。
  • 可换 LogisticRegression 等作基学习器。

SAMME 与 SAMME.R

  • SAMME(Discrete AdaBoost):用类别预测,等价于上面伪代码的多分类推广。
  • SAMME.R:用类别概率(连续),更快收敛、更优结果。

sklearn 1.4 起 SAMME.R 被弃用,1.6 起已移除 :AdaBoostClassifier 统一使用 SAMME,algorithm 参数一并弃用(新代码无需传该参数)。

AdaBoost 的多分类扩展

  • SAMME :把 y∈{1,...,K}y \in \{1, \dots, K\}y∈{1,...,K} 编码为 −1/(K−1)-1/(K-1)−1/(K−1) 与 111(其余类),套二分类。
  • SAMME.R(旧版本):用 softmax 输出,更稳定。

AdaBoost 回归

AdaBoostRegressor:基学习器为回归树桩,损失为平方 / 线性 / 指数损失。

易错点

  • 基学习器过强 :用 max_depth=1(树桩);若用深树,每步几乎完美拟合 → 后续无提升空间。
  • 学习率与学习器数联动 :αt⋅lr\alpha_t \cdot \text{lr}αt⋅lr 决定每步贡献;lr 越小需越多 n_estimators,反之亦然。
  • 类别不平衡 :初始样本权重均匀,算法本身不感知类别比例------错分的少数类样本权重会上升,但这不保证整体对少数类友好;不平衡严重时用 sample_weight 显式调整。
  • 噪声数据:AdaBoost 对 label noise 极敏感(错分噪声样本权重爆炸);考虑用更鲁棒的变体。

2.2 GBDT

GBDT(Gradient Boosting Decision Tree, Friedman 1999)把 Boosting 推广到任意可微损失函数,是 AdaBoost 之后最广泛使用的 Boosting 框架。

核心思想

每步训练一棵 CART 去拟合当前模型的负梯度("伪残差"):

For t=1,...,T:\text{For } t = 1, \dots, T:For t=1,...,T:

ri(t)=−∂L(yi,F(xi))∂F(xi)F=Ft−1r_i^{(t)} = -\left\\frac{\\partial L(y_i, F(x_i))}{\\partial F(x_i)}\\right{F = F{t-1}}ri(t)=−∂F(xi)∂L(yi,F(xi))F=Ft−1

ht=CART(X,r(t))h_t = \text{CART}(X, r^{(t)})ht=CART(X,r(t))

Ft=Ft−1+η⋅htF_t = F_{t-1} + \eta \cdot h_tFt=Ft−1+η⋅ht

其中 FFF 是集成模型,η\etaη 是学习率(learning_rate),hth_tht 是该步训练的 CART。

与 AdaBoost 的对比

维度 AdaBoost GBDT
损失 指数(特定) 任意可微(平方、绝对、log、Hinge)
每步拟合 带权样本 负梯度
基学习器权重 由训练误差决定 由线搜索决定(shrinkage)
适用范围 分类为主 分类 + 回归 + 排序

回归任务

常用损失:

  • 平方损失 :L=12(y−F)2L = \frac{1}{2}(y - F)^2L=21(y−F)2 → 残差 r=y−Fr = y - Fr=y−F(残差即负梯度)。
  • 绝对损失 :L=∣y−F∣L = |y - F|L=∣y−F∣ → 残差 r=sign(y−F)r = \text{sign}(y - F)r=sign(y−F),对异常值鲁棒。
  • Huber 损失:综合两者优势。

分类任务

二分类 y∈{0,1}y \in \{0, 1\}y∈{0,1},用对数损失:

L=−ylog⁡p+(1−y)log⁡(1−p),p=σ(F)L = -y \\log p + (1-y) \\log(1-p), \quad p = \sigma(F)L=−ylogp+(1−y)log(1−p),p=σ(F)

负梯度 r=y−pr = y - pr=y−p(残差即类别概率差)。每步用 CART 拟合残差 rrr。

多分类用 softmax + 多类对数损失(见 §2.3)。

前向分步算法的视角

GBDT 是前向分步算法(Forward Stagewise Additive Modeling)的一个特例:每步加一棵新树使目标函数下降最多。

Shrinkage(步长收缩)

每步 Ft=Ft−1+η⋅htF_t = F_{t-1} + \eta \cdot h_tFt=Ft−1+η⋅ht,η∈(0,1]\eta \in (0, 1]η∈(0,1]。η\etaη 小 → 学得慢但更稳;η\etaη 大 → 学得快但易过拟合。Friedman 经验:η≤0.1\eta \le 0.1η≤0.1 时表现最好。

行采样与列采样(Subsampling)

随机取子样本/子特征训练每棵树,引入随机性:

  • 行采样 (subsample):每棵树用部分样本,类似 Bagging。
  • 列采样 (colsample_bytree):每棵树用部分特征,类似 RF。
  • 减小过拟合、加速训练。

scikit-learn 实现

python 复制代码
from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor

clf = GradientBoostingClassifier(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=3,           # 浅树(典型 3-8)
    subsample=1.0,         # 1.0 = 不采样;0.5 = 随机采样 50%
    random_state=0,
)
clf.fit(X_train, y_train)

GBDT 调参的"两难"

learning_rate 与 n_estimators 强相关:

  • 减小 η\etaη(如 0.01)需显著增加 n_estimators(如 1000+)才能达到同等效果。
  • η\etaη 大(如 0.3)需 n_estimators 小(30-50)否则过拟合。
  • 经验:先固定 η=0.1\eta = 0.1η=0.1 调 n_estimators,再微调。

GBDT 的早停(Early Stopping)

用验证集监控 loss,若连续若干步不下降则停训:

python 复制代码
clf = GradientBoostingClassifier(n_estimators=1000, validation_fraction=0.1,
                                  n_iter_no_change=10, tol=1e-4, random_state=0)
clf.fit(X_train, y_train)
print("实际迭代数:", clf.n_estimators_)

sklearn 自带 n_iter_no_change 机制;loss 参数自 1.2 起 "deviance" 更名为 "log_loss"(另支持 "exponential")。

多分类 GBDT

sklearn 的 GradientBoostingClassifier 多分类损失为 multinomial deviance(softmax):

  • 每轮训练 KKK 棵树(分别拟合各类的伪残差),共 T×KT \times KT×K 棵。
  • 预测用 softmax 聚合 KKK 棵树的输出。

在大数据上不如专用实现(XGBoost、LightGBM)高效。

GBDT 的优点与局限

维度 评价
准确率 极强,工业界广泛验证
偏差 显著降低
方差 略增,需 shrinkage + subsample 控制
训练速度 慢(序贯),sklearn 实现百兆数据已吃力
特征缩放 不需要
异常值 平方损失敏感,绝对损失或 Huber 鲁棒
解释性 SHAP 可解释每棵树的贡献
工程 sklearn 实现不支持 GPU,大数据用 XGBoost / LightGBM

易错点

  • 基学习器深度:常用 3-8;太深(> 10)易过拟合。
  • 学习率与树数 :n_estimators=100, learning_rate=0.1 是常用起点;不要把两者都设大。
  • 未做早停:可显著减少训练时间并防止过拟合。
  • 类别特征:sklearn GBDT 不支持类别特征直入,先编码(独热 / 目标)。
  • 不平衡数据 :用 sample_weight 或重采样;对数损失 + 阈值调整。

2.3 梯度提升实现

XGBoost(Chen & Guestrin 2016)、LightGBM(Ke et al. 2017)、CatBoost(Dorogush et al. 2018)是 GBDT 的工业级实现,在多数 Kaggle / 工业任务上仍是最强基线。

XGBoost(eXtreme Gradient Boosting)

核心改进:

  1. 二阶导数:用 Newton 步代替梯度下降,收敛更快。
  2. 正则化目标 :L+γ∣T∣+λ∥w∥2/2L + \gamma |T| + \lambda \|\mathbf{w}\|^2 / 2L+γ∣T∣+λ∥w∥2/2,∣T∣|T|∣T∣ 为叶节点数。
  3. 近似分箱:对特征做直方图化,近似找最优分裂点。
  4. 缺失值处理:训练时学得"默认方向",对缺失样本自动分流。
  5. 特征重要性:gain / weight / cover 三种。
  6. 列采样:类似 RF。
  7. 早停:内置。

XGBoost 接口

python 复制代码
import xgboost as xgb

dtrain = xgb.DMatrix(X_train, label=y_train)
dtest  = xgb.DMatrix(X_test, label=y_test)

params = {
    "objective": "binary:logistic",
    "max_depth": 6,
    "eta": 0.1,            # learning_rate
    "subsample": 0.8,
    "colsample_bytree": 0.8,
    "min_child_weight": 1,
    "gamma": 0,            # 分裂所需最小 loss 下降
    "reg_alpha": 0,        # L1
    "reg_lambda": 1,       # L2
    "tree_method": "hist", # 近似分箱
}
bst = xgb.train(params, dtrain, num_boost_round=200,
                 evals=[(dtest, "test")],
                 callbacks=[xgb.callback.EarlyStopping(rounds=10)])
print("best iteration:", bst.best_iteration)

也提供 sklearn 风格接口 xgb.XGBClassifier / xgb.XGBRegressor。

XGBoost 的 tree_method 与设备

  • "exact":精确贪心(仅小数据)。
  • "approx":近似分位数(支持大数据,单机)。
  • "hist":分箱直方图(推荐,CPU 极快)。
  • GPU 加速:XGBoost 2.0 起已移除 gpu_hist ,改用 tree_method="hist" + "device": "cuda"。

LightGBM(Light Gradient Boosting Machine, Microsoft)

核心改进(相对 XGBoost):

  1. 基于直方图的分裂(如 XGBoost hist)。
  2. Leaf-wise 生长:每次分裂当前增益最大的叶(vs XGBoost level-wise)。更深、可能过拟合。
  3. GOSS(Gradient-based One-Side Sampling):保留大梯度样本(未拟合好的),随机采样小梯度样本。
  4. EFB(Exclusive Feature Bundling):把稀疏互斥的特征合并成一列。
  5. 类别特征原生支持 :categorical_feature=[col_idx]。
  6. GPU 支持。

LightGBM 接口

python 复制代码
import lightgbm as lgb

dtrain = lgb.Dataset(X_train, label=y_train, categorical_feature=[cat_idx])
dtest  = lgb.Dataset(X_test, label=y_test, reference=dtrain)

params = {
    "objective": "binary",
    "learning_rate": 0.1,
    "num_leaves": 31,         # leaf-wise 树复杂度
    "max_depth": -1,
    "min_data_in_leaf": 20,
    "feature_fraction": 0.8,
    "bagging_fraction": 0.8,
    "bagging_freq": 5,
    "lambda_l1": 0,
    "lambda_l2": 1,
    "verbose": -1,
}
bst = lgb.train(params, dtrain, num_boost_round=200,
                valid_sets=[dtest], callbacks=[lgb.early_stopping(10)])

categorical_feature=[...] 原生支持类别特征(无需独热/目标编码)。LightGBM 同时支持 pandas category dtype(字符串取值也可)与整数索引。

CatBoost(Categorical Boosting, Yandex)

核心改进:

  1. 类别特征原生支持 (比 LightGBM 更强):用目标编码变种(Ordered Target Statistics / CTR)避免泄漏。
  2. 对称树(oblivious tree):每层分裂条件一致,便于预测加速。
  3. 有序 Boosting(Ordered Boosting):避免预测偏移(prediction shift)。

CatBoost 接口

python 复制代码
import catboost as cb

model = cb.CatBoostClassifier(
    iterations=200,
    learning_rate=0.1,
    depth=6,
    cat_features=[cat_col_names],  # 类别特征列名/索引
    verbose=False,
)
model.fit(X_train, y_train, eval_set=(X_val, y_val), early_stopping_rounds=10)

三大实现的对比

维度 XGBoost LightGBM CatBoost
直方图 ✓ ✓ ✓
类别特征 需编码 原生(较简单) 原生(最完善)
大数据速度 较快 最快 中等
内存 中 较低 中
缺失值 原生 原生 原生
GPU ✓(device="cuda") ✓ ✓
调参难度 中 中(num_leaves 等) 较低(默认好)
对类别不平衡 需手动 需手动 较鲁棒

目标函数(objective)

  • 回归:"reg:squarederror"、"reg:absoluteerror"、"reg:quantileerror"、"reg:pseudohubererror"。
  • 二分类:"binary:logistic"、"binary:hinge"。
  • 多分类:"multi:softprob"、"multi:softmax"。
  • 排序:"rank:pairwise"、"rank:ndcg"。

SHAP 与 GBDT

GBDT 类模型(XGBoost / LightGBM)天然支持 SHAP 解释:

python 复制代码
import shap
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)
shap.summary_plot(shap_values, X_test)  # 全局特征重要性
shap.force_plot(explainer.expected_value, shap_values[0, :], X_test.iloc[0])  # 单样本

SHAP 给出每个特征对每个预测的边际贡献,是 GBDT 类模型最常用的解释工具。

GBDT 的"工程经验法则"

  1. 数据 < 1 万:sklearn GBDT / RF。
  2. 数据 1 万 - 100 万:XGBoost / LightGBM。
  3. 数据 > 100 万:LightGBM(速度优势明显)。
  4. 类别特征多:CatBoost。
  5. 调参:先粗网格(max_depth, learning_rate, n_estimators),再细调正则与采样。

易错点

  • 调参顺序 :先 max_depth / num_leaves + learning_rate + n_estimators,再 subsample / colsample,最后 reg_alpha / reg_lambda。
  • n_estimators 设很大不早停:极慢且可能过拟合。
  • 类别特征独热后传 XGBoost:可工作但效果差;优先用目标编码或换 CatBoost/LightGBM。
  • 测试集分布偏移 :早停看的是验证集,不代表生产分布;生产监控更重要。
  • 特征尺度 :GBDT 对特征量级不敏感(树模型),无需标准化。

三、Stacking

3.1 堆叠泛化

Stacking(Wolpert 1992)是一种分层 集成:把多个基学习器的输出作为"元特征"训练一个元学习器,让元学习器学会如何最好地组合基学习器。

核心思想

  • 第 0 层 :训练多个异质基学习器(如 LR、SVM、RF、XGBoost)。
  • 第 1 层 :把基学习器在验证集上的预测作为新特征,训练元学习器。
  • 预测时先让基学习器各自预测,再让元学习器基于这些预测给出最终结果。

为何不直接平均/投票

平均/投票假设基学习器同等重要、错误模式相似。Stacking 让元学习器学到每个基学习器在什么情况下更可靠------比如 LR 在线性关系下可靠、RF 在非线性下可靠、GBDT 在带噪声下可靠。

两层 Stacking 的标准流程
#mermaid-svg-qtkFwV0bxQPhoHvg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qtkFwV0bxQPhoHvg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qtkFwV0bxQPhoHvg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qtkFwV0bxQPhoHvg .error-icon{fill:#552222;}#mermaid-svg-qtkFwV0bxQPhoHvg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qtkFwV0bxQPhoHvg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qtkFwV0bxQPhoHvg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qtkFwV0bxQPhoHvg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qtkFwV0bxQPhoHvg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qtkFwV0bxQPhoHvg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qtkFwV0bxQPhoHvg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qtkFwV0bxQPhoHvg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qtkFwV0bxQPhoHvg .marker.cross{stroke:#333333;}#mermaid-svg-qtkFwV0bxQPhoHvg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qtkFwV0bxQPhoHvg p{margin:0;}#mermaid-svg-qtkFwV0bxQPhoHvg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qtkFwV0bxQPhoHvg .cluster-label text{fill:#333;}#mermaid-svg-qtkFwV0bxQPhoHvg .cluster-label span{color:#333;}#mermaid-svg-qtkFwV0bxQPhoHvg .cluster-label span p{background-color:transparent;}#mermaid-svg-qtkFwV0bxQPhoHvg .label text,#mermaid-svg-qtkFwV0bxQPhoHvg span{fill:#333;color:#333;}#mermaid-svg-qtkFwV0bxQPhoHvg .node rect,#mermaid-svg-qtkFwV0bxQPhoHvg .node circle,#mermaid-svg-qtkFwV0bxQPhoHvg .node ellipse,#mermaid-svg-qtkFwV0bxQPhoHvg .node polygon,#mermaid-svg-qtkFwV0bxQPhoHvg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qtkFwV0bxQPhoHvg .rough-node .label text,#mermaid-svg-qtkFwV0bxQPhoHvg .node .label text,#mermaid-svg-qtkFwV0bxQPhoHvg .image-shape .label,#mermaid-svg-qtkFwV0bxQPhoHvg .icon-shape .label{text-anchor:middle;}#mermaid-svg-qtkFwV0bxQPhoHvg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qtkFwV0bxQPhoHvg .rough-node .label,#mermaid-svg-qtkFwV0bxQPhoHvg .node .label,#mermaid-svg-qtkFwV0bxQPhoHvg .image-shape .label,#mermaid-svg-qtkFwV0bxQPhoHvg .icon-shape .label{text-align:center;}#mermaid-svg-qtkFwV0bxQPhoHvg .node.clickable{cursor:pointer;}#mermaid-svg-qtkFwV0bxQPhoHvg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qtkFwV0bxQPhoHvg .arrowheadPath{fill:#333333;}#mermaid-svg-qtkFwV0bxQPhoHvg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qtkFwV0bxQPhoHvg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qtkFwV0bxQPhoHvg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qtkFwV0bxQPhoHvg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qtkFwV0bxQPhoHvg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qtkFwV0bxQPhoHvg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qtkFwV0bxQPhoHvg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qtkFwV0bxQPhoHvg .cluster text{fill:#333;}#mermaid-svg-qtkFwV0bxQPhoHvg .cluster span{color:#333;}#mermaid-svg-qtkFwV0bxQPhoHvg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qtkFwV0bxQPhoHvg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qtkFwV0bxQPhoHvg rect.text{fill:none;stroke-width:0;}#mermaid-svg-qtkFwV0bxQPhoHvg .icon-shape,#mermaid-svg-qtkFwV0bxQPhoHvg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qtkFwV0bxQPhoHvg .icon-shape p,#mermaid-svg-qtkFwV0bxQPhoHvg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qtkFwV0bxQPhoHvg .icon-shape .label rect,#mermaid-svg-qtkFwV0bxQPhoHvg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qtkFwV0bxQPhoHvg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qtkFwV0bxQPhoHvg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qtkFwV0bxQPhoHvg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 训练集 D
5 折划分
基学习器 1

LR
基学习器 2

RF
基学习器 3

XGBoost
元特征 1
元特征 2
元特征 3
元学习器

LR / LGBM
最终预测

关键:避免数据泄漏

直接把基学习器在训练集 上的预测作为元特征,会让元学习器"看"到训练集标签。正确做法是:

  1. 把训练集划分为 KKK 折。
  2. 对第 iii 折:基学习器在其余 K−1K-1K−1 折 上训练,预测第 iii 折的样本。
  3. 把所有 KKK 折的"留一"预测拼起来,得到每个训练样本的"无泄漏"基学习器预测。
  4. 元学习器在这些无泄漏预测上训练。

scikit-learn 实现

python 复制代码
from sklearn.ensemble import StackingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
import xgboost as xgb

estimators = [
    ("lr", LogisticRegression(max_iter=1000)),
    ("svm", SVC(probability=True)),
    ("rf", RandomForestClassifier(n_estimators=200, random_state=0)),
    ("xgb", xgb.XGBClassifier(n_estimators=200, learning_rate=0.1, random_state=0)),
]
stack = StackingClassifier(
    estimators=estimators,
    final_estimator=LogisticRegression(max_iter=1000),  # 元学习器
    cv=5,                # 内部 CV 产生元特征
    passthrough=False,   # 元学习器只看基学习器输出
    n_jobs=-1,
)
stack.fit(X_train, y_train)

关键参数

  • cv=5:内部 CV 折数(越大越稳但越慢)。
  • final_estimator:元学习器。简单问题用 LR,复杂问题可用 LightGBM。
  • passthrough=False:元学习器只看基学习器输出;True 时还看原始特征。
  • stack_method="auto"(默认)/ "predict_proba":auto 模式下若基学习器提供 predict_proba 则自动用概率,否则用类别。predict_proba 是更常用的人工推荐。

Stacking 的层次扩展

  • 多层 Stacking:基学习器 → 一层元学习器 → 二层元学习器......3 层以上工程上少见。
  • 多输出 Stacking :每个基学习器输出多类(predict_proba),元学习器接全部。

元学习器的选择

元学习器 适用 风险
LR 基学习器数 ≤ 5、差异显著 线性组合可能不够
简单线性回归(stacking regression) 回归问题 同上
岭回归 防止过拟合 失去稀疏性
LightGBM 复杂非线性元映射 易过拟合元特征
简单平均 / 加权平均 强基学习器很多时 退化为简单集成

硬投票与软投票(Voting)

比 Stacking 更简单的组合方式:sklearn.ensemble.VotingClassifier 的 voting="hard" 做多数投票,voting="soft" 平均各基学习器的 predict_proba。软投票通常优于硬投票;它是"不想引入元学习器时的默认选择",效果上限接近加权平均------可视为 Stacking 的退化形式(元学习器 = 等权)。

python 复制代码
from sklearn.ensemble import VotingClassifier

vote = VotingClassifier(
    estimators=[("lr", clf1), ("rf", clf2), ("svc", clf3)],
    voting="soft",
).fit(X_train, y_train)

与 Blending 的对比

  • Blending:用 holdout 集(如 10% 训练)做元特征;比 Stacking 快,但样本利用率低。
  • Stacking:用 K 折 CV 做元特征;样本利用率高(每个训练样本都参与元特征生成),但更慢。

sklearn 用 K 折实现,是 Stacking。

Stacking 的常见陷阱

  1. 基学习器太相似 :全用 GBDT 不同超参 → 高度相关,元学习器难学到差异。应选异质学习器(线性 + 树 + 核方法)。
  2. 元学习器过拟合 :元特征可能高度相关、维度有限;元学习器必须简单(如 LR、岭回归)。
  3. 泄漏 :用 cross_val_predict 而非直接 predict 生成元特征------sklearn 内部已处理。
  4. 计算代价 :基学习器 + 元学习器 + 内部 CV,O(M×K)O(M \times K)O(M×K) 倍开销。

Stacking vs 加权平均

  • 加权平均:基学习器权重由验证集表现或超参决定,简单。
  • Stacking :权重由元学习器从数据中学得,更灵活但更易过拟合。

对小数据(N<1000N < 1000N<1000)Stacking 收益有限;中大数据(N≥104N \ge 10^4N≥104)才有稳定提升。

与级联(Cascading)的区别

  • 级联(如 Viola-Jones):把上一个学习器的"难样本"传给下一个,顺序分类。
  • Stacking :把多个学习器的预测作为新特征给元学习器。

两者都属分层集成,但结构不同。

scikit-learn 的 StackingRegressor

python 复制代码
from sklearn.ensemble import StackingRegressor
stack = StackingRegressor(
    estimators=[("rf", RandomForestRegressor()), ("gbr", GradientBoostingRegressor())],
    final_estimator=Ridge(),
    cv=5,
)

易错点

  • 基学习器选同质 (如 5 个 GBDT 不同超参):集成提升有限。混搭异质(LR + RF + SVM + GBDT)才有 Stacking 的价值。
  • 元学习器过复杂:用 LR 或 Ridge;用 LightGBM 易过拟合元特征。
  • passthrough=True 引入原始特征:会让元学习器把原始特征当主、基学习器当辅;不一定更好。
  • 未做 CV 直接 stacking :会引入严重数据泄漏。sklearn 内部已用 K 折,但用 cross_val_predict 自实现时务必注意。
  • 基学习器与元学习器用同一份数据:必须内部 CV 隔离。
相关推荐
Shulex4 小时前
2026 出海企业 AI 客服选型指南:智齿、七鱼与 Shulex,谁是跨境业务的真解?
人工智能·智能客服·跨境电商
pigdreams4 小时前
Google 官方 Android CLI:让AI直接读布局结构而不是用adb截图验证
android·人工智能·adb
Highcharts.js4 小时前
2026年,AI正在重写技术:“工具思维”正在失效?
人工智能·echarts·客户端·highcharts·可视化图表·企业级图表
林伽一4 小时前
训练断网、部署上门槛:AI 基础设施的两道新闸门|2026年10月25日
人工智能·科技·安全·ai·1024程序员节
在所不辞兄4 小时前
【零基础学智能仿真-62】从应变—应力计算弹性常数
人工智能·深度学习·神经网络·机器学习·有限元·智能仿真
二宝哥4 小时前
02.springboot项目对接国内模型实现对话聊天
数据库·人工智能·spring boot
kaixin_啊啊4 小时前
【零基础学AI】第 17 章——一个机器学习项目怎样完成
人工智能·机器学习
Elastic 中国社区官方博客5 小时前
5 分钟内将 56 页扫描文档转化为带引用的答案:面向搜索无法读取的文档的检索增强生成流水线
大数据·运维·人工智能·elasticsearch·搜索引擎·信息可视化·全文检索
天天代码码天天5 小时前
我用 C# 做了个运动计数器:打开摄像头,让电脑帮你数次数
人工智能