搜尋課程與筆記

搜尋筆記

輸入主題、關鍵字或單元編號,例如 Flow Matching、U4。

    按 Esc 關閉 · ⌘ / Ctrl K 開啟搜尋

    L1.2 15 分鐘閱讀 2026年10月

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

    本篇重用2 篇

    老師用考古題教學,又用考古題出題

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

    全班平均 92 分。

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

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

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

    上一篇的 toy 知道真實函數,現在只有成交紀錄。若考題已經用來教過,92 分只代表舊題表現,不能直接代表新題能力。能不能保留一批未參與學習的題目,專門回答模型面對新資料時會怎樣?

    上一篇量 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[min⁡jR^j]≤min⁡jE[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 個候選在看到 validation 前就固定(可以依 training 決定),validation 是獨立 iid 樣本,loss 在 [0,1][0,1]。Hoeffding 控制每個固定候選的偏差;再用 union bound 同時控制全部:

    P(max⁡j≤m∣R^val(fj)−R(fj)∣>t)≤2m e−2nvalt2 ⟹ t≈log⁡m+log⁡(2/δ)2 nval.\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}}}}.

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

    mm 要包含這個事先固定集合的全部候選,不只是最後回報的三個。若根據 validation 結果不斷製造新候選,它們已經依賴 validation,不能只把嘗試次數填進上式就宣稱界仍成立。這是 adaptive selection 需要獨立最終評估的原因。

    洩漏破壞了上面需要的獨立性,因此這個界不能直接使用。效果不一定總是巨大,也不一定完全沒有徵兆;重點是評估流程已經不再模擬未見資料。

    展開細節為什麼「最小值是有偏的」用 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[min⁡jZj] ≤ min⁡jE[Zj]=min⁡jR(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

    這次實驗的 test 與 population 分數接近,validation 則較樂觀。但表中是重複實驗的中位數;前面的 unbiased 證明講的是期望,兩者不能當成同一個定理。三個候選數的結果也不足以證明偏差恰好按 log⁡m\sqrt{\log m} 增長,該式是上界尺度。

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

    第二段:刻意違反切分順序。 造 100 個樣本、1000 個 Gaussian noise 特徵與獨立的公平二元標籤。新資料的期望 accuracy 是 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.(跨學科盤點:洩漏在多少已發表研究裡造成了無法重現的結論。)