集成方法:把弱学习器拧成强学习器


文档摘要

集成方法:把弱学习器拧成强学习器 本节摘要:一群弱学习器,组合得当,就变成一个强学习器——这不是比喻,是一个定理。集成方法(Ensemble Methods)是表格数据上最可靠的技术:它横扫 Kaggle 表格赛,驱动着多数生产 ML 系统,并直接展示了偏差方差权衡在行动中如何运作。其核心机制只有三条:Bagging(自助聚合)通过在不同数据子集上训练多个模型再平均预测来降方差;Boosting(提升)通过顺序建模、每个新模型专攻集成当前的错误来降偏差;Stacking(堆叠)用一个元学习器学习「在哪种输入上该信哪个模型」。

集成方法:把弱学习器拧成强学习器

本节摘要:一群弱学习器,组合得当,就变成一个强学习器——这不是比喻,是一个定理。集成方法(Ensemble Methods)是表格数据上最可靠的技术:它横扫 Kaggle 表格赛,驱动着多数生产 ML 系统,并直接展示了偏差方差权衡在行动中如何运作。其核心机制只有三条:Bagging(自助聚合)通过在不同数据子集上训练多个模型再平均预测来降方差;Boosting(提升)通过顺序建模、每个新模型专攻集成当前的错误来降偏差;Stacking(堆叠)用一个元学习器学习「在哪种输入上该信哪个模型」。本节将推导「多数投票为何会提升准确率」(关键要求是多样性),从零实现 AdaBoost 和梯度提升,讲透 XGBoost 为何在表格数据上常年压制神经网络,并给出「该用哪种集成」的决策表。

学习目标

阅读完本节,你应当能够:

  1. 从零实现 AdaBoost 和梯度提升,解释 Boosting 如何顺序地降偏差。
  2. 构建 Bagging 集成,演示平均「去相关模型」如何在几乎不增偏差的前提下降方差。
  3. 对比 Bagging、Boosting、Stacking 三者各自瞄准的误差分量。
  4. 评估集成多样性,解释为何多数投票准确率会随独立弱学习器数量增加而提升。

一、问题与直觉

一棵单独的决策树训练快、好解释,但它过拟合。一个单独的线性模型在复杂边界上欠拟合。你可以花几天去雕琢完美的模型架构。或者,你可以把一堆不完美的模型组合起来,得到比任何一个都更好的东西。

集成方法做的就是这件事。它的核心直觉来自一个数学事实:假设你有 N 个相互独立的分类器,每个准确率 p > 0.5。多数投票的准确率是:

P(多数正确) = 对 k > N/2 求和 C(N,k) * p^k * (1-p)^(N-k)

21 个准确率各为 60% 的分类器,多数投票准确率约 74%。101 个分类器则升到 84%。当模型犯不同的错误时,错误相互抵消。

关键要求是多样性。如果所有模型犯同样的错,组合它们毫无帮助。集成方法之所以有效,是因为它们通过不同方式制造多样化:

  • 不同的训练子集(Bagging)
  • 不同的特征子集(随机森林)
  • 顺序的错误纠正(Boosting)
  • 不同的模型族(Stacking)

💡 「集成」是赢得 Kaggle 表格赛最可靠的技术,也是多数生产 ML 系统背后的引擎。在表格数据上,梯度提升几乎总是强于神经网络。

Bagging(自助聚合)

Bagging 通过在训练数据的不同自助样本上训练每个模型来制造多样性。

自助样本是从原始数据有放回抽取的,大小与原始数据相同。每个自助样本里约 63.2% 是不重复的样本。剩下的 36.8%(袋外样本,out-of-bag)提供了一个免费的验证集。

Bagging 降方差而几乎不增偏差。每棵单独的树都过拟合自己的自助样本,但每个的过拟合方式不同,平均就把噪声抵消了。

随机森林(Random Forests) 是 Bagging 加一个额外花样:在每次分裂时,只考虑特征的随机子集。这强制树之间更加多样。候选特征数的典型取值是分类用 sqrt(n_features),回归用 n_features / 3

Boosting(顺序错误纠正)

Boosting 顺序训练模型。每个新模型聚焦于前一些模型搞错的样本。

