神经网络的复兴和深度学习的崛起离不开大数据的驱动,对于我们这个时代的机器学习模型来说,数据是一切的基础。没有真实而有效的数据支撑,再精妙的算法也会黯然失色,模型设计便如同纸上谈兵。因此,在学习 TensorFlow 模型开发方法的同时,很有必要掌握一种高效的高效处理数据的方法 - TFRecords
格式。
别被下面那些密密麻麻的汉字和代码吓倒,只要跟着我一步一步来,你会发现其实并没有想像中的那么困难。当然,如果你看完了这篇教程之后,发现自己明白了很多,却又几乎什么都记不得,那也是很正常的——我认为,没接触过 TFRecords
文件的入在看完这篇教程后,能把提到过的知识记住80%以上的可能性为零。这里只是让你明白基本的原理和怎么应用,以后你还需要多练习,多使用,才能熟练掌握 TFRecords
格式进行高效地读取数据。
除了作为入门教程之外,本文还试图成为可以在日常工作中 TFRecords
的参考手册。就博主本人的经历来说,这个目标还是完成得不错的——你看,我自己也没能把所有的东西记下来,不是吗?
最重要的是——请给我30分钟,如果你没有使用
TFRecords
文件的经验,请不要试图在30秒内入门——除非你是超人
关于 TensorFlow 读取数据的方法,官网给出了三种方法:
用户处理输入数据的典型流程是:首先将输入数据集从文件系统种读取到内存中,然后将其转换为模型需要的输入数据格式,接着以某种方式传入数据流图,继而开始真正的模型训练过程。
输入数据集一般被存储在各种类型的文件系统中,根据文件系统类型和输入数据集大小,有两种不同的数据读取方法:
queue
,也就是第二种方法从文件读取数据。batch
输入网络进行训练,也就是第三种方法预加载数据。本文主要介绍的是一种比较通用和高效的读取方法,经过亲身试用也确实好用的格式—— TFRecords
格式。
博主注:如果你的电脑显卡不够好,显存也不够大,数据集还想读取的更多,
TFRecords
格式绝对是你的最佳选择。
TFRecords
数据文件是一种将图像数据和标签统一存储的二进制文件,虽然它不如其他格式好理解,但是它能更好的利用内存,在 TensorFlow 中快速的复制,移动,读取,存储等。TFRecords
文件存储的是有结构的序列化字符块,它是 TensorFIow 推荐的标准文件格式。
uint64 length
uint32 masked_crc32_of_length
byte data[length]
uint32 masked_crc32_of_data
上面是 Tensorflow 的官网给出的文档结构。整个文件由文件长度信息、长度校验码、数据、数据校验码组成。但对于我们普通开发者而言,我们并不需要关心这些,只要知道 Tensorflow 提供了丰富的 API
,可以帮助我们轻松读写 TFRecords
文件就行了。
tf.train.Example
的定义如下:
message Example{
Features features = 1;
};
message Features{
map<string,Feature> featrue = 1;
};
message Feature{
oneof kind{
BytesList bytes_list = 1;
FloatList float_list = 2;
Int64List int64_list = 3;
}
};
message BytesList {
repeated bytes value = 1;
}
message FloatList {
repeated float value = 1 [packed = true];
}
message Int64List {
repeated int64 value = 1 [packed = true];
}
可以看得出一个 Example
消息体中包含了一系列的 feature
属性。每一个 feature
是一个 map
,也就是 key-value
的键值对。key
取值是 String
类型。而 value
是 Feature
类型的消息体,它的取值有 3 种:
BytesList
FloatList
Int64List
需要注意的是,他们都是列表的形式。List
对应到 python 语言当中是列表,而对于 Java 或者 C/C++ 来说他们就是数组。举个例子,一个 BytesList
可以存储 Byte
数组,因此像字符串、图片、视频等等都可以容纳进去。所以 TFRecord 可以存储几乎任何格式的信息。但需要说明的是,更官方的文档来源于 Tensorflow 的源码,这里面有详细的定义及注释说明。
当处理规模很大的数据集时,比如 ImageNet 图像分类数据集(约为140GB),TensorFlow 提供了以输入流水线方式从多个文件中并行读取数据的方法,这使得模型训练所需的数据能够实时填充进数据流图。该方法的核心思想是实现多个数据缓冲区以确保任何时刻内存中都有数据可以填充进数据流图。下图展示了一个典型的输入流水线并行读取数据的工作流程。
首先,创建一个文件名列表(Filenames) ABC
,然后通过 tf.train.string_input_producer
创建输出是一个先入先出(FIFO)的文件名队列(Filename Queue),之后可以使用 tf.TFRecordReader
的 tf.parse_single_example
解析器,这个parse_single_example
操作可以将 Example
协议内存块(protocol buffer)解析为张量。 MNIST 的例子就使用了convert_to_records
所构建的数据。请参看tensorflow/tensorflow/examples/how_tos/reading_data/fully_connected_reader.py。
理解整个工作流程的关键是理解两个队列:文件名队列和样例队列。因为模型训练过程不止一次遍历整个数据集,所以文件名队列为程序读取数据文件提供了一个缓冲区。在将文件名传入文件名队列时,程序打乱了文件名的顺序,增加了输入数据的随机性。又因为程序需要向数据流图中持续不断地填充符合特定数据属性的样例,所以样例队列为填充数据流图提供了一个缓冲区,使得每一步训练都能够实时地获取到输入样例。
文件名列表是指组成输入数据集的所有文件的名称构成的列表,它们可能是本地文件系统上的文件位置,也可能是共享文件系统或分布式文件系统上的统一资源标志符(URI)。用户需要确保 TensorFlow 程序有权限访问 URI 标识的文件。这里我们推荐下面两种创建文件名列表的方法。
使用 Python 列表如果文件名的个数不多,或文件命名遵循某种规则,那么用户可以直接使用 python 列表存储文件名,比如 ["filee.csv","filel.csv"]
或者 [("file%d.csv"%i) for i in xrange(100)]
使用 tf.train.match_filenames_once
方法。该方法在数据流图中创建一个获取文件名列表的操作,它输入一个文件名列表的匹配模式,返回一个存储了符合该匹配模式的文件名列表变量。在初始化全局变量时,该文件名列表变量也会被初始化。
我们使用 tf.train.string_input_producer
方法创建文件名队列,它的输入是前面创建的文件名列表,输出是一个先入先出(FIFO)的文件名队列。通常,我们称完整遍历一次输入数据集为模型的一个训练周期,而训练模型需要反复遍历整个输入数据集,以不断检验更新的模型参数是否能够更好地表达训练数据的潜在模式。
用户可以通过 tf.train.string_input_producer
方法的输入参数 num-epochs
设置模型的最大训练周期数。但是在每一次遍历数据集时,我们希望输入样本的顺序有所不同,通过增加一些随机因素减小模型的过拟合。因此,我们可以将 tf.train.string_input_producer
方法的输入参数 shuffle
设置为 True
,此时程序便能打乱每个训练周期的文件名顺序。同时,TensorFlow 保证打乱文件名顺序后仍然采用均匀抽样,避免了用户自己实现文件名乱序时可能造成的欠采样或过采样的问题。
如下图所示,文件名列表中的文件名顺序原本是 ABC
,乱序后输入文件名队列中的顺序是 ACB
和 CAB
。
tf.train.string_input_producer
方法的原型如下:
tf.train.string_input_producer(string_tensor, num_epochs=None, shuffle=True, seed=None,
capacity=32, shared_name=None, name=None, cancel_op=None)
下表列出了 tf.train.string_input_producer
方法的所有输入参数,它们均可以作为创建文件名时的配置项。
参数名称 | 功能说明 |
---|---|
string_tensor |
存储文件名列表的字符串张量 |
num_epochs |
最大训练周期 |
shuffle |
是否打乱文件名顺序 |
seed |
随机化种子,当 shuffle 等于 True 的时候生效 |
capacity |
文件名队列容量 |
shared_name |
多个会话间共享的文件名队列名称 |
name |
创建文件名队列操作的名称 |
cancel_op |
取消队列的操作 |
Reader
的功能是读取数据记录,Decoder
的功能是将数据记录转换为张量格式。Reader
和 Decoder
的类型与数据文件格式相关,下表列出了 TensorFlow 推荐的 TFRecords
数据文件格式及其对应的Reader和Decoder类型。
文件格式 | Reader 类型 | Decoder 类型 |
---|---|---|
TFRecords 文件 |
tf.TFRecordReader |
tf.parse_single_example |
我们使用 Reader
和 Decoder
的典型流程是:
首先,创建输入数据文件对应的 Reader
;
然后,从文件名队列中取出文件名;
接着,将它传入 Reader
的 read
方法,后者返回形如(输入数据文件,数据记录)的元组;
最后,使用对应的 Decoder
操作,将数据记录中的每一列数据都转换为张量格式。
TFRecords
文件包含了 tf.train.Example
协议缓冲区(protocol buffer
),协议缓冲区包含了特征 Features
。TensorFlow 通过 Protocol Buffers
定义了 TFRecords
文件中存储的数据记录及其所含字段的数据结构,它们分别定义在 tensorflow/core/example 目录下的 example.proto 和 feature.proto 文件中。因此,我们将数据记录转换后的张量称为样例,将记录包含的字段称为特征域。
TFRecords
文件的样例结构层次非常清晰,一个样例包含一组特征。一组特征由多个特征向量组成的 Python 字典构成。为了说明读取 TFRecords
文件中样例的方法,我们首先使用 tf.python_io.TFRecordWriter
方法将下表中的数据写入 TFRecords
文件 stat.tfrecord 中。
表格如下:
id | age | income | outgo |
---|---|---|---|
1 | 24 | 2048.0 | 1024.0 |
2 | 48 | 4096.0 | 2048.0 |
相关代码如下:
'''writer.py'''
# -*- coding: utf-8 -*-
import tensorflow as tf
# 创建向TFRecords文件写数据记录的writer
writer = tf.python_io.TFRecordWriter('stat.tfrecord')
# 2轮循环构造输入样例
for i in range(1,3):
# 创建example.proto中定义的样例
example = tf.train.Example(
features = tf.train.Features(
feature = {
'id': tf.train.Feature(int64_list = tf.train.Int64List(value=[i])),
'age': tf.train.Feature(int64_list = tf.train.Int64List(value=[i*24])),
'income': tf.train.Feature(float_list = tf.train.FloatList(value=[i*2048.0])),
'outgo': tf.train.Feature(float_list = tf.train.FloatList(value=[i*1024.0]))
}
)
)
# 将样例序列化为字符串后,写入stat.tfrecord文件
writer.write(example.SerializeToString())
# 关闭输出流
writer.close()
然后使用 tf.TFRecordReader
方法读取 stat.tfrecord 文件中的样例,接着使用 tf.parse_single_example
将样例转换为张量。tf.parse_single_example
方法的输入参数 features
是一个 Python 字典,具体包括组成样例的所有特征的名称和数据类型,它们必须与 writer. py 中使用 tf.train.Features
方法定义的特征保持完全一致。tf.FixedLenFeature
方法的输入参数为特征形状和特征数据类型。因为本例中的4个特征都是标量,所以形状为 [] 。
相关代码如下:
'''reader.py'''
# -*- coding: utf-8 -*-
import tensorflow as tf
# 创建文件名队列filename_queue
filename_queue = tf.train.string_input_producer(['stat.tfrecord'])
# 创建读取TFRecords文件的reader
reader = tf.TFRecordReader()
# 取出stat.tfrecord文件中的一条序列化的样例serialized_example
_, serialized_example = reader.read(filename_queue)
# 将一条序列化的样例转换为其包含的所有特征张量
features = tf.parse_single_example(
serialized_example,
features={
'id': tf.FixedLenFeature([], tf.int64),
'age': tf.FixedLenFeature([], tf.int64),
'income': tf.FixedLenFeature([], tf.float32),
'outgo': tf.FixedLenFeature([], tf.float32),
}
)
执行上面的步骤后,我们得到4个特征张量一一 age
、outgo
、id
和 income
,如下所示:
{'age': <tf.Tensor 'ParseSing1eExample/Squeeze_age:0' shape=() dtype=int64>,
'outgo': <tf.Tensor 'ParseSing1eExample/Squeeze_outgo:0' shape=() dtype=float32>,
'id': <tf.Tensor 'ParseSingleExamp1e/Squeezeid:0' shape=() dtype=int64>,
'income':<tf.Tensor 'ParseSing1eExamp1e/Squeeze_income:0' shape=() dtype=f10at32>}
在会话执行时,为了使计算任务顺利获取到输入数据,我们需要使用 tf.train.start_queue_runners
方法启动执行入队操作的所有线程,具体包括将文件名入队到文件名队列的操作,以及将样例入队到样例队列的操作。这些队列操作相关的线程属于 TensorFIow 的后台线程,它们确保文件名队列和样例队列始终有数据可以供后续操作读取。
下面我们补全读取 TFRecords
文件数据的代码 reader. py:
init_op = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init_op)
# 启动执行入队搡作的后台线程
tf.start_queue_runners(sess=sess)
# 读取第一条数据记录
for i in range(2):
example=sess.run(features)
print(example)
'''
{'age': 24, 'outgo': 1024.0, 'id': 1, 'income': 2048.0}
{'age': 48, 'outgo': 2048.0, 'id': 2, 'income': 4096.0}
'''
虽然我们用上面的代码成功读取并输出了 stat.tfrecord 文件中的数据,但是这种方法并不适用于生产环境。因为它的容错性较差,主要体现在队列操作后台线程的生命周期“无入管理",任何线程出现异常都会导致程序崩溃。常见的异常是文件名队列或样例队列越界抛出的 tf.errors.0ut0fRangeError
。队列越界的原因通常是读取的数据记录数量超过了 tf.train_string_input_producer
方法中指定的数据集遍历次数。
为了处理这种异常,我们使用 tf.train.coordinator
方法创建管理多线程生命周期的协调器。协调器的工作原理很简单,它监控 TensorFlow 的所有后台线程。当其中某个线程出现异常时,它的 should_stop
成员方法返回 True
,for
循环结束。然后程序执行 finally
中协调器的 request_stop
成员方法,请求所有线程安全退出。
需要注意的是,当我们使用协调器管理多线程前,需要先执行 tf.local_variables_initializer
方法对其进行初始化。为此,我们使用 tf.group
方法将它和 tf.global_variables_initializer
方法聚合生成整个程序的初始化操作 init_op
。
使用协调器的示例如下:
import tensorflow as tf
# 创建文件名队列filename_queue,并制定遍历两次数据集
filename_queue = tf.train.string_input_producer(['stat.tfrecord'], num_epochs=2)
# 省略中间过程
# 聚合两种初始化操作
init_op = tf.group(tf.global_variables_initializer(),
tf.local_variables_initializer())
sess.run(init_op)
# 创建协调器,管理线程
coord = tf.train.Coordinator()
# 启动QueueRunner, 此时文件名队列已经进队。
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
# 打印程序的后台线程信息
print('Threads: %s' % threads)
try:
for i in range(10):
if not coord.should_stop():
example = sess.run(features)
print(example)
except tf.errors.OutOfRangeError:
print('Catch OutOfRangeError')
finally:
# 请求停止所有后台线程
coord.request_stop()
print('Finishreading')
# 等待所有后台线程安全退出
coord.join(threads)
sess.close()
'''
输出:
Threads: [, \
]
{'age': 24, 'outgo': 1024.0, 'id': 1, 'income': 2048.0}
{'age': 48, 'outgo': 2048.0, 'id': 2, 'income': 4096.0}
{'age': 24, 'outgo': 1024.0, 'id': 1, 'income': 2048.0}
{'age': 48, 'outgo': 2048.0, 'id': 2, 'income': 4096.0}
Catch OutOfRangeError
Finish reading
'''
根据输出结果上看,程序启动了两个后台线程进行队列操作。在成功输出四条数据记录后,程序抛出了 tf.errors.OutOfRangeError
异常。在发起停止所有后台的请求后,程序输出 Finish reading
。接着协调器等待所有后台线程安全退出,最后关闭会话。
# 创建协调器,管理线程
coord = tf.train.Coordinator()
# 启动QueueRunner, 此时文件名队列已经进队。
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
特别要注意的就是上面两句,非常重要!非常重要!非常重要!(破音)
这两句实现的功能就是创建线程并使用 QueueRunner
对象来提取数据。简单来说:使用 tf.train
函数添加 QueueRunner
到 TensorFlow 中。在运行任何训练步骤之前,需要调用 tf.train.start_queue_runners
函数,否则 TensorFlow 将一直挂起。
前面说过 tf.train.start_queue_runners
这个函数将会启动输入管道的线程,填充样本到队列中,以便出队操作可以从队列中拿到样本。这种情况下最好配合使用一个 tf.train.Coordinator
,这样可以在发生错误的情况下正确地关闭这些线程。如果你对训练迭代数做了限制,那么需要使用一个训练迭代数计数器,并且需要被初始化。
经过之前的介绍,我们最后得到了许多样例,但是这些样例需要打包聚合成批数据才能供模型训练、评价和推理使用。TensorFlow 提供的 tf.train.shuffle_batch
方法不仅能够使用样例创建批数据,而且能顾在打包过程中打乱样例顺序,增加随机性。因此,我们认为完整的输入流水线应该还包括一个批数据队列。
伪代码实例如下:
def get_my_example(filename_queue):
reader = tf.SomeReader()
_, value = reader.read(filename_queue)
features = tf.decodesome(value)
# 对样例进行预处理
processed_example = some_processing(features)
return processed_example
def input_pipeline(filenames, batchsize, num_epochs=None):
# 当num_epochs--None时,表示文件名队列总是可用的,一直循环入队
filename_queue.tf.train.string_input_producer(
filenames, num_epochs=num_epochs, shuffle=True)
example = get_my_example(filename_queue)
# min_after_dequeue表示从样例队列中出队的样例个数,
# 值越大表示打乱顺序效果越好,同时意味着消耗更多内存
min_after_dequeue = 10000
# capacity表示扯数据队列的容量,推荐设置:
# min_after_dequeue + (num_threads + a small safety margin) * batchsize
capacity = min_after_dequeue + 3 * batch_size
# 创建样例example_batch
examplebatch = tf.train.shuffle_batch(
[example], batch_size=batch_size, capacity=capacity,
min_after_dequeue=min_after_dequeue)
return example_batch
tf.train.shuffle_batch
方法除了上面使用的参数外,常用的还有设置进行队列操作的线程个数的 num-threads
参数,设置队列中进行随机排列的随机化种子的 seed
参数,以及为入队多条样例设置的 enqueue_many
参数。
除了这种方法,也可以创建样例队列。通过 tf.RandomShuffleQueue
函数创建一个 queue
,按随机顺序进行 dequeue
。
tf.RandomShuffleQueue
有一定的容量限制 capacity
,支持多个生产者和消费者;tf.RandomShuffleQueue
中的每个元素是固定长度的 tensor
元组,数据类型由 dtypes
定义,形状为 shapes
。如果 shapes
没有定义,那么不同的 queue
元素可能有不同的形状,此时就不能使用 dqueue_many
;如果 shapes
定义了,则所有的元素必须有相同的形状。min_after_dequeue
决定 queue
在 dequeue
以后要保持的元素个数,如果没有足够的元素,就会 block
住 dequeue
的相关操作,直到有足够元素进来。当 queue
关闭,则这个参数被忽略。样例队列的伪代码如下:
# 创建样例队列
example_queue = tf.RandomShuffleQueue(
capacity,
min_after_dequeue,
dtypes=[tf.float32, tf.float32],
shapes)
num_threads = 设置线程个数
# 创建样例队列的入队操作
example_enqueue_op = example_queue.enqueue([img, label])
# 将定义的线程添加到queue_runner中
tf.train.add_queue_runner(tf.train.queue_runner.QueueRunner(
example_queue, [example_enqueue_op]*num_threads))
# 从样例队列中读取批样例图片和标签
images, labels = example_queue.dequeue_many(batch_size)
这块代码还是比较固定的,这里拿出我自己在用的代码作为推荐,代码模板如下:
# -*- coding: utf-8 -*-
import tensorflow as tf
def read_and_decode(filename):
filename_list = tf.gfile.Glob(filename_pattern)
filename_queue = tf.train.string_input_producer(filename_list, shuffle=True)
reader = tf.TFRecordReader()
_, serialized_example = reader.read(filename_queue)
features = tf.parse_single_example(
serialized_example,
features={
'label_raw': tf.FixedLenFeature([], tf.string),
'img_raw': tf.FixedLenFeature([], tf.string),
})
label = tf.decode_raw(features['label_raw'], tf.uint8)
label = tf.reshape(label, [512, 512, 1])
label = tf.cast(label, tf.float32)
label_max = tf.reduce_max(label)
label_min = tf.reduce_min(label)
label = (label - label_min) / (label_max - label_min)
img = tf.decode_raw(features['img_raw'], tf.uint8)
img = tf.reshape(img, [512, 512, 1])
img = tf.cast(img, tf.float32)
img_max = tf.reduce_max(img)
img_min = tf.reduce_min(img)
img = (img - img_min) / (img_max - img_min)
example_queue = tf.RandomShuffleQueue(
capacity=16*batch_size,
min_after_dequeue=8*batch_size,
dtypes=[tf.float32, tf.float32],
shapes=[[512, 512, 1], [512, 512, 1]])
num_threads = 16
example_enqueue_op = example_queue.enqueue([img, label])
tf.train.add_queue_runner(tf.train.queue_runner.QueueRunner(
example_queue, [example_enqueue_op]*num_threads))
images, labels = example_queue.dequeue_many(batch_size)
return images, labels
train_images, train_labels = read_tfrecord('./data/train.tfrecord',
batch_size=train_batch_size)
val_images, val_labels = read_tfrecord('./data/validation.tfrecord',
batch_size=valid_batch_size)
sess = tf.Session()
init_op = tf.group(tf.global_variables_initializer(),
tf.local_variables_initializer())
sess.run(init_op)
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
try:
while not coord.should_stop():
example = sess.run(train_op)
print(example)
except tf.errors.OutOfRangeError:
print('Catch OutOfRangeError')
finally:
coord.request_stop()
print('Finishreading')
coord.join(threads)
sess.close()