使用python实现深度神经网络 3python
第一次实验最后咱们说了,咱们已经学习了深度学习中的模型model
(神经网络)、衡量模型性能的损失函数
和使损失函数减少的学习算法learn
(梯度降低算法),还了解了训练数据data
的一些概念。可是尚未解决梯度降低算法
中如何求损失函数梯度的问题。网络
本次实验课,咱们就来学习一个可以快速计算梯度的算法--反向传播算法(backpropogate algorithm)
,这个算法在神经网络中很是重要,同时这个算法也很是巧妙,很是好玩。框架
咱们还会在本次实验课中用代码实现反向传播算法。dom
第一次实验我留的一个课后做业里问你是否可以想出一个求解梯度的办法,其实不难想到一种简单的办法就是使用“数值法”计算梯度。
办法很简单,就是对于损失函数中的一个初始取值为a0
的参数a
,先计算当前的损失函数值J0
,再保持其余参数不变,而使a
改变一个很小的量,好比变成a0+0.000001
,再求改变以后的损失函数值J1
。而后(J1-J0)/0.000001
就是J
对于a
的偏导的近似值。咱们对每个参数采用相似的方法求偏导,最后将偏导的值组成一个向量,即为梯度向量。
这个办法看上去很简单,但却没法应用在实际的神经网络当中。一方面的缘由是,咱们很难知道对参数的改变,有多小才算足够小,即咱们很难保证最后求出的梯度是准确的。
另外一方面的缘由是,这种方法计算量太大,如今的神经网络中常常会有上亿个参数,而这里每求一个份量的偏导都要把全部参数值代入损失函数求两次损失函数值,并且每一个份量都要执行这样的计算。至关于每计算一次梯度须要2x1亿x1亿次计算,而梯度降低算法又要求咱们屡次(多是上万次)计算梯度。这样巨大的计算量即便是超级计算机也很难承受(世界第一的“神威·太湖之光”超级计算机峰值性能为12.5亿亿次/秒,每秒也只能计算大概6次梯度)。函数
因此,咱们须要更加高效准确的算法来计算梯度,而反向传播算法正好能知足咱们的需求。性能
其实若是你已经理解了链式法则,那么能够说,你几乎已经学会反向传播算法了。让人感到很愉快对不对,好像什么都还没作,咱们就已经掌握了一个名字看起来有些吓人的算法。
为了帮助咱们真正理解反向传播算法,咱们先来看一下什么是“计算图”,咱们以第一次实验提到的sigmoid
函数为例:学习
它的计算图,是这样的:优化
咱们将sigmoid函数视为一个复合函数,并将其中的每个子函数都视为一个节点,每一个节点按照复合函数实际的运算顺序连接起来,最终获得的F
其实就是sigmoid函数自己。spa
根据求导法则,咱们能够求得每个节点对它直接子节点的导函数:
最重要的地方来了,再根据求导链式法则,咱们如今能够轻易写出图中任意一个高层节点对其任意后代节点的导函数:只须要把链接它们的路径上的全部部分导函数都乘起来就能够了。
好比:
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
到这里反向传播算法已经呼之欲出了,对于一个具体的参数值,咱们只须要把每一个节点的值代入求得的导函数公式就能够求得导数(偏导数),进而获得梯度。
这很简单,咱们先从计算图的底部开始向上,逐个节点计算函数值并保存下来。这个步骤,叫作前向计算(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亿次计算就能够求出梯度。复杂度大大下降,速度将大大加快。
sigmoid函数中没有参数,在实际的神经网络中,咱们都是将sigmoid函数视为一个总体来对待,不必求它的内部节点的导函数。
sigmoid函数的导函数是什么呢?你能够本身求导试试,实际上sigmoid(x)'=sigmoid(x)*(1-sigmoid(x))
。
激动人心的时刻到了,咱们终于要开始用python代码实现深度神经网络的过程,这里咱们打算对第一次实验中的神经网络示例图中的“复合函数”编写反向传播算法。不过为了按部就班,咱们考虑第一层(输入层)只有两个节点,第二层只有一个节点的状况,即以下图:
注意咱们将sigmoid函数图像放在了b1节点后面,表明咱们这里对b1运用sigmoid函数获得了最终的输出h1。
若是你对本身比较有信心,能够不看接下来实现的代码,本身动手试一试。
咱们能够先把图中包含的函数表达式写出来,方便咱们以后写代码参考:b1=w11*a1+w12*a2+bias1
h1=sigmoid(b1)
h1=sigmoid(w11*a1+w12*a2+bias1)
如今咱们建立bp.py
文件,开始编写代码。先来编写从第一层到第二层之间的代码:
1 import numpy as np 2 3 4 5 class FullyConnect: 6 7 def __init__(self, l_x, l_y): # 两个参数分别为输入层的长度和输出层的长度 8 9 self.weights = np.random.randn(l_y, l_x) # 使用随机数初始化参数 10 11 self.bias = np.random.randn(1) # 使用随机数初始化参数 12 13 14 15 def forward(self, x): 16 17 self.x = x # 把中间结果保存下来,以备反向传播时使用 18 19 self.y = np.dot(self.weights, x) + self.bias # 计算w11*a1+w12*a2+bias1
self.y = np.dot(self.weights, x.T) + self.bias # 计算w11*a1+w12*a2+bias1#上面一行貌似不对,应该用这行 21 return self.y # 将这一层计算的结果向前传递 22 23 24 25 def backward(self, d): 26 27 self.dw = d * self.x # 根据链式法则,将反向传递回来的导数值乘以x,获得对参数的梯度 28 29 self.db = d 30 31 self.dx = d * self.weights 32 33 return self.dw, self.db # 返回求得的参数梯度,注意这里若是要继续反向传递梯度,应该返回self.dx
注意在神经网络中,咱们将层与层之间的每一个点都有链接的层叫作全链接(fully connect)层
,因此咱们将这里的类命名为FullyConnect
。
上面的代码很是清楚简洁,咱们的全链接层完成了三个工做:
而后是第二层的输入到最后的输出之间的代码,也就是咱们的sigmoid层:
1 class Sigmoid: 2 3 def __init__(self): # 无参数,不需初始化 4 5 pass 6 7 8 9 def sigmoid(self, x): 10 11 return 1 / (1 + np.exp(-x)) 12 13 14 15 def forward(self, x): 16 17 self.x = x 18 19 self.y = self.sigmoid(x) 20 21 return self.y 22 23 24 25 def backward(self): # 这里sigmoid是最后一层,因此从这里开始反向计算梯度 26 27 sig = self.sigmoid(self.x) 28 29 self.dx = sig * (1 - sig) 30 31 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
上面的代码将每一个网络层写在不一样的类里,而且类里面的接口都是一致的(forward 和 backward),这样作有不少好处,一是最大程度地下降了不一样模块之间的耦合程度,若是某一个层里面的代码须要修改,则只须要修改该层的代码就够了,不须要关心其余层是怎么实现的。另外一方面能够彻底自由地组合不一样的网络层(咱们最后会介绍神经网络里其余种类的网络层)。
实际上,目前不少用于科研和工业生产的深度学习框架不少都是采用这种结构,你能够找一个深度学习框架(好比caffe
)看看它的源码,你会发现里面就是这样一个个写好的网络层。
本次实验,咱们彻底地掌握了梯度降低算法中的关键--反向传播算法。至此,神经网络中最基本的东西你已经所有掌握了。你如今彻底能够本身尝试构建神经网络并使用反向传播算法优化网络中的参数。
若是你把到此为止讲的东西差很少都弄懂了,那很是恭喜你,你应该为本身感到骄傲。若是你暂时还有些东西没有理解,不要气馁,回过头去仔细看看,到网上查查资料,若是实在没法理解,问问咱们实验楼的助教,我相信你最终也能理解。
本次实验,咱们学习了: