caffe(汇总)

 深度卷积网络

 

涉及问题:

1.每个图如何卷积:

  1)一个图如何变成几个?

  2)卷积核如何选择?

2.节点之间如何连接?

3.S2-C3如何进行分配?

4.16-120全连接如何连接?

5.最后output输出什么形式?

①各个层解释:

   我们先要明确一点:每个层有多个Feature Map,每个Feature Map通过一种卷积滤波器提取输入的一种特征,然后每个Feature Map有多个神经元。

   C1层是一个卷积层(为什么是卷积?卷积运算一个重要的特点就是,通过卷积运算,可以使原信号特征增强,并且降低噪音),由6个特征图Feature Map构成。特征图中每个神经元与输入中5*5的邻域相连。特征图的大小为28*28,这样能防止输入的连接掉到边界之外(是为了BP反馈时的计算,不致梯度损失,个人见解)。C1有156个可训练参数(每个滤波器5*5=25个unit参数和一个bias参数,一共6个滤波器,共(5*5+1)*6=156个参数),共156*(28*28)=122,304个连接。

   S2层是一个下采样层(为什么是下采样?利用图像局部相关性的原理,对图像进行子抽样,可以减少数据处理量同时保留有用信息),有6个14*14的特征图。特征图中的每个单元与C1中相对应特征图的2*2邻域相连接。S2层每个单元的4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果通过sigmoid函数计算。可训练系数和偏置控制着sigmoid函数的非线性程度。如果系数比较小,那么运算近似于线性运算,亚采样相当于模糊图像。如果系数比较大,根据偏置的大小亚采样可以被看成是有噪声的“或”运算或者有噪声的“与”运算。每个单元的2*2感受野并不重叠,因此S2中每个特征图的大小是C1中特征图大小的1/4(行和列各1/2)。S2层有12个可训练参数和5880个连接。

图:卷积和子采样过程:卷积过程包括:用一个可训练的滤波器fx去卷积一个输入的图像(第一阶段是输入的图像,后面的阶段就是卷积特征map了),然后加一个偏置bx,得到卷积层Cx。子采样过程包括:每邻域四个像素求和变为一个像素,然后通过标量Wx+1加权,再增加偏置bx+1,然后通过一个sigmoid激活函数,产生一个大概缩小四倍的特征映射图Sx+1

   所以从一个平面到下一个平面的映射可以看作是作卷积运算,S-层可看作是模糊滤波器,起到二次特征提取的作用。隐层与隐层之间空间分辨率递减,而每层所含的平面数递增,这样可用于检测更多的特征信息。

   C3层也是一个卷积层,它同样通过5x5的卷积核去卷积层S2,然后得到的特征map就只有10x10个神经元,但是它有16种不同的卷积核,所以就存在16个特征map了。这里需要注意的一点是:C3中的每个特征map是连接到S2中的所有6个或者几个特征map的,表示本层的特征map是上一层提取到的特征map的不同组合(这个做法也并不是唯一的)。(看到没有,这里是组合,就像之前聊到的人的视觉系统一样,底层的结构构成上层更抽象的结构,例如边缘构成形状或者目标的部分)。

   刚才说C3中每个特征图由S2中所有6个或者几个特征map组合而成。为什么不把S2中的每个特征图连接到每个C3的特征图呢?原因有2点。第一,不完全的连接机制将连接的数量保持在合理的范围内。第二,也是最重要的,其破坏了网络的对称性。由于不同的特征图有不同的输入,所以迫使他们抽取不同的特征(希望是互补的)。

   例如,存在的一个方式是:C3的前6个特征图以S2中3个相邻的特征图子集为输入。接下来6个特征图以S2中4个相邻特征图子集为输入。然后的3个以不相邻的4个特征图子集为输入。最后一个将S2中所有特征图为输入。这样C3层有1516个可训练参数和151600个连接。

   S4层是一个下采样层,由16个5*5大小的特征图构成。特征图中的每个单元与C3中相应特征图的2*2邻域相连接,跟C1和S2之间的连接一样。S4层有32个可训练参数(每个特征图1个因子和一个偏置)和2000个连接。

   C5层是一个卷积层,有120个特征图。每个单元与S4层的全部16个单元的5*5邻域相连。由于S4层特征图的大小也为5*5(同滤波器一样),故C5特征图的大小为1*1:这构成了S4和C5之间的全连接。之所以仍将C5标示为卷积层而非全相联层,是因为如果LeNet-5的输入变大,而其他的保持不变,那么此时特征图的维数就会比1*1大。C5层有48120个可训练连接。

   F6层有84个单元(之所以选这个数字的原因来自于输出层的设计),与C5层全相连。有10164个可训练参数。如同经典神经网络,F6层计算输入向量和权重向量之间的点积,再加上一个偏置。然后将其传递给sigmoid函数产生单元i的一个状态。

   最后,输出层由欧式径向基函数(Euclidean Radial Basis Function)单元组成,每类一个单元,每个有84个输入。换句话说,每个输出RBF单元计算输入向量和参数向量之间的欧式距离。输入离参数向量越远,RBF输出的越大。一个RBF输出可以被理解为衡量输入模式和与RBF相关联类的一个模型的匹配程度的惩罚项。用概率术语来说,RBF输出可以被理解为F6层配置空间的高斯分布的负log-likelihood。给定一个输入模式,损失函数应能使得F6的配置与RBF参数向量(即模式的期望分类)足够接近。这些单元的参数是人工选取并保持固定的(至少初始时候如此)。这些参数向量的成分被设为-1或1。虽然这些参数可以以-1和1等概率的方式任选,或者构成一个纠错码,但是被设计成一个相应字符类的7*12大小(即84)的格式化图片。这种表示对识别单独的数字不是很有用,但是对识别可打印ASCII集中的字符串很有用。

   使用这种分布编码而非更常用的“1 of N”编码用于产生输出的另一个原因是,当类别比较大的时候,非分布编码的效果比较差。原因是大多数时间非分布编码的输出必须为0。这使得用sigmoid单元很难实现。另一个原因是分类器不仅用于识别字母,也用于拒绝非字母。使用分布编码的RBF更适合该目标。因为与sigmoid不同,他们在输入空间的较好限制的区域内兴奋,而非典型模式更容易落到外边。

   RBF参数向量起着F6层目标向量的角色。需要指出这些向量的成分是+1或-1,这正好在F6 sigmoid的范围内,因此可以防止sigmoid函数饱和。实际上,+1和-1是sigmoid函数的最大弯曲的点处。这使得F6单元运行在最大非线性范围内。必须避免sigmoid函数的饱和,因为这将会导致损失函数较慢的收敛和病态问题。

