跳到主要内容

深度学习中的正则化

本节定位

深度网络参数量巨大,非常容易过拟合。本节介绍深度学习特有的正则化技术——Dropout 和 BatchNorm 是你必须掌握的两个。

学习目标

  • 🔧 掌握 Dropout 的原理和使用
  • 🔧 掌握 Batch Normalization(BN)
  • 理解 Layer Normalization(LN)
  • 🔧 掌握数据增强和早停法

先建立一张地图

正则化这节如果只背方法名,很容易变成“工具清单”。更适合新人的理解方式是:

所以这节真正想解决的是:

  • 模型为什么会过拟合
  • 不同正则化方法分别在哪一层起作用
  • 第一次遇到过拟合时,应该先试什么

这节和第 5 站、前面训练主线是怎么接上的

如果你从第 5 站过来,其实你已经见过:

  • 欠拟合 / 过拟合
  • 正则化
  • 交叉验证和泛化

到了这一节,只是把“控制泛化”这件事带到深度学习场景里,并补上更深度学习风格的方法:

  • Dropout
  • BatchNorm / LayerNorm
  • 数据增强
  • Early Stopping

一、回顾:L1/L2 正则化

第 5 站已学过——L2 正则化(权重衰减)在深度学习中直接通过优化器的 weight_decay 参数使用:

import torch
import torch.nn as nn

# AdamW 自带权重衰减
optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)

1.1 为什么深度学习里也仍然需要先记住 weight_decay

因为它往往是最简单、最稳、最先该试的一种正则化手段。

也就是说,深度学习里的正则化不是“全都换成新概念”,而是:

  • 先保留第 5 站你已经认识的那部分
  • 再往上叠深度学习里的结构和训练技巧

二、Dropout——随机丢弃

2.1 原理

训练时,随机让一部分神经元不工作(输出置为 0)。这迫使网络不依赖任何单个神经元,增强鲁棒性。

2.2 PyTorch 使用

import torch
import torch.nn as nn
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split

# 数据
X, y = make_moons(500, noise=0.3, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
X_train_t = torch.FloatTensor(X_train)
y_train_t = torch.LongTensor(y_train)
X_test_t = torch.FloatTensor(X_test)
y_test_t = torch.LongTensor(y_test)

# 对比有无 Dropout
class MLP(nn.Module):
def __init__(self, dropout_rate=0.0):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 64),
nn.ReLU(),
nn.Dropout(dropout_rate),
nn.Linear(64, 64),
nn.ReLU(),
nn.Dropout(dropout_rate),
nn.Linear(64, 2),
)

def forward(self, x):
return self.net(x)

results = {}
for name, drop in [('无 Dropout', 0.0), ('Dropout=0.3', 0.3), ('Dropout=0.5', 0.5)]:
model = MLP(drop)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
train_losses, test_losses = [], []

for epoch in range(200):
model.train()
loss = criterion(model(X_train_t), y_train_t)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_losses.append(loss.item())

model.eval()
with torch.no_grad():
test_loss = criterion(model(X_test_t), y_test_t)
test_losses.append(test_loss.item())

results[name] = (train_losses, test_losses)

fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for ax, (name, (tr, te)) in zip(axes, results.items()):
ax.plot(tr, label='训练', linewidth=2)
ax.plot(te, label='测试', linewidth=2)
ax.set_title(name)
ax.set_xlabel('Epoch')
ax.set_ylabel('Loss')
ax.legend()
ax.grid(True, alpha=0.3)
plt.suptitle('Dropout 对过拟合的影响', fontsize=13)
plt.tight_layout()
plt.show()
重要
  • model.train() 开启 Dropout
  • model.eval() 关闭 Dropout
  • 推理时一定要调 model.eval()

2.3 Dropout 到底适不适合所有模型?

不是。

一个更实用的记法是:

  • MLP:常见且有用
  • CNN:有时用,但不一定是第一优先
  • Transformer:通常不靠 Dropout 一招解决所有问题

所以不要把 Dropout 当成“只要过拟合就必开”的万能开关。

2.4 第一次遇到过拟合时,为什么不要只会想到 Dropout?

因为过拟合并不只来自一种原因。
它可能来自:

  • 数据太少
  • 模型太大
  • 训练太久
  • 特征或样本多样性不够

所以更稳的习惯是:

  • 先判断问题大概发生在哪一层
  • 再决定是从数据、结构、参数还是训练过程去处理

三、Batch Normalization(BN)

3.1 原理

对每一层的输出做归一化(均值为 0,标准差为 1),然后用可学习的参数缩放和平移。

作用:

  • 加速收敛
  • 减少对初始化的敏感性
  • 有轻微正则化效果

3.2 PyTorch 使用

