正则化
上一章我们学会了给模型"打分",也发现了最头疼的问题:模型在训练数据上表现神勇,一到新数据就原形毕露——这叫过拟合。这一章要讲对付它的正则化(regularization):L2 权重衰减、L1 稀疏、Dropout、早停、数据增强……像是对抗过拟合的"十八般武艺"。学完这章,你会知道每件兵器是干什么的、为什么管用、怎么组合使用。
过拟合:把噪声当成了规律
先回顾一下"敌人"长什么样。想象一堆散点数据,真实规律是一条平滑曲线,但数据里混着噪声。容量很大的模型为了把训练损失降到最低,会拼命让曲线"穿过"每一个点,把噪声的抖动也记了下来。这就是过拟合:把噪声当规律,背答案而不是学方法。
过拟合的典型症状是"训练好、测试差":训练损失一路走低,测试损失却居高不下,两条曲线之间出现越来越宽的泛化差距。正则化就是一系列缩小这条差距的方法——严格说是给损失函数加一个额外项去"偏好"某些参数;实际使用中,任何能提升泛化能力的招数都算正则化。
L2 权重衰减:把参数往零拉一点
最常用的正则化是 L2 正则化,也叫权重衰减(weight decay)。想法很朴素:过于复杂的模型往往带着很大的参数,那就给大参数"加税"。原来的损失是每个样本误差的累加,现在再加上一项"罚金"——所有参数的平方和:
$$\hat{\phi} = \arg\min_{\phi} \left[ \sum_{i=1}^{I} \ell_i(x_i, y_i) + \lambda \sum_{j} \phi_j^2 \right]$$这个式子意思是:还是在找让损失最小的参数 $\phi$,只是损失里多了一笔罚金 $\lambda \sum_j \phi_j^2$。$\lambda$ 叫正则化系数,决定罚金多重:$\lambda = 0$ 等于没加;$\lambda$ 越大,参数被逼得越靠近零。这项惩罚通常只加在权重上、不加在偏置上,所以叫"权重衰减"。
把每个参数想象成绑着橡皮筋的小球,橡皮筋另一头固定在零点。参数越往外跑,往回拉的力越大。L2 正则化就是这根橡皮筋:参数不会完全归零,但会被限制在"温和"的范围里。
为什么参数小就等于模型简单?网络的输出本质上是一层层"加权求和"——权重小,输出随输入变化的幅度就小,曲线更平滑,不容易为了迁就噪声点而急剧拐弯。极端情况下权重全变零,网络就退化成输出一个常数,这是"最简单"的模型。所以 L2 是在"精确拟合数据"和"保持平滑"之间找折中。
从梯度下降看更直观:加了 L2 后,每次更新参数都会多"往回拉"一点。这个操作在 PyTorch 里只需给优化器加一个参数,后面代码会看到。
L1 正则化:让参数变稀疏
L2 用"平方和"当罚金,L1 正则化改用"绝对值之和":
$$\hat{\phi} = \arg\min_{\phi} \left[ \sum_{i=1}^{I} \ell_i(x_i, y_i) + \lambda \sum_{j} |\phi_j| \right]$$差别只是把 $\phi_j^2$ 换成 $|\phi_j|$,效果却大不一样:L2 把参数往零拉,但很少真正拉到零;L1 却能让很多参数"恰好"等于零。这种性质叫稀疏性(sparsity),等于模型自动做了一次"特征选择"——没用的输入对应的权重被清零,模型更轻、更好解释。
为什么 L1 更容易得到零?看罚金的"推力":L2 罚金 $\phi^2$ 的斜率是 $2\lambda\phi$,参数越接近零推力越小,最后停在很小的非零值;L1 罚金 $|\phi|$ 的斜率是常数 $\lambda$,推它归零的力不减弱,于是干脆推到位。L1 也叫 LASSO;L1 和 L2 一起用叫弹性网(elastic net)。
Dropout:训练时随机"关掉"一些神经元
权重衰减从"参数大小"入手,Dropout 则从"结构"入手。做法很直接:每次训练迭代,随机把一部分隐藏单元(通常一半)的输出临时置为零,相当于这些神经元"请假"了,下次迭代再换一批请假。
为什么能防过拟合?如果模型老靠几个固定神经元"抱团"工作,它们会互相依赖、形成只有凑在一起才成立的"暗号"——训练集上很有效,换到新数据就失灵。Dropout 逼着网络学会:任何一个神经元都可能随时缺席,所以每个神经元都得独立学到有用的特征,不能过度依赖别人。
Dropout 像球队训练时随机让几名队员下场,逼所有人都练好基本功、不能只靠某个球星。正式比赛全员上场,配合反而更稳——就像测试时所有神经元恢复工作,网络整体更可靠。
在 PyTorch 里,加 Dropout 层和加权重衰减都只需一两行代码:
import torch
import torch.nn as nn
# 一个带 Dropout 的小网络:28x28 手写数字 -> 10 类
model = nn.Sequential(
nn.Linear(28 * 28, 128),
nn.ReLU(),
nn.Dropout(p=0.5), # 训练时随机关掉一半隐藏单元
nn.Linear(128, 10),
)
# weight_decay 就是 L2 正则化的强度 lambda
optimizer = torch.optim.SGD(
model.parameters(), lr=0.1, weight_decay=1e-4)
for images, labels in dataloader:
model.train() # 训练模式:Dropout 生效
out = model(images)
loss = nn.functional.cross_entropy(out, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step() # 更新时参数被"往零拉"了一点
评估时记得调用 model.eval():它会自动关闭 Dropout 并做权重缩放,保证输出量级和训练时一致。
早停与数据增强:另两条捷径
早停(early stopping)一句话:别等训练完全收敛,看情况提前刹车。训练中每隔一段时间在验证集上测一次损失并保存模型;当训练损失还在降、验证损失却开始回升时,说明模型开始背噪声了——就停在这里,用之前保存的"最好版本"。这个时机往往就是炖汤火候刚好的那一刻:再煮就糊了。
早停为什么管用?参数的初始值都很小,训练越久参数才长得越大;早停等于把参数的"生长时间"掐短,效果上有点像 L2 正则化——都在阻止权重变得过大。
数据增强(data augmentation)换个思路:与其让模型少犯错,不如多给它一些"题"。对图像,把样本随机翻转、旋转、裁剪、调色,标签依然是"鸟";对文本,可替换同义词或把句子翻译成别的语言再翻回来。模型见惯了这些"变形金刚"版本,就会对与任务无关的变化不敏感,泛化能力更强——等于免费扩大训练集。