第19课:PyTorch|模型正则化与过拟合全套解决方案【让泛化能力超越记忆】

文章目录
📖 课前导读
为什么模型会“死记硬背”?
想象你准备一场考试:如果你只死记硬背题库中的原题,考试时题目稍作改动你就不会了。这就像过拟合——模型把训练样本的噪声和细节都记住了,却没学到背后的规律。
深度神经网络拥有海量参数,理论上可以拟合任意复杂的数据分布。但这种强大的表达能力是一把双刃剑:当训练数据有限或噪声较多时,模型很容易陷入“过度学习”的状态,把每个样本的个性当成了共性。
过拟合的表现非常典型:训练损失持续下降,训练准确率接近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对比:
| 特性 | BatchNorm | LayerNorm |
|---|---|---|
| 标准化维度 | 通道维度(跨batch) | 特征维度(跨通道) |
| 依赖batch size | 是(batch size小则不稳定) | 否 |
| 适用架构 | CNN | RNN、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.BatchNorm2d | CNN,加速收敛+正则化 |
| LayerNorm | 特征维度标准化 | nn.LayerNorm | RNN、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 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
更多推荐


所有评论(0)