从多层感知器到卷积网络(二)




上一篇中,我们讲解了什么是MLP以及如何训练得到一个MLP,读者大概对神经网络有一定的印象了。我们总说,好戏在后头,接下来这一大块头,我们将重点介绍卷积网络。

何谓卷积

单凭卷积这一个称号大概可以吓死一半的普通老百姓了。一开始接触卷积网络的时候,我就差点成了那一半的老百姓,幸好我命大,最终挺过来了。卷积,只依稀记得当年大学概率论稍有提过这样的名词,那时不愿深究,现在胆子大了,没事,维基搞起,卷积定义为:

f(t)g(xt)dt∫−∞+∞f(t)∗g(x−t)dt

好吧,这货长得这么奇怪,根本无法直觉地知道此货的作用,难怪一般人受不了。这还是是卷积一维连续的情况,但是我们的图片天生是二维的,而且计算机也只能处理离散的情况,所以这里我们的卷积应该是这样:

h[m,n]=i=0Kj=0Kk[i,j]f[ni,mj]h[m,n]=∑i=0K∑j=0Kk[i,j]∗f[n+i,m+j]

去掉了积分,现在友好一点。然而该一头雾水还是会一头雾水的,Ufldl的一张图片,一图胜万语啊,且看:

卷积过程 
图中,我们有一张图片Image,大小是5X5,而经过卷积后,产生的卷积特征(图右),我们记为h,其大小是3X3,那个一直在移动的橘色方块术语比较多,有filter,kernel等等,我们就以kernel称呼吧,其大小为3X3。现在,我们可以和公式对应对应: 
原始图片: 

f=1,1,1,0,00,1,1,1,00,0,1,1,10,0,1,1,00,1,1,0,0f=[1,1,1,0,00,1,1,1,00,0,1,1,10,0,1,1,00,1,1,0,0]

我们的核函数:  
k=1,0,10,1,01,0,1k=[1,0,10,1,01,0,1]

卷积后的特征:  
h=4,3,42,4,32,3,4h=[4,3,42,4,32,3,4]

注意,我们的目的是要求取卷积后的特征,那么将我们的卷积公式代入这个例子中,可以得到:  
h[m,n]=i=03j=03k[i,j]f[ni,mj]m[0,3),n[0,3)h[m,n]=∑i=03∑j=03k[i,j]∗f[n+i,m+j]m∈[0,3),n∈[0,3)

还不爽?还好我还有料,再来一发python代码:

width = 3
height = 3
kernel_size = 3
for m in range(width):
    for n in range(height):
        for i in range(kernel_size):
            for j in range(kernel_size):
                h[m,n] += k[i,j] * f[m+i,n+j]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