Boosting 降偏差。每个新模型纠正集成到目前为止的系统性错误。最终预测是所有模型的加权和,更好的模型获得更高权重。

代价是:Boosting 跑太多轮会过拟合,因为它持续拟合更难的样本,其中有些是噪声。

AdaBoost

AdaBoost(Adaptive Boosting,自适应提升)是第一个实用的提升算法。它能配合任何基学习器,通常用决策树桩(depth-1 的树)。算法如下:

1. 初始化样本权重:对所有 i,w_i = 1/N 2. 对 t = 1 到 T: a. 在带权重数据上训练弱学习器 h_t b. 计算加权错误率: err_t = sum(w_i * I(h_t(x_i) != y_i)) / sum(w_i) c. 计算模型权重: alpha_t = 0.5 * ln((1 - err_t) / err_t) d. 更新样本权重: w_i = w_i * exp(-alpha_t * y_i * h_t(x_i)) e. 归一化权重使其和为 1 3. 最终预测:H(x) = sign(sum(alpha_t * h_t(x)))

错误率低的模型得到更高的 alpha。被错分的样本得到更高权重,于是下一个模型聚焦它们。

梯度提升(Gradient Boosting)

梯度提升把提升推广到任意损失函数。它不再重加权样本,而是让每个新模型去拟合当前集成的残差(损失的负梯度)。

1. 初始化:F_0(x) = argmin_c sum(L(y_i, c)) 2. 对 t = 1 到 T: a. 计算伪残差: r_i = -dL(y_i, F_{t-1}(x_i)) / dF_{t-1}(x_i) b. 用树 h_t 拟合残差 r_i c. 找最优步长: gamma_t = argmin_gamma sum(L(y_i, F_{t-1}(x_i) + gamma * h_t(x_i))) d. 更新: F_t(x) = F_{t-1}(x) + learning_rate * gamma_t * h_t(x) 3. 最终预测:F_T(x)

对平方误差损失,伪残差就是真实残差:r_i = y_i - F_{t-1}(x_i)。每棵树字面意义上在拟合前一个集成的错误。

学习率(收缩,shrinkage)控制每棵树的贡献。更小的学习率需要更多树但泛化更好。典型取值:0.01 到 0.3。

XGBoost:为何它统治表格数据

XGBoost(eXtreme Gradient Boosting,极端梯度提升)是梯度提升加一系列工程优化,使它又快又准又抗过拟合:

  • 正则化目标:对叶权重加 L1 和 L2 惩罚,防止单棵树过度自信。
  • 二阶近似:同时用损失的一阶和二阶导数,给出更好的分裂决策。
  • 稀疏感知分裂:原生处理缺失值,在每个分裂点学习缺失数据的最佳走向。
  • 列子采样:像随机森林那样,在每个分裂点采样特征以增加多样性。
  • 加权分位数草图:在分布式数据上高效地为连续特征找分裂点。
  • 缓存感知的块结构:针对 CPU 缓存行优化的内存布局。

对表格数据,XGBoost(及其后继 LightGBM)持续压制神经网络。这种局面短期内不会变。如果你的数据能装进一张行列分明的表里,先从梯度提升开始。

Stacking(元学习)

Stacking 用多个基模型的预测作为元学习器的特征。

元学习器学习「在哪种输入上该信哪个基模型」。如果随机森林在某些区域更好、SVM 在另一些区域更好,元学习器会学会相应地路由。

为避免数据泄漏,基模型的预测必须通过在训练集上交叉验证生成。永远不要在同一份数据上既训练基模型又生成元特征。

投票(Voting)

最简单的集成。直接组合预测。

  • 硬投票:对类别标签做多数投票。
  • 软投票:平均预测概率,选平均概率最高的类。通常更好,因为它利用了置信度信息。

该用哪种集成

方法 降低 最适合 当心
Bagging / 随机森林 方差 噪声数据,多特征 对偏差无帮助
AdaBoost 偏差 干净数据,简单基学习器 对离群点和噪声敏感
梯度提升 偏差 表格数据,竞赛 训练慢,不调参易过拟合
XGBoost / LightGBM 两者 生产表格 ML 超参数多
Stacking 两者 抠最后 1~2% 准确率 复杂,元学习器有过拟合风险
投票 方差 快速组合多样模型 只在模型多样时有用

