基于tensorflow的MNIST数字识别

一、MNIST数据集介绍

MNIST是一个非常有名的手写体数字识别数据集,在很多资料中,这个数据集都会作为深度学习的入门样例。下面大致介绍这个数据集的基本情况,并介绍temsorflow对MNIST数据集做的封装。tensorflow的封装让使用MNIST数据集变得更加方便。MNIST数据集是NIST数据集的一个子集,它包含了60000张图片作为训练数据,10000张图片作为测试数据。在MNIST数据集中的每一张图片都代表了0~9中的一个数字。图片的大小都为28*28,且数字都会出现在图片的正中间。

在Yann LeCun教授的网站中(http://yann.lecun.com/exdb/mnist/)对MNIST数据集做出了详细的介绍。MNIST数据集提供了4个下载文件,下表归纳了下载文件中提供的内容。

网址

内容

http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz

训练数据图片

http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz

训练数据答案

http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz

测试数据图片

http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz

测试数据答案

虽然这个数据集只提供了训练和测试数据,但是为了验证模型训练的效果,一般会从训练数据中划分出一部分数据作为验证(validation)数据。为了方便实用,tensorflow提供了一个类来处理MINST数据。这个类会自动下载并转化MNIST数据的格式,将数据从原始的数据包中解析成训练和测试神经网络时使用的格式。下面给出了使用这个函数的样例程序。

from tensorflow.examples.tutorials.mnist import input_data

# 载入MNIST数据集,如果指定地址/path/to/MNIST_data下没有已经下载好的数据集,
# 那么tensorflow自动从上表给出的网站下载数据。
mnist = input_data.read_data_sets("/path/to/MNIST_data/", one_hot=True)

# 打印Training data size:  55000。
print "Training data size: ", mnist.train.num_examples


# 打印Validation data size:  5000。
print "Validating data size: ", mnist.validation.num_examples

# 打印Testing data size:  10000。
print "Testing data size: ", mnist.test.num_examples

# 打印Example training data: [0.  0.  0.  ...  0.380  0.376  ...  0.]。
print "Example training data: ", mnist.train.images[0]



# 打印Example training data label:
# [0.  0.  0.  0.  0.  0.  0.  1.  0.  0.]
print "Example training data label: ", mnist.train.labels[0]

从以上代码可以看出,通过input_data.read_data_sets函数生成的类会自动将MNIST数据集划分为train、validation和test三个数据集,其中train这个集合内有55000张图片,validation集合内有5000张图片,这两个集合组成了MNIST本身提供的训练数据集。test集合内有10000张图片,这些图片都来自于MNIST提供的测试数据集。处理后的每一张图片是一个长度为784的一维数组,这个数组中的元素对应了图片像素矩阵中的每一个数字(28*28=784)。因为神经网络的输入时一个特征向量,所以在此吧一张二维图像的像素矩阵放到一个一维数组中可以方便tensorflow将图片的像素矩阵提供给神经网络的输入层。像素矩阵中的元素的取值范围为[0, 1],它代表了颜色的深浅。其中0表示白色背景(background),1表示前景(foreground)。为了方便实用随机梯度下降,input_data.read_data_sets函数生成的类还提供了mnist.train.next_batch函数,它可以从所有的训练数据中读取一小部分为一个训练batch。以下代码显示了如何使用这个功能。

batch_size = 100
xs ,ys = mnist.train.next_batch(batch_size)
# 从train的集合中选取batch_size个训练数据。

print "X shape:", xs.shape
# 输出X shape: (100, 784)。

print "Y shape:", ys.shape
# 输出Y shape: (100, 10)。

二、基于tensorflow的MNIST手写数字识别

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


# MNIST数据集相关的常数
INPUT_NODE = 784        # 输入层的节点数。对于MNIST数据集,这个就等于图片的像素。
OUTPUT_NODE = 10        # 输出层的节点数。这个等于类别的数目。因为在MNSIT数据集中
                        # 需要区分的是0~9这10个数字,所以这里输出层的节点数为10。


# 配置神经网络的参数
LAYER!_NODE = 500       # 隐藏层节点数。这里使用只有一个隐藏层的网络结构作为样例。
                        # 这个隐藏层有500个节点。
BATCH_SIZE = 100        # 一个训练batch中的训练数据个数。数字越小时,训练过程越接近
                        # 随机梯度下降;数字越大时,训练越接近梯度下降。
LEARNING_RATE_BASE = 0.8        # 基础的学习率。
LEARNING_RATE_DECAY = 0.99      # 学习率的衰减率。

REGULARIZATION_RATE = 0.0001    # 描述模型复杂度的正则化项在损失函数中的系数。
TRAINING_SIZE = 30000           # 训练轮数。
MOVING_AVERAGE_DEACY = 0.99     # 滑动平均衰减率。

# 一个辅助函数,给定神经网络的输入和所有参数,计算神经网络的前向传播结果。在这里
# 定义了一个使用ReLU激活函数的三层全连接神经网络。通过加入隐藏层实现了多层神经网络结构,
# 通过ReLU激活函数实现了去线性化。在这个函数中也支持传入用于计算参数平均值的类,
# 这样方便在测试时使用滑动平均模型。
def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
    # 当没有提供滑动平均类时,直接使用参数当前的取值。
    if avg_class == None:
       # 计算隐藏层的前向传播结果,这里使用了ReLU激活函数。
       layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)
       
       # 计算输出层的前向传播结果。因为在计算损失函数时会一并计算softmax函数,
       # 所以这里不需要加入激活函数。而且不加入sotfmax不会影响预测结果。因为
       # 预测结果使用的是不同类别对应节点输出值的的绝对大小,有没有softmax层对最后分类结果的
       # 计算没有影响。于是在计算整个神经网络的前向传播时可以不加入最后的softmax层。
       return tf.matmul(layer1, weights2) + biases2
    else:
       # 首先使用avg_class.average函数来计算得出变量的滑动平均值,
       # 然后计算相应的神经网络前向传播结果。
       layer1 = tf.nn.relu(tf.matmul(input_tensor, avg_class.average(weights1)) + 
       avg_class.avergae(biases1))
       
       return tf.matmul(layer1, avg_class.average(weight2)) + avg_class.average(biases2)

