一文上手最新Tensorflow2.0系列(三)|“tf.data”API 使用

除了GPU和TPU等硬件加速设备以外,一个高效的数据输入管道也可以很大程度的提升模型性能,减少模型训练所需要的时间。数据输入管道本质是一个ELT(Extract、Transform和Load)过程:

  • Extract:从硬盘中读取数据(可以是本地的也可以是云端的)。
  • Transform:数据的预处理(例如数据清洗、格式转换等)。
  • Load:将处理好的数据加载到计算设备(例如CPU、GPU以及TPU等)。

数据输入管道一般使用CPU来执行ELT过程,GPU等其他硬件加速设备则负责模型的训练,ELT过程和模型的训练并行执行,从而提高模型训练的效率。另外ELT过程的各个步骤也都可以进行相应的优化,例如并行的读取数据以及并行的处理数据等。在TensorFlow中我们可以使用“tf.data”API来构建这样的数据输入管道。

我们首先下载实验中需要用的图像数据集(下载地址百度网盘地址)。

该数据集是一个花朵图片的数据集,将下载下来的数据解压后如图所示,除了一个License文件以外主要是五个分别存放着对应类别花朵图片的文件夹。其中“daisy(雏菊)”文件夹中有633张图片,“dandelion(蒲公英)”文件夹中有898张图片,“roses(玫瑰)”文件夹中有641张图片,“sunflowers(向日葵)”文件夹中有699张图片,“tulips(郁金香)”文件夹中有799张图片。


解压后的数据集

接下来我们开始实现代码,首先我们导入需要使用的包:

import tensorflow as tf
import pathlib

pathlib提供了一组用于处理文件系统路径的类。导入需要的包后,可以先检查一下TensorFlow的版本:

print(tf.__version__)

首先获取所有图片样本文件的路径:

# 获取当前路径
data_root = pathlib.Path.cwd()
# 获取指定目录下的文件路径(返回是一个列表,每一个元素是一个PosixPath对象)
all_image_paths = list(data_root.glob('*/*/*'))
print(type(all_image_paths[0]))
# 将PosixPath对象转为字符串
all_image_paths = [str(path) for path in all_image_paths]
print(all_image_paths[0])
print(data_root)

输出结果如图所示:


文件路径输出结果

接下来我们需要统计图片的类别,并给每一个类别分配一个类标:

# 获取图片类别的名称,即存放样本图片的五个文件夹的名称
label_names = sorted(item.name for item in data_root.glob('*/*/') if item.is_dir())
# 将类别名称转为数值型的类标
label_to_index = dict((name, index) for index, name in enumerate(label_names))
# 获取所有图片的类标
all_image_labels = [label_to_index[pathlib.Path(path).parent.name]
print(label_to_index)
print("First 10 labels indices: ", all_image_labels[:2])
print("First 10 labels indices: ", all_image_paths[:2])

输出结果如图所示,daisy(雏菊)、dandelion(蒲公英)、roses(玫瑰)、sunflowers(向日葵)、tulips(郁金香)的类标分别为0、1、2、3、4、5。


图片类标的输出结果

处理完类标之后,我们接下来需要对图片本身做一些处理,这里我们定义一个函数用来加载和预处理图片数据:

def load_and_preprocess_image(path):
 # 读取图片
 image = tf.io.read_file(path)
 # 将jpeg格式的图片解码,得到一个张量(三维的矩阵)
 image = tf.image.decode_jpeg(image, channels=3)
 # 由于数据集中每张图片的大小不一样,统一调整为192*192
 image = tf.image.resize(image, [192, 192])
 # 对每个像素点的RGB值做归一化处理
 image /= 255.0
 return image

完成对类标和图像数据的预处理之后,我们使用“tf.data.Dataset”来构建和管理数据集:

# 构建图片路径的“dataset”
path_ds = tf.data.Dataset.from_tensor_slices(all_image_paths)
# 使用AUTOTUNE自动调节管道参数
AUTOTUNE = tf.data.experimental.AUTOTUNE
# 构建图片数据的“dataset”
image_ds = path_ds.map(load_and_preprocess_image,
num_parallel_calls=AUTOTUNE)
# 构建类标数据的“dataset”
label_ds = tf.data.Dataset.from_tensor_slices(tf.cast(all_image_labels, tf.int64))
# 将图片和类标压缩为(图片,类标)对
image_label_ds = tf.data.Dataset.zip((image_ds, label_ds))
print(image_ds)
print(label_ds)
print(image_label_ds)

输出结果:


构建的“dataset”

在代码中,我们使用了from_tensor_slices方法使用张量的切片元素构建“dataset”,tf.data.Dataset类还提供了“from_tensor”直接使用单个张量来构建“dataset”,以及可以使用生成器生成的元素来构建“dataset”的“from_generator”方法。

我们还使用了“tf.data.Dataset”的“map”方法,该方法允许我们自己定义一个函数,将原数据集中的元素依次经过该函数处理,并将处理后的数据作为新的数据集,处理前和处理后的数据顺序不变。例如这里我们自己定义了一个“load_and_preprocess_image”函数,将“path_ds”中的图片路径转换成了经过预处理的图像数据,并保存在了“image_ds”中。

最后我们使用“tf.data.Dataset”的“zip”方法将图片数据和类标数据压缩成“(图片,类标)”对,其结构如图所示。我们可视化一下数据集中的部分数据:

import matplotlib.pyplot as plt
plt.figure(figsize=(8,8))
for n,image_label in enumerate(image_label_ds.take(4)):
 plt.subplot(2,2,n+1)
 plt.imshow(image_label[0])
 plt.grid(False)
 plt.xticks([])
 plt.yticks([])
 plt.xlabel(image_label[1])

结果如图所示:


数据集中部分数据的可视化

接下来我们用创建的dataset训练一个分类模型,为了简单,我们直接使用“tf.keras.applications”包中训练好的模型,并将其迁移到我们的花朵分类任务上来。这里我们使用“MobileNetV2”模型。

# 下载的模型在用户根目录下,具体位置:“~/.keras/models/
mobilenet_v2_weights_tf_dim_ordering_tf_kernels_1.0_192_no_top.h5”
mobile_net = tf.keras.applications.MobileNetV2(input_shape=(192, 192, 3),
include_top=False)
# 禁止训练更新“MobileNetV2”模型的参数
mobile_net.trainable = False

当我们执行代码后,训练好的“MobileNetV2”模型会被下载到本地,该模型是在ImageNet数据集上训练的。因为我们是想把该训练好的模型迁移到我们的花朵分类问题中来,所以我们设置该模型的参数不可训练和更新。

接下来我们打乱一下数据集,以及定义好训练过程中每个“batch”的大小。

# 使用Dataset类的shuffle方法打乱数据集
image_count = len(all_image_paths)
ds = image_label_ds.shuffle(buffer_size=image_count)
# 让数据集重复多次
ds = ds.repeat()
# 设置每个batch的大小
BATCH_SIZE = 32
ds = ds.batch(BATCH_SIZE)
# 通过“prefetch”方法让模型的训练和每个batch数据集的加载并行
ds = ds.prefetch(buffer_size=AUTOTUNE)

在代码中,使用“tf.data.Dataset”类的“shuffle”方法将数据集进行打乱。“repeat”方法让数据集可以重复获取,通常情况下如果我们一个“epoch”只对完整的数据集训练一遍的话,可以不需要设置“repeat”。“repeat”方法可以设置参数,例如“ds.repeat(2)”是让数据集可以重复获取两遍,即一个训练回合(epoch)中我们可以使用两遍数据集,不加参数的话,则默认可以无限次重复获取数据集。

由于“MobileNetV2”模型接收的输入数据是归一化后范围在[-1,1]之间的数据,我们在代码中对数据进行了一次归一化处理后,其范围在[0,1]之间,因此我们需要将我们的数据映射到[-1,1]之间。

def change_range(image,label):
 return 2*image-1, label
# 使用“map”方法对dataset进行处理
keras_ds = ds.map(change_range)

接下来我们定义模型,由于预训练好的“MobileNetV2”返回的数据维度为“(32,6,6,1280)”,其中32是一个“batch”的大小,“6,6”代表输出的特征图的大小为“6X6”,1280代表该层使用了1280个卷积核。为了适应我们的分类任务,我们需要在“MobileNetV2”返回数据的基础上再增加两层网络层。

model = tf.keras.Sequential([
mobile_net,
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(len(label_names))])

全局平均池化(GAP,Global Average Pooling)将每一个特征图求平均,将该平均值作为该特征图池化后的结果,因此经过该操作后数据的维度变为了(32,1280)。由于我们的花朵分类任务是一个5分类的任务,因此我们再使用一个全连接(Dense),将维度变为(32,5)。

接着我们编译一下模型,同时指定使用的优化器和损失函数:

model.compile(optimizer=tf.keras.optimizers.Adam(),
loss='sparse_categorical_crossentropy',
metrics=["accuracy"])
model.summary()

model.summary()可以输出模型各层的参数概况,如图所示:


模型各层的参数概况

最后我们使用“model.fit”训练模型:

model.fit(ds, epochs=1, steps_per_epoch=10)

这里参数“epochs”指定需要训练的回合数,“steps_per_epoch”代表每个回合要取多少个“batch”数据,通常“steps_per_epoch”的大小等于我们数据集的大小除以“batch”的大小后上取整。

https://www.jianshu.com/p/e8ae78bef371

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
NumPy
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论