TensorFlow学习笔记——MNIST数字识别问题

引言

TensorFlow 版本1.15pip3 install tensorflow==1.15.0
这是《TensorFlow实战Google深度学习框架(第2版)》的学习笔记,所有代码在TensorFlow 1.15版本中运行正常

本文通过一个实际问题来验证上篇文章中介绍的方法。

MNIST 数据处理

MNIST是一个手写数字识别数据集。每张图片都是28x28的。

from tensorflow.examples.tutorials.mnist import input_data 

mnist = input_data.read_data_sets ("./datasets/mnist", one_hot=True) 
import matplotlib.pyplot as plt
# 看一下数据集大小
print('Trainning data size: ', mnist.train.num_examples)
print('Validating data size: ', mnist.validation.num_examples)
print('Testing data size: ', mnist.test.num_examples)
# 看一下一张图片 处理后的维度是(784,),要显式这张图片需要转换为(28,28)
plt.imshow(mnist.train.images[1].reshape(-1,28),cmap='binary') 
print("Example trainning data label: ",mnist.train.labels[1])

输出:

Trainning data size:  55000
Validating data size:  5000
Testing data size:  10000
Example trainning data label:  [0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]

在这里插入图片描述

通过 input_data.read_ data_ sets 函数生成的类会自动将 MNIST数据集划分为train validation test 个数据集,大小分别为55000,5000,10000。

input data.read_ data_ sets 函数生成的类还提供了 mnist.train.next_batch 函数,它可以从所有的训练数据中读取小部分作为一个训练batch。 以下代码显示了如何使用这个功能:

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

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

本节将利用MNIST数据集实现并研究上篇文章中介绍的神经网络模型设计及优化的方法。

TensorFlow训练神经网络

在训练神经网络时,我们知道可以带指数衰减的学习率设置、使用正则化来避免过拟合,以及使用滑动平均模型来使得最终模型更加健壮。以下代码给出了一个在MNIST数据集上实现这些功能的完整的TensorFlow程序。

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

# MNIST数据集相关的常数
INPUT_NODE = 784 #28x28
OUTPUT_NODE = 10 #分别对应数字0-9

# 配置神经网络的参数
LAYER1_NODE = 500 #隐藏层的节点数
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 #滑动平均衰减率


