小林刚当上物流园区的调度组长。园区里的包裹从多个仓库出发,经过分拣、贴单、装车、中转等流程,最终送到客户手里。他要解决一个棘手问题:如果客户满意度下降了 1%,每一道工序应该各自调整多少?正向流程很好算,从仓库推到客户就行;但反向倒查,则需要从客户那一个数值出发,沿着流程逆着走,把“责任”一格格传回源头。这正是自动微分中反向模式在做的事:它用一张计算图描述运算流程,用链式法则把最终变化拆给每个变量,最后在一条反向传播的追责单上汇总出所有偏导数。深度学习框架的训练引擎,每天都在重复这件事。

岗位关联

掌握反向模式,你才能真正看懂 PyTorch、TensorFlow 等框架的 autograd 引擎,也才能在模型训练变慢、显存爆炸时快速定位优化方向,这是 AI 从业者进入编译器与系统层的必修课。

目录
  1. 可视化演示
  2. 计算图构建:先画一张快递转运图
  3. 链式法则:一份追责令的拆分规则
  4. 反向传播顺序:逆着箭头一站站往回走
  5. 雅可比转置向量积:不展开整张矩阵的聪明账本
  6. 内存与开销:追责单不能凭空生成
  7. 实践优化:让倒查更快更省
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示反向模式自动微分三维度:前向-后向传播流程(前向按拓扑序执行运算记录值和梯度函数后向从输出出发反向遍历计算图逐节点用链式法则传播梯度)、向量-雅可比乘积VJP(每个节点梯度=上游梯度×局部雅可比转置VJP避免显式构造雅可比矩阵时间复杂度O(1)每节点)、前向vs反向模式对比(前向模式n个输入需n次pass适合少输入多输出反向模式m个输出需1次pass适合多输入少输出深度学习n远大于m反向模式最优),三模式切换理解自动微分引擎核心机制。

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

'+C1+'

计算图构建:先画一张快递转运图

小林接手第一天,先做了一件事:把园区所有线路画成一张图。仓库是起点,客户是终点,中间的每个分拣口都是一个圆圈;箭头表示包裹接下来往哪走。快递业务可以外包,但追责必须依赖这张图。自动微分也一样,第一步不是求导,而是把目标函数拆成一张计算图。

计算图用节点表示数值或运算,用有向边表示数据流向。比如 y = w·x + b 会被拆成乘法节点和加法节点。每一步运算的结果——中间激活值——都会被记录下来,因为反向模式倒查时需要它们作为“本地小票”。

在框架里,每个节点是一个基础算子,允许的运算只有加、乘、求幂等可微原语。前向求值就是从输入节点出发,按拓扑序逐个执行节点,算出输出并保存中间值。自动微分引擎不需要研究者手写梯度,只要图存在,梯度传播路径就存在。

画完图后,小林还特意在每条边上标了箭头方向。因为他知道,等会儿追责时必须逆着箭头走;图一旦画错,整条追责链都会断。

ak=φk(ap1,ap2,…,apm)a_k = \varphi_k\left(a_{p_1}, a_{p_2}, \ldots, a_{p_m}\right)
这行公式说的是:图上每个节点的输出,都由它上游若干个输入经过一个基础算子计算得到。
  1. 节点输出a_k 表示第 k 个节点在前向计算后的输出值,它是后续反向追责的起点依据。
    aka_k
  2. 基础算子\varphi_k 表示该节点对应的可微算子,如加法、乘法等,它是追责令在本站放大倍数的来源。
    φk\varphi_k
  3. 输入集合a_{p_1}, a_{p_2}, \ldots, a_{p_m} 是节点 k 的上游输入,对应快递送到本分拣口的几路包裹。
    ap1,ap2,…,apma_{p_1}, a_{p_2}, \ldots, a_{p_m}

链式法则:一份追责令的拆分规则

客户投诉不是只由最后一个分拣口造成,而是整条线路逐级放大。小林手里的追责令写着一句话:每一站只能把上一站的改动放大或缩小;把沿途的放大倍数乘起来,就是源头应有的责任。这就是链式法则的直觉。

