TensorFlow 实战Google深度学习框架(第2版)第五章读书笔记

第五章:MNIST数字识别问题

* 5.1MNIST数据处理
* 5.2神经网络模型训练及不同模型结果对比
       * 5.2.1TensorFlow训练神经网络
       * 5.2.2使用验证数据集判断模型效果
       * 5.2.3不同模型效果比较
* 5.3变量管理
* 5.4TensorFlow模型持久化
       * 5.4.1持久化代码实现
       * 5.4.2持久化原理及数据格式
* 5.5TensorFlow最佳实践样例程序

本章通过一个具体的问题:MNIST数字识别,来验证第4章中介绍的神经网络模型优化办法;
本章使用的数据集是MNIST手写体数字识别数据集,通常被用作深度学习的入门样例;
本章也会给出使用 TensorFlow训练神经网络的最佳实践。

-5.1- MNIST数据处理

本节中,将:
* 介绍MNIST 手写体数字识别数据集
* 给出 TensorFlow 样例程序处理 MNIST数据

MNIST数据集NIST数据集的一个子集,在很多资料中,这个数据集都会被用作深度学习的入门样例。它包含了 60000 张图片作为训练数据, 10000 张图片作为测试数据。该数据集中每一张图都代表了 0~9 中的一个数字。图片的大小都为 28x28, 且数字都会出现在图片的正中间。

一张数字图片及和它对应的像素矩阵

上图左侧显示了一张数字1的图片,而右侧显示了这个图片所对应的像素矩阵。
YannLeCun 教授的网站中,对 MNIST 数据集做出了详细的介绍。

MNIST 数据下载地址和内容:

网址 内容
http://yann.lecun.corn/exdb/mnist/train-images-idx3-ubyte.gz 训练数据图片
http://yann.lecun.corn/exdb/mnist/train-labels-idx1-ubyte.gz 训练数据答案
http://yann.lecun.corn/exdb/mnist/t10k-images-idx3-ubyte.gz 测试数据图片
http://yann.lecun.corn/exdb/mnist/t10k-labels-idx1-ubyte.gz 测试数据答案

MNIST 数据集提供了 4 个下载文件。虽然这个数据集只提供了训练和测试数据 ,但是为了验证模型训练的效果,一般会从 训练数据中划分出一部分数据作为验证( validation)数据。在 5.2.2 节中将更加 详细 地介 绍 验证数据的作用。

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

from tensorflow.examples.tutorials.mnist import input_data

# 载入 MNIST 数据集
# 如果指定地址 /path/to/MNIST_data 下没有已经下载好的数据 
# 那么 TensorFlow 会自动从上表 5-1 给出的网址下载数据。
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)

#打印 Validating 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 数据集划分为 trainvalidationtest 三个数据集。其中:
* train 集合内有 55000 张图片
* validation 集合内有 5000 张图片
* test 集合内有 10000 张图片
train、validation这两个集合组成了 MNIST 本身提供的训练数据集 。
test集合图片都来自于 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)  <-- 0 ~9 种label, 取one hot的方式表示

-5.2- 神经网络模型训练及不同模型结果对比

本节中,将:
* 对比第4章中提到的神经网络结构设计和参数优化的不同方法
* 从实际的问题中验证不同优化方法带来的性能提升

-5.2.1- TensorFlow 训练神经网络

本节将给出一个完整的 TensorFlow程序来解决 MNIST 手写体数字识别问题
这个程序整合实现了第 4 章中介绍的神经网络结构设计和训练的所有优化方法
训练好的神经网络模型在 MNIST 测试数据集上可以达到98.4%左右的正确率。
在给出 具体的代码之前 ,先回顾一下第 4 章中提到的主要概念:

在神经网络的结构上,深度学习
* 一方面需要使用激活函数实现神经网络模型的去线性化
* 另一方面需要使用一个或多个隐藏层使得神经网络的结构更深,以解决复杂问题 。
在训练神经网络时,第 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_size越小时,训练过程越接近随机梯度下降
# batch_size越大时,训练过程越接近梯度下降
LEARNING_RATE_BASE = 0.8  # 基础的学习率
LEARNING_RATE_DECAY = 0.99  # 学习率的衰减率
REGULARIZATION_RATE = 0.0001  # 描述模型复杂度的正则化项在损失函数中的系数
TRAINING_STEPS = 30000  # 训练轮数
MOVING_AVERAGE_DECAY = 0.99  # 滑动平均衰减率

