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

讓模型舉一反三:擊敗過度擬合

在訓練影像上滿分、面對新影像卻不及格,那只是死記而非學會——這裡是逼出真正泛化能力的工具。

死記 vs 理解:什麼是過度擬合

在第 1 篇你學會了看一次訓練的兩條曲線:訓練損失(模型在它拿來學的影像上表現多好)與驗證損失(模型在從未拿來訓練、被保留下來的影像上表現多好)。一開始兩者會一起下降——模型確實在學東西。但若把一個強大的模型放在固定的一批影像上訓練得夠久,會發生一件令人不安的事:訓練損失持續往零掉,驗證損失卻先觸底、接著開始往上爬。那道愈拉愈大的差距,正是過度擬合的指紋。

想像兩個學生準備考試。一個把去年考卷的標準答案背得滾瓜爛熟;另一個則弄懂背後的概念。重考去年那份考卷時,死背的那個看起來像天才——滿分。但換一份新考卷給他,他就崩潰了,因為他從沒真正理解什麼,他只是把答案存了起來。一個高容量的神經網路,參數多到足以做到一模一樣的事:它幾乎可以把每一張訓練影像都背下來,拿到接近完美的訓練準確率,卻沒學到任何能搬到新照片上的東西。本篇的全部工作,就是要養出那個「真正理解」的學生。

欠擬合、良好擬合、過度擬合。過擬合的曲線扭曲自己只為穿過每一個訓練點——而它的泛化能力最差。

三個面板,分別顯示太簡單的模型、剛好抓住趨勢的模型,以及扭來扭去穿過每個帶雜訊資料點的模型。

經典畫面是兩條分岔的曲線:訓練損失一路下滑,驗證損失則畫出一個 U 形——先降、到達最低點、再往上翻。那個 U 的最低點,就是泛化能力最好的時刻;它右邊的一切,都是模型拿真正的理解去換取背下來的細節。解法是一整族技術,統稱正則化:凡是我們加進訓練、用來打消死記、把模型推向更簡單、更可遷移之模式的手段都算。本篇接下來就是一趟逛遍最重要那幾招的旅程。

偏差與變異的權衡

在動手拿工具之前,先有一張「為什麼泛化會失敗」的心智地圖很有幫助。經典的地圖把模型在新資料上犯的誤差拆成三塊。偏差(bias)是因為模型太簡單、抓不住真正規律而產生的誤差——它欠擬合,不管你餵多少資料都漏掉了結構。變異(variance)是因為模型太敏感而產生的誤差——它去追逐「這一批」訓練資料裡的隨機雜訊,於是過擬合,只要把資料重新洗牌它就劇烈改變。第三塊是不可消除的雜訊:烘焙在資料本身裡的隨機性(一張糊掉的照片、一個標錯的影像),任何模型都永遠解釋不掉。

用射飛鏢的比喻會讓這件事黏進腦袋。想像你朝靶心射飛鏢,每一支鏢就是「在某一種可能的資料集上訓練出來的模型」。高偏差代表你的鏢緊緊聚成一團卻落在錯的地方——一致地偏向某一側;你很精準,卻系統性地射錯。高變異代表你的鏢散落整片靶——有時靠近中心、有時遠遠飛出;平均而言你也許瞄準了靶心,但任何單獨一射都不可靠。夢想是又緊又準:低偏差、低變異。麻煩在於,把其中一個壓下去,往往會把另一個頂上來。

\mathbb{E}\big[(y-\hat{y})^2\big] = \underbrace{\text{Bias}^2}_{\text{too simple}} + \underbrace{\text{Variance}}_{\text{too complex}} + \underbrace{\sigma^2}_{\text{irreducible noise}}

一個預測的期望平方誤差,乾淨地拆成三個來源。

