一棵不夠,就種一座森林

ISLP 第 8 章 — 對應講義 08(含集成學習那一週)
遞迴二元分裂|成本複雜度剪枝|Gini 與交叉熵|Bagging|OOB|Random Forest|Boosting|XGBoost
向下捲動開始互動
📌 本頁使用方式(ISLP Ch.8|講義 08)

照節次讀:每節先讀說明,動手玩互動元件——先預測結果,再按按鈕驗證。 ② 對照講義:每個 §徽章都標了 ISLP 節號與講義頁碼,細節與完整推導請回講義與課本。 ③ 每節做 quiz:答錯就回到該節重讀,不要往下跳;錯的選項也寫了「錯在哪」。 ④ 最後翻關鍵詞彙卡自測術語,並用 REF 總覽當速查表。標「ESL 進階」的節是課堂沒細講的延伸,第一輪可略過。

CONTENTS · 內容目錄
PROLOGUE · 開場

把特徵空間切成方塊,每塊給一個預測值 ISLP §8.1講義 08 · p.2–9

前面幾章的模型都在配一個式子:線性迴歸配一條線、樣條配一條彎的曲線、 邏輯斯迴歸配一個機率。這一章換一個完全不同的想法——不配式子,直接把特徵空間切成方塊, 每個方塊裡的所有點都給同一個預測值。

ISLP §8.1 的例子是棒球員薪水(Hitters):用 Years(打了幾年) 和 Hits(去年幾支安打)預測 log 薪水。整棵樹只有兩刀,就把球員切成三塊:

$$R_1 = \{X \mid \texttt{Years} < 4.5\}, \quad R_2 = \{X \mid \texttt{Years} \ge 4.5,\ \texttt{Hits} < 117.5\}, \quad R_3 = \{X \mid \texttt{Years} \ge 4.5,\ \texttt{Hits} \ge 117.5\}$$

三塊的預測值就是各塊訓練資料的平均:$e^{5.107}$、$e^{5.999}$、$e^{6.740}$ 千美元,也就是約 16.5 萬、40.3 萬、84.5 萬。整個模型可以用一句話講完: 資淺的便宜;資深的看安打數。這種「一句話講得完」的能力,是樹最大的賣點。

先把四個詞釘住 終端節點/葉(terminal node / leaf):樹最底下那些不再分裂的節點, 每一個對應特徵空間的一塊方塊 $R_m$。
內部節點(internal node):寫著分裂規則(例如 Years < 4.5)的節點。
分支(branch):連接節點的線段。慣例是左邊=條件成立
樹是倒著畫的:根在上、葉在下。這件事第一次看都會愣一下。

樹的整體形狀就是一個階梯函數

$$f(X) = \sum_{m=1}^{M} c_m \cdot \mathbf{1}(X \in R_m)$$

對照線性迴歸的 $f(X) = \beta_0 + \sum_j X_j \beta_j$——一個是連續的斜面, 一個是一格一格的台階。哪個對?看真實關係長什麼樣,這是 PART 05 的主題。

線性迴歸決策樹
模型長相一個斜面(連續、光滑)一堆方塊(階梯、有稜角)
外插能力有(可以往定義域外推)沒有(葉的值是常數,出界就給邊界值)
類別變數要做虛擬變數編碼天生就會處理(把水準分兩堆)
變數尺度要小心(正則化時必須標準化)完全不受影響(只看大小順序)
交互作用要自己乘出來自動有(連續兩刀就是一個交互作用)
最大弱點真實關係非線性時失手不穩定:資料動一點,樹就長成另一棵

最後那一列是這一整章後半的動機。單一棵樹的變異大得離譜——把訓練資料隨機切成兩半、 各配一棵樹,兩棵樹可能完全不像。所以真正實用的做法從來不是「一棵樹」, 而是種很多棵再合起來:bagging、random forest、boosting。 這一頁的後六節就在講這件事。

講義完整實作:把 Sales 變成二元的 High

講義 08 · Carseats 與 High(lab 的起點)
Carseats = load_data('Carseats') High = np.where(Carseats.Sales > 8, "Yes", "No") Carseats.head()
預期輸出
   Sales  CompPrice  Income  Advertising  Population  Price ShelveLoc  Age  \
0   9.50        138      73           11         276    120       Bad   42   
1  11.22        111      48           16         260     83      Good   65   
2  10.06        113      35           10         269     80    Medium   59   
3   7.40        117     100            4         466     97    Medium   55   
4   4.15        141      64            3         340    128       Bad   38   

   Education Urban   US  
0         17   Yes  Yes  
1         10   Yes  Yes  
2         12   Yes  Yes  
3         14   Yes  Yes  
4         13   Yes   No

lab 前半用 Carseats 練分類樹(Sales > 8 記為 High = Yes),後半用 Boston 練迴歸樹與集成。注意 ShelveLocUrbanUS 是類別變數——理論上樹不必編碼,但 scikit-learn 的實作不支援,所以 lab 還是做了 one-hot。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 12、13
QUIZ · 樹在幹什麼

一棵迴歸樹對落在同一個葉節點裡的兩個觀測值,會給出什麼預測?

(A) 完全相同的預測值,也就是該葉節點內訓練資料的平均
(B) 不同的預測值,因為兩點的特徵值不同
(C) 先落在同一葉,再用該葉內的線性迴歸算出各自的預測
PART 01 · 迴歸樹的生長

遞迴二元分裂:每一刀都選讓 RSS 掉最多的那一刀 ISLP §8.1.1講義 08 · p.10–14

方塊要怎麼切?理想上我們想找到讓下面這個 RSS 最小的一組方塊 $R_1, \dots, R_J$:

$$\sum_{j=1}^{J} \sum_{i \in R_j} \left(y_i - \hat y_{R_j}\right)^2$$

問題是——所有可能的切法多到算不完。所以實務上用一個貪婪的近似: 遞迴二元分裂(recursive binary splitting)。

做法只有一句話:每一步,在所有變數 $X_j$ 與所有切點 $s$ 裡面, 選讓 RSS 掉最多的那一刀。切完之後,對切出來的兩塊各自再問同樣的問題,一直遞迴下去。 數學上就是找 $(j, s)$ 最小化

$$\sum_{i:\, x_i \in R_1(j,s)} \left(y_i - \hat y_{R_1}\right)^2 + \sum_{i:\, x_i \in R_2(j,s)} \left(y_i - \hat y_{R_2}\right)^2$$

其中 $R_1(j,s) = \{X \mid X_j < s\}$、$R_2(j,s) = \{X \mid X_j \ge s\}$。

「貪婪」是什麼意思,為什麼要在意 貪婪=每一步只看這一步最好, 不往前看。所以第一刀是「單獨看只切一刀時最好的那一刀」, 不保證是「最終要切五刀時,第一刀該切哪裡」。
代價是可能錯過「這一刀本身沒什麼用,但切完之後下一刀超好」的組合。 好處是快到可以在幾毫秒內算完——這是能夠實用的唯一理由。 下一節的剪枝,正是為了補救貪婪的短視。
按「單步」切一刀:程式會掃過兩個變數的每個候選切點,挑 RSS 下降最多的那一個。
虛擬碼 CODE
葉子 = [整個特徵空間] while 還可以切: 對每個葉子、每個變數 j、每個切點 s: 算 RSS 下降量 切下降最多的那一刀 葉子的預測值 = 該葉子內的 y 平均
這一刀
切在哪個變數
切點 s
RSS 下降量
目前葉子數 |T|1
目前訓練 RSS
兩邊在看同一件事
左邊是特徵空間:每塊方塊填該塊 y 平均值的顏色(藍=低、橘紅=高),點也照 y 上色。右邊是同一刀一刀長出來的樹,R1、R2… 的編號兩邊對得上。
資料是固定種子的合成資料,真值有四塊;分裂搜尋是在瀏覽器裡即時算的,不是預錄的動畫。

玩過上面的元件之後有三件事應該很明顯:

講義完整實作:Boston 上的迴歸樹

講義 08 · Boston 迴歸樹(max_depth=3)
Boston = load_data("Boston") model = MS(Boston.columns.drop('medv'), intercept=False) D = model.fit_transform(Boston) feature_names = list(D.columns) X = np.asarray(D) (X_train, X_test, y_train, y_test) = skm.train_test_split(X, Boston['medv'], test_size=0.3, random_state=0) reg = DTR(max_depth=3) reg.fit(X_train, y_train) ax = subplots(figsize=(12,12))[1] plot_tree(reg, feature_names=feature_names, ax=ax);

這一格畫出的樹,ISLP 書上的解讀是:lstat(低社經地位人口比例)愈低、房價愈高;而對於 lstat > 14.4crim > 5.8rm < 6.8 的郊區小房子,樹預測房價中位數 12,042 美元max_depth=3 是手動限制——下一節改成用 CV 學出來。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 50、52、54
QUIZ · 遞迴二元分裂

找第一刀時,演算法實際上比較了多少種可能?(p 個變數、n 筆資料)

(A) 大約 p × (n−1) 種:每個變數都試過所有相鄰觀測值之間的切點
(B) 所有可能的 J 塊分割,因為目標函數就是定義在整組方塊上的
(C) 只有 p 種:每個變數用它的中位數當切點
PART 02 · 剪枝