表格数据的生产技术栈

对多数表格预测问题,尝试顺序是:

  1. LightGBM 或 XGBoost 用默认参数。
  2. n_estimatorslearning_ratemax_depthmin_child_weight
  3. 若要抠最后 0.5%,用 3~5 个多样模型搭 Stacking。
  4. 全程用交叉验证。

表格数据上的神经网络几乎总是不如梯度提升,尽管研究持续在尝试。TabNet、NODE 等架构偶尔能持平,但很少击败调好的 XGBoost。

二、从零实现

code/ensembles.py 从零实现全部内容。逐步来看。

第 1 步:决策树桩(基学习器)

class DecisionStump: def __init__(self): self.feature_idx = None self.threshold = None self.polarity = 1 self.alpha = None def fit(self, X, y, weights): n_samples, n_features = X.shape best_error = float("inf") for f in range(n_features): thresholds = np.unique(X[:, f]) for thresh in thresholds: for polarity in [1, -1]: pred = np.ones(n_samples) pred[polarity * X[:, f] < polarity * thresh] = -1 error = np.sum(weights[pred != y]) if error < best_error: best_error = error self.feature_idx = f self.threshold = thresh self.polarity = polarity def predict(self, X): n = X.shape[0] pred = np.ones(n) idx = self.polarity * X[:, self.feature_idx] < self.polarity * self.threshold pred[idx] = -1 return pred

第 2 步:从零实现 AdaBoost

class AdaBoostScratch: def __init__(self, n_estimators=50): self.n_estimators = n_estimators self.stumps = [] self.alphas = [] def fit(self, X, y): n = X.shape[0] weights = np.full(n, 1 / n) for _ in range(self.n_estimators): stump = DecisionStump() stump.fit(X, y, weights) pred = stump.predict(X) err = np.sum(weights[pred != y]) err = np.clip(err, 1e-10, 1 - 1e-10) alpha = 0.5 * np.log((1 - err) / err) weights *= np.exp(-alpha * y * pred) weights /= weights.sum() stump.alpha = alpha self.stumps.append(stump) self.alphas.append(alpha) def predict(self, X): total = sum(a * s.predict(X) for a, s in zip(self.alphas, self.stumps)) return np.sign(total)

第 3 步:从零实现梯度提升

class GradientBoostingScratch: def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3): self.n_estimators = n_estimators self.lr = learning_rate self.max_depth = max_depth self.trees = [] self.initial_pred = None def fit(self, X, y): self.initial_pred = np.mean(y) current_pred = np.full(len(y), self.initial_pred) for _ in range(self.n_estimators): residuals = y - current_pred tree = SimpleRegressionTree(max_depth=self.max_depth) tree.fit(X, residuals) update = tree.predict(X) current_pred += self.lr * update self.trees.append(tree) def predict(self, X): pred = np.full(X.shape[0], self.initial_pred) for tree in self.trees: pred += self.lr * tree.predict(X) return pred

第 4 步:与 sklearn 对比

代码验证从零实现与 sklearn 的 AdaBoostClassifierGradientBoostingClassifier 给出相近准确率,并把所有方法放一起对比。

三、框架对比

sklearn 的集成实现

from sklearn.ensemble import ( BaggingClassifier, RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier, VotingClassifier, StackingClassifier, ) # 随机森林(Bagging + 随机特征子采样) rf = RandomForestClassifier(n_estimators=100, max_features="sqrt", random_state=42) rf.fit(X_train, y_train) # AdaBoost ada = AdaBoostClassifier(n_estimators=50, learning_rate=1.0, random_state=42) ada.fit(X_train, y_train) # 梯度提升 gb = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3) gb.fit(X_train, y_train)

软投票与硬投票