数学上,复合函数 y = f(g(x)) 对 x 的导数不是直接算出来的,而是先看 y 对 g 的敏感度,再看 g 对 x 的敏感度,两者相乘。反向模式把这条规则递归地用在计算图中,每个节点只需处理一个“责任系数”。

具体来说,设输出为 y,经过 n 个中间节点到达输入 x,则梯度是一串局部导数连乘。反向模式并不一次性展开整串式子,而是从后往前依次乘以局部导数,因此每个节点只做一次乘法、一次本地求导。

有了链式法则,小林不需要懂得每台机器的内部机械结构,只要每个分拣口能报告“货物数量变化时我放大了几倍”,追责令就能一路传回源头。

∂y∂x=∂y∂an⋅∂an∂an−1⋯∂a1∂x\frac{\partial y}{\partial x} =\frac{\partial y}{\partial a_n}\cdot\frac{\partial a_n}{\partial a_{n-1}}\cdots\frac{\partial a_1}{\partial x}
这行公式说的是:从输出到输入的总放大倍数,等于沿途每一站局部放大倍数的乘积。
  1. 外层导数\frac{\partial y}{\partial a_n} 表示输出关于最后一个中间状态的敏感度,相当于追责令刚出发时携带的初始系数。
    ∂y∂an\frac{\partial y}{\partial a_n}
  2. 中间连乘\frac{\partial a_n}{\partial a_{n-1}}\cdots\frac{\partial a_1}{\partial x} 表示每站局部导数依次相乘,对应沿途放大倍数的累积。
    ∂an∂an−1⋯∂a1∂x\frac{\partial a_n}{\partial a_{n-1}}\cdots\frac{\partial a_1}{\partial x}
  3. 总梯度\frac{\partial y}{\partial x} 是最终传给输入变量的偏导值,也就是小林最终要给源头仓库下达的调整量。
    ∂y∂x\frac{\partial y}{\partial x}

反向传播顺序:逆着箭头一站站往回走

小林明确了追责令的算法:先到离终点最近的环节,再逆着箭头向源头推进。如果先查源头,中间环节还没传出系数,查到也白查。所以,正确顺序是计算图的拓扑逆序。

反向模式的每一步都在回答:当前节点应该把多少梯度传给它的每个输入?设节点 j 是节点 i 的下游,节点 j 的梯度为 ∂L/∂a_j,那么节点 i 应从这条边收到 ∂L/∂a_j × ∂a_j/∂a_i。若 i 有多个下游,收到的梯度必须相加。

这里的加法不是可选项。一个变量可能被多个下游运算共同使用,例如在 y = u + v 中,u 可能同时参与了别的计算;反向传播必须把来自不同下游的贡献累加到一起,否则梯度就会漏算。

当追责令走到输入节点 x 时,\bar{x} 就是损失对 x 的全部偏导。小林把结果写在追责单最上方,交给对应仓库,整个反向模式就算完成了一次全链路倒查。

aˉi=∑j∈succ(i)aˉj⋅∂aj∂ai\bar{a}_i = \sum_{j \in \mathrm{succ}(i)} \bar{a}_j \cdot \frac{\partial a_j}{\partial a_i}
这行公式说的是:一个节点收到的总梯度,等于所有下游节点传回来的“梯度 × 局部导数”之和。
  1. 伴随值\bar{a}_i 表示节点 i 收到的累积梯度,也称 adjoint,它是追责令在节点 i 上汇总的应担责任。
    aˉi\bar{a}_i
  2. 下游集合\mathrm{succ}(i) 表示节点 i 的所有直接下游节点,对应所有会把包裹继续送往下游的出口。
    succ(i)\mathrm{succ}(i)
  3. 每条边贡献\bar{a}_j \cdot \frac{\partial a_j}{\partial a_i} 是下游 j 传回的一段梯度,沿边相乘后累加到节点 i。
    aˉj⋅∂aj∂ai\bar{a}_j \cdot \frac{\partial a_j}{\partial a_i}

雅可比转置向量积:不展开整张矩阵的聪明账本

