TensorFlow-实战(七)

9 阅读57分钟

TensorFlow 实战(七)

原文:zh.annas-archive.org/md5/63f1015b3af62117a4a51b25a6d19428

译者:飞龙

协议:CC BY-NC-SA 4.0

第十四章:TensorBoard:TensorFlow 的大兄弟

本章内容包括

  • 在 TensorBoard 上运行和可视化图像数据

  • 实时监测模型性能和行为

  • 使用 TensorBoard 进行性能分析模型建模

  • 使用 tf.summary 在自定义模型训练过程中记录自定义指标

  • 在 TensorBoard 上可视化和分析词向量

到目前为止,我们已经重点关注了各种模型。我们讨论了全连接模型(例如自动编码器)、卷积神经网络和循环神经网络(例如 LSTM、GRU)。在第十三章,我们谈到了 Transformer,这是一类强大的深度学习模型,为语言理解领域的最新最优性能打下了基础。此外,受到自然语言处理领域的成就启发,Transformer 在计算机视觉领域也引起了轰动。我们已经完成了建模步骤,但还需要通过其他几个步骤来最终收获成果。其中一个步骤是确保向模型提供的数据/特征是正确的,并且模型按预期工作。

在本章中,我们将探索机器学习的一个新方面:利用可视化工具包来可视化高维数据(例如图像、词向量等),以及跟踪和监控模型性能。让我们了解为什么这是一个关键需求。由于机器学习在许多不同领域的成功示范,机器学习已经深深扎根于许多行业和研究领域。因此,这意味着我们需要更快速地训练新模型,并在数据科学工作流程的各个步骤中减少摩擦(例如了解数据、模型训练、模型评估等)。TensorBoard 是迈出这一步的方向。它可以轻松跟踪和可视化数据、模型性能,甚至对模型进行性能分析,了解数据在哪里花费了最多的时间。

通常,您会将数据和模型度量值以及其他要可视化的内容写入日志目录。写入日志目录的内容通常被组织成子文件夹,文件夹的命名包含了日期、时间以及对实验的简要描述。这将有助于在 TensorBoard 中快速识别一个实验。TensorBoard 会不断搜索指定的日志目录以查找更改,并在仪表板上进行可视化。您将在接下来的章节中了解到这些步骤的具体细节。

14.1 使用 TensorBoard 可视化数据

假设您是一家时尚公司的数据科学家。他们要求您评估构建一个可以在给定照片中识别服装的模型的可行性。为此,您选择了 Fashion-MNIST 数据集,该数据集包含黑白服装图像,属于 10 个类别之一。一些类别包括 T 恤、裤子和连衣裙。您将首先加载数据并通过 TensorBoard 进行分析,TensorBoard 是一种可视化工具,用于可视化数据和模型。在这里,您将可视化一些图像,并确保在加载到内存后正确分配了类标签。

首先,我们将下载 Fashion-MNIST 数据集。Fashion-MNIST 是一个带有各种服装图片和相应标签/类别的标记数据集。Fashion-MNIST 主要受到了 MNIST 数据集的启发。为了恢复我们的记忆,MNIST 数据集由 28×28 大小的 0-9 数字图像和相应的数字标签构成。由于任务的容易性,许多人建议不再将 MNIST 作为性能基准数据集,因此 Fashion-MNIST 应运而生。与 MNIST 相比,Fashion-MNIST 被认为是一项更具挑战性的任务。

下载 Fashion-MNIST 数据集非常容易,因为它可通过 tensorflow_datasets 库获取:

import tensorflow_datasets as tfds

# Construct a tf.data.Dataset
fashion_ds = tfds.load('fashion_mnist')

现在,让我们打印来查看数据的格式:

print(fashion_ds)

这将返回

{'test': <PrefetchDataset shapes: {image: (28, 28, 1), label: ()}, types: 
➥ {image: tf.uint8, label: tf.int64}>, 'train': <PrefetchDataset shapes: 
➥ {image: (28, 28, 1), label: ()}, types: {image: tf.uint8, label: 
➥ tf.int64}>}

数据集包含两个部分,一个训练数据集和一个测试数据集。训练集有两个项:图像(每个图像尺寸为 28×28×1)和一个整数标签。测试集上也有同样的项。接下来,我们将创建三个 tf.data 数据集:训练集、验证集和测试集。我们将原始训练数据集分为两部分,一个训练集和一个验证集,然后将测试集保持不变(参见下一个代码清单)。

代码清单 14.1 生成训练、验证和测试数据集

def get_train_valid_test_datasets(fashion_ds, batch_size, 
➥ flatten_images=False):

    train_ds = fashion_ds["train"].shuffle(batch_size*20).map(
        lambda xy: (xy["image"], tf.reshape(xy["label"], [-1]))         ❶
    )
    test_ds = fashion_ds["test"].map(
        lambda xy: (xy["image"], tf.reshape(xy["label"], [-1]))         ❷
    )

    if flatten_images:
        train_ds = train_ds.map(lambda x,y: (tf.reshape(x, [-1]), y))   ❸
        test_ds = test_ds.map(lambda x,y: (tf.reshape(x, [-1]), y))     ❸

    valid_ds = train_ds.take(10000).batch(batch_size)                   ❹

    train_ds = train_ds.skip(10000).batch(batch_size)                   ❺

    return train_ds, valid_ds, test_ds

❶ 获取训练数据集,对其进行洗牌,并输出一个(image, label)元组。

❷ 获取测试数据集,并输出一个(image, label)元组。

❸ 将图像扁平化为 1D 向量,用于全连接网络。

❹ 将验证数据集设置为前 10,000 个数据点。

❺ 将训练数据集设置为其余数据。

这是一个简单的数据流程。原始数据集中的每个记录都以字典形式提供,包含两个键:image 和 label。首先,我们使用 tf.data.Dataset.map()函数将基于字典的记录转换为(image, label)的元组。然后,如果数据集要用于全连接网络,则可选择性地将 2D 图像扁平化为 1D 向量。换句话说,28 × 28 的图像将变为 784 大小的向量。最后,我们将前 10,000 个数据点(经过洗牌)作为验证集,其余数据作为训练集。

在 TensorBoard 上可视化数据的方式是通过将信息记录到预定义的日志目录中,通过 tf.summary.SummaryWriter()。这个写入器将以 TensorBoard 理解的特殊格式写入我们感兴趣的数据。接下来,您启动一个 TensorBoard 的实例,将其指向日志目录。使用这种方法,让我们使用 TensorBoard 可视化一些训练数据。首先,我们定义一个从标签 ID 到字符串标签的映射:

id2label_map = {
    0: "T-shirt/top",
    1: "Trouser",
    2:"Pullover",
    3: "Dress",
    4: "Coat",
    5: "Sandal",
    6: "Shirt",
    7: "Sneaker",
    8: "Bag",
    9: "Ankle boot"
}

这些映射是从 mng.bz/DgdR 中获得的。然后我们将定义日志目录。我们将使用日期时间戳来生成不同运行的唯一标识符,如下所示:

log_datetimestamp_format = "%Y%m%d%H%M%S"

log_datetimestamp = datetime.strftime(datetime.now(), 
➥ log_datetimestamp_format)
image_logdir = "./logs/data_{}/train".format(log_datetimestamp)

如果你对 log_datetimestamp_format 变量中的奇怪格式感到困惑,那么它是 Python 的 datetime 库使用的标准格式,用于定义日期和时间的格式(mng.bz/lxM2),如果你在你的代码中使用它们。具体来说,我们将会得到运行时间(由 datetime.now() 给出)作为一个没有分隔符的数字字符串。我们将得到一天中给定时间的年份(%Y)、月份(%m)、日期(%d)、24 小时制的小时(%H)、分钟(%M)和秒(%S)。然后,我们将数字字符串附加到日志目录中,以根据运行时间创建一个唯一标识符。接下来,我们通过调用以下函数定义一个 tf.summary.SummaryWriter(),并将日志目录作为参数传递。有了这个,我们使用这个摘要写入器所做的任何写入都将被记录在定义的目录中:

image_writer = tf.summary.create_file_writer(image_logdir)

接下来,我们打开已定义的摘要写入器作为默认写入器,使用一个 with 子句。一旦摘要写入器打开,任何 tf.summary.<数据类型> 对象都将将该信息记录到日志目录中。在这里,我们使用了一个 tf.summary.image 对象。你可以使用几种不同的对象来记录(www.tensorflow.org/api_docs/python/tf/summary):

  • tf.summary.audio—用于记录音频文件并在 TensorBoard 上听取的对象类型

  • tf.summary.histogram—用于记录直方图并在 TensorBoard 上查看的对象类型

  • tf.summary.image—用于记录图像并在 TensorBoard 上查看的对象类型

  • tf.summary.scalar—用于记录标量值(例如,在几个周期内计算的模型损失)并在 TensorBoard 上显示的对象类型

  • tf.summary.text—用于记录原始文本数据并在 TensorBoard 上显示的对象类型

在这里,我们将使用 tf.summary.image() 来编写并在 TensorBoard 上显示图像。 tf.summary.image() 接受几个参数:

  • name—摘要的描述。这将在在 TensorBoard 上显示图像时用作标签。

  • data—大小为[b, h, w, c]的图像批次,其中 b 是批次大小,h 是图像高度,w 是图像宽度,c 是通道数(例如,RGB)。

  • step—一个整数,可用于显示属于不同批次/迭代的图像(默认为 None)。

  • max_outputs—在给定时间内显示的最大输出数量。如果数据中的图片数量超过 max_outputs,则将显示前 max_outputs 个图片,其余图片将被静默丢弃(默认为三)。

  • description—摘要的详细描述(默认为 None)

我们将以两种方式编写两组图像数据:

  • 首先,我们将从训练数据集中逐个取出 10 张图像,并带有其类标签标记地写入它们。然后,具有相同类标签(即,类别)的图像将被嵌套在 TensorBoard 上的同一部分中。

  • 最后,我们将一次写入一批 20 张图像。

with image_writer.as_default():
    for data in fashion_ds["train"].batch(1).take(10):
        tf.summary.image(
            id2label_map[int(data["label"].numpy())], 
            data["image"], 
            max_outputs=10, 
            step=0
        )

# Write a batch of 20 images at once
with image_writer.as_default():
    for data in fashion_ds["train"].batch(20).take(1):
        pass
    tf.summary.image("A training data batch", data["image"], max_outputs=20, step=0)

然后,我们就可以开始可视化 TensorBoard 了。在 Jupyter 笔记本代码单元格中只需运行以下命令即可简单地初始化和加载 TensorBoard。您现在已经知道,Jupyter 笔记本由单元格组成,您可以在其中输入文本/代码。单元格可以是代码单元格、Markdown 单元格等:

%load_ext tensorboard
%tensorboard --logdir ./logs --port 6006

图 14.1 显示了代码在笔记本单元格中的外观。

14-01

图 14.1 在笔记本单元格中的 Jupyter 魔术命令

您可能会注意到,这不是典型的 Python 语法。以 % 符号开头的命令被称为 Jupyter 魔术命令。请记住,Jupyter 是生成笔记本的 Python 库的名称。您可以在 mng.bz/BMd1 查看此类命令的列表。第一个命令加载 TensorBoard Jupyter 笔记本扩展程序。第二个命令使用提供的日志目录(--logdir)参数和端口(--port)参数实例化 TensorBoard。如果不指定端口,则 TensorBoard 默认在 6006(或大于 6006 的第一个未使用的端口)上运行。图 14.2 显示了可视化图像的 TensorBoard 的外观。

14-02

图 14.2 TensorBoard 可视化记录的图像,以内联方式在 Jupyter 笔记本中显示。您可以对图像执行各种操作,例如调整亮度或对比度。此外,数据被记录到的子目录显示在左侧面板上,让您可以轻松地显示/隐藏不同的子目录以便进行更容易的比较。

或者,您还可以将 TensorBoard 可视化为 Jupyter 笔记本之外的浏览器选项卡。在浏览器中运行这两个命令后,打开 http://localhost:6006,将显示 TensorBoard,如图 14.2 所示。在下一节中,我们将看到在模型训练过程中如何使用 TensorBoard 来跟踪和监视模型性能。

练习 1

您有一个名为 step_image_batches 的变量中包含五批图像的列表。列表中的每个项目对应于训练的前五个步骤。您希望在 TensorBoard 中显示这些批次,每个批次都具有正确的步骤值。您可以将每个批次命名为 batch_0、batch_1 等等。您该如何做?

14.2 使用 TensorBoard 跟踪和监视模型

通过对 Fashion-MNIST 数据集中的数据有很好的理解,您将使用神经网络在此数据上训练模型,以衡量您能够对不同类型的服装进行多么准确的分类。您计划使用密集网络和卷积神经网络。您将在相同的条件下训练这两个模型(例如,数据集),并在 TensorBoard 上可视化模型的准确性和损失。

TensorBoard 的主要作用是能够在模型训练时可视化模型的性能。深度神经网络以其长时间的训练时间而闻名。毫无疑问,尽早识别模型中的问题是非常值得的。TensorBoard 在其中发挥着至关重要的作用。您可以将模型性能(通过评估指标)导入到 TensorBoard 中实时显示。因此,您可以在花费过多时间之前快速发现模型中的任何异常行为并采取必要的行动。

在本节中,我们将比较全连接网络和卷积神经网络在 Fashion-MNIST 数据集上的性能。让我们将一个小型全连接模型定义为我们想要使用该数据集测试的第一个模型。它将有三层:

  • 一个具有 512 个神经元和 ReLU 激活的层,该层接收来自 Fashion-MNIST 数据集的平坦图像

  • 一个具有 256 个神经元和 ReLU 激活的层,该层接收前一层的输出

  • 一个具有 softmax 激活的具有 10 个输出的层(表示类别)

该模型使用稀疏分类交叉熵损失和 Adam 优化器进行编译。由于我们对模型准确性感兴趣,因此我们将其添加到要跟踪的指标列表中:

from tensorflow.keras import layers, models

dense_model = models.Sequential([
    layers.Dense(512, activation='relu', input_shape=(784,)),
    layers.Dense(256, activation='relu'),
    layers.Dense(10, activation='softmax')
])

dense_model.compile(loss="sparse_categorical_crossentropy", optimizer='adam', metrics=['accuracy'])

模型完全定义后,我们对训练数据进行训练,并在验证数据集上进行评估。首先,让我们定义全连接模型的日志目录:

log_datetimestamp_format = "%Y%m%d%H%M%S"
log_datetimestamp = datetime.strftime(
    datetime.now(), log_datetimestamp_format
)

dense_log_dir = os.path.join("logs","dense_{}".format(log_datetimestamp))

与以往一样,您可以看到我们不仅将写入子目录而不是普通的平面目录,而且还使用了基于运行时间的唯一标识符。这些子目录中的每一个代表了 TensorBoard 术语中所谓的一个 run

在 TensorBoard 中组织运行

通常,用户在通过 TensorBoard 可视化时利用某种运行组织。除了为多个算法创建多个运行之外,常见的做法是向运行添加日期时间戳,以区分同一算法在不同场合运行的不同运行。

例如,您可能会测试相同的算法与不同的超参数(例如,层数、学习率、优化器等),并且可能希望将它们都放在一个地方查看。假设您想测试具有不同学习率(0.01、0.001 和 0.0005)的全连接层。您将在主日志目录中具有以下目录结构:

./logs/dense/run_2021-05-27-03-14-21_lr=0.01
./logs/dense/run_2021-05-27-09-02-52_lr=0.001
./logs/dense/run_2021-05-27-10-12-09_lr=0.001
./logs/dense/run_2021-05-27-14-43-12_lr=0.0005

或者您甚至可以使用更嵌套的目录结构:

./logs/dense/lr=0.01/2021-05-27-03-14-21
./logs/dense/lr=0.001/2021-05-27-09-02-52
./logs/dense/lr=0.001/2021-05-27-10-12-09
./logs/dense/lr=0.0005/2021-05-27-14-43-12

我想强调时间戳您的运行很重要,如侧边栏中所述。这样,您将为每次运行都有一个唯一的文件夹,并且可以随时返回到以前的运行以进行比较。接下来,让我们使用 get_train_valid_test()函数生成训练/验证/测试数据集。请确保设置 flatten_images=True:

batch_size = 64
tr_ds, v_ds, ts_ds = get_train_valid_test_datasets(
    fashion_ds, batch_size=batch_size, flatten_images=True
)

将模型指标传递给 TensorBoard 非常简单。在模型训练/评估期间,可以传递一个特殊的 TensorBoard 回调函数:

tb_callback = tf.keras.callbacks.TensorBoard(
    log_dir=dense_log_dir, profile_batch=0
)

让我们讨论一些您可以传递给 TensorBoard 回调函数的关键参数。默认的 TensorBoard 回调函数如下所示:

tf.keras.callbacks.TensorBoard(
    log_dir='logs', histogram_freq=0, write_graph=True,
    write_images=False, write_steps_per_second=False, update_freq='epoch',
    profile_batch=2, embeddings_freq=0, embeddings_metadata=None, 
)

现在我们将查看所提供的参数:

  • log_dir—用于日志记录的目录。一旦使用该目录(或该目录的父目录)启动了 TensorBoard,可以在 TensorBoard 上可视化信息(默认为“logs”)。

  • histogram_freq—在各个层中创建激活分布的直方图(稍后详细讨论)。此选项指定要多频繁(以 epochs 为单位)记录这些直方图(默认值为 0,即禁用)。

  • write_graph—确定是否将模型以图形的形式写入 TensorBoard 以进行可视化(默认为 True)。

  • write_image—确定是否将模型权重写为图像(即热图)以在 TensorBoard 上可视化权重(默认为 False)。

  • write_steps_per_second—确定是否将每秒执行的步骤数写入 TensorBoard(默认为 False)。

  • update_freq('batch'、'epoch'或整数)—确定是否每个批次(如果值设置为 batch)或每个 epoch(如果值设置为 epoch)向 TensorBoard 写入更新。传递一个整数,TensorFlow 将解释为“每 x 个批次写入 TensorBoard”。默认情况下,将每个 epoch 写入更新。写入磁盘很昂贵,因此过于频繁地写入将会降低训练速度。

  • profile_batch(整数或两个数字的列表)—确定要用于对模型进行分析的批次。分析计算模型的计算和内存使用情况(稍后详细讨论)。如果传递一个整数,它将分析一个批次。如果传递一个范围(即一个包含两个数字的列表),它将分析该范围内的批次。如果设置为零,则不进行分析(默认为 2)。

  • embedding_freq—如果模型具有嵌入层,则此参数指定可视化嵌入层的间隔(以 epochs 为单位)。如果设置为零,则禁用此功能(默认为 0)。

  • embedding_metadata—一个将嵌入层名称映射到文件名的字典。该文件应包含与嵌入矩阵中每行对应的标记(按顺序排列;默认为 None)。

最后,我们将像以前一样训练模型。唯一的区别是将 tb_callback 作为回调参数传递给模型:

dense_model.fit(tr_ds, validation_data=v_ds, epochs=10, callbacks=[tb_callback])

模型应该达到约 85%的验证准确率。现在打开 TensorBoard,访问 http:/ /localhost:6006。它将显示类似于图 14.3 的仪表板。随着日志目录中出现新数据,仪表板将自动刷新。

14-03

图 14.3 显示了 TensorBoard 上如何显示跟踪的指标。您可以看到训练和验证准确率以及损失值被绘制为折线图。此外,还有各种控件,例如最大化图形,切换到对数刻度 y 轴等等。

TensorBoard 仪表板具有许多控件,可以帮助用户通过记录的指标深入了解他们的模型。您可以打开或关闭不同的运行,具体取决于您要分析的内容。例如,如果您只想查看验证指标,则可以关闭 dense/train 运行,并反之亦然。Data/train 运行不会影响此面板,因为它包含我们从训练数据中记录的图像。要查看它们,可以单击 IMAGES 面板。

接下来,您可以更改平滑参数以控制曲线的平滑程度。通过使用曲线的平滑版本,有助于消除指标中的局部小变化,聚焦于整体趋势。图 14.4 描述了平滑参数对折线图的影响。

14-04

图 14.4 展示了平滑参数如何改变折线图。在这里,我们显示了使用不同平滑参数的相同折线图。您可以看到,随着平滑参数的增加,线条变得更平滑。原始线条以淡色显示。

此外,您还可以进行其他控制,例如切换到对数刻度 y 轴而不是线性。如果指标随时间观察到大幅变化,则这非常有用。在对数刻度下,这些大变化将变得更小。如果您需要更详细地检查图表,还可以在标准大小和全尺寸图之间切换。图 14.3 突出显示了这些控件。

之后,我们将定义一个简单的卷积神经网络,并执行相同的操作。也就是说,我们将首先定义网络,然后在使用回调函数到 TensorBoard 的同时训练模型。

让我们定义下一个我们将与全连接网络进行比较的模型:卷积神经网络(CNN)。同样,我们正在定义一个非常简单的 CNN,它包括

  • 一个 2D 卷积层,具有 32 个过滤器,5×5 内核,2×2 步幅和 ReLU 激活,该层接受来自 Fashion-MNIST 数据集的 2D 28×28 大小的图像

  • 一个具有 16 个过滤器的 2D 卷积层,具有 3×3 内核,1×1 步幅和 ReLU 激活,该层接受先前层的输出

  • 一个扁平化层,将卷积输出压成适用于密集层的 1D 向量

  • 具有 10 个输出(表示类别)并具有 softmax 激活的层

conv_model = models.Sequential([
    layers.Conv2D(
       filters=32, 
       kernel_size=(5,5), 
       strides=(2,2), 
       padding='same', 
       activation='relu', 
       input_shape=(28,28,1)
    ),
    layers.Conv2D(
        filters=16, 
        kernel_size=(3,3), 
        strides=(1,1), 
        padding='same', 
        activation='relu'
    ),
    layers.Flatten(),
    layers.Dense(10, activation='softmax')
])

conv_model.compile(
    loss="sparse_categorical_crossentropy", optimizer='adam', 
➥ metrics=['accuracy']
)
conv_model.summary()

接下来,我们将 CNN 相关的指标记录到一个名为./logs/conv_{datetimestamp}的单独目录中。这样,我们可以分别绘制完全连接的网络和 CNN 的评估指标。我们将生成训练和验证数据集以及一个 TensorBoard 回调,就像之前做的那样。然后,在调用 fit()方法训练模型时将它们传递给模型:

log_datetimestamp_format = "%Y%m%d%H%M%S"
log_datetimestamp = datetime.strftime(
    datetime.now(), log_datetimestamp_format
)

conv_log_dir = os.path.join("logs","conv_{}".format(log_datetimestamp))

batch_size = 64
tr_ds, v_ds, ts_ds = get_train_valid_test_datasets(
    fashion_ds, batch_size=batch_size, flatten_images=False
)

tb_callback = tf.keras.callbacks.TensorBoard(
    log_dir=conv_log_dir, histogram_freq=2, profile_batch=0
)

conv_model.fit(
    tr_ds, validation_data=v_ds, epochs=10, callbacks=[tb_callback]
)

注意我们在训练 CNN 时所做的更改。首先,我们不像训练完全连接的网络时那样将图像展平(即,在 get_train_valid_test_datasets()函数中设置 flatten_ images=False)。接下来,我们向 TensorBoard 回调引入了一个新参数。我们将使用 histogram_freq 参数来记录模型在训练过程中的层激活直方图。我们将很快更深入地讨论层激活直方图。这将在同一张图中显示两个模型(即密集模型和卷积模型)的准确度和损失指标,以便它们可以轻松比较(图 14.5)。