def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
    '''
    计算神经网络前向传播结果函数。
    
    input_tensor: 输入张量
    avg_class: 滑动平均类
    weights1: 隐藏层的权重
    biases1: 隐藏层的偏差
    weights2: 输出层的权重
    biases2: 输出层的偏差
    '''
    
    
    # 当没有提供滑动平均类时, 直接使用参数当前的取值
    if avg_class == None:
        # 计算隐藏层的前向传播结果,使用了ReLU激活函数
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)
        # 计算输出层的前向传播结果,因为在计算损失函数时会一并计算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]))
    
    # 计算在当前参数下神经网络前向传播的结果,这里没有使用滑动平均
    # 主要是用来计算损失
    y = inference(x, None, weights1, biases1, weights2, biases2)
    
    # 定义存储训练数的变量,这个变量不需要计算滑动平均值,所以指定为不可训练的变量,trainable=False。默认为True
    global_step = tf.Variable(0, trainable=False)
    
    # 给定滑动平均衰减率和训练轮数的变量,初始化滑动平均类。
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    # 在所有代表神经网络参数的变量上使用滑动平均。tf.trainable_variables返回的就是图上集合
    # GraphKeys.TRAINABLE_VARIABLES中的元素,这个集合的元素就是所有没有指定trainable=False的参数
    variable_averages_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函数来计算交叉熵。
    # 当分类问题只有一个正确答案时,可以使用这个函数来加速交叉熵的计算。
    # 通过argmax得到正确答案对应的类别编号
    # 这个函数的第1个参数是神经网络不包括 softmax 层的前向传播结果,第2个参数个是训练数据的正确答案
    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)
    # 计算模型的正则化损失。 一般只计算权重的正则化损失,偏置不需要计算
    regularizatioin = regularizer(weights1) + regularizer(weights2)
    # 总损失等于交叉熵损失和正则化损失的和
    loss = cross_entropy_mean + regularizatioin
    # 设置指数衰减的学习率
    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, variable_averages_op)是等价的
    with tf.control_dependencies([train_step, variable_averages_op]):
        train_op = tf.no_op(name='train')

    # 检验使用了滑动平均模型的神经网络前向传播结果是否正确。
    # tf.agrmax(average_y,1)计算每一个样例的预测答案。其中average_y是一个batch_size * 10的二维数组,每一行表示一个样例的前向传播结果。
    # tf.argmax的第二个参数1表示选取最大值的操作仅在第1个维度中进行,也就是说,只在每一行选取最大值对应的下标。
    # 于是得到的结果是一个长度为batch的一维数组,这个一维数组中的值就表示了每一个样例对应的数字识别结果。
    # tf.equal判断两个张量的每一维是否相等,如果相等返回True,否则返回False。
    correct_prediction = tf.equal(tf.argmax(average_y, 1),tf.argmax(y_,1))
    # tf.case这里将布尔类型的数值转换为实数型,然后计算平均值。这个平均值就是模型在这一组数据上的正确率
    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数据集比较小,所以一次可以处理所有的验证数据,这里就没有分批次。
                validate_acc = sess.run(accuracy, feed_dict=validate_feed)
                print("After %d trainning 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("./datasets/mnist", one_hot=True)
    train(mnist)
    
if __name__ == '__main__':
    # TensorFlow提供的程序入口,会调用上面定义的main函数
    tf.app.run()
    

                          

输出:

Extracting ./datasets/mnist\train-images-idx3-ubyte.gz
Extracting ./datasets/mnist\train-labels-idx1-ubyte.gz
Extracting ./datasets/mnist\t10k-images-idx3-ubyte.gz
Extracting ./datasets/mnist\t10k-labels-idx1-ubyte.gz
After 0 trainning step(s), validation accuracy using average model is 0.158
After 1000 trainning step(s), validation accuracy using average model is 0.9778
After 2000 trainning step(s), validation accuracy using average model is 0.9818
After 3000 trainning step(s), validation accuracy using average model is 0.9836
After 4000 trainning step(s), validation accuracy using average model is 0.9844
After 5000 trainning step(s), validation accuracy using average model is 0.9844
After 6000 trainning step(s), validation accuracy using average model is 0.9854
...
After 24000 trainning step(s), validation accuracy using average model is 0.9848
After 25000 trainning step(s), validation accuracy using average model is 0.9848
After 26000 trainning step(s), validation accuracy using average model is 0.9852
After 27000 trainning step(s), validation accuracy using average model is 0.9852
After 28000 trainning step(s), validation accuracy using average model is 0.9854
After 29000 trainning step(s), validation accuracy using average model is 0.9854
After 30000 training step(s) , test accuracy using average model is 0.9832

从结果可以看到,在训练初期,随着训练的进行,模型在验证数据集上的表现越来越好。

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

上一小节给出了使用神经网络解决MNIST问题的完整程序。这个程序的开始设置了初始学习率、学习率衰减率、隐藏层节点数据、迭代轮数等7种不同参数。那么如何设置这些参数的取值呢?一般是通过实验来调整的。
虽然一个模型的效果最终是通过测试数据来评判的,但是我们不能直接通过模型在测试数据上的效果来选择参数。因为这样会导致模型过度拟合测试数据,从而失去对未知数据的判断能力。

于是,为了评判神经网络模型在不同参数下的效果,一般会从训练数据中抽取一部分作为验证数据,使用验证数据来评判不同参数取值下模型的表现。

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

# 计算滑动平均模型在验证数据上的结果,因为MNIST数据集比较小,所以一次可以处理所有的验证数据,这里就没有分批次。
                validate_acc = sess.run(accuracy, feed_dict=validate_feed)
                test_acc = sess.run(accuracy, feed_dict=test_feed)
                print("After %d trainning step(s), validation accuracy using average model is %g, test accuracy using average model is %g"
                       % (i, validate_acc,test_acc))

在这里插入图片描述

上图给出了每1000轮滑动平均模型在不同数据集上的正确率曲线。图中蓝色曲线表示模型在验证数据上的正确率;而黄色曲线表示测试数据上的正确率。从图中可以看出,虽然这两条曲线不会完全重合,但是这两条曲线的趋势基本一致(注意准确率的刻度)。这意味着在MNIST问题上,完全可以通过模型在验证数据上的表现来判断一个模型的优劣。

不同模型效果比较

本节通过MNIST数据集来比较上篇文章中提到的不同优化方法对神经网络模型正确率(测试集上的正确率)的影响。下面给出了在相同神经网络参数下,使用不同优化方法,经过30000轮训练迭代后,得到的最终模型的正确率。图中给出的结果中包含了使用所有优化方法训练得到的模型和不用其中某一项优化方法(控制变量法)训练得到的模型。通过这种方式,可以有效验证每一项优化方法的效果。

在这里插入图片描述
从上图可以很明显地看出,调整神经网络的结构对最终的准确率有非常大的影响。
同时可以发现使用滑动平均模型、指数衰减的学习率和使用正则化带来的正确率的提升并不是特别明显。

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

变量管理

我们上面将计算神经网络前向传播结果的过程抽象成了一个函数。这个函数包含了我们单隐藏层神经网络的所有参数。然而,当神经网络的结构更加复杂、参数更多时,就需要一个更好的方式来传递和管理神经网络中的参数了。

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

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.constant_initializer和常数生成函数tf.constant功能上是一致的。
TensorFlow提供了7种不同的初始化函数,下标总结了它们的功能和主要参数。

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

tf.get_variable函数与tf.Variable函数最大的区别在于指定变量名称的参数。对于前者,变量名称时必填的参数;对于后者变量名称时可选的参数。

上面的代码中,tf.get_variable会首选尝试创建一个名为v的参数,如果创建失败(因为已经有同名的参数),那么就会报错。这可以避免无意识的变量复用造成的错误。

如果需要通过tf.get_variable获取一个已经创建的变量,需要通过tf.variable_scope函数来生成一个上下文管理器,并明确指定在这个上下文管理器中,tf.get_variable将直接获取已经生成的变量。下面给出代码示例:

import tensorflow as tf
# 在名为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 or reuse=tf.AUTO_REUSE in VarScope? Originally defined at:
#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(v1 == v)  #输出为 True ,代表 v, vl 代表的是相同的 TensorFlow 中变盘。

# 将reuse设为True时,tf.variable_scope将只能获取已经创建过的变量。因为在命名空间bar中还没有创建变量v,所以下面代码会报错:
# ValueError: Variable bar/v does not exist, or was not created with tf.get_variable(). 
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.varaible_scope函数使用参数reuse=Nonereuse=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为True
    with tf.variable_scope('foo',reuse=True):
        print(tf.get_variable_scope().reuse) # True
        # 新建一个嵌套的上下文管理器,但不指定reuse,这时reuse的取值会和外面一层保持一致
        with tf.variable_scope('bar'):
                print(tf.get_variable_scope().reuse) # True
    print(tf.get_variable_scope().reuse) # False。在当前上下文中,reuse的值为False

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

import tensorflow as tf

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 当bar命名空间退出之后,变量名称也不会再被加入其前缀了
    
# 创建一个名称为空的命名空间,并设置reuse=True
with tf.variable_scope("",reuse=True):
    v5 = tf.get_variable('foo/bar/v',[1]) #可以直接通过带命名空间名称的变量名来获取其他命名空间下的变量
    print(v5 == v3) # True
    v6 = tf.get_variable('foo/v1',[1]) 
    print(v6 == v4) # True

通过tf.variable_scopetf.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):
        # 根据入参reuse来判断是创建新变量还是使用已经创建好的
        # 在第一次构造网络时需要创建新的变量,以后每次调用这个函数直接使用reuse=True
        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)

