欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

TensorFlow:实战Google深度学习框架第二版——第五章

程序员文章站 2024-03-02 11:52:40
...

目录

第五章——MNIST数字识别问题

5.1 MNIST数据处理

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

5.2.1 TensorFlow训练神经网络——完整程序

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

5.2.3不同模型效果的比较

5.3 变量管理

5.4 TensorFlow模型持久化

5.4.1持久化代码实现

5.4.2 持久化原理及数据格式

model.ckpt.meta文件

剩余三个文件

5.5 TensorFlow最佳实践样例程序


第五章——MNIST数字识别问题

本章将使用的数据集是 MNIST手写体数字识别数据集。在很多深度学习教程中,这个数据集都会被当作第一个案例。在验证神经网络优化方法的同时,本章也会介绍使用 TensorFlow 训练神经网络的最佳实践。 


5.1 MNIST数据处理

MNIST 是一个非常有名的手写体数字识别数据集,在很多资料中,这个数据集都会被用作深度学习的入门样例。MNIST 数据集是 NIST 数据集的一个子集,它包含了 60000 张图片作为训练数据, 10000 张图片作为测试数据。在 MNIST 数据集中的每一张图片都代表了 0~9 中的一个数字,标签集用 ont_hot 编码表示手写数字(例:表示数字3→[0,0,0,1,0,0,0,0,0,0])。图片的大小都为 28x28, 且数字都会出现在图片的正中间。

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

from tensorflow.examples.tutorials.mnist import input_data

#载入 MNIST 数据集,若指定地址下没有,则自动从网上下载
mnist = input_data.read_data_sets("F:\MNIST_data",one_hot = True)

print('Training data size',mnist.train.num_examples)    #55000
print('Validating data size', mnist.validation.num_examples)  #5000
print('Testing data size', mnist.test.num_examples)           #10000
#分别输出示例图片数据及对应标签
print('example training data :',mnist.train.images[0])   #28*28=784  一维数组
print('example training data label:',mnist.train.labels[0])

为了方便使用随机梯度下降,input_data.read_data_sets函数生成的类还提供了 mnist.train.next_batch 函数,它可以从所有的训练数据中读取一小部分作为一个训练 batchbatch。代码如下:

batch_size = 100
#该函数每次执行都会从数据集中顺序读取
xs, ys = mnist.train.next_batch(batch_size)


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

OUT:
X shape: (100, 784)
Y shape: (100, 10)

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

5.2.1 TensorFlow训练神经网络——完整程序

给出一个完整的 Tensorflow 程序来解决 MNIST 手写体数字识别问题。

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

点击此处链接获取代码     

这里的程序是将所有流程写在一个py文件中,在本章最后,会进行优化划分得到一个最佳示例。

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

一般两种方式来判断模型效果:

交叉验证(cross validation)是在机器学习建立模型和验证模型参数时常用的办法。交叉验证,顾名思义,就是重复的使用数据,把得到的样本数据进行切分,组合为不同的训练集和测试集,用训练集来训练模型,用测试集来评估模型预测的好坏。在此基础上可以得到多组不同的训练集和测试集,某次训练集中的某样本在下次可能成为测试集中的样本,即所谓“交叉”。常用于数据量少时。

划分为固定的训练集,验证集,测试集,用验证集来进行验证判断。 常用于数据量比较充足时。

将上一节中的代码做略微调整,使 validate_set 和 test_set 的正确率同时显示,发现 validate dataset 分布接近 test dataset 分布,可验证模型在验证数据上的表现可以很好的体现模型在测试数据上的表现。因此,对于验证数据的选择很重要。

#迭代地训练神经网络
for i in range(0,TRAINING_STEPS):
    if i % 1000 == 0:
        validate_acc = sess.run(accuracy, feed_dict = validate_feed)
        test_acc = sess.run(accuracy, feed_dict = test_feed)
        print("在 %d 次迭代后,验证数据集的正确率为 : %g , 测试数据集的正确率为 : %g" % (i, validate_acc,test_acc))

TensorFlow:实战Google深度学习框架第二版——第五章

5.2.3不同模型效果的比较

这里是指使用不同的优化方法,网络结构不变,最终训练结果的不同。第四章共提到五种优化方法:在神经网络结构的设计上,需要使用**函数与多层隐藏层,在神经网络优化时,可以使用指数衰减的学习率,加入正则化的损失函数,加入滑动平均模型。