14-05

图 14.5 查看密集模型和卷积模型的指标。你可以根据需要比较不同的运行状态。

让我们再次回到激活直方图。激活直方图让我们可以可视化不同层的神经元激活分布随着训练的进行而变化的情况。这是一个重要的检查,它可以让你看到模型在优化过程中是否正在收敛,从而提供关于模型训练或数据质量的问题的见解。

让我们更深入地看一下这些直方图显示了什么。图 14.6 说明了我们训练的 CNN 生成的直方图。我们每两个时代绘制一次直方图。以直方图表示的权重堆叠在一起,这样我们就可以很容易地了解它们随时间的变化情况。直方图中的每个切片显示了给定层和给定时代中的权重分布。换句话说,它将提供诸如“有 x 个输出,其值为 y,大约为 z”的信息。

14-06

图 14.6 由 TensorBoard 显示的激活直方图。这些图表显示了给定层的激活分布随时间的变化情况(较浅的图表表示更近期的时代)。

通常,如果你有一个值的向量,创建直方图就相当简单。例如,假设值为[0.1, 0.3, 0.35, 0.5, 0.6, 0.61, 0.63],并且假设你有四个箱子:0.0, 0.2),[0.2, 0.4),[0.4, 0.6),和[0.6, 0.8)。你将得到图 14.7 所示的直方图。如果你看一下连接各条的中点的线,它类似于你在仪表板中看到的内容。

![14-07

图 14.7 生成的序列[0.1, 0.3, 0.35, 0.5, 0.6, 0.61, 0.63]的直方图

然而,当数据很大且稀疏(例如在权重矩阵中)时,计算直方图涉及更复杂的数据操作。例如,在 TensorBoard 中计算直方图涉及使用指数 bin 大小(与示例中的均匀 bin 大小相反),这在接近零时提供了更细粒度的 bin 和远离零时提供了更宽的 bin。然后,它将这些不均匀大小的 bin 重新采样为统一大小的 bin,以便更容易、更有意义地进行可视化。这些计算的具体细节超出了本书的范围。如果您想了解更多细节,请参考mng.bz/d26o

我们可以看到,在训练过程中,权重正在收敛于一个近似的正态分布。但是偏差收敛于一个多峰分布,并在许多不同的地方出现峰值。

本节阐述了如何使用 TensorBoard 进行一些主要数据可视化和模型性能跟踪。这些是您在数据科学项目中必须设置的核心检查点的重要组成部分。数据可视化需要在项目早期完成,以帮助您理解数据及其结构。模型性能跟踪非常重要,因为深度学习模型需要更长的训练时间,而您需要在有限的预算(时间和成本)内完成培训。在下一节中,我们将讨论如何记录自定义指标到 TensorBoard 并可视化它们。

练习 2

您有一个由 classif_model 表示的二元分类模型。您想在 TensorBoard 中跟踪此模型的精度和召回率。此外,您想在每个时期可视化激活直方图。您将如何使用 TensorBoard 回调编译模型,并使用 TensorBoard 回调拟合数据以实现此目的?TensorFlow 提供了 tf.keras.metrics.Precision()和 tf.keras.metrics.Recall()来分别计算精度和召回率。您可以假设您直接记录到./logs 目录。假设您已经提供了 tf.data.Dataset 对象的训练数据(tr_ds)和验证数据(v_ds)。

14.3 使用 tf.summary 在模型训练期间编写自定义度量

想象一下,您是一名博士研究批量归一化的影响。特别是,您需要分析给定层中的权重均值和标准偏差如何随时间变化,以及有无批量归一化。为此,您将使用一个全连接网络,并在 TensorBoard 上记录每个步骤的权重均值和标准偏差。由于这不是您可以使用 Keras 模型生成的典型指标,因此您将在自定义培训循环中记录每个步骤的模型训练期间的指标。

为了比较批量归一化的效果,我们需要定义两个不同的模型:一个没有批量归一化,一个有批量归一化。这两个模型将具有相同的规格,除了使用批量归一化。首先,让我们定义一个没有批量归一化的模型:

from tensorflow.keras import layers, models
import tensorflow.keras.backend as K

K.clear_session()
dense_model = models.Sequential([
    layers.Dense(512, activation='relu', input_shape=(784,)),    
    layers.Dense(256, activation='relu', name='log_layer'),    
    layers.Dense(10, activation='softmax')
])

dense_model.compile(loss="sparse_categorical_crossentropy", optimizer='adam', metrics=['accuracy'])

该模型非常简单,与我们之前定义的完全连接模型相同。它有三个层,分别有 512、256 和 10 个节点。前两层使用 ReLU 激活函数,而最后一层使用 softmax 激活函数。请注意,我们将第二个 Dense 层命名为 log_layer。我们将使用该层来计算我们感兴趣的指标。最后,该模型使用稀疏分类交叉熵损失、Adam 优化器和准确度作为指标进行编译。接下来,我们使用批量归一化定义相同的模型:

dense_model_bn = models.Sequential([
    layers.Dense(512, activation='relu', input_shape=(784,)),
    layers.BatchNormalization(),
    layers.Dense(256, activation='relu', name='log_layer_bn'),
    layers.BatchNormalization(),
    layers.Dense(10, activation='softmax')
])

dense_model_bn.compile(
    loss="sparse_categorical_crossentropy", optimizer='adam', 
➥ metrics=['accuracy']
)

引入批量归一化意味着在 Dense 层之间添加 tf.keras.layers.BatchNormalization()层。我们将第二个模型中感兴趣的层命名为 log_layer_bn,因为我们不能同时使用相同名称的两个层。

有了定义好的模型,我们的任务是在每一步计算权重的均值和标准差。为此,我们将观察两个网络的第二层的权重的均值和标准差(log_layer 和 log_layer_bn)。正如我们已经讨论过的,我们不能简单地传递一个 TensorBoard 回调并期望这些指标可用。由于我们感兴趣的指标不常用,我们必须费力确保这些指标在每一步都被记录。

我们将定义一个 train_model()函数,可以将定义的模型传递给它,并在数据上进行训练。在训练过程中,我们将计算每一步权重的均值和标准差,并将其记录到 TensorBoard 中(见下一个清单)。

清单 14.2 在自定义循环中训练模型时编写 tf.summary 对象

def train_model(model, dataset, log_dir, log_layer_name, epochs):    

    writer = tf.summary.create_file_writer(log_dir)                        ❶
    step = 0

    with writer.as_default():                                              ❷

        for e in range(epochs):
            print("Training epoch {}".format(e+1))
            for batch in tr_ds:

                model.train_on_batch(*batch)                               ❸

                weights = model.get_layer(log_layer_name).get_weights()[0] ❹

                tf.summary.scalar("mean_weights",np.mean(np.abs(weights)), ❺
➥ step=step)                                                              ❺
                tf.summary.scalar("std_weights", np.std(np.abs(weights)),  ❺
➥ step=step)                                                              ❺

                writer.flush()                                             ❻

                step += 1
            print('\tDone')

    print("Training completed\n")

❶ 定义写入器。

❷ 打开写入器。

❸ 用一个批次进行训练。

❹ 获取层的权重。它是一个数组列表[权重,偏差],顺序是这样的。因此,我们只取权重(索引 0)。

❺ 记录权重的均值和标准差(对于给定 epoch 是两个标量)。

❻ 从缓冲区刷新到磁盘。

注意我们如何打开一个 tf.summary.writer(),然后使用 tf.summary.scalar()调用在每一步记录指标。我们给指标起了有意义的名称,以确保在 TensorBoard 上可视化时知道哪个是哪个。有了函数定义,我们为我们编译的两个不同模型调用它:

batch_size = 64
tr_ds, _, _ = get_train_valid_test_datasets(
    fashion_ds, batch_size=batch_size, flatten_images=True
)
train_model(dense_model, tr_ds, exp_log_dir + '/standard', "log_layer", 5)

tr_ds, _, _ = get_train_valid_test_datasets(
    fashion_ds, batch_size=batch_size, flatten_images=True
)
train_model(dense_model_bn, tr_ds, exp_log_dir + '/bn', "log_layer_bn", 5)

请注意,我们指定不同的日志子目录,以确保出现的两个模型是不同的运行。运行后,您将看到两个新的附加部分,名为 mean_weights 和 std_weights(图 14.8)。似乎当使用批量归一化时,权重的均值和方差更加剧烈地变化。这可能是因为批量归一化在层之间引入了显式归一化,使得层的权重更自由地移动。

14-08

图 14.8 权重的均值和标准差在 TensorBoard 中绘制

接下来的部分详细介绍了如何使用 TensorBoard 来分析模型并深入分析模型执行时时间和内存消耗情况。

练习 3

你计划计算斐波那契数列(即 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 等),其中第 n 个数字 x_n 由 x_n = x_{n - 1} + x_{n - 2} 给出。编写一个代码,计算 100 步的斐波那契数列并在 TensorBoard 中将其绘制为折线图。你可以将名字“fibonacci”作为指标名称。

14.4 对模型进行性能瓶颈检测

你现在作为一位数据科学家加入了一家正在识别濒临灭绝的花卉物种的生物技术公司。之前的一位数据科学家开发了一个模型,而你将继续这项工作。首先,你想确定是否存在任何性能瓶颈。为了分析这些问题,你计划使用 TensorBoard 分析器。你将使用一个较小的花卉数据集来训练模型,以便分析器可以捕获各种计算配置文件。

我们从列表 14.3 中的模型开始。这是一个具有四个卷积层的 CNN 模型,中间有池化层,包括三个完全连接的层,最后一个是具有 17 个输出类别的 softmax 层。

列表 14.3 你可用的 CNN 模型

def get_cnn_model():

    conv_model = models.Sequential([                                ❶
        layers.Conv2D(                                              ❷
            filters=64, 
            kernel_size=(5,5), 
            strides=(1,1), 
            padding='same', 
            activation='relu', 
            input_shape=(64,64,3)
        ),
        layers.BatchNormalization(),                                ❸
        layers.MaxPooling2D(pool_size=(3,3), strides=(2,2)),        ❹
        layers.Conv2D(                                              ❺
            filters=128, 
            kernel_size=(3,3), 
            strides=(1,1), 
            padding='same', 
            activation='relu'
        ),
        layers.BatchNormalization(),                                ❺
        layers.Conv2D(                                              ❺
            filters=256, 
            kernel_size=(3,3), 
            strides=(1,1), 
            padding='same', 
            activation='relu'
        ),
        layers.BatchNormalization(),                                ❺
        layers.Conv2D(                                              ❺
            filters=512, 
            kernel_size=(3,3), 
            strides=(1,1), 
            padding='same', 
            activation='relu'
        ),
        layers.BatchNormalization(),                                ❺
        layers.AveragePooling2D(pool_size=(2,2), strides=(2,2)),    ❻
        layers.Flatten(),                                           ❼
        layers.Dense(512),                                          ❽
        layers.LeakyReLU(),                                         ❽
        layers.LayerNormalization(),                                ❽
        layers.Dense(256),                                          ❽
        layers.LeakyReLU(),                                         ❽
        layers.LayerNormalization(),                                ❽
        layers.Dense(17),                                           ❽
        layers.Activation('softmax', dtype='float32')               ❽
    ])
    return conv_model

❶ 使用顺序 API 定义一个 Keras 模型。

❷ 定义一个接受大小为 64 × 64 × 3 的输入的第一个卷积层。

❸ 一个批量归一化层

❹ 一个最大池化层

❺ 一系列交替的卷积和批量归一化层

❻ 一个平均池化层,标志着卷积/池化层的结束

❼ 将最后一个池化层的输出展平。

❽ 一组稠密层(带有渗漏线性整流激活),接着是一个具有 softmax 激活的层

我们将使用的数据集是在www.robots.ox.ac.uk/~vgg/data/flowers找到的花卉数据集,具体来说,是 17 类别数据集。它有一个包含花朵图像的单独文件夹,每个图像文件名上都有一个数字。这些图像按照文件名排序时,前 80 个图像属于类别 0,接下来的 80 个图像属于类别 1,依此类推。你已经提供了下载数据集的代码,位于笔记本 Ch14/14.1_Tensorboard.ipynb 中,我们这里不会讨论。接下来,我们将编写一个简单的 tf.data 流水线,通过读取这些图像来创建数据批次:

def get_flower_datasets(image_dir, batch_size, flatten_images=False):

    # Get the training dataset, shuffle it, and output a tuple of (image, 
➥ label)
    dataset = tf.data.Dataset.list_files(
        os.path.join(image_dir,'*.jpg'), shuffle=False
    )

    def get_image_and_label(file_path):

        tokens = tf.strings.split(file_path, os.path.sep)
        label = (
            tf.strings.to_number(
                tf.strings.split(
                    tf.strings.split(tokens[-1],'.')[0], '_'
                )[-1]
            ) -1
        )//80

        # load the raw data from the file as a string
        img = tf.io.read_file(file_path)
        img = tf.image.decode_jpeg(img, channels=3)

        return tf.image.resize(img, [64, 64]), label

    dataset = dataset.map(get_image_and_label).shuffle(400)

    # Make the validation dataset the first 10000 data
    valid_ds = dataset.take(250).batch(batch_size)
    # Make training dataset the rest
    train_ds = dataset.skip(250).batch(batch_size)
    )

    return train_ds, valid_ds

让我们分析一下我们在这里所做的事情。首先,我们从给定文件夹中读取具有.jpg 扩展名的文件。然后我们有一个名为 get_image_and_label()的嵌套函数,它接受一个图像的文件路径,并通过从磁盘中读取该图像产生图像和标签。标签可以通过计算得到

  • 提取图像 ID

  • 减去 1(即将 ID 减 1,以使其成为从零开始的索引)并除以 80

之后,我们对数据进行洗牌,并将前 250 个数据作为验证数据,其余的作为训练数据。接下来,我们使用定义的这些函数并训练 CNN 模型,同时创建模型的各种计算性能分析。为了使性能分析工作,你需要两个主要的先决条件:

  • 安装 Python 包tensorboard_plugin_profile

  • 安装 libcupti,CUDA 性能分析工具包接口。

安装 CUDA 性能分析工具包接口(libcupti)

TensorBoard 需要 libcupti CUDA 库才能进行模型性能分析。安装此库需要根据所使用的操作系统的不同步骤。这假设您的计算机配备了 NVIDIA GPU。不幸的是,你将无法在 Mac 上执行此操作,因为 Mac 上没有可用于数据收集的性能分析 API。(查看developer.nvidia.com/cupti-ctk10_1u1中的需求部分。)

在 Ubuntu 上安装 libcupti

要在 Linux 上安装 libcupti,只需运行sudo apt-get install libcupti-dev

在 Windows 上安装 libcupti

在 Windows 上安装 libcupti 需要更多工作:

  • 确保你已经安装了推荐的 CUDA 版本(例如 CUDA 11 [>= TensorFlow 2.4.0])。有关 CUDA 版本的更多信息,请访问www.tensorflow.org/install/source#gpu

  • 接下来,打开 NVIDIA 控制面板(通过右键单击桌面并选择菜单项)进行几项更改(mng.bz/rJVJ):

    • 确保你点击桌面 > 设置开发者模式,设置开发者模式。

    • 确保你为所有用户启用了 GRU 性能分析,而不仅仅是管理员(图 14.9)。

    • 更多可能遇到的错误,请参考来自官方 NVIDIA 网站的mng.bz/VMxy

  • 要安装 libcupti,请访问mng.bz/xn2d

    • extras\CUPTI\lib64中的libcupti_<version>.dllnvperf_host.dllnvperf_target .dll文件复制到bin文件夹中。确保 libcupti 文件的名称为libcupti_110.dll

    • extras\CUPTI\lib64中的所有文件复制到lib\x64中。

    • extras\CUPTI\include中的所有文件复制到include中。

14-09

图 14.9 为所有用户启用 GPU 性能分析

确保你已经在你所使用的环境中正确安装了 libcupti(例如 Ubuntu 或 Windows)。否则,你将看不到预期的结果。然后,要启用性能分析,你只需要将参数profile_batch传递给 TensorBoard 回调函数。该值是两个数字的列表:起始步骤和结束步骤。通常情况下,性能分析是跨越几个批次进行的,因此值需要一个范围。但是,也可以对单个批次进行性能分析:

batch_size = 32
tr_ds, v_ds = get_flower_datasets(
    os.path.join(
        'data', '17flowers','jpg'), batch_size=batch_size, 
➥ flatten_images=False
)

tb_callback = tf.keras.callbacks.TensorBoard(
    log_dir=profile_log_dir, profile_batch=[10, 20]
)

conv_model.fit(
    tr_ds, validation_data=v_ds, epochs=2, callbacks=[tb_callback]
)

训练完成后,您可以在 TensorBoard 上查看结果。TensorBoard 提供了大量有价值的信息和对模型性能的洞察。它将计算分解为更小的子任务,并根据这些子任务提供细粒度的计算时间分解。此外,TensorBoard 提供了关于改进空间的建议(图 14.10)。现在让我们更深入地了解该页面提供的信息。

14-10

图 14.10 TensorBoard 性能分析界面。它提供了有关在 GPU 上运行模型涉及的各种子任务的宝贵信息。此外,它还提供了改进模型性能的建议。

平均步骤时间是几个较小任务的总和:

  • 输入时间—用于读取与数据相关的操作(例如,tf.data.Dataset 操作)的时间。

  • 主机计算时间—在主机上执行的与模型相关的计算(例如,CPU)。

  • 设备到设备时间—要在 GPU 上运行东西,首先需要将数据传输到 GPU。这个子任务测量了这种传输所花费的时间。

  • 内核启动时间—为了使 GPU 执行传输的数据上的操作,CPU 需要为 GPU 启动内核。内核封装了对数据执行的原始计算(例如,矩阵乘法)。这测量了启动内核所需的时间。

  • 设备计算时间—发生在设备上的与模型相关的计算(例如,GPU)。

  • 设备集体通信时间—与在多设备(例如,多个 GPU)或多节点环境中通信所花费的时间相关。

  • 所有其他时间(例如,编译时间、输出时间、所有其他剩余时间)。

在这里,我们可以看到大部分时间都花在了设备计算上。从某种意义上说,这是好的,因为它意味着大多数计算发生在 GPU 上。下一个最大的时间消耗者是输入时间。这是有道理的,因为我们没有对我们的 tf.data 流水线进行任何优化,并且它是一个高度依赖磁盘的流水线,因为图像是从磁盘中读取的。

然后,在下面,您可以看到更多信息。接近 80%的 TensorFlow 操作被放置在此主机上,而仅有 20%在 GPU 上运行。此外,所有操作都是 32 位操作,没有 16 位操作;16 位(半精度浮点)操作比 32 位(单精度浮点)数据类型运行得更快,节省了大量内存。GPU 和 Tensor 处理单元(TPU)是经过优化的硬件,可以比 32 位操作更快地运行 16 位操作。因此,必须尽可能地将它们纳入其中。话虽如此,我们必须小心如何使用 16 位操作,因为不正确的使用可能会严重影响模型性能(例如,模型准确性)。将 16 位操作与 32 位操作一起用于训练模型称为混合精度训练

如果你看推荐部分,你会看到两个主要的建议:

  • 优化输入数据管道。

  • 在模型训练中利用更多的 16 位操作。

Brain Floating Point 数据类型(bfloat16)

Brain Floating Point 值,或称 bfloat16 值,是 Google 提出的一种数据类型。它与 float16(即,16 位)具有相同的位数,但能够表示 float32 值的动态范围,但在精度上会有一定损失。这种变化是通过增加更多的指数位(小数点左侧)和减少小数位(小数点右侧)来实现的。这种数据类型可以在优化的硬件上获得显著的优势,比如 TPU 和 GPU(假设它们有 Tensor 核心;developer.nvidia.com/tensor-cores))。

让我们看看如何利用这些建议来减少模型训练时间。

14.4.1 优化输入管道

为了优化数据管道,我们将对 get_flower_datasets() 函数进行两项更改:

  • 使用数据预取以避免模型等待数据可用。

  • 在调用 get_image_and_label() 函数时使用并行化的 map 函数。

就这些变化在代码中的体现来说,它们是小变化。在下面的列表中,这些变化用粗体表示。

列表 14.4 从花数据集生成训练/验证数据集的函数

def get_flower_datasets(image_dir, batch_size, flatten_images=False):

    dataset = tf.data.Dataset.list_files(
        os.path.join(image_dir,'*.jpg'), shuffle=False          ❶
    )

    def get_image_and_label(file_path):                         ❷

        tokens = tf.strings.split(file_path, os.path.sep)       ❸
        label = (tf.strings.to_number(
            tf.strings.split(
                tf.strings.split(tokens[-1],'.')[0], '_')[-1]   ❸
            ) - 1
        )//80

        img = tf.io.read_file(file_path)                        ❹
        img = tf.image.decode_jpeg(img, channels=3)             ❹

        return tf.image.resize(img, [64, 64]), label

    dataset = dataset.map(
        get_image_and_label,
        *num_parallel_calls=tf.data.AUTOTUNE        *             ❺
    ).shuffle(400)

    # Make the validation dataset the first 10000 data
    valid_ds = dataset.take(250).batch(batch_size)
    # Make training dataset the rest
    train_ds = dataset.skip(250).batch(batch_size).prefetch(
        tf.data.experimental.AUTOTUNE                           ❻
 )

    return train_ds, valid_ds

❶ 获取训练数据集,对其进行洗牌,并输出(图像,标签)元组。

❷ 定义一个函数,根据文件名获取图像和标签。

❸ 获取文件路径中的标记并从图像 ID 计算标签。

❹ 读取图像并转换为张量。

❺ 并行化 map 函数。

❻ 结合预取。

为了并行化 dataset.map() 函数,我们在其后添加了 num_parallel_calls=tf.data .AUTOTUNE 参数,这将导致 TensorFlow 在并行执行 map 函数,其中线程数将由主机在执行时承载的工作量确定。接下来,在批处理后我们调用 prefetch() 函数,以确保模型训练不会因为等待数据可用而受阻。

接下来,我们将设置一个特殊的环境变量,称为 TF_GPU_THREAD_MODE。要理解这个变量的影响,你首先需要弄清楚 GPU 如何高效执行指令。当你在一台带有 GPU 的机器上运行深度学习模型时,大多数数据并行操作(即可以并行执行的数据操作)都会在 GPU 上执行。但数据和指令是如何传输到 GPU 的呢?假设使用 GPU 执行两个矩阵之间的逐元素乘法。由于可以并行地对个别元素进行乘法,这是一个数据并行操作。为了在 GPU 上执行此操作(定义为一组指令并称为内核),主机(CPU)首先需要启动内核,以便 GPU 使用该函数对数据进行操作。特别地,CPU 中的一个线程(现代 Intel CPU 每个核心大约有两个线程)将需要触发此操作。想象一下如果 CPU 中的所有线程都非常忙碌会发生什么。换句话说,如果有很多 CPU 绑定的操作正在进行(例如,从磁盘读取大量数据),它可能会导致 CPU 竞争,从而延迟 GPU 内核的启动。这反过来又延迟了在 GPU 上执行的代码。有了 TF_GPU_THREAD_MODE 变量,你可以缓解 CPU 竞争引起的 GPU 延迟。更具体地说,这个变量控制着 CPU 线程如何分配到 GPU 上启动内核。它可以有三个不同的值:

  • 全局—对于为不同的进程分配线程没有特殊的偏好(默认)。

  • gpu_private—分配了一些专用线程来为 GPU 启动内核。这样,即使 CPU 正在执行大量负载,内核启动也不会延迟。如果有多个 GPU,则它们将拥有自己的私有线程。线程的数量默认为两个,并可以通过设置 TF_GPU_THREAD_COUNT 变量进行更改。

  • shared—与 gpu_private 相同,但在多 GPU 环境中,一组线程将在 GPU 之间共享。

