在这里插入图片描述

文章目录


📖 课前导读

为什么模型会“死记硬背”?

想象你准备一场考试:如果你只死记硬背题库中的原题,考试时题目稍作改动你就不会了。这就像过拟合——模型把训练样本的噪声和细节都记住了,却没学到背后的规律。

深度神经网络拥有海量参数,理论上可以拟合任意复杂的数据分布。但这种强大的表达能力是一把双刃剑:当训练数据有限或噪声较多时,模型很容易陷入“过度学习”的状态,把每个样本的个性当成了共性。

过拟合的表现非常典型:训练损失持续下降,训练准确率接近100%,但验证损失开始回升,验证准确率停滞甚至下降。解决过拟合的核心思想是“正则化”——给模型添加约束,迫使它学习更简单、更通用的模式。

💡 核心比喻:正则化就像给模型戴上“思想的镣铐”。Dropout强制每次迭代只使用部分神经元,迫使网络不依赖单个特征;BatchNorm通过标准化层输入,稳定训练并附带正则化效果;L2正则化相当于对权重施加惩罚,鼓励权重分散而非集中。这些方法都能有效提高泛化能力。

学完这一课,你将能够:

  • ✅ 根据训练/验证曲线准确判断过拟合、欠拟合、良好拟合
  • ✅ 正确使用Dropout及其变体(DropConnect、SpatialDropout)
  • ✅ 理解BatchNorm与LayerNorm的原理及适用场景(CNN vs RNN/Transformer)
  • ✅ 实现L1/L2正则化并理解其对权重稀疏性的影响
  • ✅ 编写早停(Early Stopping)回调,自动保存最佳模型
  • ✅ 应用数据增强策略扩充训练集(承接第15课)
  • ✅ 了解模型剪枝的基本流程
  • ✅ 组合多种正则化技术提升模型泛化能力

一、知识原理:过拟合与欠拟合的本质

1.1 偏差-方差分解视角

模型的泛化误差可以分解为:偏差(Bias)+ 方差(Variance)+ 不可约误差。

  • 欠拟合:高偏差,模型过于简单,无法捕捉数据中的规律(训练和验证误差都高)。
  • 过拟合:高方差,模型过于复杂,对训练数据中的噪声也进行了拟合(训练误差极低,验证误差高)。
  • 良好拟合:偏差和方差平衡,训练误差略低于验证误差,但两者都较低。

1.2 过拟合的常见原因

原因解释解决方案
训练数据不足样本量过少,模型容易记住每个样本数据增强、迁移学习、合成数据
模型容量过大参数量远多于样本数减少层数/神经元数、正则化
训练时间过长迭代次数过多,模型开始拟合噪声早停、保存最佳模型
数据噪声大标签错误或特征包含无关信息数据清洗、增加正则化强度

1.3 欠拟合的常见原因

原因解决方案
模型过于简单增加层数/神经元数、使用更复杂的架构
学习率过低提高学习率或使用学习率调度
正则化过强降低Dropout比例、减小L2权重
特征不足增加特征工程或使用预训练特征

二、环境搭建与准备

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from sklearn.model_selection import train_test_split
from collections import defaultdict

print(f"PyTorch版本: {torch.__version__}")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

# 固定随机种子
torch.manual_seed(42)
np.random.seed(42)

# 准备一个小型数据集(CIFAR-10子集)用于演示过拟合
def get_cifar10_subset(num_samples=500):
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
    ])
    full_train = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    # 取前num_samples作为训练集
    subset, _ = random_split(full_train, [num_samples, len(full_train)-num_samples])
    return subset

# 可视化训练曲线辅助函数
def plot_training_curves(history, title="Training Curves"):
    epochs = range(1, len(history['train_loss'])+1)
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
    ax1.plot(epochs, history['train_loss'], label='Train Loss')
    ax1.plot(epochs, history['val_loss'], label='Val Loss')
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('Loss')
    ax1.legend()
    ax1.grid(True)
    ax2.plot(epochs, history['train_acc'], label='Train Acc')
    ax2.plot(epochs, history['val_acc'], label='Val Acc')
    ax2.set_xlabel('Epoch')
    ax2.set_ylabel('Accuracy')
    ax2.legend()
    ax2.grid(True)
    plt.suptitle(title)
    plt.tight_layout()
    plt.show()

