老王开的自助餐厅“雅记小厨”最近遇到了一个难题:他有 8 道招牌菜,每道菜的口感由盐、糖、辣椒等 5 种调料决定。老王想做新品研发,他想知道:“如果我同时把几种调料稍微调整一下,每道菜的评分分别会变化多少?”他把每道菜的评分当作一个“输出”,把每种调料的用量当作一个“输入”,这便是一张多对多的“敏感度大表”——数学上叫雅可比矩阵。可这张表太大了,每次查都费劲。后来他家的总厨给出了一个妙招:如果只关心一个具体的调整方向(比如“盐多加一点、糖少放一点、辣椒略增”),根本不需要填完整张大表,只需让每道菜“跟着调料方向一起微动”,在出锅那一刻量出各道菜的变化率即可。这个“按方向一次性测出所有输出变化率”的运算,就是雅可比向量积(Jacobian-vector product,简称 JVP)——它是前向模式自动微分的心脏,也是现代 AI 编译器中高效求导的核心原语。理解了 JVP,你就读懂了编译器里那些“微妙而快速”的求导魔法。

岗位关联

对 AI 从业者而言,JVP 是自动微分前向引擎的基石,掌握它能帮你理解编译器如何生成高效的求导内核、做算子融合与性能优化,是打通“深度学习框架到底怎么算梯度”这最后一公里的关键能力。

目录
  1. 可视化演示
  2. 多维函数
  3. 雅可比矩阵
  4. JVP 原理
  5. 前向模式
  6. 编译器应用
  7. 性能优化
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示雅可比向量积三维度:JVP vs VJP对比(JVP=J·v前向传播切向量从输入到输出VJP=J^T·v̄反向传播余切向量从输出到输入)、对偶数机制(对偶数a+bε其中ε²=0一次前向同时得到函数值和方向导数乘法规则ac+(ad+bc)ε无需保存计算图内存开销极低)、成本对比(完整雅可比JVP需O(n)次前向适合n小于m少输入多输出VJP需O(m)次反向适合n大于m多输入少输出深度学习n远大于m选VJP),三模式切换理解自动微分前向模式核心运算。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+C1+'

多维函数

雅记小厨的后厨里,盐、糖、辣椒三种调料共同决定了招牌菜“红烧肉”和“麻婆豆腐”的评分。对老王来说,一个“作料组合”(盐 0.8 勺、糖 1.2 勺、辣椒 0.5 勺)通过菜谱,就得到一个“评分组合”(红烧肉 8.5 分、麻婆豆腐 7.2 分)。这种“一组数字变成另一组数字”的关系,在数学里叫多维函数:输入是一个 n 维向量,输出是一个 m 维向量。

把三种调料按固定顺序写成一个向量,两道菜的评分也按固定顺序写成一个向量,菜谱就抽象成一句话:输入向量进,输出向量出。这听起来像套娃,但它让“菜品的敏感度研究”有了统一的数学框架——我们可以像处理单个数字一样处理整组数字,于是后面所有关于“变化率”的讨论都能在向量空间里展开。

有了“向量进、向量出”的视角,老王的下一问就自然浮现:当所有调料同时变化时,每道菜的评分到底会跟着怎么变?这就要用到一本更精细的“账本”——雅可比矩阵。

f:Rn→Rm,x=[x1x2⋮xn]↦y=f(x)=[f1(x)f2(x)⋮fm(x)]f: \mathbb{R}^{n} \to \mathbb{R}^{m}, \quad \mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{bmatrix} \mapsto \mathbf{y} = f(\mathbf{x}) = \begin{bmatrix} f_1(\mathbf{x}) \\ f_2(\mathbf{x}) \\ \vdots \\ f_m(\mathbf{x}) \end{bmatrix}
这个式子只是把“输入一组数、输出一组数”这件事写得规范一些:f 是函数,x 是输入向量,y 是输出向量,每个 y_i 都由同一个 x 共同决定。
  1. 函数签名f 从 n 维实数空间映射到 m 维实数空间,输入输出都是向量
    f:Rn→Rmf: \mathbb{R}^{n} \to \mathbb{R}^{m}
  2. 输入向量把 n 种调料的用量按固定顺序摞成一个列向量
    x=[x1x2⋯xn]T\mathbf{x} = \begin{bmatrix} x_1 & x_2 & \cdots & x_n \end{bmatrix}^{\mathsf{T}}
  3. 输出向量m 道菜的评分也按固定顺序摞成一个列向量
    y=[y1y2⋯ym]T\mathbf{y} = \begin{bmatrix} y_1 & y_2 & \cdots & y_m \end{bmatrix}^{\mathsf{T}}
  4. 分量函数每一道菜的评分 y_i 都由全部调料共同决定,只是“配方”不同
    yi=fi(x),i=1,2,…,my_i = f_i(\mathbf{x}), \quad i = 1, 2, \dots, m

