L1.2 16 分鐘閱讀 2026年9月

L1.2 老師用考古題教學又出題:為什麼要切資料

本篇重用M1.1從鞋子猜身高:Conditional Expectation·M5.0保險為什麼存在:Convex Function 與 Jensen 不等式

起點:老師用考古題教學,又用考古題出題

一位老師整個學期都用歷屆考題上課,每一題都講過。期末考,他從同一批歷屆考題裡挑了二十題。

全班平均 92 分。

這個 92 分代表什麼? 它不代表學生會這門課,也不代表他們不會——它幾乎不提供資訊,因為衡量用的題目和教學用的題目是同一批。

這件事在機器學習裡不只發生一次,而是至少發生在三個層次,一個比一個難察覺:

  1. 模型調參數時看過那些資料——所以訓練誤差不能當成績(台北的房仲去台中:風險與經驗風險 已經量過這個偏差)。
  2. 你調超參數時看過那些資料——所以驗證分數也不能當成績。這是這一篇的主題。
  3. 資料在切開之前就被前處理「混」過——這時三個集合全部同時失效,而且你不會有任何徵狀。這是本篇最貴的一節。

上一篇量 bias 與 variance 時,我們一直呼叫 truth()真實專案裡沒有 truth() 你手上只有資料,而要用資料估「在沒看過的資料上表現如何」,唯一的辦法就是真的留一部分不看

上一篇量 bias 與 variance 時用了真實函數,
真實專案沒有它。
只有資料的時候,「在沒看過的資料上表現如何」要怎麼估?

課堂提問Q1

把三個集合翻成三個不同的問題。為什麼「用驗證集挑出來的模型,它的驗證分數」是有偏的?測試集為什麼只能看一次——看第二次會發生什麼?

先想一想,再展開看整理後的答案

三個問題,三份資料:

集合回答的問題誰在用它做決定
訓練給定這個模型設定,參數該設多少?最佳化器
驗證這些設定裡,哪一個比較好?
測試我最後交出去的這一個,實際上多好?沒有人做決定——只讀一次

驗證分數為什麼有偏。 這是 L0.2 的論證再用一次,只是換一層:你在候選 {f1,,fm}\{f_1,\dots,f_m\} 裡挑驗證分數最低的那個。每個 R^val(fj)\widehat R_{\text{val}}(f_j) 都是 R(fj)R(f_j) 的無偏估計,但取最小值不是無偏的——E[minjR^j]minjE[R^j]\mathbb E[\min_j \widehat R_j]\le\min_j\mathbb E[\widehat R_j](Jensen 不等式的方向,見 保險為什麼存在:Convex Function 與 Jensen 不等式min\min 是凹函數)。你挑中的往往是「真的好」加上「這次驗證集剛好對它友善」的組合,而後者不會重現。

測試集看第二次會怎樣。 一旦你根據測試分數做了任何決定——換一個模型、多訓一輪、調一個閾值——測試集就變成了驗證集,它的分數就開始有同一種偏差。而人幾乎不可能看了不做決定,所以規則寫成「只看一次」。

「看」本身無害,根據它做決定才是。

每一次決定,花掉一點誠實度

把資料想成一種會被消耗的資源:每次你用某份資料做一個決定,那份資料對「未來的決定」的誠實度就下降一點。

  • 用訓練集調參數 → 訓練誤差不再誠實。
  • 用驗證集選 20 個候選 → 驗證分數不再誠實(偏一點點)。
  • 用驗證集選 200 個候選、跑了三個月、每天看一次 → 驗證分數偏很多。

這解釋了為什麼要有第三份資料,也解釋了為什麼「第三份」不是萬靈丹:如果你把測試分數拿回來當回饋,它就退化成第二份。 唯一能保護它的是紀律,不是統計。

還有一種更徹底的失效:洩漏(leakage)。上面的偏差是「用得太多次」,洩漏是「一開始就不該給」——測試集的資訊在訓練時就已經以某種形式進到模型裡。這時三個集合同時失效,而且你看到的所有數字都很漂亮。它的三種常見形式:

  • 前處理跨越切分:用全部資料算平均與標準差、做 PCA、挑特徵、填補缺失值,然後才切。本篇最後會示範這一種能造出多大的假象。
  • 重複或近重複的樣本:同一位病人的多張影像、同一則新聞的轉載、資料增強之後才切分。模型在測試集看到的是它訓練過的東西的複本。
  • 時間:用未來預測過去。隨機切分一份時間序列,訓練集裡就有測試時點之後的資訊。

樂觀量隨候選數怎麼長

mm 個候選、驗證集大小 nvaln_{\text{val}}、損失有界。對每個固定的 fjf_jR^val(fj)\widehat R_{\text{val}}(f_j) 集中在 R(fj)R(f_j) 附近,偏離超過 tt 的機率隨 e2nvalt2e^{-2n_{\text{val}}t^2} 衰減(Hoeffding)。要同時mm 個候選都成立,取 union bound:

