TensorFlow 实战(一)(4)https://developer.aliyun.com/article/1522667
3.2.1 tf.data API
让我们看看输入管道可能是什么样子。例如,用于您的图像分类任务的输入管道可能看起来像图 3.7。首先,从文本文件中读取整数标签(存储为 [文件名、标签] 记录)。接下来,读取与文件名对应的图像并将其调整为固定的高度和宽度。然后,将标签转换为 one-hot 编码表示。One-hot 编码表示将整数转换为由零和一组成的向量。然后,将图像和 one-hot 编码标签压缩在一起,以保持图像与其相应标签之间的正确对应关系。现在,这些数据可以直接馈送到 Keras 模型中。
图 3.7 您将使用 tf.data API 开发的输入管道
在我们的数据集中,我们有一组花卉图像和一个包含文件名及其对应标签的 CSV 文件。我们将按照以下步骤创建数据管道:
- 将 CSV 文件读取为 tf.data.Dataset。
- 将文件名和标签作为单独的数据集提取出来。
- 读取与文件名数据集中的文件名对应的图像文件。
- 解码图像数据并将其转换为 float32 张量。
- 将图像调整为 64 × 64 像素。
- 将标签转换为 one-hot 编码向量。
- 将图像数据集和 one-hot 向量数据集压缩在一起。
- 将数据集分批为五个样本的批次。
为了将 CSV 文件读取为一个数据集实体,我们将使用方便的 tf.data.experimental.CsvDataset 对象。您可能会发现,实际上,这是一个实验性的对象。这意味着它的测试程度没有 tf.data API 中的其他功能那么多,并且在某些情况下可能会出现问题。但对于我们的小而简单的示例,不会出现任何问题:
import os # Provides various os related functions data_dir = os.path.join('data','flower_images') + os.path.sep csv_ds = tf.data.experimental.CsvDataset( os.path.join(data_dir,'flower_labels.csv') , record_defaults=("",-1), header=True )
tf.data.experimental.CsvDataset 对象需要两个强制参数:一个或多个文件名和一个默认记录,如果记录损坏或不可读,将使用默认记录。在我们的案例中,默认记录是一个空文件名(“”)和标签 -1。您可以通过调用 tf.data.Dataset 打印一些记录
for item in csv_ds.take(5): print(item)
在这里,take() 是一个函数,它以数字作为参数,并从数据集中返回那么多的记录。这将输出以下内容:
(<tf.Tensor: shape=(), dtype=string, numpy=b'0001.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=0>) (<tf.Tensor: shape=(), dtype=string, numpy=b'0002.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=0>) (<tf.Tensor: shape=(), dtype=string, numpy=b'0003.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=2>) (<tf.Tensor: shape=(), dtype=string, numpy=b'0004.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=0>) (<tf.Tensor: shape=(), dtype=string, numpy=b'0005.png'>, <tf.Tensor: shape=(), dtype=int32, numpy=0>)
如果你还记得,flower_labels.csv 文件包含两列:文件名和相应的标签。您可以在数据集输出中看到,每个元组都包含两个元素:文件名和标签。接下来,我们将这两列拆分为两个单独的数据集。这可以很容易地通过使用 map() 函数来完成,该函数将一个给定的函数应用于数据集中的所有记录:
fname_ds = csv_ds.map(lambda a,b: a) label_ds = csv_ds.map(lambda a,b: b)
Lambda 表达式
Lambda 表达式是一个很棒的工具,它使您可以在代码中使用匿名函数。就像普通函数一样,它们接受参数并返回一些输出。例如,以下函数将添加两个给定值(x 和 y):
lambda x, y : x + y
Lambda 表达式是一种很好的写函数的方式,如果它们只被使用一次,因此不需要名称。学会有效使用 lambda 表达式将使您的代码清晰而简洁。
在这里,我们使用简洁的 lambda 表达式告诉 map() 函数我们想要实现什么。现在,我们可以专注于获取图像数据。为了做到这一点,我们将再次使用 map() 函数。但这一次,我们将编写一个单独的函数来定义需要发生的事情:
import tensorflow as tf def get_image(file_path): # loading the image from disk as a byte string img = tf.io.read_file(data_dir + file_path) # convert the compressed string to a 3D uint8 tensor img = tf.image.decode_png(img, channels=3) # Use `convert_image_dtype` to convert to floats in the [0,1] range. img = tf.image.convert_image_dtype(img, tf.float32) # resize the image to the desired size. return tf.image.resize(img, [64, 64])
要从文件名中获取图像张量,我们所需要做的就是将该函数应用于 fname_ds 中的所有文件名:
image_ds = fname_ds.map(get_image)
随着图像数据集的读取,让我们将标签数据转换为独热编码向量:
label_ds = label_ds.map(lambda x: tf.one_hot(x, depth=10))
为了训练图像分类器,我们需要两个项目:一个图像和一个标签。我们确实有这两个作为两个单独的数据集。但是,我们需要将它们合并为一个数据集,以确保一致性。例如,如果我们需要对数据进行洗牌,将数据集合并成一个非常重要,以避免不同的随机洗牌状态,这将破坏数据中的图像到标签的对应关系。tf.data.Dataset.zip() 函数让您可以轻松地做到这一点:
data_ds = tf.data.Dataset.zip((image_ds, label_ds))
我们已经做了大量工作。让我们回顾一下:
- 读取一个包含文件名和标签的 CSV 文件作为 tf.data.Dataset
- 将文件名(fname_ds)和标签(label_ds)分开为两个单独的数据集
- 从文件名加载图像作为数据集(images_ds)同时进行一些预处理
- 将标签转换为独热编码向量
- 使用 zip() 函数创建了一个组合数据集
让我们花点时间看看我们创建了什么。tf.data.Dataset 的行为类似于普通的 python 迭代器。这意味着你可以使用循环(例如 for/while)轻松地迭代项,也可以使用 next() 等函数获取项。让我们看看如何在 for 循环中迭代数据:
for item in data_ds: print(item)
这会返回以下内容:
>>> (<tf.Tensor: shape=(64, 64, 3), dtype=float32, numpy= array([[[0.05490196, 0.0872549 , 0.0372549 ], [0.06764706, 0.09705883, 0.04411765], [0.06862745, 0.09901962, 0.04509804], ..., [0.3362745 , 0.25686276, 0.21274512], [0.26568627, 0.18823531, 0.16176471], [0.2627451 , 0.18627453, 0.16960786]]], dtype=float32)>, <tf.Tensor: shape=(10,), dtype=float32, numpy=array([1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)>)
如你所见,item 是一个元组,第一个元素是图像张量(大小为 64 × 64 × 3),第二个元素是一个独热编码向量(大小为 10)。还有一些工作要做。首先,让我们对数据集进行洗牌,以确保在馈送给模型之前不引入任何有序数据:
data_ds = data_ds.shuffle(buffer_size= 20)
buffer_size 参数起着重要作用。它在运行时指定了加载到内存中用于洗牌的元素数量。在本例中,输入管道将加载 20 条记录到内存中,并在迭代数据时从中随机抽样。较大的 buffer_size 可以提供更好的随机化,但会增加内存需求。接下来,我们将讨论如何从数据集中创建数据批次。
请记住,我们说过 Keras 在创建模型时,如果指定了 input_shape(Sequential API)或 shape(functional API),会自动添加批次维度。这就是深度网络处理数据的方式:作为数据批次(即,不是单个样本)。因此,在将数据馈送到模型之前进行批处理非常重要。例如,如果使用批次大小为 5,如果迭代之前的数据集,你将得到一个大小为 5 × 64 × 64 × 3 的图像张量和一个大小为 5 × 10 的标签张量。使用 tf.data.Dataset API 对数据进行批处理非常简单:
data_ds = data_ds.batch(5)
你可以使用以下方式打印其中一个元素:
for item in data_ds: print(item) break
运行这个命令后,你将得到以下结果:
( <tf.Tensor: shape=(5, 64, 64, 3), dtype=float32, numpy= array( [ [ [ [0.5852941 , 0.5088236 , 0.39411768], [0.5852941 , 0.50980395, 0.4009804 ], [0.5862745 , 0.51176476, 0.40490198], ..., [0.82156867, 0.7294118 , 0.62352943], [0.82745105, 0.74509805, 0.6392157 ], [0.8284314 , 0.75098044, 0.64509803] ], [ [0.07647059, 0.10784315, 0.05882353], [0.07843138, 0.11078432, 0.05882353], [0.11862746, 0.16078432, 0.0892157 ], ..., [0.17745098, 0.23529413, 0.12450981], [0.2019608 , 0.27549022, 0.14509805], [0.22450982, 0.28921568, 0.16470589] ] ] ], dtype=float32 )>, <tf.Tensor: shape=(5, 10), dtype=float32, numpy= array( [ [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.], [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.] ], dtype=float32 )> )
这就是本练习的结束。下面的代码展示了最终的代码的样子。
代码清单 3.5 tf.data 用于花朵图像数据集的输入管道
import tensorflow as tf import os data_dir = os.path.join('data','flower_images', 'flower_images') + os.path.sep csv_ds = tf.data.experimental.CsvDataset( ❶ os.path.join(data_dir,'flower_labels.csv') , ("",-1), header=True ❶ ) ❶ fname_ds = csv_ds.map(lambda a,b: a) ❷ label_ds = csv_ds.map(lambda a,b: b) ❷ def get_image(file_path): img = tf.io.read_file(data_dir + file_path) # convert the compressed string to a 3D uint8 tensor img = tf.image.decode_png(img, channels=3) # Use `convert_image_dtype` to convert to floats in the [0,1] range. img = tf.image.convert_image_dtype(img, tf.float32) # resize the image to the desired size. return tf.image.resize(img, [64, 64]) image_ds = fname_ds.map(get_image) ❸ label_ds = label_ds.map(lambda x: tf.one_hot(x, depth=10)) ❹ data_ds = tf.data.Dataset.zip((image_ds, label_ds)) ❺ data_ds = data_ds.shuffle(buffer_size= 20) ❻ data_ds = data_ds.batch(5) ❻
❶ 使用 TensorFlow 从 CSV 文件中读取数据。
❷ 将文件名和整数标签分开为两个数据集对象
❸ 从文件名中读取图像
❹ 将整数标签转换为独热编码标签
❺ 将图像和标签合并为一个数据集
❻ 对数据进行洗牌和分批处理,为模型做准备。
注意,你无法使用我们在鸢尾花数据集练习中创建的模型,因为那些是全连接网络。我们需要使用卷积神经网络来处理图像数据。为了让你有所了解,练习笔记本 3.2.Creating_Input_ Pipelines.ipynb 中提供了一个非常简单的卷积神经网络模型。不用担心这里使用的各种层和它们的参数,我们将在下一章详细讨论卷积神经网络。
model = Sequential([ Conv2D(64,(5,5), activation='relu', input_shape=(64,64,3)), Flatten(), Dense(10, activation='softmax') ]) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])
使用此输入管道,你可以方便地使用适当的模型馈送数据:
model.fit(data_ds, epochs=10)
运行此命令后,你将获得以下结果:
Epoch 1/10 42/42 [==============================] - 1s 24ms/step - loss: 3.1604 - acc: 0.2571 Epoch 2/10 42/42 [==============================] - 1s 14ms/step - loss: 1.4359 - acc: 0.5190 ... Epoch 9/10 42/42 [==============================] - 1s 14ms/step - loss: 0.0126 - acc: 1.0000 Epoch 10/10 42/42 [==============================] - 1s 15ms/step - loss: 0.0019 - acc: 1.0000
在你上任的第一个星期里迅速取得了一些很好的成果,你自豪地走到老板面前展示你所做的工作。他对你建立的流程的清晰性和高效性感到非常印象深刻。然而,你开始思考,我能用 Keras 数据生成器做得更好吗?
练习 2
想象一下你有一个标签数据集叫 labels_ds(即一个整数标签序列),并且有一些值为 -1 的损坏标签。你能写一个 lambda 函数并将其与 tf.Dataset.map() 函数一起使用来删除这些标签吗?
3.2.2 Keras 数据生成器
另一个获取图像数据的途径是使用 Keras 提供的数据生成器。目前,Keras 提供了两个数据生成器:
tf.keras.preprocessing.image.ImageDataGenerator tf.keras.preprocessing.sequence.TimeSeriesDataGenerator
虽然不像 tf.data API 那样可定制,但这些生成器仍然提供了一种快速简便的方式将数据输入模型。我们来看看如何使用 ImageDataGenerator 将这些数据提供给模型。ImageDataGenerator (mng.bz/lxpB
) 有一个非常长的允许参数列表。在这里,我们只关注如何使 ImageDataGenerator 适应我们所拥有的数据。
然后,为了获取数据,Keras ImageDataGenerator 提供了 flow_from_dataframe() 函数。这个函数对我们来说非常理想,因为我们有一个包含文件名和它们关联标签的 CSV 文件,可以表示为一个 pandas DataFrame。让我们从一些变量定义开始:
data_dir = os.path.join('data','flower_images', 'flower_images')
接下来,我们将使用默认参数定义一个 ImageDataGenerator:
img_gen = ImageDataGenerator()
现在我们可以使用 flow_from_dataframe() 函数:
labels_df = pd.read_csv(os.path.join(data_dir, 'flower_labels.csv'), header=0) gen_iter = img_gen.flow_from_dataframe( dataframe=labels_df, directory=data_dir, x_col='file', y_col='label', class_mode='raw', batch_size=5, target_size=(64,64) )
我们首先加载包含两列的 CSV 文件:file(文件名)和 label(整数标签)。接下来,我们调用 flow_from_dataframe() 函数,同时还有以下重要参数:
- dataframe—包含标签信息的数据框
- directory—定位图像的目录
- x_col—数据框中包含文件名的列的名称
- y_col—包含标签的列的名称
- class_mode—标签的性质(由于我们有原始标签,class_mode 设置为原始)
你可以通过运行下面的代码来查看第一个样本是什么样子的
for item in gen_iter: print(item) break
这将输出
( array([[[[ 10., 11., 11.], [ 51., 74., 46.], [ 36., 56., 32.], ..., [ 4., 4., 3.], [ 16., 25., 11.], [ 17., 18., 13.]], ... [[197., 199., 174.], [162., 160., 137.], [227., 222., 207.], ..., [ 57., 58., 50.], [ 33., 34., 27.], [ 55., 54., 43.]]]], dtype=float32 ), array([5, 6], dtype=int64) )
再次,使用批量大小为 5,你会看到一个图像批(即大小为 5 × 64 × 64 × 3)和一个 one-hot 编码的标签批(大小为 5 × 6)生成为一个元组。完整的代码如下所示。
图 3.6 Keras ImageDataGenerator 用于花卉图像数据集
from tensorflow.keras.preprocessing.image import ImageDataGenerator ❶ import os ❶ import pandas as pd ❶ data_dir = os.path.join('data','flower_images', 'flower_images') ❷ img_gen = ImageDataGenerator() ❸ print(os.path.join(data_dir, 'flower_labels.csv')) labels_df = pd.read_csv(os.path.join(data_dir, 'flower_labels.csv'), header=0)❹ gen_iter = img_gen.flow_from_dataframe( ❺ dataframe=labels_df, directory=data_dir, x_col='file', y_col='label', ❺ class_mode='raw', batch_size=2, target_size=(64,64)) ❺
❶ 导入必要的模块
❷ 定义数据目录
❸ 定义 ImageDataGenerator 来处理图像和标签
❹ 通过读取 CSV 文件作为数据框来定义标签
❺ 从数据框中的文件名和标签读取图像和标签
这看起来比之前的流程更好。你仅用三行代码就创建了一个数据流程。你的知识肯定让你的老板印象深刻,你正在走上快速晋升的道路。
我们将在后面的章节详细讨论 ImageDataGenerator 的参数以及它支持的其他数据检索函数。
然而,要记住简洁并不总是好的。通常,简洁意味着你可以通过这种方法实现的功能有限。对于 tf.data API 和 Keras 数据生成器来说也是如此。tf.data API 尽管需要比 Keras 数据生成器更多的工作,但比 Keras 数据生成器更灵活(并且可以提高效率)。
3.2.3 tensorflow-datasets 包
在 TensorFlow 中检索数据的最简单方法是使用 tensorflow-datasets (www.tensorflow.org/datasets/overview
) 包。然而,一个关键的限制是 tensorflow-datasets 只支持一组定义好的数据集,而不像 tf.data API 或 Keras 数据生成器可以用于从自定义数据集中获取数据。这是一个单独的包,不是官方 TensorFlow 包的一部分。如果你按照说明设置了 Python 环境,你已经在你的环境中安装了这个包。如果没有,你可以通过执行以下命令轻松安装它:
pip install tensorflow-datasets
在你的虚拟 Python 环境的终端(例如,Anaconda 命令提示符)中执行上述命令。为了确保软件包安装正确,运行以下行在你的 Jupyter 笔记本中,确保没有出现任何错误:
import tensorflow_datasets as tfds
tensorflow-datasets 提供了许多不同类别的数据集。你可以在www.tensorflow.org/datasets/catalog
找到一个全面的可用列表。表 3.2 还概述了一些在 tensorflow-datasets 中可用的热门数据集。
表 3.2 tensorflow-datasets 中可用的几个数据集
数据类型 | 数据集名称 | 任务 |
Audio | librispeech | 语音识别 |
ljspeech | 语音识别 | |
Images | caltech101 | 图像分类 |
cifar10 和 cifar100 | 图像分类 | |
imagenet2012 | 图像分类 | |
Text | imdb_reviews | 情感分析 |
tiny_shakespeare | 语言模型 | |
wmt14_translate | 机器翻译 |
让我们使用 tensorflow-datasets 来检索 cifar10 数据集,这是一个广泛使用的图像分类数据集,其中包含属于 10 个类别(例如汽车、船、猫、马等)的 32×32 大小的 RGB 图像。首先,让我们确保它作为一个数据集可用。在 Jupyter 笔记本上执行以下操作:
tfds.list_builders()
我们可以看到 cifar10 是其中一个数据集,正如我们所期望的那样。让我们使用 tfds.load()函数加载数据集。当你首次调用这个方法时,TensorFlow 会先下载数据集,然后为你加载它:
data, info = tfds.load("cifar10", with_info=True)
当它成功下载后,查看(info)变量中可用的信息:
print(info) >>> tfds.core.DatasetInfo( name='cifar10', version=3.0.0, description='The CIFAR-10 dataset consists of 60000 32x32 colour images in 10 classes, with 6000 images per class. There are 50000 training images and 10000 test images.', homepage='https:/ /www.cs.toronto.edu/~kriz/cifar.xhtml', features=FeaturesDict({ 'image': Image(shape=(32, 32, 3), dtype=tf.uint8), 'label': ClassLabel(shape=(), dtype=tf.int64, num_classes=10), }), total_num_examples=60000, splits={ 'test': 10000, 'train': 50000, }, supervised_keys=('image', 'label'), citation="""@TECHREPORT{Krizhevsky09learningmultiple, author = {Alex Krizhevsky}, title = {Learning multiple layers of features from tiny images}, institution = {}, year = {2009} }""", redistribution_info=, )
这非常有信息量。我们现在知道有 60,000 个 32 × 32 的彩色图像属于 10 个类别。数据集分为 50,000(训练)和 10,000(测试)。现在让我们看看数据变量:
print(data) >>> {'test': <DatasetV1Adapter shapes: {image: (32, 32, 3), label: ()}, types: {image: tf.uint8, label: tf.int64}>, 'train': <DatasetV1Adapter shapes: {image: (32, 32, 3), label: ()}, types: {image: tf.uint8, label: tf.int64}> }
我们可以看到它是一个包含键“train”和“test”的字典,每个键都有一个 tf.data.Dataset。幸运的是,我们已经学习了 tf.data.Dataset 的工作原理,所以我们可以快速了解如何准备数据。让我们看一下训练数据。你可以通过以下方式访问这个训练数据集。
train_ds = data["train"]
然而,如果你尝试迭代这个数据集,你会注意到数据并没有被分批。换句话说,数据是一次检索一个样本。但是,正如我们已经说过很多次的那样,我们需要批量数据。修复方法很简单:
train_ds = data["train"].batch(16)
现在,为了看一下 train_ds 中的一批数据是什么样子,你可以执行以下操作:
for item in train_ds: print(item) break
这将输出
{ 'id': <tf.Tensor: shape=(16,), dtype=string, numpy= array( [ b'train_16399', b'train_01680', b'train_47917', b'train_17307', b'train_27051', b'train_48736', b'train_26263', b'train_01456', b'train_19135', b'train_31598', b'train_12970', b'train_04223', b'train_27152', b'train_49635', b'train_04093', b'train_17537' ], dtype=object )>, 'image': <tf.Tensor: shape=(16, 32, 32, 3), dtype=uint8, numpy= array( [ [ [ [143, 96, 70], [141, 96, 72], [135, 93, 72], ..., [128, 93, 60], [129, 94, 61], [123, 91, 58] ] ] ], dtype=uint8 )>, 'label': <tf.Tensor: shape=(16,), dtype=int64, numpy= array( [7, 8, 4, 4, 6, 5, 2, 9, 6, 6, 9, 9, 3, 0, 8, 7], dtype=int64 )> }
它将是一个包含三个键的字典:id、image 和 label。id 是每个训练记录的唯一标识。image 将有一个大小为 16 × 32 × 32 × 3 的张量,而 label 将有一个大小为 16 的张量(即整数标签)。当将 tf.data.Dataset 传递给 Keras 模型时,模型期望数据集对象产生一个元组 (x,y),其中 x 是一批图像,y 是标签(例如,one-hot 编码)。因此,我们需要编写一个额外的函数,将数据放入正确的格式:
def format_data(x): return (x["image"], tf.one_hot(x["label"], depth=10)) train_ds = train_ds.map(format_data)
通过这个简单的转换,你可以将这个数据集馈送给一个模型,方法如下:
model.fit(train_ds, epochs=25)
这是令人惊讶的工作。现在你知道了为模型检索数据的三种不同方法:tf.data API、Keras 数据生成器和 tensorflow-datasets 包。我们将在这里结束对 Keras API 和不同数据导入 API 的讨论。
练习 3
你能写一行代码导入 caltech101 数据集吗?在你这样做之后,探索这个数据集。
摘要
- Keras,现在已经集成到 TensorFlow 中,提供了几种高级模型构建 API:串行 API、功能 API 和子类化 API。这些 API 有不同的优缺点。
- 串行 API 是使用最简单的,但只能用于实现简单的模型。
- 功能和子类化 API 可能难以使用,但允许开发人员实现复杂的模型。
- TensorFlow 包含几种获取数据的方法:tf.data API、Keras 数据生成器和 tensorflow-datasets。tf.data。
- API 提供了向模型提供数据的最可定制方式,但需要更多的工作来获取数据。
- tensorflow-datasets 是使用最简单的,但是它有限,因为它只支持有限的数据集。
练习答案
练习 1: 功能 API。由于有两个输出层,我们不能使用串行 API。没有必要使用子类化 API,因为我们需要的一切都可以使用 Keras 层完成。
练习 2: labels_ds.map(lambda x: x if x != -1). 你也可以使用 tf.Dataset .filter() 方法(即 labels_ds.filter(lambda x: x != -1))。
练习 3: tfds.load(“caltech101”, with_info=True)