雅可比矩阵

老王想系统地回答“每道菜对每种调料的敏感度”。他花了一个晚上,做了一本账:每一行写一道菜,每一列写一种调料,格子里的数字是“这种调料加一勺(其他不变),这道菜评分变化多少”。这本账,数学上就叫雅可比矩阵。它把“一道菜对一种料”的零散敏感度,组织成了一张 m 行 n 列的总表。

雅可比矩阵的每个格子是偏导数——“固定其他所有调料,只动这一种调料,评分的变化率”。矩阵的行有清晰的含义:第 i 行就是第 i 道菜对所有调料的完整敏感度列表,也就是这道菜评分函数的梯度;列的含义同样直观:第 j 列告诉你“第 j 种调料动一点,所有菜分别怎么变”。如果老王研究的是“复合菜谱”——比如先做酱料、再用酱料做菜,那么复合函数的雅可比矩阵等于两步雅可比矩阵相乘,这就是链式法则的矩阵版。

账本虽全,老王却皱眉:“我每次只想算一个具体方案,难道要把整本账都翻一遍?”这便是雅可比向量积登场的时刻——它只回答“按某个方向调整时,所有输出各变化多少”,而且不需要把整个矩阵完整算出来。

Jf(x)=[∂y1∂x1∂y1∂x2⋯∂y1∂xn∂y2∂x1∂y2∂x2⋯∂y2∂xn⋮⋮⋱⋮∂ym∂x1∂ym∂x2⋯∂ym∂xn]∈Rm×nJ_f(\mathbf{x}) = \begin{bmatrix} \dfrac{\partial y_1}{\partial x_1} & \dfrac{\partial y_1}{\partial x_2} & \cdots & \dfrac{\partial y_1}{\partial x_n} \\ \dfrac{\partial y_2}{\partial x_1} & \dfrac{\partial y_2}{\partial x_2} & \cdots & \dfrac{\partial y_2}{\partial x_n} \\ \vdots & \vdots & \ddots & \vdots \\ \dfrac{\partial y_m}{\partial x_1} & \dfrac{\partial y_m}{\partial x_2} & \cdots & \dfrac{\partial y_m}{\partial x_n} \end{bmatrix} \in \mathbb{R}^{m \times n}
雅可比矩阵就是“敏感度账本”的数学形态:第 i 行第 j 列的格子 J_ij 表示第 j 种调料扰动一单位时第 i 道菜评分的变化量。
  1. 矩阵形状m 行 n 列,一行一道菜,一列一种调料
    Jf(x)∈Rm×nJ_f(\mathbf{x}) \in \mathbb{R}^{m \times n}
  2. 单个元素第 i 个输出对第 j 个输入的偏导数,刻画“局部敏感度”
    Jij=∂yi∂xjJ_{ij} = \dfrac{\partial y_i}{\partial x_j}
  3. 行视角第 i 行就是第 i 道菜的评分梯度,指示该菜如何随所有调料变
    ∇yi=[∂yi∂x1∂yi∂x2⋯∂yi∂xn]\nabla y_i = \begin{bmatrix} \dfrac{\partial y_i}{\partial x_1} & \dfrac{\partial y_i}{\partial x_2} & \cdots & \dfrac{\partial y_i}{\partial x_n} \end{bmatrix}
  4. 链式法则复合配方的敏感度 = 外层雅可比 × 内层雅可比
    Jg∘f(x)=Jg(f(x))⋅Jf(x)J_{g \circ f}(\mathbf{x}) = J_g(f(\mathbf{x})) \cdot J_f(\mathbf{x})

JVP 原理

这天老王想试一个新方案:盐加 2%、糖减 1%、辣椒增 0.5%。他把这三个数写成一个列向量 v,叫方向向量——它只记录“每种调料往哪个方向动、动多少比例”,不关心绝对数值。他要的答案是:在这个方向上,所有菜分别变化多少。雅可比向量积(JVP)就是“账本的一行和这个方向向量做点积,得到一道菜的变化量;所有行都做一遍,就得到所有菜的变化量”。

