TensorFlow2-口袋参考-全-

TensorFlow2 口袋参考(全)

原文:PyTorch Pocket Reference

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

TensorFlow 生态系统已经发展成许多不同的框架,以服务各种角色和功能。这种灵活性是其被广泛采用的原因之一,但也增加了数据科学家、机器学习(ML)工程师和其他技术利益相关者的学习曲线。有很多方法可以管理 TensorFlow 模型用于常见任务,比如数据和特征工程、数据摄取、模型选择、训练模式、交叉验证防止过拟合以及部署策略,选择可能会让人感到不知所措。

这本便携参考书将帮助您在 TensorFlow 中做出选择,包括如何使用 Python 中的 TensorFlow 2.0 设计模式设置常见的数据科学和 ML 工作流程。示例描述和演示了 TensorFlow 编码模式和您在 ML 项目工作中可能经常遇到的其他任务。您可以将其用作操作指南和参考书。

本书适用于当前和潜在的 ML 工程师、数据科学家和企业 ML 解决方案架构师,他们希望在 TensorFlow 建模中的可重用模式和最佳实践方面提升知识和经验。也许您已经阅读过一本介绍性的 TensorFlow 书籍,并且对数据科学领域保持了解。本书假定您具有使用 Python(可能还有 NumPy、pandas 和 JSON 库)进行数据工程、特征工程例程和构建 TensorFlow 模型的实际经验。熟悉常见数据结构,如列表、字典和 NumPy 数组,也将非常有帮助。

与许多其他 TensorFlow 书籍不同,本书围绕您可能需要执行的任务进行结构化,比如:

  • 何时以及为什么应该将训练数据作为 NumPy 数组或流式数据集传递?(第 2 和 5 章)

  • 如何利用预训练模型进行迁移学习?(第 3 和 4 章)

  • 您应该使用通用的 fit 函数进行训练,还是编写自定义训练循环?(第六章)

  • 您应该如何管理和利用模型检查点?(第七章)

  • 如何使用 TensorBoard 审查训练过程?(第七章)

  • 如果你的数据无法全部放入运行时的内存中,你如何使用多个加速器(如 GPU)进行分布式训练?(第八章)

  • 在推断期间如何将数据传递给模型以及如何处理输出?(第九章)

  • 您的模型是否公平?(第十章)

如果您正在处理这类问题,本书将对您有所帮助。

本书使用的约定

本书使用以下排版约定:

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

等宽粗体

显示用户应该按照字面意思输入的命令或其他文本。

等宽斜体

显示应该用用户提供的值或上下文确定的值替换的文本。

提示

这个元素表示提示或建议。

使用代码示例

补充材料(代码示例、练习等)可在https://github.com/shinchan75034/tensorflow-pocket-ref下载。

如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com

这本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们以获得许可。例如,编写一个程序使用本书中的几个代码块不需要许可。销售或分发 O'Reilly 图书中的示例需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书中大量示例代码整合到产品文档中需要许可。

我们感谢,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“TensorFlow 2 Pocket Reference by KC Jung (O’Reilly). Copyright 2021 Favola Vera, LLC, 978-1-492-08918-6.”

如果您认为您对代码示例的使用超出了合理使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。

第一章:TensorFlow 2 简介

TensorFlow 长期以来一直是最受欢迎的开源 Python 机器学习(ML)库。它是由 Google Brain 团队作为内部工具开发的,但在 2015 年以 Apache 许可证发布。从那时起,它已经发展成一个充满重要资产的生态系统,用于模型开发和部署。今天,它支持各种特定设计用于处理数据摄入和转换、特征工程、模型构建和服务等任务的 API 和模块。

TensorFlow 变得越来越复杂。本书的目的是帮助简化数据科学家或 ML 工程师在端到端模型开发过程中需要执行的常见任务。本书不关注数据科学和算法;相反,这里的示例使用预构建的模型作为教授相关概念的工具。

本书适用于具有构建 ML 模型的基本经验和知识的读者。强烈建议具备一定的 Python 编程能力。如果您从头到尾阅读本书,您将获得关于端到端模型开发过程和涉及的主要任务的大量知识,包括数据工程、摄入和准备;模型训练;以及服务模型。

本书中示例的源代码是在 Google Colaboratory(简称 Colab)和运行 macOS Big Sur,版本 11.2.3 的 MacBook Pro 上开发和测试的。使用的 TensorFlow 版本是 2.4.1,Python 版本是 3.7。

TensorFlow 2 的改进

随着 TensorFlow 的发展,它的复杂性也在增加。新用户学习 TensorFlow 的曲线是陡峭的,因为需要记住许多不同的方面。我如何准备数据进行摄入和训练?如何处理不同的数据类型?对于不同的处理方法需要考虑什么?这些只是您在 ML 旅程初期可能遇到的一些基本问题。

一个特别难以适应的概念是惰性执行,这意味着 TensorFlow 实际上不会处理您的数据,直到您明确告诉它执行整个代码。这个想法是为了加快性能。您可以将 ML 模型看作一组节点和边(换句话说,一个图)。当您在路径中运行计算并通过节点转换数据时,结果只有数据路径中的计算被执行。换句话说,您不必计算每个计算,只需计算数据通过图从输入到输出的路径中直接位于的计算。如果数据的形状和格式在一个节点和下一个节点之间没有正确匹配,当您编译模型时将会出现错误。在传递数据结构或张量形状时,很难调查您在哪里犯了错误以进行调试。

通过 TensorFlow 1.x,惰性执行是构建和训练 ML 模型的方式。然而,从 TensorFlow 2 开始,急切执行是构建和训练模型的默认方式。这种改变使得调试代码和尝试不同的模型架构变得更加容易。急切执行还使得学习 TensorFlow 变得更加容易,因为您将立即在执行每行代码时看到任何错误。您不再需要在调试和测试输入数据是否具有正确形状之前构建整个模型图。这是使 TensorFlow 2 比以前版本更易于使用的几个主要功能和改进之一。

Keras API

Keras 是由 AI 研究员 François Chollet 创建的开源、高级、深度学习 API 或框架。它与多个 ML 库兼容。

高级意味着在更低级别上有另一个实际执行计算的框架,事实上确实如此。这些低级框架包括 TensorFlow、Theano 和 Microsoft 认知工具包(CNTK)。Keras 的目的是为那些想要利用低级框架构建深度学习模型的用户提供更简单的语法和编码风格。

2015 年,Chollet 加入 Google 后,Keras 逐渐成为 TensorFlow 采用的基石。2019 年,随着 TensorFlow 团队推出 2.0 版本,正式采用了 Keras 作为 TensorFlow 的一流 API,即tf.keras,用于所有未来版本。从那时起,TensorFlow 已经将tf.keras与许多其他重要模块集成在一起。例如,它与tf.io API 无缝配合,用于读取分布式训练数据。它还与tf.data.Dataset类一起工作,用于流式传输训练数据,这些数据太大,无法容纳在一台计算机中。本书在所有章节中都使用这些模块。

今天,TensorFlow 用户主要依赖tf.keras API 来快速轻松地构建深度模型。快速获得训练例程的便利性使得更多时间可以用来尝试不同的模型架构和调整模型和训练例程中的参数。

TensorFlow 中的可重用模型

学术研究人员已经构建和测试了许多 ML 模型,所有这些模型在其架构上都很复杂。用户学习如何构建这些模型并不现实。这就引入了迁移学习的概念,其中为一个任务开发的模型被重用来解决另一个任务,即用户定义的任务。这基本上归结为将用户数据转换为适当的数据结构,以便模型输入和输出。

自然地,这些模型及其潜在用途引起了极大的兴趣。因此,应大众需求,许多模型已经在开源生态系统中可用。TensorFlow 创建了一个仓库,TensorFlow Hub,向公众提供这些复杂模型的免费访问。如果您感兴趣,您可以尝试这些模型,而无需自己构建它们。在第四章中,您将学习如何从 TensorFlow Hub 下载和使用模型。一旦您这样做了,您只需要了解模型在输入时期望的数据结构,并添加一个适合您预测目标的最终输出层。TensorFlow Hub 中的每个模型都包含简洁的文档,为您提供构建输入数据所需的信息。

另一个获取预构建模型的地方是tf.keras.applications模块,它是 TensorFlow 分发的一部分。在第四章中,您将学习如何使用此模块来利用预构建模型处理您自己的数据。

使常用操作变得简单

TensorFlow 2 中的所有这些改进使得许多重要操作更容易实现。即便如此,从头到尾构建和训练一个 ML 模型并不是一项简单的任务。本书将向您展示如何处理 TensorFlow 2 模型训练过程的每个方面,从一开始。以下是其中一些操作。

开源数据

集成到 TensorFlow 2 中的一个方便的包是TensorFlow 数据集库。这是一个由精心策划的开源数据集组成的集合,可供使用。该库包含图像、文本、音频、视频等多种格式的数据集。有些是 NumPy 数组,而其他的是数据集结构。该库还提供了如何使用 TensorFlow 加载这些数据集的文档。通过在其产品中分发各种开源数据集,TensorFlow 团队真正为用户节省了搜索、集成和重塑训练数据以适应 TensorFlow 工作负载的麻烦。本书中将使用的一些开源数据集是用于结构化数据分类的泰坦尼克数据集和用于图像分类的CIFAR-10 数据集

处理分布式数据集

首先,您必须处理如何处理训练数据的问题。许多教学示例使用 TensorFlow 中的预构建训练数据,以其原生格式,例如小型 pandas DataFrame 或 NumPy 数组,这些数据可以很好地适应计算机的内存。然而,在更现实的情况下,您可能需要处理比计算机内存更多的训练数据。从 SQL 数据库读取的表的大小很容易达到几十亿字节。即使您有足够的内存将其加载到 pandas DataFrame 或 NumPy 数组中,您的 Python 运行时在计算过程中可能会耗尽内存并崩溃。

通常将大量数据保存为多个文件,常见格式为 CSV(逗号分隔值)或文本。因此,您不应尝试在 Python 运行时加载每个文件。处理分布式数据集的正确方法是创建一个引用,指向所有文件的位置。第二章将向您展示如何使用tf.io API,该 API 提供一个包含文件路径和名称列表的对象。无论数据大小和文件数量如何,这都是处理训练数据的首选方式。

数据流式传输

您打算如何将数据传递给模型进行训练?这是一个重要的技能,但许多流行的教学示例通过将整个 NumPy 数组传递到模型训练例程中来处理。就像加载大型训练数据一样,如果尝试将大型 NumPy 数组传递给模型进行训练,您将遇到内存问题。

更好的处理方式是通过数据流。不是一次性传递整个训练数据,而是为模型训练提供一个子集或批量数据进行流式传输。在 TensorFlow 中,这被称为您的数据集。在第二章中,您还将学习如何从tf.io对象创建数据集。数据集对象可以从各种本地数据结构创建。在第三章中,您将看到如何从 CSV 文件和图像创建tf.data.Dataset对象。

通过tf.iotf.data.Dataset的组合,您将为模型训练设置一个数据处理工作流程,而无需在 Python 运行时内存中读取或打开任何数据文件。

数据工程

为了使模型能够学习模式,您需要对训练数据应用数据或特征工程任务。根据数据类型,有不同的方法可以做到这一点。

如果您正在处理表格数据,可能在不同列中有不同的值或数据类型。在第三章中,您将看到如何使用 TensorFlow 的feature_column API 对训练数据进行标准化。它可以帮助您正确标记哪些列是数值列,哪些是分类列。

对于图像数据,您将有不同的任务。例如,数据集中的所有图像必须具有相同的尺寸。此外,像素值通常被归一化或缩放到[0, 1]范围。对于这些任务,tf.keras提供了ImageDataGenerator类,用于标准化图像尺寸并为您归一化像素值。

迁移学习

TensorFlow Hub 为所有人提供了预构建的开源模型。在第四章中,您将学习如何使用 Keras 层 API 访问 TensorFlow Hub。此外,tf.keras附带了这些预构建模型的清单,可以使用tf.keras.applications模块调用。在第四章中,您将学习如何使用此模块进行迁移学习。

模型风格

使用tf.keras可以以多种方式实现模型。这是因为一些深度学习模型架构或模式比其他更复杂。对于常见用途,符号 API风格,按顺序设置模型架构,可能足够了。另一种风格是命令式 API,其中您将模型声明为一个类,因此每次调用模型对象时,都会创建该类的一个实例。这要求您了解类继承的工作原理(我将在第六章中讨论)。如果您的编程背景源自面向对象的编程语言,如 C++或 Java,那么这个 API 可能对您更自然。使用命令式 API 方法的另一个原因是将模型架构代码与其余工作流程分开。在第六章中,您将学习如何设置和使用这两种 API 风格。

监控训练过程

监控您的模型在每个epoch(即一次通过训练集)中是如何训练和验证的是模型训练的一个重要方面。在每个 epoch 结束时进行验证步骤是您可以采取的最简单的措施,以防止模型过拟合,这是一种现象,模型开始记忆训练数据模式而不是按预期学习特征。在第七章中,您将学习如何使用各种回调来保存每个 epoch 的模型权重和偏差。我还将指导您如何设置和使用 TensorBoard 来可视化训练过程。

分布式训练

即使您知道如何处理分布式数据和文件并将其流式传输到模型训练例程中,但如果发现训练需要不切实际的时间呢?这就是分布式训练可以帮助的地方。它需要一组硬件加速器,例如图形处理单元(GPU)或张量处理单元(TPU)。这些加速器可以通过许多公共云提供商获得。您还可以在 Google Colab 中免费使用一个 GPU 或 TPU(而不是集群);您将学习如何使用tf.distribute.MirroredStrategy类,简化和减少设置分布式训练的繁重工作,在第八章的第一部分示例中进行操作。

tf.distribute.MirroredStrategy之前发布的 Horovod API 来自 Uber 工程团队,是一个相对复杂的替代方案。它专门用于在计算集群上运行训练例程。要学习如何使用 Horovod,您需要使用基于云的计算平台 Databricks,在第八章的第二部分示例中进行操作。这将帮助您学习如何重构您的代码以分发和分片数据以供 Horovod API 使用。

为您的 TensorFlow 模型提供服务

一旦您构建了模型并成功训练了它,现在是时候将模型持久化或存储起来,以便可以提供给处理用户输入。您将看到使用tf.saved_model API 保存您的模型是多么容易。

通常,模型由 Web 服务托管。这就是 TensorFlow Serving 出现的地方:它是一个框架,包装您的模型并通过 HTTP 公开为 Web 服务调用。在第九章中,您将学习如何使用 TensorFlow Serving Docker 镜像来托管您的模型。

改进培训体验

最后,第十章讨论了评估和改进模型训练过程的一些重要方面。您将学习如何使用 TensorFlow 模型分析模块来查看模型偏差的问题。该模块提供了一个交互式仪表板,称为公平性指标,旨在揭示模型偏差。使用 Jupyter Notebook 环境和您在第三章中训练的泰坦尼克号数据集上的模型,您将看到公平性指标是如何工作的。

tf.keras API 带来的另一个改进是使超参数调整更加方便。超参数是与模型训练例程或模型架构相关的属性。调整它们通常是一个繁琐的过程,因为它涉及彻底搜索参数空间。在第十章中,您将看到如何使用 Keras Tuner 库和一个称为 Hyperband 的高级搜索算法来进行超参数调整工作。

总结

TensorFlow 2 是对以前版本的重大改进。其最重要的改进是将tf.keras API 指定为使用 TensorFlow 的推荐方式。这个 API 与tf.iotf.data.Dataset无缝配合,用于端到端的模型训练过程。这些改进加快了模型构建和调试的速度,因此您可以尝试模型训练的其他方面,比如尝试不同的架构或进行更有效的超参数搜索。所以,让我们开始吧。

第二章:数据存储和摄入

要想设想如何设置一个 ML 模型来解决问题,您必须开始考虑数据结构模式。在本章中,我们将看一些存储、数据格式和数据摄入中的一般模式。通常,一旦您理解了您的业务问题并将其设置为数据科学问题,您必须考虑如何将数据转换为模型训练过程可以使用的格式或结构。在训练过程中进行数据摄入基本上是一个数据转换管道。没有这种转换,您将无法在企业驱动或用例驱动的环境中交付和提供模型;它将仍然只是一个探索工具,无法扩展以处理大量数据。

本章将向您展示如何为两种常见的数据结构(表格和图像)设计数据摄入管道。您将学习如何使用 TensorFlow 的 API 使管道可扩展。

数据流式处理是模型在训练过程中以小批量摄入数据的方式。在 Python 中进行数据流式处理并不是一个新概念。然而,理解它对于理解 TensorFlow 中更高级 API 的工作方式是基本的。因此,本章将从 Python 生成器开始。然后我们将看一下如何存储表格数据,包括如何指示和跟踪特征和标签。然后我们将转向设计您的数据结构,并讨论如何将数据摄入到模型进行训练以及如何流式传输表格数据。本章的其余部分涵盖了如何为图像分类组织图像数据以及如何流式传输图像数据。

使用 Python 生成器进行数据流式处理

有时候 Python 运行时的内存不足以处理整个数据集的加载。当发生这种情况时,建议的做法是将数据分批加载。因此,在训练过程中,数据被流式传输到模型中。

以小批量发送数据还有许多其他优点。其中之一是对每个批次应用梯度下降算法来计算误差(即模型输出与实际值之间的差异),并逐渐更新模型的权重和偏差以使这个误差尽可能小。这让我们可以并行计算梯度,因为一个批次的误差计算(也称为损失计算)不依赖于其他批次。这被称为小批量梯度下降。在每个时代结束时,当整个训练数据集经过模型时,所有批次的梯度被求和并更新权重。然后,使用新更新的权重和偏差重新开始训练下一个时代,并计算误差。这个过程根据用户定义的参数重复进行,这个参数被称为训练时代数

Python 生成器是返回可迭代对象的迭代器。以下是它的工作示例。让我们从一个 NumPy 库开始,进行这个简单的 Python 生成器演示。我创建了一个名为my_generator的函数,它接受一个 NumPy 数组,并以数组中的两条记录为一组进行迭代:

import numpy as np

def my_generator(my_array):
    i = 0
    while True:
        yield my_array[i:i+2, :] # output two elements at a time
        i += 1

这是我创建的测试数组,将被传递给my_generator

test_array = np.array([[10.0, 2.0],
                       [15, 6.0],
                       [3.2, -1.5],
                       [-3, -2]], np.float32)

这个 NumPy 数组有四条记录,每条记录由两个浮点值组成。然后我将这个数组传递给my_generator

output = my_generator(test_array)

要获得输出,请使用:

next(output)

输出应该是:

array([[10.,  2.],
       [15.,  6.]], dtype=float32)

如果您再次运行next(output)命令,输出将不同:

array([[15\. ,  6\. ],
       [ 3.2, -1.5]], dtype=float32)

如果您再次运行它,输出将再次不同:

array([[ 3.2, -1.5],
       [-3\. , -2\. ]], dtype=float32)

如果您第四次运行它,输出现在是:

array([[-3., -2.]], dtype=float32)

现在最后一条记录已经显示,您已经完成了对这些数据的流式处理。如果您再次运行它,它将返回一个空数组:

array([], shape=(0, 2), dtype=float32)

正如您所看到的,my_generator 函数每次运行时都会以 NumPy 数组的形式流式传输两条记录。生成器函数的独特之处在于使用 yield 语句而不是 return 语句。与 return 不同,yield 会生成一个值序列,而不会将整个序列存储在 Python 运行时内存中。yield 在我们调用 next 函数时每次生成一个序列,直到到达数组的末尾。

此示例演示了如何通过生成器函数生成数据子集。但是,在此示例中,NumPy 数组是即时创建的,因此保存在 Python 运行时内存中。让我们看看如何迭代存储为文件的数据集。

使用生成器流式传输文件内容

要理解如何流式传输存储中的文件,您可能会发现使用 CSV 文件作为示例更容易。我在这里使用的文件是 Pima 印第安人糖尿病数据集,这是一个可供下载的开源数据集。下载并将其存储在本地机器上。

这个文件没有包含标题,因此您还需要下载此数据集的列名和描述。

简而言之,该文件中的列是:

['Pregnancies', 'Glucose', 'BloodPressure',
 'SkinThickness', 'Insulin', 'BMI',
 'DiabetesPedigree', 'Age', 'Outcome']

让我们用以下代码行查看这个文件:

import csv
import pandas as pd

file_path = 'working_data/'
file_name = 'pima-indians-diabetes.data.csv'

col_name = ['Pregnancies', 'Glucose', 'BloodPressure',
            'SkinThickness', 'Insulin', 'BMI',
            'DiabetesPedigree', 'Age', 'Outcome']
pd.read_csv(file_path + file_name, names = col_name)

文件的前几行显示在 图 2-1 中。

Pima Indians Diabetes Dataset

图 2-1. Pima 印第安人糖尿病数据集

由于我们想要流式处理这个数据集,更方便的方法是将其作为 CSV 文件读取,并使用生成器输出行,就像我们在前一节中对 NumPy 数组所做的那样。实现这一点的方法是通过以下代码:

import csv
file_path = 'working_data/'
file_name = 'pima-indians-diabetes.data.csv'

with open(file_path + file_name, newline='\n') as csvfile:
    f = csv.reader(csvfile, delimiter=',')
    for row in f:
        print(','.join(row))

让我们仔细看看这段代码。我们使用 with open 命令创建一个文件句柄对象 csvfile,该对象知道文件存储在哪里。下一步是将其传递给 CSV 库中的 reader 函数:

f = csv.reader(csvfile, delimiter=',')

f 是 Python 运行时内存中的整个文件。要检查文件,执行这段简短的 for 循环代码:

for row in f:
        print(','.join(row))

前几行的输出看起来像 图 2-2。

Pima 印第安人糖尿病数据集 CSV 输出

图 2-2. Pima 印第安人糖尿病数据集 CSV 输出

现在您已经了解了如何使用文件句柄,让我们重构前面的代码,以便可以在函数中使用 yield,有效地创建一个生成器来流式传输文件的内容:

def stream_file(file_handle):
    holder = []
    for row in file_handle:
        holder.append(row.rstrip("\n"))
        yield holder
        holder = []

with open(file_path + file_name, newline = '\n') as handle:
    for part in stream_file(handle):
        print(part)

回想一下,Python 生成器是一个使用 yield 来迭代通过 iterable 对象的函数。您可以像往常一样使用 with open 获取文件句柄。然后我们将 handle 传递给一个包含 for 循环的生成器函数 stream_file,该循环逐行遍历 handle 中的文件,删除换行符 \n,然后填充一个 holder。每行通过 yield 从生成器传回到主线程的 print 函数。输出显示在 图 2-3 中。

由 Python 生成器输出的 Pima 印第安人糖尿病数据集

图 2-3. 由 Python 生成器输出的 Pima 印第安人糖尿病数据集

现在您已经清楚了数据集如何进行流式处理,让我们看看如何在 TensorFlow 中应用这一点。事实证明,TensorFlow 利用这种方法构建了一个用于数据摄入的框架。流式处理通常是摄入大量数据(例如一个表中的数十万行,或分布在多个表中)的最佳方式。

JSON 数据结构

表格数据是用于对 ML 模型训练的特征和标签进行编码的常见和便捷格式,CSV 可能是最常见的表格数据格式。您可以将逗号分隔的每个字段视为一列。每列都定义了一个数据类型,例如数字(整数或浮点数)或字符串。

表格数据不是唯一的结构化数据格式,我指的是每个记录遵循相同约定,每个记录中字段的顺序相同。另一个常见的数据结构是 JSON。JSON(JavaScript 对象表示)是一个由嵌套、分层键值对构建的结构。您可以将键视为列名,将值视为该样本中数据的实际值。JSON 可以转换为 CSV,反之亦然。有时原始数据是以 JSON 格式存在,需要将其转换为 CSV,这样更容易显示和检查。

这里是一个示例 JSON 记录,显示了键值对:

{
   "id": 1,
   "name": {
      "first": "Dan",
      "last": "Jones"
   },
   "rating": [
      8,
      7,
      9
   ]
},

请注意,“rating”键与数组[8, 7, 9]的值相关联。

有很多例子使用 CSV 文件或表作为训练数据,并将其纳入 TensorFlow 模型训练过程。通常,数据被读入一个 pandas DataFrame。然而,这种策略只有在所有数据都能适应 Python 运行时内存时才有效。您可以使用流处理数据,而不受 Python 运行时限制内存分配。由于您在前一节学习了 Python 生成器的工作原理,现在您可以看一下 TensorFlow 的 API,它遵循与 Python 生成器相同的原理,并学习如何使用 TensorFlow 采用 Python 生成器框架。

设置文件名的模式

在处理一组文件时,您会遇到文件命名约定中的模式。为了模拟一个不断生成和存储新数据的企业环境,我们将使用一个开源 CSV 文件,按行数拆分成多个部分,然后使用固定前缀重命名每个部分。这种方法类似于 Hadoop 分布式文件系统(HDFS)如何命名文件的部分。

如果您有一个方便的 CSV 文件,可以随时使用您自己的 CSV 文件。如果没有,您可以为本示例下载建议的CSV 文件(一个 COVID-19 数据集)。(如果您愿意,您可以克隆这个存储库。)

现在,你只需要owid-covid-data.csv。一旦下载完成,检查文件并确定行数:

wc -l owid-covid-data.csv

输出表明有超过 32,000 行:

32788 owid-covid-data.csv

接下来,检查 CSV 文件的前三行,看看是否有标题:

head -3 owid-covid-data.csv
iso_code,continent,location,date,total_cases,new_cases,
total_deaths,new_deaths,total_cases_per_million,
new_cases_per_million,total_deaths_per_million,
new_deaths_per_million,new_tests,total_tests,
total_tests_per_thousand,new_tests_per_thousand,
new_tests_smoothed,new_tests_smoothed_per_thousand,tests_units,
stringency_index,population,population_density,median_age,
aged_65_older,aged_70_older,gdp_per_capita,extreme_poverty,
cardiovasc_death_rate,diabetes_prevalence,female_smokers,
male_smokers,handwashing_facilities,hospital_beds_per_thousand,
life_expectancy
AFG,Asia,Afghanistan,2019-12-31,0.0,0.0,0.0,0.0,0.0,0.0,0.0,
0.0,,,,,,,,,38928341.0,
54.422,18.6,2.581,1.337,1803.987,,597.029,9.59,,,37.746,0.5,64.8

由于这个文件包含一个标题,你会在每个部分文件中看到标题。你也可以查看一些数据行,看看它们实际上是什么样子的。

将单个 CSV 文件拆分为多个 CSV 文件

现在让我们将这个文件分割成多个 CSV 文件,每个文件有 330 行。你应该最终得到 100 个 CSV 文件,每个文件都有标题。如果你使用 Linux 或 macOS,请使用以下命令:

cat owid-covid-data.csv| parallel --header : --pipe -N330 
'cat >owid-covid-data-
part00{#}.csv'

对于 macOS,您可能需要先安装parallel命令:

brew install parallel

这里是一些已创建的文件:

-rw-r--r--  1 mbp16  staff    54026 Jul 26 16:45 
owid-covid-data-part0096.csv
-rw-r--r--  1 mbp16  staff    54246 Jul 26 16:45 
owid-covid-data-part0097.csv
-rw-r--r--  1 mbp16  staff    51278 Jul 26 16:45 
owid-covid-data-part0098.csv
-rw-r--r--  1 mbp16  staff    62622 Jul 26 16:45 
owid-covid-data-part0099.csv
-rw-r--r--  1 mbp16  staff    15320 Jul 26 16:45 
owid-covid-data-part00100.csv

这个模式代表了多个 CSV 格式的标准存储安排。命名约定有一个明显的模式:要么所有文件都有相同的标题,要么没有一个文件有标题。

保持文件命名模式是个好主意,无论您有几十个还是几百个文件都会很方便。当您的命名模式可以很容易地用通配符表示时,更容易创建一个指向存储中所有数据的引用或文件模式对象。

在下一节中,我们将看看如何使用 TensorFlow API 创建一个文件模式对象,我们将使用它来为这个数据集创建一个流对象。

使用 tf.io 创建文件模式对象

TensorFlow 的tf.io API 用于引用包含具有共同命名模式的文件的分布式数据集。这并不意味着您要读取分布式数据集:您想要的是一个包含您想要读取的所有数据集文件的文件路径和名称列表。这并不是一个新的想法。例如,在 Python 中,glob 库是检索类似列表的常用选择。tf.io API 简单地利用 glob 库生成符合模式对象的文件名列表:

import tensorflow as tf

base_pattern = 'dataset'
file_pattern = 'owid-covid-data-part*'
files = tf.io.gfile.glob(base_pattern + '/' + file_pattern)

files是一个包含所有原始 CSV 文件名的列表,没有特定顺序:

['dataset/owid-covid-data-part0091.csv',
 'dataset/owid-covid-data-part0085.csv',
 'dataset/owid-covid-data-part0052.csv',
 'dataset/owid-covid-data-part0046.csv',
 'dataset/owid-covid-data-part0047.csv',
…]

这个列表将成为下一步的输入,即基于 Python 生成器创建一个流数据集对象。

创建一个流数据集对象

现在您已经准备好文件列表,您可以将其用作输入来创建一个流数据集对象。请注意,此代码旨在演示如何将 CSV 文件列表转换为 TensorFlow 数据集对象。如果您真的要使用这些数据来训练一个监督式 ML 模型,您还将执行数据清洗、归一化和聚合等操作,所有这些我们将在第八章中介绍。在本例中,“new_deaths”被选为目标列:

csv_dataset = tf.data.experimental.make_csv_dataset(files,
              header = True,
              batch_size = 5,
              label_name = 'new_deaths',
              num_epochs = 1,
              ignore_errors = True)

前面的代码指定files中的每个文件都包含一个标题。为了方便起见,我们将批量大小设置为 5。我们还使用label_name指定一个目标列,就好像我们要使用这些数据来训练一个监督式 ML 模型。num_epochs用于指定您希望对整个数据集进行多少次流式传输。

要查看实际数据,您需要使用csv_dataset对象迭代数据:

for features, target in csv_dataset.take(1):
    print("'Target': {}".format(target))
    print("'Features:'")
    for k, v in features.items():
        print("  {!r:20s}: {}".format(k, v))

这段代码使用数据集的第一个批次(take(1)),其中包含五个样本。

由于您指定了label_name作为目标列,其他列都被视为特征。在数据集中,内容被格式化为键值对。前面代码的输出将类似于这样:

'Target': [ 0\.  0\. 16\.  0\.  0.]
'Features:'
  'iso_code'          : [b'SWZ' b'ESP' b'ECU' b'ISL' b'FRO']
  'continent'         : 
[b'Africa' b'Europe' b'South America' b'Europe' b'Europe']
  'location'          : 
[b'Swaziland' b'Spain' b'Ecuador' b'Iceland' b'Faeroe Islands']
  'date'              : 
[b'2020-04-04' b'2020-02-07' b'2020-07-13' b'2020-04-01' 
  b'2020-06-11']
  'total_cases'       : [9.000e+00 1.000e+00 6.787e+04 
1.135e+03 1.870e+02]
  'new_cases'         : [  0\.   0\. 661\.  49\.   0.]
  'total_deaths'      : [0.000e+00 0.000e+00 5.047e+03 
2.000e+00 0.000e+00]
  'total_cases_per_million': 
              [7.758000e+00 2.100000e-02 3.846838e+03 
3.326007e+03 3.826870e+03]
  'new_cases_per_million': [  0\.      0\.     37.465 
143.59    0\.   ]
  'total_deaths_per_million': [  0\.      0\.    286.061   
5.861   0\.   ]
  'new_deaths_per_million': 
[0\.    0\.    0.907 0\.    0\.   ]
  'new_tests'         : 
[b'' b'' b'1331.0' b'1414.0' b'']
  'total_tests'       : 
[b'' b'' b'140602.0' b'20889.0' b'']
  'total_tests_per_thousand': 
[b'' b'' b'7.969' b'61.213' b'']
  'new_tests_per_thousand': 
[b'' b'' b'0.075' b'4.144' b'']
  'new_tests_smoothed': 
[b'' b'' b'1986.0' b'1188.0' b'']
  'new_tests_smoothed_per_thousand': 
[b'' b'' b'0.113' b'3.481' b'']
  'tests_units'       : 
[b'' b'' b'units unclear' b'tests performed' b'']
  'stringency_index'  : 
[89.81 11.11 82.41 53.7   0\.  ]
  'population'        : 
[ 1160164\. 46754784\. 17643060\.   341250\.    48865.]
  'population_density': 
[79.492 93.105 66.939  3.404 35.308]
  'median_age'        : 
[21.5 45.5 28.1 37.3  0\. ]
  'aged_65_older'     : 
[ 3.163 19.436  7.104 14.431  0\.   ]
  'aged_70_older'     :
[ 1.845 13.799  4.458  9.207  0\.   ]
  'gdp_per_capita'    : 
[ 7738.975 34272.36  10581.936 46482.957     0\.   ]
  'extreme_poverty'   : [b'' b'1.0' b'3.6' b'0.2' b'']
  'cardiovasc_death_rate': 
[333.436  99.403 140.448 117.992   0\.   ]
  'diabetes_prevalence': [3.94 7.17 5.55 5.31 0\.  ]
  'female_smokers'    : 
[b'1.7' b'27.4' b'2.0' b'14.3' b'']
  'male_smokers'      : 
[b'16.5' b'31.4' b'12.3' b'15.2' b'']
  'handwashing_facilities': 
[24.097  0\.    80.635  0\.     0\.   ]
  'hospital_beds_per_thousand': 
[2.1  2.97 1.5  2.91 0\.  ]
  'life_expectancy'   : 
[60.19 83.56 77.01 82.99 80.67]

此数据在运行时检索(延迟执行)。根据批量大小,每列包含五条记录。接下来,让我们讨论如何流式传输这个数据集。

流式传输 CSV 数据集

现在已经创建了一个 CSV 数据集对象,您可以使用这行代码轻松地按批次迭代它,该代码使用iter函数从 CSV 数据集创建一个迭代器,并使用next函数返回迭代器中的下一个项目:

features, label = next(iter(csv_dataset))

请记住,在这个数据集中有两种类型的元素:featureslabel。这些元素作为元组返回(类似于对象列表,不同之处在于对象的顺序和值不能被更改或重新分配)。您可以通过将元组元素分配给变量来解压元组。

如果您检查标签,您将看到第一个批次的内容:

<tf.Tensor: shape=(5,), dtype=float32, 
numpy=array([ 0.,  0.,  1., 33., 29.], dtype=float32)>

如果再次执行相同的命令,您将看到第二个批次:

features, label = next(iter(csv_dataset))

让我们看一下label

<tf.Tensor: shape=(5,), dtype=float32, 
numpy=array([ 7., 15.,  1.,  0.,  6.], dtype=float32)>

确实,这是第二批观察结果;它包含与第一批不同的值。这就是在数据摄入管道中生成流式传输 CSV 数据集的方式。当每个批次被发送到模型进行训练时,模型通过前向传递计算预测,该计算通过将输入值与神经网络中每个节点的当前权重和偏差相乘来计算输出。然后,它将预测与标签进行比较并计算损失函数。接下来是反向传递,模型计算与预期输出的变化,并向网络中的每个节点后退以更新权重和偏差。然后模型重新计算并更新梯度。将新的数据批次发送到模型进行训练,过程重复。

接下来我们将看看如何为存储组织图像数据,并像我们流式传输结构化数据一样流式传输它。

组织图像数据

图像分类任务需要以特定方式组织图像,因为与 CSV 或表格数据不同,将标签附加到图像需要特殊技术。组织图像文件的一种直接和常见模式是使用以下目录结构:

<PROJECT_NAME>
       train
           class_1
                <FILENAME>.jpg
                <FILENAME>.jpg
                …
           class_n
                <FILENAME>.jpg
                <FILENAME>.jpg
                …
       validation
           class_1
               <FILENAME>.jpg
               <FILENAME>.jpg
               …
           class_n
               <FILENAME>.jpg
               <FILENAME>.jpg
       test
           class_1
               <FILENAME>.jpg
               <FILENAME>.jpg
               …
           class_n
               <FILENAME>.jpg
               <FILENAME>.jpg
               …

<PROJECT_NAME>是基本目录。它的下一级包含训练、验证和测试目录。在每个目录中,都有以图像标签命名的子目录(class_1class_2等,在下面的示例中是花卉类型),每个子目录包含原始图像文件。如图 2-4 所示。

这种结构很常见,因为它可以方便地跟踪标签及其相应的图像,但这绝不是组织图像数据的唯一方式。让我们看看另一种组织图像的结构。这与之前的结构非常相似,只是训练、测试和验证都是分开的。在<PROJECT_NAME>目录的正下方是不同图像类别的目录,如图 2-5 所示。

用于图像分类和训练工作的文件组织

图 2-4. 用于图像分类和训练工作的文件组织

基于标签的图像文件组织

图 2-5. 基于标签的图像文件组织

使用 TensorFlow 图像生成器

现在让我们看看如何处理图像。除了文件组织的细微差别外,处理图像还需要一些步骤来标准化和归一化图像文件。模型架构需要所有图像具有固定形状(固定尺寸)。在像素级别,值通常被归一化为[0, 1]的范围(将像素值除以 255)。

在这个例子中,您将使用一个包含五种不同类型的花朵的开源图像集(或者随意使用您自己的图像集)。假设图像应该是 224×224 像素,其中尺寸对应高度和宽度。如果您想要使用预训练的残差神经网络(ResNet)作为图像分类器,这些是输入图像的预期尺寸。

首先让我们下载这些图像。以下代码下载五种不同尺寸的花朵,并将它们放入稍后在图 2-6 中显示的文件结构中:

import tensorflow as tf

data_dir = tf.keras.utils.get_file(
    'flower_photos',
'https://storage.googleapis.com/download.tensorflow.org/
example_images/flower_photos.tgz', untar=True)

我们将data_dir称为基本目录。它应该类似于:

'/Users/XXXXX/.keras/datasets/flower_photos'

如果列出基本目录中的内容,您将看到:

-rw-r-----    1 mbp16  staff  418049 Feb  8  2016 LICENSE.txt
drwx------  801 mbp16  staff   25632 Feb 10  2016 tulips
drwx------  701 mbp16  staff   22432 Feb 10  2016 sunflowers
drwx------  643 mbp16  staff   20576 Feb 10  2016 roses
drwx------  900 mbp16  staff   28800 Feb 10  2016 dandelion
drwx------  635 mbp16  staff   20320 Feb 10  2016 daisy

流式传输图像有三个步骤。让我们更仔细地看一下:

  1. 创建一个ImageDataGenerator对象并指定归一化参数。使用rescale参数指示归一化比例,使用validation_split参数指定将 20%的数据留出用于交叉验证:

    train_datagen = tf.keras.preprocessing.image.
        ImageDataGenerator(
        rescale = 1./255, 
        validation_split = 0.20)
    

    可选地,您可以将rescalevalidation_split包装为一个包含键值对的字典:

    datagen_kwargs = dict(rescale=1./255, 
                          validation_split=0.20)
    
    train_datagen = tf.keras.preprocessing.image.
        ImageDataGenerator(**datagen_kwargs)
    

    这是一种方便的方式,可以重复使用相同的参数并将多个输入参数包装在一起。 (将字典数据结构传递给函数是一种称为字典解包的 Python 技术。)

  2. ImageDataGenerator对象连接到数据源,并指定参数将图像调整为固定尺寸:

    IMAGE_SIZE = (224, 224) # Image height and width 
    BATCH_SIZE = 32             
    dataflow_kwargs = dict(target_size=IMAGE_SIZE, 
                          batch_size=BATCH_SIZE, 
                          interpolation="bilinear")
    
    train_generator = train_datagen.flow_from_directory(
    data_dir, subset="training", shuffle=True, 
    **dataflow_kwargs)
    
  3. 为索引标签准备一个映射。在这一步中,您检索生成器为每个标签分配的索引,并创建一个将其映射到实际标签名称的字典。TensorFlow 生成器内部会跟踪data_dir下目录名称的标签。它们可以通过train_generator.class_indices检索,返回标签和索引的键值对。您可以利用这一点并将其反转以部署模型进行评分。模型将输出索引。要实现这种反向查找,只需反转由train_generator.class_indices生成的标签字典:

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

    这些是idx_labels

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

    现在您可以检查train_generator生成的项目的形状:

    for image_batch, labels_batch in train_generator:
      print(image_batch.shape)
      print(labels_batch.shape)
      break
    

    预计通过迭代基本目录生成器产生的第一批数据将会看到以下内容:

    (32, 224, 224, 3)
    (32, 5)
    

    第一个元组表示 32 张图片的批量大小,每张图片的尺寸为 224×224×3(高度×宽度×深度,深度代表三个颜色通道 RGB)。第二个元组表示 32 个标签,每个标签对应五种花的其中一种。它是根据idx_labels进行独热编码的。

流式交叉验证图片

回想一下,在创建用于流式训练数据的生成器时,您使用了validation_split参数,值为 0.2。如果不这样做,validation_split默认为 0。如果validation_split设置为非零小数,则在调用flow_from_directory方法时,还必须指定subsettrainingvalidation。在前面的示例中,它是subset="training"

您可能想知道如何知道哪些图片属于我们之前创建的训练生成器的training子集。好消息是,如果您重新分配和重复使用训练生成器,您就不需要知道这一点:

valid_datagen = train_datagen

valid_generator = valid_datagen.flow_from_directory(
    data_dir, subset="validation", shuffle=False, 
    **dataflow_kwargs)

正如您所看到的,TensorFlow 生成器知道并跟踪trainingvalidation子集,因此您可以重复使用相同的生成器来流式传输不同的子集。dataflow_kwargs字典也被重用。这是 TensorFlow 生成器提供的一个便利功能。

因为您重复使用train_datagen,所以可以确保图像重新缩放的方式与图像训练的方式相同。在valid_datagen.flow_from_directory方法中,您将传入相同的dataflow_kwargs字典,以设置交叉验证的图像大小与训练图像相同。

如果您希望自行组织图片到训练、验证和测试中,之前学到的内容仍然适用,但有两个例外。首先,您的data_dir位于训练、验证或测试目录的级别。其次,在ImageDataGeneratorflow_from_directory中不需要指定validation_splitsubset

检查调整大小的图片

现在让我们检查生成器生成的调整大小的图片。以下是通过生成器流式传输的数据批次进行迭代的代码片段:

import matplotlib.pyplot as plt
import numpy as np

image_batch, label_batch = next(iter(train_generator))

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

这段代码将从生成器中产生的第一批数据中生成 32 张图片(参见图 2-6)。

一批调整大小的图片

图 2-6. 一批调整大小的图片

让我们来检查代码:

image_batch, label_batch = next(iter(train_generator))

这将通过生成器迭代基本目录。它将iter函数应用于生成器,并利用next函数将图像批次和标签批次输出为 NumPy 数组:

fig, axes = plt.subplots(8, 4, figsize=(10, 20))

这行代码设置了您期望的子图数量,即 32,即批量大小:

axes = axes.flatten()
for img, lbl, ax in zip(image_batch, label_batch, axes):
    ax.imshow(img)
    label_ = np.argmax(lbl)
    label = idx_labels[label_]
    ax.set_title(label)
    ax.axis('off')
plt.show()

然后您设置图形轴,使用for循环将 NumPy 数组显示为图片和标签。如图 2-6 所示,所有图片都被调整为 224×224 像素的正方形。尽管子图容器是一个尺寸为(10, 20)的矩形,您可以看到所有图片都是正方形的。这意味着您在生成器工作流中调整大小和归一化图片的代码按预期工作。

总结

在本章中,你学会了使用 Python 处理流数据的基础知识。这是在处理大型分布式数据集时的一种重要技术。你还看到了一些常见的表格和图像数据的文件组织模式。

在表格数据部分,你学会了如何选择一个良好的文件命名约定,可以更容易地构建对所有文件的引用,无论有多少个文件。这意味着你现在知道如何构建一个可扩展的流水线,可以将所需的数据输入到 Python 运行时中,用于任何用途(在本例中,用于 TensorFlow 创建数据集)。

你还学会了图像文件通常如何在文件存储中组织,以及如何将图像与标签关联起来。在下一章中,你将利用你在这里学到的关于数据组织和流式处理的知识,将其与模型训练过程整合起来。

第三章:数据预处理

在本章中,您将学习如何为训练准备和设置数据。机器学习工作中最常见的数据格式是表格、图像和文本。与每种数据格式相关的常见实践技术,尽管如何设置数据工程流水线当然取决于您的问题陈述是什么以及您试图预测什么。

我将详细查看所有三种格式,使用具体示例来引导您了解这些技术。所有数据都可以直接读入 Python 运行时内存;然而,这并不是最有效的使用计算资源的方式。当讨论文本数据时,我将特别关注标记和字典。通过本章结束时,您将学会如何准备表格、图像和文本数据进行训练。

为训练准备表格数据

在表格数据集中,重要的是要确定哪些列被视为分类列,因为您必须将它们的值编码为类或类的二进制表示(独热编码),而不是数值值。表格数据集的另一个方面是多个特征之间的相互作用的潜力。本节还将查看 TensorFlow 提供的 API,以便更容易地建模列之间的交互。

通常会遇到作为 CSV 文件的表格数据集,或者仅仅作为数据库查询结果的结构化输出。在这个例子中,我们将从已经在 pandas DataFrame 中的数据集开始,并学习如何转换它并为模型训练设置它。我们将使用泰坦尼克数据集,这是一个开源的表格数据集,通常用于教学,因为其可管理的大小和可用性。该数据集包含每位乘客的属性,如年龄、性别、舱位等级,以及他们是否幸存。我们将尝试根据他们的属性或特征来预测每位乘客的生存概率。请注意,这是一个用于教学和学习目的的小数据集。实际上,您的数据集可能会更大。您可能会对一些输入参数的默认值做出不同的决定,并选择不同的默认值,所以不要对这个例子过于字面理解。

让我们从加载所有必要的库开始:

import functools
import numpy as np
import tensorflow as tf
import pandas as pd
from tensorflow import feature_column
from tensorflow.keras import layers
from sklearn.model_selection import train_test_split

从谷歌的公共存储中加载数据:

TRAIN_DATA_URL = "https://storage.googleapis.com/
tf-datasets/titanic/train.csv"
TEST_DATA_URL = "https://storage.googleapis.com/
tf-datasets/titanic/eval.csv"

train_file_path = tf.keras.utils.get_file("train.csv", 
TRAIN_DATA_URL)
test_file_path = tf.keras.utils.get_file("eval.csv", TEST_DATA_URL)

现在看看train_file_path

print(train_file_path)

/root/.keras/datasets/train.csv

这个文件路径指向一个 CSV 文件,我们将其读取为一个 pandas DataFrame:

titanic_df = pd.read_csv(train_file_path, header='infer')

图 3-1 展示了titanic_df作为 pandas DataFrame 的样子。

泰坦尼克数据集作为 pandas DataFrame

图 3-1。泰坦尼克数据集作为 pandas DataFrame

标记列

如您在图 3-1 中所见,这些数据中既有数值列,也有分类列。目标列,或者用于预测的列,是“survived”列。您需要将其标记为目标,并将其余列标记为特征。

提示

在 TensorFlow 中的最佳实践是将您的表格转换为流式数据集。这种做法确保数据的大小不会影响内存消耗。

为了做到这一点,TensorFlow 提供了函数tf.data.experimental.make_csv_dataset

LABEL_COLUMN = 'survived'
LABELS = [0, 1]

train_ds = tf.data.experimental.make_csv_dataset(
      train_file_path,
      batch_size=3,
      label_name=LABEL_COLUMN,
      na_value="?",
      num_epochs=1,
      ignore_errors=True)

test_ds = tf.data.experimental.make_csv_dataset(
      test_file_path,
      batch_size=3,
      label_name=LABEL_COLUMN,
      na_value="?",
      num_epochs=1,
      ignore_errors=True)

在前面的函数签名中,您指定要生成数据集对象的文件路径。batch_size被任意设置为一个较小的值(在本例中为 3),以便方便检查数据。我们还将label_name设置为“survived”列。对于数据质量,如果任何单元格中指定了问号(?),您希望将其解释为“NA”(不适用)。对于训练,将num_epochs设置为对数据集进行一次迭代。您可以忽略任何解析错误或空行。

接下来,检查数据:

for batch, label in train_ds.take(1):
  print(label)
  for key, value in batch.items():
    print("{}: {}".format(key,value.numpy()))

它看起来类似于图 3-2。

一个批次的泰坦尼克数据集

图 3-2. 泰坦尼克数据集的一个批次

以下是训练范式消耗训练数据集的主要步骤:

  1. 按特征类型指定列。

  2. 决定是否嵌入或交叉列。

  3. 选择感兴趣的列,可能作为一个实验。

  4. 为训练范式创建一个“特征层”以供使用。

现在您已经将数据设置为数据集,可以根据其特征类型指定每列,例如数字或分类,如果需要的话可以进行分桶。如果唯一类别太多且降维会有帮助,还可以嵌入列。

让我们继续进行第 1 步。有四个数字列:agen_siblings_spousesparchfare。五列是分类的:sexclassdeckembark_townalone。完成后,您将创建一个feature_columns列表来保存所有特征列。

以下是如何根据实际数字值严格指定数字列的方法,而不进行任何转换:

feature_columns = []

# numeric cols
for header in ['age', 'n_siblings_spouses', 'parch', 'fare']:
feature_columns.append(feature_column.numeric_column(header))

请注意,除了使用age本身,您还可以将age分箱,例如按年龄分布的四分位数。但是分箱边界(四分位数)是什么?您可以检查 pandas DataFrame 中数字列的一般统计信息:

titanic_df.describe()

图 3-3 显示了输出。

泰坦尼克号数据集中数字列的统计

图 3-3。泰坦尼克号数据集中的数字列统计

让我们尝试为年龄设置三个分箱边界:23、28 和 35。这意味着乘客年龄将被分组为第一四分位数、第二四分位数和第三四分位数(如图 3-3 所示):

age = feature_column.numeric_column('age')
age_buckets = feature_column.
bucketized_column(age, boundaries=[23, 28, 35])

因此,除了“age”之外,您还生成了另一个列“age_bucket”。

为了了解每个分类列的性质,了解其中的不同值将是有帮助的。您需要使用每列中的唯一条目对词汇表进行编码。对于分类列,这意味着您需要确定哪些条目是唯一的:

h = {}
for col in titanic_df:
  if col in ['sex', 'class', 'deck', 'embark_town', 'alone']:
    print(col, ':', titanic_df[col].unique())
    h[col] = titanic_df[col].unique()

结果显示在图 3-4 中。

泰坦尼克号数据集中每个分类列中的唯一值

图 3-4。数据集中每个分类列的唯一值

您需要以字典格式跟踪这些唯一值,以便模型进行映射和查找。因此,您将对“sex”列中的唯一分类值进行编码:

sex_type = feature_column.categorical_column_with_vocabulary_list(
      'Type', ['male' 'female'])
sex_type_one_hot = feature_column.indicator_column(sex_type)

然而,如果列表很长,逐个写出会变得不方便。因此,当您遍历分类列时,可以将每列的唯一值保存在 Python 字典数据结构h中以供将来查找。然后,您可以将唯一值作为列表传递到这些词汇表中:

sex_type = feature_column.
categorical_column_with_vocabulary_list(
      'Type', h.get('sex').tolist())
sex_type_one_hot = feature_column.
indicator_column(sex_type)

class_type = feature_column.
categorical_column_with_vocabulary_list(
      'Type', h.get('class').tolist())
class_type_one_hot = feature_column.
indicator_column(class_type)

deck_type = feature_column.
categorical_column_with_vocabulary_list(
      'Type', h.get('deck').tolist())
deck_type_one_hot = feature_column.
indicator_column(deck_type)

embark_town_type = feature_column.
categorical_column_with_vocabulary_list(
      'Type', h.get('embark_town').tolist())
embark_town_type_one_hot = feature_column.
indicator_column(embark_town_type)

alone_type = feature_column.
categorical_column_with_vocabulary_list(
      'Type', h.get('alone').tolist())
alone_one_hot = feature_column.
indicator_column(alone_type)

您还可以嵌入“deck”列,因为有八个唯一值,比任何其他分类列都多。将其维度减少到 3:

deck = feature_column.
categorical_column_with_vocabulary_list(
      'deck', titanic_df.deck.unique())
deck_embedding = feature_column.
embedding_column(deck, dimension=3)

减少分类列维度的另一种方法是使用哈希特征列。该方法根据输入数据计算哈希值,然后为数据指定一个哈希桶。以下代码将“class”列的维度减少到 4:

class_hashed = feature_column.categorical_column_with_hash_bucket(
      'class', hash_bucket_size=4)

将列交互编码为可能的特征

现在来到最有趣的部分:您将找到不同特征之间的交互(这被称为交叉列),并将这些交互编码为可能的特征。这也是您的直觉和领域知识可以有益于您的特征工程努力的地方。例如,基于泰坦尼克号灾难的历史背景,一个问题是:一等舱的女性是否比二等或三等舱的女性更有可能生存?为了将这个问题重新表述为一个数据科学问题,您需要考虑乘客的性别和舱位等级之间的交互。然后,您需要选择一个起始维度大小来表示数据的变化性。假设您任意决定将变化性分成五个维度(hash_bucket_size):


cross_type_feature = feature_column.
crossed_column(['sex', 'class'], hash_bucket_size=5)

现在您已经创建了所有特征,需要将它们组合在一起,并可能进行实验以决定在训练过程中包含哪些特征。为此,您首先要创建一个列表来保存您想要使用的所有特征:

feature_columns = []

然后,您将每个感兴趣的特征附加到列表中:

# append numeric columns
for header in ['age', 'n_siblings_spouses', 'parch', 'fare']:
  feature_columns.append(feature_column.numeric_column(header))

# append bucketized columns
age = feature_column.numeric_column('age')
age_buckets = feature_column.
bucketized_column(age, boundaries=[23, 28, 35])
feature_columns.append(age_buckets)

# append categorical columns
indicator_column_names = 
['sex', 'class', 'deck', 'embark_town', 'alone']
for col_name in indicator_column_names:
  categorical_column = feature_column.
  categorical_column_with_vocabulary_list(
      col_name, titanic_df[col_name].unique())
  indicator_column = feature_column.

indicator_column(categorical_column)
  feature_columns.append(indicator_column)

# append embedding columns
deck = feature_column.categorical_column_with_vocabulary_list(
      'deck', titanic_df.deck.unique())
deck_embedding = feature_column.
embedding_column(deck, dimension=3)
feature_columns.append(deck_embedding)

# append crossed columns
feature_columns.
append(feature_column.indicator_column(cross_type_feature))

现在创建一个特征层:

feature_layer = tf.keras.layers.DenseFeatures(feature_columns)

这一层将作为您即将构建和训练的模型的第一(输入)层。这是您为模型的训练过程提供所有特征工程框架的方式。

创建一个交叉验证数据集

在开始训练之前,您需要为交叉验证目的创建一个小数据集。由于一开始只有两个分区(训练和测试),生成一个交叉验证数据集的一种方法是简单地将其中一个分区细分:

val_df, test_df = train_test_split(test_df, test_size=0.4)

在这里,原始test_df分区的 40%被随机保留为test_df,剩下的 60%现在是val_df。通常,测试数据集是三个数据集中最小的,因为它们仅用于最终评估,而不用于模型训练。

现在您已经处理了特征工程和数据分区,还有最后一件事要做:使用数据集将数据流入训练过程。您将把三个 DataFrame(训练、验证和测试)分别转换为自己的数据集:

batch_size = 32
labels = train_df.pop('survived')
working_ds = tf.data.Dataset.
from_tensor_slices((dict(train_df), labels))
working_ds = working_ds.shuffle(buffer_size=len(train_df))
train_ds = working_ds.batch(batch_size)

如前面的代码所示,首先您将任意决定要包含在一个批次中的样本数量(batch_size)。然后,您需要设置一个标签指定(survived)。tf.data.Dataset.from_tensor_slices方法接受一个元组作为参数。在这个元组中,有两个元素:特征列和标签列。

第一个元素是dict(train_df)。这个dict操作实质上将 DataFrame 转换为键值对,其中每个键代表一个列名,相应的值是该列中的值数组。另一个元素是labels

最后,我们对数据集进行洗牌和分批处理。由于这种转换将应用于所有三个数据集,将这些步骤合并到一个辅助函数中以减少重复会很方便:

def pandas_to_dataset(dataframe, shuffle=True, batch_size=32):
  dataframe = dataframe.copy()
  labels = dataframe.pop('survived')
  ds = tf.data.Dataset.
from_tensor_slices((dict(dataframe), labels))
  if shuffle:
    ds = ds.shuffle(buffer_size=len(dataframe))
  ds = ds.batch(batch_size)
  return ds

现在您可以将此函数应用于验证和测试数据:

val_ds = pandas_to_dataset(val_df, shuffle=False, 
batch_size=batch_size)
test_ds = pandas_to_dataset(test_df, shuffle=False, 
batch_size=batch_size)

开始模型训练过程

现在,您已经准备好开始模型训练过程。从技术上讲,这并不是预处理的一部分,但通过这个简短的部分,您可以看到您所做的工作如何融入到模型训练过程中。

您将从构建模型架构开始:

model = tf.keras.Sequential([
  feature_layer,
  layers.Dense(128, activation='relu'),
  layers.Dense(128, activation='relu'),
  layers.Dropout(.1),
  layers.Dense(1)
])

为了演示目的,您将构建一个简单的两层深度学习感知器模型,这是一个前馈神经网络的基本配置。请注意,由于这是一个多层感知器模型,您将使用顺序 API。在这个 API 中,第一层是feature_layer,它代表所有特征工程逻辑和派生特征,例如年龄分段和交叉,用于建模特征交互。

编译模型并为二元分类设置损失函数:

model.compile(optimizer='adam',
              loss=tf.keras.losses.BinaryCrossentropy(
from_logits=True),
              metrics=['accuracy'])

然后您可以开始训练。您只会训练 10 个 epochs:

model.fit(train_ds,
          validation_data=val_ds,
          epochs=10)

您可以期望得到与图 3-5 中所示类似的结果。

泰坦尼克数据集中生存预测的示例训练结果

图 3-5. 泰坦尼克数据集中生存预测的示例训练结果

总结

在本节中,您看到了如何处理由多种数据类型组成的表格数据。您还看到了 TensorFlow 提供的feature_columnAPI,它可以正确转换数据类型、处理分类数据,并为潜在交互提供特征交叉。这个 API 在简化数据和特征工程任务方面非常有帮助。

为处理图像数据做准备

对于图像,您需要将所有图像重塑或重新采样为相同的像素计数;这被称为标准化。您还需要确保所有像素值在相同的颜色范围内,以便它们落在每个像素的 RGB 值的有限范围内。

图像数据具有不同的文件扩展名,例如.jpg.tiff.bmp。这些并不是真正的问题,因为 Python 和 TensorFlow 中有可以读取和解析任何文件扩展名的图像的 API。关于图像数据的棘手部分在于捕获其维度——高度、宽度和深度——由像素计数来衡量。(如果是用 RGB 编码的彩色图像,这些会显示为三个独立的通道。)

如果您的数据集中的所有图像(包括训练、验证以及测试或部署时的所有图像)都预期具有相同的尺寸并且您将构建自己的模型,那么处理图像数据并不是太大的问题。然而,如果您希望利用预构建的模型如 ResNet 或 Inception,那么您必须符合它们的图像要求。例如,ResNet 要求每个输入图像为 224 × 224 × 3 像素,并呈现为 NumPy 多维数组。这意味着在预处理过程中,您必须重新采样您的图像以符合这些尺寸。

另一个需要重新采样的情况是当您无法合理地期望所有图像,特别是在部署时,具有相同的尺寸。在这种情况下,您需要在构建模型时考虑适当的图像尺寸,然后设置预处理程序以确保重新采样正确进行。

在本节中,您将使用 TensorFlow 提供的花卉数据集。它包含五种类型的花卉和不同的图像尺寸。这是一个方便的数据集,因为所有图像都已经是 JPEG 格式。您将处理这些图像数据,训练一个模型来解析每个图像并将其分类为五类花卉之一。

像往常一样,导入所有必要的库:

import tensorflow as tf
import numpy as np
import matplotlib.pylab as plt
import pathlib

现在从以下网址下载花卉数据集:

data_dir = tf.keras.utils.get_file(
    'flower_photos',
'https://storage.googleapis.com/download.tensorflow.org/
example_images/flower_photos.tgz',
    untar=True)

这个文件是一个压缩的 TAR 存档文件。因此,您需要设置untar=True

使用tf.keras.utils.get_file时,默认情况下会在~/.keras/datasets目录中找到下载的数据。

在 Mac 或 Linux 系统的 Jupyter Notebook 单元格中执行:

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

您将找到如图 3-6 所示的花卉数据集。

花卉数据集文件夹

图 3-6. 花卉数据集文件夹

现在让我们看看其中一种花卉:

!ls -lrt ~/.keras/datasets/flower_photos/roses | head -10

您应该看到前九个图像,如图 3-7 所示。

玫瑰目录中的十个示例图像文件

图 3-7. 玫瑰目录中的九个示例图像文件

这些图像都是不同的尺寸。您可以通过检查几张图像来验证这一点。以下是一个您可以利用的辅助函数,用于显示原始尺寸的图像:¹

def display_image_in_actual_size(im_path):

    dpi = 100
    im_data = plt.imread(im_path)
    height, width, depth = im_data.shape
    # What size does the figure need to be in inches to fit 
    # the image?
    figsize = width / float(dpi), height / float(dpi)
    # Create a figure of the right size with one axis that 
    # takes up the full figure
    fig = plt.figure(figsize=figsize)
    ax = fig.add_axes([0, 0, 1, 1])
    # Hide spines, ticks, etc.
    ax.axis('off')
    # Display the image.
    ax.imshow(im_data, cmap='gray')
    plt.show()

让我们用它来显示一张图像(如图 3-8 所示):

IMAGE_PATH = "/root/.keras/datasets/flower_photos/roses/
7409458444_0bfc9a0682_n.jpg"
display_image_in_actual_size(IMAGE_PATH)

玫瑰图像样本 1

图 3-8。玫瑰图像示例 1

现在尝试不同的图像(如图 3-9 所示):

IMAGE_PATH = "/root/.keras/datasets/flower_photos/roses/
5736328472_8f25e6f6e7.jpg"
display_image_in_actual_size(IMAGE_PATH)

玫瑰图像示例 2

图 3-9。玫瑰图像示例 2

显然,这些图像的尺寸和长宽比是不同的。

将图像转换为固定规格

现在您已经准备好将这些图像转换为固定规格。在这个特定的示例中,您将使用 ResNet 输入图像规格,即 224×224,带有三个颜色通道(RGB)。此外,尽可能使用数据流。因此,您的目标是将这些彩色图像转换为 224×224 像素的形状,并从中构建一个数据集,以便流式传输到训练范式中。

为了实现这一点,您将使用ImageDataGenerator类和flow_from_directory方法。

ImageDataGenerator负责创建一个生成器对象,该对象从由flow_from_directory指定的目录中生成流式数据。

一般来说,编码模式是:

my_datagen = tf.keras.preprocessing.image.ImageDataGenerator(
    **datagen_kwargs)
my_generator = my_datagen.flow_from_directory(
data_dir, **dataflow_kwargs)

在这两种情况下,关键字参数选项或kwargs为您的代码提供了很大的灵活性。(关键字参数在 Python 中经常见到。)这些参数使您能够将可选参数传递给函数。事实证明,在ImageDataGenerator中,有两个与您需求相关的参数:rescalevalidation_splitrescale参数用于将像素值归一化为有限范围,validation_split允许您将数据的一个分区细分,例如用于交叉验证。

flow_from_directory中,有三个对于本示例有用的参数:target_sizebatch_sizeinterpolationtarget_size参数帮助您指定每个图像的期望尺寸,batch_size用于指定批量图像中的样本数。至于interpolation,请记住您需要对每个图像进行插值或重新采样,以达到用target_size指定的规定尺寸?插值的支持方法有nearestbilinearbicubic。对于本示例,首先尝试bilinear

您可以将这些关键字参数定义如下。稍后将把它们传递给它们的函数调用:

pixels =224
BATCH_SIZE = 32
IMAGE_SIZE = (pixels, pixels)

datagen_kwargs = dict(rescale=1./255, validation_split=.20)
dataflow_kwargs = dict(target_size=IMAGE_SIZE, 
batch_size=BATCH_SIZE,
interpolation="bilinear")

创建一个生成器对象:

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

现在您可以指定此生成器将从中流式传输数据的源目录。此生成器将仅流式传输 20%的数据,并将其指定为验证数据集:

valid_generator = valid_datagen.flow_from_directory(
    data_dir, subset="validation", shuffle=False, 
    **dataflow_kwargs)

您可以使用相同的生成器对象进行训练数据:

train_datagen = valid_datagen
train_generator = train_datagen.flow_from_directory(
data_dir, subset="training", shuffle=True, **dataflow_kwargs)

检查生成器的输出:

for image_batch, labels_batch in train_generator:
  print(image_batch.shape)
  print(labels_batch.shape)
  break

(32, 224, 224, 3)
(32, 5)

输出表示为 NumPy 数组。对于一批图像,样本大小为 32,高度和宽度为 224 像素,三个通道表示 RGB 颜色空间。对于标签批次,同样有 32 个样本。每行都是独热编码,表示属于五类中的哪一类。

另一个重要的事情是检索标签的查找字典。在推断期间,模型将输出每个五类中的概率。唯一的解码方式是使用标签的预测查找字典来确定哪个类具有最高的概率:

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

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

我们分类模型的典型输出将类似于以下的 NumPy 数组:

(0.7, 0.1, 0.1, 0.05, 0.05)

具有最高概率值的位置是第一个元素。将此索引映射到idx_labels中的第一个键 - 在本例中为daisy。这是您捕获预测结果的方法。保存idx_labels字典:

import pickle
with open('prediction_lookup.pickle', 'wb') as handle:
    pickle.dump(idx_labels, handle, 
    protocol=pickle.HIGHEST_PROTOCOL)

这是如何加载它的方法:

with open('prediction_lookup.pickle', 'rb') as handle:
    lookup = pickle.load(handle)

训练模型

最后,对于训练,您将使用从预训练的 ResNet 特征向量构建的模型。这种技术称为迁移学习。TensorFlow Hub 免费提供许多预训练模型。这是在模型构建过程中访问它的方法:

import tensorflow_hub as hub
NUM_CLASSES = 5
mdl = tf.keras.Sequential([
    tf.keras.layers.InputLayer(input_shape=IMAGE_SIZE + (3,)),
         hub.KerasLayer("https://tfhub.dev/google/imagenet/
resnet_v1_101/feature_vector/4", trainable=False),
tf.keras.layers.Dense(NUM_CLASSES, activation='softmax', 
 name = 'custom_class')
])
mdl.build([None, 224, 224, 3])

第一层是InputLayer。请记住,预期输入为 224×224×3 像素。您将使用元组添加技巧将额外的维度附加到IMAGE_SIZE

IMAGE_SIZE + (3,)

现在您有了(224, 224, 3),这是一个表示图像维度的 NumPy 数组的元组。

下一层是由指向 TensorFlow Hub 的预训练 ResNet 特征向量引用的层。让我们直接使用它,这样我们就不必重新训练它。

接下来是具有五个输出节点的Dense层。每个输出是图像属于该类的概率。然后,您将构建模型骨架,第一个维度为None。这意味着第一个维度,代表批处理的样本大小,在运行时尚未确定。这是如何处理批输入的方法。

检查模型摘要以确保它符合您的预期:

mdl.summary()

输出显示在图 3-10 中。

图像分类模型摘要

图 3-10. 图像分类模型摘要

使用optimizers和相应的losses函数编译模型:

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

然后对其进行训练:

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

您可能会看到类似于图 3-11 的输出。

训练图像分类模型的输出

图 3-11. 训练图像分类模型的输出

摘要

在本节中,您学习了如何处理图像文件。具体来说,在设计模型之前,有必要确保您已经设置了一个预定的图像大小要求。一旦这个标准被接受,下一步就是将图像重新采样到该大小,并将像素的值归一化为更小的动态范围。这些例程几乎是通用的。此外,将图像流式传输到训练工作流程是最有效的方法和最佳实践,特别是在您的工作样本大小接近 Python 运行时内存的情况下。

为处理文本数据做准备

对于文本数据,每个单词或字符都需要表示为一个数字整数。这个过程被称为标记化。此外,如果目标是分类,那么目标需要被编码为类别。如果目标是更复杂的,比如翻译,那么训练数据中的目标语言(比如英语到法语翻译中的法语)也需要自己的标记化过程。这是因为目标本质上是一个长字符串的文本,就像输入文本一样。同样,您还需要考虑是在单词级别还是字符级别对目标进行标记化。

文本数据可以以许多不同的格式呈现。从内容组织的角度来看,它可以被存储和组织为一个表格,其中一列包含文本的主体或字符串,另一列包含标签,例如二进制情感指示器。它可能是一个自由格式的文件,每行长度不同,每行末尾有一个换行符。它可能是一份手稿,其中文本块由段落或部分定义。

有许多方法可以确定要使用的处理技术和逻辑,当您设置自然语言处理(NLP)机器学习问题时;本节将涵盖一些最常用的技术。

这个例子将使用威廉·莎士比亚的悲剧《科里奥兰纳斯》中的文本,这是一个简单的公共领域示例,托管在谷歌上。您将构建一个文本生成模型,该模型将学习如何以莎士比亚的风格写作。

对文本进行标记化

文本由字符字符串表示。这些字符需要转换为整数以进行建模任务。这个例子是科里奥兰纳斯的原始文本字符串。

让我们导入必要的库并下载文本文件:

import tensorflow as tf
import numpy as np
import os
import time

FILE_URL = 'https://storage.googleapis.com/download.tensorflow.org/
data/shakespeare.txt'
FILE_NAME = 'shakespeare.txt'
path_to_file = tf.keras.utils.get_file('shakespeare.txt', FILE_URL)

打开它并输出几行示例文本:

text = open(path_to_file, 'rb').read().decode(encoding='utf-8')
print ('Length of text: {} characters'.format(len(text)))

通过打印前 400 个字符来检查这段文本:

print(text[:400])

输出显示在图 3-12 中。

为了对该文件中的每个字符进行标记化,简单的set操作就足够了。这个操作将创建在文本字符串中找到的字符的一个唯一集合:

vocabulary = sorted(set(text))
print ('There are {} unique characters'.format(len(vocabulary)))

There are 65 unique characters

图 3-13 展示了vocabulary列表的一瞥。

威廉·莎士比亚《科里奥兰纳斯》的样本

图 3-12. 威廉·莎士比亚《科里奥兰纳斯》的示例

《科里奥兰纳斯》词汇列表的一部分

图 3-13.《科里奥兰纳斯》词汇列表的一部分

这些标记包括标点符号,以及大写和小写字符。不一定需要同时包含大写和小写字符;如果不想要,可以在执行set操作之前将每个字符转换为小写。由于您对标记列表进行了排序,您可以看到特殊字符也被标记化了。在某些情况下,这是不必要的;这些标记可以手动删除。以下代码将所有字符转换为小写,然后执行set操作:

vocabulary = sorted(set(text.lower()))
print ('There are {} unique characters'.format(len(vocabulary)))

There are 39 unique characters

您可能会想知道是否将文本标记化为单词级而不是字符级是否合理。毕竟,单词是对文本字符串的语义理解的基本单位。尽管这种推理是合理的并且有一定的逻辑性,但实际上它会增加更多的工作和问题,而并没有真正为训练过程增加价值或为模型的准确性增加价值。为了说明这一点,让我们尝试按单词对文本字符串进行标记化。首先要认识到的是单词是由空格分隔的。因此,您需要在空格上拆分文本字符串:

vocabulary_word = sorted(set(text.lower().split(' ')))
print ('There are {} unique words'.format(len(vocabulary_word)))

There are 41623 unique words

检查vocabulary_word列表,如图 3-14 所示。

由于每个单词标记中嵌入了特殊字符和换行符,这个列表几乎无法使用。需要通过正则表达式或更复杂的逻辑来清理它。在某些情况下,标点符号附加在单词上。此外,单词标记列表比字符级标记列表要大得多。这使得模型更难学习文本中的模式。出于这些原因和缺乏已证明的好处,将文本标记化为单词级并不是一种常见做法。如果您希望使用单词级标记化,则通常会执行单词嵌入操作以减少话语表示的变异性和维度。

标记化单词示例

图 3-14. 标记化单词的示例

创建字典和反向字典

一旦您有包含所选字符的标记列表,您将需要将每个标记映射到一个整数。这被称为字典。同样,您需要创建一个反向字典,将整数映射回标记。

使用enumerate函数很容易生成一个整数。这个函数以列表作为输入,并返回与列表中每个唯一元素对应的整数。在这种情况下,列表包含标记:

for i, u in enumerate(vocabulary):
  print(i, u)

您可以在图 3-15 中看到这个结果的示例。

标记列表的示例枚举输出

图 3-15. 标记列表的示例枚举输出

接下来,您需要将其制作成一个字典。字典实际上是一组键值对,用作查找表:当您给出一个键时,它会返回与该键对应的值。构建字典的表示法,键是标记,值是整数:

char_to_index = {u:i for i, u in enumerate(vocabulary)}

输出将类似于图 3-16。

这个字典用于将文本转换为整数。在推理时,模型输出也是整数格式。因此,如果希望输出为文本,则需要一个反向字典将整数映射回字符。要做到这一点,只需颠倒iu的顺序:

index_to_char = {i:u for i, u in enumerate(vocabulary)}

字符到索引字典的示例

图 3-16. 字符到索引字典的示例

标记化是大多数自然语言处理问题中最基本和必要的步骤。文本生成模型不会生成纯文本作为输出;它会生成一系列整数作为输出。为了使这一系列索引映射到字母(标记),您需要一个查找表。index_to_char就是专门为此目的构建的。使用index_to_char,您可以通过键查找每个字符(标记),其中键是模型输出的索引。没有index_to_char,您将无法将模型输出映射回可读的纯文本格式。

总结

在本章中,您学习了如何处理一些最常见的数据结构:表格、图像和文本。表格数据集(结构化的、类似 CSV 的数据)非常常见,通常从数据库查询返回,并经常用作训练数据。您学会了如何处理这些结构中不同数据类型的列,以及如何通过交叉感兴趣的列来建模特征交互。

对于图像数据,您学会了在使用整个图像集训练模型之前需要标准化图像大小和像素值,以及需要跟踪图像标签。

文本数据在格式和用途方面是最多样化的数据类型。然而,无论数据是用于文本分类、翻译还是问答模型,标记化和字典构建过程都非常常见。本章描述的方法和方法并不是详尽或全面的;相反,它们代表了处理这些数据类型时的“基本要求”。

¹ 用户 Joe Kington 在StackOverflow上的回答,2016 年 1 月 13 日,2020 年 10 月 23 日访问。

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

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

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

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

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

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

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

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

基本的 TensorFlow Hub 工作流程

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

TensorFlow Hub 主页

图 4-1. TensorFlow Hub 主页

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

pip install --upgrade tensorflow_hub

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

import tensorflow_hub as hub

首先,调用模型:

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

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

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

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

print(embeddings.shape)

应该是:

(4, 128)

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

print(embeddings[0])

文本嵌入输出

图 4-2. 文本嵌入输出

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

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

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

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

迁移学习的一般模式

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

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

输入层

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

输出层

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

可选层

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

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

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

  • 模型要求

  • 数据转换和输入处理

  • TFH 模型实现

  • 输出定义

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

模型要求

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

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

数据转换和输入处理

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

  1. 首先加载库:

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

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

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

    这是将显示的内容:

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

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

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

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

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

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

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

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

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

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

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

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

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

使用 TensorFlow Hub 实现模型

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

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

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

注意这里有几点:

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

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

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

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

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

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

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

然后开始训练过程:

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

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

定义输出

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

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

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

将输出映射到纯文本格式

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

sample_test_images, ground_truth_labels = next(valid_generator)

prediction = model.predict(sample_test_images)

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

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

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

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

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

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

print (predicted_idx)

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

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

def find_label(idx):
    return idx_labels[idx]

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

find_label_batch = np.vectorize(find_label)

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

result = find_label_batch(predicted_idx)

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

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

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

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

文件 预测
0 daisy/100080576_f52e8ee070_n.jpg daisy
1 daisy/10140303196_b88d3d6cec.jpg sunflowers
2 daisy/10172379554_b296050f82_n.jpg daisy
3 daisy/10172567486_2748826a8b.jpg dandelion
4 daisy/10172636503_21bededa75_n.jpg daisy
... ... ...
726 tulips/14068200854_5c13668df9_m.jpg sunflowers
727 tulips/14068295074_cd8b85bffa.jpg roses
728 tulips/14068348874_7b36c99f6a.jpg dandelion
729 tulips/14068378204_7b26baa30d_n.jpg tulips
730 tulips/14071516088_b526946e17_n.jpg dandelion

评估:创建混淆矩阵

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

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

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

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

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

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

花卉图像分类的混淆矩阵

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

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

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

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

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

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

总结

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

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

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

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

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

使用 tf.keras.applications 实现模型

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

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

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

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

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

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

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

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

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

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

从 tf.keras.applications 微调模型

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

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

Number of layers in the base model:  377

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

fine_tune_at = 370

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

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

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

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

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

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

启动训练过程:

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

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

结束

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

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

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

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

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

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

使用 text_dataset_from_directory 函数流式文本文件

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

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

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

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

下载文本数据并设置目录

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

创建数据流水线

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

batch_size = 1024
seed = 123

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

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

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

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

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

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

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

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

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

检查数据集

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

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

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

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

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

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

总结

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

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

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

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

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

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

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

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

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

下载图像并设置目录

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

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

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

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

!unzip -q flower_photos.zip

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

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

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

!ls -alt flower_photos

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

创建数据摄入管道

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

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

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

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

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

文件名 标签
0 7176723954_e41618edc1_n.jpg 向日葵
1 2788276815_8f730bd942.jpg 玫瑰
2 6103898045_e066cdeedf_n.jpg 蒲公英
3 1441939151_b271408c8d_n.jpg 雏菊
4 2491600761_7e9d6776e8_m.jpg 玫瑰
... ... ...
3615 9558628596_722c29ec60_m.jpg 向日葵
3616 4580206494_9386c81ed8_n.jpg 郁金香

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

检查数据集

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

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

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

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

数据集中一些花的图像

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

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

构建和训练 tf.keras 模型

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

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

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

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

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

然后启动训练过程:

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

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

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

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

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

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

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

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

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

加载示例数据和库

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

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

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

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

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

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

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

print(train_images.shape, train_labels.shape)

(60000, 28, 28) (60000,)

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

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

检查 NumPy 数组

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

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

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

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

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

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

为 NumPy 数据构建输入管道

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

train_images = train_images/255

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

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

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

SHUFFLE_BUFFER_SIZE = 10000
TRAIN_BATCH_SIZE = 50
VALIDATION_BATCH_SIZE = 10000

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

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

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

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

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

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

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

现在您可以开始训练:

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

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

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

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

总结

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

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

第六章:模型创建风格

正如您可能想象的那样,构建深度学习模型有多种方法。在前几章中,您了解了tf.keras.Sequential,被称为符号 API,通常是教授模型创建的起点。您可能遇到的另一种 API 风格是命令式 API。符号 API 和命令式 API 都能够构建深度学习模型。

总的来说,选择哪种 API 取决于风格。根据您的编程经验和背景,其中一种可能对您来说更自然。在本章中,您将学习如何使用两种 API 构建相同的模型。具体来说,您将学习如何使用CIFAR-10 图像数据集构建图像分类模型。该数据集包含 10 种常见的类别或图像类别。与之前使用的花卉图像一样,CIFAR-10 图像作为 TensorFlow 分发的一部分提供。然而,花卉图像是 JPEG 格式,而 CIFAR-10 图像是 NumPy 数组。为了将它们流式传输到训练过程中,您将使用from_tensor_slices方法,而不是像在第五章中所做的flow_from_directory方法。

通过使用from_tensor_slices建立数据流程后,您将首先使用符号 API 构建和训练图像分类模型,然后使用命令式 API。您将看到,无论如何构建模型架构,结果都非常相似。

使用符号 API

您已经在本书的示例中看到了符号 APItf.keras.Sequential的工作原理。在tf.keras.Sequential中有一堆层,每个层对输入数据执行特定操作。由于模型是逐层构建的,这是一种直观的方式来设想这个过程。在大多数情况下,您只有一个输入源(在本例中是一系列图像),输出是输入图像的类别。在“使用 TensorFlow Hub 实现模型”中,您学习了如何使用 TensorFlow Hub 构建模型。模型架构是使用顺序 API 定义的,如图 6-1 所示。

顺序 API 模式和数据流

图 6-1。顺序 API 模式和数据流

在本节中,您将学习如何使用此 API 构建和训练一个使用 CIFAR-10 图像的图像分类模型。

加载 CIFAR-10 图像

CIFAR-10 图像数据集包含 10 个类别:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。所有图像大小为 32×32 像素,带有三个通道(RGB)。

首先导入必要的库:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import matplotlib.pylab as plt

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()

此代码将 CIFAR-10 图像下载到您的 Python 运行时中,分为训练集和测试集。您可以使用type语句验证格式:

print(type(train_images))

输出将是一个数据类型:

<class 'numpy.ndarray'>

还重要的是要知道数组的形状,您可以使用以下命令找到:

print(train_images.shape, train_labels.shape)

以下是图像和标签的数组形状:

(50000, 32, 32, 3) (50000, 1)

您可以对测试数据执行相同的操作:

print(test_images.shape, test_labels.shape)

您应该得到以下输出:

(10000, 32, 32, 3) (10000, 1)

从输出中可以看出,CIFAR-10 数据集包含 50,000 个训练图像,每个图像大小为 32×32×3 像素。伴随的 50,000 个标签是一个一维数组,表示图像类别的索引。同样,还有 10,000 个测试图像和相应的标签。标签索引对应以下名称:

CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

因此,索引 0 表示标签“飞机”,而索引 9 表示“卡车”。

检查标签分布

现在是时候找出这些类别的分布并查看一些图像了。通过查看训练标签的分布,可以了解每个类别有多少样本,使用 NumPy 的unique函数:

unique, counts = np.unique(train_labels, return_counts=True)

这将返回每个标签的样本计数。要显示它:

print(np.asarray((unique, counts)))

它将显示以下内容:

[[ 0 1 2 3 4 5 6 7 8 9]
 [5000 5000 5000 5000 5000 5000 5000 5000 5000 5000]]

这意味着每个标签(类)中有 5,000 张图片。训练数据在所有标签之间均匀分布。

同样,您可以验证测试数据的分布:

unique, counts = np.unique(test_labels, return_counts=True)
print(np.asarray((unique, counts)))

输出确认了每个标签有 1,000 张图片:

[[ 0 1 2 3 4 5 6 7 8 9]
 [1000 1000 1000 1000 1000 1000 1000 1000 1000 1000]]

检查图像

让我们看一些图像,以确保它们的数据质量。在这个练习中,您将随机抽样并显示训练数据集中的 50,000 张图像中的 25 张。

TensorFlow 如何进行这种随机选择?图像从 0 到 49,999 进行索引。要从此范围中随机选择有限数量的索引,使用 Python 的random库,该库以 Python 列表作为输入,并从中随机选择有限数量的样本:

selected_elements = random.sample(a_list, 25)

此代码从a_list中随机选择 25 个元素,并将结果存储在selected_elements中。如果a_list对应于图像索引,则selected_elements将包含从a_list中随机抽取的 25 个索引。您将使用selected_elements来访问和显示这 25 张训练图像。

现在您需要创建train_idx,该列表保存训练图像的索引。您将使用 Python 的range函数创建一个包含 0 到 49,999 之间整数的对象:

range(len(train_labels))

前面的代码创建了一个range对象,其中包含从 0 开始到len(train_labels)或列表training_labels的长度的整数。

现在,将range对象转换为 Python 列表:

list(range(len(train_labels)))

这个列表现在已准备好作为 Pythonrandom.sample函数的输入。现在您可以开始编写代码了。

首先,创建train_idx,这是一个从 0 到 49,999 的索引列表:

train_idx = list(range(len(train_labels)))

然后使用random库生成随机选择:

import random
random.seed(2)
random_sel = random.sample(train_idx, 25)

第二行中的种子操作确保您的选择是可重现的,这对于调试很有帮助。您可以为seed使用任何整数。

random_sel列表将保存 25 个随机选择的索引,看起来像这样:

[3706,
 6002,
 5562,
 23662,
 11081,
 48232,
…

现在您可以根据这些索引绘制图像并显示它们的标签:

plt.figure(figsize=(10,10))
for i in range(len(random_sel)):
 plt.subplot(5,5,i+1)
 plt.xticks([])
 plt.yticks([])
 plt.grid(False)
 plt.imshow(train_images[random_sel[i]], cmap=plt.cm.binary)
 plt.xlabel(CLASS_NAMES[train_labels[random_sel[i]][0]])
plt.show()

此代码片段显示了一个包含 25 张图像及其标签的面板,如图 6-2 所示。(由于这是一个随机样本,您的结果会有所不同。)

从 CIFAR-10 数据集中随机选择的 25 张图像

图 6-2。从 CIFAR-10 数据集中随机选择的 25 张图像

构建数据管道

在本节中,您将使用from_tensor_slices构建数据摄入管道。由于只有两个分区,训练和测试,您需要在训练过程中将测试分区的一半保留为交叉验证。选择前 500 个作为交叉验证数据,剩下的 500 个作为测试数据:

validation_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[:500], test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[500:], test_labels[500:]))

此代码基于图像索引创建了两个数据集对象,validation_datasettest_dataset,每个集合中有 500 个样本。

现在为训练数据创建一个类似的数据集对象:

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

这里使用了所有的训练图像。您可以通过计算train_dataset中的样本数量来确认:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

这是预期结果:

Training data sample size: 50000

为训练批处理数据

要完成用于训练的数据摄入管道的设置,您需要将训练数据划分为批次。批次的大小,或训练样本的数量,是模型训练过程中更新模型权重和偏差所需的数量,然后沿着一步减少误差梯度。

使用以下代码对训练数据进行批处理,首先对训练数据集进行洗牌,然后创建多个包含 200 个样本的批次:

TRAIN_BATCH_SIZE = 200
train_dataset = train_dataset.shuffle(50000).batch(
TRAIN_BATCH_SIZE)

同样,您将对交叉验证和测试数据执行相同的操作:

validation_dataset = validation_dataset.batch(500)
test_dataset = test_dataset.batch(500)

STEPS_PER_EPOCH = train_dataset_size // TRAIN_BATCH_SIZE
VALIDATION_STEPS = 1 #validation data // validation batch size

交叉验证和测试数据集各包含一个 500 样本批次。代码设置参数以通知训练过程应该期望多少批次的训练和验证数据。训练数据的参数是STEPS_PER_EPOCH。交叉验证数据的参数是VALIDATION_STEPS,设置为 1,因为数据大小和批次大小都是 500。请注意,双斜杠(//)表示地板除法(即向下取整到最接近的整数)。

现在您的训练和验证数据集已经准备好了,下一步是使用符号 API 构建模型。

构建模型

现在您已经准备好构建模型了。以下是一个使用tf.keras.Sequential类包装的一堆层构建的深度学习图像分类模型的示例代码:

model = tf.keras.Sequential([
 tf.keras.layers.Conv2D(32, kernel_size=(3, 3), activation='relu',
 kernel_initializer='glorot_uniform', padding='same', 
 input_shape = (32,32,3)),
 tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
 tf.keras.layers.Conv2D(64, kernel_size=(3, 3), activation='relu',
 kernel_initializer='glorot_uniform', 
 padding='same'),
 tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
 tf.keras.layers.Flatten(),
 tf.keras.layers.Dense(256, activation='relu', 
 kernel_initializer='glorot_uniform'),
 tf.keras.layers.Dense(10, activation='softmax', 
 name = 'custom_class')
])
model.build([None, 32, 32, 3])

接下来,使用为分类任务指定的损失函数编译模型:

model.compile(
 loss='sparse_categorical_crossentropy',
 optimizer=tf.keras.optimizers.SGD(learning_rate=0.1, 
 momentum=0.9),
 metrics=['accuracy'])

为了想象模型如何通过不同层处理和转换数据,您可能希望绘制模型架构,包括它期望的张量的输入和输出形状。您可以使用以下命令:

tf.keras.utils.plot_model(model, show_shapes=True)

在运行此命令之前,您可能需要安装pydotgraphviz库:

pip install pydot
pip install graphviz

图 6-3 展示了模型架构。问号表示表示样本大小的维度,在执行期间才知道。这是因为模型设计为适用于任何大小的训练样本。处理样本大小所需的内存是无关紧要的,不需要在模型架构级别指定。相反,所需的内存将在训练执行期间定义。

接下来,开始训练过程:

hist = model.fit(
 train_dataset,
 epochs=5, steps_per_epoch=STEPS_PER_EPOCH,
 validation_data=validation_dataset,
 validation_steps=VALIDATION_STEPS).history

您的结果应该与图 6-4 中的结果类似。

这是如何利用tf.keras.Sequential来构建和训练深度学习模型的。正如您所看到的,只要您指定输入和输出形状与图像和标签一致,您可以堆叠任意多的层。训练过程也非常常规;它不偏离您在第五章中看到的内容。

图像分类模型架构

图 6-3. 图像分类模型架构

模型训练结果

图 6-4. 模型训练结果

在我们查看命令式 API 之前,我们将进行一个快速的绕道:您需要了解 Python 中的类继承的一些知识才能理解命令式 API。

理解继承

继承是面向对象编程中使用的一种技术。它使用的概念来封装与特定类型对象相关的属性和方法。它还处理不同类型对象之间的关系。继承是允许特定类使用另一个类中的方法的手段。

通过一个简单的例子更容易理解这个工作原理。想象我们有一个名为vehicle的基类(或父类)。我们还有另一个类truck,它是vehicle子类:这也被称为派生类继承类。我们可以定义vehicle类如下:

class vehicle():
 def __init__(self, make, model, horsepower, weight):
 self.make = make
 self.model = model
 self.horsepower = horsepower
 self.weight = weight

 def horsepower_to_weight_ratio(self, horsepower, weight):
 hp_2_weight_ratio = horsepower / weight
 return hp_2_weight_ratio

这段代码展示了定义类的常见模式。它有一个构造函数__init__,用于初始化类的属性,比如制造商、型号、马力和重量。然后有一个名为horsepower_to_weight_ratio的函数,正如您可能猜到的,它计算车辆的马力重量比(我们将其称为 HW 比)。这个函数也可以被vehicle类的任何子类访问。

现在让我们创建truck,作为vehicle的子类:

class truck(vehicle):
 def __init__(self, make, model, horsepower, weight, payload):
 super().__init__(make, model, horsepower, weight)
 self.payload = payload

 def __call__(self, horsepower, payload):
 hp_2_payload_ratio = horsepower / payload
 return hp_2_payload_ratio

在这个定义中,class truck(vehicle)表示truckvehicle的子类。

在构造函数__init__中,super返回父类vehicle的临时对象给truck类。然后这个对象调用父类的__init__,这使得truck类能够重用父类中定义的相同属性:制造商、型号、马力和重量。然而,卡车还有一个独特的属性:有效载荷。这个属性不是从基类继承的;相反,它是在truck类中定义的。您可以用self.payload = payload定义有效载荷。这里,self关键字指的是这个类的实例。在这种情况下,它是一个truck实例,而payload是您为这个属性定义的任意名称。

接下来是一个__call__函数。这个函数使truck类“可调用”。在我们探讨__call__做什么或类可调用意味着什么之前,让我们定义一些参数并创建一个truck实例:

MAKE = 'Tesla'
MODEL = 'Cybertruck'
HORSEPOWER = 800 #HP
WEIGHT = 3000 #kg
PAYLOAD = 1600 #kg

MyTruck = truck(MAKE, MODEL, HORSEPOWER, WEIGHT, PAYLOAD)

为了确保这样做得当,请打印这些属性:

print('Make: ', MyTruck.make,
 '\nModel: ', MyTruck.model,
 '\nHorsepower (HP): ', MyTruck.horsepower,
 '\nWeight (kg): ', MyTruck.weight,
 '\nPayload (kg): ', MyTruck.payload)

这应该产生以下输出:

Make: Tesla
Model: Cybertruck
Horsepower (HP): 800
Weight (kg): 3000
Payload (kg): 1600

让一个 Python 类变得可调用意味着什么?假设您是一名砌砖工,需要在卡车上运送重物。对您来说,卡车最重要的属性是其马力与有效载荷比(HP 比率)。幸运的是,您可以创建一个truck对象的实例,并立即计算比率:

MyTruck(HORSEPOWER, PAYLOAD)

输出将是 0.5。

这意味着MyTruck实例实际上有一个与之关联的值。这个值被定义为马力与有效载荷比。这个计算是由truck类的__call__函数完成的,这是 Python 类的内置函数。当这个函数被显式定义为执行某种逻辑时,它几乎像一个函数调用。再看一下这行代码:

MyTruck(HORSEPOWER, PAYLOAD)

如果您只看到这一行,您可能会认为MyTruck是一个函数,而HORSEPOWERPAYLOAD是输入。

通过显式定义__call__方法来计算 HP 比率,您使truck类可调用;换句话说,您使其表现得像一个函数。现在它可以像 Python 函数一样被调用。

接下来我们想要找到我们的对象MyTruck的 HW 比率。您可能会注意到truck类中没有为此定义任何方法。然而,由于父类vehicle中确实有这样一个方法,horsepower_to_weight_ratioMyTruck可以使用这个方法进行计算。这是类继承的演示,子类可以使用父类直接定义的方法。要做到这一点,您可以使用:

MyTruck.horsepower_to_weight_ratio(HORSEPOWER, WEIGHT)

输出是 0.26666666666666666。

使用命令式 API

看过 Python 的类继承如何工作后,您现在可以学习如何使用命令式 API 构建模型。命令式 API 也被称为模型子类 API,因为您构建的任何模型实际上都是从一个“Model”类继承的。如果您熟悉面向对象编程语言,如 C#、C++或 Java,那么命令式风格应该感觉很熟悉。

将模型定义为一个类

在前面的部分中,您如何定义您构建的模型为一个类?让我们看看代码:

class myModel(tf.keras.Model):
 def __init__(self, input_dim):
 super(myModel, self).__init__()
 self.conv2d_initial = tf.keras.layers.Conv2D(32, 
 kernel_size=(3, 3),
 activation='relu',
 kernel_initializer='glorot_uniform',
 padding='same',
 input_shape = (input_dim,input_dim,3))
 self.cov2d_mid = tf.keras.layers.Conv2D(64, kernel_size=(3, 3),
 activation='relu',
 kernel_initializer='glorot_uniform',
 padding='same')
 self.maxpool2d = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))
 self.flatten = tf.keras.layers.Flatten()
 self.dense = tf.keras.layers.Dense(256, activation='relu',
 kernel_initializer='glorot_uniform')
 self.fc = tf.keras.layers.Dense(10, activation='softmax',
 name = 'custom_class')

 def call(self, input_dim):
 x = self.conv2d_initial(input_dim)
 x = self.maxpool2d(x)
 x = self.cov2d_mid(x)
 x = self.maxpool2d(x)
 x = self.flatten(x)
 x = self.dense(x)
 x = self.fc(x)

 return x

正如前面的代码所示,myModel类从父类tf.keras.Model继承,就像我们的truck类从父类vehicle继承一样。

模型中的层被视为myModel类中的属性。这些属性在构造函数__init__中定义。(回想一下,属性是参数,如马力、制造商和型号,而层是通过语法定义的,如tf.keras.layers.Conv2D。)对于模型中的第一层,代码是:

self.conv2d_initial = tf.keras.layers.Conv2D(32, 
 kernel_size=(3, 3),
 activation='relu',
 kernel_initializer='glorot_uniform',
 padding='same',
 input_shape = (input_dim,input_dim,3))

正如您所看到的,分配层只需要一个名为conv2d_initial的对象。在这个定义中的另一个重要元素是,您可以将用户定义的参数传递给属性。在这里,构造函数__init__期望用户提供一个参数input_dim,它将传递给input_shape参数。

这种风格的好处在于,如果您想要为其他类型的图像尺寸重用此模型架构,您无需创建新模型;只需将图像尺寸作为用户参数传递给此类,您将获得一个可以处理您选择的图像尺寸的类的实例。实际上,您可以向构造函数的输入添加更多用户参数,并将它们传递到对象的不同部分,比如kernel_size。这是面向对象编程风格促进代码重用的一种方式。

让我们再看一下另一个层的定义:

self.maxpool2d = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))

