《TensorFlow:实战Google深度学习框架(第二版)》笔记【7-12章】

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiang_freedom/article/details/81949920

第七章 图像数据处理

本章介绍如何对图像进行预处理使得模型尽可能不被无关因素(亮度、对比度等)所影响,同时使用多线程处理提高效率。

TFRecord输入数据格式

tf提供了一种统一的格式存储数据,TFRecord。第六章中花朵分类使用了字典存储数据,这种方式扩展性很差。TFRecord可以有效地记录来源更加复杂、信息更加丰富的数据。

TFRecord的数据都是通过tf.train.Example Protocol Buffer存储的。tf.train.Example定义:

tf.train.Example包含了一个从属性名称到取值的字典,取值可以为字符串(BytesList)、实数(FloatList)或者整数(Int64List)列表。

将MNIST数据转化为TFRecord格式:

当数据量较大时,也可以将数据写入多个TFRecord文件。

读取TFRecord文件:

图像数据处理

通过图像的预处理,可以尽量避免模型受到无关因素的影响。在大部分图像识别问题中,可以提高模型的准确率。

Tensorflow图像处理函数

图像编码处理
图像在存储时是记录压缩编码之后的结果。要将一张图像还原成一个三维矩阵,需要解码。tf提供了对jpeg和png图像的编码、解码函数。

图像大小调整
一般来说,图像大小是不固定的,但神经网络输入节点的个数是固定的。所以需要将图像的大小统一。图像大小调整第一种方式是通过算法使得新的图像尽量保存原始信息。tf提供了四种方法,封装到了tf.image.resize_images函数。

四种不同算法以及各自效果:

第二种方式是对图像进行裁剪或者填充,tf.image.resize_image_with_crop_or_pad。

tf也支持通过比例调整图像大小

上面的函数都是截取或者填充图像中间的部分。tf还提供了tf.image.crop_to_bounding_box和tf.image.pad_to_bounding_box来裁剪或填充给定区域的图像。这两个函数都要求给出的尺寸满足一定要求,否则会报错。

图像翻转
tf提供了一些函数来支持对图像的翻转。

随机翻转:

图像色彩调整
亮度、对比度、饱和度和色相等。

图像标准化:

处理标注框
tf.image.draw_bounding_boxes在图像中加入标注框。

随机截取图像上有信息量的部分也是提高模型鲁棒性的方式,使得模型不受被识别物体大小的影响。

图像预处理完整样例

以下tf程序完成了从图像片段截取、大小调整再到翻转和色彩调整的整个图像预处理过程。

这样可以通过一张训练图像衍生出很多训练样本。训练得到的神经网络模型可以识别不同大小、方位、色彩等方面的实体。

多线程输入数据处理框架

复杂的预处理过程会减慢整个训练过程,为了避免图像预处理称为神经网络模型训练效率的瓶颈,tf提供了一套多线程处理输入数据的框架。

主要内容:

  • 队列:Tensorflow多线程输入数据处理框架的基础
  • tf.train.string_input_producer:管理原始输入文件列表
  • tf.train.shuffle_batch_join和tf.train.shuffle_batch

队列与多线程

在tf中,队列和变量类似,都是计算图上有状态的节点。其他的计算节点可以修改它们的状态。变量的修改是赋值操作,队列的修改有Enqueue、EnqueueMany和Dequeue。

tf提供了FIFOQueue和RandomShuffleQueue。RandomShuffleQueue会将队列中的元素打乱,每次出队列操作得到的是随机选择的一个。

在tf中,队列不仅仅是一种数据结构,还是异步计算张量取值的一个重要机制。比如多线程读写元素。
tf提供了tf.Coordinator和tf.QueueRunner两个类完成多线程协同的功能。tf.Coordinator用于协同多个线程一起停止,提供了should_stop、request_stop和join三个函数。启动线程之前,需要先声明一个tf.Coordinator类,并将这个类传入每一个线程中。启动的线程需要一直查询tf.Coordinator提供的should_stop函数,当函数返回值为True时,当前线程也需要退出。每一个启动的线程都可以调用request_stop函数通知其他线程退出(将should_stop设为True)。

结果:

tf.QueueRunner主要用于启动多个线程操作同一个队列。启动的这些线程可以通过上面的tf.Coordinator统一管理。

输入文件队列

本小节介绍如何使用tf中的队列管理输入文件列表。假设所有的输入数据都整理成了TFRecord格式。当训练数据量较大时,可以将数据分成多个TFRecord文件来提高处理效率。tf提供了tf.train.match_filenames_once函数来获取符合一个正则表达式的所有文件,得到的文件列表通过tf.train.string_input_producer函数进行有效的管理。

tf.train.string_input_producer会使用初始化时提供的文件列表创建一个输入队列。文件读取函数如果没有或者打开的文件已经读完,就会从输入队列中出队一个文件。设置shuffle为True时,tf.train.string_input_producer随机打乱文件顺序。随机的操作跑在一个单独的线程中,这样不会影响获取文件的速度。tf.train.string_input_producer生成的输入队列可以同时被多个文件读取线程操作,而且会把队列中的文件均匀地分给不同的线程,不出现有些文件被处理过多次而有些文件还没有被处理过的情况。

当一个输入队列中所有文件都被处理完后,它会将初始化的文件列表重新全部加入队列。tf.train.string_input_producer可以设置num_epochs限制最大轮数,超过这个轮数会报错。比如测试模型时,测试数据只需要使用一次,num_epochs设置为1。
简单的生成样例数据程序:

生成两个文件,每个文件中存储两个样例。
tf.train.match_filenames_once函数和tf.train.string_input_producer函数操作:

输出:

依次读出每一个样例。当所有样例读完之后,程序自动从头开始。如果设置num_epochs为1,程序报错:

组合训练数据(batching)