更妙的是,J·v 在数学上有一个人尽皆知的名字:方向导数——它度量 f 在 x 处沿 v 方向的瞬时变化率。它还是一阶泰勒展开的线性项:在 x 附近,函数真实变化 f(x+εv)−f(x) 约等于 ε·J·v。也就是说,JVP 是“非线性函数在某个方向的局部直线化”。

老王很满意,但总厨提醒他:“老板,账本有 8 行 5 列,每算一个方案要查 40 个格子,太累。我有个更快的法子——让每道菜'跟着调料方向走一步',出锅时直接读出变化率。”这个法子,就是前向模式的切线追踪。

Jf(x) v=[∑j=1n∂y1∂xjvj∑j=1n∂y2∂xjvj⋮∑j=1n∂ym∂xjvj]=lim⁡ε→0f(x+εv)−f(x)εJ_f(\mathbf{x})\,\mathbf{v} = \begin{bmatrix} \sum_{j=1}^{n} \dfrac{\partial y_1}{\partial x_j} v_j \\ \sum_{j=1}^{n} \dfrac{\partial y_2}{\partial x_j} v_j \\ \vdots \\ \sum_{j=1}^{n} \dfrac{\partial y_m}{\partial x_j} v_j \end{bmatrix} = \lim_{\varepsilon \to 0} \frac{f(\mathbf{x} + \varepsilon \mathbf{v}) - f(\mathbf{x})}{\varepsilon}
J·v 的每个分量是账本一行与方向向量 v 的点积,整体上它等于函数沿 v 方向的方向导数,也就是“所有调料按 v 的比例微调时,各道菜评分的瞬时变化率”。
  1. 方向向量v 的第 j 个分量表示第 j 种调料的调整比例
    v=[v1v2⋯vn]T\mathbf{v} = \begin{bmatrix} v_1 & v_2 & \cdots & v_n \end{bmatrix}^{\mathsf{T}}
  2. 逐行点积第 i 个分量 = 雅可比第 i 行与 v 的点积,等价于加权求和
    (Jv)i=∑j=1n∂yi∂xjvj(J\mathbf{v})_i = \sum_{j=1}^{n} \frac{\partial y_i}{\partial x_j} v_j
  3. 方向导数这个加权和恰好是 f 在 x 处沿 v 方向的变化率
    Jf(x)v=ddεf(x+εv)∣ε=0J_f(\mathbf{x})\mathbf{v} = \left. \frac{\mathrm{d}}{\mathrm{d}\varepsilon} f(\mathbf{x} + \varepsilon \mathbf{v}) \right|_{\varepsilon = 0}
  4. 线性近似在一阶精度下,真实变化可以用 JVP 代替
    f(x+εv)≈f(x)+ε⋅Jf(x)vf(\mathbf{x} + \varepsilon \mathbf{v}) \approx f(\mathbf{x}) + \varepsilon \cdot J_f(\mathbf{x})\mathbf{v}

前向模式

总厨的办法是“边做菜边测”:既然方向向量 v 已经告诉了每种调料的走向,那么后厨在加工任何中间原料时,都额外带一个“小尾巴”——这个中间原料对方向 v 的敏感度(切线)。比如“酱汁 = 盐 × 糖”,盐的切线是 2%,糖的切线是 −1%,酱汁的切线就用乘法法则算出:盐的切线 × 糖的数值 + 盐的数值 × 糖的切线。一路传到出锅,每道菜的评分自然带着切线,而这个切线正是 J·v 的对应分量。这种“每步都带一个切线量”的技术,叫前向模式自动微分。

数学上,前向模式先给每个输入变量绑定一个“初速度” ε·v,然后把所有运算都放到二元数(dual number)的世界里:一个数写成 value + tangent·ε,其中 ε 满足 ε²=0。加法、乘法、正弦等运算都按微积分法则自动更新切线。因为 ε²=0,所有二阶及以上的项自动消失,留下的线性项恰好就是导数。整个过程只沿着计算图从输入扫到输出,一次前向扫描得到一条方向的 JVP。

总厨的法子让老王眼前一亮。但真正把这个“边做边测”自动化、规模化、快上加快的,是看不见的后台系统——AI 编译器。它像一个超级后厨管理系统,把“切线追踪”翻译成机器指令,还能顺手合并多个灶台动作,省时省力。

