想象你推着一辆购物车去超市采购。每拿起一件商品,收银小票上就会自动多一行:这件商品多少钱,它对总账单有多“敏感”——如果它涨价1元,总账单会跟着涨多少。这个“敏感度”,在数学里就叫导数。前向模式(Forward Mode)自动微分,就是让这辆购物车从超市入口出发,每经过一个货架、每做一次计算,都同步更新小票上的“敏感度”信息,一路推到收银台结账。等结账时,你手里的小票已经写满了所有商品对总价的敏感度,不需要回头再算。 为什么这件事值得搞懂?因为 PyTorch、JAX 这些深度学习框架,底层全靠自动微分在算梯度。理解前向模式,你就看懂了它们在什么场景下选哪条“购物路线”,也就能在写 AI 编译器、做模型优化时,准确判断该用哪种求导方式。这篇文章就用这辆“带小票的购物车”,带你逛完前向模式的全过程。

岗位关联

对 AI 从业者而言,掌握前向模式意味着你能看懂框架底层的雅可比计算逻辑,在编译器优化、二阶优化器、仿真敏感度分析等场景中做出正确的技术选型,而不是只会调 loss.backward()。

目录
  1. 可视化演示
  2. 自动微分基础
  3. 对偶数原理
  4. 前向模式推导
  5. 反向模式对比
  6. 复杂度分析
  7. 工程实现
  8. 应用场景
  9. 实践误区
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示自动微分中的前向模式原理与实的核心机制,通过输入/处理/存储/输出四种模式展示关键概念与交互流程。

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

自动微分中的前向模式原理与实:对偶数原 对偶数原理与运算规则 前向累积计算机制 核心机制可视化 自动微分中的前向模式原理与实整体架构与关键组件

自动微分基础

在超市里,想知道“西瓜涨价 1 元,购物车总账单会涨多少”,老一辈有几种土办法。第一种叫数值微分——真的去货架上把西瓜价格标签撕下来改 1 元,推车重新结一次账,看总价差多少。改一次只能测一个商品,货架上有一百种商品就得结一百次账,而且因为收银台只显示整数分,测出来的敏感度还带误差。第二种叫符号微分——把整个购物清单当成一个超长数学表达式,用纸笔法则一步步展开求导。如果清单里写着“满 100 减 20”、“第二件半价”这种嵌套规则,展开后的式子会膨胀得比超市还大,根本算不完。

自动微分(Automatic Differentiation,AD)的思路完全不同:它先把购物流程拆成一连串细小的“货架操作”——拿起商品、加入购物车、应用折扣、计算小计……这就像把整个购物过程画成一张计算图,每个节点是一次基本运算(加法、乘法、三角函数)。然后,AD 让每个数值都随身携带一张“小票”记录敏感度,每经过一个货架操作,小票就按照该操作的规则自动更新,全程不需要猜测、也不需要展开巨大公式。

这张计算图是整个自动微分的地基。无论是前向模式还是后面要讲的反向模式,都是在这张图上“走路”的规则:从哪个节点出发、往哪个方向走、沿路怎么更新小票。理解自动微分,本质上就是理解“把复杂函数拆成基本运算图,再让敏感度沿图流动”这件事。

回到超市的比喻:自动微分就是给每个商品挂上小票,每过一个货架按规则更新敏感度,不需要反复结账,也不需要把清单展开成天书。这条思路干净利落,为后面两种模式铺好了路。

∂z∂x=∂z∂y⋅∂y∂x\displaystyle \frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \cdot \frac{\partial y}{\partial x}
这个公式是整座超市的“传送带规则”:敏感度像货物一样,从 x 传到 y,再传到 z,两段敏感度相乘就得到总敏感度。
  1. 左侧整体敏感度表示输出 z 对输入 x 最终有多敏感,也就是我们想求的导数。
    ∂z∂x\displaystyle \frac{\partial z}{\partial x}
  2. 第一段传送带z 对中间量 y 的敏感度,记录“y 动一点,z 动多少”。
    ∂z∂y\displaystyle \frac{\partial z}{\partial y}
  3. 第二段传送带y 对源头 x 的敏感度,记录“x 动一点,y 动多少”。
    ∂y∂x\displaystyle \frac{\partial y}{\partial x}
  4. 两段相乘像两段传送带首尾相接,把 "x → y → z" 两段变化率乘起来,得到整条链的敏感度。
    ∂z∂y⋅∂y∂x\displaystyle \frac{\partial z}{\partial y} \cdot \frac{\partial y}{\partial x}

