(未完)Tensorflow,深度神经网络的优化与复用

,使用Tensorflow实现简单神经网络见前文,本文主要讲深度神经网络中常用的优化与加速技巧。

sigmoid函数下的梯度消失于梯度爆炸问题

由于sigmoid函数的自身缺陷:两段导数值小,中间导数值大,而其导数值最大也不超过0.25,所以在深度网络的反向传播算法中的逐级求导就出现了问题。考虑两种可能出现的极端情况:
- 导数值过小,由于网络层数而导致的导数累乘,最后求出来的低层网络导数值会过大,发生梯度爆炸
- 导数值过大,由于网络层数而导致的导数累乘,最后求出来的低层网络导数值会过小,发生梯度消失。

所以对使用梯度下降法的反向传播算法而言,激活函数sigmoid最理想的部分就是其中部的线性区域。

随机初始化

Glorot与Bengio在其论文中提出了一种在实际应用中能有效解决这个问题的方法:每一层的输出与输入必须是同方差的,并且前向传播与反向传播时梯度也是同方差的。初始权重值应当为均值为0, σ = 2 n i n p u t s + n o u t p u t s 的正态分布,这种策略称为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

R e L U ( x ) = m a x ( 0 , x )

  • 优点:导数易于计算,不是0就是1,并且没有上饱和区间
  • 缺点:存在不可导点,且有下饱和区域

LeakyReLU

L e a k y R e L U ( x ) = m a x ( α x , x )

其中 α 称为泄露系数,它指示函数在负区间的泄露程度。

  • 优点:导数易于计算,没有饱和区间
  • 缺点:存在不可导点
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)

E L U α ( x ) = { α ( e x p ( x ) 1 ) x < 0 x x >= 0

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是让模型去学习一个对每一层都最优的伸缩变换。整个操作如下式:

{ μ B = 1 m B i = 1 m B x ( i ) σ B 2 = 1 m B i = 1 m B ( x ( i ) μ B ) 2 x ^ ( i ) = x ( i ) μ B σ B 2 + ϵ z ( i ) = γ x ^ ( i ) + β

其中 γ 是伸缩系数, β 是变换系数, ϵ 是一个防止除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均值的老化系数,算法使用 θ ¯ ( c u r ) = θ ¯ ( p r e ) m o m e n t u m + θ ( c u r ) ( 1 m o m e n t u m ) 来计算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 β 1 m + ( 1 β 1 ) θ J ( θ ) s β 2 s + ( 1 β 2 ) θ J ( θ ) θ J ( θ ) m m 1 β 1 T s s 1 β 2 T θ θ η m s + ϵ

- m表示动量,它记录每次计算中的梯度值,并根据动量衰退系数 β 1 衰减,相当于计算梯度的均值
- 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这种本来就会在训练过程中调整学习率的算法,并不需要进行学习率调度。

避免过拟合

l 1 l 2 正则化

l 1 l 2 正则化的思想是将模型的权重范数加入到损失函数中,使得模型在学习中必须考虑模型的复杂度,从而避免了一味的追求低损失而导致的模型过于复杂。

实现时,需要在创建网络层时进行参数声明:

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的思想是在训练过程中,对每一层的神经元都以概率进行屏蔽,被屏蔽的神经元在此次传播中是失活的。假设神经元的保留概率为 k e e p _ p r o b ,则每个神经元被屏蔽的概率为 1 k e e p _ p r o b 。因为Dropout会改变整个网络最终输出的期望值,所以为了不改变原网络的输出期望,需要对网络进行补偿运算,可以用每层的权重乘以 k e e p _ p r o b ,也可以用每层的输出除以 k e e p _ p r o b

因为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):

猜你喜欢

转载自blog.csdn.net/qq_31823267/article/details/79508664