上节介绍了从文件列表中读取单个样例,将样例通过预处理之后,就可以提供给神经网络输入层。将多个样例组织成一个batch可以提高训练效率,所以预处理之后,还需要组织成batch,在提供给输入层。tf提供了tf.train.batch和tf.train.shuffle_batch函数将单个的样例组织成batch的形式输出。这两个函数都会生成一个队列,入队操作是单个样例的生成方法(数据读取和预处理),出队的是一个batch的样例。唯一的区别是是否将数据顺序打乱。

tf.train.shuffle_batch函数

tf.train.batch和tf.train.shuffle_batch函数也提供了并行化处理输入数据的方法,两个函数的并行化方式一致。通过设置num_threads参数,可以指定多个线程同时执行入队操作,即多个线程同时读取一个文件中的不同样例并预处理。上小节介绍的多线程读取输入文件队列中的不同文件可以使用tf.train.batch_join或者tf.train.shuffle_batch_join函数。

tf.train.shuffle_batch和tf.train.shuffle_batch_join函数都可以完成多线程并行方式进行数据预处理,且各有优劣。tf.train.shuffle_batch是不同线程读取同一个文件,如果一个文件中的样例比较相似(比如都属于同一个类别),那么神经网络的训练效果有可能受到影响,所以使用tf.train.shuffle_batch时,尽量将TFRecord文件中的样例随机打乱。使用tf.train.shuffle_batch_join时,不同线程读取不同文件。如果线程数多于文件数,那么多个线程可能读取同一个文件中相近部分的数据,而且多个线程读取多个文件可能大致过多的硬盘寻址,从而使得效率降低。

输入数据处理框架

下图展示了以上代码中输入数据处理的整个流程。

可以归纳为:

  1. 获取存储训练数据的文件列表{A,B,C}
  2. 通过tf.train.string_input_producer创建输入队列,可以选择随机打乱和读取轮数。tf.train.shuffle_batch多线程读取一个文件中的样例,并预处理,得到组合队列,从组合队列中得到batch数据。

通过这个框架,可以有效提高数据预处理的效率,避免数据预处理称为神经网络训练中的性能瓶颈。

数据集

除队列以外,tf还提供了一套更高层的数据处理框架。新的框架中,每一个数据来源被抽象成一个“数据集”,开发者可以以数据集为基本对象,方便地进行batching、随机打乱(shuffle)等操作。从1.3版本起,tf正式推荐使用数据集作为输入数据的首选框架。

数据集的基本使用方法

每一个数据集代表一个数据来源:数据可能来自一个张量,一个TFRecord文件,一个文本文件或者经过sharding的一系列文件,等等。由于训练数据通常无法全部写入内存,从数据集中读取数据需要迭代器(iterator)顺序读取,类似于队列的dequeue()和Reader的read()操作。和队列一样,数据集也是计算图上的一个节点。

# 从数组创建数据集
input_data = [1, 2, 3, 5, 8]
dataset = tf.data.Dataset.from_tensor_slices(input_data)

# 定义迭代器。
iterator = dataset.make_one_shot_iterator()

# get_next() 返回代表一个输入数据的张量。
x = iterator.get_next()
y = x * x

with tf.Session() as sess:
    for i in range(len(input_data)):
        print(sess.run(y))
1
4
9
25
64

可以看到,利用数据集读取数据有三个基本步骤:

  1. 定义数据集的构造方法
    这个例子中使用了tf.data.Dataset.from_tensor_slices,表明数据集是从一个张量中构建的。如果数据集是从文件中构建的,则需要调用不同的构造方法。
  2. 定义遍历器
    这里使用的是make_one_shot_iterator,还有更灵活的initializable_iterator 。
  3. 使用get_next()从遍历器中读取数据张量
# 创建文本文件作为本例的输入。
with open("./test1.txt", "w") as file:
    file.write("File1, line1.\n") 
    file.write("File1, line2.\n")
with open("./test2.txt", "w") as file:
    file.write("File2, line1.\n") 
    file.write("File2, line2.\n")

# 从文本文件创建数据集。这里可以提供多个文件。
input_files = ["./test1.txt", "./test2.txt"]
dataset = tf.data.TextLineDataset(input_files)

# 定义迭代器。
iterator = dataset.make_one_shot_iterator()

# 这里get_next()返回一个字符串类型的张量,代表文件中的一行。
x = iterator.get_next()  
with tf.Session() as sess:
    for i in range(4):
        print(sess.run(x))
File1, line1.
File1, line2.
File2, line1.
File2, line2.
# 解析一个TFRecord的方法。
def parser(record):
    features = tf.parse_single_example(
        record,
        features={
            'image_raw':tf.FixedLenFeature([],tf.string),
            'pixels':tf.FixedLenFeature([],tf.int64),
            'label':tf.FixedLenFeature([],tf.int64)
        })
    decoded_images = tf.decode_raw(features['image_raw'],tf.uint8)
    retyped_images = tf.cast(decoded_images, tf.float32)
    images = tf.reshape(retyped_images, [784])
    labels = tf.cast(features['label'],tf.int32)
    #pixels = tf.cast(features['pixels'],tf.int32)
    return images, labels

# 从TFRecord文件创建数据集。这里可以提供多个文件。
input_files = ["output.tfrecords"]
dataset = tf.data.TFRecordDataset(input_files)

# map()函数表示对数据集中的每一条数据进行调用解析方法。
dataset = dataset.map(parser)

# 定义遍历数据集的迭代器。
iterator = dataset.make_one_shot_iterator()

# 读取数据,可用于进一步计算
image, label = iterator.get_next()

with tf.Session() as sess:
    for i in range(10):
        x, y = sess.run([image, label]) 
        print(y)
7
3
4
6
1
8
1
0
9
8

以上例子中都使用了最简单的make_one_shot_iterator遍历数据集。如果需要用placeholder初始化数据集,需要用到initializable_iterator。

