过拟合和怎么对付它
网络在训练集上全对,在没见过的数据上却不见长进,这叫过拟合。故意制造一次过拟合,再试权重衰减、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 积分。内容经审核后公开。
正在加载讨论…