先長太大再剪:成本複雜度與 α ISLP §8.1.1講義 08 · p.15–21

上一節的過程如果不管它,樹會一直長到每個葉子只剩幾個點。訓練誤差很漂亮, 測試誤差很難看——典型的過度配適

直覺的解法是「RSS 下降量小於某個門檻就停」。這個解法是錯的, 而且錯得很有教育意義:它太短視。一刀本身看起來沒用, 但切完之後下一刀可能超級有用;提早停就永遠看不到那一刀。

正確的做法是先長太大,再剪回來。剪的依據是 成本複雜度剪枝(cost complexity pruning,也叫 weakest link pruning): 對每個 $\alpha \ge 0$,找讓下式最小的子樹 $T \subset T_0$

$$\sum_{m=1}^{|T|} \sum_{i:\, x_i \in R_m} \left(y_i - \hat y_{R_m}\right)^2 + \alpha |T|$$

$|T|$ 是葉子數。左邊是配適程度,右邊是複雜度的罰款—— 形式跟第 6 章的 lasso 一模一樣:一個要小的目標加上一個乘了調整參數的罰項。 $\alpha = 0$ 時罰款是零,最小化的就是訓練誤差,答案是完整的 $T_0$; $\alpha$ 愈大,多留一個葉子愈貴,樹就愈小。

為什麼這招可行:α 一動,樹是「一層一層剝」的 關鍵的技術事實是: $\alpha$ 從 0 慢慢調大時,分支是以巢狀而且可預測的方式被剪掉—— $\alpha_1 < \alpha_2$ 對應的子樹一定滿足 $T(\alpha_2) \subset T(\alpha_1)$。
所以整條「子樹序列」只有 $O(|T_0|)$ 條,一次算完就好, 不必去枚舉指數多的子樹。scikit-learncost_complexity_pruning_path() 回傳的就是這條序列的斷點。

剩下的問題是 α 該取多少——這是第 5 章的老題目:用交叉驗證選。 ISLP 演算法 8.1 把整套流程寫成四步:

  1. 用遞迴二元分裂在訓練資料上長一棵很大的樹。
  2. 對它做成本複雜度剪枝,得到一整條「子樹 vs α」的序列。
  3. 用 K 折交叉驗證選 α:每一折都重跑步驟 1 與 2, 在留出的那折上算誤差,然後對每個 α 平均,挑誤差最小的 α̂。
  4. 回到完整訓練資料,交出對應 α̂ 的那棵子樹。
圖表需要連網載入 Chart.js。此圖的重點:訓練 MSE 隨葉子數單調下降,但 CV 與測試 MSE 先降後平——曲線拉平的位置就是該停的地方。
拖動 α:α 愈大,樹被剪得愈小。看三條誤差線怎麼反應。
α
目前的剪枝強度
α
葉子數 |T|
訓練 MSE
CV MSE(六折)
測試 MSE
怎麼看這張圖 圖 8.5
x 軸是剪完之後的葉子數(α 從右往左遞增),三條線分別是訓練、六折 CV、與測試 MSE。訓練那條一定單調下降——它不能當選擇依據。CV 那條才是可以拿來選的,虛線標的是 CV 選出來的葉子數。
跟課本比
課本圖 8.5 的 CV 最低點在 3 個葉子,我們這個分割是 個。數字不會一樣(132/131 的隨機切分不同),但形狀一樣:從 1 到 3 個葉子誤差雪崩式下降,之後就在雜訊裡晃。

講義完整實作:Boston 上的成本複雜度剪枝

講義 08 · cost_complexity_pruning_path + GridSearchCV(迴歸)
ccp_path = reg.cost_complexity_pruning_path(X_train, y_train) kfold = skm.KFold(5, shuffle=True, random_state=10) grid = skm.GridSearchCV(reg, {'ccp_alpha': ccp_path.ccp_alphas}, refit=True, cv=kfold, scoring='neg_mean_squared_error') G = grid.fit(X_train, y_train) best_ = grid.best_estimator_ np.mean((y_test - best_.predict(X_test))**2)
預期輸出
np.float64(28.069857549754044)

流程完全照演算法 8.1:先拿 ccp_path.ccp_alphas 當候選格點,再用 GridSearchCV 在五折上挑,最後 refit=True 用全部訓練資料重配。測試 MSE 28.07,開根號約 5.30——也就是預測誤差大約在 5,300 美元的量級。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 57、59

講義完整實作:分類樹的剪枝(Carseats)

講義 08 · 用 CV 挑 ccp_alpha(分類)
ccp_path = clf.cost_complexity_pruning_path(X_train, High_train) kfold = skm.KFold(10, random_state=1, shuffle=True) grid = skm.GridSearchCV(clf, {'ccp_alpha': ccp_path.ccp_alphas}, refit=True, cv=kfold, scoring='accuracy') grid.fit(X_train, High_train) grid.best_score_
預期輸出
np.float64(0.685)

被選中的樹有 30 個葉子(儲存格 43),在測試集上的正確率 0.72(儲存格 45),比未剪枝的 0.735 還略差。lab 的原話是:「交叉驗證在這裡對我們的幫助不大」。這很正常也很重要——CV 是無偏的選擇工具,但它自己有變異,換個種子結果就會動。剪枝不是保證變好的魔法。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 37、39
QUIZ · 剪枝

為什麼不直接「RSS 下降量小於門檻就停止分裂」,而要先長大再剪?

(A) 因為那樣太短視:一刀本身沒什麼用,但它之後可能接著一刀非常有用
(B) 因為 RSS 下降量無法計算,只有剪枝時才算得出來
(C) 因為門檻法會讓樹變得太大,剪枝法才會讓樹變小
PART 03 · 分類樹

Gini 指數與交叉熵:為什麼不用錯誤率當分裂準則 ISLP §8.1.2講義 08 · p.22–27

分類樹跟迴歸樹幾乎一樣:一樣遞迴二元分裂、一樣剪枝。只有兩件事要換。

第一,預測值換成多數類別。落在某個葉子的觀測, 預測為該葉子內訓練資料最常出現的那一類。 但別忘了同時看類別比例——「90% 是 Yes」跟「51% 是 Yes」的可信度完全不同。

第二,RSS 換掉。類別沒有平方誤差可算。最直覺的替代品是 錯誤率(classification error rate):

$$E = 1 - \max_k \hat p_{mk}$$

$\hat p_{mk}$ 是第 $m$ 個節點裡屬於第 $k$ 類的比例。看起來很合理—— 但它不夠敏感,不適合當分裂準則。實務上用另外兩個:

$$G = \sum_{k=1}^{K} \hat p_{mk} (1 - \hat p_{mk}) \qquad\text{(Gini 指數)}$$ $$D = -\sum_{k=1}^{K} \hat p_{mk} \log \hat p_{mk} \qquad\text{(交叉熵 / entropy)}$$

兩個都在量節點純度(node purity):所有 $\hat p_{mk}$ 都靠近 0 或 1 時值很小。 兩者數值上非常接近,實務上選哪個幾乎沒差。

拖動 p̂ 看三個不純度指標的值。注意錯誤率是折線、另兩個是曲線。
0.50
目前的節點組成
p̂(第一類的比例)0.50
錯誤率 E
Gini 指數 G
交叉熵 D
三條線的關鍵差別 ISLP 8.4 第 3 題
錯誤率是兩段直線,在 p̂ = 0.5 折一下。直線代表「斜率是常數」:從 0.5 移到 0.4 跟從 0.2 移到 0.1,它給的獎勵一樣多。
Gini 與交叉熵是凹的曲線,兩端特別陡——「把 0.2 推到 0.1」拿到的獎勵比「把 0.5 推到 0.4」多得多。這就是「對純度更敏感」的意思。
為什麼折線會出事
因為錯誤率是線性的,「切完之後兩塊的加權錯誤率」常常剛好等於「不切」的錯誤率——演算法看到下降量 0,就以為這一刀沒用。下面那張表是最經典的例子。

下面這個例子把問題講得最清楚。父節點有 800 筆、兩類各 400 筆。兩種切法:

左邊(Yes / No)右邊(Yes / No)加權錯誤率加權 Gini加權交叉熵
切法 A300 / 100100 / 3000.2500.3750.562
切法 B200 / 400200 / 00.2500.3330.477

兩種切法都是 200 筆被分錯, 錯誤率完全一樣,所以用錯誤率當準則的話,這兩刀「一樣好」。 但切法 B 生出了一個完全純的葉子(200 筆全是 Yes), Gini 與交叉熵都認得出這件事比較有價值。交叉熵用自然對數。

ISLP 圖 8.6 的 Heart 例子有同一個現象的實例: 未剪枝樹右下角那一刀 RestECG < 1,兩邊的預測都是 Yes—— 錯誤率完全沒降,可是右邊那 9 筆全是 Yes、左邊只有 7/11, 純度差很多。這一刀之所以被切,就是因為 Gini 與交叉熵看得到差別。