对偶数原理

现在正式介绍小票长什么样。在自动微分的前向模式里,每个数不再是孤零零的一个“价格”,而是一个携带影子信息的对偶数。比如 3 + 4ε,可以理解成“这件商品标价 3 元,而且它对某个基准量(比如预算)的敏感度是 4”。这里 ε 是一个特殊的符号,它不是数字,而是一个“影子标记”,数学上规定它满足一个神奇的规则:ε² = 0。

这个“ε² = 0“是整辆购物车能正常工作的核心机关。它意味着:影子信息只参与第一层传播,一旦它和另一个影子相乘,就会像”假一赔十的优惠券叠优惠券“一样自动失效归零,绝不会引发影子套影子的混乱。正是这个规则,保证了小票上的敏感度永远是简洁的”价格 + 一个影子”,不会无限膨胀。

对偶数参与运算时,规则极其简单。两个对偶数相加,价格和影子分别相加;两个对偶数相乘,价格部分按普通乘法算,影子部分则按“交叉相乘再相加”来处理。别看规则简单,它背后有泰勒展开撑腰——任何光滑函数在微小扰动下的变化,首阶项恰好就是导数乘以扰动,高阶项因为含 ε² 全部归零。

所以,每一件“商品”(数值)都自带价格和影子两栏。购物车每经过一次运算,小票就按“价格归价格、影子按法则更新”的方式刷新。这就是前向模式能在一次遍历中就得到精确导数的底层秘诀。

(a+bε)+(c+dε)=(a+c)+(b+d)ε\displaystyle (a + b\varepsilon) + (c + d\varepsilon) = (a + c) + (b + d)\varepsilon
(a+bε)(c+dε)=ac+(ad+bc)ε,ε2=0\displaystyle (a + b\varepsilon)(c + d\varepsilon) = ac + (ad + bc)\varepsilon,\qquad \varepsilon^2 = 0
乘法法则可以记成一句话:“价格乘价格,影子交叉加”。ε² = 0 负责把乘积中的影子与影子相乘项直接扔掉。
  1. 常数部分直接相乘两个对偶数的“价格”部分 a 和 c 按普通乘法得到 ac,这是结果的主体。
    ac\displaystyle ac
  2. 交叉项出场影子部分来自“左价格 × 右影子”与“左影子 × 右价格”,即 a·d·ε 和 b·c·ε。
    (ad+bc)ε\displaystyle (ad + bc)\varepsilon
  3. 影子与影子相乘归零bε × dε = bd·ε² = 0,所以不会出现 ε² 项,小票保持简洁。
    bdε2=0\displaystyle bd\varepsilon^{2} = 0
  4. 合并影子系数把两个交叉项合到一起,得到最终影子系数 ad + bc。
    (a+bε)(c+dε)=ac+(ad+bc)ε\displaystyle (a + b\varepsilon)(c + d\varepsilon) = ac + (ad + bc)\varepsilon

前向模式推导

现在推着购物车上路。假设我们要算函数 f(x, y) = x·y + y 在点 x=3, y=4 处对 x 的偏导数。前向模式的做法是:在入口处给 x 的小票上写一个初始影子 ε=1(种子),意思是“我们现在只观察 x 动 1 单位时,所有量跟着动多少”;给 y 写影子 ε=0,表示“这场观察里 y 保持不变”。

然后购物车沿计算图前进。第一步来到乘法货架 t1 = x·y:价格 3×4=12,影子按乘法法则 x 的影子×y 的价格 + x 的价格×y 的影子,即 1×4 + 3×0 = 4。第二步来到加法货架 t2 = t1 + y:价格 12+4=16,影子 4+0=4。最后到达收银台,输出 f = 16,小票上的影子也是 4——这就是 ∂f/∂x 的精确值。

整个过程购物车只从入口推到出口一次,所有中间值的影子都随着计算同步更新,没有任何近似。为什么能用对偶数一路算到底?因为把 x = 3 + ε 代入任意光滑函数做泰勒展开后,系数恰好就是导数:

前向模式的特点由此凸显:它给每个中间值都维护一个影子导数,像购物车推过每个货架时,收银员顺手在小票上盖一个章。等结账时,小票上已经写满了这一路所有“如果 x 动 1 单位,每个中间量会动多少”的信息。

f(x+ε)=f(x)+f′(x)ε\displaystyle f(x + \varepsilon) = f(x) + f'(x)\varepsilon
这句话是说:把“带影子的数”扔进函数里算一遍,算出来的影子部分天然就是导数。泰勒展开里所有含 ε² 及更高次的项都被规则清零,只剩一阶项。
  1. 泰勒展开对 f 在 x 附近做展开,把 h = ε 当作微小扰动代入。
    f(x+ε)=f(x)+f′(x)ε+12f′′(x)ε2+⋯\displaystyle f(x + \varepsilon) = f(x) + f'(x)\varepsilon + \frac{1}{2}f''(x)\varepsilon^{2} + \cdots
  2. 高阶项清零因为 ε² = 0,展开式中第二项之后的所有项全部消失。
    12f′′(x)ε2+⋯=0\displaystyle \frac{1}{2}f''(x)\varepsilon^{2} + \cdots = 0
  3. 只剩首阶项等式右边只剩下 f(x) 加上 f'(x)ε,影子系数就是所求导数。
    f(x+ε)=f(x)+f′(x)ε\displaystyle f(x + \varepsilon) = f(x) + f'(x)\varepsilon

反向模式对比

有前向就有反向,它是另一条完全相反的购物路线。前向模式是“从入口出发,推着车往前走,沿路收集敏感度”;反向模式则是“从收银台的总账单出发,倒着走回每个货架,问每一件商品:你为总账单贡献了多少”。深度学习里天天听到的“反向传播”,就是反向模式在损失函数这个“总账单”上的特例。

这两个模式拿到的信息不太一样。在一张有 n 个输入、m 个输出的计算图里,前向模式每走一次,得到的是“一个输入对所有输出的敏感度”——对应雅可比矩阵的一行;反向模式每走一次,得到的是“所有输入对一个输出的敏感度”——对应雅可比矩阵的一列。打个比方:前向是“揪住西瓜问它对整辆购物车每件商品价格的影响”,反向是“揪住购物车总额,问每件商品各贡献了多少”。

当中间节点有多条路径汇聚时,反向模式需要把各路贡献累加,这就是求和形式的链式法则。比如损失 L 依赖中间量 v₁, v₂, …,而每个 vᵢ 又依赖 x,那么 L 对 x 的敏感度是每条路径敏感度的总和:

对比两种购物路线,前向模式适合“输入少、输出多”——就像你只问 1 个问题(西瓜涨价多少),却想知道它对 1000 件商品的影响;反向模式适合“输入多、输出少”——大多数神经网络训练正是这种情况:几百万个参数(输入),最终只汇聚成一个损失值(输出)。两条路线没有谁绝对更好,只有谁更适合当前的购物清单。

∂L∂x=∑k∂L∂vk⋅∂vk∂x\displaystyle \frac{\partial L}{\partial x} = \sum_{k} \frac{\partial L}{\partial v_k} \cdot \frac{\partial v_k}{\partial x}
这条公式像超市查监控:想弄清“西瓜涨价对总账单的影响”,就把所有经过西瓜的收银小票路径都翻出来,每条路径的贡献加起来。
  1. 求和范围遍历所有直接依赖 x 的中间节点 v_k,因为 x 可能通过多条路影响最终输出。
    ∑k\displaystyle \sum_{k}
  2. 每条路径的贡献对每个中间节点 v_k,把“输出对它的敏感度”和“它对 x 的敏感度”相乘。
    ∂L∂vk⋅∂vk∂x\displaystyle \frac{\partial L}{\partial v_k} \cdot \frac{\partial v_k}{\partial x}
  3. 汇总成总敏感度把每条路径的贡献加在一起,得到 L 对 x 的总导数。
    ∑k∂L∂vk⋅∂vk∂x\displaystyle \sum_{k} \frac{\partial L}{\partial v_k} \cdot \frac{\partial v_k}{\partial x}

复杂度分析