x˙=v,y˙i=∑j=1n∂yi∂xjx˙j,y˙=Jf(x) v\dot{\mathbf{x}} = \mathbf{v}, \qquad \dot{y}_i = \sum_{j=1}^{n} \frac{\partial y_i}{\partial x_j} \dot{x}_j, \qquad \dot{\mathbf{y}} = J_f(\mathbf{x})\,\mathbf{v}
前向模式的关键是:方向 v 作为所有输入变量的“初速度”被注入,计算过程中每个中间量都按链式法则携带切线,最终输出向量的切线正是 J·v。
  1. 注入初速度把方向向量 v 设为每个输入变量的切线初值
    x˙j=vj,j=1,2,…,n\dot{x}_j = v_j, \quad j = 1, 2, \dots, n
  2. 局部链式法则任意中间量 t 的切线由它的所有依赖项线性叠加
    t˙k=∑p∂tk∂tpt˙p\dot{t}_k = \sum_{p} \frac{\partial t_k}{\partial t_p} \dot{t}_p
  3. 输出切线所有运算走完一遍后,输出向量 y 的切线就是 J·v
    y˙=Jf(x) v\dot{\mathbf{y}} = J_f(\mathbf{x})\,\mathbf{v}
  4. 成本对比一次前向 JVP 的成本与一次函数求值同阶,既不要建矩阵也不要额外遍历
    Cost(JVP)=O(1)×Cost(f)\mathrm{Cost}(JVP) = O(1) \times \mathrm{Cost}(f)

编译器应用

AI 编译器拿到一个深度学习模型,相当于拿到一本厚厚的菜谱。它的任务之一是自动为每个算子生成求导代码。在编译器的中间表示(IR)里,JVP 是一个“一等公民”:每个算子都有一份 JVP 规则,描述“给定输入数值和输入切线,如何算出输出数值和输出切线”。编译器沿着计算图把这些规则串起来,就自动生成了整个模型的前向模式求导程序。

编译器还有两招省时利器。第一招是算子融合:把多个操作的 JVP 规则合并成一个内核。比如“矩阵乘法 + 激活函数”的 JVP,原本要分两步读写中间结果,融合后一次循环搞定,减少访存开销。第二招是向量化批量:如果要同时试几十个方向 v,编译器把它们排成一个矩阵 V,一次算出 J·V 的所有列。这正好匹配 GPU 和 SIMD 指令——同一套切线规则,对多个方向批量执行,吞吐量大幅提升。

编译器让 JVP 又快又省,但老王这本“敏感度账本”里其实有不少空格——辣椒对糖醋排骨的甜度毫无影响。如果编译器能看到这些空格并绕开它们,JVP 还能更快。这就是性能优化的艺术。

Y˙=Jf(x) V,V=[v1v2⋯vk]\dot{Y} = J_f(\mathbf{x})\,V, \qquad V = \begin{bmatrix} \mathbf{v}_1 & \mathbf{v}_2 & \cdots & \mathbf{v}_k \end{bmatrix}
多个方向向量拼成矩阵 V 后,J·V 可以一次性算出全部方向上的输出切线,编译器只需把同一份 JVP 代码对 V 的每一列并行执行。
  1. 方向矩阵把 k 个方向向量 v_1 到 v_k 并排成 n×k 矩阵
    V=[v1v2⋯vk]∈Rn×kV = \begin{bmatrix} \mathbf{v}_1 & \mathbf{v}_2 & \cdots & \mathbf{v}_k \end{bmatrix} \in \mathbb{R}^{n \times k}
  2. 批量推进输出切线矩阵等于雅可比矩阵乘方向矩阵
    Y˙=Jf(x) V∈Rm×k\dot{Y} = J_f(\mathbf{x})\,V \in \mathbb{R}^{m \times k}
  3. 逐列解读结果第 r 列就是第 r 个方向上的 JVP
    y˙r=Jf(x) vr,r=1,2,…,k\dot{\mathbf{y}}_r = J_f(\mathbf{x})\,\mathbf{v}_r, \quad r = 1, 2, \dots, k
  4. 并行收益编译器按列拆分,让核函数同时处理多个方向
    Perf≈k×Cost(JVP)并行度\text{Perf} \approx \frac{k \times \mathrm{Cost}(JVP)}{\text{并行度}}

性能优化

