卷积神经网络(CNN)学习笔记

时间:2022-03-03 13:54:49


从去年开始接触神经网络,觉得CNN结构很简单没什么难的,直到有一天被一位拿过信息竞赛金牌的学弟鄙视了……中期答辩之后不忙,于是我花了几天用MATLAB写了一个CNN,算是给自己一个交代。


为什么用MATLAB?

  1. 相比c/c++,MATLAB实现更方便
  2. 我没打算写又快又漂亮的产品级的代码,只是希望抠一遍算法的各种细节,所以theano等各种成熟的工具包就不考虑了

实现


这篇文章不打算讲各种基础的细节,只是总结一下我踩的各种坑……

CNN训练无非是反向传播,现成的公式很容易找到,自己花一些时间也能推导出来。我参照的是这篇文章:

@article{bouvrie2006notes,
title={Notes on convolutional neural networks},
author={Bouvrie, Jake},
year={2006}
}

这篇文章的好处在于作者在推导公式的同时顺便给出了MATLAB上的实现方法。


遇到的问题


梯度消失

我在实现过程中犯的第一个错误是没有循序渐进。仗着自己写过一些神经网络的代码以为手到擒来,直接按照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还使网络的激活具有了稀疏的特性,我对这方面就没研究了。

非线性映射的位置

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

权重衰减(weight decay)

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

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

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

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

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

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

参数的随机初始化

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

增加全连接层数后的性能

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


网络的改进


自动学习组合系数

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

ReLU的位置

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

其他高级玩法

本来还打算玩一些其他更有趣的东西,比如dropout、maxout、max pooling等等。但是时间有限,老板已经嫌我不务正业了。


总结


这次的收获:

  1. 写复杂的算法不能急于求成,要循序渐进
  2. 测试很重要。上Andrew Ng公开课时候觉得gradient check很烦,现在看来简直是神器
  3. 机器越快越好……
转自:http://blog.csdn.net/huangbo10/article/details/24941079?utm_source=tuicool&utm_medium=referral

训练深度神经网络的时候需要注意的一些小技巧

1、准备数据:务必保证有大量、高质量并且带有干净标签的数据,没有如此的数据,学习是不可能的

2、预处理:这个不多说,就是0均值和1方差化

3、minibatch:建议值128,1最好,但是效率不高,但是千万不要用过大的数值,否则很容易过拟合

4、梯度归一化:其实就是计算出来梯度之后,要除以minibatch的数量。这个不多解释

5、下面主要集中说下学习率

  • 总的来说是用一个一般的学习率开始,然后逐渐的减小它
  • 一个建议值是0.1,适用于很多NN的问题,一般倾向于小一点。
  • 一个对于调度学习率的建议:如果在验证集上性能不再增加就让学习率除以2或者5,然后继续,学习率会一直变得很小,到最后就可以停止训练了。
  • 很多人用的一个设计学习率的原则就是监测一个比率(每次更新梯度的norm除以当前weight的norm),如果这个比率在10-3附近,如果小于这个值,学习会很慢,如果大于这个值,那么学习很不稳定,由此会带来失败。

6、使用验证集,可以知道什么时候开始降低学习率,和什么时候停止训练。

7、关于对weight初始化的选择的一些建议:

  • 如果你很懒,直接用0.02*randn(num_params)来初始化,当然别的值你也可以去尝试
  • 如果上面那个不太好使,那么久依次初始化每一个weight矩阵用init_scale / sqrt(layer_width) * randn,init_scale可以被设置为0.1或者1
  • 初始化参数对结果的影响至关重要,要引起重视。
  • 在深度网络中,随机初始化权重,使用SGD的话一般处理的都不好,这是因为初始化的权重太小了。这种情况下对于浅层网络有效,但是当足够深的时候就不行了,因为weight更新的时候,是靠很多weight相乘的,越乘越小,有点类似梯度消失的意思(这句话是我加的)

8、如果训练RNN或者LSTM,务必保证gradient的norm被约束在15或者5(前提还是要先归一化gradient),这一点在RNN和LSTM中很重要。

9、检查下梯度,如果是你自己计算的梯度。

10、如果使用LSTM来解决长时依赖的问题,记得初始化bias的时候要大一点

12、尽可能想办法多的扩增训练数据,如果使用的是图像数据,不妨对图像做一点扭转啊之类的,来扩充数据训练集合。

13、使用dropout

14、评价最终结果的时候,多做几次,然后平均一下他们的结果。

via:@kevin好好学习

End.

转自:http://www.36dsj.com/archives/25608