# 当再次使用已经训练好的神经网络时,可以直接下面这样调用
new_x = ...
new_y = inference(new_x, True)

使用上述代码所示的方式,就不再需要将所有变量都作为参数传递到不同的函数中了。这样可以大大提高程序的可读性。

TensorFlow模型持久化

为了复用训练好的模型,需要进行持久化。

持久化代码实现

TensorFlow提供了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)
    # 保存模型到models文件下,若没有models文件夹,tf会新建一个
    saver.save(sess, './models/model.ckpt')

上面这段代码生成的一个文件为model.ckpt.meta,它保存了TensorFlow计算图的结构。
第二个文件为model.ckpt,保存了TensorFlow程序中每一个变量的取值,
最后一个文件为ckeckpoint文件,这个文件保存了一个目录下所有的模型文件类别。

下面给出加载这个已经保存的模型的方法:

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()

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

输出:

INFO:tensorflow:Restoring parameters from ./models/model.ckpt
[3.]

在加载模型的代码中没有运行变量的初始化过程,而是将变量的值通过已经保存的模型加载进来。
如果不希望重复定义图上的运算,也可以直接加载已经持久化的图:

import tensorflow as tf

# 直接加载持久化的图
saver = tf.train.import_meta_graph('./models/model.ckpt.meta')

