零基础入门深度学习(4) - 卷积神经网络

往期回顾

在前面的文章中,咱们介绍了全链接神经网络,以及它的训练和使用。咱们用它来识别了手写数字,然而,这种结构的网络对于图像识别任务来讲并非很合适。本文将要介绍一种更适合图像、语音识别任务的神经网络结构——卷积神经网络(Convolutional Neural Network, CNN)。说卷积神经网络是最重要的一种神经网络也不为过,它在最近几年大放异彩,几乎全部图像、语音识别领域的重要突破都是卷积神经网络取得的,好比谷歌的GoogleNet、微软的ResNet等,战胜李世石的AlphaGo也用到了这种网络。本文将详细介绍卷积神经网络以及它的训练算法,以及动手实现一个简单的卷积神经网络。html

 

一个新的激活函数——Relu

最近几年卷积神经网络中,激活函数每每不选择sigmoid或tanh函数,而是选择relu函数。Relu函数的定义是:python

 

 

 

 

Relu函数图像以下图所示:git

Relu函数做为激活函数,有下面几大优点:github

  • 速度快 和sigmoid函数须要计算指数和倒数相比,relu函数其实就是一个max(0,x),计算代价小不少。
  • 减轻梯度消失问题 回忆一下计算梯度的公式。其中,是sigmoid函数的导数。在使用反向传播算法进行梯度计算时,每通过一层sigmoid神经元,梯度就要乘上一个。从下图能够看出,函数最大值是1/4。所以,乘一个会致使梯度愈来愈小,这对于深层网络的训练是个很大的问题。而relu函数的导数是1,不会致使梯度变小。固然,激活函数仅仅是致使梯度减少的一个因素,但不管如何在这方面relu的表现强于sigmoid。使用relu激活函数可让你训练更深的网络。

  • 稀疏性 经过对大脑的研究发现,大脑在工做的时候只有大约5%的神经元是激活的,而采用sigmoid激活函数的人工神经网络,其激活率大约是50%。有论文声称人工神经网络在15%-30%的激活率时是比较理想的。由于relu函数在输入小于0时是彻底不激活的,所以能够得到一个更低的激活率。
 

全链接网络 VS 卷积网络

全链接神经网络之因此不太适合图像识别任务,主要有如下几个方面的问题:算法

  • 参数数量太多 考虑一个输入1000*1000像素的图片(一百万像素,如今已经不能算大图了),输入层有1000*1000=100万节点。假设第一个隐藏层有100个节点(这个数量并很少),那么仅这一层就有(1000*1000+1)*100=1亿参数,这实在是太多了!咱们看到图像只扩大一点,参数数量就会多不少,所以它的扩展性不好。
  • 没有利用像素之间的位置信息 对于图像识别任务来讲,每一个像素和其周围像素的联系是比较紧密的,和离得很远的像素的联系可能就很小了。若是一个神经元和上一层全部神经元相连,那么就至关于对于一个像素来讲,把图像的全部像素都等同看待,这不符合前面的假设。当咱们完成每一个链接权重的学习以后,最终可能会发现,有大量的权重,它们的值都是很小的(也就是这些链接其实可有可无)。努力学习大量并不重要的权重,这样的学习必将是很是低效的。
  • 网络层数限制 咱们知道网络层数越多其表达能力越强,可是经过梯度降低方法训练深度全链接神经网络很困难,由于全链接神经网络的梯度很难传递超过3层。所以,咱们不可能获得一个很深的全链接神经网络,也就限制了它的能力。

那么,卷积神经网络又是怎样解决这个问题的呢?主要有三个思路:数组

  • 局部链接 这个是最容易想到的,每一个神经元再也不和上一层的全部神经元相连,而只和一小部分神经元相连。这样就减小了不少参数。
  • 权值共享 一组链接能够共享同一个权重,而不是每一个链接有一个不一样的权重,这样又减小了不少参数。
  • 下采样 可使用Pooling来减小每层的样本数,进一步减小参数数量,同时还能够提高模型的鲁棒性。

对于图像识别任务来讲,卷积神经网络经过尽量保留重要的参数,去掉大量不重要的参数,来达到更好的学习效果。网络

接下来,咱们将详述卷积神经网络究竟是何方神圣。架构

 

卷积神经网络是啥

首先,咱们先获取一个感性认识,下图是一个卷积神经网络的示意图:app

图1 卷积神经网络

 

网络架构

如图1所示,一个卷积神经网络由若干卷积层、Pooling层、全链接层组成。你能够构建各类不一样的卷积神经网络,它的经常使用架构模式为:dom

INPUT -> [[CONV]*N -> POOL?]*M -> [FC]*K

也就是N个卷积层叠加,而后(可选)叠加一个Pooling层,重复这个结构M次,最后叠加K个全链接层。

对于图1展现的卷积神经网络:

INPUT -> CONV -> POOL -> CONV -> POOL -> FC -> FC

按照上述模式能够表示为:

