回顧第 2 章,我們學過訓練誤差和測試誤差的區別:訓練誤差是模型在「見過的資料」上的表現,測試誤差才是模型在「全新資料」上的真正實力。問題是——現實世界裡我們通常沒有獨立的測試集。
想像你要參加日文檢定,但你手邊只有歷屆試題。你把所有歷屆試題都做完了才去考試,分數當然漂亮——但你根本不知道自己是不是真的學會了,還是只是背了考古題的答案。這就像只用訓練誤差來評估模型:欺騙自己。
驗證集法是最直覺的做法:把資料隨機切成兩塊——一塊當「教科書」(訓練集),一塊當「模擬考卷」(驗證集)。模型看完教科書就去考試,分數就當作是對真實實力的估計。
課本以 Auto 資料集為例,想預測汽車的 mpg(每加侖英里數)用 horsepower 的多項式:一次、二次、三次⋯⋯哪個最好?驗證集法把 392 筆資料切半,196 筆訓練、196 筆驗證,結果發現:二次多項式最好,三次以上沒有明顯改進。
# Colab / 本機通用資料讀取前置
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# 讀取 Auto 資料
auto = pd.read_csv(DATA_PATH + 'Auto.csv', na_values='?')
auto = auto.dropna()
X = auto[['horsepower']].values
y = auto['mpg'].values
# 單次驗證集法:隨機切成訓練/驗證
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.5, random_state=1)
degrees = range(1, 11)
val_mse = []
for d in degrees:
poly = PolynomialFeatures(degree=d)
X_poly_train = poly.fit_transform(X_train)
X_poly_val = poly.transform(X_val)
model = LinearRegression().fit(X_poly_train, y_train)
y_pred = model.predict(X_poly_val)
val_mse.append(mean_squared_error(y_val, y_pred))
# 找出最佳多項式次數
best_d = degrees[np.argmin(val_mse)]
print(f"最佳多項式次數: {best_d}")
print(f"最低驗證 MSE: {min(val_mse):.2f}")
# 繪圖
plt.figure(figsize=(8, 4))
plt.plot(degrees, val_mse, 'o-', color='#58a6ff', linewidth=2)
plt.axvline(best_d, color='#3fb950', linestyle='--',
label=f'最佳 d={best_d}')
plt.xlabel('多項式次數')
plt.ylabel('驗證 MSE')
plt.title('驗證集法:不同多項式次數的 MSE')
plt.legend()
plt.grid(alpha=0.3)
plt.show()
驗證集法簡單,但有兩個大問題:
LOOCV(Leave-One-Out Cross-Validation)是把驗證集法做到極致:每次只留「一筆」資料當驗證,其餘 \(n-1\) 筆當訓練。重複 \(n\) 次,每次輪流讓不同的一筆當「被留下來的那個」。最後把 \(n\) 次的誤差平均。
用日檢比喻:你手上有 100 題考古題。LOOCV 的做法是:每次只留 1 題當模擬考,用其他 99 題練習,重複 100 次(每題都輪流當一次考題)。這樣每一題都被考過一次,不會有「剛好沒考到弱點」的運氣成分。
# Colab / 本機通用資料讀取前置
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.model_selection import LeaveOneOut
from sklearn.metrics import mean_squared_error
# 讀取 Auto 資料(用前 80 筆加速示範)
auto = pd.read_csv(DATA_PATH + 'Auto.csv', na_values='?')
auto = auto.dropna().head(80)
X = auto[['horsepower']].values
y = auto['mpg'].values
# LOOCV 計算
degrees = range(1, 8)
loocv_mse = []
for d in degrees:
poly = PolynomialFeatures(degree=d)
X_poly = poly.fit_transform(X)
loo = LeaveOneOut()
errors = []
for train_idx, val_idx in loo.split(X_poly):
model = LinearRegression().fit(X_poly[train_idx], y[train_idx])
y_pred = model.predict(X_poly[val_idx])
errors.append((y[int(val_idx[0])] - y_pred[0]) ** 2)
loocv_mse.append(np.mean(errors))
best_d = degrees[np.argmin(loocv_mse)]
print(f"LOOCV 最佳多項式次數: {best_d}")
print(f"最低 LOOCV MSE: {min(loocv_mse):.2f}")
plt.figure(figsize=(8, 4))
plt.plot(degrees, loocv_mse, 's-', color='#d2991d', linewidth=2)
plt.axvline(best_d, color='#3fb950', linestyle='--',
label=f'最佳 d={best_d}')
plt.xlabel('多項式次數')
plt.ylabel('LOOCV MSE')
plt.title('LOOCV:不同多項式次數的交叉驗證誤差')
plt.legend()
plt.grid(alpha=0.3)
plt.show()
LOOCV 聽起來很完美,但有兩大痛點:
k 折交叉驗證是折衷方案:把資料隨機分成 k 等份(k 折),每次用 k−1 折訓練、1 折驗證,重複 k 次。最常用的是 k=5 或 k=10。
用日檢比喻:100 題分成 10 疊(k=10),每次留 1 疊(10 題)當模擬考,用另外 9 疊(90 題)練習,重複 10 次。比 LOOCV(每次只考 1 題)更有效率。
# Colab / 本機通用資料讀取前置
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.model_selection import LeaveOneOut, KFold, cross_val_score
from sklearn.metrics import make_scorer, mean_squared_error
# 讀取 Auto 資料(前 80 筆)
auto = pd.read_csv(DATA_PATH + 'Auto.csv', na_values='?')
auto = auto.dropna().head(80)
X = auto[['horsepower']].values
y = auto['mpg'].values
degrees = range(1, 8)
loocv_mse, cv5_mse, cv10_mse = [], [], []
mse_scorer = make_scorer(mean_squared_error, greater_is_better=False)
for d in degrees:
poly = PolynomialFeatures(degree=d)
X_poly = poly.fit_transform(X)
# LOOCV
loocv_mse.append(-cross_val_score(LinearRegression(), X_poly, y,
cv=LeaveOneOut(), scoring=mse_scorer).mean())
# 5-fold
cv5_mse.append(-cross_val_score(LinearRegression(), X_poly, y,
cv=KFold(5, shuffle=True, random_state=1),
scoring=mse_scorer).mean())
# 10-fold
cv10_mse.append(-cross_val_score(LinearRegression(), X_poly, y,
cv=KFold(10, shuffle=True, random_state=1),
scoring=mse_scorer).mean())
print(f"LOOCV 最佳 d={degrees[np.argmin(loocv_mse)]}, MSE={min(loocv_mse):.2f}")
print(f"5-fold 最佳 d={degrees[np.argmin(cv5_mse)]}, MSE={min(cv5_mse):.2f}")
print(f"10-fold 最佳 d={degrees[np.argmin(cv10_mse)]}, MSE={min(cv10_mse):.2f}")
plt.figure(figsize=(8, 4))
plt.plot(degrees, loocv_mse, 's--', color='#d2991d', label='LOOCV')
plt.plot(degrees, cv5_mse, 'o-', color='#58a6ff', label='5-fold CV')
plt.plot(degrees, cv10_mse, 'D-', color='#3fb950', label='10-fold CV')
plt.xlabel('多項式次數')
plt.ylabel('CV MSE')
plt.title('LOOCV vs 5-fold vs 10-fold 交叉驗證比較')
plt.legend()
plt.grid(alpha=0.3)
plt.show()
這是最違反直覺的一節。多數人會想:LOOCV 用 n−1 筆訓練,幾乎用了全部資料,偏差應該最小啊——為什麼 k 折(例如 5 折或 10 折)反而給出更準確的測試誤差估計?
答案是:偏差不是唯一的考量,變異數也很重要。
結論:k=5 或 k=10 實證上給出最平衡的偏差-變異權衡——偏差不會太高(用了 80-90% 的資料訓練),變異也不會太大(只訓練 5-10 個不那麼相關的模型)。
# Colab / 本機通用資料讀取前置
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.model_selection import cross_val_score, KFold, LeaveOneOut
from sklearn.metrics import make_scorer, mean_squared_error
# 讀取 Auto 資料(前 50 筆,使效果更明顯)
auto = pd.read_csv(DATA_PATH + 'Auto.csv', na_values='?')
auto = auto.dropna().head(50)
X = auto[['horsepower']].values
y = auto['mpg'].values
mse_scorer = make_scorer(mean_squared_error, greater_is_better=False)
# 固定 d=2(二次多項式),比較不同方法的穩定性
poly = PolynomialFeatures(degree=2)
X_poly = poly.fit_transform(X)
# 10-fold CV:重複 20 次不同 shuffle 觀察變異
cv10_runs = []
for seed in range(20):
score = cross_val_score(LinearRegression(), X_poly, y,
cv=KFold(10, shuffle=True, random_state=seed),
scoring=mse_scorer)
cv10_runs.append(-score.mean())
# 5-fold CV:重複 20 次
cv5_runs = []
for seed in range(20):
score = cross_val_score(LinearRegression(), X_poly, y,
cv=KFold(5, shuffle=True, random_state=seed),
scoring=mse_scorer)
cv5_runs.append(-score.mean())
# LOOCV 只有一個值(無隨機性)
loocv_score = -cross_val_score(LinearRegression(), X_poly, y,
cv=LeaveOneOut(), scoring=mse_scorer).mean()
print(f"LOOCV MSE: {loocv_score:.2f} (單一值,無隨機變異)")
print(f"5-fold CV MSE: mean={np.mean(cv5_runs):.2f}, std={np.std(cv5_runs):.2f}")
print(f"10-fold CV MSE: mean={np.mean(cv10_runs):.2f}, std={np.std(cv10_runs):.2f}")
plt.figure(figsize=(8, 4))
plt.axhline(loocv_score, color='#d2991d', linestyle='-', linewidth=2, label=f'LOOCV = {loocv_score:.2f}')
plt.plot(range(20), cv5_runs, 'o-', color='#58a6ff', alpha=0.7, label='5-fold (每次重抽)')
plt.plot(range(20), cv10_runs, 's-', color='#3fb950', alpha=0.7, label='10-fold (每次重抽)')
plt.xlabel('隨機種子 (不同 shuffle)')
plt.ylabel('CV MSE')
plt.title('不同 CV 方法的穩定性比較 (d=2)')
plt.legend()
plt.grid(alpha=0.3)
plt.show()
前面都在講迴歸(Y 是連續值),但 CV 在分類問題上一樣好用!唯一的差別是:用錯誤分類率取代 MSE。
課本用二維模擬分類資料(Figure 2.13)做示範:用多項式羅吉斯迴歸(從線性到四次),10-fold CV 的最低點在四次多項式附近,真實測試誤差的最低點在三次——CV 近似得很好!右圖用 KNN 分類器,CV 的最佳 K 也非常接近真實最佳值。
# Colab / 本機通用資料讀取前置
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.preprocessing import StandardScaler
# 用 ISLP 內建 Caravan 資料(保險購買預測)
from ISLP import load_data
Caravan = load_data('Caravan')
y = (Caravan['Purchase'] == 'Yes').astype(int).values
X = Caravan.drop('Purchase', axis=1).values
X = StandardScaler().fit_transform(X)
# 用 10-fold CV 選最佳 K
k_values = [1, 3, 5, 7, 9, 15, 25, 50]
cv_errors = []
for k in k_values:
knn = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knn, X, y,
cv=StratifiedKFold(10, shuffle=True, random_state=1),
scoring='accuracy')
cv_errors.append(1 - scores.mean())
best_k = k_values[np.argmin(cv_errors)]
print(f"10-fold CV 最佳 K = {best_k}, 錯誤率 = {min(cv_errors):.4f}")
plt.figure(figsize=(8, 4))
plt.plot(k_values, cv_errors, 'o-', color='#58a6ff', linewidth=2)
plt.axvline(best_k, color='#3fb950', linestyle='--',
label=f'最佳 K={best_k}')
plt.xlabel('K (鄰居數)')
plt.ylabel('10-fold CV 錯誤率')
plt.title('用 CV 為 KNN 分類器選最佳 K (Caravan 資料)')
plt.legend()
plt.grid(alpha=0.3)
plt.xscale('log')
plt.show()
| 方法 | 訓練次數 | 訓練集大小 | 偏差 | 變異 | 適用場景 |
|---|---|---|---|---|---|
| 驗證集法 | 1 次 | ~50% | 偏高 | 偏高(依賴隨機切法) | 超大資料集、快速原型 |
| LOOCV | n 次 | n−1 | 最低(幾乎無偏) | 偏高(輸出高度相關) | n 小時可考慮;線性迴歸有快速公式 |
| k-fold CV | k 次 | (k−1)n/k | 中等 | 較低 | ⭐ 業界標準,k=5 或 k=10 |
銀行建立貸款違約預測模型後,用 5-fold CV 評估 AUC。由於違約樣本稀少,必須使用 StratifiedKFold 確保每折的違約比例一致。CV 結果用來決定模型是否上線、以及設定授信門檻。
用 MRI 影像預測腫瘤良性/惡性時,樣本數通常很少(n < 200)。LOOCV 在這類小樣本場景很常見,因為每一筆昂貴的醫療資料都不能浪費。但要注意 LOOCV 的高變異可能導致模型選擇不穩定。
新推薦演算法上線前,先在歷史資料上用 10-fold CV 評估點擊率提升。但要注意:使用者行為有時間相依性——不能把同一個使用者今天的資料放訓練、昨天的放驗證(時間穿越)。實務上常用 group-based CV 或時間分割。
電信業者預測哪些客戶可能跳槽。用 10-fold stratified CV 比較羅吉斯迴歸 vs 隨機森林 vs XGBoost。CV 的標準差用來判斷模型之間的差異是否「統計顯著」——而不只是看平均準確率的高低。
交叉驗證的核心思想——「反覆切割、獨立驗證、不求單次完美」——對 AI agent 系統設計有深刻啟發:
subagent-performance-management skill 的設計哲學——連續劣化才切換模型,而非單次失敗就換。就像 CV 的「最低點位置比數值重要」——agent 表現的趨勢比單次分數更關鍵。