tf2-pkt-ref-merge-1

175 阅读34分钟

TensorFlow2 口袋参考(二)

原文:PyTorch Pocket Reference

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:可重用的模型元素

开发一个 ML 模型可能是一项艰巨的任务。除了任务的数据工程方面,您还需要了解如何构建模型。在 ML 的早期阶段,基于树的模型(如随机森林)是将直接应用于表格数据集的分类或回归任务的王者,模型架构是由与模型初始化相关的参数确定的。这些参数,称为超参数,包括森林中的决策树数量以及在拆分节点时每棵树考虑的特征数量。然而,将某些类型的数据,如图像或文本,转换为表格形式并不是直截了当的:图像可能具有不同的尺寸,文本长度也不同。这就是为什么深度学习已经成为图像和文本分类的事实标准模型架构的原因。

随着深度学习架构的流行,围绕它形成了一个社区。创建者为学术和 Kaggle 挑战构建和测试了不同的模型结构。许多人已经将他们的模型开源,以便进行迁移学习-任何人都可以将它们用于自己的目的。

例如,ResNet 是在 ImageNet 数据集上训练的图像分类模型,该数据集约为 150GB,包含超过一百万张图像。这些数据中的标签包括植物、地质形态、自然物体、体育、人物和动物。那么您如何重用 ResNet 模型来对您自己的图像集进行分类,即使具有不同的类别或标签?

像 ResNet 这样的开源模型具有非常复杂的结构。虽然源代码可以在 GitHub 等网站上供任何人访问,但下载源代码并不是复制或重用这些模型的最用户友好的方式。通常还有其他依赖项需要克服才能编译或运行源代码。那么我们如何使这些模型对非专家可用和可用?

TensorFlow Hub(TFH)旨在解决这个问题。它通过将各种 ML 模型作为库或 Web API 调用免费提供,从而实现迁移学习。任何人都可以写一行代码来加载模型。所有模型都可以通过简单的 Web 调用调用,然后整个模型将下载到您的源代码运行时。您不需要自己构建模型。

这绝对节省了开发和训练时间,并增加了可访问性。它还允许用户尝试不同的模型并更快地构建自己的应用程序。迁移学习的另一个好处是,由于您不是从头开始重新训练整个模型,因此您可能不需要高性能的 GPU 或 TPU 即可开始。

在本章中,我们将看一看如何轻松利用 TensorFlow Hub。所以让我们从 TFH 的组织方式开始。然后您将下载 TFH 预训练的图像分类模型之一,并看看如何将其用于您自己的图像。

基本的 TensorFlow Hub 工作流程

TensorFlow Hub(图 4-1)是由 Google 策划的预训练模型的存储库。用户可以将任何模型下载到自己的运行时,并使用自己的数据进行微调和训练。

TensorFlow Hub 主页

图 4-1. TensorFlow Hub 主页

要使用 TFH,您必须通过熟悉的 Pythonic pip install命令在您的 Python 单元格或终端中安装它:

pip install --upgrade tensorflow_hub

然后您可以通过导入它在您的源代码中开始使用它:

import tensorflow_hub as hub

首先,调用模型:

model = hub.KerasLayer("https://tfhub.dev/google/nnlm-en-dim128/2")

这是一个预训练的文本嵌入模型。文本嵌入是将文本字符串映射到数字表示的多维向量的过程。您可以给这个模型四个文本字符串:

embeddings = model(["The rain in Spain.", "falls",
                      "mainly", "In the plain!"])

在查看结果之前,请检查模型输出的形状:

print(embeddings.shape)

应该是:

(4, 128)

有四个输出,每个输出长度为 128 个单位。图 4-2 显示其中一个输出:

print(embeddings[0])

文本嵌入输出

图 4-2. 文本嵌入输出

正如在这个简单示例中所示,您没有训练这个模型。您只是加载它并用它来处理您自己的数据。这个预训练模型简单地将每个文本字符串转换为一个 128 维的向量表示。

在 TensorFlow Hub 首页,点击“Models”选项卡。如您所见,TensorFlow Hub 将其预训练模型分类为四个问题领域:图像、文本、视频和音频。

图 4-3 展示了迁移学习模型的一般模式。

从图 4-3 中,您可以看到预训练模型(来自 TensorFlow Hub)被夹在输入层和输出层之间,输出层之前可能还有一些可选层。

迁移学习的一般模式

图 4-3. 迁移学习的一般模式

要使用任何模型,您需要解决一些重要的考虑因素,例如输入和输出:

输入层

输入数据必须被正确格式化(或“塑造”),因此请特别注意每个模型的输入要求(在描述各个模型的网页上的“Usage”部分中找到)。以ResNet 特征向量为例:Usage 部分说明了输入图像所需的大小和颜色值,以及输出是一批特征向量。如果您的数据不符合要求,您需要应用一些数据转换技术,这些技术可以在“为处理准备图像数据”中学到。

输出层

另一个重要且必要的元素是输出层。如果您希望使用自己的数据重新训练模型,这是必须的。在之前展示的简单嵌入示例中,我们没有重新训练模型;我们只是输入了一些文本字符串来查看模型的输出。输出层的作用是将模型的输出映射到最可能的标签,如果问题是分类问题的话。如果是回归问题,那么它的作用是将模型的输出映射到一个数值。典型的输出层称为“密集层”,可以是一个节点(用于回归或二元分类)或多个节点(例如用于多类分类)。

可选层

可选地,您可以在输出层之前添加一个或多个层以提高模型性能。这些层可以帮助您提取更多特征以提高模型准确性,例如卷积层(Conv1D、Conv2D)。它们还可以帮助防止或减少模型过拟合。例如,通过随机将输出设置为零,dropout 可以减少过拟合。如果一个节点输出一个数组,例如[0.5, 0.1, 2.1, 0.9],并且您设置了 0.25 的 dropout 比率,那么在训练过程中,根据随机机会,数组中的四个值中的一个将被设置为零;例如,[0.5, 0, 2.1, 0.9]。再次强调,这是可选的。您的训练不需要它,但它可能有助于提高模型的准确性。