INPUT -> [[CONV]*1 -> POOL]*2 -> [FC]*2

也就是:N=1, M=2, K=2

 

三维的层结构

从图1咱们能够发现卷积神经网络的层结构和全链接神经网络的层结构有很大不一样。全链接神经网络每层的神经元是按照一维排列的,也就是排成一条线的样子;而卷积神经网络每层的神经元是按照三维排列的,也就是排成一个长方体的样子,有宽度、高度和深度。

对于图1展现的神经网络,咱们看到输入层的宽度和高度对应于输入图像的宽度和高度,而它的深度为1。接着,第一个卷积层对这幅图像进行了卷积操做(后面咱们会讲如何计算卷积),获得了三个Feature Map。这里的"3"多是让不少初学者迷惑的地方,实际上,就是这个卷积层包含三个Filter,也就是三套参数,每一个Filter均可以把原始输入图像卷积获得一个Feature Map,三个Filter就能够获得三个Feature Map。至于一个卷积层能够有多少个Filter,那是能够自由设定的。也就是说,卷积层的Filter个数也是一个超参数。咱们能够把Feature Map能够看作是经过卷积变换提取到的图像特征,三个Filter就对原始图像提取出三组不一样的特征,也就是获得了三个Feature Map,也称作三个通道(channel)。

继续观察图1,在第一个卷积层以后,Pooling层对三个Feature Map作了下采样(后面咱们会讲如何计算下采样),获得了三个更小的Feature Map。接着,是第二个卷积层,它有5个Filter。每一个Fitler都把前面下采样以后的3个**Feature Map卷积在一块儿,获得一个新的Feature Map。这样,5个Filter就获得了5个Feature Map。接着,是第二个Pooling,继续对5个Feature Map进行下采样**,获得了5个更小的Feature Map。

图1所示网络的最后两层是全链接层。第一个全链接层的每一个神经元,和上一层5个Feature Map中的每一个神经元相连,第二个全链接层(也就是输出层)的每一个神经元,则和第一个全链接层的每一个神经元相连,这样获得了整个网络的输出。

至此,咱们对卷积神经网络有了最基本的感性认识。接下来,咱们将介绍卷积神经网络中各类层的计算和训练。

 

卷积神经网络输出值的计算

 

卷积层输出值的计算

咱们用一个简单的例子来说述如何计算卷积,而后,咱们抽象出卷积层的一些重要概念和计算方法。

假设有一个5*5的图像,使用一个3*3的filter进行卷积,想获得一个3*3的Feature Map,以下所示:

为了清楚的描述卷积计算过程,咱们首先对图像的每一个像素进行编号,用表示图像的第行第列元素;对filter的每一个权重进行编号,用表示第行第列权重,用表示filter的偏置项;对Feature Map的每一个元素进行编号,用表示Feature Map的第行第列元素;用表示激活函数(这个例子选择relu函数做为激活函数)。而后,使用下列公式计算卷积:

 

 

 

例如,对于Feature Map左上角元素来讲,其卷积计算方法为:

 

 

 

 

计算结果以下图所示:

接下来,Feature Map的元素的卷积计算方法为:

 

 

 

 

计算结果以下图所示:

能够依次计算出Feature Map中全部元素的值。下面的动画显示了整个Feature Map的计算过程:

图2 卷积计算

上面的计算过程当中,步幅(stride)为1。步幅能够设为大于1的数。例如,当步幅为2时,Feature Map计算以下:

咱们注意到,当步幅设置为2的时候,Feature Map就变成2*2了。这说明图像大小、步幅和卷积后的Feature Map大小是有关系的。事实上,它们知足下面的关系:

 

 

式式

 

在上面两个公式中,是卷积后Feature Map的宽度;是卷积前图像的宽度;是filter的宽度;是Zero Padding数量,Zero Padding是指在原始图像周围补几圈0,若是的值是1,那么就补1圈0;是步幅;是卷积后Feature Map的高度;是卷积前图像的宽度。式2和式3本质上是同样的。

之前面的例子来讲,图像宽度,filter宽度,Zero Padding,步幅,则

 

 

 

 

说明Feature Map宽度是2。一样,咱们也能够计算出Feature Map高度也是2。

前面咱们已经讲了深度为1的卷积层的计算方法,若是深度大于1怎么计算呢?其实也是相似的。若是卷积前的图像深度为D,那么相应的filter的深度也必须为D。咱们扩展一下式1,获得了深度大于1的卷积计算公式:

 

 

 

在式4中,D是深度;F是filter的大小(宽度或高度,二者相同);表示filter的第层第行第列权重;表示图像的第层第行第列像素;其它的符号含义和式1是相同的,再也不赘述。

咱们前面还曾提到,每一个卷积层能够有多个filter。每一个filter和原始图像进行卷积后,均可以获得一个Feature Map。所以,卷积后Feature Map的深度(个数)和卷积层的filter个数是相同的。