# 定义一个辅助函数
# 给定神经网络的输入和所有参数,计算神经网络的前向传播结果
# 在这里定义了一个使用ReLU激活函数的三层全链接神经网络
# 通过加入隐藏层实现了多层神经网络的结构
# 通过ReLU激活函数实现了去线形化
# 在这个函数中也支持传入用于计算参数平均值的类
# 这样方便在测试时,使用滑动平均模型
def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
    # 没有提供滑动平均class时,直接使用参数当前的取值
    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:
        # 首先使用average_class.average来计算得出变量的滑动平均值
        # 然后再计算相应的神经网络的前向传播结果
        layer1 = tf.nn.relu(
            tf.matmul(input_tensor, avg_class.average(weights1)) 
            + avg_class.average(biases1)
        )
        return tf.matmul(
            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.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=Fasle)
    # 在使用TensorFlow训练神经网络时,一般会将代表训练轮数的变量指定为不可训练
    global_step = tf.Variable(0, trainable=False)
    # 给定滑动平均衰减率和训练轮数的变量,初始化滑动平均类
    # 在第4章中介绍过给定训练轮数的变量
    # 可以加快训练过程前期,模型变量的更新速度
    variable_averages = tf.train.ExponentialMovingAverage(
        MOVING_AVERAGE_DECAY, global_step
    )

    # 在所有代表神经网络参数的变量上使用滑动平均
    # 其他辅助变量(比如global_step)就不需要了
    # tf.trainable_variables返回的就是图上的集合GraphKeys.TRAINABLE VARIABLES中的元素
    # 这个集合的元素就是所有没有指定trainable=False的模型变量参数
    variables_averages_op = variable_average.apply(
        tf.trainable_variables()
    )

    # 计算使用了滑动平均之后的前向传播结果
    # 第4章中介绍过滑动平均不会改变变量本身的取值,而是会维护一个影子变量来记录其滑动平均值
    # 所以,当使用这个滑动平均值时,需要明确调用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)
    # 计算模型的正则化损失
    # 一般只计算神经网络layer上权重的正则化损失,而不包括偏置项
    regularization = regularizer(weights1) + regularizer(weights2)
    # 总损失等于交叉熵损失和正则化损失的和
    loss = cross_entropy_mean + regularization
    # 设置指数衰减的学习率
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE,  # 基础学习率,随着迭代的进行,更新变量时使用的学习率在这个基础上递减
        global_step,
        mnist.train.num_example / BATCH_SIZE,
        LEARNING_RATE_DECAY
    )

# 使用tf.train.GradientDescentOptimizer优化算法来优化损失函数
# 注意这里损失函数包含了交叉熵损失函数和L2正则化损失
train_step = tf.train.GradientDescentOptimizer(
        learning_rate
    ).minize(loss, global_step=global_step)

