学习《TensorFlow实战Google深度学习框架》(三)神经网络优化方法

版权声明:本文由洛北辰南原创,转载请注明原创地址,谢谢。 https://blog.csdn.net/qq_18941713/article/details/88354226

4.3 神经网络优化算法

反向传播算法(BP,back propagation)和梯度下降算法(gradient decent)调整神经网络中参数的取值。
梯度下降算法主要用于优化单个参数的取值,而反向传播算法给出了一个高效的方式在所有参数上使用梯度下降算法从而使神经网络模型在训练数据上的损失函数尽可能小。

反向传播算法是训练神经网络的核心算法,它可以根据定义好的损失函数优化神经网络中参数的取值,从而使神经网络模型在训练数据集上的损失函数达到一个最小值。

神经网络模型中参数的优化过程直接决定了模型的质量,是使用神经网络时非常重要的一步。

假设用θ表示神经网络中的参数,J(θ)表示在给定的参数取值下,训练数据集上损失函数的大小,那么整个优化过程可以抽象为寻找一个参数θ,使得J(θ)最小。

梯度下降算法会迭代式更新参数θ,不断沿着梯度的反方向让参数朝着总损失更小的方向更新。
可以想象为,在山坡上,向山脚走去,每次都朝向最陡的方向移动。

每次移动的步幅,便是梯度下降的参数更新幅度,称为学习率η(learning rate)。

假设用梯度下降算法来优化参数x,使得损失函数J(x)=x²的值尽量小。梯度下降算法的第一步需要随机产生一个参数x的初始值,然后再通过梯度和学习率来更新参数x的取值。在这个样例中,参数x的梯度为
▽=∂J(x)/∂x=2x,那么使用梯度下降算法每次对参数x的更新公式为x(n+1)=x(n)-η▽

假设参数初始值为5,学习率为0.3,那么优化过程可以总结为下表
在这里插入图片描述
神经网络的优化过程分为两个步骤

  • 第一阶段,通过前向传播算法得到预测值,并将预测值和真实值做对比得出两者之间的差距。
  • 第二阶段,通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数。

梯度下降算法的问题:
梯度下降不能保证得到全局最优解,容易陷入局部最优解。
计算时间太长,每一轮迭代都要计算在全部训练数据上的损失函数。

随机梯度下降可以解决计算时间长的问题,因为它每次只选取随机一条数据计算损失函数,但问题也很明显它甚至无法达到局部最优。

实际应用中,一般采用SGD(随机梯度下降,stochastic gradient decent)和GD的折中,每次计算一小部分训练数据的损失函数。这一小部分数据被称之为batch。

4.4 神经网络进一步优化

  • 介绍通过指数衰减的方法设置梯度下降算法中的学习率。通过指数衰减的学习率既可以让模型在训练的前期快速接近较优解,又可以保证模型在训练后期不会有太大的波动,从而更加接近局部最优。
  • 介绍过拟合问题,过拟合的影响及其解决方案。
  • 介绍滑动平均模型。滑动平均模型会将每一轮迭代得到的模型综合起来,从而使得最终得到的模型更佳健壮(robust)。

4.4.1 学习率的设置

学习率决定了参数每次更新的幅度。如果幅度过大,那么可能导致参数在极优值的两侧来回移动。如果J(x)=x²的学习率为1,那么过程会如下表。
在这里插入图片描述
无论进行多少次迭代,参数都会在-5和5之间摇摆,不会收敛到一个极小值。相反,当学习率过小时,虽能保证收敛性,但是这会大大降低优化速度。比如当学习率为0.001时,迭代5次之后,x的值为4.95。要将x训练到0.05需要大约2300轮迭代;而当学习率为0.3时,只需5轮就可以达到。

综上所述,学习率既不能过大,也不能过小。为了解决设定学习率的问题,TensorFlow提供了一种更加灵活的学习率设置方法——指数衰减法

通过tf.train.exponential_decay函数会指数级地减小学习率。可以先使用较大学习率来快速得到一个比较优的解,然后随着迭代的继续逐步减小学习率,使得模型在训练后期更加稳定。