既然有两条购物路线,怎么选?关键看两个数字:超市入口有几个商品(输入维度 n),收银台出口要打印几份小票(输出维度 m)。前向模式从入口出发推一次车,途中对每个中间量都更新影子,代价大约和输入数量 n 成正比——因为每多关心一个输入,就要多设一种种子;反向模式从出口倒推一次,代价大约和输出数量 m 成正比——因为每多关心一个输出,就要多跑一轮反向。

这个规律直接给了我们一个“选路判据”:当 n 远小于 m 时,前向模式更划算;当 m 远小于 n 时,反向模式更划算。举两个极端例子:神经网络训练是典型的上百万参数对一个损失值(n 巨大、m=1),所以必须用反向;而一个物理仿真器输入只有 10 个物理参数,输出却是 10 万个时刻的轨迹状态(n=10、m=100000),这时前向模式一次遍历就能把整张雅可比矩阵扫完,反向反而要跑 10 万轮。

除了维度,还有存储的权衡。反向模式通常需要把计算图的中间结果缓存下来,像超市保存每一笔交易记录,内存开销大;前向模式则像边走边盖章,中间值用完就扔,内存友好但计算次数受输入维度牵制。编译器在做自动微分变换时,正是根据这些权衡自动生成最优的“购物路线”。

n<m⇒前向模式更优,n>m⇒反向模式更优\displaystyle n < m \Rightarrow \text{前向模式更优},\qquad n > m \Rightarrow \text{反向模式更优}
购物路线选择法则:入口商品少就走前向,出口小票少就走反向。两边都不少时,就要考虑存储换时间。
  1. n 的意义n 是输入维度,对应超市里的商品种类数,前向模式每多一个输入就得额外推一轮车。
    n\displaystyle n
  2. m 的意义m 是输出维度,对应收银台要打印的小票份数,反向模式每多一个输出就得额外倒推一轮。
    m\displaystyle m
  3. 比较判据比较 n 与 m 的大小关系,较小的那一边决定哪种模式总代价更低。
    n<m\displaystyle n < m

工程实现

这么聪明的小票机制,框架是怎么实现的?答案是“算子重载”——让每个数值都变成一个对偶数对象,把 +、× 这些运算符重新定义成会同步更新影子的版本。这就像超市给所有商品都贴上了电子标签,收银台的扫描枪一碰到标签,价格和敏感度自动录入,完全不需要人工干预。我们在 Python 里实现一个 Dual 类覆盖 __add__、__mul__,就等于给收银台换上了智能扫描枪。

在实际工业级框架里,JAX 提供了 jvp(Jacobian-vector product)接口来实现前向模式。给定函数 f、初始输入 primals 和种子方向 tangents,它返回“函数值 + 沿该方向的导数”。PyTorch 也在 torch.autograd.forward_ad 模块里提供了前向模式支持,方便处理反向模式吃力的场景。这些接口的数学本质,都是计算雅可比矩阵与种子向量的乘积:

更高级的玩法是嵌套微分:既然对偶数的影子本身也是数,那影子也可以再带一个影子,从而用“两层前向”求二阶导数。JAX 正是靠这种嵌套能力实现 jvp 与 grad 的灵活组合,让编译器在自动微分变换时像搭积木一样自由选择路线。

Jf(x)⋅v=(∇f1(x)⋅v,  ∇f2(x)⋅v,  …,  ∇fm(x)⋅v)\displaystyle J_f(x) \cdot v = \bigl(\nabla f_1(x)\cdot v,\; \nabla f_2(x)\cdot v,\; \dots,\; \nabla f_m(x)\cdot v\bigr)
这个式子描述了智能扫描枪的工作过程:J 是“全景敏感度报表”(雅可比矩阵),v 是本次观察的种子方向,两者做乘法后,得到每个输出沿该方向的影子导数。
  1. 雅可比矩阵J_f(x) 是一个 m×n 矩阵,第 i 行第 j 列是第 i 个输出对第 j 个输入的偏导。
    Jf(x)=[∂f1∂x1⋯∂f1∂xn⋮⋮∂fm∂x1⋯∂fm∂xn]\displaystyle J_f(x)=\begin{bmatrix}\frac{\partial f_1}{\partial x_1}&\cdots&\frac{\partial f_1}{\partial x_n}\\\vdots&&\vdots\\\frac{\partial f_m}{\partial x_1}&\cdots&\frac{\partial f_m}{\partial x_n}\end{bmatrix}
  2. 种子向量v 是用户指定的方向,比如要观察第一个输入,就取 v = (1, 0, …, 0)。
    v=(v1,v2,…,vn)\displaystyle v = (v_1, v_2, \ldots, v_n)
  3. 逐行点积结果向量的第 i 个分量,是雅可比第 i 行与 v 的点积,即 f_i 沿 v 方向的方向导数。
    ∇fi(x)⋅v=∑j=1n∂fi∂xjvj\displaystyle \nabla f_i(x)\cdot v = \sum_{j=1}^n \frac{\partial f_i}{\partial x_j} v_j