三、代码实战:过拟合与欠拟合的观测

3.1 故意制造过拟合

使用一个容量较大的模型(如ResNet-18)在小数据集(CIFAR-10 500张)上训练,直观观察过拟合现象。

from torchvision.models import resnet18

# 创建过拟合场景:小数据集 + 大模型
train_dataset_small = get_cifar10_subset(num_samples=500)
val_dataset = datasets.CIFAR10(root='./data', train=False, download=True, 
                               transform=transforms.Compose([
                                   transforms.ToTensor(),
                                   transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
                               ]))

batch_size = 32
train_loader = DataLoader(train_dataset_small, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)

# 不使用任何正则化的ResNet-18
model_overfit = resnet18(num_classes=10).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model_overfit.parameters(), lr=0.01, momentum=0.9)

def train_epoch(model, loader, optimizer, criterion):
    model.train()
    total_loss = 0
    correct = 0
    total = 0
    for inputs, labels in loader:
        inputs, labels = inputs.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * inputs.size(0)
        _, pred = torch.max(outputs, 1)
        correct += (pred == labels).sum().item()
        total += labels.size(0)
    return total_loss / total, correct / total

def evaluate(model, loader, criterion):
    model.eval()
    total_loss = 0
    correct = 0
    total = 0
    with torch.no_grad():
        for inputs, labels in loader:
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            total_loss += loss.item() * inputs.size(0)
            _, pred = torch.max(outputs, 1)
            correct += (pred == labels).sum().item()
            total += labels.size(0)
    return total_loss / total, correct / total

epochs = 30
history_overfit = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}
for epoch in range(epochs):
    train_loss, train_acc = train_epoch(model_overfit, train_loader, optimizer, criterion)
    val_loss, val_acc = evaluate(model_overfit, val_loader, criterion)
    history_overfit['train_loss'].append(train_loss)
    history_overfit['train_acc'].append(train_acc)
    history_overfit['val_loss'].append(val_loss)
    history_overfit['val_acc'].append(val_acc)
    if (epoch+1) % 5 == 0:
        print(f"Epoch {epoch+1}: Train Loss {train_loss:.4f} Acc {train_acc:.4f} | Val Loss {val_loss:.4f} Acc {val_acc:.4f}")

plot_training_curves(history_overfit, title="Overfitting Example (No Regularization)")
# 你会看到训练损失降到接近0,训练准确率接近100%,但验证准确率可能只有50%左右,且验证损失后期回升。

3.2 欠拟合的模拟

使用一个过于简单的模型(如单层线性分类器)在足够大的数据集上训练,观察欠拟合。

class LinearClassifier(nn.Module):
    def __init__(self, input_dim=3*32*32, num_classes=10):
        super().__init__()
        self.fc = nn.Linear(input_dim, num_classes)
    
    def forward(self, x):
        x = x.view(x.size(0), -1)
        return self.fc(x)

model_underfit = LinearClassifier().to(device)
optimizer = optim.SGD(model_underfit.parameters(), lr=0.01, momentum=0.9)

# 使用全部CIFAR-10训练集(50000张)来展示欠拟合
full_train = datasets.CIFAR10(root='./data', train=True, download=True, transform=transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
]))
full_train_loader = DataLoader(full_train, batch_size=64, shuffle=True)

history_underfit = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': []}
for epoch in range(epochs):
    train_loss, train_acc = train_epoch(model_underfit, full_train_loader, optimizer, criterion)
    val_loss, val_acc = evaluate(model_underfit, val_loader, criterion)
    history_underfit['train_loss'].append(train_loss)
    history_underfit['train_acc'].append(train_acc)
    history_underfit['val_loss'].append(val_loss)
    history_underfit['val_acc'].append(val_acc)
    if (epoch+1) % 5 == 0:
        print(f"Epoch {epoch+1}: Train Loss {train_loss:.4f} Acc {train_acc:.4f} | Val Loss {val_loss:.4f} Acc {val_acc:.4f}")

