[Tensorflow实战Google深度学习框架]笔记3

  本系列为Tensorflow实战Google深度学习框架知识笔记,仅为博主看书过程中觉得较为重要的知识点,简单摘要下来,内容较为零散,请各位指出,共同进步。  

  2017-11-06

  [第四章] 深层神经网络

  1. 深度学习与深层神经网络

  维基百科对深度学习的精确定义为:一类通过多层非线性变换对高复杂性数据建模算法的合集。因为深层神经网络时实现“多层非线性变换”最常用的一种方法,深度学习有两个非常重要的特性---多层和非线性。

  只通过线性变换,任意层的全连接神经网络和单层网络模型的表达能力没有任何区别,而且他们都是线性模型,然后线性模型能够解决的问题是有限的。

  如果将每一个神经元(也就是神经网络中的节点)的输出通过一个非线性函数,那么整个神经网络的模型也就不再是线性的了。这个非线性函数就是激活函数。

  分类问题和回归问题时监督学习的两大种类。

  通过神经网络解决多分类问题最常用的方法时设置n个输出节点,其中n为类别的个数。对于每一个样例,神经网络可以得到一个n维数组作为输出结果,数组中的每一个维度(也就是每一个输出节点)对应一个类别。在理想情况下,如果一个样本属于类别k,那么这个类别所对应的输出节点的输出值应该为1,而其他节点的输出都为0。以识别数字1为例,神经网络模型的输出结果越接近[0,1,0,0,0,0,0,0,0,0]越好。那么如何判断一个输出向量和期望的向量有多接近呢?交叉熵(cross entropy)是常用的评判方法之一。交叉熵刻画了两个概率分布之间的距离,它是分类问题中使用比较广的一种损失函数。注意:交叉熵刻画的时俩个概率分布之间的距离,然而神经网络的输出却不一定是一个概率分布,如何将神经网络前向传播得到的结果也变成概率分布呢?Softmax回归就是一个非常常用的方法。Softmax回归本身可以作为一个学习算法来优化分类结果,但在Tensorflow中,Softmax回归的参数被去掉了,它只是一层额外的处理层,将神经网络的输出变成一个概率分布。

  [Tensorflow实战Google深度学习框架]笔记3 - 王老头

  从以上公式可以看出,原始神经网络的输出被用作置信度来生成新的输出,而新的输出满足概率分布的所有要求。这个新的输出可以理解为经过神经网络的推导,一个样例为不同类别的概率分别是多大,这样就把神经网络的输出也变成了一个概率分布,从而可以通过交叉熵来计算预测的概率分布和真实答案的概率分布之间的距离了。

  从交叉熵的公式中可以看到交叉熵函数不是对称的,它刻画的是通过概率分布q来表达概率分布p的困难程度。因为正确答案时希望得到的结果,所以当交叉熵作为神经网络的损失函数时,p代表的是正确答案,q代表的时预测值。交叉熵刻画的时两个概率分布的距离,交叉熵值越小,两个概率分布越接近。

  看到一句话,很形象。(SVM只选自己喜欢的男神,Softmax把所有备胎全部拉出来评分,最后还归一化一下。)

  因为交叉熵一般会与softmax回归一起使用,所以Tensorflow对这两个功能进行了统一封装,并提供了tf.nn.softmax_cross_entropy_with_logits函数。比如可以直接通过下面的代码来实现使用了softmax回归之后的交叉熵损失函数:

  corss_entropy = tf.nn.softmax_cross_entropy_with_logits(y,y_)

  其中y代表原始神经网络的输出结果,而y_给出了标准答案。这样通过一个命令就可以得到使用了Softmax回归之后的交叉熵。在只有一个正确答案的分类问题中,Tensorflow提供了tf.nn.sparse_softmax_cross_entropy_with_logits函数进一步加速计算过程。

  与分类beni不同,回归问题解决的是对具体数值的预测,比如房价预测,销量预测等都是回归问题。解决回归问题的神经网络一般只有一个输出节点,这个节点的输出值就是预测值。对于回归问题,最常用的损失函数就是均方误差(MSE,mean squared error),代码如下:

  mse = tf.reduce_mean(tf.square(y-y_))

  其中y代表了神经网络的输出答案,y_代表了标准答案。

  自定义损失函数的优化,在Tensorflow中,可以通过以下代码来实现这个损失函数:

  loss = tf.reduce_sum(tf.select(tf.greater(v1,v2),(v1-v2)*a,(v2-v1)*b))

  上面的代码用到了tf.greater和tf.select来实现选择操作。tf.greater的输入时两个张量,此函数会比较这两个输入张量中每一个元素的大小,并返回比较结果。当tf.greater的输入张量的维度不一样时,Tensorflow会进行类似numpy的广播(broadcasting)操作处理。

  3. 神经网络优化算法

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

  神经网络的优化过程可以分为两个阶段,第一个阶段先通过前向传播算法计算得到预测值,并将预测值和真实值作对比得出两者之间的差距rancour在第二个阶段通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数。需要注意的是,梯度下降算法并不能保证被优化的函数达到全局最优解,参数的初始值会很大程度上影响最后得到的结果,只有当损失函数为凸函数时,梯度下降算法才能保证达到全局最优解。

  除了不一定能达到全局最优之外,梯度下降算法的另外一个问题就是计算时间太长,因为要在全部训练数据上最小化损失,所有损失哈市南湖J(θ)是在所有训练数据上的损失和。为了加速训练过程,可以使用随机梯度下降算法(SGD,stochastic gradient descent),这个算法优化的不是在全部训练数据上的损失函数,而是在每一轮迭代中,随即优化某一条训练数据上的损失函数。也由于在某一条数据上损失函数并不代表全部数据,于是使用随机梯度下降优化得到的神经网络甚至可能无法达到局部最优。

  4. 神经网络进一步优化

  为了设定学习率的问题,Tensorflow提供了一种更加灵活的学习率设置方法---指数衰减法。tf.train.exponential_decay函数实现了指数衰减学习率。通过这个函数,可以先使用较大的学习率来快速得到一个比较优的解,然后随着迭代的继续逐步减少学习率,使得模型在训练后期更加稳定。exponential_decay函数会指数级地减少学习率,它实现了如下功能:

  decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)

  其中decayed_learning_rate为每一轮优化时使用的学习率,learning_rate为事先设定的初始学习率,decat_rate为衰减系数,decay_steps为衰减速度。tf.train.exponential_decay函数可以通过设置参数staircase选择不同的衰减方式,staircase默认值为False,当被设置为True时,global_step / decay_steps会被转化成整数。decay_steps通常代表了完整的使用一遍训练数据所需要的迭代轮数,这个迭代轮数也就是总训练样本数除以每一个batch中的训练样本数。下面通过简单代码展示:

  global_step = tf.Variable(0)

  learning_rate = tf.train.exponential_decay(0.1,global_step,100,0.96,staircase=True)

  #使用指数衰减的学习率,在minimize函数中传入global_step将自动更新global_step参数,从而使得学习率也得到相应更新

  learning_step = tf.train.GraidentDescentOptimizer(learning_rate).minimize(...my_loss...,global_step=global_step)

  上面这段代码中设定了初始学习率为0.1,因为指定了staircase为True,所以每训练100轮后学习率乘以0.96。

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

  [Tensorflow实战Google深度学习框架]笔记3 - 王老头

  另一种时L2正则化,计算公式是:

  [Tensorflow实战Google深度学习框架]笔记3 - 王老头,无论时哪一种正则化方式,基本的思想都是希望通过限制权重的大小,使得模型不能任意拟合训练数据中的随机噪声。但这两种正则化的方法也有很大的区别,首先,L1正则化会让参数变得更稀疏,而L2正则化不会,所谓参数变得稀疏是指会有更多的参数变为0,这样可以达到类似特征提取的功能。在实践中,也可以将L1正则化和L2正则化同时使用:

  [Tensorflow实战Google深度学习框架]笔记3 - 王老头

  下面代码给出通过集合(collection)的方式计算一个5层神经网络带L2正则化的损失函数的计算方法:

  #获取一层神经网络边上的权重,并将这个权重的L2正则化损失加入名称为\'losses\'的集合中

  def get_weight(shape,lambda):

    #生成一个变量

    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(lambda)(var))

    return var

  使用这样的方式来计算损失函数将大大增强代码的可读性。

  在Tensorflow中还提供了tf.train.ExponentialMovingAverage来实现滑动平均模型。在初始化ExponentialMovingAverage时,需要提供一个衰减率,这个衰减率将用于控制模型更新的速度,ExponentialMovingAverage对每一个变量会维护一个影子变量(shadow variable),这个影子变量的初始值就是相应变量的初始值,而每次运行变量更新时,影子变量的值会更新为:  

  shadow_variable = decay * shadow_variable + (1 - decay) * variable

  其中shadow_variable为影子变量,variable为待更新的变量,decay为衰减率。从公式中可以看到,decay决定了模型更新的速度,decay越大模型越稳定,在实际应用中,decay一般会设成非常接近1的数(比如0.999),为了使得模型在训练前期可以更新得更快。

  下面通过一段代码来解释ExponentialMovingAverage时如何被使用的。

   import tensorflow as tf


  #定义一个变量用于计算滑动平均,这个变量的初始值为0,注意这里手动指定了变量的类型为tf.float32
  # 因为所有需要计算滑动平均的变量必须时实数型
  v1 = tf.Variable(0,dtype=tf.float32)
  #这里step变量模拟神经网络中迭代的轮数,用于动态控制衰减率
  step = tf.Variable(0,trainable=False)

  #定义一个滑动平均的类(class).初始化给定了衰减率(0.99)和控制衰减率的变量step
  ema = tf.train.ExponentialMovingAverage(0.99,step)

  #定义一个更新变量滑动平均的操作,这里需要给定一个列表,每次执行这个操作时这个列表中的变量都会更新
  maintain_average_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)])) #输出[0.0,0.0]

   #更新变量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_average_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_average_op)
   print(sess.run([v1,ema.average(v1)])) #输出[10.0,4.555]

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