深度学习中的正则化
本节定位
深度网络参数量巨大,非常容易过拟合。本节介绍深度学习特有的正则化技术——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()开启 Dropoutmodel.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 Normalization | Layer Normalization |
|---|---|---|
| 归一化维度 | 跨样本(batch 维) | 跨特征(layer 维) |
| 依赖 batch size | 是 | 否 |
| 适用 | CNN | Transformer、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 里的常见稳定器
先把应用场景记对,比一开始追归一化公式细节更重要。