老王的账本并非每一格都填满。事实上,很多“菜—调料”组合根本没有关系:辣椒放多少,不影响糖醋排骨的“甜度评分”。这样的矩阵叫稀疏雅可比矩阵。如果编译器能静态分析出哪些偏导恒为 0——比如通过数据流分析确定某个算子的两个输入之间没有依赖——它就可以在生成代码时直接跳过这些项,只计算非零路径上的切线。

除了稀疏剪枝,还有两类常用优化。一是结果复用:同一个中间切线在后续多个输出中被反复用到,编译器通过公共子表达式消除(CSE)让它只算一次。二是多路复用:上面讲过的批量 JVP,把多个方向打包成矩阵乘法,利用硬件并行度摊薄开销。三者合起来,可以让原本 O(m×n) 的朴素计算,在实际稀疏结构下降到与“非零元素总数”相当的量级。

老王看着被“瘦身”后的计算流程,感慨道:“原来算变化率,不需要翻遍整本账,也不需要大动干戈——找准方向、沿着路子走、只碰有关系的格子,答案自然就到手了。”这正是雅可比向量积在 AI 编译器中的生存智慧。

y˙i=∑j∈Si∂yi∂xjvj,Si={j  |  ∂yi∂xj≠0}\dot{y}_i = \sum_{j \in \mathcal{S}_i} \frac{\partial y_i}{\partial x_j} v_j, \qquad \mathcal{S}_i = \left\{ j \;\middle|\; \frac{\partial y_i}{\partial x_j} \neq 0 \right\}
稀疏优化的核心是:只遍历那些雅可比元素非零的(i, j)组合,把零贡献的乘法全部省掉,总计算量由矩阵元素总数降到非零元素个数。
  1. 支撑集第 i 行非零偏导对应的列号集合,即“真正影响第 i 道菜的调料”
    Si={j∣Jij≠0}\mathcal{S}_i = \left\{ j \mid J_{ij} \neq 0 \right\}
  2. 缩减求和只对支撑集内的 j 做累加,其余项直接跳过
    y˙i=∑j∈SiJij vj\dot{y}_i = \sum_{j \in \mathcal{S}_i} J_{ij}\, v_j
  3. 计算量总浮点运算量等于所有行的非零元素数之和,远小于 m×n
    FLOPs=∑i=1m∣Si∣≪m×n\text{FLOPs} = \sum_{i=1}^{m} |\mathcal{S}_i| \ll m \times n
  4. 编译期剪枝支撑集由编译器静态分析得出,运行期零开销
    Si={j∣依赖分析证明 xj 影响 yi}\mathcal{S}_i = \{ j \mid \text{依赖分析证明 } x_j \text{ 影响 } y_i \}

AI 应用场景

编译器前向模式代码生成

阶段:AI 编译器中自动微分内核的生成与优化

解决的问题:深度学习框架需要为每个算子生成求导代码。传统做法要么手写每个算子的反向传播,工作量大且易错;要么先显式构造雅可比矩阵再相乘,内存和算力开销高。基于 JVP 的前向模式将“求导规则”编成算子的 JVP 原语,编译器沿计算图自动拼接,无需手写海量导数代码。

具体案例:在 XLA 或 TVM 中,编译器开发者为一个自定义的“指数线性单元 + 批量归一化”融合算子实现 JVP 规则:给定输入数值张量、输入切线张量和方向向量,一次内核调用同时输出数值与切线。后续所有用到该算子的模型,其前向求导都由编译器自动生成并融合。

大模型训练中的方向敏感度分析

阶段:模型训练与超参数调试阶段的梯度诊断

解决的问题:大模型参数量动辄百亿,完整计算 Hessian 矩阵不现实。但工程师常只需知道“参数沿某个方向(如主成分方向)扰动时,损失变化多快”。JVP 恰好提供“方向导数”式的答案,比暴力求完整二阶信息便宜几个数量级。

具体案例:某团队训练千亿参数大模型时,用 JVP 实现 Hessian 向量积(HVP):先通过反向模式算出梯度,再对梯度做一次 JVP,得到损失函数在指定参数方向上的二阶曲率,从而判断优化器是否该增大学习率。

科学计算中的参数扰动传播

阶段:仿真模型的不确定性与敏感性分析

解决的问题:物理仿真中,输入参数(如流体黏度、初始压强)的微小扰动会怎样影响输出(如各测点温度)?传统蒙特卡洛需要反复跑成千上万次仿真;JVP 则一次前向扫描就能给出所有输出对某一扰动方向的敏感度,成本骤降。

