模組 08 · 第 6 課

過擬合和怎麼對付它

網路在訓練集上全對,在沒見過的資料上卻不見長進,這叫過擬合。故意製造一次過擬合,再試權重衰減、dropout、早停和增加資料這幾種辦法,看真實的結果裡哪一個最管用。

  • 約 40 分鐘
  • 難度:進階
  • 實測:2026-09-14 torch 2.14,scikit-learn 1.9,CPU,固定隨機種子

程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。

上一課的網路在訓練集上是 100%,在測試集上是 97.8%。這 2.2 個百分點的差距,就是網路"背下來"而沒有真正學會的那部分。

差距小的時候無關緊要。可如果訓練資料很少、模型很大,網路就會把訓練集整個背下來:訓練集上全對,換一批新資料就原形畢露。這叫過擬合(overfitting)。這一課故意製造一次過擬合,再試幾種常見的對付辦法。

python overfitting.py

三份資料

這一課把資料分成三份:

X_pool, X_rest, y_pool, y_rest = train_test_split(X, y, test_size=0.5, random_state=0)
X_val, X_test, y_val, y_test = train_test_split(X_rest, y_rest, test_size=0.5, random_state=0)
X_small, y_small = X_pool[:100], y_pool[:100]  # 故意只用 100 张
可用于训练的图 898 张(先只用其中 100 张),验证集 449 张,测试集 450 张
模型共 301066 个参数
  • 訓練集:用來更新參數。
  • 驗證集:訓練過程中用來觀察、調整設定(學習率多大、什麼時候停),但不用來更新參數。
  • 測試集:所有設定都定下來之後,最後看一次,作為最終成績。

為什麼要多一個驗證集?因為你每看一次測試集的成績,然後據此調整設定,測試集的資訊就漏了一點進你的決定裡。調了幾十次之後,你挑出來的設定可能只是碰巧適合這份測試集。所以調參看驗證集,測試集留到最後。

模型故意做得很大:兩個 512 個神經元的隱藏層,30 萬個參數,卻只給它 100 張圖。每張圖攤到三千個參數,背下來輕而易舉。

過擬合長什麼樣

== 什么都不加
  第   1 轮  训练损失 2.2147 准确率 26.0%  |  验证损失 2.2507 准确率 16.5%
  第  10 轮  训练损失 1.3077 准确率 79.0%  |  验证损失 1.5603 准确率 62.6%
  第  25 轮  训练损失 0.1552 准确率 98.0%  |  验证损失 0.4420 准确率 87.3%
  第  50 轮  训练损失 0.0058 准确率 100.0%  |  验证损失 0.3772 准确率 88.4%
  第 100 轮  训练损失 0.0013 准确率 100.0%  |  验证损失 0.4038 准确率 89.8%
  第 200 轮  训练损失 0.0004 准确率 100.0%  |  验证损失 0.4371 准确率 90.0%
  第 300 轮  训练损失 0.0002 准确率 100.0%  |  验证损失 0.4667 准确率 89.8%
  最终:测试集准确率 88.7%,测试损失 0.5089

(第一輪的損失 2.21,接近上一課說的 2.30:一開始網路什麼都不懂。)

第 50 輪,訓練集已經 100% 正確,訓練損失 0.006,接下來還一路降到 0.0002。可驗證集的準確率停在 90% 上下,驗證損失從第 50 輪的 0.377 開始往上漲,到第 300 輪是 0.467。

兩條曲線分開了:訓練的損失還在降,驗證的損失卻在漲。這就是過擬合的標誌。驗證損失上漲,是因為網路對訓練集越來越"自信",在沒見過的圖上答錯時也錯得很自信,交叉熵對這種錯誤罰得很重。

畫成圖,大概是這樣:

损失
 │ \
 │  \  验证损失
 │   \____/ ̄ ̄ ̄ ̄
 │    \
 │     \_  训练损失
 │         ̄ ̄______
 └──────────────────────── 轮
          ↑
    验证损失最低的地方