Deep learning:四十一(Dropout简单理解)

    前言

  训练神经网络模型时,如果训练样本较少,为了防止模型过拟合,Dropout可以作为一种trikc供选择。Dropout是hintion最近2年提出的,源于其文章Improving neural networks by preventing co-adaptation of feature detectors.中文大意为:通过阻止特征检测器的共同作用来提高神经网络的性能。本篇博文就是按照这篇论文简单介绍下Dropout的思想,以及从用一个简单的例子来说明该如何使用dropout。

 

  基础知识:

  Dropout是指在模型训练时随机让网络某些隐含层节点的权重不工作,不工作的那些节点可以暂时认为不是网络结构的一部分,但是它的权重得保留下来(只是暂时不更新而已),因为下次样本输入时它可能又得工作了(有点抽象,具体实现看后面的实验部分)。

  按照hinton的文章,他使用Dropout时训练阶段和测试阶段做了如下操作:

  在样本的训练阶段,在没有采用pre-training的网络时(Dropout当然可以结合pre-training一起使用),hintion并不是像通常那样对权值采用L2范数惩罚,而是对每个隐含节点的权值L2范数设置一个上限bound,当训练过程中如果该节点不满足bound约束,则用该bound值对权值进行一个规范化操作(即同时除以该L2范数值),说是这样可以让权值更新初始的时候有个大的学习率供衰减,并且可以搜索更多的权值空间(没理解)。

  在模型的测试阶段,使用”mean network(均值网络)”来得到隐含层的输出,其实就是在网络前向传播到输出层前时隐含层节点的输出值都要减半(如果dropout的比例为50%),其理由文章说了一些,可以去查看(没理解)。

  关于Dropout,文章中没有给出任何数学解释,Hintion的直观解释和理由如下:

  1. 由于每次用输入网络的样本进行权值更新时,隐含节点都是以一定概率随机出现,因此不能保证每2个隐含节点每次都同时出现,这样权值的更新不再依赖于有固定关系隐含节点的共同作用,阻止了某些特征仅仅在其它特定特征下才有效果的情况。

  2. 可以将dropout看作是模型平均的一种。对于每次输入到网络中的样本(可能是一个样本,也可能是一个batch的样本),其对应的网络结构都是不同的,但所有的这些不同的网络结构又同时share隐含节点的权值。这样不同的样本就对应不同的模型,是bagging的一种极端情况。个人感觉这个解释稍微靠谱些,和bagging,boosting理论有点像,但又不完全相同。

  3. native bayes是dropout的一个特例。Native bayes有个错误的前提,即假设各个特征之间相互独立,这样在训练样本比较少的情况下,单独对每个特征进行学习,测试时将所有的特征都相乘,且在实际应用时效果还不错。而Droput每次不是训练一个特征,而是一部分隐含层特征。

  4. 还有一个比较有意思的解释是,Dropout类似于性别在生物进化中的角色,物种为了使适应不断变化的环境,性别的出现有效的阻止了过拟合,即避免环境改变时物种可能面临的灭亡。

  文章最后当然是show了一大把的实验来说明dropout可以阻止过拟合。这些实验都是些常见的benchmark,比如Mnist, Timit, Reuters, CIFAR-10, ImageNet.

                           

  实验过程:

  本文实验时用mnist库进行手写数字识别,训练样本2000个,测试样本1000个,用的是matlab的https://github.com/rasmusbergpalm/DeepLearnToolbox,代码在test_example_NN.m上修改得到。关于该toolbox的介绍可以参考网友的博文【面向代码】学习 Deep Learning(一)Neural Network。这里我只用了个简单的单个隐含层神经网络,隐含层节点的个数为100,所以输入层-隐含层-输出层节点依次为784-100-10. 为了使本例子简单话,没用对权值w进行规则化,采用mini-batch训练,每个mini-batch样本大小为100,迭代20次。权值采用随机初始化。

 

  实验结果:

  没用Dropout时:

  训练样本错误率(均方误差):0.032355

  测试样本错误率:15.500%

  使用Dropout时:

  训练样本错误率(均方误差):0.075819

  测试样本错误率:13.000%

  可以看出使用Dropout后,虽然训练样本的错误率较高,但是训练样本的错误率降低了,说明Dropout的泛化能力不错,可以防止过拟合。

 

  实验主要代码及注释:

  test_dropout.m:  

卷积神经网络(CNN)学习笔记
%% //导入minst数据并归一化
load mnist_uint8;
train_x
= double(train_x(1:2000,:)) / 255;
test_x
= double(test_x(1:1000,:)) / 255;
train_y
= double(train_y(1:2000,:));
test_y
= double(test_y(1:1000,:));
% //normalize
[train_x, mu, sigma] = zscore(train_x);% //归一化train_x,其中mu是个行向量,mu是个列向量
test_x = normalize(test_x, mu, sigma);% //在线测试时,归一化用的是训练样本的均值和方差,需要特别注意

%% //without dropout
rng(0);
nn
= nnsetup([784 100 10]);% //初步构造了一个输入-隐含-输出层网络,其中包括了
% //权值的初始化,学习率,momentum,激发函数类型,
% //惩罚系数,dropout等
opts.numepochs = 20; % //Number of full sweeps through data
opts.batchsize = 100; % //Take a mean gradient step over this many samples
[nn, L] = nntrain(nn, train_x, train_y, opts);
[er, bad]
= nntest(nn, test_x, test_y);
str
= sprintf('testing error rate is: %f',er);
disp(str)

