Learning Objectives
- 說明 bootstrap 抽樣與投票,以及為什麼模型犯的錯要不一樣,投票才有用
- 說明隨機森林和單純 bagging 的差別,並用 RandomForestClassifier 訓練
- 用 numpy 寫出一輪一輪修正殘差的提升法,說明 learning_rate 的作用
- 分辨不純度重要性與置換重要性,並說明兩者的限制
一棵樹不穩定,那就用很多棵
上一課看到,決策樹長深了容易過擬合,而且訓練資料稍微變動,整棵樹就可能不同。把很多個模型組合起來,叫做集成(ensemble)。主要有兩種想法:
| bagging(隨機森林) | boosting(梯度提升) | |
|---|---|---|
| 模型之間的關係 | 各自獨立訓練,可以同時進行 | 一個接一個,後面的修正前面的錯 |
| 每個模型 | 通常是很深的樹 | 通常是很淺的樹 |
| 主要解決 | 模型太不穩定(過擬合) | 模型太簡單(欠擬合) |
| 怎麼合起來 | 投票或平均 | 加起來 |
bagging 這個名稱來自 bootstrap aggregating,由 Leo Breiman 在 1996 年提出;隨機森林也是他在 2001 年的論文提出的。
要訓練很多棵不一樣的樹,第一個方法是給每棵樹看不一樣的資料:bootstrap 抽樣,從 n 筆資料裡有放回地抽 n 筆,有的資料會被抽到好幾次,有的一次也沒抽到。
逐步看:有放回抽樣與投票
前半段看抽樣的結果,後半段看三棵樹投票。這裡的三棵樹的預測是直接寫好的,讓你看清楚「錯的地方不一樣」時會發生什麼事。
隨機森林:讓每棵樹更不一樣
投票要有用,模型的錯就不能都一樣。只靠 bootstrap,如果資料裡有一個特別強的特徵,每棵樹的第一刀都會切它,樹和樹還是很像。
隨機森林(random forest)多加一道隨機:每一次分割時,只從隨機挑出的幾個特徵裡找最佳分割。
RandomForestClassifier(
n_estimators=100, # 樹的數量(預設 100)
max_features="sqrt", # 每次分割看幾個特徵(分類的預設是特徵數的平方根)
bootstrap=True, # 每棵樹用 bootstrap 抽樣(預設)
max_depth=None, # 每棵樹不限深度(預設)
random_state=0,
)
樹的數量增加,結果會慢慢穩定下來,代價是訓練與預測變慢;每棵樹各自獨立,可以用 n_jobs 同時訓練。
提升法:一輪一輪修正錯誤
提升法(boosting)走另一條路。以迴歸為例:
- 先給一個很粗的預測,例如全部預測平均;
- 算殘差(真實值 − 目前的預測),就是還差多少;
- 訓練一棵很淺的樹去預測這個殘差;
- 把這棵樹的預測乘上一個小比例(
learning_rate)加進目前的預測; - 回到第 2 步。
每一輪都往「損失變小」的方向走一點,和第 4 課的梯度下降是同一個想法,所以叫梯度提升(gradient boosting),Jerome Friedman 在 2001 年的論文系統性地提出這個架構。平方誤差時,要修正的方向剛好就是殘差;分類時用的是對數損失的梯度,意思相同。
下面用 numpy 寫一次,每一輪只用一個門檻(只分一次的樹,叫 stump)。
逐步看:用 numpy 寫提升法
八筆資料:讀書時數 x 與分數 y。注意每一輪的 residual 怎麼變小,pred 怎麼一步一步靠近 y。
逐步看:一棵樹、森林與提升法比一比
用 students.csv(程式產生的模擬資料,不是任何學校的真實資料)預測是否及格。每一行印出訓練準確率與測試準確率;基準是全部猜及格。GradientBoostingClassifier 的預設是 n_estimators=100、learning_rate=0.1、max_depth=3。
怎麼解讀這些數字
- 森林穩定:樹變多以後測試準確率維持在 0.862,不太需要調參數就有不錯的結果,很適合當第一個嘗試的模型。
- 提升法要調:輪數、
learning_rate、樹的深度互相影響。learning_rate越小,通常需要越多輪。輪數太多會過擬合,所以要用驗證資料決定何時停止(GradientBoostingClassifier有n_iter_no_change參數可以提早停止)。 - 不要過度解讀這次的高低:提升法 10 輪的 0.894 比森林高,但測試資料只有 94 筆,差一筆就差 1%,而且這是我們看過測試結果才挑出來的輪數,會偏樂觀。正式比較要用交叉驗證(第 14 課)。
在表格資料上,這兩種樹的集成常是表現很好的選擇。實務上常見的 XGBoost、LightGBM 也是梯度提升的實作,觀念相同。
特徵重要性
集成模型沒辦法像單棵樹那樣印出來讀,但可以問:哪些特徵比較有用? 常用兩種方法:
- 不純度重要性(
feature_importances_):統計每個特徵在所有樹裡總共讓 Gini 減少多少。計算快,但是在訓練資料上算的,scikit-learn 文件也提醒,它對數值種類很多的特徵可能會有誤導。 - 置換重要性(
permutation_importance):把某一欄的值打亂,看測試資料上的表現掉多少。掉越多表示模型越依賴這個特徵。
下面的範例故意加入一欄純粹的亂數 random_noise,它不可能真的有用,正好用來檢驗這兩種方法。
逐步看:加一欄亂數,檢驗特徵重要性
看 random_noise 在兩個排名裡的位置。
解讀特徵重要性要小心
從上面的結果可以看到幾件事:
random_noise是一欄連續的亂數,每一筆的值幾乎都不同,樹可以切的門檻很多,所以在訓練資料上「看起來有用」。這就是不純度重要性的偏差。quiz_avg的不純度重要性是 0.225,置換重要性卻只有 0.002。可能的原因是它和midterm帶著相近的資訊(這份資料裡兩者的相關係數是 0.62):打亂其中一欄,模型還可以靠另一欄。特徵之間相關時,置換重要性會低估它們。- 測試資料只有 94 筆,0.017 這種大小的數字很可能只是雜訊。
而且重要性說的是「這個模型依賴哪個特徵」,不是「這個特徵造成及格」。「期中考分數重要」不代表提高分數的方法就是這個特徵,況且這是模擬資料,不能推論真實世界。
實作:寫出提升法的主迴圈
fit_stump 已經寫好:給它 x 與殘差 r,它回傳最好的門檻與左右兩邊的預測值。請完成 boost(x, y, rounds, lr):從平均開始,每一輪算殘差、呼叫 fit_stump、把乘上 lr 的預測加進去,最後回傳預測。
小測驗:集成
資料很多時,對一份 n 筆的資料做一次 bootstrap 抽樣(抽 n 筆),大約多少比例的資料至少被抽到一次?
三棵樹投票(每一列是一棵樹),下面程式會印出什麼?
import numpy as np
preds = np.array([[1, 0, 1], [0, 0, 1], [1, 1, 1]])
print((preds.sum(axis=0) >= 2).astype(int).tolist())
隨機森林和「用 bootstrap 訓練很多棵決策樹再投票」最主要的差別是?
目前預測與真實值如下,這一輪訓練出的樹剛好完全預測出殘差。下面程式會印出什麼?
import numpy as np
y = np.array([48.0, 50.0, 55.0])
pred = np.array([50.0, 50.0, 50.0])
residual = y - pred
print((pred + 0.5 * residual).tolist())
提升法 500 輪的訓練準確率是 1.0,測試準確率比 10 輪時低。應該怎麼做?
一欄純亂數的 feature_importances_ 排在好幾個真實特徵之前。最恰當的解讀是?
重點整理
- 集成把很多模型組合起來;模型犯的錯要不一樣,組合才有用。
- bagging:每個模型用 bootstrap 抽樣各自訓練,再投票。隨機森林再加上「每次分割只看隨機幾個特徵」,預設 100 棵、調參數少。
- 梯度提升:淺樹一棵接一棵修正殘差,每次只修正
learning_rate的比例;輪數太多會過擬合,要用驗證資料決定。 - 特徵重要性:不純度重要性會高估值很多種的特徵,置換重要性在特徵相關時會低估;都只表示模型依賴什麼,不是因果。
- 下一課的支援向量機是另一種想法:找一條離兩邊都最遠的邊界。