目录
引言:为什么需要 Stacking?
在机器学习竞赛(如 Kaggle)和工业实践中,集成学习(Ensemble Learning) 已成为提升模型性能的标准手段。传统的集成方法如 Bagging(随机森林)和 Boosting(XGBoost、LightGBM)通过组合同质基学习器来降低方差或偏差。
然而,当面对异质模型(如神经网络、梯度提升树、支持向量机的组合)时,简单的投票(Voting)或平均(Averaging)往往无法充分挖掘各模型的互补性。Stacking(Stacked Generalization,堆叠泛化) 应运而生,它通过引入元学习器(Meta-Learner) 来学习如何最优地组合基学习器的预测,实现了从"简单平均"到"智能加权"的跃迁。
核心洞见: Stacking 的本质是将模型预测作为特征,构建更高层次的特征表示,再由元学习器进行最终决策。
Stacking 的核心概念
2.1 定义
Stacking 是一种两层(或多层)的集成学习架构,由 Wolpert 于 1992 年在论文 “Stacked Generalization” 中首次提出。其核心思想是:
- 第一层(Level-0): 训练多个 diverse 的基学习器(Base Learners)
- 第二层(Level-1): 使用基学习器的输出作为输入特征,训练一个元学习器(Meta-Learner)进行最终预测
2.2 与相关概念的区分
| Voting/Averaging | 固定权重(等权或手工设定) | 无学习 | 支持 |
| Weighted Averaging | 优化权重(如通过验证集性能) | 浅层优化 | 支持 |
| Stacking | 元学习器动态学习组合策略 | 深层学习 | 支持 |
| Blending | 使用 hold-out 验证集训练元学习器 | 单层学习 | 支持 |
| Boosting | 序列训练,关注错误样本 | 自适应重加权 | 通常同质 |
关键区别: Stacking 通过学习来发现基学习器之间的非线性交互,而非预设组合规则。
算法原理与数学形式化
3.1 符号定义
设训练集为
D
=
{
(
x
i
,
y
i
)
}
i
=
1
N
\\mathcal{D} = \\{(\\mathbf{x}_i, y_i)\\}_{i=1}^N
D={(xi,yi)}i=1N,其中
x
i
∈
R
d
\\mathbf{x}_i \\in \\mathbb{R}^d
xi∈Rd,
y
i
∈
Y
y_i \\in \\mathcal{Y}
yi∈Y。
- 基学习器集合:
M
=
{
M
1
,
M
2
,
…
,
M
K
}
\\mathcal{M} = \\{M_1, M_2, \\dots, M_K\\}
M={M1,M2,…,MK} - 基学习器
M
k
M_k
Mk 的预测:f
k
(
x
)
∈
R
C
f_k(\\mathbf{x}) \\in \\mathbb{R}^{C}
fk(x)∈RC(C
C
C 为类别数,回归时C
=
1
C=1
C=1) - 元学习器:
M
m
e
t
a
M_{meta}
Mmeta
3.2 训练阶段
Step 1: 生成元特征(Meta-Features)
为避免信息泄露(Data Leakage),必须使用交叉验证生成元特征:
对于每个基学习器
M
k
M_k
Mk,进行
T
T
T-折交叉验证:
- 将训练集划分为
T
T
T 折:D
=
⋃
t
=
1
T
D
t
v
a
l
\\mathcal{D} = \\bigcup_{t=1}^T \\mathcal{D}_t^{val}
D=⋃t=1TDtval - 对第
t
t
t 折,使用D
∖
D
t
v
a
l
\\mathcal{D} \\setminus \\mathcal{D}_t^{val}
D∖Dtval 训练M
k
(
t
)
M_k^{(t)}
Mk(t) - 对
D
t
v
a
l
\\mathcal{D}_t^{val}
Dtval 中的样本x
i
\\mathbf{x}_i
xi,生成预测f
k
(
t
)
(
x
i
)
f_k^{(t)}(\\mathbf{x}_i)
fk(t)(xi)
最终,样本
x
i
\\mathbf{x}_i
xi 的元特征向量为:
z
i
=
[
f
1
(
x
i
)
,
f
2
(
x
i
)
,
…
,
f
K
(
x
i
)
]
⊤
∈
R
K
⋅
C
\\mathbf{z}_i = [f_1(\\mathbf{x}_i), f_2(\\mathbf{x}_i), \\dots, f_K(\\mathbf{x}_i)]^\\top \\in \\mathbb{R}^{K \\cdot C}
zi=[f1(xi),f2(xi),…,fK(xi)]⊤∈RK⋅C
Step 2: 训练元学习器
构建元训练集
D
m
e
t
a
=
{
(
z
i
,
y
i
)
}
i
=
1
N
\\mathcal{D}_{meta} = \\{(\\mathbf{z}_i, y_i)\\}_{i=1}^N
Dmeta={(zi,yi)}i=1N,训练:
M
m
e
t
a
:
R
K
⋅
C
→
Y
M_{meta}: \\mathbb{R}^{K \\cdot C} \\to \\mathcal{Y}
Mmeta:RK⋅C→Y
3.3 预测阶段
对于新样本
x
\\mathbf{x}
x:
{
M
k
∗
}
k
=
1
K
\\{M_k^*\\}_{k=1}^K
{Mk∗}k=1K
z
=
[
M
1
∗
(
x
)
,
…
,
M
K
∗
(
x
)
]
⊤
\\mathbf{z} = [M_1^*(\\mathbf{x}), \\dots, M_K^*(\\mathbf{x})]^\\top
z=[M1∗(x),…,MK∗(x)]⊤
y
^
=
M
m
e
t
a
∗
(
z
)
\\hat{y} = M_{meta}^*(\\mathbf{z})
y^=Mmeta∗(z)
3.4 数学优化视角
Stacking 可视为最小化泛化误差的优化问题:
min
M
m
e
t
a
E
(
x
,
y
)
∼
P
[
L
(
y
,
M
m
e
t
a
(
f
1
(
x
)
,
…
,
f
K
(
x
)
)
)
]
\\min_{M_{meta}} \\mathbb{E}_{(\\mathbf{x},y) \\sim \\mathcal{P}} \\left[ \\mathcal{L}\\left(y, M_{meta}(f_1(\\mathbf{x}), \\dots, f_K(\\mathbf{x}))\\right) \\right]
MmetaminE(x,y)∼P[L(y,Mmeta(f1(x),…,fK(x)))]
其中
L
\\mathcal{L}
L 为损失函数。元学习器学习的是条件期望的估计:
M
m
e
t
a
∗
(
z
)
≈
E
[
Y
∣
Z
=
z
]
M_{meta}^*(\\mathbf{z}) \\approx \\mathbb{E}[Y | Z=\\mathbf{z}]
Mmeta∗(z)≈E[Y∣Z=z]
完整的 Stacking 流程
┌─────────────────────────────────────────────────────────────┐
│ Stacking 架构示意图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Level-0 (Base Learners) Level-1 (Meta-Learner) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Model 1 │ │ Model 2 │ │ Model K │ ┌─────────┐ │
│ │ (RF) │ │ (LGB) │ │ (NN) │───────▶│ LR/ │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │ Ridge │ │
│ │ │ │ └────┬────┘ │
│ └────────────┴────────────┘ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────┐ │
│ │ Meta-Features │ │ Final │ │
│ │ (Z-matrix) │──────────────▶│ Output │ │
│ └─────────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
4.1 算法流程
训练阶段:
# 伪代码表示
def train_stacking(X_train, y_train, base_models, meta_model, n_folds=5):
# 1. 初始化元特征矩阵
n_samples = len(X_train)
n_models = len(base_models)
Z = np.zeros((n_samples, n_models)) # 元特征矩阵
# 2. 交叉验证生成元特征
kfold = KFold(n_splits=n_folds, shuffle=True)
for i, model in enumerate(base_models):
for train_idx, val_idx in kfold.split(X_train):
# 划分数据
X_tr, X_val = X_train[train_idx], X_train[val_idx]
y_tr = y_train[train_idx]
# 训练基学习器并预测验证集
model.fit(X_tr, y_tr)
Z[val_idx, i] = model.predict(X_val)
# 3. 在完整数据上重新训练基学习器(用于后续预测)
fitted_base_models = []
for model in base_models:
model_copy = clone(model)
model_copy.fit(X_train, y_train)
fitted_base_models.append(model_copy)
# 4. 训练元学习器
meta_model.fit(Z, y_train)
return fitted_base_models, meta_model
预测阶段:
def predict_stacking(X_new, fitted_base_models, meta_model):
# 1. 生成元特征
Z_new = np.column_stack([
model.predict(X_new) for model in fitted_base_models
])
# 2. 元学习器预测
return meta_model.predict(Z_new)
关键设计决策
5.1 基学习器的选择:多样性与准确性的权衡
多样性(Diversity) 是 Stacking 成功的关键。理想情况下,基学习器应:
- 算法不同: 树模型、线性模型、神经网络、KNN 等
- 数据视角不同: 不同特征子集、不同样本权重
- 超参数不同: 同一算法的不同配置
常用组合:
- 经典组合: Random Forest + Gradient Boosting + SVM + Neural Network
- 竞赛组合: XGBoost + LightGBM + CatBoost + Neural Network
- 深度学习组合: ResNet + EfficientNet + Vision Transformer
5.2 元学习器的选择
| 逻辑回归/线性回归 | 分类/回归基准 | 简单、可解释、不易过拟合 | 只能学习线性组合 |
| Ridge/Lasso | 多重共线性严重 | 正则化防止过拟合 | 仍是线性模型 |
| 梯度提升树 | 需要非线性交互 | 学习复杂模式 | 容易过拟合 |
| 神经网络 | 大规模数据 | 表达能力强 | 需要大量数据、调参复杂 |
| 随机森林 | 鲁棒性要求高 | 抗噪声 | 可解释性较差 |
推荐实践: 从简单的 Ridge 回归(回归任务)或 逻辑回归(分类任务)开始,逐步尝试更复杂的元学习器。
5.3 交叉验证策略
必须避免: 使用训练集预测训练集自身(In-sample prediction),这会导致严重的信息泄露和过拟合。
推荐策略:
- K-Fold CV: 标准选择,
K
=
5
K=5
K=5 或10
10
10 - Stratified K-Fold: 分类任务中保持类别比例
- Time Series Split: 时序数据避免未来信息泄露
5.4 特征增强策略
除了基学习器的预测概率/值,元特征还可包括:
- 原始特征: 将
x
\\mathbf{x}
x 与z
\\mathbf{z}
z 拼接,形成(
x
,
z
)
(\\mathbf{x}, \\mathbf{z})
(x,z) - 统计特征: 基学习器预测的标准差、最大值、最小值
- 高阶交互: 基学习器预测的乘积、比值
变体与演进
6.1 Blending
Blending 是 Stacking 的简化版本,使用固定的 hold-out 验证集生成元特征,而非交叉验证。
优点: 实现简单、计算成本低 缺点: 元学习器训练数据较少、验证集划分敏感、信息利用不充分
适用: 快速原型验证或数据量极大的场景。
6.2 Multi-Level Stacking
将 Stacking 扩展到多层:
Level-0: 基学习器 (RF, GBDT, NN)
↓
Level-1: 元学习器1 (RF, GBDT) ← 可多个
↓
Level-2: 元学习器2 (LR) ← 最终输出
风险: 随着层数增加,过拟合风险急剧上升,可解释性下降。
6.3 Feature-Weighted Linear Stacking (FWLS)
Netflix 竞赛中提出的变体,元学习器为:
y
^
=
∑
k
=
1
K
w
k
(
x
)
⋅
f
k
(
x
)
\\hat{y} = \\sum_{k=1}^K w_k(\\mathbf{x}) \\cdot f_k(\\mathbf{x})
y^=k=1∑Kwk(x)⋅fk(x)
其中权重
w
k
(
x
)
w_k(\\mathbf{x})
wk(x) 依赖于原始特征
x
\\mathbf{x}
x,通过另一组模型学习。
6.4 深度学习中的 Stacking
在现代深度学习中,Stacking 思想演变为:
- Mixture of Experts (MoE): 门控网络动态选择专家
- Ensemble Distillation: 将 Stacking 集成知识蒸馏到单一模型
- Stacked Encoders: 自编码器的逐层堆叠预训练
常见问题
| 信息泄露 | 训练集准确率极高,测试集极差 | 严格使用交叉验证生成元特征 |
| 同质化基学习器 | Stacking 效果不如单一最佳模型 | 确保基学习器多样性(相关性 < 0.9) |
| 元学习器过拟合 | 元特征维度高、样本少 | 使用简单元学习器、增加正则化 |
| 数据划分不一致 | 时序数据泄露、类别不平衡 | 使用 StratifiedKFold、TimeSeriesSplit |
| 计算资源爆炸 | 多层 Stacking 训练时间过长 | 限制层数、使用并行训练 |
代码示例
8.1 使用 Scikit-learn 实现
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import numpy as np
# 加载数据
data = load_breast_cancer()
X, y = data.data, data.target
# 定义基学习器
base_learners = [
('rf', RandomForestClassifier(n_estimators=100, random_state=42)),
('gbdt', GradientBoostingClassifier(n_estimators=100, random_state=42)),
('svc', Pipeline([
('scaler', StandardScaler()),
('svc', SVC(probability=True, random_state=42))
]))
]
# 元学习器
meta_learner = LogisticRegression(C=1.0, solver='lbfgs', max_iter=1000)
# Stacking 实现
class StackingClassifier:
def __init__(self, base_learners, meta_learner, n_folds=5):
self.base_learners = base_learners
self.meta_learner = meta_learner
self.n_folds = n_folds
self.base_models_ = []
def fit(self, X, y):
n_samples = X.shape[0]
n_base = len(self.base_learners)
# 初始化元特征矩阵 (使用概率预测)
self.n_classes_ = len(np.unique(y))
Z = np.zeros((n_samples, n_base * self.n_classes_))
# 交叉验证生成元特征
kfold = StratifiedKFold(n_splits=self.n_folds, shuffle=True, random_state=42)
for i, (name, model) in enumerate(self.base_learners):
print(f"Training base learner: {name}")
# 存储每折训练的模型
fold_models = []
for train_idx, val_idx in kfold.split(X, y):
X_train, X_val = X[train_idx], X[val_idx]
y_train = y[train_idx]
model_copy = clone(model)
model_copy.fit(X_train, y_train)
# 预测概率作为元特征
probs = model_copy.predict_proba(X_val)
Z[val_idx, i*self.n_classes_:(i+1)*self.n_classes_] = probs
fold_models.append(model_copy)
# 在完整数据上重新训练,用于后续预测
full_model = clone(model)
full_model.fit(X, y)
self.base_models_.append(full_model)
# 训练元学习器
print("Training meta-learner…")
self.meta_learner.fit(Z, y)
return self
def predict(self, X):
# 生成元特征
Z = self._generate_meta_features(X)
return self.meta_learner.predict(Z)
def predict_proba(self, X):
Z = self._generate_meta_features(X)
return self.meta_learner.predict_proba(Z)
def _generate_meta_features(self, X):
n_samples = X.shape[0]
n_base = len(self.base_learners)
Z = np.zeros((n_samples, n_base * self.n_classes_))
for i, model in enumerate(self.base_models_):
probs = model.predict_proba(X)
Z[:, i*self.n_classes_:(i+1)*self.n_classes_] = probs
return Z
# 使用示例
from sklearn.base import clone
from sklearn.metrics import accuracy_score
stacking_clf = StackingClassifier(base_learners, meta_learner, n_folds=5)
stacking_clf.fit(X, y)
# 交叉验证评估
scores = cross_val_score(stacking_clf, X, y, cv=5, scoring='accuracy')
print(f"Stacking CV Accuracy: {scores.mean():.4f} (+/- {scores.std():.4f})")
# 对比单个模型
for name, model in base_learners:
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"{name}: {scores.mean():.4f}")
8.2 使用 ML-Ensemble 库(生产环境)
# pip install mlens
from mlens.ensemble import SuperLearner
from sklearn.linear_model import Ridge
from sklearn.ensemble import RandomForestRegressor
from xgboost import XGBRegressor
import numpy as np
# 构建 Super Learner (Stacking 的一种实现)
ensemble = SuperLearner(
folds=5, # 5折交叉验证
random_state=42,
verbose=2,
backend="multiprocessing" # 并行训练
)
# 添加第一层(基学习器)
ensemble.add([
RandomForestRegressor(n_estimators=100),
XGBRegressor(n_estimators=100),
Ridge()
])
# 添加第二层(元学习器)
ensemble.add_meta(Ridge())
# 训练与预测
ensemble.fit(X_train, y_train)
predictions = ensemble.predict(X_test)