TensorFlow:实战Google深度学习框架第二版——第五章

从上图可以看出,神经网络结构对最终模型的效果具有本质性的影响。

我们看到,滑动平均模型和指数衰减的学习率对正确率的结果似乎没有什么影响,这是因为滑动平均模型和指数衰减的学习率在一定程度上都是限制神经网络中参数更新的速度,在 MNIST 数据集中,迭代在4000轮的时候就已经接近收敛,收敛速度很快,所以这两种优化对最终模型的影响不大。但是当问题更加复杂时,迭代不会这么快接近收敛,这时滑动平均模型和指数衰减的学习率可以发挥出更大的作用。比如 Cifar-10 图像分类数据集,使用滑动平均模型可以将错误率降低 11%,而使用指数衰减的学习率可以将错误率降低 7%。

正则化损失对于模型的影响较大。

在图 5-6 中灰色(淡色)和黑色(深色)的实线给出了两个模型正确率的变化趋势, 虚线给出了在当前训练batch 上的交叉熵损失。

TensorFlow:实战Google深度学习框架第二版——第五章

从图5-6可以看出,不使用正则化损失的话,在训练集上只优化交叉熵(灰色虚线)确实比优化总损失(黑色虚线)的损失值要小,但是在测试数据的正确率上,反而是优化总损失(黑色实线)的正确率高。

总的来说,各种优化方式,首先使用**函数和隐藏层是必要的;其次,指数衰减的学习率,加入正则化的损失函数,加入滑动平均模型这三种优化方法,对于不同问题的提升效果是不同的,需要具体问题具体验证分析。


5.3 变量管理

在5.2.1节中,将前向传播的过程抽象为一个函数,通过这种方式在训练和测试的过程中可以统一调用同一个函数来得到模型的前向传播结果。如下:

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

这个函数的参数包括了神经网络的所有参数(weights1,biases1,weights2,biases2),但是当网络结构复杂时,上述方式不再适用,需要更好的方式来传递和管理神经网络中的参数。

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

重要函数:tf.get_variable()与tf.variable_scope()

#下列两个函数功能基本等价,都是创建变脸

#第四章所用
v=tf.Variable(tf.constant(1.0,shape=[1]),name='v')

#第五章所学重点函数
v=tf.get_variable('v',shape=[1],initializer=tf.constant_initializer(1.0))

两个函数创建变量的过程基本上是一样的。tf.get_variable函数调用时提供的维度(shape)信息以及初始化方法(initializer)的参数和 tf.Variable 函数调用时提供的初始化过程中的参数也类似。 TensorFlow 中提供的 initializer 函数同 3.4.3 小节中介绍地随机数以及常量生成函数大部分是一一对应的。 比如变量初始化函数 tf.constant_initializer 和常数生成函数 tf.constant 功能上就是一致地。 TensorFlow 提供了 7 种不同地初始化函数,如下: 

TensorFlow:实战Google深度学习框架第二版——第五章

tf.get_variable()与tf.Variable()函数最大的区别在于变量名参数,对于前者是必须的,对后者是可选的,通过name='v'的形式给出!

在上面的案例程序中,tf.get_variavle()会先试图去创建一个名字为v的参数,若创建失败(比如已经存在同名的参数),那么程序会报错,这是为了防止无意识的变量复用造成的错误。比如在定义神经网络参数时,第一层网络权重已经叫 weights 了,那么在创建第二层神经网络时,如果参数名仍叫 weights,就会触发变量重用的错误。否则两层神经网络共用一个权重会出现一些比较难以发现的错误。

如果需要通过tf.get_varivable()获取一个已经创建的变量,需要通过tf.variable_scope()来生成一个上下文管理器,并明确指明在这个上下文管理器中,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 的变量
# ValueError: 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('a', [1])
    print(v == v1) #输出为 True, 代表v, v1 代表的是相同的 TensorFlow 中变量

# 当参数 reuse 设置为 True 时, tf.variable_scope 将只能获取已经创建过的变量。
# 因为在命名空间 bar 中还没有创建变量 v,所以下面的代码会报错
# ValueError: Variable bar/v does not exist, or was not created with tf.get_variable(). 
#Did you mean to set reuse=tf.AUTO_REUSE 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 参数的取值是如何确定的。

通过 tf.get_variable_scope().reuse 函数来获得当前上下文管理器中 reuse 参数的值。

import tensorflow as tf