with tf.Session() as sess:
    saver.restore(sess, './models/model.ckpt')
    # 通过张量的名称来获取张量
    print(sess.run(tf.get_default_graph().get_tensor_by_name('add:0')))

输出:

INFO:tensorflow:Restoring parameters from ./models/model.ckpt
[3.]

上面给出的代码,默认保存和加载了计算图上定义的全部变量。也可以只保存或加载部分变量。

在声明tf.train.Saver类时可以提供一个列表来指定需要保存或加载的变量。
比如使用saver = tf.train.Saver([v1])命令来构建tf.train.Saver类,那么只有变量v1会被加载进来。

除了可以选取需要被加载的变量,tf.train.Saver类也支持保存或加载时给变量重命名:

import tensorflow as tf

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})

下面给出一个保存滑动平均模型的样例:

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) # v:0

ema = tf.train.ExponentialMovingAverage(0.99)
maintain_averages_op = ema.apply(tf.global_variables())
# 在声明滑动平均模型后,TensorFlow会自动生成一个影子变量
# v/ExponentialMovingAverage
for variables in tf.global_variables():
    print(variables.name) #v/ExponentialMovingAverage:0
    
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)
    # 保存时,TensorFlow会将v:0和v/ExponentialMovingAverage:0 两个变量都保存下来
    saver.save(sess, './models/model1.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, './models/model1.ckpt')
    print(sess.run(v)) # 0.099999905

输出的就是原来模型中变量v的滑动平均值。

为了方便加载时重命名滑动平均变量,tf.train.ExponentialMovingAverage类提供了variables_to_restore函数来生成tf.train.Saver类所需要的变量重命名字典:

import tensorflow as tf
v = tf.Variable(0, dtype=tf.float32, name='v')

ema = tf.train.ExponentialMovingAverage(0.99)

# 通过variables_to_restore函数可以直接生成上面代码中提供的字典
# {'v/ExponentialMovingAverage':v}
# 这句代码会输出:
# {'v/ExponentialMovingAverage': <tf.Variable 'v:0' shape=() dtype=float32_ref>}
# 输出中后面的Variable类就代表了变量v
print(ema.variables_to_restoreVariable())

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

with tf.Session() as sess:
    saver.restore(sess, './models/model1.ckpt')
    print(sess.run(v)) # 0.099999905

使用tf.train.Saver会报错运行TensorFlow程序所需要的全部信息,然而有时并不需要某些信息。
于是TensorFlow提供了convert_variables_to_constants函数,通过这个函数可以将计算图中的变量及其取值通过常量的方式保存,
这样整个TensorFlow计算图可以统一存放在一个文件中:

import tensorflow as tf
from tensorflow.python.framework import graph_util
v1 = tf.Variable(tf.constant(1.0,shape=[1]), name='v1')
v2 = tf.Variable(tf.constant(2.0,shape=[1]), name='v2')
result = v1 + v2

init_op = tf.global_variables_initializer()



with tf.Session() as sess:
    sess.run(init_op)
    # 导出当前计算图的GraphDef部分,只需要这一部分就可以完成从输入层到输出层的计算过程
    graph_def = tf.get_default_graph().as_graph_def()
    # 将图中的变量及其取值转化为常量,同时将图中不必要的节点去掉
    # ['add']给出了需要保存的节点名称。add节点是上面定义的两个变量相加的操作
    # 这里给出的是计算节点的名称,所以没有后面的:0。
    output_graph_def = graph_util.convert_variables_to_constants(sess,graph_def,['add'])
    # 将导出的模型存入文件
    with tf.gfile.GFile('./models/combined_model.pb','wb') as f:
        f.write(output_graph_def.SerializeToString())
    

再通过以下程序可以直接计算定义的加法运算结果。当只需要得到计算图中某个节点的取值时,这提供了一个更加方便的方法。

import tensorflow as tf

from tensorflow.python.platform import gfile

with tf.Session() as sess:
    model_filename = './models/combined_model.pb'
    # 读取保存的模型文件,并将文件解析成对应的GraphDef Protocol Buffer
    with gfile.FastGFile(model_filename, 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
    # 将graph_def 中保存的图加载到当前的图中
    # return_elements=['add:0'] 给出了返回的张量的名称
    # 在保存的时候给出的是计算节点的名称,所以为"add"。
    # 在加载的时候给出的是张量的名称,所以是add:0
    result = tf.import_graph_def(graph_def, return_elements=['add:0'])
    print(sess.run(result)) # [array([3.], dtype=float32)]

持久化原理及数据格式

当调用saver.save函数时,TensorFlow会自动生成4个文件,持久化就是通过这个4个文件完成的。

TensorFlow是一个通过图的形式来表述计算的编程系统,TensorFlow程序中的所有计算都会被表达为计算图上的节点。TensorFlow通过元图(MetaGraph)来记录计算图中节点的信息以及运算计算图中节点所需要的元数据。

TensorFlow中元图是由MetaGraphaDef 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 AssetFielDef asset_file_def = 6;
}

可以看出,元图中主要记录了6类信息。下面将结合上面变量相加样例的持久化结果,逐一介绍MetaGraphDef类型的每一个属性中存储的信息。
保存MetaGraphDef信息的文件默认以.meta为后缀名,在样例中,文件model.ckpt.meta中存储的就是元图的数据。

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()
# 通过export_meta_graph函数导出计算图的元图,并保存为JSON格式
saver.export_meta_graph('./models/model.ckpt.meda.json',as_text=True)


通过上面的代码,将计算图元图以JSON的格式导出并存储在model.ckpt.meta.json文件中。下棉将集合model.ckpt.meta.json文件具体介绍TensorFlow元图中存储的新。

meta_info_def属性

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

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

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

stripped_op_list时不为空的,记录了计算图上使用到的所有运算方法的信息。该属性的类型是OpListOpList类型是一个OpDef类型的列表,下面给出OpDef类型的定义:

message OpDef {
    string name = l ;
    repeated ArgDef input_arg = 2;
    repeated ArgDef output_arg = 3;
    repeated AttrDef attr = 4;
    OpDeprecation deprecation = 8;
    string summary = 5;
    string description = 6;
    bool is_commutative = 18;
    bo is_aggregate = 16
    bool is_stateful = 17;
    bool allows_uinitialized_input = 19; 
}

OpDef类型中前4个属性定义了一个运算最核心的信息。OpDef中的name定义了运算的名称,这是运算的唯一标识符。
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_BFLOAT16
        type: DT_HALF
        type: DT_FLOAT
        type: DT_DOUBLE
        type: DT_UINT8
        type: DT_INT8
        type: DT_INT16
        type: DT_INT32
        type: DT_INT64
        type: DT_COMPLEX64
        type: DT_COMPLEX128
      }
    }
  }
}