# 训练模型的过程。
def train(mnist):
    x  = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.flaot32, [NOne, OUTPUT_NODE], name='y-input')
    
    # 生成隐藏层的参数。
    weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddec=0.1))
    biases1  = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))

    # 生成输出层的参数。
    weights2 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddec=0.1))
    biases2  = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))
    
    # 计算在当前参数下神经网络前向传播的结果。这里给出的用于计算滑动平均的类为None,
    # 所以函数不会使用参数的滑动平均。
    y = inference(x, None, weights1, biases1, weights2, biases2)
    
    
    # 定义存储轮数的变量。这个变量不需要计算滑动平均值,所以这里指定这个变量为
    # 不可训练的变量(trainable=False)。在使用Tensorflow训练神经网络时,
    # 一般将代表训练轮数的变量指定为不可训练的参数。
    global_step = tf.Variable(0, trainable=False)


    
   # 给定滑动平均衰减率和训练轮数的变量,初始化滑动平均类。给定训练轮数的变量可以加快
   # 训练早期变量的更新速度。
   variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, 
                       global_step)
   
   
   # 在所有代表神经网络参数的变量上使用滑动平均。其他辅助变量(比如global_step)就不需要了。
   # tf.trainable_variables返回的就是图上集合
   # GraphKeys.TRAINABLE_VARIABLES中的元素。这个集合的元素就是所有没有指定
   # trainable=False的参数。
   variable_average_op = variable_averages.apply(tf,trainable_variables())

   # 计算使用了滑动平均之后的前向传播结果。滑动平均不会改变变量本身的取值,而是会维护一个影子变量
   # 来记录其滑动平均值。所以当需要使用这个滑动平均值时,需要明确调用average函数。
   average_y = inference(x, variable_averages, weights1, biases1, weights2, biases2)
   
   
   # 计算交叉熵作为刻画预测值和真实值之间的差距的损失函数。这里使用了tensorflow中提供的
   # sparse_softmax_cross_entropy_with_logits函数来计算交叉熵。当分类问题只有一个正确答案时
   # 可以使用这个函数来加速交叉熵的计算。MNIST问题的图片中
   # 只包含了0~9中的一个数字,所以可以使用这个函数来计算交叉熵损失。这个函数的第一个
   # 参数是神经网络不包括softmax层的前向传播结果,第二个是训练数据的正确答案。因为
   # 标准答案是一个长度为10的一维数组,而该函数徐亚提供的是一个正确答案的数字,所以需要使用
   # tf.argmax函数来得到正确答案对应的类别编号。
   cross_entropy = tf.nn.sparse_sfotmax_cross_entropy_with_logits(
       logits=y,labels=tf.argmax(y_, 1))
   
   # 计算当前batch中所有样例的交叉熵平均值
   cross_entropy_mean = tf.reduce_mean(cross_entropy)
  
   # 计算L2正则化损失函数。
   regularizer = tf.contrib.layers_l2_regularizer(REGULARIZATION_RATE)  
   # 计算模型的正则化损失。一般只计算神经网络边上权重的正则化损失,而不使用偏置项。
   regularization = regularizer(weights1) + regularizer(weights2)
   # 总损失等于交叉熵损失和正则化损失的和。
   loss = cross_entropy_mean + regularization
   # 设置指数衰减的学习率。
   learning_rate = tf.train.exponential_decay(
   LEARNING_RATE_BASE,          # 基础的学习率,随着迭代的进行,更新变量时使用的学习率
   global_step,                 # 在这个基础上递减。
   mnist.train.num_examples / BATCH_SIZE   # 过完所有的训练数据需要的迭代次数。
   LEARNING_RATE_DECAY                     # 学习率衰减速度。
   )
   
# 使用tf.train.GradientDescentOptimizer优化算法来优化损失函数。注意这里损失函数
# 包含了交叉熵损失和L2正则化损失。
train_step=tf.train.GradientDescentOptimizer(learning_rate)\.minimize(loss, 
              global_step=global_step)

# 在训练神经网络模型时,每过一遍数据需要通过反向传播来更新神经网络中的参数,
# 又要更新每一个参数的滑动平均值。为了一次完成多个操作,tensorflow提供了
# tf.control_dependencies和tf.group两种机制。下面两行程序和
# train_op = tf.group(train_step, variables_averages_op)是等价的。
with tf.control_dependencies([train_step, variables_variables_averages_op]):
   train_op = tf.no_op(name='train')

# 检验使用了滑动平均模型的神经网络前向传播结果是否正确。tf.argmax(average_y ,1)
# 计算每一个样例的预测答案。其中average_y是一个batch_size * 10的二维数组,每一行
# 表示一个样例的前向传播结果。tf.argmax的第二个参数“1”表示选取最大值的操作仅在第一个
# 维度中进行,也就是说,只在每一行选取最大值对应的下标。于是得到的结果是长度为batch的
# 一维数组,这个一维数组中的值就表示了每一个样例对应的数字识别结果。tf.equal
# 判断两个张量的每一维是否相等,如果相等返回True,否则返回False。
correct_prediction = tf.equal(tf.argmax(average_y , 1),tf.argmax(y_, 1))

# 这个运算首先将一个布尔型的数值转换为实数型,然后计算平均值。这个平均值就是模型在这
# 一组数据生的正确率。
accuracy tf.reduce_mean(tf.cast(correct_prediction, tf.flaot32))

# 初始化会话并开始训练过程
with tf.Session() as sess:
   tf.global_variables_initializer().run()
   # 准备验证数据。一般在神经网络的训练过程中会通过验证数据来大致判断停止的
   # 条件和评判训练的效果。
   validate_feed = {x: mnist.validation.images, y_:mnist.validation.labels}
   
   # 准备测试数据。在真实的应用中,这部分数据在训练时是不可见的,这个数据只是作为
   # 模型优劣的最后评价标准。
   test_feed = {x: mnist.test.images, y_:mnist.test.labels}

   # 迭代地训练神经网络。
   for i in range(TRAINING_STEPS):
   # 每1000轮输出一次在验证数据集上的测试结果。
   if i % 1000 == 0:
   # 计算滑动平均模型在验证数据集上的结果。因为MNIST数据集比较小,所以一次
   # 可以处理所有的验证数据。为了计算方便,本样例程序没有将验证数据划分更小的
   # batch。当神经网络模型比较复杂或者验证数据比较大时,太大的batch会导致
   # 计算时间过长甚至发生内存溢出的错误。
        validate_acc = sess.run(accuracy, feed_dict=validation_feed)
        print("After %d training step(s), validation accuracy "
              "using average model is %g " % (i, validate_acc))
   # 产生这一轮使用的一个batch的训练数据,并运行训练过程。
   xs, ys = mnist.train.next_batch(BATCH_SIZE)
   sess.run(train_op, feed_dict={x: xs, y: ys})

# 训练结束之后,在测试数据上检测神经网络模型的最终正确率。
test_acc = sess.run(accuracy, feed_dict=test_feed)
print("After %d training step(s), test accuracy using average "
      "model is %g" % (TRAINING_STEPS, test_acc))


# 主程序入口
def min(argv=None):
    # 声明处理MNIST数据集的类,这个类在初始化时会自动下载数据。
    mnist = input_data.read_data_sets("/tmp/data", one_hot=True)



# Tensorflow提供的一个主程序入口,tf.app.run会调用上面定义的main函数。
if __name__ == ' __main__ '
   tf.app.run()

