一、MNIST数据集介绍

MNIST是一个非常有名的手写体数字识别数据集,在很多资料中,这个数据集都会作为深度学习的入门样例。下面大致介绍这个数据集的基本情况,并介绍tensorflow对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)。
二、神经网络模型训练及不同模型结果对比

1、tensorflow训练神经网络

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。


# 配置神经网络的参数
LAYER1_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], stddev=0.1))
    biases1  = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))

    # 生成输出层的参数。
    weights2 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=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_softmax_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 main(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()

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

    基于tensorflow的MNIST数字识别_tensorflow

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

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

以上程序的开始设置了初始学习率、学习率衰减率、隐藏层节点数量、迭代数等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))

不同问题的数据分布不一样,如果验证数据分布不能很好地代表测试数据分布,那么模型在这两个数据集上的表现就有可能不一样。所以,验证数据的选取方法是非常重要的,一般来说选取的验证数据分布越接近测试数据分布,模型在验证数据的选取方式是非常重要的,一般来说选取的验证数据分布越接近测试数据分布,模型在验证数据上的表现越可以体现模型在测试数据上的表现。

3.不同模型效果比较

在神经网络结构的设计上,需要使用激活函数和多层隐藏层。在神经网络优化时,可以使用指数衰减的学习率、加入正则化的损失函数以及滑动平均模型。调整神经网络的结构对最终的正确率有非常大的影响,没有隐藏层或者没有激活函数时,模型的正确率只有大约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了,那么在创建第二层神经网络时,如果参数名仍然叫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的变量,所以以下代码将会报错:
# 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完整的MNIST手写数字识别问题解决

程序一共分为三部分,第一个是mnist_inference.py,它定义了前向传播的过程以及神经网络中的参数,第二个是mnist_train.py,它定义了神经网络的训练过程。第三个是mnist_eval.py,它定义了测试过程。以下代码给出了mnist_inference.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。

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 = "/home/user8/PycharmProjects/test/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)

    variables_averages_op = variable_averages.apply(
        tf.trainable_variables())

    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
        logits=y, labels=tf.argmax(y_, 1))

    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.train.next_batch(BATCH_SIZE)
            _, loss_value, step = sess.run([train_op, loss, global_step],
                                           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数据集的类,这个类在初始化时会自动下载数据。
    mnist = input_data.read_data_sets("/home/user8/dataset", one_hot = True)
    train(mnist)

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

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

                                    基于tensorflow的MNIST数字识别_数据集_02

在新的训练代码中,不再将训练数据和测试跑在一起。训练过程中,每1000轮输出一次在当前训练batch上损失函数的大小来大致估计模型的效果。在以上程序中,每1000轮保存一次训练好的模型,这样可以通过一个单独的测试程序,更加方便地在滑动平均模型上做测试。以下代码给出了测试程序mnist_eval.py。

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_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.