在上一章中詳細(xì)介紹了卷積神經(jīng)網(wǎng)絡(luò),并提到通過(guò)卷積神經(jīng)網(wǎng)絡(luò)給圖像識(shí)別技術(shù)帶來(lái)了突破性進(jìn)展。這一章將從另外一個(gè)維度來(lái)進(jìn)一步提升圖像識(shí)別的精度以及訓(xùn)練的速度。喜歡攝影的讀者都知道圖像的亮度、對(duì)比度等屬性對(duì)圖像的影響是非常大的,相同物體在不同亮度、對(duì)比度下差別非常大。然而在很多圖像識(shí)別問(wèn)題中,這些因素都不應(yīng)該影響最后的識(shí)別結(jié)果。所以本章將介紹如何對(duì)圖像數(shù)據(jù)進(jìn)行預(yù)處理使訓(xùn)練得到的神經(jīng)網(wǎng)絡(luò)模型盡可能小地減少預(yù)處理對(duì)于訓(xùn)練速度的影響,在本章中也將詳細(xì)地介紹TensorFlow中多線程處理輸入數(shù)據(jù)的解決方案。
本章將根據(jù)數(shù)據(jù)預(yù)處理的先后順序來(lái)組織不同的小節(jié)。
1.首先將介紹如何統(tǒng)一輸入數(shù)據(jù)的格式,使得在之后系統(tǒng)中可以更加方便地處理。來(lái)自實(shí)際問(wèn)題的數(shù)據(jù)往往有很多格式和屬性,這一節(jié)將介紹的TFRecord格式可以統(tǒng)一不同的原始數(shù)據(jù)格式,并更加有效地管理不同的屬性。
2.介紹如何對(duì)圖像數(shù)據(jù)進(jìn)行預(yù)處理。這一節(jié)將列舉TensorFlow支持的圖像處理函數(shù),并介紹如何使用這些處理方式來(lái)弱化與圖像識(shí)別無(wú)關(guān)的因素。復(fù)雜的圖像處理函數(shù)有可能降低訓(xùn)練的速度。
3.為了加速數(shù)據(jù)預(yù)處理過(guò)程,將完整地介紹TensorFlow多線程數(shù)據(jù)預(yù)處理流程。在這一節(jié)中將首先介紹TensorFlow中多線程和隊(duì)列的概念,這是TensorFlow多線程數(shù)據(jù)預(yù)處理的基本組成部分。
4.具體介紹數(shù)據(jù)預(yù)處理流程中的每個(gè)部分。
5.給出一個(gè)完整的多線程數(shù)據(jù)預(yù)處理流程圖和程序框架。
1.TFRecord輸入數(shù)據(jù)格式
TensorFlow提供了一種統(tǒng)一的格式來(lái)存儲(chǔ)數(shù)據(jù),這個(gè)格式就是TFRecord。
1.1TFRecord格式介紹
TFRecord文件中的數(shù)據(jù)都是通過(guò)tf.train.Example Protocol的格式存儲(chǔ)的。以下代碼給出了tf.train.Example的定義:

從以上代碼可以看出tf.train.Example的數(shù)據(jù)結(jié)構(gòu)是比較簡(jiǎn)潔的。tf.train.Example中包含了一個(gè)從屬性名稱到取值的字典。其中屬性名稱為一個(gè)字符串,屬性的取值可以為字符串,實(shí)數(shù)列表或者整數(shù)列表。比如將一張解碼前的圖像存為一個(gè)字符串,圖像所對(duì)應(yīng)的類別編號(hào)存為整數(shù)列表。下面給出一個(gè)使用TFRecord的具體樣例
1.2TFRecord樣例程序
本小節(jié)將給出具體的樣例程序來(lái)讀寫(xiě)TFRecord文件。下面的程序給出了如何將MNIST輸入數(shù)據(jù)轉(zhuǎn)化為T(mén)FRecord的格式:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import numpy as np
# 定義函數(shù)轉(zhuǎn)化變量類型。
def _int64_feature(value):
return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
def _bytes_feature(value):
return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
# 將數(shù)據(jù)轉(zhuǎn)化為tf.train.Example格式。
def _make_example(pixels, label, image):
image_raw = image.tostring()
example = tf.train.Example(features=tf.train.Features(feature={
'pixels': _int64_feature(pixels),
'label': _int64_feature(np.argmax(label)),
'image_raw': _bytes_feature(image_raw)
}))
return example
# 讀取mnist訓(xùn)練數(shù)據(jù)。
mnist = input_data.read_data_sets("../../datasets/MNIST_data",dtype=tf.uint8, one_hot=True)
images = mnist.train.images
labels = mnist.train.labels
pixels = images.shape[1]
num_examples = mnist.train.num_examples
# 輸出包含訓(xùn)練數(shù)據(jù)的TFRecord文件。
with tf.python_io.TFRecordWriter("output.tfrecords") as writer:
for index in range(num_examples):
example = _make_example(pixels, labels[index], images[index])
writer.write(example.SerializeToString())
print("TFRecord訓(xùn)練文件已保存。")
# 讀取mnist測(cè)試數(shù)據(jù)。
images_test = mnist.test.images
labels_test = mnist.test.labels
pixels_test = images_test.shape[1]
num_examples_test = mnist.test.num_examples
# 輸出包含測(cè)試數(shù)據(jù)的TFRecord文件。
with tf.python_io.TFRecordWriter("output_test.tfrecords") as writer:
for index in range(num_examples_test):
example = _make_example(
pixels_test, labels_test[index], images_test[index])
writer.write(example.SerializeToString())
print("TFRecord測(cè)試文件已保存。")
運(yùn)行代碼,如下:
Extracting ../../datasets/MNIST_data/train-images-idx3-ubyte.gz
Extracting ../../datasets/MNIST_data/train-labels-idx1-ubyte.gz
Extracting ../../datasets/MNIST_data/t10k-images-idx3-ubyte.gz
Extracting ../../datasets/MNIST_data/t10k-labels-idx1-ubyte.gz
TFRecord訓(xùn)練文件已保存。
TFRecord測(cè)試文件已保存。
以上程序可以實(shí)現(xiàn)將MNIST數(shù)據(jù)集中所有的訓(xùn)練數(shù)據(jù)存儲(chǔ)到一個(gè)TFRecord文件中。當(dāng)數(shù)據(jù)量較大時(shí),也可以將數(shù)據(jù)寫(xiě)入多個(gè)TFRecord文件。TensorFlow對(duì)從文件列表中讀取數(shù)據(jù)提供了很好的支持,以下程序給出了如何讀取TFRecord文件中的數(shù)據(jù):
# 讀取文件。
reader = tf.TFRecordReader()
filename_queue = tf.train.string_input_producer(["output.tfrecords"])
_,serialized_example = reader.read(filename_queue)
# 解析讀取的樣例。
features = tf.parse_single_example(
serialized_example,
features={
'image_raw':tf.FixedLenFeature([],tf.string),
'pixels':tf.FixedLenFeature([],tf.int64),
'label':tf.FixedLenFeature([],tf.int64)
})
images = tf.decode_raw(features['image_raw'],tf.uint8)
labels = tf.cast(features['label'],tf.int32)
pixels = tf.cast(features['pixels'],tf.int32)
sess = tf.Session()
# 啟動(dòng)多線程處理輸入數(shù)據(jù)。
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess,coord=coord)
for i in range(10):
image, label, pixel = sess.run([images, labels, pixels])
2.圖像數(shù)據(jù)處理
在之前的幾章中多次使用到了圖像識(shí)別數(shù)據(jù)集。然而在之前的章節(jié)中都是直接使用圖像原始的像素矩陣。這一節(jié)將介紹圖像預(yù)處理過(guò)程。通過(guò)對(duì)圖像的預(yù)處理,可以盡量避免模型受到無(wú)關(guān)因素的影響。在大部分圖像識(shí)別問(wèn)題中,通過(guò)圖像預(yù)處理過(guò)程可以提高模型的準(zhǔn)確率。在下一小節(jié)中將介紹TensorFlow提供的主要圖像處理函數(shù),并給出具體圖像在處理前和處理后的變化。然后將給出一個(gè)完整的圖像處理流程
2.1TensorFlow圖像處理函數(shù)
TensorFlow提供了幾類圖像處理函數(shù),在本小節(jié)中將一一介紹這些圖像處理函數(shù)
圖像編碼處理
在之前的章節(jié)中提到一張RGB色彩模式的圖像可以看成一個(gè)三維矩陣,矩陣中的每一個(gè)數(shù)表示了圖像上不同位置,不同顏色的亮度。然而圖像在存儲(chǔ)時(shí)不是直接記錄這些矩陣中的數(shù)字,而是記錄經(jīng)過(guò)壓縮編碼之后的結(jié)果。所以要將一張圖像還原成一個(gè)三維矩陣,需要解碼的過(guò)程。TensorFlow提供了對(duì)jpeg和png格圖像的編碼/解碼函數(shù)。以下代碼示范了如何使用TensorFlow中對(duì)jpeg格式的編碼/解碼函數(shù):
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
# 讀取圖片
image_raw_data = tf.gfile.FastGFile("C:\\Users\\1003342\\Desktop\\work\\picture\\cat.jpg",'rb').read()
with tf.Session() as sess:
img_data = tf.image.decode_jpeg(image_raw_data)
# 輸出解碼之后的三維矩陣。
print(img_data.eval())
img_data.set_shape([1797, 2673, 3])
print(img_data.get_shape())
運(yùn)行代碼,得到部分結(jié)果如下:
[[[162 161 140]
[162 162 138]
[161 161 137]
...,
[106 140 46]
[101 137 47]
[102 141 52]]
[[164 162 139]
[163 161 136]
[163 161 138]
...,
打印圖片代碼如下:
with tf.Session() as sess:
plt.imshow(img_data.eval())
plt.show()
運(yùn)行結(jié)果如下:

圖像大小調(diào)整
一般來(lái)說(shuō),網(wǎng)絡(luò)上獲取的圖像大小是不固定,但神經(jīng)網(wǎng)絡(luò)輸入節(jié)點(diǎn)的個(gè)數(shù)是固定的。所以在將圖像的像素作為輸入提供給神經(jīng)網(wǎng)絡(luò)之前,需要先將圖像的大小統(tǒng)一。這就是圖像大小調(diào)整需要完成的任務(wù)。圖像大小調(diào)整有兩種方式,第一種是通過(guò)算法使得新的圖像盡量保存原始圖像上的所有信息。TensorFlow提供了四種不同的方法,并且將它們封裝到了tf.image.resize_images函數(shù)。

下表給出了tf.image.resize_images函數(shù)的method參數(shù)取值對(duì)應(yīng)的圖像大小調(diào)整算法:

下圖對(duì)比了不同大小調(diào)整算法得到的結(jié)果:

從上圖中可以看出,不同算法調(diào)整出來(lái)的結(jié)果會(huì)有細(xì)微差別,但不會(huì)相差太遠(yuǎn)。除了將整張圖像信息完整保存,TensorFlow還提供了API對(duì)圖像進(jìn)行剪裁或者填充。以下代碼展示了通過(guò)tf.image.resize_image_with_crop_or_pad函數(shù)來(lái)調(diào)整圖像大小的功能:
with tf.Session() as sess:
croped = tf.image.resize_image_with_crop_or_pad(img_data, 1000, 1000)
padded = tf.image.resize_image_with_crop_or_pad(img_data, 3000, 3000)
plt.imshow(croped.eval())
plt.show()
plt.imshow(padded.eval())
plt.show()
運(yùn)行代碼,得到如下圖所示的結(jié)果:

TensorFlow還支持通過(guò)比例調(diào)整圖像大小,以下代碼給出了一個(gè)樣例:
with tf.Session() as sess:
central_cropped = tf.image.central_crop(img_data, 0.5)
plt.imshow(central_cropped.eval())
plt.show()
運(yùn)行代碼,如下:

上面介紹的圖像剪裁函數(shù)都是截取或者填充圖像中間的部分。TensorFlow也提供了tf.image.crop_to_bounding_box函數(shù)和tf.image.pad_to_bounding_box函數(shù)來(lái)剪裁或者填充給定區(qū)域的圖像。這兩個(gè)函數(shù)都要求給出的尺寸滿足一定的要求,否則程序會(huì)報(bào)錯(cuò)。比如在使用tf.image.crop_to_bounding_box函數(shù)時(shí),TensorFlow要求提供的圖像尺寸要大于目標(biāo)尺寸,也就是要求原始圖像能夠剪裁出目標(biāo)圖像的大小。這里就不再給出每個(gè)函數(shù)的具體樣例,有興趣的讀者可以自行參考TensorFlow的API文檔。
圖像翻轉(zhuǎn)
TensorFlow提供了一些函數(shù)來(lái)支持對(duì)圖像的翻轉(zhuǎn)。以下代碼實(shí)現(xiàn)了將圖像上下反轉(zhuǎn)、左右翻轉(zhuǎn)以及沿對(duì)角線翻轉(zhuǎn)的功能:
with tf.Session() as sess:
# 上下翻轉(zhuǎn)
flipped1 = tf.image.flip_up_down(img_data)
plt.imshow(flipped1.eval())
plt.show()
# 左右翻轉(zhuǎn)
flipped2 = tf.image.flip_left_right(img_data)
plt.imshow(flipped2.eval())
plt.show()
#對(duì)角線翻轉(zhuǎn)
transposed = tf.image.transpose_image(img_data)
plt.imshow(transposed.eval())
plt.show()
# 以一定概率上下翻轉(zhuǎn)圖片。
flipped3 = tf.image.random_flip_up_down(img_data)
plt.imshow(flipped3.eval())
plt.show()
# 以一定概率左右翻轉(zhuǎn)圖片。
flipped4 = tf.image.random_flip_left_right(img_data)
plt.imshow(flipped4.eval())
plt.show()
運(yùn)行代碼,得到如下結(jié)果:

在很多圖像識(shí)別問(wèn)題中,圖像的翻轉(zhuǎn)不會(huì)影響到識(shí)別的結(jié)果。于是在訓(xùn)練圖像識(shí)別的神經(jīng)網(wǎng)絡(luò)模型時(shí),可以隨機(jī)地翻轉(zhuǎn)訓(xùn)練圖像,這樣訓(xùn)練得到的模型可以識(shí)別不同角度的實(shí)體。比如假設(shè)在訓(xùn)練數(shù)據(jù)中所有的貓頭都是向右的,那么訓(xùn)練出來(lái)的模型就無(wú)法很好的識(shí)別貓頭向左的貓。雖然這個(gè)問(wèn)題可以通過(guò)收集更多的訓(xùn)練數(shù)據(jù)來(lái)解決,但是通過(guò)隨機(jī)翻轉(zhuǎn)訓(xùn)練圖像的方式可以在零成本的情況下很大程度地緩解該問(wèn)題。所以隨即翻轉(zhuǎn)訓(xùn)練圖像是一種很常用的圖像預(yù)處理方式。TensorFlow提供了方便的API完成隨機(jī)圖像翻轉(zhuǎn)的過(guò)程。如上代碼中最后的幾行就是圖像翻轉(zhuǎn)的樣例代碼。
圖像色彩調(diào)整
和圖像翻轉(zhuǎn)類似,調(diào)整圖像的亮度、對(duì)比度、飽和度和色相在很多圖像識(shí)別應(yīng)用中都不會(huì)影響到識(shí)別結(jié)果。所以在訓(xùn)練神經(jīng)網(wǎng)絡(luò)模型時(shí),可以隨機(jī)調(diào)整訓(xùn)練圖像的這些屬性,從而使得訓(xùn)練得到的模型盡可能小地受到無(wú)關(guān)因素的影響。TensorFlow提供了調(diào)整這些色彩相關(guān)屬性的API。以下代碼顯示了如何修改圖像的亮度:
with tf.Session() as sess:
# 在進(jìn)行一系列圖片調(diào)整前,先將圖片轉(zhuǎn)換為實(shí)數(shù)形式,有利于保持計(jì)算精度。
image_float = tf.image.convert_image_dtype(img_data, tf.float32)
# 將圖片的亮度-0.5。
#adjusted = tf.image.adjust_brightness(image_float, -0.5)
# 將圖片的亮度-0.5
#adjusted = tf.image.adjust_brightness(image_float, 0.5)
# 在[-max_delta, max_delta)的范圍隨機(jī)調(diào)整圖片的亮度。
adjusted = tf.image.random_brightness(image_float, max_delta=0.5)
# 將圖片的對(duì)比度-5
#adjusted = tf.image.adjust_contrast(image_float, -5)
# 將圖片的對(duì)比度+5
#adjusted = tf.image.adjust_contrast(image_float, 5)
# 在[lower, upper]的范圍隨機(jī)調(diào)整圖的對(duì)比度。
#adjusted = tf.image.random_contrast(image_float, lower, upper)
# 在最終輸出前,將實(shí)數(shù)取值截取到0-1范圍內(nèi)。
adjusted = tf.clip_by_value(adjusted, 0.0, 1.0)
plt.imshow(adjusted.eval())
plt.show()
依次運(yùn)行每段代碼,得到如下結(jié)果:





以下代碼顯示了如何調(diào)整圖像的飽和度:
with tf.Session() as sess:
# 在進(jìn)行一系列圖片調(diào)整前,先將圖片轉(zhuǎn)換為實(shí)數(shù)形式,有利于保持計(jì)算精度。
image_float = tf.image.convert_image_dtype(img_data, tf.float32)
adjusted = tf.image.adjust_hue(image_float, 0.1)
#adjusted = tf.image.adjust_hue(image_float, 0.3)
#adjusted = tf.image.adjust_hue(image_float, 0.6)
#adjusted = tf.image.adjust_hue(image_float, 0.9)
# 在[-max_delta, max_delta]的范圍隨機(jī)調(diào)整圖片的色相。max_delta的取值在[0, 0.5]之間。
#adjusted = tf.image.random_hue(image_float, max_delta)
# 將圖片的飽和度-5。
#adjusted = tf.image.adjust_saturation(image_float, -5)
# 將圖片的飽和度+5。
#adjusted = tf.image.adjust_saturation(image_float, 5)
# 在[lower, upper]的范圍隨機(jī)調(diào)整圖的飽和度。
#adjusted = tf.image.random_saturation(image_float, lower, upper)
# 將代表一張圖片的三維矩陣中的數(shù)字均值變?yōu)?,方差變?yōu)?。
#adjusted = tf.image.per_image_whitening(image_float)
# 在最終輸出前,將實(shí)數(shù)取值截取到0-1范圍內(nèi)。
adjusted = tf.clip_by_value(adjusted, 0.0, 1.0)
plt.imshow(adjusted.eval())
plt.show()
依次運(yùn)行每行代碼,得到如下圖結(jié)果:








除了調(diào)整圖像的亮度、對(duì)比度、飽和度和色相,TensorFlow還提供API來(lái)完成圖像標(biāo)準(zhǔn)化的過(guò)程。這個(gè)操作就是將圖像上的亮度均值變?yōu)?,方差變?yōu)?.以下代碼實(shí)現(xiàn)了這個(gè)功能:
# 將代表一張圖片的三維矩陣中的數(shù)字均值變?yōu)?,方差變?yōu)?。
#adjusted = tf.image.per_image_whitening(image_float)
處理標(biāo)注框
在很多圖像識(shí)別的數(shù)據(jù)集中,圖像中需要關(guān)注的物體通常會(huì)被標(biāo)注框圈出來(lái)。TensorFlow提供了一些工具來(lái)處理標(biāo)注框。下面這段代碼展示了如何通過(guò)tf.image.draw_bounding_boxes函數(shù)在圖像中加入標(biāo)注框:
with tf.Session() as sess:
boxes = tf.constant([[[0.05, 0.05, 0.9, 0.7], [0.35, 0.47, 0.5, 0.56]]])
# sample_distorted_bounding_box要求輸入圖片必須是實(shí)數(shù)類型。
image_float = tf.image.convert_image_dtype(img_data, tf.float32)
begin, size, bbox_for_draw = tf.image.sample_distorted_bounding_box(
tf.shape(image_float), bounding_boxes=boxes, min_object_covered=0.4)
# 截取后的圖片
distorted_image = tf.slice(image_float, begin, size)
plt.imshow(distorted_image.eval())
plt.show()
# 在原圖上用標(biāo)注框畫(huà)出截取的范圍。由于原圖的分辨率較大(2673x1797),生成的標(biāo)注框
# 在Jupyter Notebook上通常因邊框過(guò)細(xì)而無(wú)法分辨,這里為了演示方便先縮小分辨率。
image_small = tf.image.resize_images(image_float, [180, 267], method=0)
batchced_img = tf.expand_dims(image_small, 0)
image_with_box = tf.image.draw_bounding_boxes(batchced_img, bbox_for_draw)
print(bbox_for_draw.eval())
plt.imshow(image_with_box[0].eval())
plt.show()
運(yùn)行代碼,得到結(jié)果如下圖:

2.2圖像預(yù)處理完整樣例
在上一小節(jié)詳細(xì)講解了TensorFlow提供的主要的圖像處理函數(shù)。在解決真實(shí)的圖像識(shí)別問(wèn)題時(shí),一般會(huì)同時(shí)使用多種處理方法。這一小節(jié)將給出一個(gè)完整的樣例程序展示如何將不同的圖像處理函數(shù)結(jié)合成一個(gè)完成的圖像預(yù)處理流程。以下TensorFlow程序完成了從圖像片段截取,到圖像大小調(diào)整再到圖像翻轉(zhuǎn)及色彩調(diào)整的整個(gè)圖像預(yù)處理過(guò)程:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
- 隨機(jī)調(diào)整圖片的色彩,定義兩種順序。
def distort_color(image, color_ordering=0):
if color_ordering == 0:
image = tf.image.random_brightness(image, max_delta=32./255.)
image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
image = tf.image.random_hue(image, max_delta=0.2)
image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
else:
image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
image = tf.image.random_brightness(image, max_delta=32./255.)
image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
image = tf.image.random_hue(image, max_delta=0.2)
return tf.clip_by_value(image, 0.0, 1.0)
- 對(duì)圖片進(jìn)行預(yù)處理,將圖片轉(zhuǎn)化成神經(jīng)網(wǎng)絡(luò)的輸入層數(shù)據(jù)。
def preprocess_for_train(image, height, width, bbox):
# 查看是否存在標(biāo)注框。
if bbox is None:
bbox = tf.constant([0.0, 0.0, 1.0, 1.0], dtype=tf.float32, shape=[1, 1, 4])
if image.dtype != tf.float32:
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
# 隨機(jī)的截取圖片中一個(gè)塊。
bbox_begin, bbox_size, _ = tf.image.sample_distorted_bounding_box(
tf.shape(image), bounding_boxes=bbox)
bbox_begin, bbox_size, _ = tf.image.sample_distorted_bounding_box(
tf.shape(image), bounding_boxes=bbox)
distorted_image = tf.slice(image, bbox_begin, bbox_size)
# 將隨機(jī)截取的圖片調(diào)整為神經(jīng)網(wǎng)絡(luò)輸入層的大小。
distorted_image = tf.image.resize_images(distorted_image, [height, width], method=np.random.randint(4))
distorted_image = tf.image.random_flip_left_right(distorted_image)
distorted_image = distort_color(distorted_image, np.random.randint(2))
return distorted_image
- 讀取圖片。
image_raw_data = tf.gfile.FastGFile("../../datasets/cat.jpg", "r").read()
with tf.Session() as sess:
img_data = tf.image.decode_jpeg(image_raw_data)
boxes = tf.constant([[[0.05, 0.05, 0.9, 0.7], [0.35, 0.47, 0.5, 0.56]]])
for i in range(9):
result = preprocess_for_train(img_data, 299, 299, boxes)
plt.imshow(result.eval())
plt.show()
運(yùn)行代碼,依次彈出如下圖片:









從以上程序可以看到,通過(guò)一張訓(xùn)練圖像衍生出很多訓(xùn)練樣本。通過(guò)將訓(xùn)練圖像進(jìn)行預(yù)處理,訓(xùn)練得到的神經(jīng)網(wǎng)絡(luò)模型可以識(shí)別不同大小、方位、色彩等方面的實(shí)體。
3.多線程輸入數(shù)據(jù)處理框架
在上一節(jié)中介紹了使用TensorFlow對(duì)圖像進(jìn)行預(yù)處理的方法。雖然使用這些圖像數(shù)據(jù)預(yù)處理的方法可以減小無(wú)關(guān)因素對(duì)圖像識(shí)別模型效果的影響,但這些復(fù)雜的預(yù)處理過(guò)程也會(huì)減慢整個(gè)訓(xùn)練過(guò)程。為了避免圖像預(yù)處理成為神經(jīng)網(wǎng)絡(luò)模型訓(xùn)練效率的瓶頸,TensorFlow提供了一套多線程處理輸入數(shù)據(jù)的框架。在本節(jié)中將詳細(xì)介紹這個(gè)框架。下圖總結(jié)了一個(gè)經(jīng)典的輸入數(shù)據(jù)處理的流程,在以下的各個(gè)小節(jié)中,將依次介紹這個(gè)流程的不同部分。

