5深度学习

正则化

训练深度网络

L1/L2DropoutBatchNorm

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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||²₂
python
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||₁
python
# 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 对比

python
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 图像数据增强

python
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 数据增强

python
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,迫使网络不依赖任何单个神经元。

python
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作为集成学习

python
# 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

python
# 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 原理

早停在验证集性能不再提升时停止训练,防止模型继续拟合训练数据中的噪声。

python
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)
        break

2.4.2 早停与L2正则化的等价性

python
# 早停等价于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 的正则化效果

python
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 案例一:正则化方法对比实验

python
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的集成效果可视化

python
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值对模型性能的影响。

5/10正则化
0%