计算图应用非常广,例如,内存计算框架Spark的有向无环图(DAG),Neo4J图数据库、深度学习中的神经网络图,以及TensorBoard中的可视化图,都是计算图的应用场景。本文所讲的也是计算图的一个应用场景:计算神经网络的梯度,包括计算激活函数和典型神经结构(也叫卷积核)的梯度:
1、用计算图分解和解决 激活函数 的导数的计算
2、用计算图分解和解决 神经网络 在反向传播路径上梯度的计算。
回顾上一篇 利用BP神经网络逼近函数(Python)
神经网络的学习过程就是优化目标函数,目标函数的优化需要不断的随着迭代步骤更新神经网络各层的权重和偏置,而更新的一个重要依据就是要计算反向传播路径上的各层的梯度,当神经网络层数很多时,计算和传递就更复杂。这是神经网络的一个核心问题,所以一种什么方式来理解和计算梯度就显得比较重要。
要正确理解和计算反向传播法上梯度,有两种方法:一种是基于数学式,常见于网上洋洋洒洒的公式推导;另一种是基于计算图(computational graph)的。 关于BP反向传播梯度推导,在网上和书上,笔者看了数次,但是当时觉得懂了,但是过了不久印象几乎消失殆尽,大概是东方人擅长具象思维,西方人擅长逻辑思维的缘故吧。不知读者是否有这样的感觉。本文讲述后者,计算图作为一种解决问题的方式,有着形象直观,易于理解和掌握的优势。关于什么是梯度参见我的文章 神经网络的数学基础:张量和梯度,
目录
首先讲述如何拆解复合函数求导,然后讲述如何在神经网络中传导,然后,再结合实际的代码加深理解,相信大家看完会有一定会有种“原来如此!”的感觉。
首先定义计算图的概念,计算图是有向图,神经网络是其特殊形式, 其中节点对应于操作或变量。变量可以将其值提供给操作,操作可以将其输出提供给其他操作。这样,图中的每个节点都定义了变量的函数。进入节点并从节点出来的值称为张量(多维数组的另一别称),它包含标量,向量和矩阵以及更高等级的张量。
计算图将计算过程用图形表示出来。这里说的图形是数据结构图,通过多个节点和边表示(连接节点的直线称为“边”)
“从左向右进行计算”是一种正方向上的传播,简称为正向传播(forward propagation)。正向传播是从计算图出发点到结束点的传播。既然有正向传播这个名称,当然也可以考虑反向(从图上看的话,就是从右向左)的传播。实际上,这种传播称为反向传播(backward propagation)。传播的是什么?传播的是线上的张量!
复合函数是由多个函数构成的函数。比如,函数 z = ( x + y ) 2 z = (x + y)^2 z=(x+y)2 是由下面两个函数式子组成的:
z = t 2 z = t^2 z=t2
t = x + y t = x + y t=x+y
复合函数的求导,遵循链式法则,规则是这样:如果某个函数是复合函数,则其导数可以用构成复合函数的各个函数的导数的乘积表示,例如上面公式对x的导数就可以由以下公式推导得出:
∂ z ∂ x = ∂ z ∂ t ∂ t ∂ x = 2 t . 1 = 2 ( x + y ) \frac {\partial z}{\partial x}=\frac {\partial z}{\partial t}\frac {\partial t}{\partial x}=2t.1=2(x+y) ∂x∂z=∂t∂z∂x∂t=2t.1=2(x+y)
可见,把复合函数分解为几个小的子函数,通过求子函数的导数,然后把它们乘起来,这样大大降低了求导数的难度,而且理解起来也很容易,复合函数求导,在神经网络中用的多场合是激活函数和损失函数,这里我们以 激活函数 s i g m o i d sigmoid sigmoid 为例说明, s i g m o i d sigmoid sigmoid 函数由以下公式表示:
y = 1 1 + e x p ( − x ) y=\frac {1}{1+exp(-x)} y=1+exp(−x)1
按照函数内进行的操作单元分解后,计算图表示如下:
我们可以把 x x x理解为神经网络里的信号,信号通过这个计算图最终输出 y y y,这是信号的正向传播,这是我们常规思维容易理解的地方。下面我们看看反向传播的流程。
第一步
先看一下反向的信号源头,一般来说在神经网络我们优化的都是损失函数 L o s s Loss Loss,所以一般从损失函数对输出求导,作为信号起始点,标记为 ∂ L ∂ y \frac{\partial L}{\partial y} ∂y∂L,这是一个信号量。首先看这个信号通过 “/” 变成什么?其实,“/”节点表示 y = 1 x y = \frac {1}{x} y=x1,它的导数:
∂ y ∂ x = − 1 x 2 = − y 2 \frac {\partial y}{\partial x} = -\frac {1}{x^2} = - y^2 ∂x∂y=−x21=−y2
所以经过"/"后,信号为: − ∂ L ∂ y y 2 -\frac{\partial L}{\partial y}y^2 −∂y∂Ly2
第二步
“+”节点将上游的值原封不动地传给下游。计算图如下所示:
第三步
“exp”节点表示y = exp(x),它的导数由下式表示:
∂ y ∂ x = e x p ( x ) \frac {\partial y}{\partial x}=exp(x) ∂x∂y=exp(x)
计算图中,上游的值乘以正向传播时的输出(这个例子中是exp(−x))后,再传给下游。
第四步
“×”节点将正向传播时的值翻转后做乘法运算。因此,这里要乘以−1。
最终反向传播的输出: ∂ L ∂ y y 2 e x p ( − x ) \frac{\partial L}{\partial y}y^2exp(-x) ∂y∂Ly2exp(−x), 这个值只根据正向传播时的输入x和输出y就可以算出来. 计算图可以收缩为神经网络中的 s i g m o i d sigmoid sigmoid节点图画法,如下:
最终输出结果还可以进一步整理为如下:
∂ L ∂ y y 2 e x p ( − x ) = ∂ L ∂ y 1 ( 1 + e x p ( − x ) ) 2 e x p ( − x ) = ∂ L ∂ y y ( 1 − y ) \frac{\partial L}{\partial y}y^2exp(-x)=\frac{\partial L}{\partial y}\frac {1}{(1+exp^{(-x)})^2}exp(-x)=\frac{\partial L}{\partial y}y(1-y) ∂y∂Ly2exp(−x)=∂y∂L(1+exp(−x))21exp(−x)=∂y∂Ly(1−y)
结论是Sigmoid 层的反向传播,只根据正向传播的输出就能计算出来。
深度学习中,复杂的神经网络一般都可以由以下这个典型神经网络结构通过层层叠加而组成。所以,了解了这个基本结构的梯度计算,再复杂的网络结构也就是同理可得了。这个典型网络单元(也叫一个卷积核)由公式 Y = n p . d o t ( X , W ) + B Y= np.dot(X,W) + B Y=np.dot(X,W)+B 描述,Y 经过激活函数转换后,传递给下一层。这就是神经网络正向传播的流程. 结构可分解为以下计算图:一个矩阵相乘再加上偏置:
图中括号内容表示该张量的形状,当然里面的数字是一个举例的数字
这里参与计算X,W,B都是矩阵或者叫做多维数组。以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同。公式如下:
∂ L ∂ X = ∂ L ∂ Y . W T \frac {\partial L}{\partial X}=\frac {\partial L}{\partial Y}.W^T ∂X∂L=∂Y∂L.WT
∂ L ∂ W = X T . ∂ L ∂ Y \frac {\partial L}{\partial W}=X^T.\frac {\partial L}{\partial Y} ∂W∂L=XT.∂Y∂L
首先 ∂ L ∂ Y \frac {\partial L}{\partial Y} ∂Y∂L 信号通过“+”后,信号不变到了 d o t dot dot节点, d o t dot dot是矩阵相乘,导数需要对应的矩阵做转置运算。以上述图为例:
W T W^T WT 的T表示转置。转置操作会把W的元素(i, j) 换成元素 (j, i)。用数学式表示的话,可以写成下面这样,
W = ∣ w 11 w 12 w 13 w 21 w 22 w 23 ∣ W=\left| \begin{matrix} w_{11} & w_{12} & w_{13}\\ w_{21} & w_{22} & w_{23}\\ \end{matrix} \right| W=∣∣∣∣w11w21w12w22w13w23∣∣∣∣
W T = ∣ w 11 w 21 w 12 w 22 w 13 w 23 ∣ W^T=\left| \begin{matrix} w_{11} & w_{21} \\ w_{12} & w_{22} \\ w_{13} & w_{23} \\ \end{matrix} \right| WT=∣∣∣∣∣∣w11w12w13w21w22w23∣∣∣∣∣∣
如果W的形状是(2, 3), W T W^T WT的形状就是(3, 2)。
根据上述,我们可以写出计算图的反向传播,如下
这个图所示的X是一个一维数组的情形,如果X是批量数据,形状是(samples features),计算图如下:
用Python表示如下:
def backward(self, dout):
dx = np.dot(dout, self.W.T)
dW = np.dot(self.x.T, dout)
db = np.sum(dout, axis=0)
return dx, db, dW
再通过一个层级循环,就可以算出各层权重和偏置需要调整的值
for layer in layers:
dout = layer.backward(dout)
然后再利用,类似如下代码进行权重和偏置的更新
grad = network.gradient(x_batch, t_batch)
# 更新
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
完整例子可以参看这一篇 利用BP神经网络逼近函数(Python)
本文讲述了计算过程可视化的计算图,并使用计算图,计算了复合函数求导和神经网络单元求梯度。通过使用层进行模块化,神经网络中可以自由地组装层,轻松构建出自己喜欢的网络:
• 通过使用计算图,可以直观地把握计算过程。
• 计算图的节点是由局部计算构成的,局部计算构成全局计算。
• 计算图的正向传播进行一般的计算,通过计算图的反向传播,可以计算各个节点的导数。
• 通过将神经网络的组成元素实现为层,可以高效地计算梯度(反向传播法)
1、Hacker’s guide to Neural Networks
2、CS231n: Convolutional Neural Networks for Visual Recognition
3、Deep Learning From Scratch I: Computational Graphs
4、从头学习Deep Learning