ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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 的自动求导机制可以概括为两步:

  1. 前向传播时:张量之间发生运算,PyTorch 会记录这些运算关系,构建一张有向无环图(DAG)。
  2. 调用.backward()时:PyTorch 从当前标量损失出发,沿着这张图反向计算梯度,并把梯度存储到对应张量的.grad属性中。

理解这张“图”是怎么构建的,以及图中每个节点扮演什么角色,是理解反向传播的关键。

2. 环境准备与版本说明

本文所有示例基于以下环境:

  • 操作系统:Windows / Linux / macOS 均可
  • Python 版本:3.8 及以上
  • PyTorch:1.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_grad=True) 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...>

可以看到:

  • 用户直接创建的x,grad_fn为None。
  • 由运算得到的y、z,grad_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_grad=True) b = torch.tensor([0.0], requires_grad=True)

w和b都是叶子节点,因为它们是直接创建的。

叶子节点的特点:

  • is_leaf属性为True。
  • grad_fn为None(因为不是通过运算产生的)。
  • 反向传播后,grad属性中会保存梯度(如果requires_grad=True且参与了前向计算)。

在深度学习中,模型的权重参数通常就是叶子节点。它们是我们希望优化的对象。

4.2 非叶子节点:运算得到的中间结果

通过张量运算得到的张量,属于非叶子节点(Non-leaf Tensor)。

例如:

x = torch.tensor([2.0, 3.0], requires_grad=True) 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)。

例如下面代码中的loss:

pred = model(x) loss = loss_fn(pred, target) loss.backward()

loss就是反向传播的起点,也就是根节点。

需要注意,PyTorch 官方文档中其实很少直接使用“根节点”这个术语,更多是在解释计算图拓扑结构时使用。社区中常见的说法是:

  • 叶子节点:没有父节点的节点(用户创建)
  • 根节点:没有子节点的节点(最终输出)

一个计算图只有一个最终输出(通常是标量损失),所以根节点往往只有一个。如果输出是向量,需要调用.backward(gradient=...)传入外部梯度,或用.sum()之类的操作先变成标量。

4.4 通过代码理解节点类型

我们用一个简单的例子来验证:

import torch # 叶子节点 x = torch.tensor([2.0], requires_grad=True) w = torch.tensor([3.0], requires_grad=True) # 非叶子节点 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: None

x的梯度是w的值 3,验证了链式法则:( \frac{\partial z}{\partial x} = w = 3 )。

w的梯度是x的值 2,( \frac{\partial z}{\partial w} = x = 2 )。

y.grad为None,因为默认不保存中间节点梯度。

这就是理解“叶子节点”和“非叶子节点”实际意义的绝佳例子:只有叶子节点默认会获得梯度(当然要求它requires_grad=True且参与反向传播路径)。

5. requires_grad 的深度理解

5.1 requires_grad 决定是否需要梯度

requires_grad是张量的一个布尔属性:

  • False:不需要梯度,运算不会被记录到计算图中(或者说不会追踪该张量的运算)。
  • True:需要梯度,PyTorch 会追踪该张量的所有操作,以便自动求导。

默认情况下,普通张量的requires_grad=False。只有nn.Parameter或手动设置的张量才会是True。

a = torch.randn(3) print(a.requires_grad) # False b = torch.randn(3, requires_grad=True) print(b.requires_grad) # True

5.2 requires_grad 与计算图构建的关系

如果某个张量requires_grad=False,那么由它参与计算得到的输出,通常也不会requires_grad=True(除非有另一个requires_grad=True的张量参与)。

举个例子:

x = torch.tensor([1.0], requires_grad=True) w = torch.tensor([2.0], requires_grad=False) y = x * w print(y.requires_grad) # True,因为 x 需要梯度

相比之下:

a = torch.tensor([1.0], requires_grad=False) b = torch.tensor([2.0], requires_grad=False) c = a * b print(c.requires_grad) # False,所有输入都不需要梯度

这说明requires_grad会沿着计算图“传播”。只要有一条路径通向需要梯度的张量,输出张量的requires_grad就会是True。

5.3 从 requires_grad=False 到 True

有时我们希望临时标记一个张量需要梯度。可以使用x.requires_grad_(True)原地修改,但要注意:

  • 这种方法通常只对叶子节点有效。
  • 如果对非叶子节点调用,可能会破坏计算图结构,一般不建议。

更常见的方法是在创建张量时指定:

x = torch.tensor([1.0, 2.0], requires_grad=True)

如果你是加载预训练模型,希望部分层不参与训练,可以:

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_grad=True,计算图中也不会记录操作,输出张量的requires_grad为 False,也无法调用backward()。

这对应前面提到的动态图特性:每次前向传播都重新构建计算图。在推理阶段直接“不建图”,是最省资源的做法。

6. grad 属性与梯度存储机制

6.1 梯度存储在哪

