一、遇到的问题
(1)梯度消失
我在实现过程中犯的第一个错误是没有循序渐进。仗着自己写过一些神经网络的代码以为手到擒来,直接按照LeNet-5的结构写,过于复杂的结构给测试和调试都带来了很大的麻烦,可谓不作死就不会死。
简单分析一下LeNet-5的结构:第一层8个5*5的卷积核,第二层分别作2*2pooling,第三层16个5*5的卷积核,第四层2*2pooling,随后是三个节点数分别为120、84、10的全连接层(做手写数字识别)。这时的参数个数为卷积核及其偏置+pooling层放缩系数及偏置+全连接层权重及偏置,全链接层的参数个数比前四层的参数个数多了两个数量级。
过多的参数个数会极大地提升运算时间,降低测试的效率;同时过多的层数伴随着严重的梯度消失问题(vanishing gradient),严重影响训练效果。在上面提到的网络结构下,我发现传到第一层的梯度已经小于1e-10,只有后面的全连接层在变化,卷积层几乎训不动了。
我对付这个问题的手段包括:
- 减少层数
- 增大学习率(learning rate)
- 用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等等。但是时间有限,老板已经嫌我不务正业了。
三、总结
- 写复杂的算法不能急于求成,要循序渐进
- 测试很重要。上Andrew Ng公开课时候觉得gradient check很烦,现在看来简直是神器
- 机器越快越好……