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

时间:2022-05-23 23:35:17

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

  2017-11-06

  [第五章] MNIST数字识别问题

  1. MNIST数据处理

  为了方便使用,Tensorflow提供了一个类来处理MNIST数据,这个类会自动下载并转化MNIST数据的格式,将数据从原始的数据包中解析成训练和测试神经网络时使用的格式。

  2. 神经网络模型训练及不同模型结果对比

  为了评测神经网络模型在不同参数下的效果,一般会从训练数据中抽取一部分作为验证数据。使用验证数据就可以评判不同参数取值下模型的表现。除了使用验证数据集,还可以采用交叉验证(cross validation)的方式来验证模型效果,但因为神经网络训练实践本身就比较长,采用cross validation会花费大量时间。所以在海量数据的情况下,一般会更多地采用验证数据集的形式来评测模型的效果。

  为了说明验证数据在一定程度上可以作为模型效果的评判标准,我们将对比在不同迭代轮数的情况下,模型在验证数据和测试数据上的正确率。为了同时得到同一个模型在验证数据和测试数据上的正确率,可以在每1000轮的输出中加入在测试数据集上的正确率。

  在神经网络结构的设计上,需要使用激活函数和多层隐藏层。在神经网络优化时,可以使用指数衰减的学习率,加入正则化的损失函数以及滑动平均模型。

  3. 变量管理

  Tensorflow提供了通过变量名称来创建或者获取一个变量的机制,通过这个机制,在不同的函数中可以直接通过变量的名字来使用变量,而不需要将变量通过参数的形式到处传递。Tensorflow中通过变量名称获取变量的机制主要时通过tf.get_variable和tf.variable_scope函数实现的。

  除了tf.Variable函数,Tensorflow还提供了tf.get_variable函数来创建或者获取变量。当tf.get_variable用于创建变量时,它和tf.Variable的功能是基本等价的。

  如:#下面这两个定义是等价的。

    v = tf.get_variable("v",shape=[1],initializer=tf.constant_initilizer(1.0))

    v = tf.Variable(tf.constant(1.0,shape=[1]),name="v")

  Tensorflow提供了7种不同的初始化函数,如下:

    初始化函数        功能        主要参数

  tf.constant_initializer--->将变量初始化为给定常量----> 常量的取值

  tf.random_normal_initializer--->将变量初始化为满足正太分布的随机值--->正太分布的均值和标准差

  tf.truncated_normal_initializer--->将变量初始化为满足正太分布的随机值,但若随机出来的值偏离平均值超过两个标准差,那么这个数将会被重新随机---->正太分布的均值和标准差

  tf.random_uniform_initializer--->将变量初始化为满足平均分布的随机值---->最大,最小值

  tf.uniform_unit_scaling_initializer--->将变量初始化为满足平均分布但不影响输出数量级的随机值---->factor(产生随机值时乘以的系数)

  tf.zeros_initializer--->将变量设置为全为0--->变量维度

  tf.ones_initializer--->将变量设置为全为1--->变量维度

  在上面的样例定义程序中,tf.get_variable首先会试图去创建一个名字为v的参数,如果创建失败(比如已经有同名的参数),那么这个程序就会报错。这是为了避免无意识的变量复用造成的错误。比如在定义神经网络参数时,第一层网络的权重已经叫weights了,那么在创建第二层神经网络时,如果参数名仍然叫weights,就会触发变量重用的错误。否则两层神经网络共用一个权重会出现一些比较难以发现的错误。如果需要通过tf.get_variable获取一个已经创建的变量,需要通过tf.variable_scope函数来生成一个上下文管理器,并明确指定在这个上下文管理器中,tf.get_variable将直接获取已经生成的变量。下面给出一段代码说明如何通过tf.variable_scope函数来控制tf.get_variable函数获取已经创建过的变量。

  #在名字为foo的命名空间内创建名字为v的变量

  with tf.variable_scope("foo"):

    v = tf.get_variable("v",[1],initializer=tf.constant_initializer(1.0))

  #因为在命名空间foo已经存在名字为v的变量,所有下面的代码将会报错:

  with tf.variable_scope("foo"):

    v = tf.get_variable("v",[1])

  #在生成上下文管理器时,将参数reuse设置为True。这样tf.get_variable函数将直接获取已经生成的变量

   with tf.variable_scope("foo",reuse=True):

    v1 = tf.get_variable("v",[1])

    print v == v1    #输出为True,代表v,v1是相同的Tensorflow中的变量

  #将参数reuse设置为True时,tf.variable_scope将只能获取已经创建的变量,因为在命名空间bar中还没有创建变量v,所以下面的代码将会报错:

  with tf.variable_scope("bar",reuse=True):

    v = tf.get_variable("v",[1])

  同样,如果tf.variable_scope函数使用参数reuse=None或者reuse=False创建上下文管理器,tf.get_variable操作将创建新的变量,如果同名的变量已经存在,则tf.get_variable函数将报错。另外,Tensorflow中tf.variable_scope函数是可以嵌套的。

  使用变量管理后,就不再需要将所有变量都作为参数传递到不同的函数中了,当神经网络结构更加复杂,参数更多时,使用这种变量管理的方式将大大提高程序的可读性。

  4. Tensorflow模型持久化

  为了让训练结果可以复用,需要将训练得到的神经网络模型持久化(保存下来方便下次使用)。

  Tensorflow提供了一个非常简单的API来保存和还原一个神经网络模型,这个API就是tf.train.Saver类。

  saver = tf.train.Saver()

  saver.save(sess,"/path/to/model/model.ckpt")

  Tensorflow模型一般会存在后缀为.ckpt文件中,虽然上面的程序只指定了一个文件路径,但是在这个文件目录下会出现三个文件,这是因为Tensorflow会将计算图的结构和涂上的参数取值分来保存。第一个文件为model.ckpt.meta,它保存了Tensorflow计算图的结构,第二个文件为model.ckpt,这个文件保存了Tensorflow程序中每一个变量的取值,最后一个文件为checkpoint文件,这个文件保存了一个目录下所有的模型文件列表。

  加载模型的代码:

  saver = tf.train.Saver()

  saver.restore(sess,"/path/to/model/model.ckpt")

  上面给出的程序中,默认保存和加载了Tensorflow计算图上定义的全部变量。但有时候可能只需要保存或者加载部分变量,比如,可能有一个之前训练好的五层神经网络模型,但现在想尝试一个六层的神经网络,那么可以将前面五层神经网络中的参数直接加载到新的模型,而仅仅将最后一层神经网络重新训练。为了保存或者加载部分变量,在声明tf.train.Saver类时可以提供一个列表来指定需要保存或者加载的变量。比如在加载模型的代码中使用saver=tf.train.Saver([v1])命令来构建tf.train.Saver类,那么只有变量v1会被加载进来,如果运行修改后之家在v1的代码会得到变量未初始化的错误:

  tensorflow.python.framework.errors.FailedPreconditionError:Attempting to use uninitialized value v2

  因为v2没有被加载,所以v2在运行初始化之前是没有值的。除了可以选取需要被加载的变量,tf.train.Saver类也支持在保存或者加载时给变量重命名。

  下面给出一个简单的程序来说明重命名时如何被调用的

  #这里声明的变量名称和已经保存的模型中变量的名称不同

  v1 = tf.Variable(tf.constant(1.0,shape=[1]),name="other-v1")

  v2 = tf.Variable(tf.constant(2.0,shape=[1]),name="other-v2")

  #如果直接使用tf.train.Saver类来加载模型会报变量找不到的错误。

  #使用一个字典(dictionary)来重命名变量就可以加载原来的模型了。这个字典指定了原来名称为v1的变量现在加载到变量v1中(名称为other-v1),

  #名称为v2的变量加载到变量v2中(名称为other-v2)

  saver = tf.train.Saver({"v1":v1, "v2":v2})

  在这个程序中,对变量v1和v2的名称进行了修改,如果直接通过tf.train.Saver默认的构造函数来加载保存的模型,那么程序会报变量找不到的错误,因为保存时候变量的名称和加载时变量的名称不一致。为了解决这个问题,Tensorflow可以通过字典(dictionary)将模型保存时的变量名和需要加载的变量联系起来。这样做主要目的之一时方便使用变量的滑动平均值,在Tensorflow中,每一个变量的滑动平均值是通过影子变量维护的,所以要获取变量的滑动平均值实际上就是获取这个影子变量的取值。如果在加载模型时直接将影子变量映射到变量自身,那么在使用训练好的模型时就不需要再调用函数来获取变量的滑动平均值了。下面的代码给出了一个保存滑动平均模型的样例:

  import tensorflow as tf
  v = tf.Variable(0,dtype=tf.float32,name="v")
  #在没有申明滑动平均模型时只有一个变量v,所以下面的语句只会输出"v:0"
  for variables in tf.all_variables():
    print(variables.name)

  ema = tf.train.ExponentialMovingAverage(0.99)
  maintain_average_op = ema.apply(tf.all_variables())  
  #在申明滑动平均模型之后,Tensorflow会自动生成一个影子变量v/ExponentialMoving Average
  #于是下面的语句会输出"v:0"和“v/ExponentialMoving Average:0”
  for variables in tf.all_variables():
    print(variables.name)

  saver = tf.train.Saver()
  with tf.Session() as sess:
    init_op = tf.global_variables_initializer()
    sess.run(init_op)

    sess.run(tf.assign(v,10))
    sess.run(maintain_average_op)
    #保存时Tensorflow会将v:0和v/ExponentialMoving Average:0两个变量都保存下来
    saver.save(sess,"path/to/model/model.ckpt")
    print(sess.run([v,ema.average(v)])) #输出[10.0,0.09999]

  使用tf.train.Saver会保存运行Tensorflow程序所需要的全部信息,然后有时并不需要某些信息。比如在测试或者离线预测时,只需要知道如何从神经网络的输入层经过前向传播计算得到输出层即可,而不需要类似于变量初始化,模型保存等辅助节点的信息。而且,将变量取值和计算图结构分成不同的文件存储有时候也不方便,于是Tensorflow提供了convert_variables_to_constants函数,通过这个函数可以将计算图中的变量及其取值通过常量的方式保存,这样整个Tensorflow计算图可以统一存放在一个文件中。如下:

  import tensorflow as tf
  from tensorflow.python.framework import graph_util   v1 = tf.Variable(tf.constant(1.0,shape=[1]),name="v1")
  v2 = tf.Variable(tf.constant(2.0,shape=[1]),name="v2")
  result = v1 + v2   init_op = tf.global_variables_initializer()
  with tf.Session() as sess:
   sess.run(init_op)
   #导出当前计算图的GraphDef部分,只需要这部分就可以完成从输入层到输出层的计算过程
   graph_def = tf.get_default_graph().as_graph_def()
   #将图中的变量及其取值转化为常量,同时将图中不必要的节点去掉
   output_graph_def = graph_util.convert_variables_to_constants(sess,graph_def,['add'])
   with tf.gfile.GFile("/path/to/model/combined_model.pb","wb") as f:f.write(output_graph_def.SerializeToString())

  通过下面的程序可以直接计算定义的加法运算的结果:

  import tensorflow as tf
  from tensorflow.python.platform import gfile

  with tf.Session() as sess:
     model_filename = "/path/to/model/combined_model.pb"
    #读取保存的模型文件,并将文件解析成对应的GraphDef Protocol Buffer
    with gfile.FastGFile(model_filename,'rb') as f:
      graph_def = tf.GraphDef()
      graph_def.ParseFromString(f.read())

  #将graph_def中保存的图加载到当前的图中.return_elements=["add:0"]给出了返回的张量的名称。
  #在保存能的时候给出的时计算节点的名称,所以为"add",在加载的时候给出的是张量的名称,所以时add:0
  result = tf.import_graph_def(graph_def,return_elements=["add:0"])
  print(sess.run(result))

  Tensorflow是一个通过图的形式来表达计算的编程系统,Tensorflow程序中的所有计算都会表达为计算图上的节点。Tensorflow通过元图(MetGraph)来记录计算图中节点的信息以及运行计算图中节点所需要的元数据。Tensorflow中元图是由MetaGraphDef Protocol BUffer定义的,MetaGraphDef中的内容就构成了Tensorflow持久化时的第一个文件。关于MetaGraphDef类型的定义以及模型持久化的原理和数据的格式请另查阅相关资料。[page 116 129/297]

  5. Tensorflow最佳实践样例程序

  本节中将介绍一个Tensorflow训练神经网络模型的最佳实践,将训练和测试分成两个独立的程序,这可以使得每一个组件更加灵活。比如训练神经网络的程序可以持续输出训练好的模型,而测试程序可以每隔一段实践检验最新模型的正确率,如果模型效果更好,则将这个模型提供给产品使用。除了将不同的功能模块分开,本节还将前向传播的过程抽象成一个单独的库函数。因为神经网络的前向传播过程在训练和测试的过程中都会用到,所以通过库函数的方式使用起来既方便又可以保证训练和测试过程中使用的前向传播方法是一致的。

  下面将提供重构之后的程序来解决MNIST问题:第一个是mnist_inference.py,它定义了前向传播的过程以及神经网络中的参数,第二个时minst_train.py,它定义了神经网络的训练过程,第三个时mnist_eval.py,它定义了测试过程。下面给出具体代码:

  mnist_inference.py:

  #-*- coding:utf-8 -*-

  import tensorflow as tf

  #定义神经网络结构相关的参数
  INPUT_NODE = 784
  OUTPUT_NODE = 10
  LAYER1_NODE = 500

  #通过tf.get_variable函数来获取变量
  def get_weight_variable(shape,regularizer):
    weights = tf.get_variable("weights",shape,initializer=tf.truncated_normal_initializer(stddev=0.1))

    #当给出了正则化生成函数时,将当前变量的正则化损失加入名字为losses的集合
    #这是自定义的集合,不再Tensorflow自动管理的集合列表中
    if regularizer != None:
      tf.add_to_collection('losses',regularizer(weights))
      return weights

  #定义神经网络的前向传播过程
  def inference(input_tensor,regularizer):
    #声明第一层神经网络的变量并完成前向传播的过程
    with tf.variable_scope('layer1'):
      weights = get_weight_variable([INPUT_NODE,LAYER1_NODE],regularizer)
      biases = tf.get_variable("biases",[LAYER1_NODE],initializer=tf.constant_initializer(0.0))
      layer1 = tf.nn.relu(tf.matmul(input_tensor,weights)+biases)

    #类似的声明第二层神经网络的变量并完成前向传播过程
    with tf.variable_scope('layer2'):
      weights = get_weight_variable([LAYER1_NODE, OUTPUT_NODE], regularizer)
      biases = tf.get_variable("biases", [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))
      layer2 = tf.nn.relu(tf.matmul(layer1, weights) + biases)

  #返回最后前向传播的结果
  return layer2

  mnist_train.py:

  #-*- coding:utf-8 -*-

  import os
  import tensorflow as tf
  from tensorflow.examples.tutorials.mnist import input_data

  #加载mnist_inference.py中定义的常量和前向传播的函数
  import mnist_inference

  #配置神经网络的参数
  BATCH_SIZE = 100
  LEARNING_RATE_BASE = 0.8
  LEARNING_RATE_DECAY = 0.99
  REGULARAZTION_RATE = 0.0001
  TRAINING_STEPS = 30000
  MOVING_AVERAGE_DECAY = 0.99
  #模型保存的路径和文件名
  MODEL_SAVE_PATH = "/path/to/model/"
  MODEL_NAME = "model.ckpt"

  def train(mnist):
    #定义输入输出placeholder
    x = tf.placeholder(tf.float32,[None,mnist_inference.INPUT_NODE],name='x-input')
    y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')

    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    #直接使用mnist_inference.py中定义的前向传播过程
    y = mnist_inference.inference(x,regularizer)
    global_step = tf.Variable(0,trainable=False)

    #定义损失函数,学习率,滑动平均操作以及训练过程
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY,global_step)
    variable_averages_op = variable_averages.apply(tf.trainable_variables())
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(y,tf.arg_max(y_,1))
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean+tf.add_n(tf.get_collection_ref('losses'))
    learning_rate = tf.train.exponential_decay(LEARNING_RATE_BASE,global_step,mnist.train.num_examples/BATCH_SIZE,LEARNING_RATE_DECAY)
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss,global_step=global_step)
    with tf.control_dependencies([train_step,variable_averages_op]):
      train_op = tf.no_op(name='train')

    #初始化Tensorflow持久化类
    saver = tf.train.Saver()
    with tf.Session() as sess:
      tf.global_variables_initializer().run()
      #在训练过程中不再测试模型在验证数据上的表现,验证和测试的过程将会有一个独立的程序来完成
      for i in range(TRAINING_STEPS):
      xs,ys = mnist.train.next_batch(BATCH_SIZE)
      _,loss_value,step = sess.run([train_op,global_step],feed_dict={x:xs,y_:ys})
      #每1000轮保存一次模型
      if i % 1000 == 0:
        #输出当前的训练情况
        print("After %d training steps, loss on training batch is %g."%(step,loss_value))
        #保存当前的模型
        saver.save(sess,os.path.join(MODEL_SAVE_PATH,MODEL_NAME),global_step=global_step)

  def main(argv=None):
    mnist = input_data.read_data_sets("MNIST_DATA",one_hot=True)
    train(mnist)

  if __name__ == '__main__':
    tf.app.run()

  mnist_eval.py:

  #-*- coding:utf-8 -*-

  import time
  import tensorflow as tf
  from tensorflow.examples.tutorials.mnist import input_data

  #加载mnist_inference.py和mnist_train.py中定义的常量和函数
  import mnist_inference
  import mnist_train

  #每10秒加载一次最新的模型,并在测试数据上测试最新模型的正确率
  EVAL_INTERVAL_SECS = 10

  def evaluate(mnist):
    with tf.Graph().as_default() as g:
      x = tf.placeholder(tf.float32,[None,mnist_inference.INPUT_NODE],name='x-input')
      y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
      validate_feed = {x:mnist.validation.images,y_:mnist.validation.labels}

      #直接通过调用封装好的函数来计算前向传播的结果,因为测试时不关注正则化损失的值,所以这里用于计算正则化损失的函数被设置为None
      y = mnist_inference.inference(x,None)

      #使用前向传播的结果计算正确率,如果需要对未知的样例进行分类,那么使用tf.argmax(y,1)就可以得到样例的预测类别了
      correct_prediction = tf.equal(tf.argmax(y,1),tf.argmax(y_,1))
      accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

      #通过变量重命名的方式来加载模型,这样就可以完全共用mnist_inference.py中定义的前向传播过程
      variable_averages = tf.train.ExponentialMovingAverage(mnist_train.MOVING_AVERAGE_DECAY)
      variable_to_restore = variable_averages.variables_to_restore()
      saver = tf.train.Saver(variable_to_restore)

      #每隔EVAL_INTERVAL_SECS秒调用一次计算正确率的过程以检测训练过程中正确率的变化
      while True:
        with tf.Session() as sess:
        #tf.train.get_checkpoint_state函数会通过checkpoint文件自动找到目录中最新模型的文件名
        ckpt = tf.train.get_checkpoint_state(mnist_train.MODEL_SAVE_PATH)
        if ckpt and ckpt.model_checkpoint_path:
          #加载模型
          saver.restore(sess,ckpt.model_checkpoint_path)
          #通过文件名得到模型保存时迭代的轮数
          global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]
          accuracy_score = sess.run(accuracy,feed_dict=validate_feed)
          print("After %s training steps, validation accuracy = %g." %(global_step,accuracy_score))
        else:
          print('No checkpoint file found.')
          return
        time.sleep(EVAL_INTERVAL_SECS)

  def main(argv=None):
    mnist = input_data.read_data_sets("MNIST_DATA",one_hot=True)
    evaluate(mnist)

  if __name__ == '__main__':
    tf.app.run()