应用场景

前向模式在真实 AI 工程里绝不是摆设,它专治“输入少输出多”的病人。第一个典型场景是超参数敏感性分析:比如一个语音合成模型只有 4 个全局温度参数(输入 n=4),但要在 1000 个频点输出上观察音色变化(输出 m=1000)。用前向模式,给每个温度参数设一次种子,4 轮就能拿到完整的 1000×4 雅可比行,画出一张“哪个参数对哪个频点影响最大”的热力图。换成数值微分,得跑 4000 次推理,差距立竿见影。

第二个场景藏在机器人仿真的 ODE 求解器里。强化学习训练时,仿真器输入是机械臂的关节质量、摩擦系数等少量物理参数,输出却是几十个时刻的完整轨迹状态。用前向模式顺着仿真时间步一路更新影子,就能一次性得到“参数扰动对整条轨迹的影响”,这对系统辨识和策略梯度估计都极其宝贵。

第三个场景是二阶优化中的海森对角估计。训练大规模神经网络时,完整海森矩阵太大存不下,但我们可以用前向模式计算“梯度的 JVP”来近似海森对角,给 AdaHessian 这类二阶优化器提供预条件。这就像购物车不仅记录了总账单的敏感度,还能告诉收银员“小票上哪一行最容易受手抖影响”,帮助优化器在参数空间里走得更稳。在这些场景里,前向模式不是反向模式的替补,而是那条更短、更聪明的购物路线。

实践误区

前向模式概念不复杂,但实操时大家常在几个坑里打转。第一个坑是维度判断反了:听到“反向模式适合训练”就以为前向没用,或者反过来在输入百万级、输出 1 的模型里强行用前向模式,结果慢到怀疑人生。判据其实就一句话——看输入和输出哪个少,少的决定选哪条路。

第二个坑是误以为前向模式有精度问题。这是把它和数值微分搞混了。数值微分需要取一个很小的 h 做近似,必然有截断误差;前向模式用的是对偶数代数,每一步都是精确的符号运算,误差只来自浮点数的机器精度,和反向模式完全同级别。你不需要“担心它不准”,只需要担心“选没选对”。

第三个坑是不知道框架原生支持。很多人以为前向模式要自己写算子重载,其实 PyTorch 的 torch.autograd.forward_ad、JAX 的 jvp 都是现成的轮子。自己手写对偶类练手很好,但上了生产环境要优先用框架接口,它们的算子覆盖更全、性能调优更好。顺便一提,手写对偶数时要小心把“梯度方向”搞反——小票上的影子是 输出如何随输入变化,不是 输入如何随输出变化,方向反了,整个优化就奔着反方向去了。

AI 应用场景

语音合成超参数敏感度分析

阶段:模型调试与可解释性

解决的问题:传统做法为了分析 4 个温度参数对 1000 个频点输出的影响,需要跑 4000 次前向推理再算差分,既慢又有数值误差。前向模式给每个参数设一个种子,4 次前向遍历就能拿到完整的雅可比行。

具体案例:某语音团队想调整“语速、音调、音量、呼吸感”四个全局风格参数,观察对 1000 维声码器输出的影响。用 JAX 的 jvp 实现前向模式,一次函数调用同时返回音频值和敏感度矩阵,据此画出的热力图直接指导了风格插值算法的设计。

机器人仿真轨迹对物理参数的敏感性

阶段:仿真训练与系统辨识

解决的问题:仿真器的物理参数(质量、摩擦、刚度)数量少,但输出是整个时间轴上几十步的状态向量,属于典型的“小输入大输出”。反向模式要为每个输出时刻单独倒推,代价太大。