上面给出了名称为Add的运算,这个运算有2个输入和1个输出,输入输出属性都指定了属性type_attr,并且这个属性的值为T
OpDefattr属性中,必须要出现名称(name)为T的属性。以上样例中,这个属性指定了运算输入输出允许的参数类型allowed_values

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

graph_def属性

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

message GraphDef {
    repeated NodeDef node = 1;
    VersionDef versions = 4;
    #还有一些已经不用的或者还在试验中的属性
    message NodeDef {
        string name = 1;
        string op = 2;
        repeated string input = 3;
        string device =4;
        map<string,AttrValue> attr = 5;
    }
    ...
 }

versions记录了TensorFlow的版本号,GraphDef的主要信息都存在node属性中,记录了计算图上所有的节点信息。其中name是节点的唯一标识符。
op属性给出了该节点使用的运算方法的名称。

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

NodeDef类型中的device属性指定了处理这个运算的设备,为空时,TensorFlow自动选取一个最合适的设备来运行。
最后NodeDef类型中的attr属性指定了和当前运算相关的配置信息。下面列举了model.ckpt.meta.json文件中的一些计算节点来更加具体地介绍graph_def属性。

graph_def {
  node {
    name: "v1"
    op: "VariableV1"
    attr {
      key: "_output_shapes"
      value {
        list {
          shape {
          }
        }
      }
    }
    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"
        ...
    }
    
  
   
    
  }

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

NodeDef 类型中还定义了运算相关的属性。在节点 vl 中,attr 属性指定了这个变量的维度以及类型。

给出的第二个节点是代表加法运算的节点 。它指定了2个输入, 一个为 v1/read,另一个为 v2/read
其中 v1/read 的节点可读取变量v1的值。

v2/read类似的代表了变量v2的取值。
以上样例文件中给出的最后一个名称为 save/control_dependency,该节点是系统在完成
TensorFlow 模型持久化过程中自动生成的一个运算。

