Learning Objectives
- 說明為什麼不能用測試資料挑超參數,並分清訓練、驗證、測試資料的用途
- 寫出 k 折切分,用 cross_val_score 得到平均與標準差,並用它比較超參數
- 用 GridSearchCV 搜尋超參數,最後才用測試資料評估一次
- 認出資料洩漏:前處理或選特徵用到驗證資料時,分數會虛高
第 8 課留下的問題
第 8 課試了幾個 k,看到 k=5 的測試準確率最高(0.809),也留下一個問題:可以用測試資料挑 k 嗎?
不可以。測試資料的用途是估計「模型遇到沒看過的資料時表現如何」。如果你看著測試分數挑 k,就等於讓測試資料參與了決定,這個分數會偏樂觀,也不再有資格回答原本的問題。
所以資料要分三種用途:
| 資料 | 用途 | 使用次數 |
|---|---|---|
| 訓練資料 | 學模型的參數(權重、樹的分割) | 很多次 |
| 驗證資料 | 比較超參數(k、C、樹的深度) | 很多次 |
| 測試資料 | 全部決定之後,估計最終表現 | 一次 |
但只切一次驗證資料也有問題:切法不同,分數就不同,資料少時差得更多。k 折交叉驗證(k-fold cross-validation)的做法是:把訓練資料分成 k 份,每一份輪流當驗證資料,其他份拿來訓練,得到 k 個分數。這樣每一筆都當過一次驗證資料,也能從 k 個分數看出浮動有多大。
逐步看:k 折怎麼切
先只看編號,不訓練模型。10 筆資料切成 5 折,看每一輪的驗證資料是哪幾筆。
逐步看:親手跑一次交叉驗證
用 students.csv(程式產生的模擬資料,不是任何學校的真實資料)預測是否及格,模型是 k=5 的 k 近鄰。注意 scaler 寫在迴圈裡面:每一折只用該折的訓練資料算平均與標準差。
用 cross_val_score 省下迴圈
上面的迴圈 scikit-learn 已經包好了:
model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
scores = cross_val_score(model, X, y, cv=5)
make_pipeline把 scaler 和模型接成一個物件(第 16 課細談)。交叉驗證的每一折都會重新 fit 整個流程,所以 scaler 不會看到驗證資料。cv=5用在分類器時,預設是StratifiedKFold:每一折的類別比例和全體相近,而且不洗牌;用在迴歸模型時是一般的KFold。- 分數預設是模型的
score:分類是準確率,迴歸是 R²。要換指標用scoring,例如scoring="recall"。
怎麼讀結果:平均代表表現,標準差代表浮動。兩個超參數的平均差距如果比標準差還小,就不要說其中一個比較好;這時可以選比較簡單的那一個。折數常用 5 或 10:折數越多,每次訓練用的資料越多,但也要跑越久。
逐步看:用交叉驗證重新挑 k
這和第 8 課是同一份資料、同一個切分,但這次完全不碰測試資料,只在訓練資料上做 5 折交叉驗證。比較第 8 課的結論:k=5 真的比較好嗎?
超參數一多,用 GridSearchCV
參數是模型從資料學來的(權重、分割點);超參數是訓練之前由你決定的(k、C、樹的深度)。超參數不只一個時,手寫迴圈會很長。GridSearchCV 把你列出的組合全部試過,每一種都做交叉驗證:
search = GridSearchCV(pipe, grid, cv=5)
search.fit(X_train, y_train)
- 參數名稱用「步驟名稱+兩個底線+參數名稱」指定,例如
knn__n_neighbors。 - 組合數會相乘:4 個 k、2 種權重、5 折就要訓練 40 次;組合太多時可以改用
RandomizedSearchCV,只隨機試固定次數。 - 預設
refit=True:找到最好的組合後,用全部訓練資料再訓練一次,search.predict、search.score用的就是這個模型。
best_score_ 是挑出來的那個組合的分數,因為是「試很多次取最高」,會略為樂觀,所以最後要用測試資料評估一次,而且只用這一次。
逐步看:GridSearchCV
weights="distance" 表示距離越近的鄰居票越重,預設 "uniform" 是每票一樣重。
資料洩漏:驗證資料偷偷參與了訓練
資料洩漏(data leakage)是指驗證或測試資料的資訊,在評估之前就進入了模型。結果是分數看起來很好,真正用的時候卻做不到。
交叉驗證本身擋不住洩漏,要看你在哪裡做前處理:
| 寫法 | 問題 |
|---|---|
| 先用全部資料 fit scaler,再做交叉驗證 | 驗證資料參與了平均與標準差的計算 |
| 先用全部資料選特徵,再做交叉驗證 | 驗證資料參與了「哪些特徵有用」的決定 |
| 同一個人的多筆紀錄同時出現在訓練與驗證 | 模型認得這個人,不是學到規律 |
| 預測未來時,用未來的資料訓練 | 模型看到了當時還不存在的資訊 |
洩漏的嚴重程度差很多。標準化的洩漏通常很小(只偷看了平均與標準差);選特徵的洩漏可以大到把純雜訊變成高分,下面就來試。後兩種要改切分方式:同一個人或同一組的資料用 GroupKFold,時間序列用 TimeSeriesSplit。
逐步看:雜訊也能拿高分
60 筆資料、1000 個特徵,全部是亂數,標籤也是亂數。任何模型都不可能真的預測,正確的估計應該接近 0.5。SelectKBest 依每個特徵和標籤的關聯(預設 F 值)挑出最高的 k 個。
實作:寫一個 kfold_indices 函數
完成 kfold_indices(n, k):不洗牌,把 0 到 n-1 分成 k 折,回傳串列,每個元素是 (訓練編號串列, 驗證編號串列)。
切不完時,前面幾折多分一筆(和 scikit-learn 的 KFold 相同):7 筆切 3 折,每折的大小是 3、2、2。
小測驗:交叉驗證與資料洩漏
下面程式會印出什麼?
from sklearn.model_selection import KFold
import numpy as np
X = np.arange(7).reshape(-1, 1)
sizes = [len(val) for train, val in KFold(n_splits=3).split(X)]
print(sizes)
你看著測試分數試了 20 個超參數,選最高的那個回報。這個分數會怎樣?
k=5 的平均是 0.776、標準差 0.047;k=41 的平均是 0.777、標準差 0.009。最恰當的結論是?
下面哪一種寫法沒有資料洩漏?
用 GridSearchCV 試 3 個 C 和 4 個 gamma,cv=5,預設設定下總共訓練幾次(含最後重新訓練)?
重點整理
- 訓練資料學參數、驗證資料比較超參數、測試資料最後用一次。用測試資料挑超參數,測試分數就不可信。
- k 折交叉驗證讓每一筆輪流當驗證資料;看平均,也看標準差。差距小於浮動時,不要說誰比較好。
cross_val_score跑交叉驗證,GridSearchCV搜尋超參數;分類器的cv=5預設用StratifiedKFold。- 資料洩漏:縮放、補值、選特徵都要在每一折內做,把它們放進 Pipeline 就不會出錯。選特徵的洩漏可以讓純雜訊拿到 0.867。
- 回到第 8 課的問題:用交叉驗證看,k=5 和 k=41 幾乎一樣,那次的 0.809 有一部分是運氣。
- 下一課處理兩個評估時常被忽略的問題:類別不平衡,以及整體的數字會藏住某些群組的錯誤。