由左讀到右:y 是我們希望能預測的真實標籤;ŷ(y-hat)是模型的預測;(y − ŷ)² 是平方誤差,而 E[·] 表示把這個誤差在所有資料、以及在我們可能拿來訓練的所有資料集上取平均。Bias²(偏差平方)是「平均預測」離真相有多遠——當模型太簡單(欠擬合)時會很大。Variance(變異)是當你換一批資料重新訓練時,預測會晃動多少——當模型太複雜(過擬合)時會很大。σ²(sigma 平方)是不可消除的雜訊地板:世界裡真正隨機的那部分,任何模型再完美也拿不掉。關鍵叮嚀:這是一種思考方式,不是一條讓你代數字進去的公式——在真實資料上你永遠沒辦法把偏差和變異分開量出來。它是一副鏡片,而且是非常能讓人看清的一副。

隨著模型容量增加,偏差下降但變異上升;總誤差呈 U 形,最佳點落在中間。

一張圖,偏差隨複雜度下降、變異隨複雜度上升,兩者之和形成一個有最低點的 U 形。

你落在這個權衡的哪一點,由兩個旋鈕決定:模型容量與資料集大小。更大、更深的網路偏差較低(它能表達更豐富的模式)卻變異較高(有更多空間去死背)。更大的資料集則是偉大的平衡器——例子一多,任何單一樣本的隨機雜訊就互相抵消平均掉,模型便無法把雜訊誤認成訊號,變異於是下降。這正是為什麼對付過度擬合最強的兩帖藥——蒐集更多資料、施加正則化——主要都是在攻擊變異,而不是偏差。請把這點記在心裡:底下每一個技術,骨子裡都是一台「降低變異」的機器。

權重衰減與 L2 正則化:偏好更簡單的權重