# exponential_decay完成了如下功能
decayed_learning_rate = \
	learning_rate * decay_rate ^ (global_step / decay_steps)
  • decay_learning_rate为每一轮迭代的学习率
  • learning_rate为事先设定的学习率
  • decay_rate为衰减系数
  • decay_steps为衰减速度

tf.train.exponential_decay可以通过设置staircase的值来选择不同的衰减方式,若为True,则global_step / decay_steps会被转化成整数。这使得学习率关于迭代轮数的图像成为一个阶梯函数(staircase function)

在这样的设置下,decay_steps通常代表了完整的使用一遍训练数据所需要的轮数。在这种场景下,每完整地过完一遍训练数据,学习率就衰减一次。

global_step = tf.Variable(0)

# 通过exponential_decay函数生成学习率
learning_rate = tf.train.exponential_decay(
	0.1, global_step, 100, 0.96, staircase=True)

# 使用指数衰减的学习率。在minimize函数中传入global_step将自动更新
# global_step参数,从而使得学习率也得到相应更新
learning_step = tr.train.GradientDescentOptimizer(learning_rate)\
				  .minimize(..my loss.., global_step=global_step)

这里的global_step=global_step,如果去掉,那么学习率不会变化,迭代轮数也不会增加,始终为0。
加上之后,更新了global_step参数,但是更新之后也是0。

一般来讲,实参传到其他函数就是形参,只有传地址,才能通过形参改变其数值。
这里大概看源码可以理解吧,我暂且理解为

  • 不更新global_step,学习率的迭代衰减便不会执行
  • 更新global_step,将参数内存传入底层,交由其他函数修改数据内容

如上代码设定了初始学习率为0.1,由于指定了staircase=True,所以每训练100轮后学习率乘以0.96。一般来说衰减系数是小于1的,否则学习会反向衰减。

4.4.2 过拟合问题

前面讲述了如何在训练数据上优化一个给定的损失函数。然而在真实的应用中想要的并不是让模型尽量模拟训练数据的行为,而是希望通过训练出来的模型对未知的数据给出判断。模型在训练数据上的表现并不一定代表了它在未知数据上的表现,

所谓过拟合,指的是当一个模型过于复杂之后,它可以很好地“记忆”每一个训练数据中随机噪音的部分而忘记了要去“学习”训练数据中通用的趋势。

举一个极端的例子,如果一个模型中的参数比训练数据的总数还要多,那么只要训练数据不冲突,这个模型完全可以记住所有的训练数据的结果从而使得损失函数为0。

可以直观的想象一个包含n个变量和n个等式的方程组,当方程不冲突时,这个方程组是可以通过数学的方法来求解的。

然而,过度拟合训练数据中的随机噪音虽然可以得到非常小的损失函数,但是对于未知数据可能无法做出可靠的判断。
在这里插入图片描述

避免过拟合的方法,正则化(regularization)。正则化的思想就是在损失函数中加入刻画模型复杂程度的指标。假设用于科化模型在训练数据上表现的损失函数为J(θ),那么在优化时不是直接优化J(θ),而是优化J(θ)+λR(w)。其中R(w)刻画的是模型的复杂程度,而λ表示模型复杂损失在总损失中的比例。

通常刻画模型复杂程度的函数R(w)有两种

  • L1正则化
    R(w)=权重参数w的绝对值之和
  • L2正则化
    R(w)=权重参数w的平方和

L1正则化会让小权重变为0,使权重矩阵变得稀疏。
而L2正则化只会让小权重趋于0,因为小权重的平方和,可以忽略。

L1正则化不可导,在优化过程中,需要计算损失函数的偏导数,所以用L1正则化的损失函数会比较复杂。
L2正则化就不会这样,它是可导的。

在实践中,也可以将L1正则化和L2正则化同时使用:R(w)=∑α|w|+(1-α)w²

TensorFlow也可以优化带正则化的损失函数,以下代码给出了一个简单的带L2正则化的损失函数定义。