from sklearn.ensemble import VotingClassifier from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC voting = VotingClassifier( estimators=[ ("lr", LogisticRegression()), ("rf", RandomForestClassifier()), ("svm", SVC(probability=True)), ], voting="soft", # 用概率平均,通常优于 "hard" ) voting.fit(X_train, y_train)

Stacking:带交叉验证的元特征

from sklearn.ensemble import StackingClassifier stack = StackingClassifier( estimators=[ ("lr", LogisticRegression()), ("rf", RandomForestClassifier()), ("knn", KNeighborsClassifier()), ], final_estimator=LogisticRegression(), # 元学习器 cv=5, # 内部交叉验证防泄漏 ) stack.fit(X_train, y_train)

StackingClassifier 内部用交叉验证生成基模型预测,避免在同一份数据上既训练基模型又训练元学习器导致的数据泄漏。

维度 手写实现 scikit-learn
控制粒度 可逐步跟踪权重、残差 封装好,默认行为合理
速度 纯 Python,慢 C 实现,快
适用 理解原理 生产环境

💡 实战中 XGBoost/LightGBM 已成表格数据事实标准,sklearn 的 GradientBoostingClassifier 仍是很好的学习起点和基线。

四、可复用产物

本节产出 outputs/prompt-ensemble-selector.md——一个帮你为给定数据集选对集成方法的提示词。喂给它你的数据描述(规模、特征类型、噪声水平、类别平衡)和你解决的问题,它会走一遍决策清单、推荐方法、给出起始超参数,并警告该方法常见的坑。另产出 outputs/skill-ensemble-builder.md,含完整的选择指南。

Python 代码(code/ensembles.py)是独立的从零实现,可与 sklearn 直接对照验证。

五、练习

  1. 跟踪 AdaBoost 训练曲线:修改 AdaBoost 实现,记录每轮训练后的准确率,画出准确率随估计器数的变化曲线。它何时收敛?

  2. 从零实现随机森林:在回归树里加入随机特征子采样,训练 100 棵树(max_features=sqrt(n_features)),平均预测。对比单棵树,展示方差下降。

  3. 梯度提升加早停:在梯度提升实现里加早停,每轮跟踪验证损失,连续 10 轮无改善就停。它实际需要多少棵树?

  4. 搭 Stacking:用三个基模型(逻辑回归、决策树、k 近邻)和一个逻辑回归元学习器搭 Stacking,用 5 折交叉验证生成元特征,对比每个基模型单独的表现。

  5. 对比 XGBoost:在同一数据集上跑默认参数的 XGBoost,对比从零梯度提升的准确率和耗时,差距有多大?

本节要点回顾

  1. 集成是定理不是比喻:N 个相互独立、准确率 > 0.5 的弱学习器,多数投票准确率随 N 增长——21 个 60% 准确率的分类器可达 74%。
  2. 多样性是关键:模型犯同样的错则组合无效;通过不同数据子集、不同特征子集、顺序纠错、不同模型族制造多样性。
  3. Bagging 降方差:在自助样本上训练、平均预测,每棵树过拟合方式不同,平均抵消噪声;约 36.8% 袋外样本提供免费验证。
  4. 随机森林=Bagging+特征子采样:每次分裂只看 sqrt(分类)或 n/3(回归)个特征,进一步增加树间多样性。
  5. Boosting 降偏差:顺序建模,每个新模型专攻集成当前的错误;AdaBoost 重加权样本,梯度提升拟合残差(损失的负梯度)。
  6. AdaBoost 公式:错误率越低模型权重越高,被错分样本权重指数级上升,最终预测是符号函数作用下的加权和。
  7. 梯度提升=拟合残差:平方损失下伪残差就是真实残差,每棵树字面拟合前一个集成的错误;学习率控制每树贡献,小学习率需更多树。
  8. XGBoost 统治表格:正则化目标、二阶导、稀疏感知、列子采样、缓存优化,使其在表格数据上常年压制神经网络。
  9. Stacking 用交叉验证生成元特征:避免在同一份数据上既训练基模型又训练元学习器;元学习器学习「何时该信哪个模型」。
  10. 基模型高方差用 Bagging,高偏差用 Boosting:这是选集成方法的首要判据;生产首选 LightGBM/XGBoost 默认参数起步。

下一节,我们讲超参数调优——网格搜索、随机搜索、贝叶斯优化如何用更少算力找到更好的超参数组合。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U