plot_training_curves(history_underfit, title="Underfitting Example (Linear Model)")
# 训练和验证准确率都很低(约40%),损失下降缓慢,说明模型表达能力不足。

四、Dropout:随机失活正则化

4.1 Dropout原理与PyTorch实现

Dropout在训练时以概率p随机将神经元的输出置为0,被丢弃的神经元不参与前向传播和反向传播。测试时,所有神经元都激活,但输出乘以1-p以保持期望一致。

# PyTorch中的Dropout使用
dropout_layer = nn.Dropout(p=0.5)
x = torch.randn(1, 10)
out_train = dropout_layer(x)  # 训练时约一半元素变为0
print("训练时输出:\n", out_train)

dropout_layer.eval()
out_eval = dropout_layer(x)   # 评估时,所有元素乘以(1-p) = 0.5
print("评估时输出:\n", out_eval)

4.2 在CNN中正确使用Dropout

对于卷积层,通常使用nn.Dropout2d(或nn.Dropout作用于通道)。更常用的是在全连接层之前使用Dropout。

class CNNWithDropout(nn.Module):
    def __init__(self, dropout_rate=0.5):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(64 * 8 * 8, 256)
        self.dropout = nn.Dropout(dropout_rate)
        self.fc2 = nn.Linear(256, 10)
        self.relu = nn.ReLU()
    
    def forward(self, x):
        x = self.pool(self.relu(self.conv1(x)))
        x = self.pool(self.relu(self.conv2(x)))
        x = x.view(x.size(0), -1)
        x = self.relu(self.fc1(x))
        x = self.dropout(x)   # Dropout放在全连接层后
        x = self.fc2(x)
        return x

4.3 不同Dropout比例对过拟合的抑制效果

def train_with_dropout(dropout_rate, train_loader, val_loader, epochs=30):
    model = CNNWithDropout(dropout_rate=dropout_rate).to(device)
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    history = {'train_loss': [], 'val_loss': [], 'val_acc': []}
    for epoch in range(epochs):
        train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion)
        val_loss, val_acc = evaluate(model, val_loader, criterion)
        history['train_loss'].append(train_loss)
        history['val_loss'].append(val_loss)
        history['val_acc'].append(val_acc)
    return history

# 使用之前的小数据集(500张)实验不同dropout
dropout_rates = [0.0, 0.3, 0.5, 0.7]
histories = {}
for dr in dropout_rates:
    print(f"训练 Dropout rate = {dr}")
    histories[dr] = train_with_dropout(dr, train_loader, val_loader, epochs=30)

# 绘制最终验证准确率对比
final_val_acc = [histories[dr]['val_acc'][-1] for dr in dropout_rates]
plt.bar([str(dr) for dr in dropout_rates], final_val_acc)
plt.xlabel('Dropout Rate')
plt.ylabel('Final Validation Accuracy')
plt.title('Effect of Dropout on Overfitting')
plt.show()

4.4 Dropout的变体

  • DropConnect:随机丢弃权重而非激活值。
  • SpatialDropout:对2D特征图(如图像)整通道丢弃,而非随机像素。在卷积层中效果更好。
  • AlphaDropout:保持自归一化属性的Dropout,用于SELU激活函数。
# SpatialDropout2d: 随机将整个通道置0
spatial_dropout = nn.Dropout2d(p=0.5)
x = torch.randn(2, 3, 32, 32)  # [batch, channels, h, w]
out = spatial_dropout(x)
print("Dropout2d后,某些通道完全为0:", out.shape)

五、归一化层:BatchNorm与LayerNorm

5.1 BatchNorm原理与效果

BatchNorm在batch维度上对每个特征通道进行标准化,使其均值为0、方差为1,然后通过可学习的缩放和平移参数恢复表达能力。它能够:

  • 加速收敛(允许更大学习率)
  • 降低对初始化敏感度
  • 提供轻微的正则化效果(因为batch统计量有噪声)
# BatchNorm2d用于卷积层后
bn_layer = nn.BatchNorm2d(64)  # 输入通道数64
x = torch.randn(16, 64, 32, 32)
out = bn_layer(x)
print(f"均值: {out.mean().item():.4f}, 方差: {out.var().item():.4f}")  # 接近0和1

