TensorFlow之MNIST数字识别问题 – Python量化投资

TensorFlow之MNIST数字识别问题

前面一章介绍了训练神经网络模型时需要考虑的主要问题以及解决这些问题的常用方法。这一章将通过一个实际问题来验证上一章中介绍的解决方法。本章将使用的数据集是MNIST手写体数字识别数据集。在很多深度学习教程中,这个数据集都会被当做第一个案例。在验证神经网络优化方法的同时,本章也会介绍使用TensorFlow训练神经网络的最佳实践。
1.介绍MNIST手写体数字识别数据集,并且给出TensorFlow程序处理MNIST数据。
2.对上一章提到的神经网络结构设计和参数优化的不同方法,从实际的问题中验证不同优化方法带来的性能提升。
3.指出上一节中TensorFlow程序实现神经网络的不足之处,并介绍TensorFlow最佳实践来解决这些不足。
4.将介绍TensorFlow变量重用的问题和变量的命名空间
5.介绍如何将一个神经网络模型持久化,使得之后可以直接使用训练好的模型
6.最后将整合前面介绍的TensorFlow最佳实践,通过一个完整的TensorFlow程序解决MNIST问题。

1.MNIST数据处理

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


数字图片及其像素矩阵

在上图左侧显示了一张数字1的图片,而右侧显示了这个图片所对应的的像素矩阵。在Yann LeCun教授的网站中(http://yann.lecun.com/exdb/mnist)对MNIST数据集做出了详细的介绍。MNIST数据集提供了4个下载文件,下面归纳了下载文件中提供的内容:

MNIST数据下载地址和内容

虽然这个数据集只提供了训练和测试数据,但是为了验证模型训练的效果,一般会从训练数据中划分出一部分数据作为验证数据。下节将更加详细地介绍验证数据的作用。为了方便使用,TensorFlow提供了一个类来处理MNIST数据。这个类会自动下载并转化MNIST数据的格式,将数据从原始的数据包中解析成训练和测试神经网络时使用的格式。下面给出了使用这个函数的样例程序:
1.读取数据集,第一次TensorFlow会自动下载数据集到下面的路径中:

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("../../datasets/MNIST_data/", one_hot=True)

运行代码,结果如下:
Extracting ../../datasets/MNIST_data/train-images-idx3-ubyte.gz
Extracting ../../datasets/MNIST_data/train-labels-idx1-ubyte.gz
Extracting ../../datasets/MNIST_data/t10k-images-idx3-ubyte.gz
Extracting ../../datasets/MNIST_data/t10k-labels-idx1-ubyte.gz

2.数据集会自动被分成3个子集,train、validation和test。以下代码会显示数据集的大小:

print "Training data size: ", mnist.train.num_examples
print "Validating data size: ", mnist.validation.num_examples
print "Testing data size: ", mnist.test.num_examples

运行代码,结果如下:
Training data size: 55000
Validating data size: 5000
Testing data size: 10000

3.查看training数据集中某个成员的像素矩阵生成的一维数组和其属于的数字标签:

print "Example training data: ", mnist.train.images[0] 
print "Example training data label: ", mnist.train.labels[0]

打印结果如下图:


某个成员的像素矩阵

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

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

打印结果如下:
X shape: (100, 784)
Y shape: (100, 10)

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

本节将利用MNIST数据集实现并研究上一章中介绍的神经网络模型设计及优化的方法。
1.给出一个完整的TensorFlow程序来解决MNIST问题。这个程序整合了上一章介绍的所有优化方法,训练好的神经网络模型在MNIST测试集上可以达到98.4%左右的正确率。
2.介绍验证数据集在训练神经网络过程中的作用,将通过上一小节中得到的验证数据来证明,神经网络在验证数据集上的表现可以近似地作为评价不同神经网络模型的标准或者决定迭代轮数的依据
3.最后一小节将通过MNIST数据集验证上一章介绍的每一个优化方法。通过在MNIST数据集上的实验可以看到,这些优化方法都可以或多或少地提高神经网络的分类正确率。

2.1TensorFlow训练神经网络

这一小节将给出一个完整的TensorFlow程序来解决MNIST手写体数字识别问题。这一小节中给出的程序实现了上一章中介绍的神经网络结构设计和训练优化的所有方法。在给出具体的代码之前,先回顾一下第4章中提到的主要概念。在神经网络的结构上,深度学习一方面需要使用激活函数实现神经网络模型的去线性化,另一方面需要使用一个或多个隐藏层使得神经网络的结构更深,以解决复杂问题。在训练神经网络时,上一章介绍了使用带指数衰减的学习率设置、使用正则化来避免过度拟合,以及使用滑动平均模型来使得最终模型更加健壮。以下代码给出了一个在MNIST数据集上实现这些功能的完整的TensorFlow程序:

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
# MNIST数据集相关的常数
INPUT_NODE = 784  # 输入层的节点数。对于MNIST数据集,这个就等于图片的像素。
OUTPUT_NODE = 10  # 输出层的节点数。这个等于类别的数目。因为在MNIST数据集中需要区分的是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_STEPS = 30000  # 训练轮数
MOVING_AVERAGE_DECAY = 0.99  # 滑动平均衰减率
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', [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))
        layer2 = tf.nn.relu(tf.matmul(layer1, weights) + biases)
    return layer2
# 一个辅助函数,给定神经网络的输入和所有参数,计算神经网络的前向传播结果。在这里定义了一个使用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函数,
        # 所以这里不需要加入激活函数。而且不加入Softmax不会影响预测结果。因为预测时
        # 使用的是不同类别对应节点输出值的相对大小,有没有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.average(biases1))
        return tf.matmul(layer1, avg_class.average(weights2)) +\
               avg_class.average(biases2)
# 训练模型的过程
def train(mnist):
    x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [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([LAYER1_NODE, OUTPUT_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的参数。
    variables_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函数来得到正确答案对应的类别编号。
    labels = tf.argmax(y_, 1)
    cross_entroy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=labels)
    # 计算当前batch中所有样例的交叉熵平均值。
    cross_entropy_mean = tf.reduce_mean(cross_entroy)
    # 计算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_average_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.float32))
    # 初始化会话并开始训练过程
    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=validate_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("C:\\Users\\1003342\\Desktop\\study\\tensorflow\\data", one_hot=True)
    train(mnist)
if __name__ == '__main__':
    tf.app.run()

运行代码,打印结果如下图:


完整的神经网络程序运行结果

结果分析:从上面的结果可以看出,在训练初期,随着训练的进行,模型在验证数据集上的表现越来越好。从第1000轮开始,模型在验证数据集上的表现开始波动,这说明模型已经接近极小值了,所以迭代也就可以结束了。下面小节将详细介绍验证数据集的作用。

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

在上一小节给出了使用神经网络解决MNIST问题的完整程序。在这个程序的开始设置了初始学习率、学习率衰减率、隐藏层节点数量、迭代轮数等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))

运行代码,得到如下图所示的结果:


使用滑动平均模型在验证数据和测试数据上的正确率

下图给出了通过上面代码得到的每1000轮滑动平均模型在不同数据集上的正确率曲线。下图中灰色曲线代表随着迭代轮数的增加,模型在验证数据上的正确率;而黑色的曲线表示了在测试数据上的正确率。从下图可以看出,虽然这两条曲线不会完全重合,但是着两条曲线的趋势基本一样,完全可以通过模型在验证数据上的表现来判断一个模型的优劣:


不同迭代轮数下滑动平均模型在验证数据集和测试数据集上的正确率

当然,以上结论是针对MNIST这个数据集的,对于其他问题,还需要具体问题具体分析。不同问题的数据分布不一样。所以,验证数据的选取方法是非常重要的,一般来说选取的验证数据分布越接近测试数据分布,模型在验证数据上的表现越可以体现模型在测试数据上的表现。但通过本小节介绍的实验,至少可以说明通过神经网络在验证数据上的效果来选取模型的参数是一个可行的方案。

2.3不同模型效果比较

本小节将通过MNIST数据集来比较上一章提到的不同优化方法对神经网络模型正确率的影响。本小节将使用神经网络模型在MNIST测试数据集上的正确率将简称为“正确率”。在第4章中提到了设计神经网络时的5种优化方法。在神经网络结构的设计上,需要使用激活函数和多层隐藏数。在神经网络优化时,可以使用指数衰减的学习率、加入正则化的损失函数以及滑动平均模型。在下图中,给出了在相同神经网络参数下,使用不同优化方法,经过30000轮训练迭代后,得到的最终模型。通过这种方式,可以有效验证每一项优化方法的效果:


不同模型的正确率

从上图可以很明显地看出,调整神经网络的结构对最终的正确率有非常大的影响。没有隐藏层或者没有激活函数时,模型的正确率只有大约92.6%,这个数字要远远小于使用了隐藏层和激活函数时可以达到的大约98.4%的正确率。这说明神经网络的结构对最终模型的效果有本质性的影响。下一章将会介绍一种更加特殊的神经网络结构——卷积神经网络。卷积神经网络可以更加有效地处理图像信息。通过卷积神经网络,可以进一步将正确率提高到大约99.5%。
从上图中的数字可发现使用滑动平均模型、指数衰减率的学习率和使用正则化带来的正确率的提升并不是特别明显。其中使用了所有优化算法的模型以及不使用指数衰减的学习率在一定程度上都是限制神经网络中参数更新的速度,然而在MNIST数据上,因为模型收敛的速度很快,所以这两种优化对最终模型的影响不大。从上上图可以看到,当模型迭代到4000轮时正确率就已经接近最终的正确率了。而在迭代的早期,是否使用滑动平均模型或者指数衰减的学习率对训练结果的影响相对较小。下图显示了不同迭代轮数时,使用了所有优化方法的模型的正确率与平均绝对梯度的变化趋势:


使用了所有优化方法的模型正确率与平均梯度在不同迭代轮数时的变化趋势

从上图可以看到,前4000轮迭代模型的改变是最大的。在4000轮之后,因为梯度本身比较小,所以参数的改变也就比较缓慢了。于是滑动平均模型或者指数衰减的学习率的作用也就没有那么突出了。

下图显示了不同迭代轮数时,正确率与衰减之后的学习率的变化趋势:


使用了所有优化方法的模型的正确率与学习率在不同迭代轮数时的变化趋势

从上图可以看到,学习率曲线呈现出阶梯状衰减,在前4000轮时,衰减之后的学习率和最初的学习率差距并不大。那么,这是否能说明这些优化方法作用不大呢?答案是否定的。当问题更加复杂时,迭代不会这么快接近收敛,这时滑动平均模型和指数衰减的学习率可以发挥更大的作用。比如在Cifar-10图像分类数据集上,使用滑动平均模型可以将错误率降低11%,而使用指数衰减的学习率可以将错误率降低7%。
相比滑动平均模型和指数衰减学习率,使用加入正则化的损失函数给模型效果带来的提升要相对显著。使用了正则化损失函数的神经网络模型可以降低大约6%的错误率(从1.69%降低到1.59%)。
下面两图显示了正则化给模型优化过程带来的影响,两个图对比了两个使用了不同损失函数的神经网络模型。一个模型只最小化交叉熵损失,以下代码给出了只优化交叉熵模型的模型优化函数的声明语句:

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=global_step)