試幾種辦法

下面幾種都是常用的對付過擬合的辦法。每種只改一處,其餘完全一樣(同樣的隨機種子、同樣的 100 張圖)。

權重衰減(weight decay):每一步更新時,把所有權重都往 0 拉一點點。它的想法是,參數值小的模型更"簡單",不容易對個別樣本反應過度。AdamW 最佳化器的 weight_decay 參數控制拉的力度:

optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=weight_decay)

dropout:訓練時,每一步隨機讓一部分神經元"休息"(輸出置為 0)。這裡設成 0.5,每一步都有一半的隱藏神經元不工作。網路就沒法依賴某幾個特定的神經元去記住某張圖,只能學更穩健的規律。評估時要用 model.eval() 把 dropout 關掉,訓練時用 model.train() 開啟:

nn.Linear(64, 512), nn.ReLU(), nn.Dropout(dropout),

早停(early stopping):每一輪都看驗證損失,記下最低的那一刻的參數。驗證損失連續 20 輪沒有創新低,就停下來,回到最低的那一刻:

if val_loss < best_val:
    best_val, best_epoch, patience = val_loss, epoch, 0
    best_state = {k: v.clone() for k, v in model.state_dict().items()}
else:
    patience += 1
if early_stop and patience >= 20:  # 验证损失连续 20 轮没有创新低,就停下来
    break

還有一個最樸素的辦法:多給資料。把可用於訓練的 898 張圖全部用上。

結果:不太一樣的結論

== 权重衰减 weight_decay=0.1
  第  50 轮  训练损失 0.0059 准确率 100.0%  |  验证损失 0.3762 准确率 88.4%
  第 300 轮  训练损失 0.0002 准确率 100.0%  |  验证损失 0.4543 准确率 89.8%
  最终:测试集准确率 88.0%,测试损失 0.5008

== 权重衰减 weight_decay=1.0
  第  50 轮  训练损失 0.0079 准确率 100.0%  |  验证损失 0.3674 准确率 89.1%
  第 300 轮  训练损失 0.0012 准确率 100.0%  |  验证损失 0.4243 准确率 89.3%
  最终:测试集准确率 88.0%,测试损失 0.4574

== dropout 0.5
  第  50 轮  训练损失 0.0561 准确率 100.0%  |  验证损失 0.3784 准确率 88.4%
  第 300 轮  训练损失 0.0003 准确率 100.0%  |  验证损失 0.3879 准确率 90.2%
  最终:测试集准确率 88.2%,测试损失 0.4321

== 早停
  第 55 轮停止:验证损失从第 35 轮之后就没再下降
  最终:测试集准确率 87.3%,测试损失 0.4053

== 什么都不加,但用 898 张图训练
  第   1 轮  训练损失 2.2261 准确率 34.6%  |  验证损失 2.2390 准确率 27.4%
  第  10 轮  训练损失 1.3170 准确率 88.1%  |  验证损失 1.3714 准确率 84.4%
  第  25 轮  训练损失 0.2208 准确率 94.4%  |  验证损失 0.2675 准确率 91.1%
  第  50 轮  训练损失 0.0437 准确率 99.3%  |  验证损失 0.1072 准确率 96.9%
  第 100 轮  训练损失 0.0074 准确率 100.0%  |  验证损失 0.0854 准确率 97.1%
  第 200 轮  训练损失 0.0016 准确率 100.0%  |  验证损失 0.0850 准确率 97.1%
  第 300 轮  训练损失 0.0007 准确率 100.0%  |  验证损失 0.0875 准确率 97.3%
  最终:测试集准确率 96.2%,测试损失 0.1682

测试集汇总:
  什么都不加            准确率 88.7%  损失 0.5089
  权重衰减 0.1         准确率 88.0%  损失 0.5008
  权重衰减 1.0         准确率 88.0%  损失 0.4574
  dropout 0.5      准确率 88.2%  损失 0.4321
  早停               准确率 87.3%  损失 0.4053
  898 张训练数据        准确率 96.2%  损失 0.1682