下面我們將介紹:
1.TensorFlow中隊(duì)列的概念。在TensorFlow中,隊(duì)列不僅是一種數(shù)據(jù)結(jié)構(gòu),它更提供了多線程機(jī)制。隊(duì)列也是TensorFlow多線程輸入數(shù)據(jù)處理框架的基礎(chǔ)。
2.TensorFlow實(shí)現(xiàn)上圖中的前三步。TensorFlow提供了tf.train.string_input_producer函數(shù)來(lái)有效管理原始輸入文件列表。重點(diǎn)介紹如何使用這個(gè)函數(shù)。上圖中數(shù)據(jù)預(yù)處理的部分已經(jīng)在之前有過(guò)詳細(xì)介紹,本節(jié)不再重復(fù)。接著下一小節(jié)將介紹4.上圖中最后一個(gè)流程。這個(gè)流程將處理好的單個(gè)訓(xùn)練數(shù)據(jù)整理成訓(xùn)練數(shù)據(jù)batch,這些batch就可以作為神經(jīng)網(wǎng)絡(luò)的輸入。
5.介紹tf.train.shuffle_batch_join和tf.train.shuffle_batch函數(shù),并比較不同函數(shù)的多線程并行方式
6.最后將給出一個(gè)完整的TensorFlow程序來(lái)展示整個(gè)輸入數(shù)據(jù)處理框架。
3.1隊(duì)列與多線程
在TensorFlow中,隊(duì)列和變量類似,都是計(jì)算圖上有狀態(tài)的節(jié)點(diǎn)。其他的計(jì)算節(jié)點(diǎn)可以修改它們的狀態(tài)。對(duì)于變量,可以通過(guò)賦值操作修改變量的取值。對(duì)于隊(duì)列,修改隊(duì)列狀態(tài)的操作主要有Enqueue、EnqueneMany和Dequeue.以下程序展示了如何使用這些函數(shù)來(lái)操作一個(gè)隊(duì)列:
# 創(chuàng)建一個(gè)先進(jìn)先出的隊(duì)列,指定隊(duì)列中最多可以保存兩個(gè)元素,并指定類型為整數(shù)
q = tf.FIFOQueue(2, "int32")
# 使用enqueue_many函數(shù)來(lái)初始化隊(duì)列中的元素。和變量初始化類似,在使用隊(duì)列之前需要明確的調(diào)用這個(gè)初始化過(guò)程。
init = q.enqueue_many(([0, 10],))
# 使用Dequeue函數(shù)將隊(duì)列中的第一個(gè)元素出隊(duì)列。這個(gè)元素的值將被存在變量x中。
x = q.dequeue()
# 將得到的值加1.
y = x + 1
# 將加1后的值在重新加入隊(duì)列
q_inc = q.enqueue([y])
with tf.Session() as sess:
# 運(yùn)行初始化隊(duì)列的操作。
init.run()
for _ in range(5):
# 運(yùn)行q_inc將執(zhí)行數(shù)據(jù)出隊(duì)列、出隊(duì)的元素+1、重新加入隊(duì)列的整個(gè)過(guò)程。
v, _ = sess.run([x, q_inc])
# 打印出隊(duì)元素的取值
print v
運(yùn)行代碼,結(jié)果如下:
0
10
1
11
2
結(jié)果分析:
隊(duì)列開(kāi)始有[0,10]兩個(gè)元素,第一個(gè)出隊(duì)的為0,加1之后再次入隊(duì)得到的隊(duì)列為[10,1];第二次出隊(duì)的為10,加1之后入隊(duì)的為11,得到的隊(duì)列為[1,11];以此類推,最后得到的輸出為以上結(jié)果。
TensorFlow中提供了FIFOQueue和RandomShuffleQueue兩種隊(duì)列。在上面的程序中已經(jīng)展示了如何使用FIFOQue,它的實(shí)現(xiàn)的是一個(gè)先進(jìn)先出隊(duì)列。RandomShffleQueue會(huì)將隊(duì)列中的元素打亂,每次出隊(duì)列操作得到的是從當(dāng)前隊(duì)列所有元素中隨機(jī)選擇的一個(gè)。在訓(xùn)練神經(jīng)網(wǎng)絡(luò)時(shí)希望每次使用的訓(xùn)練數(shù)據(jù)盡量隨機(jī),RandomShuffleQueue就提供了這樣的功能。
在TensorFlow中,隊(duì)列不僅僅是一種數(shù)據(jù)結(jié)構(gòu),還是異步計(jì)算張量取值的一個(gè)重要機(jī)制。比如多個(gè)線程可以同時(shí)向一個(gè)隊(duì)列中寫(xiě)元素,或者同時(shí)讀取一個(gè)隊(duì)列中的元素。在后面的小節(jié)中將具體介紹TensorFlow是如何利用隊(duì)列來(lái)實(shí)現(xiàn)多線程輸入數(shù)據(jù)處理的。在本小節(jié)之后的內(nèi)容中將先介紹TensorFlow提供的輔助函數(shù)來(lái)更好地協(xié)同不同的線程。
TensorFlow提供了tf.Coordinator和tf.QueueRunner兩個(gè)類來(lái)完成多線程協(xié)同的功能。tf.Coordinator主要用于協(xié)同多個(gè)線程一起停止,并提供了should_stop、request_stop和join三個(gè)函數(shù)。在啟動(dòng)線程之前,需要先聲明一個(gè)tf.Coordinator類,并將這個(gè)類傳入每一個(gè)創(chuàng)建的線程中。啟動(dòng)的線程需要一直查詢tf.Coordinator類中提供的should_stop函數(shù),當(dāng)這個(gè)函數(shù)的返回值為T(mén)rue時(shí),則當(dāng)前線程也需要退出。每一個(gè)啟動(dòng)的線程都可以通過(guò)調(diào)用request_stop函數(shù)來(lái)通知其他線程退出。當(dāng)某一個(gè)線程調(diào)用request_stop函數(shù)之后,should_stop函數(shù)的返回值將被設(shè)置為T(mén)rue,這樣其他的線程就可以同時(shí)終止了。以下程序展示了如何使用tf.Coordinator:
import tensorflow as tf
import numpy as np
import threading
import time
# 線程中運(yùn)行的程序,這個(gè)程序每隔1秒判斷是否需要停止并打印自己的ID。
def MyLoop(coord, worker_id):
# 使用tf.Coordinator類提供的協(xié)同工具判斷當(dāng)前線程是否需要停止
while not coord.should_stop():
# 隨機(jī)停止所有的線程
if np.random.rand() <0.1:
print("stoping from id:{}".format(worker_id))
# 調(diào)用coord.request_stop()函數(shù)來(lái)通知其他線程停止
coord.request_stop()
else:
# 打印當(dāng)前線程的Id
print("working on id:{}".format(worker_id))
time.sleep(1)
# 聲明一個(gè)tf.train.Coordinator類來(lái)協(xié)同多個(gè)線程
coord = tf.train.Coordinator()
# 聲明創(chuàng)建5個(gè)線程
threads = [threading.Thread(target=MyLoop, args=(coord,i,)) for i in range(5)]
# 啟動(dòng)所有的線程
for t in threads:t.start()
# 等待所有線程退出
coord.join(threads)
運(yùn)行代碼,得到結(jié)果如下:
working on id:0
working on id:1
working on id:2
stoping from id:4
working on id:3
當(dāng)所有線程啟動(dòng)之后,每個(gè)線程會(huì)打印各自的ID,于是前面4行打印出了它們的ID。然后在暫停1秒之后,所有線程又開(kāi)始第二遍打印ID。在這個(gè)時(shí)候有一個(gè)線程退出的條件達(dá)到,于是調(diào)用了coord.request_stop函數(shù)來(lái)停止所有其他的線程。然而在打印Stoping from id:4之后可以看到有線程仍然在輸出。這是因?yàn)檫@些線程已經(jīng)執(zhí)行完coord.should_stop的判斷,于是仍然會(huì)繼續(xù)輸出自己的ID。但在下一輪判斷是否需要停止時(shí)將退出線程。于是在打印一次ID之后就不會(huì)再有輸出了。
tf.QueueRunner主要用于啟動(dòng)多個(gè)線程來(lái)操作同一個(gè)隊(duì)列,啟動(dòng)的這些線程可以通過(guò)上面介紹的tf.Coordinator類來(lái)統(tǒng)一管理。以下代碼展示了如何使用tf.QueueRunner和tf.Coordinator來(lái)管理多線程隊(duì)列操作:
1.定義隊(duì)列及其操作
import tensorflow as tf
# 聲明一個(gè)先進(jìn)先出的隊(duì)列,隊(duì)列中最多100個(gè)元素,類型為實(shí)數(shù)
queue = tf.FIFOQueue(100,"float")
# 定義隊(duì)列的入隊(duì)操作
enqueue_op = queue.enqueue([tf.random_normal([1])])
# 使用tf.train.QueueRunner來(lái)創(chuàng)建多個(gè)線程運(yùn)行隊(duì)列的入隊(duì)操作。
# tf.train.QueueRunner的第一個(gè)參數(shù)給出了被操作的隊(duì)列,[enqueue_op]*5表示需要啟動(dòng)5個(gè)線程,
# 每個(gè)縣城中運(yùn)行的是enqueue_op操作
qr = tf.train.QueueRunner(queue, [enqueue_op] * 5)
# 將定義過(guò)的QueueRunner加入TensorFlow計(jì)算圖上指定的集合。
tf.train.add_queue_runner(qr)
# 定義出隊(duì)操作
out_tensor = queue.dequeue()
2.啟動(dòng)線程
with tf.Session() as sess:
# 使用tf.train.Coordinator()來(lái)協(xié)同啟動(dòng)的線程
coord = tf.train.Coordinator()
# 啟動(dòng)所有線程
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
# 獲取隊(duì)列中的值
for _ in range(3): print(sess.run(out_tensor)[0])
# 使用tf.train.Coordinator來(lái)停止所有的線程
coord.request_stop()
coord.join(threads)
運(yùn)行代碼,結(jié)果如下:
-0.494588
-0.31537
0.153534
上面的程序?qū)?dòng)五個(gè)線程來(lái)執(zhí)行隊(duì)列入隊(duì)的操作,其中每一個(gè)線程都是將隨機(jī)數(shù)寫(xiě)入隊(duì)列。于是在每次運(yùn)行出隊(duì)操作時(shí),可以得到一個(gè)隨機(jī)數(shù)。
3.2輸入文件隊(duì)列
本小節(jié)將介紹如何使用TensorFlow中的隊(duì)列管理輸入文件列表。在這一小節(jié)中,假設(shè)所有的輸入數(shù)據(jù)都已經(jīng)整理成TFRecord格式。雖然一個(gè)TFRecord文件中可以存儲(chǔ)多個(gè)訓(xùn)練樣例,但是當(dāng)訓(xùn)練數(shù)據(jù)量較大時(shí),可以將數(shù)據(jù)分成多個(gè)TFRecord文件來(lái)提高處理效率。TensorFlow提供了tf.train.match_filenames_once函數(shù)來(lái)獲取符合一個(gè)正則表達(dá)式的所有文件,得到的文件列表可以通過(guò)tf.train.string_input_producer函數(shù)進(jìn)行有效的管理。
tf.train.string_input_producer函數(shù)會(huì)使用初始化時(shí)提供的文件列表創(chuàng)建一個(gè)輸入隊(duì)列,輸入隊(duì)列中原始的像素為文件列表中的所有文件。如上一節(jié)中的樣例代碼所示,創(chuàng)建好的輸入隊(duì)列可以作為文件讀取函數(shù)的參數(shù)。每次調(diào)用文件讀取函數(shù)時(shí),該函數(shù)會(huì)先判斷當(dāng)前是否已有打開(kāi)的文件可讀,如果沒(méi)有或者打開(kāi)的文件已經(jīng)讀完,這個(gè)函數(shù)會(huì)從輸入隊(duì)列中出隊(duì)一個(gè)文件并從這個(gè)文件中讀取數(shù)據(jù)。
通過(guò)設(shè)置shuffle參數(shù),tf.train.string_input_producer函數(shù)支持隨機(jī)打亂文件列表中文件出隊(duì)順序。當(dāng)shuffle參數(shù)為T(mén)rue時(shí),文件在加入隊(duì)列之前會(huì)被打亂順序,所以出隊(duì)的順序也是隨機(jī)的。隨機(jī)打亂文件順序以及加入輸入隊(duì)列的過(guò)程會(huì)跑在一個(gè)單獨(dú)的線程上,這樣不會(huì)影響獲取文件的速度。tf.train.string_input_producer生成的輸入隊(duì)列可以同時(shí)被多個(gè)文件讀取線程操作,而且輸入隊(duì)列會(huì)將隊(duì)列中的文件均勻地分給不同的線程,不出現(xiàn)有些文件被處理過(guò)多次而有些文件還沒(méi)有被處理過(guò)的情況。
當(dāng)一個(gè)輸入隊(duì)列中的所有文件都被處理完后,它會(huì)將初始化時(shí)提供的文件列表中的文件全部重新加入隊(duì)列。tf.train.string_input_producer函數(shù)可以設(shè)置num_epochs參數(shù)來(lái)限制加載初始文件列表的最大輪數(shù)。當(dāng)所有文件都已經(jīng)被使用了設(shè)定的輪數(shù)后,如果繼續(xù)嘗試讀取新的文件,輸入隊(duì)列會(huì)報(bào)OutOfRange的錯(cuò)誤。在測(cè)試神經(jīng)網(wǎng)絡(luò)模型時(shí),因?yàn)樗袦y(cè)試數(shù)據(jù)只需要使用一次,所以可以將num_epochs參數(shù)設(shè)置為1.這樣在計(jì)算完一輪之后程序?qū)⒆詣?dòng)停止。在展示tf.train.match_filenames_once和tf.train.string_input_producer函數(shù)的使用方法之前,下面先給出一個(gè)簡(jiǎn)單的程序來(lái)生成樣例數(shù)據(jù):
import tensorflow as tf
# 創(chuàng)建TFRecord文件的幫助函數(shù)
def _int64_feature(value):
return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
# 模擬海量數(shù)據(jù)情況下將數(shù)據(jù)寫(xiě)入不同的文件。num_shards定義了總共寫(xiě)入多少文件
# instances_per_shard定義了每個(gè)文件中有多少個(gè)數(shù)據(jù)
num_shards = 2
instances_per_shard = 2
for i in range(num_shards):
# 將數(shù)據(jù)分為多個(gè)文件時(shí),可以將不同文件以類似0000n-of-0000m的后綴區(qū)分。其中m表示了數(shù)據(jù)總共被存在了多少個(gè)文件中,
# n表示當(dāng)前文件的編號(hào)。式樣的方式既方便了通過(guò)正則表達(dá)式獲取文件列表,又在文件名中加入了更多的信息。
filename = ('data.tfrecords-%.5d-of-%.5d' % (i, num_shards))
# 將Example結(jié)構(gòu)寫(xiě)入TFRecord文件。
writer = tf.python_io.TFRecordWriter(filename)
for j in range(instances_per_shard):
# Example結(jié)構(gòu)僅包含當(dāng)前樣例屬于第幾個(gè)文件以及是當(dāng)前文件的第幾個(gè)樣本。
example = tf.train.Example(features=tf.train.Features(feature={
'i': _int64_feature(i),
'j': _int64_feature(j)}))
writer.write(example.SerializeToString())
writer.close()
程序運(yùn)行之后,在指定的目錄下將生成兩個(gè)文件,如下圖:

