第四章:深层神经网络
* 4.1
深度学习与深层神经网络
* 4.1.1
线性模型的局限性
* 4.1.2
激活函数实现去线性化
* 4.1.3
多层网络解决异或运算
* 4.2
损失函数定义
* 4.2.1
经典损失函数
* 4.2.2
自定义损失函数
* 4.3
神经网络优化算法
* 4.4
神经网络进一步优化
* 4.4.1
学习率的设置
* 4.4.2
过拟合问题
* 4.4.3
滑动平均模型
在这一章中,将进一步介绍如何设计和优化神经网络,
使它能够更好地对未知 的样本进行预测。
-4.1- 深度学习与深层神经网络
本节中,将介绍
深度学习
与深层神经网络
的概念,
并给出样例来说明深层神经网络
可以解决部分浅层神经网络
解决不了的问题。
* 深度学习就是深层神经网络的代名词
* 深度学习有两个非常重要的特性一一多层和非线性
-4.1.1- 线性模型的局限性
本节将先介绍
线性变换
存在的问题
以及为什么要在深度学习的定义中强调“复杂 问题”
如果知识简单的线性前向传播,可以看到,虽然这个神经网络有两层(不算输入层),但是它和单层的神经网络井没有区别。
以此类推,只通过线性变换,任意层的全连接神经网络和单层神经网络模型的表达能力没有任何区别,而且它们都是线性模型 。
然而线性模型能够解决的问题
是有限的
,这就是线性模型最大的局限性,也是为什么深度学习要强调非线性。
上图中,正负样本的划分是线性可分
的,可以看到,线性模型
能够很好地区分正负样本。
上图中,正负样本的划分是线性不可分
的,会发现,为了较好地区分正负样本,需要使用非
线性模型。在这个样例中使用了 ReLU激活函数
。 使用其他非线性激活函数
也可以得到类似的效果。
-4.1.2- 激活函数实现去线性化
本节中,将介绍如何实现
去线性化
并给出 TensorFlow程序来实现去线性化的功能。
-4.1.3- 多层网络解决异或运算
本节将介绍具体样例来说明
深层网络
比浅层网络
可以解决更多的问题。
-4.2- 损失函数定义
本节中,将介绍如何设定神经网络的
优化目标
。这个优化目标也就是损失函数
。
这一节将分别介绍分类
问题和回归
问题中比较常用的几种损失函数。
除使用经典的损失函数
,在这一节中将给出一个样例来讲解如何通过对损失函数的设置
,使神经网络优化的目标更加接近实际问题的需求。
-4.2.1- 经典损失函数
* 分类问题常用的损失函数
如何判断一个输出向量和期望的向盘有多接近呢? 交叉熵(cross entropy)是常用的评判方法之 一 。交叉熵刻画了两个概率分布之间的距离, 它是分类问题中使用 比较广的一种损失函数。
如何将神经网络前向传播得到的结果也变成概率分布呢? Softmax 回归就是一个非常常用的方法 。
交叉熵函数不是对称的(),它刻画的是 通过概率分布 q 来表达概率分布 p 的困难程度。
在 3.4.5节中,已经通过 TensorFlow实现过交叉熵,其代码实现如下:
import tensorflow as tf
cross_entropy = -tf . reduce_mean(
y_ * tf.log(tf.clip_by_value(y, le-10, 1.0))
)
其中:
*
tf.clip_by_value()函数
可以将一个张量中 的 数值限制在一个范围之内,这样可以避免一些运算错误(比如 log0 是无效的)。
*
tf.log()函数
完成了对张量中所有元素依次求对数的功能。
*
*乘法
,注意⚠️两个矩阵通过"*"相乘不是矩阵乘法,而是元素之间直接相乘
。即:⚠️
*
tf.reduce_mean()函数
根据交叉熵的公式,应该将每行中的 m 个结果相加得到所有样例的 交叉熵,然后再对这 n 行取平均得到 一个 batch 的平均交叉熵。但因为分类问题的类别数 量是不变的,所以可以直接对整个矩阵做平均而并不改变计算结果的意义 。
因为交叉熵一般会与 softmax 回归一起使用,所以 TensorFlow 对这两个功能进行了统 一封装,并提供了 tf.nn.softmax_cross_entropy_with_logits()函数
。
比如可以直接通过以下代 码来实现使用了 softmax 回归之后的交叉熵损失函数:
import tensorflow as tf
cross entropy= tf.nn.softmax_cross_entropy_with_logits( labels=y , logits=y)
在只有一个正确答案的分类问题中,TensorFlow 提供了 tf.nn.sparse_softmax_cross_entropy_with_logits() 函数
来进一步加速
计算过程。
在第 5 章中将提供使用这个函数的完整样例。
* 回归问题常用的损失函数
对于回归 问题,最常用的损失函数是均方误差(MSE, mean squared error)
。
以下代 码展示了如何通过 TensorFlow 实现均方误差损失函数 :
import tensorflow as tf
mse = tf.reduce_mean(tf.square(y_ - y))
其中 y 代表了神经网络的输出答案, y_代表了标准答案 。
类似 4.2.1 节中介绍的乘法操作,这里的减法运算“-”也是两个矩阵中对应元素的减法。
-4.2.2- 自定义损失函数
比如如果一个商品的成本是 1元,但是利润是 10 元,那么:
*
少预测一 个就少挣 10 元;
*
而多预测一个才少挣 1 元。
如果神经网络模型最小化的是均方误差,
那么 很有可能此模型就无法最大化预期的利润 。
为了最大化预期利润,需要将损失函数和利润 直接联系起来 。
在 TensorFlow 中,可以通过以下代码来实现这个损失函数:
import tensorflow as tf
loss= tf.reduce sum(tf . where(tf.greater(v1, v2) ,(v1 - v2) * a, (v2 - v1) * b))
注意⚠️在TF中,除了tf.matmul()表示的是矩阵乘法,其他+
、-
、*
、/
代表的都是element-wise级别的操作⚠️
以上代码用到了 tf.greater和 tf.where来实现选择操作。
*
tf.greater()
的输入是两个张量,此函数会比较这两个输入张量中每一个元素的大小,并返回比较结果。 当 tf.greater()的输入张量维度不一样时,TensorFlow会进行类似 NumPy广播操作( broadcasting)的处理。
*
tf.where()
函数有三个参数。第一个为选择条件
根据,当选择条件为True时,tf.where() 函数会选择第二个参数中的值 , 否则使用第三个参数中的值。注意 tf.where 函数判断和选择都是在元素级别
进行
以下代码展示了 tf.where() 函数和 tf.greater() 函数的用法 :
import tensorflow as tf
v1 = tf.constant([1.0, 2.0, 3.0, 4.0])
v2 = tf.constant([4.0, 3.0, 2.0 ,1.0])
sess = tf.IneractiveSession()
# 不需要再指定default_session
print(tf.greater(v1, v2).eval())
# 输出:[False False True True]
print(tf.where(tf.greater(v1, v2), v1, v2).eval())
# 输出:[4. 3. 3. 4.]
sess.close()
* 损失函数对模型训练结果的影响
在下面这个程序中 , 实现了一个拥有两个输入节点、 一个输出节点,没有隐藏层的神经网络。这个程序的主体流程和 3.4.5节中给出来的样例基本一致,但用到了上面定义的有权重的损失函数
。
import tensorflow as tf
from bumpy.random import RandomState
batch_size = 8
# 两个输入节点
x = tf.placeholder(tf.float32, shape=[None, 2], name="calculate_node_placholder_input_x")
# 一个输出节点,回归问题一般只有一个输出节点
y_ = tf.placeholder(tf.float32, shape=[None, 1], name="calculate_node_placeholder_input_y")
# 定义一个单层的神经网络前向传播过程
# 这里就是简单的加权和
w1 = tf.Variable(tf.random_normal(shape=[2, 1], stddev=1, seed=1))
y = tf.matmul(x, w1)
# 定义预测多了or少了的成本、损失函数
loss_less = 10
loss_more = 1
loss = tf.reduce_mean(tf.where(
tf.greater(y, y_),
(y - y_) * loss_more,
(y_ - y) * loss_less
))
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)
# 通过随机数生成一个模拟数据集
rdm = RandomState(1)
dataset_size = 128
X = rum.rand(dataset_size, 2)
# 设置回归的正确值为 两个输入的和 + 一个随机量
# 之所以要加上一个随机量是为了加入不可预测的噪音
# 否则不同损失函数的意义就不大了
# 因为不同的损失函数都能会在能完全预测正确的时候最低
# 一般来说,噪音为一个均值为0的小量
# 所以这里的噪音为 -0.5 ~ +0.5 的随机数
Y = [[x1+x2+rdm.rand()/10.0-0.5] for (x1, x2) in X]
# 训练神经网络
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
STEPS = 5000
for i in range(STEPS):
start = (i * batch_size) % dataset_size
end = min(start+batch_size, dataset_size)
sess.run(
train_step,
feed_dict = {x: X[start:end], y:Y[start:end]}
)
# 经过STEP步batch迭代之后
print(sess.run(w1))
*
运行以上代码会得到w1的值为[1.01934695, 1.04280889],也就是说得到的预测函数是,这其实是要比大的,因为在损失函数中,有指定预测少了的损失更大,所以训练出来的模型会倾向于预测更多。
*
如果将loss_less的值调整为1,loss_more的值调整为10,这样的话训练出来的权重矩阵为[0.95525807, 0.9813394],即模型更倾向于预测少一点。
*
而如果使用均方误差作为损失函数,那么迭代之后的w1会是[0.97437561, 1.0243336]。使用mse作为损失函数,会尽量让预测值离标准答案更近。
通过这个样例可以看出,设计不同的损失函数,会对模型产生重要影响。
-4.3- 神经网络优化算法
本节将更加详细地介绍神经网络的反向传播算法,
并且给出一个TensorFlow框架来实现反向传播的过程。
本节将更加具体地介绍如何通过
反向传播算法( backpropagation)
和梯度下降算法 (gradient decent)
调整神经网络中参数的取值。
*
梯度下降算法
主要用于优化单个参数
(loss)的取值,
*
而反向传播算法
给出了一个高效的方式在所有参数
(模型中待训练参数矩阵)上使用梯度下降算法,从而使神经网络模型在训练数据上的损失函数尽可能小。
反向传播算法是训练神经网络的核心算法,它可以根据定义好的
损失
函数优化神经网络中参数
的取值,从而使神经网络模型在训练数据集上的损失函数达到一个较小值。
神经网络模型中参数的优化过程直接决定了模型的质量,是使用神经网络时非常重要的 一步。
在本节中,将主要介绍神经网络优化过程的基本概念和主要思想,而略去算法的数学推导和证明 。
本节将给出一个具体的样例来解释使用梯度 下降算法优化参数取值的过程。
*
假设用 θ表示神经网络中的参数, J(θ)表示在给定的参数取值下,训练数据集上损失函数的大小,
*
那么整个优化过程可以抽象为寻找一个参数 θ ,使得 J(θ)最小。
*
因为目前没 有一个通用的方法可以对任意损失函数直接求解最佳的参数取值,所以在实践中,梯度下降算法是最常用的神经网络优化方法。
*
梯度下降算法会以步步迭代
的方式更新参数 θ ,不断沿着梯度的反方向
让参数朝着总损失更小的方向更新。
参数的
梯度
可以通过求偏导的方式计算
有了梯度,还需要定义一个学习率η(learningrate)
来计算出每次参数更新的幅度。
神经网络的优化过程可以分为两个阶段:
*
第一个阶段先通过前向传播算法计算得到预测值,井将预测值和真实值做对比得出两者之间的差距。
*
然后在第二个阶段通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数。
此处略去反向传播算法具体的实现方法和数学证明,有兴趣的同学可以参考 David Rumelhart、Geoffrey Hinton和 Ronald Williams教授发表的论文 Learning representations by back-propagαtingerrors。
* 局部最优解问题
需要注意的是,梯度下降算法并不能保证被优化的函数达到
全局最优
解。
在训练神经网络时,参数的初始值会很大程度影响最后得到的结果 。
只有当损失函数为凸函数
时,梯度下降算法才能保证达到全局最优解。
* 迭代耗时,计算成本高
除了不一定能达到全局最优 ,梯度下降算法的另外一个问题就是计算时间太长 。 因为 要在全部训练数据上最小 化损 失,所以损失函数 冽的 是在所有训练数据上的损失和 。 这样 在每一轮迭代中都需要计算在全部训练数据上的损失函数 。 在海量训练数据下,要计算所 有训练数据的损失函数是非常消耗时间的。
为了加速训练过程,可以使用随机梯度下降的算法 Cstochastic gradient descent)。
这个算法优化的不是在全部训练数据上的损失函数,
而是:
在每一轮法代中,
(只读入一条样本?还是读入一个batch的样本?)
随机
优化某一条
训练数据上的损失函数 。
这样每一轮参数更新的速度就大大加快了。
因为随机梯度下降算法每次优化的只是某一条数据上的损失函数,所以它 的问题也非常明显 :
在某 一 条数据上损失函数更小并不代表在全部数据上损失函数更小, 于是使用随机梯度下降优化得到的神经网络甚至可能无法达到局部最优 。
为了综合梯度下降算法和随机梯度下降算法的优缺点,在实际应用中 一般采用这两个 算法的折中一一每次计算一小部分训练数据的损失函数。
这一小部分数据被称之为一个 mini-batch。
通过矩阵运算,每次在 一个 mini-batch 上优化神经网络的参数并不会比单个数据慢太多 。
另一方面,每次使用一个 mini-batch 可以大大减小收敛所需要的迭代次数,同时可以使收敛到 的结果更加接近梯度下降的效果。
* 批量梯度下降(BGD)、随机梯度下降(SGD)以及小批量梯度下降(MBGD)
一次迭代计算loss时,用到的样本条数
*
批量梯度下降(BGD)
:
*
随机梯度下降(SGD)
:
*
小批量梯度下降(MBGD)
:
* GradientDescentOptimizer、AdamOptimizer、MomentumOptimizer
一次迭代计算完本次loss之后,沿梯度反方向更新参数时的步长计算
*
GradientDescentOptimizer
:使参数沿着 梯度的反方向,即总损失减小的方向移动,以固定学习率更新参数;
*
AdamOptimizer
:通过计算梯度的一阶矩估计和二阶矩估计而为不同的参数设计独立的自适应性学习率;
*
MomentumOptimizer
:更新参数时,利用了超参数,在梯度下降较慢的地方,加大参数更新的幅度。
以下代码给出了在 TensorFlow 中如何实现神经网络的训 练过程。在本书的所有样例中 ,神经网络的训练都大致遵循以下过程 :
import tensorflow as tf
batch_size = n
# 每次读取一小部分数据作为当前的训练数据来执行反向传播算法
x = tf.placeholder(tf.float32, shape=[batch_size, 2], name="calculate_node_x_input")
y_ = tf.placeholder(tf.float32, shape=[batch_size, 1], name="calculate_node_y_input")
# 定义神经网络结构和优化算法
loss = ...
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)
# 训练神经网络
with tf.Sessio() as sess:
# 参数初始化
...
for i in range(STEPS):
# 准备 batch_size个训练数据。
# 一般将所有训练数据随机打乱之后再选取
# 可以取到更好的优化效果
current_x, current_y = ...
sess.run(train_step, feed_dict={x: current_X, y_: current_Y})
-4.4- 神经网络进一步优化
本节将介绍在神经网络优化中经常遇到的几个问题,
井且给出解决这些问题的具体方法。
本节将紧接上节,继续介绍的神经网络优化过程中可能遇到的
问题
和解决方法
,掌握本节内容可以帮助读者更好地理解这些优化方法 。
本节将继续介绍神经网络优化过程中可能遇到的一些问题,以及解决这些问题的常用方法。
-4.4.1- 学习率的设置
本节将介绍通过指数衰减的方法设置梯度 下降算法中的学习率。
通过指数衰减的学习率
既可以让模型在训练的前期快速接近较优解,
又可以保证模型在训练后期不会有太大的波动,从而更加接近局部最优。
4.3节介绍了在训练神经网络时, 需要设置学习率(learningrate)控制参数更新的速度。 本节将进一步介绍如何设置学习率。
学习率
决定了参数每次更新的幅度
。
如果幅度过大, 那么可能导致参数在极优值的两侧来回移动。
相反,当学习率过小时,虽然能保证收敛性,但是这会大大降低优化速度 。会需要更多轮的法代才能达到一个比较理想的优化效果。
综上所述,学习率既不能过大,也不能过小。为了解决设定学习率的问题,TensorFlow提供了一种更加灵活的学习率设置方法一一指数衰减法
。
tf.train.exponential_decay()
函数实现了指数衰减学习率。通过这个函数,可以
*
先使用较大的学 习率来快速得到一个比较优的解,
*
然后随着迭代的继续逐步减小学习率,使得模型在训练后 期更加稳定。
exponential_decay()
函数会指数级地减小学习率,它实现了以下代码的功能:
import tensorflow as tf
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)
其中:
*
decayed_learning_rate
为每一轮优化时使用的学习率
*
learning_rate
为事先设定的初始学习率
*
decay_rate
为衰减系数
*
decay_steps
为衰减速度
图 4-13 显示了随着迭代轮数的增加,学习率逐步降低的过程。
tf.train.exponential_decay()函数
可以通过设置参数 staircase
选择不同的衰减方式。
*
staircase 的默认值为False
,这时学习率随迭代轮数变化的趋势如图中灰色曲线所示。
*
当 staircase 的值被设置为True
时,global_step/decay_steps 会被转化成整数。这使得学习率成为一个阶梯函数( staircase function)。图中黑色曲线显示了阶梯状的学习率。
在这样的设置下,decay_steps通常代表了完整的使用一遍训练数据所需要的迭代轮数。
这个迭代轮数也就是总训练样本数除以每一个 batch 中的训练样本数 。
这种设置的常用场景是每完整地过完一遍训练数据,学习率就减小一次。
这可以使得训练数据集中的所有数据对模型训练有相等的作用。
当使用连续的指数衰减学习率时,不同的训练数据有不同的学习率,而当学习率减小时,对应的训练数据对模型训练结果的影响也就小了。
下面给出了一段代码来示范如何在TensorFlow中使用tf.train.exponential_decay()
函数:
import tensorflow as tf
global_step = tf.Variable(initial_value=0)
# 通过exponential_decay() 函数生成学习率:
learning_rate = tf.train.exponential_decay(
0.1, global_step, 100, 0.96, staircase=True
)
# 使用指数衰减的学习率,
# 在minimize()函数中加入global_step将自动更新global_step这个tf.Variable
# 从而使学习率得到相应的更新
learning_step = tf.train.GradientDecentOptimizer(learning_rate).minimize(loss, global_step=global_step)
上面这段代码中设定了初始学习率为0.1,因为指定了staircase=True
,所以每训练100轮后,学习率衰减0.96倍。
一般来说,初始学习率
、衰减系数
、衰减速度
都是根据经验设置的。而且损失函数
下降的速度和迭代结束之后总损失
的大小没有必然的联系,而仅仅是影响迭代的效率、耗时。也就是说并不能通过训练过程中最开始前几轮损失函数下降的速度
来判断不同神经网络模型的好坏。
-4.4.2- 过拟合问题
本节将介绍过拟合问题。
在训练复杂神经网络模型时,过拟合是一个非常常见的问题。
这一节将具体介绍这个问题的影响以及解决这个问题的主要方法。
模型在训练数据上的表现并不一定代表了它在未知数据上的表现。
本节将介绍的过拟合问题就是可以导致这个差距的一个很重要因素。
在真实的应用中,训练模型想要的:
*
并不是让模型尽量模拟训练数据
的行为,
*
而是希望通过训练出来的模型对未知的数据
给出判断。
所谓过拟合
,指的是当一个模型过为复杂之后,它
*
可以很好地“记忆”每一个训练数据
中随机噪音的部分
*
而忘记 了要去“学习”训练数据中通用的趋势
。
举一个极端的例子,如果一个模型中的参数个数
比训练样本条数
还多,
那么只要训练数据不冲突,这个模型完全可以记住所有训练数据的结果从而使得损失函数为0。
然而,过度拟合训练数据中的随机噪音虽然可以得到非常小的损失函数,
但是对于未知数据可能无法做出可靠的判断。
图 4-14显示了模型训练的三种不同情况。
*
在第一种情况下,由于模型过于简单,无法刻画问题的趋势。
*
第二个模型是比较合理的,它既不会过于关注训练数据中的噪音,又能够比较好地刻画问题的整体趋势。
*
第三个模型就是过拟合了,虽然第三个模型完美地划分了不同形状的点,但是这样的划分并不能很好地对未知数据做出判断,因为它过度拟合了训练数据中的噪音而忽视了问题的整体规律。比如图中浅色方框“口”更有可能和“ X” 属于同 一类,而不是根据图上的划分和“ 。”属于同 一类。
* 正则化(regularization)
为了避免过拟合问题,一个非常常用的方法是正则化( regularization)
。正则化的思想就是在损失函数中加入刻画模型复杂程度的指标。
假设用于刻画模型在训练数据上表现的损失函数
为
那么在优化时,不是直接优化J(θ),而是优化 J(θ)+λR(w) 。其中:
*
θ
是一个神经网络中所有的参数,包括边上权重w和偏置项b
*
w
是边上的权重
*
R(w)
刻画的是模型的复杂程度
*
λ
表示模型复杂损失在总损失中的比例
一般来说
模型复杂度
只由权重w
决定 。
常用的刻画模型复杂度的函数 R(w)有两种:
*
一种是 L1 正则化,计算公式是:
*
另一种是 L2 正则化,计算公式是:
无论是哪一种正则化方式,基本的思想都是希望通过限制权重的大小,使得模型不能任意拟合训练数据中的随机噪音。
但这两种正则化的方法也有很大的区别:
*
首先,L1正则化
会让参数变得更稀疏
,而L2正则化
不会。
所谓参数变得更稀疏是指:会有更多的参数变为0,这样可以达到类似特征选取
的功能。
之所以L2正则化不会让参数变得稀疏的原因是:当参数很小时,比如 0.001,这个参数的平方基本上就可以忽略了,于是模型不会进一步将这 个参数调整为0。
*
其次,LI正则化
的计算公式不可导
,而L2正则化
公式可导
。
因为在优化时
需要计算损失函数的偏导数
,所以对含有L2正则化损失函数的优化要更加简洁。优化带LI正则化的损失函数要更加复杂,而且优化方法也有很多种。
*
在实践中,也可以将L1正则化
和L2正则化
同时使用:
4.2 节提到过 TensorFlow 可以优化
任意形式的损失函数
,所以 TensorFlow 自然也可以优化带正则化的损失函数。
以下代码给出了 一个简单的带 L2 正则化的损失函数定义 :
# 此处以【回归任务】的MSE损失为例
import tensorflow as tf
w = tf.Variable(initial_value=tf.random_normal([2, 1], stddev=2, seed=1))
y = tf.matmul(x, w)
loss = tf.reduce_mean(tf.square(y_ - y)) + tf.contrib.layers.l2regularizer(lambda)(w)
在以上程序中,loss
为定义的损失函数,它由两个部分组成:
*
第一个部分是 4.2.1 节中介绍的均方误差损失函数
,它刻画了模型在训练数据上的表现(这里举例是以回归场景为例,所以选取了MSE这个损失计算函数)。
*
第二个部分就是正则化
,它防止模型过度模拟训练、数据中的随机噪音。其中:
lambda
参数是公式 J(θ)+λR(w)
中的λ
,表示正则化项在loss中的权重。
w
为需要参与计算正则化损失的模型参数。
TensorFlow 提供了
tf.contrib.layers.l2_regularizer()
函数,
它可以返回一个函数
,这个函数可以计算一个给定参数 的L2正则化项的值。
类似的,tf.contrib.layers.l1_regularizer()
可以计算L1 正则化项的值。
以下代码给出了使用这两个函数的样例:
import tensorflow as tf
weights = tf.constant([[1.0, -2.0], [-3.0, 4.0]])
with tf.Session() as sess:
# 输出为 (|1| + |-2| + |-3| + |4|) x 0.5 = 5。
# 其中 0.5 为正则化项的权重
print(sess.run(
tf.contrib.layers.l1_regularizer(.5)(weights)
))
# 输出为 (1^2+ (-2)^2+ (-3)^2+4^2)/2 × 0.5 = 7.5
# 为什么要 /2 ???
print(sess.run(
tf.contrib.layers.l2_regularizer(.5)(weights)
))
在简单的神经网络中,这样的方式就可以很好地计算
带正则的损失函数
了。但当神经网络的参数增多之后,这样的方式:
*
首先可能导致损失函数 loss的定义很长,可读性差
且容易出错
。
*
但更主要的是,当网络结构复杂之后,定义网络结构的代码
部分和计算损失函数的代码
部分可能不在同一个函数中,这样通过变量这种方式计算损失函数就不方便了。
*集合(Collection)
为了解决这个问题,可以使用 TensorFlow 中提供的集合(Collection)。
集合的概念在3.1节
中介绍过,它可以在一个计算图(tf.Graph)中保存一组实体(比如张量)。
以下代码给出了通过集合(Collection)计算一个5层神经网络带 L2 正则化的损失函数的计算方法:
import tensorflow as tf
# 获取一层神经网络边上的权重
# 并将这个权重的L2正则化损失加入名称为'losses'的集合(Collection)中
def get_weight(shape, lambda):
# 生成一个变量
var = tf.Variable(
initial_value=tf.random_normal(shape),
dtype=tf.float32
)
# add_to_collection()函数将这个新生成的变量的L2正则化损失项加入集合
# 这个函数的第一个参数'losses'是集合的名字,
# 第二个参数是要加入这个集合的内容
tf.add_to_collection(
'losses',
tf.contrib.layers.l2_regularizer(lambda)(var)
)
# 返回生成的变量
return var
x = tf.placeholder(dtype=tf.float32, shape=(None, 2))
y_ = tf.placeholder(dtype=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(n_layers):
# layer_dimension[i]为下一层的节点数
out_dimension = layer_dimension[i]
# 生成当前层中的权重变量
# 并将这个变量的L2正则化损失加入计算图上的'losses'集合(Collection)
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 # 这里是element-wise的加
)
# 进入下一层之前,
# 将下一层的节点个数更新为当前层的节点个数
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') # 返回这个名为'losses'的Collection集合中的各个L2正则项
)
从以上代码可以看出通过使用集合的方法在网络结构比较复杂的情况下可以使代码的可读性更高 。
以上代码给出的是一个只有 5 层的全连接网络 ,
在更加复杂的网络结构中,使用这样的方式来计算损失函数将大大增强代码的可读性。
-4.4.3- 滑动平均模型
本节将介绍
滑动平均模型
。
滑动平均模型会将每一轮迭代得到的模型综合起来,从而使得最终得到的模型更加健壮(robust)。
这一个节将介绍另外一个可以便模型在测试数据上更健壮(robust)的方法一一滑动平均模型。
在采用随机梯度下降算法训练神经网络时,使用滑动平均模型在很多应用中都可以在一定程度提高最终模型在测试数据上的表现。
而每次运行变量更新时,影子变量的值会更新为 :在TensorFlow中提供了
tf.train.ExponentialMovingAverage
来实现滑动平均模型。
在初始化ExponentialMovingAverage
时,需要提供一个衰减率( decay)
。
这个衰减率将用于控制模型更新的速度。
ExponentialMovingAverage
对每一个变量会维护一个影子变量( shadow variable)
,
这个影子变量的初始值就是相应变量的初始值,
其中:
*
shadow_variable
为影子变量
*
variable
为待更新的变量
*
decay
为衰减率
如果在 ExponentialMovingAverage 初始化时提供了 num_updates参数,那么每次使用的衰减率将是 :从公式中可以看到,decay决定了模型更新的速度
decay越大,模型越趋于稳定
在实际应用中,decay一般会设成非常接近1的数,比如0.999或者0.9999
为了使模型在训练前期可以更新得更快,ExponentialMovingAverage
还提供了num_updates参数
来动态
设置decay的大小。
下面通过一段代码来解释 ExponentialMovingAverage是如何被使用的:
import tensorflow as tf
# 定义一个变量用于计算滑动平均,这个变量的初值为0
# 注意这里手动指定了比纳凉的类型为tf.float32
# 因为所有需要计算滑动平均的变量必须是实数型
v1 = tf.Variable(0, dtype=tf.float32)
# 这里step变量模拟神经网络中的迭代论数
# 可以用于动态控制衰减率
step = tf.Varibale(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_variable_initializer()
sess.run(train_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)]))
# 将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]
以上代码给出了 ExponentialMovingAverage 的简单样例,在第 5 章中将给出在真实应用中使用滑动平均的样例。
网友评论