当输入和输出都是向量时,小林面对的就不是一张张单独的追责单,而是一沓投诉清单。理论上,每一站都要维护一张“放大倍数表”——雅可比矩阵。如果输入有一百万维,这张表就有百万行,根本记不下。

反向模式的聪明之处在于:它从来不需要把雅可比矩阵完整建出来。它只需要计算“雅可比矩阵的转置”乘上“下游传来的梯度向量”,即 VJP。这个操作可以用一次轻量级局部计算完成,复杂度与算子本身结构有关,而不是先拼出整个大矩阵再相乘。

设 L 是最终标量损失,y ∈ R^m 是某个节点的输出,x ∈ R^n 是该节点的输入,则当 \bar{y} 已知时,反向传播要算的就是 \bar{x} = J^T \bar{y}。每个基础算子都会提供一个 VJP 函数,反向模式沿图调用这些函数即可,完全不需要接触完整雅可比矩阵。

在小林眼里,VJP 就是站点只关心“进来的多张追责单该如何分摊到出去的几个变量上”,绝不在墙上贴满所有输入输出组合的表格。框架把这一笔聪明账内置在每个算子中,反向传播因此又快又省。

xˉ=JTyˉ,J=∂y∂x\bar{\mathbf{x}} = \mathbf{J}^{\mathsf{T}} \bar{\mathbf{y}},\qquad\mathbf{J} = \frac{\partial \mathbf{y}}{\partial \mathbf{x}}
这行公式说的是:反向模式中真正被计算的不是雅可比矩阵本身,而是它转置后与下游梯度向量的乘积。
  1. 雅可比矩阵\mathbf{J} \in \mathbb{R}^{m\times n} 表示局部输出对局部输入各分量之间的偏导关系,相当于站点每一步的放大倍数表。
    J∈Rm×n\mathbf{J}\in\mathbb{R}^{m\times n}
  2. 伴随向量\bar{\mathbf{y}} 是从下游传来的梯度向量,对应多张投诉汇总成的责任清单。
    yˉ\bar{\mathbf{y}}
  3. 转置向量积\mathbf{J}^{\mathsf{T}}\bar{\mathbf{y}} 把责任清单逆向映射回输入空间;反向模式只计算这个乘积,不保存完整 J,也不做矩阵全展开。
    JTyˉ\mathbf{J}^{\mathsf{T}}\bar{\mathbf{y}}

内存与开销:追责单不能凭空生成

小林很快发现,反向模式不是免费的。为了让追责令沿路传回,每一站都必须保留前向时收到的包裹记录;如果中途把中转货场清空,倒查时就没有数据可依据。因此,反向模式的内存开销往往与中间节点数量成正比。

训练神经网络时,前向传播保存的中间激活量占显存大头,反向传播需要这些值来求局部导数。假设有 N 个可微算子,前向和反向各扫描一次计算图,所以时间开销大约是正向计算的 2 到 3 倍;内存则与所有中间张量大小之和成正比。

这与前向模式形成对比。前向模式沿箭头方向传播方向导数,每个方向只需保存当前导数,内存占用通常较小,但计算次数随输入维度增长。反向模式适合“输入极多、输出极少”的场景,而深度学习损失函数恰好只输出一个标量,所以它成为训练引擎的默认选择。

理解了这笔账,小林不再抱怨显存不够。反向模式像一次需要完整台账的审计:账记得越细,倒查越方便,但仓库也越挤。后面所有优化,本质上都是在“记多少账”和“重新跑多少路”之间找平衡。

M_{reverse} = O\!\left(\sum_{i=1}^{N} \mathrm{size}(a_i)\right),\qquadT_{reverse} = O\!\left(\sum_{i=1}^{N}\left(T_i^{f}+T_i^{b}\right)\right)
这组公式说的是:反向模式的内存约等于所有中间激活的总大小,时间约为前向与反向每个算子耗时之和。
  1. 峰值内存M_{reverse} 表示反向模式需要同时保存的中间量总量,\sum_i \mathrm{size}(a_i) 是所有节点输出尺寸之和。
    Mreverse=O(∑isize(ai))M_{reverse} = O\left(\sum_i \mathrm{size}(a_i)\right)
  2. 前向耗时T_i^{f} 是第 i 个算子前向计算耗时,反向模式必须先完整执行一次前向。
    TifT_i^{f}
  3. 反向耗时T_i^{b} 是第 i 个算子 VJP 的计算耗时,反向模式要再沿图执行一次反向。
    TibT_i^{b}