最基本的顯式正則化問了一個簡單的問題:在所有能擬合訓練資料的模型裡,為什麼不挑最簡單的那個?這就是把奧坎剃刀(Occam's razor)變成數學。網路靠大權重來表達複雜、鋸齒狀的函數——大數字讓一層能對輸入的微小變化做出劇烈反應,而那正是我們在過擬合曲線裡看到的扭動。於是我們加上一個懲罰,讓大權重變得昂貴。這就是權重衰減,視覺領域裡正則化的主力,而且用起來幾乎不花成本。

L_{\text{total}} = L_{\text{data}} + \frac{\lambda}{2}\,\lVert\theta\rVert^2 \quad\Longrightarrow\quad \theta \;\leftarrow\; (1-\eta\lambda)\,\theta \;-\; \eta\,\nabla L_{\text{data}}

在損失上加一個 L2 懲罰,會把更新變成「先收縮、再前進」的規則。

我們來拆解。L_data 是第 1 篇那個一般的資料損失——預測錯得多離譜。θ(theta)是網路全部權重打成的一捆。‖θ‖² 的意思是把每個權重平方、再全部加起來,所以它是一個「權重一變大就變大」的單一數字。λ(lambda)是正則化強度:一個你自己設的旋鈕,λ 愈大就愈用力把權重推小、把模型推簡單(λ = 0 代表完全不正則化)。那個 ½ 只是記帳用的,讓微分算出來乾淨。現在看箭頭的魔法:當你把那個懲罰的梯度算出來、走一步時,懲罰恰好貢獻一個乘在 θ 上的因子 (1 − ηλ),其中 η(eta)是學習率。由於 η 和 λ 都是小的正數,(1 − ηλ) 是一個略小於 1 的數。所以在每一次梯度步之前,每個權重都被乘上大約 0.999——它真的被往零「衰減」了一點。這就是為什麼它叫權重「衰減」。

用一個具體數字讓收縮變得摸得到。假設學習率 η = 0.1、權重衰減 λ = 0.1,那麼收縮因子是 1 − ηλ = 1 − 0.01 = 0.99。一個目前停在 2.0 的權重,若資料梯度剛好為零,這一步會變成 2.0 × 0.99 = 1.98,下一步 1.96,依此類推。把這個 0.99 因子套用一百次,你會得到 0.99¹⁰⁰ ≈ 0.37——光靠衰減的壓力,權重就縮到大約原來的三分之一。實務上資料梯度會反抗,在權重真正有用的地方把它往上拉;最後的平衡,是一個只留下「真正需要」之權重的模型,而那正是我們要的那種平滑、簡單的函數。

現在來談第 2 篇預告過、關於 Adam 優化器 的微妙之處。上面那個乾淨的 (1 − ηλ) 收縮,正是用單純 SGD 時會發生的事。但 Adam 會用每個梯度大小的滑動估計去重新縮放梯度。如果你用最天真的方式實作權重衰減——把 λθ 懲罰塞進梯度裡——Adam 接著會把那個懲罰除以同一個「逐權重的縮放」,於是歷史梯度大的權重被衰減得比較少,這並不是你要的。解法叫做解耦權重衰減(AdamW 裡的那個 W):讓懲罰跳過梯度機器,直接把 (1 − ηλ) 收縮當成獨立的一步套在權重上。正因如此,AdamW 如今是訓練現代視覺模型的預設選擇。

# SGD: naive L2 and decoupled decay are identical.
# Adam: they differ. AdamW does the shrink OUTSIDE the adaptive step.

lr, wd = 0.1, 0.1          # learning rate eta, weight decay lambda

# --- Plain L2 folded into the gradient (the naive way) ---
grad = data_grad + wd * theta        # penalty rides through Adam's rescaling
theta = adam_step(theta, grad, lr)   # ... so it gets distorted per-weight

# --- Decoupled weight decay (AdamW, preferred) ---
theta = adam_step(theta, data_grad, lr)   # adaptive step on the DATA grad only
theta = (1 - lr * wd) * theta             # then shrink directly: 1 - 0.1*0.1 = 0.99
搭配 Adam 時,把衰減解耦,讓收縮維持成乾淨的 (1 − ηλ) 因子。

Dropout:用隨機刪除來訓練一個集成

Dropout(隨機失活)是一個漂亮而粗暴的正則化點子:在每一次訓練步,隨機挑出一層裡比例為 p 的神經元,把它們整個關掉——把它們的激活設成零。下一步,換一組隨機的神經元變黑。網路永遠無法依賴任何單一神經元,因為那個神經元下一個批次可能就消失了。被迫在自己一部分隨機缺席的情況下存活,網路便學會把每一塊資訊冗餘地分散到許多神經元上,而不是讓少數幾個互相共適應的單元結成一個脆弱、死記的小圈圈。

還有一個更深的看法。每一種「哪些神經元被關掉」的隨機模式,都定義出一個不同的、瘦身過的子網路。在數千個訓練步裡,你其實是在訓練一個數量大到天文數字的子網路集合,而它們全都共用同一組權重。在測試時,使用完整的網路就近似於把整個集成的預測取平均。集成——把很多模型平均——是削減變異最古老的招數之一,而 Dropout 讓你用一個網路的代價,買到一個巨大的集成。

\tilde{a} = \frac{1}{1-p}\,\big(\mathbf{m}\odot a\big), \qquad \mathbf{m}\sim \text{Bernoulli}(1-p)

訓練時的反向 Dropout:先遮罩,再把存活者重新縮放。

逐符號來看:a 是該層在 Dropout 之前的激活向量。p 是丟棄機率——任何一個單元被殺掉的機率(例如 0.5)。m(遮罩)是一個由 0 與 1 組成的隨機向量,每一項以機率 (1 − p) 為 1——也就是每個單元以機率 1 − p 被保留。⊙ 表示逐元素相乘,所以 m ⊙ a 留下存活的激活、把其餘歸零。最後我們除以 (1 − p),把存活者重新放大,得到丟棄後的激活 ã。為什麼要重新縮放?因為在測試時我們用全部的神經元,所以流進下一層的總訊號會比訓練時更大。在訓練時除以 (1 − p),能讓「期望的總激活」對齊測試時的總量——下游那一層在兩個階段看到的平均輸入量級相同,於是訓練與部署之間不必再重新調校任何東西。

一個極小的演算檢查能讓重新縮放「卡」進腦中。假設一層有 4 個神經元,每個輸出 1.0,所以總和是 4.0,且 p = 0.5。在某一步,遮罩殺掉其中 2 個:存活的總和是 2.0。除以 (1 − p) = 0.5——也就是乘以 2——重新縮放後的總和又回到 4.0。所以在許多隨機遮罩上平均下來,這一層送給下一層的總量仍然是 4.0,正是測試時「什麼都不丟」會送出的量。這就是為什麼這套做法叫反向 Dropout:我們把縮放放在訓練時,好讓測試時保持簡單而乾淨。

def dropout(a, p, training):
    # a: layer activations; p: drop probability in [0, 1)
    if not training or p == 0:
        return a                      # test time: use the full network, no scaling
    keep = 1.0 - p
    mask = (rand_like(a) < keep)       # 1 with prob (1-p), else 0
    return (mask * a) / keep           # zero the dropped, scale up the survivors
反向 Dropout:所有工作都發生在訓練時;推論完全不動。

資料擴增:用變換換來免費的資料

如果你能蒐集到十倍多的標註影像,你一定會去蒐集。資料擴增是退而求其次的最佳選擇,而在電腦視覺裡,它是最有效的單一正則化手段。這個想法簡單到讓人卸下心防:與其把同一張訓練影像一遍又一遍餵給模型,不如在每次看到它時都稍微變換一下——裁切不同的區域、左右翻轉、把顏色推一推、轉幾度。標籤維持不變(一隻被裁切、被翻轉的貓仍然是貓),但網路看到的像素是全新的。你等於免費製造出了新的訓練樣本。

這個工具箱從經典的幾何與光度變換,一路到現代的混合方法。主力包括隨機裁切與縮放、水平翻轉、色彩抖動(亮度、對比、飽和度、色相的小幅移動)以及小角度旋轉。在這之外,Mixup 把兩張影像連同它們的標籤混在一起(70% 貓 + 30% 狗),CutMix 把一張影像的一塊補丁貼到另一張上、再按面積比例混合標籤,而像 RandAugment 這類基於策略的方法,會以隨機強度套用一串隨機的操作,讓你不必逐一手調。現代的影像分類器,幾乎總是疊著一整組這些手法來訓練。

為什麼這招這麼能打過度擬合?兩個原因。第一,它教給模型我們真正在乎的不變性:我們希望它無論貓出現在畫面哪裡、朝哪個方向、光線如何,都能認出那是貓——所以我們給它看位移過、翻轉過、重新打光過的貓,它便學到這些變化不會改變答案。那是真正的理解,從建構上就烘焙了進去。第二,它讓純粹的死背幾乎不可能。如果模型基本上永遠不會看到同一個像素陣列兩次,就沒有固定的標準答案可背;要在一條無止盡的變化流上把損失降下來,唯一的辦法就是學到那個真正、可遷移的模式。因此擴增是一種同時從兩側攻擊變異的正則化

一張標註影像變成許多張:裁切、翻轉、色彩抖動與旋轉,全都讓「貓」這個標籤維持不變。

一個網格,顯示一張貓的原始照片,被裁切、翻轉、改色與旋轉成好幾個變體。

# A typical training-time augmentation pipeline (applied fresh every epoch).
# Order matters: geometric first, then photometric, then normalize.
train_tf = Compose([
    RandomResizedCrop(224),          # random region + scale -> position/scale invariance
    RandomHorizontalFlip(p=0.5),     # left-right mirror -> left-right invariance
    ColorJitter(0.4, 0.4, 0.4, 0.1), # brightness/contrast/saturation/hue wobble
    RandAugment(),                   # random op sequence at random strength
    ToTensor(),
    Normalize(MEAN, STD),            # the SAME normalization guide 3 covered
])

# Validation/test: NO random augmentation -- evaluate on the clean image.
eval_tf = Compose([Resize(256), CenterCrop(224), ToTensor(), Normalize(MEAN, STD)])
對訓練流大量擴增;讓評估保持乾淨且確定性。

提前停止與以驗證集為準的訓練

回想第 1 節那條 U 形的驗證曲線:驗證損失先降、觸底、然後在模型開始死背時往上爬。提前停止很單純地說:停在 U 的底部。具體而言,每個 epoch 之後你在一個保留的驗證集上量損失,一旦那個損失不再進步,就把訓練喊停——在模型滑進過度擬合之前,把它捕捉在泛化能力最好的那一刻。這是對「分岔曲線」最直接的回應:與其去對抗那段爬升,你乾脆拒絕往上走。

兩個實務細節讓它變得穩健。第一,耐心(patience):驗證損失有雜訊,即使整體還在下降,也可能抽搐著往上跳個一兩個 epoch,所以你不會在第一個變差的 epoch 就停——你會等固定數目的 epoch(耐心值,比方說 10)都沒有進步,才喊停。第二,存檔(checkpointing):因為損失會遊走,你要的不是最後一個 epoch 的權重,而是最佳那個 epoch 的權重。所以每當驗證損失創新低,你就存一份權重快照,等訓練終於停下時,再把那份最佳快照還原回來。提前停止幾乎不花成本——反正你本來就要算驗證損失——這讓它成為一個近乎免費的正則化。

best_val = float('inf')
patience, wait = 10, 0
for epoch in range(max_epochs):
    train_one_epoch(model)
    val = validate(model)                 # loss on the held-out validation set
    if val < best_val:                    # new best -> snapshot the weights
        best_val = val
        save_checkpoint(model)            # keep the BEST, not the LAST
        wait = 0
    else:                                 # no improvement this epoch
        wait += 1
        if wait >= patience:              # stalled for `patience` epochs
            break                         # stop before the curve turns up
model = load_checkpoint()                 # restore the best-generalizing weights
耐心吸收掉有雜訊的 epoch;存檔留下最佳權重,而不是最後的權重。

你現在有了一整套工具,所以收尾的問題是:先伸手拿哪一個。一個合理的優先順序,從報酬最大到最後手段:

  1. 更多資料,做不到的話就用強力的資料擴增。真實而多樣的例子從根本攻擊變異,且不論架構都能改善每一個模型——擴增是逼近它最便宜的方式,所以排第一。
  2. 權重衰減。幾乎免費、被充分理解、對基本上每一個網路都有幫助;預設就把它打開(搭配自適應優化器時用 AdamW)。
  3. Dropout,用在合適的地方——尤其是分類頭與 Transformer。把丟棄率 p 當成變異旋鈕上的一個旋鈕來調。
  4. 提前停止放最後,當作安全網。它不會讓任何單一模型變得更好;它只是避免你訓練到超過模型最好的那一刻。

這個順序背後的道理,就是第 2 節那副偏差–變異的鏡片。資料擴增和更多資料排第一,因為它們在不抬高偏差的情況下降低變異——它們讓模型變得更好,而不只是更不過擬合。權重衰減排第二,因為它便宜又幾乎總是有幫助,雖然用過頭會增加偏差。Dropout 排第三:強大,但有調參成本,且可能與正規化起摩擦。提前停止排最後,因為它根本不會改善底層的模型——它只是在過程中挑出最佳的那個存檔。把它們組合起來,看著訓練/驗證的差距縮小,你就建出了那個「真正理解」的學生,而不是那個「死記」的學生。

整套工具只服務一個目標:把模型從過擬合的那一格,推向良好擬合的那一格。

再次出現的欠擬合/良好擬合/過擬合三聯圖,把良好擬合標示為目標。