热闻岛
返回全网热点

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步

6月16日35 阅读
《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图
第 14 章:训练循环:PyTorch 最经典的五步本章主线训练循环不是把几行代码背下来。它是一条完整闭环:数据进入模型,模型给出预测,损失函数判断错在哪里,Autograd 计算梯度,Optimizer 根据梯度修改参数。只要这条链路断一环,模型就学不动。很多人第一次写 PyTorch,最容易卡在训练循环。模型已经定
《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

训练循环不是把几行代码背下来。它是一条完整闭环:数据进入模型,模型给出预测,损失函数判断错在哪里,Autograd 计算梯度,Optimizer 根据梯度修改参数。只要这条链路断一环,模型就学不动。

很多人第一次写 PyTorch,最容易卡在训练循环。

模型已经定义了,数据也能读出来,loss 也能算。可训练几轮之后,结果没变化,显存越来越高,验证集表现乱跳。问题往往不是网络结构,而是训练循环写错了。

这一章就把训练循环讲透。少写代码,多讲逻辑。你只要理解五个动作:forward、loss、zero_grad、backward、step,就能看懂绝大多数 PyTorch 训练脚本。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

一、训练循环的本质:用错误反向修改参数

模型不是“跑一次”就学会。它要不断重复同一个过程。

先拿一个 batch。让模型预测。把预测和真实答案比较,得到 loss。再沿着计算图反向求导,得到每个参数应该怎么改。最后由优化器真正修改参数。

这就是训练循环。它不是仪式感代码,而是梯度下降在 PyTorch 里的工程实现。

一句话记忆

forward 给答案,loss 给错误,backward 给梯度,step 给改变。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

二、最小训练循环长什么样

代码可以很短,但每一行都有明确职责。

for epoch in range(num_epochs):
model.train()
for X, y in train_loader:
X, y = X.to(device), y.to(device)
optimizer.zero_grad()
pred = model(X)
loss = loss_fn(pred, y)
loss.backward()
optimizer.step()

这里最关键的不是代码形式,而是顺序。先清旧梯度,再前向预测,再算 loss,再反向传播,最后更新参数。

如果你把 step 漏掉,模型不会更新。如果你把 backward 漏掉,优化器没有梯度可用。如果你忘记 zero_grad,梯度会跨 batch 累加。

注意

官方教程里有时会把 optimizer.zero_grad() 放在 optimizer.step() 后面。它的含义是:本轮更新结束后立刻清梯度,保证下一轮开始时是干净的。经典写法是每轮开始先清梯度。两者都能成立,核心原则是 backward 前不能带着上一轮的脏梯度。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

三、第一步:forward,模型先做一次预测

forward 就是让模型基于当前参数,对输入数据做一次计算。

当你写 model(X) 时,表面上像是在调用函数。源码里它并不是直接裸调 forward,而是走 nn.Module 的调用流程。这个流程会处理 hooks、编译状态、训练状态,再进入 forward。

这也是为什么前面章节一直强调:不要手动调用 model.forward(X)。正确写法是 model(X)。因为直接调 forward 可能绕过 Module 的一些机制。

源码级理解

model(X) -> Module.__call__ -> _call_impl -> forward。forward 负责本层真实计算,__call__ 负责把 PyTorch 模块体系里的钩子、状态和包装逻辑串起来。

forward 还有一个重要副作用:如果参与计算的 Tensor 需要梯度,PyTorch 会在前向过程中记录反向传播需要的信息。也就是说,forward 不只是算出 pred,它还在为 backward 铺路。

四、第二步:loss,告诉模型错在哪里

pred 只是预测值。模型要学习,必须知道自己错了多少。这个“错了多少”,就是 loss。

分类任务常用 CrossEntropyLoss。回归任务常用 MSELoss 或 L1Loss。多标签任务常用 BCEWithLogitsLoss。

loss 最好是一个标量。因为它代表这一个 batch 的整体错误。后面的 backward 会从这个标量出发,沿着计算图一路反向,把梯度传回每个可训练参数。

关键点

loss 不是普通数字。只要它还带着 grad_fn,它就是反向图的入口。不要在 backward 前把它变成 loss.item(),否则只剩 Python float,计算图就断了。

五、第三步:zero_grad,为什么每轮要清梯度

这是 PyTorch 训练循环里最容易被忽略的一行。

PyTorch 的梯度默认是累加的。也就是说,每次 backward 计算出来的梯度,会加到参数的 .grad 上,而不是自动覆盖。

这个设计不是 bug。它是为了支持梯度累积、复杂多 loss 训练、多次反向传播等高级场景。可是普通训练里,如果你不清梯度,上一轮的梯度会污染下一轮。

