【从零开始学习Tensorflow】(三)第5章 MNIST数字识别问题

转载请注明作者和出处: https://blog.csdn.net/weixin_37392582
代码平台https://gitee.com/wuweijun
开发平台: Win10 + Python3.6 + Anaconda3
编  者: 无尾


文章性质:【从零开始学习Tensorflow】系列博客为 《TensorFlow+实战Google深度学习框架》一书的学习笔记。



5.1、MNIST数据处理

MNIST数据集,包含60000张图片作为训练数据(28*28),10000张图片作为测试数据,标签集用 ont_hot 编码表示手写数字(例:表示数字3→[0,0,0,1,0,0,0,0,0,0])。

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

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)
print('Validating data size', mnist.validation.num_examples)
print('Testing data size', mnist.test.num_examples)
print('example training data label:',mnist.train.labels[0])

train 集合内有 55000 张图片,validation集合内有 5000 张图片,test集合内有 10000 张图片。每一张图片是一个长度为784的一维数组,对应了图片像素矩阵中的每一个数字(28*28 = 784)。

为了方便使用随机梯度下降, i n p u t _ d a t a . r e a d _ d a t a _ s e t s 函数生成的类还提供了 m n i s t . t r a i n . n e x t _ b a t c h 函数,它可以从所有的训练数据中读取一小部分作为一个训练 b a t c h 。代码如下:

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


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

OUTX shape: (100, 784)
Y shape: (100, 10)

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


5.2.1、TensorFlow 训练神经网络


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

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

Click here to get the code:
【神经网络实现 MNIST 手写体识别】


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


将上一节中的代码做略微调整,使 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))
在 0 次迭代后,验证数据集的正确率为 : 0.093 , 测试数据集的正确率为 : 0.0965
在 1000 次迭代后,验证数据集的正确率为 : 0.9772 , 测试数据集的正确率为 : 0.9764
在 2000 次迭代后,验证数据集的正确率为 : 0.9828 , 测试数据集的正确率为 : 0.9804

CSDN 无尾君


5.2.3、不同模型效果比较


在第4章中,提到了设计神经网络时的 5 种优化方法。在神经网络结构的设计上,需要使用激活函数和多层隐藏层。在神经网络优化时,可以使用指数衰减的学习律、加入正则化的损失函数以及滑动平均模型。在图 5-3 中,给出了在相同神经网络参数下,使用不同优化方法没经过 30000 轮训练迭代后,得到的最终模型的正确率。

这里写图片描述

从图中的反映可以说明,神经网络的结构对最终模型的效果有本质性的影响。

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

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

这里写图片描述


5.3、变量管理

前面,将前向传播过程抽象成了一个函数,在训练和测试的过程中可以统一调用同一个函数来得到模型的前向传播结果。这个函数的定义为:

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

从定义中可以看到,这个函数的参数中包括了神经网络中的所有参数。然而,当网络结构更加复杂、参数更多时,就需要一个更好的方式来传递和管理神经网络中的参数了。TensorFlow 提供了通过变量名称来创建或获取一个变量的机制。通过这个机制,在不同的函数中可以直接通过变量的名字来使用变量,而不需要将变量通过参数的形式到处传递。 TensorFlow 中通过变量名称获取变量的机制主要是通过 t f . g e t _ v a r i a b l e t f . v a r i a b l e _ s c o p e 函数实现的。

t f . g e t _ v a r i a b l e t f . V a r i a b l e 的功能时基本等价的。

v = tf.get_variable("v", shape = [1], initializer = tf.constant_initializer(1.0))
v = tf.Variable(tf.constant(1.0, shape = [1])m name = "v")

两个函数创建变量的过程基本上是一样的。 t f . g e t _ v a r i a b l e 函数调用时提供的维度(shape)信息以及初始化方法(initializer)的参数和 t f . V a r i a b l e 函数调用时提供的初始化过程中的参数也类似。 TensorFlow 中提供的 i n i t i a l i z e r 函数 同 3.4.3 小节中介绍地随机数以及常量生成函数大部分是一一对应的。 比如常数初始化函数 t f . c o n s t a n t i n i t i a l i z e r 和常数生成函数 t f . c o n s t a n t 功能上就是一致地。 TensorFlow 提供了 7 种不同地初始化函数,如下:
这里写图片描述

t f . g e t _ v a r i a b l e 函数与 t f . V a r i a b l e 函数最大的区别在于指定变量名称的参数。对于 t f . V a r i a b l e 函数,变量名称是一个可选的参数,通过 name=”v” 的形式给出。但是对于 t f . g e t _ v a r i a b l e 函数,变量名称是一个必填的参数, t f . g e t _ v a r i a b l e 首先会试图取创建一个名字为 v 的参数,如果创建失败(比如已经有同名的参数),那么这个程序就会报错。这是为了避免无意识的变量复用造成的错误。比如在定义神经网络参数时,第一层网络权重已经叫 weights 了,那么在创建第二层神经网络时,如果参数名仍叫 weights,就会触发变量重用的错误。否则两层神经网络共用一个权重会出现一些比较难以发现的错误。