通过迁移学习进行图像分类

我们将通过一个使用迁移学习的图像分类示例来进行讲解。在这个示例中,您的图像数据包括五类花。您将使用 ResNet 特征向量作为预训练模型。我们将解决以下常见任务:

  • 模型要求

  • 数据转换和输入处理

  • TFH 模型实现

  • 输出定义

  • 将输出映射到纯文本格式

模型要求

让我们看看ResNet v1_101 特征向量模型。这个网页包含了一个概述、一个下载 URL、说明以及最重要的是您需要使用该模型的代码。

在使用部分中,您可以看到要加载模型,您只需要将 URL 传递给hub.KerasLayer。使用部分还包括模型要求。默认情况下,它期望输入图像,写为形状数组[高度,宽度,深度],为[224, 224, 3]。像素值应在范围[0, 1]内。作为输出,它提供了具有节点数的Dense层,反映了训练图像中类别的数量。

数据转换和输入处理

您的任务是将图像转换为所需的形状,并将像素比例标准化到所需范围内。正如我们所见,图像通常具有不同的大小和像素值。每个 RGB 通道的典型彩色 JPEG 图像像素值可能在 0 到 225 之间。因此,我们需要操作来将图像大小标准化为[224, 224, 3],并将像素值标准化为[0, 1]范围。如果我们在 TensorFlow 中使用ImageDataGenerator,这些操作将作为输入标志提供。以下是如何加载图像并创建生成器:

  1. 首先加载库:

    import tensorflow as tf
    import tensorflow_hub as hub
    import numpy as np
    import matplotlib.pylab as plt
    
  2. 加载所需的数据。在这个例子中,让我们使用 TensorFlow 提供的花卉图像:

    data_dir = tf.keras.utils.get_file(
        'flower_photos',
    'https://storage.googleapis.com/download.tensorflow.org/
    example_images/flower_photos.tgz',
        untar=True)
    
  3. 打开data_dir并找到图像。您可以在文件路径中看到文件结构:

    !ls -lrt /root/.keras/datasets/flower_photos
    

    这是将显示的内容:

    total 620
    -rw-r----- 1 270850 5000 418049 Feb  9  2016 LICENSE.txt
    drwx------ 2 270850 5000  45056 Feb 10  2016 tulips
    drwx------ 2 270850 5000  40960 Feb 10  2016 sunflowers
    drwx------ 2 270850 5000  36864 Feb 10  2016 roses
    drwx------ 2 270850 5000  53248 Feb 10  2016 dandelion
    drwx------ 2 270850 5000  36864 Feb 10  2016 daisy
    

    有五类花卉。每个类对应一个目录。

  4. 定义一些全局变量来存储像素值和批量大小(训练图像批次中的样本数)。目前您只需要图像的高度和宽度,不需要图像的第三个维度:

    pixels =224
    BATCH_SIZE = 32
    IMAGE_SIZE = (pixels, pixels)
    NUM_CLASSES = 5
    
  5. 指定图像标准化和用于交叉验证的数据分数。将一部分训练数据保留用于交叉验证是一个好主意,这是通过每个时代评估模型训练过程的一种方法。在每个训练时代结束时,模型包含一组经过训练的权重和偏差。此时,用于交叉验证的数据,模型从未见过,可以用作模型准确性的测试:

    datagen_kwargs = dict(rescale=1./255, validation_split=.20)
    dataflow_kwargs = dict(target_size=IMAGE_SIZE, 
    batch_size=BATCH_SIZE,
    interpolation="bilinear")
    
    valid_datagen = tf.keras.preprocessing.image.
    ImageDataGenerator(
        **datagen_kwargs)
    valid_generator = valid_datagen.flow_from_directory(
        data_dir, subset="validation", shuffle=False, 
        **dataflow_kwargs)
    

    ImageDataGenerator定义和生成器实例都以字典格式接受我们的参数。重新缩放因子和验证分数进入生成器定义,而标准化图像大小和批量大小进入生成器实例。

    插值参数表示生成器需要将图像数据重新采样到target_size,即 224×224 像素。

    现在,对训练数据生成器执行相同操作:

    train_datagen = valid_datagen
    train_generator = train_datagen.flow_from_directory(
        data_dir, subset="training", shuffle=True, 
        **dataflow_kwargs)
    
  6. 识别类索引到类名的映射。由于花卉类别被编码在索引中,您需要一个映射来恢复花卉类别名称:

    labels_idx = (train_generator.class_indices)
    idx_labels = dict((v,k) for k,v in labels_idx.items())
    

    您可以显示idx_labels以查看这些类是如何映射的:

    idx_labels
    
    {0: 'daisy', 1: 'dandelion', 2: 'roses', 3: 'sunflowers',
    4: 'tulips'}
    

现在您已经对图像数据进行了标准化和标准化。图像生成器已被定义并实例化用于训练和验证数据。您还具有标签查找来解码模型预测,并且已准备好使用 TFH 实现模型。

使用 TensorFlow Hub 实现模型

正如您在图 4-3 中看到的,预训练模型被夹在输入层和输出层之间。您可以相应地定义这个模型结构:

model = tf.keras.Sequential([
     tf.keras.layers.InputLayer(input_shape=IMAGE_SIZE + (3,)),
hub.KerasLayer("https://tfhub.dev/google/imagenet/resnet_v1_101/
feature_vector/4", trainable=False),
     tf.keras.layers.Dense(NUM_CLASSES, activation='softmax', 
name = 'flower_class') 
])

model.build([None, 224, 224, 3]) !!C04!!

注意这里有几点:

  • 有一个输入层,定义图像的输入形状为[224, 224, 3]。

  • 当调用InputLayer时,trainable应设置为 False。这表示您希望重用预训练模型的当前值。

  • 有一个名为Dense的输出层提供模型输出(这在摘要页面的使用部分中有描述)。

构建模型后,您可以开始训练。首先,指定损失函数并选择优化器:

model.compile(
  optimizer=tf.keras.optimizers.SGD(lr=0.005, momentum=0.9),
loss=tf.keras.losses.CategoricalCrossentropy(
from_logits=True, 
label_smoothing=0.1),
metrics=['accuracy'])

然后指定用于训练数据和交叉验证数据的批次数:

steps_per_epoch = train_generator.samples // 
train_generator.batch_size
validation_steps = valid_generator.samples // 
valid_generator.batch_size

然后开始训练过程:

model.fit(
    train_generator,
    epochs=5, steps_per_epoch=steps_per_epoch,
    validation_data=valid_generator,
    validation_steps=validation_steps)

在经过指定的所有时代运行训练过程后,模型已经训练完成。

定义输出

根据使用指南,输出层Dense由一定数量的节点组成,反映了预期图像中有多少类别。这意味着每个节点为该类别输出一个概率。您的任务是找到这些概率中哪一个最高,并使用idx_labels将该节点映射到花卉类别。回想一下,idx_labels字典如下所示:

{0: 'daisy', 1: 'dandelion', 2: 'roses', 3: 'sunflowers', 
4: 'tulips'}

Dense层的输出由五个节点以完全相同的顺序组成。您需要编写几行代码将具有最高概率的位置映射到相应的花卉类别。

将输出映射到纯文本格式

让我们使用验证图像来更好地了解如何将模型预测输出映射到每个图像的实际类别。您将使用predict函数对这些验证图像进行评分。检索第一批次的 NumPy 数组:

sample_test_images, ground_truth_labels = next(valid_generator)

prediction = model.predict(sample_test_images)

在交叉验证数据中有 731 张图像和 5 个对应的类别。因此,输出形状为[731, 5]:

array([[9.9994004e-01, 9.4704428e-06, 3.8405190e-10, 5.0486942e-05,
        1.0701914e-08],
       [5.9500107e-06, 3.1842374e-06, 3.5622744e-08, 9.9999082e-01,
        3.0683900e-08],
       [9.9994218e-01, 5.9974178e-07, 5.8693445e-10, 5.7049790e-05,
        9.6709634e-08],
       ...,
       [3.1268091e-06, 9.9986601e-01, 1.5343730e-06, 1.2935932e-04,
        2.7383029e-09],
       [4.8439368e-05, 1.9247003e-05, 1.8034354e-01, 1.6394027e-02,
        8.0319476e-01],
       [4.9799957e-07, 9.9232978e-01, 3.5823192e-08, 7.6697678e-03,
        1.7666844e-09]], dtype=float32)

每行代表了图像类别的概率分布。对于第一张图像,最高概率为 1.0701914e-08(在上述代码中突出显示),位于该行的最后位置,对应于该行的索引 4(请记住,索引的编号从 0 开始)。

现在,您需要使用以下代码找到每行中最高概率出现的位置:

predicted_idx = tf.math.argmax(prediction, axis = -1)

如果您使用print命令显示结果,您将看到以下内容:

print (predicted_idx)

<tf.Tensor: shape=(731,), dtype=int64, numpy=
array([0, 3, 0, 1, 0, 4, 4, 1, 2, 3, 4, 1, 4, 0, 4, 3, 1, 4, 4, 0,
       …
       3, 2, 1, 4, 1])>

现在,对该数组中的每个元素应用idx_labels的查找。对于每个元素,使用一个函数:

def find_label(idx):
    return idx_labels[idx]

要将函数应用于 NumPy 数组的每个元素,您需要对函数进行矢量化:

find_label_batch = np.vectorize(find_label)

然后将此矢量化函数应用于数组中的每个元素:

result = find_label_batch(predicted_idx)

最后,将结果与图像文件夹和文件名并排输出,以便保存以供报告或进一步调查。您可以使用 Python pandas DataFrame 操作来实现这一点:

import pandas as pd
predicted_label = result_class.tolist()
file_name = valid_generator.filenames

results=pd.DataFrame({"File":file_name,
                      "Prediction":predicted_label})

让我们看看results数据框,它是 731 行×2 列。

 文件预测
0daisy/100080576_f52e8ee070_n.jpgdaisy雏菊
1daisy/10140303196_b88d3d6cec.jpgsunflowers向日葵
2daisy/10172379554_b296050f82_n.jpgdaisy雏菊
3daisy/10172567486_2748826a8b.jpgdandelion玛丽金花
4daisy/10172636503_21bededa75_n.jpgdaisy雏菊
.........
726tulips/14068200854_5c13668df9_m.jpgsunflowers向日葵
727tulips/14068295074_cd8b85bffa.jpgroses玫瑰
728tulips/14068348874_7b36c99f6a.jpgdandelion郁金香
729tulips/14068378204_7b26baa30d_n.jpgtulips郁金香
730tulips/14071516088_b526946e17_n.jpgdandelion玛丽金花

评估:创建混淆矩阵

混淆矩阵通过比较模型输出和实际情况来评估分类结果,是了解模型表现的最简单方法。让我们看看如何创建混淆矩阵。

您将使用 pandas Series 作为构建混淆矩阵的数据结构:

y_actual = pd.Series(valid_generator.classes)
y_predicted = pd.Series(predicted_idx)

然后,您将再次利用 pandas 生成矩阵:

pd.crosstab(y_actual, y_predicted, rownames = ['Actual'],
colnames=['Predicted'], margins=True)

图 4-4 显示了混淆矩阵。每行代表了实际花标签的分布情况。例如,看第一行,您会注意到总共有 126 个样本实际上是类别 0,即雏菊。模型正确地将这些图像中的 118 个预测为类别 0;四个被错误分类为类别 1,即蒲公英;一个被错误分类为类别 2,即玫瑰;三个被错误分类为类别 3,即向日葵;没有被错误分类为类别 4,即郁金香。

花卉图像分类的混淆矩阵

图 4-4. 花卉图像分类的混淆矩阵

接下来,使用sklearn库为每个图像类别提供统计报告:

from sklearn.metrics import classification_report
report = classification_report(truth, predicted_results)
print(report)
              precision    recall  f1-score   support

           0       0.90      0.94      0.92       126
           1       0.93      0.87      0.90       179
           2       0.85      0.86      0.85       128
           3       0.85      0.88      0.86       139
           4       0.86      0.86      0.86       159

    accuracy                           0.88       731
   macro avg       0.88      0.88      0.88       731
weighted avg       0.88      0.88      0.88       731

这个结果表明,当对雏菊(类别 0)进行分类时,该模型的性能最佳,f1 分数为 0.92。在对玫瑰(类别 2)进行分类时,其性能最差,f1 分数为 0.85。“支持”列显示了每个类别中的样本量。

总结

您刚刚完成了一个使用来自 TensorFlow Hub 的预训练模型的示例项目。您添加了必要的输入层,执行了数据归一化和标准化,训练了模型,并对一批图像进行了评分。

这个经验表明了满足模型的输入和输出要求的重要性。同样重要的是,要密切关注预训练模型的输出格式。(这些信息都可以在 TensorFlow Hub 网站上的模型文档页面找到。)最后,您还需要创建一个函数,将模型的输出映射到纯文本,以使其具有意义并可解释。

使用 tf.keras.applications 模块进行预训练模型

另一个为您自己使用找到预训练模型的地方是 tf.keras.applications 模块(请参阅可用模型列表)。当 Keras API 在 TensorFlow 中可用时,该模块成为 TensorFlow 生态系统的一部分。

每个模型都带有预训练的权重,使用它们和使用 TensorFlow Hub 一样简单。Keras 提供了方便地微调模型所需的灵活性。通过使模型中的每一层可访问,tf.keras.applications 让您可以指定哪些层要重新训练,哪些层保持不变。

使用 tf.keras.applications 实现模型

与 TensorFlow Hub 一样,您只需要一行代码从 Keras 模块加载一个预训练模型:

base_model = tf.keras.applications.ResNet101V2(
input_shape = (224, 224, 3), 
include_top = False, 
weights = 'imagenet')

注意 include_top 输入参数。请记住,您需要为自己的数据添加一个输出层。通过将 include_top 设置为 False,您可以为分类输出添加自己的 Dense 层。您还将从 imagenet 初始化模型权重。

然后将 base_model 放入一个顺序架构中,就像您在 TensorFlow Hub 示例中所做的那样:

model2 = tf.keras.Sequential([
  base_model,
  tf.keras.layers.GlobalAveragePooling2D(),
  tf.keras.layers.Dense(NUM_CLASSES, 
  activation = 'softmax', 
  name = 'flower_class')
])

添加 GlobalAveragePooling2D,将输出数组平均为一个数值,然后将其发送到最终的 Dense 层进行预测。

现在编译模型并像往常一样启动训练过程:

model2.compile(
  optimizer=tf.keras.optimizers.SGD(lr=0.005, momentum=0.9),
  loss=tf.keras.losses.CategoricalCrossentropy(
  from_logits=True, label_smoothing=0.1),
  metrics=['accuracy']
)

model2.fit(
    train_generator,
    epochs=5, steps_per_epoch=steps_per_epoch,
    validation_data=valid_generator,
    validation_steps=validation_steps)

要对图像数据进行评分,请按照您在 “将输出映射到纯文本格式” 中所做的步骤进行。

从 tf.keras.applications 微调模型

如果您希望通过释放一些基础模型的层进行训练来尝试您的训练例程,您可以轻松地这样做。首先,您需要找出基础模型中有多少层,并将基础模型指定为可训练的:

print("Number of layers in the base model: ", 
       len(base_model.layers))
base_model.trainable = True

Number of layers in the base model:  377

如所示,在这个版本的 ResNet 模型中,有 377 层。通常我们从模型末尾附近的层开始重新训练过程。在这种情况下,将第 370 层指定为微调的起始层,同时保持在第 300 层之前的权重不变:

fine_tune_at = 370

for layer in base_model.layers[: fine_tune_at]:
  layer.trainable = False

然后使用 Sequential 类将模型组合起来:

model3 = tf.keras.Sequential([
  base_model,
  tf.keras.layers.GlobalAveragePooling2D(),
  tf.keras.layers.Dense(NUM_CLASSES, 
  activation = 'softmax', 
  name = 'flower_class')
])
提示

您可以尝试使用 tf.keras.layers.Flatten() 而不是 tf.keras.layers.GlobalAveragePooling2D(),看看哪一个给您一个更好的模型。

编译模型,指定优化器和损失函数,就像您在 TensorFlow Hub 中所做的那样:

model3.compile(
  optimizer=tf.keras.optimizers.SGD(lr=0.005, momentum=0.9),
  loss=tf.keras.losses.CategoricalCrossentropy(
  from_logits=True, 
  label_smoothing=0.1),
  metrics=['accuracy']
)

启动训练过程:

fine_tune_epochs = 5
steps_per_epoch = train_generator.samples // 
train_generator.batch_size
validation_steps = valid_generator.samples // 
valid_generator.batch_size
model3.fit(
    train_generator,
    epochs=fine_tune_epochs, 
    steps_per_epoch=steps_per_epoch,
    validation_data=valid_generator,
    validation_steps=validation_steps)

由于您已经释放了更多基础模型的层进行重新训练,这个训练可能需要更长时间。训练完成后,对测试数据进行评分,并按照 “将输出映射到纯文本格式” 和 “评估:创建混淆矩阵” 中描述的方式比较结果。

结束

在这一章中,您学习了如何使用预训练的深度学习模型进行迁移学习。有两种方便的方式可以访问预训练模型:TensorFlow Hub 和 tf.keras.applications 模块。两者都简单易用,具有优雅的 API 和风格,可以快速开发模型。然而,用户需要正确地塑造他们的输入数据,并提供一个最终的 Dense 层来处理模型输出。

有大量免费可访问的预训练模型,具有丰富的库存,您可以使用它们来处理自己的数据。利用迁移学习来利用它们,让您花费更少的时间来构建、训练和调试模型。

第五章:流摄入的数据管道

数据摄入是你工作流程中的一个重要部分。在原始数据达到模型期望的正确输入格式之前,需要执行几个步骤。这些步骤被称为 数据管道。数据管道中的步骤很重要,因为它们也将应用于生产数据,这是模型在部署时使用的数据。无论你是在构建和调试模型还是准备部署模型,你都需要为模型的消费格式化原始数据。

在模型构建过程中使用与部署规划相同的一系列步骤是很重要的,这样测试数据就会与训练数据以相同的方式处理。

在第三章中,你学习了 Python 生成器的工作原理,在第四章中,你学习了如何使用 flow_from_directory 方法进行迁移学习。在本章中,你将看到 TensorFlow 提供的更多处理其他数据类型(如文本和数值数组)的工具。你还将学习如何处理另一种图像文件结构。当处理文本或图像进行模型训练时,文件组织变得尤为重要,因为通常会使用目录名称作为标签。本章将在构建和训练文本或图像分类模型时推荐一种目录组织实践。

使用 text_dataset_from_directory 函数流式文本文件

只要正确组织目录结构,你几乎可以在管道中流式传输任何文件。在本节中,我们将看一个使用文本文件的示例,这在文本分类和情感分析等用例中会很有用。这里我们感兴趣的是 text_dataset_from_directory 函数,它的工作方式类似于我们用于流式传输图像的 flow_from_directory 方法。

为了将这个函数用于文本分类问题,你必须按照本节中描述的目录组织。在你当前的工作目录中,你必须有与文本标签或类名匹配的子目录。例如,如果你正在进行文本分类模型训练,你必须将训练文本组织成积极和消极。这是训练数据标记的过程;必须这样做以设置数据,让模型学习积极或消极评论的样子。如果文本是被分类为积极或消极的电影评论语料库,那么子目录的名称可能是 posneg。在每个子目录中,你有该类别的所有文本文件。因此,你的目录结构将类似于这样:

Current working directory
    pos
        p1.txt
        p2.txt
    neg
        n1.txt
        n2.txt

举个例子,让我们尝试使用来自互联网电影数据库(IMDB)的电影评论语料库构建一个文本数据摄入管道。

下载文本数据并设置目录

你将在本节中使用的文本数据是大型电影评论数据集。你可以直接下载它,也可以使用 get_file 函数来下载。让我们首先导入必要的库,然后下载文件:

import io
import os
import re
import shutil
import string
import tensorflow as tf

url = "https://ai.stanford.edu/~amaas/data/sentiment/
       aclImdb_v1.tar.gz"

ds = tf.keras.utils.get_file("aclImdb_v1.tar.gz", url,
                                    untar=True, cache_dir='.',
                                    cache_subdir='')

通过传递 untar=Trueget_file 函数也会解压文件。这将在当前目录中创建一个名为 aclImdb 的目录。让我们将这个文件路径编码为一个变量以供将来参考:

ds_dir = os.path.join(os.path.dirname(ds), 'aclImdb')

列出这个目录以查看里面有什么:

train_dir = os.path.join(ds_dir, 'train')
os.listdir(train_dir)

['neg',
 'unsup',
 'urls_neg.txt',
 'urls_unsup.txt',
 'pos',
 'urls_pos.txt',
 'unsupBow.feat',
 'labeledBow.feat']

有一个目录(unsup)没有在使用中,所以你需要将其删除:

unused_dir = os.path.join(train_dir, 'unsup')
shutil.rmtree(unused_dir)

现在看一下训练目录中的内容:

!ls -lrt ./aclImdb/train
-rw-r--r-- 1 7297 1000  2450000 Apr 12  2011 urls_unsup.txt
drwxr-xr-x 2 7297 1000   364544 Apr 12  2011 pos
drwxr-xr-x 2 7297 1000   356352 Apr 12  2011 neg
-rw-r--r-- 1 7297 1000   612500 Apr 12  2011 urls_pos.txt
-rw-r--r-- 1 7297 1000   612500 Apr 12  2011 urls_neg.txt
-rw-r--r-- 1 7297 1000 21021197 Apr 12  2011 labeledBow.feat
-rw-r--r-- 1 7297 1000 41348699 Apr 12  2011 unsupBow.feat

这两个目录是 posneg。这些名称将在文本分类任务中被编码为分类变量。

清理子目录并确保所有目录都包含用于分类训练的文本非常重要。如果我们没有删除那个未使用的目录,它的名称将成为一个分类变量,这绝不是我们的意图。那里的其他文件都很好,不会影响这里的结果。再次提醒,目录名称用作标签,因此请确保只有用于模型学习和映射到标签的目录。

创建数据流水线

现在您的文件已经正确组织,可以开始创建数据流水线了。让我们设置一些变量:

batch_size = 1024
seed = 123

批量大小告诉生成器在训练的一个迭代中使用多少样本。还可以分配一个种子,以便每次执行生成器时,它以相同的顺序流式传输文件。如果不分配种子,生成器将以随机顺序输出文件。

然后使用test_dataset_from_directory函数定义一个流水线。它将返回一个数据集对象:

train_ds = tf.keras.preprocessing.text_dataset_from_directory(
    'aclImdb/train', batch_size=batch_size, validation_split=0.2,
    subset='training', seed=seed)

在这种情况下,包含子目录的目录是aclImdb/train。此流水线定义用于 80%的训练数据集,由subset='training'指定。其他 20%用于交叉验证。

对于交叉验证数据,您将以类似的方式定义流水线:

val_ds = tf.keras.preprocessing.text_dataset_from_directory(
    'aclImdb/train', batch_size=batch_size, validation_split=0.2,
    subset='validation', seed=seed)

一旦您在上述代码中执行了这两个流水线,这就是预期的输出:

Found 25000 files belonging to 2 classes.
Using 20000 files for training.
Found 25000 files belonging to 2 classes.
Using 5000 files for validation.

因为aclImdb/train中有两个子目录,生成器将其识别为类。由于 20%的拆分,有 5,000 个文件用于交叉验证。

检查数据集

有了生成器,让我们来查看这些文件的内容。检查 TensorFlow 数据集的方法是遍历它并选择一些样本。以下代码片段获取第一批样本,然后随机选择五行电影评论:

import random
idx = random.sample(range(1, batch_size), 5)
for text_batch, label_batch in train_ds.take(1):
  for i in idx:
    print(label_batch[i].numpy(), text_batch.numpy()[i])

在这里,idx是一个列表,其中包含在batch_size范围内生成的五个随机整数。然后,idx被用作索引,从数据集中选择文本和标签。

数据集将产生一个元组,包含text_batchlabel_batch;元组在这里很有用,因为它跟踪文本及其标签(类)。这是五个随机选择的文本行和相应的标签:

1 b'Very Slight Spoiler<br /><br /> This movie (despite being….
1 b"Not to mention easily Pierce Brosnan's best performance….
0 b'Bah. Another tired, desultory reworking of an out of copyright…
0 b'All the funny things happening in this sitcom is based on the…
0 b'This is another North East Florida production, filmed mainly…

前两个是正面评价(由数字 1 表示),最后三个是负面评价(由 0 表示)。这种方法称为按类分组

总结

在本节中,您学习了如何流式传输文本数据集。该方法类似于图像的流式传输,唯一的区别是使用text_dataset_from_directory函数。您学习了按类分组以及数据的推荐目录组织方式,这很重要,因为目录名称用作模型训练过程中的标签。在图像和文本分类中,您看到目录名称被用作标签。

使用 flow_from_dataframe 方法流式传输图像文件列表

数据的组织方式影响您处理数据摄入流水线的方式。这在处理图像数据时尤为重要。在第四章中的图像分类任务中,您看到不同类型的花卉是如何组织到与每种花卉类型对应的目录中的。

按类分组不是您在现实世界中会遇到的唯一文件组织方法。在另一种常见风格中,如图 5-1 所示,所有图像都被放入一个目录中(这意味着您命名目录的方式并不重要)。

另一种存储图像文件的目录结构

图 5-1。另一种存储图像文件的目录结构

在这个组织中,您会看到与包含所有图像的目录flowers在同一级别的位置,有一个名为all_labels.csv的 CSV 文件。该文件包含两列:一个包含所有文件名,另一个包含这些文件的标签:

file_name,label
7176723954_e41618edc1_n.jpg,sunflowers
2788276815_8f730bd942.jpg,roses
6103898045_e066cdeedf_n.jpg,dandelion
1441939151_b271408c8d_n.jpg,daisy
2491600761_7e9d6776e8_m.jpg,roses

要使用以这种格式存储的图像文件,您需要使用all_labels.csv来训练模型以识别每个图像的标签。这就是flow_from_dataframe方法的用武之地。

下载图像并设置目录

让我们从一个示例开始,其中图像组织在一个单独的目录中。下载文件 flower_photos.zip,解压缩后,您将看到图 5-1 中显示的目录结构:

或者,如果您在 Jupyter Notebook 环境中工作,请运行 Linux 命令wget来下载flower_photos.zip。以下是 Jupyter Notebook 单元格的命令:

!wget https://data.mendeley.com/public-files/datasets/jxmfrvhpyz/
files/283004ff-e529-4c3c-a1ee-4fb90024dc94/file_downloaded \
--output-document flower_photos.zip

前面的命令下载文件并将其放在当前目录中。使用此 Linux 命令解压缩文件:

!unzip -q flower_photos.zip

这将创建一个与 ZIP 文件同名的目录:

drwxr-xr-x 3 root root      4096 Nov  9 03:24 flower_photos
-rw-r--r-- 1 root root 228396554 Nov  9 20:14 flower_photos.zip

如您所见,有一个名为flower_photos的目录。使用以下命令列出其内容,您将看到与图 5-1 中显示的内容完全相同:

!ls -alt flower_photos

现在您已经有了目录结构和图像文件,可以开始构建数据流水线,将这些图像馈送到用于训练的图像分类模型中。为了简化操作,您将使用 ResNet 特征向量,这是 TensorFlow Hub 中的一个预构建模型,因此您无需设计模型。您将使用ImageDataGenerator将这些图像流式传输到训练过程中。

创建数据摄入管道

通常,首先要做的是导入必要的库:

import tensorflow as tf
import tensorflow_hub as hub
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

请注意,在此示例中您需要 pandas 库。此库用于将标签文件解析为数据框。以下是如何将标签文件读取到 pandas DataFrame 中:

traindf=pd.read_csv('flower_photos/all_labels.csv',dtype=str)

如果您查看数据框traindf,您将看到以下内容。

 文件名标签
07176723954_e41618edc1_n.jpg向日葵
12788276815_8f730bd942.jpg玫瑰
26103898045_e066cdeedf_n.jpg蒲公英
31441939151_b271408c8d_n.jpg雏菊
42491600761_7e9d6776e8_m.jpg玫瑰
.........
36159558628596_722c29ec60_m.jpg向日葵
36164580206494_9386c81ed8_n.jpg郁金香

接下来,您需要创建一些变量来保存稍后使用的参数:

data_root = 'flower_photos/flowers'
IMAGE_SIZE = (224, 224)
TRAINING_DATA_DIR = str(data_root)
BATCH_SIZE = 32

另外,请记住,当我们使用 ResNet 特征向量时,我们必须将图像像素强度重新缩放到[0, 1]的范围内,这意味着对于每个图像像素,强度必须除以 255。此外,我们需要保留一部分图像用于交叉验证,比如 20%。因此,让我们在一个字典中定义这些标准,我们可以将其用作ImageDataGenerator定义的输入:

datagen_kwargs = dict(rescale=1./255, validation_split=.20)

另一个字典将保存一些其他参数。ResNet 特征向量期望图像具有 224×224 的像素尺寸,我们还需要指定批处理大小和重采样算法:

dataflow_kwargs = dict(target_size=IMAGE_SIZE, 
batch_size=BATCH_SIZE,
interpolation="bilinear")

这个字典将作为数据流定义的输入。

用于训练图像的生成器定义如下:

train_datagen = tf.keras.preprocessing.image.
                ImageDataGenerator(**datagen_kwargs)

请注意,我们将datagen_kwargs传递给ImageDataGenerator实例。接下来,我们使用flow_from_dataframe方法创建数据流水线:

train_generator=train_datagen.flow_from_dataframe(
dataframe=traindf,
directory=data_root,
x_col="file_name",
y_col="label",
subset="training",
seed=10,
shuffle=True,
class_mode="categorical",
**dataflow_kwargs)

我们定义的train_datagen是用来调用flow_from_dataframe方法的。让我们看一下输入参数。第一个参数是dataframe,被指定为traindf。然后directory指定了在目录路径中可以找到图像的位置。x_coly_coltraindf中的标题:x_col对应于在all_labels.csv中定义的列“file_name”,而y_col是列“label”。现在我们的生成器知道如何将图像与它们的标签匹配。

接下来,它指定了一个要进行training的子集,因为这是训练图像生成器。提供了种子以便批次的可重现性。图像被洗牌,图像类别被指定为分类。最后,dataflow_kwargs被传递到这个flow_from_dataframe方法中,以便将原始图像从其原始分辨率重新采样为 224×224 像素。

这个过程对验证图像生成器也是重复的:

valid_datagen = tf.keras.preprocessing.image.ImageDataGenerator(
**datagen_kwargs)
valid_generator=valid_datagen.flow_from_dataframe(
dataframe=traindf,
directory=data_root,
x_col="file_name",
y_col="label",
subset="validation",
seed=10,
shuffle=True,
class_mode="categorical",
**dataflow_kwargs)

检查数据集

现在,检查 TensorFlow 数据集内容的唯一方法是通过迭代:

image_batch, label_batch = next(iter(train_generator))
fig, axes = plt.subplots(8, 4, figsize=(20, 40))
axes = axes.flatten()
for img, lbl, ax in zip(image_batch, label_batch, axes):
    ax.imshow(img)
    label_ = np.argmax(lbl)
    label = idx_labels[label_]
    ax.set_title(label)
    ax.axis('off')
plt.show()

前面的代码片段从train_generator中获取了第一批图像,其输出是一个由image_batchlabel_batch组成的元组。

您将看到 32 张图像(这是批处理大小)。有些看起来像图 5-2。

数据集中一些花的图像

图 5-2. 数据集中一些花的图像

现在数据摄入管道已经设置好,您可以在训练过程中使用它了。

构建和训练 tf.keras 模型

以下分类模型是如何在 TensorFlow Hub 中使用预构建模型的示例:

mdl = tf.keras.Sequential([
      tf.keras.layers.InputLayer(input_shape=IMAGE_SIZE + (3,)),
                 hub.KerasLayer(
"https://tfhub.dev/tensorflow/resnet_50/feature_vector/1", 
trainable=False),

tf.keras.layers.Dense(5, activation='softmax', 
name = 'custom_class')
])
mdl.build([None, 224, 224, 3])

一旦模型架构准备好,就编译它:

mdl.compile(
  optimizer=tf.keras.optimizers.SGD(lr=0.005, momentum=0.9),
  loss=tf.keras.losses.CategoricalCrossentropy(
  from_logits=True, 
  label_smoothing=0.1),
  metrics=['accuracy'])

然后启动训练过程:

steps_per_epoch = train_generator.samples // 
train_generator.batch_size
validation_steps = valid_generator.samples // 
valid_generator.batch_size

mdl.fit(
    train_generator,
    epochs=13, steps_per_epoch=steps_per_epoch,
    validation_data=valid_generator,
    validation_steps=validation_steps)

请注意,train_generatorvalid_generator被传递到我们的fit函数中。这些将在训练过程中生成图像样本,直到所有时代完成。您应该期望看到类似于这样的输出:

Epoch 10/13
90/90 [==============================] - 17s 194ms/step
loss: 1.0338 - accuracy: 0.9602 - val_loss: 1.0779
val_accuracy: 0.9020
Epoch 11/13
90/90 [==============================] - 17s 194ms/step
loss: 1.0311 - accuracy: 0.9623 - val_loss: 1.0750
val_accuracy: 0.9077
Epoch 12/13
90/90 [==============================] - 17s 193ms/step
loss: 1.0289 - accuracy: 0.9672 - val_loss: 1.0741
val_accuracy: 0.9091
Epoch 13/13
90/90 [==============================] - 17s 192ms/step
loss: 1.0266 - accuracy: 0.9693 - val_loss: 1.0728
val_accuracy: 0.9034

这表明您已成功将训练图像生成器和验证图像生成器传递到训练过程中,并且两个生成器都可以在训练时摄入数据。验证数据准确性val_accuracy的结果表明,我们选择的 ResNet 特征向量对于我们的用于分类花卉图像的用例效果很好。

使用 from_tensor_slices 方法流式传输 NumPy 数组

您还可以创建一个流式传输 NumPy 数组的数据管道。您可以直接将 NumPy 数组传递到模型训练过程中,但为了有效利用 RAM 和其他系统资源,最好建立一个数据管道。此外,一旦您对模型满意并准备好将其扩展以处理更大量的数据以供生产使用,您将需要一个数据管道。因此,建立一个数据管道是一个好主意,即使是像 NumPy 数组这样简单的数据结构也是如此。

Python 的 NumPy 数组是一种多功能的数据结构。它可以用来表示数值向量和表格数据,也可以用来表示原始图像。在本节中,您将学习如何使用from_tensor_slices方法将 NumPy 数据流式传输为数据集。

您将在本节中使用的示例 NumPy 数据是Fashion-MNIST 数据集,其中包含 10 种服装类型的灰度图像。这些图像使用 NumPy 结构表示,而不是典型的图像格式,如 JPEG 或 PNG。总共有 70,000 张图像。该数据集在 TensorFlow 的分发中可用,并且可以使用tf.kerasAPI 轻松加载。

加载示例数据和库

首先,让我们加载必要的库和 Fashion-MNIST 数据:

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

fashion_mnist = tf.keras.datasets.fashion_mnist
(train_images, train_labels), 
(test_images, test_labels) = fashion_mnist.load_data()

这些数据是使用tf.kerasAPI 中的load_data函数加载的。数据被分成两个元组。每个元组包含两个 NumPy 数组,图像和标签,如下面的命令所确认的:

print(type(train_images), type(train_labels))

<class 'numpy.ndarray'> <class 'numpy.ndarray'>

这确认了数据类型。了解数组维度很重要,您可以使用shape命令显示:

print(train_images.shape, train_labels.shape)

(60000, 28, 28) (60000,)

正如您所见,train_images由 60,000 条记录组成,每条记录都是一个 28×28 的 NumPy 数组,而train_labels是一个 60,000 条记录的标签索引。TensorFlow 提供了一个有用的教程,介绍了这些索引如何映射到类名,但这里是一个快速查看。

标签类别
0T 恤/上衣
1裤子
2套衫
3连衣裙
4外套
5凉鞋
6衬衫
7运动鞋
8
9短靴

检查 NumPy 数组

接下来,检查其中一条记录,看看图像。要将 NumPy 数组显示为颜色刻度,您需要使用之前导入的matplotlib库。对象plt代表这个库:

plt.figure()
plt.imshow(train_images[5])
plt.colorbar()
plt.grid(False)
plt.show()

图 5-3 显示了train_images[5]的 NumPy 数组。

来自 Fashion-MNIST 数据集的示例记录

图 5-3。来自 Fashion-MNIST 数据集的示例记录

与 JPEG 格式中包含三个独立通道(RGB)的彩色图像不同,Fashion-MNIST 数据集中的每个图像都表示为一个扁平的、二维的 28×28 像素结构。请注意,像素值介于 0 和 255 之间;我们需要将它们归一化为[0, 1]。

为 NumPy 数据构建输入管道

现在您已经准备好构建一个流水线。首先,您需要将图像中的每个像素归一化到范围[0, 1]内:

train_images = train_images/255

现在数据值是正确的,并且准备传递给from_tensor_slices方法:

train_dataset = tf.data.Dataset.from_tensor_slices((train_images, 
train_labels))

接下来,将此数据集拆分为训练集和验证集。在以下代码片段中,我指定验证集为 10,000 张图像,剩下的 50,000 张图像进入训练集:

SHUFFLE_BUFFER_SIZE = 10000
TRAIN_BATCH_SIZE = 50
VALIDATION_BATCH_SIZE = 10000

validation_ds = train_dataset.shuffle(SHUFFLE_BUFFER_SIZE).
take(VALIDATION_SAMPLE_SIZE).
batch(VALIDATION_BATCH_SIZE)

train_ds = train_dataset.skip(VALIDATION_BATCH_SIZE).
batch(TRAIN_BATCH_SIZE).repeat()

当交叉验证是训练过程的一部分时,您还需要定义一些参数,以便模型知道何时停止并在训练迭代期间评估交叉验证数据:

steps_per_epoch = 50000 // TRAIN_BATCH_SIZE
validation_steps = 10000 // VALIDATION_BATCH_SIZE

以下是一个小型分类模型:

model = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    tf.keras.layers.Dense(30, activation='relu'),
    tf.keras.layers.Dense(10)
])