# 在训练神经网络模型时,每过一个batch的数据,
# 即需要通过反向传播来更新神经网络中的参数,又要更新每一个参数的滑动平均值
# 为了一次完成多个操作,TensorFlow提供了 tf.control_dependicies() 和 tf.group() 两种机制
# 下面两行程序和train_op=tf.group(train_step, variables_averages_op)是等价的
with tf.control_dependencies([train_step, 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.equanl(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.validation.images,
        y_: mnist.validation.labels
    }
    # 迭代地训练神经网络
    for i in range(TRAINING_STEPS):
        # 每1000轮输出一次在验证数据集上的测试结果
        if i%1000 == 0:
            # 计算滑动平均模型在验证数据上的结果
            # 因为MNIST数据集比较小,所以一次可以处理所有的验证数据
            # 为了计算方便,本样例程序没有将验证数据划分为更小的batch_size
            # 当神经网络模型结构比较复杂,或者验证数据比较多时
            # 太大的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("/tmp/data", one_hot=True)
    train(mnist)

# 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/t10k-images-idx3-ubyte.gz
Extracting /tmp/data/t10k-labels-idx1-ubyte .gz
After 0 training step(s), validation accuracy on average model is 0.105 
After 1000 training step(s), validation accuracy using average model is 0.9774 
After 2000 training step(s), validation accuracy using average model is 0.9816 
After 3000 training step(s), validation accuracy using average model is 0.9834 
After 4000 training step(s), validation accuracy using average model is 0.9832

...

After 27000 training step(s), validation accuracy using average model is 0.984
After 28000 training step(s), validation accuracy using average model is 0.985 
After 29000 training step(s), validation accuracy using average model is 0.985 
After 29999 training step(s), validation accuracy using average model is 0.985 

After 30000 training step(s), test accuracy on average model is 0. 984

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

下面的 5.2.2 节将详细介绍验证数据集的作用

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

本节将介绍验证数据集在训练神经网络过程中的作用。
这一节将通过 -5.2.1- 节中得到的实验数据来证明,神经网络在验证数据集上的表现可以近似地作为:
* 评价不同神经网络模型的标准
* 决定迭代轮数的依据

在 5.2.1 节给出了使用神经网络解决 MNIST 问题的完整程序 。在这个程序的开始设置 了初始学习率学习率衰减率隐藏层节点数量迭代轮数等 7 种不同的参数 。

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

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

在 5.2.1 节给出的代码中加入以下代码,就可以得到每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 average "
    "model is %g, test accuracy using average model is %g " % (i, validate_acc, test_acc)
)

上图给出了通过上面代码得到的每 1000 轮滑动平均模型在不同数据集上的正确率曲线。 图中:
* 灰色曲线表示随着法代轮数的增加,模型在验证数据上的正确率 ;
* 黑色曲线表示 了在测试数据上的正确率 。

从图中可以看出,虽然这两条曲线不会完全重合,但是这两条曲线的趋势基本一样,而且他们的相关系数( correlation coefficient)大于 0.9999。 这意味着在 MNIST 问题上,完全可以通过模型在验证数据上的表现来判断一个模型的优劣 。

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

-5.2.3- 不同模型效果比较

本节将通过 MNIST 数据集验证第 4章中介绍的每一个优化方法。
通过在 MNIST数据集上的实验可以看到,这些优化方法都可以或多或少地提高神经网络的分类正确率。

本节将通过 MNIST数据集来比较第 4章中提到的不同优化方法对神经网络模型正确率 的影响。在对比影响时,使用神经网络模型在 MNIST测试数据集上的正确率作为评价不同优化方法 的标准 。在本节中 一个模型在 MNIST 测试数据集上的正确率将简称为“正确率” 。

在第 4 章中提到了设计神经网络时的 5 种优化方法:
* 在神经网络结构的设计上,需要使用激活函数多层隐藏层
* 在神经网络迭代优化时,可以使用指数衰减的学习率加入正则化的损失函数以及滑动平均模型

上图给出了在相同神经网络参数下,使用不同优化方法,经过30000轮训练法代后,得到的最终模型的正确率。 图中给出的结果中包含了使用所有优化方法训练得到的模型和不用其中某一项优化方法训练得到的模型。通过这种方式,可以有效验证每一项优化方法的效果。

针对模型结构的优化隐藏层 & 激活函数

从图中可以很明显地看出,调整神经网络的结构对最终的正确率有非常大的影响 。 没有隐藏层或者没有激活函数时,模型的正确率只有大约 92.6%,这个数字要远远小于使用了隐藏层和激活函数时可以达到的大约 98.4%的正确率。这说明神经网络的结构对最终模型的效果有本质性的影响。第6章将会介绍一种更加特殊的神经网络结构一一卷积神经网络。卷积神经网络可以更加有效地处理图像信息。通过卷积神经网络,可以进一步将正确率提高到大约99.5%。

针对迭代过程的优化滑动平均 & 指数衰减的学习率 & 正则化损失函数

从图上的数字中可发现使用滑动平均模型指数衰减的学习率和使用正则化带来的正确率的提升并不是特别明显。其中使用了所有优化算法的模型和不使用滑动平均的模型以及不使用指数衰减的学习率的模型都可以达到大约98.4%的正确率。这是因为滑动平均模型指数衰减的学习率在一定程度上都是限制神经网络中参数迭代更新的速度,然而在MNIST数据上,因为模型收敛的速度很快,所以这两种优化对最终模型的影响不大。从图5-2中可以看到,当模型迭代到4000轮时正确率就己经接近最终的正确率了。而在迭代的早期,是否使用滑动平均模型或者指数衰减的学习率对训练结果的影响相对较小。

图 5-4 显示了不同迭代轮数时, 使用了所有优化方法的模型的正确率平均绝对梯度的变化趋势。从图 5-4 中可以看到,前4000轮迭代对模型的改变是最大的。在4000轮之后,因为梯度本身比较小,所以参数的改变也就比较缓慢了。于是滑动平均模型或者指数衰减的学习率的作用也就没有那么突出了。

图 5-5 显示了不同迭代轮数时,正确率衰减之后的学习率的变化趋势。从图 5-5 中可以看到,学习率曲线呈现出阶梯状衰减,在前 4000 轮时,衰减之后的学习率和最初的学习率差距并不大。

那么,这是否能说明这些优化方法作用不大呢?答案是否定的。
当问题更加复杂时,迭代不会这么快接近收敛,这时滑动平均模型和指数衰减的学习率可以发挥更大的作用。
比如在CIFAR-10 图像分类数据集上:
* 使用滑动平均模型可以将错误率降低 11%,
* 而使用指数衰减的学习率可以将错误率降低 7%。

相比「滑动平均模型」和「指数衰减学习率」,使用加入正则化的损失函数给模型效果带来的提升要相对显著。使用了正则化损失函数的神经网络模型可以降低大约6%的错误率(从1.69%降低到1.59%)。图 5-6 和图 5-7 显示了正则化损失函数给模型优化过程带来的影响。两图对比了两个使用了不同损失函数的神经网络模型。一个模型只最小化交叉熵损失,另一个模型优化的是交叉熵和 L2 正则化损失的和

以下代码给出了只优化交叉熵损失的模型优化函数的声明语句 。

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

以下代码给出了优化添加了L2正则项的交叉熵损失的模型优化函数的声明语句。

loss = cross_entropy_mean + regularization
train_step = tf.train.GradientDescentOptimizer(
    learning_rate
).minimize(loss , global_step=global_ step )

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

图 5-7 横轴:迭代的次数
图 5-7 纵轴:正则化损失、总损失(=正则化损失+交叉熵损失)

图 5-7 显示了不同模型的损失函数的变化趋势。
图 5-7 的左侧显示了只优化交叉熵损失的模型损失函数的变化规律。
可以看到随着迭代的进行,正则化损失是在不断加大的。因为 MNIST 问题相对比较简单, 迭代后期的梯度很小(参考图 5-4),所以正则化损失的增长也不快。如果问题更加复杂,选代后期的梯度更大,就会发现总损失(交叉熵损失+正则化损失)会呈现出一个 U 字型。
在图 5-7 的右侧,显示了优化总损失的模型损失函数的变化规律。
从图 5-7 中可以看出,这个模型的正则化损失部分也可以随着迭代的进行越来越小,从而使得整体的损失呈现一个逐步递减的趋势。

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

-5.3- 变量管理

本节中,将:
* 指出 5.2 节中TensorFlow程序实现神经网络的变量重用的问题
* 介绍 TensorFlow 的变量的命名空间来解决这一不足

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

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

从定义中可以看到,这个函数的传入参数中包括了神经网络中的所有参数。
然而,当神经网络的结构更加复杂、参数更多时,就需要一个更好的方式来传递和管理神经网络中的参数了。

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

下面将分别介绍如何使用这两个函数。第4章介绍了通过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函数和3.4.3节中介绍的随机数以及常量生成函数大部分是一一对应的。
比如,在以上样例程序中使用到的常数初始化函数tf.constant_initializer()和常数生成函数tf.constant功能上就是一致的。

TensorFlow提供了7种不同的初始化函数,表 5-2 总结了它们的功能和主要参数。
表 5-2 TensorFlow 中的变量初始化函数

初始化函数 功能 主要参数
tf.constant_initializer 将变量初始化为给定常量 常量的取值
tf.random_normal_initializer 将变量初始化为满足正态分布的随机值 正态分布的均值和标准差
tf.truncated_normal_initializer 将变量初始化为满足正态分布的随机值,但如果随机出来的值偏离平均值超过两个标准差,那么这个数将会被重新随机 正态分布的均值和标准差
tf.random_uniform_initializer 将变量初始化为满足平均分布的随机值 最大、最小值
tf.uniform_unit_scaling_initializer 将变量初始化为满足平均分布但不影响输出数量级的随机值 factor(产生随机值时乘以的系数)
tf.zeros_initializer 将变量设置为全0 变量维度
tf.ones_initializer 将变量设置为全1 变量维度

tf.get_variable()函数与tf.Variable()函数最大的区别在于指定变量名称的参数,对于tf.Variable()函数,变量名称是一个可选的参数,通过 name="v"的形式给出。但是对于tf.get_varibale()函数,变量名称是一个必填的参数,tf.get_varibale()函数会根据这个名字去创建or获取变量。

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

下面给出了一段代码说明如何通过 tf.variable_scope 函数来控制 tf.get_variable 函数获取己经创建过的变量。

# 在名字为foo的命名空间内创建名字为v的变量
with tf.variable_scope("foo"):
    v = tf.get_variable("v", [1], initializer=tf.constant_initializer(1.0))

# 因为在命名空间foo中已经存在名字为v的变量,所以以下代码会报错
# 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 exists, disallowed. Did you 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() 函数可以:
* 控制tf.get_variable()执行的功能;
* 创建一个TensorFlow 中的命名空间,管理变量的名称。

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_varaible("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/ 中创建的变量
    print(v5==v3)  # 输出True
    v6 = tf.get_variable("foo/v1", [1])
    print(v6==v4)  # 输出True

通过 tf.variable_scope 和 tf.get_variable 函数,以下代码对 5.2.1 节中定义的计算前向传播结果的函数做了一些改进:

def inference(input_tensor, reuse=False):
    # 定义第一层神经网络的变量和前向传播过程。
    with tf.variable_scope('layer_1', 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("layer_2", 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 

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

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

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

-5.4- TensorFlow模型持久化

本节中,将:
* 指出 5.2 节 中 TensorFlow 程序实现神经网络模型未持久化保存的问题。在 5.2.1 节中给出的样例代码在训练完成之后就直接退出了,并没有将训练得到的模型保存下来方便下次直接使用 。 为了让训练结果可以复用,需要将训练得到的神经网络模型持久化
* 介绍 TensorFlow 的模型持久化实践,来使得之后可以直接使用训练好的模型。
5.4.1 节将介绍通过 TensorFlow程序来持久化一个训练好的模型,并从持久化之后的模型文件中还原被保存的模型 。
然后 5.4.2 节将介绍 TensorFlow 持久化的工作原理和持 久化之后文件中的数据格式 。

-5.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)
    # 将模型保存到/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计算图的结构。第3章中介绍过TensorFlow计算图的原理,这里可以简单理解为神经网络的网络结构。
* 第二个文件为model.ckpt,这个文件中保存了TensorFlow程序中每一个变量的取值。
* 最后一个文件为checkpoint文件,这个文件中保存了一个目录下所有的模型文件列表 。

对这些文件的具体内容,5.4.2节中将详细描述。
以下代码给出了加载这个已经保存的TensorFlow模型的方法。

import tensorflow as tf 

# 使用和保存模型代码中一样的方式来声明变量
v1 = tf.Variabel(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.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 类时可以提供一个列表来指定需要保存或者加载的变量。比如在加载模型的代码中使用 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类也支持在保存or加载变量时,给变量重命名。下面给出一个简单的样例程序,说明变量重命名是如何被使用的:

# 注意,这里声明的变量名称和已保存的模型中的变量名称不同
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/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)将模型保存时的变量名和需要加载的变量联系起来。

这样做主要目的之一是方便使用变量的滑动平均值
在 4.4.3 节中介绍了使用变量的滑动平均值可以让神经网络模型更加健壮( robust)
在 TensorFlow 中,每一个变量的滑动平均值是通过影子变量维护的,所以要获取变量的滑动平均值实际上就是获取这个影子变量的取值。
如果在加载模型时直接将影子变量映射到变量自身,那么在使用训练好的模型时就不需要再调用函数来获取变量的滑动平均值了。
这样大大方便了滑动平均模型的使用 。

以下代码给出了 一个保存滑动平均模型的样例。

import tensorflow as tf 

v = tf.Variable(0, dtype=tf.float32, name="v")

# 在没有声明滑动平均模型时,只有一个变量v
# 所以,以下语句会输出"v:0" 
for variables in tf.global_variables():
    print(variables.name)

ema = tf.train.ExeponentialMovingAverage(0.99) 
maintain_average_op = ema.apply(tf.global_variables()) 
# 在声明滑动平均模型之后, TensorFlow会自动生成一个影子变量 v/ExponentialMovingAverage
# 于是,以下语句会输出 "v:0" 和 "v/ExponentialMovingAverage:0"
for variable 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.099999905]

以下代码给出了如何通过变量重命名直接读取变量的滑动平均值。
从下面程序的输出可以看出,读取的变量 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/ExeponentialMovingAverage": v}
# 以下代码会输出:
# {"v/ExponentialMovingAverage", <tensorflow.Variable 'v:0' shape=() dtype=float32_ref>}
print(ema.variables_to_restore())
saver = tf.train.Saver(ema.variables_to_restore())

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

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

import tensorflow as tf 
from tensorflow.python.framework import graph_util

v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result = v1 + v2 

init_op = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init_op)
    # 导出当前计算图的 GraphDef 部分,只需要这一部分就可以完成输入层到输出层的计算
    graph_def = tf.get_default_graph().as_graph_def()
    
    # 将图中的变量及其取值转化为常量,同时将图中不必要的节点去掉
    # 在 5. 4 .2 节中将会看到,一些系统运算也会被转化为图中的运算节点(比如变量初始化操作)
    # 如果只关心程序中定义的某些运算时,和这些计算无关的节点就没有必要到处并保存了
    # 在下面一行代码中,最后一行参数['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())

通过以下程序可以直接计算定义的加法运算的结果,当只需要得到计算图中某个节点的取值时,这提供了一个更加方便的方法。第 6 章将使用这种方法来使用训练好的模型完成迁移学习。

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

with tf.Session() as sess:
    model_filename = "/path/to/model/combined_model.pb"

    # 读取保存的模型文件,并将文件解析成对应的 GraphDef Protocol Buffer
    with gfile.FastGFile(model_file_name, 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())

    # 将praph_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))
-5.4.2- 持久化原理及数据格式

5.4.1 节介绍了当调用saver.save()函数时,TensorFlow程序会自动生成 4 个文件。TensorFlow模型的持久化就是通过这4个文件完成的 。这一节将详细介绍这 4 个文件中保存的内容以及数据格式。

在具体介绍每一个文件之前,先简单回顾一下第 3 章中介绍过的 TensorFlow 的一些基本概念。
TensorFlow是一个通过图的形式来表述计算的编程系统,TensorFlow程序中的所有计算都会被表达为计算图上的节点。

MetaGraphDef Protocol Buffer

TensorFlow通过元图(MetaGraph)来记录计算图中节点的信息以及运行计算图中节点所需要的元数据
TensorFlow中元图是由MetaGraphDef Protocol Buffer定义的。MetaGraphDef中的内容就构成了 TensorFlow持久化时的第一个文件。

以下代码给出了 MetaGraphDef类型的定义:

message MetaGraphDef {
    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类信息。

下面的篇幅将结合 5.4.1 节中变量相加样例的持久化结果,逐一介绍 MetaGraphDef类型的每一个属性中存储的信息。
保存MetaGraphDef信息的文件默认以 .meta 为后缀名,在 5.4.1 节的样例中,文件model.ckpt.meta中存储的就是元图的数据。

直接运行 5.4.1 节样例得到的是一个二进制文件,无法直接查看。为了方便调试,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="v2")
result = v1 + v2 

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

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

------------------ MetaGraphDefMetaInfoDef

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

下面是 MetaInfoDef Protocol Buffer 的定义:

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

TensorFlow 计算图的元数据包括了计算图的版本号(meta_graph_version 属性)以及用户指定的一些标签(tags属性)。如果没有在 saver 中特殊指定,那么这些属性都默认为空。

stripped_op_list属性
在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 attire = 4;

    OpDeprecationn deprecation = 8;
    string summary = 5;
    string description = 6;
    bool is_commutative = 18;
    bool is_aggregated = 16;
    bool is_stateful = 17;
    bool allows_uninitialized_input = 19;
};