②问题讲解

1个问题:

(1)输入-C1

用6个5*5大小的patch(即权值,训练得到,随机初始化,在训练过程中调节)对32*32图片进行卷积,得到6个特征图。

(2)S2-C3

C3那16张10*10大小的特征图是怎么来?

将S2的特征图用1个输入层为150(=5*5*6,不是5*5)个节点,输出层为16个节点的网络进行convolution。

该第3号特征图的值(假设为H3)是怎么得到的呢

   首先我们把网络150-16(以后这样表示,表面输入层节点为150,隐含层节点为16)中输入的150个节点分成6个部分,每个部分为连续的25个节点。取出倒数第3个部分的节点(为25个),且同时是与隐含层16个节点中的第4(因为对应的是3号,从0开始计数的)个相连的那25个值,reshape为5*5大小,用这个5*5大小的特征patch去convolution S2网络中的倒数第3个特征图,假设得到的结果特征图为h1。

   同理,取出网络150-16中输入的倒数第2个部分的节点(为25个),且同时是与隐含层16个节点中的第5个相连的那25个值,reshape为5*5大小,用这个5*5大小的特征patch去convolution S2网络中的倒数第2个特征图,假设得到的结果特征图为h2。

   最后,取出网络150-16中输入的最后1个部分的节点(为25个),且同时是与隐含层16个节点中的第5个相连的那25个值,reshape为5*5大小,用这个5*5大小的特征patch去convolution S2网络中的最后1个特征图,假设得到的结果特征图为h3。

   最后将h1,h2,h3这3个矩阵相加得到新矩阵h,并且对h中每个元素加上一个偏移量b,且通过sigmoid的激发函数,即可得到我们要的特征图H3了。

第二个问题:

上图S2中为什么是150个节点?(涉及到权值共享参数减少

CNN一个牛逼的地方就在于通过感受野和权值共享减少了神经网络需要训练的参数的个数。

下图左:如果我们有1000x1000像素的图像,有1百万个隐层神经元,那么他们全连接的话(每个隐层神经元都连接图像的每一个像素点),就有1000x1000x1000000=10^12个连接,也就是10^12个权值参数。然而图像的空间联系是局部的,就像人是通过一个局部的感受野去感受外界图像一样,每一个神经元都不需要对全局图像做感受,每个神经元只感受局部的图像区域,然后在更高层,将这些感受不同局部的神经元综合起来就可以得到全局的信息了。这样,我们就可以减少连接的数目,也就是减少神经网络需要训练的权值参数的个数了。如下图右:假如局部感受野是10x10,隐层每个感受野只需要和这10x10的局部图像相连接,所以1百万个隐层神经元就只有一亿个连接,即10^8个参数。比原来减少了四个0(数量级),这样训练起来就没那么费力了,但还是感觉很多的啊,那还有啥办法没?

  

   我们知道,隐含层的每一个神经元都连接10x10个图像区域,也就是说每一个神经元存在10x10=100个连接权值参数。那如果我们每个神经元这100个参数是相同的呢?也就是说每个神经元用的是同一个卷积核去卷积图像。这样我们就只有多少个参数??只有100个参数啊!!!亲!不管你隐层的神经元个数有多少,两层间的连接我只有100个参数啊!亲!这就是权值共享啊!亲!这就是卷积神经网络的主打卖点啊!亲!(有点烦了,呵呵)也许你会问,这样做靠谱吗?为什么可行呢?这个……共同学习。

   好了,你就会想,这样提取特征也忒不靠谱吧,这样你只提取了一种特征啊?对了,真聪明,我们需要提取多种特征对不?假如一种滤波器,也就是一种卷积核就是提出图像的一种特征,例如某个方向的边缘。那么我们需要提取不同的特征,怎么办,加多几种滤波器不就行了吗?对了。所以假设我们加到100种滤波器,每种滤波器的参数不一样,表示它提出输入图像的不同特征,例如不同的边缘。这样每种滤波器去卷积图像就得到对图像的不同特征的放映,我们称之为Feature Map。所以100种卷积核就有100个Feature Map。这100个Feature Map就组成了一层神经元。到这个时候明了了吧。我们这一层有多少个参数了?100种卷积核x每种卷积核共享100个参数=100x100=10K,也就是1万个参数。才1万个参数啊!亲!(又来了,受不了了!)见下图右:不同的颜色表达不同的滤波器。

  

   嘿哟,遗漏一个问题了。刚才说隐层的参数个数和隐层的神经元个数无关,只和滤波器的大小和滤波器种类的多少有关。那么隐层的神经元个数怎么确定呢?它和原图像,也就是输入的大小(神经元个数)、滤波器的大小和滤波器在图像中的滑动步长都有关!例如,我的图像是1000x1000像素,而滤波器大小是10x10,假设滤波器没有重叠,也就是步长为10,这样隐层的神经元个数就是(1000x1000 )/ (10x10)=100x100个神经元了,假设步长是8,也就是卷积核会重叠两个像素,那么……我就不算了,思想懂了就好。注意了,这只是一种滤波器,也就是一个Feature Map的神经元个数哦,如果100个Feature Map就是100倍了。由此可见,图像越大,神经元个数和需要训练的权值参数个数的贫富差距就越大。

所以这里可以知道刚刚14*14的图像计算它的节点,按步长为3计算,则一幅图可得5*5个神经元个数,乘以6得到150个神经元个数。

需要注意的一点是,上面的讨论都没有考虑每个神经元的偏置部分。所以权值个数需要加1 。这个也是同一种滤波器共享的。

总之,卷积网络的核心思想是将:局部感受野、权值共享(或者权值复制)以及时间或空间亚采样这三种结构思想结合起来获得了某种程度的位移、尺度、形变不变性。

第三个问题:

 

如果C1层减少为4个特征图,同样的S2也减少为4个特征图,与之对应的C3和S4减少为11个特征图,则C3和S2连接情况如图:

 

第四个问题:

全连接:

C5C4层进行卷积操作,采用全连接方式,即每个C5中的卷积核均在S4所有16个特征图上进行卷积操作。

 

第五个问题:

采用one-of-c的方式,在输出结果的1*10的向量中最大分量对应位置极为网络输出的分类结果。对于训练集的标签也采用同样的方式编码,例如1000000000,则表明是数字0的分类。





简化的LeNet-5系统

简化的LeNet-5系统把下采样层和卷积层结合起来,避免了下采样层过多的参数学习过程,同样保留了对图像位移,扭曲的鲁棒性。其网络结构图如下所示:    
                         

         简化的LeNet-5系统包括输入层的话,只有5层结构,而原始LeNet-5结构不包含输入层就已经是7层网络结构了。它实现下采样非常简单,直接取其第一个位置节点上的值可以了。

 1、 输入层。MNIST手写数字图像的大小是28x28的,这里通过补零扩展为29x29的大小。这样输入层神经节点个数为29x29等于841个。

 2、第一层。由6张不同的特征映射图组成。每一张特征图的大小是13x13. 注意,由于卷积窗大小为5x5,加上下采样过程,易得其大小为13x13. 所以,    第二层共有6x13x13等于1014个神经元节点。每一张特征图加上偏置共有5x5+1等于26个权值需要训练,总共有6x26等于156个不同的权值。即总共有1014x156等于26364条连接线。

 3、 第二层。由50张不同的特征映射图组成。每一张特征图的大小是5x5. 注意,由于卷积窗大小为5x5,加上下采样过程,易得其大小为5x5. 由于上一   层是由多个特征映射图组成,那么,如何组合这些特征形成下一层特征映射图的节点呢?简化的LeNet-5系统采用全部所有上层特征图的组合。也就是原始LeNet-5 特征映射组合图的最后一列的组合方式。因此,总共有5x5x50等于1250 个神经元节点,有(5x5+1)x6x50等于7800 个权值,总共有1250x26等于32500条连接线。

4、第三层。 这一层是一个一维线性排布的网络节点,与前一层是全连接的网络,其节点个数设为为100,故而总共有100x(1250+1)等于125100个不同的权值,同时,也有相同数目的连接线。

5、第四层。这一层是网络的输出层,如果要识别0-9数字的话,就是10个节点。该层与前一层是全连接的,故而,总共有10x(100+1)等于1010个权值,有相同数目的连接线。

常见遇到的问题:

(1)梯度消失

我在实现过程中犯的第一个错误是没有循序渐进。仗着自己写过一些神经网络的代码以为手到擒来,直接按照LeNet-5的结构写,过于复杂的结构给测试和调试都带来了很大的麻烦,可谓不作死就不会死。

简单分析一下LeNet-5的结构:第一层8个5*5的卷积核,第二层分别作2*2pooling,第三层16个5*5的卷积核,第四层2*2pooling,随后是三个节点数分别为120、84、10的全连接层(做手写数字识别)。这时的参数个数为卷积核及其偏置+pooling层放缩系数及偏置+全连接层权重及偏置,全链接层的参数个数比前四层的参数个数多了两个数量级

过多的参数个数会极大地提升运算时间,降低测试的效率;同时过多的层数伴随着严重的梯度消失问题(vanishing gradient),严重影响训练效果。在上面提到的网络结构下,我发现传到第一层的梯度已经小于1e-10,只有后面的全连接层在变化,卷积层几乎训不动了。

我对付这个问题的手段包括:

  1. 减少层数
  2. 增大学习率(learning rate)
  3. 用ReLU代替sigmoid

其中前两种虽然也有效果,只能算是权宜之计,第三种才算是正经的解决了这个问题。在采用ReLU作为激活函数之后,传到输入层的梯度已经达到1e-5~1e-4这个量级。我用MNIST数据集里的5000个样本训练,1000个样本测试,发现测试结果差不多,收敛速度快了2倍左右。据@kevin好好学习的说法,ReLU还使网络的激活具有了稀疏的特性,我对这方面就没研究了。

(2)非线性映射的位置:

在前面提到的Bouvrie的文章中,非线性映射放在卷积层还是pooling层后面都可以,微博上几位大牛也是这个观点。奇怪的是,我在一开始实现的时候把sigmoid放在了pooling层后面,虽然很渣但至少能跑出结果。后来我把sigmoid放在卷积层后面就训不动了,感觉跟梯度消失的时候很像。我猜测也许是卷积层和pooling层激活程度不同导致传回去的梯度大小不一样。

(3)权重衰减(weight decay)

因为我用的数据很少(数据多了跑起来太慢),我担心会出现过拟合,所以在cost function里加了一项正则项。但结果是不加正则项训练结果很好,一加就出现严重的under fitting,不管怎么调参数都没用。原来一直听说CNN的权重共享就相当于自带某种正则化,现在看起来确实是这样。

(4)随机梯度下降(SGD)的参数选择

最主要的就一个参数,minibatch的大小。在Deep Learning Toolbox的demo里,这个值取的是50。但是我的实验中,似乎minibatch取1的时候收敛最快。

我的感觉:训练样本顺序随机的话,每次产生的梯度也是随机的,那么50个样本平均一下梯度就很小了,也许这样比较稳健,但收敛会比较慢;相反,每个样本更新一次梯度就大得多,不过也许会比较盲目。

另外还有一个参数是learning rate。在实验中,增大minibatch会出现训不动的情况,这时适当增大learning rate能够让training cost继续下降。

另外Yann LeCun似乎说过每一层应该采取不同的learning rate,不过我没试。

(5)参数的随机初始化

我试过将参数初始化到(0, 1)区间和(-1, 1)区间,感觉似乎差别不大?

(6)增加全连接层数后的性能

关于这点我不是很确定,但似乎除了增加训练时间外没什么实际作用…


二、网络的改进

(1)自动学习组合系数

为了打破对称性,从第一个卷积层到第二个卷积层并不是全链接,例如第二层第一个卷积核只卷积第一层的第123个feature map,第二层第二个卷积核只卷积第一层的第345个feature map。在LeNet里这个组合是人为规定的,Bouvrie的文章中提出这个组合系数也是可以学出来的。但是我的实验里人为规定和自动学习的效果好像差别不大,不知道更复杂的数据集上会怎么样。

(2)ReLU的位置

如果网络的最后一层是softmax分类器的话似乎其前一层就不能用ReLU,因为ReLU输出可能相差很大(比如0和几十),这时再经过softmax就会出现一个节点为1其它全0的情况。softmax的cost function里包含一项log(y),如果y正好是0就没法算了。所以我在倒数第二层还是采用sigmoid。

(3)其他高级玩法

本来还打算玩一些其他更有趣的东西,比如dropout、maxout、max pooling等等。


Reference Link:
http://zhangliliang.com/2015/05/27/about-caffe-code-softmax-loss-layer/

关于softmax回归

看过最清晰的关于softmax回归的文档来源自UFLDL,简单摘录如下。
softmax用于多分类问题,比如0-9的数字识别,共有10个输出,而且这10个输出的概率和加起来应该为1,所以可以用一个softmax操作归一化这10个输出。进一步一般化,假如共有k个输出,softmax的假设可以形式化表示为:

然后给这个假设定义一个loss function,就是softmax回归的loss function咯,形式化如下:

也很直观,对于某个样本i,他对应的gt label是j,那么对于loss function来说,显然只需要关心第k路是否是一个概率很大的值,所以就用一个l{·}的示性函数来表示只关心第 y(i) y(i)路(即label对应的那一路),其他路都忽略为0。然后log的部分其实就是第k路的概率值取log。最后需要注意到前面还有一个负号。
所以总的来说,这个loss function的意思是说,对于某个样本,我只看他gt对应的那个路子输出的概率,然后取一个-log从最大化概率变成最小化能量。
然后softmax可以求梯度,梯度的公式是:

然后在实际应用中,一般还是要加上一个正则项,或者在UFLDL教程中被称为权重衰减项,于是loss function和回传梯度都多出了一项,变成了:


然后softmax回归就介绍完了,感觉不懂的话具体还是看UFLDL的教程比较好。

Caffe中的实现

注意这里贴的代码是基于笔者所使用的caffe版本的,大概是2015年初的吧,跟目前的最新caffe版本可能有所出入。
在实现细节上,train时候在最后接上SoftmaxWithLossLayer,test的时候换成SoftmaxLayer即可。这里可以看loss_layer.hpp的注释:

1
2
3
4
* This layer should be preferred over separate
* SoftmaxLayer + MultinomialLogisticLossLayer
* as its gradient computation is more numerically stable.
* At test time, this layer can be replaced simply by a SoftmaxLayer.

先看softmax_layer.cpp,由于只会用到他的forward,所以只看forward就好了。代码如下:

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
template <typename Dtype>
void SoftmaxLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
    vector<Blob<Dtype>*>* top) {
  const Dtype* bottom_data = bottom[0]->cpu_data();
  Dtype* top_data = (*top)[0]->mutable_cpu_data();
  Dtype* scale_data = scale_.mutable_cpu_data();
  int num = bottom[0]->num();
  int channels = bottom[0]->channels();
  int dim = bottom[0]->count() / bottom[0]->num();
  int spatial_dim = bottom[0]->height() * bottom[0]->width();
  caffe_copy(bottom[0]->count(), bottom_data, top_data);
  // We need to subtract the max to avoid numerical issues, compute the exp,
  // and then normalize.
  for (int i = 0; i < num; ++i) {
    // initialize scale_data to the first plane
    caffe_copy(spatial_dim, bottom_data + i * dim, scale_data);
    for (int j = 0; j < channels; j++) {
      for (int k = 0; k < spatial_dim; k++) {
        scale_data[k] = std::max(scale_data[k],
            bottom_data[i * dim + j * spatial_dim + k]);
      }
    }
    // subtraction
    caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, channels, spatial_dim,
        1, -1., sum_multiplier_.cpu_data(), scale_data, 1., top_data + i * dim);
    // exponentiation
    caffe_exp<Dtype>(dim, top_data + i * dim, top_data + i * dim);
    // sum after exp
    caffe_cpu_gemv<Dtype>(CblasTrans, channels, spatial_dim, 1.,
        top_data + i * dim, sum_multiplier_.cpu_data(), 0., scale_data);
    // division
    for (int j = 0; j < channels; j++) {
      caffe_div(spatial_dim, top_data + (*top)[0]->offset(i, j), scale_data,
          top_data + (*top)[0]->offset(i, j));
    }
  }
}