model.compile(optimizer=tf.keras.optimizers.RMSprop(),
  loss=tf.keras.losses.SparseCategoricalCrossentropy(
  from_logits=True),
  metrics=['sparse_categorical_accuracy'])

现在您可以开始训练:

model.fit(
    train_ds,
    epochs=13, steps_per_epoch=steps_per_epoch,
    validation_data=validation_ds,
    validation_steps=validation_steps)

您的输出应该类似于这样:

…
Epoch 10/13
1562/1562 [==============================] - 4s 3ms/step
loss: 0.2982 - sparse_categorical_accuracy: 0.8931
val_loss: 0.3476 - val_sparse_categorical_accuracy: 0.8778
Epoch 11/13
1562/1562 [==============================] - 4s 3ms/step
loss: 0.2923 - sparse_categorical_accuracy: 0.8954
val_loss: 0.3431 - val_sparse_categorical_accuracy: 0.8831
Epoch 12/13
1562/1562 [==============================] - 4s 3ms/step
loss: 0.2867 - sparse_categorical_accuracy: 0.8990
val_loss: 0.3385 - val_sparse_categorical_accuracy: 0.8854
Epoch 13/13
1562/1562 [==============================] - 4s 3ms/step
loss: 0.2826 - sparse_categorical_accuracy: 0.8997
val_loss: 0.3553 - val_sparse_categorical_accuracy: 0.8811

请注意,您可以直接将 train_ds 和 validation_ds 传递给 fit 函数。这正是您在第四章中学到的方法,当时您构建了一个图像生成器并训练了图像分类模型以对五种类型的花进行分类。

总结

在本章中,您学习了如何为文本、数值数组和图像构建数据流水线。正如您所见,数据和目录结构在应用不同的 API 将数据摄入模型之前是很重要的。我们从一个文本数据示例开始,使用了 TensorFlow 提供的text_dataset_from_directory函数来处理文本文件。您还学到了flow_from_dataframe方法是专门为按类别分组的图像文件设计的,这是与您在第四章中看到的完全不同的文件结构。最后,对于 NumPy 数组结构中的数值数组,您学会了使用from_tensor_slices方法构建用于流式传输的数据集。当构建数据摄入管道时,您必须了解文件结构以及数据类型,以便使用正确的方法。

现在您已经看到如何构建数据流水线,接下来将在下一章中学习更多关于构建模型的内容。