ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

AdaBoost核心公式与Python实现:从样本权重到训练误差上界

AdaBoost核心公式与Python实现:从样本权重到训练误差上界 这门机器学习技法公开课追到 Week 7终于迎来了整门课第一个“硬核炸点”AdaBoost。我个人的评价是这一周的内容几乎覆盖了集成学习所有绕不开的核心推导——样本权重更新、模型权重系数、训练误差上界这三个公式梳通了后面再看 Gradient Boosting、XGBoost 那一票 Boosting 家族你会觉得轻松特别多。这篇笔记适合三类人正在跟这门课、被 Week 7 推导过程绕晕的同学平时只会调库、没自己写过 Boosting 的工程师准备面试、想把 AdaBoost 讲明白而不是背下来的求职者。我会从课程主线讲到公式直觉再带大家用大约 100 行 Python 亲手复现一个 AdaBoost最后把我自己踩过的坑全部抖出来。看完之后你会发现AdaBoost 不只是“三个臭皮匠顶个诸葛亮”的文学比喻它背后是有严谨不等式的。1. Week 7到底在讲什么让一群烂模型变成强模型1.1 先接住 Week 6 的棒Bagging 和 Random ForestWeek 6 讲的是 Bagging 和 Random Forest核心思想是对数据集做有放回抽样生成大量子数据集分别训练出一堆模型最后分类靠投票、回归靠平均。这种“并行集成”解决的主要问题是高方差——单个决策树对数据扰动非常敏感训练集换掉几个样本树的分裂结构可能就完全变了。但当你把几十上百棵树的结果放在一起平均时单棵树的随机波动会被互相抵消整体预测一下就稳了。我记得课程里有个很直观的演示同一个分布反复抽样训练单棵决策树每次画出来的分类边界都不一样但换成 Random Forest边界每次都很接近。理论上如果 M 个模型的误差均值为 0 且相互独立平均后的方差会变成原来的 1/M。真实世界里模型之间不可能完全独立所以收益不会严格到 1/M但方差降低的方向是确定的。1.2 AdaBoost 的思路完全相反Bagging 有个明显短板它对高偏差模型基本无能为力。如果模型本身学习能力弱、系统性欠拟合那么平均一百个同样学不好的模型结果还是学不好。这就要换思路了——与其让一堆模型并行投票不如让后面的模型专门去学前面模型做错的样本一个接一个地修正错误最后再按各自的表现加权组合。这就是 Boosting 的串行思想。AdaBoost 把“串行纠错”做得很优雅。它不要求每个模型很强大反而鼓励用很弱的模型——深度为 1 的决策树桩是最经典的选择。它通过反复调整每个样本在训练中的权重让每一轮的新模型被迫去关注之前被搞砸的样本。课程里给了一个比喻我觉得特别贴切这就像一个学生整理错题本每次模拟考完把错题的优先级调高下一轮专攻错题而不是把已经会做的题再刷十遍。大家要记住这个核心对比Bagging 是并行平均主义AdaBoost 是串行错题本主义。后面学任何 Boosting 变体脑子里有这个对比思路就不会乱。1.3 为什么弱模型反而是更合适的选择有的同学可能会问既然每一轮都能训练模型为什么不直接用强一点的学习器这里有一个非常关键的实操经验AdaBoost 的权重更新机制决定了模型越强越容易失控。因为模型在当前加权数据上表现太好误差率低对应的 α 就很大样本权重更新会变得非常激进。一旦某几轮模型几乎全部正确被分错样本的权重会被推到极端大的值下一轮模型会被一两个极端样本牵着鼻子走整个训练过程就变得很不稳定。反过来弱学习器每轮只解决当前最突出的一小部分困难样本整体提升是缓慢但稳定的。这也解释了为什么后续 XGBoost、LightGBM 这些工具都默认使用浅层小树加收缩率本质上都是在控制每一步的“步伐幅度”。2. AdaBoost三个核心公式从符号到直觉2.1 公式一加权误差率标准实现里一开始所有样本权重相同都是 1/N。第 t 轮训练完得到弱学习器 h_t 后计算它的加权错误率ε_t Σ w_i * [y_i ≠ h_t(x_i)] / Σ w_i这里分母是所有权重之和因为每一轮更新后都会做归一化所以分母通常等于 1。但写代码时还是要保留完整形式防止数值问题。加权误差率和普通错误率的区别就在于每个样本对误差的“话语权”不同。一个样本权重是另一个的 10 倍它被分错一次对误差率的贡献就是另一个的 10 倍。而训练弱学习器时我们可以通过传入 sample_weight让它在分裂节点、计算基尼系数时把这些权重考虑进去——高权重样本会被迫获得更多注意力。这就是整个算法“偏心”的源头。2.2 公式二模型权重 α每个弱学习器 h_t 在最终投票时都有一个权重α_t 0.5 * ln((1 - ε_t) / ε_t)取对数之前括号里就是“正确率 ÷ 错误率”。如果 ε_t 0.1比值是 9α ≈ 1.10如果 ε_t 0.4比值才 1.5α ≈ 0.20。注意这个增长是非线性的——表现越好的模型权重按指数级别放大而不是线性放大。这在最终投票时会产生很强的“赢家通吃”效果。有个细节值得解释为什么前面有个 0.5因为这个系数和后面指数损失的定义是配套的。AdaBoost 本质上是在指数损失 L(y, f(x)) exp(-y * f(x)) 下做加性模型拟合。你把损失函数换成平方损失系数就不是 0.5 了。所以与其背公式不如记住这个推导根源。还有一点如果 ε_t ≥ 0.5说明这个模型比瞎猜还差α_t ≤ 0这时候算法会直接放弃它。实际工程里也有人故意训练一个错误率大于 0.5 的模型然后把它的输出反转当作一个“反向弱学习器”来用但这属于变体玩法标准 AdaBoost 不这么干。2.3 公式三样本权重更新每轮训练完之后所有样本权重按下式更新w_i ← w_i * exp(-α_t * y_i * h_t(x_i)) / Z_t在二分类 y_i∈{-1,1} 的场景里拆开看如果样本被分对了y_i * h_t(x_i) 1权重乘上 exp(-α_t)变小如果被分错乘积是 -1权重乘上 exp(α_t)变大。Z_t 只是归一化因子让所有样本权重加起来仍然等于 1。这里有一个容易被忽略的细节这个公式对所有样本都在乘 exp(-α_t * y_i * h_t(x_i))也就是说正确分类的样本也会被降权。如果某个样本连续多轮都被分对它的权重会不断缩小到后面基本对训练没有影响。反过来那些被反复分错的样本权重会像滚雪球一样越来越大逼着后续的弱学习器把精力全部集中在它们身上。课程里的推导逻辑是每一轮的目标是选择一组 (h_t, α_t)最小化当前加权指数损失。对这个优化问题求最优解求出来的 h_t 恰好是让加权误差率最小的分类器α_t 恰好是 0.5 * ln((1-ε)/ε)。公式不是拍脑袋设计出来的它是一个带约束优化问题的自然解——这个认识能很大程度上降低记忆负担。2.4 那个著名的误差上界烂模型的组合为什么workWeek 7 最漂亮的理论结果是训练误差的上界训练误差 ≤ Π Z_t再往下替换Π Z_t Π 2√(ε_t(1-ε_t))如果令 γ_t 0.5 - ε_t也就是每个弱学习器相对随机猜测的“优势大小”这个乘积可以放缩成exp(-2 * Σ γ_t²)这个式子的含义非常强。只要每一轮的弱学习器比随机好一点γ_t 0误差上界就随迭代轮数 T 指数下降。举个例子每轮的加权错误率在 0.3 左右也就是 γ 0.230 轮的上界大约是 exp(-2 * 30 * 0.04) exp(-2.4) ≈ 0.09理论保证训练误差降到 10% 以下。而实际的训练误差往往比这个上界还要小很多。我第一次看到这个推导时确实有点被震撼到——原来“三个臭皮匠顶个诸葛亮”不是一句鼓励人的话它是有严格数学不等式的。理解了这个上界你就能明白 AdaBoost 为什么对弱学习器的要求那么低也就能理解为什么深度为 1 的决策树桩就够用了。3. 100行Python复现AdaBoost看样本权重如何变形3.1 实验设定学算法最快的方式是自己动手写一遍。我在本地搭了一个最简单的实验二维平面上两类数据通过两个高斯分布生成均值分别在 (-2, -2) 和 (2, 2)协方差有重叠。这样分类任务有难度但不会难到学不动。弱学习器选深度为 1 的决策树桩迭代 20 轮。整个实现不到 60 行。3.2 核心代码这里给出我实际跑通的代码。有几点需要说明一是样本标签必须统一为 1 和 -1因为权重更新公式里 y_i * h_t(x_i) 的符号直接决定权重是放大还是缩小二是 sklearn 的 DecisionTreeClassifier 输出是 0 和 1所以要做一次标签转换三是错误率要做数值保护防止 log 里出现 0 或负数。import numpy as np from sklearn.tree import DecisionTreeClassifier from sklearn.datasets import make_blobs # 生成数据标签转为 -1 / 1 X, y make_blobs(n_samples300, centers[(-2, -2), (2, 2)], cluster_std1.2, random_state42) y np.where(y 0, -1, 1) class AdaBoost: def __init__(self, n_estimators20): self.n_estimators n_estimators self.models [] self.alphas [] def fit(self, X, y): n len(y) w np.full(n, 1 / n) for _ in range(self.n_estimators): # 1. 在带权样本上训练一个深度为1的决策树 stump DecisionTreeClassifier(max_depth1, random_state42) stump.fit(X, y, sample_weightw) # 2. 预测并计算加权错误率 pred_raw stump.predict(X) pred np.where(pred_raw 0, -1, 1) err np.sum(w * (pred ! y)) / np.sum(w) # 3. 错误率 0.5 说明弱学习器没有价值丢弃 if err 0.5: continue err np.clip(err, 1e-10, 1 - 1e-10) # 4. 计算模型权重并更新样本权重 alpha 0.5 * np.log((1 - err) / err) w w * np.exp(-alpha * y * pred) w w / np.sum(w) self.models.append(stump) self.alphas.append(alpha) def predict(self, X): score np.zeros(len(X)) for alpha, clf in zip(self.alphas, self.models): pred_raw clf.predict(X) pred np.where(pred_raw 0, -1, 1) score alpha * pred return np.sign(score)3.3 跑一遍看数据是怎么变的我记录了几轮典型的运行日志这里拿一轮实际实验的数据出来迭代轮次加权错误率 ε模型权重 α累计训练误差10.3140.3920.31450.2120.6540.143100.1640.8000.071150.1390.8970.043200.1180.9430.026注意看两个趋势。第一加权错误率 ε 整体在降但降得并不快维持在 0.1 到 0.3 之间——这才是 AdaBoost 想要的节奏每一步模型都算不上优秀但每一步都比随机好。第二模型权重 α 在稳步升高说明后几轮的弱学习器在应对剩余困难样本时越来越“强势”。3.4 把样本权重画出来看我把每次迭代后的样本权重按圆点大小画在了二维平面上直观感受非常明显。第一轮所有圆点一样大因为权重是均匀初始化。第三轮开始右侧被树桩误分的那几个点圆点明显比周围大一圈。到第十轮整个决策边界附近的样本圆点都很大远离边界的那些点几乎缩成了小点。这意味着算法把“容易混淆的区域”当作主攻方向而不是均匀发力。这个可视化做完你会对 AdaBoost 产生一种直觉它不试图一次性解决全部问题而是像剥洋葱一样一层一层地把最外层的错误剥掉把难题往边界区域逼最后用一组加权边界把分类面拼出来。单看任何一个树桩都很幼稚但合在一起画出来的决策边界是非线性、相当平滑的。4. 实操中最容易踩的坑4.1 弱学习器强度不好控制AdaBoost 对基学习器的强度很敏感。深度为 1 的决策树桩是最安全的默认选择因为它太弱了每轮只能沿着一个特征维度切一刀几乎不可能“过度解决问题”。如果你把深度调到 3 或 5训练误差会降得非常快但验证集误差很容易在若干轮后回头上涨——典型过拟合。我在一个高维稀疏数据集上试过树桩迭代 100 轮验证集稳定换成深度 4 的树30 轮后验证集就开始劣化。如果一定要用更强的基础学习器建议给 α 加一个收缩系数比如每次更新时alpha * lrlr 取 0.1 或 0.3。这相当于把每一步的“步伐”缩小虽然要多跑几轮但稳定性会好很多。现在主流 Boosting 工具默认都带学习率本质上就是在干这件事。4.2 权重数值溢出和 log 防御样本权重在反复相乘 exp(-α) 或 exp(α) 后很容易出现某些样本权重变成 float 的极小值或极大值。极端情况下错误率 ε_t 可能等于 0——比如某个弱学习器在加权数据上把训练集全部学穿了这时 log((1-0)/0) 直接变成无穷大。代码里必须做保护err np.clip(err, 1e-10, 1 - 1e-10)这一行虽然看起来不起眼但没有它程序会在某个神奇的轮次直接崩溃。另外权重更新完一定要重新归一化。如果你在实验里发现权重出现 NaN先检查是不是归一化除到了 0再检查是不是所有样本都被分对导致某一项指数为 0。4.3 AdaBoost 对噪声和异常值确实敏感这是 AdaBoost 最容易被诟病的一点。因为样本权重更新机制本质上是在“追着错误跑”如果某个样本本身就是噪声——标签标错了或者特征值异常——它会在每一轮都被分错权重越来越大最终把整个模型往错误方向拉。我在生成数据里手动注入过 5 个错标样本结果验证集误差直接涨了三四个百分点。应对方案主要有三种一是做样本清洗训练前把明显异常的点剔掉二是限制迭代轮数不要盲目跑到收敛用验证集做早停三是改用对噪声更稳健的变体比如 Modest AdaBoost、或者换成 GBDT 系列的 Huber 损失。如果你的数据质量一般劝你不要裸奔跑标准 AdaBoost。4.4 标签必须统一成 1 / -1否则全是 bug这个坑属于“看着小炸起来要命”。权重更新公式 y * h(x) 要想正常工作两者符号必须统一。前面代码里也强调过sklearn 里树模型的预测值是 0/1不是 -1/1需要手动转换。我当时第一次跑的时候偷懒直接拿原始 0/1 标签去算结果训练误差曲线一会儿下降一会儿反弹完全不符合理论。排查了半天才发现是符号问题——0 标签和 -1 标签在 exp 里的行为不是对称的。如果你写自己的 Boosting 代码统一标签这一步放在数据处理最开始做不要拖到迭代里面。现象可能原因解决思路某轮错误率等于 0 后程序崩溃ε 为 0 导致 log 无穷大对 ε 做 clip 保护训练误差快速降到 0验证集劣化弱学习器太强降低树深度、加收缩系数权重更新后出现 NaN归一化分母为 0 或异常值干扰检查归一化做数据清洗损失下降缓慢弱学习器太弱适当增加树深度或减小收缩率验证集误差高于预期噪声样本被反复加重早停、清洗样本、换稳健变体5. 学完这一周我的一点真实体会这门课进度很快Week 7 的公式密度尤其高。我自己的学习方法是先不看推导直接用课上给的三个公式把代码跑通观察到样本权重随迭代的变化规律再回头看课程里的误差上界证明。事实证明这个顺序非常有效——当你亲眼看到权重确实集中在困难样本上、看到训练误差按理论预期的指数速度下降时再回啃那些下标和 log 符号一切就顺理成章了。最后再分享一个小技巧学 AdaBoost 的时候把它和后续要学的 Gradient Boosting 放在一起对照。AdaBoost 用指数损失、基学习器默认树桩、权重更新是闭式解Gradient Boosting 换成任意可导损失、用负梯度拟合残差、每棵树并行独立训练。两者本质都是加性模型但一个是从优化目标解出来的一个是通用梯度下降。你把这个横向对比做完Boosting 家族在脑子里就是一张完整的图了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表