下面的动画显示了包含两个filter的卷积层的计算。咱们能够看到7*7*3输入,通过两个3*3*3filter的卷积(步幅为2),获得了3*3*2的输出。另外咱们也会看到下图的Zero padding是1,也就是在输入元素的周围补了一圈0。Zero padding对于图像边缘部分的特征提取是颇有帮助的。

以上就是卷积层的计算方法。这里面体现了局部链接和权值共享:每层神经元只和上一层部分神经元相连(卷积计算规则),且filter的权值对于上一层全部神经元都是同样的。对于包含两个3*3*3的fitler的卷积层来讲,其参数数量仅有(3*3*3+1)*2=56个,且参数数量与上一层神经元个数无关。与全链接神经网络相比,其参数数量大大减小了。

 

用卷积公式来表达卷积层计算

不想了解太多数学细节的读者能够跳过这一节,不影响对全文的理解。

式4的表达非常繁冗,最好能简化一下。就像利用矩阵能够简化表达全链接神经网络的计算同样,咱们利用卷积公式能够简化卷积神经网络的表达。

下面咱们介绍二维卷积公式。

设矩阵,其行、列数分别为,则二维卷积公式以下:

 

 

 

 

,知足条件

咱们能够把上式写成

 

 

 

若是咱们按照式5来计算卷积,咱们能够发现矩阵A其实是filter,而矩阵B是待卷积的输入,位置关系也有所不一样:

从上图能够看到,A左上角的值与B对应区块中右下角的值相乘,而不是与左上角的相乘。所以,数学中的卷积和卷积神经网络中的『卷积』仍是有区别的,为了不混淆,咱们把卷积神经网络中的『卷积』操做叫作互相关(cross-correlation)操做。

卷积和互相关操做是能够转化的。首先,咱们把矩阵A翻转180度,而后再交换A和B的位置(即把B放在左边而把A放在右边。卷积知足交换率,这个操做不会致使结果变化),那么卷积就变成了互相关。

若是咱们不去考虑二者这么一点点的区别,咱们能够把式5代入到式4:

 

 

 

其中,是卷积层输出的feature map。同式4相比,式6就简单多了。然而,这种简洁写法只适合步长为1的状况。

 

Pooling层输出值的计算

Pooling层主要的做用是下采样,经过去掉Feature Map中不重要的样本,进一步减小参数数量。Pooling的方法不少,最经常使用的是Max Pooling。Max Pooling实际上就是在n*n的样本中取最大值,做为采样后的样本值。下图是2*2 max pooling:

除了Max Pooing以外,经常使用的还有Mean Pooling——取各样本的平均值。

对于深度为D的Feature Map,各层独立作Pooling,所以Pooling后的深度仍然为D。

 

全链接层

全链接层输出值的计算和上一篇文章零基础入门深度学习(3) - 神经网络和反向传播算法讲过的全链接神经网络是同样的,这里就再也不赘述了。

 

卷积神经网络的训练

和全链接神经网络相比,卷积神经网络的训练要复杂一些。但训练的原理是同样的:利用链式求导计算损失函数对每一个权重的偏导数(梯度),而后根据梯度降低公式更新权重。训练算法依然是反向传播算法。

咱们先回忆一下上一篇文章零基础入门深度学习(3) - 神经网络和反向传播算法介绍的反向传播算法,整个算法分为三个步骤:

  1. 前向计算每一个神经元的输出值表示网络的第个神经元,如下同);
  2. 反向计算每一个神经元的偏差项在有的文献中也叫作敏感度(sensitivity)。它其实是网络的损失函数对神经元加权输入的偏导数,即
  3. 计算每一个神经元链接权重的梯度(表示从神经元链接到神经元的权重),公式为,其中,表示神经元的输出。

最后,根据梯度降低法则更新每一个权重便可。

对于卷积神经网络,因为涉及到局部链接、下采样的等操做,影响到了第二步偏差项的具体计算方法,而权值共享影响了第三步权重的梯度的计算方法。接下来,咱们分别介绍卷积层和Pooling层的训练算法。

 

卷积层的训练

对于卷积层,咱们先来看看上面的第二步,即如何将偏差项传递到上一层;而后再来看看第三步,即如何计算filter每一个权值的梯度。

 

卷积层偏差项的传递

 
最简单状况下偏差项的传递

咱们先来考虑步长为一、输入的深度为一、filter个数为1的最简单的状况。

假设输入的大小为3*3,filter大小为2*2,按步长为1卷积,咱们将获得2*2的feature map。以下图所示:

在上图中,为了描述方便,咱们为每一个元素都进行了编号。用表示第层第行第列的偏差项;用表示filter第行第列权重,用表示filter的偏置项;用表示第层第行第列神经元的输出;用表示第行神经元的加权输入;用表示第层第行第列的偏差项;用表示第层的激活函数。它们之间的关系以下:

 

 

 

 

上式中,都是数组,是由组成的数组,表示卷积操做。

在这里,咱们假设第中的每一个值都已经算好,咱们要作的是计算第层每一个神经元的偏差项