我们将此变量设置为 gpu_private。我们将保持专用线程的数量为两个,因此不会创建 TF_GPU_THREAD_COUNT 变量。

设置环境变量

要设置 TF_GPU_THREAD_MODE 环境变量,你可以执行以下操作:

Linux 操作系统(例如 Ubuntu)

设置环境变量

  • 打开一个终端。

  • 运行 export TF_GPU_THREAD_MODE=gpu_private。

  • 通过调用 echo $TF_GPU_THREAD_MODE 来验证环境变量是否设置。

  • 打开一个新的 shell 并启动 Jupyter 笔记本服务器。

Windows 操作系统

环境变量

  • 从开始菜单中,选择编辑系统环境变量。

  • 单击名为环境变量的按钮。

  • 在打开的对话框中添加一个新的环境变量 TF_GPU_THREAD_MODE=gpu_private。

  • 打开一个新的命令提示符并启动 Jupyter 笔记本服务器。

conda 环境(Anaconda)

在 conda 环境中设置环境变量

  • 使用 conda activate manning.tf2 激活 conda 环境。

  • 运行 conda env config vars set TF_GPU_THREAD_MODE=gpu_private。

  • 停用并重新启用环境以使变量生效。

  • 启动 Jupyter 笔记本服务器。

在更改操作系统或 conda 环境中的环境变量后,重启笔记本服务器非常重要。有关更多详细信息,请参阅以下边栏。

重要:设置环境变量后重新启动笔记本服务器。

当您从 shell(例如,Windows 上的命令提示符或 Linux 上的终端)创建笔记本服务器时,笔记本服务器将作为 shell 的子进程创建。在启动笔记本服务器后对环境进行的更改(例如,添加环境变量)将不会反映在该子进程中。因此,您必须关闭任何现有的笔记本服务器,更改环境变量,然后重新启动笔记本服务器以查看效果。

我们对我们的 tf.data 流水线进行了三项优化:

  • 预取数据批次

  • 使用并行化的 map() 函数而不是标准的 map() 函数

  • 通过设置 TF_GPU_THREAD_MODE=gpu_private 使用专用的内核启动线程。

14.4.2 混合精度训练

正如前面所解释的,混合精度训练是指在模型训练中采用 16 位和 32 位操作的组合。例如,可训练参数(即变量)保持为 32 位浮点值,而操作(例如,矩阵乘法)产生 16 位浮点输出。

在 Keras 中,启用混合精度训练非常简单。您只需从 Keras 中导入 mixed_precision 命名空间,并创建一个使用 mixed precision 数据类型的策略,通过传递 mixed_float16。最后,将其设置为全局策略。然后,每当您定义一个新模型时,它都会使用此策略来确定模型的数据类型:

from tensorflow.keras import mixed_precision
policy = mixed_precision.Policy('mixed_float16')
mixed_precision.set_global_policy(policy)

让我们重新定义我们定义的 CNN 模型并快速检查数据类型,以了解此新策略如何更改模型数据类型:

conv_model = get_cnn_model()

现在我们将选择一个层并检查输入/内部参数(例如,可训练权重)和输出的数据类型:

print("Input to the layers have the data type: {}".format(
    conv_model.get_layer("conv2d_1").input.dtype)
)
print("Variables in the layers have the data type: {}".format(
    conv_model.get_layer("conv2d_1").trainable_variables[0].dtype)
)
print("Output of the layers have the data type: {}".format(
    conv_model.get_layer("conv2d_1").output.dtype)
)

这将打印。

Input to the layers have the data type: <dtype: 'float16'>
Variables in the layers have the data type: <dtype: 'float32'>
Output of the layers have the data type: <dtype: 'float16'>

正如您所见,输入和输出的数据类型为 float16,而变量的数据类型为 float32。这是混合精度训练所采用的设计原则。为了确保在更新权重时保留精度,变量保持为 float32 类型。

损失缩放以避免数值下溢。

使用混合精度训练时,必须小心处理损失。半精度浮点数(float16)值的动态范围比单精度浮点数(float32)值更小。动态范围是指每种数据类型可以表示的值的范围。例如,float16 可以表示的最大值为 65,504 (最小正数为 0.000000059604645),而 float32 可以达到 3.4 × 10³⁸ (最小正数为 1.4012984643 × 10 − 45)。由于 float16 数据类型的动态范围较小,损失值很容易下溢或溢出,导致反向传播时出现数值问题。为了避免这种情况,损失值需要乘以适当的值进行缩放,以使梯度保持在 float16 值的动态范围之内。幸运的是,Keras 会自动处理此问题。

当策略设置为 mixed_float16 且调用 model.compile() 时,优化器会自动包装为 tf.keras.mixed_precision.LossScaleOptimizer() (mng.bz/Aydo)。LossScaleOptimizer() 会在模型优化期间动态缩放损失,以避免数值上的问题。如果您没有使用 Keras 构建模型,则必须手动处理此问题。

现在重新运行模型训练:

batch_size = 32
tr_ds, v_ds = get_flower_datasets(
    os.path.join('data', '17flowers','jpg'), batch_size=batch_size, 
➥ flatten_images=False
)

# This tensorboard call back does the following
# 1\. Log loss and accuracy
# 2\. Profile the model memory/time for 10  batches
tb_callback = tf.keras.callbacks.TensorBoard(
    log_dir=profile_log_dir, profile_batch=[10, 20]
)

conv_model.fit(
    tr_ds, validation_data=v_ds, epochs=2, callbacks=[tb_callback]
)

在加入我们介绍的各种优化步骤后运行模型训练。通过改变 TensorBoard 上的运行来进行比较。例如,我们在概览页面上显示了使用和不使用优化技巧的元素的并排比较。我们可以看到,在引入 tf.data pipeline 相关的优化后,时间大大减少(图 14.11)。

14-11

图 14.11 显示了使用和不使用数据和模型相关优化的分析概览的并排比较。引入优化后,输入时间大大减少。

你可能认为,使用 16 位操作后设备的计算时间并没有显著降低。使用 16 位操作最大的优势在于减少了 GPU 的内存消耗。TensorBoard 提供了一个称为 memory profile 的单独视图,用于分析模型的内存占用情况(图 14.12)。您可以使用此视图来分析模型的内存瓶颈或内存泄漏。

14-12

图 14.12 显示了经过优化前后的内存占用情况差异。使用 16 位操作减少了模型的内存消耗。差异非常明显。

可以清楚地看出,在使用混合精度训练后,内存需求显著下降。当使用混合精度训练时(从 5.48 GB 至 1.25 GB),模型对内存的需求降低了约 76%。

图表明了两种类型的内存:。这些是程序用于在执行程序时跟踪变量和函数调用的基本内存空间。从这些中,堆将帮助我们了解内存使用模式或与内存相关的问题,因为在程序执行期间创建的各种对象和变量都保存在其中。例如,如果存在内存泄漏,您将看到堆使用的内存量正在增加。在这里,我们可以看到内存使用情况相当线性,并且可以假设没有重大的内存泄漏。您可以在下一页的侧边栏中阅读有关堆和栈的更多信息。

堆 vs. 栈

程序运行时的内存保持在堆栈或堆中。例如,函数调用保持在堆栈中,其中最后一次调用位于堆栈顶部,最早的调用位于底部。当这些函数调用创建对象时,例如,它们被写入堆中(术语“堆”来自与堆数据结构无关的对象集合)。您可以想象堆中包含许多对象和属性,没有特定顺序(因此术语“堆”)。随着函数调用结束,项目将自动从堆栈中弹出。但是,当对象不再使用时,由程序员释放堆的责任,因为它们在函数调用结束后仍然存在。然而,在现代编程语言中,垃圾收集器会自动处理这个问题。 (请参阅mng.bz/VMZ5mng.bz/ZAER。)

您可能听说过“堆栈溢出”的术语,当代码中的递归函数调用没有合理满足终止条件时,大量的函数调用会溢出堆栈。另外,我们不能忽视一个受开发者欢迎的网站的名称是如何产生的(stackoverflow.com)。我认为没有比 Stack Overflow 本身更好的资源来解释这个问题了:mng.bz/R4ZZ

我们还可以看到有关哪些操作使用了多少内存的细节。例如,我们知道 CNN 的主要瓶颈是在一系列卷积/池化层之后的第一个 Dense 层。表 14.1 证实了这一点。也就是说,它显示了 Dense 层,其形状为 [115200, 512](即第一个 Dense 层),使用了最多的内存。

表 14.1 内存分解表。该表显示了各种 TensorFlow 操作的内存使用情况以及它们的数据形状。

操作名称分配大小(GiBs)请求大小(GiBs)发生次数区域类型数据类型形状
预分配/未知0.7430.7431持久/动态无效未知
gradient_tape/sequential/dense/MatMul/Cast/Cast0.2200.2201输出浮点[115200,512]
gradient_tape/sequential/batch_normalisation_3/FusedBatchNormGradV30.0510.0291temphalf[32,512,31,31]
gradient_tape/sequential/average_pooling2d/AvgPool/AvgPoolGrad0.0360.0291outputhalf[32,31,31,512]
gradient_tape/sequential/batch_normalisation_3/FusedBatchNormGradV30.0290.0291outputhalf[32,31,31,512]
gradient_tape/sequential/batch_normalisation_3/FusedBatchNormGradV30.0290.0292temphalf[32,512,31,31]

最后,您可以查看trace viewer。这个工具提供了各种操作在 CPU 或 GPU 上是如何执行的纵向视图以及所花费的时间。这提供了关于各种操作何时以及如何被安排和执行的非常详细的视图。

在左侧,您可以看到在 CPU 上执行了什么操作,而在 GPU 上执行了什么操作。例如,您可以看到大多数与模型相关的操作(例如,卷积)在 GPU 上执行,而 tf.data 操作(例如,解码图像)在 GPU 上执行。您还可以注意到,跟踪查看器单独显示了 GPU 私有线程。

TensorBoard 的用途远不止我们在这里列出的。要了解更多,请参考以下侧边栏。

TensorBoard 的其他视图

TensorBoard 有许多不同的视图可用。我们已经讨论了最常用的视图,我将让读者探索我们没有讨论的视图。然而,剩下的视图中有一些值得注意的视图:

Debugger v2

Debugger v2 是 TensorFlow 2.3 以后引入的工具。它的主要目的是调试模型中的数值问题。例如,在模型训练过程中出现 NaN 值是深度网络的一个非常常见的问题。Debugger v2 将提供模型中各种元素(例如,输入和输出张量)的全面逐步分解,以及哪些元素产生了数值错误。有关更多信息,请访问www.tensorflow.org/tensorboard/debugger_v2

HParams

Hparams 是一个视图,帮助超参数优化,并允许您深入研究个别运行,以了解哪些参数有助于改善模型性能。tensorboard.plugins.hparams.api 提供了各种有用的功能和回调,以轻松优化 Keras 模型的超参数。然后,可以在 HParams 视图中查看超参数优化期间发生的试验。有关更多信息,请访问mng.bz/2nKg

What-If 工具

What-If 是一个工具,可以为黑盒模型提供有价值的见解,有助于解释这些模型。例如,您可以使用一些数据运行模型推理。然后,您可以修改数据,并通过 What-If 工具查看输出如何变化。此外,它提供了各种工具,用于分析模型的性能和公平性。有关更多信息,请访问mng.bz/AyjW

在下一节中,我们将讨论如何在 TensorBoard 上可视化和与词向量交互。

练习 4

你已经进行了模型性能分析。你已经看到了以下时间概述:

  • 输入时间:1.5 毫秒

  • 设备计算时间:6.7 毫秒

  • 内核启动时间:9.8 毫秒

  • 输出时间:10.1 毫秒

  • 主机计算时间:21.2 毫秒

对于这种情况,假设超过 5 毫秒的时间是有改进空间的机会。列出三个代码/环境更改建议以提高模型性能。

14.5 使用 TensorBoard 可视化词向量

你正在一家电影推荐公司担任 NLP 工程师,负责开发一种可以在小设备上训练的电影推荐模型。为了减少训练开销,使用了一种技术:使用预训练的词向量并将其冻结(即不进行训练)。你认为 GloVe 词向量将是一个很好的起点,并计划使用它们。但在此之前,你必须确保这些向量充分捕捉到电影特定术语/单词中的语义/关系。为此,你需要在 TensorBoard 上可视化这些单词的词向量,并分析 GloVe 向量是否表示了单词之间的合理关系。

我们需要做的第一件事是下载 GloVe 词向量。你已经在笔记本中提供了下载 GloVe 向量的代码,它与我们过去下载数据集的方式非常相似。因此,我们不会详细讨论下载过程。GloVe 词向量可从nlp.stanford.edu/projects/glove/获取。GloVe 向量有几个不同的版本;它们具有不同的维度和词汇量:

  • 使用 Wikipedia 2014 + Gigaword 5 数据集进行训练,共有 60 亿个标记;词汇量为 400,000 个;大小写不敏感的标记;词向量维度为 50D、100D、200D 和 300D

  • 使用 Common Crawl 数据集训练,共有 420 亿个标记;词汇量为 1,900,000 个;大小写不敏感的标记;词向量维度为 300D

  • 使用 Common Crawl 数据集训练,共有 8400 亿个标记;词汇量为 2,200,000 个;大小写敏感的标记;词向量维度为 300D

  • 使用 Twitter 数据集进行训练,共有 20 亿个推文;总标记数为 270 亿个;词汇量为 1,200,000 个;大小写不敏感的标记;词向量维度为 25D、50D、100D 和 200D

GloVe 词向量

GloVe(代表 Global Vectors)是一种单词向量算法,通过查看语料库的全局和局部统计信息生成单词向量。例如,像 Skip-gram 或 Continuous Bag-of-Words 这样的单词向量算法仅依赖于给定单词的局部上下文来学习该单词的单词向量。对单词在较大语料库中的使用情况缺乏全局信息的关注会导致次优的单词向量。GloVe 通过计算一个大型共现矩阵来表示所有单词之间的共现频率(即,如果一个给定单词出现在另一个单词的上下文中)来融合全局统计信息。有关 GloVe 向量的更多信息,请参见 mng.bz/1oGX

我们将使用第一类别(最小的)中的 50 维词向量。一个 50 维词向量将对语料库中的每个标记有 50 个值的向量。一旦在笔记本中运行代码提取数据,你将看到一个名为 glove.6B.50d.txt 的文件出现在数据文件夹中。让我们使用 pd.read_csv() 函数将其加载为 pandas DataFrame:

df = pd.read_csv(
    os.path.join('data', 'glove.6B.50d.txt'), 
    header=None, 
    index_col=0, 
    sep=None, 
    error_bad_lines=False, 
    encoding='utf-8'
)
df.head()

这将返回表格 14.2. 现在我们将下载 IMDB 电影评论数据集(ai.stanford.edu/~amaas/data/sentiment/)。由于这个数据集可以轻松地作为 TensorFlow 数据集(通过 tensorflow_datasets 库)获得,我们可以使用它:

review_ds = tfds.load('imdb_reviews')
train_review_ds = review_ds["train"]

一旦我们下载了数据,我们将创建一个包含训练集中所有评论(文本)的语料库,以字符串列表的形式:

corpus = []
for data in train_review_ds:      
    txt = str(np.char.decode(data["text"].numpy(), encoding='utf-8')).lower()
    corpus.append(str(txt))

接下来,我们想要获取此语料库中最常见的 5,000 个单词,以便我们可以比较这些常见单词的 GloVe 向量,以查看它们是否包含合理的关系。为了获得最常见的单词,我们将使用内置的 Counter 对象。Counter 对象计算词汇表中单词的频率:

from collections import Counter

corpus = " ".join(corpus)

cnt = Counter(corpus.split())
most_common_words = [w for w,_ in cnt.most_common(5000)]
print(cnt.most_common(100))

这将打印

[('the', 322198), ('a', 159953), ('and', 158572), ('of', 144462), ('to', 
➥ 133967), ('is', 104171), ('in', 90527), ('i', 70480), ('this', 69714), 
➥ ('that', 66292), ('it', 65505), ('/><br', 50935), ('was', 47024), 
➥ ('as', 45102), ('for', 42843), ('with', 42729), ('but', 39764), ('on', 
➥ 31619), ('movie', 30887), ('his', 29059), 
➥ ... ,
➥ ('other', 8229), ('also', 8007), ('first', 7985), ('its', 7963), 
➥ ('time', 7945), ('do', 7904), ("don't", 7879), ('me', 7722), ('great', 
➥ 7714), ('people', 7676), ('could', 7594), ('make', 7590), ('any', 
➥ 7507), ('/>the', 7409), ('after', 7118), ('made', 7041), ('then', 
➥ 6945), ('bad', 6816), ('think', 6773), ('being', 6390), ('many', 6388), 
➥ ('him', 6385)]

14-12_table_14-2

使用了 IMDB 电影评论数据集中最常见的 5,000 个单词的语料库以及 GloVe 向量,我们找到了这两个集合之间的常见标记以进行可视化:

df_common = df.loc[df.index.isin(most_common_words)]

这将给出大约 3,600 个在两个集合中都出现的标记列表。

接下来,我们可以在 TensorBoard 上可视化这些向量。再次强调,单词向量是给定语料库中标记的数值表示。这些单词向量的特点(与独热编码单词相反)是它们捕捉了单词的语义。例如,如果计算“cat”和“dog”的单词向量之间的距离,它们会比“cat”和“volcano”更接近。但是在分析更大的一组标记之间的关系时,我们喜欢有一个可视化辅助工具。如果有一种方法可以在二维或三维平面上可视化这些单词向量,那将更容易可视化和理解。有降维算法,如主成分分析(PCA)(mng.bz/PnZw)或 t-SNE(distill.pub/2016/misread-tsne/)可以实现这一点。本书不涉及这些特定算法的使用。好消息是,使用 TensorBoard,你可以做到这一点。TensorBoard 可以将这些高维向量映射到一个更小的投影空间。要做到这一点,我们首先要将这些权重加载为一个 TensorFlow 变量,然后将其保存为 TensorFlow 检查点。然后我们还要将单词或标记保存为一个新文件,每行一个标记,对应于我们刚刚保存的一组单词向量中的每个向量。有了这个,你就可以在 TensorBoard 上可视化单词向量(见下一个列表)。

列表 14.5 在 TensorBoard 上可视化单词向量

from tensorboard.plugins import projector

log_dir=os.path.join('logs', 'embeddings')
weights = tf.Variable(df_common.values)                          ❶

checkpoint = tf.train.Checkpoint(embedding=weights)              ❷
checkpoint.save(os.path.join(log_dir, "embedding.ckpt"))         ❷

with open(os.path.join(log_dir, 'metadata.tsv'), 'w') as f:      ❸
    for w in df_common.index:
        f.write(w+'\n')

config = projector.ProjectorConfig()                             ❹
embedding = config.embeddings.add()
embedding.metadata_path = 'metadata.tsv'                         ❺
projector.visualize_embeddings(log_dir, config)

❶ 用我们捕获的嵌入创建一个 tf.Variable。

❷将嵌入保存为 TensorFlow 检查点。

❸ 保存元数据(一个 TSV 文件),其中每个与嵌入对应的单词被附加为新行。

❹创建一个特定于投影仪和嵌入的配置(有关详细信息,请参阅文本)。

❺设置元数据路径,以便 TensorBoard 可以在可视化中包含它。

要可视化来自保存的 TensorFlow 检查点和元数据(即,保存的单词向量对应的标记),我们使用 tensorboard.plugins.projector 对象。然后我们定义一个 ProjectorConfig 对象和一个嵌入配置。我们将保留它们的默认配置,这适合我们的问题。当调用 config.embeddings.add()时,它将生成一个使用默认配置的嵌入配置(类型为 EmbeddingInfo 对象)。ProjectorConfig 包含诸如以下信息:

  • model_checkpoint_directory —— 包含嵌入的检查点的目录

EmbeddingInfo 包含

  • tensor_name —— 如果嵌入使用了特殊的张量名称

  • metadata_path —— 包含嵌入标签的 TSV 文件的路径

要查看可用配置的完整列表,请参考 mng.bz/J2Zo 上的文件。在其当前状态下,投影仪的配置不支持太多的定制。因此,我们将保持默认设置。我们将在 EmbeddingInfo 配置中设置一个配置,即 metadata_path。我们将 metadata_path 设置为包含令牌的文件,最后将其传递给 projecter.visualize_embeddings() 函数。我们给它一个日志目录,投影仪将自动检测 TensorFlow 检查点并加载它。

我们一切都准备就绪。在您的笔记本上,执行以下行以打开 TensorBoard:

%tensorboard --logdir logs/embeddings/ --port 6007

要可视化词向量,它们需要在 --logdir 指向的确切目录中(即不在嵌套文件夹中)。因此,我们需要一个新的 TensorBoard 服务器。这行代码将在端口 6007 上打开一个新的 TensorBoard 服务器。图 14.13 描述了在 TensorBoard 中显示的内容。

关于 %tensorboard 魔术命令的有趣事实

%tensorboard 魔术命令足够智能,能够知道何时打开新的 TensorBoard 服务器以及何时不需要。如果您一遍又一遍地执行相同的命令,它将重用现有的 TensorBoard。但是,如果您执行带有不同 --logdir 或 --port 的命令,它将打开一个新的 TensorBoard 服务器。

14-13

图 14.13 在 TensorBoard 上的词向量视图。您可以选择使用哪种降维算法(以及参数)来获取词向量的二维或三维表示。在可视化中悬停在点上将显示由该点表示的单词。

您可以在可视化中悬停在显示的点上,它们将显示它们代表的语料库中的哪个单词。您可以通过切换维度控制器来可视化二维或三维空间中的词向量。您可能想知道我们选择的词向量。它们最初有 50 个维度 —— 我们如何在二维或三维空间中可视化这样高维度的数据呢?有一套降维算法可以为我们做到这一点。一些示例是 t-SNE (mng.bz/woxO),PCA(主成分分析; mng.bz/ZAmZ),以及 UMAP(Uniform Manifold Approximation and Projection; arxiv.org/pdf/1802.03426.pdf)。参考附带的链接以了解更多关于这些算法的信息。

您可以在 TensorBoard 上做的不仅仅是词向量的简单可视化。您可以通过突出显示可视化中的特定单词进行更详细的分析。为此,您可以使用正则表达式。例如,图 14.14 中显示的可视化是使用正则表达式(?:fred|larry|mrs.|mr.|michelle|sea|denzel|beach|comedy|theater|idiotic|sadistic|marvelous|loving|gorg|bus|truck|lugosi)生成的。

14-14

图 14.14 在可视化中搜索单词。您可以使用正则表达式来搜索单词的组合。

这就结束了我们关于 TensorBoard 的讨论。在下一章中,我们将讨论 TensorFlow 如何帮助我们轻松创建机器学习流水线并部署模型。

练习 5

如果您想在 TensorBoard 中显示单词向量时包含唯一标识符,而不仅仅是单词本身,例如,您想要看到“loving; 218”而不是“loving”,其中 218 是给予该单词的唯一标识符。为此,您需要更改写入 metadata.tsv 文件的内容。不仅仅是单词,每行上都写一个用分号分隔的递增 ID。例如,如果单词是[“a”, “b”, “c”],那么新行应该是[“a;1”, “b;2”, “c;3”]。您如何进行更改?

