跳轉到

模型評估與 Cross-Validation 實務

評估做錯,選出的模型就錯——即使訓練做得再好也白費。本頁整理三件事:該用哪個指標衡量該用哪種 CV 策略,以及超參數選擇和模型性能估計為什麼要分開做


先想清楚:你在量什麼

好的評估流程從「定義成功」開始,而不是直接查指標公式。三個問題:

  1. 任務類型:分類?回歸?排序?
  2. 誤差的非對稱性:False Negative 比 False Positive 更貴嗎?(醫療診斷 vs 垃圾郵件)
  3. 資料特性:類別不平衡?樣本有群組結構?時序?

分類指標速查

TP = 預測為正、實際為正
FP = 預測為正、實際為負(誤報)
FN = 預測為負、實際為正(漏報)
TN = 預測為負、實際為負
指標 公式 用途 陷阱
Accuracy (TP+TN)/(全部) 類別平衡時直觀 不平衡資料會誤導:99% 負例全猜負也有 99%
Precision TP/(TP+FP) 誤報代價高(垃圾郵件誤殺重要信) 配合 Recall 看,單看沒意義
Recall TP/(TP+FN) 漏報代價高(腫瘤漏診) 全猜正可得 Recall=1,但 Precision=0
F1 2·P·R/(P+R) P 和 R 並重時的調和平均 仍假設 P/R 等重;不等重用 Fβ
AUC-ROC ROC 曲線下面積 對閾值不敏感,評整體排序能力 嚴重不平衡時會偏樂觀
PR-AUC Precision-Recall 曲線下面積 不平衡資料的首選 比 AUC-ROC 更反映少數類表現

不平衡資料的評估建議

類別比例 > 1:10 時: - 不用 Accuracy,改用 PR-AUCF1 - Confusion matrix 必看,別只看彙總指標 - class_weight='balanced' 是調模型,不是調評估

from sklearn.metrics import classification_report, roc_auc_score, average_precision_score

print(classification_report(y_test, y_pred))
print("AUC-ROC:", roc_auc_score(y_test, y_prob))
print("PR-AUC:", average_precision_score(y_test, y_prob))  # 不平衡時更重要

回歸指標速查

指標 特性 適用情境
MAE 對離群值穩健,單位同 y 離群值常見時、需要可解釋誤差
MSE / RMSE 懲罰大誤差,RMSE 單位同 y 大誤差比小誤差嚴重時
MAPE 百分比誤差,可跨不同量級比較 y 值都 > 0;y 值近 0 時會爆
解釋變異的比例,1 最好,可為負 快速理解模型 vs 均值基線的差距
Median AE 比 MAE 更抗離群 離群值極端時

⚠️ 回歸模型如果目標做了 log 轉換,記得把預測值反轉回原始尺度再算 MAE/RMSE——否則指標是 log 尺度的,和業務定義的「誤差」不一致。

import numpy as np
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

# 如果 y 做過 log1p 轉換
y_pred_original = np.expm1(y_pred_log)
y_test_original = np.expm1(y_test_log)

mae  = mean_absolute_error(y_test_original, y_pred_original)
rmse = np.sqrt(mean_squared_error(y_test_original, y_pred_original))
r2   = r2_score(y_test_original, y_pred_original)

Cross-Validation 策略一覽

策略 何時用 注意事項
KFold 回歸、類別平衡分類 shuffle=True, random_state= 確保可重現
StratifiedKFold 分類(預設首選) 確保每折類別比例和全集一致
GroupKFold 同一個 entity 的多筆資料(同一個用戶的多次點擊) 防止 group 內資料跨折造成 leakage
TimeSeriesSplit 時序資料 只往前預測,不能讓未來資料進訓練折
LOOCV 小資料集(< 100 筆) 計算量大(N 次訓練);估計無偏但 variance 高
RepeatedStratifiedKFold 追求穩定的估計(多次重複取平均) 計算量 × repeat 倍

核心原則:測試折不能「看到」訓練資訊