OpDef类型中前 4个属性定义了一个运算最核心的信息。

* OpDef中的第一个属性name定义了运算的名称,这也是一个运算唯一的标识符。在 TensorFlow计算图元图的其他属性中,比如下面将要介绍的 GraphDef 属性,将通过运算名称来引用不同的运算 。
* OpDef的第二和第三个属性为input_argoutput_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: "z"
        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)。

MetalnfoDef中的tensorflow_versiontensorflow_git_version属性记录了生成当前计算图的TensorFlow版本。

------------------ MetaGraphDefgraphDef

graph_def属性主要记录了 TensorFlow计算图上的节点信息。
TensorFlow计算图的每一 个节点对应了 TensorFlow程序中的一个运算。
因为在meta_info_def属性中己经包含了所有运算的具体信息,所以grph_def属性只关注运算的连接结构 。
graph_def 属性是通过GraphDefProtocol Buffer定义的,GraphDef主要包含了一个NodeDef类型的列表。

以下代码给出了GraphDefNodeDef类型中包含的信息:

message GraphDef {
    repeated NodeDef node = 1;
    FunctionDefLibrary library = 2;
    int32 version = 3 [deprecated = true];  // 实验性的版本号
    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 的版本号 。
* node属性 存放了 GraphDef 的主要信息,记录TensorFlow计算图上所有的节点信息 。

* node属性name属性
和其他属性类似,NodeDef类型中有一个名称属性name,它是一个节点的唯一标识符。在TensorFlow程序中可以通过节点的名称来获取相应的节点。
* node属性op属性
NodeDef类型中的op属性给出了该节点使用的TensorFlow运算方法的名称,通过这个名称可以在TensorFlow计算图元图的meta_info_def属性中找到该运算的具体信息。
* node属性input属性
NodeDef类型中的input属性是一个字符串列表,它定义了运算的输入。 input属性中每个字符串的取值格式为node:src_output,其中node部分给出了一个节点的名称,src_output 部分表明了这个输入是指定节点的第几个输出。当src_output为0时,可以省略:src_output这个部分。比如node:0表示名称为node的节点的第一个输出,它也可以被记为node。
* node属性device属性
NodeDef类型中的device属性指定了处理这个运算的设备。运行 TensorFlow 运算的设备可以是本地机器的CPU或者GPU,也可以是一台远程的机器CPU或者GPU。第10章将具体介绍如何指定运行TensorFlow运算的设备。当device属性为空时,TensorFlow在运行时会自动选取一个最合适的设备来运行这个运算。
* node属性attr属性
最后,NodeDef类型中的attr属性指定了和当前运算相关的配置信息。

下面列举了 model.ckpt.meta.json文件中的一些计算节点来更加具体地介绍 graph_def属性:

graph def { 
    node {
        name: "vl"
        op: "VariableV2" 
        attr {
            key: "_output_shapes" 
            value {
                list { shape { dim { size : 1}}}
            }
        }
        attr {
            key: "dtype"
            value {
                type: DT_FLOAT 
            }
        }
        ...
    }
    node {
        name: "add"
        op: "Add"
        input: "v1/read"
        input: "v2/read"
        ... 
    }
    node {
        name: "save/control_dependency"
        op: "Identity"
        ...
    }

    versions {
        producer: 24
    }
}

上面给出了 model.ckpt.meta.json文件中graph_def属性里比较有代表性的几个节点。
* 第一个节点给出的是变量定义的运算节点
在 TensorFlow 中变量定义也是一个运算

* 计算图中节点的名称运算方法

这个运算的名称为v1 (name: "v1"),
运算方法的名称为Variable(op: "VariableV2")。
定义变量的运算可以有很多个,于是在NodeDef类型的node属性中可以有多个变量定义的节点。
但定义变量的运算方法只用到了一个,于是在MetalnfoDef类型的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 模型持久化过程中自动生成的一个运算。

* 属性versions

在样例文件的最后,属性 versions 给出了生成 model.ckpt.meta.json 文件时使用的 TensorFlow 版本号。

------------------ MetaGraphDefsaverDef

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

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

下面给出了model.ckpt.meta.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属性

filename_tensor_name属性给出了保存文件名的张量名称,这个张量就是节点save/Const的第一个输出。

* save_tensor_name属性

save_tensor_name属性给出了持久化TensorFlow模型的运算所对应的节点名称。从以上文件中可以看出,这个节点就是在graph_def属性中给出的save/control_dependency节点

* restore_op_name属性

和持久化TensorFlow模型运算对应的是加载TensorFlow模型的运算,这个运算的名称由restore_op_name属性指定。

* max_to_keep属性keep_checkpoint_every_n_hours属性

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的基础上多保存一个模型。

------------------ MetaGraphDefcollectionDef

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

message CollectionnDef {
    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 {
        NodeList node_list = 1;
        BytesList bytes_list = 2;
        Int64List int64_list = 3;
        FloatList float_list = 4;
        AnyList any_list = 5;
    }
}

通过以上定义可以看出,TensorFlow计算图上的集合主要可以维护4类不同的集合。
* NodeList 用于维护计算图上节点的集合。
* BytesList 可以维护字符串或者系列化之后的 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\004v2:0\022\tv2/Assign\032\tv2/read:0"
        }
    }
}

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

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