具体案例:在气象预报模型中,工程师将“初始场扰动方向”作为 v,用前向模式 JVP 快速算出各观测站预测值的变化率,从而筛选出对预报结果影响最大的敏感区域,指导观测站布点。

Python 演示

python66 行
import math  class DualNumber:    """二元数: value + tangent * eps, 其中 eps^2 = 0"""     def __init__(self, value, tangent=0.0):        self.value = value        self.tangent = tangent     def __add__(self, other):        o = other if isinstance(other, DualNumber) else DualNumber(other, 0.0)        return DualNumber(self.value + o.value, self.tangent + o.tangent)     def __mul__(self, other):        o = other if isinstance(other, DualNumber) else DualNumber(other, 0.0)        return DualNumber(self.value * o.value,                          self.tangent * o.value + self.value * o.tangent)     def __sub__(self, other):        o = other if isinstance(other, DualNumber) else DualNumber(other, 0.0)        return DualNumber(self.value - o.value, self.tangent - o.tangent)     def sin(self):        return DualNumber(math.sin(self.value), math.cos(self.value) * self.tangent)  def dish_dual(s, g, c):    """雅记小厨: s=盐, g=糖, c=辣椒 -> 两道菜评分"""    score1 = 3.0 * s * g + s.sin() * c    score2 = g * g + 2.0 * s * c    return [score1, score2]  def jvp_at(v_s, v_g, v_c, x):    """在点 x=(s,g,c) 处沿方向 v=(v_s,v_g,v_c) 计算 JVP"""    s = DualNumber(x[0], v_s)    g = DualNumber(x[1], v_g)    c = DualNumber(x[2], v_c)    y1, y2 = dish_dual(s, g, c)    return [y1.value, y2.value], [y1.tangent, y2.tangent]  def full_jacobian(x):    """用三个单位方向分别跑 JVP, 拼出 2x3 雅可比矩阵"""    col = []    for i in range(3):        v = [0.0, 0.0, 0.0]        v[i] = 1.0        _, tangent = jvp_at(v[0], v[1], v[2], x)        col.append(tangent)    return [[col[j][i] for j in range(3)] for i in range(2)]  if __name__ == "__main__":    x = [0.8, 1.2, 0.5]    vec = [0.2, -0.1, 0.4]    value, tangent = jvp_at(vec[0], vec[1], vec[2], x)    print("f(x) =", value)    print("JVP 切线 J·v =", tangent)    J = full_jacobian(x)    print("雅可比矩阵 J =", J)    Jv = [sum(J[i][k] * vec[k] for k in range(3)) for i in range(2)]    print("J x v (矩阵乘) =", Jv)    ok = all(abs(tangent[i] - Jv[i]) < 1e-12 for i in range(2))    print("两种方式结果一致:", ok)
目的:用 30 行二元数实现前向模式自动微分,演示“沿一个方向 v 的 JVP 不需要构造雅可比矩阵也能算出 J·v”这一核心思想,并用显式矩阵乘法验证结果一致。
  1. class DualNumber二元数同时携带数值 value 与切线 tangent,ε²=0 让求导自动忽略高阶项。
  2. __mul__乘法法则 (a+bε)(c+dε)=ac+(ad+bc)ε,切线按莱布尼茨法则传播。
  3. s = DualNumber(x[0], v_s)把方向向量的分量作为输入变量的“初速度”注入。
  4. score1 = 3.0 * s * g + s.sin() * c正常“做菜”,所有运算自动携带切线信息。
  5. return [y1.tangent, y2.tangent]剥离输出切线,即为 J·v 的两个分量。
  6. for i in range(3): v[i] = 1.0用三个标准基方向分别跑一次 JVP,拼出完整雅可比矩阵。
  7. Jv = [sum(J[i][k] * vec[k] for k in range(3)) for i in range(2)]显式的矩阵乘向量,用于对照验证。

常见误区

误区“J·v 嘛,就是先算出那个 m×n 的雅可比矩阵,然后再做一次矩阵乘向量,不就行了?”

正解这正是 JVP 的高明之处——它根本不需要显式构造矩阵。前向模式给每个输入注入方向 v 后,沿着计算图走一遍,输出切线直接就是 J·v,成本与一次函数求值同阶。如果先算完整矩阵,开销是 O(m×n) 的存储与计算,而直接走切线只需要 O(1) 倍的单次求值成本。好比老王想算“所有菜在某个方向上的变化量”,总厨直接边做菜边测,而不是先把 8×5 的账本全部填满再慢慢查。