5.2 BatchNorm的正确位置

通常放在卷积层之后、激活函数之前(Conv -> BN -> ReLU)。也可以放在激活之后,但前者更常见。

class ConvBNReLU(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
        super().__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=False)
        self.bn = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
    
    def forward(self, x):
        return self.relu(self.bn(self.conv(x)))

5.3 LayerNorm:适用于RNN/Transformer

LayerNorm在特征维度上进行标准化,不依赖于batch大小,适用于序列模型(RNN、Transformer)和batch size较小的情况。

# LayerNorm常用于Transformer
ln = nn.LayerNorm(512)  # 特征维度=512
x = torch.randn(2, 10, 512)  # [batch, seq_len, features]
out = ln(x)
print(f"最后一个样本最后一个时间步的均值: {out[0, -1, :].mean().item():.4f}")

BatchNorm vs LayerNorm对比:

特性BatchNormLayerNorm
标准化维度通道维度(跨batch)特征维度(跨通道)
依赖batch size是(batch size小则不稳定)否
适用架构CNNRNN、Transformer、小batch CNN
训练/测试差异是(使用running statistics)否(每样本独立)

5.4 使用BatchNorm抑制过拟合的实验

class CNNWithBN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(32)
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(64)
        self.pool = nn.MaxPool2d(2)
        self.fc = nn.Linear(64 * 8 * 8, 10)
        self.relu = nn.ReLU()
    
    def forward(self, x):
        x = self.pool(self.relu(self.bn1(self.conv1(x))))
        x = self.pool(self.relu(self.bn2(self.conv2(x))))
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x

# 与之前无BN的模型对比(在小数据集上)
model_no_bn = CNNWithDropout(dropout_rate=0.0)  # 无BN,无Dropout
model_bn = CNNWithBN().to(device)
# 训练对比略(实际运行可得BN显著提高验证准确率)

六、权重正则化:L1与L2

6.1 L2正则化(权重衰减)

L2正则化在损失函数中添加权重的平方和项:Loss = original_loss + λ * Σ w^2。它促使权重趋近于0但非绝对0,权重分布更分散。

PyTorch中通过优化器的weight_decay参数实现L2正则化。

# L2正则化(weight_decay)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)

# 手动实现L2损失
def l2_penalty(model, lambda_l2=0.001):
    l2_loss = 0
    for param in model.parameters():
        l2_loss += torch.sum(param ** 2)
    return lambda_l2 * l2_loss

# 在总损失中加入
# total_loss = criterion(outputs, labels) + l2_penalty(model)

6.2 L1正则化

L1正则化添加权重的绝对值之和:Loss = original_loss + λ * Σ |w|,促使权重稀疏(许多权重变为0),可用于特征选择。

def l1_penalty(model, lambda_l1=0.001):
    l1_loss = 0
    for param in model.parameters():
        l1_loss += torch.sum(torch.abs(param))
    return lambda_l1 * l1_loss

# 在训练循环中使用
# loss = criterion(outputs, labels) + l1_penalty(model)

6.3 L1 vs L2 效果可视化

通过一个简单的线性回归示例,展示L1产生稀疏解、L2产生非零小权重。

# 简单的线性回归对比(略)

6.4 权重衰减的选择建议

  • CNN(如ResNet):常用weight_decay=1e-4或5e-4
  • Transformer:常用weight_decay=0.01~0.1(AdamW)
  • 小数据集:适当增大权重衰减
  • 大数据集:可减小权重衰减

七、早停(Early Stopping)

7.1 早停的原理与实现

早停在验证集性能不再提升时停止训练,并恢复最佳模型权重,防止过拟合。

class EarlyStopping:
    def __init__(self, patience=10, min_delta=0.001, restore_best_weights=True):
        self.patience = patience
        self.min_delta = min_delta
        self.restore_best_weights = restore_best_weights
        self.counter = 0
        self.best_score = None
        self.best_weights = None
    
    def __call__(self, val_loss, model):
        if self.best_score is None:
            self.best_score = val_loss
            if self.restore_best_weights:
                self.best_weights = {k: v.cpu().clone() for k, v in model.state_dict().items()}
        elif val_loss > self.best_score - self.min_delta:
            self.counter += 1
            if self.counter >= self.patience:
                if self.restore_best_weights:
                    model.load_state_dict(self.best_weights)
                return True  # 停止训练
        else:
            self.best_score = val_loss
            if self.restore_best_weights:
                self.best_weights = {k: v.cpu().clone() for k, v in model.state_dict().items()}
            self.counter = 0
        return False

