TensorFlow-开发者认证指南-全-
TensorFlow 开发者认证指南(全)
原文:
annas-archive.org/md5/a13e63613764fcdebedea75ef780532d译者:飞龙
前言
全球对深度学习专家的需求不断增长,TensorFlow 是构建深度学习应用的领先框架。为了满足全球对具备开发深度学习应用所需技能的开发者的需求,TensorFlow 开发者认证应运而生。这个 5 小时的实践考试旨在测试开发者在计算机视觉、自然语言处理、时间序列、序列和预测方面的模型构建、训练、保存和调试的基础知识。
本书作为一本全面的指南,帮助你为考试做准备。你将从了解机器学习的基本原理以及考试本身开始。每一章,你将通过动手编程示例和练习获得新的技能,在这些练习中,你将掌握构建图像分类、自然语言处理和时间序列预测模型的科学与艺术。
本书结束时,你将掌握所有必备的知识,能够在一次考试中顺利通过考试。
本书适合谁阅读
本书面向学生、数据科学家、机器学习工程师以及任何希望掌握如何使用 TensorFlow 构建深度学习模型并在第一次尝试中通过 TensorFlow 开发者认证考试的人。
本书内容
第一章,机器学习简介,讲解了机器学习的基础知识、类型、机器学习生命周期以及机器学习的应用。我们还将深入探讨成为认证 TensorFlow 开发者所需的条件。
第二章,TensorFlow 简介,探讨了 TensorFlow 生态系统,之后我们将设置工作环境。此外,我们还会了解数据表示,然后使用 TensorFlow 构建我们的 hello world 模型。最后,我们将通过检查如何调试和解决在构建模型过程中遇到的错误信息来结束本章。
第三章,使用 TensorFlow 进行线性回归,探讨了如何使用 TensorFlow 构建线性回归模型。接下来,我们将探索各种回归模型的评估指标。我们将更进一步,构建一个薪资预测模型,并通过掌握如何保存和加载模型来结束本章。
第四章,使用 TensorFlow 进行分类,探讨了机器学习中的分类建模,并讨论了你可能遇到的不同类型的分类问题。此外,我们还将研究评估分类问题的各种方法,并讨论如何为你的应用场景选择合适的分类评估指标。最后,我们将通过一个分类问题来结束本章,我们将在该问题中学习如何使用 TensorFlow 构建、编译、训练、预测和评估分类模型。
第五章,使用神经网络进行图像分类,讲解了神经网络的基本结构。我们将讨论前向传播、反向传播和梯度下降等概念,还会介绍一些重要组成部分,如输入层、隐藏层、输出层、激活函数、损失函数和优化器。最后,我们将通过使用 TensorFlow 构建一个基于神经网络的图像分类器来结束本章内容。
第六章,提升模型性能,探讨了提升模型性能的各种方法。我们将讨论以数据为中心的策略,如数据增强,并深入了解各种超参数及其影响,以及如何调整它们来提升模型的性能。
第七章,使用卷积神经网络进行图像分类,介绍了卷积神经网络(CNNs)。我们将看到 CNN 如何在图像分类任务中改变游戏规则,通过探索其结构,讨论卷积操作和池化操作等概念。我们还将研究开发者在处理实际图像时面临的挑战。最后,我们将通过将 CNN 应用于天气图像数据的分类,展示 CNN 在实际中的应用。
第八章,处理过拟合,详细讨论了过拟合问题。我们将研究什么是过拟合,以及它在实际应用中为何会发生。接下来,我们将进一步探讨克服过拟合的各种方法,如 dropout 正则化、早停法以及 L1 和 L2 正则化。我们将通过使用天气图像数据集案例研究来测试这些思路,帮助我们巩固对这些概念的理解。
第九章,迁移学习,介绍了迁移学习的概念,并讨论了我们可以在哪些场景和如何应用迁移学习。我们还将探讨一些在工作流中应用迁移学习的最佳实践。最后,我们将使用 TensorFlow 的预训练模型构建一个实际的图像分类器,结束本章内容。
第十章,自然语言处理简介,介绍了自然语言处理的基本概念。我们将讨论处理文本数据时的挑战,以及如何将语言转化为向量表示。我们将涵盖文本预处理和数据准备技术的基础思想,例如分词、填充、排序和词嵌入。接着,我们将通过使用 TensorFlow 的投影器可视化词嵌入,进一步深入学习。最后,我们将构建一个情感分类器。
第十一章,使用 TensorFlow 的自然语言处理,深入探讨了建模文本数据的挑战。我们将介绍递归神经网络(RNNs)及其变体,长短期记忆(LSTM)和门控循环单元(GRU)。我们将了解它们如何专门针对处理序列数据(如文本和时间序列数据)进行优化。我们将应用这些模型构建一个文本分类器,并展示如何在本章中应用预训练的词向量。最后,我们将展示如何使用 LSTM 构建一个儿童故事生成器。
第十二章,时间序列、序列与预测简介,介绍了时间序列数据,并探讨了时间序列数据的独特性质、核心特征、类型及其应用。我们将讨论建模时间序列数据时的一些挑战,并审视若干解决方案。我们将学习如何使用 TensorFlow 中的工具准备时间序列数据进行预测,并应用统计方法和机器学习技术来预测一个虚构超级商店的销售数据。
第十三章,使用 TensorFlow 的时间序列、序列和预测,讨论了如何使用 TensorFlow 中的内建和自定义学习率调度器,我们还将学习如何应用 lambda 层。我们将学习如何使用 RNN、LSTM、CNN 和 CNN-LSTM 网络构建时间序列预测模型。最后,我们将通过一个实际问题来结束本章和全书,在这个问题中,我们将收集真实世界的股票收盘价格,并构建一个预测模型。
为了从本书中获得最大的收获
要充分利用本书,您需要精通 Python,并且对学习机器学习的各个方面充满兴趣。我们将在本书中使用 Google Colab,但您也可以使用您喜欢的 IDE,只要满足以下要求:
| 书中涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Python 3.9.2 | Windows, macOS 或 Linux |
tensorflow==2.13.0 |
|
numpy==1.24.3 |
|
pandas==2.0.3 |
|
Pillow==10.0.0 |
|
scipy==1.10.1 |
|
tensorflow-datasets==4.9.2 |
如果你正在使用本书的数字版本,我们建议你亲自输入代码或通过本书的 GitHub 仓库(下节会提供链接)访问代码。这样做有助于避免因复制粘贴代码而可能出现的错误。
下载示例代码文件
你可以从 GitHub 下载本书的示例代码文件,链接在此:github.com/PacktPublishing/TensorFlow-Developer-Certificate-Guide。如果代码有更新,将在 GitHub 仓库中进行更新。
我们还提供其他代码包,来自我们丰富的书籍和视频目录,详情请访问 github.com/PacktPublishing/。快来看看吧!
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如:“这些元数据存储在 info 变量中。”
一段代码如下所示:
import numpy as np
from numpy import
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会以粗体显示:
(<tf.Tensor: shape=(2, 3), dtype=int64,
numpy=array([[10, 11, 12], [ 3, 4, 5]])>,
<tf.Tensor: shape=(2,),
dtype=int64, numpy=array([13, 6])>)
粗体:表示一个新术语、重要单词,或者屏幕上显示的单词。例如,在菜单或对话框中的单词会以粗体显示。以下是一个例子:“卷积神经网络(CNNs)是进行图像分类时首选的算法。”
提示或重要说明
以这种方式呈现。
联系我们
我们始终欢迎读者的反馈。
customercare@packtpub.com 并在邮件主题中提及书名。
勘误表:虽然我们已尽最大努力确保内容的准确性,但难免会有错误发生。如果你在本书中发现错误,我们将不胜感激你能向我们报告。请访问 www.packtpub.com/support/errata 并填写表单。
copyright@packt.com 并附上素材链接。
如果你有兴趣成为一名作者:如果你在某个领域拥有专业知识,并且有兴趣编写或贡献一本书,请访问 authors.packtpub.com.
分享你的想法
阅读完《TensorFlow 开发者证书指南》后,我们很希望听到你的想法!请点击这里直接进入 Amazon 评价页面并分享你的反馈。
你的评论对我们和技术社区非常重要,能够帮助我们确保提供优质的内容。
下载本书的免费 PDF 版本
感谢你购买本书!
你喜欢随时随地阅读但又无法携带纸质书籍吗?
你的电子书购买无法在你选择的设备上使用吗?
不用担心,现在每本 Packt 书籍都附赠免费 DRM 无限制的 PDF 版本。
在任何地方、任何设备上阅读。直接从你最喜欢的技术书籍中搜索、复制并粘贴代码到你的应用程序中。
福利不仅如此,你还可以获得独家折扣、时事通讯以及每日送达的精彩免费内容
按照以下简单步骤获取这些福利:
- 扫描二维码或访问以下链接

packt.link/free-ebook/978-1-80324-013-8
-
提交购买凭证
-
就这样!我们将直接通过电子邮件发送你的免费 PDF 及其他福利
第一部分 – TensorFlow 简介
在本书的这一部分,你将学习成功通过此考试所需的机器学习和深度学习基础知识。你将学习如何从不同来源和不同格式的数据中读取数据。你还将学习如何构建回归和分类模型,调试、保存、加载模型,并使用现实世界的数据进行预测。
本节包含以下章节:
-
第一章,机器学习简介
-
第二章,TensorFlow 简介
-
第三章,使用 TensorFlow 进行线性回归
-
第四章,使用 TensorFlow 进行分类
第一章:机器学习简介
现在是成为深度学习专家最令人激动的时刻。随着超高速计算机、开源算法、精心策划的数据集和负担得起的云服务的出现,深度学习专家已经具备了在各个领域构建惊人且具有影响力的应用的必要技能。计算机视觉、自然语言处理和时间序列分析只是深度学习专家可以产生实际影响的几个领域。任何具备正确技能的人都可以构建出具有突破性的应用,或许还能成为下一个 Elon Musk。为了实现这一点,需要具备足够的深度学习框架知识,例如 TensorFlow。
TensorFlow 开发者证书旨在培养新一代深度学习专家,这些专家在各个领域的需求量都很大。因此,加入这个俱乐部可以让你具备成为深度学习专家所需的专业知识,并且为你几周、几个月或几年的努力工作提供一个证书作为证明。
本章将首先对机器学习(ML)进行高层次的介绍,然后我们将探讨不同类型的机器学习方法。接下来,我们将深入了解机器学习生命周期和应用场景(在后续章节中,我们将涵盖一些实际操作的实现)。最后,我们将通过介绍 TensorFlow 开发者证书,分析通过考试所需的核心组件。通过本章的学习,您应该能够清楚地解释什么是机器学习,并对机器学习生命周期有一个基础的理解。同时,本章结束后,您将能够区分不同类型的机器学习方法,并清晰了解 TensorFlow 开发者证书考试的内容。
在本章中,我们将涵盖以下主题:
-
什么是机器学习?
-
机器学习算法类型
-
机器学习生命周期
-
探索机器学习应用场景
-
介绍学习旅程
什么是机器学习(ML)?
机器学习是人工智能(AI)的一个子领域,其中计算机系统从数据中学习模式,以执行特定任务或对未见过的数据进行预测,而无需明确编程。在 1959 年,Arthur Samuel 将机器学习定义为“一种使计算机能够在没有明确编程的情况下进行学习的研究领域”。为了更清晰地理解 Arthur Samuel 给出的定义,让我们通过银行业中一个广为人知的机器学习应用案例来进行拆解。
假设我们在伦敦市中心的一个财富 500 强银行的机器学习团队工作。我们肩负着自动化欺诈检测过程的责任,因为当前的手动流程太慢,且由于交易处理时间的延迟,每年让银行损失数百万英镑。根据前述定义,我们请求包含欺诈和非欺诈交易的历史交易数据,之后我们将通过机器学习生命周期(我们稍后会讲解)并部署我们的解决方案来防止欺诈行为的发生。
在这个例子中,我们使用了历史数据,这些数据提供了我们需要的特征(自变量),用以确定模型的结果,这通常被称为目标(因变量)。在这个场景中,目标是欺诈性或非欺诈性的交易,如图 1.1所示。

图 1.1 – 显示我们数据中的特征和目标的流程图
在前述的场景中,我们能够使用由特征和目标组成的历史数据来训练一个模型,从而生成用于对未见数据进行预测的规则。这正是机器学习的核心——使计算机在没有显式编程的情况下做出决策。在经典编程中,如图 1.2所示,我们输入数据和一些硬编码的规则——例如,交易的日交易量来判断是否是欺诈交易。如果客户超过了这个每日限额,客户的账户会被标记,人工审核员会介入,决定该交易是否为欺诈交易。

图 1.2 – 一种传统的编程方法
这种方法很快就会让银行不堪重负,客户因交易延迟而不断抱怨,而诈骗犯和洗钱者通过简单地将交易限制在银行定义的每日允许限额内,逃避系统的检测。随着每增加一个新特征,我们就需要更新规则。这种方法很快变得不切实际,因为总会有新的东西需要更新,才能让系统正常运行。就像一座纸牌屋一样,系统最终会崩溃,因为如此复杂的问题涉及数百万次每天变化的特征,可能几乎不可能通过显式编程来实现。
幸运的是,我们不需要手动编写代码。我们可以使用机器学习来构建一个模型,该模型能够根据历史数据中的一组输入特征,学习识别欺诈交易的模式。我们使用带标签的历史交易数据来训练我们的模型,这些数据包含欺诈和非欺诈交易。这样,我们的模型可以基于数据开发规则,如图 1.3所示,这些规则可以在未来用于检测欺诈交易。

图 1.3 – 一种 ML 方法
通过检查数据生成的规则被模型用于进行新的预测,以遏制欺诈交易。这种范式转变与传统编程不同,传统编程中应用程序是通过使用明确定义的规则构建的。在基于机器学习(ML)的应用程序中,例如我们的欺诈检测系统,模型通过学习识别模式并从训练数据中创建规则;然后,它使用这些规则对新数据进行预测,以高效地标记欺诈交易,如图 1.4所示:

图 1.4 – 一个 ML 模型使用规则对未见数据进行预测
在我们刚才分析的示例中,我们可以从图 1.1中看到,我们的训练数据通常以表格形式组织,由数值型数据(如交易金额和交易频率)以及类别型变量(如位置和支付类型)组成。在这种数据表示方式中,我们可以轻松地识别特征和目标。然而,像社交媒体中的文本数据、智能手机中的图像、流媒体电影中的视频等数据该如何处理呢,如图 1.5所示?当数据是非结构化的时,我们如何处理这些问题呢?幸运的是,我们有一个解决方案,那就是深度学习。

图 1.5 – 结构化与非结构化数据类型的示意图
深度学习是机器学习(ML)的一个子集,通过使用复杂的层次模型来模拟人脑,这些模型由多个处理层组成。深度学习的热潮源于深度学习算法在过去几年在许多现实应用中所记录的最先进的表现,如物体检测、图像分类和语音识别,因为深度学习算法能够建模数据中的复杂关系。在第 2和第 3节中,我们将更详细地讨论深度学习,并分别看到它在图像和文本应用中的实际应用。现在,让我们进一步探索机器学习的世界,看看机器学习算法的类型。
ML 算法的类型
在上一节中,我们了解了什么是机器学习,并且审视了一个使用标签数据的案例。在这一节中,我们将了解四种主要的机器学习方法,以帮助我们对每种方法的作用、适用范围和应用场景有一个基础的理解。机器学习算法的四种类型如下:
-
监督学习
-
无监督学习
-
半监督学习
-
强化学习
让我们看看这四种机器学习方法,它们将作为后续章节的基础知识。
监督学习
在监督学习中,机器学习模型通过使用由特征和目标构成的数据进行训练。这使得模型能够学习数据中的潜在关系。训练完成后,模型可以利用其新学到的知识对未见过的数据进行预测。例如,假设你想买一套房子。你会考虑房子的位置、房间数量、浴室数量、是否有花园以及房产类型等因素;这些因素可以视为特征,而房子的价格则是目标。或许在完成 TensorFlow 考试后,你可以卷起袖子,抓取一些房屋数据,训练一个模型,基于这些特征来预测房价。你可以使用你的房价预测模型,与房地产网站上的价格进行对比,为自己争取到一个好交易。
监督学习有两种类型——回归和分类。在回归任务中,标签是一个数值,就像我们之前给出的示例,其中目标是预测房子的价格。相反,在分类任务中,标签是一个类别,就像我们之前讨论的欺诈例子,目标是检测一笔交易是否为欺诈交易。在分类任务中,模型会学习将目标分类为不同的类别。
在处理由两个类别组成的分类任务时(例如,欺诈交易和非欺诈交易),称为二分类。当类别数超过两个时(例如,不同汽车品牌的分类),称为多分类。

图 1.6:多标签分类的示例,模型在图像中识别多个对象
多标签分类是另一种分类类型,常用于社交媒体应用(如 Facebook 和 Instagram)中的图像标记。与二分类和多分类任务中每个实例只有一个目标不同,在多标签分类中,我们的模型会为每个实例识别多个目标,如图 1.6所示,在给定的照片中,我们的模型识别出了一个女孩、一只狗、一名男孩和一只猫。
无监督学习
无监督学习是监督学习的对立面。在这种情况下,数据没有标签。模型必须自行弄清楚。这里,给定的无监督学习算法通过提供数据,并期望它从无标签数据中提取有意义的洞察,通过识别数据中的模式,而不依赖于预定义的目标——例如,一个大零售商店的图像,旨在为营销目的对其客户进行细分。你获得了包含商店客户人口统计和消费习惯的数据。通过采用无监督机器学习模型,你成功地将具有相似特征的客户聚类到不同的客户群体中。营销团队现在可以为每个通过模型识别的客户群体定制营销活动,这可能会提高活动的转化率。
半监督学习
半监督学习是监督学习和无监督学习的结合。在这种情况下,部分数据是有标签的(即它有特征和目标),其余的数据没有标签。在这种情况下,未标记的数据通常占主导地位。在这种情况下,我们可以应用无监督和监督学习方法的组合,以生成最佳结果,特别是在手动标记数据的成本和所需时间可能不切实际的情况下。在这里,模型利用可用的标签数据学习潜在的关系,然后将其应用于未标记的数据。
假设你为一家大型公司工作,公司收集了大量需要分类并发送到适当部门(如财务、市场营销和销售)的文档,以实现有效的文档管理,并且只有少数文档有标签。在这种情况下,我们应用半监督学习,在已标记的文档上训练模型,并将学习到的模式应用于其余未标记的文档进行分类。
强化学习
在强化学习中,与监督学习不同,模型不是通过训练数据来学习,而是通过与环境的互动来学习;它因做出正确决策而获得奖励,做出错误选择则受到惩罚。这是一种试错学习方法,模型通过过去的经验来学习,以便在未来做出更好的决策。模型的目标是最大化奖励。强化学习应用于自动驾驶汽车、机器人技术、交易与金融、问答系统和文本摘要等许多激动人心的应用场景。
现在我们可以清楚地区分不同类型的机器学习方法,我们可以看看机器学习生命周期的核心组成部分是什么,以及从项目的诞生到最终用户应用,我们应该采取哪些步骤。让我们来看看机器学习生命周期。
机器学习生命周期
在开始任何机器学习项目之前,我们必须考虑一些关键组成部分,这些组成部分可以决定我们的项目是否成功。这一点很重要,因为作为数据专业人员,我们希望构建并实施成功的机器学习项目,因此我们需要了解机器学习生命周期的运作方式。机器学习生命周期是实施机器学习项目的合理框架,如图 1.7所示:

图 1.7 – 机器学习生命周期
让我们详细看一下这些内容。
商业案例
在将最先进的模型应用于任何问题之前,务必花时间与利益相关者坐下来,明确了解商业目标或需要解决的痛点,因为没有清晰的目标,整个过程几乎肯定会失败。始终记住,整个过程的目标不是测试你迫不及待想尝试的新突破性模型,而是解决一个痛点或为公司创造价值。
一旦我们理解了问题,就可以将问题归类为监督学习或非监督学习任务。机器学习生命周期的这一阶段完全是关于提出正确的问题。我们需要与相关团队一起确定哪些关键指标能够定义项目的成功。需要哪些资源,预算、人力、计算能力和项目时间表如何?我们是否具备领域知识,还是需要专家的意见来定义和理解那些将决定项目成功的潜在因素和目标?这些都是我们作为数据专业人员在开始项目之前需要问的问题。
在考试中,我们需要理解每个问题的要求,然后才能解决它们。在本章结束前,我们会更多地讨论关于考试的内容。
数据收集与理解
当所有要求都已详细列出后,下一步是收集项目所需的数据。在这个阶段,我们首先确定收集什么类型的数据,以及从哪里收集。在开始之前,我们需要问自己,数据是否相关——例如,如果我们收集 1980 年的历史汽车数据,我们能否预测 2022 年汽车的价格?数据是由利益相关者提供,还是我们需要从数据库、物联网(IoT)设备或通过网页抓取来收集?任务是否需要收集二手数据?此外,我们还需要确定数据是一次性收集,还是会是一个持续的数据收集过程。一旦我们收集了项目所需的数据,我们接下来将对数据进行检查,以便理解它。
接下来,我们将检查数据,查看收集的数据是否符合正确的格式。例如,如果你从多个来源收集汽车销售数据,其中一个来源可能使用每小时公里数来计算汽车的里程,而另一个来源可能使用每小时英里数。此外,某些特征可能缺少值,我们也可能遇到重复值、异常值和无关特征。在这个阶段,我们将进行数据探索,获取数据的洞察,并进行数据预处理,以解决格式问题、缺失值、重复值、移除无关特征、处理异常值、不平衡数据和分类特征等问题。
建模
现在我们已经对业务需求有了充分的了解,并且决定了我们将要解决的机器学习问题类型,在完成预处理步骤后,我们也得到了高质量的数据。我们将把数据分成训练集,并保留一小部分作为测试集来评估模型的性能。我们将训练我们的模型,让它理解特征与目标变量之间的关系,使用我们的训练集。例如,我们可以使用银行提供的历史数据来训练我们的欺诈检测模型,然后用留出的测试集来测试它,评估模型的性能,再决定是否部署使用。我们会经历一个反复调整模型超参数的过程,直到得到最佳模型。
定义建模过程是否成功与业务目标密切相关,因为即使我们达到了 90%的高准确率,仍然会有 10%的错误率,这在医疗等高风险领域可能至关重要。假设你部署了一个 90%准确率的早期癌症检测模型,这意味着每 10 个人中就有可能失败一次;在 100 次尝试中,模型可能会失败约 10 次,而且有可能将患癌症的人误判为健康。这不仅可能导致该个体没有及时寻求医疗建议,还可能导致错过治疗时机,甚至死亡。你的公司可能会因此被起诉,责任会落到你的头上。为了避免这种情况,我们需要了解哪些指标对我们的项目至关重要,哪些可以相对宽松。还需要关注诸如类别不平衡、模型可解释性以及伦理问题等因素。
评估模型的指标有很多种,评估的类型取决于我们将处理的问题类型。我们将在第三章**,《TensorFlow 中的线性回归》中讨论回归指标,并在第四章**,《TensorFlow 中的分类》中讨论分类指标。
错误分析
我们还没有准备好部署。记得之前提到的那 10%的数据可能会影响我们的项目吗?我们将在这里解决这个问题。我们进行错误分析,识别错误分类的标签,以找出模型未能识别的原因。我们在训练数据中是否有足够具有代表性的错误分类标签样本?我们需要决定是否收集更多数据来捕获模型失败的案例。我们能否生成合成数据来捕获错误分类的标签?还是这些错误分类的数据源自错误的标签?
错误标记的数据会影响模型的表现,因为它会让模型学习到特征和目标之间错误的关系,从而导致模型在处理未见过的数据时表现不佳,使得模型变得不可靠,整个过程也变得浪费资源和时间。一旦我们解决了这些问题并确保标签准确,我们需要重新训练并重新评估我们的模型。这些步骤是持续进行的,直到达到业务目标,然后我们才能继续部署我们的模型。
模型部署与监控
在解决了错误分析步骤中发现的问题后,我们现在可以将模型部署到生产环境中。部署方法有很多种。我们可以将模型部署为网络服务、云端服务或边缘设备上的服务。模型部署既充满挑战也令人兴奋,因为构建和训练模型的核心目的就是让最终用户能够应用它解决实际问题。一旦我们部署了模型,我们还需要监控模型,确保业务的整体目标持续达成,即便是表现最好的模型,也可能随着时间的推移因为概念漂移和数据漂移而开始表现不佳。因此,在部署模型之后,我们不能就此“退休”在某个岛上。我们需要不断监控我们的模型,并在必要时重新训练模型,以确保它继续表现最佳。
我们现在已经概览了整个机器学习生命周期。当然,实际上我们可以进一步深入讨论许多细节,但这些超出了本次考试的范围。因此,我们现在将重点转向一些机器学习可以应用的令人兴奋的用例。
探索机器学习用例
除了汽车价格预测和欺诈检测的应用案例之外,让我们再看看机器学习的其他一些令人兴奋的应用。也许这会让你既为考试振奋,也能激励你在机器学习的旅程中创造出一些精彩的作品。
医疗健康
HearAngel 使用人工智能自动防止耳机用户听力损失,通过追踪用户暴露于耳机中不健康的声音水平。Insitro 利用机器学习和生物学知识进行药物发现和开发。机器学习在医疗保健中的其他应用包括智能记录保存、数据收集、疾病爆发预测、个性化医学和疾病识别。
零售行业
ML 工程师正在通过部署模型来革新零售行业,提升客户体验并提高盈利能力。这是通过优化商品规划、预测客户行为、提供虚拟助手、库存优化、追踪客户情感、价格优化、产品推荐和客户细分等方式实现的。在零售行业中,ML 工程师通过自动化繁琐的人工流程为企业创造价值。
娱乐行业
娱乐行业目前在自动剧本和歌词生成中应用了机器学习(ML)/人工智能(AI)。是的,现在确实有电影剧本写作的 ML 模型。比如有一部短篇科幻电影叫做 Sunspring (arstechnica.com/gaming/2021/05/an-ai-wrote-this-movie-and-its-strangely-moving/)。此外,ML/AI 还被用于自动字幕生成、增强现实、游戏开发、目标营销、情感分析、电影推荐、销售预测等众多领域。因此,如果你计划作为 ML 工程师在这个行业中占有一席之地,你将能做很多事情,并且你肯定能提出一些新的创意。
教育
Readrly 利用深度学习技术创作个性化儿童故事,提升年轻读者的学习体验。通过根据每个孩子的兴趣和技能水平定制故事,Readrly 以一种有趣且富有吸引力的方式支持儿童的阅读发展。
农业
机器学习在农业中的应用案例不胜枚举。ML/AI 可以用于价格预测、疾病检测、天气预测、产量映射、土壤和作物健康监测以及精准农业等。
在这里,我们介绍了一些机器学习的应用案例。然而,更令人兴奋的是,作为一名 ML/DL 工程师,你将能够将你的知识应用于任何行业,只要有数据可用。这就是作为 ML 工程师的魅力所在——没有限制。我们已经在本章中介绍了很多内容,但还有一部分内容非常重要,因为它集中在考试本身。让我们深入了解一下。
介绍学习旅程
TensorFlow 开发者证书考试由 Google 设计和开发,旨在评估数据专业人员在模型构建和使用 TensorFlow 训练深度学习模型方面的专业技能。该考试使数据专业人员能够展示他们在解决实际问题时使用 ML/DL 技术的能力,如 图 1.8 所示。

图 1.8 – 考试目标
让我们深入探讨一下为什么你应该参加这次考试。
为什么要参加考试?
你应该参加 TensorFlow 开发者证书考试的一个最有力的理由是,它可以帮助你找到一份工作。根据 Statista 的报告,全球 AI 市场预计将呈指数级增长,到 2030 年将达到 2 万亿美元(www.statista.com/statistics/1365145/artificial-intelligence-market-size/#:~:text=The%20market%20for%20artificial%20intelligence,nearly%20two%20trillion%20U.S.%20dollars)。
这种快速增长是由自动驾驶汽车、图像识别和自然语言处理等领域的持续进展推动的,带动了各行各业一波新的应用浪潮。预计这种增长将导致对能够构建前沿机器学习解决方案的深度学习专家的需求增加。
鉴于这一发展,招聘人员和人力资源经理正寻找能够使用 TensorFlow 构建深度学习模型的熟练候选人,这个证书可以帮助你从人群中脱颖而出。为了进一步加速你的求职进程,谷歌建立了TensorFlow 证书网络,这是一个全球 TensorFlow 认证开发者的在线数据库,如图 1.9所示。招聘经理可以通过位置、工作经验年数等多种筛选条件轻松找到合适的候选人,以便为他们的机器学习和深度学习解决方案提供支持,同时也可以根据候选人的名字验证其身份。

图 1.9 – TensorFlow 认证开发者的地图展示
除了帮助你找到第一份工作,TensorFlow 开发者证书还可以帮助你提升职业生涯。如果你已经在使用 TensorFlow,这个证书可以帮助你向雇主展示你的专业技能。这可能会带来晋升和加薪。
现在我们已经了解了一些你应该参加考试的理由,接下来的逻辑步骤是看看考试的内容是什么。让我们来看看。
这次考试是关于什么的?
如果你打算成为一名认证的 TensorFlow 开发者,有一些你需要了解的事项。下面是一些你需要知道的要点,帮助你顺利通过 TensorFlow 开发者证书考试:
-
TensorFlow 开发者技能
-
使用 TensorFlow 2.x 构建和训练神经网络模型
-
图像分类
-
自然语言 处理(NLP)
-
时间序列、序列和预测
你可以在这里找到完整的考试详情:www.tensorflow.org/static/extras/cert/TF_Certificate_Candidate_Handbook.pdf。然而,本书详细介绍了考试的每个部分,帮助确保成功。考试费用为 100 美元,但可以申请助学金,如果获得批准,你只需支付一半的考试费用。助学金必须在收到后的 90 天内使用,并且仅对一次尝试有效。申请助学金时,你需要提供有关自己的信息、为什么需要助学金,以及你的 TensorFlow 项目组合(如果有的话)。你可以通过以下链接获取更多关于如何申请 TensorFlow 教育助学金的信息:www.tensorflow.org/static/extras/cert/TF_Education_Stipend.pdf。
我们现在已经讨论了“什么”和“为什么”。现在,让我们看看你如何能够通过考试。
如何通过考试
如果你希望成为一名认证的 TensorFlow 开发者,应该知道一些事情。首先,你需要熟练掌握 Python。其次,你需要对机器学习概念有深入理解,并能够使用 TensorFlow 构建和训练深度学习模型。如果你还不熟悉 Python 编程,那么 Steven F Lott 的 Modern Python Cookbook – Second Edition 是一个很好的起点。
这里有一些提示可以帮助你通过 TensorFlow 开发者证书考试:
-
复习课程材料:在参加考试之前,务必详细复习 TensorFlow 考生手册中每个主题的材料。特别要注意构建和训练模型,因为考试是实践性的。
-
模型构建:除了复习课程材料外,获得一些 TensorFlow 实践经验也非常重要。尝试构建模型,涵盖考试要求的每个部分。本书将帮助你掌握机器学习的核心基础,并通过实践方式带你逐步完成每个考试部分,确保你能够轻松地使用 TensorFlow 构建和训练各种模型——从简单的线性模型到复杂的神经网络。
-
理解考试形式:这次考试与许多其他考试不同。它是一个五小时的编程考试,问题涵盖我们之前概述的每个部分。你将会得到一个任务,并被要求在 PyCharm 中编写代码来解决它。因此,你需要花一些时间在考试前掌握如何在 PyCharm 中构建、训练和保存模型。考试是开卷的,所以你可以在考试期间使用任何资源。
-
练习,练习,再练习:为考试做准备的最佳方法之一就是通过练习解决问题。你会在本书的每一章中找到大量的实操练习题,并且书中的 GitHub 仓库也提供了代码文件。此外,你还可以在 TensorFlow 网站和 Kaggle 上找到大量数据集。
完成本书后,你应该准备好参加 TensorFlow 开发者证书考试。
何时参加考试
根据你的经验,你可能需要更多或更少的时间来准备考试。如果你已经熟悉 TensorFlow,具备动手构建模型的能力,你可能需要 3 周到 2 个月来准备考试。然而,如果你完全是 TensorFlow 新手,根据考试官网的要求,建议花费大约 6 个月来充分准备考试。但这些规则并不是一成不变的,每个人的情况不同,所以请按照自己的节奏来准备。
考试技巧
注册考试后,你可以在六个月内参加考试。提前为考试设定一个目标日期是完全可以的。考试将在 PyCharm 中进行,因此,如果你不熟悉 PyCharm,你需要提前几天或几周来适应它。以下是 Jeff Henton 的一段优秀视频教程,帮助你设置 PyCharm 环境:www.youtube.com/watch?v=zRY5lx-So-c。确保安装指定版本的 PyCharm。你还可以通过以下链接了解更多关于设置考试环境的信息:www.tensorflow.org/static/extras/cert/Setting_Up_TF_Developer_Certificate_Exam.pdf。
在考试之前,制定一个清晰的学习计划,覆盖考试大纲中的内容,这对你非常有帮助。本书将帮助你在这条学习路上,因此你应该关注接下来的章节,因为我们将开始编写代码并解决考试中涉及的核心组件。从此以后,这些内容构成了考试的基础。考试当天,我建议你找一个安静、舒适的地方来参加考试。确保你已经休息好,而不是带着疲惫的身体去应考,因为考试时长为五小时。同时,检查一下你的 PyCharm 和网络连接。不要惊慌,仔细阅读题目,确保你清楚了解每个问题的要求。从第 1 到第 5 道题开始。由于后面的题目会越来越难,因此最好先快速完成简单的题目,再解决较难的题目。
然而,你应该正确地控制自己的进度。每次提交时,你保存的模型都会被评分,在规定的 5 小时时间框架内,你可以提交任意次数,直到达到最佳结果。如果你能更快地工作,特别是当你的模型在 PyCharm 中运行时,你也可以在 Colab 中运行你的模型。Colab 提供了免费的 GPU 访问权限来训练你的模型。考试只会在 PyCharm 中评分,因此请牢记这一点。确保你保存了在 Colab 中训练的模型,并将其移动到指定的目录中,以便提交考试。
如果你需要帮助,你可以使用 Stack Overflow。你也可以查看本书中使用的代码,或者任何你用来准备考试的其他材料。不过,如果某个问题太难,继续做其他问题。当你完成后,你可以回到那个难题,逐步解决它,以避免将所有时间浪费在一个难题上。另外,你可以提交多次,因此请继续优化你的模型,直到达到最佳性能。
考试后会发生什么
考试在五小时后准时结束,但你可以提前提交。提交后,如果你通过考试,你将收到一封祝贺邮件。通过考试后,你将成为 Google TensorFlow 开发者社区的一员,为自己打开更多的机会之门。假设你通过了考试(我希望你能通过),大约一周后你将获得证书,证书将类似于图 1.10,并且大约两周后你将加入 Google TensorFlow 社区。证书有效期为三年。

图 1.10 – TensorFlow 开发者证书
现在,你已经知道了主题、时间框架、费用、如何准备、考试当天需要做什么,以及考试后会发生什么。至此,本章已结束。我们在本章中涵盖了大量的理论内容,这些内容将为我们在接下来的章节中共同完成的工作打下基础。
总结
本章概述了机器学习(ML)、深度学习以及各种机器学习方法。还介绍了机器学习生命周期以及在不同领域中的应用案例。我们概述了 TensorFlow 开发者证书的高层内容,并提供了考试组成部分和准备方法的信息。在本章结束时,你应该对机器学习的概念及其类型有一个扎实的基础理解。你现在应该能够判断哪些问题是机器学习问题,哪些问题需要经典编程。你还应该能够将机器学习问题拆解为不同的类型,并熟悉准备 Google TensorFlow 开发者证书考试所需的步骤。
在下一章中,我们将了解什么是 TensorFlow,设置我们的环境,并开始编码,直到本书结束。
问题
让我们测试一下在本章中学到的内容:
-
什么是机器学习?
-
什么是深度学习?
-
机器学习有哪些类型?
-
机器学习生命周期的步骤有哪些?
-
TensorFlow 开发者证书是什么?
-
考试的核心领域有哪些?
进一步阅读
若想了解更多,您可以查看以下资源:
-
TensorFlow 开发者证书 概述:
www.tensorflow.org/certificate -
动手学机器学习:使用 scikit-learn 和科学 Python 工具包 由 Amr T.编著,Packt 出版
-
深度学习:方法与应用 由李登和董宇编著:
doi.org/10.1561/2000000039 -
由算法编写的电影结果既搞笑又 紧张刺激:
arstechnica.com/gaming/2021/05/an-ai-wrote-this-movie-and-its-strangely-moving/ -
Python 机器学习——第三版 由 Sebastian Raschka 和 Vahid Mirjalili 编著
第二章:TensorFlow 简介
在 TensorFlow 时代之前,深度学习的格局与今天截然不同。数据专业人士没有那么多全面的工具来帮助开发、训练和部署神经网络。这给实验不同的架构以及调整模型设置以解决复杂任务带来了挑战,因为数据专家通常需要从零开始构建自己的模型。这一过程非常耗时,一些专家可能花费数天甚至数周的时间来开发有效的模型。另一个瓶颈是部署训练好的模型的难度,这使得神经网络的实际应用在早期阶段非常具有挑战性。
但如今,一切都发生了变化;有了 TensorFlow,你可以做许多令人惊叹的事情。在本章中,我们将从审视 TensorFlow 生态系统开始,从高层次讨论与使用 TensorFlow 构建最先进应用相关的各个组件。我们将通过设置工作环境来开始这段旅程,确保满足考试和即将进行的实验的要求。我们还将了解 TensorFlow 的基本概念,理解张量的概念,探索 TensorFlow 中的基本数据表示和操作,并使用这个强大的工具构建我们的第一个模型。我们将通过学习如何调试和解决 TensorFlow 中的错误信息来结束本章。
到本章结束时,你将理解 TensorFlow 的基础知识,包括什么是张量以及如何使用它们执行基本的数据操作。你将具备信心,能够使用 TensorFlow 构建你的第一个模型,并调试和解决在此过程中可能出现的任何错误信息。
本章将涵盖以下主题:
-
什么是 TensorFlow?
-
设置我们的环境
-
数据表示
-
TensorFlow 中的 Hello World
-
调试和解决错误信息
技术要求
我们将使用 python >= 3.8.0,并配合以下包,它们可以通过 pip install 命令安装:
-
tensorflow>=2.7.0 -
tensorflow-datasets==4.4.0 -
pillow==8.4.0 -
pandas==1.3.4 -
numpy==1.21.4 -
scipy==1.7.3
本书的代码包可在以下 GitHub 链接中找到:github.com/PacktPublishing/TensorFlow-Developer-Certificate。此外,所有习题的解决方案也可以在 GitHub 仓库中找到。如果你是 Google Colab 的新手,这里有一个很棒的资源,可以帮助你快速入门:www.youtube.com/watch?v=inN8seMm7UI&list=PLQY2H8rRoyvyK5aEDAI3wUUqC_F0oEroL。
什么是 TensorFlow?
在上一章中,我们探讨了可以利用我们对机器学习(ML)的知识构建的不同类型的应用,从聊天机器人到人脸识别系统,从房价预测到银行业的欺诈检测——这些都是我们可以使用深度学习框架(如 TensorFlow)构建的一些令人兴奋的应用。我们逻辑上会问,TensorFlow 究竟是什么?我们为什么要学习它呢?
TensorFlow是一个开源的端到端框架,用于构建深度学习应用。它由 Google 的一组数据专业人员于 2011 年开发,并在 2015 年公开发布。TensorFlow 是一个灵活且可扩展的解决方案,使我们能够通过 Keras API 轻松构建模型。它允许我们访问大量的预训练深度学习模型,这使得它成为行业和学术界许多数据专业人员首选的框架。目前,TensorFlow 被 Google、DeepMind、Airbnb、Intel 等众多大公司使用。
今天,使用 TensorFlow,你可以轻松地在单台 PC 上训练深度学习模型,或者通过 AWS 等云服务,或者使用集群计算进行分布式训练。构建模型只是数据专业人员工作的一部分,那么可视化、部署和监控模型呢?TensorFlow 提供了广泛的工具来满足这些需求,例如 TensorBoard、TensorFlow lite、TensorFlow.js、TensorFlow Hub,以及TensorFlow Extended(TFX)。这些工具使得数据专业人员能够构建和部署可扩展、低延迟、由机器学习驱动的应用程序,涵盖多个领域——无论是在 Web、移动端,还是边缘设备上。为了支持 TensorFlow 开发者,TensorFlow 提供了全面的文档支持,并且有一个庞大的开发者社区,他们报告 bug 并为这个框架的进一步发展和改进做出贡献。
TensorFlow 生态系统的另一个核心特性是其能够访问各种各样的数据集,这些数据集跨越了不同的机器学习问题类型,如图像数据、文本数据和时间序列数据。这些数据集通过 TensorFlow Datasets 提供,是掌握如何使用 TensorFlow 解决实际问题的绝佳途径。在接下来的章节中,我们将探索如何使用 TensorFlow 生态系统内提供的各种数据集构建模型,以解决计算机视觉、自然语言处理和时间序列预测问题。
我们已经探索了一些在 TensorFlow 生态系统中不可或缺的工具。浏览这些功能(以及将来会添加的新功能)并了解它们总是一个好主意,可以访问官方网页:www.tensorflow.org/。不过,在考试中不会涉及这些内容。这里的目的是让你熟悉生态系统中已有的工具。考试的重点是使用 TensorFlow 进行建模,因此我们将只使用生态系统中的工具,例如 TensorFlow 数据集、Keras API 和 TensorFlow Hub 来实现这一目标。
设置我们的工作环境
在我们研究 TensorFlow 中的数据表示之前,先让我们设置工作环境。我们将从导入 TensorFlow 并检查其版本开始:
import tensorflow as tf
#To check the version of TensorFlow
print(tf.__version__)
当我们运行这段代码时,得到如下输出:
2.8.0
好极了!我们已经成功导入了 TensorFlow。接下来,让我们导入 NumPy 和几个数据类型,因为我们将在本章中很快用到它们:
import numpy as np
from numpy import *
我们已经顺利完成了所有导入步骤,没有遇到任何错误。接下来,我们将探讨 TensorFlow 中的数据表示方法,因为我们的工作环境已经完全设置好。
数据表示
在我们利用机器学习解决复杂任务的过程中,我们会遇到各种类型的原始数据。我们的主要任务是将这些原始数据(可能是文本、图像、音频或视频)转化为数值表示。这些表示使得我们的机器学习模型能够轻松地消化数据并高效地学习其中的潜在模式。为此,TensorFlow 及其基础数据结构——张量,发挥了重要作用。虽然数值数据通常用于训练模型,但我们的模型同样能够高效处理二进制和分类数据。对于这类数据,我们采用诸如独热编码(one-hot encoding)等技术将其转换为适合模型使用的格式。
张量是为数值数据表示设计的多维数组;尽管它们与 NumPy 数组有一些相似之处,但它们具有一些独特的特点,使得它们在深度学习任务中具有优势。这些关键优势之一是它们能够利用 GPU 和 TPU 的硬件加速,显著提高计算操作的速度,这在处理图像、文本和视频等输入数据时尤其有用,正如我们将在本书后面的章节中看到的那样。
让我们快速看一个实际应用示例。假设我们正在构建一个汽车识别系统,如 图 2.1 所示。我们将从收集各种尺寸、形状和颜色的汽车图像开始。为了训练我们的模型识别这些不同的汽车,我们会将每张图像转换为输入张量,包含高度、宽度和颜色通道。当我们用这些输入张量训练模型时,它会根据训练集中汽车的像素值表示学习模式。训练完成后,我们可以使用训练好的模型识别不同形状、颜色和尺寸的汽车。如果我们现在将一张汽车的图像输入已训练的模型,它会返回一个输出张量,经过解码后转换成可供人类理解的格式,从而帮助我们识别出这是什么类型的汽车。

图 2.1 – TensorFlow 中的数据表示
现在我们已经有了直觉,接下来让我们进一步探讨张量的更多细节。我们将从学习几种生成张量的方法开始。
创建张量
在 TensorFlow 中,我们可以通过几种方式生成张量。然而,我们将重点介绍使用 tf.constant、tf.Variable 和 tf.range 创建张量对象。回想一下,我们已经在设置工作环境的部分导入了 TensorFlow、NumPy 和数据类型。接下来,让我们运行以下代码,使用 tf.constant 生成我们的第一个张量:
#Creating a tensor object using tf.constant
a_constant = tf.constant([1, 2, 3, 4 ,5, 6])
a_constant
当我们运行这段代码时,我们生成了第一个张量。如果一切顺利,输出应该如下所示:
<tf.Tensor: shape=(6,), dtype=int32,
numpy=array([1, 2, 3, 4, 5, 6], dtype=int32)>
很棒!别担心,随着我们继续深入,我们会讨论输出并形成更清晰的认识。但现在,让我们使用 tf.Variable 函数生成一个类似的张量对象:
#Creating a tensor object using tf.Variable
a_variable = tf.Variable([1, 2, 3, 4 ,5, 6])
a_variable
a_variable 变量返回以下输出:
<tf.Variable 'Variable:0' shape=(6,) dtype=int32,
numpy=array([1, 2, 3, 4, 5, 6], dtype=int32)>
虽然两种情况的输入是相同的,但 tf.constant 和 tf.Variable 是不同的。使用 tf.constant 生成的张量是不可变的,而 tf.Variable 生成的张量可以在未来重新赋值。随着我们进一步探索张量,我们会更详细地讨论这一点。与此同时,让我们看看另一种使用 tf.range 生成张量的方法:
# Creating tensors using the range function
a_range = tf.range(start=1, limit=7)
a_range
a_range 返回以下输出:
<tf.Tensor: shape=(6,), dtype=int32,
numpy=array([1, 2, 3, 4, 5, 6], dtype=int32)>
很好!从输出结果来看,如果我们直观地比较三种生成张量的方法,我们可以很容易得出结论:a_constant 和 a_range 的输出相同,但与 a_variable 的输出略有不同。当执行张量操作时,这种差异变得更加明显。为了展示这一点,让我们从张量秩开始,继续探索张量操作。
张量秩
如果你不是数学背景的,不要担心。我们会一起覆盖所有内容,我们这里不会讨论火箭科学——这是一个承诺。张量的秩标识张量的维度数。秩为0的张量称为标量,因为它没有维度。向量是秩为1的张量,它只有一个维度,而一个二维张量的矩阵秩为2。

图 2.2 – 张量秩
我们已经练习了如何使用三种不同的函数来生成张量。为了说明,我们可以安全地定义标量为只有大小没有方向的量。标量量的例子有时间、质量、能量和速度;这些量有一个单一的数值,例如1、23.4或50。让我们回到笔记本,使用tf.constant函数生成一个标量:
#scalar
a = tf.constant(1)
a
我们首先创建一个标量,它是一个单一的值,返回如下输出:
<tf.Tensor: shape=(), dtype=int32, numpy=1>
从返回的输出可以看出,形状没有值,因为输出是一个标量量,只有一个数值。如果我们尝试使用4,numpy的输出将是4,而其他输出属性将保持不变,因为4仍然是一个标量量。
现在我们已经了解了标量(秩为0的张量),让我们更进一步,来看一下向量。为了更好理解,向量是既有大小又有方向的量。向量的例子有加速度、速度和力。让我们回到笔记本,尝试生成一个包含四个数字的向量。为了改变一下,这次我们将使用浮点数,因为我们可以使用浮点数生成张量。另外,如果你注意到的话,返回的默认数据类型是int32,这是我们之前用来生成整数张量的数据类型:
#vector
b= tf.constant([1.2,2.3,3.4,4.5])
b
从我们的结果中可以看出,返回的数据类型是float32,形状为4:
<tf.Tensor: shape=(4,), dtype=float32,
numpy=array([1.2, 2.3, 3.4, 4.5], dtype=float32)>
接下来,让我们生成一个矩阵。矩阵是一个按行和列排列的数字数组。让我们在笔记本中尝试一个矩阵:
#matrix
c =tf.constant([[1,2],[3,4]])
c
<tf.Tensor: shape=(2, 2), dtype=int32,
numpy= array([[1, 2], [3, 4]], dtype=int32)>
上面的矩阵是一个 2 x 2 矩阵,我们可以通过检查shape输出推断出这一点。我们还看到数据类型是int32。让我们生成一个更高维度的张量:
#3-dimensional tensor
d=tf.constant([[[1,2],[3,4],[5,6]],[[7,8],[9,10],[11,12]]])
d
输出是一个 2 x 3 x 2 的张量,数据类型为int32:
<tf.Tensor: shape=(2, 3, 2), dtype=int32,
numpy= array([[[ 1, 2],[ 3, 4],[ 5, 6]],
[[ 7, 8], [ 9, 10], [11, 12]]], dtype=int32)>
你应该试着操作一些张量。尝试用tf.Variable创建一些张量,看看你是否能重现到目前为止的结果。接下来,让我们看看如何解释张量的属性。
张量的属性
现在我们已经建立了对标量、向量和张量的理解,让我们详细探讨如何解释张量输出。之前,我们以零碎的方式检查了张量。现在,我们将学习如何从张量的打印表示中识别其关键属性——秩、形状和数据类型。当我们打印张量时,它会显示变量名、形状和数据类型。到目前为止,我们在创建张量时使用的是默认参数。让我们进行一些调整,看看这如何改变输出。
我们将使用tf.``V``ariable来生成一个标量张量,选择float16作为数据类型,并将其命名为TDC。(如果你想知道TDC是什么意思,那是TensorFlow 开发者证书的缩写。)接下来,我们将运行代码:
#scalar
a = tf.Variable(1.1, name="TDC", dtype=float16)
a
<tf.Variable 'TDC:0' shape=() dtype=float16, numpy=1.1>
当我们检查输出时,我们可以看到张量的名称现在是TDC: 0,张量的形状是0,因为该张量的秩为0。我们选择的数据类型是float16。最后,张量还具有numpy值1.1。这个例子展示了我们如何在构建 TensorFlow 张量时配置诸如数据类型和名称等属性。
接下来,让我们看看一个向量,看看我们能从它的属性中学到什么信息:
#vector
b= tf.Variable([1.2,2.3,3.4,4.5], name="Vector", dtype=float16)
b
这里,我们再次包含了参数和张量的名称,并且改变了默认的数据类型。从输出中我们可以看到,结果与我们得到的标量量类似:
<tf.Variable ''Vector:0' shape=(4,) dtype=float16,
numpy=array([1.2, 2.3, 3.4, 4.5])>
这里,张量的名称是'Vector:0,形状的值为4(这对应于条目数量),并且张量的数据类型为float16。为了有点乐趣,你可以尝试不同的配置,看看你所做的更改对返回输出的影响;这是一个非常好的学习和理解事物是如何运作的方式。当我们打印张量输出的结果时,我们可以看到张量的不同属性,就像我们检查标量和向量量时那样。然而,通过利用 TensorFlow 函数,我们可以获得更多关于张量的信息。让我们从使用tf.rank()函数来检查标量、向量和矩阵的秩开始:
#scalar
a = tf.constant(1.1)
#vector
b= tf.constant([1.2,2.3,3.4,4.5])
#matrix
c =tf.constant([[1,2],[3,4]])
#Generating tensor rank
print("The rank of the scalar is: ",tf.rank(a))
print(" ")
print("The rank of the vector is: ",tf.rank(b))
print(" ")
print("The rank of the matrix is: ",tf.rank(c))
我们运行前面的代码来生成标量、向量和矩阵。之后,我们使用tf.rank函数打印出它们的秩。以下是输出结果:
The rank of the scalar is: tf.Tensor(0, shape=(), dtype=int32)
The rank of the vector is: tf.Tensor(1, shape=(), dtype=int32)
The rank of the matrix is: tf.Tensor(2, shape=(), dtype=int32)
返回的输出是一个张量对象,它显示了张量的秩以及张量的形状和数据类型。要获取张量的秩作为数值,我们必须在返回的张量上使用.numpy()来检索张量的实际秩:
print("The rank of the scalar is: ",tf.rank(a).numpy())
The rank of the scalar is: 0
然而,直接获得张量秩的一个更简单方法是使用ndim,无需重新评估。让我们接下来看看这个方法:
#Generating details of the dimension
print("The dimension of the scalar is: ",a.ndim)
print(" ")
print("The dimension of the vector is: ",b.ndim)
print(" ")
print("The dimension of the matrix is: ",c.ndim)
当我们运行代码时,得到以下输出:
The dimension of the scalar is: 0
The dimension of the vector is: 1
The dimension of the matrix is: 2
接下来,让我们通过使用dtype参数打印出所有三个量的数据类型,以生成每个张量的数据类型:
#printing the data type
print("The data type of the scalar is: ",a.dtype)
print(" ")
print("The data type of the vector is: ",b.dtype)
print(" ")
print("The data type of the matrix is: ",c.dtype)
当我们运行代码时,得到以下输出。
The data type of the scalar is: <dtype: 'float32'>
The data type of the vector is: <dtype: 'float32'>
The data type of the matrix is: <dtype: 'int32'>
从之前的输出中,我们可以看到数据类型。接下来,我们来看看张量的形状:
#Generating details of the tensor shape
print("The Shape of the scalar is: ",a.shape)
print(" ")
print("The Shape of the vector is: ",b.shape)
print(" ")
print("The Shape of the matrix is: ",c.shape)
当我们运行代码时,得到以下输出:
The Shape of the scalar is: ()
The Shape of the vector is: (4,)
The Shape of the matrix is: (2, 2)
从结果中可以看出,标量没有形状值,而向量的形状值为 1 单位,矩阵的形状值为 2 单位。接下来,我们来计算每个张量中的元素数量:
#Generating number of elements in a tensor
print("The Size of the scalar is: ",tf.size(a))
print(" ")
print("The Size of the vector is: ",tf.size(b))
print(" ")
print("The Size of the matrix is: ",tf.size(c))
当我们运行代码时,得到以下输出:
The Size of the scalar is: tf.Tensor(1, shape=(), dtype=int32)
The Size of the vector is: tf.Tensor(4, shape=(), dtype=int32)
The Size of the matrix is: tf.Tensor(4, shape=(), dtype=int32)
我们可以看到标量只有 1 个计数,因为它是一个单一的单位;而我们的向量和矩阵中都有 4,因此每个都有 4 个数值。现在,我们可以自信地使用不同的方法来检查张量的属性。接下来,我们继续实现张量的基本操作。
基本的张量操作
我们现在知道 TensorFlow 是一个强大的深度学习工具。学习 TensorFlow 的一大障碍是理解什么是张量操作以及为什么需要它们。我们已经确定,张量是 TensorFlow 中的基本数据结构,可以用来存储、操作和分析机器学习模型中的数据。另一方面,张量操作是可以应用于张量的数学运算,用来操控、解码或分析数据。这些操作从简单的元素级操作到神经网络各层中执行的更复杂计算不等。让我们来看看一些张量操作。我们将从更改数据类型开始。然后,我们将学习索引和聚合张量。最后,我们将进行张量的元素级操作、张量重塑和矩阵乘法。
更改数据类型
假设我们有一个张量,我们想将其数据类型从 int32 更改为 float32,可能是为了支持某些需要小数的操作。幸运的是,在 TensorFlow 中,有办法解决这个问题。记住,我们已经确定整数的默认数据类型是 int32,而小数的默认数据类型是 float32。让我们返回 Google Colab,看看如何在 TensorFlow 中实现这一点:
a=tf.constant([1,2,3,4,5])
a
我们生成了一个整数向量,输出如下:
<tf.Tensor: shape=(5,), dtype=int32, numpy=array([1, 2, 3, 4, 5], dtype=int32)>
我们可以看到数据类型是 int32。让我们继续进行数据类型操作,将数据类型更改为 float32。我们使用 tf.cast() 函数,并将数据类型参数设置为 float32。让我们在笔记本中实现这一点:
a =tf.cast(a,dtype=tf.float32)
a
操作返回的数据类型为 float32。我们还可以看到 numpy 数组现在是一个小数数组,不再是整数数组:
<tf.Tensor: shape=(5,), dtype=float32,
numpy=array([1., 2., 3., 4., 5.], dtype=float32)>
你可以尝试 int16 或 float64,看看效果如何。当你完成后,我们继续进行 TensorFlow 中的索引操作。
索引
让我们从创建一个 2 x 2 的矩阵开始,接下来我们将用它来演示索引操作:
# Create a 2 x 2 matrix
a = tf.constant([[1, 2],[3, 4]], dtype=float32)
a
这是返回的输出:
<tf.Tensor: shape=(2, 2), dtype=float32,
numpy=array([[1., 2.], [3., 4.]], dtype=float32)>
如果我们想从矩阵中提取一些信息呢?假设我们想提取[1,2]。我们该如何做呢?别担心:我们可以应用索引来获取所需的信息。让我们在我们的笔记本中实现它:
# Indexing
a[0]
这里是返回的输出:
<tf.Tensor: shape=(2,), dtype=float32,
numpy=array([1., 2.], dtype=float32)>
如果我们想从矩阵中提取值2怎么办?让我们看看我们该如何做到:
# Indexing
a[0][1]
这里是返回的输出:
<tf.Tensor: shape=(), dtype=float32, numpy=2.0>
现在,我们已经成功地使用索引提取了我们想要的值。为了提取图 2.3中显示的矩阵中的所有值,我们可以使用索引来提取 2 x 2 矩阵中的所需元素。

图 2.3 – 矩阵索引
接下来,让我们看另一个索引的例子——这次,使用tf.slice()函数从张量中提取信息:
c = tf.constant([0, 1, 2, 3, 4, 5])
print(tf.slice(c,begin=[2],size=[4]))
我们生成一个张量c。然后,使用tf.slice函数从索引2开始切割向量,切割的大小或数量是4。当我们运行代码时,得到以下结果:
tf.Tensor([2 3 4 5], shape=(4,), dtype=int32)
我们可以看到结果包含来自索引2的值,并且我们从向量中提取了 4 个元素来生成切片。接下来,让我们看看如何扩展一个矩阵的维度。
重要提示
请记住,在 Python 中,我们的计数是从 0 开始的,而不是从 1 开始。
扩展矩阵
我们现在已经知道如何使用ndim检查矩阵的维度。那么,让我们看看如何扩展这个矩阵的维度。我们继续使用我们的a矩阵,它是一个 2 x 2 的矩阵,如图 2.4所示。

图 2.4 – 一个 2x2 矩阵
我们可以使用以下代码来扩展维度:
tf.expand_dims(a,axis=0)
我们使用expand_dims()函数,代码将a张量的维度沿着0轴进行扩展。当你想为张量添加一个新维度时,这非常有用——例如,当你想将一个二维张量转换为三维张量时(这种技术将在第七章,卷积神经网络的图像分类中应用,我们将处理一个有趣的经典图像数据集):
<tf.Tensor: shape=(1, 2, 2), dtype=float32,
numpy= array([[[1., 2.], [3., 4.]]], dtype=float32)>
如果你查看我们的输出张量的形状,现在可以看到它在0轴上有一个额外的维度1。接下来,让我们通过检查在不同轴上扩展张量的形状来更好地理解这一过程:
(tf.expand_dims(a,axis=0)).shape,
(tf.expand_dims(a,axis=1)).shape,
(tf.expand_dims(a,axis=-1)).shape
当我们运行代码查看维度如何在0、1和-1轴上扩展时,我们得到以下结果:
(TensorShape([1, 2, 2]), TensorShape([2, 1, 2]),
TensorShape([2, 2, 1]))
在第一行代码中,a的维度在0轴上扩展了 1。这意味着a的维度将变为 1 x 2 x 2,在张量的开头添加了一个额外的维度。第二行代码是在1轴上将a的维度扩展了 1。这意味着a的维度将变为 2 x 1 x 2;这里,我们在张量的第二个位置添加了一个额外的维度。第三行代码是在-1轴上将a的维度扩展了 1。这意味着a的维度将变为 2 x 2 x 1,从而在张量的末尾添加了一个额外的维度。我们已经解释了如何扩展矩阵的维度。接下来,让我们看看张量聚合。
张量聚合
让我们继续前行,了解如何聚合张量。我们首先通过导入random库生成一些随机数。然后,我们生成一个从 1 到 100 的范围,并在该范围内生成 50 个随机数。接下来,我们将使用这些随机数来生成一个张量:
import random
random.seed(22)
a = random.sample(range(1, 100), 50)
a = tf.constant(a)
当我们打印a时,得到以下数字:
<tf.Tensor: shape=(50,), dtype=int32, numpy=array(
[16, 83, 6, 74, 19, 80, 95, 68, 66, 86, 54, 12, 91,
13, 23, 9, 82, 84, 30, 62, 89, 33, 78, 2, 97, 21,
59, 34, 48, 38, 35, 18, 46, 60, 27, 26, 73, 76, 94,
72, 15, 40, 96, 44, 61, 8, 79, 93, 11, 14],
dtype=int32)>
假设我们想找出张量中的最小值。手动浏览所有数字,5 秒钟内告诉我最小值是什么,可能会有些困难。如果我们的值的范围达到千或百万,手动检查将占用我们所有的时间。幸运的是,在 TensorFlow 中,我们不仅可以一次找到最小值,还可以找到最大值、所有值的和、均值等更多信息。让我们一起在 Colab 笔记本中做这个:
print("The smallest number in our vector is : ",
tf.reduce_min(a).numpy())
print(" ")
print("The largest number in our vector is: ",
tf.reduce_max(a).numpy())
print(" ")
print("The sum of our vector is : ",
tf.reduce_sum(a).numpy())
print(" ")
print("The mean of our vector is: ",
tf.reduce_mean(a).numpy())
我们使用这些函数可以一键提取所需的细节,生成如下结果:
The smallest number in our vector is : 1
The largest number in our vector is: 99
The sum of our vector is : 2273
The mean of our vector is: 45
现在我们已经使用 TensorFlow 提取了一些重要的细节,知道了我们向量中的最小值是 1,最大值是 99,向量的和是 2273,均值是 45。不错吧?如果我们想找出向量中最小值和最大值所在的位置,该怎么办呢?
print("The position that holds the lowest value is : ",
tf.argmin(a).numpy())
print(" ")
print("The position that holds the highest value is: ",
tf.argmax(a).numpy())
我们使用tf.argmin和tf.argmax函数分别生成最小值的索引和最大值的索引。输出结果如下:
The position that holds the lowest value is : 14
The position that holds the highest value is: 44
从print语句的结果中,我们可以看出最小值位于索引14,最大值位于索引44。如果我们手动检查数组,就会发现这是正确的。此外,我们还可以将索引位置传入数组,获取最小值和最大值:
a[14].numpy(), a[44].numpy()
如果我们运行代码,得到如下结果:
(1,99)
还有一些其他的函数可以尝试。TensorFlow 文档给了我们很多可以尝试和探索的内容。接下来,让我们看看如何转置和重塑张量。
转置和重塑张量
让我们看看如何转置和重塑一个矩阵。首先,我们生成一个 3 x 4 的矩阵:
# Create a 3 x 4 matrix
a = tf.constant([[1,2,3,4], [5,6,7,8], [9,10,11,12]])
a
当我们运行代码时,得到如下结果:
<tf.Tensor: shape=(3, 4), dtype=int32,
numpy=array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]], dtype=int32)>
我们可以使用tf.reshape函数重新调整矩阵的形状。由于矩阵中有 12 个值,我们可以将其调整为 2 x 2 x 3 的形状。如果我们相乘这些值,我们将得到 12:
tf.reshape(a, shape=(2, 2, 3))
当我们运行代码时,我们得到以下输出:
<tf.Tensor: shape=(2, 2, 3), dtype=int32,
numpy=array([[[ 1, 2, 3], [ 4, 5, 6]],
[[ 7, 8, 9], [10, 11, 12]]], dtype=int32)>
我们还可以通过改变tf.reshape函数中的shape参数来重新调整矩阵的形状为 4 x 3 矩阵或 1 x 2 x 6 矩阵。你也可以尝试一些其他的形状调整可能性。接下来,让我们看一下如何使用tf.transpose()转置这个矩阵:
tf.transpose(a)
当我们运行代码时,我们得到以下输出:
<tf.Tensor: shape=(4, 3), dtype=int32,
numpy=array([[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11],
[ 4, 8, 12]], dtype=int32)>
从输出中,我们可以看到转置操作会翻转轴。现在,我们得到了一个 4 x 3 的矩阵,而不是最初的 3 x 4 矩阵。接下来,让我们看一下逐元素矩阵操作。
逐元素操作
让我们从在 Colab 中创建一个简单的向量开始:
a= tf.constant([1,2,3])
a
让我们显示我们的输出,以便看到当我们对向量执行逐元素操作时会发生什么:
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([1, 2, 3],
dtype=int32)>
这是我们的初始输出。现在,让我们尝试一些逐元素操作,看看接下来会发生什么:
#Addition operation
print((a+4).numpy())
print(" ")
#Subtraction operation
print((a-4).numpy())
print(" ")
#Multiplication Operation
print((a*4).numpy())
print(" ")
#Division Operation
print((a/4).numpy())
print(" ")
我们可以看到加法、减法、乘法和除法操作的结果。这些操作是在我们向量中的每个元素上执行的:
[5 6 7]
[-3 -2 -1]
[ 4 8 12]
[0.25 0.5 0.75]
接下来,让我们看一下矩阵乘法。
矩阵乘法
让我们来看一下矩阵乘法,并了解它在 TensorFlow 中的工作原理。我们返回到 Colab 中的笔记本,生成矩阵a,它是一个 3 x 2 的矩阵,以及矩阵b,它是一个 2 x 3 的矩阵。我们将使用这些矩阵进行矩阵操作:
# 3 X 2 MATRIX
a = tf.constant([[1, 2], [3, 4], [5, 6]])
#2 X 3 MATRIX
b = tf.constant([[7,8,9], [10,11,12]])
现在,让我们使用tf.matmul在我们的笔记本中乘以矩阵a和矩阵b,看看 TensorFlow 中的结果会是什么样子:
tf.matmul(a,b)
我们在 TensorFlow 中使用tf.matmul函数进行矩阵乘法。在这里,我们可以看到这个操作的输出:
<tf.Tensor: shape=(3, 3), dtype=int32,
numpy= array([[ 27, 30, 33],[ 61, 68, 75],
[ 95, 106, 117]], dtype=int32)>
很好!现在,如果我们想要将矩阵a与自身相乘,结果会是什么样子呢?如果我们尝试这个操作,我们会得到一个错误,因为矩阵的形状不符合矩阵乘法的规则。该规则要求矩阵a应该由i行 x m列组成,矩阵b应该由m行 x n列组成,并且矩阵中的m值在两个矩阵中必须相同。新的矩阵将具有i x n的形状,如图 2.5所示。

图 2.5 – 矩阵乘法
现在我们可以看到为什么不能将矩阵a与自身相乘,因为第一个矩阵的行数必须等于第二个矩阵的列数。然而,如果我们希望将a与自身相乘,我们可以通过转置或重新调整矩阵a的形状来满足矩阵乘法的要求。让我们来试一下:
tf.matmul(a,tf.transpose(a, perm=[1,0]))
当我们转置矩阵a时,我们根据perm参数交换矩阵的行和列,这里我们将其设置为[1,0]。当我们使用a和a的转置执行matmul函数时,我们得到一个符合矩阵乘法规则的新矩阵:
<tf.Tensor: shape=(3, 3), dtype=int32,
numpy= array([[ 5, 11, 17], [11, 25, 39],
[17, 39, 61]], dtype=int32)>
我们来尝试使用reshape进行矩阵乘法如何?试试这个,并将你的结果与我们的 Colab 笔记本中的结果进行对比。我们已经看了很多操作了。接下来我们来构建第一个模型吧。
TensorFlow 中的 Hello World
我们已经覆盖了很多 TensorFlow 的基本操作。现在,让我们在 TensorFlow 中构建第一个模型。假设你是一个研究小组的一员,正在研究学生在一个学期内的学习小时数与他们的期末成绩之间的相关性。当然,这只是一个理论场景,实际上,影响学生表现的因素有很多。然而,在这个例子中,我们只考虑一个决定成功的属性——学习小时数。在一个学期的学习之后,我们成功地整理了学生的学习小时数及其相应的成绩,如表 2.1所示。
| 学习小时数 | 20 | 23 | 25 | 28 | 30 | 37 | 40 | 43 | 46 |
|---|---|---|---|---|---|---|---|---|---|
| 测试成绩 | 45 | 51 | 55 | 61 | 65 | 79 | 85 | 91 | 97 |
表 2.1 – 学生表现表
现在,我们要构建一个模型,预测学生未来的表现,基于他们的学习小时数。准备好了吗?现在让我们一起做吧:
-
让我们通过打开名为
hello world的配套笔记本一起构建这个。首先,我们导入 TensorFlow。在第一章《机器学习简介》中,我们讨论了特征和标签。在这里,我们只有一个特征——学习小时数——而我们的标签或目标变量是测试成绩。通过强大的 Keras API,只需几行代码,我们就能构建并训练一个模型来进行预测。让我们开始吧:import tensorflow as tffrom tensorflow import kerasfrom tensorflow.keras import Sequentialfrom tensorflow.keras.layers import Denseprint(tf.__version__)
我们首先导入 TensorFlow 和 Keras API;不用担心所有的术语,我们将在第三章《TensorFlow 的线性回归》中详细展开。这里的目标是展示如何构建一个基础模型。别太担心技术细节;运行代码并看看它是如何工作的才是最重要的。导入所需的库之后,我们继续我们的传统,打印出我们的 TensorFlow 版本。代码运行顺利。
-
接下来,我们导入
numpy用于执行数学运算,导入matplotlib用于数据可视化:#import additional librariesimport numpy as npimport matplotlib.pyplot as plt
我们运行代码并且没有出现错误,所以可以继续进行。
-
我们设置了
X和y的值列表,分别代表学习小时数和测试成绩:# Hours of studyX = [20,23,25,28,30,37,40,43,46]# Test Scoresy = [45, 51, 55, 61, 65, 79, 85, 91, 97] -
为了更好地了解数据分布,我们使用
matplotlib来可视化数据:plt.plot(X, y)plt.title("Exam Performance graph")plt.xlabel('Hours of Study')plt.ylabel('Test Score')plt.show()
代码块绘制了X(学习小时数)与y(测试成绩)之间的图形,并显示了我们图表的标题(考试表现图)。我们使用show()函数来显示图表,如图 2.6所示。

图 2.6 – 学生表现图
从图表中我们可以看到数据表现出线性关系。考虑到我们可以逻辑地预期一个学习更努力的学生能获得更好的分数,这个假设并不算坏。
-
在不对这个理论进行辩论的前提下,让我们使用 Keras API 来构建一个简单的模型:
study_model = Sequential([Dense(units=1,input_shape=[1])])study_model.compile(optimizer='adam',loss='mean_squared_error')X= np.array(X, dtype=int)y= np.array(y, dtype=int)
我们构建了一个单层模型,命名为study_model,并将我们的X和y值列表转换为 NumPy 数组。
-
接下来,我们拟合模型并运行 2,500 个迭代周期:
#fitting the modelhistory= study_model.fit(X, y, epochs=2500)
当我们运行模型时,它应该在 5 分钟以内完成。我们可以看到,损失值最初快速下降,并在大约 2,000 个迭代周期后逐渐趋于平稳,如图 2.8所示:

图 2.7 – 模型损失图
就这样,我们训练了一个可以用来预测学生在学期结束时表现的模型。这是一个非常基础的任务,感觉就像用锤子打苍蝇。不过,让我们来试试看我们的模型:
#Let us predict how well a student will perform based on their study time
n=38 #Hours of study
result =study_model.predict([n])[0][0] #Result
rounded_number = round(81.0729751586914, 2)
如果我们运行这段代码,我们将生成一个学习了 38 小时的学生的结果。记住,我们的模型并没有在这个值上进行训练。那么,让我们看看我们的模型预测这个学生的分数:
print(f"If I study for {n} hours,
I will get { rounded_number} marks as my grade.")
If I study for 38 hours, I will get 81.07 marks as my grade.
我们的模型预测这个学生的分数是 81.07 分。结果不错,但我们怎么知道模型是对的还是错的呢?如果你看看图 2.6,你可能会猜测我们的预测结果应该接近这个分数,但你也许已经猜到我们是用2x + 5 = y来生成我们的y值。如果我们输入X=38,我们会得到2(38) + 5 = 81。我们的模型在预测正确分数时表现得相当好,误差仅为 0.07;然而,我们不得不花费很长时间来训练它,以便在这样一个简单的任务上取得这个结果。在接下来的章节中,我们将学习如何使用像归一化等技术,以及通过更大的数据集来训练一个更好的模型,我们会使用训练集、验证集并在测试集上进行预测。这里的目标是让你对接下来要学习的内容有一个初步的了解,因此,尝试一些不同的数字,看看模型的表现。不要超过 47,因为你将得到一个超过 100 的分数。
现在我们已经构建了第一个模型,让我们来看一下如何调试和解决错误信息。如果你决定从事这个领域的工作,这是你将会遇到的许多问题之一。
调试和解决错误信息
当你在本书中完成练习或走过代码,或者在任何其他资源中,甚至是你自己的个人项目中,你会迅速意识到代码出错的频率,掌握如何解决这些错误将帮助你快速通过学习过程或构建项目时的难关。首先,当你遇到错误时,重要的是要检查错误信息是什么。接下来是理解错误信息的含义。让我们来看一些学生在实现 TensorFlow 基本操作时遇到的错误。让我们运行以下代码来生成一个新的向量:
tf.variable([1,2,3,4])
运行此代码会抛出如下截图所示的错误:

图 2.8 – 错误示例
从错误信息中,我们可以看到在 TensorFlow 中没有名为 variable 的属性。这引起了我们对错误来源的注意,并且我们立刻发现我们写成了 variable,而应该使用文档中要求的大写字母 V 来写 Variable。但是,如果我们自己无法调试这个问题,我们可以点击 搜索 STACK OVERFLOW 按钮,因为这是一个很好的地方,可以找到我们在日常编码中可能遇到的问题的解决方案。很可能别人也遇到过相同的问题,解决方案可以在 Stack Overflow 上找到。
让我们点击链接,看看在 Stack Overflow 上能找到什么:

图 2.9 – Stack Overflow 上解决 AttributeError 的方案
万岁!在 Stack Overflow 上,我们看到了问题的解决方案,并附有文档链接,供您查看更多详情。请记住,最好先查看错误消息,看看自己是否能解决问题,然后再去 Stack Overflow。如果你能做到这一点,并投入时间阅读文档,你会越来越擅长调试问题,并减少错误,但你仍然会需要 Stack Overflow 或文档。调试是必经之路。在我们结束这一章之前,让我们快速总结一下我们学到的内容。
总结
在这一章中,我们高层次地讨论了 TensorFlow 生态系统。我们探讨了一些关键组件,这些组件使 TensorFlow 成为许多 ML 工程师、研究人员和爱好者构建深度学习应用和解决方案的首选平台。接下来,我们讨论了张量是什么,以及它们如何在我们的模型中发挥作用。之后,我们学习了几种创建张量的方法,探讨了张量的各种属性,并看到了如何使用 TensorFlow 实现一些基本的张量操作。我们构建了一个简单的模型并用它进行预测。最后,我们讨论了如何调试和解决 TensorFlow 以及更广泛的机器学习中的错误信息。
在下一章中,我们将通过实践来探讨回归模型的构建。我们将学习如何扩展我们的简单模型,以解决公司人力资源部门的回归问题。此外,您在调试过程中学到的内容在下一章也可能会派上用场——我们下一章见。
问题
让我们测试一下在本章中学到的内容:
-
什么是 TensorFlow?
-
什么是张量?
-
使用
tf.``V``ariable生成一个矩阵,数据类型为tf.float64,并为变量命名。 -
生成 15 个 1 到 20 之间的随机数,并提取出最小值、最大值、均值以及最小和最大值的索引。
-
生成一个 4 x 3 的矩阵,并将该矩阵与其转置相乘。
进一步阅读
若想了解更多内容,您可以查看以下资源:
-
Amr, T., 2020. 使用 scikit-learn 和科学 Python 工具包的动手机器学习. [S.l.]: Packt 出版社。
-
TensorFlow 指南:
www.TensorFlow.org/guide
第三章:使用 TensorFlow 进行线性回归
本章将介绍线性回归的概念以及如何使用 TensorFlow 实现它。我们将首先讨论什么是线性回归,它是如何工作的,它的基本假设是什么,以及可以用它解决的各种问题类型。接下来,我们将探讨回归建模中使用的各种评估指标,例如均方误差、平均绝对误差、均方根误差和决定系数 R²,并努力理解如何解释这些指标的结果。
为了进行实践操作,我们将通过构建一个实际案例来实现线性回归,在这个案例中,我们使用不同的属性预测员工的薪资。在这个过程中,我们将通过动手实践学习如何加载和预处理数据,涵盖处理缺失值、编码分类变量和标准化数据等重要概念。接着,我们将探索如何使用 TensorFlow 构建、编译和拟合线性回归模型,并审视像欠拟合和过拟合这样的概念及其对模型性能的影响。在本章结束前,您还将学习如何保存和加载训练好的模型,以便对未见过的数据进行预测。
本章将涵盖以下主题:
-
使用 TensorFlow 进行线性回归
-
评估回归模型
-
使用 TensorFlow 预测工资
-
模型的保存与加载
技术要求
我们将使用 python >= 3.8.0,并安装以下包,您可以通过 pip install 命令安装:
-
tensorflow>=2.7.0 -
tensorflow-datasets==4.4.0 -
pillow==8.4.0 -
pandas==1.3.4 -
numpy==1.21.4 -
scipy==1.7.3
使用 TensorFlow 进行线性回归
线性回归是一种监督式机器学习技术,用于建模预测输出变量(因变量)与一个或多个自变量之间的线性关系。当一个自变量可以有效地预测输出变量时,我们就得到了简单线性回归的案例,其可以用方程 y = wX + b 表示,其中 y 是目标变量,X 是输入变量,w 是特征的权重,b 是偏置。

图 3.1 – 展示简单线性回归的图表
在 图 3.1 中,直线被称为回归线(最佳拟合线),它是最优地建模 X 与 y 之间关系的直线。因此,我们可以利用它根据图表中某一点上自变量的当前值来确定因变量。线性回归的目标是找到 w 和 b 的最佳值,它们能够建模 X 和 y 之间的潜在关系。预测值越接近真实值,误差就越小。
反之,当我们有多个输入变量来预测输出值时,就出现了多重线性回归的情况,我们可以用方程 y = b0 + b1X1 + b2X2 + .... + bnXn 来表示,其中 y 是目标变量,X1、X2、... Xn 是输入变量,b0 是偏差项,b1、b2、... bn 是特征权重。
简单线性回归和多重线性回归有很多实际应用,因为它们易于实现且计算成本低。因此,它们可以很容易地应用于大规模数据集。然而,当我们试图建立 X 和 y 之间的非线性关系模型,或者输入数据中包含大量无关特征时,线性回归可能会失效。
线性回归广泛应用于解决各个领域的实际问题。例如,我们可以应用线性回归预测房屋价格,考虑因素如房屋大小、卧室数量、位置以及距离社会设施的远近。同样,在人力资源(HR)领域,我们可以利用线性回归预测新员工的薪资,考虑因素包括候选人的工作经验年限和教育水平。这些都是使用线性回归可以实现的一些例子。接下来,让我们看看如何评估线性回归模型。
评估回归模型
在我们来自 第二章 的 hello world 示例中,TensorFlow 简介,我们尝试预测学生在学期内学习了 38 小时后,考试成绩是多少。我们的模型得出了 81.07 分,而真实值是 81 分。因此,我们很接近,但并不完全正确。当我们减去模型预测值和真实值之间的差异时,我们得到了一个残差 0.07。残差值可以是正数也可以是负数,具体取决于我们的模型是高估还是低估了预测结果。当我们取残差的绝对值时,就消除了任何负号;因此,绝对误差总是一个正值,无论残差是正还是负。
绝对误差的公式如下:
绝对误差 = |Y pred − Y true|
其中 Y pred = 预测值,Y true = 真实值。
平均绝对误差(MAE)是模型的所有数据点的绝对误差的平均值。MAE 衡量的是残差的平均值,可以通过以下公式表示:
MAE = 1 _ n ∑ i=1 n |Y pred − Y true|
其中:
-
n = 所考虑的数据点数量
-
∑ = 所有观测值的绝对误差之和
-
|Y pred − Y true| = 绝对值
如果 MAE = 0,这意味着 Y_pred = Y_true。这意味着模型的预测完全准确;尽管这是一个理想的情况,但极不可能发生。相反,如果 MAE = ∞,则表示模型完全失败,因为它未能捕捉到输入与输出变量之间的任何关系。误差越大,MAE 的值也越大。在性能评估中,我们希望 MAE 的值较低,但由于 MAE 是一个相对度量,其值取决于所处理数据的规模,因此很难在不同数据集之间比较 MAE 的结果。
另一个重要的评估指标是均方误差(MSE)。与 MAE 不同,MSE 对残差进行了平方处理,从而去除了残差中的负值。MSE 用以下公式表示:
MSE = 1 _ N ∑ i=1 N ( Y pred − Y true) 2
与 MAE 类似,当没有残差时,我们有一个完美的模型。因此,MSE 值越低,模型性能越好。与 MAE 不同,MAE 中的大误差或小误差对结果有相同比例的影响,而 MSE 会对较大的误差进行惩罚,相较于小误差,它具有更高的单位阶数,因为我们在此情况下对残差进行了平方。
回归建模中另一个有用的指标是均方根误差(RMSE)。顾名思义,它是 MSE 的平方根,如下所示的公式所示:
MSE = 1 _ N ∑ i=1 N ( Y pred − Y true) 2
RMSE = √_ MSE = √ ________________ 1 _ N ∑ i=1 N ( Y pred − Y true) 2
最后,让我们看一下决定系数(R 平方)。R²衡量回归建模任务中,因变量在多大程度上可以由自变量解释。我们可以通过以下公式计算 R²:
R 2 = 1 − R res _ R tot
其中 Rres 是残差平方和,Rtot 是总平方和。R²值越接近 1,模型越准确;R²值越接近 0,模型越差。此外,R²值也可能是负数。这发生在模型未能遵循数据趋势的情况下——此时,Rres 大于 Rtot。负的 R²是模型性能差的标志,说明我们的模型需要显著改进。
我们已经看过了一些回归评估指标。好消息是,我们不需要手动计算它们;我们将利用 TensorFlow 中的tf.keras.metrics模块来帮助我们完成这些繁重的计算。我们已经从高层次快速浏览了理论。接下来,让我们通过一个多元线性回归的案例研究,帮助我们理解构建 TensorFlow 模型所需的各个部分,同时了解如何评估、保存、加载并使用训练好的模型来对新数据进行预测。让我们继续进行案例研究。
使用 TensorFlow 进行薪资预测
在这个案例研究中,你将假设自己是 Tensor Limited(一个快速增长的初创公司,拥有 200 多名员工)的新晋机器学习工程师。目前,公司希望招聘 7 名新员工,人力资源部门难以根据不同的资质、工作经验、申请职位以及每个潜在新员工的培训水平来确定理想的薪资。你的任务是与人力资源部门合作,为这些潜在新员工确定最优薪资。
幸运的是,我们已经在第一章《机器学习简介》中讲解了机器学习生命周期,第二章《TensorFlow 简介》里也构建了我们的 Hello World 案例研究,并且在本章中已经涵盖了回归建模所需的一些关键评估指标。因此,从理论上讲,你已经充分准备好执行这个任务。你已经与人力资源经理进行了富有成效的讨论,现在对任务和要求有了更清晰的理解。你将任务定义为一个监督学习任务(回归)。同时,人力资源部门允许你下载员工记录和相应的薪资数据来完成这个任务。现在你已经拥有数据集,让我们继续将数据加载到笔记本中。
加载数据
执行以下步骤以加载数据集:
-
打开名为
Linear_Regression_with_TensorFlow.ipynb的笔记本。我们将从导入所有必要的库开始:# import tensorflowimport tensorflow as tffrom tensorflow import kerasfrom tensorflow.keras import Sequentialfrom tensorflow.keras.layers import Denseprint(tf.__version__)
我们将运行这段代码块。如果一切顺利,我们将看到我们正在使用的 TensorFlow 版本:
2.12.0
-
接下来,我们将导入一些额外的库,这些库将帮助我们简化工作流程:
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport seaborn as snsfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import MinMaxScaler
我们将运行这个单元格,应该一切正常。NumPy 是一个 Python 中的科学计算库,用于对数组执行数学运算,而 pandas 是 Python 内置的用于数据分析和处理的库。Matplotlib 和 Seaborn 用于数据可视化,我们将使用 sklearn 进行数据预处理和数据拆分。在这个案例研究中,我们将应用这些库,你将了解它们的作用,并且能够在考试中以及之后的工作中应用它们。
-
现在,我们将继续加载数据集,这个数据集是我们从人力资源团队获取的,用于这个项目:
#Loading from the course GitHub accountdf=pd.read_csv('https://raw.githubusercontent.com/oluwole-packt/datasets/main/salary_dataset.csv')df.head()
我们将使用 pandas 生成一个 DataFrame,以表格格式保存记录,并将使用df.head()打印数据中的前五个条目:

图 3.2 – 显示数据集快照的 DataFrame
根据每一列捕捉的细节,我们现在对收集的数据有了初步了解。接下来,我们将继续探索数据,看看我们能学到什么,并且如何有效地开发一个解决方案来实现业务目标。让我们通过查看数据预处理部分继续。
数据预处理
为了能够对数据进行建模,我们需要确保数据是正确的格式(即数值型数据)。此外,我们还需要处理缺失值并去除无关的特征。在实际应用中,数据预处理通常需要很长时间。你会反复听到这一点,且这是真的。如果数据没有正确地整理,我们就无法进行建模。让我们深入了解一下,看看如何为当前任务做这些操作。从 DataFrame 中,我们可以立即看到一些无关的列,它们包含员工的个人身份信息。因此,我们将删除这些列,并告知人力资源部门:
#drop irrelevant columns
df =df.drop(columns =['Name', 'Phone_Number',
'Date_Of_Birth'])
df.head()
我们将使用 pandas 中的drop函数删除姓名、电话号码和出生日期列。接下来,我们将再次使用df.head()显示 DataFrame,展示数据的前五行:

图 3.3 – 删除列后的 DataFrame 前五行
我们已成功删除了无关的列,现在可以继续使用 pandas 中的isnull()函数检查数据集中的缺失值:
#check the data for any missing values
df.isnull().sum()
当我们运行这个代码块时,可以看到University和Salary列没有缺失值。然而,Role、Cert、Qualification和Experience列存在缺失值:
Experience 2
Qualification 1
University 0
Role 3
Cert 2
Salary 0
dtype: int64
处理缺失值有多种方法——从简单地要求 HR 修复遗漏,到使用均值、中位数或众数进行简单的填补或替换。在这个案例研究中,我们将删除含有缺失值的行,因为它是我们数据的一个小子集:
#drop the null values
df=df.dropna()
我们使用dropna函数删除数据集中的所有缺失值,然后将新的数据集保存为df。
注意
如果你想了解更多关于如何处理缺失值的内容,可以查看 Data Scholar 的这个播放列表:www.youtube.com/playlist?list=PLB9iiBW-oO9eMF45oEMB5pvC7fsqgQv7u。
现在,我们需要检查确保没有更多的缺失值,使用isnull()函数:
#check for null values
df.isnull().sum()
运行代码,看看是否还有缺失值:
Experience 0
Qualification 0
University 0
Role 0
Cert 0
Salary 0
dtype: int64
我们可以看到数据集中不再有缺失值。我们的模型要求输入数值型数据,才能对数据进行建模并预测目标变量,因此让我们来看看数据类型:
df.dtypes
当我们运行代码时,我们会得到一个输出,显示不同的列及其数据类型:
Experience float64
Qualification object
University object
Role object
Cert object
Salary int64
dtype: object
从输出中,我们可以看到experience和salary是数值型数据,因为它们分别是float和int类型,而Qualification、University、Role和Cert是分类数据。这意味着我们还不能训练模型;我们必须找到一种方法将分类数据转换为数值数据。幸运的是,这可以通过一个叫做独热编码(one-hot encoding)的过程来实现。我们可以使用 pandas 中的get_dummies函数来完成这一任务:
#Converting categorical variables to numeric values
df = pd.get_dummies(df, drop_first=True)
df.head()
当我们运行代码时,我们将得到一个类似于图 3.4中显示的 DataFrame。我们使用drop_first参数来删除第一类。

图 3.4 – 显示数值的 DataFrame
如果你不理解为什么我们删除了其中一个分类列,让我们来看一下Cert列,它由“是”或“否”值组成。如果我们进行了独热编码,但没有删除任何列,那么我们将有两个Cert列,如图 3.5所示。在Cert_No列中,如果员工有相关证书,该列的值为0,如果员工没有相关证书,该列的值为1。查看Cert_Yes列,我们可以看到,当员工有证书时,该列的值为1;否则,该列的值为0。

图 3.5 – 来自 Cert 列的虚拟变量
从图 3.5中,我们可以看到两列都可以用来显示员工是否有证书。使用从证书列生成的两个虚拟列会导致虚拟变量陷阱。当我们的独热编码列之间存在强相关性时,就会发生这种情况,其中一列可以有效地解释另一列。因此,我们说这两列是多重共线性的,而多重共线性可能导致模型过拟合。我们将在第五章中讨论更多关于过拟合的内容,标题为“使用神经网络进行图像分类”。
目前,我们知道过拟合是指我们的模型在训练数据上表现非常好,但在测试数据上表现不佳的情况。为了避免虚拟变量陷阱,我们将删除图 3.5中的一列。如果有三类,我们只需要两列就能捕捉到所有三类;如果有四类,我们只需要三列来捕捉所有四类,依此类推。因此,我们可以删除所有其他分类列的多余列。
现在,我们将使用corr()函数来获取我们精炼数据集的相关性:
df.corr()
我们可以看到,薪资与工作经验年数之间存在强相关性。同时,Role_Senior与Salary之间也有较强的相关性,如图 3.6所示。

图 3.6 – 我们数据的相关性值
我们已经完成了任务的预处理阶段,至少目前是这样。我们已移除所有不相关的列;还通过删除缺失值的行来去除缺失值,最后使用独热编码将分类值转换为数值。需要注意的是,我们在这里跳过了一些探索性数据分析(EDA)步骤,例如可视化数据;虽然这些步骤很重要,但我们在考试中的核心重点是使用 TensorFlow 构建模型。在我们的 Colab 笔记本中,你会找到一些额外的 EDA 步骤;虽然它们与考试内容无关,但它们将帮助你更好地理解数据,并帮助你检测异常值。
现在,让我们进入建模阶段。
模型构建
要构建模型,我们必须将数据分为特征(X)和目标(y)。为此,我们将运行以下代码块:
# We split the attributes and labels into X and y variables
X = df.drop("Salary", axis=1)
y = df["Salary"]
我们将使用drop()函数从X变量中删除Salary列,同时将y变量设为仅包含Salary列,因为这就是我们的目标变量。
在特征和目标变量定义清楚后,我们可以继续将数据分割为训练集和测试集。这一步很重要,因为它使我们的模型能够从数据中学习模式,从而有效地预测员工的薪资。为了实现这一点,我们使用训练集训练模型,然后在留出的测试集上评估模型的效果。我们在第一章《机器学习导论》中讨论过这一点,介绍了机器学习的生命周期。这是一个非常重要的过程,因为我们将使用测试集来评估模型的泛化能力,然后再将其部署到实际应用中。为了将数据分割为训练集和测试集,我们将使用sklearn库:
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.2, random_state=10)
使用sklearn库中的train_test_split函数,我们将数据分割为训练集和测试集,测试集大小为0.2。我们设置random_state = 10以确保可重复性,这样每次使用相同的random_state值时,即使多次运行代码,也能得到相同的分割。例如,在我们的代码中,我们将random_state设置为10,这意味着每次运行代码时,我们都会得到相同的分割。如果我们将这个值从10改为50,那么我们的训练集和测试集就会得到不同的随机分割。在将数据分割为训练集和测试集时,设置random_state参数非常有用,因为它可以确保我们有效地比较不同的模型,因为我们在所有实验模型中使用的是相同的训练集和测试集。
在机器学习中建模数据时,通常使用 80%的数据来训练模型,剩下的 20%用来测试模型的泛化能力。这就是为什么我们将test_size设置为0.2来处理我们的数据集。现在我们已经准备就绪,我们将认真开始建模过程。当涉及使用 TensorFlow 构建模型时,有三个关键步骤,如图 3**.7所示 – 构建模型、编译模型和将其适应我们的数据。

图 3.7 – 三步建模过程
让我们看看如何使用这种三步方法来构建我们的薪资预测模型。我们将从构建我们的模型开始:
#create a model using the Keras API
Model_1 = Sequential([Dense(units=1, activation='linear',
input_shape=[len(X_train.columns)])])
在图 3**.8中,我们可以看到我们模型的第一行代码。在这里,我们使用Sequential类作为数组生成单个层。Sequential类用于定义层。Dense函数用于生成一个全连接神经元层。在这种情况下,我们只有一个单元。在这里,我们使用线性激活函数作为激活函数。激活函数用于根据给定的输入或一组输入确定神经元的输出。在线性激活函数中,输出与输入直接相关。接下来,我们传入我们数据的输入形状,在这种情况下是 8,表示X_train中的数据特征(列)。

图 3.8 – 在 TensorFlow 中构建模型
在我们三步骤过程的第一步中,我们设计了模型结构。现在,我们将进入模型编译步骤。这一步同样重要,因为它决定了模型的学习方式。在这里,我们指定参数,如损失函数、优化器以及我们想要用来评估模型的指标。
优化器决定了我们的模型将如何根据损失函数和数据更新其内部参数。损失函数的作用是衡量模型在训练数据上的表现。然后,我们使用指标来监测模型在训练步骤和测试步骤上的性能。在这里,我们使用随机梯度下降 (SGD) 作为我们的优化器,MAE 作为我们的损失和评估指标:
#compile the model
Model_1.compile(loss=tf.keras.losses.mae,
optimizer=tf.keras.optimizers.SGD(), metrics = ['mae'])
现在,我们只需用训练数据和相应的标签来喂养我们的模型,我们的模型就能学会智能地预测目标数值,这在我们的情况下是预期的薪水。每次模型进行预测时,损失函数都会比较模型预测与实际值之间的差异。这些信息传递给优化器,优化器利用信息进行改进预测,直到模型能够制定正确的数学方程以准确预测我们员工的薪水。
现在,让我们适应我们的训练模型:
#Fit the model
model_1.fit(X_train, y_train, epochs =50)
我们使用model_1.fit来拟合我们的训练数据和标签,并将尝试的次数(代数)设置为50。只需几行代码,我们就生成了一个可以随着时间训练的迷你大脑,从而做出合理的预测。让我们运行代码,看看输出是什么样子的:
Epoch 46/50
6/6 [==============================] - 0s 9ms/step - loss: 97378.0391 - mae: 97378.0391
Epoch 47/50
6/6 [==============================] - 0s 4ms/step - loss: 97377.2500 - mae: 97377.2500
Epoch 48/50
6/6 [==============================] - 0s 4ms/step - loss: 97376.4609 - mae: 97376.4609
Epoch 49/50
6/6 [==============================] - 0s 3ms/step - loss: 97375.6484 - mae: 97375.6484
Epoch 50/50
6/6 [==============================] - 0s 3ms/step - loss: 97374.8516 - mae: 97374.8516
我们展示了最后五次尝试(46–50代)。误差逐渐下降;然而,在50代后,我们仍然得到了一个非常大的误差。或许我们可以像在第二章**《TensorFlow 简介》中那样训练更多代数的模型,为什么不呢?
#create a model using the Keras API
model_2 = Sequential([Dense(units=1, activation='linear',
input_shape=[len(X_train.columns)])])
#compile the model
model_2.compile(loss=tf.keras.losses.mae,
optimizer=tf.keras.optimizers.SGD(), metrics = ['mae'])
#Fit the model
history=model_2.fit(X_train, y_train, epochs =500)
现在,我们只需将代数改为500,使用我们的单层模型。激活函数、损失函数和优化器与我们的初始模型相同:
Epoch 496/500
6/6 [==============================] - 0s 3ms/step - loss: 97014.8516 - mae: 97014.8516
Epoch 497/500
6/6 [==============================] - 0s 2ms/step - loss: 97014.0391 - mae: 97014.0391
Epoch 498/500
6/6 [==============================] - 0s 3ms/step - loss: 97013.2500 - mae: 97013.2500
Epoch 499/500
6/6 [==============================] - 0s 3ms/step - loss: 97012.4453 - mae: 97012.4453
Epoch 500/500
6/6 [==============================] - 0s 3ms/step - loss: 97011.6484 - mae: 97011.6484
从我们输出的最后五行中,我们可以看到在500代后,损失仍然相当高。你可以尝试让模型训练更长时间,看看它会有什么表现。可视化模型的损失曲线也是一个好主意,这样你可以看到它的表现。较低的损失意味着模型表现更好。考虑到这一点,让我们来探讨model_2的损失曲线:
def visualize_model(history, ymin=None, ymax=None):
# Lets visualize our model
print(history.history.keys())
# Lets plot the loss
plt.plot(history.history['loss'])
plt.title('Model loss')
plt.ylabel('Loss')
plt.xlabel('Number of epochs')
plt.ylim([ymin,ymax]) # To zoom in on the y-axis
plt.legend(['loss plot'], loc='upper right')
plt.show()
我们将生成一个实用的绘图函数visualize_model,并在我们的实验中使用它来绘制模型在训练过程中随时间变化的损失。在这段代码中,我们生成一个图形来绘制存储在history对象中的损失值。history对象是我们三步建模过程中fit函数的输出,它包含了每个代结束时的损失和度量值。
为了绘制model_2,我们只需调用函数来可视化图表,并传入history_2:
visualize_model(history_2)
当我们运行代码时,得到的图形如图 3.9所示:

图 3.9 – 500 代的模型损失
从图 3.9中,我们可以看到损失在下降,但下降的速度太慢,因为它从大约97400下降到97000需要500代。在你有空时,可以尝试将模型训练 2000 代或更多。它将无法很好地泛化,因为这个模型太简单,无法处理我们数据的复杂性。在机器学习术语中,我们说这个模型是欠拟合的。
注:
使用 TensorFlow 构建模型主要有两种方式——顺序 API 和函数式 API。顺序 API 通过使用一系列层来构建模型,数据单向流动,从输入层到输出层,这是一种简单的方式。相反,TensorFlow 的函数式 API 允许我们构建更复杂的模型——这包括具有多个输入或输出的模型以及具有共享层的模型。在这里,我们使用的是顺序 API。有关使用顺序 API 构建模型的更多信息,请查看文档:www.tensorflow.org/guide/keras/sequential_model。
因此,让我们尝试构建一个更复杂的模型,看看能否比我们的初始模型更快地降低损失值:
#Set random set
tf.random.set_seed(10)
#create a model
model_3 =Sequential([
Dense(units=64, activation='relu',
input_shape=[len(X_train.columns)]),
Dense(units=1)
])
#compile the model
model_3.compile(loss="mae", optimizer="SGD",
metrics = ['mae'])
#Fit the model
history_3 =model_3.fit(X_train, y_train, epochs=500)
在这里,我们生成了一个新模型。我们在单一神经元层上方堆叠了一个 64 神经元的层。我们还为这一层使用了整流线性单元(ReLU)激活函数;它的作用是帮助我们的模型学习数据中的更复杂模式,并提高计算效率。第二层是输出层,由一个神经元组成,因为我们在做回归任务(预测连续值)。让我们运行 500 个 epoch,看看是否会有所不同:
Epoch 496/500
6/6 [==============================] - 0s 3ms/step - loss: 3651.6785 - mae: 3651.6785
Epoch 497/500
6/6 [==============================] - 0s 3ms/step - loss: 3647.4753 - mae: 3647.4753
Epoch 498/500
6/6 [==============================] - 0s 3ms/step - loss: 3722.4863 - mae: 3722.4863
Epoch 499/500
6/6 [==============================] - 0s 3ms/step - loss: 3570.9023 - mae: 3570.9023
Epoch 500/500
6/6 [==============================] - 0s 3ms/step - loss: 3686.0293 - mae: 3686.0293
从输出的最后五行来看,我们可以看到损失值显著下降,降到了大约3686。我们也来绘制一下损失曲线,以便更直观地理解。

图 3.10 – 500 个 epoch 后的模型损失
在图 3**.10中,我们可以看到,模型的损失已经降到了我们记录的最低损失值以下。这与我们之前的模型相比,取得了巨大的进步。然而,这并不是理想的结果,也看起来不像我们希望呈现给 HR 团队的结果。这是因为,使用这个模型,如果某个员工的年薪是$50,000,模型可能会预测出大约$46,300 的薪水,这会让员工不高兴;或者预测出$53,700 的薪水,这样 HR 团队也不会高兴。所以,我们需要想办法改进我们的结果。
让我们放大图表,更好地理解我们的模型发生了什么。
visualize_model(history_3, ymin=0, ymax=10000)
当我们运行代码时,它返回了图 3**.11所示的图表。

图 3.11 – 当我们放大图表时,500 个 epoch 后的模型损失
从图 3**.11中的图表,我们可以看到,损失值急剧下降并在第 100 个 epoch 左右稳定下来,之后似乎没有显著变化。因此,像我们之前的模型那样将模型训练得更长时间可能并不是最优的解决方案。那么,我们该如何改进我们的模型呢?
也许我们可以再添加一层?让我们试试,看看结果如何。正如我们最初指出的,我们的工作需要大量的实验;只有这样,我们才能学会如何做得更好、更快:
#Set random set
tf.random.set_seed(10)
#create a model
model_4 =Sequential([
Dense(units=64, activation='relu',
input_shape=[len(X_train.columns)]),
Dense(units=64, activation='relu'),
Dense(units=1)
])
#compile the model
model_4.compile(loss="mae", optimizer="SGD",
metrics = "mae")
#fit the model
history_4 =model_4.fit(X_train, y_train, epochs=500)
在这里,我们添加了一个64神经元的密集层。请注意,我们在这里也使用了 ReLU 作为激活函数:
Epoch 496/500
6/6 [==============================] - 0s 3ms/step - loss: 97384.4141 - mae: 97384.4141
Epoch 497/500
6/6 [==============================] - 0s 3ms/step - loss: 97384.3516 - mae: 97384.3516
Epoch 498/500
6/6 [==============================] - 0s 3ms/step - loss: 97384.3047 - mae: 97384.3047
Epoch 499/500
6/6 [==============================] - 0s 3ms/step - loss: 97384.2422 - mae: 97384.2422
Epoch 500/500
6/6 [==============================] - 0s 3ms/step - loss: 97384.1797 - mae: 97384.1797
我们只展示最后五个训练周期,可以看到损失大约为97384,这比model_3中的结果要差。那么,我们如何知道在建模过程中使用多少层呢?答案是通过实验。我们通过试验和错误,并结合对结果的理解,来决定是否需要添加更多层,就像最初模型欠拟合时我们所做的那样。如果模型变得过于复杂,能够很好地掌握训练数据,但在我们的测试(留出)数据上无法很好地泛化,那么在机器学习术语中,它就被称为过拟合。
现在,我们已经尝试了更小和更大的模型,但仍然无法说我们已经达到了合适的结果,而人力资源经理也来询问我们在预测建模任务上取得了什么进展。到目前为止,我们做了一些研究,正如所有的机器学习工程师所做的那样,发现了一个非常重要的步骤,我们可以尝试。是什么步骤呢?让我们看看。
标准化
标准化是一种应用于输入特征的技术,确保它们具有一致的尺度,通常是在 0 和 1 之间。这个过程有助于我们的模型更快且更准确地收敛。值得注意的是,我们应该在完成其他数据预处理步骤后,如处理缺失值,才应用标准化。
了解改进模型输出的效果往往也强烈依赖于数据准备过程是一个很好的做法。因此,让我们在这里应用这一点。我们将暂时跳出模型构建的步骤,来看看在将所有列转换为数值后我们的特征数据:
X.describe()
我们将使用 describe 函数来获取数据的关键信息。该信息显示,大多数列的最小值为 0,最大值为 1,但Experience 列的尺度不同,正如图 3.12所示:

图 3.12 – 数据集的统计摘要(标准化前)
你可能会问,为什么这很重要?当我们数据的尺度不同,模型会不合理地给予数值较大的列更多的权重,这可能影响模型正确预测目标的能力。为了解决这个问题,我们将使用标准化方法,将数据缩放到 0 到 1 之间,从而使所有特征具有相同的尺度,给每个特征在模型学习它们与目标(y)之间关系时相等的机会。
为了标准化我们的数据,我们将使用以下公式进行缩放:
X norm = X − X min _ X max − X min
其中 X 是我们的数据,X min 是 X 的最小值,X max 是 X 的最大值。在我们的案例中,Experience 列的 X 最小值为 1,最大值为 7。好消息是,我们可以使用 sklearn 库中的 MinMaxScaler 函数轻松实现这一步骤。接下来,让我们看看如何缩放我们的数据:
# create a scaler object
scaler = MinMaxScaler()
# fit and transform the data
X_norm = pd.DataFrame(scaler.fit_transform(X),
columns=X.columns)
X_norm.describe()
让我们使用describe()函数再次查看关键统计数据,如图 3.13所示。

图 3.13 – 数据集的统计摘要(标准化后)
现在,我们所有的数据都具有相同的尺度。因此,我们通过几行代码成功地对数据进行了标准化。
现在,我们将数据分为训练集和测试集,但这次我们在代码中使用了标准化后的X(X_norm):
# Create training and test sets with the normalized data (X_norm)
X_train, X_test, y_train, y_test = train_test_split(X_norm,
y, test_size=0.2, random_state=10)
现在,我们使用从初始实验中获得的最佳模型(model_3)。让我们看看标准化后模型的表现:
#create a model
model_5 =Sequential([
Dense(units=64, activation='relu',
input_shape=[len(X_train.columns)]),
Dense(units=64, activation ="relu"),
Dense(units=1)
])
#compile the model
model_5.compile(loss="mae",
optimizer=tf.keras.optimizers.SGD(), metrics = ['mae'])
history_5 =model_5.fit(X_train, y_train, epochs=1000)
输出如下:
Epoch 996/1000
6/6 [==============================] - 0s 4ms/step - loss: 1459.2953 - mae: 1459.2953
Epoch 997/1000
6/6 [==============================] - 0s 4ms/step - loss: 1437.8248 - mae: 1437.8248
Epoch 998/1000
6/6 [==============================] - 0s 3ms/step - loss: 1469.3732 - mae: 1469.3732
Epoch 999/1000
6/6 [==============================] - 0s 4ms/step - loss: 1433.6071 - mae: 1433.6071
Epoch 1000/1000
6/6 [==============================] - 0s 3ms/step - loss: 1432.2891 - mae: 1432.2891
从结果中,我们可以看到与未应用标准化时的结果相比,MAE 减少了一半以上。

图 3.14 – model_5 的放大损失曲线
此外,如果你查看model_5在图 3.14中的损失曲线,你会发现损失在大约第 100 个 epoch 之后没有显著下降。与其猜测训练模型的理想 epoch 数,不如设置一个规则,当模型无法改进其性能时就停止训练。而且我们可以看到,model_5没有给出我们想要的结果;也许现在是尝试更大模型的好时机,我们可以训练更长时间,并设置一个规则,当模型无法改进其在训练数据上的性能时就停止训练:
#create a model
model_6 =Sequential([
Dense(units=64, activation='relu',
input_shape=[len(X_train.columns)]),
Dense(units=64, activation ="relu"), Dense(units=1)
])
#compile the model
model_6.compile(loss="mae",
optimizer=tf.keras.optimizers.SGD(), metrics = ['mae'])
#fit the model
early_stop=keras.callbacks.EarlyStopping(monitor='loss',
patience=10)
history_6 =model_6.fit(
X_train, y_train, epochs=1000, callbacks=[early_stop])
在这里,我们使用了一个三层模型,前两层由 64 个神经元组成,输出层只有一个神经元。为了设置停止训练的规则,我们使用了early stopping;这个附加参数是在我们将模型拟合到数据时应用的,用来在模型损失在 10 个 epoch 后没有改善时停止训练。这是通过指定监控损失的度量并将patience设置为10来实现的。Early stopping 也是一种防止过拟合的好技术,因为它在模型无法改进时停止训练;我们将在第六章《改进模型》中进一步讨论这个问题。现在让我们看看结果:
Epoch 25/1000
6/6 [==============================] - 0s 3ms/step - loss: 84910.6953 - mae: 84910.6953
Epoch 26/1000
6/6 [==============================] - 0s 3ms/step - loss: 81037.8516 - mae: 81037.8516
Epoch 27/1000
6/6 [==============================] - 0s 3ms/step - loss: 72761.0078 - mae: 72761.0078
Epoch 28/1000
6/6 [==============================] - 0s 3ms/step - loss: 81160.6562 - mae: 81160.6562
Epoch 29/1000
6/6 [==============================] - 0s 3ms/step - loss: 70687.3125 - mae: 70687.3125
尽管我们将训练设置为1000个 epoch,但由于Earlystopping回调在第 29 个 epoch 时停止了训练,因为它没有观察到损失值的显著下降。虽然这里的结果不是很好,但我们使用了EarlyStopping来节省了大量的计算资源和时间。也许现在是尝试不同优化器的好时机。对于下一个实验,我们使用 Adam 优化器。Adam 是深度学习中另一种流行的优化器,因为它能够自适应地控制模型中每个参数的学习率,从而加速模型的收敛:
#create a model
model_7 =Sequential([
Dense(units=64, activation='relu',
input_shape=[len(X_train.columns)]),
Dense(units=64, activation ="relu"),
Dense(units=1)
])
#compile the model
model_7.compile(loss="mae", optimizer="Adam",
metrics ="mae")
#fit the model
early_stop=keras.callbacks.EarlyStopping(monitor='loss',
patience=10)
history_7 =model_7.fit(
X_train, y_train, epochs=1000, callbacks=[early_stop])
注意,我们在编译步骤中只更改了优化器为 Adam。让我们看看这个优化器变化的结果:
Epoch 897/1000
6/6 [==============================] - 0s 4ms/step - loss: 30.4748 - mae: 30.4748
Epoch 898/1000
6/6 [==============================] - 0s 4ms/step - loss: 19.4643 - mae: 19.4643
Epoch 899/1000
6/6 [==============================] - 0s 3ms/step - loss: 17.0965 - mae: 17.0965
Epoch 900/1000
6/6 [==============================] - 0s 3ms/step - loss: 18.5009 - mae: 18.5009
Epoch 901/1000
6/6 [==============================] - 0s 3ms/step - loss: 15.5516 - mae: 15.5516
仅通过更换优化器,我们记录到了损失值的惊人下降。同时,注意我们并未使用全部1000个 epoch,因为训练在901个 epoch 时就结束了。让我们再添加一层,或许会看到更好的表现:
#create a model
model_8 =Sequential([
Dense(units=64, activation='relu',
input_shape=[len(X_train.columns)]),
Dense(units=64, activation ="relu"),
Dense(units=64, activation ="relu"),
Dense(units=1)
])
#compile the model
model_8.compile(loss="mae", optimizer="Adam",
metrics ="mae")
#fit the model
early_stop=keras.callbacks.EarlyStopping(monitor='loss',
patience=10)
history_8 =model_8.fit(
X_train, y_train, epochs=1000, callbacks=[early_stop])
在这里,我们添加了一个额外的层,包含64个神经元,并使用 ReLU 作为激活函数。其他部分保持不变:
Epoch 266/1000
6/6 [==============================] - 0s 4ms/step - loss: 73.3237 - mae: 73.3237
Epoch 267/1000
6/6 [==============================] - 0s 4ms/step - loss: 113.9100 - mae: 113.9100
Epoch 268/1000
6/6 [==============================] - 0s 4ms/step - loss: 257.4851 - mae: 257.4851
Epoch 269/1000
6/6 [==============================] - 0s 4ms/step - loss: 149.9819 - mae: 149.9819
Epoch 270/1000
6/6 [==============================] - 0s 4ms/step - loss: 179.7796 - mae: 179.7796
训练在 270 个 epoch 后停止;尽管我们的模型更复杂,但在训练中它并未表现得比model_7更好。在实验过程中,我们尝试了不同的思路,现在让我们在测试集上尝试所有八个模型并进行评估。
模型评估
为了评估我们的模型,我们将编写一个函数,将evaluate指标应用于所有八个模型:
def eval_testing(model):
return model.evaluate(X_test, y_test)
models = [model_1, model_2, model_3, model_4, model_5,
model_6, model_7, model_8]
for x in models:
eval_testing(x)
我们将生成一个eval_testing(model)函数,该函数以模型作为参数,并使用evaluate方法来评估模型在我们的测试数据集上的表现。遍历模型列表时,代码将返回所有八个模型在测试数据上的损失和 MAE 值:
2/2 [==============================] - 0s 8ms/step - loss: 100682.4609 - mae: 100682.4609
2/2 [==============================] - 0s 8ms/step - loss: 100567.9453 - mae: 100567.9453
2/2 [==============================] - 0s 10ms/step - loss: 17986.0801 - mae: 17986.0801
2/2 [==============================] - 0s 9ms/step - loss: 100664.0781 - mae: 100664.0781
2/2 [==============================] - 0s 6ms/step - loss: 1971.4187 - mae: 1971.4187
2/2 [==============================] - 0s 11ms/step - loss: 5831.1250 - mae: 5831.1250
2/2 [==============================] - 0s 7ms/step - loss: 5.0099 - mae: 5.0099
2/2 [==============================] - 0s 26ms/step - loss: 70.2970 - mae: 70.2970
在评估完模型后,我们可以看到model_7的损失最小。让我们看看它在测试集上的表现,使用它来进行预测。
进行预测
现在,我们完成了实验并评估了模型,让我们使用model_7来预测我们的测试集薪资,并查看它与真实值的对比。为此,我们将使用predict()函数:
#Let's make predictions on our test data
y_preds=model_7.predict(X_test).flatten()
y_preds
运行完这段代码后,我们会得到如下所示的数组输出:
2/2 [==============================] - 0s 9ms/step
array([ 64498.64 , 131504.89 , 116491.73 , 72500.13 , 102983.836,
60504.645, 84503.36 , 119501.664, 112497.734, 63501.168,
77994.87 , 84497.16 , 112497.734, 90980.625, 87499.88 ,
100502.234, 135498.88 , 112491.53 , 119501.664, 131504.89 ,
108990.31 , 117506.63 , 80503.16 , 123495.66 , 112497.734,
117506.63 , 111994.03 , 78985.125, 135498.88 , 129502.125,
117506.64 , 119501.664, 100502.234, 113506.43 , 101987.38 ,
113506.43 , 93990.555, 65496.2 , 61494.906, 107506.17 ,
105993.77 , 106502.5 , 72493.94 , 135498.88 , 67501.37 ,
107506.17 , 117506.63 , 70505.1 , 57500.906], dtype=float32)
为了清晰起见,我们将构建一个包含模型预测值和真实值的 DataFrame。当你看到我们的模型变得如此优秀时,这应该会非常有趣,甚至有些神奇:
#Let's make a DataFrame to compare our prediction with the ground truth
df_predictions = pd.DataFrame({'Ground_Truth': y_test,
'Model_prediction': y_preds}, columns=['Ground_Truth',
'Model_prediction']) df_predictions[
'Model_prediction']= df_predictions[
'Model_prediction'].astype(int)
在这里,我们生成了两列,并将模型的预测值从float转换为int,以便与真实值对齐。准备好查看结果了吗?
我们将使用head函数打印出测试集的前 10 个值:
#Let's look at the top 10 data points in the test set
df_predictions.sample(10)
然后我们会看到如下所示的结果,如图 3.15所示:

图 3.15 – 一个显示实际值、模型预测值和剩余值的 DataFrame
我们的模型取得了令人印象深刻的成果;它与测试数据中的初始薪资非常接近。现在,你可以向人力资源经理展示你的惊人成果了。我们必须保存模型,以便随时加载并进行预测。接下来,我们将学习如何做到这一点。
保存和加载模型
TensorFlow 的魅力在于我们能够轻松完成复杂的任务。要保存模型,我们只需要一行代码:
#Saving the model in one line of code
Model7.save('salarypredictor.h5')
#Alternate method is
#model7.save('salarypredictor')
你可以将它保存为your_model.h5或your_model,两者都可以。TensorFlow 推荐使用SavedModel格式,因为它是语言无关的,这使得它可以轻松部署到各种平台上。在这种格式下,我们可以保存模型及其各个组件,如权重和变量。相反,HDF5 格式将完整的模型结构、权重和训练配置保存为一个文件。这种方法给我们提供了更大的灵活性来共享和分发模型;然而,在部署方面,它不是首选方法。当我们运行代码时,我们可以在 Colab 笔记本的左侧面板中看到保存的模型,如图 3.16所示。

图 3.16 – 我们保存的模型快照
既然我们已经保存了模型,最好通过重新加载它并进行测试来验证它。我们来做一下这个操作。而且,加载模型只需要一行代码:
#loading the model
saved_model =tf.keras.models.load_model("/content/salarypredictor.h5")
让我们试试我们的saved_model,看看它是否能像model_7一样有效。我们将重新生成y_pred并生成一个数据框,使用y_test和y_pred,就像我们之前做的那样,首先检查我们测试数据中的 10 个随机样本:

图 3.17 – 显示实际值和保存的模型预测值的数据框
从图 3.17的结果中,我们可以看到我们保存的模型表现得非常好。现在,你可以将结果交给人力资源经理,他们应该会对你的结果感到兴奋。假设人力资源经理希望你使用你的模型来预测新员工的薪资,我们接下来就来做这个:
#Putting everything into a function for our big task
def salary_predictor(df):
df_hires= df.drop(columns=['Name', 'Phone_Number',
'Date_Of_Birth' ])
df_hires = pd.get_dummies(df_hires, drop_first=True)
X_norm = pd.DataFrame(scaler.fit_transform(df_hires),
columns=df.columns)
y_preds=saved_model.predict(X_norm).flatten()
df_predictions = pd.DataFrame({ 'Model_prediction':
y_preds}, columns=[ 'Model_prediction'])
df_predictions['Model_prediction']= df_predictions[
'Model_prediction'].astype(int)
df['Salary']=df_predictions['Model_prediction']
return df
我们使用保存的模型生成一个函数。我们只需将到目前为止所覆盖的所有步骤封装到这个函数中,然后返回一个数据框。现在,让我们读取新员工的数据:
#Load the data
df_new=pd.read_csv('https://raw.githubusercontent.com/oluwole-packt/datasets/main/new_hires.csv')
df_new
当我们运行代码块时,我们可以看到它们的数据框,如图 3.18所示。

图 3.18 – 显示新员工的数据框
现在,我们将数据传入我们生成的函数,获取新员工的预测薪资:
#Lets see how much
salary_predictor(df_new)
我们将df_new传入薪资预测函数,然后得到一个新的数据框,如图 3.19所示:

图 3.19 – 显示新员工及其预测薪资的数据框
最终,我们达成了目标。人力资源部门很高兴,新员工也很高兴,公司里的每个人都觉得你是个魔术师。也许加薪提案会摆上桌面,但就在你陶醉于第一次成功的喜悦时,经理带着另一个任务回来。这次是一个分类任务,我们将在下一章讨论这个任务。现在,干得好!
总结
在本章中,我们深入研究了监督学习,重点是回归建模。在这里,我们讨论了简单线性回归和多重线性回归的区别,并了解了一些重要的回归建模评估指标。接着,我们在案例研究中动手帮助公司构建了一个有效的回归模型,用于预测新员工的薪资。我们进行了数据预处理,并认识到在建模过程中标准化的重要性。
在案例研究的最后,我们成功构建了一个薪资预测模型,并在测试集上评估了该模型,掌握了如何保存和加载模型以供后续使用。现在,您可以自信地使用 TensorFlow 构建回归模型。
在下一章,我们将探讨分类建模。
问题
让我们测试一下本章所学的内容。
-
什么是线性回归?
-
简单线性回归和多重线性回归有什么区别?
-
哪个评估指标会惩罚回归模型中的大误差?
-
使用薪资数据集预测薪资。
进一步阅读
若要了解更多,您可以查看以下资源:
-
Amr, T., 2020. 使用 scikit-learn 和科学 Python 工具包进行实践机器学习。[S.l.]:Packt 出版社。
-
Raschka, S. 和 Mirjalili, V., 2019. Python 机器学习。 第 3 版。Packt 出版社。
-
TensorFlow 文档:
www.TensorFlow.org/guide。
第四章:使用 TensorFlow 进行分类
在上一章中,我们讲解了 TensorFlow 中的线性回归,其中我们讨论了简单线性回归和多元线性回归;同时我们还探索了评估回归模型的各种指标。我们以一个实际用例结束了这一章,其中我们构建了一个薪资预测模型,并使用该模型根据一组特征预测新员工的薪资。在本章中,我们将继续使用 TensorFlow 进行建模——这一次,我们将探索使用 TensorFlow 进行分类问题的处理。
我们将从分类建模的概念开始,然后探讨分类建模的各种评估指标,以及如何将它们应用于不同的用例。我们将讨论二分类、多分类和多标签分类建模。最后,我们将通过一个案例研究,实践我们所学的知识,构建一个二分类模型来预测学生是否会辍学。
到本章结束时,你应该清楚地理解机器学习中的分类建模是什么,并且能够区分二分类、多分类和多标签分类问题。你将熟悉如何构建、编译、训练、预测和评估分类模型。
在本章中,我们将讨论以下主题:
-
使用 TensorFlow 进行分类
-
学生辍学预测
技术要求
在本章中,我们将使用 Google Colab 来运行编码练习,你需要安装 Python >= 3.8.0,并安装以下包,使用 pip install 命令进行安装:
-
tensorflow >=2.7.0 -
tensorflow-datasets ==4.4.0 -
Pillow ==8.4.0 -
pandas ==1.3.4 -
numpy ==1.21.4 -
scipy ==1.7.3
本书的代码包可通过以下 GitHub 链接获取:github.com/PacktPublishing/TensorFlow-Developer-Certificate。所有练习的解决方案也可以在该链接中找到。
使用 TensorFlow 进行分类
在第一章,机器学习简介中,我们讨论了监督学习,并简要介绍了分类建模。分类建模涉及预测目标变量中的类别。当我们试图预测的类别是二分类时(例如,预测宠物是狗还是猫,邮件是否为垃圾邮件,或病人是否患有癌症),这种分类场景被称为二分类。
另一方面,我们可能面临这样的问题,我们想要构建一个机器学习模型来预测不同品种的狗。在这种情况下,我们有多个类别,因此这种类型的分类被称为多类分类。就像二元分类问题一样,在多类分类中,我们的目标变量只能属于多个类别中的一个 – 我们的模型将选择斗牛犬、德国牧羊犬或斗牛犬。在这里,类别是互斥的。
想象一下,你正在构建一个电影分类器,你想要分类一部如复仇者联盟:终局之战这样的大片。这部电影属于动作、冒险、超级英雄、史诗、奇幻和科幻类别。从电影的标签中,我们可以看到我们的目标变量属于多个流派;因此,这种类型的分类被称为多标签分类,其中输出类别有多个目标标签。
不像多类分类,每个例子只能属于一个类别,而在多标签分类中,每个例子可以属于多个标签。
不像二元和多类分类,每个例子只能属于一个类别,在多标签分类中,每个例子可以属于多个类别,就像复仇者联盟电影一样。现在我们已经看过三种主要的分类问题类型,下一个问题是,我们如何评估分类模型?我们需要关注哪些关键指标?现在让我们来看一下并理解它们的含义以及如何最好地应用它们到各种分类问题中。
评估分类模型
与回归问题不同,在回归模型中,我们的目标变量是数值型的,而在分类建模中,我们已经确定输出是类别。因此,我们不能使用用于评估回归模型的相同指标在第三章,TensorFlow 中的线性回归中使用,因为我们的输出不是连续的数值,而是类别。对于分类问题,比如我们建立一个垃圾邮件过滤系统来分类客户的电子邮件。客户有 250 封非垃圾邮件和另外 250 封垃圾邮件。使用我们的垃圾邮件过滤模型,我们能够正确标记 230 封垃圾邮件并且正确识别 220 封非垃圾邮件。
当我们的垃圾邮件过滤器正确将一个垃圾邮件标记为垃圾邮件(这正是我们想要的),我们称之为真正例;当模型错误地将一个垃圾邮件误分类为非垃圾邮件时,这被称为假负例。在模型正确将非垃圾邮件识别为非垃圾邮件的情况下,这称为真负例;然而,我们偶尔会在我们的垃圾邮件文件夹中找到重要的邮件,这些邮件在被错误地归类为垃圾邮件时并不是垃圾。这种情况被称为假正例。现在我们可以使用这些细节来评估我们的垃圾邮件过滤模型的性能。
让我们列出我们现在知道的重要细节:
-
垃圾邮件总数:250 个样本
-
正确预测的垃圾邮件(真正例):230 个样本
-
错误预测的垃圾邮件(假阴性或类型 2 错误):20 个样本
-
非垃圾邮件总数:250 个样本
-
正确预测的非垃圾邮件(真负例):220 个样本
-
错误预测的垃圾邮件(假阳性或类型 1 错误):30 个样本
现在我们已经收集了关键细节,接下来让我们使用这些信息来学习如何评估分类模型。为此,我们接下来将讨论混淆矩阵。
混淆矩阵
混淆矩阵是一个错误矩阵,它以表格形式展示分类模型的表现,包含真实值和预测值,如图 4.1所示。

图 4.1 – 混淆矩阵
使用混淆矩阵,我们可以计算各种分类评估指标,如准确率、精确度、召回率和 F1 分数。在混淆矩阵中,我们可以看到预测类别位于顶部,第一列显示预测为垃圾邮件的邮件,第二列显示预测为非垃圾邮件的邮件,而行则展示真实值。在这里,我们可以看到第一行显示的是真实的垃圾邮件类别和真实的非垃圾邮件类别。当我们将所有这些信息整合在一起时,我们可以以表格的形式看到真实值和错误预测,这为我们提供了模型及其在两类之间的表现的快速视图。让我们使用这些细节来计算模型的关键性能指标。
准确率是非常直观的,它是正确预测标签的总和与可用数据总数之比。我们可以用以下方程式表示:
准确率 = TP + TN _______________ (TP + FP + TN + FN)
让我们添加我们的数值,看看我们的准确率是多少:
准确率 = 230 + 220 ________________ (230 + 30 + 220 + 20) = 0.90
我们得到了 90%的准确率。这可能令人兴奋,但让我们用更现实的数据来看待它。当涉及到垃圾邮件时,我们的邮箱中可能会收到更多合法邮件,而不是垃圾邮件。
假设我们有另一个客户 B,拥有 500 封邮件,具体细节如下:
-
垃圾邮件总数:40 个样本
-
正确预测的垃圾邮件(真正例):20 个样本
-
错误预测的垃圾邮件(假阴性或类型 2 错误):20 个样本
-
非垃圾邮件总数:460 个样本
-
正确预测的非垃圾邮件(真负例):430 个样本
-
错误预测的垃圾邮件(假阳性或类型 1 错误):30 个样本
如果我们计算客户 B 的准确率,结果为:
准确率 = 20 + 430 ______________ 20 + 30 + 430 + 20 = 0.90
再次计算,我们得到了 90%的准确率,但模型只能将 50%的垃圾邮件正确预测为垃圾邮件。这表明,准确率可能并不是最佳的衡量标准,尤其是在处理像电子邮件分类、欺诈检测或疾病检测这类不平衡数据的使用案例时。
为了更好地了解我们的模型表现,我们现在将关注精确度和召回率。回顾图 4.2,实际正类与正类的比例被称为敏感性或召回率,在机器学习术语中也称为真实正例率,它通过以下公式表示:
召回率 = TP _ (TP + FN)
精确度是模型预测的正类与实际正类的比例,我们也可以用一个公式表示它:
精确度 = TP _ (TP + FP)
使用客户 B,我们来计算模型的精确度和召回率:
案例研究 2 的精确度 = 20 _ (20 + 30) = 0.4
案例研究 2 的召回率 = 20 _ (20 + 20) = 0.5
现在,我们可以看到尽管模型在整体数据上的准确率较高,但其表现却不尽如人意。另一个我们会遇到的重要指标是 F1 分数。F1 分数结合了召回率和精确度,我们通过计算精确度和召回率的调和均值来得到它:
F1 分数 = 2 * 精确度 * 召回率 _____________ (精确度 + 召回率)
我们来计算第二个案例研究的 F1 分数:
F1 分数 = 2 * 0.4 * 0.5 _ (0.4 + 0.5) = 0.44
通过使用我们的垃圾邮件过滤模型评估客户 B 的电子邮件,我们现在知道需要构建一个更有效的模型,一个对目标类别具有更高精确度和召回率的模型。然而,达到高精确度和召回率并非总是可能的。在这种情况下,我们面临一个权衡,这就是所谓的精确度/召回率权衡。在检测垃圾邮件的情况下,我们知道如果一些垃圾邮件进入收件箱,客户不太可能换用其他服务提供商;然而,如果他们未能在收件箱中找到重要邮件,他们会感到不满。在这种情况下,我们将致力于实现更高的召回率。相反,假设我们构建了一个早期癌症检测系统,重点将放在提高精确度,以减少假阳性。需要注意的是,精确度和召回率并非互斥的,在许多情况下,我们可以通过调优模型来同时实现高精确度和高召回率。
我们现在已经涵盖了一些重要的分类指标。接下来,我们来看一个案例研究(学生辍学预测),在其中我们将使用 TensorFlow 和 scikit-learn 的不同模块来构建和评估分类模型。让我们开始吧。
学生辍学预测
在第三章《使用 TensorFlow 进行线性回归》中,你开始使用 TensorFlow 构建薪资预测模型的旅程。你的老板印象深刻,现在你已经完全融入数据团队,经理希望你为一个新客户工作。你的任务是帮助他们构建一个模型,预测学生是否会辍学,这将帮助他们支持这些学生,从而防止他们辍学。经理已经授权你,并且这个任务现在属于你。为了完成这个任务,客户向你提供了历史数据。就像在第三章《使用 TensorFlow 进行线性回归》中一样,你与客户进行了富有成效的交流,并将任务识别为一个二分类问题。让我们打开 GitHub 仓库中标记为Classification with TensorFlow的笔记本,开始吧。
加载数据
让我们从加载客户提供的历史数据开始:
-
我们将首先导入用于执行任务的 TensorFlow 库:
# import tensorflowimport tensorflow as tffrom tensorflow import kerasfrom tensorflow.keras import Sequentialfrom tensorflow.keras.layers import Denseprint(tf.__version__)
运行代码后,我们可以看到将使用的 TensorFlow 版本。在我写这篇文章时,它是 2.8.0。你很可能会有一个更新的版本,但它应该同样可以正常工作:
2.8.0
-
然后,我们将导入一些额外的库,帮助我们简化工作流程。
#import additional librariesimport numpy as npimport pandas as pd# For visualizationsimport matplotlib.pyplot as pltimport seaborn as sns#for splitting the data into training and test setfrom sklearn.model_selection import train_test_split# For Normalizationfrom sklearn.preprocessing import MinMaxScaler# Confusion matrixfrom sklearn.metrics import confusion_matrix, classification_report
我们之前讨论过大部分将在这里使用的库,除了代码块的最后一行,它将用来从 scikit-learn 库中导入混淆矩阵和分类报告。我们将使用这些函数来评估模型的性能。如果你不清楚其他库的用法,可以参考第三章《使用 TensorFlow 进行线性回归》来了解,之后再继续本案例的学习。
-
现在我们已经加载了所有必要的库,让我们创建一个 DataFrame 以便于处理:
#Loading data from the course GitHub repositorydf=pd.read_csv('https://raw.githubusercontent.com/PacktPublishing/TensorFlow-Developer-Certificate/main/Chapter%204/Students-Dropout-Prediction.csv', index_col=0)df.head()
当我们运行代码时,如果一切正常,应该会显示数据集的前五行。

图 4.2 – 显示数据集前五行的 DataFrame
从输出中,我们可以看到数据由数值型和类别型列组成。每一行代表一个学生。经过检查,我们发现有 12 列,分别是:学生 ID、学生姓名、图书馆、资源、财务、奖学金、学习时间、学习小组、GPA、测试、作业、毕业。为了高效地建模我们的数据,我们需要进行一些数据准备工作,因此让我们开始一些探索性数据分析,看看能发现什么。
探索性数据分析
执行以下步骤以探索和分析数据:
- 我们将使用
df.info()函数开始探索性数据分析过程,以检查数据集中的NULL值以及数据类型,如图 4.3所示。

图 4.3 – 我们数据集的信息
好消息是,我们的数据集没有缺失值,是的,我们将处理一个比回归任务中更大的数据集。在这里,我们有 25,000 个数据点,代表从大学收集的学生数据。
-
下一步是删除无关的列。通过检查可用的列,我们删除了
学生 ID和学生姓名列,因为这些列对学生是否毕业没有影响。我们在这里使用 pandas 的drop函数来完成这一操作:df = df.drop(['Student ID', 'Student Name'], axis=1) -
接下来,让我们使用
describe函数生成数据集的关键统计信息,因为这将帮助我们了解数据集的情况,如图 4.4所示。

图 4.4 – 数值列的摘要统计
从图 4.4中,我们可以看到平均 GPA 为3.00,最低 GPA 为1.00,最高 GPA 为5.00。测试和作业列的最低分为 5,最高分为 15。然而,我们对目标列的分布没有明确的了解,因为它是一个分类列;我们稍后会解决这个问题。
-
现在,让我们为分类目标变量绘制一个直方图:
plt.hist(df['Graduated'])plt.show()
运行这段代码会生成图 4.5所示的图表。在这里,我们使用matplotlib绘制毕业列,结果显示大约有 17,500 名成功毕业的学生,而大约有 7,500 名未能毕业的学生。当然,合理的预期是更多的学生会毕业。从机器学习的角度来看,我们处理的是一个不平衡的数据集。然而,幸运的是,我们仍然拥有足够的样本来训练来自少数类的模型。无论如何,不要只听我的话;稍后,我们将在完成数据准备步骤后训练我们的模型。

图 4.5 – 数值列的摘要统计
-
我们的笔记本中还有更多图表可以探索,但我们会保持简洁,因为本书的主要目标是专注于使用 TensorFlow 构建模型。不过,让我们看一下一个非常重要的图表:
sns.set(style="darkgrid")tdc =sns.scatterplot(x ='Library', y ='GPA',data = df, hue ='Graduated')tdc.legend(loc='center left',bbox_to_anchor=(1.0, 0.5), ncol=1)
在这里,我们使用seaborn绘制一个散点图,显示图书馆列在x轴上,GPA在y轴上,并使用毕业列来为数据点着色,如图 4.6所示。

图 4.6 – 图书馆与 GPA
从这个图表中,我们可以看到有相当一部分 GPA 高于 3.50 的学生已经毕业。然而,不要认为在平均、良好和优秀列中,所有 GPA 高于 3.50 的学生都已经毕业。事实上,让我们来验证一下:
#To get the number of students with gpa equal to or greater than 3.5 and did not graduate
len(df[(df['GPA']>=3.50)&(df['Graduated']=="Drop out")])
当我们运行这段代码时,它返回了 GPA 在 3.50 及以上且辍学的学生总数。总共有 76 名辍学学生。记住,我们的图表覆盖了 25,000 个数据点,所以如果你在图 4.6的图中没有找到这些数据点,不要惊讶。
现在,让我们继续准备数据进行建模。
数据预处理
在第三章,《使用 TensorFlow 的线性回归》中,我们强调了将数据放入正确形式、处理缺失数据、删除不相关特征、将分类值转换为数值型等的必要性。我们将在这里继续沿着这个思路进行:
-
让我们从将标签转换为数值型开始:
#Replace the classes in the graduate columndf['Graduated'] = df['Graduated'].replace(['Graduated', 'Drop out'],[1,0])
在这里,我们将值为1赋给毕业的学生,将值为0赋给辍学的学生。
-
现在,让我们使用
corr()函数检查我们的数值数据与目标变量之间的相关性:df.corr()
当我们运行代码时,我们会得到图 4.7所示的相关性表。

图 4.7 – 我们数据集的相关性表
从高亮的列中,我们可以看到GPA与Graduated列的相关性最强。
-
现在,让我们将分类变量转换为数值型变量。我们将继续使用虚拟变量(dummy variables)对分类变量进行独热编码(one-hot encoding):
#Converting categorical variables to numeric valuesdf = pd.get_dummies(df, drop_first=True)df.head()
在这里,我们删除了第一列,以避免虚拟变量陷阱。当我们运行代码时,我们会得到一个新的 DataFrame,如图 4.8所示。

图 4.8 – 独热编码后的 DataFrame
-
现在,我们已经将特征转化为数值形式,接下来让我们看看它们与目标变量的相关性:
tagret_corr= df.corr()tagret_corrtagret_corr['Graduated'].sort_values(ascending=False)
一旦运行代码,它会返回所有列与目标变量的相关性,如图 4.9所示。我们的初始数值变量仍然是相关性最高的变量。

图 4.9 – 特征与目标列的相关性
-
我们已经成功将数据转换为数值型变量,接下来我们将数据拆分为特征(
X)和目标变量(y):# We split the attributes and labels into X and y variablesX = df.drop("Graduated", axis=1)y = df["Graduated"] -
别忘了,我们需要对数据进行归一化处理。因此,我们将所有特征缩放到相同的尺度,以便于建模过程:
# create a scaler objectscaler = MinMaxScaler()# fit and transform the dataX_norm = pd.DataFrame(scaler.fit_transform(X),columns=X.columns)X_norm.head()
同样,我们使用来自 scikit-learn 库的MinMaxScaler,然后将数据分为训练集和测试集。在训练中,我们使用 80% 的数据,保留 20% 作为测试数据,以测试模型的泛化能力。我们设置随机种子为 10,以确保能够复现相同的数据划分:
# Create training and test sets
#We set the random state to ensure reproducibility
X_train, X_test, y_train, y_test = train_test_split(
X_norm, test_size=0.2, random_state=10)
数据准备工作已经完成,让我们继续用 TensorFlow 构建模型。
模型构建
为了构建我们的模型,我们将首先创建一个神经网络架构;在这里,我们将使用顺序 API 来定义我们希望顺序连接的层数。如图 4.10所示,我们只有输入层和输出层。与在第三章《使用 TensorFlow 进行线性回归》中预测数值不同,我们的输出层只有一个神经元,因为我们正在处理一个二分类问题。对于输出层,所使用的激活函数取决于当前的任务。当我们处理二分类任务时,通常使用sigmoid 激活函数;对于多分类问题,我们通常使用softmax 激活函数;而在处理多标签分类问题时,我们通常使用 sigmoid 作为激活函数。

图 4.10 – 在 TensorFlow 中创建分类模型
让我们继续编译我们的模型。当我们处理二分类问题时,我们将使用二元交叉熵作为损失函数,而处理多分类问题时,我们将使用类别交叉熵或稀疏类别交叉熵。在第五章《使用神经网络进行图像分类》中,我们将深入讨论激活函数等内容,随着我们继续构建对神经网络的理解和应用:
#compile the model
model1.compile(loss='binary_crossentropy',
optimizer='adam', metrics='accuracy')
接下来,让我们编译我们的模型。在这里,我们将使用准确率作为评估指标。我们还将查看其他分类指标,这些指标我们在之前讨论过,当我们开始评估模型在测试数据上的表现时会用到。编译模型后,下一步是拟合我们的模型。在第一章《机器学习简介》中,我们谈到了训练、验证和测试数据的划分。由于我们将处理一个更大的数据集,接下来我们将使用验证集来评估模型在每个周期结束时的表现,这样我们可以在对保留的测试集进行测试之前,监控模型在未见数据上的表现。我们将validation_split参数设置为0.2;这意味着我们将在训练过程中使用 20%的训练数据作为验证数据,总共进行 40 个周期:
#fit the model
history1= model1.fit(X_train, y_train, epochs=40,
validation_split=0.2)
在图 4.11中,我们可以看到模型输出的最后五个周期:

图 4.11 – 模型训练(最后五个周期)
该模型达到了 99.35%的训练准确率和 99.33%的验证准确率。仅用了两层和三个简单的步骤,在不到五分钟的时间里,我们就在训练和验证数据上达到了接近 100%的准确率。这些结果确实令人印象深刻;然而,需要知道的是,这并不总是如此,特别是当我们处理更复杂的数据集时。它们可能需要更复杂的架构和更长的训练时间才能取得良好的结果。在本书的第二部分中,我们将处理图像数据。继续评估我们的模型之前,让我们通过summary函数查看一下模型的架构:
model1.summary()
当我们运行这行代码时,我们生成了模型的架构:
Model: "sequential"
___________________________________________________________
Layer (type) Output Shape Param #
===========================================================
dense (Dense) (None, 16) 256
dense_1 (Dense) (None, 1) 17
===========================================================
Total params: 273
Trainable params: 273
Non-trainable params: 0
___________________________________________________________
输出形状告诉我们,第一层dense层(输入层)有 16 个神经元和 256 个参数,因为我们传入了 16 个属性(16 列 x 16 个神经元 = 256 个参数),而dense_1层(输出层)有 1 个神经元和 17 个参数(17 列 x 1 个神经元 = 17 个参数)。总参数数为 273,且所有参数都是可训练的,所以这里的参数为 273,这意味着没有非训练参数。现在我们完成了模型构建,让我们将注意力转向评估模型。它在测试数据上表现如何?
分类性能评估
为了在 TensorFlow 中评估我们的模型,我们只需要一行代码——使用evaluate函数来评估我们的模型:
# Evaluate the Classication model
eval_model=model1.evaluate(X_test, y_test)
eval_model
然后我们在保留数据上生成模型的性能:
157/157 [==============================] - 1s 4ms/step - loss: 0.0592 - accuracy: 0.9944
[0.05915425345301628, 0.9944000244140625]
我们在测试数据上得到了 99.44%的准确率。这是不错的结果;然而,让我们看看本章早些时候提到的其他分类指标:
y_pred=model1.predict(X_test).flatten()
y_pred = np.round(y_pred).astype('int')
df_predictions = pd.DataFrame(
{'Ground_Truth': y_test, 'Model_prediction': y_pred},
columns=[ 'Ground_Truth', 'Model_prediction'])
len(df_predictions[(df_predictions[
'Ground_Truth']!=df_predictions['Model_prediction'])])
我们在测试数据上生成模型的预测结果。然后,我们使用np.round()函数将概率值进行四舍五入,并将数据类型转换为整数。接着,我们创建一个 pandas DataFrame,然后生成 DataFrame 中被误分类的标签数量。在我们的案例中,模型错误地分类了测试集中 5,000 个数据点中的 28 个。现在,我们将生成一个混淆矩阵和分类报告来评估我们的模型:
#Generating the confusion matrix
eval = confusion_matrix(y_test, y_pred)
print(eval)
运行这段代码将生成图 4.12所示的混淆矩阵。

图 4.12 – 我们学生退学模型的混淆矩阵
水平箭头指向真实值的方向,而垂直箭头指向预测标签的方向。我们的真实情况是有(5 + 3,498) = 3,503 名学生毕业,而模型预测的毕业生人数是(3,498 + 23) = 3,521。同时,在退学类别中,模型预测了(5 + 1,474) = 1,479 名学生退学,而真实情况是(1,474 + 23) = 1,497。从图 4.14中我们可以看到,模型错误地预测了 23 名退学的学生是毕业生,以及 5 名毕业生是退学生。
接下来,让我们打印出我们的分类报告:
class_names = [ 'Drop Out', 'Graduated']
print(classification_report(y_test, y_pred,
target_names=class_names))
现在,我们已经打印出了我们的分类报告,如图 4.15所示,我们可以看到模型在数据集中的两个类别中的精确率、召回率和 F1 分数,以及宏观和加权平均值。

图 4.13 – 分类报告
从图 4.13中的突出细节,我们可以看到模型的精确率、召回率和 F1 分数。我们已经走了很长一段路;这是一个不错的结果。如果我们希望改进结果,可以尝试更多实验。此外,错误分析非常有助于帮助我们理解错误分类的数据。我们可以深入分析误分类的学生,尝试理解模型未能正确预测的案例中的模式或共同特征。这可以带来进一步的见解,或帮助我们识别与数据质量相关的问题,等等。然而,我们在这里不会深入探讨错误分析。你已经做得很好,在两个类别中都取得了不错的结果。
现在,让我们使用 save 函数保存模型并展示给经理:
#saving our model
model1.save('classification_model.h5')
我们已经完成了任务,并且得到了一个近乎完美的模型。
现在,你应该能够使用 TensorFlow 为结构化数据问题构建一个实际的分类器,并运用本章案例研究中学到的知识。
小结
在本章中,我们讨论了分类建模,并了解了主要的分类问题类型。我们还讨论了评估分类模型的主要指标类型,以及如何将它们最佳地应用于实际使用案例。然后,我们看了一个实际使用案例,在该案例中,我们学习了如何使用 TensorFlow 构建、编译和训练一个用于二分类问题的分类模型。
最后,我们通过实践学习了如何评估我们的分类模型。现在,我们已经完成了本书的第一部分。准备好迎接接下来的章节,在那里我们将看到 TensorFlow 在处理非结构化数据(图像和文本数据)时的强大功能。
问题
让我们测试一下本章所学的内容。
-
什么是分类建模?
-
多分类和多标签分类问题之间有什么区别?
-
你在一家提供有趣儿童内容的流媒体公司工作。在精确率和召回率之间,你会重点改进哪个指标,为什么?
-
你的公司正在构建一个贷款预测系统,用来为客户提供贷款。在精确率和召回率之间,你会重点改进哪个指标,为什么?
进一步阅读
若要了解更多内容,可以查看以下资源:
-
Amr, T., 2020. 使用 scikit-learn 和科学 Python 工具包进行机器学习实战。[S.l.]: Packt 出版社。
-
Beger, A., 2016. 精确率-召回率曲线。SSRN 电子期刊。
-
Raschka, S. 和 Mirjalili, V., 2019. Python 机器学习 – 第三版。Packt 出版社。
-
TensorFlow 指南:
www.TensorFlow.org/guide。
第二部分 – 使用 TensorFlow 进行图像分类
在这部分内容中,您将学习使用卷积神经网络(CNNs)构建二元和多类图像分类器,了解如何通过调整超参数来提高模型性能,以及如何处理过拟合问题。到最后,您将能够使用迁移学习构建真实世界的图像分类器。
本节包括以下章节:
-
第五章,使用神经网络进行图像分类
-
第六章,改进模型
-
第七章,使用卷积神经网络进行图像分类
-
第八章,处理过拟合
-
第九章,迁移学习
第五章:使用神经网络进行图像分类
到目前为止,我们已经成功地构建了用于解决结构化数据的回归和分类问题的模型。接下来的问题是:我们能否构建能够区分狗和猫,或者汽车和飞机的模型?如今,在TensorFlow和PyTorch等框架的帮助下,开发人员可以仅用几行代码构建这样的机器学习解决方案。
在本章中,我们将探索神经网络的构造,并学习如何将它们应用于计算机视觉问题的模型构建。我们将首先了解什么是神经网络,以及多层神经网络的架构。我们还将探讨一些重要的概念,如前向传播、反向传播、优化器、损失函数、学习率和激活函数,以及它们在网络中的作用和位置。
在我们扎实掌握核心基础后,我们将使用 TensorFlow 中的自定义数据集构建图像分类器。在这里,我们将通过 TensorFlow 数据集的端到端过程来构建模型。使用这些自定义数据集的好处是,大部分预处理步骤已经完成,我们可以毫无障碍地对数据进行建模。因此,我们将使用这个数据集,在 TensorFlow 的Keras API 下通过几行代码构建一个神经网络,使我们的模型能够区分包和衬衫,鞋子和外套。
在本章中,我们将涵盖以下主题:
-
神经网络的构造
-
使用神经网络构建图像分类器
技术要求
我们将使用python >= 3.8.0,并配合以下可以通过pip install命令安装的包:
-
tensorflow>=2.7.0 -
tensorflow-datasets==4.4.0 -
pillow==8.4.0 -
pandas==1.3.4 -
numpy==1.21.4 -
matplotlib >=3.4.0
本章的代码可以在github.com/PacktPublishing/TensorFlow-Developer-Certificate-Guide/tree/main/Chapter%205找到。此外,所有练习的解答也可以在 GitHub 仓库中找到。
神经网络的构造
在本书的第一部分,我们讨论了模型。我们所讲解和使用的这些模型是神经网络。神经网络是一种深度学习算法,受到人脑功能的启发,但它并不完全像人脑那样运作。它通过分层的方法学习输入数据的有用表示,如图 5.1所示:

图 5.1 – 神经网络
神经网络非常适合解决复杂问题,因为它们能够识别数据中非常复杂的模式。这使得它们特别适合围绕文本和图像数据(非结构化数据)构建解决方案,而这些是传统机器学习算法难以处理的任务。神经网络通过分层表示,开发规则将输入数据映射到目标或标签。当我们用标记数据训练它们时,它们学习模式,并利用这些知识将新的输入数据映射到相应的标签。
在图 5.1中,我们看到输入层的所有神经元都与第一隐藏层的神经元相连接,第一隐藏层的所有神经元都与第二隐藏层的神经元相连接。从第二隐藏层到外层也同样如此。这种每一层的神经元都与下一层的神经元完全连接的网络,被称为全连接神经网络。拥有两个以上隐藏层的神经网络被称为深度神经网络(DNN),网络的深度由其层数决定。
让我们深入探讨神经网络架构中的各个层:
-
输入层:这是我们将输入数据(文本、图像、表格数据)输入网络的层。在这里,我们必须指定正确的输入形状,之前在我们的回归案例研究中,第三章**,TensorFlow 线性回归,以及在我们的分类案例研究中,第四章**,TensorFlow 分类中都已经做过这种操作。需要注意的是,输入数据将以数字格式呈现给我们的神经网络。在这一层,不会进行任何计算。这更像是一个将数据传递到隐藏层的通道层。
-
隐藏层:这是下一个层,位于输入层和输出层之间。之所以称为隐藏层,是因为它对外部系统不可见。在这里,进行大量计算以从输入数据中提取模式。我们在隐藏层中添加的层数越多,我们的模型就会变得越复杂,处理数据所需的时间也越长。
-
输出层:该层生成神经网络的输出。输出层神经元的数量由当前任务决定。如果我们有一个二分类任务,我们将使用一个输出神经元;而对于多类分类任务,例如我们的案例研究中有 10 个不同的标签,我们将有 10 个神经元,每个标签对应一个神经元。
我们现在知道神经网络的各层,但关键问题是:神经网络是如何工作的,它是如何在机器学习中占据特殊地位的?
神经网络通过前向传播和反向传播的结合解决复杂任务。我们先从前向传播开始。
前向传播
假设我们希望训练神经网络有效地识别图 5.2中的图像。我们将传递大量我们希望神经网络识别的图像代表性样本。这里的想法是,我们的神经网络将从这些样本中学习,并利用所学知识识别样本空间中的新项。比如,假设我们希望模型识别衬衫,我们将传递不同颜色和大小的衬衫。我们的模型将学习衬衫的定义,而不论其颜色、尺寸或样式如何。模型所学到的衬衫核心属性的表示将用于识别新衬衫。

图 5.2 – 来自 Fashion MNIST 数据集的示例图像
让我们看一下幕后发生了什么。在我们的训练数据中,我们将图像(X)传入模型 f(x) . . → ˆ y,其中 ˆ y 是模型的预测输出。这里,神经网络随机初始化权重,用于预测输出(ˆ y)。这个过程被称为前向传播或前向传递,如图 5.3所示。
注意
权重是可训练的参数,会在训练过程中进行更新。训练完成后,模型的权重会根据其训练数据集进行优化。如果我们在训练过程中适当地调整权重,就能开发出一个表现良好的模型。
当输入数据流经网络时,数据会受到节点的权重和偏置的影响而发生变换,如图 5.3所示,从而产生一组新的信息,这些信息将通过激活函数。如果希望得到新的学习信息,激活函数将触发一个输出信号,作为下一个层的输入。这一过程持续进行,直到在输出层生成输出:

图 5.3 – 神经网络的前向传播
让我们再谈谈激活函数及其在神经网络中的作用。
激活函数
想象一下,你需要从一篮苹果中挑选出好苹果。通过检查这些苹果,你可以挑出好苹果并丢掉坏苹果。这就像激活函数的作用——它充当了一个分隔器,定义了哪些信息会通过,在我们的例子中,这就是它学到的有用表示,并丢弃不必要的数据。从本质上讲,它帮助提取有用信息,就像挑选出好苹果一样,丢弃无用的数据,而在我们的场景中,坏苹果就是无用的数据。现在,激活函数决定了下一层哪个连接的神经元将被激活。它通过数学运算判断一个学习到的表示是否足够有用,能够供下一层使用。
激活函数可以为我们的神经网络添加非线性,这是神经网络学习复杂模式所必需的特性。激活函数有多种选择;对于输出层,激活函数的选择取决于手头任务的类型:
-
对于二分类问题,我们通常使用 sigmoid 函数,因为它将输入映射到介于 0 和 1 之间的输出值,表示属于某个特定类别的概率。我们通常将阈值设置为 0.5,因此大于此点的值设置为 1,小于此点的值设置为 0。
-
对于多分类问题,我们使用 Softmax 激活 作为输出层的激活函数。假设我们想要构建一个图像分类器,将四种水果(苹果、葡萄、芒果和橙子)进行分类,如图 5.4 所示。每种水果在输出层分配一个神经元,并且我们会应用 Softmax 激活函数来生成输出属于我们希望预测的水果之一的概率。当我们将苹果、葡萄、芒果和橙子的概率加起来时,结果为 1。对于分类任务,我们选择概率最大的水果类别作为从 Softmax 激活函数生成的概率中的输出标签。在这种情况下,概率最大的输出是橙子:

图 5.4 – SoftMax 激活函数的应用
对于隐藏层,我们将使用 修正线性单元(ReLU)激活函数。这个激活函数去除了负值(无用的表示),同时保留了大于 0 的学习表示。ReLU 在隐藏层表现出色,因为它收敛快速并且支持反向传播,这是我们接下来将要讨论的概念。
注意
在二分类问题中,使用 sigmoid 函数更加高效,这时我们只有一个输出神经元,而使用 Softmax 时会有两个输出神经元。此外,当我们阅读代码时,更容易理解我们处理的是二分类问题。
反向传播
当我们开始训练模型时,权重最初是随机的,这使得模型更容易错误地猜测图 5.4 中的水果是橙子。此时神经网络的智能就体现出来了;它会自动修正自己,如图 5.5 所示:

图 5.5 – 神经网络的前向传播与反向传播
在这里,神经网络衡量预测输出(ˆy)与真实值(y)的比较结果的准确性。这个损失是通过损失函数计算的,损失函数也可以称为代价函数。这些信息会传递给优化器,其任务是更新神经网络中各层的权重,目的是在接下来的迭代中减少损失,从而使我们的预测更接近真实值。这个过程会持续,直到我们实现收敛。收敛发生在模型训练过程中,损失达到了最小值。
损失函数的应用依赖于当前任务。当我们处理二分类任务时,我们使用二元交叉熵;对于多分类任务,如果目标标签是整数值(例如,0 到 9),我们使用稀疏分类交叉熵,而如果我们决定对目标标签进行独热编码,则使用分类交叉熵。与损失函数类似,我们也有不同类型的优化器;然而,我们将尝试使用随机梯度下降法(SGD)和Adam 优化器,它是 SGD 的改进版。因此,我们将使用它作为我们的默认优化器。
学习率
我们现在知道权重是随机初始化的,而优化器的目的是利用关于损失函数的信息来更新权重,从而实现收敛。神经网络使用优化器迭代更新权重,直到损失函数达到最小值,如图 5.6所示。优化器允许你设置一个重要的超参数——学习率,它控制收敛的速度,并且是我们模型学习的方式。为了到达斜率的底部,我们必须朝着底部迈出步伐(见图 5.6):

图 5.6 – 梯度下降
我们采取的步伐大小将决定我们到达底部的速度。如果我们走得步伐太小,将需要很长时间才能到达底部,且会导致收敛变慢,甚至存在优化过程可能在到达最小值的过程中卡住的风险。反之,如果步伐太大,则可能会错过最小值,并出现不稳定和异常的训练行为。正确的步伐大小将帮助我们及时到达斜率的底部而不会错过最小点。这里提到的步伐大小就是学习率。
我们现在已经高层次地了解了神经网络的直觉。接下来,让我们进行案例研究,直接应用我们刚刚学到的内容。
用神经网络构建图像分类器
我们回到了虚构的公司,现在我们希望利用神经网络的直觉来构建一个图像分类器。在这里,我们要教计算机识别服装。幸运的是,我们不需要在野外寻找数据;我们有 TensorFlow 数据集,其中包括时尚数据集。在我们的案例研究中,我们的目标是将一个由 28 x 28 灰度图像组成的时尚数据集分类为 10 类(从 0 到 9),每个像素值介于 0 到 255 之间,使用一个广为人知的数据集——Fashion MNIST 数据集。该数据集由 60,000 张训练图像和 10,000 张测试图像组成。我们的数据集中的所有图像都是相同的形状,因此我们几乎不需要做什么预处理。这里的想法是,我们可以快速构建一个神经网络,而不需要复杂的预处理。
为了训练神经网络,我们将传递训练图像,假设我们的神经网络将学习将图像(X)映射到它们相应的标签(y)。在完成训练过程后,我们将使用测试集对模型在新图像上的表现进行评估。同样,目的是让模型根据它在训练过程中学到的知识,正确识别测试图像。让我们开始吧。
加载数据
在这里,我们将首先学习如何使用 TensorFlow 数据集处理图像。在 第七章,卷积神经网络进行图像分类,我们将处理需要更多建模工作以使用的真实世界图像;不过,它将基于我们在这里学到的内容。话虽如此,让我们看看如何从 TensorFlow 加载自定义数据集:
-
在加载数据之前,我们需要加载必要的库。我们在这里做这件事:
import tensorflow as tffrom tensorflow import kerasimport pandas as pdimport randomimport numpy as npimport matplotlib.pyplot as plt #helper librariesfrom tensorflow.keras.utils import plot_model -
接下来,我们从 TensorFlow 导入
fashion_mnist数据集,并使用load_data()方法创建我们的训练集和测试集:#Lets import the fashion mnistfashion_data = keras.datasets.fashion_mnist#Lets create of numpy array of training and testing data(train_images, train_labels), (test_images,test_labels) = fashion_data.load_data()如果一切按计划进行,我们应该会得到如 图 5.7 所示的输出:

图 5.7 – 从 TensorFlow 数据集中导入数据
-
现在,我们不再使用数字标签,而是创建与数据匹配的标签,这样我们可以把一件衣服叫做“衣服”,而不是叫它编号 3。我们将通过创建一个标签列表,并将其映射到相应的数字值来实现这一点:
#We create a list of the categoriesclass_names=['Top', 'Trouser','Pullover', 'Dress', 'Coat','Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankleboot']
现在我们有了数据,让我们探索数据,看看能发现什么。与其盲目接受所说的每一件事,不如探索数据以验证大小、形状和数据分布。
执行探索性数据分析
加载完数据后,下一步是检查数据,了解数据的基本情况。当然,在这个实例中,我们已经从 TensorFlow 获得了一些关于数据分布的基本信息。同时,我们的数据已经以训练集和测试集的形式准备好了。然而,让我们通过代码确认所有细节,并查看我们目标标签的类别分布:
-
我们将使用
matplotlib库生成索引为i的图像样本,其中i在 60,000 个训练样本中:# Display a sample image from the training data (index 7)plt.imshow(train_images[7])plt.grid(False)plt.axis('off')plt.show()我们使用索引
7运行代码,返回了如图 5.7所示的上衣:

图 5.8 – Fashion MNIST 数据集索引为 7 的一件套头衫照片
我们可以切换索引值,查看数据集中其他的服装;不过,这并不是我们这里的目标。所以,让我们继续进行探索性数据分析。
-
让我们来看一下我们数据的样本:
#Lets check the shape of our training images and testing imagestrain_images.shape, test_images.shape如预期的那样,我们可以看到训练图像由 60,000 张 28 x 28 的图像组成,而测试图像有 10,000 张,分辨率为 28 x 28:
((60000, 28, 28), (10000, 28, 28)) -
接下来,让我们检查数据的分布情况。最好先了解数据的分布情况,以确保每个我们希望训练模型的服装类别都有足够的样本。让我们在这里进行检查:
df=pd.DataFrame(np.unique(train_labels,return_counts=True)).Tdict = {0: ‹Label›,1: ‹Count›}df.rename(columns=dict,inplace=True)df这将返回如图 5.9所示的
DataFrame。我们可以看到所有标签的样本数是相同的:

图 5.9 – 显示标签及其计数的 DataFrame
当然,这种类型的数据更可能出现在受控环境下,比如学术界。
-
让我们在这里可视化一些训练数据中的样本图像。让我们来看 16 个来自训练数据的样本:
plt.figure(figsize=(9,9))for i in range(16):plt.subplot(4,4,i+1)plt.xticks([])plt.yticks([])plt.grid(False)plt.imshow(train_images[i])plt.title(class_names[train_labels[i]])plt.show() -
当我们运行代码时,我们得到了图 5.10中的图像:

图 5.10 – 从 Fashion MNIST 数据集中随机选取的 16 张图像
现在我们已经确认了数据大小、数据分布和形状,并查看了一些样本图像和标签。在我们开始构建和训练图像分类器之前,回顾一下我们的数据由灰度图像组成,值范围从 0 到 255。为了对数据进行归一化并提升模型在训练过程中的表现,我们需要对数据进行归一化处理。我们可以通过简单地将训练数据和测试数据除以 255 来实现这一点:
#it's important that the training and testing set are preprocessed in the same way.
train_images=train_images/255.0
test_images=test_images/255.0
现在我们已经对数据进行了归一化处理,接下来就可以进行建模了。让我们继续构建图像分类器。
构建模型
让我们将到目前为止在本章中学到的所有知识付诸实践:
#Step 1: Model configuration
model=keras.Sequential([
keras.layers.Flatten(input_shape=(28,28)),
keras.layers.Dense(64, activation="relu"),
keras.layers.Dense(10,activation="softMax")
])
#Here we flatten the data
我们用来构建模型的代码与本书第一部分中使用的代码类似。我们首先使用 Sequential API 创建一个顺序模型,以定义我们想要按顺序连接的层数。如果你是一个细心的观察者,你会注意到我们的第一层是一个展平层。这个层用于将图像数据展平为一个 1D 数组,然后传递给隐藏层。输入层没有神经元,它充当数据预处理层,将数据展平为 1D 数组后传递给隐藏层。
接下来,我们有一个 64 个神经元的隐藏层,并对该隐藏层应用 ReLU 激活函数。最后,我们有一个包含 10 个神经元的输出层——每个输出一个神经元。由于我们处理的是多类分类问题,因此使用 softmax 函数。Softmax 返回的是所有类别的概率结果。如果你还记得激活函数部分,输出概率的总和为 1,概率值最大的输出就是预测标签。
现在,我们完成了模型构建,接下来继续编译模型。
编译模型
下一步是编译模型。我们将使用compile方法来完成这个操作。在这里,我们传入我们希望使用的优化器;在这种情况下,我们使用Adam,它是我们的默认优化器。我们还指定了损失函数和评估指标。由于我们的标签是数字值,因此我们使用稀疏分类交叉熵作为损失函数。对于评估指标,我们使用准确率,因为我们的数据集是平衡的。准确率指标将真实反映我们模型的性能:
#Step 2: Compiling the model, we add the loss, optimizer and evaluation metrics here
model.compile(optimizer='adam',
loss=›sparse_categorical_crossentropy',
metrics=[‹accuracy›])
在我们开始拟合模型之前,先来看一下几种可视化模型及其参数的方法。
模型可视化
为了可视化我们的模型,我们使用summary()方法。这将为我们提供一个详细的视觉表示,展示模型的架构、各层、参数数量(可训练和不可训练)以及输出形状:
model.summary()
当我们运行代码时,它将返回模型的详细信息,如图 5.11所示:

图 5.11 – 模型摘要
从图 5.11中,我们可以看到输入层没有参数,但输出形状为 784,这是将 28 × 28 的图像展平为一维数组的结果。要计算全连接层的参数数量,它是 784 × 64 + 64 = 50240(回想一下,X是输入数据,w是权重,b是偏置)。输出层(dense_1)的形状为 10,其中每个神经元代表一个类别,共有 650 个参数。回想一下,一个层的输出作为下一个层的输入。因此,64 × 10 + 10 = 650,其中 64 是隐藏层的输出形状,也是输出层的输入形状。
另一方面,我们还可以通过以下代码将模型显示为流程图,如图 5.12所示:
plot_model(model, to_file='model_plot.png', show_shapes=True,
show_layer_names=True)

图 5.12 – 模型流程图
这也让我们对模型的结构有了一个大致了解。我们生成的图表将保存为文件名model_plot.png。在这里,我们将show_shapes设置为true;这将在图中显示每一层的输出形状。我们还将show_layer_name设置为true,以在图中显示各层的名称,正如图 5.12所示。
接下来,让我们将模型拟合到训练数据中。
模型拟合
到现在为止,你应该已经熟悉这个过程。通过一行代码,我们可以使用fit方法来拟合我们的训练图像(X)和训练标签(y):
#Step 3: We fit our data to the model
history= model.fit(train_images, train_labels, epochs=5)
在这里,我们将数据训练了五个 epoch。我们的模型返回了损失和准确率:
1875/1875 [==============================] – 4s 2ms/step – loss: 0.5206 – accuracy: 0.8183
Epoch 2/5
1875/1875 [==============================] – 4s 2ms/step – loss: 0.3937 – accuracy: 0.8586
Epoch 3/5
1875/1875 [==============================] – 4s 2ms/step – loss: 0.3540 – accuracy: 0.8722
Epoch 4/5
1875/1875 [==============================] – 4s 2ms/step – loss: 0.3301 – accuracy: 0.8790
Epoch 5/5
1875/1875 [==============================] – 4s 2ms/step – loss: 0.3131 – accuracy: 0.8850
我们可以看到,在仅仅五个 epoch 后,我们的模型达到了0.8850的准确率。考虑到我们只训练了非常少的 epoch,这是一个不错的开始。接下来,让我们通过绘制损失和准确率图来观察模型在训练过程中的表现。
训练监控
我们在拟合训练数据时返回一个history对象。在这里,我们使用history对象来创建损失和准确率曲线。以下是绘制图表的代码:
# Plot history for accuracy
plt.plot(history.history['accuracy'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['Train'], loc='lower right')
plt.show()
# Plot history for loss
plt.plot(history.history['loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['Train'], loc='upper right')
plt.show()
当我们运行代码时,我们得到两个图表,如图 5.13所示。我们可以看到,在第五个 epoch 结束时,训练准确率仍在上升,而损失仍在下降,尽管随着接近 0,下降的速度不再那么快:

图 5.13 – 准确率和损失图
或许如果我们训练更长时间,可能会看到更好的表现。在下一章中,我们将探讨如果我们延长训练时间会发生什么,并且还会查看其他提高模型表现的方法。在这里,目标是理解图表的含义,并获取足够的信息来指导我们接下来的行动。让我们在测试集上评估我们的模型。
评估模型
我们在测试集上评估我们模型的整体表现如下:
test_loss,test_acc =model.evaluate(test_images,test_labels)
print('Test Accuracy: ', test_acc)
我们在测试集上得到了0.8567的准确率。训练准确率和测试准确率之间的差异是机器学习中常见的问题,我们称之为过拟合。过拟合是机器学习中的一个关键问题,我们将在第八章中探讨过拟合及其处理方法,过拟合处理。
接下来,让我们用我们训练过的神经网络做一些预测。
模型预测
要对模型进行预测,我们在测试集的未见数据上使用model.predict()方法。让我们看看模型在测试数据的第一个实例上的预测:
predictions=model.predict(test_images)
predictions[0].round(2)
当我们运行代码时,我们得到一个概率数组:
array([0. , 0. , 0. , 0. , 0. ,
0.13, 0. , 0.16, 0. , 0.7 ],
dtype=float32)
如果我们检查概率,会发现在第九个元素的概率最高。因此,这个标签的概率为 70%。我们将使用np.argmax来提取标签,并将其与索引为0的测试标签进行比较:
np.argmax(predictions[0]),test_labels[0]
我们看到预测标签和测试标签的值都是9。我们的模型正确预测了这一点。接下来,让我们绘制 16 张随机图片,并将预测结果与真实标签进行比较。这次,我们不会返回标签的数值,而是返回标签本身,以便更清晰地展示:
# Let us plot 16 random images and compare the labels with the model's prediction
figure = plt.figure(figsize=(9, 9))
for i, index in enumerate(np.random.choice(test_images.shape[0],
size=16, replace=False)):
ax = figure.add_subplot(4,4,i + 1,xticks=[], yticks=[])
# Display each image
ax.imshow(np.squeeze(test_images[index]))
predict_index = np.argmax(predictions[index])
true_index = test_labels[index]
# Set the title for each image
ax.set_title(f»{class_names[predict_index]} (
{class_names[true_index]})",color=(
"green" if predict_index == true_index else «red»))
结果如图 5.14所示。尽管模型能够正确分类 10 个项目,但它在一个样本上失败了,将一件衬衫误分类为套头衫:

图 5.14 – 可视化模型在测试数据上的预测
仅用几行代码,我们就训练了一个图像分类器。在五个训练周期内,我们在训练数据上的准确率达到了 88.50%,在测试数据上的准确率为 85.67%。需要注意的是,这是一个用于学习的玩具数据集,尽管它非常适合学习,但实际世界中的图像更为复杂,训练将需要更长时间,且在许多情况下,需要更复杂的模型架构。
在这一章中,我们介绍了许多新概念,这些概念在后续章节以及考试中都会非常有用。
总结
在这一章中,我们讨论了图像分类建模。现在,你应该能够解释什么是神经网络,以及前向传播和反向传播的原理。你应该了解损失函数、激活函数和优化器在神经网络中的作用。此外,你应该能够熟练加载 TensorFlow 数据集中的数据。最后,你应该了解如何构建、编译、拟合和训练一个用于图像分类的神经网络,并评估模型,绘制损失和准确率曲线,解读这些可视化结果。
在下一章中,我们将探讨几种方法,用于提高我们模型的性能。
问题
让我们来测试一下我们在这一章中学到的内容:
-
激活函数的作用是什么?
-
反向传播是如何工作的?
-
输入层、隐藏层和输出层的作用是什么?
-
使用 TensorFlow 数据集,加载一个手写数字数据集,然后你将构建、编译、训练并评估一个图像分类器。这与我们的案例研究类似。加油!
进一步阅读
若要了解更多信息,你可以查看以下资源:
-
Amr, T., 2020. 深入学习与 scikit-learn 和科学 Python 工具包的实践。 [S.l.]: Packt Publishing.
-
Vasilev, I., 2019. Python 深度学习进阶。第 1 版。Packt Publishing.
-
Raschka, S. 和 Mirjalili, V., 2019. Python 机器学习。第 3 版。Packt Publishing.
-
Gulli, A., Kapoor, A. 和 Pal, S., 2019. 使用 TensorFlow 2 和 Keras 的深度学习。伯明翰:Packt Publishing.
-
TensorFlow 指南
www.TensorFlow.org/guide
第六章:改进模型
机器学习中建模的目标是确保我们的模型能在未见过的数据上良好泛化。在我们作为数据专业人员构建神经网络模型的过程中,可能会遇到两个主要问题:欠拟合和过拟合。欠拟合是指我们的模型缺乏足够的复杂性,无法捕捉数据中的潜在模式,而过拟合则是在模型过于复杂时,模型不仅学习到模式,还会拾取到训练数据中的噪声和异常值。在这种情况下,我们的模型在训练数据上表现非常好,但在未见过的数据上无法良好泛化。第五章,使用神经网络进行图像分类,探讨了神经网络背后的科学原理。在本章中,我们将探索调整神经网络的艺术,以构建在图像分类中表现最优的模型。我们将通过动手实践来探索各种网络设置,了解这些设置(超参数)对模型性能的影响。
除了探索超参数调整的艺术,我们还将探索多种改善数据质量的方法,如数据标准化、数据增强和使用合成数据来提高模型的泛化能力。过去,人们非常注重构建复杂的网络。然而,近年来,越来越多的人开始关注使用以数据为中心的策略来提升神经网络的表现。使用这些以数据为中心的策略并不会削弱精心设计模型的必要性;相反,我们可以将它们看作是互为补充的策略,协同工作以达成目标,从而增强我们构建具有良好泛化能力的最优模型的能力。
在本章中,我们将涵盖以下主题:
-
数据至关重要
-
调整神经网络的超参数
到本章结束时,你将能够有效应对过拟合和欠拟合所带来的挑战,通过结合以模型为中心和以数据为中心的思路,构建神经网络模型。
技术要求
我们将使用python >= 3.8.0,并安装以下可以通过pip install命令安装的包:
-
tensorflow>=2.7.0 -
tensorflow-datasets==4.4.0 -
pandas==1.3.4 -
numpy==1.21.4
数据至关重要
在提升神经网络或任何其他机器学习模型的性能时,良好的数据准备工作至关重要,不能过分强调。在第三章,使用 TensorFlow 进行线性回归中,我们看到了标准化数据对模型表现的影响。除了数据标准化之外,还有其他数据准备技巧可以在建模过程中产生影响。
如你现在应该已经意识到的,机器学习需要根据手头的问题进行调查、实验和应用不同的技术。为了确保我们拥有最佳性能的模型,我们的旅程应从彻底审视数据开始。我们是否拥有每个目标类别的足够代表性样本?我们的数据是否平衡?我们是否确保了标签的正确性?我们的数据类型是否正确?我们如何处理缺失数据?这些问题是我们在建模阶段之前必须提问并处理的。
提高我们数据的质量是一项多方面的工作,涉及通过应用数据预处理技术(如数据归一化)从现有数据中工程化新特征。当我们处理不平衡数据集时,尤其是当我们缺乏少数类的代表性样本时,合理的做法是收集更多少数类数据;然而,这在所有情况下并不实际。在这种情况下,合成数据可能是一个有效的替代方案。一些初创公司,如Anyverse.ai和Datagen.tech,专注于合成数据的开发,从而可以缓解数据不平衡和数据稀缺的问题。然而,合成数据可能会很昂贵,因此在选择这条路线之前,我们需要做一个成本效益分析。
我们可能面临的另一个问题是,当我们收集的样本不足以代表模型正确运行时。例如,你训练模型识别人脸。你收集了成千上万的人脸图像,并将数据分成训练集和测试集。你训练了模型,并且在测试集上预测得非常完美。然而,当你将这个模型作为产品推向市场时,你得到的结果可能像图 6.1所示:

图 6.1 – 数据增强的必要性
令人惊讶,对吧?即使你在成千上万的图像上训练了模型,如果轴被垂直或水平翻转,或者以其他方式改变,模型也未能学会识别面孔。为了解决这种问题,我们采用了一种叫做数据增强的技术。数据增强是一种通过某种方式改变现有数据来创建新训练数据的技术,例如随机裁剪、缩放或旋转、翻转初始图像。数据增强背后的基本思想是使我们的模型即使在不可预测的条件下(例如我们在图 6.1 中看到的情况),也能识别图像中的物体。
数据增强在我们希望从有限的训练集获得更多数据样本时非常有用;我们可以使用数据增强来有效地增加数据集的大小,从而为我们的模型提供更多的数据进行学习。此外,由于我们可以模拟各种场景,模型在学习数据中的潜在模式时不太可能发生过拟合,而不是学习数据中的噪声,因为我们的模型通过多种方式学习数据。数据增强的另一个重要好处是它是一种节省成本的技术,可以避免昂贵且有时耗时的数据收集过程。在第八章《处理过拟合》中,我们将应用数据增强技术,在实际的图像分类问题中进行实践。此外,如果将来你需要处理图像或文本数据,数据增强将是一个非常实用的技术。
除了解决数据不平衡和数据多样性问题外,我们可能还希望进一步优化我们的模型,使其足够复杂,以便识别数据中的模式,并且我们希望做到这一点而不导致模型过拟合。在这里,目标是通过调整一个或多个设置来提高模型的质量,例如增加隐藏层的数量、为每层添加更多神经元、改变优化器或使用更复杂的激活函数。这些设置可以通过实验进行调优,直到获得最优的模型。
我们已经讨论了若干提高神经网络性能的思路。现在,让我们看看如何提高在第五章《使用神经网络进行图像分类》中,在 Fashion MNIST 数据集上取得的结果。
神经网络的超参数微调
在进行机器学习改进之前,建立一个基线模型非常重要。基线模型是一个简单的模型,我们可以用它来评估更复杂模型的表现。在第五章,《使用神经网络进行图像分类》中,我们在仅五个训练周期内,训练数据的准确率为 88.50%,测试数据的准确率为 85.67%。为了进一步提高我们模型的表现,我们将继续按照三步流程(构建、编译、和训练)来构建神经网络,使用TensorFlow。在构建神经网络的每个步骤中,都有一些需要在训练前配置的设置,这些设置称为超参数。超参数决定了网络如何学习和表现,掌握调整超参数的技巧是构建成功深度学习模型的关键步骤。常见的超参数包括每层神经元的数量、隐藏层的数量、学习率、激活函数以及训练周期数。通过不断尝试这些超参数,我们可以找到最适合我们使用场景的最佳设置。
在构建现实世界的模型时,特别是处理特定领域问题时,专家知识可能会非常有助于定位任务的最佳超参数值。让我们回到笔记本,尝试不同的超参数,看看通过调整一个或多个超参数是否能够超越我们的基线模型。
增加训练周期数
想象一下,你正在教一个孩子乘法表;你与孩子每次的学习互动可以比作机器学习中的一个训练周期。如果你与孩子的学习次数很少,那么他们很可能无法完全理解乘法的概念。因此,孩子将无法尝试基本的乘法问题。在机器学习中,这种情况叫做欠拟合,指的是模型由于训练不足,未能捕捉到数据中的潜在模式。
另一方面,假设你花了很多时间教孩子记住乘法表的某些方面,比如 2 的倍数、3 的倍数和 4 的倍数。孩子在背诵这些乘法表时变得熟练;然而,当遇到类似 10 x 8 这样的乘法题时,孩子却感到困难。这是因为,孩子并没有理解乘法的原理,以至于能够在处理其他数字时应用这个基本的思想,而只是单纯地记住了学习过程中遇到的例子。在机器学习中,这种情况就像过拟合的概念,我们的模型在训练数据上表现良好,但在新情况中无法很好地泛化。在机器学习中,当我们训练模型时,需要找到一个平衡,使得模型足够好地学习数据中的潜在模式,而不是仅仅记住训练数据。
让我们看看延长训练时间对结果的影响。这次,我们选择 40 个训练轮次,观察会发生什么:
#Step 1: Model configuration
model=keras.Sequential([
keras.layers.Flatten(input_shape=(28,28)),
keras.layers.Dense(100, activation=»relu»),
keras.layers.Dense(10,activation=»softmax»)
])
#Step 2: Compiling the model, we add the loss, optimizer and evaluation metrics here
model.compile(optimizer='adam',
loss=›sparse_categorical_crossentropy›,
metrics=[‹accuracy›])
#Step 3: We fit our data to the model
history= model.fit(train_images, train_labels, epochs=40)
在这里,我们将步骤 3中的训练轮次从5改为40,同时保持我们基础模型的其他超参数不变。输出的最后五轮结果如下:
Epoch 36/40
1875/1875 [==============================] - 5s 2ms/step - loss: 0.1356 - accuracy: 0.9493
Epoch 37/40
1875/1875 [==============================] - 5s 2ms/step - loss: 0.1334 - accuracy: 0.9503
Epoch 38/40
1875/1875 [==============================] - 4s 2ms/step - loss: 0.1305 - accuracy: 0.9502
Epoch 39/40
1875/1875 [==============================] - 4s 2ms/step - loss: 0.1296 - accuracy: 0.9512
Epoch 40/40
1875/1875 [==============================] - 4s 2ms/step - loss: 0.1284 - accuracy: 0.9524
请注意,当我们增加训练轮次时,训练模型所需的时间会显著增加。所以,当训练轮次很大时,计算成本可能会变得很高。经过 40 个训练轮次后,我们发现模型的训练准确率达到了0.9524,看起来你可能认为已经找到了解决问题的灵丹妙药。然而,我们的目标是确保模型的泛化能力;因此,检验模型的关键是看它在未见过的数据上表现如何。让我们看看在测试数据上的结果如何:
test_loss, test_acc=model.evaluate(test_images,test_labels)
print('Test Accuracy: ', test_acc)
当我们运行代码时,我们在测试数据上的准确率为0.8692。可以看到,随着模型训练时间的增加,模型在训练数据上的准确率逐渐提高。然而,如果我们训练得太久,模型的效果会出现收益递减的现象,这在比较训练集和测试集准确度的表现差异时尤为明显。找到一个合适的训练轮次非常重要,以确保模型能够学习和提高,但又不会过度拟合训练数据。一种实用的方法是从较少的训练轮次开始,根据需要逐步增加训练轮次。虽然这种方法有效,但也可能比较耗时,因为需要进行多次实验来找到最优的训练轮次。
那么,如果我们能够设定一个规则,在收益递减前停止训练呢?是的,这是可能的。接下来我们来研究这个想法,看看它对结果会有什么影响。
使用回调函数进行早停
早停是一种正则化技术,可以用来防止神经网络在训练时出现过拟合。当我们将训练周期数硬编码到模型中时,我们无法在达到期望的度量标准时停止训练,或者当训练开始退化或不再改进时停止训练。我们在增加训练周期数时遇到了这个问题。然而,为了应对这种情况,TensorFlow 为我们提供了早停回调,使我们可以使用内置的回调函数,或者设计自定义的回调函数。我们可以实时监控实验,并且拥有更多的控制权,从而在模型开始过拟合、训练停止学习,或者符合其他定义的标准时,提前停止训练。早停可以在训练的不同阶段调用,可以在训练开始时、结束时或基于达到特定度量时应用。
使用内置回调实现早停
让我们一起探索 TensorFlow 中内置的早停回调:
-
我们将从 TensorFlow 导入早停功能:
from tensorflow.keras.callbacks import EarlyStopping -
接下来,我们初始化早停。TensorFlow 允许我们传入一些参数,我们利用这些参数来创建一个
callbacks对象:callbacks = EarlyStopping(monitor='val_loss',patience=5, verbose=1, restore_best_weights=True)
让我们解读一下我们在早停函数中使用的一些参数:
-
monitor可以用来跟踪我们想要关注的指标;在我们的情况下,我们希望跟踪验证损失。我们也可以切换为跟踪验证准确率。建议在验证集上监控实验,因此我们将callbacks设置为监控验证损失。 -
patience参数设置为5。这意味着,如果在五个周期后验证损失没有任何进展,训练将结束。 -
我们添加了
restore_best_weight参数并将其设置为True。这使得回调可以监控整个过程,并恢复训练过程中找到的最佳训练周期的权重。如果我们将restore_best_weight设置为False,则使用最后一步训练的模型权重。 -
当我们将
verbose设置为1时,这确保了我们在回调操作发生时得到通知。如果我们将verbose设置为0,训练将停止,但我们不会收到任何输出消息。
这里还有一些其他参数可以使用,但这些参数在应用早停时对许多情况来说已经足够有效。
-
我们将继续采用我们的三步法:构建、编译和拟合模型:
#Step 1: Model configurationmodel=keras.Sequential([keras.layers.Flatten(input_shape=(28,28)),keras.layers.Dense(100, activation=»relu»),keras.layers.Dense(10,activation=»softmax»)])#Step 2: Compiling the model, we add the loss, optimizer and evaluation metrics heremodel.compile(optimizer='adam',loss=›sparse_categorical_crossentropy›,metrics=[‹accuracy›])#Step 3: We fit our data to the modelhistory= model.fit(train_images, train_labels,epochs=100, callbacks=[callbacks],validation_split=0.2)
第 1 步和第 2 步是我们之前实现的相同步骤。在构建模型时,我们进行了更长时间的训练周期。然而,在第 3 步中,我们做了一些调整,以适应我们的验证集拆分和回调。我们将 20%的训练数据用于验证,并将callbacks对象传递给model.fit()。这确保了我们的早停回调在验证损失停止下降时中断训练。输出如下:
Epoch 14/100
1500/1500 [==============================] - 4s 2ms/step - loss: 0.2197 - accuracy: 0.9172 - val_loss: 0.3194 - val_accuracy: 0.8903
Epoch 15/100
1500/1500 [==============================] - 4s 2ms/step - loss: 0.2133 - accuracy: 0.9204 - val_loss: 0.3301 - val_accuracy: 0.8860
Epoch 16/100
1500/1500 [==============================] - 4s 2ms/step - loss: 0.2064 - accuracy: 0.9225 - val_loss: 0.3267 - val_accuracy: 0.8895
Epoch 17/100
1500/1500 [==============================] - 3s 2ms/step - loss: 0.2018 - accuracy: 0.9246 - val_loss: 0.3475 - val_accuracy: 0.8844
Epoch 18/100
1500/1500 [==============================] - 4s 2ms/step - loss: 0.1959 - accuracy: 0.9273 - val_loss: 0.3203 - val_accuracy: 0.8913
Epoch 19/100
1484/1500 [============================>.] - ETA: 0s - loss: 0.1925 - accuracy: 0.9282 Restoring model weights from the end of the best epoch: 14.
1500/1500 [==============================] - 4s 2ms/step - loss: 0.1928 - accuracy: 0.9281 - val_loss: 0.3347 - val_accuracy: 0.8912
Epoch 19: early stopping
因为我们将verbose设置为1,所以可以看到我们的实验在第 19 个 epoch 结束。现在,与其担心我们需要多少个 epoch 才能有效训练,我们可以简单地选择一个较大的 epoch 数并实现早停。接下来,我们还可以看到,因为我们实现了restore_best_weights,最佳权重出现在第 14 个 epoch,此时我们记录了最低的验证损失(0.3194)。通过早停,我们节省了计算时间,并采取了具体措施防止过拟合。
-
让我们看看我们的测试准确率如何:
test_loss, test_acc = model.evaluate(test_images,test_labels)print('Test Accuracy: ', test_acc)
在这里,我们达到了0.8847的测试准确率。
现在,让我们看看如何编写自定义回调来实现早停。
使用自定义回调实现早停
我们可以通过编写自己的自定义回调来扩展回调的功能,从而实现早停。这为回调增加了灵活性,使我们能够在训练过程中实现一些期望的逻辑。TensorFlow 文档提供了几种实现方法。让我们实现一个简单的回调来跟踪我们的验证准确率:
class EarlyStop(tf.keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs={}):
if(logs.get('val_accuracy') > 0.85):
print("\n\n85% validation accuracy has been reached.")
self.model.stop_training = True
callback = EarlyStop()
例如,如果我们希望在模型在验证集上超过 85%的准确率时停止训练,我们可以通过编写自己的自定义回调EarlyStop来实现,该回调接受tf.keras.callbacks.Callback参数。然后我们定义一个名为on_epoch_end的函数,该函数返回每个 epoch 的日志。我们设置self.model.stop_training = True,一旦准确率超过 85%,训练结束并显示类似于我们在使用内置回调时将verbose设置为1时所得到的消息。现在,我们可以像使用内置回调一样,将callback传入model.fit()中。然后我们使用我们三步法训练模型:
Epoch 1/100
1490/1500 [============================>.] - ETA: 0s - loss: 0.5325 - accuracy: 0.8134/n/n 85% validation accuracy has been reached
1500/1500 [==============================] - 4s 3ms/step - loss: 0.5318 - accuracy: 0.8138 - val_loss: 0.4190 - val_accuracy: 0.8538
这一次,在第一个 epoch 结束时,我们的验证准确率已经超过了 85%。再次强调,这是通过最小化计算资源的使用来实现期望指标的智能方法。
现在我们已经掌握了如何选择 epoch 并应用早停,让我们把目光转向其他超参数,看看通过调整一个或多个超参数,我们是否能提高 88%的测试准确率。也许我们可以从尝试一个更复杂的模型开始。
让我们看看如果我们向隐藏层添加更多神经元会发生什么。
增加隐藏层的神经元
隐藏层负责神经网络中的重负载,就像我们在讨论神经网络的结构时提到的那样,参见第五章,《使用神经网络进行图像分类》。让我们尝试不同数量的隐藏层神经元。我们将定义一个名为train_model的函数,允许我们尝试不同数量的神经元。train_model函数接受一个名为hidden_neurons的参数,表示模型中隐藏层神经元的数量。此外,该函数还接受训练图像、标签、回调、验证分割和 epoch 数。该函数使用这些参数构建、编译并拟合模型:
def train_model(hidden_neurons, train_images, train_labels, callbacks=None, validation_split=0.2, epochs=100):
model = keras.Sequential([
keras.layers.Flatten(input_shape=(28, 28)),
keras.layers.Dense(hidden_neurons, activation=»relu»),
keras.layers.Dense(10, activation=»softmax»)
])
model.compile(optimizer=›adam›,
loss=›sparse_categorical_crossentropy›,
metrics=[‹accuracy›])
history = model.fit(train_images, train_labels,
epochs=epochs, callbacks=[callbacks] if callbacks else None,
validation_split=validation_split)
return model, history
为了尝试一组神经元,我们创建了一个for循环来遍历名为neuron_values的神经元列表。然后它应用train_model函数为列表中每个神经元构建并训练一个模型:
neuron_values = [1, 500]
for neuron in neuron_values:
model, history = train_model(neurons, train_images,
train_labels, callbacks=callbacks)
print(f»Trained model with {neurons} neurons in the hidden layer»)
print语句返回一条消息,指示模型已分别使用 1 个和 500 个神经元进行了训练。让我们检查运行该函数时的结果,从只有一个神经元的隐藏层开始:
Epoch 36/40
1500/1500 [==============================] - 3s 2ms/step - loss: 1.2382 - accuracy: 0.4581 - val_loss: 1.2705 - val_accuracy: 0.4419
Epoch 37/40
1500/1500 [==============================] - 2s 1ms/step - loss: 1.2360 - accuracy: 0.4578 - val_loss: 1.2562 - val_accuracy: 0.4564
Epoch 38/40
1500/1500 [==============================] - 2s 1ms/step - loss: 1.2340 - accuracy: 0.4559 - val_loss: 1.2531 - val_accuracy: 0.4507
Epoch 39/40
1500/1500 [==============================] - 2s 1ms/step - loss: 1.2317 - accuracy: 0.4552 - val_loss: 1.2553 - val_accuracy: 0.4371
Epoch 40/40
1500/1500 [==============================] - 2s 1ms/step - loss: 1.2292 - accuracy: 0.4552 - val_loss: 1.2523 - val_accuracy: 0.4401
end of experiment with 1 neuron
从我们的结果来看,隐藏层只有一个神经元的模型不足够复杂,无法识别数据中的模式。这个模型的表现远低于 50%,这是典型的欠拟合案例。接下来,让我们看看使用 500 个神经元的模型结果:
Epoch 11/40
1500/1500 [==============================] - 6s 4ms/step - loss: 0.2141 - accuracy: 0.9186 - val_loss: 0.3278 - val_accuracy: 0.8878
Epoch 12/40
1500/1500 [==============================] - 6s 4ms/step - loss: 0.2057 - accuracy: 0.9220 - val_loss: 0.3169 - val_accuracy: 0.8913
Epoch 13/40
1500/1500 [==============================] - 6s 4ms/step - loss: 0.1976 - accuracy: 0.9258 - val_loss: 0.3355 - val_accuracy: 0.8860
Epoch 14/40
1500/1500 [==============================] - 6s 4ms/step - loss: 0.1893 - accuracy: 0.9288 - val_loss: 0.3216 - val_accuracy: 0.8909
Epoch 15/40
1499/1500 [============================>.] - ETA: 0s - loss: 0.1825 - accuracy: 0.9303Restoring model weights from the end of the best epoch: 10.
1500/1500 [==============================] - 6s 4ms/step - loss: 0.1826 - accuracy: 0.9303 - val_loss: 0.3408 - val_accuracy: 0.8838
Epoch 15: early stopping
end of experiment with 500 neurons
我们可以看到模型在使用更多神经元时出现了过拟合。模型在训练集上的准确率为0.9303,但在测试集上的准确率为0.8838。通常来说,更大的隐藏层能够学习更复杂的模式;然而,它会需要更多的计算资源,并且更容易发生过拟合。在选择隐藏层神经元数量时,考虑训练数据的大小非常重要。如果我们有大量的训练样本,我们可以选择更大的神经元数量。但当训练样本较小时,可能需要考虑使用较少的神经元。正如我们在实验中看到的,更多的神经元可能导致过拟合,而这种架构的表现可能会比拥有较少神经元的模型还要差。
另一个需要考虑的因素是我们所使用的数据类型。当我们处理线性数据时,少量的隐藏层可能就足够了。然而,对于非线性数据,我们需要更复杂的模型来学习数据中的复杂性。最后,我们还必须牢记,拥有更多神经元的模型需要更长的训练时间。重要的是要考虑性能和泛化能力之间的权衡。通常的做法是,从少量神经元开始训练模型。这样训练速度更快,并能避免过拟合。
或者,我们可以通过识别对网络性能影响较小或没有影响的神经元,来优化隐藏层中的神经元数量。这种方法称为剪枝。这超出了考试的范围,所以我们到此为止。
让我们看看向基准架构中添加更多层的影响。到目前为止,我们已经考虑过使模型更加复杂并训练更长时间。那我们试试改变优化器呢?让我们稍微变换一下,看看会发生什么。
更改优化器
我们使用Adam 优化器作为默认优化器;然而,还有其他一些知名的优化器,它们各有优缺点。在本书中,以及为了你的考试,我们将重点讲解 Adam、随机梯度下降(SGD)和均方根传播(RMSprop)。RMSprop 具有较低的内存需求,并提供自适应学习率;但与 Adam 和 SGD 相比,它的收敛时间要长得多。RMSprop 在训练非常深的网络时表现良好,比如递归神经网络(RNN),这将在本书后面讨论。
另一方面,SGD 是另一种流行的优化器;它简单易实现,并且当数据稀疏时效率较高。然而,它收敛较慢,并且需要仔细调整学习率。如果学习率过高,SGD 会发散;如果学习率过低,SGD 会收敛得非常慢。SGD 在各种问题上表现良好,并且在大数据集上比其他优化器收敛得更快,但在训练非常大的神经网络时,有时会收敛得较慢。
Adam 是 SGD 的改进版;它具有较低的内存需求,提供自适应学习率,是一种非常高效的优化器,并且可以比 SGD 或 RMSprop 在更少的迭代次数下收敛到一个良好的解。Adam 也非常适合训练大型神经网络。
让我们试试这三种优化器,看看哪一种在我们的数据集上效果最好。我们已经将优化器从 Adam 更改为 RMSprop 和 SGD,并使用相同的架构和内建回调。我们可以在图 6.2中看到结果:
| Adam | RMSProp | SGD | |
|---|---|---|---|
| 早停前的训练轮数 | 13 | 9 | 39 |
| 验证准确率 | 0.8867 | 0.8788 | 0.8836 |
| 测试准确率 | 0.8787 | 0.8749 | 0.8749 |
图 6.2 – 不同优化器的性能
尽管 Adam 需要更多的训练轮次,但其结果略微优于其他优化器。当然,任何一种优化器都可以用于此问题。在后续章节中,我们将处理更复杂的真实世界图像,并会再次讨论这些优化器。
在我们结束本章之前,让我们来看一下学习率及其对模型性能的影响。
更改学习率
学习率是一个重要的超参数,它控制着我们的模型在训练过程中学习和改进的效果。一个合适的学习率将确保模型快速且准确地收敛,而一个选择不当的学习率则可能导致各种问题,如收敛缓慢、欠拟合、过拟合或网络不稳定。
要理解学习率的影响,我们需要了解它如何影响模型的训练过程。学习率是达到损失函数最小值所采取的步长。在 图 6**.3(a) 中,我们看到选择较低学习率时,模型需要太多步骤才能达到最小点。另一方面,当学习率过高时,模型可能会学习得太快,采取较大步长,并可能超过最小点,就像 图 6**.3(c) 中所示。高学习率可能导致不稳定性和过拟合。然而,当我们像 图 6**.3(b) 中找到理想学习率时,模型很可能会快速收敛并具有良好的泛化能力:

图 6.3 – 展示低、理想和高学习率的绘图
提出的问题是:我们如何找到最佳的学习率?一种方法是尝试不同的学习率,并根据在验证集上评估模型的表现来确定有效的学习率。另一种方法是使用学习率调度器。这允许我们在训练过程中动态调整学习率。我们将在本书的后面章节探讨这种方法。在这里,让我们尝试几个不同的学习率,看看它们对我们的网络的影响。
让我们编写一个函数,该函数将接受一组不同的学习率。在这个实验中,我们将尝试六种不同的学习率(1、0.1、0.01、0.001、0.0001、0.00001 和 0.000001)。首先,让我们创建一个函数来创建我们的模型:
def learning_rate_test(learning_rate):
#Step 1: Model configuration
model=keras.Sequential([
keras.layers.Flatten(input_shape=(28,28)),
keras.layers.Dense(64, activation=»relu»),
keras.layers.Dense(10,activation=»softmax»)
])
#Step 2: Compiling the model, we add the loss,
#optimizer and evaluation metrics here
model.compile(optimizer=tf.keras.optimizers.Adam(
learning_rate=learning_rate),
loss='sparse_categorical_crossentropy',
metrics=[‹accuracy›])
#Step 3: We fit our data to the model
callbacks = EarlyStopping(monitor='val_loss',
patience=5, verbose=1, restore_best_weights=True)
history=model.fit(train_images, train_labels,
epochs=50, validation_split=0.2,
callbacks=[callbacks])
score=model.evaluate(test_images, test_labels)
return score[1]
我们将使用该函数来构建、编译和拟合模型。它还将学习率作为一个变量传递到我们的函数中,并将测试准确率作为结果返回:
# Try out different learning rates
learning_rates = [1, 0.1, 0.01, 0.001, 0.0001, 0.00001,
0.000001]
# Create an empty list to store the accuracies
accuracies = []
# Loop through the different learning rates
for learning_rate in learning_rates:
# Get the accuracy for the current learning rate
accuracy = learning_rate_test(learning_rate)
# Append the accuracy to the list
accuracies.append(accuracy)
我们现在已经概述了不同的学习率。在这里,我们想要尝试不同的学习率,从非常高到非常低的学习率。我们创建了一个空列表,并附加了我们的测试集准确率。接下来,让我们以表格形式查看数值。我们使用 pandas 生成一个包含学习率和准确率的 DataFrame:
df = pd.DataFrame(list(zip(learning_rates, accuracies)),
columns =[‹Learning_rates›, ‹Test_Accuracy›])
df
下面是输出的 DataFrame 截图:

图 6.4 – 不同学习率及其测试准确率
从结果中我们可以看到,当使用非常高的学习率(1.0)时,模型表现很差。随着学习率值的降低,我们看到模型的准确性开始提高;当学习率变得太小时,模型收敛所需时间太长。在选择问题的理想学习率时,并没有银弹。这取决于诸多因素,如模型架构、数据以及应用的优化技术类型。
现在我们已经看到了各种调整模型以提高其性能的方法,本章也已经结束。我们尝试了调整不同的超参数来改善模型的性能;然而,我们的测试准确率停滞在 88%。或许现在是尝试其他方法的好时机,接下来我们将在下一章中进行尝试。休息一下,当你准备好时,让我们看看如何改善这个结果,并尝试使用真实世界的图像。
总结
在本章中,我们讨论了如何提高神经网络的性能。尽管我们使用的是一个轻量级的数据集,但我们已经学到了关于提高模型性能的一些重要概念——这些概念在考试和工作中都会派上用场。你现在知道,数据质量和模型复杂性是机器学习中的两个方面。如果你有高质量的数据,糟糕的模型也会产生不理想的结果;反之,即使是最先进的模型,如果数据不好,也会产生次优的结果。
到目前为止,你应该对微调神经网络有了深入的理解和实践经验。像一个经验丰富的专家一样,你应该能够理解微调超参数的艺术,并将其应用到不同的机器学习问题中,而不仅仅是图像分类。此外,你已经看到,构建模型需要大量实验。没有银弹,但了解各个环节和各种技巧,以及如何和为什么应用它们,这正是明星与普通人之间的区别。
在下一章中,我们将探讨卷积神经网络。我们将看到它们在图像分类任务中为何处于最先进的水平。我们将了解卷积的强大功能,并通过动手操作,深入了解它们与我们迄今为止使用的简单神经网络有何不同。
问题
让我们使用 CIFAR-10 笔记本测试我们在本章中学到的内容:
-
使用我们三步法构建神经网络。
-
将隐藏层中的神经元数量从 5 增加到 100。
-
使用自定义回调函数,当训练准确率达到 90%时停止训练。
-
尝试以下学习率:5、0.5、0.01、0.001。你观察到了什么?
进一步阅读
若要了解更多信息,可以查看以下资源:
-
Amr, T., 2020. 动手实践机器学习:使用 scikit-learn 和科学 Python 工具包,Packt 出版。
-
Gulli, A., Kapoor, A. 和 Pal, S., 2019. 使用 TensorFlow 2 和 Keras 的深度学习,Packt 出版。
-
如何编写自定义 TensorFlow 回调函数——简易方法:
towardsdatascience.com/how-to-write-custom-tensorflow-callbacks-the-easy-way-c7c4b0e31c1c -
medium.com/geekculture/introduction-to-neural-network-2f8b8221fbd3 -
www.tensorflow.org/api_docs/python/tf/keras/callbacks/EarlyStopping
第七章:卷积神经网络进行图像分类
卷积神经网络(CNNs)是进行图像分类时的首选算法。在 1960 年代,神经科学家 Hubel 和 Wiesel 对猫和猴子的视觉皮层进行了研究。他们的工作揭示了我们如何以层次结构处理视觉信息,展示了视觉系统是如何组织成一系列层次的,每一层都负责视觉处理的不同方面。这一发现为他们赢得了诺贝尔奖,但更重要的是,它为 CNN 的构建奠定了基础。CNN 本质上非常适合处理具有空间结构的数据,例如图像。
然而,在早期,由于多种因素的影响,例如训练数据不足、网络架构不成熟、计算资源匮乏,以及缺乏现代技术(如数据增强和丢弃法),CNN 未能获得广泛关注。在 2012 年 ImageNet 大规模视觉识别挑战赛中,一种名为 AlexNet 的 CNN 架构震惊了机器学习(ML)社区,它比其他所有方法都超出了一个较大的优势。今天,机器学习从业者通过应用 CNN,在计算机视觉任务(如图像分类、图像分割和目标检测等)中取得了最先进的表现。
在本章中,我们将研究 CNN,看看它们与我们迄今为止使用的全连接神经网络有什么不同。我们将从全连接网络在处理图像数据时面临的挑战开始,接着探索 CNN 的结构。我们将研究 CNN 架构的核心构建模块及其对网络性能的整体影响。接下来,我们将使用 Fashion MNIST 数据集构建一个图像分类器,然后开始构建一个真实世界的图像分类器。我们将处理不同大小的彩色图像,且图像中的目标物体位置不同。
本章结束时,您将对卷积神经网络(CNN)有一个清晰的理解,并了解为什么在图像分类任务中,它们比全连接网络更具优势。此外,您还将能够在真实世界的图像分类问题中,构建、训练、调整和测试 CNN 模型。
本章我们将讨论以下主题:
-
CNN 的结构解析
-
使用 CNN 进行 Fashion MNIST 分类
-
真实世界的图像
-
天气数据分类
-
应用超参数以提高模型的性能
-
评估图像分类器
使用全连接网络进行图像识别的挑战
在第五章《使用神经网络进行图像分类》中,我们将深度神经网络(DNN)应用于时尚 MNIST 数据集。我们看到输入层中的每个神经元都与隐藏层中的每个神经元相连,而隐藏层中的神经元又与输出层中的神经元相连,因此称为全连接。虽然这种架构可以解决许多机器学习问题,但由于图像数据的空间特性,它并不适合用于图像分类任务。假设你正在看一张人脸的照片;人脸特征的位置和朝向使得即便你只专注于某个特定特征(例如眼睛),你也能知道那是一张人脸。你本能地通过人脸各个特征之间的空间关系知道它是一张人脸;然而,DNN 在查看图像时无法看到这种全貌。它们将图像中的每个像素处理为独立的特征,而没有考虑这些特征之间的空间关系。
使用全连接架构的另一个问题是维度灾难。假设我们正在处理一张尺寸为 150 x 150、具有 3 个颜色通道的真实世界图像,红色、绿色和蓝色(RGB);我们将有一个 67,500 的输入大小。由于所有神经元都与下一层的神经元相连,如果我们将这些值输入到一个拥有 500 个神经元的隐藏层中,那么参数数量将为 67,500 x 500 = 33,750,000,并且随着我们增加更多层,这个参数数量将呈指数级增长,使得将这种网络应用于图像分类任务变得资源密集。我们可能还会遇到的另一个问题是过拟合;这是由于网络中大量参数的存在。如果我们处理的是更大尺寸的图像,或者我们为网络增加更多神经元,训练的可训练参数数量将呈指数级增长,而训练这样一个网络可能变得不切实际,因为成本和资源需求过高。考虑到这些挑战,迫切需要一种更为复杂的架构,这就是 CNN 的优势所在,它能够揭示空间关系和层次结构,确保无论特征位于图像的哪个位置,都能被识别出来。
注意
空间关系指的是图像中各个特征在位置、距离和朝向上的相对排列方式。
CNN 的结构
在上一节中,我们看到 DNN 在处理视觉识别任务时面临的一些挑战。这些问题包括缺乏空间感知、高维性、计算低效和过拟合的风险。我们如何克服这些挑战呢?这就是 CNN 登场的地方。CNN 天生就特别适合处理图像数据。让我们通过图 7.1来了解 CNN 为何及如何脱颖而出:

图 7.1 – CNN 的结构
让我们来分解图中的不同层:
- 卷积层 – 网络的眼睛:我们的旅程从将图像输入卷积层开始;这个层可以看作是我们网络的“眼睛”。它们的主要工作是提取重要特征。与 DNN(深度神经网络)不同,DNN 中的每个神经元都与下一层的每个神经元相连,而 CNN 通过应用滤波器(也叫做卷积核)以分层的方式捕捉图像中的局部模式。滤波器滑过输入图像的一段区域后,所产生的输出称为特征图。如图 7**.2所示,我们可以看到每个特征图突出显示了我们输入到网络中的衬衫特定图案。图像通过 CNN 按层次结构处理,早期层的滤波器擅长捕捉简单的特征,而后续层的滤波器则捕捉更复杂的模式,模仿人类视觉皮层的层级结构。CNN 的另一个重要特点是参数共享——这是因为模式只需要学习一次,然后应用到图像的其他地方。这确保了模型的视觉能力不依赖于特定位置。在机器学习中,我们称这个概念为平移不变性——网络能够检测衬衫,无论它是对齐在图像的左边、右边还是居中。

图 7.2 – 卷积层捕捉的特征可视化
- 池化层 – 总结器:卷积层之后是池化层。这个层可以看作是 CNN 中的总结器,它专注于压缩特征图的整体维度,同时保留重要特征,如图 7**.3所示。通过系统地对特征图进行下采样,CNN 不仅显著减少了图像处理所需的参数数量,而且提高了 CNN 的整体计算效率。

图 7.3 – 池化操作示例,保留重要细节
- 全连接层 – 决策者:我们的图像通过一系列卷积和池化层,这些层提取特征并减少特征图的维度,最终到达全连接层。这个层可以看作是决策者。这个层提供了高级推理,它将通过各层收集的所有重要细节整合在一起,用来做出最终的分类判断。CNN 的一个显著特点是其端到端的学习过程,它无缝地整合了特征提取和图像分类。这种有条理且分层的学习方法使得 CNN 成为图像识别和分析的理想工具。
我们仅仅触及了卷积神经网络(CNN)如何工作的表面。现在,让我们深入探讨不同层内发生的关键操作,从卷积开始。
卷积
我们现在知道,卷积层应用过滤器,这些过滤器会滑过输入图像的各个区域。典型的 CNN 应用多个过滤器,每个过滤器通过与输入图像的交互来学习特定类型的特征。通过组合检测到的特征,CNN 能够全面理解图像特征,并利用这些详细信息来对输入图像进行分类。从数学上讲,这一卷积过程涉及输入图像的一块区域与过滤器(一个小矩阵)之间的点积运算,如图 7.4所示。这个过程生成了一个输出,称为激活图或特征图。

图 7.4 – 卷积操作 – 应用过滤器到输入图像生成特征图
当过滤器滑过图像的各个区域时,它为每个点操作生成一个特征图。特征图是输入图像的一个表示,其中某些视觉模式通过过滤器得到增强,如图 7.2所示。当我们将网络中所有过滤器的特征图叠加在一起时,我们就能得到输入图像的丰富多维视图,为后续层提供足够的信息来学习更复杂的模式。

图 7.5 – a(上)和 b(下):点积计算
在图 7.5 a中,我们看到一个点积操作正在进行中,过滤器滑过输入图像的一个区域,得到一个目标像素值 13。如果我们将过滤器向右移动 1 个像素,如图 7.5 b所示,我们将得到下一个目标像素值 14。如果我们继续每次移动 1 个像素,滑过输入图像,就会得到图 7.5 b中显示的完整输出。
我们现在已经了解了卷积操作的工作原理;然而,我们可以在 CNN 中应用多种类型的卷积层。对于图像分类,我们通常使用 2D 卷积层,而对于音频处理则应用 1D 卷积层,视频处理则使用 3D 卷积层。在设计卷积层时,有许多可调的超参数会影响网络的性能,比如滤波器的数量、滤波器的大小、步幅和填充。探讨这些超参数如何影响我们的网络是非常重要的。
让我们通过观察卷积层中过滤器数量的影响,来开始这次探索。
滤波器数量的影响
通过增加卷积神经网络(CNN)中滤波器的数量,我们可以使其学习到输入图像更丰富、更多样化的表示。滤波器越多,学习到的表示越多。然而,更多的滤波器意味着更多的参数需要训练,这不仅会增加计算成本,还可能会减慢训练过程并增加过拟合的风险。在决定为网络应用多少滤波器时,重要的是要考虑所使用数据的类型。如果数据具有较大的变异性,可能需要更多的滤波器来捕捉数据的多样性;而对于较小的数据集,则应该更为保守,以减少过拟合的风险。
滤波器大小的影响
我们现在知道,滤波器是滑过输入图像以生成特征图的小矩阵。我们应用于输入图像的滤波器的大小将决定从输入图像中提取的特征的层次和类型。滤波器大小是指滤波器的维度——即滤波器矩阵的高度和宽度。通常,你会遇到 3x3、5x5 和 7x7 滤波器。较小的滤波器会覆盖输入图像的较小区域,而较大的滤波器则会覆盖输入图像的更广泛部分:
-
特征的粒度 – 像 3x3 滤波器这样的较小滤波器可以用于捕捉图像中更细致、更局部的细节,如边缘、纹理和角落,而像 7x7 滤波器这样的较大滤波器则可以学习更广泛的模式,如面部形状或物体部件。
-
计算效率 – 较小的滤波器覆盖输入图像较小的感受野,如图 7**.6所示,这意味着它们需要更多的计算操作。

图 7.6 – 使用 3x3 滤波器的卷积操作
另一方面,较大的滤波器覆盖了输入图像的较大部分,如图 7**.7所示。然而,许多现代卷积神经网络(例如,VGG)使用的是 3x3 滤波器。将这些较小的滤波器叠加在一起会增加网络的深度,并增强这些滤波器捕捉更复杂模式的能力,同时相比于使用大滤波器,所需的参数更少,这使得较小的滤波器更容易训练。

图 7.7 – 使用 5x5 滤波器的卷积操作
- 参数数量 – 较大的滤波器通常比较小的滤波器拥有更多的权重;例如,5x5 滤波器会有 25 个参数,而 3x3 滤波器会有 9 个参数。这里为了简便起见,我们忽略了深度。因此,较大的滤波器相对于较小的滤波器,会使模型变得更加复杂。
步幅的影响
步幅是卷积神经网络(CNN)中的一个重要超参数。它决定了滤波器在输入图像上移动的像素数。我们可以将步幅类比为我们走路时的步伐;如果步伐小,达到目的地会花费更长时间,而较大的步伐则可以更快到达目的地。在图 7.8中,我们应用了步幅为 1,这意味着滤波器每次在输入图像上移动 1 个像素。

图 7.8 – 步幅为 1 的卷积操作
如果我们应用步幅为 2,意味着滤波器每次移动 2 个像素,如图 7.9所示。我们看到,较大的步幅会导致输出特征图的空间维度减小。通过比较这两幅图的输出,我们可以看到这一点。

图 7.9 – 步幅为 2 的卷积操作
当我们应用较大的步幅时,它可以提高计算效率,但也会降低输入图像的空间分辨率。因此,在为我们的网络选择合适的步幅时,需要考虑这种权衡。接下来,我们来看看边界效应。
边界问题
当滤波器在输入图像上滑动并执行卷积操作时,很快就会到达边界或边缘,此时由于图像边界外缺少像素,难以执行点积操作。由于边缘或边界信息的丢失,这导致输出特征图小于输入图像。这个问题在机器学习中被称为边效应或边界问题。在图 7.10中,我们可以看到,由于滤波器的一部分会超出定义的图像边界,我们无法将滤波器集中在突出显示的像素值 3 上,因此无法在左下角执行点积操作。

图 7.10 – 显示边界问题
为了解决边界问题并保持输出特征图的空间维度,我们可能需要应用填充。接下来我们讨论这个概念。
填充的影响
填充是一种可以应用于卷积过程中的技术,通过向边缘添加额外的像素来防止边界效应,如图 7.11所示。

图 7.11 – 进行卷积操作的填充图像
现在,我们可以在边缘的像素上执行点积操作,从而保留边缘的信息。填充也可以应用于保持卷积前后的空间维度。这在具有多个卷积层的深度卷积神经网络(CNN)架构中可能会非常有用。我们来看一下两种主要的填充类型:
-
有效填充(无填充):这里没有应用填充。当我们希望减少空间维度,尤其是在较深的层时,这种方法非常有用。
-
相同填充:这里我们设置填充以确保输出特征图和输入图像的尺寸相同。我们在保持空间维度至关重要时使用这种方法。
在我们继续检查池化层之前,让我们将之前讨论过的卷积层的不同超参数组合起来,并看看它们的实际效果。
综合起来
在图 7.12中,我们有一个 7x7 的输入图像和一个 3x3 的滤波器。在这里,我们使用步幅为 1 并将填充设置为有效(无填充)。

图 7.12 – 设置超参数
为了计算卷积操作的输出特征图,我们可以应用以下公式:
(W − F + 2P _ S) + 1
在这个公式中,以下内容适用:
-
W 代表输入图像的大小
-
F 代表滤波器大小
-
S 代表步幅
-
P 代表填充
当我们将相应的值代入公式时,得到的结果值为 5,这意味着我们将得到一个 5x5 的输出特征图。如果我们更改任何一个值,它都会以某种方式影响输出特征图的大小。例如,如果我们增加步幅大小,输出特征图将变得更小,而如果我们将填充设置为 same,则会增加输出的大小。现在,我们可以从卷积操作转到池化操作。
池化
池化是一个重要的操作,发生在卷积神经网络(CNN)的池化层中。它是一种用于下采样卷积层生成的单个特征图空间维度的技术。让我们来看看一些常见的池化层类型。我们将从最大池化开始,如图 7.13所示。在这里,我们可以看到最大池化操作是如何工作的。池化层简单地从输入数据的每个区域提取最大值。

图 7.13 – 最大池化操作
最大池化具有多个优点,因为它直观且易于实现。它也很高效,因为它只提取区域中的最大值,并且在各类任务中都取得了良好的效果。
平均池化,顾名思义,通过对指定区域取平均值来减少数据的维度,如图 7.14所示。

图 7.14 – 平均池化操作
另一方面,最小池化会提取输入数据指定区域中的最小值。池化减少了输出特征图的空间大小,从而减少了存储中间表示所需的内存。池化对网络是有益的;然而,过度池化可能适得其反,因为这可能导致信息丢失。经过池化层后,我们到达了全连接层,这是我们网络的决策者。
全连接层
我们的 CNN 架构的最后一个组成部分是全连接层。与卷积层不同,在这里,每个神经元都与下一层中的每个神经元相连接。这个层负责决策,例如分类我们的输入图像是衬衫还是帽子。全连接层将从早期层学到的特征映射到相应的标签。现在我们已经在理论上覆盖了 CNN,接下来我们将其应用到我们的时尚数据集上。
Fashion MNIST 2.0
到现在为止,你已经熟悉了这个数据集,因为我们在第五章,《神经网络图像分类》,和第六章,《改进模型》中使用了它。现在,让我们看看 CNN 与我们迄今为止使用的简单神经网络相比如何。我们将继续保持之前的精神,首先导入所需的库:
-
我们将导入所需的库以进行预处理、建模和使用 TensorFlow 可视化我们的机器学习模型:
import tensorflow as tfimport numpy as npimport matplotlib.pyplot as plt -
接下来,我们将使用
load_data()函数从 TensorFlow 数据集中加载 Fashion MNIST 数据集。此函数返回我们的训练和测试数据,这些数据由 NumPy 数组组成。训练数据包括x_train和y_train,测试数据由x_test和y_test组成:(x_train,y_train),(x_test,y_test) = tf.keras.datasets.fashion_mnist.load_data() -
我们可以通过对训练数据和测试数据使用
len函数来确认数据的大小:len(x_train), len(x_test)
当我们运行代码时,我们得到以下输出:
(60000, 10000)
我们可以看到,我们的训练数据集有 60,000 张图像,测试数据集有 10,000 张图像。
-
在 CNN 中, 与我们之前使用的 DNN 不同,我们需要考虑输入图像的颜色通道。目前,我们的训练和测试数据是灰度图像,其形状为
(batch_size, height, width),并且只有一个通道。然而,CNN 模型需要一个 4D 输入张量,由batch_size、height、width和channels组成。我们可以通过简单地重塑数据并将元素转换为float32值来修复这个数据不匹配问题:# Reshape the images(batch_size, height, width, channels)x_train = x_train.reshape(x_train.shape[0],28, 28, 1).astype('float32')x_test = x_test.reshape(x_test.shape[0],28, 28, 1).astype('float32')
这个预处理步骤在训练机器学习模型之前是标准步骤,因为大多数模型需要浮动点输入。由于我们的图像是灰度图像,因此只有一个颜色通道,这就是我们将数据重塑为包含单一通道维度的原因。
-
我们数据的像素值(训练数据和测试数据)范围从
0到255,其中0代表黑色,255代表白色。我们通过将像素值除以 255 来规范化数据,从而将像素值缩放到0到1的区间。这样做的目的是让模型更快收敛,并提高其性能:# Normalize the pixel valuesx_train /= 255x_test /= 255 -
我们使用
tf.keras中utils模块的to_categorical函数,将标签(y_train和y_test)中的整数值(0到9)转换为一维独热编码数组。to_categorical函数接受两个参数:需要转换的标签和类别的数量;它返回一个一维独热编码数组,如 图 7.15 所示。

图 7.15 – 一维独热编码数组
一维独热编码向量的长度为 10,其中在对应标签的索引位置上为 1,其他位置则为 0:
# Convert the labels to one hot encoding format
y_train = tf.keras.utils.to_categorical(y_train, 10)
y_test = tf.keras.utils.to_categorical(y_test, 10)
-
使用
tf.keras.model中的顺序模型 API,我们将创建一个卷积神经网络(CNN)架构:# Build the Sequential modelmodel = tf.keras.models.Sequential()# Add convolutional layermodel.add(tf.keras.layers.Conv2D(64,kernel_size=(3,3),activation='relu',input_shape=(28, 28, 1)))# Add max pooling layermodel.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))# Flatten the datamodel.add(tf.keras.layers.Flatten())# Add fully connected layermodel.add(tf.keras.layers.Dense(128,activation='relu'))# Apply softmaxmodel.add(tf.keras.layers.Dense(10,activation='softmax'))
第一层是一个卷积层,包含 64 个 3x3 的滤波器,用来处理输入图像,输入图像的形状是 28x28 像素,1 个通道(灰度图)。ReLU 被用作激活函数。随后的最大池化层是一个 2D 池化层,应用最大池化对卷积层的输出进行降采样,减少特征图的维度。flatten 层将池化层的输出展平为一维数组,然后由全连接层处理。输出层使用 softmax 激活函数进行多类分类,并包含 10 个神经元,每个类一个神经元。
-
接下来,我们将在训练数据上编译并训练模型:
# Compile and fit the modelmodel.compile(loss='categorical_crossentropy',optimizer='adam', metrics=['accuracy'])model.fit(x_train, y_train, epochs=10,validation_split=0.2)
compile() 函数有三个参数:损失函数(categorical_crossentropy,因为这是一个多类分类任务)、优化器(adam)和度量标准(accuracy)。编译模型后,我们使用 fit() 函数在训练数据上训练模型。我们将迭代次数设定为 10,并使用 20% 的训练数据进行验证。
在 10 次迭代后,我们得到了训练准确率为 0.9785,验证准确率为 0.9133:
Epoch 6/10
1500/1500 [==============================] - 5s 3ms/step - loss: 0.1267 - accuracy: 0.9532 - val_loss: 0.2548 - val_accuracy: 0.9158
Epoch 7/10
1500/1500 [==============================] - 5s 4ms/step - loss: 0.1061 - accuracy: 0.9606 - val_loss: 0.2767 - val_accuracy: 0.9159
Epoch 8/10
1500/1500 [==============================] - 6s 4ms/step - loss: 0.0880 - accuracy: 0.9681 - val_loss: 0.2957 - val_accuracy: 0.9146
Epoch 9/10
1500/1500 [==============================] - 6s 4ms/step - loss: 0.0697 - accuracy: 0.9749 - val_loss: 0.3177 - val_accuracy: 0.9135
Epoch 10/10
1500/1500 [==============================] - 6s 4ms/step - loss: 0.0588 - accuracy: 0.9785 - val_loss: 0.3472 - val_accuracy: 0.9133
-
summary函数是一个非常有用的方式,用来快速概览模型架构,并理解每层的参数数量以及输出张量的形状:model.summary()
输出返回了组成我们当前模型架构的五个层。它还显示了每个层的输出形状和参数数量。总的参数数量是 1,386,506。通过输出,我们可以看到卷积层的输出形状是 26x26,这是由于边缘效应造成的,因为我们没有使用填充。接下来,最大池化层将像素大小减半,然后我们将数据展平并生成预测:
Model: "sequential"
______________________________________________________
Layer (type) Output Shape Param #
======================================================
conv2d (Conv2D) (None, 26, 26,64) 640
max_pooling2d (MaxPooling2D (None,13,13,64) 0 )
flatten (Flatten) (None, 10816) 0
dense (Dense) (None, 128) 1384576
dense_1 (Dense) (None, 10) 1290
======================================================
Total params: 1,386,506
Trainable params: 1,386,506
Non-trainable params: 0
_________________________________________________________________
-
最后,我们将使用
evaluate函数在测试数据上评估我们的模型。evaluate函数返回模型在测试数据上的损失和准确度:# Evaluate the modelscore = model.evaluate(x_test, y_test)
我们的模型在测试数据上实现了 0.9079 的准确率,超过了在 第六章**,改进模型 中使用的架构的性能。我们可以通过调整超参数和应用数据增强来进一步提高模型的性能。让我们把注意力转向现实世界的图像,CNNs 显然比我们以前的模型更出色。
处理现实世界图像
现实世界中的图像提出了不同类型的挑战,因为这些图像通常是彩色图像,具有三个色彩通道(红色、绿色和蓝色),不像我们从时尚 MNIST 数据集中使用的灰度图像。在 图 7**.16 中,我们看到了一些即将建模的来自天气数据集的实际图像示例,您会注意到这些图像的大小各异。这引入了另一层复杂性,需要额外的预处理步骤,如调整大小或裁剪,以确保我们所有的图像在输入神经网络之前具有统一的尺寸。

图 7.16 – 天气数据集中的图像
在处理现实世界图像时,我们可能会遇到的另一个问题是各种噪声源的存在。例如,我们的数据集中可能有在光线不均匀或意外模糊条件下拍摄的图像。同样,在我们的现实世界数据集中可能会有多个对象或其他意外的背景干扰图像。
要解决这些问题,我们可以应用去噪技术,如去噪,以改善数据的质量。我们还可以使用对象检测技术,如边界框或分割,帮助我们在具有多个对象的图像中识别目标对象。好消息是 TensorFlow 配备了一套完整的工具集,专门处理这些挑战。来自 TensorFlow 的一个重要工具是 tf.image 模块,提供了一系列图像预处理功能,如调整大小、亮度、对比度、色调和饱和度的应用、边界框、裁剪、翻转等等。
然而,这个模块超出了本书及考试的范围。但是,如果你希望深入了解这个模块,可以访问 TensorFlow 文档:www.tensorflow.org/api_docs/python/tf/image。TensorFlow 的另一个工具是ImageDataGenerator,它使我们能够实时执行数据增强操作,提供了在将图像输入训练管道时对其进行预处理和增广(例如旋转和翻转图像)的能力。接下来,我们将使用实际的图像数据集,看看ImageDataGenerator如何发挥作用。
天气数据集分类
在这个案例研究中,我们将作为计算机视觉顾问为一个新兴的初创公司 WeatherBIG 提供支持。你被分配了开发一个图像分类系统的任务,该系统将用于识别不同的天气状况;该任务的数据集可以通过以下链接在 Kaggle 上找到:www.kaggle.com/datasets/rahul29g/weatherdataset。该数据集已被分成三个文件夹,包括训练文件夹、验证文件夹和测试文件夹。每个文件夹下都有各自的天气类别子文件夹。让我们开始这个任务:
-
我们首先导入几个库来构建我们的图像分类器:
import osimport pathlibimport matplotlib.pyplot as pltimport matplotlib.image as mpimgimport randomimport numpy as npfrom PIL import Imageimport tensorflow as tffrom tensorflow import kerasfrom tensorflow.keras.preprocessing.image import ImageDataGenerator
我们在之前的实验中使用了这些库中的几个;然而,接下来我们将介绍一些第一次使用的库的功能。os模块作为我们操作系统的桥梁。它使我们能够读取和写入文件系统,而pathlib提供了一种直观的面向对象的方式来简化文件导航任务。对于图像操作,我们使用PIL,此外还有来自tensorflow.keras.preprocessing.image模块的ImageDataGenerator类,用于我们的数据预处理、批次生成和数据增强步骤。
-
你可以从
www.kaggle.com/datasets/rahul29g/weatherdataset获取/下载此案例研究的数据集,并将其上传到 Google Drive。完成后,你可以轻松地跟随本节中的代码进行操作。在我的例子中,数据存储在此根目录:/content/drive/MyDrive/weather dataset。在你的情况下,根目录将会不同,所以请确保将目录路径更改为与数据集存储在 Google Drive 中的目录匹配:root_dir = "/content/drive/MyDrive/weather dataset"。 -
接下来,我们应用
os.walk函数来访问根目录,并生成关于所有目录和子目录内容的信息:for dirpath, dirnames, filenames in os.walk(root_dir):print(f"Directory: {dirpath}")print(f"Number of images: {len(filenames)}")print()
运行代码会返回一个元组,包含每个目录的路径以及每个目录中图片的数量,如图 7.17所示:

图 7.17 – 快照目录及其子目录
我们通过这一步来了解每个目录和子目录的内容。
-
我们使用
retrieve_labels函数从训练、测试和验证目录中提取并显示标签及其对应的计数。为了实现这个函数,我们使用os模块中的listdir方法,并传入相应的目录路径(train_dir、test_dir和val_dir):def retrieve_labels(train_dir, test_dir, val_dir):# Retrieve labels from training directorytrain_labels = os.listdir(train_dir)print(f"Training labels: {train_labels}")print(f"Number of training labels: {len(train_labels)}")print()# Retrieve labels from test directorytest_labels = os.listdir(test_dir)print(f"Test labels: {test_labels}")print(f"Number of test labels: {len(test_labels)}")print()# Retrieve labels from validation directoryval_labels = os.listdir(val_dir)print(f"Validation labels: {val_labels}")print(f"Number of validation labels: {len(val_labels)}")print() -
我们分别在
train_dir、test_dir和val_dir参数中指定训练、测试和验证目录的路径:train_dir = "/content/drive/MyDrive/weather dataset/train"test_dir = "/content/drive/MyDrive/weather dataset/test"val_dir = "/content/drive/MyDrive/weather dataset/validation"retrieve_labels(train_dir, test_dir, val_dir)
当我们运行代码时,它将返回训练数据、测试数据、验证数据标签以及标签的数量:
Training labels: ['cloud', 'shine', 'rain', 'sunrise']
Number of training labels: 4
Test labels: ['sunrise', 'shine', 'cloud', 'rain']
Number of test labels: 4
Validation labels: ['shine', 'sunrise', 'cloud', 'rain']
Number of validation labels: 4
-
在我们的探索中,创建一个名为
view_random_images的函数,从数据集中的子目录中随机访问并显示图片。该函数接受包含子目录的主目录以及我们希望显示的图片数量。我们使用listdir来访问子目录,并引入随机性来选择图片。我们使用random库中的shuffle函数进行打乱并随机选择图片。我们利用 Matplotlib 来显示指定数量的随机图片:def view_random_images(target_dir, num_images):"""View num_images random images from the subdirectories of target_dir as a subplot."""# Get list of subdirectoriessubdirs = [d for d in os.listdir(target_dir) if os.path.isdir(os.path.join(target_dir, d))]# Select num_images random subdirectoriesrandom.shuffle(subdirs)selected_subdirs = subdirs[:num_images]# Create a subplotfig, axes = plt.subplots(1, num_images, figsize=(15,9))for i, subdir in enumerate(selected_subdirs):# Get list of images in subdirectoryimage_paths = [f for f in os.listdir(os.path.join(target_dir, subdir))]# Select a random imageimage_path = random.choice(image_paths)# Load imageimage = plt.imread(os.path.join(target_dir,subdir, image_path))# Display image in subplotaxes[i].imshow(image)axes[i].axis("off")axes[i].set_title(subdir)print(f"Shape of image: {image.shape}")#width,height, colour chDNNelsplt.show() -
让我们通过将
num_images设置为4来尝试这个函数,并查看train目录中的一些数据:view_random_images(target_dir="/content/drive/MyDrive/weather dataset/train/", num_images=4)
这将返回四张随机选择的图片,如下所示:

7.18 – 从天气数据集中随机选择的图片
从显示的数据来看,图片的尺寸(高度和宽度)各不相同,我们需要解决这个预处理问题。我们将使用 TensorFlow 中的ImageDataGenerator类。接下来我们将讨论这个问题。
图片数据预处理
我们在图 7.18中看到,训练图片的大小各不相同。在这里,我们将在训练前调整并规范化数据。此外,我们还希望开发一种有效的方法来批量加载训练数据,确保优化内存使用并与模型的训练过程无缝对接。为了实现这一目标,我们将使用TensorFlow.keras.preprocessing.image模块中的ImageDataGenerator类。在第八章《处理过拟合》中,我们将进一步应用ImageDataGenerator,通过旋转、翻转和缩放等方式生成训练数据的变种,扩大我们的训练数据集。这将有助于我们的模型变得更强大,并减少过拟合的风险。
另一个有助于我们数据预处理任务的有用工具是flow_from_directory方法。我们可以使用此方法构建数据管道。当我们处理大规模、实际数据时,它尤其有用,因为它能够自动读取、调整大小并将图像批量化,以便进行模型训练或推理。flow_from_directory方法接受三个主要参数。第一个是包含图像数据的目录路径。接下来,我们指定在将图像输入神经网络之前,图像的期望大小。然后,我们还需要指定批量大小,以确定我们希望同时处理的图像数量。我们还可以通过指定其他参数,如颜色模式、类别模式和是否打乱数据,来进一步定制该过程。现在,让我们来看一下一个多分类问题的典型目录结构,如图 7.19所示。

图 7.19 – 多分类问题的目录结构
在应用flow_from_directory方法时,重要的是我们需要将图像组织在一个结构良好的目录中,每个唯一的类别标签都有一个子目录,如图 7.19所示。在这里,我们有四个子目录,每个子目录对应我们的天气数据集中的一个类别标签。一旦所有图像都放入了适当的子目录,我们就可以应用flow_from_directory来设置一个迭代器。这个迭代器是可调的,我们可以定义图像大小、批量大小等参数,并决定是否打乱数据。接下来,我们将这些新想法应用到我们当前的案例研究中:
# Preprocess data (get all of the pixel values between 1 and 0, also called scaling/normalization)
train_datagen = ImageDataGenerator(rescale=1./255)
valid_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
在这里,我们定义了三个ImageDataGenerator类的实例:一个用于训练,一个用于验证,一个用于测试。我们对每个实例中的图像像素值应用了 1/255 的缩放因子,以便对数据进行归一化:
# Import data from directories and turn it into batches
train_data = train_datagen.flow_from_directory(train_dir,
batch_size=64, # number of images to process at a time
target_size=(224,224), # convert all images to be 224 x 224
class_mode="categorical")
valid_data = valid_datagen.flow_from_directory(val_dir,
batch_size=64,
target_size=(224,224),
class_mode="categorical")
test_data = test_datagen.flow_from_directory(test_dir,
batch_size=64,
target_size=(224,224),
class_mode="categorical")
我们使用flow_from_directory从各自的训练、验证和测试目录中导入图像,结果数据存储在train_data、valid_data和test_data变量中。除了在flow_from_directory方法中指定目录外,您会注意到我们不仅指定了目标大小(224 x 244)和批量大小(64),还指定了我们正在处理的问题类型为categorical,因为我们处理的是一个多分类问题。我们现在已经成功完成了数据预处理步骤。接下来,我们将开始对数据建模:
model_1 = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters=16,
kernel_size=3, # can also be (3, 3)
activation="relu",
input_shape=(224, 224, 3)),
#(height, width, colour channels)
tf.keras.layers.MaxPool2D(2,2),
tf.keras.layers.Conv2D(32, 3, activation="relu"),
tf.keras.layers.MaxPool2D(2,2),
tf.keras.layers.Conv2D(64, 3, activation="relu"),
tf.keras.layers.MaxPool2D(2,2),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(1050, activation="relu"),
tf.keras.layers.Dense(4, activation="softmax")
# binary activation output
])
# Compile the model
model_1.compile(loss="CategoricalCrossentropy",
optimizer=tf.keras.optimizers.Adam(),
metrics=["accuracy"])
# Fit the model
history_1 = model_1.fit(train_data,
epochs=10,
validation_data=valid_data,
)
在这里,我们使用一个由三组卷积层和池化层组成的 CNN 架构。在第一层卷积层中,我们应用了 16 个 3×3 的过滤器。请注意,输入形状也与我们在预处理步骤中定义的形状匹配。在第一层卷积层之后,我们应用了 2x2 的最大池化。接下来,我们进入第二层卷积层,使用 32 个 3×3 的过滤器,后面跟着另一个 2x2 的最大池化层。最后一层卷积层有 64 个 3×3 的过滤器,后面跟着另一个最大池化层,进一步对数据进行下采样。
接下来,我们进入全连接层。在这里,我们首先将之前层的 3D 输出展平为 1D 数组。然后,我们将数据传递到密集层进行最终分类。接下来,我们编译并拟合我们的模型到数据上。需要注意的是,在我们的compile步骤中,我们使用CategoricalCrossentropy作为loss函数,因为我们正在处理一个多类任务,并将metrics设置为accuracy。最终输出是一个概率分布,表示我们数据集中的四个类别,具有最高概率的类别即为预测标签:
Epoch 6/10
13/13 [==============================] - 8s 622ms/step - loss: 0.1961 - accuracy: 0.9368 - val_loss: 0.2428 - val_accuracy: 0.8994
Epoch 7/10
13/13 [==============================] - 8s 653ms/step - loss: 0.1897 - accuracy: 0.9241 - val_loss: 0.2967 - val_accuracy: 0.9218
Epoch 8/10
13/13 [==============================] - 8s 613ms/step - loss: 0.1093 - accuracy: 0.9671 - val_loss: 0.3447 - val_accuracy: 0.8939
Epoch 9/10
13/13 [==============================] - 8s 604ms/step - loss: 0.1756 - accuracy: 0.9381 - val_loss: 0.6276 - val_accuracy: 0.8324
Epoch 10/10
13/13 [==============================] - 8s 629ms/step - loss: 0.1472 - accuracy: 0.9418 - val_loss: 0.2633 - val_accuracy: 0.9106
我们训练了我们的模型 10 个周期,达到了在训练数据上的 94%训练准确率和在验证数据上的 91%准确率。我们使用summary方法来获取模型中不同层的信息。这些信息包括每层的概述、输出形状以及使用的参数数量(可训练和不可训练):
Model: "sequential"
___________________________________________________________
Layer (type) Output Shape Param #
===========================================================
conv2d (Conv2D) (None, 222, 222, 16) 448
max_pooling2d (MaxPooling2D) (None, 111, 111, 16) 0
conv2d_1 (Conv2D) (None, 109, 109, 32) 4640
max_pooling2d_1 (MaxPooling (None, 54, 54, 32) 0
2D)
conv2d_2 (Conv2D) (None, 52, 52, 64) 18496
max_pooling2d_2 (MaxPooling (None, 26, 26, 64) 0
2D)
flatten (Flatten) (None, 43264) 0
dense (Dense) (None, 1050) 45428250
dense_1 (Dense) (None, 4) 4204
===========================================================
Total params: 45,456,038
Trainable params: 45,456,038
Non-trainable params: 0
___________________________________________________________
从模型的总结中,我们看到我们的架构包含三层卷积层(Conv2D),每一层都配有一个池化层(MaxPooling2D)。信息从这些层流入到全连接层,在那里进行最终的分类。让我们深入分析每一层,解读它们提供的信息。第一层卷积层的输出形状为(None, 222, 222, 16)。这里,None表示我们没有硬编码批次大小,这使得我们能够灵活地使用不同的批次大小。接下来,222, 222表示输出特征图的尺寸;如果不应用填充,我们会因为边界效应而丢失 2 个像素的高度和宽度。最后,16表示使用的过滤器或内核的数量,这意味着每个过滤器将输出 16 个不同的特征图。你还会注意到这一层有448个参数。为了计算卷积层中的参数数量,我们使用以下公式:
(过滤器宽度 × 过滤器高度 × 输入通道数 + 1(偏置)) × 过滤器数量 = 卷积层的总参数数量
当我们将数值代入公式时,我们得到(3 × 3 × 3 + 1)× 16 = 448 个参数。
下一个层是第一个池化层,它是一个MaxPooling2D层,用于对卷积层的输出特征图进行降采样。在这里,我们的输出形状为(None, 111, 111, 16)。从输出中可以看到,空间维度已经缩小了一半,同时也要注意,池化层没有参数,正如我们在模型总结中看到的所有池化层一样。
接下来,我们进入第二个卷积层,注意到我们的输出深度已经增加到了32。这是因为我们在该层使用了 32 个过滤器,因此将返回 32 个不同的特征图。同时,由于边界效应,我们的特征图的空间维度再次被减少了两个像素。我们可以通过以下方式轻松计算该层的参数数量:(3 × 3 × 16 + 1)× 32 = 4,640 个参数。
接下来,我们进入第二个池化层,它进一步对特征图进行降采样,输出尺寸为(None, 54, 54, 32)。最后的卷积层使用 64 个过滤器,因此输出形状为(None, 52, 52, 64),有 18,496 个参数。最后的池化层再次将数据维度降至(None, 26, 26, 64)。最后池化层的输出被送入Flatten层,后者将数据从 3D 张量重塑为 1D 张量,尺寸为 26 x 26 x 64 = 43,264。这个数据接着被送入第一个Dense层,其输出形状为(None, 1050)。为了计算Dense层的参数数量,我们使用以下公式:
(输入节点数 + 1) × 输出节点数
当我们输入这些数值时,得到(43,264 + 1) × 1,050 = 45,428,250 个参数。最终的Dense层是输出层,其形状为(None, 4),其中4表示我们要预测的独特类别数。由于连接、偏置和输出神经元数量,该层有(1,050 + 1) × 4 = 4,204 个参数。
接下来,我们使用evaluate方法评估我们的模型:
model_1.evaluate(test_data)
我们在测试数据上达到了 91%的准确率。
让我们将我们的 CNN 架构与两个 DNN 进行比较:
model_2 = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=(224, 224, 3)),
tf.keras.layers.Dense(1200, activation='relu'),
tf.keras.layers.Dense(600, activation='relu'),
tf.keras.layers.Dense(300, activation='relu'),
tf.keras.layers.Dense(4, activation='softmax')
])
# Compile the model
model_2.compile(loss='categorical_crossentropy',
optimizer=tf.keras.optimizers.Adam(),
metrics=["accuracy"])
# Fit the model
history_2 = model_2.fit(train_data,
epochs=10,
validation_data=valid_data)
我们构建了一个名为model_2的 DNN,由 4 个Dense层组成,分别有1200、600、300和4个神经元。除了输出层使用softmax函数进行分类外,其他所有层都使用 ReLU 作为激活函数。我们与model_1的方式相同,编译并拟合model_2:
Epoch 6/10
13/13 [==============================] - 8s 625ms/step - loss: 2.2083 - accuracy: 0.6953 - val_loss: 0.9884 - val_accuracy: 0.7933
Epoch 7/10
13/13 [==============================] - 8s 606ms/step - loss: 2.7116 - accuracy: 0.6435 - val_loss: 2.0749 - val_accuracy: 0.6704
Epoch 8/10
13/13 [==============================] - 8s 636ms/step - loss: 2.8324 - accuracy: 0.6877 - val_loss: 1.7241 - val_accuracy: 0.7430
Epoch 9/10
13/13 [==============================] - 8s 599ms/step - loss: 1.8597 - accuracy: 0.6890 - val_loss: 1.1507 - val_accuracy: 0.7877
Epoch 10/10
13/13 [==============================] - 8s 612ms/step - loss: 1.0902 - accuracy: 0.7813 - val_loss: 0.9915 - val_accuracy: 0.7486
经过 10 个 epoch,我们达到了 74.86%的验证准确率,当我们查看模型的总结时,发现我们总共使用了 181,536,904 个参数,这是我们 CNN 架构参数的 4 倍。
接下来,我们来看另一个 DNN 架构:
model_3 = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=(224, 224, 3)),
tf.keras.layers.Dense(1000, activation='relu'),
tf.keras.layers.Dense(500, activation='relu'),
tf.keras.layers.Dense(500, activation='relu'),
tf.keras.layers.Dense(4, activation='softmax')
])
# Compile the model
model_3.compile(loss='categorical_crossentropy',
optimizer=tf.keras.optimizers.Adam(),
metrics=["accuracy"])
# Fit the model
history_3 = model_3.fit(train_data,
epochs=10,
validation_data=valid_data)
我们使用另外一组 4 个Dense层,分别有1000、500、500和4个神经元。我们同样为model_3进行了 10 个 epoch 的拟合和编译:
Epoch 6/10
13/13 [==============================] - 9s 665ms/step - loss: 1.6911 - accuracy: 0.6814 - val_loss: 0.5861 - val_accuracy: 0.7877
Epoch 7/10
13/13 [==============================] - 8s 606ms/step - loss: 0.7309 - accuracy: 0.7952 - val_loss: 0.5100 - val_accuracy: 0.8268
Epoch 8/10
13/13 [==============================] - 8s 572ms/step - loss: 0.6797 - accuracy: 0.7863 - val_loss: 0.9520 - val_accuracy: 0.7263
Epoch 9/10
13/13 [==============================] - 8s 632ms/step - loss: 0.7430 - accuracy: 0.7724 - val_loss: 0.5220 - val_accuracy: 0.7933
Epoch 10/10
13/13 [==============================] - 8s 620ms/step - loss: 0.5845 - accuracy: 0.7737 - val_loss: 0.5881 - val_accuracy: 0.7765
在经过 10 个 epoch 后,我们达到了 77.65%的验证准确率,该模型大约有 151,282,004 个参数;结果与我们的 CNN 架构相差较大。接下来,让我们在测试数据上比较三种模型,这是我们评估模型的标准。为此,我们将编写一个函数来生成一个 DataFrame,显示模型的名称、损失和准确率:
def evaluate_models(models, model_names,test_data):
# Initialize lists for the results
losses = []
accuracies = []
# Iterate over the models
for model in models:
# Evaluate the model
loss, accuracy = model.evaluate(test_data)
losses.append(loss)
accuracies.append(accuracy)
# Convert the results to percentages
losses = [round(loss * 100, 2) for loss in losses]
accuracies = [round(accuracy * 100, 2) for accuracy in accuracies]
# Create a dataframe with the results
results = pd.DataFrame({"Model": model_names,
"Loss": losses,
"Accuracy": accuracies})
return results
evaluate_models()函数接受一个模型列表、模型名称和测试数据作为输入,并返回一个包含每个模型评估结果(以百分比形式)的 DataFrame:
# Define the models and model names
models = [model_1, model_2, model_3]
model_names = ["Model 1", "Model 2", "Model 3"]
# Evaluate the models
results = evaluate_models(models, model_names,test_data)
# Display the results
results
当我们运行代码时,它会生成如图 7.20所示的表格。

图 7.20 – 显示所有三种模型实验结果的 DataFrame
从结果中,我们可以清楚地看到模型 1 表现最好。你可能希望尝试更大的 DNN,但很快会遇到内存不足的问题。对于更大的数据集,DNN 的结果可能会大幅下降。接下来,让我们看看模型 1 在训练和验证数据上的表现:
def plot_loss_accuracy(history_1):
# Extract the loss and accuracy history for both training and validation data
loss = history_1.history['loss']
val_loss = history_1.history['val_loss']
acc = history_1.history['accuracy']
val_acc = history_1.history['val_accuracy']
# Create subplots
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 6))
# Plot the loss history
ax1.plot(loss, label='Training loss')
ax1.plot(val_loss, label='Validation loss')
ax1.set_title('Loss history')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.legend()
# Plot the accuracy history
ax2.plot(acc, label='Training accuracy')
ax2.plot(val_acc, label='Validation accuracy')
ax2.set_title('Accuracy history')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Accuracy')
ax2.legend()
plt.show()
我们创建了一个函数,使用 matplotlib 绘制训练和验证的损失与准确率图。我们将history_1传递给这个函数:
# Lets plot the training and validation loss and accuracy
plot_loss_accuracy(history_1)
这将生成以下输出:

图 7.21 – 模型 1 的损失和准确率图
从图中可以看到,我们的训练准确率稳步上升,但在接近第 10 个 epoch 时,准确率未能达到最高点。同时,我们的验证数据的准确率出现了急剧下降。我们的损失从第 4 个 epoch 开始偏离。
总结
在这一章中,我们见识了 CNN 的强大。我们首先探讨了 DNN 在视觉识别任务中面临的挑战。接着,我们深入了解了 CNN 的构造,重点讲解了卷积层、池化层和全连接层等各个部分。在这里,我们观察了不同超参数的影响,并讨论了边界效应。接下来,我们利用所学知识,构建了一个实际的天气分类器,使用了两个 DNN 和一个 CNN。我们的 CNN 模型优于 DNN,展示了 CNN 在处理基于图像的问题中的优势。同时,我们还讨论并应用了一些 TensorFlow 函数,这些函数可以简化数据预处理和建模过程,特别是在处理图像数据时。
到目前为止,你应该已经很好地理解了 CNN 的结构和操作原理,以及如何使用它们解决实际的图像分类问题,并掌握了利用 TensorFlow 中各种工具有效、高效地预处理图像数据,从而提高模型性能。在下一章中,我们将讨论神经网络中的过拟合问题,并探索各种技术来克服这一挑战,确保我们的模型能很好地泛化到未见过的数据上。
在下一章中,我们将使用一些老技巧,如回调和超参数调整,看看是否能提高模型性能。我们还将实验数据增强和其他新技术,以进一步提高模型的表现。我们将在此任务上暂时画上句号,直到第八章**, 处理过拟合。
问题
让我们测试一下本章所学内容:
-
一个典型的 CNN 架构有哪些组成部分?
-
卷积层在 CNN 架构中是如何工作的?
-
什么是池化,为什么在 CNN 架构中使用池化?
-
在 CNN 架构中,全连接层的目的是什么?
-
填充对卷积操作有什么影响?
-
使用 TensorFlow 图像数据生成器的优势是什么?
进一步阅读
要了解更多内容,您可以查看以下资源:
-
Dumoulin, V., & Visin, F. (2016). 《深度学习的卷积算术指南》。
arxiv.org/abs/1603.07285 -
Gulli, A., Kapoor, A. 和 Pal, S., 2019. 《使用 TensorFlow 2 和 Keras 的深度学习》。伯明翰:Packt Publishing Ltd
-
Kapoor, A., Gulli, A. 和 Pal, S. (2020) 《深度学习与 TensorFlow 和 Keras 第三版:构建和部署监督、非监督、深度和强化学习模型》。Packt Publishing Ltd
-
Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). 《使用深度卷积神经网络进行 ImageNet 分类》。《神经信息处理系统进展》(第 1097-1105 页)
-
Zhang, Y., & Yang, H. (2018). 《使用卷积神经网络和多类线性判别分析进行食物分类》。2018 年 IEEE 国际信息重用与集成会议(IRI)(第 1-5 页)。IEEE
-
Zhang, Z., Ma, H., Fu, H., & Zha, C. (2020). 《基于单图像的无场景多类天气分类》。IEEE Access, 8, 146038-146049. doi:10.1109
-
tf.image模块:www.tensorflow.org/api_docs/python/tf/image
第八章:处理过拟合
机器学习(ML)中的一个主要挑战是过拟合。过拟合发生在模型对训练数据的拟合过好,但在未见过的数据上表现不佳,导致性能差。在第六章中,改进模型,我们亲眼见证了过度训练如何将我们的模型推入过拟合的陷阱。在本章中,我们将进一步探讨过拟合的细微差别,努力揭示其警告信号及其潜在原因。同时,我们还将探索可以应用的各种策略,以减轻过拟合对现实世界机器学习应用的危害。通过 TensorFlow,我们将以实践的方式应用这些思想,克服在实际案例中遇到的过拟合问题。通过本章学习结束后,你应该对过拟合的概念以及如何在现实世界的图像分类任务中减少过拟合有一个扎实的理解。
在本章中,我们将讨论以下主题:
-
机器学习中的过拟合
-
提前停止
-
更改模型架构
-
L1 和 L2 正则化
-
Dropout 正则化
-
数据增强
技术要求
我们将使用 Google Colab 来运行需要 python >= 3.8.0 的编码练习,并且需要安装以下包,可以通过 pip install 命令进行安装:
-
tensorflow>=2.7.0 -
os -
pillow==8.4.0 -
pandas==1.3.4 -
numpy==1.21.4 -
matplotlib >=3.4.0
本书的代码包可以通过以下 GitHub 链接访问:github.com/PacktPublishing/TensorFlow-Developer-Certificate。此外,所有练习的解答也可以在 GitHub 仓库中找到。
机器学习中的过拟合
从前面的章节中,我们已经知道了什么是过拟合,以及它在未见过的数据上使用时的负面影响。接下来,我们将进一步探讨过拟合的根本原因,如何在构建模型时识别过拟合,以及可以应用的一些重要策略来抑制过拟合。当我们理解了这些内容后,就可以继续构建有效且强大的机器学习模型。
触发过拟合的原因
在第六章,改进模型中,我们看到通过向隐藏层添加更多神经元,我们的模型变得过于复杂。这使得模型不仅捕捉到了数据中的模式,还捕捉到了数据中的噪声,从而导致了过拟合。另一个导致过拟合的根本原因是数据量不足。如果我们的数据无法真正捕捉到模型在部署后将面临的所有变化,当我们在这样的数据集上训练模型时,它会变得过于专门化,并且在实际应用中无法进行有效的泛化。
除了数据量之外,我们还可能面临另一个问题——噪声数据。与处理经过筛选或静态的数据不同,在构建实际应用时,我们可能会发现数据存在噪声或错误。如果我们使用这些数据开发模型,可能会导致在实际使用时出现过拟合的情况。我们已经讨论了关于过拟合可能发生的一些原因;接下来我们可能想要问的问题是,我们如何检测过拟合?让我们在接下来的子章节中讨论这个问题。
检测过拟合
检测过拟合的一种方法是比较模型在训练数据和验证/测试数据上的准确度。当模型在训练数据上表现出高准确度,而在测试数据上表现不佳时,这种差异表明模型已经记住了训练样本,因此在未见过的数据上的泛化能力较差。另一种有效的发现过拟合的方法是检查训练误差与验证误差。当训练误差随着时间的推移逐渐减小,而验证误差却增加时,这可能表明我们的模型过拟合了,因为模型在验证数据上的表现变差。当模型的验证准确度恶化,而训练准确度却不断提升时,应该引起警觉,可能存在过拟合的风险。
让我们回顾一下来自第七章的案例研究,卷积神经网络的图像分类,以及 WeatherBIG 的天气数据集,并探讨在模型训练过程中如何通过使用验证数据集来监控过拟合。通过使用验证数据集,我们可以准确地追踪模型的表现,防止过拟合。首先,我们将创建一个基准模型。
基准模型
按照构建、编译和拟合的标准三步法,我们将构建一个卷积神经网络(CNN)模型,该模型包括两个 Conv2D 和池化层,并配有一个具有 1,050 个神经元的全连接层。输出层由四个神经元组成,表示我们数据集中的四个类别。然后,我们使用训练数据将模型编译并拟合 20 个周期:
#Build
model_1 = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters=16,
kernel_size=3, # can also be (3, 3)
activation="relu",
input_shape=(224, 224, 3)),
#(height, width, colour channels)
tf.keras.layers.MaxPool2D(2,2),
tf.keras.layers.Conv2D(32, 3, activation="relu"),
tf.keras.layers.MaxPool2D(2,2),
tf.keras.layers.Conv2D(64, 3, activation="relu"),
tf.keras.layers.MaxPool2D(2,2),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(1050, activation="relu"),
tf.keras.layers.Dense(4, activation="softmax")
])
# Compile the model
model_1.compile(loss="CategoricalCrossentropy",
optimizer=tf.keras.optimizers.Adam(),
metrics=["accuracy"])
#fit
history_1 = model_1.fit(train_data,
epochs=20,
validation_data=valid_data
)
我们将validation_data参数设置为valid_data。这确保了当我们运行代码时,在每个周期结束后,模型会在验证数据上评估其性能,如图 8.1所示。

图 8.1 – 最后的五个训练周期
这是一种直观的方法,可以比较训练集和验证集之间的损失值。我们可以看到模型能够准确地预测训练集中的每个样本,达到了 100%的准确率。然而,在验证集上,它的准确率为 91%,这表明模型可能存在过拟合问题。观察过拟合的另一种有效方法是使用学习曲线,绘制训练集和验证集的损失和准确度值——如图 8.2所示,两个图之间存在较大的差距,表明模型存在过拟合。

图 8.2 – 显示训练和测试数据的损失和准确度的学习曲线
在实验开始时,训练损失和验证损失之间的差异较小;然而,进入第四轮时,验证损失开始增加,而训练损失继续下降。类似地,训练和验证的准确度开始时较为接近,但在大约第四轮时,验证准确率达到了 90%左右并保持在该水平,而训练准确率达到了 100%。
构建图像分类器的最终目标是将其应用于现实世界的数据。在完成训练过程后,我们使用保留数据集评估模型。如果在测试中获得的结果与训练过程中取得的结果有显著差异,这可能表明模型存在过拟合。
幸运的是,有几种策略可以用来克服过拟合问题。一些主要的应对过拟合的技术侧重于改进模型本身,以提高其泛化能力。另一方面,检查数据本身同样重要,观察模型在训练和评估过程中忽视的部分。通过可视化错误分类的图像,我们可以洞察模型的不足之处。我们从第七章《卷积神经网络图像分类》开始,首先重新创建我们的基线模型。这次我们将其训练 20 轮,以便观察过拟合问题,如图 8.2所示。接下来,让我们看看如何通过多种策略来抑制过拟合,首先是应用早停法。
早停法
在第六章《改进模型》中,我们介绍了早停法的概念,这是一种有效的防止过拟合的方法。它通过在模型性能未能在定义的若干轮次内改善时停止训练,如图 8.3所示,从而避免了过拟合的发生。

图 8.3 – 显示早停法的学习曲线
让我们重新创建相同的基准模型,但这次我们将应用一个内置回调,在验证精度未能提高时停止训练。我们将使用与第一个模型相同的构建和编译步骤,然后在拟合模型时添加回调:
#Fit the model
# Add an early stopping callback
callbacks = [tf.keras.callbacks.EarlyStopping(
monitor="val_accuracy", patience=3,
restore_best_weights=True)]
history_2 = model_2.fit(train_data,
epochs=20,
validation_data=valid_data,
callbacks=[callbacks])
在这里,我们将周期数指定为20,并添加了验证集来监控模型在训练过程中的表现。之后,我们使用callbacks参数指定了一个回调函数来实现早停。我们使用了一个早停回调,在验证集的精度未能提高时,训练将在三轮后停止。通过将patience参数设置为3来实现这一点。这意味着如果验证精度连续三轮没有进展,早停回调将停止训练。我们还将restore_best_weights参数设置为True;这将在训练结束时恢复训练过程中最好的模型权重。fit函数的信息存储在history_2变量中:
Epoch 8/20
25/25 [==============================] - 8s 318ms/step - loss: 0.0685 - accuracy: 0.9810 - val_loss: 0.3937 - val_accuracy: 0.8827
Epoch 9/20
25/25 [==============================] - 8s 325ms/step - loss: 0.0368 - accuracy: 0.9912 - val_loss: 0.3338 - val_accuracy: 0.9218
Epoch 10/20
25/25 [==============================] - 8s 316ms/step - loss: 0.0169 - accuracy: 0.9987 - val_loss: 0.4322 - val_accuracy: 0.8994
Epoch 11/20
25/25 [==============================] - 8s 297ms/step - loss: 0.0342 - accuracy: 0.9912 - val_loss: 0.2994 - val_accuracy: 0.8994
Epoch 12/20
25/25 [==============================] - 8s 318ms/step - loss: 0.1352 - accuracy: 0.9570 - val_loss: 0.4503 - val_accuracy: 0.8939
从训练过程来看,我们可以看到模型在第九个周期达到了0.9218的最高验证精度,之后训练继续进行了三轮才停止。由于验证精度没有进一步提升,训练被停止,并保存了最佳权重。现在,让我们在测试数据上评估model_2:
model_2.evaluate(test_data)
当我们运行代码时,我们看到模型达到了0.9355的精度。在这里,测试集的表现与验证集的表现一致,并且高于我们的基准模型,后者的精度为0.9097。这是我们创建更好模型的第一步。

图 8.4 – 模型总结快照
当我们检查模型总结时,我们可以看到我们的模型有超过 4500 万个参数,这可能导致模型容易在训练数据中拾取噪声,因为模型高度参数化。为了解决这个问题,我们可以通过减少参数数量来简化模型,使得我们的模型对于数据集来说不会过于复杂。接下来,我们将讨论模型简化。
模型简化
为了应对过拟合,你可以考虑重新评估模型的架构。简化模型的架构可能是应对过拟合的有效策略,特别是在模型高度参数化时。然而,重要的是要知道,这种方法并不总能在每种情况下保证更好的表现;事实上,你必须警惕模型过于简化,这可能导致欠拟合的陷阱。因此,重要的是在模型复杂性和简化之间找到合适的平衡,以实现最佳性能,如图 8.5所示,因为模型复杂性与过拟合之间的关系不是线性的。

图 8.5 – 机器学习中的过拟合和欠拟合
模型简化可以通过多种方式实现——例如,我们可以用更小的滤波器替换大量的滤波器,或者我们还可以减少第一个 Dense 层中的神经元数量。在我们的架构中,你可以看到第一个全连接层有 1050 个神经元。作为模型简化实验的初步步骤,让我们将神经元数量减少到 500:
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(500, activation="relu"),
tf.keras.layers.Dense(4, activation="softmax")
])
当我们编译并拟合模型时,我们的模型在验证集上达到了 0.9162 的最高准确率:
Epoch 5/50
25/25 [==============================] - 8s 300ms/step - loss: 0.1284 - accuracy: 0.9482 - val_loss: 0.4489 - val_accuracy: 0.8771
Epoch 6/50
25/25 [==============================] - 8s 315ms/step - loss: 0.1122 - accuracy: 0.9659 - val_loss: 0.2414 - val_accuracy: 0.9162
Epoch 7/50
25/25 [==============================] - 8s 327ms/step - loss: 0.0814 - accuracy: 0.9735 - val_loss: 0.2976 - val_accuracy: 0.9050
Epoch 8/50
25/25 [==============================] - 11s 441ms/step - loss: 0.0541 - accuracy: 0.9785 - val_loss: 0.2215 - val_accuracy: 0.9050
Epoch 9/50
25/25 [==============================] - 8s 313ms/step - loss: 0.1279 - accuracy: 0.9621 - val_loss: 0.2848 - val_accuracy: 0.8994
由于我们的验证准确率并没有更好,或许现在是时候尝试一些著名的想法来解决过拟合问题了。让我们在接下来的小节中看一下 L1 和 L2 正则化。我们将讨论它们如何工作,并将其应用到我们的案例研究中。
注意
模型简化的目标不是为了得到更小的模型,而是为了设计出一个能很好地泛化的模型。我们可能只需要减少不必要的层,或者通过改变激活函数,或者重新组织模型层的顺序和排列,以改善信息流动,从而简化模型。
L1 和 L2 正则化
正则化是一组通过向损失函数应用惩罚项来减少模型复杂性,从而防止过拟合的技术。正则化技术使模型对训练数据中的噪声更加抗干扰,从而提高了它对未见数据的泛化能力。正则化技术有多种类型,分别是 L1 和 L2 正则化。L1 和 L2 正则化是两种广为人知的正则化技术;L1 也可以称为 套索回归。在选择 L1 和 L2 时,重要的是要考虑我们所处理数据的类型。
当处理具有大量无关特征的数据时,L1 正则化非常有用。L1 中的惩罚项会导致一些系数变为零,从而减少在建模过程中使用的特征数量;这反过来减少了过拟合的风险,因为模型将基于较少的噪声数据进行训练。相反,当目标是创建具有小权重和良好泛化能力的模型时,L2 是一个非常好的选择。L2 中的惩罚项减少了系数的大小,防止它们变得过大,从而导致过拟合:
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(1050, activation="relu",
kernel_regularizer=regularizers.l2(0.01)),
tf.keras.layers.Dense(4, activation="softmax")
# binary activation output
])
当我们运行这个实验时,准确率大约为 92%,并没有比其他实验表现得更好。为了尝试 L1 正则化,我们只是将正则化方法从 L2 改为 L1。然而,在这种情况下,我们的结果并不好。因此,让我们尝试另一种叫做 dropout 正则化的正则化方法。
Dropout 正则化
神经网络的一个关键问题是共依赖性。共依赖性是神经网络中一种现象,当一组神经元,特别是同一层中的神经元,变得高度相关,以至于它们过度依赖彼此时,就会发生共依赖性。这可能导致它们放大某些特征,同时无法捕捉到数据中的其他重要特征。由于这些神经元同步工作,我们的模型更容易发生过拟合。为了减轻这一风险,我们可以应用一种称为 dropout 的技术。与 L1 和 L2 正则化不同,dropout 不会添加惩罚项,但顾名思义,在训练过程中我们会随机“丢弃”一部分神经元,如 图 8.6 所示,这有助于减少神经元之间的共依赖性,从而有助于防止过拟合。

图 8.6 – 应用了 dropout 的神经网络
当我们应用 dropout 技术时,模型被迫学习更鲁棒的特征,因为我们打破了神经元之间的共依赖性。然而,值得注意的是,当我们应用 dropout 时,训练过程可能需要更多的迭代才能达到收敛。让我们将 dropout 应用到我们的基础模型上,观察它的效果:
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(1050, activation="relu"),
tf.keras.layers.Dropout(0.6), # added dropout layer
tf.keras.layers.Dense(4, activation="softmax")])
要在代码中实现 dropout,我们使用 tf.keras.layers.Dropout(0.6) 函数来指定 dropout 层。这会创建一个 dropout 层,dropout 率为 0.6 —— 即在训练过程中我们会关闭 60% 的神经元。值得注意的是,我们可以将 dropout 值设置在 0 和 1 之间:
25/25 [==============================] - 8s 333ms/step - loss: 0.3069 - accuracy: 0.8913 - val_loss: 0.2227 - val_accuracy: 0.9330
Epoch 6/10
25/25 [==============================] - 8s 317ms/step - loss: 0.3206 - accuracy: 0.8824 - val_loss: 0.1797 - val_accuracy: 0.9441
Epoch 7/10
25/25 [==============================] - 8s 322ms/step - loss: 0.2557 - accuracy: 0.9166 - val_loss: 0.2503 - val_accuracy: 0.8994
Epoch 8/10
25/25 [==============================] - 9s 339ms/step - loss: 0.1474 - accuracy: 0.9469 - val_loss: 0.2282 - val_accuracy: 0.9274
Epoch 9/10
25/25 [==============================] - 8s 326ms/step - loss: 0.2321 - accuracy: 0.9241 - val_loss: 0.3958 - val_accuracy: 0.8659
在这个实验中,我们的模型在验证集上达到了 0.9441 的最佳性能,提升了基础模型的表现。接下来,让我们看看调整学习率的效果。
调整学习率
在 第六章《提高模型》中,我们讨论了学习率以及寻找最优学习率的重要性。在这个实验中,我们使用 0.0001 的学习率,这是通过尝试不同的学习率得到的一个良好结果,类似于我们在 第六章《提高模型》中做的实验。在 第十三章《使用 TensorFlow 进行时间序列、序列和预测》中,我们将研究如何应用自定义和内建的学习率调度器。这里,我们还应用了早停回调,以确保当模型无法再提高时,训练能够终止。让我们编译我们的模型:
# Compile the model
model_7.compile(loss="CategoricalCrossentropy",
optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
metrics=["accuracy"])
我们将拟合模型并运行它。在七个 epoch 后,我们的模型训练停止,达到了验证集上的最佳性能 0.9274:
Epoch 3/10
25/25 [==============================] - 8s 321ms/step - loss: 0.4608 - accuracy: 0.8508 - val_loss: 0.2776 - val_accuracy: 0.8994
Epoch 4/10
25/25 [==============================] - 8s 305ms/step - loss: 0.3677 - accuracy: 0.8824 - val_loss: 0.2512 - val_accuracy: 0.9274
Epoch 5/10
25/25 [==============================] - 8s 316ms/step - loss: 0.3143 - accuracy: 0.8925 - val_loss: 0.4450 - val_accuracy: 0.8324
Epoch 6/10
25/25 [==============================] - 8s 317ms/step - loss: 0.2749 - accuracy: 0.9052 - val_loss: 0.3427 - val_accuracy: 0.8603
Epoch 7/10
25/25 [==============================] - 8s 322ms/step - loss: 0.2241 - accuracy: 0.9279 - val_loss: 0.2996 - val_accuracy: 0.8659
我们已经探索了各种方法来改善我们的模型并克服过拟合问题。现在,让我们将焦点转向数据集本身,看看错误分析如何发挥作用。
错误分析
根据我们目前的结果,我们可以看到模型未能正确地将某些标签分类。为了进一步提高模型的泛化能力,最好检查模型所犯的错误,其背后的思想是揭示误分类数据中的模式,以便我们从查看误分类标签中获得的洞察可以用于改善模型的泛化能力。这种技术称为错误分析。进行错误分析时,我们首先通过识别验证/测试集中的误分类标签开始。接下来,我们将这些错误分组——例如,我们可以将模糊图像或光照条件差的图像归为一组。
基于从收集到的错误中获得的洞察,我们可能需要调整我们的模型架构或调整超参数,特别是当模型未能捕捉到某些特征时。此外,我们的错误分析步骤也可能会指出需要改善数据的大小和质量。解决这一问题的有效方法之一是应用数据增强,这是一种众所周知的技术,用于丰富我们的数据量和质量。接下来,让我们讨论数据增强并将其应用于我们的案例研究。
数据增强
图像数据增强是一种通过应用各种变换(例如旋转、翻转、裁剪和缩放)来增加我们训练集的大小和多样性的技术,从而创建新的合成数据,如图 8.7所示。对于许多实际应用来说,数据收集可能是一个非常昂贵且耗时的过程;因此,数据增强非常有用。数据增强帮助模型学习更具鲁棒性的特征,而不是让模型记住特征,从而提高模型的泛化能力。

图 8.7 – 应用于蝴蝶图像的各种数据增强技术(来源:https://medium.com/secure-and-private-ai-writing-challenge/data-augmentation-increases-accuracy-of-your-model-but-how-aa1913468722)
数据增强的另一个重要用途是为了在训练数据集中创建不同类别之间的平衡。如果训练集包含不平衡的数据,我们可以使用数据增强技术来创建少数类的变体,从而构建一个更加平衡的数据集,降低过拟合的可能性。在实施数据增强时,重要的是要牢记可能影响结果的各种因素。例如,使用哪种类型的数据增强取决于我们所处理的数据类型。
在图像分类任务中,诸如随机旋转、平移、翻转和缩放等技术可能会证明是有用的。然而,在处理数字数据集时,对数字应用旋转可能会导致意想不到的结果,比如将数字 6 旋转成 9。再者,翻转字母表中的字母,比如“b”和“d”,也可能带来不良影响。当我们对训练集应用图像增强时,考虑增强的幅度及其对训练数据质量的影响至关重要。过度增强可能导致图像严重失真,从而导致模型性能不佳。为防止这种情况的发生,监控模型的训练过程并使用验证集同样重要。
让我们对案例研究应用数据增强,看看我们的结果会是什么样子。
要实现数据增强,您可以使用 tf.keras.preprocessing.image 模块中的 ImageDataGenerator 类。这个类允许您指定一系列的变换,这些变换只应应用于训练集中的图像,并且它会在训练过程中实时生成合成图像。例如,您可以使用 ImageDataGenerator 类对训练图像应用旋转、翻转和缩放变换,方法如下:
train_datagen = ImageDataGenerator(rescale=1./255,
rotation_range=25, zoom_range=0.3)
valid_datagen = ImageDataGenerator(rescale=1./255)
# Set up the train, validation, and test directories
train_dir = "/content/drive/MyDrive/weather dataset/train/"
val_dir = "/content/drive/MyDrive/weather dataset/validation/"
test_dir = "/content/drive/MyDrive/weather dataset/test/"
# Import data from directories and turn it into batches
train_data = train_datagen.flow_from_directory(
train_dir,
target_size=(224,224), # convert all images to be 224 x 224
class_mode="categorical")
valid_data = valid_datagen.flow_from_directory(
val_dir,
target_size=(224,224),
class_mode="categorical")
test_data = valid_datagen.flow_from_directory(
test_dir,
target_size=(224,224),
class_mode="categorical",)
使用图像数据增强非常简单;我们为训练集、验证集和测试集创建了 keras.preprocessing.image 模块中的 ImageDataGenerator 类的三个实例。一个关键的区别是,我们在 train_datagen 对象中添加了 rotation_range=25 和 zoom_range=0.3 参数。这样,在训练过程中,图像将随机旋转 25 度并缩放 0.3 倍,其他所有设置保持不变。
接下来,我们将构建、编译并拟合我们的基准模型,并应用早停技术,在增强数据上进行训练:
Epoch 4/20
25/25 [==============================] - 8s 308ms/step - loss: 0.2888 - accuracy: 0.9014 - val_loss: 0.3256 - val_accuracy: 0.8715
Epoch 5/20
25/25 [==============================] - 8s 312ms/step - loss: 0.2339 - accuracy: 0.9115 - val_loss: 0.2172 - val_accuracy: 0.9330
Epoch 6/20
25/25 [==============================] - 8s 320ms/step - loss: 0.1444 - accuracy: 0.9507 - val_loss: 0.2379 - val_accuracy: 0.9106
Epoch 7/20
25/25 [==============================] - 8s 315ms/step - loss: 0.1190 - accuracy: 0.9545 - val_loss: 0.2828 - val_accuracy: 0.9162
Epoch 8/20
25/25 [==============================] - 8s 317ms/step - loss: 0.0760 - accuracy: 0.9785 - val_loss: 0.3220 - val_accuracy: 0.8883
在八个训练周期后,我们的训练结束了。这次,我们在验证集上的得分达到了 0.9330。到目前为止,我们已经运行了七个不同的实验。接下来,让我们在测试集上测试这些模型,看看结果如何。为此,我们将编写一个辅助函数,创建一个 DataFrame,显示前五个模型、每个模型的名称,以及每个模型的损失和准确度,如 图 8.8 所示。

图 8.8 – 显示前五个模型的损失和准确度的 DataFrame
在我们的测试数据中,表现最好的模型是模型 7,它调整了学习率。我们已经讨论了一些在现实世界中用于解决图像分类过拟合问题的想法;然而,结合这些技术可以构建一个更简单但更强大的模型,从而减少过拟合的风险。通常来说,将多种技术结合起来遏制过拟合是一个好主意,因为这可能有助于生成一个更强大且更具泛化能力的模型。然而,重要的是要记住,没有一刀切的解决方案,最好的方法组合将取决于具体的数据和任务,并可能需要多次实验。
总结
在本章中,我们讨论了图像分类中的过拟合问题,并探索了克服它的不同技术。我们首先探讨了什么是过拟合以及为什么会发生过拟合,接着讨论了如何应用不同的技术,如提前停止、模型简化、L1 和 L2 正则化、dropout 以及数据增强来缓解图像分类任务中的过拟合问题。此外,我们还在天气数据集的案例研究中应用了这些技术,并通过实际操作观察了这些技术在案例中的效果。我们还探讨了将这些技术结合起来,以构建一个最优模型的过程。到目前为止,你应该已经对过拟合以及如何在自己的图像分类项目中减轻过拟合有了深入的了解。
在下一章中,我们将深入探讨迁移学习,这是一种强大的技术,能够让你利用预训练的模型来完成特定的图像分类任务,从而节省时间和资源,同时取得令人印象深刻的结果。
问题
让我们来测试一下本章学到的内容:
-
图像分类任务中的过拟合是什么?
-
过拟合是如何发生的?
-
可以使用哪些技术来防止过拟合?
-
什么是数据增强,如何利用它来防止过拟合?
-
如何通过数据预处理、数据多样性和数据平衡来缓解过拟合?
深入阅读
如需了解更多内容,您可以查看以下资源:
-
Garbin, C., Zhu, X., & Marques, O. (2020). Dropout 与 Batch Normalization 的对比:它们对深度学习的影响的实证研究。arXiv 预印本 arXiv:1911.12677:
par.nsf.gov/servlets/purl/10166570。 -
Kandel, I., & Castelli, M. (2020). 批量大小对卷积神经网络在组织病理学数据集上泛化能力的影响。arXiv 预印本 arXiv:2003.00204。
-
Effect_batch_size_generalizability_convolutional_neural_networks_histopathology_dataset.pdf (unl.pt)。Kapoor, A., Gulli, A. 和 Pal, S. (2020):
research.unl.pt/ws/portalfiles/portal/18415506/Effect_batch_size_generalizability_convolutional_neural_networks_histopathology_dataset.pdf。 -
TensorFlow 与 Keras 深度学习(第三版),Amita Kapoor,Antonio Gulli,Sujit Pal,Packt Publishing Ltd.
-
Nitish Srivastava, Geoffrey Hinton, Alex Krizhevsky, Ilya Sutskever, 和 Ruslan Salakhutdinov. 2014. Dropout: 一种简单的防止神经网络过拟合的方法。J. Mach. Learn. Res. 15, 1 (2014), 1,929–1,958
jmlr.org/papers/volume15/srivastava14a/srivastava14a.pdf. -
Zhang, Z., Ma, H., Fu, H., & Zha, C. (2016). 无场景的单图像多类别天气分类。IEEE Access, 8, 146,038–146,049. doi:10.1109:
web.cse.ohio-state.edu/~zhang.7804/Cheng_NC2016.pdf.
第九章:转移学习
过去十年在机器学习(ML)领域最重要的进展之一是转移学习的概念,这一点当之无愧。转移学习是将从源任务中获得的知识应用到目标任务中的过程,目标任务与源任务不同但相关。这种方法不仅在节省训练深度神经网络所需的计算资源方面非常有效,而且在目标数据集较小的情况下也表现出色。转移学习通过重用从预训练模型中学到的特征,使我们能够构建性能更好的模型,并更快地达到收敛。由于其众多的优势,转移学习已经成为一个广泛研究的领域,许多研究探讨了转移学习在不同领域中的应用,如图像分类、目标检测、自然语言处理和语音识别等。
在本章中,我们将介绍转移学习的概念,探讨它是如何工作的,以及转移学习在不同应用场景中的一些最佳实践。我们将借助知名的预训练模型,在现实世界的应用中应用转移学习的概念。我们将看到如何将这些预训练模型作为特征提取器进行应用,并学习如何微调它们以实现最佳结果。在本章结束时,你将对转移学习有一个扎实的理解,并能有效地将其应用于构建现实世界的图像分类器。
在本章中,我们将涵盖以下主题:
-
转移学习简介
-
转移学习的类型
-
使用转移学习构建现实世界的图像分类器
技术要求
我们将使用 Google Colab 进行编码练习,这需要 python >= 3.8.0,并且需要以下软件包,可以通过 pip install 命令进行安装:
-
tensorflow>=2.7.0 -
os -
matplotlib >=3.4.0 -
pathlib
本书的代码包可以通过以下 GitHub 链接获取:github.com/PacktPublishing/TensorFlow-Developer-Certificate。所有习题的解答也可以在该 GitHub 仓库中找到。
转移学习简介
作为人类,我们很容易将从一个任务或活动中获得的知识转移到另一个任务中。例如,如果你精通 Python(编程语言,不是蛇),并且决定学习 Rust,凭借你在 Python 上的背景知识,你会发现学习 Rust 比没有任何编程语言基础的人要容易得多。这是因为一些概念,如面向对象编程,在不同的编程语言中有相似之处。转移学习遵循相同的原理。
迁移学习是一种技术,我们利用在任务 A上预训练的模型来解决一个不同但相关的任务 B。例如,我们使用在一个任务上训练的神经网络,并将获得的知识转移到多个相关任务中。在图像分类中,我们通常使用已经在非常大的数据集上训练的深度学习模型,比如 ImageNet,它由超过 1,000,000 张图像组成,涵盖 1,000 个类别。这些预训练模型获得的知识可以应用于许多不同的任务,例如在照片中分类不同品种的狗。就像我们因为懂得 Python 而能更快地学习 Rust 一样,迁移学习也适用——预训练的模型可以利用从源任务中获得的信息,并将其应用于目标任务,从而减少训练时间和对大量注释数据的需求,而这些数据可能在目标任务中不可得或难以收集。
迁移学习不仅限于图像分类任务;它还可以应用于其他深度学习任务,例如自然语言处理、语音识别和目标检测。在第十一章《使用 TensorFlow 进行 NLP》中,我们将把迁移学习应用于文本分类。在那里,我们将看到如何对在大规模文本语料库上训练的预训练模型(我们将从 TensorFlow Hub 获取)进行微调,从而进行文本分类。
在经典机器学习中,如在图 9.1(a)所示,我们为每个任务从头开始训练模型,正如我们在本书中迄今为止所做的那样。这种方法需要大量的资源和数据。

图 9.1 – 传统机器学习与迁移学习
然而,研究人员发现,模型可以通过学习视觉特征,从一个庞大的数据集(如 ImageNet)中学习低级特征,并将这些特征应用于一个新的、相关的任务,如图 9.1(b)所示——例如,在我们在第八章《处理过拟合》中使用的天气数据集的分类中。通过应用迁移学习,我们可以利用模型在大数据集上训练过程中获得的知识,并有效地将其适应于解决不同但相关的任务。这种方法被证明是有用的,因为它不仅节省了训练时间和资源,还学会了提高性能,即使在目标任务可用数据有限的情况下。
迁移学习的类型
我们可以在卷积神经网络(CNN)中通过两种主要方式应用迁移学习。首先,我们可以将预训练模型作为特征提取器。在这种情况下,我们冻结卷积层的权重,以保留源任务中获得的知识,并添加一个新的分类器,该分类器用于第二任务的分类。这是可行的,因为卷积层是可重用的,它们只学习了低级特征,如边缘、角落和纹理,这些是通用的并且适用于不同的图像,如图 9**.2所示,而全连接层则用于学习高级细节,这些细节用于在照片中分类不同的物体。

图 9.2 – 迁移学习作为特征提取器
迁移学习的第二种应用方法是解冻预训练模型的部分层,并添加一个分类器模型来识别高级特征,如图 9**.3所示。在这里,我们同时训练解冻的层和新的分类器。预训练模型作为新任务的起点,解冻层的权重与分类层一起微调,以使模型适应新任务。

图 9.3 – 迁移学习作为微调模型
预训练模型是已经在大数据集上训练过的深度网络。通过利用这些模型已经获得的知识和权重,我们可以将它们用作特征提取器,或通过较小的数据集和更少的训练时间对它们进行微调以适应我们的使用场景。迁移学习为机器学习实践者提供了访问最先进模型的途径,这些模型可以通过 TensorFlow 中的 API 快速、轻松地访问。这意味着我们不必总是从头开始训练我们的模型,从而节省时间和计算资源,因为微调模型比从头开始训练要快。
我们可以将预训练模型应用于相关的使用场景,从而可能提高准确性并加快收敛速度。然而,如果源领域和目标领域不相关,迁移学习可能不仅失败,还可能由于学习到的特征不相关,反而损害目标任务的性能,这种情况称为负迁移。让我们将迁移学习应用于一个真实世界的图像分类任务。我们将探索一些表现最好的预训练模型,如 VGG、Inception、MobileNetV2 和 EfficientNet。这些模型已经为图像分类任务进行了预训练。让我们看看它们在给定任务中的表现如何。
使用迁移学习构建一个真实世界的图像分类器
在这个案例研究中,你的公司获得了一个医疗项目,你被指派负责为 GETWELLAI 构建一个肺炎分类器。你已经获得了超过 5000 张 X 射线 JPEG 图像,包含两类(肺炎和正常)。数据集由专业医生标注,低质量的图像已被移除。让我们看看如何使用我们迄今为止讨论的两种迁移学习技术来解决这个问题。
加载数据
执行以下步骤以加载数据:
-
和往常一样,我们首先加载我们项目所需的必要库:
#Import necessary librariesimport osimport pathlibimport matplotlib.pyplot as pltimport matplotlib.image as mpimgimport randomimport numpy as npfrom PIL import Imageimport pandas as pdimport tensorflow as tffrom tensorflow import kerasfrom tensorflow.keras.preprocessing.image import ImageDataGeneratorfrom tensorflow.keras.callbacks import EarlyStoppingfrom tensorflow.keras import regularizer -
接下来,让我们加载 X 射线数据集。为此,我们将使用
wget命令从指定的 URL 下载文件:!wget https://storage.googleapis.com/x_ray_dataset/dataset.zip -
下载的文件作为 ZIP 文件保存在我们 Colab 实例的当前工作目录中,文件中包含 X 射线图像的数据集。
-
接下来,我们将通过运行以下代码来提取
zip文件夹的内容:!unzip dataset.zip
当我们运行代码时,我们将提取一个名为 dataset 的文件夹,其中包含 test、val 和 train 子目录,每个子目录中都有正常和肺炎 X 射线图像的数据,如 图 9.4 所示:

图 9.4 – 当前工作目录的快照,包含已提取的 ZIP 文件
-
我们将使用以下代码块来提取子目录及其中文件的数量。我们在 第八章**, 处理过拟合 中也看到了这段代码块:
root_dir = "/content/dataset"for dirpath, dirnames, filenames in os.walk(root_dir):print(f"Directory: {dirpath}")print(f"Number of images: {len(filenames)}")print()
它为我们提供了每个文件夹中数据的快照,并让我们对数据的分布有一个大致了解。
-
接下来,我们将使用
view_random_images函数从train目录显示一些随机图像及其形状:view_random_images(target_dir="/content/dataset/train",num_images=4)
当我们运行代码时,结果将类似于 图 9.5。

图 9.5 – 从 X 射线数据集的训练样本中随机显示的图像
-
我们将为训练数据和验证数据创建一个
ImageDataGenerator类的实例。我们将添加rescale参数来重新调整图像的大小,确保所有像素值都在 0 到 1 之间。这样做是为了提高稳定性,并增强训练过程中的收敛性。生成的train_datagen和valid_datagen对象分别用于生成训练数据和验证数据的批次:train_datagen = ImageDataGenerator(rescale=1./255)valid_datagen = ImageDataGenerator(rescale=1./255) -
接下来,我们设置
train、validation和test目录。# Set up the train and test directoriestrain_dir = "/content/dataset/train/"val_dir = "/content/dataset/val"test_dir = "/content/dataset/test" -
我们使用
flow_from_directory()方法从训练目录加载图像。target_size参数用于将所有图像调整为 224 x 224 像素。与我们在第八章**,过拟合处理中使用的代码相比,一个关键的不同是class_mode参数设置为binary,因为我们处理的是二分类问题(即正常和肺炎):train_data=train_datagen.flow_from_directory(train_dir,target_size=(224,224),# convert all images to be 224 x 224class_mode="binary")valid_data=valid_datagen.flow_from_directory(val_dir,target_size=(224,224),class_mode="binary",shuffle=False)test_data=valid_datagen.flow_from_directory(test_dir,target_size=(224,224),class_mode="binary",shuffle=False)
valid_data和test_data生成器与train_data生成器非常相似,因为它们的目标大小也设置为 224 x 224;关键区别在于它们将shuffle设置为false,这意味着图像不会被打乱。如果我们将其设置为true,图像将会被打乱。
建模
我们将从使用在第八章**,过拟合处理中应用的相同模型开始。为了避免重复,我们将专注于全连接层,在该层中,输出层有一个神经元,因为这是一个二分类任务。我们将与使用迁移学习的结果进行比较:
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(1050, activation="relu"),
tf.keras.layers.Dense(1, activation="sigmoid")
])
# Compile the model
model_1.compile(loss="binary_crossentropy",
optimizer=tf.keras.optimizers.Adam(),
metrics=["accuracy"])
#Fit the model
# Add an early stopping callback
callbacks = [tf.keras.callbacks.EarlyStopping(
monitor="val_accuracy", patience=3,
restore_best_weights=True)]
history_1 = model_1.fit(train_data,epochs=20,
validation_data=valid_data,
callbacks=[callbacks]
在这种情况下,输出层有一个神经元,并且我们将激活函数更改为 sigmoid 函数,因为我们正在构建一个二分类器。在编译步骤中,我们还将损失函数更改为二元交叉熵;其他部分保持不变。然后,我们进行模型拟合。
训练在第 7 个周期结束,因为验证损失未能进一步下降:
Epoch 4/20
163/163 [==============================] – 53s 324ms/step – loss: 0.0632 – accuracy: 0.9774 – val_loss: 0.0803 – val_accuracy: 1.0000
Epoch 5/20
163/163 [==============================] – 53s 324ms/step – loss: 0.0556 – accuracy: 0.9797 – val_loss: 0.0501 – val_accuracy: 1.0000
Epoch 6/20
163/163 [==============================] – 53s 323ms/step – loss: 0.0412 – accuracy: 0.9854 – val_loss: 0.1392 – val_accuracy: 0.8750
Epoch 7/20
163/163 [==============================] – 54s 334ms/step – loss: 0.0314 – accuracy: 0.9875 – val_loss: 0.2450 – val_accuracy: 0.8750
在第五个周期,模型达到了 100%的验证准确率,看起来很有希望。让我们评估一下模型:
model_1.evaluate(test_data)
当我们在测试数据上评估模型时,我们记录的准确率只有 0.7580。这表明模型可能存在过拟合的迹象。当然,我们可以尝试结合在第八章**,过拟合处理中学到的想法来提高模型的性能,并且鼓励你这么做。然而,让我们学习如何使用预训练模型,看看是否能够将这些模型获得的知识迁移到我们的应用场景中,并且如果可能的话,取得更好的结果。接下来我们就来做这个。
迁移学习建模
在本节中,我们将使用三种广泛应用的预训练 CNN 进行图像分类——VGG16、InceptionV3 和 MobileNet。我们将展示如何通过这些模型作为特征提取器应用迁移学习,接着添加一个全连接层进行标签分类。我们还将学习如何通过解冻部分层来微调预训练模型。在使用这些模型之前,我们需要导入它们。我们可以通过一行代码来实现:
from tensorflow.keras.applications import InceptionV3,
MobileNet, VGG16, ResNet50
现在我们已经有了模型并准备好开始,让我们从 VGG16 开始。
VGG16
VGG16 是由牛津大学视觉几何组开发的 CNN 架构。它是在 ImageNet 数据集上训练的。VGG16 架构在 2014 年 ImageNet 挑战赛的图像分类类别中获得了第二名。VGG16 由 13 个(3 x 3 卷积核)卷积层,5 个(2x2)最大池化层和 3 个全连接层组成,如图 9.6所示。这使我们得到了 16 层可学习参数;请记住,最大池化层用于降维,它们没有权重。该模型接收 224 x 224 RGB 图像的输入张量。

图 9.6 – VGG16 模型架构(来源:https://medium.com/analytics-vidhya/car-brand-classification-using-vgg16-transfer-learning-f219a0f09765)
让我们开始从 Keras 加载 VGG16。我们想加载该模型并使用从 ImageNet 数据集获得的预训练权重。为此,我们将weights参数设置为imagenet;我们还将include_top参数设置为false。这样做是因为我们想将该模型用作特征提取器。通过这种方式,我们可以添加自定义的全连接层用于分类。我们将输入大小设置为(224,224,3),因为这是 VGG16 期望的输入图像大小:
# Instantiate the VGG16 model
vgg16 = VGG16(weights='imagenet', include_top=False,
input_shape=(224, 224, 3))
下一步使我们能够冻结模型的权重,因为我们想要使用 VGG16 作为特征提取器。当我们冻结所有层时,这使它们变为不可训练,这意味着它们的权重在训练过程中不会更新:
# Freeze all layers in the VGG16 model
for layer in vgg16.layers:
layer.trainable = False
下一个代码块创建了一个新的顺序模型,使用 VGG 作为其顶层,然后我们添加一个由 1,024 个神经元的密集层、一个 Dropout 层和一个输出层(包含一个神经元)组成的全连接层,并将激活函数设置为 sigmoid,以进行二分类:
# Create a new model on top of VGG16
model_4 = tf.keras.models.Sequential()
model_4.add(vgg16)
model_4.add(tf.keras.layers.Flatten())
model_4.add(tf.keras.layers.Dense(1024, activation='relu'))
model_4.add(tf.keras.layers.Dropout(0.5))
model_4.add(tf.keras.layers.Dense(1, activation='sigmoid'))
我们编译并将模型拟合到数据上:
# Compile the model
model_4.compile(optimizer='adam',
loss='binary_crossentropy', metrics=['accuracy'])
# Fit the model
callbacks = [tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy', patience=3,
restore_best_weights=True)]
history_4 = model_4.fit(train_data,
epochs=20,
validation_data=valid_data,
callbacks=[callbacks]
)
在四个时期后,我们的模型停止了训练。它达到了 0.9810 的训练准确率,但在验证集上,我们得到了 0.875 的准确率:
Epoch 1/20
163/163 [==============================] - 63s 360ms/step - loss: 0.2737 - accuracy: 0.9375 - val_loss: 0.2021 - val_accuracy: 0.8750
Epoch 2/20
163/163 [==============================] - 57s 347ms/step - loss: 0.0818 - accuracy: 0.9699 - val_loss: 0.4443 - val_accuracy: 0.8750
Epoch 3/20
163/163 [==============================] - 56s 346ms/step - loss: 0.0595 - accuracy: 0.9774 - val_loss: 0.1896 - val_accuracy: 0.8750
Epoch 4/20
163/163 [==============================] - 58s 354ms/step - loss: 0.0556 - accuracy: 0.9810 - val_loss: 0.4209 - val_accuracy: 0.8750
当我们评估模型时,达到了 84.29 的准确率。现在,让我们使用另一个预训练模型作为特征提取器。
MobileNet
MobileNet 是由谷歌的工程师开发的轻量级 CNN 模型。该模型轻巧高效,是开发移动和嵌入式视觉应用的首选模型。与 VGG16 类似,MobileNet 也在 ImageNet 数据集上进行了训练,并能够取得最先进的结果。MobileNet 采用了一种简化的架构,利用了深度可分离卷积。其基本理念是在保持准确率的同时,减少训练所需的参数数量。
为了将 MobileNet 作为特征提取器,步骤与我们刚才使用 VGG16 时类似;因此,让我们来看一下代码块。我们将加载模型,冻结层,并像之前一样添加一个全连接层:
# Instantiate the MobileNet model
mobilenet = MobileNet(weights='imagenet',
include_top=False, input_shape=(224, 224, 3))
# Freeze all layers in the MobileNet model
for layer in mobilenet.layers:
layer.trainable = False
# Create a new model on top of MobileNet
model_10 = tf.keras.models.Sequential()
model_10.add(mobilenet)
model_10.add(tf.keras.layers.Flatten())
model_10.add(tf.keras.layers.Dense(1024,activation='relu'))
model_10.add(tf.keras.layers.Dropout(0.5))
model_10.add(tf.keras.layers.Dense(1,activation='sigmoid'))
接下来,我们编译并拟合模型:
# Compile the model
model_10.compile(optimizer='adam',
loss='binary_crossentropy', metrics=['accuracy'])
# Fit the model
callbacks = [tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy', patience=3,
restore_best_weights=True)]
history_10 = model_10.fit(train_data,
epochs=20,
validation_data=valid_data,
callbacks=[callbacks])
仅经过四个周期,模型达到了 87.50%的验证准确率:
Epoch 1/20
163/163 [==============================] - 55s 321ms/step - loss: 3.1179 - accuracy: 0.9402 - val_loss: 1.8479 - val_accuracy: 0.8750
Epoch 2/20
163/163 [==============================] - 51s 313ms/step - loss: 0.3896 - accuracy: 0.9737 - val_loss: 1.1031 - val_accuracy: 0.8750
Epoch 3/20
163/163 [==============================] - 52s 320ms/step - loss: 0.0795 - accuracy: 0.9896 - val_loss: 0.8590 - val_accuracy: 0.8750
Epoch 4/20
163/163 [==============================] - 52s 318ms/step - loss: 0.0764 - accuracy: 0.9877 - val_loss: 1.1536 - val_accuracy: 0.8750
接下来,让我们亲自尝试微调一个预训练模型。
将转移学习作为微调模型
InceptionV3 是 Google 开发的另一种 CNN 架构。它结合了 1x1 和 3x3 的滤波器,以捕捉图像的不同方面。让我们解冻一些层,这样我们就可以训练这些解冻的层以及全连接层。
首先,我们将加载 InceptionV3 模型。我们设置include_top=False以去除 InceptionV3 的分类层,并使用来自 ImageNet 的权重。我们通过将这些层的trainable设置为true来解冻最后 50 层。这使得我们能够在 X 光数据集上训练这些层:
# Load the InceptionV3 model
inception = InceptionV3(weights='imagenet',
include_top=False, input_shape=(224, 224, 3))
# Unfreeze the last 50 layers of the InceptionV3 model
for layer in inception.layers[-50:]:
layer.trainable = True
注意:
在小数据集上解冻和微调过多的层并不是一个好策略,因为这可能导致过拟合。
我们将像之前那样创建、拟合和编译模型,并且新模型在第五个周期达到了 100%的验证准确率:
Epoch 5/10
163/163 [==============================] - 120s 736ms/step - loss: 0.1168 - accuracy: 0.9584 - val_loss: 0.1150 - val_accuracy: 1.0000
Epoch 6/10
163/163 [==============================] - 117s 716ms/step - loss: 0.1098 - accuracy: 0.9624 - val_loss: 0.2713 - val_accuracy: 0.8125
Epoch 7/10
163/163 [==============================] - 123s 754ms/step - loss: 0.1011 - accuracy: 0.9613 - val_loss: 0.2765 - val_accuracy: 0.7500
Epoch 8/10
163/163 [==============================] - 120s 733ms/step - loss: 0.0913 - accuracy: 0.9668 - val_loss: 0.2711 - val_accuracy: 0.8125
接下来,让我们使用evaluate_models辅助函数评估这些模型:

图 9.7 – 我们实验的评估结果
从图 9.7的结果来看,MobileNet、VGG16 和 InceptionV3 表现最佳。我们可以看到这些模型的表现远远超过了我们的基准模型(模型 1)。我们还报告了来自笔记本的其他一些模型的结果。我们能观察到过拟合的迹象;因此,你可以结合我们在第八章中讨论的处理过拟合的部分来改进结果。
总结
转移学习在深度学习社区中获得了广泛关注,因为它在构建深度学习模型时提高了性能、速度和准确度。我们讨论了转移学习的原理,并探索了将转移学习作为特征提取器和微调模型。我们使用表现最好的预训练模型构建了一些解决方案,并看到它们在应用于 X 光数据集时超越了我们的基准模型。
到目前为止,你应该已经对转移学习及其应用有了扎实的理解。掌握了这些知识后,你应该能够在为各种任务构建现实世界的深度学习解决方案时,将转移学习应用于特征提取器或微调模型。
到此,我们已结束本章及本书的这一部分。在下一章中,我们将讨论自然语言处理(NLP),届时我们将使用 TensorFlow 构建令人兴奋的 NLP 应用。
问题
让我们测试一下本章所学内容:
-
使用测试笔记本,加载猫狗数据集。
-
使用图像数据生成器对图像数据进行预处理。
-
使用 VGG16 模型作为特征提取器,并构建一个新的 CNN 模型。
-
解冻 InceptionV3 模型的 40 层,并构建一个新的 CNN 模型。
-
评估 VGG16 和 InceptionV3 模型。
进一步阅读
要了解更多内容,您可以查看以下资源:
-
Kapoor, A., Gulli, A. 和 Pal, S. (2020) 与 TensorFlow 和 Keras 的深度学习第三版:构建和部署监督学习、无监督学习、深度学习和强化学习模型。Packt Publishing Ltd.
-
将深度卷积神经网络适应于迁移学习:一项比较研究,由 C. M. B. Al-Rfou、G. Alain 和 Y. Bengio 编写,发表于 arXiv 预印本 arXiv:1511。
-
非常深的卷积神经网络用于大规模图像识别,由 K. Simonyan 和 A. Zisserman 编写,发表于 2014 年 arXiv 预印本 arXiv:1409.1556。
-
EfficientNet:重新思考卷积神经网络模型缩放,由 M. Tan 和 Q. Le 编写,发表于 2019 年国际机器学习大会。
-
MobileNetV2:反向残差和线性瓶颈,由 M. Sandler、A. Howard、M. Zhu、A. Zhmoginov 和 L. Chen 编写,发表于 2018 年 arXiv 预印本 arXiv:1801.04381。
-
DeCAF:一种用于通用视觉识别的深度卷积激活特征,由 Donahue, J., Jia, Y., Vinyals, O., Hoffman, J., Zhang, N., Tzeng, E. 和 Darrell, T.(2014)编写。
-
利用迁移学习的力量进行医学图像分类,作者:Ryan Burke,Towards Data Science。
towardsdatascience.com/harnessing-the-power-of-transfer-learning-for-medical-image-classification-fd772054fdc7
第三部分 – 使用 TensorFlow 进行自然语言处理
在本部分中,您将学习如何使用 TensorFlow 构建自然语言处理(NLP)应用程序。您将了解如何进行文本处理,并构建文本分类模型。在本部分中,您还将学习如何使用 LSTM 生成文本。
本节包括以下章节:
-
第十章,自然语言处理简介
-
第十一章,使用 TensorFlow 进行 NLP
第十章:自然语言处理简介
今天,我们面临着来自各个方向的大量文本数据,无论是社交媒体平台上的内容、电子邮件通讯、短信,还是在线评论。文本数据的指数级增长,推动了基于文本的应用程序的快速发展,这些应用程序采用先进的深度学习技术,从文本数据中提取有价值的见解。我们正处在一个变革时代的黎明,这一进程得到了像谷歌、微软这样的科技巨头,以及 OpenAI、Anthropic 这样的革命性初创公司的推动。这些先驱者正在带领我们构建强大的解决方案,能够解决各种基于文本的挑战,例如总结大量文档、提取社交平台上的情感信息以及为博客文章生成文本——这类应用的清单几乎是无穷无尽的。
现实世界中的文本数据可能会很混乱;它可能充斥着不必要的信息,例如标点符号、特殊字符和一些常见词汇,这些词汇可能对文本的实际含义没有显著贡献。因此,我们将从一些基本的文本预处理步骤开始,帮助将文本数据转化为更易处理的形式,为建模做好准备。再次提问,你可能会想,这些机器是如何学习理解文本的?它们是如何理解单词和句子,甚至是掌握单词的语义含义或使用的上下文?在本章中,我们将探讨自然语言处理(NLP)的基础知识。我们将探讨一些概念,比如分词,它是处理如何将文本分割成单个单词或术语(词元)。我们还将探讨词嵌入的概念——在这里,我们将看到它们如何帮助模型捕捉单词之间的含义、上下文和关系。
然后,我们将把本章中学到的所有知识结合起来,构建一个情感分析模型,使用 Yelp Polarity 数据集对客户评论进行分类。作为一项互动活动,我们将探讨如何在 TensorFlow 中可视化词嵌入;这有助于快速了解模型如何理解和表示不同的单词。我们还将探讨各种技术来提高情感分析分类器的性能。通过本章的学习,你将对如何预处理和建模文本数据有一个良好的基础理解,并掌握解决实际 NLP 问题所需的技能。
在本章中,我们将覆盖以下主题:
-
文本预处理
-
构建情感分类器
-
嵌入可视化
-
模型改进
文本预处理
自然语言处理(NLP)是一个激动人心且不断发展的领域,位于计算机科学和语言学的交汇点。它赋予计算机理解、分析、解释和生成文本数据的能力。然而,处理文本数据提出了独特的挑战,这与我们在本书早期章节中处理的表格数据和图像数据有所不同。图 10.1为我们提供了文本数据所面临的一些固有挑战的高层次概览。让我们深入探讨这些问题,看看它们在构建文本数据的深度学习模型时如何成为问题。

图 10.1 – 文本数据所面临的挑战
文本数据以其自然形态是非结构化的,这只是我们将在本章中处理的这一有趣数据类型独特性的开始。通过对比这两句话——“我们家旁边的房子很美丽”和“我们邻居的房子是这个地区每个人都赞赏的”,我们可以说明一些问题。两者传达的情感相似,但它们的结构不同,长度也各异。对于人类来说,这种缺乏结构和长度变化并不成问题,但当我们使用深度学习模型时,这可能会成为一个挑战。为了解决这些挑战,我们可以考虑像标记化这样的思想,它指的是将文本数据拆分成更小的单元,称为标记。我们可以用这些标记表示单词、子单词或个别字符。为了处理文本数据长度的变化以便建模,我们将重用一个在处理图像数据时使用的老方法——填充。通过填充句子,我们可以确保数据(如句子或段落)的长度一致。这种一致性使得数据对我们的模型更易于消化。
再次,我们可能会遇到具有多重含义的单词,解码这些单词的含义在很大程度上取决于它们使用的上下文。例如,如果我们看到一句话是“我会在银行待着”,没有额外的上下文,很难判断银行是指金融银行还是河岸。像这样的词语增加了在建模文本数据时的复杂性。为了解决这个问题,我们需要应用能够捕捉单词及其周围单词本质的技术。一个很好的例子就是词嵌入。词嵌入是强大的向量表示,用于捕捉单词的语义含义,使得具有相似意义或上下文的单词具有相似的表示形式。
我们在处理文本数据时可能会遇到的问题包括拼写错误、拼写变异和噪音。为了解决这些问题,我们可以使用噪音过滤技术,在收集在线数据时过滤掉网址、特殊字符和其他无关的实体。假设我们有一个示例句子——“Max 喜欢在伦敦乡村俱乐部打棋,而且他是我们街上最棒的高尔夫球手。”当我们检查这个句子时,会发现它包含了一些常见的词语,如and、is和the。尽管这些词语对语言的连贯性是必要的,但在某些情况下,它们可能并不增加语义价值。如果是这种情况,我们可能会希望去除这些词语,以降低数据的维度。
现在我们已经涵盖了一些关于文本数据挑战的基础知识,让我们来看看如何预处理文本数据,了解如何从维基百科中提取和清洗机器学习的文本数据。在这里,我们将看到如何应用 TensorFlow 执行诸如分词、填充以及使用词嵌入等技术,从文本中提取意义。要访问示例数据,可以使用这个链接:en.wikipedia.org/wiki/Machine_learning。让我们开始吧:
-
我们将首先导入必要的库:
import requestsfrom bs4 import BeautifulSoupimport refrom tensorflow.keras.preprocessing.text import Tokenizer我们使用这些库来有效地获取、预处理和分词网络数据,为神经网络建模做准备。当我们想要访问互联网上的数据时,
requests库可以证明是一个有用的工具,它使我们能够通过向网络服务器发出请求并获取网页数据,简化从网页获取信息的过程。收集到的数据通常是 HTML 格式的,这种格式并不适合我们直接输入到模型中。这时,BeautifulSoup(一个直观的 HTML 和 XML 解析工具)就派上用场了,它帮助我们轻松浏览和访问所需的内容。为了执行字符串操作、文本清洗或提取模式,我们可以使用re模块。我们还从 TensorFlow 的 Keras API 中导入了Tokenizer类,它使我们能够执行分词操作,从而将数据转换为适合模型的格式。 -
然后,我们将我们的变量分配给我们想要抓取的网页;在这种情况下,我们感兴趣的是抓取维基百科关于机器学习的页面数据:
# Define the URL of the pageurl = "https://en.wikipedia.org/wiki/Machine_learning"# Send a GET request to the webpageresponse = requests.get(url)我们使用
GET方法从网络服务器获取数据。服务器回复一个状态码,告诉我们请求是成功还是失败。它还会返回其他元数据以及网页的 HTML 内容——在我们这个例子中,是维基百科页面。我们将服务器对GET请求的响应保存在response对象中。 -
我们使用
BeautifulSoup类来完成解析 HTML 内容的繁重工作,我们通过response.content来访问这些内容:# Parse the HTML content of the page with BeautifulSoupsoup = BeautifulSoup(response.content, 'html.parser')在这里,我们将
response.content中包含的原始 HTML 内容转换为可消化的格式,指定html.parser,并将结果存储在soup变量中。 -
现在,让我们提取段落中的所有文本内容:
# Extract the text from all paragraph tags on the pagepassage = " ".join([p.text for p in soup.find_all('p')])我们使用
soup.find_all('p')提取存储在soup变量中的所有段落。然后,我们应用join方法将它们合并成一段文本,每个段落之间用空格分隔,最后将这段文本存储在passage变量中。 -
下一步是从数据中删除停用词。停用词是指在某些使用场景下可能不包含有用信息的常见单词。因此,我们可能希望删除它们,以帮助减少数据的维度,特别是在这些高频词对任务贡献较小的情况下,比如信息检索或文档聚类。在这里,删除停用词有助于加速收敛并产生更好的分类结果。停用词的例子包括“and”、“the”、“in”和“is”:
# Define a simple list of stopwordsstopwords = ["i", "me", "my", "myself", "we", "our","ours", "ourselves", "you", "your","yours", "yourself", "yourselves", "he","him", "his", "himself", "she", "her","hers", "herself", "it", "its", "itself","they", "them", "their", "theirs","themselves", "what", "which", "who","whom", "this", "that", "these", "those","am", "is", "are", "was", "were", "be","been", "being", "have", "has", "had","having", "do", "does", "did", "doing","a", "an", "the", "and", "but", "if","or", "because", "as", "until", "while","of", "at", "by", "for", "with", "about","against", "between", "into", "through","during", "before", "after", "above","below", "to", "from", "up", "down","in", "out", "on", "off", "over","under", "again", "further", "then","once", "here", "there", "when", "where","why", "how", "all", "any", "both","each", "few", "more", "most", "other","some", "such", "no", "nor", "not","only", "own", "same", "so", "than","too", "very", "s", "t", "can", "will","just", "don", "should", "now"]我们已经定义了一个停用词列表。这样,我们就可以灵活地将自己选择的单词添加到这个列表中。当处理领域特定的项目时,这种方法非常有用,因为你可能需要扩展停用词列表。
注意
这一步骤可能并不总是有益的。在一些自然语言处理任务中,停用词可能包含有用的信息。例如,在文本生成或机器翻译中,模型需要生成/翻译停用词,以便产生连贯的句子。
-
让我们将整段文字转换为小写字母。这样做是为了确保语义相同的单词不会有不同的表示——例如,“DOG”和“dog”。通过确保我们的数据都是小写字母,我们为数据集引入了一致性,避免了同一单词重复表示的可能性。为了将我们的文本转换为小写字母,我们使用以下代码:
passage = passage.lower()当我们运行代码时,它会将所有的文本数据转换为小写字母。
注意
将一段文字转换为小写字母并不总是最好的解决方案。实际上,在一些使用场景中,例如情感分析,将文字转换为小写字母可能会导致信息丢失,因为大写字母通常用来表达强烈的情感。
-
接下来,让我们从我们从 Wikipedia 收集到的段落中删除 HTML 标签、特殊字符和停用词。为此,我们将使用以下代码:
# Remove HTML tags using regexpassage = re.sub(r'<[^>]+>', '', passage)# Remove unwanted special characterspassage = re.sub('[^a-zA-Z\s]', '', passage)# Remove stopwordspassage = ' '.join(word for word in passage.split() if word not in stopwords)# Print the cleaned passageprint(passage[:500]) # print only first 500 characters for brevity在第一行代码中,我们删除了 HTML 标签,接着删除数据中不需要的特殊字符。然后,我们通过停用词过滤器检查并删除停用词列表中的单词,之后将剩余的单词组合成一段文字,单词之间用空格隔开。我们打印前 500 个字符,以便了解我们处理过的文本是什么样的。
-
让我们打印前 500 个字符,并与图 10.2中的网页进行比较:
machine learning ml field devoted understanding building methods let machines learn methods leverage data improve computer performance set tasks machine learning algorithms build model based sample data known training data order make predictions decisions without explicitly programmed machine learning algorithms used wide variety applications medicine email filtering speech recognition agriculture computer vision difficult unfeasible develop conventional algorithms perform needed tasks subset ma当我们将输出与网页进行比较时,我们可以看到我们的文本都是小写字母,并且所有的停用词、特殊字符和 HTML 标签都已被删除。

图 10.2 – 机器学习的维基百科页面截图
在这里,我们探索了使用神经网络建模时准备文本数据的一些简单步骤。现在,让我们通过探索分词来扩展我们的学习。
分词
我们已经了解了一些关于如何预处理现实世界文本数据的重要概念。接下来的步骤是制定准备文本模型的策略。为此,让我们从分词的概念入手,分词意味着将句子拆分成更小的单位,称为“词元(tokens)”。分词可以在字符、子词、词语,甚至是句子层面进行。常见的是使用基于词的分词器;然而,选择哪种分词器主要取决于具体的使用场景。
让我们看看如何应用分词到句子上。假设我们有这样一句话 – “I like playing chess in my leisure time。”应用基于词的分词会得到如下输出:["i", "like", "playing", "chess", "in", "my", "leisure", "time"],而如果我们决定使用字符级分词,输出会是:['I', ' ', 'l', 'i', 'k', 'e', ' ', 'p', 'l', 'a', 'y', 'i', 'n', 'g', ' ', 'c', 'h', 'e', 's', 's', ' ', 'i', 'n', ' ', 'm', 'y', ' ', 'l', 'e', 'i', 's', 'u', 'r', 'e', ' ', 't', 'i', 'm', 'e']。在基于词的分词中,我们按单词拆分,而在字符级分词中,我们按字符拆分。你还可以看到,在字符级分词中,句子中的空格也被包括在内。对于子词和句子级的分词,我们分别按子词和句子进行拆分。现在,让我们用 TensorFlow 来实现基于词和字符的分词。
基于词的分词
让我们看看如何使用 TensorFlow 执行基于词的分词:
-
我们将从导入
Tokenizer类开始:from tensorflow.keras.preprocessing.text import Tokenizer -
接着,我们创建一个变量
text来存储我们的示例句子("Machine learning is fascinating. It is a field full of challenges!")。我们创建Tokenizer类的实例来处理我们示例句子的分词:text = "Machine learning is fascinating. It is a field full of challenges!"# Define the tokenizer and fit it on the texttokenizer = Tokenizer()tokenizer.fit_on_texts([text])我们可以根据使用场景向
tokenizer类传递多个参数。例如,我们可以使用num_words来设置希望保留的最大词数。此外,我们可能希望将整个文本转换为小写字母;这可以通过tokenizer类来实现。然而,如果不指定这些参数,TensorFlow 会应用默认参数。接着,我们使用fit_on_text方法来拟合我们的文本数据。fit_on_text方法会遍历输入文本,并创建一个由唯一单词组成的词汇表,同时计算每个单词在输入文本中的出现次数。 -
要查看单词到整数值的映射,我们使用
tokenizer对象的word_index属性:# Print out the word index to see how words are tokenizedprint(tokenizer.word_index)当我们打印结果时,我们可以看到
word_index返回一个键值对字典,其中每个键值对对应一个唯一单词及其在标记器词汇表中的整数索引:{'is': 1, 'machine': 2, 'learning': 3, 'fascinating': 4, 'it': 5, 'a': 6, 'field': 7, 'full': 8, 'of': 9, 'challenges': 10}
你可以看到我们示例句子中的感叹号已经消失,且单词 'is' 只列出了一次。此外,你可以看到我们的索引是从 1 开始的,而不是从 0,因为 0 被保留为一个特殊标记,我们稍后会遇到它。现在,让我们来看看如何进行字符级别的标记化。
字符级别标记化
在字符级别标记化中,我们根据每个字符拆分示例文本。为了使用 TensorFlow 完成此操作,我们稍微修改了用于单词级别标记化的代码:
# Define the tokenizer and fit it on the text
tokenizer = Tokenizer(char_level=True)
tokenizer.fit_on_texts([text])
# Print out the character index to see how characters are tokenized
print(tokenizer.word_index)
在这里,当我们创建 tokenizer 实例时,我们将 char_level 参数设置为 True。这样,我们可以看到文本中的唯一字符将被作为独立标记处理:
{' ': 1, 'i': 2, 'a': 3, 'n': 4, 'l': 5, 'e': 6, 's': 7,
'f': 8, 'c': 9, 'g': 10, 'h': 11, 't': 12, 'm': 13,
'r': 14, '.': 15, 'd': 16, 'u': 17, 'o': 18, '!': 19}
请注意,在这种情况下,每个唯一字符都会被表示,包括空格(' '),它的标记值是 1,句号('.')的标记值是 15,以及感叹号('!')的标记值是 19。
我们讨论的另一种标记化方式是子词标记化。子词标记化涉及将单词分解为常见字符组合,例如,“unhappiness” 可能被标记为 [“un”, “happiness”]。一旦文本被标记化,每个标记可以使用我们将在本章讨论的编码方法之一转换为数值表示。现在,让我们看看另一个叫做序列化的概念。
序列化
单词在句子中的使用顺序对于理解它们所传达的意义至关重要;序列化是将句子或一组单词或标记转换为它们的数值表示的过程,在使用神经网络构建自然语言处理应用时,能够保留单词的顺序。在 TensorFlow 中,我们可以使用 texts_to_sequences 函数将我们标记化的文本转换为整数序列。从我们单词级别标记化步骤的输出中,我们现在知道我们的示例句子(`"机器学习很迷人。它是一个充满挑战的领域!")可以通过一系列标记来表示:
# Convert the text to sequences
sequence = tokenizer.texts_to_sequences([text])
print(sequence)
通过将文本转换为序列,我们将人类可读的文本转换为机器可读的格式,同时保留单词出现的顺序。当我们打印结果时,我们会得到如下输出:
[[2, 3, 1, 4, 5, 1, 6, 7, 8, 9, 10]]
打印出的结果是表示原始文本的整数序列。在许多现实世界的场景中,我们需要处理不同长度的句子。虽然人类可以理解不同长度的句子,但神经网络要求我们将数据以特定类型的输入格式提供。在本书的图像分类部分,我们在传递图像数据作为输入时使用了固定的宽度和高度;对于文本数据,我们必须确保所有的句子长度相同。为了实现这一点,让我们回到我们在第七章,《卷积神经网络的图像分类》中讨论过的一个概念——填充,并看看如何利用它来解决句子长度不一致的问题。
填充
在第七章,《卷积神经网络的图像分类》中,我们在讨论 CNN 时介绍了填充的概念。在 NLP 的背景下,填充是向序列中添加元素以确保它达到所需长度的过程。为了在 TensorFlow 中实现这一点,我们使用keras预处理模块中的pad_sequences函数。让我们通过一个示例来解释如何将填充应用于文本数据:
-
假设我们有以下四个句子:
sentences = ["I love reading books.","The cat sat on the mat.","It's a beautiful day outside!","Have you done your homework?"]当我们执行词级标记化和序列化时,输出将如下所示:
[[2, 3, 4, 5], [1, 6, 7, 8, 1, 9],[10, 11, 12, 13, 14], [15, 16, 17, 18, 19]]我们可以看到,返回的序列长度各不相同,第二个句子比其他句子长。接下来,让我们通过填充来解决这个问题。
-
我们从 TensorFlow Keras 预处理模块中导入
pad_sequences:from tensorflow.keras.preprocessing.sequence import pad_sequencespad_sequences函数接受多个参数——在这里,我们将讨论一些重要的参数,如sequences、maxlen、truncating和padding。 -
让我们开始仅传递序列作为参数,并观察结果是什么样的:
padded = pad_sequences(sequences)print(padded)当我们使用
pad_sequences函数时,它会确保所有的句子与我们最长的句子长度相同。为了实现这一点,使用一个特殊的符号(0)来填充较短的句子,直到它们与最长句子的长度相同。这个特殊符号(0)没有任何意义,模型会在训练和推理过程中忽略它们:[[ 0 0 2 3 4 5][ 1 6 7 8 1 9][ 0 10 11 12 13 14][ 0 15 16 17 18 19]]从输出中,我们看到每隔一个句子就会在其前面添加零,直到它的长度与我们最长的句子(第二个句子)相同,而第二个句子有最长的序列。请注意,所有的零都是在每个句子的开头添加的。这个场景被称为
padding=post参数:# post paddingpadded_sequences = pad_sequences(sequences,padding='post')print(padded_sequences)当我们打印结果时,我们得到以下输出:
[[ 2 3 4 5 0 0][ 1 6 7 8 1 9][10 11 12 13 14 0][15 16 17 18 19 0]]在这种情况下,我们可以看到零被添加到较短句子的末尾。
-
另一个有用的参数是
maxlen。它用于指定我们希望保留的所有序列的最大长度。在这种情况下,任何超过指定maxlen的序列都会被截断。为了看看maxlen如何工作,让我们给我们的句子列表添加另一个句子:Sentences = ["I love reading books.","The cat sat on the mat.","It's a beautiful day outside!","Have you done your homework?","Machine Learning is a very interesting subject that enablesyou build amazing solutions beyond your imagination."] -
我们拿到新的句子列表,并对它们进行标记化和序列化处理。然后,我们填充数值表示,确保输入数据的长度一致,并确保我们的特殊标记(
0)出现在句子的末尾,我们将padding设置为post。当我们实施这些步骤时,输出结果如下:[[ 5 6 7 8 0 0 0 0 0 0 0 0 0 0 0 0][ 1 9 10 11 1 12 0 0 0 0 0 0 0 0 0 0][13 2 14 15 16 0 0 0 0 0 0 0 0 0 0 0][17 3 18 4 19 0 0 0 0 0 0 0 0 0 0 0][20 21 22 2 23 24 25 26 27 3 28 29 30 31 4 32]]从输出结果可以看到,其中一句话相当长,而其他句子大多由许多零组成的数字表示。在这种情况下,很明显我们的最长句子是一个异常值,因为其他句子都要短得多。这可能会扭曲我们模型的学习过程,并且还会增加对计算资源的需求,尤其是在处理大数据集或有限计算资源时。为了解决这个问题,我们应用了
maxlen参数。使用合适的max_length值非常重要,否则,我们可能会因截断而丢失数据中的重要信息。最好将最大长度设置得足够长,以捕捉有用信息,同时不增加太多噪声。 -
让我们看看如何在示例中应用
maxlen。我们首先将max_length变量设置为10。这意味着它最多只会使用 10 个标记。我们传递maxlen参数并打印我们的填充序列:# Define the max lengthmax_length = 10# Pad the sequencespadded = pad_sequences(sequences, padding='post',maxlen=max_length)print(padded))我们的结果生成了一个更短的序列:
[[ 5 6 7 8 0 0 0 0 0 0][ 1 9 10 11 1 12 0 0 0 0][13 2 14 15 16 0 0 0 0 0][17 3 18 4 19 0 0 0 0 0][25 26 27 3 28 29 30 31 4 32]]请注意,我们的最长句子已经在序列的开头被截断。如果我们想将句子截断在结尾呢?我们该如何实现呢?为此,我们引入了另一个参数,叫做
truncating,并将其设置为post:# Pad the sequencespadded = pad_sequences(sequences, padding='post',truncating='post', maxlen=max_length)print(padded)我们的结果将如下所示:
[[ 5 6 7 8 0 0 0 0 0 0][ 1 9 10 11 1 12 0 0 0 0][13 2 14 15 16 0 0 0 0 0][17 3 18 4 19 0 0 0 0 0][20 21 22 2 23 24 25 26 27 3]]
现在,我们已经完成了所有序列的填充和在句子末尾进行的截断。那么,如果我们训练模型来使用这五个句子进行文本分类,并且我们想对一个新的句子(“I love playing chess”)进行预测呢?记住,在训练句子中,我们的模型将有标记表示“I”和“love”。然而,它没有办法知道或表示“playing”和“Chess”。这给我们带来了另一个问题。让我们看看如何解决这个问题。
词汇表外
到目前为止,我们已经看到如何准备数据,从构成句子的文本数据序列转换为数值表示,来训练我们的模型。现在,假设我们构建一个文本分类模型,使用我们句子列表中的五个句子进行训练。当然,这只是一个假设的情况,即使我们用大量文本数据进行训练,最终也会遇到模型在训练时未见过的单词,正如我们在这个示例测试句子(“I love playing chess”)中看到的那样。这意味着我们必须准备好让模型处理那些不在我们预定义词汇表中的单词。为了解决这个问题,我们在标记化过程中使用了oov_token="<OOV>"参数:
# Define the tokenizer with an OOV token
tokenizer = Tokenizer(oov_token="<OOV>")
# Fit the tokenizer on the texts
tokenizer.fit_on_texts(sentences)
# Convert the texts to sequences
sequences = tokenizer.texts_to_sequences(sentences)
# Let's look at the word index
print(tokenizer.word_index)
在这之后,我们在训练句子上拟合分词器,将句子转换为整数序列,然后我们打印出单词索引:
{'<OOV>': 1, 'the': 2, 'a': 3, 'you': 4, 'your': 5, 'i': 6, 'love': 7, 'reading': 8, 'books': 9, 'cat': 10, 'sat': 11, 'on': 12, 'mat': 13, "it's": 14, 'beautiful': 15, 'day': 16, 'outside': 17, 'have': 18, 'done': 19, 'homework': 20, 'machine': 21, 'learning': 22, 'is': 23, 'very': 24, 'interesting': 25, 'subject': 26, 'that': 27, 'enables': 28, 'build': 29, 'amazing': 30, 'solutions': 31, 'beyond': 32, 'imagination': 33}
现在,我们可以看到"<OOV>"字符串被选中来表示这些 OOV 单词,并且它的值为1。这个标记将处理模型遇到的任何未知单词。让我们用我们的示例测试句子来看看实际效果:
# Now let's convert a sentence with some OOV words
test_sentence = "I love playing chess"
test_sequence = tokenizer.texts_to_sequences(
[test_sentence])
print(test_sequence)
我们传入我们的测试句子("I love playing chess"),其中包含了模型之前未见过的单词,然后使用texts_to_sequences方法将测试句子转换为一个序列。由于我们在训练句子上拟合了分词器,它会用单词索引中相应的数字表示来替换测试句子中的每个单词。然而,像“playing”和“chess”这样在我们的训练句子中未出现过的单词,将会被替换为特殊 OOV 标记的索引;因此,print语句返回如下内容:
[[6, 7, 1, 1]]
在这里,1的标记值被用于playing和chess这两个单词。使用 OOV 标记是自然语言处理(NLP)中处理训练数据中未出现但可能出现在测试数据或现实世界数据中的单词的常见做法。
现在,我们的文本数据已经转化为数值表示。同时,我们也保留了单词出现的顺序;然而,我们需要找到一种方法来捕捉单词的语义意义及其相互之间的关系。为此,我们使用词嵌入。接下来我们将讨论词嵌入。
词嵌入
在 NLP 领域,一个重要的里程碑就是词嵌入的使用。通过词嵌入,我们能够解决许多复杂的现代文本问题。词嵌入是一种单词表示方式,使得具有相似意义的单词能够有相似的表示,并且能够捕捉到单词使用的上下文。除了上下文,词嵌入还能够捕捉单词之间的语义和句法相似性,以及单词与其他单词之间的关系。这使得机器学习模型在使用词嵌入时,能够比将单词作为独立输入时更好地进行泛化。
像独热编码(one-hot encoding)这样的策略被证明是低效的,因为它会构建一个稀疏的单词表示,主要由零组成。这是因为我们词汇量越大,应用独热编码时生成的向量中零的数量就会越多。相反,词嵌入是一种密集的向量表示,它处在一个连续的空间中,能够使用稠密且低维的向量捕捉单词的意义、上下文及其之间的关系。
让我们来看一下以下几个示例句子,看看词嵌入是如何工作的:
-
她喜欢阅读书籍。
-
他喜欢阅读报纸。
-
他们在吃葡萄。
我们首先对每个句子进行分词,并应用序列化,将每个句子转换成一个整数序列:
-
[1, 2, 3, 4]
-
[5, 6, 3, 7]
-
[8, 9, 10, 11]
请注意,通过我们返回的序列,我们成功地捕捉到了构成每个句子的单词出现的顺序。然而,这种方法未能考虑单词的含义或单词之间的关系。例如,“enjoy”和“likes”这两个词在句子 1 和句子 2 中都表现出积极的情感,同时两个句子都有“reading”作为共同的动作。当我们设计深度学习模型时,我们希望它们能意识到“books”和“newspapers”之间的关系更为紧密,而与“grapes”和“eating”等词有很大不同,如图 10.3所示。

图 10.3 – 词嵌入
词嵌入使我们的模型能够捕捉单词之间的关系,从而增强了我们构建高性能模型的能力。我们已经探讨了一些关于文本预处理和数据准备的基础概念,将文本数据从单词转换为数值表示,同时捕捉语言中单词的顺序和潜在关系。现在,让我们将所学的知识整合起来,使用 Yelp 极性数据集构建一个情感分析模型。我们将从头开始训练自己的词嵌入,之后再将预训练的词嵌入应用到我们的应用案例中。
Yelp 极性数据集
在这个实验中,我们将使用 Yelp 极性数据集。该数据集由 56 万条训练评论和 3.8 万条测试评论组成,每条评论包含一个基于文本的评论和一个标签(正面 – 1 和负面 – 0)。这些数据来自于顾客对餐厅、美发店、锁匠等的评价。该数据集面临一些实际挑战——例如,评论的文本长度各异,从简短评论到非常长的评论都有。此外,数据中还包含了俚语和不同的方言。该数据集可以通过以下链接获取:www.tensorflow.org/datasets/catalog/yelp_polarity_reviews。
让我们开始构建我们的模型:
-
我们将从加载所需的库开始:
import tensorflow as tfimport tensorflow_datasets as tfdsfrom tensorflow.keras.preprocessing.text import Tokenizerfrom tensorflow.keras.preprocessing.sequence import pad_sequencesfrom sklearn.model_selection import train_test_splitimport numpy as npimport io我们导入必要的库来加载、拆分、预处理和可视化词嵌入,并使用 TensorFlow 对数据集进行建模,以便进行情感分析的应用。
-
然后,我们加载数据集:
# Load the Yelp Polarity Reviews dataset(train_dataset, test_dataset),dataset_info = tfds.load('yelp_polarity_reviews',split=['train', 'test'], shuffle_files=True,with_info=True, as_supervised=True)我们使用
tf.load函数从 TensorFlow 数据集中获取数据集。在这里,我们指定了我们的数据集,即 Yelp 极性评论数据集。我们还将数据拆分为训练集和测试集。通过将 shuffle 设置为True,我们对数据进行了洗牌,并设置with_info=True以确保能够检索数据集的元数据,这些信息可以通过dataset_info变量访问。我们还设置了as_supervised=True;当我们这样做时,它返回由输入和目标组成的元组,而不是字典。这样,我们就可以直接使用数据集与fit方法来训练我们的模型。现在,我们的训练数据集是train_dataset,测试集是test_dataset;这两个数据集都是tf.data.Dataset对象。在构建情感分析模型并在测试数据上评估之前,让我们先进行一些快速的数据探索。 -
让我们编写一些函数,以便探索我们的数据集:
def get_reviews(dataset, num_samples=5):reviews = []for text, label in dataset.take(num_samples):reviews.append((text.numpy().decode('utf-8'),label.numpy()))return reviewsdef dataset_insights(dataset, num_samples=2000):total_reviews = 0total_positive = 0total_negative = 0total_length = 0min_length = float('inf')max_length = 0for text, label in dataset.take(num_samples):total_reviews += 1review_length = len(text.numpy().decode('utf-8').split())total_length += review_lengthif review_length < min_length:min_length = review_lengthif review_length > max_length:max_length = review_lengthif label.numpy() == 1:total_positive += 1else:total_negative += 1avg_length = total_length / total_reviewsreturn min_length, max_length, avg_length,total_positive, total_negativedef plot_reviews(positive, negative):labels = ['Positive', 'Negative']counts = [positive, negative]plt.bar(labels, counts, color=['blue', 'red'])plt.xlabel('Review Type')plt.ylabel('Count')plt.title('Distribution of Reviews')plt.show()我们使用
get_reviews函数来查看训练集或测试集中的评论。此函数显示指定数量的评论及其相应的标签;默认情况下,它会显示前五个评论。然而,我们可以将该参数设置为任何我们想要的数字。第二个函数是dataset_insight函数——该函数执行多个分析操作,比如提取评论的最短、最长和平均长度。它还生成数据集中正面和负面评论的总数。由于我们处理的是一个大数据集,我们将dataset_insight设置为探索前 2,000 个样本。如果你增加样本数量,分析数据将需要更长的时间。我们将评论的正面和负面总数传递给plot_reviews函数,以便生成数据的图形分布。 -
让我们查看训练数据中的前七条评论:
# Check out some reviewsprint("Training Set Reviews:")train_reviews = get_reviews(train_dataset, 7)for review, label in train_reviews:print(f"Label: {label}, Review: {review[:100]}")当我们运行代码时,它返回前七条评论。此外,为了简洁起见,我们只返回每条评论的前 100 个字符:
Training Set Reviews:Label: 1, Review: The Groovy P. and I ventured to his old stomping grounds for lunch today. The '5 and Diner' on 16th...Label: 0, Review: Mediocre burgers - if you are in the area and want a fast food burger, Fatburger is a better bet th...Label: 0, Review: Not at all impressed...our server was not very happy to be there...food was very sub-par and it was ...Label: 0, Review: I wish I would have read Megan P's review before I decided to cancel my dinner reservations because ...Label: 1, Review: A large selection of food from all over the world. Great atmosphere and ambiance. Quality of food i...Label: 1, Review: I know, I know a review for Subway, come on. But I have to say that the service at this subway is t...Label: 1, Review: We came in for a pre-bachelor party madness meal and I have to say it was one of the best dining exp... -
让我们检查一下关于训练数据的一些重要统计信息:
min_length, max_length, avg_length, total_positive,total_negative = dataset_insights(train_dataset)# Display the resultsprint(f"Shortest Review Length: {min_length}")print(f"Longest Review Length: {max_length}")print(f"Average Review Length: {avg_length:.2f}")print(f"Total Positive Reviews: {total_positive}")print(f"Total Negative Reviews: {total_negative}")当我们运行代码时,它返回以下内容:
Shortest Review Length: 1Longest Review Length: 942Average Review Length: 131.53Total Positive Reviews: 1030Total Negative Reviews: 970 -
让我们绘制采样训练数据的分布图:
plot_reviews(total_positive, total_negative)在这里,我们调用
plot_reviews函数,并传入我们采样的训练数据中的正面和负面评论的总数。当我们运行代码时,我们会得到图中所示的图 10.4。

图 10.4 – 我们采样的训练数据中的评论分布
从采样的训练数据集中,我们可以看到评论分布非常均衡。因此,我们可以继续在此数据集上训练我们的模型。现在就让我们开始吧。
-
我们定义了标记化、序列化和训练过程中的关键参数:
# Define parametersvocab_size = 10000embedding_dim = 16max_length = 132trunc_type='post'padding_type='post'oov_tok = "<OOV>"num_epochs = 10# Build the Tokenizertokenizer = Tokenizer(num_words=vocab_size,oov_token=oov_tok)我们将词汇表的大小设置为 10,000。这意味着分词器将集中处理数据集中排名前 10,000 的单词。在选择词汇表大小时,需要在计算效率和捕捉数据集中存在的单词多样性之间找到平衡。如果增加词汇表的大小,我们可能会捕捉到更多细微的语言差异,从而丰富模型的理解,但这将需要更多的计算资源进行训练。此外,如果减少词汇表大小,训练将会更快;然而,我们将只能捕捉到数据集中语言变化的一小部分。
接下来,我们将嵌入维度设置为 16。这意味着每个单词将由一个 16 维的向量表示。嵌入维度的选择通常基于经验测试。在这里,我们选择 16 作为嵌入维度,主要是考虑到计算效率。如果我们使用更高的维度,比如 64 或 128,我们可能会捕捉到单词之间更加细微的关系;然而,这也需要更多的计算资源进行训练。在处理大规模数据集时,您可能希望使用更高的维度来提高性能。
我们将最大长度设置为 132 个单词;这个长度是基于我们在探索数据集中前 2,000 条评论时得到的平均单词长度来选择的。超过 132 个单词的评论将被截断,只保留前 132 个单词。我们选择这个最大长度,是为了在计算效率和捕捉数据集中大部分评论的最重要内容之间找到一个合理的平衡。我们将截断和
padding设置为post;这确保了较长的句子会在序列的末尾被截断,而较短的句子则会在序列的末尾用零进行填充。这里的关键假设是,大多数客户评论中的重要信息可能会出现在评论的开头部分。接下来,我们将 OOV(Out Of Vocabulary)标记设置为处理在测试集上可能出现但在训练期间模型未见过的 OOV 单词。设置这个参数可以防止模型在处理未见过的单词时发生错误。我们还将模型训练的 epoch 数量设置为 10。虽然我们使用 10 进行模型测试,但你可能希望训练更多的轮次,并且可以使用回调来在训练过程中监控模型在验证集上的表现。
在定义了所有参数之后,我们现在可以实例化我们的
Tokenizer类,并将num_words和oov_token作为参数传递进去。 -
为了减少处理时间,我们将使用 20,000 个样本进行训练:
# Fetch and decode the training datatrain_text = []train_label = []for example in train_dataset.take(20000):text, label = exampletrain_text.append(text.numpy().decode('utf-8'))train_label.append(label.numpy())# Convert labels to numpy arraytrain_labels = np.array(train_label)# Fit the tokenizer on the training textstokenizer.fit_on_texts(train_text)# Get the word index from the tokenizerword_index = tokenizer.word_index# Convert texts to sequencestrain_sequences = tokenizer.texts_to_sequences(train_text)在这里,我们用 Yelp Polarity 训练数据集中的前 20,000 个样本来训练模型。我们收集这些评论及其对应的标签,并且由于数据是以字节的形式存在,我们使用 UTF-8 编码来解码字符串,然后将文本及其标签分别添加到相应的列表中。我们使用 NumPy 将标签列表转换为便于操作的形式。之后,我们对所选训练数据进行分词,并将文本转换为序列。
-
为了测试,我们选择 8,000 个样本。我们在这里执行的步骤与训练集上的步骤非常相似;然而,我们并不对测试集中的文本进行拟合。此步骤仅用于训练目的,帮助神经网络学习训练集中的词到索引的映射:
# Fetch and decode the test datatest_text = []test_label = []for example in test_dataset.take(8000):text, label = exampletest_text.append(text.numpy().decode('utf-8'))test_label.append(label.numpy())# Convert labels to numpy arraytest_labels = np.array(test_label)# Convert texts to sequencestest_sequences = tokenizer.texts_to_sequences(test_text)我们从测试数据集中取出前 8,000 个样本。重要的是要使用与训练数据拟合时相同的分词器。这确保了分词器在训练过程中学到的词汇索引映射被应用到测试集上,并且训练集中未学习到的单词会被替换为 OOV(Out-Of-Vocabulary)标记。
-
下一步是对表示训练集和测试集中文本的整数序列进行填充和截断,确保它们具有相同的长度:
# Pad the sequencestrain_padded = pad_sequences(train_sequences,maxlen=max_length, padding=padding_type,truncating=trunc_type)test_padded = pad_sequences(test_sequences,maxlen=max_length, padding=padding_type,truncating=trunc_type)返回的输出
train_padded和test_padded是形状为(num_sequences和maxlen)的 NumPy 数组。现在,这些数组中的每个序列都具有相同的长度。 -
我们想要设置一个验证集,这将帮助我们跟踪模型训练过程的进展。为此,我们可以使用来自 scikit-learn 的
train_test_split函数:# Split the data into training and validation setstrain_padded, val_padded, train_labels,val_labels = train_test_split(train_padded,train_labels, test_size=0.2, random_state=42)在这里,我们将数据划分为训练集和验证集,其中 20% 用作验证集。
-
让我们继续构建我们的情感分析模型:
# Define the modelmodel = tf.keras.Sequential([tf.keras.layers.Embedding(vocab_size,embedding_dim, input_length=max_length),tf.keras.layers.GlobalAveragePooling1D(),tf.keras.layers.Dense(24, activation='relu'),tf.keras.layers.Dense(1, activation='sigmoid')# because it's binary classification])我们使用 TensorFlow 的 Keras API 构建我们的模型。请注意,我们有一个新的层——嵌入层,它用于在密集的向量空间中表示单词。此层接受词汇大小、嵌入维度和最大长度作为参数。在这个实验中,我们将词嵌入作为模型的一部分进行训练。我们也可以独立训练这一层,用于学习词嵌入。这在我们打算在多个模型之间共享相同词嵌入时非常有用。在 第十一章 使用 TensorFlow 进行自然语言处理 中,我们将看到如何应用来自 TensorFlow Hub 的预训练嵌入层。
当我们传入一个形状为(
batch_size,input_length)的二维张量,其中每个样本是一个整数序列时,嵌入层会返回一个形状为(batch_size,input_length,embedding_dim)的三维张量。在训练开始时,嵌入向量是随机初始化的。随着模型的训练,这些向量会不断调整,确保具有相似上下文的单词在嵌入空间中被紧密地聚集在一起。与使用离散值不同,词嵌入使用的是连续值,我们的模型可以利用这些值来辨别模式并模拟数据之间复杂的关系。GlobalAveragePooling1D层用于降低数据的维度;它执行平均池化操作。例如,如果我们对一个单词序列应用GlobalAveragePooling1D,它将返回一个总结后的单一向量,该向量可以输入到我们的全连接层进行分类。由于我们正在执行二分类任务,我们的输出层使用一个神经元,并将 sigmoid 作为激活函数。 -
现在,我们将编译并训练我们的模型。在编译步骤中,我们传入
binary_crossentropy作为损失函数。我们使用 Adam 优化器,并将准确率作为我们的分类评估指标:# Compile the modelmodel.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy']) -
我们使用训练数据(
train_padded)和标签(train_labels)对模型进行了 10 个 epoch 的训练,并使用验证数据来跟踪实验进展:# Train the modelhistory = model.fit(train_padded, train_labels,epochs=num_epochs, validation_data=(val_padded,val_labels))我们报告最后 5 个 epoch 的结果:
Epoch 6/10625/625 [==============================] - 4s 7ms/step - loss: 0.1293 - accuracy: 0.9551 - val_loss: 0.3149 - val_accuracy: 0.8875Epoch 7/10625/625 [==============================] - 4s 6ms/step - loss: 0.1116 - accuracy: 0.9638 - val_loss: 0.3330 - val_accuracy: 0.8880Epoch 8/10625/625 [==============================] - 5s 9ms/step - loss: 0.0960 - accuracy: 0.9697 - val_loss: 0.3703 - val_accuracy: 0.8813Epoch 9/10625/625 [==============================] - 4s 6ms/step - loss: 0.0828 - accuracy: 0.9751 - val_loss: 0.3885 - val_accuracy: 0.8796Epoch 10/10625/625 [==============================] - 4s 6ms/step - loss: 0.0727 - accuracy: 0.9786 - val_loss: 0.4258 - val_accuracy: 0.8783模型在训练集上的准确率达到了 0.9786,验证集上的准确率为 0.8783。这告诉我们模型存在过拟合现象。接下来,我们看看模型在未见过的数据上的表现。为此,我们将使用测试数据评估模型。
-
我们使用
evaluate函数来评估训练好的模型:# Evaluate the model on the test setresults = model.evaluate(test_padded, test_labels)print("Test Loss: ", results[0])print("Test Accuracy: ", results[1])我们传入测试数据(
test_padded)和测试标签(test_labels),并输出损失和准确率。模型在测试集上的准确率为 0.8783。 -
在训练和验证过程中绘制损失和准确率曲线是一个好习惯,因为它能为我们提供关于模型学习过程和性能的宝贵见解。为了实现这一点,我们将构建一个名为
plot_history的函数:def plot_history(history):plt.figure(figsize=(12, 4))# Plot training & validation accuracy valuesplt.subplot(1, 2, 1)plt.plot(history.history['accuracy'])plt.plot(history.history['val_accuracy'])plt.title('Model accuracy')plt.ylabel('Accuracy')plt.xlabel('Epoch')plt.legend(['Train', 'Validation'],loc='upper left')# Plot training & validation loss valuesplt.subplot(1, 2, 2)plt.plot(history.history['loss'])plt.plot(history.history['val_loss'])plt.title('Model loss')plt.ylabel('Loss')plt.xlabel('Epoch')plt.legend(['Train', 'Validation'],loc='upper right')plt.tight_layout()plt.show()这个函数接收
history对象,并返回损失和准确率曲线。plot_history函数将创建一个包含两个子图的图形——左侧子图显示每个 epoch 的训练和验证准确率,右侧子图显示每个 epoch 的训练和验证损失。

图 10.5 – 损失和准确率曲线
从图 10.5中的图表可以看出,模型在训练中的准确率每个 epoch 都在提高;然而,验证准确率在第一个 epoch 结束时开始略微下降。训练损失也在每个 epoch 中稳步下降,而验证损失则在每个 epoch 中稳步上升,这表明模型存在过拟合现象。
-
在我们探索如何解决过拟合问题之前,让我们先尝试四个新的句子,看看我们的模型如何处理它们:
# New sentencesnew_sentences = ["The restaurant was absolutely fantastic. The staff were kind and the food was delicious.", # positive"I've had an incredible day at the beach, the weather was beautiful.", # positive"The movie was a big disappointment. I wouldn't recommend it to anyone.", # negative"I bought a new phone and it stopped working after a week. Terrible product."] # negative -
在此示例中,我们给出了每个句子的情感供参考。让我们看看我们训练的模型在这些新句子上的表现如何:
# Preprocess the sentences in the same way as the training datanew_sequences = tokenizer.texts_to_sequences(new_sentences)new_padded = pad_sequences(new_sequences,maxlen=max_length, padding=padding_type,truncating=trunc_type)# Use the model to predict the sentiment of the new sentencespredictions = model.predict(new_padded)# Print out the sequences and the corresponding predictionsfor i in range(len(new_sentences)):print("Sequence:", new_sequences[i])print("Predicted sentiment (probability):", predictions[i])if predictions[i] > 0.5:print("Interpretation: Positive sentiment")else:print("Interpretation: Negative sentiment")print("\n") -
让我们打印出每个句子对应的序列,并显示模型预测的情感:
1/1 [==============================] - 0s 21ms/stepSequence: [2, 107, 7, 487, 533, 2, 123, 27, 290, 3, 2, 31, 7, 182]Predicted sentiment (probability): [0.9689689]Interpretation: Positive sentimentSequence: [112, 25, 60, 1251, 151, 26, 2, 3177, 2, 2079, 7, 634]Predicted sentiment (probability): [0.9956489]Interpretation: Positive sentimentSequence: [2, 1050, 7, 6, 221, 1174, 4, 454, 234, 9, 5, 528]Predicted sentiment (probability): [0.43672907]Interpretation: Negative sentimentSequence: [4, 764, 6, 161, 483, 3, 9, 695, 524, 83, 6, 393, 464, 1341]Predicted sentiment (probability): [0.36306405]Interpretation: Negative sentiment
我们的情感分析模型能够有效地预测正确的结果。如果我们想要可视化嵌入呢?TensorFlow 提供了一个嵌入投影仪,可以通过projector.tensorflow.org访问。
嵌入可视化
如果我们希望可视化来自训练模型的词嵌入,我们需要从嵌入层提取学习到的嵌入,并将其加载到 TensorBoard 提供的嵌入投影仪中。让我们看看如何操作:
-
提取嵌入层权重:
weights = model.get_layer('embedding').get_weights()[0]vocab = tokenizer.word_indexprint(weights.shape)# shape: (vocab_size, embedding_dim)第一步是从嵌入层中提取训练后的学习权重。接下来,我们获取在分词过程中生成的词汇映射。如果我们应用
print语句,我们可以看到词汇大小和嵌入维度。 -
然后,将权重和词汇保存到磁盘。TensorFlow 投影仪读取这些文件类型,并使用它们在 3D 空间中绘制向量,从而使我们能够可视化它们:
out_v = io.open('vectors.tsv', 'w', encoding='utf-8')out_m = io.open('metadata.tsv', 'w', encoding='utf-8')for word, index in vocab.items():if index < vocab_size:vec = weights[index]out_v.write('\t'.join([str(x) for x in vec]) + "\n")out_m.write(word + "\n")out_v.close()out_m.close() -
下一步是将嵌入向量和词汇(单词)分别保存为两个独立的
vecs.tsv和meta.tsv文件。当我们运行此代码块时,我们会看到在 Google Colab 笔记本中有两个新文件,如图 10.6所示。

图 10.6 – 显示元数据和向量文件的截图
-
将文件下载到本地:
try:from google.colab import filesfiles.download('vectors.tsv')files.download('metadata.tsv')except Exception:pass要将所需文件从 Google Colab 下载到本地计算机,请运行此代码块。请注意,如果您在云环境中工作,需要将这些文件从服务器转移到本地计算机。
-
可视化嵌入。打开嵌入投影仪,使用此链接:
projector.tensorflow.org/。然后,您需要点击加载按钮,将下载到本地计算机的vectors.tsv和metadata.tsv文件加载进去。一旦成功上传文件,词嵌入将以 3D 形式显示,如图 10.7所示。

图 10.7 – 词嵌入的快照查看详细
要了解更多关于嵌入可视化的信息,请参阅文档:www.tensorflow.org/text/guide/word_embeddings?hl=en。我们现在已经看到了如何可视化词嵌入。接下来,让我们尝试提高模型的性能。
提高模型性能
之前,我们讨论了在本章中设计情感分析基线架构时需要考虑的一些因素。此外,在第八章中,处理过拟合部分,我们探讨了一些基本概念,以减轻过拟合问题。在那里,我们看到了早停(early stopping)和丢弃正则化(dropout regularization)等思想。为了遏制过拟合,让我们从调整模型的一些超参数开始。为此,我们将构建一个名为sentiment_model的函数。这个函数需要三个参数——vocab_size、embedding_dim和训练集的大小。
增加词汇表的大小
我们可能考虑调整的一个超参数是词汇表的大小。增加词汇表的大小使得模型能够从我们的数据集中学习到更多独特的单词。让我们看看这会如何影响基线模型的性能。在这里,我们将vocab_size从10000调整为20000,同时保持其他超参数不变:
# Increasing the vocab_size
vocab_size = 10000 #Change from 10000 to 20000
embedding_dim = 16
training_size = 20000
num_epochs=10
model_1, history_1 = sentiment_model(vocab_size,
embedding_dim, training_size, num_epochs)
该模型的测试准确率为 0.8749,而基线模型的准确率为 0.8783。在这里,增加vocab_size对模型性能没有带来正面的影响。
当我们使用更大的词汇表时,模型将学习到更多独特的单词,这可能是个好主意,具体取决于数据集和使用场景。但另一方面,更多的参数和计算资源要求意味着我们需要更高效地训练模型。同时,过拟合的风险也会更大。
鉴于这些问题,确保我们拥有足够大的vocab_size以捕捉数据中的细微差别,同时避免引入过拟合的风险,是至关重要的。一个策略是设置最小频率阈值,从而排除可能导致过拟合的稀有单词。我们还可以尝试调整嵌入维度。接下来我们来讨论这个。
调整嵌入维度
嵌入维度是指表示单词的向量空间的大小。高维度的嵌入能够捕捉到单词之间更为细致的关系。然而,它也增加了模型的复杂度,可能导致过拟合,尤其是在处理小数据集时。让我们将embedding_dim从16调整为32,同时保持其他参数不变,看看这对我们的实验有什么影响:
vocab_size = 10000
embedding_dim = 32 #Change from 16 to 32
train_size = 20000
num_epochs=10
model_2, history_2 = sentiment_model(vocab_size,
embedding_dim, train_size, num_epochs)
经过 10 轮训练,使用较大嵌入维度的新模型在测试集上达到了 0.8720 的准确率。这个结果低于我们基准模型的表现。在这里,我们亲眼看到了嵌入维度的增加并不总是能保证模型性能的提升。当嵌入维度过小时,可能无法捕捉到数据中的重要关系。相反,嵌入维度过大会导致计算需求增加,并增加过拟合的风险。值得注意的是,对于简单任务或小型数据集,小的嵌入维度就足够了,而对于大型数据集,更大的嵌入维度则是一个更好的选择。一种务实的方法是从较小的嵌入维度开始,并在每次迭代时逐渐增加其大小,同时密切关注模型的表现。通常,性能会有所提升,但在某个点之后,收益递减现象会显现出来。到时,我们就停止训练。现在,我们可以收集更多数据,增加样本量,看看结果如何。
收集更多数据
在第八章《处理过拟合》中,我们探讨了在处理过拟合时的这一选项。收集更多的数据样本能够为我们提供更多样化的例子,供我们的模型学习。然而,这个过程可能会非常耗时。而且,当数据噪声大或不相关时,更多的数据可能并不会有所帮助。对于我们的案例研究,让我们将训练数据量从 20,000 个样本增加到 40,000 个样本:
vocab_size = 10000
embedding_dim = 16
train_size = 40000
model_3, history_3 = sentiment_model(vocab_size,
embedding_dim, train_size)
经过 10 轮训练后,我们看到模型在测试集上的性能提升至 0.8726。收集更多的数据可能是一个不错的策略,因为它可以为我们的模型提供更多样化的数据集;然而,这次并未奏效。那么,让我们尝试其他的想法;这次,我们来尝试 dropout 正则化。
Dropout 正则化
在第八章《处理过拟合》中,我们讨论了 dropout 正则化方法,在训练过程中我们随机丢弃一部分神经元,以打破神经元之间的共依赖关系。由于我们正在处理过拟合的情况,试试这个技巧吧。为了在我们的模型中实现 dropout,我们可以添加一个 dropout 层,示例如下:
model_4 = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim,
input_length=max_length),
tf.keras.layers.GlobalAveragePooling1D(),
tf.keras.layers.Dense(24, activation='relu'),
tf.keras.layers.Dropout(0.5),
# Dropout layer with 50% dropout rate
tf.keras.layers.Dense(1, activation='sigmoid')
])
# Compile the model
model_4.compile(loss='binary_crossentropy',
optimizer='adam',metrics=['accuracy'])
在这里,我们将我们的丢弃率设定为 50%,这意味着在训练期间关闭了一半的神经元。我们的新模型达到了 0.8830 的准确率,略优于基线模型。丢弃可以通过防止神经元之间的相互依赖来增强模型的稳健性。然而,我们必须谨慎应用丢弃。如果我们丢弃了太多的神经元,我们的模型会变得过于简单,开始欠拟合,因为它无法捕捉数据中的潜在模式。此外,如果我们将低丢弃值应用于模型,可能无法实现我们希望的正则化效果。尝试不同的丢弃值以找到模型复杂性和泛化之间的最佳平衡是一个好主意。现在,让我们尝试不同的优化器。
尝试不同的优化器
虽然 Adam 是一个很好的通用优化器,但您可能会发现,对于您的特定任务,不同的优化器(如 SGD 或 RMSprop)效果更好。根据手头的任务,不同的优化器可能效果更好。让我们尝试使用 RMSprop 来处理我们的用例,看看它的表现如何:
# Initialize the optimizer
optimizer = tf.keras.optimizers.RMSprop(learning_rate=0.001)
# Compile the model
model_7.compile(loss='binary_crossentropy',
optimizer=optimizer, metrics=['accuracy'])
# Train the model
history_7 = model_7.fit(train_padded, train_labels,
epochs=num_epochs, validation_data=(val_padded,
val_labels))
我们使用 RMSprop 作为优化器,在超过基线模型的情况下,实现了 0.8920 的测试准确率。在选择适当的优化器时,评估您的用例的关键属性至关重要。例如,在处理大型数据集时,SGD 比批量梯度下降更合适,因为 SGD 使用小批量减少了计算成本。在使用有限计算资源时,这一特性非常有用。值得注意的是,如果我们在使用 SGD 时有太多小批量,可能会导致嘈杂的更新;另一方面,非常大的批量大小可能会增加计算成本。
Adam 是许多深度学习用例的优秀默认优化器,因为它结合了 RMSprop 和动量的优点;然而,当处理简单的凸问题(如线性回归)时,SGD 在这些情景中表现更佳,因为 Adam 在这些情况下会过度补偿。至此,我们结束了本章。
概要
在本章中,我们探讨了 NLP 的基础知识。我们首先看了如何处理现实世界的文本数据,探索了一些预处理思路,使用了 Beautiful Soup、requests 和正则表达式等工具。然后,我们展开了各种想法,如分词、序列化和使用词嵌入将文本数据转换为向量表示,这不仅保留了文本数据的顺序,还捕捉了单词之间的关系。我们进一步迈出了一步,利用来自 TensorFlow 数据集的 Yelp 极性数据集构建了情感分析分类器。最后,我们进行了一系列实验,使用不同的超参数来改进我们基础模型的性能并克服过拟合问题。
在下一章中,我们将介绍递归神经网络(RNNs),并看看它们与本章中使用的 DNN 有何不同。我们将通过构建一个新的分类器来测试 RNNs。我们还将通过实验使用预训练的嵌入,将事情推进到一个新层次,最后,我们将在一个有趣的练习中生成文本,使用的是儿童故事数据集。到时见。
问题
让我们测试一下本章所学的内容。
-
使用测试笔记本,从 TFDS 加载 IMDB 数据集。
-
使用不同的嵌入维度,并在测试集上评估模型。
-
使用不同的词汇大小,并在测试集上评估模型。
-
添加更多层并在测试集上评估模型。
-
使用你最好的模型对给定的样本句子进行预测。
深入阅读
要了解更多信息,可以查看以下资源:
-
Kapoor, A., Gulli, A. 和 Pal, S.(2020 年) 《使用 TensorFlow 和 Keras 的深度学习(第三版)》,Packt Publishing Ltd.
-
Twitter 情感分类使用远程监督,由 Go 等人(2009 年)提出。
-
嵌入投影器:嵌入的互动可视化和解释,由 Smilkov 等人(2016 年)提出。
-
卷积神经网络在句子分类中的敏感性分析(及实务指南),由 Zhang 等人(2016 年)提出。
第十一章:使用 TensorFlow 进行自然语言处理
文本数据本质上是顺序性的,由单词出现的顺序定义。单词相互衔接,建立在前一个思想的基础上,并塑造接下来的思想。人类理解单词的顺序以及它们所应用的上下文非常直接。然而,这对前馈网络(如卷积神经网络(CNNs))和传统的深度神经网络(DNNs)构成了巨大挑战。这些模型将文本数据视为独立的输入,因此它们忽略了语言的内在联系和流动性。例如,考虑这句话:“The cat, which is a mammal, likes to chase mice。”人类会立即识别出猫与老鼠之间的关系,因为我们将整句话作为一个整体来处理,而不是单个单位。
递归神经网络(RNN)是一种旨在处理顺序数据(如文本和时间序列数据)的神经网络类型。处理文本数据时,RNN 的记忆能力使它能够回忆起序列中的早期部分,帮助它理解单词在文本中使用的上下文。例如,考虑这样一句话:“作为一名考古学家,约翰喜欢发现古代文物”,在这种情况下,RNN 能推断出考古学家会对古代文物感兴趣,而且这句话中的考古学家是约翰。
本章中,我们将开始探索 RNN 的世界,并深入研究其内部机制,了解 RNN 如何协同工作以保持一种记忆形式。我们将探讨在处理文本数据时,RNN 的优缺点,然后我们将转向研究它的变体,如长短期记忆(LSTM)和门控递归单元(GRU)。接下来,我们将运用所学知识构建一个多类文本分类器。然后,我们将探索迁移学习在自然语言处理(NLP)领域的强大能力。在这一部分,我们将看到如何将预训练的词嵌入应用到我们的工作流中。为了结束本章,我们将使用 RNN 构建一个儿童故事生成器;在这里,我们将看到 RNN 如何在生成文本数据时发挥作用。
在本章中,我们将涵盖以下主题:
-
RNN 的结构
-
使用 RNN 进行文本分类
-
使用迁移学习进行自然语言处理
-
文本生成
理解顺序数据处理——从传统神经网络到 RNN 和 LSTM
在传统神经网络中,正如我们在本书前面所讨论的那样,网络中排列着密集连接的神经元,并且没有任何形式的记忆。当我们将一串数据输入这些网络时,这是一种“全有或全无”的处理方式——整个序列一次性被处理并转换为一个单一的向量表示。这种方法与人类处理和理解文本数据的方式大不相同。当我们阅读时,我们自然而然地按单词逐一分析文本,并理解重要的单词——那些能够改变整个句子含义的单词——可以出现在句子的任何位置。例如,考虑句子“我喜欢这部电影,尽管一些评论家不喜欢”。在这里,单词“尽管”至关重要,改变了句子中情感表达的方向。
RNN 不仅通过嵌入考虑单个单词的值,它们还会考虑这些单词的顺序或相对位置。单词的顺序赋予它们意义,并使得人类能够有效地进行交流。RNN 的独特之处在于它们能够从一个时间点(或在句子的情况下,从一个单词)到下一个时间点保持上下文,从而保持输入的顺序一致性。例如,在句子“我去年访问了罗马,我觉得斗兽场非常迷人”中,RNN 会理解“斗兽场”与“罗马”的关系,这是因为单词的顺序。然而,有一个问题——在更长的句子中,当相关单词之间的距离增大时,这种上下文保持可能会失败。
这正是门控变种 RNN,如 LSTM 网络的作用所在。LSTM 通过一种特殊的“单元状态”架构设计,使其能够在更长的序列中管理和保留信息。因此,即使是像“我去年访问了罗马,体验了丰富的文化,享受了美味的食物,结识了很棒的人,我觉得斗兽场非常迷人”这样的长句,LSTM 仍然能够将“斗兽场”与“罗马”联系起来,理解句子尽管长度和复杂性较大,但它们之间的广泛联系。我们仅仅是触及了表面。接下来,我们将检查这些强大网络的结构。我们将从 RNN 开始。
RNN 的结构
在上一节中,我们讨论了 RNN 处理序列数据的能力;现在让我们深入了解 RNN 是如何做到这一点的。RNN 和前馈神经网络的关键区别在于它们的内部记忆,如图 11.1所示,这使得 RNN 能够处理输入序列的同时保留来自前一步的信息。这一特性使得 RNN 能够充分利用序列中如文本数据的时间依赖性。

图 11.1 – RNN 的结构
图 11.2 显示了一个更清晰的 RNN 图和它的内部工作原理。在这里,我们可以看到一系列相互连接的单元,数据以顺序的方式逐个元素流动。当每个单元处理输入数据时,它将输出发送到下一个单元,类似于前馈网络的工作方式。关键的不同之处在于反馈回路,它使 RNN 拥有了之前输入的记忆,从而使它们能够理解整个序列。

图 11.2 – 展示 RNN 在多个时间步中的操作的扩展视图
假设我们正在处理句子,并且希望我们的 RNN 学习句子的语法。句子中的每个单词代表一个时间步长,在每个时间步,RNN 会考虑当前单词以及来自前一个单词(或步骤)的“上下文”。让我们看一个示例句子。假设我们有一个包含五个单词的句子——“巴塞罗那是一个美丽的城市。”这个句子有五个时间步,每个单词对应一个时间步。在时间步 1,我们将单词“巴塞罗那”输入到 RNN 中。网络学习到有关这个单词的一些信息(实际上,它会从该单词的向量表示中学习),然后生成一个输出,同时也会生成一个隐藏状态,捕捉它所学到的内容,如 图 11.2 所示。现在,我们将 RNN 展开到时间步 2。我们将下一个单词“是”输入到网络中,同时也将时间步 1 的隐藏状态输入。这一隐藏状态代表了网络的“记忆”,使得网络能够考虑它迄今为止所看到的内容。网络生成一个新的输出和一个新的隐藏状态。
这一过程继续进行,RNN 会根据句子中的每个单词进一步展开。在每个时间步,网络会将当前的单词和来自上一时间步的隐藏状态作为输入,生成输出和新的隐藏状态。当你以这种方式“展开”一个 RNN 时,它看起来可能像是一个具有共享权重的深度前馈网络(因为每个时间步都使用相同的 RNN 单元进行操作),但更准确地说,它是一个应用于每个时间步的单一网络,随着传递隐藏状态而进行计算。RNN 具有学习和记住任意长度序列的能力;然而,它们也有自己的局限性。RNN 的一个关键问题是,由于梯度消失问题,它难以捕捉长期依赖关系。出现这种问题是因为在反向传播过程中,时间步对未来时间步的影响可能会随着长序列的增长而减弱,导致梯度变得非常小,从而更难进行学习。为了解决这个问题,我们将应用 RNN 的更高级版本,如 LSTM 和 GRU 网络。这些架构通过应用门控机制来控制网络中信息的流动,使得模型更容易学习长期依赖关系。接下来我们来看看这些 RNN 的变种。
RNN 的变种 — LSTM 和 GRU
假设我们正在进行一项电影评论分类项目,在检查我们的数据集时,我们发现一条类似这样的句子:“这部电影开始时很无聊且节奏缓慢,但到最后真的是越来越好,高潮部分令人惊叹。”通过分析这个句子,我们看到评论者最初使用的词汇呈现出负面情感,如“缓慢”和“无聊”,但情感在后面发生了转变,变得更加积极,词组如“越来越好”和“高潮部分令人惊叹”都在传达更为正面的情感。如果我们使用简单的 RNN 来处理这个任务,由于它本身无法很好地保留长序列的信息,它可能会因为过分强调评论中最初的负面情绪而误分类这个句子。
相反,LSTM 和 GRU 被设计用来处理长期依赖关系,这使得它们不仅能有效捕捉情感变化,还能在其他自然语言处理任务中表现出色,例如机器翻译、文本摘要和问答系统,在这些任务中,它们优于其更简单的对手。
LSTM(长短期记忆网络)
LSTM 是一种专门设计用来解决梯度消失问题的 RNN 类型,使得 LSTM 能够有效处理序列数据中的长期依赖。为了解决这个问题,LSTM 引入了一种称为记忆单元的新结构,它本质上作为信息载体,并具有在延长期间内保留信息的能力。与标准的 RNN 不同,后者将信息从一个步骤传递到下一个步骤,并随时间逐渐丢失,LSTM 借助其记忆单元可以从输入序列的任何点存储和检索信息。让我们来看看 LSTM 是如何决定存储在其记忆单元中的信息的。如图 Figure 10**.3 所示,一个 LSTM 由四个主要组件组成:

10.3 – LSTM 架构
这些组件使得 LSTM 能够在长序列中存储和访问信息。让我们来看看每个组件:
-
输入门:输入门决定将存储在记忆单元中的新信息。它由 Sigmoid 和 tanh 层组成。Sigmoid 层产生介于零和一之间的输出值,表示输入中每个值的重要性水平,其中零意味着“一点也不重要”,而一表示“非常重要”。tanh 层生成一组候选值,这些值可以添加到状态中,基本上建议应该将哪些新信息存储在记忆单元中。这两个层的输出通过逐元素乘法进行合并。这种逐元素操作生成一个输入调制门,有效地过滤新的候选值,通过决定哪些信息足够重要以存储在记忆单元中。
-
遗忘门:这个门决定哪些信息将被保留,哪些信息将被丢弃。它使用 Sigmoid 层返回介于零和一之间的输出值。如果遗忘门中的单元返回接近零的输出值,LSTM 将从细胞状态的相应单元中删除信息。
-
输出门:输出门决定下一个隐藏状态应该是什么。像其他门一样,它也使用 S 型函数来决定哪些细胞状态的部分将成为输出。
-
细胞状态:细胞状态是 LSTM 细胞的“记忆”。它基于遗忘门和输入门的输出进行更新。它可以记住信息以供后续序列使用。
门机制至关重要,因为它们允许 LSTM 自动学习适合上下文的读取、写入和重置记忆单元的方式。这些能力使得 LSTM 能够处理更长的序列,特别适用于许多复杂的顺序任务,标准 RNN 由于无法处理长期依赖而不足,如机器翻译、文本生成、时间序列预测和视频分析。
双向长短期记忆(BiLSTM)
双向长短时记忆网络(BiLSTM)是传统 LSTM 网络的扩展。然而,与按顺序从开始到结束处理信息的 LSTM 不同,BiLSTM 同时运行两个 LSTM ——一个从开始处理序列数据到结束,另一个从结束到开始处理,如图 11.4所示。

图 11.4 – BiLSTM 中的信息流
通过这种方式,BiLSTM 能够捕捉序列中每个数据点的过去和未来上下文。由于 BiLSTM 能够从数据序列的两个方向理解上下文,因此它们非常适合执行文本生成、文本分类、情感分析和机器翻译等任务。现在,让我们来看看 GRU。
GRU
2014 年,Cho 等人提出了 GRU 架构,作为 LSTM 的一种可行替代方案。GRU 旨在实现两个主要目标——一个是克服困扰传统 RNN 的梯度消失问题,另一个是简化 LSTM 架构,以提高计算效率,同时保持建模长期依赖的能力。从结构上看,GRU 有两个主要门,如图 11.5所示。

图 11.5 – GRU 的架构
GRU 和 LSTM 之间的一个关键区别是 GRU 中没有独立的细胞状态;相反,它们使用隐状态来传递和操作信息,并简化其计算需求。
GRU 有两个主要的门,更新门和重置门。让我们来看看它们:
-
更新门:更新门将 LSTM 中的输入门和遗忘门进行了简化。它决定了多少过去的信息需要传递到当前状态,以及哪些信息需要被丢弃。
-
重置门:此门定义了应忘记多少过去的信息。它帮助模型评估新输入与过去记忆之间的相对重要性。
除了这两个主要门,GRU 还引入了“候选隐状态”。此候选隐状态结合了新输入和先前的隐状态,通过这种方式,它为当前时间步开发了隐状态的初步版本。然后,这个候选隐状态在决定最终隐状态时发挥重要作用,确保 GRU 保留来自过去的相关上下文,同时接纳新信息。在决定选择 LSTM 还是 GRU 时,选择往往取决于特定的应用场景和可用的计算资源。对于某些应用,GRU 提供的计算效率更具吸引力。例如,在实时处理(如文本转语音)中,或者在处理短序列任务(如推文的情感分析)时,GRU 相较于 LSTM 可能是一个更好的选择。
我们已经提供了关于 RNN 及其变种的高层次讨论。现在,让我们开始将这些新架构应用于实际的使用案例。它们会比标准的 DNN 或 CNN 表现更好吗?让我们在文本分类案例研究中一探究竟。
使用 AG News 数据集进行文本分类——一个比较研究
AG News 数据集是一个包含超过 100 万篇新闻文章的集合,数据来自一个名为 ComeToMyHead 的新闻搜索引擎,覆盖了 2000 多个新闻来源。该数据集分为四个类别——即世界、体育、商业、科技——并且可通过TensorFlow Datasets(TFDS)获取。数据集包括 120,000 个训练样本(每个类别 30,000 个),测试集包含 7,600 个样本。
注意
由于数据集的大小和模型数量,此实验可能需要大约一个小时才能完成,因此确保你的笔记本支持 GPU 是很重要的。你也可以选择一个较小的子集,以确保实验能够更快地运行。
让我们开始构建我们的模型:
-
我们将首先加载本次实验所需的库:
import pandas as pdimport tensorflow_datasets as tfdsimport tensorflow as tffrom tensorflow.keras.preprocessing.text import Tokenizerfrom tensorflow.keras.preprocessing.sequence import pad_sequencesfrom tensorflow.keras.utils import to_categoricalimport tensorflow_hub as hubfrom sklearn.model_selection import train_test_splitfrom tensorflow.keras.models import Sequentialfrom tensorflow.keras.layers import Embedding, SimpleRNN, Conv1D, GlobalMaxPooling1D, LSTM, GRU, Bidirectional, Dense, Flatten
这些导入形成了构建模块,使我们能够解决这个文本分类问题。
-
然后,我们从 TFDS 加载 AG News 数据集:
# Load the datasetdataset, info = tfds.load('ag_news_subset',with_info=True, as_supervised=True)train_dataset, test_dataset = dataset['train'],dataset['test']
我们使用此代码从 TFDS 加载我们的数据集——tfds.load函数会获取并加载 AG News 数据集。我们将with_info参数设置为True;这确保了我们数据集的元数据,如样本总数和版本,也会被收集。此元数据信息存储在info变量中。我们还将as_supervised设置为True;我们这样做是为了确保数据以输入和标签对的形式加载,其中输入是新闻文章,标签是对应的类别。然后,我们将数据划分为训练集和测试集。
-
现在,我们需要为建模准备数据:
# Tokenize and pad the sequencestokenizer = Tokenizer(num_words=20000,oov_token="<OOV>")train_texts = [x[0].numpy().decode('utf-8') for x in train_dataset]tokenizer.fit_on_texts(train_texts)sequences = tokenizer.texts_to_sequences(train_texts)sequences = pad_sequences(sequences, padding='post')
在这里,我们执行数据预处理步骤,如分词、序列化和填充,使用 TensorFlow 的 Keras API。我们初始化了分词器,用于将我们的数据从文本转换为整数序列。我们将num_words参数设置为20000。这意味着我们只会考虑数据集中出现频率最高的 20,000 个单词进行分词;低于此频率的词汇将被忽略。我们将oov_token="<OOV>"参数设置为确保我们能处理在模型推理过程中可能遇到的未见过的单词。
然后,我们提取训练数据并将其存储在 train_texts 变量中。我们通过使用 fit_on_texts 和 texts_to_sequences() 方法分别将数据标记化并转化为整数序列。我们对每个序列应用填充,以确保输入模型的数据具有一致的形状。我们将 padding 设置为 post;这将确保填充在序列的末尾应用。现在,我们的数据已经以良好的结构化格式准备好,稍后我们将其输入深度学习模型进行文本分类。
-
在开始建模之前,我们需要将数据分为训练集和验证集。我们通过将训练集拆分为 80% 的训练数据和 20% 的验证数据来实现:
# Convert labels to one-hot encodingtrain_labels = [label.numpy() for _, label in train_dataset]train_labels = to_categorical(train_labels,num_classes=4)4 # assuming 4 classes# Split the training set into training and validation setstrain_sequences, val_sequences, train_labels,val_labels = train_test_split(sequences,train_labels, test_size=0.2)
我们将标签转换为 one-hot 编码向量,然后使用 scikit-learn 的 train_test_split 函数将训练数据拆分。我们将 test_size 设置为 0.2;这意味着我们将 80% 的数据用于训练,其余 20% 用于验证目的。
-
让我们设置
vocab_size、embedding_dim和max_length参数:vocab_size=20000embedding_dim =64max_length=sequences.shape[1]
我们将 vocab_size 和 embedding_dim 分别设置为 20000 和 64。在选择 vocab_size 时,重要的是在计算效率、模型复杂度和捕捉语言细微差别的能力之间找到良好的平衡,而我们使用嵌入维度通过 64 维向量表示词汇表中的每个单词。max_length 参数设置为与数据中最长的标记化和填充序列匹配。
-
我们开始构建模型,从 DNN 开始:
# Define the DNN modelmodel_dnn = Sequential([Embedding(vocab_size, embedding_dim,input_length=max_length),Flatten(),tf.keras.layers.Dense(64, activation='relu'),Dense(16, activation='relu'),Dense(4, activation='softmax')])
使用 TensorFlow 的 Sequential API,我们构建了一个 DNN,包含一个嵌入层、一个展平层、两个隐藏层和一个用于多类别分类的输出层。
-
然后,我们构建一个 CNN 架构:
# Define the CNN modelmodel_cnn = Sequential([Embedding(vocab_size, embedding_dim,input_length=max_length),Conv1D(128, 5, activation='relu'),GlobalMaxPooling1D(),tf.keras.layers.Dense(64, activation='relu'),Dense(4, activation='softmax')])
我们使用一个由 128 个滤波器(特征检测器)和 5 的卷积核大小组成的 Conv1D 层;这意味着它会同时考虑五个单词。我们的架构使用 GlobalMaxPooling1D 对卷积层的输出进行下采样,提取最重要的特征。我们将池化层的输出输入到一个全连接层进行分类。
-
然后,我们构建一个 LSTM 模型:
# Define the LSTM modelmodel_lstm = Sequential([Embedding(vocab_size, embedding_dim,input_length=max_length),LSTM(32, return_sequences=True),LSTM(32),tf.keras.layers.Dense(64, activation='relu'),Dense(4, activation='softmax')])
我们的 LSTM 架构由两个 LSTM 层组成,每个层有 32 个单元。在第一个 LSTM 层中,我们将 return_sequences 设置为 True;这允许第一个 LSTM 层将它收到的完整序列作为输出传递给下一个 LSTM 层。这里的目的是让第二个 LSTM 层能够访问整个序列的上下文;这使它能够更好地理解并捕捉整个序列中的依赖关系。然后,我们将第二个 LSTM 层的输出输入到全连接层进行分类。
-
对于我们的最终模型,我们使用一个双向 LSTM:
# Define the BiLSTM modelmodel_BiLSTM = Sequential([Embedding(vocab_size, embedding_dim,input_length=max_length),Bidirectional(LSTM(32, return_sequences=True)),Bidirectional(LSTM(16)),tf.keras.layers.Dense(64, activation='relu'),Dense(4, activation='softmax')])
在这里,我们没有使用 LSTM 层,而是增加了两层双向 LSTM。请注意,第一层也设置了return_sequences=True,以将完整的输出传递给下一层。使用双向包装器可以让每个 LSTM 层在处理输入序列中的每个元素时同时访问过去和未来的上下文,与单向 LSTM 相比,提供了更多的上下文信息。
堆叠 BiLSTM 层可以帮助我们构建更高层次的完整序列表示。第一层 BiLSTM 通过从两个方向查看文本来提取特征,同时保持整个序列的完整性。第二层 BiLSTM 则可以在这些特征的基础上进一步处理它们。最终的分类是由全连接层中的输出层完成的。我们的实验模型现在都已设置好,接下来让我们继续编译并拟合它们。
-
让我们编译并拟合我们目前为止构建的所有模型:
models = [model_cnn, model_dnn, model_lstm,model_BiLSTM]for model in models:model.compile(loss='categorical_crossentropy',optimizer='adam', metrics=['accuracy'])model.fit(train_sequences, train_labels,epochs=10,validation_data=(val_sequences, val_labels),verbose=False
我们使用for循环编译并拟合所有四个模型。我们将verbose设置为False;这样,我们就不会打印训练信息。我们训练 10 个周期。请预期这个步骤需要一些时间,因为我们有一个庞大的数据集,并且正在尝试四个模型。
-
让我们在未见过的数据上评估我们的模型:
# Evaluate the modeltest_texts = [x[0].numpy().decode('utf-8') for x in test_dataset]test_sequences = tokenizer.texts_to_sequences(test_texts)test_sequences = pad_sequences(test_sequences,padding='post', maxlen=sequences.shape[1])test_labels = [label.numpy() for _, label in test_dataset]test_labels = to_categorical(test_labels,num_classes=4)model_names = ["Model_CNN", "Model_DNN", "Model_LSTM","Model_BiLSTM"]for i, model in enumerate(models):loss, accuracy = model.evaluate(test_sequences,test_labels)print("Model Evaluation -", model_names[i])print("Loss:", loss)print("Accuracy:", accuracy)print()
为了评估我们的模型,我们需要以正确的方式准备我们的测试数据。我们首先从test_dataset中提取文本数据,然后使用我们训练过程中得到的分词器对文本进行分词。分词后的文本会被转换为整数序列,并应用填充,以确保所有序列的长度与训练数据中最长的序列相同。就像我们在训练过程中所做的那样,我们还对测试标签进行独热编码,然后应用for循环迭代每个单独的模型,生成所有模型的测试损失和准确率。输出如下:
238/238 [==============================] - 1s 4ms/step - loss: 0.7756 - accuracy: 0.8989
Model Evaluation - Model_CNN
Loss: 0.7755934000015259
Accuracy: 0.8989473581314087
238/238 [==============================] - 1s 2ms/step - loss: 0.7091 - accuracy: 0.8896
Model Evaluation - Model_DNN
Loss: 0.7091193199157715
Accuracy: 0.8896052837371826
238/238 [==============================] - 2s 7ms/step - loss: 0.3211 - accuracy: 0.9008
Model Evaluation - Model_LSTM
Loss: 0.32113003730773926
Accuracy: 0.9007894992828369
238/238 [==============================] - 4s 10ms/step - loss: 0.5618 - accuracy: 0.8916
Model Evaluation - Model_BiLSTM
Loss: 0.5618014335632324
Accuracy: 0.8915789723396301
从我们的返回结果来看,我们可以看到 LSTM 模型达到了最高的准确率(90.08%);其他模型的表现也相当不错。我们可以把这个表现作为一个良好的起点;我们也可以将我们在第八章《处理过拟合》和第十章《自然语言处理导论》中使用的一些方法应用到这里,以进一步改善我们的结果。
在第十章《自然语言处理导论》中,我们讨论了预训练的嵌入。这些嵌入是在大量文本数据上训练的。让我们看看如何利用它们;也许它们能帮助我们在这种情况下取得更好的结果。
使用预训练的嵌入
在第九章《迁移学习》中,我们探讨了迁移学习的概念。在这里,我们将重新审视这一概念,并与词嵌入相关联。在迄今为止构建的所有模型中,我们都是从零开始训练我们的词嵌入。现在,我们将探讨如何利用已经在大量文本数据上训练好的预训练嵌入,例如 Word2Vec、GloVe 和 FastText。使用这些嵌入有两个主要优点:
-
首先,它们已经在大量且多样化的数据集上进行了训练,因此它们对语言有着深刻的理解。
-
其次,训练过程更快,因为我们跳过了从头开始训练自己词嵌入的步骤。相反,我们可以在这些嵌入中所包含的信息基础上构建模型,专注于当前任务。
需要注意的是,使用预训练嵌入并不总是正确的选择。例如,如果你处理的是专业领域的文本数据,如医学或法律数据,包含大量特定领域术语的行业可能没有得到充分的表示。当我们盲目使用预训练的嵌入来处理这些用例时,可能会导致表现不佳。在这些情况下,你可以选择训练自己的嵌入,尽管这会增加计算成本,或者采用更平衡的方式,在你的数据上微调预训练的嵌入。让我们来看一下如何在工作流中应用预训练嵌入。
使用预训练嵌入进行文本分类
为了进行这个实验,你需要使用本章 GitHub 仓库中的第二个笔记本,名为 modelling with pretrained embeddings。我们将继续使用相同的数据集。这一次,我们将重点使用我们最好的模型与 GloVe 预训练嵌入。我们将使用在初步实验中得到的最佳模型(LSTM)。让我们开始吧:
-
我们将从导入本实验所需的库开始:
import numpy as npimport tensorflow as tffrom tensorflow.keras.models import Sequentialfrom tensorflow.keras.layers import Embedding, LSTM, Dense, Flattenfrom tensorflow.keras.preprocessing.text import Tokenizerfrom tensorflow.keras.preprocessing.sequence import pad_sequencesimport tensorflow_datasets as tfds
一旦我们导入了库,就可以下载预训练的嵌入。
-
运行以下命令来下载预训练的嵌入:
!wget http://nlp.stanford.edu/data/glove.6B.zip!unzip glove.6B.zip -d glove.6B
我们将通过 wget 命令从斯坦福 NLP 网站下载 GloVe 6B 嵌入文件到我们的 Colab 笔记本中,然后解压这些压缩文件。我们可以看到,这些文件包含了不同的预训练嵌入,如图 11.6所示:

图 11.6 – 显示 GloVe 6B 嵌入文件的目录
GloVe 6B 嵌入由 60 亿词元的词向量组成,由斯坦福大学的研究人员训练,并公开提供给我们使用。出于计算考虑,我们将使用 50 维的向量。你也许希望尝试更高维度的嵌入,以获得更丰富的表示,尤其是在处理需要捕捉更复杂语义关系的任务时,但也需要注意所需的计算资源。
-
然后,我们加载 AG News 数据集:
dataset, info = tfds.load('ag_news_subset',with_info=True, as_supervised=True)train_dataset, test_dataset = dataset['train'],dataset['test']
我们加载数据集并将其拆分为训练集和测试集。
-
然后,我们对训练集进行分词和序列化:
tokenizer = Tokenizer(num_words=20000,oov_token="<OOV>")train_texts = [x[0].numpy().decode('utf-8') for x in train_dataset]tokenizer.fit_on_texts(train_texts)train_sequences = tokenizer.texts_to_sequences(train_texts)train_sequences = pad_sequences(train_sequences,padding='post')max_length = train_sequences.shape[1]
我们像上次实验一样准备数据进行建模。我们将词汇表大小设置为 2,000 个单词,并使用OOV来表示词汇表外的单词。然后,我们对数据进行分词和填充,以确保数据长度的一致性。
-
然后,我们处理测试数据:
test_texts = [x[0].numpy().decode('utf-8') for x in test_dataset]test_sequences = tokenizer.texts_to_sequences(test_texts)test_sequences = pad_sequences(test_sequences,padding='post', maxlen=max_length)
我们以类似训练数据的方式处理测试数据。然而,值得注意的是,我们不会对测试集应用fit_on_texts,以确保分词器与训练集相同。
-
然后,我们设置嵌入参数:
vocab_size = len(tokenizer.word_index) + 1embedding_dim = 50
我们定义了词汇表的大小,并将词向量的维度设置为50。我们使用这个设置是因为我们正在使用 50 维的预训练词向量。
-
应用预训练的词向量:
# Download GloVe embeddings and prepare embedding matrixwith open('/content/glove.6B/glove.6B.50d.txt', 'r', encoding='utf-8') as f:for line in f:values = line.split()word = values[0]if word in tokenizer.word_index:idx = tokenizer.word_index[word]embedding_matrix[idx] = np.array(values[1:], dtype=np.float32)
我们访问glove.6B.50d.txt文件,并逐行读取。每一行包含一个单词及其对应的词向量。我们将 GloVe 文件中的单词与我们自己用 Keras 分词器构建的词汇表中的单词进行交叉匹配。如果匹配成功,我们从自己的词汇表中获取对应的单词索引,并将该索引位置的初始为零的嵌入矩阵更新为 GloVe 词向量。相反,未匹配的单词将在矩阵中保持为零向量。我们将使用这个嵌入矩阵来初始化我们的嵌入层权重。我们使用 50 维词向量文件的路径,如图 11.6所示。你可以通过右键点击指定文件并复制文件路径来实现。
-
接下来,我们构建、编译并训练我们的 LSTM 模型:
model_lstm = Sequential([Embedding(vocab_size, embedding_dim,input_length=max_length,weights=[embedding_matrix], trainable=False),LSTM(32, return_sequences=True),LSTM(32),Dense(64, activation='relu'),Dense(4, activation='softmax')])model_lstm.compile(optimizer='adam',loss='categorical_crossentropy',metrics=['accuracy'])# Convert labels to one-hot encodingtrain_labels = tf.keras.utils.to_categorical([label.numpy() for _, label in train_dataset])test_labels = tf.keras.utils.to_categorical([label.numpy() for _, label in test_dataset])model_lstm.fit(train_sequences, train_labels,epochs=10, validation_split=0.2)
在构建模型时,与我们之前的实验的主要区别是嵌入层的初始化。在这里,我们利用预训练的嵌入矩阵,并为了确保权重保持不变,我们将可训练参数设置为false。模型架构中的其他部分保持不变。然后,我们编译并训练模型 10 个 epoch。
-
最后,我们评估我们的模型:
loss, accuracy = model_lstm.evaluate(test_sequences,test_labels)print("Loss:", loss)print("Accuracy:", accuracy)
我们在测试集上的准确率达到了 89%,尽管当我们没有应用预训练词向量时,并没有超越我们最好的模型。也许你可以尝试使用glove6B中的更大维度的词向量,或者其他的词嵌入来提升我们的结果。那将是一个很好的练习,并且非常值得鼓励。
现在,是时候进入另一个激动人心的话题——使用 LSTM 生成文本。
使用 LSTM 生成文本
我们已经探索了 LSTM 在文本分类中的应用。现在,我们将看看如何生成小说、博客文章或儿童故事书中可能出现的文本,确保这些文本连贯且符合我们对这些类型文本的预期。LSTM 在这里非常有用,因为它能够捕捉和记住长序列中复杂的模式。当我们用大量文本数据训练 LSTM 时,我们让它学习语言结构、风格和细微差别。它可以利用这些知识生成与训练集风格和方法一致的新句子。
假设我们正在和朋友玩一个词汇预测游戏。目标是创造一个故事,每个朋友都提供一个单词来继续这个故事。为了开始,我们有一组单词,我们称之为种子词,以设定故事的基调。从种子句子开始,每个朋友都会贡献下一个单词,直到故事完成。我们也可以将这个想法应用到 LSTM 中——我们将一个种子句子输入到模型中,然后让它预测下一个单词,就像我们玩词汇预测游戏一样。然而,这次游戏只有 LSTM 在进行,而我们只需要指定它会生成的单词数。每轮游戏的结果将作为下一轮输入,直到我们达到指定的单词数。
脑海中出现的问题是,LSTM 是如何知道接下来预测哪个单词的呢?这就是滑动窗口概念发挥作用的地方。假设我们有一个样本句子,比如“我曾经有一只狗叫做杰克”。当我们对这个句子应用大小为四的滑动窗口时,我们将得到以下结果:
-
“我曾经 有一只”
-
“曾经有 一只狗”
-
“曾经有 一只狗叫做”
-
“一只狗 叫做杰克”
我们现在可以将这些句子拆分为输入-输出对。例如,在第一个句子中,输入是“曾经有”,输出是“一只”。我们将对所有其他句子应用相同的方法,得到以下输入-输出对:
-
([“我”, “曾经”, “有”], “一只”)
-
([“曾经”, “有”, “一只”], “狗”)
-
([“有”, “一只”, “狗”], “叫做”)
-
([“一只”, “狗”, “叫做”], “杰克”)
通过使用滑动窗口,我们的 LSTM 专注于最最近的词汇集,这些词汇通常包含最相关的信息,用于预测下一个单词。而且,当我们处理较小的固定长度序列时,它能简化我们的训练过程,并优化内存使用。接下来,让我们看看如何将这一思想应用到下一个案例研究中。
使用 LSTM 进行故事生成
在这个案例研究中,假设你是一个新加入伦敦初创公司 Readrly 的 NLP 工程师。你的工作是为公司构建一个 AI 讲故事系统。你收到了一个名为stories.txt的训练数据集,其中包含 30 个示例故事。你的任务是训练一个 LSTM 来生成有趣的儿童故事。让我们回到我们的笔记本,看看如何实现这一目标:
-
如同我们之前做的那样,我们将从导入所有任务所需的库开始:
import tensorflow as tffrom tensorflow.keras.preprocessing.text import Tokenizerfrom tensorflow.keras.preprocessing.sequence import pad_sequencesfrom tensorflow.keras.models import Sequentialfrom tensorflow.keras.layers import Embedding, LSTM, Dense, Bidirectionalimport numpy as np -
然后,我们加载
stories.txt数据集:text = open('stories.txt').read().lower()
我们读取故事文件,并将所有内容转换为小写,以确保数据的一致性,避免由于大写和小写版本的同一单词而生成重复的标记。
注意
这一步有助于通过删除由大小写带来的语义差异来减少词汇量。然而,这一步应该谨慎操作,因为它可能产生负面影响——例如,当我们使用“march”这个词时。如果将其转为小写,它将表示某种形式的行走,而大写的 M 则指代月份。
-
对文本进行分词:
tokenizer = Tokenizer()tokenizer.fit_on_texts([text])total_words = len(tokenizer.word_index) + 1
我们计算词汇表中唯一单词的总数。我们加 1 以考虑词汇表外的单词。
-
然后,我们将文本转换为序列:
input_sequences = []for line in text.split('\n'):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)
在这一步中,我们开发一个由n元组序列组成的数据集,其中“输入序列”中的每个条目都是在文本中出现的单词序列(即,单词编号)。对于每个n个单词的序列,n-1 个单词将作为输入特征,n-th 个单词是我们模型尝试预测的标签。
注意
一个 n-gram 是从给定文本中提取的 n 个单词的序列。
例如,假设我们有一句示例句子:“The dog played with the cat.” 使用 2 元组(bigram)模型对这句话进行分割,将其分成以下的大写二元组:
(“The”, “dog”), (“dog”, “played”), (“played”, “with”), (“with”, “the”), (“the”, “cat”)
或者,如果我们使用 3 元组(trigram)模型,它会将句子分割成三元组:
(“The”, “dog”, “played”), (“dog”, “played”, “with”), (“played”, “with”, “the”), (“with”, “the”, “cat”)
N-gram 模型在自然语言处理(NLP)中广泛应用于文本预测、拼写纠正、语言建模和特征提取。
-
然后,我们对序列进行填充:
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'))
我们使用填充来确保输入数据格式的一致性。我们将padding设置为pre,以确保我们的 LSTM 在每个序列的末尾捕捉到最新的单词;这些单词对于预测序列中的下一个单词是相关的。
-
现在,我们将序列拆分为特征和标签:
predictors, label = input_sequences[:,:-1],input_sequences[:,-1]label = tf.keras.utils.to_categorical(label,num_classes=total_words)
在这里,我们通过将标签进行一热编码(one-hot encoding)来表示它们为向量。然后,我们构建我们的模型。
-
创建模型:
model = Sequential([Embedding(total_words, 200,input_length=max_sequence_len-1),Bidirectional(LSTM(200)),Dense(total_words, activation='softmax')])model.compile(loss='categorical_crossentropy',optimizer='adam', metrics=['accuracy'])history = model.fit(predictors, label, epochs=300,verbose=0)
我们使用双向 LSTM 构建一个文本生成模型,因为它能够捕捉过去和未来的数据点。我们的嵌入层将每个单词的数字表示转换为一个密集的向量,每个向量的维度为 200。接下来是具有 200 个单元的双向 LSTM 层,之后我们将输出数据传递给全连接层。然后,我们编译并训练模型 300 个周期。
-
创建一个函数来进行预测:
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')# Get the predictionspredictions = model.predict(token_list)# Get the index with the maximum prediction valuepredicted = np.argmax(predictions)output_word = ""for word,index in tokenizer.word_index.items():if index == predicted:output_word = wordbreakseed_text += " " + output_wordreturn seed_text
我们构建了 story_generator 函数。我们从种子文本开始,用它来提示模型为我们的儿童故事生成更多的文本。种子文本被转换为标记化形式,然后在开头进行填充,以匹配输入序列的预期长度 max_sequence_len-1。为了预测下一个词的标记,我们使用 predict 方法并应用 np.argmax 来选择最可能的下一个词。预测的标记随后被映射回对应的词,并附加到现有的种子文本上。这个过程会重复,直到生成所需数量的词(next_words),函数返回完整生成的文本(seed_text + next_words)。
-
让我们生成文本:
input_text= "In the hustle and bustle of ipoti"print(generate_text(input_text, 50, model,max_sequence_len))
我们定义了种子文本,这里是 input_text 变量。我们希望模型生成的下一个词的数量是 50,并传入已训练的模型以及 max_sequence_len。当我们运行代码时,它返回以下输出:
In the hustle and bustle of ipoti the city the friends also learned about the wider context of the ancient world including the people who had lived and worshipped in the area they explored nearby archaeological sites and museums uncovering artifacts and stories that shed light on the lives and beliefs of those who had come before
生成的文本示例确实像你可能在儿童故事书中看到的内容。它连贯且富有创意地继续了初始提示。虽然这个例子展示了 LSTM 作为文本生成器的强大功能,但在这个时代,我们利用 大语言模型(LLM)来实现这样的应用。这些模型在庞大的数据集上训练,并且具有更为复杂的语言理解能力,因此,当我们适当提示或微调它们时,它们可以生成更具吸引力的故事。
现在我们已经进入了本章关于自然语言处理(NLP)的最后部分。你现在应该掌握了构建自己的 NLP 项目所需的基础知识,使用 TensorFlow 来实现。你在这里学到的所有内容,也将帮助你有效地应对 TensorFlow 开发者证书考试中的 NLP 部分。你已经走了很长一段路,应该为自己鼓掌。我们还有一章内容。让我们在继续时间序列章节之前,回顾一下本章学到的内容。
总结
在本章中,我们踏上了探索 RNN 世界的旅程。我们首先了解了 RNN 及其变体的结构,然后通过不同的模型架构探索了新闻文章分类任务。我们进一步应用了预训练的词嵌入,提升了我们最优模型的性能。在此过程中,我们学会了如何在工作流中应用预训练词嵌入。作为最后的挑战,我们进行了构建文本生成器的任务,用来生成儿童故事。
在下一章中,我们将探讨时间序列,了解它的独特特点,并揭示构建预测模型的各种方法。我们将解决一个时间序列问题,在其中掌握如何准备、训练和评估时间序列数据。
问题
让我们测试本章所学:
-
从 TensorFlow 加载 IMDB 电影评论数据集并进行预处理。
-
构建一个 CNN 电影分类器。
-
构建一个 LSTM 电影分类器。
-
使用 GloVe 6B 嵌入将预训练的词嵌入应用到 LSTM 架构中。
-
评估所有模型,并保存表现最好的那个。
进一步阅读
若要了解更多信息,您可以查看以下资源:
-
Cho, K., 等. (2014). 使用 RNN 编码器-解码器学习短语表示用于统计机器翻译。arXiv 预印本 arXiv:1406.1078。
-
Hochreiter, S., & Schmidhuber, J. (1997). 长短期记忆。神经计算,9(8),1735–1780。
-
Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). 高效估计词向量表示方法。arXiv 预印本 arXiv:1301.3781。
-
递归神经网络的非理性有效性:
karpathy.github.io/2015/05/21/rnn-effectiveness/。
第四部分 – 使用 TensorFlow 处理时间序列
在这一部分,你将学习如何使用 TensorFlow 构建时间序列预测应用程序。你将了解如何对时间序列数据进行预处理并构建模型。在这一部分,你还将学习如何生成时间序列的预测。
本部分包括以下章节:
-
第十二章,时间序列、序列和预测简介
-
第十三章,使用 TensorFlow 进行时间序列、序列和预测
第十二章:时间序列、序列与预测概述
时间序列横跨各行各业,涉及我们生活的方方面面。金融、医疗、社会科学、物理学——你说得出,时间序列数据就存在。它出现在监测我们环境的传感器中,社交媒体平台追踪我们的数字足迹,在线交易记录我们的财务行为,等等。这种按时间顺序排列的数据代表着随时间变化的动态过程,随着我们逐步数字化地球,这类数据的量和其重要性将呈指数级增长。
时间序列遵循时间顺序,捕捉事件的发生。时间序列的这种时间特性赋予了它与横截面数据的独特区别。当我们聚焦于时间序列数据时,可以观察到如趋势、季节性、噪声、周期性和自相关等特征。这些独特的特性使得时间序列数据蕴含丰富的信息,但也为我们带来了一系列独特的挑战,我们必须克服这些挑战,以充分利用这种数据类型的潜力。借助TensorFlow等框架,我们可以利用过去的模式做出关于未来的明智决策。
本章将涵盖以下内容:
-
时间序列分析——特征、应用与预测技术
-
时间序列预测的统计学技术
-
使用神经网络为预测准备数据
-
使用神经网络进行销售预测
本章结束时,你将获得理论上的见解和实际操作经验,学习如何使用统计学和深度学习技术构建、训练和评估时间序列预测模型。
时间序列分析——特征、应用与预测技术
我们知道,时间序列数据是由数据点在时间序列中的顺序定义的。想象我们正在预测伦敦的能耗模式。多年来,由于城市化,能源消耗逐年增加——这表示一个正向上升的趋势。每年冬天,我们预期能源消耗会增加,因为更多的人需要为保持温暖而加热家中和办公室的空间。季节性的天气变化也导致了能源利用的季节性波动。此外,我们还可能因为一次重大体育赛事,看到能源消耗的异常激增,这可能是因为赛事期间大量客人的涌入。这种情况会在数据中产生噪声,因为这些事件通常是一次性的或不规律发生的。
在接下来的章节中,让我们一起探索时间序列的特征、类型、应用以及建模时间序列数据的技术。
时间序列的特征
为了有效地构建高效的预测模型,我们需要清楚地理解时间序列数据的基本性质。我们可能会遇到具有正向上升趋势、月度季节性、噪声和自相关性的时间序列数据,而下一个我们处理的时间序列可能具有年度季节性和噪声,但数据中没有明显的自相关性或趋势。
了解时间序列数据的这些数据特性,使我们能够具备必要的细节来做出明智的预处理决策。例如,如果我们处理的数据集波动性较大,我们对这一点的了解可能会促使我们在预处理步骤中应用平滑技术。在本章后面,我们将构建用于预测时间序列的统计学和深度学习模型,我们将看到对时间序列数据特性的理解如何指导我们在新特征工程、选择最优超参数值以及模型选择决策方面的判断。让我们来审视时间序列的特点:
- 趋势:趋势是指时间序列在长期内所呈现的总体方向。我们可以将趋势视为时间序列数据的整体大局。趋势可以是线性的,如图 12**.1所示,或者是非线性的(如二次型和指数型);它们也可以是正向(上升)或负向(下降)的:

图 12.1 – 显示股票价格随时间变化的正向趋势的图表
趋势分析使数据专业人员、企业和决策者能够做出关于未来的明智决策。
- 季节性:季节性是指在特定周期内(如日、周、月或年)定期发生的重复性周期。这些变化通常是季节性波动的副产品;例如,位于居民区的零售商店可能在周末的销售量比工作日更高(每周季节性)。同一家商店在 12 月的假期季节可能会见证销售激增,并在节庆过后不久出现销售下降(年度季节性),如图 12**.2所示:

图 12.2 – 显示零售商店的年度季节性的图表
-
周期性:周期性指的是在较长时间内,时间序列中发生的不规则周期性波动。与季节性不同,这些周期具有长期性质,其持续时间和幅度不规律,因此相比季节性,它们更难预测。经济周期是周期性的一个典型例子。这些周期受到通货膨胀、利率和政府政策等多种因素的影响。由于周期性的不规则性,预测周期的时机、持续时间和幅度是相当具有挑战性的。通常需要先进的统计学和机器学习模型来准确建模和预测周期模式。
-
自相关:自相关是一个统计概念,指的是时间序列与其滞后版本之间的相关性,如图 12.3所示。

图 12.3 – 显示自相关的图表
它通常被称为序列相关,衡量数据点与其过去值之间的关联程度。自相关可以是正相关或负相关,值的范围从 -1 到 1。
- 噪声:噪声是任何现实世界数据中的固有部分。它指的是数据中无法通过模型解释的随机波动,也无法通过任何已知的潜在因素、模式或结构性影响解释。这些波动可能来自各种来源,如测量误差或意外事件。例如,在金融市场中,政治公告等未预测到的事件可能会产生噪声,导致股价偏离其潜在趋势。噪声呈现出随机性和无法解释的变化,预测时间序列数据时可能需要平滑这些波动。
我们已经讨论了一些可能单独或共同出现在时间序列数据中的重要特征。接下来,让我们看看时间序列数据的类型。
时间序列数据的类型
时间序列可以分为以下几类:
-
平稳与非平稳
-
单变量和多变量
平稳与非平稳时间序列
平稳时间序列是指其统计特性(均值、方差和自相关)随时间保持恒定的时间序列。这种序列展示出周期性的模式和行为,未来很可能会重复出现。非平稳时间序列则相反。它不是平稳的,我们通常在许多现实场景中发现这类时间序列,其中序列可能会展示趋势或季节性。例如,我们可以预期滑雪度假村的月度销售在冬季达到峰值,而在淡季下滑。这个季节性成分会影响序列的统计特性。
单变量和多变量时间序列
单变量时间序列是一种只跟踪单一指标随时间变化的时间序列。例如,我们可以使用智能手表来跟踪我们每天步数的变化。另一方面,当我们跟踪多个指标随时间变化时,就会有多变量时间序列,如图 12.4所示。在这张图表中,我们看到通货膨胀和工资增长之间的相互关系。随着时间的推移,我们可以看到通货膨胀持续超过工资增长,使得普通人的实际收入和购买力下降:

图 12.4 – 多变量时间序列展示通货膨胀与工资增长的关系
多变量时间序列分析可以帮助我们考虑多个变量之间随时间变化的依赖关系和相互作用。接下来,让我们深入了解时间序列数据的重要性及其各种应用。
时间序列的应用
我们已经讨论了时间序列数据的类型和属性。通过应用机器学习技术,我们可以利用这些数据类型中蕴含的大量信息。接下来,让我们来看一些机器学习在时间序列中的重要应用:
-
预测:我们可以应用机器学习模型来预测时间序列;例如,我们可能希望预测零售商店未来的销售情况,从而指导库存决策。如果我们分析商店的销售记录,可能会发现一些规律,比如在假期季节销售增加,或者在特定月份销售下降。我们可以利用这些规律来训练模型,从而做出关于商店未来销售的预测,帮助相关利益方有效规划预期需求。
-
插补数据:缺失值在分析或预测时间序列数据时可能会带来重大挑战。一个有效的解决方案是应用插补方法,允许我们使用替代值填补缺失的数据点。例如,在图 12.5 (a)中,我们看到了一张展示一年温度值的图。我们很快注意到一些温度记录缺失了。在插补方法的帮助下,我们可以利用相邻天的数据来估算这些缺失值,如图 12.5 (b)所示:

图 12.5 – 展示随时间变化的温度图(a)有缺失值(b)没有缺失值
通过填补缺失值,我们现在拥有了一个完整的数据集,可以更好地用于分析和预测。
- 异常检测:异常是指显著偏离常规的数据点。我们可以应用时间序列分析来检测异常并潜在地识别出重要问题。例如,在信用卡交易中,如图 12.6所示,一笔异常大的交易可能表示欺诈活动:

图 12.6 – 交易金额激增的图示
通过使用时间序列分析来识别这些异常,银行可以迅速采取措施,减少潜在损失。
-
趋势分析:了解趋势可以为潜在现象提供宝贵的洞察。例如,国际能源机构显示,2022 年所有新售汽车中有 14%是电动汽车。这个趋势可能表明人们正在转向更加可持续的交通方式(见
www.iea.org/reports/global-ev-outlook-2023/executive-summary)。 -
季节性分析:时间序列的另一个有用应用是季节性分析。这对于指导能源消耗规划和基础设施扩展需求可能非常有用。
我们现在已经了解了一些时间序列数据的重要应用。接下来,让我们看看一些重要的时间序列预测技术。
时间序列预测技术
在本书中,我们将探讨两种主要的时间序列预测技术:统计方法和机器学习方法。统计方法通过使用数学模型来捕捉时间序列数据的趋势、季节性和其他组成部分,常见的模型包括自回归综合滑动平均(ARIMA)和季节性与趋势分解使用 LOESS(STL)。然而,这些方法超出了本书的范围。在这里,我们将使用更简单的统计方法,如朴素预测和移动平均来建立我们的基准,然后再应用不同的机器学习方法。在本章中,我们将重点使用深度神经网络(DNNs),而在下一章中,我们将应用递归神经网络(RNNs)和长短期记忆网络(LSTMs)。
每种方法都有其优缺点,最佳的时间序列预测方法在很大程度上取决于数据的具体特征和当前的问题。值得强调的是,时间序列预测是一个广泛的领域,仍有其他方法超出了本书的范围,您可以在以后的阶段进行探索。在我们开始建模时间序列问题之前,让我们先看看如何评估这种数据。
评估时间序列预测技术
为了有效评估时间序列预测模型,我们必须使用适当的指标来衡量其表现。在第三章《使用 TensorFlow 的线性回归》中,我们探讨了几种回归指标,如 MAE、MSE、RMSE 和 MAPE。我们可以使用这些指标来评估时间序列预测模型。然而,在本章中,我们将重点应用 MAE 和 MSE,以符合考试要求。我们使用 MAE 计算预测值与真实值之间绝对差异的平均值。通过这种方式,我们可以了解预测结果有多偏差。较小的 MAE 表示模型拟合较好。想象你是一名股票市场分析师,试图预测某只特定股票的未来价格。使用 MAE 作为评估指标,你可以清楚地了解你的预测与实际股票价格之间的平均差异。这些信息可以帮助你调整模型,做出更准确的预测,降低潜在的财务风险。
另一方面,MSE 计算的是预测值与实际值之间差异的平方平均值。通过对误差进行平方处理,MSE 比 MAE 对较大误差更加敏感,因此在误差较大的情况不利时,MSE 更为有用。例如,在处理电网时,精确的负荷预测至关重要,因此 MSE 尤为重要。考虑到这一点,现在让我们转向一个销售的使用案例,并应用我们的学习来预测未来的销售额。
零售店预测
想象一下,你作为一名机器学习工程师,刚刚接到一个新项目。佛罗里达州的一家快速增长的大型超市请求你的帮助。他们希望预测未来的评论,这将为他们扩展商店并满足预期需求提供指导。你负责根据 Tensor 超市提供的历史数据构建一个预测模型。让我们一起看看你如何解决这个问题,因为公司正指望你。我们开始吧!
-
我们首先导入项目所需的库:
import pandas as pdimport numpy as npimport matplotlib.pyplot as plt在这里,我们导入
numpy和matplotlib用于数值分析和可视化,pandas用于数据转换。 -
接下来,我们加载时间序列数据:
df = pd.read_csv('/content/sales_data.csv')df.head()在这里,我们加载数据并使用
head函数查看数据的前五行。当我们运行代码时,看到给定数据中的销售第一天是2013-01-01。接下来,让我们查看一些统计信息,以便了解我们手头的数据。 -
使用以下代码检查数据类型和汇总统计信息:
# Check data typesprint(df.dtypes)# Summary statisticsprint(df.describe())当我们运行代码时,它返回的数据类型为
float64,并显示我们销售数据的关键汇总统计信息。使用describe函数,我们得到了 3,653 个数据点的计数。这指示了一个 10 年的每日数据周期。我们还看到每日的平均销售额大约为 75 美元,这给了我们一个关于数据集中趋势的直观感受。每日销售额有着相当的波动,标准差为20.2。最小值和最大值显示销售额的范围从 22 美元到 128 美元,表明存在显著的波动。第 25 和第 75 百分位数分别为60.27和89.18,显示低销售量的日子销售额大约为 60 美元,而高销售量的日子销售额则大约为 90 美元。接下来,我们通过绘图进一步探索数据。 -
让我们可视化数据:
#Sales data plotdf.set_index('Date').plot()plt.ylabel('Sales')plt.title('Sales Over Time')plt.xticks(rotation=90)plt.show()代码返回了如图 12.7所示的图表,表示公司在 10 年期间的销售情况:

图 12.7 – 显示销售随时间变化的图表
从图 12.7中的图表中,我们可以观察到一个整体的积极上升趋势,这可能表明经济增长或成功的商业策略,如新产品发布和有效的市场营销。一个明显的年度季节性波动也浮现出来;这可能表明公司销售的是季节性商品,且有着每年波动的需求。同时,我们也观察到存在一些噪声。这可能是由于天气变化、随机事件或竞争对手的进入所导致。数据中的上升趋势显示了销售额随着时间的推移表现出良好的增长。然而,季节性效应和噪声因素揭示了总体趋势下更为复杂的动态。接下来,让我们探索如何对数据进行分区。
数据分区
在时间序列预测中,我们通常将数据集分成不同的部分:用于训练机器学习模型的训练期、用于模型调优和评估的验证期,以及用于评估未见数据性能的测试期。这个过程被称为固定分区。另一种方法是滚动前进分区,我们将在接下来的部分讨论这一方法。
我们的销售数据展示了季节性波动,因此,我们必须将数据按方式划分,以确保每个分区都包含完整的季节周期。我们这样做是为了确保不会遗漏一个或多个分区中的重要季节性模式。虽然这种方法与典型的数据分区方法有所不同,但我们看到在处理其他机器学习问题时,通常是通过随机抽样来形成训练集、验证集和测试集,基本目标保持一致。我们在训练数据上训练模型,使用验证数据进行调优,并在测试数据上评估模型。然后,我们可以将验证数据合并到训练数据中,以便利用最新的信息来预测未来的数据。
通常,确保训练集足够大以捕捉数据中所有相关模式是一个好主意,包括数据的季节性行为。在设置验证集的大小时,必须找到平衡点。虽然更大的验证集能够提供更可靠的模型性能估计,但它也会减少训练集的大小。你还应该记得在做出最终预测之前,使用整个数据集(将训练集和验证集合并)重新训练最终模型。这种策略最大化了模型从数据中学习的量,可能提高其对未来未见数据的预测性能。同样,避免在分割数据之前打乱数据,因为这会破坏时间顺序,导致误导性结果。在固定划分中,我们通常使用按时间顺序划分的方法,训练数据应该来自最早的时间戳,接着是验证集,最后是包含最新时间戳的测试集。让我们将销售数据分为训练集和验证集:
# Split data into training and validation sets
split_time = int(len(df) * 0.8)
train_df = df.iloc[:split_time]
valid_df = df.iloc[split_time:]
在这里,我们将数据分为训练集和验证集。我们取 80%的数据(len(df) * 0.8),在这个例子中是 8 年的数据用于训练,最后 2 年的数据用于验证。我们使用int函数确保分割时间是整数,以便进行索引操作。我们设置了训练集和验证集,使用分割时间之前的所有数据作为训练集,分割时间之后的所有数据作为验证集。
接下来,让我们绘制训练集和验证集的数据:
plt.figure(figsize=(12, 9))
# Plotting the training data in green
plt.plot(train_df['Date'], train_df['Sales'], 'green',
label = 'Training Data')
plt.plot(valid_df['Date'], valid_df['Sales'], 'blue',
label = 'Validation Data')
plt.title('Fixed Partitioning')
plt.xlabel('Date')
plt.ylabel('Sales')
all_dates = np.concatenate([train_df['Date'],
valid_df['Date']])
plt.xticks(all_dates[::180], rotation=90)
plt.legend()
plt.tight_layout()
plt.show()
这段代码展示了销售数据的划分,将训练集和验证集分别用绿色和蓝色标记,x轴表示日期,y轴表示销售额。为了提高可读性,我们将x轴的刻度设置为每 180 天一次:

图 12.8 – 显示固定划分的图表
在图 12.8中,我们将销售数据划分为 8 年的训练数据和 2 年的验证数据。在这种情况下,我们的测试集将是来自未来的数据。这是为了确保模型在时间序列的最早部分进行训练,在最近的过去进行验证,并在未来进行测试。另一种划分时间序列数据的方法被称为滚动前移划分或“前行”验证。在这种方法中,我们从一个较短的训练期开始,并逐渐增加训练期的长度。对于每个训练期,接下来的时间段作为验证集。这种方法模拟了现实生活中的情况,我们会随着新数据的到来不断重新训练模型,并利用它来预测下一时期。让我们讨论一下我们的第一种预测方法,叫做天真预测。
天真预测
朴素预测是时间序列分析中最简单的方法之一。朴素预测的原理是将所有预测值设为最后一个观察点的值。这就是为什么它被称为“朴素”的原因。它是一种假设未来值可能与当前值相同的方法。尽管其简单,但朴素预测通常可以作为时间序列预测的一个良好基准;然而,它的表现可能会根据时间序列的特征有所不同。
让我们看看如何在代码中实现这个:
-
让我们实现朴素预测:
# Apply naive forecastdf['Naive_Forecast'] = df['Sales'].shift(1)df.head()为了实现朴素预测方法,每个预测值仅设置为前一个时间步的实际观察值,通过将
Sales列向前移动一个单位来实现。我们使用df.head()来显示 DataFrame 的前五行,以快速概览销售数据和朴素预测:

图 12.9 – 显示销售和朴素预测的 DataFrame 快照
从图 12.9中的表格来看,我们看到第一个预测值是不可用的。这是因为该方法仅仅取从验证数据前一步开始,直到序列倒数第二个值的所有值。这实际上将时间序列向未来移动了一个时间步长。
-
我们创建一个用于绘图的函数:
def plot_forecast(validation_df, forecast_df,start_date=None, end_date=None,plot_title='Naive Forecasting',forecast_label='Naive Forecast'):if start_date:validation_df = validation_df[validation_df['Date'] >= start_date]forecast_df = forecast_df[forecast_df['Date'] >= start_date]if end_date:validation_df = validation_df[validation_df['Date'] <= end_date]forecast_df = forecast_df[forecast_df['Date'] <= end_date]# Extract the dates in the selected rangeall_dates = validation_df['Date']plt.figure(figsize=(12, 9))plt.plot(validation_df['Date'],validation_df['Sales'],label='Validation Data')plt.plot(forecast_df['Date'],forecast_df['Naive_Forecast'],label=forecast_label)plt.legend(loc='best')plt.title(plot_title)plt.xlabel('Date')plt.ylabel('Sales')# Set x-ticks to every 90th date in the selected rangeplt.xticks(all_dates[::90], rotation=90)plt.legend()plt.tight_layout()plt.show()我们构建一个实用的图表来生成真实和预测验证值的图形。该函数接受我们的验证数据的预测值和真实值,并包括开始日期、结束日期、图表标题和标签。该函数为我们提供了灵活性,可以深入探讨图表中的不同兴趣区域。
-
接下来,让我们绘制朴素预测:
plot_forecast(valid_df, valid_df,plot_title='Naive Forecasting',forecast_label='Naive Forecast')我们传入所需的参数并运行代码,生成以下图表:

图 12.10 – 使用朴素方法的时间序列预测
图 12.10中的图表显示了预测值和验证数据的真实值。由于值接近,图表看起来有点密集,因此让我们放大,以帮助我们在视觉上检查预测的效果。
-
让我们看一下特定的时间范围:
plot_forecast(valid_df, valid_df,start_date='2022-01-01', end_date='2022-06-30')我们将开始日期和结束日期参数设置为
2022-01-01和2022-06-30,得到的图表如下:

图 12.11 – 使用朴素方法的放大时间序列预测
从图 12.11中的图表可以看出,预测开始得晚了一步,因为朴素预测向未来移动了一个时间步。
-
接下来,让我们评估朴素方法的表现:
# Compute and print mean squared errormse = tf.keras.metrics.mean_squared_error(valid_df['Sales'], valid_df['Naive_Forecast']).numpy()print('Mean Squared Error:', mse)# Compute and print mean absolute errormae = tf.keras.metrics.mean_absolute_error(valid_df['Sales'],valid_df['Naive_Forecast']).numpy()print('Mean Absolute Error:', mae)我们使用
.numpy()提供的metrics函数,将 TensorFlow 张量的结果转换为 NumPy 数组。当我们运行代码时,得到朴素预测的均方误差(MSE)为45.22,朴素预测的平均绝对误差(MAE)为5.43。回想一下,对于 MSE 和 MAE 值,较低的值总是更好。
在我们探索其他预测技术时,记住这一点。朴素预测可以作为基准,用于比较更复杂模型的表现。接下来,我们将研究另一种统计方法,称为移动平均。
移动平均
移动平均是一种通过将每个数据点替换为邻近数据点的平均值来平滑时间序列数据的技术。在此过程中,我们生成一个新的序列,其中的数据点是原始数据序列中的数据点的平均值。此方法中的关键参数是窗口宽度,它决定了计算平均值时包括的连续原始数据点的数量。
“移动”一词指的是沿时间序列滑动窗口来计算平均值,从而生成一个新的序列。
移动平均的类型
时间序列分析中常用的两种主要移动平均类型是中心移动平均和滞后移动平均:
- 中心移动平均:中心移动平均围绕中心点(t)计算平均值。它使用兴趣时间点前后的数据进行可视化,如图 12.12所示。中心移动平均能够提供数据趋势的均衡视图,但由于它需要未来数据,因此不适用于预测,因为在进行预测时我们无法获得未来的值。中心移动平均适用于可视化和时间序列分析。

图 12.12 – 显示中心移动平均的图形
中心移动平均能够提供数据趋势的均衡视图,但由于它需要未来数据,因此不适用于预测,因为在进行预测时我们无法获得未来的值。中心移动平均适用于可视化和时间序列分析。
- 滞后移动平均:滞后移动平均,也称为滚动平均或运行平均,通过使用最近的n个数据点来计算平均值。此方法仅需要过去的数据点,如图 12.13所示,非常适合用于预测。

图 12.13 – 显示滞后移动平均的图形
计算滞后移动平均的第一步是选择窗口宽度 (W)。这个选择可以依赖于各种因素,例如序列的模式和你希望实现的平滑程度。较小的窗口宽度能够更紧密地跟踪快速变化,但这也可能会带来更多噪音。另一方面,较大的窗口宽度能够提供更平滑的曲线,但可能会错过一些短期波动。
让我们看看如何实现移动平均:
# Calculate moving average over a 30-day window
window =30
df['Moving_Average_Forecast'] = df['Sales'].rolling(
window=window).mean().shift(1)
代码使用 pandas 的rolling函数来计算 Sales 数据的 30 天窗口移动平均,然后将结果向前移动一步,以模拟下一时间步的预测,并将结果存储在新的 Moving_Average_Forecast 列中。你可以将其视为使用 30 天的销售数据来预测第 31 天的销售。
我们调用 plot_forecast 函数来绘制验证数据和移动平均预测数据。我们可以在图 12.14中看到结果图:

图 12.14 – 使用移动平均法进行时间序列预测
在图 12.14中,移动平均预测是使用过去 30 天的销售数据计算出来的。较小的窗口大小,例如 7 天的窗口,将比 30 天的移动平均更加贴近实际销售,但也可能会捕捉到数据中的更多噪音。
下一步逻辑就是再次使用metric函数评估我们的模型。这次,我们将移动平均预测与真实的验证值进行对比:
# Compute and print mean squared error
mse = tf.keras.metrics.mean_squared_error(
valid_df['Sales'],
df.loc[valid_df.index,
'Moving_Average_Forecast']).numpy()
print('Mean Squared Error:', mse)
# Compute and print mean absolute error
mae = tf.keras.metrics.mean_absolute_error(
valid_df['Sales'],
df.loc[valid_df.index,
'Moving_Average_Forecast']).numpy()
print('Mean Absolute Error:', mae)
print('Mean Squared Error for moving average forecast:',
mse)
当我们运行代码时,移动平均预测的 MSE 为 55.55,MAE 为 6.05。这两个值比我们的基准结果要差得多。如果你将窗口大小改为 7 天,我们最终得到的 MSE 和 MAE 分别为 48.57 和 5.61,这是一个更好的结果,但比我们天真的方法稍差。你可以尝试更小的窗口大小,看看结果是否能够超过基准值。
然而,我们需要注意的是,使用移动平均法的前提假设是平稳性。我们知道我们的时间序列同时包含趋势和季节性,那么我们该如何在这些数据上实现平稳性呢?这是否能帮助我们实现更低的 MAE?为了实现平稳性,我们使用一个叫做差分的概念。接下来让我们讨论差分,并看看如何应用它,以及它是否能帮助我们实现更低的 MAE 和 MSE。
差分
差分是用于实现时间序列平稳性的一种方法。它通过计算连续观测值之间的差异来工作。其逻辑是,尽管原始序列可能存在趋势并且非平稳,但序列值之间的差异可能是平稳的。通过对数据进行差分,我们可以去除趋势和季节性,从而使序列平稳,并使其适合用于移动平均预测模型。这可以显著提高模型的准确性,从而提高预测的可靠性。让我们在以下代码中查看这一点:
# Perform seasonal differencing
df['Differenced_Sales'] = df['Sales'].diff(365)
# Plotting the differenced sales data
plt.figure(figsize=(12, 9))
# Plotting the differenced data
plt.plot(df['Date'], df['Differenced_Sales'],
label = 'Differenced Sales')
plt.title('Seasonally Differenced Sales Data')
plt.xlabel('Date')
plt.ylabel('Differenced Sales')
# Select dates to be displayed on x-axis
all_dates = df['Date']
plt.xticks(all_dates[::90], rotation=90)
plt.legend()
plt.tight_layout()
plt.show()
上述代码块对我们的时间序列数据应用了差分。我们首先生成一个新序列,其中每个值与 365 天前的值之间存在差异。我们这样做是因为我们知道数据具有年度季节性。接下来,我们绘制我们的数据:

图 12.15 – 显示差分后销售时间序列的图表
我们可以看到,图 12.15 中的图表没有趋势或季节性。因此,我们已经实现了使用移动平均法时所需的平稳性假设。
现在让我们恢复趋势和季节性:
window=7
# Restore trend and seasonality
df['Restored_Sales'] = df['Sales'].shift(
365) + df['Differenced_Sales']
# Compute moving average on the restored data
df['Restored_Moving_Average_Forecast'] = df[
'Restored_Sales'].rolling(
window=window).mean().shift(1)
# Split into training and validation again
train_df = df.iloc[:split_time]
valid_df = df.iloc[split_time:]
# Get forecast and true values on validation set
forecast = valid_df['Restored_Moving_Average_Forecast']
true_values = valid_df['Sales']
在这里,我们将季节性重新融入到经过差分的时间序列数据中,然后对这个恢复的数据应用移动平均预测。我们使用一个 7 天窗口进行移动平均计算。通过将平移后的销售数据添加到差分后的销售数据中恢复趋势和季节性后,我们计算这些恢复的销售数据在所选窗口大小上的移动平均值,然后将结果序列向前移动一步进行预测。然后,我们再次将数据拆分为训练集和验证集。使用相同的拆分时间,以确保与之前的拆分一致。这对于确保我们正确评估模型至关重要。最后,我们通过从验证集中提取Restored_Moving_Average_Forecast和Sales值,准备进行预测值和真实销售值的评估:

图 12.16 – 恢复季节性和趋势后的销售预测
在图 12.16中的图表中,橙色线表示我们将过去的值重新加入以恢复趋势和季节性后的预测结果。回想一下,我们使用了一个大小为7的窗口,这使得我们得到了更低的 MAE 和 MSE 值。现在,我们本质上是利用差分序列的预测来预测从一年到下一年的变化,然后将这些变化加到一年前的值上,从而得到最终的预测值。
当我们计算恢复季节性和趋势的预测的 MSE 时,结果是 48.57,而恢复季节性和趋势的 MAE 为 5.61,这两个值明显低于未使用差分时的结果。我们还可以尝试平滑数据中的噪声,以提高 MAE 和 MSE 的得分。接下来,让我们看看如何使用机器学习,特别是使用 TensorFlow 的神经网络进行预测。
使用机器学习进行时间序列预测
到目前为止,我们已经使用统计方法取得了相当不错的结果。接下来,我们将使用深度学习技术对时间序列数据进行建模。我们将从掌握如何设置窗口数据集开始。我们还将讨论一些概念,如洗牌和批处理,并看看如何构建和训练一个神经网络来解决我们的销售预测问题。让我们首先掌握如何使用 TensorFlow 工具准备时间序列数据,以便使用窗口数据集方法进行建模:
-
我们从导入所需的库开始:
import tensorflow as tfimport numpy as np在这里,我们将使用 NumPy 和 TensorFlow 准备并处理我们的数据,使其符合建模所需的结构。
-
让我们创建一个简单的数据集。这里我们假设数据由两周的温度值组成:
# Create an array of temperatures for 2 weekstemperature = np.arange(1, 15)print(temperature)当我们打印温度数据时,得到以下内容:
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14]我们得到一个从 1 到 14 的数值数组,其中我们假设温度从第一天的 1 上升到第 14 天的 14。虽然有些奇怪,但我们就假设是这样。
-
让我们创建窗口化数据。现在我们有了数据,接下来需要创建一个数据点的“窗口”:
window_size = 3batch_size = 2shuffle_buffer = 10window_size参数指的是正在考虑的数据窗口。如果我们将窗口大小设置为3,这意味着我们将使用连续 3 天的温度值来预测下一天的温度。批量大小决定了在每次训练迭代中处理的样本数量,而shuffle_buffer指定了在洗牌数据时,TensorFlow 从多少个元素中随机采样。我们洗牌是为了避免顺序偏差;我们将在下一章进一步讨论这一点。 -
创建数据集的过程如下:
dataset = tf.data.Dataset.from_tensor_slices(temperature)for element in dataset:print(element.numpy())这行代码用于从我们的温度数据创建一个 TensorFlow
Dataset对象。这个DatasetAPI 是一个高层次的 TensorFlow API,用于读取数据并将其转换为机器学习模型可以使用的形式。接下来,我们遍历数据集并打印每个元素。我们使用numpy()将 TensorFlow 对象转换为 NumPy 数组。当我们运行代码时,我们得到数字 1-14;然而,现在它们已经准备好进行窗口化处理了。 -
接下来,让我们将温度数据转换成一个“窗口化”的数据集:
dataset = dataset.window(window_size + 1, shift=1,drop_remainder=True)for window in dataset:window_data = ' '.join([str(element.numpy()) for element in window])print(window_data)我们使用
window方法来创建一个窗口数据集,其中每个窗口本身也是一个数据集。window_size + 1参数意味着我们将window_size个元素作为输入,接下来的一个元素作为标签。shift=1参数表示窗口每次移动一步。drop_remainder=True参数表示如果最后几个元素无法形成完整窗口,我们将丢弃它们。当我们打印出我们的
window_data时,得到如下输出:After window:1 2 3 42 3 4 53 4 5 64 5 6 75 6 7 86 7 8 97 8 9 108 9 10 119 10 11 1210 11 12 1311 12 13 14我们看到现在窗口大小被设置为 3,
+1部分将作为标签。由于我们将shift值设置为1,下一个窗口将从序列中的第二个值开始,在这个例子中是2。接下来,窗口将继续移动一步,直到我们处理完所有窗口数据。 -
展平数据的过程如下:
dataset = dataset.flat_map(lambda window: window.batch(window_size + 1))for element in dataset:print(element.numpy())很容易将上一阶段创建的每个窗口视为一个单独的数据集。通过这段代码,我们将数据展平,使得每个窗口的数据作为主数据集中的一个单独批次打包。我们使用
flat_map将其展平回张量数据集,并使用window.batch(window_size + 1)将每个窗口数据集转换为批次张量。当我们运行代码时,得到如下结果:After flat_map:[1 2 3 4][2 3 4 5][3 4 5 6][4 5 6 7][5 6 7 8][6 7 8 9][ 7 8 9 10][ 8 9 10 11][ 9 10 11 12][10 11 12 13][11 12 13 14]我们可以看到,窗口数据现在已被放入批次张量中。
-
打乱数据的过程如下:
dataset = dataset.shuffle(shuffle_buffer)print("\nAfter shuffle:")for element in dataset:print(element.numpy())在这段代码中,我们对数据进行了打乱。这是一个重要步骤,因为打乱用于确保模型在训练过程中不会错误地学习到数据呈现顺序中的模式。当我们运行代码时,得到如下结果:
After shuffle:[5 6 7 8][4 5 6 7][1 2 3 4][11 12 13 14][ 7 8 9 10][ 8 9 10 11][10 11 12 13][ 9 10 11 12][6 7 8 9][2 3 4 5][3 4 5 6]我们可以看到主数据集中的小型数据集已被打乱。但是,请注意,打乱后的数据集中,特征(窗口)和标签保持不变。
-
特征和标签映射的过程如下:
dataset = dataset.map(lambda window: (window[:-1],window[-1]))print("\nAfter map:")for x,y in dataset:print("x =", x.numpy(), "y =", y.numpy())map方法对数据集的每个元素应用一个函数。在这里,我们将每个窗口分割成特征和标签。特征是窗口中的所有元素(除了最后一个元素,window[:-1]),标签是窗口中的最后一个元素,window[-1]。当我们运行代码时,看到如下结果:After map:features = [3 4 5] label = 6features = [1 2 3] label = 4features = [10 11 12] label = 13features = [ 8 9 10] label = 11features = [4 5 6] label = 7features = [7 8 9] label = 10features = [11 12 13] label = 14features = [5 6 7] label = 8features = [2 3 4] label = 5features = [6 7 8] label = 9features = [ 9 10 11] label = 12从打印结果来看,我们可以看到特征由三次观察值组成,并且下一个值是我们的标签。
-
批次处理和预取数据的过程如下:
dataset = dataset.batch(batch_size).prefetch(1)print("\nAfter batch and prefetch:")for batch in dataset:print(batch)batch()函数将数据集分成大小为batch_size的批次。在这里,我们创建了大小为2的批次。prefetch(1)性能优化函数确保 TensorFlow 在处理当前批次时,总是有一个批次已准备好。在进行这些转换后,数据集准备好用于训练机器学习模型。让我们打印出批次数据:After batch and prefetch:(<tf.Tensor: shape=(2, 3), dtype=int64,numpy=array([[10, 11, 12],[ 3, 4, 5]])>, <tf.Tensor: shape=(2,),dtype=int64, numpy=array([13, 6])>)(<tf.Tensor: shape=(2, 3), dtype=int64,numpy=array([[ 9, 10, 11],[11, 12, 13]])>, <tf.Tensor: shape=(2,),dtype=int64, numpy=array([12, 14])>)(<tf.Tensor: shape=(2, 3), dtype=int64,numpy=array([[6, 7, 8],[7, 8, 9]])>, <tf.Tensor: shape=(2,),dtype=int64, numpy=array([ 9, 10])>)(<tf.Tensor: shape=(2, 3), dtype=int64,numpy=array([[1, 2, 3],[4, 5, 6]])>, <tf.Tensor: shape=(2,),dtype=int64, numpy=array([4, 7])>)(<tf.Tensor: shape=(2, 3), dtype=int64,numpy=array([[2, 3, 4],[5, 6, 7]])>, <tf.Tensor: shape=(2,),dtype=int64, numpy=array([5, 8])>)(<tf.Tensor: shape=(1, 3), dtype=int64, numpy=array([[ 8, 9, 10]])>, <tf.Tensor: shape=(1,),dtype=int64, numpy=array([11])>)
我们看到数据集中的每个元素是一个特征和标签对的批次,其中特征是原始序列中window_size个值的数组,而标签是我们想要预测的下一个值。我们已经看到如何准备我们的时间序列数据进行建模;我们已经将其切片、窗口化、批处理、打乱,并将其拆分成特征和标签以供使用。
接下来,我们将利用在合成销售数据上学到的内容,来预测未来的销售值。
使用神经网络进行销售预测
让我们回到我们创建的销售数据,用于通过天真方法和移动平均方法预测销售。现在我们使用一个神经网络;在这里,我们将使用一个 DNN:
-
提取数据的过程如下:
time = pd.to_datetime(df['Date'])sales = df['Sales'].values在这段代码中,我们从
Sales数据框中提取Date和Sales数据。Date被转换为日期时间格式,Sales被转换为 NumPy 数组。 -
数据拆分的过程如下:
split_time = int(len(df) * 0.8)time_train = time[:split_time]x_train = sales[:split_time]time_valid = time[split_time:]x_valid = sales[split_time:]为了统一性,我们使用 80%的
split_time将数据分为训练集和验证集,采用相同的 80:20 分割方式。 -
创建窗口化数据集的过程如下:
def windowed_dataset(series, window_size, batch_size, shuffle_buffer):dataset = tf.data.Dataset.from_tensor_slices(series)dataset = dataset.window(window_size + 1,shift=1, drop_remainder=True)dataset = dataset.flat_map(lambda window:window.batch(window_size + 1))dataset = dataset.shuffle(shuffle_buffer).map(lambda window: (window[:-1], window[-1]))dataset = dataset.batch(batch_size).prefetch(1)return datasetdataset = windowed_dataset(x_train, window_size,batch_size, shuffle_buffer_size)我们创建了
windowed_dataset函数;该函数接收一个序列、一个窗口大小、一个批量大小和一个随机缓冲区。它为训练创建数据窗口,每个窗口包含window_size + 1个数据点。这些窗口随后被打乱并映射到特征和标签,其中特征是窗口中的所有数据点(除了最后一个),标签是最后一个数据点。然后,这些窗口被批处理并预取,以提高数据加载效率。 -
构建模型的过程如下:
model = tf.keras.models.Sequential([tf.keras.layers.Dense(10,input_shape=[window_size], activation="relu"),tf.keras.layers.Dense(10, activation="relu"),tf.keras.layers.Dense(1)])model.compile(loss="mse",optimizer=tf.keras.optimizers.SGD(learning_rate=1e-6, momentum=0.9))接下来,我们使用一个简单的前馈神经网络(FFN)进行建模。该模型包含两个具有 ReLU 激活函数的全连接层,随后是一个具有单个神经元的全连接输出层。模型使用 MSE 损失函数,并以随机梯度下降(SGD)作为优化器进行编译。
-
训练模型的过程如下:
model.fit(dataset, epochs=100, verbose=0)模型在窗口化的数据集上训练 100 个周期。
-
生成预测的过程如下:
input_batches = [sales[time:time + window_size][np.newaxis] for time in range(len(sales) - window_size)]inputs = np.concatenate(input_batches, axis=0)forecast = model.predict(inputs)results = forecast[split_time-window_size:, 0]在这里,我们批量生成预测结果以提高计算效率。只保留验证期的预测结果。
-
评估模型的过程如下:
print(tf.keras.metrics.mean_squared_error(x_valid,results).numpy())print(tf.keras.metrics.mean_absolute_error(x_valid,results).numpy())计算真实验证数据和预测数据之间的 MSE 和 MAE。在这里,我们得到了 MSE 为
34.51,MAE 为4.72,这超越了我们所有的简单统计方法。 -
可视化结果的过程如下。将真实验证数据和预测数据随时间绘制,以可视化模型的表现:

图 12.17 – 使用简单 FFN 的时间序列预测
从图 12.17中的图表来看,我们可以看到我们的预测值与验证集上的真实值高度吻合,只有少量噪声波动。我们的 FFN 在这次实验中表现出了显著的成就。与传统的统计方法相比,我们的模型在性能上有了显著的提升。你可以调整超参数并实现回调来提高性能。但我们的工作在这里已经完成。
我们在最后一章见面,那时我们将预测应用股票价格。到时候见。
总结
在本章中,我们探讨了时间序列的概念,研究了时间序列的核心特征和类型,并了解了时间序列在机器学习中的一些著名应用。我们还介绍了如滞后窗口和居中窗口等概念,并研究了如何借助 TensorFlow 的工具准备时间序列数据,以便用于神经网络建模。在我们的案例研究中,我们结合统计学方法和深度学习技术,为一家虚拟公司构建了一个销售预测模型。
在下一章中,我们将使用更复杂的架构,如 RNN、CNN 和 CNN-LSTM 架构,来扩展我们的建模,进行时间序列数据预测。同时,我们还将探索学习率调度器和 Lambda 层等概念。为了结束本书的最后一章,我们将构建一个苹果公司收盘股票价格的预测模型。
问题
-
使用提供的练习笔记本,将天真预测原理应用于“航空乘客”数据集。
-
在同一数据集上实现移动平均技术,并通过计算 MAE 和 MSE 值来评估其性能。
-
接下来,将差分方法引入到你的移动平均模型中。再次,通过确定 MAE 和 MSE 值来评估你的预测准确性。
-
手头有样本温度数据集,演示如何从这些数据中创建有意义的特征和标签。
-
最后,在数据集上尝试简单的 FFN 模型,并观察其性能。
第十三章:使用 TensorFlow 进行时间序列、序列和预测
欢迎来到我们与 TensorFlow 旅程的最后一章。在上一章中,我们通过应用神经网络(如 DNN)有效地预测时间序列数据,达到了一个高潮。本章中,我们将探索一系列高级概念,例如将学习率调度器集成到我们的工作流中,以动态调整学习率,加速模型训练过程。在前几章中,我们强调了找到最佳学习率的必要性和重要性。在构建带有学习率调度器的模型时,我们可以使用 TensorFlow 中内置的学习率调度器,或者通过制作自定义学习率调度器,以动态方式实现这一目标。
接下来,我们将讨论 Lambda 层,以及如何将这些任意层应用到我们的模型架构中,以增强快速实验的能力,使我们能够无缝地将自定义函数嵌入到模型架构中,尤其是在处理 LSTM 和 RNN 时。我们将从使用 DNN 构建时间序列模型转向更复杂的架构,如 CNN、RNN、LSTM 和 CNN-LSTM 网络。我们将把这些网络应用到我们的销售数据集案例研究中。最后,我们将从 Yahoo Finance 提取苹果股票的收盘价数据,并应用这些模型构建预测模型,预测未来的股价。
本章我们将涵盖以下主题:
-
理解并应用学习率调度器
-
在 TensorFlow 中使用 Lambda 层
-
使用 RNN、LSTM 和 CNN 进行时间序列预测
-
使用神经网络进行苹果股价预测
到本章结束时,你将对使用 TensorFlow 进行时间序列预测有更深入的理解,并拥有将不同技术应用于构建时间序列预测模型的实践经验,适用于真实世界的项目。让我们开始吧。
理解并应用学习率调度器
在第十二章,《时间序列、序列和预测简介》中,我们构建了一个 DNN,成功使用了 TensorFlow 中的 LearningRateScheduler 回调函数,我们可以通过一些内置技术在训练过程中动态调整学习率。让我们来看看一些内置的学习率调度器:
-
ExponentialDecay:从指定的学习率开始,在经过一定步数后以指数方式下降。 -
PiecewiseConstantDecay:提供分段常数学习率,你可以指定边界和学习率,将训练过程划分为多个阶段,每个阶段使用不同的学习率。 -
PolynomialDecay:该学习率是调度中迭代次数的函数。它从初始学习率开始,根据指定的多项式函数,将学习率逐渐降低至最终学习率。
让我们为第十二章《时间序列、序列和预测导论》中使用的前馈神经网络添加一个学习率调度器。我们使用相同的销售数据,但这次我们将应用不同的学习率调度器来提高模型的性能。让我们开始吧:
-
我们从导入该项目所需的库开始:
import numpy as npimport matplotlib.pyplot as pltimport tensorflow as tffrom tensorflow import keras -
接下来,让我们加载我们的数据集:
#CSV sales dataurl = 'https://raw.githubusercontent.com/oluwole-packt/datasets/main/sales_data.csv'# Load the CSV data into a pandas DataFramedf = pd.read_csv(url)我们从本书的 GitHub 仓库加载销售数据,并将 CSV 数据放入 DataFrame 中。
-
现在,我们将
Date列转换为日期时间格式并将其设置为索引:df['Date'] = pd.to_datetime(df['Date'])df.set_index('Date', inplace=True)第一行代码将日期列转换为日期时间格式。我们这样做是为了方便进行时间序列操作。接下来,我们将日期列设置为 DataFrame 的索引,这使得使用日期切片和处理数据变得更容易。
-
让我们从 DataFrame 中提取销售值:
data = df['Sales'].values在这里,我们从销售 DataFrame 中提取销售值并将其转换为 NumPy 数组。我们将使用这个 NumPy 数组来创建我们的滑动窗口数据。
-
接下来,我们将创建一个滑动窗口:
window_size = 20X, y = [], []for i in range(window_size, len(data)):X.append(data[i-window_size:i])y.append(data[i])正如我们在第十二章《时间序列、序列和预测导论》中所做的那样,我们使用滑动窗口技术将时间序列转换为包含特征和标签的监督学习问题。在这里,大小为 20 的窗口作为我们的
X特征,包含 20 个连续的销售值,而我们的y是这 20 个销售值之后的下一个即时值。这里,我们使用前 20 个值来预测下一个值。 -
现在,让我们将数据拆分为训练集和验证集:
X = np.array(X)y = np.array(y)train_size = int(len(X) * 0.8)X_train, X_val = X[:train_size], X[train_size:]y_train, y_val = y[:train_size], y[train_size:]我们将数据转换为 NumPy 数组,并将数据拆分为训练集和验证集。我们使用 80%的数据用于训练,20%的数据用于验证集,我们将用它来评估我们的模型。
-
我们的下一个目标是构建一个 TensorFlow 数据集,这是训练 TensorFlow 模型时更高效的格式:
batch_size = 128buffer_size = 10000train_data = tf.data.Dataset.from_tensor_slices((X_train, y_train))train_data = train_data.cache().shuffle(buffer_size).batch(batch_size).prefetch(tf.data.experimental.AUTOTUNE)我们应用
from_tensor_slices()方法从 NumPy 数组创建数据集。之后,我们使用cache()方法通过将数据集缓存到内存中来加速训练。接着,我们使用shuffle(buffer_size)方法随机打乱训练数据,以防止诸如顺序偏差等问题。然后,我们使用batch(batch_size)方法将数据拆分成指定大小的批次;在这个例子中,训练过程中每次将 128 个样本输入到模型中。接下来,我们使用prefetch方法确保 GPU/CPU 始终有数据准备好进行处理,从而减少一个批次处理完与下一个批次之间的等待时间。我们传入tf.data.experimental.AUTOTUNE参数,告诉 TensorFlow 自动确定预取的最佳批次数量。这使得我们的训练过程更加顺畅和快速。
我们的数据现在已经准备好进行建模。接下来,我们将使用 TensorFlow 内置的学习率调度器探索这个数据,然后我们将探讨如何使用自定义学习率调度器找到最佳学习率。
内置学习率调度器
我们将使用与 第十二章* 《时间序列、序列与预测入门》 中相同的模型。让我们定义模型并探索内置的学习率调度器:
-
我们将从模型定义开始:
# Modelmodel = Sequential()model.add(Dense(10, activation='relu',input_shape=(window_size,)))model.add(Dense(10, activation='relu'))model.add(Dense(1))在这里,我们使用了三个密集层。
-
接下来,我们将使用指数衰减学习率调度器:
# ExponentialDecaylr_exp = tf.keras.optimizers.schedules.ExponentialDecay(initial_learning_rate=0.1,decay_steps=100, decay_rate=0.96)optimizer = tf.keras.optimizers.Adam(learning_rate=lr_exp)model.compile(optimizer=optimizer, loss='mse')history_exp = model.fit(X_train, y_train, epochs=100)指数衰减学习率调度器设置了一个学习率,该学习率随着时间的推移按指数方式衰减。在此实验中,初始学习率设置为
0.1。这个学习率将在每 100 步时按 0.96 的衰减速率进行指数衰减,这是由decay_steps参数定义的。接下来,我们将我们的指数学习率分配给优化器并编译模型。之后,我们将模型拟合 100 个 epochs。 -
接下来,我们将使用 MAE 和 均方误差 (MSE) 评估模型的性能,并将验证预测与真实值进行比较:
# Evaluationforecast_exp = model.predict(X_val)mae_exp = mean_absolute_error(y_val, forecast_exp)mse_exp = mean_squared_error(y_val, forecast_exp)# Plotplt.plot(forecast_exp,label='Exponential Decay Predicted')plt.plot(y_val, label='Actual')plt.title('Exponential Decay LR')plt.legend()plt.show()这将生成以下输出:

图 13.1 – 使用指数衰减的真实预测与验证预测(放大版)
当我们运行代码块时,得到的 MAE 约为 5.31,MSE 为 43.18,并且从图 13.1 中的放大图可以看到,我们的模型紧密跟踪实际的销售验证数据。然而,结果并没有比我们在 第十二章* 《时间序列、序列与预测入门》中取得的更好。接下来,我们将尝试使用 PiecewiseConstantDecay。
-
让我们使用
PiecewiseConstantDecay学习率调度器:# PiecewiseConstantDecaylr_piecewise = tf.keras.optimizers.schedules.PiecewiseConstantDecay([30, 60], [0.1, 0.01, 0.001])optimizer = tf.keras.optimizers.Adam(learning_rate=lr_piecewise)model.compile(optimizer=optimizer, loss='mse')history_piecewise = model.fit(X_train, y_train,epochs=100)PiecewiseConstantDecay学习率调度器允许我们在训练过程中为不同的时期定义特定的学习率。在我们的例子中,我们将 30 和 60 步设为边界;这意味着在前 30 步中,我们应用学习率0.1,从第 30 步到第 60 步,我们应用学习率0.01,从第 61 步到训练结束,我们应用学习率0.001。对于PiecewiseConstantDecay,学习率的数量应该比应用的边界数多一个。例如,在我们的例子中,我们有两个边界([30, 60])和三个学习率([0.1, 0.01, 0.001])。设置好调度器后,我们使用相同的优化器,并像使用指数衰减学习率调度器一样编译和拟合模型。然后,我们评估模型的性能并生成以下验证图:

图 13.2 – 使用指数衰减的真实预测与验证预测
在这次实验中,我们实现了 4.87 的 MAE 和 36.97 的 MSE。这是一次改进的表现。再次,图 13.2中的预测很好地跟随了真实值。为了清晰起见,我们放大了一些。

图 13.3 – 我们前两个实验的放大图
从图 13.3中可以看到,我们在应用多项式衰减时,将视图放大到前 200 天,相较于使用指数衰减学习率调度器时,预测图更好地与真实值匹配。
-
现在让我们应用
PolynomialDecay:# PolynomialDecaylr_poly = tf.keras.optimizers.schedules.PolynomialDecay(initial_learning_rate=0.1,decay_steps=100,end_learning_rate=0.01,power=1.0)在这个实验中,我们将
initial_learning_rate设置为0.1,它作为我们的初始学习率。我们将decay_steps参数设置为100,表示学习率将在这 100 步中衰减。接下来,我们将end_learning_rate设置为0.01,这意味着在decay_steps结束时,学习率将降到此值。power参数控制步长衰减的指数。在本实验中,我们将power值设置为1.0,实现线性衰减。当我们评估模型的表现时,我们发现目前为止我们取得了最佳结果,MAE 为 4.72,MSE 为 34.49。从图 13.4中可以看到,预测结果与数据非常接近,比我们使用
PiecewiseConstantDecay时更为准确。

图 13.4 – 使用 PolynomialDecay 的真实预测与验证预测(放大)
现在,你已经大致了解了如何应用这些学习率调度器,接下来可以调整值,看看是否能实现更低的 MAE 和 MSE。当你完成后,我们来看看一个自定义学习率调度器。
通过简单地调整学习率,我们可以看到我们的PiecewiseConstantDecay学习率调度器在这场竞争中获胜了,不仅超越了其他学习率,还优于我们在第十二章《时间序列、序列和预测导论》中使用的相同架构的简单 DNN 模型。你可以通过文档www.tensorflow.org/api_docs/python/tf/keras/callbacks/LearningRateScheduler或 Moklesur Rahman 在 Medium 上的这篇优质文章rmoklesur.medium.com/learning-rate-scheduler-in-keras-cc83d2f022a6了解更多关于学习率调度器的内容。
自定义学习率调度器
除了使用内置的学习率调度器外,TensorFlow 还提供了一种简便的方法来构建自定义学习率调度器,帮助我们找到最佳学习率。接下来我们来实现这一点:
-
让我们从定义自定义学习率调度器开始:
# Define learning rate schedulelr_schedule = tf.keras.callbacks.LearningRateScheduler(lambda epoch: 1e-7 * 10**(epoch / 10))在这里,我们从一个较小的学习率(1×10−7)开始,并随着每个 epoch 的增加,学习率呈指数增长。我们使用
10**(epoch / 10)来确定学习率增长的速度。 -
我们使用初始学习率定义优化器:
# Define optimizer with initial learning rateoptimizer = tf.keras.optimizers.SGD(learning_rate=1e-7, momentum=0.9)这里,我们使用了一个学习率为 1×10−7 的 SGD 优化器,并设置动量为
0.9。动量帮助加速优化器朝正确的方向前进,同时也减少震荡。 -
接下来,我们用定义好的优化器编译模型,并将损失设置为 MSE:
model.compile(optimizer=optimizer, loss='mse') -
现在,我们开始训练模型:
history = model.fit(train_data, epochs=200,callbacks=[lr_schedule], verbose=0)我们训练模型 200 个 epoch,然后将学习率调度器作为回调传入。这样,学习率将根据定义的自定义学习率调度器进行调整。我们还设置了
verbose=0,以避免打印训练过程。 -
计算每个 epoch 的学习率:
lrs = 1e-7 * (10 ** (np.arange(200) / 10))我们使用这段代码来计算每个 epoch 的学习率,并且它会给我们一个学习率数组。
-
我们绘制模型损失与学习率的关系图:
plt.semilogx(lrs, history.history["loss"])plt.axis([1e-7, 1e-3, 0, 300])plt.xlabel('Learning Rate')plt.ylabel('Loss')plt.title('Learning Rate vs Loss')plt.show()这个图是选择最佳学习率的有效方法。

图 13.5 – 学习率损失曲线
为了找到最佳的学习率,我们需要找出损失最迅速下降的地方,在其开始再次上升之前。从图中可以看到,学习率下降并在大约 3x10-5 附近稳定,然后开始再次上升。因此,我们将选择这个值作为本次实验的理想学习率。接下来,我们将使用这个新的学习率作为固定学习率来重新训练模型。当我们运行代码时,我们得到 MAE 为 5.96,MSE 为 55.08。
我们现在已经看到如何使用内置的学习率调度器和自定义调度器。接下来,让我们将注意力转向使用 CNN 进行时间序列预测。
用于时间序列预测的 CNN
CNN 在图像分类任务中取得了显著的成功,因为它们能够检测网格状数据结构中的局部模式。这一思想同样可以应用于时间序列预测。通过将时间序列视为一系列时间间隔,CNN 可以提取和识别有助于预测未来趋势的模式。CNN 的另一个重要优势是它们的平移不变性。这意味着,一旦它们在一个片段中学习到某个模式,网络就能在序列中出现该模式的任何地方进行识别。这对于在时间步长之间检测重复模式非常有用。
CNN 的设置还通过池化层的帮助,自动减少输入数据的维度。因此,CNN 中的卷积和池化操作将输入序列转化为一种简化的形式,捕捉核心特征,同时确保计算效率。与图像不同,在这里我们使用 1D 卷积滤波器,因为时间序列数据的特性(单一维度)。这个滤波器沿着时间维度滑动,观察作为输入的局部值窗口。它通过在每个元素上的乘法和加和操作,检测这些区间中的有用模式。
多个滤波器被用来提取多样的预测信号——趋势、季节性波动、周期等。类似于图像中的模式,卷积神经网络(CNN)能够识别这些时间序列中的变换版本。当我们应用连续的卷积层和池化层时,网络将这些低级特征组合成更高级的表示,逐步将序列浓缩成其最显著的组成部分。最终,完全连接层利用这些学习到的特征进行预测。
让我们回到笔记本,并在我们的销售数据建模中应用 1D CNN。我们已经有了训练数据和测试数据。现在,为了使用 CNN 建模我们的数据,我们需要进行一个额外的步骤,即调整数据形状以满足 CNN 所期望的输入形状。在第七章**, 使用卷积神经网络进行图像分类中,我们看到 CNN 需要 3D 数据,而我们使用 DNN 建模时则使用 2D 数据;这里也是同样的情况。
CNN 需要一个批量大小、一个窗口大小和特征数量。批量大小是输入形状的第一维,它表示我们输入 CNN 的序列数量。我们将窗口大小设置为20,特征数量指的是每个时间步的独立特征数。对于单变量时间序列,这个值是1;对于多变量时间序列,这个值将是2或更多。
由于我们在案例研究中处理的是单变量时间序列,因此我们的输入形状需要类似于(128, 20, 1):
-
让我们准备数据以适应 CNN 模型的正确形状:
# Create sequenceswindow_size = 20X = []y = []for i in range(window_size, len(data)):X.append(data[i-window_size:i])y.append(data[i])X = np.array(X)y = np.array(y)# Train/val splitsplit = int(0.8 * len(X))X_train, X_val = X[:split], X[split:]y_train, y_val = y[:split], y[split:]# Reshape dataX_train = X_train.reshape(-1, window_size, 1)X_val = X_val.reshape(-1, window_size, 1)# Set batch size and shuffle bufferbatch_size = 128shuffle_buffer = 1000train_data = tf.data.Dataset.from_tensor_slices((X_train, y_train))train_data = train_data.shuffle(shuffle_buffer).batch(batch_size)这段代码中的大部分内容是相同的。这里的关键步骤是
reshape步骤,我们用它来实现 CNN 建模所需的输入形状。 -
让我们构建我们的模型:
# Build modelmodel = Sequential()model.add(Conv1D(filters=64, kernel_size=3,strides=1,padding='causal',activation='relu',input_shape=(window_size, 1)))model.add(MaxPooling1D(pool_size=2))model.add(Conv1D(filters=32, kernel_size=3,strides=1,padding='causal',activation='relu'))model.add(MaxPooling1D(pool_size=2))model.add(Flatten())model.add(Dense(16, activation='relu'))model.add(Dense(1))在我们的模型中,由于时间序列数据是单维的,我们采用了 1D 卷积层,而不是像用于图像分类的 2D 卷积神经网络(CNN),因为图像具有二维结构。我们模型由两个 1D 卷积层组成,每个卷积层后跟一个最大池化层。在我们的第一个卷积层中,我们使用了 64 个滤波器来学习各种数据模式,滤波器大小为
3,这使得它能够识别跨越三个时间步的模式。我们使用了步幅1,这意味着我们的滤波器每次遍历数据时只跨一步,为了确保非线性,我们使用了 ReLU 作为激活函数。请注意,我们使用了一种新的填充方式,称为因果填充。这种选择是有战略意义的,因为因果填充确保模型在某个时间步的输出仅受该时间步及其前置时间步的影响,永远不会受到未来数据的影响。通过在序列的开头添加填充,因果填充尊重了我们数据的自然时间序列。这对于防止模型“不经意地提前预测”至关重要,确保预测仅依赖于过去和当前的信息。我们之前提到,我们需要 3D 形状的输入数据来馈送到由批量大小、窗口大小和特征数量组成的 CNN 模型中。在这里,我们使用了
input_shape=(window_size, 1)。我们没有在输入形状定义中说明批量大小。这意味着模型可以接受不同大小的批量,因为我们没有硬编码任何批量大小。另外,由于我们处理的是单变量时间序列,所以我们只有一个特征,这就是为什么在输入形状中指定了1以及窗口大小的原因。最大池化层减少了我们数据的维度。接下来,我们进入第二个卷积层,这次我们使用了 32 个滤波器,核大小为3,依然使用因果填充和 ReLU 作为激活函数。然后,最大池化层再次对数据进行采样。之后,数据被展平并输入到全连接层中,根据我们从销售数据中学到的模式进行预测。 -
让我们编译并训练模型 100 个周期:
model.compile(loss='mse', optimizer='adam')# Train modelmodel.fit(train_data, epochs=100) -
最后,让我们评估一下我们模型的性能:
# Make predictionspreds = model.predict(X_val)# Calculate metricsmae = mean_absolute_error(y_val, preds)mse = mean_squared_error(y_val, preds)# Print metricsprint('MAE: ', mae)print('MSE: ', mse)我们通过生成 MAE 和 MSE 在验证集上评估模型。当我们运行代码时,获得了 5.37 的 MAE 和 44.28 的 MSE。在这里,你有机会通过调整滤波器数量、滤波器的大小等来看看是否能够获得更低的 MAE。
接下来,让我们看看如何使用 RNN 系列模型来进行时间序列数据预测。
时间序列预测中的 RNN
时间序列预测在机器学习领域提出了一个独特的挑战,它涉及基于先前观察到的顺序数据预测未来的值。一种直观的思考方式是考虑一系列过去的数据点。问题就变成了,给定这个序列,我们如何预测下一个数据点或数据点序列?这正是 RNN 展示其有效性的地方。RNN 是一种专门为处理序列数据而开发的神经网络。它们保持一个内部状态或“记忆”,存储迄今为止观察到的序列元素的信息。这个内部状态会在序列的每个步骤中更新,将新的输入信息与之前的状态合并。例如,在预测销售时,RNN 可能会保留有关前几个月销售趋势、过去一年的总体趋势以及季节性效应等数据。
然而,标准的 RNN 存在一个显著的局限性:“梯度消失”问题。这个问题导致在序列中很难保持和利用来自早期步骤的信息,特别是当序列长度增加时。为了解决这个问题,深度学习社区引入了先进的架构。LSTM 和 GRU 是专门设计用来解决梯度消失问题的 RNN 变种。由于它们内置的门控机制,这些类型的 RNN 能够学习长期依赖关系,控制信息在记忆状态中进出的流动。
因此,RNN、LSTM 和 GRU 可以是强大的时间序列预测工具,因为它们本身就包含了问题的时间动态。例如,在预测销售时,这些模型可以通过保持对先前销售时期的记忆来考虑季节性模式、假期、周末等因素,从而提供更准确的预测。
让我们在这里运行一个简单的 RNN,看看它在我们的数据集上的表现如何:
-
让我们从准备数据开始:
# Create sequencesseq_len = 20X = []y = []for i in range(seq_len, len(data)):X.append(data[i-seq_len:i])y.append(data[i])X = np.array(X)y = np.array(y)# Train/val splitsplit = int(0.8*len(X))X_train, X_val = X[:split], X[split:]y_train, y_val = y[:split], y[split:]# Create datasetbatch_size = 128dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train))dataset = dataset.shuffle(buffer_size=1024).batch(batch_size)在这里,你会看到我们准备数据的方式与使用 DNN 时相同,并且我们没有像在 CNN 模型中那样重新塑形数据。接下来会有一个简单的技巧,帮助你在我们的模型架构中实现这一点。
-
让我们定义我们的模型架构:
model = tf.keras.models.Sequential([tf.keras.layers.Lambda(lambda x: tf.expand_dims(x, axis=-1),input_shape=[None]),tf.keras.layers.SimpleRNN(40,return_sequences=True),tf.keras.layers.SimpleRNN(40),tf.keras.layers.Dense(1),tf.keras.layers.Lambda(lambda x: x * 100.0)])在使用 RNN 建模时,就像我们在使用 CNN 时所看到的那样,我们需要将数据重新塑形,因为我们的模型也需要 3D 形状的输入数据。然而,在你希望保持原始输入形状不变,以便进行不同模型的各种实验时,我们可以采用一个简单而有效的解决方案——Lambda 层。这个层是我们工具箱中的一个强大工具,允许我们对输入数据执行简单的任意函数,使其成为快速实验的绝佳工具。
使用 Lambda 层,我们可以执行逐元素的数学操作,例如归一化、线性缩放和简单的算术运算。例如,在我们的案例中,我们使用 Lambda 层将 2D 输入数据的维度扩展,以符合 RNN 的 3D 输入要求(
batch_size、time_steps和features)。在 TensorFlow 中,您可以利用 Keras API 的tf.keras.layers.Lambda来创建 Lambda 层。Lambda 层作为适配器,允许我们对数据进行微小调整,确保其格式适合我们的模型,同时保持原始数据不变,供其他用途。接下来,我们遇到两个每个有 40 个单元的简单 RNN 层。需要注意的是,在第一个 RNN 中,我们设置了return_sequence=True。我们在 RNN 和 LSTM 中使用此设置,当一个 RNN 或 LSTM 层的输出需要输入到另一个 RNN 或 LSTM 层时。我们设置此参数,以确保第一个 RNN 层会为序列中的每个输入返回一个输出。然后将输出传递到第二个 RNN 层,该层仅返回最终步骤的输出,然后将其传递到密集层,密集层输出每个序列的预测值。接下来,我们遇到了另一个 Lambda 层,它将输出乘以 100。我们使用这个操作来扩展输出值。 -
让我们编译并拟合我们的模型:
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=8e-4), loss='mse')# Train modelmodel.fit(dataset, epochs=100)momentum=0.9))
在这里,我们在验证集上获得了 4.84 的 MAE 和 35.65 的 MSE。这个结果稍微比我们使用 PolynomialDecay 学习率调度器时的结果要差一些。也许在这里,您有机会尝试不同的学习率调度器,以实现更低的 MAE。
接下来,让我们探索 LSTM。
时间序列预测中的 LSTM
在 NLP 部分,我们讨论了 LSTM 的能力及其相对于 RNN 的改进,解决了如梯度消失问题等问题,使得模型能够学习更长的序列。在时间序列预测的背景下,LSTM 网络可以非常强大。让我们看看如何将 LSTM 应用于我们的销售数据集:
-
让我们首先准备我们的数据:
# Create sequencesseq_len = 20X = []y = []for i in range(seq_len, len(data)):X.append(data[i-seq_len:i])y.append(data[i])X = np.array(X)X = X.reshape(X.shape[0], X.shape[1], 1)y = np.array(y)# Train/val splitsplit = int(0.8*len(X))X_train, X_val = X[:split], X[split:]y_train, y_val = y[:split], y[split:]# Set batch size and buffer sizebatch_size = 64buffer_size = 1000# Create datasetdataset = tf.data.Dataset.from_tensor_slices((X_train, y_train))dataset = dataset.shuffle(buffer_size).batch(batch_size)注意,由于我们不使用 Lambda 层,因此我们在这里使用
reshape步骤,以避免重复此代码块。请注意,我们将在此次实验和下一次使用 CNN-LSTM 架构的实验中使用它。 -
接下来,让我们定义我们的模型:
model_lstm = tf.keras.models.Sequential([tf.keras.layers.LSTM(50, return_sequences=True,input_shape=[None, 1]),tf.keras.layers.LSTM(50),tf.keras.layers.Dense(1)])我们在这里使用的架构是一个使用 LSTM 单元的 RNN 结构——第一层是一个具有 50 个神经元的 LSTM 层,它的
return_sequence参数设置为True,以确保返回的输出是完整的序列,这些输出会传递到最终的 LSTM 层。在这里,我们还使用了[None, 1]的输入形状。下一层也有 50 个神经元,并且输出一个单一的值,因为我们没有在此处设置return_sequence参数为True,该输出将传递到密集层进行预测。 -
下一步是编译模型。我们像以前一样编译并拟合我们的模型,然后对其进行评估。在这里,我们获得了 4.56 的 MAE 和 32.42 的 MSE,这是迄今为止最好的结果。

图 13.6 – 使用 LSTM 的真实预测与验证预测(放大显示)
从图中我们可以看到,预测值和真实值比我们迄今为止进行的任何其他实验都更加一致。让我们看看是否可以使用 CNN-LSTM 架构来改进我们的结果,进行下一个实验。
用于时间序列预测的 CNN-LSTM 架构
深度学习为时间序列预测提供了引人注目的解决方案,而这一领域中的一个显著架构是 CNN-LSTM 模型。该模型利用了 CNN 和 LSTM 网络的优势,为处理时间序列数据的独特特性提供了有效的框架。CNN 因其在图像处理任务中的表现而闻名,主要得益于其在图像中学习空间模式的能力,而在顺序数据中,它们能够学习局部模式。网络中的卷积层通过一系列滤波器对数据进行处理,学习并提取显著的局部和全局时间模式及趋势。这些特征作为原始数据的压缩表示,保留了关键信息,同时减少了维度。维度的减少导致更高效的表示,能够捕捉到相关模式。
一旦通过卷积层提取了显著特征,这些特征将作为网络 LSTM 层的输入。CNN-LSTM 模型在端到端学习能力上具有优势。在这种架构中,CNN 和 LSTM 各自扮演着互补的角色。CNN 层捕捉局部模式,而 LSTM 层从这些模式中学习时间关系。与独立架构相比,这种联合优化是 CNN-LSTM 模型性能提升的关键。让我们看看如何将这种架构应用到我们的销售数据中。由于我们之前已经多次查看过数据准备步骤,因此这里直接进入模型架构:
-
让我们使用卷积层来构建我们的模型:
# Build the Modelmodel = tf.keras.models.Sequential([tf.keras.layers.Conv1D(filters=64, kernel_size=3,strides=1,activation="relu",padding='causal',input_shape=[window_size, 1]),我们使用 1D 卷积层从值序列中检测模式,就像我们在 CNN 预测实验中做的那样。记得将
padding设置为causal,以确保输出大小与输入保持一致。我们将输入形状设置为[window_size, 1]。这里,window_size代表每个输入样本中的时间步长数量。1表示我们正在处理单变量时间序列。例如,如果我们将window_size设置为7,这意味着我们使用一周的数据进行预测。 -
接下来,我们的数据进入 LSTM 层,由 2 个 LSTM 层组成,每个 LSTM 层包含 64 个神经元:
tf.keras.layers.LSTM(64, return_sequences=True),tf.keras.layers.LSTM(64), -
然后,我们有密集层:
tf.keras.layers.Dense(30, activation="relu"),tf.keras.layers.Dense(10, activation="relu"),tf.keras.layers.Dense(1),LSTM 层为 CNN 层提取的特征添加了时间上下文,而全连接层则生成最终的预测值。在这里,我们使用了三层全连接层,并输出最终的预测结果。通过这种架构,我们实现了 4.98 的 MAE 和 40.71 的 MSE。效果不错,但不如单独使用 LSTM 模型的结果。
调整超参数以优化模型性能在这里是个好主意。通过调整学习率、批处理大小或优化器等参数,我们可能能够提升模型的能力。我们不会在这里详细讨论,因为你已经具备了处理这些的能力。接下来让我们继续探讨苹果股票价格数据,并运用我们所学的知识,创建一系列实验,预测苹果股票的未来价格,看看哪种架构最终会脱颖而出。
预测苹果股票价格数据
现在我们已经涵盖了 TensorFlow 开发者证书考试中关于时间序列的所有内容。让我们用一个实际的时间序列用例来结束这一章和本书。在这个练习中,我们将使用一个真实世界的数据集(苹果股票的收盘日价格)。接下来让我们看看如何进行操作。这个练习的 Jupyter notebook 可以在这里找到:github.com/PacktPublishing/TensorFlow-Developer-Certificate-Guide。让我们开始:
-
我们首先导入所需的库:
import numpy as npimport matplotlib.pyplot as pltimport tensorflow as tffrom tensorflow import kerasimport yfinance as yf这里我们使用了一个新的库
yfinance。它让我们能够访问苹果股票数据,用于我们的案例分析。
注意
如果导入失败,你可能需要运行pip install yfinance来让其正常工作。
-
创建一个 DataFrame:
df_apple = yf.Ticker(tickerSymbol)df_apple = df_apple.history(period='1d',start='2013-01-01', end='2023-01-01')我们通过使用
AAPL股票代码来创建一个 DataFrame,AAPL代表苹果公司(Apple)在股票市场中的代号。为此,我们使用来自yfinance库的yf.Ticker函数来访问苹果的历史数据,该数据来自 Yahoo Finance。我们将history方法应用到我们的Ticker对象上,以访问苹果的历史市场数据。在这里,我们将period设置为1d,意味着获取的是每日数据。同时,我们也设置了start和end参数,定义了我们想要访问的日期范围;在此案例中,我们收集了从 2013 年 1 月 1 日到 2023 年 1 月 31 日的 10 年数据。 -
接下来,我们使用
df.head()来查看我们 DataFrame 的快照。可以看到数据集由七列组成(Open,High,Low,Close,Volume,Dividends,和Stock Splits),如下图所示。

图 13.7 – 苹果股票数据快照
让我们来理解这些列的含义:
-
Open表示交易日的开盘价格。 -
High表示交易日内股票交易的最高价格。 -
Low表示交易日内股票交易的最低价格。 -
Close表示交易日的收盘价格。 -
交易量表示在交易日内成交的股票数量。这可以作为市场强度的一个指标。 -
股息表示公司如何将其收益分配给股东。 -
股票分割可以看作是公司通过拆分每一股股票,从而增加公司流通股本的一种行为。
-
让我们绘制每日收盘价:
plt.figure(figsize=(14,7))plt.plot(df_apple.index, df_apple['Close'],label='Close price')plt.title('Historical prices for AAPL')plt.xlabel('Date')plt.ylabel('Price')plt.grid(True)plt.legend()plt.show()当我们运行代码时,我们得到以下图表:

图 13.8 – 显示 2013 年 1 月到 2023 年 1 月期间苹果股票收盘价的图表
从图 13.8的图表中,我们可以看到股票呈现出正向上升的趋势,偶尔会有下跌。
-
将数据转换为 NumPy 数组:
Series = df_apple['Close'].values在这个练习中,我们将预测每日的股票收盘价。因此,我们取收盘价列并将其转换为 NumPy 数组。这样,我们为实验创建了一个单变量时间序列。
-
准备窗口数据集:
# Sliding windowwindow_size = 20X, y = [], []for i in range(window_size, len(data)):X.append(data[i-window_size:i])y.append(data[i])X = np.array(X)y = np.array(y)# Train/val splittrain_size = int(len(X) * 0.8)X_train, X_val = X[:train_size], X[train_size:]y_train, y_val = y[:train_size], y[train_size:]# Dataset using tf.databatch_size = 128buffer_size = 10000train_data = tf.data.Dataset.from_tensor_slices((X_train, y_train))train_data = train_data.cache().shuffle(buffer_size).batch(batch_size).prefetch(tf.data.experimental.AUTOTUNE)我们现在已经熟悉了这个代码块,它用于准备数据以进行建模。接下来,我们将使用相同的架构进行苹果股票数据集的实验。结果如下:
| 模型 | MAE |
|---|---|
| DNN | 4.56 |
| RNN | 2.24 |
| LSTM | 3.02 |
| CNN-LSTM | 18.75 |
图 13.9 – 显示各种模型的 MAE 表格
从我们的结果来看,我们使用 RNN 架构取得了最佳表现的模型,MAE 为 2.24。现在你可以保存你最好的模型,它可以用来预测未来的股票价值,或应用于其他预测问题。你也可以进一步调整超参数,看看是否能达到更低的 MAE。
注释
该模型展示了利用神经网络进行预测的可能性。然而,我们必须意识到它的局限性。请不要使用此模型做出金融决策,因为现实世界的股市预测涉及复杂的关系,如经济指标、市场情绪以及其他相互依赖性,而我们的基础模型并未考虑这些因素。
这样,我们就来到了本章和整本书的结尾。
总结
在本章的最后,我们探讨了一些使用 TensorFlow 进行时间序列预测的高级概念。我们了解了如何使用内建的学习率调度器,也学习了如何设计定制的调度器来满足我们的需求。接着,我们使用了更专业的模型,如 RNN、LSTM 网络,以及 CNN 和 LSTM 的组合。我们还学习了如何应用 Lambda 层来实现自定义操作并为我们的网络架构增加灵活性。
本章总结时,我们进行了苹果股票收盘价的预测。到本章结束时,你应该已经充分理解如何应用学习率调度器、Lambda 层等概念,并且能够使用不同架构有效地构建时间序列预测模型,为你的考试做好准备。祝你好运!
作者的话
看到你从机器学习的基础知识到使用 TensorFlow 构建各种项目,我感到非常高兴。你现在已经探索了如何使用不同的神经网络架构来构建模型。你已经拥有了坚实的基础,可以并且应该在此基础上,作为一名认证的 TensorFlow 开发者,构建一个令人惊叹的职业生涯。你只有通过构建解决方案,才能成为顶尖开发者。
本书中涵盖的所有内容将帮助你在最终准备 TensorFlow 开发者证书考试以及更远的未来中取得成功。我想祝贺你没有放弃,成功完成了本书中的所有概念、项目和练习。我鼓励你继续学习、实验,并保持对机器学习领域最新发展的关注。祝你考试顺利,并在未来的职业生涯中取得成功。
问题
-
从 Yahoo Finance 加载 2015-01-01 到 2020-01-01 的 Google 股票数据。
-
创建训练、预测和绘图函数。
-
准备数据以进行训练。
-
构建 DNN、CNN、LSTM 和 CNN-LSTM 模型来建模数据。
-
使用 MAE 和 MSE 评估模型。
参考文献
-
Shi, X., Chen, Z., Wang, H., Yeung, D. Y., Wong, W. K., & Woo, W. C. (2015). 卷积 LSTM 网络:一种用于降水短期预测的机器学习方法。在神经信息处理系统的进展(第 802–810 页)
papers.nips.cc/paper/2015/hash/07563a3fe3bbe7e3ba84431ad9d055af-Abstract.xhtml -
Karim, F., Majumdar, S., Darabi, H., & Harford, S. (2019). 用于时间序列分类的 LSTM 全卷积网络。IEEE Access, 7, 1662-1669
-
Siami-Namini, S., Tavakoli, N., & Siami Namin, A. (2019). LSTM 和 BiLSTM 在时间序列预测中的表现。2019 IEEE 大数据国际会议
-
TensorFlow 学习率调度器:
www.tensorflow.org/api_docs/python/tf/keras/callbacks/LearningRateScheduler -
时间序列 预测:
www.tensorflow.org/tutorials/structured_data/time_series -
tf.data:构建 TensorFlow 输入 管道。
www.tensorflow.org/guide/data -
时间序列的窗口数据集:
www.tensorflow.org/tutorials/structured_data/time_series#data_windowing


浙公网安备 33010602011771号