TensorFlow-机器学习秘籍-全-
TensorFlow 机器学习秘籍(全)
原文:
annas-archive.org/md5/fdd2c78f646d889f873e10b2a8485875译者:飞龙
前言
由 Google 开发的 TensorFlow 2.x 是一个端到端的开源机器学习平台。它拥有一个全面且灵活的工具、库和社区资源生态系统,允许研究人员推动前沿的机器学习,并使开发人员能够轻松构建和部署机器学习驱动的应用程序。
本书中的独立食谱将教你如何使用 TensorFlow 进行复杂的数据计算,并让你能够深入挖掘数据,从而获得比以往更多的洞察力。在本书的帮助下,你将学习如何使用食谱进行模型训练、模型评估、回归分析、表格数据、图像与文本处理及预测等,除此之外还有更多内容。你将探索 RNN、CNN、GAN 和强化学习,每种方法都使用了 Google 最新的机器学习库 TensorFlow。通过实际案例,你将获得使用 TensorFlow 解决各种数据问题和技术的实践经验。一旦你熟悉并舒适地使用 TensorFlow 生态系统,本书还会展示如何将其应用到生产环境中。
本书结束时,你将熟练掌握使用 TensorFlow 2.x 的机器学习领域知识。你还将对深度学习有深入了解,能够在实际场景中实现机器学习算法。
本书适合人群
本书适合数据科学家、机器学习开发人员、深度学习研究人员以及具有基础统计背景的开发人员,他们希望使用神经网络并探索 TensorFlow 的结构及其新功能。为了最大化利用本书,读者需要具备 Python 编程语言的基本知识。
本书的内容
第一章,TensorFlow 2.x 入门,介绍了 TensorFlow 中的主要对象和概念。我们介绍了张量、变量和占位符,还展示了如何在 TensorFlow 中处理矩阵及各种数学运算。在本章的结尾,我们展示了如何访问本书其余部分所使用的数据源。
第二章,TensorFlow 方式,阐述了如何将 第一章,TensorFlow 入门 中的所有算法组件以多种方式连接成计算图,从而创建一个简单的分类器。在此过程中,我们涵盖了计算图、损失函数、反向传播和数据训练。
第三章,Keras,专注于名为 Keras 的高层次 TensorFlow API。在介绍了作为模型构建块的各个层之后,我们将讲解 Sequential、Functional 和 Sub-Classing API 来创建 Keras 模型。
第四章,线性回归,重点介绍如何使用 TensorFlow 探索各种线性回归技术,如 Lasso 和 Ridge、ElasticNet 和逻辑回归。我们通过扩展线性模型来介绍 Wide & Deep,并展示如何使用估算器实现每个模型。
第五章,提升树,讨论了 TensorFlow 对提升树的实现——这是最流行的表格数据模型之一。我们通过解决一个预测酒店预订取消的商业问题来演示其功能。
第六章,神经网络,涵盖了如何在 TensorFlow 中实现神经网络,从操作门和激活函数的概念开始。然后我们展示了一个浅层神经网络,并演示如何构建不同类型的层。最后,通过教 TensorFlow 神经网络玩井字游戏来结束本章。
第七章,使用表格数据进行预测,本章通过展示如何使用 TensorFlow 处理表格数据,扩展了上一章的内容。我们展示了如何处理缺失值、二进制、名义、顺序和日期特征的数据。我们还介绍了激活函数,如 GELU 和 SELU(特别适合深度架构),以及如何在数据不足时正确使用交叉验证来验证架构和参数。
第八章,卷积神经网络,通过展示如何使用图像和卷积层(以及其他图像层和功能)来扩展我们对神经网络的理解。我们展示了如何构建一个简化的 CNN 用于 MNIST 数字识别,并将其扩展到 CIFAR-10 任务中的彩色图像。我们还说明了如何将先前训练过的图像识别模型扩展到自定义任务。最后,通过解释和展示 TensorFlow 中的 StyleNet/神经风格和 DeepDream 算法来结束这一章。
第九章,循环神经网络,介绍了一种强大的架构类型(RNN),该类型在不同模式的序列数据上取得了最先进的成果;介绍的应用包括时间序列预测和文本情感分析。
第十章,变压器,专门介绍了变压器——一种深度学习模型的新类型,彻底改变了自然语言处理(NLP)领域。我们展示了如何利用其优势进行生成性和判别性任务。
第十一章,使用 TensorFlow 和 TF-Agents 进行强化学习,介绍了专门用于强化学习的 TensorFlow 库。这种结构化方法使我们能够处理从简单游戏到电子商务中的内容个性化等各种问题。
第十二章,将 TensorFlow 应用于生产环境,提供了关于将 TensorFlow 迁移到生产环境的技巧和示例,并展示了如何利用多种处理设备(例如,GPU)以及如何在多台机器上设置 TensorFlow 分布式系统。我们还展示了 TensorBoard 的多种用途,以及如何查看计算图指标和图表。最后,我们通过展示如何在 TensorFlow 中设置 RNN 模型并提供 API 的示例来结束这一章。
如何最大化利用本书
您需要对神经网络有一定的基础了解,但这不是强制性的,因为本书会从实践的角度讲解相关主题,并在需要时提供理论信息。
具备基本机器学习算法和技术的工作知识是一个加分项。您需要熟练掌握 Python 3,并且应当已经知道如何使用pip安装包,以及如何设置工作环境以便与 TensorFlow 一起使用。
环境设置将在第一章中讲解,《TensorFlow 2.x 入门》。
下载示例代码文件
本书的代码包托管在 GitHub 上:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook。我们还在github.com/PacktPublishing/上提供了我们丰富书籍和视频目录中的其他代码包。快去看看吧!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。您可以在这里下载:static.packt-cdn.com/downloads/9781800208865_ColorImages.pdf。
使用的约定
本书中使用了若干文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入以及 Twitter 用户名。例如:“truncated_normal()函数始终在指定均值的两个标准差范围内选取正态值。”
代码块按如下方式设置:
import TensorFlow as tf
import NumPy as np
任何命令行输入或输出都按如下方式书写:
pip install tensorflow-datasets
粗体:表示新术语、重要词汇,或者您在屏幕上看到的单词,例如出现在菜单或对话框中的单词,也会像这样出现在文本中。例如:“TF-Agents 是一个用于强化学习(RL)的 TensorFlow 库。”
警告或重要说明会像这样显示。
小贴士和技巧会像这样显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请通过电子邮件feedback@packtpub.com联系我们,并在邮件主题中提及书名。如果您有任何关于本书的疑问,请通过questions@packtpub.com发送邮件与我们联系。
勘误表:尽管我们已尽力确保内容的准确性,但错误是难以避免的。如果您在本书中发现错误,我们将不胜感激,您能向我们报告。请访问www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并填写相关详情。
盗版:如果您在互联网上发现我们作品的任何非法复制品,我们将感激您提供其位置地址或网站名称。请通过copyright@packtpub.com与我们联系,并附上该材料的链接。
如果你有兴趣成为作者:如果你在某个领域拥有专业知识,并且有兴趣写书或为书籍做贡献,请访问 authors.packtpub.com。
评价
请留下评价。阅读并使用本书后,为什么不在你购买书籍的站点上留下评价呢?潜在读者可以查看并参考你的公正意见来做出购买决定,我们 Packt 团队也能了解你对我们产品的看法,而我们的作者也能看到你对他们书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packtpub.com。
第一章:开始使用 TensorFlow 2.x
Google 的 TensorFlow 引擎有一种独特的解决问题方式,使我们能够非常高效地解决机器学习问题。如今,机器学习已应用于几乎所有的生活和工作领域,著名的应用包括计算机视觉、语音识别、语言翻译、医疗保健等。我们将在本书的后续页面中覆盖理解 TensorFlow 操作的基本步骤,并最终讲解如何构建生产代码。此时,本章中呈现的基础内容至关重要,它们将为你提供核心理解,帮助你更好地理解本书中其他部分的食谱。
在本章中,我们将从一些基本的食谱开始,帮助你理解 TensorFlow 2.x 的工作原理。你还将学习如何访问本书中示例所使用的数据,以及如何获取额外的资源。到本章结束时,你应该掌握以下知识:
-
理解 TensorFlow 2.x 的工作原理
-
声明和使用变量与张量
-
与矩阵合作
-
声明操作
-
实现激活函数
-
与数据源合作
-
寻找额外资源
不再多说,让我们从第一个食谱开始,它以简易的方式展示了 TensorFlow 如何处理数据和计算。
TensorFlow 的工作原理
TensorFlow 最初是由 Google Brain 团队的研究人员和工程师作为一个内部项目开始的,最初名为 DistBelief,并于 2015 年 11 月发布为一个开源框架,名称为 TensorFlow(张量是标量、向量、矩阵及更高维度矩阵的广义表示)。你可以在这里阅读关于该项目的原始论文:download.tensorflow.org/paper/whitepaper2015.pdf。在 2017 年发布 1.0 版本后,去年,Google 发布了 TensorFlow 2.0,它在继续发展和改进 TensorFlow 的同时,使其变得更加用户友好和易于使用。
TensorFlow 是一个面向生产的框架,能够处理不同的计算架构(CPU、GPU,现在还有 TPU),适用于需要高性能和易于分布式的各种计算。它在深度学习领域表现出色,可以创建从浅层网络(由少数层组成的神经网络)到复杂的深度网络,用于图像识别和自然语言处理。
在本书中,我们将呈现一系列食谱,帮助你以更高效的方式使用 TensorFlow 进行深度学习项目,减少复杂性,帮助你实现更广泛的应用并取得更好的结果。
一开始,TensorFlow 中的计算可能看起来不必要地复杂。但这背后是有原因的:由于 TensorFlow 处理计算的方式,当你习惯了 TensorFlow 风格时,开发更复杂的算法会变得相对容易。本方案将引导我们通过 TensorFlow 算法的伪代码。
准备开始
目前,TensorFlow 已在以下 64 位系统上测试并获得支持:Ubuntu 16.04 或更高版本、macOS 10.12.6(Sierra)或更高版本(不过不支持 GPU)、Raspbian 9.0 或更高版本,以及 Windows 7 或更高版本。本书中的代码已在 Ubuntu 系统上开发并测试,但它在其他任何系统上也应该能正常运行。本书的代码可在 GitHub 上找到,地址为 github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook,它作为本书所有代码和一些数据的代码库。
在本书中,我们将只关注 TensorFlow 的 Python 库封装,尽管 TensorFlow 的大多数核心代码是用 C++ 编写的。TensorFlow 与 Python 很好地兼容,支持 3.7 至 3.8 版本。此书将使用 Python 3.7(你可以在 www.python.org 获取该解释器)和 TensorFlow 2.2.0(你可以在 www.tensorflow.org/install 查找安装它所需的所有说明)。
虽然 TensorFlow 可以在 CPU 上运行,但大多数算法如果在 GPU 上处理,运行会更快,并且支持在具有 Nvidia 计算能力 3.5 或更高版本的显卡上运行(特别是在运行计算密集型的复杂网络时更为推荐)。
你在书中找到的所有方案都与 TensorFlow 2.2.0 兼容。如有必要,我们将指出与以前的 2.1 和 2.0 版本在语法和执行上的区别。
在工作站上运行基于 TensorFlow 的脚本时,常用的 GPU 有 Nvidia Titan RTX 和 Nvidia Quadro RTX,而在数据中心,我们通常会找到至少配备 24 GB 内存的 Nvidia Tesla 架构(例如,Google Cloud Platform 提供了 Nvidia Tesla K80、P4、T4、P100 和 V100 型号)。要在 GPU 上正常运行,你还需要下载并安装 Nvidia CUDA 工具包,版本为 5.x+(developer.nvidia.com/cuda-downloads)。
本章中的一些方案将依赖于安装当前版本的 SciPy、NumPy 和 Scikit-learn Python 包。这些附带包也包含在 Anaconda 包中(https://www.anaconda.com/products/individual#Downloads)。
如何进行…
在这里,我们将介绍 TensorFlow 算法的一般流程。大多数方案将遵循这个大纲:
-
导入或生成数据集:我们所有的机器学习算法都依赖于数据集。在本书中,我们将生成数据或使用外部数据源。有时,依赖生成的数据更好,因为我们可以控制如何变化并验证预期结果。大多数情况下,我们将访问给定食谱的公共数据集。有关如何访问这些数据集的详细信息,请参见本章末尾的 附加资源 章节:
import tensorflow as tf import tensorflow_datasets as tfds import numpy as np data = tfds.load("iris", split="train") -
转换和规范化数据:通常,输入数据集的形式并不是我们实现目标所需要的精确形式。TensorFlow 期望我们将数据转换为接受的形状和数据类型。实际上,数据通常不符合我们算法所期望的正确维度或类型,我们必须在使用之前正确地转换它。大多数算法还期望规范化数据(这意味着变量的均值为零,标准差为一),我们也将在这里讨论如何实现这一点。TensorFlow 提供了内置函数,可以加载数据、将数据拆分为批次,并允许您使用简单的 NumPy 函数转换变量和规范化每个批次,包括以下内容:
for batch in data.batch(batch_size, drop_remainder=True): labels = tf.one_hot(batch['label'], 3) X = batch['features'] X = (X - np.mean(X)) / np.std(X) -
将数据集划分为训练集、测试集和验证集:我们通常希望在不同的数据集上测试我们的算法,这些数据集是我们训练过的。许多算法还需要进行超参数调整,因此我们预留了一个验证集,用于确定最佳的超参数组合。
-
设置算法参数(超参数):我们的算法通常会有一组参数,这些参数在整个过程中保持不变。例如,这可能是迭代次数、学习率或我们选择的其他固定参数。通常建议将这些参数一起初始化为全局变量,以便读者或用户能够轻松找到它们,如下所示:
epochs = 1000 batch_size = 32 input_size = 4 output_size = 3 learning_rate = 0.001 -
初始化变量:TensorFlow 依赖于知道它可以修改什么以及不能修改什么。在优化过程中,TensorFlow 将修改/调整变量(模型的权重/偏置),以最小化损失函数。为了实现这一点,我们通过输入变量输入数据。我们需要初始化变量和占位符的大小和类型,以便 TensorFlow 知道该期待什么。TensorFlow 还需要知道期望的数据类型。在本书的大部分内容中,我们将使用
float32。TensorFlow 还提供了float64和float16数据类型。请注意,使用更多字节来获得更高精度会导致算法变慢,而使用更少字节则会导致结果算法的精度降低。请参考以下代码,了解如何在 TensorFlow 中设置一个权重数组和一个偏置向量的简单示例:weights = tf.Variable(tf.random.normal(shape=(input_size, output_size), dtype=tf.float32)) biases = tf.Variable(tf.random.normal(shape=(output_size,), dtype=tf.float32)) -
定义模型结构:在获得数据并初始化变量之后,我们必须定义模型。这是通过构建计算图来完成的。这个示例中的模型将是一个逻辑回归模型(logit E(Y) = bX + a):
logits = tf.add(tf.matmul(X, weights), biases) -
声明损失函数:在定义模型之后,我们必须能够评估输出。这就是我们声明损失函数的地方。损失函数非常重要,因为它告诉我们预测值与实际值之间的偏差。不同类型的损失函数将在 第二章,TensorFlow 实践方式 中的 实现反向传播 这一章节中详细探讨。在此,我们以交叉熵为例,使用 logits 计算 softmax 交叉熵与标签之间的差异:
loss = tf.reduce_mean( tf.nn.softmax_cross_entropy_with_logits(labels, logits)) -
初始化并训练模型:现在我们已经准备好了一切,需要创建图的实例,输入数据,并让 TensorFlow 调整变量,以更好地预测我们的训练数据。以下是初始化计算图的一种方法,通过多次迭代,使用 SDG 优化器收敛模型结构中的权重:
optimizer = tf.optimizers.SGD(learning_rate) with tf.GradientTape() as tape: logits = tf.add(tf.matmul(X, weights), biases) loss = tf.reduce_mean( tf.nn.softmax_cross_entropy_with_logits(labels, logits)) gradients = tape.gradient(loss, [weights, biases]) optimizer.apply_gradients(zip(gradients, [weights, biases])) -
评估模型:一旦我们建立并训练了模型,我们应该通过某些指定的标准评估模型,看看它在新数据上的表现如何。我们会在训练集和测试集上进行评估,这些评估将帮助我们判断模型是否存在过拟合或欠拟合的问题。我们将在后续的内容中讨论这个问题。在这个简单的例子中,我们评估最终的损失,并将拟合值与真实的训练值进行比较:
print(f"final loss is: {loss.numpy():.3f}") preds = tf.math.argmax(tf.add(tf.matmul(X, weights), biases), axis=1) ground_truth = tf.math.argmax(labels, axis=1) for y_true, y_pred in zip(ground_truth.numpy(), preds.numpy()): print(f"real label: {y_true} fitted: {y_pred}") -
调整超参数:大多数时候,我们会希望回到之前的步骤,调整一些超参数,并根据我们的测试结果检查模型的性能。然后,我们使用不同的超参数重复之前的步骤,并在验证集上评估模型。
-
部署/预测新结果:了解如何对新数据和未见过的数据进行预测也是一个关键要求。一旦我们训练好模型,就可以通过 TensorFlow 轻松实现这一点。
它是如何工作的……
在 TensorFlow 中,我们必须先设置数据、输入变量和模型结构,然后才能告诉程序训练并调整其权重,以提高预测效果。TensorFlow 通过计算图来完成这项工作。计算图是没有递归的有向图,允许并行计算。
为此,我们需要创建一个损失函数,以便 TensorFlow 最小化它。TensorFlow 通过修改计算图中的变量来实现这一点。TensorFlow 能够修改变量,因为它跟踪模型中的计算,并自动计算变量的梯度(如何改变每个变量),以最小化损失。因此,我们可以看到,进行更改并尝试不同数据源是多么容易。
另见
-
有关 TensorFlow 的进一步介绍以及更多资源,请参考 TensorFlow 官方页面上的官方文档和教程:
www.tensorflow.org/ -
在官方页面中,一个更具百科全书性质的入门位置是官方 Python API 文档,
www.tensorflow.org/api_docs/python/,在那里您可以找到所有可能的命令列表。 -
还有教程可供学习:
www.tensorflow.org/tutorials/ -
除此之外,还可以在这里找到一个非官方的 TensorFlow 教程、项目、演示和代码库集合:
github.com/dragen1860/TensorFlow-2.x-Tutorials
声明变量和张量
张量是 TensorFlow 用于在计算图上进行操作的主要数据结构。即使在 TensorFlow 2.x 中,这一方面被隐藏了,但数据流图仍然在幕后运行。这意味着构建神经网络的逻辑在 TensorFlow 1.x 和 TensorFlow 2.x 之间并没有发生太大变化。最引人注目的变化是,您不再需要处理占位符,后者是 TensorFlow 1.x 图中数据的输入门。
现在,您只需将张量声明为变量,然后继续构建图。
张量 是一个数学术语,指的是广义的向量或矩阵。如果向量是一维的,矩阵是二维的,那么张量就是 n 维的(其中 n 可以是 1、2 或更大)。
我们可以将这些张量声明为变量,并将它们用于计算。为了做到这一点,我们首先需要学习如何创建张量。
准备工作
当我们创建一个张量并将其声明为变量时,TensorFlow 会在我们的计算图中创建多个图结构。还需要指出的是,仅仅创建一个张量并不会向计算图中添加任何内容。TensorFlow 仅在执行操作以初始化变量后才会这样做。有关更多信息,请参阅下节关于变量和占位符的内容。
如何做到这一点…
在这里,我们将介绍在 TensorFlow 中创建张量的四种主要方式。
在本食谱或其他食谱中,我们不会进行不必要的详细说明。我们倾向于仅说明不同 API 调用中的必需参数,除非您认为覆盖某些可选参数对食谱有帮助;当这种情况发生时,我们会说明其背后的理由。
-
固定大小的张量:
- 在以下代码中,我们创建了一个全为 0 的张量:
row_dim, col_dim = 3, 3 zero_tsr = tf.zeros(shape=[row_dim, col_dim], dtype=tf.float32)- 在以下代码中,我们创建了一个全为 1 的张量:
ones_tsr = tf.ones([row_dim, col_dim])- 在以下代码中,我们创建了一个常量填充的张量:
filled_tsr = tf.fill([row_dim, col_dim], 42)- 在以下代码中,我们从一个现有常量创建了一个张量:
constant_tsr = tf.constant([1,2,3])请注意,
tf.constant()函数可以用来将一个值广播到数组中,通过写tf.constant(42, [row_dim, col_dim])来模仿tf.fill()的行为。 -
相似形状的张量:我们也可以根据其他张量的形状初始化变量,如下所示:
zeros_similar = tf.zeros_like(constant_tsr) ones_similar = tf.ones_like(constant_tsr)请注意,由于这些张量依赖于先前的张量,我们必须按顺序初始化它们。尝试以随机顺序初始化张量会导致错误。
-
序列张量:在 TensorFlow 中,所有的参数都被文档化为张量。即使需要标量,API 也会将其作为零维标量提及。因此,TensorFlow 允许我们指定包含定义区间的张量也就不足为奇了。以下函数的行为与 NumPy 的
linspace()输出和range()输出非常相似(参考:docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html)。请看以下函数:linear_tsr = tf.linspace(start=0.0, stop=1.0, num=3)请注意,起始值和终止值参数应为浮点值,而
num应为整数。结果张量的序列为[0.0, 0.5, 1.0](
print(linear_tsr命令将提供必要的输出)。请注意,此函数包括指定的终止值。以下是tf.range函数的对比:integer_seq_tsr = tf.range(start=6, limit=15, delta=3)结果是序列[6, 9, 12]。请注意,此函数不包括限制值,并且可以处理起始值和限制值的整数和浮点数。
-
随机张量:以下生成的随机数来自均匀分布:
randunif_tsr = tf.random.uniform([row_dim, col_dim], minval=0, maxval=1)
请注意,这种随机均匀分布是从包含minval但不包含maxval的区间中抽取的(minval <= x < maxval)。因此,在这种情况下,输出范围是[0, 1)。如果你需要仅抽取整数而不是浮点数,只需在调用函数时添加dtype=tf.int32参数。
若要获得从正态分布中随机抽取的张量,可以运行以下代码:
randnorm_tsr = tf.random.normal([row_dim, col_dim],
mean=0.0, stddev=1.0)
还有一些情况,我们希望生成在某些范围内保证的正态分布随机值。truncated_normal()函数总是从指定均值的两个标准差内挑选正态值:
runcnorm_tsr = tf.random.truncated_normal([row_dim, col_dim],
mean=0.0, stddev=1.0)
我们可能还对随机化数组的条目感兴趣。为此,两个函数可以帮助我们:random.shuffle()和image.random_crop()。以下代码执行此操作:
shuffled_output = tf.random.shuffle(input_tensor)
cropped_output = tf.image.random_crop(input_tensor, crop_size)
在本书后续的内容中,我们将关注对大小为(高度,宽度,3)的图像进行随机裁剪,其中包含三种颜色光谱。为了在cropped_output中固定某一维度,你必须为该维度指定最大值:
height, width = (64, 64)
my_image = tf.random.uniform([height, width, 3], minval=0,
maxval=255, dtype=tf.int32)
cropped_image = tf.image.random_crop(my_image,
[height//2, width//2, 3])
这段代码将生成随机噪声图像,这些图像会被裁剪,既能减小高度和宽度的一半,但深度维度不会受影响,因为你已将其最大值固定为参数。
它是如何工作的…
一旦我们决定了如何创建张量,我们还可以通过将张量包装在Variable()函数中来创建相应的变量,如下所示:
my_var = tf.Variable(tf.zeros([row_dim, col_dim]))
在接下来的示例中会有更多内容介绍。
还有更多…
我们不局限于内置函数:我们可以将任何 NumPy 数组转换为 Python 列表,或使用 convert_to_tensor() 函数将常量转换为张量。注意,这个函数也接受张量作为输入,以便我们希望在函数内对计算进行通用化时使用。
使用急切执行
在开发深度且复杂的神经网络时,你需要不断地尝试不同的架构和数据。这在 TensorFlow 1.0 中是困难的,因为你总是需要从头到尾运行代码,以检查是否成功。TensorFlow 2.x 默认在急切执行模式下工作,这意味着你可以随着项目的进展,逐步开发并检查代码。这是个好消息;现在我们只需要理解如何在急切执行模式下进行实验,这样我们就能充分利用 TensorFlow 2.x 的这一特性。本教程将为你提供入门的基础知识。
准备开始
TensorFlow 1.x 的表现最优,因为它在编译静态计算图之后执行计算。所有计算都被分配并连接成一个图,你编译网络时,该图帮助 TensorFlow 执行计算,利用可用资源(多核 CPU 或多个 GPU)以最佳方式,并在资源之间以最及时高效的方式分配操作。这也意味着,无论如何,一旦你定义并编译了图,就不能在运行时对其进行更改,而必须从头开始实例化,这会带来额外的工作量。
在 TensorFlow 2.x 中,你仍然可以定义你的网络,编译并以最佳方式运行它,但 TensorFlow 开发团队现在默认采用了一种更为实验性的方式,允许立即评估操作,从而使调试更容易,尝试网络变种也更加方便。这就是所谓的急切执行。现在,操作返回的是具体的值,而不是指向稍后构建的计算图部分的指针。更重要的是,你现在可以在模型执行时使用主机语言的所有功能,这使得编写更复杂、更精细的深度学习解决方案变得更容易。
如何做……
你基本上不需要做任何事情;在 TensorFlow 2.x 中,急切执行(eager execution)是默认的操作方式。当你导入 TensorFlow 并开始使用它的功能时,你会在急切执行模式下工作,因为你可以在执行时进行检查:
tf.executing_eagerly()
True
这就是你需要做的全部。
它是如何工作的……
只需运行 TensorFlow 操作,结果将立即返回:
x = [[2.]]
m = tf.matmul(x, x)
print("the result is {}".format(m))
the result is [[4.]]
就是这么简单!
还有更多…
由于 TensorFlow 现在默认启用了即时执行模式,你不会惊讶地发现 tf.Session 已经从 TensorFlow API 中移除。你不再需要在运行计算之前构建计算图;现在你只需构建你的网络,并在过程中进行测试。这为常见的软件最佳实践打开了道路,比如文档化代码、在编写代码时使用面向对象编程,以及将代码组织成可重用的自包含模块。
与矩阵的操作
理解 TensorFlow 如何与矩阵协作在开发数据流通过计算图时非常重要。在这个方案中,我们将涵盖矩阵的创建以及可以使用 TensorFlow 执行的基本操作。
值得强调的是,矩阵在机器学习(以及数学中的一般应用)中的重要性:机器学习算法在计算上是通过矩阵运算来表示的。了解如何进行矩阵运算是使用 TensorFlow 时的一个加分项,尽管你可能不需要经常使用它;其高级模块 Keras 可以在后台处理大多数矩阵代数内容(更多关于 Keras 的内容请参见第三章,Keras)。
本书不涵盖矩阵属性和矩阵代数(线性代数)的数学背景,因此不熟悉的读者强烈建议学习足够的矩阵知识,以便能够熟练掌握矩阵代数。在另见部分,你可以找到一些资源,帮助你复习微积分技巧或从零开始学习,以便从 TensorFlow 中获得更多收益。
准备工作
许多算法依赖于矩阵运算。TensorFlow 为我们提供了易于使用的操作来执行这些矩阵计算。你只需要导入 TensorFlow,并按照本节内容进行操作;如果你不是矩阵代数专家,请首先查看本方案的另见部分,寻找帮助你充分理解下面方案的资源。
如何操作…
我们按如下步骤进行:
-
创建矩阵:我们可以从 NumPy 数组或嵌套列表创建二维矩阵,正如本章开头的声明和使用变量与张量方案中所描述的那样。我们可以使用张量创建函数,并为如
zeros()、ones()和truncated_normal()等函数指定二维形状。TensorFlow 还允许我们使用diag()函数从一维数组或列表创建对角矩阵,示例如下:identity_matrix = tf.linalg.diag([1.0, 1.0, 1.0]) A = tf.random.truncated_normal([2, 3]) B = tf.fill([2,3], 5.0) C = tf.random.uniform([3,2]) D = tf.convert_to_tensor(np.array([[1., 2., 3.], [-3., -7., -1.], [0., 5., -2.]]), dtype=tf.float32) print(identity_matrix) [[ 1\. 0\. 0.] [ 0\. 1\. 0.] [ 0\. 0\. 1.]] print(A) [[ 0.96751703 0.11397751 -0.3438891 ] [-0.10132604 -0.8432678 0.29810596]] print(B) [[ 5\. 5\. 5.] [ 5\. 5\. 5.]] print(C) [[ 0.33184157 0.08907614] [ 0.53189191 0.67605299] [ 0.95889051 0.67061249]]请注意,C 张量是随机创建的,它可能与你的会话中显示的内容有所不同。
print(D) [[ 1\. 2\. 3.] [-3\. -7\. -1.] [ 0\. 5\. -2.]] -
加法、减法和乘法:要对相同维度的矩阵进行加法、减法或乘法,TensorFlow 使用以下函数:
print(A+B) [[ 4.61596632 5.39771316 4.4325695 ] [ 3.26702736 5.14477345 4.98265553]] print(B-B) [[ 0\. 0\. 0.] [ 0\. 0\. 0.]] print(tf.matmul(B, identity_matrix)) [[ 5\. 5\. 5.] [ 5\. 5\. 5.]]需要注意的是,
matmul()函数有一些参数,用于指定是否在乘法前转置参数(布尔参数transpose_a和transpose_b),或者每个矩阵是否为稀疏矩阵(a_is_sparse和b_is_sparse)。如果你需要对两个形状和类型相同的矩阵进行逐元素乘法(这非常重要,否则会报错),只需使用
tf.multiply函数:print(tf.multiply(D, identity_matrix)) [[ 1\. 0\. 0.] [-0\. -7\. -0.] [ 0\. 0\. -2.]]请注意,矩阵除法没有明确定义。虽然许多人将矩阵除法定义为乘以逆矩阵,但它在本质上不同于实数除法。
-
转置:转置矩阵(翻转列和行),如下所示:
print(tf.transpose(C)) [[0.33184157 0.53189191 0.95889051] [0.08907614 0.67605299 0.67061249]]再次提到,重新初始化会给我们不同于之前的值。
-
行列式:要计算行列式,请使用以下代码:
print(tf.linalg.det(D)) -38.0 -
逆矩阵:要找到一个方阵的逆矩阵,请参阅以下内容:
print(tf.linalg.inv(D)) [[-0.5 -0.5 -0.5 ] [ 0.15789474 0.05263158 0.21052632] [ 0.39473684 0.13157895 0.02631579]]逆矩阵方法仅在矩阵是对称正定时,基于 Cholesky 分解。如果矩阵不是对称正定的,则基于 LU 分解。
-
分解:对于 Cholesky 分解,请使用以下代码:
print(tf.linalg.cholesky(identity_matrix)) [[ 1\. 0\. 1.] [ 0\. 1\. 0.] [ 0\. 0\. 1.]] -
特征值和特征向量:对于特征值和特征向量,请使用以下代码:
print(tf.linalg.eigh(D)) [[-10.65907521 -0.22750691 2.88658212] [ 0.21749542 0.63250104 -0.74339638] [ 0.84526515 0.2587998 0.46749277] [ -0.4880805 0.73004459 0.47834331]]
请注意,tf.linalg.eigh()函数输出两个张量:第一个张量包含特征值,第二个张量包含特征向量。在数学中,这种操作称为矩阵的特征分解。
它的工作原理……
TensorFlow 为我们提供了所有开始进行数值计算的工具,并将这些计算添加到我们的神经网络中。
另见
如果你需要快速提升微积分技能,并深入了解更多关于 TensorFlow 操作的内容,我们建议以下资源:
-
这本免费的书籍《机器学习数学》(Mathematics for Machine Learning),可以在这里找到:
mml-book.github.io/。如果你想在机器学习领域中成功操作,这本书包含了你需要知道的一切。 -
对于一个更加易于获取的资源,可以观看 Khan Academy 的关于向量和矩阵的课程(
www.khanacademy.org/math/precalculus),以便学习神经网络中最基本的数据元素。
声明操作
除了矩阵操作,TensorFlow 还有许多其他操作我们至少应该了解。这个教程将为你提供一个简要且必要的概览,帮助你掌握真正需要知道的内容。
准备好
除了标准的算术操作外,TensorFlow 还为我们提供了更多需要了解的操作。在继续之前,我们应该认识到这些操作并学习如何使用它们。再次提醒,我们只需要导入 TensorFlow:
import tensorflow as tf
现在我们准备运行接下来的代码。
如何操作……
TensorFlow 提供了张量的标准操作,即add()、subtract()、multiply()和division(),它们都位于math模块中。请注意,本节中的所有操作,除非另有说明,否则都将逐元素计算输入:
-
TensorFlow 还提供了
division()及相关函数的变种。 -
值得注意的是,
division()返回与输入相同类型的结果。这意味着如果输入是整数,它实际上返回除法的地板值(类似 Python 2)。要返回 Python 3 版本的除法,即在除法前将整数转换为浮点数并始终返回浮点数,TensorFlow 提供了truediv()函数,具体如下:print(tf.math.divide(3, 4)) 0.75 print(tf.math.truediv(3, 4)) tf.Tensor(0.75, shape=(), dtype=float64) -
如果我们有浮点数并且需要整数除法,可以使用
floordiv()函数。请注意,这仍然会返回浮点数,但它会向下舍入到最接近的整数。该函数如下:print(tf.math.floordiv(3.0,4.0)) tf.Tensor(0.0, shape=(), dtype=float32) -
另一个重要的函数是
mod()。该函数返回除法后的余数,具体如下:print(tf.math.mod(22.0, 5.0)) tf.Tensor(2.0, shape=(), dtype=float32) -
两个张量的叉积通过
cross()函数实现。请记住,叉积仅对两个三维向量定义,因此它只接受两个三维张量。以下代码展示了这一用法:print(tf.linalg.cross([1., 0., 0.], [0., 1., 0.])) tf.Tensor([0\. 0\. 1.], shape=(3,), dtype=float32) -
下面是常用数学函数的简明列表。所有这些函数均逐元素操作:
函数 操作 tf.math.abs()输入张量的绝对值 tf.math.ceil()输入张量的向上取整函数 tf.math.cos()输入张量的余弦函数 tf.math.exp()输入张量的底数 e 指数函数 tf.math.floor()输入张量的向下取整函数 tf.linalg.inv()输入张量的乘法逆(1/x) tf.math.log()输入张量的自然对数 tf.math.maximum()两个张量的逐元素最大值 tf.math.minimum()两个张量的逐元素最小值 tf.math.negative()输入张量的负值 tf.math.pow()第一个张量按逐元素方式升至第二个张量 tf.math.round()对输入张量进行四舍五入 tf.math.rsqrt()一个张量的平方根倒数 tf.math.sign()根据张量的符号返回 -1、0 或 1 tf.math.sin()输入张量的正弦函数 tf.math.sqrt()输入张量的平方根 tf.math.square()输入张量的平方 -
特殊数学函数:有一些在机器学习中经常使用的特殊数学函数值得一提,TensorFlow 为它们提供了内建函数。再次强调,除非另有说明,否则这些函数都是逐元素操作:
tf.math.digamma() |
Psi 函数,即lgamma()函数的导数 |
|---|---|
tf.math.erf() |
一个张量的高斯误差函数(逐元素) |
tf.math.erfc() |
一个张量的互补误差函数 |
tf.math.igamma() |
下正则化不完全伽马函数 |
tf.math.igammac() |
上不完全伽马函数的正则化形式 |
tf.math.lbeta() |
beta 函数绝对值的自然对数 |
tf.math.lgamma() |
伽马函数绝对值的自然对数 |
tf.math.squared_difference() |
计算两个张量之间差值的平方 |
它是如何工作的……
了解哪些函数对我们可用是很重要的,这样我们才能将它们添加到我们的计算图中。我们将主要关注前面提到的函数。我们也可以通过组合这些函数生成许多不同的自定义函数,如下所示:
# Tangent function (tan(pi/4)=1)
def pi_tan(x):
return tf.tan(3.1416/x)
print(pi_tan(4))
tf.Tensor(1.0000036, shape=(), dtype=float32)
组成深度神经网络的复杂层仅由前面的函数组成,因此,凭借这个教程,你已经掌握了创建任何你想要的内容所需的所有基础知识。
还有更多……
如果我们希望向图中添加其他未列出的操作,我们必须从前面的函数中创建自己的操作。下面是一个示例,这是之前未使用过的操作,我们可以将其添加到图中。我们可以使用以下代码添加一个自定义的多项式函数,3 * x² - x + 10:
def custom_polynomial(value):
return tf.math.subtract(3 * tf.math.square(value), value) + 10
print(custom_polynomial(11))
tf.Tensor(362, shape=(), dtype=int32)
现在,你可以创建无限制的自定义函数,不过我始终建议你首先查阅 TensorFlow 文档。通常,你不需要重新发明轮子;你会发现你需要的功能已经被编码实现了。
实现激活函数
激活函数是神经网络逼近非线性输出并适应非线性特征的关键。它们在神经网络中引入非线性操作。如果我们小心选择激活函数并合理放置它们,它们是非常强大的操作,可以指示 TensorFlow 进行拟合和优化。
准备就绪
当我们开始使用神经网络时,我们会经常使用激活函数,因为激活函数是任何神经网络的重要组成部分。激活函数的目标就是调整权重和偏置。在 TensorFlow 中,激活函数是对张量进行的非线性操作。它们的作用类似于之前的数学操作。激活函数有很多用途,但主要的概念是它们在图中引入非线性,同时对输出进行归一化。
如何实现……
激活函数位于 TensorFlow 的 神经网络 (nn) 库中。除了使用内置的激活函数外,我们还可以使用 TensorFlow 操作设计自己的激活函数。我们可以导入预定义的激活函数(通过 tensorflow import nn),或者在函数调用中明确写出 nn。在这里,我们选择对每个函数调用显式声明:
-
修正线性单元(ReLU)是最常见和最基本的方式,用于在神经网络中引入非线性。这个函数就叫做
max(0,x)。它是连续的,但不光滑。它的形式如下:print(tf.nn.relu([-3., 3., 10.])) tf.Tensor([ 0\. 3\. 10.], shape=(3,), dtype=float32) -
有时我们希望限制前面 ReLU 激活函数的线性增涨部分。我们可以通过将
max(0,x)函数嵌套在min()函数中来实现。TensorFlow 实现的版本被称为 ReLU6 函数,定义为min(max(0,x),6)。这是一个硬 sigmoid 函数的版本,计算速度更快,并且不容易遇到梯度消失(趋近于零)或梯度爆炸的问题。这在我们后续讨论卷积神经网络和递归神经网络时会非常有用。它的形式如下:print(tf.nn.relu6([-3., 3., 10.])) tf.Tensor([ 0\. 3\. 6.], shape=(3,), dtype=float32) -
Sigmoid 函数是最常见的连续且平滑的激活函数。它也被称为 logistic 函数,形式为 1 / (1 + exp(-x))。由于在训练过程中容易导致反向传播的梯度消失,sigmoid 函数并不常用。它的形式如下:
print(tf.nn.sigmoid([-1., 0., 1.])) tf.Tensor([0.26894143 0.5 0.7310586 ], shape=(3,), dtype=float32)我们应该意识到一些激活函数,如 sigmoid,并不是零中心的。这将要求我们在使用这些函数前对数据进行零均值化处理,特别是在大多数计算图算法中。
-
另一个平滑的激活函数是双曲正切函数。双曲正切函数与 sigmoid 函数非常相似,只不过它的范围不是 0 到 1,而是 -1 到 1。这个函数的形式是双曲正弦与双曲余弦的比值。另一种写法如下:
((exp(x) – exp(-x))/(exp(x) + exp(-x))这个激活函数如下:
print(tf.nn.tanh([-1., 0., 1.])) tf.Tensor([-0.7615942 0\. 0.7615942], shape=(3,), dtype=float32) -
softsign函数也被用作激活函数。这个函数的形式是 x/(|x| + 1)。softsign函数应该是符号函数的一个连续(但不光滑)近似。见以下代码:print(tf.nn.softsign([-1., 0., -1.])) tf.Tensor([-0.5 0\. -0.5], shape=(3,), dtype=float32) -
另一个函数是
softplus函数,它是 ReLU 函数的平滑版本。这个函数的形式是 log(exp(x) + 1)。它的形式如下:print(tf.nn.softplus([-1., 0., -1.])) tf.Tensor([0.31326166 0.6931472 0.31326166], shape=(3,), dtype=float32)softplus函数随着输入的增大趋于无穷大,而softsign函数则趋向 1。不过,随着输入变小,softplus函数接近零,而softsign函数则趋向 -1。 -
指数线性单元 (ELU) 与 softplus 函数非常相似,只不过它的下渐近线是 -1,而不是 0。其形式为 (exp(x) + 1),当 x < 0 时;否则为 x。它的形式如下:
print(tf.nn.elu([-1., 0., -1.])) tf.Tensor([-0.63212055 0\. -0.63212055], shape=(3,), dtype=float32) -
现在,通过这个公式,你应该能理解基本的关键激活函数。我们列出的现有激活函数并不全面,你可能会发现对于某些问题,你需要尝试其中一些不太常见的函数。除了这个公式中的激活函数,你还可以在 Keras 激活函数页面上找到更多激活函数:
www.tensorflow.org/api_docs/python/tf/keras/activations
它的工作原理…
这些激活函数是我们未来可以在神经网络或其他计算图中引入非线性的方法。需要注意的是,我们在网络中的哪个位置使用了激活函数。如果激活函数的值域在 0 和 1 之间(如 sigmoid),那么计算图只能输出 0 到 1 之间的值。如果激活函数位于节点之间并被隐藏,那么我们需要注意这个范围对张量的影响,特别是在通过张量时。如果我们的张量被缩放为零均值,我们将希望使用一个能够尽可能保持零附近方差的激活函数。
这意味着我们希望选择一个激活函数,比如双曲正切(tanh)或softsign。如果张量都被缩放为正数,那么我们理想中会选择一个能够保持正域方差的激活函数。
还有更多…
我们甚至可以轻松创建自定义的激活函数,如 Swish,公式为 x**sigmoid(x)(参见 Swish: a Self-Gated Activation Function, Ramachandran 等,2017,arxiv.org/abs/1710.05941),它可以作为 ReLU 激活函数在图像和表格数据问题中的一个更高效的替代品:
def swish(x):
return x * tf.nn.sigmoid(x)
print(swish([-1., 0., 1.]))
tf.Tensor([-0.26894143 0\. 0.7310586 ], shape=(3,), dtype=float32)
在尝试过 TensorFlow 提供的激活函数后,你的下一步自然是复制那些你在深度学习论文中找到的激活函数,或者你自己创建的激活函数。
处理数据源
本书的大部分内容都将依赖于使用数据集来训练机器学习算法。本节提供了如何通过 TensorFlow 和 Python 访问这些数据集的说明。
一些数据源依赖于外部网站的维护,以便你能够访问数据。如果这些网站更改或删除了数据,那么本节中的部分代码可能需要更新。你可以在本书的 GitHub 页面上找到更新后的代码:
github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook
准备就绪
在本书中,我们将使用的大多数数据集可以通过 TensorFlow 数据集(TensorFlow Datasets)访问,而一些其他的数据集则需要额外的努力,可能需要使用 Python 脚本来下载,或者通过互联网手动下载。
TensorFlow 数据集(TFDS)是一个现成可用的数据集集合(完整列表可以在此处找到:www.tensorflow.org/datasets/catalog/overview)。它自动处理数据的下载和准备,并且作为 tf.data 的封装器,构建高效且快速的数据管道。
为了安装 TFDS,只需在控制台中运行以下安装命令:
pip install tensorflow-datasets
现在,我们可以继续探索本书中你将使用的核心数据集(并非所有这些数据集都会包含在内,只有最常见的几个数据集会被介绍,其他一些非常特定的数据集将在本书的不同章节中介绍)。
如何操作…
-
鸢尾花数据集:这个数据集可以说是机器学习中经典的结构化数据集,可能也是所有统计学示例中的经典数据集。它是一个测量三种不同类型鸢尾花的萼片长度、萼片宽度、花瓣长度和花瓣宽度的数据集:Iris setosa、Iris virginica 和 Iris versicolor。总共有 150 个测量值,这意味着每个物种有 50 个测量值。要在 Python 中加载该数据集,我们将使用 TFDS 函数,代码如下:
import tensorflow_datasets as tfds iris = tfds.load('iris', split='train')当你第一次导入数据集时,下载数据集时会显示一个进度条,指示你所在的位置。如果你不想看到进度条,可以通过输入以下代码来禁用它:
tfds.disable_progress_bar() -
出生体重数据:该数据最初来自 1986 年 Baystate 医疗中心(马萨诸塞州斯普林菲尔德)。该数据集包含了出生体重和母亲的其他人口统计学及医学测量数据,以及家庭病史的记录。数据集有 189 条记录,包含 11 个变量。以下代码展示了如何将该数据作为
tf.data.dataset来访问:import tensorflow_datasets as tfds birthdata_url = 'https://raw.githubusercontent.com/PacktPublishing/TensorFlow-2-Machine-Learning-Cookbook-Third-Edition/master/birthweight.dat' path = tf.keras.utils.get_file(birthdata_url.split("/")[-1], birthdata_url) def map_line(x): return tf.strings.to_number(tf.strings.split(x)) birth_file = (tf.data .TextLineDataset(path) .skip(1) # Skip first header line .map(map_line) ) -
波士顿房价数据集:卡内基梅隆大学在其
StatLib库中维护了一系列数据集。该数据可以通过加州大学欧文分校的机器学习仓库轻松访问(archive.ics.uci.edu/ml/index.php)。该数据集包含 506 条房价观察记录,以及各种人口统计数据和房屋属性(14 个变量)。以下代码展示了如何在 TensorFlow 中访问该数据:import tensorflow_datasets as tfds housing_url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data' path = tf.keras.utils.get_file(housing_url.split("/")[-1], housing_url) def map_line(x): return tf.strings.to_number(tf.strings.split(x)) housing = (tf.data .TextLineDataset(path) .map(map_line) ) -
MNIST 手写数字数据集:美国国家标准与技术研究院(NIST)的手写数据集的子集即为MNIST数据集。MNIST 手写数字数据集托管在 Yann LeCun 的网站上(
yann.lecun.com/exdb/mnist/)。该数据库包含 70,000 张单个数字(0-9)的图像,其中大约 60,000 张用于训练集,10,000 张用于测试集。这个数据集在图像识别中使用非常频繁,以至于 TensorFlow 提供了内置函数来访问该数据。在机器学习中,提供验证数据以防止过拟合(目标泄露)也是很重要的。因此,TensorFlow 将训练集中的 5,000 张图像分配为验证集。以下代码展示了如何在 TensorFlow 中访问此数据:import tensorflow_datasets as tfds mnist = tfds.load('mnist', split=None) mnist_train = mnist['train'] mnist_test = mnist['test'] -
垃圾邮件-正常邮件文本数据。UCI 的机器学习数据集库也包含了一个垃圾邮件-正常邮件文本数据集。我们可以访问这个
.zip文件并获取垃圾邮件-正常邮件文本数据,方法如下:import tensorflow_datasets as tfds zip_url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip' path = tf.keras.utils.get_file(zip_url.split("/")[-1], zip_url, extract=True) path = path.replace("smsspamcollection.zip", "SMSSpamCollection") def split_text(x): return tf.strings.split(x, sep='\t') text_data = (tf.data .TextLineDataset(path) .map(split_text) ) -
电影评论数据:康奈尔大学的 Bo Pang 发布了一个电影评论数据集,将评论分类为好或坏。你可以在康奈尔大学网站上找到该数据:
www.cs.cornell.edu/people/pabo/movie-review-data/。要下载、解压并转换这些数据,我们可以运行以下代码:import tensorflow_datasets as tfds movie_data_url = 'http://www.cs.cornell.edu/people/pabo/movie-review-data/rt-polaritydata.tar.gz' path = tf.keras.utils.get_file(movie_data_url.split("/")[-1], movie_data_url, extract=True) path = path.replace('.tar.gz', '') with open(path+filename, 'r', encoding='utf-8', errors='ignore') as movie_file: for response, filename in enumerate(['\\rt-polarity.neg', '\\rt-polarity.pos']): with open(path+filename, 'r') as movie_file: for line in movie_file: review_file.write(str(response) + '\t' + line.encode('utf-8').decode()) def split_text(x): return tf.strings.split(x, sep='\t') movies = (tf.data .TextLineDataset('movie_reviews.txt') .map(split_text) ) -
CIFAR-10 图像数据:加拿大高级研究院发布了一个包含 8000 万标记彩色图像的图像集(每张图像的尺寸为 32 x 32 像素)。该数据集包含 10 个不同的目标类别(如飞机、汽车、鸟类等)。CIFAR-10 是一个子集,包含 60,000 张图像,其中训练集有 50,000 张图像,测试集有 10,000 张图像。由于我们将在多种方式下使用该数据集,并且它是我们较大的数据集之一,我们不会每次都运行脚本来获取它。要获取该数据集,只需执行以下代码来下载 CIFAR-10 数据集(这可能需要较长时间):
import tensorflow_datasets as tfds ds, info = tfds.load('cifar10', shuffle_files=True, with_info=True) print(info) cifar_train = ds['train'] cifar_test = ds['test'] -
莎士比亚作品文本数据:Project Gutenberg 是一个发布免费书籍电子版的项目。他们已经将莎士比亚的所有作品汇编在一起。以下代码展示了如何通过 TensorFlow 访问这个文本文件:
import tensorflow_datasets as tfds shakespeare_url = 'https://raw.githubusercontent.com/PacktPublishing/TensorFlow-2-Machine-Learning-Cookbook-Third-Edition/master/shakespeare.txt' path = tf.keras.utils.get_file(shakespeare_url.split("/")[-1], shakespeare_url) def split_text(x): return tf.strings.split(x, sep='\n') shakespeare_text = (tf.data .TextLineDataset(path) .map(split_text) ) -
英语-德语句子翻译数据:Tatoeba 项目(
tatoeba.org)收集了多种语言的句子翻译。他们的数据已根据创意共享许可证发布。从这些数据中,ManyThings.org(www.manythings.org)编译了可供下载的文本文件,包含逐句翻译。在这里,我们将使用英语-德语翻译文件,但你可以根据需要更改 URL 来使用其他语言:import os import pandas as pd from zipfile import ZipFile from urllib.request import urlopen, Request import tensorflow_datasets as tfds sentence_url = 'https://www.manythings.org/anki/deu-eng.zip' r = Request(sentence_url, headers={'User-Agent': 'Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11'}) b2 = [z for z in sentence_url.split('/') if '.zip' in z][0] #gets just the '.zip' part of the url with open(b2, "wb") as target: target.write(urlopen(r).read()) #saves to file to disk with ZipFile(b2) as z: deu = [line.split('\t')[:2] for line in z.open('deu.txt').read().decode().split('\n')] os.remove(b2) #removes the zip file # saving to disk prepared en-de sentence file with open("deu.txt", "wb") as deu_file: for line in deu: data = ",".join(line)+'\n' deu_file.write(data.encode('utf-8')) def split_text(x): return tf.strings.split(x, sep=',') text_data = (tf.data .TextLineDataset("deu.txt") .map(split_text) )
使用完这个数据集后,我们已完成对本书中您在使用配方时最常遇到的数据集的回顾。在每个配方开始时,我们会提醒您如何下载相关数据集,并解释它为何与该配方相关。
工作原理……
当涉及到在某个配方中使用这些数据集时,我们将参考本节内容,并假定数据已经按照我们刚才描述的方式加载。如果需要进一步的数据转换或预处理,相关代码将在配方中提供。
通常,当我们使用来自 TensorFlow 数据集的数据时,方法通常如下所示:
import tensorflow_datasets as tfds
dataset_name = "..."
data = tfds.load(dataset_name, split=None)
train = data['train']
test = data['test']
无论如何,根据数据的位置,可能需要下载、解压并转换它。
另见
这是我们在本书中使用的部分数据资源的附加参考资料:
-
Hosmer, D.W., Lemeshow, S., 和 Sturdivant, R. X.(2013 年) 《应用逻辑回归:第 3 版》
-
Lichman, M. (2013). UCI 机器学习库:
archive.ics.uci.edu/ml。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院 -
Bo Pang, Lillian Lee 和 Shivakumar Vaithyanathan,好评?使用机器学习技术进行情感分类,EMNLP 2002 会议论文:
www.cs.cornell.edu/people/pabo/movie-review-data/ -
Krizhevsky. (2009). 从小图像中学习多层特征:
www.cs.toronto.edu/~kriz/cifar.html -
Project Gutenberg. 访问于 2016 年 4 月:
www.gutenberg.org/
其他资源
在这一部分,你将找到更多的链接、文档资源和教程,这些在学习和使用 TensorFlow 时会提供很大帮助。
准备工作
在学习如何使用 TensorFlow 时,知道在哪里寻求帮助或提示是很有帮助的。本节列出了启动 TensorFlow 和解决问题的一些资源。
如何做到……
以下是 TensorFlow 资源的列表:
-
本书的代码可以在 Packt 仓库在线访问:
github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook -
TensorFlow 官方 Python API 文档位于
www.tensorflow.org/api_docs/python。在这里,你可以找到所有 TensorFlow 函数、对象和方法的文档及示例。 -
TensorFlow 的官方教程非常全面和详细,位于
www.tensorflow.org/tutorials/index.html。它们从图像识别模型开始,接着讲解 Word2Vec、RNN 模型以及序列到序列模型。它们还提供了生成分形图形和求解 PDE 系统的额外教程。请注意,他们会不断地向这个合集添加更多教程和示例。 -
TensorFlow 的官方 GitHub 仓库可以通过
github.com/tensorflow/tensorflow访问。在这里,你可以查看开源代码,甚至如果你愿意,可以分叉或克隆当前版本的代码。你也可以通过访问issues目录来查看当前已提交的问题。 -
TensorFlow 提供的一个由官方维护的公共 Docker 容器,始终保持最新版本,位于 Dockerhub:
hub.docker.com/r/tensorflow/tensorflow/。 -
Stack Overflow 是一个很好的社区帮助源。在这里有一个 TensorFlow 标签。随着 TensorFlow 越来越受欢迎,这个标签的讨论也在增长。要查看该标签的活动,可以访问
stackoverflow.com/questions/tagged/Tensorflow。 -
虽然 TensorFlow 非常灵活,能够用于许多用途,但它最常见的用途是深度学习。为了理解深度学习的基础、底层数学如何运作,并培养更多的深度学习直觉,Google 创建了一个在线课程,课程在 Udacity 上提供。要注册并参加这个视频讲座课程,请访问
www.udacity.com/course/deep-learning--ud730。 -
TensorFlow 还创建了一个网站,你可以在其中通过调整参数和数据集来直观地探索训练神经网络。访问
playground.tensorflow.org/来探索不同设置如何影响神经网络的训练。 -
Andrew Ng 讲授了一门名为《神经网络与深度学习》的在线课程:
www.coursera.org/learn/neural-networks-deep-learning -
斯坦福大学提供了一个在线大纲和详细的课程笔记,内容涉及卷积神经网络与视觉识别:
cs231n.stanford.edu/
第二章:TensorFlow 方式
在第一章,TensorFlow 2.x 入门中,我们介绍了 TensorFlow 如何创建张量并使用变量。在本章中,我们将介绍如何使用急切执行将这些对象组合在一起,从而动态地设置计算图。基于此,我们可以设置一个简单的分类器,并查看其表现如何。
另外,请记住,本书当前和更新后的代码可以在 GitHub 上在线获取,地址为github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook。
在本章中,我们将介绍 TensorFlow 操作的关键组件。然后,我们将把它们结合起来,创建一个简单的分类器并评估结果。在本章结束时,你应该已学到以下内容:
-
使用急切执行的操作
-
分层嵌套操作
-
使用多层
-
实现损失函数
-
实现反向传播
-
批量训练和随机训练
-
将所有内容结合在一起
让我们开始处理越来越复杂的配方,展示 TensorFlow 处理和解决数据问题的方法。
使用急切执行的操作
感谢第一章,TensorFlow 2.x 入门,我们已经能够创建 TensorFlow 中的对象,如变量。现在,我们将介绍针对这些对象的操作。为了做到这一点,我们将返回到急切执行,并通过一个新的基本配方展示如何操作矩阵。本配方及后续配方仍然是基础的,但在本章过程中,我们将把这些基础配方结合起来形成更复杂的配方。
准备开始
首先,我们加载 TensorFlow 和 NumPy,如下所示:
import TensorFlow as tf
import NumPy as np
这就是我们开始所需的一切;现在我们可以继续进行。
如何操作...
在这个例子中,我们将使用到目前为止学到的内容,将列表中的每个数字发送到 TensorFlow 命令进行计算并打印输出。
首先,我们声明我们的张量和变量。在所有可能的将数据传递给变量的方式中,我们将创建一个 NumPy 数组来传递给变量,然后将其用于操作:
x_vals = np.array([1., 3., 5., 7., 9.])
x_data = tf.Variable(x_vals, dtype=tf.float32)
m_const = tf.constant(3.)
operation = tf.multiply(x_data, m_const)
for result in operation:
print(result.NumPy())
上述代码的输出结果如下:
3.0
9.0
15.0
21.0
27.0
一旦你习惯了使用 TensorFlow 变量、常量和函数,就会自然而然地从 NumPy 数组数据开始,逐步构建数据结构和操作,并在过程中测试它们的结果。
它是如何工作的...
使用急切执行,TensorFlow 会立即评估操作值,而不是操作符号句柄,这些句柄指向计算图中的节点,后者将在之后编译和执行。因此,你可以直接遍历乘法操作的结果,并使用 .NumPy 方法打印出返回的 NumPy 对象,这个方法会从 TensorFlow 张量中返回一个 NumPy 对象。
分层嵌套操作
在这个示例中,我们将学习如何将多个操作结合起来工作;了解如何将操作串联在一起非常重要。这将为我们的网络设置分层操作进行执行。在这个示例中,我们将用两个矩阵乘以一个占位符,并执行加法操作。我们将输入两个以三维 NumPy 数组形式表示的矩阵。
这是另一个简单的示例,给你一些关于如何使用 TensorFlow 编写代码的灵感,利用函数或类等常见结构,提高代码的可读性和模块化性。即使最终产品是一个神经网络,我们依然在编写计算机程序,并且应该遵循编程最佳实践。
准备工作
和往常一样,我们只需要导入 TensorFlow 和 NumPy,代码如下:
import TensorFlow as tf
import NumPy as np
我们现在准备好继续进行我们的示例。
如何实现…
我们将输入两个大小为3 x 5的 NumPy 数组。我们将每个矩阵与大小为5 x 1的常量相乘,得到一个大小为3 x 1的矩阵。接着,我们将这个结果与一个1 x 1的矩阵相乘,最终得到一个3 x 1的矩阵。最后,我们在末尾加上一个3 x 1的矩阵,代码如下:
-
首先,我们创建要输入的数据和相应的占位符:
my_array = np.array([[1., 3., 5., 7., 9.], [-2., 0., 2., 4., 6.], [-6., -3., 0., 3., 6.]]) x_vals = np.array([my_array, my_array + 1]) x_data = tf.Variable(x_vals, dtype=tf.float32) -
然后,我们创建用于矩阵乘法和加法的常量:
m1 = tf.constant([[1.], [0.], [-1.], [2.], [4.]]) m2 = tf.constant([[2.]]) a1 = tf.constant([[10.]]) -
接下来,我们声明要立即执行的操作。作为好的编程实践,我们创建执行所需操作的函数:
def prod1(a, b): return tf.matmul(a, b) def prod2(a, b): return tf.matmul(a, b) def add1(a, b): return tf.add(a, b) -
最后,我们嵌套我们的函数并显示结果:
result = add1(prod2(prod1(x_data, m1), m2), a1) print(result.NumPy()) [[ 102.] [ 66.] [ 58.]] [[ 114.] [ 78.] [ 70.]]
使用函数(以及类,我们将进一步介绍)将帮助你编写更清晰的代码。这使得调试更加高效,并且便于代码的维护和重用。
它是如何工作的…
借助即时执行,我们不再需要使用那种"万事俱备"的编程风格(指的是将几乎所有内容都放在程序的全局作用域中;详情请见stackoverflow.com/questions/33779296/what-is-exact-meaning-of-kitchen-sink-in-programming)这种风格在使用 TensorFlow 1.x 时非常常见。目前,你可以选择采用函数式编程风格或面向对象编程风格,就像我们在这个简短示例中展示的那样,你可以将所有操作和计算按逻辑合理的方式排列,使其更易理解:
class Operations():
def __init__(self, a):
self.result = a
def apply(self, func, b):
self.result = func(self.result, b)
return self
operation = (Operations(a=x_data)
.apply(prod1, b=m1)
.apply(prod2, b=m2)
.apply(add1, b=a1))
print(operation.result.NumPy())
类比函数更能帮助你组织代码并提高重用性,这得益于类继承。
还有更多…
在这个示例中,我们需要在运行数据之前声明数据形状并知道操作的结果形状。并非所有情况下都如此。可能会有一维或二维我们事先不知道的情况,或者某些维度在数据处理过程中会发生变化。为此,我们将可以变化(或未知)的维度标记为None。
例如,要初始化一个具有未知行数的变量,我们将写下以下行,然后我们可以分配任意行数的值:
v = tf.Variable(initial_value=tf.random.normal(shape=(1, 5)),
shape=tf.TensorShape((None, 5)))
v.assign(tf.random.normal(shape=(10, 5)))
矩阵乘法具有灵活的行数是可以接受的,因为这不会影响我们操作的安排。当我们将数据馈送给多个大小批次时,这将在后续章节中非常方便。
虽然使用None作为维度允许我们使用可变大小的维度,但我建议在填写维度时尽可能明确。如果我们的数据大小预先已知,则应将该大小明确写入维度中。建议将None用作维度的使用限制在数据的批大小(或者我们同时计算的数据点数)。
处理多层
现在我们已经涵盖了多个操作,接下来我们将讨论如何连接各层,这些层中有数据通过它们传播。在本示例中,我们将介绍如何最佳连接各层,包括自定义层。我们将生成和使用的数据将代表小随机图像。最好通过一个简单的示例理解这种操作,并看看如何使用一些内置层执行计算。我们将探讨的第一层称为移动窗口。我们将在二维图像上执行一个小的移动窗口平均值,然后第二层将是一个自定义操作层。
移动窗口对与时间序列相关的所有内容都非常有用。尽管有专门用于序列的层,但在分析 MRI 扫描(神经影像)或声谱图时,移动窗口可能会很有用。
此外,我们将看到计算图可能变得很大,难以查看。为了解决这个问题,我们还将介绍如何命名操作并为层创建作用域。
准备工作
首先,您必须加载常用的包 - NumPy 和 TensorFlow - 使用以下内容:
import TensorFlow as tf
import NumPy as np
现在让我们继续进行配方。这次事情变得更加复杂和有趣。
如何做...
我们按以下步骤进行。
首先,我们用 NumPy 创建我们的示例二维图像。这个图像将是一个 4x4 像素的图像。我们将在四个维度上创建它;第一个和最后一个维度将具有大小为1(我们保持批维度不同,以便您可以尝试更改其大小)。请注意,某些 TensorFlow 图像函数将操作四维图像。这四个维度是图像编号、高度、宽度和通道,为了使它与一个通道的图像兼容,我们将最后一个维度明确设置为1,如下所示:
batch_size = [1]
x_shape = [4, 4, 1]
x_data = tf.random.uniform(shape=batch_size + x_shape)
要创建跨我们的 4x4 图像的移动窗口平均值,我们将使用一个内置函数,该函数将在形状为 2x2 的窗口上卷积一个常数。我们将使用的函数是conv2d();这个函数在图像处理和 TensorFlow 中经常使用。
此函数对我们指定的窗口和滤波器进行分段乘积。我们还必须在两个方向上指定移动窗口的步幅。在这里,我们将计算四个移动窗口的平均值:左上角、右上角、左下角和右下角的四个像素。我们通过创建一个 2 x 2 窗口,并且在每个方向上使用长度为 2 的步幅来实现这一点。为了取平均值,我们将 2 x 2 窗口与常数 0.25 做卷积,如下所示:
def mov_avg_layer(x):
my_filter = tf.constant(0.25, shape=[2, 2, 1, 1])
my_strides = [1, 2, 2, 1]
layer = tf.nn.conv2d(x, my_filter, my_strides,
padding='SAME', name='Moving_Avg_Window')
return layer
请注意,我们还通过函数的 name 参数将此层命名为 Moving_Avg_Window。
要计算卷积层的输出大小,我们可以使用以下公式:输出 = (W – F + 2P)/S + 1),其中 W 是输入大小,F 是滤波器大小,P 是零填充,S 是步幅。
现在,我们定义一个自定义层,该层将操作移动窗口平均值的 2 x 2 输出。自定义函数将首先将输入乘以另一个 2 x 2 矩阵张量,然后将每个条目加 1。之后,我们对每个元素取 sigmoid,并返回 2 x 2 矩阵。由于矩阵乘法仅适用于二维矩阵,我们需要去除大小为 1 的图像的额外维度。TensorFlow 可以使用内置的 squeeze() 函数来完成这一点。在这里,我们定义了新的层:
def custom_layer(input_matrix):
input_matrix_sqeezed = tf.squeeze(input_matrix)
A = tf.constant([[1., 2.], [-1., 3.]])
b = tf.constant(1., shape=[2, 2])
temp1 = tf.matmul(A, input_matrix_sqeezed)
temp = tf.add(temp1, b) # Ax + b
return tf.sigmoid(temp)
现在,我们必须安排网络中的两个层。我们将通过依次调用一个层函数来完成这一点,如下所示:
first_layer = mov_avg_layer(x_data)
second_layer = custom_layer(first_layer)
现在,我们只需将 4 x 4 图像输入函数中。最后,我们可以检查结果,如下所示:
print(second_layer)
tf.Tensor(
[[0.9385519 0.90720266]
[0.9247799 0.82272065]], shape=(2, 2), dtype=float32)
现在让我们更深入地了解它是如何工作的。
工作原理...
第一层被命名为 Moving_Avg_Window。第二层是称为 Custom_Layer 的一组操作。这两个层处理的数据首先在左侧被折叠,然后在右侧被扩展。正如示例所示,您可以将所有层封装到函数中并依次调用它们,以便后续层处理前一层的输出。
实现损失函数
对于这个示例,我们将介绍在 TensorFlow 中可以使用的一些主要损失函数。损失函数是机器学习算法的关键组成部分。它们衡量模型输出与目标(真实)值之间的距离。
为了优化我们的机器学习算法,我们需要评估结果。在 TensorFlow 中,评估结果取决于指定的损失函数。损失函数告诉 TensorFlow 模型输出与目标结果的好坏程度。在大多数情况下,我们会有一组数据和一个目标,用于训练我们的算法。损失函数比较目标和预测(它衡量模型输出与目标真实值之间的距离),并提供两者之间的数值量化。
准备工作
我们首先启动一个计算图,并加载 matplotlib,一个 Python 绘图包,如下所示:
import matplotlib.pyplot as plt
import TensorFlow as tf
既然我们已经准备好绘制图表了,让我们毫不拖延地进入配方部分。
如何实现...
首先,我们将讨论回归的损失函数,这意味着预测一个连续的因变量。首先,我们将创建一个预测序列和目标作为张量。我们将在-1和1之间的 500 个 x 值上输出结果。有关输出的图表,请参见如何工作...部分。使用以下代码:
x_vals = tf.linspace(-1., 1., 500)
target = tf.constant(0.)
L2 范数损失也称为欧几里得损失函数。它只是与目标的距离的平方。在这里,我们将假设目标为零来计算损失函数。L2 范数是一个很好的损失函数,因为它在接近目标时非常曲线,算法可以利用这一事实在接近零时更慢地收敛。我们可以按如下方式实现:
def l2(y_true, y_pred):
return tf.square(y_true - y_pred)
TensorFlow 有一个内置的 L2 范数形式,叫做tf.nn.l2_loss()。这个函数实际上是 L2 范数的一半。换句话说,它与之前的函数相同,只是除以了 2。
L1 范数损失也称为绝对损失函数。它不对差异进行平方处理,而是取绝对值。L1 范数比 L2 范数更适合处理离群值,因为它对较大值的陡峭度较低。需要注意的问题是,L1 范数在目标处不平滑,这可能导致算法无法很好地收敛。它的形式如下所示:
def l1(y_true, y_pred):
return tf.abs(y_true - y_pred)
伪-Huber 损失是Huber 损失函数的连续且平滑的近似。这个损失函数试图通过在接近目标时采用 L1 和 L2 范数的优点,并且对于极端值更加平缓,来结合这两者。它的形式取决于一个额外的参数delta,它决定了它的陡峭度。我们将绘制两个形式,delta1 = 0.25和delta2 = 5,以显示差异,如下所示:
def phuber1(y_true, y_pred):
delta1 = tf.constant(0.25)
return tf.multiply(tf.square(delta1), tf.sqrt(1\. +
tf.square((y_true - y_pred)/delta1)) - 1.)
def phuber2(y_true, y_pred):
delta2 = tf.constant(5.)
return tf.multiply(tf.square(delta2), tf.sqrt(1\. +
tf.square((y_true - y_pred)/delta2)) - 1.)
现在,我们将继续讨论分类问题的损失函数。分类损失函数用于评估在预测分类结果时的损失。通常,我们模型的输出是一个介于0和1之间的实数值。然后,我们选择一个阈值(通常选择 0.5),如果结果大于该阈值,则将结果分类为该类别。接下来,我们将考虑针对分类输出的各种损失函数。
首先,我们需要重新定义我们的预测值(x_vals)和target。我们将保存输出并在下一部分进行绘制。使用以下代码:
x_vals = tf.linspace(-3., 5., 500)
target = tf.fill([500,], 1.)
Hinge 损失主要用于支持向量机,但也可以用于神经网络。它用于计算两个目标类别之间的损失,1 和 -1。在以下代码中,我们使用目标值1,因此我们的预测值越接近1,损失值就越低:
def hinge(y_true, y_pred):
return tf.maximum(0., 1\. - tf.multiply(y_true, y_pred))
二分类情况下的交叉熵损失有时也被称为逻辑损失函数。当我们预测 0 或 1 两个类别时,便会使用该函数。我们希望衡量实际类别(0 或 1)与预测值之间的距离,预测值通常是介于 0 和 1 之间的实数。为了衡量这个距离,我们可以使用信息理论中的交叉熵公式,如下所示:
def xentropy(y_true, y_pred):
return (- tf.multiply(y_true, tf.math.log(y_pred)) -
tf.multiply((1\. - y_true), tf.math.log(1\. - y_pred)))
Sigmoid 交叉熵损失与之前的损失函数非常相似,不同之处在于我们在将 x 值代入交叉熵损失之前,使用 sigmoid 函数对其进行转换,如下所示:
def xentropy_sigmoid(y_true, y_pred):
return tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true,
logits=y_pred)
加权交叉熵损失是 sigmoid 交叉熵损失的加权版本。我们对正类目标进行加权。作为示例,我们将正类目标的权重设为 0.5,如下所示:
def xentropy_weighted(y_true, y_pred):
weight = tf.constant(0.5)
return tf.nn.weighted_cross_entropy_with_logits(labels=y_true,
logits=y_pred,
pos_weight=weight)
Softmax 交叉熵损失适用于非归一化的输出。该函数用于在目标类别只有一个而不是多个时,计算损失。因此,该函数通过 softmax 函数将输出转换为概率分布,然后根据真实的概率分布计算损失函数,如下所示:
def softmax_xentropy(y_true, y_pred):
return tf.nn.softmax_cross_entropy_with_logits(labels=y_true, logits=y_pred)
unscaled_logits = tf.constant([[1., -3., 10.]])
target_dist = tf.constant([[0.1, 0.02, 0.88]])
print(softmax_xentropy(y_true=target_dist, y_pred=unscaled_logits))
[ 1.16012561]
稀疏 softmax 交叉熵损失与 softmax 交叉熵损失几乎相同,区别在于目标不是概率分布,而是表示哪个类别是true的索引。我们传入的是该类别的索引,而不是一个稀疏的全零目标向量,其中有一个值是1,如下所示:
def sparse_xentropy(y_true, y_pred):
return tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=y_true,
logits=y_pred)
unscaled_logits = tf.constant([[1., -3., 10.]])
sparse_target_dist = tf.constant([2])
print(sparse_xentropy(y_true=sparse_target_dist,
y_pred=unscaled_logits))
[ 0.00012564]
现在让我们通过将这些损失函数绘制在图上,进一步理解它们是如何工作的。
它是如何工作的...
以下是如何使用matplotlib绘制回归损失函数:
x_vals = tf.linspace(-1., 1., 500)
target = tf.constant(0.)
funcs = [(l2, 'b-', 'L2 Loss'),
(l1, 'r--', 'L1 Loss'),
(phuber1, 'k-.', 'P-Huber Loss (0.25)'),
(phuber2, 'g:', 'P-Huber Loss (5.0)')]
for func, line_type, func_name in funcs:
plt.plot(x_vals, func(y_true=target, y_pred=x_vals),
line_type, label=func_name)
plt.ylim(-0.2, 0.4)
plt.legend(loc='lower right', prop={'size': 11})
plt.show()
我们从前面的代码中得到以下图示:

图 2.1:绘制不同的回归损失函数
以下是如何使用matplotlib绘制不同的分类损失函数:
x_vals = tf.linspace(-3., 5., 500)
target = tf.fill([500,], 1.)
funcs = [(hinge, 'b-', 'Hinge Loss'),
(xentropy, 'r--', 'Cross Entropy Loss'),
(xentropy_sigmoid, 'k-.', 'Cross Entropy Sigmoid Loss'),
(xentropy_weighted, 'g:', 'Weighted Cross Enropy Loss (x0.5)')]
for func, line_type, func_name in funcs:
plt.plot(x_vals, func(y_true=target, y_pred=x_vals),
line_type, label=func_name)
plt.ylim(-1.5, 3)
plt.legend(loc='lower right', prop={'size': 11})
plt.show()
我们从前面的代码中得到以下图示:

图 2.2:分类损失函数的绘图
每条损失曲线都为神经网络优化提供了不同的优势。接下来我们将进一步讨论这些内容。
还有更多...
这里有一张表格,总结了我们刚刚以图形方式描述的不同损失函数的属性和优点:
| 损失函数 | 用途 | 优势 | 缺点 |
|---|---|---|---|
| L2 | 回归 | 更稳定 | 鲁棒性差 |
| L1 | 回归 | 更具鲁棒性 | 稳定性差 |
| 伪-Huber | 回归 | 更具鲁棒性和稳定性 | 多了一个参数 |
| Hinge | 分类 | 在支持向量机(SVM)中创建最大间隔 | 损失无界,受异常值影响 |
| 交叉熵 | 分类 | 更稳定 | 损失无界,鲁棒性差 |
剩余的分类损失函数都与交叉熵损失的类型有关。交叉熵 Sigmoid 损失函数适用于未缩放的 logits,并且优先于计算 Sigmoid 损失,然后计算交叉熵损失,因为 TensorFlow 有更好的内置方法来处理数值边缘情况。Softmax 交叉熵和稀疏 softmax 交叉熵也是如此。
这里描述的大多数分类损失函数适用于两类预测。可以通过对每个预测/目标求和来将其扩展到多类。
在评估模型时,还有许多其他指标可供参考。以下是一些需要考虑的其他指标列表:
| 模型指标 | 描述 |
|---|---|
| R-squared (决定系数) | 对于线性模型,这是因变量方差中由独立数据解释的比例。对于具有较多特征的模型,请考虑使用调整后的 R-squared。 |
| 均方根误差 | 对于连续模型,这衡量了预测值与实际值之间的差异,通过平均平方误差的平方根。 |
| 混淆矩阵 | 对于分类模型,我们查看预测类别与实际类别的矩阵。完美的模型在对角线上有所有计数。 |
| 召回率 | 对于分类模型而言,这是真正例占所有预测正例的比例。 |
| 精确度 | 对于分类模型而言,这是真正例占所有实际正例的比例。 |
| F-score | 对于分类模型而言,这是精确度和召回率的调和平均值。 |
在选择正确的指标时,您必须同时评估您要解决的问题(因为每个指标的行为会有所不同,并且根据手头的问题,一些损失最小化策略对我们的问题可能比其他更好),并且对神经网络的行为进行实验。
实施反向传播
使用 TensorFlow 的好处之一是它可以跟踪操作并根据反向传播自动更新模型变量。在这个教程中,我们将介绍如何在训练机器学习模型时利用这一方面。
准备就绪
现在,我们将介绍如何以使得损失函数最小化的方式改变模型中的变量。我们已经学会了如何使用对象和操作,以及如何创建损失函数来衡量预测与目标之间的距离。现在,我们只需告诉 TensorFlow 如何通过网络反向传播错误,以便更新变量,使损失函数最小化。这通过声明一个优化函数来实现。一旦声明了优化函数,TensorFlow 将遍历计算图中的所有计算,找出反向传播项。当我们输入数据并最小化损失函数时,TensorFlow 将相应地修改网络中的变量。
对于这个食谱,我们将做一个非常简单的回归算法。我们将从一个正态分布中采样随机数,均值为 1,标准差为 0.1。然后,我们将这些数字通过一次操作,操作是将它们乘以一个权重张量,然后加上一个偏置张量。由此,损失函数将是输出和目标之间的 L2 范数。我们的目标将与输入高度相关,因此任务不会太复杂,但该食谱会非常具有示范性,并且可以轻松地用于更复杂的问题。
第二个例子是一个非常简单的二分类算法。在这里,我们将从两个正态分布中生成 100 个数字,N(-3,1) 和 N(3,1)。所有来自 N(-3, 1) 的数字将属于目标类别 0,而所有来自 N(3, 1) 的数字将属于目标类别 1。用于区分这些类别的模型(它们是完全可分的)将再次是一个线性模型,并根据 sigmoid 交叉熵损失函数进行优化,因此,首先对模型结果进行 sigmoid 转换,然后计算交叉熵损失函数。
虽然指定合适的学习率有助于算法的收敛,但我们还必须指定一种优化类型。从前面的两个例子中,我们使用的是标准的梯度下降法。这是通过 tf.optimizers.SGD TensorFlow 函数实现的。
如何操作...
我们将从回归示例开始。首先,我们加载通常伴随我们食谱的数值 Python 库,NumPy 和 TensorFlow:
import NumPy as np
import TensorFlow as tf
接下来,我们创建数据。为了使一切易于复制,我们希望将随机种子设置为特定值。我们将在我们的食谱中始终重复这一点,这样我们就能精确获得相同的结果;你可以自己检查,通过简单地更改种子数值,随机性是如何影响食谱中的结果的。
此外,为了确保目标和输入有很好的相关性,可以绘制这两个变量的散点图:
np.random.seed(0)
x_vals = np.random.normal(1, 0.1, 100).astype(np.float32)
y_vals = (x_vals * (np.random.normal(1, 0.05, 100) - 0.5)).astype(np.float32)
plt.scatter(x_vals, y_vals)
plt.show()

图 2.3:x_vals 和 y_vals 的散点图
我们将网络的结构(类型为 bX + a 的线性模型)添加为一个函数:
def my_output(X, weights, biases):
return tf.add(tf.multiply(X, weights), biases)
接下来,我们将我们的 L2 损失函数添加到网络的结果中进行应用:
def loss_func(y_true, y_pred):
return tf.reduce_mean(tf.square(y_pred - y_true))
现在,我们必须声明一种方法来优化图中的变量。我们声明一个优化算法。大多数优化算法需要知道在每次迭代中应该走多远。这种距离由学习率控制。设置正确的值对于我们处理的问题是特定的,因此我们只能通过实验来找到合适的设置。无论如何,如果我们的学习率太高,算法可能会超过最小值,但如果学习率太低,算法可能会需要太长时间才能收敛。
学习率对收敛有很大影响,我们将在本节末尾再次讨论它。虽然我们使用的是标准的梯度下降算法,但还有许多其他替代选择。例如,有些优化算法的操作方式不同,取决于问题的不同,它们可以达到更好的或更差的最优解。关于不同优化算法的全面概述,请参见 Sebastian Ruder 在 另见 部分列出的文章:
my_opt = tf.optimizers.SGD(learning_rate=0.02)
关于最优学习率的理论有很多。这是机器学习算法中最难解决的问题之一。有关学习率与特定优化算法之间关系的好文章,列在此食谱末尾的 另见 部分。
现在,我们可以初始化网络变量(weights 和 biases),并设置一个记录列表(命名为 history),以帮助我们可视化优化步骤:
tf.random.set_seed(1)
np.random.seed(0)
weights = tf.Variable(tf.random.normal(shape=[1]))
biases = tf.Variable(tf.random.normal(shape=[1]))
history = list()
最后一步是循环我们的训练算法,并告诉 TensorFlow 进行多次训练。我们将训练 100 次,并在每 25 次迭代后打印结果。为了训练,我们将选择一个随机的 x 和 y 输入,并将其输入到图中。TensorFlow 会自动计算损失,并略微调整权重和偏置,以最小化损失:
for i in range(100):
rand_index = np.random.choice(100)
rand_x = [x_vals[rand_index]]
rand_y = [y_vals[rand_index]]
with tf.GradientTape() as tape:
predictions = my_output(rand_x, weights, biases)
loss = loss_func(rand_y, predictions)
history.append(loss.NumPy())
gradients = tape.gradient(loss, [weights, biases])
my_opt.apply_gradients(zip(gradients, [weights, biases]))
if (i + 1) % 25 == 0:
print(f'Step # {i+1} Weights: {weights.NumPy()} Biases: {biases.NumPy()}')
print(f'Loss = {loss.NumPy()}')
Step # 25 Weights: [-0.58009654] Biases: [0.91217995]
Loss = 0.13842473924160004
Step # 50 Weights: [-0.5050226] Biases: [0.9813488]
Loss = 0.006441597361117601
Step # 75 Weights: [-0.4791306] Biases: [0.9942327]
Loss = 0.01728087291121483
Step # 100 Weights: [-0.4777394] Biases: [0.9807473]
Loss = 0.05371852591633797
在循环中,tf.GradientTape() 使 TensorFlow 能够追踪计算过程,并计算相对于观测变量的梯度。GradientTape() 范围内的每个变量都会被监控(请记住,常量不会被监控,除非你明确使用命令 tape.watch(constant) 来监控它)。一旦完成监控,你可以计算目标相对于一组源的梯度(使用命令 tape.gradient(target, sources)),并获得一个渐变的急切张量,可以应用于最小化过程。此操作会自动通过用新值更新源(在我们的例子中是 weights 和 biases 变量)来完成。
当训练完成后,我们可以可视化优化过程在连续梯度应用下的变化:
plt.plot(history)
plt.xlabel('iterations')
plt.ylabel('loss')
plt.show()

图 2.4:我们方法中 L2 损失随迭代的变化
到这个阶段,我们将介绍简单分类示例的代码。我们可以使用相同的 TensorFlow 脚本,做一些更新。记住,我们将尝试找到一组最优的权重和偏置,使得数据能够分成两类。
首先,我们从两个不同的正态分布 N(-3, 1) 和 N(3, 1) 拉取数据。我们还将生成目标标签,并可视化这两类如何在我们的预测变量上分布:
np.random.seed(0)
x_vals = np.concatenate((np.random.normal(-3, 1, 50),
np.random.normal(3, 1, 50))
).astype(np.float32)
y_vals = np.concatenate((np.repeat(0., 50), np.repeat(1., 50))).astype(np.float32)
plt.hist(x_vals[y_vals==1], color='b')
plt.hist(x_vals[y_vals==0], color='r')
plt.show()

图 2.5:x_vals 上的类别分布
由于这个问题的特定损失函数是 Sigmoid 交叉熵,我们更新我们的损失函数:
def loss_func(y_true, y_pred):
return tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true,
logits=y_pred))
接下来,我们初始化变量:
tf.random.set_seed(1)
np.random.seed(0)
weights = tf.Variable(tf.random.normal(shape=[1]))
biases = tf.Variable(tf.random.normal(shape=[1]))
history = list()
最后,我们对一个随机选择的数据点进行了数百次循环,并相应地更新了weights和biases变量。像之前一样,每进行 25 次迭代,我们会打印出变量的值和损失值:
for i in range(100):
rand_index = np.random.choice(100)
rand_x = [x_vals[rand_index]]
rand_y = [y_vals[rand_index]]
with tf.GradientTape() as tape:
predictions = my_output(rand_x, weights, biases)
loss = loss_func(rand_y, predictions)
history.append(loss.NumPy())
gradients = tape.gradient(loss, [weights, biases])
my_opt.apply_gradients(zip(gradients, [weights, biases]))
if (i + 1) % 25 == 0:
print(f'Step {i+1} Weights: {weights.NumPy()} Biases: {biases.NumPy()}')
print(f'Loss = {loss.NumPy()}')
Step # 25 Weights: [-0.01804185] Biases: [0.44081175]
Loss = 0.5967269539833069
Step # 50 Weights: [0.49321094] Biases: [0.37732077]
Loss = 0.3199256658554077
Step # 75 Weights: [0.7071932] Biases: [0.32154965]
Loss = 0.03642747551202774
Step # 100 Weights: [0.8395616] Biases: [0.30409005]
Loss = 0.028119442984461784
一个图表,也可以在这种情况下,揭示优化过程的进展:
plt.plot(history)
plt.xlabel('iterations')
plt.ylabel('loss')
plt.show()

图 2.6:我们方法中通过迭代的 Sigmoid 交叉熵损失
图表的方向性很清晰,尽管轨迹有些崎岖,因为我们一次只学习一个示例,从而使学习过程具有决定性的随机性。图表还可能指出需要稍微降低学习率的必要性。
它是如何工作的...
总结和解释一下,对于这两个示例,我们做了以下操作:
-
我们创建了数据。两个示例都需要将数据加载到计算网络所用的特定变量中。
-
我们初始化了变量。我们使用了一些随机高斯值,但初始化本身是一个独立的话题,因为最终的结果可能在很大程度上取决于我们如何初始化网络(只需在初始化前更改随机种子即可找到答案)。
-
我们创建了一个损失函数。回归使用了 L2 损失,分类使用了交叉熵损失。
-
我们定义了一个优化算法。两个算法都使用了梯度下降法。
-
我们遍历了随机数据样本,以逐步更新我们的变量。
还有更多...
正如我们之前提到的,优化算法对学习率的选择很敏感。总结这一选择的影响非常重要,可以简明扼要地表述如下:
| 学习率大小 | 优点/缺点 | 用途 |
|---|---|---|
| 较小的学习率 | 收敛较慢,但结果更准确 | 如果解不稳定,首先尝试降低学习率 |
| 较大的学习率 | 准确性较低,但收敛速度较快 | 对于某些问题,有助于防止解停滞 |
有时,标准的梯度下降算法可能会被卡住或显著变慢。这可能发生在优化停留在鞍点的平坦区域时。为了解决这个问题,解决方案是考虑动量项,它加上了上一步梯度下降值的一部分。你可以通过在tf.optimizers.SGD中设置动量和 Nesterov 参数,以及你的学习率,来实现这个解决方案(详细信息请参见 www.TensorFlow.org/api_docs/python/tf/keras/optimizers/SGD)。
另一种变体是对我们模型中每个变量的优化器步骤进行变化。理想情况下,我们希望对移动较小的变量采取较大的步长,对变化较快的变量采取较小的步长。我们不会深入探讨这种方法的数学原理,但这种思想的常见实现被称为Adagrad 算法。该算法考虑了变量梯度的整个历史。在 TensorFlow 中,这个函数被称为 AdagradOptimizer() (www.TensorFlow.org/api_docs/python/tf/keras/optimizers/Adagrad)。
有时,Adagrad 会因为考虑到整个历史而过早地将梯度推到零。解决这个问题的方法是限制我们使用的步数。这就是Adadelta 算法。我们可以通过使用 AdadeltaOptimizer() 函数来应用这一方法 (www.TensorFlow.org/api_docs/python/tf/keras/optimizers/Adadelta)。
还有一些其他不同梯度下降算法的实现。关于这些,请参阅 TensorFlow 文档 www.TensorFlow.org/api_docs/python/tf/keras/optimizers。
另见
关于优化算法和学习率的一些参考文献,请参阅以下论文和文章:
-
本章的食谱如下:
-
实现损失函数部分。
-
实现反向传播部分。
-
-
Kingma, D., Jimmy, L. Adam: 随机优化方法。ICLR 2015
arxiv.org/pdf/1412.6980.pdf -
Ruder, S. 梯度下降优化算法概述。2016
arxiv.org/pdf/1609.04747v1.pdf -
Zeiler, M. ADADelta: 一种自适应学习率方法。2012
arxiv.org/pdf/1212.5701.pdf
使用批量和随机训练
当 TensorFlow 根据反向传播更新我们的模型变量时,它可以处理从单个数据观察(如我们在之前的食谱中所做的)到一次处理大量数据的情况。仅处理一个训练示例可能导致非常不稳定的学习过程,而使用过大的批量则可能在计算上非常昂贵。选择合适的训练类型对于使我们的机器学习算法收敛到一个解非常关键。
准备工作
为了让 TensorFlow 计算反向传播所需的变量梯度,我们必须在一个或多个样本上测量损失。随机训练每次只处理一个随机抽样的数据-目标对,就像我们在前面的示例中所做的那样。另一个选择是一次处理一大部分训练样本,并对梯度计算进行损失平均。训练批次的大小可以有所不同,最大可以包括整个数据集。这里,我们将展示如何将之前使用随机训练的回归示例扩展为批量训练。
我们将首先加载 NumPy、matplotlib 和 TensorFlow,如下所示:
import matplotlib as plt
import NumPy as np
import TensorFlow as tf
现在我们只需编写脚本并在 如何做... 部分中测试我们的配方。
如何做...
我们从声明批次大小开始。这将是我们一次性通过计算图输入的观察数据量:
batch_size = 20
接下来,我们只需对之前用于回归问题的代码进行一些小的修改:
np.random.seed(0)
x_vals = np.random.normal(1, 0.1, 100).astype(np.float32)
y_vals = (x_vals * (np.random.normal(1, 0.05, 100) - 0.5)).astype(np.float32)
def loss_func(y_true, y_pred):
return tf.reduce_mean(tf.square(y_pred - y_true))
tf.random.set_seed(1)
np.random.seed(0)
weights = tf.Variable(tf.random.normal(shape=[1]))
biases = tf.Variable(tf.random.normal(shape=[1]))
history_batch = list()
for i in range(50):
rand_index = np.random.choice(100, size=batch_size)
rand_x = [x_vals[rand_index]]
rand_y = [y_vals[rand_index]]
with tf.GradientTape() as tape:
predictions = my_output(rand_x, weights, biases)
loss = loss_func(rand_y, predictions)
history_batch.append(loss.NumPy())
gradients = tape.gradient(loss, [weights, biases])
my_opt.apply_gradients(zip(gradients, [weights, biases]))
if (i + 1) % 25 == 0:
print(f'Step # {i+1} Weights: {weights.NumPy()} \
Biases: {biases.NumPy()}')
print(f'Loss = {loss.NumPy()}')
自从上一个示例以来,我们已经学会了在网络和成本函数中使用矩阵乘法。此时,我们只需要处理由更多行组成的输入,即批量而非单个样本。我们甚至可以将其与之前的方法进行比较,后者我们现在可以称之为随机优化:
tf.random.set_seed(1)
np.random.seed(0)
weights = tf.Variable(tf.random.normal(shape=[1]))
biases = tf.Variable(tf.random.normal(shape=[1]))
history_stochastic = list()
for i in range(50):
rand_index = np.random.choice(100, size=1)
rand_x = [x_vals[rand_index]]
rand_y = [y_vals[rand_index]]
with tf.GradientTape() as tape:
predictions = my_output(rand_x, weights, biases)
loss = loss_func(rand_y, predictions)
history_stochastic.append(loss.NumPy())
gradients = tape.gradient(loss, [weights, biases])
my_opt.apply_gradients(zip(gradients, [weights, biases]))
if (i + 1) % 25 == 0:
print(f'Step # {i+1} Weights: {weights.NumPy()} \
Biases: {biases.NumPy()}')
print(f'Loss = {loss.NumPy()}')
运行代码后,将使用批量数据重新训练我们的网络。此时,我们需要评估结果,获取一些关于它如何工作的直觉,并反思这些结果。让我们继续进入下一部分。
它是如何工作的...
批量训练和随机训练在优化方法和收敛性方面有所不同。找到合适的批量大小可能是一个挑战。为了观察批量训练和随机训练在收敛上的差异,建议你调整批次大小至不同的水平。
两种方法的视觉对比能更好地解释,使用批量训练如何在这个问题上得到与随机训练相同的优化结果,尽管在过程中波动较少。以下是生成同一回归问题的随机训练和批量损失图的代码。请注意,批量损失曲线更加平滑,而随机损失曲线则更加不稳定:
plt.plot(history_stochastic, 'b-', label='Stochastic Loss')
plt.plot(history_batch, 'r--', label='Batch Loss')
plt.legend(loc='upper right', prop={'size': 11})
plt.show()

图 2.7:使用随机优化和批量优化时的 L2 损失比较
现在我们的图表显示了一个更平滑的趋势线。存在的波动问题可以通过降低学习率和调整批次大小来解决。
还有更多...
| 训练类型 | 优势 | 劣势 |
|---|---|---|
| 随机训练 | 随机性有助于摆脱局部最小值 | 通常需要更多的迭代才能收敛 |
| 批量训练 | 更快地找到最小值 | 计算所需资源更多 |
将一切结合在一起
在本节中,我们将结合迄今为止的所有内容,并创建一个用于鸢尾花数据集的分类器。鸢尾花数据集在第一章《TensorFlow 入门》中的与数据源一起使用部分中有更详细的描述。我们将加载该数据并创建一个简单的二分类器,用于预测一朵花是否为鸢尾花 setosa。需要明确的是,这个数据集有三种花卉,但我们只预测一朵花是否为某种单一的品种,即鸢尾花 setosa 或非 setosa,从而使我们得到一个二分类器。
准备开始
我们将首先加载所需的库和数据,然后相应地转换目标数据。首先,我们加载所需的库。对于鸢尾花数据集,我们需要 TensorFlow Datasets 模块,这是我们在之前的例子中没有用到的。请注意,我们还在这里加载了matplotlib,因为我们接下来想要绘制结果图:
import matplotlib.pyplot as plt
import NumPy as np
import TensorFlow as tf
import TensorFlow_datasets as tfds
如何操作...
作为起点,我们首先使用全局变量声明我们的批次大小:
batch_size = 20
接下来,我们加载鸢尾花数据集。我们还需要将目标数据转换为1或0,表示该目标是否为 setosa。由于鸢尾花数据集中 setosa 的标记为0,我们将把所有目标值为0的标签改为1,其他值则改为0。我们还将只使用两个特征,花瓣长度和花瓣宽度。这两个特征分别是数据集每行中的第三和第四个条目:
iris = tfds.load('iris', split='train[:90%]', W)
iris_test = tfds.load('iris', split='train[90%:]', as_supervised=True)
def iris2d(features, label):
return features[2:], tf.cast((label == 0), dtype=tf.float32)
train_generator = (iris
.map(iris2d)
.shuffle(buffer_size=100)
.batch(batch_size)
)
test_generator = iris_test.map(iris2d).batch(1)
如前一章所示,我们使用 TensorFlow 的数据集函数加载数据并执行必要的转换,通过创建一个数据生成器动态地向网络提供数据,而不是将数据保存在内存中的 NumPy 矩阵中。第一步,我们加载数据,并指定要将其拆分(使用参数split='train[:90%]'和split='train[90%:]')。这样我们可以保留数据集的 10%作为模型评估所用,使用未参与训练阶段的数据。
我们还指定了参数as_supervised=True,该参数将允许我们在遍历数据集时,将数据作为特征和标签的元组进行访问。
现在,我们通过应用连续的转换,将数据集转换为一个可迭代的生成器。我们打乱数据,定义生成器返回的批次大小,最重要的是,我们应用一个自定义函数,过滤并同时转换数据集返回的特征和标签。
然后,我们定义线性模型。该模型将采用通常的形式 bX+a。请记住,TensorFlow 内置了带有 sigmoid 函数的损失函数,因此我们只需要在 sigmoid 函数之前定义模型的输出:
def linear_model(X, A, b):
my_output = tf.add(tf.matmul(X, A), b)
return tf.squeeze(my_output)
现在,我们使用 TensorFlow 内置的sigmoid_cross_entropy_with_logits()函数添加 sigmoid 交叉熵损失函数:
def xentropy(y_true, y_pred):
return tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true,
logits=y_pred))
我们还必须告诉 TensorFlow 如何通过声明优化方法来优化我们的计算图。我们将希望最小化交叉熵损失。我们还选择了0.02作为我们的学习率:
my_opt = tf.optimizers.SGD(learning_rate=0.02)
现在,我们将使用 300 次迭代来训练我们的线性模型。我们将输入所需的三个数据点:花瓣长度、花瓣宽度和目标变量。每 30 次迭代,我们会打印变量的值:
tf.random.set_seed(1)
np.random.seed(0)
A = tf.Variable(tf.random.normal(shape=[2, 1]))
b = tf.Variable(tf.random.normal(shape=[1]))
history = list()
for i in range(300):
iteration_loss = list()
for features, label in train_generator:
with tf.GradientTape() as tape:
predictions = linear_model(features, A, b)
loss = xentropy(label, predictions)
iteration_loss.append(loss.NumPy())
gradients = tape.gradient(loss, [A, b])
my_opt.apply_gradients(zip(gradients, [A, b]))
history.append(np.mean(iteration_loss))
if (i + 1) % 30 == 0:
print(f'Step # {i+1} Weights: {A.NumPy().T} \
Biases: {b.NumPy()}')
print(f'Loss = {loss.NumPy()}')
Step # 30 Weights: [[-1.1206311 1.2985772]] Biases: [1.0116111]
Loss = 0.4503694772720337
…
Step # 300 Weights: [[-1.5611029 0.11102282]] Biases: [3.6908474]
Loss = 0.10326375812292099
如果我们绘制损失与迭代次数的关系图,我们可以从损失随时间平滑减少的趋势中看出,线性模型的学习过程相对简单:
plt.plot(history)
plt.xlabel('iterations')
plt.ylabel('loss')
plt.show()

图 2.8:Iris setosa 数据的交叉熵误差
我们将通过检查在保留的测试数据上的表现来结束。本次我们仅取测试数据集中的示例。如预期的那样,得到的交叉熵值与训练时的值相似:
predictions = list()
labels = list()
for features, label in test_generator:
predictions.append(linear_model(features, A, b).NumPy())
labels.append(label.NumPy()[0])
test_loss = xentropy(np.array(labels), np.array(predictions)).NumPy()
print(f"test cross-entropy is {test_loss}")
test cross-entropy is 0.10227929800748825
下一组命令提取模型变量并在图表上绘制这条线:
coefficients = np.ravel(A.NumPy())
intercept = b.NumPy()
# Plotting batches of examples
for j, (features, label) in enumerate(train_generator):
setosa_mask = label.NumPy() == 1
setosa = features.NumPy()[setosa_mask]
non_setosa = features.NumPy()[~setosa_mask]
plt.scatter(setosa[:,0], setosa[:,1], c='red', label='setosa')
plt.scatter(non_setosa[:,0], non_setosa[:,1], c='blue', label='Non-setosa')
if j==0:
plt.legend(loc='lower right')
# Computing and plotting the decision function
a = -coefficients[0] / coefficients[1]
xx = np.linspace(plt.xlim()[0], plt.xlim()[1], num=10000)
yy = a * xx - intercept / coefficients[1]
on_the_plot = (yy > plt.ylim()[0]) & (yy < plt.ylim()[1])
plt.plot(xx[on_the_plot], yy[on_the_plot], 'k--')
plt.xlabel('Petal Length')
plt.ylabel('Petal Width')
plt.show()
结果图表位于它是如何工作的...一节中,我们还讨论了所获得结果的有效性和可重现性。
它是如何工作的...
我们的目标是通过仅使用花瓣宽度和花瓣长度,在 Iris setosa 点和其他两种物种之间拟合一条直线。如果我们绘制这些点,并用一条线将分类为零的区域与分类为一的区域分开,我们可以看到我们已经实现了这一目标:

图 2.9:Iris setosa 和非 setosa 的花瓣宽度与花瓣长度关系图;实线是我们在 300 次迭代后获得的线性分隔线
分隔线的定义方式取决于数据、网络架构和学习过程。不同的初始情况,甚至由于神经网络权重的随机初始化,可能会为你提供稍微不同的解决方案。
还有更多...
虽然我们成功地实现了用一条线分隔两个类别的目标,但这可能不是最适合分隔两个类别的模型。例如,在添加新观察数据后,我们可能会发现我们的解决方案不能很好地分隔这两个类别。随着我们进入下一章,我们将开始处理那些通过提供测试、随机化和专用层来解决这些问题的方案,这些方法将增强我们方案的泛化能力。
另见
-
关于 Iris 数据集的更多信息,请参见文档:
archive.ics.uci.edu/ml/datasets/iris。 -
如果你想更深入了解机器学习算法的决策边界绘制,我们强烈推荐 Navoneel Chakrabarty 在 Medium 上发表的这篇精彩文章:
towardsdatascience.com/decision-boundary-visualization-a-z-6a63ae9cca7d
第三章:Keras
本章将重点介绍名为 Keras 的高层 TensorFlow API。
本章结束时,您应该对以下内容有更好的理解:
-
Keras 顺序 API
-
Keras 功能 API
-
Keras 子类化 API
-
Keras 预处理 API
介绍
在上一章中,我们讲解了 TensorFlow 的基础知识,现在我们已经能够设置计算图。本章将介绍 Keras,一个用 Python 编写的高层神经网络 API,支持多个后端,TensorFlow 是其中之一。Keras 的创建者是法国软件工程师兼 AI 研究员 François Chollet,他最初为个人使用而开发 Keras,直到 2015 年开源。Keras 的主要目标是提供一个易于使用且易于访问的库,以便快速实验。
TensorFlow v1 存在可用性问题,特别是 API 庞大且有时令人困惑。例如,TensorFlow v1 提供了两个高层 API:
-
Estimator API(在 1.1 版本中添加)用于在本地主机或分布式环境中训练模型。
-
Keras API 在之后的版本(1.4.0 发布)中被添加,并旨在用于快速原型设计。
随着 TensorFlow v2 的发布,Keras 成为官方高层 API。Keras 可以扩展并适应不同的用户需求,从研究到应用开发,从模型训练到部署。Keras 提供四大优势:易于使用(不牺牲灵活性和性能)、模块化、可组合、可扩展。
TensorFlow Keras API 与 Keras API 相同。然而,Keras 在 TensorFlow 后端中的实现已针对 TensorFlow 进行了优化。它集成了 TensorFlow 特有的功能,如急切执行、数据管道和 Estimator。
Keras 作为独立库与 Keras 作为 TensorFlow 集成的实现之间的区别仅在于导入方式。
这是导入 Keras API 规范的命令:
import keras
这是 TensorFlow 对 Keras API 规范的实现:
import tensorflow as tf
from tensorflow import keras
现在,让我们从发现 Keras 的基本构建块开始。
理解 Keras 层
Keras 层是 Keras 模型的基本构建块。每一层接收数据作为输入,执行特定任务,并返回输出。
Keras 包括广泛的内置层:
-
核心层:Dense、Activation、Flatten、Input、Reshape、Permute、RepeatVector、SpatialDropOut 等。
-
卷积层用于卷积神经网络:Conv1D、Conv2D、SeparableConv1D、Conv3D、Cropping2D 等。
-
池化层执行下采样操作以减少特征图:MaxPooling1D、AveragePooling2D 和 GlobalAveragePooling3D。
-
循环层用于循环神经网络处理循环或序列数据:RNN、SimpleRNN、GRU、LSTM、ConvLSTM2D 等。
-
嵌入层,仅用作模型中的第一层,将正整数转换为固定大小的稠密向量。
-
合并层: 加法、减法、乘法、平均值、最大值、最小值等多种操作。
-
高级激活层: LeakyReLU、PReLU、Softmax、ReLU 等。
-
批量归一化层,它会在每个批次中归一化前一层的激活值。
-
噪声层: GaussianNoise、GaussianDropout 和 AlphaDropout。
-
层封装器: TimeDistributed 将层应用于输入的每个时间切片,Bidirectional 是 RNN 的双向封装器。
-
局部连接层: LocallyConnected1D 和 LocallyConnected2D。它们的工作方式类似于 Conv1D 或 Conv2D,但不共享权重。
我们还可以按照本章中 Keras 子类化 API 部分的说明编写自己的 Keras 层。
准备开始
首先,我们将回顾一些在所有 Keras 层中常见的方法。这些方法对于了解层的配置和状态非常有用。
如何操作...
-
让我们从层的权重开始。权重可能是层中最重要的概念;它决定了输入对输出的影响程度,表示了层的状态。
get_weights()函数返回层的权重,以 NumPy 数组的列表形式:layer.get_weights()set_weights()方法可以通过一组 Numpy 数组来设置层的权重:layer.set_weights(weights) -
正如我们将在 Keras 函数式 API 配方中解释的那样,有时神经网络的拓扑结构不是线性的。在这种情况下,层可以在网络中多次使用(共享层)。如果层是单一节点(无共享层),我们可以通过这个命令轻松获得层的输入和输出:
layer.input layer.output或者如果层包含多个节点,可以使用这个:
layer.get_input_at(node_index) layer.get_output_at(node_index) -
如果层是单一节点(无共享层),我们还可以通过这个命令轻松获得层的输入和输出形状:
layer.input_shape layer.output_shape或者如果层包含多个节点,可以使用这个:
layer.get_input_shape_at(node_index) layer.get_output_shape_at(node_index) -
现在,我们将讨论层的配置。由于相同的层可能会实例化多次,配置中不包括权重或连接信息。
get_config()函数返回一个字典,包含层的配置:layer.get_config()from_config()方法用于实例化层的配置:layer.from_config(config)请注意,层配置存储在关联数组(Python 字典)中,这是一种将键映射到值的数据结构。
它是如何工作的...
层是模型的构建模块。Keras 提供了广泛的构建层和有用的方法,让你更深入地了解模型的工作原理。
使用 Keras,我们可以通过三种方式构建模型:使用 Sequential、Functional 或 Subclassing API。稍后我们会看到,只有最后两种 API 允许访问层。
另见
有关 Keras 层 API 的一些参考文档,请参见以下文档:
-
Keras 层 API 文档:
keras.io/layers/about-keras-layers/ -
TensorFlow Keras 层 API 文档:
www.tensorflow.org/api_docs/python/tf/keras/layers
使用 Keras Sequential API
Keras 的主要目标是简化深度学习模型的创建。Sequential API 允许我们创建顺序模型,这是一种线性堆叠的层。通过逐层连接的模型可以解决许多问题。要创建一个顺序模型,我们需要实例化一个 Sequential 类,创建一些模型层并将它们添加进去。
我们将从创建顺序模型开始,到编译、训练和评估步骤,最后实现模型的预测。在这个配方结束时,您将拥有一个可以在生产中部署的 Keras 模型。
准备工作
本文将介绍创建顺序模型的主要方法,并使用 Keras Sequential API 组装层来构建模型。
首先,我们加载 TensorFlow 和 NumPy,如下所示:
import tensorflow as tf
from tensorflow import keras
from keras.layers import Dense
import numpy as np
我们准备好继续解释如何实现它。
如何实现...
-
首先,我们将创建一个顺序模型。Keras 提供了两种等效的方式来创建顺序模型。我们首先通过将层实例的列表作为数组传递给构造函数来开始。我们将通过输入以下代码构建一个多类分类器(10 个类别)完全连接的模型,也称为多层感知机。
model = tf.keras.Sequential([ # Add a fully connected layer with 1024 units to the model tf.keras.layers.Dense(1024, input_dim=64), # Add an activation layer with ReLU activation function tf.keras.layers.Activation('relu'), # Add a fully connected layer with 256 units to the model tf.keras.layers.Dense(256), # Add an activation layer with ReLU activation function tf.keras.layers.Activation('relu'), # Add a fully connected layer with 10 units to the model tf.keras.layers.Dense(10), # Add an activation layer with softmax activation function tf.keras.layers.Activation('softmax') ])创建顺序模型的另一种方式是实例化一个 Sequential 类,然后通过
.add()方法添加层。model = tf.keras.Sequential() # Add a fully connected layer with 1024 units to the model model.add(tf.keras.layers.Dense(1024, input_dim=64)) # Add an activation layer with ReLU activation function model.add(tf.keras.layers.Activation(relu)) # Add a fully connected layer with 256 units to the model model.add(tf.keras.layers.Dense(256)) # Add an activation layer with ReLU activation function model.add(tf.keras.layers.Activation('relu')) # Add a fully connected Layer with 10 units to the model model.add(tf.keras.layers.Dense(10)) # Add an activation layer with softmax activation function model.add(tf.keras.layers.Activation('softmax')) -
让我们仔细看看层的配置。
tf.keras.layersAPI 提供了许多内置层,并且还提供了创建我们自己的层的 API。在大多数情况下,我们可以将这些参数设置为层的构造函数:-
我们可以通过指定内置函数的名称或可调用对象来添加激活函数。该函数决定一个神经元是否应该被激活。默认情况下,层没有激活函数。以下是创建带有激活函数的层的两种方式。请注意,您不需要运行以下代码,这些层未分配给变量。
# Creation of a dense layer with a sigmoid activation function: Dense(256, activation='sigmoid') # Or: Dense(256, activation=tf.keras.activations.sigmoid) -
我们还可以通过传递内置初始化器的字符串标识符或可调用对象来指定初始权重(内核和偏置)的初始化策略。内核默认设置为“Glorot uniform”初始化器,偏置设置为零。
# A dense layer with a kernel initialized to a truncated normal distribution: Dense(256, kernel_initializer='random_normal') # A dense layer with a bias vector initialized with a constant value of 5.0: Dense(256, bias_initializer=tf.keras.initializers.Constant(value=5)) -
我们还可以为内核和偏置指定正则化器,如 L1(也称为 Lasso)或 L2(也称为 Ridge)正则化。默认情况下,不应用正则化。正则化器旨在通过惩罚具有大权重的模型来防止过拟合。这些惩罚被纳入网络优化的损失函数中。
# A dense layer with L1 regularization of factor 0.01 applied to the kernel matrix: Dense(256, kernel_regularizer=tf.keras.regularizers.l1(0.01)) # A dense layer with L2 regularization of factor 0.01 applied to the bias vector: Dense(256, bias_regularizer=tf.keras.regularizers.l2(0.01))
-
-
在 Keras 中,强烈建议为第一层设置输入形状。然而,与表面上看起来的不同,输入层不是一层,而是一个张量。它的形状必须与我们的训练数据相同。以下层将执行自动形状推断;它们的形状是根据前一层的单元计算出来的。
每种类型的层需要输入特定维度的张量,因此有不同的方法来指定输入形状,具体取决于层的种类。在这里,我们将重点介绍 Dense 层,因此我们将使用
input_dim参数。由于权重的形状取决于输入的大小,如果没有预先指定输入形状,模型将没有权重:模型没有被构建。在这种情况下,你无法调用Layer类的任何方法,例如summary、layers、weights等。在这个示例中,我们将创建包含 64 个特征的数据集,并处理每批 10 个样本。我们的输入数据的形状是(10,64),即(
batch_size,number_of_features)。默认情况下,Keras 模型定义为支持任何批次大小,因此批次大小不是强制性的。我们只需要通过input_dim参数为第一层指定特征数量。Dense(256, input_dim=(64))然而,为了效率的考虑,我们可以通过
batch_size参数强制设置批次大小。Dense(256, input_dim=(64), batch_size=10) -
在学习阶段之前,我们的模型需要进行配置。通过
compile方法来完成这一配置。我们需要指定:-
用于训练我们神经网络的优化算法。我们可以从
tf.keras.optimizers模块传递一个优化器实例。例如,我们可以使用tf.keras.optimizers.RMSprop的实例,或者使用字符串'RMSprop',它是一个实现了RMSprop算法的优化器。 -
一个损失函数,也叫做目标函数或优化得分函数,目的是最小化模型。它可以是现有损失函数的名称(例如
categorical_crossentropy或mse),一个符号化的 TensorFlow 损失函数(如tf.keras.losses.MAPE),或一个自定义的损失函数,它接收两个张量(真实张量和预测张量)并为每个数据点返回一个标量。 -
用于评估我们模型性能的度量列表,这些度量不会在模型训练过程中使用。我们可以传递字符串名称或来自
tf.keras.metrics模块的可调用函数。 -
如果你想确保模型以急切执行的方式进行训练和评估,可以将
run_eagerly参数设置为 true。
请注意,图形通过
compile方法最终确定。现在,我们将使用 Adam 优化器来编译模型,采用类别交叉熵损失并显示准确率度量。
model.compile( optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"] ) -
-
现在,我们将生成三个包含 64 个特征的玩具数据集,数据值为随机生成。其中一个用于训练模型(2,000 个样本),另一个用于验证(500 个样本),最后一个用于测试(500 个样本)。
data = np.random.random((2000, 64)) labels = np.random.random((2000, 10)) val_data = np.random.random((500, 64)) val_labels = np.random.random((500, 10)) test_data = np.random.random((500, 64)) test_labels = np.random.random((500, 10)) -
在模型配置完成后,通过调用
fit方法开始学习阶段。训练配置由以下三个参数完成:-
我们必须设置训练轮次,即遍历整个输入数据的迭代次数。
-
我们需要指定每个梯度的样本数,称为
batch_size参数。请注意,如果总样本数不能被批次大小整除,则最后一批次的样本可能较小。 -
我们可以通过设置
validation_data参数(一个包含输入和标签的元组)来指定验证数据集。这个数据集可以方便地监控模型的性能。在每个训练周期结束时,损失和度量会在推理模式下计算。
现在,我们将通过调用
fit方法在我们的玩具数据集上训练模型:model.fit(data, labels, epochs=10, batch_size=50, validation_data=(val_data, val_labels)) -
-
接下来,我们将在测试数据集上评估我们的模型。我们将调用
model.evaluate函数,它预测模型在测试模式下的损失值和度量值。计算是按批次进行的。它有三个重要参数:输入数据、目标数据和批次大小。此函数为给定输入预测输出,然后计算metrics函数(在model.compile中根据目标数据指定),并返回计算后的度量值作为输出。model.evaluate(data, labels, batch_size=50) -
我们也可以仅使用模型进行预测。
tf.keras.Model.predict方法仅接受数据作为输入并返回预测结果。以下是如何预测提供数据的最后一层推理输出,结果以 NumPy 数组形式呈现:result = model.predict(data, batch_size=50)分析这个模型的性能在这个示例中并不重要,因为我们使用的是随机生成的数据集。
现在,让我们继续分析这个示例。
它是如何工作的...
Keras 提供了 Sequential API 来创建由一系列线性堆叠的层组成的模型。我们可以将层实例的列表作为数组传递给构造函数,或者使用add方法。
Keras 提供了不同种类的层。它们大多数共享一些常见的构造参数,如activation、kernel_initializer和bias_initializer,以及kernel_regularizer和bias_regularizer。
注意延迟构建模式:如果没有在第一层指定输入形状,当第一次将模型应用于输入数据时,或者调用fit、eval、predict和summary等方法时,模型才会被构建。图形在调用compile方法时最终确定,该方法在学习阶段之前配置模型。然后,我们可以评估模型或进行预测。
另见
关于 Keras Sequential API 的一些参考资料,请访问以下网站:
-
tf.keras.Sequential 模型 API 文档:
www.tensorflow.org/api_docs/python/tf/keras/Sequential -
Keras Sequential 模型 API 文档:
keras.io/models/sequential/
使用 Keras 函数式 API
Keras Sequential API 在大多数情况下非常适合开发深度学习模型。然而,这个 API 有一些限制,例如线性拓扑结构,可以通过函数式 API 来克服。需要注意的是,许多高效的网络都基于非线性拓扑结构,如 Inception、ResNet 等。
函数式 API 允许定义具有非线性拓扑结构、多个输入、多个输出、残差连接的复杂模型,以及具有非顺序流动的共享和可重用层。
深度学习模型通常是一个有向无环图(DAG)。功能 API 是一种构建层图的方式,它比tf.keras.Sequential API 创建更灵活的模型。
准备工作
本实例将涵盖创建功能模型的主要方式,使用可调用模型、操作复杂的图拓扑、共享层,并最终引入使用 Keras 顺序 API 的“层节点”概念。
一如既往,我们只需要按如下方式导入 TensorFlow:
import tensorflow as tf
from tensorflow import keras
from keras.layers import Input, Dense, TimeDistributed
import keras.models
我们准备好继续解释如何实现这一点。
如何做到这一点...
让我们开始构建一个功能模型,用于识别 MNIST 手写数字数据集。我们将从灰度图像中预测手写数字。
创建功能模型
-
首先,我们将加载 MNIST 数据集。
mnist = tf.keras.datasets.mnist (X_mnist_train, y_mnist_train), (X_mnist_test, y_mnist_test) = mnist.load_data() -
然后,我们将创建一个 28x28 维度的输入节点。记住,在 Keras 中,输入层并不是一个层,而是一个张量,我们必须为第一个层指定输入形状。这个张量的形状必须与我们的训练数据形状一致。默认情况下,Keras 模型被定义为支持任何批次大小,因此批次大小不是必需的。
Input()用于实例化 Keras 张量。inputs = tf.keras.Input(shape=(28,28)) -
然后,我们将使用以下命令对大小为(28,28)的图像进行展平操作。这将生成一个包含 784 个像素的数组。
flatten_layer = keras.layers.Flatten() -
我们将通过在
inputs对象上调用flatten_layer来在层图中添加一个新的节点:flatten_output = flatten_layer(inputs)“层调用”操作就像是从
inputs到flatten_layer绘制一条箭头。我们正在“传递”输入到展平层,结果是它产生了输出。层实例是可调用的(在张量上)并返回一个张量。 -
然后,我们将创建一个新的层实例:
dense_layer = tf.keras.layers.Dense(50, activation='relu') -
我们将添加一个新节点:
dense_output = dense_layer(flatten_output) -
为了构建一个模型,多个层将被堆叠。在这个示例中,我们将添加另一个
dense层来进行 10 个类别之间的分类任务:predictions = tf.keras.layers.Dense(10, activation='softmax')(dense_output) -
输入张量和输出张量用于定义模型。模型是一个由一个或多个输入层和一个或多个输出层构成的函数。模型实例形式化了计算图,描述数据如何从输入流向输出。
model = keras.Model(inputs=inputs, outputs=predictions) -
现在,我们将打印模型的摘要。
model.summary() -
这将产生如下输出:
![]()
图 3.1:模型摘要
-
这样的模型可以通过与 Keras 顺序模型中相同的
compile, fit、evaluate和predict方法进行训练和评估。model.compile(optimizer='sgd', loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.fit(X_mnist_train, y_mnist_train, validation_data=(X_mnist_train, y_mnist_train), epochs=10)
在这个实例中,我们使用功能 API 构建了一个模型。
使用像层这样的可调用模型
让我们深入了解功能 API 与可调用模型的细节。
-
使用功能 API,重复使用训练好的模型变得非常容易:任何模型都可以通过在张量上调用它作为一层来处理。我们将重用前面定义的模型作为一层,以便查看其实现效果。它是一个用于 10 个类别的分类器。该模型返回 10 个概率值:每个类别一个概率值。这被称为 10 分类 softmax。因此,通过调用上述模型,模型将为每个输入预测 10 个类别中的一个。
x = Input(shape=(784,)) # y will contain the prediction for x y = model(x)请注意,通过调用一个模型,我们不仅仅是重用模型架构,我们还在重用它的权重。
-
如果我们面临一个序列问题,使用功能性 API 创建模型将变得非常简单。例如,假设我们不是处理一张图片,而是处理由多张图片组成的视频。我们可以通过使用
TimeDistributed层包装器,将图像分类模型转变为视频分类模型,仅需一行代码。这个包装器将我们的前一个模型应用于输入序列的每一个时间切片,换句话说,就是应用于视频的每一帧图像。from keras.layers import TimeDistributed # Input tensor for sequences of 50 timesteps, # Each containing a 28x28 dimensional matrix. input_sequences = tf.keras.Input(shape=(10, 28, 28)) # We will apply the previous model to each sequence so one for each timestep. # The MNIST model returns a vector with 10 probabilities (one for each digit). # The TimeDistributed output will be a sequence of 50 vectors of size 10. processed_sequences = tf.keras.layers.TimeDistributed(model)(input_sequences)
我们已经看到,模型像层一样是可以调用的。现在,我们将学习如何创建具有非线性拓扑的复杂模型。
创建一个具有多个输入和输出的模型
功能性 API 使得操作大量交织的数据流变得简单,具有多个输入输出和非线性连接拓扑。这些是顺序 API 无法处理的,顺序 API 无法创建具有非顺序连接或多个输入输出的模型。
让我们来看一个例子。我们将构建一个系统,用于预测特定房子的价格和销售前的经过时间。
该模型将有两个输入:
-
关于房子的资料,例如卧室数量、房屋大小、空调、内置厨房等。
-
房子的最新照片
这个模型将有两个输出:
-
销售前的经过时间(两个类别——慢或快)
-
预测价格
-
为了构建这个系统,我们将从构建第一个模块开始,用于处理关于房子的表格数据。
house_data_inputs = tf.keras.Input(shape=(128,), name='house_data') x = tf.keras.layers.Dense(64, activation='relu')(house_data_inputs) block_1_output = tf.keras.layers.Dense(32, activation='relu')(x) -
然后,我们将构建第二个模块来处理房子的图像数据。
house_picture_inputs = tf.keras.Input(shape=(128,128,3), name='house_picture') x = tf.keras.layers.Conv2D(64, 3, activation='relu', padding='same')(house_picture_inputs) x = tf.keras.layers.Conv2D(64, 3, activation='relu', padding='same')(x) block_2_output = tf.keras.layers.Flatten()(x) -
现在,我们将通过拼接将所有可用特征合并为一个大的向量。
x = tf.keras.layers.concatenate([block_1_output, block_2_output]) -
然后,我们将在特征上加一个用于价格预测的逻辑回归。
price_pred = tf.keras.layers.Dense(1, name='price', activation='relu')(x) -
接着,我们将在特征上加一个时间分类器。
time_elapsed_pred = tf.keras.layers.Dense(2, name='elapsed_time', activation='softmax')(x) -
现在,我们将构建模型。
model = keras.Model([house_data_inputs, house_picture_inputs], [price_pred, time_elapsed_pred], name='toy_house_pred') -
现在,我们将绘制模型图。
keras.utils.plot_model(model, 'multi_input_and_output_model.png', show_shapes=True) -
这将产生以下输出:
![]()
图 3.2:具有多个输入和输出的模型图
在这个示例中,我们使用功能性 API 创建了一个复杂模型,具有多个输入输出,用于预测特定房子的价格和销售前的经过时间。现在,我们将引入共享层的概念。
共享层
一些模型在其架构内多次重用相同的层。这些层实例学习与层图中多个路径对应的特征。共享层通常用于对来自相似空间的输入进行编码。
为了在不同的输入之间共享一个层(包括权重),我们只需实例化该层一次,并将其应用于我们需要的多个输入。
让我们考虑两种不同的文本序列。我们将对这两个具有相似词汇的序列应用相同的嵌入层。
# Variable-length sequence of integers
text_input_a = tf.keras.Input(shape=(None,), dtype='int32')
# Variable-length sequence of integers
text_input_b = tf.keras.Input(shape=(None,), dtype='int32')
# Embedding for 1000 unique words mapped to 128-dimensional vectors
shared_embedding = tf.keras.layers.Embedding(1000, 128)
# Reuse the same layer to encode both inputs
encoded_input_a = shared_embedding(text_input_a)
encoded_input_b = shared_embedding(text_input_b)
在这个例子中,我们已经学习了如何在同一个模型中多次重用一个层。现在,我们将介绍提取和重用层的概念。
在层的图中提取和重用节点
在本章的第一个例子中,我们看到一个层是一个实例,它以一个张量作为参数并返回另一个张量。一个模型是由多个层实例组成的。这些层实例是通过它们的输入和输出张量相互连接的对象。每次我们实例化一个层时,该层的输出是一个新的张量。通过向层添加一个“节点”,我们将输入和输出张量连接起来。
层的图是一个静态数据结构。通过 Keras 函数式 API,我们可以轻松访问和检查模型。
tf.keras.application 模块包含具有预训练权重的现成架构。
-
让我们去下载 ResNet 50 预训练模型。
resnet = tf.keras.applications.resnet.ResNet50() -
然后,我们将通过查询图数据结构来显示模型的中间层:
intermediate_layers = [layer.output for layer in resnet.layers] -
然后,我们将通过查询图数据结构来显示模型的前 10 个中间层:
intermediate_layers[:10] -
这将产生以下输出:
[<tf.Tensor 'input_7:0' shape=(None, 224, 224, 3) dtype=float32>, <tf.Tensor 'conv1_pad/Pad:0' shape=(None, 230, 230, 3) dtype=float32>, <tf.Tensor 'conv1_conv/BiasAdd:0' shape=(None, 112, 112, 64) dtype=float32>, <tf.Tensor 'conv1_bn/cond/Identity:0' shape=(None, 112, 112, 64) dtype=float32>, <tf.Tensor 'conv1_relu/Relu:0' shape=(None, 112, 112, 64) dtype=float32>, <tf.Tensor 'pool1_pad/Pad:0' shape=(None, 114, 114, 64) dtype=float32>, <tf.Tensor 'pool1_pool/MaxPool:0' shape=(None, 56, 56, 64) dtype=float32>, <tf.Tensor 'conv2_block1_1_conv/BiasAdd:0' shape=(None, 56, 56, 64) dtype=float32>, <tf.Tensor 'conv2_block1_1_bn/cond/Identity:0' shape=(None, 56, 56, 64) dtype=float32>, <tf.Tensor 'conv2_block1_1_relu/Relu:0' shape=(None, 56, 56, 64) dtype=float32>] -
现在,我们将选择所有特征层。我们将在卷积神经网络章节中详细讲解。
feature_layers = intermediate_layers[:-2] -
然后,我们将重用这些节点来创建我们的特征提取模型。
feat_extraction_model = keras.Model(inputs=resnet.input, outputs=feature_layers)
深度学习模型的一个有趣的好处是,它可以在类似的预测建模问题上部分或完全重复使用。这种技术称为“迁移学习”:通过减少训练时间,它显著提高了训练阶段的效果,并增强了模型在相关问题上的表现。
新的模型架构基于预训练模型中的一个或多个层。预训练模型的权重可以作为训练过程的起点。它们可以是固定的、微调的,或者在学习阶段完全适应。实现迁移学习的两种主要方法是权重初始化和特征提取。别担心,我们稍后会在本书中详细讲解。
在这个例子中,我们加载了基于 VGG19 架构的预训练模型。我们从这个模型中提取了节点,并在新模型中重用了它们。
它是如何工作的...
Keras 顺序 API 在绝大多数情况下是合适的,但仅限于创建逐层模型。函数式 API 更加灵活,允许提取和重用节点、共享层,并创建具有多个输入和多个输出的非线性模型。需要注意的是,许多高性能的网络基于非线性拓扑结构。
在这个例子中,我们已经学习了如何使用 Keras 函数式 API 构建模型。这些模型的训练和评估使用与 Keras 顺序模型相同的 compile、fit、evaluate 和 predict 方法。
我们还看了如何将训练好的模型作为层进行重用,如何共享层,以及如何提取并重用节点。最后一种方法在迁移学习技术中被使用,能够加速训练并提高性能。
还有更多……
由于我们可以访问每一层,使用 Keras 函数式 API 构建的模型具有一些特定功能,如模型绘图、整模型保存等。
使用函数式 API 构建的模型可能会很复杂,因此这里有一些提示,帮助你避免在过程中抓狂:
-
命名层:在展示模型图的摘要和图表时,这将非常有用。
-
分离子模型:将每个子模型视为一个乐高积木,最后我们将它们与其他子模型一起组合。
-
查看层摘要:使用
summary方法查看每一层的输出。 -
查看图形绘图:使用
plot方法显示并检查层之间的连接。 -
一致的变量命名:对输入和输出层使用相同的变量名,避免复制粘贴错误。
另见
关于 Keras 函数式 API 的一些参考资料,访问以下网站:
-
Keras 函数式 API 文档:
keras.io/getting-started/functional-api-guide/ -
tf.keras.ModelAPI:www.tensorflow.org/api_docs/python/tf/keras/Model -
机器学习大师:
machinelearningmastery.com/keras-functional-api-deep-learning/ -
在 TensorFlow 内部:tf.Keras 由 François Chollet 提供(第一部分)
www.youtube.com/watch?v=UYRBHFAvLSs -
在 TensorFlow 内部:tf.Keras(第二部分)
www.youtube.com/watch?v=uhzGTijaw8A
使用 Keras 子类化 API
Keras 基于面向对象设计原则。因此,我们可以继承 Model 类并创建我们的模型架构定义。
Keras 子类化 API 是 Keras 提出的第三种构建深度神经网络模型的方法。
这个 API 完全可定制,但这种灵活性也带来了复杂性!所以,请系好安全带,它比顺序 API 或函数式 API 更难使用。
但你可能会想,如果这个 API 使用起来这么难,我们为什么还需要它。某些模型架构和一些自定义层可能会非常具有挑战性。一些研究人员和开发人员希望完全控制他们的模型以及训练模型的方式。子类化 API 提供了这些功能。让我们深入了解细节。
准备工作
在这里,我们将介绍使用 Keras 子类化 API 创建自定义层和自定义模型的主要方法。
首先,我们加载 TensorFlow,如下所示:
import tensorflow as tf
from tensorflow import keras
我们已准备好继续解释如何操作。
如何操作……
我们从创建我们的层开始。
创建自定义层
正如 理解 Keras 层 部分所解释的那样,Keras 通过其层化 API 提供了各种内置的层,如全连接层、卷积层、循环层和归一化层。
所有层都是 Layer 类的子类,并实现了这些方法:
-
build方法,用于定义层的权重。 -
call方法,指定层执行的从输入到输出的转换。 -
compute_output_shape方法,如果该层修改了输入的形状。这样 Keras 就可以执行自动的形状推断。 -
get_config和from_config方法,如果该层被序列化和反序列化。
-
让我们将理论付诸实践。首先,我们将为自定义全连接层创建一个子类层:
class MyCustomDense(tf.keras.layers.Layer): # Initialize this class with the number of units def __init__(self, units): super(MyCustomDense, self).__init__() self.units = units # Define the weights and the bias def build(self, input_shape): self.w = self.add_weight(shape=(input_shape[-1], self.units), initializer='random_normal', trainable=True) self.b = self.add_weight(shape=(self.units,), initializer='random_normal', trainable=True) # Applying this layer transformation to the input tensor def call(self, inputs): return tf.matmul(inputs, self.w) + self.b # Function to retrieve the configuration def get_config(self): return {'units': self.units} -
然后,我们将使用前一步创建的
MyCustomDense层来创建模型:# Create an input layer inputs = keras.Input((12,4)) # Add an instance of MyCustomeDense layer outputs = MyCustomDense(2)(inputs) # Create a model model = keras.Model(inputs, outputs) # Get the model config config = model.get_config() -
接下来,我们将从配置文件重新加载模型:
new_model = keras.Model.from_config(config, custom_objects={'MyCustomDense': MyCustomDense})
在这个食谱中,我们已经创建了我们的 Layer 类。现在,我们将创建我们的模型。
创建自定义模型
通过子类化 tf.keras.Model 类,我们可以构建一个完全可定制的模型。
我们在 __init__ 方法中定义我们的层,并通过实现 call 方法完全控制模型的前向传播。training 布尔参数可以用来指定训练阶段和推理阶段期间的不同行为。
-
首先,我们将加载 MNIST 数据集并对灰度进行归一化:
mnist = tf.keras.datasets.mnist (X_mnist_train, y_mnist_train), (X_mnist_test, y_mnist_test) = mnist.load_data() train_mnist_features = X_mnist_train/255 test_mnist_features = X_mnist_test/255 -
让我们创建一个
Model的子类,用于识别 MNIST 数据:class MyMNISTModel(tf.keras.Model): def __init__(self, num_classes): super(MyMNISTModel, self).__init__(name='my_mnist_model') self.num_classes = num_classes self.flatten_1 = tf.keras.layers.Flatten() self.dropout = tf.keras.layers.Dropout(0.1) self.dense_1 = tf.keras.layers.Dense(50, activation='relu') self.dense_2 = tf.keras.layers.Dense(10, activation='softmax') def call(self, inputs, training=False): x = self.flatten_1(inputs) # Apply dropout only during the training phase x = self.dense_1(x) if training: x = self.dropout(x, training=training) return self.dense_2(x) -
现在,我们将实例化模型并处理训练:
my_mnist_model = MyMNISTModel(10) # Compile my_mnist_model.compile(optimizer='sgd', loss='sparse_categorical_crossentropy', metrics=['accuracy']) # Train my_mnist_model.fit(train_features, y_train, validation_data=(test_features, y_test), epochs=10)
它是如何工作的……
子类化 API 是深度学习从业者使用面向对象的 Keras 设计原则构建层或模型的一种方式。如果你的模型无法使用 Sequential 或 Functional API 实现,我们建议使用此 API。尽管这种方式的实现可能会很复杂,但在某些情况下仍然非常有用,了解如何在 Keras 中实现层和模型对于所有开发者和研究人员来说都是非常有趣的。
另见
关于 Keras 子类化 API 的一些参考,见以下教程、论文和文章:
-
使用 Keras 编写自定义层和模型:
www.tensorflow.org/guide/keras/custom_layers_and_models -
编写你自己的 Keras 层:
keras.io/layers/writing-your-own-keras-layers/
使用 Keras 预处理 API
Keras 预处理 API 汇集了数据处理和数据增强的模块。该 API 提供了处理序列、文本和图像数据的工具。数据预处理是机器学习和深度学习中的一个重要步骤,它将原始数据转换、转换或编码为适合学习算法理解、有效且有用的格式。
准备工作
本食谱将介绍 Keras 提供的一些预处理方法,用于处理序列、文本和图像数据。
如往常一样,我们只需导入 TensorFlow 如下:
import tensorflow as tf
from tensorflow import keras
import numpy as np
from tensorflow.keras.preprocessing.sequence import TimeseriesGenerator, pad_sequences, skipgrams, make_sampling_table
from tensorflow.keras.preprocessing.text import text_to_word_sequence, one_hot, hashing_trick, Tokenizer
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
我们准备好继续解释如何做了。
如何做…
让我们从序列数据开始。
序列预处理
序列数据是有序的数据,如文本或时间序列。因此,时间序列由按时间顺序排列的一系列数据点定义。
时间序列生成器
Keras 提供了处理序列数据(如时间序列数据)的实用工具。它接收连续的数据点,并使用时间序列参数(如步幅、历史长度等)进行转换,返回一个 TensorFlow 数据集实例。
-
让我们使用一个玩具时间序列数据集,包含 10 个整数值:
series = np.array([i for i in range(10)]) print(series) -
这导致以下输出:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) -
我们希望根据最后五个滞后观测值预测下一个值。因此,我们将定义一个生成器,并将
length参数设置为 5。此参数指定输出序列的长度(以时间步为单位):generator = TimeseriesGenerator(data = series, targets = series, length=5, batch_size=1, shuffle=False, reverse=False) -
我们希望生成由 5 个滞后观测组成的样本,用于预测,而玩具时间序列数据集包含 10 个值。因此,生成的样本数为 5:
# number of samples print('Samples: %d' % len(generator)) -
接下来,我们将显示每个样本的输入和输出,并检查数据是否准备就绪:
for i in range(len(generator)): x, y = generator[i] print('%s => %s' % (x, y)) -
这导致以下输出:
[[0 1 2 3 4]] => [5] [[1 2 3 4 5]] => [6] [[2 3 4 5 6]] => [7] [[3 4 5 6 7]] => [8] [[4 5 6 7 8]] => [9] -
现在,我们将创建并编译一个模型:
model = Sequential() model.add(Dense(10, activation='relu', input_dim=5)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse') -
然后,我们将通过将生成器作为输入数据来训练模型:
model.fit(generator, epochs=10)
使用深度学习方法准备时间序列数据可能非常具有挑战性。但幸运的是,Keras 提供了一个生成器,可以帮助我们将单变量或多变量时间序列数据集转换为准备用于训练模型的数据结构。该生成器提供了许多选项来准备数据,如洗牌、采样率、起始和结束偏移量等。建议查阅官方 Keras API 获取更多详细信息。
现在,我们将专注于如何准备可变长度输入序列的数据。
填充序列
处理序列数据时,每个样本的长度通常不同。为了使所有序列适合所需的长度,解决方案是对它们进行填充。比定义的序列长度短的序列在每个序列的末尾(默认)或开头填充值。否则,如果序列大于所需长度,则截断序列。
-
让我们从四个句子开始:
sentences = [["What", "do", "you", "like", "?"], ["I", "like", "basket-ball", "!"], ["And", "you", "?"], ["I", "like", "coconut", "and", "apple"]] -
首先,我们将构建词汇查找表。我们将创建两个字典,一个用于从单词到整数标识符的转换,另一个反之。
text_set = set(np.concatenate(sentences)) vocab_to_int = dict(zip(text_set, range(len(text_set)))) int_to_vocab = {vocab_to_int[word]:word for word in vocab_to_int.keys()} -
然后,在构建词汇查找表之后,我们将句子编码为整数数组。
encoded_sentences = [] for sentence in sentences: encoded_sentence = [vocab_to_int[word] for word in sentence] encoded_sentences.append(encoded_sentence) encoded_sentences -
这导致以下输出:
[[8, 4, 7, 6, 0], [5, 6, 2, 3], [10, 7, 0], [5, 6, 1, 9, 11]] -
现在,我们将使用
pad_sequences函数轻松地截断和填充序列到一个公共长度。默认情况下,启用了序列前填充。pad_sequences(encoded_sentences) -
这导致以下输出:
array([[ 8, 4, 7, 6, 0], [ 0, 5, 6, 2, 3], [ 0, 0, 10, 7, 0], [ 5, 6, 1, 9, 11]], dtype=int32) -
然后,我们将激活序列后填充,并将
maxlen参数设置为所需的长度 – 这里是 7。pad_sequences(encoded_sentences, maxlen = 7) -
这导致以下输出:
array([[ 0, 0, 8, 4, 7, 6, 0], [ 0, 0, 0, 5, 6, 2, 3], [ 0, 0, 0, 0, 10, 7, 0], [ 0, 0, 5, 6, 1, 9, 11]], dtype=int32) -
序列的长度也可以裁剪为所需的长度——此处为 3。默认情况下,该函数会从每个序列的开头移除时间步长。
pad_sequences(encoded_sentences, maxlen = 3) -
这将产生以下输出:
array([[ 7, 6, 0], [ 6, 2, 3], [10, 7, 0], [ 1, 9, 11]], dtype=int32) -
将截断参数设置为
post,以便从每个序列的末尾移除时间步长。pad_sequences(encoded_sentences, maxlen = 3, truncating='post') -
这将产生以下输出:
array([[ 8, 4, 7], [ 5, 6, 2], [10, 7, 0], [ 5, 6, 1]], dtype=int32)
当我们希望列表中的所有序列具有相同长度时,填充非常有用。
在接下来的章节中,我们将介绍一种非常流行的文本预处理技术。
Skip-grams
Skip-grams 是自然语言处理中的一种无监督学习技术。它为给定的单词找到最相关的单词,并预测该单词的上下文单词。
Keras 提供了skipgrams预处理函数,它接收一个整数编码的单词序列,并返回定义窗口中每对单词的相关性。如果一对单词相关,样本为正样本,相关标签设置为 1。否则,样本被认为是负样本,标签设置为 0。
一例胜千言。让我们以这个句子为例:“I like coconut and apple,”选择第一个单词作为我们的“上下文单词”,并使用窗口大小为二。我们将上下文单词“I”与指定窗口中的单词配对。所以,我们有两个单词对(I, like)和(I, coconut),这两者的值都为 1。
让我们将理论付诸实践:
-
首先,我们将把一个句子编码为单词索引的列表:
sentence = "I like coconut and apple" encoded_sentence = [vocab_to_int[word] for word in sentence.split()] vocabulary_size = len(encoded_sentence) -
然后,我们将调用
skipgrams函数,窗口大小为 1:pairs, labels = skipgrams(encoded_sentence, vocabulary_size, window_size=1, negative_samples=0) -
现在,我们将打印结果:
for i in range(len(pairs)): print("({:s} , {:s} ) -> {:d}".format( int_to_vocab[pairs[i][0]], int_to_vocab[pairs[i][1]], labels[i])) -
这将产生以下输出:
(coconut , and ) -> 1 (apple , ! ) -> 0 (and , coconut ) -> 1 (apple , and ) -> 1 (coconut , do ) -> 0 (like , I ) -> 1 (and , apple ) -> 1 (like , coconut ) -> 1 (coconut , do ) -> 0 (I , like ) -> 1 (coconut , like ) -> 1 (and , do ) -> 0 (like , coconut ) -> 0 (I , ! ) -> 0 (like , ! ) -> 0 (and , coconut ) -> 0
请注意,非单词在词汇表中由索引 0 表示,并将被跳过。我们建议读者参考 Keras API,了解更多关于填充的细节。
现在,让我们介绍一些文本数据预处理的技巧。
文本预处理
在深度学习中,我们不能直接将原始文本输入到网络中。我们必须将文本编码为数字,并提供整数作为输入。我们的模型将生成整数作为输出。这个模块提供了文本输入预处理的工具。
将文本拆分为单词序列
Keras 提供了text_to_word_sequence方法,将序列转换为单词或标记的列表。
-
让我们使用这个句子:
sentence = "I like coconut , I like apple" -
然后,我们将调用将句子转换为单词列表的方法。默认情况下,该方法会根据空格拆分文本。
text_to_word_sequence(sentence, lower=False) -
这将产生以下输出:
['I', 'like', 'coconut', 'I', 'like', 'apple'] -
现在,我们将
lower参数设置为True,文本将被转换为小写:text_to_word_sequence(sentence, lower=True, filters=[]) -
这将产生以下输出:
['i', 'like', 'coconut', ',', 'i', 'like', 'apple']
请注意,默认情况下,filter参数会过滤掉一系列字符,如标点符号。在我们上一次的代码执行中,我们移除了所有预定义的过滤器。
让我们继续使用一种方法来编码单词或类别特征。
Tokenizer
Tokenizer类是文本分词的实用工具类。它是深度学习中准备文本的首选方法。
这个类的输入参数为:
-
要保留的最大词数。只有最常见的词会根据词频被保留。
-
用于过滤的字符列表。
-
一个布尔值,用于决定是否将文本转换为小写字母。
-
用于分词的分隔符。
-
让我们从这句话开始:
sentences = [["What", "do", "you", "like", "?"], ["I", "like", "basket-ball", "!"], ["And", "you", "?"], ["I", "like", "coconut", "and", "apple"]] -
现在,我们将创建一个
Tokenizer实例,并对前述句子进行拟合:# create the tokenizer t = Tokenizer() # fit the tokenizer on the documents t.fit_on_texts(sentences) -
分词器创建了文档的几部分信息。我们可以获得一个字典,包含每个词的计数。
print(t.word_counts) -
这会产生以下输出:
OrderedDict([('what', 1), ('do', 1), ('you', 2), ('like', 3), ('?', 2), ('i', 2), ('basket-ball', 1), ('!', 1), ('and', 2), ('coconut', 1), ('apple', 1)]) -
我们还可以获得一个字典,包含每个词出现在多少个文档中:
print(t.document_count) -
这会产生以下输出:
4 -
一个字典包含每个词的唯一整数标识符:
print(t.word_index) -
这会产生以下输出:
{'like': 1, 'you': 2, '?': 3, 'i': 4, 'and': 5, 'what': 6, 'do': 7, 'basket-ball': 8, '!': 9, 'coconut': 10, 'apple': 11} -
用于拟合
Tokenizer的独特文档数量。print(t.word_docs) -
这会产生以下输出:
defaultdict(<class 'int'>, {'do': 1, 'like': 3, 'what': 1, 'you': 2, '?': 2, '!': 1, 'basket-ball': 1, 'i': 2, 'and': 2, 'coconut': 1, 'apple': 1}) -
现在,我们已经准备好编码文档,感谢
texts_to_matrix函数。这个函数提供了四种不同的文档编码方案,用来计算每个标记的系数。让我们从二进制模式开始,它返回文档中每个标记是否存在。
t.texts_to_matrix(sentences, mode='binary') -
这会产生以下输出:
[[0\. 1\. 1\. 1\. 0\. 0\. 1\. 1\. 0\. 0\. 0\. 0.] [0\. 1\. 0\. 0\. 1\. 0\. 0\. 0\. 1\. 1\. 0\. 0.] [0\. 0\. 1\. 1\. 0\. 1\. 0\. 0\. 0\. 0\. 0\. 0.] [0\. 1\. 0\. 0\. 1\. 1\. 0\. 0\. 0\. 0\. 1\. 1.]] -
TokenizerAPI 提供了另一种基于词频的模式——它返回文档中每个词的计数:t.texts_to_matrix(sentences, mode='count') -
这会产生以下输出:
[[0\. 1\. 1\. 1\. 0\. 0\. 1\. 1\. 0\. 0\. 0\. 0.] [0\. 1\. 0\. 0\. 1\. 0\. 0\. 0\. 1\. 1\. 0\. 0.] [0\. 0\. 1\. 1\. 0\. 1\. 0\. 0\. 0\. 0\. 0\. 0.] [0\. 1\. 0\. 0\. 1\. 1\. 0\. 0\. 0\. 0\. 1\. 1.]]
请注意,我们也可以使用tfidf模式或频率模式。前者返回每个词的词频-逆文档频率分数,后者返回文档中每个词的频率,且与文档中的总词数相关。
Tokenizer API 可以拟合训练数据集并对训练、验证和测试数据集中的文本数据进行编码。
在本节中,我们介绍了一些在训练和预测之前准备文本数据的技术。
现在,让我们继续准备并增强图像。
图像预处理
数据预处理模块提供了一套实时数据增强工具,用于图像数据。
在深度学习中,神经网络的性能通常通过训练数据集中的示例数量来提高。
Keras 预处理 API 中的ImageDataGenerator类允许从训练数据集中创建新数据。它不应用于验证或测试数据集,因为其目的是通过合理的新图像扩展训练数据集中的示例数量。这种技术称为数据增强。请注意不要将数据准备与数据归一化或图像调整大小混淆,后者适用于所有与模型交互的数据。数据增强包括图像处理领域的许多变换,例如旋转、水平和垂直偏移、水平和垂直翻转、亮度调整等。
策略可能会根据任务的不同而有所不同。例如,在 MNIST 数据集中,它包含了手写数字的图像,应用水平翻转没有意义。除了数字 8 之外,这种变换是不合适的。
而在婴儿照片的情况下,应用这种变换是有意义的,因为图像可能是从左边或右边拍摄的。
-
让我们将理论付诸实践,对
CIFAR10数据集进行数据增强。我们将首先下载CIFAR数据集。# Load CIFAR10 Dataset (x_cifar10_train, y_cifar10_train), (x_cifar10_test, y_cifar10_test) = tf.keras.datasets.cifar10.load_data() -
现在,我们将创建一个图像数据生成器,应用水平翻转、0 到 15 度之间的随机旋转,以及在宽度和高度方向上平移 3 个像素。
datagen = tf.keras.preprocessing.image.ImageDataGenerator( rotation_range=15, width_shift_range=3, height_shift_range=3, horizontal_flip=True) -
创建训练数据集的迭代器。
it= datagen.flow(x_cifar10_train, y_cifar10_train, batch_size = 32) -
创建一个模型并编译它。
model = tf.keras.models.Sequential([ tf.keras.layers.Conv2D(filters=32, kernel_size=3, padding="same", activation="relu", input_shape=[32, 32, 3]), tf.keras.layers.Conv2D(filters=32, kernel_size=3, padding="same", activation="relu"), tf.keras.layers.MaxPool2D(pool_size=2), tf.keras.layers.Conv2D(filters=64, kernel_size=3, padding="same", activation="relu"), tf.keras.layers.Conv2D(filters=64, kernel_size=3, padding="same", activation="relu"), tf.keras.layers.MaxPool2D(pool_size=2), tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation="relu"), tf.keras.layers.Dense(10, activation="softmax") ]) model.compile(loss="sparse_categorical_crossentropy", optimizer=tf.keras.optimizers.SGD(lr=0.01), metrics=["accuracy"]) -
通过调用
fit方法来处理训练。请注意设置step_per_epoch参数,该参数指定一个 epoch 包含的样本批次数。history = model.fit(it, epochs=10, steps_per_epoch=len(x_cifar10_train) / 32, validation_data=(x_cifar10_test, y_cifar10_test))
使用图像数据生成器,我们通过创建新图像扩展了原始数据集的大小。通过更多的图像,深度学习模型的训练效果可以得到提升。
它是如何工作的...
Keras 预处理 API 允许转换、编码和增强神经网络的数据。它使得处理序列、文本和图像数据变得更加容易。
首先,我们介绍了 Keras Sequence 预处理 API。我们使用时间序列生成器将单变量或多变量时间序列数据集转换为适合训练模型的数据结构。然后,我们重点介绍了可变长度输入序列的数据准备,即填充(padding)。最后,我们以 skip-gram 技术结束这一部分,skip-gram 可以为给定单词找到最相关的词,并预测该单词的上下文词。
然后,我们介绍了 Keras 文本预处理 API,它提供了一个完整的解决方案来处理自然语言。我们学习了如何将文本拆分为单词,并使用二进制、词频计数、tfidf 或频率模式对单词进行标记化。
最后,我们重点介绍了使用ImageDataGenerator的图像预处理 API,这是增加训练数据集大小并处理图像的一个实际优势。
另见
关于 Keras 预处理 API 的一些参考资料,请访问以下网站:
-
序列预处理 Keras API:
keras.io/preprocessing/sequence/ -
文本处理 Keras API:
keras.io/preprocessing/text/ -
关于句法和语义词汇相似性的更多信息:Tomas Mikolov、Kai Chen、Greg Corrado 和 Jeffrey Dean。(2013)。高效估算词向量表示
arxiv.org/pdf/1301.3781v3.pdf -
图像预处理 Keras API:
keras.io/preprocessing/image/
第四章:线性回归
线性回归可能是统计学、机器学习和科学领域中最重要的算法之一。它是最广泛使用的算法之一,理解如何实现它及其不同变种非常重要。线性回归相较于许多其他算法的一个优势是它非常易于解释。我们为每个特征得到一个数字(系数),这个数字直接表示该特征如何影响目标(即所谓的因变量)。
例如,如果你需要预测一栋房子的售价,并且你获得了一组包含房屋特征的历史销售数据(例如地块大小、房屋质量和状况的指标,以及离市中心的距离),你可以轻松地应用线性回归。你可以通过几步获得一个可靠的估算器,所得模型也容易理解并向他人解释。事实上,线性回归首先估算一个基准值,即截距,然后为每个特征估算一个乘法系数。每个系数可以将每个特征转化为预测的正向或负向部分。通过将基准值和所有系数转化后的特征相加,你可以得到最终预测。因此,在我们的房屋售价预测问题中,你可能会得到一个正系数表示地块大小,这意味着较大的地块会卖得更贵,而离市中心的负系数则表示位于郊区的房产市场价值较低。
使用 TensorFlow 计算此类模型快速、适合大数据,并且更容易投入生产,因为它可以通过检查权重向量进行普遍的解释。
在本章中,我们将向你介绍如何通过 Estimators 或 Keras 在 TensorFlow 中实现线性回归的配方,并进一步提供更为实用的解决方案。事实上,我们将解释如何使用不同的损失函数进行调整,如何正则化系数以实现特征选择,以及如何将回归应用于分类、非线性问题和具有高基数的类别变量(高基数指的是具有许多独特值的变量)。
请记住,所有的代码都可以在 GitHub 上找到,链接:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook。
在本章中,我们将介绍涉及线性回归的配方。我们从使用矩阵求解线性回归的数学公式开始,然后转向使用 TensorFlow 范式实现标准线性回归及其变体。我们将涵盖以下主题:
-
学习 TensorFlow 回归方法
-
将 Keras 模型转化为 Estimator
-
理解线性回归中的损失函数
-
实现 Lasso 和 Ridge 回归
-
实现逻辑回归
-
求助于非线性解决方案
-
使用 Wide & Deep 模型
本章结束时,你会发现,使用 TensorFlow 创建线性模型(以及一些非线性模型)非常简单,利用提供的配方可以轻松完成。
学习 TensorFlow 中的线性回归方法
线性回归中的统计方法,使用矩阵和数据分解方法,非常强大。无论如何,TensorFlow 有另一种方式来解决回归问题中的斜率和截距系数。TensorFlow 可以通过迭代方式解决这些问题,即逐步学习最佳的线性回归参数,以最小化损失,就像我们在前面的章节中所看到的配方一样。
有趣的是,实际上在处理回归问题时,你不需要从零开始编写所有代码:Estimators 和 Keras 可以帮助你完成这项工作。Estimators 位于 tf.estimator 中,这是 TensorFlow 的一个高级 API。
Estimators 在 TensorFlow 1.3 中首次引入(见 github.com/tensorflow/tensorflow/releases/tag/v1.3.0-rc2)作为 "现成 Estimators",这些是预先制作的特定程序(例如回归模型或基本神经网络),旨在简化训练、评估、预测以及模型导出以供服务使用。使用现成的程序有助于以更简单、更直观的方式进行开发,而低级 API 则用于定制或研究解决方案(例如,当你想要测试你在论文中找到的解决方案时,或当你的问题需要完全定制的方法时)。此外,Estimators 可以轻松部署到 CPU、GPU 或 TPU 上,也可以在本地主机或分布式多服务器环境中运行,而无需对模型进行任何额外的代码更改,这使得它们适用于生产环境的现成使用场景。这也是 Estimators 不会在短期内从 TensorFlow 中消失的原因,即使 Keras,正如上一章所介绍的,是 TensorFlow 2.x 的主要高级 API。相反,更多的支持和开发将致力于 Keras 和 Estimators 之间的集成,你将很快在我们的配方中看到,如何轻松地将 Keras 模型转化为你自己的自定义 Estimators。
开发 Estimator 模型涉及四个步骤:
-
使用
tf.data函数获取数据 -
实例化特征列
-
实例化并训练 Estimator
-
评估模型性能
在我们的配方中,我们将探索这四个步骤,并为每个步骤提供可重用的解决方案。
准备工作
在这个配方中,我们将循环遍历数据点的批次,并让 TensorFlow 更新斜率和 y 截距。我们将使用波士顿房价数据集,而不是生成的数据。
波士顿住房数据集来源于 Harrison, D. 和 Rubinfeld, D.L. 的论文 Hedonic Housing Prices and the Demand for Clean Air(《环境经济学与管理学杂志》,第 5 卷,第 81-102 页,1978 年),该数据集可以在许多分析包中找到(例如 scikit-learn 中),并且存在于 UCI 机器学习库以及原始的 StatLib 存档(http://lib.stat.cmu.edu/datasets/boston)。这是一个经典的回归问题数据集,但并非简单数据集。例如,样本是有顺序的,如果你没有随机打乱样本,进行训练/测试集划分时可能会产生无效且有偏的模型。
详细来说,数据集由 1970 年波士顿人口普查中的 506 个普查区组成,包含 21 个变量,涉及可能影响房地产价值的各个方面。目标变量是房屋的中位数货币价值,单位为千美元。在这些可用的特征中,有一些非常明显的特征,如房间数量、建筑物年龄和邻里犯罪水平,还有一些不那么明显的特征,如污染浓度、附近学校的可用性、高速公路的接入情况和距离就业中心的远近。
回到我们的解决方案,具体来说,我们将找到一个最优特征集,帮助我们估算波士顿的房价。在下一节讨论不同损失函数对这一问题的影响之前,我们还将展示如何从 Keras 函数开始创建一个回归 Estimator,在 TensorFlow 中进行自定义,这为解决不同问题提供了重要的自定义选项。
如何操作...
我们按以下步骤继续进行:
我们首先加载必要的库,然后使用 pandas 函数将数据加载到内存中。接下来,我们将预测变量与目标变量(MEDV,中位房价)分开,并将数据划分为训练集和测试集:
import tensorflow as tf
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
housing_url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data'
path = tf.keras.utils.get_file(housing_url.split("/")[-1], housing_url)
columns = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV']
data = pd.read_table(path, delim_whitespace=True,
header=None, names=columns)
np.random.seed(1)
train = data.sample(frac=0.8).copy()
y_train = train['MEDV']
train.drop('MEDV', axis=1, inplace=True)
test = data.loc[~data.index.isin(train.index)].copy()
y_test = test['MEDV']
test.drop('MEDV', axis=1, inplace=True)
接下来,我们为我们的方案声明两个关键函数:
-
make_input_fn是一个函数,用于从转换为 Python 字典的 pandas DataFrame(特征作为键,值为特征向量的 pandas Series)创建一个tf.data数据集。它还提供批量大小定义和随机打乱功能。 -
define_feature_columns是一个函数,用于将每一列的名称映射到特定的tf.feature_column转换。tf.feature_column是一个 TensorFlow 模块 (www.tensorflow.org/api_docs/python/tf/feature_column),提供能够以适当方式处理各种数据的函数,以便将其输入到神经网络中。
make_input_fn 函数用于实例化两个数据函数,一个用于训练(数据被打乱,批量大小为 256,设置为消耗 1400 个周期),一个用于测试(设置为单个周期,无打乱,因此顺序保持原样)。
define_feature_columns函数用于通过numeric_column函数(www.tensorflow.org/api_docs/python/tf/feature_column/numeric_column)映射数值变量,通过categorical_column_with_vocabulary_list(www.tensorflow.org/api_docs/python/tf/feature_column/categorical_column_with_vocabulary_list)映射类别变量。两者都会告诉我们的估算器如何以最佳方式处理这些数据:
learning_rate = 0.05
def make_input_fn(data_df, label_df, num_epochs=10,
shuffle=True, batch_size=256):
def input_function():
ds = tf.data.Dataset.from_tensor_slices((dict(data_df), label_df))
if shuffle:
ds = ds.shuffle(1000)
ds = ds.batch(batch_size).repeat(num_epochs)
return ds
return input_function
def define_feature_columns(data_df, categorical_cols, numeric_cols):
feature_columns = []
for feature_name in numeric_cols:
feature_columns.append(tf.feature_column.numeric_column(
feature_name, dtype=tf.float32))
for feature_name in categorical_cols:
vocabulary = data_df[feature_name].unique()
feature_columns.append(
tf.feature_column.categorical_column_with_vocabulary_list(
feature_name, vocabulary))
return feature_columns
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns = define_feature_columns(data, categorical_cols, numeric_cols)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
接下来的步骤是实例化线性回归模型的估算器。我们将回顾线性模型的公式,y = aX + b,这意味着存在一个截距值的系数,然后对于每个特征或特征转换(例如,类别数据是独热编码的,因此每个变量值都有一个单独的系数)也会有一个系数:
linear_est = tf.estimator.LinearRegressor(feature_columns=feature_columns)
现在,我们只需要训练模型并评估其性能。使用的指标是均方根误差(越小越好):
linear_est.train(train_input_fn)
result = linear_est.evaluate(test_input_fn)
print(result)
以下是报告的结果:
INFO:tensorflow:Loss for final step: 25.013594.
...
INFO:tensorflow:Finished evaluation at 2020-05-11-15:48:16
INFO:tensorflow:Saving dict for global step 2800: average_loss = 32.715736, global_step = 2800, label/mean = 22.048513, loss = 32.715736, prediction/mean = 21.27578
在这里是个很好的地方,可以注意如何判断模型是否出现了过拟合或欠拟合。如果我们的数据被分为测试集和训练集,并且在训练集上的表现优于测试集,则说明我们正在过拟合数据。如果准确率在测试集和训练集上都在增加,则说明模型欠拟合,我们应该继续训练。
在我们的案例中,训练结束时的平均损失为 25.0。我们的测试集平均损失为 32.7,这意味着我们可能已经过拟合,应该减少训练的迭代次数。
我们可以可视化估算器在训练数据时的表现,以及它与测试集结果的比较。这需要使用 TensorBoard(www.tensorflow.org/tensorboard/),TensorFlow 的可视化工具包,稍后在本书中将详细讲解。
无论如何,你只需使用4\. Linear Regression with TensorBoard.ipynb笔记本,而不是4\. Linear Regression.ipynb版本,就可以重现这些可视化内容。两者都可以在本书的 GitHub 仓库中找到,链接为github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook。

图 4.1:回归估算器训练损失的 TensorBoard 可视化
可视化结果显示,估计器快速拟合了问题,并在 1,000 个观测批次后达到了最佳值。随后,它围绕已达到的最小损失值波动。由蓝点表示的测试性能接近最佳值,从而证明即使是在未见过的示例上,模型也表现稳定。
它是如何工作的……
调用适当 TensorFlow 功能的估计器,从数据函数中筛选数据,并根据匹配的特征名称和tf.feature_column函数将数据转换为适当的形式,完成了整个工作。剩下的就是检查拟合情况。实际上,估计器找到的最佳拟合线并不保证就是最优拟合线。是否收敛到最优拟合线取决于迭代次数、批量大小、学习率和损失函数。始终建议在训练过程中观察损失函数,因为这有助于排查问题或调整超参数。
还有更多……
如果你想提高线性模型的性能,交互特征可能是关键。这意味着你在两个变量之间创建组合,而这个组合比单独的特征更能解释目标。在我们的波士顿住房数据集中,结合房屋的平均房间数和某个地区低收入人群的比例,可以揭示更多关于邻里类型的信息,并帮助推断该地区的住房价值。我们通过将它们传递给tf.feature_column.crossed_column函数来组合这两个特征。估计器在接收这些输出作为特征的一部分时,会自动创建这个交互特征:
def create_interactions(interactions_list, buckets=5):
interactions = list()
for (a, b) in interactions_list:
interactions.append(tf.feature_column.crossed_column([a, b], hash_bucket_size=buckets))
return interactions
derived_feature_columns = create_interactions([['RM', 'LSTAT']])
linear_est = tf.estimator.LinearRegressor(feature_columns=feature_columns+derived_feature_columns)
linear_est.train(train_input_fn)
result = linear_est.evaluate(test_input_fn)
print(result)
这里是训练损失和相应测试集结果的图表。

图 4.2:带有交互特征的回归模型的 TensorBoard 图
观察现在的拟合速度比之前更快、更稳定,这表明我们为模型提供了更多有用的特征(交互特征)。
另一个有用的配方函数适用于处理预测:估计器将其作为字典返回。一个简单的函数将把所有内容转换为更有用的预测数组:
def dicts_to_preds(pred_dicts):
return np.array([pred['predictions'] for pred in pred_dicts])
preds = dicts_to_preds(linear_est.predict(test_input_fn))
print(preds)
将预测结果作为数组有助于你以比字典更方便的方式重用和导出结果。
将 Keras 模型转化为估计器
到目前为止,我们已经使用了tf.estimator模块中特定的 Estimators 来解决我们的线性回归模型。这具有明显的优势,因为我们的模型大部分是自动运行的,并且我们可以轻松地在云端(如 Google 提供的 Google Cloud Platform)和不同类型的服务器(基于 CPU、GPU 和 TPU)上进行可伸缩的部署。然而,通过使用 Estimators,我们可能会缺乏模型架构的灵活性,而这正是 Keras 模块化方法所要求的,我们在前一章中已经讨论过。在这个配方中,我们将通过展示如何将 Keras 模型转换为 Estimators 来解决这个问题,从而同时利用 Estimators API 和 Keras 的多样性。
准备工作
我们将使用与前一配方中相同的波士顿住房数据集,同时还将利用make_input_fn函数。与之前一样,我们需要导入我们的核心包:
import tensorflow as tf
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
我们还需要从 TensorFlow 导入 Keras 模块。
import tensorflow.keras as keras
将tf.keras导入为keras还允许您轻松地重用之前使用独立 Keras 包编写的任何脚本。
如何操作...
我们的第一步将是重新定义创建特征列的函数。事实上,现在我们必须为我们的 Keras 模型指定一个输入,这是在原生 Estimators 中不需要的,因为它们只需要一个tf.feature函数来映射特征:
def define_feature_columns_layers(data_df, categorical_cols, numeric_cols):
feature_columns = []
feature_layer_inputs = {}
for feature_name in numeric_cols:
feature_columns.append(tf.feature_column.numeric_column(feature_name, dtype=tf.float32))
feature_layer_inputs[feature_name] = tf.keras.Input(shape=(1,), name=feature_name)
for feature_name in categorical_cols:
vocabulary = data_df[feature_name].unique()
cat = tf.feature_column.categorical_column_with_vocabulary_list(feature_name, vocabulary)
cat_one_hot = tf.feature_column.indicator_column(cat)
feature_columns.append(cat_one_hot)
feature_layer_inputs[feature_name] = tf.keras.Input(shape=(1,), name=feature_name, dtype=tf.int32)
return feature_columns, feature_layer_inputs
互动也是一样的。在这里,我们还需要定义将由我们的 Keras 模型使用的输入(在本例中是独热编码):
def create_interactions(interactions_list, buckets=5):
feature_columns = []
for (a, b) in interactions_list:
crossed_feature = tf.feature_column.crossed_column([a, b], hash_bucket_size=buckets)
crossed_feature_one_hot = tf.feature_column.indicator_column(crossed_feature)
feature_columns.append(crossed_feature_one_hot)
return feature_columns
准备好必要的输入后,我们可以开始模型本身。这些输入将被收集在一个特征层中,该层将数据传递给一个batchNormalization层,该层将自动标准化数据。然后数据将被导向输出节点,该节点将生成数值输出。
def create_linreg(feature_columns, feature_layer_inputs, optimizer):
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1, kernel_initializer='normal', activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss='mean_squared_error')
return model
在此时,已经设置了所有必要的输入,新函数被创建,我们可以运行它们:
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_linreg(feature_columns, feature_layer_inputs, optimizer)
现在我们已经获得了一个工作的 Keras 模型。我们可以使用model_to_estimator函数将其转换为 Estimator。这需要为 Estimator 的输出建立一个临时目录:
import tempfile
def canned_keras(model):
model_dir = tempfile.mkdtemp()
keras_estimator = tf.keras.estimator.model_to_estimator(
keras_model=model, model_dir=model_dir)
return keras_estimator
estimator = canned_keras(model)
将 Keras 模型转换为 Estimator 后,我们可以像以前一样训练模型并评估结果。
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
当我们使用 TensorBoard 绘制拟合过程时,我们将观察到训练轨迹与之前 Estimators 获得的轨迹非常相似:

图 4.3:使用 Keras 线性 Estimator 进行训练
Canned Keras Estimators 确实是将 Keras 用户定义解决方案的灵活性与 Estimators 高性能训练和部署结合在一起的快速而健壮的方法。
工作原理...
model_to_estimator函数并不是 Keras 模型的包装器。相反,它解析你的模型并将其转换为静态 TensorFlow 图,从而实现分布式训练和模型扩展。
还有更多...
使用线性模型的一个重要优势是能够探索其权重,并了解哪个特征对我们获得的结果产生了影响。每个系数会告诉我们,鉴于输入在批处理层被标准化,特征相对于其他特征的影响(系数在绝对值上是可以比较的),以及它是增加还是减少结果(根据正负符号):
weights = estimator.get_variable_value('layer_with_weights-1/kernel/.ATTRIBUTES/VARIABLE_VALUE')
print(weights)
无论如何,如果我们从模型中提取权重,我们会发现无法轻松解释它们,因为它们没有标签且维度不同,因为tf.feature函数应用了不同的转换。
我们需要一个函数来提取从特征列中正确的标签,因为在将它们输入到预设估算器之前,我们已经将它们映射过:
def extract_labels(feature_columns):
labels = list()
for col in feature_columns:
col_config = col.get_config()
if 'key' in col_config:
labels.append(col_config['key'])
elif 'categorical_column' in col_config:
if col_config['categorical_column']['class_name']=='VocabularyListCategoricalColumn':
key = col_config['categorical_column']['config']['key']
for item in col_config['categorical_column']['config']['vocabulary_list']:
labels.append(key+'_val='+str(item))
elif col_config['categorical_column']['class_name']=='CrossedColumn':
keys = col_config['categorical_column']['config']['keys']
for bucket in range(col_config['categorical_column']['config']['hash_bucket_size']):
labels.append('x'.join(keys)+'_bkt_'+str(bucket))
return labels
该函数仅适用于 TensorFlow 2.2 或更高版本,因为在早期的 TensorFlow 2.x 版本中,get_config方法在tf.feature对象中并不存在。
现在,我们可以提取所有标签,并有意义地将每个输出中的权重与其相应的特征匹配:
labels = extract_labels(feature_columns)
for label, weight in zip(labels, weights):
print(f"{label:15s} : {weight[0]:+.2f}")
一旦你得到了权重,就可以通过观察每个系数的符号和大小,轻松地得出每个特征对结果的贡献。然而,特征的尺度可能会影响大小,除非你事先通过减去均值并除以标准差对特征进行了统计标准化。
理解线性回归中的损失函数
了解损失函数对算法收敛性的影响非常重要。在这里,我们将说明 L1 和 L2 损失函数如何影响线性回归的收敛性和预测。这是我们对预设 Keras 估算器进行的第一次自定义。本章的更多配方将在此基础上通过添加更多功能来增强该初始估算器。
准备工作
我们将使用与前面配方中相同的波士顿房价数据集,并使用以下函数:
* define_feature_columns_layers
* make_input_fn
* create_interactions
然而,我们将更改损失函数和学习率,看看收敛性如何变化。
如何实现...
我们按如下方式继续配方:
程序的开始与上一个配方相同。因此,我们加载必要的包,并且如果波士顿房价数据集尚不可用,我们将下载它:
import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
之后,我们需要重新定义create_linreg,通过添加一个新的参数来控制损失类型。默认值仍然是均方误差(L2 损失),但现在在实例化预设估算器时可以轻松更改:
def create_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error']):
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1, kernel_initializer='normal',
activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()],
outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
这样做之后,我们可以通过使用不同学习率的Ftrl优化器显式地训练我们的模型,更适合 L1 损失(我们将损失设置为平均绝对误差):
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_absolute_error',
metrics=['mean_absolute_error', 'mean_squared_error'])
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这是我们通过切换到 L1 损失函数得到的结果:
{'loss': 3.1208777, 'mean_absolute_error': 3.1208777, 'mean_squared_error': 27.170328, 'global_step': 2800}
我们现在可以使用 TensorBoard 来可视化训练过程中的性能:

图 4.4:均方误差优化
结果图显示了均方误差的良好下降,直到 400 次迭代后减慢,并在 1,400 次迭代后趋于稳定,形成一个平台。
如何运作……
在选择损失函数时,我们还必须选择一个相应的学习率,以确保其与我们的问题匹配。在这里,我们测试了两种情况,第一种是采用 L2,第二种是首选 L1。
如果我们的学习率较小,收敛过程将需要更多时间。然而,如果学习率过大,我们的算法可能会无法收敛。
还有更多……
为了理解发生了什么,我们应当观察大学习率和小学习率对L1 范数和L2 范数的作用。如果学习率过大,L1 可能会停滞在次优结果,而 L2 可能会得到更差的性能。为了可视化这一点,我们将查看关于两种范数的学习步长的一维表示,如下所示:

图 4.5:L1 和 L2 范数在较大和较小学习率下的表现
如前所示,小学习率确实能保证更好的优化结果。较大的学习率与 L2 不太适用,但可能对 L1 证明是次优的,因为它会在一段时间后停止进一步优化,而不会造成更大的损害。
实现 Lasso 和 Ridge 回归
有几种方法可以限制系数对回归输出的影响。这些方法被称为正则化方法,其中最常见的两种正则化方法是 Lasso 和 Ridge 回归。在本食谱中,我们将讲解如何实现这两种方法。
准备工作
Lasso 和 Ridge 回归与普通线性回归非常相似,不同之处在于我们在公式中加入了正则化项,以限制斜率(或偏斜率)。这背后可能有多个原因,但常见的一个原因是我们希望限制对因变量有影响的特征数量。
如何操作……
我们按照以下步骤继续进行:
我们将再次使用波士顿房价数据集,并按照之前的食谱设置函数。特别是,我们需要define_feature_columns_layers、make_input_fn 和 create_interactions。我们再次首先加载库,然后定义一个新的 create_ridge_linreg,在其中我们使用keras.regularizers.l2作为我们密集层的regularizer来设置一个新的 Keras 模型:
import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
def create_ridge_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error'],
l2=0.01):
regularizer = keras.regularizers.l2(l2)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
完成这些后,我们可以再次运行之前的线性模型,并使用 L1 损失来观察结果的改进:
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_ridge_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error', 'mean_squared_error'],
l2=0.01)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这是 Ridge 回归的结果:
{'loss': 25.903751, 'mean_absolute_error': 3.27314, 'mean_squared_error': 25.676477, 'global_step': 2800}
此外,这里是使用 TensorBoard 进行训练的图表:

图 4.6:Ridge 回归训练损失
我们也可以通过创建一个新函数来复制 L1 正则化:
create_lasso_linreg.
def create_lasso_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error', metrics=['mean_absolute_error'],
l1=0.001):
regularizer = keras.regularizers.l1(l1)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_lasso_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error', 'mean_squared_error'],
l1=0.001)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这是从 L1 Lasso 回归得到的结果:
{'loss': 24.616476, 'mean_absolute_error': 3.1985352, 'mean_squared_error': 24.59167, 'global_step': 2800}
此外,这里是训练损失的图表:

图 4.7:Lasso 回归训练损失
比较 Ridge 和 Lasso 方法时,我们注意到它们在训练损失方面没有太大差异,但测试结果偏向 Lasso。这可能是由于一个噪声变量必须被排除,才能让模型得到改进,因为 Lasso 会定期从预测估计中排除无用的变量(通过赋予它们零系数),而 Ridge 只是对它们进行下调权重。
它是如何工作的...
我们通过向线性回归的损失函数添加一个连续的 Heaviside 阶跃函数来实现 Lasso 回归。由于阶跃函数的陡峭性,我们必须小心步长。如果步长太大,模型将无法收敛。关于 Ridge 回归,请参见下一节所需的更改。
还有更多...
弹性网回归是一种回归方法,通过将 Lasso 回归与 Ridge 回归结合,在损失函数中添加 L1 和 L2 正则化项。
实现弹性网回归很简单,跟随前两个方法,因为你只需更改正则化器。
我们只需创建一个 create_elasticnet_linreg 函数,它将 L1 和 L2 强度的值作为参数传入:
def create_elasticnet_linreg(feature_columns, feature_layer_inputs,
optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error'],
l1=0.001, l2=0.01):
regularizer = keras.regularizers.l1_l2(l1=l1, l2=l2)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='linear')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()],
outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
最后,我们重新运行从数据开始的完整训练步骤,并评估模型的性能:
categorical_cols = ['CHAS', 'RAD']
numeric_cols = ['CRIM', 'ZN', 'INDUS', 'NOX', 'RM', 'AGE', 'DIS', 'TAX', 'PTRATIO', 'B', 'LSTAT']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
interactions_columns = create_interactions([['RM', 'LSTAT']])
feature_columns += interactions_columns
optimizer = keras.optimizers.Ftrl(learning_rate=0.02)
model = create_elasticnet_linreg(feature_columns, feature_layer_inputs, optimizer,
loss='mean_squared_error',
metrics=['mean_absolute_error',
'mean_squared_error'],
l1=0.001, l2=0.01)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=1400)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这是得到的结果:
{'loss': 24.910872, 'mean_absolute_error': 3.208289, 'mean_squared_error': 24.659771, 'global_step': 2800}
这是 ElasticNet 模型的训练损失图:

图 4.8:ElasticNet 训练损失
获得的测试结果与 Ridge 和 Lasso 相差不大,位于它们之间。如前所述,问题在于从数据集中去除变量以提高性能,正如我们现在所看到的,Lasso 模型是最适合执行这一任务的。
实现逻辑回归
对于本教程,我们将实现逻辑回归,利用乳腺癌威斯康星数据集(archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic))来预测乳腺癌的概率。我们将从基于乳腺肿块的细针穿刺(FNA)图像计算得到的特征中预测诊断结果。FNA 是一个常见的乳腺癌检测方法,通过小量组织活检进行,活检样本可以在显微镜下进行检查。
该数据集可以直接用于分类模型,无需进一步转换,因为目标变量由 357 个良性病例和 212 个恶性病例组成。这两个类别的样本数并不完全相同(在进行二分类回归模型时,这是一个重要要求),但它们的差异并不极端,使我们能够构建一个简单的例子并使用普通准确率来评估模型。
请记得检查你的类别是否平衡(换句话说,是否具有大致相同数量的样本),否则你需要采取特定的方法来平衡样本,例如应用权重,或者你的模型可能会提供不准确的预测(如果你需要更多细节,可以参考以下 Stack Overflow 问题:datascience.stackexchange.com/questions/13490/how-to-set-class-weights-for-imbalanced-classes-in-keras)。
准备工作
逻辑回归是一种将线性回归转化为二分类的方法。这是通过将线性输出转换为一个 sigmoid 函数来实现的,该函数将输出值缩放到 0 和 1 之间。目标值是 0 或 1,表示一个数据点是否属于某个类别。由于我们预测的是一个 0 到 1 之间的数值,当预测值超过指定的阈值时,预测结果被分类为类别 1,否则为类别 0。对于本例,我们将阈值设置为 0.5,这样分类就变得和四舍五入输出一样简单。
在分类时,无论如何,有时候你需要控制自己犯的错误,这对于医疗应用(比如我们提出的这个例子)尤其重要,但对于其他应用(例如保险或银行领域的欺诈检测),这也是一个值得关注的问题。事实上,在分类时,你会得到正确的预测结果,但也会有假阳性和假阴性。假阳性是模型在预测为阳性(类别 1)时,真实标签却为阴性所产生的错误。假阴性则是模型将实际为阳性的样本预测为阴性。
在使用 0.5 阈值来决定类别(正类或负类)时,实际上你是在平衡假阳性和假阴性的期望值。实际上,根据你的问题,假阳性和假阴性错误可能会有不同的后果。例如,在癌症检测的情况下,显然你绝对不希望发生假阴性错误,因为这意味着将一个实际患病的病人误判为健康,可能导致生命危险。
通过设置更高或更低的分类阈值,你可以在假阳性和假阴性之间进行权衡。较高的阈值将导致更多的假阴性,而假阳性较少。较低的阈值将导致较少的假阴性,但假阳性更多。对于我们的示例,我们将使用 0.5 的阈值,但请注意,阈值也是你需要考虑的因素,尤其是在模型的实际应用中。
如何实现...
我们按照以下步骤继续进行示例:
我们首先加载库并从互联网恢复数据:
import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
breast_cancer = 'https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data'
path = tf.keras.utils.get_file(breast_cancer.split("/")[-1], breast_cancer)
columns = ['sample_code', 'clump_thickness', 'cell_size_uniformity',
'cell_shape_uniformity',
'marginal_adhesion', 'single_epithelial_cell_size',
'bare_nuclei', 'bland_chromatin',
'normal_nucleoli', 'mitoses', 'class']
data = pd.read_csv(path, header=None, names=columns, na_values=[np.nan, '?'])
data = data.fillna(data.median())
np.random.seed(1)
train = data.sample(frac=0.8).copy()
y_train = (train['class']==4).astype(int)
train.drop(['sample_code', 'class'], axis=1, inplace=True)
test = data.loc[~data.index.isin(train.index)].copy()
y_test = (test['class']==4).astype(int)
test.drop(['sample_code', 'class'], axis=1, inplace=True)
接下来,我们指定逻辑回归函数。与我们之前的线性回归模型相比,主要的修改是将单个输出神经元的激活函数从linear改为sigmoid,这就足够让我们得到一个逻辑回归模型,因为我们的输出将是一个概率值,范围从 0.0 到 1.0:
def create_logreg(feature_columns, feature_layer_inputs, optimizer,
loss='binary_crossentropy', metrics=['accuracy'],
l2=0.01):
regularizer = keras.regularizers.l2(l2)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='sigmoid')(norm)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
最后,我们运行我们的程序:
categorical_cols = []
numeric_cols = ['clump_thickness', 'cell_size_uniformity', 'cell_shape_uniformity',
'marginal_adhesion', 'single_epithelial_cell_size', 'bare_ nuclei',
'bland_chromatin',
'normal_nucleoli', 'mitoses']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
optimizer = keras.optimizers.Ftrl(learning_rate=0.007)
model = create_logreg(feature_columns, feature_layer_inputs, optimizer, l2=0.01)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=300, batch_size=32)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
下面是我们逻辑回归的准确率报告:
{'accuracy': 0.95, 'loss': 0.16382739, 'global_step': 5400}
此外,你可以在这里找到损失图:

图 4.9:逻辑回归模型的 TensorBoard 训练损失图
通过几个命令,我们在准确率和损失方面取得了不错的结果,尽管目标类别略有不平衡(良性病例比恶性病例更多)。
它是如何工作的...
逻辑回归的预测基于 sigmoid 曲线,若要相应地修改我们之前的线性模型,我们只需要切换到 sigmoid 激活函数。
还有更多...
当你在进行多类或多标签预测时,你不需要通过不同类型的一对多(OVA)策略来扩展二分类模型,而只需要扩展输出节点的数量,以匹配你需要预测的类别数。使用带有 sigmoid 激活函数的多个神经元,你将得到一个多标签方法,而使用 softmax 激活函数,你将获得一个多类预测。你将在本书的后续章节中找到更多的示例,说明如何使用简单的 Keras 函数来实现这一点。
寻求非线性解决方案
线性模型具有较强的可接近性和可解释性,因为特征列与回归系数之间存在一对一的关系。然而,有时候你可能希望尝试非线性解法,以检查更复杂的模型是否能更好地拟合你的数据,并以更专业的方式解决你的预测问题。支持向量机(SVMs)是一种与神经网络竞争了很长时间的算法,且由于最近在大规模核机器的随机特征方面的进展,它们仍然是一个可行的选择(Rahimi, Ali; Recht, Benjamin. Random features for large-scale kernel machines. In: Advances in neural information processing systems. 2008. 第 1177-1184 页)。在本示例中,我们展示了如何利用 Keras 获得非线性解法来解决分类问题。
准备工作
我们仍然会使用之前示例中的函数,包括define_feature_columns_layers和make_input_fn。和逻辑回归示例一样,我们将继续使用乳腺癌数据集。和以前一样,我们需要加载以下包:
import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import pandas as pd
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
到目前为止,我们已经准备好继续执行这个步骤。
如何做…
除了之前的包外,我们还专门导入了RandomFourierFeatures函数,它可以对输入进行非线性变换。根据损失函数,RandomFourierFeatures层可以逼近基于核的分类器和回归器。之后,我们只需要应用我们通常的单输出节点并获取预测结果。
根据你使用的 TensorFlow 2.x 版本,你可能需要从不同的模块导入它:
try:
from tensorflow.python.keras.layers.kernelized import RandomFourierFeatures
except:
# from TF 2.2
from tensorflow.keras.layers.experimental import RandomFourierFeatures
现在我们开发create_svc函数。它包含了一个 L2 正则化项用于最终的全连接节点,一个用于输入的批归一化层,以及一个插入其中的RandomFourierFeatures层。在这个中间层中,产生了非线性特征,你可以通过设置output_dim参数来确定层生成的非线性交互的数量。当然,你可以通过增加 L2 正则化值来对比在设置较高output_dim值后出现的过拟合,从而实现更多的正则化:
def create_svc(feature_columns, feature_layer_inputs, optimizer,
loss='hinge', metrics=['accuracy'],
l2=0.01, output_dim=64, scale=None):
regularizer = keras.regularizers.l2(l2)
feature_layer = keras.layers.DenseFeatures(feature_columns)
feature_layer_outputs = feature_layer(feature_layer_inputs)
norm = keras.layers.BatchNormalization()(feature_layer_outputs)
rff = RandomFourierFeatures(output_dim=output_dim, scale=scale, kernel_initializer='gaussian')(norm)
outputs = keras.layers.Dense(1,
kernel_initializer='normal',
kernel_regularizer = regularizer,
activation='sigmoid')(rff)
model = keras.Model(inputs=[v for v in feature_layer_inputs.values()], outputs=outputs)
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
return model
和之前的示例一样,我们定义了不同的列,设置了模型和优化器,准备了输入函数,最后我们训练并评估结果:
categorical_cols = []
numeric_cols = ['clump_thickness', 'cell_size_uniformity', 'cell_shape_uniformity',
'marginal_adhesion', 'single_epithelial_cell_size', 'bare_nuclei', 'bland_chromatin',
'normal_nucleoli', 'mitoses']
feature_columns, feature_layer_inputs = define_feature_columns_layers(data, categorical_cols, numeric_cols)
optimizer = keras.optimizers.Adam(learning_rate=0.00005)
model = create_svc(feature_columns, feature_layer_inputs, optimizer,
loss='hinge', l2=0.001, output_dim=512)
estimator = canned_keras(model)
train_input_fn = make_input_fn(train, y_train, num_epochs=500, batch_size=512)
test_input_fn = make_input_fn(test, y_test, num_epochs=1, shuffle=False)
estimator.train(train_input_fn)
result = estimator.evaluate(test_input_fn)
print(result)
这里是报告的准确度。为了获得更好的结果,你需要尝试不同的RandomFourierFeatures层的输出维度和正则化项的组合:
{'accuracy': 0.95 'loss': 0.7390725, 'global_step': 1000}
这是来自 TensorBoard 的损失图:

图 4.10:基于 RandomFourierFeatures 的模型的损失图
由于我们使用了比平常更大的批次,图形的效果确实相当好。由于任务的复杂性(需要训练大量的神经元),通常更大的批次会比较小的批次效果更好。
它是如何工作的…
随机傅里叶特征是一种近似支持向量机(SVM)核函数的方法,从而实现更低的计算复杂度,并使得这种方法在神经网络实现中也变得可行。如果你需要更深入的解释,可以阅读本文开头引用的原始论文,或者你可以参考这个非常清晰的 Stack Exchange 回答:stats.stackexchange.com/questions/327646/how-does-a-random-kitchen-sink-work#327961。
还有更多……
根据损失函数的不同,你可以得到不同的非线性模型:
-
铰链损失将你的模型设定为支持向量机(SVM)。
-
逻辑损失将你的模型转化为核逻辑回归模型(分类性能几乎与支持向量机(SVM)相同,但核逻辑回归可以提供类别概率)。
-
均方误差将你的模型转化为一个核回归模型。
由你决定首先尝试哪种损失函数,并决定如何设置来自随机傅里叶变换的输出维度。一般建议是,你可以从较多的输出节点开始,并逐步测试减少节点数量是否能改善结果。
使用宽深模型
线性模型相较于复杂模型有一个巨大优势:它们高效且易于解释,即使在你使用多个特征且特征之间存在相互作用时也如此。谷歌研究人员提到这一点作为记忆化的力量,因为你的线性模型将特征与目标之间的关联记录为单一系数。另一方面,神经网络具备泛化的力量,因为在其复杂性中(它们使用多个权重层并且相互关联每个输入),它们能够近似描述支配过程结果的一般规则。
宽深模型,如谷歌研究人员所构想的那样(arxiv.org/abs/1606.07792),能够融合记忆化和泛化,因为它们将线性模型(应用于数值特征)与泛化(应用于稀疏特征,例如编码为稀疏矩阵的类别特征)结合在一起。因此,名称中的宽指的是回归部分,深指的是神经网络部分:

图 4.11:宽模型(线性模型)如何与神经网络在宽深模型中融合(摘自 Cheng, Heng-Tze 等人的论文《Wide & deep learning for recommender systems》,《第 1 届深度学习推荐系统研讨会论文集》,2016 年)。
这样的结合可以在处理推荐系统问题时取得最佳结果(例如 Google Play 中展示的推荐系统)。Wide & Deep 模型在推荐问题中表现最好,因为每个部分处理的是正确类型的数据。宽度部分处理与用户特征相关的特征(密集的数值特征、二进制指示符或它们在交互特征中的组合),这些特征相对稳定;而深度部分处理表示先前软件下载的特征字符串(稀疏输入在非常大的矩阵中),这些特征随着时间变化而更为不稳定,因此需要一种更复杂的表示方式。
准备工作
实际上,Wide & Deep 模型同样适用于许多其他数据问题,推荐系统是它们的专长,且此类模型在 Estimators 中已经很容易获得(见www.tensorflow.org/api_docs/python/tf/estimator/DNNLinearCombinedEstimator)。在这个示例中,我们将使用一个混合数据集,成人数据集(archive.ics.uci.edu/ml/datasets/Adult)。该数据集也被广泛称为人口普查数据集,其目的是预测基于人口普查数据,您的年收入是否超过 5 万美元。可用的特征种类非常多样,从与年龄相关的连续值,到具有大量类别的变量,包括职业。然后,我们将使用每种不同类型的特征,输入到 Wide & Deep 模型的正确部分。
如何操作...
我们首先从 UCI 存档中下载成人数据集:
census_dir = 'https://archive.ics.uci.edu/ml/machine-learning-databases/adult/'
train_path = tf.keras.utils.get_file('adult.data', census_dir + 'adult.data')
test_path = tf.keras.utils.get_file('adult.test', census_dir + 'adult.test')
columns = ['age', 'workclass', 'fnlwgt', 'education', 'education_num',
'marital_status', 'occupation', 'relationship', 'race',
'gender', 'capital_gain', 'capital_loss', 'hours_per_week',
'native_country', 'income_bracket']
train_data = pd.read_csv(train_path, header=None, names=columns)
test_data = pd.read_csv(test_path, header=None, names=columns, skiprows=1)
然后,我们根据需要选择特征子集,并提取目标变量,将其从字符串类型转换为整数类型:
predictors = ['age', 'workclass', 'education', 'education_num',
'marital_status', 'occupation', 'relationship', 'gender']
y_train = (train_data.income_bracket==' >50K').astype(int)
y_test = (test_data.income_bracket==' >50K.').astype(int)
train_data = train_data[predictors]
test_data = test_data[predictors]
该数据集需要额外处理,因为某些字段存在缺失值。我们通过用均值替换缺失值来处理这些数据。作为一般规则,我们必须在将数据输入 TensorFlow 模型之前,填补所有缺失的数据:
train_data[['age', 'education_num']] = train_data[['age', 'education_num']].fillna(train_data[['age', 'education_num']].mean())
test_data[['age', 'education_num']] = test_data[['age', 'education_num']].fillna(train_data[['age', 'education_num']].mean())
现在,我们可以通过正确的 tf.feature_column 函数来定义列:
-
数值列:处理数值型数据(如年龄)
-
分类列:处理分类值,当唯一类别数量较少时(如性别)
-
嵌入:处理当唯一类别数量较多时的分类值,将分类值映射到一个密集的低维数值空间
我们还定义了一个函数,用于简化分类列和数值列之间的交互:
def define_feature_columns(data_df, numeric_cols, categorical_cols, categorical_embeds, dimension=30):
numeric_columns = []
categorical_columns = []
embeddings = []
for feature_name in numeric_cols:
numeric_columns.append(tf.feature_column.numeric_column(feature_name, dtype=tf.float32))
for feature_name in categorical_cols:
vocabulary = data_df[feature_name].unique()
categorical_columns.append(tf.feature_column.categorical_column_with_vocabulary_list(feature_name, vocabulary))
for feature_name in categorical_embeds:
vocabulary = data_df[feature_name].unique()
to_categorical = tf.feature_column.categorical_column_with_vocabulary_list(feature_name,
vocabulary)
embeddings.append(tf.feature_column.embedding_column(to_categorical,
dimension=dimension))
return numeric_columns, categorical_columns, embeddings
def create_interactions(interactions_list, buckets=10):
feature_columns = []
for (a, b) in interactions_list:
crossed_feature = tf.feature_column.crossed_column([a, b], hash_bucket_size=buckets)
crossed_feature_one_hot = tf.feature_column.indicator_column( crossed_feature)
feature_columns.append(crossed_feature_one_hot)
return feature_columns
现在所有函数已经定义完毕,我们将不同的列进行映射,并添加一些有意义的交互(例如将教育与职业进行交叉)。我们通过设置维度参数,将高维分类特征映射到一个固定的 32 维低维数值空间:
numeric_columns, categorical_columns, embeddings = define_feature_columns(train_data,
numeric_cols=['age', 'education_num'],
categorical_cols=['gender'],
categorical_embeds=['workclass', 'education',
'marital_status', 'occupation',
'relationship'],
dimension=32)
interactions = create_interactions([['education', 'occupation']], buckets=10)
映射完特征后,我们将它们输入到我们的估计器中(参见www.tensorflow.org/api_docs/python/tf/estimator/DNNLinearCombinedClassifier),并指定由宽部分处理的特征列和由深部分处理的特征列。对于每个部分,我们还指定优化器(通常线性部分使用 Ftrl,深部分使用 Adam),并且对于深部分,我们指定隐藏层的架构,作为一个神经元数量的列表:
estimator = tf.estimator.DNNLinearCombinedClassifier(
# wide settings
linear_feature_columns=numeric_columns+categorical_columns+interactions, linear_optimizer=keras.optimizers.Ftrl(learning_rate=0.0002),
# deep settings
dnn_feature_columns=embeddings,
dnn_hidden_units=[1024, 256, 128, 64],
dnn_optimizer=keras.optimizers.Adam(learning_rate=0.0001))
然后,我们继续定义输入函数(与本章其他食谱中所做的没有不同):
def make_input_fn(data_df, label_df, num_epochs=10, shuffle=True, batch_size=256):
def input_function():
ds = tf.data.Dataset.from_tensor_slices((dict(data_df), label_df))
if shuffle:
ds = ds.shuffle(1000)
ds = ds.batch(batch_size).repeat(num_epochs)
return ds
return input_function
最后,我们训练 Estimator 1,500 步并在测试数据上评估结果:
train_input_fn = make_input_fn(train_data, y_train,
num_epochs=100, batch_size=256)
test_input_fn = make_input_fn(test_data, y_test,
num_epochs=1, shuffle=False)
estimator.train(input_fn=train_input_fn, steps=1500)
results = estimator.evaluate(input_fn=test_input_fn)
print(results)
我们在测试集上获得了约 0.83 的准确率,这是通过使用 Estimator 的 evaluate 方法报告的:
{'accuracy': 0.83391684, 'accuracy_baseline': 0.76377374, 'auc': 0.88012385, 'auc_precision_recall': 0.68032277, 'average_loss': 0.35969484, 'label/mean': 0.23622628, 'loss': 0.35985297, 'precision': 0.70583993, 'prediction/mean': 0.21803579, 'recall': 0.5091004, 'global_step': 1000}
这是训练损失和测试估计(蓝点)的图示:

图 4.12:Wide & Deep 模型的训练损失和测试估计
对于完整的预测概率,我们只需从 Estimator 使用的字典数据类型中提取它们。predict_proba函数将返回一个 NumPy 数组,包含正类(收入超过 50K 美元)和负类的概率,分别位于不同的列中:
def predict_proba(predictor):
preds = list()
for pred in predictor:
preds.append(pred['probabilities'])
return np.array(preds)
predictions = predict_proba(estimator.predict(input_fn=test_input_fn))
它是如何工作的...
Wide & Deep 模型代表了一种将线性模型与更复杂的神经网络方法结合使用的方式。与其他 Estimator 一样,这个 Estimator 也非常简单易用。该方法在其他应用中的成功关键,绝对在于定义输入数据函数并将特征与tf.features_columns中更合适的函数进行映射。
第五章:梯度提升树
本章介绍了梯度提升树:TensorFlow(TF)的方法。它是一类机器学习算法,通过一组弱预测模型(通常是决策树)生成预测模型。该模型以阶段性方式构建,并通过使用任意(可微)损失函数来进行泛化。梯度提升树是一类非常流行的算法,因为它们可以并行化(在树构建阶段),本地处理缺失值和异常值,并且需要最少的数据预处理。
引言
在本章中,我们简要展示了如何使用BoostedTreesClassifier处理二分类问题。我们将应用这种技术解决一个实际的商业问题,使用一个流行的教育数据集:预测哪些顾客可能会取消他们的预订。这个问题的数据——以及其他几个商业问题——以表格格式存在,通常包含多种不同的特征类型:数值型、类别型、日期等。在缺乏复杂领域知识的情况下,梯度提升方法是创建一个可解释的、即开即用的解决方案的不错选择。在接下来的部分,将通过代码演示相关的建模步骤:数据准备、结构化为函数、通过tf.estimator功能拟合模型,以及结果的解释。
如何实现...
我们首先加载必要的包:
import tensorflow as tf
import numpy as np
import pandas as pd
from IPython.display import clear_output
from matplotlib import pyplot as plt
import matplotlib.pyplot as plt
import seaborn as sns
sns_colors = sns.color_palette('colorblind')
from numpy.random import uniform, seed
from scipy.interpolate import griddata
from matplotlib.font_manager import FontProperties
from sklearn.metrics import roc_curve
原则上,类别变量可以简单地重新编码为整数(使用如LabelEncoder之类的函数),而梯度提升模型也能很好地工作——数据预处理的这些最小要求是树集成方法流行的原因之一。然而,在本案例中,我们希望集中展示模型的可解释性,因此我们要分析各个指标的值。基于这个原因,我们创建了一个在 TF 友好的格式中执行独热编码的函数:
def one_hot_cat_column(feature_name, vocab):
return tf.feature_column.indicator_column(
tf.feature_column.categorical_column_with_vocabulary_list(feature_name, vocab))
如引言中所述,在这个案例中,我们将使用以下网址提供的酒店取消数据集:
www.sciencedirect.com/science/article/pii/S2352340918315191
我们选择这个数据集是因为它对于读者可能遇到的典型商业预测问题来说相当现实:数据中包含时间维度,并且有数值型和类别型特征。同时,它也相当干净(没有缺失值),这意味着我们可以专注于实际的建模,而不是数据处理:
xtrain = pd.read_csv('../input/hotel-booking- demand/hotel_bookings.csv')
xtrain.head(3)
数据集有时间维度,因此可以基于reservation_status_date进行自然的训练/验证集划分:
xvalid = xtrain.loc[xtrain['reservation_status_date'] >= '2017-08-01']
xtrain = xtrain.loc[xtrain['reservation_status_date'] < '2017-08-01']
将特征与目标分开:
ytrain, yvalid = xtrain['is_canceled'], xvalid['is_canceled']
xtrain.drop('is_canceled', axis = 1, inplace = True)
xvalid.drop('is_canceled', axis = 1, inplace = True)
我们将列分为数值型和类别型,并以 TF 期望的格式进行编码。我们跳过一些可能改善模型性能的列,但由于它们的特性,它们引入了泄露的风险:在训练中可能改善模型性能,但在对未见数据进行预测时会失败。在我们的案例中,其中一个变量是 arrival_date_year:如果模型过于依赖这个变量,当我们提供一个更远未来的数据集时(该变量的特定值显然会缺失),模型将会失败。
我们从训练数据中去除一些额外的变量——这个步骤可以在建模过程之前基于专家判断进行,或者可以通过自动化方法进行。后一种方法将包括运行一个小型模型并检查全局特征的重要性:如果结果显示某一个非常重要的特征主导了其他特征,它可能是信息泄露的潜在来源:
xtrain.drop(['arrival_date_year','assigned_room_type', 'booking_changes', 'reservation_status', 'country', 'days_in_waiting_list'], axis =1, inplace = True)
num_features = ["lead_time","arrival_date_week_number",
"arrival_date_day_of_month",
"stays_in_weekend_nights",
"stays_in_week_nights","adults","children",
"babies","is_repeated_guest", "previous_cancellations",
"previous_bookings_not_canceled","agent","company",
"required_car_parking_spaces",
"total_of_special_requests", "adr"]
cat_features = ["hotel","arrival_date_month","meal","market_segment",
"distribution_channel","reserved_room_type",
"deposit_type","customer_type"]
def one_hot_cat_column(feature_name, vocab):
return tf.feature_column.indicator_column(
tf.feature_column.categorical_column_with_vocabulary_list( feature_name,
vocab))
feature_columns = []
for feature_name in cat_features:
# Need to one-hot encode categorical features.
vocabulary = xtrain[feature_name].unique()
feature_columns.append(one_hot_cat_column(feature_name, vocabulary))
for feature_name in num_features:
feature_columns.append(tf.feature_column.numeric_column(feature_name,
dtype=tf.float32))
下一步是为提升树算法创建输入函数:我们指定如何将数据读入模型,用于训练和推理。我们使用 tf.data API 中的 from_tensor_slices 方法直接从 pandas 读取数据:
NUM_EXAMPLES = len(ytrain)
def make_input_fn(X, y, n_epochs=None, shuffle=True):
def input_fn():
dataset = tf.data.Dataset.from_tensor_slices((dict(X), y))
if shuffle:
dataset = dataset.shuffle(NUM_EXAMPLES)
# For training, cycle thru dataset as many times as need (n_epochs=None).
dataset = dataset.repeat(n_epochs)
# In memory training doesn't use batching.
dataset = dataset.batch(NUM_EXAMPLES)
return dataset
return input_fn
# Training and evaluation input functions.
train_input_fn = make_input_fn(xtrain, ytrain)
eval_input_fn = make_input_fn(xvalid, yvalid, shuffle=False, n_epochs=1)
现在我们可以构建实际的 BoostedTrees 模型。我们设置了一个最小的参数列表(max_depth 是其中一个最重要的参数)——在定义中没有指定的参数将保持默认值,这些可以通过文档中的帮助函数查找。
params = {
'n_trees': 125,
'max_depth': 5,
'n_batches_per_layer': 1,
'center_bias': True
}
est = tf.estimator.BoostedTreesClassifier(feature_columns, **params)
# Train model.
est.train(train_input_fn, max_steps=100)
一旦我们训练好一个模型,就可以根据不同的评估指标来评估其性能。BoostedTreesClassifier 包含一个 evaluate 方法,输出涵盖了广泛的可能指标;使用哪些指标进行指导取决于具体应用,但默认输出的指标已经能让我们从多个角度评估模型(例如,如果我们处理的是一个高度不平衡的数据集,auc 可能会有些误导,此时我们应该同时评估损失值)。更详细的说明,请参考文档:www.tensorflow.org/api_docs/python/tf/estimator/BoostedTreesClassifier:
# Evaluation
results = est.evaluate(eval_input_fn)
pd.Series(results).to_frame()
你看到的结果应该是这样的:

pred_dicts = list(est.predict(eval_input_fn))
probs = pd.Series([pred['probabilities'][1] for pred in pred_dicts])
我们可以在不同的泛化层次上评估结果——以下给出全球性和局部性差异的具体说明。我们从接收者操作特征(ROC)曲线开始:这是一种图形,显示了分类模型在所有可能分类阈值下的性能。我们绘制假阳性率与真正阳性率的关系:一个随机分类器会表现为从 (0,0) 到 (1,1) 的对角线,越远离这种情况,朝左上角移动,我们的分类器就越好:
fpr, tpr, _ = roc_curve(yvalid, probs)
plt.plot(fpr, tpr)
plt.title('ROC curve')
plt.xlabel('false positive rate')
plt.ylabel('true positive rate')
plt.xlim(0,); plt.ylim(0,); plt.show()

图 5.1:训练分类器的 ROC 曲线
局部可解释性指的是对模型在单个示例层面的预测的理解:我们将创建并可视化每个实例的贡献。这对于需要向具有技术认知多样性的观众解释模型预测时尤其有用。我们将这些值称为方向性特征贡献(DFC):
pred_dicts = list(est.experimental_predict_with_explanations(eval_input_fn))
# Create DFC Pandas dataframe.
labels = yvalid.values
probs = pd.Series([pred['probabilities'][1] for pred in pred_dicts])
df_dfc = pd.DataFrame([pred['dfc'] for pred in pred_dicts])
df_dfc.describe().T

完整的 DFC 数据框的总结初看起来可能有些让人不知所措,实际上,通常我们会关注某些列的子集。每一行展示的是特征(第一行的arrival_date_week_number,第二行的arrival_date_day_of_month,依此类推)在验证集中的所有观测值的方向性贡献的汇总统计(如mean、std等)。
它是如何工作的……
以下代码块演示了提取某个记录的预测特征贡献所需的步骤。为了方便和可重用,我们首先定义了一个绘制选定记录的函数(为了更容易解释,我们希望使用不同的颜色绘制特征重要性,具体取决于它们的贡献是正向还是负向):
def _get_color(value):
"""To make positive DFCs plot green, negative DFCs plot red."""
green, red = sns.color_palette()[2:4]
if value >= 0: return green
return red
def _add_feature_values(feature_values, ax):
"""Display feature's values on left of plot."""
x_coord = ax.get_xlim()[0]
OFFSET = 0.15
for y_coord, (feat_name, feat_val) in enumerate(feature_values. items()):
t = plt.text(x_coord, y_coord - OFFSET, '{}'.format(feat_val), size=12)
t.set_bbox(dict(facecolor='white', alpha=0.5))
from matplotlib.font_manager import FontProperties
font = FontProperties()
font.set_weight('bold')
t = plt.text(x_coord, y_coord + 1 - OFFSET, 'feature\nvalue',
fontproperties=font, size=12)
def plot_example(example):
TOP_N = 8 # View top 8 features.
sorted_ix = example.abs().sort_values()[-TOP_N:].index # Sort by magnitude.
example = example[sorted_ix]
colors = example.map(_get_color).tolist()
ax = example.to_frame().plot(kind='barh',
color=[colors],
legend=None,
alpha=0.75,
figsize=(10,6))
ax.grid(False, axis='y')
ax.set_yticklabels(ax.get_yticklabels(), size=14)
# Add feature values.
_add_feature_values(xvalid.iloc[ID][sorted_ix], ax)
return ax
定义好样板代码后,我们可以以一种直接的方式绘制特定记录的详细图表:
ID = 10
example = df_dfc.iloc[ID] # Choose ith example from evaluation set.
TOP_N = 8 # View top 8 features.
sorted_ix = example.abs().sort_values()[-TOP_N:].index
ax = plot_example(example)
ax.set_title('Feature contributions for example {}\n pred: {:1.2f}; label: {}'.format(ID, probs[ID], labels[ID]))
ax.set_xlabel('Contribution to predicted probability', size=14)
plt.show()
输出如下:

图 5.2:不同特征对预测概率的贡献
除了分析单个观测值的特征相关性外,我们还可以从全局(聚合)角度进行分析。全局可解释性指的是对模型整体的理解:我们将提取并可视化基于增益的特征重要性和置换特征重要性,同时也会展示聚合的 DFC。
基于增益的特征重要性衡量在对特定特征进行切分时损失变化,而置换特征重要性是通过评估模型在评估集上的表现来计算的,方法是将每个特征依次打乱,并将模型表现的变化归因于被打乱的特征。
通常,置换特征重要性比基于增益的特征重要性更为优选,尽管在潜在预测变量的度量尺度或类别数量不同,以及特征之间存在相关性时,二者的方法都可能不可靠。
计算置换特征重要性的函数如下:
def permutation_importances(est, X_eval, y_eval, metric, features):
"""Column by column, shuffle values and observe effect on eval set.
source: http://explained.ai/rf-importance/index.html
A similar approach can be done during training. See "Drop-column importance"
in the above article."""
baseline = metric(est, X_eval, y_eval)
imp = []
for col in features:
save = X_eval[col].copy()
X_eval[col] = np.random.permutation(X_eval[col])
m = metric(est, X_eval, y_eval)
X_eval[col] = save
imp.append(baseline - m)
return np.array(imp)
def accuracy_metric(est, X, y):
"""TensorFlow estimator accuracy."""
eval_input_fn = make_input_fn(X,
y=y,
shuffle=False,
n_epochs=1)
return est.evaluate(input_fn=eval_input_fn)['accuracy']
我们使用以下函数来显示最相关的列:
features = CATEGORICAL_COLUMNS + NUMERIC_COLUMNS
importances = permutation_importances(est, dfeval, y_eval, accuracy_metric,
features)
df_imp = pd.Series(importances, index=features)
sorted_ix = df_imp.abs().sort_values().index
ax = df_imp[sorted_ix][-5:].plot(kind='barh', color=sns_colors[2], figsize=(10, 6))
ax.grid(False, axis='y')
ax.set_title('Permutation feature importance')
plt.show()
这将为你输出如下结果:

图 5.3:不同特征的置换特征重要性
我们使用以下函数以相同的方式显示增益特征重要性列:
importances = est.experimental_feature_importances(normalize=True)
df_imp = pd.Series(importances)
# Visualize importances.
N = 8
ax = (df_imp.iloc[0:N][::-1]
.plot(kind='barh',
color=sns_colors[0],
title='Gain feature importances',
figsize=(10, 6)))
ax.grid(False, axis='y')
这将为你输出如下结果:

图 5.4:不同特征的增益特征重要性
DFCs 的绝对值可以被平均,以理解全局层面的影响:
dfc_mean = df_dfc.abs().mean()
N = 8
sorted_ix = dfc_mean.abs().sort_values()[-N:].index # Average and sort by absolute.
ax = dfc_mean[sorted_ix].plot(kind='barh',
color=sns_colors[1],
title='Mean |directional feature contributions|',
figsize=(10, 6))
ax.grid(False, axis='y')
这将输出以下结果:

图 5.5: 不同特征的平均方向性特征贡献
在这个教程中,我们介绍了 GradientBoostingClassifier 的 TensorFlow 实现:一种灵活的模型架构,适用于广泛的表格数据问题。我们构建了一个模型来解决一个实际的业务问题:预测客户可能会取消酒店预订的概率,在这个过程中,我们介绍了 TF Boosted Trees 管道的所有相关组件:
-
为模型准备数据
-
使用
tf.estimator配置GradientBoostingClassifier -
评估特征重要性和模型可解释性,既要从全局层面,也要从局部层面
另见
有大量文章介绍梯度提升算法家族:
-
一篇精彩的 Medium 博文:
medium.com/analytics-vidhya/introduction-to-the-gradient-boosting-algorithm-c25c653f826b -
官方 XGBoost 文档:
xgboost.readthedocs.io/en/latest/tutorials/model.html -
LightGBM 文档:
papers.nips.cc/paper/6907-lightgbm-a-highly-efficient-gradient-boosting-decision-tree.pdf
第六章:神经网络
在本章中,我们将介绍神经网络以及如何在 TensorFlow 中实现它们。后续大部分章节将基于神经网络,因此学习如何在 TensorFlow 中使用它们是非常重要的。
神经网络目前在图像和语音识别、阅读手写字、理解文本、图像分割、对话系统、自动驾驶汽车等任务中创下了纪录。虽然这些任务将在后续章节中涵盖,但介绍神经网络作为一种通用、易于实现的机器学习算法是很重要的,这样我们后面可以进一步展开。
神经网络的概念已经存在了几十年。然而,直到最近,由于处理能力、算法效率和数据规模的进步,我们才有足够的计算能力来训练大型网络,因此神经网络才开始获得广泛关注。
神经网络本质上是一系列操作应用于输入数据矩阵的过程。这些操作通常是加法和乘法的组合,之后应用非线性函数。我们已经见过的一个例子是逻辑回归,这在第四章、线性回归中有所讨论。逻辑回归是各个偏斜特征乘积的和,之后应用非线性的 sigmoid 函数。神经网络通过允许任意组合的操作和非线性函数进行更广泛的泛化,这包括应用绝对值、最大值、最小值等。
神经网络中最重要的技巧叫做反向传播。反向传播是一种允许我们根据学习率和损失函数输出更新模型变量的过程。我们在第三章、Keras和第四章、线性回归中都使用了反向传播来更新模型变量。
关于神经网络,另一个需要注意的重要特性是非线性激活函数。由于大多数神经网络只是加法和乘法操作的组合,它们无法对非线性数据集进行建模。为了解决这个问题,我们将在神经网络中使用非线性激活函数。这将使神经网络能够适应大多数非线性情况。
需要记住的是,正如我们在许多已介绍的算法中看到的,神经网络对我们选择的超参数非常敏感。在本章中,我们将探讨不同学习率、损失函数和优化过程的影响。
还有一些我推荐的学习神经网络的资源,它们会更深入、更详细地讲解这个话题:
-
描述反向传播的开创性论文是 Yann LeCun 等人所写的Efficient Back Prop。PDF 文件位于这里:
yann.lecun.com/exdb/publis/pdf/lecun-98b.pdf。 -
CS231,卷积神经网络与视觉识别,由斯坦福大学提供。课程资源可以在这里找到:
cs231n.stanford.edu/。 -
CS224d,自然语言处理的深度学习,由斯坦福大学提供。课程资源可以在这里找到:
cs224d.stanford.edu/。 -
深度学习,由 MIT 出版社出版的书籍。Goodfellow 等,2016 年。本书可以在这里找到:
www.deeplearningbook.org。 -
在线书籍 神经网络与深度学习 由 Michael Nielsen 编写,可以在这里找到:
neuralnetworksanddeeplearning.com/。 -
对于更务实的方法和神经网络的介绍,Andrej Karpathy 写了一个很棒的总结,里面有 JavaScript 示例,叫做 黑客的神经网络指南。该文可以在这里找到:
karpathy.github.io/neuralnets/。 -
另一个很好的总结深度学习的网站是 深度学习入门,由 Ian Goodfellow、Yoshua Bengio 和 Aaron Courville 编写。该网页可以在这里找到:
randomekek.github.io/deep/deeplearning.html。
我们将从介绍神经网络的基本概念开始,然后逐步深入到多层网络。在最后一节,我们将创建一个神经网络,学习如何玩井字游戏。
在本章中,我们将覆盖以下内容:
-
实现操作门
-
与门和激活函数的工作
-
实现单层神经网络
-
实现不同的层
-
使用多层神经网络
-
改善线性模型的预测
-
学习玩井字游戏
读者可以在 github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook 上找到本章的所有代码,并在 Packt 的代码库中找到:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook。
实现操作门
神经网络最基本的概念之一是它作为操作门的功能。在本节中,我们将从乘法运算作为门开始,然后再考虑嵌套的门操作。
准备工作
我们将实现的第一个操作门是 f(x) = a · x:

为了优化这个门,我们将 a 输入声明为变量,并将x作为我们模型的输入张量。这意味着 TensorFlow 会尝试改变 a 的值,而不是 x 的值。我们将创建损失函数,它是输出值和目标值之间的差异,目标值为 50。
第二个嵌套的操作门将是 f(x) = a · x + b:

再次,我们将声明 a 和 b 为变量,x 为我们模型的输入张量。我们再次优化输出,使其趋向目标值 50。需要注意的有趣之处在于,第二个示例的解并不是唯一的。有许多不同的模型变量组合都能使输出为 50。对于神经网络,我们并不太在意中间模型变量的值,而更关注最终的输出值。
如何操作...
为了在 TensorFlow 中实现第一个运算门 f(x) = a · x,并将输出训练到值 50,请按照以下步骤操作:
-
首先,通过以下方式加载 TensorFlow:
import tensorflow as tf -
现在我们需要声明我们的模型变量和输入数据。我们将输入数据设为 5,这样乘数因子将是 10(即 5*10=50),具体如下:
a = tf.Variable(4.) x_data = tf.keras.Input(shape=(1,)) x_val = 5. -
接下来,我们创建一个 Lambda 层来计算操作,并创建一个具有以下输入的功能性 Keras 模型:
multiply_layer = tf.keras.layers.Lambda(lambda x:tf.multiply(a, x)) outputs = multiply_layer(x_data) model = tf.keras.Model(inputs=x_data, outputs=outputs, name="gate_1") -
现在,我们将声明我们的优化算法为随机梯度下降,具体如下:
optimizer=tf.keras.optimizers.SGD(0.01) -
现在,我们可以优化我们的模型输出,使其趋向目标值 50。我们将使用损失函数作为输出与目标值 50 之间的 L2 距离。我们通过不断输入值 5 并反向传播损失来更新模型变量,使其趋向 10,具体如下所示:
print('Optimizing a Multiplication Gate Output to 50.') for i in range(10): # Open a GradientTape. with tf.GradientTape() as tape: # Forward pass. mult_output = model(x_val) # Loss value as the difference between # the output and a target value, 50. loss_value = tf.square(tf.subtract(mult_output, 50.)) # Get gradients of loss with reference to the variable "a" to adjust. gradients = tape.gradient(loss_value, a) # Update the variable "a" of the model. optimizer.apply_gradients(zip([gradients], [a])) print("{} * {} = {}".format(a.numpy(), x_val, a.numpy() * x_val)) -
前面的步骤应该会得到以下输出:
Optimizing a Multiplication Gate Output to 50\. 7.0 * 5.0 = 35.0 8.5 * 5.0 = 42.5 9.25 * 5.0 = 46.25 9.625 * 5.0 = 48.125 9.8125 * 5.0 = 49.0625 9.90625 * 5.0 = 49.5312 9.95312 * 5.0 = 49.7656 9.97656 * 5.0 = 49.8828 9.98828 * 5.0 = 49.9414 9.99414 * 5.0 = 49.9707接下来,我们将对嵌套的两个运算门 f(x) = a · x + b 进行相同的操作。
-
我们将以与前一个示例完全相同的方式开始,但会初始化两个模型变量,
a和b,具体如下:import tensorflow as tf # Initialize variables and input data x_data = tf.keras.Input(dtype=tf.float32, shape=(1,)) x_val = 5. a = tf.Variable(1., dtype=tf.float32) b = tf.Variable(1., dtype=tf.float32) # Add a layer which computes f(x) = a * x multiply_layer = tf.keras.layers.Lambda(lambda x:tf.multiply(a, x)) # Add a layer which computes f(x) = b + x add_layer = tf.keras.layers.Lambda(lambda x:tf.add(b, x)) res = multiply_layer(x_data) outputs = add_layer(res) # Build the model model = tf.keras.Model(inputs=x_data, outputs=outputs, name="gate_2") # Optimizer optimizer=tf.keras.optimizers.SGD(0.01) -
我们现在优化模型变量,将输出训练到目标值 50,具体如下所示:
print('Optimizing two Gate Output to 50.') for i in range(10): # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. two_gate_output = model(x_val) # Loss value as the difference between # the output and a target value, 50. loss_value = tf.square(tf.subtract(two_gate_output, 50.)) # Get gradients of loss with reference to # the variables "a" and "b" to adjust. gradients_a = tape.gradient(loss_value, a) gradients_b = tape.gradient(loss_value , b) # Update the variables "a" and "b" of the model. optimizer.apply_gradients(zip([gradients_a, gradients_b], [a, b])) print("Step: {} ==> {} * {} + {}= {}".format(i, a.numpy(), x_val, b.numpy(), a.numpy()*x_val+b.numpy())) -
前面的步骤应该会得到以下输出:
Optimizing Two Gate Output to 50\. 5.4 * 5.0 + 1.88 = 28.88 7.512 * 5.0 + 2.3024 = 39.8624 8.52576 * 5.0 + 2.50515 = 45.134 9.01236 * 5.0 + 2.60247 = 47.6643 9.24593 * 5.0 + 2.64919 = 48.8789 9.35805 * 5.0 + 2.67161 = 49.4619 9.41186 * 5.0 + 2.68237 = 49.7417 9.43769 * 5.0 + 2.68754 = 49.876 9.45009 * 5.0 + 2.69002 = 49.9405 9.45605 * 5.0 + 2.69121 = 49.9714
需要注意的是,第二个示例的解并不是唯一的。这在神经网络中并不那么重要,因为所有参数都被调整以减少损失。这里的最终解将取决于 a 和 b 的初始值。如果这些值是随机初始化的,而不是设为 1,我们会看到每次迭代后模型变量的最终值不同。
它是如何工作的...
我们通过 TensorFlow 的隐式反向传播实现了计算门的优化。TensorFlow 跟踪我们模型的操作和变量值,并根据我们的优化算法规范和损失函数的输出进行调整。
我们可以继续扩展运算门,同时跟踪哪些输入是变量,哪些输入是数据。这一点非常重要,因为 TensorFlow 会调整所有变量以最小化损失,但不会更改数据。
能够隐式跟踪计算图并在每次训练步骤中自动更新模型变量,是 TensorFlow 的一个重要特点,这也使得它非常强大。
与门和激活函数的工作
现在,我们可以将操作门连接在一起,我们希望通过激活函数运行计算图输出。在本节中,我们将介绍常见的激活函数。
准备工作
在本节中,我们将比较并对比两种不同的激活函数:sigmoid和rectified linear unit(ReLU)。回顾一下,这两个函数由以下方程给出:


在这个例子中,我们将创建两个单层神经网络,它们具有相同的结构,唯一不同的是一个使用 sigmoid 激活函数,另一个使用 ReLU 激活函数。损失函数将由与 0.75 的 L2 距离决定。我们将随机提取批处理数据,然后优化输出使其趋向 0.75。
如何操作...
我们按以下步骤进行操作:
-
我们将从加载必要的库开始。这也是我们可以提到如何在 TensorFlow 中设置随机种子的好时机。由于我们将使用 NumPy 和 TensorFlow 的随机数生成器,因此我们需要为两者设置随机种子。设置相同的随机种子后,我们应该能够复制结果。我们通过以下输入来实现:
import tensorflow as tf import numpy as np import matplotlib.pyplot as plt tf.random.set_seed(5) np.random.seed(42) -
现在,我们需要声明我们的批处理大小、模型变量和数据模型输入。我们的计算图将包括将正态分布数据输入到两个相似的神经网络中,这两个网络仅在末端的激活函数上有所不同,如下所示:
batch_size = 50 x_data = tf.keras.Input(shape=(1,)) x_data = tf.keras.Input(shape=(1,)) a1 = tf.Variable(tf.random.normal(shape=[1,1], seed=5)) b1 = tf.Variable(tf.random.uniform(shape=[1,1], seed=5)) a2 = tf.Variable(tf.random.normal(shape=[1,1], seed=5)) b2 = tf.Variable(tf.random.uniform(shape=[1,1], seed=5)) -
接下来,我们将声明我们的两个模型,sigmoid 激活模型和 ReLU 激活模型,如下所示:
class MyCustomGateSigmoid(tf.keras.layers.Layer): def __init__(self, units, a1, b1): super(MyCustomGateSigmoid, self).__init__() self.units = units self.a1 = a1 self.b1 = b1 # Compute f(x) = sigmoid(a1 * x + b1) def call(self, inputs): return tf.math.sigmoid(inputs * self.a1 + self.b1) # Add a layer which computes f(x) = sigmoid(a1 * x + b1) my_custom_gate_sigmoid = MyCustomGateSigmoid(units=1, a1=a1, b1=b1) output_sigmoid = my_custom_gate_sigmoid(x_data) # Build the model model_sigmoid = tf.keras.Model(inputs=x_data, outputs=output_sigmoid, name="gate_sigmoid") class MyCustomGateRelu(tf.keras.layers.Layer): def __init__(self, units, a2, b2): super(MyCustomGateRelu, self).__init__() self.units = units self.a2 = a2 self.b2 = b2 # Compute f(x) = relu(a2 * x + b2) def call(self, inputs): return tf.nn.relu(inputs * self.a2 + self.b2) # Add a layer which computes f(x) = relu(a2 * x + b2) my_custom_gate_relu = MyCustomGateRelu(units=1, a2=a2, b2=b2) outputs_relu = my_custom_gate_relu(x_data) # Build the model model_relu = tf.keras.Model(inputs=x_data, outputs=outputs_relu, name="gate_relu") -
现在我们需要声明我们的优化算法并初始化变量,如下所示:
optimizer=tf.keras.optimizers.SGD(0.01) -
现在,我们将对两个模型进行 750 次迭代训练,如下所示的代码块所示。损失函数将是模型输出与 0.75 的 L2 范数平均值。我们还将保存损失输出和激活输出值,稍后用于绘图:
# Run loop across gate print('\n Optimizing Sigmoid AND Relu Output to 0.75') loss_vec_sigmoid = [] loss_vec_relu = [] activation_sigmoid = [] activation_relu = [] for i in range(500): rand_indices = np.random.choice(len(x), size=batch_size) x_vals = np.transpose([x[rand_indices]]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output_sigmoid = model_sigmoid(x_vals) output_relu = model_relu(x_vals) # Loss value as the difference as the difference between # the output and a target value, 0.75. loss_sigmoid = tf.reduce_mean(tf.square(tf.subtract(output_sigmoid, 0.75))) loss_vec_sigmoid.append(loss_sigmoid) loss_relu = tf.reduce_mean(tf.square(tf.subtract(output_relu, 0.75))) loss_vec_relu.append(loss_relu) # Get gradients of loss_sigmoid with reference to the variable "a1" and "b1" to adjust. gradients_a1 = tape.gradient(loss_sigmoid, my_custom_gate_sigmoid.a1) gradients_b1 = tape.gradient(loss_sigmoid , my_custom_gate_sigmoid.b1) # Get gradients of loss_relu with reference to the variable "a2" and "b2" to adjust. gradients_a2 = tape.gradient(loss_relu, my_custom_gate_relu.a2) gradients_b2 = tape.gradient(loss_relu , my_custom_gate_relu.b2) # Update the variable "a1" and "b1" of the model. optimizer.apply_gradients(zip([gradients_a1, gradients_b1], [my_custom_gate_sigmoid.a1, my_custom_gate_sigmoid.b1])) # Update the variable "a2" and "b2" of the model. optimizer.apply_gradients(zip([gradients_a2, gradients_b2], [my_custom_gate_relu.a2, my_custom_gate_relu.b2])) output_sigmoid = model_sigmoid(x_vals) output_relu = model_relu(x_vals) activation_sigmoid.append(np.mean(output_sigmoid)) activation_relu.append(np.mean(output_relu)) if i%50==0: print('sigmoid = ' + str(np.mean(output_sigmoid)) + ' relu = ' + str(np.mean(output_relu))) -
要绘制损失和激活输出,我们需要输入以下代码:
plt.plot(activation_sigmoid, 'k-', label='Sigmoid Activation') plt.plot(activation_relu, 'r--', label='Relu Activation') plt.ylim([0, 1.0]) plt.title('Activation Outputs') plt.xlabel('Generation') plt.ylabel('Outputs') plt.legend(loc='upper right') plt.show() plt.plot(loss_vec_sigmoid, 'k-', label='Sigmoid Loss') plt.plot(loss_vec_relu, 'r--', label='Relu Loss') plt.ylim([0, 1.0]) plt.title('Loss per Generation') plt.xlabel('Generation') plt.ylabel('Loss') plt.legend(loc='upper right') plt.show()
激活输出需要如以下图所示进行绘制:

图 6.1:具有 sigmoid 激活函数的网络与具有 ReLU 激活函数的网络的计算图输出
这两个神经网络具有相似的架构和目标(0.75),但使用了两种不同的激活函数:sigmoid 和 ReLU。需要注意的是,ReLU 激活网络比 sigmoid 激活网络更快速地收敛到 0.75 这一目标,如下图所示:

图 6.2:该图展示了 sigmoid 和 ReLU 激活网络的损失值。注意 ReLU 损失在迭代初期的极端情况
它是如何工作的...
由于 ReLU 激活函数的形式,它比 sigmoid 函数更频繁地返回零值。我们将这种行为视为一种稀疏性。这种稀疏性加速了收敛速度,但却失去了对梯度的控制。另一方面,sigmoid 函数具有非常好的梯度控制,且不像 ReLU 激活函数那样会产生极端值,以下表格进行了说明:
| 激活函数 | 优势 | 劣势 |
|---|---|---|
| Sigmoid | 输出较少极端 | 收敛较慢 |
| ReLU | 快速收敛 | 可能产生极端输出值 |
还有更多内容...
在本节中,我们比较了 ReLU 激活函数和 sigmoid 激活函数在神经网络中的表现。虽然有许多其他激活函数常用于神经网络,但大多数都属于以下两类:第一类包含类似 sigmoid 函数形状的函数,如 arctan、hypertangent、Heaviside 阶跃函数等;第二类包含类似 ReLU 函数形状的函数,如 softplus、leaky ReLU 等。我们在本节中讨论的关于比较这两种函数的大部分内容,适用于这两类激活函数。然而,值得注意的是,激活函数的选择对神经网络的收敛性和输出有很大影响。
实现一个单层神经网络
我们已经具备了实施神经网络所需的所有工具,因此在本节中,我们将创建一个在Iris数据集上运行的单层神经网络。
准备工作
在本节中,我们将实现一个具有一个隐藏层的神经网络。理解全连接神经网络大多数基于矩阵乘法是非常重要的。因此,确保数据和矩阵的维度正确对齐也很重要。
由于这是一个回归问题,我们将使用均方误差(MSE)作为损失函数。
如何操作...
我们按如下步骤继续操作:
-
为了创建计算图,我们将从加载以下必要的库开始:
import matplotlib.pyplot as plt import numpy as np import tensorflow as tf from sklearn import datasets -
现在,我们将加载
Iris数据集,并通过以下代码将长度存储为目标值:iris = datasets.load_iris() x_vals = np.array([x[0:3] for x in iris.data]) y_vals = np.array([x[3] for x in iris.data]) -
由于数据集较小,我们希望设置一个种子,使得结果可复现,具体如下:
seed = 3 tf.set_random_seed(seed) np.random.seed(seed) -
为了准备数据,我们将创建一个 80-20 的训练集和测试集拆分,并通过 min-max 缩放将
x特征规范化到0和1之间,具体如下所示:train_indices = np.random.choice(len(x_vals), round(len(x_vals)*0.8), replace=False) test_indices = np.array(list(set(range(len(x_vals))) - set(train_indices))) x_vals_train = x_vals[train_indices] x_vals_test = x_vals[test_indices] y_vals_train = y_vals[train_indices] y_vals_test = y_vals[test_indices] def normalize_cols(m): col_max = m.max(axis=0) col_min = m.min(axis=0) return (m-col_min) / (col_max - col_min) x_vals_train = np.nan_to_num(normalize_cols(x_vals_train)) x_vals_test = np.nan_to_num(normalize_cols(x_vals_test)) -
现在,我们将声明批次大小和数据模型输入,具体代码如下:
batch_size = 50 x_data = tf.keras.Input(dtype=tf.float32, shape=(3,)) -
关键部分是用适当的形状声明我们的模型变量。我们可以声明我们隐藏层的大小为任意大小;在下面的代码块中,我们设置它为五个隐藏节点:
hidden_layer_nodes = 5 a1 = tf.Variable(tf.random.normal(shape=[3,hidden_layer_nodes], seed=seed)) b1 = tf.Variable(tf.random.normal(shape=[hidden_layer_nodes], seed=seed)) a2 = tf.Variable(tf.random.normal(shape=[hidden_layer_nodes,1], seed=seed)) b2 = tf.Variable(tf.random.normal(shape=[1], seed=seed)) -
现在我们将分两步声明我们的模型。第一步将创建隐藏层的输出,第二步将创建模型的
final_output,如下所示:请注意,我们的模型从三个输入特征到五个隐藏节点,最后到一个输出值。
hidden_output = tf.keras.layers.Lambda(lambda x: tf.nn.relu(tf.add(tf.matmul(x, a1), b1))) final_output = tf.keras.layers.Lambda(lambda x: tf.nn.relu(tf.add(tf.matmul(x, a2), b2))) model = tf.keras.Model(inputs=x_data, outputs=output, name="1layer_neural_network") -
现在我们将使用以下代码声明优化算法:
optimizer = tf.keras.optimizers.SGD(0.005) -
接下来,我们循环执行训练迭代。我们还将初始化两个列表,用于存储我们的
train和test_loss函数。在每个循环中,我们还希望从训练数据中随机选择一个批次,以适应模型,如下所示:# First we initialize the loss vectors for storage. loss_vec = [] test_loss = [] for i in range(500): rand_index = np.random.choice(len(x_vals_train), size=batch_size) rand_x = x_vals_train[rand_index] rand_y = np.transpose([y_vals_train[rand_index]]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output = model(rand_x) # Apply loss function (MSE) loss = tf.reduce_mean(tf.square(rand_y - output)) loss_vec.append(np.sqrt(loss)) # Get gradients of loss with reference to the variables to adjust. gradients_a1 = tape.gradient(loss, a1) gradients_b1 = tape.gradient(loss, b1) gradients_a2 = tape.gradient(loss, a2) gradients_b2 = tape.gradient(loss, b2) # Update the variables of the model. optimizer.apply_gradients(zip([gradients_a1, gradients_b1, gradients_a2, gradients_b2], [a1, b1, a2, b2])) # Forward pass. output_test = model(x_vals_test) # Apply loss function (MSE) on test loss_test = tf.reduce_mean(tf.square(np.transpose([y_vals_test]) - output_test)) test_loss.append(np.sqrt(loss_test)) if (i+1)%50==0: print('Generation: ' + str(i+1) + '. Loss = ' + str(np.mean(loss))) print('Generation: ' + str(i+1) + '. Loss = ' + str(temp_loss)) -
我们可以用
matplotlib和以下代码绘制损失:plt.plot(loss_vec, 'k-', label='Train Loss') plt.plot(test_loss, 'r--', label='Test Loss') plt.title('Loss (MSE) per Generation') plt.xlabel('Generation') plt.ylabel('Loss') plt.legend(loc='upper right') plt.show()
我们继续通过绘制以下图表来进行本次实验:
(img/B16254_06_07.png)
图 6.3:我们绘制了训练集和测试集的损失(MSE)
注意,我们还可以看到训练集的损失不像测试集那样平滑。这是由于两个原因:首先,我们使用的批量大小比测试集小,尽管差距不大;第二个原因是我们在训练集上训练,而测试集不影响模型的变量。
工作原理...
我们的模型现在已经被可视化为神经网络图,如下所示:
(img/B16254_06_08.png)
图 6.4:神经网络图
前面的图是我们的神经网络的可视化,隐藏层有五个节点。我们输入三个值:sepal length(S.L.)、sepal width(S.W.)和petal length(P.L.)。目标将是花瓣宽度。总共,模型中将有 26 个变量。
实现不同的层
重要的是要知道如何实现不同的层。在前面的示例中,我们实现了全连接层。在这个示例中,我们将进一步扩展我们对各种层的了解。
准备工作
我们已经探讨了如何连接数据输入和完全连接的隐藏层,但是在 TensorFlow 中还有更多类型的内置函数可用作层。最常用的层是卷积层和最大池层。我们将展示如何在一维数据和二维数据上创建和使用这些层。首先,我们将看看如何在一维数据上使用这些层,然后是二维数据。
尽管神经网络可以以任何方式分层,但最常见的设计之一是首先使用卷积层和全连接层创建特征。如果然后有太多的特征,常见的做法是使用最大池层。
在这些层之后,通常会引入非线性层作为激活函数。卷积神经网络(CNNs),我们将在第八章中讨论,通常具有卷积层、最大池化层和激活层。
如何操作...
我们将首先查看一维数据。我们需要为这个任务生成一个随机数据数组,操作步骤如下:
-
我们将首先加载所需的库,如下所示:
import tensorflow as tf import numpy as np -
现在我们将初始化一些参数,并使用以下代码创建输入数据层:
data_size = 25 conv_size = 5 maxpool_size = 5 stride_size = 1 num_outputs = 5 x_input_1d = tf.keras.Input(dtype=tf.float32, shape=(data_size,1), name="input_layer") -
接下来,我们将定义一个卷积层,如下所示:
对于我们的示例数据,批处理大小为
1,宽度为1,高度为25,通道大小为1。还请注意,我们可以通过output_size=(W-F+2P)/S+1公式来计算卷积层的输出尺寸,其中W是输入尺寸,F是滤波器尺寸,P是填充尺寸,S是步幅尺寸。my_conv_output = tf.keras.layers.Conv1D(kernel_size=(conv_size), filters=data_size, strides=stride_size, padding="VALID", name="convolution_layer")(x_input_1d) -
接下来,我们添加一个 ReLU 激活层,如下所示:
my_activation_output = tf.keras.layers.ReLU(name="activation_layer")(my_conv_output) -
现在我们将添加一个最大池化层。这个层将在我们的一维向量上创建一个
maxpool,并在一个移动窗口中应用。对于这个例子,我们将初始化它,使宽度为 5,如下所示:TensorFlow 的
maxpool参数与卷积层的非常相似。虽然maxpool参数没有滤波器,但它有大小、步幅和填充选项。由于我们有一个宽度为 5 的窗口,并且使用有效填充(没有零填充),所以我们的输出数组将少 4 个元素。my_maxpool_output = tf.keras.layers.MaxPool1D(strides=stride_size, pool_size=maxpool_size, padding='VALID', name="maxpool_layer")(my_activation_output) -
我们将连接的最后一层是全连接层。在这里,我们将使用一个密集层,如下所示的代码块:
my_full_output = tf.keras.layers.Dense(units=num_outputs, name="fully_connected_layer")(my_maxpool_output) -
现在我们将创建模型,并打印每一层的输出,如下所示:
print('>>>> 1D Data <<<<') model_1D = tf.keras.Model(inputs=x_input_1d, outputs=my_full_output, name="model_1D") model_1D.summary() # Input print('\n== input_layer ==') print('Input = array of length %d' % (x_input_1d.shape.as_list()[1])) # Convolution print('\n== convolution_layer ==') print('Convolution w/ filter, length = %d, stride size = %d, results in an array of length %d' % (conv_size,stride_size,my_conv_output.shape.as_list()[1])) # Activation print('\n== activation_layer ==') print('Input = above array of length %d' % (my_conv_output.shape.as_list()[1])) print('ReLU element wise returns an array of length %d' % (my_activation_output.shape.as_list()[1])) # Max Pool print('\n== maxpool_layer ==') print('Input = above array of length %d' % (my_activation_output.shape.as_list()[1])) print('MaxPool, window length = %d, stride size = %d, results in the array of length %d' % (maxpool_size,stride_size,my_maxpool_output.shape.as_list()[1])) # Fully Connected print('\n== fully_connected_layer ==') print('Input = above array of length %d' % (my_maxpool_output.shape.as_list()[1])) print('Fully connected layer on all 4 rows with %d outputs' % (my_full_output.shape.as_list()[1])) -
前面的步骤应该会生成以下输出:
>>>> 1D Data <<<< Model: "model_1D" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_layer (InputLayer) [(None, 25, 1)] 0 _________________________________________________________________ convolution_layer (Conv1D) (None, 21, 25) 150 _________________________________________________________________ activation_layer (ReLU) (None, 21, 25) 0 _________________________________________________________________ maxpool_layer (MaxPooling1D) (None, 17, 25) 0 _________________________________________________________________ fully_connected_layer (Dense (None, 17, 5) 130 ================================================================= Total params: 280 Trainable params: 280 Non-trainable params: 0 _________________________________________________________________ == input_layer == Input = array of length 25 == convolution_layer == Convolution w/ filter, length = 5, stride size = 1, results in an array of length 21 == activation_layer == Input = above array of length 21 ReLU element wise returns an array of length 21 == maxpool_layer == Input = above array of length 21 MaxPool, window length = 5, stride size = 1, results in the array of length 17 == fully_connected_layer == Input = above array of length 17 Fully connected layer on all 4 rows with 17 outputs
一维数据对神经网络来说非常重要。时间序列、信号处理和一些文本嵌入都被认为是一维数据,并且在神经网络中被频繁使用。
现在我们将考虑相同类型的层,但对于二维数据,顺序是等效的:
-
我们将首先初始化变量,如下所示:
row_size = 10 col_size = 10 conv_size = 2 conv_stride_size = 2 maxpool_size = 2 maxpool_stride_size = 1 num_outputs = 5 -
然后我们将初始化输入数据层。由于我们的数据已经有了高度和宽度,我们只需要在两个维度上扩展它(批处理大小为 1,通道大小为 1),如下所示:
x_input_2d = tf.keras.Input(dtype=tf.float32, shape=(row_size,col_size, 1), name="input_layer_2d") -
就像一维示例中一样,现在我们需要添加一个二维卷积层。对于滤波器,我们将使用一个随机的 2x2 滤波器,步幅为 2,方向上都为 2,并使用有效填充(换句话说,不使用零填充)。由于我们的输入矩阵是 10x10,因此卷积输出将是 5x5,如下所示:
my_convolution_output_2d = tf.keras.layers.Conv2D(kernel_size=(conv_size), filters=conv_size, strides=conv_stride_size, padding="VALID", name="convolution_layer_2d")(x_input_2d) -
接下来,我们添加一个 ReLU 激活层,如下所示:
my_activation_output_2d = tf.keras.layers.ReLU(name="activation_layer_2d")(my_convolution_output_2d) -
我们的最大池化层与一维情况非常相似,唯一不同的是我们需要为最大池化窗口和步幅声明宽度和高度。在我们的例子中,我们将在所有空间维度上使用相同的值,因此我们将设置整数值,如下所示:
my_maxpool_output_2d = tf.keras.layers.MaxPool2D(strides=maxpool_stride_size, pool_size=maxpool_size, padding='VALID', name="maxpool_layer_2d")(my_activation_output_2d) -
我们的全连接层与一维输出非常相似。我们使用一个稠密层,具体如下:
my_full_output_2d = tf.keras.layers.Dense(units=num_outputs, name="fully_connected_layer_2d")(my_maxpool_output_2d) -
现在我们将创建模型,并打印每一层的输出,具体如下:
print('>>>> 2D Data <<<<') model_2D = tf.keras.Model(inputs=x_input_2d, outputs=my_full_output_2d, name="model_2D") model_2D.summary() # Input print('\n== input_layer ==') print('Input = %s array' % (x_input_2d.shape.as_list()[1:3])) # Convolution print('\n== convolution_layer ==') print('%s Convolution, stride size = [%d, %d] , results in the %s array' % ([conv_size,conv_size],conv_stride_size,conv_stride_size,my_convolution_output_2d.shape.as_list()[1:3])) # Activation print('\n== activation_layer ==') print('Input = the above %s array' % (my_convolution_output_2d.shape.as_list()[1:3])) print('ReLU element wise returns the %s array' % (my_activation_output_2d.shape.as_list()[1:3])) # Max Pool print('\n== maxpool_layer ==') print('Input = the above %s array' % (my_activation_output_2d.shape.as_list()[1:3])) print('MaxPool, stride size = [%d, %d], results in %s array' % (maxpool_stride_size,maxpool_stride_size,my_maxpool_output_2d.shape.as_list()[1:3])) # Fully Connected print('\n== fully_connected_layer ==') print('Input = the above %s array' % (my_maxpool_output_2d.shape.as_list()[1:3])) print('Fully connected layer on all %d rows results in %s outputs' % (my_maxpool_output_2d.shape.as_list()[1],my_full_output_2d.shape.as_list()[3])) feed_dict = {x_input_2d: data_2d} -
上述步骤应该会得到以下输出:
>>>> 2D Data <<<< Model: "model_2D" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_layer_2d (InputLayer) [(None, 10, 10, 1)] 0 _________________________________________________________________ convolution_layer_2d (Conv2D (None, 5, 5, 2) 10 _________________________________________________________________ activation_layer_2d (ReLU) (None, 5, 5, 2) 0 _________________________________________________________________ maxpool_layer_2d (MaxPooling (None, 4, 4, 2) 0 _________________________________________________________________ fully_connected_layer_2d (De (None, 4, 4, 5) 15 ================================================================= Total params: 25 Trainable params: 25 Non-trainable params: 0 _________________________________________________________________ == input_layer == Input = [10, 10] array == convolution_layer == [2, 2] Convolution, stride size = [2, 2] , results in the [5, 5] array == activation_layer == Input = the above [5, 5] array ReLU element wise returns the [5, 5] array == maxpool_layer == Input = the above [5, 5] array MaxPool, stride size = [1, 1], results in [4, 4] array == fully_connected_layer == Input = the above [4, 4] array Fully connected layer on all 4 rows results in 5 outputs
它是如何工作的...
我们现在应该知道如何在 TensorFlow 中使用卷积层和最大池化层,处理一维和二维数据。不管输入的形状如何,我们最终都会得到相同大小的输出。这对于展示神经网络层的灵活性非常重要。本节还应再次提醒我们,形状和大小在神经网络操作中的重要性。
使用多层神经网络
我们将通过在低出生体重数据集上使用多层神经网络,应用我们对不同层的知识于实际数据。
准备工作
现在我们知道如何创建神经网络并处理层,我们将应用这种方法来预测低出生体重数据集中的出生体重。我们将创建一个有三层隐藏层的神经网络。低出生体重数据集包括实际出生体重和一个指示变量,表明给定的出生体重是否高于或低于 2500 克。在这个示例中,我们将目标设为实际出生体重(回归),然后查看分类结果的准确性。最终,我们的模型应该能够判断出生体重是否低于 2500 克。
如何操作...
我们按照以下步骤继续操作:
-
我们将开始加载库,具体如下:
import tensorflow as tf import matplotlib.pyplot as plt import csv import random import numpy as np import requests import os -
我们将使用
requests模块从网站加载数据。之后,我们将把数据拆分为感兴趣的特征和目标值,具体如下:# name of data file birth_weight_file = 'birth_weight.csv' # download data and create data file if file does not exist in current directory if not os.path.exists(birth_weight_file): birthdata_url = 'https://github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook/blob/master/ch6/06_Using_Multiple_Layers/birth_weight.csv' birth_file = requests.get(birthdata_url) birth_data = birth_file.text.split('\r\n') birth_header = birth_data[0].split('\t') birth_data = [[float(x) for x in y.split('\t') if len(x)>=1] for y in birth_data[1:] if len(y)>=1] with open(birth_weight_file, "w") as f: writer = csv.writer(f) writer.writerows([birth_header]) writer.writerows(birth_data) f.close() # read birth weight data into memory birth_data = [] with open(birth_weight_file, newline='') as csvfile: csv_reader = csv.reader(csvfile) birth_header = next(csv_reader) for row in csv_reader: birth_data.append(row) birth_data = [[float(x) for x in row] for row in birth_data] # Extract y-target (birth weight) y_vals = np.array([x[8] for x in birth_data]) # Filter for features of interest cols_of_interest = ['AGE', 'LWT', 'RACE', 'SMOKE', 'PTL', 'HT', 'UI'] x_vals = np.array([[x[ix] for ix, feature in enumerate(birth_header) if feature in cols_of_interest] for x in birth_data]) -
为了帮助结果的可重复性,我们现在需要为 NumPy 和 TensorFlow 设置随机种子。然后我们声明我们的批处理大小,具体如下:
# make results reproducible seed = 3 np.random.seed(seed) tf.random.set_seed(seed) # set batch size for training batch_size = 150 -
接下来,我们将数据分割为 80-20 的训练集和测试集。之后,我们需要对输入特征进行归一化,使其值在 0 到 1 之间,采用最小-最大缩放,具体如下:
train_indices = np.random.choice(len(x_vals), round(len(x_vals)*0.8), replace=False) test_indices = np.array(list(set(range(len(x_vals))) - set(train_indices))) x_vals_train = x_vals[train_indices] x_vals_test = x_vals[test_indices] y_vals_train = y_vals[train_indices] y_vals_test = y_vals[test_indices] # Record training column max and min for scaling of non-training data train_max = np.max(x_vals_train, axis=0) train_min = np.min(x_vals_train, axis=0) # Normalize by column (min-max norm to be between 0 and 1) def normalize_cols(mat, max_vals, min_vals): return (mat - min_vals) / (max_vals - min_vals) x_vals_train = np.nan_to_num(normalize_cols(x_vals_train, train_max, train_min)) x_vals_test = np.nan_to_num(normalize_cols(x_vals_test, train_max, train_min))对输入特征进行归一化是一种常见的特征转换方法,尤其对于神经网络特别有用。如果我们的数据在 0 到 1 之间进行中心化,这将有助于激活函数的收敛。
-
由于我们有多个具有相似初始化变量的层,现在我们需要创建一个函数来初始化权重和偏置。我们使用以下代码完成此任务:
# Define Variable Functions (weights and bias) def init_weight(shape, st_dev): weight = tf.Variable(tf.random.normal(shape, stddev=st_dev)) return(weight) def init_bias(shape, st_dev): bias = tf.Variable(tf.random.normal(shape, stddev=st_dev)) return(bias) -
现在我们需要初始化我们的输入数据层。将有七个输入特征,输出将是出生体重(以克为单位):
x_data = tf.keras.Input(dtype=tf.float32, shape=(7,)) -
全连接层将用于所有三个隐藏层,每个层都会使用三次。为了避免重复代码,我们将创建一个层函数,以便在初始化模型时使用,具体如下:
# Create a fully connected layer: def fully_connected(input_layer, weights, biases): return tf.keras.layers.Lambda(lambda x: tf.nn.relu(tf.add(tf.matmul(x, weights), biases)))(input_layer) -
现在是时候创建我们的模型了。对于每一层(以及输出层),我们将初始化一个权重矩阵、一个偏置矩阵和全连接层。对于这个示例,我们将使用大小分别为 25、10 和 3 的隐藏层:
我们使用的模型将有 522 个变量需要拟合。为了得到这个数字,我们可以看到在数据和第一个隐藏层之间有 7*25+25=200 个变量。如果我们继续这样加总,我们会得到 200+260+33+4=497 个变量。这比我们在逻辑回归模型中使用的九个变量要大得多。
#--------Create the first layer (25 hidden nodes)-------- weight_1 = init_weight(shape=[7,25], st_dev=5.0) bias_1 = init_bias(shape=[25], st_dev=10.0) layer_1 = fully_connected(x_data, weight_1, bias_1) #--------Create second layer (10 hidden nodes)-------- weight_2 = init_weight(shape=[25, 10], st_dev=5.0) bias_2 = init_bias(shape=[10], st_dev=10.0) layer_2 = fully_connected(layer_1, weight_2, bias_2) #--------Create third layer (3 hidden nodes)-------- weight_3 = init_weight(shape=[10, 3], st_dev=5.0) bias_3 = init_bias(shape=[3], st_dev=10.0) layer_3 = fully_connected(layer_2, weight_3, bias_3) #--------Create output layer (1 output value)-------- weight_4 = init_weight(shape=[3, 1], st_dev=5.0) bias_4 = init_bias(shape=[1], st_dev=10.0) final_output = fully_connected(layer_3, weight_4, bias_4) model = tf.keras.Model(inputs=x_data, outputs=final_output, name="multiple_layers_neural_network") -
接下来,我们将声明优化器(使用 Adam 优化算法),并循环执行训练迭代。我们将使用 L1 损失函数(绝对值)。我们还将初始化两个列表,用于存储我们的
train和test_loss函数。在每次循环中,我们还希望随机选择一批训练数据进行模型拟合,并在每 25 代时打印状态,如下所示:# Declare Adam optimizer optimizer = tf.keras.optimizers.Adam(0.025) # Training loop loss_vec = [] test_loss = [] for i in range(200): rand_index = np.random.choice(len(x_vals_train), size=batch_size) rand_x = x_vals_train[rand_index] rand_y = np.transpose([y_vals_train[rand_index]]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output = model(rand_x) # Apply loss function (MSE) loss = tf.reduce_mean(tf.abs(rand_y - output)) loss_vec.append(loss) # Get gradients of loss with reference to the weights and bias variables to adjust. gradients_w1 = tape.gradient(loss, weight_1) gradients_b1 = tape.gradient(loss, bias_1) gradients_w2 = tape.gradient(loss, weight_2) gradients_b2 = tape.gradient(loss, bias_2) gradients_w3 = tape.gradient(loss, weight_3) gradients_b3 = tape.gradient(loss, bias_3) gradients_w4 = tape.gradient(loss, weight_4) gradients_b4 = tape.gradient(loss, bias_4) # Update the weights and bias variables of the model. optimizer.apply_gradients(zip([gradients_w1, gradients_b1, gradients_w2, gradients_b2, gradients_w3, gradients_b3, gradients_w4, gradients_b4], [weight_1, bias_1, weight_2, bias_2, weight_3, bias_3, weight_4, bias_4])) # Forward pass. output_test = model(x_vals_test) # Apply loss function (MSE) on test temp_loss = tf.reduce_mean(tf.abs(np.transpose([y_vals_test]) - output_test)) test_loss.append(temp_loss) if (i+1) % 25 == 0: print('Generation: ' + str(i+1) + '. Loss = ' + str(loss.numpy())) -
上一步应该会得到如下输出:
Generation: 25\. Loss = 1921.8002 Generation: 50\. Loss = 1453.3898 Generation: 75\. Loss = 987.57074 Generation: 100\. Loss = 709.81696 Generation: 125\. Loss = 508.625 Generation: 150\. Loss = 541.36774 Generation: 175\. Loss = 539.6093 Generation: 200\. Loss = 441.64032 -
以下是一个代码片段,它使用
matplotlib绘制训练和测试损失:plt.plot(loss_vec, 'k-', label='Train Loss') plt.plot(test_loss, 'r--', label='Test Loss') plt.title('Loss per Generation') plt.xlabel('Generation') plt.ylabel('Loss') plt.legend(loc='upper right') plt.show()我们通过绘制以下图表继续进行该步骤:
![/var/folders/6z/j6f33tds3v7dv8tryfqnxf5m0000gn/T/com.microsoft.Word/Content.MSO/AE276A4A.tmp]()
图 6.5:在上图中,我们绘制了为预测出生体重(单位:克)而训练的神经网络的训练和测试损失。注意,大约经过 30 代后,我们已经得到一个很好的模型。
-
现在,我们需要输出训练和测试的回归结果,并通过创建一个指示器来将它们转化为分类结果,判断它们是否高于或低于 2,500 克。为了找出模型的准确性,我们需要使用以下代码:
# Model Accuracy actuals = np.array([x[0] for x in birth_data]) test_actuals = actuals[test_indices] train_actuals = actuals[train_indices] test_preds = model(x_vals_test) train_preds = model(x_vals_train) test_preds = np.array([1.0 if x < 2500.0 else 0.0 for x in test_preds]) train_preds = np.array([1.0 if x < 2500.0 else 0.0 for x in train_preds]) # Print out accuracies test_acc = np.mean([x == y for x, y in zip(test_preds, test_actuals)]) train_acc = np.mean([x == y for x, y in zip(train_preds, train_actuals)]) print('On predicting the category of low birthweight from regression output (<2500g):') print('Test Accuracy: {}'.format(test_acc)) print('Train Accuracy: {}'.format(train_acc)) -
上一步应该会得到如下输出:
Test Accuracy: 0.7631578947368421 Train Accuracy: 0.7880794701986755
正如你所看到的,训练集准确率和测试集准确率都相当不错,且模型没有出现欠拟合或过拟合的情况。
它是如何工作的...
在本步骤中,我们创建了一个回归神经网络,具有三个完全连接的隐藏层,用于预测低出生体重数据集的出生体重。在下一个步骤中,我们将尝试通过将逻辑回归模型转变为多层神经网络来改进它。
改进线性模型的预测
在本步骤中,我们将通过提高低出生体重预测的准确性来改进我们的逻辑回归模型。我们将使用神经网络。
准备就绪
对于本步骤,我们将加载低出生体重数据,并使用具有两个隐藏层的神经网络,这些隐藏层采用 sigmoid 激活函数来拟合低出生体重的概率。
怎么做...
我们按以下步骤继续进行:
-
我们首先加载库并初始化我们的计算图,如下所示:
import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import requests import os.path import csv -
接下来,我们加载、提取并规范化数据,方法与前面的步骤相同,不同的是这里我们将使用低出生体重指标变量作为目标,而不是实际出生体重,如下所示:
# Name of data file birth_weight_file = 'birth_weight.csv' birthdata_url = 'https://github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook/blob/master/ch6/06_Using_Multiple_Layers/birth_weight.csv' # Download data and create data file if file does not exist in current directory if not os.path.exists(birth_weight_file): birth_file = requests.get(birthdata_url) birth_data = birth_file.text.split('\r\n') birth_header = birth_data[0].split('\t') birth_data = [[float(x) for x in y.split('\t') if len(x) >= 1] for y in birth_data[1:] if len(y) >= 1] with open(birth_weight_file, "w") as f: writer = csv.writer(f) writer.writerows([birth_header]) writer.writerows(birth_data) # read birth weight data into memory birth_data = [] with open(birth_weight_file, newline='') as csvfile: csv_reader = csv.reader(csvfile) birth_header = next(csv_reader) for row in csv_reader: birth_data.append(row) birth_data = [[float(x) for x in row] for row in birth_data] # Pull out target variable y_vals = np.array([x[0] for x in birth_data]) # Pull out predictor variables (not id, not target, and not birthweight) x_vals = np.array([x[1:8] for x in birth_data]) train_indices = np.random.choice(len(x_vals), round(len(x_vals)*0.8), replace=False) test_indices = np.array(list(set(range(len(x_vals))) - set(train_indices))) x_vals_train = x_vals[train_indices] x_vals_test = x_vals[test_indices] y_vals_train = y_vals[train_indices] y_vals_test = y_vals[test_indices] def normalize_cols(m, col_min=np.array([None]), col_max=np.array([None])): if not col_min[0]: col_min = m.min(axis=0) if not col_max[0]: col_max = m.max(axis=0) return (m - col_min) / (col_max - col_min), col_min, col_max x_vals_train, train_min, train_max = np.nan_to_num(normalize_cols(x_vals_train)) x_vals_test, _, _ = np.nan_to_num(normalize_cols(x_vals_test, train_min, train_max)) -
接下来,我们需要声明批量大小、种子(以确保结果可重复)以及输入数据层,如下所示:
batch_size = 90 seed = 98 np.random.seed(seed) tf.random.set_seed(seed) x_data = tf.keras.Input(dtype=tf.float64, shape=(7,)) -
如前所述,我们现在需要声明初始化变量和模型中各层的函数。为了创建一个更好的逻辑函数,我们需要创建一个函数,它返回输入层上的逻辑层。换句话说,我们将使用一个完全连接的层,并为每一层返回一个 sigmoid 元素。需要记住的是,我们的损失函数将包含最终的 sigmoid,因此我们希望在最后一层中指定不返回输出的 sigmoid,如下所示:
# Create variable definition def init_variable(shape): return(tf.Variable(tf.random.normal(shape=shape, dtype="float64", seed=seed))) # Create a logistic layer definition def logistic(input_layer, multiplication_weight, bias_weight, activation = True): # We separate the activation at the end because the loss function will # implement the last sigmoid necessary if activation: return tf.keras.layers.Lambda(lambda x: tf.nn.sigmoid(tf.add(tf.matmul(x, multiplication_weight), bias_weight)))(input_layer) else: return tf.keras.layers.Lambda(lambda x: tf.add(tf.matmul(x, multiplication_weight), bias_weight))(input_layer) -
现在我们将声明三层(两层隐藏层和一层输出层)。我们将从为每一层初始化权重和偏置矩阵开始,并定义各层操作,如下所示:
# First logistic layer (7 inputs to 14 hidden nodes) A1 = init_variable(shape=[7,14]) b1 = init_variable(shape=[14]) logistic_layer1 = logistic(x_data, A1, b1) # Second logistic layer (14 hidden inputs to 5 hidden nodes) A2 = init_variable(shape=[14,5]) b2 = init_variable(shape=[5]) logistic_layer2 = logistic(logistic_layer1, A2, b2) # Final output layer (5 hidden nodes to 1 output) A3 = init_variable(shape=[5,1]) b3 = init_variable(shape=[1]) final_output = logistic(logistic_layer2, A3, b3, activation=False) # Build the model model = tf.keras.Model(inputs=x_data, outputs=final_output, name="improving_linear_reg_neural_network") -
接下来,我们定义损失函数(交叉熵)并声明优化算法,如下所示:
# Loss function (Cross Entropy loss) def cross_entropy(final_output, y_target): return tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=final_output, labels=y_target)) # Declare optimizer optimizer = tf.keras.optimizers.Adam(0.002)交叉熵是一种衡量概率之间距离的方法。在这里,我们希望衡量确定性(0 或 1)与我们模型的概率(0 < x < 1)之间的差异。TensorFlow 通过内置的 sigmoid 函数实现交叉熵。这一点也很重要,因为它是超参数调优的一部分,我们更有可能找到适合当前问题的最佳损失函数、学习率和优化算法。为了简洁起见,本食谱中不包括超参数调优。
-
为了评估和比较我们的模型与之前的模型,我们需要在图中创建一个预测和准确性操作。这将允许我们输入整个测试集并确定准确性,如下所示:
# Accuracy def compute_accuracy(final_output, y_target): prediction = tf.round(tf.nn.sigmoid(final_output)) predictions_correct = tf.cast(tf.equal(prediction, y_target), tf.float32) return tf.reduce_mean(predictions_correct) -
我们现在准备开始我们的训练循环。我们将训练 1,500 代,并保存模型损失以及训练和测试集的准确性,以便稍后绘制图表。我们的训练循环通过以下代码启动:
# Training loop loss_vec = [] train_acc = [] test_acc = [] for i in range(1500): rand_index = np.random.choice(len(x_vals_train), size=batch_size) rand_x = x_vals_train[rand_index] rand_y = np.transpose([y_vals_train[rand_index]]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output = model(rand_x) # Apply loss function (Cross Entropy loss) loss = cross_entropy(output, rand_y) loss_vec.append(loss) # Get gradients of loss with reference to the weights and bias variables to adjust. gradients_A1 = tape.gradient(loss, A1) gradients_b1 = tape.gradient(loss, b1) gradients_A2 = tape.gradient(loss, A2) gradients_b2 = tape.gradient(loss, b2) gradients_A3 = tape.gradient(loss, A3) gradients_b3 = tape.gradient(loss, b3) # Update the weights and bias variables of the model. optimizer.apply_gradients(zip([gradients_A1, gradients_b1,gradients_A2, gradients_b2, gradients_A3, gradients_b3], [A1, b1, A2, b2, A3, b3])) temp_acc_train = compute_accuracy(model(x_vals_train), np.transpose([y_vals_train])) train_acc.append(temp_acc_train) temp_acc_test = compute_accuracy(model(x_vals_test), np.transpose([y_vals_test])) test_acc.append(temp_acc_test) if (i+1)%150==0: print('Loss = ' + str(loss.numpy())) -
前面的步骤应产生以下输出:
Loss = 0.5885411040188063 Loss = 0.581099555117532 Loss = 0.6071769535895101 Loss = 0.5043174136225906 Loss = 0.5023625777095964 Loss = 0.485112570717733 Loss = 0.5906992621835641 Loss = 0.4280814147901789 Loss = 0.5425164697605331 Loss = 0.35608561907724867 -
以下代码块展示了如何使用
matplotlib绘制交叉熵损失以及训练集和测试集的准确性:# Plot loss over time plt.plot(loss_vec, 'k-') plt.title('Cross Entropy Loss per Generation') plt.xlabel('Generation') plt.ylabel('Cross Entropy Loss') plt.show() # Plot train and test accuracy plt.plot(train_acc, 'k-', label='Train Set Accuracy') plt.plot(test_acc, 'r--', label='Test Set Accuracy') plt.title('Train and Test Accuracy') plt.xlabel('Generation') plt.ylabel('Accuracy') plt.legend(loc='lower right') plt.show()我们获得了每代的交叉熵损失图,如下所示:
![/var/folders/6z/j6f33tds3v7dv8tryfqnxf5m0000gn/T/com.microsoft.Word/Content.MSO/E3D23CF0.tmp]()
图 6.6:1,500 次迭代的训练损失
在大约 150 代之后,我们已经达到了一个不错的模型。随着训练的继续,我们可以看到在剩余的迭代中几乎没有什么提升,如下图所示:

图 6.7:训练集和测试集的准确性
如你在前面的图中所见,我们很快得到了一个不错的模型。
它是如何工作的...
在考虑使用神经网络建模数据时,必须权衡其优缺点。虽然我们的模型比之前的模型收敛得更快,也许准确率更高,但这也有代价;我们训练了更多的模型变量,过拟合的风险也更大。要检查是否发生了过拟合,我们需要查看训练集和测试集的准确率。如果训练集的准确率持续增加,而测试集的准确率保持不变或稍有下降,我们可以假设发生了过拟合。
为了应对欠拟合,我们可以增加模型的深度或训练更多的迭代次数。为了应对过拟合,我们可以增加数据量或在模型中加入正则化技术。
还需要注意的是,我们的模型变量不像线性模型那样具有可解释性。神经网络模型的系数比线性模型更难解释,因为它们用来解释模型中各特征的重要性。
学习玩井字棋
为了展示神经网络的适应性,我们现在尝试使用神经网络来学习井字棋的最优走法。在这一过程中,我们已知井字棋是一个确定性游戏,且最优走法已被确定。
准备开始
为了训练我们的模型,我们将使用一组棋盘位置,并在其后附上多个不同棋盘的最优回应。通过仅考虑在对称性方面不同的棋盘位置,我们可以减少训练的棋盘数量。井字棋棋盘的非恒等变换包括 90 度、180 度和 270 度的旋转(无论方向)、水平反射和垂直反射。基于这一思想,我们将使用一份包含最优走法的棋盘简短清单,应用两个随机变换,然后将其输入到神经网络进行学习。
由于井字棋是一个确定性游戏,值得注意的是,先手的一方应该要么获胜,要么与对方平局。我们希望模型能够对我们的走法做出最优回应,并最终导致平局。
如果我们使用 1 表示 X,-1 表示 O,0 表示空格,那么下图展示了如何将棋盘位置和最优走法视为数据行:

图 6.8:这里,我们展示了如何将棋盘和最优走法视为数据行。请注意,X = 1,O = -1,空格为 0,并且我们从 0 开始索引。
除了模型损失外,为了检查模型的表现,我们将执行两项操作。我们首先要做的检查是从训练集中移除一个位置和最优走法的数据行。这将帮助我们判断神经网络模型是否能够推广到它未见过的走法。评估模型的第二种方法是最终与它进行一局对弈。
可以在 GitHub 的目录中找到此食谱的所有可能棋盘和最优走法列表,网址为github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook/tree/master/ch6/08_Learning_Tic_Tac_Toe,或者在 Packt 的仓库中找到github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook。
如何做到这一点...
我们按照以下步骤继续进行:
-
我们需要首先加载此脚本所需的库,代码如下:
import tensorflow as tf import matplotlib.pyplot as plt import csv import numpy as np import random -
接下来,我们声明用于训练模型的批次大小,代码如下:
batch_size = 50 -
为了使得可视化棋盘变得更加简单,我们将创建一个函数,输出带有 X 和 O 的井字棋棋盘。可以通过以下代码实现:
def print_board(board): symbols = ['O', ' ', 'X'] board_plus1 = [int(x) + 1 for x in board] board_line1 = ' {} | {} | {}'.format(symbols[board_plus1[0]], symbols[board_plus1[1]], symbols[board_plus1[2]]) board_line2 = ' {} | {} | {}'.format(symbols[board_plus1[3]], symbols[board_plus1[4]], symbols[board_plus1[5]]) board_line3 = ' {} | {} | {}'.format(symbols[board_plus1[6]], symbols[board_plus1[7]], symbols[board_plus1[8]]) print(board_line1) print('___________') print(board_line2) print('___________') print(board_line3) -
现在,我们需要创建一个函数,返回一个新棋盘和一个在变换下的最优响应位置。可以通过以下代码实现:
def get_symmetry(board, response, transformation): ''' :param board: list of integers 9 long: opposing mark = -1 friendly mark = 1 empty space = 0 :param transformation: one of five transformations on a board: rotate180, rotate90, rotate270, flip_v, flip_h :return: tuple: (new_board, new_response) ''' if transformation == 'rotate180': new_response = 8 - response return board[::-1], new_response elif transformation == 'rotate90': new_response = [6, 3, 0, 7, 4, 1, 8, 5, 2].index(response) tuple_board = list(zip(*[board[6:9], board[3:6], board[0:3]])) return [value for item in tuple_board for value in item], new_response elif transformation == 'rotate270': new_response = [2, 5, 8, 1, 4, 7, 0, 3, 6].index(response) tuple_board = list(zip(*[board[0:3], board[3:6], board[6:9]]))[::-1] return [value for item in tuple_board for value in item], new_response elif transformation == 'flip_v': new_response = [6, 7, 8, 3, 4, 5, 0, 1, 2].index(response) return board[6:9] + board[3:6] + board[0:3], new_response elif transformation == 'flip_h': # flip_h = rotate180, then flip_v new_response = [2, 1, 0, 5, 4, 3, 8, 7, 6].index(response) new_board = board[::-1] return new_board[6:9] + new_board[3:6] + new_board[0:3], new_response else: raise ValueError('Method not implmented.') -
棋盘列表及其最优响应存储在一个
.csv文件中,该文件位于 GitHub 仓库中的目录,网址为github.com/nfmcclure/tensorflow_cookbook或 Packt 仓库中的github.com/PacktPublishing/TensorFlow-Machine-Learning-Cookbook-Second-Edition。我们将创建一个函数,加载包含棋盘和响应的文件,并将其存储为一个元组列表,代码如下:def get_moves_from_csv(csv_file): ''' :param csv_file: csv file location containing the boards w/ responses :return: moves: list of moves with index of best response ''' moves = [] with open(csv_file, 'rt') as csvfile: reader = csv.reader(csvfile, delimiter=',') for row in reader: moves.append(([int(x) for x in row[0:9]],int(row[9]))) return moves -
现在,我们需要将所有部分结合起来,创建一个函数,返回一个随机变换的棋盘和响应。可以通过以下代码实现:
def get_rand_move(moves, rand_transforms=2): # This function performs random transformations on a board. (board, response) = random.choice(moves) possible_transforms = ['rotate90', 'rotate180', 'rotate270', 'flip_v', 'flip_h'] for i in range(rand_transforms): random_transform = random.choice(possible_transforms) (board, response) = get_symmetry(board, response, random_transform) return board, response -
接下来,我们加载数据并创建一个训练集,代码如下:
moves = get_moves_from_csv('base_tic_tac_toe_moves.csv') # Create a train set: train_length = 500 train_set = [] for t in range(train_length): train_set.append(get_rand_move(moves)) -
请记住,我们要从训练集中删除一个棋盘和最优响应,看看模型是否能够泛化并做出最佳决策。以下棋盘的最佳走法是将棋子放在索引位置 6:
test_board = [-1, 0, 0, 1, -1, -1, 0, 0, 1] train_set = [x for x in train_set if x[0] != test_board] -
现在我们可以初始化权重和偏置,并创建我们的模型:
def init_weights(shape): return tf.Variable(tf.random_normal(shape)) A1 = init_weights([9, 81]) bias1 = init_weights([81]) A2 = init_weights([81, 9]) bias2 = init_weights([9]) -
现在,我们创建我们的模型。请注意,我们在以下模型中没有包含
softmax()激活函数,因为它已包含在损失函数中:# Initialize input data X = tf.keras.Input(dtype=tf.float32, batch_input_shape=[None, 9]) hidden_output = tf.keras.layers.Lambda(lambda x: tf.nn.sigmoid(tf.add(tf.matmul(x, A1), bias1)))(X) final_output = tf.keras.layers.Lambda(lambda x: tf.add(tf.matmul(x, A2), bias2))(hidden_output) model = tf.keras.Model(inputs=X, outputs=final_output, name="tic_tac_toe_neural_network") -
接下来,我们将声明优化器,代码如下:
optimizer = tf.keras.optimizers.SGD(0.025) -
现在,我们可以使用以下代码进行神经网络的训练循环。请注意,我们的
loss函数将是最终输出对数(未经标准化)的平均 softmax:# Initialize variables loss_vec = [] for i in range(10000): rand_indices = np.random.choice(range(len(train_set)), batch_size, replace=False) batch_data = [train_set[i] for i in rand_indices] x_input = [x[0] for x in batch_data] y_target = np.array([y[1] for y in batch_data]) # Open a GradientTape. with tf.GradientTape(persistent=True) as tape: # Forward pass. output = model(np.array(x_input, dtype=float)) # Apply loss function (Cross Entropy loss) loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=output, labels=y_target)) loss_vec.append(loss) # Get gradients of loss with reference to the weights and bias variables to adjust. gradients_A1 = tape.gradient(loss, A1) gradients_b1 = tape.gradient(loss, bias1) gradients_A2 = tape.gradient(loss, A2) gradients_b2 = tape.gradient(loss, bias2) # Update the weights and bias variables of the model. optimizer.apply_gradients(zip([gradients_A1, gradients_b1, gradients_A2, gradients_b2], [A1, bias1, A2, bias2])) if i % 500 == 0: print('Iteration: {}, Loss: {}'.format(i, loss)) -
以下是绘制模型训练过程中损失的代码:
plt.plot(loss_vec, 'k-', label='Loss') plt.title('Loss (MSE) per Generation') plt.xlabel('Generation') plt.ylabel('Loss') plt.show()我们应该得到以下每代的损失曲线:
![/var/folders/6z/j6f33tds3v7dv8tryfqnxf5m0000gn/T/com.microsoft.Word/Content.MSO/2B562542.tmp]()
图 6.9:井字棋训练集在 10,000 次迭代后的损失
在前面的图表中,我们绘制了训练步骤中的损失曲线。
-
为了测试模型,我们需要查看它在从训练集移除的测试板上的表现。我们希望模型能够泛化并预测移动的最优索引,即索引编号 6。大多数时候,模型会成功,如下所示:
test_boards = [test_board] logits = model.predict(test_boards) predictions = tf.argmax(logits, 1) print(predictions) -
前面的步骤应该会得到以下输出:
[6] -
为了评估我们的模型,我们需要与训练好的模型对战。为此,我们必须创建一个函数来检查是否获胜。这样,程序就能知道何时停止请求更多的移动。可以通过以下代码实现:
def check(board): wins = [[0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6]] for i in range(len(wins)): if board[wins[i][0]]==board[wins[i][1]]==board[wins[i][2]]==1.: return 1 elif board[wins[i][0]]==board[wins[i][1]]==board[wins[i][2]]==-1.: return 1 return 0 -
现在我们可以循环并与模型进行游戏。我们从一个空白的棋盘开始(全为零),然后让用户输入一个索引(0-8)表示要下的位置,然后将其输入模型进行预测。对于模型的移动,我们选择最大的可用预测,并且该位置必须是空的。从这场游戏中,我们可以看到我们的模型并不完美,如下所示:
game_tracker = [0., 0., 0., 0., 0., 0., 0., 0., 0.] win_logical = False num_moves = 0 while not win_logical: player_index = input('Input index of your move (0-8): ') num_moves += 1 # Add player move to game game_tracker[int(player_index)] = 1. # Get model's move by first getting all the logits for each index [potential_moves] = model(np.array([game_tracker], dtype=float)) # Now find allowed moves (where game tracker values = 0.0) allowed_moves = [ix for ix, x in enumerate(game_tracker) if x == 0.0] # Find best move by taking argmax of logits if they are in allowed moves model_move = np.argmax([x if ix in allowed_moves else -999.0 for ix, x in enumerate(potential_moves)]) # Add model move to game game_tracker[int(model_move)] = -1. print('Model has moved') print_board(game_tracker) # Now check for win or too many moves if check(game_tracker) == -1 or num_moves >= 5: print('Game Over!') win_logical = True elif check(game_tracker) == 1: print('Congratulations, You won!') win_logical = True -
前面的步骤应该会得到以下交互式输出:
Input index of your move (0-8): 4 Model has moved | | ___________ | X | ___________ | | O Input index of your move (0-8): 6 Model has moved O | | ___________ | X | ___________ X | | O Input index of your move (0-8): 2 Model has moved O | | X ___________ | X | ___________ X | O | O Congratulations, You won!
如你所见,人类玩家很快且轻松地战胜了机器。
它是如何工作的...
在这一部分,我们训练了一个神经网络,通过输入棋盘位置和一个九维向量来玩井字游戏,并预测最优反应。我们只需要输入几个可能的井字游戏棋盘,并对每个棋盘应用随机变换来增加训练集的大小。
为了测试我们的算法,我们移除了一个特定棋盘的所有实例,并查看我们的模型是否能够泛化并预测最优反应。最后,我们与模型进行了一场样本游戏。这个模型还不完美。我们可以通过使用更多数据或应用更复杂的神经网络架构来改进它。但更好的做法是改变学习的类型:与其使用监督学习,我们更应该使用基于强化学习的方法。
第七章:使用表格数据进行预测
目前可以轻松找到的大多数数据并不是由图像或文本文件组成,而是由关系型表格构成,每个表格可能包含数字、日期和简短文本,这些数据可以结合在一起。这是因为基于关系模型(可以通过某些列的值作为连接键将数据表结合在一起)的数据库应用得到了广泛采用。如今,这些表格是表格数据的主要来源,因此也带来了一些挑战。
下面是应用深度神经网络(DNNs)于表格数据时常见的挑战:
-
混合特征数据类型
-
数据以稀疏格式呈现(零值数据多于非零值数据),这对于 DNN 找到最优解并不是最理想的情况。
-
目前尚未出现最先进的架构,只有一些不同的最佳实践。
-
针对单一问题可用的数据比常见的图像识别问题要少。
-
非技术人员会产生怀疑,因为与更简单的机器学习算法相比,DNN 在表格数据上的可解释性较差。
-
通常情况下,DNN 并不是表格数据的最佳解决方案,因为梯度提升方法(如 LightGBM、XGBoost 和 CatBoost)可能表现得更好。
即使这些挑战看起来相当困难,也请不要气馁。将深度神经网络(DNN)应用于表格数据时的挑战确实是严峻的,但另一方面,机会也同样巨大。斯坦福大学兼职教授、深度学习专家安德鲁·吴最近表示:“深度学习在用户众多的消费互联网公司中得到了极大的应用,因而产生了大量数据,但要突破到其他行业,那里的数据集较小,我们现在需要针对小数据的更好技术。”
在本章中,我们将向你介绍一些处理小型表格数据的最佳方法,使用的是 TensorFlow。在此过程中,我们将使用 TensorFlow、Keras,以及两个专门的机器学习包:pandas(pandas.pydata.org/)和 scikit-learn(scikit-learn.org/stable/index.html)。在前几章中,我们经常使用 TensorFlow Datasets(www.tensorflow.org/datasets)和专门用于特征列的层(www.tensorflow.org/api_docs/python/tf/feature_column)。我们本可以在这一章中重复使用它们,但那样我们就会错过一些只有 scikit-learn 才能提供的有趣转换,而且交叉验证也会变得困难。
此外,考虑到使用 scikit-learn 如果你在比较不同算法在某个问题上的表现,并且需要标准化一个数据准备管道,不仅适用于 TensorFlow 模型,还适用于其他更经典的机器学习和统计模型。
为了安装 pandas 和 scikit-learn(如果你使用的是 Anaconda,它们应该已经安装在你的系统中),请按照以下指南操作:
-
对于 pandas:
pandas.pydata.org/docs/getting_started/install.html -
对于 scikit-learn:
scikit-learn.org/stable/install.html
本章我们将讨论一系列专注于从表格数据中学习的方法,这些数据以表格形式组织,行表示观察结果,列表示每个特征的观察值。
表格数据是大多数机器学习算法的常见输入数据,但对于 DNN 来说并不常见,因为 DNN 在处理其他类型的数据(如图像和文本)时表现更好。
针对表格数据的深度学习方法需要解决一些问题,比如数据异质性,这些问题并非主流,并且需要使用许多常见的机器学习策略,比如交叉验证,而这些策略在 TensorFlow 中目前尚未实现。
本章结束时,你应当掌握以下内容:
-
处理数值数据
-
处理日期数据
-
处理类别数据
-
处理有序数据
-
处理高卡特性类别数据
-
完成所有处理步骤
-
设置数据生成器
-
为表格数据创建自定义激活函数
-
对难题进行测试运行
我们立即开始学习如何处理数值数据。你会惊讶于这些方法在许多表格数据问题中的有效性。
处理数值数据
我们将从准备数值数据开始。你有数值数据时,数据是:
-
你的数据是浮动数字表示的
-
你的数据是整数,并且有一定数量的唯一值(否则,如果只有少数值按顺序排列,你正在处理的是有序变量,如排名)。
-
你的整数数据并不代表一个类别或标签(否则你正在处理一个类别变量)。
在处理数值数据时,有几种情况可能会影响 DNN 处理数据时的性能:
-
缺失数据(NULL 或 NaN 值,甚至 INF 值)会导致 DNN 完全无法工作。
-
常数值会导致计算变慢,并干扰每个神经元已经提供的偏差。
-
偏斜分布
-
非标准化数据,尤其是带有极端值的数据
在将数值数据输入神经网络之前,你必须确保所有这些问题已经得到妥善处理,否则你可能会遇到错误或学习过程无法正常进行。
准备工作
为了解决所有潜在问题,我们将主要使用来自 scikit-learn 的专门函数。在开始我们的配方之前,我们将把它们导入到环境中:
import numpy as np
import pandas as pd
try:
from sklearn.impute import IterativeImputer
except:
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import ExtraTreesRegressor
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, QuantileTransformer
from sklearn.feature_selection import VarianceThreshold
from sklearn.pipeline import Pipeline
为了测试我们的配方,我们将使用一个简单的 3x4 表格,其中一些列包含 NaN 值,还有一些常数列不包含 NaN 值:
example = pd.DataFrame([[1, 2, 3, np.nan], [1, 3, np.nan, 4], [1, 2, 2, 2]], columns = ['a', 'b', 'c', 'd'])
如何操作…
我们的配方将基于我们相对于以下内容的指示构建一个 scikit-learn 管道:
-
一个特征被保留的最小可接受方差,否则你可能会将不需要的常量引入网络,进而阻碍学习过程(
variance_threshold参数) -
用作填充缺失值的基准策略是什么(
imputer参数,默认设置为用特征的均值替代缺失值),以便完成输入矩阵,使矩阵乘法成为可能(这是神经网络中的基本计算) -
我们是否应该使用基于所有数值数据缺失值的更复杂的填充策略(
multivariate_imputer参数),因为有时候数据点并非随机缺失,其他变量可能提供你所需的信息以进行正确的估计 -
是否添加一个二进制特征,标示每个特征的缺失值位置,这是一个好的策略,因为你通常可以通过缺失模式找到有用的信息(
add_indicator参数) -
是否对变量的分布进行转换,以强制它们接近对称分布(
quantile_transformer参数,默认为normal),因为网络将从对称的数据分布中学习得更好 -
我们是否应该基于统计归一化来重新缩放输出,即在去除均值后除以标准差(
scaler参数,默认为True)
现在,考虑到这些,让我们按照以下方式构建我们的管道:
def assemble_numeric_pipeline(variance_threshold=0.0,
imputer='mean',
multivariate_imputer=False,
add_indicator=True,
quantile_transformer='normal',
scaler=True):
numeric_pipeline = []
if variance_threshold is not None:
if isinstance(variance_threshold, float):
numeric_pipeline.append(('var_filter',
VarianceThreshold(threshold=variance_threshold)))
else:
numeric_pipeline.append(('var_filter',
VarianceThreshold()))
if imputer is not None:
if multivariate_imputer is True:
numeric_pipeline.append(('imputer',
IterativeImputer(estimator=ExtraTreesRegressor(n_estimators=100, n_jobs=-2),
initial_strategy=imputer,
add_indicator=add_indicator)))
else:
numeric_pipeline.append(('imputer',
SimpleImputer(strategy=imputer,
add_indicator=add_indicator)
)
)
if quantile_transformer is not None:
numeric_pipeline.append(('transformer',
QuantileTransformer(n_quantiles=100,
output_distribution=quantile_transformer,
random_state=42)
)
)
if scaler is not None:
numeric_pipeline.append(('scaler',
StandardScaler()
)
)
return Pipeline(steps=numeric_pipeline)
我们现在可以通过指定我们的转换偏好来创建我们的数值管道:
numeric_pipeline = assemble_numeric_pipeline(variance_threshold=0.0,
imputer='mean',
multivariate_imputer=False,
add_indicator=True,
quantile_transformer='normal',
scaler=True)
我们可以立即在示例上尝试我们新的函数,首先应用fit方法,然后应用transform方法:
numeric_pipeline.fit(example)
np.round(numeric_pipeline.transform(example), 3)
这是生成的输出 NumPy 数组:
array([[-0.707, 1.225, -0\. , -0.707, 1.414],
[ 1.414, -0\. , 1.225, 1.414, -0.707],
[-0.707, -1.225, -1.225, -0.707, -0.707]])
如你所见,所有原始数据已经完全转换,所有缺失值都已被替换。
它是如何工作的…
如前所述,我们使用 scikit-learn 以便与其他机器学习解决方案进行比较,并且因为在构建此配方时涉及到一些独特的 scikit-learn 函数:
-
VarianceThreshold(scikit-learn.org/stable/modules/generated/sklearn.feature_selection.VarianceThreshold.html) -
IterativeImputer(scikit-learn.org/stable/modules/generated/sklearn.impute.IterativeImputer.html) -
SimpleImputer(scikit-learn.org/stable/modules/generated/sklearn.impute.SimpleImputer.html) -
QuantileTransformer(scikit-learn.org/stable/modules/generated/sklearn.preprocessing.QuantileTransformer.html) -
StandardScaler(scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html) -
Pipeline(scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html)
对于每个函数,您会找到一个指向 scikit-learn 文档的链接,提供关于该函数如何工作的详细信息。解释为什么 scikit-learn 方法对于这个配方(以及本章中您将找到的其他配方)如此重要至关重要。
处理图像或文本时,通常不需要为训练数据和测试数据定义特定的处理过程。这是因为您对两者应用的是确定性的转换。例如,在图像处理中,您只需将像素值除以 255 来进行标准化。
然而,对于表格数据,您需要更复杂的转换,并且这些转换完全不是确定性的,因为它们涉及学习和记住特定的参数。例如,当使用均值填充一个特征的缺失值时,您首先需要从训练数据中计算均值。然后,您必须对任何新的数据应用相同的填充值(不能重新计算新数据的均值,因为这些新数据可能来自稍有不同的分布,可能与您的 DNN 所学的值不匹配)。
所有这些都涉及跟踪从训练数据中学习到的许多参数。scikit-learn 可以帮助您,因为当您使用 fit 方法时,它会学习并存储从训练数据中推导出的所有参数。使用 transform 方法,您将使用通过 fit 学到的参数对任何新数据(或相同的训练数据)应用转换。
还有更多…
scikit-learn 函数通常返回一个 NumPy 数组。如果没有进行进一步的特征创建,使用输入列标记返回的数组并没有问题。不幸的是,由于我们创建的转换管道,这种情况并不成立:
-
方差阈值将移除无用的特征
-
缺失值填充将创建缺失的二进制指示器
我们实际上可以通过检查拟合的管道,找出哪些列已经被移除,哪些内容已经从原始数据中添加。可以创建一个函数来自动执行这一操作:
def derive_numeric_columns(df, pipeline):
columns = df.columns
if 'var_filter' in pipeline.named_steps:
threshold = pipeline.named_steps.var_filter.threshold
columns = columns[pipeline.named_steps.var_filter.variances_>threshold]
if 'imputer' in pipeline.named_steps:
missing_cols = pipeline.named_steps.imputer.indicator_.features_
if len(missing_cols) > 0:
columns = columns.append(columns[missing_cols] + '_missing')
return columns
当我们在示例中尝试时:
derive_numeric_columns(example, numeric_pipeline)
我们获得一个包含剩余列和二进制指示符的 pandas 索引(由原始特征的名称和 _missing 后缀表示):
Index(['b', 'c', 'd', 'c_missing', 'd_missing'], dtype='object')
跟踪你在转换列时的操作,可以帮助你在需要调试转换后的数据时,以及在使用像 shap (github.com/slundberg/shap) 或 lime (github.com/marcotcr/lime) 等工具解释 DNN 工作原理时提供帮助。
这个配方应该能满足你关于数值数据的所有需求。现在,让我们继续探索日期和时间。
处理日期
日期在数据库中非常常见,特别是在处理未来估算的预测(如销售预测)时,它们显得不可或缺。神经网络无法直接处理日期,因为它们通常以字符串形式表示。因此,你必须通过分离日期的数值元素来转换它们,一旦你将日期拆分成它的组成部分,你就得到了一些数字,任何神经网络都能轻松处理这些数字。然而,某些时间元素是周期性的(例如天、月、小时、星期几),低数字和高数字实际上是相邻的。因此,你需要使用正弦和余弦函数,这将使这些周期性数字以一种 DNN 可以理解和正确解释的格式呈现。
准备工作
由于我们需要编写一个使用 fit/transform 操作的类,这是 scikit-learn 中典型的操作方式,我们从 scikit-learn 导入 BaseEstimator 和 TransformerMixin 类进行继承。这个继承将帮助我们使我们的代码与 scikit-learn 的所有其他函数完美兼容:
from sklearn.base import BaseEstimator, TransformerMixin
为了测试,我们还准备了一个包含日期的字符串形式的示例数据集,采用日/月/年格式:
example = pd.DataFrame({'date_1': ['04/12/2018', '05/12/2019',
'07/12/2020'],
'date_2': ['12/5/2018', '15/5/2015',
'18/5/2016'],
'date_3': ['25/8/2019', '28/8/2018',
'29/8/2017']})
提供的示例非常简短和简单,但它应该能说明我们在处理时的所有相关要点。
如何操作……
这一次我们将设计我们自己的类 DateProcessor。实例化该类后,它可以选择一个 pandas DataFrame,并将每个日期筛选并处理成一个新的 DataFrame,供 DNN 处理。
这个过程一次处理一个日期,提取日期、星期几、月份和年份(另外,还包括小时和分钟),并使用正弦和余弦变换对所有周期性时间进行转换:
class DateProcessor(BaseEstimator, TransformerMixin):
def __init__(self, date_format='%d/%m/%Y', hours_secs=False):
self.format = date_format
self.columns = None
self.time_transformations = [
('day_sin', lambda x: np.sin(2*np.pi*x.dt.day/31)),
('day_cos', lambda x: np.cos(2*np.pi*x.dt.day/31)),
('dayofweek_sin',
lambda x: np.sin(2*np.pi*x.dt.dayofweek/6)),
('dayofweek_cos',
lambda x: np.cos(2*np.pi*x.dt.dayofweek/6)),
('month_sin',
lambda x: np.sin(2*np.pi*x.dt.month/12)),
('month_cos',
lambda x: np.cos(2*np.pi*x.dt.month/12)),
('year',
lambda x: (x.dt.year - x.dt.year.min() ) / (x.dt.year.max() - x.dt.year.min()))
]
if hours_secs:
self.time_transformations = [
('hour_sin',
lambda x: np.sin(2*np.pi*x.dt.hour/23)),
('hour_cos',
lambda x: np.cos(2*np.pi*x.dt.hour/23)),
('minute_sin',
lambda x: np.sin(2*np.pi*x.dt.minute/59)),
('minute_cos',
lambda x: np.cos(2*np.pi*x.dt.minute/59))
] + self.time_transformations
def fit(self, X, y=None, **fit_params):
self.columns = self.transform(X.iloc[0:1,:]).columns
return self
def transform(self, X, y=None, **fit_params):
transformed = list()
for col in X.columns:
time_column = pd.to_datetime(X[col],
format=self.format)
for label, func in self.time_transformations:
transformed.append(func(time_column))
transformed[-1].name += '_' + label
transformed = pd.concat(transformed, axis=1)
return transformed
def fit_transform(self, X, y=None, **fit_params):
self.fit(X, y, **fit_params)
return self.transform(X)
现在我们已经将配方编写成 DateProcessor 类的形式,让我们进一步探索它的内部工作原理。
它是如何工作的……
整个类的关键是通过 pandas to_datetime 函数进行的转换,它将任何表示日期的字符串转换为 datetime64[ns] 类型。
to_datetime之所以有效,是因为你提供了一个模板(format参数),用来将字符串转换为日期。有关如何定义该模板的完整指南,请访问docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior。
当你需要拟合和转换数据时,类会自动将所有日期处理成正确的格式,并进一步使用正弦和余弦函数进行变换:
DateProcessor().fit_transform(example)
一些变换结果是显而易见的,但有些与周期时间相关的变换可能会让人感到困惑。我们花点时间探讨它们如何运作以及为什么会这样。
还有更多……
这个类不会返回时间元素的原始提取结果,如小时、分钟或天数,而是首先通过正弦变换,然后是余弦变换来转换它们。让我们绘制出它如何转换 24 小时,以便更好地理解这个方法:
import matplotlib.pyplot as plt
sin_time = np.array([[t, np.sin(2*np.pi*t/23)] for t in range(0, 24)])
cos_time = np.array([[t, np.cos(2*np.pi*t/23)] for t in range(0, 24)])
plt.plot(sin_time[:,0], sin_time[:,1], label='sin hour')
plt.plot(cos_time[:,0], cos_time[:,1], label='cos hour')
plt.axhline(y=0.0, linestyle='--', color='lightgray')
plt.legend()
plt.show()
这是你将得到的图:

图 7.1:正弦和余弦变换后的小时时间绘图
从图中,我们可以看出一天的开始和结束是如何重合的,从而完成时间周期的闭环。每个变换也会返回相同的值,对于几个不同的小时来说都是如此。这就是为什么我们应该同时使用正弦和余弦的原因;如果你同时使用这两者,每个时间点都会有一对不同的正弦和余弦值,因此你可以精确地检测你在连续时间中的位置。通过将正弦和余弦值绘制成散点图,这一点也可以通过可视化方式进行解释:
ax = plt.subplot()
ax.set_aspect('equal')
ax.set_xlabel('sin hour')
ax.set_ylabel('cos hour')
plt.scatter(sin_time[:,1], cos_time[:,1])
plt.show()
这是结果:

图 7.2:将小时时间的正弦和余弦变换结合到散点图中
就像时钟一样,小时被绘制在一个圆圈中,每个小时是分开的、独立的,但却是完整的周期连续体。
处理分类数据
字符串通常在表格数据中表示分类数据。分类特征中的每个唯一值代表一个质量,指的是我们正在检查的示例(因此,我们认为这些信息是定性的,而数字信息是定量的)。从统计学角度看,每个唯一值被称为水平,而分类特征被称为因子。有时你会看到用于分类的数字代码(标识符),当定性信息之前已被编码为数字时,但处理方式不会改变:信息是数字值,但应该当作分类数据处理。
由于你不知道每个分类特征中每个唯一值与特征中其他值的关系(如果你提前将值分组或排序,实际上是在表达你对数据的假设),你可以将每个唯一值视为一个独立的值。因此,你可以从每个唯一的分类值中推导出创建二进制特征的想法。这个过程被称为独热编码(one-hot encoding),它是最常见的数据处理方法,可以使得分类数据适用于深度神经网络(DNN)和其他机器学习算法。
比如,如果你有一个分类变量,其中包含红色、蓝色和绿色这些唯一值,你可以将它转化为三个独立的二进制变量,每个变量唯一地表示一个值,如下图所示:

然而,这种方法对 DNN 来说存在一个问题。当你的分类变量拥有过多的取值(通常超过 255 个)时,所得到的二进制派生特征不仅数量过多,导致数据集变得庞大,而且还携带很少的信息,因为大部分数值将是零(我们称这种情况为稀疏数据)。稀疏数据对 DNN 有一定问题,因为当数据中有过多的零时,反向传播效果不好,因为信息的缺失会导致信号在网络中传播时无法产生有意义的变化。
因此,我们根据分类变量的唯一值数量区分低基数和高基数的分类变量,并且只对我们认为基数较低的分类变量进行处理(通常如果唯一值少于 255 个,我们认为它是低基数,但你也可以选择一个更低的阈值,比如 64、32,甚至是 24)。
准备工作
我们导入 scikit-learn 的独热编码函数,并准备一个简单的示例数据集,其中包含字符串和数值形式的分类数据:
from sklearn.preprocessing import OneHotEncoder
example = pd.DataFrame([['car', 1234], ['house', 6543],
['tree', 3456]], columns=['object', 'code'])
现在我们可以继续执行方案了。
如何操作…
我们准备一个可以将数字转换为字符串的类,使用它后,每个数值型分类特征将与字符串一样进行处理。然后,我们准备好我们的方案,这个方案是一个 scikit-learn 管道,结合了我们的字符串转换器和独热编码(我们不会忘记通过将缺失值转换为唯一值来自动处理缺失数据)。
class ToString(BaseEstimator, TransformerMixin):
def fit(self, X, y=None, **fit_params):
return self
def transform(self, X, y=None, **fit_params):
return X.astype(str)
def fit_transform(self, X, y=None, **fit_params):
self.fit(X, y, **fit_params)
return self.transform(X)
categorical_pipeline = Pipeline(steps=[
('string_converter', ToString()),
('imputer', SimpleImputer(strategy='constant',
fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore'))])
尽管代码片段很简短,但它实际上实现了很多功能。我们来理解它是如何工作的。
它是如何工作的…
像我们之前看到的其他方法一样,我们只需要拟合并转换我们的示例:
categorical_pipeline.fit_transform(example).todense()
由于返回的数组是稀疏的(即在数据集中零值占主导的特殊格式),我们可以使用 .todense 方法将其转换回我们常用的 NumPy 数组格式。
还有更多内容…
独热编码通过将每个类别的唯一值转换为自己的变量,生成许多新特征。为了给它们打标签,我们必须检查我们使用的 scikit-learn 独热编码实例,并从中提取标签:
def derive_ohe_columns(df, pipeline):
return [str(col) + '_' + str(lvl)
for col, lvls in zip(df.columns,
pipeline.named_steps.onehot.categories_) for lvl in lvls]
例如,在我们的示例中,现在我们可以通过调用以下函数来弄清楚每个新特征所代表的含义:
derive_ohe_columns(example, categorical_pipeline)
结果为我们提供了有关原始特征和由二元变量表示的独特值的指示:
['object_car',
'object_house',
'object_tree',
'code_1234',
'code_3456',
'code_6543']
正如你所看到的,结果同时提供了原始特征和由二元变量表示的独特值的指示。
处理序数数据
序数数据(例如,排名或评论中的星级)无疑更像数字数据,而不是类别数据,但我们必须首先考虑一些差异,然后才能将其直接作为数字来处理。类别数据的问题在于,你可以将其作为数字数据处理,但在标度中一个点与下一个点之间的距离,可能不同于下一个点与再下一个点之间的距离(从技术上讲,步骤可能不同)。这是因为序数数据并不代表数量,而只是表示顺序。另一方面,我们也将它视为类别数据,因为类别是独立的,而这样做会丧失顺序中隐含的信息。处理序数数据的解决方案就是将它视为数字和类别变量的组合。
准备工作
首先,我们需要从 scikit-learn 导入OrdinalEncoder函数,它将帮助我们对序数值进行数字化编码,即使它们是文本形式(例如,序数等级“差、中等、好”):
from sklearn.preprocessing import OrdinalEncoder
然后,我们可以使用包含按序信息并记录为字符串的两个特征来准备我们的示例:
example = pd.DataFrame([['first', 'very much'],
['second', 'very little'],
['third', 'average']],
columns = ['rank', 'importance'])
再次强调,示例只是一个玩具数据集,但它应该能帮助我们测试这个配方所展示的功能。
如何做……
到这个阶段,我们可以准备两个流水线。第一个流水线将处理序数数据,将其转换为有序的数字(该转换将保留原始特征的顺序)。第二个转换将对序数数据进行独热编码(这种转换将保留序数等级之间的步长信息,但不保留它们的顺序)。正如在本章前面“处理日期”配方中所述,对于你要在 DNN 中处理序数数据来说,来自原始数据的仅有两部分信息就足够了:
oe = OrdinalEncoder(categories=[['first', 'second', 'third'],
['very much', 'average', 'very little']])
categorical_pipeline = Pipeline(steps=[
('string_converter', ToString()),
('imputer', SimpleImputer(strategy='constant',
fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore'))])
由于这个配方主要由 scikit-learn 流水线组成,所以它对你来说应该是相当熟悉的。让我们深入了解它,了解更多的工作原理。
它是如何工作的……
你所需要做的就是单独操作这些转换,然后将生成的向量堆叠在一起:
np.hstack((oe.fit_transform(example), categorical_pipeline.fit_transform(example).todense()))
这是我们示例的结果:
matrix([[0., 0., 1., 0., 0., 0., 0., 1.],
[1., 2., 0., 1., 0., 0., 1., 0.],
[2., 1., 0., 0., 1., 1., 0., 0.]])
列可以通过之前看到的derive_ohe_columns函数轻松推导出来:
example.columns.tolist() + derive_ohe_columns(example, categorical_pipeline)
这是包含转换后的列名的列表:
['rank',
'importance',
'rank_first',
'rank_second',
'rank_third',
'importance_average',
'importance_very little',
'importance_very much']
通过将覆盖数值部分的变量与有序变量的唯一值结合起来,我们现在应该能够利用来自数据的所有真实信息。
处理高基数类别数据
在处理高基数类别特征时,我们可以使用前面提到的独热编码策略。然而,我们可能会遇到一些问题,因为生成的矩阵过于稀疏(许多零值),从而阻碍了我们的深度神经网络(DNN)收敛到一个好的解,或者使数据集变得不可处理(因为稀疏矩阵变为密集矩阵后可能占用大量内存)。
最好的解决方案是将它们作为数值标签特征传递给我们的深度神经网络(DNN),并让 Keras 嵌入层来处理它们(www.tensorflow.org/api_docs/python/tf/keras/layers/Embedding)。嵌入层实际上是一个权重矩阵,可以将高基数类别输入转换为低维度的数值输出。它本质上是一个加权线性组合,其权重经过优化,以将类别转换为最佳帮助预测过程的数字。
在幕后,嵌入层将你的类别数据转换为独热编码向量,这些向量成为一个小型神经网络的输入。这个小型神经网络的目的是将输入混合并组合成一个较小的输出层。该层执行的独热编码仅适用于数值标签的类别(不适用于字符串),因此正确转换我们的高基数类别数据是至关重要的。
scikit-learn 包提供了 LabelEncoder 函数作为一种可能的解决方案,但这种方法存在一些问题,因为它无法处理之前未见过的类别,也无法在拟合/转换模式下正常工作。我们的方案需要将其封装并使其适用于为 Keras 嵌入层生成正确的输入和信息。
准备工作
在这个方案中,我们需要重新定义 scikit-learn 中的 LabelEncoder 函数,并使其适用于拟合/转换过程:
from sklearn.preprocessing import LabelEncoder
由于我们需要模拟一个高基数类别变量,我们将使用一个简单脚本创建的随机唯一值(由字母和数字组成)。这将使我们能够测试更多的示例:
import string
import random
def random_id(length=8):
voc = string.ascii_lowercase + string.digits
return ''.join(random.choice(voc) for i in range(length))
example = pd.DataFrame({'high_cat_1': [random_id(length=2)
for i in range(500)],
'high_cat_2': [random_id(length=3)
for i in range(500)],
'high_cat_3': [random_id(length=4)
for i in range(500)]})
这是我们随机示例生成器的输出:

第一列包含一个两字母代码,第二列使用三个字母,最后一列使用四个字母。
如何做到这一点……
在这个方案中,我们将准备另一个 scikit-learn 类。它扩展了现有的 LabelEncoder 函数,因为它能够自动处理缺失值。它记录了原始类别值与其对应的数值之间的映射关系,并且在转换时,它能够处理之前未见过的类别,将它们标记为未知:
class LEncoder(BaseEstimator, TransformerMixin):
def __init__(self):
self.encoders = dict()
self.dictionary_size = list()
self.unk = -1
def fit(self, X, y=None, **fit_params):
for col in range(X.shape[1]):
le = LabelEncoder()
le.fit(X.iloc[:, col].fillna('_nan'))
le_dict = dict(zip(le.classes_,
le.transform(le.classes_)))
if '_nan' not in le_dict:
max_value = max(le_dict.values())
le_dict['_nan'] = max_value
max_value = max(le_dict.values())
le_dict['_unk'] = max_value
self.unk = max_value
self.dictionary_size.append(len(le_dict))
col_name = X.columns[col]
self.encoders[col_name] = le_dict
return self
def transform(self, X, y=None, **fit_params):
output = list()
for col in range(X.shape[1]):
col_name = X.columns[col]
le_dict = self.encoders[col_name]
emb = X.iloc[:, col].fillna('_nan').apply(lambda x:
le_dict.get(x, le_dict['_unk'])).values
output.append(pd.Series(emb,
name=col_name).astype(np.int32))
return output
def fit_transform(self, X, y=None, **fit_params):
self.fit(X, y, **fit_params)
return self.transform(X)
就像我们迄今为止看到的其他类一样,LEncoder有一个拟合方法,能够存储信息以供将来使用,还有一个转换方法,基于之前拟合到训练数据时存储的信息应用转换。
它是如何工作的……
在实例化标签编码器后,我们只需拟合并转换我们的示例,将每个类别特征转化为一系列数字标签:
le = LEncoder()
le.fit_transform(example)
完成所有编码以实现配方后,这个类的执行确实简单明了。
还有更多……
为了让 Keras 嵌入层正常工作,我们需要指定高基数类别变量的输入大小。通过访问我们示例中的le.dictionary_size,我们在示例变量中有412、497和502个不同的值:
le.dictionary_size
在我们的示例中,示例变量分别有412、497和502个不同的值:
[412, 497, 502]
这个数字包括缺失和未知标签,即使在我们拟合的示例中没有缺失或未知元素。
完成所有处理
现在我们已经完成了处理不同类型表格数据的配方,在本配方中,我们将把所有内容封装到一个类中,这个类可以轻松处理所有的 fit/transform 操作,输入为 pandas DataFrame,并明确指定要处理的列以及处理方式。
准备好
由于我们将结合多个转换,我们将利用 scikit-learn 的FeatureUnion函数,这是一个可以轻松地将它们拼接在一起的函数:
from sklearn.pipeline import FeatureUnion
作为测试数据集,我们将简单地合并之前使用过的所有测试数据:
example = pd.concat([
pd.DataFrame([[1, 2, 3, np.nan], [1, 3, np.nan, 4],[1, 2, 2, 2]],
columns = ['a', 'b', 'c', 'd']),
pd.DataFrame({'date_1': ['04/12/2018', '05/12/2019','07/12/2020'],
'date_2': ['12/5/2018', '15/5/2015', '18/5/2016'],
'date_3': ['25/8/2019', '28/8/2018', '29/8/2017']}),
pd.DataFrame([['first', 'very much'], ['second', 'very little'],
['third', 'average']],
columns = ['rank', 'importance']),
pd.DataFrame([['car', 1234], ['house', 6543], ['tree', 3456]],
columns=['object', 'code']),
pd.DataFrame({'high_cat_1': [random_id(length=2)
for i in range(3)],
'high_cat_2': [random_id(length=3)
for i in range(3)],
'high_cat_3': [random_id(length=4)
for i in range(3)]})
], axis=1)
至于我们的玩具数据集,我们只需将迄今为止使用过的所有数据集合并在一起。
如何做到……
本配方的包装类已拆分为多个部分,以帮助您更好地检查和学习代码。第一部分包含初始化,它有效地整合了本章迄今为止所有看到的配方:
class TabularTransformer(BaseEstimator, TransformerMixin):
def instantiate(self, param):
if isinstance(param, str):
return [param]
elif isinstance(param, list):
return param
else:
return None
def __init__(self, numeric=None, dates=None,
ordinal=None, cat=None, highcat=None,
variance_threshold=0.0, missing_imputer='mean',
use_multivariate_imputer=False,
add_missing_indicator=True,
quantile_transformer='normal', scaler=True,
ordinal_categories='auto',
date_format='%d/%m/%Y', hours_secs=False):
self.numeric = self.instantiate(numeric)
self.dates = self.instantiate(dates)
self.ordinal = self.instantiate(ordinal)
self.cat = self.instantiate(cat)
self.highcat = self.instantiate(highcat)
self.columns = None
self.vocabulary = None
在记录了所有包装器的关键参数之后,我们继续检查它的所有独立部分。请不要忘记,这些代码片段都属于同一个__init__方法,我们仅仅是重新使用之前看到的配方,因此关于这些代码片段的任何细节,请参考之前的配方。
这里我们记录了数字管道:
self.numeric_process = assemble_numeric_pipeline(
variance_threshold=variance_threshold,
imputer=missing_imputer,
multivariate_imputer=use_multivariate_imputer,
add_indicator=add_missing_indicator,
quantile_transformer=quantile_transformer,
scaler=scaler)
之后,我们记录与管道处理时间相关的特征:
self.dates_process = DateProcessor(
date_format=date_format, hours_secs=hours_secs)
现在轮到有序变量了:
self.ordinal_process = FeatureUnion(
[('ordinal',
OrdinalEncoder(categories=ordinal_categories)),
('categorial',
Pipeline(steps=[('string_converter', ToString()),
('imputer',
SimpleImputer(strategy='constant',
fill_value='missing')),
('onehot',
OneHotEncoder(handle_unknown='ignore'))]))])
我们以分类管道作为结尾,包括低类别和高类别的管道:
self.cat_process = Pipeline(steps=[
('string_converter', ToString()),
('imputer', SimpleImputer(strategy='constant',
fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore'))])
self.highcat_process = LEncoder()
下一部分涉及拟合。根据不同的变量类型,将应用相应的拟合过程,新的处理或生成的列将记录在.columns索引列表中:
def fit(self, X, y=None, **fit_params):
self.columns = list()
if self.numeric:
self.numeric_process.fit(X[self.numeric])
self.columns += derive_numeric_columns(
X[self.numeric],
self.numeric_process).to_list()
if self.dates:
self.dates_process.fit(X[self.dates])
self.columns += self.dates_process.columns.to_list()
if self.ordinal:
self.ordinal_process.fit(X[self.ordinal])
self.columns += self.ordinal + derive_ohe_columns(
X[self.ordinal],
self.ordinal_process.transformer_list[1][1])
if self.cat:
self.cat_process.fit(X[self.cat])
self.columns += derive_ohe_columns(X[self.cat],
self.cat_process)
if self.highcat:
self.highcat_process.fit(X[self.highcat])
self.vocabulary = dict(zip(self.highcat,
self.highcat_process.dictionary_size))
self.columns = [self.columns, self.highcat]
return self
transform方法提供了所有的转换和矩阵连接,以返回一个包含处理后数据的数组列表,第一个元素是数值部分,后面是代表高基数类别变量的数值标签向量:
def transform(self, X, y=None, **fit_params):
flat_matrix = list()
if self.numeric:
flat_matrix.append(
self.numeric_process.transform(X[self.numeric])
.astype(np.float32))
if self.dates:
flat_matrix.append(
self.dates_process.transform(X[self.dates])
.values
.astype(np.float32))
if self.ordinal:
flat_matrix.append(
self.ordinal_process.transform(X[self.ordinal])
.todense()
.astype(np.float32))
if self.cat:
flat_matrix.append(
self.cat_process.transform(X[self.cat])
.todense()
.astype(np.float32))
if self.highcat:
cat_vectors = self.highcat_process.transform(
X[self.highcat])
if len(flat_matrix) > 0:
return [np.hstack(flat_matrix)] + cat_vectors
else:
return cat_vectors
else:
return np.hstack(flat_matrix)
最后,我们设置了fit_transform方法,它依次执行 fit 和 transform 操作:
def fit_transform(self, X, y=None, **fit_params):
self.fit(X, y, **fit_params)
return self.transform(X)
现在我们已经完成了所有的封装工作,可以看看它是如何工作的。
它是如何工作的……
在我们的测试中,我们根据列的类型将列名的列表赋值给变量:
numeric_vars = ['a', 'b', 'c', 'd']
date_vars = ['date_1', 'date_2', 'date_3']
ordinal_vars = ['rank', 'importance']
cat_vars = ['object', 'code']
highcat_vars = ['high_cat_1', 'high_cat_2', 'high_cat_3']
tt = TabularTransformer(numeric=numeric_vars, dates=date_vars,
ordinal=ordinal_vars, cat=cat_vars,
highcat=highcat_vars)
在实例化了TabularTransformer并将需要处理的变量映射到它们的类型后,我们开始拟合并转换我们的示例数据集:
input_list = tt.fit_transform(example)
结果是一个 NumPy 数组的列表。我们可以通过遍历它们并打印它们的形状,来检查输出的组成:
print([(item.shape, item.dtype) for item in input_list])
打印的结果显示第一个元素是一个较大的数组(所有过程的结合结果,除了高基数类别变量的部分):
[((3, 40), dtype('float32')), ((3,), dtype('int32')), ((3,), dtype('int32')), ((3,), dtype('int32'))]
我们的 DNN 现在可以期望一个列表作为输入,其中第一个元素是数值矩阵,接下来的元素是要传递给类别嵌入层的向量。
还有更多内容……
为了能够追溯每个列和向量的名称,TabularTransformer有一个columns方法,tt.columns,可以调用。TabularTransformer还可以调用tt.vocabulary来获取关于类别变量维度的信息,这对于正确设置网络中嵌入层的输入形状是必需的。返回的结果是一个字典,其中列名是键,字典的大小是值:
{'high_cat_1': 5, 'high_cat_2': 5, 'high_cat_3': 5}
现在我们已经有了这两个方法来追踪变量名(tt.columns)和定义高基数变量的词汇(tt.vocabulary),我们距离一个完整的深度学习框架已经只差一步,用于处理表格数据的深度学习。
设置数据生成器
在我们尝试在一个难度较大的测试任务上使用我们的框架之前,还缺少一个关键成分。前面的步骤展示了一个TabularTransformer,它可以有效地将 pandas DataFrame 转化为 DNN 可以处理的数值数组。然而,这个步骤只能一次性处理所有数据。下一步是提供一种方法,能够创建不同大小的数据批次。这可以通过使用tf.data或 Keras 生成器来实现,并且由于我们在本书之前已经探讨了许多tf.data的例子,这次我们将为 Keras 生成器准备代码,使其能够在 DNN 学习时动态生成随机批次。
准备工作
我们的生成器将继承自Sequence类:
from tensorflow.keras.utils import Sequence
Sequence 类是拟合数据序列的基础对象,要求你实现自定义的 __getitem__(返回完整批次)和 __len__(报告完成一个周期所需的批次数)方法。
如何实现…
我们现在编写一个名为 DataGenerator 的新类,继承自 Keras 的 Sequence 类:
class DataGenerator(Sequence):
def __init__(self, X, y,
tabular_transformer=None,
batch_size=32,
shuffle=False,
dict_output=False
):
self.X = X
self.y = y
self.tbt = tabular_transformer
self.tabular_transformer = tabular_transformer
self.batch_size = batch_size
self.shuffle = shuffle
self.dict_output = dict_output
self.indexes = self._build_index()
self.on_epoch_end()
self.item = 0
def _build_index(self):
return np.arange(len(self.y))
def on_epoch_end(self):
if self.shuffle:
np.random.shuffle(self.indexes)
def __len__(self):
return int(len(self.indexes) / self.batch_size) + 1
def __iter__(self):
for i in range(self.__len__()):
self.item = i
yield self.__getitem__(index=i)
self.item = 0
def __next__(self):
return self.__getitem__(index=self.item)
def __call__(self):
return self.__iter__()
def __data_generation(self, selection):
if self.tbt is not None:
if self.dict_output:
dct = {'input_'+str(j) : arr for j,
arr in enumerate(
self.tbt.transform(self.X.iloc[selection, :]))}
return dct, self.y[selection]
else:
return self.tbt.transform(
self.X.iloc[selection, :]), self.y[selection]
else:
return self.X.iloc[selection, :], self.y[selection]
def __getitem__(self, index):
indexes = self.indexes[
index*self.batch_size:(index+1)*self.batch_size]
samples, labels = self.__data_generation(indexes)
return samples, labels, [None]
生成器已经设置好。接下来让我们进入下一部分,详细探索它是如何工作的。
它是如何工作的…
除了 __init__ 方法,该方法实例化类的内部变量外,DataGenerator 类还包括以下方法:
-
_build_index:用于创建提供数据的索引 -
on_epoch_end:在每个周期结束时,这个方法将随机打乱数据 -
__len__:报告完成一个周期所需的批次数 -
__iter__:使该类成为可迭代对象 -
__next__:调用下一个批次 -
__call__:返回__iter__方法的调用 -
__data_generation:在这里,TabularTransformer对数据批次进行操作,返回转换后的输出(作为数组列表或数组字典返回) -
__getitem__:将数据拆分成批次,并调用__data_generation方法进行转换
这完成了最后一块拼图。通过使用最后两个配方,你只需填写几个参数,就可以将任何混合变量的表格数据完全转换并交付给 TensorFlow 模型。在接下来的两个配方中,我们将为你提供一些特定的技巧,以帮助我们的 DNN 更好地处理表格数据,并且我们将展示一个来自著名 Kaggle 竞赛的完整示例。
为表格数据创建自定义激活函数
对于图像和文本数据来说,由于数据是稀疏的,在 DNN 处理表格数据时反向传播误差更加困难。虽然 ReLU 激活函数广泛使用,但已经发现新的激活函数在这种情况下表现更好,可以提高网络性能。这些激活函数包括 SeLU、GeLU 和 Mish。由于 SeLU 已经包含在 Keras 和 TensorFlow 中(参见 www.tensorflow.org/api_docs/python/tf/keras/activations/selu 和 www.tensorflow.org/api_docs/python/tf/nn/selu),在这个配方中我们将使用 GeLU 和 Mish 激活函数。
准备工作
你需要导入常见的模块:
from tensorflow import keras as keras
import numpy as np
import matplotlib.pyplot as plt
我们已经添加了 matplotlib,可以绘制这些新激活函数的工作效果,并了解它们有效性的原因。
如何实现…
GeLU 和 Mish 的数学公式已在它们的原始论文中定义,您可以在其中找到详细信息:
-
高斯误差线性单元(GELUs):
arxiv.org/abs/1606.08415 -
Mish, 一种自正则化的 非单调神经激活函数:
arxiv.org/abs/1908.08681
下面是翻译成代码的公式:
def gelu(x):
return 0.5 * x * (1 + tf.tanh(tf.sqrt(2 / np.pi) *
(x + 0.044715 * tf.pow(x, 3))))
keras.utils.get_custom_objects().update(
{'gelu': keras.layers.Activation(gelu)})
def mish(inputs):
return inputs * tf.math.tanh(tf.math.softplus(inputs))
keras.utils.get_custom_objects().update(
{'mish': keras.layers.Activation(mish)})
这个方法有趣的地方在于 get_custom_objects 是一个函数,允许你在自定义的 TensorFlow 对象中记录新的函数,并且可以轻松地在层参数中将其作为字符串调用。你可以通过查看 TensorFlow 文档了解更多关于自定义对象的工作原理:www.tensorflow.org/api_docs/python/tf/keras/utils/get_custom_objects。
它是如何工作的…
我们可以通过绘制正负输入与其输出的关系来了解这两个激活函数是如何工作的。使用 matplotlib 的几个命令将帮助我们进行可视化:
gelu_vals = list()
mish_vals = list()
abscissa = np.arange(-4, 1, 0.1)
for val in abscissa:
gelu_vals.append(gelu(tf.cast(val, tf.float32)).numpy())
mish_vals.append(mish(tf.cast(val, tf.float32)).numpy())
plt.plot(abscissa, gelu_vals, label='gelu')
plt.plot(abscissa, mish_vals, label='mish')
plt.axvline(x=0.0, linestyle='--', color='darkgray')
plt.axhline(y=0.0, linestyle='--', color='darkgray')
plt.legend()
plt.show()
运行代码后,你应该得到以下图形:

图 7.3:GeLU 和 Mish 激活函数从输入到输出的映射
与 ReLU 激活函数类似,输入从零开始到正数的部分会被直接映射为输出(保持正激活时的线性关系)。有趣的部分出现在输入小于零时,实际上,因为它不像 ReLU 那样被抑制。在 GeLU 和 Mish 激活函数中,负输入的输出是经过抑制的变换,当输入非常负时会趋近于零。这避免了神经元“死亡”的问题,因为负输入仍然可以传递信息,也避免了神经元饱和的问题,因为过于负的值会被关闭。
通过不同的策略,GeLU 和 Mish 激活函数都能处理和传播负输入。这使得负输入的梯度是有定义的,并且不会对网络造成伤害。
在一个困难问题上进行测试
本章中,我们提供了一些有效处理表格数据的技巧。每个技巧本身并不是一个完整的解决方案,而是一个拼图的一部分。当这些部分组合在一起时,你可以获得出色的结果。在这最后一个技巧中,我们将展示如何将所有技巧结合起来,成功完成一个困难的 Kaggle 挑战。
Kaggle 竞赛 Amazon.com – Employee Access Challenge (www.kaggle.com/c/amazon-employee-access-challenge) 是一个因涉及高基数变量而著名的竞赛,也是比较梯度提升算法的一个重要基准。该竞赛的目的是开发一个模型,根据员工的角色和活动预测是否应该给予其访问特定资源的权限。结果应该以可能性形式给出。作为预测因子,你将使用不同的 ID 代码,这些代码对应于你正在评估访问权限的资源类型、员工在组织中的角色以及推荐经理。
准备工作
和往常一样,我们先导入 TensorFlow 和 Keras:
import tensorflow as tf
import tensorflow.keras as keras
使用基于顺序的数据生成器可能会触发 TensorFlow 2.2 中的一些错误。这是由于启用执行(eager execution),因此作为预防措施,我们必须为此配方禁用它:
tf.compat.v1.disable_eager_execution()
为了获取亚马逊数据集,最佳且最快的方法是安装CatBoost,一种使用数据集作为基准的梯度提升算法。如果它还没有安装在你的环境中,你可以通过运行pip install catboost命令轻松安装:
from catboost.datasets import amazon
X, Xt = amazon()
y = X["ACTION"].apply(lambda x: 1 if x == 1 else 0).values
X.drop(["ACTION"], axis=1, inplace=True)
由于测试数据(已上传至Xt变量)包含未标记的目标变量,我们将只使用X变量中的训练数据。
如何实现…
首先,我们将定义此问题的 DNN 架构。由于问题只涉及具有高基数的分类变量,我们从为每个特征设置输入和嵌入层开始。
我们首先为每个特征定义一个输入,其中数据流入网络,然后每个输入被导入到其各自的嵌入层。输入的大小根据特征的唯一值数量来确定,输出的大小则基于输入大小的对数。每个嵌入层的输出随后传递到空间丢弃层(由于嵌入层会返回一个矩阵,空间丢弃层会将整个矩阵的列置空),然后进行展平。最后,所有展平后的结果被连接成一个单一的层。从此,数据必须通过两个带丢弃层的全连接层,最终到达输出响应节点,该节点是一个经过 sigmoid 激活的节点,返回一个概率作为答案:
def dnn(categorical_variables, categorical_counts,
feature_selection_dropout=0.2, categorical_dropout=0.1,
first_dense = 256, second_dense = 256,
dense_dropout = 0.2,
activation_type=gelu):
categorical_inputs = []
categorical_embeddings = []
for category in categorical_variables:
categorical_inputs.append(keras.layers.Input(
shape=[1], name=category))
category_counts = categorical_counts[category]
categorical_embeddings.append(
keras.layers.Embedding(category_counts+1,
int(np.log1p(category_counts)+1),
name = category +
"_embed")(categorical_inputs[-1]))
def flatten_dropout(x, categorical_dropout):
return keras.layers.Flatten()(
keras.layers.SpatialDropout1D(categorical_dropout)(x))
categorical_logits = [flatten_dropout(cat_emb,
categorical_dropout)
for cat_emb in categorical_embeddings]
categorical_concat = keras.layers.Concatenate(
name = "categorical_concat")(categorical_logits)
x = keras.layers.Dense(first_dense,
activation=activation_type)(categorical_concat)
x = keras.layers.Dropout(dense_dropout)(x)
x = keras.layers.Dense(second_dense,
activation=activation_type)(x)
x = keras.layers.Dropout(dense_dropout)(x)
output = keras.layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(categorical_inputs, output)
return model
该架构仅适用于分类数据。它将每个分类输入(预期为单个整数编码)传入一个嵌入层,嵌入层的输出是一个降维向量(其维度通过启发式方法int(np.log1p(category_counts)+1)计算得出)。接着应用SpatialDropout1D,最后将输出展平。SpatialDropout1D会去除输出矩阵一行中的所有连接,去掉所有通道中的一些信息。所有分类变量的输出将被连接并传递到一系列带 GeLU 激活和丢弃层的全连接层。最后,输出一个单一的 sigmoid 节点(因此你可以得到[0,1]范围内的概率输出)。
定义完架构后,我们将定义评分函数,取自 scikit-learn,并使用 TensorFlow 中的tf.py_function将它们转换为 Keras 可以使用的函数(www.tensorflow.org/api_docs/python/tf/py_function),这是一个包装器,可以将任何函数转化为一个一次可微分的 TensorFlow 操作,并可以进行即时执行。
作为得分函数,我们使用平均精度和 ROC AUC。这两者都能帮助我们了解在二分类问题上的表现,告诉我们预测的概率与真实值之间的接近程度。有关 ROC AUC 和平均精度的更多信息,请参见 scikit-learn 文档中的scikit-learn.org/stable/modules/generated/sklearn.metrics.average_precision_score.html和scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html#sklearn.metrics.roc_auc_score。
我们还实例化了一个简单的绘图函数,可以绘制在训练过程中记录的选定误差和得分度量,既适用于训练集也适用于验证集:
from sklearn.metrics import average_precision_score, roc_auc_score
def mAP(y_true, y_pred):
return tf.py_function(average_precision_score,
(y_true, y_pred), tf.double)
def auc(y_true, y_pred):
try:
return tf.py_function(roc_auc_score,
(y_true, y_pred), tf.double)
except:
return 0.5
def compile_model(model, loss, metrics, optimizer):
model.compile(loss=loss, metrics=metrics, optimizer=optimizer)
return model
def plot_keras_history(history, measures):
"""
history: Keras training history
measures = list of names of measures
"""
rows = len(measures) // 2 + len(measures) % 2
fig, panels = plt.subplots(rows, 2, figsize=(15, 5))
plt.subplots_adjust(top = 0.99, bottom=0.01,
hspace=0.4, wspace=0.2)
try:
panels = [item for sublist in panels for item in sublist]
except:
pass
for k, measure in enumerate(measures):
panel = panels[k]
panel.set_title(measure + ' history')
panel.plot(history.epoch, history.history[measure],
label="Train "+measure)
panel.plot(history.epoch, history.history["val_"+measure],
label="Validation "+measure)
panel.set(xlabel='epochs', ylabel=measure)
panel.legend()
plt.show(fig)
此时,你需要设置训练阶段。考虑到样本数量有限以及你需要测试你的解决方案,使用交叉验证是最好的选择。scikit-learn 中的StratifiedKFold函数将为你提供完成此任务的正确工具。
在StratifiedKFold中,你的数据会随机(你可以提供一个种子值以确保可重复性)被划分为k个部分,每一部分的目标变量比例与原始数据中的比例相同。
这些k拆分被用来生成k个训练测试,这些测试可以帮助你推断你所设置的 DNN 架构的表现。事实上,在进行k次实验时,除了一个拆分,其余的都被用来训练模型,而那个被保留的拆分则每次都用于测试。这确保了你有k个测试,都是在未用于训练的拆分上进行的。
这种方法,尤其是在训练样本较少时,比选择单一的测试集来验证模型更为优越,因为通过抽取测试集,你可能会发现一个与训练集分布不同的样本。此外,使用单一的测试集也有可能导致过拟合。如果你反复测试不同的解决方案,最终可能会找到一个非常适合测试集的解决方案,但这个解决方案并不一定具有泛化能力。
让我们在这里实践一下:
from sklearn.model_selection import StratifiedKFold
SEED = 0
FOLDS = 3
BATCH_SIZE = 512
skf = StratifiedKFold(n_splits=FOLDS,
shuffle=True,
random_state=SEED)
roc_auc = list()
average_precision = list()
categorical_variables = X.columns.to_list()
for fold, (train_idx, test_idx) in enumerate(skf.split(X, y)):
tt = TabularTransformer(highcat = categorical_variables)
tt.fit(X.iloc[train_idx])
categorical_levels = tt.vocabulary
model = dnn(categorical_variables,
categorical_levels,
feature_selection_dropout=0.1,
categorical_dropout=0.1,
first_dense=64,
second_dense=64,
dense_dropout=0.1,
activation_type=mish)
model = compile_model(model,
keras.losses.binary_crossentropy,
[auc, mAP],
tf.keras.optimizers.Adam(learning_rate=0.0001))
train_batch = DataGenerator(X.iloc[train_idx],
y[train_idx],
tabular_transformer=tt,
batch_size=BATCH_SIZE,
shuffle=True)
val_X, val_y = tt.transform(X.iloc[test_idx]), y[test_idx]
history = model.fit(train_batch,
validation_data=(val_X, val_y),
epochs=30,
class_weight=[1.0,
(np.sum(y==0) / np.sum(y==1))],
verbose=2)
print("\nFOLD %i" % fold)
plot_keras_history(history, measures=['auc', 'loss'])
preds = model.predict(val_X, verbose=0,
batch_size=1024).flatten()
roc_auc.append(roc_auc_score(y_true=val_y, y_score=preds))
average_precision.append(average_precision_score(
y_true=val_y, y_score=preds))
print(f"mean cv roc auc {np.mean(roc_auc):0.3f}")
print(f"mean cv ap {np.mean(average_precision):0.3f}")
该脚本会为每个折叠执行训练和验证测试,并存储结果,这些结果将帮助你正确评估你在表格数据上的深度神经网络(DNN)表现。
它是如何工作的……
每个折叠将打印出一张图,详细展示 DNN 在训练集和验证集上的表现,包括对数损失和 ROC AUC:

图 7.4:DNN 在训练集和验证集上的表现
所有的折叠都有类似的轨迹,在训练和验证曲线在 5 个 epoch 之后明显分离,并且在 15 个 epoch 后差距扩大,暗示训练阶段可能存在一定的过拟合。通过修改你的 DNN 架构,并改变如学习率或优化算法等参数,你可以放心地进行实验,尝试获得更好的结果,因为交叉验证过程可以确保你做出正确的决策。
第八章:卷积神经网络
卷积神经网络(CNNs)是近年来图像识别领域取得重大突破的关键。在本章中,我们将讨论以下主题:
-
实现一个简单的 CNN
-
实现一个高级 CNN
-
重新训练现有的 CNN 模型
-
应用 StyleNet 和神经风格项目
-
实现 DeepDream
提醒读者,本章的所有代码可以在这里在线获取:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook,以及 Packt 仓库:github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook。
介绍
在前面的章节中,我们讨论了密集神经网络(DNNs),其中一层的每个神经元都与相邻层的每个神经元相连接。在本章中,我们将重点介绍一种在图像分类中表现良好的特殊类型的神经网络:CNN。
CNN 是由两个组件组成的:一个特征提取模块,后接一个可训练的分类器。第一个组件包括一堆卷积、激活和池化层。一个 DNN 负责分类。每一层的神经元都与下一层的神经元相连接。
在数学中,卷积是一个应用于另一个函数输出的运算。在我们的例子中,我们考虑使用矩阵乘法(滤波器)作用于图像。对于我们的目的,我们将图像视为一个数字矩阵。这些数字可以代表像素或图像属性。我们将对这些矩阵应用卷积操作,方法是将一个固定宽度的滤波器在图像上移动,并使用逐元素相乘得到结果。
请参阅下图,以便更好地理解图像卷积的工作原理:

图 8.1:一个 2x2 的卷积滤波器应用于一个 5x5 的输入矩阵,生成一个新的 4x4 特征层
在图 8.1中,我们看到如何将卷积滤波器应用于图像(长×宽×深度),从而创建一个新的特征层。在这里,我们使用一个2x2的卷积滤波器,作用于5x5输入的有效空间,并且在两个方向上的步幅都为 1。结果是一个4x4的矩阵。这个新特征层突出了输入图像中激活滤波器最多的区域。
CNN 还具有其他操作来满足更多的需求,例如引入非线性(ReLU),或聚合参数(最大池化、平均池化)以及其他类似操作。上面的示例图展示了在一个5x5数组上应用卷积操作,卷积滤波器是一个2x2的矩阵。步幅为 1,并且我们只考虑有效的放置位置。在此操作中的可训练变量将是2x2滤波器的权重。
在卷积操作之后,通常会跟随一个聚合操作,例如最大池化。池化操作的目标是减少参数数量、计算负担和内存使用。最大池化保留了最强的特征。
以下图示提供了最大池化操作的一个示例。在这个例子中,池化操作的区域是一个 2x2 的区域,步长为 2。

图 8.2:在 4x4 输入图像上应用最大池化操作
图 8.2 展示了最大池化操作的工作原理。在这里,我们有一个 2x2 的窗口,在一个 4x4 的输入图像上滑动,步长为 2。结果是一个 2x2 的矩阵,它就是每个区域的最大值。
虽然我们将通过创建自己的 CNN 来进行图像识别,但我建议使用现有的架构,正如我们在本章其余部分将要做的那样。
通常我们会使用一个预训练的网络,并通过新的数据集和一个新的全连接层对其进行再训练。这种方法很有利,因为我们不必从零开始训练模型;我们只需要对预训练模型进行微调,以适应我们的新任务。我们将在本章稍后的 重新训练现有的 CNN 模型 部分进行演示,其中我们将重新训练现有架构,以提高 CIFAR-10 的预测性能。
不再拖延,我们立即开始实现一个简单的 CNN。
实现一个简单的 CNN
在这个实例中,我们将开发一个基于 LeNet-5 架构的 CNN,LeNet-5 首次由 Yann LeCun 等人于 1998 年提出,用于手写和机器打印字符的识别。

图 8.3:LeNet-5 架构 – 原始图像来源于 [LeCun 等人, 1998]
该架构由两组 CNN 组成,包含卷积-ReLU-最大池化操作,用于特征提取,随后是一个扁平化层和两个全连接层,用于分类图像。
我们的目标是提高对 MNIST 数字的预测准确性。
准备开始
要访问 MNIST 数据,Keras 提供了一个包(tf.keras.datasets),它具有出色的数据集加载功能。(请注意,TensorFlow 也提供了自己的现成数据集集合,通过 TF Datasets API。)加载数据后,我们将设置模型变量,创建模型,按批次训练模型,然后可视化损失、准确率和一些样本数字。
如何做到...
执行以下步骤:
-
首先,我们将加载必要的库并启动图形会话:
import matplotlib.pyplot as plt import numpy as np import tensorflow as tf -
接下来,我们将加载数据并将图像重塑为四维矩阵:
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() # Reshape x_train = x_train.reshape(-1, 28, 28, 1) x_test = x_test.reshape(-1, 28, 28, 1) #Padding the images by 2 pixels x_train = np.pad(x_train, ((0,0),(2,2),(2,2),(0,0)), 'constant') x_test = np.pad(x_test, ((0,0),(2,2),(2,2),(0,0)), 'constant')请注意,这里下载的 MNIST 数据集包括训练集和测试集。该数据集由灰度图像(形状为(num_sample, 28, 28)的整数数组)和标签(范围为 0-9 的整数)组成。我们对图像进行了 2 像素的填充,因为在 LeNet-5 论文中,输入图像是32x32的。
-
现在,我们将设置模型的参数。记住,图像的深度(通道数)是 1,因为这些图像是灰度图像。我们还将设置一个种子,以确保结果可复现:
image_width = x_train[0].shape[0] image_height = x_train[0].shape[1] num_channels = 1 # grayscale = 1 channel seed = 98 np.random.seed(seed) tf.random.set_seed(seed) -
我们将声明我们的训练数据变量和测试数据变量。我们将为训练和评估使用不同的批次大小。你可以根据可用的物理内存调整这些大小:
batch_size = 100 evaluation_size = 500 epochs = 300 eval_every = 5 -
我们将对图像进行归一化,将所有像素的值转换为统一的尺度:
x_train = x_train / 255 x_test = x_test/ 255 -
现在我们将声明我们的模型。我们将有一个特征提取模块,由两个卷积/ReLU/最大池化层组成,接着是一个由全连接层构成的分类器。此外,为了使分类器能够工作,我们将特征提取模块的输出展平,以便可以在分类器中使用。请注意,我们在分类器的最后一层使用了 softmax 激活函数。Softmax 将数值输出(logits)转换为概率,使其总和为 1:
input_data = tf.keras.Input(dtype=tf.float32, shape=(image_width,image_height, num_channels), name="INPUT") # First Conv-ReLU-MaxPool Layer conv1 = tf.keras.layers.Conv2D(filters=6, kernel_size=5, padding='VALID', activation="relu", name="C1")(input_data) max_pool1 = tf.keras.layers.MaxPool2D(pool_size=2, strides=2, padding='SAME', name="S1")(conv1) # Second Conv-ReLU-MaxPool Layer conv2 = tf.keras.layers.Conv2D(filters=16, kernel_size=5, padding='VALID', strides=1, activation="relu", name="C3")(max_pool1) max_pool2 = tf.keras.layers.MaxPool2D(pool_size=2, strides=2, padding='SAME', name="S4")(conv2) # Flatten Layer flatten = tf.keras.layers.Flatten(name="FLATTEN")(max_pool2) # First Fully Connected Layer fully_connected1 = tf.keras.layers.Dense(units=120, activation="relu", name="F5")(flatten) # Second Fully Connected Layer fully_connected2 = tf.keras.layers.Dense(units=84, activation="relu", name="F6")(fully_connected1) # Final Fully Connected Layer final_model_output = tf.keras.layers.Dense(units=10, activation="softmax", name="OUTPUT" )(fully_connected2) model = tf.keras.Model(inputs= input_data, outputs=final_model_output) -
接下来,我们将使用 Adam(自适应矩估计)优化器来编译模型。Adam 使用自适应学习率和动量,使我们能够更快地达到局部最小值,从而加速收敛。由于我们的目标是整数,而不是独热编码格式,我们将使用稀疏分类交叉熵损失函数。然后,我们还将添加一个准确度指标,以评估模型在每个批次上的准确性:
model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] -
接下来,我们打印网络的字符串摘要:
model.summary()![]()
图 8.4:LeNet-5 架构
LeNet-5 模型有 7 层,包含 61,706 个可训练参数。现在,让我们开始训练模型。
-
现在我们可以开始训练我们的模型了。我们通过随机选择的批次来遍历数据。每隔一段时间,我们选择在训练集和测试集批次上评估模型,并记录准确率和损失值。我们可以看到,经过 300 个周期后,我们很快在测试数据上达到了 96-97%的准确率:
train_loss = [] train_acc = [] test_acc = [] for i in range(epochs): rand_index = np.random.choice(len(x_train), size=batch_size) rand_x = x_train[rand_index] rand_y = y_train[rand_index] history_train = model.train_on_batch(rand_x, rand_y) if (i+1) % eval_every == 0: eval_index = np.random.choice(len(x_test), size=evaluation_size) eval_x = x_test[eval_index] eval_y = y_test[eval_index] history_eval = model.evaluate(eval_x,eval_y) # Record and print results train_loss.append(history_train[0]) train_acc.append(history_train[1]) test_acc.append(history_eval[1]) acc_and_loss = [(i+1), history_train [0], history_train[1], history_eval[1]] acc_and_loss = [np.round(x,2) for x in acc_and_loss] print('Epoch # {}. Train Loss: {:.2f}. Train Acc (Test Acc): {:.2f} ({:.2f})'.format(*acc_and_loss)) -
这将产生以下输出:
Epoch # 5\. Train Loss: 2.19\. Train Acc (Test Acc): 0.23 (0.34) Epoch # 10\. Train Loss: 2.01\. Train Acc (Test Acc): 0.59 (0.58) Epoch # 15\. Train Loss: 1.71\. Train Acc (Test Acc): 0.74 (0.73) Epoch # 20\. Train Loss: 1.32\. Train Acc (Test Acc): 0.73 (0.77) ... Epoch # 290\. Train Loss: 0.18\. Train Acc (Test Acc): 0.95 (0.94) Epoch # 295\. Train Loss: 0.13\. Train Acc (Test Acc): 0.96 (0.96) Epoch # 300\. Train Loss: 0.12\. Train Acc (Test Acc): 0.95 (0.97) -
以下是使用
Matplotlib绘制损失和准确率的代码:# Matlotlib code to plot the loss and accuracy eval_indices = range(0, epochs, eval_every) # Plot loss over time plt.plot(eval_indices, train_loss, 'k-') plt.title('Loss per Epoch') plt.xlabel('Epoch') plt.ylabel('Loss') plt.show() # Plot train and test accuracy plt.plot(eval_indices, train_acc, 'k-', label='Train Set Accuracy') plt.plot(eval_indices, test_acc, 'r--', label='Test Set Accuracy') plt.title('Train and Test Accuracy') plt.xlabel('Epoch') plt.ylabel('Accuracy') plt.legend(loc='lower right') plt.show()然后我们得到以下图表:
![]()
图 8.5:左图是我们 300 个训练周期中训练集和测试集的准确率。右图是 300 个周期中的 softmax 损失值。
-
如果我们想要绘制最新批次结果的示例,这里是绘制包含六个最新结果的样本的代码:
# Plot some samples and their predictions actuals = y_test[30:36] preds = model.predict(x_test[30:36]) predictions = np.argmax(preds,axis=1) images = np.squeeze(x_test[30:36]) Nrows = 2 Ncols = 3 for i in range(6): plt.subplot(Nrows, Ncols, i+1) plt.imshow(np.reshape(images[i], [32,32]), cmap='Greys_r') plt.title('Actual: ' + str(actuals[i]) + ' Pred: ' + str(predictions[i]), fontsize=10) frame = plt.gca() frame.axes.get_xaxis().set_visible(False) frame.axes.get_yaxis().set_visible(False) plt.show()上述代码的输出如下:
![/var/folders/6z/j6f33tds3v7dv8tryfqnxf5m0000gn/T/com.microsoft.Word/Content.MSO/46B36DDA.tmp]()
图 8.6:六个随机图像的图示,标题中包括实际值和预测值。左下角的图片被预测为 6,实际上它是 4。
使用一个简单的 CNN,我们在此数据集上取得了较好的准确率和损失结果。
它是如何工作的...
我们在 MNIST 数据集上的表现有所提升,构建了一个从零开始训练并迅速达到约 97%准确率的模型。我们的特征提取模块是卷积、ReLU 和最大池化的组合。我们的分类器是全连接层的堆叠。我们在批次大小为 100 的情况下进行训练,并查看了跨越各个 epoch 的准确率和损失情况。最后,我们还绘制了六个随机数字,发现模型预测失败,未能正确预测一张图片。模型预测为 6,实际上是 4。
CNN 在图像识别方面表现非常出色。部分原因在于卷积层生成其低级特征,当遇到图像中的重要部分时,这些特征会被激活。这种模型能够自我创建特征并将其用于预测。
还有更多...
在过去几年里,CNN 模型在图像识别方面取得了巨大进展。许多新颖的想法正在被探索,并且新的架构频繁被发现。该领域有一个庞大的科学论文库,名为 arXiv.org(arxiv.org/),由康奈尔大学创建和维护。arXiv.org 包含许多领域的最新文章,包括计算机科学及其子领域,如计算机视觉和图像识别(arxiv.org/list/cs.CV/recent)。
另见
这里有一些你可以用来了解 CNN 的优秀资源:
-
斯坦福大学有一个很好的维基,详情请见:
scarlet.stanford.edu/teach/index.php/An_Introduction_to_Convolutional_Neural_Networks -
深度学习 由 Michael Nielsen 编写,详情请见:
neuralnetworksanddeeplearning.com/chap6.html -
卷积神经网络简介 由吴建新编写,详情请见:
pdfs.semanticscholar.org/450c/a19932fcef1ca6d0442cbf52fec38fb9d1e5.pdf -
LeNet-5,卷积神经网络 由 Yann LeCun 编写:
yann.lecun.com/exdb/lenet/ -
基于梯度的学习应用于文档识别 由 Yann LeCun 等人编写:
yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf
实现一个高级 CNN
能够扩展 CNN 模型进行图像识别至关重要,这样我们才能理解如何增加网络的深度。这样,如果我们有足够的数据,我们可能提高预测的准确性。扩展 CNN 网络的深度是以标准方式进行的:我们只需重复卷积、最大池化和 ReLU 直到我们对深度感到满意。许多更精确的图像识别网络都是以这种方式运行的。
数据加载和预处理可能会让人头疼:大多数图像数据集过大,无法完全加载到内存中,但为了提高模型性能,图像预处理是必须的。我们可以使用 TensorFlow 的 tf.data API 创建输入管道。这个 API 提供了一套用于加载和预处理数据的工具。通过它,我们将从 CIFAR-10 数据集实例化一个 tf.data.Dataset 对象(通过 Keras 数据集 API tf.keras.datasets 下载),将该数据集的连续元素合并成批次,并对每张图像应用变换。另外,在图像识别数据中,通常会在训练前随机扰动图像。在这里,我们将随机裁剪、翻转并调整亮度。
准备就绪
在本食谱中,我们将实现一种更高级的图像数据读取方法,并使用一个更大的 CNN 对 CIFAR-10 数据集进行图像识别(www.cs.toronto.edu/~kriz/cifar.html)。该数据集包含 60,000 张32x32的图像,属于 10 个可能类别中的一个。图像的潜在标签包括飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。请参阅另见部分的第一点。
官方的 TensorFlow tf.data 教程可以在本食谱末尾的另见部分找到。
如何操作...
执行以下步骤:
-
首先,我们加载必要的库:
import matplotlib.pyplot as plt import numpy as np import tensorflow as tf from tensorflow import keras -
现在我们将声明一些数据集和模型参数,然后声明一些图像变换参数,例如随机裁剪图像的大小:
# Set dataset and model parameters batch_size = 128 buffer_size= 128 epochs=20 #Set transformation parameters crop_height = 24 crop_width = 24 -
现在我们将使用
keras.datasetsAPI 从 CIFAR-10 数据集中获取训练和测试图像。这个 API 提供了几个可以完全加载到内存的小型数据集,因此数据会以 NumPy 数组的形式表示(NumPy 是用于科学计算的核心 Python 库):(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data() -
接下来,我们将使用
tf.data.Dataset从 NumPy 数组创建训练和测试的 TensorFlow 数据集,以便利用tf.dataAPI 构建一个灵活高效的图像管道:dataset_train = tf.data.Dataset.from_tensor_slices((x_train, y_train)) dataset_test = tf.data.Dataset.from_tensor_slices((x_test, y_test)) -
我们将定义一个读取函数,该函数将加载并稍微扭曲图像,以便使用 TensorFlow 内建的图像修改功能进行训练:
# Define CIFAR reader def read_cifar_files(image, label): final_image = tf.image.resize_with_crop_or_pad(image, crop_width, crop_height) final_image = image / 255 # Randomly flip the image horizontally, change the brightness and contrast final_image = tf.image.random_flip_left_right(final_image) final_image = tf.image.random_brightness(final_image,max_delta=0.1) final_image = tf.image.random_contrast(final_image,lower=0.5, upper=0.8) return final_image, label -
现在我们有了一个图像处理管道函数和两个 TensorFlow 数据集,我们可以初始化训练图像管道和测试图像管道:
dataset_train_processed = dataset_train.shuffle(buffer_size).batch(batch_size).map(read_cifar_files) dataset_test_processed = dataset_test.batch(batch_size).map(lambda image,label: read_cifar_files(image, label, False))请注意,在这个例子中,我们的输入数据适合内存,因此我们使用
from_tensor_slices()方法将所有图像转换为tf.Tensor。但是tf.dataAPI 允许处理不适合内存的大型数据集。对数据集的迭代是以流式传输的方式进行的。 -
接下来,我们可以创建我们的序列模型。我们将使用的模型有两个卷积层,后面跟着三个全连接层。两个卷积层将分别创建 64 个特征。第一个全连接层将第二个卷积层与 384 个隐藏节点连接起来。第二个全连接操作将这 384 个隐藏节点连接到 192 个隐藏节点。最后一个隐藏层操作将这 192 个节点连接到我们要预测的 10 个输出类别。在最后一层我们将使用 softmax 函数,因为一张图片只能取一个确切的类别,所以输出应该是对这 10 个目标的概率分布:
model = keras.Sequential( [# First Conv-ReLU-Conv-ReLU-MaxPool Layer tf.keras.layers.Conv2D(input_shape=[32,32,3], filters=32, kernel_size=3, padding='SAME', activation="relu", kernel_initializer='he_uniform', name="C1"), tf.keras.layers.Conv2D(filters=32, kernel_size=3, padding='SAME', activation="relu", kernel_initializer='he_uniform', name="C2"), tf.keras.layers.MaxPool2D((2,2), name="P1"), tf.keras.layers.Dropout(0.2), # Second Conv-ReLU-Conv-ReLU-MaxPool Layer tf.keras.layers.Conv2D(filters=64, kernel_size=3, padding='SAME', activation="relu", kernel_initializer='he_uniform', name="C3"), tf.keras.layers.Conv2D(filters=64, kernel_size=3, padding='SAME', activation="relu", kernel_initializer='he_uniform', name="C4"), tf.keras.layers.MaxPool2D((2,2), name="P2"), tf.keras.layers.Dropout(0.2), # Third Conv-ReLU-Conv-ReLU-MaxPool Layer tf.keras.layers.Conv2D(filters=128, kernel_size=3, padding='SAME', activation="relu", kernel_initializer='he_uniform', name="C5"), tf.keras.layers.Conv2D(filters=128, kernel_size=3, padding='SAME', activation="relu", kernel_initializer='he_uniform', name="C6"), tf.keras.layers.MaxPool2D((2,2), name="P3"), tf.keras.layers.Dropout(0.2), # Flatten Layer tf.keras.layers.Flatten(name="FLATTEN"), # Fully Connected Layer tf.keras.layers.Dense(units=128, activation="relu", name="D1"), tf.keras.layers.Dropout(0.2), # Final Fully Connected Layer tf.keras.layers.Dense(units=10, activation="softmax", name="OUTPUT") ]) -
现在我们将编译我们的模型。我们的损失将是分类交叉熵损失。我们添加了一个精度度量,它接收模型预测的 logits 和实际目标,并返回用于记录训练/测试集统计信息的准确率。我们还运行 summary 方法以打印一个总结页面:
model.compile(loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) model.summary()![]()
图 8.7:模型摘要由 3 个 VGG 块组成(VGG - Visual Geometry Group - 块是一系列卷积层,后跟用于空间下采样的最大池化层),随后是一个分类器。
-
现在我们拟合模型,通过我们的训练和测试输入管道进行循环。我们将保存训练损失和测试准确率:
history = model.fit(dataset_train_processed, validation_data=dataset_test_processed, epochs=epochs) -
最后,这里是一些
Matplotlib代码,将绘制整个训练过程中的损失和测试准确率:# Print loss and accuracy # Matlotlib code to plot the loss and accuracy epochs_indices = range(0, 10, 1) # Plot loss over time plt.plot(epochs_indices, history.history["loss"], 'k-') plt.title('Softmax Loss per Epoch') plt.xlabel('Epoch') plt.ylabel('Softmax Loss') plt.show() # Plot accuracy over time plt.plot(epochs_indices, history.history["val_accuracy"], 'k-') plt.title('Test Accuracy per Epoch') plt.xlabel('Epoch') plt.ylabel('Accuracy') plt.show()我们为这个配方得到以下图表:
![]()
图 8.8:训练损失在左侧,测试准确率在右侧。对于 CIFAR-10 图像识别 CNN,我们能够在测试集上达到大约 80%的准确率。
它是如何工作的...
在我们下载了 CIFAR-10 数据之后,我们建立了一个图像管道。我们使用这个训练和测试管道来尝试预测图像的正确类别。到最后,模型在测试集上达到了大约 80%的准确率。我们可以通过使用更多数据、微调优化器或增加更多 epochs 来达到更高的准确率。
另请参阅
-
有关 CIFAR-10 数据集的更多信息,请参阅《从小图像中学习多层特征》,Alex Krizhevsky,2009:
www.cs.toronto.edu/~kriz/learning-features-2009-TR.pdf。 -
tf.dataTensorFlow 教程:www.tensorflow.org/guide/data。 -
面向工程师的 Keras 简介(数据加载和预处理):
keras.io/getting_started/intro_to_keras_for_engineers/#data-loading-amp-preprocessing。
重新训练现有的 CNN 模型
从零开始训练一个新的图像识别模型需要大量时间和计算资源。如果我们能拿一个预训练的网络,并用我们的图像重新训练它,可能会节省计算时间。在这个方案中,我们将展示如何使用一个预训练的 TensorFlow 图像识别模型,并对其进行微调,以便处理不同的图像集。
我们将演示如何使用来自预训练网络的迁移学习来处理 CIFAR-10。这个方法的思路是重用先前模型中卷积层的权重和结构,并重新训练网络顶部的全连接层。这个方法被称为微调。
准备就绪
我们将使用的 CNN 网络采用一种非常流行的架构,叫做Inception。Inception CNN 模型由谷歌创建,在许多图像识别基准测试中表现出色。详细信息请参见另见部分第二个项目中提到的论文。
我们将介绍的主要 Python 脚本展示了如何获取 CIFAR-10 图像数据,并将其转换为 Inception 重训练格式。之后,我们将再次说明如何在我们的图像上训练 Inception v3 网络。
如何实现...
执行以下步骤:
-
我们将从加载必要的库开始:
import tensorflow as tf from tensorflow import keras from tensorflow.keras.applications.inception_v3 import InceptionV3 from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions -
我们现在将设置稍后通过
tf.data.DatasetAPI 使用的参数:batch_size = 32 buffer_size= 1000 -
现在,我们将下载 CIFAR-10 数据,并声明用于稍后保存图像时引用的 10 个类别:
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data() objects = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] -
然后,我们将使用
tf.data.Dataset初始化数据管道,用于训练和测试数据集:dataset_train = tf.data.Dataset.from_tensor_slices((x_train, y_train)) dataset_test = tf.data.Dataset.from_tensor_slices((x_test, y_test)) -
Inception v3 在 ImageNet 数据集上进行过预训练,因此我们的 CIFAR-10 图像必须与这些图像的格式匹配。预期的宽度和高度应不小于 75,因此我们将图像调整为75x75的空间大小。然后,图像应该被归一化,我们将对每张图像应用 Inception 预处理任务(
preprocess_input方法)。def preprocess_cifar10(img, label): img = tf.cast(img, tf.float32) img = tf.image.resize(img, (75, 75)) return tf.keras.applications.inception_v3.preprocess_input(img) , label dataset_train_processed = dataset_train.shuffle(buffer_size).batch(batch_size).map(preprocess_cifar10) dataset_test_processed = dataset_test.batch(batch_size).map(preprocess_cifar10) -
现在,我们将基于 InceptionV3 模型创建我们的模型。我们将使用
tensorflow.keras.applicationsAPI 加载 InceptionV3 模型。该 API 包含可以用于预测、特征提取和微调的预训练深度学习模型。然后,我们将加载没有分类头的权重。inception_model = InceptionV3( include_top=False, weights="imagenet", input_shape=(75,75,3) ) -
我们在 InceptionV3 模型的基础上构建自己的模型,添加一个具有三层全连接层的分类器。
x = inception_model.output x= keras.layers.GlobalAveragePooling2D()(x) x = keras.layers.Dense(1024, activation="relu")(x) x = keras.layers.Dense(128, activation="relu")(x) output = keras.layers.Dense(10, activation="softmax")(x) model=keras.Model(inputs=inception_model.input, outputs = output) -
我们将把 Inception 的基础层设置为不可训练。只有分类器的权重会在反向传播阶段更新(Inception 的权重不会更新):
for inception_layer in inception_model.layers: inception_layer.trainable= False -
现在我们将编译我们的模型。我们的损失函数将是分类交叉熵损失。我们添加了一个精度指标,该指标接收模型预测的对数和实际目标,并返回用于记录训练/测试集统计数据的精度:
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"]) -
现在我们将拟合模型,通过我们的训练和测试输入流水线循环进行:
model.fit(x=dataset_train_processed , validation_data=dataset_test_processed) -
到最后,该模型在测试集上达到了约 63% 的准确率:
loss: 1.1316 - accuracy: 0.6018 - val_loss: 1.0361 - val_accuracy: 0.6366...
它是如何工作的...
在下载了 CIFAR-10 数据之后,我们建立了一个图像流水线来将图像转换为所需的 Inception 格式。我们在 InceptionV3 模型之上添加了一个分类器,并对其进行了训练,以预测 CIFAR-10 图像的正确类别。到最后,该模型在测试集上达到了约 63% 的准确率。请记住,我们正在微调模型并重新训练顶部的全连接层,以适应我们的十类数据。
参见
-
TensorFlow Inception-v3 文档:
www.tensorflow.org/api_docs/python/tf/keras/applications/inception_v3 -
Keras Applications 文档:
keras.io/api/applications/ -
GoogLeNet Inception-v3 论文:
arxiv.org/abs/1512.00567
应用 StyleNet 和神经风格项目
一旦我们训练了一个图像识别的 CNN,我们可以使用网络本身进行一些有趣的数据和图像处理。StyleNet 是一个过程,试图从一个图片中学习风格,并将其应用到第二张图片,同时保持第二张图片的结构(或内容)不变。为了做到这一点,我们必须找到与风格强相关的中间 CNN 节点,独立于图片内容。
StyleNet 是一个过程,它接受两幅图像并将一幅图像的风格应用到第二幅图像的内容上。它基于 2015 年 Leon Gatys 的著名论文,A Neural Algorithm of Artistic Style(参见下一节参见部分下的第一条)进行操作。作者在一些 CNN 中发现了一种包含中间层的特性。其中一些似乎编码了图片的风格,而另一些则编码了其内容。因此,如果我们在风格图片上训练风格层,并在原始图片上训练内容层,并反向传播这些计算出的损失,我们就可以将原始图片更改为更像风格图片。
准备工作
这个食谱是官方 TensorFlow 神经风格迁移的改编版本,可在本食谱结尾的参见部分找到。
为了实现这一点,我们将使用 Gatys 在A Neural Algorithm of Artistic Style中推荐的网络,称为imagenet-vgg-19。
如何做...
执行以下步骤:
-
首先,我们将通过加载必要的库启动我们的 Python 脚本:
import imageio import numpy as np from skimage.transform import resize import tensorflow as tf import matplotlib.pyplot as plt import matplotlib as mpl import IPython.display as display import PIL.Image -
然后,我们可以声明两张图像的位置:原始图像和风格图像。对于我们的示例,我们将使用本书的封面图像作为原始图像;风格图像我们将使用文森特·凡高的 《星夜》。你也可以使用任何你想要的两张图片。如果你选择使用这些图片,它们可以在本书的 GitHub 网站上找到,
github.com/PacktPublishing/Machine-Learning-Using-TensorFlow-Cookbook(导航到 StyleNet 部分):content_image_file = 'images/book_cover.jpg' style_image_file = 'images/starry_night.jpg' -
现在,我们将使用
scipy加载两张图像,并调整风格图像的大小,使其与内容图像的尺寸相符:# Read the images content_image = imageio.imread(content_image_file) style_image = imageio.imread(style_image_file) content_image = tf.image.convert_image_dtype(content_image, tf.float32) style_image = tf.image.convert_image_dtype(style_image, tf.float32) # Get shape of target and make the style image the same target_shape = content_image.shape style_image = resize(style_image, target_shape) -
接下来,我们将展示内容图像和风格图像:
mpl.rcParams['figure.figsize'] = (12,12) mpl.rcParams['axes.grid'] = False plt.subplot(1, 2, 1) plt.imshow(content_image) plt.title("Content Image") plt.subplot(1, 2, 2) plt.imshow(style_image) plt.title("Style Image")![/var/folders/6z/j6f33tds3v7dv8tryfqnxf5m0000gn/T/com.microsoft.Word/Content.MSO/B6B0CA0A.tmp]()
图 8.9:示例内容和风格图像
-
现在,我们将加载在 ImageNet 上预训练的 VGG-19 模型,但不包括分类头。我们将使用
tensorflow.keras.applicationsAPI。这个 API 包含了可用于预测、特征提取和微调的预训练深度学习模型。vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') vgg.trainable = False -
接下来,我们将展示 VGG-19 的架构:
[layer.name for layer in vgg.layers] -
在神经风格迁移中,我们希望将一张图像的风格应用到另一张图像的内容上。卷积神经网络(CNN)由多个卷积层和池化层组成。卷积层提取复杂特征,而池化层提供空间信息。Gatys 的论文推荐了一些策略,用于为内容图像和风格图像分配中间层。我们应当保留
block4_conv2作为内容图像的层,可以尝试其他blockX_conv1层输出的不同组合来作为风格图像的层:content_layers = ['block4_conv2', 'block5_conv2'] style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1'] num_content_layers = len(content_layers) num_style_layers = len(style_layers) -
虽然中间特征图的值代表了图像的内容,但风格可以通过这些特征图的均值和相关性来描述。在这里,我们定义了 Gram 矩阵来捕捉图像的风格。Gram 矩阵衡量每个特征图之间的相关性程度。这个计算是针对每个中间特征图进行的,只获得图像的纹理信息。注意,我们会丢失关于图像空间结构的信息。
def gram_matrix(input_tensor): result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor) input_shape = tf.shape(input_tensor) num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32) return result/(num_locations) -
接下来,我们构建一个模型,返回包含每个层名称及其相关内容/风格张量的风格和内容字典。Gram 矩阵应用于风格层:
class StyleContentModel(tf.keras.models.Model): def __init__(self, style_layers, content_layers): super(StyleContentModel, self).__init__() self.vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') outputs = [vgg.get_layer(name).output for name in style_layers + content_layers] self.vgg = tf.keras.Model([vgg.input], outputs) self.style_layers = style_layers self.content_layers = content_layers self.num_style_layers = len(style_layers) self.vgg.trainable = False def call(self, inputs): "Expects float input in [0,1]" inputs = inputs*255.0 inputs = inputs[tf.newaxis, :] preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs) outputs = self.vgg(preprocessed_input) style_outputs, content_outputs = (outputs[:self.num_style_layers], outputs[self.num_style_layers:]) style_outputs = [gram_matrix(style_output) for style_output in style_outputs] content_dict = {content_name:value for content_name, value in zip(self.content_layers, content_outputs)} style_dict = {style_name:value for style_name, value in zip(self.style_layers, style_outputs)} return {'content':content_dict, 'style':style_dict} -
设置风格和内容的目标值,它们将用于损失计算:
extractor = StyleContentModel(style_layers, content_layers) style_targets = extractor(style_image)['style'] content_targets = extractor(content_image)['content'] -
Adam 和 LBFGS 通常会有相同的误差并且很快收敛,但 LBFGS 在处理大图像时优于 Adam。虽然论文推荐使用 LBFGS,由于我们的图像较小,我们将选择 Adam 优化器。
#Optimizer configuration learning_rate = 0.05 beta1 = 0.9 beta2 = 0.999 opt = tf.optimizers.Adam(learning_rate=learning_rate, beta_1=beta1, beta_2=beta2) -
接下来,我们将计算总损失,它是内容损失和风格损失的加权和:
content_weight = 5.0 style_weight = 1.0 -
内容损失将比较我们的原始图像和当前图像(通过内容层特征)。风格损失将比较我们预先计算的风格特征与输入图像中的风格特征。第三个也是最终的损失项将有助于平滑图像。我们在这里使用总变差损失来惩罚相邻像素的剧烈变化,如下所示:
def style_content_loss(outputs): style_outputs = outputs['style'] content_outputs = outputs['content'] style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) for name in style_outputs.keys()]) style_loss *= style_weight / num_style_layers content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) for name in content_outputs.keys()]) content_loss *= content_weight / num_content_layers loss = style_loss + content_loss return loss -
接下来,我们声明一个工具函数。由于我们有一个浮动图像,需要将像素值保持在 0 和 1 之间:
def clip_0_1(image): return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0) -
现在,我们声明另一个工具函数,将张量转换为图像:
def tensor_to_image(tensor): tensor = tensor*255 tensor = np.array(tensor, dtype=np.uint8) if np.ndim(tensor)>3: assert tensor.shape[0] == 1 tensor = tensor[0] return PIL.Image.fromarray(tensor) -
接下来,我们使用梯度带运行梯度下降,生成我们的新图像,并显示如下:
epochs = 100 image = tf.Variable(content_image) for generation in range(epochs): with tf.GradientTape() as tape: outputs = extractor(image) loss = style_content_loss(outputs) grad = tape.gradient(loss, image) opt.apply_gradients([(grad, image)]) image.assign(clip_0_1(image)) print(".", end='') display.display(tensor_to_image(image))![]()
图 8.10:使用 StyleNet 算法将书籍封面图像与《星夜》结合
请注意,可以通过更改内容和样式权重来使用不同的强调方式。
它是如何工作的…
我们首先加载了两张图像,然后加载了预训练的网络权重,并将层分配给内容图像和风格图像。我们计算了三个损失函数:内容图像损失、风格损失和总变差损失。然后,我们训练了随机噪声图片,以使用风格图像的风格和原始图像的内容。风格迁移可以用于照片和视频编辑应用、游戏、艺术、虚拟现实等。例如,在 2019 年游戏开发者大会上,Google 推出了 Stadia,可以实时改变游戏的艺术风格。其实时演示视频可在本食谱最后的另见部分查看。
另见
-
艺术风格的神经算法,由 Gatys、Ecker、Bethge 著,2015 年:
arxiv.org/abs/1508.06576 -
Leon Gatys 在 2016 年 CVPR(计算机视觉与模式识别)上的演讲推荐视频可以在这里查看:
www.youtube.com/watch?v=UFffxcCQMPQ -
要查看神经风格迁移过程的原始 TensorFlow 代码,请参见
www.tensorflow.org/tutorials/generative/style_transfer -
若要深入了解理论,请参见
towardsdatascience.com/neural-style-transfer-tutorial-part-1-f5cd3315fa7f -
Google Stadia – 风格迁移 ML:
stadiasource.com/article/2/Stadia-Introducing-Style-Transfer-ML-GDC2019
实现 DeepDream
训练好的 CNN 还有一个用途,那就是利用一些中间节点检测标签特征(例如,猫的耳朵或鸟的羽毛)。利用这一点,我们可以找到将任何图像转化为反映这些节点特征的方式,适用于我们选择的任何节点。这个教程是官方 TensorFlow DeepDream 教程的改编版本(参见下文 另见 部分的第一个项目)。欢迎访问 DeepDream 的创造者亚历山大·莫尔维茨夫(Alexander Mordvintsev)在 Google AI 博客上写的文章(下文 另见 部分的第二个项目)。希望通过这个教程,能够帮助你使用 DeepDream 算法探索 CNN 和其中创建的特征。
准备工作
最初,这项技术是为了更好地理解 CNN 如何“看”图像而发明的。DeepDream 的目标是过度解读模型检测到的模式,并生成具有超现实模式的激发性视觉内容。这种算法是一种新型的迷幻艺术。
如何进行操作…
执行以下步骤:
-
要开始使用 DeepDream,我们首先需要加载必要的库:
import numpy as np import PIL.Image import imageio import matplotlib.pyplot as plt import matplotlib as mpl import tensorflow as tf import IPython.display as display -
我们将准备图像进行梦幻化处理。我们将读取原始图像,将其重塑为最大 500 尺寸,并显示出来:
# Read the images original_img_file = path + 'images/book_cover.jpg' original_img = imageio.imread(original_img_file) # Reshape to 500 max dimension new_shape = tf.cast((500, 500 * original_img.shape[1] / original_img.shape[0]), tf.int32) original_img = tf.image.resize(original_img, new_shape, method='nearest').numpy() # Display the image mpl.rcParams['figure.figsize'] = (20,6) mpl.rcParams['axes.grid'] = False plt.imshow(original_img) plt.title("Original Image") -
我们将加载在 ImageNet 上预训练的 Inception 模型,并去除分类头。我们将使用
tf.keras.applicationsAPI:inception_model = tf.keras.applications.InceptionV3(include_top=False, weights='imagenet') -
我们总结了这个模型。我们可以注意到,Inception 模型相当庞大:
inception_model.summary() -
接下来,我们将选择用于后续 DeepDream 处理的卷积层。在 CNN 中,较早的层提取基本特征,如边缘、形状、纹理等,而较深的层则提取高级特征,如云、树木或鸟类。为了创建 DeepDream 图像,我们将专注于卷积层混合的地方。现在,我们将创建一个以这两个混合层为输出的特征提取模型:
names = ['mixed3', 'mixed5'] layers = [inception_model.get_layer(name).output for name in names] deep_dream_model = tf.keras.Model(inputs=inception_model.input, outputs=layers) -
现在我们将定义损失函数,返回所有输出层的总和:
def compute_loss(img, model): # Add a dimension to the image to have a batch of size 1. img_batch = tf.expand_dims(img, axis=0) # Apply the model to the images and get the outputs to retrieve the activation. layer_activations = model(img_batch) # Compute the loss for each layer losses = [] for act in layer_activations: loss = tf.math.reduce_mean(act) losses.append(loss) return tf.reduce_sum(losses) -
我们声明了两个实用函数,用于撤销缩放并显示处理后的图像:
def deprocess(img): img = 255*(img + 1.0)/2.0 return tf.cast(img, tf.uint8) def show(img): display.display(PIL.Image.fromarray(np.array(img))) -
接下来,我们将应用梯度上升过程。在 DeepDream 中,我们不是通过梯度下降最小化损失,而是通过梯度上升最大化这些层的激活,通过最大化它们的损失。这样,我们会过度解读模型检测到的模式,并生成具有超现实模式的激发性视觉内容:
def run_deep_dream(image, steps=100, step_size=0.01): # Apply the Inception preprocessing image = tf.keras.applications.inception_v3.preprocess_input(image) image = tf.convert_to_tensor(image) loss = tf.constant(0.0) for n in tf.range(steps): # We use gradient tape to track TensorFlow computations with tf.GradientTape() as tape: # We use watch to force TensorFlow to track the image tape.watch(image) # We compute the loss loss = compute_loss(image, deep_dream_model) # Compute the gradients gradients = tape.gradient(loss, image) # Normalize the gradients. gradients /= tf.math.reduce_std(gradients) + 1e-8 # Perform the gradient ascent by directly adding the gradients to the image image = image + gradients*step_size image = tf.clip_by_value(image, -1, 1) # Display the intermediate image if (n % 100 ==0): display.clear_output(wait=True) show(deprocess(image)) print ("Step {}, loss {}".format(n, loss)) # Display the final image result = deprocess(image) display.clear_output(wait=True) show(result) return result -
然后,我们将在原始图像上运行 DeepDream:
dream_img = run_deep_dream(image=original_img, steps=100, step_size=0.01)输出结果如下:
![/var/folders/6z/j6f33tds3v7dv8tryfqnxf5m0000gn/T/com.microsoft.Word/Content.MSO/DFE47502.tmp]()
图 8.11:DeepDream 应用于原始图像
虽然结果不错,但还是可以做得更好!我们注意到图像输出很杂乱,模式似乎以相同的粒度应用,并且输出分辨率较低。
-
为了改善图像,我们可以使用“八度”概念。我们对同一张图像进行梯度上升,并多次调整图像大小(每次增大图像的大小就是一次八度的改进)。通过这个过程,较小尺度上检测到的特征可以应用到更高尺度上,展现出更多细节的图案。
OCTAVE_SCALE = 1.30 image = tf.constant(np.array(original_img)) base_shape = tf.shape(image)[:-1] float_base_shape = tf.cast(base_shape, tf.float32) for n in range(-2, 3): # Increase the size of the image new_shape = tf.cast(float_base_shape*(OCTAVE_SCALE**n), tf.int32) image = tf.image.resize(image, new_shape).numpy() # Apply deep dream image = run_deep_dream(image=image, steps=50, step_size=0.01) # Display output display.clear_output(wait=True) image = tf.image.resize(image, base_shape) image = tf.image.convert_image_dtype(image/255.0, dtype=tf.uint8) show(image)输出结果如下:
![/var/folders/6z/j6f33tds3v7dv8tryfqnxf5m0000gn/T/com.microsoft.Word/Content.MSO/AE675C60.tmp]()
图 8.12:应用八度概念后的原始图像 DeepDream
通过使用八度概念,结果变得非常有趣:输出变得不那么嘈杂,网络能更好地放大它所看到的图案。
还有更多...
我们建议读者使用官方的 DeepDream 教程作为进一步了解的来源,同时也可以访问原始的 Google 研究博客文章,了解 DeepDream(请参阅以下 另见 部分)。
另见
-
TensorFlow 深度梦境教程:
www.tensorflow.org/tutorials/generative/deepdream -
原始的 Google 研究博客文章关于 DeepDream:
research.googleblog.com/2015/06/inceptionism-going-deeper-into-neural.html
第九章:循环神经网络
循环神经网络(RNN)是建模顺序数据的现代主要方法。架构类名中的“循环”一词指的是当前步骤的输出成为下一步的输入(可能还会作为后续步骤的输入)。在序列中的每个元素上,模型都会同时考虑当前的输入和它对前面元素的“记忆”。
自然语言处理(NLP)任务是 RNN 的主要应用领域之一:如果你正在阅读这一句,你就是通过前面出现的词来理解每个词的上下文。基于 RNN 的 NLP 模型可以利用这种方式实现生成任务,如新文本创作,也可以完成预测任务,如情感分类或机器翻译。
在本章中,我们将涵盖以下主题:
-
文本生成
-
情感分类
-
时间序列 – 股票价格预测
-
开放领域问答
我们要处理的第一个主题是文本生成:它很容易演示我们如何使用 RNN 生成新内容,因此可以作为一个温和的 RNN 入门。
文本生成
演示 RNN 强大功能的最著名应用之一是生成新文本(我们将在稍后的 Transformer 架构章节中回到这个应用)。
在这个实例中,我们将使用长短期记忆(LSTM)架构——一种流行的 RNN 变体——来构建文本生成模型。LSTM 这个名字来源于它们开发的动机:传统的 RNN 在处理长依赖关系时会遇到困难(即所谓的梯度消失问题),而 LSTM 通过其架构解决了这一问题。LSTM 模型通过维持一个单元状态和一个“携带”信息来确保信号(以梯度的形式)在序列处理时不会丢失。在每个时间步,LSTM 模型会联合考虑当前的单词、携带的信息和单元状态。
这个主题本身并不简单,但出于实际应用考虑,完全理解结构设计并非必需。只需记住,LSTM 单元允许将过去的信息在稍后的时间点重新注入。
我们将使用纽约时报评论和标题数据集(www.kaggle.com/aashita/nyt-comments)来训练我们的模型,并利用它生成新的标题。我们选择这个数据集是因为它的中等规模(该方法不需要强大的工作站即可复现)和易获取性(Kaggle 是免费开放的,而一些数据源只能通过付费墙访问)。
如何实现...
和往常一样,首先我们导入必要的包。
import tensorflow as tf
from tensorflow import keras
# keras module for building LSTM
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Embedding, LSTM, Dense
from keras.preprocessing.text import Tokenizer
from keras.callbacks import EarlyStopping
from keras.models import Sequential
import keras.utils as ku
我们希望确保我们的结果是可复现的——由于 Python 深度学习生态系统内的相互依赖关系,我们需要初始化多个随机机制。
import pandas as pd
import string, os
import warnings
warnings.filterwarnings("ignore")
warnings.simplefilter(action='ignore', category=FutureWarning)
下一步是从 Keras 本身导入必要的功能:
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Embedding, LSTM, Dense
from keras.preprocessing.text import Tokenizer
from keras.callbacks import EarlyStopping
from keras.models import Sequential
import keras.utils as ku
最后,通常来说,定制化代码执行过程中显示的警告级别是方便的——尽管这未必总是符合纯粹主义者的最佳实践标准——这主要是为了处理围绕给 DataFrame 子集分配值的普遍警告:在当前环境下,清晰展示代码比遵守生产环境中的编码标准更为重要:
import warnings
warnings.filterwarnings("ignore")
warnings.simplefilter(action='ignore', category=FutureWarning)
我们将定义一些函数,以便后续简化代码。首先,让我们清理文本:
def clean_text(txt):
txt = "".join(v for v in txt if v not in string.punctuation).lower()
txt = txt.encode("utf8").decode("ascii",'ignore')
return txt
我们将使用一个封装器,围绕内置的 TensorFlow 分词器,操作如下:
def get_sequence_of_tokens(corpus):
## tokenization
tokenizer.fit_on_texts(corpus)
total_words = len(tokenizer.word_index) + 1
## convert data to sequence of tokens
input_sequences = []
for line in corpus:
token_list = tokenizer.texts_to_sequences([line])[0]
for i in range(1, len(token_list)):
n_gram_sequence = token_list[:i+1]
input_sequences.append(n_gram_sequence)
return input_sequences, total_words
一个常用的步骤是将模型构建步骤封装到函数中:
def create_model(max_sequence_len, total_words):
input_len = max_sequence_len - 1
model = Sequential()
model.add(Embedding(total_words, 10, input_length=input_len))
model.add(LSTM(100))
model.add(Dense(total_words, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')
return model
以下是对序列进行填充的一些模板代码(在实际应用中,这部分的作用会变得更加清晰):
def generate_padded_sequences(input_sequences):
max_sequence_len = max([len(x) for x in input_sequences])
input_sequences = np.array(pad_sequences(input_sequences, maxlen=max_sequence_len, padding='pre'))
predictors, label = input_sequences[:,:-1],input_sequences[:,-1]
label = ku.to_categorical(label, num_classes=total_words)
return predictors, label, max_sequence_len
最后,我们创建一个函数,用来从已拟合的模型生成文本:
def generate_text(seed_text, next_words, model, max_sequence_len):
for _ in range(next_words):
token_list = tokenizer.texts_to_sequences([seed_text])[0]
token_list = pad_sequences([token_list], maxlen=max_sequence_len-1, padding='pre')
predicted = model.predict_classes(token_list, verbose=0)
output_word = ""
for word,index in tokenizer.word_index.items():
if index == predicted:
output_word = word
break
seed_text += " "+output_word
return seed_text.title()
下一步是加载我们的数据集(break 子句作为快速方法,只选择文章数据集,而不包括评论数据集):
curr_dir = '../input/'
all_headlines = []
for filename in os.listdir(curr_dir):
if 'Articles' in filename:
article_df = pd.read_csv(curr_dir + filename)
all_headlines.extend(list(article_df.headline.values))
break
all_headlines[:10]
我们可以按如下方式检查前几个元素:
['The Opioid Crisis Foretold',
'The Business Deals That Could Imperil Trump',
'Adapting to American Decline',
'The Republicans' Big Senate Mess',
'States Are Doing What Scott Pruitt Won't',
'Fake Pearls, Real Heart',
'Fear Beyond Starbucks',
'Variety: Puns and Anagrams',
'E.P.A. Chief's Ethics Woes Have Echoes in His Past',
'Where Facebook Rumors Fuel Thirst for Revenge']
正如现实中大多数文本数据的情况一样,我们需要清理输入文本。为了简单起见,我们仅进行基础的预处理:去除标点符号并将所有单词转换为小写:
corpus = [clean_text(x) for x in all_headlines]
清理操作后,前 10 行的数据如下所示:
corpus[:10]
['the opioid crisis foretold',
'the business deals that could imperil trump',
'adapting to american decline',
'the republicans big senate mess',
'states are doing what scott pruitt wont',
'fake pearls real heart',
'fear beyond starbucks',
'variety puns and anagrams',
'epa chiefs ethics woes have echoes in his past',
'where facebook rumors fuel thirst for revenge']
下一步是分词。语言模型要求输入数据以序列的形式呈现——给定一个由单词(词元)组成的序列,生成任务的关键在于预测上下文中下一个最可能的词元。我们可以利用 Keras 的 preprocessing 模块中内置的分词器。
清理后,我们对输入文本进行分词:这是从语料库中提取单独词元(单词或术语)的过程。我们利用内置的分词器来提取词元及其相应的索引。每个文档都会转化为一系列词元:
tokenizer = Tokenizer()
inp_sequences, total_words = get_sequence_of_tokens(corpus)
inp_sequences[:10]
[[1, 708],
[1, 708, 251],
[1, 708, 251, 369],
[1, 370],
[1, 370, 709],
[1, 370, 709, 29],
[1, 370, 709, 29, 136],
[1, 370, 709, 29, 136, 710],
[1, 370, 709, 29, 136, 710, 10],
[711, 5]]
像 [1,708]、[1,708, 251] 这样的向量表示从输入数据生成的 n-grams,其中整数是该词元在从语料库生成的总体词汇表中的索引。
我们已经将数据集转化为由词元序列组成的格式——这些序列的长度可能不同。有两种选择:使用 RaggedTensors(这在用法上稍微复杂一些)或者将序列长度统一,以符合大多数 RNN 模型的标准要求。为了简化展示,我们选择后者方案:使用 pad_sequence 函数填充短于阈值的序列。这一步与将数据格式化为预测值和标签的步骤容易结合:
predictors, label, max_sequence_len = generate_padded_sequences(inp_sequences)
我们利用简单的 LSTM 架构,使用 Sequential API:
-
输入层:接收分词后的序列
-
LSTM 层:使用 LSTM 单元生成输出——为了演示,我们默认取 100 作为值,但此参数(以及其他多个参数)是可定制的
-
Dropout 层:我们对 LSTM 输出进行正则化,以减少过拟合的风险
-
输出层:生成最可能的输出标记:
model = create_model(max_sequence_len, total_words) model.summary() _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, 23, 10) 31340 _________________________________________________________________ lstm_1 (LSTM) (None, 100) 44400 _________________________________________________________________ dense_1 (Dense) (None, 3134) 316534 ================================================================= Total params: 392,274 Trainable params: 392,274 Non-trainable params: 0 _________________________________________________________________
现在我们可以使用标准的 Keras 语法训练我们的模型:
model.fit(predictors, label, epochs=100, verbose=2)
现在我们有了一个拟合的模型,我们可以检查其性能:基于种子文本,我们的 LSTM 生成的标题有多好?我们通过对种子文本进行分词、填充序列,并将其传入模型来获得预测结果:
print (generate_text("united states", 5, model, max_sequence_len))
United States Shouldnt Sit Still An Atlantic
print (generate_text("president trump", 5, model, max_sequence_len))
President Trump Vs Congress Bird Moving One
print (generate_text("joe biden", 8, model, max_sequence_len))
Joe Biden Infuses The Constitution Invaded Canada Unique Memorial Award
print (generate_text("india and china", 8, model, max_sequence_len))
India And China Deal And The Young Think Again To It
print (generate_text("european union", 4, model, max_sequence_len))
European Union Infuses The Constitution Invaded
正如你所看到的,即使使用相对简单的设置(一个中等大小的数据集和一个基础模型),我们也能生成看起来相当真实的文本。当然,进一步的微调将允许生成更复杂的内容,这将是我们在第十章中讨论的内容,Transformers。
另见
网上有多个优秀的资源可以学习 RNN:
-
要了解一个很好的介绍——并且有很棒的例子——请参阅 Andrej Karpathy 的文章:
karpathy.github.io/2015/05/21/rnn-effectiveness/ -
可以在
github.com/kjw0612/awesome-rnn找到一个精选资源列表(教程、代码库) -
另一个很好的介绍可以在
medium.com/@humble_bee/rnn-recurrent-neural-networks-lstm-842ba7205bbf找到
情感分类
自然语言处理(NLP)中的一个常见任务是情感分类:根据文本片段的内容,识别其中表达的情感。实际应用包括评论分析、调查回复、社交媒体评论或健康护理材料。
我们将在 www-cs.stanford.edu/people/alecmgo/papers/TwitterDistantSupervision09.pdf 中介绍的 Sentiment140 数据集上训练我们的网络,该数据集包含 160 万条带有三类标注的推文:负面、中立和正面。为了避免本地化问题,我们对编码进行标准化(最好从控制台级别进行,而不是在笔记本内进行)。逻辑如下:原始数据集包含原始文本,这些文本——由于其固有性质——可能包含非标准字符(例如,表情符号,这在社交媒体通信中显然很常见)。我们希望将文本转换为 UTF8——这是英语 NLP 的事实标准。最快的方法是使用 Linux 命令行功能:
-
Iconv是一个标准的工具,用于在编码之间进行转换 -
-f和-t标志分别表示输入编码和目标编码 -
-o指定输出文件:
iconv -f LATIN1 -t UTF8 training.1600000.processed.noemoticon.csv -o training_cleaned.csv
如何实现...
我们首先按如下方式导入必要的包:
import json
import tensorflow as tf
import csv
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from tensorflow.keras import regularizers
接下来,我们定义模型的超参数:
-
嵌入维度是我们将使用的词嵌入的大小。在本食谱中,我们将使用 GloVe:一种无监督学习算法,基于维基百科和 Gigaword 的合并语料库的词共现统计信息进行训练。得到的(英文)单词向量为我们提供了有效的文本表示方式,通常称为嵌入。
-
max_length和padding_type是指定如何填充序列的参数(见前面的食谱)。 -
training_size指定了目标语料库的大小。 -
test_portion定义了我们将作为保留数据使用的数据比例。 -
dropout_val和nof_units是模型的超参数:
embedding_dim = 100
max_length = 16
trunc_type='post'
padding_type='post'
oov_tok = "<OOV>"
training_size=160000
test_portion=.1
num_epochs = 50
dropout_val = 0.2
nof_units = 64
让我们将模型创建步骤封装成一个函数。我们为我们的分类任务定义了一个相当简单的模型——一个嵌入层,后接正则化、卷积、池化,再加上 RNN 层:
def create_model(dropout_val, nof_units):
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size+1, embedding_dim, input_length=max_length, weights=[embeddings_matrix], trainable=False),
tf.keras.layers.Dropout(dropout_val),
tf.keras.layers.Conv1D(64, 5, activation='relu'),
tf.keras.layers.MaxPooling1D(pool_size=4),
tf.keras.layers.LSTM(nof_units),
tf.keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])
return model
收集我们将用于训练的语料库内容:
num_sentences = 0
with open("../input/twitter-sentiment-clean-dataset/training_cleaned.csv") as csvfile:
reader = csv.reader(csvfile, delimiter=',')
for row in reader:
list_item=[]
list_item.append(row[5])
this_label=row[0]
if this_label=='0':
list_item.append(0)
else:
list_item.append(1)
num_sentences = num_sentences + 1
corpus.append(list_item)
转换为句子格式:
sentences=[]
labels=[]
random.shuffle(corpus)
for x in range(training_size):
sentences.append(corpus[x][0])
labels.append(corpus[x][1])
Tokenize the sentences:
tokenizer = Tokenizer()
tokenizer.fit_on_texts(sentences)
word_index = tokenizer.word_index
vocab_size = len(word_index)
sequences = tokenizer.texts_to_sequences(sentences)
使用填充规范化句子长度(见前一节):
padded = pad_sequences(sequences, maxlen=max_length, padding=padding_type, truncating=trunc_type)
将数据集划分为训练集和保留集:
split = int(test_portion * training_size)
test_sequences = padded[0:split]
training_sequences = padded[split:training_size]
test_labels = labels[0:split]
training_labels = labels[split:training_size]
在使用基于 RNN 的模型进行 NLP 应用时,一个关键步骤是embeddings矩阵:
embeddings_index = {};
with open('../input/glove6b/glove.6B.100d.txt') as f:
for line in f:
values = line.split();
word = values[0];
coefs = np.asarray(values[1:], dtype='float32');
embeddings_index[word] = coefs;
embeddings_matrix = np.zeros((vocab_size+1, embedding_dim));
for word, i in word_index.items():
embedding_vector = embeddings_index.get(word);
if embedding_vector is not None:
embeddings_matrix[i] = embedding_vector;
在所有准备工作完成后,我们可以设置模型:
model = create_model(dropout_val, nof_units)
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding (Embedding) (None, 16, 100) 13877100
_________________________________________________________________
dropout (Dropout) (None, 16, 100) 0
_________________________________________________________________
conv1d (Conv1D) (None, 12, 64) 32064
_________________________________________________________________
max_pooling1d (MaxPooling1D) (None, 3, 64) 0
_________________________________________________________________
lstm (LSTM) (None, 64) 33024
_________________________________________________________________
dense (Dense) (None, 1) 65
=================================================================
Total params: 13,942,253
Trainable params: 65,153
Non-trainable params: 13,877,100
_________________________________________________________________
训练按常规方式进行:
num_epochs = 50
history = model.fit(training_sequences, training_labels, epochs=num_epochs, validation_data=(test_sequences, test_labels), verbose=2)
Train on 144000 samples, validate on 16000 samples
Epoch 1/50
144000/144000 - 47s - loss: 0.5685 - acc: 0.6981 - val_loss: 0.5454 - val_acc: 0.7142
Epoch 2/50
144000/144000 - 44s - loss: 0.5296 - acc: 0.7289 - val_loss: 0.5101 - val_acc: 0.7419
Epoch 3/50
144000/144000 - 42s - loss: 0.5130 - acc: 0.7419 - val_loss: 0.5044 - val_acc: 0.7481
Epoch 4/50
144000/144000 - 42s - loss: 0.5017 - acc: 0.7503 - val_loss: 0.5134 - val_acc: 0.7421
Epoch 5/50
144000/144000 - 42s - loss: 0.4921 - acc: 0.7563 - val_loss: 0.5025 - val_acc: 0.7518
Epoch 6/50
144000/144000 - 42s - loss: 0.4856 - acc: 0.7603 - val_loss: 0.5003 - val_acc: 0.7509
我们还可以通过可视化来评估模型的质量:
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(len(acc))
plt.plot(epochs, acc, 'r', label='Training accuracy')
plt.plot(epochs, val_acc, 'b', label='Validation accuracy')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'r', label='Training Loss')
plt.plot(epochs, val_loss, 'b', label='Validation Loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

图 9.1:训练准确率与验证准确率随训练轮次变化

图 9.2:训练损失与验证损失随训练轮次变化
正如我们从两个图表中看到的,模型在有限的训练轮次后已经取得了不错的表现,并且在此之后稳定下来,只有少量波动。潜在的改进包括早停法,并扩大数据集的规模。
另见
有兴趣了解 RNN 在情感分类中的应用的读者可以查阅以下资源:
-
TensorFlow 文档教程:
www.tensorflow.org/tutorials/text/text_classification_rnn -
link.springer.com/chapter/10.1007/978-3-030-28364-3_49是众多展示将 RNN 应用于情感检测的文章之一,其中包含了大量的参考文献。 -
GloVe 文档可以在
nlp.stanford.edu/projects/glove/找到
股票价格预测
像 RNN 这样的顺序模型非常适合时间序列预测——其中一个最为宣传的应用是金融数量的预测,尤其是不同金融工具的价格预测。在本食谱中,我们展示了如何将 LSTM 应用于时间序列预测问题。我们将重点关注比特币的价格——最受欢迎的加密货币。
需要说明的是:这是一个基于流行数据集的演示示例。它并不打算作为任何形式的投资建议;建立一个在金融领域适用的可靠时间序列预测模型是一项具有挑战性的工作,超出了本书的范围。
如何做...
我们首先导入必要的包:
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from sklearn.preprocessing import MinMaxScaler
我们任务的通用参数是预测的未来范围和网络的超参数:
prediction_days = 30
nof_units =4
如前所述,我们将把模型创建步骤封装到一个函数中。它接受一个参数units,该参数是 LSTM 内单元的维度:
def create_model(nunits):
# Initialising the RNN
regressor = Sequential()
# Adding the input layer and the LSTM layer
regressor.add(LSTM(units = nunits, activation = 'sigmoid', input_shape = (None, 1)))
# Adding the output layer
regressor.add(Dense(units = 1))
# Compiling the RNN
regressor.compile(optimizer = 'adam', loss = 'mean_squared_error')
return regressor
现在我们可以开始加载数据,并采用常见的时间戳格式。为了演示的目的,我们将预测每日平均价格——因此需要进行分组操作:
# Import the dataset and encode the date
df = pd.read_csv("../input/bitcoin-historical-data/bitstampUSD_1-min_data_2012-01-01_to_2020-09-14.csv")
df['date'] = pd.to_datetime(df['Timestamp'],unit='s').dt.date
group = df.groupby('date')
Real_Price = group['Weighted_Price'].mean()
下一步是将数据分为训练期和测试期:
df_train= Real_Price[:len(Real_Price)-prediction_days]
df_test= Real_Price[len(Real_Price)-prediction_days:]
理论上可以避免预处理,但在实践中它有助于加速收敛:
training_set = df_train.values
training_set = np.reshape(training_set, (len(training_set), 1))
sc = MinMaxScaler()
training_set = sc.fit_transform(training_set)
X_train = training_set[0:len(training_set)-1]
y_train = training_set[1:len(training_set)]
X_train = np.reshape(X_train, (len(X_train), 1, 1))
拟合模型非常简单:
regressor = create_model(nunits = nof_unit)
regressor.fit(X_train, y_train, batch_size = 5, epochs = 100)
Epoch 1/100
3147/3147 [==============================] - 6s 2ms/step - loss: 0.0319
Epoch 2/100
3147/3147 [==============================] - 3s 928us/step - loss: 0.0198
Epoch 3/100
3147/3147 [==============================] - 3s 985us/step - loss: 0.0089
Epoch 4/100
3147/3147 [==============================] - 3s 1ms/step - loss: 0.0023
Epoch 5/100
3147/3147 [==============================] - 3s 886us/step - loss: 3.3583e-04
Epoch 6/100
3147/3147 [==============================] - 3s 957us/step - loss: 1.0990e-04
Epoch 7/100
3147/3147 [==============================] - 3s 830us/step - loss: 1.0374e-04
Epoch 8/100
通过拟合模型,我们可以在预测范围内生成一个预测,记得要反转我们的标准化处理,以便将值还原到原始尺度:
test_set = df_test.values
inputs = np.reshape(test_set, (len(test_set), 1))
inputs = sc.transform(inputs)
inputs = np.reshape(inputs, (len(inputs), 1, 1))
predicted_BTC_price = regressor.predict(inputs)
predicted_BTC_price = sc.inverse_transform(predicted_BTC_price)
这是我们预测结果的样子:
plt.figure(figsize=(25,15), dpi=80, facecolor='w', edgecolor='k')
ax = plt.gca()
plt.plot(test_set, color = 'red', label = 'Real BTC Price')
plt.plot(predicted_BTC_price, color = 'blue', label = 'Predicted BTC Price')
plt.title('BTC Price Prediction', fontsize=40)
df_test = df_test.reset_index()
x=df_test.index
labels = df_test['date']
plt.xticks(x, labels, rotation = 'vertical')
for tick in ax.xaxis.get_major_ticks():
tick.label1.set_fontsize(18)
for tick in ax.yaxis.get_major_ticks():
tick.label1.set_fontsize(18)
plt.xlabel('Time', fontsize=40)
plt.ylabel('BTC Price(USD)', fontsize=40)
plt.legend(loc=2, prop={'size': 25})
plt.show()

图 9.3:实际价格和预测价格随时间变化
总体而言,很明显,即使是一个简单的模型也能生成合理的预测——但有一个重要的警告:这种方法仅在环境保持稳定的情况下有效,即过去和现在的值之间的关系随着时间的推移保持稳定。如果出现体制变化或突发干预,价格可能会受到显著影响,例如,如果某个主要司法管辖区限制了加密货币的使用(正如过去十年所发生的情况)。这种情况可以建模,但需要更复杂的特征工程方法,并且超出了本章的范围。
开放领域问答
问答(QA)系统旨在模拟人类在网上搜索信息的过程,并通过机器学习方法提高提供答案的准确性。在这个示例中,我们将演示如何使用 RNN 预测关于维基百科文章的长短答案。我们将使用 Google 的自然问题数据集,并且在ai.google.com/research/NaturalQuestions/visualization上可以找到一个非常好的可视化工具,帮助理解 QA 背后的思想。
基本思想可以总结如下:对于每一对文章-问题对,你必须预测/选择从文章中直接提取的长答案和短答案:
-
长答案是指回答问题的较长一段文字——通常是几句话或一段话。
-
简短的回答可能是一个句子或短语,甚至在某些情况下是简单的 YES/NO。简短的答案总是包含在或作为某个合理长答案的子集。
-
给定的文章可以(并且通常会)允许长和短答案,具体取决于问题。
本章所展示的配方改编自 Xing Han Lu 公开的代码:www.kaggle.com/xhlulu。
如何做...
正如往常一样,我们首先加载必要的包。这次我们使用 fasttext 嵌入来表示(可以从fasttext.cc/获取)。其他流行的选择包括 GloVe(在情感分析部分使用)和 ELMo(allennlp.org/elmo)。在 NLP 任务的性能方面没有明显的优劣之分,所以我们会根据需要切换选择,以展示不同的可能性:
import os
import json
import gc
import pickle
import numpy as np
import pandas as pd
from tqdm import tqdm_notebook as tqdm
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense, Embedding, SpatialDropout1D, concatenate, Masking
from tensorflow.keras.layers import LSTM, Bidirectional, GlobalMaxPooling1D, Dropout
from tensorflow.keras.preprocessing import text, sequence
from tqdm import tqdm_notebook as tqdm
import fasttext
from tensorflow.keras.models import load_model
一般设置如下:
embedding_path = '/kaggle/input/fasttext-crawl-300d-2m-with-subword/crawl-300d-2m-subword/crawl-300d-2M-subword.bin'
我们的下一步是添加一些样板代码,以便之后简化代码流。由于当前任务比之前的任务稍微复杂一些(或不太直观),我们将更多的准备工作封装在数据集构建函数中。由于数据集的大小,我们仅加载训练数据的一个子集,并从中采样带有负标签的数据:
def build_train(train_path, n_rows=200000, sampling_rate=15):
with open(train_path) as f:
processed_rows = []
for i in tqdm(range(n_rows)):
line = f.readline()
if not line:
break
line = json.loads(line)
text = line['document_text'].split(' ')
question = line['question_text']
annotations = line['annotations'][0]
for i, candidate in enumerate(line['long_answer_candidates']):
label = i == annotations['long_answer']['candidate_index']
start = candidate['start_token']
end = candidate['end_token']
if label or (i % sampling_rate == 0):
processed_rows.append({
'text': " ".join(text[start:end]),
'is_long_answer': label,
'question': question,
'annotation_id': annotations['annotation_id']
})
train = pd.DataFrame(processed_rows)
return train
def build_test(test_path):
with open(test_path) as f:
processed_rows = []
for line in tqdm(f):
line = json.loads(line)
text = line['document_text'].split(' ')
question = line['question_text']
example_id = line['example_id']
for candidate in line['long_answer_candidates']:
start = candidate['start_token']
end = candidate['end_token']
processed_rows.append({
'text': " ".join(text[start:end]),
'question': question,
'example_id': example_id,
'sequence': f'{start}:{end}'
})
test = pd.DataFrame(processed_rows)
return test
使用下一个函数,我们训练一个 Keras tokenizer,将文本和问题编码成整数列表(分词),然后将它们填充到固定长度,形成一个用于文本的单一 NumPy 数组,另一个用于问题:
def compute_text_and_questions(train, test, tokenizer):
train_text = tokenizer.texts_to_sequences(train.text.values)
train_questions = tokenizer.texts_to_sequences(train.question.values)
test_text = tokenizer.texts_to_sequences(test.text.values)
test_questions = tokenizer.texts_to_sequences(test.question.values)
train_text = sequence.pad_sequences(train_text, maxlen=300)
train_questions = sequence.pad_sequences(train_questions)
test_text = sequence.pad_sequences(test_text, maxlen=300)
test_questions = sequence.pad_sequences(test_questions)
return train_text, train_questions, test_text, test_questions
与基于 RNN 的 NLP 模型一样,我们需要一个嵌入矩阵:
def build_embedding_matrix(tokenizer, path):
embedding_matrix = np.zeros((tokenizer.num_words + 1, 300))
ft_model = fasttext.load_model(path)
for word, i in tokenizer.word_index.items():
if i >= tokenizer.num_words - 1:
break
embedding_matrix[i] = ft_model.get_word_vector(word)
return embedding_matrix
接下来是我们的模型构建步骤,用一个函数包装起来:
-
我们构建了两个 2 层的双向 LSTM,一个用于读取问题,另一个用于读取文本。
-
我们将输出连接起来,并将其传递到一个全连接层:
-
我们在输出上使用 sigmoid:
def build_model(embedding_matrix):
embedding = Embedding(
*embedding_matrix.shape,
weights=[embedding_matrix],
trainable=False,
mask_zero=True
)
q_in = Input(shape=(None,))
q = embedding(q_in)
q = SpatialDropout1D(0.2)(q)
q = Bidirectional(LSTM(100, return_sequences=True))(q)
q = GlobalMaxPooling1D()(q)
t_in = Input(shape=(None,))
t = embedding(t_in)
t = SpatialDropout1D(0.2)(t)
t = Bidirectional(LSTM(150, return_sequences=True))(t)
t = GlobalMaxPooling1D()(t)
hidden = concatenate([q, t])
hidden = Dense(300, activation='relu')(hidden)
hidden = Dropout(0.5)(hidden)
hidden = Dense(300, activation='relu')(hidden)
hidden = Dropout(0.5)(hidden)
out1 = Dense(1, activation='sigmoid')(hidden)
model = Model(inputs=[t_in, q_in], outputs=out1)
model.compile(loss='binary_crossentropy', optimizer='adam')
return model
使用我们定义的工具包,我们可以构建数据集,具体如下:
directory = '../input/tensorflow2-question-answering/'
train_path = directory + 'simplified-nq-train.jsonl'
test_path = directory + 'simplified-nq-test.jsonl'
train = build_train(train_path)
test = build_test(test_path)
这就是数据集的样子:
train.head()

tokenizer = text.Tokenizer(lower=False, num_words=80000)
for text in tqdm([train.text, test.text, train.question, test.question]):
tokenizer.fit_on_texts(text.values)
train_target = train.is_long_answer.astype(int).values
train_text, train_questions, test_text, test_questions = compute_text_and_questions(train, test, tokenizer)
del train
现在我们可以构建模型本身:
embedding_matrix = build_embedding_matrix(tokenizer, embedding_path)
model = build_model(embedding_matrix)
model.summary()
Model: "functional_1"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_1 (InputLayer) [(None, None)] 0
__________________________________________________________________________________________________
input_2 (InputLayer) [(None, None)] 0
__________________________________________________________________________________________________
embedding (Embedding) (None, None, 300) 24000300 input_1[0][0]
input_2[0][0]
__________________________________________________________________________________________________
spatial_dropout1d (SpatialDropo (None, None, 300) 0 embedding[0][0]
__________________________________________________________________________________________________
spatial_dropout1d_1 (SpatialDro (None, None, 300) 0 embedding[1][0]
__________________________________________________________________________________________________
bidirectional (Bidirectional) (None, None, 200) 320800 spatial_dropout1d[0][0]
__________________________________________________________________________________________________
bidirectional_1 (Bidirectional) (None, None, 300) 541200 spatial_dropout1d_1[0][0]
__________________________________________________________________________________________________
global_max_pooling1d (GlobalMax (None, 200) 0 bidirectional[0][0]
__________________________________________________________________________________________________
global_max_pooling1d_1 (GlobalM (None, 300) 0 bidirectional_1[0][0]
__________________________________________________________________________________________________
concatenate (Concatenate) (None, 500) 0 global_max_pooling1d[0][0]
global_max_pooling1d_1[0][0]
__________________________________________________________________________________________________
dense (Dense) (None, 300) 150300 concatenate[0][0]
__________________________________________________________________________________________________
dropout (Dropout) (None, 300) 0 dense[0][0]
__________________________________________________________________________________________________
dense_1 (Dense) (None, 300) 90300 dropout[0][0]
__________________________________________________________________________________________________
dropout_1 (Dropout) (None, 300) 0 dense_1[0][0]
__________________________________________________________________________________________________
dense_2 (Dense) (None, 1) 301 dropout_1[0][0]
==================================================================================================
Total params: 25,103,201
Trainable params: 1,102,901
Non-trainable params: 24,000,300
__________________________________________________________________________________________________
接下来的步骤是拟合,按照通常的方式进行:
train_history = model.fit(
[train_text, train_questions],
train_target,
epochs=2,
validation_split=0.2,
batch_size=1024
)
现在,我们可以构建一个测试集,来查看我们生成的答案:
directory = '/kaggle/input/tensorflow2-question-answering/'
test_path = directory + 'simplified-nq-test.jsonl'
test = build_test(test_path)
submission = pd.read_csv("../input/tensorflow2-question-answering/sample_submission.csv")
test_text, test_questions = compute_text_and_questions(test, tokenizer)
我们生成实际的预测:
test_target = model.predict([test_text, test_questions], batch_size=512)
test['target'] = test_target
result = (
test.query('target > 0.3')
.groupby('example_id')
.max()
.reset_index()
.loc[:, ['example_id', 'PredictionString']]
)
result.head()

正如你所看到的,LSTM 使我们能够处理一些相当抽象的任务,比如回答不同类型的问题。这个配方的核心工作是在数据格式化为合适的输入格式,并在之后对结果进行后处理——实际的建模过程与前几章中的方法非常相似。
总结
在本章中,我们展示了 RNN 的不同功能。它们能够在一个统一的框架内处理多种具有顺序性任务(文本生成与分类、时间序列预测以及问答)。在下一章,我们将介绍 transformer:这一重要的架构类别使得在自然语言处理问题上取得了新的最先进成果。
第十章:Transformers
Transformers 是 Google 在 2017 年提出的深度学习架构,旨在处理序列数据,用于下游任务,如翻译、问答或文本摘要。这样,它们旨在解决与第九章《循环神经网络》中讨论的 RNNs 类似的问题,但 Transformers 具有显著优势,因为它们不需要按顺序处理数据。除此之外,这使得更高程度的并行化成为可能,从而加速了训练过程。
由于其灵活性,Transformers 可以在大量未标记的数据上进行预训练,然后再针对其他任务进行微调。这些预训练模型的两大主要类型是双向编码器表示的 Transformers(BERT)和生成预训练 Transformers(GPT)。
在本章中,我们将涵盖以下主题:
-
文本生成
-
情感分析
-
文本分类:讽刺检测
-
问答
我们将首先展示 GPT-2 的文本生成能力——这是最受广大用户使用的 Transformers 架构之一。虽然情感分析也可以由 RNNs 处理(如前一章所示),但正是生成能力最能清晰地展示 Transformers 在自然语言处理中的影响。
文本生成
第一个 GPT 模型是在 2018 年由 OpenAI 的 Radford 等人发布的论文中介绍的——它展示了生成性语言模型如何通过在大量多样的连续文本语料库上进行预训练,获得知识并处理长程依赖关系。随后几年发布了两个继任模型(在更大语料库上训练):2019 年的 GPT-2(15 亿参数)和 2020 年的 GPT-3(1750 亿参数)。为了在演示能力和计算需求之间取得平衡,我们将使用 GPT-2——截至本文编写时,GPT-3 的 API 访问受到限制。
我们将通过展示如何基于给定提示生成自己的文本,来开始使用 GPT-2 模型,而不进行任何微调。
我们该如何进行?
我们将利用由 Hugging Face 创建的优秀 Transformers 库(huggingface.co/)。它抽象了构建过程中的多个组件,使我们能够专注于模型性能和预期表现。
像往常一样,我们首先加载所需的包:
#get deep learning basics
import tensorflow as tf
Transformers 库的一个优势——也是其流行的原因之一——是我们可以轻松下载特定模型(并且还可以定义合适的分词器):
from transformers import TFGPT2LMHeadModel, GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained("gpt2-large")
GPT2 = TFGPT2LMHeadModel.from_pretrained("gpt2-large", pad_token_id=tokenizer.eos_token_id)
通常,固定随机种子是一个好主意,以确保结果的可重复性:
# settings
#for reproducability
SEED = 34
tf.random.set_seed(SEED)
#maximum number of words in output text
MAX_LEN = 70
有关 Transformer 中解码器架构的详细描述,请参阅本节末尾的另见部分——现在,我们将重点关注解码方式,它是使用 GPT-2 模型时最重要的决策之一。下面,我们将回顾一些可用的方法。
使用贪心搜索,预测具有最高概率的单词作为序列中的下一个单词:
input_sequence = "There are times when I am really tired of people, but I feel lonely too."
一旦我们有了输入序列,就将其编码,然后调用decode方法:
# encode context the generation is conditioned on
input_ids = tokenizer.encode(input_sequence, return_tensors='tf')
# generate text until the output length (which includes the context length) reaches 70
greedy_output = GPT2.generate(input_ids, max_length = MAX_LEN)
print("Output:\n" + 100 * '-')
print(tokenizer.decode(greedy_output[0], skip_special_tokens = True))
Output:
----------------------------------------------------------------------------------------------------
There are times when I am really tired of people, but I feel lonely too. I feel like I'm alone in the world. I feel like I'm alone in my own body. I feel like I'm alone in my own mind. I feel like I'm alone in my own heart. I feel like I'm alone in my own mind
如你所见,结果仍有改进空间:模型开始自我重复,因为高概率词汇掩盖了低概率词汇,使其无法探索更多样化的组合。
一种简单的解决方法是束搜索:我们跟踪备选变体,从而使得更多的比较成为可能:
# set return_num_sequences > 1
beam_outputs = GPT2.generate(
input_ids,
max_length = MAX_LEN,
num_beams = 5,
no_repeat_ngram_size = 2,
num_return_sequences = 5,
early_stopping = True
)
print('')
print("Output:\n" + 100 * '-')
# now we have 5 output sequences
for i, beam_output in enumerate(beam_outputs):
print("{}: {}".format(i, tokenizer.decode(beam_output, skip_special_ tokens=True)))
Output:
----------------------------------------------------------------------------------------------------
0: There are times when I am really tired of people, but I feel lonely too. I don't know what to do with myself."
"I feel like I can't do anything right now," she said. "I'm so tired."
1: There are times when I am really tired of people, but I feel lonely too. I don't know what to do with myself."
"I feel like I can't do anything right now," she says. "I'm so tired."
2: There are times when I am really tired of people, but I feel lonely too. I don't know what to do with myself."
"I feel like I can't do anything right now," she says. "I'm not sure what I'm supposed to be doing with my life."
3: There are times when I am really tired of people, but I feel lonely too. I don''t know what to do with myself.""
"I feel like I can't do anything right now," she says. "I'm not sure what I'm supposed to be doing."
4: There are times when I am really tired of people, but I feel lonely too. I don't know what to do with myself."
"I feel like I can't do anything right now," she says. "I'm not sure what I should do."
这确实更加多样化——信息相同,但至少从风格上来看,表达方式有所不同。
接下来,我们可以探索采样——不确定性解码。我们并不严格按照路径来找到具有最高概率的最终文本,而是根据条件概率分布随机选择下一个单词。这个方法有可能生成不连贯的胡言乱语,因此我们使用temperature参数,它会影响概率质量分布:
# use temperature to decrease the sensitivity to low probability candidates
sample_output = GPT2.generate(
input_ids,
do_sample = True,
max_length = MAX_LEN,
top_k = 0,
temperature = 0.2
)
print("Output:\n" + 100 * '-')
print(tokenizer.decode(sample_output[0], skip_special_tokens = True))
Output:
----------------------------------------------------------------------------------------------------
There are times when I am really tired of people, but I feel lonely too. I feel like I'm alone in my own world. I feel like I'm alone in my own life. I feel like I'm alone in my own mind. I feel like I'm alone in my own heart. I feel like I'm alone in my own
稍微有点诗意地说,若我们提高温度,会发生什么呢?
sample_output = GPT2.generate(
input_ids,
do_sample = True,
max_length = MAX_LEN,
top_k = 0,
temperature = 0.8
)
print("Output:\n" + 100 * '-')
print(tokenizer.decode(sample_output[0], skip_special_tokens = True))
Output:
----------------------------------------------------------------------------------------------------
There are times when I am really tired of people, but I feel lonely too. I find it strange how the people around me seem to be always so nice. The only time I feel lonely is when I'm on the road. I can't be alone with my thoughts.
What are some of your favourite things to do in the area
这变得更加有趣,尽管它仍然有点像思路流——这或许是可以预见的,考虑到我们提示词的内容。让我们探索更多调优输出的方法。
在Top-K 采样中,选择最有可能的前k个单词,并将整个概率质量转移到这k个单词上。因此,我们并不增加高概率词汇出现的机会或减少低概率词汇的机会,而是直接将低概率词汇完全移除:
#sample from only top_k most likely words
sample_output = GPT2.generate(
input_ids,
do_sample = True,
max_length = MAX_LEN,
top_k = 50
)
print("Output:\n" + 100 * '-')
print(tokenizer.decode(sample_output[0], skip_special_tokens = True), '...')
Output:
----------------------------------------------------------------------------------------------------
There are times when I am really tired of people, but I feel lonely too. I go to a place where you can feel comfortable. It's a place where you can relax. But if you're so tired of going along with the rules, maybe I won't go. You know what? Maybe if I don't go, you won''t ...
这似乎是朝着正确方向迈出的步伐。我们能做得更好吗?
Top-P 采样(也叫做核采样)类似于 Top-K 采样,但不是选择最有可能的前k个单词,而是选择概率总和大于p的最小单词集合,然后将整个概率质量转移到该集合中的单词上。这里的主要区别是,Top-K 采样中,单词集合的大小是静态的(显然),而在 Top-P 采样中,集合的大小可以变化。要使用这种采样方法,我们只需设置top_k = 0并选择一个top_p值:
#sample only from 80% most likely words
sample_output = GPT2.generate(
input_ids,
do_sample = True,
max_length = MAX_LEN,
top_p = 0.8,
top_k = 0
)
print("Output:\n" + 100 * '-')
print(tokenizer.decode(sample_output[0], skip_special_tokens = True), '...')
Output:
----------------------------------------------------------------------------------------------------
There are times when I am really tired of people, but I feel lonely too. I feel like I should just be standing there, just sitting there. I know I'm not a danger to anybody. I just feel alone." ...
我们可以结合这两种方法:
#combine both sampling techniques
sample_outputs = GPT2.generate(
input_ids,
do_sample = True,
max_length = 2*MAX_LEN, #to test how long we can generate and it be coherent
#temperature = .7,
top_k = 50,
top_p = 0.85,
num_return_sequences = 5
)
print("Output:\n" + 100 * '-')
for i, sample_output in enumerate(sample_outputs):
print("{}: {}...".format(i, tokenizer.decode(sample_output, skip_ special_tokens = True)))
print('')
Output:
----------------------------------------------------------------------------------------------------
0: There are times when I am really tired of people, but I feel lonely too. I don't feel like I am being respected by my own country, which is why I am trying to change the government."
In a recent video posted to YouTube, Mr. Jaleel, dressed in a suit and tie, talks about his life in Pakistan and his frustration at his treatment by the country's law enforcement agencies. He also describes how he met a young woman from California who helped him organize the protest in Washington.
"She was a journalist who worked with a television channel in Pakistan," Mr. Jaleel says in the video. "She came to my home one day,...
1: There are times when I am really tired of people, but I feel lonely too. It's not that I don't like to be around other people, but it's just something I have to face sometimes.
What is your favorite thing to eat?
The most favorite thing I have eaten is chicken and waffles. But I love rice, soups, and even noodles. I also like to eat bread, but I like it a little bit less.
What is your ideal day of eating?
It varies every day. Sometimes I want to eat at home, because I'm in a house with my family. But then sometimes I just have to have some sort...
2: There are times when I am really tired of people, but I feel lonely too. I think that there is something in my heart that is trying to be a better person, but I don't know what that is."
So what can be done?
"I want people to take the time to think about this," says Jorja, who lives in a small town outside of Boston.
She has been thinking a lot about her depression. She wants to make a documentary about it, and she wants to start a blog about it.
"I want to make a video to be a support system for people who are going through the same thing I was going through...
3: There are times when I am really tired of people, but I feel lonely too.
I want to be able to take good care of myself. I am going to be a very good person, even if I am lonely.
So, if it's lonely, then I will be happy. I will be a person who will be able to have good care of myself.
I have made this wish.
What is my hope? What is my goal? I want to do my best to be able to meet it, but…
"Yuu, what are you saying, Yuu?"
"Uwa, what is it?"
I...
4: There are times when I am really tired of people, but I feel lonely too. The only person I really love is my family. It's just that I'm not alone."
-Juan, 24, a student
A study from the European Economic Area, a free trade area between the EU and Iceland, showed that there are 2.3 million EU citizens living in Iceland. Another survey in 2014 showed that 1.3 million people in Iceland were employed.
The government is committed to making Iceland a country where everyone can live and work.
"We are here to help, not to steal," said one of the people who drove up in a Volkswagen.
...
显然,更复杂的方法设置可以给我们带来相当令人印象深刻的结果。让我们进一步探索这个方向——我们将使用从 OpenAI 的 GPT-2 网站上获取的提示词,并将其输入完整的 GPT-2 模型。这种对比将让我们了解本地(较小)模型与用于原始演示的大型模型之间的表现差异:
MAX_LEN = 500
prompt1 = 'In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.'
input_ids = tokenizer.encode(prompt1, return_tensors='tf')
sample_outputs = GPT2.generate(
input_ids,
do_sample = True,
max_length = MAX_LEN, #to test how long we can generate and it be coherent
#temperature = .8,
top_k = 50,
top_p = 0.85
#num_return_sequences = 5
)
print("Output:\n" + 100 * '-')
for i, sample_output in enumerate(sample_outputs):
print("{}: {}...".format(i, tokenizer.decode(sample_output, skip_ special_tokens = True)))
print('')
Output:
----------------------------------------------------------------------------------------------------
0: In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.
This is the first time a herd of unicorns have been discovered in the Andes Mountains, a vast region stretching from the Himalayas to the Andes River in Bolivia.
According to the BBC, the unicorns were spotted by a small group of researchers on a private expedition, but they were the only ones that came across the bizarre creatures.
It was later learned that these were not the wild unicorns that were spotted in the wild in recent years, but rather a domesticated variety of the species.
Although they do not speak English, they do carry their own unique language, according to the researchers, who have named it "Ungla."
The herd of unicorns, which was discovered by a small group of researchers, is the first of its kind discovered in the Andes Mountains. It is thought that the herd of wild unicorns were introduced to the area hundreds of years ago by a local rancher who was attempting to make a profit from the animals.
Although they do not speak English, they do carry their own unique language, according to the researchers, who have named it "Ungla."
The researchers claim that the unicorns have only been sighted in the Andes Mountains, where they can be seen throughout the mountains of South America.
While the unicorns do not speak English, they do carry their own unique language, according to the researchers, who have named it "Ungla."
Ungla is a highly intelligent, cooperative species with a high level of social and cognitive complexity, and is capable of displaying sophisticated behaviors.
They are a particularly unique species, because they are capable of surviving in extreme conditions for long periods of time and without being fed or watered.
The team believes that the species was probably domesticated in the Andes Mountains, where it could not survive in its natural habitat.
"We can see from the genetics that the animals were probably domesticated in the Andes Mountains where they could not survive in their natural habitat and with water and food sources," said Professor David Catt, from the University of Cambridge, who led the study.
"So these were animals that would have been...
作为对比,这是完整模型的输出:
输出:
0: 在一项 震惊的发现中,科学家们在安第斯山脉的一个偏远、以前未被探索的山谷中发现了一群独角兽。更令研究人员惊讶的是,这些独角兽能说一口流利的英语。
“这不仅是一个科学发现,它也是一个非常重要的发现,因为它将使我们能够进一步研究这个 现象,”哥伦比亚国家人类学与历史研究所(INAH)的乔治·利亚马斯博士在一份声明中说道。
“我们之前发现人类曾用人类的声音与动物交流。在这种情况下,动物正在与我们交流。换句话说,这是动物交流领域的一个突破,”利亚马斯补充道……
在另一个例子中,似乎模型作者的担忧是有道理的:GPT-2 确实能够生成假新闻故事。
prompt2 = 'Miley Cyrus was caught shoplifting from Abercrombie and Fitch on Hollywood Boulevard today.'
input_ids = tokenizer.encode(prompt2, return_tensors='tf')
sample_outputs = GPT2.generate(
input_ids,
do_sample = True,
max_length = MAX_LEN, #to test how long we can generate and it be coherent
#temperature = .8,
top_k = 50,
top_p = 0.85
#num_return_sequences = 5
)
print("Output:\n" + 100 * '-')
for i, sample_output in enumerate(sample_outputs):
print("{}: {}...".format(i, tokenizer.decode(sample_output, skip_ special_tokens = True)))
print('')
Output:
----------------------------------------------------------------------------------------------------
0: Miley Cyrus was caught shoplifting from Abercrombie and Fitch on Hollywood Boulevard today. In a video captured by one of her friends, the singer is seen grabbing her bag, but then quickly realizing the merchandise she has to leave is too expensive to be worth a $1.99 purchase.
The video has already gone viral, and while the celebrity is certainly guilty of breaking the law (even if she can't be accused of stealing for a second time), there's one aspect of the situation that should make the most sense. It's just like the shopping situation in the movie The Fast and the Furious, where Michael Corleone is caught in possession of counterfeit designer clothing.
This time around, though, the situation involves Cyrus. It's not a copy, per se. It's actually a replica, a pair of a black and white Nike Air Force 1s, a colorway she wore in her music video.
It seems that the actress is caught by a friend who had gotten her a pair of those sneakers when she was in school, so this is no surprise to her. After all, there was a video of her stealing from her own store back in 2012, when she was a freshman at New York University.
It's not that there's anything wrong with the product. If the merchandise is in good shape, that's all that matters. But there are a few things that should come to mind when it comes to these shoes.
For one, the fabric is incredibly thin. The fabric is so thin that the upper actually makes the shoes look like they're made of leather. There's even a thin layer of plastic between the upper and the shoe.
Secondly, the material isn't even a shoe. It's just a piece of leather. It's not actually a leather shoe at all, even though it's made of the same material as the other items on the show. It's just a piece of leather. And it's not the kind of leather that would actually hold up in a fight.
This is something that should be familiar to anyone who's ever shopped at the store. If you go into the store looking for a pair of new Nike Air Force 1s, and the salesperson is just selling you a piece of leather, you're going to get disappointed. That's the nature of these shoes.
In addition to the aforementioned "stolen" footwear, Miley Cyrus...
输出:
0:麦莉·赛勒斯今天在好莱坞大道上被抓到从阿贝克朗比和费奇商店偷东西。有人看到她试穿了三件裙子,然后试图走出商店。
阿贝克朗比是这位明星常去的几家店之一。
这位歌手在今天中午过后被看到走进位于西好莱坞的阿贝克朗比与费奇商店,随后离开了商店。
这位明星目前正在澳大利亚和新西兰进行巡演,参加 2 月 28 日的《X Factor》演出……
那么,像托尔金这样的文学经典,怎么样?
prompt3 = 'Legolas and Gimli advanced on the orcs, raising their weapons with a harrowing war cry'
input_ids = tokenizer.encode(prompt3, return_tensors='tf')
sample_outputs = GPT2.generate(
input_ids,
do_sample = True,
max_length = MAX_LEN, #to test how long we can generate and it be coherent
#temperature = .8,
top_k = 50,
top_p = 0.85
#num_return_sequences = 5
)
print("Output:\n" + 100 * '-')
for i, sample_output in enumerate(sample_outputs):
print("{}: {}...".format(i, tokenizer.decode(sample_output, skip_ special_tokens = True)))
print('')
Output:
----------------------------------------------------------------------------------------------------
0: Legolas and Gimli advanced on the orcs, raising their weapons with a harrowing war cry, and they roared their battle cries as they charged the orcs with their spears and arrows. They reached the front of the line, where the enemy were gathered, and they fell upon them with a hail of fire and arrows, slaying many orcs and wounding others. The battle raged on for a long time, and eventually the two sides met and fought for a long time more. The orcs fell and the two armies were victorious. The orcs were killed and the two armies were victorious.
The two armies fought one last time in battle. Gimli slew many of the orcs and led his men to safety. They went to the city and took it. When they returned, Sauron's servants were waiting to kill them. The two armies fought again, and the battle raged on for a long time more. Gimli slew many of the orcs and led his men to safety. They went to the city and took it. When they returned, Sauron's servants were waiting to kill them. The two armies fought again, and the battle raged on for a long time more. Gimli slew many of the orcs and led his men to safety. They went to the city and took it. When they returned, Sauron's servants were waiting to kill them. The two armies fought again, and the battle raged on for a long time more. Gimli slew many of the orcs and led his men to safety. They went to the city and took it. When they returned, Sauron's servants were waiting to kill them. The two armies fought again, and the battle raged on for a long time more. Gimli slew many of the orcs and led his men to safety. They went to the city and took it. When they returned, Sauron's servants were waiting to kill them. The two armies fought again, and the battle raged on for a long time more. Gimli slew many of the orcs and led his men to safety. They went to the city and took it. When they returned, Sauron's servants were waiting to kill them. The two armies fought again, and the battle raged on for a long time more. Gimli slew many of the orcs and led his men to safety. They went to the city and took it. When they returned, Sauron's servants were waiting to kill them. The two armies fought again, and the battle raged on for a...
输出:
0:莱戈拉斯和金雳带着一声刺耳的战吼向兽人进军,举起了他们的武器。
然后,兽人 发动了进攻。
伟大的兽人战争团在战斗声中前进。他们骄傲地佩戴着武器,俯视着他们的敌人。
远处,可以听见兽人低声喊着命令。
但战斗还没有结束。兽人的斧头和战锤猛烈地击打敌军,就像他们是一支万人的军队一样,斧头砍中了兽人,使他们流血。
在这场大屠杀中,精灵领袖阿拉贡大喊道:“来吧,勇士们,让我们与兽人战斗!”
如上面的例子所示,一个未经微调的 GPT-2 模型(即开箱即用)已经能够生成看似合理的长篇文本。评估这种技术未来对传播领域的影响仍然是一个开放且极具争议的问题:一方面,对于假新闻泛滥的恐惧是完全有理由的(见上面的麦莉·赛勒斯故事)。这尤其令人担忧,因为大规模自动检测生成文本是一个极具挑战性的话题。另一方面,GPT-2 的文本生成能力对于创意工作者可能非常有帮助:无论是风格实验还是讽刺,一个由 AI 驱动的写作助手可以提供巨大的帮助。
另见
网络上有多个关于 GPT-2 文本生成的优秀资源:
-
介绍该模型的原始 OpenAI 文章:
-
顶级 GPT-2 开源项目:
-
Hugging Face 文档:
情感分析
在本节中,我们将演示如何使用 DistilBERT(BERT 的轻量版本)来处理情感分析中的常见问题。我们将使用 Kaggle 比赛中的数据(www.kaggle.com/c/tweet-sentiment-extraction):给定一条推文及其情感(积极、中立或消极),参与者需要识别出定义该情感的推文部分。情感分析通常在商业中应用,作为帮助数据分析师评估公众舆论、进行详细市场调研和跟踪客户体验的系统的一部分。一个重要的应用领域是医疗:可以根据患者的交流模式评估不同治疗对其情绪的影响。
我们该如何进行?
和往常一样,我们首先加载必要的包。
import pandas as pd
import re
import numpy as np
np.random.seed(0)
import matplotlib.pyplot as plt
%matplotlib inline
import keras
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Input, Dense, LSTM, GRU, Embedding
from keras.layers import Activation, Bidirectional, GlobalMaxPool1D, GlobalMaxPool2D, Dropout
from keras.models import Model
from keras import initializers, regularizers, constraints, optimizers, layers
from keras.preprocessing import text, sequence
from keras.callbacks import ModelCheckpoint
from keras.callbacks import EarlyStopping
from keras.optimizers import RMSprop, adam
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer,PorterStemmer
import seaborn as sns
import transformers
from transformers import AutoTokenizer
from tokenizers import BertWordPieceTokenizer
from keras.initializers import Constant
from keras.wrappers.scikit_learn import KerasClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import accuracy_score
from collections import Counter
stop=set(stopwords.words('english'))
import os
为了简化代码,我们定义了一些帮助函数来清理文本:去除网站链接、星号遮蔽的 NSFW 词汇和表情符号。
def basic_cleaning(text):
text=re.sub(r'https?://www\.\S+\.com','',text)
text=re.sub(r'[^A-Za-z|\s]','',text)
text=re.sub(r'\*+','swear',text) #capture swear words that are **** out
return text
def remove_html(text):
html=re.compile(r'<.*?>')
return html.sub(r'',text)
# Reference : https://gist.github.com/slowkow/7a7f61f495e3dbb7e3d767f97bd7304b
def remove_emoji(text):
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F" # emoticons
u"\U0001F300-\U0001F5FF" # symbols & pictographs
u"\U0001F680-\U0001F6FF" # transport & map symbols
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
u"\U00002702-\U000027B0"
u"\U000024C2-\U0001F251"
"]+", flags=re.UNICODE)
return emoji_pattern.sub(r'', text)
def remove_multiplechars(text):
text = re.sub(r'(.)\1{3,}',r'\1', text)
return text
def clean(df):
for col in ['text']:#,'selected_text']:
df[col]=df[col].astype(str).apply(lambda x:basic_cleaning(x))
df[col]=df[col].astype(str).apply(lambda x:remove_emoji(x))
df[col]=df[col].astype(str).apply(lambda x:remove_html(x))
df[col]=df[col].astype(str).apply(lambda x:remove_multiplechars(x))
return df
def fast_encode(texts, tokenizer, chunk_size=256, maxlen=128):
tokenizer.enable_truncation(max_length=maxlen)
tokenizer.enable_padding(max_length=maxlen)
all_ids = []
for i in range(0, len(texts), chunk_size):
text_chunk = texts[i:i+chunk_size].tolist()
encs = tokenizer.encode_batch(text_chunk)
all_ids.extend([enc.ids for enc in encs])
return np.array(all_ids)
def preprocess_news(df,stop=stop,n=1,col='text'):
'''Function to preprocess and create corpus'''
new_corpus=[]
stem=PorterStemmer()
lem=WordNetLemmatizer()
for text in df[col]:
words=[w for w in word_tokenize(text) if (w not in stop)]
words=[lem.lemmatize(w) for w in words if(len(w)>n)]
new_corpus.append(words)
new_corpus=[word for l in new_corpus for word in l]
return new_corpus
加载数据。
df = pd.read_csv('/kaggle/input/tweet-sentiment-extraction/train.csv')
df.head()

图 10.1:推文情感分析数据示例
上面的快照展示了我们将关注分析的一个数据样本:完整文本、关键短语及其相关的情感(积极、消极或中立)。
我们继续进行相对标准的数据预处理:
-
basic_cleaning– 用于去除网站 URL 和非字符内容,并将*脏话替换为 swear 一词。 -
remove_html。 -
remove_emojis。 -
remove_multiplechars– 该函数用于处理当一个单词中有超过 3 个连续字符时,例如,wayyyyy。该函数会去除其中的多余字母,保留一个。
df.dropna(inplace=True)
df_clean = clean(df)
对于标签,我们进行独热编码,将它们分词,并转换为序列。
df_clean_selection = df_clean.sample(frac=1)
X = df_clean_selection.text.values
y = pd.get_dummies(df_clean_selection.sentiment)
tokenizer = text.Tokenizer(num_words=20000)
tokenizer.fit_on_texts(list(X))
list_tokenized_train = tokenizer.texts_to_sequences(X)
X_t = sequence.pad_sequences(list_tokenized_train, maxlen=128)
DistilBERT 是 BERT 的轻量版本:它的参数比 BERT 少 40%,但性能达到 BERT 的 97%。对于本例,我们主要使用它的分词器和嵌入矩阵。虽然该矩阵是可训练的,但为了减少训练时间,我们不使用这个选项。
tokenizer = transformers.AutoTokenizer.from_pretrained("distilbert-base-uncased") ## change it to commit
# Save the loaded tokenizer locally
save_path = '/kaggle/working/distilbert_base_uncased/'
if not os.path.exists(save_path):
os.makedirs(save_path)
tokenizer.save_pretrained(save_path)
# Reload it with the huggingface tokenizers library
fast_tokenizer = BertWordPieceTokenizer('distilbert_base_uncased/vocab.txt', lowercase=True)
fast_tokenizer
X = fast_encode(df_clean_selection.text.astype(str), fast_tokenizer, maxlen=128)
transformer_layer = transformers.TFDistilBertModel.from_pretrained('distilbert-base-uncased')
embedding_size = 128 input_ = Input(shape=(100,))
inp = Input(shape=(128, ))
embedding_matrix=transformer_layer.weights[0].numpy()
x = Embedding(embedding_matrix.shape[0], embedding_matrix.shape[1],embeddings_initializer=Constant(embedding_matrix),trainable=False)(inp)
我们按常规步骤定义模型。
x = Bidirectional(LSTM(50, return_sequences=True))(x)
x = Bidirectional(LSTM(25, return_sequences=True))(x)
x = GlobalMaxPool1D()(x) x = Dropout(0.5)(x)
x = Dense(50, activation='relu', kernel_regularizer='L1L2')(x)
x = Dropout(0.5)(x)
x = Dense(3, activation='softmax')(x)
model_DistilBert = Model(inputs=[inp], outputs=x)
model_DistilBert.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
model_DistilBert.summary()
Model: "model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) (None, 128) 0
_________________________________________________________________
embedding_1 (Embedding) (None, 128, 768) 23440896
_________________________________________________________________
bidirectional_1 (Bidirection (None, 128, 100) 327600
_________________________________________________________________
bidirectional_2 (Bidirection (None, 128, 50) 25200
_________________________________________________________________
global_max_pooling1d_1 (Glob (None, 50) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 50) 0
_________________________________________________________________
dense_1 (Dense) (None, 50) 2550
_________________________________________________________________
dropout_2 (Dropout) (None, 50) 0
_________________________________________________________________
dense_2 (Dense) (None, 3) 153
=================================================================
Total params: 23,796,399
Trainable params: 355,503
Non-trainable params: 23,440,896
_________________________________________________________________
现在我们可以拟合模型了:
model_DistilBert.fit(X,y,batch_size=32,epochs=10,validation_split=0.1)
Train on 24732 samples, validate on 2748 samples
Epoch 1/10
24732/24732 [==============================] - 357s 14ms/step - loss: 1.0516 - accuracy: 0.4328 - val_loss: 0.8719 - val_accuracy: 0.5466
Epoch 2/10
24732/24732 [==============================] - 355s 14ms/step - loss: 0.7733 - accuracy: 0.6604 - val_loss: 0.7032 - val_accuracy: 0.6776
Epoch 3/10
24732/24732 [==============================] - 355s 14ms/step - loss: 0.6668 - accuracy: 0.7299 - val_loss: 0.6407 - val_accuracy: 0.7354
Epoch 4/10
24732/24732 [==============================] - 355s 14ms/step - loss: 0.6310 - accuracy: 0.7461 - val_loss: 0.5925 - val_accuracy: 0.7478
Epoch 5/10
24732/24732 [==============================] - 347s 14ms/step - loss: 0.6070 - accuracy: 0.7565 - val_loss: 0.5817 - val_accuracy: 0.7529
Epoch 6/10
24732/24732 [==============================] - 343s 14ms/step - loss: 0.5922 - accuracy: 0.7635 - val_loss: 0.5817 - val_accuracy: 0.7584
Epoch 7/10
24732/24732 [==============================] - 343s 14ms/step - loss: 0.5733 - accuracy: 0.7707 - val_loss: 0.5922 - val_accuracy: 0.7638
Epoch 8/10
24732/24732 [==============================] - 343s 14ms/step - loss: 0.5547 - accuracy: 0.7832 - val_loss: 0.5767 - val_accuracy: 0.7627
Epoch 9/10
24732/24732 [==============================] - 346s 14ms/step - loss: 0.5350 - accuracy: 0.7870 - val_loss: 0.5767 - val_accuracy: 0.7584
Epoch 10/10
24732/24732 [==============================] - 346s 14ms/step - loss: 0.5219 - accuracy: 0.7955 - val_loss: 0.5994 - val_accuracy: 0.7580
从上述输出可以看出,模型收敛得相当快,并且在验证集上的准确率在 10 次迭代后已经达到了 76%。进一步微调超参数和更长时间的训练可以提高性能,但即使在这个水平,经过训练的模型——例如,通过 TensorFlow Serving——也能为商业应用中的情感分析逻辑提供有价值的补充。
另见
最好的起点是 Hugging Face 的文档:curiousily.com/posts/sentiment-analysis-with-bert-and-hugging-face-using-pytorch-and-python/。
开放领域问题回答
给定一段文本及与该文本相关的问题,问题回答(QA)的概念是确定回答该问题的文本子集。这是应用 Transformer 架构成功的许多任务之一。Transformers 库有多个预训练的 QA 模型,可以在没有数据集进行微调的情况下应用(这是一种零样本学习形式)。
然而,不同的模型在不同的示例上可能会失败,了解原因可能是有益的。在本节中,我们将展示 TensorFlow 2.0 的 GradientTape 功能:它允许我们记录对一组变量进行自动微分的操作。为了解释模型在给定输入上的输出,我们可以:
-
对输入进行一热编码——与整数令牌(通常在此上下文中使用)不同,一热编码表示是可微分的
-
实例化 GradientTape 并监视我们的输入变量
-
计算通过模型的前向传播
-
获取我们感兴趣的输出的梯度(例如,特定类别的 logit),相对于监视的输入
-
使用标准化梯度作为解释
本节中的代码改编自 Fast Forward Labs 发布的结果:experiments.fastforwardlabs.com/。
我们如何开始?
import os
import zipfile
import shutil
import urllib.request
import logging
import lzma
import json
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import time
import tensorflow as tf
from transformers import AutoTokenizer, TFAutoModelForQuestionAnswering, TFBertForMaskedLM, TFBertForQuestionAnswering
和往常一样,我们需要一些样板代码:首先编写一个用于获取预训练 QA 模型的函数。
def get_pretrained_squad_model(model_name):
model, tokenizer = None, None
if model_name == "distilbertsquad1":
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-cased-distilled-squad",use_fast=True)
model = TFBertForQuestionAnswering.from_pretrained("distilbert-base-cased-distilled-squad", from_pt=True)
elif model_name == "distilbertsquad2":
tokenizer = AutoTokenizer.from_pretrained("twmkn9/distilbert-base-uncased-squad2",use_fast=True)
model = TFAutoModelForQuestionAnswering.from_pretrained("twmkn9/distilbert-base-uncased-squad2", from_pt=True)
elif model_name == "bertsquad2":
tokenizer = AutoTokenizer.from_pretrained("deepset/bert-base-cased-squad2",use_fast=True)
model = TFBertForQuestionAnswering.from_pretrained("deepset/bert-base-cased-squad2", from_pt=True)
elif model_name == "bertlargesquad2":
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased",use_fast=True)
model = TFBertForQuestionAnswering.from_pretrained("deepset/bert-large-uncased-whole-word-masking-squad2", from_pt=True)
elif model_name == "albertbasesquad2":
tokenizer = AutoTokenizer.from_pretrained("twmkn9/albert-base-v2-squad2",use_fast=True)
model = TFBertForQuestionAnswering.from_pretrained("twmkn9/albert-base-v2-squad2", from_pt=True)
elif model_name == "distilrobertasquad2":
tokenizer = AutoTokenizer.from_pretrained("twmkn9/distilroberta-base-squad2",use_fast=True)
model = TFBertForQuestionAnswering.from_pretrained("twmkn9/
distilroberta-base-squad2", from_pt=True)
elif model_name == "robertasquad2":
tokenizer = AutoTokenizer.from_pretrained("deepset/roberta-base-squad2",use_fast=True)
model = TFAutoModelForQuestionAnswering.from_pretrained("deepset/roberta-base-squad2", from_pt=True)
elif model_name == "bertlm":
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased", use_fast=True)
model = TFBertForMaskedLM.from_pretrained("bert-base-uncased", from_pt=True)
return model, tokenizer
确定答案的范围。
def get_answer_span(question, context, model, tokenizer):
inputs = tokenizer.encode_plus(question, context, return_tensors="tf", add_special_tokens=True, max_length=512)
answer_start_scores, answer_end_scores = model(inputs)
answer_start = tf.argmax(answer_start_scores, axis=1).numpy()[0]
answer_end = (tf.argmax(answer_end_scores, axis=1) + 1).numpy()[0]
print(tokenizer.convert_tokens_to_string(inputs["input_ids"][0][answer_start:answer_end]))
return answer_start, answer_end
我们需要一些用于数据准备的函数。
def clean_tokens(gradients, tokens, token_types):
"""
Clean the tokens and gradients gradients
Remove "[CLS]","[CLR]", "[SEP]" tokens
Reduce (mean) gradients values for tokens that are split ##
"""
token_holder = []
token_type_holder = []
gradient_holder = []
i = 0
while i < len(tokens):
if (tokens[i] not in ["[CLS]","[CLR]", "[SEP]"]):
token = tokens[i]
conn = gradients[i]
token_type = token_types[i]
if i < len(tokens)-1 :
if tokens[i+1][0:2] == "##":
token = tokens[i]
conn = gradients[i]
j = 1
while i < len(tokens)-1 and tokens[i+1][0:2] == "##":
i +=1
token += tokens[i][2:]
conn += gradients[i]
j+=1
conn = conn /j
token_holder.append(token)
token_type_holder.append(token_type)
gradient_holder.append(conn)
i +=1
return gradient_holder,token_holder, token_type_holder
def get_best_start_end_position(start_scores, end_scores):
answer_start = tf.argmax(start_scores, axis=1).numpy()[0]
answer_end = (tf.argmax(end_scores, axis=1) + 1).numpy()[0]
return answer_start, answer_end
def get_correct_span_mask(correct_index, token_size):
span_mask = np.zeros((1, token_size))
span_mask[0, correct_index] = 1
span_mask = tf.constant(span_mask, dtype='float32')
return span_mask
def get_embedding_matrix(model):
if "DistilBert" in type(model).__name__:
return model.distilbert.embeddings.word_embeddings
else:
return model.bert.embeddings.word_embeddings
def get_gradient(question, context, model, tokenizer):
"""Return gradient of input (question) wrt to model output span prediction
Args:
question (str): text of input question
context (str): text of question context/passage
model (QA model): Hugging Face BERT model for QA transformers.modeling_tf_distilbert.TFDistilBertForQuestionAnswering, transformers.modeling_tf_bert.TFBertForQuestionAnswering
tokenizer (tokenizer): transformers.tokenization_bert.BertTokenizerFast
Returns:
(tuple): (gradients, token_words, token_types, answer_text)
"""
embedding_matrix = get_embedding_matrix(model)
encoded_tokens = tokenizer.encode_plus(question, context, add_special_tokens=True, return_token_type_ids=True, return_tensors="tf")
token_ids = list(encoded_tokens["input_ids"].numpy()[0])
vocab_size = embedding_matrix.get_shape()[0]
# convert token ids to one hot. We can't differentiate wrt to int token ids hence the need for one hot representation
token_ids_tensor = tf.constant([token_ids], dtype='int32')
token_ids_tensor_one_hot = tf.one_hot(token_ids_tensor, vocab_size)
with tf.GradientTape(watch_accessed_variables=False) as tape:
# (i) watch input variable
tape.watch(token_ids_tensor_one_hot)
# multiply input model embedding matrix; allows us do backprop wrt one hot input
inputs_embeds = tf.matmul(token_ids_tensor_one_hot,embedding_matrix)
# (ii) get prediction
start_scores,end_scores = model({"inputs_embeds": inputs_embeds, "token_type_ids": encoded_tokens["token_type_ids"], "attention_mask": encoded_tokens["attention_mask"] })
answer_start, answer_end = get_best_start_end_position(start_scores, end_scores)
start_output_mask = get_correct_span_mask(answer_start, len(token_ids))
end_output_mask = get_correct_span_mask(answer_end, len(token_ids))
# zero out all predictions outside of the correct span positions; we want to get gradients wrt to just these positions
predict_correct_start_token = tf.reduce_sum(start_scores * start_output_mask)
predict_correct_end_token = tf.reduce_sum(end_scores * end_output_mask)
# (iii) get gradient of input with respect to both start and end output
gradient_non_normalized = tf.norm(
tape.gradient([predict_correct_start_token, predict_correct_end_token], token_ids_tensor_one_hot),axis=2)
# (iv) normalize gradient scores and return them as "explanations"
gradient_tensor = (
gradient_non_normalized /
tf.reduce_max(gradient_non_normalized)
)
gradients = gradient_tensor[0].numpy().tolist()
token_words = tokenizer.convert_ids_to_tokens(token_ids)
token_types = list(encoded_tokens["token_type_ids"].numpy()[0])
answer_text = tokenizer.decode(token_ids[answer_start:answer_end])
return gradients, token_words, token_types,answer_text
def explain_model(question, context, model, tokenizer, explain_method = "gradient"):
if explain_method == "gradient":
return get_gradient(question, context, model, tokenizer)
最后进行绘图:
def plot_gradients(tokens, token_types, gradients, title):
""" Plot explanations
"""
plt.figure(figsize=(21,3))
xvals = [ x + str(i) for i,x in enumerate(tokens)]
colors = [ (0,0,1, c) for c,t in zip(gradients, token_types) ]
edgecolors = [ "black" if t==0 else (0,0,1, c) for c,t in zip(gradients, token_types) ]
# colors = [ ("r" if t==0 else "b") for c,t in zip(gradients, token_types) ]
plt.tick_params(axis='both', which='minor', labelsize=29)
p = plt.bar(xvals, gradients, color=colors, linewidth=1, edgecolor=edgecolors)
plt.title(title)
p=plt.xticks(ticks=[i for i in range(len(tokens))], labels=tokens, fontsize=12,rotation=90)
我们将比较一小部分模型在不同问题上的表现。
questions = [
{ "question": "what is the goal of the fourth amendment? ", "context": "The Fourth Amendment of the U.S. Constitution provides that '[t]he right of the people to be secure in their persons, houses, papers, and effects, against unreasonable searches and seizures, shall not be violated, and no Warrants shall issue, but upon probable cause, supported by Oath or affirmation, and particularly describing the place to be searched, and the persons or things to be seized.'The ultimate goal of this provision is to protect people's right to privacy and freedom from unreasonable intrusions by the government. However, the Fourth Amendment does not guarantee protection from all searches and seizures, but only those done by the government and deemed unreasonable under the law." },
{ "question": ""what is the taj mahal made of?", "context": "The Taj Mahal is an ivory-white marble mausoleum on the southern bank of the river Yamuna in the Indian city of Agra. It was commissioned in 1632 by the Mughal emperor Shah Jahan (reigned from 1628 to 1658) to house the tomb of his favourite wife, Mumtaz Mahal; it also houses the tomb of Shah Jahan himself. The tomb is the centrepiece of a 17-hectare (42-acre) complex, which includes a mosque and a guest house, and is set in formal gardens bounded on three sides by a crenellated wall. Construction of the mausoleum was essentially completed in 1643, but work continued on other phases of the project for another 10 years. The Taj Mahal complex is believed to have been completed in its entirety in 1653 at a cost estimated at the time to be around 32 million rupees, which in 2020 would be approximately 70 billion rupees (about U.S. $916 million). The construction project employed some 20,000 artisans under the guidance of a board of architects led by the court architect to the emperor. The Taj Mahal was designated as a UNESCO World Heritage Site in 1983 for being the jewel of Muslim art in India and one of the universally admired masterpieces of the world's heritage. It is regarded by many as the best example of Mughal architecture and a symbol of India's rich history. The Taj Mahal attracts 7–8 million visitors a year and in 2007, it was declared a winner of the New 7 Wonders of the World (2000–2007) initiative." },
{ "question": "Who ruled macedonia ", "context": "Macedonia was an ancient kingdom on the periphery of Archaic and Classical Greece, and later the dominant state of Hellenistic Greece. The kingdom was founded and initially ruled by the Argead dynasty, followed by the Antipatrid and Antigonid dynasties. Home to the ancient Macedonians, it originated on the northeastern part of the Greek peninsula. Before the 4th century BC, it was a small kingdom outside of the area dominated by the city-states of Athens, Sparta and Thebes, and briefly subordinate to Achaemenid Persia" },
{ "question": "what are the symptoms of COVID-19", "context": "COVID-19 is the infectious disease caused by the most recently discovered coronavirus. This new virus and disease were unknown before the outbreak began in Wuhan, China, in December 2019\. The most common symptoms of COVID-19 are fever, tiredness, and dry cough. Some patients may have aches and pains, nasal congestion, runny nose, sore throat or diarrhea. These symptoms are usually mild and begin gradually. Some people become infected but don't develop any symptoms and don't feel unwell. Most people (about 80%) recover from the disease without needing special treatment. Around 1 out of every 6 people who gets COVID-19 becomes seriously ill and develops difficulty breathing. Older people, and those with underlying medical problems like high blood pressure, heart problems or diabetes, are more likely to develop serious illness. People with fever, cough and difficulty breathing should seek medical attention." },
]
model_names = ["distilbertsquad1","distilbertsquad2","bertsquad2","bertlargesquad2"]
result_holder = []
for model_name in model_names:
bqa_model, bqa_tokenizer = get_pretrained_squad_model(model_name)
for row in questions:
start_time = time.time()
question, context = row["question"], row["context"]
gradients, tokens, token_types, answer = explain_model(question, context, bqa_model, bqa_tokenizer)
elapsed_time = time.time() - start_time
result_holder.append({"question": question, "context":context, "answer": answer, "model": model_name, "runtime": elapsed_time})
result_df = pd.DataFrame(result_holder)
格式化结果以便于检查。
question_df = result_df[result_df["model"] == "bertsquad2"].reset_index()[["question"]]
df_list = [question_df]
for model_name in model_names:
sub_df = result_df[result_df["model"] == model_name].reset_index()[["answer", "runtime"]]
sub_df.columns = [ (col_name + "_" + model_name) for col_name in sub_df.columns]
df_list.append(sub_df)
jdf = pd.concat(df_list, axis=1)
answer_cols = ["question"] + [col for col in jdf.columns if 'answer' in col]
jdf[answer_cols]

图 10.2:展示不同模型生成的答案的样本记录
从结果数据中我们可以观察到,即使在这个样本数据集上,模型之间也存在明显的差异:
-
DistilBERT(SQUAD1)能回答 5/8 个问题,正确 2 个
-
DistilBERT(SQUAD2)能回答 7/8 个问题,正确 7 个
-
BERT base 能回答 5/8 个问题,正确 5 个
-
BERT large 能回答 7/8 个问题,正确 7 个
runtime_cols = [col for col in jdf.columns if 'runtime' in col]
mean_runtime = jdf[runtime_cols].mean()
print("Mean runtime per model across 4 question/context pairs")
print(mean_runtime)
Mean runtime per model across 4 question/context pairs
runtime_distilbertsquad1 0.202405
runtime_distilbertsquad2 0.100577
runtime_bertsquad2 0.266057
runtime_bertlargesquad2 0.386156
dtype: float64
基于上述结果,我们可以对基于 BERT 的问答模型的工作原理获得一些洞见:
-
在 BERT 模型无法生成答案的情况下(例如,它只给出 CLS 标记),几乎没有输入的标记具有高的归一化梯度分数。这表明在使用的指标方面还有改进的空间——可以超越解释分数,并可能将其与模型置信度分数结合,以获得更完整的情境概览。
-
分析基础版和大版本 BERT 模型的性能差异表明,应该进一步研究性能提升与推理时间延长之间的权衡(更好的性能对比更长的推理时间)。
-
考虑到我们在选择评估数据集时可能存在的问题,一个可能的结论是,DistilBERT(在 SQuAD2 上训练)比基础 BERT 表现更好——这凸显了使用 SQuAD1 作为基准存在的问题。
第十一章:使用 TensorFlow 和 TF-Agents 进行强化学习
TF-Agents 是一个用于 强化学习(RL)的 TensorFlow(TF)库。通过提供多个模块化组件,TF-Agents 使得各种算法的设计和实现变得更加简单,这些组件对应 RL 问题的核心部分:
-
一个智能体在 环境 中操作,并通过处理每次选择动作时收到的信号来进行学习。在 TF-Agents 中,环境通常用 Python 实现,并用 TF 包装器封装,以便高效并行化。
-
策略 将环境中的观测映射为动作的分布。
-
驱动器 在环境中执行策略,经过指定的步数(也叫 回合)。
-
回放缓冲区 用于存储在环境中执行策略的经验(智能体在动作空间中的轨迹,以及相关的奖励);训练时,会查询缓冲区中的一部分轨迹。
基本思路是将我们讨论的每个问题转化为一个 RL 问题,然后将各个组件映射到 TF-Agents 对应的部分。在本章中,我们将展示如何使用 TF-Agents 来解决一些简单的 RL 问题:
-
GridWorld 问题
-
OpenAI Gym 环境
-
多臂强盗问题用于内容个性化
展示 TF-Agents 中强化学习能力的最佳方式是通过一个玩具问题:GridWorld 是一个很好的选择,因为它具有直观的几何结构和易于理解的动作,尽管如此,它仍然是一个合适的目标,我们可以研究智能体为达成目标而采取的最佳路径。
GridWorld
本节中的代码改编自 github.com/sachag678。
我们首先展示在 GridWorld 环境中基本的 TF-Agents 功能。RL 问题最好在游戏(我们有一套明确的规则和完全可观察的环境)或像 GridWorld 这样的玩具问题中研究。一旦基本概念在一个简化但不简单的环境中得到了清晰的定义,我们就可以转向逐步更具挑战性的情境。
第一步是定义一个 GridWorld 环境:这是一个 6x6 的方形棋盘,智能体从 (0,0) 开始,终点在 (5,5),智能体的目标是找到从起点到终点的路径。可能的动作包括上/下/左/右移动。如果智能体到达终点,它将获得 100 的奖励,并且如果智能体在 100 步内没有到达终点,游戏将结束。这里提供了一个 GridWorld “地图”的示例:

图 11.1:GridWorld “地图”
现在我们理解了要处理的内容,让我们构建一个模型,从 (0,0) 找到通向 (5,5) 的路径。
我们该如何进行?
和往常一样,我们首先加载必要的库:
import tensorflow as tf
import numpy as np
from tf_agents.environments import py_environment, tf_environment, tf_py_environment, utils, wrappers, suite_gym
from tf_agents.specs import array_spec
from tf_agents.trajectories import trajectory,time_step as ts
from tf_agents.agents.dqn import dqn_agent
from tf_agents.networks import q_network
from tf_agents.drivers import dynamic_step_driver
from tf_agents.metrics import tf_metrics, py_metrics
from tf_agents.policies import random_tf_policy
from tf_agents.replay_buffers import tf_uniform_replay_buffer
from tf_agents.utils import common
from tf_agents.drivers import py_driver, dynamic_episode_driver
from tf_agents.utils import common
import matplotlib.pyplot as plt
TF-Agents 是一个积极开发中的库,因此尽管我们尽力保持代码更新,但在你运行这段代码时,某些导入可能需要修改。
一个关键步骤是定义智能体将要操作的环境。通过继承PyEnvironment类,我们指定init方法(动作和观察定义)、重置/终止状态的条件以及移动机制:
class GridWorldEnv(py_environment.PyEnvironment):
# the _init_ contains the specifications for action and observation
def __init__(self):
self._action_spec = array_spec.BoundedArraySpec(
shape=(), dtype=np.int32, minimum=0, maximum=3, name='action')
self._observation_spec = array_spec.BoundedArraySpec(
shape=(4,), dtype=np.int32, minimum=[0,0,0,0], maximum=[5,5,5,5], name='observation')
self._state=[0,0,5,5] #represent the (row, col, frow, fcol) of the player and the finish
self._episode_ended = False
def action_spec(self):
return self._action_spec
def observation_spec(self):
return self._observation_spec
# once the same is over, we reset the state
def _reset(self):
self._state=[0,0,5,5]
self._episode_ended = False
return ts.restart(np.array(self._state, dtype=np.int32))
# the _step function handles the state transition by applying an action to the current state to obtain a new one
def _step(self, action):
if self._episode_ended:
return self.reset()
self.move(action)
if self.game_over():
self._episode_ended = True
if self._episode_ended:
if self.game_over():
reward = 100
else:
reward = 0
return ts.termination(np.array(self._state, dtype=np.int32), reward)
else:
return ts.transition(
np.array(self._state, dtype=np.int32), reward=0, discount=0.9)
def move(self, action):
row, col, frow, fcol = self._state[0],self._state[1],self._ state[2],self._state[3]
if action == 0: #down
if row - 1 >= 0:
self._state[0] -= 1
if action == 1: #up
if row + 1 < 6:
self._state[0] += 1
if action == 2: #left
if col - 1 >= 0:
self._state[1] -= 1
if action == 3: #right
if col + 1 < 6:
self._state[1] += 1
def game_over(self):
row, col, frow, fcol = self._state[0],self._state[1],self._ state[2],self._state[3]
return row==frow and col==fcol
def compute_avg_return(environment, policy, num_episodes=10):
total_return = 0.0
for _ in range(num_episodes):
time_step = environment.reset()
episode_return = 0.0
while not time_step.is_last():
action_step = policy.action(time_step)
time_step = environment.step(action_step.action)
episode_return += time_step.reward
total_return += episode_return
avg_return = total_return / num_episodes
return avg_return.numpy()[0]
def collect_step(environment, policy):
time_step = environment.current_time_step()
action_step = policy.action(time_step)
next_time_step = environment.step(action_step.action)
traj = trajectory.from_transition(time_step, action_step, next_time_step)
# Add trajectory to the replay buffer
replay_buffer.add_batch(traj)
我们有以下初步设置:
# parameter settings
num_iterations = 10000
initial_collect_steps = 1000
collect_steps_per_iteration = 1
replay_buffer_capacity = 100000
fc_layer_params = (100,)
batch_size = 128 #
learning_rate = 1e-5
log_interval = 200
num_eval_episodes = 2
eval_interval = 1000
我们首先创建环境并将其封装,以确保它们在 100 步后终止:
train_py_env = wrappers.TimeLimit(GridWorldEnv(), duration=100)
eval_py_env = wrappers.TimeLimit(GridWorldEnv(), duration=100)
train_env = tf_py_environment.TFPyEnvironment(train_py_env)
eval_env = tf_py_environment.TFPyEnvironment(eval_py_env)
对于这个任务,我们将使用深度 Q 网络(DQN)智能体。这意味着我们需要首先定义网络及其关联的优化器:
q_net = q_network.QNetwork(
train_env.observation_spec(),
train_env.action_spec(),
fc_layer_params=fc_layer_params)
optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate=learning_rate)
如上所示,TF-Agents 库正在积极开发中。目前版本适用于 TF > 2.3,但它最初是为 TensorFlow 1.x 编写的。此改编中使用的代码是基于先前版本开发的,因此为了兼容性,我们需要使用一些不太优雅的解决方法,例如以下代码:
train_step_counter = tf.compat.v2.Variable(0)
定义智能体:
tf_agent = dqn_agent.DqnAgent(
train_env.time_step_spec(),
train_env.action_spec(),
q_network=q_net,
optimizer=optimizer,
td_errors_loss_fn = common.element_wise_squared_loss,
train_step_counter=train_step_counter)
tf_agent.initialize()
eval_policy = tf_agent.policy
collect_policy = tf_agent.collect_policy
接下来的步骤是创建回放缓冲区和回放观察者。前者用于存储训练用的(动作,观察)对:
replay_buffer = tf_uniform_replay_buffer.TFUniformReplayBuffer(
data_spec = tf_agent.collect_data_spec,
batch_size = train_env.batch_size,
max_length = replay_buffer_capacity)
print("Batch Size: {}".format(train_env.batch_size))
replay_observer = [replay_buffer.add_batch]
train_metrics = [
tf_metrics.NumberOfEpisodes(),
tf_metrics.EnvironmentSteps(),
tf_metrics.AverageReturnMetric(),
tf_metrics.AverageEpisodeLengthMetric(),
]
然后我们从回放缓冲区创建数据集,以便可以进行迭代:
dataset = replay_buffer.as_dataset(
num_parallel_calls=3,
sample_batch_size=batch_size,
num_steps=2).prefetch(3)
最后的准备工作是创建一个驱动程序,模拟游戏中的智能体,并将(状态,动作,奖励)元组存储在回放缓冲区中,同时还需要存储若干个度量:
driver = dynamic_step_driver.DynamicStepDriver(
train_env,
collect_policy,
observers=replay_observer + train_metrics,
num_steps=1)
iterator = iter(dataset)
print(compute_avg_return(eval_env, tf_agent.policy, num_eval_episodes))
tf_agent.train = common.function(tf_agent.train)
tf_agent.train_step_counter.assign(0)
final_time_step, policy_state = driver.run()
完成准备工作后,我们可以运行驱动程序,从数据集中获取经验,并用它来训练智能体。为了监控/记录,我们会在特定间隔打印损失和平均回报:
episode_len = []
step_len = []
for i in range(num_iterations):
final_time_step, _ = driver.run(final_time_step, policy_state)
experience, _ = next(iterator)
train_loss = tf_agent.train(experience=experience)
step = tf_agent.train_step_counter.numpy()
if step % log_interval == 0:
print('step = {0}: loss = {1}'.format(step, train_loss.loss))
episode_len.append(train_metrics[3].result().numpy())
step_len.append(step)
print('Average episode length: {}'.format(train_metrics[3]. result().numpy()))
if step % eval_interval == 0:
avg_return = compute_avg_return(eval_env, tf_agent.policy, num_eval_episodes)
print('step = {0}: Average Return = {1}'.format(step, avg_return))
一旦代码成功执行,你应该看到类似以下的输出:
step = 200: loss = 0.27092617750167847 Average episode length: 96.5999984741211 step = 400: loss = 0.08925052732229233 Average episode length: 96.5999984741211 step = 600: loss = 0.04888586699962616 Average episode length: 96.5999984741211 step = 800: loss = 0.04527277499437332 Average episode length: 96.5999984741211 step = 1000: loss = 0.04451741278171539 Average episode length: 97.5999984741211 step = 1000: Average Return = 0.0 step = 1200: loss = 0.02019939199090004 Average episode length: 97.5999984741211 step = 1400: loss = 0.02462056837975979 Average episode length: 97.5999984741211 step = 1600: loss = 0.013112186454236507 Average episode length: 97.5999984741211 step = 1800: loss = 0.004257255233824253 Average episode length: 97.5999984741211 step = 2000: loss = 78.85380554199219 Average episode length: 100.0 step = 2000:
Average Return = 0.0 step = 2200: loss = 0.010012316517531872 Average episode length: 100.0 step = 2400: loss = 0.009675763547420502 Average episode length: 100.0 step = 2600: loss = 0.00445540901273489 Average episode length: 100.0 step = 2800: loss = 0.0006154756410978734
尽管训练过程的输出很详细,但并不适合人类阅读。不过,我们可以通过可视化来观察智能体的进展:
plt.plot(step_len, episode_len)
plt.xlabel('Episodes')
plt.ylabel('Average Episode Length (Steps)')
plt.show()
这将为我们提供以下图表:

图 11.2:每集平均长度与集数的关系
图表展示了我们模型的进展:在前 4000 集后,平均每集时长出现大幅下降,表明我们的智能体在达到最终目标时所需时间越来越少。
另见
有关自定义环境的文档可以参考www.tensorflow.org/agents/tutorials/2_environments_tutorial。
强化学习(RL)是一个庞大的领域,甚至是一个简单的介绍也超出了本书的范围。但对于那些有兴趣了解更多的人,最好的推荐是经典的 Sutton 和 Barto 书籍:incompleteideas.net/book/the-book.html
倾斜摆杆(CartPole)
在本节中,我们将使用 Open AI Gym,这是一个包含可以通过强化学习方法解决的非平凡基础问题的环境集。我们将使用 CartPole 环境。智能体的目标是学习如何在移动的小车上保持一根杆子平衡,可能的动作包括向左或向右移动:

图 11.3:CartPole 环境,黑色小车平衡着一根长杆
现在我们了解了环境,接下来让我们构建一个模型来平衡一个杆子。
我们该如何进行呢?
我们首先安装一些前提条件并导入必要的库。安装部分主要是为了确保我们能够生成训练智能体表现的可视化效果:
!sudo apt-get install -y xvfb ffmpeg
!pip install gym
!pip install 'imageio==2.4.0'
!pip install PILLOW
!pip install pyglet
!pip install pyvirtualdisplay
!pip install tf-agents
from __future__ import absolute_import, division, print_function
import base64
import imageio
import IPython
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import PIL.Image
import pyvirtualdisplay
import tensorflow as tf
from tf_agents.agents.dqn import dqn_agent
from tf_agents.drivers import dynamic_step_driver
from tf_agents.environments import suite_gym
from tf_agents.environments import tf_py_environment
from tf_agents.eval import metric_utils
from tf_agents.metrics import tf_metrics
from tf_agents.networks import q_network
from tf_agents.policies import random_tf_policy
from tf_agents.replay_buffers import tf_uniform_replay_buffer
from tf_agents.trajectories import trajectory
from tf_agents.utils import common
tf.compat.v1.enable_v2_behavior()
# Set up a virtual display for rendering OpenAI gym environments.
display = pyvirtualdisplay.Display(visible=0, size=(1400, 900)).start()
和之前一样,我们定义了一些我们玩具问题的超参数:
num_iterations = 20000
initial_collect_steps = 100
collect_steps_per_iteration = 1
replay_buffer_max_length = 100000
# parameters of the neural network underlying at the core of an agent
batch_size = 64
learning_rate = 1e-3
log_interval = 200
num_eval_episodes = 10
eval_interval = 1000
接下来,我们继续定义我们问题的函数。首先计算一个策略在环境中固定时间段内的平均回报(以回合数为衡量标准):
def compute_avg_return(environment, policy, num_episodes=10):
total_return = 0.0
for _ in range(num_episodes):
time_step = environment.reset()
episode_return = 0.0
while not time_step.is_last():
action_step = policy.action(time_step)
time_step = environment.step(action_step.action)
episode_return += time_step.reward
total_return += episode_return
avg_return = total_return / num_episodes
return avg_return.numpy()[0]
收集单个步骤及相关数据聚合的样板代码如下:
def collect_step(environment, policy, buffer):
time_step = environment.current_time_step()
action_step = policy.action(time_step)
next_time_step = environment.step(action_step.action)
traj = trajectory.from_transition(time_step, action_step, next_time_step)
# Add trajectory to the replay buffer
buffer.add_batch(traj)
def collect_data(env, policy, buffer, steps):
for _ in range(steps):
collect_step(env, policy, buffer)
如果一张图片值千言万语,那么视频一定更好。为了可视化我们智能体的表现,我们需要一个渲染实际动画的函数:
def embed_mp4(filename):
"""Embeds an mp4 file in the notebook."""
video = open(filename,'rb').read()
b64 = base64.b64encode(video)
tag = '''
<video width="640" height="480" controls>
<source src="img/mp4;base64,{0}" type="video/mp4">
Your browser does not support the video tag.
</video>'''.format(b64.decode())
return IPython.display.HTML(tag)
def create_policy_eval_video(policy, filename, num_episodes=5, fps=30):
filename = filename + ".mp4"
with imageio.get_writer(filename, fps=fps) as video:
for _ in range(num_episodes):
time_step = eval_env.reset()
video.append_data(eval_py_env.render())
while not time_step.is_last():
action_step = policy.action(time_step)
time_step = eval_env.step(action_step.action)
video.append_data(eval_py_env.render())
return embed_mp4(filename)
在初步工作完成后,我们可以开始真正地设置我们的环境:
env_name = 'CartPole-v0'
env = suite_gym.load(env_name)
env.reset()
在 CartPole 环境中,适用以下内容:
-
一个观察是一个包含四个浮动数值的数组:
-
小车的位置和速度
-
杆子的角位置和速度
-
-
奖励是一个标量浮动值
-
一个动作是一个标量整数,只有两个可能的值:
-
0 — "向左移动"
-
1 — "向右移动"
-
和之前一样,分开训练和评估环境,并应用包装器:
train_py_env = suite_gym.load(env_name)
eval_py_env = suite_gym.load(env_name)
train_env = tf_py_environment.TFPyEnvironment(train_py_env)
eval_env = tf_py_environment.TFPyEnvironment(eval_py_env)
定义构成我们智能体学习算法基础的网络:一个神经网络,根据环境的观察作为输入,预测所有动作的预期回报(通常在强化学习文献中称为 Q 值):
fc_layer_params = (100,)
q_net = q_network.QNetwork(
train_env.observation_spec(),
train_env.action_spec(),
fc_layer_params=fc_layer_params)
optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate=learning_rate)
train_step_counter = tf.Variable(0)
有了这个,我们可以实例化一个 DQN 智能体:
agent = dqn_agent.DqnAgent(
train_env.time_step_spec(),
train_env.action_spec(),
q_network=q_net,
optimizer=optimizer,
td_errors_loss_fn=common.element_wise_squared_loss,
train_step_counter=train_step_counter)
agent.initialize()
设置策略——主要用于评估和部署的策略,以及用于数据收集的次要策略:
eval_policy = agent.policy
collect_policy = agent.collect_policy
为了进行一个不算很复杂的比较,我们还将创建一个随机策略(顾名思义,它是随机执行的)。然而,这展示了一个重要的观点:策略可以独立于智能体创建:
random_policy = random_tf_policy.RandomTFPolicy(train_env.time_step_spec(), train_env.action_spec())
为了从策略中获得一个动作,我们调用 policy.action(time_step) 方法。time_step 包含来自环境的观察。这个方法返回一个策略步骤,这是一个包含三个组件的命名元组:
-
动作:要执行的动作(向左移动或向右移动)
-
状态:用于有状态(基于 RNN)的策略
-
信息:辅助数据,例如动作的对数概率:
example_environment = tf_py_environment.TFPyEnvironment(
suite_gym.load('CartPole-v0'))
time_step = example_environment.reset()
重放缓冲区跟踪从环境中收集的数据,这些数据用于训练:
replay_buffer = tf_uniform_replay_buffer.TFUniformReplayBuffer(
data_spec=agent.collect_data_spec,
batch_size=train_env.batch_size,
max_length=replay_buffer_max_length)
对于大多数代理,collect_data_spec 是一个命名元组,称为轨迹(Trajectory),它包含关于观察、动作、奖励以及其他项的规格。
我们现在利用随机策略来探索环境:
collect_data(train_env, random_policy, replay_buffer, initial_collect_steps)
现在,代理可以通过管道访问重放缓冲区。由于我们的 DQN 代理需要当前观察和下一次观察来计算损失,因此管道一次会采样两行相邻数据(num_steps = 2):
dataset = replay_buffer.as_dataset(
num_parallel_calls=3,
sample_batch_size=batch_size,
num_steps=2).prefetch(3)
iterator = iter(dataset)
在训练部分,我们在两个步骤之间切换:从环境中收集数据并用它来训练 DQN:
agent.train = common.function(agent.train)
# Reset the train step
agent.train_step_counter.assign(0)
# Evaluate the agent's policy once before training.
avg_return = compute_avg_return(eval_env, agent.policy, num_eval_episodes)
returns = [avg_return]
for _ in range(num_iterations):
# Collect a few steps using collect_policy and save to the replay buffer.
collect_data(train_env, agent.collect_policy, replay_buffer, collect_steps_per_iteration)
# Sample a batch of data from the buffer and update the agent's network.
experience, unused_info = next(iterator)
train_loss = agent.train(experience).loss
step = agent.train_step_counter.numpy()
if step % log_interval == 0:
print('step = {0}: loss = {1}'.format(step, train_loss))
if step % eval_interval == 0:
avg_return = compute_avg_return(eval_env, agent.policy, num_eval_episodes)
print('step = {0}: Average Return = {1}'.format(step, avg_return))
returns.append(avg_return)
代码块的(部分)输出如下。快速回顾一下,step 是训练过程中的迭代次数,loss 是深度网络中驱动代理逻辑的损失函数值,Average Return 是当前运行结束时的奖励:
step = 200: loss = 4.396056175231934
step = 400: loss = 7.12950325012207
step = 600: loss = 19.0213623046875
step = 800: loss = 45.954856872558594
step = 1000: loss = 35.900394439697266
step = 1000: Average Return = 21.399999618530273
step = 1200: loss = 60.97482681274414
step = 1400: loss = 8.678962707519531
step = 1600: loss = 13.465248107910156
step = 1800: loss = 42.33995056152344
step = 2000: loss = 42.936370849609375
step = 2000: Average Return = 21.799999237060547
每次迭代包含 200 个时间步,保持杆子竖立会得到 1 分奖励,因此每一轮的最大奖励为 200:

图 11.4:每次迭代的平均回报
从前面的图表中可以看出,代理大约需要 1 万次迭代才能发现一个成功的策略(其中有一些波动,正如奖励曲线中的 U 形模式所示)。之后,奖励趋于稳定,算法能够每次成功完成任务。
我们还可以通过视频观察代理的表现。关于随机策略,您可以尝试以下操作:
create_policy_eval_video(random_policy, "random-agent")
而关于已经训练的策略,您可以尝试以下操作:
create_policy_eval_video(agent.policy, "trained-agent")
另请参阅
Open AI Gym 环境文档可以在gym.openai.com/找到。
MAB
在概率论中,多臂赌博机(MAB)问题指的是一种情境,其中有限的资源必须在多个竞争的选择之间分配,以最大化某种形式的长期目标。这个名称来源于用于制定模型第一版的类比。假设我们有一个赌徒,面前是一排老丨虎丨机,他必须决定选择哪些老丨虎丨机进行游戏,玩多少次,以及以什么顺序进行。在强化学习(RL)中,我们将其表述为一个代理(agent),该代理希望平衡探索(获得新知识)和开发(基于已经获得的经验优化决策)。这种平衡的目标是在一段时间内最大化总奖励。
MAB 是一个简化的强化学习问题:代理采取的动作不会影响环境的后续状态。这意味着不需要建模状态转移,不需要为过去的动作分配奖励,也不需要提前规划以到达奖励状态。MAB 代理的目标是确定一个策略,使得随时间推移能够最大化累积奖励。
主要挑战是有效应对探索与利用的难题:如果我们总是尝试利用期望奖励最高的动作,就有可能错过那些通过更多探索可以发现的更好的动作。
本示例中使用的设置来源于 Vowpal Wabbit 教程,网址为vowpalwabbit.org/tutorials/cb_simulation.html。
在本节中,我们将模拟个性化在线内容的问题:Tom 和 Anna 在一天中的不同时间访问网站并查看一篇文章。Tom 早上喜欢政治,下午喜欢音乐,而 Anna 早上喜欢体育或政治,下午喜欢政治。将这个问题用多臂赌博机(MAB)术语表述,这意味着:
-
上下文是一个包含{用户,时间段}的对。
-
可能的动作是新闻主题{政治、体育、音乐、食物}。
-
如果用户在此时看到他们感兴趣的内容,则奖励为 1,否则为 0。
目标是最大化通过用户的点击率(CTR)衡量的奖励。
我们该怎么做呢?
像往常一样,我们首先加载必要的包:
!pip install tf-agents
import abc
import numpy as np
import tensorflow as tf
from tf_agents.agents import tf_agent
from tf_agents.drivers import driver
from tf_agents.environments import py_environment
from tf_agents.environments import tf_environment
from tf_agents.environments import tf_py_environment
from tf_agents.policies import tf_policy
from tf_agents.specs import array_spec
from tf_agents.specs import tensor_spec
from tf_agents.trajectories import time_step as ts
from tf_agents.trajectories import trajectory
from tf_agents.trajectories import policy_step
tf.compat.v1.reset_default_graph()
tf.compat.v1.enable_resource_variables()
tf.compat.v1.enable_v2_behavior()
nest = tf.compat.v2.nest
from tf_agents.bandits.agents import lin_ucb_agent
from tf_agents.bandits.environments import stationary_stochastic_py_environment as sspe
from tf_agents.bandits.metrics import tf_metrics
from tf_agents.drivers import dynamic_step_driver
from tf_agents.replay_buffers import tf_uniform_replay_buffer
import matplotlib.pyplot as plt
我们接下来定义一些超参数,这些参数将在后续使用:
batch_size = 2
num_iterations = 100
steps_per_loop = 1
我们需要的第一个函数是一个上下文采样器,用于生成来自环境的观察值。由于我们有两个用户和一天中的两个时间段,实际上是生成两元素二进制向量:
def context_sampling_fn(batch_size):
def _context_sampling_fn():
return np.random.randint(0, 2, [batch_size, 2]).astype(np.float32)
return _context_sampling_fn
接下来,我们定义一个通用函数来计算每个臂的奖励:
class CalculateReward(object):
"""A class that acts as linear reward function when called."""
def __init__(self, theta, sigma):
self.theta = theta
self.sigma = sigma
def __call__(self, x):
mu = np.dot(x, self.theta)
#return np.random.normal(mu, self.sigma)
return (mu > 0) + 0
我们可以使用该函数来定义每个臂的奖励。这些奖励反映了在本食谱开头描述的偏好集:
arm0_param = [2, -1]
arm1_param = [1, -1]
arm2_param = [-1, 1]
arm3_param = [ 0, 0]
arm0_reward_fn = CalculateReward(arm0_param, 1)
arm1_reward_fn = CalculateReward(arm1_param, 1)
arm2_reward_fn = CalculateReward(arm2_param, 1)
arm3_reward_fn = CalculateReward(arm3_param, 1)
我们函数设置的最后一部分涉及计算给定上下文的最优奖励:
def compute_optimal_reward(observation):
expected_reward_for_arms = [
tf.linalg.matvec(observation, tf.cast(arm0_param, dtype=tf.float32)),
tf.linalg.matvec(observation, tf.cast(arm1_param, dtype=tf.float32)),
tf.linalg.matvec(observation, tf.cast(arm2_param, dtype=tf.float32)),
tf.linalg.matvec(observation, tf.cast(arm3_param, dtype=tf.float32))
]
optimal_action_reward = tf.reduce_max(expected_reward_for_arms, axis=0)
return optimal_action_reward
为了本示例的目的,我们假设环境是静态的;换句话说,偏好在时间上没有变化(这在实际场景中不一定成立,取决于你关注的时间范围):
environment = tf_py_environment.TFPyEnvironment(
sspe.StationaryStochasticPyEnvironment(
context_sampling_fn(batch_size),
[arm0_reward_fn, arm1_reward_fn, arm2_reward_fn, arm3_reward_fn],
batch_size=batch_size))
我们现在准备实例化一个实现赌博机算法的代理。我们使用预定义的LinUCB类;像往常一样,我们定义观察值(两个元素,表示用户和时间),时间步长和动作规范(四种可能内容类型之一):
observation_spec = tensor_spec.TensorSpec([2], tf.float32)
time_step_spec = ts.time_step_spec(observation_spec)
action_spec = tensor_spec.BoundedTensorSpec(
dtype=tf.int32, shape=(), minimum=0, maximum=2)
agent = lin_ucb_agent.LinearUCBAgent(time_step_spec=time_step_spec,
action_spec=action_spec)
多臂赌博机(MAB)设置的一个关键组成部分是遗憾,它被定义为代理实际获得的奖励与oracle 策略的期望奖励之间的差异:
regret_metric = tf_metrics.RegretMetric(compute_optimal_reward)
我们现在可以开始训练我们的代理。我们运行训练循环num_iterations次,每次执行steps_per_loop步。找到这些参数的合适值通常是要在更新的时效性和训练效率之间找到平衡:
replay_buffer = tf_uniform_replay_buffer.TFUniformReplayBuffer(
data_spec=agent.policy.trajectory_spec,
batch_size=batch_size,
max_length=steps_per_loop)
observers = [replay_buffer.add_batch, regret_metric]
driver = dynamic_step_driver.DynamicStepDriver(
env=environment,
policy=agent.collect_policy,
num_steps=steps_per_loop * batch_size,
observers=observers)
regret_values = []
for _ in range(num_iterations):
driver.run()
loss_info = agent.train(replay_buffer.gather_all())
replay_buffer.clear()
regret_values.append(regret_metric.result())
我们可以通过绘制后续迭代中的遗憾(负奖励)来可视化实验结果:
plt.plot(regret_values)
plt.ylabel('Average Regret')
plt.xlabel('Number of Iterations')
这将为我们绘制出以下图表:

图 11.5:训练过的 UCB 代理随时间的表现
如前图所示,在初始学习阶段(在第 30 次迭代附近的遗憾出现峰值),代理会不断改进,提供期望的内容。过程中有很多变化,表明即便在一个简化的环境下——两个用户——高效个性化仍然是一个挑战。改进的可能方向包括更长时间的训练,或者调整 DQN 代理,使其能够采用更复杂的逻辑来进行预测。
另见
相关的强盗算法及其环境的广泛集合可以在TF-Agents 文档库中找到:github.com/tensorflow/agents/tree/master/tf_agents/bandits/agents/examples/v2。
对上下文多臂强盗问题感兴趣的读者可以参考Sutton 和 Barto的书中的相关章节:web.stanford.edu/class/psych209/Readings/SuttonBartoIPRLBook2ndEd.pdf。
第十二章:将 TensorFlow 应用到生产环境
在本书中,我们已经看到 TensorFlow 能够实现许多模型,但 TensorFlow 能做的远不止这些。本章将向你展示其中的一些内容。在本章中,我们将涵盖以下主题:
-
在 TensorBoard 中可视化图表
-
使用 TensorBoard 的 HParams 进行超参数调优
-
使用 tf.test 实现单元测试
-
使用多个执行器
-
使用 tf.distribute.strategy 进行 TensorFlow 并行化
-
保存和恢复 TensorFlow 模型
-
使用 TensorFlow Serving
我们将首先展示如何使用 TensorBoard 的各个方面,TensorBoard 是 TensorFlow 自带的一个功能。这个工具允许我们即使在模型训练过程中,也能可视化总结指标、图表和图像。接下来,我们将展示如何编写适用于生产环境的代码,重点是单元测试、多处理单元的训练分配,以及高效的模型保存和加载。最后,我们将通过将模型托管为 REST 端点,解决机器学习服务方案。
在 TensorBoard 中可视化图表
监控和排查机器学习算法可能是一项艰巨的任务,尤其是在你必须等待训练完成后才能知道结果的情况下。为了应对这种情况,TensorFlow 提供了一个计算图可视化工具,称为TensorBoard。借助 TensorBoard,我们可以在训练过程中可视化图表和重要数值(如损失、准确率、批次训练时间等)。
准备工作
为了展示我们如何使用 TensorBoard 的各种方式,我们将重新实现第八章中卷积神经网络章节里的入门 CNN 模型配方中的 MNIST 模型。然后,我们将添加 TensorBoard 回调并拟合模型。我们将展示如何监控数值、值集的直方图,如何在 TensorBoard 中创建图像,以及如何可视化 TensorFlow 模型。
如何实现...
-
首先,我们将加载脚本所需的库:
import tensorflow as tf import numpy as np import datetime -
现在我们将重新实现 MNIST 模型:
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() x_train = x_train.reshape(-1, 28, 28, 1) x_test = x_test.reshape(-1, 28, 28, 1) # Padding the images by 2 pixels since in the paper input images were 32x32 x_train = np.pad(x_train, ((0,0),(2,2),(2,2),(0,0)), 'constant') x_test = np.pad(x_test, ((0,0),(2,2),(2,2),(0,0)), 'constant') # Normalize x_train = x_train / 255 x_test = x_test/ 255 # Set model parameters image_width = x_train[0].shape[0] image_height = x_train[0].shape[1] num_channels = 1 # grayscale = 1 channel # Training and Test data variables batch_size = 100 evaluation_size = 500 generations = 300 eval_every = 5 # Set for reproducible results seed = 98 np.random.seed(seed) tf.random.set_seed(seed) # Declare the model input_data = tf.keras.Input(dtype=tf.float32, shape=(image_width,image_height, num_channels), name="INPUT") # First Conv-ReLU-MaxPool Layer conv1 = tf.keras.layers.Conv2D(filters=6, kernel_size=5, padding='VALID', activation="relu", name="C1")(input_data) max_pool1 = tf.keras.layers.MaxPool2D(pool_size=2, strides=2, padding='SAME', name="S1")(conv1) # Second Conv-ReLU-MaxPool Layer conv2 = tf.keras.layers.Conv2D(filters=16, kernel_size=5, padding='VALID', strides=1, activation="relu", name="C3")(max_pool1) max_pool2 = tf.keras.layers.MaxPool2D(pool_size=2, strides=2, padding='SAME', name="S4")(conv2) # Flatten Layer flatten = tf.keras.layers.Flatten(name="FLATTEN")(max_pool2) # First Fully Connected Layer fully_connected1 = tf.keras.layers.Dense(units=120, activation="relu", name="F5")(flatten) # Second Fully Connected Layer fully_connected2 = tf.keras.layers.Dense(units=84, activation="relu", name="F6")(fully_connected1) # Final Fully Connected Layer final_model_output = tf.keras.layers.Dense(units=10, activation="softmax", name="OUTPUT" )(fully_connected2) model = tf.keras.Model(inputs= input_data, outputs=final_model_output) -
接下来,我们将使用稀疏类别交叉熵损失和 Adam 优化器编译模型。然后,我们将展示总结:
model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) model.summary() -
我们将为每次运行创建一个带时间戳的子目录。总结写入器将把
TensorBoard日志写入这个文件夹:log_dir="logs/experiment-" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") -
接下来,我们将实例化一个
TensorBoard回调并将其传递给fit方法。训练阶段的所有日志将存储在此目录中,并可以立即在TensorBoard中查看:tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, write_images=True, histogram_freq=1 ) model.fit(x=x_train, y=y_train, epochs=5, validation_data=(x_test, y_test), callbacks=[tensorboard_callback]) -
然后我们通过运行以下命令启动
TensorBoard应用程序:$ tensorboard --logdir="logs" -
然后我们在浏览器中导航到以下链接:
http://127.0.0.0:6006。如果需要,我们可以通过传递--port 6007命令(例如,在 6007 端口运行)来指定不同的端口。我们还可以通过在笔记本中运行%tensorboard --logdir="logs"命令来启动 TensorBoard。请记住,TensorBoard 将在你的程序运行时可见。 -
我们可以通过 TensorBoard 的标量视图快速且轻松地可视化和比较多个实验的度量。在默认情况下,TensorBoard 会在每个训练周期记录度量和损失。我们可以使用以下参数通过每个批次更新该频率:
update_freq='batch'。我们还可以使用参数write_images=True将模型权重可视化为图像,或者使用histogram_freq=1以直方图的形式(每个周期计算)显示偏差和权重。 -
这是标量视图的截图:
![]()
图 12.1:训练和测试损失随着时间推移而减少,而训练和测试准确度则增加
-
这里,我们展示如何通过直方图摘要可视化权重和偏差。通过此仪表板,我们可以绘制非标量张量(如权重和偏差)在不同时间点的多个直方图可视化。这样,我们就能看到这些值是如何随时间变化的:
![]()
图 12.2:在 TensorBoard 中通过直方图视图可视化权重和偏差
-
现在,我们将通过 TensorFlow 的图形仪表板可视化 TensorFlow 模型,仪表板通过不同的视图展示模型。该仪表板不仅可以可视化操作级别的图,还可以显示概念级别的图。操作级图展示了 Keras 模型以及指向其他计算节点的额外边缘,而概念级图仅展示 Keras 模型。这些视图可以帮助我们快速检查并比较我们的设计,并理解 TensorFlow 模型结构。
-
这里,我们展示如何可视化操作级别的图:
![]()
图 12.3:TensorBoard 中的操作级图
-
通过添加 TensorBoard 回调,我们可以可视化损失、度量、模型权重作为图像等内容。但我们也可以使用
tf.summary模块来写入可以在 TensorFlow 中可视化的摘要数据。首先,我们需要创建一个FileWriter,然后就可以写入直方图、标量、文本、音频或图像摘要。在这里,我们将使用图像摘要 API 来写入图像,并在 TensorBoard 中进行可视化:# Create a FileWriter for the timestamped log directory. file_writer = tf.summary.create_file_writer(log_dir) with file_writer.as_default(): # Reshape the images and write image summary. images = np.reshape(x_train[0:10], (-1, 32, 32, 1)) tf.summary.image("10 training data examples", images, max_outputs=10, step=0)

图 12.4:在 TensorBoard 中可视化图像
请注意,不要过于频繁地将图像摘要写入 TensorBoard。例如,如果我们在 10,000 次训练中每次都写入一次图像摘要,那么将生成 10,000 张图像的摘要数据。这会非常迅速地占用磁盘空间。
工作原理...
在本节中,我们在 MNIST 数据集上实现了一个 CNN 模型。我们添加了 TensorBoard 回调并训练了模型。然后,我们使用 TensorFlow 的可视化工具来监控数值和数值集合的直方图,进而可视化模型图等。
请记住,我们可以通过命令行启动 TensorBoard,如食谱中所示,但也可以通过使用 %tensorboard 魔法命令在笔记本中启动它。
另见
关于 TensorBoard API 的一些参考资料,请访问以下网站:
-
官方的 TensorBoard 指南:
www.tensorflow.org/tensorboard/get_started -
TensorFlow 摘要 API:
www.tensorflow.org/api_docs/python/tf/summary
还有更多...
TensorBoard.dev 是 Google 提供的免费托管服务。其目的是轻松托管、跟踪和分享机器学习实验给任何人。在我们启动实验后,只需将 TensorBoard 日志上传到 TensorBoard 服务器。然后,分享链接,任何拥有该链接的人都可以查看我们的实验。请注意不要上传敏感数据,因为上传的 TensorBoard 数据集是公开的,所有人都可以看到。
使用 TensorBoard 的 HParams 管理超参数调优
在机器学习项目中调优超参数可能是一项真正的挑战。这个过程是迭代的,并且可能需要很长时间来测试所有的超参数组合。但幸运的是,HParams——一个 TensorBoard 插件,来拯救我们。它允许我们通过测试找到最佳的超参数组合。
准备工作
为了说明 HParams 插件如何工作,我们将使用一个在 MNIST 数据集上的顺序模型实现。我们将配置 HParams,并比较几个超参数组合,以找到最佳的超参数优化。
如何操作...
-
首先,我们将加载脚本所需的库:
import tensorflow as tf from tensorboard.plugins.hparams import api as hp import numpy as np import datetime -
接下来,我们将加载并准备 MNIST 数据集:
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() # Normalize x_train = x_train / 255 x_test = x_test/ 255 ## Set model parameters image_width = x_train[0].shape[0] image_height = x_train[0].shape[1] num_channels = 1 # grayscale = 1 channel -
然后,对于每个超参数,我们将定义要测试的值列表或区间。在这一部分,我们将介绍三个超参数:每层单元数、Dropout 率和优化器:
HP_ARCHITECTURE_NN = hp.HParam('archi_nn', hp.Discrete(['128,64','256,128'])) HP_DROPOUT = hp.HParam('dropout', hp.RealInterval(0.0, 0.1)) HP_OPTIMIZER = hp.HParam('optimizer', hp.Discrete(['adam', 'sgd'])) -
该模型将是一个顺序模型,包含五层:一个展平层,接着是一个全连接层,一个 Dropout 层,再是另一个全连接层,最后是一个具有 10 个单元的输出层。训练函数将接收一个包含超参数组合的 HParams 字典作为参数。由于我们使用的是 Keras 模型,我们在 fit 方法中添加了 HParams Keras 回调来监控每次实验。对于每次实验,插件将记录超参数组合、损失值和指标。如果需要监控其他信息,我们还可以添加一个 Summary File Writer:
def train_model(hparams, experiment_run_log_dir): nb_units = list(map(int, hparams[HP_ARCHITECTURE_NN].split(","))) model = tf.keras.models.Sequential() model.add(tf.keras.layers.Flatten(name="FLATTEN")) model.add(tf.keras.layers.Dense(units=nb_units[0], activation="relu", name="D1")) model.add(tf.keras.layers.Dropout(hparams[HP_DROPOUT], name="DROP_OUT")) model.add(tf.keras.layers.Dense(units=nb_units[1], activation="relu", name="D2")) model.add(tf.keras.layers.Dense(units=10, activation="softmax", name="OUTPUT")) model.compile( optimizer=hparams[HP_OPTIMIZER], loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=experiment_run_log_dir) hparams_callback = hp.KerasCallback(experiment_run_log_dir, hparams) model.fit(x=x_train, y=y_train, epochs=5, validation_data=(x_test, y_test), callbacks=[tensorboard_callback, hparams_callback] ) model = tf.keras.Model(inputs= input_data, outputs=final_model_output) -
接下来,我们将对所有超参数进行迭代:
for archi_nn in HP_ARCHITECTURE_NN.domain.values: for optimizer in HP_OPTIMIZER.domain.values: for dropout_rate in (HP_DROPOUT.domain.min_value, HP_DROPOUT.domain.max_value): hparams = { HP_ARCHITECTURE_NN : archi_nn, HP_OPTIMIZER: optimizer, HP_DROPOUT : dropout_rate } experiment_run_log_dir="logs/experiment-" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") train_model(hparams, experiment_run_log_dir) -
然后,我们通过运行此命令启动 TensorBoard 应用程序:
$ tensorboard --logdir="logs" -
然后,我们可以快速而轻松地在 HParams 表格视图中可视化结果(超参数和指标)。如果需要,左侧面板可以应用过滤器和排序:
![]()
图 12.5:在 TensorBoard 中可视化的 HParams 表格视图
-
在平行坐标视图中,每个轴表示一个超参数或指标,每次运行由一条线表示。这个可视化方法可以快速识别出最佳的超参数组合:
![]()
图 12.6:在 TensorBoard 中可视化的 HParams 平行坐标视图
使用 TensorBoard HParams 是一种简单且富有洞察力的方式,可以识别最佳超参数,并帮助管理你在 TensorFlow 中的实验。
另见
有关 HParams TensorBoard 插件的参考,请访问以下网站:
- 官方 TensorBoard 指南:
www.tensorflow.org/tensorboard/hyperparameter_tuning_with_hparams
实现单元测试
测试代码可以加速原型开发,提高调试效率,快速变更,并且使得代码共享变得更容易。TensorFlow 2.0 提供了tf.test模块,我们将在本节中介绍它。
准备工作
在编写 TensorFlow 模型时,单元测试可以帮助检查程序功能。这对我们很有帮助,因为当我们想对程序单元进行修改时,测试可以确保这些修改不会以未知的方式破坏模型。在 Python 中,主要的测试框架是unittest,但 TensorFlow 提供了自己的测试框架。在本节中,我们将创建一个自定义层类,并实现一个单元测试,演示如何在 TensorFlow 中编写单元测试。
如何操作...
-
首先,我们需要加载必要的库,如下所示:
import tensorflow as tf import numpy as np -
然后,我们需要声明我们的自定义门控函数,应用
f(x) = a1 * x + b1:class MyCustomGate(tf.keras.layers.Layer): def __init__(self, units, a1, b1): super(MyCustomGate, self).__init__() self.units = units self.a1 = a1 self.b1 = b1 # Compute f(x) = a1 * x + b1 def call(self, inputs): return inputs * self.a1 + self.b1 -
接下来,我们创建一个继承自
tf.test.TestCase类的单元测试类。setup方法是一个hook方法,在每个test方法之前被调用。assertAllEqual方法检查预期输出和计算输出是否相等:class MyCustomGateTest(tf.test.TestCase): def setUp(self): super(MyCustomGateTest, self).setUp() # Configure the layer with 1 unit, a1 = 2 et b1=1 self.my_custom_gate = MyCustomGate(1,2,1) def testMyCustomGateOutput(self): input_x = np.array([[1,0,0,1], [1,0,0,1]]) output = self.my_custom_gate(input_x) expected_output = np.array([[3,1,1,3], [3,1,1,3]]) self.assertAllEqual(output, expected_output) -
现在我们需要在脚本中加入一个
main()函数,用于运行所有单元测试:tf.test.main() -
从终端运行以下命令。我们应该会得到如下输出:
$ python3 01_implementing_unit_tests.py ... [ OK ] MyCustomGateTest.testMyCustomGateOutput [ RUN ] MyCustomGateTest.test_session [ SKIPPED ] MyCustomGateTest.test_session ---------------------------------------------------------------------- Ran 2 tests in 0.016s OK (skipped=1)
我们实现了一个测试并且通过了。不要担心那两个test_session测试——它们是虚拟测试。
请注意,许多专门为 TensorFlow 量身定制的断言可以在tf.test API 中找到。
它是如何工作的...
在这一部分,我们使用tf.test API 实现了一个 TensorFlow 单元测试,它与 Python 的单元测试非常相似。记住,单元测试有助于确保代码按预期功能运行,增加共享代码的信心,并使得可重复性更容易实现。
另见
有关tf.test模块的参考,请访问以下网站:
- 官方 TensorFlow 测试 API:
www.tensorflow.org/api_docs/python/tf/test
使用多个执行器
你可能知道,TensorFlow 有许多特性,包括计算图,它们天生适合并行计算。计算图可以在不同的处理器之间拆分,也可以在不同的批次之间进行处理。我们将在本节中讨论如何在同一台机器上访问不同的处理器。
准备工作
在本教程中,我们将向您展示如何访问同一系统上的多个设备并在其上进行训练。设备是 CPU 或加速单元(GPU、TPU),TensorFlow 可以在其上运行操作。这是非常常见的情况:除了 CPU,机器可能还有一个或多个 GPU 可以共享计算负载。如果 TensorFlow 能够访问这些设备,它将通过贪婪过程自动将计算分配到多个设备。然而,TensorFlow 也允许程序通过命名范围位置指定哪些操作将在哪个设备上执行。
在本教程中,我们将向您展示不同的命令,这些命令将允许您访问系统上的各种设备;我们还将演示如何查找 TensorFlow 正在使用的设备。请记住,一些功能仍然是实验性的,可能会发生变化。
如何实现...
-
为了查找 TensorFlow 为哪些操作使用了哪些设备,我们将通过设置
tf.debugging.set_log_device_placement为True来激活设备分配日志。如果 TensorFlow 操作同时支持 CPU 和 GPU 设备,该操作将在默认情况下执行在 GPU 设备上(如果 GPU 可用):tf.debugging.set_log_device_placement(True) a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a') b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b') c = tf.matmul(a, b) Executing op Reshape in device /job:localhost/replica:0/task:0/device:GPU:0 Executing op Reshape in device /job:localhost/replica:0/task:0/device:GPU:0 Executing op MatMul in device /job:localhost/replica:0/task:0/device:GPU:0 -
我们还可以使用张量设备属性来返回该张量将分配到的设备名称:
a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a') print(a.device) b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b') print(b.device) Executing op Reshape in device /job:localhost/replica:0/task:0/device:GPU:0 Executing op MatMul in device /job:localhost/replica:0/task:0/device:GPU:0 -
默认情况下,TensorFlow 会自动决定如何在计算设备(CPU 和 GPU)之间分配计算,有时我们需要通过创建
tf.device函数的设备上下文来选择要使用的设备。在此上下文中执行的每个操作都会使用所选设备:tf.debugging.set_log_device_placement(True) with tf.device('/device:CPU:0'): a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a') b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b') c = tf.matmul(a, b) Executing op Reshape in device /job:localhost/replica:0/task:0/device:CPU:0 Executing op Reshape in device /job:localhost/replica:0/task:0/device:CPU:0 Executing op MatMul in device /job:localhost/replica:0/task:0/device:CPU:0 -
如果我们将
matmul操作移出上下文,如果有可用的 GPU 设备,则该操作将在 GPU 设备上执行:tf.debugging.set_log_device_placement(True) with tf.device('/device:CPU:0'): a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a') b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b') c = tf.matmul(a, b) Executing op Reshape in device /job:localhost/replica:0/task:0/device:CPU:0 Executing op Reshape in device /job:localhost/replica:0/task:0/device:CPU:0 Executing op MatMul in device /job:localhost/replica:0/task:0/device:GPU:0 -
在使用 GPU 时,TensorFlow 会自动占用 GPU 内存的大部分。虽然这通常是期望的行为,但我们可以采取措施更加谨慎地分配 GPU 内存。虽然 TensorFlow 永远不会释放 GPU 内存,但我们可以通过设置 GPU 内存增长选项,逐步增加其分配,直到达到最大限制(仅在需要时)。注意,物理设备初始化后不能修改:
gpu_devices = tf.config.list_physical_devices('GPU') if gpu_devices: try: tf.config.experimental.set_memory_growth(gpu_devices[0], True) except RuntimeError as e: # Memory growth cannot be modified after GPU has been initialized print(e) -
如果我们想要对 TensorFlow 使用的 GPU 内存设置硬性限制,我们还可以创建一个虚拟 GPU 设备并设置分配到该虚拟 GPU 的最大内存限制(单位:MB)。注意,虚拟设备初始化后不能修改:
gpu_devices = tf.config.list_physical_devices('GPU') if gpu_devices: try: tf.config.experimental.set_virtual_device_configuration(gpu_devices[0], [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=1024)]) except RuntimeError as e: # Memory growth cannot be modified after GPU has been initialized print(e) -
我们还可以通过单个物理 GPU 来模拟虚拟 GPU 设备。通过以下代码可以实现:
gpu_devices = tf.config.list_physical_devices('GPU') if gpu_devices: try: tf.config.experimental.set_virtual_device_configuration(gpu_devices[0], [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=1024), tf.config.experimental.VirtualDeviceConfiguration(memory_limit=1024) ]) except RuntimeError as e: # Memory growth cannot be modified after GPU has been initialized print(e) -
有时我们可能需要编写稳健的代码来判断是否有可用的 GPU。TensorFlow 有一个内置函数,可以测试 GPU 是否可用。当我们希望在 GPU 可用时利用它并将特定操作分配给它时,这非常有帮助。通过以下代码可以实现:
if tf.test.is_built_with_cuda(): <Run GPU specific code here> -
如果我们需要将特定操作分配给 GPU,可以输入以下代码。这将执行简单的计算,并将操作分配给主 CPU 和两个辅助 GPU:
if tf.test.is_built_with_cuda(): with tf.device('/cpu:0'): a = tf.constant([1.0, 3.0, 5.0], shape=[1, 3]) b = tf.constant([2.0, 4.0, 6.0], shape=[3, 1]) with tf.device('/gpu:0'): c = tf.matmul(a,b) c = tf.reshape(c, [-1]) with tf.device('/gpu:1'): d = tf.matmul(b,a) flat_d = tf.reshape(d, [-1]) combined = tf.multiply(c, flat_d) print(combined) Num GPUs Available: 2 Executing op Reshape in device /job:localhost/replica:0/task:0/device:CPU:0 Executing op Reshape in device /job:localhost/replica:0/task:0/device:CPU:0 Executing op MatMul in device /job:localhost/replica:0/task:0/device:GPU:0 Executing op Reshape in device /job:localhost/replica:0/task:0/device:GPU:0 Executing op MatMul in device /job:localhost/replica:0/task:0/device:GPU:1 Executing op Reshape in device /job:localhost/replica:0/task:0/device:GPU:1 Executing op Mul in device /job:localhost/replica:0/task:0/device:CPU:0 tf.Tensor([ 88\. 264\. 440\. 176\. 528\. 880\. 264\. 792\. 1320.], shape=(9,), dtype=float32)
我们可以看到,前两个操作已在主 CPU 上执行,接下来的两个操作在第一个辅助 GPU 上执行,最后两个操作在第二个辅助 GPU 上执行。
工作原理...
当我们希望为 TensorFlow 操作设置特定设备时,我们需要了解 TensorFlow 如何引用这些设备。TensorFlow 中的设备名称遵循以下约定:
| 设备 | 设备名称 |
|---|---|
| 主 CPU | /device:CPU:0 |
| 主 GPU | /GPU:0 |
| 第二 GPU | /job:localhost/replica:0/task:0/device:GPU:1 |
| 第三 GPU | /job:localhost/replica:0/task:0/device:GPU:2 |
请记住,TensorFlow 将 CPU 视为一个独立的处理器,即使该处理器是一个多核处理器。所有核心都被包装在/device:CPU:0中,也就是说,TensorFlow 默认确实使用多个 CPU 核心。
还有更多...
幸运的是,现在在云端运行 TensorFlow 比以往任何时候都更容易。许多云计算服务提供商提供 GPU 实例,这些实例拥有主 CPU 和强大的 GPU。请注意,获得 GPU 的简单方法是通过 Google Colab 运行代码,并在笔记本设置中将 GPU 设置为硬件加速器。
并行化 TensorFlow
训练模型可能非常耗时。幸运的是,TensorFlow 提供了几种分布式策略来加速训练,无论是针对非常大的模型还是非常大的数据集。本食谱将向我们展示如何使用 TensorFlow 分布式 API。
准备中
TensorFlow 分布式 API 允许我们通过将模型复制到不同节点并在不同数据子集上进行训练来分布式训练。每个策略支持一个硬件平台(多个 GPU、多个机器或 TPU),并使用同步或异步训练策略。在同步训练中,每个工作节点在不同的数据批次上训练,并在每一步汇聚它们的梯度。而在异步模式下,每个工作节点独立训练数据,变量异步更新。请注意,目前 TensorFlow 仅支持上面描述的数据并行性,根据路线图,它很快将支持模型并行性。当模型太大,无法放入单个设备时,就需要将模型分布到多个设备上进行训练。本例将介绍该 API 提供的镜像策略。
如何实现...
-
首先,我们将加载该食谱所需的库,如下所示:
import tensorflow as tf import tensorflow_datasets as tfds -
我们将创建两个虚拟 GPU:
# Create two virtual GPUs gpu_devices = tf.config.list_physical_devices('GPU') if gpu_devices: try: tf.config.experimental.set_virtual_device_configuration(gpu_devices[0], [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=1024), tf.config.experimental.VirtualDeviceConfiguration(memory_limit=1024) ]) except RuntimeError as e: # Memory growth cannot be modified after GPU has been initialized print(e) -
接下来,我们将通过
tensorflow_datasetsAPI 加载 MNIST 数据集,如下所示:datasets, info = tfds.load('mnist', with_info=True, as_supervised=True) mnist_train, mnist_test = datasets['train'], datasets['test'] -
然后,我们将准备数据:
def normalize_img(image, label): """Normalizes images: `uint8` -> `float32`.""" return tf.cast(image, tf.float32) / 255., label mnist_train = mnist_train.map( normalize_img, num_parallel_calls=tf.data.experimental.AUTOTUNE) mnist_train = mnist_train.cache() mnist_train = mnist_train.shuffle(info.splits['train'].num_examples) mnist_train = mnist_train.prefetch(tf.data.experimental.AUTOTUNE) mnist_test = mnist_test.map( normalize_img, num_parallel_calls=tf.data.experimental.AUTOTUNE) mnist_test = mnist_test.cache() mnist_test = mnist_test.prefetch(tf.data.experimental.AUTOTUNE) -
我们现在准备应用镜像策略。这个策略的目标是在同一台机器上的所有 GPU 上复制模型。每个模型在不同的批次数据上进行训练,并应用同步训练策略:
mirrored_strategy = tf.distribute.MirroredStrategy() -
接下来,我们检查是否有两个设备对应于在本示例开始时创建的两个虚拟 GPU,如下所示:
print('Number of devices: {}'.format(mirrored_strategy.num_replicas_in_sync)) -
然后,我们将定义批量大小的值。给数据集的批量大小就是全局批量大小。全局批量大小是每个副本的所有批量大小的总和。所以,我们需要使用副本的数量来计算全局批量大小:
BATCH_SIZE_PER_REPLICA = 128 BATCH_SIZE = BATCH_SIZE_PER_REPLICA * mirrored_strategy.num_replicas_in_sync mnist_train = mnist_train.batch(BATCH_SIZE) mnist_test = mnist_test.batch(BATCH_SIZE) -
接下来,我们将使用镜像策略作用域定义并编译我们的模型。请注意,所有在作用域内创建的变量都会在所有副本之间镜像:
with mirrored_strategy.scope(): model = tf.keras.Sequential() model.add(tf.keras.layers.Flatten(name="FLATTEN")) model.add(tf.keras.layers.Dense(units=128 , activation="relu", name="D1")) model.add(tf.keras.layers.Dense(units=64 , activation="relu", name="D2")) model.add(tf.keras.layers.Dense(units=10, activation="softmax", name="OUTPUT")) model.compile( optimizer="sgd", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) -
一旦编译完成,我们就可以像平常一样拟合之前的模型:
model.fit(mnist_train, epochs=10, validation_data= mnist_test )
使用策略作用域是分布式训练所需要做的唯一事情。
它是如何工作的…
使用分布式 TensorFlow API 非常简单。你需要做的就是分配作用域。然后,可以手动或自动将操作分配给工作节点。请注意,我们可以轻松切换不同的策略。
这是一些分布式策略的简要概述:
-
TPU 策略类似于镜像策略,但它运行在 TPU 上。
-
多工作节点镜像策略与镜像策略非常相似,但模型在多台机器上进行训练,可能配有多个 GPU。我们需要指定跨设备通信。
-
中央存储策略在一台机器上使用同步模式并配备多个 GPU。变量不进行镜像,而是放置在 CPU 上,操作会复制到所有本地 GPU 上。
-
参数服务器策略是在一组机器上实现的。一些机器担任工作节点角色,另一些则担任参数服务器角色。工作节点进行计算,参数服务器存储模型的变量。
另见
有关 tf.distribute.Strategy 模块的一些参考,请访问以下网站:
-
TensorFlow 分布式训练:
www.tensorflow.org/guide/distributed_training -
tf.distributeAPI:www.tensorflow.org/api_docs/python/tf/distribute
还有更多…
在这个示例中,我们刚刚使用了镜像策略,并通过 Keras API 执行了程序。请注意,当在图模式下使用时,TensorFlow 分布式 API 的效果比在急切模式下要好。
这个 API 更新速度很快,因此请随时查阅官方文档,了解在什么场景下支持哪些分布式策略(Keras API、自定义训练循环或 Estimator API)。
保存和恢复 TensorFlow 模型
如果我们想在生产环境中使用机器学习模型,或者将我们训练好的模型用于迁移学习任务,我们必须保存我们的模型。在本节中,我们将概述一些保存和恢复权重或整个模型的方法。
准备工作
在本篇中,我们将总结几种保存 TensorFlow 模型的方法。我们将涵盖保存和恢复整个模型、仅保存权重以及模型检查点的最佳方式。
如何操作...
-
我们首先加载必要的库:
import tensorflow as tf -
接下来,我们将使用 Keras Sequential API 构建一个 MNIST 模型:
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() # Normalize x_train = x_train / 255 x_test = x_test/ 255 model = tf.keras.Sequential() model.add(tf.keras.layers.Flatten(name="FLATTEN")) model.add(tf.keras.layers.Dense(units=128 , activation="relu", name="D1")) model.add(tf.keras.layers.Dense(units=64 , activation="relu", name="D2")) model.add(tf.keras.layers.Dense(units=10, activation="softmax", name="OUTPUT")) model.compile(optimizer="sgd", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) model.fit(x=x_train, y=y_train, epochs=5, validation_data=(x_test, y_test) ) -
然后,我们将使用推荐的格式保存整个模型为磁盘上的 SavedModel 格式。此格式保存模型图和变量:
model.save("SavedModel") -
在磁盘上创建一个名为
SavedModel的目录。它包含一个 TensorFlow 程序,saved_model.pb文件;variables目录,包含所有参数的确切值;以及assets目录,包含 TensorFlow 图使用的文件:SavedModel |_ assets |_ variables |_ saved_model.pb请注意,
save()操作也接受其他参数。可以根据模型的复杂性以及传递给save方法的签名和选项创建额外的目录。 -
接下来,我们将恢复我们保存的模型:
model2 = tf.keras.models.load_model("SavedModel") -
如果我们更倾向于将模型保存为 H5 格式,我们可以传递一个以
.h5结尾的文件名,或者添加save_format="h5"参数:model.save("SavedModel.h5") model.save("model_save", save_format="h5") -
我们还可以使用
ModelCheckpoint回调来将整个模型或仅仅是权重保存到检查点结构中,间隔一定的训练周期。这个回调会被添加到fit方法中的callback参数中。在下面的配置中,模型的权重会在每个 epoch 后被保存:checkpoint_callback = tf.keras.callbacks.ModelCheckpoint(filepath="./checkpoint",save_weights_only=True, save_freq='epoch') model.fit(x=x_train, y=y_train, epochs=5, validation_data=(x_test, y_test), callbacks=[checkpoint_callback] ) -
我们稍后可以加载整个模型或仅加载权重,以便继续训练。在这里,我们将重新加载权重:
model.load_weights("./checkpoint")
现在,您已经准备好保存和恢复整个模型、仅保存权重或模型检查点。
工作原理...
在本节中,我们提供了几种保存和恢复整个模型或仅保存权重的方法。这使得您能够将模型投入生产,或避免从头开始重新训练一个完整的模型。我们还介绍了如何在训练过程中以及训练后保存模型。
另见
关于这个主题的一些参考资料,请访问以下网站:
-
官方训练检查点指南:
www.tensorflow.org/guide/checkpoint -
官方 SavedModel 格式指南:
www.tensorflow.org/guide/saved_model -
tf.saved_modelAPI:www.tensorflow.org/api_docs/python/tf/saved_model/save -
Keras 模型检查点 API:
www.tensorflow.org/api_docs/python/tf/keras/callbacks/ModelCheckpoint
使用 TensorFlow Serving
在本节中,我们将向您展示如何在生产环境中提供机器学习模型。我们将使用TensorFlow 扩展(TFX)平台的 TensorFlow Serving 组件。TFX 是一个 MLOps 工具,旨在为可扩展和高性能的模型任务构建完整的端到端机器学习管道。TFX 管道由一系列组件组成,涵盖数据验证、数据转换、模型分析和模型服务等内容。在这个教程中,我们将重点介绍最后一个组件,它支持模型版本控制、多个模型等功能。
准备开始
本节开始时,建议您阅读官方文档和 TFX 网站上的简短教程,网址为www.tensorflow.org/tfx。
在本例中,我们将构建一个 MNIST 模型,保存它,下载 TensorFlow Serving 的 Docker 镜像,运行它,并向 REST 服务器发送 POST 请求以获取一些图像预测。
如何操作...
-
在这里,我们将像之前一样开始,先加载必要的库:
import tensorflow as tf import numpy as np import requests import matplotlib.pyplot as plt import json -
我们将使用 Keras 的 Sequential API 构建一个 MNIST 模型:
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() # Normalize x_train = x_train / 255 x_test = x_test/ 255 model = tf.keras.Sequential() model.add(tf.keras.layers.Flatten(name="FLATTEN")) model.add(tf.keras.layers.Dense(units=128 , activation="relu", name="D1")) model.add(tf.keras.layers.Dense(units=64 , activation="relu", name="D2")) model.add(tf.keras.layers.Dense(units=10, activation="softmax", name="OUTPUT")) model.compile(optimizer="sgd", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) model.fit(x=x_train, y=y_train, epochs=5, validation_data=(x_test, y_test) ) -
然后,我们将把模型保存为 SavedModel 格式,并为每个版本的模型创建一个目录。TensorFlow Serving 需要特定的目录结构,并且模型必须以 SavedModel 格式保存。每个模型版本应导出到给定路径下的不同子目录中。这样,当我们调用服务器进行预测时,就可以轻松指定我们想要使用的模型版本:
![]()
图 12.7:TensorFlow Serving 期望的目录结构截图
上面的截图展示了所需的目录结构。在该结构中,我们有定义好的数据目录
my_mnist_model,后跟模型版本号1。在版本号目录下,我们保存 protobuf 模型和一个variables文件夹,其中包含需要保存的变量。我们需要注意,在数据目录中,TensorFlow Serving 会查找整数编号的文件夹。TensorFlow Serving 会自动启动并获取最大整数编号下的模型。这意味着,要部署一个新模型,我们需要将其标记为版本 2,并放入一个新的文件夹,该文件夹也标记为 2。TensorFlow Serving 随后会自动识别该模型。
-
然后,我们将通过 Docker 安装 TensorFlow Serving。如果需要,我们建议读者访问官方 Docker 文档,以获取 Docker 安装说明。
第一步是拉取最新的 TensorFlow Serving Docker 镜像:
$ docker pull tensorflow/serving -
现在,我们将启动一个 Docker 容器:将 REST API 端口 8501 映射到主机的 8501 端口,使用之前创建的模型
my_mnist_model,将其绑定到模型的基本路径/models/my_mnist_model,并将环境变量MODEL_NAME设置为my_mnist_model:$ docker run -p 8501:8501 \ --mount type=bind,source="$(pwd)/my_mnist_model/",target=/models/my_mnist_model \ -e MODEL_NAME=my_mnist_model -t tensorflow/serving -
然后,我们将显示图像进行预测:
num_rows = 4 num_cols = 3 plt.figure(figsize=(2*2*num_cols, 2*num_rows)) for row in range(num_rows): for col in range(num_cols): index = num_cols * row + col image = x_test[index] true_label = y_test[index] plt.subplot(num_rows, 2*num_cols, 2*index+1) plt.imshow(image.reshape(28,28), cmap="binary") plt.axis('off') plt.title('\n\n It is a {}'.format(y_test[index]), fontdict={'size': 16}) plt.tight_layout() plt.show()![]()
-
现在,我们可以将二进制数据提交到
<host>:8501并获取 JSON 响应,显示结果。我们可以通过任何机器和任何编程语言来完成此操作。这样做非常有用,因为不必依赖客户端拥有 TensorFlow 的本地副本。在这里,我们将向我们的服务器发送 POST 预测请求并传递图像。服务器将返回每个图像对应的 10 个概率,表示每个数字(从
0到9)的概率:json_request = '{{ "instances" : {} }}'.format(x_test[0:12].tolist()) resp = requests.post('http://localhost:8501/v1/models/my_mnist_model:predict', data=json_request, headers = {"content-type": "application/json"}) print('response.status_code: {}'.format(resp.status_code)) print('response.content: {}'.format(resp.content)) predictions = json.loads(resp.text)['predictions'] -
接下来,我们将展示我们图像的预测结果:
num_rows = 4 num_cols = 3 plt.figure(figsize=(2*2*num_cols, 2*num_rows)) for row in range(num_rows): for col in range(num_cols): index = num_cols * row + col image = x_test[index] predicted_label = np.argmax(predictions[index]) true_label = y_test[index] plt.subplot(num_rows, 2*num_cols, 2*index+1) plt.imshow(image.reshape(28,28), cmap="binary") plt.axis('off') if predicted_label == true_label: color = 'blue' else: color = 'red' plt.title('\n\n The model predicts a {} \n and it is a {}'.format(predicted_label, true_label), fontdict={'size': 16}, color=color) plt.tight_layout() plt.show()现在,让我们看一下 16 个预测的可视化表示:
![]()
它是如何工作的…
机器学习团队专注于创建机器学习模型,而运维团队则专注于部署模型。MLOps 将 DevOps 原则应用于机器学习。它将软件开发的最佳实践(如注释、文档、版本控制、测试等)带入数据科学领域。MLOps 的目标是消除生产模型的机器学习团队与部署模型的运维团队之间的隔阂。
在这个示例中,我们只关注使用 TFX Serving 组件来提供模型,但 TFX 是一个 MLOps 工具,可以构建完整的端到端机器学习管道。我们只鼓励读者探索这个平台。
还有许多其他解决方案可以用于提供模型,如 Kubeflow、Django/Flask,或者 AWS SageMaker、GCP AI 平台或 Azure ML 等托管云服务。
还有更多…
本章未涵盖的架构工具和资源链接如下:
-
使用 TensorFlow Serving 与 Docker:
www.tensorflow.org/serving/docker -
使用 TensorFlow Serving 与 Kubernetes:
www.tensorflow.org/tfx/serving/serving_kubernetes -
安装 TensorFlow Serving:
www.tensorflow.org/tfx/tutorials/serving/rest_simple -
TensorFlow 扩展:
www.tensorflow.org/tfx -
Kubeflow – Kubernetes 的机器学习工具包:
www.kubeflow.org/ -
GCP AI 平台:
cloud.google.com/ai-platform -
AWS SageMaker:
aws.amazon.com/fr/sagemaker/
分享您的经验感谢您花时间阅读本书。如果您喜欢本书,请帮助其他人找到它。请在www.amazon.com/dp/1800208863留下评论 |
|---|
























浙公网安备 33010602011771号