# 在训练循环中使用
early_stopping = EarlyStopping(patience=10)
for epoch in range(epochs):
    train_loss, train_acc = train_epoch(...)
    val_loss, val_acc = evaluate(...)
    if early_stopping(val_loss, model):
        print("Early stopping triggered")
        break

7.2 早停与学习率调度的配合

可以与ReduceLROnPlateau结合:先降低学习率,如果多次后仍无改善再早停。


八、数据集扩充(数据增强)

第15课已详细讲解数据增强,这里总结其对过拟合的抑制效果。

# 强数据增强 vs 弱增强
weak_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean, std)
])
strong_transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomCrop(32, padding=4),
    transforms.ColorJitter(0.2, 0.2, 0.2),
    transforms.ToTensor(),
    transforms.Normalize(mean, std)
])

# 使用同样的模型在两种增强下训练,对比过拟合程度

九、模型剪枝基础

模型剪枝通过移除不重要的权重或神经元,减少模型容量,间接防止过拟合。虽然剪枝更多用于压缩模型,但也有正则化效果。

9.1 非结构化剪枝(权重剪枝)

import torch.nn.utils.prune as prune

model = CNNWithDropout(dropout_rate=0.0)  # 一个简单的CNN
# 对conv1的权重进行L1非结构化剪枝,剪掉50%的权重
prune.l1_unstructured(model.conv1, name='weight', amount=0.5)
# 查看剪枝后的权重稀疏度
print(f"卷积层权重稀疏度: {100 * (model.conv1.weight == 0).float().mean().item():.2f}%")
# 剪枝后需要移除重参数化(使其永久生效)
prune.remove(model.conv1, 'weight')

9.2 结构化剪枝(通道剪枝)

# 结构化剪枝:剪除整个卷积核
prune.ln_structured(model.conv1, name='weight', amount=0.3, n=2, dim=0)

剪枝与微调(Pruning + Fine-tuning)可以恢复部分精度,同时减小过拟合。


十、综合实战:组合正则化方法

在CIFAR-10上比较无正则化、仅Dropout、仅BN、L2、以及组合使用。

def get_model_with_regularization(reg_type):
    if reg_type == 'none':
        return CNNWithDropout(dropout_rate=0.0)
    elif reg_type == 'dropout':
        return CNNWithDropout(dropout_rate=0.5)
    elif reg_type == 'bn':
        return CNNWithBN()
    elif reg_type == 'l2':
        model = CNNWithDropout(dropout_rate=0.0)
        return model
    elif reg_type == 'all':
        model = CNNWithBN()
        # 在优化器中添加weight_decay
        return model

# 训练并记录验证准确率
reg_types = ['none', 'dropout', 'bn', 'l2', 'all']
results = {}
for reg in reg_types:
    model = get_model_with_regularization(reg).to(device)
    if reg == 'l2':
        optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
    elif reg == 'all':
        optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
    else:
        optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
    # 使用小数据集快速观察
    train_loader_small = DataLoader(train_dataset_small, batch_size=32, shuffle=True)
    history = {'val_acc': []}
    for epoch in range(30):
        train_epoch(model, train_loader_small, optimizer, criterion)
        _, val_acc = evaluate(model, val_loader, criterion)
        history['val_acc'].append(val_acc)
    results[reg] = history['val_acc']

# 绘制曲线
for reg, acc_list in results.items():
    plt.plot(acc_list, label=reg)
plt.xlabel('Epoch')
plt.ylabel('Validation Accuracy')
plt.legend()
plt.title('Effect of Different Regularization Methods on Overfitting')
plt.show()

十一、难点解析:常见问题与陷阱

11.1 Dropout在训练和测试时的行为不一致

忘记调用model.eval()会导致测试时仍然随机失活,输出不稳定。务必在验证/测试前调用model.eval()。