下图中灰色和黑色的实线给出了两个模型正确率的变化趋势,虚线给出了在当前训练batch上的交叉熵损失:


不同模型在不同迭代轮数时交叉熵和正确率的关系

从上图可以看出,只优化交叉熵的模型在训练数据上的交叉熵损失(灰色虚线)要比优化总损失的模型更小(黑色虚线)。然而在测试数据上,优化总损失的模型(黑色实线)却要好于只优化交叉熵的模型(灰色实线)。这个原因就是上一章中介绍的过拟合问题。只优化交叉熵的模型可以更好地拟合训练数据(交叉熵损失更小),但是却不能很好地挖掘数据中潜在的规律来判断未知的测试数据,所以在测试数据上的正确率低。

下图显示了不同模型的损失函数的变化趋势:


正则化损失值和总损失的变化趋势

上图的左侧显示了只优化交叉熵的模型损失函数的变化规律。可以看到随着迭代的进行,正则化损失是在不断加大的。因为MNIST问题相对比较简单,迭代后期的梯度很小,所以正则化损失的增长也不快。如果问题更加复杂,迭代后期的梯度更大,就会发现总损失(交叉熵损失加上正则化损失)会呈现一个U型。在上图的右侧,显示了优化总损失的模型损失函数的变化规律。从上图中可以看出,这个模型的正则化损失部分也可以随着迭代的进行越来越小,从而使得整体的损失呈现一个逐步递减的趋势。
总的来说,通过MNIST数据集有效地验证了激活函数、隐藏层可以给模型的效果带来质的飞跃。由于MNIST问题本身相对简单,滑动平均模型、指数衰减的学习率和正则化损失对最终正确率的提升效果不明显。但通过进一步分析实验的结果,可以得出这些优化方法确实可以解决上一章提到的神经网络优化过程中的问题。当需要解决的问题和使用到的神经网络模型更加复杂时,这些优化方法将更有可能对训练效果产生更大的影响。