根据链式求导法则:

 

 

 

 

咱们先求第一项。咱们先来看几个特例,而后从中总结出通常性的规律。

例1,计算仅与的计算有关:

 

 

 

 

所以:

 

 

 

 

例2,计算的计算都有关:

 

 

 

 

所以:

 

 

 

 

例3,计算的计算都有关:

 

 

 

 

所以:

 

 

 

 

从上面三个例子,咱们发挥一下想象力,不难发现,计算,至关于把第层的sensitive map周围补一圈0,在与180度翻转后的filter进行cross-correlation,就能获得想要结果,以下图所示:

由于卷积至关于将filter旋转180度的cross-correlation,所以上图的计算能够用卷积公式完美的表达:

 

 

 

 

上式中的表示第层的filter的权重数组。也能够把上式的卷积展开,写成求和的形式:

 

 

 

 

如今,咱们再求第二项。由于

 

 

 

 

因此这一项极其简单,仅求激活函数的导数就好了。

 

 

 

 

将第一项和第二项组合起来,咱们获得最终的公式:

 

 

 

也能够将式7写成卷积的形式:

 

 

 

其中,符号表示element-wise product,即将矩阵中每一个对应元素相乘。注意式8中的都是矩阵。

以上就是步长为一、输入的深度为一、filter个数为1的最简单的状况,卷积层偏差项传递的算法。下面咱们来推导一下步长为S的状况。

 
卷积步长为S时的偏差传递

咱们先来看看步长为S与步长为1的差异。

如上图,上面是步长为1时的卷积结果,下面是步长为2时的卷积结果。咱们能够看出,由于步长为2,获得的feature map跳过了步长为1时相应的部分。所以,当咱们反向计算偏差项时,咱们能够对步长为S的sensitivity map相应的位置进行补0,将其『还原』成步长为1时的sensitivity map,再用式8进行求解。

 
输入层深度为D时的偏差传递

当输入深度为D时,filter的深度也必须为D,层的通道只与filter的通道的权重进行计算。所以,反向计算偏差项时,咱们可使用式8,用filter的第通道权重对第层sensitivity map进行卷积,获得第通道的sensitivity map。以下图所示:

 
filter数量为N时的偏差传递

filter数量为N时,输出层的深度也为N,第个filter卷积产生输出层的第个feature map。因为第层每一个加权输入都同时影响了第层全部feature map的输出值,所以,反向计算偏差项时,须要使用全导数公式。也就是,咱们先使用第个filter对第层相应的第个sensitivity map进行卷积,获得一组N个层的偏sensitivity map。依次用每一个filter作这种卷积,就获得D组偏sensitivity map。最后在各组之间将N个偏sensitivity map 按元素相加,获得最终的N个层的sensitivity map:

 

 

 

以上就是卷积层偏差项传递的算法,若是读者还有所困惑,能够参考后面的代码实现来理解。

 

卷积层filter权重梯度的计算

咱们要在获得第层sensitivity map的状况下,计算filter的权重的梯度,因为卷积层是权重共享的,所以梯度的计算稍有不一样。

如上图所示,是第层的输出,是第层filter的权重,是第层的sensitivity map。咱们的任务是计算的梯度,即

为了计算偏导数,咱们须要考察权重的影响。权重项经过影响的值,进而影响。咱们仍然经过几个具体的例子来看权重项的影响,而后再从中总结出规律。

例1,计算

 

 

 

 

从上面的公式看出,因为权值共享,权值对全部的都有影响。...的函数,而...又是的函数,根据全导数公式,计算就是要把每一个偏导数都加起来:

 

 

 

 

例2,计算

经过查看的关系,咱们很容易获得:

 

 

 

 

实际上,每一个权重项都是相似的,咱们不一一举例了。如今,是咱们再次发挥想象力的时候,咱们发现计算规律是:

 

 

 

 

也就是用sensitivity map做为卷积核,在input上进行cross-correlation,以下图所示:

最后,咱们来看一看偏置项的梯度。经过查看前面的公式,咱们很容易发现:

 

 

 

 

也就是偏置项的梯度就是sensitivity map全部偏差项之和。

对于步长为S的卷积层,处理方法与传递**偏差项*是同样的,首先将sensitivity map『还原』成步长为1时的sensitivity map,再用上面的方法进行计算。

得到了全部的梯度以后,就是根据梯度降低算法来更新每一个权重。这在前面的文章中已经反复写过,这里就再也不重复了。

至此,咱们已经解决了卷积层的训练问题,接下来咱们看一看Pooling层的训练。

 

Pooling层的训练

不管max pooling仍是mean pooling,都没有须要学习的参数。所以,在卷积神经网络的训练中,Pooling层须要作的仅仅是将偏差项传递到上一层,而没有梯度的计算。

 

Max Pooling偏差项的传递

以下图,假设第层大小为4*4,pooling filter大小为2*2,步长为2,这样,max pooling以后,第层大小为2*2。假设第层的值都已经计算完毕,咱们如今的任务是计算第层的值。

咱们用表示第层的加权输入;用表示第层的加权输入。咱们先来考察一个具体的例子,而后再总结通常性的规律。对于max pooling:

 

 

 

 

也就是说,只有区块中最大的才会对的值产生影响。咱们假设最大的值是,则上式至关于:

 

 

 

 

那么,咱们不难求得下面几个偏导数:

 

 

 

 

所以:

 

 

 

 

而:

 

 

 

 

如今,咱们发现了规律:对于max pooling,下一层的偏差项的值会原封不动的传递到上一层对应区块中的最大值所对应的神经元,而其余神经元的偏差项的值都是0。以下图所示(假设为所在区块中的最大输出值):

 

Mean Pooling偏差项的传递

咱们仍是用前面屡试不爽的套路,先研究一个特殊的情形,再扩展为通常规律。

如上图,咱们先来考虑计算。咱们先来看看如何影响

 

 

 

 

根据上式,咱们一眼就能看出来:

 

 

 

 

因此,根据链式求导法则,咱们不难算出:

 

 

 

 

一样,咱们能够算出

 

 

 

 

如今,咱们发现了规律:对于mean pooling,下一层的偏差项的值会平均分配到上一层对应区块中的全部神经元。以下图所示:

上面这个算法能够表达为高大上的克罗内克积(Kronecker product)的形式,有兴趣的读者能够研究一下。

 

 

 

 

其中,是pooling层filter的大小,都是矩阵。

至此,咱们已经把卷积层、Pooling层的训练算法介绍完毕,加上上一篇文章讲的全链接层训练算法,您应该已经具有了编写卷积神经网络代码所须要的知识。为了加深对知识的理解,接下来,咱们将展现如何实现一个简单的卷积神经网络。

 

卷积神经网络的实现

完整代码请参考GitHub: https://github.com/hanbt/learn_dl/blob/master/cnn.py (python2.7)

如今,咱们亲自动手实现一个卷积神经网络,以便巩固咱们所学的知识。

首先,咱们要改变一下代码的架构,『层』成为了咱们最核心的组件。这是由于卷积神经网络有不一样的层,而每种层的算法都在对应的类中实现。

此次,咱们用到了在python中编写算法常常会用到的numpy包。为了使用numpy,咱们须要先将numpy导入:

 
  1. import numpy as np
 

卷积层的实现

 

卷积层初始化

咱们用ConvLayer类来实现一个卷积层。下面的代码是初始化一个卷积层,能够在构造函数中设置卷积层的超参数。

 
  1. class ConvLayer(object):
  2. def __init__(self, input_width, input_height,
  3. channel_number, filter_width,
  4. filter_height, filter_number,
  5. zero_padding, stride, activator,
  6. learning_rate):
  7. self.input_width = input_width
  8. self.input_height = input_height
  9. self.channel_number = channel_number
  10. self.filter_width = filter_width
  11. self.filter_height = filter_height
  12. self.filter_number = filter_number
  13. self.zero_padding = zero_padding
  14. self.stride = stride
  15. self.output_width = \
  16. ConvLayer.calculate_output_size(
  17. self.input_width, filter_width, zero_padding,
  18. stride)
  19. self.output_height = \
  20. ConvLayer.calculate_output_size(
  21. self.input_height, filter_height, zero_padding,
  22. stride)
  23. self.output_array = np.zeros((self.filter_number,
  24. self.output_height, self.output_width))
  25. self.filters = []
  26. for i in range(filter_number):
  27. self.filters.append(Filter(filter_width,
  28. filter_height, self.channel_number))
  29. self.activator = activator
  30. self.learning_rate = learning_rate

calculate_output_size函数用来肯定卷积层输出的大小,其实现以下:

 
  1. @staticmethod
  2. def calculate_output_size(input_size,
  3. filter_size, zero_padding, stride):
  4. return (input_size - filter_size +
  5. 2 * zero_padding) / stride + 1

Filter类保存了卷积层的参数以及梯度,而且实现了用梯度降低算法来更新参数。

 
  1. class Filter(object):
  2. def __init__(self, width, height, depth):
  3. self.weights = np.random.uniform(-1e-4, 1e-4,
  4. (depth, height, width))
  5. self.bias = 0
  6. self.weights_grad = np.zeros(
  7. self.weights.shape)
  8. self.bias_grad = 0
  9. def __repr__(self):
  10. return 'filter weights:\n%s\nbias:\n%s' % (
  11. repr(self.weights), repr(self.bias))
  12. def get_weights(self):
  13. return self.weights
  14. def get_bias(self):
  15. return self.bias
  16. def update(self, learning_rate):
  17. self.weights -= learning_rate * self.weights_grad
  18. self.bias -= learning_rate * self.bias_grad

咱们对参数的初始化采用了经常使用的策略,即:权重随机初始化为一个很小的值,而偏置项初始化为0。