一句话

zero_grad 的作用不是优化模型,而是保证这一轮 backward 算出来的梯度是干净的。

六、第四步:backward,反向传播真正开始

loss.backward() 是训练循环的核心。

它会从 loss 这个节点出发,沿着 forward 时留下的计算图反向走。每经过一个算子,就根据链式法则计算梯度。最后把每个可训练参数对应的梯度写入 param.grad。

注意:backward 不会修改参数。它只计算梯度。真正改参数的是 optimizer.step()。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

源码级理解

loss.backward() -> Tensor.backward -> torch.autograd.backward -> C++ Autograd Engine 执行反向图。反向结束后,参数本身还没变,但参数的 .grad 已经有值。

七、第五步:step,参数终于被更新

optimizer.step() 才是参数真正变化的地方。

优化器会遍历 param_groups,读取每个参数的 param.grad,再结合学习率、动量、一阶矩、二阶矩等状态,计算新的参数值。

SGD 比较直接:沿梯度方向走一步。Adam 会维护一阶矩和二阶矩,用历史梯度信息调节每个参数的更新幅度。AdamW 则把 weight decay 从梯度更新里解耦出来。

训练闭环

forward 和 loss 让模型知道自己错了;backward 让模型知道每个参数该往哪里动;step 让参数真的动起来。

八、训练循环和验证循环不能混在一起

训练时,模型要更新参数。验证时,模型只做评估。

所以验证循环里通常要做两件事:model.eval() 和 with torch.no_grad()。

model.eval() 会切换模块行为。比如 Dropout 不再随机丢弃神经元,BatchNorm 使用运行时统计量。torch.no_grad() 会关闭梯度记录,减少内存消耗。

这两个不是一回事。eval 控制模块状态,no_grad 控制梯度追踪。验证时通常两个都要用。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

model.eval()
val_loss = 0
correct = 0
with torch.no_grad():
for X, y in val_loader:
X, y = X.to(device), y.to(device)
pred = model(X)
val_loss += loss_fn(pred, y).item()

九、完整训练循环还要有指标和保存

真正的工程训练循环,不能只会更新参数。

你还要记录 loss、accuracy、learning rate、耗时、显存。还要在验证集效果变好时保存模型。否则训练几小时,结果一崩,没有 checkpoint,所有工作都白做。

尤其要注意:训练 loss 下降,不代表模型真的变强。它也可能只是越来越会背训练集。验证 loss 和验证指标,才更接近泛化能力。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

十、源码级主线:一轮训练到底穿过哪些文件

如果从源码角度看,一轮训练会穿过多条主线。

第一条是 Module 主线。model(X) 进入 nn.Module 的 __call__ 和 _call_impl,再进入你写的 forward。

第二条是 Loss 主线。loss_fn(pred, y) 进入具体 Loss 模块,再进入 torch.nn.functional,最后落到底层 C++/ATen 算子。

第三条是 Autograd 主线。loss.backward() 会进入 Tensor.backward,再进入 torch.autograd.backward,最后由 Autograd Engine 执行反向图。

第四条是 Optimizer 主线。optimizer.zero_grad() 负责清梯度,optimizer.step() 负责读取梯度并修改参数。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

源码阅读顺序

先读 torch/nn/modules/module.py,再读 torch/nn/modules/loss.py 和 torch/nn/functional.py,再读 torch/_tensor.py、torch/autograd,再读 torch/optim/optimizer.py 与具体优化器实现。

十一、最容易踩的坑

训练循环的坑,大多不是语法错,而是逻辑错。它们不会立刻报错,但会让模型训练失败。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

十二、把这一章压成一张图

训练循环的核心就是一条闭环。

DataLoader 提供 batch。model 做预测。loss 衡量错误。backward 计算梯度。step 修改参数。eval/no_grad 做验证。

这条链路理解了,后面的混合精度、梯度累积、分布式训练、FSDP、大模型训练,其实都是在这条循环上继续加组件。

《PyTorch深度修炼》训练循环:PyTorch 最经典的五步配图

本章结论

训练循环是 PyTorch 的主战场。模型定义只是骨架,数据加载只是供给,损失函数只是方向。真正让模型变强的,是每一轮稳定、正确、可观察的训练闭环。

要点速读

第 14 章:训练循环:PyTorch 最经典的五步本章主线训练循环不是把几行代码背下来。它是一条完整闭环:数据进入模型

  • 第 14 章:训练循环:PyTorch 最经典的五步本章主线训练循环不是把几行代码背下来
  • 它是一条完整闭环:数据进入模型
  • 更多细节仍在持续更新中