class MLP_BN(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 64),
nn.BatchNorm1d(64), # BN 放在激活函数前面
nn.ReLU(),
nn.Linear(64, 64),
nn.BatchNorm1d(64),
nn.ReLU(),
nn.Linear(64, 2),
)

def forward(self, x):
return self.net(x)

# 对比有无 BN
for name, ModelClass in [('无 BN', MLP), ('有 BN', MLP_BN)]:
model = ModelClass() if name == '有 BN' else ModelClass(0.0)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1) # 用 SGD 更明显
criterion = nn.CrossEntropyLoss()

for epoch in range(100):
model.train()
loss = criterion(model(X_train_t), y_train_t)
optimizer.zero_grad()
loss.backward()
optimizer.step()

model.eval()
with torch.no_grad():
acc = (model(X_test_t).argmax(1) == y_test_t).float().mean()
print(f"{name}: 测试准确率 = {acc:.4f}")

四、Layer Normalization(LN)

BN vs LN

特性Batch NormalizationLayer Normalization
归一化维度跨样本(batch 维)跨特征(layer 维)
依赖 batch size
适用CNNTransformer、RNN
# BN vs LN 使用
bn = nn.BatchNorm1d(64) # 输入: (batch, 64)
ln = nn.LayerNorm(64) # 输入: (batch, 64)

x = torch.randn(32, 64)
print(f"BN 输出形状: {bn(x).shape}")
print(f"LN 输出形状: {ln(x).shape}")
信息

记住:CNN 用 BN,Transformer 用 LN。 这是实际工程中的标准选择。

4.1 BN 和 LN 为什么新人总容易混?

因为它们看起来都像“归一化”,但关注的维度不同:

  • BN 更依赖 batch 统计量
  • LN 更关注单个样本内部特征

你先不用死背所有细节,只先记住:

  • 图像 CNN 里,先优先想到 BN
  • Transformer 里,先优先想到 LN

4.2 BN / LN 最值得先记的,不是公式,而是“放在哪”

对新人更有帮助的记忆方式通常是:

  • BN 更像是 CNN 训练里的常见稳定器
  • LN 更像是 Transformer 里的常见稳定器

先把应用场景记对,比一开始追归一化公式细节更重要。


五、数据增强

5.1 图像数据增强

from torchvision import transforms

# 常用的图像增强组合
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.RandomRotation(15), # 随机旋转 ±15°
transforms.ColorJitter(brightness=0.2, contrast=0.2), # 颜色扰动
transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), # 随机裁剪
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])

# 测试集不做增强
test_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])

六、早停法(Early Stopping)

6.1 原理

监控验证集损失,连续 N 轮不下降就停止训练。

class EarlyStopping:
def __init__(self, patience=10, min_delta=0.001):
self.patience = patience
self.min_delta = min_delta
self.counter = 0
self.best_loss = float('inf')
self.should_stop = False

def step(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.should_stop = True
return self.should_stop

# 使用示例
early_stop = EarlyStopping(patience=10)
# for epoch in range(max_epochs):
# train(...)
# val_loss = validate(...)
# if early_stop.step(val_loss):
# print(f"早停! Epoch {epoch}")
# break

6.2 Early Stopping 为什么特别适合新人先学会?

因为它是最容易落地、最不容易写坏、收益又常常很直接的一招。

很多时候你甚至还没完全搞清模型结构,只要先做到:

  • 有验证集
  • 监控验证损失
  • 保存最佳权重

项目质量就已经会明显提升。

新人第一次遇到过拟合时,最稳的处理顺序

如果你已经看到“train_loss 继续降,val_loss 开始坏”,建议先按这个顺序试:

  1. 先检查数据量和数据增强
  2. 再考虑早停
  3. 再试 weight decay
  4. 再试 Dropout
  5. 最后再大改模型结构

这样会比“看到过拟合就随便加点正则化”更有条理。


小结

技术类型要点
Dropout防过拟合训练时随机丢弃,推理时关闭
Batch Norm加速+正则CNN 标配,放在激活前
Layer Norm加速+正则Transformer 标配
数据增强增加多样性只在训练集用
早停法防过拟合监控验证集 loss
权重衰减L2 正则optimizer 的 weight_decay

这节最该带走什么

  • 正则化不是一种方法,而是一整组控制泛化的手段
  • 不同方法作用在不同层面:参数、结构、数据、训练过程
  • 第一次处理过拟合时,先按顺序排查,比堆很多技巧更有效

如果再压成一句话,那就是:

正则化不是“加一个技巧”,而是在用不同层面的手段,让模型别把训练集学得太死。


动手练习

练习 1:正则化组合

在 MNIST 数据集上训练 MLP,依次添加 Dropout、BatchNorm、数据增强,观察测试准确率的变化。

练习 2:Early Stopping 实践

实现完整的早停训练循环,保存最佳模型权重,训练结束后加载最佳权重评估。