最近在回看反向传播算法(Backpropagation,BP算法)时,注意到目前各大深度学习框架如Tensorflow,Theano,CNTK等都以计算图(Computational Graph)作为描述反向传播算法的基础。
计算图
计算图是用来描述计算的语言,是一种将计算形式化的方法。在计算图中,计算被表示成有向图,图中的每一个节点表示一个变量(variable),变量可以是标量(scalar)、向量(vector)、矩阵(matrix)、张量(tensor)等。图中的边表示操作(operation),即一个或多个变量的简单函数。如果变量
x
x
x经过一个操作
f
f
f计算得到变量
y
y
y,那么我们画一条从
x
x
x到
y
y
y的有向边。下图是计算图的一个简单例子:
y
=
f
(
g
(
h
(
x
)
)
)
u
=
h
(
x
)
v
=
g
(
u
)
y
=
f
(
v
)
y=f\Big(g\big(h(x)\big)\Big)\\ u=h(x)\quad v=g(u)\quad y=f(v)
y=f(g(h(x)))u=h(x)v=g(u)y=f(v)

同样我们也可以描述多变量函数,下图展示了Logistic回归 y = σ ( w T x + b ) y=\sigma(w^Tx+b) y=σ(wTx+b)的计算图

在计算中,有些中间结果在代数表达式中没有名称,但是在图形中是需要的,比如上图中 u u u、 v v v和 u ( 1 ) u^{(1)} u(1)、 u ( 2 ) u^{(2)} u(2)。当同一个变量多次出现在代数表达式中时,在计算图中需要将其当作不同的变量对待。例如式子 y = x e x 2 y=xe^{x^2} y=xex2的计算图如下

计算表达式的值时,我们从叶子节点开始,顺着有向边逐步归约到根节点即可得到整个表达式的值,可以看作是BP算法的前向传播过程。剩下需要解决的是计算图的求导过程,对应于BP算法的反向传播过程。
首先需要复习下求导的链式法则:
case 1

因为
x
x
x的变化会带来
y
y
y的变化,
y
y
y的变化最终影响
z
z
z的变化,所以
d
z
d
x
=
d
z
d
y
d
y
d
x
\frac{dz}{dx}=\frac{dz}{dy}\frac{dy}{dx}
dxdz=dydzdxdy
即图中一个顶点对另一个顶点的导数等于该顶点到另一个顶点的路径上所有导数的乘积。
case 2

因为
z
=
k
(
x
,
y
)
z=k(x,y)
z=k(x,y)是一个二元函数,受到
x
x
x和
y
y
y的影响,而
s
s
s的变化又同时影响到
x
x
x和
y
y
y,所以
d
z
d
s
=
d
z
d
x
d
x
d
s
+
d
z
d
y
d
y
d
s
\frac{dz}{ds}=\frac{dz}{dx}\frac{dx}{ds}+\frac{dz}{dy}\frac{dy}{ds}
dsdz=dxdzdsdx+dydzdsdy
即当存在从一个顶点到另一个顶点的多条路径时,那么这个顶点对另一个顶点的导数等于所有路径上导数的和。
下面通过两个例子直观展示下计算图求导过程:
Example 1:e=(a+b)*(b+1)

首先画出表达式 e = ( a + b ) ∗ ( b + 1 ) e=(a+b)*(b+1) e=(a+b)∗(b+1)的计算图,然后计算出每条边的导数,最后根据求解问题,找出所有的路径。如图,我们可以轻松写出 ∂ e ∂ a \frac{\partial e}{\partial a} ∂a∂e和 ∂ e ∂ b \frac{\partial e}{\partial b} ∂b∂e。 ∂ e ∂ a \frac{\partial e}{\partial a} ∂a∂e只对应了黄色箭头路径,因此 ∂ e ∂ a = ∂ e ∂ u ∂ u ∂ a = ( b + 1 ) \frac{\partial e}{\partial a}=\frac{\partial e}{\partial u}\frac{\partial u}{\partial a}=(b+1) ∂a∂e=∂u∂e∂a∂u=(b+1)。 ∂ e ∂ b \frac{\partial e}{\partial b} ∂b∂e对应了两条红色箭头路径,因此 ∂ e ∂ b = ∂ e ∂ u ∂ u ∂ b + ∂ e ∂ v ∂ v ∂ b = ( b + 1 ) + ( a + b ) \frac{\partial e}{\partial b}=\frac{\partial e}{\partial u}\frac{\partial u}{\partial b}+\frac{\partial e}{\partial v}\frac{\partial v}{\partial b}=(b+1)+(a+b) ∂b∂e=∂u∂e∂b∂u+∂v∂e∂b∂v=(b+1)+(a+b)。
当出现同一个变量多次出现在代数表达式中时,虽然在画计算图时我们将其当作不同的变量对待,但是计算该变量的导数值时,我们需要将图中关于该变量的所有导数值相加。仍然以 y = x e x 2 y=xe^{x^2} y=xex2为例:

因为
x
x
x在表达式中共出现三次
x
1
x_1
x1、
x
2
x_2
x2和
x
3
x_3
x3,因此我们需要分别计算出
∂
y
∂
x
1
\frac{\partial y}{\partial x_1}
∂x1∂y、
∂
y
∂
x
2
\frac{\partial y}{\partial x_2}
∂x2∂y和
∂
y
∂
x
3
\frac{\partial y}{\partial x_3}
∂x3∂y,然后将其相加。当计算出每一条边的导数后,就可以将出现在不同位置的相同变量同等对待了,因此
∂
y
∂
x
1
=
x
⋅
e
x
2
⋅
x
∂
y
∂
x
2
=
x
⋅
e
x
2
⋅
x
∂
y
∂
x
3
=
e
x
2
∂
y
∂
x
=
∂
y
∂
x
1
+
∂
y
∂
x
2
+
∂
y
∂
x
3
=
e
x
2
+
2
x
⋅
e
x
2
⋅
x
2
\begin{aligned} \frac{\partial y}{\partial x_1}&=x\cdot e^{x^2}\cdot x\\ \frac{\partial y}{\partial x_2}&=x\cdot e^{x^2}\cdot x\\ \frac{\partial y}{\partial x_3}&=e^{x^2}\\ \frac{\partial y}{\partial x}&=\frac{\partial y}{\partial x_1}+\frac{\partial y}{\partial x_2}+\frac{\partial y}{\partial x_3}=e^{x^2}+2x\cdot e^{x^2}\cdot x_2 \end{aligned}
∂x1∂y∂x2∂y∂x3∂y∂x∂y=x⋅ex2⋅x=x⋅ex2⋅x=ex2=∂x1∂y+∂x2∂y+∂x3∂y=ex2+2x⋅ex2⋅x2
反向传播算法的计算图
多层神经网络的表达式可以写成:
y
=
σ
(
w
L
⋯
σ
(
w
2
σ
(
w
1
x
+
b
1
)
+
b
2
)
⋯
+
b
L
)
y=\sigma(w^L\cdots\sigma(w^2\sigma(w^1x+b^1)+b^2)\cdots+b^L)
y=σ(wL⋯σ(w2σ(w1x+b1)+b2)⋯+bL)
对于一个双隐层神经网络,其计算图如图所示:

图中 x x x是输入数据,是一个向量, w w w表示层与层之间的连接权重,是一个矩阵, b b b表示偏置向量, y y y是网络的输出, C C C是损失值,是一个标量。为了计算出参数的梯度,首先需要出每条边的偏导数,因为存在顶点是向量和矩阵的情况,所以涉及到向量对向量的导数和向量对矩阵的导数。
向量对向量的导数
对于标量对向量的导数我们已经会求,推广到向量
y
y
y对向量
x
x
x的导数时,我们分步将向量
y
y
y中每一个元素对向量
x
x
x求偏导,然后将结果组合成一个矩阵,实际也确实是这样。具体的,假设
y
y
y是m维向量,
x
x
x是n维向量,将
y
y
y中每一个元素对
x
x
x的偏导数横排放成行,最终将形成一个
m
∗
n
m*n
m∗n的矩阵,这个矩阵就是Jacobian矩阵。以Sigmoid函数作为激活函数为例,
y
y
y和
z
z
z都是向量,因此
∂
a
∂
z
\frac{\partial a}{\partial z}
∂z∂a是一个方阵,
[
a
1
a
2
⋮
a
i
⋮
]
=
σ
(
[
z
1
z
2
⋮
z
i
⋮
]
)
\begin{aligned} \left[ \begin{array}{ccc} a_1\\ a_2 \\ \vdots\\ a_i\\ \vdots \end{array} \right]=\sigma\left( \left[ \begin{array}{ccc} z_1\\ z_2 \\ \vdots\\ z_i\\ \vdots \end{array} \right]\right) \end{aligned}
⎣⎢⎢⎢⎢⎢⎢⎡a1a2⋮ai⋮⎦⎥⎥⎥⎥⎥⎥⎤=σ⎝⎜⎜⎜⎜⎜⎜⎛⎣⎢⎢⎢⎢⎢⎢⎡z1z2⋮zi⋮⎦⎥⎥⎥⎥⎥⎥⎤⎠⎟⎟⎟⎟⎟⎟⎞
其中
a
i
=
1
1
+
e
−
z
i
a_i=\frac{1}{1+e^{-z_i}}
ai=1+e−zi1,因此当
i
≠
j
i\neq j
i̸=j时,
∂
a
i
z
j
=
0
\frac{\partial a_i}{z_j}=0
zj∂ai=0,当
i
=
j
i=j
i=j时,
∂
a
i
z
j
=
σ
′
(
z
i
)
\frac{\partial a_i}{z_j}=\sigma^{'}(z_i)
zj∂ai=σ′(zi),所以最终的矩阵将是一个对角阵,对角线上的值为
σ
′
(
z
i
)
\sigma^{'}(z_i)
σ′(zi)。
∂
a
∂
z
=
[
σ
′
(
z
1
)
σ
′
(
z
2
)
⋱
]
\frac{\partial a}{\partial z}= \left[ \begin{array}{ccc} \sigma^{'}(z_1) & & \\ & \sigma^{'}(z_2) & \\ & & \ddots \end{array} \right]
∂z∂a=⎣⎡σ′(z1)σ′(z2)⋱⎦⎤
向量对矩阵的导数
m维向量
y
y
y对
m
∗
n
m*n
m∗n维矩阵
x
x
x求导也是向量中每一个元素对矩阵中每一个元素求偏导,因此将得到m个
m
∗
n
m*n
m∗n矩阵。这写起来将非常不方便,通常将
m
∗
n
m*n
m∗n矩阵变平为一个向量,因此结果是一个
m
∗
(
m
×
n
)
m*(m×n)
m∗(m×n)的矩阵。以
z
=
w
a
z=wa
z=wa为例:
[
z
1
z
2
⋮
z
i
⋮
]
=
[
w
11
w
12
⋯
w
21
w
22
⋮
⋱
]
[
a
1
a
2
⋮
a
i
⋮
]
\begin{aligned} \left[ \begin{array}{ccc} z_1 \\ z_2 \\ \vdots \\ z_i\\ \vdots \end{array} \right]=\left[ \begin{array}{ccc} w_{11} & w_{12} & \cdots &\\ w_{21} & w_{22} & \\ \vdots & &\ddots &\\ & & & & \end{array} \right]\left[ \begin{array}{ccc} a_1 \\ a_2 \\ \vdots \\ a_i\\ \vdots \end{array} \right] \end{aligned}
⎣⎢⎢⎢⎢⎢⎢⎡z1z2⋮zi⋮⎦⎥⎥⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎡w11w21⋮w12w22⋯⋱⎦⎥⎥⎥⎤⎣⎢⎢⎢⎢⎢⎢⎡a1a2⋮ai⋮⎦⎥⎥⎥⎥⎥⎥⎤
对于任意一个
z
i
z_i
zi,其计算结果等于
z
i
=
w
i
1
a
1
+
w
i
2
a
2
+
⋯
+
w
i
n
a
n
z_i=w_{i1}a_1+w_{i2}a_2+\cdots+w_{in}a_n
zi=wi1a1+wi2a2+⋯+winan
可以看到,
z
i
z_i
zi只与矩阵
w
w
w中第
i
i
i有关,因此
i
=
/
 
j
i{=}\mathllap{/\,}j
i=/j时,
∂
z
i
∂
w
j
k
=
0
\frac{\partial z_i}{\partial w_{jk}}=0
∂wjk∂zi=0;当
i
=
j
i=j
i=j时,
∂
z
i
∂
w
j
k
=
a
k
\frac{\partial z_i}{\partial w_{jk}}=a_k
∂wjk∂zi=ak。所以得到的矩阵中只有第
i
i
i行有值,并且其值正好等于向量
a
a
a。所以
∂
z
∂
w
\frac{\partial z}{\partial w}
∂w∂z的结果如下
∂
z
∂
w
=
[
a
1
⋯
a
m
a
1
⋯
a
m
a
1
⋯
a
m
⋱
]
\frac{\partial z}{\partial w}=\left[ \begin{array}{ccc} a_1 & \cdots & a_m\\ & & & a_1 & \cdots & a_m\\ & & & & & & a_1 & \cdots & a_m\\ & & & & & & & & & \ddots \end{array} \right]
∂w∂z=⎣⎢⎢⎡a1⋯ama1⋯ama1⋯am⋱⎦⎥⎥⎤
依次求出所有边的导数,如下图

最终
∂
C
∂
w
1
=
∂
C
∂
y
∂
y
∂
z
2
∂
z
2
∂
a
1
∂
a
1
∂
z
1
∂
z
1
∂
w
1
=
[
⋯
∂
C
∂
w
i
j
1
⋯
 
]
\frac{\partial C}{\partial w^1}= \frac{\partial C}{\partial y}\frac{\partial y}{\partial z^2}\frac{\partial z^2}{\partial a^1}\frac{\partial a^1}{\partial z^1}\frac{\partial z^1}{\partial w^1}=[\cdots\quad\frac{\partial C}{\partial w^1_{ij}}\quad\cdots]
∂w1∂C=∂y∂C∂z2∂y∂a1∂z2∂z1∂a1∂w1∂z1=[⋯∂wij1∂C⋯]
#参考文献
台湾大学李宏毅Machine Learning and having it deep and structured (2017,Spring)