剪枝的時候可以換回錯誤率 分裂用 Gini/交叉熵, 剪枝時三個都可以用;如果你最終在意的是預測正確率, 剪枝階段用錯誤率反而更對題。scikit-learnDecisionTreeClassifier(criterion=...) 只管分裂準則; 剪枝用的是 ccp_alpha,而 GridSearchCV(scoring='accuracy') 那一步就等於「用錯誤率挑 α」。lab 正是這樣寫的。

講義完整實作:用交叉熵長分類樹,再把樹印成文字

講義 08 · DecisionTreeClassifier(criterion='entropy') + export_text
clf = DTC(criterion='entropy', max_depth=3, random_state=0) clf.fit(X, High) print(export_text(clf, feature_names=feature_names, show_weights=True))
預期輸出
|--- ShelveLoc[Good] <= 0.50
|   |--- Price <= 92.50
|   |   |--- Income <= 57.00
|   |   |   |--- weights: [7.00, 3.00] class: 0
|   |   |--- Income >  57.00
|   |   |   |--- weights: [7.00, 29.00] class: 1
|   |--- Price >  92.50
|   |   |--- Advertising <= 13.50
|   |   |   |--- weights: [183.00, 41.00] class: 0
|   |   |--- Advertising >  13.50
|   |   |   |--- weights: [20.00, 25.00] class: 1
|--- ShelveLoc[Good] >  0.50
|   |--- Price <= 135.00
|   |   |--- US[Yes] <= 0.50
|   |   |   |--- weights: [6.00, 11.00] class: 1
|   |   |--- US[Yes] >  0.50
|   |   |   |--- weights: [2.00, 49.00] class: 1
|   |--- Price >  135.00
|   |   |--- Income <= 46.00
|   |   |   |--- weights: [6.00, 0.00] class: 0
|   |   |--- Income >  46.00
|   |   |   |--- weights: [5.00, 6.00] class: 1

訓練正確率 0.79(儲存格 19),對應偏差 log_loss = 0.4711(儲存格 21)——那正是 (8.7) 式的交叉熵。show_weights=True 印出的 weights: [7.00, 3.00] 就是該葉子裡 No/Yes 的筆數,拿它算 $\hat p_{{mk}}$ 就能自己驗算 Gini 與交叉熵。第一刀切在 ShelveLoc[Good]——貨架位置好不好最重要。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 15、17、29
觀念釐清
Q:為什麼分裂準則用 Gini 或交叉熵,而不用我們真正在意的錯誤率?

因為錯誤率對純度的變化不敏感,常常給出「下降量 0」的假訊號,讓貪婪演算法以為這一刀沒用而不切。

根本原因是形狀。兩類的情況下錯誤率是 $\min(\hat p, 1-\hat p)$,這是兩段直線;Gini 是 $2\hat p(1-\hat p)$、交叉熵是 $-\hat p \log \hat p - (1-\hat p)\log(1-\hat p)$,兩者都嚴格凹。而「切一刀的不純度下降量」等於父節點的不純度減掉兩個子節點不純度的加權平均。對嚴格凹的函數,這個下降量永遠大於 0(除非兩邊的 $\hat p$ 剛好相同);對線性的函數,只要兩邊的 $\hat p$ 落在 0.5 的同一側,加權平均就剛好等於父節點的值,下降量是 0。

上面那張 800 筆的表就是這件事的實例:切法 B 生出一個完全純的葉子,錯誤率卻報「跟切法 A 一樣」。而且純度不只是美學問題——落在純葉子裡的測試點,我們對它的預測有信心;落在 7/11 那個葉子裡的,我們沒有。這個差別在需要輸出機率時尤其要緊,而集成方法(bagging 的多數投票、boosting 的加權和)全都靠葉子的機率估計吃飯。

最後補一句實務規則:分裂用 Gini/交叉熵,剪枝與最終評估用錯誤率(或 AUC)。兩者的角色不同,不必統一。

QUIZ · 不純度

兩類問題中,某節點有 50 筆 Yes、50 筆 No。它的 Gini 指數是多少?

(A) 0.5
(B) 0.25
(C) 1.0
PART 04 · 樹 vs 線性模型

哪一種對,看真實邊界長什麼樣 ISLP §8.1.3–8.1.4講義 08 · p.28–29

兩個模型形式擺在一起:

$$f(X) = \beta_0 + \sum_{j=1}^{p} X_j \beta_j \qquad\text{對上}\qquad f(X) = \sum_{m=1}^{M} c_m \cdot \mathbf{1}(X \in R_m)$$

哪一個好?沒有普遍答案,看真實的邊界長什麼樣。 ISLP 圖 8.7 用兩排圖把這件事講完:

真實邊界線性模型(左欄)樹(右欄)誰贏
線性(斜的一刀)一刀就完美切開要用很多階梯去逼近那條斜線,邊界呈鋸齒狀線性模型
非線性(方塊狀)怎麼轉都切不對,一定有一大塊錯的幾刀就切得漂亮

關鍵在於樹的每一刀都是軸平行的。要表現「$X_1 + X_2 > 1$」這種斜邊界, 樹只能用一堆小台階去爬那條斜線——能逼近,但要很多刀, 而每一刀都花掉自由度、都增加變異。反過來, 「$X_1 < 3$ 且 $X_2 > 5$ 的那一塊特別高」這種方塊狀結構, 線性模型除非你手動把交互作用項乘出來,否則永遠抓不到。

怎麼決定用哪個:別猜,用第 5 章的工具 這不是靠肉眼看散佈圖決定的事。 把兩個都配一次,用交叉驗證比測試誤差,就這樣。
但誤差不是唯一考量:有時候你選樹是因為要能畫給人看 (醫療、法規、風控場景),這時候即使樹的誤差稍差一點也值得。 反過來,如果只追求準確率,本章後半的集成方法幾乎一定打得贏兩者, 代價是失去那張可以貼在牆上的圖。

ISLP §8.1.4 把樹的優缺點列成清單,值得整段記下來:

內容
✔ 好解釋比線性迴歸還好解釋。可以畫出來給非專業的人看懂
✔ 像人在想事情「先看這個、再看那個」的層層判斷,貼近人的決策過程
✔ 類別變數免編碼一刀就是「把某些水準分到左邊」,不需要虛擬變數
✔ 尺度無關只看大小順序,不必標準化,也不怕離群的 x
✘ 準確率不夠好單一棵樹通常打不過本書其他方法
非常不穩定資料動一點點,整棵樹的結構可能完全改變

最後那一項是後半章的引擎。「不穩定」用統計的話講就是變異很大, 而降變異最古典的手段就是平均——這正好是下三節的主題。

QUIZ · 樹 vs 線性模型

真實的決策邊界是一條斜線 $X_1 + X_2 = 1$。用決策樹去配會發生什麼事?

(A) 樹會用許多軸平行的小台階去逼近那條斜線,能逼近但需要很多刀
(B) 樹完全配不出來,因為它只能表示水平或垂直的邊界
(C) 樹會自動找到 X₁ + X₂ 這個組合當新的分裂變數
PART 05 · 為什麼要集成

投票與大數法則:一群弱分類器怎麼變強 講義 08 · p.30–31ESL §16.1 · 進階

先講一個跟樹無關的事實,因為整個集成學習都建在它上面。

你把一個難題丟給幾千個隨機的路人,把他們的答案彙總起來—— 彙總的答案常常比一個專家還準。這叫群眾智慧(wisdom of the crowd)。 機器學習版本的說法是:一群預測器合起來,常常比裡面最好的那一個還準。 這一群叫做集成(ensemble),做法叫集成方法(ensemble method)。

最簡單的集成是多數投票(hard voting):M 個分類器各投一票, 票多的那一類就是答案。假設每個分類器各自獨立、正確率都是 $p$, 那麼多數投票的正確率就是二項分佈的尾機率:

$$P(\text{投票正確}) = \sum_{k > M/2} \binom{M}{k} p^k (1-p)^{M-k}$$

講義第 31 頁舉的例子:1000 個只有 51% 正確率的弱學習器, 多數投票之後可望達到 75% 的正確率。這個數字大得不像真的——下面自己算一次。

圖表需要連網載入 Chart.js。此圖的重點:只要 p > 0.5,多數投票的正確率隨分類器數量單調上升並趨近 1;但 p < 0.5 時它反而趨近 0——投票會放大偏誤,不會修正它。
拖動 p 與 M:看多數投票的正確率怎麼變。p 拖到 0.5 以下會發生有趣的事。
p0.55M15
目前設定
單一分類器正確率 p0.55
分類器個數 M15
多數投票正確率
比單一個高多少
講義第 31 頁的例子
p = 0.51, M = 1000
p = 0.49, M = 1000
上面那排方塊
每個方塊是一個分類器在某一筆資料上的表現:綠=答對紅=答錯(固定種子的模擬,所以你重載頁面看到的是同一組)。下面那條線是多數投票的結果。拖 p 到 0.45 看看——紅色一多,投票就開始穩定地答錯。
「各自獨立」是整件事的命門 上面那條公式只有在 分類器彼此完全獨立、錯的地方互不相關時才成立。
真實世界裡沒這種好事:同一份資料訓練出來的模型,錯的地方通常也一樣, 這時候投一百票跟投一票差不多。所以集成方法真正在解的問題不是「怎麼投票」, 而是「怎麼弄出一群不一樣的預測器」
① 用不同的演算法(voting/stacking,PART 11)
② 用不同的資料(bagging,下一節)
③ 用不同的變數(random forest,PART 08)
④ 讓後面的人專門修前面的錯(boosting,PART 09)