P(maxjmR^val(fj)R(fj)>t)2me2nvalt2  tlogm+log(2/δ)2nval.\mathbb P\Big(\max_{j\le m}\big|\widehat R_{\text{val}}(f_j)-R(f_j)\big|>t\Big)\le 2m\,e^{-2n_{\text{val}}t^2} \ \Longrightarrow\ t\approx\sqrt{\frac{\log m+\log(2/\delta)}{2\,n_{\text{val}}}}.

兩個依賴要記住:分子是 logm\log m(候選數的對數——所以候選從 5 個變 200 個,樂觀量只成長幾倍,不是四十倍),分母是 nvaln_{\text{val}} 這給了兩個實務推論:驗證集要夠大(樂觀量以 1/nval1/\sqrt{n_{\text{val}}} 縮小),而「多試幾個設定」的代價是溫和但真實的——它不是零。

logm\log m 裡的 mm你實際比較過的候選數,包含你手動試過又丟掉的那些。一個跑了三個月、每天看驗證分數並據此改一點東西的專案,mm 是幾百,不是你最後寫在論文裡的三個。

洩漏不在這個框架裡。 上面的界假設驗證集與訓練過程獨立。洩漏違反的正是這個假設,所以沒有任何界適用——這也是為什麼洩漏造成的假象可以任意大,下面會看到 50%81%50\%\to81\%

展開細節為什麼「最小值是有偏的」用 Jensen 就能看出來

Zj=R^val(fj)Z_j=\widehat R_{\text{val}}(f_j),每個都無偏:E[Zj]=R(fj)\mathbb E[Z_j]=R(f_j)。函數 min\min 是凹的,所以由 Jensen(保險為什麼存在:Convex Function 與 Jensen 不等式

E[minjZj]  minjE[Zj]=minjR(fj).\mathbb E\big[\min_j Z_j\big]\ \le\ \min_j \mathbb E[Z_j]=\min_j R(f_j).

不等號的方向是固定的:你回報的那個最小驗證分數,在期望上不高於候選中最好的真實風險——而你真正拿到的模型甚至不一定是最好的那個候選。兩層樂觀疊在一起。

差距有多大,取決於 ZjZ_j 之間的相關性與各自的散佈:候選彼此高度相關(例如 degree 8 與 degree 9 的模型幾乎一樣)時差距小;候選各自獨立地亂跳(例如比較 200 個隨機種子)時差距大。這也是為什麼「比較很多個幾乎相同的設定」比「比較很多個天差地遠的設定」安全一些。

回到情境:規則與它們的理由

規則一:先切分,再做任何事。 切分是流程的第一步,不是資料清理之後的第二步。所有從資料估出來的東西——平均、標準差、詞彙表、PCA 的基底、缺失值的填補量、特徵選擇的名單——都只能用訓練集擬合,再套用到驗證與測試。

規則二:切分要沿著「相依的單位」切,不是沿著列。 同一位病人、同一個使用者、同一份原始文件的所有列,必須整組落在同一邊。以列為單位隨機切,等於把近重複樣本同時放進兩邊。

規則三:有時間結構就用時間切。 訓練用較早的區間、驗證與測試用較晚的區間。這同時模擬了真實部署(你總是用過去預測未來),也避免了未來資訊回流。

規則四:測試集只讀一次,而且要有紀錄。 實務上的作法是把它加密封存、或交給不做建模的人保管,並且在讀之前先寫下「我預期會看到多少」。這一條看起來像儀式,但它是唯一能保護第三份資料的東西。

這一切依賴什麼。 一,三份資料同分佈;若測試集刻意取自更晚的時間或另一個地區,它量的是「偏移下的表現」,是另一個(通常更有用的)問題,但別把它當同分佈的估計。二,測試集夠大——ntest=30n_{\text{test}}=30 的分數本身有很大的 variance,兩個模型差 1% 沒有意義。三,你誠實計算 mm

回到情境:兩個實驗

第一段:候選越多,驗證分數越樂觀;測試集不動如山。 每次抽 120 筆資料,切成 60 訓練 / 30 驗證 / 30 測試,用驗證集從 mm 個候選(不同次數與 ridge 強度)裡挑一個,然後比較三個數字。重複 600 次取中位數(分佈右尾很長,平均會被少數幾次災難拉走——這件事本身就值得知道):

候選   5 個:validation 0.0241   test 0.0250   真實風險 0.0251
候選  20 個:validation 0.0234   test 0.0257   真實風險 0.0261
候選 200 個:validation 0.0224   test 0.0251   真實風險 0.0255

測試集那一欄與真實風險那一欄始終貼合0.0250/0.02510.0250/0.02510.0257/0.02610.0257/0.02610.0251/0.02550.0251/0.0255)——它沒有參與挑選,所以它誠實。驗證那一欄則隨候選數變多而越來越低於真相:從低 0.00100.0010 到低 0.00310.0031。候選數乘了四十倍,樂觀量只成長約三倍——正是 logm\sqrt{\log m} 的形狀。