这个层将在模型架构中多次使用,但您只需要定义一次。但是,如果您需要不同的超参数值,比如不同的pool_size,那么您需要创建另一个属性:

self.maxpool2d_2 = tf.keras.layers.MaxPooling2D(pool_size=(5, 5))

在这里,没有必要这样做,因为我们的模型架构重用了maxpool2d

现在让我们看一下call函数。回想一下,通过处理 Python 内置的__call__函数中的某些类型的逻辑或计算,您可以使一个类可调用。在类似的精神中,TensorFlow 创建了一个内置的call函数,使模型类可调用。在这个函数内部,您可以看到层的顺序与顺序 API 中的顺序相同(如您在“构建模型”中看到的)。唯一的区别是这些层现在由类属性表示,而不是硬编码的层定义。

此外,请注意,在以下输入中,用户参数input_dim被传递给属性:

def call(self, input_dim)

这可以根据您的图像尺寸要求为您的模型提供灵活性和可重用性。

call函数中,对象x被用来迭代表示模型层。在声明最终层self.fc(x)之后,它将x作为模型返回。

要创建一个处理 32×32 像素 CIFAR-10 图像尺寸的模型实例,请将实例定义为:

mdl = myModel(32)

此代码创建了一个myModel实例,并用 CIFAR-10 数据集的图像尺寸进行初始化。这个模型表示为mdl对象。接下来,就像您在“构建模型”中所做的那样,您必须使用相同的语法指定损失函数和优化器选择:

mdl.compile(loss='sparse_categorical_crossentropy',
 optimizer=tf.keras.optimizers.SGD(learning_rate=0.1, 
 momentum=0.9),
 metrics=['accuracy'])

现在您可以启动训练例程:

mdl_hist = mdl.fit(
 train_dataset,
 epochs=5, steps_per_epoch=STEPS_PER_EPOCH,
 validation_data=validation_dataset,
 validation_steps=VALIDATION_STEPS).history

您可以期望与图 6-5 中的训练结果类似的训练结果。

命令式 API 模型训练结果

图 6-5。命令式 API 模型训练结果

使用符号 API 和命令式 API 训练的模型应该产生类似的训练结果。

选择 API

您已经看到符号 API 和命令式 API 可以用来构建具有相同架构的模型。在大多数情况下,您选择 API 的依据将基于您喜欢的风格和对语法的熟悉程度。然而,值得注意的是有一些值得注意的权衡。

符号 API 的最大优势是其代码可读性,这使得维护更容易。可以直观地看到模型架构,并且可以看到输入数据通过不同层的张量流动,就像一个图一样。使用符号 API 构建的模型还可以利用tf.keras.utils.plot_model来显示模型架构。通常,这是我们设计深度学习模型时的起点。

当涉及到实现模型架构时,命令式 API 绝对不像符号 API 那样直接。正如您所了解的,这种风格源自类继承的面向对象编程技术。如果您更喜欢将模型视为一个对象而不是一堆操作层,您可能会发现这种风格更直观,如图 6-6 所示。

TensorFlow 模型的命令式 API(也称为模型子类化)

图 6-6。TensorFlow 模型的命令式 API(也称为模型子类化)

实质上,您构建的任何模型都是基本模型tf.keras.Model扩展或继承类。因此,当您构建一个模型时,实际上只是创建了一个继承了基本模型所有属性和函数的类的实例。要适应不同维度的图像模型,您只需使用不同的超参数实例化它。如果重用相同的模型架构是您的工作流程的一部分,那么命令式 API 是保持代码清洁简洁的明智选择。

使用内置训练循环

到目前为止,您已经看到启动模型训练过程所需的只是fit函数。这个函数为您包装了许多复杂的操作,如图 6-7 所示。

内置训练循环中的要素

图 6-7。内置训练循环中的要素

模型对象包含有关架构、损失函数、优化器和模型指标的信息。在fit中,您提供训练和验证数据,要训练的时期数,以及多久更新模型参数并使用验证数据进行测试。

这就是您需要做的全部。内置训练循环知道当一个训练时期完成时,是时候使用批处理验证数据执行交叉验证了。这很方便清晰,使您的代码非常易于维护。输出在每个时期结束时产生,如图 6-4 和 6-5 所示。

如果您需要查看训练过程的详细信息,例如在时期结束之前每个增量改进步骤中的模型准确性,或者如果您想要创建自己的训练指标,那么您需要构建自己的训练循环。接下来,我们将看看这是如何工作的。

创建和使用自定义训练循环

使用自定义训练循环,您失去了fit函数的便利性;相反,您需要编写代码来编排训练过程。假设您想要在每个步骤中监视模型参数在一个时期内的准确性。您可以从“构建模型”中重用模型对象(model)。

创建循环的要素

首先,创建优化器和损失函数对象:

optimizer = tf.keras.optimizers.SGD(
learning_rate=0.1, 
 momentum=0.9)
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(
from_logits=True)

然后创建代表模型指标的对象:

train_acc_metric = tf.keras.metrics.SparseCategoricalAccuracy()
val_acc_metric = tf.keras.metrics.SparseCategoricalAccuracy()

这段代码为模型准确性创建了两个对象:一个用于训练数据,一个用于验证数据。使用SparseCategoricalAccuracy函数是因为输出是一个计算预测与标签匹配频率的指标。

接下来,对于训练,您需要创建一个函数:

@tf.function
def train_step(train_data, train_label):
    with tf.GradientTape() as tape:
    logits = model(train_data, training=True)
    loss_value = loss_fn(train_label, logits)
    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))
    train_acc_metric.update_state(train_label, logits)
    return loss_value

在前面的代码中,@tf.function是一个 Python 装饰器,它将一个以张量作为输入的函数转换为一个可以加速函数执行的形式。这个函数还包括一个新对象tf.GradientTape。在这个范围内,TensorFlow 为您执行梯度下降算法;它通过不同 iating 每个节点中的训练权重相对于损失函数的梯度来自动计算梯度。

以下行指示GradientTape对象的范围:

with tf.GradientTape() as tape

接下来的代码行表示您调用model将训练数据映射到一个输出(logits):

logits = model(train_data, training=True)

现在计算损失函数的输出,将模型输出与真实标签train_label进行比较:

loss_value = loss_fn(train_label, logits)

然后使用模型的参数(trainable_weights)和损失函数的值(loss_value)来计算梯度并更新模型的准确性。

您需要对验证数据执行相同的操作:

@tf.function
def test_step(validation_data, validation_label):
 val_logits = model(validation_data, training=False)
 val_acc_metric.update_state(validation_label, val_logits)

将要素组合在一起形成自定义训练循环

现在您已经拥有所有的要素,可以开始创建自定义训练循环了。以下是一般的步骤:

  1. 使用for循环来迭代每个时期。

  2. 在每个时期内,使用另一个for循环来迭代数据集中的每个批次。

  3. 在每个批次中,打开一个GradientTape对象范围。

  4. 在范围内,计算损失函数。

  5. 在范围外,检索模型权重的梯度。

  6. 使用优化器根据梯度值更新模型权重。

以下是自定义训练循环的代码片段:

import time

epochs = 2
for epoch in range(epochs):
 print("\nStarting epoch %d" % (epoch,))
 start_time = time.time()

 # Iterate dataset batches
 for step, (x_batch_train, y_batch_train) in 
 enumerate(train_dataset):
 loss_value = train_step(x_batch_train, y_batch_train)

    # In every 100 batches, log results.
    if step % 100 == 0:
         print(
         "Training loss (for one batch) at step %d: %.4f"
         % (step, float(loss_value))
         )
 print("Sample processed so far: %d samples" % 
 ((step + 1) * TRAIN_BATCH_SIZE))

 # Show accuracy metrics after each epoch is completed
 train_accuracy = train_acc_metric.result()
 print("Training accuracy over epoch: %.4f" % 
 (float(train_accuracy),))

 # Reset training metrics before next epoch starts
 train_acc_metric.reset_states()

 # Test with validation data at end of each epoch
 for x_batch_val, y_batch_val in validation_dataset:
 test_step(x_batch_val, y_batch_val)

 val_accuracy = val_acc_metric.result()
 val_acc_metric.reset_states()
 print("Validation accuracy: %.4f" % (float(val_accuracy),))
 print("Time taken: %.2fs" % (time.time() - start_time))

图 6-8 显示了执行自定义训练循环的典型输出。

执行自定义训练循环的输出

图 6-8。执行自定义训练循环的输出

正如您所看到的,每个 200 个样本批次结束时,训练循环会计算并显示损失函数的值,让您可以查看训练过程内部发生的情况。如果您需要这种可见性,构建自己的自定义训练循环将提供它。只需知道,这比fit函数的便捷内置训练循环需要更多的努力。

总结

在本章中,您学习了如何使用符号和命令式 API 在 TensorFlow 中构建深度学习模型。通常情况下,两者都能够实现相同的架构,特别是当数据从输入到输出以直线流动时(意味着没有反馈或多个输入)。您可能会看到使用命令式 API 的复杂架构和定制实现的模型。选择适合您情况、方便和可读性的 API。

无论您选择哪种方式,您都将使用内置的fit函数以相同的方式训练模型。fit函数执行内置的训练循环,并让您不必担心如何实际编排训练过程。诸如计算损失函数、将模型输出与真实标签进行比较以及使用梯度值更新模型参数等细节都在幕后为您处理。您将看到的是每个时代结束时的结果:模型相对于训练数据和交叉验证数据的准确性。

如果您需要查看每个批次训练数据中模型的准确性等时代内部发生的情况,那么您需要编写自己的训练循环,这是一个相当费力的过程。

在下一章中,您将看到模型训练过程中提供的其他选项,这些选项提供了更多的灵活性,而无需进行自定义训练循环的复杂编码过程。

第七章:监控训练过程

在上一章中,您学习了如何启动模型训练过程。在本章中,我们将介绍过程本身。

在本书中,我使用了相当直接的例子来帮助您理解每个概念。然而,在 TensorFlow 中运行真实的训练过程时,事情可能会更加复杂。例如,当出现问题时,您需要考虑如何确定您的模型是否过拟合训练数据(当模型学习并记忆训练数据和训练数据中的噪声以至于负面影响其学习新数据时就会发生过拟合)。如果是,您需要设置交叉验证。如果不是,您可以采取措施防止过拟合。

在训练过程中经常出现的其他问题包括:

  • 在训练过程中应该多久保存一次模型?

  • 在过拟合发生之前,我应该如何确定哪个时期给出了最佳模型?

  • 我如何跟踪模型性能?

  • 如果模型没有改进或出现过拟合,我可以停止训练吗?

  • 有没有一种方法可以可视化模型训练过程?

TensorFlow 提供了一种非常简单的方法来解决这些问题:回调函数。在本章中,您将学习如何快速使用回调函数来监视训练过程。本章的前半部分讨论了ModelCheckpointEarlyStopping,而后半部分侧重于 TensorBoard,并向您展示了几种调用 TensorBoard 和使用它进行可视化的技巧。

回调对象

TensorFlow 的回调对象是一个可以执行由tf.keras提供的一组内置函数的对象。当训练过程中发生某些事件时,回调对象将执行特定的代码或函数。

使用回调是可选的,因此您不需要实现任何回调对象来训练模型。我们将看一下最常用的三个类:ModelCheckpointEarlyStopping和 TensorBoard。¹

ModelCheckpoint

ModelCheckpoint类使您能够在训练过程中定期保存模型。默认情况下,在每个训练时期结束时,模型的权重和偏差会被最终确定并保存为权重文件。通常,当您启动训练过程时,模型会从该时期的训练数据中学习并更新权重和偏差,这些权重和偏差会保存在您在开始训练过程之前指定的目录中。然而,有时您只想在模型从上一个时期改进时保存模型,以便最后保存的模型始终是最佳模型。为此,您可以使用ModelCheckpoint类。在本节中,您将看到如何在模型训练过程中利用这个类。

让我们尝试在第六章中使用的 CIFAR-10 图像分类数据集中进行。像往常一样,我们首先导入必要的库,然后读取 CIFAR-10 数据:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import matplotlib.pylab as plt
import os
from datetime import datetime

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()

首先,将图像中的像素值归一化为 0 到 1 的范围:

train_images, test_images = train_images / 255.0, 
test_images / 255.0

该数据集中的图像标签由整数组成。使用 NumPy 命令验证这一点:

np.unique(train_labels)

这显示的值为:

array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)

现在您可以将这些整数映射到纯文本标签。这里提供的标签(由 Alex Krizhevsky、Vinod Nair 和 Geoffrey Hinton 提供)按字母顺序排列。因此,airplanetrain_labels中映射为 0,而truck映射为 9:

CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

由于test_images有一个单独的分区,从test_images中提取前 500 张图像用于交叉验证,并将其命名为validation_images。剩下的图像将用于测试。

为了更有效地利用计算资源,将test_images的图像和标签从其原生 NumPy 数组格式转换为数据集格式:

validation_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[:500], 
 test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[500:], 
 test_labels[500:]))

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

执行这些命令后,您应该在三个数据集中拥有所有图像:一个训练数据集(train_dataset)、一个验证数据集(validation_dataset)和一个测试数据集(test_dataset)。

知道这些数据集的大小会很有帮助。要找到 TensorFlow 数据集的样本大小,将其转换为列表,然后使用len函数找到列表的长度:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

validation_dataset_size = len(list(validation_dataset.
as_numpy_iterator()))
print('Validation data sample size: ', 
validation_dataset_size)

test_dataset_size = len(list(test_dataset.as_numpy_iterator()))
print('Test data sample size: ', test_dataset_size)

您可以期待以下结果:

Training data sample size:  50000
Validation data sample size:  500
Test data sample size:  9500

接下来,对这三个数据集进行洗牌和分批处理:

TRAIN_BATCH_SIZE = 128
train_dataset = train_dataset.shuffle(50000).batch(
TRAIN_BATCH_SIZE, 
drop_remainder=True)

validation_dataset = validation_dataset.batch(
 validation_dataset_size)
test_dataset = test_dataset.batch(test_dataset_size)

请注意,train_dataset将被分成多个批次。每个批次将包含TRAIN_BATCH_SIZE个样本(在本例中为 128)。每个训练批次在训练过程中被馈送到模型中,以实现对权重和偏差的增量更新。对于验证和测试,不需要创建多个批次。它们将作为一个批次使用,但仅用于记录指标和测试。

接下来,指定多久更新权重和验证一次:

STEPS_PER_EPOCH = train_dataset_size // TRAIN_BATCH_SIZE
VALIDATION_STEPS = 1

前面的代码意味着在模型看到由STEPS_PER_EPOCH指定的训练数据批次数量后,是时候使用验证数据集(作为一个批次)测试模型了。

