目录
第四章——深层神经网络
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滑动平均模型
维基百科对深度学习的精确定义为“一类通过多层非线性变换对高复杂性数据建模算法的合集”。
深度学习有两个非常重要的特性:多层和非线性。
在线性模型中,模型的输出为输入的加权和。假设一个模型的输出y和输入x满足,那么这个模型就是一个线性模型。被称为线性模型是因为当模型的输入只有一个的时候,x和y会形成二维坐标系上的一条直线。类似的,当模型有n个输入时,x和y形成了n+1维空间中的一个平面。而一个线性模型中通过输入得到输出的函数被称为一个线性变换。
线性模型的最大特点是任意线性模型的组合仍是线性模型。因此,只通过线性变换,任意层的全连接神经网络和单层神经网络模型的表达能力没有任何区别,并且都是线性模型。(就是说,只有一层神经网络就可以等同于任意层的全连接神经网络,只要这一层中神经元个数足够多,就可以达到任意层的全连接神经网络模型的表达能力)
如果一个问题可以通过一条直线来划分,那么线性模型就可以用来解决这个问题。所谓复杂问题,至少是无法通过直线(或者高维空间的平面)划分的,在现实生活中,绝大部分的问题都是无法线性分割的。
上图左为线性可分,右为线性不可分。
如果将每一个神经元(也就是神经网络中的节点)的输出通过一个非线性函数,那么整个神经网络模型也就变成非线性模型了,这里用的非线性函数就是激活函数。
比如,对于上面的线性不可分问题,使用ReLU激活函数后效果如下:
常用的激活函数:
目前TensorFlow提供了多种不同的非线性激活函数,如tf.nn.relu,tf.sigmoid,tf.sigmoid,tf.tanh等。
异或问题直观来说就是如果两个输入的符号相同时(同时为正或者同时为负)则输出为0,否则(一正一负)输出为1.
单层神经网络无法解决异或问题,而有了隐藏层的多层神经网络可以解决异或问题。
深层神经网络实际上有组合特征提取的功能,这个特性对于解决不易提取特征向量的问题(比如图片识别、语音识别等)有很大帮助。这也是深度学习在这些问题上更加容易取得突破性进展的原因。
神经网络模型的效果以及优化的目标是通过损失函数(loss function)来定义的。
分类问题和回归问题是监督学习的两大种类。
分类问题是预测离散值的输出,如0,1,2,......。
回归问题预测的连续值输出,是具体数值,如房价,销售值等。
通过神经网络解决多分类问题最常用的方法使设置n个输出节点,其中n为类别的个数。对每一个样例,神经网络可以得到一个n维数组作为输出结果。数组中的每一个维度(也就是每一个输出节点)对应一个类别。在理想情况下,如果一个样本属于类别k,那么这个类别所对应的输出节点的输出值应该为1,而其他节点的输出都为0。如MNIST手写识别one_hot编码,对应位数的数值为1, [0,0,1,0,0,0,0,0,0,0,0],神经网络模型的输出结果越接近 [0,0,1,0,0,0,0,0,0,0,0]越好。
那么判断一个输出向量和期望的向量有多接近,交叉熵(cross entropy)是常用的评判方法之一。交叉熵刻画了两个概率分布之间的距离,它是分类问题中使用比较广的一种损失函数。
交叉熵刻画的是两个概率分布之间的距离,然而神经网络的输出却不一定是一个概率分布。概率分布刻画了不同事件发生的概率。当时间总数是有限的情况下,概率分布函数 p(X=x)p(X=x)满足: (及所有情况概率和为1)
如何将神经网络前向传播得到的结果变为概率分布呢?Softmax回归就是一个非常常用的方法。
Softmax本身可以作为一个学习算法来优化分类结果,但在TensorFlow中,Softmax回归的参数被去掉了,它只是一层额外的处理层,将神经网络的输出变为一个概率分布。
假设原始的神经网络输出为y1,y2,y3.。。。yn,那么经过Softmax回归处理后输出变为:
这个新的输出可以理解为经过神经网络的推导,一个样例为不同类别的概率分别为多大,这样就把神经网络的输出也变成了一个概率分布,从而可以通过交叉熵来计算预测的概率分布和真实答案的概率分布之间的距离了。
交叉熵函数不是对称的(H(p,q)!=H(q,p)),它刻画的是通过概率分布q来表达概率分布p的困难程度。因为正确答案是希望得到的结果,所以当交叉熵作为神经网络的损失函数时,p代表的是正确答案,q代表的是预测值。熵值越小,两个概率分布越接近。
#tensorflow的交叉熵代码
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))
#y_为正确结果,y为预测结果
#tf.clip_by_value(y, 1e-10, 1.0) : 防止出现log0的情况以及大于1的概率,将预测数值限制在一个范围内
#*为对应元素相乘,不是矩阵乘法,*等同于tf.multiply(),矩阵乘法用tf.matmul()
##tf.reduce_mean : 取所有元素的平均值
注意:本来不求平均前得到的结果为n×m矩阵,n为一个batch的样本数,m为类别数。正常根据交叉熵的公式,应该是将每行m个结果相加得到该batch的每一个样本的交叉熵的和,然后除以n得到这个batch的平均交叉熵。但是因为分类问题中m固定,每行数量相同,所以可以直接对整个矩阵求和取平均而不改变计算结果的意义,这样可以使得程序更加简洁。
因为交叉熵一般会与Softmax回归一起使用,所以有 tf.nn.softmax_cross_entropy_with_logits 函数对这两个功能进行了统一封装,可通过下面代码实现使用了softmax回归之后的交叉熵损失函数:
cross_entropy=tf.nn.softmax_cross_entropy_with_logits(labels=y_,logits=y)
对于回归问题,如房价预测,销量预测等,并不需要一个事先定义好的类别,而是需要一个任意实数。解决回归问题的神经网络一般只有一个输出节点,这个节点的输出值就是预测值。对于回归问题,最常用的损失函数是均方误差(MSE,mean squared error)。它的定义如下:
其中yi为一个batch中第i个数据的正确答案 。而yi'为神经网络给出的预测值。
mse=tf.reduce_mean(tf.square(y_-y))
具体问题具体定义损失函数以及不同的损失函数对训练结果的影响不同。
当涉及到最大化问题时,比如最大化利润,需要对预测结果在大于真实值或小与真实值时,有不同损失系数的损失函数:
与均方误差公式类似,yi为第一个batch中第i个数据的正确答案,y’为神经网络得到的预测值,a和b是常量。两个常量作为惩罚系数。如下代码实现:
loss = tf.reduce_sum(tf.where(tf.greater(v1, v2), (v1 - v2) * a, (v2 - v1) * b))
#tf.greater:如果第一个值大于第二个值,返回True,否则返回False
#tf.where:以第一个参数为选择条件根据,当选择条件为True时,
#会选择第二个参数中的值,否则使用第三个参数中的值(tf.select是旧版本函数,已废弃)
定义了损失函数之后,下面通过一个简单的神经网络程序来讲解损失函数对模型训练结果的影响。下面的程序中,实现了一个拥有两个输入结点、一个输出节点,没有隐藏层的神经网络,用来预测商品销量,如果预测多了,商家损失的是生产商品的成本;如果预测少了,损失的是商品利润。最小化该损失函数。
import tensorflow as tf
from numpy.random import RandomState
batch_size = 8
#两个输入节点
x = tf.placeholder(tf.float32,shape = (None, 2), name = 'x_input')
#回归问题一般只有一个输出节点
y_ = tf.placeholder(tf.float32, shape = (None, 1), name = 'y_input')
#定义了一个单层神经网络前向传播的过程,简单加权和
w1 = tf.Variable(tf.random_normal([2,1], stddev = 1, seed = 1))
y = tf.matmul(x, w1)
#预测值<真实值,给予更大的损失系数
loss_less = 10
#预测>真实值,给予较小的损失系数
loss_more = 1
#自定义的损失函数,对于预测少于真实值的结果给予更大的惩罚系数
loss = tf.reduce_sum(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 = rdm.rand(dataset_size, 2)
#加入随机变量rdm.rand()/10.0 - 0.05,表示不可预测的噪音。这里的噪音设置为 -0.05~0.05的随机数
#rand-生成 0~1之间均值为0符合均匀分布的数值
Y = [[x1 + x2 + rdm.rand()/10.0 - 0.05] for (x1, x2) in X]
#训练神经网路
with tf.Session() as sess:
tf.global_variables_initializer().run()
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]})
print(sess.run(w1))
OUT:
[[ 1.01934695]
[ 1.04280889]]
上述代码w1的值为[ 1.01934695,1.04280889],得到的结果会比x1+x2更大,因为在损失函数中指定预测少了的损失更大(loss_less > loss_more),所以模型更加偏向于预测多一些。
本部分内容将详细介绍如何通过反向传播算法(backpropagation)和梯度下降算法(gradient decent)调整神经网络中参数的取值。梯度下降算法主要用于优化单个参数的取值,而反向传播算法给出了一个高效的方式在所有参数上使用梯度下降算法,从而使神经网络模型在训练数据上的损失函数尽可能小,是训练神经网络的核心算法。
假设用θ表示神经网络中的参数,J(θ)表示给定参数情况下训练集数据上损失函数大小。那么整个优化过程就是要寻找一个参数θ,使得J(θ)达到局部最小。梯度下降算法会迭代式的更新参数θ,不断延着梯度的反方向让参数朝着总损失更小的方向更新,最终得到的是一个局部最小值!
其中,学习率*梯度来控制每次参数更新的幅度。
例,损失函数为,对x的梯度为,那么梯度下降算法每次对参数x的更新公式为 xn+1=xn−η∇n。假定参数初始值为5,学习率为0.3,那么这个优化过程总结如下表:
这是一个简单样例,可以类推神经网络的优化过程。分为两个阶段,第一个阶段先通过前向传播算法计算得到预测值,并将预测值和真实值做对比得出两者之间的差距J。然后在第二个阶段通过反向传播算法计算损失函数J对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数。
需要注意的是,梯度下降算法并不能保证被优化的函数达到全局最优解。 如图 4-12 所 示,图中给出的函数就有可能只能得到局部最优解而不是全局最优解。 在小黑点处,损失函数的偏导为 0,于是参数就不会再进一步更新。在这个样例中,如果参数 x 的初始值落在右侧深色的区间中,那么通过梯度下降得到的结果都会落到小黑点代表的局部最优解。 只有当 x 的初始值落在左侧浅色的区间时梯度下降才能给出全局最优答案。 由此可见在训练神经网络时,参数的初始值会很大程度影响最后得到的结果。 只有当损失函数为凸函数时,梯度下降算法才能保证达到全局最优解。
除了不一定能达到全局最优,梯度下降算法的另外一个问题就是计算时间太长。 因为要在全部训练数据上最小化损失,所以损失函数J的是在所有训练数据上的损失和。 这样在每一轮迭代中都需要计算在全部训练数据上的损失函数。在海量训练数据下,要计算所有训练数据的损失函数是非常消耗时间的。为了加速训练过程,可以使用随机梯度下降的算法(stochastic gradient descent,SGD)。这个算法优化的不是在全部训练数据上的损失函数,而 是在每一轮法代中,随机优化某一条训练数据上的损失函数。 这样每一轮参数更新的速度就大大加快了。因为随机梯度下降算法每次优化的只是某一条数据上的损失函数,所以它的问题也非常明显: 在某一条数据上损失函数更小并不代表在全部数据上损失函数更小, 于是使用随机梯度下降优化得到的神经网络甚至可能无法达到局部最优。
所以一般取梯度下降算法和随机梯度下降算法折中方案,即在一个batch size数据上计算损失函数并优化参数,即mini-batch gradient descent.现在基本都使用的是这种方式。
神经网络训练优化框架:
batch_size = n
#每次读取一小部分数据作为当前的训练数据来执行反向传播算法
x = tf.placeholder(tf.float32, shape = (batch_size,2), name = 'x_input')
y = tf.placeholder(tf.float32, shape = (batch_size,1), name = 'y_input')
#定义神经网络结构和优化算法
loss = ...
train_step = tf.train.AdaOptimizer(0.001).minimize(loss)
#训练神经网络
with tf.Session() as sess:
#参数初始化
...
#迭代的更新参数
for i in range(steps):
#准备batch_size个训练数据,一般将所有训练数据随机打乱后再选取来得到更好的优化效果。
current_X,xurrent_Y=....
sess.run(train_step,feed_dict={x:current_X,y_:current_Y})
本节主要介绍神经网络训练优化过程中常见问题及对应解决方案。
学习率决定了训练过程中参数更新的幅度,幅度过大可能会导致参数在极优值两侧震荡,幅度过小会导致训练时间过长。
经验:训练前期学习率设大点,后期设小点。
TensorFlow提供了一种灵活的学习率设置方法——指数衰减法。函数:tf.train.exponential_decay(),通过这个函数,可以先使用较大的学习率来加速得到一个比较优的解,然后随着迭代的继续逐步减小学习率,使得模型在训练后期更加稳定。该函数实现了以下代码的功能:
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)
#deayed_learning_rate : 每一轮优化时使用的学习率
#learning_rate : 初始学习率
#decay_rate : 衰减系数
#decay_steps : 衰减速度
#globel_step : 全局步数计数器,每次为当前步数
tf.train.exponential_decay(learning_rate, global_, decay_steps, decay_rate, staircase=False)
通过staircase参数选择不同的衰减模式,默认值为Fasle,此时为下图中光滑曲线,当设为True时,global_step/decay_steps会被转化为整数,此时学习率成为一个阶梯函数(staircase function)
当阶梯状时,decay_steps 通常代表了完整的使用一遍训练数据 所需要的迭代轮数。这个迭代轮数也就是总训练样本数除以 batch size大小。 这种设置的常用场景是每完整地过完一遍训练数据,学习率就减小一次。这可以使得训练数据集中的所有数据对模型训练有相等的作用。
当使用连续的指数衰减学习率时(上图曲线),不同的训练数据有不同的学习率,而当学习率减小时,对应的训练数据对模型训练结果的影响也就小了。
以下代码示范在TensorFlow中使用tf.train.exponential_decay函数:
global_steps=tf.Variavle(0)
#通过exponential_decay函数生成学习率,下列参数相当于每100个step,学习率乘以0.96
learning_rate=tf.train.exponential_decay(
0.1,global_step,100,0.96,staircase=True)
#使用指数衰减的学习率,在minimize函数中传入global_step将自动更新global_step参数,
#从而使得学习率也得到相应更新
train_step=tf.train.GradientDescentOptimizer(learning_rate)\
.minimize(...my loss..,global_step=global_step)
所谓过拟合,是指训练后,模型在训练集上表现很好,但在测试集上表现很差。即模型可以很好的记忆每个训练数据中得噪声部分,却忘记了学习训练数据中通用得趋势。
为避免过拟合,最常用的方式为——正则化(regularization)。
正则化的思想就是在损失函数中加入刻画模型复杂程度的指标。假设用于刻画模型在训练数据上表现的损失函数为J(θ),那么不直接优化J(θ),而是优化J(θ)+λR(w)。其中R(w)刻画的是模型的复杂程度,而 λ 表示模型复杂损失在总损失中的比例。θ表示神经网络中的所有参数(包括边上的权重 w和偏置项 b)。一般来说模型复杂度只由权重 w 决定。
常用的刻画复杂度的函数 R(w) 有两种,一种是 L1 正则化,计算公式是:
另外一种是L2正则化,计算公式是:
无论哪一种正则化方式,基本的思想都是希望通过限制权重的大小,使得模型不能任意拟合训练数据中的随机噪音。在使用上L2相对更佳,一是L2正则化公式可导,对含有L2正则化损失函数的优化要更加简洁;二是L2正则化不会让参数变得更加稀疏(更多参数变为0)。之所以 L2 正则化不会让参数变得稀疏的原因是当参数w很小时,比如 0.001 ,这个参数的平方基本上就可以忽略了,于是模型不会进一步将这个参数调整为 0。
实践中,也可将L1与L2一起使用:
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)
#loss损失函数:均方误差 + $L2$ 正则化函数(用于防止模型过度拟合训练数据中的随机噪音)
#lambda : 正则化项权重,J(θ)+λR(w)中的λ
#w : 需要计算的正则化损失的参数
TensorFlow提供了tf.contrib.layers.l2_regularizer()函数,它会返回一个函数,返回的函数用于计算给定参数的l2正则化项的值。
同样的有:tf.contrib.layers.l1_regularizer()函数来返回一个函数用于计算l1正则化项的值。
例子:注意:TensorFlow会将L2的正则化损失值除以2使得求导得到的结果更加简洁。
weights = tf.constant([[1., -2.],[-3., 4.]])
with tf.Session() as sess:
#输出 0.5 * (1+2+3+4) = 5
print(sess.run(tf.contrib.layers_l1_regularizer(.5)(weights)))
#输出 0.5 * (1 + 4 + 9 + 16)/2 = 7.5
print(sess.run(tf.contrid.layer_l2_regularizer(.5)(weights)))
使用集合(collection),解决损失函数loss定义很长、可读性差且容易出错i的问题,以及定义网络结构部分和计算损失函数的部分不在同一个函数中的问题。 以下代码给出了通过集合计算一个5层神经网络带 L2正则化的损失函数的计算方法。
import tensorflow as tf
#获取一层神经网络边上的权重,并将这个权重的 L2 正则化损失加入名称为‘losses’的集合中
def get_weight(shape, lamba):
# 生成一个变量
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(lamba)(var))
#返回生成的变量
return var
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'))
在更加复杂的网络结构中,使用这样的方式来计算损失函数将大大增强代码的可读性
滑动平均模型(Moving average model,MA模型)——用于更新参数,可以使模型在测试数据上更健壮(robust) ,在采用随机梯度下降算法训练神经网络时,使用滑动平均模型在很多应用中都可以在一定程度提高最终模型在测试数据上的表现。
在 TensorFlow 中提供了 tf.train.ExponentialMovingAverage 来实现滑动平均模型:
初始化参数decay:在初始化 ExponentialMovingAverage 时,需要提供一个衰减率(decay)。这个衰减率将用于控制模型更新的速度。
变量参数shadow_variable : ExponentialMovingAverage 对每一个变量会维护一个影子变量(shadow_variable),这个影子变量的初始值就是相应变量的初始值,每次运行变量更新时,影子变量的值会更新为:
即:decay*原值+(1-decay)*新值
其中 variable为待更新的变量的值。从公式中可以看出,decay决定了模型更新的速度,decay越大模型越趋于稳定。在实际应用中,decay一般会被设成非常接近1的数(比如0.99、0.999 ),为了使模型在训练前期可以更新的更快,还提供了num_updates参数。
初始化参数num_updates : 来动态设置decay的大小。如果初始化时提供了num_updates,每次使用的衰减率将是:
下面通过一段代码解释 ExponentialMovingAverage 如何被使用。
import tensorflow as tf
# 定义一个变量用于计算滑动平均,初始值为0
# 滑动平均的变量必须是实数型
v1 = tf.Variable(0,dtype = tf. float32)
# step变量模拟神经网络中的迭代次数,可以用于动态控制衰减率
step = tf.Variable(0, trainable = False) #即num_updates
#定义一个滑动平均的类(class)。初始化时给定了衰减率(0.99)和控制衰减率的变量 setp
ema = tf.train.ExponentialMovingAverage(0.99, step)
#定义一个更新变量滑动平均的操作,这里需要给定一个列表,每次执行这个操作时这个列表中的变量都会被更新
maintain_averages_op = ema.apply([v1])
with tf.Session() as sess:
tf.global_variables_initializer().run()
#通过 ema.average(v1) 获取滑动平均之后变量的取值。 在初始化之后变量 v1 的值和 v1 的滑动平均都为 0
print(sess.run([v1, ema.average(v1)])) #输出 [0.0, 0.0]
#更新变量 v1 的值 到 5
sess.run(tf.assign(v1, 5))
# 新 shadow_average = decay * 新average + 1-decay * 旧average(shadw_average)
#更新 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+10000)/(10+10000) ≈ 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]
下一章,将给出真实在应用中使用滑动平均的样例