可以看出基本就是softmax的假设时候的实现公式,即这条。

不同之处是先求取max然后所有值先减去了这个max,目的作者也给了注释是数值问题,毕竟之后是要接上e为底的指数运算的,所以值不可以太大,这个操作相当合理。

然后就到了softmax_loss_layer.cpp了,总共代码不超100行,就全贴在下面了:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <algorithm>
#include <cfloat>
#include <vector>

#include "caffe/layer.hpp"
#include "caffe/util/math_functions.hpp"
#include "caffe/vision_layers.hpp"

namespace caffe {

template <typename Dtype>
void SoftmaxWithLossLayer<Dtype>::LayerSetUp(
    const vector<Blob<Dtype>*>& bottom, vector<Blob<Dtype>*>* top) {
  LossLayer<Dtype>::LayerSetUp(bottom, top);
  softmax_bottom_vec_.clear();
  softmax_bottom_vec_.push_back(bottom[0]);
  softmax_top_vec_.clear();
  softmax_top_vec_.push_back(&prob_);
  softmax_layer_->SetUp(softmax_bottom_vec_, &softmax_top_vec_);
}

template <typename Dtype>
void SoftmaxWithLossLayer<Dtype>::Reshape(
    const vector<Blob<Dtype>*>& bottom, vector<Blob<Dtype>*>* top) {
  LossLayer<Dtype>::Reshape(bottom, top);
  softmax_layer_->Reshape(softmax_bottom_vec_, &softmax_top_vec_);
  if (top->size() >= 2) {
    // softmax output
    (*top)[1]->ReshapeLike(*bottom[0]);
  }
}

template <typename Dtype>
void SoftmaxWithLossLayer<Dtype>::Forward_cpu(
    const vector<Blob<Dtype>*>& bottom, vector<Blob<Dtype>*>* top) {
  // The forward pass computes the softmax prob values.
  softmax_layer_->Forward(softmax_bottom_vec_, &softmax_top_vec_);
  const Dtype* prob_data = prob_.cpu_data();
  const Dtype* label = bottom[1]->cpu_data();
  int num = prob_.num();
  int dim = prob_.count() / num;
  int spatial_dim = prob_.height() * prob_.width();
  Dtype loss = 0;
  for (int i = 0; i < num; ++i) {
    for (int j = 0; j < spatial_dim; j++) {
      loss -= log(std::max(prob_data[i * dim +
          static_cast<int>(label[i * spatial_dim + j]) * spatial_dim + j],
                           Dtype(FLT_MIN)));
    }
  }
  (*top)[0]->mutable_cpu_data()[0] = loss / num / spatial_dim;
  if (top->size() == 2) {
    (*top)[1]->ShareData(prob_);
  }
}

template <typename Dtype>
void SoftmaxWithLossLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
    const vector<bool>& propagate_down,
    vector<Blob<Dtype>*>* bottom) {
  if (propagate_down[1]) {
    LOG(FATAL) << this->type_name()
               << " Layer cannot backpropagate to label inputs.";
  }
  if (propagate_down[0]) {
    Dtype* bottom_diff = (*bottom)[0]->mutable_cpu_diff();
    const Dtype* prob_data = prob_.cpu_data();
    caffe_copy(prob_.count(), prob_data, bottom_diff);
    const Dtype* label = (*bottom)[1]->cpu_data();
    int num = prob_.num();
    int dim = prob_.count() / num;
    int spatial_dim = prob_.height() * prob_.width();
    for (int i = 0; i < num; ++i) {
      for (int j = 0; j < spatial_dim; ++j) {
        bottom_diff[i * dim + static_cast<int>(label[i * spatial_dim + j])
            * spatial_dim + j] -= 1;
      }
    }
    // Scale gradient
    const Dtype loss_weight = top[0]->cpu_diff()[0];
    caffe_scal(prob_.count(), loss_weight / num / spatial_dim, bottom_diff);
  }
}