Activator类实现了激活函数,其中,forward方法实现了前向计算,而backward方法则是计算导数。好比,relu函数的实现以下:

 
  1. class ReluActivator(object):
  2. def forward(self, weighted_input):
  3. #return weighted_input
  4. return max(0, weighted_input)
  5. def backward(self, output):
  6. return 1 if output > 0 else 0
 

卷积层前向计算的实现

ConvLayer类的forward方法实现了卷积层的前向计算(即计算根据输入来计算卷积层的输出),下面是代码实现:

 
  1. def forward(self, input_array):
  2. '''
  3. 计算卷积层的输出
  4. 输出结果保存在self.output_array
  5. '''
  6. self.input_array = input_array
  7. self.padded_input_array = padding(input_array,
  8. self.zero_padding)
  9. for f in range(self.filter_number):
  10. filter = self.filters[f]
  11. conv(self.padded_input_array,
  12. filter.get_weights(), self.output_array[f],
  13. self.stride, filter.get_bias())
  14. element_wise_op(self.output_array,
  15. self.activator.forward)

上面的代码里面包含了几个工具函数。element_wise_op函数实现了对numpy数组进行按元素操做,并将返回值写回到数组中,代码以下:

 
  1. # 对numpy数组进行element wise操做
  2. def element_wise_op(array, op):
  3. for i in np.nditer(array,
  4. op_flags=['readwrite']):
  5. i[...] = op(i)

conv函数实现了2维和3维数组的卷积,代码以下:

 
  1. def conv(input_array,
  2. kernel_array,
  3. output_array,
  4. stride, bias):
  5. '''
  6. 计算卷积,自动适配输入为2D和3D的状况
  7. '''
  8. channel_number = input_array.ndim
  9. output_width = output_array.shape[1]
  10. output_height = output_array.shape[0]
  11. kernel_width = kernel_array.shape[-1]
  12. kernel_height = kernel_array.shape[-2]
  13. for i in range(output_height):
  14. for j in range(output_width):
  15. output_array[i][j] = (
  16. get_patch(input_array, i, j, kernel_width,
  17. kernel_height, stride) * kernel_array
  18. ).sum() + bias

padding函数实现了zero padding操做:

 
  1. # 为数组增长Zero padding
  2. def padding(input_array, zp):
  3. '''
  4. 为数组增长Zero padding,自动适配输入为2D和3D的状况
  5. '''
  6. if zp == 0:
  7. return input_array
  8. else:
  9. if input_array.ndim == 3:
  10. input_width = input_array.shape[2]
  11. input_height = input_array.shape[1]
  12. input_depth = input_array.shape[0]
  13. padded_array = np.zeros((
  14. input_depth,
  15. input_height + 2 * zp,
  16. input_width + 2 * zp))
  17. padded_array[:,
  18. zp : zp + input_height,
  19. zp : zp + input_width] = input_array
  20. return padded_array
  21. elif input_array.ndim == 2:
  22. input_width = input_array.shape[1]
  23. input_height = input_array.shape[0]
  24. padded_array = np.zeros((
  25. input_height + 2 * zp,
  26. input_width + 2 * zp))
  27. padded_array[zp : zp + input_height,
  28. zp : zp + input_width] = input_array
  29. return padded_array
 

卷积层反向传播算法的实现

如今,是介绍卷积层核心算法的时候了。咱们知道反向传播算法须要完成几个任务:

  1. 将偏差项传递到上一层。
  2. 计算每一个参数的梯度。
  3. 更新参数。

如下代码都是在ConvLayer类中实现。咱们先来看看将偏差项传递到上一层的代码实现。

 
  1. def bp_sensitivity_map(self, sensitivity_array,
  2. activator):
  3. '''
  4. 计算传递到上一层的sensitivity map
  5. sensitivity_array: 本层的sensitivity map
  6. activator: 上一层的激活函数
  7. '''
  8. # 处理卷积步长,对原始sensitivity map进行扩展
  9. expanded_array = self.expand_sensitivity_map(
  10. sensitivity_array)
  11. # full卷积,对sensitivitiy map进行zero padding
  12. # 虽然原始输入的zero padding单元也会得到残差
  13. # 但这个残差不须要继续向上传递,所以就不计算了
  14. expanded_width = expanded_array.shape[2]
  15. zp = (self.input_width +
  16. self.filter_width - 1 - expanded_width) / 2
  17. padded_array = padding(expanded_array, zp)
  18. # 初始化delta_array,用于保存传递到上一层的
  19. # sensitivity map
  20. self.delta_array = self.create_delta_array()
  21. # 对于具备多个filter的卷积层来讲,最终传递到上一层的
  22. # sensitivity map至关于全部的filter的
  23. # sensitivity map之和
  24. for f in range(self.filter_number):
  25. filter = self.filters[f]
  26. # 将filter权重翻转180度
  27. flipped_weights = np.array(map(
  28. lambda i: np.rot90(i, 2),
  29. filter.get_weights()))
  30. # 计算与一个filter对应的delta_array
  31. delta_array = self.create_delta_array()
  32. for d in range(delta_array.shape[0]):
  33. conv(padded_array[f], flipped_weights[d],
  34. delta_array[d], 1, 0)
  35. self.delta_array += delta_array
  36. # 将计算结果与激活函数的偏导数作element-wise乘法操做
  37. derivative_array = np.array(self.input_array)
  38. element_wise_op(derivative_array,
  39. activator.backward)
  40. self.delta_array *= derivative_array