运行以上程序,将得到类似下面的输出结果:

 Extracting /tmp/data/train-images-idx3-ubyte.gz
 Extracting /tmp/data/train-labels-idx1-ubyte.gz
 Extracting /tmp/data/train-images-idx3-ubyte.gz
 Extracting /tmp/data/train-images-idx1-ubyte.gz
 After 0 training steps(s), validation accuracy on average model is 0.105
 After 1000 training steps(s), validation accuracy on average model is 0.9774
 After 2000 training steps(s), validation accuracy on average model is 0.9816
 After 3000 training steps(s), validation accuracy on average model is 0.9834
 After 4000 training steps(s), validation accuracy on average model is 0.9832
 ...
 
 After 27000 training steps(s), validation accuracy on average model is 0.984
 After 28000 training steps(s), validation accuracy on average model is 0.985
 After 29000 training steps(s), validation accuracy on average model is 0.985
 After 30000 training steps(s), validation accuracy on average model is 0.985
 

从以上结果可以看出,在训练初期,随着训练的进行。模型在验证数据集上的表现越来越好。从4000轮开始。模型在验证数据集上的表现开始波动,这说明模型已经接近极小值了,所以迭代也就可以结束了。

使用验证数据集判断模型效果

以上程序的开始设置了初始学习率、学习率衰减率、隐藏层节点数量、迭代数等7中不同的参数。那么如何设置这些参数的取值呢?在大部分情况下,配置神经网络这些参数需要通过实验来调整的。虽然一个神经网络模型的效果最终是通过测试数据来评判的,但是我们不能直接通过模型在测试数据上的效果选择来选择参数。使用测试数据来选取参数可能会导致神经网络模型过度拟合测试数据,从而失去对未知数据的预判能力。因为一个神经网络的最终目标是对未知数据提供判断,所以为了估计模型在未知数据上的效果,需要保证测试数据在训练过程中是不可见的。只有这样才能保证通过测试数据苹果出来的效果和在真实应用场景下模型对未知数据预判的效果是接近的。于是,为了评测神经网络模型在不同参数取值下模型的表现。除了使用验证数据集,还可以采用交叉验证(cross validation)的方式来验证模型效果。但因为神经网络训练时间本身就长,采用cross validation会花费大量的时间。所以在海量数据的情况下,一般会更多地采用验证集的形式来评测模型的效果。

为了说明验证数据和测试数据上的正确率。为了同时得到同一个模型在验证数据和测试数据上的正确率,可以在每1000轮的输出中加入在测试数据集的正确率。在二中的代码中加入以下代码,就可以得到每1000轮迭代后,使用了滑动平均的模型在验证数据和测试数据上的正确率。

# 计算滑动平均模型在测试数据和验证数据上的正确率。
validate_acc = sess.run(accuracy, feed_dict=validate_feed)
test_acc = sess.run(accuracy, feed_dict=test_feed)

# 输出正确率信息
print("After %d training step(s), validation accuracy using average "
      "model is %g, test accuracy using average model is %g"  %
     (i, validate_acc, test_acc))

验证数据的选取方法是非常重要的,一般来说选取的验证数据分布越接近测试数据分布,模型在验证数据上的表现越可以体现模型在测试数据上的表现。

不同模型效果比较

在神经网络结构的设计上,需要使用激活函数和多层隐藏层。在神经网络优化时,可以使用指数衰减的学习率、加入正则化的损失函数以及滑动平均模型。调整神经网络的结构对最终的正确率有非常大的影响,没有隐藏层或者没有激活函数时,模型的正确率只有大约92.6%,这个数字要远小于使用了隐藏层和激活函数时可以达到的大约98.4%的正确率。这说明神经网络的结构对最终的模型有本质的影响。

滑动平均模型、指数衰减的学习率和使用正则化带来的正确率的提升并不是特别明显。其中使用了所有优化算法的模型和不使用滑动平均的模型以及不使用指数衰减的学习率的模型都可以达到大约98.4%的正确率。这是因为滑动平均模型和指数衰减的学习率在一定程度上都是限制神经网络中参数更新的速度,然而在MNIST数据上,因为模型收敛的速度很快,所以这两种优化对最终模型的影响不大。

当模型迭代到4000轮时正确率就已经接近最终的正确率。而在迭代的早期,是否使用滑动平均模型或者指数帅阿金的学习率对训练结果的影响相对较小。相比滑动平均模型和指数衰减学习率,使用加入正则化的损失函数给模型效果带来的提升要相对显著。使用了正则化损失函数的神经网络模型可以降低大约6%的错误率。一个模型只最小化交叉熵损失,以下代码给出了只优化交叉熵模型的模型优化函数的声明语句。

train_step = tf.train.GradientDescentOptimizer(learning_rate)\.minimize(cross_entropy_mean, global_step=global_step)

另一个模型优化的是交叉熵和L2正则化损失的和。以下代码给出了这个模型优化函数的声明语句。

loss = cross_entropy_mean +regularaztion
train_step = tf.train.GradientDescentOptimizer(learning_rate) \ 
             .minimize(loss, global_step=glabel_step)

只优化交叉熵的模型在训练数据上的交叉熵损失要比优化总损失的模型更小。然而在测试数据集上,优化总损失的模型却要好于只优化交叉熵的模型。这个原因是因为过拟合。只优化交叉熵的模型可以更好地拟合训练数据(交叉熵损失更小),但是却不能很好地挖掘数据中潜在的规律来判断未知的模型数据,所有在测试数据上的正确率低。

通过MNIST数据集有效地验证了激活函数、隐藏层可以给模型的效果带来质的飞跃。由于MNIST问题本身相对简单,滑动平均模型、指数衰减的学习率和正则化损失对最终正确率的提升效果不明显。但通过进一步分析实验的结果,可以得出这些优化方法确实可以解决神经网络优化过程中的问题。当需要解决的问题和使用到的神经网络模型更加复杂时,这些优化方法将更有可能对训练效果产生更大的影响。

三、变量管理

将神经网络前向传播结果的过程抽象成一个函数。通过这种方式再训练和测试的过程中可以统一调用同一个函数来得模型的前向传播结果,这个函数的定义为:

def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):

从定义可也看到,这个函数的参数包括了神经网络中的所有参数。然而,当神经网络的结果更加复杂、参数更多时,就需要一个更好的方式来传递和管理神经网络中的参数了。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_initializer(1.0))

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

从以上代码可以看出,通过tf.Variable和tf.get_variable函数创建变量的过程基本上是一样的。tf.get_variable函数调节时提供的维度(shape)信息以及初始化方式(initializer)的参数和tf.Variable函数嗲用时提供的初始化过程中的参数也类似。tensorflow中提供的

initializer函数和随机数以及常量生成函数大部分是一一对应的。比如,在以上样例中使用到的常数初始化函数tf.constant_initializer和常数生成函数tf.constant功能上就是一致的。tensorflow提供了7中不同的初始化函数,下表总结了它们的功能和主要参数。

