1. 训练流程
flowchart LR
A[原始数据] --> B[划分 train / val / test]
B --> C[Dataset 与 transform]
C --> D[DataLoader]
D --> E[模型前向]
E --> F[损失与指标]
F --> G[反向传播与参数更新]
G --> H[验证与选模]
H --> I[独立测试]
I --> J[保存配置、权重与结果]
三个数据集的职责必须分开:
- 训练集:用于计算梯度和更新参数。
- 验证集:用于调参、早停和选择最佳检查点。
- 测试集:只在方案冻结后做最终估计,不能每轮参与选模。
2. Dataset 与 DataLoader 的边界
Dataset 回答“第 i 个样本是什么”,DataLoader 回答“如何组成批次并送给训练循环”。
from torch.utils.data import DataLoader, Dataset
import torch
class ToyDataset(Dataset):
def __init__(self):
self.x = torch.arange(100, dtype=torch.float32).unsqueeze(1)
self.y = (self.x[:, 0] % 2).long()
def __len__(self):
return len(self.x)
def __getitem__(self, index):
return self.x[index], self.y[index]
常用参数:
| 参数 | 作用 | 实践建议 |
|---|---|---|
batch_size |
每批样本数 | 受显存、吞吐和泛化需求共同影响 |
shuffle |
每轮重排索引 | 训练集通常开启,验证和测试关闭 |
num_workers |
数据加载子进程数 | 从 0 开始调试,再通过实测提高 |
pin_memory |
把 CPU 张量放到页锁定内存 | CUDA 训练时通常值得测试 |
drop_last |
丢弃最后一个不足批次 | BatchNorm 或固定形状需求下再开启 |
collate_fn |
自定义组批 | 可变长序列、图或复杂样本结构 |
persistent_workers |
跨 epoch 保留 worker | num_workers > 0 且多轮训练时可测试 |
Windows 多进程加载
Windows 默认使用 spawn 创建 worker,不是“只能设为 0”。使用正数时应:
def main():
dataset = ToyDataset()
loader = DataLoader(dataset, batch_size=16, num_workers=2)
for batch in loader:
pass
if __name__ == "__main__":
main()
自定义 Dataset、collate_fn 和 worker_init_fn 应定义在模块顶层,避免使用不能被 pickle 的 lambda。Notebook 或调试阶段先使用 num_workers=0,错误栈更清楚。
3. 数据预处理与数据增强
- 确定性预处理:解码、缩放、标准化、格式转换。
- 随机增强:随机裁剪、翻转、颜色扰动等,只应用于训练集。
- 验证/测试变换:保持确定性,并与模型预训练权重要求的输入范围一致。
迁移学习时不要凭经验手写预处理。TorchVision 的权重对象同时提供匹配的推理变换:
from torchvision.models import ResNet18_Weights
weights = ResNet18_Weights.DEFAULT
preprocess = weights.transforms()
4. 一份可复用的分类 epoch 模板
这份实现按样本数累计损失和正确数,避免“先算每批准确率、再对批次取平均”带来的最后小批次偏差。
from contextlib import nullcontext
import torch
def run_epoch(model, loader, loss_fn, device, optimizer=None):
training = optimizer is not None
model.train(training)
loss_sum = 0.0
correct = 0
sample_count = 0
grad_context = nullcontext() if training else torch.inference_mode()
with grad_context:
for inputs, targets in loader:
inputs = inputs.to(device, non_blocking=True)
targets = targets.to(device, non_blocking=True)
if training:
optimizer.zero_grad(set_to_none=True)
logits = model(inputs)
loss = loss_fn(logits, targets)
if training:
loss.backward()
optimizer.step()
batch_size = targets.size(0)
loss_sum += loss.item() * batch_size
correct += (logits.argmax(dim=1) == targets).sum().item()
sample_count += batch_size
if sample_count == 0:
raise ValueError("loader contains no samples")
return {
"loss": loss_sum / sample_count,
"accuracy": correct / sample_count,
}
注意:
CrossEntropyLoss接收未归一化的 logits,不要在模型末尾先做softmax()。model.train(training)会影响 Dropout 和 BatchNorm,但不负责开关自动微分。- 回归、多标签分类或分割任务需要替换指标计算,不能直接复用
argmax()。
5. 训练、验证和选择最佳模型
import copy
import torch
best_val_loss = float("inf")
best_state = None
for epoch in range(num_epochs):
train_metrics = run_epoch(
model, train_loader, loss_fn, device, optimizer=optimizer
)
val_metrics = run_epoch(
model, val_loader, loss_fn, device
)
if val_metrics["loss"] < best_val_loss:
best_val_loss = val_metrics["loss"]
best_state = copy.deepcopy(model.state_dict())
print(
f"epoch={epoch + 1:03d} "
f"train_loss={train_metrics['loss']:.4f} "
f"val_loss={val_metrics['loss']:.4f} "
f"val_acc={val_metrics['accuracy']:.4f}"
)
if best_state is None:
raise RuntimeError("no valid checkpoint was selected")
model.load_state_dict(best_state)
test_metrics = run_epoch(model, test_loader, loss_fn, device)
如果直接保存到磁盘:
torch.save(model.state_dict(), "best_model.pth")
state_dict = torch.load(
"best_model.pth",
map_location=device,
weights_only=True,
)
model.load_state_dict(state_dict)
model.eval()
只保存权重时要同时保存足够的模型配置,例如类别数、模型版本、输入变换和标签映射,否则无法可靠重建模型。
完整训练检查点
需要断点续训时,至少保存:
checkpoint = {
"epoch": epoch,
"model": model.state_dict(),
"optimizer": optimizer.state_dict(),
"best_val_loss": best_val_loss,
"config": config,
}
torch.save(checkpoint, "checkpoint.pth")
学习率调度器、混合精度缩放器和随机数状态如参与训练,也应按恢复需求纳入检查点。
6. 学习率调度器的调用位置
StepLR等按 epoch 更新:通常在每个 epoch 训练结束后scheduler.step()。ReduceLROnPlateau依赖验证指标:scheduler.step(val_loss)。- OneCycle 等按 step 更新:通常每个训练 batch 调用一次。
不要把所有 scheduler 都套用同一种调用方式。
7. 监控什么
最小可用记录:
- epoch、global step、学习率;
- 训练和验证 loss;
- 与任务匹配的指标,如 accuracy、F1、ROC-AUC;
- 最佳验证 epoch 和对应测试结果;
- 数据划分、随机种子、模型配置和运行环境。
诊断逻辑:
| 现象 | 优先检查 |
|---|---|
| 训练 loss 不下降 | 标签、损失、学习率、梯度是否存在、输入尺度 |
| 训练好而验证差 | 数据泄漏、过拟合、划分偏差、增强不一致 |
| loss 为 NaN/Inf | 数据异常、学习率、除零、数值范围、梯度爆炸 |
| GPU 利用率低 | 数据加载、batch 太小、CPU 预处理、同步点 |
| 复现实验失败 | 数据划分、随机种子、非确定性算子、版本差异 |
8. 从“跑通”到“验收”
一次运行结束不等于训练完成。交付前至少检查:
- [ ] 数据集划分无交叉,测试集未参与调参。
- [ ] 训练、验证、测试的变换符合各自职责。
- [ ] 指标按样本而不是按 batch 等权累计。
- [ ] 最佳检查点确实来自验证集。
- [ ] 重新加载检查点后结果可复现。
- [ ] 保存了类别映射、输入尺寸、归一化参数和配置。
- [ ] 对类别不平衡任务补充混淆矩阵、macro-F1 等指标。
- [ ] 记录失败实验和数据异常,而不是只保留最好数字。
来源映射
标准训练过程.md第二周.mdLeNet_MNIST.md/LeNet_MNIST.ipynbd2l_Study.ipynbkaggle-cifar10.ipynb- PyTorch DataLoader 文档
- PyTorch 保存与加载模型