模型训练与评估
PyTorch 提供了灵活而强大的训练框架。本文将系统地介绍从训练循环搭建、评估指标实现、过拟合诊断到 TensorBoard 可视化的完整流程,并通过一个 CIFAR-10 图像分类案例串联所有知识点。
完整训练循环模板
PyTorch 的训练循环是所有深度学习任务的核心骨架,包含以下五个关键步骤:forward(前向传播)→ loss(计算损失)→ backward(反向传播)→ step(更新参数)→ zero_grad(清零梯度)。
基本训练循环
python
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 定义模型、损失函数、优化器
model = MyModel()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# 2. 训练循环
num_epochs = 10
for epoch in range(num_epochs):
model.train() # 设置为训练模式
running_loss = 0.0
for batch_idx, (inputs, targets) in enumerate(train_loader):
# ---- forward ----
outputs = model(inputs)
# ---- loss ----
loss = criterion(outputs, targets)
# ---- backward ----
optimizer.zero_grad() # 清零梯度(重要!放在 backward 之前或之后均可)
loss.backward() # 反向传播,计算梯度
# ---- step ----
optimizer.step() # 更新模型参数
running_loss += loss.item()
avg_loss = running_loss / len(train_loader)
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")zero_grad 放在哪里?
optimizer.zero_grad() 的位置有两种常见写法,功能等价:
python
# 写法一:在 backward 之前清零(推荐,语义更清晰)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 写法二:在 step 之后清零
loss.backward()
optimizer.step()
optimizer.zero_grad()注意:如果不清零梯度,梯度会在多次 backward 中累加。这在某些场景(如梯度累积)中是有意为之的,但在常规训练中必须每次清零。
带验证的完整训练循环
python
for epoch in range(num_epochs):
# ---------- 训练阶段 ----------
model.train()
train_loss = 0.0
train_correct = 0
train_total = 0
for inputs, targets in train_loader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
train_loss += loss.item()
_, predicted = outputs.max(1)
train_total += targets.size(0)
train_correct += predicted.eq(targets).sum().item()
train_acc = 100.0 * train_correct / train_total
# ---------- 验证阶段 ----------
model.eval()
val_loss = 0.0
val_correct = 0
val_total = 0
with torch.no_grad():
for inputs, targets in val_loader:
outputs = model(inputs)
loss = criterion(outputs, targets)
val_loss += loss.item()
_, predicted = outputs.max(1)
val_total += targets.size(0)
val_correct += predicted.eq(targets).sum().item()
val_acc = 100.0 * val_correct / val_total
print(f"Epoch [{epoch+1}/{num_epochs}] "
f"Train Loss: {train_loss/len(train_loader):.4f} "
f"Train Acc: {train_acc:.2f}% "
f"Val Loss: {val_loss/len(val_loader):.4f} "
f"Val Acc: {val_acc:.2f}%")训练模式与评估模式
PyTorch 中部分模块在训练和评估时行为不同,通过 model.train() 和 model.eval() 切换。
两种模式的差异
| 特性 | model.train() | model.eval() |
|---|---|---|
| Dropout | 随机丢弃神经元 | 所有神经元参与计算 |
| BatchNorm | 使用当前 batch 的均值/方差 | 使用训练时累积的运行均值/方差 |
| 梯度计算 | 计算梯度 | 需配合 torch.no_grad() |
受影响的常见模块
python
# Dropout —— 训练时随机置零,评估时全部保留
drop = nn.Dropout(p=0.5)
drop.train()
print(drop(torch.ones(10))) # 大约一半被置零
drop.eval()
print(drop(torch.ones(10))) # 全部保留(但值变为 1-p 的缩放后值,eval 下不做缩放)
# BatchNorm —— 训练时用 batch 统计量,评估时用全局统计量
bn = nn.BatchNorm1d(100)
bn.train() # 使用当前 batch 的 mean / var
bn.eval() # 使用 running_mean / running_var常见陷阱
python
# 错误:忘记切换模式导致验证结果不稳定
model.eval() # 忘记调用,Dropout 和 BatchNorm 仍在训练模式
with torch.no_grad():
outputs = model(val_inputs)
# 正确做法
model.eval()
with torch.no_grad():
outputs = model(val_inputs)最佳实践:在验证和测试代码之前,始终显式调用
model.eval(),结束后用model.train()切回。
验证与评估流程
完整评估函数
python
def evaluate(model, dataloader, criterion, device='cpu'):
"""通用评估函数"""
model.eval()
total_loss = 0.0
all_preds = []
all_targets = []
all_probs = []
with torch.no_grad():
for inputs, targets in dataloader:
inputs, targets = inputs.to(device), targets.to(device)
outputs = model(inputs)
loss = criterion(outputs, targets)
total_loss += loss.item() * inputs.size(0)
probs = torch.softmax(outputs, dim=1)
_, predicted = probs.max(1)
all_preds.extend(predicted.cpu().numpy())
all_targets.extend(targets.cpu().numpy())
all_probs.extend(probs.cpu().numpy())
avg_loss = total_loss / len(dataloader.dataset)
return avg_loss, all_preds, all_targets, all_probs使用模型进行推理
python
def predict(model, image, device='cpu'):
"""单张图片推理"""
model.eval()
with torch.no_grad():
image = image.unsqueeze(0).to(device) # 添加 batch 维度
output = model(image)
prob = torch.softmax(output, dim=1)
predicted_class = prob.argmax(dim=1).item()
confidence = prob.max().item()
return predicted_class, confidence常用评估指标实现
分类指标
Accuracy(准确率)
python
def accuracy(outputs, targets):
"""计算准确率"""
_, predicted = outputs.max(1)
correct = predicted.eq(targets).sum().item()
return correct / targets.size(0)Precision、Recall、F1-Score
python
def precision_recall_f1(outputs, targets, num_classes, average='macro'):
"""
计算 Precision、Recall、F1-Score
average: 'macro'(各类别等权平均), 'micro'(全局计算), 'weighted'(按类别频率加权)
"""
_, predicted = outputs.max(1)
precision_list = []
recall_list = []
f1_list = []
for cls in range(num_classes):
tp = ((predicted == cls) & (targets == cls)).sum().float()
fp = ((predicted == cls) & (targets != cls)).sum().float()
fn = ((predicted != cls) & (targets == cls)).sum().float()
precision = tp / (tp + fp + 1e-8)
recall = tp / (tp + fn + 1e-8)
f1 = 2 * precision * recall / (precision + recall + 1e-8)
precision_list.append(precision.item())
recall_list.append(recall.item())
f1_list.append(f1.item())
if average == 'macro':
return sum(precision_list) / num_classes, sum(recall_list) / num_classes, sum(f1_list) / num_classes
elif average == 'micro':
# 微平均:将所有类别的 TP/FP/FN 汇总后计算
pass # 逻辑类似,汇总后一次性计算Confusion Matrix(混淆矩阵)
python
import numpy as np
def confusion_matrix(outputs, targets, num_classes):
"""计算混淆矩阵"""
_, predicted = outputs.max(1)
cm = np.zeros((num_classes, num_classes), dtype=np.int64)
for t, p in zip(targets.numpy(), predicted.numpy()):
cm[t][p] += 1
return cm
def plot_confusion_matrix(cm, class_names):
"""可视化混淆矩阵"""
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 8))
im = ax.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
ax.set_title('Confusion Matrix')
plt.colorbar(im, ax=ax)
tick_marks = np.arange(len(class_names))
ax.set_xticks(tick_marks)
ax.set_xticklabels(class_names, rotation=45)
ax.set_yticks(tick_marks)
ax.set_yticklabels(class_names)
ax.set_ylabel('True Label')
ax.set_xlabel('Predicted Label')
# 在格子中显示数值
for i in range(len(class_names)):
for j in range(len(class_names)):
ax.text(j, i, str(cm[i][j]), ha='center', va='center',
color='white' if cm[i][j] > cm.max() / 2 else 'black')
plt.tight_layout()
plt.show()AUC-ROC
python
from sklearn.metrics import roc_auc_score, roc_curve
import matplotlib.pyplot as plt
def compute_auc_roc(all_probs, all_targets, num_classes):
"""计算多分类 AUC-ROC(One-vs-Rest)"""
from sklearn.preprocessing import label_binarize
targets_onehot = label_binarize(all_targets, classes=list(range(num_classes)))
probs = np.array(all_probs)
# 宏平均 AUC
auc = roc_auc_score(targets_onehot, probs, multi_class='ovr', average='macro')
return auc
def plot_roc_curve(all_probs, all_targets, num_classes, class_names):
"""绘制 ROC 曲线"""
from sklearn.preprocessing import label_binarize
targets_onehot = label_binarize(all_targets, classes=list(range(num_classes)))
probs = np.array(all_probs)
plt.figure(figsize=(10, 8))
for i in range(num_classes):
fpr, tpr, _ = roc_curve(targets_onehot[:, i], probs[:, i])
auc_val = roc_auc_score(targets_onehot[:, i], probs[:, i])
plt.plot(fpr, tpr, label=f'{class_names[i]} (AUC={auc_val:.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve (One-vs-Rest)')
plt.legend()
plt.grid(True)
plt.show()回归指标
python
import torch.nn.functional as F
def mse(outputs, targets):
"""均方误差"""
return F.mse_loss(outputs, targets).item()
def rmse(outputs, targets):
"""均方根误差"""
return torch.sqrt(F.mse_loss(outputs, targets)).item()
def mae(outputs, targets):
"""平均绝对误差"""
return F.l1_loss(outputs, targets).item()
def r_squared(outputs, targets):
"""决定系数 R²"""
ss_res = ((targets - outputs) ** 2).sum()
ss_tot = ((targets - targets.mean()) ** 2).sum()
return (1 - ss_res / (ss_tot + 1e-8)).item()
# 使用示例
y_pred = torch.tensor([2.5, 0.0, 2.1, 7.8])
y_true = torch.tensor([3.0, -0.5, 2.0, 7.5])
print(f"MSE: {mse(y_pred, y_true):.4f}")
print(f"RMSE: {rmse(y_pred, y_true):.4f}")
print(f"MAE: {mae(y_pred, y_true):.4f}")
print(f"R²: {r_squared(y_pred, y_true):.4f}")过拟合与欠拟合诊断
学习曲线分析
通过观察训练集和验证集的 loss/accuracy 变化,可以判断模型状态:
| 现象 | 诊断 | 解决策略 |
|---|---|---|
| 训练 loss 高,验证 loss 高 | 欠拟合 | 增大模型容量、降低正则化、延长训练 |
| 训练 loss 低,验证 loss 高且差距大 | 过拟合 | 增加正则化、数据增强、减小模型容量 |
| 训练 loss 低,验证 loss 低且差距小 | 良好拟合 | 模型状态理想 |
学习曲线绘制
python
import matplotlib.pyplot as plt
def plot_learning_curves(train_losses, val_losses, train_accs=None, val_accs=None):
"""绘制学习曲线"""
epochs = range(1, len(train_losses) + 1)
fig, axes = plt.subplots(1, 2 if train_accs else 1, figsize=(14, 5))
if not isinstance(axes, (list, np.ndarray)):
axes = [axes]
# Loss 曲线
axes[0].plot(epochs, train_losses, 'b-', label='Train Loss')
axes[0].plot(epochs, val_losses, 'r-', label='Val Loss')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].set_title('Loss Curve')
axes[0].legend()
axes[0].grid(True)
# Accuracy 曲线(如果有)
if train_accs and val_accs:
axes[1].plot(epochs, train_accs, 'b-', label='Train Acc')
axes[1].plot(epochs, val_accs, 'r-', label='Val Acc')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Accuracy (%)')
axes[1].set_title('Accuracy Curve')
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
plt.show()正则化策略
Dropout
python
class RegularizedModel(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim, dropout_rate=0.5):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Dropout(p=dropout_rate), # 全连接层后添加 Dropout
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Dropout(p=dropout_rate),
nn.Linear(hidden_dim, output_dim)
)
def forward(self, x):
return self.net(x)
# 不同层可以设置不同 dropout 率
# 卷积网络中常用 nn.Dropout2d,按通道置零
conv_dropout = nn.Dropout2d(p=0.25)Weight Decay(L2 正则化)
python
# 在优化器中设置 weight_decay 参数
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4)
# 注意:通常不对 bias 和 BatchNorm 的参数施加 weight decay
def get_param_groups(model, weight_decay):
"""区分需要和不需要 weight decay 的参数"""
decay_params = []
no_decay_params = []
for name, param in model.named_parameters():
if 'bias' in name or 'bn' in name or 'norm' in name:
no_decay_params.append(param)
else:
decay_params.append(param)
return [
{'params': decay_params, 'weight_decay': weight_decay},
{'params': no_decay_params, 'weight_decay': 0.0}
]
param_groups = get_param_groups(model, weight_decay=1e-4)
optimizer = optim.Adam(param_groups, lr=1e-3)数据增强
python
from torchvision import transforms
# 训练集使用数据增强
train_transform = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
transforms.RandomRotation(degrees=15),
transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]),
transforms.RandomErasing(p=0.25), # CutOut 变体,在 ToTensor 之后
])
# 验证集不做增强
val_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]),
])Early Stopping
python
class EarlyStopping:
"""早停机制:验证指标不再改善时提前终止训练"""
def __init__(self, patience=10, min_delta=0.0, mode='min', verbose=True):
"""
Args:
patience: 容忍多少个 epoch 没有改善
min_delta: 最小改善幅度
mode: 'min'(指标越小越好,如 loss)或 'max'(越大越好,如 accuracy)
verbose: 是否打印提示信息
"""
self.patience = patience
self.min_delta = min_delta
self.mode = mode
self.verbose = verbose
self.counter = 0
self.best_score = None
self.early_stop = False
self.best_model_state = None
def __call__(self, score, model):
if self.best_score is None:
self.best_score = score
self.best_model_state = model.state_dict().copy()
elif self._is_improvement(score):
self.best_score = score
self.best_model_state = model.state_dict().copy()
self.counter = 0
if self.verbose:
print(f" Validation improved. Saving model...")
else:
self.counter += 1
if self.verbose:
print(f" No improvement for {self.counter}/{self.patience} epochs.")
if self.counter >= self.patience:
self.early_stop = True
if self.verbose:
print(" Early stopping triggered!")
def _is_improvement(self, score):
if self.mode == 'min':
return score < self.best_score - self.min_delta
else:
return score > self.best_score + self.min_delta
def load_best_model(self, model):
"""恢复最优模型权重"""
model.load_state_dict(self.best_model_state)
return model
# 使用示例
early_stopping = EarlyStopping(patience=10, mode='max', verbose=True)
for epoch in range(num_epochs):
train_one_epoch(model, train_loader, optimizer, criterion)
val_acc = evaluate(model, val_loader, criterion)
early_stopping(val_acc, model)
if early_stopping.early_stop:
print(f"Early stopping at epoch {epoch+1}")
break
# 恢复最优模型
model = early_stopping.load_best_model(model)TensorBoard 可视化
TensorBoard 是训练过程可视化的强大工具,PyTorch 通过 torch.utils.tensorboard 提供原生支持。
初始化 SummaryWriter
python
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter(log_dir='runs/experiment_1')记录 Loss 和 Accuracy 曲线
python
for epoch in range(num_epochs):
train_loss = train_one_epoch(...)
val_loss, val_acc = evaluate(...)
# 使用 add_scalar 记录标量
writer.add_scalar('Loss/train', train_loss, epoch)
writer.add_scalar('Loss/val', val_loss, epoch)
writer.add_scalar('Accuracy/val', val_acc, epoch)
# 学习率记录
writer.add_scalar('LR', optimizer.param_groups[0]['lr'], epoch)
writer.close()在同一图表中对比多个实验
python
# 使用不同的 log_dir 或 add_hparams
for lr in [1e-2, 1e-3, 1e-4]:
writer = SummaryWriter(f'runs/lr_{lr}')
# ... 训练代码 ...
writer.close()记录模型图
python
# 将模型结构写入 TensorBoard
dummy_input = torch.randn(1, 3, 32, 32)
writer.add_graph(model, dummy_input)
writer.close()记录图像
python
from torchvision.utils import make_grid
# 记录一个 batch 的训练图片
dataiter = iter(train_loader)
images, labels = next(dataiter)
# add_images 会自动将多张图片拼成网格
writer.add_image('Train Images', make_grid(images, nrow=8), global_step=0)
# 记录模型的卷积核权重
def log_conv_weights(model, writer, epoch):
for name, param in model.named_parameters():
if 'conv' in name and 'weight' in name:
# 取前 16 个卷积核
kernels = param[:16].detach().cpu()
# 归一化到 [0, 1]
kernels = (kernels - kernels.min()) / (kernels.max() - kernels.min() + 1e-8)
writer.add_images(f'Weights/{name}', kernels.unsqueeze(1), epoch)记录直方图
python
def log_histograms(model, writer, epoch):
"""记录参数和梯度的分布"""
for name, param in model.named_parameters():
if param.grad is not None:
writer.add_histogram(f'Gradients/{name}', param.grad, epoch)
writer.add_histogram(f'Parameters/{name}', param.data, epoch)启动 TensorBoard
bash
# 在终端运行
tensorboard --logdir=runs --port=6006
# 然后在浏览器中访问 http://localhost:6006完整图像分类训练案例(CIFAR-10)
以下是一个完整的、可直接运行的 CIFAR-10 图像分类训练代码:
python
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.tensorboard import SummaryWriter
import time
# ==================== 1. 超参数配置 ====================
class Config:
batch_size = 128
num_epochs = 50
learning_rate = 0.001
weight_decay = 1e-4
num_classes = 10
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
log_dir = 'runs/cifar10_resnet'
config = Config()
# ==================== 2. 数据准备 ====================
train_transform = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)),
])
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)),
])
train_dataset = torchvision.datasets.CIFAR10(
root='./data', train=True, download=True, transform=train_transform)
test_dataset = torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=test_transform)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=config.batch_size, shuffle=True, num_workers=4)
test_loader = torch.utils.data.DataLoader(
test_dataset, batch_size=config.batch_size, shuffle=False, num_workers=4)
classes = ('plane', 'car', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck')
# ==================== 3. 模型定义(简化版 ResNet) ====================
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 1, stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x)
return self.relu(out)
class SimpleResNet(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.conv1 = nn.Conv2d(3, 64, 3, 1, 1, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.layer1 = self._make_layer(64, 64, 2, stride=1)
self.layer2 = self._make_layer(64, 128, 2, stride=2)
self.layer3 = self._make_layer(128, 256, 2, stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(256, num_classes)
def _make_layer(self, in_ch, out_ch, num_blocks, stride):
layers = [BasicBlock(in_ch, out_ch, stride)]
for _ in range(1, num_blocks):
layers.append(BasicBlock(out_ch, out_ch))
return nn.Sequential(*layers)
def forward(self, x):
x = self.relu(self.bn1(self.conv1(x)))
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
return self.fc(x)
# ==================== 4. 训练与评估 ====================
model = SimpleResNet(config.num_classes).to(config.device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=config.learning_rate,
weight_decay=config.weight_decay)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=config.num_epochs)
writer = SummaryWriter(config.log_dir)
best_acc = 0.0
for epoch in range(config.num_epochs):
# ---- 训练 ----
model.train()
train_loss = 0.0
train_correct = 0
train_total = 0
start_time = time.time()
for images, labels in train_loader:
images, labels = images.to(config.device), labels.to(config.device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
_, predicted = outputs.max(1)
train_total += labels.size(0)
train_correct += predicted.eq(labels).sum().item()
scheduler.step()
train_acc = 100.0 * train_correct / train_total
avg_train_loss = train_loss / len(train_loader)
# ---- 测试 ----
model.eval()
test_loss = 0.0
test_correct = 0
test_total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(config.device), labels.to(config.device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = outputs.max(1)
test_total += labels.size(0)
test_correct += predicted.eq(labels).sum().item()
test_acc = 100.0 * test_correct / test_total
avg_test_loss = test_loss / len(test_loader)
elapsed = time.time() - start_time
# ---- 日志 ----
writer.add_scalar('Loss/train', avg_train_loss, epoch)
writer.add_scalar('Loss/test', avg_test_loss, epoch)
writer.add_scalar('Accuracy/train', train_acc, epoch)
writer.add_scalar('Accuracy/test', test_acc, epoch)
writer.add_scalar('LR', scheduler.get_last_lr()[0], epoch)
print(f"Epoch [{epoch+1}/{config.num_epochs}] "
f"Train Loss: {avg_train_loss:.4f} Acc: {train_acc:.2f}% | "
f"Test Loss: {avg_test_loss:.4f} Acc: {test_acc:.2f}% | "
f"Time: {elapsed:.1f}s")
# ---- 保存最优模型 ----
if test_acc > best_acc:
best_acc = test_acc
torch.save(model.state_dict(), 'best_model.pth')
print(f" => Saved best model with accuracy: {best_acc:.2f}%")
writer.close()
print(f"Training complete. Best accuracy: {best_acc:.2f}%")训练技巧汇总
Learning Rate Warmup
python
class WarmupCosineScheduler:
"""带 Warmup 的余弦退火调度器"""
def __init__(self, optimizer, warmup_epochs, total_epochs, base_lr, min_lr=1e-6):
self.optimizer = optimizer
self.warmup_epochs = warmup_epochs
self.total_epochs = total_epochs
self.base_lr = base_lr
self.min_lr = min_lr
def step(self, epoch):
import math
if epoch < self.warmup_epochs:
# 线性 warmup
lr = self.base_lr * (epoch + 1) / self.warmup_epochs
else:
# 余弦退火
progress = (epoch - self.warmup_epochs) / (self.total_epochs - self.warmup_epochs)
lr = self.min_lr + 0.5 * (self.base_lr - self.min_lr) * (1 + math.cos(math.pi * progress))
for param_group in self.optimizer.param_groups:
param_group['lr'] = lr
return lrGradient Accumulation
当显存不足以容纳大 batch 时,通过梯度累积模拟大 batch 训练:
python
accumulation_steps = 4 # 累积 4 个 mini-batch 的梯度,等效于 4 倍 batch_size
optimizer.zero_grad()
for i, (inputs, targets) in enumerate(train_loader):
outputs = model(inputs)
loss = criterion(outputs, targets)
loss = loss / accumulation_steps # 归一化 loss
loss.backward()
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
# 处理最后不足 accumulation_steps 的部分
if (i + 1) % accumulation_steps != 0:
optimizer.step()
optimizer.zero_grad()Mixed Precision Training(混合精度训练)
混合精度训练使用 FP16 加速计算,同时保持 FP32 的关键操作以确保数值稳定性:
python
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
model = model.cuda()
for epoch in range(num_epochs):
model.train()
for inputs, targets in train_loader:
inputs, targets = inputs.cuda(), targets.cuda()
optimizer.zero_grad()
# 自动混合精度前向传播
with autocast():
outputs = model(inputs)
loss = criterion(outputs, targets)
# 缩放 loss 后反向传播
scaler.scale(loss).backward()
# 先 unscale 再裁剪梯度(可选)
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 更新参数
scaler.step(optimizer)
scaler.update() # 更新缩放因子PyTorch 2.x torch.compile 加速
python
# PyTorch 2.0+ 的编译优化,可显著加速训练
model = torch.compile(model, mode='reduce-overhead')
# mode 选项:
# 'default' - 默认优化
# 'reduce-overhead' - 减少 Python 开销,适合小 batch
# 'max-autotune' - 最大化优化,编译较慢但运行最快