expand_sensitivity_map方法就是将步长为S的sensitivity map『还原』为步长为1的sensitivity map,代码以下:

 
  1. def expand_sensitivity_map(self, sensitivity_array):
  2. depth = sensitivity_array.shape[0]
  3. # 肯定扩展后sensitivity map的大小
  4. # 计算stride为1时sensitivity map的大小
  5. expanded_width = (self.input_width -
  6. self.filter_width + 2 * self.zero_padding + 1)
  7. expanded_height = (self.input_height -
  8. self.filter_height + 2 * self.zero_padding + 1)
  9. # 构建新的sensitivity_map
  10. expand_array = np.zeros((depth, expanded_height,
  11. expanded_width))
  12. # 从原始sensitivity map拷贝偏差值
  13. for i in range(self.output_height):
  14. for j in range(self.output_width):
  15. i_pos = i * self.stride
  16. j_pos = j * self.stride
  17. expand_array[:,i_pos,j_pos] = \
  18. sensitivity_array[:,i,j]
  19. return expand_array

create_delta_array是建立用来保存传递到上一层的sensitivity map的数组。

 
  1. def create_delta_array(self):
  2. return np.zeros((self.channel_number,
  3. self.input_height, self.input_width))

接下来,是计算梯度的代码。

 
  1. def bp_gradient(self, sensitivity_array):
  2. # 处理卷积步长,对原始sensitivity map进行扩展
  3. expanded_array = self.expand_sensitivity_map(
  4. sensitivity_array)
  5. for f in range(self.filter_number):
  6. # 计算每一个权重的梯度
  7. filter = self.filters[f]
  8. for d in range(filter.weights.shape[0]):
  9. conv(self.padded_input_array[d],
  10. expanded_array[f],
  11. filter.weights_grad[d], 1, 0)
  12. # 计算偏置项的梯度
  13. filter.bias_grad = expanded_array[f].sum()

最后,是按照梯度降低算法更新参数的代码,这部分很是简单。

 
  1. def update(self):
  2. '''
  3. 按照梯度降低,更新权重
  4. '''
  5. for filter in self.filters:
  6. filter.update(self.learning_rate)
 

卷积层的梯度检查

为了验证咱们的公式推导和代码实现的正确性,咱们必需要对卷积层进行梯度检查。下面是代吗实现:

 
  1. def init_test():
  2. a = np.array(
  3. [[[0,1,1,0,2],
  4. [2,2,2,2,1],
  5. [1,0,0,2,0],
  6. [0,1,1,0,0],
  7. [1,2,0,0,2]],
  8. [[1,0,2,2,0],
  9. [0,0,0,2,0],
  10. [1,2,1,2,1],
  11. [1,0,0,0,0],
  12. [1,2,1,1,1]],
  13. [[2,1,2,0,0],
  14. [1,0,0,1,0],
  15. [0,2,1,0,1],
  16. [0,1,2,2,2],
  17. [2,1,0,0,1]]])
  18. b = np.array(
  19. [[[0,1,1],
  20. [2,2,2],
  21. [1,0,0]],
  22. [[1,0,2],
  23. [0,0,0],
  24. [1,2,1]]])
  25. cl = ConvLayer(5,5,3,3,3,2,1,2,IdentityActivator(),0.001)
  26. cl.filters[0].weights = np.array(
  27. [[[-1,1,0],
  28. [0,1,0],
  29. [0,1,1]],
  30. [[-1,-1,0],
  31. [0,0,0],
  32. [0,-1,0]],
  33. [[0,0,-1],
  34. [0,1,0],
  35. [1,-1,-1]]], dtype=np.float64)
  36. cl.filters[0].bias=1
  37. cl.filters[1].weights = np.array(
  38. [[[1,1,-1],
  39. [-1,-1,1],
  40. [0,-1,1]],
  41. [[0,1,0],
  42. [-1,0,-1],
  43. [-1,1,0]],
  44. [[-1,0,0],
  45. [-1,0,1],
  46. [-1,0,0]]], dtype=np.float64)
  47. return a, b, cl
  48. def gradient_check():
  49. '''
  50. 梯度检查
  51. '''
  52. # 设计一个偏差函数,取全部节点输出项之和
  53. error_function = lambda o: o.sum()
  54. # 计算forward值
  55. a, b, cl = init_test()
  56. cl.forward(a)
  57. # 求取sensitivity map,是一个全1数组
  58. sensitivity_array = np.ones(cl.output_array.shape,
  59. dtype=np.float64)
  60. # 计算梯度
  61. cl.backward(a, sensitivity_array,
  62. IdentityActivator())
  63. # 检查梯度
  64. epsilon = 10e-4
  65. for d in range(cl.filters[0].weights_grad.shape[0]):
  66. for i in range(cl.filters[0].weights_grad.shape[1]):
  67. for j in range(cl.filters[0].weights_grad.shape[2]):
  68. cl.filters[0].weights[d,i,j] += epsilon
  69. cl.forward(a)
  70. err1 = error_function(cl.output_array)
  71. cl.filters[0].weights[d,i,j] -= 2*epsilon
  72. cl.forward(a)
  73. err2 = error_function(cl.output_array)
  74. expect_grad = (err1 - err2) / (2 * epsilon)
  75. cl.filters[0].weights[d,i,j] += epsilon
  76. print 'weights(%d,%d,%d): expected - actural %f - %f' % (
  77. d, i, j, expect_grad, cl.filters[0].weights_grad[d,i,j])