with tf.variable_scope("root"):
    #可以通过 tf.get_variable_scope().reuse 函数来获得当前上下文管理器中 reuse 参数的值
    print(tf.get_variable_scope().reuse) # 输出 False,即最外层 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) # 退出 reuse设置为 True的上下文之后, reuse的值又回到了False

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

import tensorflow as tf

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

with tf.variable_scope('foo'):
    v2 = tf.get_variable('v', [1])
    print(v2.name) # 输出 foo/v:0   这里的foo为命名空间

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])
    print(v5 == v3)  # 输出 True, 调用了 该name的变量,v5输出该name下的value

    v6 = tf.get_variable('foo/v1', [1])
    print(v6 == v4) # 输出 True

通过变量管理函数tf.get_variable()与tf.variable_scope(),以下代码对5.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_trnsor, weights) + biases)

    #类似的定义第二层神经网络的变量和前向传播过程。
    with tf.variable_scope('layer2'. reuse = reuse):
        weights = tf.get_variable('weights',[LAYER1_NODE, OUT_NODE],
                                  initializer= tf.truncated_normal_initializer(stddev= 0.1))
        biases = tf.get_variable('biases',[OUT_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 函数中即可。获取或创建变量的部分不需要改变。

new_x = ...
new_y = inference(new_x, True)

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


5.4 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

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

with tf.Session() as sess:
    tf.global_variables_initializer().run()
    saver.save(sess,'/path/to/model/model.ckpt')

# 注,此处保存文件,应在建立了 path/to 两个文件夹的前提下保存,
#这样才能够创建 model文件夹,否则会报错

TensorFlow:实战Google深度学习框架第二版——第五章这是保存的目录下结果。

在指定的保存路径下,生成了四个文件(书上说3个,tf.train.Saver版本为1.0时候)。

书上说的三个是:model.ckpt.meta(计算图结构)+model.ckpt(每一个变量值)+checkpoint(是记录一个目录下所有的模型文件列表)

后来tf.train.Saver版本变为2.0后,保存了四个文件,这是因为 TensorFlow 会将计算图的结构和图上参数取值分开保存。

第一个文件为model.ckpt.meta,它保存了 TensorFlow 计算图的结构,即神经网络的网络结构。 
第二个文件为 model.ckpt.index,索引,用于将data中数据与meta的结构匹配,即值是图上哪个部分的值。
第三个文件为 model.ckpt.data-00000-of-00001,保存了每一个变量的取值 
最后一个文件为 checkpoint 文件,这个文件中保存了一个目录下所有的模型文件列表。

以下示例代码简单给出了加载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
saver = tf.train.Saver()

with tf.Session() as sess:
    saver.restore(sess,'/path/to/model/model.ckpt')
    print(sess.run(result)) 

注意:加载模型代码中没有像保存模型那样进行变量初始化过程,而是通过已保存的模型来加载变量值。如果不希望重复定义图上的运算,也可以直接加载已经持久化的图(计算图就代表了计算结构),如下所示:

import tensorflow as tf 

#直接加载持久化的图,默认加载到当前默认计算图中,这一步是加载网络结构
saver=tf.train.import_meta_graph(
    'path/to/model/model.ckpt/model.ckpt.meta')
with tf.Session() as sess:
    #加载值
    saver.restore(sess,'/path/to/model/model.ckpt')
    #通过张量的名称来获取张量
    print(sess.run(tf.get_default_graph().get_tensor_by_name('add:0')))

上面的程序中,默认保存和加载了 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')

#若直接加载模型,会报错,提示找不到变量,因为变量名不同,在已经保存的模型中找不到新定义得变量。

#可以使用一个字典来重命名变量就可以加载原来得模型了
#这个字典制定了原来名称为v1的变量现在加载到变量v1中(名称为other-v1),同样v2
saver = tf.train.Saver({'v1':v1, 'v2':v2})

这样做的主要目的之一是方便使用变量的滑动平均值。在 TensorFLow 中,每一个变量的滑动平均值是通过影子变量维护的,所以要获取变量的滑动平均值实际上就是获取这个影子变量的取值。如果在加载模型时直接将影子变量映射到变量自身,那么在使用训练好的模型时就不需要再调用函数来获取变量的滑动平均值了。

这样可以大大方便滑动平均模型的使用,以下代码给出了一个保存滑动平均模型的样例:

import tensorflow as tf
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/ExponentiallMoving Average:0  和v:0
for variables in tf.all_variables():
    print(variables.name)

saver = tf.train.Saver()
with tf.Session() as sess:
    tf.global_variables_initializer().run()
    sess.run(tf.assign(v, 10))
    sess.run(maintain_averages_op)

    # 保存时,TensorFLow 会将 v:0 和v/ExponentialMovingAverage:0 两个变量都保存下来
    saver.save(sess,'/path/to/model/model1.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})   #这里v是上面的变量v
with tf.Session() as sess:
    saver.restore(sess,'/path/to/model/model.ckpt')
    print(sess.run(v))  #输出的是原来模型中的变量v的滑动平均值

需要注意是:这里虽说是以字典的形式来重命名,但和字典不同。字典是——名:值,而这里是——值:被赋值变量

为了方便加载时重命名滑动平均变量,tf.train.ExponentialMovingAverage类提供了variable_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':<tensorflow.Variable 'v:0' shape=(),dtype=float32_ref>}
#其中后面的Variable类就代表了变量v
print(ema.variable_to_restore())

saver=tf.train.Saver(ema.variable_to_resotre())
with tf.Session() as sess:
    saver.restore(sess,'/path/to/model/model.ckpt')
    print(sess.run(v))   #输出原模型中的变量v的滑动平均值

使用 tf.train.Saver 会保存运行 TensorFlow 程序所需要的全部信息,然而有时并不需要某些信息。比如在测试或者离线预测时,只需要知道如何从神经网络的输入层经过前向传 播计算得到输出层即可,而不需要类似于变量初始化、模型保存等辅助节点的信息。在第 6 章介绍迁移学习时,会遇到类似的情况。而且,将变量取值和计算图结构分成不同的文件存储有时候也不方便,于是 TensorFlow 提供了 convert_variables_to_constants() 函数,通过这个函数可以将计算图中的变量及其取值通过常量的方式保存,这样整个 TensorFlow 计算图可以统一存放在一个文件中。以下程序提供了一个样例(抽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)
    #导出当前计算图的GraphDef部分,只需要这一部分就可完成从输入层到输出层的计算过程
    graph_def=tf.get_default_graph().as_graph_def()
    
    #将图中的变量及其取值转换为常量,同时将图中不必要的节点去掉。
    #在5.4.2节中将会看到一些系统运算也会被转化为计算图中的节点(比如变量初始化操作)。
    #如果只关心程序中定义的某些运算时,和这些计算无关的节点就没有必要导出并保存了。
    #在下面一行代码中,最后一个参数['add']给出了需要保存的节点名称,所以后面没有:0
    #之前介绍的是张量名称后面有:0,表示某个节点的第一个输出,而计算节点本身是没有:0的
    
    output_graph_def=graph_util.convert_variables_to_constants(
                        sess,graph_def,['add'])
    #将导出的模型存入文件,wb表示写
    with tf.gfile.GFile('/path/to/model/combined_model.pb','wb') as f:
        f.write(output_graph_def.SerializeToString())

上述代码已经完成了模型的固化及保存,接下来的示例代码演示如何加载该模型并进行前向传播运算(加载pb并使用):

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_filename,'rb') as f:
        graph_def=tf.GraphDef()
        graph_def.ParseFromString(f.read())
    
    #将graph_def中保存的图加载到当前的图中,return_elements=['add:0']给出了返回的张量名称。
    #在保存的时候给出的是计算节点的名称,所以为'add'。在加载的时候给出的是张量的名称,所以是add:0
    result=tf.import_graph_def(graph_def,return_elements=['add:0'])
    #输出[3.0]
    print(sess.run(result))

上述内容比较繁杂,初学时会混乱,简单了解即可,当实战时候不停的遇到,即可熟练掌握。

一般是先训练,使用tf.train.Saver类保存为ckpt等文件,然后用类似上面方法将ckpt文件转为pb文件(抽取pb文件),最后直接加载pb文件进行推理即可。

5.4.2 持久化原理及数据格式

本节主要介绍通过tf.train.Saver类保存模型后生成的四个文件,介绍每一个文件中保存的内容及数据格式。初学者简单了解即可,我也迷迷糊糊,以后TensorFlow学习深入后再来加深这部分学习。

注意:接下来的内容比较绕,都是一层层嵌套的!

model.ckpt.meta文件

TensorFLow 是一个通过图的形式来表述计算的编程系统,程序中的所有计算都会被表达为计算图上的节点。 TensorFlow 通过元图(MetaGraph) 来记录计算图中节点的信息及运行计算图中节点所需要的元数据。元图是由 MetaGraphDef Protocol Buffrt 定义的。MetaFraphDef 中的内容就构成了 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类信息,保存 MetaGraphDef 信息的文件默认以 .meta 为后缀名(model.ckpt.meta文件中存储的就是元图的数据)。通过 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()
#通过saver.export_meta_graph()函数导出TensorFlow计算图的元图(meta文件),并保存为json格式
saver.export_meta_graph('/path/to/model.ckpt.meta.json', as_text=True)

运行上面代码,得到的model.ckpt.meta.json文件打开后如下截图所示:

TensorFlow:实战Google深度学习框架第二版——第五章

1.meta_info_def属性

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

下面是metaInfoDef Protocol Buffer的定义:

message MetaInfoDef{
    string meta_graph_version=1;    #计算图的版本号,saver没指定则为空
    
    #记录了TensorFlow计算图上使用到的所有运算方法的信息,一个运算方法仅记录一次
    OpList stripped_op_list=2;      
    google.protobuf.Any any_info=3;
    repeated string tags=4;         #用户指定的标签,saver没指定则为空
    string tensorflow_version=5;    #这个属性和下一行属性记录了生成当前计算图的TensorFlow版本
    string tensorflow_git_version=6;
}

对应上部分定义的实验结果如下:

TensorFlow:实战Google深度学习框架第二版——第五章

stripped_op_list属性的类型为OpList,该类型是一个OpDef类型的列表,即列表中每一个元素op的类型均为OpDef,定义如下:

message OpDef{
    string name=1;   #运算名称,运算的唯一标识符
    
    repeated ArgDef input_arg=2;   #输入
    repeated ArgDef output_arg=3;  #输出
    repeated AttrDef attr=4;    #其他的运算参数信息

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

注意上面的name属性,为运算的唯一标识,将在后面的graph_def中通过name引用!

在 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
                type: DT_STRING
            }
        }
    }
}

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