3.变量管理

在上一小节中将计算神经网络的前向传播结果的过程抽象成了一个函数。通过这种方式在训练和测试的过程中可以统一调用同一个函数来得到模型的前向传播结果。在上一节中,这个函数的定义为:
def inference(input_tensor, avg_class, weights1, biases1,weights2,biases2):
从定义中可以看到,这个函数的参数中包含了神经网络中的所有参数。然而神经网络的结构更加复杂、参数更多时,就需要一个更好的方式来传递和管理神经网络中的参数了。TensorFlow提供了通过变量名称来创建或者一个变量的机制。通过这个机制,在不同的函数中可以直接通过变量的名字来使用变量,而不需要将变量通过参数的形式到处传递。TensorFlow中通过变量名称获取变量的机制主要是通过tf.get_variable和tf.variable_scope函数实现的。下面将分别介绍如何使用这两个函数。
在上一章介绍了通过tf.Variable函数来创建一个变量。除了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种不同的初始化函数,下表总结了它们的功能和主要参数。


TensorFlow中的变量初始化函数

tf.get_variable函数与tf.Variable函数最大的区别在于指定变量名称的参数。对于tf.Variable函数,变量名称是一个可选的参数,通过name=”v”的形式给出。但是对于tf.get_variable函数,变量名称是一个必填的参数。tf.get_variable会根据这个名字去创建或者获取变量。在上面的样例程序中,tf.get_variable首先会试图去创建一个名字为v的参数,如果创建失败(比如应有同名的参数),那么这个程序就会报错。这是为了避免无意识的变量复用造成的错误。比如在定义神经网络参数时,第一层网络的权重已经叫weights了,那么在创建第二层神经网络时,如果参数名仍然叫weights,就会触发变量重用的错误。否则两层神经网络共用一个权重会出现一些比较难以发现的错误。如果需要通过tf.get_variable获取一个已经创建的变量,需要通过tf.variable_score函数来生成一个上下文管理器,并明确指定在这个上下文管理器中,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)
# 将参数reuse设置为True时,tf.variable_scope将只能获取已经创建过的变量。
# 因为在命名空间bar中还没有创建变量v,所以下面的代码将会报错:
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.varaible_scope函数时可以嵌套的。下面的程序说明了当tf.variable_scope函数嵌套时,reuse参数的取值是如何确定的。

