過擬合和怎麼對付它
網路在訓練集上全對,在沒見過的資料上卻不見長進,這叫過擬合。故意製造一次過擬合,再試權重衰減、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 張,同一個數字的各種寫法根本沒見全。模型再怎麼約束,也不可能認出它從沒見過的寫法。
實際訓練模型時,對付過擬合的順序大致是:
- 先想辦法多弄資料。更多、更多樣的資料幾乎總是最有效的。
- 一定要有驗證集,並且盯著它。訓練損失降得再好看,驗證指標不漲,就沒有意義。早停幾乎不花成本,預設開著。
- 再用正則化做微調。權重衰減、dropout 的強度是要在驗證集上試出來的,沒有放之四海而皆準的數值。
- 模型不必一開始就很大。資料少的時候,一個小一點的模型往往就夠了。
大模型的訓練也是這個道理。它們的正則化手段並不複雜,真正的關鍵是海量、多樣、乾淨的訓練資料。第 09 模組訓練小 GPT 時,你會親眼看到資料太少時它背下了詩句原文,而不是學會了作詩。
練習
- 把訓練資料分別設成 200、400 張,畫一張"訓練資料量 → 測試準確率"的表。準確率是隨資料量均勻上漲的嗎?
- 把模型的隱藏層從 512 改成 32,只用 100 張圖訓練,測試準確率比 30 萬參數的大模型高還是低?
- 把 dropout 調成 0.2 和 0.8,權重衰減調成 0.01 和 5.0,在驗證集上找一個最好的組合,最後再看一次測試集。
自測
1. 怎麼從訓練過程判斷出現了過擬合?
訓練損失還在下降,驗證損失卻停止下降甚至開始上漲,兩條曲線分開了。訓練集準確率遠高於驗證集準確率,也是一個訊號。
2. 已經有了測試集,為什麼還要一個驗證集?
調參時要反覆看成績再改設定。如果看的是測試集,調了多次之後選出來的設定可能只是碰巧適合這份測試集,測試成績就不再能代表在新資料上的表現。所以調參看驗證集,測試集只在最後看一次。
3. 在本課的實驗裡,權重衰減、dropout、早停都沒有提高準確率,為什麼多給資料卻提高了很多?
正則化方法只能約束模型不要過度依賴訓練集裡的細節,但不能提供新的資訊。100 張圖裡每個數字只有約 10 張,很多寫法沒見過,模型怎麼約束都認不出來。多給資料直接讓模型見到了更多寫法,所以效果遠超其他辦法。這幾種方法在本實驗裡的作用主要是降低了測試損失,讓模型不再自信地答錯。
提問與討論
這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。
提問 +3 點,回答別人 +6 點。內容經審核後公開。
正在載入討論…