JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

過擬合與正則化

一個在訓練資料上拿滿分的模型,到了真實世界裡照樣可能翻車。這正是機器學習最核心的一齣戲——以及我們用來打贏這場仗的工具箱:正則化、早停、還有那條學習曲線。

我們要的從來就不是訓練集

在本階梯前面的幾篇裡,你已經搭好了「學習」這台引擎:一個度量誤差的損失函數,外加一套不斷擰旋鈕、把誤差往下壓的梯度下降。那麼這裡有個讓人不太舒服的問題:如果「學習」就是把訓練損失壓到盡可能小,那為什麼不乾脆壓到*零*?把每一個樣本都背得滴水不漏,你就有了一個永不出錯的模型。這難道不該是目標嗎?

因為訓練集我們再也不會遇到第二次了。我們收集了它,但模型真正的工作,是在它*從未見過*的資料上——明天的郵件、下個月的病人、下一位使用者上傳的照片。我們真正在乎的那個東西有個名字:[[generalization|泛化]],也就是在來自同一個世界的新資料上的表現。一個把訓練樣本背得很熟、卻在新樣本上栽跟頭的模型,就是發生了[[overfitting|過擬合]]。相反的失敗——模型太粗糙,連訓練資料裡的規律都抓不住——叫做[[underfitting|欠擬合]]

打個比方就很清楚了。想像一堆散落的點,大致沿著一條平緩的曲線分布,再帶一點隨機雜訊。欠擬合的模型,會拿一條直線穿過去——太僵硬,把那個彎給漏掉了。好的模型,會順著那條平緩的曲線走。而過擬合的模型,會扭來扭去地穿過*每一個點*,為了命中每一個而瘋狂擺動——連雜訊也一起命中了。換成新的點,那條瘋狂扭動的蛇就徹底沒轍,而那條沉穩的曲線卻笑到最後。把雜訊也背下來,正是這個陷阱。

偏差與變異:出錯的兩種方式

欠擬合和過擬合,是同一個旋鈕的兩端,經典的命名方式叫做[[bias-variance-tradeoff|偏差—變異權衡]]。*偏差*(bias)是來自錯誤假設的誤差——模型太簡單,彎不到真相彎的那個方向,於是不管你餵多少資料,它都一貫地偏;那條穿過曲線的直線就是高偏差。*變異*(variance)是來自敏感性的誤差——模型把自己擰成正好貼合它碰巧看到的那批資料的樣子,於是換一批訓練樣本,就會得出一個面目全非的模型;那條穿過每個點的蛇就是高變異。

*權衡*這個詞,正是它的靈魂。把模型變得更靈活——更多層、更多參數、更高的容量——你壓低了偏差,卻抬高了變異;把它變簡單,則正好反過來。幾十年來,這幅圖景是一條 U 形曲線:隨著容量增加,總誤差先下降,在某個甜蜜點觸底,然後隨著變異佔上風又重新爬升。你的任務,就是找到這個 U 形的底部。

讀懂學習曲線

看不見的東西沒法修,所以在任何治療之前,你都需要先做診斷:那就是[[learning-curve|學習曲線]]。這套做法來自前面的階梯——你把資料切成訓練集和驗證集,用前者訓練,並在訓練過程中一個輪次接一個輪次地盯著*兩邊*的損失。一張圖上的兩條線,幾乎能告訴你正處於哪一種失敗模式。

如果兩條線都又高又平,你就是在*欠擬合*——模型沒有足夠的容量或訓練量來抓住規律;那就給它更多靈活性,或者訓練更久。如果訓練那條線一路下降,而驗證那條線先觸底、然後掉頭*向上*,那兩條線之間的縫隙,就是*過擬合*的招牌徵兆:模型這會兒正在學訓練資料裡那些到別處反而有害的怪癖。這道越拉越大的縫隙,有時被稱作泛化差距,而縮小它,正是本篇要玩的整個遊戲。

for epoch in range(max_epochs):
    train_one_epoch(model, train_data)   # weights move
    train_loss = evaluate(model, train_data)
    val_loss   = evaluate(model, val_data)
    log(epoch, train_loss, val_loss)
    # diagnosis, read off the two curves:
    #   both high      -> underfitting
    #   gap widening   -> overfitting
    #   val at minimum -> best point to stop
學習曲線,無非就是每個輪次記錄下來的兩個損失數字——訓練損失和驗證損失。這兩條線的形狀,就是你的診斷結果。

