想象你是一家连锁奶茶店的会计。深夜总部审计师来查账,想知道“如果白糖进货价涨 1 元,咱们店里 8 款饮品的成本分别会浮多少?”你当然可以重新算一遍所有成本表,但更聪明的做法是提前准备一张“变化率账本”:每一行对应一款饮品,每一列对应一种原料,格子里只写“原料涨价 1 元,这款饮品成本变化多少”。有了这个账本,审计师可以沿着行读,也可以沿着列读。沿着一列读,是问“一种原料的变动会怎样影响所有饮品”,这是前向模式想做的事;沿着一行反过来汇总,是问“如果希望某款饮品利润提高 1 元,每种原料价格应该承担多少反馈”,这是反向模式想做的事。这两种读法,就是雅可比向量积 JVP 与向量雅可比积 VJP。AI 训练里的反向传播,本质上是把无数个“反馈分摊”动作串成一条流水线;这篇文章带你把这本账彻底看透。
理解 JVP 与 VJP,就是拿到自动微分引擎的“内部汇率表”。无论你调 PyTorch、写自定义算子,还是开发 AI 编译器,都能精确预测一次反向传播到底在算什么。
目录
可视化演示
展示JVP/VJP三维度:链式法则对比(JVP前向链式J*v沿输入到输出传播每步用局部雅可比乘切向量VJP反向链式J^T*v̄沿输出到输入传播)、计算图实现(JVP前向流式传播无需保存中间结果内存O(1)VJP需保存前向中间值反向重放内存O(n)以空间换时间)、选型决策(n小于m少输入多输出选JVP需O(n)次前向n大于m多输入少输出选VJP需O(m)次反向DL中n远大于m必选VJP),三模式切换理解自动微分底层运算机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+C1+'
雅可比矩阵:变化率账本
你是奶茶店会计,总部审计师每个月都会来查“价格对利润的影响”。你不会把所有价格重新算一遍,而是提前维护一张表:行是 8 款饮品,列是 6 种原料,每个格子只记一个变化率——原料涨价 1 元,某款饮品的成本变动多少。这张表不记录绝对价格,只记录“彼此拉扯的敏感度”,学名叫雅可比矩阵。
把业务翻译成数学语言:函数 \(f:\mathbb{R}^n \to \mathbb{R}^m\) 有 \(n\) 个输入和 \(m\) 个输出。雅可比矩阵的第 \(i\) 行第 \(j\) 列,就是第 \(i\) 个输出对第 \(j\) 个输入的偏导数。于是整张表有 \(m\) 行、\(n\) 列,像一本竖着排的账本。
账本一旦建立,审计师就拥有了两种提问方式。他可以拿笔指着某一列问:“如果白糖涨 1 元,所有饮品分别怎样变?”也可以拿着总部的反馈单问:“如果珍珠奶茶利润要提高 1 元,每种原料各要承担多少?”前者引出 JVP,后者引出 VJP。
\displaystyleJ = \begin{bmatrix}\frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n} \\\vdots & \ddots & \vdots \\\frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n}\end{bmatrix} \in \mathbb{R}^{m \times n}- 行对应输出矩阵有 \(m\) 行,每一行是某个输出分量 \(f_i\) 对全部输入的偏导。
- 列对应输入每一列是某个输入 \(x_j\) 对全部输出分量的影响。
- 形状由函数决定输出数 \(m\) 决定行数,输入数 \(n\) 决定列数。
JVP前向模式:顺着账本往前读
某天审计师问:“白糖价格涨 1 元,店里 8 款饮品利润各受影响多少?”你不需要把 8 款饮品逐一重新核算,只需要把“白糖”这一列的敏感度取出来,乘上“涨 1 元”这个幅度。这就是雅可比向量积 JVP:给定一个输入方向向量 \(v\),把它从账本左边乘进去,得到输出方向向量。
\(v\) 不是一个具体的新价格,而是一个“扰动方向”。例如 \(v_j=1\) 表示第 \(j\) 个输入动 1 个单位,其他输入不动。前向模式会把这个方向传给每一行:每一行和 \(v\) 做内积,得到该输出沿这个方向的变化率。
在复合函数里,JVP 从输入层一路向前传播:先算第一个操作对方向的响应,再把结果当作下一个操作的输入方向。每一步都不需要构造完整雅可比矩阵,只做“局部雅可比乘向量”。所以 JVP 是前向模式的引擎,特别适合回答“单输入方向如何影响所有输出”这类问题。
- 定义方向向量\(v\) 的每个分量 \(v_k\) 表示第 \(k\) 个输入施加的扰动幅度。
- 每行做点积第 \(i\) 个输出方向变化等于雅可比第 \(i\) 行点乘 \(v\)。
- 结果是输出侧向量因为有 \(m\) 个输出,所以 \(Jv\) 是 \(m\) 维向量。
VJP反向模式:拿着反馈单倒着分摊
总部这次发来一张反馈单:“珍珠奶茶利润要提高 1 元,请告诉我每种原料进货价应该分摊多少调整。”这不再是问“白糖涨价会怎样”,而是问“要让某个输出变化,每个输入该承担多少”。你手里的算法就是把账本转置,再把反馈向量从输出端往输入端乘回去,这就是向量雅可比积 VJP。
数学上,输出侧反馈用向量 \(u\) 表示,\(u_i\) 是第 \(i\) 个输出拿到的梯度信号。VJP 计算 \(J^T u\),得到一个 \(n\) 维向量,正好是每个输入的分摊值。它和 JVP 不是换汤不换药:JVP 固定输入方向,VJP 固定输出反馈,两者互为转置关系。
在神经网络中,损失 \(L\) 是一个标量,所以输出维度 \(m=1\)。反向传播只需要一次 VJP,就能把损失的反馈分摊给所有参数。这就像总部只关心“总利润”一个数,却可以一口气算出几十万个原料商各自该承担多少——这就是反向传播高效得可怕的原因。
- 输出反馈向量\(u\) 的每个分量是某个输出拿到的上游梯度。
- 转置后与反馈点积第 \(j\) 个输入的分摊值,是所有输出对它的偏导与对应反馈的加权和。
- 结果回到输入空间因为输入有 \(n\) 个,所以 \(J^T u\) 是 \(n\) 维向量。
计算图实现:流水线上的账房
真实模型不是一张现成的雅可比表,而是一长串操作:煮茶、加糖、加奶、封装。每个操作都是一个“账房站”,它只知道自己局部的输入输出和局部变化率。把这些站串成一张有向图,就是计算图。
前向传播时,模型按原顺序执行每个操作,同时把中间结果记在小本子上;反向传播时,模型从最后一个站出发,把损失反馈逐站往回传。每个站收到上游反馈后,计算自己这个操作的局部 VJP,得到对本站输入的梯度,再传给上一个站。整个过程中,没有任何一站会张开一张完整的大雅可比矩阵。
这种“边算边记、倒着结算”的流水线让自动微分可以处理任意深度的网络。代价是反向传播必须保存前向的中间结果,相当于每个账房站都留了一沓底单,内存占用因此也会上升。
- 链式法则连乘复合函数的雅可比矩阵是各个操作局部雅可比的连乘。
- 反向逐层传播反馈每个操作站把上游反馈变成对本站输入的反馈。
- 一路传到输入所有 VJP 倒序执行完,就得到输入侧梯度。
框架API设计:收银台的点单方式
不同奶茶品牌给审计师提供的查询工具不一样:有的要他自己翻厚厚的报表,有的直接问“你想问的是原料涨价还是利润反馈”。深度学习框架的 API 也是这样:把 JVP 和 VJP 封装成不同的“点单入口”。
JAX 把两种运算都摆到台面上:jvp(f, primals, tangents) 让你显式传入输入点和方向向量;vjp(f, primals) 则返回一个函数,你把输出反馈 \(u\) 喂给它,它给你 VJP 结果。PyTorch 的 loss.backward() 把反向 VJP 藏在内部,用户只需要触发一次反向传播;自定义 torch.autograd.Function 时,开发者要在 backward 里返回局部 VJP。TensorFlow 的 GradientTape 则是先录完整段前向操作,退出上下文后再统一反推梯度。
PyTorch 和 TensorFlow 默认把 VJP 作为主力,是因为训练阶段损失是标量,反向模式一次就能拿到所有参数梯度。而 JAX 把 JVP 也开放出来,方便做方向导数、高阶微分和自定义自动微分变换。
- JAX 的 jvp同时返回前向结果和输入方向 \(v\) 对应的输出方向。
- JAX 的 vjp先返回前向结果,再返回一个“反馈函数”。
- 两种 API 的差别jvp 需要你给输入方向,vjp 需要你给输出反馈。
模式选择与复杂度:哪种记账更划算
既然前向模式有 JVP,反向模式有 VJP,那到底用哪个?答案是:看账本的行数和列数。
想得到完整的雅可比矩阵 \(J\),前向模式需要分别用 \(n\) 个单位方向做 JVP:每个方向得到一列,凑齐 \(n\) 列。反向模式需要分别用 \(m\) 个单位反馈做 VJP:每个反馈得到一行,凑齐 \(m\) 行。因此,完整雅可比矩阵的成本对比非常简单。
但成本不只看次数,还要看内存。反向模式需要保存前向中间结果,像流水线上每个账房都要留底单;前向模式则不用保存太多中间状态,内存压力更小。所以实际选择要综合“输入输出维度、网络深度、内存预算”来做判断,而不是死记某一个结论。
- 前向覆盖列每个标准基方向对应一列,所以要 \(n\) 次 JVP。
- 反向覆盖行每个标准反馈对应一行,所以要 \(m\) 次 VJP。
- 标量损失特例\(m=1\) 时一次 VJP 就能拿到全部梯度。
AI 应用场景
神经网络训练:反向模式自动分账
阶段:模型训练 / 反向传播
解决的问题:传统手推梯度在深层网络里几乎不可能完成,数值差分既慢又易受舍入误差影响。反向模式用 VJP 把标量损失的反馈逐层分摊给每个参数,一次反向就能得到全部梯度。
具体案例:PyTorch 中调用 loss.backward() 后,自动微分引擎沿着计算图执行一系列局部 VJP:每一层的 backward 接收上游梯度,返回对权重和输入的梯度。框架设计者只需要保证每个算子提供正确的局部 VJP,就可以组合出任意的深度网络。
灵敏度分析:实时回答“涨价会怎样”
阶段:模型分析 / 输入扰动影响评估
解决的问题:风控或量化场景中,经常要评估某个输入特征的小幅扰动会如何影响多个预测结果。逐样本重算代价太高,而 JVP 可以直接沿指定输入方向传播变化率,相当于审计师沿着账本某一列快速扫过去。
具体案例:一家金融科技公司用神经网络同时预测多个风险指标,风控经理问“贷款利率上调 0.1 个百分点,违约率、回收率、客户流失率分别会变化多少?”用 JAX 的 jvp 传入扰动方向,一次前向即可拿到所有输出对该维度的敏感度,不用重新训练模型。
AI编译器:根据形状自动选择模式
阶段:编译优化 / 自动微分变换
解决的问题:AI 编译器在把模型编译到 GPU 或 NPU 时,需要决定用 JVP 还是 VJP 来实现梯度。传统手工实现一个算子一个样,维护成本高。基于形状分析:输入维度大、输出维度小时选反向;输入维度小、输出维度大时选前向。
具体案例:在 JAX 的 jvp 与 vjp 原语之上,编译器可以自动把 grad(f) 展开为 VJP 组合;对于某些特殊算子,也可以用 JVP 做更省内存的高阶微分。自定义算子只要声明局部 JVP/VJP,编译器就能统一调度。
Python 演示
import math def f(x, y): """从 R^2 到 R^2 的示例映射: out1 = x^2 + y out2 = sin(x) * y """ return [x * x + y, math.sin(x) * y] def jacobian(x, y): """返回示例映射在 (x, y) 处的 2x2 雅可比矩阵。""" return [ [2.0 * x, 1.0], [math.cos(x) * y, math.sin(x)] ] def jvp(x, y, vx, vy): """前向模式:J * v。""" J = jacobian(x, y) return [ J[0][0] * vx + J[0][1] * vy, J[1][0] * vx + J[1][1] * vy ] def vjp(x, y, ux, uy): """反向模式:J^T * u。""" J = jacobian(x, y) return [ J[0][0] * ux + J[1][0] * uy, J[0][1] * ux + J[1][1] * uy ] if __name__ == "__main__": x, y = 0.5, 1.5 vx, vy = 1.0, 0.0 print("f =", f(x, y)) print("J =", jacobian(x, y)) print("JVP =", jvp(x, y, vx, vy)) print("VJP =", vjp(x, y, 1.0, 0.0))
def f(x, y):定义一个双输入双输出的示例函数,相当于奶茶店的“成本转利润”规则。def jacobian(x, y):手工构造 \(2 \times 2\) 账本,第一行是 \(f_1\) 对 \(x,y\) 的偏导,第二行是 \(f_2\) 对 \(x,y\) 的偏导。J[0][0] * vx + J[0][1] * vy这就是“雅可比第一行点乘方向向量”,对应 JVP 的第一个输出分量。J[0][0] * ux + J[1][0] * uy这是 \(J^T u\) 的第一个分量,把所有输出反馈汇总到输入 \(x\) 上,对应 VJP。print("JVP =", ...)固定输入方向 \((1,0)\),等价于只让 \(x\) 变化,观察两个输出分别怎么变。print("VJP =", ...)固定输出反馈 \((1,0)\),等价于只给第一个输出 1 的梯度,看每个输入各得到多少分摊。
常见误区
误区JVP 是 \(Jv\),VJP 是 \(J^Tu\),不都是“矩阵乘向量”吗?转置一下就好,理解成一个东西完全没问题。
正解两者虽然都是矩阵乘向量,但语义完全不同。JVP 从输入侧给定“扰动方向”,回答的是“一个输入方向如何影响所有输出”;VJP 从输出侧给定“反馈梯度”,回答的是“一个输出反馈如何分摊给所有输入”。就像账本里“问白糖涨价”和“问利润反馈”是两类截然不同的问题,方向相反、信息流向相反。
为何常见:矩阵公式长得太像,初学者容易只盯着乘法符号,忽略自动微分里“从哪一侧进入”才决定前向还是反向。
误区每次 `backward()` 都要先算出完整雅可比矩阵,再拿去乘梯度向量,所以显存爆炸是因为雅可比矩阵太大。
正解框架不会显式构造完整雅可比矩阵。计算图的每个操作只保存局部雅可比,或者更准确地说是局部 VJP 规则;反向传播沿途做矩阵向量乘,而不是把整张大表展开。真正让显存上升的往往是前向保存的中间激活值,而不是一张巨型 \(J\)。
为何常见:数学推导里 \(J\) 作为整体出现,很容易让人以为软件也把整块矩阵建出来。
误区反向传播是“倒着算”,所以 VJP 应该是在对雅可比矩阵求逆,把输出梯度变回输入梯度。
正解VJP 用的是转置,不是逆。转置是把反馈按列汇总回输入空间;求逆则要求方阵且可逆,绝大多数网络映射根本不存在可逆雅可比。倒着读账本不等于把账本翻个底朝天,而是把行和列交换后做加权汇总。
为何常见:“反向”一词太有迷惑性,让人联想到逆运算。
误区反向传播在深度学习中无敌,所以任何情况下只要输入维度大于输出维度,直接选反向模式就对了。
正解反向模式确实适合 \(m=1\) 的标量损失,但代价是要保存大量中间状态。如果输出维度也很大,反向模式的完整雅可比需要 \(m\) 次 VJP,可能并不划算。选择模式要综合维度比和内存预算,而不是只看“输入多不多”。
为何常见:大家最常接触的是损失函数 \(m=1\),容易把反向模式的优点过度推广到所有场景。
误区JVP 是雅可比乘向量,梯度向量也是向量,所以让 \(v=(1,1,\dots,1)\) 就能一次得到所有输入参数的梯度。
正解一次 JVP 只能得到一个组合方向上的输出变化率,而不是每个输入各自的梯度。想让每个输入分别拿到自己的梯度,必须用 \(n\) 个单位方向逐一做 JVP。反向模式一次 VJP 能同时给出所有输入梯度,这才是它在训练中胜出的关键。
为何常见:把“沿某个方向的导数”误解为“对每个变量的偏导”,两者在数学上是不同的对象。
面试题
Q1. 在自动微分中,JVP 是哪种模式的核心计算?
- A.前向模式← 正确答案
- B.反向模式
- C.正则化模式
- D.混合精度模式
答案:A。JVP 把输入方向 \(v\) 从矩阵左边乘进去,对应信息从输入传向输出的前向模式;反向模式的核心是 VJP。
Q2. 反向传播的每一步都在执行哪种基本操作?
- A.雅可比矩阵求逆
- B.向量雅可比积VJP← 正确答案
- C.雅可比向量积JVP
- D.标量除法
答案:B。反向传播把输出侧反馈沿计算图传回输入侧,每一步都是局部 VJP,也就是 \(J^Tu\) 或它的复合形式。
Q3. 若 \(f\) 把 \(\mathbb{R}^n\) 映射到 \(\mathbb{R}^m\),雅可比矩阵的形状是?
- A.\(n \times n\)
- B.\(n \times m\)
- C.\(m \times n\)← 正确答案
- D.\(m \times m\)
答案:C。输出数 \(m\) 决定行数,输入数 \(n\) 决定列数,所以形状是 \(m \times n\)。
Q4. 关于 JVP 与 VJP 的说法,正确的有?
- A.JVP 沿输入方向传播变化率← 正确答案
- B.VJP 沿输出反馈传播梯度← 正确答案
- C.两者都需要显式求逆雅可比
- D.JVP 适合输入维度很大的情形
答案:AB。JVP 是给定输入扰动方向后看所有输出如何变化,VJP 是给定输出反馈后看每个输入如何分摊。两者都用雅可比矩阵乘向量,但不用求逆;输入维度很大时一次 JVP 只能覆盖一个方向,往往不划算。
Q5. 要得到标量损失 \(l=f(x)\) 对所有 \(n\) 个输入的梯度,反向模式最少需要几次 VJP?
- A.1 次← 正确答案
- B.\(n\) 次
- C.\(m\) 次
- D.\(n \times m\) 次
答案:A。标量损失对应 \(m=1\),反向模式一次 VJP 就能把损失反馈分摊给全部 \(n\) 个输入,这正是反向传播高效的根本原因。
Q6. 对于输入维度 \(n\) 很大、输出维度 \(m=1\) 的函数,哪种方式通常更划算?
- A.前向模式
- B.反向模式← 正确答案
- C.两种完全等价
- D.必须用数值差分
答案:B。完整雅可比时前向需要 \(n\) 次 JVP,反向只需要 \(m=1\) 次 VJP,因此反向模式远更划算。
Q7. 关于前向模式 JVP,下列说法正确的有?
- A.适合输入少输出多的情况← 正确答案
- B.一次JVP开销接近一次前向计算← 正确答案
- C.输出侧需要给梯度反馈
- D.它是方向导数的推广← 正确答案
答案:ABD。前向模式一次 JVP 对应一个输入方向,需要 \(n\) 次才能覆盖完整雅可比,所以适合输入少、输出多的场景;它不需要输出侧反馈,只是沿输入方向传播变化率,本质是方向导数的多维推广。
Q8. PyTorch 中调用 loss.backward(),自动微分引擎默认执行的核心操作是?
- A.前向 JVP
- B.反向 VJP← 正确答案
- C.雅可比显式求逆
- D.符号积分
答案:B。backward() 从标量损失出发,沿计算图反向传播梯度,每个节点执行局部 VJP,最终得到所有参数的梯度。
Q9. 对比 JAX 的 jvp 和 vjp 接口,正确的说法是?
- A.
jvp需要传入 tangent 方向← 正确答案 - B.
vjp返回计算输出梯度的函数← 正确答案 - C.两者返回值完全一样
- D.
jvp沿反向传播路径计算
答案:AB。jvp 需要显式给出输入方向向量;vjp 先返回前向值,再返回一个接收输出反馈并给出梯度的函数。两者返回结构不同,jvp 走前向模式,而不是反向传播路径。
Q10. 已知 \(J = \begin{bmatrix} 2 & 0 \\ 1 & 1 \end{bmatrix}\),\(v = \begin{bmatrix} 1 \\ 2 \end{bmatrix}\),则 JVP 的结果是?
- A.\([2, 2]\)
- B.\([2, 3]\)← 正确答案
- C.\([4, 3]\)
- D.\([0, 2]\)
答案:B。JVP 是 \(Jv\)。第一行 \(2\times1+0\times2=2\),第二行 \(1\times1+1\times2=3\),所以结果是 \([2,3]\)。