w = tf.Variable(tf.random_normal([2, 1], stddev=1, seed=1))
y = tf.matmul(x, w)

loss = tf.reduce_mean(tf.square(y_ - y)) +
	   tf.contrib.layers.l2_regularizer(lambda)(w)

上述损失函数,包括两部分

  • 第一部分是均方误差函数(预测值与真实值的差的平方的平均值)
  • 第二部分就是L2正则化,lambda为公式中的λ,w是需要计算正则化损失的权重参数

TensorFlow提供了tf.contrib.layers.l2_regularizer方法,它可以返回一个函数,这个函数计算了给定参数的L2正则化项的值。同样的也有计算L1正则化项的方法。

weights = tf.constant([[1.0, -2.0], [-3.0, 4.0]])
with tf.Session() as sess:
	print(sess.run(tf.contrib.layers.l1_regularizer(.5)(weights)))
	print(sess.run(tf.contrib.layers.l2_regularizer(.5)(weights)))
# output
5.0	(|1|+|-2|+|-3|+|4|)*0.5 = 5
7.5 ((1)²+(-2)²+(-3)²+4²)*0.5 = 7.5

当网络结构变复杂了,可能网络结构部分和计算损失函数部分不在同一个函数中,这样按照上述代码就不方便计算了。可以使用TensorFlow提供的集合(collection)。它可以在一个计算图(tf.Graph)中保存一组实体(比如张量)。

以下代码给出了通过集合计算一个5层神经网络带L2正则化损失函数的计算方法,代码只是定义没有输出。

# -*- coding: utf-8 -*-
# @Time    : 2019/3/13 10:02
# @Author  : Chord

import tensorflow as tf;

# 获取一层神经网络边上的权重,并将这个权重的L2正则化损失加入名称为'losses'的集合中
def get_weight(shape, lambda1):
    # 生成一个变量
    var = tf.Variable(tf.random_normal(shape), dtype = tf.float32)
    # add_to_collection函数将这个新生成变量的L2正则化损失项加入集合
    # 这个函数的第一个参数'losses'是集合的名字,第二个参数是要加入这个集合的内容
    tf.add_to_collection(
        'losses', tf.contrib.layers.l2_regularizer(lambda1)(var)
    )
    # 返回生成的变量
    return var

if __name__ == "__main__":
    x = tf.placeholder(tf.float32, shape=(None, 2))
    y_ = tf.placeholder(tf.float32, shape=(None, 1))
    batch_size = 8

    # 定义了每一层网络中节点的个数
    layer_dimension = [2, 10, 10, 10, 1]
    # 神经网络的层数
    n_layers = len(layer_dimension)

    # 这个变量维护前向传播时最深层的节点,开始的时候就是输入层
    cur_layer = x
    # 当前层节点个数
    in_dimension = layer_dimension[0]

    # 通过一个循环来生成5层全连接的神经网络结构
    for i in range(1, n_layers):
        # layer_dimension[i]为下一层节点个数
        out_dimension = layer_dimension[i]
        # 生成当前层中权重的变量,并将这个变量的L2正则化损失加入计算图上的集合
        weight = get_weight([in_dimension, out_dimension], 0.001)
        bias = tf.Variable(tf.constant(0.1, shape=[out_dimension]))
        # 使用ReLU激活函数
        cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight) + bias)
        # 进入下一层之前将下一层的节点个数更新为当前层节点个数
        in_dimension = layer_dimension[i]

    # 在定义神经网络前向传播的同时已经将所有的L2正则化损失加入了图上的集合
    # 这里只需要计算刻画模型在训练数据上的表现的损失函数
    mse_loss = tf.reduce_mean(tf.square(y_ - cur_layer))

    # 将均方误差损失函数加入损失集合
    tf.add_to_collection('losses', mse_loss)

    # get_collection返回一个列表,这个列表是所有这个集合中的元素。在这样例中,
    # 这些元素就是损失函数的不同部分,将它们加起来就可以得到最终的损失函数
    loss = tf.add_n(tf.get_collection('losses'))