實務翻譯:如果你只有驗證分數,你回報的數字大約是「真實表現」再打一個折,而折扣的大小取決於你試過幾個設定。 這個折扣在 nval=30n_{\text{val}}=30m=200m=200 時大約是 12%12\%;驗證集越小、試得越多,折扣越大。

第二段:刻意違反「先切分再前處理」。 造一份標籤與特徵完全無關的資料:100 個樣本、1000 個純 Gaussian noise 特徵、標籤是公平的銅板。任何誠實的評估都必須給出 50%50\%

import numpy as np
rng = np.random.default_rng(0)
n, p, K, k_sel = 100, 1000, 5, 20
acc_wrong, acc_right = [], []
for rep in range(50):
    X = rng.standard_normal((n, p))                 # 1000 個純噪聲特徵
    y = rng.integers(0, 2, n)                       # 標籤與 X 完全無關
    folds = np.array_split(rng.permutation(n), K)

    def cv(select_inside):
        if not select_inside:                       # 錯誤:先用全部資料挑特徵
            c = np.array([abs(np.corrcoef(X[:, j], y)[0, 1]) for j in range(p)])
            sel_all = np.argsort(-c)[:k_sel]
        out = []
        for f in folds:
            te, tr = f, np.setdiff1d(np.arange(n), f)
            if select_inside:                       # 正確:只用訓練折挑特徵
                c = np.array([abs(np.corrcoef(X[tr, j], y[tr])[0, 1]) for j in range(p)])
                sel = np.argsort(-c)[:k_sel]
            else:
                sel = sel_all
            A = np.c_[np.ones(len(tr)), X[np.ix_(tr, sel)]]
            w = np.linalg.lstsq(A, y[tr]*2 - 1, rcond=None)[0]
            B = np.c_[np.ones(len(te)), X[np.ix_(te, sel)]]
            out.append((((B @ w) > 0).astype(int) == y[te]).mean())
        return np.mean(out)

    acc_wrong.append(cv(False)); acc_right.append(cv(True))

print(f"先挑特徵再做 CV(洩漏):  {np.mean(acc_wrong):.3f}")
print(f"每一折內部才挑特徵(正確):{np.mean(acc_right):.3f}")
先挑特徵再做 CV(洩漏):  0.814
每一折內部才挑特徵(正確):0.491

81.4%81.4\% 的準確率,資料裡沒有任何訊號。

機制很簡單也很可怕:在 1000 個純噪聲特徵裡,一定有幾個碰巧和這 100 個標籤相關。用全部資料挑出這幾個,等於把每一筆測試樣本的標籤資訊寫進了「哪些特徵被選中」這件事裡。之後的交叉驗證再怎麼嚴謹,都是在一個已經偷看過答案的特徵子集上做的。

這個錯誤在真實專案裡長什麼樣。 它幾乎從不長成「我故意用了測試集」,而是長成一行放錯位置的程式:

  • scaler.fit(X) 寫在 train_test_split 之前。
  • 先對整份資料做 PCA 降維、或用整份資料訓一個 embedding,再切。
  • 先用整份資料選出「重要特徵」,再開始做嚴謹的交叉驗證。
  • 先做 SMOTE / 資料增強、再切分(複本橫跨兩邊)。

沒有任何徵狀。 訓練與驗證曲線都很正常,四格診斷會告訴你「成功」。唯一的防線是流程本身。而唯一能事後抓到它的,是一份從來沒進過這條流程的資料——或是像這裡一樣,在一份你知道答案是 50%50\% 的假資料上跑一遍整條流程。這是一個值得對任何評估流程做一次的健康檢查:把標籤打亂,重跑,看看它是不是誠實地回報「沒有訊號」。

切分是第一步,不是資料清理之後的第二步。

切分順序與候選數:兩種偏差的來源

互動 demo:候選越多,勝出者的驗證分數越樂觀;把驗證集加大比少試幾組有效得多。

先消化一下

想一想

一個團隊三個月來每天在驗證集上比較新想法,最後在論文裡回報「我們比較了 3 個方法,最好的驗證準確率是 94.1%94.1\%」,並用同一份驗證集當作最終成績。最準確的批評是:

想一想

在噪聲實驗裡,把 1000 個特徵改成 20 個特徵(其餘不變),洩漏版的準確率會怎麼變?

想一想

下列哪一個做法不會造成洩漏?

參考文獻

  1. Hastie, T., Tibshirani, R., Friedman, J. The Elements of Statistical Learning, 2nd ed. Springer 2009, Ch. 7.10.2.(「錯誤的交叉驗證方式」——本篇噪聲實驗的原型。)
  2. Kaufman, S., Rosset, S., Perlich, C. Leakage in Data Mining: Formulation, Detection, and Avoidance. KDD 2011.(洩漏的分類、在真實競賽資料上的案例,以及可操作的偵測方法。)
  3. Kapoor, S., Narayanan, A. Leakage and the Reproducibility Crisis in ML-based Science. Patterns 2023.(跨學科盤點:洩漏在多少已發表研究裡造成了無法重現的結論。)