摘要

  • TensorBoard 是一个用于可视化数据(例如图像)和实时跟踪模型性能的优秀工具。

  • 在使用 Keras 构建模型时,您可以使用方便的 tf.keras.callbacks.TensorBoard() 回调来记录模型性能、层激活直方图等。

  • 如果您有自定义指标想要记录到 TensorBoard 中,您可以在 tf.summary 命名空间中使用相应的数据类型(例如,如果您想要记录随时间变化的模型精度等,可以使用 tf.summary.scalar())。

  • 每次将信息记录到 TensorBoard 中的会话称为一次运行。您应该为不同的运行制定一个可读且健壮的命名约定。一个好的命名约定应该捕捉您所做的主要更改以及运行执行的日期/时间。

  • TensorBoard Profile 提供了各种各样的性能分析结果(使用 NVIDIA 的 libcupti 库),例如模型训练过程中各个子任务所花费的时间(例如,设备计算时间、主机计算时间、输入时间等)、模型使用的内存以及各种操作是如何进行的顺序视图。

  • TensorBoard 是一个用于可视化高维数据(如图像和单词向量)的强大工具。

练习答案

练习 1

image_writer = tf.summary.create_file_writer(image_logdir)

with image_writer.as_default():
    for bi, batch in enumerate(steps_image_batches):
        tf.summary.image(
            “batch_{}”.format(bi), 
            batch, 
            max_outputs=10, 
            step=bi
        )

练习 2

log_dir = "./logs "

classif_model.compile(
    loss=’binary_crossentropy', 
    optimizer=’adam’, 
    metrics=[tf.keras.metrics.Precision(), tf.keras.metrics.Recall()]
)

tb_callback = tf.keras.callbacks.TensorBoard(
    log_dir=log_dir, histogram_freq=1, profile_batch=0
)

classif_model.fit(tr_ds, validation_data=v_ds, epochs=10, callbacks=[tb_callback])

练习 3

 writer = tf.summary.create_file_writer(log_dir)

 x_n_minus_1 = 1
 x_n_minus_2 = 0

 with writer.as_default():        
     for i in range(100):
         x_n = x_n_minus_1 + x_n_minus_2
         x_n_minus_1 = x_n
      x_n_minus_2 = x_n_minus_1

      tf.summary.scalar("fibonacci", x_n, step=i)

      writer.flush()

练习 4

  1. 主机上正在进行大量的计算。这可能是因为设备(例如,GPU)的内存不足。使用混合精度训练将有助于缓解这个问题。此外,可能有太多无法在 GPU 上运行的非 TensorFlow 代码。为此,使用更多的 TensorFlow 操作并将这样的代码转换为 TensorFlow 将获得加速。

  2. 内核启动时间增加了。这可能是因为工作负载严重受限于 CPU。在这种情况下,我们可以合并 TF_GPU_THREAD_MODE 环境变量,并将其设置为 gpu_private。这将确保有几个专用线程用于为 GPU 启动内核。

  3. 输出时间显著偏高。这可能是因为频繁向磁盘写入过多输出。为解决此问题,我们可以考虑将数据在内存中保存更长时间,并仅在少数时刻将其刷新到磁盘上。

练习 5

log_dir=os.path.join('logs', 'embeddings')

weights = tf.Variable(df_common.values)
   checkpoint = tf.train.Checkpoint(embedding=weights)
checkpoint.save(os.path.join(log_dir, "embedding.ckpt"))

with open(os.path.join(log_dir, 'metadata.tsv'), 'w') as f:
    for i, w in enumerate(df_common.index):
        f.write(w+'; '+str(i)+'\n')

第十五章:TFX:MLOps 和使用 TensorFlow 部署模型

本章涵盖内容

  • 使用 TFX(TensorFlow-Extended)编写端到端数据流水线

  • 通过 TFX Trainer API 训练一个简单的神经网络

  • 使用 Docker 将模型服务(推理)容器化,并将其作为服务呈现

  • 在本地机器上部署模型,以便通过 API 使用

在第十四章,我们研究了一个非常多功能的工具,它与 TensorFlow 捆绑在一起:TensorBoard。TensorBoard 是一个可视化工具,可以帮助你更好地理解数据和模型。除其他外,它可以方便

  • 监控和追踪模型性能

  • 可视化模型的数据输入(例如图片、音频)

  • 对模型进行分析以了解其性能或内存瓶颈

我们学习了如何使用 TensorBoard 来可视化像图片和词向量这样的高维数据。我们探讨了如何将 Keras 回调嵌入到 TensorBoard 中,以便可视化模型性能(准确率和损失)以及自定义指标。然后,我们使用 CUDA 性能分析工具来分析模型的执行,以理解执行模式和内存瓶颈。

在本章中,我们将探索最近引起极大关注的机器学习新领域:MLOps。MLOps 源自 ML 和 DevOps(源自开发和运维)术语。根据亚马逊网络服务(AWS)的说法,“DevOps 是文化哲学、实践和工具的组合,它增加了组织交付应用和服务的能力:以比使用传统软件开发和基础设施管理流程的组织更快的速度进化和改进产品。”还有一个与 MLOps 密切相关的术语,即模型的实际投入使用。很难区分这两个术语,因为它们有重叠之处,有时可以互换使用,但我倾向于这样理解这两个事物:MLOps 定义了一个工作流,将自动化大部分步骤,从收集数据到交付在该数据上训练的模型,几乎不需要人工干预。实际投入使用是部署训练好的模型(在私有服务器或云上),使客户能够以稳健的方式使用模型进行设计目的。它可以包括任务,例如设计可扩展的 API,可以扩展以处理每秒数千个请求。换句话说,MLOps 是一段旅程,让你到达的目的地是模型的实际投入使用。

让我们讨论为什么拥有(大部分)自动化的流水线来开发机器学习模型是重要的。要实现其价值,你必须考虑到规模问题。对于像谷歌、Facebook 和亚马逊这样的公司,机器学习已经深深扎根于他们提供的产品中。这意味着数以百计甚至数千个模型每秒产生预测。此外,对于拥有数十亿用户的公司来说,他们不能容忍他们的模型变得过时,这意味着不断地训练/微调现有模型以适应新数据的收集。MLOps 可以解决这个问题。MLOps 可用于摄取收集的数据、训练模型、自动评估模型,并在它们通过预定义的验证检查后将其推送到生产环境中。验证检查是为了确保模型达到预期的性能标准,并防范对抗不良表现的模型(例如,由于新的入站训练数据发生大幅变化、推送了新的未经测试的超参数变更等,可能会生成不良的模型)。最后,模型被推送到生产环境,通过 Web API 访问以获取输入的预测。具体而言,API 将为用户提供一些端点(以 URL 的形式),用户可以访问这些端点(可选地带上需要完成请求的参数)。话虽如此,即使对于依赖机器学习模型的较小公司来说,MLOps 也可以极大地标准化和加速数据科学家和机器学习工程师的工作流程。这将大大减少数据科学家和机器学习工程师在每次开展新项目时从头开始创建这些工作流程所花费的时间。阅读有关 MLOps 的更多信息,请访问mng.bz/Pnd9

我们如何在 TensorFlow 中进行 MLOps?无需寻找其他,TFX(TensorFlow 扩展)就是答案。TFX 是一个库,提供了实现摄取数据、将数据转换为特征、训练模型和将模型推送到指定生产环境所需的所有功能。这是通过定义一系列执行非常具体任务的组件来完成的。在接下来的几节中,我们将看看如何使用 TFX 来实现这一目标。

使用 TFX 编写数据管道

想象一下,你正在开发一个系统,根据天气条件来预测森林火灾的严重程度。你已经获得了过去观察到的森林火灾的数据集,并被要求创建一个模型。为了确保你能够将模型提供为服务,你决定创建一个工作流程来摄取数据并使用 TFX 训练模型。这个过程的第一步是创建一个能够读取数据(以 CSV 格式)并将其转换为特征的数据管道。作为这个管道的一部分,你将拥有一个数据读取器(从 CSV 生成示例),显示字段的摘要统计信息,了解数据的模式,并将其转换为模型理解的正确格式。

关于环境的重要信息

要运行本章的代码,强烈建议使用 Linux 环境(例如 Ubuntu),并且将提供该环境的说明。TFX 未针对 Windows 环境进行测试(mng.bz/J2Y0)。另一个重要的事项是我们将使用稍旧版本的 TFX(1.6.0)。撰写时,最新版本为 1.9.0。这是因为在 1.6.0 版本之后的版本中,运行 TFX 在诸如笔记本等交互式环境中所需的关键组件已损坏。此外,本章后面我们将使用一种名为 Docker 的技术。由于对资源的访问受到严格限制,使 Docker 按我们所需的方式运行在 Windows 上可能会相当困难。此外,对于本章,我们将定义一个新的 Anaconda 环境。要执行此操作,请按照以下说明操作:

  • 打开一个终端窗口,并进入代码存储库中的 Ch15-TFX-for-MLOps-in-TF2 目录。

  • 如果您已经激活了 Anaconda 虚拟环境(例如 manning.tf2),请通过运行 conda deactivate manning.tf2 来停用它。

  • 运行 conda create -n manning.tf2.tfx python=3.6 来创建一个新的虚拟 Anaconda 环境。

  • 运行 conda activate manning.tf2.tfx 以激活新环境。

  • 运行 pip install --use-deprecated=legacy-resolver -r requirements.txt。

  • 运行 jupyter notebook。

  • 打开 tfx/15.1_MLOps_with_tensorflow.ipynb 笔记本。

第一件事是下载数据集(列表 15.1)。我们将使用一个记录了葡萄牙蒙特西尼奥公园历史森林火灾的数据集。该数据集在archive.ics.uci.edu/ml/datasets/Forest+Fires上免费提供。它是一个 CSV 文件,具有以下特征:

  • X—蒙特西尼奥公园地图中的 x 轴空间坐标

  • Y—蒙特西尼奥公园地图中的 y 轴空间坐标

  • month—一年中的月份

  • day—一周中的日期

  • Fine Fuel Moisture Code (FFMC)—代表森林树冠阴影下的林地燃料湿度

  • DMC—土壤平均含水量的数字评级

  • Drought Code (DC)—表示土壤干燥程度的深度

  • Initial Spread Index (ISI)—预期的火灾蔓延速率

  • temp—摄氏度温度

  • RH—相对湿度,单位%

  • wind—风速,单位 km/h

  • rain—外部降雨量,单位 mm/m2

  • area—森林烧毁面积(单位公顷)

选择机器学习模型的特征

选择机器学习模型的特征不是一个微不足道的任务。通常,在使用特征之前,您必须了解特征,特征间的相关性,特征-目标相关性等等,然后就可以判断是否应使用特征。因此,不应该盲目地使用模型的所有给定特征。然而,在这种情况下,重点在于 MLOps,而不是数据科学决策,我们将使用所有特征。使用所有这些特征将稍后有助于解释在定义 MLOps 管道时可用的各种选项。

我们的任务是在给出所有其他特征的情况下预测烧毁面积。请注意,预测连续值(如面积)需要回归模型。因此,这是一个回归问题,而不是分类问题。

图 15.1 下载数据集

import os
import requests
import tarfile

import shutil

if not os.path.exists(os.path.join('data', 'csv', 'forestfires.csv')):    ❶
    url = "http:/ /archive.ics.uci.edu/ml/machine-learning-databases/forest-
➥ fires/forestfires.csv"
    r = requests.get(url)                                                 ❷

    if not os.path.exists(os.path.join('data', 'csv')):                   ❸
        os.makedirs(os.path.join('data', 'csv'))                          ❸

    with open(os.path.join('data', 'csv', 'forestfires.csv'), 'wb') as f: ❸
        f.write(r.content)                                                ❸
else:
    print("The forestfires.csv file already exists.")

if not os.path.exists(os.path.join('data', 'forestfires.names')):         ❹

    url = "http:/ /archive.ics.uci.edu/ml/machine-learning-databases/forest-
➥ fires/forestfires.names"
    r = requests.get(url)                                                 ❹

    if not os.path.exists('data'):                                        ❺
        os.makedirs('data')                                               ❺

    with open(os.path.join('data', 'forestfires.names'), 'wb') as f:      ❺
        f.write(r.content)                                                ❺

else:
    print("The forestfires.names file already exists.")

❶ 如果未下载数据文件,请下载该文件。

❷ 此行下载给定 URL 的文件。

❸ 创建必要的文件夹并将下载的数据写入其中。

❹ 如果未下载包含数据集描述的文件,请下载它。

❺ 创建必要的目录并将数据写入其中。

在这里,我们需要下载两个文件:forestfires.csv 和 forestfires.names。forestfires.csv 以逗号分隔的格式包含数据,第一行是标题,其余部分是数据。forestfires.names 包含更多关于数据的信息,以便您想更多地了解它。接下来,我们将分离出一个小的测试数据集以供后续手动测试。拥有一个专用的测试集,在任何阶段都没有被模型看到,将告诉我们模型的泛化情况如何。这将是原始数据集的 5%。其余 95%将用于训练和验证数据:

import pandas as pd

df = pd.read_csv(
    os.path.join('data', 'csv', 'forestfires.csv'), index_col=None, 
➥ header=0
)
train_df = df.sample(frac=0.95, random_state=random_seed)
test_df = df.loc[~df.index.isin(train_df.index), :]

train_path = os.path.join('data','csv','train')
os.makedirs(train_path, exist_ok=True)
test_path = os.path.join('data','csv','test')
os.makedirs(test_path, exist_ok=True)

train_df.to_csv(
    os.path.join(train_path, 'forestfires.csv'), index=False, header=True
)
test_df.to_csv(
    os.path.join(test_path, 'forestfires.csv'), index=False, header=True
)

现在,我们将开始 TFX 管道。第一步是定义存储管道工件的根目录。您可能会问什么是管道工件?在运行 TFX 管道时,它会在目录中存储各个阶段的中间结果(在某个子目录结构下)。其中的一个例子是,当您从 CSV 文件中读取数据时,TFX 管道会将数据拆分为训练和验证子集,将这些示例转换为 TFRecord 对象(即 TensorFlow 内部用于数据的对象类型),并将数据存储为压缩文件:

_pipeline_root = os.path.join(
    os.getcwd(), 'pipeline', 'examples', 'forest_fires_pipeline'
)

TFX 使用 Abseil 进行日志记录。Abseil 是从 Google 的内部代码库中提取的开源 C ++库集合。它提供了日志记录,命令行参数解析等功能。如果您感兴趣,请在abseil.io/docs/python/阅读有关该库的更多信息。我们将设置日志记录级别为 INFO,以便我们可以在 INFO 级别或更高级别看到日志记录语句。日志记录是具有重要功能的,因为我们可以获得很多见解,包括哪些步骤成功运行以及哪些错误被抛出:

absl.logging.set_verbosity(absl.logging.INFO)

完成初始设置后,我们将定义一个 InteractiveContext:

from tfx.orchestration.experimental.interactive.interactive_context import 
➥ InteractiveContext

context = InteractiveContext(
    pipeline_name = "forest_fires", pipeline_root=_pipeline_root
)

TFX 在一个上下文中运行流水线。上下文被用来运行你在流水线中定义的各个步骤。它还起着非常重要的作用,就是在我们在流水线中进行过程中管理不同步骤之间的状态。为了管理状态之间的转换并确保流水线按预期运行,它还维护了一个元数据存储(一个小规模的数据库)。元数据存储包含各种信息,如执行顺序、组件的最终状态和产生的错误。你可以在以下侧边栏中了解有关元数据的信息。

元数据中有什么?

一旦创建了 InteractiveContext,你会在流水线根目录中看到一个名为 metadata.sqlite 的数据库。这是一个轻量级、快速的 SQL 数据库(www.sqlite.org/index.xhtml),专为处理少量数据和传入请求而设计的。该数据库将记录有关输入、输出和执行相关输出的重要信息(组件的运行标识符、错误)。这些信息可以用于调试你的 TFX 流水线。元数据可以被视为不是直接输入或输出,但仍然是正确执行组件所必需的数据,以提供更大的透明度。在具有许多组件以许多不同方式相互连接的复杂 TFX 流水线的调试中,元数据可能非常有帮助。你可以在www.tensorflow.org/tfx/guide/mlmd上了解更多信息。

我们要开始定义流水线了。本节流水线的主要目的是

  • 从 CSV 文件加载数据并拆分为训练和验证数据

  • 了解数据的模式(例如各个列、数据类型、最小/最大值等)

  • 显示关于各种特征分布的摘要统计和图形

  • 将原始列转换为特征,可能需要特殊的中间处理步骤

这些步骤是模型训练和部署的前奏。每个任务都将是流水线中的一个单独组件,在合适的时候我们将详细讨论这些步骤。

15.1.1 从 CSV 文件加载数据

第一步是定义一个组件来从 CSV 文件中读取示例并将数据拆分为训练和评估数据。为此,你可以使用 tfx.components.CsvExampleGen 对象。我们所需要做的就是将包含数据的目录提供给 input_base 参数:

from tfx.components import CsvExampleGen

example_gen = CsvExampleGen(input_base=os.path.join('data', 'csv', 'train'))

然后我们使用之前定义的 InteractiveContext 来运行示例生成器:

context.run(example_gen)

让我们来看看这一步骤产生了什么。要查看数据,请前往 _pipeline_root 目录(例如,Ch15-TFX-for-MLOps-in-TF2/tfx/pipeline)。它应该具有类似于图 15.1 所示的目录/文件结构。

15-01

图 15.1 运行 CsvExampleGen 之后的目录/文件结构

您将看到在管道中创建了两个 GZip 文件(即带有 .gz 扩展名)。您会注意到在 CsvExampleGen 文件夹中有两个子目录:Split-train 和 Split-eval,分别包含训练和验证数据。当您运行包含前述代码的笔记本单元时,您还将看到一个输出 HTML 表格,显示 TFX 组件的输入和输出(图 15.2)。

15-02

图 15.2 运行 CsvExampleGen 组件生成的输出 HTML 表格

有一些值得注意的事项。首先,您将看到 execution_id,这是一个计数器生成的值,该计数器跟踪您运行 TFX 组件的次数。换句话说,每次运行 TFX 组件(如 CsvExampleGen)时,计数器都会增加 1。如果您继续向下看,您会看到一些关于 CsvExampleGen 如何分割数据的重要信息。如果您查看 component > CsvExampleGen > exec_properties > output_config 下,您会看到类似于

"split_config": { 
    "splits": [ 
        { "hash_buckets": 2, "name": "train" }, 
        { "hash_buckets": 1, "name": "eval" } 
    ] 
} 

这里说数据集已被分成两组:train 和 eval。训练集大约占原始数据的三分之二,而评估集大约占原始数据的三分之一。这些信息是通过查看 hash_buckets 属性推断出来的。TFX 使用哈希将数据分成训练集和评估集。默认情况下,它将定义三个哈希桶。然后 TFX 使用每个记录中的值为该记录生成哈希。记录中的值传递给哈希函数以生成哈希。然后使用生成的哈希来将该示例分配到一个桶中。例如,如果哈希值为 7,则 TFX 可以轻松找到具有 7% 的桶,3 = 1,这意味着它将被分配到第二个桶(因为桶是从零开始索引的)。您可以按以下方式访问 CsvExampleGen 中的元素。

关于哈希的更多信息

有许多哈希函数,例如 MD5、SHA1 等。您可以在 blog.jscrambler.com/hashing-algorithms/ 上阅读有关哈希函数的更多信息。在 TensorFlow 中,有两种不同的函数可用于生成哈希:tf.strings.to_hash_bucket_fast (mng.bz/woJq) 和 tf.strings.to_ hash_bucket_strong ()。强哈希函数速度较慢,但更能抵御可能操纵输入以控制生成的哈希值的恶意攻击。

artifact = example_gen.outputs['examples'].get()[0]