实践优化:让倒查更快更省

为了在显存有限的背景下训练大模型,小林学会了“检查点”:他不再每站都留记录,而是每隔几站存一份快照。倒查时如果发现某段没有台账,就从最近的快照重新跑一次前向,现场补齐这一段记录。这就是梯度检查点技术。

代价很明显:内存下降,但计算增多。实际工程中还常用算子融合来减少中间张量的落盘、用混合精度减少每个张量的占用、用梯度累积在通信瓶颈下保持有效批量大小。深度学习框架默认用反向模式实现 autograd,同时向开发者开放这些优化开关。

AI 编译器把反向模式视作一张可以变换的计算图:先由高层框架生成反向子图,再由编译器做算子融合、内存规划和并行调度。未来趋势是自动选择前向、反向或混合模式,甚至用动态规划寻找最优检查点位置,让倒查策略随硬件自动调整。

若干年后,小林把整套倒查机制做成了一个自动调度系统:开发者只描述业务,剩下怎么记台账、怎么选路线、怎么调存储,都由系统自动决定。这就是反向模式从算法走向工程后最有价值的样子。

θˉ=∑t=1Taˉt⋅∂at∂θ\bar{\theta} = \sum_{t=1}^{T} \bar{a}_t \cdot \frac{\partial a_t}{\partial \theta}
这行公式说的是:一个参数如果在多个节点被使用,它的最终梯度就是所有使用位置上回传贡献的总和。
  1. 参数\theta 表示要被训练更新的权重,它是小林要最终下达调整指令的目标岗位。
    θ\theta
  2. 使用位置a_t 表示参数参与的第 t 个节点,同一个参数可能被多个节点复用。
    ata_t
  3. 梯度汇总\bar{\theta} 是各站传来的贡献之和,也就是反向模式最终写出的参数追责结果。
    θˉ\bar{\theta}

AI 应用场景

深度学习框架的 Autograd 引擎

阶段:模型训练与反向传播

解决的问题:手写每个算子的梯度极易出错且难以维护;反向模式自动微分让研究员只用定义前向网络,框架自动生成梯度。

具体案例:在 PyTorch 中训练 ResNet 时,前向传播逐层保存激活值,调用 loss.backward() 后,引擎沿计算图逆序执行各算子的 VJP,得到每个参数梯度,最后由 optimizer.step() 更新权重。

大模型训练中的显存优化

阶段:训练工程与性能优化

解决的问题:Transformer 等大模型中间激活值极多,朴素反向模式会撑爆显存;因此需要检查点、重算和混合精度来控制内存峰值。

具体案例:训练GPT类模型时,工程师在每个 Transformer 层之间只保存少量激活快照,反向传播时临时重算被释放的中间张量,从而把单卡可承载的模型规模提升数倍。

AI编译器中的计算图变换

阶段:编译器后端与中间表示优化

解决的问题:AI编译器需要为任意算子自动生成反向子图,并把它与原有前向图融合、调度,避免手工编写低效的反向内核。

具体案例:TensorFlow/XLA 在接收到高层计算图后,自动插入反向模式子图,再做算子融合与内存复用,最终生成适合 GPU 执行的底层代码。

Python 演示