%% //with dropout
rng(0);
nn
= nnsetup([784 100 10]);
nn.dropoutFraction
= 0.5; % //Dropout fraction,每一次mini-batch样本输入训练时,随机扔掉50%的隐含层节点
opts.numepochs = 20; % //Number of full sweeps through data
opts.batchsize = 100; % //Take a mean gradient step over this many samples
nn = nntrain(nn, train_x, train_y, opts);
[er, bad]
= nntest(nn, test_x, test_y);
str
= sprintf('testing error rate is: %f',er);
disp(str)
卷积神经网络(CNN)学习笔记

  下面来分析与dropout相关的代码,集中在上面test.m代码的后面with drop部分。首先在训练过程中需要将神经网络结构nn的dropoutFraction设置为一定比例,这里设置为50%:nn.dropoutFraction = 0.5;

  然后进入test_dropout.m中的nntrain()函数,没有发现与dropoutFraction相关的代码,继续进入网络前向传播函数nnff()函数中,在网络的隐含层节点激发函数值被计算出来后,有下面的代码:

卷积神经网络(CNN)学习笔记
    if(nn.dropoutFraction > 0)

if(nn.testing)

nn.a{i}
= nn.a{i}.*(1 - nn.dropoutFraction);

else

nn.dropOutMask{i}
= (rand(size(nn.a{i}))>nn.dropoutFraction);

nn.a{i}
= nn.a{i}.*nn.dropOutMask{i};

end

end
卷积神经网络(CNN)学习笔记

      由上面的代码可知,隐含层节点的输出值以dropoutFraction百分比的几率被随机清0(注意此时是在训练阶段,所以是else那部分的代码),既然前向传播时有些隐含节点值被清0了,那么在误差方向传播时也应该有相应的处理,果然,在反向传播函数nnbp()中,有下面的代码:

    if(nn.dropoutFraction>0)

d{i}
= d{i} .* [ones(size(d{i},1),1) nn.dropOutMask{i}];

end

  也就是说计算节点误差那一项时,其误差项也应该清0。从上面可以看出,使用dropout时,其训练部分的代码更改很少。

  (有网友发私信说,反向传播计算误差项时可以不用乘以dropOutMask{i}矩阵,后面我仔细看了下bp的公式,一开始也感觉不用乘有道理。因为源码中有为:

卷积神经网络(CNN)学习笔记
for i = 1 : (n - 1)
if i+1==n
nn.dW{i}
= (d{i + 1}' * nn.a{i}) / size(d{i + 1}, 1);
else
nn.dW{i}
= (d{i + 1}(:,2:end)' * nn.a{i}) / size(d{i + 1}, 1);
end
end
卷积神经网络(CNN)学习笔记

  代码进行权重更新时,由于需要乘以nn.a{i},而nn.a{i}在前向过程中如果被mask清掉的话(使用了dropout前提下),则已经为0了。但其实这时错误的,因为对误差

敏感值作用的是与它相连接的前一层权值,并不是本层的权值,而本层的输出a只对它的下一层权值更新有效。)        

  再来看看测试部分,测试部分如hintion论文所说的,采用mean network,也就是说前向传播时隐含层所有节点的输出同时减小dropoutFraction百分比,即保留(1- dropoutFraction)百分比,代码依旧是上面贴出的nnff()函数里满足if(nn.testing)的部分:

卷积神经网络(CNN)学习笔记
    if(nn.dropoutFraction > 0)

if(nn.testing)

nn.a{i}
= nn.a{i}.*(1 - nn.dropoutFraction);

else

nn.dropOutMask{i}
= (rand(size(nn.a{i}))>nn.dropoutFraction);

nn.a{i}
= nn.a{i}.*nn.dropOutMask{i};

end

end
卷积神经网络(CNN)学习笔记

   上面只是个简单的droput实验,可以用来帮助大家理解dropout的思想和使用步骤。其中网络的参数都是采用toolbox默认的,并没有去调整它,如果该实验将训练样本增大,比如6w张,则参数不变的情况下使用了dropout的识别率还有可能会降低(当然这很有可能是其它参数没调到最优,另一方面也说明在样本比较少的情况下,droput确实可以防止过拟合),为了体现droput的优势,这里我只用了2000张训练样本。

 

  参考资料:

  Hinton, G. E., et al. (2012). "Improving neural networks by preventing co-adaptation of feature detectors." arXiv preprint arXiv:1207.0580.

      https://github.com/rasmusbergpalm/DeepLearnToolbox

     【面向代码】学习 Deep Learning(一)Neural Network

 

作者:tornadomeet 出处:http://www.cnblogs.com/tornadomeet 欢迎转载或分享,但请务必声明文章出处。 (新浪微博:tornadomeet,欢迎交流!)