4.4.3 滑动平均模型

在采用随机梯度下降算法训练神经网络时,使用滑动平均模型在很多应用中都可以在一定程度上提高最终模型在测试数据上的表现。

在TensorFlow中提供了tf.train.ExponentialMovingAverage来实现滑动平均模型,初始化时提供一个衰减率decay,这个衰减率将用于控制模型更新的速度。

ExponentialMovingAverage对每一个变量会维护一个影子变量(shadow variable),这个影子变量的初始值就是对应变量的初始值,而每次变量更新时,影子变量的值会更新为:
shadow_variable = decay×shadow_variable+(1-decay)×variable

其中shadow_variable为影子变量,variable为待更新的变量,decay为衰减率
。从公式中可以看到,decay决定了模型更新的速度,decay越大模型越趋于稳定。

在实际应用中,decay一般会设定为非常接近1的数(比如0.999)为了使模型在训练前期更新的更快,ExponentialMovingAverage还提供了num_updates参数来动态设置decay的大小。
如果在ExponentialMovingAverage初始化时提供了num_updates参数,那么每次使用的衰减率将是:
min{decay,(1+num_updates)/(10+num_updates)}
下面通过一段代码来解释ExponetialMovingAverage是如何被使用的

# -*- coding: utf-8 -*-
# @Time    : 2019/3/13 10:54
# @Author  : Chord

import tensorflow as tf;

if __name__ == "__main__":
    # 定义一个变量用于计算滑动平均,这个变量的初始值为0.注意这里手动指定了变量
    # 的类型为tf.float32,因为所有需要计算滑动平均的变量必须是实数型
    v1 = tf.Variable(0, dtype=tf.float32)
    # 这里step变量模拟神经网络中迭代轮数,可以用于动态控制衰减率
    # 如果为True,会把它加入到GraphKeys.TRAINABLE_VARIABLES,才能对它使用Optimizer
    step = tf.Variable(0, trainable=False)

    # 定义一个滑动平均的类(class)。初始化时给定了衰减率(0.99)和控制衰减率的变量step
    ema = tf.train.ExponentialMovingAverage(0.99,step)
    # 定义一个更新变量滑动平均的操作。这里需要给定一个列表,每次执行这个操作时
    # 这个列表中的变量都会被更新
    maintain_averages_op = ema.apply([v1])

    with tf.Session() as sess:
        # 初始化所有变量
        init_op = tf.global_variables_initializer()
        sess.run(init_op)

        # 通过ema.average(v1)获取滑动平均之后的变量的取值。在初始化之后变量v1的值和v1的滑动平均都为0
        print(sess.run([v1, ema.average(v1)]))

        # 更新变量v1的值到5
        sess.run(tf.assign(v1, 5))
        # 更新v1的滑动平均值。衰减率为min{0.99,(1+step)/(10+step)=0.1}=0.1
        # 所有v1的滑动平均会被更新为0.1*0+0.9*5=4.5
        sess.run(maintain_averages_op)
        print(sess.run([v1, ema.average(v1)]))
        # 输出[5.0, 4.5]

        # 更新step的值为10000
        sess.run(tf.assign(step, 10000))
        # 更新v1的值为10
        sess.run(tf.assign(v1, 10))
        # 更新v1的滑动平均值。衰减率为min{0.99,(1+step)/(10+step)≈0.999}=0.99
        # 所以v1的滑动平均会被更新为0.99*4.5+0.01*10=4.555
        sess.run(maintain_averages_op)
        print(sess.run([v1, ema.average(v1)]))
        # 输出[10.0, 4.5549998]

        # 再次更新滑动平均值,得到的新滑动平均值为0.99*4.555+0.01*10=4.60945
        sess.run(maintain_averages_op)
        print(sess.run([v1, ema.average(v1)]))
        # 输出[10.0, 4.6094499]
        
	# output
	[0.0, 0.0]
	[5.0, 4.5]
	[10.0, 4.555]
	[10.0, 4.60945]

猜你喜欢

转载自blog.csdn.net/qq_18941713/article/details/88354226