初始化函数

功能

主要参数

tf.constant_initializer

将变量初始化为给定常量

常量的取值

tf.random_normal_initializer

将变量初始化为满足正太分布的随机值

正太分布的均值和标准差

tf.truncated_normal_initializer

将变量初始化为满足正太分布的随机值,但如果随机出来的值偏离平均值超过2个标准差,那么这个数将会被重新随机

正太分布的均值和标准差

tf.ranodm_uniform_initializer

将变量初始化为满足平均分布的随机值

最大、最小值

tf.uniform_unit_scaling_initializer

将变量初始化为满足平均分布但不影响输出数量级的随机值

factor(产生随机值时乘以的系数)

tf.zeros_initializer

将变量设置为全0

变量维度

tf.ones_initializer

将变量设置为全1

变量维度

tf.get_variable函数与tf.Variable函数最大的区别在于指定变量名称的参数。对于tf.Variable函数,变量名称是一个可选的参数,通过name="v"的形式给出。但是对于tf.get_variable函数,变量名称是一个必填的参数。tf.get_variable会根据这个名字去创建或者获取变量。在以上样例程序中,tf.get_variable首先会试图去创建一个名字为v的参数,如果创建失败(比如已经有同名的参数),那么这个程序就会报错。这是为了避免无意识的变量复用造成的错误。比如在定义神经网络参数时,第一层网络的权重已经叫weghts了,那么在创建第二层神经网络时,就会触发变量重用的错误。如果需要通过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的变量,所以以下代码将会报错:
# Variable foo/v already exists, disallowed. Did you mean to set reuse = True
# in VarScope ?
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,所以以下代码将会报错:
# Variable bar/v does not exist, disallowed. Did you mean to set reuse=None
# in VarScope?
with tf.variable_scope("bar", reuse=True):
     v =  tf.get_variable("v", [1])

以上样例简单地说明了通过tf.Variable_scope函数就可以控制tf.get_variable函数的语义。当tf.Variable_scope函数使用参数reuse=True生成上下文管理器时,这个上下文管理器内所有的tf.get_variable函数会直接获取已经创建饿变量。如果变量不存在,则tf.get_variable函数将报错;相反,如果tf.variable_scope函数使用参数reuse=None或者reuse=False创建上下文管理器,tf.get_variable操作将创建新的变量。如果同名的变量已经存在,则tf.get_variable函数将报错。tensorflow中tf.variable_scope函数时可以嵌套的。下面的程序说明了当tf.variable_scope函数嵌套时,reuse参数的取值是如何确定的。

with tf.variable_scope("root"):
    # 可以通过tf.get_variable_scope().reuse函数来获取当前上下文管理器中reuse参数的取值
    print tf.get_variable_scope().reuse   # 输出False,即最外层reuse是False。
    
    
    with tf.variable_scope("foo", reuse=True):    # 新建一个嵌套的上下文管理器,
                                                  # 并指定reuse为True。
       print tf.get_variable_scope().reuse        # 输出为True。
       with tf.variable_scope("bar"):             # 新建一个嵌套的上下文管理器但
                                                  # 不指定reuse,这时reuse
                                                  # 的取值会和外面一层保持一致。
          print  tf.get_variable_scope().reuse    # 输出为True。
    print tf.get_variable_scope().reuse           # 输出为False。退出reuse设置
                                                  # 为True的上下文之后
                                                  # reuse的值又回到了False。

tf.variable_scope函数生成的上下文管理器也会创建一个tensorflow中的命名空间,在命名空间内创建的变量名称都会带上这个命名空间作为前缀。所以,tf.variable_scope函数除了可以控制tf.get_variable执行的功能,这个函数也提供了一个管理变量命名空间的方式。以下代码显示了如何通过tf.variable_scope来管理变量的名称。

v1 = tf.get_variable("v", [1])
print  v1.name     # 输出v:0。"v"为变量的名称。“:0”表示这个变量是生成变量这个运算的第一个结果。


with tf.variable_scope("foo"):
     v2 = tf.get_variable("v", [1])
     print v2.name   #  输出foo/v:0。在tf.variable_scope中创建的变量,变量名前会加入
                     #  命名空间的名称,并通过/来分隔命名空间的名称和变量的名称



with tf.variable_scope("foo"):
    with tf.variable_scope("bar"):
       v3 = tf.get_variable("v", [1])
       print v3.name        # 输出foo/bar/v:0.命名空间可以嵌套,同时变量的名称也会加
                            # 入所有命名空间的名称作为前缀。


   v4 = tf.get_variable("v1", [1])
   print v4.name     # 输出foo/v1:0。当命名空间退出之后,变量名称也就不会再被加入
                     # 其前缀了。


# 创建一个名称为空的命名空间,并设置reuse=True。
with tf.variable_scope(" ",reuse=True):
     
     v5 = tf.get_variable("foo/bar/v", [1])     # 可以直接通过带命名空间名称的变量名
                                                # 来获取其他命名空间下的变量。比如这
                                                # 里通过指定名称foo/bar/v来获取在
                                                # 命名空间foo/bar/中创建的变量。
                                                # 输出True。
     print v5 == v3                             
     v6 = tf.get_variable("foo/v1", [1])        
     print  v6 == v4                            # 输出True。

通过tf.variable_scope和tf.get_variable函数,对计算前向传播结果的函数做了一些改进。

def inference(input_tensor, reuse=False):
    # 定义第一层神经网络的变量和前向传播过程。
    with tf.variable_scope('layer1', reuse=reuse):
       # 根据传进来的reuse判断是创建新变量还是使用已经创建好的。在第一次构造网络时
       # 需要创建新的变量,以后每次调用这个函数直接使用reuse=True就不需要
       # 将变量传进来了。
       weights = tf.get_variable("weights", [INPUT_NODE, LAYER1_NODE]
                 initializer=tf.truncated_normal_initializer(stddev=0.1))
       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', reuse=reuse):
         weights =  tf.get_variable("weights", [LAYER1_NODE, OUTPUT_NODE],
                    initializer=tf.truncated_normal_initializer(stddev=0.1))
         biases  =  tf.get_variable("biases", [LAYER1_NODE], 
                    initializer=tf.constant_initializer(0.0)) 
         layer2  =  tf.nn.relu(tf.matmul(layer1, weights) + biases)
    # 返回最后的前向传播结果
    return layer2

x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
y = inference(x)


# 在程序中需要使用训练好的神经网络进行推导时,可以直接调用inference(new_x, True)
# 如果需要使用滑动平均模型,把计算滑动平均的类传到inference函数中即可。获取或者创建变量的
# 部分不需要改变。
new_x = ...
new_y = inference(nex_x, True)

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

四、tensorflow模型持久化

