3、正则化与防止过拟合:L1/L2正则化原理、Dropout机制详解、早停法(Early Stopping)实践、数据增强技巧
做模型训练,最怕什么?
不是loss降不下去,而是loss降得太漂亮了——训练集上准确率99%,一上验证集直接崩到60%。
这就是过拟合。说白了,模型把训练数据里的噪声和细节都背下来了,反而没学到真正的规律。我刚开始做AI那会儿,就吃过这个亏。一个图像分类模型,训练了三天三夜,loss曲线漂亮得像教科书一样,结果上线第一天就被用户骂惨了。
从那以后,我养成了一个习惯:每次训练模型,第一件事不是调学习率,而是先把正则化手段安排上。
这一章,我就把四种最常用的防过拟合手段掰开揉碎了讲给你听。
核心观点:过拟合的本质是模型复杂度超过了数据的信息量。正则化的目标,就是在模型能力和泛化能力之间找到那个甜点。
3.1 L1/L2正则化:给权重戴上镣铐
正则化的核心思想很简单——在损失函数里加一项惩罚项,让模型不敢把权重学得太大。
你想想看,如果某个特征的权重特别大,那这个特征稍微变一点,预测结果就剧烈抖动。这种模型肯定不稳定。正则化就是告诉模型:「权重别太嚣张,收敛点。」
L2正则化(权重衰减)
L2正则化,也叫权重衰减。它的惩罚项是权重的平方和。
# L2正则化的损失函数
loss = 原始损失 + λ * Σ(w_i²)
# 梯度更新时,相当于每次多减一点权重
w = w - η * (∂loss/∂w + 2λ * w)
λ是正则化强度,我一般从1e-4开始试。调大λ,权重会更小,模型更平滑;调小了,正则化效果就不明显。
我的经验:L2正则化对大多数模型都有效,尤其是全连接层。我在做推荐系统时,把L2系数从1e-5调到1e-4,AUC直接涨了0.3个点。别小看这0.3,在工业界这就是一个版本迭代的收益。
L1正则化
L1正则化用的是权重的绝对值之和。它有个神奇的特性——会让部分权重变成0。
# L1正则化的损失函数
loss = 原始损失 + λ * Σ|w_i|
为什么会这样?因为L1的梯度是常数,不像L2那样越靠近0梯度越小。权重被推到0附近时,L1会一脚把它踹到0。说白了,L1在做特征选择。
选L1还是L2?
- 如果你怀疑很多特征是噪声,用L1——它帮你把没用的特征权重直接干掉
- 如果你觉得所有特征都有点用,只是别太极端,用L2
- 两者可以结合,叫Elastic Net,我偶尔会用
| 对比项 | L1正则化 | L2正则化 |
|---|---|---|
| 惩罚形式 | 权重绝对值之和 | 权重平方和 |
| 效果 | 产生稀疏权重(部分为0) | 权重整体变小,但不为0 |
| 适用场景 | 特征维度高,需要特征选择 | 大多数通用场景 |
| 梯度特性 | 常数梯度 | 梯度随权重减小而减小 |
3.2 Dropout机制详解:让神经元学会独立
Dropout这个想法,我第一次看到时觉得挺暴力的——训练时随机干掉一部分神经元,让它们不工作。
但仔细一想,这招其实很聪明。你想想看,如果每个神经元都知道队友随时可能「掉线」,它就不敢过度依赖别人,必须自己学会提取有效特征。
import torch
import torch.nn as nn
class MyModel(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.dropout1 = nn.Dropout(p=0.5) # 50%的神经元被随机丢弃
self.fc2 = nn.Linear(256, 128)
self.dropout2 = nn.Dropout(p=0.3)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.dropout1(x) # 训练时随机丢弃
x = torch.relu(self.fc2(x))
x = self.dropout2(x)
return self.fc3(x)
注意:Dropout只在训练时生效。推理时,所有神经元都参与计算,但权重要乘以(1-p)做缩放。PyTorch和TensorFlow会自动处理这个,但如果你手写实现,别忘了这步。
Dropout的p值怎么设?我个人的习惯是:
- 全连接层:p=0.5,效果比较稳
- 卷积层:p=0.1~0.3,因为卷积层参数少,丢太多反而学不到东西
- RNN/LSTM:p=0.2~0.3,而且一般只用在非循环连接上
我曾经在一个NLP项目里,把Dropout从0.3调到0.5,结果验证集准确率掉了2个点。后来才发现,对于小数据集,Dropout太强反而让模型欠拟合了。嗯,这里要注意——正则化不是越强越好。
3.3 早停法(Early Stopping)实践
早停法,说白了就是「见好就收」。
训练过程中,验证集的loss通常会先下降,然后触底,最后反弹。那个反弹的点,就是模型开始过拟合的时刻。早停法就是在这个时刻之前停下来。
class EarlyStopping:
def __init__(self, patience=5, min_delta=0.001):
self.patience = patience # 容忍多少次没有改善
self.min_delta = min_delta # 最小改善阈值
self.best_loss = float('inf')
self.counter = 0
self.early_stop = False
def __call__(self, val_loss):
if val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss
self.counter = 0
else:
self.counter += 1
if self.counter >= self.patience:
self.early_stop = True
我的建议:patience一般设5~10。设太小了,模型还没收敛就停了;设太大了,过拟合已经发生了才停。我通常先跑一轮完整的训练,看看验证集loss大概在第几个epoch触底,然后patience设成那个epoch数的1/3左右。
早停法还有一个好处——省时间。我做过一个对比实验,同样的模型,用早停法比固定epoch数训练,平均节省了40%的时间。在调参阶段,这个效率提升非常可观。
3.4 数据增强技巧
数据增强,是防止过拟合最「治本」的方法。因为过拟合的本质是数据不够,那我们就想办法「变」出更多数据。
对于图像任务,常用的增强手段包括:
- 几何变换:随机旋转、平移、缩放、翻转
- 颜色变换:调整亮度、对比度、饱和度、色相
- 噪声注入:高斯噪声、椒盐噪声
- 裁剪:Random Crop + Resize
from torchvision import transforms
# 一个典型的数据增强pipeline
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224), # 随机裁剪并缩放
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.ColorJitter( # 颜色抖动
brightness=0.2,
contrast=0.2,
saturation=0.2,
hue=0.1
),
transforms.RandomRotation(degrees=15), # 随机旋转
transforms.ToTensor(),
transforms.Normalize( # 标准化
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
对于文本任务,数据增强稍微麻烦点:
- 同义词替换:把句子中的词替换成同义词
- 回译:中文→英文→中文,句子结构会变但语义不变
- 随机插入/删除:小概率地插入或删除一些词
一个重要的原则:数据增强不能改变数据的标签。比如把「6」旋转180度就变成了「9」,这种增强会引入错误。我见过有人把医学影像左右翻转,结果心脏跑到右边去了——这显然不对。
数据增强的强度怎么控制?我一般这样试:先不加增强,看模型是否过拟合。如果过拟合了,逐步增加增强强度,直到验证集loss不再下降为止。记住,增强太强会让模型学不到有效信息,反而降低性能。
好了,这四种手段讲完了。它们可以单独用,也可以组合用。我个人最常用的组合是:L2正则化 + Dropout + 早停法,再配合适度的数据增强。这个组合在大多数场景下都能把过拟合控制得很好。
下一章,我们会聊优化器的选择——Adam、SGD、RMSprop这些到底该怎么选?到时候见。