,使用Tensorflow实现简单神经网络见前文,本文主要讲深度神经网络中常用的优化与加速技巧。
sigmoid函数下的梯度消失于梯度爆炸问题
由于sigmoid函数的自身缺陷:两段导数值小,中间导数值大,而其导数值最大也不超过0.25,所以在深度网络的反向传播算法中的逐级求导就出现了问题。考虑两种可能出现的极端情况:
- 导数值过小,由于网络层数而导致的导数累乘,最后求出来的低层网络导数值会过大,发生梯度爆炸
- 导数值过大,由于网络层数而导致的导数累乘,最后求出来的低层网络导数值会过小,发生梯度消失。
所以对使用梯度下降法的反向传播算法而言,激活函数sigmoid最理想的部分就是其中部的线性区域。
随机初始化
Glorot与Bengio在其论文中提出了一种在实际应用中能有效解决这个问题的方法:每一层的输出与输入必须是同方差的,并且前向传播与反向传播时梯度也是同方差的。初始权重值应当为均值为0, 的正态分布,这种策略称为Xavier初始化,而应用于ReLU及其变种函数的策略被称为He初始化。
he_init = tf.contrib.layers.variance_scaling_initializer()
hidden1 = tf.layers.dense(X,n_hidden1,activation=tf.nn.relu,kernel_initializer=he_init,name="hidden1")
更换激活函数
梯度爆炸\消失的问题是由于sigmoid函数的自身缺陷引起的,避免问题的另一个解决办法就是更换激活函数。
ReLU
- 优点:导数易于计算,不是0就是1,并且没有上饱和区间
- 缺点:存在不可导点,且有下饱和区域
LeakyReLU
其中 称为泄露系数,它指示函数在负区间的泄露程度。
- 优点:导数易于计算,没有饱和区间
- 缺点:存在不可导点
def leaky_relu(z, name=None):
return tf.maximum(0.01 * z, z, name=name)
hidden1 = tf.layers.dense(X, n_hidden1, activation=leaky_relu, name="hidden1")
Exponential Linear Unit(ELU)
ELU函数的负区间是一个指数函数。
- 优点:处处可导且没有饱和区间
- 缺点:增加了反向传播算法的计算复杂度
hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.elu, name="hidden1")
Scaled Exponential Linear Unit(SELU)
SELU是2017年提出的一种新激活函数,对于深度网络性能的提升非常明显。
def selu(z,
scale=1.0507009873554804934193349852946,
alpha=1.6732632423543772848170429916717):
return scale * tf.where(z >= 0.0, z, alpha * tf.nn.elu(z))
hidden1 = tf.layers.dense(X, n_hidden1, activation=selu, name="hidden1")
分批归一化(Batch Normalization)
虽然随即初始化与更换激活函数消除了起始阶段的梯度消失/爆炸的问题,但是如果网络太深训练时间太长,在训练过程中仍可能出现梯度问题。BN背后的思想是在每一层激活之前,将输入进行零中心化与归一化,然后进行伸缩变换,换句话说,BN是让模型去学习一个对每一层都最优的伸缩变换。整个操作如下式:
其中 是伸缩系数, 是变换系数, 是一个防止除0的极小值。BN将每一层的输入都转换成了符合标准正态分布的形式,然后再进行线性求和与激活输出。
在测试期间,数据是整个输入进行计算的,没有mini-batch,此时使用整个测试集进行计算。BN的主要缺点就是增加了计算量。
training = tf.placeholder_with_default(False, shape=(), name='training')
hidden1 = tf.layers.dense(X, n_hidden1, name="hidden1")
bn1 = tf.layers.batch_normalization(hidden1, training=training, momentum=0.9)
bn1_act = tf.nn.elu(bn1)
在实现时,布尔变量training
用于指示程序运行是处于training还是testing,而参数momentum
是用来计算mini-batch均值的老化系数,算法使用
来计算mini-batch的均值。
梯度截断
解决梯度爆炸最简单的方法就是设置一个阈值,当计算期间梯度超出阈值时就将其截断。
在实现时,梯度截断是应用到optimizer上的:
threshold = 1.0
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
grads_and_vars = optimizer.compute_gradients(loss)
capped_gvs = [(tf.clip_by_value(grad, -threshold, threshold), var)
for grad, var in grads_and_vars]
training_op = optimizer.apply_gradients(capped_gvs)
更快地找到最优参数
更快的optimizer
从实践上来说,通常情况下optimizer的最佳选择是AdamOptimizer,当然还有其余的选择如MomentumOptimizer、Nesterov Accelerated Gradient、AdagradOptimizer及RMSPropOptimizer等,此处皆不作讨论,只简单介绍Adam Optimization。
Adam算法运算过程具体如下:
- m表示动量,它记录每次计算中的梯度值,并根据动量衰退系数 衰减,相当于计算梯度的均值
- s表示伸缩,s记录的是梯度的平方均值( 表示元素相乘) ,平方操作会将大值变得更大
- T表示迭代次数,其值从1开始。因为m、s是被零始化的,所以第3、4步目的是在前几次的迭代中放大m、s
- 最后一步是更新参数, 表示元素相除
Adam(adaptive moment estimation)作为一种自适应学习率算法,它对于调参的依赖程度并没有其他算法那样高。
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
学习率调度
很容易理解,在学习过程中,前期使用较大的学习率,后期使用较小的学习率有助于加速。
在Tensorflow中实现学习率的指数衰减:
initial_learning_rate = 0.1
decay_steps = 10000
decay_rate = 1/10
global_step = tf.Variable(0, trainable=False, name="global_step") #追踪迭代次数
learning_rate = tf.train.exponential_decay(initial_learning_rate, global_step,
decay_steps, decay_rate)
注意像Adam这种本来就会在训练过程中调整学习率的算法,并不需要进行学习率调度。
避免过拟合
与 正则化
与 正则化的思想是将模型的权重范数加入到损失函数中,使得模型在学习中必须考虑模型的复杂度,从而避免了一味的追求低损失而导致的模型过于复杂。
实现时,需要在创建网络层时进行参数声明:
scale=0.001
hidden1 = tf.layers.dense(X, n_hidden1,activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l1_regularizer(scale),name="hidden1")
然后将正则化项加入到损失中去:
with tf.name_scope("loss"):
xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logits)
base_loss = tf.reduce_mean(xentropy, name="avg_xentropy")
reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
loss = tf.add_n([base_loss] + reg_losses, name="loss")
Dropout
Dropout的思想是在训练过程中,对每一层的神经元都以概率进行屏蔽,被屏蔽的神经元在此次传播中是失活的。假设神经元的保留概率为 ,则每个神经元被屏蔽的概率为 。因为Dropout会改变整个网络最终输出的期望值,所以为了不改变原网络的输出期望,需要对网络进行补偿运算,可以用每层的权重乘以 ,也可以用每层的输出除以 。
因为Dropout在测试阶段是不起作用的,所以用一个布尔变量来表明是training还是testing:
training = tf.placeholder_with_default(False, shape=(), name='training')
dropout_rate = 0.5 # == 1 - keep_prob
X_drop = tf.layers.dropout(X, dropout_rate, training=training)
hidden1 = tf.layers.dense(X_drop, n_hidden1, activation=tf.nn.relu,
name="hidden1")
hidden1_drop = tf.layers.dropout(hidden1, dropout_rate, training=training)
简单测试效果
使用Tensorflow中内置的MNIST数据集对这些优化方法进行简单测试,神经网络的参数与前文保持一致,不过降低了迭代次数,由400次降到了40次。
import tensorflow as tf
n_inputs=28*28 #MNIST图片像素,即样本特征数
n_hidden1=300 #第一隐含层的神经元数
n_hidden2=100 #第二隐含层的神经元数
n_outputs=10 #输出数
#从tensorflow中载入MNIST数据
from tensorflow.examples.tutorials.mnist import input_data
mnist=input_data.read_data_sets("/tmp/data/")
He+ELU+BN+Adam+MaxNorm
tf.reset_default_graph()
X=tf.placeholder(dtype=tf.float32,shape=(None,n_inputs),name="X") #X为二维矩阵,行数为样本数(未知),列数为特征数
Y=tf.placeholder(dtype=tf.int64,shape=(None),name="Y") #Y为单维向量
he_init = tf.contrib.layers.variance_scaling_initializer()
training = tf.placeholder_with_default(False, shape=(), name='training')
with tf.name_scope("dnn"):
hidden1=tf.layers.dense(X,n_hidden1,name="hidden1",
kernel_initializer=he_init)
bn1 = tf.layers.batch_normalization(hidden1, training=training, momentum=0.9)
bn1_act = tf.nn.elu(bn1)
hidden2=tf.layers.dense(bn1_act,n_hidden2,name="hidden2",
kernel_initializer=he_init)
bn2 = tf.layers.batch_normalization(hidden2, training=training, momentum=0.9)
bn2_act = tf.nn.elu(bn2)
logits_before_bn=tf.layers.dense(bn2_act,n_outputs,name="outputs",
kernel_initializer=he_init)
logits=tf.layers.batch_normalization(logits_before_bn, training=training, momentum=0.9)
def max_norm_regularizer(threshold, axes=1, name="max_norm",
collection="max_norm"):
def max_norm(weights):
clipped = tf.clip_by_norm(weights, clip_norm=threshold, axes=axes)
clip_weights = tf.assign(weights, clipped, name=name)
tf.add_to_collection(collection, clip_weights)
return None # there is no regularization loss term
return max_norm
with tf.name_scope("loss"):
entropy=tf.nn.sparse_softmax_cross_entropy_with_logits(labels=Y,logits=logits)
loss=tf.reduce_mean(entropy,name="loss")
loss_summary=tf.summary.scalar('loss',loss)
learning_rate=0.01
with tf.name_scope("train"):
optimizer=tf.train.AdamOptimizer(learning_rate)
training_op=optimizer.minimize(loss)
with tf.name_scope("eval"):
correct=tf.nn.in_top_k(logits,Y,1) #取某一样本所属类别概率最大的预测结果,再与此样本的标签Y进行对比
accuracy=tf.reduce_mean(tf.cast(correct,tf.float32)) #将correct矩阵进行类型转换,再求均值
accuracy_summary=tf.summary.scalar('accuracy',accuracy)
root_logdir="tf_logs" #设置根目录为当前目录下的tf_logs文件夹
logdir="{}/He+ELU+BN+Adam+MaxNorm/".format(root_logdir) #文件夹名加入时间戳
file_writer=tf.summary.FileWriter(logdir,tf.get_default_graph())
n_epochs=40
batch_size=50
init=tf.global_variables_initializer() #全局变量初始化器
saver=tf.train.Saver() #模型数据保存器
clip_all_weights = tf.get_collection("max_norm")
with tf.Session() as sess:
init.run()
for epoch in range(n_epochs):
for iteration in range(mnist.train.num_examples//batch_size):
X_batch,Y_batch=mnist.train.next_batch(batch_size)
sess.run(training_op,feed_dict={X:X_batch,Y:Y_batch})
sess.run(clip_all_weights)
loss_summary_str,acc_summary_str=sess.run([accuracy_summary,loss_summary],feed_dict={X:mnist.test.images,Y:mnist.test.labels}) #给数据
file_writer.add_summary(loss_summary_str,epoch)
file_writer.add_summary(acc_summary_str,epoch)
if (epoch+1)%5==0:
acc_train=accuracy.eval(feed_dict={X:X_batch,Y:Y_batch})
acc_test=accuracy.eval(feed_dict={X:mnist.test.images,Y:mnist.test.labels})
print(epoch+1,"Train acc:",acc_train,"Test acc:",acc_test)
save_path=saver.save(sess,"./my_model_final.ckpt") #保存模型数据
file_writer.close()
He+SELU+Adam+Dropout
tf.reset_default_graph()
X=tf.placeholder(dtype=tf.float32,shape=(None,n_inputs),name="X") #X为二维矩阵,行数为样本数(未知),列数为特征数
Y=tf.placeholder(dtype=tf.int64,shape=(None),name="Y") #Y为单维向量
def selu(z,
scale=1.0507009873554804934193349852946,
alpha=1.6732632423543772848170429916717):
return scale * tf.where(z >= 0.0, z, alpha * tf.nn.elu(z))
he_init = tf.contrib.layers.variance_scaling_initializer()
training = tf.placeholder_with_default(False, shape=(), name='training')
dropout_rate = 0.5 # == 1 - keep_prob
X_drop = tf.layers.dropout(X, dropout_rate, training=training)
with tf.name_scope("dnn"):
hidden1 = tf.layers.dense(X, n_hidden1, activation=selu,kernel_initializer=he_init, name="hidden1")
hidden1_drop=tf.layers.dropout(hidden1,dropout_rate,training=training)
hidden2 = tf.layers.dense(hidden1_drop, n_hidden2, activation=selu, kernel_initializer=he_init,name="hidden2")
hidden2_drop=tf.layers.dropout(hidden2,dropout_rate,training=training)
logits = tf.layers.dense(hidden2_drop, n_outputs,kernel_initializer=he_init, name="outputs")
with tf.name_scope("loss"):
entropy=tf.nn.sparse_softmax_cross_entropy_with_logits(labels=Y,logits=logits)
loss=tf.reduce_mean(entropy,name="loss")
loss_summary=tf.summary.scalar('loss',loss)
learning_rate=0.01
with tf.name_scope("train"):
optimizer=tf.train.AdamOptimizer(learning_rate)
training_op=optimizer.minimize(loss)
with tf.name_scope("eval"):
correct=tf.nn.in_top_k(logits,Y,1) #取某一样本所属类别概率最大的预测结果,再与此样本的标签Y进行对比
accuracy=tf.reduce_mean(tf.cast(correct,tf.float32)) #将correct矩阵进行类型转换,再求均值
accuracy_summary=tf.summary.scalar('accuracy',accuracy)
root_logdir="tf_logs" #设置根目录为当前目录下的tf_logs文件夹
logdir="{}/He+SELU+Adam+Dropout/".format(root_logdir) #文件夹名加入时间戳
file_writer=tf.summary.FileWriter(logdir,tf.get_default_graph())
n_epochs=40
batch_size=50
init=tf.global_variables_initializer() #全局变量初始化器
saver=tf.train.Saver() #模型数据保存器
with tf.Session() as sess:
init.run()
for epoch in range(n_epochs):
for iteration in range(mnist.train.num_examples//batch_size):
X_batch,Y_batch=mnist.train.next_batch(batch_size)
sess.run(training_op,feed_dict={X:X_batch,Y:Y_batch})
loss_summary_str,acc_summary_str=sess.run([accuracy_summary,loss_summary],feed_dict={X:mnist.test.images,Y:mnist.test.labels}) #给数据
file_writer.add_summary(loss_summary_str,epoch)
file_writer.add_summary(acc_summary_str,epoch)
if (epoch+1)%5==0:
acc_train=accuracy.eval(feed_dict={X:X_batch,Y:Y_batch})
acc_test=accuracy.eval(feed_dict={X:mnist.test.images,Y:mnist.test.labels})
print(epoch+1,"Train acc:",acc_train,"Test acc:",acc_test)
save_path=saver.save(sess,"./my_model_final.ckpt") #保存模型数据
file_writer.close()
对比
准确率(橙线为He+ELU+BN+Adam+MaxNorm,蓝线为He+SELU+Adam+Dropout):
损失值(橙线为He+ELU+BN+Adam+MaxNorm,蓝线为He+SELU+Adam+Dropout):