自动求导(基础知识)
注:本系列搭建的深度学习框架名称叫numpyflow,缩写nf,用以熟悉目前主流的深度学习框架的基础和原理。本系列的目标是使用nf可以训练resnet。
开源地址:RanFeng/NumpyFlow
简介
这里的自动求导,我们可以理解为自动求解梯度而不是导函数。道理都是一样的,就是基础的链式法则,参考知乎的这个问题tensorflow的函数自动求导是如何实现的?
自动求导常用的方法有三种:
- Numerical differentiation 数值微分
- Symbolic differentiation 符号微分
- Automatic differentiation 自动微分
广泛应用于深度学习框架的是后面两种微分。
Numerical differentiation
对于求导,我们第一时间能想到的就是根据导数定义来计算:
f
′
(
x
)
=
lim
Δ
x
→
0
f
(
x
+
Δ
x
)
−
f
(
x
)
Δ
x
f^{\prime}(x)=\lim _{\Delta x \rightarrow 0} \frac{f(x+\Delta x)-f(x)}{\Delta x}
f′(x)=Δx→0limΔxf(x+Δx)−f(x)
数值微分最大的特点就是很直观,好计算,利用了导数定义。
不过这里有一个很大的问题:
Δ
x
\Delta x
Δx 的选取怎么选择?选大了,误差会很大;选小了,不小心就陷进了浮点数的精度极限里,造成舍入误差。所以数值微分仅仅只能在手算的时候稍微计算一下,实际应用并不广泛。若一定要用的话,经常使用如下的公式,从两侧逼近:
f
′
(
x
)
=
lim
Δ
x
→
0
f
(
x
+
Δ
x
)
−
f
(
x
−
Δ
x
)
2
Δ
x
f^{\prime}(x)=\lim _{\Delta x \rightarrow 0} \frac{f(x+\Delta x)-f(x-\Delta x)}{2 \Delta x}
f′(x)=Δx→0lim2Δxf(x+Δx)−f(x−Δx)
Symbolic differentiation
还有一种求导方法,就是——写出导函数,然后求解梯度。这个很容易理解,比如:
f
(
x
)
=
x
2
f(x)=x^2
f(x)=x2的导函数是
f
′
(
x
)
=
2
x
f'(x)=2x
f′(x)=2x,那么其在
x
=
3
x=3
x=3处的梯度就是
f
′
(
3
)
=
6
f'(3)=6
f′(3)=6。
这个过程就是符号微分做的事了,很明显,符号微分的优势就是计算出来的梯度比较精确。但是缺点就很多了:
- 难以计算导函数,每写一个函数,符号微分框架都会封装一个人到框架中用于求解导函数😨。比起数值微分,符号微分难度要大很多。
- 时间复杂度高,每写出一个函数,符号微分都要分析函数的表达式,合并表达式等等操作,然后得出其导函数。
实现符号微分的技术点:
- 统一封装算子和运算
- 预定义初等函数的导数运算
- 线性运算
- 三角函数运算
- 指数对数
- …
- 预定义导数的求导规律:
- d ( u / v ) = ( u d v − v d u ) / v 2 d(u/v)=(udv−vdu)/v2 d(u/v)=(udv−vdu)/v2
- d ( u ∗ v ) = u d v + v d u d(u∗v)=udv+vdu d(u∗v)=udv+vdu
- …
- 预定义导数的链式法则:
- d y d x = d y d u ∗ d u d x \frac{dy}{dx}=\frac{dy}{du}*\frac{du}{dx} dxdy=dudy∗dxdu
而TensorFlow中,就使用了这种求导方式,它将每个计算比如a+b看成一个op,我们可以将op当做基本的算子,整个框架中的所有计算都是由有限多个基本算子组合而成的。TensorFlow对每个算子都计算出对应的导函数,这样就能完成整个框架的导函数。
Automatic differentiation
接下来就是自动微分方式了,自动微分不关注导函数什么样,它只负责求解对应的梯度,这个正是我们所需要的。搞那些花里胡哨的导函数干啥?但是它跟符号求导的共同点就是,都要预定义好一堆的基本算子op,对每个op计算出当前的梯度,然后根据这些基本op的梯度推算出整个计算图的梯度。
自动求导分成两种模式,一种是 Forward Mode,另外一种是 Reverse Mode。
举个例子:
f
(
x
,
y
)
=
x
2
+
y
f(x,y)=x^2+y
f(x,y)=x2+y,可以分为两个op,一个是指数op,一个是加法op,我们拆分一下,
f
1
=
x
2
f
2
=
f
1
+
y
f1=x^2\\ f2=f1+y
f1=x2f2=f1+y
这样,带入
x
=
2
,
y
=
2
x=2,y=2
x=2,y=2,我们求得
f
2
=
6
,
f
1
=
4
f2=6,f1=4
f2=6,f1=4,求解梯度的时候
∂
f
2
∂
f
1
(
2
,
2
)
=
1
∂
f
2
∂
y
(
2
,
2
)
=
1
∂
f
2
∂
x
(
2
,
2
)
=
∂
f
2
∂
f
1
(
2
,
2
)
∗
∂
f
1
∂
x
(
2
,
2
)
=
1
∗
4
=
4
\frac{∂f2}{∂f1}(2,2)=1 \\ \\ \frac{∂f2}{∂y}(2,2)=1 \\ \frac{∂f2}{∂x}(2,2)=\frac{∂f2}{∂f1}(2,2) *\frac{∂f1}{∂x}(2,2) =1*4=4
∂f1∂f2(2,2)=1∂y∂f2(2,2)=1∂x∂f2(2,2)=∂f1∂f2(2,2)∗∂x∂f1(2,2)=1∗4=4
所以我们用这种方式求得了
x
=
2
,
y
=
2
x=2,y=2
x=2,y=2时候的梯度,而整个过程,我们都没有求解其导函数,
f
2
f2
f2也没有直接对
x
x
x求导过。这样一看,天然的递归!而我们上面所使用的方法,也就是reverse mode。该方法首先正向遍历整个图,计算出每个节点的值;然后逆向(从上到下)遍历整个图,计算出节点的偏导值。 这是一种自顶向下的求梯度方式。
那foward mode是怎么样的呢?
∂
f
1
∂
x
(
2
,
2
)
=
4
∂
f
2
∂
f
1
(
2
,
2
)
=
1
∂
f
2
∂
x
(
2
,
2
)
=
∂
f
2
∂
f
1
(
2
,
2
)
∗
∂
f
1
∂
x
(
2
,
2
)
=
1
∗
4
=
4
∂
f
2
∂
y
(
2
,
2
)
=
1
\frac{∂f1}{∂x}(2,2)=4 \\ \frac{∂f2}{∂f1}(2,2)=1 \\ \frac{∂f2}{∂x}(2,2)=\frac{∂f2}{∂f1}(2,2) *\frac{∂f1}{∂x}(2,2) =1*4=4 \\ \frac{∂f2}{∂y}(2,2)=1 \\
∂x∂f1(2,2)=4∂f1∂f2(2,2)=1∂x∂f2(2,2)=∂f1∂f2(2,2)∗∂x∂f1(2,2)=1∗4=4∂y∂f2(2,2)=1
好像两个差距不大,只是顺序问题,下面通过一个更加复杂的例子来说明两者的区别,顺便熟悉自动求导的细节。
一个例子🌰
接下来,我们从一个🌰来了解求解梯度的过程,然后下一章了解自动求导的必须组件。
我们随机设定一个函数
f
=
x
y
3
z
(
y
+
z
)
=
x
y
4
z
+
z
y
3
z
2
f = xy^3z(y+z)=xy^4z+zy^3z^2
f=xy3z(y+z)=xy4z+zy3z2
拆解开来,它的计算图如下
我们带入
x
=
5
,
y
=
2
,
z
=
3
x=5,y=2,z=3
x=5,y=2,z=3,求解如下图
forward mode
假设我们要求出
∂
f
6
∂
y
(
5
,
2
,
3
)
\frac{∂f6}{∂y}(5,2,3)
∂y∂f6(5,2,3)的值,那么forward mode计算过程如下:
对于forward mode,求解梯度是从底层开始的,这样经过一次自底向上的计算,我们求得了原函数对于 y y y的梯度,如果要求解原函数对于 x x x和 z z z的梯度,那还得按照上面的方法计算一次。所以,forward mode一次只能计算一个变量的梯度,多少个变量就得forward多少次。
如果函数输入输出为:
R
→
R
m
R \to R^m
R→Rm
那么利用forward mode只需计算一次上图的过程即可,非常高效。对于输入输出映射为如下的:
R
n
→
R
m
R^n \to R^m
Rn→Rm
这样一个有
n
n
n个输入的函数,求解函数梯度需要
n
n
n遍如上计算过程。
而在神经网络中,输入输出一般来说是
n
>
>
m
n>>m
n>>m,所以forward mode实在是太不适合了
那么利用forward mode进行自动微分就太低效了,因此便有下面要介绍的reverse mode。
reverse mode
可以看出,一次的reverse mode就可以求出整个图中变量的梯度,这在深度学习中是非常适合的。
由于这种方法比较简单而且适合深度学习,所以在numpyflow中,我们采用此种(reverse mode)求解梯度的方法。
下一节,我们开始实现自动微分reverse mode代码。
引用
1.自动微分(Automatic Differentiation)简介
2.自动微分方法(auto diff)
3.自动求导–Deep Learning框架必备技术二三事