講義完整實作:VotingClassifier

講義 08 · 三個不同演算法的多數投票(Carseats)
clf1 = make_pipeline(StandardScaler(),KNeighborsClassifier(n_neighbors=10)) clf2 = DecisionTreeClassifier(random_state=1) clf3 = GaussianNB() estimators = [ ("knn", clf1), ("dc", clf2), ("nb", clf3) ] vclf = VotingClassifier(estimators=estimators, voting='hard') k = 5 kf5 = KFold(n_splits=k, shuffle=True, random_state=1) print('5-fold cross validation:\n') for clf, label in zip([clf1, clf2, clf3, vclf], ['KNN', 'DecisionTreeClassifier', 'Naive Bayes', 'StackingClassifier']): scores = cross_val_score(clf, X_train, y_train, cv=kf5, scoring='accuracy') print("Accuracy: %0.2f (+/- %0.2f) [%s]" % (scores.mean(), scores.std(), label))
預期輸出
5-fold cross validation:

Accuracy: 0.78 (+/- 0.06) [KNN]
Accuracy: 0.73 (+/- 0.07) [DecisionTreeClassifier]
Accuracy: 0.79 (+/- 0.07) [Naive Bayes]
Accuracy: 0.82 (+/- 0.04) [StackingClassifier]

KNN 0.78、決策樹 0.73、樸素貝氏 0.79,投票之後 0.82——比裡面任何一個都好,而且標準差還從 0.06/0.07 掉到 0.04。這裡三個成員是完全不同的演算法,所以它們錯的地方不一樣,投票才有效。(標籤印成 StackingClassifier 是 lab 裡的筆誤,這一格用的是 VotingClassifier(voting='hard')。)

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 237、238
QUIZ · 投票與大數法則

1000 個分類器各自的正確率都是 0.48,且彼此獨立。多數投票的正確率大約是多少?

(A) 接近 0,比單一個分類器差得多
(B) 還是 0.48 左右,投票不會改變平均正確率
(C) 接近 0.52,因為投票會把錯誤的方向反轉過來
PART 06 · Bagging 與 OOB

重抽樣造很多棵樹再平均;順手拿到免費的驗證集 ISLP §8.2.1講義 08 · p.32–37

回到樹。單一棵樹的問題是變異太大。而降變異最古典的手段, 第 5 章就講過了:平均。給定 $n$ 個獨立的觀測值、每個變異數都是 $\sigma^2$, 它們的平均 $\bar Z$ 的變異數是 $\sigma^2 / n$。

所以理想的做法是:蒐集 $B$ 份獨立的訓練資料、各配一棵樹、把預測平均起來。 問題是我們只有一份資料。那就用 bootstrap 假造出 $B$ 份—— 這就是 bootstrap aggregation,簡稱 bagging

$$\hat f_{\text{bag}}(x) = \frac{1}{B} \sum_{b=1}^{B} \hat f^{*b}(x)$$

分類問題就把平均換成多數投票:B 棵樹各投一票,票多的那一類就是答案。

bagging 的樹要長很深,而且不要剪枝 這一點跟前兩節的直覺剛好相反, 但完全講得通:
每棵樹都長到很深、不剪枝——所以每棵樹的偏差很小、變異很大。 然後平均 B 棵樹,把變異壓下來。偏差在平均的過程裡不會變 (B 個無偏估計的平均還是無偏),所以我們用「降變異」換到了「保留低偏差」。
反過來如果每棵樹都剪成三個葉子,偏差大,平均一百棵之後偏差還是那麼大—— 白費工。bagging 只治變異,不治偏差。

還有一個副產品,好用到有點不像話。有放回抽 $n$ 次,某一筆始終沒被抽到的機率是

$$\left(1 - \frac{1}{n}\right)^n \;\xrightarrow[n \to \infty]{}\; e^{-1} \approx 0.368$$

所以每棵樹平均只用到約 2/3 的資料,剩下那 1/3 沒被用到的叫做 袋外樣本(out-of-bag, OOB)。它們對這棵樹來說是天然的驗證集—— 完全免費的測試誤差估計,不必再做交叉驗證。 (這個 0.632/0.368 在第 5 章 bootstrap 那一節推導過,回去對一下。)

按「長一棵樹」看一次有放回重抽:虛線框的球就是這棵樹的袋外樣本。
這一棵樹的抽樣
被抽中幾筆(去重)
OOB 幾筆
這次的 OOB 比例
累計樹的棵數 B0
累計 OOB 比例
理論值 1/e36.79%
怎麼看
20 顆球=20 筆訓練資料。每按一次就是「長一棵樹」:有放回抽 20 次,實心=被抽到(右上角的 ×k 是被抽到幾次),虛線空框=這棵樹沒看過它。按幾十次,右邊的累計 OOB 比例就會定在 36.8% 附近。
OOB 誤差怎麼算 ISLP §8.2.1
對第 i 筆資料,把「所有沒用到它的那些樹」找出來(大約 B/3 棵),用它們預測第 i 筆再平均(或投票),得到一個 OOB 預測。n 筆各做一次,就得到 OOB 誤差。B 夠大時它幾乎等於 LOOCV 誤差,但成本只有一次配適。

還有一件實務上很重要的事:B 不是需要調的參數。 ISLP 圖 8.8 顯示誤差隨 B 上升而下降、然後平掉就不動了—— B 太大不會過度配適(只是浪費算力),B 太小才會欠配適。 所以做法是「挑一個大到誤差已經平掉的 B」,通常 100 到 500 就夠。

講義完整實作:Boston 上的 bagging

講義 08 · bagging = max_features = p 的 random forest
bag_boston = RF(max_features=X_train.shape[1], random_state=0) bag_boston.fit(X_train, y_train) bag_boston = RF(max_features=X_train.shape[1], n_estimators=500, random_state=0).fit(X_train, y_train) y_hat_bag = bag_boston.predict(X_test) np.mean((y_test - y_hat_bag)**2)
預期輸出
np.float64(14.605662565263161)

max_features=X_train.shape[1](=12)就是「每次分裂都考慮全部變數」,也就是 bagging。B = 100 時測試 MSE 是 14.6347(儲存格 66),B = 500 時 14.6057——幾乎沒動,正是「B 大不會過度配適也不會再變好」。對照單一棵剪枝樹的 28.07:誤差直接砍半

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 64、66、68
觀念釐清
Q:Bagging 為什麼能降變異,卻幾乎不降偏差?

從公式看最清楚。設每棵樹的預測 $\hat f^{*b}(x)$ 期望值都是 $\mu(x)$、變異數都是 $\sigma^2(x)$、兩兩相關係數是 $\rho$。那麼平均的期望與變異是

$$\mathbb{E}\left[\hat f_{\text{bag}}(x)\right] = \mu(x), \qquad \mathrm{Var}\left[\hat f_{\text{bag}}(x)\right] = \frac{1-\rho}{B}\sigma^2(x) + \rho\, \sigma^2(x)$$

期望值完全沒變——平均一堆同分佈的東西,期望還是那個期望,所以偏差 $\mu(x) - f(x)$ 一動也不動。變異數則被壓成兩項:第一項隨 B 變大而消失,第二項 $\rho \sigma^2$ 不隨 B 消失。這第二項就是下一節 random forest 要對付的東西。

所以 bagging 的正確用法是:拿變異很大、偏差很小的東西去平均。長很深不剪枝的樹剛好就是這種東西——它把訓練資料配到幾乎完美(低偏差),但資料換一點就長成另一棵(高變異)。反過來,拿 bagging 去平均一堆線性迴歸幾乎沒有用:線性迴歸本來變異就小,沒什麼可壓的。

Q:既然有 OOB 誤差,還需要交叉驗證嗎?

估 bagging/random forest 本身的測試誤差時,OOB 就夠了,而且便宜太多:配一次模型就順手拿到,不必像 k-fold 那樣配 k 次。ISLP 說 B 夠大時 OOB 誤差幾乎等於 LOOCV 誤差

要小心的是兩件事。第一,如果你用 OOB 誤差去挑超參數(例如挑 m、挑樹的深度),那被挑中的那組的 OOB 誤差就跟第 5 章講的一樣會偏低,不能再當誠實的測試誤差報出來。第二,OOB 只有在有 bootstrap 抽樣時才存在——boosting 沒有 bootstrap,所以沒有 OOB,只能用驗證集或 CV。

QUIZ · Bagging 與 OOB

做 bagging 時,每一棵樹該長多深?

(A) 長到很深、不剪枝——刻意讓每棵樹低偏差高變異,再靠平均壓變異
(B) 用交叉驗證幫每一棵樹各自挑最佳的 ccp_alpha,才不會過度配適
(C) 全部剪成單一分裂的 stump,這樣集成才穩定
PART 07 · Random Forest