2. graph_def属性

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

message GraphDef{
    repeated NodeDef node=1  #节点信息
    VersionDef versions=4    #tensorflow版本信息
    #还有一些已经不用了或者该书出版时还在试验中的属性,这里就不列举了(书上内容也到此为止)
}

message NodeDef{
    string name=1;
    #该节点使用的TensorFlow运算方法的名称,
    #通过该名称可以在计算元图的meta_info_def属性中找到该运算的具体信息
    string op=2;   
    repeated string input=3;
    string device=4;
    map<string,AttrValue> attr=5;
}

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

NodeDef类型中的 device 属性指定了处理这个运算的设备。运行 TensorFlow 运算的设 备可以是本地机器的 CPU 或者 GPU,也可以是一台远程的机器 CPU 或者 GPU。第 10 章 将具体介绍如何指定运行 TensorFlow 运算的设备。当 device 属性为空时, TensorFlow 在运行时会自动选取一个最合适的设备来运行这个运算。

最后 NodeDef类型中的 attr 属性指定了和当前运算相关的配置信息。下面列举了 model.ckpt.meta.json 文件中的一些计算节点来更加具体地介绍 graph_def属性。

TensorFlow:实战Google深度学习框架第二版——第五章

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

具体node示例:

TensorFlow:实战Google深度学习框架第二版——第五章