1.持久化代码实现

tensorflow提供了一个非常简单的API来保存和还原一个神经网络模型。这个API就是tf,train.Saver类。以下代码给出了保存tensorflow计算图的方法。

import tenosrflow as tf

# 声明两个变量并计算它们的和。
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
result = v1 + v2


init_op = tf.global_variables_initializer()

# 声明tf.train.Saver类用于保存模型。
saver = tf.train.Saver()


with tf.Session() as sess:
   sess.run(init_op):
   # 将模型保存到/path/to/model/model.ckpt文件。
   saver.save(sess, "/path/to/model/model.ckpt")

以上代码实现了持久化一个简单tensorflow模型的功能。在这段代码中,通过saver.save函数将tensorflow模型保存到了/path/to/model/model.ckpt文件中。tensorflow模型一般会保存在后缀为.ckpt的文件中。虽然以上程序只指定了一个文件路径,但是在这个文件目录下会出现三个文件。这是因为tensorflow会将计算图的结构和图上参数取值分来保存。

上面这段代码会生成的第一个文件为model.ckpt.meta,它保存了tensorflow计算图的结构。第二个文件为model.ckpt,这个文件中保存了tensorflow程序中每一个变量的取值。最后一个文件为checkpoint文件,这个文件中保存了一个目录下所有的模型文件列表。以下代码中给出了加载这个已经保存的tensorflow模型的方法。

import tensorflow as tf


# 使用核保存模型代码中一样的方式来声明变量。
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(1.0, shape=[1]), name="v2")
result = v1 + v2


saver = tf.train.Saver()



with tf.Session() as sess:
     # 加载已经保存的模型,并通过已经保存的模型中变量的值来计算加法。
     saver.restore(sess, "/path/to/model/model.ckpt")
     print sess.run(result)

这段加载模型的代码基本上和保存模型的代码时一样的。在加载模型的程序中也是先定义了tensorflow计算图上的所有运算,并声明了一个tf.train.Saver类。两段代码唯一不同的是,在加载模型的代码中没有运行变量的初始化过程,而是将变量的值通过已经保存的模型加载进来。如果不希望重复定义图上的运算,也可以直接加载已经持久化的图。以下代码给出了一个样例。

import tensorflow as tf
# 直接加载持久化的图。
saver = tf.train.import_meta_graph
        ("path/to/model/model.ckpt/model.ckpt.meta")

with tf.session() as sess:
     saver.restore(sess, "/path/to/model/model.ckpt")
     # 通过张量的名称来获取张量。
     print sess.run(tf.get_default_graph().get_tensor_by_name("add:0"))
     # 输出[ 3.]

在上面的程序中,默认保存和加载了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()来加载模型会报变量找不到的错误。下面显示了报错信息:
# tensorflow.python.framework.errors.NotFoundError: Tensor name "other=v2"
# not found in checkpoint files  /path/to/model.ckpt

# 使用一个字典(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.flaot32, name = "v")
for variables in tf.global_variable():
    print variables.name


ema = tf.train.ExponentialMocvingAverage(0.99)
maintian_average_op = ema.qpply(tf.global_variables())
# 在申明滑动平均模型之后,tensorflow会自动生成一个影子变量
# v/ExponentialMoving Average。于是以下语句会输出
# "v:0"和"v/ExponentialMovingAverage:0"。
for variables in tf.global_variables():
    print variable.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/ExponentialMovingAverage:0两个变量都存下来。
     saver.save(sess, "/path/to/model/model.ckpt")
     print sess.run([v, ema.average(v)])   # 输出[10.0, 0.0999999005]

以下代码给出了如何通过变量重命名直接读取变量的滑动平均。从下面的输出可以看出,读取的变量v的值实际上是上面代码中变量v的滑动平均值。通过这个方法,就可以使用完全一样的代码来计算滑动平均模型前前向传播的结果。

v = tf.Variable(0, dtype=tf.float32, name="v")
# 通过变量重命名将原来变量v的滑动平均值直接赋给v。
saver = tf.train.Saver(("v/ExponentialMovingAverage": v))
with tf.Session() as sess:
     saver.restore(sess, "/path/to/model/model.ckpt")
     print sess.run(v)  # 输出0.099999905,这个值就是原来模型中变量v的滑动平均值。

为了方便加载时重命名滑动平均变量,tf.train.ExponentialMovingAverage类提供了variables_to_restore函数来生成tf.train_Saver类所需要的变量重命名字典。以下代码给出类variables_to_restore函数的使用样例。

import tensorflow as tf

v = tf.Variable(0, dtype=tf.float32, name="v")
ema = tf.train.ExponentialMovingAverage(0.99)


# 通过使用variables_to_restore函数可以直接生成上面代码中提供的字典。
# {"v/ExponentialMOvingAverage": v}。
# 以下代码会输出:
# {'v/ExponentialMOvingAverage': <tensorflow.Variable 'v:0' shape=()
# dtpye=float32_ref>}
# 其中后面的Variable类就代表了变量v。
print ema.variables_to_restore()


saver = tf.train.Saver(ema.variables_to_restore())

with tf.Session() as sess:
     saver.restore(sess, "/path/to/model/model.ckpt")
     print sess.run(v)  # 输出0.099999905,即原来模型中变量v的滑动平均值

使用tf.train.Saver会保存运行tensorflow程序所需要的全部信息,然而有时候并不需要某些信息。比如在测试或者离西安与测试,只需要知道如何从神经网络的输入层经过前向传播稀疏得到输出层即可,而不需要类似于变量初始化、模型保存等辅助节点的信息。而且,将变量取值和计算图结构分成不同的文件存储有时候也不方便,于是tensorflow提供了convert_variable_to_cosntants函数,通过这个函数可以将计算图中的变量及其取值通过常量的方式保存,这样整个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(1.0, shape=[1]), name="v2")
result = v1 + v2

init_op = tf.global_variable_iniitializer()

with tf.Session() as sess:
     sess.run(init_op)
     # 导出当前计算图的GraphDef部分,只需要这一部分就可以完成从输入层到输出层的计算
     # 过程。
     graph_def = tf.get_default_graph().as_graph_def()
     
     # 将图中的变量及其取值转化为常量,同时将图中不必要的节点去掉。
     # 如果只关心程序中定义的某些计算时,和这些计算无关的节点就没有必要导出并保存了。在下面一行
     # 代码中,最后一个参数['add']给出了需要保存的节点名称。add节点是上面定义的两个变量相加的
     # 操作。注意这里给出的是计算节点的名称,所以没有后面的:0。
     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.platfrom 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"])
    # 输出[3.0]
    print sess.run(result)

五、持久化原理及数据格式

