资讯动态

PyTorch反向传播机制详解:计算图、叶子节点与梯度调试

发布时间:2026/10/18 12:41:07 来源:尧图企业网站定制
反向传播是深度学习训练里每步都在运行的底层机制但大多数开发者对它的理解停在loss.backward()这个调用上。这篇文章把反向传播彻底讲透计算图如何在前向传播中自动建立叶子节点、非叶子节点、根节点究竟怎么区分requires_grad和grad怎么配合以及为什么有的张量能拿到梯度、有的只能得到None。全程基于 PyTorch 演示CPU 环境就能跑通适合正在学自动微分、或者排查梯度不更新的同学。这个主题最值得关注的点有三个第一理解计算图的生成机制能解释“为什么 backward 要依赖于前向保存的图”第二搞清叶子节点和非叶子节点的划分规则能看懂每个张量的is_leaf和grad_fn第三掌握requires_grad、grad、retain_grad、no_grad这些属性与工具的配合能够定位训练中梯度不累计、中间层梯度丢失、显存占用异常等实际问题。1. 核心概念速览概念项说明计算图前向传播时自动建立的有向无环图记录张量之间的依赖关系叶子节点is_leafTrue由用户直接创建通常是输入张量或模型参数非叶子节点is_leafFalse由运算派生默认不保存梯度根节点调用backward()的输出张量也是反向传播的起点requires_grad标记张量是否需要计算梯度控制是否加入自动微分grad保存反向传播计算得到的梯度值位于.grad属性中grad_fn记录当前张量由哪种运算产生是反向传播沿图回溯的线索这套概念不是孤立的。叶子节点决定哪些参数可以优化非叶子节点是计算图中间的桥梁根节点决定了backward()从哪个位置启动。requires_grad决定“是否参与记录”grad则是“记录下来的结果”。这些属性在日常训练中都会反复出现尤其是model.parameters()对应的权重张量天然就是叶子节点。2. 适用场景与使用边界2.1 谁最需要搞清楚这些概念最典型的人群是正在用 PyTorch 写训练循环的开发者。当你发现loss.backward()执行后某个参数的grad是None或者手动修改了计算图、想要查看中间层的梯度时前面这几个概念会直接决定你能不能看明白报错。调参、调试梯度爆炸、验证自定义算子也都绕不开计算图与节点类型。2.2 使用边界本文基于 PyTorch 的自动微分机制讨论不涉及 TensorFlow 或 JAX 的自动微分实现。演示环境是 CPU模型规模非常小但结论与 GPU 环境一致。分布式训练、混合精度、torch.compile 等高级场景中计算图的构建和释放策略会有差别本文不展开。文中涉及的手动梯度验证适合教学与调试正式训练仍然依赖框架自动求导完成。3. 环境准备与前置条件3.1 硬件与系统整篇只需要一个能运行 Python 的机器Windows、macOS、Linux 都可以。不需要独立显卡CPU 推理即可完成全部演示。如果你只有 GPU 环境也不会影响结果代码不区分设备。3.2 Python 与 PyTorch建议使用 Python 3.8 及以上版本PyTorch 2.x 均可。CPU 版本安装命令如下pip install torch --index-url https://download.pytorch.org/whl/cpu如果你已经安装了 GPU 版本直接使用现有环境即可。验证安装import torch print(torch.__version__) print(torch.cuda.is_available())CUDA 显示False不影响本文任何示例。3.3 运行方式推荐使用 Jupyter Notebook因为逐步查看张量属性更直观。普通 Python 脚本也可以但要注意print输出顺序和变量作用域。所有代码块都是完整可执行的不需要额外下载数据集或模型文件。4. 计算图与前向传播自动微分的地基4.1 计算图是怎么生成的PyTorch 在前向传播时自动构建计算图。每个参与运算的张量都成为一个节点每个运算操作成为连接节点的边。对用户来说这个构建过程是隐式的但张量对象上会留下痕迹grad_fn。看一个简单的例子import torch a torch.tensor([2.0], requires_gradTrue) b torch.tensor([3.0], requires_gradTrue) c torch.tensor([4.0], requires_gradTrue) d a * b e d c loss e.sum() for name, tensor in [(a, a), (b, b), (d, d), (e, e), (loss, loss)]: print(f{name}: is_leaf{tensor.is_leaf}, requires_grad{tensor.requires_grad}, grad_fn{tensor.grad_fn})输出结果大致如下a: is_leafTrue, requires_gradTrue, grad_fnNone b: is_leafTrue, requires_gradTrue, grad_fnNone d: is_leafFalse, requires_gradTrue, grad_fnMulBackward0 e: is_leafFalse, requires_gradTrue, grad_fnAddBackward0 loss: is_leafFalse, requires_gradTrue, grad_fnSumBackward0前向传播执行到d a * b时PyTorch 会记录d是由乘法产生的于是d.grad_fn指向一个乘法反向算子。e记录加法loss记录求和。这样一来整个链路 a、b - d - e - loss 就被保存下来。4.2 为什么计算图能驱动反向传播反向传播的本质是链式法则。计算图中每条边都对应一个局部梯度从根节点出发沿着grad_fn的反向箭头把全局梯度逐层回传。由于前向传播已经保存了每一步的运算类型和张量依赖关系反向阶段只需要调用对应的 backward 函数即可。这里需要提醒一点计算图是有向无环图PyTorch 默认在backward()结束后释放整张图。这样设计是为了节省内存但也意味着你不能直接对同一张图反复调用反向传播。如果想要多次 backward必须传retain_graphTrue后面会专门讲。5. 叶子节点、非叶子节点与根节点5.1 叶子节点叶子节点是计算图的起点也是最容易被理解的一类节点。简单来说叶子节点是用户直接创建的张量不依赖任何其他张量。判断方式看is_leafx torch.tensor(1.0, requires_gradTrue) print(x.is_leaf) # True对于模型训练叶子节点就是权重和偏置参数。model.parameters()返回的每个参数张量其is_leaf都是True。正因为它们是叶子节点反向传播才会把梯度写入这些张量的.grad中优化器才能读取grad去更新数值。需要注意叶子节点可以由用户创建也可以由requires_grad_()方法强制转换。例如先创建一个不需要梯度的张量再用requires_grad_()设为需要梯度它仍然属于叶子节点。5.2 非叶子节点凡是经过运算得到的张量都是非叶子节点。上面例子里的d、e、loss都属于这一类。它们的is_leafFalse并且grad_fn不为None。非叶子节点有一个容易踩坑的特点默认不会保存梯度。即使你对包含它的计算图执行了backward().grad仍然是None。这是深度框架有意为之的优化策略因为中间张量在反向传播之后就不再被使用了保留它们的梯度会白白占用内存。不过 PyTorch 留了后门调用retain_grad()可以让非叶子节点保留梯度。d.retain_grad() # 之后 backwardd.grad 就不会是 None5.3 根节点根节点是调用backward()的那个张量也就是计算图的最终输出。在训练场景中根节点就是标量loss。反向传播从根节点开始沿着grad_fn一路回溯到所有叶子节点。如果根节点不是标量直接调用backward()会报错。术语上这叫“梯度权重缺失”因为对向量求梯度必须指定一个同形状的权重矩阵。常见做法是先求和得到标量或者显式传入grad_tensors。loss (a * b).sum() loss.backward() # 正确loss 是标量out a * b out.backward() # 报错只能对标量输出隐式求梯度5.4 三类节点的关系用一句话概括叶子节点是图的入口根节点是出口非叶子节点是中间道路。反向传播从出口反向流回入口每条道路上的局部梯度都会传给对应的入口节点。模型参数作为叶子节点最终收集到全部梯度分量。6. requires_grad 与 grad梯度从哪里来存到哪里去6.1 requires_grad 决定是否参与自动微分requires_grad是一个布尔状态决定 PyTorch 是否为该张量构建计算梯度所需的记录。设置为True后所有与它相关的运算都会进入计算图设置为False则相关张量不会记录梯度信息。x torch.tensor(2.0, requires_gradTrue) y x * 2 print(y.requires_grad) # True w torch.tensor(2.0) z w * 2 print(z.requires_grad) # False6.2 grad 保存梯度结果当backward()执行完成后满足条件的叶子节点会在.grad中拿到梯度。以下代码演示了最基础的结果import torch x torch.tensor(2.0, requires_gradTrue) y x ** 2 y.backward() print(x.grad) # tensor(4.0)即 2*x 4梯度值需要满足几个条件才会出现张量requires_gradTrue张量是叶子节点并且从根节点到它的路径上没有阻断。不满足条件的张量grad要么是None要么根本不存在。6.3 梯度是累积的同一张叶子节点反复执行backward()梯度会在.grad上累加而不是覆盖。看这个例子import torch x torch.tensor(2.0, requires_gradTrue) y x ** 2 y.backward() print(x.grad) # tensor(4.) y x ** 2 y.backward() print(x.grad) # tensor(8.)累加了因此标准训练循环才会在每个迭代开始前调用optimizer.zero_grad()或model.zero_grad()把梯度清零避免上一次迭代的梯度累积到当前批次上。6.4 非叶子节点如何保留梯度想要查看中间节点梯度需要调用retain_grad()import torch x torch.tensor(2.0, requires_gradTrue) y x ** 3 z y.sum() y.retain_grad() z.backward() print(x.grad) # tensor(12.) print(y.grad) # tensor(1.)根节点传递下来的局部梯度y.grad保存的是“根节点对 y 的梯度”。对于这个例子z y.sum()所以dz/dy就是 1。这个技巧在调试网络中间层梯度时非常实用。7. 完整代码演示从建图到 backward7.1 演示一观察计算图属性运行下面代码熟悉每个张量的关键属性import torch # 构造两个叶子节点 x1 torch.tensor([1.0, 2.0], requires_gradTrue) x2 torch.tensor([3.0, 4.0], requires_gradTrue) # 构造非叶子节点 h x1 * x2 out h.sum() print(x1.is_leaf:, x1.is_leaf) print(x1.requires_grad:, x1.requires_grad) print(x1.grad_fn:, x1.grad_fn) print(h.is_leaf:, h.is_leaf) print(h.grad_fn:, h.grad_fn) print(out.is_leaf:, out.is_leaf) print(out.grad_fn:, out.grad_fn)预期输出x1.is_leaf: True x1.requires_grad: True x1.grad_fn: None h.is_leaf: False h.grad_fn: MulBackward0 out.is_leaf: False out.grad_fn: SumBackward0这段代码验证了计算图的基本结构叶子节点没有grad_fn非叶子节点记录了运算来源。7.2 演示二backward 后查看 gradimport torch x1 torch.tensor([1.0, 2.0], requires_gradTrue) x2 torch.tensor([3.0, 4.0], requires_gradTrue) h x1 * x2 out h.sum() out.backward() print(x1.grad:, x1.grad) # tensor([3., 4.]) print(x2.grad:, x2.grad) # tensor([1., 2.]) print(h.grad:, h.grad) # None非叶子节点默认不保存手动验算out x1[0]*x2[0] x1[1]*x2[1]对x1[0]求导等于x2[0] 3对x2[0]求导等于x1[0] 1。结果一致。7.3 演示三retain_grad 查看中间层梯度import torch x1 torch.tensor([1.0, 2.0], requires_gradTrue) x2 torch.tensor([3.0, 4.0], requires_gradTrue) h x1 * x2 h.retain_grad() out h.sum() out.backward() print(h.grad:, h.grad) # tensor([1., 1.])因为 out 对 h 求导是逐元素 17.4 演示四detach 与 no_grad 的隔离作用detach()可以切断某个张量与计算图之间的联系返回一个requires_gradFalse的新张量。torch.no_grad()则是一个上下文管理器内部的运算不会进入计算图。import torch x torch.tensor(2.0, requires_gradTrue) y x.detach() z y * 3 print(y.requires_grad:, y.requires_grad) print(z.requires_grad:, z.requires_grad)import torch x torch.tensor(2.0, requires_gradTrue) with torch.no_grad(): y x * 3 print(y.requires_grad:, y.requires_grad) # False推理阶段推荐用torch.no_grad()因为不构建计算图显存占用和耗时都会下降。detach()常用于需要把某个中间结果复制出来参与独立运算但又不希望它影响原图的梯度回传。7.5 演示五手动验证链式法则自动微分的结果可以手动验证。例如loss (x y) ** 2其中 x、y 是叶子节点import torch x torch.tensor(2.0, requires_gradTrue) y torch.tensor(3.0, requires_gradTrue) loss (x y) ** 2 loss.backward() # 理论d(loss)/dx 2 * (x y)带 x2,y3 得 10 print(x.grad:, x.grad) # tensor(10.) print(y.grad:, y.grad) # tensor(10.)这里 x 和 y 的梯度相同因为(xy)对两者的偏导都是 1外层乘 2 再乘(xy)最终数值都是 10。8. PyTorch 反向传播常见问题排查问题现象可能原因排查方式解决方案非叶子节点.grad为 NonePyTorch 默认不保存中间节点梯度打印is_leaf和grad_fn调用retain_grad()手动保留叶子节点梯度是上一轮积累的旧值没有清零旧梯度观察多个训练轮次中的grad值每轮前调用optimizer.zero_grad()对非标量backward()报错缺少梯度权重参数检查输出张量 shape先sum()得到标量或传入grad_tensors梯度为 None但张量 requires_gradTrue该张量不是叶子节点检查is_leaf如果是参数确认未被.detach()包裹多次调用backward()报错计算图已被释放看报错信息中的 “Trying to backward through the graph”首次调用时传retain_graphTrueno_grad()块内的参数梯度为 None当前上下文禁用了梯度计算检查上下文范围退出no_grad()环境后再执行 backward推理时显存占用仍然很高正在构建计算图检查是否用了torch.no_grad()/torch.no_grad()推理阶段显式关梯度中间层梯度数值异常大或异常小网络结构导致梯度消失/爆炸用retain_grad()观察每层调整初始化、加归一化、减小学习率排查梯度问题先看三个属性requires_grad、is_leaf、grad_fn。说句实在话绝大多数“梯度不更新”问题都出在requires_gradFalse或者张量被detach()后脱离计算图而不是反向传播本身写错了。9. 梯度调试最佳实践9.1 训练循环习惯标准训练循环应该包含三件套zero_grad()、backward()、step()。顺序不能乱import torch model torch.nn.Linear(4, 1) optimizer torch.optim.SGD(model.parameters(), lr0.01) x torch.randn(8, 4) y torch.randn(8, 1) loss_fn torch.nn.MSELoss() optimizer.zero_grad() loss loss_fn(model(x), y) loss.backward() optimizer.step()先把旧梯度清零再计算新损失再反向传播最后更新参数。如果调换zero_grad和backward的位置会导致梯度累积到历史轮次上训练过程变得很不稳定。9.2 调试中间层梯度需要监控网络中间层时在对应张量上调用retain_grad()hidden model.fc1(x) hidden.retain_grad() out model.fc2(hidden) loss out.sum() loss.backward() print(hidden.grad)用这种方法可以快速定位梯度消失发生在哪一层。如果是全 0说明前面网络无法传递梯度如果是 NaN说明数值计算已经溢出。9.3 推理阶段及时关闭计算图验证集评估或正式推理时使用torch.no_grad()model.eval() with torch.no_grad(): pred model(x_val)这个习惯能显著降低显存占用因为前向时不会再保存中间张量也不需要为参数计算梯度。model.eval()只切换 Dropout 和 BatchNorm 等层的行为真正关掉梯度记录的是no_grad()上下文。9.4 冻结参数的场景迁移学习中低频次冻结主干网络时可以直接把对应参数的requires_grad设为Falsefor param in model.backbone.parameters(): param.requires_grad False这样该部分参数不参与梯度计算优化器step()时也不会更新它们。同时由于不构建这部分计算图训练速度和显存占用都会改善。9.5 不要轻易对整图保留除非确实需要多次反向传播否则不要给backward()加retain_graphTrue。保留计算图意味着所有中间张量都滞留在内存中显存占用会明显上升。标准做法是让图在每次backward()后释放到下一步前向时重新建立。10. 总结与下一步这次我们把反向传播里最容易混淆的一组概念完整过了一遍。计算图是自动微分的地基前向传播建图反向传播沿图求梯度叶子节点是图入口非叶子节点是中间桥梁根节点是backward()起点requires_grad控制是否参与微分.grad存放最终梯度值。建议你立刻在本地跑一遍第二个完整演示把每个张量的is_leaf、grad_fn、requires_grad打印出来确认输出与文章中的预期一致。最容易踩的坑有两个非叶子节点默认为None以及训练循环没调zero_grad()。前者用retain_grad()解决后者养成固定习惯即可。接下来可以继续深入autograd.Function自定义算子或者查看register_hook钩子机制这两块都属于自动微分的高级玩法。把这些基础吃透后面看任何梯度相关报错都会轻松很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