具体案例:某四足机器人团队用 PyTorch 的 forward_ad 求“关节摩擦系数对 30 步足底力轨迹”的敏感度,用于校准仿真器和真机之间的动力学差异。前向模式顺着仿真时间步推进,影子导数一路同步更新,一次仿真就能完成全部参数分析。

二阶优化器的海森对角预条件估计

阶段:训练加速与优化器设计

解决的问题:大规模模型想用二阶信息加速收敛,但完整海森矩阵的平方级存储不可行。研究者用前向模式的 JVP 来估计海森对角,作为优化器的预条件。

具体案例:在 JAX 中实现 AdaHessian 风格的优化器,利用 jvp(grad(f), primals, tangents) 得到对角海森近似值,把 BERT 微调的收敛步数减少了约 30%。前向模式在这里不是“求梯度”,而是给梯度再算一次方向导数,完美发挥嵌套微分的优势。

Python 演示

python77 行
class Dual:    """对偶数:value 是主体值,deriv 是影子导数"""    def __init__(self, value, deriv=0.0):        self.value = value        self.deriv = deriv     def __add__(self, other):        if isinstance(other, Dual):            return Dual(self.value + other.value,                        self.deriv + other.deriv)        return Dual(self.value + other, self.deriv)     def __radd__(self, other):        return Dual(other + self.value, self.deriv)     def __mul__(self, other):        if isinstance(other, Dual):            return Dual(self.value * other.value,                        self.deriv * other.value + self.value * other.deriv)        return Dual(self.value * other, self.deriv * other)     def __rmul__(self, other):        return Dual(other * self.value, other * self.deriv)     def __sub__(self, other):        if isinstance(other, Dual):            return Dual(self.value - other.value,                        self.deriv - other.deriv)        return Dual(self.value - other, self.deriv)     def __truediv__(self, other):        if isinstance(other, Dual):            return Dual(self.value / other.value,                        (self.deriv * other.value - self.value * other.deriv)                        / (other.value * other.value))        return Dual(self.value / other, self.deriv / other)     def __repr__(self):        return "Dual(value=%s, deriv=%s)" % (self.value, self.deriv)  def apply(f, x):    """把普通函数 map 到 Dual 对象上"""    return f(x)  def f(x, y):    return x * y + y  def g(x):    return [x * x, 2.0 * x + 1.0]  def demo():    # 对 x 求偏导:给 x 的种子为 1,y 的种子为 0    x = Dual(3.0, 1.0)    y = Dual(4.0, 0.0)    out = f(x, y)    print("f(3,4) =", out.value)    print("df/dx =", out.deriv)     # 对 y 求偏导:交换种子    x2 = Dual(3.0, 0.0)    y2 = Dual(4.0, 1.0)    out2 = f(x2, y2)    print("df/dy =", out2.deriv)     # 雅可比行:多个输出对同一输入的导数    xin = Dual(2.0, 1.0)    res = g(xin)    print("g(2) =", [r.value for r in res])    print("dg/dx =", [r.deriv for r in res])  if __name__ == "__main__":    demo()
目的:用 60 行 Python 实现前向模式自动微分的核心——对偶数运算,并演示单输入偏导和雅可比行的计算。
  1. class Dual:定义对偶数对象,同时保存主体值和影子导数,相当于给普通数贴上一张小票。
  2. def __init__(self, value, deriv=0.0):构造器,deriv 默认 0 表示“不敏感”,需要在入口显式设置种子。
  3. def __add__(self, other):重载加法运算符,让 x + y 自动按照“值相加、影子相加”的法则更新。
  4. def __mul__(self, other):重载乘法运算符,实现 ad + bc 交叉法则,这是前向模式的引擎。
  5. def __radd__(self, other):处理 3 + dual 这种数字在左边的加法,确保普通数与对偶数混合运算不出错。
  6. deriv * other.value + self.value * other.deriv乘法影子法则的代码形态:左影子×右值 + 左值×右影子。
  7. return Dual(self.value / other.value, ...)除法法则同样支持,满足基本算子覆盖需求。
  8. def demo():演示入口,分别计算 df/dx、df/dy 和函数 g 的雅可比行。
  9. Dual(3.0, 1.0)给 x 设置种子 1,表示“本次观察只关心 x 的变化”。
  10. print("df/dx =", out.deriv)前向模式的核心便利:算完函数值,导数已经在结果对象的 deriv 字段里。

