跳到主要内容

后向自动微分

阐述

自动微分中从输出变量开始,依次计算它对各个中间变量的导数,从而逐渐推出对输入变量的导数。

考虑由 LL 个步骤复合成的函数

f=fLfL1f1f=f^L\circ f^{L-1}\circ \cdots\circ f^1

对应的 Jacobian 矩阵为 J=JLJL1J1J=J_LJ_{L-1}\cdots J_1

  • 前向微分计算的是 Jv=JL(JL1((J1v)))Jv=J_L(J_{L-1}(\cdots(J_1v)\cdots))
  • 后向微分计划的是 vTJ=(((vTJL)JL1))J1v^TJ=(\cdots((v^TJ_L)J_{L-1})\cdots)J_1 

后拉函数

xi=jyjyjxi=Bfx(yˉ)\overline{x_i}=\sum_j \overline{y_j} \frac{\partial y_j}{\partial x_i}=\mathcal{B}_f^x(\bar{y})

也即对于一个输入 xx,我们构造一个类似于闭包的函数 Bfx\mathcal B_f^x,它作用于 yˉ\bar y 来将导数向后传播一步。

如果输出是多个数,则我们需要多次不同的后向传播过程,每次的种子可以选取 v=eiv=e_i,这样就可以计算出整个 Jacobian。

类似于前向自动微分的多个种子,后向自动微分也可以一次性算多个后向传播。

后向自动微分也可以使用稀疏自动微分的技术。

实例

静态图实现

Tensorflow 要求用户使用嵌入在 Python 中的图语言来编写计算过程,这个图语言结构简单,所以很容易后向微分。但是,把已经存在的程序重写成这种图并不容易。

基于痕迹的微分,Wengert 列表

注意到构造 Bfx(yˉ)\mathcal B_f^x(\bar y) 需要知道前向计算中 xx 的值,而不仅仅是 yˉ\bar y。所以,在前向计算的时候需要以某种形式把 xx 记录下来成为一个列表,也称为痕迹。

Tracker.jl, ReverseDiff.jl, PyTorch, Tensorflow Eager, Autograd, Autograd.jl 都属于这一类别。

但是这种微分有很大的冗余,因为每个实数都变成了一个需要堆分配的

mutable struct TrackedReal{T<:Real} <: Real
data::T
tracker::Tracked{T}
end

所以仅仅是一个 + 都需要至少 500 ns;另外痕迹取决于值,它不能被 JIT 编译,所以对每个新的值都需要构建一回新的 trace。

源到源微分

源到源微分的冗余比较小,因为栈分配的值可以保持在栈上,而且编译一次的函数可以用于所有值。但是源到源微分的实现非常困难。

性质

相关内容

参考文献