# 从TFRecord文件创建数据集,具体文件路径是一个placeholder,稍后再提供具体路径。
input_files = tf.placeholder(tf.string)
dataset = tf.data.TFRecordDataset(input_files)
dataset = dataset.map(parser)

# 定义遍历dataset的initializable_iterator。
iterator = dataset.make_initializable_iterator()
image, label = iterator.get_next()

with tf.Session() as sess:
    # 首先初始化iterator,并给出input_files的值。
    sess.run(iterator.initializer,
             feed_dict={input_files: ["output.tfrecords"]})
    # 遍历所有数据一个epoch。当遍历结束时,程序会抛出OutOfRangeError。
    while True:
        try:
            x, y = sess.run([image, label])
        except tf.errors.OutOfRangeError:
            break

注意的是,上面的循环体不是指定循环10次sess.run,而是使用while(True)try-except的形式来将所有数据遍历一遍(即一个epoch),因为在动态指定输入数据时,不同数据来源的数据量大小难以预知,而这个方法我们不必提前知道数据量的精确大小。

除这两种之外,tf还提供了reinitializable_iterator和feedable_iterator两种更灵活的迭代器。前者可以多次initialize用于遍历不同的数据来源,后者可以用feed_dict方式动态指定运行那个iterator。

数据集的高层操作

介绍数据集框架提供的一些方便实用的高层API。

dataset = dataset.map(parser)

map是在数据集上操作的常用方法,这个方法表示对每一条数据调用parser方法。对每一条数据进行处理后,map将处理后的数据包装成一个新的数据集返回。map非常灵活,可以对数据进行任何预处理、例如使用上一节的preprocess_for_train方法:

dataset=daaset.map(
	lambda x: preprocess_for_train(x,image_size,image_size,None))

lambda表达式的作用是将原来有4个参数的函数转化为只有1个参数的函数。preprocess for train 函数的第一个参数decoded_image 变成了 lambda 表达式中的 x ,这个参数就是原来函数中的参数 decoded_ image.preprocess for train 函数中后3个参数都被换成了具体的数值。注意这里的 image_size 是一个变量,有具体取值,该值需要在程序的上文中给出。

在返回的新的数据集上,可以直接继续调用其他高层操作,比如预处理、shuffle、batch等操作。相比上一节的方式,在队列和张量间来回操作,更加干净简洁。

dataset=dataset.shuffle(buffer_size)#随机打乱顺序
dataset=dataset.batch(batch_size)#将数据组合成batch

其中buffer_size相当于tf. train.shuffle_ batch 的 min_ after_ dequeue 参数。shuffle 算法在内部使用一个缓冲区中保存buffer_size条数据,每读入一条新数据时,从这个缓冲区中随机选择一条数据进行输出。缓冲区的大小越大,随机的性能越好,但占用的内存也越多 。

repeat 是另一个常用的操作方法 。 这个方法将数据集中的数据复制多份,其中每一份数据被称为一个epoch 。

dataset= dataset.repeat(N)#将数据集重复N份 。

如果数据集在 repeat前己经进行了shuffle操作,输出的每个 epoch 中随机 shuffle 的结果并不会相同 。repeat 和 map 、 shuffle 、batch 等操作一样,都只是计算图中的一个计算节点 。repeat 只代表重复相同的处理过程,并不会记录前一个epoch的处理结果。

除此之外,concatenate()将两个数据集顺序连接起来, take(N)从数据集中读取前 N 项数据, skip(N)在数据集中跳过前N项数据,flap_map()从多个数据集中轮流读取数据,等等,可以查询TensorFlow相关文档 。

使用数据集实现上小节的数据输入流程,与上小节类似,从文件中读取原始数据,进行预处理、shuffle 、batching等操作,并通过 repeat 方法训练多个 epoch 。不同的是,以下例子在训练数据集之外,还另外读取了测试数据集 ,并对测试集和数据集进行了略微不同的预处 理。在训练时,调用上小节中的 preprocess for train 方法对图像进行随机反转等预处理操作:而在测试时,测试数据以原本的样子直接输入测试。

第八章 循环神经网络

todo

第九章 自然语言处理

todo

第十章 Tensorflow高层封装

todo

第十一章 TensorBoard可视化

训练神经网络十分复杂,为了更好的管理、调试和优化神经网络的训练过程,tf提供了一个可视化工具Tensorboard(以下简称tb)。tb可以有效地展示tf在运行过程中的计算图、指标变化趋势以及图像信息等。

Tensorboard简介

Tensorboard是tf的可视化工具,它可以通过tf程序运行过程中输出的日志文件可视化tf程序的运行状态。tb和tf跑在不同的进程中,tb会自动读取最新的tf日志文件。

# 运行 TensorBoard ,并将日志的地址指向上面程序日志输出的地址 。
tensorboard --logdir=/path/to/log

浏览器打开localhost:6006,可以看到

界面上方的“GRAPHS”表示图中内容是tf 的计算图。右上方有一个“INACTIVE” 选项,列出的是当前没有可视化数据的项目:SCALARS、IMAGES、AUDIO、DISTRIBUTIONS、HISTOGRAMS、PROJECTOR、TEXT和PROFILE。

PROFILE目前仅供内部使用,不做介绍

Tensorflow计算图可视化

本节利用命名空间整理tb计算图的结构,同时详细介绍tb展示的其他信息。

命名空间与Tensorboard图上节点

当神经网络模型的结构更加复杂、运算更多时,计算图会很复杂,没有经过整理的可视化效果图无法很好地表示模型结构。
tb支持通过tf命名空间整理可视化效果图上的节点。tb视图中,tf计算图中同一个命名空间下的所有节点会被缩略成一个节点,只有顶层命名空间中的节点才会被显示。第五章中介绍过变量的命名空间以及tf.variable _scope 函数。除了 tf.variable_scope 函数, tf.name_scope 函数也提供了命名 空间管理 的功能 。这两个函数在大部分情况下是等价的 ,唯 一 的区别是在使用 tf.get_variable 函数时 。

