tf.data
为tensorflow官方最为推荐的处理数据的模块。本文通过实例详细叙述使用tf.data
处理数据的方法,其中包括读入数据、预处理数据、使用数据增强等,同时本文针对上述过程中涉及到的python 及 tensorflow 的api均进行了详细的说明,以求充分理解并自行完成满足实际需求的代码。本文使用tensorflow版本为v2.6且本文内容在v2.0以上版本均适用。
本次示例使用花分类数据集,分为trian
和val
两个文件夹,每个文件夹中均含有以5个类别命名的文件夹,文件中为相应的数据集图片。示例图如下
-- data
-- train
-- daisy
-- *.jpg
...
-- dandelion
-- roses
-- sunflowers
-- tulips
-- val
-- daisy
-- dandelion
-- roses
-- sunflowers
-- tulips
# class dict
data_class = [cla for cla in os.listdir(train_dir) if os.path.isdir(os.path.join(train_dir, cla))]
class_num = len(data_class)
class_dict = dict((value, index) for index, value in enumerate(data_class))
# reverse value and key of dict
inverse_dict = dict((val, key) for key, val in class_dict.items())
说明:
train_dir
为训练集文件夹的路径,其中包含以各类别命名的文件夹。所以通过os.listdir()
获得train_dir中各个文件夹的名称,也就是各个类别的名称。即**data_class=[daisy, dendelion, roses, sunflowers, tulips]
**。这里值得注意data_class列表获取的方法。class_dict
获得类别名对应的序号,inverse_dict
获得序号对应类别名。二者均为字典类型。后期获得训练集和验证集的迭代器,需要使用tf.data.Dataset.from_tensor_slices((all_images_path, all_imgs_labels))
函数获得,其中需要图片和标签数据,图片数据以图片路径组成的列表表示,标签数据以与图片相应的数字列表组成。所以我们需要先构造出满足上述条件的两个列表!
# load train images list
train_image_list = glob.glob(train_dir+"/*/*.jpg")
random.shuffle(train_image_list)
train_num = len(train_image_list)
assert train_num > 0, "cannot find any .jpg file in {}".format(train_dir)
train_label_list = [class_dict[path.split(os.path.sep)[-2]] for path in train_image_list]
# load validation images list
val_image_list = glob.glob(validation_dir+"/*/*.jpg")
random.shuffle(val_image_list)
val_num = len(val_image_list)
assert val_num > 0, "cannot find any .jpg file in {}".format(validation_dir)
val_label_list = [class_dict[path.split(os.path.sep)[-2]] for path in val_image_list]
print("using {} images for training, {} images for validation.".format(train_num,
val_num))
说明:
glob.glob(train_dir+"/*/*.jpg")
:通过glob函数获得满足 文件名称条件 的文件根目录列表。具体api的使用方法见博客。split
方法即可获得。其中注意os.path.sep
是操作系统用来分隔路径名组件的字符,这里相当于\
。def process_path(img_path, label):
label = tf.one_hot(label, depth=class_num)
image = tf.io.read_file(img_path)
image = tf.io.decode_jpeg(image)
image = tf.image.convert_image_dtype(image, tf.float32)
image = tf.image.resize(image, [im_height, im_width])
return image, label
AUTOTUNE = tf.data.AUTOTUNE
# load train dataset
train_dataset = tf.data.Dataset.from_tensor_slices((train_image_list, train_label_list))
train_dataset = train_dataset.shuffle(buffer_size=train_num)\
.map(process_path, num_parallel_calls=AUTOTUNE)\
.repeat().batch(batch_size).prefetch(AUTOTUNE)
# load train dataset
val_dataset = tf.data.Dataset.from_tensor_slices((val_image_list, val_label_list))
val_dataset = val_dataset.map(process_path, num_parallel_calls=tf.data.experimental.AUTOTUNE)\
.repeat().batch(batch_size)
说明:(以train_dataset为例子)
tf.data.Dataset.from_tensor_slices((imgs_path, labels))
:创建数据集的常用方法,同时传入图片地址(或图片),和对应的标签,即可获得一个可迭代的数据集train_dataset
。
train_dataset.map(map_func, num_parallel_calls=None, deterministic=None)
:对数据集进行变换(transformations)。该函数会对数据集中每一个元素执行map_func
处理,并返回处理后的数据集。
num_parallel_calls
:该参数表示要异步并行处理数据的数量,可以为tf.int64
和tf.Tensor
类型数据。如果不指定,数据将按顺序处理。如果指定为**tf.data.AUTOTUNE
**,则会根据可用的CPU动态设置并行处理数据的数量。deterministic
:布尔型。当num_parallel_calls
确定时,deterministic将会控制数据转换过程中处理数据的顺序。具体参考。一般不指定(默认参数)。train_dataset.shuffle(buffer_size=train_num)
:shuffle()方法可以充分打乱数据,其中的buffer_size参数需要设置与数据集大小一致,以保证数据集被充分打乱。
train_dataset.repeat(count=None)
:重复数据集,重复次数为count传入的参数。
count
:可以为tf.int64或tf.Tensor类型数据,设置数据集重复的次数。若不设置(默认)或设为-1,则会无限期的重复数据集。train_dataset.batch(batch_size, drop_remainder=False, num_parallel_calls=None, deterministic=None)
:结合连续数量的数据为一个batch。**drop_remainder
**设置为True时,当数据集的个数不能整除一个batch中数据的个数时,则会舍弃最后几个不够一个batch的数据。其余两个参数与map函数中相同。
train_dataset.prefetch(buffer_size)
:通常在创建一个迭代数据集最后调用prefetch
方法。可以使得处理当前数据的同时准备后续数据,尽快提供batch。当buffer_size
设定为tf.data.AUTOTUNE
时,将会自动调整缓冲区的大小(buffer size)。
process path
函数详解:
tf.one_hot(label, depth=class_num)
:将数据变为one-hot类型,depth为一个标量,决定one-hot的维度。在分类数据集上,则为类别的个数。
tf.io.read_file(img_path)
:返回包括文件所有内容的tensor, tensor的dtype为“string”。所以其一般为数据流的,得到的数据还需要进行解码。
tf.image/io.decode_jpeg(image, channel=0)
:示例中给的解码操作使用的是tf.image
工具包,但tensorflow_v2.6版本所有解码操作已经都移入tf.io
模块中。该函数作用为解码jpeg(jpg)图片为uint8的tensor。
channel
参数可以指定为0, 1, 3。默认为0,表示安装原始jpg的通道数获取图片,如果设为1,则会解码为一个灰度图片。tf.image.convert_image_dtype(image, tf.float32)
:该api实现对img数据类型进行转换,支持的数据类型有uint8
, uint16
, uint32
, uint64
, int8
, int16
, int32
, int64
, float16
, float32
, float64
, bfloat16
。当使用浮点数类型时,图片中数据的值将在 ( 0 , 1 ] (0,1] (0,1]之间;使用整型时,图片中数据的值将在 ( 0 , M a x ] (0, Max] (0,Max]之间,Max为该整型的最大值。
image = tf.image.resize(image, [im_height, im_width],method=ResizeMethod.BILINEAR)
:resize图片到特定的尺寸,可以指定method参数确定resize使用的方法。各方法说明见官网
当原始图片和resize后的图片长宽比 比例不一致时会出现扭曲,如果不想出现这个问题,可以使用**
tf.image.resize_with_pad()
**函数,其会保证resize后的图片部分长宽比与原始图像相同,其余部分通过padding 0来填充。
tensorflow支持的数据增强的方法主要有两种:1.使用**tf.image
模块中提供的方法。2.使用keras
中的预处理层**,比如tf.keras.layers.RandomFlip
,tf.keras.layers.Resizing
等。
由于上面我们已经使用过tf.image
模块,也大致了解了其配合tf.data
使用方法,同时**相对于使用keras预处理层来说,tf.image更具有灵活性和可控制性,且配合tf.data使用可以自定义数据增强方法。**所以我们先从tf.image
实现数据增强进行说明。
tf.image
进行数据增强为了方便观察数据增强API的效果,我们定义一个函数,用来对比原始图片和增强后的图片。
def visualize(original, augmented):
fig = plt.figure()
plt.subplot(1,2,1)
plt.title('Original image')
plt.imshow(original)
plt.subplot(1,2,2)
plt.title('Augmented image')
plt.imshow(augmented)
tf.image
提供了固定增强和随机增强等两大类增强方式,本文只列举每个类中的几种。具体api详见官网
Part1 固定参数增强
左右翻转(Flip an image)
flipped = tf.image.flip_left_right(image)
visualize(image, flipped)
改变图片饱和度(Saturate an image)
saturated = tf.image.adjust_saturation(image, 3)
visualize(image, saturated)
Part2 随机增强
注意:在tensorflow2.6版本中,tf.image含有两种随机增强api:tf.image.random*
和tf.image.stateless_random*
官方更推荐使用后者。
随机亮度(Randomly change image brightness)
for i in range(3):
seed = (i, 0) # tuple of size (2,)
stateless_random_brightness = tf.image.stateless_random_brightness(
image, max_delta=0.95, seed=seed)
visualize(image, stateless_random_brightness)
函数说明:tf.image.stateless_random_brightness
需要传入三个参数
随机裁剪(Randomly crop an image)
for i in range(3):
seed = (i, 0) # tuple of size (2,)
stateless_random_crop = tf.image.stateless_random_crop(
image, size=[210, 300, 3], seed=seed)
visualize(image, stateless_random_crop)
函数说明:tf.image.stateless_random_crop
中size为指定crop图像的大小。seed仍为size为(2,)的元组。
Part3 数据集应用增强
在上文加载训练集和验证集部分中我们已经介绍了dataset的map
方法,可以发现通过map函数可以灵活的按照自己的要求处理数据,只需要定义一个预处理数据的函数。因此数据增强方法也可以定义在预处理函数中。预处理函数示例如下:
def resize_and_rescale(image, label):
image = tf.cast(image, tf.float32)
image = tf.image.resize(image, [IMG_SIZE, IMG_SIZE])
image = (image / 255.0)
return image, label
先定义一个基本的数据预处理函数,包括图像数据的类型转化,resize和数据归一化(若使用tf.image.convert_image_dtype(image, tf.float32)
则会自动将数据变为float32并归一化[0,1)之间)。
def augment(image_label, seed):
image, label = image_label
image, label = resize_and_rescale(image, label)
image = tf.image.resize_with_crop_or_pad(image, IMG_SIZE + 6, IMG_SIZE + 6)
# Make a new seed.
new_seed = tf.random.experimental.stateless_split(seed, num=1)[0, :]
# Random crop back to the original size.
image = tf.image.stateless_random_crop(
image, size=[IMG_SIZE, IMG_SIZE, 3], seed=seed)
# Random brightness.
image = tf.image.stateless_random_brightness(
image, max_delta=0.5, seed=new_seed)
image = tf.clip_by_value(image, 0, 1)
return image, label
再定义一个数据增强函数,其中引用了resize_and_rescale
函数对图像进行基本预处理。再增加亿点点细节,就可以随心所欲的增加数据增强方法了。
函数说明:
tf.image.resize_with_crop_or_pad
:对图片进行resize,该函数相较于tf.image.resize
会根据原始图片比例适当增加padding或者crop后再进行resize。具体参考
tf.random.experimental.stateless_split(seed, num)
:该函数需要传入一个RNG形式的seed(一个shape为2的tensor,类型为int32,或int64。比如seed=[1,2]
),和返回seeds的个数num
。该函数会返回一个shape为[num, 2]的新的seed。在augment
函数代码中我们对该tf.random.experimental.stateless_split
的返回值进行了切片操作,就是因为其返回值为[1, 2]的二维数组,而需要传入随机增强中的seed的shape应为(2, )的一维数组。
**tf.image.stateless_random_crop
和tf.image.stateless_random_brightness
**都为随机增强函数,在上述示例中都有提到,这里不再叙述。
tf.clip_by_value(image, 0, 1)
:将image数据限制在[0, 1]之间,调用该函数是为了防止数据增强过程中导致图像的数据不在[0, 1]范围之内。
Part4 随机种子的产生
在上文中我们提到了随机增强函数,可以了解到调用随机增强函数需要传入一个seed参数,传入相同的seed在其他参数相同的情况下会产生相同的增强效果。**所以我们需要创造一个seed迭代器,让其每次调用产生不同的seed。**官方提供了tf.random.Generator
,我们只需要创建一个tf.random.Generator.from_seed
实例,通过每次调用make_seeds
方法即可得到随机的种子。具体实现见下文代码。
tf.random.Generator.from_seed
实例,传入一个初始化种子值(这里为123),和产生随机数的方法,这里设为philox
,其他产生随机数的方法,参见官网# Create a generator.
rng = tf.random.Generator.from_seed(123, alg='philox')
make_seed
方法,产生随机种子,传入前面定义的augment
函数中。# Create a wrapper function for updating seeds.
def f(x, y):
seed = rng.make_seeds(2)[0]
image, label = augment((x, y), seed)
return image, label
说明:
make_seeds(count=1)
:该方法会返回一个shape为[2, count]
的tensor,dtype为int64。到此随机增强部分就全部明了了,接着使用map函数对数据进行f(x,y)
就可以构造出一个经过增强后的数据集了。
train_datasets =
train_datasets
.shuffle(1000)
.map(f, num_parallel_calls=AUTOTUNE)
.batch(batch_size)
.prefetch(AUTOTUNE)
训练部分使用tensorflow提供的高级api即可,model.fit
可以直接接受上文中得到的train_datasets进行训练。训练部分代码详解将不在本文叙述,可以参考官网,或即将更新的下一篇博文。下面仅附上参考代码。
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0005),
loss=tf.keras.losses.CategoricalCrossentropy(from_logits=False),
metrics=["accuracy"])
callbacks = [tf.keras.callbacks.ModelCheckpoint(filepath='./save_weights/myAlex_{epoch}.h5',
save_best_only=True,
save_weights_only=True,
monitor='val_loss')]
history = model.fit(x=train_dataset,
steps_per_epoch=train_num // batch_size,
epochs=epochs,
validation_data=val_dataset,
validation_steps=val_num // batch_size,
callbacks=callbacks)