每次分裂只看 m 個變數:去相關才是關鍵 ISLP §8.2.2講義 08 · p.38–42

上一節的公式留了一個尾巴:

$$\mathrm{Var}\left[\hat f_{\text{bag}}(x)\right] = \frac{1-\rho}{B}\sigma^2(x) + \rho\, \sigma^2(x)$$

第二項 $\rho \sigma^2$ 不管 B 多大都不會消失。 $\rho$ 是樹跟樹之間預測的相關係數,而 bagging 的樹相關得很嚴重。原因很具體:

如果資料裡有一個特別強的變數,那麼幾乎每一棵 bagged 樹都會拿它當根節點的分裂變數。 根一樣,接下來的結構也就大同小異——B 棵樹長得像複製品,$\rho$ 接近 1, 平均一百棵跟平均一棵差不多。

Random forest 的解法簡單到有點粗暴:每一次分裂, 只准從隨機挑出的 $m$ 個變數裡選($m < p$,而且每一刀都重新抽一次)。 典型取 $m \approx \sqrt{p}$(分類)或 $m = p/3$(迴歸)。

於是平均有 $(p-m)/p$ 比例的分裂根本看不到那個強變數, 其他變數就有機會出頭。樹跟樹長得不一樣了,$\rho$ 掉下來, 第二項跟著縮小——這叫去相關(decorrelate)。 注意 $m = p$ 時 random forest 就退化成 bagging, 所以 bagging 只是 random forest 的一個特例。

圖表需要連網載入 Chart.js。此圖的重點:m 愈小,樹與樹之間的相關 ρ 愈低(右側面板),但單棵樹也愈弱——m 是一個要調的參數,不是愈小愈好。
換資料集看 m 的效果。注意兩份資料給出相反的結論。
每個 m 的表現(B = 300)
m = p(bagging)
m = p/2
m ≈ √p
m = 2
單一棵樹
怎麼看這張圖 圖 8.8/8.10
x 軸是樹的棵數 B(前 B 棵的平均預測),y 軸是測試 MSE。右邊面板每一列同時列出該 m 的測試 MSE 與樹間平均相關 ρρ 一定隨 m 變小而下降——那是去相關的直接證據;但誤差會不會跟著下降,要看資料。
兩份資料為什麼結論不同
Boston:只有 12 個變數、lstatrm 真的最有用,硬是不給樹看它們只是自找麻煩——所以 m = p 最好。lab 的原話就是「隨機森林比 bagging 表現稍差」。
模擬資料:30 個彼此相關的變數、其中 20 個都帶訊號,一個特別強——這正是 ISLP §8.2.2 描述的情境,這時 m ≈ √p 明顯贏。

講義完整實作:random forest 與變數重要度

講義 08 · max_features = 6 的 random forest + feature_importances_
RF_boston = RF(max_features=6, random_state=0).fit(X_train, y_train) y_hat_RF = RF_boston.predict(X_test) np.mean((y_test - y_hat_RF)**2) feature_imp = pd.DataFrame( {'importance':RF_boston.feature_importances_}, index=feature_names) feature_imp.sort_values(by='importance', ascending=False)
預期輸出
         importance
lstat      0.356203
rm         0.332163
ptratio    0.067270
crim       0.055404
indus      0.053851
dis        0.041582
nox        0.035225
tax        0.025355
age        0.021506
rad        0.004784
chas       0.004203
zn         0.002454

測試 MSE 20.0428(儲存格 70),比 bagging 的 14.63 差——在 Boston 上限制 m 沒有幫助,這一點 lab 講得很直白。下面那張表是變數重要度:每個變數造成的不純度下降總量,在所有樹上平均。lstat(0.356)與 rm(0.332)合起來就佔了近 70%——社區的財富水準與房子大小最重要。這張表怎麼讀、有什麼陷阱,放在 PART 11 講。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 70、72
觀念釐清
Q:Random Forest 的 m 為什麼要小於 p?「去相關」在數學上到底降低了什麼?

降低的是變異數公式裡那個不隨 B 消失的項

$$\mathrm{Var}\left[\hat f_{\text{avg}}\right] = \frac{1-\rho}{B}\sigma^2 + \rho\,\sigma^2$$

把 B 開到一萬,第一項趨近 0,剩下的是 $\rho \sigma^2$。所以「多種幾棵樹」有一個天花板,而天花板的高度由 $\rho$ 決定。bagging 沒有任何機制去壓 $\rho$——每棵樹都看得到全部變數,貪婪法就會一再選中同一個最強的變數,$\rho$ 因此很高(本頁 Boston 上實測 m = p 時 ρ ≈ 0.82)。限制 m 之後,不同的樹被迫用不同的變數,ρ 掉到 0.68。

但這裡有一個取捨,而且非常實在:m 變小同時讓每棵樹變差,也就是上式的 $\sigma^2$ 變大(元件的側欄有列出每棵樹預測的變異,m 愈小它愈大)。所以 $\rho \sigma^2$ 是「$\rho$ 下降 × $\sigma^2$ 上升」的乘積,不保證變小。$m$ 太小的時候,樹弱到連訊號都找不到,整體反而變糟。

什麼時候壓 $\rho$ 划算?變數多、而且彼此相關的時候。此時「不給樹看變數 A」的損失很小(相關的變數 B 幾乎能代替它),但換到的多樣性很大。反過來像 Boston 這種只有 12 個變數、其中兩個明顯不可替代的資料,遮住它們的代價就付不起。結論:$m$ 是超參數,$\sqrt{p}$ 只是好用的預設值,該調就調。

QUIZ · Random Forest

Random forest 每次分裂只從 m 個隨機挑出的變數裡選。這個 m 是怎麼抽的?

(A) 每一次分裂都重新隨機抽 m 個變數,不是整棵樹共用一組
(B) 每棵樹開始前抽一次 m 個變數,整棵樹都只用這 m 個
(C) 抽出重要度最高的 m 個變數,這樣樹才不會浪費分裂
PART 08 · Boosting

慢慢學:AdaBoost 與梯度提升 ISLP §8.2.3講義 08 · p.43–51ESL §10.1–10.10 · 進階

Bagging 與 random forest 是並行的:B 棵樹互不相干, 誰先誰後無所謂,可以開 B 個執行緒一起長。Boosting 完全相反——樹是序列長出來的, 每一棵都靠前面那些樹的資訊決定自己要做什麼。而且沒有 bootstrap, 每棵樹都看全部資料,只是看的目標被改過。

ISLP 演算法 8.2 是整章最值得背下來的十行:

梯度提升(regression 版),ISLP 演算法 8.2 1. 令 $\hat f(x) = 0$, 殘差 $r_i = y_i$。
2. 對 $b = 1, 2, \dots, B$:
   (a) 用 $(X, r)$ 配一棵只有 $d$ 刀($d+1$ 個葉子)的小樹 $\hat f^b$;
   (b) 把收縮後的它加進去:$\hat f(x) \leftarrow \hat f(x) + \lambda \hat f^b(x)$;
   (c) 更新殘差:$r_i \leftarrow r_i - \lambda \hat f^b(x_i)$。
3. 輸出 $\hat f(x) = \sum_{b=1}^{B} \lambda \hat f^b(x)$。

注意 (a):配的目標是殘差 $r$,不是 $y$。這就是整個把戲。 第一棵樹抓走一部分結構,剩下沒解釋掉的變成新的目標,第二棵樹去抓它,以此類推。 ISLP 的說法是 boosting 學得很慢(learns slowly)—— 而在統計學習裡,學得慢的方法往往表現得好。

按「單步」加一棵淺樹:它配的是目前的殘差,然後乘上 λ 加進配適裡。
λ0.35
虛擬碼 CODE
f = 0;r = y for b in range(B): tree = 配一棵淺樹(X, r) f += λ * tree(X) r -= λ * tree(X)
目前狀態
已加入的樹 b0
學習率 λ0.35
每棵樹的葉子數2
訓練 MSE
殘差的標準差
兩個面板
上面是目前的配適 $\hat f$(橘線)疊在資料上。下面是目前的殘差 $r$,以及下一棵樹準備加上去的那個階梯(虛線)——注意它總是往殘差最偏的地方去。
λ 調小:每一步只走一點點,需要更多棵樹,但配出來的曲線更平滑;λ 調到 1:幾步就衝過去,然後開始抖。

Boosting 有三個要調的參數,而且跟 bagging 不同, 它真的會過度配適

參數意思典型值調錯會怎樣
B(樹的棵數)跑幾輪由 CV 決定太大會過度配適(只是通常發生得很慢)。bagging 沒有這個問題
λ(學習率/收縮)每棵樹只採用 λ 倍0.01 或 0.001太小 → 需要非常大的 B;太大 → 幾步就衝過頭,開始配噪音
d(每棵樹幾刀)交互作用深度常常 1 就夠d = 1(stump)的集成是加法模型;d 愈大能抓愈高階的交互作用,也愈容易過度配適

$B$ 與 $\lambda$ 是綁在一起的:λ 砍十倍,B 大約要放大十倍。 lab 用的是 n_estimators=5000, learning_rate=0.001, 換成 learning_rate=0.2 之後測試 MSE 幾乎一樣(14.48 vs 14.50)—— 在這份資料上兩組設定都落在「已經收斂」的區域裡。