每一個(gè)文件中存儲(chǔ)了兩個(gè)樣例。在生成了樣例數(shù)據(jù)之后,以下代碼展示了tf.train.match_filenames_once函數(shù)和tf.train.string_input_producer函數(shù)的使用方法。
# 使用tf.train.match_filenames_once函數(shù)獲取文件列表
files = tf.train.match_filenames_once("data.tfrecords-*")
# 通過(guò)tf.train.string_input_producer函數(shù)創(chuàng)建輸入隊(duì)列,輸入隊(duì)列中的文件列表為上面步驟獲取的文件列表。
# 這里shuffle參數(shù)設(shè)為false來(lái)避免隨機(jī)打亂讀文件的順序。但一般在解決真實(shí)問(wèn)題時(shí),會(huì)將shuffle參數(shù)設(shè)置為T(mén)rue。
filename_queue = tf.train.string_input_producer(files, shuffle=False)
# 如上一節(jié)所示讀取并解析一個(gè)樣本
reader = tf.TFRecordReader()
_, serialized_example = reader.read(filename_queue)
features = tf.parse_single_example(
serialized_example,
features={
'i': tf.FixedLenFeature([], tf.int64),
'j': tf.FixedLenFeature([], tf.int64),
})
with tf.Session() as sess:
# 雖然在本段程序中沒(méi)有聲明任何變量,但使用tf.train.match_filenames_once函數(shù)時(shí)需要初始化一些變量。
sess.run([tf.global_variables_initializer(), tf.local_variables_initializer()])
print(sess.run(files))
# 聲明tf.train.Coordinator類來(lái)協(xié)同不同線程,并啟動(dòng)線程。
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
# 多次執(zhí)行獲取數(shù)據(jù)的操作
for i in range(6):
print(sess.run([features['i'], features['j']]))
coord.request_stop()
coord.join(threads)
運(yùn)行代碼,打印輸出如下:

在不打亂文件列表的情況下,會(huì)依次讀出樣例數(shù)據(jù)中的每一個(gè)樣例。而且當(dāng)所有樣例都被讀完之后,程序會(huì)自動(dòng)從頭開(kāi)始。如果限制num_epochs為1,那么程序?qū)?huì)報(bào)錯(cuò),如下圖所示:

3.3組合訓(xùn)練數(shù)據(jù)(batching)
在上一小節(jié)中已經(jīng)介紹了如何從文件列表中讀取單個(gè)樣例,將這些單個(gè)樣例通過(guò)上節(jié)介紹的預(yù)處理方法進(jìn)行處理,就可以得到提供給神經(jīng)網(wǎng)絡(luò)輸入層的訓(xùn)練數(shù)據(jù)了。前面介紹過(guò),將多個(gè)輸入樣例組織成一個(gè)batch可以提高模型訓(xùn)練的效率。所以在得到單個(gè)樣例的與處理結(jié)果之后,還需要將它們組織成batch,然后再提供給神經(jīng)網(wǎng)絡(luò)的輸入層。TensorFlow提供了tf.train.batch和tf.train.shuffle_batch函數(shù)來(lái)將單個(gè)的樣例組織成batch的形式輸出。這兩個(gè)函數(shù)都會(huì)生成一個(gè)隊(duì)列,隊(duì)列的入隊(duì)操作是生成單個(gè)樣例的方法,而每次出隊(duì)得到的是一個(gè)batch的樣例。它們唯一的區(qū)別在于是否會(huì)將數(shù)據(jù)順序打亂。以下代碼展示了這兩個(gè)函數(shù)的使用方法:
# 讀取并解析得到樣例。這里假設(shè)Example結(jié)構(gòu)中i表示一個(gè)樣例的特征向量,j表示該樣例對(duì)應(yīng)的標(biāo)簽
example, label = features['i'], features['j']
# 一個(gè)batch中樣例的個(gè)數(shù)
batch_size = 2
# 組合樣例的隊(duì)列中最多可以存儲(chǔ)的樣例個(gè)數(shù)。這個(gè)隊(duì)列如果太大,那么需要占用很多內(nèi)存資源;
# 如果太小,那么出隊(duì)操作可能會(huì)因?yàn)闆](méi)有數(shù)據(jù)而被阻礙(block),從而導(dǎo)致訓(xùn)練效率降低。一般來(lái)說(shuō)
# 這個(gè)隊(duì)列的大小會(huì)和每一個(gè)batch的大小相關(guān),下面一行代碼給出了設(shè)置隊(duì)列大小的一種方式:
capacity = 1000 + 3 * batch_size
# 使用tf.train.batch函數(shù)來(lái)組合樣例。example, label參數(shù)給出了需要組合的元素,
# 一般example和label分別代表訓(xùn)練樣本和樣本對(duì)應(yīng)的正確標(biāo)簽。batch_size參數(shù)給出TensorFlow
# 將暫停入隊(duì)操作,而只是等待元素出隊(duì)。當(dāng)元素個(gè)數(shù)小于容量時(shí),TensorFlow將暫停入隊(duì)操作,而只是等待元素出隊(duì)。當(dāng)
# 元素小于容量時(shí),TensorFlow將自動(dòng)重新啟動(dòng)入隊(duì)操作。
example_batch, label_batch = tf.train.batch([example, label], batch_size=batch_size, capacity=capacity)
with tf.Session() as sess:
tf.global_variables_initializer().run()
tf.local_variables_initializer().run()
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
# 獲取并打印組合之后的樣例。在真是問(wèn)題中,這個(gè)輸出一般會(huì)作為神經(jīng)網(wǎng)絡(luò)的輸入。
for i in range(3):
cur_example_batch, cur_label_batch = sess.run([example_batch, label_batch])
print cur_example_batch, cur_label_batch
coord.request_stop()
coord.join(threads)
運(yùn)行上面的程序可以得到下面的輸出:
[0 0 1] [0 1 0]
[1 0 0] [1 0 1]
這個(gè)輸出可以看到tf.train.batch函數(shù)可以將單個(gè)的數(shù)據(jù)組織成3個(gè)一組的batch。在example,label中讀到的數(shù)據(jù)依次為:
example:0,example:0
example:0,example:1
example:1,example:0
example:1,example:1
這是因?yàn)閠f.train.batch函數(shù)不會(huì)隨機(jī)打亂順序,所以組合之后得到的數(shù)據(jù)組合成了上面給出的輸出。
下面一段代碼展示了tf.train.shuffle_batch函數(shù)的使用方法:
example, label = features['i'], features['j']
# 使用tf.train.shuffle_batch函數(shù)來(lái)組合樣例。tf.train.shuffle_batch函數(shù)的參數(shù)大部分都和tf.train.batch函數(shù)相似
# 但是min_after_dequeue參數(shù)是tf.train.shuffle_batch函數(shù)特有的。min_after_dequeue參數(shù)限制了出隊(duì)時(shí)隊(duì)列中元素的最少個(gè)數(shù)。
# 當(dāng)隊(duì)列中元素太少時(shí),隨機(jī)打亂樣例順序的作用就不大了。所以tf.train.shuffle_batch函數(shù)提供了限制出隊(duì)時(shí)最少元素的個(gè)數(shù)
# 來(lái)保證隨機(jī)打亂順序的作用。當(dāng)出隊(duì)函數(shù)被調(diào)用但是隊(duì)列中元素不夠時(shí),出隊(duì)操作將等待更多的元素入隊(duì)才會(huì)完成。
# 如果min_after_dequeue參數(shù)被設(shè)定,capacity也應(yīng)該相應(yīng)調(diào)整來(lái)滿足性能需求。
example_batch, label_batch = tf.train.shuffle_batch([example, label], batch_size=batch_size, capacity=capacity,min_after_dequeue=30)
with tf.Session() as sess:
tf.global_variables_initializer().run()
tf.local_variables_initializer().run()
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
# 獲取并打印組合之后的樣例。在真是問(wèn)題中,這個(gè)輸出一般會(huì)作為神經(jīng)網(wǎng)絡(luò)的輸入。
for i in range(2):
cur_example_batch, cur_label_batch = sess.run([example_batch, label_batch])
print(cur_example_batch, cur_label_batch)
coord.request_stop()
coord.join(threads)
第一次運(yùn)行代碼結(jié)果:
[0 0 1] [0 1 0]
[1 0 1] [0 0 0]
第二次運(yùn)行代碼結(jié)果:
[1 0 1] [0 1 0]
[1 1 1] [1 1 1]
從輸出中可以看到,得到的樣例順序已經(jīng)被打亂了。
tf.train.batch函數(shù)和tf.train.shuffle_batch函數(shù)除了可以將單個(gè)訓(xùn)練數(shù)據(jù)整理成輸入batch,也提供了并行化處理輸入數(shù)據(jù)的方法。tf.train.batch函數(shù)和tf.train.shuffle_batch函數(shù)并行化的方式一致。所以在本小節(jié)中僅以應(yīng)用得更多的tf.train.shuffle_batch函數(shù)為例。通過(guò)設(shè)置tf.train.shuffle_batch函數(shù)中的num_threads參數(shù),可以指定多個(gè)線程同時(shí)執(zhí)行入隊(duì)操作。tf.train.shuffle_batch函數(shù)的入隊(duì)操作就是數(shù)據(jù)讀取以及預(yù)處理的過(guò)程。當(dāng)num_threads參數(shù)大于1時(shí),多個(gè)線程會(huì)同時(shí)讀取一個(gè)文件中的不同樣例并進(jìn)行預(yù)處理。如果需要多個(gè)線程處理不同文件中的樣例時(shí),可以使用tf.train.shuffle_batch_join函數(shù)。此函數(shù)會(huì)從輸入文件隊(duì)列中獲取不同的文件分配給不同的線程。一般來(lái)說(shuō),輸入文件隊(duì)列是通過(guò)前面介紹的tf.train.string_input_producer函數(shù)生成的。這個(gè)函數(shù)會(huì)平均分配文件以保證不同文件中的數(shù)據(jù)會(huì)被盡量平均地使用。
3.4輸入數(shù)據(jù)處理框架