TensorFlow:实战Google深度学习框架第二版——第五章

上面给出了 model.ckpt.meta.json 文件中 graph_def属性里比较有代表性的几个节点。

第一个节点给出的是变量定义的运算。在 TensorFlow 中变量定义也是一个运算,这个运算的名称为 vl (name:”vl”),运算方法的名称为 Variable (op:VariableV2")。定义变量的运算可以有很多个,于是在 NodeDef类型的 node 属性中可以有多个变量定义的节点。但定义变量的运算方法只用到了一个,于是在 MetalnfoDef 类型的 stripped_op _list 属性中只有一个名称为 VariableV2 的运算方法。除了指定计算图中节点的名称和运算方法, NodeDef类型中还定义了运算相关的属性。在节点 vl 中,attr属性指定了这个变量的维度以及类型。

给出的第二个节点是代表加法运算的节点。它指定了 2 个输入, 一个为 vl/read, 另 一个为 v2/read。其中 vl/read 代表的节点可以读取变量 vl 的值。因为 vl 的值是节点 vl/read 的第一个输出,所以后面的:0 就可以省略了。 v2/read 也类似的代表了变量v2的取值。

TensorFlow:实战Google深度学习框架第二版——第五章

最后一个示例节点名称为 save/control_dependency ,该节点是系统在完成 TensorFlow 模型持久化过程中自动生成的一个运算。

3. saver_def属性

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 sharded=5;
    float keep_checkpoint_every_n_hours=6;

    enum CheckpointFormatVersion{
        LEGACY=0;
        V1=1;
        V2=2;
    }
    CheckpointFormatVersion version=7;

}