11.2 BatchNorm的running statistics更新

训练时BatchNorm会累计全局均值和方差,测试时使用这些统计量。如果在model.eval()后仍想更新,需要设置model.train()。多GPU训练时需注意同步。

11.3 权重衰减与学习率的关系

较大的学习率需要配合较大的权重衰减?一般经验:学习率加倍,权重衰减可保持不变或略微增加。

11.4 L1正则化导致梯度不稳定

L1的导数在0处不可导,实际实现中使用次梯度。如果模型收敛困难,可尝试降低L1系数或改用L2。

11.5 早停与模型保存时机

早停应该在验证损失最低时保存模型,而不是停止时。上面的EarlyStopping实现中,当验证损失下降时更新最佳权重。

11.6 数据增强导致验证集泄露

不要在验证集上使用随机增强(如RandomHorizontalFlip),否则评估结果不可重复。


十二、课后总结

正则化技术速查表

方法原理PyTorch实现适用场景
Dropout随机丢弃神经元nn.Dropout(p)全连接层、防止过拟合
BatchNorm标准化+缩放平移nn.BatchNorm2dCNN,加速收敛+正则化
LayerNorm特征维度标准化nn.LayerNormRNN、Transformer
L2正则化权重平方惩罚weight_decay参数通用
L1正则化权重绝对值和惩罚手动添加特征稀疏化
早停验证损失不再下降时停止自定义回调所有任务
数据增强在线生成变换样本transforms图像任务
模型剪枝移除不重要权重torch.nn.utils.prune压缩+轻度正则化

过拟合诊断清单

  • 训练准确率接近100%,验证准确率低 → 过拟合
  • 训练损失持续下降,验证损失回升 → 过拟合
  • 训练和验证准确率都很低 → 欠拟合
  • 训练和验证准确率都较高且接近 → 良好拟合

正则化强度调整指南

  • 过拟合严重:增加Dropout比例、增加权重衰减、使用数据增强、减小模型容量
  • 欠拟合:降低正则化强度、增加模型容量、增加训练轮次

检查清单

  • 能根据曲线判断过拟合/欠拟合
  • 掌握Dropout的使用及训练/测试差异
  • 理解BatchNorm和LayerNorm的区别与应用
  • 会设置weight_decay实现L2正则化
  • 能实现早停回调
  • 了解模型剪枝的基本概念
  • 会组合使用多种正则化方法提升泛化能力

十三、课后作业

作业1:不同Dropout比例对过拟合的影响

在小数据集CIFAR-10-500上,训练带Dropout的CNN(dropout=0, 0.3, 0.5, 0.7),记录验证准确率曲线。分析哪个比例最合适。

作业2:BatchNorm的位置实验

在CNN中尝试三种配置:Conv->BN->ReLU、Conv->ReLU->BN、BN->Conv->ReLU。观察训练收敛速度和最终准确率差异。

作业3:L1与L2正则化对权重分布的影响

训练两个简单的线性回归模型,分别添加L1和L2正则化。训练结束后,可视化权重直方图,观察L1产生的稀疏性。

作业4:早停的实现与效果

在过拟合模型上加入早停(patience=10),与不加早停的模型对比,观察验证准确率的最高点与最终停止点的差异。

作业5:组合正则化实验

设计实验比较以下配置的验证准确率:

  • 无正则化
  • 仅Dropout(0.5)
  • 仅BatchNorm
  • Dropout(0.5) + BN
  • Dropout(0.5) + BN + L2

作业6:模型剪枝的轻度正则化效果

训练一个全连接网络在MNIST上(故意过拟合),然后进行50%的非结构化剪枝,再微调5个epoch。对比剪枝前后的验证准确率和训练-验证差距。


十四、下一课预告

第20课我们将学习PyTorch训练日志可视化与训练监控,内容包括:

  • TensorBoard完整使用教程(标量、直方图、图像、网络结构)
  • 训练损失/准确率曲线绘制
  • 特征图可视化
  • 参数分布直方图
  • 实时监控训练进程

学会可视化,你将能远程监控训练过程,及时发现问题。


🔗《精讲25课|PyTorch 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

更多推荐