PyTorch反向传播实战:计算图、叶子节点与梯度为None排查

发布时间:2026/10/18 9:30:13
PyTorch反向传播实战:计算图、叶子节点与梯度为None排查 从训练模型时的“梯度消失”或“梯度为 None”开始往往是反向传播没有真正理解。很多时候我们发现loss.backward()之后某个参数的.grad依然是None或者发现两个模型的参数明明参与了前向计算却没有梯度。这些问题的根源几乎都指向同一个方向对计算图、叶子节点和requires_grad的关系不够清楚。这篇教程就围绕反向传播中的几个核心概念展开计算图、叶子节点与非叶子节点、根节点、requires_grad和grad。我会结合 PyTorch 代码逐步拆解尽量把每个概念背后的原理和实际使用场景讲清楚。适合刚入门深度学习、写过简单训练脚本但遇到梯度相关问题的读者也适合想系统梳理 PyTorch 自动求导机制的开发者。1. 反向传播到底在做什么1.1 从梯度下降说起神经网络训练的本质是不断调整参数让损失函数变小。假设我们的损失函数是 ( L(\theta) )其中 ( \theta ) 是模型参数那么标准的梯度下降更新公式是θ θ - lr * ∂L/∂θ这里最关键的量就是梯度 ( \partial L / \partial \theta )。在简单模型里可以手推公式算出梯度但在深度神经网络中参数可能成百上千万如果手动推导每个参数的偏导工作量巨大且极易出错。反向传播算法提供了一种系统化的计算方法。它利用微积分中的链式法则从输出端开始沿着计算路径由后往前逐层求导把最终的损失梯度一步步“传回”到每个参数。1.2 反向传播与自动求导的关系现代深度学习框架并不需要用户手动实现反向传播而是基于**计算图Computational Graph**自动完成求导。PyTorch 的自动求导机制可以概括为两步前向传播时张量之间发生运算PyTorch 会记录这些运算关系构建一张有向无环图DAG。调用.backward()时PyTorch 从当前标量损失出发沿着这张图反向计算梯度并把梯度存储到对应张量的.grad属性中。理解这张“图”是怎么构建的以及图中每个节点扮演什么角色是理解反向传播的关键。2. 环境准备与版本说明本文所有示例基于以下环境操作系统Windows / Linux / macOS 均可Python 版本3.8 及以上PyTorch1.x 或 2.x 均可本文示例以 2.x 通用 API 演示安装方式pip 安装pip install torch如果你的环境没有 GPU也可以正常运行因为本文示例规模很小使用 CPU 即可。建议在 Jupyter Notebook 或 VSCode 中逐段运行代码方便查看每个张量的属性变化。版本不需要完全与我一致PyTorch 的核心 API 如requires_grad、is_leaf、grad_fn、backward()在近几个大版本中保持一致。如果你使用的是 0.x 老版本建议升级到 1.13 以上。3. 计算图反向传播的“地图”3.1 什么是计算图计算图就是把数学表达式中的运算过程用图结构表示出来。图中的节点分为两类数据节点张量Tensor运算节点加减乘除、矩阵乘法、激活函数等操作Op边的方向表示数据流动方向。前向传播时数据从输入流向输出反向传播时梯度从输出流向输入。举个例子表达式z (x y) * w可以用下面的流程理解x ─┐ ├─→ add ─→ sum ─┐ y ─┘ ├─→ mul ─→ z w ──────────────────┘PyTorch 在运行sum x y和z sum * w这两行代码时会自动记录相关的运算信息生成内部计算图。3.2 静态图与动态图的区别TensorFlow 1.x 时代常用的是静态图先定义好完整的计算图再喂数据执行。这种模式在调试时不太直观。PyTorch 采用的是动态图Define-by-Run即每次前向执行时都重新构建计算图代码怎么写图就怎么构建。动态图的优势在于调试方便可以随时打印中间结果。控制流if、for、while可以自然融入模型。更方便实现变长输入、动态结构模型。理解这一点之后你会明白为什么 PyTorch 可以像写普通 Python 一样写神经网络。3.3 grad_fn运算节点的身份证在 PyTorch 中每个由运算产生的张量都会有一个grad_fn属性指向创建它的运算函数。这个属性是判断一个张量是“直接创建”还是“计算得到”的重要标志。看一个简单示例import torch # 直接创建的张量 x torch.tensor([2.0], requires_gradTrue) print(x 的 grad_fn:, x.grad_fn) # 通过加法运算产生的张量 y x 3 print(y 的 grad_fn:, y.grad_fn) # 通过乘法产生的张量 z y * 2 print(z 的 grad_fn:, z.grad_fn)输出类似x 的 grad_fn: None y 的 grad_fn: AddBackward0 object at 0x... z 的 grad_fn: MulBackward0 object at 0x...可以看到用户直接创建的xgrad_fn为None。由运算得到的y、zgrad_fn指向对应的反向传播函数。AddBackward0表示y x 3这个加法操作。MulBackward0表示z y * 2这个乘法操作。grad_fn不仅仅是“标识”它内部还保存了反向传播需要的必要信息。例如加法反向传播需要知道输入是什么乘法反向传播需要知道另一个因子是什么。3.4 计算图的作用计算图让反向传播有了明确路径。调用z.backward()时PyTorch 会沿着z.grad_fn找到产生z的乘法操作根据链式法则对y和w求梯度再沿着y.grad_fn找到加法操作把梯度继续传回x。也就是说构建计算图的目的就是为了在反向传播时能准确、高效地计算每个叶子节点的梯度。4. 叶子节点、非叶子节点与根节点4.1 叶子节点用户亲手创建的张量在 PyTorch 中**叶子节点Leaf Tensor**指的是用户直接创建、不依赖于其他张量运算结果的张量。例如w torch.tensor([1.0, 2.0], requires_gradTrue) b torch.tensor([0.0], requires_gradTrue)w和b都是叶子节点因为它们是直接创建的。叶子节点的特点is_leaf属性为True。grad_fn为None因为不是通过运算产生的。反向传播后grad属性中会保存梯度如果requires_gradTrue且参与了前向计算。在深度学习中模型的权重参数通常就是叶子节点。它们是我们希望优化的对象。4.2 非叶子节点运算得到的中间结果通过张量运算得到的张量属于非叶子节点Non-leaf Tensor。例如x torch.tensor([2.0, 3.0], requires_gradTrue) y x * 2 z y.sum()这里的y是非叶子节点因为它是x * 2的结果z也是非叶子节点因为它是y.sum()的结果。非叶子节点的特点is_leaf属性为False。grad_fn不为None至少指向一个运算函数。默认情况下PyTorch 不会为非叶子节点保存梯度这是为了节省内存。如果确实需要中间变量的梯度需要调用.retain_grad()。4.3 根节点反向传播的起点“根节点”这个概念需要看语境。在计算图中通常把最终输出节点或损失值称为根节点或根张量Root Tensor。例如下面代码中的losspred model(x) loss loss_fn(pred, target) loss.backward()loss就是反向传播的起点也就是根节点。需要注意PyTorch 官方文档中其实很少直接使用“根节点”这个术语更多是在解释计算图拓扑结构时使用。社区中常见的说法是叶子节点没有父节点的节点用户创建根节点没有子节点的节点最终输出一个计算图只有一个最终输出通常是标量损失所以根节点往往只有一个。如果输出是向量需要调用.backward(gradient...)传入外部梯度或用.sum()之类的操作先变成标量。4.4 通过代码理解节点类型我们用一个简单的例子来验证import torch # 叶子节点 x torch.tensor([2.0], requires_gradTrue) w torch.tensor([3.0], requires_gradTrue) # 非叶子节点 y x * w # 非叶子 z y 1 # 非叶子也是最终根节点 print(x.is_leaf:, x.is_leaf) print(w.is_leaf:, w.is_leaf) print(y.is_leaf:, y.is_leaf) print(z.is_leaf:, z.is_leaf) print(x.grad_fn:, x.grad_fn) print(y.grad_fn:, y.grad_fn) print(z.grad_fn:, z.grad_fn)输出x.is_leaf: True w.is_leaf: True y.is_leaf: False z.is_leaf: False x.grad_fn: None y.grad_fn: MulBackward0 object at 0x... z.grad_fn: AddBackward0 object at 0x...对应关系x ───┐ ├─→ Mul ─→ y ─→ Add ─→ z w ───┘x、w叶子节点没有父节点。y非叶子节点由乘法产生。z非叶子节点由加法产生同时是根节点因为它是最终输出。如果对z调用反向传播z.backward() print(x.grad:, x.grad) print(w.grad:, w.grad) print(y.grad:, y.grad) # 默认应该是 None输出x.grad: tensor([3.]) w.grad: tensor([2.]) y.grad: Nonex的梯度是w的值 3验证了链式法则( \frac{\partial z}{\partial x} w 3 )。w的梯度是x的值 2( \frac{\partial z}{\partial w} x 2 )。y.grad为None因为默认不保存中间节点梯度。这就是理解“叶子节点”和“非叶子节点”实际意义的绝佳例子只有叶子节点默认会获得梯度当然要求它requires_gradTrue且参与反向传播路径。5. requires_grad 的深度理解5.1 requires_grad 决定是否需要梯度requires_grad是张量的一个布尔属性False不需要梯度运算不会被记录到计算图中或者说不会追踪该张量的运算。True需要梯度PyTorch 会追踪该张量的所有操作以便自动求导。默认情况下普通张量的requires_gradFalse。只有nn.Parameter或手动设置的张量才会是True。a torch.randn(3) print(a.requires_grad) # False b torch.randn(3, requires_gradTrue) print(b.requires_grad) # True5.2 requires_grad 与计算图构建的关系如果某个张量requires_gradFalse那么由它参与计算得到的输出通常也不会requires_gradTrue除非有另一个requires_gradTrue的张量参与。举个例子x torch.tensor([1.0], requires_gradTrue) w torch.tensor([2.0], requires_gradFalse) y x * w print(y.requires_grad) # True因为 x 需要梯度相比之下a torch.tensor([1.0], requires_gradFalse) b torch.tensor([2.0], requires_gradFalse) c a * b print(c.requires_grad) # False所有输入都不需要梯度这说明requires_grad会沿着计算图“传播”。只要有一条路径通向需要梯度的张量输出张量的requires_grad就会是True。5.3 从 requires_gradFalse 到 True有时我们希望临时标记一个张量需要梯度。可以使用x.requires_grad_(True)原地修改但要注意这种方法通常只对叶子节点有效。如果对非叶子节点调用可能会破坏计算图结构一般不建议。更常见的方法是在创建张量时指定x torch.tensor([1.0, 2.0], requires_gradTrue)如果你是加载预训练模型希望部分层不参与训练可以for param in model.frozen_layers.parameters(): param.requires_grad False这样反向传播时梯度就不会传到这些层也就不会更新它们的参数。5.4 torch.no_grad() 的作用在模型推理或计算验证指标时我们不需要反向传播也不希望 PyTorch 追踪计算图因为这会浪费内存和计算资源。此时可以用torch.no_grad()model.eval() with torch.no_grad(): pred model(x)在no_grad上下文中即使张量requires_gradTrue计算图中也不会记录操作输出张量的requires_grad为 False也无法调用backward()。这对应前面提到的动态图特性每次前向传播都重新构建计算图。在推理阶段直接“不建图”是最省资源的做法。6. grad 属性与梯度存储机制6.1 梯度存储在哪当一个张量满足以下条件时反向传播后梯度会保存在.grad属性中是叶子节点requires_gradTrue在反向传播路径上参与了运算.grad是一个与原始张量形状相同的张量。如果张量是标量.grad也是标量张量。x torch.tensor([1.0, 2.0, 3.0], requires_gradTrue) y x.sum() y.backward() print(x.grad) # tensor([1., 1., 1.])因为y是三个元素的和对每个元素的偏导都是 1。6.2 梯度累积机制PyTorch 中.grad是累加的。也就是说每次调用backward()梯度会加到已有的.grad上而不是直接覆盖。看示例x torch.tensor([1.0], requires_gradTrue) y x * 2 y.backward() print(第一次 backward 后:, x.grad) y.backward() print(第二次 backward 后:, x.grad)输出第一次 backward 后: tensor([2.]) 第二次 backward 后: tensor([4.])第二次backward()后梯度变成了 4而不是 2。这就是为什么每个训练迭代都要调用optimizer.zero_grad()的原因——如果不把之前的梯度清零梯度会不断累加导致参数更新异常。6.3 为什么训练时要在 backward 前 zero_grad标准训练循环通常长这样optimizer.zero_grad() loss.backward() optimizer.step()zero_grad()会把模型所有参数的.grad属性清零。这样做是为了保证当前迭代的梯度不被上一次迭代的梯度污染。如果不调用zero_grad()每个 batch 累积后的梯度会导致参数更新方向错误模型很难收敛。6.4 非叶子节点的 grad 如何保存retain_grad默认情况下非叶子节点的.grad是None。如果需要中间变量的梯度比如调试、可视化、实现某些特殊算法可以调用.retain_grad()。x torch.tensor([2.0], requires_gradTrue) y x * 3 z y * y # 默认不保存 y 的梯度 z.backward() print(未调用 retain_grady.grad:, y.grad)输出未调用 retain_grady.grad: None再看调用retain_grad()的情况x torch.tensor([2.0], requires_gradTrue) y x * 3 y.retain_grad() z y * y z.backward() print(调用 retain_grady.grad:, y.grad)输出调用 retain_grady.grad: tensor([12.])因为 ( z y^2 )所以 ( \partial z / \partial y 2y 12 )。retain_grad()使得中间节点的梯度被保存下来。不过要谨慎使用retain_grad()因为它会增加显存或内存占用。平时调试完最好去掉。6.5 梯度为 None 的常见场景下面列出几种常见的grad为None的情况张量不是叶子节点且没有调用retain_grad()。张量requires_gradFalse。张量虽然没有参与反向传播路径例如用它构造了损失但计算图中没有连接。在no_grad上下文里创建的张量。7. 完整实战案例手动观察线性模型的梯度为了把这些概念串起来我们来实现一个最简单的线性回归训练流程并手动观察叶子节点、非叶子节点和梯度。7.1 场景设定假设我们有一个简单的数据集目标是用线性模型拟合。为聚焦反向传播本身不使用数据集而是用一个人为构造的函数来模拟损失。我们直接定义一个参数w一个输入x并计算一个简单的二次损失。import torch # 构造叶子节点参数 w torch.tensor([1.0], requires_gradTrue) b torch.tensor([0.0], requires_gradTrue) # 输入数据 x torch.tensor([2.0]) # 前向传播预测值 y_pred w * x b # 假设目标值为 5 target torch.tensor([5.0]) # 损失 loss (y_pred - target) ** 2 print(w.is_leaf:, w.is_leaf) print(b.is_leaf:, b.is_leaf) print(y_pred.is_leaf:, y_pred.is_leaf) print(loss.is_leaf:, loss.is_leaf) print(y_pred.grad_fn:, y_pred.grad_fn) print(loss.grad_fn:, loss.grad_fn)7.2 调用反向传播并观察梯度# 反向传播 loss.backward() print(w.grad:, w.grad) print(b.grad:, b.grad) print(y_pred.grad:, y_pred.grad) # 默认为 None print(loss.grad:, loss.grad) # 默认为 None我们先手动推导一下梯度。前向过程y_pred w*x b 1*2 0 2 loss (y_pred - 5)^2 (2 - 5)^2 9求梯度∂loss/∂y_pred 2 * (y_pred - 5) 2 * (2 - 5) -6 ∂y_pred/∂w x 2 ∂y_pred/∂b 1 ∂loss/∂w ∂loss/∂y_pred * ∂y_pred/∂w -6 * 2 -12 ∂loss/∂b ∂loss/∂y_pred * ∂y_pred/∂b -6 * 1 -6因此预期输出w.grad: tensor([-12.]) b.grad: tensor([-6.])y_pred.grad和loss.grad默认是None因为非叶子节点默认不保存梯度。7.3 如果希望保存中间梯度现在我们把y_pred.retain_grad()打开重新运行w torch.tensor([1.0], requires_gradTrue) b torch.tensor([0.0], requires_gradTrue) x torch.tensor([2.0]) target torch.tensor([5.0]) y_pred w * x b y_pred.retain_grad() loss (y_pred - target) ** 2 loss.backward() print(y_pred.grad:, y_pred.grad) # tensor([-6.])这个输出与前面推导的∂loss/∂y_pred -6一致。7.4 用 optimizer 实现训练循环实际工程中不会手动对每个参数设置.grad而是使用优化器统一管理。上面例子对应的完整训练循环如下import torch import torch.nn as nn # 定义模型参数 w torch.tensor([1.0], requires_gradTrue) b torch.tensor([0.0], requires_gradTrue) # 优化器 optimizer torch.optim.SGD([w, b], lr0.01) # 模拟训练数据 x torch.tensor([[2.0], [3.0], [4.0]]) target torch.tensor([[5.0], [7.0], [9.0]]) def predict(x): return w * x b # 训练 10 个 epoch for epoch in range(10): optimizer.zero_grad() y_pred predict(x) loss ((y_pred - target) ** 2).mean() loss.backward() optimizer.step() print(fepoch {epoch}, loss{loss.item():.4f}, w{w.item():.4f}, b{b.item():.4f})在这里optimizer.zero_grad()清零上一步的梯度。loss.backward()计算当前梯度。optimizer.step()根据梯度更新w和b。因为w和b是叶子节点所以它们的.grad在backward()后会被自动填充。8. 常见问题与排查思路下面整理了使用反向传播时最常见的几类问题。8.1 参数 grad 一直是 None现象常见原因解决思路loss.backward()后参数.grad还是None参数requires_gradFalse检查参数是否被正确设置为requires_gradTrue或使用nn.Parameter参数没有参与计算图的路径该参数虽存在但前向计算中没用到确保该参数参与了产生损失的前向计算在torch.no_grad()上下文中调用no_grad下不构建计算图把反向传播放到no_grad上下文之外变量是非叶子节点中间变量默认不保存梯度调用.retain_grad()但尽量只用于调试8.2 梯度数值异常NaN / 无穷大现象常见原因解决思路梯度变成 NaN学习率过大降低学习率或使用学习率预热梯度爆炸网络层过深、梯度累乘过大使用梯度裁剪clip_grad_norm_数据中有 NaN输入或标签存在缺失值检查数据预处理清理异常样本损失本身是 NaN损失函数计算中出现了除零或 log(0)检查损失函数输入范围增加 epsilon常见梯度裁剪写法torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)8.3 backward 报错只能对标量调用backward()默认只能对标量输出调用。如果输出不是标量需要传入一个相同形状的gradient参数或者先对输出做sum()/mean()等聚合操作。错误示例x torch.tensor([1.0, 2.0], requires_gradTrue) y x * 2 # y 是 vector # y.backward() # 报错正确做法loss y.sum() loss.backward()或y.backward(torch.ones_like(y))但后者在深度学习中不常用日常更推荐先sum()或mean()。8.4 原地操作导致梯度计算错误在反向传播中对参与计算的叶子节点做原地修改如x.add_()可能导致计算图信息失效。看一个典型报错RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.这意味着你对叶子节点执行了原地操作。解决方法不要修改叶子节点本身而是创建新的变量。# 错误 x torch.tensor([1.0], requires_gradTrue) x.add_(1.0) # 会报错 # 正确 x x 1.08.5 为什么第二次 backward 需要 retain_graph如果同一个计算图需要调用多次backward()例如某些对比学习、二次求导场景第二次调用会报错因为默认情况下反向传播后会释放计算图。此时需要在第一次调用时传入retain_graphTrueloss.backward(retain_graphTrue) # 中间做某些操作 loss.backward()不过频繁使用retain_graphTrue会占用更多内存生产训练中尽量避免。9. 最佳实践与工程建议9.1 每个 batch 都要 zero_grad在训练循环中一定不要忘记optimizer.zero_grad()。如果不调用梯度会持续累加导致参数更新严重偏离预期。如果你的损失函数本身需要跨 batch 累加梯度可以暂时不 zero_grad但这种做法需要足够的理解和控制力新手不建议尝试。9.2 善用 no_grad 区分训练与推理推理、计算验证集指标、保存模型结构时都应该在torch.no_grad()中进行。这能显著减少内存占用也能避免误操作导致计算图保留。def evaluate(model, dataloader): model.eval() total_loss 0 with torch.no_grad(): for x, y in dataloader: pred model(x) loss loss_fn(pred, y) total_loss loss.item() return total_loss / len(dataloader)9.3 确认哪些参数需要梯度当加载预训练模型做迁移学习时优先冻结不需要更新的层for name, param in model.named_parameters(): if name.startswith(backbone): param.requires_grad False这样可以减小显存开销也可以防止预训练知识被破坏。9.4 梯度检查手动验证计算图如果怀疑某个参数梯度不对可以用数值梯度做近似验证def numerical_gradient(f, x, eps1e-6): grad torch.zeros_like(x) for i in range(x.numel()): x_plus x.clone() x_minus x.clone() x_plus.view(-1)[i] eps x_minus.view(-1)[i] - eps grad.view(-1)[i] (f(x_plus) - f(x_minus)) / (2 * eps) return grad不过这种遍历方法只适用于小规模参数仅作为调试手段。9.5 注意内存和显存非叶子节点的默认不保存梯度正是 PyTorch 节省内存的设计。所以在日常代码中不要随意调用retain_grad()如果只是为了打印中间结果使用tensor.item()或tensor.detach()更合适。detach()会返回一个新的张量和原始张量共享数据但不参与计算图。它常用于输出可视化保存中间特征计算不要求梯度的指标feature hidden_layer.detach()9.6 二次求导需要 grad 参数如果你要计算 Hessian 或实现某些高阶优化算法需要对梯度继续求导。此时可以让某个叶子节点的梯度作为后续计算的一部分但必须保留计算图。相关 API 包括torch.autograd.grad和create_graphTrue。例如x torch.tensor([2.0], requires_gradTrue) y x ** 3 grad, torch.autograd.grad(y, x, create_graphTrue) second_grad, torch.autograd.grad(grad, x) print(second_grad) # tensor([12.]) 因为 6*x12这类高阶求导在自然语言处理、对抗样本生成中偶尔会用到但入门阶段不必深究。10. 总结与下一步学习建议这篇教程围绕反向传播中的核心概念做了系统梳理。关键结论可以归纳成几点计算图是反向传播的基础PyTorch 在前向传播时构建动态图反向传播时沿着图传递梯度。叶子节点是用户直接创建、可作为优化对象的张量通常是模型参数非叶子节点是运算产生的中间结果默认不保存梯度。根节点通常是最终损失张量是backward()的起点。requires_grad控制张量是否参与梯度计算False可以用于冻结参数和推理阶段。grad属性保存叶子节点累积的梯度训练时要通过zero_grad()清零。如果你已经掌握了这些概念下一步可以继续学习torch.autograd.Function如何自定义反向传播函数实现自定义算子。梯度裁剪与梯度累积解决大 batch 训练中的显存问题。hooks机制通过register_hook打印和修改中间梯度。学习率调度与优化器原理理解SGD、Adam与梯度的关系。反向传播本身不难难的是把计算图、叶子节点、梯度的生命周期串起来理解。建议你手写几个小的张量运算手动推导一下梯度公式再对比 PyTorch 的输出这样会比单纯看文章深刻得多。如果这篇教程对你有帮助可以收藏备用也欢迎在评论区分享你在反向传播中踩过的坑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询