题目
https://www.kaggle.com/c/digit-recognizer
前言
上一篇用了个简单的神经网络来解决mnist的问题,介绍了一下权重初始化的技巧,防止训练梯度到最后一层的时候变为nan,还使用了bn算法,取得了一些成效。这一章里,我会介绍一下训练中使用的更新梯度的优化算法,还有对神经网络进行正则化和dropout的操作。
SGD的问题
使用随机梯度下降算法,虽然能够使得梯度不断下降,让模型收敛到一个较优解,但是也存在不少问题,看下面的图:
假设中间的笑脸是最优解,sgd的更新的轨迹会像图中显示的那样,不断波动。这是因为我们每次选择了一个batch去更新,每次的更新完全是根据当前计算的loss,就会出现这种情况:当前batch让你往东南方向走,下一个batch让你往西南方向走,虽然大体的方向是对的,但是会不断波动,导致更新速度比较慢。
Momentum算法
上面也讲了,sgd算法的问题是因为每次更新都完全依赖当前的batch。momentum算法来源于物理世界中动量的概念,更新模拟了物体的惯性,更新的时候使用一部分上次更新的方向,使用当前batch的loss对更新方向进行调整,得到最终更新的方向。利用这种方式,减少梯度震荡带来的影响,对更新进行加速:
这里 ρ 是一个超参数,表示上一个梯度更新方向衰减的系数,一般用0.9左右。 α 是学习率。
更新方式用图像表示就是这样,真正的更新方向是上一个更新的方向与当前方向的合成,有点类似与力的合成:
Nesterov Momentum算法
nesterov momentum其实和momentum差不多,只是修改了更新方向的合成方式:
公式:
和momentum差不多,就不多说了
AdaGrad算法
与上面的算法不同的是,AdaGrad算法关注的不是更新方向的问题,而是更新速率的问题,下面看一小段AdaGrad的代码:
可以看到代码里在更新的时候除了一个数,这个数是梯度的平方累积的开方,而1e-7是一个常数,这个只是为了防止计算的时候除0,可以发现,更新的速率是不断递减的,当更新到最后最后的时候,更新基本就会停止。
使用AdaGrad主要的目的是让学习率在学习过程中进行一些调节,因为开始的时候,学习率大一些可以加快训练速度,但是到了训练后期,可能需要进行一些细微方向的调整,但是学习率比较大的话就无法做到。
RMSProp算法
可以看到,AdaGrad算法中,学习率是不断递减的,这样就带来了一些问题。初始学习率要设置的比较合理,学习率设置较小的话,全程训练都会很慢,尤其到了后期,训练基本进行不下去,但是学习率过大,前期又有可能导致梯度爆炸之类的问题。
为了解决这个问题,RMSProp在AdaGrad算法的基础上,对分母做了一些处理,把前面累积的和削减,再加上新的值,这样既可以保证后期更新速率不至于太慢,又能较为灵活的调整学习率。
Adam算法
Adam其实就是把Momentum算法和RMSProp算法结合到一起,各取所长,双剑合璧,可以达到比较好的效果~
实验及效果
使用tensorflow的话,这几种算法的api都是有的,直接调用就好了:
# SGD
opt = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate)
# Momentum
opt = tf.train.MomentumOptimizer(learning_rate=self.learning_rate,momentum=0.9)
# Nesterov
opt = tf.train.MomentumOptimizer(learning_rate=self.learning_rate,momentum=0.9, use_nesterov=True)
# Adagrad
opt = tf.train.AdagradOptimizer(learning_rate=self.learning_rate)
# RMSProp
opt = tf.train.RMSPropOptimizer(learning_rate=self.learning_rate)
# Adam
opt = tf.train.AdamOptimizer(learning_rate=self.learning_rate)
我用了[784,256,64,10]这个网络,使用了bn算法,学习率用的0.01,50batch,训了5个epoch看效果,下面是测试结果:
Sgd:
效果一般般吧~
Momentum
要比sgd效果好一些,更新速度要更高一些。
Nesterov
和momentum没多大差距,毕竟原理差不多
AdaGrad
AdaGrad就表现得不尽如人意了,但是个人认为这是学习率设置不合理导致的,最开始的学习率设置的低了,导致更新速度一直很慢。
RMSProp
相比之下,RMSProp就好多了,因为做了一些decay操作,使得更新不会太僵硬。
Adam
我寄予厚望的Adam这次实验表现的没有RMSProp好啊……但这个也应该是学习率的问题,后面用了这个,效果还是很好的。
小结
虽然做了几个简单的实验比较,但是我发现,这个结果并不是那么有说服力,这些算法对与学习率和各种参数的要求是不同的,很难拉到同一起跑线上去比较,所以具体用哪个还是看自己的需求和实际情况。不过Adam应该是比较普适的一个方案。
正则化与Dropout
在神经网络训练的时候,如果模型比较复杂,很有可能出现过拟合的情况:在训练集上效果很好,但是在测试集里就表现的很惨,如下图:
虽然前面我们介绍了bn算法可以有效地缓解过拟合的问题,但是不妨碍我们研究一下别的方法,而且这些方法与bn算法也不冲突,也是有机会登场的~
之前也说过,模型的规模过大,很容易导致过拟合的问题,因此我们可以对模型做一个人为的限制,使得模型的复杂度不要过大,不要去过分拟合测试数据。
通常使用的正则化方法有两种,L1正则化与L2正则化,这两个方法的公式都比较类似:
L1:
L2:
这两种方法基本差不多,都是引入了权重作为loss的一部分,这使得梯度会向着权重变小的方向偏移,这么做有什么用呢?在神经网络比较复杂的时候,拟合训练数据的方式可能并不只有一种,引入了正则化项,相当于把拟合方向往某一个方向拉扯,不让参数自由生长,无形之中就限制了模型的复杂度。
那么L1和L2两种正则化方法有什么不同?这个在整体上来看,可能差不多,但是L1正则化会另外一个特性,通过推导公式可以证明,L1正则化会把一部分参数衰减到0,也就是某个w为0,这个特性可以在特征选择中起到作用,比如卷积神经网络就可以用这个特性来提取图片某个区域的特征。
使用tensorflow代码也比较好写:
## add_to_collection可以收集参数到一个集合中,这是为了方便计算
## 因为我们每一层都有一个w
## tf.contrib.layers.l1_regularizer是tensorflow提供计算正则项的函数
## 0.002相当于公式中的lambda,会乘到w的累积上,是个超参数
tf.add_to_collection('loss', tf.contrib.layers.l1_regularizer(0.002)(w))
# other code ....
# 计算交叉熵损失函数
self.cross_entropy = -tf.reduce_sum(self.label*tf.log(self.y))
# 原来用的就是交叉熵,我们把它和之前求得的正则化项加在一起
tf.add_to_collection('loss', self.cross_entropy)
self.loss = tf.add_n(tf.get_collection(