最近一段时间,在跑模型的过程中,发现数据量很大(1g以上)的时候,内存很容易就爆表了,这是不能接受的。所幸
Tensorflow
中对输入有着比较好的封装,因此抽时间学习一下dataset
的概念和用法,这个玩意可以帮助我们用封装好的方法 一行一行的读数据,但是不能帮我们完成batch的操作哟。不过batch操作tensorflow
中也有提供的!特此用本文进行一下总结。参考官方教程.
概述
tf.data
包提供的 API 就是用来帮助用户快速的构建输入的管道pipeline
的。以文本的输入为例,tf.data
提供的功能包括:从原始文本中抽取符号;将文本符号转化成查找表(embeddings
);把长度不同的输入字符串转化成规范的batch数据。总的来说,这个接口能够帮助用户轻松的应对大数据量的处理,以及不同格式的归一化处理。
tf.data
包主要提供了一下两个主要的接口:
tf.data.Dataset
可以用来表示一个序列的元素,每个元素都是tensor
或者tensor
的集合。举例来说,在图像处理的管道中,上文提到的元素就可以指一个训练样本(包括输入tensor
和输出标签tensor
)。创建一个Dataset
对象有两种不同的方式:
* 从数据源构造dataset
。 (例如Dataset.from_tensor_slices()
) constructs a dataset from one or moretf.Tensor
objects.
* 从其他dataset
转换得到新的dataset
。 (例如Dataset.batch()
) (https://www.tensorflow.org/api_docs/python/tf/data/Dataset) objects.tf.data.Iterator
接口主要作用是获取数据值。其中Iterator.get_next()
返回数据集的下一个元素。很显然,这个接口在数据集和模型之间充当了桥梁的作用。最简单的Iterator是 "one-shot iterator"。这是一种和单一数据集绑定的iterator,并且只能遍历一次。如果想要使用更加复杂的功能,就可以使用Iterator.initializer
操作重新初始化iterator,这个重新初始化包括了重新设定参数,甚至可以重新设定数据集,这样的话我们就能够做到一个数据集遍历多次。
基本原理
本文的这一部分主要描述了构造 Dataset
和 Iterator
对象的基本方法,以及如何利用这些对象获取数据。
首先,想要启动一个输入数据管道,我们必须定义一个数据源(即source
)。数据源可以很多啦,拿内存中tensor
当数据源也是可以的,用这两个方法就好tf.data.Dataset.from_tensors()
或者tf.data.Dataset.from_tensor_slices()
. 再比如呢,你的数据在磁盘上,并且是Tensorflow
亲儿子格式TFRecordDataset
,那么我们就可以用 tf.data.TFRecordDataset
.这个方法。
一旦有了Dataset
对象,利用链式变换将它转换成新的Dataset
对象。举例来说,可以进行的变换包括:逐元素变换Dataset.map()
;多元素变换 Dataset.batch()
. See the documentation for tf.data.Dataset
。
从Dataset
中获取数据的最主要方法还是前文提到的iterator
的方法。这个方法能够一次调用为我们提供一个元素。iterator
包括两个方法:Iterator.initializer主要用来初始化遍历器;
Iterator.get_next()主要用来返回下一个元素。同时呢,iterator
的品种口味有很多,用户可以根据自己的需要使用不同的iterator
详情将会在下文中介绍。
Dataset 结构
dataset
的每一个元素都是同构的。每一个元素是一个或者多个 tf.Tensor
对象,这些对象被称为组成元素. 每个组成元素都有一个tf.DType
属性,用来标识组成元素的类型。还有tf.TensorShape
](https://www.tensorflow.org/api_docs/python/tf/TensorShape) 对象用来标识组成元素的维度。
而利用Dataset.output_types
和 Dataset.output_shapes
这两个属性能够检查每个元素的输出类型和输出大小是否规范。同时呢,嵌套的类型也是存在的。下面用例子来说明问题。
import tensorflow as tf
# 4行数据
dataset1 = tf.data.Dataset.from_tensor_slices(tf.random_uniform([4, 10]))
print(dataset1.output_shapes) # (10,)=>1行1行的输出数据
print(dataset1.output_types)
print()
print("##########################################")
# 1个tensor就1个输出,那多个tensor就多个输出咯
dataset2 = tf.data.Dataset.from_tensor_slices(
(tf.random_uniform([4]),
tf.random_uniform([4, 100], maxval=100, dtype=tf.int32)))
print(dataset2.output_types) # ==> "(tf.float32, tf.int32)"
print(dataset2.output_shapes) # ==> "((), (100,))"
print()
print("##########################################")
# 有时候我们也可以先构建不同的dataset然后再组合起来
dataset3 = tf.data.Dataset.zip((dataset1, dataset2))
print(dataset3.output_types)
print(dataset3.output_shapes)
print()
print("##########################################")
# 有时候我们也可以给这些tensor起个名字哇,不然显得多乱
named_dataset = tf.data.Dataset.from_tensor_slices({
"single_value": tf.random_uniform([4]),
"array": tf.random_uniform([4, 10])
})
print(named_dataset.output_shapes)
print(named_dataset.output_types)
这里遗留了一个问题,就是如果采用这种拼接的方式对两个tensor进行封装,那么如果tensor的长度不一致可咋办哟。
It is often convenient to give names to each component of an element, for example if they represent different features of a training example. In addition to tuples, you can use collections.namedtuple
or a dictionary mapping strings to tensors to represent a single element of a Dataset
.
dataset1 = tf.data.Dataset.from_tensor_slices(tf.random_uniform([4, 10]))
print(dataset1.output_types) # ==> "tf.float32"
print(dataset1.output_shapes) # ==> "(10,)"
dataset2 = tf.data.Dataset.from_tensor_slices(
(tf.random_uniform([4]),
tf.random_uniform([4, 100], maxval=100, dtype=tf.int32)))
print(dataset2.output_types) # ==> "(tf.float32, tf.int32)"
print(dataset2.output_shapes) # ==> "((), (100,))"
dataset3 = tf.data.Dataset.zip((dataset1, dataset2))
print(dataset3.output_types) # ==> (tf.float32, (tf.float32, tf.int32))
print(dataset3.output_shapes) # ==> "(10, ((), (100,)))"
Dataset
的变换支持任何结构的Dataset
,使用 Dataset.map()
, Dataset.flat_map()
, 和 Dataset.filter()
这三个变换时将会对每一个元素都进行相同的变化,而元素结构的变换就是Dataset
变换的本质。这些东西在后面的介绍中会用到,所以在这里只是给出了一个简单的介绍,在后面的应用场景中将会具体的介绍使用方法。
dataset1 = dataset1.map(lambda x: ...)
dataset2 = dataset2.flat_map(lambda x, y: ...)
# Note: Argument destructuring is not available in Python 3.
dataset3 = dataset3.filter(lambda x, (y, z): ...)
创建iterator
一旦有了Dataset
对象,我们的下一步就是创造一个iterator
来从dataset
中获取数据。
tf.data
这个接口现在支持以下几种iterator
.
- one-shot,
- initializable,
- reinitializable, and
- feedable.
这其中呢,one-shot iterator 是最简单的一种啦。这种遍历器只支持遍历单一dataset,并且还不需要显式的初始化。简单但是有效!大部分的应用场景这种遍历器都是可以handle的。但是它不支持参数化。下面给出一个例子:
print()
print("##########################################")
# one-shot iterator的使用
range_dataset = tf.data.Dataset.range(100)
iterator = range_dataset.make_one_shot_iterator()
next_elem = iterator.get_next()
with tf.Session() as sess:
for i in range(100):
num = sess.run(next_elem)
print(num)
注意: 目前,封装好的模型(Estimator)支持这种遍历器。至于其他三种iterator在这里就不给介绍啦,对于我等菜鸡没什么鬼用
从iterator获取数据
不瞎的话你就会发现,前面其实已经多次提到啦获取数据的方法,最简单的就是使用iterator.get_next()
方法来获取下一个元素咯。当然啦这个方法同样是lazy_evaluation,也就是说只有在session中运行的时候才会打印出结果,否则的话知识一个符号化的标记。
另外需要注意的是,当遍历器遍历到了dataset
的地段的时候就会报错 tf.errors.OutOfRangeError
. 报完错,这个遍历器就瞎了不能用了,需要重新初始化。所以说呀,常见的做法就是包裹一层try catch 咯:
sess.run(iterator.initializer)
while True:
try:
sess.run(result)
except tf.errors.OutOfRangeError:
break
嵌套的dataset
的使用方法也是很直观的。不过呢,需要注意一点,就是下面代码中,iterator.get_next()
返回值是俩,这俩都是产生自同一个tensor
,所以对其中的任何一个进行run
操作都会直接导致iterator
进入下一步的循环中。因此我们常规操作是如果需要evaluate
就把所有的下一个元素同时evaluate
。
print()
print("##########################################")
# 嵌套的dataset 利用iterator获取数据
nested_iterator=dataset2.make_initializable_iterator()
next1,next2=nested_iterator.get_next()
with tf.Session() as sess:
sess.run(nested_iterator.initializer)
num1=sess.run(next1)
print(num1)
保存iterator状态
tf.contrib.data.make_saveable_from_iterator
这个方法创建一个 SaveableObject
对象来保存iterator的状态。存下来了之后就可以中断然后改天再接着训练啦。,这个对象可以添加到tf.train.Saver
的变量列表中,或者 tf.GraphKeys.SAVEABLE_OBJECTS
集合中,或者直接按照变量的方式进行保存。具体的存储过程自己看这个教程吧。 Saving and Restoring
# Create saveable object from iterator.
saveable = tf.contrib.data.make_saveable_from_iterator(iterator)
# Save the iterator state by adding it to the saveable objects collection.
tf.add_to_collection(tf.GraphKeys.SAVEABLE_OBJECTS, saveable)
saver = tf.train.Saver()
with tf.Session() as sess:
if should_checkpoint:
saver.save(path_to_checkpoint)
# Restore the iterator state.
with tf.Session() as sess:
saver.restore(sess, path_to_checkpoint)
读取数据
前面把基本流程都讲完啦,但是!!!没有用!我们想要的是大数据能够读进内存!还是没有解决。如果原来的数据可以放进内存,我们可以按照上面说的方法进行操作,但是那样我们还废了牛鼻子劲在这学这玩意干嘛。所以在这里tensorflow
支持使用Dataset
对象管理文件,包括TFRecord
,txt
,csv
等等文件。从直接读进内存的那种我们就不讲啦!只重点解剖txt
这样一种方法,杀鸡儆猴!以儆效尤!
Consuming NumPy arrays
Consuming TFRecord data
Consuming text data
许许多多的数据集都是text文件组成的。tf.data.TextLineDataset
接口提供了一种炒鸡简单的方法从这些数据文件中读取。我们提供只需要提供文件名(1个或者好多个都可以)。这个接口就会自动构造一个dataset
,这个dataset
的每一个元素就是一行数据,是一个string
类型的tensor
。
filenames = ["/var/data/file1.txt", "/var/data/file2.txt"]
dataset = tf.data.Dataset.from_tensor_slices(filenames)
# Use `Dataset.flat_map()` to transform each file as a separate nested dataset,
# and then concatenate their contents sequentially into a single "flat" dataset.
# * Skip the first line (header row).
# * Filter out lines beginning with "#" (comments).
dataset = dataset.flat_map(
lambda filename: (
tf.data.TextLineDataset(filename)
.skip(1)
.filter(lambda line: tf.not_equal(tf.substr(line, 0, 1), "#"))))
Consuming CSV data
使用Dataset.map()
进行预处理
Dataset.map(f)
方法的通过对每个元素进行f
变换得到一个新的dataset
使用的方法如下。
# Transforms a scalar string `example_proto` into a pair of a scalar string and
# a scalar integer, representing an image and its label, respectively.
def _parse_function(example_proto):
features = {"image": tf.FixedLenFeature((), tf.string, default_value=""),
"label": tf.FixedLenFeature((), tf.int32, default_value=0)}
parsed_features = tf.parse_single_example(example_proto, features)
return parsed_features["image"], parsed_features["label"]
# Creates a dataset that reads all of the examples from two files, and extracts
# the image and label features.
filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(_parse_function)
在map函数中调用任意函数tf.py_func()
处于性能的要求哇,我们建议你尽量使用Tensorflow
内部提供的函数进行数据的预处理。但是呢,我们有时候不得不用一些外部的函数,要这么做就需要调用 tf.py_func()
这个方法啦,具体的使用实例如下:
import cv2
# Use a custom OpenCV function to read the image, instead of the standard
# TensorFlow `tf.read_file()` operation.
def _read_py_function(filename, label):
image_decoded = cv2.imread(filename.decode(), cv2.IMREAD_GRAYSCALE)
return image_decoded, label
# Use standard TensorFlow operations to resize the image to a fixed shape.
def _resize_function(image_decoded, label):
image_decoded.set_shape([None, None, None])
image_resized = tf.image.resize_images(image_decoded, [28, 28])
return image_resized, label
filenames = ["/var/data/image1.jpg", "/var/data/image2.jpg", ...]
labels = [0, 37, 29, 1, ...]
dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
dataset = dataset.map(
lambda filename, label: tuple(tf.py_func(
_read_py_function, [filename, label], [tf.uint8, label.dtype])))
dataset = dataset.map(_resize_function)
构造batch数据
简单batch
最简单的构造方法就是把几个输入元素堆叠起来组成一个新的元素,这就构造出来一个batch的数据啦。代码如下
The simplest form of batching stacks n
consecutive elements of a dataset into a single element. The Dataset.batch()
transformation does exactly this, with the same constraints as the tf.stack()
operator, applied to each component of the elements: i.e. for each component i, all elements must have a tensor of the exact same shape.
inc_dataset = tf.data.Dataset.range(100)
dec_dataset = tf.data.Dataset.range(0, -100, -1)
dataset = tf.data.Dataset.zip((inc_dataset, dec_dataset))
batched_dataset = dataset.batch(4)
iterator = batched_dataset.make_one_shot_iterator()
next_element = iterator.get_next()
print(sess.run(next_element)) # ==> ([0, 1, 2, 3], [ 0, -1, -2, -3])
print(sess.run(next_element)) # ==> ([4, 5, 6, 7], [-4, -5, -6, -7])
print(sess.run(next_element)) # ==> ([8, 9, 10, 11], [-8, -9, -10, -11])
Batching tensors with padding
不用说你也知道上面的这种情况实在是太简单啦,它假设所有的tensor
都是有相同长度的,但是这怎么可能呢???为了解决这个问题,,tensorflow
中特地提出了Dataset.padded_batch()
这种构造方法。通过指定一个或多个需要补充(设置默认值的)维度 ,tensorflow
就会自动帮你完成这样的填充。注意:填充是对每个元素填充成同样的长度,而不是不够一个batch 凑够一个batch
dataset = tf.data.Dataset.range(100)
# 第一行0个0;第二行1个1;第三行2个2;
dataset = dataset.map(lambda x: tf.fill([tf.cast(x, tf.int32)], x))
dataset = dataset.padded_batch(4, padded_shapes=[None])
iterator = dataset.make_one_shot_iterator()
next_element = iterator.get_next()
print(sess.run(next_element)) # ==> [[0, 0, 0], [1, 0, 0], [2, 2, 0], [3, 3, 3]]
print(sess.run(next_element)) # ==> [[4, 4, 4, 4, 0, 0, 0],
# [5, 5, 5, 5, 5, 0, 0],
# [6, 6, 6, 6, 6, 6, 0],
# [7, 7, 7, 7, 7, 7, 7]]
另外,这个接口还有一些功能,包括:支持对不同的维度进行不同的padding
策略;支持变长或者定长的padding
策略;支持自定义的默认值;
训练流程
多个epoch的训练
tf.data
包提供两种主要的方法用来进行多个epoch 的训练。
最简单的做法是使用 Dataset.repeat()
这个变换操作。这个变换操作相当于叠加了几次数据集,对后端毫无影响。具体代码如下:
filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)
dataset = dataset.repeat(10)
dataset = dataset.batch(32)
简单的方法好是好!但是有个问题,如果我们每个epoch后面想要打印一下信息或者计算一下错误率,这可肿么办??所以还有一个不那么智能的操作。基本的逻辑就是,我们就一个epoch一个epoch的跑,跑过了,就会报错,报错就说明一个epoch跑完了,就测测错误率,然后重新初始化一下遍历器就好了。具体操作如下:
filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)
dataset = dataset.batch(32)
iterator = dataset.make_initializable_iterator()
next_element = iterator.get_next()
# Compute for 100 epochs.
for _ in range(100):
sess.run(iterator.initializer)
while True:
try:
sess.run(next_element)
except tf.errors.OutOfRangeError:
break
# [Perform end-of-epoch calculations here.]
总结
总结一下,后面还有什么随机化的输入 什么的,我暂时用不到就先不写了。但是也有一些用的到的却没在教程中涉及,比如说我们的单词字典的构造(初步想法是使用feature column),字母字典的构造?还没想好怎么写,后续补上。