为此,您首先要定义模型架构:

model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(32, kernel_size=(3, 3), 
      activation='relu',
      kernel_initializer='glorot_uniform', padding='same', 
      input_shape = (32,32,3)),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Conv2D(64, kernel_size=(3, 3), 
     activation='relu',
      kernel_initializer='glorot_uniform', padding='same'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(256, 
     activation='relu', kernel_initializer='glorot_uniform'),
    tf.keras.layers.Dense(10, activation='softmax', 
    name = 'custom_class')
])
model.build([None, 32, 32, 3])

现在,编译模型以确保它设置正确:

model.compile(
          loss=tf.keras.losses.SparseCategoricalCrossentropy(
               from_logits=True),
          optimizer='adam',
          metrics=['accuracy'])

接下来,命名 TensorFlow 应在每个检查点保存模型的文件夹。通常,您会多次重新运行训练例程,可能会觉得每次创建一个唯一的文件夹名称很烦琐。一个简单且经常使用的方法是在模型名称后附加一个时间戳:

MODEL_NAME = 'myCIFAR10-{}'.format(datetime.now().strftime(
"%Y%m%d-%H%M%S"))
print(MODEL_NAME)

前面的命令会产生一个名为myCIFAR10-20210123-212138的名称。您可以将此名称用于检查点目录:

checkpoint_dir = './' + MODEL_NAME
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt-{epoch}")
print(checkpoint_prefix)

前面的命令指定了目录路径为./myCIFAR10-20210123-212138/ckpt-{epoch}。该目录位于当前目录的下一级。{epoch}将在训练期间用 epoch 号进行编码。现在定义myCheckPoint对象:

myCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    monitor='val_accuracy',
    mode='max')

在这里,您指定了 TensorFlow 将在每个 epoch 保存模型的文件路径。您还设置了监视验证准确性。

当您使用回调启动训练过程时,回调将期望一个 Python 列表。因此,让我们将myCheckPoint对象放入 Python 列表中:

myCallbacks = [
    myCheckPoint
]

现在启动训练过程。此命令将整个模型训练历史分配给对象hist,这是一个 Python 字典:

hist = model.fit(
    train_dataset,
    epochs=12,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=myCallbacks).history

您可以使用命令hist['val_accuracy']查看从训练的第一个 epoch 到最后一个 epoch 的交叉验证准确性。显示应该类似于这样:

[0.47200000286102295,
 0.5680000185966492,
 0.6000000238418579,
 0.5899999737739563,
 0.6119999885559082,
 0.6019999980926514,
 0.6100000143051147,
 0.6380000114440918,
 0.6100000143051147,
 0.5699999928474426,
 0.5619999766349792,
 0.5960000157356262]

在这种情况下,交叉验证准确性在一些 epoch 中有所提高,然后逐渐下降。这种下降是过拟合的典型迹象。这里最好的模型是具有最高验证准确性(数组中最高值)的模型。要确定其在数组中的位置(或索引),请使用此代码:

max_value = max(hist['val_accuracy'])
max_index = hist['val_accuracy'].index(max_value)
print('Best epoch: ', max_index + 1)

请记住将max_index加 1,因为 epoch 从 1 开始,而不是 0(与 NumPy 数组索引不同)。输出是:

Best epoch:  8

接下来,通过在 Jupyter Notebook 单元格中运行以下 Linux 命令来查看检查点目录:

!ls -lrt ./cifar10_training_checkpoints

您将看到此目录的内容(如图 7-1 所示)。

在每个检查点保存的模型

图 7-1。在每个检查点保存的模型

您可以重新运行此命令并指定特定目录,以查看特定 epoch 构建的模型(如图 7-2 所示):

!ls -lrt ./cifar10_training_checkpoints/ckpt_8

在检查点 8 保存的模型文件

图 7-2。在检查点 8 保存的模型文件

到目前为止,您已经看到如何使用CheckPoint在每个 epoch 保存模型。如果您只希望保存最佳模型,请指定save_best_only = True

best_only_checkpoint_dir = 
 './best_only_cifar10_training_checkpoints'
best_only_checkpoint_prefix = os.path.join(
best_only_checkpoint_dir, 
"ckpt_{epoch}")

bestCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=best_only_checkpoint_prefix,
    monitor='val_accuracy',
    mode='max',
    save_best_only=True)

然后将bestCheckPoint放入回调列表中:

    bestCallbacks = [
    bestCheckPoint
]

之后,您可以启动训练过程:

best_hist = model.fit(
    train_dataset,
    epochs=12,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=bestCallbacks).history

在这个训练中,而不是保存所有检查点,bestCallbacks只有在验证准确率比上一个 epoch 更好时才保存模型。save_best_only选项允许您在第一个 epoch 之后在模型指标有增量改进时保存检查点(使用monitor指定),因此最后保存的检查点是最佳模型。

要查看您保存的内容,请在 Jupyter Notebook 单元格中运行以下命令:

 !ls -lrt ./best_only_cifar10_training_checkpoints

在图 7-3 中显示了验证准确率逐渐提高的保存模型。

设置 save_best_only 为 True 保存的模型

图 7-3。设置 save_best_only 为 True 的模型

第一个 epoch 的模型始终会被保存。在第三个 epoch,模型在验证准确率上有所改进,因此第三个检查点模型被保存。训练继续进行。第九个 epoch 中验证准确率提高,因此第九个检查点模型是最后一个被保存的目录。训练持续到第 12 个 epoch,没有进一步增加验证准确率的改进。这意味着第九个检查点目录包含了最佳验证准确率的模型。

现在您熟悉了ModelCheckpoint,让我们来看看另一个回调对象:EarlyStopping

EarlyStopping

EarlyStopping回调对象使您能够在达到最终 epoch 之前停止训练过程。通常,如果模型没有改进,您会这样做以节省训练时间。

该对象允许您指定一个模型指标,例如验证准确率,通过所有 epoch 进行监视。如果指定的指标在一定数量的 epoch 后没有改进,训练将停止。

要定义一个EarlyStopping对象,请使用以下命令:

myEarlyStop = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
patience=4)

在这种情况下,您在每个 epoch 监视验证准确率。您将patience参数设置为 4,这意味着如果验证准确率在四个 epoch 内没有改进,训练将停止。

提示

要了解更多自定义提前停止的方法,请参阅TensorFlow 2 文档

要在回调中使用ModelCheckpoint对象实现提前停止,需要将其放入列表中:

myCallbacks = [
    myCheckPoint,
    myEarlyStop
]

训练过程是相同的,但您指定了callbacks=myCallbacks

hist = model.fit(
    train_dataset,
    epochs=20,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=myCallbacks).history

一旦您启动了前面的训练命令,输出应该类似于图 7-4。

训练过程中的提前停止

图 7-4。训练过程中的提前停止

在图 7-4 中显示的训练中,最佳验证准确率出现在第 15 个 epoch,值为 0.7220。再经过四个 epoch,验证准确率没有超过该值,因此在第 19 个 epoch 后停止训练。

总结

ModelCheckpoint类允许您设置条件或频率以在训练期间保存模型,而EarlyStopping类允许您在模型没有改进到您选择的指标时提前终止训练。这些类一起在 Python 列表中指定,并将此列表作为回调传递到训练例程中。

许多其他用于监控训练进度的功能可用(请参阅tf.keras.callbacks.CallbackKeras Callbacks API),但ModelCheckpointEarlyStopping是最常用的两个。

本章的其余部分将深入探讨被称为TensorBoard的流行回调类,它提供了您的训练进度和结果的可视化表示。

TensorBoard

如果您希望可视化您的模型和训练过程,TensorBoard 是您的工具。TensorBoard 提供了一个视觉表示,展示了您的模型参数和指标在训练过程中如何演变。它经常用于跟踪训练周期中的模型准确性。它还可以让您看到每个模型层中的权重和偏差如何演变。就像ModelCheckpointEarlyStopping一样,TensorBoard 通过回调模块应用于训练过程。您创建一个代表Tensorboard的对象,然后将该对象作为回调列表的成员传递。

让我们尝试构建一个对 CIFAR-10 图像进行分类的模型。像往常一样,首先导入库,加载 CIFAR-10 图像,并将像素值归一化到 0 到 1 的范围内:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import matplotlib.pylab as plt
import os
from datetime import datetime

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()

train_images, test_images = train_images / 255.0, 
 test_images / 255.0

定义您的纯文本标签:

CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
               'deer','dog', 'frog', 'horse', 'ship', 'truck']

现在将图像转换为数据集:

validation_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[:500], test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[500:], test_labels[500:]))

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

然后确定用于训练、验证和测试分区的数据大小:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

validation_dataset_size = len(list(validation_dataset.
as_numpy_iterator()))
print('Validation data sample size: ', 
 validation_dataset_size)

test_dataset_size = len(list(test_dataset.as_numpy_iterator()))
print('Test data sample size: ', test_dataset_size)

您的结果应该如下所示:

Training data sample size:  50000
Validation data sample size:  500
Test data sample size:  9500

现在您可以对数据进行洗牌和分批处理:

TRAIN_BATCH_SIZE = 128
train_dataset = train_dataset.shuffle(50000).batch(
TRAIN_BATCH_SIZE, 
drop_remainder=True)

validation_dataset = validation_dataset.batch(
validation_dataset_size)
test_dataset = test_dataset.batch(test_dataset_size)

然后指定参数以设置更新模型权重的节奏:

STEPS_PER_EPOCH = train_dataset_size // TRAIN_BATCH_SIZE
VALIDATION_STEPS = 1

STEPS_PER_EPOCH是一个整数,从train_dataset_sizeTRAIN_BATCH_SIZE之间的除法向下取整得到。(双斜杠表示除法并向下取整到最接近的整数。)

我们将重用我们在“ModelCheckpoint”中构建的模型架构:

model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(32, 
     kernel_size=(3, 3), 
     activation='relu', 
     name = 'conv_1',
     kernel_initializer='glorot_uniform', 
     padding='same', input_shape = (32,32,3)),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Conv2D(64, kernel_size=(3, 3), 
     activation='relu', name = 'conv_2',
      kernel_initializer='glorot_uniform', padding='same'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Conv2D(64, 
     kernel_size=(3, 3), 
     activation='relu', 
     name = 'conv_3',
      kernel_initializer='glorot_uniform', padding='same'),
    tf.keras.layers.Flatten(name = 'flat_1',),
    tf.keras.layers.Dense(64, activation='relu',   
     kernel_initializer='glorot_uniform', 
     name = 'dense_64'),
    tf.keras.layers.Dense(10, 
     activation='softmax', 
     name = 'custom_class')
])
model.build([None, 32, 32, 3])

请注意,这次每个层都有一个名称。为每个层指定一个名称有助于您知道您正在检查哪个层。这不是必需的,但对于在 TensorBoard 中进行可视化是一个好的实践。

现在编译模型以确保模型架构有效,并指定损失函数:

model.compile(
          loss=tf.keras.losses.SparseCategoricalCrossentropy(
               from_logits=True),
          optimizer='adam',
          metrics=['accuracy'])

设置模型名称将在以后有所帮助,当 TensorBoard 让您选择一个模型(或多个模型)并检查其训练结果的可视化时。您可以像在使用ModelCheckpoint时那样在模型名称后附加一个时间戳:

MODEL_NAME =
'myCIFAR10-{}'.format(datetime.now().strftime("%Y%m%d-%H%M%S"))

print(MODEL_NAME)

在这个例子中,MODEL_NAMEmyCIFAR10-20210124-135804。您的将类似。

接下来,设置检查点目录:

checkpoint_dir = './' + MODEL_NAME
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt-{epoch}")
print(checkpoint_prefix)

./myCIFAR10-20210124-135804/ckpt-{epoch}是这个检查点目录的名称。

定义模型检查点:

myCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    monitor='val_accuracy',
mode='max')

接下来,您将定义一个 TensorBoard,然后我们将更仔细地查看这段代码:

myTensorBoard = tf.keras.callbacks.TensorBoard(
log_dir='./tensorboardlogs/{}'.format(MODEL_NAME),
write_graph=True,
write_images=True,
histogram_freq=1)

这里指定的第一个参数是log_dir。这是您想要保存训练日志的路径。如指示,它在当前级别下的一个名为tensorboardlogs的目录中,后面跟着一个名为MODEL_NAME的子目录。随着训练的进行,您的日志将在这里生成和存储,以便 TensorBoard 可以解析它们进行可视化。

参数write_graph设置为 True,这样模型图将被可视化。另一个参数write_images也设置为 True。这确保模型权重将被写入日志,这样您可以可视化它们在训练过程中的变化。

最后,histogram_freq设置为 1。这告诉 TensorBoard 何时按 epoch 创建可视化:1 表示每个 epoch 创建一个可视化。有关更多参数,请参阅 TensorBoard 的文档

最后,您有两个回调对象要设置:myCheckPointmyTensorBoard。要将两者放入 Python 列表中,您只需执行以下操作:

myCallbacks = [
    myCheckPoint,
    myTensorBoard
]

然后将您的myCallbacks列表传递到训练例程中:

hist = model.fit(
    train_dataset,
    epochs=30,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=myCallbacks).history

一旦训练过程完成,有三种方法可以调用 TensorBoard。您可以在您自己的计算机上的 Jupyter Notebook 中的下一个单元格中运行它,也可以在您自己计算机上的命令终端中运行,或者在 Google Colab 中运行。我们将依次查看这些选项。

通过本地 Jupyter Notebook 调用 TensorBoard

如果您选择使用您的 Jupyter Notebook,在下一个单元格中运行以下命令:

!tensorboard --logdir='./tensorboardlogs/'

请注意,在这种情况下,当您指定路径以查找训练日志时,参数是logdir,而不是在定义myTensorBoard时的log_dir

运行上述命令后,您将看到以下内容:

Serving TensorBoard on localhost; to expose to the network, use a
proxy or pass --bind_all
TensorBoard 2.3.0 at http://localhost:6006/ (Press CTRL+C to quit)

正如您所看到的,TensorBoard 正在您当前的计算实例(localhost)上以端口号 6006 运行。

现在打开浏览器,导航至http://localhost:6006,您将看到 TensorBoard 正在运行,如图 7-5 所示。

TensorBoard 可视化

图 7-5. TensorBoard 可视化

正如您所看到的,通过每个时代,准确性和损失都在图表中追踪。训练数据显示为浅灰色,验证数据显示为深灰色。

使用 Jupyter Notebook 单元格的主要优势是方便。缺点是运行!tensorboard命令的单元格将保持活动状态,直到您停止 TensorBoard,您将无法使用此笔记本。

通过本地命令终端调用 TensorBoard

您的第二个选项是在本地环境的命令终端中启动 TensorBoard。如图 7-6 所示,等效命令是:

tensorboard --logdir='./tensorboardlogs/'

从命令终端调用 TensorBoard

图 7-6. 从命令终端调用 TensorBoard

请记住,logdir是由训练回调 API 创建的训练日志的目录路径。前面代码中的命令使用相对路径表示法;如果您愿意,可以使用完整路径。

输出与在 Jupyter Notebook 中看到的完全相同:URL(http://localhost:6006)。使用浏览器打开此 URL 以显示 TensorBoard。

通过 Colab 笔记本调用 TensorBoard

现在是我们的第三个选项。如果您在此练习中使用 Google Colab 笔记本,那么调用 TensorBoard 将与您迄今为止看到的有所不同。您将无法在本地计算机上打开浏览器指向 Colab 笔记本,因为它将在 Google 的云环境中运行。因此,您需要安装 TensorBoard 笔记本扩展。这可以在第一个单元格中完成,当您导入所有库时。只需添加此命令并在第一个 Colab 单元格中运行:

%load_ext tensorboard

完成后,每当您准备调用 TensorBoard(例如在训练完成后),请使用此命令:

%tensorboard --logdir ./tensorboardlogs/

您将看到输出在您的 Colab 笔记本中运行,看起来如图 7-5 所示。

使用 TensorBoard 可视化模型过拟合

当您将 TensorBoard 用作模型训练的回调时,您将获得从第一个时代到最后一个时代的模型准确性和损失的图表。

例如,对于我们的 CIFAR-10 图像分类模型,您将看到类似于图 7-5 中所示的输出。在该特定训练运行中,尽管训练和验证准确性都在提高,损失在减少,但这两个趋势开始趋于平缓,表明进一步的训练时期可能只会带来较小的收益。

还要注意,在此运行中,验证准确性低于训练准确性,而验证损失高于训练损失。这是有道理的,因为模型在训练数据上的表现比在交叉验证数据上测试时更好。

您还可以在 TensorBoard 的 Scalars 选项卡中获得图表,就像图 7-7 中所示的那样。

在图 7-7 中,较暗的线表示验证指标,而较浅灰色的线表示训练指标。训练数据中的模型准确性远高于交叉验证数据,而训练数据中的损失远低于交叉验证数据。

您可能还注意到,交叉验证准确率在第 10 个时期达到峰值,略高于 0.7。之后,验证数据准确率开始下降,而损失开始增加。这是模型过拟合的明显迹象。这些图表告诉您的是,在第 10 个时期之后,您的模型开始记忆训练数据的模式。当遇到新的、以前未见过的数据(如交叉验证图像)时,这并没有帮助。事实上,模型在交叉验证中的表现(准确率和损失)将开始变差。

在 TensorBoard 中显示的模型过拟合

图 7-7. 在 TensorBoard 中显示的模型过拟合

一旦您检查了这些图表,您将知道哪个时期提供了训练过程中最佳的模型。您还将了解模型何时开始过拟合并记忆其训练数据。

如果您的模型仍有改进的空间,就像 图 7-5 中的那个,您可能决定增加训练时期,并在过拟合模式开始出现之前继续寻找最佳模型(参见 图 7-7)。

使用 TensorBoard 可视化学习过程

TensorBoard 中的另一个很酷的功能是权重和偏差分布的直方图。这些显示为训练结果的每个时期。通过可视化这些参数是如何分布的,以及它们的分布随时间如何变化,您可以深入了解训练过程的影响。

让我们看看如何使用 TensorBoard 来检查模型的权重和偏差分布。这些信息将在 TensorBoard 的直方图选项卡中(图 7-8)。

TensorBoard 中的权重和偏差直方图

图 7-8. TensorBoard 中的权重和偏差直方图

左侧是训练过的所有模型的面板。请注意有两个模型被选中。右侧是它们的权重(表示为 kernel_0)和偏差在每个训练时期的分布。每一行的图表示模型中的特定层。第一层被命名为 conv_1,这是您在设置模型架构时给这一层取的名字。

让我们更仔细地检查这些图表。我们将从 conv_1 层开始,如 图 7-9 所示。

conv_1 层中的偏差分布经过训练

图 7-9. conv_1 层中的偏差分布经过训练

在两个模型中,conv_1 层中偏差值的分布从第一个时期(背景)到最后一个时期(前景)肯定发生了变化。方框表明随着训练的进行,这一层的所有节点中开始出现某种偏差分布模式。新值远离零,或整体分布的中心。

让我们也看一看权重的分布。这次,让我们只关注一个模型和一个层:conv_3。这在 图 7-10 中显示。

conv_3 层中的权重分布经过训练

图 7-10. conv_3 层中的权重分布经过训练

值得注意的是,随着训练的进行,分布变得更广泛、更平坦。这可以从直方图从第一个到最后一个时期的峰值计数中看出,从 1.22e+4 到 7.0e+3。这意味着直方图逐渐变得更广泛,有更多的权重被更新为远离零的值(直方图的中心)。

使用 TensorBoard,您可以检查不同层和模型训练运行的组合,看看它们如何受训练过程或模型架构的变化影响。这就是为什么 TensorBoard 经常用于直观检查模型训练过程。

总结

在本章中,您看到了一些用于跟踪模型训练过程的最流行方法。本章介绍了模型检查点的概念,并提供了两种重要的方法来帮助您管理如何在训练过程中保存模型:在每个周期保存模型,或者仅在模型指标有增量改进时保存模型。您还了解到,交叉验证中的模型准确度决定了模型何时开始过拟合训练数据。

在本章中,您了解到的另一个重要工具是 TensorBoard,它可以用来可视化训练过程。TensorBoard 通过训练周期展示基本指标(准确度和损失)的趋势的可视图像。它还允许您检查每个层的权重和偏差分布。所有这些技术都可以通过回调函数轻松实现在训练过程中。

在下一章中,您将看到如何在 TensorFlow 中实现分布式训练,利用诸如 GPU 之类的高性能计算单元,以提供更短的训练时间。

¹ 这里没有涵盖的另外两个常见且有用的功能是LearningRateSchedulerCSVLogger

第八章:分布式训练

训练一个机器学习模型可能需要很长时间,特别是如果您的训练数据集很大或者您正在使用单台机器进行训练。即使您有一个 GPU 卡可供使用,训练一个复杂的模型(如 ResNet50,一个具有 50 个卷积层的计算机视觉模型,用于将对象分类为一千个类别)仍可能需要几周的时间。

减少模型训练时间需要采用不同的方法。您已经看到了一些可用的选项:例如,在第五章中,您学习了如何利用数据管道中的数据集。然后还有更强大的加速器,如 GPU 和 TPU(这些加速器仅在 Google Cloud 中提供)。

本章将介绍一种不同的模型训练方式,称为分布式训练。分布式训练在一组设备(如 CPU、GPU 和 TPU)上并行运行模型训练过程,以加快训练过程。 (在本章中,为了简洁起见,我将把 GPU、CPU 和 TPU 等硬件加速器称为工作节点设备。)阅读完本章后,您将了解如何为分布式训练重构您的单节点训练例程。(到目前为止,您在本书中看到的每个示例都是单节点:也就是说,它们都使用一台带有一个 CPU 的机器来训练模型。)

在分布式训练中,您的模型由多个独立的进程进行训练。您可以将每个进程视为一个独立的训练尝试。每个进程在单独的设备上运行训练例程,使用训练数据的一个子集(称为)。这意味着每个进程使用不同的训练数据。当每个进程完成一个训练周期时,它将结果发送回主例程,该主例程收集和聚合结果,然后向所有进程发出更新。然后,每个进程使用更新后的权重和偏差继续训练。

在我们深入实现代码之前,让我们更仔细地看一看分布式 ML 模型训练的核心。我们将从数据并行性的概念开始。

数据并行性

关于分布式训练,您需要了解的第一件事是如何处理训练数据。分布式训练中的主要架构被称为数据并行性。在这种架构中,您在每个工作节点上运行相同的模型和计算逻辑。每个工作节点使用不同于其他工作节点的数据片段计算损失和梯度,然后使用这些梯度来更新模型参数。然后,每个单独的工作节点中更新的模型将在下一轮计算中使用。这个概念在图 8-1 中有所说明。

设计用于使用这些梯度更新模型的两种常见方法:异步参数服务器和同步 allreduce。我们将依次查看每种方法。

数据并行性架构(改编自 Google I/O 2018 视频中的分布式 TensorFlow 训练)

图 8-1。数据并行性架构(改编自分布式 TensorFlow 训练中的 Google I/O 2018 视频)

异步参数服务器

让我们首先看一下异步参数服务器方法,如图 8-2 所示。

使用异步参数服务器进行分布式训练(改编自 Google I/O 2018 视频中的分布式 TensorFlow 训练)

图 8-2。使用异步参数服务器进行分布式训练。 (改编自分布式 TensorFlow 训练中的 Google I/O 2018 视频)

在图 8-2 中标记为 PS0 和 PS1 的设备是参数服务器;这些服务器保存模型的参数。其他设备被指定为工人,如图 8-2 中标记的那样。

工人们承担了大部分计算工作。每个工人从服务器获取参数,计算损失和梯度,然后将梯度发送回参数服务器,参数服务器使用这些梯度来更新模型的参数。每个工人都独立完成这个过程,因此这种方法可以扩展到使用大量工人。这里的优势在于,如果训练工人被高优先级的生产工作抢占,如果工人之间存在不对称,或者如果一台机器因维护而宕机,都不会影响你的扩展,因为工人们不需要等待彼此。

然而,存在一个缺点:工人可能会失去同步。这可能导致在过时的参数值上计算梯度,从而延迟模型收敛,因此延迟朝着最佳模型的训练。随着硬件加速器的普及和流行,这种方法比同步全局归约实现得更少,接下来我们将讨论同步全局归约。

同步全局归约

随着 GPU 和 TPU 等快速硬件加速器变得更加普遍,同步全局归约方法变得更加常见。

在同步全局归约架构中(如图 8-3 所示),每个工人在自己的内存中保存模型的参数副本。没有参数服务器。相反,每个工人根据一部分训练样本计算损失和梯度。一旦计算完成,工人们相互通信以传播梯度并更新模型参数。所有工人都是同步的,这意味着下一轮计算仅在每个工人接收到新梯度并相应地更新模型参数后才开始。

使用同步全局归约架构进行分布式训练(改编自 Google I/O 2018 视频中的分布式 TensorFlow 培训)

图 8-3. 使用同步全局归约架构的分布式训练(改编自分布式 TensorFlow 培训在 Google I/O 2018 视频中)

在一个连接的集群中,工人之间的处理时间差异不是问题。因此,这种方法通常比异步参数服务器架构更快地收敛到最佳模型。

全局归约是一种算法,它将不同工人的梯度合并在一起。这种算法通过汇总不同工人的梯度值,例如将它们求和,然后将它们复制到不同的工人中。它的实现可以非常高效,因为它减少了同步梯度所涉及的开销。根据工人之间可用的通信类型和架构的拓扑结构,有许多全局归约算法的实现。这种算法的常见实现被称为环形全局归约,如图 8-4 所示。

在环形全局归约实现中,每个工人将其梯度发送给环上的后继者,并从前任者接收梯度。最终,每个工人都会收到合并梯度的副本。环形全局归约能够最优地利用网络带宽,因为它同时使用每个工人的上传和下载带宽。无论是在单台机器上与多个工人一起工作,还是在少量机器上工作,都能快速完成。

环形全局归约实现(改编自 Google I/O 2018 视频中的分布式 TensorFlow 培训)

图 8-4. 环形全局归约实现(改编自分布式 TensorFlow 培训在 Google I/O 2018 视频中)

现在让我们看看如何在 TensorFlow 中完成所有这些。我们将专注于使用同步 allreduce 架构扩展到多个 GPU。您将看到将单节点训练代码重构为 allreduce 有多么容易。这是因为这些高级 API 在幕后处理了许多数据并行性的复杂性和细微差别。

使用tf.distribute.MirroredStrategy

实现分布式训练的最简单方法是使用 TensorFlow 提供的tf.distribute.MirroredStrategy类。(有关 TensorFlow 支持的各种分布式训练策略的详细信息,请参见TensorFlow 文档中的“策略类型”)。正如您将看到的,实现此类仅需要在源代码中进行最小更改,您仍然可以在单节点模式下运行,因此您无需担心向后兼容性。它还负责为您更新权重和偏差、指标和模型检查点。此外,您无需担心如何将训练数据分割为每个设备的碎片。您无需编写代码来处理从每个设备检索或更新参数。您也无需担心如何确保跨设备聚合梯度和损失。分发策略会为您处理所有这些。

我们将简要查看一些代码片段,演示在一个机器上使用多个设备时需要对训练代码进行的更改:

  1. 创建一个对象来处理分布式训练。您可以在源代码的开头执行此操作:

    strategy = tf.distribute.MirroredStrategy()
    

    strategy对象包含一个属性,其中包含机器上可用设备的数量。您可以使用此命令显示您可以使用多少个 GPU 或 TPU:

    print('Number of devices: {}'.format(
     strategy.num_replicas_in_sync))
    

    如果您正在使用 GPU 集群,例如通过 Databricks 或云提供商的环境,您将看到您可以访问的 GPU 数量:

    Number of devices: 2
    

    请注意,Google Colab 仅为每个用户提供一个 GPU。

  2. 将您的模型定义和损失函数包装在strategy范围内。您只需确保模型定义和编译,包括您选择的损失函数,封装在特定范围内:

    with strategy.scope():
      model = tf.keras.Sequential([
      …
      ])
      model.build(…)
      model.compile(
        loss=tf.keras.losses…,
        optimizer=…,
        metrics=…)
    

    这是您需要进行代码更改的唯一两个地方。

tf.distribute.MirroredStrategy类是幕后的工作马。正如您所见,我们创建的strategy对象知道有多少设备可用。这些信息使其能够将训练数据分成不同的碎片,并将每个碎片馈送到特定设备中。由于模型架构包装在此对象的范围内,因此它也保存在每个设备的内存中。这使得每个设备可以在相同的模型架构上运行训练例程,最小化相同的损失函数,并根据其特定的训练数据碎片更新梯度。模型架构和参数被复制,或镜像,在所有设备上。MirroredStrategy类还在幕后实现了环形 allreduce 算法,因此您无需担心从每个设备聚合所有梯度。

该类知道您的硬件设置及其潜力进行分布式训练,因此您无需更改model.fit训练例程或数据摄入方法。保存模型检查点和模型摘要的方式与单节点训练中相同,正如我们在“ModelCheckpoint”中看到的那样。

设置分布式训练

要尝试本章中的分布式训练示例,您需要访问多个 GPU 或 TPU。为简单起见,考虑使用提供 GPU 集群的各种商业平台之一,例如DatabricksPaperspace。其他选择包括主要的云供应商,它们提供各种平台,从托管服务到容器。为了简单起见和易于获取,本章中的示例是在 Databricks 中完成的,这是一个基于云的计算供应商。它允许您设置一个分布式计算集群,可以是 GPU 或 CPU,以运行单节点机器无法处理的重型工作负载。

虽然 Databricks 提供免费的“社区版”,但它不提供访问 GPU 集群的权限;为此,您需要创建一个付费账户。然后,您可以将 Databricks 与您选择的云供应商关联,并使用图 8-5 中显示的配置创建 GPU 集群。我的建议是:完成工作后,下载您的笔记本并删除您创建的集群。

设置 Databricks GPU 集群

图 8-5。设置 Databricks GPU 集群

您可能会注意到图 8-5 中有自动驾驶选项。启用自动缩放选项将根据工作负载的需要自动扩展到更多的工作节点。为了节约成本,我还设置了此集群在 120 分钟不活动后自动终止的选项。(请注意,终止集群并不意味着您已经删除它。它将继续存在并在您的账户中产生一小笔费用,直到您删除它。)完成配置后,点击顶部的“创建集群”按钮。通常需要大约 10 分钟来完成整个过程。

接下来,创建一个笔记本(图 8-6)。

在 Databricks 环境中创建笔记本

图 8-6。在 Databricks 环境中创建笔记本

给您的笔记本命名,确保默认语言设置为 Python,并选择您刚刚创建的集群(图 8-7)。点击“创建”按钮生成一个空白笔记本。

设置您的笔记本

图 8-7。设置您的笔记本

现在确保您的笔记本已连接到 GPU 集群(图 8-8)。

将您的笔记本附加到活动集群

图 8-8。将您的笔记本附加到活动集群

现在继续启动 GPU 集群。然后转到“库”选项卡,点击“安装新”按钮,如图 8-9 所示。

在 Databricks 集群中安装库

图 8-9。在 Databricks 集群中安装库

当出现“安装库”提示时(图 8-10),选择 PyPl 作为库源,在“包”字段中输入**tensorflow-datasets**,然后点击“安装”按钮。

完成后,您将能够使用 TensorFlow 的数据集 API 来完成本章中的示例。在下一节中,您将看到如何使用 Databricks 笔记本来尝试使用您刚刚创建的 GPU 集群进行分布式训练。

在 Databricks 集群中安装 tensorflow-datasets 库

图 8-10。在 Databricks 集群中安装 tensorflow-datasets 库

使用 tf.distribute.MirroredStrategy 的 GPU 集群

在第七章中,您使用 CIFAR-10 图像数据集构建了一个单节点训练的图像分类器。在这个例子中,您将使用分布式训练方法来训练分类器。

像往常一样,您需要做的第一件事是导入必要的库:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import os
from datetime import datetime

现在是创建MirroredStrategy对象以处理分布式训练的好时机:

strategy = tf.distribute.MirroredStrategy()

您的输出应该看起来像这样:

INFO:tensorflow:Using MirroredStrategy with devices 
('/job:localhost/replica:0/task:0/device:GPU:0', 
'/job:localhost/replica:0/task:0/device:GPU:1')

这表明有两个 GPU。您可以使用以下语句确认这一点,就像我们之前做过的那样:

print('Number of devices: {}'.format(
strategy.num_replicas_in_sync))
Number of devices: 2

现在加载训练数据并将每个图像像素范围归一化为 0 到 1 之间:

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()
# Normalize pixel values to be between 0 and 1
train_images, test_images = train_images / 255.0, 
test_images / 255.0

您可以在列表中定义纯文本标签:

# Plain-text name in alphabetical order. See
https://www.cs.toronto.edu/~kriz/cifar.html
CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat', 
               'deer','dog', 'frog', 'horse', 'ship', 'truck']