通过对MetaGraphDef类型中主要属性的讲解,本节己经介绍了 TensorFlow 模型持久化得到的第一个文件中的内容。除了持久化 TensorFlow计算图的结构,持久化 TensorFlow 中变量的取值也是非常重要的一个部分。5.4.1 节中使用 tf.Saver得到的 model.ckpt.index和 model.ckpt.data-*****-of-*****文件就保存了所有变量的取值。其中 model.ckpt.data文件是通过 SSTable 格式存储的,可以大致理解为就是一个(key, value)列表。

TensroFlow提供了tf.train.NewCheckpointReader类来查看保存的变量信息。以下代码展示了如何使用tf.train.NewCheckpointReader类:

import tensorflow as tf 

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

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

# 获取名称为v1的变量的取值
print("Value for variable v1 is ", reader.get_tensor("v1"))
"""
这个程序将输出
v1 [1]  # 变量v1的维度为 [1]
v2 [1]  # 变量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_path = 2;
}

* model_checkpoint_path属性保存了最新的TensorFlow模型文件的文件名。
* all_model_checkpoint_paths属性列出了当前还没有被删除的所有TensorFlow模型文件的文件名。

下面给出了通过 5.4.1 节中样例程序生成的 checkpoint 文件:

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

-5.5- TensorFlow最佳实践样例程序

本节中,将:
* 整合 5.3 和 5.4 节中介绍的 TensorFlow最佳实践
* 通过一个完整的 TensorFlow程序解决MNIST问题

在 5.2.1 节中已经给出了一个完整的TensorFlow程序来解决MNIST问题。然而这个程序的可扩展性并不好。
* 如在 5.3 节中提到的,计算前向传播的函数需要将所有变量都传入,当神经网络的结构变得更加复杂、参数更多时,程序可读性会变得非常差。而且这种方式会导致程序中有大量的冗余代码,降低编程的效率。
* 5.2.1节给出的程序的另外一个问题是没有持久化训练好的模型。当程序退出时,训练好的模型也就被无法再使用了,这导致得到的模型无法被重用。更严重的问题是,一般神经网络模型训练的时间都比较长,少则几个小时,多则几天甚至几周。如果在训练过程中程序死机了,那么没有保存训练的中间结果会浪费大量的时间和资源。所以,在训练的过程中需要每隔一段时间保存一次模型训练的中间结果。

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

本节将提供重构之后的程序来解决 MNIST问题。重构之后的代码将会被拆成 3个程序:
* 第一个是mnist_inference.py,它定义了前向传播的过程以及神经网络中的参数。
* 第二个是mnist_train.py,它定义了神经网络的训练过程。
* 第三个是mnist_eval.py,它定义了测试过程。

以下代码给出mnist_inference.py中的内容:

#! /usr/bin/env python 
# -*- 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)
    )

    # 当给出了正则化生成函数时,将当前变量的正则化损失加入名字为loss的集合。
    # 在这里,使用add_to_collection 函数将一个张量加入一个集合,而这个集合的名称为losses。
    # 这是自定义的集合,不在TensorFlow自动管理的集合列表中。
    if regulrizer != 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_tensor, weights) + biases)
    
    # 类似地,声明第二层神经网络的变量,并完成前向传播过程
    with tf.variable_scope('layer2'):
        weights = get_weight_variable(
            [LAYER1_NODE, OUTPUT_NODE],
            regularizer
        )
        biases = tf.get_variable(
            "biases",
            [OUTPUT_NODE],
            initializer=tf.constant_initializer(0.0)
        )
        layer2 = tf.matmul(layer1, weights) + biases

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

这段代码定义了神经网络的前向传播算法。无论是训练时还是测试时,都可以直接调用inference()这个函数,而不用关心具体的神经网络结构。

使用定义好的前向传播过程,以下代码给出了神经网络的训练程序mnist_train.py

#! /usr/bin/env python 
# -*- coding:utf-8 -*- 

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

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

# 配置神经网络的参数
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8 
LEARNING_RATE_DECAY = 0.99 
REGULARIZATION_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(
        REGULARIZATION_RATE
    )
    # 直接使用mnist_inference.py中定义的前向传播过程
    y = mnist_inference.inference(x, regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 和 5.2.1 节样例中类似地定义损失函数、学习率、滑动平均操作以及训练过程
    variable_averages = tf.train.ExponentialMovingAverage(
        MOVING_AVERAGE_DECAY,
        global_step
    )
    variables_average_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 = 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
Extracting /tmp/data/train-images-idx3-ubyte.gz
Extracting /tmp/data/train-labels-idx1-ubyte.gz
Extracting /tmp/data/t10k-images-idx3-ubyte.gz
Extracting /tmp/data/t10k-labels-idx1-ubyte.gz

After 1 training step(s) , loss on training batch is 3.32075.
After 1001 training step(s), loss on training batch is 0.241039.
After 2001 training step(s), loss on training batch is 0.227391.
After 3001 training step(s), loss on training batch is 0.138462.
After 4001 training step(s), loss on training batch is 0.132074.
After 5001 training step(s), loss on training batch is  0.103472.
...

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

#! /usr/bin/env python 
# -*- coding:utf-8 -*- 

import time
import tensorflow as tf 
from tensorflow.examples.toturials.mnist import input_data
# 加载mnist_inference.py和mnist_train.py中定义的常量和函数
import mint_inference
import mint_train

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

def evaluate(mnist):
    with tf.Graph().as_default() as g:
        # 定义输入输出的格式
        x = tf.placeholder(
            tf.float32, [None, tf.mnist_inference.INPUT_NODE], name='x-input'
        )
        y_ = tf.placeholder(
            tf.float32, [None, tf.mnist_inference.OUTPUT_NODE], name='y-input'
        )
        validate_feed = {
            x: mnist.validation.images, 
            y: mnist.validation.labels
        }
        # 直接通过封装好的函数来计算前向传播的结果
        # 因为测试时,不关注正则化损失的值,所以这里用于计算正则化损失的函数定义为None
        y = mnist_inference.inference(x, None) 

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

        # 通过变量重命名的方式来加载模型,这样在前向传播的过程中,就不需要调用求滑动平均的函数来获取平均值了。
        # 这样就可以完全用mnist_inference.py中定义的前向传播过程。
        variable_averages = tf.train.ExponentialMovingAverage(mnist_train.MOVING_AVERAGE_DECAY)
        variables_to_restore = variable_averages.variables_to_restore()
        saver = tf.train.Saver(variables_to_restore)

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

                else:
                    print('No checkpoint file found')
                    return 

                time.sleep(EVAL_ENTERVAL_SECS)

def main(argv=None):
    mnist = input_data.read_data_sets('/path/to/mnist_data', one_hot=True)
    evaluate(mnist)

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

上面给出的mnist_eval.py程序会每隔10秒运行一次,每次运行都是读取最新保存的模型,并在MNIST验证数据集上计算模型的正确率。
如果需要离线预测未知数据的类别(比如这个样例程序可以判断手写体数字图片中所包含的数字),只需要将计算正确率的部分改为答案输出即可。
运行mnist_eval.py程序可以得到类似下面的结果。

~/mnist$ python mnist_eval.py
Extracting /tmp/data/train-images-idx3-ubyte.gz
Extracting /tmp/data/train-labels-idx1-ubyte.gz
Extracting /tmp/data/t10k-images-idx3-ubyte.gz
Extracting /tmp/data/t10k-labels-idx1-ubyte.gz

After 1 training step(s), test accuracy = 0.1282
After 1001 training step(s), validation accuracy = 0.9769
After 1001 training step(s), validation accuracy = 0.9769
After 2001 training step(s), validation accuracy = 0.9804
After 3001 training step(s), validation accuracy = 0.982
After 4001 training step(s), validation accuracy = 0.983
After 5001 training step(s), validation accuracy = 0.9829
After 6001 training step(s), validation accuracy = 0.9832
After 6001 training step(s), validation accuracy = 0.9832

注意因为这个程序每10秒自动运行一次,而训练程序不一定每10秒输出一个新模型,所以在下面的结果中会发现有些模型被测试了多次。一般在解决真实问题时,不会这么频繁地运行评测程序。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容