改进版:

import tensorflow as tf

with tf.name_scope("input1"):
    input1=tf.constant([1.0,2.0,3.0],name="input1")
with tf.name_scope("input2"):

    input2=tf.Variable(tf.random_uniform([3]),name="input2")
output=tf.add_n([input1,input2],name="add")

writer=tf.summary.FileWriter("log/",tf.get_default_graph())
writer.close()

可视化效果图:

可见初始化节点被缩略起来了。双击input2或者点击input2节点右上角的+号,可以展开input2节点的视图。

下面给出一个样例程序来展示如何很好地可视化一个真实的神经网络结构图。

相比第五章的程序,最大的改变是将计算放到了tf.name_scope函数生成的上下文管理器中。因为mnist_inference.py 程序中已经使用了 tf. variable_ scope 来 管理变量的命名空间,所以这里不需要再做调整。
效果图:

input 节点代表了训练神经网络需要的输入数据,这些输入数据会提供给神经网络的第 一层 layer1 。然后layer1的结果会被传到第 二层 layer2 ,进过 layer2的计算得到前向传播的结果。 loss function 节点表示计算损失函数的过程,这个过程既依赖于前向传播的结果来计算交叉熵( layer2 到 loss_function 的边),又依赖于每一层中所定义的变量来计算 L2 正则化损失( layer1和 layer2 到 loss_function 的边) 。 lo ss_function 的计算结果会提供给神经网络的优化过程,也就是图中train_step 所代表的节点。

图上节点之间有两种不同的边,实线的边刻画了数据传输,箭头表达了数据传输的方向。边上还标注了张量的维度信息,比如input和layer1之间传输张量的维度为?*784,说明训练时提供的batch大小不固定(定义时为None),输入层节点784个。当两个节点之间传输的张量多于1时,图上将只显示张量的个数。边的粗细表示的是两个节点之间传输的张量维度的总大小,而不是张量个数。张量维度无法确定时,tb用最细的边表示,比如layer1到layer2的边。
另外一种边是虚线,比如moving_ average 和 train_step 之 间的 边。表达了计算之间的依赖关系,在程序 中,通过 tf.control_dependencies 函数指定了更新参数滑动平均 值 的操作和通过反 向 传播更新变量的操作需要同时进行 , 于是 moving_average 与 train_step 之 间 存在 一条虚边 。

tf中部分计算节点会有比较多的依赖关系,如果全部画在一张图上会使可视化得到的效果图非常拥挤。于是tb将tf计算图分成了主图(Main Graph)和辅助图(Auxiliary nodes)两个部分。tb会自动将连接比较多的节点放在辅助图中,使得主图结构更加清晰。除此之外,也可以手动调整将节点加入或者移除主图,右键单击节点或者点击信息框下部的选项即可。

节点信息

除了展示tf计算图的结构,tb还可以展示每个节点的基本信息以及运行时消耗的时间和空间。加入以下代码统计节点的消耗时间和内存:

点击页面左侧的Session runs会出现所有通过train_writer.add_run_metadata函数记录的运行数据。选择一个记录后,Color栏中Compute time 和 Memory 这两个选项将可以被选择。

选择Compute time可以看到每个节点的运行时间,颜色越深表示消耗越大。Memory类似。

性能调优时,一般选择迭代轮数较大的数据,这样可以减少tf初始化对性能的影响。

Color栏还有Structure和Device两个选项。默认的是structure视图,灰色的节点表示没有其他节点和它拥有相同的结构。如果两个以上节点结构相同,那么它们会被涂上相同的颜色。Device选项会根据运行的机器类型给节点染色。

点击节点时,界面的右上角会弹出一个信息卡片显示这个节点的基本信息。当节点为命名空间时,tb展示空间内所有计算节点的输入、输出和依赖关系以及消耗时间内存(若有session runs选中)等。节点为tf计算节点时,tb还会展示属性信息。

监控指标可视化

除了 GRAPHS 以外, Tensor Board界面中还提供了 SCALARS 、 IMAGES 、 AUDIO 、 DISTRIBUTIONS 、HISTOGRAMS 和 TEXT六个界面来可视化其他的监控指标 。以下程序展示了如何将 TensorFlow 程序运行时的信息
输出到 TensorBoard 日 志文件中。因为需要在变量定义时加上日志输出,所以这里先不共用5.5 节中的 mnist_inference.py。

每个指标对应一个日志生成函数:

运行程序得到tb图:

这是生成的所有标量监控信息。在训练神经网络时,通过tb监控神经网络中变量取值的变化、模型在训练batch上的损失函数大小以及学习率的变化等信息可以更加方便的掌握模型的训练情况。

这是tb可视化当前轮训练使用的图像信息,可以大致看出数据随机打乱的效果。因为tf程序和tb程序可以同时运行,所以从tb上可以实时看到tf程序中最新使用的训练或者测试图像。

DISTRIBUTIONS提供了对张量取值分布的可视化。

HISTOGRAMS视图更加清晰地展示参数取值分布和训练迭代轮数之间的关系。与DISTRIBUTIONS不同,HISTOGRAMS中不同轮数中参数的取值是通过不同的平面的表示的。颜色越深的平面表示迭代轮数越小的取值分布。比如layer1/biases/summaries/layer1/biases,最上面比较尖的平面表示训练一轮之后的bias分布,集中于0附近。颜色较浅的平面是迭代轮数较大时的分布,可以看到越来越接近平均分布。