来了这么一发,大家应该满足了吧。 
但是,这样计算的目的是什么呢,这么奇怪的计算方式,到底对于以后的识别有什么帮助呢?我们来看看卷积到底带给我们什么了,且看Coursera上海交通大学医学图像处理课程中卷积(因为图片是黑白的,其卷积过程完全由上面的几行代码即可实现)的几个例子: 
检测边缘
去噪
看到了吧,原始图片经过中间特殊的卷积核处理之后,可以得到图片的边缘,或者可以去除图片的噪音。我们就是想利用神经网络,学习得出类似这样的核函数,进而获取有利于图像识别的特征(读者若还是不爽,可以参阅http://colah.github.io,此博客图文并茂,生动而详细地阐明了卷积)。

彩图卷积

上面交大的老师向我们展示了黑白图片卷积效果,然而现实的图片基本是彩色的,黑白时代早已不复存在。那么如何进行彩图卷积呢?且容我道来: 
首先,我们稍微科普一下图片处理的一些知识,我们知道,任何颜色都可以通过红绿蓝(RGB)这三种颜色合成,彩色图片就是通过RGB这三个通道合成的,且看https://en.wikipedia.org/wiki/Channel_(digital_image)上面的一张图片: 
彩色图片的RGB通道
彩色图片就是这么得来的,现在,我们已经知道如何对一张黑白图片进行卷积,那么对于彩色图片,可以认为同时对三张灰色图片进行处理,那么一张灰色图片对应一个卷积核,此时的核应该为:

k=K[kernelsize,kernelsize,depth]k=K[kernelsize,kernelsize,depth]

其中depth代表同时处理图片的个数,对于一张彩色图片,depth=3。

此时的图片ff:

f=F[width,height,depth]f=F[width,height,depth]

而输出的卷积特征还是不变。且看我用代码说明:

width = 5
height = 5
depth = 3
kernel_size = 3
for d in range(depth):
    for m in range(width):
        for n in range(height):
            for i in range(kernel_size):
                for j in range(kernel_size):
                    h[m,n] += k[i,j,d] * f[m+i,n+j,d]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

用了5重循环,略复杂,但是比较容易理解。

多核卷积

稍等一下,我们刚刚提到,不同的核函数可以学到不同的特征,那么求知若渴的我们肯定想来多几个核,学到更多不一样的特征。如何将上面讲到的单核卷积扩展到多个核呢,且看:

多个核的的核函数定义:

k=K[kernelsize,kernelsize,depth,kernelnum]k=K[kernelsize,kernelsize,depth,kernelnum]

简单吧,再扩展一维,马上搞定一切。而对应的输出卷积特征也会相应地扩展,还是代码比较容易说明,behold:

width = 5
height = 5
depth = 3
kernelnum = 44个卷积核
kernel_size = 3
for k in range(kernelnum):
    for d in range(depth):
        for m in range(width):
            for n in range(height):
                for i in range(kernel_size):
                    for j in range(kernel_size):
                        h[m,n,k] += k[i,j,d,k] * f[m+i,n+j,d]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

好了,卷积在我们这里不会再复杂下去了,我们以6重循环(虽然做法比较Naive,但是却能实实在在地掌握)揭开真实世界的卷积过程。

卷积网络中的卷积

理解了卷积的过程,理解卷积网络也就指日可待了。卷积网络其实和我们上节所讲述的前馈网络并无太大区别,只是将其中某些层换成了卷积层而已,所以卷积网络也是一种特殊的前馈网络。而卷积网络中的卷积层,就是一个多核卷积的过程的抽象。那么卷积网络最简单的结构是怎样的呢?又是怎样确定和学习到网络中的每一个未知参数呢?且听我娓娓道来。

卷积中超参数确定

到目前为止,前面所讲的,好多参数都没介绍是如何获得的,接下来,我一个一个讲清楚。 
首先,对于图片,这是输入,我们能做的只是对其进行扩大或缩小,而一般实践中,有时候我们会对图片进行zero-padding,就是在其外围进行一些补0操作,假如一张5X5的图片,我们对其zero-padding,那么它会变成7X7的图片,外围都是用0填充,为什么要进行zero-padding呢,一个是为了使图片的形状更方便我们进行卷积,另一个是因为它可以提高识别表现(详细原因请参考cs231n的课程)。由于zero-padding的存在,我们引入了hyper-parameter P。 
处理好输入后,我们第二个要确定的是核函数,基本上核的大小和个数都是我们人为指定的,也就是KernelSize还有KernelNum,此处我们又引入了两个hyper-parameter Ksize,Knum。 
那么决定好输入还有核函数之后,对于卷积,我们还会引入一个步长Stride的概念,我们上面介绍卷积的时候,默认Stride=1,也就是核每一次只会向右或向下移动一个像素点,为什么不能一次移动两个或三个像素点呢,于是Stride就应运而生了。此时引入第三个hyper-parameter S。 
行了,需要我们人为指定的参数就这么多。等一下,有些读者可能还会问,卷积后特征形状怎么确定呢,读者请拿起笔,在纸上验证验证一下下面这条公式,设,图片宽度为W,卷积核宽为F,步长S,zero-padding P,输出卷积特征的宽为H,则有: 

H=(WF+2P)/S+1H=(W−F+2P)/S+1

也就是说卷积特征的大小是确定的,而卷积特征的个数和你选取的核函数个数是一样的。

Pooling过程

在卷积网络中,通常会引入一个操作,称为Pooling,在Yann Lecun的LeNet中使用的是mean pooling,但是后来研究卷积网络的人发现另外一种操作Max pooling更为有效,接下来,我们着重介绍Max Pooling。 
此处我们再次借用ufldl中的一张动图:

max pooling过程

其中红色区域是pooling size,对于Max pooling,在红色方块每走一步,它都要进行一次pooling,也就是选取它自身区域下输出最大的值,作为下一个输出。这样说抽象了一点,幸好cs231n种有一张图片比较清晰反映这个过程:

max pooling

pooling过程就是一个采样过程,这个过程可以起到一定的图像旋转不会影响识别的作用,想象一下,一张图片,你稍微旋转一些角度,它的Max pooling的值大致是不变的。 
再分析一下,经过max-pooling之后,输出应该是怎样的呢,一般max-pooling我们通常使用non-overlapping的方式,也就是如上图所示,各个pooling的方块之间是没有重叠的,那么对于2x2,步长为2的max-pooling,其输出将会将原始输入缩减为原来的1/4,且看代码:

#input = [8X8X5]的输入
input_w = 8
input_h = 8
input_d = 5

pool_w = pool_h = 2
stride = 2

output_w = input_w / 2
output_h = input_h / 2
output_d = input_d

for d in range(output_d):
    for w in range(output_w):
        for h in range(output_h):
            output[w,h,d] = max_pool(input,d,h,w)

def max_pool(self,input,dIdx,hIdx,wIdx):
    w = wIdx * self.stride
    h = hIdx * self.stride
    maxVal = -999999999
    for i in range(self.kernelSize):
       for j in range(self.kernelSize):
          if maxVal < input[w+i,h+j,dIdx]:
             maxVal = input[w+i,h+j,dIdx]
    return maxVal
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

很好,卷积网络的架构部分我们已经学习了完毕了,通常一张图片会作为输入,然后经过conv卷积一下,再经过pooling层,然后又再次卷积,再pooling,知道设计者觉得到达合适程度后,再把输出作为我们上一节讲解的feedforward network的输入,再进行识别。

学习卷积网络中的参数

又开始进入我们的高潮环节了,读者系好裤腰带,喝点水,坐稳了,听我道来。 
如何学习网络的参数呢,上一次,我们介绍了一种高效的学习方法,backpropagation,并且实践说明它确实是一种行之有效的方法。那么学习卷积网络的参数,没有别人,依旧是我们的老朋友,backprop。上一节中,我们的输入是一个向量,但是对于卷积网络,我们的输入是一个三维的输入,那么如何将backprop应用到卷积网络中呢?我们假定我们的网络是如下结构:

input=>conv=>tanh=>maxpooling=>conv=>tanh=>maxpooling=>fullcon=>tanh=>fullcon=>tanhinput=>conv=>tanh=>maxpooling=>conv=>tanh=>maxpooling=>fullcon=>tanh=>fullcon=>tanh

也就是“输入=>卷积=>pooling=>卷积=>pooling=>全连接=>全链接=>输出“这样的结构。注意,我们将非线性变换也抽象出一层并作为输出,此处我们利用了tanh变换。

非线性变换层中的backprop算法

与上一节的推导不同,我们的输出层是单独分离的一层tanhtanh变换,那么tanhtanh层第jj个神经元的输入是从FullCon层传来的输出sjsj,输出则是tanh(sj)tanh(sj),那么输出与真实值产生的errorerror: 

errorj=(tanh(sLj)yj)2errorj=(tanh(sjL)−yj)2

当执行backprop的时候,第 jj个神经元将接收 δLjδjL为:  
δLj=2(tanh(sLj)yj)δjL=2∗(tanh(sjL)−yj)

然后向后传播乘上相应的导数:  
δL1j=δLjtanh(sLj)δjL−1=δjL∗tanh′(sjL)

对比一下上一节我们的推导:  
δLj=2(tanh(sLj)yj)tanh(sLj)δjL=2∗(tanh(sjL)−yj)∗tanh′(sjL)

我们很容易观察到抽象出一层的结果是将 2(tanh(sLj)yj)2∗(tanh(sjL)−yj)tanh(sLj)tanh′(sjL)两部分乘积给分离开来了。这样将变换单独抽出一层的好处是,当你不想用 tanhtanh变换而是想用 sigmoidsigmoid或者 LeRULeRU变换时,只需简单替换变换层即可,而不用改动其它层的代码。总而言之,因为这一层没有参数需要学习,所以非线性变换层的backprop过程就是将下一层传来的 δδ不管三七二十一,乘上相应变换的导数,然后直接传播到上一层去。

Fullcon层的backprop算法

在上一节中,我们已经推导过,对于一个全连接网络,他的权重导数为:

wlij=δljx(l1)i∇wijl=δjl∗xi(l−1)

δδ可以通过chain rule得出

δlj=kδl+1kwl+1jkδjl=∑kδkl+1∗wjkl+1

所以FullCon层的各个权重的梯度我们可以不费力气求出。值得注意的是,上述推导δljδjl的时候,同样因为tanhtanh变换被单独抽出一层,从而少了tanh(slj)tanh′(sjl)。我们知道网络的各个参数的梯度,均由δδ相应可以推导得出,接下来我们将δδ传播到上一层,也就是Pooling层,并试图求解其中参数的梯度。

Max Pooling中的backprop算法

max pooling层的backprop算是比较简单了,因为max-pooling的过程并没有参数要学习,它只需将下一层传来的δδ传播到上一层就好。我们知道max-pooling的上一层就是卷积层,但是它们的形状却是不相同的,不能一对一地传播。那么该如何传播δδ呢?此时我们需要一个upsample的过程,也就是将δδ强行变成卷积层的形状。我们知道,maxpooling层的输出是上一层中poolsize区域中最大的那个值组成的,而其他都直接丢弃,那么传播δδ的时候,最大那个值直接将δδ传回去,其他的直接补0。所以我们在进行pooling的过程时,有必要记录此时取得最大值的坐标,方便upsample的过程。 
记录最大值的下标,我们只需在pooling代码中插入以下代码:

def max_pool(self,input,dIdx,hIdx,wIdx):
    w = wIdx * self.stride
    h = hIdx * self.stride
    maxVal = -999999999
    for i in range(self.kernelSize):
       for j in range(self.kernelSize):
          if maxVal < input[w+i,h+j,dIdx]:
             maxVal = input[w+i,h+j,dIdx]
             self.poolIdx2MaxIdx[(wIdx,hIdx,dIdx)] = (w+i,h+j,dIdx)
    return maxVal
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
self.poolIdx2MaxIdx[(wIdx,hIdx,dIdx)] = (w+i,h+j,dIdx)
  • 1
  • 2

的作用就是记录最大值是来源于上一层输入的哪一个坐标,那么在进行backprop的时候,我们只需将δδ传会这些坐标,其他位置补0即可,具体代码如下:

def back_prop(self):
        self.preLayer.delta = np.zeros(self.preLayer.output.shape)
        for key in self.poolIdx2MaxIdx.keys():
            d = self.delta[key]
            k = self.poolIdx2MaxIdx[key]
            self.preLayer.delta[k] = d
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们首先初始化上一层的delta并设置初始值为0,然后将该层的delta传回maxpooling时记录的位置。

卷积层的Backprop算法

到了高潮中的高潮了,读者请注意,休息时间到,请自行休息5分钟。 
休息完毕,接着我们的学习。在知道如何学习卷积层地参数之前,我们必须先知道要学习怎样的参数,也就是卷积层的参数是怎么确定的。

卷积层的参数确定

首先,卷积网络有什么好处呢?上一节中我们已经介绍了前馈网络,而且它可以在Mnist上得到不错的结果,那么我们发明卷积网络到底有什么好处呢? 
我们回顾一下历史,卷积网络其实也并非凭空捏造出来的,和上一节介绍神经网络的时候一样,又是生物学家的研究给来的灵感,一个叫Hubel的生物学家,在研究猫的视觉的时候,发现了一个receptive field这样的概念。也就是生物大脑在处理视觉的时候,是由很多receptive field在进行处理加工的,然后再传播开去。而大致过程就是我们的利用核进行卷积的过程类似,才因此发明了卷积神经网络。 
机器学习中,一个最大的敌人就是Overfitting,我们上一节所介绍的网络,对于高清无码的大图片,需要学习的参数太多,很容易就导致overfitting,举个例子,我们来算一笔账:对于一张100x100的图片,接入一个拥有100个神经元的layer,再输出到一个有10个神经元的output,那么,其需要学习的参数有: 

(10000+1)100+(100+1)10=1001110(10000+1)∗100+(100+1)∗10=1001110

一百多万个参数,这还不是高清无码的图片,这么多参数,使得网络非常容易出现overfitting,至于原因,讲清楚超出了本节的范围,读者可以自行上网查找资料,或者上上Ng的cs229的课程,自然明白。  
那么卷积网络需要学习多少参数呢?  
首先,我们利用核进行卷积操作,每个核对于输入都不是全连接的,而是局部连接,也就是每个核都只是和输入的一部分连接(因为核通常都是比输入小很多),我们来算一下账,还是用上面的例子,我们的选一个核,大小为5x5,步长为1,经过卷积后,输入层和卷积层之间需要学习的参数为:  
55[(1005+1)(1005+1)+1]=2304255∗5∗[(100−5+1)∗(100−5+1)+1]=230425

要学习的参数还是太多,但是计算机学者总会有办法的,他们通过提出一个假设,然后提出weight-sharing这种办法。也就是对于locally connect的核,限制他们的边权重必须一样,以下是这种假设的英文原文:

If detecting a horizontal edge is important at some location in the image, it should intuitively be useful at some other location as well due to the translationally-invariant structure of images。

且看下图,来自Hinton老爷子在Coursera课程上的截图: 
weight sharing example

图中,输入是一张图片,而卷积核有3个,每个卷积核通过局部连接9个输入,此时需要学习的参数有Nx9个(N是卷积特征的个数),但是通过weight-sharing这个条件,图中红色边的权重都必须是一样的,蓝色边也是如此,所以需要学习的参数就变为只是卷积核的大小,也就是9个而已。

有了这个假设,输入层到卷积层之间需要学习的参数变为: 
5x5+1 = 26 (1代表bias项,上一节已经讲过) 
此时我们再加多几个核,就是用10个核去卷积图片,输入和卷积之间需要学习的参数为: 

(55+1)10=260(5∗5+1)∗10=260

经过卷积后,得到的卷积特征有96x96x10。此时我们再进行一个2x2,步长为2的maxpooling操作,特征就变为:48x48x10,再经过16个5x5的卷积核步伐为1的卷积层,此时需要学习的参数为:  
(5510+1)16=4016(5∗5∗10+1)∗16=4016

此时输出特征为:44x44x16,再经过pooling,输出特征变为:22x22x16  
此时再连接到有100个神经元的隐藏层,需要学习的参数为:  
(222216+1)100774500(22∗22∗16+1)∗100=774500

加上输出层和隐藏层之间的参数:  
(1001)101010(100+1)∗10=1010

所以,总体的学习参数为:  
774500+1010+4016+260=7797861001110774500+1010+4016+260=779786≪1001110

这就是卷积网络自身自带的regularization的作用。

学习卷积层参数

对于卷积层中的任意一个权重

wli,j,k,ni[0,width)j[0,height)k[0,depth)n[0,kernelnum)wi,j,k,nli∈[0,width)j∈[0,height)k∈[0,depth)n∈[0,kernelnum)

注意 (width,height,depth,kernelnum)(width,height,depth,kernelnum)正是我们卷积权重的shape。

我们应该如何求得wli,j,k,n∇wi,j,k,nl? 
回忆一下,对于常规的多层前馈网络,我们是根据以下式子求出权重梯度的:

wlij=δljx(l1)i∇wijl=δjl∗xi(l−1)

我们知道,梯度应该等于δδ乘上它当前层与δδ对应的输入,读者请拿起笔,画画对于二维的卷积网络,对于核里面一个权重,当前层中输入有多少个地方和该权重相乘过,如果你照做了,下面结论就不会难得出:

wli,j,k,n=abδla,b,k,ninput[i+astride,j+bstride,k]∇wi,j,k,nl=∑a∑bδa,b,k,nl∗input[i+a∗stride,j+b∗stride,k]

代码实现上述:

 def compute_single_w_grad(self,wIdx,hIdx,dIdx,num):
     (w,h,_) = self.delta.shape
      data = self.get_padding_input()
      d_w = 0.0
      for i in range(w):
         for j in range(h):
            d_w += self.delta[i,j,num]* *data[wIdx+i*self.stride,hIdx+j*self.stride,dIdx]
      return d_w
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

好了,解决了如何求取卷积层权重的梯度,接下来我们要推导如何将卷积层的δδ传播给上一层。 
首先我们还是回忆一下,普通前馈网络中,δδ是如何求得的,看回上一篇,我们可以知道(因为我们单独将tanh抽出一层,所以相比上一节的介绍,会少了一个tanh(slj)tanh′(sjl)):

δlj=kwl+1jkδl+1kδjl=∑kwjkl+1∗δkl+1

也就是,求取δjδj可以分为两部分,一部分是求取下一层δl+1kδkl+1和所在那一层的权重wl+1jkwjkl+1的点积。那么在卷积网络中,对于一个δli,j,kδi,j,kl,假设第l+1l+1层中,核的个数为KKl+1l+1层中权重的宽为WW,高为HH,而卷积步长为ss,那么我们可以以如下式子求出:

δli,j,k=dKaWbHwl+1a,b,k,dδl+1[(ia)/s,(jb)/s,d]δi,j,kl=∑dK∑aW∑bHwa,b,k,dl+1∗δl+1[(i−a)/s,(j−b)/s,d]

其中限制条件有(假设δl+1δl+1的宽为WbWb,高为HbHb):

ia>0i−a>0

(ia)/s<Wb(i−a)/s<Wb

jb>0j−b>0

(jb)/s<Hb(j−b)/s<Hb

(ia)/s(i−a)/s为整数

(jb)/s(j−b)/s为整数

利用代码实现上述,且看:

 def propagate_delta(self):
     delta = np.empty(self.preLayer.output.shape)
     (w,h,d) = self.preLayer.output.shape
     for k in range(d):
        for i in range(w):
           for j in range(h):
              delta[i,j,k] = self.compute_delta(i,j,k)
     return delta

 def compute_delta(self,wIdx,hIdx,dIdx):
     weighted_delta = 0.0
     for kernelIdx in range(self.depth):
         weight = self.weights[:,:,:,kernelIdx]
         weighted_delta += self.compute_weighted_delta(weight,wIdx,hIdx,dIdx,kernelIdx)
     return weighted_delta

 def compute_weighted_delta(self,w,wIdx,hIdx,dIdx,kernelIdx):
     val = 0.0
     (width,height,_) = w.shape
     (wBound,hBound,_) = self.delta.shape
     for i in range(width):
         if wIdx - i < 0:break
         if self.qualified(wIdx,i,wBound):
            for j in range(height):
                if hIdx - j < 0 :break
                if self.qualified(hIdx,j,hBound):
                   val += w[i,j,dIdx] * self.delta[(wIdx-i)/self.stride,(hIdx-j)/self.stride,kernelIdx]
     return val
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

这是本篇中最为triky的部分,需要读者静下心,用纸画出卷积层与上一层的关系,然后再慢慢推出,也花了我不少时间,期间还推错了几次,幸好后来用gradient check慢慢发现错误,并推导正确的。

卷积网络的代码设计

很不错,至此我们已经推出了卷积网络的训练过程和应用过程,虽然代码没有经过优化,但是是最原生也是最直接的推导,并没有加入太多的tricks,个人认为比较容易理解,最后,我们来讲讲卷积网络的代码实现过程,首先,在讲解原理的时候,我们对于网络结构给出了一种分层结构,分别分为卷积层,pooling层,全连接层,还有激活层。那么代码实现时,很显然可以利用面向对象机制,将所有层抽象一个基础层Layer,对于Layer的接口,应该有如下实现:

    def __init__(self,preLayer,width,height,depth):
        self.__preLayer = preLayer
        self.__width = width
        self.__height = height
        self.__depth = depth

    def forward_prop(self):
        pass

    def back_prop(self):
        pass

    def update_weights(self,eta):
        pass
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

除了必须有前向和后向两个方法,我还而外抽出了更新权重的方法,在构造的时候,必须传入该层上一层的引用,以便于backprop的操作。将激活层单独抽出,是方便于更换激活函数,使得更换的时候只需修改配置,而不用修改代码,网络最终再根据配置搭建构造任意网络模型,其用法是

     config = [
            {
                "type":LayerType.INPUT_TYPE,
                "width":28,
                "height":28,
                "depth":1
            },
            {
                "type":LayerType.CONV_TYPE,
                "kernelNum":4,
                "kernelSize":5,
                "stride":1,
                "padding":0
            },
            {
                "type":LayerType.TANH_TYPE
            },
            {
                 "type":LayerType.MAX_POOL_TYPE,
                 "kernelSize":2,
                 "stride":2
            },
            {
                "type":LayerType.CONV_TYPE,
                "kernelNum":16,
                "kernelSize":3,
                "stride":1,
                "padding":0
            },
            {
                 "type":LayerType.MAX_POOL_TYPE,
                 "kernelSize":2,
                 "stride":2
            },
            {
                "type":LayerType.FULL_CONNECT_TYPE,
                "numNeurons":20
            },
            {
                "type":LayerType.TANH_TYPE
            },

            {
                "type":LayerType.FULL_CONNECT_TYPE,
                "numNeurons":10
            },
            {
                "type":LayerType.TANH_TYPE
            }
        ]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

这样的可插拔方式使得变换网络非常自由,可以轻松更替激活层,调整参数。当然网络的卷积实现和backprop实现都是极其Naive的,导致训练起来非常慢,基本上只能作为toy实现,当然本文的目的只是为了带领初学者认识卷积网络,所以这样的toy实现也足够了。对于那些不满足的读者,请参考论文《High Performance Convolutional Neural Networks for Document Processing》。里面有介绍如何将卷积过程化为矩阵相乘,进而可以利用BLAS或者GPU进行高效实现。

当然实现一个toy卷积网络也并非轻松,你要实现好一个准确的gradient check程序,对于每一层的实现,先用人为数据进行测试,再进行gradient check,对于tanh变换,analytic gradient的数值和 numerical gradient的数值的相对误差最好在1-e6左右,当你将所有层都通过gradient check之后,再在一份小的数据上进行训练,人为使网络overfitting,如果会overfitting,恭喜你,你的网络实现大概是没什么问题了。每一个训练pass,都可以打印相应的cost,进而进行分析。

我利用实现好的卷积网络,取Mninst的训练数据1000份进行训练,取测试数据2000份进行测试,训练次数相同,结果都表明,普通前馈网络很容易就overfitting,而卷积网络则不会,并且测试结果都是卷积网络略胜一筹,当然卷积网络的训练时间是普通网络训练时间的上百倍。

当实现完卷积网络后,我还是挺高兴的,从头到尾,翻阅资料,理解原理并实现,排除bug,过程艰辛,比如当网络gradient check有误差时,我都很难确定是gradient check的程序出了问题还是网络实现出了问题,怪只怪工程实现能力还比较弱,总之,理解实现卷积网络,又为我揭开计算机科学的一个面纱,值得庆祝。

完整代码请戳:https://gitee.com/Carl-Xie/NN-Toys/blob/master/ConvNetwork.py

参考引用

cs231n:http://cs231n.github.io/ 
ufldl:http://ufldl.stanford.edu/ 
网络书籍:http://neuralnetworksanddeeplearning.com/ 
论文:http://cogprints.org/5869/1/cnn_tutorial.pdf 
外国blog: 
http://deeplearning.net/tutorial/lenet.html 
http://andrew.gibiansky.com/blog/machine-learning/convolutional-neural-networks/

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值