:從樣本權(quán)重到訓(xùn)練誤差上界)
這門機器學(xué)習(xí)技法公開課追到 Week 7終于迎來了整門課第一個“硬核炸點”AdaBoost。我個人的評價是這一周的內(nèi)容幾乎覆蓋了集成學(xué)習(xí)所有繞不開的核心推導(dǎo)——樣本權(quán)重更新、模型權(quán)重系數(shù)、訓(xùn)練誤差上界這三個公式梳通了后面再看 Gradient Boosting、XGBoost 那一票 Boosting 家族你會覺得輕松特別多。這篇筆記適合三類人正在跟這門課、被 Week 7 推導(dǎo)過程繞暈的同學(xué)平時只會調(diào)庫、沒自己寫過 Boosting 的工程師準備面試、想把 AdaBoost 講明白而不是背下來的求職者。我會從課程主線講到公式直覺再帶大家用大約 100 行 Python 親手復(fù)現(xiàn)一個 AdaBoost最后把我自己踩過的坑全部抖出來??赐曛竽銜l(fā)現(xiàn)AdaBoost 不只是“三個臭皮匠頂個諸葛亮”的文學(xué)比喻它背后是有嚴謹不等式的。1. Week 7到底在講什么讓一群爛模型變成強模型1.1 先接住 Week 6 的棒Bagging 和 Random ForestWeek 6 講的是 Bagging 和 Random Forest核心思想是對數(shù)據(jù)集做有放回抽樣生成大量子數(shù)據(jù)集分別訓(xùn)練出一堆模型最后分類靠投票、回歸靠平均。這種“并行集成”解決的主要問題是高方差——單個決策樹對數(shù)據(jù)擾動非常敏感訓(xùn)練集換掉幾個樣本樹的分裂結(jié)構(gòu)可能就完全變了。但當你把幾十上百棵樹的結(jié)果放在一起平均時單棵樹的隨機波動會被互相抵消整體預(yù)測一下就穩(wěn)了。我記得課程里有個很直觀的演示同一個分布反復(fù)抽樣訓(xùn)練單棵決策樹每次畫出來的分類邊界都不一樣但換成 Random Forest邊界每次都很接近。理論上如果 M 個模型的誤差均值為 0 且相互獨立平均后的方差會變成原來的 1/M。真實世界里模型之間不可能完全獨立所以收益不會嚴格到 1/M但方差降低的方向是確定的。1.2 AdaBoost 的思路完全相反Bagging 有個明顯短板它對高偏差模型基本無能為力。如果模型本身學(xué)習(xí)能力弱、系統(tǒng)性欠擬合那么平均一百個同樣學(xué)不好的模型結(jié)果還是學(xué)不好。這就要換思路了——與其讓一堆模型并行投票不如讓后面的模型專門去學(xué)前面模型做錯的樣本一個接一個地修正錯誤最后再按各自的表現(xiàn)加權(quán)組合。這就是 Boosting 的串行思想。AdaBoost 把“串行糾錯”做得很優(yōu)雅。它不要求每個模型很強大反而鼓勵用很弱的模型——深度為 1 的決策樹樁是最經(jīng)典的選擇。它通過反復(fù)調(diào)整每個樣本在訓(xùn)練中的權(quán)重讓每一輪的新模型被迫去關(guān)注之前被搞砸的樣本。課程里給了一個比喻我覺得特別貼切這就像一個學(xué)生整理錯題本每次模擬考完把錯題的優(yōu)先級調(diào)高下一輪專攻錯題而不是把已經(jīng)會做的題再刷十遍。大家要記住這個核心對比Bagging 是并行平均主義AdaBoost 是串行錯題本主義。后面學(xué)任何 Boosting 變體腦子里有這個對比思路就不會亂。1.3 為什么弱模型反而是更合適的選擇有的同學(xué)可能會問既然每一輪都能訓(xùn)練模型為什么不直接用強一點的學(xué)習(xí)器這里有一個非常關(guān)鍵的實操經(jīng)驗AdaBoost 的權(quán)重更新機制決定了模型越強越容易失控。因為模型在當前加權(quán)數(shù)據(jù)上表現(xiàn)太好誤差率低對應(yīng)的 α 就很大樣本權(quán)重更新會變得非常激進。一旦某幾輪模型幾乎全部正確被分錯樣本的權(quán)重會被推到極端大的值下一輪模型會被一兩個極端樣本牽著鼻子走整個訓(xùn)練過程就變得很不穩(wěn)定。反過來弱學(xué)習(xí)器每輪只解決當前最突出的一小部分困難樣本整體提升是緩慢但穩(wěn)定的。這也解釋了為什么后續(xù) XGBoost、LightGBM 這些工具都默認使用淺層小樹加收縮率本質(zhì)上都是在控制每一步的“步伐幅度”。2. AdaBoost三個核心公式從符號到直覺2.1 公式一加權(quán)誤差率標準實現(xiàn)里一開始所有樣本權(quán)重相同都是 1/N。第 t 輪訓(xùn)練完得到弱學(xué)習(xí)器 h_t 后計算它的加權(quán)錯誤率ε_t Σ w_i * [y_i ≠ h_t(x_i)] / Σ w_i這里分母是所有權(quán)重之和因為每一輪更新后都會做歸一化所以分母通常等于 1。但寫代碼時還是要保留完整形式防止數(shù)值問題。加權(quán)誤差率和普通錯誤率的區(qū)別就在于每個樣本對誤差的“話語權(quán)”不同。一個樣本權(quán)重是另一個的 10 倍它被分錯一次對誤差率的貢獻就是另一個的 10 倍。而訓(xùn)練弱學(xué)習(xí)器時我們可以通過傳入 sample_weight讓它在分裂節(jié)點、計算基尼系數(shù)時把這些權(quán)重考慮進去——高權(quán)重樣本會被迫獲得更多注意力。這就是整個算法“偏心”的源頭。2.2 公式二模型權(quán)重 α每個弱學(xué)習(xí)器 h_t 在最終投票時都有一個權(quán)重α_t 0.5 * ln((1 - ε_t) / ε_t)取對數(shù)之前括號里就是“正確率 ÷ 錯誤率”。如果 ε_t 0.1比值是 9α ≈ 1.10如果 ε_t 0.4比值才 1.5α ≈ 0.20。注意這個增長是非線性的——表現(xiàn)越好的模型權(quán)重按指數(shù)級別放大而不是線性放大。這在最終投票時會產(chǎn)生很強的“贏家通吃”效果。有個細節(jié)值得解釋為什么前面有個 0.5因為這個系數(shù)和后面指數(shù)損失的定義是配套的。AdaBoost 本質(zhì)上是在指數(shù)損失 L(y, f(x)) exp(-y * f(x)) 下做加性模型擬合。你把損失函數(shù)換成平方損失系數(shù)就不是 0.5 了。所以與其背公式不如記住這個推導(dǎo)根源。還有一點如果 ε_t ≥ 0.5說明這個模型比瞎猜還差α_t ≤ 0這時候算法會直接放棄它。實際工程里也有人故意訓(xùn)練一個錯誤率大于 0.5 的模型然后把它的輸出反轉(zhuǎn)當作一個“反向弱學(xué)習(xí)器”來用但這屬于變體玩法標準 AdaBoost 不這么干。2.3 公式三樣本權(quán)重更新每輪訓(xùn)練完之后所有樣本權(quán)重按下式更新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權(quán)重乘上 exp(-α_t)變小如果被分錯乘積是 -1權(quán)重乘上 exp(α_t)變大。Z_t 只是歸一化因子讓所有樣本權(quán)重加起來仍然等于 1。這里有一個容易被忽略的細節(jié)這個公式對所有樣本都在乘 exp(-α_t * y_i * h_t(x_i))也就是說正確分類的樣本也會被降權(quán)。如果某個樣本連續(xù)多輪都被分對它的權(quán)重會不斷縮小到后面基本對訓(xùn)練沒有影響。反過來那些被反復(fù)分錯的樣本權(quán)重會像滾雪球一樣越來越大逼著后續(xù)的弱學(xué)習(xí)器把精力全部集中在它們身上。課程里的推導(dǎo)邏輯是每一輪的目標是選擇一組 (h_t, α_t)最小化當前加權(quán)指數(shù)損失。對這個優(yōu)化問題求最優(yōu)解求出來的 h_t 恰好是讓加權(quán)誤差率最小的分類器α_t 恰好是 0.5 * ln((1-ε)/ε)。公式不是拍腦袋設(shè)計出來的它是一個帶約束優(yōu)化問題的自然解——這個認識能很大程度上降低記憶負擔(dān)。2.4 那個著名的誤差上界爛模型的組合為什么workWeek 7 最漂亮的理論結(jié)果是訓(xùn)練誤差的上界訓(xùn)練誤差 ≤ Π Z_t再往下替換Π Z_t Π 2√(ε_t(1-ε_t))如果令 γ_t 0.5 - ε_t也就是每個弱學(xué)習(xí)器相對隨機猜測的“優(yōu)勢大小”這個乘積可以放縮成exp(-2 * Σ γ_t2)這個式子的含義非常強。只要每一輪的弱學(xué)習(xí)器比隨機好一點γ_t 0誤差上界就隨迭代輪數(shù) T 指數(shù)下降。舉個例子每輪的加權(quán)錯誤率在 0.3 左右也就是 γ 0.230 輪的上界大約是 exp(-2 * 30 * 0.04) exp(-2.4) ≈ 0.09理論保證訓(xùn)練誤差降到 10% 以下。而實際的訓(xùn)練誤差往往比這個上界還要小很多。我第一次看到這個推導(dǎo)時確實有點被震撼到——原來“三個臭皮匠頂個諸葛亮”不是一句鼓勵人的話它是有嚴格數(shù)學(xué)不等式的。理解了這個上界你就能明白 AdaBoost 為什么對弱學(xué)習(xí)器的要求那么低也就能理解為什么深度為 1 的決策樹樁就夠用了。3. 100行Python復(fù)現(xiàn)AdaBoost看樣本權(quán)重如何變形3.1 實驗設(shè)定學(xué)算法最快的方式是自己動手寫一遍。我在本地搭了一個最簡單的實驗二維平面上兩類數(shù)據(jù)通過兩個高斯分布生成均值分別在 (-2, -2) 和 (2, 2)協(xié)方差有重疊。這樣分類任務(wù)有難度但不會難到學(xué)不動。弱學(xué)習(xí)器選深度為 1 的決策樹樁迭代 20 輪。整個實現(xiàn)不到 60 行。3.2 核心代碼這里給出我實際跑通的代碼。有幾點需要說明一是樣本標簽必須統(tǒng)一為 1 和 -1因為權(quán)重更新公式里 y_i * h_t(x_i) 的符號直接決定權(quán)重是放大還是縮小二是 sklearn 的 DecisionTreeClassifier 輸出是 0 和 1所以要做一次標簽轉(zhuǎn)換三是錯誤率要做數(shù)值保護防止 log 里出現(xiàn) 0 或負數(shù)。import numpy as np from sklearn.tree import DecisionTreeClassifier from sklearn.datasets import make_blobs # 生成數(shù)據(jù)標簽轉(zhuǎn)為 -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. 在帶權(quán)樣本上訓(xùn)練一個深度為1的決策樹 stump DecisionTreeClassifier(max_depth1, random_state42) stump.fit(X, y, sample_weightw) # 2. 預(yù)測并計算加權(quán)錯誤率 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 說明弱學(xué)習(xí)器沒有價值丟棄 if err 0.5: continue err np.clip(err, 1e-10, 1 - 1e-10) # 4. 計算模型權(quán)重并更新樣本權(quán)重 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 跑一遍看數(shù)據(jù)是怎么變的我記錄了幾輪典型的運行日志這里拿一輪實際實驗的數(shù)據(jù)出來迭代輪次加權(quán)錯誤率 ε模型權(quán)重 α累計訓(xùn)練誤差10.3140.3920.31450.2120.6540.143100.1640.8000.071150.1390.8970.043200.1180.9430.026注意看兩個趨勢。第一加權(quán)錯誤率 ε 整體在降但降得并不快維持在 0.1 到 0.3 之間——這才是 AdaBoost 想要的節(jié)奏每一步模型都算不上優(yōu)秀但每一步都比隨機好。第二模型權(quán)重 α 在穩(wěn)步升高說明后幾輪的弱學(xué)習(xí)器在應(yīng)對剩余困難樣本時越來越“強勢”。3.4 把樣本權(quán)重畫出來看我把每次迭代后的樣本權(quán)重按圓點大小畫在了二維平面上直觀感受非常明顯。第一輪所有圓點一樣大因為權(quán)重是均勻初始化。第三輪開始右側(cè)被樹樁誤分的那幾個點圓點明顯比周圍大一圈。到第十輪整個決策邊界附近的樣本圓點都很大遠離邊界的那些點幾乎縮成了小點。這意味著算法把“容易混淆的區(qū)域”當作主攻方向而不是均勻發(fā)力。這個可視化做完你會對 AdaBoost 產(chǎn)生一種直覺它不試圖一次性解決全部問題而是像剝洋蔥一樣一層一層地把最外層的錯誤剝掉把難題往邊界區(qū)域逼最后用一組加權(quán)邊界把分類面拼出來。單看任何一個樹樁都很幼稚但合在一起畫出來的決策邊界是非線性、相當平滑的。4. 實操中最容易踩的坑4.1 弱學(xué)習(xí)器強度不好控制AdaBoost 對基學(xué)習(xí)器的強度很敏感。深度為 1 的決策樹樁是最安全的默認選擇因為它太弱了每輪只能沿著一個特征維度切一刀幾乎不可能“過度解決問題”。如果你把深度調(diào)到 3 或 5訓(xùn)練誤差會降得非常快但驗證集誤差很容易在若干輪后回頭上漲——典型過擬合。我在一個高維稀疏數(shù)據(jù)集上試過樹樁迭代 100 輪驗證集穩(wěn)定換成深度 4 的樹30 輪后驗證集就開始劣化。如果一定要用更強的基礎(chǔ)學(xué)習(xí)器建議給 α 加一個收縮系數(shù)比如每次更新時alpha * lrlr 取 0.1 或 0.3。這相當于把每一步的“步伐”縮小雖然要多跑幾輪但穩(wěn)定性會好很多?,F(xiàn)在主流 Boosting 工具默認都帶學(xué)習(xí)率本質(zhì)上就是在干這件事。4.2 權(quán)重數(shù)值溢出和 log 防御樣本權(quán)重在反復(fù)相乘 exp(-α) 或 exp(α) 后很容易出現(xiàn)某些樣本權(quán)重變成 float 的極小值或極大值。極端情況下錯誤率 ε_t 可能等于 0——比如某個弱學(xué)習(xí)器在加權(quán)數(shù)據(jù)上把訓(xùn)練集全部學(xué)穿了這時 log((1-0)/0) 直接變成無窮大。代碼里必須做保護err np.clip(err, 1e-10, 1 - 1e-10)這一行雖然看起來不起眼但沒有它程序會在某個神奇的輪次直接崩潰。另外權(quán)重更新完一定要重新歸一化。如果你在實驗里發(fā)現(xiàn)權(quán)重出現(xiàn) NaN先檢查是不是歸一化除到了 0再檢查是不是所有樣本都被分對導(dǎo)致某一項指數(shù)為 0。4.3 AdaBoost 對噪聲和異常值確實敏感這是 AdaBoost 最容易被詬病的一點。因為樣本權(quán)重更新機制本質(zhì)上是在“追著錯誤跑”如果某個樣本本身就是噪聲——標簽標錯了或者特征值異?!鼤诿恳惠喍急环皱e權(quán)重越來越大最終把整個模型往錯誤方向拉。我在生成數(shù)據(jù)里手動注入過 5 個錯標樣本結(jié)果驗證集誤差直接漲了三四個百分點。應(yīng)對方案主要有三種一是做樣本清洗訓(xùn)練前把明顯異常的點剔掉二是限制迭代輪數(shù)不要盲目跑到收斂用驗證集做早停三是改用對噪聲更穩(wěn)健的變體比如 Modest AdaBoost、或者換成 GBDT 系列的 Huber 損失。如果你的數(shù)據(jù)質(zhì)量一般勸你不要裸奔跑標準 AdaBoost。4.4 標簽必須統(tǒng)一成 1 / -1否則全是 bug這個坑屬于“看著小炸起來要命”。權(quán)重更新公式 y * h(x) 要想正常工作兩者符號必須統(tǒng)一。前面代碼里也強調(diào)過sklearn 里樹模型的預(yù)測值是 0/1不是 -1/1需要手動轉(zhuǎn)換。我當時第一次跑的時候偷懶直接拿原始 0/1 標簽去算結(jié)果訓(xùn)練誤差曲線一會兒下降一會兒反彈完全不符合理論。排查了半天才發(fā)現(xiàn)是符號問題——0 標簽和 -1 標簽在 exp 里的行為不是對稱的。如果你寫自己的 Boosting 代碼統(tǒng)一標簽這一步放在數(shù)據(jù)處理最開始做不要拖到迭代里面?,F(xiàn)象可能原因解決思路某輪錯誤率等于 0 后程序崩潰ε 為 0 導(dǎo)致 log 無窮大對 ε 做 clip 保護訓(xùn)練誤差快速降到 0驗證集劣化弱學(xué)習(xí)器太強降低樹深度、加收縮系數(shù)權(quán)重更新后出現(xiàn) NaN歸一化分母為 0 或異常值干擾檢查歸一化做數(shù)據(jù)清洗損失下降緩慢弱學(xué)習(xí)器太弱適當增加樹深度或減小收縮率驗證集誤差高于預(yù)期噪聲樣本被反復(fù)加重早停、清洗樣本、換穩(wěn)健變體5. 學(xué)完這一周我的一點真實體會這門課進度很快Week 7 的公式密度尤其高。我自己的學(xué)習(xí)方法是先不看推導(dǎo)直接用課上給的三個公式把代碼跑通觀察到樣本權(quán)重隨迭代的變化規(guī)律再回頭看課程里的誤差上界證明。事實證明這個順序非常有效——當你親眼看到權(quán)重確實集中在困難樣本上、看到訓(xùn)練誤差按理論預(yù)期的指數(shù)速度下降時再回啃那些下標和 log 符號一切就順理成章了。最后再分享一個小技巧學(xué) AdaBoost 的時候把它和后續(xù)要學(xué)的 Gradient Boosting 放在一起對照。AdaBoost 用指數(shù)損失、基學(xué)習(xí)器默認樹樁、權(quán)重更新是閉式解Gradient Boosting 換成任意可導(dǎo)損失、用負梯度擬合殘差、每棵樹并行獨立訓(xùn)練。兩者本質(zhì)都是加性模型但一個是從優(yōu)化目標解出來的一個是通用梯度下降。你把這個橫向?qū)Ρ茸鐾闎oosting 家族在腦子里就是一張完整的圖了。