如果通过 t f . g e t _ v a r i a b l e 获取一个已经创建的变量,需要通过 t f . v a r i a b l e _ s c o p e 来生成一个上下文管理器,并明确指定在这个上下文管理器中, t f . g e t _ v a r i a b l e 将直接获取已经声称的变量。下面给出了一段代码说明如何通过 t f . v a r i a b l e s c o p e 函数来控制 t f . g e t _ v a r i a b l e 函数获取已经创建过的变量。

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 无尾君
"""
import tensorflow as tf

# 在名字为 foo 的命名空间的创建名字为 v 的变量
with tf.variable_scope('foo'):
    v = tf.get_variable("a", [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])

样例简单说明了通过 t f . v a r i a b l e s c o p e 函数可以空值 t f . g e t _ v a r i a b l e 函数的语义。 当参数 reuse 为 False 或 None 时创建上下文管理器,将创建新的变量。如果同名变量已经存在则报错。 t f . v a r i a b l e _ s c o p e 是可以嵌套的。下面程序说明了当 t f . v a r i a b l e _ s c o p e 函数嵌套时, resuse 参数的取值是如何确定的。

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 无尾君
"""
import tensorflow as tf

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):
        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) # 退出 reuse设置为 True的上下文之后, reuse的值又回到了False

t f . v a r i a b l e _ s c o p e 函数声称的上下文管理器也会创建一个 TensorFlow 中的命名空间,在命名空间内创建的变量名称都会带上这个命名空间名作为前缀。所以 t f . v a r i a b l e _ s c o p e 函数除了可以控制 t f . g e t _ v a r i a b l e 执行的功能之外, 也提供了一个管理变量命名空间的方式。代码如下,

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 无尾君
"""
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

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)

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.variable_scope 和 tf.get_variable 函数,以下代码对 5.2.1 小节中定义的计算前向传播结果的函数做了一些改进。

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 无尾君
"""
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.2.1小节中给出的样例代码在训练完成之后就直接退出了,并没有将训练得到的模型保存下来方便下次直接使用。为了让训练结果可以复用,需要将训练得到的神经网络模型持久化。


5.4.1 持久化代码实现


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

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 无尾君
"""
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')

#声明 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文件夹,否则会报错

这里写图片描述

在指定的保存路径下,生成了四个文件(书上说3个)。这是因为 TensorFlow 会将计算图的结构和图上参数取值分开保存。

第一个文件为 5.4.1_model.ckpt.meta,它保存了 TensorFlow 计算图的结构,即神经网络的网络结构。
第二个文件为 5.4.1_model.ckpt.index,I DON’T KNOW
第三个文件为 5.4.1_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)) 

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

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 12 16:28:38 2018

@author: 无尾君
"""

# 直接加载持久化的图
import tensorflow as tf
saver = tf.train.import_meta_graph('./path/to/model/model.ckpt.meta')
with tf.Session() as sess:
    saver.restore(sess,r'path/to/model/model.ckpt')
    print(sess.run(tf.get_default_graph().get_tensor_by_name('add:0')))

OUT:[3.]

上面的程序中,默认保存和加载了 TensorFlow 计算图上定义的全部变量。但是有时可能只需要保存或者加载部分变量。比如,可能有一个之前训练好的五层神经网络模型,但现在想尝试一个六层的神经网络,那么可以将前面五层神经网络中的参数直接加在到新的模型,而仅仅将最后一层神经网络重新训练。

为了保存或者加载部分变量,再声明 tf.train.Saver 类时可以提供一个列表来指定需要保存或者加载的变量。比如在加载模型的代码中使用 saver = tf.train.Saver([v1]) 命令来构建 tf.train.Saver 类,那么只有变量 v1 会被加载进来。如果运行修改后只加载了 v1 的代码会得到变量未初始化的错误,因为 v2 没有被加载,所以 v2 在运行初始化之前是没有值的。除了可以选取需要被加载的变量, tf.train.Saver 类也支持在保存或者加载时给变量重命名。

# 变量重命名
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name = 'other-v1')
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name = 'other-v2')
saver = tf.train.Saver({'v1':v1, 'v2':v2})

这个程序中,如果直接通过 tf.train.Saver 默认的构造函数来加载保存的模型,那么程序会报找不到的错误。因为保存时变量的名称和加载时变量的名称不一致。为了解决这个问题,可以通过字典(dictionary)将模型保存时的变量名和需要加在的变量联系起来。

这样做的主要目的之一是方便使用变量的滑动平均值。在 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
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)]))

这里写图片描述

以下代码给出了如何通过变量重命名直接读取变量的滑动平均值。


5.4.2、持久化原理及数据格式


当调用了 saver.save 函数时, TensorFlow 程序会自动生成几个文件。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;
    }

从上面的代码中可以看到,元图中记录了 5 类信息。保存 MetaGraphDef 信息的文件默认以 .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('/path/to/model.ckpt,meda.json', as_text=True)

这里写图片描述

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

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

…………………………
…………省略…………
…………………………

猜你喜欢

转载自blog.csdn.net/weixin_37392582/article/details/79869973
今日推荐