快速计算梯度的魔法--反向传播算法
一、实验介绍
1.1 实验内容
第一次实验最后我们说了,我们已经学习了深度学习中的模型model
(神经网络)、衡量模型性能的损失函数
和使损失函数减小的学习算法learn
(梯度下降算法),还了解了训练数据data
的一些概念。但是还没有解决梯度下降算法
中如何求损失函数梯度的问题。
本次实验课,我们就来学习一个能够快速计算梯度的算法--反向传播算法(backpropogate
,这个算法在神经网络中非常重要,同时这个算法也非常巧妙,非常好玩。
algorithm)
我们还会在本次实验课中用代码实现反向传播算法。
1.2 实验知识点
- 链式法则与“计算图”的概念
- 反向传播算法
1.3 实验环境
- python 2.7
- numpy 1.12.1
二、实验步骤
2.1 计算梯度的数值方法
第一次实验我留的一个课后作业里问你是否能够想出一个求解梯度的办法,其实不难想到一种简单的办法就是使用“数值法”计算梯度。
办法很简单,就是对于损失函数中的一个初始取值为a0
的参数a
,先计算当前的损失函数值J0
,再保持其他参数不变,而使a
改变一个很小的量,比如变成a0+0.000001
,再求改变之后的损失函数值J1
。然后(J1-J0)/0.000001
就是J
对于a
的偏导的近似值。我们对每一个参数采用类似的方法求偏导,最后将偏导的值组成一个向量,即为梯度向量。
这个办法看上去很简单,但却无法应用在实际的神经网络当中。一方面的原因是,我们很难知道对参数的改变,有多小才算足够小,即我们很难保证最后求出的梯度是准确的。
另一方面的原因是,这种方法计算量太大,现在的神经网络中经常会有上亿个参数,而这里每求一个分量的偏导都要把所有参数值代入损失函数求两次损失函数值,而且每个分量都要执行这样的计算。相当于每计算一次梯度需要2x1亿x1亿次计算,而梯度下降算法又要求我们多次(可能是上万次)计算梯度。这样巨大的计算量即使是超级计算机也很难承受(世界第一的“神威·太湖之光”超级计算机峰值性能为12.5亿亿次/秒,每秒也只能计算大概6次梯度)。
所以,我们需要更加高效准确的算法来计算梯度,而反向传播算法正好能满足我们的需求。
2.2 “计算图(compute graph)”与链式法则
其实如果你已经理解了链式法则,那么可以说,你几乎已经学会反向传播算法了。让人感到很愉快对不对,好像什么都还没做,我们就已经掌握了一个名字看起来有些吓人的算法。
为了帮助我们真正理解反向传播算法,我们先来看一下什么是“计算图”,我们以第一次实验提到的sigmoid
函数为例:
它的计算图,是这样的:
我们将sigmoid函数视为一个复合函数,并将其中的每一个子函数都视为一个节点,每个节点按照复合函数实际的运算顺序链接起来,最终得到的F
其实就是sigmoid函数本身。
根据求导法则,我们可以求得每一个节点对它直接子节点的导函数:
最重要的地方来了,再根据求导链式法则,我们现在可以轻易写出图中任意一个高层节点对其任意后代节点的导函数:只需要把连接它们的路径上的所有部分导函数都乘起来就可以了。
比如:
dF/dC=(dF/dE)*(dE/dC)=(-1/E^2)*1=-1/E^2
dF/dA=(dF/dE)*(dE/dC)*(dC/dB)*(dB/dA)=(-1/E^2)*(1)*(e^B)*(-1)=e^B/E^2
2.3 反向传播算法
到这里反向传播算法已经呼之欲出了,对于一个具体的参数值,我们只需要把每个节点的值代入求得的导函数公式就可以求得导数(偏导数),进而得到梯度。
这很简单,我们先从计算图的底部开始向上,逐个节点计算函数值并保存下来。这个步骤,叫做前向计算(forward)
。
然后,我们从计算图的顶部开始向下,逐步计算损失函数对每个子节点的导函数,代入前向计算
过程中得到的节点值,得到导数值。这个步骤,叫做反向传播(backward)
或者更明确一点叫做反向梯度传播
。
我们来具体实践一下,对于上图中的sigmoid函数,计算x=0时的导数:
前向计算:
A=0, B=0, C=1, D=1, E=2, F=-1/4
反向传播:
dF/dE=-1/E^2=-1/2^2=-1/4
dF/dC=dF/dE*dE/dC=-1/4
dF/dB=dF/dC*dC/dB=-1/4*e^B=-1/4*1=-1/4
dF/dA=dF/dB*dB/dA=-1/4*(-1)=1/4
以上就是反向传播算法的全部内容。对于有1亿个参数的损失函数,我们只需要2*1亿次计算就可以求出梯度。复杂度大大降低,速度将大大加快。
2.4 将sigmoid视为一个整体
sigmoid函数中没有参数,在实际的神经网络中,我们都是将sigmoid函数视为一个整体来对待,没必要求它的内部节点的导函数。
sigmoid函数的导函数是什么呢?你可以自己求导试试,实际上sigmoid(x)'=sigmoid(x)*(1-sigmoid(x))
。
2.5 反向传播算法--动手实现
激动人心的时刻到了,我们终于要开始用python代码实现深度神经网络的过程,这里我们打算对第一次实验中的神经网络示例图中的“复合函数”编写反向传播算法。不过为了循序渐进,我们考虑第一层(输入层)只有两个节点,第二层只有一个节点的情况,即如下图:
注意我们将sigmoid函数图像放在了b1节点后面,代表我们这里对b1运用sigmoid函数得到了最终的输出h1。
如果你对自己比较有信心,可以不看接下来实现的代码,自己动手试一试。
我们可以先把图中包含的函数表达式写出来,方便我们之后写代码参考:b1=w11*a1+w12*a2+bias1
h1=sigmoid(b1)
h1=sigmoid(w11*a1+w12*a2+bias1)
现在我们创建bp.py
文件,开始编写代码。先来编写从第一层到第二层之间的代码:
import numpy as np
class FullyConnect:
def __init__(self, l_x, l_y): # 两个参数分别为输入层的长度和输出层的长度
self.weights = np.random.randn(l_y, l_x) # 使用随机数初始化参数
self.bias = np.random.randn(1) # 使用随机数初始化参数
def forward(self, x):
self.x = x # 把中间结果保存下来,以备反向传播时使用
self.y = np.dot(self.weights, x) + self.bias # 计算w11*a1+w12*a2+bias1
return self.y # 将这一层计算的结果向前传递
def backward(self, d):
self.dw = d * self.x # 根据链式法则,将反向传递回来的导数值乘以x,得到对参数的梯度
self.db = d
self.dx = d * self.weights
return self.dw, self.db # 返回求得的参数梯度,注意这里如果要继续反向传递梯度,应该返回self.dx
注意在神经网络中,我们将层与层之间的每个点都有连接的层叫做全连接(fully
,所以我们将这里的类命名为
connect)层FullyConnect
。
上面的代码非常清楚简洁,我们的全连接层完成了三个工作:
- 随机初始化网络参数
- 根据x计算这层的输出y,并前向传递给下一层
- 运用求导链式法则,将前面的网络层向后传递的导数值与本层的相关数值相乘,得到最后一层对本层参数的梯度。注意这里如果要继续反向传递梯度(如果后面还有别的层的话),backward()应该返回self.dx
然后是第二层的输入到最后的输出之间的代码,也就是我们的sigmoid层:
class Sigmoid:
def __init__(self): # 无参数,不需初始化
pass
def sigmoid(self, x):
return 1 / (1 + np.exp(x))
def forward(self, x):
self.x = x
self.y = self.sigmoid(x)
return self.y
def backward(self): # 这里sigmoid是最后一层,所以从这里开始反向计算梯度
sig = self.sigmoid(self.x)
self.dx = sig * (1 - sig)
return self.dx # 反向传递梯度
由于我们要多次使用sigmoid函数,所以我们单独的把sigmoid写成了类的一个成员函数。
我们这里同样完成了三个工作。只不过由于Sigmoid层没有参数,所以不需要进行参数初始化。同时由于这里需要反向传播梯度,所以backward()函数必须返回self.dx
把上面的两层拼起来,就完成了我们的总体的网络结构:
def main():
fc = FullyConnect(2, 1)
sigmoid = Sigmoid()
x = np.array([[1], [2]])
print 'weights:', fc.weights, ' bias:', fc.bias, ' input: ', x
# 执行前向计算
y1 = fc.forward(x)
y2 = sigmoid.forward(y1)
print 'forward result: ', y2
# 执行反向传播
d1 = sigmoid.backward()
dx = fc.backward(d1)
print 'backward result: ', dx
if __name__ == '__main__':
main()
请你自行运行上面的代码,并修改输入的x值。观察输出的中间值和最终结果,并手动验证我们计算的梯度是否正确。
如果你发现你不知道如何手动计算验证结果,那说明你还没有理解反向传播算法的原理,请回过头去再仔细看一下之前的讲解。
这里给出完整代码的下载链接,但我还是希望你能尽量自己尝试写出代码,至少自己动手将上面的代码重新敲一遍。这样学习效果会好得多。
完整代码文件下载:
wget http://labfile.oss.aliyuncs.com/courses/814/bp.py
2.6 层次化的网络结构
上面的代码将每个网络层写在不同的类里,并且类里面的接口都是一致的(forward 和 backward),这样做有很多好处,一是最大程度地降低了不同模块之间的耦合程度,如果某一个层里面的代码需要修改,则只需要修改该层的代码就够了,不需要关心其他层是怎么实现的。另一方面可以完全*地组合不同的网络层(我们最后会介绍神经网络里其他种类的网络层)。
实际上,目前很多用于科研和工业生产的深度学习框架很多都是采用这种结构,你可以找一个深度学习框架(比如caffe
)看看它的源码,你会发现里面就是这样一个个写好的网络层。
三、实验总结
本次实验,我们完全地掌握了梯度下降算法中的关键--反向传播算法。至此,神经网络中最基本的东西你已经全部掌握了。你现在完全可以自己尝试构建神经网络并使用反向传播算法优化网络中的参数。
如果你把到此为止讲的东西差不多都弄懂了,那非常恭喜你,你应该为自己感到骄傲。如果你暂时还有些东西没有理解,不要气馁,回过头去仔细看看,到网上查查资料,如果实在无法理解,问问我们实验楼的助教,我相信你最终也能理解。
本次实验,我们学习了:
- 使用计算图理解反向传播算法
- 层次化的神经网络结构
四、课后作业
- [选做]请你自己尝试将我们上面实现的第二层网络的节点改为2个(或多个),注意这里涉及到对矩阵求导,如果你没学过相关知识可能无法下手。