這組結果和很多教程裡的說法不太一樣,值得仔細看。

權重衰減、dropout、早停,都沒有提高準確率。 測試集準確率全部在 87% 到 89% 之間,和什麼都不加的 88.7% 差不多,早停甚至還低了一點。450 張測試圖,1 個百分點只是四五張圖,這點差別說明不了誰好誰壞。

它們降低了測試損失。 從 0.51 降到了 0.40 到 0.50,早停最明顯。也就是說,網路答對的題還是那些,但它不再那麼"自信地答錯"了,給出的機率更靠譜。在有些場合這很重要,比如你要根據機率決定要不要把結果交給人複核。

多給資料,效果遠遠超過其他所有辦法。 同一個模型,同樣什麼都不加,訓練資料從 100 張換成 898 張,測試準確率從 88.7% 跳到 96.2%,測試損失從 0.51 降到 0.17。驗證損失也不再明顯上漲,在 0.085 附近就穩住了。

該怎麼理解

這幾種正則化方法(權重衰減、dropout、早停統稱正則化)不是沒用,而是有它們的適用範圍。它們能緩解過擬合,但變不出資料裡沒有的資訊。100 張圖,每個數字平均才 10 張,同一個數字的各種寫法根本沒見全。模型再怎麼約束,也不可能認出它從沒見過的寫法。

實際訓練模型時,對付過擬合的順序大致是:

  1. 先想辦法多弄資料。更多、更多樣的資料幾乎總是最有效的。
  2. 一定要有驗證集,並且盯著它。訓練損失降得再好看,驗證指標不漲,就沒有意義。早停幾乎不花成本,預設開著。
  3. 再用正則化做微調。權重衰減、dropout 的強度是要在驗證集上試出來的,沒有放之四海而皆準的數值。
  4. 模型不必一開始就很大。資料少的時候,一個小一點的模型往往就夠了。

大模型的訓練也是這個道理。它們的正則化手段並不複雜,真正的關鍵是海量、多樣、乾淨的訓練資料。第 09 模組訓練小 GPT 時,你會親眼看到資料太少時它背下了詩句原文,而不是學會了作詩。

練習

  1. 把訓練資料分別設成 200、400 張,畫一張"訓練資料量 → 測試準確率"的表。準確率是隨資料量均勻上漲的嗎?
  2. 把模型的隱藏層從 512 改成 32,只用 100 張圖訓練,測試準確率比 30 萬參數的大模型高還是低?
  3. 把 dropout 調成 0.2 和 0.8,權重衰減調成 0.01 和 5.0,在驗證集上找一個最好的組合,最後再看一次測試集。

自測

1. 怎麼從訓練過程判斷出現了過擬合?

訓練損失還在下降,驗證損失卻停止下降甚至開始上漲,兩條曲線分開了。訓練集準確率遠高於驗證集準確率,也是一個訊號。

2. 已經有了測試集,為什麼還要一個驗證集?

調參時要反覆看成績再改設定。如果看的是測試集,調了多次之後選出來的設定可能只是碰巧適合這份測試集,測試成績就不再能代表在新資料上的表現。所以調參看驗證集,測試集只在最後看一次。

3. 在本課的實驗裡,權重衰減、dropout、早停都沒有提高準確率,為什麼多給資料卻提高了很多?

正則化方法只能約束模型不要過度依賴訓練集裡的細節,但不能提供新的資訊。100 張圖裡每個數字只有約 10 張,很多寫法沒見過,模型怎麼約束都認不出來。多給資料直接讓模型見到了更多寫法,所以效果遠超其他辦法。這幾種方法在本實驗裡的作用主要是降低了測試損失,讓模型不再自信地答錯。

提問與討論

這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。

提問 +3 點,回答別人 +6 點。內容經審核後公開。

正在載入討論…