当一个张量满足以下条件时,反向传播后梯度会保存在.grad属性中:

  • 是叶子节点
  • requires_grad=True
  • 在反向传播路径上参与了运算

.grad是一个与原始张量形状相同的张量。如果张量是标量,.grad也是标量张量。

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) 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_grad=True) 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_grad=True) y = x * 3 z = y * y # 默认不保存 y 的梯度 z.backward() print("未调用 retain_grad,y.grad:", y.grad)

输出:

未调用 retain_grad,y.grad: None

再看调用retain_grad()的情况:

x = torch.tensor([2.0], requires_grad=True) y = x * 3 y.retain_grad() z = y * y z.backward() print("调用 retain_grad,y.grad:", y.grad)

输出:

调用 retain_grad,y.grad: tensor([12.])

因为 ( z = y^2 ),所以 ( \partial z / \partial y = 2y = 12 )。retain_grad()使得中间节点的梯度被保存下来。

不过要谨慎使用retain_grad(),因为它会增加显存或内存占用。平时调试完最好去掉。

6.5 梯度为 None 的常见场景

下面列出几种常见的grad为None的情况:

  • 张量不是叶子节点,且没有调用retain_grad()。
  • 张量requires_grad=False。
  • 张量虽然没有参与反向传播路径(例如用它构造了损失,但计算图中没有连接)。
  • 在no_grad上下文里创建的张量。

7. 完整实战案例:手动观察线性模型的梯度

为了把这些概念串起来,我们来实现一个最简单的线性回归训练流程,并手动观察叶子节点、非叶子节点和梯度。

7.1 场景设定

假设我们有一个简单的数据集,目标是用线性模型拟合。为聚焦反向传播本身,不使用数据集,而是用一个人为构造的函数来模拟损失。

我们直接定义一个参数w,一个输入x,并计算一个简单的二次损失。

import torch # 构造叶子节点参数 w = torch.tensor([1.0], requires_grad=True) b = torch.tensor([0.0], requires_grad=True) # 输入数据 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_grad=True) b = torch.tensor([0.0], requires_grad=True) 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_grad=True) b = torch.tensor([0.0], requires_grad=True) # 优化器 optimizer = torch.optim.SGD([w, b], lr=0.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(f"epoch {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_grad=False检查参数是否被正确设置为requires_grad=True,或使用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_norm=1.0)

8.3 backward 报错:只能对标量调用

backward()默认只能对标量输出调用。如果输出不是标量,需要传入一个相同形状的gradient参数,或者先对输出做sum()/mean()等聚合操作。

错误示例:

x = torch.tensor([1.0, 2.0], requires_grad=True) 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_grad=True) x.add_(1.0) # 会报错 # 正确 x = x + 1.0

8.5 为什么第二次 backward 需要 retain_graph

如果同一个计算图需要调用多次backward()(例如某些对比学习、二次求导场景),第二次调用会报错,因为默认情况下反向传播后会释放计算图。

此时需要在第一次调用时传入retain_graph=True:

loss.backward(retain_graph=True) # 中间做某些操作 loss.backward()

不过频繁使用retain_graph=True会占用更多内存,生产训练中尽量避免。

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, eps=1e-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_graph=True。

例如:

x = torch.tensor([2.0], requires_grad=True) y = x ** 3 grad, = torch.autograd.grad(y, x, create_graph=True) second_grad, = torch.autograd.grad(grad, x) print(second_grad) # tensor([12.]) 因为 6*x=12

这类高阶求导在自然语言处理、对抗样本生成中偶尔会用到,但入门阶段不必深究。

10. 总结与下一步学习建议

这篇教程围绕反向传播中的核心概念做了系统梳理。关键结论可以归纳成几点:

  1. 计算图是反向传播的基础,PyTorch 在前向传播时构建动态图,反向传播时沿着图传递梯度。
  2. 叶子节点是用户直接创建、可作为优化对象的张量,通常是模型参数;非叶子节点是运算产生的中间结果,默认不保存梯度。
  3. 根节点通常是最终损失张量,是backward()的起点。
  4. requires_grad控制张量是否参与梯度计算,False可以用于冻结参数和推理阶段。
  5. grad属性保存叶子节点累积的梯度,训练时要通过zero_grad()清零。

如果你已经掌握了这些概念,下一步可以继续学习:

  • torch.autograd.Function:如何自定义反向传播函数,实现自定义算子。
  • 梯度裁剪与梯度累积:解决大 batch 训练中的显存问题。
  • hooks机制:通过register_hook打印和修改中间梯度。
  • 学习率调度与优化器原理:理解SGD、Adam与梯度的关系。

反向传播本身不难,难的是把计算图、叶子节点、梯度的生命周期串起来理解。建议你手写几个小的张量运算,手动推导一下梯度公式,再对比 PyTorch 的输出,这样会比单纯看文章深刻得多。如果这篇教程对你有帮助,可以收藏备用,也欢迎在评论区分享你在反向传播中踩过的坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进