AdaBoost:不改目標,改權重

Boosting 還有另一種更早的版本。AdaBoost 不去配殘差, 而是調整每一筆資料的權重:這一輪答錯的點,下一輪權重變大, 於是下一個分類器會特別在意它們。講義第 44 頁的式子是

$$\alpha_j = \eta \log \frac{1 - e_j}{e_j}, \qquad w_{j+1}(i) = \begin{cases} w_j(i) \cdot e^{-\alpha_j} & \text{答對} \\ w_j(i) \cdot e^{+\alpha_j} & \text{答錯} \end{cases}$$

$e_j$ 是第 $j$ 個分類器的加權錯誤率,$\alpha_j$ 同時是它在最終投票裡的份量: 錯誤率愈低、$\alpha$ 愈大、票愈重。最終預測是 $\hat y = \arg\max_k \sum_{j:\, h_j(x) = k} \alpha_j$。 下面的元件取 $\eta = \tfrac{1}{2}$,那正是 Freund–Schapire 原版的 AdaBoost; $\eta = 1$(scikit-learn 的 SAMME)重新加權更兇,在小樣本上容易讓權重 幾輪就集中到少數幾點上。

按「單步」跑一輪 AdaBoost:看答錯的點怎麼變大,切點怎麼被逼著移動。
這一輪
第幾輪 j0 / 10
這一輪的切點
加權錯誤率 e
這一輪的權重 α
集成的訓練錯誤率
怎麼看
每個圈是一筆資料,圈的大小=它現在的權重;上排是 A 類、下排是 B 類。灰色虛線是這一輪 stump 的切點,答錯的點畫成粗黑框。按單步幾次就會看到:答錯的圈一輪一輪變大,逼得後面的 stump 改切在別的地方。
集成錯誤率為什麼會來回跳
每一輪都在換一份加權後的資料,所以新加進來的 stump 是為了那份加權資料而選的,對「未加權的訓練錯誤率」不保證每一輪都更好。這個例子在第 6 輪第一次歸零、第 7 輪又跳回 1 個錯,之後穩定在 0——看趨勢,不要看單一輪。
為什麼 boosting 的樹要很淺 ISLP §8.2.3
因為它降的是偏差不是變異。序列裡的每一棵只需要修掉一小塊誤差,所以 stump 就夠;樹長深了反而一步就把殘差配光,後面的樹只能開始配噪音。
這跟 bagging 剛好互補——bagging 要深樹低偏差,boosting 要淺樹低變異。

講義完整實作:Boston 上的梯度提升

講義 08 · GradientBoostingRegressor(5000 棵樹,λ = 0.001)
boost_boston = GBR(n_estimators=5000, learning_rate=0.001, max_depth=3, random_state=0) boost_boston.fit(X_train, y_train) y_hat_boost = boost_boston.predict(X_test); np.mean((y_test - y_hat_boost)**2)
預期輸出
np.float64(14.481405918831591)

測試 MSE 14.4814,跟 bagging 的 14.63 差不多,比 RF 的 20.04 好。把 learning_rate 換成 0.2(儲存格 82)得到 14.5015——幾乎一樣。儲存格 78 用 staged_predict() 畫出訓練與測試誤差隨棵數變化的曲線,那是判斷「B 是不是太大」唯一可靠的辦法。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 76、78、80、82
觀念釐清
Q:Bagging 與 Boosting 的根本差別是什麼?

三句話:並行 vs 序列、降變異 vs 降偏差、深樹 vs 淺樹。而第三點是前兩點的必然結果。

Bagging 降變異。它平均一堆同分佈的估計,期望值不變(偏差不變)、變異被 $1/B$ 壓下去。既然偏差不會被改善,就必須讓每棵樹的偏差一開始就很低——所以樹要長很深、不剪枝。樹的高變異不是問題,那正是要被平均掉的東西。

Boosting 降偏差。它把「還沒解釋掉的部分」(殘差/被放大權重的難樣本)交給下一棵樹,是一個逐步把偏差咬掉的過程。每一步只需要修一小塊,所以每棵樹只要很淺(ISLP 說 $d = 1$ 常常就夠)。反過來,如果第一棵樹就長很深,它會把殘差一次配光——包括噪音;後面 4999 棵就只剩噪音可配,而每一棵都在增加整體的變異。

由此還推得幾個實務差別:bagging/RF 的 B 不會過度配適(平均更多同分佈的東西只會更穩),所以 B 挑大一點就好;boosting 的 B 會過度配適,必須用 CV 或 early stopping 挑。而且 bagging 可以完全平行、boosting 天生序列(這正是 XGBoost 要花那麼多力氣在工程上加速的原因)。最後:bagging 有 bootstrap 所以有免費的 OOB 誤差,boosting 沒有 bootstrap,所以沒有 OOB。

QUIZ · Boosting

梯度提升的第 b 棵樹,配的目標(response)是什麼?

(A) 目前模型的殘差 r,不是原始的 y
(B) 原始的 y,但只用 bootstrap 抽出來的那份資料
(C) 原始的 y,但只用前一棵樹預測錯的那些觀測值
PART 09 · XGBoost 與後繼者

XGBoost、LightGBM、CatBoost 與該調的超參數 講義 08 · p.52–61ESL §10.10–10.14 · 進階ESL 進階

這一節是課堂沒細講的延伸(講義 p.52–61):三個現代 GBDT 套件與該調的超參數。 第一輪讀可以整節略過,回頭要用套件時再看。

上一節的梯度提升在概念上已經完整了,剩下的全是工程正則化。 三個套件把這件事推到了工業級:

全名/來源核心賣點在 lab 的實測
XGBoostExtreme Gradient Boosting目標函數內建正則化(葉子數與葉值的 L1/L2 罰項);用類似牛頓法的二階近似而非單純梯度;分位數草圖做近似分裂搜尋、稀疏感知、快取友善24.3 秒(儲存格 114)
LightGBMMicrosoft直方圖分箱(預設 255 箱)把排序的 $O(n \log n)$ 降成 $O(n)$;GOSS 只對小梯度的樣本抽樣;leaf-wise 而非 level-wise 長樹;互斥特徵綁定(EFB)13.1 秒(儲存格 185)
CatBoostYandex對稱樹(同一層用同一個分裂條件,本身就是正則化,預測極快);ordered boosting 用另一份子集算殘差以防過度配適;類別變數原生支援15.1 秒(儲存格 210)
對照組sklearnGradientBoostingClassifier純 Python 迴圈的參考實作624.8 秒(儲存格 113)

同一份系外行星資料(3197 個特徵)、同樣 100 棵樹、 max_depth=2625 秒 vs 13 秒,差了快 50 倍,而正確率還略微更好 (0.9874 → 0.9914)。資料一大,這種差距就是「跑得完」與「跑不完」的差別。

XGBoost 的正則化到底加了什麼 原本的梯度提升只最小化損失。 XGBoost 的目標函數多了一項對每棵樹本身的懲罰:
$$\text{Obj} = \sum_i L(y_i, \hat y_i) + \sum_b \Omega(f_b), \qquad \Omega(f) = \gamma |T| + \tfrac{1}{2}\lambda \sum_{m} w_m^2$$ $|T|$ 是葉子數、$w_m$ 是葉子的輸出值。$\gamma$ 就是「多開一個葉子要付的錢」—— 形式上跟本頁 PART 03 的成本複雜度剪枝一模一樣,只是這次直接寫進目標函數, 分裂增益算出來小於 $\gamma$ 就不切。這也是為什麼 XGBoost 常被稱為 「a regularized version of gradient boosting」。

該調哪些超參數?講義第 60–61 頁把三個套件的參數名對照起來, 分成「求快」「求準」「防過度配適」三組:

目的XGBoostLightGBMCatBoost
求快(抽樣列/欄、少幾棵)subsample · colsample_bytree · n_estimatorsbagging_fraction · feature_fraction · num_iterationssubsample · rsm · iterations
控制過度配適/求準learning_rate(0.01–0.2)· max_depth · min_child_weight · gammalearning_rate · max_depth · num_leaves · min_data_in_leaflearning_rate · depth · l2-leaf-reg
類別變數實驗性支援(建議自己先編碼)categorical_featurecat_features · one_hot_max_size

lab 在 heart_disease.csv(303 筆、13 個特徵,用 !wget 抓 Packt 的那份) 上一個一個網格搜過去,基準是 0.79:

調的參數搜尋範圍最佳值最佳 CV 正確率lab 儲存格
(基準)全部預設0.79124
learning_rate0.01 – 0.50.50.79557130
max_depth2, 3, 5, 6, 830.81197134
gamma0 – 20.10.81197138
min_child_weight1 – 540.81197142
subsample0.5 – 10.50.81536145
colsample_bytree0.5 – 10.70.80552149
n_estimators100 – 8004000.79541153

最大的一筆進步來自把 max_depth 從預設的 6 降到 3(0.79 → 0.812)——「把模型調簡單一點」subsample 降到 0.5 又多賺一點,同樣是在減少變異。 而把樹加到 800 棵反而變差:這份資料只有 303 筆,B 大不會有幫助。