with tf.variable_scope("root"):
    # 可以通过tf.get_variable_scope().reuse 函数来获取当前上下文管理器中reuse参数的取值
    print(tf.get_variable_scope().reuse)
    
    with tf.variable_scope("foo", reuse=True):
        print(tf.get_variable_scope().reuse)
        
        with tf.variable_scope("bar"):
            print(tf.get_variable_scope().reuse)
    
    print(tf.get_variable_scope().reuse)

运行代码,得到如下结果:
False
True
True
False

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

v1 = tf.get_variable("v", [1])
print(v1.name)
with tf.variable_scope("foo",reuse=True):
    v2 = tf.get_variable("v", [1])
print(v2.name)
with tf.variable_scope("foo"):
    with tf.variable_scope("bar"):
        v3 = tf.get_variable("v", [1])
        print(v3.name)
        
v4 = tf.get_variable("v1", [1])
print(v4.name)
# 创建一个名称为空的命名空间,并设置reuse=True。

运行代码,得到如下结果:
v:0
foo/v:0
foo/bar/v:0
v1:0

通过变量名来获取变量:

with tf.variable_scope("", reuse=True):
    v5 = tf.get_variable("foo/bar/v", [1])
    print(v5 == v3)
    v6 = tf.get_variable("foo/v1", [1])
    print(v6==v4)