HISTOGRAMS左侧有一个OVERLAY选项,可以看到下图的效果。和默认的OFFSET视图一样,颜色越深表示迭代轮数越小。但是下图中比较尖的曲线看上去颜色比较浅,因为更多曲线靠近平均分布,所以合在一起反而颜色更深了。鼠标移到一条曲线上时会显示迭代轮数信息。

高维向量可视化

第六章迁移学习时介绍过,在ImageNet训练好的CNN的卷积层可以被看成是对图片进行特征提取的过程,这个特征提取的结果在没有可视化的情况下是不容易被直观判断的。tb提供了PROJECTOR可视化高维向量之间的关系。比如图像迁移学习中经过特征提取得到的瓶颈层就是多个高维向量,如果数据集上同一种类的图片的高维向量在空间上比较接近,那么迁移学习的效果会更好。类似的,训练单词向量时,语义相近的单词所对应的向量在空间上距离接近的话,自然语言模型的效果也可能更好。

这里给出一个样例程序,在MNIST上训练一个简单的全连接神经网络。本节真实训练100轮和10000轮后,测试数据经过整个神经网络得到的输出层向量通过PROJECTOR得到的可视化结果。为了在PROJECTOR中更好地展示MNIST图片和标签信息,PROJECTOR要求提供一个sprite图像和一个tsv文件。以下代码使用MNIST测试数据生成这两个文件。

得到sprite图像包含了所有的MNIST测试图像:

另一个mnist_meta.tsv,前几行如下,第二行开始每一行代表图片编号以及对应的标签(数字)。

Index Label
0 7
1 2
2 2
3 0
4 4
...

以下代码使用tf代码生成PROJECTOR所需要的日志文件:

效果图:

这是10000轮后的分类效果,可以看到不同颜色的图片的区分度还是挺大的。PROJECTOR左上角三个选项,第一个FINAL_LOGITS是选择需要可视化的Tensor,这里默认选择的是通过ProjectorConfig指定的tensor_name,也就是名为FINAL_LOGITS的张量。另外选项虽然也可以可视化,但这里意义不大。Label by选项控制鼠标移到一个向量上时鼠标附近显示的是index还是label。Color by控制每一个小图片的背景颜色,根据标签分类还是编号分类。

左下角提供了不同的高维向量的可视化方法,目前主要是T-SNE和PCA。这两个都可以将一个高维向量转化成一个低维向量并尽量保证转化后信息不受影响。右侧提供了搜索和高亮功能,下图展示了搜索5的图片。

可以看到大部分的图片都集中在一个比较小的区域,只有少数比较远。通过这种方式可以很快的找到每个类别中比较难分的图片,加速错误案例分析的过程。

第十二章 TensorFlow计算加速

本章介绍如何将TensorFlow利用GPU以及分布式计算进行模型训练。包括tf.Session的一些常用参数、多GPU训练、分布式训练等。

TensorFlow使用GPU

tf程序可以通过tf.device函数来指定运行每一个操作的设备。可以是本地的CPU、GPU,也可以是远程的服务器。tf会给每一个可用的设备一个名称,tf.device通过设备名称指定执行的设备。比如CPU的名称为/cpu:0。即使有多台CPU,名称都是/cpu:0。而不同GPU的名称是不同的,第n个GPU在tf的名称为/gpu:n。

在生成会话时,设置log_device_placement参数打印运行每一个运算的设备。

配置好GPU的tf中,优先选择GPU。

如果有多个GPU,且需要将运算放到不同GPU上,需要手工指定:

不是所有操作都能放到GPU上,强行设置会报错:

不同版本tf对GPU的支持不同,如果程序中全部强制指定会降低程序的可移植性。tf的kernel中定义了哪些操作可以跑在GPU上,比如在variable_ops.cc程序中找到以下定义:

可以看到GPU只在部分数据类型上支持tf.Variable操作,在tf代码库中搜索TF_CALL_GPU_NUMBER_TYPES,可以发现GPU上tf.Variable只支持实数型(float16,float32和double)参数。为了避免这个问题,生成会话时可以指定allow_soft_placement参数,如果运算无法再GPU上执行,那么tf会自动将它放到CPU上。

虽然GPU可以加速tf计算,但一般不会把所有的操作全部放在GPU上。一个比较好的实践是将计算密集型的运算放在GPU上,其他放在CPU上。GPU是相对独立的资源,计算和数据传输到GPU需要额外的时间。

深度学习训练并行模式

多个GPU训练模型,需要了解如何并行化地训练模型。常用的训练方式有两种:同步模式和异步模式。
首先回顾下如何训练模型:

异步模式:

每一轮迭代时,不同设备会读取参数最新的取值,但因为不同设备读取时间不同,得到的值可能也不同。这样可能导致一个问题,如下图所示

t0时刻损失函数在黑色小球处,假设两个设备d0和d1在时间t0同时读取参数取值,它们计算出来的梯度都是向左移动。假设t1时d0已经完成了参数更新,修改后位于小灰球的位置,而d1并不知道参数已经更新,在t2时,d1会继续将小球向左移动达到小白球的地方,从而错过最优点。

为了避免更新不同步的问题,可以使用同步模式。

所有的设备同时读取参数的取值,并且反向传播算法完成之后再统一更新参数。每一轮迭代时,不同设备首先统一读取当前参数的取值,随机获取小部分数据运行反向传播过程得到梯度,所有设备计算完成之后,计算出不同设备上梯度的平均值,再用平均值更新参数。

tf也支持灵活的同步更新方式使计算不会因为设备故障而被卡主,而且同步模式下,tf会保证没有设备能使用陈旧的梯度更新参数。

同步模式解决了异步模式中存在的更新问题,可是效率较低。虽然理论上异步模式存在缺陷,但训练深度学习模型使用的随机梯度下降本来就是梯度下降的近似解,而且即使是梯度下降也无法保证达到全局最优。实际应用汇总,异步模式不一定比同步模式差。所以两种模式都有广泛的应用。

