2018-07-22-tensorflow-dataset的使用

最近一段时间,在跑模型的过程中,发现数据量很大(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 more tf.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,这个重新初始化包括了重新设定参数,甚至可以重新设定数据集,这样的话我们就能够做到一个数据集遍历多次。

基本原理

   本文的这一部分主要描述了构造 DatasetIterator 对象的基本方法,以及如何利用这些对象获取数据。

   首先,想要启动一个输入数据管道,我们必须定义一个数据源(即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_typesDataset.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),字母字典的构造?还没想好怎么写,后续补上。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容