filename_tensor_name 属性给出了保存文件名的张量名称,这个张量就是节点 save/Const 的第一个输出。save_tensor_ name 属性给出了持久化 TensorFlow 模型的运算所对应的节点名称。从以上文件中可以看出 ,这个节点就是在 graph_def 属性中给出的 save/control_ dependency 节点。 和持久化 TensorFlow 模型运算对应的是加载 TensorFlow 模型的运算,这个运算的名称由 restore_op_ name 属性指定。

max_to_ keep 属性和 keep_ checkpoint_ every_ n_ hours 属性设定了 tf.train. Saver 类清理之前保存的模型的策略。 比如当 max_to_ keep 为 5 的时候, 在第六次调用 saver.save 时,第一次保存的模型就会被自动删除。 通过设置 keep_checkpoint_ every_ n _hours,每n小时可以在 max_to_ keep 的基础上多保存一个模型。

实验结果如下所示

TensorFlow:实战Google深度学习框架第二版——第五章

4. collection_def属性

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

以下代码给出了 CollectionDef类型的定义。

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

    message Int64List{
        repeated int64 value=1 [packed=true];
    }

    message FloatList{
        repeated float value=1 [packed=true];
    }

    message AnyList{
        repeated google.protobuf.Any value=1;
    }

    oneof kind{
        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属性的内容。

TensorFlow:实战Google深度学习框架第二版——第五章

从以上文件可以看出样例程序中维护了两个集合。 一个是所有变量的集合,这个集合的名称为 variables。另外一个是可训练变量的集合,名为 trainable_variables。在样例程序中,这两个集合中的元素是一样的,都是变量 vl 和 v2。它们都是系统自动维护的。更多关于 TensorFlow自动维护的集合的介绍,参考第三章给出的表格。

剩余三个文件

接下来是model.ckpt.index与model.ckpt.data文件

通过对 MetaGraphDef 类型中主要属性的讲解,本节己经介绍了 TensorFlow 模型持久化得到的第一个model.ckpt.meta文件中的内容。除了持久化 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.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])

#获取名称为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_paths=2;
}

model_ checkpoint_path 属性保存了所保存模型中最新的 TensorFlow 模型文件的文件名 。 all_ model_ checkpoint_paths 属性列出了当前还没有被删除的所有 TensorFlow 模型文件的文件名 。 下面给出一个我训练网络时候生成的checkpoint部分内容截图

TensorFlow:实战Google深度学习框架第二版——第五章


5.5 TensorFlow最佳实践样例程序

5.2.1节中已经给出了一个完整的TensorFlow程序来解决MNIST手写体识别问题,但这个程序存在以下问题:

第一,根据5.3节中提到的变量管理问题,原5.2.1代码计算前向传播的函数需要将所有变量都传入,当神经网络的结构更加复杂,参数更多时,程序的可读性会变得很差,并且这种方式导致程序中会有大量冗余代码,降低了编程效率。

第二,根据5.4节中提到的模型持久化问题,原5.2.1代码运行结束退出后,未保存模型,训练好的模型也就无法再次使用,再次训练会浪费大量的时间和资源。

因此,结合5.3的变量管理和5.4的模型持久化,本节将介绍一个TensorFlow训练神经网络模型的最佳实践。将训练和测试分为两个独立的程序,增强了程序使用的灵活性。此外,还将前向传播的过程抽象成一个单独的库函数,这是因为前向传播过程在训练和测试过程中都会使用,在使用更加方便的同时,保证了训练和测试所使用的前向传播方法的一致性。

重构后的代码被拆分成三部分:

第一个是mnist_inference.py,它定义了前向传播的过程以及神经网络的参数。

第二个是mnist_train.py,它定义了神经网络的训练过程。

第三个是mnist_eval.py,它定义了神经网络的测试过程。

代码链接地址:TensorFlow最佳实践样例程序