上面代码值得思考的地方在于,传递给卷积层的sensitivity map是全1数组,留给读者本身推导一下为何是这样(提示:激活函数选择了identity函数:)。读者若是还有困惑,请写在文章评论中,我会回复。

运行上面梯度检查的代码,咱们获得的输出以下,指望的梯度和实际计算出的梯度一致,这证实咱们的算法推导和代码实现确实是正确的。

以上就是卷积层的实现。

 

Max Pooling层的实现

max pooling层的实现相对简单,咱们直接贴出所有代码以下:

 
  1. class MaxPoolingLayer(object):
  2. def __init__(self, input_width, input_height,
  3. channel_number, filter_width,
  4. filter_height, stride):
  5. self.input_width = input_width
  6. self.input_height = input_height
  7. self.channel_number = channel_number
  8. self.filter_width = filter_width
  9. self.filter_height = filter_height
  10. self.stride = stride
  11. self.output_width = (input_width -
  12. filter_width) / self.stride + 1
  13. self.output_height = (input_height -
  14. filter_height) / self.stride + 1
  15. self.output_array = np.zeros((self.channel_number,
  16. self.output_height, self.output_width))
  17. def forward(self, input_array):
  18. for d in range(self.channel_number):
  19. for i in range(self.output_height):
  20. for j in range(self.output_width):
  21. self.output_array[d,i,j] = (
  22. get_patch(input_array[d], i, j,
  23. self.filter_width,
  24. self.filter_height,
  25. self.stride).max())
  26. def backward(self, input_array, sensitivity_array):
  27. self.delta_array = np.zeros(input_array.shape)
  28. for d in range(self.channel_number):
  29. for i in range(self.output_height):
  30. for j in range(self.output_width):
  31. patch_array = get_patch(
  32. input_array[d], i, j,
  33. self.filter_width,
  34. self.filter_height,
  35. self.stride)
  36. k, l = get_max_index(patch_array)
  37. self.delta_array[d,
  38. i * self.stride + k,
  39. j * self.stride + l] = \
  40. sensitivity_array[d,i,j]

全链接层的实现和上一篇文章相似,在此就再也不赘述了。至此,你已经拥有了实现了一个简单的卷积神经网络所须要的基本组件。对于卷积神经网络,如今有不少优秀的开源实现,所以咱们并不须要真的本身去实现一个。贴出这些代码的目的是为了让咱们更好的了解卷积神经网络的基本原理。

 

卷积神经网络的应用

 

MNIST手写数字识别

LeNet-5是实现手写数字识别的卷积神经网络,在MNIST测试集上,它取得了0.8%的错误率。LeNet-5的结构以下:

关于LeNet-5的详细介绍,网上的资料不少,所以就再也不重复了。感兴趣的读者能够尝试用咱们本身实现的卷积神经网络代码去构造并训练LeNet-5(固然代码会更复杂一些)。

 

小节

因为卷积神经网络的复杂性,咱们写出了整个系列目前为止最长的一篇文章,相信读者也和做者同样累的要死。卷积神经网络是深度学习最重要的工具(我犹豫要不要写上『之一』呢),付出一些辛苦去理解它也是值得的。若是您真正理解了本文的内容,至关于迈过了入门深度学习最重要的一到门槛。在下一篇文章中,咱们介绍深度学习另一种很是重要的工具:循环神经网络,届时咱们的系列文章也将完成过半。每篇文章都是一个过滤器,对于坚持到这里的读者们,入门深度学习曙光已现,加油。

 

 

参考资料

  1. CS231n Convolutional Neural Networks for Visual Recognition
  2. ReLu (Rectified Linear Units) 激活函数
  3. Jake Bouvrie, Notes on Convolutional Neural Networks, 2006
  4. Ian Goodfellow, Yoshua Bengio, Aaron Courville, Deep Learning, MIT Press, 2016
  5. 转载自:https://www.zybuluo.com/hanbingtao/note/485480
相关文章
相关标签/搜索