常见误区

误区我听说“反向传播适合神经网络”,所以前向模式就是给简单函数用的玩具,实际项目用不上。

正解前向模式的适用条件不是“简单”,而是“输入少输出多”。比如只有 5 个物理参数、却要观察 10 万个轨迹点的仿真场景,前向模式比反向模式快几个数量级。选路标准永远是 n 和 m 的比较,而不是函数复杂程度。

为何常见:深度学习课程几乎只讲反向传播,大家把“训练神经网络”当成了自动微分唯一的应用场景,忽略了其他大量“小输入大输出”的工程问题。

误区前向模式本质上也是用差分近似吧?比如那个 ε 是不是相当于很小的 h,会带来截断误差?

正解对偶数里的 ε 不是一个小数值,而是一个抽象符号,满足 ε²=0 的代数规则。所有运算都是精确的符号推导,和数值微分的“取 h=0.0001 估算斜率”有本质区别。前向模式的结果只有浮点数的机器精度误差,与反向模式完全同级。

为何常见:人们容易把“用 ε 表示扰动”和“数值微分取小步长 h”混为一谈,因为表面形式很像,但一个来自代数系统、一个来自近似计算。

误区要用前向模式只能自己手写对偶类,太麻烦,所以还是算了。

正解PyTorch 从 1.7 起提供了 torch.autograd.forward_ad,JAX 更是把 jvp 作为一等公民接口。生产环境中应该优先使用这些经过算子覆盖和性能优化的内置实现。手写 Dual 类适用于教学和理解,但全量算子重载的工程量大且易出错。

为何常见:很多教材用“手动实现”来讲原理,读者误以为框架没这功能,或者以为框架的自动微分只有 backward() 这一条路。

误区前向模式算出来的影子导数应该是“参数如何被损失影响”,所以优化时直接用它更新参数就行。

正解导数永远是“输出如何随输入变化”,即 ∂输出/∂输入。优化参数时要沿着负梯度方向走,影子导数给出的是“输入增大时输出增大多少”,要更新参数减小损失,得用 参数 -= 学习率 * 影子导数。方向搞反,训练会直接发散。

为何常见:初学者在写优化循环时容易把“梯度值”和“更新方向”两件事合并,忽略了梯度下降需要负号这个基本事实。

误区反向模式只需要一次前向一次反向,时间上总是最优,所以内存问题无所谓。

正解反向模式要保存前向过程的全部中间激活值,内存开销随网络深度线性增长。前向模式恰恰相反,因为影子跟着值走,中间结果用完即弃,内存占用很小。在处理超长序列或深度很大的模型时,前向模式的内存优势可能是选它的决定性理由。

为何常见:大家只比较了时间复杂度 n vs m,忽略了反向模式“用存储换时间”的代价,对训练时的显存瓶颈认识不足。

面试题

Q1. 对偶数乘法 (3 + 2ε) × (4 + 0ε) 的结果中,ε 的系数是多少?

单选题 · mid

  • A.a·d = 3×0 = 0
  • B.a·d + b·c = 3×0 + 2×4 = 8← 正确答案
  • C.a·c = 3×4 = 12
  • D.b·d·ε² = 2×0 = 0

答案:B。对偶数的影子乘法法则是“交叉加”:ad + bc,即 3×0 + 2×4 = 8。A 只算了一半交叉项,C 是主体值的乘积,D 是 ε² 项按规则归零的部分。

Q2. 前向模式自动微分一次求值,能获得雅可比矩阵的哪一部分?

单选题 · mid

  • A.完整雅可比矩阵
  • B.一个 m×n 的稠密矩阵
  • C.雅可比矩阵的某一列
  • D.雅可比矩阵的某一行← 正确答案

答案:D。前向模式给某个输入设种子,等价于计算 J·v,即雅可比矩阵与标准基向量的乘积,结果是对应的一行。反向模式给某个输出设种子,得到的才是雅可比的一列。A、B 需要多轮求值才能拼出。

Q3. 一个深度学习模型的输入是 10000 维特征,输出是 1 维损失值,训练时应该优先选择哪种求导模式?