tensoeflow是一个通过图的形式来表达计算的编程系统,tensflow程序中的所有计算都会被表达为计算图上的节点。tensorflow通过原图(MateGraph)来记录计算图中节点的信息以及运行计算图中节点所需要的原结构。tensorflow中元图是由MetaGraphDef Proticol Buffer定义的。MetaGraphDef中的内容就构成了tensorflow持久化时的第一个文件。以下代码给出了MetaGraphDef类型的定义。

message MetaGrapher {
    MetaInfoDef meta_info_def = 1;
    
   GraphDef graph_def = 2;
   SaverDef saver_def = 3;
   map<string, CollectionDef> collection_def = 4; 
   map<string, SignatureDef>  signature_def = 5;
   repeated AssetFileDef asset_file_def = 6; 
}

从以上代码可以看到,元图主要记录了6类信息。保存MetaGraphDef信息的文件默认以.meta为后缀名,文件model.ckpt.mate中存储的就是元图的数据。tensorflow提供了export_meta_graph函数,这个函数支持以json格式导出MetaGraphDef Protocol Buffer。以下代码展示了如何使用这个函数。

import tensorflow as tf


# 定义变量相加的计算。
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v1")
result = v1 + v2


saver = tf.train.Saver()
# 通过export_meta_graph函数导出tensorflow计算图的元图,并保存为json格式。
saver.export_meta_graph("/path/to/model.ckpt.meda.json", as_text=True)

通过上面给出的代码,可以将计算图以json的格式导出并存储在model.ckpt.meta.json文件中。下文将结合model.ckpt.meta.json文件具体介绍tensorflow元图中存储的信息。

meta_info_def属性

meta_info_def属性是通过MetaInfoDef定义的,它记录了tensorflow计算图中的元数据以及tensorflow程序中所有使用到的运算方法的信息。下面是MetaInfoDef Protocol Buffer的定义:

message MetaInfoDef {
    string meta_graph_version = 1;
    OpList stripped_op_list = 2;
    google.protobuf.Any any_info = 3;
    repeated string tags = 4;
    string tensorflow_version = 5;
    string tensorflow_git_version = 6;
}

tensorflow计算图的元数据包括了计算图的版本号(meta_graph_version属性)以及用户指定的一些标签(tags属性)。如果没有在saver中特殊指定,那么这些属性都默认为空。在model.ckpt.meta.json文件中,meta_info_def属性里只有stripped_op_list属性是不为空的。stripped_op_list属性记录了tensorflow计算图上使用到的所有运算方法的信息。注意stripped_op_list属性保存的是tensorflow运算方法的信息,所以如果某一个运算在tensorflow计算图中出现了多次,那么在stripped_op_list也只出现一次。比如在model.ckpt.meta.json文件中的stripped_op_list属性中只有一个Variable运算,但这个运算在程序中被使用了两次,stripped_op_list属性的类型是Oplist。Oplist类型是一个OpDef的列表,以下代码给出了OpDef类型的定义:

message OpDef {
    string name = 1;


    repeated ArgDef input_arg = 2;
    repeated ArgDef output_arg = 3;
    repeated AttrDef attr = 4;

    OpDeprecation deprecation = 8;
    string summary = 5;
    string description = 6;
    
    bool is_commutative = 18;
    bool is_aggregate   = 16;
    bool is_statefull   = 17;
    bool allows_uninitialized_input = 19;
};

OpDef类型中前4个属性定义了一个运算最核心的信息。OpDef中的第一个属性name定义了运算的名称,这也是一个运算唯一的标识符。在tensorflow计算图元图的其他属性中,比如下面将要介绍的GraphDef属性,将通过运算名称来引用不同的运算。OpDef的第二和第三个属性为input_arg和output_arg,它们定义了运算的输入和输出。因为输入输出都可以有多个,所以这两个属性都是列表(repeated)。第四个属性attr个西湖了其他的运算参数信息。在model.ckpt,meta,json文件中总共定义了8个运算,下面将给出比较有代表性的一个运算来辅助说明OpDef的数据结构。

op  {
    name: "Add"
    input_arg {
        name: "x"
        type_attr: "T"
    }    
    input_arg {
        name: "y"
        type_attr: "T"
    }
    output_arg{
        name: "x"
        type_attr: "T"
    }
    attr {
       name: "T"
       type: "type"  
       allowed_values  {
          list  {
             type: DT_HALF
             type: DT_FLOAT
             ...
             }
         }
    }
}

上面给出了名称为Add的运算。这个运算有2个输入和1个输出。输入输出属性都指定了属性type_attr,并且这个属性的值为T。在OpDef的attr属性中,必须要出现名称为(name)为T的属性。以上样例中,这个属性指定了晕眩输入输出允许的参数类型(allowed_values)。MetaInfoDef中的tensorflow_version和tesnorflow_git_version属性记录了生成当前计算图的tensorflow版本。

graph_def属性

graph_def属性主要记录了tensorflow计算图上的节点悉尼。tensorflow计算图的每一个节点对盈利额tensorflow程序中的一个运算。因为在meta_info_def属性中已经包含了所有运算的具体信息,所以graph_def属性只关注运算的连接结构。graph_def属性是通过GraphDef Protocol Buffer定义的,GraphDef主要包含了一个NodeDef类型的列表。以下代码给出了GraphDef和NodeDef类型中包含的信息:

message GraphDef{
   repeated NOdeDef node = 1;
   VersionDef versions = 4;
};


message NodeDef {
   string name = 1;
   string op = 2;
   repeated string input = 3;
   string device = 4;
   map<string, AttrValue> attr = 5;
};

GraphDef中的versions属性比较简单,它只要存储了tensorflow的版本号。GraphDef的主要信息都存在node属性中,它记录了tensorflow计算图上所有的节点信息。和其他属性类似,NodeDef类型中有一个名称属性name,它是一个节点的唯一标识符。在tensorflow程序中可以通过节点的名称来获取想用的节点。NodeDef类型中的op属性给出了该节点使用的tensorflow运算方法的名称,通过这个名称可以在tenosrflow计算图元图的meta_info_def属性中找到该运算的具体信息。

NodeDef类型中input属性是一个字符串列表,它定义了运算的输入。input属性中每个字符串的取值格式为node:src_output,其中node部分给出了一个节点的名称,src_ouput部分表明了这个输入时指定节点的第几个输出。当src_output为0时,可以省略:src_output这个部分。比如node:0表示名称为node的节点的第一个输出,它可以被记为node。

NodeDef类型中的device属性指定了处理这个运算的设备。运行tensorflow运算的设备可以是本地机器的CPU或者GPU,也可以是一台远程的机器CPU或者GPU。当device属性为空时,tensorflow在运行时会自动选取一个最合适的设备来运行这个运算。最后NodeDef类型中的attr属性指定了和当前运算相关的配置信息。下面列举了model.ckpt.meta.json文件中的一些计算节点来更加具体地介绍graph_def属性。