运行代码,得到如下结果:
True
True
通过tf.variable_scope和tf.get_variable函数,以下代码对2.1小节中定义的计算前向传播结果的函数做了一些改进:

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', [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))
        layer2 = tf.nn.relu(tf.matmul(layer1, weights) + biases)
x = tf.placeholder(tf.float32,[None, INPUT_NODE],name='x-input')
y = inference(x)
new_x = ...
new_y = inference(new_x,True)

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

4.TensorFlow模型持久化

在2.1小节中给出的样例代码在训练完成之后就直接退出了,并没有将训练得到的模型保存下来方便下次直接使用。为了让训练结果可以复用,需要将训练得到的神经网络模型持久化.我们将介绍:
1.通过TensorFlow程序来持久化一个训练好的模型,并从持久化之后的模型文件中还原被保存的模型
2.TensorFlow持久化的工作原理和持久化之后文件中的数据格式。

4.1持久化代码实现

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

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="v2")
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)
    # 将模型保存
    saver.save(sess, "C:\\Users\\1003342\\Desktop\\study\\tensorflow\\model_data\\model.ckpt")

上面的代码实现了持久化一个简单的TensorFlow模型的功能。在这段代码中,通过saver.save函数将TensorFlow模型保存到了C:\Users\1003342\Desktop\study\tensorflow\model_data\model.ckpt文件中。TensorFlow模型一般会存在后缀为.ckpt的文件中。虽然上面的程序只指定了一个文件路径,但是在这个文件目录下会出现三个文件。这是因为TensorFlow会将计算图的结构和图上参数取值分开保存。
上面这段代码会生成的第一个文件为model.ckpt.meta,它保存了TensorFlow计算图的结构。在前面2章介绍了TensorFlow计算图的原理,这里可以简单理解为神经网络的网络结构。第二个文件为model.ckpt,这个文件中保存了TensorFlow程序中每一个变量的取值。最后一个文件为checkpoint文件,这个文件中保存了一个目录下
所有的模型文件列表。对这些文件中的具体内容,下一小节将详细讲述。以下代码给出了加载这个已经保存的TensorFlow模型的方法:

with tf.Session() as sess:
    saver.restore(sess, "C:\\Users\\1003342\\Desktop\\study\\tensorflow\\model_data\\model.ckpt")
    print(sess.run(result))

运行代码,结果如下:
INFO:tensorflow:Restoring parameters from C:\Users\1003342\Desktop\study\tensorflow\model_data\model.ckpt
[ 3.]
这两段代码唯一不同的是,在加载模型的代码中没有运行变量的初始化过程,而是将变量的值通过已经保存的模型加载进来。如果不希望重复定义图上的运算,也可以直接加载已经持久化的图,以下代码给出了一个样例:

saver = tf.train.import_meta_graph("C:\\Users\\1003342\\Desktop\\study\\tensorflow\\model_data\\model.ckpt.meta")
with tf.Session() as sess:
    saver.restore(sess, "C:\\Users\\1003342\\Desktop\\study\\tensorflow\\model_data\\model.ckpt")
    # 通过张量的名称来获取张量
    print(sess.run(tf.get_default_graph().get_tensor_by_name("add:0")))

运行代码,得到如下结果:
INFO:tensorflow:Restoring parameters from C:\Users\1003342\Desktop\study\tensorflow\model_data\model.ckpt
[ 3.]
在上面给出的程序中,默认保存和加载了TensorFlow计算图上定义的全部变量。但有时可能只需要保存或者加载部分变量。比如,可能有一个之前训练好的五层神经网络模型,但现在想尝试一个六层的神经网络,那么可以将前面五层神经网络中的参数直接加载到新的模型,而仅仅将最后一层神经网络重新训练。为了保存或者加载部分变量,在声明tf.train.Saver类时可以提供一个列表来指定需要保存或者加载的变量。比如在加载模型的代码中使用saver=tf.train.Saver([v1])命令来构建tf.train.Saver类,那么只有变量v1会被加载进来。如果运行修改后只加载了v1的代码会得到变量未初始化的错误,因为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()来加载模型会报找不到的错误。下面显示了报错信息
# 使用一个字典来重命名变量就可以加载原来的模型了。这个字典指定了原来名称为v1的变量现在加载到变量v1中(名称为other-v1),
# 名称为v2的变量加载到变量v2中(other-v2)
saver = tf.train.Saver({"v1":v1, "v2":v2})

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

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_averages_op = ema.apply(tf.all_variables())
# 在申明滑动平均模型之后,TensorFlow会自动生成一个影子变量
# v/ExponentialMoving Average。于是下面的语句会输出
for variables in tf.all_variables():
    print(variables.name)