講義完整實作:XGBoost + StratifiedKFold 的基準分數

講義 08 · XGBClassifier 在 heart_disease 上的 5 折基準
kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) # The 'binary:logistic' objective is standard for binary 分類 in determining the loss function model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=42) # Obtain scores of cross-validation scores = cross_val_score(model, X, y, cv=kfold) # Display accuracy print('Accuracy:', np.round(scores, 2)) # Display mean accuracy print('Accuracy mean: %0.2f' % (scores.mean()))
預期輸出
Accuracy: [0.82 0.75 0.74 0.82 0.8 ]
Accuracy mean: 0.79

兩個細節值得學。① StratifiedKFold 而不是 KFold:分類問題要保住每折的類別比例。② kfold 物件存下來重複用:後面所有 GridSearchCV 都吃同一組折,這樣「調參前 vs 調參後」的分數才可比——這正是第 5 章 PART 03 的規矩。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 119、123、124

講義完整實作:early stopping

講義 08 · early_stopping_rounds 取代「調 n_estimators」
eval_metric = 'error' model = XGBClassifier(random_state=42, early_stopping_rounds=10, eval_metric=eval_metric) eval_set = [(X_test, y_test)] # eval_metric 和 eval_set 保留在 fit() 中,因為它們是訓練時需要的資料 model.fit(X_train, y_train, eval_set=eval_set, verbose=True) y_pred = model.predict(X_test) accuracy = accuracy_score(y_test, y_pred) print("Accuracy: %.2f%%" % (accuracy * 100.0))
預期輸出
[0]	validation_0-error:0.15789
[1]	validation_0-error:0.19737
[2]	validation_0-error:0.17105
[3]	validation_0-error:0.19737
[4]	validation_0-error:0.15789
[5]	validation_0-error:0.18421
[6]	validation_0-error:0.18421
[7]	validation_0-error:0.18421
[8]	validation_0-error:0.18421
[9]	validation_0-error:0.18421
Accuracy: 84.21%

early_stopping_rounds 不是超參數,是策略:設 n_estimators=5000 加上 early_stopping_rounds=100,連續 100 輪沒進步就停,等於讓演算法自己決定 B。這裡 10 輪就停了,測試正確率 84.21%。注意 eval_set 用的是測試集——這在教學程式碼裡很常見,但正式做法要另外切一份驗證集,否則 B 是照測試集挑的,報出來的 84.21% 就偏樂觀了。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 162、164
QUIZ · XGBoost 與後繼者

XGBoost 相對於課本演算法 8.2 的梯度提升,最主要的統計差別是什麼?(不算工程加速)

(A) 目標函數裡多了對樹本身的正則化項(葉子數 γ|T| 與葉值的 L2 罰項)
(B) 它改用 bootstrap 抽樣,所以每棵樹只看部分資料
(C) 它把序列改成並行,B 棵樹可以同時長
PART 10 · Stacking 與 BART

變數重要度、堆疊法,以及貝氏版的加法樹 ISLP §8.2.4講義 08 · p.66–79ESL 進階

這一節是課堂沒細講的延伸(講義 p.66–79、ISLP §8.2.4):變數重要度的讀法、 stacking,以及貝氏版的加法樹 BART。第一輪讀可以整節略過。

變數重要度:把可解釋性買回來一點

單一棵樹最大的優點是可以畫出來給人看。集成之後這個優點就沒了—— 你不可能把 500 棵樹貼在牆上。變數重要度(variable importance) 是把可解釋性買回來一點點的標準做法:

ISLP 圖 8.9 就是 Heart 資料上的這張圖(相對於最大值標準化), 最重要的三個是 ThalCaChestPain。 下面用 lab 的 Boston 例子重畫(Heart 不在 ISLP 0.4.0 裡):

圖表需要連網載入 Chart.js。此圖的重點:lstat 與 rm 兩個變數就吃掉近 70% 的不純度下降總量,其餘十個變數加起來還不到三分之一。
換一種重要度的定義,看排名會不會變。
排行
第一名
第二名
前兩名合計佔比
這個模型的測試 MSE
兩種重要度差在哪 ISLP 圖 8.9
不純度下降(impurity / Gini importance):直接從訓練好的樹裡加總,零成本,但它是在訓練資料上算的,而且會系統性偏袒「取值很多的變數」(連續變數、高基數的類別變數)。
Permutation importance:把某一欄隨機打亂,看測試誤差變差多少。定義在測試資料上,直接對應「這個變數對預測的貢獻」,但要多跑很多次預測。
最大的陷阱:相關的變數會互相稀釋
如果兩個變數幾乎一樣(例如身高(公分)與身高(英吋)),樹會隨機挑一個來切,兩個的重要度各自被砍半,看起來都「不太重要」。重要度低不等於沒用,只等於「在有其他變數陪著的情況下,這個變數沒有被用到」。
變數重要度不是因果,也不是「拿掉它會怎樣」 三件常見的誤讀,每一件都會出事:
① 它不是因果效應。重要度高只表示「樹很愛用它來切」, 跟「改變它會改變 y」是兩件事。
② 它不帶方向。線性迴歸的係數有正負,重要度永遠是正的—— 你不知道它是往上推還是往下壓(要方向請用 partial dependence 或 SHAP)。
③ 相關變數會互相稀釋。上面那張側欄卡講的就是這件事。 所以「重要度排最後 → 可以刪掉」是危險的推論。

Stacking:不用投票,訓練一個模型來合併

PART 06 的 voting 是用一個固定的規則(多數票、平均)去合併。 Stacking 問了一個很自然的問題:為什麼不訓練一個模型來做這件合併?

做法的關鍵在怎麼造合併器的訓練資料——這裡有一個必須避開的洩漏:

  1. 對集成裡的每個成員模型,用交叉驗證產生 out-of-sample 的預測(每一筆的預測,都來自沒看過它的那個折)。
  2. 把這些預測當成新的特徵,原始的 y 照抄,訓練一個合併器 (blender/meta-learner,常用邏輯斯迴歸這種簡單模型)。
  3. 預測時:成員各給一個預測,合併器把它們吃進去輸出最終答案。

第 1 步為什麼一定要用 CV?因為如果拿成員模型在訓練資料上的預測去餵合併器, 那些預測好得不真實(成員在訓練資料上本來就準),合併器會學到「完全相信最會過度配適的那個成員」。 這就是第 5 章那個「所有用到 y 的步驟都要關在折裡面」的老規矩。

講義完整實作:StackingClassifier

講義 08 · Stacking(KNN + 決策樹 + 樸素貝氏,合併器是邏輯斯迴歸)
sclf = StackingClassifier(estimators=estimators, final_estimator=LogisticRegression()) print('5-fold cross validation:\n') for clf, label in zip([clf1, clf2, clf3, sclf], ['KNN', 'DecisionTreeClassifier', 'Naive Bayes', 'StackingClassifier']): scores = cross_val_score(clf, X_train, y_train, cv=kf5, scoring='accuracy') print("Accuracy: %0.2f (+/- %0.2f) [%s]" % (scores.mean(), scores.std(), label))
預期輸出
5-fold cross validation:

Accuracy: 0.78 (+/- 0.06) [KNN]
Accuracy: 0.73 (+/- 0.07) [DecisionTreeClassifier]
Accuracy: 0.79 (+/- 0.07) [Naive Bayes]
Accuracy: 0.81 (+/- 0.05) [StackingClassifier]

0.81,跟同一組成員的 hard voting(0.82,儲存格 238)幾乎一樣。這很典型:成員只有三個、資料只有 200 筆訓練樣本時,合併器沒什麼可學的,複雜的做法不會贏過簡單的平均。stacking 真正發威是在成員很多、而且強弱差很多的時候(Kaggle 的解法動輒疊十幾個模型)。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 240

BART:貝氏版的加法樹

BART(Bayesian additive regression trees)站在 bagging 與 boosting 中間:

因為每一輪只是微調,BART 沒辦法一次把資料配得太狠, 這本身就是防過度配適的機制。要選三個數字:樹的棵數 $K$、迭代次數 $B$、 丟掉的暖機輪數 $L$。講義的建議是 $K = 200$、$B = 1000$、$L = 100$, 最終預測是暖機後的平均

$$\hat f(x) = \frac{1}{B - L} \sum_{b = L+1}^{B} \hat f^b(x)$$

BART 出名的地方是幾乎不用調參就能跑出好結果(out-of-box performance)。

講義完整實作:BART

講義 08 · ISLP.bart 在 Boston 上
bart_boston = BART(random_state=0, burnin=5, ndraw=15) bart_boston.fit(X_train, y_train) yhat_test = bart_boston.predict(X_test.astype(np.float32)) np.mean((y_test - yhat_test)**2)
預期輸出
np.float64(22.145009458109225)

測試 MSE 22.15,跟 random forest 的 20.04 同一個量級。注意 burnin=5, ndraw=15 小得離譜——那是為了讓課堂上跑得完,正式用要拉到 $L = 100$、$B = 1000$。儲存格 90 的 variable_inclusion_ 是 BART 版的變數重要度:算每個變數在整組樹裡出現幾次,lstat 31.0、rm 29.8 最高,跟上面 random forest 的排名一致。