saver_def属性

saver_def属性中记录了持久化模型时需要用到的一些参数,其定义如下:

message SaverDef {
    string filename_tensor_name = l;

    string save_tensor_name = 2;
    string restore_op_name = 3;
    int32 max_to_keep = 4;
    bool sharded = 5;
    float keep_checkpoint_every_n_hours = 6;
    enum CheckpointFormatVersion {
        LEGACY = 0;
        Vl = l ;
        V2 = 2;
    }
    CheckpointFormatVersion version = 7; 
}

下面给出了 modelckpt.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属性给出了保存文件名的张量名称,这个张量就是节点save/Const 的第以个输出。
save_tensor_name 属性给出了持久化 TensorFlow 模型的运算所对应的节点名称。
从以上文件中可以看出 这个节点就是在 graph_def 属性中给出的save/control_denpendency 节点。

和持久化TensorFlow 模型运算对应的是加载 TensorFlow模型的运算,这个运算的名称由 restore op_ name 属性指定。
max to_ keep 属性和keep_checkpoint_every_n_hours 属性设定了 tf.train.Saver 类清理之前保存的模型的策略。
比如当max to_ keep为5的时候,在第六次调用 saver.save 时,第一次保存的模型就会被自动删除。

通过设置keep_checkpoint_every_n_hours ,每n小时可以在 max_to_ keep 的基础上多保存一个模型。

collection_def 属性

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

message CollectionDef {
    message NodeList {
        repeated string value = 1;
    }
    message BytesList {
        repeated bytes value = 1;
    }
    message Int64List {
        repeated int64 value= 1 [packed= true];
    }
    message FloatList {
        repeated float value= 1 [packed= true];
    }
    message AnyList {
        repeated google.protobuf.Any value = 1;
    }
    oneof kind {
        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\004vl :0\022\tvl/Assign\032\tvl/read:0"
            value :"\n\004v2 :0\022\tv2/Assign\032\tv2/read:0"
        }
    }
    collection_def {
        key: "variables"
        value {
            bytes_list {
                value :"\n\004vl :0\022\tvl/Assign\032\tvl/read:0"
                value :"\n\004v2 :0\022\tv2/Assign\032\tv2/read:0"
            }
        }
    }

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

除了持久化 TensorFlow 计算图的结构,持久化 TensorFlow中变量的取值也是非常重要一个部分。
使用 tf.Saver 得到的 model.ckpt.indexmodel.ckpt.data-*****-of-*****文件就保存了所有变量的取值。
其中model.ckpt.data 文件是通过 SSTable 格式存储的,可以大致理解为就是一个(key,value)列表。
TensroFlow 提供tf.train.NewCheckpointReader 类来查看保存的变量信息:

import tensorflow as tf

reader = tf.train.NewCheckpointReader('./models/model.ckpt')

# 获取所有变量列表,这是一个从变量名到变量维度的字典

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_name] )

print("Value for variable v1 is ", reader.get_tensor("v1"))

输出:

v1 [1]
v2 [1]
Value for variable v1 is  [1.]

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

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

model_checkpoint_path属性保存了最新的TensorFlow模型文件的文件名。

all_model_checkpoint_paths属性列出了当前还没有被删除的所有TensorFlow模型文件的文件名。

TensorFlow最佳实践样例程序

本节将介绍一个TensorFlow训练神经网络模型的最佳实践。将训练和测试分成两个独立的程序,这可以使得每一个组件更加灵活。

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

先给出mnist_inference.py的代码:

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的集合。
    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_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.constant_initializer(0.0))
        layer2 = tf.matmul(layer1, weights) + biases
    
    # 返回前向传播的结果
    return layer2


这段代码中定义了神经网络前向传播算法。训练和测试都可以直接调用inference这个函数,下面给出训练过程的代码:

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

# 加载mnist_inference.py中定义的常量和前向传播的函数
# import mnist_inference 本人在jupyter中运行这些程序,所以实际不需要这句代码
 
# 配置神经网络的参数
BATCH_SIZE = 100
LEARING_RATE_BASE = 0.8
LEARING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 30000
MOVING_AVERAGE_DECAY = 0.99
# 模型保存的路径和文件名
MODEL_SAVE_PATH = './models'
MODEL_NAME = 'model.ckpt'

