tf.data.Dataset影象預處理詳解
目錄
1、tf.data.Dataset
當訓練集的樣本特別大時, 比較適合tf.data.Dataset作為資料輸入管線,相當方便。然而真正在使用tf.data.Dataset時,還是有許多坑,在這裡寫出來,當作參考。由於我只涉及影象處理,本文只專注於影象預處理相關內容。
本文的第二部分主要講一些講一些Dataset的常用函式;第三部分講了使用Tensorflow原生API來完成圖片預處理的方法;第四部分是使用tf.py_func來完成任意邏輯的預處理;第五部分是例子的完整程式碼。實際上,還有另外一種預處理資料的方法,就是先用不涉及tensorflow的純python程式碼來完成預處理,然後把處理後的資料(比如Numpy陣列)存到硬碟上,然後再使用tf.py_func使用相同的邏輯來讀取處理後的資料,這樣就不用每次訓練都預處理資料了。
參考連結如下:
2、Dataset常用函式
先來看一個例子
# 讀取filename指定的影象,並調整其大小。label是其對應的標籤
def _parse_function(filename, label):
image_string = tf.read_file(filename)
# 讀取圖片
image_decoded = tf.image.decode_image(image_string)
# 調整大小
image_resized = tf.image.resize_images(image_decoded, [28, 28])
return image_resized, label
# 影象名稱組成的常量tensor
filenames = tf.constant(["data/image1.jpg", "data/image2.jpg", ...])
# 影象標籤。`labels[i]`-->`filenames[i].
labels = tf.constant([0, 37, ...])
# 定義一個Dataset例項
dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
# 對dataset中的每一對(filename, label)呼叫_parse_function進行處理
dataset = dataset.map(_parse_function)
# 設定每批次的大小
dataset = dataset.batch(batch_size=32)
# 無限重複資料集
dataset = dataset.repeat()
- tf.data.Dataset.from_tensor_slices((data, labels)):
建立一個Dataset例項。如果函式的引數包含NumPy陣列,並且未啟用Eager Execution,則值將作為一個或多個tf.constant操作嵌入到graph中。 對於大型資料集(> 1 GB),這可能會浪費記憶體並超過graph序列化(儲存圖的時候需要序列化)的位元組限制。 如果函式的引數包含一個或多個大型NumPy陣列,請參考替代方案。 - tf.data.Dataset.map(f, num_parallel_calls)
Dataset.map 轉換通過將函式 f 應用於輸入資料集的每對元素(data, label)來生成新資料集。比如在上面的例子中,就是把(filename, label)中filename指定的影象讀取出來並調整大小。
num_parallel_calls指定使用多少個執行緒來進行map操作。可以設定為CPU的最大核心數目(=multiprocessing.cpu_count())。如果不指定的話,只使用一個執行緒順序處理資料。 - tf.data.Dataset.batch(batch_size)
這個函式特別重要。 假如輸入影象大小為(227,227,3),模型的輸入shape為(None,227,227,3),其中None是batch_size。如果不呼叫這個函式,那麼從dataset獲取一批資料時,返回的資料shape為(227,227,3),輸入到模型維度肯定匹配不上,就會出現如下類似的錯誤:
Index out of range using input dim 4; input has only 4 dims
或者
Error when checking target: expected softmax to have 2 dimensions, but got array with shape (250,)
如果呼叫了這個函式,再從dataset獲取一批資料時,返回的資料shape為(batch_size,227,227,3),就能和模型的輸入shape匹配上了。 - tf.data.Dataset.repeat(count)
重複這個資料集多少次。如果不傳count這個引數,預設會無限重複這個資料集。加入count=1,那麼當你訓練完一輪之後,就會報錯tensorflow.python.framework.errors_impl.OutOfRangeError: End of sequence
。在實際使用中,基本可以不傳count引數,無限重複這個資料集。
常用函式還有tf.data.Dataset.shuffle(),是用來打亂資料集的。另外還需要注意的是:這些函式都是返回呼叫該操作之後的一個Dataset例項,並沒有在本身上應用該操作。
3、影象預處理的第一種方式
首先說下需求,主要是需要訓練一個分類模型。訓練集放在一個txt文字中,每一行是由圖片和標籤組成,一部分如下
data/test/001/001_01_01_051_09.png 0
data/test/001/001_01_01_051_10.png 0
data/test/002/002_01_01_051_19.png 1
data/test/002/002_01_01_051_09.png 1
data/test/003/003_01_01_051_14.png 2
data/test/003/003_01_01_051_03.png 2
data/test/004/004_01_01_051_05.png 3
data/test/004/004_01_01_051_06.png 3
...
現在需要把文字中的圖片和標籤讀入到一個Dataset裡面。
3.1、匯入依賴庫
# coding=utf-8
# 相容python3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import multiprocessing as mt
import numpy as np
import tensorflow as tf
from tensorflow import keras
3.2、定義常量
# 分類問題,總共有250個類
NUM_CLASSES = 250
# 訓練批次大小
TRAIN_BATCH_SIZE = 128
# 影象每個畫素的每個通道的最大值,對於8點陣圖像,就是255
IMAGE_DEPTH=255
# 包含訓練集的文字
TRAIN_LIST = 'data/train.txt'
3.3、讀取文字中的圖片標籤對
# 讀取由path指定的文字檔案,並返回由很多(圖片路徑,標籤)組成的列表
lists_and_labels = np.loadtxt(path, dtype=str).tolist()
# 打亂下lists_and_labels
np.random.shuffle(lists_and_labels)
# 把圖片路徑和標籤分開
list_files, labels = zip(*[(l[0], int(l[1])) for l in lists_and_labels])
# 如果使用keras構建模型,還需要對標籤進行one_hot編碼,如果使用tensorflow構建的模型,則不需要。
one_shot_labels = keras.utils.to_categorical(labels, NUM_CLASSES).astype(dtype=np.int32)
3.4、例項化Dataset並完成影象預處理
# 定義資料集例項
dataset = tf.data.Dataset.from_tensor_slices((tf.constant(list_files), tf.constant(one_shot_labels)))
# 對沒一對 (image, label)呼叫_parse_image,完成影象的預處理
dataset = dataset.map(_parse_image, num_parallel_calls=mt.cpu_count())
# 設定訓練批次大小。非常重要!!!
dataset = dataset.batch(TRAIN_BATCH_SIZE)
# 無限重複資料集
dataset = dataset.repeat()
# 計算遍歷一遍資料集需要多少步
steps_per_epoch = np.ceil(len(labels) / TRAIN_BATCH_SIZE).astype(np.int32)
return dataset, steps_per_epoch
_parse_image函式需要實現的是:讀取圖片,調整大小,並將影象畫素值的範圍從[0, 255]縮放到[-0.5, 0.5]。_parse_image不能直接呼叫其他庫來實現功能,只能使用tensorflow中預定的操作來完成所需要的功能。實現如下:
def _parse_image(filename, label):
# 讀取並解碼圖片
image_string = tf.read_file(filename)
image_decoded = tf.image.decode_image(image_string)
# 一定要在這裡轉換型別!!!
image_converted = tf.cast(image_decoded, tf.float32)
# 縮放範圍
image_scaled = tf.divide(tf.subtract(image_converted, IMAGE_DEPTH/2), IMAGE_DEPTH)
return image_scaled, label
至此dataset就可以作為model.fit和model.evaluate(keras中)的引數了。
3.5、從Dataset中獲取資料
在使用Dataset作為使用tensorflow編寫的模型的輸入時,需要把資料取出來,作為feed_dict的引數的資料。另外,在使用Dataset作為模型輸入是,需要看看資料預處理的結果對不對,把資料取出來,看看實際的資料是否符合預期。
# 列印dataset的相關資訊
print('shapes:', dataset.output_shapes)
print('types:', dataset.output_types)
print('steps:', steps)
# 獲取一個用來迭代資料的iterator
data_it = dataset.make_one_shot_iterator()
# 定義個獲取下一組資料的操作(operator)
next_data = data_it.get_next()
# 新建Session
with tf.Session() as sess:
# 獲取前10組資料
for i in range(10):
# 獲取一批圖片和對應的標籤
data, label = sess.run(next_data)
# 列印資料的長度,標籤的長度,資料的shape,資料的最大值和最小值
print(len(data), len(label), data.shape, np.min(data), np.max(data))
執行上面的程式,輸出類似於
128 128 (128, 227, 227, 3) -0.5 0.5
128 128 (128, 227, 227, 3) -0.49607846 0.5
128 128 (128, 227, 227, 3) -0.5 0.5
128 128 (128, 227, 227, 3) -0.49607846 0.5
...
3.6、處理需要預測的樣本
預測(predict)樣本時,在預處理圖片時,預處理的操作一定要和訓練時的相同,否則評估或者預測的結果是不對的。在上面的方法中,預處理的程式碼為:
def read_image(filename):
with tf.Session() as sess:
read_op = _parse_image(tf.constant(filename, dtype=tf.string), tf.constant(0))
image, label = sess.run(read_op)
return image
image = read_image('data/test/001/001_01_01_051_09.png')
print('shape: ', image.shape)
在使用model.predict(keras)時,還需要擴充套件image的維度為四維,程式碼如下
# 讀圖片
image = read_image('data/train/022/022_01_01_051_00.png')
# 擴充套件維度為 (1, 227, 227, 3)
image = image[np.newaxis, :]
print(image.shape)
....
# 預測
softmax = model.predict(image, 1)
print(np.argmax(softmax)+1)
4、使用tf.py_func進行圖片預處理
有時候,需要完成特別複雜的預處理的時候,無法使用tensorflow內建的操作完成預處理,就可以使用tf.py_func來完成任意邏輯的預處理。先來個例子:
# coding=utf-8
import cv2
import tensorflow as tf
# 使用OpenCV程式碼來完成讀取圖片,在這個函式裡,你可以使用任意的python庫來完成任意操作
def _read_py_function(filename, label):
image_decoded = cv2.imread(filename.decode(), cv2.IMREAD_UNCHANGED)
return image_decoded, label
# 讀取圖片
def _read_image_caller(filename, label):
return tf.py_func(_read_py_function, [filename, label], [tf.uint8, label.dtype])
# 使用標準TensorFlow操作來調整圖片大小
def _resize_function(image_decoded, label):
# 由於無法從image_decoded推斷shape,所以要先手動設定
image_decoded.set_shape([None, None, None])
# 調整大小
image_resized = tf.image.resize_images(image_decoded, [28, 28])
return image_resized, label
filenames = ["data/train/001/001_01_01_051_04.png", "data/train/001/001_01_01_051_05.png", ]
labels = [0, 37, ]
# 定義dataset物件
dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
# 呼叫map,完成圖片讀取
dataset = dataset.map(_read_image_caller)
# 再次呼叫map,完成圖片的調整大小的操作
dataset = dataset.map(_resize_function)
# 定義獲取資料的tensorflow操作
next_op = dataset.make_one_shot_iterator().get_next()
with tf.Session() as sess:
for _ in range(len(labels)):
# 獲取下一組image,label影象對
image, label = sess.run(next_op)
print image.shape, label
tf.py_func的作用是把一個普通的python函式包裝(wrap)為tensorflow操作(類似於tf.read_file之類的),主要引數如下
- func: 指定要包裹的普通python函式F。
- inp: F所需要的引數組成的列表。
- Tout: 指定F返回值的型別。
4.1、來個例子
模型輸入的shape為(None,12, 227,227,3),其中的None是批次的大小,12是一個物體模型有12張圖片,(227,227,3)是一張影象的大小。所以預處理的要求是,把一個物體的12張圖片讀進來,完成調整大小,縮放畫素值的範圍到[-0.5, 0.5],併疊在一起(shape為(12,227,227,3))。物體模型的列表和標籤train.txt如下:
data/train/001/list.txt 0
data/train/002/list.txt 1
data/train/003/list.txt 2
data/train/004/list.txt 3
data/train/005/list.txt 4
...
每一行的一個list.txt指定了一個物體模型的12張圖片,其中的一個如下:
data/train/001/001_01_01_051_14.png
data/train/001/001_01_01_051_19.png
data/train/001/001_01_01_051_18.png
data/train/001/001_01_01_051_10.png
data/train/001/001_01_01_051_07.png
data/train/001/001_01_01_051_16.png
data/train/001/001_01_01_051_04.png
data/train/001/001_01_01_051_17.png
data/train/001/001_01_01_051_13.png
data/train/001/001_01_01_051_15.png
data/train/001/001_01_01_051_11.png
data/train/001/001_01_01_051_05.png
4.2、匯入依賴庫
# coding=utf-8
# 相容python3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import cv2
import numpy as np
import tensorflow as tf
import multiprocessing as mt
from tensorflow import keras
4.3、定義常量
# 分類問題,總共有40個類
NUM_CLASSES = 40
# 訓練批次大小
TRAIN_BATCH_SIZE = 1
# 影象每個畫素的每個通道的最大值,對於8點陣圖像,就是255
IMAGE_DEPTH=255
# 包含訓練集的文字
TRAIN_LIST = 'data/train.txt'
# 一個物體有12張圖片
NUM_VIEWS = 12
# 一張圖的大小
IMAGE_SHAPE = (227, 227, 3)
4.4、定義Dataset
這裡我這給出定義Dataset的函式
def prepare_dataset(path=''):
# 讀取物體模型列表
lists_and_labels = np.loadtxt(path, dtype=str).tolist()
# 打亂資料
np.random.shuffle(lists_and_labels)
# 分為模型列表和標籤
list_files, labels = zip(*[(l[0], int(l[1])) for l in lists_and_labels])
# 對標籤進行one_hot編碼
one_shot_labels = keras.utils.to_categorical(labels, NUM_CLASSES).astype(dtype=np.int32)
# 定義資料集
dataset = tf.data.Dataset.from_tensor_slices((tf.constant(list_files), tf.constant(one_shot_labels)))
# 讀取每個模型的12張圖片的路徑
dataset = dataset.map(read_object_caller, num_parallel_calls=mt.cpu_count())
# 調整每張圖片的大小,轉換圖片的資料型別為float32,並將12張圖片堆疊到一起
dataset = dataset.map(read_resize_concat, num_parallel_calls=mt.cpu_count())
# 非常重要,記得要設定批次大小
dataset = dataset.batch(TRAIN_BATCH_SIZE)
# 無限重複
dataset = dataset.repeat()
# 計算每次迭代需要多少步
steps_per_epoch = np.ceil(len(labels)/TRAIN_BATCH_SIZE).astype(np.int32)
return dataset, steps_per_epoch
4.5、讀取物體模型列表
我在寫程式碼的時候,讀取一個物體的12張圖片的路徑列表花了很久很久,就是因為不知道tf.py_func這個神器,接下來的程式碼,就是如何讀取一個物體模型的列表。
def read_object_caller(filename, label):
# 使用tf.py_func呼叫一個普通python函式來讀取一個物體的12張圖片路徑
# 注意返回值的型別是[tf.string, label.dtype]。
return tf.py_func(read_object_list, [filename, label], [tf.string, label.dtype])
def read_object_list(filename, label):
# 讀取一個物體模型的列表
image_lists = np.loadtxt(filename.decode(), dtype=str)
# 擷取前NUM_VIEWS個圖片路徑
image_lists = image_lists[:NUM_VIEWS]
# 如果圖片路徑沒有NUM_VIEWS個,丟擲錯誤
if len(image_lists) != NUM_VIEWS:
raise ValueError('There haven\'t %d views in %s ' % (NUM_VIEWS, filename))
# 返回圖片列表與標籤
return image_lists, label
4.5、圖片的預處理操作
def read_resize_concat(image_list, label):
# image_list是物體模型的12張圖片路徑的列表
# 下面這個函式就是處理列表中的每一個影象的函式
def process_one_image(image):
# 讀取圖片並解碼
image_string = tf.read_file(image)
image_decoded = tf.image.decode_image(image_string)
# 由於無法從image_decoded推斷shape,所以要先手動設定,否則resize_images會報錯
image_decoded.set_shape([None, None, None])
# 調整大小
image_resized = tf.image.resize_images(image_decoded, IMAGE_SHAPE[0:2])
# 轉換影象畫素型別
image_converted = tf.cast(image_resized, tf.float32)
# 把畫素值的範圍從[0, 255]縮放到[-0.5, 0.5]
image_scaled = tf.divide(tf.subtract(image_converted, IMAGE_DEPTH / 2), IMAGE_DEPTH)
return image_scaled
# 呼叫tf.map_fn對image_list的每個元素,也就是一張圖片的路徑,呼叫process_one_image函式,完成
# 對一張圖片的預處理,並返回一個處理後的list
image_prepocessed_list = tf.map_fn(process_one_image, image_list, dtype=tf.float32)
# 把12個處理後圖片在維度0上堆疊起來,一張圖片的shape為(227, 227, 3),堆疊後的shape為(12, 227,227,3)
concat = tf.concat(image_prepocessed_list, axis=0)
return concat, label
注意:tf.image.decode_image返回的image_decoded沒有shape,如果直接對image_decoded呼叫tf.image.resize_images,會出現如下錯誤ValueError: 'images' contains no shape.
4.6、從Dataset讀取資料
def inputs_test():
dataset, steps = prepare_dataset(TRAIN_LIST)
print('shapes:', dataset.output_shapes)
print('types:', dataset.output_types)
print('steps:', steps)
next_op = dataset.make_one_shot_iterator().get_next()
with tf.Session() as sess:
for i in range(5):
data, label = sess.run(next_op)
print(len(data), len(label), data.shape, np.min(data), np.max(data))
if __name__ == '__main__':
inputs_test()
4.7、獲取預測樣本
同樣,在預測時,樣本資料需要經過和訓練資料同樣的預處理,程式碼如下:
def process_one_sample(path):
label = 0
# 讀取圖片列表
image_list, _ = read_object_list(path, label)
# 定義處理操作
process_op = read_resize_concat(tf.constant(image_list), tf.constant(label))
# 處理
with tf.Session() as sess:
concat_image, _ = sess.run(process_op)
return concat_image
if __name__ == '__main__':
concat_image = process_one_sample('data/train/004/list.txt')
print(concat_image.shape)
5、兩種方法的完整程式碼
資料我就不提供了,自行準備吧。
5.1、第一種方法
# coding=utf-8
# 相容python3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import multiprocessing as mt
import numpy as np
import tensorflow as tf
from tensorflow import keras
# 分類問題,總共有250個類
NUM_CLASSES = 250
# 訓練批次大小
TRAIN_BATCH_SIZE = 128
# 影象每個畫素的每個通道的最大值,對於8點陣圖像,就是255
IMAGE_DEPTH = 255
# 包含訓練集的文字
TRAIN_LIST = 'data/train.txt'
def prepare_dataset(path=''):
"""
prepaer dataset using tf.data.Dataset
:param path: the list file like data/train_lists_demo.txt
and data/val_lists_demo.txt
:return: a Dataset object
"""
# read image list files name and labels
lists_and_labels = np.loadtxt(path, dtype=str).tolist()
# shuffle dataset
np.random.shuffle(lists_and_labels)
# split lists an labels
list_files, labels = zip(*[(l[0], int(l[1])) for l in lists_and_labels])
# one_shot encoding on labels
one_shot_labels = keras.utils.to_categorical(labels, NUM_CLASSES).astype