一場你無法預習的考試
想像一個學生把去年那份試卷上的每一個答案都背了下來。再把那份完全相同的卷子遞給他,他能拿滿分。但考試的意義從來都不在那份舊卷子上——而是要弄清楚他是否真正理解了這門學科,能不能應對從沒見過的新題目。一個機器學習模型面對的正是這種危險。它擅長在自己已經學過的資料上拿高分;真正難回答的問題是:它是否真的學到了能遷移到明天資料上的東西。
這種在從未見過的資料上也能表現良好的能力,叫做泛化,而一旦模型離開你的筆記型電腦,它就是唯一重要的東西。我們之所以要把一個資料集切分成幾堆,正是為了手裡始終握著一個對泛化能力的誠實估計。如果我們只在模型訓練過的那些例子上去衡量它,我們什麼有用的東西都學不到——我們衡量的不過是它的記憶力。切分,就是一種始終把一部分題目藏起來的辦法,這樣它在這些題目上的表現,才能公平地預演真實世界。
三堆資料,三種用途
標準做法——訓練/驗證/測試切分——把你的資料分成三堆,每一堆都有一份絕不允許它做第二次的工作。訓練集是課本:模型直接學習它,透過梯度下降或它所採用的任何學習規則,調整自己的參數去擬合這些例子。這通常是最大的一堆,往往佔 70%–80% 左右,因為學習材料越多,模型一般準備得越充分。
驗證集是模擬考。你不在它上面訓練——而是用它來對模型做各種選擇:要多少層、學習率設多大、用多少隨機失活、什麼時候停。這些你手動撥動的旋鈕就是超參數,而驗證集正是你比較各種設定好壞的地方。因為你不斷地拿它來檢查、並朝著有效的方向去調整,所以驗證集會悄悄地塑造模型——儘管模型從不直接在它上面訓練。
測試集是真正的期末考,它有一條神聖的規則:你只在最後、在所有選擇都已敲定之後,看它一次。它是你對「模型在從未見過的資料上將如何表現」的唯一一張誠實快照。提前去碰它——哪怕只是偷看一眼——它就不再是一場公平的考試了,因為你必然會不自覺地把自己的決定往它身上靠。典型的切分會把 10%–20% 留在這裡,原封不動地鎖在抽屜裡,直到工作完成。
data = shuffle(all_examples) # break any hidden ordering train = data[0 : 70%] # learn parameters here val = data[70% : 85%] # choose hyperparameters here test = data[85% : 100%] # touch only ONCE, at the end model.fit(train) # train repeatedly tune_until_happy(model, val) # compare on val, never on test final_score = model.eval(test) # the one honest number
為什麼測試集必須鎖起來
這裡有一個連老手都會中招的微妙陷阱。假設你跳過驗證集,直接拿測試集來調超參數——試五十種設定,留下在測試集上得分最高的那個。這感覺無傷大雅;畢竟你又沒在它上面訓練過。但當你挑出那個恰好在這些特定測試樣本上獲勝的設定時,你已經悄悄讓測試集滲進了你的決策。它的分數現在偏樂觀了,而你在真實世界裡的表現,將會是一個令人失望的意外。
這是資料洩漏的一種形式:你本應去預測的那部分資料,其資訊偷偷溜進了模型的建構過程。每瞄一眼測試集,都會花掉它一點誠實,而這份誠實不會再生。驗證集存在的意義,恰恰就是用來承受這種磨損。你被允許「濫用」驗證集——檢查一千次、把你的選擇過度貼合它——因為它本來就不該是最終定論。而保持純淨的測試集,正是在你不小心把決策過擬合到驗證集上時,能把你抓個正著的那個。
切分遠不止「切一刀」那麼簡單
人們很容易以為切分不過是把一個清單砍成三段,但一刀切得馬虎,就會悄悄毒害下游的一切。如果你的資料本來就是排好序的——所有貓的照片在前,所有狗的照片在後——那麼從最前面切出來當訓練集,會讓模型直到考試當天才第一次見到狗。這就是為什麼要先打亂、再切分。而光是打亂,有時也並不夠。
當某一類很稀少時——比如詐欺只佔交易的 1%——一次普通的隨機切分,可能讓你的測試集裡幾乎一條詐欺都沒有,從而讓它的分數失去意義。解決辦法是分層切分,它讓每一堆都保持相同的類別比例;在嚴重的類別不平衡之下,這一點尤其重要。而對於活在時間裡的資料——股價、感測器日誌——你絕不能打亂,因為那會讓模型用未來去預測過去。在那種情況下,你要按時間切分:用較舊的資料訓練,用較新的資料測試。
還有一條規則,決定了整件事是否誠實:先切分,後清洗。如果你要算一個平均值來填補缺失值,或者用資料的均值和離散程度去縮放你的特徵,那就只用訓練集來算——然後把這同一套數字套用到驗證集和測試集上。要是把所有資料放在一起算,你就已經讓測試集把它的統計量悄悄說進了訓練裡。這還是同一種洩漏,只是換上了一身更體面的偽裝。
當資料稀少時:交叉驗證
三分切分有一個實打實的代價:它要花掉資料。如果你總共只有 300 個例子,切出 45 個當驗證集,剩給你的就是一個搖搖晃晃的估計——換一批不同的 45 個,分數就能蹦躂好幾個百分點,告訴你的更多是運氣,而不是你的模型。在小資料集上,單獨一個驗證集的雜訊實在太大,靠不住。
交叉驗證是優雅的出路。最常見的形式 k 摺,把訓練資料切成 k 份等大的薄片——比如五份。你訓練五次:每一輪,用四份教模型,留出的第五份給它打分。輪流讓每一份都當一次評卷人,然後把五個分數平均起來。現在每個例子都既幫過訓練、又幫過評估,只是從不在同一輪裡——於是你得到一個穩定得多的估計,卻沒浪費任何資料。
交叉驗證替代的是驗證集,而不是測試集。那場誠實的期末考依然躺在它上鎖的抽屜裡:你所有的 k 摺調參都在訓練那部分上進行,唯有當每一個決定都塵埃落定,你才去讀一次測試集。我們會在後面的指南裡深入它的機制——分層摺、留一法、在巨型模型上跑它的代價。眼下,先記住這個核心想法:資料充裕時,一次乾淨的三分切分就夠了;資料稀少時,就輪轉起來。
切分真正守護的是什麼
剝去所有的操作細節,切分守護的只有一件事:你對自己的模型說真話的能力。如果一個排行榜上的數字,是在模型早已嚐過的資料上測出來的,那它什麼也不意味著。現實世界裡那些最尷尬的 AI 翻車,往往並不離奇——不過是一個在實驗室裡看起來很棒的模型,因為某個人、在某個環節,讓測試資料洩漏了,而那個閃閃發光的分數從一開始就不是真的。
所以,把這三堆資料當作一種紀律,而不是一道手續。在開始之前就定好怎麼切,記清楚哪些例子歸哪一堆,絕不讓測試集透過清洗、調參或一次偷瞄而洩漏,並且只報告一次測試分數——哪怕它令人失望。正是這份誠實,把一個你敢部署的模型,和一個只是演示得好看的模型區分開來。認真切分這個並不光鮮的習慣,歸根結底,正是讓你在它之上建構的一切都值得信任的根基。