多GPU并行

本节改造第五章的MNIST程序:多GPU版本。一般来说一台机器上的多个GPU性能相似,所以更多地采用同步模式训练。

下图展示了tb可视化的计算图,不同颜色代表了不同设备,可以看出训练主要过程放到了GPU_0,GPU_1,GPU_2,GPU_3四个模块中。对比计算图和上小节同步模式的流程图非常接近。

调整参数N_GPU,可以试验同步模式下GPU个数的增加训练速度的加速比率:

分布式TensorFlow

一台机器上安装的GPU有限,要进一步提升深度学习模型的训练速度,需要将tf分布式运行在多台机器上。

分布式TensorFlow原理

创建一个最简单的tf集群:

import tensorflow as tf

# 创建一个本地集群。
c = tf.constant("Hello, distributed TensorFlow!")
server = tf.train.Server.create_local_server()
sess = tf.Session(server.target)
print sess.run(c)

tf.train.Server.create_local_server()在本地建立了只有一台机器的tf集群,然后在集群上生成会话,会话上的运算运行在集群上。这是一个单机集群,但大致反映了tf集群的工作流程。tf集群通过一系列的任务(tasks)执行计算图中的运算。一般来说,不同任务跑在不同机器上,有多个GPU时,不同任务也可以使用同一台机器上的不同GPU。任务也会被聚合成工作(jobs)。比如一台运行反向传播的机器是一个任务,所有运行反向传播的机器的集合是一个工作。

当一个集群有多个任务时,需要使用tf.train.ClusterSpec指定运行每一个任务的机器。
第一个任务的代码:

# 创建两个集群
c = tf.constant("Hello from server1!")
cluster = tf.train.ClusterSpec({"local": ["localhost:2222", "localhost:2223"]})
server = tf.train.Server(cluster, job_name="local", task_index=0)
sess = tf.Session(server.target, config=tf.ConfigProto(log_device_placement=True)) 
print sess.run(c)
server.join()

第二个任务的代码:

import tensorflow as tf
c = tf.constant("Hello from server2!")
cluster = tf.train.ClusterSpec({"local": ["localhost:2222", "localhost:2223"]})
server = tf.train.Server(cluster, job_name="local", task_index=1)
sess = tf.Session(server.target, config=tf.ConfigProto(log_device_placement=True)) 
print sess.run(c)
server.join()

启动第一个任务,输出:

持续输出CreateSession still waiting for response from worker:/job:local/replica:0/task: 1,因为程序在等待第二个任务启动。第二个任务启动后,第一个任务会输出Hello from server1!。第二个任务输出:

值得注意的是第二个任务中的计算也被放在了job:local/replica:0/task: 0/cpu:0上,也就是由第一个任务来执行。所以可以看出tf.train.Server.target生成的会话可以统一管理整个tf集群的资源。
和使用多GPU类似,tf支持通过tf.device指定操作运行在哪个任务上。

with tf.device ( "/job:local/task:1" ):
	c=tf.constant ( "Hello from server2!")

在训练模型时,一般会定义两个工作。一个工作专门负责存储、获取以及更新变量的值,这个工作称为参数服务器(parameter server,ps)。另外一个工作负责运行反向传播算法获取梯度,称为计算服务器(worker)。

比较常见的tf集群配置方法(tf-worker(i)和tf-ps(i)都是服务器地址):

分布式tf训练模型有两种方式:

  • 计算图内分布式(in-graph replication):所有任务使用一个计算图的变量(参数),只是将计算部分发布到不同计算服务器上。比如上小节的多GPU程序。因为参数都是在同一个计算图中,所以同步更新参数比较容易控制。但计算图内分布式需要有一个中心节点来生成计算图并分配计算任务,当数据量较大时,中心节点容易造成性能瓶颈。
  • 计算图之间分布式(between-graph replication):每一个计算服务器都会创建一个独立的计算图,但不同计算图中的相同参数需要以一种固定的方式放到同一个参数服务器上。tf提供了tf.train.replica device setter 函数帮助完成这一过程。同时还提供了tf.train.SyncRepIicasOptimizer 函数来帮助实现参数的同步更新 。这让计算图之间分布式方式被更加广泛地使用。

分布式Tensorflow模型训练

分别实现计算图之间分布式完成分布式深度学习模型训练的异步更新和同步更新。先介绍异步更新,同步更新的代码大部分相似,只需要关注其不同点。

异步模式
改写第五章的MNIST代码,复用mnist_inference.py程序。

# -*- coding: utf-8 -*-

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

import mnist_inference

# 配置神经网络的参数。
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.01
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 20000
MOVING_AVERAGE_DECAY = 0.99

# 模型保存的路径。
MODEL_SAVE_PATH = "logs/log_async"
# MNIST数据路径。
DATA_PATH = "../../datasets/MNIST_data"

# 通过flags指定运行的参数。在12.4.1小节中对于不同的任务(task)给出了不同的程序,
# 但这不是一种可扩展的方式。在这一小节中将使用运行程序时给出的参数来配置在不同
# 任务中运行的程序。
FLAGS = tf.app.flags.FLAGS

# 指定当前运行的是参数服务器还是计算服务器。参数服务器只负责TensorFlow中变量的维护
# 和管理,计算服务器负责每一轮迭代时运行反向传播过程。
tf.app.flags.DEFINE_string('job_name', 'worker', ' "ps" or "worker" ')
# 指定集群中的参数服务器地址。
tf.app.flags.DEFINE_string(
    'ps_hosts', ' tf-ps0:2222,tf-ps1:1111',
    'Comma-separated list of hostname:port for the parameter server jobs. e.g. "tf-ps0:2222,tf-ps1:1111" ')
