模块 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 积分。内容经审核后公开。

正在加载讨论…