正则化与防过拟合
本教程共 60 篇 · 第 27 篇 · 更新于 2026-08-17 · 约 4 分钟阅读
本节目标:看懂过拟合的典型症状,学会三件防过拟合的武器:Dropout、权重衰减、数据增强,并知道什么时候该用哪个。
过拟合:背答案的学生
训练集准确率 99%,测试集只有 70%。这个现象你迟早会撞上,它叫过拟合(Overfitting)。
打个比方:学生把练习册答案全背下来了,平时作业满分,一换新题就抓瞎。模型也一样——参数太多、数据太少时,它不去学规律,反而把训练样本的噪声都记了下来。
过拟合通常是三个因素凑出来的:模型容量太大,扛得住复杂细节;数据量太少,撑不起模型的野心;训练时间太长,把噪声也学了个遍。
举个极端例子:把 100 张图硬塞给一个能记住 1000 万参数的模型,它完全可以把每张图「背」下来。但换个只有 1 万参数的小模型,想背都背不动,只能老老实实学规律。判断标准很简单:训练准确率高、验证准确率明显低,中间的大缺口就是过拟合。反过来两边都低,那是欠拟合,该加大模型而不是加正则。
所有防过拟合的手段,本质就一句话:给模型的学习能力上点约束。下面三件武器各有各的招。
武器一:Dropout
Dropout 的做法很粗暴:训练时每轮随机把一部分神经元的输出置零,强迫网络不能依赖任何单个神经元。
类比一下:团队每次开会随机抽掉几个人,剩下的人就得把活都学会,谁都不敢划水。训练出来的网络冗余度高,换新数据也不慌。从另一个角度看,Dropout 每次都在训练一个「缺胳膊少腿」的子网络,推理时所有子网络一起投票,效果接近集成学习。
import torch.nn as nn
model = nn.Sequential(
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(p=0.5), # 训练时随机丢 50% 的神经元输出
nn.Linear(128, 10),
)
p 是置零概率。关键细节在训练和推理的差异上:
dropout = nn.Dropout(p=0.5)
x = torch.ones(2, 10)
dropout.train()
print(dropout(x)) # 大约一半是 0,另一半是 2.0(放大 1/(1-p) 倍)
dropout.eval()
print(dropout(x)) # 全 1.0,Dropout 自动关闭
训练时活下来的神经元输出放大 1/(1-p) 倍,是为了保持整体量级不变;推理时 Dropout 关闭、全部神经元参与,所以验证前一定记得 model.eval()。这也是同一份代码 eval 前后输出不同的原因。
Dropout 放哪层也有讲究:通常放在激活函数之后、下一层之前,输出层前一般不加。卷积层则要慎用——相邻像素高度相关,逐点丢弃效果差,要用也是 nn.Dropout2d 按整个通道丢。
Tip
p的参考值:大的全连接层用 0.5,中小网络 0.2~0.3,Transformer 通常 0.1,卷积层一般不用。感觉过拟合严重就调大,欠拟合就调小。
武器二:权重衰减
权重衰减(Weight Decay)的思路:给损失函数加一项「权重越大罚得越狠」的约束,也就是 L2 正则化。模型为了少交罚款,会主动把权重往小了压。
权重小有什么好处?输出对输入的微小变化不那么敏感,拟合的曲线更平滑,自然不容易过拟合。它和 Dropout 一唱一和:一个压参数的个头,一个断神经元的连接,从两个方向给模型「减负」。
PyTorch 里用起来只需一行:
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
常用取值 1e-4 到 1e-2,从小的开始试。weight_decay 越大压得越狠,但过大了模型会变「呆」,连训练集都学不动,所以要一点点往上加。
Warning这里有个著名的坑:标准 Adam 把 weight_decay 直接加进梯度(等价于 L2 正则),但在 Adam 的自适应学习率缩放下,L2 正则的效果会被弱化。
torch.optim.AdamW把权重衰减从梯度里解耦出来,直接在参数更新时衰减,这才是论文里推荐的做法。现在 AdamW 是业界默认选择。
武器三:数据增强
前两件武器都在模型上做文章,第三件换个角度:数据太少,那就「伪造」数据。
训练时对每张图随机裁剪、翻转、调色,模型每次看到的样本都略有不同。数据量名义上没变,多样性却翻了倍,模型被迫去学不变的本质特征,而不是死记像素。
from torchvision import transforms
train_tf = transforms.Compose([
transforms.RandomResizedCrop(224), # 随机裁剪缩放
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.ColorJitter(brightness=0.2), # 随机调亮度
transforms.ToTensor(),
])
注意两点:增强只加在训练集上,验证集保持原样;增强手段要符合任务常识,比如识别数字 6 和 9 的任务就别随机旋转。第 12 章讲过 transforms 的细节,这里不展开。数据增强常和 Dropout、权重衰减一起用,效果叠加。
其他防过拟合手段
工具箱里还有几样:
- 早停(Early Stopping):每个 epoch 看验证集指标,连续几个 epoch 不涨就停。它还有个附带好处:省时间。模型从「刚好」到「过拟合」之间通常还有一段缓慢下降期,早停把这段也省了。实现也不难:
if val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), "best.pth") # 顺手存最好的
elif no_improve_epochs >= 5:
break # 5 轮没进步,收工
- BatchNorm:本身带一点正则化效果。用了它之后,对 Dropout 的依赖会减轻(下一章细讲)。
- 标签平滑(Label Smoothing):别让模型对标签 100% 自信,
nn.CrossEntropyLoss(label_smoothing=0.1)一行搞定。它对错误分类留有余地,损失曲线也更平滑。 - 剪枝(Pruning):训练完把不重要的权重删掉。本是压缩模型的手段,顺带改善泛化,
torch.nn.utils.prune里有现成工具,当进阶方向了解即可。
实战思路
我的习惯是:先不加正则,把模型跑通;看到过拟合迹象,再逐个上手段。
- 数据增强和早停成本最低,先上
- 还压不住,加 Dropout 和权重衰减
- 都不行,回头检查数据质量和模型复杂度
另外提一句,正则化和数据质量是两码事。标签标错了、类别不均衡,也会让验证集难看,别全甩锅给过拟合。
记住一句话:正则化是调料,不是主菜。模型结构和数据本身不对,加再多调料也救不回来。
下一章聊聊让训练变稳的另一类层:归一化层。