为何常见:大多数教学资料先定义雅可比矩阵再讲乘法,让人误以为矩阵是必由之路;实际工程中矩阵往往只存在于概念里,运行时代码里只有“数值 + 切线”两条通道。

误区“JVP 和反向传播(VJP)不是一回事吗?反正都是算导数。”

正解两者维度方向完全相反。JVP 把输入方向向量 v(n 维)推成输出切线 J·v(m 维),是“从输入向输出推”;VJP 把输出侧向量 u(m 维)拉回输入侧梯度 Jᵀu(n 维),是“从输出向输入拉”。JVP 适合输出维数远大于输入的场景(一次前推覆盖所有输出),VJP 适合输入维数远大于输出的场景(深度学习训练正是这种)。把它们混为一谈,会在选型时犯方向性错误。

为何常见:两者统称“自动微分”且互为转置,名字又只差一个字母,初学者极易混淆;深度学习框架里反向传播太常见,导致人们以为所有求导都是“反向”的。

误区“前向模式每次只能处理一个输出,如果输出有很多个,效率太低了吧?”

正解前向模式的成本取决于输入方向的数量,而不是输出个数。一次 JVP 无论输出维数 m 多大,都只是一次前向扫描,输出侧所有分量的切线一次性全部得到。真正影响完整雅可比总成本的是:前向模式要跑 n 次(n=输入维数),反向模式要跑 m 次(m=输出维数)。所以当 m≫n 时,前向模式反而是最优选择。老王的问题是“所有菜一起变多少”——一次前推就全出来了,跟菜的数量无关。

为何常见:人们习惯把“梯度下降”想成“对一个标量损失求导”,于是默认求导只能从输出往输入走;其实只要画出 m 和 n 的大小关系,选择立刻反转。

误区“现在训练神经网络全靠反向传播,JVP 只是数学练习题,工程上没人用。”

正解反向传播确实是训练主路径,但 JVP 在工程上遍地开花:计算 Hessian-vector product(二阶优化、梯度诊断)需要 JVP;对输入做敏感度分析、鲁棒性测试需要 JVP;AI 编译器中前向模式算子融合、稀疏求导也依赖 JVP 作为 IR 原语。它和 VJP 不是竞争关系,而是互补关系——VJP 负责“训练求梯度”,JVP 负责“沿任意方向测变化”,两者经常嵌套使用。

为何常见:教程和框架文档大多只讲反向传播,让学习者误以为自动微分就是反向传播;实际上,完整自动微分体系的另一半——前向模式,正是 JVP 的主场。

面试题

Q1. 在雅可比矩阵 J 中,元素 J_{ij} 的数学含义是什么?

单选题 · easy

  • A.第 i 个输出对第 j 个输入的偏导数← 正确答案
  • B.第 j 个输出对第 i 个输入的偏导数
  • C.第 i 个输入对第 j 个输出的偏导数
  • D.第 j 个输入的梯度值

答案:A。雅可比矩阵的约定是“行对应输出、列对应输入”,所以 J_{ij} 是第 i 个输出 y_i 对第 j 个输入 x_j 的偏导。选项 B 把行列颠倒了,C 又把输入输出关系说反了,D 丢掉“偏导数”的精确含义。

Q2. 雅可比向量积 J·v 的输入是什么?

单选题 · easy

  • A.一个方向向量 v← 正确答案
  • B.一个输出向量 y
  • C.一个标量学习率
  • D.一个完整矩阵

答案:A。JVP 的核心输入是方向向量 v,它指明每个输入变量的扰动比例。输出向量 y 是函数的输出而非 JVP 的输入;学习率是优化器的概念;完整矩阵是中间产物,不是 JVP 的输入。

Q3. 关于前向模式求 JVP,以下说法正确的是?

多选题 · mid

  • A.一次前向扫描就能得到 J·v← 正确答案
  • B.必须先写出完整雅可比矩阵
  • C.求完整雅可比需要 n 个线性无关方向← 正确答案
  • D.求完整雅可比需要 m 个方向

答案:AC。前向模式给输入注入方向 v 后沿计算图走一遍,输出切线就是 J·v,所以 A 对、B 错。每个方向只能得到 J 乘以该方向的一列值,要还原完整 n 列信息需要 n 个线性无关方向(C 对);m 个方向是反向模式凑齐 m 行的思路(D 错)。

