从训练模型时的“梯度消失”或“梯度为 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 及以上
- 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: Nonex的梯度是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) # True5.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.08.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. 总结与下一步学习建议
这篇教程围绕反向传播中的核心概念做了系统梳理。关键结论可以归纳成几点:
- 计算图是反向传播的基础,PyTorch 在前向传播时构建动态图,反向传播时沿着图传递梯度。
- 叶子节点是用户直接创建、可作为优化对象的张量,通常是模型参数;非叶子节点是运算产生的中间结果,默认不保存梯度。
- 根节点通常是最终损失张量,是
backward()的起点。 - requires_grad控制张量是否参与梯度计算,
False可以用于冻结参数和推理阶段。 - grad属性保存叶子节点累积的梯度,训练时要通过
zero_grad()清零。
如果你已经掌握了这些概念,下一步可以继续学习:
torch.autograd.Function:如何自定义反向传播函数,实现自定义算子。- 梯度裁剪与梯度累积:解决大 batch 训练中的显存问题。
hooks机制:通过register_hook打印和修改中间梯度。- 学习率调度与优化器原理:理解
SGD、Adam与梯度的关系。
反向传播本身不难,难的是把计算图、叶子节点、梯度的生命周期串起来理解。建议你手写几个小的张量运算,手动推导一下梯度公式,再对比 PyTorch 的输出,这样会比单纯看文章深刻得多。如果这篇教程对你有帮助,可以收藏备用,也欢迎在评论区分享你在反向传播中踩过的坑。