print("Artifact split names: {}".format(artifact.split_names))
print("Artifact URI: {}".format(artifact.uri)

这将打印以下输出:

Artifact split names: ["train", "eval"]
Artifact URI: <path to project>/Ch15-TFX-for-MLOps-in-
➥ TF2/tfx/pipeline/examples/forest_fires_pipeline/CsvExampleGen/examples/1

之前我们说过,随着我们在管道中的进展,TFX 会将中间输出存储起来。我们看到 CsvExampleGen 组件已将数据存储为 .gz 文件。事实上,它将 CSV 文件中找到的示例存储为 TFRecord 对象。TFRecord 用于将数据存储为字节流。由于 TFRecord 是在使用 TensorFlow 时存储数据的常用方法;这些记录可以轻松地作为 tf.data.Dataset 检索,并且可以检查数据。下一个清单显示了如何做到这一点。

列表 15.2 打印 CsvExampleGen 存储的数据

train_uri = os.path.join(
    example_gen.outputs['examples'].get()[0].uri, 'Split-train'       ❶
) 

tfrecord_filenames = [
    os.path.join(train_uri, name) for name in os.listdir(train_uri)   ❷
]

dataset = tf.data.TFRecordDataset(
    tfrecord_filenames, compression_type="GZIP"
)                                                                     ❸

for tfrecord in dataset.take(2):                                      ❹
  serialized_example = tfrecord.numpy()                               ❺
  example = tf.train.Example()                                        ❻
  example.ParseFromString(serialized_example)                         ❼
  print(example)                                                      ❽

❶ 获取代表训练示例的输出工件的 URL,该工件是一个目录。

❷ 获取此目录中的文件列表(所有压缩的 TFRecord 文件)。

❸ 创建一个 TFRecordDataset 来读取这些文件。GZip(扩展名为 .gz)包含一组 TFRecord 对象。

❹ 迭代前两个记录(可以是小于或等于数据集大小的任何数字)。

❺ 从 TFRecord(包含一个示例)获取字节流。

❻ 定义一个知道如何解析字节流的 tf.train.Example 对象。

❼ 将字节流解析为适当可读的示例。

❽ 打印数据。

如果你运行这段代码,你会看到以下内容:

features {
  feature {
    key: "DC"
    value {
      float_list {
        value: 605.7999877929688
      }
    }
  }
  ...
  feature {
    key: "RH"
    value {
      int64_list {
        value: 43
      }
    }
  }
  feature {
    key: "X"
    value {
      int64_list {
        value: 5
      }
    }
  }
  ...
  feature {
    key: "area"
    value {
      float_list {
        value: 2.0
      }
    }
  }
  feature {
    key: "day"
    value {
      bytes_list {
        value: "tue"
      }
    }
  }
  ...
}

...

tf.train.Example 将数据保存为一组特征,每个特征都有一个键(列描述符)和一个值。你会看到给定示例的所有特征。例如,DC 特征具有浮点值 605.799,RH 特征具有整数值 43,area 特征具有浮点值 2.0,而 day 特征具有 bytes_list(用于存储字符串)值为 "tue"(即星期二)。

在移动到下一节之前,让我们再次提醒自己我们的目标是什么:开发一个模型,可以根据数据集中的所有其他特征来预测火灾蔓延(以公顷为单位)。这个问题被构建为一个回归问题。

15.1.2 从数据生成基本统计信息

作为下一步,我们将更好地理解数据。这称为探索性数据分析(EDA)。EDA 通常不是很明确,并且非常依赖于您正在解决的问题和数据。您还必须考虑到通常在项目交付之前的有限时间。换句话说,您不能测试所有内容,必须优先考虑要测试的内容和要假设的内容。对于我们在这里处理的结构化数据,一个很好的起点是了解类型(数值与分类)以及各列值的分布。TFX 为此提供了一个组件。StatisticsGen 将自动生成这些统计信息。我们很快将更详细地看到此模块提供了什么样的见解:

from tfx.components import StatisticsGen

statistics_gen = StatisticsGen(
    examples=example_gen.outputs['examples'])

context.run(statistics_gen)

这将生成一个 HTML 表格,类似于您在运行 CsvExampleGen 后看到的表格(见图 15.3)。

15-03

图 15.3 StatisticsGen 组件提供的输出

然而,要检索此步骤的最有价值的输出,您必须运行以下命令:

context.show(statistics_gen.outputs['statistics'])

这将在管道根目录中创建以下文件(见图 15.4)。

15-04

图 15.4 运行 StatisticsGen 后的目录/文件结构

图 15.5 展示了 TFX 提供的有关数据的宝贵信息集合。图 15.5 中的输出图是一个包含丰富数据的金矿,提供了大量关于我们处理的数据的信息。它为你提供了基本但全面的图表套件,提供了有关数据中存在的列的许多信息。让我们从上到下来看。在顶部,你可以选择排序和过滤图 15.5 中显示的输出。例如,你可以改变图表的顺序,选择基于数据类型的图表,或者通过正则表达式进行筛选。

15-05

图 15.5 由 StatisticsGen 组件生成的数据的摘要统计图

默认情况下,StatisticsGen 将为训练集和评估集生成图表。然后每个训练和评估部分将有几个子部分;在这种情况下,我们有数值列和分类列的部分。

在左边,你可以看到一些数字统计和特征的评估,而在右边,你可以看到特征分布的视觉表示。例如,拿训练集中的 FFMC 特征来说。我们可以看到它有 333 个例子且 0%的特征缺失值。它的平均值约为 90,标准偏差为 6.34。在图表中,你可以看到分布是相当倾斜的。几乎所有的值都集中在 80-90 范围内。你将看到稍后这可能会给我们制造问题以及我们将如何解决它们。

在分类部分,你可以看到日和月特征的值。例如,日特征有七个唯一值,且 0%缺失。日特征的最频繁值(即模式)出现了 60 次。请注意,日表示为条形图,月表示为线图,因为对于唯一值高于阈值的特征,使用线图可以使图表清晰且减少混乱。

15.1.3 从数据推断模式

到目前为止,我们已经从 CSV 文件中加载了数据并探索了数据集的基本统计信息。下一个重要的步骤是推断数据的模式。一旦提供了数据,TFX 可以自动推断数据的模式。如果你使用过数据库,推断出的模式与数据库模式相同。它可以被视为数据的蓝图,表达数据的结构和重要属性。它也可以被视为一组规则,规定数据应该看起来像什么。例如,如果你有了模式,你可以通过参考模式来分类给定的记录是否有效。

不做更多的话,让我们创建一个 SchemaGen 对象。SchemaGen 需要前一步的输出(即 StatisticsGen 的输出)和一个名为 infer_feature_shape 的布尔参数。

from tfx.components import SchemaGen

schema_gen = SchemaGen(
    statistics=statistics_gen.outputs[‘statistics’],
    infer_feature_shape=False)

context.run(schema_gen)

在这里,我们将 infer_feature_shape 设置为 False,因为我们将在特征上进行一些转换。因此,我们将有更大的灵活性来自由操作特征形状。然而,设置这个参数(infer_feature_shape)意味着对下游步骤(称为 transform 步骤)的重要改变。当 infer_feature_shape 设置为 False 时,传递给 transform 步骤的张量被表示为 tf.SparseTensor 对象,而不是 tf.Tensor 对象。如果设置为 True,则需要是一个具有已知形状的 tf.Tensor 对象。接下来,要查看 SchemaGen 的输出,可以执行以下操作

context.show(schema_gen.outputs['schema'])

这将产生表 15.1 所示的输出。

表 15.1 TFX 生成的模式输出

特征名称类型存在价值
‘day’STRING必须的单个的‘day’
‘month’STRING必须的单个的‘month’
‘DC’FLOAT必须的单个的-
‘DMC’FLOAT必须的单个的-
‘FFMC’FLOAT必须的单个的-
‘ISI’FLOAT必须的单个的-
‘RH’INT必须的单个的-
‘X’INT必须的单个的-
‘Y’INT必须的单个的-
‘area’FLOAT必须的单个的-
‘rain’FLOAT必须的单个的-
‘temp’FLOAT必须的单个的-
‘wind’FLOAT必须的单个的
‘day’‘fri’‘mon’‘sat’‘sun’‘thu’‘tue’‘wed’
‘month’‘apr’‘aug’‘dec’‘feb’‘jan’‘jul’‘jun’‘mar’‘may’‘oct’‘sep’‘nov’

域定义了给定特征的约束。我们列出了 TFX 中定义的一些最受欢迎的域:

  • 整数域值(例如,定义整数特征的最小/最大值)

  • 浮点域值(例如,定义浮点值特征的最小/最大值)

  • 字符串域值(例如,为字符串特征定义允许的值/标记)

  • 布尔域值(例如,可以用于定义真/假状态的自定义值)

  • 结构域值(例如,可以用于定义递归域[域内的域]或具有多个特征的域)

  • 自然语言域值(例如,为相关语言特征定义一个词汇表[允许的标记集合])

  • 图像域值(例如,可以用来限制图像的最大字节大小)

  • 时间域值(例如,可以用来定义数据/时间特征)

  • 时间值域(例如,可以用来定义不带日期的时间)

域的列表可在名为 schema.proto 的文件中找到。schema.proto 在mng.bz/7yp9上定义。这些文件是使用一个叫做 Protobuf 的库定义的。Protobuf 是一种用于对象序列化的库。您可以阅读下面的侧边栏了解有关 Protobuf 库的更多信息。

Protobuf 库

Protobuf 是由 Google 开发的对象序列化/反序列化库。需要序列化的对象被定义为 Protobuf 消息。消息的模板由 .proto 文件定义。然后,为了进行反序列化,Protobuf 提供了诸如 ParseFromString() 等函数。要了解有关该库的更多信息,请参阅 mng.bz/R45P

接下来,我们将看到如何将数据转换为特征。

15.1.4 将数据转换为特征

我们已经到达了数据处理管道的最终阶段。最后一步是将我们提取的列转换为对我们的模型有意义的特征。我们将创建三种类型的特征:

  • 密集的浮点数特征—值以浮点数(例如,温度)的形式呈现。这意味着该值会按原样传递(可以选择进行归一化处理;例如,Z 分数归一化)以创建一个特征。

  • 分桶特征—根据预定义的分桶间隔对数值进行分桶。这意味着该值将根据其落入的分桶而转换为桶索引(例如,我们可以将相对湿度分成三个值:低[-inf,33),中[33,66),高[66,inf))。

  • 分类特征(基于整数或字符串)—值是从预定义的值集中选择的(例如,日期或月份)。如果该值尚未是整数索引(例如,日期作为字符串),则将使用将每个单词映射到索引的词汇表将其转换为整数索引(例如,“mon” 被映射为 0,“tue” 被映射为 1,等等)。

我们将向数据集中的每个字段介绍其中一种特征转换:

  • X(空间坐标)—以浮点数值表示

  • Y(空间坐标)—以浮点数值表示

  • wind(风速)—以浮点数值表示

  • rain(室外降雨)—以浮点数值表示

  • FFMC(燃料湿度)—以浮点数值表示

  • DMC(平均含水量)—以浮点数值表示

  • DC(土壤干燥深度)—以浮点数值表示

  • ISI(预期火灾蔓延速率)—以浮点数值表示

  • temp(温度)—以浮点数值表示

  • RH(相对湿度)—作为分桶值表示

  • month—作为分类特征表示

  • day—作为分类特征表示

  • area(烧毁面积)—作为数值保留的标签特征

我们首先要定义一些常量,这些常量将帮助我们跟踪哪个特征分配给了哪个类别。此外,我们将保留特定属性(例如,分类特征的最大类数;请参阅下一个列表)。

列表 15.3 定义特征转换步骤中与特征相关的常量

%%writefile forest_fires_constants.py                              ❶

VOCAB_FEATURE_KEYS = ['day','month']                               ❷

MAX_CATEGORICAL_FEATURE_VALUES = [7, 12]                           ❸

DENSE_FLOAT_FEATURE_KEYS = [
    'DC', 'DMC', 'FFMC', 'ISI', 'rain', 'temp', 'wind', 'X', 'Y'   ❹
]

BUCKET_FEATURE_KEYS = ['RH']                                       ❺

BUCKET_FEATURE_BOUNDARIES = [(33, 66)]                             ❻

LABEL_KEY = 'area'def transformed_name(key):                                         ❽

    return key + '_xf'

❶ 此命令将将此单元格的内容写入文件(阅读侧边栏以获取更多信息)。

❷ 基于词汇(或字符串)的分类特征。

❸ 数据集中假设每个分类特征都有一个最大值。

❹ 密集特征(这些将作为模型输入,或进行归一化处理)。

❺ 分桶特征。

❻ 分桶特征的分桶边界(例如,特征 RH 将被分桶为三个箱子:[0, 33),[33, 66),[66,inf))。

❼ 标签特征将保留为数值特征,因为我们正在解决回归问题。

❽ 定义一个函数,将在特征名称后添加后缀。这将帮助我们区分生成的特征和原始数据列。

我们将这些笔记本单元格写为 Python 脚本(或 Python 模块)的原因是因为 TFX 期望运行所需的一些代码部分作为 Python 模块。

%%writefile 魔术命令

%%writefile 是一个 Jupyter 魔术命令(类似于%%tensorboard)。它会导致 Jupyter 笔记本将单元格中的内容写入到新文件中(例如,Python 模块/脚本)。这是从笔记本单元格创建独立 Python 模块的好方法。笔记本很适合进行实验,但对于生产级别的代码,Python 脚本更好。例如,我们的 TFX 管道期望某些函数(例如,如何将原始列预处理为特征)是独立的 Python 模块。我们可以方便地使用%%writefile 命令来实现这一点。

此命令必须指定为要写入文件的单元格中的第一个命令。

接下来,我们将编写另一个模块 forest_fires_transform.py,其中将有一个预处理函数(称为 preprocessing_fn),该函数定义了每个数据列应如何处理以成为特征(请参见下一个列表)。

列表 15.4 定义将原始数据转换为特征的 Python 模块。

%%writefile forest_fires_transform.py                                      ❶

import tensorflow as tf
import tensorflow_transform as tft

import forest_fires_constants                                              ❷

_DENSE_FLOAT_FEATURE_KEYS = forest_fires_constants.DENSE_FLOAT_FEATURE_KEYS❸
_VOCAB_FEATURE_KEYS = forest_fires_constants.VOCAB_FEATURE_KEYS            ❸
_BUCKET_FEATURE_KEYS = forest_fires_constants.BUCKET_FEATURE_KEYS          ❸
_BUCKET_FEATURE_BOUNDARIES = 
➥ forest_fires_constants.BUCKET_FEATURE_BOUNDARIES                        ❸
_LABEL_KEY = forest_fires_constants.LABEL_KEY                              ❸
_transformed_name = forest_fires_constants.transformed_name                ❸

def preprocessing_fn(inputs):                                              ❹

  outputs = {}

  for key in _DENSE_FLOAT_FEATURE_KEYS:                                    ❺
    outputs[_transformed_name(key)] = tft.scale_to_z_score(                ❻
        sparse_to_dense(inputs[key])                                       ❼
    )

  for key in _VOCAB_FEATURE_KEYS:
    outputs[_transformed_name(key)] = tft.compute_and_apply_vocabulary(    ❽
        sparse_to_dense(inputs[key]),
        num_oov_buckets=1)

  for key, boundary in zip(_BUCKET_FEATURE_KEYS,                           ❾
➥ _BUCKET_FEATURE_BOUNDARIES):                                            ❾
    outputs[_transformed_name(key)] = tft.apply_buckets(                   ❾
        sparse_to_dense(inputs[key]), bucket_boundaries=[boundary]         ❾
    )                                                                      ❾

  outputs[_transformed_name(_LABEL_KEY)] = 
➥ sparse_to_dense(inputs[_LABEL_KEY])                                     ❿

  return outputs

def sparse_to_dense(x):                                                    ⓫

    return tf.squeeze(
        tf.sparse.to_dense(
            tf.SparseTensor(x.indices, x.values, [x.dense_shape[0], 1])
        ),
        axis=1
    )

❶ 此代码列表中的内容将被写入到单独的 Python 模块中。

❷ 导入先前定义的特征常量。

❸ 导入 forest_fires_constants 模块中定义的所有常量。

❹ 这是 tf.transform 库中必不可少的回调函数,用于将原始列转换为特征。

❺ 对所有密集特征进行处理。

❻ 对密集特征执行基于 Z-score 的缩放(或标准化)。

❼ 因为在 SchemaGen 步骤中 infer_feature_shape 设置为 False,我们的输入是稀疏张量。它们需要转换为密集张量。

❽ 对于基于词汇的特征,构建词汇表并将每个标记转换为整数 ID。

❾ 对待分桶的特征,使用定义的分桶边界,对特征进行分桶。

❿ 标签特征只是简单地转换为密集张量,没有其他特征转换。

⓫ 一个将稀疏张量转换为密集张量的实用函数。

您可以看到该文件被命名为 forest_fires_transform.py。它定义了一个 preprocessing_fn()函数,该函数接受一个名为 inputs 的参数。inputs 是一个从特征键到在 CSV 文件中找到的数据列的映射字典,从 example_gen 输出流动。最后,它返回一个字典,其中特征键映射到使用 tensorflow_transform 库转换的特征。在方法的中间,您可以看到预处理函数执行三项重要工作。

首先,它读取所有密集特征(其名称存储在 _DENSE_FLOAT_FEATURE_KEYS 中),并使用 z 分数对值进行归一化。z 分数将某一列x归一化为

15_05a

其中,μ(x)是列的平均值,σ(x)是列的标准差。要对数据进行归一化,可以调用 tensorflow_transform 库中的 scale_to_z_score()函数。您可以阅读有关 tensorflow_transform 的侧边栏,了解更多有关该库提供的内容。然后,该函数使用新的键(通过 _transformed_name 函数生成)将每个特征存储在输出中,该新键衍生自原始特征名称(新键通过在原始特征名称末尾添加 _xf 生成)。

接下来,它处理基于词汇的分类特征(其名称存储在 _VOCAB_FEATURE_KEYS 中),通过使用字典将每个字符串转换为索引。该字典将每个字符串映射到索引,并且可以自动从提供的训练数据中学习。这类似于我们如何使用 Keras 的 Tokenizer 对象学习字典,将单词转换为单词 ID。在 tensorflow_transform 库中,您可以使用 compute_and_apply_vocabulary()函数完成这一操作。对于 compute_and_apply_vocabulary()函数,我们可以通过传递 num_oov_buckets=1 来将任何未见字符串分配给特殊类别(除了已分配给已知类别的类别)。

然后,函数处理待进行桶化的特征。Bucketization 是将连续值应用于桶的过程,其中桶由一组边界定义。使用 apply_buckets()函数可以轻松地对特征进行 bucket 化,该函数将特征(在输入字典中提供)和桶边界作为输入参数。

最后,我们保留包含标签的列不变。通过这样,我们定义了 Transform 组件(mng.bz/mOGr)。

tensorflow_transform:将原始数据转换为特征

tensorflow_transform 是 TensorFlow 中的一个子库,主要关注特征转换。它提供了各种功能来计算各种东西:

  • 对特征进行桶化(例如,将一系列值分组到预定义的一组桶中)

  • 从字符串列中提取词袋特征

  • 数据集的协方差矩阵

  • 列的均值、标准差、最小值、最大值、计数等

您可以在mng.bz/5QgB上阅读有关此库提供的功能的更多信息。

from tfx.components import Transform

transform = Transform(
    examples=example_gen.outputs['examples'],
    schema=schema_gen.outputs['schema'],
    module_file=os.path.abspath('forest_fires_transform.py'),
)

context.run(transform)

Transform 组件接受三个输入:

  • CsvExampleGen 组件的输出示例

  • SchemaGen 生成的架构

  • 用于将数据转换为特征的 preprocessing_fn()函数的 Python 模块

当涉及到多组件流水线,比如 TFX 流水线时,我们必须尽可能地检查每一个中间输出。这比交给偶然性并祈祷一切顺利要好得多(通常情况下都不是这样)。因此,让我们通过打印运行 Transform 步骤后保存到磁盘上的一些数据来检查输出(见下一列表)。打印数据的代码与使用 CsvExampleGen 组件时打印数据的代码类似。

列表 15.5 检查 TFX Transform 步骤产生的输出

import forest_fires_constants

_DENSE_FLOAT_FEATURE_KEYS = forest_fires_constants.DENSE_FLOAT_FEATURE_KEYS
_VOCAB_FEATURE_KEYS = forest_fires_constants.VOCAB_FEATURE_KEYS
_BUCKET_FEATURE_KEYS = forest_fires_constants.BUCKET_FEATURE_KEYS
_LABEL_KEY = forest_fires_constants.LABEL_KEY

# Get the URI of the output artifact representing the training examples, which is a directory
train_uri = os.path.join(
    transform.outputs['transformed_examples'].get()[0].uri, 'Split-train'
)
tfrecord_filenames = [
    os.path.join(train_uri, name) for name in os.listdir(train_uri)        ❶
]

dataset = tf.data.TFRecordDataset(
    tfrecord_filenames, compression_type="GZIP"
)                                                                          ❷

example_records = []                                                       ❸
float_features = [
    _transformed_name(f) for f in _DENSE_FLOAT_FEATURE_KEYS + [_LABEL_KEY] ❹
]
int_features = [
    _transformed_name(f) for f in _BUCKET_FEATURE_KEYS + 
➥ _VOCAB_FEATURE_KEYS                                                     ❹
]
for tfrecord in dataset.take(5):                                           ❺
  serialized_example = tfrecord.numpy()                                    ❻
  example = tf.train.Example()                                             ❻
  example.ParseFromString(serialized_example)                              ❻
  record = [
    example.features.feature[f].int64_list.value for f in int_features     ❼
  ] + [
    example.features.feature[f].float_list.value for f in float_features   ❼
  ]
  example_records.append(record)                                           ❽
  print(example)
  print("="*50)

❶ 获取此目录中文件的列表(所有压缩的 TFRecord 文件)。

❷ 创建一个 TFRecordDataset 来读取这些文件。

❸ 用于存储检索到的特征值(以供以后检查)

❹ 稠密(即,浮点数)和整数(即,基于词汇和分桶)特征

❺ 获取数据集中的前五个示例。

❻ 获取一个 TF 记录并将其转换为可读的 tf.train.Example。

❼ 我们将从 tf.train.Example 对象中提取特征的值以供后续检查。

❽ 将提取的值作为记录(即,值的元组)附加到 example_records 中。

解释的代码将打印特征转换后的数据。每个示例都将整数值存储在属性路径下,例如 example.features.feature[] .int64_list.value,而浮点值存储在 example.features.feature [].float_list.value 中。这将打印例如

features {
  feature {
    key: "DC_xf"
    value {
      float_list {
        value: 0.4196213185787201
      }
    }
  }

  ...

  feature {
    key: "RH_xf"
    value {
      int64_list {
        value: 0
      }
    }
  }

  ...

  feature {
    key: "area_xf"
    value {
      float_list {
        value: 2.7699999809265137
      }
    }
  }

  ...
}

请注意,我们使用 _transformed_name()函数来获取转换后的特征名称。我们可以看到,浮点值(DC_xf)使用 z 分数标准化,基于词汇的特征(day_xf)转换为整数,并且分桶特征(RH_xf)被呈现为整数。

经验法则:尽可能检查您的管道

当使用 TFX 等第三方库提供的组件时,对于底层实际发生的事情几乎没有透明度。TFX 并不是一个高度成熟的工具,并且正在开发过程中,这使问题更加严重。因此,我们总是尝试并入一些代码片段来探查这些组件,这将帮助我们检查这些组件的输入和输出是否正常。

在下一节中,我们将训练一个简单的回归模型,作为我们一直在创建的流水线的一部分。

练习 1

假设你想要做以下事情而不是先前定义的特征转换:

  • DC—将数据缩放到[0, 1]的范围内

  • temp—利用边界值(-inf,20],(20,30]和(30,inf)进行分桶处理

一旦特征被转换,将它们添加到名为 outputs 的字典中,其中每个特征都以转换后的特征名称作为键。假设你可以通过调用 _transformed_name('temp') 来获取 temp 的转换后的特征名称。您如何使用 tensorflow_transform 库来实现此目标?您可以使用 scale_to_0_1() 和 apply_buckets() 函数来实现这一点。

15.2 训练一个简单的回归神经网络:TFX Trainer API

您已经定义了一个 TFX 数据管道,可以将 CSV 文件中的示例转换为模型准备的特征。现在,您将使用 TFX 定义一个模型训练器,该模型训练器将采用一个简单的两层全连接回归模型,并将其训练在从数据管道流出的数据上。最后,您将使用模型对一些样本评估数据进行预测。

使用 TFX 定义了一个良好定义的数据管道后,我们就可以使用从该管道流出的数据来训练模型。通过 TFX 训练模型一开始可能会稍微费劲,因为它期望的函数和数据的严格结构。但是,一旦您熟悉了您需要遵循的格式,它就会变得更容易。

我们将分三个阶段进行本节的学习。首先,让我们看看如何定义一个适合 TFX Transform 组件中定义的输出特征的 Keras 模型。最终,模型将接收 Transform 组件的输出。接下来,我们将研究如何编写一个封装了模型训练的函数。此函数将使用所定义的模型,并结合几个用户定义的参数,对模型进行训练并将其保存到所需的路径。保存的模型不能只是任意模型;在 TensorFlow 中它们必须具有所谓的 签名。这些签名规定了模型在最终通过 API 使用时的输入和输出是什么样子的。API 通过一个服务器提供,该服务器公开一个网络端口供客户端与 API 通信。图 15.6 描述了 API 如何与模型关联。

15-06

图 15.6 模型如何与 API、TensorFlow 服务器和客户端交互

让我们理解图 15.6 中发生了什么。首先,一个 HTTP 客户端发送请求到服务器。正在监听任何传入请求的服务器(即 TensorFlow 服务服务器)将读取请求并将其指向所需的模型签名。一旦模型签名接收到数据,它将对数据进行必要的处理,将其传递给模型,并生成输出(例如预测)。一旦预测可用,服务器将其返回给客户端。我们将在单独的部分详细讨论 API 和服务器端。在本节中,我们的重点是模型。

TensorFlow 服务中的签名是什么?

在现实生活中,签名的目的是唯一标识一个人。同样,TensorFlow 使用签名来唯一确定当通过 HTTP 请求将输入传递给模型时模型应该如何行为。一个签名有一个键和一个值。键是一个唯一标识符,定义了要激活该签名的确切 URL。值被定义为一个 TensorFlow 函数(即用 @tf.function 装饰的函数)。这个函数将定义如何处理输入并将其传递给模型以获得最终期望的结果。你现在不需要担心细节。我们有一个专门的部分来学习关于签名的内容。

我们将在单独的子部分回顾签名以更详细地理解它们。最后,我们将通过加载模型并向其提供一些数据来直观地检查模型预测。

15.2.1 定义 Keras 模型

使用 TFX 训练模型的基石是定义一个模型。有两种方法可以为 TFX 定义模型:使用 Estimator API 或使用 Keras API。我们将使用 Keras API,因为 Estimator API 不推荐用于 TensorFlow 2(有关详细信息,请参见下面的侧边栏)。

Estimator API vs. Keras API

我的观点是,未来,Keras 可能会成为构建模型的首选 API,而 Estimator API 可能会被弃用。TensorFlow 网站上说:

不建议使用 Estimators 编写新代码。Estimators 运行 v1.Session 风格的代码,这更难以编写正确,并且可能表现出乎意料,特别是当与 TF 2 代码结合使用时。Estimators 落在我们的兼容性保证下,但除了安全漏洞之外将不会收到任何修复。详情请参阅迁移指南。

来源:www.tensorflow.org/tfx/tutorials/tfx/components

我们首先要创建一个名为 _build_keras_model() 的函数,它将执行两项任务。首先,它将为我们在 Transform 步骤中定义的所有特征创建 tf.feature_column 类型的对象。tf.feature_column 是一种特征表示标准,被 TensorFlow 中定义的模型所接受。它是一种用于以列为导向的方式定义数据的便利工具(即,每个特征都表示为一列)。列式表示非常适用于结构化数据,其中每列通常是目标变量的独立预测器。让我们来看一些在 TensorFlow 中找到的具体 tf.feature_column 类型:

  • tf.feature_column.numeric_column——用于表示像温度这样的稠密浮点字段。

  • tf.feature_column.categorical_column_with_identity——用于表示分类字段或桶化字段,其中值是指向类别或桶的整数索引,例如日或月。因为传递给列本身的值是类别 ID,所以使用了“identity”这个术语。

  • tf.feature_column.indicator_column—将 tf.feature_column.categorical_column_with_identity 转换为独热编码表示。

  • tf.feature_column.embedding_column—可以用于从基于整数的列(如 tf.feature_column.categorical_column_with_identity)生成嵌入。它在内部维护一个嵌入层,并将给定整数 ID 返回相应的嵌入。

要查看完整列表,请参考mng.bz/6Xeo。在这里,我们将使用 tf.feature_columns 的前三种类型作为我们待定义模型的输入。以下列表概述了如何使用 tf.feature_columns 作为输入。

第 15.6 节 构建使用特征列的 Keras 模型

def _build_keras_model() -> tf.keras.Model:                     ❶

  real_valued_columns = [                                       ❷
      tf.feature_column.numeric_column(key=key, shape=(1,))
      for key in _transformed_names(_DENSE_FLOAT_FEATURE_KEYS)
  ]

  categorical_columns = [                                       ❸
      tf.feature_column.indicator_column(
          tf.feature_column.categorical_column_with_identity(
              key, 
              num_buckets=len(boundaries)+1
          )
      ) for key, boundaries in zip(
          _transformed_names(_BUCKET_FEATURE_KEYS),
          _BUCKET_FEATURE_BOUNDARIES
      )
  ]

  categorical_columns += [                                      ❹
      tf.feature_column.indicator_column(
          tf.feature_column.categorical_column_with_identity( 
              key,
              num_buckets=num_buckets,
              default_value=num_buckets-1
          )
      ) for key, num_buckets in zip(
              _transformed_names(_VOCAB_FEATURE_KEYS),
              _MAX_CATEGORICAL_FEATURE_VALUES
      )      
  ]

  model = _dnn_regressor(                                       ❺
      columns=real_valued_columns+categorical_columns,          ❻
      dnn_hidden_units=[128, 64]                                ❼
  )

  return model

❶ 定义函数签名。它将一个 Keras 模型作为输出返回。

❷ 为密集特征创建 tf.feature_column 对象。

❸ 为分桶特征创建 tf.feature_column 对象。

❹ 为分类特征创建 tf.feature_column 对象。

❺ 使用该函数定义一个深度回归模型。

❻ 使用上面定义的列

❼ 它将有两个中间层:128 个节点和 64 个节点。

让我们看一下存储在 real_valued_columns 中的第一组特征列。我们取密集浮点值列的原始键的转换名称,并为每列创建一个 tf.feature_column.numeric_column。您可以看到我们正在传递

  • (字符串)—特征的名称

  • 形状(一个列表/元组)—完整形状将派生为[批量大小] + 形状

例如,列 temp 的键将为 temp_xf,形状为(1,),意味着完整形状为[批量大小,1]。这个形状为[批量大小,1]是有意义的,因为每个密集特征每条记录只有一个值(这意味着我们在形状中不需要特征维度)。让我们通过一个玩具例子来看看 tf.feature_column.numeric_column 的运作:

a = tf.feature_column.numeric_column("a")
x = tf.keras.layers.DenseFeatures(a)({'a': [0.5, 0.6]})
print(x)

这将输出

tf.Tensor(
[[0.5]
 [0.6]], shape=(2, 1), dtype=float32)

在为分桶特征定义 tf.feature_column.categorical_column_with_identity 时,您需要传递

  • 键(字符串)—特征的名称

  • num_buckets(整数)—分桶特征中的桶数

例如,对于被分桶的 RH 特征,其键为 RH_xf,num_buckets = 3,其中桶为[[-inf,33),[33,66),[66,inf]]。由于我们将 RH 的分桶边界定义为(33, 66),num_buckets 被定义为 len(boundaries) +1 = 3。最后,每个分类特征都包装在 tf.feature_column.indicator_column 中,以将每个特征转换为独热编码表示。同样,我们可以进行一个快速实验来查看这些特征列的效果如何:

b = tf.feature_column.indicator_column(
    tf.feature_column.categorical_column_with_identity('b', num_buckets=10)
)
y = tf.keras.layers.DenseFeatures(b)({'b': [5, 2]})
print(y)

这将产生

tf.Tensor(
[[0\. 0\. 0\. 0\. 0\. 1\. 0\. 0\. 0\. 0.]
 [0\. 0\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]], shape=(2, 10), dtype=float32)

最后,基于词汇的分类特征与分桶特征类似处理。对于每个特征,我们获取特征名称和最大类别数,并使用 tf.feature_column.categorical_column_with_identity 列定义一个列,其中

  • 键(字符串)—特征的名称。

  • num_buckets(整数)—类别数。

  • default_value(int)—如果遇到以前看不见的类别,将分配该值。

在这里,默认值是一个重要的部分。它将决定测试数据中出现的任何看不见的类别会发生什么,这些类别不是训练数据的一部分。我们问题中基于词汇的分类特征是天和月,分别只能有 7 和 12 个不同的值。但可能会出现这样的情况,训练集只有 11 个月,测试集有 12 个月。为了解决这个问题,我们将任何看不见的类别分配给我们可用的最后一个类别 ID(即,num_buckets - 1)。

现在我们有了一组明确定义的数据列,这些数据列包装在 tf.feature_column 对象中,准备馈送给模型。最后,我们看到一个名为 _dnn_regressor() 的函数,它将创建一个 Keras 模型,如下图所示,并将我们创建的列和一些其他超参数传递给它。现在让我们讨论一下这个函数的具体内容。

列表 15.7 定义回归神经网络

def _dnn_regressor(columns, dnn_hidden_units):                            ❶

  input_layers = {
      colname: tf.keras.layers.Input(
          name=colname, shape=(), dtype=tf.float32
      )                                                                   ❷
      for colname in _transformed_names(_DENSE_FLOAT_FEATURE_KEYS)
  }
  input_layers.update({
      colname: tf.keras.layers.Input(
          name=colname, shape=(), dtype='int32'
      )                                                                   ❸
      for colname in _transformed_names(_VOCAB_FEATURE_KEYS)
  })
  input_layers.update({
      colname: tf.keras.layers.Input(
          name=colname, shape=(), dtype='int32'
      )                                                                   ❹
      for colname in _transformed_names(_BUCKET_FEATURE_KEYS)
  })  

  output = tf.keras.layers.DenseFeatures(columns)(input_layers)           ❺
  for numnodes in dnn_hidden_units:
    output = tf.keras.layers.Dense(numnodes, activation='tanh')(output)   ❻

  output = tf.keras.layers.Dense(1)(output)                               ❼

  model = tf.keras.Model(input_layers, output)                            ❽
  model.compile(
      loss='mean_squared_error',                                          ❾
      optimizer=tf.keras.optimizers.Adam(lr=0.001)
  )
  model.summary(print_fn=absl.logging.info)                               ❿

  return model

❶ 定义一个函数,它以一堆列和一列隐藏维度的列表作为输入。

❷ 模型的输入: 输入字典,其中键是特征名称,值是 Keras 输入层

❸ 通过为基于词汇的分类特征创建输入层更新字典。

❹ 通过为分桶特征创建输入层更新字典。

❺ 由于输入层被定义为字典,我们使用 DenseFeatures 层生成单一的张量输出。

❻ 我们通过创建一系列稠密层来递归计算输出。

❼ 创建一个最终的回归层,它有一个输出节点和线性激活。

❽ 使用输入和输出定义模型。

❾ 编译模型。请注意它使用均方误差作为损失函数。

❿ 通过我们在开始定义的 absl 记录器打印模型的摘要。

我们已按列的形式定义了数据,其中每列都是 TensorFlow 特征列。定义数据后,我们使用一个特殊层叫做 tf.keras.layers.DenseFeatures 来处理这些数据。 DenseFeatures 接受

  • 特征列列表

  • 一个 tf.keras.layers.Input 层的字典,其中每个输入层的键都在特征列列表中找到的列名

有了这些数据,DenseFeatures 层可以将每个输入层映射到相应的特征列,并在最后产生一个单一的张量输出(存储在变量输出中)(图 15.7)。

15-07

图 15.7 DenseFeatures 层功能概述

然后我们通过将数据通过几个隐藏层流动来递归计算输出。这些隐藏层的大小(一个整数列表)作为参数传递给函数。我们将使用 tanh 非线性激活作为隐藏层。最终的隐藏输出进入具有线性激活的单节点回归层。

最后,我们使用 Adam 优化器和均方损失作为损失函数对模型进行编译。重要的是要注意,我们必须为模型使用与回归兼容的损失函数。均方误差是用于回归问题的非常常见的损失函数。

Python 中的类型提示

您将看到一些函数的定义方式与我们过去所做的方式不同。例如,函数定义为

def _build_keras_model() -> tf.keras.Model:

def run_fn(fn_args: tfx.components.FnArgs):

这是 Python 中的可视类型提示,并且在 Python 中是可用的。这意味着类型不会以任何方式由 Python 解释器强制执行;相反,它们是一种视觉提示,以确保开发人员使用正确的输入和输出类型。在函数中定义参数时,可以使用以下语法定义该参数期望的数据类型 def (: ):。例如,在函数 run_fn() 中,第一个参数 fn_args 必须是 tfx.components.FnArgs 类型。

然后,您还可以将函数返回的输出定义为 def (: ) -> :。例如,_build_keras_model() 函数返回的对象必须是一个 tf.keras.Model 对象。

有些对象需要使用多种数据类型或自定义数据类型(例如,字符串列表)创建复杂数据类型。对于这一点,您可以使用一个名为 typing 的内置 Python 库。typing 允许您方便地定义数据类型。有关更多信息,请参阅 docs.python.org/3/library/typing.xhtml

在列表 15.8 中,我们定义了一个函数,给定一组训练数据文件名和评估数据文件名,生成用于训练和评估数据的 tf.data.Dataset 对象。我们将这个特殊函数定义为 _input_fn()。_input_fn() 接受三个参数:

  • file_pattern — 一组文件路径,其中文件包含数据

  • data_accessor — TFX 中的特殊对象,通过接受文件名列表和其他配置来创建 tf.data.Dataset

  • batch_size — 指定数据批次大小的整数

列表 15.8 用于使用输入文件生成 tf.data.Dataset 的函数

from typing import List, Text                                ❶

def _input_fn(file_pattern: List[Text],                      ❷
              data_accessor: tfx.components.DataAccessor,    ❸
              tf_transform_output: tft.TFTransformOutput,    ❹
              batch_size: int = 200) -> tf.data.Dataset:     ❺

  return data_accessor.tf_dataset_factory(
      file_pattern,
      tfxio.TensorFlowDatasetOptions(
          batch_size=batch_size, label_key=_transformed_name(_LABEL_KEY)),
      tf_transform_output.transformed_metadata.schema)

❶ typing 库定义了函数输入的类型。

❷ 输入 tfrecord 文件的路径或模式的列表。它是 Text 类型对象(即字符串)的列表。

❸ DataAccessor 用于将输入转换为 RecordBatch

❹ 一个 TFTransformOutput

❺ 表示要合并为单个批次的返回数据集的连续元素的数量

您可以看到我们如何使用类型提示来标记参数以及返回对象。该函数通过调用 tf_dataset_factory() 函数获取 tf.data.Dataset,该函数使用文件路径列表和数据集选项(如批量大小和标签键)进行调用。标签键对于 data_accessor 来说非常重要,因为它能确定输入字段和目标。您可以看到 data_accessor 也需要从 Transform 步骤获取模式。这有助于 data_accessor 将原始示例转换为特征,然后分离输入和标签。在解释了所有关键函数之后,我们现在继续看看所有这些将如何被编排以进行模型训练。

15.2.2 定义模型训练

现在我们需要做的主要任务是模型的实际训练。负责模型训练的 TFX 组件(称为 Trainer)期望有一个名为 run_fn() 的特殊函数,该函数将告诉模型应该如何被训练和最终保存(见清单 15.9)。这个函数接受一个特殊类型的对象 called FnArgs,这是 TensorFlow 中的一个实用对象,可以用来声明需要传递给模型训练函数的与模型训练相关的用户定义参数。

清单 15.9 运行 Keras 模型训练与数据。

def run_fn(fn_args: tfx.components.FnArgs):                        ❶

  absl.logging.info("="*50)
  absl.logging.info("Printing the tfx.components.FnArgs object")   ❷
  absl.logging.info(fn_args)                                       ❷
  absl.logging.info("="*50)

  tf_transform_output = tft.TFTransformOutput(
    fn_args.transform_graph_path
  )                                                                ❸

  train_dataset = _input_fn(
    fn_args.train_files, fn_args.data_accessor, tf_transform_output, 
➥ 40                                                              ❹
  )
  eval_dataset = _input_fn(
    fn_args.eval_files, fn_args.data_accessor, tf_transform_output, 
➥ 40                                                              ❹
  )
  model = _build_keras_model()                                     ❺

  csv_write_dir = os.path.join(
    fn_args.model_run_dir,'model_performance'
)                                                                  ❻
  os.makedirs(csv_write_dir, exist_ok=True)

  csv_callback = tf.keras.callbacks.CSVLogger(
    os.path.join(csv_write_dir, 'performance.csv'), append=False   ❼
  )

  model.fit(                                                       ❽
      train_dataset,
      steps_per_epoch=fn_args.train_steps,
      validation_data=eval_dataset,
      validation_steps=fn_args.eval_steps,
      epochs=10,
      callbacks=[csv_callback]
  )

  signatures = {                                                   ❾
      'serving_default':
          _get_serve_tf_examples_fn(
              model, tf_transform_output
          ).get_concrete_function(
              tf.TensorSpec(
                  shape=[None],
                  dtype=tf.string,
                  name='examples'
              )
          ),

  }
  model.save(fn_args.serving_model_dir, save_format='tf', signatures=signatures)                                        ❿

❶ 定义一个名为 run_fn 的函数,该函数以 tfx.components.FnArgs 对象作为输入。

❷ 记录 fn_args 对象中的值。

❸ 加载 tensorflow_transform 图。

❹ 使用函数 _input_fn(即将讨论)将 CSV 文件中的数据转换为 tf.data.Dataset 对象。

❺ 使用先前定义的函数构建 Keras 模型。

❻ 定义一个目录来存储 Keras 回调 CSVLogger 生成的 CSV 日志。

❼ 定义 CSVLogger 回调。

❽ 使用创建的数据集和 fn_args 对象中存在的超参数来拟合模型。

❾ 为模型定义签名。签名告诉模型在模型部署时通过 API 调用时该做什么。

❿ 将模型保存到磁盘。

让我们首先检查 run_fn()的方法签名。run_fn()接受一个 FnArgs 类型的单一参数作为输入。如前所述,FnArgs 是一个实用对象,它存储了对模型训练有用的键值对集合。这个对象中的大部分元素是由 TFX 组件本身填充的。不过,你也有灵活性传递一些值。我们将定义这个对象中一些最重要的属性。但是一旦我们看到 TFX Trainer 组件生成的完整输出,我们将学习更多关于这个对象的属性列表。表 15.2 为你提供了这个对象中存储的内容的概览。如果你对这些元素的用途不是很理解,不要担心。随着我们的学习,它们会变得更清晰。一旦我们运行 Trainer 组件,它将显示用于每一个属性的值,因为我们在其中包含了记录语句来记录 fn_args 对象。这将帮助我们对当前运行的示例将这些属性进行上下文化,并更清晰地理解它们。

表 15.2 fn_args 类型对象中存储的属性概览

属性描述示例
train_files训练文件名列表['.../Transform/transformed_examples/16/Split-train/*'],
eval_files评估/验证文件名列表['.../Transform/transformed_examples/16/Split-eval/*']
train_steps训练步数100
eval_steps评估/验证步数100
schema_pathTFX 组件 SchemaGen 生成的模式路径'.../SchemaGen/schema/15/schema.pbtxt'
transform_graph_pathTFX 组件 Transform 生成的转换图路径'.../SchemaGen/schema/15/schema.pbtxt'
serve_model_dir存储可提供服务的模型的输出目录'.../Trainer/model/17/Format-Serving'
model_run_dir存储模型的输出目录'.../Trainer/model_run/17'

这个函数完成的第一个重要任务是为训练和评估数据生成 tf.data.Dataset 对象。我们定义了一个特殊的函数叫做 _input_fn()来实现这个功能(见 15.8 节)。

定义了数据集之后,我们使用之前讨论过的 _build_keras_model() 函数定义 Keras 模型。然后我们定义了一个 CSVLogger 回调函数来记录性能指标随时间的变化,就像我们之前做的那样。简要回顾一下,tf.keras.callbacks.CSVLogger 会在模型编译期间创建一个 CSV 文件,记录每个周期的所有损失和指标。我们将使用 fn_arg 对象的 model_run_dir 属性来为 CSV 文件创建一个路径,该路径位于模型创建目录内。这样,如果我们运行多个训练试验,每个试验都将与模型一起保存其自己的 CSV 文件。之后,我们像之前无数次那样调用 model.fit() 函数。我们使用的参数很简单,所以我们不会详细讨论它们,也不会不必要地延长这个讨论。

15.2.3 SignatureDefs:定义模型在 TensorFlow 外部的使用方式

一旦模型训练完成,我们必须将模型存储在磁盘上,以便以后可以重用。存储此模型的目的是通过基于 Web 的 API(即 REST API)来查询模型使用输入并获取预测结果。这通常是在在线环境中为客户提供服务的机器学习模型的使用方式。为了让模型理解基于 Web 的请求,我们需要定义称为 SignatureDefs 的东西。签名定义了模型的输入或目标是什么样子的(例如,数据类型)。您可以看到我们定义了一个叫做 signatures 的字典,并将其作为参数传递给 model.save()(清单 15.9)。

signatures 字典应该有键值对,其中键是签名名称,值是使用 @tf.function 装饰器装饰的函数。如果您想快速回顾一下此装饰器的作用,请阅读下面的侧边栏。

@tf.function 装饰器

@tf.function 装饰器接受一个执行各种 TensorFlow 操作的函数,该函数使用 TensorFlow 操作数,然后跟踪所有步骤并将其转换为数据流图。在大多数情况下,TensorFlow 需要显示输入和输出如何在操作之间连接的数据流图。尽管在 TensorFlow 1.x 中,您必须显式构建此图,但 TensorFlow 2.x 以后不再让开发人员负责此责任。每当一个函数被 @tf.function 装饰器装饰时,它会为我们构建数据流图。

还要注意,您不能将任意名称用作签名名称。TensorFlow 有一组根据您的需求定义的签名名称。这些在 TensorFlow 的特殊常量模块中定义(mng.bz/o2Kd)。有四种签名可供选择:

  • PREDICT_METHOD_NAME(值:'tensorflow/serving/predict')—这个签名用于预测传入输入的目标。这不期望目标存在。

  • REGRESS_METHOD_NAME(值为 'tensorflow/serving/regress')——此签名可用于从示例进行回归。它期望 HTTP 请求体中同时存在输入和输出(即目标值)。

  • CLASSIFY_METHOD_NAME(值为 'tensorflow/serving/classify')——与 REGRESS_METHOD_NAME 类似,但用于分类。此签名可用于分类示例。它期望 HTTP 请求中同时存在输入和输出(即目标值)。

  • DEFAULT_SERVING_SIGNATURE_DEF_KEY(值为 'serving_default')——这是默认签名名称。模型至少应该有默认的服务签名才能通过 API 使用。如果没有定义其他签名,则请求将经过此签名。

我们只定义了默认签名。签名采用 TensorFlow 函数(即用 @tf.function 装饰的函数)作为值。因此,我们需要定义一个函数(我们将其称为 _get_serve_tf_examples_fn() ),以告诉 TensorFlow 对输入做什么(请参见下一个清单)。

清单 15.10 解析通过 API 请求发送的示例并从中进行预测。

def _get_serve_tf_examples_fn(model, tf_transform_output):            ❶

  model.tft_layer = tf_transform_output.transform_features_layer()    ❷

  @tf.function
  def serve_tf_examples_fn(serialized_tf_examples):                   ❸
    """Returns the output to be used in the serving signature."""
    feature_spec = tf_transform_output.raw_feature_spec()             ❹
    feature_spec.pop(_LABEL_KEY)                                      ❺
    parsed_features = tf.io.parse_example(serialized_tf_examples, 
➥ feature_spec)                                                      ❻
    transformed_features = model.tft_layer(parsed_features)           ❼
    return model(transformed_features)                                ❽

  return serve_tf_examples_fn                                         ❾

❶ 返回一个函数,该函数解析序列化的 tf.Example 并应用特征转换。

❷ 以 Keras 层的形式获取要执行的特征转换。

❸ 被 @tf.function 装饰的函数将被返回。

❹ 获取原始列规范。

❺ 删除标签的特征规范,因为我们不需要在预测中使用它。

❻ 使用特征规范解析序列化示例。

❼ 使用定义的层将原始列转换为特征。

❽ 在提供转换后的特征之后返回模型的输出。

❾ 返回 TensorFlow 函数。

首先要注意的一件重要事情是,_get_serve_tf_examples_fn() 返回一个函数(即 serve_tf_examples_fn ),它是 TensorFlow 函数。_get_serve_tf_examples_fn() 接受两个输入:

  • Model — 我们在训练时建立的 Keras 模型。

  • tf_transform_output——将原始数据转换为特征的转换图。

返回函数应指示 TensorFlow 在模型部署后通过 API 请求传入的数据要执行什么操作。返回的函数以序列化示例作为输入,将它们解析为符合模型输入规范的正确格式,生成并返回输出。我们不会深入解析此功能的输入和输出,因为我们不会直接调用它,而是访问 TFX,在 API 调用时将访问它。

在这个过程中,函数首先获得原始特征规范映射,这是一个列名映射到 Feature 类型的字典。Feature 类型描述了放入特征中的数据类型。例如,对于我们的数据,特征规范将是这样的:

{
  'DC': VarLenFeature(dtype=tf.float32), 
  'DMC': VarLenFeature(dtype=tf.float32),
  'RH': VarLenFeature(dtype=tf.int64), 
  ...
  'X': VarLenFeature(dtype=tf.int64), 
  'area': VarLenFeature(dtype=tf.float32), 
  'day': VarLenFeature(dtype=tf.string), 
  'month': VarLenFeature(dtype=tf.string)
}

可以观察到,根据该列中的数据使用了不同的数据类型(例如 float、int、string)。您可以在 www.tensorflow.org/api_docs/python/tf/io/ 上看到一列特征类型的列表。接下来,我们删除了具有 _LABEL_KEY 的特征,因为它不应该是输入的一部分。然后我们使用 tf.io.parse_example() 函数通过传递特征规范映射来解析序列化的示例。结果被传递给 TransformFeaturesLayer (mng.bz/nNRa),它知道如何将一组解析后的示例转换为一批输入,其中每个输入具有多个特征。最后,转换后的特征被传递给模型,该模型返回最终输出(即,预测的森林烧毁面积)。让我们重新审视列表 15.9 中的签名定义:

signatures = {
      'serving_default':
          _get_serve_tf_examples_fn(
              model, tf_transform_output
          ).get_concrete_function(
              tf.TensorSpec(
                  shape=[None],
                  dtype=tf.string,
                  name='examples'
              )
          ),    
  }

您可以看到,我们并不只是简单地传递 _get_serve_tf_examples_fn() 的返回 TensorFlow 函数。相反,我们在返回函数(即 TensorFlow 函数)上调用了 get_concrete_function()。如果您还记得我们之前的讨论,当您执行带有 @tf.function 装饰的函数时,它会执行两件事:

  • 追踪函数并创建数据流图以执行函数的工作

  • 执行图以返回输出

get_concrete_function() 只做第一个任务。换句话说,它返回了追踪的函数。您可以在 mng.bz/v6K7 上阅读更多相关内容。

使用 TFX Trainer 训练 Keras 模型 15.2.4

我们现在有了训练模型的所有必要条件。再次强调,我们首先定义了一个 Keras 模型,定义了一个运行模型训练的函数,最后定义了指令,告诉模型当通过 API 发送 HTTP 请求时应该如何行事。现在我们将在 TFX 流水线的一部分中训练模型。为了训练模型,我们将使用 TFX Trainer 组件:

from tfx.components import Trainer
from tfx.proto import trainer_pb2
import tensorflow.keras.backend as K

K.clear_session()

n_dataset_size = df.shape[0]
batch_size = 40

n_train_steps_mod = 2*n_dataset_size % (3*batch_size)
n_train_steps = int(2*n_dataset_size/(3*batch_size))
if n_train_steps_mod != 0:
    n_train_steps += 1

n_eval_steps_mod = n_dataset_size % (3*batch_size)
n_eval_steps = int(n_dataset_size/(3*batch_size))
if n_eval_steps != 0:
    n_eval_steps += 1

trainer = Trainer(
    module_file=os.path.abspath("forest_fires_trainer.py"),
    transformed_examples=transform.outputs['transformed_examples'],
    schema=schema_gen.outputs['schema'],
    transform_graph=transform.outputs['transform_graph'],
    train_args=trainer_pb2.TrainArgs(num_steps=n_train_steps),
    eval_args=trainer_pb2.EvalArgs(num_steps=n_eval_steps))

context.run(trainer)

在 Trainer 组件之前的代码只是计算了一个周期中所需的正确迭代次数。为了计算这个值,我们首先得到了数据的总大小(记住我们将数据集存储在 DataFrame df 中)。然后我们为训练使用了两个哈希桶,评估使用了一个哈希桶。因此,我们大约有三分之二的训练数据和三分之一的评估数据。最后,如果值不能完全被整除,我们就会加上 +1 来包含数据的余数。

让我们更详细地研究 Trainer 组件的实例化。有几个重要的参数需要传递给构造函数:

  • module_file——包含 run_fn() 的 Python 模块的路径。

  • transformed_examples——TFX Transform 步骤的输出,特别是转换后的示例。

  • schema——TFX SchemaGen 步骤的输出。

  • train_args——指定与训练相关的参数的 TrainArgs 对象。(要查看为该对象定义的 proto 消息,请参见 mng.bz/44aw。)

  • eval_args—一个指定评估相关参数的 EvalArgs 对象。(要查看为此对象定义的 proto 消息,请参见mng.bz/44aw。)

这将输出以下日志。由于日志输出的长度,我们已经截断了某些日志消息的部分:

INFO:absl:Generating ephemeral wheel package for'/home/thushv89/code/manning_tf2_in_action/Ch15-TFX-for-MLOps-in-
➥ TF2/tfx/forest_fires_trainer.py' (including modules: 
➥ ['forest_fires_constants', 'forest_fires_transform', 
➥ 'forest_fires_trainer']).