Q4. 相比“显式构造雅可比矩阵再相乘”,前向模式 JVP 的主要优势是?

单选题 · mid

  • A.必须构造 m×n 矩阵,但乘法更快
  • B.无需构造矩阵,开销与一次函数求值同阶← 正确答案
  • C.开销恒为 O(m),与输入无关
  • D.可以用更大的 batch size

答案:B。前向模式把方向 v 注入输入后只推一遍计算图,不产生中间矩阵,开销是单次函数求值的常数倍(B 对)。A 描述了低效做法,C 混淆了“完整雅可比”与“单次 JVP”的成本,D 是并行策略而非 JVP 本身的优势。

Q5. 什么情况下应优先选择 JVP(前向模式)而非 VJP(反向模式)?

单选题 · mid

  • A.输出维数 m 远大于输入维数 n← 正确答案
  • B.输入维数 n 远大于输出维数 m
  • C.网络层数较深时
  • D.任何情况两者等价

答案:A。完整雅可比时前向需要 n 次、反向需要 m 次。m≫n 时前向次数远少于反向,故选 A。深度学习反向传播常用是因为 n≫m,B 恰恰应选 VJP;网络深度不影响这个结论。

Q6. 关于 JVP 与方向导数的关系,正确的是?

多选题 · mid

  • A.J·v 就是 f 在 x 处沿 v 的方向导数← 正确答案
  • B.J·v 只对单位向量 v 有定义
  • C.f(x+v) ≈ f(x) + J·v 是一阶线性近似← 正确答案
  • D.J·v 就是 Hessian 矩阵乘以 v

答案:AC。方向导数定义为 lim(f(x+εv)−f(x))/ε,正是 J·v(A 对),且对任意非零方向都有定义,不要求单位向量(B 错)。一阶泰勒展开就是 C 的式子。D 说的是 HVP(Hessian-vector product),与 JVP 是两回事。

Q7. AI 编译器把多个算子的 JVP 规则融合成一个内核的主要收益是什么?

单选题 · hard

  • A.减少中间张量的访存与存储开销← 正确答案
  • B.增大模型参数量
  • C.使损失函数变得更光滑
  • D.让梯度自动变为零

答案:A。算子融合把“算数值、算切线”合并进一个循环,中间结果留在寄存器或 L1 缓存,减少读写主存的次数,这正是现代编译器优化 JVP 的核心动机。参数量、光滑性、梯度归零都与融合无关。

Q8. 二元数(dual number)中 ε 满足的核心性质是什么?

单选题 · easy

  • A.ε² = 0← 正确答案
  • B.ε² = 1
  • C.ε = 0
  • D.ε 无定义

答案:A。ε²=0 是前向模式自动微分的魔法开关:它让所有二阶及以上项自然消失,只剩下一阶导数项。若 ε²=1,运算会混入二阶信息;ε=0 则切线信息全部丢失。

Q9. 雅可比矩阵高度稀疏时,编译器最有效的优化手段是?

单选题 · hard

  • A.只对非零偏导对应的路径计算切线← 正确答案
  • B.把所有偏导值置为零
  • C.用更大的 batch size 掩盖计算
  • D.把矩阵转置后再用反向模式

答案:A。稀疏意味着很多 J_{ij}=0,这些项对 J·v 没有贡献,编译器通过依赖分析确定支撑集后,生成代码直接跳过它们,运算量从 mn 降到非零元数(A 对)。B 会算错结果,C 只能提升利用率而非减少无用计算,D 不改变稀疏结构本身。

Q10. 关于 JVP 与 VJP 的对比,正确的是?

多选题 · mid

  • A.JVP 沿计算图正向传播,VJP 反向传播← 正确答案
  • B.JVP 结果在输出空间 R^m,VJP 结果在输入空间 R^n← 正确答案
  • C.两者计算结果总是完全相同
  • D.VJP 可视为 JVP 的转置/对偶运算← 正确答案

答案:ABD。JVP 把 n 维方向推成 m 维输出切线,VJP 把 m 维伴随拉回 n 维梯度,方向与结果空间都相反(A、B 对)。C 忽略了维度都不一定相同。数学上 VJP 就是 Jᵀu,而 (Jv)ᵀ = vᵀJᵀ 揭示了它们的对偶关系(D 对)。