正則化:溫柔地懲罰複雜

治過擬合最根本的一招,是正則化:你不再要求優化器*只*去擬合資料,而是再加上一項,讓它偏好更簡單的模型。新的目標變成 `損失 = 資料誤差 + lambda × 複雜度`。優化器這下要平衡兩股拉力——既要擬合樣本,又要保持簡單——而 `lambda` 就是那個超參數旋鈕,決定你往「簡單」那邊推得有多用力。把 `lambda` 擰到零,正則化就消失了;擰得太高,又會逼出欠擬合。

那「複雜度」要怎麼量?最常見的答案是權重的大小。大的權重,會讓模型對輸入的微小變化做出劇烈反應——這正是那條扭動的蛇的行為——所以我們要懲罰它們。L2 正則化加上的是權重的平方和;它把每個權重都平滑地往零收縮,卻又不會真的到零,於是影響力被攤薄到許多個小權重上。L1 正則化加上的是絕對值之和;它會把許多權重*恰好*推到零,等於刪掉了一些特徵,給出一個稀疏、更易解讀的模型。這兩者的經典搭配——嶺迴歸對應 L2、套索對應 L1——正是從線性迴歸裡來的。

在深度學習裡,你會聽到 L2 正則化被叫做[[weight-decay|權重衰減]],這兩者幾乎是同一個想法的兩個視角。在損失裡懲罰權重的平方,在數學上等價於:每走一步梯度,就把每個權重乘上一個略小於一的數——權重於是溫柔地朝零「衰減」,除非資料不斷把它們推回去。(用上 Adam 這類自適應最佳化器時,這兩種形式會略有分歧,這也是為什麼人們發明了「解耦」權重衰減——這個細微之處,知道它存在就好。)

早停,以及更大的工具箱

學習曲線還順手遞給你一個最簡單、最省錢的正則化手段:[[early-stopping|早停]]。既然驗證損失會先觸底再爬升,那就*在底部停下來*唄。實操上,你盯著驗證損失,記住目前為止見過的最佳模型,一旦它連續若干個輪次(這個數叫「耐心值」)都沒再變好,就喊停——然後回滾到那個最佳存檔點。你就這樣幾乎不花代價地,拿到了訓練中段那個還沒開始過擬合的、擬合得恰到好處的模型。

專門針對神經網路,最強力的招數當屬[[dropout|隨機失活]](dropout):在每一步訓練裡,隨機關掉一部分神經元,逼著網路把賭注分散開,而不是依賴某一條單一通路。這就像一支隊伍在排練時隨機讓幾個成員缺席——沒有誰能變成唯一的故障點。與之密切相關的,是去搞到*更多或更多樣的資料*:資料增強會製造出新的訓練樣本(把圖像翻轉、加點雜訊、把句子換種說法),它往往是抗過擬合裡槓桿率最高的一招,因為這劑藥直接對準了病根——相對於模型容量,樣本太少了。

把它們串起來——再說句實在話

  1. 先訓練,並畫出學習曲線。兩條線都又高又平?那是欠擬合——先加容量或訓練更久,根本還輪不到操心正則化。
  2. 看到驗證那條線掉頭向上、而訓練那條還在下降?那道縫隙就是過擬合。這時候才該伸手去拿工具箱。
  3. 能拿到更多或更多樣的資料就先拿(包括增強)——它打的是病根。
  4. 加上權重衰減(L2),神經網路的話再加 dropout;在驗證集上、而不是測試集上調它們的強度。
  5. 打開早停,這樣你總能保住最佳存檔點。最後一刻才打開測試集一次,報出一個誠實的數字。

退一步看,這種統一性令人驚嘆:正則化、早停、dropout、更多資料——它們每一個,都是在對模型說一句*「別那麼死心眼地全信你的訓練資料」*。這份謙遜,正是全部的祕訣。一個把樣本擬合得天衣無縫的學習器,學會的是過去;一個抵抗著不去完美擬合樣本的學習器,才有機會贏得未來。

最後留一句實在的提醒。沒有哪一種正則化對所有問題都最好——沒有免費午餐定理保證了這一點,而你做的每一個選擇,都內嵌了一種歸納偏置,也就是關於「簡單」對你的資料該意味著什麼的一個假設。正則化不會白送你泛化能力;它是拿一個你能講清道理的偏差,去換一份你能量出來的變異下降。帶著這份清醒去用它,它就是整個機器學習裡最可靠的那根槓桿。