python67 行
class Var:    def __init__(self, value, parents=None, grad_fn=None):        self.value = value        self.grad = 0.0        self.parents = parents if parents is not None else []        self.grad_fn = grad_fn     def __add__(self, other):        other = _to_var(other)         def grad_fn(g):            return [(self, g), (other, g)]         return Var(self.value + other.value, [self, other], grad_fn)     def __mul__(self, other):        other = _to_var(other)         def grad_fn(g):            return [(self, other.value * g), (other, self.value * g)]         return Var(self.value * other.value, [self, other], grad_fn)     def relu(self):        def grad_fn(g):            return [(self, g if self.value > 0.0 else 0.0)]         return Var(self.value if self.value > 0.0 else 0.0, [self], grad_fn)     def backward(self, seed=1.0):        topo = []        seen = set()         def dfs(v):            if id(v) in seen:                return            seen.add(id(v))            for p in v.parents:                dfs(p)            topo.append(v)         dfs(self)        self.grad = seed        for v in reversed(topo):            if v.grad_fn is not None:                for parent, contrib in v.grad_fn(v.grad):                    parent.grad += contrib  def _to_var(x):    if isinstance(x, (int, float)):        return Var(float(x))    return x  def main():    x = Var(2.0)    w = Var(3.0)    b = Var(1.0)    y = (w * x + b).relu()    y.backward()    print("y =", y.value)    print("dx =", x.grad, "dw =", w.grad, "db =", b.grad)  if __name__ == "__main__":    main()
目的:用不到 60 行实现一个微型反向模式自动微分引擎,验证 y = relu(w·x + b) 对 x、w、b 的梯度。
  1. self.parents = parents if parents is not None else []记录当前节点的计算图父节点,相当于快递线路上的上游站点。
  2. def grad_fn(g): return [(self, other.value * g), (other, self.value * g)]乘法算子的 VJP,把下游梯度乘以另一个输入值后回传。
  3. dfs(self)` 配合 `topo.append(v)用深度优先搜索生成拓扑序,保证反向时先处理输出节点。
  4. for v in reversed(topo)从输出开始按拓扑逆序回传,正是反向模式的核心顺序。
  5. parent.grad += contrib把来自不同下游的贡献累加到父节点上,防止多路径梯度漏算。

常见误区

误区很多初学者以为反向模式必须保存所有中间结果,所以训练大模型一定显存不够,只能换更大的显卡。

正解朴素实现确实是 O(N) 内存,但工程上已经发展出梯度检查点、算子融合、混合精度等手段。检查点相当于每几站拍一张快照,中间记录丢了就临时从快照重算,用时间换空间,因此反向模式的显存压力是可以通过策略控制的。

为何常见:教材通常先讲最直观的“保存所有中间激活”版本,很少把优化手段放进同一条逻辑线中。

误区许多人觉得反正都是链式法则,反向走和正向走没什么差别,既然方向相反,效果就应该等价。

正解两者的计算次数由不同维度决定。前向模式每增加一个输入维度就要多做一遍前向,反向模式每增加一个输出维度就要多做一遍反向。深度学习损失通常只有一个标量输出,所以反向模式占绝对优势;但如果模型输出几千类而输入很少,前向模式可能反而更划算。

为何常见:对单个标量来看,两类模式算出的结果相同,容易让人忽略输入/输出维度对计算量的决定性影响。

误区看到链式法则展开,就以为自动微分和高中做求导题一样,可以整理出一个完整的代数表达式存下来。

正解反向模式不维护符号表达式,而是在运行时给定一个具体的梯度 seed(例如标量 1.0),沿计算图逐步算出数值梯度。它更像按图执行的追责流程,而非公式化简器。

为何常见:链式法则本身就是符号求导的基础,初学者容易把“用链式法则”和“生成符号表达式”画等号。

误区有人以为反向传播只是更新模型参数,所以只要把参数和最终损失留下来,就能算梯度。

正解每个算子的 VJP 都需要前向时的输入和输出值。比如乘法 y = a·b 的局部导数分别是 b 和 a,没有前向激活值就无法计算。反向模式需要的是“中间结果台账”,而不仅仅是参数快照。

为何常见:框架的 backward() 调用太自动化,让使用者误以为梯度是凭空生成的。

面试题

Q1. 反向模式自动微分最基本的思路是什么?

单选题 · easy

  • A.从输出向输入回传梯度← 正确答案
  • B.从输入向输出推算数值
  • C.用差分近似替代导数
  • D.用符号展开整个公式

