AI正则化踩坑记录
这篇是AI正则化的实操备忘,偏决策和限制条件。
什么是过拟合
先说人话版的定义:过拟合就是模型把训练集里的噪点也当成了规律记下来。就像学生背题,把正确答案的位置、甚至题目里的错别字都背了,换个数字就不会做了。
判断过拟合最直接的方法是看训练集和验证集的指标曲线:
左边的曲线一直在涨,这是训练集。右边的曲线涨到某个点就开始往下掉,这是验证集——说明模型开始「背题」了。
L1 和 L2 正则化
L1/L2 是最经典的正则化手段,原理是在损失函数里加一项权重惩罚。但实际用的时候,这两个的选择和调参有很多坑。
L2 正则化的实战效果
先说结论:L2 正则化(权重衰减)是默认首选,稳定性好,调参相对宽容。
PyTorch 里最简单的用法:
# 方式 1:在优化器里设置 weight_decay
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.001,
weight_decay=1e-4 # L2 正则化系数
)
# 方式 2:手动加 L2 损失项
l2_lambda = 1e-4
l2_norm = sum(p.pow(2).sum() for p in model.parameters())
loss = criterion(output, target) + l2_lambda * l2_norm
第一个方式更常见,因为优化器会自动处理梯度更新。但有个坑:不同优化器对 weight_decay 的实现不一样。
踩坑记录:Adam + L2 正则化在某些场景下效果不如 SGD。原因在于 Adam 的自适应学习率和 L2 的耦合,会导致某些权重更新变慢。如果你用 Adam 时发现 L2 正则化效果不明显,可以试试把 weight_decay 改用 AdamW 优化器:
# AdamW 对 L2 的处理更干净
optimizer = torch.optim.AdamW(
model.parameters(),
lr=0.001,
weight_decay=1e-4
)
L1 正则化的适用场景
L1 正则化的特点是会「稀疏化」权重,把不重要的特征权重直接压到 0。听起来很美好,但实际场景里用得不多。
什么时候用 L1:特征维度特别高、且有明确「特征选择」需求时。比如文本分类里词表有几万维,你希望模型自动忽略大部分词。
l1_lambda = 1e-5
l1_norm = sum(p.abs().sum() for p in model.parameters())
loss = criterion(output, target) + l1_lambda * l1_norm
踩坑记录:L1 正则化容易把学习率搞得太小。因为权重被不断压缩,梯度也会变小。用 L1 时通常要把学习率调大一些,或者用学习率调度器。
调参经验
L1/L2 的正则化系数通常从 1e-4 开始尝试,按 10 倍调:
1e-3:强正则化,适合小数据集、高模型容量1e-4:中等强度,常用默认值1e-5:弱正则化,大数据集时用1e-6:几乎没效果,一般不用
我之前的项目里,用 weight_decay=1e-4 把验证集准确率从 73% 提升到了 78%,算是立竿见影。
Dropout
Dropout 是 Bengio 团队 2014 年提出的方法,核心思想是「训练时随机丢弃部分神经元」。听起来很粗暴,但效果出奇地好。
基础用法
PyTorch 里加 Dropout 很简单:
import torch.nn as nn
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 512)
self.dropout1 = nn.Dropout(p=0.5) # 50% 概率丢弃
self.fc2 = nn.Linear(512, 256)
self.dropout2 = nn.Dropout(p=0.3)
self.fc3 = nn.Linear(256, 10)
def forward(self, x):
x = F.relu(self.fc1(x))
x = self.dropout1(x)
x = F.relu(self.fc2(x))
x = self.dropout2(x)
return self.fc3(x)
重要提醒:Dropout 只在训练时生效,预测时会自动关闭。这是通过 model.eval() 和 model.train() 自动控制的,但手动实现时容易忘。
Dropout 率的选择
常见的 Dropout 率范围:
0.2 - 0.3:轻量模型,如小型 CNN0.5:默认值,适用于大多数全连接层0.7 - 0.8:极强正则化,一般不推荐
踩坑记录:Dropout 加在卷积层里效果不好。卷积层的参数共享和局部连接已经自带了某种「正则化」,再加 Dropout 反而会破坏空间结构。
实践建议:
- 全连接层:
Dropout(0.5) - 卷积层:不用 Dropout,或者用很低的
Dropout(0.1)
Dropout 的变体
PyTorch 提供了几种 Dropout 变体:
# 标准 Dropout,按概率丢弃
nn.Dropout(p=0.5)
# Dropout2d,适用于通道维度(常用于 CNN)
nn.Dropout2d(p=0.3)
# AlphaDropout,适用于 SELU 激活函数
nn.AlphaDropout(p=0.5)
实际经验:除非你在用 SELU 激活函数,否则 Dropout 和 Dropout2d 就够用了。其他变体大部分时候是锦上添花,不是雪中送炭。
Batch Normalization
BatchNorm 是 2015 年提出来加速训练的,但实际用下来,它也有正则化效果。原理是给每一层的输出做标准化,减去均值、除以方差。
基础用法
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=3)
self.bn1 = nn.BatchNorm2d(64)
self.conv2 = nn.Conv2d(64, 128, kernel_size=3)
self.bn2 = nn.BatchNorm2d(128)
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = F.max_pool2d(x, 2)
x = F.relu(self.bn2(self.conv2(x)))
return x
BatchNorm 的位置有讲究:通常放在激活函数之前。但也有实验表明放在激活函数之后效果更好,这个需要按具体任务尝试。
踩坑记录:BatchNorm 的小批量问题
BatchNorm 的「Batch」指的是批次大小。如果你的 batch size 太小(比如 2、4),BatchNorm 的统计量会不稳定,导致训练抖动。
解决方案:
- 把 batch size 调大(如果显存够)
- 用
GroupNorm替代BatchNorm(不依赖 batch 统计) - 用
LayerNorm(常见于 NLP 任务)
# GroupNorm 不依赖 batch 统计,适合小 batch
self.norm = nn.GroupNorm(num_groups=32, num_channels=64)
BatchNorm 的正则化效果
BatchNorm 的正则化效果不是主要目标,但它确实能抑制过拟合。原因有两个:
- 每一批的数据都引入了噪声(因为均值和方差是随机的)
- 减少了对初始化的依赖,让训练更稳定
我之前的实验里,加了 BatchNorm 后验证集准确率提升了 1-2%,不如 Dropout 明显,但胜在稳定。
其他正则化手段
Early Stopping
早停是最简单的正则化手段:监控验证集指标,如果连续几个 epoch 不提升就停止训练。
best_val_acc = 0.0
patience = 5 # 容忍 5 个 epoch 不提升
no_improve_count = 0
for epoch in range(100):
train_loss = train_one_epoch()
val_acc = validate()
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
no_improve_count = 0
else:
no_improve_count += 1
if no_improve_count >= patience:
print(f'Early stopping at epoch {epoch}')
break
踩坑记录:早停的 patience 不能设太小,否则模型还没充分学习就被停了。我一般用 5-10,根据训练速度调整。
数据增强
虽然数据增强不算「模型正则化」,但效果比大多数正则化手段都好。
# PyTorch 的常用数据增强
transform = transforms.Compose([
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomRotation(15), # 随机旋转 15 度
transforms.ColorJitter(brightness=0.2, contrast=0.2), # 颜色抖动
transforms.ToTensor(),
])
实战经验:数据增强 + 适度的 L2 正则化,通常能达到 80% 的正则化效果。剩下的再用 Dropout 和 BatchNorm 补。
组合使用与调参策略
常见组合
根据任务类型,正则化的组合大致有这些套路:
| 任务类型 | 推荐组合 |
|---|---|
| 图像分类(CNN) | L2 + Dropout(全连接层) + 数据增强 |
| 文本分类(RNN/Transformer) | L2 + Dropout(0.1-0.3) + Early Stopping |
| 小数据集(< 1万样本) | L2(1e-3) + Dropout(0.5) + 强数据增强 |
| 大数据集(> 10万样本) | L2(1e-5) + Dropout(0.2) + 轻量数据增强 |
调参顺序
不要一次性把所有正则化都加上,会搞不清是哪个起了作用。推荐的调参顺序:
- 先加 L2 正则化(
weight_decay=1e-4),观察训练曲线 - 如果还过拟合,加数据增强
- 全连接层加 Dropout(
p=0.5) - 最后考虑 BatchNorm(如果训练还不稳定)
每次改动后至少跑 3-5 个 epoch,等指标稳定再做判断。
模型容量与正则化的关系
这里有个容易被忽略的点:正则化是「治标」,控制模型容量才是「治本」。
如果你的模型参数量远超数据量,比如用 ResNet-50 分类 100 张图片,加多少正则化都救不回来。这时候应该:
- 换小模型(比如 ResNet-18)
- 冻结部分层,只训练最后几层
- 减少网络深度和宽度
# 冻结预训练模型的参数
for param in pretrained_model.parameters():
param.requires_grad = False
# 只训练最后的分类层
model.fc = nn.Linear(512, num_classes)
optimizer = torch.optim.SGD(model.fc.parameters(), lr=0.01)
实战判断:如果训练集准确率长期在 70-80% 以下,大概率是模型容量不够;如果训练集 99% 而验证集 60%,那就该上正则化了。
实际项目中的调参记录
最后贴一段我之前项目的调参日志,给个直观参考:
Epoch 0: Train acc 45%, Val acc 42% # 刚开始,大家差不多
Epoch 5: Train acc 78%, Val acc 70% # 模型开始学习,但差距拉大
Epoch 10: Train acc 92%, Val acc 68% # 明显过拟合了
# 加上 weight_decay=1e-4
Epoch 15: Train acc 88%, Val acc 74% # 训练集掉了一点,验证集涨了
Epoch 20: Train acc 90%, Val acc 76% # 还是有差距,但好多了
# 加上数据增强 + Dropout(0.5)
Epoch 25: Train acc 85%, Val acc 79% # 训练集更低了,但泛化更好
Epoch 30: Train acc 87%, Val acc 81% # 接近收敛,差距 6 个点可接受
下图按 epoch 汇总了同一段调参日志里训练集与验证集准确率的变化,两条曲线的分叉程度直接反映过拟合程度。

可以看到正则化手段加入后,训练集准确率主动「降下来」,而验证集曲线则持续回升——这正是我们期望的泛化改善。
从这个记录能看出:加了正则化后,训练集准确率反而会下降(因为模型「背题」能力被限制了),但验证集表现会更好。这才是我们真正想要的。
收一下
正则化的本质,是给模型「套上一点约束」,让它不要过度拟合训练集里的噪声。L1/L2 是约束权重大小,Dropout 是约束网络结构,BatchNorm 是约束数据分布,Early Stopping 是约束训练时长。
但约束也有边界:正则化太强会把模型「限制死」,导致欠拟合;太弱又管不住过拟合。这个度,得靠具体任务和数据量来试。
最后再说一句:正则化能缓解过拟合,但不能替代好的数据。如果数据本身就有问题,再多的 L2、Dropout 也救不回来。模型的能力上限,还是由数据质量和数量决定的。
版权声明: 本文首发于 指尖魔法屋-AI正则化踩坑记录(https://blog.thinkmoon.cn/post/239-regularization-overfitting-generalization-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。