在前面的小節(jié)中已經(jīng)介紹了上圖所展示的流程圖中的所有步驟。在這一小節(jié)將把這些步驟串成一個(gè)完整的TensorFlow來(lái)處理輸入數(shù)據(jù)。以下代碼給出了這個(gè)完整的程序:
- 創(chuàng)建文件列表,通過(guò)文件列表創(chuàng)建輸入文件隊(duì)列,讀取文件為本章第一節(jié)創(chuàng)建的文件。
import tensorflow as tf
# 創(chuàng)建文件列表,并通過(guò)文件列表創(chuàng)建輸入文件隊(duì)列。在調(diào)用輸入數(shù)據(jù)處理流程前,需要統(tǒng)一所有原始數(shù)據(jù)的格式并
# 將它們存儲(chǔ)到TFRecord文件中。下面給出的文件列表應(yīng)該包含所有提供訓(xùn)練數(shù)據(jù)的TFRecord文件
files = tf.train.match_filenames_once("output.tfrecords")
filename_queue = tf.train.string_input_producer(files, shuffle=False)
2.解析TFRecord文件里的數(shù)據(jù)
# 讀取文件。
reader = tf.TFRecordReader()
_,serialized_example = reader.read(filename_queue)
# 解析讀取的樣例。
features = tf.parse_single_example(
serialized_example,
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)
labels = tf.cast(features['label'],tf.int32)
#pixels = tf.cast(features['pixels'],tf.int32)
images = tf.reshape(retyped_images, [784])
3.將文件以100個(gè)為一組打包。
min_after_dequeue = 10000
batch_size = 100
capacity = min_after_dequeue + 3 * batch_size
image_batch, label_batch = tf.train.shuffle_batch([images, labels],
batch_size=batch_size,
capacity=capacity,
min_after_dequeue=min_after_dequeue)
- 訓(xùn)練模型
def inference(input_tensor, weights1, biases1, weights2, biases2):
layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)
return tf.matmul(layer1, weights2) + biases2
# 模型相關(guān)的參數(shù)
INPUT_NODE = 784
OUTPUT_NODE = 10
LAYER1_NODE = 500
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 5000
weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))
biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))
weights2 = tf.Variable(tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))
biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))
y = inference(image_batch, weights1, biases1, weights2, biases2)
# 計(jì)算交叉熵及其平均值
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=label_batch)
cross_entropy_mean = tf.reduce_mean(cross_entropy)
# 損失函數(shù)的計(jì)算
regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
regularaztion = regularizer(weights1) + regularizer(weights2)
loss = cross_entropy_mean + regularaztion
# 優(yōu)化損失函數(shù)
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(loss)
# 初始化會(huì)話,并開(kāi)始訓(xùn)練過(guò)程。
with tf.Session() as sess:
# tf.global_variables_initializer().run()
sess.run((tf.global_variables_initializer(),
tf.local_variables_initializer()))
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
# 循環(huán)的訓(xùn)練神經(jīng)網(wǎng)絡(luò)。
for i in range(TRAINING_STEPS):
if i % 1000 == 0:
print("After %d training step(s), loss is %g " % (i, sess.run(loss)))
sess.run(train_step)
coord.request_stop()
coord.join(threads)
運(yùn)行代碼,結(jié)果如下:
After 0 training step(s), loss is 504.133
After 1000 training step(s), loss is 1.45948
After 2000 training step(s), loss is 1.85053
After 3000 training step(s), loss is 1.56273
After 4000 training step(s), loss is 1.65542

上圖展示了輸入數(shù)據(jù)處理的整個(gè)流程。從圖上可以看出,輸入數(shù)據(jù)處理的第一步為獲取存儲(chǔ)訓(xùn)練數(shù)據(jù)的文件列表。在圖中,這個(gè)文件列表為{A,B,C}。通過(guò)tf.train.string_input_producer函數(shù),可以選擇性地將文件列表中文件的順序打亂,并加入輸入隊(duì)列。因?yàn)槭欠翊騺y的順序是可選的,所以在圖中通過(guò)虛線表示。tf.train.string_input_producer函數(shù)會(huì)生成并維護(hù)一個(gè)輸入文件隊(duì)列,不同線程中的文件讀取函數(shù)可以共享這個(gè)輸入文件隊(duì)列。在讀取樣例數(shù)據(jù)之后,需要將圖像進(jìn)行預(yù)處理。圖像預(yù)處理的過(guò)程也會(huì)通過(guò)tf.train.shuffle_batch提供的機(jī)制并行地跑在多個(gè)線程中。輸入數(shù)據(jù)處理流程的最后通過(guò)tf.train.shuffle_batch函數(shù)將處理好的單個(gè)輸入樣例整理成batch提供給神經(jīng)網(wǎng)絡(luò)的輸入層。通過(guò)這種方式,可以有效地提高數(shù)據(jù)預(yù)處理的效率,避免數(shù)據(jù)預(yù)處理稱為神經(jīng)網(wǎng)絡(luò)模型訓(xùn)練過(guò)程中的性能瓶頸。