最近一段時間,在跑模型的過程中,發(fā)現(xiàn)數(shù)據(jù)量很大(1g以上)的時候宙刘,內(nèi)存很容易就爆表了,這是不能接受的牢酵。所幸
Tensorflow
中對輸入有著比較好的封裝悬包,因此抽時間學(xué)習一下dataset
的概念和用法艺沼,這個玩意可以幫助我們用封裝好的方法 一行一行的讀數(shù)據(jù),但是不能幫我們完成batch的操作喲瑞佩。不過batch操作tensorflow
中也有提供的晨川!特此用本文進行一下總結(jié)绑警。參考官方教程.
概述
?? tf.data
包提供的 API 就是用來幫助用戶快速的構(gòu)建輸入的管道pipeline
的。以文本的輸入為例绞铃,tf.data
提供的功能包括:從原始文本中抽取符號声畏;將文本符號轉(zhuǎn)化成查找表(embeddings
)激率;把長度不同的輸入字符串轉(zhuǎn)化成規(guī)范的batch數(shù)據(jù)铁追〖韭欤總的來說茫船,這個接口能夠幫助用戶輕松的應(yīng)對大數(shù)據(jù)量的處理琅束,以及不同格式的歸一化處理。
?? tf.data
包主要提供了一下兩個主要的接口:
tf.data.Dataset
可以用來表示一個序列的元素算谈,每個元素都是tensor
或者tensor
的集合涩禀。舉例來說,在圖像處理的管道中然眼,上文提到的元素就可以指一個訓(xùn)練樣本(包括輸入tensor
和輸出標簽tensor
)艾船。創(chuàng)建一個Dataset
對象有兩種不同的方式:
?? * 從數(shù)據(jù)源構(gòu)造dataset
。 (例如Dataset.from_tensor_slices()
) constructs a dataset from one or moretf.Tensor
objects.
?? * 從其他dataset
轉(zhuǎn)換得到新的dataset
。 (例如Dataset.batch()
) (https://www.tensorflow.org/api_docs/python/tf/data/Dataset) objects.tf.data.Iterator
接口主要作用是獲取數(shù)據(jù)值屿岂。其中Iterator.get_next()
返回數(shù)據(jù)集的下一個元素践宴。很顯然,這個接口在數(shù)據(jù)集和模型之間充當了橋梁的作用爷怀。最簡單的Iterator是 "one-shot iterator"阻肩。這是一種和單一數(shù)據(jù)集綁定的iterator,并且只能遍歷一次运授。如果想要使用更加復(fù)雜的功能烤惊,就可以使用Iterator.initializer
操作重新初始化iterator,這個重新初始化包括了重新設(shè)定參數(shù)吁朦,甚至可以重新設(shè)定數(shù)據(jù)集柒室,這樣的話我們就能夠做到一個數(shù)據(jù)集遍歷多次。
基本原理
?? 本文的這一部分主要描述了構(gòu)造 Dataset
和 Iterator
對象的基本方法逗宜,以及如何利用這些對象獲取數(shù)據(jù)雄右。
?? 首先,想要啟動一個輸入數(shù)據(jù)管道纺讲,我們必須定義一個數(shù)據(jù)源(即source
)不脯。數(shù)據(jù)源可以很多啦,拿內(nèi)存中tensor
當數(shù)據(jù)源也是可以的刻诊,用這兩個方法就好tf.data.Dataset.from_tensors()
或者tf.data.Dataset.from_tensor_slices()
. 再比如呢防楷,你的數(shù)據(jù)在磁盤上,并且是Tensorflow
親兒子格式TFRecordDataset
,那么我們就可以用 tf.data.TFRecordDataset
.這個方法则涯。
?? 一旦有了Dataset
對象复局,利用鏈式變換將它轉(zhuǎn)換成新的Dataset
對象。舉例來說粟判,可以進行的變換包括:逐元素變換Dataset.map()
亿昏;多元素變換 Dataset.batch()
. See the documentation for tf.data.Dataset
。
?? 從Dataset
中獲取數(shù)據(jù)的最主要方法還是前文提到的iterator
的方法档礁。這個方法能夠一次調(diào)用為我們提供一個元素角钩。iterator
包括兩個方法:Iterator.initializer主要用來初始化遍歷器;
Iterator.get_next()主要用來返回下一個元素呻澜。同時呢递礼,iterator
的品種口味有很多,用戶可以根據(jù)自己的需要使用不同的iterator
詳情將會在下文中介紹羹幸。
Dataset 結(jié)構(gòu)
??dataset
的每一個元素都是同構(gòu)的脊髓。每一個元素是一個或者多個 tf.Tensor
對象,這些對象被稱為組成元素. 每個組成元素都有一個tf.DType
屬性栅受,用來標識組成元素的類型将硝。還有tf.TensorShape
](https://www.tensorflow.org/api_docs/python/tf/TensorShape) 對象用來標識組成元素的維度恭朗。
??而利用Dataset.output_types
和 Dataset.output_shapes
這兩個屬性能夠檢查每個元素的輸出類型和輸出大小是否規(guī)范。同時呢依疼,嵌套的類型也是存在的痰腮。下面用例子來說明問題。
import tensorflow as tf
# 4行數(shù)據(jù)
dataset1 = tf.data.Dataset.from_tensor_slices(tf.random_uniform([4, 10]))
print(dataset1.output_shapes) # (10,)=>1行1行的輸出數(shù)據(jù)
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("##########################################")
# 有時候我們也可以先構(gòu)建不同的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
的變換支持任何結(jié)構(gòu)的Dataset
,使用 Dataset.map()
, Dataset.flat_map()
, 和 Dataset.filter()
這三個變換時將會對每一個元素都進行相同的變化稀余,而元素結(jié)構(gòu)的變換就是Dataset
變換的本質(zhì)悦冀。這些東西在后面的介紹中會用到,所以在這里只是給出了一個簡單的介紹睛琳,在后面的應(yīng)用場景中將會具體的介紹使用方法盒蟆。
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): ...)
創(chuàng)建iterator
??一旦有了Dataset
對象,我們的下一步就是創(chuàng)造一個iterator
來從dataset
中獲取數(shù)據(jù)师骗。
tf.data
這個接口現(xiàn)在支持以下幾種iterator
.
- one-shot,
- initializable,
- reinitializable, and
- feedable.
??這其中呢历等,one-shot iterator 是最簡單的一種啦。這種遍歷器只支持遍歷單一dataset辟癌,并且還不需要顯式的初始化寒屯。簡單但是有效!大部分的應(yīng)用場景這種遍歷器都是可以handle的黍少。但是它不支持參數(shù)化寡夹。下面給出一個例子:
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獲取數(shù)據(jù)
??不瞎的話你就會發(fā)現(xiàn),前面其實已經(jīng)多次提到啦獲取數(shù)據(jù)的方法昵济,最簡單的就是使用iterator.get_next()
方法來獲取下一個元素咯智绸。當然啦這個方法同樣是lazy_evaluation,也就是說只有在session中運行的時候才會打印出結(jié)果访忿,否則的話知識一個符號化的標記瞧栗。
??另外需要注意的是,當遍歷器遍歷到了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()
返回值是倆,這倆都是產(chǎn)生自同一個tensor
廊酣,所以對其中的任何一個進行run
操作都會直接導(dǎo)致iterator
進入下一步的循環(huán)中能耻。因此我們常規(guī)操作是如果需要evaluate
就把所有的下一個元素同時evaluate
。
print()
print("##########################################")
# 嵌套的dataset 利用iterator獲取數(shù)據(jù)
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狀態(tài)
tf.contrib.data.make_saveable_from_iterator
這個方法創(chuàng)建一個 SaveableObject
對象來保存iterator的狀態(tài)亡驰。存下來了之后就可以中斷然后改天再接著訓(xùn)練啦晓猛。,這個對象可以添加到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)
讀取數(shù)據(jù)
??前面把基本流程都講完啦透乾,但是:樵铩!乳乌!沒有用捧韵!我們想要的是大數(shù)據(jù)能夠讀進內(nèi)存!還是沒有解決汉操。如果原來的數(shù)據(jù)可以放進內(nèi)存再来,我們可以按照上面說的方法進行操作,但是那樣我們還廢了牛鼻子勁在這學(xué)這玩意干嘛磷瘤。所以在這里tensorflow
支持使用Dataset
對象管理文件其弊,包括TFRecord
,txt
,csv
等等文件。從直接讀進內(nèi)存的那種我們就不講啦膀斋!只重點解剖txt
這樣一種方法梭伐,殺雞儆猴!以儆效尤仰担!
Consuming NumPy arrays
Consuming TFRecord data
Consuming text data
??許許多多的數(shù)據(jù)集都是text文件組成的糊识。tf.data.TextLineDataset
接口提供了一種炒雞簡單的方法從這些數(shù)據(jù)文件中讀取。我們提供只需要提供文件名(1個或者好多個都可以)摔蓝。這個接口就會自動構(gòu)造一個dataset
赂苗,這個dataset
的每一個元素就是一行數(shù)據(jù),是一個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()
進行預(yù)處理
?? 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函數(shù)中調(diào)用任意函數(shù)tf.py_func()
??處于性能的要求哇,我們建議你盡量使用Tensorflow
內(nèi)部提供的函數(shù)進行數(shù)據(jù)的預(yù)處理猜谚。但是呢败砂,我們有時候不得不用一些外部的函數(shù)赌渣,要這么做就需要調(diào)用 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)
構(gòu)造batch數(shù)據(jù)
簡單batch
??最簡單的構(gòu)造方法就是把幾個輸入元素堆疊起來組成一個新的元素昌犹,這就構(gòu)造出來一個batch的數(shù)據(jù)啦坚芜。代碼如下
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
??不用說你也知道上面的這種情況實在是太簡單啦,它假設(shè)所有的tensor
都是有相同長度的斜姥,但是這怎么可能呢鸿竖??铸敏?為了解決這個問題缚忧,,tensorflow
中特地提出了Dataset.padded_batch()
這種構(gòu)造方法杈笔。通過指定一個或多個需要補充(設(shè)置默認值的)維度 搔谴,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
策略右钾;支持自定義的默認值;
訓(xùn)練流程
多個epoch的訓(xùn)練
??tf.data
包提供兩種主要的方法用來進行多個epoch 的訓(xùn)練旱爆。
??最簡單的做法是使用 Dataset.repeat()
這個變換操作舀射。這個變換操作相當于疊加了幾次數(shù)據(jù)集,對后端毫無影響怀伦。具體代碼如下:
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.]
總結(jié)
??總結(jié)一下,后面還有什么隨機化的輸入 什么的轻猖,我暫時用不到就先不寫了帆吻。但是也有一些用的到的卻沒在教程中涉及域那,比如說我們的單詞字典的構(gòu)造(初步想法是使用feature column)咙边,字母字典的構(gòu)造?還沒想好怎么寫次员,后續(xù)補上败许。