一、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=−ylogp+(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)
核心改进:
- 二阶导数:用 Newton 步代替梯度下降,收敛更快。
- 正则化目标 :L+γ∣T∣+λ∥w∥2/2L + \gamma |T| + \lambda \|\mathbf{w}\|^2 / 2L+γ∣T∣+λ∥w∥2/2,∣T∣|T|∣T∣ 为叶节点数。
- 近似分箱:对特征做直方图化,近似找最优分裂点。
- 缺失值处理:训练时学得"默认方向",对缺失样本自动分流。
- 特征重要性:gain / weight / cover 三种。
- 列采样:类似 RF。
- 早停:内置。
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):
- 基于直方图的分裂(如 XGBoost hist)。
- Leaf-wise 生长:每次分裂当前增益最大的叶(vs XGBoost level-wise)。更深、可能过拟合。
- GOSS(Gradient-based One-Side Sampling):保留大梯度样本(未拟合好的),随机采样小梯度样本。
- EFB(Exclusive Feature Bundling):把稀疏互斥的特征合并成一列。
- 类别特征原生支持 :
categorical_feature=[col_idx]。 - 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)
核心改进:
- 类别特征原生支持 (比 LightGBM 更强):用目标编码变种(Ordered Target Statistics / CTR)避免泄漏。
- 对称树(oblivious tree):每层分裂条件一致,便于预测加速。
- 有序 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 万:sklearn GBDT / RF。
- 数据 1 万 - 100 万:XGBoost / LightGBM。
- 数据 > 100 万:LightGBM(速度优势明显)。
- 类别特征多:CatBoost。
- 调参:先粗网格(
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
最终预测
关键:避免数据泄漏
直接把基学习器在训练集 上的预测作为元特征,会让元学习器"看"到训练集标签。正确做法是:
- 把训练集划分为 KKK 折。
- 对第 iii 折:基学习器在其余 K−1K-1K−1 折 上训练,预测第 iii 折的样本。
- 把所有 KKK 折的"留一"预测拼起来,得到每个训练样本的"无泄漏"基学习器预测。
- 元学习器在这些无泄漏预测上训练。
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 的常见陷阱
- 基学习器太相似 :全用 GBDT 不同超参 → 高度相关,元学习器难学到差异。应选异质学习器(线性 + 树 + 核方法)。
- 元学习器过拟合 :元特征可能高度相关、维度有限;元学习器必须简单(如 LR、岭回归)。
- 泄漏 :用
cross_val_predict而非直接predict生成元特征------sklearn 内部已处理。 - 计算代价 :基学习器 + 元学习器 + 内部 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 隔离。