答案:A。反向模式从最终输出出发,按计算图逆序回传梯度。前向模式才从输入向输出推;数值差分和符号展开都不是反向模式的本质。

Q2. 在反向模式中,链式法则的主要作用是什么?

单选题 · mid

  • A.把局部导数相乘后传给上游← 正确答案
  • B.用数值方法估计梯度
  • C.直接从输入计算导数
  • D.构建前向计算图

答案:A。链式法则把输出到输入的梯度拆成沿途局部导数的乘积,反向模式按这个规则逐层回传。它并不做数值估计,也不负责构建计算图。

Q3. 计算图上的反向传播执行顺序是?

单选题 · mid

  • A.从输出节点按拓扑逆序← 正确答案
  • B.从输入节点按拓扑顺序
  • C.随机遍历所有节点
  • D.前向和反向同时执行

答案:A。反向传播必须先拿到下游梯度,才能计算上游梯度,所以要从输出开始,按拓扑逆序处理,和快递从终点站逐级倒查同理。

Q4. 一个只有标量损失函数的神经网络,默认应使用哪种自动微分模式?

单选题 · easy

  • A.反向模式自动微分← 正确答案
  • B.前向模式自动微分
  • C.符号微分展开公式
  • D.有限差分近似求导

答案:A。标量损失对应一个输出、海量参数输入,反向模式的成本与输出维度相关,因此效率最高,这也是深度学习框架默认选择反向模式的原因。

Q5. 以下哪些是反向模式计算梯度时需要的信息?

多选题 · mid

  • A.计算图的节点依赖关系← 正确答案
  • B.前向传播的中间输出← 正确答案
  • C.每个算子的局部导数规则← 正确答案
  • D.所有参数的完整二阶导数

答案:ABC。反向模式需要一张可逆推的计算图、前向保存的中间激活值,以及每个算子给出的 VJP 规则;它不需要二阶导数信息,因为计算目标是一阶梯度。

Q6. 若网络的输入维度远大于输出维度,通常更高效的是?

单选题 · hard

  • A.前向模式自动微分
  • B.反向模式自动微分← 正确答案
  • C.数值差分近似
  • D.手工推导所有导数

答案:B。前向模式计算次数随输入维度增长,反向模式随输出维度增长。输入远大于输出时,反向模式需要走通的追责路径更少,因此更高效。

Q7. 梯度检查点是用额外什么资源来换取更低内存?

单选题 · hard

  • A.额外计算时间← 正确答案
  • B.额外显存空间
  • C.额外通信带宽
  • D.额外模型参数

答案:A。检查点只保存部分中间结果,反向传播遇到缺失记录时重新前向计算,本质是“多跑一段路、少存一本账”,因此用计算换内存。

Q8. 关于 VJP 的计算,下列说法正确的是?

多选题 · hard

  • A.不需要显式构造完整雅可比矩阵← 正确答案
  • B.用下游梯度向量与雅可比转置相乘← 正确答案
  • C.结果通常是与输入同维的梯度向量← 正确答案
  • D.结果是与输出同维的输出向量

答案:ABC。反向模式只计算 Jᵀv,v 是下游梯度,结果落在输入空间,因此与输入同维。它避免构造完整矩阵,也不会给出输出维度的向量。

Q9. 反向模式在内存上的主要瓶颈是?

单选题 · mid

  • A.保存前向中间结果← 正确答案
  • B.保存参数副本
  • C.保存训练数据集
  • D.保存反向梯度历史

答案:A。反向模式必须依赖前向时留下的中间激活值来计算局部导数,这些中间张量通常是显存占用的大头,而不是参数副本或数据集本身。

Q10. 深度学习训练中缓解反向模式显存压力的常用方法有哪些?

多选题 · mid

  • A.梯度检查点重算中间值← 正确答案
  • B.算子融合减少中间张量← 正确答案
  • C.混合精度降低存储大小← 正确答案
  • D.关闭计算图自动微分

答案:ABC。检查点、算子融合、混合精度分别从“少存”“少产生”“更小占用”三个方向降低显存压力;关闭自动微分则意味着无法训练。