# 指定集群中的计算服务器地址。
tf.app.flags.DEFINE_string(
    'worker_hosts', ' tf-worker0:2222,tf-worker1:1111',
    'Comma-separated list of hostname:port for the worker jobs. e.g. "tf-worker0:2222,tf-worker1:1111" ')
# 指定当前程序的任务ID。TensorFlow会自动根据参数服务器/计算服务器列表中的端口号
# 来启动服务。注意参数服务器和计算服务器的编号都是从0开始的。
tf.app.flags.DEFINE_integer('task_id', 0, 'Task ID of the worker/replica running the training.')

# 定义TensorFlow的计算图,并返回每一轮迭代时需要运行的操作。这个过程和5.5节中的主
# 函数基本一致,但为了使处理分布式计算的部分更加突出,本小节将此过程整理为一个函数。
def build_model(x, y_, is_chief):
    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    # 通过和5.5节给出的mnist_inference.py代码计算神经网络前向传播的结果。
    y = mnist_inference.inference(x, regularizer)
    global_step = tf.contrib.framework.get_or_create_global_step()

    # 计算损失函数并定义反向传播过程。
    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,
        60000 / BATCH_SIZE,
        LEARNING_RATE_DECAY)
    
    train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(
        loss, global_step=global_step)

    # 定义每一轮迭代需要运行的操作。
    if is_chief:
        # 计算变量的滑动平均值。   
        variable_averages = tf.train.ExponentialMovingAverage(
            MOVING_AVERAGE_DECAY, global_step)
        variables_averages_op = variable_averages.apply(
            tf.trainable_variables())
        with tf.control_dependencies([variables_averages_op, train_op]):
            train_op = tf.no_op()
    return global_step, loss, train_op

def main(argv=None):
    # 解析flags并通过tf.train.ClusterSpec配置TensorFlow集群。
    ps_hosts = FLAGS.ps_hosts.split(',')
    worker_hosts = FLAGS.worker_hosts.split(',')
    cluster = tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})
    # 通过tf.train.ClusterSpec以及当前任务创建tf.train.Server。
    server = tf.train.Server(cluster,
                             job_name=FLAGS.job_name,
                             task_index=FLAGS.task_id)

    # 参数服务器只需要管理TensorFlow中的变量,不需要执行训练的过程。server.join()会
    # 一致停在这条语句上。
    if FLAGS.job_name == 'ps':
        with tf.device("/cpu:0"):
            server.join()

    # 定义计算服务器需要运行的操作。
    is_chief = (FLAGS.task_id == 0)
    mnist = input_data.read_data_sets(DATA_PATH, one_hot=True)

    # 通过tf.train.replica_device_setter函数来指定执行每一个运算的设备。
    # tf.train.replica_device_setter函数会自动将所有的参数分配到参数服务器上,而
    # 计算分配到当前的计算服务器上。图12-9展示了通过TensorBoard可视化得到的第一个计
    # 算服务器上运算分配的结果。
    device_setter = tf.train.replica_device_setter(
        worker_device="/job:worker/task:%d" % FLAGS.task_id,
        cluster=cluster)
    
    with tf.device(device_setter):
        # 定义输入并得到每一轮迭代需要运行的操作。
        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')
        global_step, loss, train_op = build_model(x, y_, is_chief)

        hooks=[tf.train.StopAtStepHook(last_step=TRAINING_STEPS)]
        sess_config = tf.ConfigProto(allow_soft_placement=True,
                                     log_device_placement=False)

        # 通过tf.train.MonitoredTrainingSession管理训练深度学习模型的通用功能。
        with tf.train.MonitoredTrainingSession(master=server.target,
                                               is_chief=is_chief,
                                               checkpoint_dir=MODEL_SAVE_PATH,
                                               hooks=hooks,
                                               save_checkpoint_secs=60,
                                               config=sess_config) as mon_sess:
            print "session started."
            step = 0
            start_time = time.time()

            # 执行迭代过程。在迭代过程中tf.train.MonitoredTrainingSession会帮助完成初始
            # 化、从checkpoint中加载训练过的模型、输出日志并保存模型, 所以下面的程序中不需要
            # 在调用这些过程。tf.train.StopAtStepHook会帮忙判断是否需要退出。
            while not mon_sess.should_stop():                
                xs, ys = mnist.train.next_batch(BATCH_SIZE)
                _, loss_value, global_step_value = mon_sess.run(
                    [train_op, loss, global_step], feed_dict={x: xs, y_: ys})

                # 每隔一段时间输出训练信息。不同的计算服务器都会更新全局的训练轮数,所以这里使用
                # global_step_value得到在训练中使用过的batch的总数。
                if step > 0 and step % 100 == 0:
                    duration = time.time() - start_time
                    sec_per_batch = duration / global_step_value
                    format_str = "After %d training steps (%d global steps), " +\
                                 "loss on training batch is %g. (%.3f sec/batch)"
                    print format_str % (step, global_step_value, loss_value, sec_per_batch)
                step += 1

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

假设以上代码文件名为dist_tf_mnist_async.py,且拥有一个参数服务器、两个计算服务器的集群。在参数服务器的机器上启动:

python dist_tf_mnist_async.py \
--job_name ='ps' \
--task_id=O \
--ps_hosts = 'tf-ps0:2222' \
--worker_hosts = 'tf-worker0:2222,tf-worker1:2222'

在第一个计算服务器启动:

python dist_tf_mnist_async.py \
--job_name ='worker' \
--task_id=O \
--ps_hosts = 'tf-ps0:2222' \
--worker_hosts = 'tf-worker0:2222,tf-worker1:2222'

在第二个计算服务器启动:

python dist_tf_mnist_async.py \
--job_name ='worker' \
--task_id=1 \
--ps_hosts = 'tf-ps0:2222' \
--worker_hosts = 'tf-worker0:2222,tf-worker1:2222'

