从过拟合走到泛化:AI 正则化笔记
我这次就是 L2 正则化 + Dropout + 数据增强一起上的。
问题是怎么出现的
前几个月在做一个图像分类任务,数据量不算大,大概 5 万张图片,分成 10 个类别。一开始的想法很直接:搭个深层网络,多加几个卷积层,训练的时候加大学习率,应该很快就能收敛。
实际跑下来,训练集上的准确率确实很快就到了 98% 以上,但验证集的曲线完全不是那么回事。准确率卡在 70% 左右晃悠,有时候还往下掉。训练损失一路走低,验证损失却开始往上走——这是最典型的过拟合信号。
这种现象背后有几个原因。网络参数太多,而数据量又不够充分,模型有足够的容量记住所有训练样本。数据本身可能存在噪声或者标注不准确,但模型强行拟合了这些错误。还有可能是数据分布不够均衡,某些类别的样本很少,模型就记住了这些少样本的特征,而不是学到了真正的类别特征。
下面这张图可以帮你看清楚过拟合和正常学习的区别。左侧是典型的过拟合模式,训练损失持续下降但验证损失反而开始上升;右侧是加上正则化后的正常学习模式,两条曲线基本同步下降。

从图中能看出,过拟合的关键特征就是训练集表现和验证集表现开始出现明显分歧。一旦发现这种趋势,就该考虑上正则化手段了。
先搞清楚正则化到底是什么
说人话的话,正则化就是在训练过程中给模型加一点"约束",让它不要走得太偏。就像考试时如果只背答案,考场上遇到新题就傻眼;但如果学会了解题思路,就算题目变一点也能应付。
最常用的正则化方法有几种:
L1/L2 正则化:在损失函数里加一项惩罚项,让模型的权重保持比较小的值。L2 会把所有权重都往 0 压,L1 则会让很多权重真正变成 0,起到特征选择的作用。
Dropout:训练时随机"扔掉"一部分神经元,让网络不能过度依赖某几个特定的神经元。每次训练都相当于在训练一个"残缺"的子网络,最后 ensemble 起来就变得更稳健。
数据增强:通过旋转、翻转、缩放、颜色抖动等手段,从现有数据中造出"伪新样本"。这相当于增加了训练数据的数量和多样性,让模型学到的是"不变特征"而不是样本的具体像素。
早停:训练时盯着验证集的指标,一旦发现验证损失不再下降就停止训练。这其实也是一种正则化思想,不让模型在训练集上走得太远。
这些方法不是互相排斥的,实际项目中经常会组合使用。我这次就是 L2 正则化 + Dropout + 数据增强一起上的。
实际怎么做
先搭一个基础网络,用 ResNet-50 作为 backbone,改掉最后的全连接层来适配我的 10 分类任务。
import torch
import torch.nn as nn
from torchvision import models
class ImageClassifier(nn.Module):
def __init__(self, num_classes=10, dropout_rate=0.5):
super().__init__()
# 用预训练的 ResNet-50
self.backbone = models.resnet50(pretrained=True)
# 冻结前面的层,只训练最后的分类头
for param in self.backbone.parameters():
param.requires_grad = False
# 替换最后的全连接层
num_features = self.backbone.fc.in_features
self.backbone.fc = nn.Sequential(
nn.Linear(num_features, 512),
nn.ReLU(),
nn.Dropout(dropout_rate), # Dropout
nn.Linear(512, num_classes)
)
def forward(self, x):
return self.backbone(x)
训练的时候在损失函数里加上 L2 正则化,PyTorch 里这个参数叫 weight_decay。
# 优化器配置,weight_decay 就是 L2 正则化系数
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-4,
weight_decay=1e-4 # L2 正则化
)
# 损失函数
criterion = nn.CrossEntropyLoss()
数据增强这块,我主要用了翻转、旋转、颜色抖动和随机裁剪。这些变换虽然会改变图像的外观,但不会改变图像的语义类别——这就是为什么数据增强有效的原因。
下面这张图展示了不同数据增强技术的效果,从基础变换到组合变换:

数据增强的关键在于保持语义不变性。比如一张猫的照片,不管怎么旋转、翻转、调整亮度,它还是一张猫的照片。这样模型学到的就是"猫"这个概念,而不是某一组特定的像素值。
from torchvision import transforms
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224), # 随机裁剪和缩放
transforms.RandomHorizontalFlip(p=0.5), # 水平翻转
transforms.RandomRotation(15), # 旋转 ±15 度
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), # 颜色抖动
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
训练循环里要做的就是把这些东西都加上,然后一边训练一边监控指标。
def train_epoch(model, dataloader, criterion, optimizer, device):
model.train()
running_loss = 0.0
correct = 0
total = 0
for images, labels in dataloader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
epoch_loss = running_loss / len(dataloader)
epoch_acc = 100. * correct / total
return epoch_loss, epoch_acc
def validate(model, dataloader, criterion, device):
model.eval()
running_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in dataloader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
running_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
epoch_loss = running_loss / len(dataloader)
epoch_acc = 100. * correct / total
return epoch_loss, epoch_acc
踩坑过程与参数调整
一开始我把 weight_decay 设置成了 1e-2,结果模型根本学不动,训练集上的准确率一直卡在 10% 左右——这比随机猜测还差。后来改成 1e-4,情况就正常了。正则化系数太大会让模型过度受限,该学的东西学不到。
Dropout 的比例也是个坑。一开始我用了 0.7,结果模型又学不动了,验证集上的表现比训练集还差,说明 Dropout 太激进,把有用的信息也扔掉了。调到 0.3 之后,泛化能力明显提升,但也没影响模型的学习能力。
数据增强这块也得小心。旋转角度设置成 30 度的时候,有些类别的图像(比如文字、方向性强的图案)被转得太厉害,反而不合理了。最后改成 15 度,既能增加多样性,又不会破坏语义。
还有一个问题是早停的判断。不能只看一个 epoch 的指标下降就认为还在提升,我设了一个 patience=5,就是连续 5 个 epoch 验证损失都不创新低才停止。这样可以避免偶尔的波动导致提前结束训练。
最后用的参数组合是:
weight_decay: 1e-4dropout_rate: 0.3(分类层)learning_rate: 1e-4- 数据增强:翻转、±15 度旋转、轻微颜色抖动
结果怎么样
加上正则化之后,训练集上的准确率没有之前那么高了,大概在 92% 左右,但验证集的准确率从 70% 提升到了 85%。这说明模型没有再死记硬背训练样本,而是学到了真正的类别特征。
最明显的变化是损失曲线:训练损失和验证损失基本同步下降,不再出现验证损失往上走的情况。模型的泛化能力确实变强了,在测试集上的表现也更稳定。
这个结果其实很有意思:牺牲一点训练集上的"完美表现",换来了更好的泛化能力。在实际应用中,我们更看重的是模型在新数据上的表现,而不是它在训练集上能多"完美"。
还有一些边界情况需要说明
正则化不是万能的。如果你的数据量实在太少,比如几百张图片要分 10 个类别,就算正则化手段都上齐了,效果可能还是很有限。这种情况下,考虑用迁移学习或者收集更多数据才是正解。
还有就是不同任务对正则化的敏感度不一样。文本分类任务可能更依赖 Dropout,图像任务可能更依赖数据增强。具体用哪种、用多少,还是要根据实际效果来调。
模型本身的复杂度也要考虑。如果一个简单的任务用了一个超大的模型,正则化可能也救不回来。这时候不如直接用一个小一点的模型,反而效果更好。
写在后面
这次折腾下来,我对正则化的理解比以前深了一些。它不是为了"提升性能",而是为了让模型学会该学的东西,而不是记住不该记的东西。训练集上的高准确率不等于好模型,真正重要的是模型在新数据上的表现。
正则化有点像生活中的"克制"。知道什么该做,更重要的是知道什么不该做。模型训练也是一样,知道如何拟合数据很重要,但知道何时停止、何时约束自己更重要。
最后说一句,这些方法都是经验和技巧,不是绝对真理。实际项目中还是要多试验、多对比,找到最适合当前任务的那套参数组合。技术就是这样,没有银弹,只有适合和不适合。
版权声明: 本文首发于 指尖魔法屋-从过拟合走到泛化:AI 正则化笔记(https://blog.thinkmoon.cn/post/337-ai-regularization-overfitting-generalization-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。