graph_def  {
  node {
    name: "v1"
    op: "VariableV2"
    attr  { 
      key: "_output_shapes" 
      value { 
        list  {  shape  {   dim   {  size:  1    }   }   }
      }
    }
    
    attr  {
       keys: "dtype"
       value  {
          type: DT_FLOAT
       }
    }
    ...
    }
    node  {
       name: "add"
       op: "Add"
       input: "v1/read"
       input: "v2/read"
       ...
    }
    
    node  {
      named: "save/control_dependency"
      op: "Identity"
      ...
    }
    
    version {
      preducer: 24
    }


 }

上面给出了model.ckpt.meta.json文件中graph_def属性里比较有代表性的几个节点。第一个节点给出的是变量定义的运算。在tensorflow中变量定义也是一个运算,这个运算的名称为v1(name:"v1"),运算方法的名称为Variable(op:"VariableV2")。定义变量的运算可以有很多个,于是在NodeDef类型的node属性中可以有多个变量定义的节点。但定义变量的运算方法只用到了一个,于是在MetaInfoDef类型的stripped_op_list属性中只有一个名称为VariableV2的运算方法。除了指定计算图中节点的名称和运算方法,NodeDef类型还定义了运算相关的属性。在节点v1中,attr属性指定了这个变量的维度以及类型。

给出的第二个节点是代表加法的节点。它指定了2个输入,一个为v1/read,另一个为v2/read。其中v1/read代表的节点可以读取变量v1的值。因为v1的值是节点v1/read的第一个输出,所以后面的:0就可以省略了。v2/read也类似的代表了变量v2的取值。以上样例文件中给出的最后一个名称为save/control_dependency,该节点是系统在完成tensorflow模型持久化过程中自动生成的一个运损。在样例文件的最后,属性version给出了生成model.skpt.meta.json文件时使用的tensorflow版本号。

saver_def属性

save_def属性中记录了持久化模型时需要用到的一些参数,比如保存到文件的文件名、保存操作和加载操作和加载操作的名称以及保存频率、清理历史记录等。saver_def属性的类型为SaverDef,其定义如下。

message SaverDef {
    string filename_tensor_name = 1;
    string save_tensor_name = 2;
    string restore_op = 3;
    int32 max_to_keep = 4; 
    bool sharded = 5;
    float keep_checkpoint_every_n_hours = 6;
    
    enum CheckpointFormatVersion {
      LEGACY = 0; 
      V1 = 1;
      v2 = 2;
    }
    CheckpointFormatVersion version = 7;
}

下面给出了model.ckpt.mate.json文件中saver_def属性的内容。

saver_def {
   filename_tensor_name: "save/Const:0"
   save_tensor_name: "save/control_dependency:0"
   restore_op_name: "save/restore_all"
   max_to_keep: 5
   keep_checkpoint_every_n_hours: 10000.0
   version: V2
}

filename_tensor_name属性给出了保存文件名的张量名称,这个张量就是节点save/Const的第一个输出。save_tensor_name属性给出了持久化tensorflow模型的运算所对应的节点名称。从以上文件可以看出,这个节点就是在graph_def属性中给出的save/control_dependency节点。和持久化tensorflow模型运算对应的是加载tensorflow模型的运算,这个运算的名称是由restore_op_name属性指定。max_to_keep属性和keep_checkpoint_every_n_hours属性设定了tf,train.Saver类清理之前保存的模型的策略。比如当max_to_keep为5的时候,在第六次调用saver.save时,第一次保存的模型就会被自动删除。通过设置keep_checkpoint_every_n_hours,每n小时可以在max_to_keep的基础上多保存一个模型。

collection_def属性

在tensorflow的计算图(tf.Graph)中可以维护不同集合,而维护这些集合的底层实现就是通过collection_def这个属性。collection_def属性是一个从集合内容的映射,其中集合名称为字符串,而集合内容为CollectionDef Protocol Buffer。以下代码给出了CollectionDef类型的定义。