...

INFO:absl:Training model.

...

43840.0703WARNING:tensorflow:11 out of the last 11 calls to <function 
➥ recreate_function.<locals>.restored_function_body at 0x7f53c000ea60> 
➥ triggered tf.function retracing. Tracing is expensive and the excessive 
➥ number of tracings could be due to (1) creating @tf.function repeatedly 
➥ in a loop, (2) passing tensors with different shapes, (3) passing 
➥ Python objects instead of tensors. 

INFO:absl:____________________________________________________________________________
INFO:absl:Layer (type)                    Output Shape         Param #    
➥ Connected to                     
INFO:absl:=================================================================
➥ ===========

...

INFO:absl:dense_features (DenseFeatures)  (None, 31)           0           
➥ DC_xf[0][0]                      
INFO:absl:                                                                
➥ DMC_xf[0][0]                     
INFO:absl:                                                               
➥ FFMC_xf[0][0]                    
...
INFO:absl:                                                               
➥ temp_xf[0][0]                    
INFO:absl:                                                               
➥ wind_xf[0][0]                    
INFO:absl:_________________________________________________________________
➥ ___________

...

INFO:absl:Total params: 12,417

...

Epoch 1/10
9/9 [==============================] - ETA: 3s - loss: 43840.070 - 1s 
➥ 32ms/step - loss: 13635.6658 - val_loss: 574.2498
Epoch 2/10
9/9 [==============================] - ETA: 0s - loss: 240.241 - 0s 
➥ 10ms/step - loss: 3909.4543 - val_loss: 495.7877
...
Epoch 9/10
9/9 [==============================] - ETA: 0s - loss: 42774.250 - 0s 
➥ 8ms/step - loss: 15405.1482 - val_loss: 481.4183
Epoch 10/10
9/9 [==============================] - 1s 104ms/step - loss: 1104.7073 - 
➥ val_loss: 456.1211
...