#ifdef CPU_ONLY
STUB_GPU(SoftmaxWithLossLayer);
#endif

INSTANTIATE_CLASS(SoftmaxWithLossLayer);


}  // namespace caffe

其实这个函数挺好懂的,总结起来大致是:

  • 首先这里直接内置了一个SoftmaxLayer,利用它直接得到概率值prob_
  • 之后的forward和backward都很直观了,就是没有正则项的loss function和梯度的实现方式。(这里为啥没有考虑正则项,是因为正则项的代码不是写在这这里的,而是在更新梯度时候再一起考虑的,具体可以看layer的更新代码,会发现考虑了一个叫decay的东西)
  • 这里有了spatial_dim的概念后,就可以直接支持做全图的softmax了,具体来可以参考FCN一文中最后做20类分类的概率图的那个全图softmax
  • 一开始笔者先看了卷积层的梯度传导公式,参考了这两篇:

    1. http://ufldl.stanford.edu/tutorial/supervised/ConvolutionalNeuralNetwork/
    2. http://cogprints.org/5869/1/cnn_tutorial.pdf

    卷积层的参数的梯度可以这样来求:

    W(l)kJ(W,b;x,y)b(l)kJ(W,b;x,y)=i=1m(a(l)i)rot90(δ(l+1)k,2),=a,b(δ(l+1)k)a,b. ∇Wk(l)J(W,b;x,y)=∑i=1m(ai(l))∗rot90(δk(l+1),2),∇bk(l)J(W,b;x,y)=∑a,b(δk(l+1))a,b.

    看上去比全连接层复杂多了,但其实, 他们本质上基本是一样的,依然可以套回全连接层的参数求导公式:
    W(l)J(W,b;x,y)b(l)J(W,b;x,y)=δ(l+1)(a(l))T,=δ(l+1). ∇W(l)J(W,b;x,y)=δ(l+1)(a(l))T,∇b(l)J(W,b;x,y)=δ(l+1).

    只需要额外增加一步im2col。这一步的意思是将首先将整张图片按照卷积的窗口大小切好(按照stride来切,可以有重叠),然后各自拉成一列。
    为啥要怎样做,因为对于这个小窗口内拉成一列的神经元来说来说,它们跟下一层神经元就是全连接了,所以这个小窗口里面的梯度计算就可以按照全连接来计算就可以了。

    如果对照着Caffe的卷积层源码来看,就很清晰了。
    forward的代码如下,假设没有分group,这段代码的意思是对于一个大小为num_的batch里面的任意一张图片,首先通过im2col展开成多个列向量,之后直接就用wx+b的方式就能够算到输出了。

    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
    
    void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
          vector<Blob<Dtype>*>* top) {
      for (int i = 0; i < bottom.size(); ++i) {
        const Dtype* bottom_data = bottom[i]->cpu_data();
        Dtype* top_data = (*top)[i]->mutable_cpu_data();
        Dtype* col_data = col_buffer_.mutable_cpu_data();
        const Dtype* weight = this->blobs_[0]->cpu_data();
        int weight_offset = M_ * K_;  // number of filter parameters in a group
        int col_offset = K_ * N_;  // number of values in an input region / column
        int top_offset = M_ * N_;  // number of values in an output region / column
        for (int n = 0; n < num_; ++n) {
          // im2col transformation: unroll input regions for filtering
          // into column matrix for multplication.
          im2col_cpu(bottom_data + bottom[i]->offset(n), channels_, height_,
              width_, kernel_h_, kernel_w_, pad_h_, pad_w_, stride_h_, stride_w_,
              col_data);
          // Take inner products for groups.
          for (int g = 0; g < group_; ++g) {
            caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, M_, N_, K_,
              (Dtype)1., weight + weight_offset * g, col_data + col_offset * g,
              (Dtype)0., top_data + (*top)[i]->offset(n) + top_offset * g);
          }
          // Add bias.
          if (bias_term_) {
            caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, num_output_,
                N_, 1, (Dtype)1., this->blobs_[1]->cpu_data(),
                bias_multiplier_.cpu_data(),
                (Dtype)1., top_data + (*top)[i]->offset(n));
          }
        }
      }
    }
    

    所以卷积看成是多个局部的全连接。笔者记得在某个地方看到过,实现卷积的方式有两个,一个是像caffe里面的im2col,另外一个是傅里叶变换。而后者的速度比较快,那么看来facebook给出的加速代码应该是用了傅里叶变换咯:)
    言归正传,如果能够理解到im2col的作用,那么backward的代码也很容易理解了。
    对于bias,直接就是delta(可能还要乘以bias_multiplier_,这个是Caffe自己的功能,默认不开启,即bias_multiplier_=1)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // Bias gradient, if necessary.
    if (bias_term_ && this->param_propagate_down_[1]) {
      top_diff = top[i]->cpu_diff();
      for (int n = 0; n < num_; ++n) {
    	caffe_cpu_gemv<Dtype>(CblasNoTrans, num_output_, N_,
    		1., top_diff + top[0]->offset(n),
    		bias_multiplier_.cpu_data(), 1.,
    		bias_diff);
      }
    }
    

    对于weights,batch中的每张图片,首先还是用im2col展开,之后用矩阵乘法表示累加:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    // Since we saved memory in the forward pass by not storing all col
    // data, we will need to recompute them.
    im2col_cpu(bottom_data + (*bottom)[i]->offset(n), channels_, height_,
    		   width_, kernel_h_, kernel_w_, pad_h_, pad_w_,
    		   stride_h_, stride_w_, col_data);
    // gradient w.r.t. weight. Note that we will accumulate diffs.
    if (this->param_propagate_down_[0]) {
      for (int g = 0; g < group_; ++g) {
    	caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasTrans, M_, K_, N_,
    		(Dtype)1., top_diff + top[i]->offset(n) + top_offset * g,
    		col_data + col_offset * g, (Dtype)1.,
    		weight_diff + weight_offset * g);
      }
    }
    

    最后是计算需要传回前一层的delta,也是先看成多个独立的全连接,但是最后需要还原成带有空间结构的形状,需要调用逆过程col2im,其实也是一个累加的过程,让每个空间位置的delta累加起来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    // gradient w.r.t. bottom data, if necessary.
    if (propagate_down[i]) {
      if (weight == NULL) {
    	weight = this->blobs_[0]->cpu_data();
      }
      for (int g = 0; g < group_; ++g) {
    	caffe_cpu_gemm<Dtype>(CblasTrans, CblasNoTrans, K_, N_, M_,
    		(Dtype)1., weight + weight_offset * g,
    		top_diff + top[i]->offset(n) + top_offset * g,
    		(Dtype)0., col_diff + col_offset * g);
      }
      // col2im back to the data
      col2im_cpu(col_diff, channels_, height_, width_,
    	  kernel_h_, kernel_w_, pad_h_, pad_w_,
    	  stride_h_, stride_w_, bottom_diff + (*bottom)[i]->offset(n));
    }
    

    最后附带一提,在“关于卷积”那篇也提到了卷积运算是先要将kernel旋转180度之后再扫过去的。可以看出Caffe源码是没有这一步的,所以最后学出来的“kernel”实际上是应该还要旋转回来才是正确的卷积核。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值