saver = tf.train.Saver()
with tf.Session() as sess:
    init_op = tf.initialize_all_variables()
    sess.run(init_op)
    sess.run(tf.assign(v, 10))
    sess.run(maintain_averages_op)
    # 保存时,TensorFlow会将两个变量保存下来。
    saver.save(sess, "C:\\Users\\1003342\\Desktop\\study\\tensorflow\\data2\model.ckpt")
    print(sess.run(v, ema.average(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, "C:\\Users\\1003342\\Desktop\\study\\tensorflow\\data2\model.ckpt")
    print(sess.run(v))

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

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_averages_op)
    # 保存的时候会将v:0  v/ExponentialMovingAverage:0这两个变量都存下来。
    saver.save(sess, "Saved_model/model2.ckpt")
    print(sess.run([v, ema.average(v)]))

运行代码,输出如下:
[10.0, 0.099999905]

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

v = tf.Variable(0, dtype=tf.float32, name="v")
saver = tf.train.Saver({"v/ExponentialMovingAverage":v})
with tf.Session() as sess:
    saver.restore(sess, "Saved_model/model2.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函数可以直接生成上面代码中提供的字典
# 其中后面的Variable类就代表了变量v
print(ema.variables_to_restore())
saver = tf.train.Saver(ema.variables_to_restore())
with tf.Session() as sess:
    saver.restore(sess, "C:\\Users\\1003342\\Desktop\\study\\tensorflow\\data2\model.ckpt")
    print(sess.run(v)) #输出0.099999905,这个值就是原来模型中变量v的滑动平均值

运行代码,输出如下图所示:


variables_to_restore函数的使用样例输出结果

使用tf.train.Saver会保存运行TensorFlow程序所需要的全部信息,然而有时并不需要某些信息。比如在测试或者离线预测时,只需要知道如何从神经网络的输入层经过前向传播计算得到输出层即可,会遇到类似的情况。而且,将变量取值和计算图结构分成不同的文件存储有时候可以将计算图中的变量及其取值通过常量的方式保存,这样整个TensorFlow计算图可以统一存放在一个文件中。下面的程序提供了一个样例:
1.pb文件的保存:

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)
    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("Saved_model/combined_model.pb", "wb") as f:
           f.write(output_graph_def.SerializeToString())

运行代码,输出:
INFO:tensorflow:Froze 2 variables.
Converted 2 variables to const ops.

  1. 加载pb文件。
from tensorflow.python.platform import gfile
with tf.Session() as sess:
    model_filename = "Saved_model/combined_model.pb"
   # 读取保存的模型文件,并将文件解析成对应的GrahphDef 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))

运行代码,输出
[array([ 3.], dtype=float32)]

4.2持久化原理及数据格式

上一小节介绍了当调用saver.save函数时,TensorFlow程序会自动生成3个文件。TensorFlow模型的持久化就是通过这3个文件完成的。这一小节将详细介绍这3个文件中保存的内容以及数据格式。在具体介绍每一个文件之前,先简单回顾一下第三章中介绍过的TensorFlow的一些基本概念。TensorFlow是一个通过图的形式来表述计算的编程系统,TensorFlow程序中的所有计算都会被表达为计算图上的节点。TensorFlow通过元图(MetaGraph)来记录计算图中节点的信息以及运行计算图中节点所需要的元数据。TensorFlow中元图是由MetaGraphDef Protocol Buffer定义的。MetaGraphDef中的内容就构成了TensorFlow持久化的第一个文件。以下代码给出了MetaGraphDef类型的定义:

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="v2")
result1 = v1 + v2
saver = tf.train.Saver()
saver.export_meta_graph("/path/to/model.ckpt.meda.json", as_text=True)

运行代码,输出部分如下图:


TensorFlow持久化文件的内容

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

https://www.jianshu.com/p/9f35d452e81c

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
Python
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论