來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 86、88
QUIZ · Stacking 與變數重要度

訓練 stacking 的合併器時,成員模型的預測值一定要用交叉驗證產生。為什麼?

(A) 否則成員在訓練資料上的預測好得不真實,合併器會學成「相信最會過度配適的那個成員」
(B) 因為 scikit-learn 的 StackingClassifier API 規定必須傳入 cv 參數
(C) 因為合併器需要比成員模型更多的訓練資料,CV 可以把資料量放大
EXERCISES · 練習

動手驗證:ISLP 第 8 章精選題 ISLP §8.4 習題

下面幾題取自 ISLP §8.4 的課後習題,題號都對得回課本。先自己想過再點選項; 每個選項——包含錯的——都寫了為什麼。想看完整解答再對照下面3個站。

EXERCISE 1 · ISLP 8.4 第 3 題

課本第 3 題要你把 Gini 指數、錯誤率、交叉熵都畫成 $\hat p_{{m1}}$ 的函數(兩類)。畫出來之後,哪一句話最能說明「為什麼分裂準則不用錯誤率」?

(A) 錯誤率是兩段直線,Gini 與交叉熵是嚴格凹的曲線;只有凹的函數才保證「切一刀」的不純度下降量嚴格大於 0
(B) 三條曲線的最大值都在 p̂ = 0.5,所以錯誤率沒有辦法分辨純與不純
(C) 交叉熵的最大值是 log 2 ≈ 0.693,跟另外兩個不同,所以三者不能一起比較
EXERCISE 2 · ISLP 8.4 第 5 題

十個 bootstrap 樣本各配一棵分類樹,對同一個 X 給出 $P(\text{{紅}} \mid X)$ 的十個估計:0.1, 0.15, 0.2, 0.2, 0.55, 0.6, 0.6, 0.65, 0.7, 0.75。用多數投票與用平均機率,分別會判成哪一類?

(A) 多數投票 → 紅;平均機率 → 綠
(B) 兩種方法都判紅
(C) 兩種方法都判綠,因為十個估計的中位數是 0.375
EXERCISE 3 · ISLP 8.4 第 2 題

課本第 2 題要你說明:用深度 1 的樹(stump)做 boosting,為什麼結果是一個加法模型 $f(X) = \sum_{{j=1}}^{{p}} f_j(X_j)$?

(A) 每個 stump 只切一個變數,所以它是「只含那一個變數的函數」;把所有 stump 依變數分組加總,就得到每個變數各一個函數的加法形式
(B) 因為 stump 的葉子只有兩個,所以每個 stump 都是線性函數,線性函數相加還是線性
(C) 因為 boosting 的收縮參數 λ 讓每棵樹的貢獻很小,小貢獻疊起來近似可加
EXERCISE 4 · ISLP 8.4 第 7 題

課本第 7 題要你在 Boston 上掃過一整片 max_features(m)與 n_estimators(B)的組合,畫成圖 8.10 那樣。預期會看到什麼?

(A) 每條曲線都隨 B 上升而下降、然後平掉;不同 m 的曲線收斂到不同高度,而在 Boston 上 m = p 那條最低
(B) 曲線會先下降、到某個 B 之後又上升,所以要用 CV 挑最佳的 B
(C) m 愈小曲線一定愈低,因為去相關永遠讓變異更小
REFERENCE · 總覽

樹狀方法與集成學習速查表 ISLP Ch.8

考前把這一頁掃過去就好。

五種方法對照

方法樹怎麼來樹的深度主要降的是B 會過度配適嗎免費驗證集
單一棵樹一棵,貪婪長 + 剪枝由 CV 選 α—(偏差與變異都要自己顧)沒有(要 CV)
BaggingB 個 bootstrap,並行很深、不剪枝變異不會(B 大只是浪費算力)有(OOB)
Random Forest同上 + 每刀只看 m 個變數很深、不剪枝變異(多壓了 ρ)不會有(OOB)
Boosting配殘差/調權重,序列很淺(d 常常 = 1)偏差(要用 CV 或 early stopping)沒有(沒 bootstrap)
BART微調上一輪的樹,MCMC小樹兩者(貝氏平均)不會(B 是 MCMC 迭代數)沒有

lab 在 Boston 上的實測數字(同一份 70/30 切分)

模型設定測試 MSElab 儲存格
剪枝後的單一棵樹ccp_alpha 由五折 CV 選28.0759
Baggingmax_features=12, B = 10014.6366
Baggingmax_features=12, B = 50014.6168
Random Forestmax_features=6, B = 10020.0470
Gradient BoostingB = 5000, λ = 0.001, d = 314.4880
Gradient BoostingB = 5000, λ = 0.2, d = 314.5082
BARTburnin=5, ndraw=15(刻意調小)22.1588

兩個結論:①集成把單一棵樹的誤差砍了一半 (28.07 → 14.5 左右)。②在這份資料上 bagging 與 boosting 打平,random forest 反而較差—— m 不是愈小愈好,別把 $\sqrt{p}$ 當定律。

公式速查

名稱式子備註
樹的模型形式$f(X) = \sum_{m=1}^{M} c_m \mathbf{1}(X \in R_m)$式 8.9
要最小化的 RSS$\sum_{j=1}^{J} \sum_{i \in R_j} (y_i - \hat y_{R_j})^2$式 8.1
一刀的目標$\sum_{i \in R_1(j,s)} (y_i - \hat y_{R_1})^2 + \sum_{i \in R_2(j,s)} (y_i - \hat y_{R_2})^2$式 8.3,掃過所有 (j, s)
成本複雜度剪枝$\sum_{m=1}^{|T|} \sum_{i \in R_m} (y_i - \hat y_{R_m})^2 + \alpha|T|$式 8.4,形式同 lasso
錯誤率$E = 1 - \max_k \hat p_{mk}$式 8.5,當分裂準則
Gini 指數$G = \sum_{k} \hat p_{mk}(1 - \hat p_{mk})$式 8.6
交叉熵$D = -\sum_{k} \hat p_{mk} \log \hat p_{mk}$式 8.7
Bagging$\hat f_{\text{bag}}(x) = \frac1B \sum_{b} \hat f^{*b}(x)$分類版改多數投票
平均的變異$\frac{1-\rho}{B}\sigma^2 + \rho\sigma^2$第二項不隨 B 消失 → random forest 要壓 ρ
OOB 比例$(1 - 1/n)^n \to e^{-1} \approx 0.368$第 5 章的 0.632 反面
Boosting 更新$\hat f(x) \leftarrow \hat f(x) + \lambda \hat f^b(x)$式 8.10
Boosting 殘差$r_i \leftarrow r_i - \lambda \hat f^b(x_i)$式 8.11
Boosting 輸出$\hat f(x) = \sum_{b=1}^{B} \lambda \hat f^b(x)$式 8.12,d = 1 時是加法模型
AdaBoost 的權重$\alpha_j = \eta \log\frac{1-e_j}{e_j}$講義 p.44,$e_j$ 是加權錯誤率

三個 GBDT 套件怎麼選(講義 p.62)

套件什麼時候選它
XGBoost社群最大、生產環境的支援最完整。不確定就先用它
LightGBM資料很大、同時要速度與準確率時的較好選擇
CatBoost資料量小,或類別變數很重要的時候
三個一定要記住的觀念 1. 樹的分裂用 Gini/交叉熵,不用錯誤率。 因為錯誤率是折線、對純度的變化不敏感,會把「生出一個純葉子」的好刀報成「下降量 0」。
2. Bagging 降變異、Boosting 降偏差——所以 bagging 的樹要很深,boosting 的樹要很淺。 平均不會改變偏差,序列修正不需要深樹。順帶:bagging 的 B 不會過度配適,boosting 的會。
3. Random Forest 的 m 在壓 $\rho\sigma^2$ 裡的 $\rho$,代價是 $\sigma^2$ 變大。 變數多又彼此相關時這筆交易划算;變數少又不可替代時(例如 Boston)就不划算。 $\sqrt{p}$ 是預設值,不是定律。

本頁「預期輸出」逐字取自課程 lab notebook(老師在課程環境實跑);圖表用的烘焙資料由 tools/frames/ 在固定種子下產生,環境為 numpy 1.24.4 · pandas 2.3.2 · scikit-learn 1.6.1 · scipy 1.13.1 · statsmodels 0.14.2 · ISLP 0.4.0 · pygam 0.10.1。每張卡下方的「來源」標了 lab 的儲存格編號,可以直接回去對。

QUIZ · 自我檢測

本章自我檢測 課程題庫 ch8 · 6 題

這些題目取自課程題庫,只給對錯與說明,不計分。答錯就回到對應章節重讀。

CARDS · 關鍵詞彙卡

關鍵詞彙卡:點卡片翻面 課程題庫 · 30 張

詞彙卡取自本章講義與 ISLP 第 8 章,正面是中文術語(附英文原名)。 先看正面、心裡默想定義,再翻面對答案;洗牌後再過一輪,直到每張都能不看答案講出來。