单选题 · mid

  • A.前向模式
  • B.反向模式← 正确答案
  • C.符号微分
  • D.数值微分

答案:B。n=10000、m=1,属于“输入多输出少”,反向模式每轮只需一次倒推即可得到损失对所有输入的梯度,而前向模式要为 10000 个输入各设种子跑 10000 轮。复杂度判据 n > m 时选反向。

Q4. 在对偶数体系中,ε² 的值等于多少?

单选题 · easy

  • A.ε
  • B.1
  • C.0← 正确答案
  • D.-1

答案:C。ε² = 0 是对偶数定义的核心规则,它保证影子信息在一阶传播后自动归零,使展开式不会无限膨胀。这个规则是整个前向模式自动微分不产生表达式爆炸的基础。

Q5. 下列关于自动微分相对数值微分和符号微分的优势,哪些是正确的?

多选题 · mid

  • A.结果精确,没有手撕差分步长的截断误差← 正确答案
  • B.避免了符号微分中表达式急剧膨胀的问题← 正确答案
  • C.计算过程中完全不占用任何额外内存
  • D.不需要存储计算图的中间结果

答案:AB。自动微分基于对偶数代数给出精确导数,且只沿着计算图做局部传播,不会产生符号微分的爆炸式表达式。C 和 D 都不绝对成立——反向模式必须缓存中间激活值,内存开销不可忽略。

Q6. 在 JAX 的 jvp(f, primals, tangents) 接口中,tangents 参数对应前向模式里的什么概念?

单选题 · hard

  • A.函数输出值
  • B.种子方向向量← 正确答案
  • C.模型的权重张量
  • D.优化器的学习率

答案:B。tangents 正是前向模式里的种子方向,即对偶数中的 ε 系数向量,指定了“本次观察沿哪个输入方向求导”。前向模式的核心就是给每个输入设置种子并同步更新影子导数,与 jvp 的语义完全对应。

Q7. 哪些场景下使用前向模式会比反向模式更合适?

多选题 · hard

  • A.输入只有几个参数但输出维度很大的仿真分析← 正确答案
  • B.输入是百万维特征、输出是一维损失的分类训练
  • C.用 JVP 近似海森矩阵对角线的二阶优化器← 正确答案
  • D.计算标量损失函数对所有网络参数的梯度

答案:AC。A 是典型的“输入少输出多”,前向模式一次遍历拿一行雅可比,效率极高。C 中 JVP 本身就是前向模式的核心原语。B 和 D 都是 n 巨大、m=1 的场景,反向模式才占优。

Q8. 对偶数求导的理论依据是以下哪种数学展开?

单选题 · easy

  • A.洛朗级数展开
  • B.泰勒级数展开← 正确答案
  • C.傅里叶级数展开
  • D.柯西主值展开

答案:B。f(x + ε) = f(x) + f'(x)ε 正是将 f 在 x 处做泰勒展开后,利用 ε²=0 消去高阶项得到的。洛朗级数涉及负幂项、傅里叶级数涉及谐波正交基,都与对偶数无关。

Q9. 用前向模式计算 h(x, y) = sin(x) × y 在 x = π/2, y = 2 处对 x 的偏导数,结果是多少?

单选题 · easy

  • A.0← 正确答案
  • B.2
  • C.-2
  • D.1

答案:A。对 x 求偏导时把 y 视作常数:∂h/∂x = cos(x) × y。代入 x = π/2 得 cos(π/2) = 0,再乘 y = 2 仍为 0。对偶数方法会给出同样的精确结果,不会有数值误差。

Q10. 关于前向模式与嵌套自动微分的关系,下列哪些说法是正确的?

多选题 · extreme

  • A.用两层前向模式可以求出函数的二阶导数← 正确答案
  • B.对偶数的影子导数本身可以再做一次对偶数← 正确答案
  • C.两层嵌套前向模式的复杂度必然呈指数爆炸
  • D.JAX 支持前向与反向模式混合嵌套来求高阶导← 正确答案

答案:ABD。对偶数的影子部分也是普通数,自然可以再挂一层影子,因此两层前向模式能得到二阶导,JAX 正是用 jvp 与 grad 的组合实现高阶导。嵌套的复杂度随层数多项式增长而非指数爆炸,所以 C 错误。