INFO:tensorflow:Assets written to: 
➥ /home/thushv89/code/manning_tf2_in_action/Ch15-TFX-for-MLOps-in-
➥ TF2/tfx/pipeline/examples/forest_fires_pipeline/Trainer/model/5/Format-
➥ Serving/assets
INFO:absl:Training complete. Model written to 
➥ /home/thushv89/code/manning_tf2_in_action/Ch15-TFX-for-MLOps-in-
➥ TF2/tfx/pipeline/examples/forest_fires_pipeline/Trainer/model/5/Format-
➥ Serving. ModelRun written to 
➥ /home/thushv89/code/manning_tf2_in_action/Ch15-TFX-for-MLOps-in-
➥ TF2/tfx/pipeline/examples/forest_fires_pipeline/Trainer/model_run/5
INFO:absl:Running publisher for Trainer
INFO:absl:MetadataStore with DB connection initialized

在日志消息中,我们可以看到 Trainer 做了大量的繁重工作。首先,它使用 forest_fires_trainer.py 中定义的模型训练代码创建一个 wheel 包。wheel(扩展名为 .whl)是 Python 打包库的方式。例如,当你执行 pip install tensorflow 时,它会首先下载带有最新版本的 wheel 包并在本地安装。如果你有一个本地下载的 wheel 包,你可以使用 pip install <wheel 的路径>。你可以在 <pipeline 根目录路径>/examples/forest_fires_pipeline/_wheels 目录中找到生成的 wheel 包。然后它打印模型摘要。它为传递给模型的每个特征都有一个输入层。你可以看到 DenseFeatures 层聚合了所有这些输入层,以生成一个 [None, 31] 大小的张量。作为最终输出,模型产生了一个 [None, 1] 大小的张量。然后进行模型训练。你会看到警告,比如

out of the last x calls to <function 
➥ recreate_function.<locals>.restored_function_body at 0x7f53c000ea60> 
➥ triggered tf.function retracing. Tracing is expensive and the excessive 
➥ number of tracings could be due to

当 TensorFlow 函数跟踪发生太多次时,就会出现这个警告。这可能是代码编写不佳的迹象(例如,模型在循环内部被重建多次),有时是不可避免的。在我们的案例中,是后者。Trainer 模块的行为导致了这种行为,我们对此无能为力。最后,组件将模型以及一些实用工具写入到管道根目录的一个文件夹中。到目前为止,我们的管道根目录看起来是这样的(图 15.8)。

15-08

图 15.8 运行 Trainer 后的完整目录/文件结构

在 Trainer 的输出日志中,我们可以注意到一个主要问题是训练和验证损失。对于这个问题,它们相当大。我们使用的是计算得出的均方误差。

15_08a

其中 N 是示例的数量,y[i] 是第 i 个示例,[i] 是第 i 个示例的预测值。在训练结束时,我们的平方损失约为 481,意味着每个示例约有 22 公顷(即 0.22 平方公里)的误差。这不是一个小错误。如果你调查这个问题,你会意识到这主要是由数据中存在的异常引起的。有些异常是如此之大,以至于它们可能会使模型严重偏离正确方向。我们将在本章的一个即将到来的部分中解决这个问题。你将能够看到传递给 run_fn() 的 FnArgs 对象中的值:

INFO:absl:==================================================
INFO:absl:Printing the tfx.components.FnArgs object
INFO:absl:FnArgs(
    working_dir=None, 
    train_files=['.../Transform/transformed_examples/16/Split-train/*'], 
    eval_files=['.../Transform/transformed_examples/16/Split-eval/*'], 
    train_steps=100, 
    eval_steps=100, 
    schema_path='.../SchemaGen/schema/15/schema.pbtxt', 
    schema_file='.../SchemaGen/schema/15/schema.pbtxt', 
    transform_graph_path='.../Transform/transform_graph/16', 
    transform_output='.../Transform/transform_graph/16', 
    data_accessor=DataAccessor(
        tf_dataset_factory=<function 
➥ get_tf_dataset_factory_from_artifact.<locals>.dataset_factory at 
➥ 0x7f7a56329a60>, 
        record_batch_factory=<function 
➥ get_record_batch_factory_from_artifact.<locals>.record_batch_factory at 
➥ 0x7f7a563297b8>, 
        data_view_decode_fn=None
    ), 
    serving_model_dir='.../Trainer/model/17/Format-Serving', 
    eval_model_dir='.../Trainer/model/17/Format-TFMA', 
    model_run_dir='.../Trainer/model_run/17', 
    base_model=None, 
    hyperparameters=None, 
    custom_config=None
)
INFO:absl:==================================================

以下侧边栏讨论了我们在本讨论中的这一点上如何评估模型。

评估保存的模型

在流水线中,我们的模型将以 URL 形式通过 HTTP 接口提供服务。但是与其等待不如手动加载模型并用它来预测数据。这样做将为我们提供两个优势:

  • 验证模型是否按预期工作

  • 提供对模型输入和输出格式的深入理解

我们不会在本书中详细介绍这个问题,以保持我们讨论的重点在流水线上。但是,已在 tfv/15.1_MLOps_with_tensorflow.ipynb 笔记本中提供了代码,因此您可以进行实验。

接下来,我们将讨论如何检测数据中存在的异常并将其移除,以创建一个干净的数据集来训练我们的模型。

检测和移除异常值

我们的模型目前显示的验证损失约为 568。这里使用的损失是均方误差。我们已经看到,这意味着每个预测偏差 24 公顷(即 0.24 平方公里)。这不是一个可以忽略的问题。我们的数据中有很多异常值,这可能是我们看到如此大的误差边界的一个关键原因。以下图显示了我们早期创建的统计图。

15-08-unnumb

由 StatisticsGen 组件为数据生成的摘要统计图

您可以看到一些列严重偏斜。例如,特征 FFMC 具有最高的密度,约为 80-90,但范围为 18.7-96.2。

为了解决这个问题,我们将使用 tensorflow_data_validation(缩写为 tfdv)库。它提供了有用的功能,如 tfdv.validate_statistics(),可用于根据我们之前生成的数据模式验证数据,以及 tfdv.display_anomalies()函数,以列出异常样本。此外,我们可以编辑模式以修改异常值的标准。例如,要更改允许的 ISI 特征的最大值,您可以执行以下操作:

isi_feature = tfdv.get_feature(schema, 'ISI')
isi_feature.float_domain.max = 30.0

最后,您可以使用 tfdv.visualize_statistics()函数可视化原始数据与清理后的数据。最后,您可以使用 TFX 流水线中的 ExampleValidator 对象(mng.bz/XZxv)确保数据集中没有异常。

运行此操作后,您应该比以前得到更小的损失。例如,在这个实验中,平均观察到了约 150 的损失。这是之前错误的 75%减少。您可以在 tfv/15.1_MLOps_with_tensorflow.ipynb 笔记本中找到此代码。

接下来,我们将看一看一种名为 Docker 的技术,该技术用于在隔离且便携的环境中部署模型。我们将看看如何将我们的模型部署在所谓的 Docker 容器中。

练习 2

而不是使用 one-hot 编码来表示日和月的特征,并将它们追加到 categorical_columns 变量中,让我们假设你想使用嵌入来表示这些特征。您可以使用特征列 tf.feature_column.embedding_column 来完成这个任务。假设嵌入的维度是 32。你有存储在 _VOCAB_FEATURE_KEYS 中的特征名称(包括['day', 'month'])以及存储在 _MAX_CATEGORICAL_FEATURE_VALUES 中的维度(包括[7, 12])。

15.3 设置 Docker 以提供经过训练的模型

您已经开发了一个数据管道和一个强大的模型,可以根据天气数据预测森林火灾的严重程度。现在,您希望更进一步,通过在一台机器上部署模型并通过 REST API 提供更易访问的服务,这个过程也称为生产化机器学习模型。为了做到这一点,您首先要创建一个专门用于模型服务的隔离环境。您将使用的技术是 Docker。

注意在继续之前,确保您的计算机上已经安装了 Docker。要安装 Docker,请按照此指南:docs.docker.com/engine/install/ubuntu/

在 TFX 中,你可以将你的模型部署为一个容器,而这个容器是由 Docker 提供的。根据官方 Docker 网站的说法,Docker 容器是

软件的标准单元,它打包了代码和所有的依赖项,以便应用程序可以在一个计算环境中快速、可靠地运行,并在另一个计算环境中运行。

源:www.docker.com/resources/what-container

Docker 是一种容器技术,它可以帮助您在主机上隔离运行软件(或微服务)。在 Docker 中,您可以创建一个镜像,该镜像将使用各种规格(例如操作系统、库、依赖项)指示 Docker 需要在容器中以正确运行软件。然后,容器就是该镜像的运行时实例。这意味着您可以在一个计算机上创建一个容器,并且可以轻松地在另一台计算机上运行它(只要两台计算机上都安装了 Docker)。虚拟机(VMs)也试图实现类似的目标。有许多资源可以比较和对比 Docker 容器和虚拟机(例如,mng.bz/yvNB)。

如我们所说,要运行一个 Docker 容器,首先需要一个 Docker 镜像。Docker 有一个公共镜像注册表(称为 Docker Hub),位于 hub.docker.com/。我们正在寻找的 Docker 镜像是 TensorFlow serving 镜像。这个镜像已经安装了一切用于提供 TensorFlow 模型的服务,使用了 TensorFlow serving (github.com/tensorflow/serving),这是 TensorFlow 中的一个子库,可以围绕给定的模型创建一个 REST API,以便你可以发送 HTTP 请求来使用模型。你可以通过运行以下命令来下载这个镜像:

docker pull tensorflow/serving:2.6.3-gpu

让我们解析一下这条命令的结构。docker pull 是下载镜像的命令。tensorflow/serving 是镜像名称。Docker 镜像是有版本控制的,意味着每个 Docker 镜像都有一个版本标签(如果你没有提供的话,默认为最新版本)。2.6.3-gpu 是镜像的版本。这个镜像相当大,因为它支持 GPU 执行。如果你没有 GPU,你可以使用 docker pull tensorflow/serving:2.6.3,这个版本更轻量级。一旦命令成功执行,你就可以运行

docker images 

列出你下载的所有镜像。有了下载的镜像,你可以使用 docker run 命令来使用给定的镜像启动一个容器。docker run 命令非常灵活,带有许多可以设置和更改的参数。我们使用了其中的几个:

docker run \
  --rm \
  -it \
  --gpus all \
  -p 8501:8501 \
  --user $(id -u):$(id -g) \
  -v ${PWD}/tfx/forest-fires-pushed:/models/forest_fires_model \
  -e MODEL_NAME=forest_fires_model \
  tensorflow/serving:2.6.3-gpu

理解这里提供的参数是很重要的。通常,在 shell 环境中定义参数时,使用单划线前缀来表示单字符的参数(例如,-p),使用双划线前缀来表示更详细的参数(例如,--gpus):

  • --rm—容器是临时运行时,可以在服务运行后移除。--rm 意味着容器将在退出后被移除。

  • -it(简写形式为 -i 和 -t)—这意味着你可以进入容器,并在容器内部交互式地运行命令。

  • --gpus all—这告诉容器确保 GPU 设备(如果存在)在容器内可见。

  • -p—这将容器中的网络端口映射到主机。如果你想将某些服务(例如,用于提供模型的 API)暴露给外部,这一点很重要。例如,TensorFlow serving 默认运行在 8501 端口上。因此,我们将容器的 8501 端口映射到主机的 8501 端口。

  • --user (idu):(id -u):(id -g)—这意味着命令将以与您在主机上登录的用户相同的用户身份运行。每个用户由用户 ID 标识,并分配给一个或多个组(由组 ID 标识)。您可以按照 --user <用户 ID>:<组 ID> 的语法传递用户和组。例如,您当前的用户 ID 可以通过命令 id -u 给出,而组则由 id -g 给出。默认情况下,容器以 root 用户(即通过 sudo 运行)运行命令,这可能会使您的服务更容易受到外部攻击。因此,我们使用较低特权的用户在容器中执行命令。

  • -v—这将一个目录挂载到容器内的位置。默认情况下,您在容器内存储的东西对外部是不可见的。这是因为容器有自己的存储空间/卷。如果您需要使容器看到主机上的某些内容,或者反之亦然,则需要将主机上的目录挂载到容器内的路径上。这被称为绑定挂载。例如,在这里,我们将我们推送的模型(将位于 ./tfx/forest-fires-pushed)暴露到容器内部路径 /models/forest_fires_model。

  • -e—此选项可用于将特殊环境变量传递给容器。例如,TensorFlow 服务服务期望一个模型名称(它将成为从模型获取结果所需命中的 URL 的一部分)。

此命令在 Ch15-TFX-for-MLOps-in-TF2 目录中的 tfx/run_server.sh 脚本中为您提供。让我们运行 run_server.sh 脚本,看看我们将得到什么。要运行脚本

  1. 打开一个终端。

  2. 将 cd 移动到 Ch15-TFX-for-MLOps-in-TF2/tfx 目录中。

  3. 运行 ./run_server.sh。

它将显示类似于以下的输出:

2.6.3-gpu: Pulling from tensorflow/serving
Digest: 
➥ sha256:e55c44c088f6b3896a8f66d8736f38b56a8c5687c105af65a58f2bfb0bf90812
Status: Image is up to date for tensorflow/serving:2.6.3-gpu
docker.io/tensorflow/serving:2.6.3-gpu
2021-07-16 05:59:37.786770: I
tensorflow_serving/model_servers/server.cc:88] Building single TensorFlow 
➥ model file config: model_name: forest_fires_model model_base_path: 
➥ /models/forest_fires_model
2021-07-16 05:59:37.786895: I
tensorflow_serving/model_servers/server_core.cc:464] Adding/updating 
➥ models.
2021-07-16 05:59:37.786915: I
tensorflow_serving/model_servers/server_core.cc:587]  (Re-)adding model: 
➥ forest_fires_model
2021-07-16 05:59:37.787498: W
tensorflow_serving/sources/storage_path/file_system_storage_path_source.cc:
➥ 267] No versions of servable forest_fires_model found under base path 
➥ /models/forest_fires_model. Did you forget to name your leaf directory 
➥ as a number (eg. '/1/')?
...

当然,这并不能完全奏效,因为我们提供的目录作为模型位置尚未被填充。我们仍然需要做一些事情,以便将最终模型放置在正确的位置。

在下一节中,我们将完成我们流水线的其余部分。我们将看到如何在流水线中自动评估新训练的模型,如果性能良好,则部署模型,并使用 REST API(即基于 Web 的 API)从模型进行预测。

练习 3

假设您想要下载 TensorFlow Docker 映像(其名称为 tensorflow/tensorflow),版本为 2.5.0,并启动一个容器,将您计算机上的 /tmp/inputs 目录挂载到容器内的 /data 卷中。此外,您希望将容器中的 5000 端口映射到计算机上的 5000 端口。您如何使用 Docker 命令执行此操作?您可以假设您在容器内以 root 用户身份运行命令。

15.4 部署模型并通过 API 进行服务

现在,你已经有了一个数据管道、训练好的模型以及一个可以运行包含了运行模型和访问模型的 API 所需的一切的脚本。现在,使用 TFX 提供的一些服务,你将在 Docker 容器中部署模型,并通过 API 进行访问。在这个过程中,你将运行一些步骤来验证基础结构(例如,容器是否可运行且健康)和模型(即在模型的新版本发布时,检查它是否比上一个版本更好),最后,如果一切都好,将模型部署到基础结构上。

这是一个漫长的旅程。让我们回顾一下我们到目前为止取得的成就。我们已经使用了以下 TFX 组件:

  • CsvExampleGen—从 CSV 文件中以 TFRecord 对象的形式加载数据。

  • StatisticsGen—关于 CSV 数据中各列分布的基本统计数据和可视化。

  • SchemaGen—生成数据的模式/模板(例如数据类型、域、允许的最小/最大值等)。

  • Transform—使用 tensorflow_transform 库中提供的操作(例如,独热编码、桶化)将原始列转换为特征。

  • Trainer—定义一个 Keras 模型,使用转换后的数据进行训练,并保存到磁盘。此模型具有一个名为 serving default 的签名,指示模型对于传入的请求应该执行什么操作。

  • ExampleValidator—用于验证训练和评估示例是否符合定义的模式,并可用于检测异常。

15.4.1 验证基础结构

使用 TFX,当你拥有一个完全自动化的管道时,几乎可以确保一切工作正常。我们将在这里讨论一个这样的步骤:基础结构验证步骤。在这个步骤中,tfx.components.InfraValidator 将自动进行

  • 使用提供的特定版本的 TensorFlow serving 镜像创建一个容器

  • 加载并在其中运行模型

  • 发送多个请求以确保模型能够响应

  • 关闭容器

让我们看一下如何使用这个组件来验证我们在前一节中设置的本地 Docker 配置(请参阅下一个清单)。

Listing 15.11 定义 InfraValidator

from tfx.components import InfraValidator
from tfx.proto import infra_validator_pb2

infra_validator = InfraValidator(
    model=trainer.outputs['model'],                                        ❶

    examples=example_gen.outputs['examples'],                              ❷
    serving_spec=infra_validator_pb2.ServingSpec(                          ❸
        tensorflow_serving=infra_validator_pb2.TensorFlowServing(          ❹
            tags=['2.6.3-gpu']
        ),
        local_docker=infra_validator_pb2.LocalDockerConfig(),              ❺
    ),
    request_spec=infra_validator_pb2.RequestSpec(                          ❻
        tensorflow_serving=infra_validator_pb2.TensorFlowServingRequestSpec(❼
            signature_names=['serving_default']
        ),
        num_examples=5                                                     ❽
    )
)

