题目
https://www.kaggle.com/c/digit-recognizer
前言
上一篇用了个简单的神经网络来解决mnist的问题,介绍了一下权重初始化的技巧,防止训练梯度到最后一层的时候变为nan,还使用了bn算法,取得了一些成效。这一章里,我会介绍一下训练中使用的更新梯度的优化算法,还有对神经网络进行正则化和dropout的操作。
SGD的问题
使用随机梯度下降算法,虽然能够使得梯度不断下降,让模型收敛到一个较优解,但是也存在不少问题,看下面的图:
假设中间的笑脸是最优解,sgd的更新的轨迹会像图中显示的那样,不断波动。这是因为我们每次选择了一个batch去更新,每次的更新完全是根据当前计算的loss,就会出现这种情况:当前batch让你往东南方向走,下一个batch让你往西南方向走,虽然大体的方向是对的,但是会不断波动,导致更新速度比较慢。
Momentum算法
上面也讲了,sgd算法的问题是因为每次更新都完全依赖当前的batch。momentum算法来源于物理世界中动量的概念,更新模拟了物体的惯性,更新的时候使用一部分上次更新的方向,使用当前batch的loss对更新方向进行调整,得到最终更新的方向。利用这种方式,减少梯度震荡带来的影响,对更新进行加速:
这里$\rho$是一个超参数,表示上一个梯度更新方向衰减的系数,一般用0.9左右。$\alpha$是学习率。
更新方式用图像表示就是这样,真正的更新方向是上一个更新的方向与当前方向的合成,有点类似与力的合成:
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('loss'))
ok,那么Dropout呢?这又是什么操作?这个操作也比较简单,就是给训练的输入加入噪声,让模型拥有更好的泛化能力,看下面的图:
dropout在每批数据过网络的时候,会随机选择一些结点抛弃掉,图中的虚线。也就是这个输入不起作用,又或者看作把输入的值抹去置0。通过这种方法,从某种意义上来说,训练数据变多了,不同结点抛弃的组合,会产生许多不一样的样本,神经网络必须在这种情况下,抓住输入的主要的、关键的特征才能训练好。大体就是给神经网络制造更多的难题,让它学会提纲挈领,这样才会有更好的泛化能力,不去过分拟合训练数据。
dropout的代码就更简单了,对输入进行下面的处理就行了:
# 0.8是抛弃x的某个值的概率
x = tf.nn.dropout(x,0.8)
代码与结果
测试一下发现正则化和dropout的效果并不明显,而用adam效果非常好(可能也是之前训练的不够充分)。后来想了想,把网络扩得更大一些,然后加了dropout训了一下,发现acc有所提高。最终的参数:
layers = [784,512,256,10]
act:elu
batch_size:50
epoch:25
learning_rate: 0.02
use_bn: True use_
dropout:True
opt:AdamOptimizer
train_acc is: 0.995741, test_acc is 0.977619
这个模型在kaggle上的准确率达到了0.97814,效果可以说已经很好了。在不使用卷积神经网络的情况下,我认为还是比较高的吧。下一步打算上卷积神经网络,看看能达到什么效果。
import os
import numpy as np
import tensorflow as tf
import random
# layers = [784, 1024,2048,512, 10] act:elu batch_size:128 epoch:15 learning_rate: 0.01 train_acc is: 0.921534, test_acc is 0.920000
# layers = [784,30,10] act:elu batch_size:30 epoch:10 learning_rate: 0.2 train_acc is: 0.958571, test_acc is 0.947857
# layers = [784,256,64,10] act:elu batch_size:50 epoch:20 learning_rate: 0.02 use_bn: True train_acc is: 0.972566, test_acc is 0.956190
# layers = [784,256,64,10] act:elu batch_size:50 epoch:20 learning_rate: 0.02 use_bn: True opt:AdamOptimizer train_acc is: 0.998995, test_acc is 0.97595
# layers = [784,512,256,10] act:elu batch_size:50 epoch:25 learning_rate: 0.02 use_bn: True use_dropout:True opt:AdamOptimizer train_acc is: 0.995741, test_acc is 0.977619
class SimpleModel(object):
def __init__(self):
self.learning_rate = 0.02
self.batch_size = 50
self.epoch = 25
self.use_bn = True
self.use_regularizer = False
self.use_dropout = True
def setRegularizer(self,w):
if self.use_regularizer:
tf.add_to_collection('loss', tf.contrib.layers.l1_regularizer(0.002)(w))
def hide_layer(self, x, layer_name, var_shape, is_train=True, decay=0.999):
if self.use_dropout and is_train:
x = tf.nn.dropout(x,0.8)
n_input = np.prod(var_shape[:-1])
w_initializer = tf.truncated_normal_initializer(mean=0,stddev=1,dtype=tf.float32)
b_initializer = tf.constant_initializer(0.1, dtype=tf.float32)
W = tf.get_variable(layer_name + '_w', var_shape, initializer=w_initializer,dtype=tf.float32, trainable=True)/np.sqrt(n_input/2)
b = tf.get_variable(layer_name + '_b', [var_shape[-1]],initializer=b_initializer,dtype=tf.float32, trainable=True)
self.setRegularizer(W)
out = tf.matmul(x,W) + b
if self.use_bn:
zero_initializer = tf.constant_initializer(0.0, dtype=tf.float32)
one_initializer = tf.constant_initializer(1.0, dtype=tf.float32)
scale = tf.get_variable(layer_name + '_scale', var_shape[-1], initializer=one_initializer,dtype=tf.float32, trainable=True)
beta = tf.get_variable(layer_name + '_beta', var_shape[-1], initializer=zero_initializer,dtype=tf.float32, trainable=True)
ema_mean = tf.get_variable(layer_name + '_emamean', var_shape[-1],initializer=zero_initializer,dtype=tf.float32, trainable=False)
ema_var = tf.get_variable(layer_name + '_emavar', var_shape[-1],initializer=one_initializer,dtype=tf.float32, trainable=False)
if is_train:
batch_mean, batch_var = tf.nn.moments(out,[0])
self.train_mean = tf.assign(ema_mean, ema_mean * decay + batch_mean * (1 - decay))
self.train_var = tf.assign(ema_var, ema_var * decay + batch_var * (1 - decay))
with tf.control_dependencies([self.train_mean, self.train_var]):
out = tf.nn.batch_normalization(out, self.train_mean, self.train_var, beta, scale, variance_epsilon=0.00001)
else:
out = tf.nn.batch_normalization(out, ema_mean, ema_var,beta, scale, variance_epsilon=0.00001)
return tf.nn.elu(out)
def build_model(self, is_train=True):
print 'build_model'
self.x = tf.placeholder(tf.float32,[None, 784])
layer_output = self.x
layers = [784,30,10]
layers = [784,256,64,10]
layers = [784,512,256,10]
n = len(layers) - 1
for i in range(n):
var_shape = [layers[i], layers[i + 1]]
layer_output = self.hide_layer(layer_output, 'layer_' + str(i), var_shape, is_train)
# layer_output = tf.nn.elu(tf.matmul(layer_output,W) + b)
self.y = tf.nn.softmax(layer_output)
self.label = tf.placeholder(tf.float32,[None,10])
self.cross_entropy = -tf.reduce_sum(self.label*tf.log(self.y))
# opt = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate)
# opt = tf.train.MomentumOptimizer(learning_rate=self.learning_rate,momentum=0.9)
# opt = tf.train.MomentumOptimizer(learning_rate=self.learning_rate,momentum=0.9, use_nesterov=True)
# opt = tf.train.AdagradOptimizer(learning_rate=self.learning_rate)
# opt = tf.train.RMSPropOptimizer(learning_rate=self.learning_rate)
opt = tf.train.AdamOptimizer(learning_rate=self.learning_rate)
self.loss = self.cross_entropy
if self.use_regularizer:
tf.add_to_collection('loss', self.cross_entropy)
self.loss = tf.add_n(tf.get_collection('loss'))
self.train_step = opt.minimize(self.loss)
config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=False)
self.sess = tf.Session(config=config)
init = tf.global_variables_initializer()
self.sess.run(init)
self.saver = tf.train.Saver(tf.global_variables())
def randomBatch(self,size, epoch):
self.data_tags = []
for i in range(epoch):
for j in range(size):
self.data_tags.append(j)
random.shuffle(self.data_tags)
self.data_pos = 0
def getNextBatch(self, x_inputs, y_labels):
batch_x = []
batch_y = []
m = len(self.data_tags)
for i in range(self.batch_size):
p = self.data_tags[self.data_pos]
self.data_pos = (self.data_pos + 1)%m
batch_x.append(x_inputs[p])
batch_y.append(y_labels[p])
return np.array(batch_x),np.array(batch_y)
def train(self,x_inputs, y_labels):
pos = 0
count = 0
total = int(len(x_inputs)/self.batch_size)
self.randomBatch(len(x_inputs),self.epoch)
for i in range(self.epoch*total):
x_batch,y_batch = self.getNextBatch(x_inputs,y_labels)
y, loss,_ = self.sess.run([self.y, self.loss,self.train_step],feed_dict={self.x:x_batch,self.label:y_batch})
# print 'y:' + str(y)
# print 'loss :' + str(loss)
count += 1
if count % 50 == 0:
print 'step %d: ,loss:%.6f' % (count, loss)
self.saver.save(self.sess, './train_models/simple2.model.ckpt',global_step=0)
print 'train over'
def init_model(self,modelName):
self.build_model(False)
self.saver.restore(self.sess, os.path.join('./train_models/',modelName) )
def test(self, x):
predict = self.sess.run(self.y, feed_dict={self.x:x})
#predict = np.array(predict).astype(float)
res = np.argmax(predict, axis=1)
# print res
return res
#return np.argmax(predict, axis=1)