训练循环:前向、反向、更新一步到位
本教程共 60 篇 · 第 16 篇 · 更新于 2026-08-17 · 约 3 分钟阅读
本节目标:把模型、损失函数、优化器串起来,写出标准训练循环,并搞懂每一步为什么不能省。
训练循环:一切的核心
前面几章我们分别学了模型、损失函数、优化器。把它们组装起来的那段代码,就是训练循环(training loop)。
无论多复杂的训练代码,剥开外壳都长这样:
- 清空梯度
- 前向传播:模型算预测
- 计算损失:损失函数打分
- 反向传播:算出每个参数的梯度
- 更新参数:优化器迈一步
这五步反复执行,模型就一点点变聪明。先记住骨架,后面所有变体都是它的加料。
最小可运行的训练示例
光说不练假把式。下面是一段完整的、可以直接跑的训练代码。我们随机造一批数据,让一个小网络去学「输入三个数,输出它们的和」这个简单映射:
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 造数据:输入 3 个数,标签是它们的和
x = torch.randn(500, 3)
y = x.sum(dim=1, keepdim=True)
# 2. 定义模型
model = nn.Sequential(
nn.Linear(3, 16),
nn.ReLU(),
nn.Linear(16, 1),
)
# 3. 损失函数和优化器
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 4. 训练循环
for epoch in range(200):
optimizer.zero_grad() # 清梯度
pred = model(x) # 前向
loss = criterion(pred, y) # 算损失
loss.backward() # 反向
optimizer.step() # 更新
if epoch % 40 == 0:
print(f"epoch {epoch:3d}, loss = {loss.item():.4f}")
跑起来你会看到 loss 一路下降,最后趋近于 0。说明网络真的学会了求和。
五步逐一拆解
为什么先清梯度?
第 7 章讲过,backward() 算出的梯度是累加到 .grad 上的。不清空的话,这一步的梯度会混进上一步的旧账,方向就偏了。
optimizer.zero_grad(set_to_none=True) # 推荐:梯度设为 None,省显存
Note一个 batch 更新一次,
zero_grad()就放在 batch 循环的开头。千万别放到 epoch 循环外面,那是经典 bug。
前向与损失:算分
model(x) 就是前向传播(forward pass):数据流过各层,得到预测。再交给损失函数打分。这步没什么花样,注意输入形状对上就行。
反向:传播梯度
loss.backward() 是反向传播(backward pass)。PyTorch 的自动求导引擎从损失出发,沿计算图往回走,算出每个参数的梯度。你一行导数公式都不用写,这是框架最大的便利。
更新:迈出一步
optimizer.step() 拿着刚算好的梯度,按优化器自己的算法(SGD、Adam……)更新参数。到这里,一轮学习才真正完成。
train 和 eval 模式
模型身上有个隐藏开关:model.train() 和 model.eval()。平时不用管,但涉及 Dropout、BatchNorm 这类层时必须切换:
# 训练阶段
model.train()
for inputs, labels in train_loader:
...
# 验证阶段
model.eval()
with torch.no_grad():
for inputs, labels in val_loader:
...
原因:Dropout 训练时随机丢弃神经元防过拟合,验证时要全部启用;BatchNorm 训练时用本批数据统计,验证时用训练积累的全局统计。不切 eval(),验证分数会莫名其妙变差。
torch.no_grad() 也别忘。验证不需要梯度,关掉它既省显存又跑得快。
日志与监控
训练不是黑箱,得盯着损失看。两个小细节:
- 用
loss.item()把张量转成 Python 数字,避免计算图堆积 - 顺手算个准确率,比光看 loss 直观
running_loss = 0.0
for i, (inputs, labels) in enumerate(train_loader):
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99: # 每 100 个 batch 报一次
print(f'[{epoch + 1}, {i + 1}] loss: {running_loss / 100:.3f}')
running_loss = 0.0
epoch 和 iteration 是什么
两个词别搞混:
- iteration(迭代):处理一个 batch 算一次
- epoch(轮):把整个训练集完整过一遍
训练集 50000 张图,batch 大小 64,那一个 epoch 大约有 782 个 iteration。调度器通常按 epoch 更新学习率,就是这个道理。
排查清单:训练不正常怎么办
- loss 变成 NaN:学习率太大,或某层输出爆炸。先降学习率,加梯度裁剪
- loss 不下降:学习率太小;数据没打乱;损失函数选错
- loss 在降但准确率不动:正常,可能还在早期,先看趋势
- 显存溢出:减小 batch 大小;用
zero_grad(set_to_none=True)省显存
后续还能加什么料
这个五步骨架是地基,往上能盖不少楼。显存不够就加梯度累积,训练不稳就加梯度裁剪,想提速就上混合精度或 torch.compile,想调收敛节奏就加学习率调度器。这些后面章节会逐个展开,现在你只要知道:一切变体都围绕这五步转。
Tip训练循环是深度学习的万金油模板,以后你会写几百遍。这个五步骨架先背下来,再谈技巧。