这些纯文本标签来自CIFAR-10 数据集,按字母顺序排列:“airplane”映射到train_labels中的值 0,而“truck”映射到 9。

由于test_images有一个单独的分区,从test_images中提取前 500 个图像用作验证图像,同时保留其余部分用于测试。此外,为了更有效地使用计算资源,将这些图像和标签从其原生 NumPy 数组格式转换为数据集格式:

validation_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[:500], 
 test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[500:], 
 test_labels[500:]))

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

执行这些命令后,您将拥有训练数据集、验证数据集和测试数据集的所有图像格式。

了解数据集的大小将是很好的。要找出 TensorFlow 数据集的样本大小,将其转换为列表,然后使用len函数找到列表的长度:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

validation_dataset_size = len(list(validation_dataset.
as_numpy_iterator()))
print('Validation data sample size: ', validation_dataset_size)

test_dataset_size = len(list(test_dataset.as_numpy_iterator()))
print('Test data sample size: ', test_dataset_size)

您可以期待这些结果:

Training data sample size:  50000
Validation data sample size:  500
Test data sample size:  9500

接下来,对三个数据集进行洗牌和分批处理:

TRAIN_BATCH_SIZE = 128
train_dataset = train_dataset.shuffle(50000).batch(
TRAIN_BATCH_SIZE, drop_remainder=True)

validation_dataset = validation_dataset.batch(validation_dataset_size)
test_dataset = test_dataset.batch(test_dataset_size)

请注意,train_dataset将被分成多个批次,每个批次包含TRAIN_BATCH_SIZE个样本。每个训练批次在训练过程中被馈送到模型中,以实现对权重和偏差的增量更新。无需为验证和测试创建多个批次:这些将作为一个批次使用,仅用于指标记录和测试。

接下来,指定权重更新和验证应该发生的频率:

STEPS_PER_EPOCH = train_dataset_size // TRAIN_BATCH_SIZE
VALIDATION_STEPS = 1

前面的代码意味着在模型看到STEPS_PER_EPOCH批次的训练数据后,是时候用作一个批次的验证数据集进行测试了。

现在您需要将模型定义、模型编译和损失函数包含在策略范围内:

with strategy.scope():
  model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(32, kernel_size=(3, 3),
activation='relu', name = 'conv_1',
      kernel_initializer='glorot_uniform',
padding='same', input_shape = (32,32,3)),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Conv2D(64, kernel_size=(3, 3),
activation='relu', name = 'conv_2',
      kernel_initializer='glorot_uniform', padding='same'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Flatten(name = 'flat_1'),
    tf.keras.layers.Dense(256, activation='relu',
kernel_initializer='glorot_uniform', name = 'dense_64'),
    tf.keras.layers.Dense(10, activation='softmax',
name = 'custom_class')
  ])
  model.build([None, 32, 32, 3])

  model.compile(
    loss=tf.keras.losses.SparseCategoricalCrossentropy(
    from_logits=True),
    optimizer='adam',
    metrics=['accuracy'])

其余部分与您在第七章中所做的相同。您可以定义目录名称模式以在训练例程中检查点模型:

MODEL_NAME = 'myCIFAR10-{}'.format(datetime.now().strftime(
"%Y%m%d-%H%M%S"))

checkpoint_dir = './' + MODEL_NAME
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt-{epoch}")
print(checkpoint_prefix)

前面的命令指定目录路径类似于./ myCIFAR10-20210302-014804/ckpt-{epoch}

一旦定义了检查点目录,只需将定义传递给ModelCheckpoint。为简单起见,我们只会在训练时代提高模型在验证数据上的准确性时保存检查点:

myCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    monitor='val_accuracy',
    mode='max',
    save_weights_only=True,
    save_best_only = True)

然后将定义包装在一个列表中:

myCallbacks = [
    myCheckPoint
]

现在使用fit函数启动训练例程,就像您在本书中的其他示例中所做的那样:

hist = model.fit(
    train_dataset,
    epochs=12,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=myCallbacks).history

在前面的命令中,hist对象以 Python 字典格式包含有关训练结果的信息。这个字典中感兴趣的属性是val_accuracy项:

hist['val_accuracy']

这将显示从训练的第一个到最后一个时代的验证准确性。从这个列表中,我们可以确定在评分验证数据时具有最高准确性的时代。这就是您要用于评分的模型:

max_value = max(hist['val_accuracy'])
max_index = hist['val_accuracy'].index(max_value)
print('Best epoch: ', max_index + 1)

由于您设置了检查点以保存最佳模型而不是每个时代,一个更简单的替代方法是加载最新的时代:

tf.train.latest_checkpoint(checkpoint_dir)

这将为您提供checkpoint_dir下的最新检查点。从该检查点加载具有该检查点权重的模型如下:

model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))

使用加载了最佳权重的模型对测试数据进行评分:

eval_loss, eval_acc = model.evaluate(test_dataset)
print('Eval loss: {}, Eval Accuracy: {}'.format(
eval_loss, eval_acc))

典型的结果看起来像这样:

1/1 [==============================] - 0s 726us/step
 loss: 1.7533
 accuracy: 0.7069 
Eval loss: 1.753335952758789, 
 Eval Accuracy: 0.706947386264801

摘要

您所看到的是使用分布式 TensorFlow 模型训练的最简单方法。您学会了如何从商业供应商的平台创建一个 GPU 集群,以及如何将单节点训练代码重构为分布式训练例程。此外,您还了解了分布式机器学习的基本知识以及不同的系统架构。

在下一节中,您将学习另一种实现分布式训练的方法。您将使用由 Uber 创建的开源库 Horovod,该库在其核心也利用了环形全局归约算法。虽然这个库需要更多的重构,但如果您想比较训练时间差异,它可能会作为另一个选项为您提供服务。

Horovod API

在上一节中,您学习了 allreduce 的工作原理。您还看到了tf.distribute API 如何在幕后自动化分布式训练的各个方面,因此您只需要创建一个分布式训练对象,并将训练代码包装在对象的范围内。在本节中,您将了解 Horovod,这是一个较旧的分布式训练 API,需要您在代码中处理这些分布式训练的方面。由于tf.distribute很受欢迎且易于使用,Horovod API 通常不是程序员的首选。在这里介绍它的目的是为您提供另一个分布式训练的选项。

与上一节一样,我们将使用 Databricks 作为学习分布式模型训练 Horovod 基础知识的平台。如果您按照我的说明操作,您将拥有一个由两个 GPU 组成的集群。

要了解 Horovod API 的工作原理,您需要了解两个关键参数:每个 GPU 的标识和用于并行训练的进程数。每个参数都分配给一个 Horovod 环境变量,该变量将在您的代码中使用。在这种特殊情况下,您应该有两个 GPU。每个 GPU 将在一个数据片段上进行训练,因此将有两个训练进程。您可以使用以下函数检索 Horovod 环境变量:

等级

等级表示 GPU 的标识。如果有两个 GPU,则一个 GPU 将被指定为等级值 0,另一个 GPU 将被指定为等级值 1。如果有更多 GPU,则指定的等级值将是 2、3 等。

大小

大小表示 GPU 的总数。如果有两个 GPU,则 Horovod 的范围大小为 2,训练数据将被分成两个片段。同样,如果有四个 GPU,则该值将为 4,数据将被分成四个片段。

您将经常看到这两个函数被使用。您可以参考Horovod 文档获取更多详细信息。

实现 Horovod API 的代码模式

在我展示在 Databricks 中运行 Horovod 的完整源代码之前,让我们看一下如何运行 Horovod 训练作业。在 Databricks 中使用 Horovod 进行分布式训练的一般模式是:

from sparkdl import HorovodRunner
hr = HorovodRunner(np=2)
hr.run(train_hvd, checkpoint_path=CHECKPOINT_PATH, 
learning_rate=LEARNING_RATE)

使用 Databricks,您需要根据上述模式运行 Horovod 分布式训练。基本上,您创建一个名为hrHorovodRunner对象,该对象分配两个 GPU。然后,该对象执行一个run函数,该函数将train_hvd函数分发到每个 GPU。train_hvd函数负责在每个 GPU 上执行数据摄入和训练例程。此外,checkpoint_path用于在每个训练时代保存模型,learning_rate用于训练过程的反向传播步骤中使用。

随着每个时代的训练进行,模型的权重和偏差被聚合、更新并存储在 GPU 0 上。learning_rate是由 Databricks 驱动程序指定的另一个参数,并传播到每个 GPU。然而,在使用上述模式之前,您需要组织和实现几个函数,接下来我们将详细介绍。

封装模型架构

Databricks 的主驱动程序的工作是将训练数据和模型架构蓝图分发到每个 GPU。因此,您需要将模型架构包装在一个函数中。当执行hr.run时,train_hvd函数将在每个 GPU 上执行。在train_hvd中,将调用一个类似于这样的模型架构包装函数:

def get_model(num_classes):
  from tensorflow.keras import models
  from tensorflow.keras import layers

  model = tf.keras.Sequential([
  tf.keras.layers.Conv2D(32, 
    kernel_size=(3, 3), 
    activation='relu', 
    name = 'conv_1',
    kernel_initializer='glorot_uniform', 
    padding='same', 
    input_shape = (32,32,3)),
  tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
  tf.keras.layers.Conv2D(64, 
    kernel_size=(3, 3), 
    activation='relu', 
    name = 'conv_2',
    kernel_initializer='glorot_uniform', 
    padding='same'),
  tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
  tf.keras.layers.Flatten(name = 'flat_1'),
  tf.keras.layers.Dense(256, 
    activation='relu', 
    kernel_initializer='glorot_uniform', 
    name = 'dense_64'),
  tf.keras.layers.Dense(num_classes, 
    activation='softmax', 
    name = 'custom_class')
  ])
  model.build([None, 32, 32, 3])
  return model

正如您所看到的,这是您在前一节中使用的相同模型架构,只是包装为一个函数。该函数将在每个 GPU 中将模型对象返回给执行过程。

封装数据分离和分片过程

为了确保每个 GPU 接收到一部分训练数据,您还需要将数据处理步骤封装为一个函数,该函数可以传递到每个 GPU 中,就像模型架构一样。

举个例子,让我们使用相同的数据集 CIFAR-10 来说明如何确保每个 GPU 获得不同的训练数据片段。看一下以下函数:

def get_dataset(num_classes, rank=0, size=1):
  from tensorflow.keras import backend as K
  from tensorflow.keras import datasets, layers, models
  from tensorflow.keras.models import Sequential
  import tensorflow as tf
  from tensorflow import keras
  import horovod.tensorflow.keras as hvd
  import numpy as np

  (train_images, train_labels), (test_images, test_labels) =
      datasets.cifar10.load_data()

  #50000 train samples, 10000 test samples.
  train_images = train_images[rank::size]
  train_labels = train_labels[rank::size]

  test_images = test_images[rank::size]
  test_labels = test_labels[rank::size]

  # Normalize pixel values to be between 0 and 1
  train_images, test_images = train_images / 255.0, 
    test_images / 255.0

  return train_images, train_labels, test_images, test_labels

注意函数签名中的输入参数ranksizerank默认为 0,size默认为 1,因此与单节点训练兼容。在具有多个 GPU 的分布式训练中,每个 GPU 将hvd.rankhvd.size作为输入传递到此函数中。由于每个 GPU 的身份由hvd.rank通过双冒号(::)表示,图像和标签根据从一个记录到下一个跳过多少步来切片和分片。因此,此函数返回的数组train_imagestrain_labelstest_imagestest_labels对于每个 GPU 都是不同的,取决于其hvd.rank。(有关 NumPy 数组跳过和切片的详细解释,请参见此 Colab 笔记本。)

参数同步在工作节点之间

在开始训练之前,重要的是初始化和同步所有工作节点(设备)之间的权重和偏置的初始状态。这是通过一个回调函数完成的:

hvd.callbacks.BroadcastGlobalVariablesCallback(0)

这实际上是将变量状态从排名为 0 的 GPU 广播到所有其他 GPU。

所有工作节点的错误指标需要在每个训练步骤之间进行平均。这是通过另一个回调函数完成的:

hvd.callbacks.MetricAverageCallback()

这也在训练期间传递到回调函数列表中。

最好在早期使用低学习率,然后在前 5 个时期之后切换到首选学习率,您可以通过以下代码指定热身时期的数量来实现:

hvd.callbacks.LearningRateWarmupCallback(warmup_epochs=5)

此外,在训练过程中,当模型指标停止改善时,包括一种减小学习率的方法:

tf.keras.callbacks.ReduceLROnPlateau(patience=10, factor = 0.2)

在这个例子中,如果模型指标在 10 个时期后没有改善,您将开始将学习率降低 0.2 倍。

为了简化事情,我建议将所有这些回调函数放在一个列表中:

callbacks = [
   hvd.callbacks.BroadcastGlobalVariablesCallback(0),
   hvd.callbacks.MetricAverageCallback(),
   hvd.callbacks.LearningRateWarmupCallback(warmup_epochs=5, 
     verbose=1),
   tf.keras.callbacks.ReduceLROnPlateau(patience=10, verbose=1)
]

模型检查点作为回调

如前所述,当所有工作节点完成一个时期的训练后,模型参数将作为检查点保存在排名为 0 的工作节点中。这是使用以下代码片段完成的:

if hvd.rank() == 0:
   callbacks.append(keras.callbacks.ModelCheckpoint(
   filepath=checkpoint_path,
   monitor='val_accuracy',
   mode='max',
   save_best_only = True
   ))

这是为了防止工作节点之间的冲突,确保在模型性能和验证指标方面只有一个真相版本。如前面的代码所示,当save_best_only设置为 True 时,只有在该时期的验证指标优于上一个时期时,模型和训练参数才会被保存。因此,并非所有时期都会导致模型被保存,您可以确保最新的检查点是最佳模型。

梯度聚合的分布式优化器

梯度计算也是分布式的,因为每个工作节点都会执行自己的训练例程并单独计算梯度。您需要聚合然后平均来自不同工作节点的所有梯度,然后将平均值应用于所有工作节点,用于下一步训练。这是通过以下代码片段实现的:

optimizer = tf.keras.optimizers.Adadelta(
lr=learning_rate * hvd.size())
optimizer = hvd.DistributedOptimizer(optimizer)

在这里,hvd.DistributedOptimizer将单节点优化器的签名包装在 Horovod 的范围内。

使用 Horovod API 进行分布式训练

现在让我们看一下在 Databricks 中使用 Horovod API 进行分布式训练的完整实现。此实现使用与“使用类 tf.distribute.MirroredStrategy”中看到的相同数据集(CIFAR-10)和模型架构:

import tensorflow as tf
import horovod.tensorflow.keras as hvd
import os
import time

def get_dataset(num_classes, rank=0, size=1):
  from tensorflow.keras import backend as K
  from tensorflow.keras import datasets, layers, models
  from tensorflow.keras.models import Sequential
  import tensorflow as tf
  from tensorflow import keras
  import horovod.tensorflow.keras as hvd
  import numpy as np

  (train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()
  #50000 train samples, 10000 test samples.
  train_images = train_images[rank::size]
  train_labels = train_labels[rank::size]

  test_images = test_images[rank::size]
  test_labels = test_labels[rank::size]

  # Normalize pixel values to be between 0 and 1
  train_images, test_images = train_images / 255.0, 
    test_images / 255.0

  return train_images, train_labels, test_images, test_labels

前面的代码将在每个工作人员上执行。每个工作人员都会收到自己的train_imagestrain_labelstest_imagestest_labels

以下代码是一个包装模型架构的函数;它将构建到每个工作人员中:

def get_model(num_classes):
  from tensorflow.keras import models
  from tensorflow.keras import layers

  model = tf.keras.Sequential([
  tf.keras.layers.Conv2D(32, 
    kernel_size=(3, 3), 
    activation='relu', 
    name = 'conv_1',
    kernel_initializer='glorot_uniform', 
    padding='same', 
    input_shape = (32,32,3)),
  tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
  tf.keras.layers.Conv2D(64, kernel_size=(3, 3), 
    activation='relu', 
    name = 'conv_2',
    kernel_initializer='glorot_uniform', 
    padding='same'),
  tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
  tf.keras.layers.Flatten(name = 'flat_1'),
  tf.keras.layers.Dense(256, activation='relu', 
     kernel_initializer='glorot_uniform', 
     name = 'dense_64'),
  tf.keras.layers.Dense(num_classes, 
     activation='softmax', 
     name = 'custom_class')
  ])
  model.build([None, 32, 32, 3])
  return model

接下来是主要的训练函数train_hvd,它调用了刚刚展示的两个函数。这个函数相当冗长,所以我会分块解释它。

train_hvd内部,使用命令hvd.init创建并初始化了一个 Horovod 对象。此函数将checkpoint_pathlearning_rate作为输入,用于存储每个时代的模型并在反向传播过程中设置梯度下降的速率。一开始,导入所有库:

def train_hvd(checkpoint_path, learning_rate=1.0):

  # Import tensorflow modules to each worker
  from tensorflow.keras import backend as K
  from tensorflow.keras.models import Sequential
  import tensorflow as tf
  from tensorflow import keras
  import horovod.tensorflow.keras as hvd
  import numpy as np

然后,创建并初始化一个 Horovod 对象,并使用它来访问您的工作人员的配置,以便稍后可以正确分片数据:


# Initialize Horovod
hvd.init()

# Pin GPU to be used to process local rank (one GPU per process)
# These steps are skipped on a CPU cluster
gpus = tf.config.experimental.list_physical_devices('GPU')
for gpu in gpus:
  tf.config.experimental.set_memory_growth(gpu, True)
if gpus:
  tf.config.experimental.set_visible_devices(
  gpus[hvd.local_rank()], 'GPU')

print(' Horovod size (processes): ', hvd.size())

现在您已经创建了hvd对象,使用它来提供工作人员标识(hvd.rank)和并行进程数(hvd.size)给get_dataset函数,该函数将以分片返回训练和验证数据。

一旦您有了这些分片,将它们转换为数据集,以便您可以像在“使用 tf.distribute.MirroredStrategy 的 GPU 集群”中那样流式传输训练数据:


  # Call the get_dataset function you created, this time with the
    Horovod rank and size
  num_classes = 10
  train_images, train_labels, test_images, test_labels = get_dataset(
num_classes, hvd.rank(), hvd.size())

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

对训练和验证数据集进行洗牌和分批处理:

  NUM_CLASSES = len(np.unique(train_labels))
  BUFFER_SIZE = 10000
  BATCH_SIZE_PER_REPLICA = 64
  validation_dataset_size = len(test_labels)
  BATCH_SIZE = BATCH_SIZE_PER_REPLICA * hvd.size()
  train_dataset = train_dataset.repeat().shuffle(BUFFER_SIZE).
    batch(BATCH_SIZE
  validation_dataset = validation_dataset.
  repeat().shuffle(BUFFER_SIZE).
batch(BATCH_SIZE, drop_remainder = True)

  train_dataset_size = len(train_labels)

  print('Training data sample size: ', train_dataset_size)

  validation_dataset_size = len(test_labels)
  print('Validation data sample size: ', validation_dataset_size)

现在定义批量大小、训练步数和训练时代:

TRAIN_DATASET_SIZE = len(train_labels)
STEPS_PER_EPOCH = TRAIN_DATASET_SIZE // BATCH_SIZE_PER_REPLICA
VALIDATION_STEPS = validation_dataset_size // 
  BATCH_SIZE_PER_REPLICA
EPOCHS = 20

使用get_model函数创建一个模型,设置优化器,指定学习率,然后使用适合此分类任务的正确损失函数编译模型。请注意,优化器被DistributedOptimizer包装以进行分布式训练:

model = get_model(10)

# Adjust learning rate based on number of GPUs
optimizer = tf.keras.optimizers.Adadelta(
             lr=learning_rate * hvd.size())

# Use the Horovod Distributed Optimizer
optimizer = hvd.DistributedOptimizer(optimizer)

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

在这里,您将创建一个回调列表,以在工作人员之间同步变量,对梯度进行聚合和平均以进行同步更新,并根据时代或训练性能调整学习率:

# Create a callback to broadcast the initial variable states from
  rank 0 to all other processes.
# This is required to ensure consistent initialization of all 
# workers when training is started with random weights or 
# restored from a checkpoint.
callbacks = [
    hvd.callbacks.BroadcastGlobalVariablesCallback(0),
    hvd.callbacks.MetricAverageCallback(),
    hvd.callbacks.LearningRateWarmupCallback(warmup_epochs=5, 
      verbose=1),
    tf.keras.callbacks.ReduceLROnPlateau(patience=10, 
      verbose=1)
  ]

最后,这是每个时代模型检查点的回调。此回调仅在 0 级工作人员(hvd.rank() == 0)中执行:

# Save checkpoints only on worker 0 to prevent conflicts between 
# workers
if hvd.rank() == 0:
    callbacks.append(keras.callbacks.ModelCheckpoint(
        filepath=checkpoint_path,
    monitor='val_accuracy',
    mode='max',
    save_best_only = True
    ))

现在是最终的fit函数,将启动模型训练例程:

model.fit(train_dataset,
            batch_size=BATCH_SIZE,
            epochs=EPOCHS,
            steps_per_epoch=STEPS_PER_EPOCH,
            callbacks=callbacks,
            validation_data=validation_dataset,
            validation_steps=VALIDATION_STEPS,
           verbose=1)
print('DISTRIBUTED TRAINING DONE')

这结束了train_hvd函数。

在您的 Databricks 笔记本的下一个单元格中,为每个训练时代指定一个检查点目录:

# Create directory
checkpoint_dir = '/dbfs/ml/CIFAR10DistributedDemo/train/{}/'.
format(time.time())
os.makedirs(checkpoint_dir)
print(checkpoint_dir)

checkpoint_dir将看起来像/dbfs/ml/CIFAR10DistributedDemo/train/1615074200.2146788/

在下一个单元格中,继续启动分布式训练例程:

from sparkdl import HorovodRunner

checkpoint_path = checkpoint_dir + '/checkpoint-{epoch}.ckpt'
learning_rate = 0.1
hr = HorovodRunner(np=2)
hr.run(train_hvd, checkpoint_path=checkpoint_path, 
       learning_rate=learning_rate)

在运行器定义中,HorovodRunner(np=2),将进程数指定为每个设置的两个(请参阅“设置分布式训练”),这将设置两个 Standard_NC12 工作人员 GPU。

训练例程完成后,请使用以下命令查看检查点目录:

ls -lrt  /dbfs/ml/CIFAR10DistributedDemo/train/1615074200.2146788/

您应该看到类似于这样的内容:

drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-9.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-8.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-7.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-6.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-5.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-4.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-3.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-2.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-20.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-1.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-19.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-17.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-16.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-15.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-14.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-13.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-11.ckpt/
drwxrwxrwx 2 root root 4096 Mar 6 23:18 checkpoint-10.ckpt/

如果没有模型在先前的时代中有所改进,则会跳过一些检查点。最新的检查点代表具有最佳验证指标的模型。

总结

在本章中,您了解了在具有多个工作人员的环境中使分布式模型训练正常工作所需的内容。在数据并行框架中,有两种主要的分布式训练模式:异步参数服务器和同步 allreduce。如今,由于高性能加速器的普遍可用性,同步 allreduce 更受欢迎。

通过学习如何使用 Databricks GPU 集群执行两种类型的同步 allreduce API:TensorFlow 自己的tf.distribute API 和 Uber 的 Horovod API。TensorFlow 选项提供了最优雅和方便的使用方式,并且需要最少的代码重构,而 Horovod API 需要用户手动处理数据分片、分发管道、梯度聚合和平均以及模型检查点。这两种选项通过确保每个工作节点执行自己的训练,然后在每个训练步骤结束时,在所有工作节点之间同步和一致地更新梯度来执行分布式训练。这是分布式训练的标志。

恭喜!通过学习本章内容,您学会了如何使用云中的一组 GPU 训练具有分布式数据管道和分布式训练例程的深度学习模型。在下一章中,您将学习如何为推理服务一个 TensorFlow 模型。

第九章:为 TensorFlow 模型提供服务

如果您按顺序阅读本书中的章节,现在您已经了解了如何处理数据工程流水线、构建模型、启动训练例程、在每个时代检查点模型,甚至对测试数据进行评分。到目前为止,所有这些任务大多都被捆绑在一起以进行教学。然而,在本章中,您将更多地了解如何根据保存的格式为 TensorFlow 模型提供服务。

这一章与之前的章节之间的另一个重要区别是,您将学习处理测试数据的数据工程编码模式。以前,您看到测试数据和训练数据在相同的运行时转换。然而,作为一个机器学习工程师,您还必须考虑到您的模型部署的情况。

想象一下,您的模型已经加载到 Python 运行时并准备就绪。您有一批样本或一个样本。您需要对输入数据进行哪些操作,以便模型可以接受它并返回预测?换句话说:您有一个模型和原始测试数据;您如何实现转换原始数据的逻辑?在本章中,您将通过几个示例了解如何为模型提供服务。

模型序列化

TensorFlow 模型可以以两种不同的本机格式(没有任何优化)保存:HDF5(h5)或 protobuf(pb)。这两种格式都是 Python 和其他编程语言中的标准数据序列化(保存)格式,用于持久化对象或数据结构;它们不是特定于 TensorFlow 甚至 ML 模型的。在 TensorFlow 2.0 之前,pb 是唯一可用的本机格式。随着 TensorFlow 2.0 的到来,其中 Keras API 是未来的事实高级 API,h5 已成为 pb 的替代品。

如今,这两种格式都可以用于部署,特别是在各种公共云提供商中。这是因为每个云提供商现在都有自己的 API 来包装模型。只要模型保存在您的工作空间中,您就可以通过诸如 RESTful(表述性状态转移)API 之类的网络服务来访问它,这些 API 利用 HTTP 方法在网络上进行请求。因此,无论格式如何,您的模型都可以通过来自客户端程序的 RESTful API 调用进行服务。

让我们从上一章中使用 CIFAR-10 数据构建的图像分类模型开始。如果您还没有完成那一章,可以使用以下代码片段快速构建和训练图像分类器。(顺便说一句,这里的代码是在 Google Colab 中使用一个 GPU 开发的。)

首先导入所有必要的库:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import matplotlib.pylab as plt
import os
from datetime import datetime

加载并标准化图像:

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()

train_images, test_images = train_images / 255.0, 
 test_images / 255.0

定义您的图像标签:

CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
               'deer','dog', 'frog', 'horse', 'ship', 'truck']

将原始图像转换为数据集。由于测试图像的分区可用,以下代码将该分区中的前 500 个图像分离出来,用作每个训练周期结束时用于验证数据集,并将其余部分保留为测试数据集。所有训练图像都被转换为训练数据集:

validation_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[:500], test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[500:], test_labels[500:]))

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

