过拟合是深度学习的头号敌人。正则化技术帮助模型在训练集和测试集上都表现良好。本课深入分析Dropout和BatchNorm的原理、实现和效果。
训练误差 << 测试误差 → 过拟合
原因:模型容量过大,记住训练数据的噪声
解决方案:①更多数据 ②正则化 ③早停 ④模型简化
训练时以概率p随机"丢弃"神经元,测试时使用所有神经元但输出乘以(1-p)。
训练:h̃ = m ⊙ h,其中 m ~ Bernoulli(1-p)
测试:h̃ = (1-p) · h
或使用倒置Dropout:训练时除以(1-p),测试时不变
Dropout的效果:①隐式集成多个子网络②减少神经元共适应③提供模型平均效果
import torch
import torch.nn as nn
# Dropout效果验证
torch.manual_seed(42)
X = torch.randn(1000, 50)
y = (X[:, 0] + X[:, 1] > 0).float().unsqueeze(1)
# 有Dropout的模型
class ModelWithDropout(nn.Module):
def __init__(self, use_dropout=False, p=0.5):
super().__init__()
self.use_dropout = use_dropout
self.net = nn.Sequential(
nn.Linear(50, 200),
nn.ReLU(),
nn.Dropout(p) if use_dropout else nn.Identity(),
nn.Linear(200, 100),
nn.ReLU(),
nn.Dropout(p) if use_dropout else nn.Identity(),
nn.Linear(100, 1),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x)
# 训练对比
for use_drop, name in [(False, "无Dropout"), (True, "有Dropout(p=0.5)")]:
torch.manual_seed(42)
model = ModelWithDropout(use_dropout=use_drop)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
loss_fn = nn.BCELoss()
train_losses = []
test_losses = []
# Split train/test
X_train, X_test = X[:800], X[800:]
y_train, y_test = y[:800], y[800:]
for epoch in range(200):
model.train()
optimizer.zero_grad()
output = model(X_train)
loss = loss_fn(output, y_train)
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
test_loss = loss_fn(model(X_test), y_test)
if epoch % 50 == 0 or epoch == 199:
train_losses.append(loss.item())
test_losses.append(test_loss.item())
gap = [t - tr for tr, t in zip(train_losses, test_losses)]
print(f"\n{name}:")
print(f" 训练loss: {' → '.join([f'{l:.4f}' for l in train_losses])}")
print(f" 测试loss: {' → '.join([f'{l:.4f}' for l in test_losses])}")
print(f" 过拟合差距: {' → '.join([f'{g:.4f}' for g in gap])}")
# BatchNorm验证
print("\n=== BatchNorm效果 ===")
class ModelWithBN(nn.Module):
def __init__(self, use_bn=False):
super().__init__()
self.net = nn.Sequential(
nn.Linear(50, 200),
nn.BatchNorm1d(200) if use_bn else nn.Identity(),
nn.ReLU(),
nn.Linear(200, 100),
nn.BatchNorm1d(100) if use_bn else nn.Identity(),
nn.ReLU(),
nn.Linear(100, 1),
)
def forward(self, x):
return self.net(x)
for use_bn, name in [(False, "无BN"), (True, "有BN")]:
torch.manual_seed(42)
model = ModelWithBN(use_bn=use_bn)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
loss_fn = nn.MSELoss()
y_reg = X[:, :1] * 2 + 1
for epoch in range(100):
optimizer.zero_grad()
loss = loss_fn(model(X), y_reg)
loss.backward()
optimizer.step()
if epoch % 25 == 0:
print(f" {name} Epoch {epoch}: loss={loss.item():.4f}")
Dropout可以从多个角度理解:
| 层类型 | 推荐Dropout率 | 说明 |
|---|---|---|
| 输入层 | 0.1-0.2 | 不要丢弃太多输入信息 |
| 隐层(小) | 0.2-0.3 | 小网络不宜过多丢弃 |
| 隐层(大) | 0.3-0.5 | 大网络可以更激进 |
| 全连接层 | 0.5 | 标准选择 |
| 卷积层 | 很少用 | 卷积参数少,不易过拟合 |
| 注意力层 | 0.1 | Transformer中常用 |
标准Dropout:随机丢弃单个神经元
Spatial Dropout(nn.Dropout2d):随机丢弃整个通道
→ 适合CNN,因为相邻像素高度相关
DropPath:随机丢弃整个残差块
→ 适合深层网络(如ResNet, ViT)
💡 推荐资源:
分别用p=0.1, 0.3, 0.5, 0.7训练,画出验证准确率随p的变化曲线。
实现nn.Dropout2d,理解为什么对CNN要用整个通道的Dropout。
打印BatchNorm的running_mean和running_var,理解训练和推理时BN的行为差异。
训练时:μ_B = mean(x_B), σ²_B = var(x_B)
标准化:x̂ = (x - μ_B) / √(σ²_B + ε)
缩放平移:y = γx̂ + β (γ和β可学习)
推理时:使用训练时的running_mean和running_var
三种解释:
| 方法 | 归一化维度 | 适用场景 | 对小batch敏感? |
|---|---|---|---|
| BatchNorm | batch维度 | CV(大batch) | 是 |
| LayerNorm | 特征维度 | NLP/Transformer | 否 |
| InstanceNorm | 通道维度 | 风格迁移 | 否 |
| GroupNorm | 通道组 | CV(小batch) | 否 |
| RMSNorm | 特征维度 | LLaMA | 否 |
理解本课内容需要将其置于更大的知识网络中。每个核心概念都不是孤立的,它们相互关联、相互支撑:
建立正确的直觉比记住公式更重要:
1. 维度直觉:高维空间中,数据分布与低维直觉截然不同(维度诅咒)
2. 信息瓶颈:神经网络逐层压缩信息,保留任务相关的、丢弃无关的
3. 流形假设:高维数据实际分布在低维流形上,学习即发现流形结构
4. 偏差-方差权衡:简单模型欠拟合(高偏差),复杂模型过拟合(高方差)
本课作为神经网络基础阶段的核心内容,与前后课程紧密关联:
| 关联课程 | 关联点 | 协同效应 |
|---|---|---|
| 前序课程 | 提供了本课的基础知识 | 循序渐进的理解 |
| 后续课程 | 本课内容是后续的基础 | 逐步构建能力 |
| 平行课程 | 同一阶段的互补知识 | 多角度深入理解 |
| 实战项目 | 综合运用所有知识 | 理论与实践结合 |
💡 准备面试时,确保能回答以下问题:
掌握本课内容后,可以通过以下方式继续深入:
掌握Dropout和BatchNorm,你已经有了对抗过拟合的利器!