def train(mnist):
    # 定义输入输出placeholder
    x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-input')
    
    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    # 直接调用已经定义好的前向传播过程
    y = inference(x, regularizer)
    global_step = tf.Variable(0, trainable=False)
    
    # 定义损失函数、学习率、滑动平均操作以及训练过程
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    variable_averages_op = variable_averages.apply(tf.trainable_variables())
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(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')) # add_n将参数列表中的元素相加
    learing_rate = tf.train.exponential_decay(
        LEARING_RATE_BASE,
        global_step,
        mnist.train.num_examples / BATCH_SIZE,
        LEARING_RATE_DECAY
    )
    train_step = tf.train.GradientDescentOptimizer(learing_rate).minimize(loss, global_step=global_step)
    with tf.control_dependencies([train_step, variable_averages_op]):
        train_op = tf.no_op(name='train')
    
    # 初始化TensorFlow持久类
    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        # 在训练过程中不再测试模型在验证数据上的表现,验证和测试的过程将会有一个独立的程序来完成
        for i in range(TRAINING_STEPS):
            xs ,ys = mnist.train.next_batch(BATCH_SIZE)
            # 得到损失值,step信息
            _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys})
            # 每1000轮保存一次模型
            if i % 1000 == 0 :
                print('After %d trainning step(s), loss on trainning batch is %g.' % (step, loss_value))
                saver.save(sess, os.path.join(MODEL_SAVE_PATH,MODEL_NAME), global_step=global_step)
    
def main(argv=None):
    mnist = input_data.read_data_sets('./datasets/mnist',one_hot=True)
    train(mnist)

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

输出:

After 1 trainning step(s), loss on trainning batch is 3.01793.
After 1001 trainning step(s), loss on trainning batch is 0.174706.
After 2001 trainning step(s), loss on trainning batch is 0.185019.
After 3001 trainning step(s), loss on trainning batch is 0.130362.
After 4001 trainning step(s), loss on trainning batch is 0.113058.
After 5001 trainning step(s), loss on trainning batch is 0.106483.
...

不再将训练和测试跑一起。现在通过在当前batch上损失值的大小来估计训练的效果。并且每1000轮保存一次训练好的模型。
下面给出测试程序mnist_eval.py

import time
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
# 没10秒加载一次最新的模型,并在测试数据上验证最新模型的正确率
EVAL_INTERVAL_SECS = 10

def evalute(mnist):
    with tf.Graph().as_default() as g:
        # 定义输入输出的格式
        x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
        y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-input')
        validate_feed = {x: mnist.validation.images,
                         y_: mnist.validation.labels}
        # 测试时不关注正则化损失的值
        y = inference(x, None)
        # 使用前向传播的结果计算正确率
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        # 通过变量重命名的方式来加载模型
        variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY)
        variable_to_restore = variable_averages.variables_to_restore()
        saver = tf.train.Saver(variable_to_restore)
        
        # 每隔EVAL_INTERVAL_SECS调用一次计算正确率的过程以检验训练过程中正确率的变化
        while True:
            with tf.Session() as sess:
                # get_checkpoint_state会通过checkpoint问卷自动找到目录中最新模型的文件名
                ckpt = tf.train.get_checkpoint_state(MODEL_SAVE_PATH)
                if ckpt and ckpt.model_checkpoint_path:
                    # 加载模型
                    saver.restore(sess, ckpt.model_checkpoint_path)
                    # 通过文件名得到模型保存时迭代的轮数
                    global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]
                    accuracy_score = sess.run(accuracy, feed_dict=validate_feed)
                    print('After %s training step(s) ,validation accuracy = %g' % (global_step, accuracy_score))
                else:
                    print('No checkpoint file found')
            time.sleep(EVAL_INTERVAL_SECS)

def main(argv=None):
    mnist = input_data.read_data_sets('./datasets/mnist',one_hot=True)
    evalute(mnist)

    

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

输出类似于:

After 29001 training step(s) ,validation accuracy = 0.986
INFO:tensorflow:Restoring parameters from ./models\model.ckpt-29001
After 29001 training step(s) ,validation accuracy = 0.986
INFO:tensorflow:Restoring parameters from ./models\model.ckpt-29001
After 29001 training step(s) ,validation accuracy = 0.986
INFO:tensorflow:Restoring parameters from ./models\model.ckpt-29001

上面给出的程序会每隔10s运行一次,每次运行都是读取最新保存的模型,并在 MNIST 验证数据集上计算模型的正确率。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值