① 照節次讀:每節先讀說明,動手玩互動元件——先預測結果,再按按鈕驗證。 ② 對照講義:每個 §徽章都標了 ISLP 節號與講義頁碼,細節與完整推導請回講義與課本。 ③ 每節做 quiz:答錯就回到該節重讀,不要往下跳;錯的選項也寫了「錯在哪」。 ④ 最後翻關鍵詞彙卡自測術語,並用 REF 總覽當速查表。標「ESL 進階」的節是課堂沒細講的延伸,第一輪可略過。
前面幾章的模型都在配一個式子:線性迴歸配一條線、樣條配一條彎的曲線、 邏輯斯迴歸配一個機率。這一章換一個完全不同的想法——不配式子,直接把特徵空間切成方塊, 每個方塊裡的所有點都給同一個預測值。
ISLP §8.1 的例子是棒球員薪水(Hitters):用 Years(打了幾年)
和 Hits(去年幾支安打)預測 log 薪水。整棵樹只有兩刀,就把球員切成三塊:
三塊的預測值就是各塊訓練資料的平均:$e^{5.107}$、$e^{5.999}$、$e^{6.740}$ 千美元,也就是約 16.5 萬、40.3 萬、84.5 萬。整個模型可以用一句話講完: 資淺的便宜;資深的看安打數。這種「一句話講得完」的能力,是樹最大的賣點。
Years < 4.5)的節點。樹的整體形狀就是一個階梯函數:
$$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 變成二元的 HighSales 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 練迴歸樹與集成。注意 ShelveLoc、Urban、US 是類別變數——理論上樹不必編碼,但 scikit-learn 的實作不支援,所以 lab 還是做了 one-hot。
Ch08-baggboost-lab-zh.ipynb · 儲存格 12、13
一棵迴歸樹對落在同一個葉節點裡的兩個觀測值,會給出什麼預測?
方塊要怎麼切?理想上我們想找到讓下面這個 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\}$。
玩過上面的元件之後有三件事應該很明顯:
這一格畫出的樹,ISLP 書上的解讀是:lstat(低社經地位人口比例)愈低、房價愈高;而對於 lstat > 14.4、crim > 5.8、rm < 6.8 的郊區小房子,樹預測房價中位數 12,042 美元。max_depth=3 是手動限制——下一節改成用 CV 學出來。
Ch08-baggboost-lab-zh.ipynb · 儲存格 50、52、54
找第一刀時,演算法實際上比較了多少種可能?(p 個變數、n 筆資料)
上一節的過程如果不管它,樹會一直長到每個葉子只剩幾個點。訓練誤差很漂亮, 測試誤差很難看——典型的過度配適。
直覺的解法是「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$ 愈大,多留一個葉子愈貴,樹就愈小。
scikit-learn 的
cost_complexity_pruning_path() 回傳的就是這條序列的斷點。
剩下的問題是 α 該取多少——這是第 5 章的老題目:用交叉驗證選。 ISLP 演算法 8.1 把整套流程寫成四步:
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
np.float64(0.685)
被選中的樹有 30 個葉子(儲存格 43),在測試集上的正確率 0.72(儲存格 45),比未剪枝的 0.735 還略差。lab 的原話是:「交叉驗證在這裡對我們的幫助不大」。這很正常也很重要——CV 是無偏的選擇工具,但它自己有變異,換個種子結果就會動。剪枝不是保證變好的魔法。
來源:Ch08-baggboost-lab-zh.ipynb · 儲存格 37、39
為什麼不直接「RSS 下降量小於門檻就停止分裂」,而要先長大再剪?
分類樹跟迴歸樹幾乎一樣:一樣遞迴二元分裂、一樣剪枝。只有兩件事要換。
第一,預測值換成多數類別。落在某個葉子的觀測, 預測為該葉子內訓練資料最常出現的那一類。 但別忘了同時看類別比例——「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 時值很小。 兩者數值上非常接近,實務上選哪個幾乎沒差。
下面這個例子把問題講得最清楚。父節點有 800 筆、兩類各 400 筆。兩種切法:
| 左邊(Yes / No) | 右邊(Yes / No) | 加權錯誤率 | 加權 Gini | 加權交叉熵 | |
|---|---|---|---|---|---|
| 切法 A | 300 / 100 | 100 / 300 | 0.250 | 0.375 | 0.562 |
| 切法 B | 200 / 400 | 200 / 0 | 0.250 | 0.333 | 0.477 |
兩種切法都是 200 筆被分錯, 錯誤率完全一樣,所以用錯誤率當準則的話,這兩刀「一樣好」。 但切法 B 生出了一個完全純的葉子(200 筆全是 Yes), Gini 與交叉熵都認得出這件事比較有價值。交叉熵用自然對數。
ISLP 圖 8.6 的 Heart 例子有同一個現象的實例:
未剪枝樹右下角那一刀 RestECG < 1,兩邊的預測都是 Yes——
錯誤率完全沒降,可是右邊那 9 筆全是 Yes、左邊只有 7/11,
純度差很多。這一刀之所以被切,就是因為 Gini 與交叉熵看得到差別。
scikit-learn 的
DecisionTreeClassifier(criterion=...) 只管分裂準則;
剪枝用的是 ccp_alpha,而 GridSearchCV(scoring='accuracy')
那一步就等於「用錯誤率挑 α」。lab 正是這樣寫的。
|--- 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
因為錯誤率對純度的變化不敏感,常常給出「下降量 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)。兩者的角色不同,不必統一。
兩類問題中,某節點有 50 筆 Yes、50 筆 No。它的 Gini 指數是多少?
兩個模型形式擺在一起:
$$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$ 的那一塊特別高」這種方塊狀結構, 線性模型除非你手動把交互作用項乘出來,否則永遠抓不到。
ISLP §8.1.4 把樹的優缺點列成清單,值得整段記下來:
| 內容 | |
|---|---|
| ✔ 好解釋 | 比線性迴歸還好解釋。可以畫出來給非專業的人看懂 |
| ✔ 像人在想事情 | 「先看這個、再看那個」的層層判斷,貼近人的決策過程 |
| ✔ 類別變數免編碼 | 一刀就是「把某些水準分到左邊」,不需要虛擬變數 |
| ✔ 尺度無關 | 只看大小順序,不必標準化,也不怕離群的 x |
| ✘ 準確率不夠好 | 單一棵樹通常打不過本書其他方法 |
| ✘ 非常不穩定 | 資料動一點點,整棵樹的結構可能完全改變 |
最後那一項是後半章的引擎。「不穩定」用統計的話講就是變異很大, 而降變異最古典的手段就是平均——這正好是下三節的主題。
真實的決策邊界是一條斜線 $X_1 + X_2 = 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% 的正確率。這個數字大得不像真的——下面自己算一次。
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
1000 個分類器各自的正確率都是 0.48,且彼此獨立。多數投票的正確率大約是多少?
回到樹。單一棵樹的問題是變異太大。而降變異最古典的手段, 第 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 棵樹各投一票,票多的那一類就是答案。
還有一個副產品,好用到有點不像話。有放回抽 $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 那一節推導過,回去對一下。)
還有一件實務上很重要的事:B 不是需要調的參數。 ISLP 圖 8.8 顯示誤差隨 B 上升而下降、然後平掉就不動了—— B 太大不會過度配適(只是浪費算力),B 太小才會欠配適。 所以做法是「挑一個大到誤差已經平掉的 B」,通常 100 到 500 就夠。
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
從公式看最清楚。設每棵樹的預測 $\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 去平均一堆線性迴歸幾乎沒有用:線性迴歸本來變異就小,沒什麼可壓的。
估 bagging/random forest 本身的測試誤差時,OOB 就夠了,而且便宜太多:配一次模型就順手拿到,不必像 k-fold 那樣配 k 次。ISLP 說 B 夠大時 OOB 誤差幾乎等於 LOOCV 誤差。
要小心的是兩件事。第一,如果你用 OOB 誤差去挑超參數(例如挑 m、挑樹的深度),那被挑中的那組的 OOB 誤差就跟第 5 章講的一樣會偏低,不能再當誠實的測試誤差報出來。第二,OOB 只有在有 bootstrap 抽樣時才存在——boosting 沒有 bootstrap,所以沒有 OOB,只能用驗證集或 CV。
做 bagging 時,每一棵樹該長多深?
上一節的公式留了一個尾巴:
$$\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 的一個特例。
lstat 與 rm 真的最有用,硬是不給樹看它們只是自找麻煩——所以 m = p 最好。lab 的原話就是「隨機森林比 bagging 表現稍差」。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
降低的是變異數公式裡那個不隨 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}$ 只是好用的預設值,該調就調。
Random forest 每次分裂只從 m 個隨機挑出的變數裡選。這個 m 是怎麼抽的?
Bagging 與 random forest 是並行的:B 棵樹互不相干, 誰先誰後無所謂,可以開 B 個執行緒一起長。Boosting 完全相反——樹是序列長出來的, 每一棵都靠前面那些樹的資訊決定自己要做什麼。而且沒有 bootstrap, 每棵樹都看全部資料,只是看的目標被改過。
ISLP 演算法 8.2 是整章最值得背下來的十行:
注意 (a):配的目標是殘差 $r$,不是 $y$。這就是整個把戲。 第一棵樹抓走一部分結構,剩下沒解釋掉的變成新的目標,第二棵樹去抓它,以此類推。 ISLP 的說法是 boosting 學得很慢(learns slowly)—— 而在統計學習裡,學得慢的方法往往表現得好。
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)——
在這份資料上兩組設定都落在「已經收斂」的區域裡。
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)重新加權更兇,在小樣本上容易讓權重
幾輪就集中到少數幾點上。
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
三句話:並行 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。
梯度提升的第 b 棵樹,配的目標(response)是什麼?
這一節是課堂沒細講的延伸(講義 p.52–61):三個現代 GBDT 套件與該調的超參數。 第一輪讀可以整節略過,回頭要用套件時再看。
上一節的梯度提升在概念上已經完整了,剩下的全是工程與正則化。 三個套件把這件事推到了工業級:
| 全名/來源 | 核心賣點 | 在 lab 的實測 | |
|---|---|---|---|
| XGBoost | Extreme Gradient Boosting | 目標函數內建正則化(葉子數與葉值的 L1/L2 罰項);用類似牛頓法的二階近似而非單純梯度;分位數草圖做近似分裂搜尋、稀疏感知、快取友善 | 24.3 秒(儲存格 114) |
| LightGBM | Microsoft | 直方圖分箱(預設 255 箱)把排序的 $O(n \log n)$ 降成 $O(n)$;GOSS 只對小梯度的樣本抽樣;leaf-wise 而非 level-wise 長樹;互斥特徵綁定(EFB) | 13.1 秒(儲存格 185) |
| CatBoost | Yandex | 對稱樹(同一層用同一個分裂條件,本身就是正則化,預測極快);ordered boosting 用另一份子集算殘差以防過度配適;類別變數原生支援 | 15.1 秒(儲存格 210) |
| 對照組 | sklearn 的 GradientBoostingClassifier | 純 Python 迴圈的參考實作 | 624.8 秒(儲存格 113) |
同一份系外行星資料(3197 個特徵)、同樣 100 棵樹、
max_depth=2。625 秒 vs 13 秒,差了快 50 倍,而正確率還略微更好
(0.9874 → 0.9914)。資料一大,這種差距就是「跑得完」與「跑不完」的差別。
該調哪些超參數?講義第 60–61 頁把三個套件的參數名對照起來, 分成「求快」「求準」「防過度配適」三組:
| 目的 | XGBoost | LightGBM | CatBoost |
|---|---|---|---|
| 求快(抽樣列/欄、少幾棵) | subsample · colsample_bytree · n_estimators | bagging_fraction · feature_fraction · num_iterations | subsample · rsm · iterations |
| 控制過度配適/求準 | learning_rate(0.01–0.2)· max_depth · min_child_weight · gamma | learning_rate · max_depth · num_leaves · min_data_in_leaf | learning_rate · depth · l2-leaf-reg |
| 類別變數 | 實驗性支援(建議自己先編碼) | categorical_feature | cat_features · one_hot_max_size |
lab 在 heart_disease.csv(303 筆、13 個特徵,用 !wget 抓 Packt 的那份)
上一個一個網格搜過去,基準是 0.79:
| 調的參數 | 搜尋範圍 | 最佳值 | 最佳 CV 正確率 | lab 儲存格 |
|---|---|---|---|---|
| (基準) | 全部預設 | — | 0.79 | 124 |
learning_rate | 0.01 – 0.5 | 0.5 | 0.79557 | 130 |
max_depth | 2, 3, 5, 6, 8 | 3 | 0.81197 | 134 |
gamma | 0 – 2 | 0.1 | 0.81197 | 138 |
min_child_weight | 1 – 5 | 4 | 0.81197 | 142 |
subsample | 0.5 – 1 | 0.5 | 0.81536 | 145 |
colsample_bytree | 0.5 – 1 | 0.7 | 0.80552 | 149 |
n_estimators | 100 – 800 | 400 | 0.79541 | 153 |
最大的一筆進步來自把 max_depth
從預設的 6 降到 3(0.79 → 0.812)——「把模型調簡單一點」。
subsample 降到 0.5 又多賺一點,同樣是在減少變異。
而把樹加到 800 棵反而變差:這份資料只有 303 筆,B 大不會有幫助。
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
[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
XGBoost 相對於課本演算法 8.2 的梯度提升,最主要的統計差別是什麼?(不算工程加速)
這一節是課堂沒細講的延伸(講義 p.66–79、ISLP §8.2.4):變數重要度的讀法、 stacking,以及貝氏版的加法樹 BART。第一輪讀可以整節略過。
單一棵樹最大的優點是可以畫出來給人看。集成之後這個優點就沒了—— 你不可能把 500 棵樹貼在牆上。變數重要度(variable importance) 是把可解釋性買回來一點點的標準做法:
ISLP 圖 8.9 就是 Heart 資料上的這張圖(相對於最大值標準化),
最重要的三個是 Thal、Ca、ChestPain。
下面用 lab 的 Boston 例子重畫(Heart 不在 ISLP 0.4.0 裡):
PART 06 的 voting 是用一個固定的規則(多數票、平均)去合併。 Stacking 問了一個很自然的問題:為什麼不訓練一個模型來做這件合併?
做法的關鍵在怎麼造合併器的訓練資料——這裡有一個必須避開的洩漏:
第 1 步為什麼一定要用 CV?因為如果拿成員模型在訓練資料上的預測去餵合併器, 那些預測好得不真實(成員在訓練資料上本來就準),合併器會學到「完全相信最會過度配適的那個成員」。 這就是第 5 章那個「所有用到 y 的步驟都要關在折裡面」的老規矩。
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(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)。
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
訓練 stacking 的合併器時,成員模型的預測值一定要用交叉驗證產生。為什麼?
下面幾題取自 ISLP §8.4 的課後習題,題號都對得回課本。先自己想過再點選項; 每個選項——包含錯的——都寫了為什麼。想看完整解答再對照下面3個站。
課本第 3 題要你把 Gini 指數、錯誤率、交叉熵都畫成 $\hat p_{{m1}}$ 的函數(兩類)。畫出來之後,哪一句話最能說明「為什麼分裂準則不用錯誤率」?
十個 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。用多數投票與用平均機率,分別會判成哪一類?
課本第 2 題要你說明:用深度 1 的樹(stump)做 boosting,為什麼結果是一個加法模型 $f(X) = \sum_{{j=1}}^{{p}} f_j(X_j)$?
課本第 7 題要你在 Boston 上掃過一整片 max_features(m)與 n_estimators(B)的組合,畫成圖 8.10 那樣。預期會看到什麼?
考前把這一頁掃過去就好。
| 方法 | 樹怎麼來 | 樹的深度 | 主要降的是 | B 會過度配適嗎 | 免費驗證集 |
|---|---|---|---|---|---|
| 單一棵樹 | 一棵,貪婪長 + 剪枝 | 由 CV 選 α | —(偏差與變異都要自己顧) | — | 沒有(要 CV) |
| Bagging | B 個 bootstrap,並行 | 很深、不剪枝 | 變異 | 不會(B 大只是浪費算力) | 有(OOB) |
| Random Forest | 同上 + 每刀只看 m 個變數 | 很深、不剪枝 | 變異(多壓了 ρ) | 不會 | 有(OOB) |
| Boosting | 配殘差/調權重,序列 | 很淺(d 常常 = 1) | 偏差 | 會(要用 CV 或 early stopping) | 沒有(沒 bootstrap) |
| BART | 微調上一輪的樹,MCMC | 小樹 | 兩者(貝氏平均) | 不會(B 是 MCMC 迭代數) | 沒有 |
| 模型 | 設定 | 測試 MSE | lab 儲存格 |
|---|---|---|---|
| 剪枝後的單一棵樹 | ccp_alpha 由五折 CV 選 | 28.07 | 59 |
| Bagging | max_features=12, B = 100 | 14.63 | 66 |
| Bagging | max_features=12, B = 500 | 14.61 | 68 |
| Random Forest | max_features=6, B = 100 | 20.04 | 70 |
| Gradient Boosting | B = 5000, λ = 0.001, d = 3 | 14.48 | 80 |
| Gradient Boosting | B = 5000, λ = 0.2, d = 3 | 14.50 | 82 |
| BART | burnin=5, ndraw=15(刻意調小) | 22.15 | 88 |
兩個結論:①集成把單一棵樹的誤差砍了一半 (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$ 是加權錯誤率 |
| 套件 | 什麼時候選它 |
|---|---|
| XGBoost | 社群最大、生產環境的支援最完整。不確定就先用它 |
| LightGBM | 資料很大、同時要速度與準確率時的較好選擇 |
| CatBoost | 資料量小,或類別變數很重要的時候 |
本頁「預期輸出」逐字取自課程 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 的儲存格編號,可以直接回去對。
這些題目取自課程題庫,只給對錯與說明,不計分。答錯就回到對應章節重讀。
詞彙卡取自本章講義與 ISLP 第 8 章,正面是中文術語(附英文原名)。 先看正面、心裡默想定義,再翻面對答案;洗牌後再過一輪,直到每張都能不看答案講出來。