message CollectionDef{
   message NodeList {
      repeated string value = 1;
   }

   message BytesList {
      repeated bytes value = 1;
   }
   
   message Int64List {
      repeated int64 value = 1 [packed = true];
   
   message FloatList {
      repeated float value = 1 [packed = true];
   }
   
   message AnyList {
      repeated google.protobuf.Any value = 1;
   } 
   
   oneof kind {
     NoneList node_list = 1;
     BytesList bytes_list = 2;
     Int64List int64_list = 3;
     FloatList float_list = 4;
     AnyList any_list = 5;
   }
}

通过以上定义可以看出,tensorflow计算图上的集合主要可以维护4类不同的集合。NodeLIst用于维护计算图上节点的集合。ByteList可以维护字符串或者系列化之后的Procotol Buffer的集合。比如张量是通过Protocol Buffer表示的,而张量的集合是通过BytesList维护的,我们将在model.ckpt.meta.json文件中看到具体样例。Int64List用于维护整数集合,FloatLIst用于维护实数集合。下面给出了model.ckpt.meta.json文件中collection_def属性的内容。

collection_def {
   key: "trainable_variables"
   value {
      bytes_list {
         value: "\n\004v1:0\022\tv1/Assign\032\tv1/read:0"
         value: "\n\004v1:0\022\tv1/Assign\032\tv2read:0"
      }
   }
}
collection_def {
   key: "vatiables"
   value {
      bytes_list {
         value: "\n\004v1:0\022\tv1/Assign\032\tv1/read:0" 
         value: "\n\004v1:0\022\tv1/Assign\032\tv2/read:0"
     }
   }
}

从以上文件可以看出样例程序中维护了两个集合。一个是所有变量的集合。这个集合的名称为variable。另外一个是可训练变量的集合,名为trainable_variables。在样例程序中,这两个集合中的元素是一样的,都是变量v1和v2,它们都是系统自动维护的。

通过对MetaGraphDef类型中主要属性的讲解,本节已经介绍了tensorflow模型持久化得到的第一个文件的内容。除了持久化tensorflow计算图的结构,持久化tensorflow中变量的取值也是非常重要的一个部分。其中model.ckpt.data文件时通过SSTable格式存储的,可以大致理解为就是一个(key, value)列表。tensorflow提供了tf.train.NewCheckpointReader类来查看保存的变量信息。以下代码展示了如何使用tf.train.NewCheckpointReader类。

import tensorflow as tf

# tf.train.NewCheckpointReader可以读取checkpoint文件中保存的所有变量。
# 注意后面的.data和.index可以省去。
reader = tf.train.NewCheckpointReader('/path/to/model/model.test')


# 获取所有变量列表。这个是一个从变量名到变量维度的字典。
global_variable = reader.get_variable_to_shape_map()
for variable_name in global_variables:
    # variable_name 为变量名称,global_variable[variable_name]为变量的维度。
    print variable_name, global_variables[variable_name]


# 获取名称为v1的变量的取值。
print "Value for variable v1 is ", reader.get_tensor("v1")


'''
这个程序将输出:
v1 [1]                            # 变量v1的维度为[1]。
v2 [2]                            # 变量v2的维度为[1]。
Value for variable v1 is [ 1.]    # 变量v1的取值为1。

最后一个文件的名字是固定的,叫checkpoint。这个文件是tf.train.Saver类自动生成且自动维护的。在checkpoint文件中维护了由一个tf.train.Saver类持久化的所有tensorflow模型文件的文件名。当某个保存的tensorflow模型文件被删除时,这个模型所对应的文件名也从checkpoint文件中删除。checkpoint中内容的格式为CheckpointState Protocol Buffer,下面给出了CheckpointState类型的定义。

message CheckpointState {
   string model_checkpoint_path = 1;
   repeated string all_model_checkpoint_paths = 2;
}

model_checkpoint_path属性保存了最新的tensorflow模型文件的文件名。all_model_checkpoint_paths属性列出了当前还没有被删除的所有tensorflow模型文件的文件名,下面给出了checkpoint文件。

model_checkpoint_path: "/path/to/model/model.ckpt"
all_model_checkpoint_paths: "/path/to/model/model.ckpt"

五、基于tensorflow完整的MNIST手写数字识别问题解决

程序一共分为三部分,第一个是mnist_inference.py,它定义了前向传播的过程以及神经网络中的参数,第二个是mnist_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的集合。在这里
# 使用了add_to_collection函数将一个张量加入一个集合,而这个集合的名称为losses。
# 这里自定义的集合,不在tensorflow自动管理的集合列表中。
if regularizer !=None:

    tf.add_to_collection('losses', regularizer(weights))

return weights

# 定义神经网络的前向传播过程
def inference(input_tensor, regularizer):
    # 声明第一层神经网络的变量并完成前向传播过程。
    with tf.variable_scope('layer1'):
       # 这里通过tf.get_variable或tf.Variable没有本质区别,因为在训练或者是测试中
       # 没有在同一程序中多次调用这个函数。如果在同一个程序中多次调用,在第一次调用
       # 之后需要将reuse参数设置为True。
       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_tenosr, 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_initilizer(0.0))
        layer2 = tf.matmul(layer1, weights) + biases
    # 返回最后前向传播的结果。
    return layer2

这段代码中定义了神经网络的前向传播算法。无论是训练还是测试时,都可以直接调用inference这个函数,而不用关心具体的神经网络结构。使用定义好的前向传播过程,以下代码给出了神经网络的训练程序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



# 配置神经网络的参数。
BTACH_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_average = tf.train.ExponentialMovingAVerage(
       MOVING_AVERAGE_DECAY, global_step)
   variables_average_op = variable_average.apply(
       tf.trainable_variables())
   cross_entropy_mean = tf.reduce_mean(cross_entropy)
   loss = cross_entropy_mean + tf.add_n(tf.get_collection('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, variables_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.next_batch(BATCH_SIZE)
            _, loss_value, step = sess.run([train_op, loss,global_steps],
                                           feed_dict={x: xs, y_: ys})
        # 每1000轮保存一次模型。
        if i % 1000 == 0:
            # 输出当前的训练情况,这里只输出了模型在当前训练batch上的损失函
            # 数大小。通过损失函数的大小可以大概了解训练的情况。在验证数据集上的
            # 正确率信息会有一个单独的程序来生成。
            print("After %d training step(s), loss on training "
                 "batch is %g." % (step, loss_value))
            # 保存当前的模型,注意这里给出了global_step参数。这样可以让每个被
            # 保存模型的文件名末尾加上训练的轮数,比如"model.ckpt-1000"表示
            # 训练1000轮之后得到的模型。
            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("/path/to/mnist_data", one_hot=True)
   train(mnist)


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

运行以上程序,可以得到类似下面的结果。

~/mnist python mnist_train.py
Exracting /tmp/data/train-images-idx3-ubytes.gz
Exracting /tmp/data/train-labels-idx1-ubytes.gz
Exracting /tmp/data/t10k-images-idx3-ubytes.gz
Exracting /tmp/data/t10k-labels-idx1-ubytes.gz
After 1 traing step(s), loss on training batch is 3.32075.
After 1001 traing step(s), loss on training batch is 0.241039.
After 2001 traing step(s), loss on training batch is 0.227391.
After 3001 traing step(s), loss on training batch is 0.138462.
After 4001 traing step(s), loss on training batch is 0.132074.
After 5001 traing step(s), loss on training batch is 0.103472.

在新的训练代码中,不再将训练数据和测试跑在一起。训练过程中,每1000轮输出一次在当前训练batch上损失函数的大小来大致估计模型的效果。在以上程序中,每1000轮保存一次训练好的模型,这样可以通过一个单独的测试程序,更加方便地在滑动平均模型上做测试。以下代码给出了测试程序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.palceholder(
          tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')
          
      y_ = tf.placeholder(
          tf.float32, [None, mnist_inferernce.OUTPUT_NODE], name='y-input')
      validation_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_DEACY)
      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_check_point)
                  # 通过文件名得到模型保存时迭代的轮数。
                  global_step = ckpt.model_checkpoint_path\ 
                                    .split('/')[-1].split('-')[-1]
                  accuracy_score = sess.run(accuracy,
                                           feed_dict=validate_feed) 
                  print("After %s training step(s), 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("/path/to/mnist_data", one_hot=True)


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

上面给出的mnist_eval.py程序会每个10秒运行一次,每次运行都是最新保存的模型,并在MNIST验证数据集上计算模型的正确率。如果需要离线预测未知数据的类别(比如这个样例程序可以判断手写体数字图片中包含的数字)。只需要将计算正确率的部分改为答案输出即可。运行mnist_eval.py程序可以得到类似下面的结果。注意因为这个程序每10秒自动运行一次,而训练程序不一定每10秒输出一个新模型,所以在下面的结果中发现有些模型被测试了多次。一般在解决真实问题时,不会这么频繁地运行评测程序。

~/mnist python mnist_train.py
Exracting /tmp/data/train-images-idx3-ubytes.gz
Exracting /tmp/data/train-labels-idx1-ubytes.gz
Exracting /tmp/data/t10k-images-idx3-ubytes.gz
Exracting /tmp/data/t10k-labels-idx1-ubytes.gz
After 1 traing step(s), loss on training batch is 0.1282.
After 1001 traing step(s), loss on training batch is 0.9769.
After 1001 traing step(s), loss on training batch is 0.9769.
After 2001 traing step(s), loss on training batch is 0.9804.
After 3001 traing step(s), loss on training batch is 0.982.
After 4001 traing step(s), loss on training batch is 0.983.
After 5001 traing step(s), loss on training batch is 0.9829.
After 6001 traing step(s), loss on training batch is 0.9832.
After 6001 traing step(s), loss on training batch is 0.9832.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券