为确保您知道每个数据集的样本大小,请遍历它们并显示样本大小:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

validation_dataset_size = len(list(validation_dataset.
as_numpy_iterator()))
print('Validation data sample size: ', validation_dataset_size)

test_dataset_size = len(list(test_dataset.as_numpy_iterator()))
print('Test data sample size: ', test_dataset_size)

您应该看到以下输出:

Training data sample size:  50000
Validation data sample size:  500
Test data sample size:  9500

为分布式训练定义一个分发策略:

strategy = tf.distribute.MirroredStrategy()
print('Number of devices: {}'.format(
strategy.num_replicas_in_sync))

您应该看到一个可用的 GPU:

Number of devices: 1

现在设置训练的批次大小:

BUFFER_SIZE = 10000
BATCH_SIZE_PER_REPLICA = 64
BATCH_SIZE = BATCH_SIZE_PER_REPLICA * strategy.num_replicas_in_sync

将批次应用于每个数据集,并在评估模型的准确性之前计算每个训练周期的批次数:

train_dataset = train_dataset.repeat().shuffle(BUFFER_SIZE).batch(
                BATCH_SIZE)

validation_dataset = validation_dataset.shuffle(BUFFER_SIZE).batch(
                validation_dataset_size)

test_dataset = test_dataset.batch(test_dataset_size)

STEPS_PER_EPOCH = train_dataset_size // BATCH_SIZE_PER_REPLICA
VALIDATION_STEPS = 1

创建一个名为build_model的函数来定义模型架构,并在分布式训练策略范围内使用分类损失函数、优化器和指标对其进行编译:

def build_model():
  with strategy.scope():
    model = tf.keras.Sequential([
      tf.keras.layers.Conv2D(32, kernel_size=(3, 3), 
        activation='relu', 
        name = 'conv_1',
        kernel_initializer='glorot_uniform', 
        padding='same', 
        input_shape = (32,32,3)),
      tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
      tf.keras.layers.Conv2D(64, kernel_size=(3, 3), 
        activation='relu', 
        name = 'conv_2',
        kernel_initializer='glorot_uniform', 
        padding='same'),
      tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
      tf.keras.layers.Flatten(name = 'flat_1'),
      tf.keras.layers.Dense(256, activation='relu', 
kernel_initializer='glorot_uniform', name = 'dense_64'),
      tf.keras.layers.Dense(10, activation='softmax', 
        name = 'custom_class')
    ])
    model.build([None, 32, 32, 3])

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

现在通过调用build_model创建一个模型实例:

model = build_model()

为文件路径定义一个别名,并保存模型检查点:

MODEL_NAME = 'myCIFAR10-{}'.format(datetime.now().strftime(
"%Y%m%d-%H%M%S"))

使用print语句显示模型名称格式:

print(MODEL_NAME)
myCIFAR10-20210319-214456

现在设置您的检查点目录:

checkpoint_dir = './' + MODEL_NAME
checkpoint_prefix = os.path.join(checkpoint_dir, 
    "ckpt-{epoch}")
print(checkpoint_prefix)

检查点目录将设置为具有以下模式的目录:

./myCIFAR10-20210319-214456/ckpt-{epoch}

在您当前的目录级别中,您将看到一个包含以ckpt-{epoch}为前缀的权重文件的myCIFAR10-20210319-214456目录,其中{epoch}是 epoch 编号。

接下来,定义一个检查点对象。只有在验证数据上的模型性能比上一个 epoch 有所改善时,才在一个 epoch 结束时保存模型权重。将权重保存在相同的目录(myCIFAR10-20210319-214456)中,以便最新保存的检查点权重来自最佳 epoch。这样可以节省时间,因为您不需要确定哪个 epoch 呈现了最佳模型。确保save_weights_onlysave_best_only都设置为 True:

myCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    monitor='val_accuracy',
    mode='max',
    save_weights_only = True,
    save_best_only = True
    )

现在将前面的检查点定义传递到列表中,这是fit函数所需的:

myCallbacks = [
myCheckPoint
]

然后启动训练过程:

model.fit(
    train_dataset,
    epochs=30,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=myCallbacks)

在训练过程中,您可以检查输出。您可能会看到类似于这样的内容:

Epoch 7/30
781/781 [==============================] - 4s 6ms/step 
loss: 0.0202 - accuracy: 0.9972 - val_loss: 10.5573 
val_accuracy: 0.6900
Epoch 8/30
781/781 [==============================] - 4s 6ms/step 
loss: 0.0217 - accuracy: 0.9967 - val_loss: 10.4517 
val_accuracy: 0.7000
Epoch 9/30
781/781 [==============================] - 5s 6ms/step 
loss: 0.0203 - accuracy: 0.9971 - val_loss: 10.4553 
val_accuracy: 0.7080
Epoch 10/30
781/781 [==============================] - 5s 6ms/step 
loss: 0.0232 - accuracy: 0.9966 - val_loss: 11.3774 
val_accuracy: 0.6600
…
Epoch 30/30
781/781 [==============================] - 5s 6ms/step
loss: 0.0221 - accuracy: 0.9971 - val_loss: 11.9106 
val_accuracy: 0.6680
<tensorflow.python.keras.callbacks.History at 0x7fd447ce9a50>

在这个例子中,最高的验证准确率出现在第 9 个 epoch,其中val_accuracy为 0.7080。

检查检查点目录:

!ls -lrt {checkpoint_dir}

您将看到它的内容:

total 87896
-rw-r--r-- 1 root root 12852661 Mar 19 21:45 
ckpt-1.data-00000-of-00001
-rw-r--r-- 1 root root     2086 Mar 19 21:45 ckpt-1.index
-rw-r--r-- 1 root root 12852661 Mar 19 21:45 
ckpt-2.data-00000-of-00001
-rw-r--r-- 1 root root     2086 Mar 19 21:45 ckpt-2.index
-rw-r--r-- 1 root root 12852661 Mar 19 21:45 
ckpt-3.data-00000-of-00001
-rw-r--r-- 1 root root     2086 Mar 19 21:45 ckpt-3.index
-rw-r--r-- 1 root root 12852661 Mar 19 21:45 
ckpt-4.data-00000-of-00001
-rw-r--r-- 1 root root     2086 Mar 19 21:45 ckpt-4.index
-rw-r--r-- 1 root root 12852661 Mar 19 21:46 
ckpt-7.data-00000-of-00001
-rw-r--r-- 1 root root     2086 Mar 19 21:46 ckpt-7.index
-rw-r--r-- 1 root root 12852661 Mar 19 21:46 
ckpt-8.data-00000-of-00001
-rw-r--r-- 1 root root     2086 Mar 19 21:46 ckpt-8.index
-rw-r--r-- 1 root root 12852661 Mar 19 21:46 
ckpt-9.data-00000-of-00001
-rw-r--r-- 1 root root     2086 Mar 19 21:46 ckpt-9.index
-rw-r--r-- 1 root root       69 Mar 19 21:46 checkpoint

由于myCheckpoint对象的save_best_onlysave_weights_only设置为 True,最后一个权重是ckpt-9.data-00000-of-00001

要在单个目录中从所有保存的权重文件中编程地定位最后一个权重文件,您可以使用tf.train API 中的latest_checkpoint函数。运行以下命令:

tf.train.latest_checkpoint(checkpoint_dir)

这是预期的输出:

./myCIFAR10-20210319-214456/ckpt-9

这将标识最后一个权重文件的前缀。然后可以将最佳权重加载到模型中:

model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))

现在您有了最佳模型,可以保存为 h5 或 pb 格式。我们先看 h5。

将模型保存为 h5 格式

高级tf.keras API 使用save函数来保存模型为 h5 格式:

KERAS_MODEL_PATH = "/models/HDF5/tfkeras_cifar10.h5"
model.save(KERAS_MODEL_PATH)

看一下目录:

!ls -lrt {KERAS_MODEL_PATH}

您将看到模型作为一个 h5 文件:

-rw-r--r-- 1 root root 12891752 Mar 20 21:19 
/models/HDF5/tfkeras_cifar10.h5

将来,如果您想重新加载此模型进行评分,只需使用load_model函数。(确保您还导入了本节开头指示的所有库。)

new_h5_model = tf.keras.models.load_model(
'/models/HDF5/tfkeras_cifar10.h5')

为了快速评分,使用您与training_dataset同时准备的test_dataset

new_h5_model.predict(test_dataset)

它将产生这样的结果:

array([[1.77631108e-07, 8.12380506e-07, 9.94834751e-02, ...,
        4.93609463e-04, 1.97697682e-05, 2.55090754e-06],

       [6.76187535e-12, 6.38716233e-11, 1.67756411e-07, ...,
        9.99815047e-01, 1.25759464e-14, 1.24499985e-11]], 
        dtype=float32)

有 9500 个元素;每个元素都是一个概率数组。最大概率的索引映射到CLASS_NAMES中的标签。

将模型保存为 pb 格式

要将相同的模型保存为 pb 格式,您将使用tf.saved_model.save函数:

SAVED_MODEL_PATH = "/models/pb/tfsaved_cifar10"
tf.saved_model.save(model, SAVED_MODEL_PATH)

查看SAVED_MODEL_PATH中的内容:

!ls -lrt {SAVED_MODEL_PATH}

内容应该是这样的:

drwxr-xr-x 2 root root   4096 Mar 20 21:50 variables
drwxr-xr-x 2 root root   4096 Mar 20 21:50 assets
-rw-r--r-- 1 root root 138184 Mar 20 21:50 saved_model.pb

权重文件在 variables 目录中。您可以使用此命令检查它:

!ls -lrt {SAVED_MODEL_PATH}/variables

这是您将看到的内容(或多或少):

-rw-r--r-- 1 root root 12856259 Mar 20 21:50 
variables.data-00000-of-00001
-rw-r--r-- 1 root root     2303 Mar 20 21:50 variables.index

现在您已经看到了将模型保存为 protobuf 时的文件夹结构,让我们看看如何加载模型 protobuf。在这种情况下,您需要从包含saved_model.pb的目录名称加载它:

load_strategy = tf.distribute.MirroredStrategy()
with load_strategy.scope():
  load_options = tf.saved_model.LoadOptions(
              experimental_io_device='/job:localhost')
  loaded_pb = tf.keras.models.load_model(
              SAVED_MODEL_PATH, 
              options=load_options)

如果您仔细查看前面的命令,您会注意到,就像在模型训练中一样,模型加载也是在分布策略范围内完成的。如果您正在运行云 TPU 或 GPU(如Google Colab的情况),请将experimental_io_device设置为 localhost,即保存模型的节点。然后使用tf.keras.models.load_model加载包含saved_model.pb的目录:在这种情况下,它是SAVED_MODEL_PATH

现在使用模型loaded_pbtest_dataset进行评分:

loaded_pb.predict(test_dataset)

您将看到与 h5 模型预测中相同的输出:

array([[1.77631108e-07, 8.12380506e-07, 9.94834751e-02, ...,
        4.93609463e-04, 1.97697682e-05, 2.55090754e-06],

       [6.76187535e-12, 6.38716233e-11, 1.67756411e-07, ...,
        9.99815047e-01, 1.25759464e-14, 1.24499985e-11]], 
dtype=float32)

同样,每个内部括号都是一个测试图像的概率列表。最大概率的索引可以映射到CLASS_NAMES中的正确条目。

正如您所看到的,无论是 h5 还是 pb 格式的模型都可以用于对数据集格式中的测试数据进行评分。该模型还可以对 NumPy 数组格式的测试数据进行评分。回想一下,test_images[500:]是原始的 NumPy 测试数据格式;子集从第 500 张图像开始,一直持续下去(总共 9500 张测试图像)。您可以直接将这个 NumPy 数组传递给模型进行评分:

loaded_pb.predict(test_images[500:])

您将看到与之前相同的输出:

array([[1.77631108e-07, 8.12380506e-07, 9.94834751e-02, ...,
        4.93609463e-04, 1.97697682e-05, 2.55090754e-06],

       [6.76187535e-12, 6.38716233e-11, 1.67756411e-07, ...,
        9.99815047e-01, 1.25759464e-14, 1.24499985e-11]], 
dtype=float32)

选择模型格式

您现在已经看到如何使用 h5 和 pb 模型格式对测试数据进行评分。然而,选择使用哪种格式取决于许多因素。从概念上讲,h5 格式非常容易理解;它由一个模型骨架和权重组成,保存为一个单独的文件。这与pickle对象或文件的工作方式非常相似:只要导入库,您就可以打开包含重新实例化对象所需的一切的单个文件(在这种情况下,您的模型)。这种方法适用于简单的部署,其中运行 Python 运行时的驱动程序可以简单地使用tf.keras.models.load_model加载模型并在测试数据上运行它。

然而,如果模型必须以更复杂的设置运行,则 protobuf 格式是更好的选择。这是因为 pb 格式是与编程语言无关的:它可以被许多其他编程语言读取,除了 Python,还有 Java、JavaScript、C、C++等等。事实上,当您将模型投入生产时,您将使用 TensorFlow Serving 来托管 pb 模型,以通过互联网对测试数据进行评分。在下一节中,您将学习 TensorFlow Serving 的工作原理。

TensorFlow Serving

TensorFlow Serving(TFS)是专门设计用于在生产环境中运行 ML 模型的框架。由于通过互联网(或在虚拟专用网络中使用 Internet 协议)对测试数据进行评分可能是最常见的模型服务场景,因此需要一个 HTTP 或 HTTPS 端点作为模型的“前门”。将传递测试数据给模型的客户端程序需要通过 HTTP 与模型的端点进行通信。这种通信遵循 RESTful API 的风格,该 API 指定了通过 HTTP 发送数据的一组规则和格式。

TFS 在这里为您处理所有复杂性。接下来,您将看到如何运行 TFS 来在您的本地环境中托管这个模型。

使用 Docker 镜像运行 TensorFlow Serving

学习 TFS 的最简单方法是使用 Docker 镜像。如果您需要一些关于 Docker 和一般容器技术的背景知识,请参考 Adrian Mouat(O'Reilly)的使用 Docker。第一章提供了对 Docker 容器的简明解释,而第二章向您展示如何安装 Docker 引擎并在本地节点上运行它。

简而言之,Docker 镜像是一个轻量级的、独立的、可执行的软件包,包括运行应用程序所需的一切:代码、运行时、系统工具、系统库和设置。要运行 Docker 镜像,您需要一个 Docker 引擎。当您在 Docker 引擎上运行 Docker 镜像时,该镜像就成为一个容器

有关安装 Docker 引擎的说明,请查看Docker 文档。macOS、Windows 10 和 Linux 都有可用的版本。选择适合您环境的版本,并按照安装说明进行操作。在本章的示例和工作流中,我的本地系统运行的是 macOS Big Sur 版本 11.2,我的 Docker 引擎版本是 3.0.3。

现在确保您的 Docker 引擎已经运行起来:通过双击环境中的图标来启动它。当它运行时,在 Mac 上您将在顶部栏中看到 Docker 鲸鱼图标,如图 9-1 所示,或者在 PC 上的通知区域(右下角)。

Docker 引擎运行状态

图 9-1. Docker 引擎运行状态

安装并运行 Docker 引擎后,将 TFS 的 Docker 镜像作为基础,将 CIFAR-10 模型添加到基础镜像中,然后构建一个新镜像。这个新镜像将通过 HTTP 端点和特定的 TCP/IP 端口提供服务。客户端程序将向此 HTTP 地址和端口发送测试数据。

确保将模型保存为 pb 格式。这次,将其命名为001。这个目录不一定要命名为001,但必须按照 TFS 所需的层次结构和命名约定进行命名。

继续使用前一节中创建的笔记本,并使用以下命令将模型保存在本地目录中:

SAVED_MODEL_PATH = "./models/CIFAR10/001"
tf.saved_model.save(model, SAVED_MODEL_PATH)

这将产生以下目录结构:

models
    CIFAR10
        001
            assets
            saved_model.pb
            variables

现在,使用命令终端导航到models目录。

现在 Docker 引擎正在运行,您可以开始拉取 TFS 镜像。在models目录中输入以下命令:

docker pull tensorflow/serving

此命令将 TFS 镜像下载到本地 Docker 环境。现在运行该镜像:

docker run -d --name serv_base_img tensorflow/serving

前述命令启动了一个名为serv_base_img的容器作为 TFS 镜像。运行以下命令将构建的模型添加到基础镜像中:

docker cp $PWD/CIFAR10 serv_base_img:/models/cifar10

saved_model.pb文件视为其他所有内容的参考。请记住,CIFAR10是本地目录,距离 pb 文件两级。它们之间是001目录。现在,CIFAR10被复制到基础镜像中作为/models/cifar10。请注意,在 Docker 中,目录名称全部小写。

接下来,提交您对基础镜像所做的更改。运行以下命令:

docker commit --change "ENV MODEL_NAME cifar10model" 
serv_base_img cifar10model\

现在您可以停止基础镜像;您不再需要它:

docker kill serv_base_img

让我们回顾一下到目前为止您所做的事情。通过将 CIFAR-10 模型添加到 TFS(基础镜像)中,您创建了一个新的 Docker 镜像。该模型现在已部署并在 TFS 容器中运行。一旦运行 TFS 容器,模型就会启动并准备为任何客户端提供服务。

为了提供托管 CIFAR-10 模型的 TFS 容器,请运行以下命令:

docker run -p 8501:8501 \
  --mount type=bind,\
source=$PWD/CIFAR10,\
target=/models/cifar10 \
  -e MODEL_NAME=cifar10 -t tensorflow/serving &

这是一个相对较长的命令。让我们稍微解析一下。

首先,将本地端口 8501 映射到 Docker 引擎的端口 8501。您的本地端口号并没有什么神奇之处。如果您的本地环境中使用了 8501 端口,您可以使用不同的端口号,比如 8515。如果是这样,那么命令将是-p 8515:8501。由于 TFS 容器始终在端口 8501 上运行,因此前述命令中的第二个目标始终是 8501。

源代码指示当前目录(* $PWD )下方有一个CIFAR10*目录,这是模型所在的位置。这个模型被命名为 CIFAR10,tensorflow/serving容器已准备好接收输入。

您将看到图 9-2 中显示的输出。它表示您正在运行 CIFAR10 模型版本 1,该版本取自名为001的目录。

在命令终端中运行自定义 Docker 容器

图 9-2。在命令终端中运行自定义 Docker 容器

使用 TensorFlow Serving 对测试数据进行评分

现在您的 TFS 容器正在运行您的模型,您可以准备将测试数据传递给模型。这是通过 HTTP 请求完成的。您可以使用另一个 Jupyter Notebook 作为客户端,将 NumPy 数组发送到 TFS。TFS 的 HTTP 地址是http://localhost:8501/v1/models/cifar10:predict

以下是另一个 Jupyter Notebook 中的客户端代码:

  1. 导入所有必要的库:

    import tensorflow as tf
    from tensorflow.keras import datasets
    import requests
    import json
    import numpy as np
    
  2. 加载测试图像并将像素值范围归一化为 0 到 1 之间,然后选择图像。为简单起见,让我们只选择 10 张图像;我们将使用 500 到 510 之间的图像:

    (train_images, train_labels), (test_images, test_labels) = 
    datasets.cifar10.load_data()
    
    # Normalize pixel values to be between 0 and 1
    train_images, test_images = train_images / 255.0, 
     test_images / 255.0
    test_images = test_images[500:510]
    
  3. 将 NumPy 数组test_images转换为 JSON,这是客户端和服务器之间通过 HTTP 进行数据交换的常用格式:

    DATA = json.dumps({
        "instances": test_images.tolist()
    })
    

    您还需要定义标头:

    HEADERS = {"content-type": "application/json"}:
    

    现在您已经用适当的格式和标头包装了您的 NumPy 数据,可以将整个包发送到 TFS 了。

  4. 构建整个包作为 HTTP 请求:

    response = requests.post(
    'http://localhost:8501/v1/models/cifar10:predict', 
    data=DATA, headers=HEADERS)
    

    请注意,您使用post方法从 TFS 获取响应。TFS 将对DATA进行评分,并将结果作为response返回。这种通信框架使用 JSON 请求格式和规则来建立和处理客户端和服务器之间的通信,也被称为 RESTful API。

  5. 查看 TFS 预测的内容:

    response.json()
    

    上述命令将解码预测为概率数组:

    {'predictions': [[9.83229938e-07,
       1.24386987e-10,
       0.0419323482,
       0.00232415553,
       0.91928196,
       3.26286099e-05,
       0.0276549552,
       0.00877290778,
       8.02750222e-08,
       5.4040652e-09],
    …..
    [2.60355654e-10,
       5.17050935e-09,
       0.000181202529,
       1.92517109e-06,
       0.999798834,
       1.04122219e-05,
       3.32912987e-06,
       4.38272036e-06,
       4.2479078e-09,
       9.54967494e-11]]}
    

    在上述代码中,您可以看到第一和第十个测试图像。每个内部数组由 10 个概率值组成,每个值都映射到一个CLASS_NAME

  6. 要将每个预测中的最大概率映射回标签,您需要从 Python 字典response中检索先前显示的值。您可以通过键名predictions检索这些值:

    predictions_prob_list = response.json().get('predictions')
    

    CIFAR-10 数据的标签是:

    CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
                   'deer','dog', 'frog', 'horse', 'ship', 'truck']
    

    CLASS_NAMES是一个包含 CIFAR-10 标签的列表。

  7. 现在将predictions_prob_list转换为 NumPy 数组,然后使用argmax找到最大概率值的索引:

    predictions_array = np.asarray(predictions_prob_list)
    predictions_idx = np.argmax(predictions_array, axis = 0)
    
  8. 将每个索引(共有 10 个)映射到一个 CIFAR-10 标签:

    for i in predictions_idx:
        print(CLASS_NAMES[i])
    

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

    ship
    ship
    airplane
    bird
    truck
    ship
    automobile
    frog
    ship
    horse
    

    这就是您将概率数组解码回标签的方法。

您刚刚在 TFS Docker 容器中运行了自己的图像分类模型,该模型位于 HTTP 端点后面。该容器接受 JSON 有效负载形式的输入数据作为post请求。TFS 解包请求正文;提取包含 NumPy 数组的 JSON 有效负载;对每个数组进行评分;并将结果返回给客户端。

总结

本章向您展示了模型持久性(保存)和模型服务(评分)的基础知识。TensorFlow 模型灵活,利用tf.keras API 提供的简单性将模型保存为单个 HDF5 文件。这种格式易于处理并与他人共享。

对于适用于生产环境的服务框架,通常需要在运行时托管模型,并且该运行时需要通过 HTTP 等基于 Web 的通信协议进行访问。事实证明,TFS 提供了一个处理 HTTP 请求的框架。您只需要将您的 protobuf 模型文件夹复制到 TFS 基础映像中,并提交更改到基础映像。现在,您已经创建了一个包含您的模型的 Docker 映像,并且该模型在 TFS 后面运行。

您学会了如何使用另一个运行时创建正确形状的数值数组,将其包装在 JSON 格式的数据有效负载中,并使用post命令将其发送到由 TFS 托管的 HTTP 端点进行评分。

这基本上完成了构建、训练和提供模型的知识循环。在下一章中,您将学习更多关于模型调整和公平性的实践。

第十章:改善建模体验:公平性评估和超参数调整

使 ML 模型运行良好是一个迭代过程。它需要多轮调整模型参数、架构和训练持续时间。当然,您必须使用可用的数据,但理想情况下,您希望训练数据是平衡的。换句话说,它应该包含相等数量的类或跨范围的均匀分布。

为什么这种平衡很重要?因为如果数据中的任何特征存在偏差,那么训练的模型将重现该偏差。这被称为模型偏差

想象一下,您正在训练一个识别狗的模型。如果您的训练图像中有 99 个负例和 1 个正例,即只有一张实际的狗图像,那么模型将每次都简单地预测负结果,有 99%的准确率。模型学会了在训练过程中最小化错误,最简单的方法是产生一个负预测,简而言之,每次都猜“不是狗”。这被称为数据不平衡问题,在现实世界中很普遍;这也是一个复杂的主题,我无法在这里充分阐述。它需要许多不同的方法,包括通过一种称为数据增强的技术添加合成数据。

在本章中,我将向您介绍公平性指标,这是一个用于评估模型偏差的新工具(截至撰写本文)。它是 TensorFlow Model Analysis 库的一部分,并可用于 Jupyter 笔记本。

