假设我们定义了一个方程: f(x,y)=x2y+y+2 f ( x , y ) = x 2 y + y + 2 ,我们需要对 x x 和 求偏导,此时通常由以下几种做法:
- 手动求导
- 符号微分
- 数值微分
- 前向自动微分
- 反向自动微分
1. 手动求导
拿起纸笔应用链式法则逐层求导即可。缺点是易于出错。
2. 符号微分
下图是符号微分方法在函数 g(x,y) g ( x , y ) 上的运算示例。
链式法则的运算过程与右边这棵树的生成过程对应。计算时首先从叶节点开始,逐层向上。
当函数比较复杂时,由该方法生成的树规模十分庞大,且难以进行简化。另外,符号微分无法无法对一些迭代计算的表达式进行计算:
def my_func(a, b): z=0
for i in range(100):
z = a * np.cos(z + i) + z * np.sin(b - i)
return z
3. 数值微分
最简单的方法就是对偏导数进行数值近似。函数
h(x)
h
(
x
)
在
x0
x
0
点处的导数为:
在计算微分的时候,取一个很小的 ϵ ϵ 代入计算即可。但由此得到的结果不够准确,当函数比较复杂时尤为如此。
4. 前向自动微分
前向自动微分结合了符号微分与数值微分的思想,主要依赖 dual numbers ϵ ϵ 实现, ϵ ϵ 是一个无穷小的数, ϵ≠0 ϵ ≠ 0 但是 ϵ2=0 ϵ 2 = 0 。
注意,因为有:
因此,我们可以通过计算 h(a+ϵ) h ( a + ϵ ) 来求解 h′(a) h ′ ( a ) 。
下图为对 f(x,y) f ( x , y ) 在点 x=3,y=4 x = 3 , y = 4 处求偏导的计算过程。要求 f(3+ϵ,4) f ( 3 + ϵ , 4 ) ,所得 dual number 的第一部分对应于 f(3,4) f ( 3 , 4 ) ,第二部分对应于 ∂ f∂ x(3,4) ∂ f ∂ x ( 3 , 4 ) 。
需要说明的是,如果要求解 ∂ f∂ y(3,4) ∂ f ∂ y ( 3 , 4 ) ,我们必须按照流程从头到尾再计算一遍。
总的来说,前向自动微分要比数值微分精确得多。但真如上一段所说,每对一个变量求解微分,我们就需要从叶子节点开始计算到根节点,当变量数很大时,前向自动微分方法就不够实用了。
5. 反向自动微分
反向自动微分正是TensorFlow采用的自动微分方法。该方法共分为两个阶段完成:
- 依照 forward direction (输入->输出,叶节点->根节点) 计算图中每个节点的取值
- 依照 reverse direction 计算所有的导数取值
下图为 reverse direction 的计算过程示意。
- n7 n 7 : 输出节点, f=n7 f = n 7 => ∂ f∂ n7=1 ∂ f ∂ n 7 = 1
- n5 n 5 : ∂ f∂ n5=∂ f∂ n7×∂ n7∂ n5 ∂ f ∂ n 5 = ∂ f ∂ n 7 × ∂ n 7 ∂ n 5 ,我们知道 ∂ f∂ n7=1 ∂ f ∂ n 7 = 1 ,而 n7=n5+n6 n 7 = n 5 + n 6 ,因此 ∂ n7∂ n5=1 ∂ n 7 ∂ n 5 = 1 => ∂ f∂ n5=1×1=1 ∂ f ∂ n 5 = 1 × 1 = 1
- n4 n 4 : ∂ f∂ n4=∂ f∂ n5×∂ n5∂ n4 ∂ f ∂ n 4 = ∂ f ∂ n 5 × ∂ n 5 ∂ n 4 ,因为 n5=n4×n2 n 5 = n 4 × n 2 ,所以 ∂ n5∂ n4=n2 ∂ n 5 ∂ n 4 = n 2 => ∂ f∂ n4=1×n2=4 ∂ f ∂ n 4 = 1 × n 2 = 4
总的来说,反向自动微分法是一个非常清大且精确的技术,当输入输出维度较大时,优势尤为明显。具体来说
- 该方法只需要 forward pass + reverse pass 两次计算就可以求得所有输出对所有输入的导数
- 能够处理 functions defined by arbitrary code (如,上面的代码示例)
- 可计算不完全可导的函数在可导处的导数值