context.run(infra_validator)

❶ InfraValidator 需要验证的模型的位置。

❷ 用于构建对模型的 API 调用的数据来源

❸ 包含一组与对模型进行的具体调用相关的规范

❹ 定义要使用的 TensorFlow serving Docker Image 的版本/标签

❺ 告诉 InfraValidator 我们将使用本地的 Docker 服务进行测试

❻ 包含与对模型进行的特定调用相关的规范集合

❼ 定义要使用的模型签名

❽ 定义了向模型发送的请求数量

InfraValidator 和其他任何 TFX 组件一样,需要准确地提供多个参数才能运行。

  • model—由 Trainer 组件返回的 Keras 模型。

  • examples—由 CSVExampleGen 给出的原始示例。

  • serving_spec—期望一个 ServingSpec protobuf 消息。它将指定 TensorFlow serving Docker 镜像的版本以及是否使用本地 Docker 安装(这里已完成)。

  • request_spec—一个 RequestSpec protobuf 消息,它将指定需要达到的签名以验证模型是否正常工作。

如果此步骤无误完成,您将在管道根目录中看到图 15.9 中显示的文件。

15-09

图 15.9 运行 InfraValidator 后的目录/文件结构

您可以看到一个名为 INFRA_BLESSED 的文件出现在 InfraValidator 子目录中。这引出了“祝福”的概念。TFX 将在成功运行流水线中祝福某些元素。一旦被祝福,它将创建一个带有后缀 BLESSED 的文件。如果该步骤失败,那么将创建一个带有后缀 NOT_BLESSED 的文件。祝福有助于区分运行正常和运行失败的事物。例如,一旦被祝福,我们可以确信基础设施按预期工作。这意味着像

  • 架设一个容器

  • 加载模型

  • 达到定义的 API 端点

可以无问题地执行。

15.4.2 解析正确的模型

接下来,我们将定义一个解析器。解析器的目的是使用明确定义的策略(例如,使用最低验证错误的模型)解决随时间演变的特殊工件(如模型)。然后,解析器通知后续组件(例如,我们接下来将定义的模型评估器组件)要使用哪个工件版本。正如您可能已经猜到的那样,我们将使用解析器来解析管道中的经过训练的 Keras 模型。因此,如果您多次运行管道,则解析器将确保在下游组件中使用最新和最优秀的模型:

from tfx import v1 as tfx

model_resolver = tfx.dsl.Resolver(
      strategy_class=tfx.dsl.experimental.LatestBlessedModelStrategy,
      model=tfx.dsl.Channel(type=tfx.types.standard_artifacts.Model),
      model_blessing=tfx.dsl.Channel(
          type=tfx.types.standard_artifacts.ModelBlessing
      )
).with_id('latest_blessed_model_resolver')

context.run(model_resolver)

在定义验证模型的解析器时,我们将定义三件事:

  • strategy_class(来自 tfx.dsl.components.common.resolver.ResolverStrategy 命名空间的类)—定义解析策略。当前支持两种策略:最新的祝福模型(即通过一组定义的评估检查的模型)和最新的模型。

  • 模型(tfx.dsl.Channel)—将 TFX 工件类型的模型包装在一个 tfx.dsl.Channel 对象中。tfx.dsl.Channel 是一个 TFX 特定的抽象概念,连接数据消费者和数据生产者。例如,在管道中选择正确的模型时就需要一个通道,以从可用模型池中选择。

  • model_blessing(tfx.dsl.Channel)—将类型为 ModelBlessing 的 TFX 工件包装在 tfx.dsl.Channel 对象中。

你可以查看各种工件,将其包装在一个 tf.dsl.Channel 对象中,网址为mng.bz/2nQX

15.4.3 评估模型

我们将在将模型推送到指定的生产环境之前的最后一步对模型进行评估。基本上,我们将定义几个模型需要通过的评估检查。当模型通过时,TFX 将对模型进行认可。否则,TFX 将使模型保持未认可状态。我们将在后面学习如何检查模型是否被认可。为了定义评估检查,我们将使用 tensorflow_model_analysis 库。第一步是定义一个评估配置,其中指定了检查项:

import tensorflow_model_analysis as tfma

eval_config = tfma.EvalConfig(
    model_specs=[
        tfma.ModelSpec(label_key='area')                                  ❶
    ],
    metrics_specs=[
        tfma.MetricsSpec(
            metrics=[                                                     ❷
                tfma.MetricConfig(class_name='ExampleCount'),             ❸
                tfma.MetricConfig(
                    class_name='MeanSquaredError',                        ❹
                    threshold=tfma.MetricThreshold(                       ❺
                       value_threshold=tfma.GenericValueThreshold(
                           upper_bound={'value': 200.0}
                       ),
                       change_threshold=tfma.GenericChangeThreshold(      ❻
                           direction=tfma.MetricDirection.LOWER_IS_BETTER,
                           absolute={'value': 1e-10}
                       )
                   )
               )
           ]
        )
    ],
    slicing_specs=[                                                       ❼
        tfma.SlicingSpec(),                                               ❽
        tfma.SlicingSpec(feature_keys=['month'])                          ❾
    ])

❶ 定义一个包含标签特征名称的模型规范。

❷ 定义一个指标规范列表。

❸ 获取评估的示例数。

❹ 将均方误差定义为一项指标。

❺ 将阈值上限定义为一个检查。

❻ 将误差变化(与先前模型相比)定义为一个检查(即,误差越低越好)。

❼ 切片规范定义了在评估时数据需要如何分区。

❽ 在整个数据集上进行评估,不进行切片(即,空切片)。

❾ 在分区数据上进行评估,其中数据根据月份字段进行分区。

EvalConfig 相当复杂。让我们慢慢来。我们必须定义三件事:模型规范(作为 ModelSpec 对象)、指标规范(作为 MetricsSpec 对象列表)和切片规范(作为 SlicingSpec 对象列表)。ModelSpec 对象可用于定义以下内容:

  • name—可用于在此步骤中标识模型的别名模型名称。

  • model_type—标识模型类型的字符串。允许的值包括 tf_keras、tf_estimator、tf_lite 和 tf_js、tf_generic。对于像我们的 Keras 模型,类型会自动推导。

  • signature_name—用于推断的模型签名。默认情况下使用 serving_default。

  • label_key—示例中标签特征的名称。

  • label_keys—对于多输出模型,使用标签键列表。

  • example_weight_key—如果存在,则用于检索示例权重的可选键(或特征名称)。

有关 ModelSpec 对象的更多信息,请参阅mng.bz/M5wW。在 MetricsSpec 对象中,可以设置以下属性:

  • metrics—MetricConfig 对象的列表。每个 MetricConfig 对象将类名作为输入。您可以选择在 tfma.metrics.Metric(mng.bz/aJ97)或 tf.keras.metrics.Metric(mng.bz/gwmV)命名空间中定义的任何类。

SlicingSpec 定义了评估期间数据需要如何进行分区。例如,对于时间序列问题,您需要查看模型在不同月份或天数上的表现。为此,SlicingSpec 是一个方便的配置。SlicingSpec 具有以下参数:

  • feature_keys—可用于定义一个特征键,以便您可以根据其对数据进行分区。例如,对于特征键月份,它将通过选择具有特定月份的数据来为每个月份创建一个数据分区。如果未传递,它将返回整个数据集。

注意,如果没有提供,TFX 将使用您在管道最开始(即实施 CsvExampleGen 组件时)定义的评估集合。换句话说,所有指标都在数据集的评估集合上进行评估。接下来,它定义了两个评估通过的条件:

  • 均方误差小于 200。

  • 均方损失改善了 1e - 10。

如果对于一个新训练的模型满足下列两个条件,那么该模型将被标记为“通过”(即通过了测试)。

最后,我们定义了评估器(mng.bz/e7BQ),它将接收一个模型并运行在 eval_config 中定义的评估检查。您可以通过为 examples、model、baseline_model 和 eval_config 参数传入值来定义一个 TFX 评估器。baseline_model 是由 Resolver 解析的:

from tfx.components import Evaluator

evaluator = Evaluator(
    examples=example_gen.outputs['examples'],
    model=trainer.outputs['model'],
    baseline_model=model_resolver.outputs['model'],
    eval_config=eval_config)
context.run(evaluator)

不幸的是,运行评估器不会提供您所需的结果。事实上,它会导致评估失败。在日志的底部,您将看到如下输出:

INFO:absl:Evaluation complete. Results written to 
➥ .../pipeline/examples/forest_fires_pipeline/Evaluator/evaluation/14.
INFO:absl:Checking validation results.
INFO:absl:Blessing result False written to 
➥ .../pipeline/examples/forest_fires_pipeline/Evaluator/blessing/14.

上面的输出表明,Blessing 结果为 False。鉴于它只显示了约为 150 的损失,而我们将阈值设为 200,为什么模型失败仍然是一个谜。要了解发生了什么,我们需要查看写入磁盘的结果。如果您在/ examples\forest_fires_pipeline\Evaluator<execution ID>目录中查看,您会看到像 validation、metrics 等文件。使用 tensorflow_model_analysis 库,这些文件可以提供宝贵的见解,帮助我们理解出了什么问题。tensorflow_model_analysis 库提供了几个方便的函数来加载存储在这些文件中的结果:

import tensorflow_model_analysis as tfma

validation_path = os.path.join(
    evaluator.outputs['evaluation']._artifacts[0].uri, "validations"
)
validation_res = tfma.load_validation_result(validation_path)

print('='*20, " Output stored in validations file ", '='*20)
print(validation_res)
print("="*75)

运行结果为:

metric_validations_per_slice {
  slice_key {
    single_slice_keys {
      column: "month"
      bytes_value: "sep"
    }
  }
  failures {
    metric_key {
      name: "mean_squared_error"
    }
    metric_threshold {
      value_threshold {
        upper_bound {
          value: 200.0
        }
      }
    }
    metric_value {
      double_value {
        value: 269.11712646484375
      }
    }
  }
}
validation_details {
  slicing_details {
    slicing_spec {
    }
    num_matching_slices: 12
  }
}

您可以清楚地看到发生了什么。它指出,为月份"sep"创建的切片导致了 269 的错误,这就是为什么我们的评估失败了。如果您想要关于所有使用的切片及其结果的详细信息,您可以检查指标文件:

metrics_path = os.path.join(
    evaluator.outputs['evaluation']._artifacts[0].uri, "metrics"
)
metrics_res = tfma.load_metrics(metrics_path)

print('='*20, " Output stored in metrics file ", '='*20)
for r in metrics_res:
    print(r)
    print('-'*75)
print("="*75)

运行结果为以下内容。为了节省空间,这里只显示了完整输出的一小部分:

slice_key {
  single_slice_keys {
    column: "month"
    bytes_value: "sep"
  }
}
metric_keys_and_values {
  key {
    name: "loss"
  }
  value {
    double_value {
      value: 269.11712646484375
    }
  }
}
metric_keys_and_values {
  key {
    name: "mean_squared_error"
  }
  value {
    double_value {
      value: 269.11712646484375
    }
  }
}
metric_keys_and_values {
  key {
    name: "example_count"
  }
  value {
    double_value {
      value: 52.0
    }
  }
}

---------------------------------------------------------------------------
slice_key {
}
metric_keys_and_values {
  key {
    name: "loss"
  }
  value {
    double_value {
      value: 160.19691467285156
    }
  }
}
metric_keys_and_values {
  key {
    name: "mean_squared_error"
  }
  value {
    double_value {
      value: 160.19691467285156
    }
  }
}
metric_keys_and_values {
  key {
    name: "example_count"
  }
  value {
    double_value {
      value: 153.0
    }
  }
}
...

这个输出让我们对发生了什么有了更多了解。由于我们将示例计数视为指标之一,我们可以看到每个切片中的示例数。例如,在五月份,评估集合中只有一个示例,这很可能是一个异常值。为了解决这个问题,我们将阈值提高到 300。一旦您这样做了,需要重新运行评估器,从评估器的日志中可以看到我们的模型已通过检查:

INFO:absl:Evaluation complete. Results written to 
➥ .../pipeline/examples/forest_fires_pipeline/Evaluator/evaluation/15.
INFO:absl:Checking validation results.
INFO:absl:Blessing result True written to 
➥ .../pipeline/examples/forest_fires_pipeline/Evaluator/blessing/15.

解决这个问题的最佳方法是确定为什么“sep” 月份给出的值如此之大,而其他月份与整体损失值持平或低于。确定问题后,我们应该确定纠正措施以更正此问题(例如,重新考虑异常值定义)。在此之后,我们将继续进行管道的下一部分。

15.4.4 推送最终模型

我们已经达到管道中的最后步骤。我们需要定义一个推送器。 推送器(mng.bz/pOZz)负责将经过评估检查的认可模型(即通过的模型)推送到定义好的生产环境。 生产环境可以简单地是您文件系统中的本地位置:

from tfx.components import Pusher
from tfx.proto import pusher_pb2

pusher = Pusher(
  model=trainer.outputs['model'],
  model_blessing=evaluator.outputs['blessing'],
  infra_blessing=infra_validator.outputs['blessing'],
  push_destination=pusher_pb2.PushDestination(
    filesystem=pusher_pb2.PushDestination.Filesystem(
        base_directory=os.path.join('forestfires-model-pushed'))
  )
)
context.run(pusher)

推送器接受以下元素作为参数:

  • model—由 Trainer 组件返回的 Keras 模型

  • model_blessing—Evaluator 组件的认可状态

  • infra_blessing—InfraValidator 的认可状态

  • push_destination—作为 PushDestination protobuf 消息推送的目标

如果这一步运行成功,您将在我们的管道根目录中的称为 forestfires-model-pushed 的目录中保存模型。

15.4.5 使用 TensorFlow serving API 进行预测

最后一步是从推送目的地检索模型,并基于我们下载的 TensorFlow 服务镜像启动 Docker 容器。 Docker 容器将提供一个 API,我们可以通过各种请求进行 ping 。

让我们更详细地看一下如何将 API 融入整体架构中(图 15.10)。 机器学习模型位于 API 的后面。 API 定义了各种 HTTP 端点,您可以通过 Python 或类似 curl 的包来 ping 这些端点。 这些端点将以 URL 的形式提供,并且可以在 URL 中期望参数或将数据嵌入请求体中。 API 通过服务器提供。 服务器公开了一个网络端口,客户端可以与服务器进行通信。 客户端可以使用格式<主机名>:<端口>/<端点>向服务器发送请求。 我们将更详细地讨论请求实际的样子。

15-10

图 15.10 模型如何与 API、TensorFlow 服务器和客户端交互

要启动容器,只需

  1. 打开一个终端

  2. 将 cd 移入 Ch15-TFX-for-MLOps-in-TF2/tfx 目录

  3. 运行 ./run_server.sh

接下来,在 Jupyter 笔记本中,我们将发送一个 HTTP POST 请求。 HTTP 请求有两种主要类型:GET 和 POST。 如果您对差异感兴趣,请参考侧边栏。 HTTP POST 请求是一个包含了可以请求的 URL 和头信息的请求,也包含了负载,这对 API 完成请求是必要的。 例如,如果我们正在击中与 serving_default 签名对应的 API 端点,我们必须发送一个输入以进行预测。

GET vs. POST 请求

GET 和 POST 是 HTTP 方法。HTTP 是一个定义客户端和服务器应该如何通信的协议。客户端将发送请求,服务器将在特定的网络端口上监听请求。客户端和服务器不一定需要是两台单独的机器。在我们的情况下,客户端和服务器都在同一台机器上。

每当您通过键入 URL 访问网站时,您都在向该特定网站发出请求。一个请求具有以下解剖结构(mng.bz/OowE):

  • 方法类型 — GET 或 POST

  • 一个路径 — 到达您想要到达的服务器端点的 URL

  • 一个主体 — 需要客户端完成请求的任何大型有效载荷(例如,用于机器学习预测服务的输入)

  • 一个头部 — 服务器需要的附加信息(例如,发送主体中的数据类型)

主要区别在于 GET 用于请求数据,而 POST 请求用于将数据发送到服务器(可以选择返回某些内容)。GET 请求不会有请求主体,而 POST 请求会有请求主体。另一个区别是 GET 请求可以被缓存,而 POST 请求不会被缓存,这使得它们对于敏感数据更安全。您可以在mng.bz/YGZA中了解更多信息。

我们将定义一个请求主体,其中包含我们要击中的签名名称以及我们要为其预测的输入。接下来,我们将使用 Python 中的 requests 库发送一个请求到我们的 TensorFlow 模型服务器(即 Docker 容器)。在此请求中,我们将定义要到达的 URL(由 TensorFlow 模型服务器自动生成)和要携带的有效载荷。如果请求成功,我们应该会得到一个有效的预测作为输出:

import base64
import json
import requests

req_body = {
  "signature_name": "serving_default",

  "instances": 
    [
            str(base64.b64encode(
                b"{\"X\": 7,\"Y\": 
➥ 4,\"month\":\"oct\",\"day\":\"fri\",\"FFMC\":60,\"DMC\":30,\"DC\":200,\
➥ "ISI\":9,\"temp\":30,\"RH\":50,\"wind\":10,\"rain\":0}]")
               )
    ]

}

data = json.dumps(req_body)

json_response = requests.post(
    'http:/ /localhost:8501/v1/models/forest_fires_model:predict', 
    data=data, 
    headers={"content-type": "application/json"}
)
predictions = json.loads(json_response.text)

我们首先要做的是用特定的请求主体定义一个请求。对请求主体的要求在www.tensorflow.org/tfx/serving/api_rest中定义。它是一个键值对字典,应该有两个键:signature_name 和 instances。signature_name 定义要在模型中调用哪个签名,而 instances 将包含输入数据。请注意,我们不是直接传递原始形式的输入数据。相反,我们使用 base64 编码。它将字节流(即二进制输入)编码为 ASCII 文本字符串。您可以在mng.bz/1o4g中了解更多信息。您可以看到我们首先将字典转换为字节流(即 b"" 格式),然后在其上使用 base64 编码。如果您还记得我们之前讨论的编写模型服务函数(其中包含 signature def serve_tf_examples_fn(serialized_tf_examples))时,它期望一组序列化的示例。序列化是通过将数据转换为字节流来完成的。

当数据准备好后,我们使用 requests 库创建一个 POST 请求到 API。首先,我们定义一个头部,以表示我们传递的内容或载荷是 JSON 格式的。接下来,我们通过 requests.post() 方法发送一个 POST 请求,传递 URL,格式为 <server’s hostname>:<port>/v1/models/<model name>:predict,数据(即 JSON 载荷),和头部信息。这不是我们唯一可以使用的 API 端点。我们还有其他端点(www.tensorflow.org/tfx/serving/api_rest)。主要有四个可用的端点:

  • http:/ /<server’s hostname>:/v1/models/:predict — 使用模型和请求中传递的数据预测输出值。不需要提供给定输入的目标值。

  • http:/ /<server’s hostname>:/v1/models/:regress — 用于回归问题。当输入和目标值都可用时使用(即可以计算误差)。

  • http:/ /<server’s hostname>:/v1/models/:classify — 用于分类问题。当输入和目标值都可用时使用(即可以计算误差)。

  • http:/ /<server’s hostname>:/v1/models//metadata — 提供有关可用端点/模型签名的元数据。

这将返回一些响应。如果请求成功,将包含响应;否则,会包含 HTTP 错误。您可以在 mng.bz/Pn2P 上看到各种 HTTP 状态/错误代码。在我们的情况下,我们应该得到类似于

{'predictions': [[2.77522683]]}

这意味着我们的模型已成功处理了输入并产生了有效的预测。我们可以看到,模型返回的预测值完全在我们在数据探索期间看到的可能值范围内。这结束了我们对 TensorFlow 扩展(TFX)的讨论。

练习 4

如何将多个输入发送到模型的 HTTP 请求中?假设您有以下两个输入,您想要使用模型进行预测。

Example 1Example 2
X97
Y64
monthaugaug
dayfrifri
FFMC9191
DMC248248
DC553553
ISI66
temp20.520.5
RH5820
wind30
rain00

要在 HTTP 请求中为该输入传递多个值,可以在 JSON 数据的实例列表中附加更多示例。

摘要

  • MLOps 定义了一个工作流程,将自动化大部分步骤,从收集数据到交付对该数据进行训练的模型。

  • 生产部署涉及部署一个带有健壮 API 的训练模型,使客户能够使用模型进行其设计目的的操作。该 API 提供几个 HTTP 端点,格式为客户端可以使用与服务器通信的 URL。

  • 在 TFX 中,您将 MLOps 管道定义为一系列 TFX 组件。

  • TFX 有组件用于加载数据(CsvExampleGen)、生成基本统计信息和可视化(StatisticsGen)、推断模式(SchemaGen)以及将原始列转换为特征(Transform)。

  • 要通过 HTTP 请求提供 Keras 模型,需要签名。

    • 签名定义输入和输出的数据格式,以及通过 TensorFlow 函数(例如,用@tf.function 装饰的函数)生成输出所需的步骤。
  • Docker 是一种容器化技术,可以将一个软件单元封装为一个单一容器,并可以在不同环境(或计算机)之间轻松移植。

  • Docker 在容器中运行一个软件单元。

  • TFX 为验证基础设施和模型提供了验证组件。TFX 可以启动一个容器并确保它按预期运行,还可以确保模型通过各种评估标准(例如,损失小于阈值),从而确保高质量的模型。

  • 一旦模型被推送到生产环境,我们会启动一个 Docker 容器(基于 TensorFlow 服务镜像),将模型装入容器并通过 API 提供服务。我们可以发出 HTTP 请求(嵌入输入),以生成预测。

练习答案

练习 1

  outputs = {}

  # Treating dense features
  outputs[_transformed_name('DC')] = tft.scale_to_0_1(
        sparse_to_dense(inputs['DC'])
    )

  # Treating bucketized features
  outputs[_transformed_name('temp')] = tft.apply_buckets(
        sparse_to_dense(inputs['temp']), bucket_boundaries=[(20, 30)])

练习 2

categorical_columns = [
      tf.feature_column.embedding_column(
          tf.feature_column.categorical_column_with_identity( 
              key,
              num_buckets=num_buckets,
              default_value=0
          ),
          dimension=32
      ) for key, num_buckets in zip(
              _transformed_names(_VOCAB_FEATURE_KEYS),
              _MAX_CATEGORICAL_FEATURE_VALUES
      )

练习 3

docker run -v /tmp/inputs:/data -p 5000:5000 tensorflow/tensorflow:2.5.0

练习 4

req_body = {
  "signature_name": "serving_default",

  "instances": 
    [
        str(base64.b64encode(
            b"{\"X\": 9,\"Y\": 
➥ 6,\"month\":\"aug\",\"day\":\"fri\",\"FFMC\":91,\"DMC\":248,\"DC\":553,
➥ \"ISI\":6,\"temp\":20.5,\"RH\":58,\"wind\":3,\"rain\":0}]")
        ),
        str(base64.b64encode(
            b"{\"X\": 7,\"Y\": 
➥ 4,\"month\":\"aug\",\"day\":\"fri\",\"FFMC\":91,\"DMC\":248,\"DC\":553,
➥ \"ISI\":6,\"temp\":20.5,\"RH\":20,\"wind\":0,\"rain\":0}]")
        ),

    ]

}