您还将学习如何执行超参数调整超参数是模型架构和模型训练过程中的变量。有时您想要尝试不同的值或实现选择,但不知道哪个对您的模型最好。为了找出,您需要评估许多超参数组合的模型性能。我将向您展示如何使用 Keras Tuner 库进行超参数调整的新方法。该库与 TensorFlow 2 的 Keras API 无缝配合。它非常灵活,易于设置为训练过程的一部分。我们将在下一节开始设置公平性指标。

提示

模型偏差及其现实生活后果是众所周知的。模型偏差最显著的例子之一是 COMPAS(Correctional Offender Management Profiling for Alternative Sanctions)框架,该框架曾用于美国法院系统预测累犯。由于训练数据,该模型对黑人被告的假阳性预测是白人被告的两倍。如果您对公平性有兴趣,可以查看 Aileen Nielsen 的《Practical Fairness》(O’Reilly,2020)和 Trisha Mahoney、Kush R. Varshney 和 Michael Hind 的《AI Fairness》(O’Reilly,2020)。

模型公平性

您需要安装 TensorFlow Model Analysis 库,该库不是 TensorFlow 2.4.1 常规发行版的一部分。您可以通过pip install命令下载并安装它:

pip install tensorflow-model-analysis

您还需要安装protobuf库来解析您选择的模型指标:

pip install protobuf

该库使您能够在测试数据上显示和审查模型统计信息,以便检测模型预测中的任何偏差。

为了说明这一点,我们将再次使用Titanic数据集。在第三章中,您使用此数据集构建了一个模型来预测乘客的生存。这个小数据集包含有关每位乘客的几个特征,是一个很好的起点。

我们将生存视为一个离散的结果:某人要么幸存,要么不幸存。然而,对于模型来说,我们真正意味着的是基于乘客的给定特征的生存概率。回想一下,您构建的模型是一个逻辑回归模型,输出是一个二元结果(幸存或未幸存)的概率。一门Google 课程这样表达:

为了将逻辑回归值映射到一个二进制类别,您必须定义一个分类阈值(也称为决策阈值)。高于该阈值的值表示[积极];低于该值表示[消极]。人们很容易假设分类阈值应该始终为 0.5,但阈值是依赖于问题的,并且是您必须调整的值。

决定阈值是用户的责任。理解这一点的直观方式是,生存概率为 0.51 并不保证生存;它仍然意味着 49%的幸存的机会。同样,生存概率为 0.49 并不是零。一个好的阈值是能够最小化两个方向的误分类。因此,阈值是一个用户确定的参数。通常,在您的模型训练和测试过程中,您将尝试几个不同的阈值,并查看哪个在测试数据中给出了最正确的分类。对于这个数据集,您可以从不同的阈值列表开始,例如 0.1、0.5 和 0.9。对于每个阈值,一个积极的结果——即,一个预测概率高于阈值的预测——表示这个个体幸存的预测。

回想一下,Titanic数据集看起来像图 10-1 所示。

用于训练的 Titanic 数据集

图 10-1. 用于训练的 Titanic数据集

图 10-1 中的每一行代表一个乘客和几个相应的特征。模型的目标是根据这些特征预测“survived”列中的值。在训练数据中,这一列是二进制的,1 表示乘客幸存,0 表示乘客未幸存。测试数据也是由Titanic数据集提供的一个单独的分区。

模型训练和评分

让我们从“为训练准备表格数据”之后继续,您已经完成了模型训练。在继续之前再次运行这些导入语句:

import functools
import numpy as np
import tensorflow as tf
import pandas as pd
from tensorflow import feature_column
from tensorflow.keras import layers
from sklearn.model_selection import train_test_split
import pprint
import tensorflow_model_analysis as tfma
from google.protobuf import text_format

一旦模型训练完成(您在第三章中完成了),您可以使用它来预测测试数据集中每个乘客的生存概率:

prediction_raw = model.predict(test_ds)

这个命令prediction_raw会生成一个 NumPy 数组,其中包含每个乘客的概率值:

array([[0.6699142 ],
       [0.6239286 ],
       [0.06013593],
…..
       [0.10424912]], dtype=float32)

现在让我们将这个数组转换成一个 Python 列表,并将其附加为测试数据集的新列:

prediction_list = prediction_raw.squeeze().tolist()
test_df['predicted'] = prediction_list

新列名为“predicted”,是最后一列。为了可见性,您可能希望通过将这最后一列移动到第一列旁边的“survived”列来重新排列列。

# Put predicted as first col, next to survived.
cols = list(test_df.columns)
cols = [cols[-1]] + cols[:-1]
test_df = test_df[cols]

现在test_df看起来像图 10-2,我们可以轻松地将模型的预测与真实结果进行比较。

附加了预测的 Titanic 测试数据集

图 10-2. 附加了预测的 Titanic测试数据集

公平性评估

要调查模型预测的公平性,您需要对您的用例和用于训练的数据有一个很好的理解。仅仅看数据本身可能不会给您足够的背景、上下文或情境意识来调查公平性或模型偏差。因此,您必须了解用例、模型的目的、谁在使用它以及如果模型预测错误可能产生的潜在现实影响,这是至关重要的。

Titanic有三个船舱等级:头等舱是最昂贵的,二等舱位于中间,而三等舱,或者称为船舱,是最便宜的并位于下层甲板。有充分的证据表明,大多数幸存者是女性,并且在头等舱。我们也知道性别和等级在上救生艇的选择过程中起着重要作用。该选择过程优先考虑了妇女和儿童,而不是男性。由于这个背景是如此出名,这个数据集适合作为一个教学示例来调查模型偏差。

在进行预测时,正如我在介绍中提到的,模型不可避免地重现训练数据中的任何偏见或不平衡。因此,我们调查的一个有趣问题是:模型在不同性别和等级的乘客生存预测方面表现如何?

让我们从以下代码块开始eval_config,定义我们的调查:

eval_config = text_format.Parse("""    // ①
  model_specs {                        // ②
    prediction_key: 'predicted',
    label_key: 'survived'
  }
  metrics_specs {                      // ③
    metrics {class_name: "AUC"}
    metrics {
      class_name: "FairnessIndicators"
      config: '{"thresholds": [0.1, 0.50, 0.90]}'
    }
    metrics { class_name: "ExampleCount" }
  }

  slicing_specs {                      // ④
    feature_keys: ['sex', 'class']
  }
  slicing_specs {}
  """, tfma.EvalConfig())              // ⑤

eval_config对象必须格式化为协议缓冲区数据类型,这就是为什么您需要导入text_format来解析定义。

指定model_specs以记录要比较的两列:“predicted”和“survived”。

定义分类准确性的指标以及基于生存概率分配生存状态的三个阈值。

这是您声明要用来调查模型偏差的特征的地方。slicing_specs中的feature_keys保存要检查偏差的特征列表:在我们的案例中是“sex”和“class”。由于性别有两个唯一值,而不同的班级有三个不同的类别,公平性指标将评估六个不同交互组的模型偏差。如果只列出一个特征,公平性指标将仅评估该特征上的偏差。

所有这些信息都包含在“““ ”””三重双引号中,这使得它成为一个纯文本表示。这个文本字符串被合并到tfma.EvalConfig消息中。

现在定义一个输出路径来存储公平性指标的结果:

OUTPUT_PATH = './titanic-fairness'

然后运行模型分析例程:

eval_result = tfma.analyze_raw_data(
  data= test_df,
  eval_config=eval_config,
  output_path=OUTPUT_PATH)

从前面的代码中,您可以看到test_df是测试数据,其中添加了预测。您将使用tfma.analyze_raw_data函数执行公平性分析。

提示

如果您在本地 Jupyter Notebook 中运行此示例,则需要启用 Jupyter Notebook 以显示公平性指标 GUI。在下一个单元格中,输入以下命令:

!jupyter nbextension enable tensorflow_model_analysis 
--user –py

如果您正在使用 Google Colab 笔记本,则此步骤是不必要的。

渲染公平性指标

现在您已经准备好查看您的eval_result。运行此命令:

tfma.addons.fairness.view.widget_view.render_fairness_indicator(
 eval_result)

您将在笔记本中看到公平性指标的交互式 GUI(参见图 10-3)。

公平性指标交互式 GUI

图 10-3. 公平性指标交互式 GUI

在这个图中,选择了假阳性率指标。在Titanic数据集的背景下,假阳性表示模型预测乘客会生存,但实际上他们没有。

让我们看看这个 GUI。在左侧面板(如图 10-3 所示),您可以看到所有可用的指标。在右侧面板中,您会看到所选指标的条形图。在slice_specs中指示特征组合(性别和等级)。

在阈值为 0.5(概率大于 0.5 被视为正面,即生存)时,头等舱男性乘客和二三等舱女性乘客的假阳性率很高。

在条形图下方的表格中,您可以更详细地查看实际指标和样本大小(参见图 10-4)。

显示假阳性摘要的公平性指标仪表板

图 10-4. 显示假阳性摘要的公平性指标仪表板

毫不奇怪,模型正确预测了所有头等舱女性乘客的幸存,您可以从实际情况(“survived”)列中看到。

那么为什么二等和三等舱的男性乘客产生了 0 的假阳性率?让我们查看test_df中的实际结果以了解原因。执行以下命令选择二等舱住宿的男性乘客:

sel_df = test_df[(test_df['sex'] == 'male') & (test_df['class'] == 
'Second')]

然后显示sel_df

sel_df

图 10-5 显示了所有二等舱男性乘客的结果列表。让我们用它来寻找假阳性。在这个组中,是否有任何乘客没有幸存(即,“survived”列显示 0),但模型预测的概率大于阈值(0.5)?没有。因此,公平性指标表明假阳性率为 0。

泰坦尼克号测试数据集的二等舱男性乘客

图 10-5. 泰坦尼克号测试数据集的二等舱男性乘客

您可能会注意到,一些二等舱男性乘客确实幸存了,但模型预测他们的生存概率低于 0.5 的阈值。这些被称为假阴性案例:他们实际上幸存了,但模型预测他们没有。简而言之,他们打破了规律!因此,让我们取消假阳性指标并检查我们的假阴性指标,看看在性别和阶级组合中它是什么样子(参见图 10-6)。

正如您在图 10-6 中所看到的,在当前阈值 0.5 的情况下,二等和三等舱的男性和男孩有很高的假阴性率。

泰坦尼克号测试数据集的假阴性指标

图 10-6. 泰坦尼克号测试数据集的假阴性指标

这个练习清楚地表明,模型在性别和阶级方面存在偏见。该模型对头等舱女性乘客表现最佳,这反映了数据中明显的偏斜:几乎所有头等舱女性乘客都幸存了,而其他人的机会则不那么明确。您可以通过以下语句检查训练数据以确认这种偏斜,该语句将训练数据按“sex”,“class”和“survived”列分组:

 TRAIN_DATA_URL = 
https://storage.googleapis.com/tf-datasets/titanic/train.csv
train_file_path = tf.keras.utils.get_file("train.csv", 
TRAIN_DATA_URL)
train_df = pd.read_csv(train_file_path, 
header='infer')
train_df.groupby(['sex', 'class', 'survived' ]).size().
reset_index(name='counts')

这将产生图 10-7 中显示的摘要。

如您所见,69 名头等舱女性乘客中只有两人死亡。您根据这些数据训练了您的模型,因此模型很容易学会,只要阶级和性别指示为头等和女性,它就应该预测高概率的生存。您还可以看到,第三等舱的女性乘客中,52 人幸存,41 人死亡,没有享受到同样有利的结果。这是一个典型的数据标签不平衡(“survived”列)导致模型偏见的案例。在所有可能的性别-阶级组合中,没有其他组合的胜算像头等舱女性乘客那样好。这使得模型更难正确预测。

泰坦尼克号训练数据集中乘客组的生存计数摘要

图 10-7. 泰坦尼克号训练数据集中乘客组的生存计数摘要

使用公平性指标时,您可以切换阈值下拉框以在同一条形图中显示三个不同阈值的指标,如图 10-8 所示。

展示所有阈值的公平性指标指标

图 10-8. 展示所有阈值的公平性指标指标

为了易读性,每个阈值都有颜色编码。您可以将鼠标悬停在柱状图上以确定颜色分配。这有助于您解释不同阈值对每个指标的影响。

从这个例子中,我们可以得出以下结论:

  • 乘客的特征,如性别和舱位等级(作为社会阶级的替代品),主要决定了结果。

  • 对于头等舱和二等舱的女性乘客,模型的准确性要高得多,这强烈表明模型偏见是由性别和阶级驱动的。

  • 生存概率偏向于某些性别和某些阶级。这被称为数据不平衡,与历史记载的发生一致。

  • 特征之间的交互作用,比如性别和阶级(称为特征交叉),揭示了模型偏见的更深层次。在最低(第三)阶级的妇女和女孩在训练数据和模型准确性中都没有获得有利的生存结果,更不用说在现实生活中了。

当您看到模型偏见时,您可能会尝试调整模型架构或训练策略。但这里的根本问题是训练数据中的不平衡,这反映了基于性别和阶级的现实不平等结果。如果没有为其他性别和阶级带来更公平的结果,修复模型架构或训练策略将无法创建一个公平的模型。

这个例子相对简单:这里的假设是检查性别和阶级可能会揭示模型偏见,因此值得调查。由于这是一个被广泛讨论的历史事件,大多数人至少对导致泰坦尼克号悲剧的背景和事件有一定的了解。然而,在许多情况下,制定一个假设来调查数据中潜在偏见可能并不简单。您的数据可能不包含个人属性或个人可识别信息(PII),您可能没有域知识或对训练数据的收集方式和影响结果的了解。数据科学家和 ML 模型开发人员应该与主题专家合作,以使他们的训练数据具有背景。有时,如果模型公平性是主要关注点,删除相关特征可能是最好和最明智的选择。

超参数调整

超参数是指定用于控制模型训练过程的值或属性。想法是您希望使用这些值的组合来训练模型,并确定最佳组合。知道哪种组合最好的唯一方法是尝试这些值,因此您希望以有效的方式迭代所有组合并将选择范围缩小到最佳组合。超参数通常应用于模型架构,比如深度学习神经网络中密集层中的节点数。超参数可以应用于训练例程,比如执行反向传播的优化器(您在第八章中学到的)在训练过程中。它们还可以应用于学习率,指定您希望使用增量更改来更新模型的权重和偏差的程度,从而确定反向传播在训练过程中更新权重和偏差的步长有多大。综合起来,超参数可以是数值(节点数、学习率)或非数值(优化器名称)。

截至 TensorFlow 分发 2.4.1 版本,Keras Tuner 库尚未成为标准分发的一部分。这意味着您需要安装它。您可以在命令终端或 Google Colab 笔记本中运行此安装语句:

pip install keras-tuner

如果在 Jupyter Notebook 单元格中运行它,请使用此版本:

!pip install keras-tuner

感叹号(!)告诉笔记本单元格将其解释为命令而不是 Python 代码。

安装完成后,您将像往常一样导入它。请注意,当您安装库时,库名称是带连字符的,但在导入语句中不使用连字符:

import kerastuner as kt

从 Keras Tuner 的角度来看,超参数有三种数据类型:整数、项目选择(一组离散值或对象)和浮点数。

整数列表作为超参数

通过示例更容易看到如何使用 Keras Tuner,所以假设您想尝试在密集层中使用不同数量的节点。首先定义可能的数字,然后将该列表传递给密集层的定义:

hp_node_count = hp.Int('units', min_value = 16, max_value = 32, 
step = 8)
tf.keras.layers.Dense(units = hp_node_count, activation = 'relu')

在上述代码中,hp.Int定义了一个别名:hp_node_count。这个别名包含一个整数列表(16、24 和 32),您将其作为units传递给Dense层。您的目标是看哪个数字效果最好。

项目选择作为超参数

设置超参数的另一种方法是将所有选择放在一个列表中作为离散项或选择。这可以通过hp.Choice函数实现:

hp_units = hp.Choice('units', values = [16, 18, 21])

以下是通过名称指定激活函数的示例:

hp_activation = hp.Choice('dense_activation', 
                values=['relu', 'tanh', 'sigmoid'])

浮点值作为超参数

在许多情况下,您可能希望在训练例程中尝试不同的小数值。如果您想为优化器选择学习率,这是非常常见的。要这样做,请使用以下命令:

hp_learning_rate = hp.Float('learning_rate', 
 min_value = 1e-4, 
 max_value = 1e-2, 
 step = 1e-3)
optimizer=tf.keras.optimizers.SGD(
 lr=hp_learning_rate, 
 momentum=0.5)

端到端超参数调整

超参数调整是一个耗时的过程。它涉及尝试多种组合,每种组合都经过相同数量的 epochs 训练。对于“蛮力”方法,您需要循环遍历每个超参数组合,然后启动训练例程。

使用 Keras Tuner 的优势在于其提前停止实现:如果特定组合似乎没有改善结果,它将终止训练例程并转移到下一个组合。这有助于减少总体训练时间。

接下来,您将看到如何使用一种称为超带搜索的策略执行和优化超参数搜索。超带搜索利用训练过程中的逐渐减少原则。在每次循环中,算法会对所有超参数组合的模型表现进行排名,并丢弃参数组合中较差的一半。下一轮中,较好的组合将获得更多的处理器核心和内存。这将一直持续到最后一种组合保留下来,消除所有但最佳超参数组合。

这有点像体育比赛中的淘汰赛:每一轮和每一场比赛都会淘汰排名较低的球队。然而,在超带搜索中,失败的球队在比赛完成之前就被宣布失败了。这个过程会一直持续到冠军赛,冠军赛中排名第一的球队最终成为冠军。这种策略比蛮力方法要节约得多,因为每种组合都会被训练到完整的 epochs,这会消耗大量的训练资源和时间。

让我们将您在上一章中使用的 CIFAR-10 图像分类数据集中学到的知识应用起来。

导入库和加载数据

我建议使用 Google Colab 笔记本电脑和 GPU 来运行此示例中的代码。截至 TensorFlow 2.4.1,Keras Tuner 尚未成为 TensorFlow 分发的一部分,因此在 Google Colab 环境中,您需要运行pip install命令:

pip install -q -U keras-tuner

安装完成后,将其与所有其他库一起导入:

import tensorflow as tf
import kerastuner as kt
from tensorflow.keras import datasets, layers, models
import numpy as np
import matplotlib.pylab as plt
import os
from datetime import datetime
print(tf.__version__)

它将显示当前版本的 TensorFlow——在本例中为 2.4.1。然后在一个单元格中加载和归一化图像:

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()

# Normalize pixel values to be between 0 and 1
train_images, test_images = train_images / 255.0, 
test_images / 255.0

现在提供一个标签列表:

# Plain-text name in alphabetical order. 
https://www.cs.toronto.edu/~kriz/cifar.html
CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
               'deer','dog', 'frog', 'horse', 'ship', 'truck']

接下来,通过将图像和标签合并为张量将图像转换为数据集。将测试数据集分为两组——前 500 张图像(用于训练期间的验证)和其他所有图像(用于测试):

validation_dataset = tf.data.Dataset.from_tensor_slices(
 (test_images[:500], test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
 (test_images[500:], test_labels[500:]))

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

要确定每个数据集的样本大小,请执行以下命令:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

validation_dataset_size = len(list(validation_dataset.
as_numpy_iterator()))
print('Validation data sample size: ', validation_dataset_size)

test_dataset_size = len(list(test_dataset.as_numpy_iterator()))
print('Test data sample size: ', test_dataset_size)

您应该会得到类似以下输出:

Training data sample size:  50000
Validation data sample size:  500
Test data sample size:  9500

接下来,为了利用分布式训练,定义一个MirroredStrategy对象来处理分布式训练:

strategy = tf.distribute.MirroredStrategy()
print('Number of devices: {}'.format(
strategy.num_replicas_in_sync))

在您的 Colab 笔记本中,您应该看到以下输出:

Number of devices: 1

现在设置样本批处理参数:

BUFFER_SIZE = 10000
BATCH_SIZE_PER_REPLICA = 64
BATCH_SIZE = BATCH_SIZE_PER_REPLICA * strategy.num_replicas_in_sync
STEPS_PER_EPOCH = train_dataset_size // BATCH_SIZE_PER_REPLICA
VALIDATION_STEPS = 1

对所有数据集进行洗牌和批处理:

train_dataset = train_dataset.repeat().shuffle(BUFFER_SIZE).batch(
BATCH_SIZE)
validation_dataset = validation_dataset.shuffle(BUFFER_SIZE).batch(
validation_dataset_size)
test_dataset = test_dataset.batch(test_dataset_size)

现在您可以创建一个函数来包装模型架构:

def build_model(hp):
  model = tf.keras.Sequential()
  # Node count for next layer as hyperparameter
  hp_node_count = hp.Int('units', min_value=16, max_value=32, 
      step=8)
  model.add(tf.keras.layers.Conv2D(filters = hp_node_count,
      kernel_size=(3, 3),
      activation='relu',
      name = 'conv_1',
      kernel_initializer='glorot_uniform',
      padding='same', input_shape = (32,32,3)))
  model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
  model.add(tf.keras.layers.Flatten(name = 'flat_1'))
  # Activation function for next layer as hyperparameter
  hp_AF = hp.Choice('dense_activation', 
      values = ['relu', 'tanh'])
  model.add(tf.keras.layers.Dense(256, activation=hp_AF,
      kernel_initializer='glorot_uniform',
      name = 'dense_1'))
  model.add(tf.keras.layers.Dense(10, 
      activation='softmax',
      name = 'custom_class'))

  model.build([None, 32, 32, 3])
  # Compile model with optimizer
  # Learning rate as hyperparameter
  hp_LR = hp.Float('learning_rate', 1e-2, 1e-4)

  model.compile(
     loss=tf.keras.losses.SparseCategoricalCrossentropy(
          from_logits=True),
       optimizer=tf.keras.optimizers.Adam(
          learning_rate=hp_LR),
       metrics=['accuracy'])

  return model

这个函数与您在第九章中看到的函数之间有一些主要区别。该函数现在期望一个输入对象hp。这意味着该函数将由名为hp的超参数调整对象调用。

在模型架构中,第一层conv_1的节点计数通过使用hp_node_count进行超参数搜索。层dense_1的激活函数也通过使用hp_AF进行超参数搜索进行声明。最后,optimizer中的学习率通过hp_LR进行超参数搜索进行声明。此函数返回具有声明的超参数的模型。

接下来,使用kt.Hyperband定义一个对象(tuner),该对象将build_model函数作为输入:

tuner = kt.Hyperband(hypermodel = build_model,
                     objective='val_accuracy',
                     max_epochs=10,
                     factor=3,
                     directory='hp_dir',
                     project_name='hp_kt')

您传递以下输入以定义tuner对象:

超模型

定义模型架构和优化器的函数。

目标

用于评估模型性能的训练指标。

max_epochs

模型训练的最大时期数。

因子

每个框架中的时期和模型数量的减少因子。排名在前 1/因子的模型被选中并进入下一轮训练。如果因子是 2,那么前一半将进入下一轮。如果因子是 4,那么前一四分之一将进入下一轮。

目录

将结果写入的目标目录,例如每个模型的检查点。

project_name

在目标目录中保存的所有文件的前缀。

在这里,您可以定义一个提前停止,如果验证准确性在五个时期内没有改善,则停止训练:

early_stop = tf.keras.callbacks.EarlyStopping(
 monitor='val_accuracy', 
 patience=5)

现在您已准备好通过 Hyperband 算法启动搜索。当搜索完成时,它将打印出最佳超参数:

tuner.search(train_dataset,
             steps_per_epoch = STEPS_PER_EPOCH,
             validation_data = validation_dataset,
             validation_steps = VALIDATION_STEPS,
             epochs = 15,
             callbacks = [early_stop]
             )
# Get the optimal hyperparameters
best_hps=tuner.get_best_hyperparameters(num_trials=1)[0]

print(f"""
The hyperparameter search is complete. The optimal number of units 
in conv_1 layer is {best_hps.get('units')} and the optimal 
learning rate for the optimizer is {best_hps.get('learning_rate')} 
and the optimal activation for dense_1 layer
is {best_hps.get('dense_activation')}.
""")

正如您所看到的,搜索后,best_hps保存了有关最佳超参数值的所有信息。

当您在带有 GPU 的 Colab 笔记本中运行此示例时,通常需要大约 10 分钟才能完成。期望看到类似以下的输出:

Trial 42 Complete [00h 01m 14s]
val_accuracy: 0.593999981880188

Best val_accuracy So Far: 0.6579999923706055
Total elapsed time: 00h 28m 53s
INFO:tensorflow:Oracle triggered exit

The hyperparameter search is complete. The optimal number of units 
in conv_1 layer is 24 and the optimal learning rate for the 
optimizer is 0.0013005004751682134 and the optimal activation 
for dense_1 layer is relu.

此输出告诉我们最佳超参数如下:

  • conv_1层的最佳节点计数为 24。

  • optimizer的最佳学习率为 0.0013005004751682134。

  • dense_1的最佳激活函数选择为“relu”。

现在您已经获得了最佳超参数,需要使用这些值正式训练模型。Keras Tuner 具有一个称为hypermodel.build的高级函数,使这成为一个单一命令过程:

best_hp_model = tuner.hypermodel.build(best_hps)

之后,设置检查点目录,方式与您在第九章中所做的相同:

MODEL_NAME = 'myCIFAR10-{}'.format(datetime.datetime.now().
strftime("%Y%m%d-%H%M%S"))
print(MODEL_NAME)
checkpoint_dir = './' + MODEL_NAME
checkpoint_prefix = os.path.join(
checkpoint_dir, "ckpt-{epoch}")
print(checkpoint_prefix)

您还将设置检查点,方式与您在第九章中所做的相同:

myCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    monitor='val_accuracy',
    mode='max',
    save_weights_only = True,
    save_best_only = True
    )

现在是时候使用best_hp_model对象启动模型训练过程:

best_hp_model.fit(train_dataset,
             steps_per_epoch = STEPS_PER_EPOCH,
             validation_data = validation_dataset,
             validation_steps = VALIDATION_STEPS,
             epochs = 15,
             callbacks = [early_stop, myCheckPoint])

训练完成后,加载具有最高验证准确性的模型。将save_best_only设置为 True,最佳模型将是最新检查点中的模型:

best_hp_model.load_weights(tf.train.latest_checkpoint(
checkpoint_dir))

现在best_hp_model已准备好用于服务。它是使用最佳超参数训练的,并且权重和偏差从产生最高验证准确性的最佳训练时期加载。

总结

在这一章中,您学会了如何改进模型构建和质量保证流程。

机器学习模型中最常见和重要的质量保证问题之一是公平性。公平性指标是一个工具,可以帮助你调查模型在许多不同特征交互和组合中的偏见。在评估模型公平性时,你必须查找训练数据中的模型偏见。你还需要依赖主题专家的背景知识,以便在调查任何模型偏见时制定假设。在《泰坦尼克号》的例子中,这个过程相当直接,因为很明显性别和阶级在决定每个人生存机会中起着重要作用。然而,在实践中,还有许多其他因素使事情复杂化,包括数据是如何收集的,以及数据收集的背景或条件是否偏向于样本来源中的某一组。

在模型构建过程中,超参数调整是耗时的。过去,你必须迭代每个潜在超参数值的组合,以搜索最佳组合。然而,使用 Keras Tuner 库,一个相对先进的搜索算法称为 Hyperband 可以高效地进行搜索,使用一种类似锦标赛的框架。在这个框架中,基于弱超参数训练的模型会在训练周期完成之前被终止和移除。这减少了总体搜索时间,最佳超参数会脱颖而出。你只需要用获胜组合正式训练相同的模型。

有了这些知识,你现在可以将你的 TensorFlow 模型开发和测试技能提升到下一个水平。

如果你需要对分类指标进行复习,我建议你查看谷歌的《机器学习速成课程》中包含的简洁而有用的复习部分。

posted @ 2025-09-12 14:07  绝不原创的飞龙  阅读(13)  评论(0)    收藏  举报