深度学习三人行(第7期)----深度学习之避免过拟合(正则化)
上一期,我们一起学习了深度学习中的学习率的悲惨命运,
今天我们一起学习下深度学习中如何避免过拟合,我们多多交流,共同进步。本期主要内容如下:
提前停止训练
L1和L2范数正则化
DroupOut
最大范数正则化
数据增强
小结
我们知道,深度学习有成千上万个参数,甚至数百万。由于有巨大的参数,所以整个网络有着难以置信的自由度,能够拟合复杂的数据集。也就是说,这个巨大的灵活度意味着在训练集上很容易过拟合。这里,我们主要通过几个常见的方法,来看下如何避免过拟合,主要有:过早的停止训练,L1和L2范数的正则化,DroupOut, 最大范数正则化,数据增强等。
一. 提前停止训练
为了避免对训练集造成过拟合,一个很好的方法就是在过拟合之前就停止对网络的训练(之前文章有介绍过)。也就是说在测试集上的性能开始下降之前的时候停止对训练数据集的训练。
在TensorFlow中就是在训练的时候,通过对测试集上性能的评估,每隔一定的间隔进行保存一个当前最优的网络,如果该网络比上一个间隔更好,则替换掉上一个网络。这样在训练结束后,将保存一个整个网络训练过程中出现最优的一个模型。尽管在实践中提前停止训练的方法能够的很好,但是通常情况下,如果能够结合其他正则化技术的话,能够变现更佳。
二. L1和L2范数正则化
正如之前我们学习线性模型的时候一样,我们也可以在神经网络用L1和L2范数进行约束权重(一般 不对偏置项)。在TensorFlow中实现正则化还是比较简单的,只需要在损失函数中加上合适的正则项即可。比如:假如我们的网络只有一个隐藏层,权重为weights1,一个输出层,权重为weight2。那么我们就能对权重用L1正则化,如下:
1[...] # construct the neural network
2base_loss = tf.reduce_mean(xentropy, name="avg_xentropy")
3reg_losses = tf.reduce_sum(tf.abs(weights1)) + tf.reduce_sum(tf.abs(weights2))
4loss = tf.add(base_loss, scale * reg_losses, name="loss")
上面的方法虽然没问题,但是,如果我们的网络有很多层,那么上面的这种方法就不是太方便了。幸运的是,对于多层的正则化,TensorFlow有更好的方法。TensorFlow中有很多创建变量的函数在创建的时候都会接受一个正则化的参数。我们可以传输带权重的函数作为一个参数,并且返回相应的正则化是损失。如下:
1with arg_scope(
2 [fully_connected],weights_regularizer=tf.contrib.layers.l1_regularizer(scale=0.01)):
3 hidden1 = fully_connected(X, n_hidden1, scope="hidden1")
4 hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2")
5 logits = fully_connected(hidden2, n_outputs, activation_fn=None,scope="out")
上面的代码创建一个有两个隐藏层和一个输出层的神经网络,并且对于每一层的权重,都在图中创建了节点和计算L1正则化损失。TensorFlow自动把所有的正则化损失加到一个特定的集合中。我们只需要将这些正则化损失加到整体损失中,如下:
1reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
2loss = tf.add_n([base_loss] + reg_losses, name="loss")
三. DropOut
然而,在深度神经网络中,最流行的正则化技术则是DropOut, 这项技术在2012年由G.E.Hinton提出。后来被Nitish Srivastava发展并证明是非常成功的。即使最优秀的网络,如果增加DropOut技术,准确率也能够提升1~2个百分点。这看起来提高并不是太多,但是如果一个模型已经达到95%的准确率,那么提高两个百分点意味着错误率降低了40%(从5%到3%)。
其实,这是一个蛮简单的算法:就是在training的每一步,每一个神经元(包括输入神经元,但是不包括输出神经元),都有一个概率p被丢弃。被丢弃的神经元,意味着在本次training中完全被放弃,但是可能在下次迭代中被激活。这个超参数p成为DropOut率,一般设置为50%。如下图:
咋一看,这种粗鲁的做法能够很好的工作有些不可思议。但是事实证明确实可行!由于每一个神经元随机的放弃,最终训练的结果会使每一个神经元都不会过分依赖于其他的神经元,而是使努力使自己达到最优。最终网络对输入的轻微变化不在敏感,进而得到一个鲁棒性很强的网络。
因为我们训练的时候会随机的丢弃一些神经元(概率为p),但是预测的时候就没办法随机丢弃了。如果丢弃一些神经元,这会带来结果不稳定的问题,也就是给定一个测试数据,有时候输出a有时候输出b,结果不稳定,这是实际系统不能接受的,用户可能认为模型预测不准。那么一种”补偿“的方案就是每个神经元的权重都乘以一个(1-p),或者在训练的时候除以(1-p),这样在“总体上”使得测试数据和训练数据是大致一样的。比如一个神经元的输出是x,那么在训练的时候它有(1-p)的概率参与训练,p的概率丢弃,那么它输出的期望是(1-p)x+p0=(1-p)x。因此测试的时候把这个神经元的权重乘以(1-p)可以得到同样的期望。
在TensorFlow中如何运用dropout呢?只需要简单的在输入层和隐藏层之前加上dropout函数即可。在training的 过程中,这个函数会随机将一些神经元置为0,并且自动除以(1-p)。下面代码展示了如何在TensorFlow中运用dropout正则化技术,如下:
1from tensorflow.contrib.layers import dropout
2[...]
3is_training = tf.placeholder(tf.bool, shape=(), name='is_training')
4keep_prob = 0.5
5X_drop = dropout(X, keep_prob, is_training=is_training)
6hidden1 = fully_connected(X_drop, n_hidden1, scope="hidden1")
7hidden1_drop = dropout(hidden1, keep_prob, is_training=is_training)
8hidden2 = fully_connected(hidden1_drop, n_hidden2, scope="hidden2")
9hidden2_drop = dropout(hidden2, keep_prob, is_training=is_training)
10logits = fully_connected(hidden2_drop, n_outputs, activation_fn=None,scope="outputs")
正如之前batch Normalization一样,在训练的时候我们需要设置is_training 为true,测试的时候设为false。当我们观察到模型出现过拟合的时候,我们可以增加dropout率,也就是说减小keep_prob率。相反,如果模型欠出现拟合的时候,可以增加keep_prob率,减小dropout率。通常对于大的网络增加dropout率,小的网络减少dropout率往往会有帮助。
然而,一般情况下,加入dropout的话,会使训练收敛明显放慢。但是往往会得到更好的模型,很值!
四. 最大范数正则化
另外一个神经网络中常见的正则化技术就是最大范数正则化。对于每一个神经元的权重,都受到如下的约束:
其中||w||_2为L2范数,r为最大范数。通常情况下,我们通过计算w的L2范数来进行达到目的。如下:
最大范数正则化,往往能够降低过拟合,如果不适用batch正则化的话,也可以减轻梯度消失和梯度爆炸的问题。
TensorFlow并没有提供一个现成的最大范数正则化函数,但是实施起来也并不麻烦。如下:
1threshold = 1.0
2clipped_weights = tf.clip_by_norm(weights, clip_norm=threshold, axes=1)
3clip_weights = tf.assign(weights, clipped_weights)
上面代码创建一个clip_weights的节点,用来调整weights变量。我们可以在每一次迭代之后加上这个操作,如下:
1with tf.Session() as sess:
2 [...]
3 for epoch in range(n_epochs):
4 [...]
5 for X_batch, y_batch in zip(X_batches, y_batches):
6 sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
7 clip_weights.eval()
我们可以通过scope来观察权重的变化,如下:
1hidden1 = fully_connected(X, n_hidden1, scope="hidden1")
2with tf.variable_scope("hidden1", reuse=True):
3 weights1 = tf.get_variable("weights")
也可以通过root scope来得到权重,如下:
1hidden1 = fully_connected(X, n_hidden1, scope="hidden1")
2hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2")
3[...]
4with tf.variable_scope("", default_name="", reuse=True): # root scope
5 weights1 = tf.get_variable("hidden1/weights")
6 weights2 = tf.get_variable("hidden2/weights")
如果我们不知道变量的名字,那么可以通过TensorBoard或者简单的用global_variables()函数来查看变量名,如下:
1for variable in tf.global_variables():
2 print(variable.name)
尽管上面的方法可行,但是显得有些繁琐。一个更为简洁的方法就是创建一个最大范数正则化的函数,就好比前面学的L1,L2范数正则化函数一样,如下:
1def max_norm_regularizer(threshold, axes=1, name="max_norm",
2collection="max_norm"):
3 def max_norm(weights):
4 clipped = tf.clip_by_norm(weights, clip_norm=threshold, axes=axes)
5 clip_weights = tf.assign(weights, clipped, name=name)
6 tf.add_to_collection(collection, clip_weights)
7 return None # there is no regularization loss term
8 return max_norm
上面的函数返回的是一个参数化的函数max_norm(),可以向其他正则化一样:
1max_norm_reg = max_norm_regularizer(threshold=1.0)
2hidden1 = fully_connected(X, n_hidden1, scope="hidden1",weights_regularizer=max_norm_reg)
注意到最大范数正则化不需要在全局的损失函数上增加正则项,所以max_norm()函数返回为None。但是我们仍需要在每次迭代之后运行clip_weights,这就是为什么max_norm()函数中将clip_weights增加到collection中。我们需要获取clip操作并在每次迭代之后运行,如下:
1clip_all_weights = tf.get_collection("max_norm")
2with tf.Session() as sess:
3 [...]
4 for epoch in range(n_epochs):
5 [...]
6 for X_batch, y_batch in zip(X_batches, y_batches):
7 sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
8 sess.run(clip_all_weights)
五. 数据增强
最后一个正则化技术就是数据增强,主要是认为的从已有的数据中人为的产生新的数据,进而来增加数据集,降低过拟合。比如,我们要对蘑菇图片进行分类,我们可以通过轻微的旋转,平移,缩放等方法来产生新的数据,如下图:
这就去迫使模型能对蘑菇的位置,大小,方位等适应,也可以通过改变图片的对比度来使得模型适应不同的光照。通过这些方法,可以使大大扩大数据集。
TensorFlow中提供了一些图像操作的方法,比如平移,旋转,缩放,翻转,剪切,调整光照对比度,饱和度,色度等,这就使得对数据增强比较方便。
六. 小结
这期我们主要通过几个常见的方法:过早的停止训练,L1和L2范数的正则化,DroupOut, 最大范数正则化,数据增强等。来避免模型过拟合,几种方法各有千秋,找到最适合自己模型的方法才是王道。
(如需更好的了解相关知识,欢迎加入智能算法社区,在“智能算法”公众号发送“社区”,即可加入算法微信群和QQ群)