習(xí)7天實(shí)戰(zhàn):Bagging、Boosting與Stacking全攻略)
簡(jiǎn)介由杰森·布朗利編寫的《Python集成學(xué)習(xí)算法7天速成》中文翻譯版源自MachineLearningMastery系列面向已有Python、NumPy和scikit-learn基礎(chǔ)的開(kāi)發(fā)者用七節(jié)課快速補(bǔ)齊集成學(xué)習(xí)核心技能。資源共1個(gè)PDF文件壓縮包大小481KB便于下載后離線閱讀文檔按課程01至07組織章節(jié)獨(dú)立性較強(qiáng)依次講解什么是集成學(xué)習(xí)、裝袋集成、隨機(jī)森林、AdaBoost、梯度提升、投票集成和堆疊集成每課設(shè)有明確任務(wù)并提供基于scikit-learn的完整代碼示例可照做預(yù)測(cè)建模實(shí)驗(yàn)。針對(duì)模型方差、數(shù)據(jù)泄露和融合策略等容易出錯(cuò)的環(huán)節(jié)書(shū)中給出精簡(jiǎn)說(shuō)明幫助讀者把多個(gè)模型組合成更可靠的預(yù)測(cè)方案也可遷移到Kaggle等競(jìng)賽場(chǎng)景。目前已有108人學(xué)習(xí)瀏覽適合希望利用一周時(shí)間系統(tǒng)掌握集成學(xué)習(xí)并提升模型表現(xiàn)的開(kāi)發(fā)者。1. 集成學(xué)習(xí) 7 天速成為什么“打群架”的模型通常比單打獨(dú)斗更強(qiáng)如果你正打算用 7 天時(shí)間把 Python 集成學(xué)習(xí)算法從只會(huì)調(diào)庫(kù)練到能講清原理、能上手調(diào)優(yōu)、能排查問(wèn)題這篇筆記就是按這個(gè)目標(biāo)寫的。集成學(xué)習(xí)的核心思想一句話就能說(shuō)透單個(gè)模型容易偏、容易抖但把一群各有毛病、又不太一樣的模型湊在一起投票或接力整體誤差通常會(huì)被磨平一大截。這也是 Kaggle 競(jìng)賽里幾乎每支前排隊(duì)伍都會(huì)在最后一步用上 Stacking 或 Boosting 的原因。7 天速成不是讓你背公式而是按“并行集成 → 串行集成 → 集成之集成 → 避坑調(diào)優(yōu)”這條主線用 Python 生態(tài)里現(xiàn)成的庫(kù)把每個(gè)環(huán)節(jié)跑通。適合的對(duì)象很明確已經(jīng)會(huì)用 sklearn 訓(xùn)練單個(gè)模型、但對(duì)集成學(xué)習(xí)只停留在“聽(tīng)過(guò)名字”階段的從業(yè)者以及想在項(xiàng)目里把模型精度再往上頂一檔的工程師。第一天到第三天你會(huì)感受到“多模型投票”帶來(lái)的穩(wěn)定收益第四到第六天進(jìn)入 Boosting 與 Stacking 后更多的功夫要花在防止過(guò)擬合和評(píng)估偏差上最后一天請(qǐng)務(wù)必留給自己把整個(gè)實(shí)驗(yàn)過(guò)程固化成可復(fù)現(xiàn)的記錄。接下來(lái)我們從最基礎(chǔ)的并行集成開(kāi)始。2. 并行集成Voting 與 Bagging先讓模型學(xué)會(huì)“投團(tuán)隊(duì)票”2.1 集成學(xué)習(xí)的理論地基偏差-方差分解與“三個(gè)臭皮匠”邏輯先回答一個(gè)新手必然要問(wèn)的問(wèn)題為什么多個(gè)模型放在一起效果反而比最好的單個(gè)模型還穩(wěn)這要從偏差-方差分解說(shuō)起。一個(gè)模型的泛化誤差大致能拆成三項(xiàng)偏差bias、方差variance和不可約噪聲。偏差大說(shuō)明模型對(duì)規(guī)律的理解不夠經(jīng)常欠擬合方差大說(shuō)明模型對(duì)訓(xùn)練數(shù)據(jù)的波動(dòng)太敏感換個(gè)數(shù)據(jù)集表現(xiàn)就劇烈起伏。單棵決策樹(shù)是典型的低偏差高方差模型而線性模型則常常反過(guò)來(lái)。集成學(xué)習(xí)的思路就是在這兩項(xiàng)之間做文章。Bagging 的核心操作是“并行訓(xùn)練 平均”它通過(guò) Bootstrap 采樣讓每個(gè)子模型看到不同的訓(xùn)練子集子模型之間天然存在差異最后投票或平均時(shí)彼此的高方差部分會(huì)互相抵消。這背后的數(shù)學(xué)直覺(jué)是如果各基模型誤差是獨(dú)立的那么平均后的方差會(huì)按子模型數(shù)量下降哪怕基模型之間不完全獨(dú)立只要不是完全相關(guān)方差也能被顯著壓縮。這里有一張常用的誤差拆解表方便你對(duì)照自己的項(xiàng)目來(lái)判斷該用 Bagging 還是 Boosting當(dāng)前問(wèn)題主要矛盾推薦的集成方向理由單模型欠擬合訓(xùn)練分很低高偏差Boosting / Stacking串行擬合殘差能系統(tǒng)性降低偏差單模型分?jǐn)?shù)不錯(cuò)但測(cè)試集波動(dòng)大高方差Bagging / RandomForest并行平均能壓低方差數(shù)據(jù)噪聲大特征冗余多方差與偏差都高先用 Bagging 穩(wěn)底再上 Boosting直接 Boosting 容易把噪聲也學(xué)進(jìn)去“三個(gè)臭皮匠勝過(guò)諸葛亮”這句話在機(jī)器學(xué)習(xí)里是有前提的皮匠們得有自己的判斷不能全是同一個(gè)師傅教出來(lái)的。如果 10 個(gè)模型高度雷同集成只是在放大同一個(gè)錯(cuò)誤毫無(wú)意義。這也是整篇筆記里反復(fù)會(huì)出現(xiàn)的一條主線多樣性是集成的靈魂。2.2 用 scikit-learn 跑通 Voting 與 Bagging最小可執(zhí)行代碼先動(dòng)手跑一個(gè)最樸素的 Voting 分類器。Voting 分硬投票和軟投票硬投票是每個(gè)模型投一票、少數(shù)服從多數(shù)軟投票是每個(gè)模型輸出類別概率后加權(quán)平均再取概率最高的類別。數(shù)據(jù)量小、模型校準(zhǔn)良好時(shí)軟投票通常更穩(wěn)。下面這段代碼在一個(gè)模擬的二分類數(shù)據(jù)集上直接對(duì)比單模型與 Voting 的效果from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.ensemble import VotingClassifier, RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score # 生成 2000 個(gè)樣本、20 個(gè)特征的模擬二分類數(shù)據(jù) X, y make_classification(n_samples2000, n_features20, n_informative15, random_state42) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42) # 三個(gè)差異明顯的基模型 lr LogisticRegression(max_iter1000, random_state42) dt DecisionTreeClassifier(max_depth5, random_state42) rf RandomForestClassifier(n_estimators50, max_depth8, random_state42) # 軟投票按類別概率加權(quán)平均 voting_clf VotingClassifier( estimators[(lr, lr), (dt, dt), (rf, rf)], votingsoft ) voting_clf.fit(X_train, y_train) for name, model in [(LR, lr), (DT, dt), (RF, rf), (Voting, voting_clf)]: acc accuracy_score(y_test, model.predict(X_test)) print(f{name}: {acc:.4f})這段代碼里最關(guān)鍵的是votingsoft參數(shù)。軟投票要求基模型都有predict_proba方法邏輯回歸和決策樹(shù)都滿足。如果你換了某些不支持概率輸出的模型就只能退回到votinghard。我一般會(huì)在跑 Baseline 時(shí)先用硬投票確認(rèn)三個(gè)模型分?jǐn)?shù)接近后再切到軟投票看是否有提升。接下來(lái)是 Bagging 的最小示例。BaggingClassifier 本身是一個(gè)通用包裝器你可以把任意基模型塞進(jìn)去它內(nèi)部通過(guò) Bootstrap 采樣生成多個(gè)訓(xùn)練子集并行訓(xùn)練多個(gè)副本from sklearn.ensemble import BaggingClassifier from sklearn.tree import DecisionTreeClassifier # 用決策樹(shù)作為基模型訓(xùn)練 200 個(gè)副本 bag_clf BaggingClassifier( estimatorDecisionTreeClassifier(max_depth10, random_state42), n_estimators200, max_samples0.8, # 每個(gè)子模型采樣 80% 的樣本 max_features1.0, # 每個(gè)子模型使用全部特征 bootstrapTrue, # 有放回采樣 oob_scoreTrue, # 計(jì)算袋外分?jǐn)?shù) n_jobs-1, # 使用全部 CPU 核 random_state42 ) bag_clf.fit(X_train, y_train) print(fBagging OOB Score: {bag_clf.oob_score_:.4f}) print(fBagging Test Accuracy: {accuracy_score(y_test, bag_clf.predict(X_test)):.4f})max_samples0.8意味著每個(gè)子模型只看到 80% 的樣本剩下的 20% 沒(méi)參與訓(xùn)練正好可以用作天然的驗(yàn)證集。oob_scoreTrue就是利用這些沒(méi)被采到的樣本計(jì)算一個(gè)袋外分?jǐn)?shù)它約等于交叉驗(yàn)證的效果但省掉了重復(fù)訓(xùn)練的開(kāi)銷。對(duì)新手來(lái)說(shuō)如果訓(xùn)練集只有幾千條建議把max_samples調(diào)高到 0.9否則每個(gè)子模型看到的樣本太少基模型容易欠擬合。2.3 隨機(jī)森林Bagging 加特征隨機(jī)性以及 OOB 分?jǐn)?shù)的妙用隨機(jī)森林可以理解為 Bagging 的一個(gè)升級(jí)版它在每次節(jié)點(diǎn)分裂時(shí)不再?gòu)娜刻卣骼镎易顑?yōu)切分點(diǎn)而是隨機(jī)抽取一部分特征再?gòu)闹羞x最優(yōu)。這個(gè)改動(dòng)很關(guān)鍵它讓每棵樹(shù)學(xué)到的“特長(zhǎng)”不同。比如有的樹(shù)更依賴特征 A有的樹(shù)更依賴特征 B投票時(shí)彼此互補(bǔ)整體方差進(jìn)一步下降。用 sklearn 訓(xùn)練隨機(jī)森林只需要改一行from sklearn.ensemble import RandomForestClassifier rf_clf RandomForestClassifier( n_estimators300, max_depthNone, # 不限制深度讓樹(shù)自由生長(zhǎng) min_samples_leaf2, # 葉子節(jié)點(diǎn)最少 2 個(gè)樣本限制過(guò)擬合 max_featuressqrt, # 分類問(wèn)題默認(rèn)取特征數(shù)的平方根 oob_scoreTrue, n_jobs-1, random_state42 ) rf_clf.fit(X_train, y_train) print(fRF OOB Score: {rf_clf.oob_score_:.4f}) print(fRF Test Accuracy: {accuracy_score(y_test, rf_clf.predict(X_test)):.4f}) # 查看特征重要性 importance rf_clf.feature_importances_ top_idx importance.argsort()[-5:][::-1] print(Top-5 重要特征索引:, top_idx)max_featuressqrt是分類問(wèn)題的默認(rèn)推薦值回歸問(wèn)題一般用1.0或log2。這段代碼里我把max_depth設(shè)為None配合min_samples_leaf2來(lái)讓樹(shù)自由生長(zhǎng)但葉子不能太純這個(gè)組合在中小數(shù)據(jù)集上往往表現(xiàn)不錯(cuò)。特征重要性feature_importances_是基于“該特征在所有樹(shù)中帶來(lái)的不純度減少量”累加得到的注意它偏向數(shù)值型特征和取值多的特征只能用作粗篩不能當(dāng)作因果證據(jù)。OOB 分?jǐn)?shù)在這里還有額外價(jià)值你不必專門切一塊驗(yàn)證集就能知道模型大概的水平。做法是先跑一遍不設(shè)置max_depth的隨機(jī)森林看 OOB 分?jǐn)?shù)再跑一遍有限制條件的版本對(duì)比 OOB 分?jǐn)?shù)漲了說(shuō)明限制有效跌了說(shuō)明限制過(guò)度。我習(xí)慣把 OOB 分?jǐn)?shù)當(dāng)作調(diào)參時(shí)的主要參照省下交叉驗(yàn)證的大量時(shí)間。3. 串行集成Boosting 家族從 AdaBoost 到 GBDT 再到 XGBoost3.1 Boosting 的核心邏輯每個(gè)模型負(fù)責(zé)“上一輪沒(méi)做對(duì)的事”如果把 Bagging 比作“并行開(kāi)會(huì)、獨(dú)立發(fā)言”Boosting 就是“接力攻堅(jiān)、步步為營(yíng)”。它訓(xùn)練一堆模型但順序有先后第 2 個(gè)模型重點(diǎn)關(guān)注第 1 個(gè)模型做錯(cuò)的樣本第 3 個(gè)模型重點(diǎn)關(guān)注前兩個(gè)模型的殘差如此往復(fù)。AdaBoost 的做法是改變樣本權(quán)重——被分錯(cuò)的樣本權(quán)重變大下一輪模型被迫把注意力集中在這些難樣本上。GBDT梯度提升決策樹(shù)換了一種更通用的表述每一輪新增的樹(shù)去擬合前面所有樹(shù)的負(fù)梯度也就是“殘差”。對(duì)于回歸任務(wù)殘差就是真實(shí)值與當(dāng)前預(yù)測(cè)值之差對(duì)于分類任務(wù)這個(gè)殘差被替換為概率空間上的梯度。為什么擬合殘差有效因?yàn)槊恳惠喍荚谛拚?dāng)前的錯(cuò)誤方向相當(dāng)于沿著損失函數(shù)下降的方向逐步逼近最優(yōu)解。這里有一個(gè)新手容易犯的概念混淆AdaBoost 和 GBDT 都叫 Boosting但 AdaBoost 通過(guò)樣本權(quán)重來(lái)體現(xiàn)“關(guān)注錯(cuò)誤”GBDT 通過(guò)擬合殘差來(lái)體現(xiàn)。前者對(duì)異常值極其敏感因?yàn)楫惓|c(diǎn)權(quán)重會(huì)被不斷放大后者在損失函數(shù)選擇上更靈活因此衍生出了 XGBoost、LightGBM 等一堆工程化實(shí)現(xiàn)。理解了這一點(diǎn)你就能明白為什么大多數(shù)比賽和工業(yè)項(xiàng)目最終都選了 GBDT 系算法而不是 AdaBoost。3.2 AdaBoost 與梯度提升的最小實(shí)現(xiàn)從 sklearn 到手動(dòng)理解先用 sklearn 把 AdaBoost 跑通代碼很簡(jiǎn)單from sklearn.ensemble import AdaBoostClassifier ada_clf AdaBoostClassifier( estimatorDecisionTreeClassifier(max_depth1, random_state42), # 樹(shù)樁 n_estimators200, learning_rate0.5, algorithmSAMME, random_state42 ) ada_clf.fit(X_train, y_train) print(fAdaBoost Test Accuracy: {accuracy_score(y_test, ada_clf.predict(X_test)):.4f})AdaBoost 的基模型在 sklearn 里默認(rèn)就是深度為 1 的決策樹(shù)樁algorithmSAMME是為了兼容多分類的變體。learning_rate0.5表示每一步只按 0.5 的步長(zhǎng)采納當(dāng)前模型的貢獻(xiàn)這樣后續(xù)模型還能有修正空間步長(zhǎng)太大會(huì)導(dǎo)致模型在少數(shù)難樣本上用力過(guò)猛太小則訓(xùn)練緩慢。跑完這段代碼后你可以打印ada_clf.estimator_errors_看看每一輪的加權(quán)誤差誤差曲線如果后期開(kāi)始震蕩說(shuō)明步長(zhǎng)沒(méi)調(diào)好或者樹(shù)樁太弱。再看回歸任務(wù)的 GBDT 實(shí)現(xiàn)同樣是 sklearn 一行搞定from sklearn.ensemble import GradientBoostingRegressor from sklearn.metrics import mean_squared_error gb_reg GradientBoostingRegressor( n_estimators200, learning_rate0.05, max_depth3, subsample0.8, random_state42 ) gb_reg.fit(X_train_reg, y_train_reg) # 回歸數(shù)據(jù)集 pred gb_reg.predict(X_test_reg) print(fGBDT RMSE: {mean_squared_error(y_test_reg, pred, squaredFalse):.4f})subsample0.8是隨機(jī)梯度提升的做法每輪只用 80% 的樣本訓(xùn)練當(dāng)前樹(shù)。這個(gè)參數(shù)不是為了省計(jì)算而是引入隨機(jī)性來(lái)降低與 Bagging 類似的方差同時(shí)加快訓(xùn)練。經(jīng)驗(yàn)上learning_rate與n_estimators必須聯(lián)合調(diào)整學(xué)習(xí)率減半需要的樹(shù)數(shù)量大致翻倍。你想快速驗(yàn)證這一點(diǎn)可以分別跑(0.1, 100)和(0.05, 200)兩組參數(shù)觀察測(cè)試分?jǐn)?shù)是否接近。3.3 GBDT 的三件套調(diào)參學(xué)習(xí)率、樹(shù)深度與子采樣GBDT 系模型在實(shí)踐中最需要關(guān)注的三個(gè)參數(shù)就是learning_rate、max_depth和subsample。它們之間是互相牽制的關(guān)系。learning_rate控制每棵樹(shù)貢獻(xiàn)的權(quán)重。數(shù)值越小模型越保守需要越多樹(shù)來(lái)擬合同樣的信號(hào)數(shù)值太大前幾棵樹(shù)就把訓(xùn)練集學(xué)滿了后面全是噪音擬合。我一般先用固定學(xué)習(xí)率 0.1 跑一遍記錄驗(yàn)證集分?jǐn)?shù)曲線然后把學(xué)習(xí)率降到 0.05、樹(shù)數(shù)量翻倍再對(duì)比曲線。如果兩次效果接近說(shuō)明模型容量足夠問(wèn)題在別處如果 0.05 顯著更好說(shuō)明 0.1 時(shí)已經(jīng)過(guò)擬合了。max_depth在 GBDT 中通常取值 3 到 6遠(yuǎn)小于單棵決策樹(shù)的深度。原因是 Boosting 每次只擬合殘差不需要太復(fù)雜的樹(shù)深度過(guò)深反而讓當(dāng)前樹(shù)“想太多”把殘差里的噪聲也學(xué)進(jìn)去。subsample是行采樣比例0.7 到 0.9 之間是常用區(qū)間它對(duì)防止過(guò)擬合的貢獻(xiàn)在數(shù)據(jù)量小的時(shí)候尤其明顯。這三個(gè)參數(shù)的正確調(diào)參順序很重要。我建議先固定learning_rate0.1用交叉驗(yàn)證搜max_depth和subsample找到合適區(qū)間后再把學(xué)習(xí)率降下來(lái)、把樹(shù)數(shù)量補(bǔ)上去最后微調(diào)早停。順序反了會(huì)浪費(fèi)時(shí)間因?yàn)槟阍谝粋€(gè)過(guò)擬合狀態(tài)下調(diào)學(xué)習(xí)率結(jié)論沒(méi)有參考價(jià)值。3.4 工程化 BoostingXGBoost 與 LightGBM 該怎么選當(dāng)你從 sklearn 切到真正的工程化 Boosting 庫(kù)時(shí)先看這張對(duì)比表再?zèng)Q定用哪個(gè)對(duì)比維度XGBoostLightGBM分裂策略預(yù)排序 直方圖近似基于梯度的單邊采樣GOSS 互斥特征捆綁EFB訓(xùn)練速度中等大數(shù)據(jù)集上明顯更快內(nèi)存占用較高更低類別特征支持需手動(dòng)編碼支持原生類別特征擅長(zhǎng)場(chǎng)景中小數(shù)據(jù)、稀疏特征大規(guī)模數(shù)據(jù)、高維特征如果你只是想在 7 天速成里快速拿到可靠結(jié)果我的建議是中小數(shù)據(jù)集優(yōu)先 XGBoost因?yàn)樗€(wěn)定、文檔更成熟數(shù)據(jù)量超過(guò)幾十萬(wàn)行且特征稀疏時(shí)直接換 LightGBM速度差距會(huì)非常明顯。下面用 XGBoost 的 sklearn 接口做一個(gè)最小示例from xgboost import XGBClassifier from sklearn.metrics import roc_auc_score xgb_clf XGBClassifier( n_estimators300, learning_rate0.05, max_depth4, subsample0.8, colsample_bytree0.8, # 每棵樹(shù)隨機(jī)使用 80% 的特征 eval_metriclogloss, early_stopping_rounds30, random_state42 ) xgb_clf.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) print(fXGB AUC: {roc_auc_score(y_test, xgb_clf.predict_proba(X_test)[:, 1]):.4f})這段代碼里early_stopping_rounds30是防止過(guò)擬合的關(guān)鍵當(dāng)測(cè)試集合上的 logloss 連續(xù) 30 輪沒(méi)有提升訓(xùn)練自動(dòng)終止best_iteration屬性會(huì)告訴你最佳樹(shù)數(shù)量。colsample_bytree0.8是隨機(jī)森林里特征隨機(jī)性的 Boosting 版本它迫使每棵樹(shù)只依賴部分特征增加多樣性。LightGBM 的接口略有不同它用n_estimators對(duì)應(yīng)同樣的概念需要額外設(shè)置num_leaves而不是max_depth。num_leaves理論上可以大于2^max_depth因此它能擬合更復(fù)雜的局部結(jié)構(gòu)但也更容易過(guò)擬合。我的經(jīng)驗(yàn)是先用默認(rèn)num_leaves31如果驗(yàn)證集分?jǐn)?shù)不如 XGBoost再逐步調(diào)整而不是一上來(lái)就改得面目全非。4. 集成之集成Stacking 與 Blending讓模型預(yù)測(cè)值變成新特征4.1 Stacking 為什么能漲點(diǎn)元模型學(xué)的是“誰(shuí)在哪些樣本上更可靠”Stacking 的思路比 Bagging 和 Boosting 又進(jìn)了一步Bagging 讓基模型投票Boosting 讓基模型接力而 Stacking 干脆把基模型的預(yù)測(cè)結(jié)果當(dāng)作新特征再訓(xùn)練一個(gè)元模型來(lái)學(xué)習(xí)“在什么情況下應(yīng)該更相信哪個(gè)基模型”。舉例來(lái)說(shuō)邏輯回歸可能在樣本 A 上很準(zhǔn)隨機(jī)森林在樣本 B 上很準(zhǔn)Stacking 的元模型會(huì)學(xué)到這個(gè)規(guī)律而不是簡(jiǎn)單投票。這里最關(guān)鍵的操作是防止“數(shù)據(jù)泄漏”。如果直接用基模型在訓(xùn)練集上的預(yù)測(cè)結(jié)果作為元模型的輸入基模型已經(jīng)見(jiàn)過(guò)這些樣本預(yù)測(cè)結(jié)果會(huì)偏樂(lè)觀元模型學(xué)到的就是虛高信號(hào)。正確做法是用 K 折交叉驗(yàn)證為每個(gè)訓(xùn)練樣本生成“袋外預(yù)測(cè)”O(jiān)ut-of-Fold簡(jiǎn)稱 OOF比如 5 折每次用 4 折訓(xùn)練基模型預(yù)測(cè)剩下 1 折循環(huán) 5 次后每個(gè)訓(xùn)練樣本都拿到一個(gè)“沒(méi)見(jiàn)過(guò)它”的預(yù)測(cè)值。測(cè)試集上的處理同樣要注意每一折訓(xùn)練好的基模型都要對(duì)完整測(cè)試集做預(yù)測(cè)然后取平均得到測(cè)試集的最終預(yù)測(cè)特征。很多人第一版 Stacking 漲不了點(diǎn)幾乎都是因?yàn)檫@一步偷懶直接用全量訓(xùn)練模型預(yù)測(cè)測(cè)試集導(dǎo)致特征分布不一致。4.2 用交叉驗(yàn)證生成 OOF 預(yù)測(cè)完整可復(fù)現(xiàn)代碼下面這段代碼手動(dòng)實(shí)現(xiàn)了 5 折 Stacking 的全流程基模型用隨機(jī)森林和邏輯回歸元模型用邏輯回歸import numpy as np from sklearn.model_selection import StratifiedKFold from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score # 準(zhǔn)備 OOF 矩陣形狀是 (訓(xùn)練樣本數(shù), 基模型個(gè)數(shù)) kf StratifiedKFold(n_splits5, shuffleTrue, random_state42) base_models [ (rf, RandomForestClassifier(n_estimators100, max_depth6, random_state42)), (lr, LogisticRegression(max_iter1000, random_state42)) ] # OOF 預(yù)測(cè)和測(cè)試集預(yù)測(cè)容器 oof_pred np.zeros((len(X_train), len(base_models))) test_pred np.zeros((len(X_test), len(base_models))) for idx, (name, model) in enumerate(base_models): test_fold_pred np.zeros(len(X_test)) # 累計(jì)測(cè)試集預(yù)測(cè) for train_idx, val_idx in kf.split(X_train, y_train): # 拆出當(dāng)前折的訓(xùn)練集和驗(yàn)證集 X_fold_train, X_fold_val X_train[train_idx], X_train[val_idx] y_fold_train, y_fold_val y_train[train_idx], y_train[val_idx] # 在當(dāng)前折上訓(xùn)練基模型 model.fit(X_fold_train, y_fold_train) # 對(duì)驗(yàn)證集預(yù)測(cè)概率填充 OOF oof_pred[val_idx, idx] model.predict_proba(X_fold_val)[:, 1] # 對(duì)完整測(cè)試集預(yù)測(cè)概率累加后取平均 test_fold_pred model.predict_proba(X_test)[:, 1] / kf.get_n_splits() test_pred[:, idx] test_fold_pred # 訓(xùn)練元模型輸入是基模型的預(yù)測(cè)概率 meta_model LogisticRegression(max_iter1000, random_state42) meta_model.fit(oof_pred, y_train) final_pred meta_model.predict(test_pred) print(fStacking Test Accuracy: {accuracy_score(y_test, final_pred):.4f})這段代碼有幾個(gè)細(xì)節(jié)值得展開(kāi)。第一StratifiedKFold保證每折的正負(fù)樣本比例和全量數(shù)據(jù)一致分類問(wèn)題必須用分層采樣否則某些折里可能全是一個(gè)類別。第二OOF 矩陣的每一列對(duì)應(yīng)一個(gè)基模型在全部訓(xùn)練樣本上的“干凈預(yù)測(cè)”元模型拿到的特征不會(huì)包含“記憶過(guò)的答案”。第三測(cè)試集預(yù)測(cè)用的是每一折模型預(yù)測(cè)的平均值這一步不可省如果某一折因?yàn)殡S機(jī)種子問(wèn)題導(dǎo)致某模型沒(méi)收斂平均值能平滑掉異常波動(dòng)。跑通這段代碼后建議你檢查一下 OOF 矩陣兩列之間的相關(guān)性。如果相關(guān)性超過(guò) 0.95說(shuō)明兩個(gè)基模型幾乎沒(méi)有差異Stacking 提升空間有限。這時(shí)候與其繼續(xù)堆模型不如回頭改進(jìn)基模型的多樣性。4.3 Blending 作為輕量替代五分鐘版 StackingBlending 是 Stacking 的簡(jiǎn)化版它不做交叉驗(yàn)證而是直接把訓(xùn)練集切出一部分比如 10%當(dāng)作“小驗(yàn)證集”基模型只在剩余 90% 上訓(xùn)練然后對(duì)小驗(yàn)證集和測(cè)試集做預(yù)測(cè)元模型用小驗(yàn)證集的預(yù)測(cè)結(jié)果來(lái)訓(xùn)練。代碼如下from sklearn.model_selection import train_test_split # 從訓(xùn)練集中切出 10% 作為元模型的小驗(yàn)證集 X_meta_train, X_meta_val, y_meta_train, y_meta_val train_test_split( X_train, y_train, test_size0.1, stratifyy_train, random_state42 ) def get_base_predictions(model, X_fit, y_fit, X_predict): model.fit(X_fit, y_fit) return model.predict_proba(X_predict)[:, 1] # 基模型在小驗(yàn)證集和測(cè)試集上的預(yù)測(cè) rf_meta_val get_base_predictions(RandomForestClassifier(n_estimators100, random_state42, max_depth6), X_meta_train, y_meta_train, X_meta_val) lr_meta_val get_base_predictions(LogisticRegression(max_iter1000, random_state42), X_meta_train, y_meta_train, X_meta_val) rf_test get_base_predictions(RandomForestClassifier(n_estimators100, random_state42, max_depth6), X_meta_train, y_meta_train, X_test) lr_test get_base_predictions(LogisticRegression(max_iter1000, random_state42), X_meta_train, y_meta_train, X_test) # 拼接特征后訓(xùn)練元模型 meta_features_val np.column_stack([rf_meta_val, lr_meta_val]) meta_features_test np.column_stack([rf_test, lr_test]) meta_model LogisticRegression(max_iter1000) meta_model.fit(meta_features_val, y_meta_val) blend_pred meta_model.predict(meta_features_test) print(fBlending Test Accuracy: {accuracy_score(y_test, blend_pred):.4f})Blending 的優(yōu)點(diǎn)是代碼量少、邏輯直白訓(xùn)練成本低適合在時(shí)間緊急時(shí)快速出一個(gè)集成結(jié)果。代價(jià)是它比 Stacking 多浪費(fèi)了 10% 的訓(xùn)練數(shù)據(jù)給元模型小數(shù)據(jù)集上效果可能不如 OOF 方式。我的建議是7 天速成的第 5 天先跑 Blending 理解流程第 6 天再切到完整的 Stacking兩者對(duì)比一下就能直觀感受到 OOF 的價(jià)值。4.4 基模型多樣性與元模型選擇的實(shí)操建議Stacking 漲不漲點(diǎn)一半取決于基模型的選擇一半取決于元模型的設(shè)計(jì)?;P筒灰x同類算法否則學(xué)到的錯(cuò)誤模式高度一致。我常用的組合是邏輯回歸簡(jiǎn)單線性基線、隨機(jī)森林高方差、并行、GBDT高偏差、串行、XGBoost帶正則的 Boosting。這四個(gè)模型的預(yù)測(cè)概率相關(guān)性通常能控制在 0.85 以下相當(dāng)于提供了四種不同視角。元模型的選擇沒(méi)有統(tǒng)一答案。我的經(jīng)驗(yàn)是先從邏輯回歸開(kāi)始因?yàn)樗?xùn)練快、結(jié)果穩(wěn)、不容易過(guò)擬合如果把 OOF 特征換成邏輯回歸后驗(yàn)證分?jǐn)?shù)有明顯提升說(shuō)明信息融合有效再試更復(fù)雜的元模型。如果你在元模型里加入原始特征比如把訓(xùn)練數(shù)據(jù)的原始數(shù)值特征也拼進(jìn) OOF 矩陣效果可能更好但也更容易過(guò)擬合因?yàn)樵P同F(xiàn)在有了直接“看到”原始特征的能力對(duì) OOF 特征的依賴會(huì)下降。這一步要不要做建議以驗(yàn)證集分?jǐn)?shù)為準(zhǔn)。另外一個(gè)經(jīng)常被忽略的點(diǎn)如果基模型里有 XGBoost 或 LightGBM它們的predict_proba輸出在極端類別不均衡下可能非常集中比如全是 0.01 或 0.99這會(huì)干擾元模型的邏輯回歸擬合。解決辦法是先把 OOF 概率做標(biāo)簽編碼或分位變換再輸入元模型。這個(gè)細(xì)節(jié)在第 5 章的類別不平衡部分還會(huì)再提。5. 集成學(xué)習(xí)避坑指南5 個(gè)真實(shí)踩坑記錄與排查思路5.1 OOF 與驗(yàn)證集劃分不當(dāng)導(dǎo)致線下漲點(diǎn)、線上翻車現(xiàn)象在本地用隨機(jī)劃分的訓(xùn)練測(cè)試集Stacking 之后準(zhǔn)確率提升了 2%一上真實(shí)環(huán)境或線上 A/B 測(cè)試效果反而比單模型還差。原因數(shù)據(jù)里可能存在時(shí)間依賴或分組結(jié)構(gòu)。比如同一條用戶的多條行為記錄被隨機(jī)分到了訓(xùn)練集和測(cè)試集模型等于“見(jiàn)過(guò)”這個(gè)人了OOF 預(yù)測(cè)和元模型訓(xùn)練都受到了泄漏信號(hào)的干擾。解決檢查數(shù)據(jù)里有沒(méi)有“同一實(shí)體多條記錄”的情況比如用戶 ID、設(shè)備 ID、訂單 ID。有的話用GroupKFold按實(shí)體分組切分保證同一個(gè)實(shí)體的數(shù)據(jù)全部落在同一折里。如果數(shù)據(jù)是按時(shí)間產(chǎn)生的直接按時(shí)間點(diǎn)切分訓(xùn)練集和測(cè)試集并且用時(shí)間前移的交叉驗(yàn)證方式。排查時(shí)先畫(huà)一張數(shù)據(jù)的時(shí)間分布圖如果訓(xùn)練集和測(cè)試集的類別分布或特征分布有明顯漂移先處理分布問(wèn)題再談集成。5.2 基模型多樣性不足集成活成了“復(fù)讀機(jī)”現(xiàn)象隨機(jī)森林 XGBoost LightGBM 三個(gè)模型做了 Stacking結(jié)果和直接用 XGBoost 幾乎一樣。原因這三個(gè)模型雖然算法不同但在同一個(gè)數(shù)據(jù)集上、用同一套特征工程學(xué)到的主要規(guī)律高度相似。它們對(duì)正確樣本的看法一致對(duì)錯(cuò)誤樣本的錯(cuò)誤方式也雷同元模型沒(méi)有差異化信息可用。解決先計(jì)算 OOF 預(yù)測(cè)之間的相關(guān)性矩陣如果相關(guān)系數(shù)普遍高于 0.9說(shuō)明多樣性嚴(yán)重不足。這時(shí)優(yōu)先調(diào)整方向不是繼續(xù)加模型而是往特征子集、樣本子集或算法類型三個(gè)方向制造差異。常見(jiàn)做法是利用特征分組讓模型 A 只用數(shù)值特征模型 B 只用類別特征模型 C 用全部特征或者同一種算法換不同超參數(shù)比如深樹(shù)和淺樹(shù)。用這種手法把相關(guān)性壓到 0.8 以下Stacking 才有實(shí)際意義。5.3 早停與學(xué)習(xí)率n_estimators 加得越多越準(zhǔn)是錯(cuò)覺(jué)現(xiàn)象訓(xùn)練 Boosting 模型時(shí)每次把n_estimators翻倍驗(yàn)證集分?jǐn)?shù)都在漲直到某個(gè)點(diǎn)后開(kāi)始劇烈下降但你看訓(xùn)練集分?jǐn)?shù)還在穩(wěn)步上升。原因Boosting 的每一輪都在擬合當(dāng)前殘差樹(shù)的數(shù)量越多模型對(duì)訓(xùn)練集的記憶越完整。驗(yàn)證集分?jǐn)?shù)開(kāi)始掉頭的那一刻就是模型開(kāi)始把噪聲當(dāng)成規(guī)律的時(shí)刻。解決固定使用early_stopping_rounds同時(shí)把learning_rate降到 0.05 或更低。一個(gè)常用做法是先用 0.1 的學(xué)習(xí)率找出大致的最佳區(qū)間再把學(xué)習(xí)率減半、樹(shù)數(shù)量翻倍繼續(xù)觀察驗(yàn)證曲線是否改善。注意早停是基于驗(yàn)證集指標(biāo)不是訓(xùn)練集有些人習(xí)慣用訓(xùn)練集 logloss 做早停等于用開(kāi)卷考試成績(jī)判斷學(xué)生水平不可取。5.4 類別不均衡下 Boosting 的“強(qiáng)者恒強(qiáng)”現(xiàn)象二分類問(wèn)題正樣本只占 5%負(fù)樣本占 95%跑完 XGBoost 后準(zhǔn)確率 95%看起來(lái)很高但一看 F1 分?jǐn)?shù)慘不忍睹正樣本幾乎全被預(yù)測(cè)成負(fù)類。原因Boosting 在每一輪加權(quán)時(shí)會(huì)把更多權(quán)重放在被分錯(cuò)的樣本上。但初始階段負(fù)樣本數(shù)量壓倒性占優(yōu)模型很容易把全部樣本預(yù)測(cè)為負(fù)類造成后來(lái)的輪次里正樣本權(quán)重雖然上升但整體決策邊界仍偏向負(fù)類一側(cè)。解決先換評(píng)估指標(biāo)不要用準(zhǔn)確率改用 F1、PR-AUC 或 ROC-AUC。然后給模型傳遞類別權(quán)重參數(shù)XGBoost 里是scale_pos_weight通常設(shè)為負(fù)樣本數(shù)除以正樣本數(shù)LightGBM 里用is_unbalanceTrue或scale_pos_weight。如果還不行對(duì)少數(shù)類做樣本加權(quán)或上采樣別急著用 SMOTE先看類別權(quán)重能不能把決策邊界拉回來(lái)。集成模型內(nèi)部機(jī)制復(fù)雜改完權(quán)重后一定要同時(shí)觀察訓(xùn)練集和驗(yàn)證集的分差權(quán)重過(guò)大會(huì)讓模型在訓(xùn)練集上過(guò)擬合多數(shù)類。5.5 隨機(jī)種子與多線程集成結(jié)果像開(kāi)盲盒現(xiàn)象同一份代碼、同一個(gè)參數(shù)配置每次運(yùn)行得到的驗(yàn)證集分?jǐn)?shù)都不一樣反復(fù)橫跳 0.5% 左右。原因隨機(jī)森林的行采樣、Boosting 的子采樣、交叉驗(yàn)證的分割順序都依賴隨機(jī)數(shù)生成器同時(shí)n_jobs-1并行訓(xùn)練時(shí)線程調(diào)度的隨機(jī)性也會(huì)影響浮點(diǎn)數(shù)累加順序產(chǎn)生微小差異。解決在代碼里給所有模型統(tǒng)一設(shè)置random_state交叉驗(yàn)證也用同一個(gè)隨機(jī)種子如果用了 XGBoost 或 LightGBM把它們的random_state也設(shè)成同一個(gè)整數(shù)。不光模型要固定數(shù)據(jù) shuffle 的順序也要固定。如果你發(fā)現(xiàn)固定了所有隨機(jī)種子后結(jié)果仍不穩(wěn)大概率是多線程浮點(diǎn)累加導(dǎo)致此時(shí)把n_jobs從-1調(diào)成1復(fù)測(cè)一次如果結(jié)果穩(wěn)定了說(shuō)明是并行計(jì)算引入的數(shù)值波動(dòng)。對(duì)上線模型我通常用固定隨機(jī)種子跑 3 次取均值把這個(gè)均值作為提交分?jǐn)?shù)。6. 7 天收尾手法用三份驗(yàn)收?qǐng)?bào)告鎖定模型效果別把調(diào)參過(guò)程當(dāng)黑匣子7 天速成的最后一天千萬(wàn)不要急著去搜新模型而是做三件事。第一件事是回顧實(shí)驗(yàn)記錄把 7 天里跑過(guò)的每一個(gè)模型組合、每一組超參數(shù)、對(duì)應(yīng)的驗(yàn)證集分?jǐn)?shù)和 OOF 相關(guān)性整理成一張表。沒(méi)有記錄就等于沒(méi)做過(guò)因?yàn)榛叵氩怀瞿慕M配置是因?yàn)楦牧四膫€(gè)參數(shù)才漲的。我習(xí)慣用 Markdown 表格記錄每行是一次實(shí)驗(yàn)列包括基模型列表、關(guān)鍵參數(shù)、驗(yàn)證分?jǐn)?shù)、OOF 相關(guān)性、訓(xùn)練用時(shí)備注里寫當(dāng)時(shí)的判斷。這張表就是你下次面對(duì)同類問(wèn)題時(shí)的最強(qiáng)參考。第二件事是穩(wěn)定性驗(yàn)證用一個(gè)小腳本對(duì)最終選定的集成模型跑 5 次不同隨機(jī)種子記錄每次的驗(yàn)證集分?jǐn)?shù)計(jì)算均值和標(biāo)準(zhǔn)差。標(biāo)準(zhǔn)差小于 0.003 才說(shuō)明模型結(jié)果可復(fù)現(xiàn)如果標(biāo)準(zhǔn)差偏大說(shuō)明模型容量或數(shù)據(jù)切分本身不穩(wěn)定這時(shí)候分析單次分?jǐn)?shù)沒(méi)有意義。穩(wěn)定性驗(yàn)證的目的是讓你在匯報(bào)結(jié)果時(shí)心里有底漲的那 2% 是真實(shí)差異還是隨機(jī)波動(dòng)。第三件事是特征重要性歸因把最終 Stacking 模型里每個(gè)基模型的 OOF 預(yù)測(cè)重要性輸出出來(lái)看看元模型到底在依賴誰(shuí)。如果某個(gè)基模型的 OOF 特征在元模型里的系數(shù)接近零說(shuō)明它對(duì)這個(gè)數(shù)據(jù)集沒(méi)有貢獻(xiàn)下次可以去掉如果某個(gè)特征在隨機(jī)森林里重要性極高但在 XGBoost 里很低說(shuō)明它對(duì)不同算法有不同價(jià)值這個(gè)特征值得保留。用permutation_importance計(jì)算擾動(dòng)每個(gè)特征后驗(yàn)證分?jǐn)?shù)的下降量比內(nèi)置的重要性更直觀。最后分享一個(gè)我自己的教訓(xùn)有一次做時(shí)間序列預(yù)測(cè)花了三天調(diào)好了 Stacking線上效果異常優(yōu)秀但因?yàn)閷?shí)驗(yàn)中途換過(guò)兩次數(shù)據(jù)切分方式?jīng)]有統(tǒng)一記錄復(fù)盤時(shí)完全無(wú)法確認(rèn)哪個(gè)版本對(duì)應(yīng)哪個(gè)結(jié)果最終只能重跑一組基礎(chǔ)實(shí)驗(yàn)確認(rèn)。從那以后我把“當(dāng)天實(shí)驗(yàn)當(dāng)天記錄”定成鐵律調(diào)參過(guò)程絕不當(dāng)黑匣子。這 7 天的速成也許不能讓你變成集成學(xué)習(xí)專家但能讓你建立起一套從模型選型到實(shí)驗(yàn)驗(yàn)收的完整工作流。希望你也能在最后的驗(yàn)收環(huán)節(jié)里找到自己最順手的那一套記錄格式。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取