最常見的 CV 錯誤是在 cross_val_score 外面先做了 StandardScaler、SMOTE 或特徵選擇——這讓 scaler 的 mean_/scale_ 來自全部資料,測試折被「看見了」。

正解:把所有前處理包進 Pipeline,讓每折各自 fit

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score

pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('clf', LogisticRegression(max_iter=1000))
])

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=cv, scoring='roc_auc')
print(f"AUC: {scores.mean():.4f} ± {scores.std():.4f}")

時序 CV:用 TimeSeriesSplit

from sklearn.model_selection import TimeSeriesSplit, cross_val_score

tscv = TimeSeriesSplit(n_splits=5)
scores = cross_val_score(pipe, X_ts, y_ts, cv=tscv, scoring='neg_mean_absolute_error')
print(f"MAE: {-scores.mean():.4f}")

⚠️ 時序資料也要注意 feature leakage:如果特徵裡有「未來才能知道的資訊」(如月底才有的月報數字用於預測月中),CV 分數會虛高,上線後必然崩。

GroupKFold:防止 entity 跨折

from sklearn.model_selection import GroupKFold

gkf = GroupKFold(n_splits=5)
# groups = 每筆資料屬於哪個 entity(如 user_id)
scores = cross_val_score(pipe, X, y, cv=gkf, groups=groups, scoring='f1')

Nested CV:超參數選擇 ≠ 模型性能估計

最常見的評估錯誤之一:用同一份 validation set 既選超參數又估模型性能——這會讓性能估計樂觀偏誤(你在測試集上「挑」了表現最好的一次)。

正確做法是 Nested CV

外層 CV(評估性能)
  └── 每折裡做內層 CV(選超參數)
from sklearn.model_selection import GridSearchCV, cross_val_score, KFold
from sklearn.ensemble import RandomForestClassifier

inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=1)
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

param_grid = {'clf__n_estimators': [100, 300], 'clf__max_depth': [None, 5, 10]}

pipe = Pipeline([('clf', RandomForestClassifier(random_state=0))])
grid_search = GridSearchCV(pipe, param_grid, cv=inner_cv, scoring='roc_auc')

# 外層 CV 估計泛化性能(每折都跑一次完整的內層超參數搜索)
nested_scores = cross_val_score(grid_search, X, y, cv=outer_cv, scoring='roc_auc')
print(f"Nested CV AUC: {nested_scores.mean():.4f} ± {nested_scores.std():.4f}")

Nested CV 計算量是 outer_splits × inner_splits × 超參數組數,資料大時很貴。折中做法:用訓練集的一個固定 holdout 做內層調參,外層仍做 CV 估性能。不如 nested CV 嚴謹,但工程上常用。


選最終模型並估真實性能

標準三切法:

from sklearn.model_selection import train_test_split

X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.15, stratify=y, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.18, stratify=y_temp, random_state=42)
# 結果約 70 / 15 / 15

# 用 train+val 做 CV 調參 → 用 test 最後報告性能
# test 只碰一次。碰了就不能再調。

⚠️ Test set 只能碰一次。看了 test 分數再調超參數,test 就污染了,不再是真實估計。如果測試結果不如預期,要回頭改訓練/驗證策略,而不是反覆試到 test 分數變好看。


常見陷阱整理

陷阱 症狀 解法
CV 前全量 fit scaler 線下 CV 分數虛高,上線後表現差 用 Pipeline 包前處理
時序不用 TimeSeriesSplit 訓練時「看到未來」,分數虛高 TimeSeriesSplit 或手動按時間切
用 accuracy 評不平衡資料 全猜多數類也有高分 改用 PR-AUC 或 F1
超參數選擇和性能估計用同一份 val 估計樂觀偏誤 Nested CV 或嚴格三切
Group leakage 同一 entity 的樣本跨了 train/test GroupKFold
多次比較膨脹 試了 50 組超參數選最好的,分數虛報 Nested CV;記錄所有試驗結果而非只報最好

延伸閱讀(本站)

來源