第5章:正则化(Regularization)
一、导读
1.1 本章学习目标
第5章深入讲解深度学习中防止过拟合的各种正则化技术。过拟合是深度学习实践中最常见的问题之一——模型在训练集上表现完美,但在未见过的数据上表现糟糕。正则化技术通过限制模型的有效容量,迫使模型学习更通用的模式而非记住训练数据。
本章的核心学习目标包括:
- 理解正则化的本质:通过限制模型容量提高泛化能力
- 掌握参数范数惩罚:L1/L2正则化的原理和效果差异
- 掌握数据增强技术:如何扩充训练数据的多样性
- 理解Dropout的工作原理:随机失活如何实现隐式集成
- 掌握早停策略:如何在过拟合发生前停止训练
- 了解Batch Normalization的正则化效果
1.2 为什么本章如此重要
深度学习的模型容量极大——数百万到数十亿参数——这使得过拟合成为几乎必然发生的问题。没有正则化技术,深度学习模型无法在实际任务中取得良好的泛化性能。
正则化技术的选择和调优直接影响模型的最终性能。不同的任务可能需要不同的正则化策略组合。理解每种技术的原理,才能根据具体问题选择合适的方案。
1.3 核心问题引导
正则化的本质是什么?为什么限制模型容量能提高泛化能力?
L1和L2正则化在几何上有什么区别?为什么L1产生稀疏解?
Dropout为什么能防止过拟合?它与模型集成有什么关系?
数据增强为什么有效?什么样的数据增强是"好"的?
早停的本质是什么?它与L2正则化有什么关系?
Batch Normalization为什么有正则化效果?
二、核心概念详解
2.1 参数范数惩罚
2.1.1 L2 正则化
L2正则化在损失函数中添加权重平方和的惩罚:
L_regularized = L + λ/2 · ||w||²₂import torch
import torch.nn as nn
# PyTorch中的L2正则化(weight_decay)
model = nn.Linear(100, 10)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
# 手动实现L2正则化
def l2_regularization_loss(model, loss_fn, X, y, lambda_l2):
loss = loss_fn(model(X), y)
l2_penalty = sum(p.pow(2).sum() for p in model.parameters())
return loss + lambda_l2 * l2_penalty
# L2正则化的效果:权重趋向小值
import numpy as np
import matplotlib.pyplot as plt
# 模拟训练过程
np.random.seed(42)
n_epochs = 200
w_no_reg = np.random.randn(10)
w_l2 = np.random.randn(10)
lambda_l2 = 0.1
for epoch in range(n_epochs):
# 模拟梯度(随机)
grad = np.random.randn(10) * 0.1
w_no_reg -= 0.01 * grad
w_l2 -= 0.01 * (grad + lambda_l2 * w_l2)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.bar(range(10), w_no_reg, alpha=0.7, label='无正则化')
plt.bar(range(10), w_l2, alpha=0.7, label='L2正则化')
plt.legend()
plt.title('权重分布对比')
plt.subplot(1, 2, 2)
plt.plot([np.linalg.norm(w_no_reg)], [0], 'bo', label='无正则化')
plt.plot([np.linalg.norm(w_l2)], [0], 'ro', label='L2正则化')
plt.legend()
plt.title(f'权重范数: 无正则={np.linalg.norm(w_no_reg):.2f}, L2={np.linalg.norm(w_l2):.2f}')
plt.tight_layout()
plt.savefig('l2_regularization.png', dpi=150)
plt.show()2.1.2 L1 正则化
L1正则化添加权重绝对值和的惩罚:
L_regularized = L + λ · ||w||₁# L1正则化产生稀疏解的几何解释
# L1的约束区域是菱形(多面体),与损失函数的等高线更容易在坐标轴上相交
import numpy as np
import matplotlib.pyplot as plt
w1 = np.linspace(-3, 3, 200)
w2 = np.linspace(-3, 3, 200)
W1, W2 = np.meshgrid(w1, w2)
# 损失函数等高线(假设最优解在(2, 1)附近)
Z = (W1 - 2)**2 + (W2 - 1)**2
plt.figure(figsize=(12, 5))
# L2约束
plt.subplot(1, 2, 1)
plt.contour(W1, W2, Z, levels=20, cmap='viridis', alpha=0.5)
circle = plt.Circle((0, 0), 1.5, fill=False, color='red', linewidth=2, label='L2约束')
plt.gca().add_patch(circle)
plt.plot(2, 1, 'b*', markersize=10, label='无约束最优解')
plt.plot(1.06, 1.06, 'r*', markersize=10, label='L2约束最优解')
plt.legend()
plt.title('L2正则化(各向同性收缩)')
plt.xlabel('w1')
plt.ylabel('w2')
plt.axis('equal')
# L1约束
plt.subplot(1, 2, 2)
plt.contour(W1, W2, Z, levels=20, cmap='viridis', alpha=0.5)
diamond = plt.Polygon([(-1.5, 0), (0, 1.5), (1.5, 0), (0, -1.5)],
fill=False, color='red', linewidth=2, label='L1约束')
plt.gca().add_patch(diamond)
plt.plot(2, 1, 'b*', markersize=10, label='无约束最优解')
plt.plot(0.5, 1.0, 'r*', markersize=10, label='L1约束最优解(稀疏)')
plt.legend()
plt.title('L1正则化(产生稀疏解)')
plt.xlabel('w1')
plt.ylabel('w2')
plt.axis('equal')
plt.tight_layout()
plt.savefig('l1_vs_l2.png', dpi=150)
plt.show()2.1.3 L1 vs L2 对比
from sklearn.linear_model import LinearRegression, Ridge, Lasso
# 生成高维稀疏数据
np.random.seed(42)
n_samples, n_features = 100, 50
X = np.random.randn(n_samples, n_features)
true_weights = np.zeros(n_features)
true_weights[:5] = [2, -1.5, 3, -0.5, 1] # 只有5个非零
y = X @ true_weights + 0.5 * np.random.randn(n_samples)
models = {
'无正则化': LinearRegression(),
'L2 (Ridge, α=1)': Ridge(alpha=1.0),
'L1 (Lasso, α=0.1)': Lasso(alpha=0.1),
}
for name, model in models.items():
model.fit(X, y)
n_nonzero = np.sum(np.abs(model.coef_) > 0.01)
mse = np.mean((model.predict(X) - y) ** 2)
print(f"{name:20s}: 非零权重={n_nonzero:2d}, MSE={mse:.4f}")2.2 数据增强
2.2.1 图像数据增强
import torch
from torchvision import transforms
# 训练时的数据增强
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(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]),
])
# 测试时不做增强
test_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]),
])
# 可视化增强效果
from torchvision.datasets import FakeData
dataset = FakeData(size=1, image_size=(3, 256, 256), transform=train_transform)
fig, axes = plt.subplots(2, 5, figsize=(15, 6))
for i in range(10):
img, _ = dataset[0]
axes[i//5, i%5].imshow(img.permute(1, 2, 0))
axes[i//5, i%5].axis('off')
plt.suptitle('数据增强示例(同一图像的不同变换)')
plt.tight_layout()
plt.savefig('data_augmentation.png', dpi=150)
plt.show()2.2.2 Mixup 数据增强
def mixup_data(x, y, alpha=0.2):
"""Mixup数据增强"""
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1.0
batch_size = x.size(0)
index = torch.randperm(batch_size, device=x.device)
mixed_x = lam * x + (1 - lam) * x[index]
mixed_y_a, mixed_y_b = y, y[index]
return mixed_x, mixed_y_a, mixed_y_b, lam
def mixup_criterion(criterion, pred, y_a, y_b, lam):
"""Mixup损失函数"""
return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)
# 使用示例
x = torch.randn(32, 3, 32, 32)
y = torch.randint(0, 10, (32,))
mixed_x, y_a, y_b, lam = mixup_data(x, y, alpha=0.2)
print(f"Mixup lambda: {lam:.3f}")
print(f"混合后数据形状: {mixed_x.shape}")2.3 Dropout
2.3.1 原理
Dropout在训练时随机将一部分神经元的输出设为0,迫使网络不依赖任何单个神经元。
import torch
import torch.nn as nn
# Dropout的使用
class DropoutModel(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 512)
self.dropout1 = nn.Dropout(0.5)
self.fc2 = nn.Linear(512, 256)
self.dropout2 = nn.Dropout(0.3)
self.fc3 = nn.Linear(256, 10)
def forward(self, x):
x = x.view(-1, 784)
x = torch.relu(self.fc1(x))
x = self.dropout1(x)
x = torch.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x)
return x
model = DropoutModel()
x = torch.randn(1, 784)
# 训练模式:dropout生效
model.train()
outputs = [model(x) for _ in range(5)]
print("训练模式(每次输出不同):")
for i, out in enumerate(outputs):
print(f" 第{i+1}次: {out[0, :3].tolist()}")
# 评估模式:dropout关闭
model.eval()
outputs = [model(x) for _ in range(5)]
print("\n评估模式(每次输出相同):")
for i, out in enumerate(outputs):
print(f" 第{i+1}次: {out[0, :3].tolist()}")2.3.2 Dropout作为集成学习
# Dropout可以近似为模型集成
# 每次dropout产生一个不同的子网络
# 训练时随机采样子网络,测试时使用所有子网络的平均
# 直观理解:
# 假设隐藏层有n个神经元,dropout率p
# 可能的子网络数量:2^n
# 训练时采样其中一部分,测试时近似所有子网络的平均
n_neurons = 10
dropout_rate = 0.5
n_subnetworks = 2 ** n_neurons
print(f"可能的子网络数量: {n_subnetworks}")
print(f"训练时每次采样1个子网络")
print(f"测试时近似所有子网络的集成")2.3.3 DropConnect
# DropConnect:随机将权重(而非激活值)设为0
class DropConnect(nn.Module):
def __init__(self, module, drop_prob=0.5):
super().__init__()
self.module = module
self.drop_prob = drop_prob
def forward(self, x):
if self.training:
mask = torch.bernoulli(torch.ones_like(self.module.weight) * (1 - self.drop_prob))
masked_weight = self.module.weight * mask / (1 - self.drop_prob)
return torch.nn.functional.linear(x, masked_weight, self.module.bias)
return self.module(x)2.4 早停(Early Stopping)
2.4.1 原理
早停在验证集性能不再提升时停止训练,防止模型继续拟合训练数据中的噪声。
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 = None
self.early_stop = False
self.best_weights = None
def __call__(self, val_loss, model):
if self.best_loss is None:
self.best_loss = val_loss
self.best_weights = {k: v.clone() for k, v in model.state_dict().items()}
elif val_loss > self.best_loss - self.min_delta:
self.counter += 1
if self.counter >= self.patience:
self.early_stop = True
else:
self.best_loss = val_loss
self.best_weights = {k: v.clone() for k, v in model.state_dict().items()}
self.counter = 0
# 使用示例
early_stopping = EarlyStopping(patience=5)
for epoch in range(100):
# 训练...
train_loss = train_one_epoch()
val_loss = validate()
early_stopping(val_loss, model)
if early_stopping.early_stop:
print(f"早停在 epoch {epoch}")
model.load_state_dict(early_stopping.best_weights)
break2.4.2 早停与L2正则化的等价性
# 早停等价于L2正则化的直观解释
# 从最优解(无正则化的最优参数w*)开始
# 训练步数t相当于从w*出发走了t步
# 步数越少,参数离w*越近,相当于更强的正则化
# 可视化训练轨迹
np.random.seed(42)
n_steps = 200
w_optimal = np.array([2.0, 1.0]) # 无正则化的最优解
# 模拟SGD轨迹(从原点出发)
trajectory = [np.array([0.0, 0.0])]
for t in range(n_steps):
direction = w_optimal - trajectory[-1]
noise = np.random.randn(2) * 0.05
step = direction * 0.05 + noise
trajectory.append(trajectory[-1] + step)
trajectory = np.array(trajectory)
plt.figure(figsize=(8, 6))
plt.plot(trajectory[:, 0], trajectory[:, 1], 'b-', alpha=0.5)
plt.scatter([0], [0], c='green', s=100, marker='*', label='初始点')
plt.scatter([w_optimal[0]], [w_optimal[1]], c='red', s=100, marker='*', label='最优解')
# 标记不同早停点
for t in [20, 50, 100, 200]:
if t < len(trajectory):
plt.scatter([trajectory[t, 0]], [trajectory[t, 1]], s=50,
label=f'epoch={t}')
plt.xlabel('w1')
plt.ylabel('w2')
plt.title('训练轨迹与早停')
plt.legend()
plt.savefig('early_stopping.png', dpi=150)
plt.show()2.5 Batch Normalization 的正则化效果
import torch
import torch.nn as nn
# Batch Normalization有轻微的正则化效果
# 因为每个batch的统计量不同,引入了噪声
class ModelWithBN(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.bn1 = nn.BatchNorm1d(256)
self.fc2 = nn.Linear(256, 10)
def forward(self, x):
x = self.bn1(self.fc1(x))
x = torch.relu(x)
return self.fc2(x)
model = ModelWithBN()
x = torch.randn(32, 784)
# 训练模式:使用batch统计量(有正则化效果)
model.train()
out1 = model(x)
# 评估模式:使用全局统计量(无正则化效果)
model.eval()
out2 = model(x)
print(f"训练和评估模式输出差异: {(out1 - out2).abs().mean().item():.6f}")
# 差异来自batch统计量 vs 全局统计量三、实战案例
3.1 案例一:正则化方法对比实验
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 数据
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000)
# 不同正则化策略的模型
def create_model(reg_type='none'):
layers = [nn.Linear(784, 256), nn.ReLU()]
if reg_type == 'dropout':
layers.append(nn.Dropout(0.5))
if reg_type == 'batchnorm':
layers = [nn.Linear(784, 256), nn.BatchNorm1d(256), nn.ReLU()]
layers.extend([nn.Linear(256, 128), nn.ReLU()])
if reg_type == 'dropout':
layers.append(nn.Dropout(0.3))
layers.append(nn.Linear(128, 10))
return nn.Sequential(*layers)
# 训练和评估函数
def train_and_evaluate(model, train_loader, test_loader, epochs=10, weight_decay=0):
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=weight_decay)
results = {'train_acc': [], 'test_acc': []}
for epoch in range(epochs):
# 训练
model.train()
correct, total = 0, 0
for data, target in train_loader:
optimizer.zero_grad()
output = model(data.view(-1, 784))
loss = criterion(output, target)
loss.backward()
optimizer.step()
pred = output.argmax(1)
correct += pred.eq(target).sum().item()
total += target.size(0)
results['train_acc'].append(correct / total)
# 测试
model.eval()
correct, total = 0, 0
with torch.no_grad():
for data, target in test_loader:
output = model(data.view(-1, 784))
pred = output.argmax(1)
correct += pred.eq(target).sum().item()
total += target.size(0)
results['test_acc'].append(correct / total)
return results
# 对比实验
reg_types = ['none', 'dropout', 'batchnorm']
colors = {'none': 'blue', 'dropout': 'red', 'batchnorm': 'green'}
plt.figure(figsize=(10, 5))
for reg_type in reg_types:
model = create_model(reg_type)
results = train_and_evaluate(model, train_loader, test_loader, epochs=10)
plt.plot(results['train_acc'], '--', color=colors[reg_type], label=f'{reg_type} (train)')
plt.plot(results['test_acc'], '-', color=colors[reg_type], label=f'{reg_type} (test)')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('不同正则化方法的对比')
plt.legend()
plt.savefig('regularization_comparison.png', dpi=150)
plt.show()3.2 案例二:Dropout的集成效果可视化
import torch
import torch.nn as nn
# 训练一个带Dropout的模型
model = nn.Sequential(
nn.Linear(784, 128),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(128, 10)
)
# 模拟:多次前向传播(相当于不同的子网络)
x = torch.randn(1, 784)
model.train()
predictions = []
with torch.no_grad():
for _ in range(100):
out = model(x)
predictions.append(torch.softmax(out, dim=-1))
predictions = torch.stack(predictions)
mean_prediction = predictions.mean(dim=0)
std_prediction = predictions.std(dim=0)
print(f"平均预测: {mean_prediction[0]}")
print(f"预测标准差: {std_prediction[0]}")
print(f"标准差最大的类别不确定性: {std_prediction[0].max().item():.4f}")四、本章小结
本章系统介绍了深度学习的正则化技术:
参数范数惩罚:L2正则化(权重衰减)限制权重大小,L1正则化产生稀疏解用于特征选择。
数据增强:通过变换扩充训练数据,提高模型的泛化能力。Mixup通过混合样本创造新的训练数据。
Dropout:随机失活神经元,实现隐式集成学习。是深度学习中最常用的正则化方法之一。
早停:在验证集性能不再提升时停止训练,等价于对参数空间的隐式约束。
Batch Normalization:除了加速训练,还有轻微的正则化效果。
正则化是深度学习实践中不可或缺的技术。合理选择和组合正则化方法,是获得良好泛化性能的关键。
五、思考与练习
思考题
L1正则化为什么产生稀疏解?从优化景观和概率先验两个角度解释。
Dropout的推理阶段为什么不需要缩放?训练阶段的inverted dropout是什么?
数据增强的"标签保持"原则是什么?什么样的增强是合理的?
早停与模型检查点保存如何配合使用?
为什么过参数化的深度学习模型不容易过拟合?这与传统理论矛盾吗?
编程练习
正则化对比:在CIFAR-10上比较无正则化、L2、Dropout、数据增强的效果。
早停实现:实现带早停的训练循环,比较不同patience值的效果。
Mixup实现:实现Mixup数据增强,观察对不同数据集的效果。
Dropout集成:在测试时对同一输入做多次前向传播,比较单次预测和集成预测的差异。
权重衰减调优:系统比较不同weight_decay值对模型性能的影响。