启动第一个计算服务器之后,这个服务器就会尝试连接其他的服务器(包括ps和worker),如果其他服务器还没有启动,则会提示等待。当每一个服务器都启动之后,训练过程开始执行。

第一个计算服务器的tf计算图如下,可以看到参数放在了参数服务器上(浅灰色节点),反向传播的计算放在了当前的计算服务器上(深灰色节点)。

第一个计算服务器输出:

第二个计算服务器输出:

可以看到,第二个worker启动之前,第一个worker已经运行了很多轮迭代了。异步模式下,即使有计算服务器没有正常工作,参数更新的过程仍可继续。且全局迭代轮数是所有worker轮数之和。

同步模式

# -*- coding: utf-8 -*-

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

import mnist_inference

# 配置神经网络的参数。
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.01
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 20000
MOVING_AVERAGE_DECAY = 0.99

MODEL_SAVE_PATH = "logs/log_sync"
DATA_PATH = "../../datasets/MNIST_data"

# 和异步模式类似的设置flags。
FLAGS = tf.app.flags.FLAGS

tf.app.flags.DEFINE_string('job_name', 'worker', ' "ps" or "worker" ')
tf.app.flags.DEFINE_string(
    'ps_hosts', ' tf-ps0:2222,tf-ps1:1111',
    'Comma-separated list of hostname:port for the parameter server jobs. e.g. "tf-ps0:2222,tf-ps1:1111" ')
tf.app.flags.DEFINE_string(
    'worker_hosts', ' tf-worker0:2222,tf-worker1:1111',
    'Comma-separated list of hostname:port for the worker jobs. e.g. "tf-worker0:2222,tf-worker1:1111" ')
tf.app.flags.DEFINE_integer('task_id', 0, 'Task ID of the worker/replica running the training.')

# 和异步模式类似的定义TensorFlow的计算图。唯一的区别在于使用
# tf.train.SyncReplicasOptimizer函数处理同步更新。
def build_model(x, y_, n_workers, is_chief):
    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    y = mnist_inference.inference(x, regularizer)
    global_step = tf.contrib.framework.get_or_create_global_step()

    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,
        60000 / BATCH_SIZE,
        LEARNING_RATE_DECAY)
    
    # 通过tf.train.SyncReplicasOptimizer函数实现同步更新。
    opt = tf.train.SyncReplicasOptimizer(
        tf.train.GradientDescentOptimizer(learning_rate),
        replicas_to_aggregate=n_workers,
        total_num_replicas=n_workers)
    sync_replicas_hook = opt.make_session_run_hook(is_chief)
    train_op = opt.minimize(loss, global_step=global_step)
    
    if is_chief:
        variable_averages = tf.train.ExponentialMovingAverage(
            MOVING_AVERAGE_DECAY, global_step)
        variables_averages_op = variable_averages.apply(
            tf.trainable_variables())
        with tf.control_dependencies([variables_averages_op, train_op]):
            train_op = tf.no_op()
            
    return global_step, loss, train_op, sync_replicas_hook

def main(argv=None):
    # 和异步模式类似的创建TensorFlow集群。
    ps_hosts = FLAGS.ps_hosts.split(',')
    worker_hosts = FLAGS.worker_hosts.split(',')
    n_workers = len(worker_hosts)
    cluster = tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})

    server = tf.train.Server(cluster,
                             job_name=FLAGS.job_name,
                             task_index=FLAGS.task_id)

    if FLAGS.job_name == 'ps':
        with tf.device("/cpu:0"):
            server.join()

    is_chief = (FLAGS.task_id == 0)
    mnist = input_data.read_data_sets(DATA_PATH, one_hot=True)

    device_setter = tf.train.replica_device_setter(
        worker_device="/job:worker/task:%d" % FLAGS.task_id,
        cluster=cluster)
    
    with tf.device(device_setter):
        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')
        global_step, loss, train_op, sync_replicas_hook = build_model(x, y_, n_workers, is_chief)

        # 把处理同步更新的hook也加进来。
        hooks=[sync_replicas_hook, tf.train.StopAtStepHook(last_step=TRAINING_STEPS)]
        sess_config = tf.ConfigProto(allow_soft_placement=True,
                                     log_device_placement=False)

        # 训练过程和异步一致。
        with tf.train.MonitoredTrainingSession(master=server.target,
                                               is_chief=is_chief,
                                               checkpoint_dir=MODEL_SAVE_PATH,
                                               hooks=hooks,
                                               save_checkpoint_secs=60,
                                               config=sess_config) as mon_sess:
            print "session started."
            step = 0
            start_time = time.time()

            while not mon_sess.should_stop():                
                xs, ys = mnist.train.next_batch(BATCH_SIZE)
                _, loss_value, global_step_value = mon_sess.run(
                    [train_op, loss, global_step], feed_dict={x: xs, y_: ys})

                if step > 0 and step % 100 == 0:
                    duration = time.time() - start_time
                    sec_per_batch = duration / global_step_value
                    format_str = "After %d training steps (%d global steps), " +\
                                 "loss on training batch is %g. (%.3f sec/batch)"
                    print format_str % (step, global_step_value, loss_value, sec_per_batch)
                step += 1

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

第一个worker输出:

第二个worker输出:

和异步模式不同,同步模式下,global_step差不多是两个worker的local_step的平均值。比如第二个worker没开始之前,global_step是第一个worker的local_step的一半。这是因为同步模式要求收集replicas_to_aggregate份梯度才会开始更新(不要求每一份梯度来自不同的计算服务器)。同步模式不仅是一次使用多份梯度,tf.train.SyncRepIicasOptimizer同时实现了不会出现陈旧变量的问题,它会记录每一份梯度是不是由最新的变量值计算得到的,如果不是,这个梯度会被抛弃。

猜你喜欢

转载自blog.csdn.net/xiang_freedom/article/details/81949920