PyTorch张量计算梯度,前向传播,反向传播——我真的彻底悟了!
1、标量张量、向量张量
| 名称 | 数学样子 | PyTorch 形状 (.shape) |
举例 |
|---|---|---|---|
| 标量张量 | 一个数 | () 空元组 |
torch.tensor(5.0) |
| 向量张量 | 一列 / 一行数 | (n,) 一维元组 |
torch.tensor([1., 2., 3.]) |
2、自动计算的是 “梯度”,不是 “负梯度”
先清楚计算和保存的是什么,至于具体是怎么计算的以及为什么要这么计算,继续往后看
问题:要计算最快速减少损失的方向,那就是“负梯度”,那 PyTorch 中保存的 .grad 是负梯度吗?还是梯度?
答案:.grad 保存的是「梯度」,不是「负梯度」。
也就是说,对于参数 w,w.grad 存的是:

而不是:

🔍 为什么?
-
梯度
指向的是 损失函数增长最快的方向。
-
所以,负梯度
才是 损失下降最快的方向。
但 PyTorch 的设计原则是:只负责正确计算数学导数,不替你决定优化方向。
因此:
-
.backward()计算并存储的是 数学上的偏导数(即梯度)。 -
是否取负号、如何更新参数,由你自己或优化器(如
SGD)来处理。
✅ 示例验证
import torch
w = torch.tensor(1.0, requires_grad=True)
loss = w ** 2 # loss = w²,最小值在 w = 0
loss.backward()
print(w.grad) # 输出: tensor(2.)
-
数学上:
-
所以
w.grad = 2→ 这是正梯度 -
要减小 loss,我们需要让 w 变小,即沿
方向更新:
-
其中个
lr是 学习率 learning_rate(也就是 常说的 η)
📌 总结
| 内容 | 值 |
|---|---|
w.grad 存的是 |
|
| 最速下降方向 | |
| 参数更新公式 | |
| PyTorch 是否存负梯度? | ❌ 不存!它只存真实的数学梯度 |
💡 记住:负号是在 更新参数时加上去的,不是存在
.grad里的!
3、线性函数的导数 = 全局变化率
清楚【“增加多少”的说法仅在线性关系下可直接用于有限变化;非线性情况下,导数仅适用于无穷小变化的局部近似。】
我们来彻底澄清这个看似矛盾、实则精妙的问题:
a = w · x,既然
= x = 2 跟 w 无关,为什么还能说“w 增加 1,a 就增加 2”?这合理吗?
✅ 答案是:完全合理!而且这恰恰体现了偏导数的“局部”和“瞬时”特性。
🔍 第一步:回顾函数关系
我们有:
其中:
-
x = 2(看成固定值)
-
所以实际上,在这个具体计算中,a 是 w 的线性函数:
这是一个斜率为 2 的直线!
📈 第二步:为什么“w 增加 1,a 增加 2”?
因为:
-
当前 a = 2w
-
如果 w 从 3 变成 4(+1),那么:
-
新的 a = 2
4 = 8
-
原来的 a = 2
3 = 6
-
所以 a 增加了 8 - 6 = 2
-
✅ 这不是近似,而是精确成立!因为函数本身就是线性的。
💡 关键点:当函数关于某个变量是线性的时候,偏导数就是全局的“变化率”,不随该变量的位置改变。
❓那如果函数是非线性的呢?
比如:a = ,此时
= 2w
-
如果当前 w = 3,则导数 = 6
-
“w 增加 1,a 增加约 6” → 这只是近似(实际增加
)
-
导数只在 “无穷小变化” 下精确
但在我们的情况中:
-
a =
,而 x 是常数(=2)
-
所以 a 对 w 是严格线性的
-
因此,“w 增加 1,a 增加 2” 不仅合理,而且精确
🧠 回答核心疑问
“a 对 w 的偏导等于 2,跟 w 都没有关系。但为什么还能说‘w 增加 1,a 就增加 2’?”
✅ 因为:
-
偏导数
是在这个特定前向计算上下文下的值。
-
在这个上下文中,x 是已知常数(=2),所以 a 与 w 的关系就是一条斜率为 2 的直线。
-
对于线性函数,导数 = 全局变化率,所以“增加 1 → 输出增加 2”是准确无误的。
换句话说:
“跟 w 无关” 正是它能精确预测变化的原因!
如果导数依赖于 w(如 2w),那“增加 1”的效果就会随 w 的位置变化;
但现在导数是常数(2),所以无论 w 是 3、100 还是 -5,w 增加 1,a 总是增加 2。
🌟 终极总结
| 问题 | 解释 |
|---|---|
| 偏导是 2,跟 w 无关 | 是的!因为 x 固定为 2,所以 a = 2w 是线性函数 |
| 为什么能说“w+1 → a+2” | 因为线性函数的变化率处处相同,导数 = 实际变化量 |
| 这是否普遍成立? | 只对线性关系精确成立;非线性关系下只是“无穷小”近似 |
| PyTorch 是否利用这一点? | 是的!它在反向时使用前向保存的 x=2,直接算出 |
这不是一个“巧合”,而是线性系统的优美性质:
当输入和输出成正比时,比例系数就是导数,也是任意单位变化的精确响应。
4、上游梯度
📌 什么是“上游梯度”?
“上游梯度”指的是:从计算图的后方(靠近 loss 的方向)传过来的、当前变量对最终损失(loss)的总影响。
换句话说:
-
它不是某一步自己的导数,
-
而是已经包含了后面所有步骤影响的累积梯度。
🔍 举个例子
我们有:
反向传播时,从后往前走:
第一步:计算
这个 14 表示:b 变化一点,会对 loss 造成多大影响。
当我们接下来要计算 a 对 loss 的影响 时,就需要用到这个 14。
👉 此时,对 a 来说,14 就是“上游梯度” —— 因为它是从“上游”(即更靠近 loss 的方向)传下来的总影响。
🔄 类比:水流或电流
想象一条小溪从山顶(loss)流下来,经过几个水车:
-
最下面的水车是 b,它直接被水流冲击,感受到的水流强度是 14。
-
再往上的水车是 a,它不直接接触最终水流,但通过 b 间接感受到水流。
-
那么,传给 a 的“水流强度”就是 14 —— 这就是它的“上游梯度”。
在计算 a 的梯度时:

✅ 总结一句话
“上游梯度” = 从 loss 方向传来的、当前节点之后所有变化的累积影响。
它是反向传播中“乘以前面局部导数”的那个“前面的数”。
📚 补充说明
| 术语 | 含义 |
|---|---|
| 局部导数 | 当前运算对其输入的导数(如 |
| 上游梯度 | 从 loss 一路传到当前节点的梯度值(已包含后续所有影响) |
| 本节点梯度 | 上游梯度 × 局部导数 = 当前变量对 loss 的总影响 |
例如:
-
对 a 来说:
-
局部导数(b 对 a 偏导) = 1
-
上游梯度(loss 对 b 偏导) = 14
-
所以 a 的梯度 =
-
5、计算图
📌 什么是“计算图”(Computation Graph)?
计算图是一种用图形表示数学计算过程的方式:
节点(Node) 表示变量或中间结果;
边(Edge) 表示运算操作(如加法、乘法);
整个图从输入开始,到输出结束,完整记录了前向计算的每一步。
📌 “计算图” 什么时候开始创建?
计算图是在前向传播(forward pass)过程中,随着张量运算的执行而“实时动态创建”的。
PyTorch 在前向传播时,会为每个带
requires_grad=True的张量构建动态计算图。只要参与运算的张量中至少有一个设置了
requires_grad=True,PyTorch 就会自动记录该操作,并构建计算图的一部分。
有了这张图,系统就能自动反向遍历它来计算梯度——这就是自动微分的基础。
🔢 举个例子:手动画一个计算图
考虑以下计算:

假设 w = 3, x = 2。
✅ 前向计算流程(从左到右):
w ──┐
× ──→ a ──→ + ──→ b ──→ ^2 ──→ loss
x ──┘ ↑
1
-
圆形/方框节点:
w,x,a,b,loss(变量) -
操作节点:
×(乘法)、+(加常数1)、^2(平方)
这个结构就是计算图。
🧠 计算图的作用
1. 前向计算(Forward Pass)
-
按图从左到右计算:
-
先算
a = w * x = 6 -
再算
b = a + 1 = 7 -
最后算
loss = b² = 49
-
-
所有中间值(
a=6,b=7)会被临时保存,因为反向要用。
2. 反向传播(Backward Pass)
-
从
loss开始,沿着图反向走(从右到左); -
每经过一个操作,就用该操作的求导规则计算局部导数;
-
把上游传来的梯度 × 局部导数,得到当前节点的梯度。
例如:
-
loss = b²→ 局部导数:2b = 14 -
b = a + 1→ 局部导数:1 -
a = w * x→ 局部导数:x = 2
最终得到:
💡 计算图让链式法则变得可执行:每一步都知道“怎么求导”和“从哪接收梯度”。
🖥️ 在 PyTorch 中,计算图是“动态”的
-
PyTorch 使用动态计算图(Dynamic Computation Graph):
-
每次前向计算时,实时构建一张新图;
-
反向传播完成后,图就被释放(除非设置
retain_graph=True)。
-
-
这使得代码像普通 Python 一样灵活(可以写循环、条件判断),而图会自动适应实际执行路径。
例如:
y = x * 2
if y.sum() > 0:
z = y ** 2 # ← 图只包含这一分支
else:
z = y.abs()
计算图会根据 if 的结果动态生成。
📚 关键总结
| 概念 | 说明 |
|---|---|
| 计算图 | 用节点和边表示整个计算流程的有向图 |
| 节点类型 |
- 输入/参数(如 - 中间变量(如 - 输出(如 |
| 边的含义 | 数学运算(+、×、sin、exp 等) |
| 为什么需要它? | 自动微分必须知道“每一步是怎么算出来的”,才能正确应用链式法则 |
| PyTorch 特点 | 动态图:每次前向重新构建,用完即焚,灵活但不可复用(默认) |
💡 类比理解
想象你在组装一台机器:
-
前向计算 = 按说明书一步步装零件,最后得到成品;
-
计算图 = 那份说明书,记录了每个零件怎么装、依赖哪些前置步骤;
-
反向传播 = 出现问题后,按说明书倒着查:哪个零件松了?影响有多大?
没有说明书(计算图),你就无法定位问题根源(梯度)。
✅ 一句话记住:
计算图 = 前向计算的“施工蓝图”,也是反向求导的“导航地图”。
6、如何计算梯度的
前面的所有铺垫,就是为了这里
📌 梯度究竟是如何计算的?——从“前向”到“反向”的完整过程
我们通过一个具体例子,一步步讲清楚: 计算机(或人)是如何算出“损失对某个参数的梯度”的?
🧮 1. 我们的计算任务
假设我们有以下三步计算:
其中:
-
w 和 x 是输入(比如模型参数或数据)
-
是最终结果(比如误差大小)
已知当前数值:
-
w = 3
-
x = 2
我们的目标是:求 ,即“当 w 变化一点点时,loss 会怎么变?”
🔁 2. 第一步:前向计算(Forward Pass)
✅ 什么是“前向计算”?
前向计算就是按照公式顺序,从输入开始,一步一步算出最终结果的过程。
就像你手动解题一样:先算中间值,再算最后答案。
📝 在本例中:
-
先算第1步:
-
再算第2步:
-
最后算第3步:
✅ 所以,前向计算的结果是:
这些中间值都会被暂时记住,因为反向计算时要用到它们。
🔙 3. 第二步:反向传播(Backward Pass)
✅ 什么是“反向传播”?
反向传播是从最终结果(如 loss)出发,倒着往回走,计算每一步输入对最终结果的“影响程度”,这个影响程度就是梯度。
它的数学基础是 链式法则(Chain Rule):
整体变化 = 每一段局部变化的乘积。
🔄 反向传播分三步进行:
▶ 第1段:b 对 loss 的影响
我们知道:
对 b 求导(即“b 变一点,loss 变多少?”):
但注意:这里的 b 不是符号,而是前向计算得到的具体数 b = 7。 所以代入得:
👉 这个 14 就是传给上一步的“上游梯度”。
▶ 第2段:a 对 b 的影响
我们知道:
对 a 求导:
意思是:a 增加 1,b 就增加 1(精确成立,因为是线性关系)。
于是,a 对 loss 的总影响为:(这里就用到了链式法则)
▶ 第3段:w 对 a 的影响
我们知道:
对 w 求偏导(把 x 当作常数):
而前向计算中 x = 2,所以:
意思是:w 增加 1,a 就增加 2(因为 ,这是线性关系,所以精确成立)。
于是,w 对 loss 的总影响(即梯度)为:
这个 28 就是最终计算出的梯度值,也就是:
在自动微分系统(比如 PyTorch、TensorFlow)或手动计算中,这个值会被保存下来,通常称为 w 的梯度(gradient of w)。
🔍 更具体地说:
-
在训练神经网络时,我们用这个梯度来更新参数 w,例如:
其中 是学习率(比如 0.01),而
就是我们刚算出的值。
-
所以,28 不仅是一个中间结果,更是决定参数如何调整的关键数值。
-
系统会把这个值存储在
w.grad(PyTorch)或类似字段中,供优化器使用。
📝 补充说明:为什么是“保存”而不是“丢掉”?
因为在前向计算中,我们只关心 loss 的值是多少(比如 49); 但在反向传播中,我们关心的是 每个可学习参数对 loss 的影响有多大。
所以:
-
前向:算出 loss = 49(用于评估好坏)
-
反向:算出
,
(用于改进模型)
这些梯度值必须被保存下来,否则就无法更新参数。
| 项目 | 内容 |
|---|---|
| 计算结果 | |
| 是否保存 | 是,作为参数 w 的梯度 |
| 用途 | 用于参数更新(如梯度下降) |
| 存储位置 | 与变量 w 关联的 .grad 字段(在框架中)或手动记录(在纸上/代码中) |
📚 4. 关键名词解释
| 名词 | 含义 |
|---|---|
| 前向计算(Forward Pass) | 从输入开始,按顺序计算每一步,直到得到最终输出(如 loss)的过程。目的是得到所有中间变量的具体数值。 |
| 反向传播(Backward Pass) | 从最终输出开始,倒着计算每个变量对输出的影响(即梯度)的过程。核心是链式法则。 |
| 梯度(Gradient) | 表示某个变量对最终结果的“敏感程度”。数学上就是偏导数,如 |
| 链式法则(Chain Rule) | 复合函数求导的规则。例如:若 |
| 局部导数(Local Derivative) | 某一步运算对其输入的导数,如 |
| 上游梯度(Incoming Gradient) | 从后往前传来的“当前对最终结果的影响”,用于乘以局部导数,得到更前面变量的梯度。 |
💡 5. 为什么能说“w 增加 1,a 就增加 2”?
因为在这个计算中,x 被当作固定值(=2),所以:
这是一个关于 w 的线性函数。
-
线性函数的特点:变化量 = 系数 × 自变量变化量
-
所以 w 增加 1 → a 增加
(精确成立)
⚠️ 注意:这种说法只在线性关系下对有限变化成立。 如果是非线性关系(如 ),导数(如
)只在变化量趋于 0 时精确,大变化时只是近似。
✅ 6. 总结流程
-
前向计算:
-
输入
-
算出
-
保存所有中间值
-
-
反向传播:
-
从
开始,计算每一步的局部导数
-
用链式法则将“影响”逐层传回
-
最终得到
-
🎯 梯度的本质:不是凭空算出来的,而是依赖前向计算的具体数值 + 每一步的数学求导规则 + 链式法则的组合。
7、loss 可以是什么
📌 损失函数(Loss)与梯度计算
1. 损失函数的来源
损失函数并不必须直接由 w 和 b 得来,而是可以由模型的所有可训练参数通过一系列运算得到。实际上,在深度学习中,损失通常是模型输出与真实标签之间的某种差异度量(如均方误差、交叉熵等),而模型输出是由输入数据经过多层神经网络变换(即权重和偏置的作用)后得出的。
例如,假设我们有一个简单的线性回归模型:
其中:
-
是权重,
-
是偏置,
-
是输入特征,
-
是预测值。
损失函数可能是预测值 与真实值
之间的均方误差(MSE):
在这个例子中,虽然 直接依赖于
,但因为
又依赖于
和
,所以最终
也会对
和
的变化敏感。
2. 反向传播的本质
反向传播的核心是利用链式法则,从最终的损失函数开始,逐层向前计算每个可训练参数对损失的导数(即梯度)。因此,只要损失函数可以通过一系列可微操作追溯回某个参数,那么就可以计算出该参数的梯度。
例如,考虑以下更复杂的计算图:
w ──┐
× ──→ a ──→ + ──→ b ──→ ^2 ──→ loss
x ──┘ ↑
c ──┐
│
e ──→ sin ──→ d ──┐
│ │
└─── exp ───────-─┘
即使 loss 最终由 b 和 d 计算得来,但由于 b 和 d 都间接依赖于 w,因此仍然可以计算出 。
3. 不可训练参数的情况
如果某些变量或中间结果不是可训练参数(即没有设置 requires_grad=True),它们仍然会参与前向计算和反向传播中的梯度计算,但不会被赋予 .grad 属性。例如:
# 假设 x 是输入数据,不需要求梯度
x = torch.tensor([2.0], requires_grad=False)
w = torch.tensor([3.0], requires_grad=True)
a = w * x # 这里的乘法操作会被记录到计算图中
loss = a ** 2
loss.backward()
print(w.grad) # 输出: tensor([24.])
在这种情况下,尽管 x 不需要求梯度,但它仍然会影响 w 的梯度计算。
🚀 关键点总结
| 项目 | 解释 |
|---|---|
| 损失函数来源 | 损失函数不一定要直接由 w 和 b 得来,它可以是任何可微分的操作组合的结果,只要这些操作最终能追溯回 w 和 b。 |
| 反向传播条件 | 只要损失函数可以通过一系列可微操作追溯回某个参数,就能计算出该参数的梯度。 |
| 不可训练参数 | 即使某些变量没有设置 requires_grad=True,它们仍然会参与计算图的构建,影响其他可训练参数的梯度计算。 |
✅ 实际应用中的例子
在实际的神经网络训练中,损失函数通常由网络最后一层的输出(即预测值)与真实标签之间的差异计算得来。例如,在分类任务中,常用的损失函数是交叉熵损失:
其中:
-
是第
类的真实标签(one-hot 编码),
-
是模型预测的概率分布。
尽管这个公式看起来与 w 和 b 没有直接关系,但实际上, 是由输入数据经过多层神经网络变换(包括多个
w 和 b)后得出的。因此,损失函数依然能够通过反向传播计算出所有 w 和 b 的梯度。
✅ 一句话总结:
loss 只要能通过计算图连接到参数,就能求梯度(不需显式包含)。
8、loss 只能是 0维 张量(学到这里时这样理解)
loss 可以是 0维 张量,也可以是向量,但学到这里时,只知道 loss 只能是 0维张量 就行了
只有标量才能调用 .backward()(除非指定 gradient 参数)
调用 .backward() 时,loss 必须是一个标量张量(0 维张量),不能是向量或高维张量。
🔍 详细解释:
-
在 PyTorch 中,
.backward()默认只支持从标量出发的反向传播。 -
原因:
-
优化目标是一个单一数值(如总误差);
-
梯度定义为 标量对向量的导数,结果是一个与参数同形状的向量;
-
若
loss是向量(如w * x当w和x是向量时),则导数是 Jacobian 矩阵,不符合优化器输入要求。
-
-
因此,像
loss = w * x(结果为向量)不能直接调用.backward(),会报错:
RuntimeError: grad can be implicitly created only for scalar outputs
✅ 正确做法:将 loss 聚合为标量
| 方法 | 说明 |
|---|---|
loss = (w * x).sum() |
最常用,表示总损失 |
loss = (w * x).mean() |
平均损失,适合 batch 训练 |
loss = torch.sum(w * x) |
等价于 .sum() |
📝 示例:
w = torch.tensor([2.0, 3.0], requires_grad=True)
x = torch.tensor([4.0, 5.0])
# ❌ 错误:loss 是向量(shape=[2])
# loss = w * x
# loss.backward() # 报错!
# ✅ 正确:转为标量张量(shape=[])
loss = (w * x).sum() # = 8 + 15 = 23
loss.backward() # 成功
print(w.grad) # tensor([4., 5.])
✅ 结论:
loss必须是标量张量(0 维),可通过.sum()、.mean()等聚合操作实现。
📚 补充:什么是“标量张量”?
-
Python 的
float≠ PyTorch 的标量张量; -
标量张量:
torch.tensor(3.14),其shape = torch.Size([]),dim() = 0; -
只有这种 0 维张量才能直接调用
.backward()。
✅ 一句话总结:
loss 必须是标量张量,才能触发自动反向传播(向量需先聚合)。
只有标量才能调用
.backward()(除非指定gradient参数)
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)