生产就绪的应用深度学习指南-全-

生产就绪的应用深度学习指南(全)

原文:zh.annas-archive.org/md5/e14f18bbb2088c66db1babf46e8b6eee

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着对人工智能AI)日益增长的兴趣,有数以百万计的资源介绍了各种深度学习DL)技术,解决了广泛的问题。它们可能足以让您获得许多朋友梦寐以求的数据科学家职位。然而,您很快就会发现,DL 项目的真正困难不仅在于为给定问题选择正确的算法,还包括以正确的格式高效预处理必要的数据并提供稳定的服务。

本书将引导您完成深度学习项目的每一个步骤。我们从在笔记本中编写的概念验证模型开始,将该模型转变为服务或应用程序,旨在在部署后最大化用户满意度。然后,我们使用亚马逊网络服务AWS)有效地提供稳定的服务。此外,我们还将看看如何在部署后监控运行深度学习模型的系统,从而完全闭环。

在整本书中,我们重点介绍了工程师们在技术前沿日常使用的各种技术,以满足严格的服务规范要求。

完成本书阅读后,您将更广泛地了解大规模部署深度学习应用的真实困难,并能够以最高效、最有效的方式克服这些挑战。

本书适合对象

机器学习工程师、深度学习专家和数据科学家会发现本书在通过详细示例缩小理论与应用之间的差距方面很有帮助。具备机器学习或软件工程的初学者级知识将有助于您轻松掌握本书涵盖的概念。

本书内容涵盖

第一章, 深度学习驱动项目的有效规划,全面介绍了如何准备深度学习项目。我们介绍了项目规划中使用的各种术语和技术,并描述了如何构建项目手册,总结计划。

第二章, 深度学习项目的数据准备,描述了深度学习项目的第一步,即数据收集和数据准备。本章介绍了如何为项目准备笔记本设置,收集必要的数据,并有效地为训练深度学习模型进行处理。

第三章, 开发强大的深度学习模型,解释了深度学习的理论及如何使用最流行的框架 PyTorch 和 TensorFlow 开发模型。

第四章, 实验跟踪、模型管理和数据集版本控制,介绍了一系列有用的工具,用于实验跟踪、模型管理和数据集版本控制,从而有效管理深度学习项目。

第五章云中的数据准备,专注于使用 AWS 扩展数据处理流水线。具体来说,我们看看如何以成本效益的方式设置和调度提取、转换和加载ETL)作业。

第六章高效模型训练,首先描述了如何配置 TensorFlow 和 PyTorch 的训练逻辑,以利用不同机器上的多个 CPU 和 GPU 设备。然后,我们看看为分布式训练开发的工具:SageMaker、Horovod、Ray 和 Kubeflow。

第七章揭示深度学习模型的秘密,介绍了超参数调整,这是找到正确训练配置的最标准过程。我们还涵盖了可解释 AI,一套用于了解 DL 模型在幕后工作的过程和方法。

第八章简化深度学习模型部署,描述了如何利用开放神经网络交换ONNX),这是用于机器学习模型的标准文件格式,将模型转换为各种框架,有助于将模型开发与模型部署分离。

第九章扩展深度学习流水线,介绍了两个最流行的 AWS 特性,旨在将 DL 模型部署为推理端点:弹性 Kubernetes 服务EKS)和 SageMaker。

第十章提高推理效率,介绍了在部署过程中如何通过网络量化、权重共享、网络修剪、知识蒸馏和网络架构搜索等技术来提高推理延迟,同时尽可能保持原始性能。

第十一章移动设备上的深度学习,描述了如何使用 TensorFlow Lite 和 PyTorch Mobile 在移动设备上部署 TensorFlow 和 PyTorch 模型。

第十二章监控生产中的深度学习端点,解释了用于监控运行中 DL 模型系统的现有解决方案。具体来说,我们讨论了如何将 CloudWatch 集成到在 SageMaker 和 EKS 集群上运行的端点中。

第十三章审查完成的深度学习项目,涵盖了 DL 项目的最后阶段,审查过程。我们描述了如何有效评估项目并为下一个项目做准备。

要充分利用本书

尽管我们在旅程中将与许多工具互动,所有安装说明均包含在书籍和 GitHub 存储库中。在阅读本书之前,您唯一需要准备的是 AWS 账户。AWS 提供免费层级(aws.amazon.com/free),应该足以让您开始。

书中涉及的软件/硬件 操作系统要求
TensorFlow Windows、macOS 或 Linux
PyTorch
Docker
Weights & Biases、MLflow 和 DVC
ELI5 and SHAP
Ray and Horovod
AWS SageMaker
AWS EKS

如果您想尝试运行本书中的示例,请使用我们仓库或官方文档页面上的完整版本,因为书中的版本可能会缺少某些组件,以增强内容的传递效果。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,链接为github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning。如果代码有更新,将在 GitHub 仓库中更新。

我们还有其他代码包,来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/查看!

下载彩色图像

我们还提供一个 PDF 文件,其中包含本书中使用的截图和图表的彩色图像。您可以在此处下载:packt.link/fUhAv

使用的约定

本书中使用了许多文本约定。

文本中的代码: 表示文本中的代码词汇,数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 句柄。例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一磁盘。”

代码块设置如下:

html, body, #map {
 height: 100%; 
 margin: 0;
 padding: 0
}

当我们希望引起您对代码块特定部分的注意时,相关行或条目将设置为粗体:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出均以以下格式编写:

$ mkdir css
$ cd css

粗体: 表示一个新术语、重要词或您在屏幕上看到的词。例如,菜单或对话框中的词以粗体显示。例如:“从管理面板中选择系统信息。”

提示或重要注意事项

显示如下。

联系我们

我们的读者的反馈总是受欢迎的。

一般反馈: 如果您对本书的任何方面有疑问,请通过电子邮件联系我们,邮件地址为 customercare@packtpub.com,并在主题中提到书名。

勘误: 尽管我们已竭尽全力确保内容的准确性,但错误确实偶尔会发生。如果您发现本书中的错误,请向我们报告。请访问www.packtpub.com/support/errata并填写表单。

盗版: 如果您在互联网上发现我们作品的任何非法副本,请向我们提供位置地址或网站名称。请通过链接至版权@packt.com 与我们联系。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有意参与书籍的撰写或贡献,请访问 authors.packtpub.com

分享您的想法

一旦您阅读完《生产就绪的应用深度学习》,我们很乐意听取您的想法!请点击此处前往亚马逊书评页面并分享您的反馈。

您的评论对我们和技术社区非常重要,将帮助我们确保提供优质的内容。

第一部分:建立最小可行产品

AI 项目始于规划和理解给定问题的难度。一旦项目范围明确定义,下一步是创建最小可行产品MVP)。对于基于深度学习的项目,此过程涉及准备数据集并探索各种模型架构,以找到解决问题的可行方案。在本书的第一部分中,我们解释如何通过利用各种资源高效地完成上述步骤。

本部分包括以下章节:

  • 第一章, 深度学习驱动项目的有效规划

  • 第二章, 深度学习项目的数据准备

  • 第三章, 开发强大的深度学习模型

  • 第四章, 实验跟踪、模型管理和数据集版本控制

第一章:有效规划深度学习驱动项目

在书的第一章中,我们将介绍什么是深度学习DL),以及 DL 项目通常如何进行。本章以对 DL 的介绍开始,提供一些关于它如何影响我们日常生活的见解。然后,我们将把重点转移到 DL 项目上,描述它们的结构。在整个章节中,我们将特别强调第一阶段,即项目规划;您将学习到关键概念,如理解业务目标、如何定义适当的评估指标、识别利益相关者、资源规划,以及最小可行产品MVP)和完全特性产品FFP)之间的区别。到本章末,您应该能够制定一个 DL 项目手册,清楚地描述项目的目标、里程碑、任务、资源分配及其时间表。

在本章中,我们将讨论以下主要主题:

  • DL 是什么?

  • 理解 DL 在我们日常生活中的作用

  • DL 项目概述

  • 规划 DL 项目

技术要求

您可以从以下 GitHub 链接下载本章的补充材料:

github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_1

DL 是什么?

自 DL 出现以来仅过去了十年,但它已经迅速开始在我们的日常生活中扮演重要角色。在人工智能AI)领域内,存在一套被归类为机器学习ML)的流行方法。通过从历史数据中提取有意义的模式,ML 的目标是构建一个能为新收集的数据做出合理预测和决策的模型。DL 是一种 ML 技术,利用人工神经网络ANNs)来捕捉给定模式。图 1.1展示了自 20 世纪 50 年代开始的 AI 演变的关键组成部分,包括艾伦·图灵等 AI 领域的先驱讨论智能机器。虽然自 AI 问世以来引入了各种 ML 算法,但实际上,这个领域花了几十年才真正开花结果。同样地,自 DL 成为主流也只有大约十年,因为这个领域的许多算法需要大量的计算能力。

图 1.1 – AI 的历史

图 1.1 – AI 的历史

图 1.2所示,DL 的关键优势来自于人工神经网络(ANNs),它们能够自动选择必要的特征。类似于人类大脑的结构,ANNs 也由称为神经元的组件组成。一组神经元形成一个,多个层链接在一起形成一个网络。这种架构可以理解为多个嵌套指令的多步骤。随着输入数据通过网络传递,每个神经元提取不同的信息,模型被训练为选择给定任务最相关的特征。考虑到神经元作为模式识别的构建块,更深的网络通常导致更好的性能,因为它们更好地学习细节。

图 1.2 - 机器学习与深度学习之间的区别

图 1.2 - 机器学习与深度学习之间的区别

虽然典型的机器学习技术需要手动选择特征,但 DL 学会了自动选择重要的特征。因此,它可能适用于更广泛的问题。然而,这种优势并非免费。为了正确训练 DL 模型,训练数据集需要足够大且多样化,这导致训练时间增加。有趣的是,图形处理单元GPU)在缩短训练时间方面发挥了重要作用。虽然中央处理单元CPU)通过其广泛的指令集在执行复杂计算方面表现有效,但 GPU 更适合通过其大规模并行性处理更简单但更大的计算。通过利用 DL 模型严重依赖的矩阵乘法中的这种优势,GPU 已成为 DL 中的关键组成部分。

由于我们仍处于人工智能时代的早期阶段,芯片技术正在不断发展,目前尚不清楚这些技术将如何在未来发生变化。值得一提的是,新设计不仅来自初创企业,还来自大型科技公司。这场持续的竞争清楚地显示出,基于人工智能的产品和服务将不断推出。考虑到市场增长和工作机会的增加,我们认为现在是学习 DL 的绝佳时机。

要记住的事情

a. DL 是一种利用人工神经网络进行模式识别的机器学习技术。

b. DL 非常灵活,因为它在整个训练过程中自动选择给定任务的最相关特征。

c. GPU 可以通过其大规模并行性加速 DL 模型的训练。

现在我们高层次地理解了 DL 是什么,接下来我们将在下一节中描述它如何塑造我们的日常生活。

理解 DL 在我们日常生活中的角色

通过利用 DL 的灵活性,研究人员在传统 ML 技术性能有限的领域取得了显著进展(见图 1.3)。首先在计算机视觉CV)领域为数字识别和目标检测任务打下了基础。然后,DL 被采用于自然语言处理NLP),在翻译和语音识别任务中显示出显著进展。它还展示了在强化学习RL)以及生成建模中的有效性。

本章的进一步阅读部分列出了与 DL 相关的论文的流行用例。

下图显示了 DL 的各种应用:

图 1.3 – DL 的应用

图 1.3 – DL 的应用

然而,将 DL 集成到现有技术基础设施中并非易事;困难可能来自各个方面,包括但不限于预算、时间以及人才。因此,对 DL 的深入理解已成为那些管理 DL 项目的人必备的技能:项目经理、技术负责人以及高管。此外,对这一快速增长领域的了解将使他们能够在特定垂直领域及整个组织中找到未来的机会,因为从事 AI 项目的人积极收集新知识以获得创新和竞争优势。总体而言,深入了解 DL 管道并开发可投入生产的输出使管理者能够通过有效避免已知的常见问题更好地执行项目。

不幸的是,DL 项目尚未达到即插即用的状态。在许多情况下,它们涉及广泛的研究和调整阶段,这可能迅速耗尽可用资源。最重要的是,我们注意到许多公司在从概念验证转向生产时遇到困难,因为关键决策是由少数只对项目需求和 DL 管道有有限理解的人做出的。话虽如此,我们的书旨在全面介绍 DL 项目的全貌;我们将从项目规划开始,然后讨论如何开发 MVP 和 FFP,如何利用云服务进行扩展,最后讨论如何将产品交付给目标用户。

需要记住的事情

a. DL 已应用于各个领域的许多问题,包括但不限于 CV、NLP、RL 和生成建模。

b. 对 DL 的深入理解对于领导 DL 项目的人员至关重要,无论其职位或背景如何。

c. 本书通过描述 DL 项目从项目规划到部署的过程,提供了对 DL 项目的全面理解。

接下来,我们将仔细研究 DL 项目的结构。

DL 项目概述

尽管深度学习和其他软件工程项目有很多共同点,但深度学习项目强调规划,主要来自模型复杂性和涉及的大量数据的资源需求。一般而言,深度学习项目可以分为以下阶段:

  1. 项目规划

  2. 构建 MVP

  3. 构建 FFP

  4. 部署和维护

  5. 项目评估

在本节中,我们提供了这些阶段的高级概述。接下来的部分将详细介绍每个阶段。

项目规划

作为第一步,项目负责人必须明确定义项目需要实现的目标,并了解可能影响或被项目影响的群体。必须定义并达成一致的评估指标,因为它们将在项目评估期间重新审视。然后,团队成员集结在一起,讨论个人责任,并利用现有资源实现业务目标。这个过程自然而然地导致了一个时间表,即项目需要多长时间完成的估计。总体而言,项目规划应该产生一个称为 playbook 的文件,其中包括项目如何进行和评估的详细描述。

构建最小可行产品

一旦方向对每个人都清晰,下一步是构建 MVP,这是目标交付物的简化版本,展示项目的价值。此阶段的另一个重要方面是了解项目的困难,并通过“快速失败,频繁失败”的哲学拒绝风险更大或前景不明确的路径。因此,数据科学家和工程师通常在开发环境中使用部分采样的数据集,并忽略不重要的优化。

构建完整功能产品

一旦 MVP 项目的可行性得到确认,必须将其打包成 FFP。此阶段旨在优化 MVP,构建一个可投入生产的交付成果,并进行各种优化。对于深度学习项目,引入额外的数据准备技术以提高输入数据的质量,或者稍微增强模型管道以获得更好的模型性能。此外,数据准备管道和模型训练管道可能会迁移到云端,利用各种网络服务提高吞吐量和质量。在这种情况下,整个管道通常会使用不同的工具和服务重新实现。本书专注于处理大量数据和昂贵计算的最流行网络服务亚马逊网络服务(AWS)

部署和维护

在许多情况下,部署设置与开发设置不同。因此,在将 FFP 移至生产环境时通常涉及不同的工具集。此外,部署过程可能会引入在开发过程中看不到的问题,这主要是由于计算资源有限而引起的。因此,许多工程师和科学家在此阶段花费额外时间来改善用户体验。大多数人认为部署是最后一步。然而,还有一个步骤:维护。需要持续监控数据质量和模型性能,以向目标用户提供稳定的服务。

项目评估

在最后阶段,项目评估阶段,团队应重新审视项目规划期间所做的讨论,评估项目是否成功进行。此外,需要记录项目的详细信息,并讨论潜在的改进措施,以便更有效地完成下一个项目。

需记住的事情

a. DL 项目内的阶段包括项目规划、构建 MVP、构建 FFP、部署和维护以及项目评估。

b. 在项目规划阶段,确定项目目标和评估指标,团队讨论个人责任、可用资源和项目的时间表。

c. 建立 MVP 的目的是了解项目的困难之处,并拒绝存在更大风险或提供较少前景的路径。

d. FFP 是一个生产就绪的可交付成果,是 MVP 的优化版本。数据准备管道和模型训练管道可以迁移到云端,利用各种网络服务以获得更高的吞吐量和质量。

e. 部署设置通常提供有限的计算资源。在这种情况下,系统需要进行调优,以向目标用户提供稳定的服务。

f. 项目完成后,团队需要重新审视时间表、分配的责任和业务需求,评估项目的成功与否。

在接下来的部分中,我们将详细介绍如何正确规划一个 DL 项目。

规划一个 DL 项目

每个项目都始于规划。在规划过程中,需要清晰定义项目的目的,并且关键成员应该对可以分配给项目的可用资源有深入了解。一旦确定了团队成员和利益相关者,下一步是讨论每个人的责任,并为项目制定时间表。

这个阶段应该产生一个详细记录的项目手册,精确定义业务目标以及项目评估方式。典型的手册包括主要交付内容概述、利益相关者列表、甘特图定义的步骤和瓶颈、责任定义、时间表和评估标准。在高度复杂的项目中,强烈建议遵循项目管理知识体系指南PMBOK®)(www.pmi.org/pmbok-guide-standards/foundational/pmbok),并考虑每个知识领域(例如,整合管理、项目范围管理和时间管理)。当然,还有其他的项目管理框架,如PRINCE2www.prince2.com/usa/what-is-prince2),它可以作为一个很好的起点。一旦手册编制完成,每个利益相关者都必须审查和修订,直到所有人都同意内容。

在现实生活中,很多人低估了规划的重要性。特别是在初创公司中,工程师们渴望投入到 MVP 开发中,并尽量少花时间在规划上。然而,在 DL 项目中这样做是非常危险的,因为训练过程可能会迅速消耗所有可用资源。

定义目标和评估指标

规划的第一步是理解项目的目的。目标可能是开发新产品、改善现有服务的性能或节省运营成本。项目的动机自然有助于定义评估指标。

在 DL 项目中,有两种评估指标:与业务相关的指标和基于模型的指标。一些业务相关的指标的例子如下:转化率点击率CTR),用户生命周期价值用户参与度度量运营成本节约投资回报率ROI)和收入。这些在广告、营销和产品推荐领域中广泛使用。

另一方面,基于模型的指标包括准确率精确率召回率F1 分数排名准确度指标平均绝对误差MAE),均方误差MSE),均方根误差RMSE)和标准化平均绝对误差NMAE)。通常,可以在这些指标之间进行权衡。例如,如果满足延迟要求对项目更为关键,那么稍微降低准确率可能是可以接受的。

除了因项目而异的目标评估指标外,大多数项目中还包括其他常见的指标,如截止日期和资源使用情况。必须利用现有资源,在特定日期达到目标状态。

目标及相应的评估指标需要公平。如果目标过于难以实现,项目成员可能会失去动力。如果评估指标不正确,理解项目的影响也变得困难。因此,建议将选择的评估指标与他人分享,并确保公平对待每个人。

图 1.4 – 一个填写了项目描述部分的示例 playbook

图 1.4 – 一个填写了项目描述部分的示例 playbook

图 1.4所示,playbook 的第一部分从一般描述开始,估计技术方面的复杂性,并列出所需的工具和框架。接下来,它清晰地描述了项目的目标及其评估方式。

利益相关者识别

就像“利益相关者”这个术语在业务中使用一样,项目的利益相关者指的是可以影响或受到项目影响的人或群体。利益相关者可以分为两类,即内部和外部。内部利益相关者直接参与项目执行,而外部利益相关者可能在外围支持项目的执行。

每个利益相关者在项目中扮演不同的角色。首先,我们来看内部利益相关者。内部利益相关者是项目的主要推动者。因此,他们密切合作处理和分析数据,开发模型并建立可交付成果。表 1.1列出了在 DL 项目中常见的内部利益相关者:

表 1.1 – DL 项目的常见内部利益相关者

表 1.1 – DL 项目的常见内部利益相关者

另一方面,外部利益相关者通常扮演支持角色,例如收集项目所需的数据或对可交付成果提供反馈。在表 1.2中,我们描述了一些 DL 项目的常见外部利益相关者:

表 1.2 – DL 项目的常见外部利益相关者

表 1.2 – DL 项目的常见外部利益相关者

Playbook 的第二部分描述了利益相关者。如图 1.4所示,playbook 必须列出项目中的利益相关者及其责任。

任务组织

里程碑指的是项目中发生重大事件的时间点。因此,有一系列要求导致里程碑。一旦满足了这些要求,就可以宣称已经达到了里程碑。项目规划中最重要的步骤之一是定义里程碑及其相关任务。通向目标的任务排序称为关键路径。值得一提的是,任务并不总是需要按顺序处理。理解关键路径很重要,因为它允许团队适当地优先考虑任务,以确保项目的成功。

在这一步骤中,还需要识别工作量评估LOE)活动和支持活动,这些活动对项目的执行至关重要。在软件开发项目中,工作量评估活动包括辅助任务,如设置 Git 仓库或审查他人的合并请求。下图(图 1.5)描述了深度学习项目的典型关键路径。如果底层项目包含不同的任务、需求、技术和所需的详细级别,则其结构将有所不同:

图 1.5 – 深度学习项目的典型关键路径

图 1.5 – 深度学习项目的典型关键路径

资源分配

对于深度学习项目,需要明确分配两大主要资源:人力资源和计算资源。人力资源指的是将积极参与个人任务的员工。通常,他们担任数据工程、数据科学、DevOps 或软件工程等职位。在谈论人力资源时,人们往往只考虑人数。然而,个人所掌握的知识和技能也是其他重要因素。人力资源与项目能够快速进行的相关性密切。

计算资源指的是分配给项目的硬件和软件资源。与典型的软件工程项目(如移动应用程序开发或网页开发)不同,深度学习项目需要大量的计算和大量的数据。加速开发过程的常见技术包括并行处理或使用计算能力更强的机器。在某些情况下,需要在它们之间进行权衡,因为一台高计算能力的单机可能比多台低计算能力的机器成本更高。

总体上说,新颖的深度学习流水线利用灵活和无状态的资源,例如具有容错代码的 AWS Spot 实例。除了硬件资源外,还有可能提供必要功能的框架和服务。如果所需服务需要付费,了解它如何改变项目执行以及如果团队决定自行处理它时对人力资源的需求是非常重要的。

在这一步中,需要将可用资源分配给每个任务。图 1.6列出了前一节中描述的任务,并描述了分配的资源,以及操作成本的估计。每个任务都有自己的风险水平指示器。它适用于一个由三人组成的小团队,在几个 AWS 弹性计算云EC2)实例上进行简单的 DL 项目工作,预计为 4 至 6 个月。请注意,人力资源的成本估算未包含在示例中,因为它根据地理位置和团队资历有很大差异:

图 1.6 – 深度学习项目示例资源分配部分

图 1.6 – 深度学习项目示例资源分配部分

在我们继续下一步之前,我们想提到,将一部分资源作为备份非常重要,以防里程碑所需资源超过分配的资源。

定义时间表

现在我们知道了可用资源,我们应该能够为项目制定时间表。在这一步中,团队需要讨论每个步骤需要多长时间完成。值得一提的是,事情并不总是按计划进行。在项目过程中会遇到许多困难,这些困难可能会延迟最终产品的交付。

因此,在时间表中包含缓冲区是许多组织的常见做法。每位利益相关者都同意时间表是非常重要的。如果有人认为这不合理,需要立即进行调整。图 1.7是根据图 1.6所示信息的最有可能估计时间表的样本甘特图:

图 1.7 – 描述时间表的样本甘特图

图 1.7 – 描述时间表的样本甘特图

值得一提的是,该图表还可用于监视每个任务及整体项目的进度。在这种情况下,可以附加每个指示条的附加注释或可视化,总结进展情况。

管理项目

在项目规划阶段讨论的深度学习项目的另一个重要方面是团队将如何更新其他团队成员并确保项目按时交付的流程。在各种项目管理方法中,敏捷方法尤为适用,因为它有助于将工作分解为较小的部分,并快速迭代开发周期,直至 FFP 出现。由于深度学习项目通常被认为是高度复杂的,因此在研究、开发和优化阶段内工作在短周期内更为便利。在每个周期结束时,利益相关者审查结果并调整其长期目标。敏捷方法论特别适合经验丰富的小团队。在典型情况下,发现 2 周冲刺最为有效,特别是在清晰定义短期目标时。

在冲刺会议期间,团队回顾了上个冲刺的目标,并为即将到来的冲刺定义了目标。还建议每天进行短暂的日常会议,回顾前一天的工作并计划下一天的工作,因为这个过程有助于团队快速识别瓶颈并根据需要调整优先级。用于此过程的常用工具包括JiraAsanaQuickbase。前述工具中大多数还支持预算管理、时间线审查、想法管理和资源跟踪。

需要记住的事情

a. 项目规划应该产生一个清晰描述项目用途及团队如何共同前进达到特定目标状态的操作手册。

b. 项目规划的第一步是定义目标及其相应的评估指标。在深度学习项目的情况下,有两种类型的评估指标:与业务相关的指标和基于模型的指标。

c. 利益相关者是指可以影响或被项目影响的个人或群体。利益相关者可以分为两类:内部和外部。

d. 项目规划的下一个阶段是任务组织。团队需要确定里程碑,识别任务以及 LOE 活动,并理解关键路径。

e. 对于深度学习项目,有两个主要资源需要明确分配:人力资源和计算资源。在资源分配期间,将资源的一部分作为备用非常重要。

f. 在估计项目时间表时,必须与团队共享,并且每个利益相关者必须同意时间表。

g. 敏捷方法论非常适合管理深度学习项目,因为它有助于将工作分解为较小的部分,并快速迭代开发周期。

总结

这一章是我们旅程的介绍。在前两节中,我们描述了深度学习在人工智能大局中的定位,以及它如何不断地塑造我们的日常生活。关键要点是深度学习由于其独特的模型架构而具有高度的灵活性,并且已经积极应用于传统机器学习技术未能显示显著成就的领域。

接下来,我们提供了深度学习项目的高层视角。一般来说,深度学习项目可以分为以下阶段:项目规划、建立最小可行产品(MVP)、建立最终可行产品(FFP)、开发与维护以及项目评估。

本章的主要内容涵盖了深度学习项目中最重要的步骤:项目规划。在这个阶段,项目的目的需要明确定义,评估指标需要明确,每个人必须对利益相关者及其各自的角色有深入了解,最后,参与者需要就任务、里程碑和时间表达成共识。这个阶段的结果将会是一个名为“playbook”的格式良好的文档。在下一章中,我们将学习如何为深度学习项目准备数据。

进一步阅读

这里是一些参考资料,可以帮助您更深入地了解与本章相关的主题。以下研究论文总结了深度学习的流行用例:

  • 计算机视觉

    • 基于梯度的学习在文档识别中的应用 by LeCun et al.

    • ImageNet:大规模分层图像数据库 by Deng et al.

  • 自然语言处理

    • 神经概率语言模型 by Bengio et al.

    • 深度递归神经网络语音识别 by Grave et al.

  • 强化学习

    • 深度强化学习简介 by François-Lavet et al.
  • 生成建模

    • 生成对抗网络 by Goodfellow et al.

第二章:准备深度学习项目的数据

机器学习ML)项目的第一步是数据收集和数据准备。作为机器学习的一个子集,深度学习DL)涉及相同的数据处理过程。我们将从使用 Anaconda 设置标准的 DL Python 笔记本环境开始这一章节。然后,我们将提供在各种格式(JSON、CSV、HTML 和 XML)中收集数据的具体示例。在许多情况下,收集到的数据会进行清理和预处理,因为它包含不必要的信息或无效的格式。

本章将介绍该领域中的流行技术:填补缺失值、删除不必要的条目和归一化值。接下来,您将学习常见的特征提取技术:词袋模型、词频-逆文档频率、独热编码和降维。此外,我们将介绍最流行的数据可视化库 matplotlibseaborn。最后,我们将介绍 Docker 镜像,这些镜像是工作环境的快照,通过将应用程序及其依赖项捆绑在一起,最大限度地减少潜在的兼容性问题。

在本章中,我们将涵盖以下主要主题:

  • 设置笔记本环境

  • 数据收集、数据清洗和数据预处理

  • 从数据中提取特征

  • 执行数据可视化

  • Docker 简介

技术要求

本章的补充材料可以从 GitHub 上下载,网址为 github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_2

设置笔记本环境

Python 是一种最流行的编程语言之一,广泛用于数据分析。其优势在于动态类型和无需编译。凭借其灵活性,它已成为数据科学家最常用的语言。在本节中,我们将介绍如何使用 AnacondaPreferred Installer ProgramPIP)为 DL 项目设置 Python 环境。这些工具允许您为每个项目创建独立的环境,同时简化包管理。Anaconda 提供了一个名为 Anaconda Navigator 的带有 GUI 的桌面应用程序。我们将指导您如何设置 Python 环境,并安装用于 DL 项目的流行 Python 库,如 TensorFlowPyTorchNumPypandasscikit-learnMatplotlibSeaborn

设置 Python 环境

Python 可以从 www.python.org/downloads 安装。但是,Python 版本通常可以通过操作系统提供的包管理器(例如 Linux 上的 高级包管理工具 (APT) 和 macOS 上的 Homebrew)获取。建立 Python 环境的第一步是使用 PIP 安装必要的软件包,PIP 是一个允许您安装和管理各种 Python 软件包的软件包管理系统。

安装 Anaconda

当在计算机上设置了多个 Python 项目时,将环境分开可能是理想的,因为每个项目可能依赖于这些软件包的不同版本。Anaconda 可以帮助您管理环境,因为它既设计用于 Python 软件包管理,又设计用于环境管理。它允许您创建虚拟环境,其中安装的软件包绑定到当前活动的每个环境。此外,Anaconda 超越了 Python 的界限,允许用户安装非 Python 库依赖项。

首先,可以从其官方网站 www.anaconda.com 安装 Anaconda。为了完整起见,我们已经在 github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_2/anaconda/anaconda_graphical_installer.md 上用图片描述了安装过程。

也可以直接从终端安装。Anaconda 为每个操作系统提供安装脚本(repo.anaconda.com/archive)。您可以简单地下载适合您系统的脚本的正确版本,并运行它来在您的计算机上安装 Anaconda。例如,我们将描述如何从这些脚本之一为 macOS 安装 Anaconda:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_2/anaconda/anaconda_zsh.md

使用 Anaconda 设置 DL 项目

在此时,您应该已经准备好使用 Anaconda 环境。现在,我们将创建我们的第一个虚拟环境,并安装 DL 项目所需的必要软件包:

conda create --name bookenv python=3.8

您可以使用以下命令列出可用的 conda 环境:

conda info --envs

您应该看到我们之前创建的 bookenv 环境。要激活此环境,您可以使用以下命令:

conda activate bookenv

同样地,通过以下命令可以实现去激活:

conda deactivate

通过 pip 命令可以安装 Python 包。可以通过 conda install <package name>pip install <package name> 安装 Python 包。在下面的代码片段中,首先我们通过 pip 命令下载了 NumPy,这是科学计算的基础包。然后,我们将通过 conda 命令安装 PyTorch。在安装 PyTorch 时,你必须为 CUDA 提供版本,CUDA 是一种用于通用计算的并行计算平台和编程模型。CUDA 可以通过允许 GPU 并行处理来加速 DL 模型训练:

pip install numpy
conda install pytorch torchvision torchaudio \
cudatoolkit=<cuda version> -c pytorch -c nvidia

TensorFlow 是另一个用于深度学习项目的流行包。与 PyTorch 类似,TensorFlow 为每个 CUDA 版本提供不同的包。完整列表可以在这里找到:www.tensorflow.org/install/source#gpu。为了使所有与深度学习相关的库能够无缝工作,必须保证 Python 版本、TensorFlow 版本、GCC 编译器版本、CUDA 版本和 Bazel 构建工具版本之间的兼容性,如下图所示:

图 2.1 – TensorFlow、Python、GCC、Bazel、cuDNN 和 CUDA 版本的兼容矩阵

图 2.1 – TensorFlow、Python、GCC、Bazel、cuDNN 和 CUDA 版本的兼容矩阵

回到 pip 命令,你可以通过生成一个文本文件来安装所有必需的包,并在单个命令中安装它们,而不是重复输入 install 命令。为了实现这一点,可以将 --requirement (-r) 选项与 pip install 命令一起使用,如下所示:

pip install -r requirements.txt

CPU-only 环境中列出的常见所需包在示例 requirements.txt 文件中列出:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_2/anaconda/requirements.txt。列表中的主要包括 TensorFlow 和 PyTorch。

现在,让我们看一些有用的 Anaconda 命令。就像 pip install 可以与 requirements.txt 文件一起使用一样,你也可以使用 YAML 文件创建一个包含一组包的环境。在以下示例中,我们使用了一个 env.yml 文件来保存现有环境中的库列表。稍后,可以使用 env.yml 创建一个具有相同包的新环境,如下代码片段所示:

conda create -n env_1
conda activate env_1
# save environment to a file
conda env export > env.yml
# clone existing environment 
conda create -n env_2 --clone env_1
# delete existing environment (env_1)
conda remove -n env_1 --all
# create environment (env_1) from the yaml file
conda env create -f env.yml
# using conda to install the libraries from requirements.txt
conda install --force-reinstall -y -q --name py37 -c conda-forge --file requirements.txt

下面的代码片段描述了从 conda env export 生成的样本 YAML 文件:

# env.yml
name: env_1
channels:
  - defaults
dependencies:
  - appnope=0.1.2=py39hecd8cb5_1001
  - ipykernel=6.4.1=py39hecd8cb5_1
  - ipython=7.29.0=py39h01d92e1_0
prefix: /Users/userA/opt/anaconda3/envs/new_env

此 YAML 文件的主要组件是环境的名称 (name)、库的来源 (channels) 和库的列表 (dependencies)。

需要记住的事情

a. Python 是由于其简单的语法而成为数据分析的标准语言

b. Python 不需要显式编译

c. PIP 用于安装 Python 包

d. Anaconda 可同时处理 Python 包管理和环境管理

在接下来的部分中,我们将解释如何从各种来源收集数据。然后,我们将对收集到的数据进行清理和预处理,以供后续处理使用。

数据收集、数据清理和数据预处理

在本节中,我们将向您介绍数据收集过程中涉及的各种任务。我们将描述如何从多个来源收集数据,并将其转换为数据科学家可以使用的通用形式,而不论底层任务是什么。这个过程可以分解为几个部分:数据收集、数据清理和数据预处理。值得一提的是,任务特定的转换被认为是特征提取,这将在下一节中讨论。

收集数据

首先,我们将介绍用于构建初始数据集的不同数据收集方法。根据原始数据的格式,需要使用不同的技术。大多数数据集要么作为 HFML 文件在线可用,要么作为 JSON 对象存储。一些数据以 逗号分隔值CSV)格式存储,可以通过流行的数据分析和操作工具 pandas 轻松加载。因此,我们将主要专注于在本节中收集 HTML 和 JSON 数据,并将其保存为 CSV 格式。此外,我们还将介绍一些流行的数据集库。

爬取网页

作为 Web 的基本组成部分,超文本标记语言HTML)数据易于访问,包含多样化的信息。因此,通过爬取网页可以帮助您收集大量有趣的数据。在本节中,我们将使用 BeautifulSoup,这是一个基于 Python 的网络爬虫库(www.crummy.com/software/BeautifulSoup/)。作为示例,我们将演示如何爬取 Google 学术页面,并将爬取的数据保存为 CSV 文件。

在这个示例中,将使用 BeautifulSoup 的多个函数来提取作者的名字、姓氏、电子邮件、研究兴趣、引用次数、h 指数(高指数)、共同作者和论文标题。下表显示了我们希望在此示例中收集的数据:

表 2.1 – 可从 Google 学术页面收集的数据

爬取网页是一个两步过程:

  1. 利用请求库获取 HTML 数据,并存储在 response 对象中。

  2. 构建一个 BeautifulSoup 对象,用于解析 response 对象中的 HTML 标签。

这两个步骤可以用以下代码片段总结:

# url points to the target google scholar page 
response = requests.get(url) 
html_soup = BeautifulSoup(response.text, 'html.parser')

下一步是从 BeautifulSoup 对象获取感兴趣的内容。 表 2.2 总结了常见的 BeautifulSoup 函数,允许您从解析的 HTML 数据中提取感兴趣的内容。 由于我们在这个例子中的目标是将收集的数据存储为 CSV 文件,我们将简单地生成页面的逗号分隔字符串表示,并将其写入文件。 完整的实现可以在 github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_2/google_scholar/google_scholar.py 找到。

以下表格提供了从 Google Scholar 页面处理原始数据所需的方法列表:

表 2.2 – 可能的特征提取技术

接下来,我们将学习另一种流行的原始数据格式 JSON。

收集 JSON 数据

JSON 是一种与语言无关的格式,将数据存储为键-值和/或键-数组字段。 由于大多数编程语言支持键-值数据结构(例如 Python 中的 Dictionary 或 Java 中的 HashMap),因此 JSON 被认为是可互换的(独立于程序)。 以下代码片段显示了一些示例 JSON 数据:

{
    "first_name": "Ryan",
    "last_name": "Smith",
    "phone": [{"type": "home",
               "number": "111 222-3456"}],
    "pets": ["ceasor", "rocky"],
    "job_location": null
}

查看 Awesome JSON Datasets GitHub 仓库 (github.com/jdorfman/awesome-json-datasets),其中包含一些有用的 JSON 数据源的列表。 此外,Public API’s GitHub 仓库 (github.com/public-apis/public-apis) 包含一些可以检索各种 JSON 数据的 Web 服务器端点的列表。 此外,我们提供一个脚本,从端点收集 JSON 数据,并将必要的字段存储为 CSV 文件: github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_2/rest/get_rest_api_data.py。 此示例使用位于 www.reddit.com/r/all.json 的 Reddit 数据集。

接下来,我们将介绍数据科学领域中流行的公共数据集。

流行的数据集存储库

除了网页和 JSON 数据之外,许多公共数据集可以用于各种目的。例如,您可以从流行的数据集中心如Kaggle (www.kaggle.com/datasets)或MIT Data Hub (datahub.csail.mit.edu/browse/public)获取数据集。这些公共数据集经常被许多研究机构和企业用于广泛的活动。从各种领域如医疗保健、政府、生物学和计算机科学的数据在研究过程中收集,并捐赠给仓库以造福更多人。就像这些组织如何管理和提供多样化的数据集一样,社区也在努力管理各种公共数据集:github.com/awesomedata/awesome-public-datasets

另一个常见的数据集来源是数据分析库,如sklearnKerasTensorFlow。每个库提供的数据集列表可以在以下链接找到:scikit-learn.org/stable/datasetskeras.io/api/datasets/www.tensorflow.org/datasets

最后,政府机构也向公众提供许多数据集。例如,您可以在由 AWS 托管的数据湖中找到与 COVID 相关的有趣且经过精心策划的数据集:dj2taa9i652rf.cloudfront.net。从这些数据集列表中,您可以轻松下载关于 Moderna 疫苗在不同州分布的 CSV 格式数据,方法是导航至cdc-moderna-vaccine-distribution页面。

现在您已经收集了初始数据集,接下来的步骤是清理它。

清理数据

数据清理是将原始数据精磨以保持条目一致的过程。常见的操作包括使用默认值填充空字段,移除非字母数字字符如?!,去除停用词,以及删除 HTML 标记如<p></p>。数据清理还侧重于保留从收集的数据中提取的相关信息。例如,用户个人资料页面可能包含广泛的信息,如传记、名字、电子邮件和隶属关系。在数据收集过程中,目标信息被按原样提取,以便保留在原始 HTML 或 JSON 标记中。换句话说,已收集的传记信息可能仍然带有换行的 HTML 标签(<br>)或加粗(<b></b>),这些对后续分析任务并不增加多少价值。在整个数据清理过程中,这些不必要的组件应该被丢弃。

在讨论单个数据清理操作之前,了解一下 DataFrames 将会很有帮助,DataFrames 是由 pandas 库提供的类似表格的数据结构(pandas.pydata.org/)。它们拥有行和列,就像 SQL 表或 Excel 表格一样。它们的一个基本功能是pandas.read_csv,它允许你将 CSV 文件加载到一个 DataFrame 中,就像下面的代码片段所演示的那样。在终端上显示内容时,tabulate库是一个不错的选择,因为 DataFrame 可以将数据结构化成表格格式。

下面的代码片段显示了如何读取 CSV 文件并使用tabulate库打印数据(在上面的例子中,tabulate将模仿 Postgres psql CLI 的格式,因为我们使用了tablefmt="psql"选项):

import pandas as pd
from tabulate import tabulate
in_file = "../csv_data/data/cdc-moderna-covid-19-vaccine-distribution-by-state.csv"
# read the CSV file and store the returned dataframe to a variable "df_vacc"
df_vacc = pd.read_csv(in_file)
print(tabulate(df_vacc.head(5), headers="keys", tablefmt="psql"))

下面的截图显示了前述代码片段中 DataFrame 在终端上使用tabulate库显示的内容(你可以通过使用df_vacc.head(5)而不使用tabulate库来查看类似的输出)。以下截图显示了每个司法管辖区疫苗剂量的分配情况:

图 2.2 – 使用 pandas 加载 CSV 文件并使用 tabulate 显示内容

图 2.2 – 使用 pandas 加载 CSV 文件并使用 tabulate 显示内容

我们将讨论的第一个数据清理操作是使用默认值填充缺失字段。

使用默认值填充空字段

我们将使用本章早些时候抓取的 Google Scholar 数据来演示如何使用默认值填充空字段。在数据检查后,您会发现一些作者没有填写他们的机构,因为这些信息没有明确指定:

图 2.3 – 机构列包含缺失值(nan)

图 2.3 – 机构列包含缺失值(nan)

每个字段的默认值根据上下文和数据类型而异。例如,将 9 到 6 作为操作时间的典型默认值,将空字符串作为缺少中间名的良好选择。短语“不适用(N/A)”经常用于明确指出字段为空。在我们的示例中,我们将填写包含na的空字段,以指示这些值在原始网页中缺失,而不是在整个收集过程中由于错误而丢失。我们将在这个例子中演示的技术涉及pandas库;DataFrame 有一个fillna方法,它填充指定值中的空值。fillna方法接受一个参数True,用于在原地更新对象而不创建副本。

下面的代码片段解释了如何使用fillna方法填充 DataFrame 中的缺失值:

df = pd.read_csv(in_file)
# Fill out the empty "affiliation" with "na"
df.affiliation.fillna(value="na", inplace=True)

在上述代码片段中,我们将 CSV 文件加载到 DataFrame 中,并将缺失的关联条目设置为na。此操作将在原地执行,而不会创建额外的副本。

在下一节中,我们将描述如何移除停用词。

移除停用词

停用词是从信息检索角度来看没有多大价值的词语。常见的英文停用词包括itsandtheforthat。例如,在 Google Scholar 数据的研究兴趣字段中,我们看到security and privacy preservation for wireless networks。像andfor这样的词在解释这段文本的含义时并不有用。因此,在自然语言处理NLP)任务中建议移除这些词。自然语言工具包NLTK)提供了一个流行的停用词移除功能,它是一套用于符号和统计 NLP 的库和程序。以下是 NLTK 库认为是停用词标记的一些单词:

['doesn', "doesn't", 'hadn', "hadn't", 'hasn', "hasn't", 'haven', "haven't", 'isn', "isn't", 'ma', …]

单词标记化是将句子分解为单词标记(单词向量)的过程。通常在停用词移除之前应用。下面的代码片段演示了如何标记化 Google Scholar 数据的research_interest字段并移除停用词:

import pandas as pd
import nltk
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
import traceback
from nltk.corpus import stopwords
# download nltk corpuses
nltk.download('punkt')
nltk.download('stopwords')
# create a set of stop words
stop_words = set(stopwords.words('english'))
# read each line in dataframe (i.e., each line of input file)
for index, row in df.iterrows():
   curr_research_interest = str(row['research_interest'])\
       .replace("##", " ")\
       .replace("_", " ")
   # tokenize text data.
   curr_res_int_tok = tokenize(curr_research_interest)
   # remove stop words from the word tokens
   curr_filtered_research = [w for w in curr_res_int_tok\
                             if not w.lower() in stop_words]

正如您所见,我们首先使用stopwords.words('english')下载 NLTK 的停用词语料库,并移除不在语料库中的单词标记。完整版本位于github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_2/data_preproessing/bag_of_words_tf_idf.py

与停用词类似,非字母数字文本在信息检索角度上也没有多大价值。因此,我们将在下一节中说明如何移除它们。

移除非字母数字文本

字母数字字符是既不是英文字母字符也不是数字的字符。例如,在文本“Hi, How are you?”中,有两个非字母数字字符:,?。与停用词类似,它们可以被去除,因为它们对上下文的信息贡献不大。一旦这些字符被移除,文本将变成Hi How are you

要删除一组特定字符,我们可以使用正则表达式regex)。正则表达式是一系列字符,表示搜索模式。下面的表 2.3显示了一些重要的正则表达式搜索模式,并解释了每个模式的含义:

表 2.3 – 关键正则表达式搜索模式

您可以在docs.python.org/3/library/re.html找到其他有用的模式。

Python 提供了一个内置的regex库,支持查找和删除匹配给定正则表达式的一组文本。以下代码片段展示了如何删除非字母数字字符。\W模式匹配任何非单词字符。模式后面的+表示我们想保留前面的元素一次或多次。将它们组合在一起,我们将在以下代码片段中找到一个或多个字母数字字符:

def clean_text(in_str):
   clean_txt = re.sub(r'\W+', ' ', in_str)
   return clean_txt
# remove non alpha-numeric characters for feature "text"
text = clean_text(text)

作为最后的数据清理操作,我们将介绍如何高效地删除换行符。

移除换行符

最后,收集的文本数据可能包含不必要的换行符。在许多情况下,即使是结尾的换行符也可以毫无损害地删除,而不管接下来的任务是什么。可以使用 Python 的内置replace功能将这些字符轻松地替换为空字符串。

以下代码片段展示了如何在文本中删除换行符:

# replace the new line in the given text with empty string. 
text = input_text.replace("\n", "")

在上述代码片段中,"abc\n"将会变成"abc"

清理后的数据通常会进一步处理,以更好地表示底层数据。这个过程称为数据预处理。我们将在下一节深入研究这个过程。

数据预处理

数据预处理的目标是将清洗后的数据转换为适合各种数据分析任务的通用形式。数据清理和数据预处理之间没有明确的区别。因此,像替换一组文本或填充缺失值这样的任务既可以归类为数据清理,也可以归类为数据预处理。在本节中,我们将重点介绍前一节未涵盖的技术:标准化、将文本转换为小写、将文本转换为词袋模型,并对单词应用词干提取。

完整的示例实现可以在github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_2/data_preproessing找到。

标准化

有时候,字段的值可能以不同的方式表示,尽管它们表示相同的含义。在谷歌学术数据的情况下,研究兴趣的条目可能用不同的词汇,尽管它们指的是类似的领域。例如,数据科学,ML 和人工智能AI)在更大的上下文中指的是 AI 的相同领域。在数据预处理阶段,我们通常通过将 ML 和数据科学转换为 AI 来标准化它们,这样可以更好地表示底层信息。这有助于数据科学算法利用特征来完成目标任务。

如在示例存储库中的normalize.py脚本中所示,可以通过保持一个映射预期值到标准化值的字典来轻松实现前述案例的标准化。在以下代码片段中,artificial_intelligence将是research_interestsdata_sciencemachine_learning特征的标准化值:

# dictionary mapping the values are commonly used for normalization
dict_norm = {"data_science": "artificial_intelligence",
    "machine_learning": "artificial_intelligence"}
# normalize.py
if curr in dict_norm:
   return dict_norm[curr]
else:
   return curr

字段的数值也需要标准化。对于数值,标准化将是将每个值重新缩放到特定范围内的过程。在以下示例中,我们将每个州的每周疫苗分配的平均计数缩放到 0 和 1 之间。首先,我们计算每个州的平均计数。然后,通过将平均计数除以最大平均计数来计算标准化的平均计数:

# Step 1: calculate state-wise mean number for weekly corora vaccine distribution
df = df_in.groupby("jurisdiction")["_1st_dose_allocations"]\
   .mean().to_frame("mean_vaccine_count").reset_index()
# Step 2: calculate normalized mean vaccine count
df["norm_vaccine_count"] = df["mean_vaccine_count"] / df["mean_vaccine_count"].max()

标准化的结果可以在以下屏幕截图中看到。此屏幕截图的表格包含两列 - 标准化之前和之后的平均疫苗计数:

图 2.4 - 各州标准化平均疫苗分配量

图 2.4 - 各州标准化平均疫苗分配量

接下来介绍的下一个数据预处理是文本数据的大小写转换。

大小写转换

在许多情况下,文本数据会被转换为小写或大写作为标准化的一种方式。这在涉及比较的任务时带来了一定的一致性。在停用词移除示例中,curr_res_int_tok变量中的单词标记将在 NLTK 库的标准英文停用词中搜索。为了成功比较,大小写应保持一致。在以下代码片段中,在停用词搜索之前,标记会转换为小写:

# word tokenize
curr_resh_int_tok = word_tokenize(curr_research_interest)
# remove stop words from the word tokens
curr_filtered_research = [w for w in curr_res_int_tok\
                         if not w.lower() in stop_words]

另一个示例可以在get_rest_api_data.py中找到,我们从 Reddit 收集和处理数据。在从脚本中提取的以下代码片段中,每个文本字段在收集时都转换为小写:

def convert_lowercase(in_str):
   return str(in_str).lower()
# convert string to lowercase
text = convert_lowercase(text)

接下来,您将了解如何通过词干化来改善数据质量。

词干化

词干化是将一个词转换为其根词的过程。词干化的好处在于,如果它们的基本含义相同,可以保持单词的一致性。例如,“信息”,“通知”和“通知”有相同的词根 - “通知”。以下示例展示了如何利用 NLTK 库进行词干化。NLTK 库提供了基于波特词干算法(Porter, Martin F. “An algorithm for suffix stripping.” Program (1980))的词干化功能:

from nltk.stem import PorterStemmer
# porter stemmer for stemming word tokens
ps = PorterStemmer()
word = "information"
stemmed_word = ps.stem(word) // "inform"

在上述代码片段中,我们从nltk.stem库实例化了PosterStemmer,并将文本传递给stem函数。

需要记住的事项

a. 数据以不同格式出现,如 JSON,CSV,HTML 和 XML。每种类型的数据都有许多数据收集工具可用。

b. 数据清洗是将原始数据整理成一致性条目的过程。常见操作包括用默认值填充空特征、删除非字母数字字符、删除停用词和不必要的标记。

c. 数据预处理的目标是应用通用数据增强技术,将清洗后的数据转换为适用于任何数据分析任务的形式。

d. 数据清洗和数据预处理的领域有重叠,这意味着某些操作可用于两个过程之一。

到目前为止,我们已讨论了数据准备的通用过程。接下来,我们将讨论最后一个过程:特征提取。与我们讨论过的其他过程不同,特征提取涉及特定于任务的操作。让我们更详细地看一下。

从数据中提取特征

特征提取特征工程)是将数据转换为表达特定任务底层信息的特征的过程。数据预处理应用通用技术,通常对大多数数据分析任务都是必需的。但是,特征提取要求利用领域知识,因为它是特定于任务的。在本节中,我们将介绍流行的特征提取技术,包括文本数据的词袋模型、词频-逆文档频率、将彩色图像转换为灰度图像、序数编码、独热编码、降维和用于比较两个字符串的模糊匹配。

这些示例的完整实现可以在github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_2/data_preproessing在线找到。

首先,我们将从词袋模型技术开始。

使用词袋模型转换文本

Sklearn 的 CountVectorizer 类可帮助从文本创建词袋模型。以下代码演示了如何使用 Sklearn 的功能进行词袋模型:

import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
document_1 = "This is a great place to do holiday shopping"
document_2 = "This is a good place to eat food"
document_3 = "One of the best place to relax is home"
# 1-gram (i.e., single word token used for BoW creation)
count_vector = CountVectorizer(ngram_range=(1, 1), stop_words='english')
# transform the sentences
count_fit = count_vector.fit_transform([document_1, document_2, document_3])
# create dataframe
df = pd.DataFrame(count_fit.toarray(), columns=count_vector.get_feature_names_out())
print(tabulate(df, headers="keys", tablefmt="psql"))

下面的屏幕截图总结了词袋模型的输出,采用表格格式:

图 2.5 – 词袋模型在三个样本文档上的输出

图 2.5 – 词袋模型在三个样本文档上的输出

接下来,我们将介绍词频-逆文档频率TF-IDF)用于文本数据。

应用词频-逆文档频率(TF-IDF)转换

使用词频的问题在于具有更高频率的文档将主导模型或分析。因此,更好的方法是根据单词在所有文档中出现的频率重新调整频率。这种缩放有助于惩罚那些高频单词(如thehave),使文本的数值表示更好地表达上下文。

在介绍 TF-IDF 的公式之前,我们必须定义一些符号。让n表示文档的总数,t表示一个单词(术语)。df(t)指的是单词t的文档频率(包含单词t的文档数),而tf(t, d)指的是单词t在文档d中的频率(单词t在文档d中出现的次数)。有了这些定义,我们可以定义idf(t),即逆文档频率,为log [ n / df(t) ] + 1

总体而言,tf-idf(t, d),即单词t在文档d中的 tf-idf 值,可以表示为tf(t, d) * idf(t)

在样本代码脚本bag_of_words_tf_idf.py中,我们使用 Google Scholar 数据的研究兴趣领域来计算 TF-IDF。在这里,我们利用 Sklearn 的TfidfVectorizer函数。fit_transform函数接受一组文档并生成 TF-IDF 加权的文档-术语矩阵。从这个矩阵中,我们可以打印出前N个研究兴趣:

tfidf_vectorizer = TfidfVectorizer(use_idf=True)
# use the tf-idf instance to fit list of research_interest 
tfidf = tfidf_vectorizer.fit_transform(research_interest_list)
# tfidf[0].T.todense() provides the tf-idf dense vector 
# calculated for the research_interest
df = pd.DataFrame(tfidf[0].T.todense(), index=tfidf_vectorizer.get_feature_names_out(), columns=["tf-idf"])
# sort the tf-idf calculated using 'sort_values' of dataframe.
df = df.sort_values('tf-idf', ascending=False)
# top 3 words with highest tf-idf
print(df.head(3))

在前面的示例中,我们创建了一个TfidfVectorizer实例,并使用研究兴趣文本列表(research_interest_list)触发了fit_transform函数。然后,我们在输出上调用todense方法,以获取结果矩阵的稠密表示。该矩阵被转换为 DataFrame 并排序以显示前几条条目。下面的屏幕截图显示了df.head(3)的输出 – 研究兴趣领域中具有最高 TF-IDF 值的三个单词:

图 2.6 – 研究兴趣领域中具有最高 TF-IDF 值的三个单词

图 2.6 – 研究兴趣领域中具有最高 TF-IDF 值的三个单词

接下来,您将学习如何使用独热编码处理分类数据。

创建独热编码(one-of-k)

独热编码(one-hot 编码)是将离散值转换为一系列二进制值的过程。让我们从一个简单的例子开始,其中一个字段可以具有"猫"或"狗"的分类值。独热编码将被表示为两位,其中一位指代"猫",另一位指代"狗"。编码中数值为 1 的位表示该字段具有相应的值。因此,1 0 表示猫,而 0 1 表示狗:

品种 宠物类型
猎犬 1 0
缅因库恩 0 1
德国牧羊犬 1 0

表 2.4 – 将宠物类型(pet_type)中的分类值转换为独热编码(狗和猫)

one_hot_encoding.py中展示了独热编码的演示。在以下代码片段中,我们专注于核心操作,涉及 Sklearn 中的OneHotEncoder

from sklearn.preprocessing import LabelEncoder
labelencoder = LabelEncoder()
encoded_data = labelencoder.fit_transform(df_research ['is_artifical_intelligent'])

在前述代码片段中使用的is_artificial_intelligence列包含两个不同的值:"yes"和"no"。以下屏幕截图总结了独热编码的结果:

图 2.7 – 字段的独热编码

图 2.7 – is_artificial_intelligence字段的独热编码

在接下来的部分中,我们将介绍另一种称为序数编码的编码类型。

创建序数编码

序数编码是将分类值转换为数值的过程。在表 2.5中,有两种宠物类型,狗和猫。狗分配值为 1,猫分配值为 2:

品种 宠物类型 序数编码
猎犬 1
缅因库恩猫 2
德国牧羊犬 1

表 2.5 – pet_type 字段中的分类值在 ordinal_encoding 中被编码为序数

在下面的代码片段中,我们使用 Sklearn 中的LabelEncoder类将研究兴趣领域转换为数值。有关序数编码的完整示例可以在ordinal_encoding.py中找到:

from sklearn.preprocessing import LabelEncoder
labelencoder = LabelEncoder()
encoded_data = labelencoder.fit_transform(df_research ['research_interest'])

前面的代码片段几乎是自解释的 - 我们只需构造一个LabelEncoder实例,并将目标列传递给fit_transform方法。下图显示了生成的 DataFrame 的前三行:

图 2.8 – 研究兴趣的序数编码结果

图 2.8 – 研究兴趣的序数编码结果

接下来,我们将解释一种图像技术:将彩色图像转换为灰度图像。

将彩色图像转换为灰度图像

在计算机视觉任务中,最常见的技术之一是将彩色图像转换为灰度图像。OpenCV是图像处理的标准库(opencv.org/)。在以下示例中,我们只是导入 OpenCV 库(import cv2)并使用cvtColor函数将加载的图像转换为灰度图:

image = cv2.imread('./images/tiger.jpg')
# filter to convert color tiger image to gray one
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# write the gray image to a file
cv2.imwrite('./images/tiger_gray.jpg', gray_image)

在处理包含多个字段的大量数据时,通常需要减少维度。在下一节中,我们将探讨此过程的可用选项。

执行降维

在许多情况下,任务需要的特征多于实际需要的特征;并非所有特征都包含有用信息。在这种情况下,可以使用降维技术,如主成分分析PCA)、奇异值分解SVD)、线性判别分析LDA)、t-SNEUMAPISOMAP等。另一种选择是使用 DL。您可以构建用于降维的自定义模型,或者使用预定义的网络结构,如AutoEncoder。在本节中,我们将详细描述 PCA,因为它是我们提到的技术中最流行的一种。

给定一组特征,PCA 识别特征之间的关系,并生成一组新的变量,以最有效的方式捕捉样本之间的差异。这些新变量称为主成分,并按重要性排名;在构建第一个主成分时,它压缩了不重要的变量,并将它们留给第二个主成分。因此,第一个主成分与其余变量不相关。这个过程重复进行,以构建后续顺序的主成分。

如果我们更正式地描述 PCA 过程,我们可以说该过程有两个步骤:

  1. 构建代表每对特征的相关性的协方差矩阵。

  2. 通过计算协方差矩阵的特征值,生成能够捕获不同信息量的新功能集。

新功能集是主要组件。通过按从最高到最低排序相应的特征值,您可以在顶部获得最有用的新功能。

为了理解细节,我们将查看 Iris 数据集(archive.ics.uci.edu/ml/datasets/iris)。该数据集包括三类鸢尾植物(山鸢尾、变色鸢尾和维吉尼亚鸢尾),以及四个特征(花萼宽度、花萼长度、花瓣宽度和花瓣长度)。在下图中,我们使用从 PCA 构建的两个新特征绘制每个条目。根据这个图表,我们可以轻松地得出结论,我们只需要前两个主成分来区分这三个类别:

图 2.9 - Iris 数据集上 PCA 的结果

图 2.9 - Iris 数据集上 PCA 的结果

在下面的示例中,我们将使用来自 Kaggle 的人力资源数据来演示 PCA。初始数据集包括多个字段,如工资、过去五年内是否晋升以及员工是否离开公司。一旦构建了主成分,就可以使用 matplotlib 绘制它们:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler, 
# read the HR data in csv format
df_features = pd.read_csv("./HR.csv")
# Step 1: Standardize features by removing the mean and scaling to unit variance
scaler = StandardScaler()
# train = scaler.fit(X)
X_std = scaler.fit_transform(X)
# Step 2: Instantiate PCA & choose minimum number of 
# components such that it covers 95% variance
pca = PCA(0.95).fit(X_std)

在前面的代码片段中,首先使用 pandas 库的read_csv函数加载数据,使用 Sklearn 的StandardScaler对条目进行标准化,并使用 Sklearn 应用 PCA。完整示例可以在pca.py中找到。

作为特征提取的最后一种技术,我们将解释如何有效地计算两个序列之间的距离度量。

应用模糊匹配来处理字符串之间的相似性

模糊匹配 (pypi.org/project/fuzzywuzzy/) 使用距离度量来衡量两个序列的差异,并且如果它们被认为是相似的,则对待它们相同。在这一部分中,我们将演示如何使用莱文斯坦距离(Levenshtein, Vladimir I. (February 1966). "Binary codes capable of correcting deletions, insertions, and reversals". Soviet Physics Doklady. 10 (8): 707–710. Bibcode: 1966SPhD...10..707L)来应用模糊匹配。

模糊字符串匹配最流行的库是 fuzzywuzzyratio 函数将提供莱文斯坦距离分数,我们可以用它来决定是否要在接下来的过程中将两个字符串视为相同。下面的代码片段描述了 ratio 函数的使用方式:

from fuzzywuzzy import fuzz
# compare strings using ratio method
fuzz.ratio("this is a test", "this is a test!") // 91
fuzz.ratio("this is a test!", "this is a test!") // 100

如前例所示,ratio 函数将在两个文本更相似时输出较高的分数。

记住的事情

a. 特征提取 是将数据转换为能更好地表达目标任务下潜在信息的特征的过程。

b. BoW 是基于单词出现情况的文档表示。TF-IDF 可以通过惩罚高频词语更好地表达文档的上下文。

c. 使用 OpenCV 库,可以将彩色图像更新为灰度图像。

d. 分类特征可以使用序数编码或独热编码进行数值表示。

e. 当数据集具有过多特征时,降维可以减少具有最多信息的特征数量。PCA 构建新特征同时保留大部分信息。

f. 在评估两个文本之间的相似性时,可以应用模糊匹配方法。

一旦数据被转换为合理的格式,通常需要可视化数据以理解其特性。在下一节中,我们将介绍用于数据可视化的流行库。

执行数据可视化

当应用机器学习技术分析数据集时,第一步必须是理解可用数据,因为每个算法的优势与底层数据密切相关。数据科学家需要理解的数据关键方面包括数据格式、分布和特征之间的关系。当数据量较小时,可以通过手动分析每个条目来收集必要信息。然而,随着数据量的增长,可视化在理解数据中扮演关键角色。

Python 中有许多用于数据可视化的工具。MatplotlibSeaborn 是最流行的用于统计数据可视化的库。我们将在本节逐一介绍这两个库。

使用 Matplotlib 执行基本可视化

在以下示例中,我们将演示如何使用 Matplotlib 生成条形图和饼图。我们使用的数据代表 COVID 疫苗的周分布。要使用 matplotlib 功能,必须首先导入该包 (import matplotlib.pyplot as plt)。plt.bar 函数接受前 10 个州名称的列表和其平均分布列表,以生成条形图。类似地,plt.pie 函数用于从字典生成饼图。plt.figure 函数调整绘图大小,并允许用户在同一画布上绘制多个图表。完整的实现可以在 visualize_matplotlib.py 中找到:

# PIE CHART PLOTTING
# colors for pie chart
colors = ['orange', 'green', 'cyan', 'skyblue', 'yellow', 'red', 'blue', 'white', 'black', 'pink']
# pie chart plot
plt.pie(list(dict_top10.values()), labels=dict_top10.keys(), colors=colors, autopct='%2.1f%%', shadow=True, startangle=90)
# show the actual plot
plt.show()
# BAR CHART PLOTTING
x_states = dict_top10.keys()
y_vaccine_dist_1 = dict_top10.values()
fig = plt.figure(figsize=(12, 6))  # figure chart with size
ax = fig.add_subplot(111)
# bar values filling with x-axis/y-axis values
ax.bar(np.arange(len(x_states)), y_vaccine_dist_1, log=1)
plt.show()

前述代码的结果如下:

图 2.10 – 使用 Matplotlib 生成的条形图和饼图

图 2.10 – 使用 Matplotlib 生成的条形图和饼图

在接下来的部分中,我们将介绍 Seaborn,另一个流行的数据可视化库。

使用 Seaborn 绘制统计图

Seaborn 是建立在 Matplotlib 之上的库,提供了绘制统计图形的高级接口,这是 Matplotlib 不支持的。在本节中,我们将学习如何使用 Seaborn 为相同的数据集生成折线图和直方图。首先,我们需要导入 Seaborn 库以及 Matplotlib (import seaborn as sns)。sns.line_plot 函数设计用于接受 DataFrame 和列名。因此,我们必须提供包含分布最高的前 10 个州的平均值的 df_mean_sorted_top10,以及两个列名 state_namescount_vaccine 作为 XY 轴。要绘制直方图,可以使用 sns.dist_plot 函数,该函数接受具有 Y 轴列值的 DataFrame。如果我们要使用相同的平均值,可以这样做 sns.displot(df_mean_sorted_top10['count_vaccine'], kde=False)

import seaborn as sns 
# top 10 stats by largest mean
df_mean_sorted_top10 = ... # top 10 stats by largest mean
# LINE CHART PLOT
sns.lineplot(data=df_mean_sorted_top10, x="state_names", y="count_vaccine")
# show the actual plot
plt.show()
# HISTOGRAM CHART PLOTTING
# plot histogram bars with top 10 states mean distribution count of vaccine
sns.displot(df_mean_sorted_top10['count_vaccine'], kde=False)
plt.show()

下图显示了生成的图形:

图 2.11 – 使用 Seaborn 生成的折线图和直方图

图 2.11 – 使用 Seaborn 生成的折线图和直方图

完整的实现可以在本书的 GitHub 仓库中找到 (visualize_seaborn.py)。

许多库可用于增强可视化:pyROOT,这是来自 CERN 的数据分析框架,通常用于研究项目 (root.cern/manual/python),Streamlit,用于轻松创建 Web 应用 (streamlit.io),Plotly,一个免费开源的绘图库 (plotly.com),和Bokeh,用于交互式 Web 可视化 (docs.bokeh.org/en/latest)。

记住的事情

a. 可视化数据有助于分析和理解对选择正确的机器学习算法至关重要的数据。

b. Matplotlib 和 Seaborn 是最流行的数据可视化工具。其他工具包括 pyRoot、Streamlit、Plotly 和 Bokeh。

本章的最后一节将描述 Docker,它允许你为项目实现操作系统OS)级别的虚拟化。

Docker 简介

在上一节 设置笔记本环境 中,你学习了如何使用condapip命令设置带有各种包的虚拟环境用于深度学习。此外,你还知道如何将环境保存到 YAML 文件并重新创建相同的环境。然而,基于虚拟环境的项目在需要在多台机器上复制环境时可能不够,因为可能会出现来自非明显的操作系统级别依赖项的问题。在这种情况下,Docker 将是一个很好的解决方案。使用 Docker,你可以创建一个包含操作系统版本的工作环境快照。总之,Docker 允许你将应用程序与基础设施分开,以便快速交付软件。安装 Docker 可以按照 www.docker.com/get-started 上的说明进行。在本书中,我们将使用版本 3.5.2。

在本节中,我们将介绍 Docker 镜像,即 Docker 上下文中虚拟环境的表示,并解释如何为目标 Docker 镜像创建 Dockerfile。

Dockerfile 简介

Docker 镜像由所谓的 Dockerfile 创建。每个 Docker 镜像都有一个基础(或父)镜像。对于深度学习环境,作为基础镜像的一个很好选择是为 Linux Ubuntu 操作系统开发的镜像之一:ubuntu:18.04releases.ubuntu.com/18.04)或 ubuntu:20.04releases.ubuntu.com/20.04)。除了基础操作系统的镜像,还有安装了特定包的镜像。例如,建立基于 TensorFlow 的环境的最简单方法是下载已安装 TensorFlow 的镜像。TensorFlow 开发人员已经创建了一个基础镜像,并可以通过使用 docker pull tensorflow/serving 命令轻松下载(hub.docker.com/r/tensorflow/serving)。还提供了一个带有 PyTorch 的环境:github.com/pytorch/serve/blob/master/docker/README.md

接下来,你将学习如何使用自定义 Docker 镜像进行构建。

构建自定义 Docker 镜像

创建自定义镜像也很简单。但是,它涉及许多命令,我们将详细信息委托给 github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_2/dockerfiles。一旦构建了 Docker 镜像,您可以使用称为 Docker 容器的东西来实例化它。Docker 容器是一个独立的可执行软件包,包括运行目标应用程序所需的所有内容。通过按照 README.md 文件中的说明操作,您可以创建一个 Docker 镜像,该镜像将在本章中描述的标准库中运行一个容器化的 Jupyter 笔记本服务。

要记住的事情

a. Docker 创建了您工作环境的快照,包括底层操作系统。创建的镜像可用于在不同的机器上重新创建相同的环境。

b. Docker 帮助您将环境与基础设施分离。这使得您可以将应用程序轻松移动到不同的云服务提供商(如 AWS 或 Google Cloud)。

此时,您应该能够为您的深度学习项目创建一个 Docker 镜像。通过实例化 Docker 镜像,您应该能够在本地机器或各种云服务提供商上收集所需的数据并进行处理。

总结

在本章中,我们描述了如何为数据分析任务准备数据集。第一个关键点是如何使用 Anaconda 和 Docker 实现环境虚拟化,并使用 pip 进行 Python 包管理。

数据准备过程可以分解为四个步骤:数据收集、数据清理、数据预处理和特征提取。首先,我们介绍了支持不同数据类型的数据收集工具。一旦数据被收集,它就会被清理和预处理,以便能够转换为通用形式。根据目标任务,我们经常应用各种特定于任务的特征提取技术。此外,我们介绍了许多数据可视化工具,可以帮助您了解准备好的数据的特性。

现在我们已经学习了如何为分析任务准备数据,下一章中,我们将解释深度学习模型开发。我们将介绍基本概念以及如何使用两个最流行的深度学习框架:TensorFlow 和 PyTorch。

第三章:开发一个强大的深度学习模型

在本章中,我们将描述如何设计和训练深度学习(DL)模型。在上一章节中描述的笔记本上下文中,数据科学家们调查各种网络设计和模型训练设置,以生成适合给定任务的工作模型。本章的主要内容包括 DL 的理论以及如何使用最流行的 DL 框架PyTorchTensorFlowTF)训练模型。在本章末尾,我们将解构StyleGAN的实现,这是一个用于图像生成的流行 DL 模型,以解释如何使用我们在本章介绍的组件构建复杂模型。

在本章中,我们将涵盖以下主要内容:

  • 学习深度学习基础理论

  • 理解 DL 框架的组成部分

  • 在 PyTorch 中实现和训练模型

  • 在 TF 中实现和训练模型

  • 解构复杂的最新模型实现

技术要求

您可以从以下 GitHub 链接下载本章的补充材料:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_3

本章中的示例可以在安装了必要包的任何 Python 环境中执行。您可以使用上一章介绍的示例环境:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_2/dockerfiles

学习深度学习基础理论

第一章中简要描述的,深度学习驱动项目的有效规划,DL 是基于人工神经网络(ANNs)的机器学习(ML)技术。本节的目标是在不深入数学的情况下解释 ANNs 的工作原理。

DL 是如何工作的?

人工神经网络(ANN)基本上是一组相连的神经元。如图 3.1所示,ANN 中的神经元和我们大脑中的神经元表现出类似的行为。在 ANN 中,每个连接都有一个可调参数称为权重。当神经元 A 到神经元 B 有连接时,神经元 A 的输出乘以连接的权重,得到的加权值成为神经元 B 的输入。偏置是神经元内的另一个可调参数;神经元将所有输入求和并加上偏置。最后的操作是激活函数,将计算得到的值映射到不同的范围。新范围内的值是神经元的输出,根据连接传递给其他神经元。

在研究中发现,神经元组根据其组织方式捕捉不同的模式。一些强大的组织形式被标准化为,已成为 ANN 的主要构建模块,为复杂的神经元间互动提供了一层抽象。

图 3.1 – 生物神经元与 ANN 神经元数学模型的比较

图 3.1 – 生物神经元与 ANN 神经元数学模型的比较

正如前面的图表所述,DL 中的操作基于数值。因此,网络的输入数据必须转换为数值。例如,红、绿、蓝RGB)颜色代码是用数值表示图像的标准方式。对于文本数据,通常使用词嵌入。类似地,网络的输出将是一组数值。这些值的解释可以根据任务和定义而异。

DL 模型训练

总体而言,训练 ANN 是一个寻找一组权重、偏差和激活函数的过程,使得网络能够从数据中提取有意义的模式。接下来的问题是:我们如何找到合适的参数集? 许多研究人员尝试使用各种技术解决这个问题。在所有尝试中,发现的最有效算法是一种称为梯度下降的优化算法,这是一个迭代过程,用于找到局部或全局最小值。

在训练 DL 模型时,我们需要定义一个函数,将预测值与地面实况标签之间的差异量化为一个称为损失的数值。一旦损失函数被明确定义,我们就会迭代生成中间预测,计算损失值,并朝向最小损失方向更新模型参数。

鉴于优化的目标是找到最小损失,模型参数需要根据训练集样本在梯度的反方向上进行更新(见图 3.2)。为了计算梯度,网络在预测过程中跟踪计算中间值(前向传播)。然后,从最后一层开始,利用链式法则计算每个参数的梯度(反向传播)。有趣的是,模型的性能和训练时间可能会根据每次迭代中参数更新的方式有很大的差异。不同的参数更新规则包含在优化器的概念中。DL 的一个主要任务之一是选择能够产生最佳性能模型的优化器类型。

图 3.2 – 利用梯度下降,模型参数将在每次迭代中朝着梯度的相反方向更新

图 3.2 – 使用梯度下降,模型参数将在每次迭代中朝着梯度的反方向更新

然而,这个过程有一个需要注意的地方。如果模型被训练来在训练集上获得最佳性能,那么在未见数据上的性能可能会下降。这被称为过拟合;模型专门针对它以前见过的数据进行训练,无法正确预测新数据。另一方面,训练不足会导致欠拟合,即模型未能捕获训练集的潜在模式。为了避免这些问题,训练集的一部分被保留用于在训练过程中评估训练好的模型:验证集。总体而言,DL 的训练涉及根据训练集更新模型参数的过程,但选择在验证集上表现最佳的模型。最后一种数据集,测试集,代表了模型部署后可能与之交互的数据。测试集在模型训练时可能可用,也可能不可用。测试集的目的是了解训练好的模型在生产环境中的表现。为了进一步理解整体的训练逻辑,我们可以看一下图 3.3

Figure 3.3 – 训练 DL 模型的步骤

图 3.3 – 训练 DL 模型的步骤

这张图清晰地描述了迭代过程中的步骤,以及每种类型数据集在场景中的角色。

需要记住的事情

a. 训练人工神经网络(ANN)是一个寻找一组权重、偏置和激活函数的过程,使网络能够从数据中提取有意义的模式。

b. 在训练流程中有三种类型的数据集。使用训练集更新模型参数,并选择在验证集上表现最佳的模型。测试集反映了训练好的模型在部署时将与之交互的数据分布。

接下来,我们将看看旨在帮助我们进行模型训练的 DL 框架。

DL 框架的组成部分

由于模型训练的配置遵循相同的流程,无论底层任务如何,许多工程师和研究人员已经将常见的构建模块整合到框架中。大多数框架通过将数据加载逻辑和模型定义与训练逻辑分离,简化了 DL 模型的开发。

数据加载逻辑

数据加载逻辑包括从内存加载原始数据到为每个样本准备训练和评估所需的所有步骤。在许多情况下,训练集、验证集和测试集的数据存储在不同的位置,因此每个集合都需要不同的加载和准备逻辑。标准框架将这些逻辑与其他构建模块分开,以便可以以动态方式使用不同的数据集进行模型训练,并在模型方面进行最小更改。此外,这些框架已经标准化了这些逻辑的定义方式,以提高可重用性和可读性。

模型定义

另一个基础模块,模型定义,指的是人工神经网络的架构本身及其对应的前向和反向传播逻辑。尽管使用算术运算建立模型是一种选择,但标准框架提供了常见的层定义,用户可以组合这些定义以构建复杂的模型。因此,用户需负责实例化必要的网络组件,连接这些组件,并定义模型在训练和推断时的行为方式。

在接下来的两个部分中,在 PyTorch 中实现和训练模型在 TF 中实现和训练模型,我们将介绍如何在 PyTorch 和 TF 中实例化流行的层:稠密(线性)、池化、归一化、dropout、卷积和循环层。

模型训练逻辑

最后,我们需要结合这两个组件并定义训练逻辑的详细信息。这个包装组件必须清晰描述模型训练的关键部分,例如损失函数、学习率、优化器、epochs、迭代次数和批处理大小。

损失函数可以根据学习任务的类型分为两大类:分类损失回归损失。这两类之间的主要区别来自输出格式;分类任务的输出是分类的,而回归任务的输出是连续值。在不同的损失中,我们将主要讨论用于回归损失的均方误差MSE)和平均绝对误差MAE)损失,以及用于分类损失的交叉熵CE)和二元交叉熵BCE)损失。

学习率LR)定义了梯度下降在局部最小值方向上迈出的步伐大小。选择适当的学习率可以帮助过程更快地收敛,但如果学习率过高或过低,收敛性将无法保证(见图 3.4):

图 3.4 – 学习率在梯度下降中的影响

图 3.4 – 学习率在梯度下降中的影响

谈到优化器,我们关注两个主要的优化器:随机梯度下降SGD),一个具有固定学习率的基本优化器,以及自适应矩估计Adam),一种基于自适应学习率的优化器,在大多数情况下表现最佳。如果你有兴趣了解不同优化器及其背后的数学原理,我们推荐阅读 Choi 等人的调查论文(arxiv.org/pdf/1910.05446.pdf)。

一个epoch表示训练集中的每个样本都已经通过网络进行了前向和反向传播,并且网络参数已经更新。在许多情况下,训练集中的样本数量太大,无法一次通过,因此会将其分成mini-batchesbatch size指的是单个 mini-batch 中的样本数量。给定一组 mini-batches 组成整个数据集,迭代次数指的是模型需要与每个样本进行梯度更新事件(更确切地说是 mini-batch 的数量)。例如,如果一个 mini-batch 有 100 个样本,总共有 1,000 个样本,那么完成一个 epoch 需要 10 次迭代。选择合适的 epoch 数量并不容易。这取决于其他训练参数,如学习率和 batch size。因此,通常需要通过试验和错误的过程来确定,同时注意欠拟合和过拟合的问题。

需要记住的事情

a. 模型训练的组成部分可以分为数据加载逻辑、模型定义和模型训练逻辑。

b. 数据加载逻辑包括从内存加载原始数据到为训练和评估准备每个样本的所有步骤。

c. 模型定义指的是网络架构及其正向和反向传播逻辑的定义。

d. 模型训练逻辑通过将数据加载逻辑和模型定义结合起来处理实际的训练过程。

在众多可用的框架中,我们将在本书中讨论两个最流行的框架:TFPyTorch。在今天,运行在 TF 上的 Keras 已经变得非常流行,而 PyTorch 以其出色的灵活性和简易性在研究中被广泛使用。

在 PyTorch 中实现和训练模型

PyTorch 是一个用于 Lua 的 ML 包的 Python 库。 PyTorch 的主要特性包括图形处理单元GPU)加速的矩阵计算和用于构建和训练神经网络的自动微分。在代码执行过程中动态创建计算图,PyTorch 因其灵活性、易用性以及在模型训练中的高效性而受到青睐。

基于 PyTorch,PyTorch LightningPL)提供了另一层抽象,隐藏了许多样板代码。新框架更多关注研究人员,通过将 PyTorch 的研究相关组件与工程相关组件解耦。PL 的代码通常比 PyTorch 代码更可扩展和易读。尽管本书中的代码片段更加强调 PL,但 PyTorch 和 PL 共享许多功能,因此大多数组件是可互换的。如果您愿意深入了解细节,我们推荐访问官方网站,pytorch.org

市场上还有其他扩展 PyTorch 的选择:

我们不会在本书中涵盖这些库,但如果您对这个领域是新手,可能会发现它们很有帮助。

现在,让我们深入了解 PyTorch 和 PL。

PyTorch 数据加载逻辑

为了可读性和模块化,PyTorch 和 PL 利用名为Dataset的类进行数据管理,并利用另一个名为DataLoader的类来迭代访问样本。

虽然Dataset类处理获取单个样本,但模型训练会批量输入数据,并需要重排以减少模型过拟合。DataLoader通过提供简单的 API 来为用户抽象这种复杂性。此外,它在后台利用 Python 的多进程功能加速数据检索。

Dataset子类必须实现的两个核心函数是__len____getitem__。如下课程大纲所述,__len__应返回总样本数,__getitem__应返回给定索引的样本:

from torch.utils.data import Dataset
class SampleDataset(Dataset):
   def __len__(self):
      """return number of samples"""
   def __getitem__(self, index):
      """loads and returns a sample from the dataset at the given index"""

PL 的LightningDataModule封装了处理数据所需的所有步骤。关键组件包括下载和清理数据,预处理每个样本,并将每种数据集包装在DataLoader中。以下代码片段描述了如何创建LightningDataModule类。该类具有prepare_data函数用于下载和预处理数据,以及三个函数用于实例化每种数据集的DataLoader,分别是train_dataloaderval_dataloadertest_dataloader

from torch.utils.data import DataLoader
from pytorch_lightning.core.lightning import LightningDataModule
class SampleDataModule(LightningDataModule):
   def prepare_data(self):
       """download and preprocess the data; triggered only on single GPU"""
       ...
   def setup(self):
       """define necessary components for data loading on each GPU"""
       ...
   def train_dataloader(self):
       """define train data loader"""
       return data.DataLoader(
         self.train_dataset, 
           batch_size=self.batch_size, 
           shuffle=True)
   def val_dataloader(self):
       """define validation data loader"""
       return data.DataLoader(
          self.validation_dataset, 
          batch_size=self.batch_size, 
          shuffle=False)             
   def test_dataloader(self):
       """define test data loader"""
       return data.DataLoader(
          self.test_dataset, 
          batch_size=self.batch_size, 
          shuffle=False)

LightningDataModule 的官方文档可以在 pytorch-lightning.readthedocs.io/en/stable/extensions/datamodules.html 找到。

PyTorch 模型定义

PL 的关键优势来自 LightningModule,它将复杂的 PyTorch 代码简化为六个部分:

  • 计算(__init__

  • 训练循环(training_step

  • 验证循环(validation_step

  • 测试循环(test_step

  • 预测循环(predict_step

  • 优化器和学习率调度器(configure_optimizers

模型架构是计算部分的一部分。必要的层在 __init__ 方法中实例化,计算逻辑在 forward 方法中定义。在以下代码片段中,三个线性层在 __init__ 方法内注册到 LightningModule 模块,并在 forward 方法内定义它们之间的关系:

from pytorch_lightning import LightningModule
from torch import nn
class SampleModel(LightningModule):
   def __init__(self):
      """instantiate necessary layers"""
       self.individual_layer_1 = nn.Linear(..., ...)
       self.individual_layer_2 = nn.Linear(..., ...)
       self.individual_layer_3 = nn.Linear(..., ...)
   def forward(self, input):
       """define forward propagation logic"""
       output_1 = self.individual_layer_1(input)
       output_2 = self.individual_layer_2(output_1)
       final_output = self.individual_layer_3(output_2)
       return final_output

另一种定义网络的方法是使用 torch.nn.Sequential,如下所示。使用此模块,一组层可以被组合在一起,并且自动实现输出链式化:

class SampleModel(LightningModule):
   def __init__(self):
       """instantiate necessary layers"""
       self.multiple_layers = nn.Sequential(
       nn.Linear(    ,    ),
       nn.Linear(    ,    ),
       nn.Linear(    ,    ))
   def forward(self, input):
       """define forward propagation logic"""
       final_output = self.multiple_layers(input)
       return final_output

在前面的代码中,三个线性层被组合在一起并存储为单个实例变量 self.multiple_layers。在 forward 方法中,我们只需触发 self.multiple_layers 与输入张量以逐个通过每个层传递张量。

以下部分旨在介绍常见的层实现。

PyTorch DL 层

DL 框架的一个主要优势来自于各种层定义:梯度计算逻辑已经成为层定义的一部分,因此您可以专注于找到适合任务的最佳模型架构。在本节中,我们将了解跨项目常用的层。如果您感兴趣的层在本节中未涵盖,请参阅官方文档(pytorch.org/docs/stable/nn.html)。

PyTorch 密集(线性)层

第一种类型的层是 torch.nn.Linear。顾名思义,它对输入张量应用线性变换。函数的两个主要参数是 in_featuresout_features,分别定义输入和输出张量的维度:

linear_layer = torch.nn.Linear(
              in_features,   # Size of each input sample
              out_features,  # Size of each output sample)
# N = batch size
# * = any number of additional dimensions
input_tensor = torch.rand(N, *, in_features)
output_tensor = linear_layer(input_tensor) # (N, *, out_features)

来自 torch.nn 模块的层实现已经定义了 forward 函数,因此您可以将层变量像函数一样使用,以触发前向传播。

PyTorch 池化层

池化层通常用于对张量进行下采样。最流行的两种类型是最大池化和平均池化。这些层的关键参数是 kernel_sizestride,分别定义窗口的大小以及每个池化操作的移动方式。

最大池化层通过选择每个窗口中的最大值来对输入张量进行下采样:

# 2D max pooling
max_pool_layer = torch.nn.MaxPool2d(
   kernel_size,         # the size of the window to take a max over
   stride=None,         # the stride of the window. Default value is kernel_size
   padding=0,           # implicit zero padding to be added on both sides
   dilation=1,          # a parameter that controls the stride of elements in the window)
# N = batch size
# C = number of channels
# H = height of input planes in pixels
# W = width of input planes in pixels
input_tensor = torch.rand(N, C, H, W)
output_tensor = max_pool_layer(input_tensor) # (N, C, H_out, W_out) 

另一方面,平均池化层通过计算每个窗口的平均值来降低输入张量的分辨率:

# 2D average pooling
avg_pool_layer = torch.nn.AvgPool2d(
   kernel_size,         # the size of the window to take a max over
   stride=None,         # the stride of the window. Default value is kernel_size
   padding=0,           # implicit zero padding to be added on both sides)
# N = batch size
# C = number of channels
# H = height of input planes in pixels
# W = width of input planes in pixels
input_tensor = torch.rand(N, C, H, W)
output_tensor = avg_pool_layer(input_tensor) # (N, C, H_out, W_out)

你可以在 pytorch.org/docs/stable/nn.html#pooling-layers 找到其他类型的池化层。

PyTorch 归一化层

在数据处理中常用的归一化的目的是将数值数据缩放到一个公共尺度,而不改变其分布。在深度学习中,归一化层用于以更大的数值稳定性训练网络 (pytorch.org/docs/stable/nn.html#normalization-layers)。

最常用的归一化层是批量归一化层,它对一个小批量的值进行缩放。在下面的代码片段中,我们介绍了 torch.nn.BatchNorm2d,一个为带有额外通道维度的二维张量小批量设计的批量归一化层:

batch_norm_layer = torch.nn.BatchNorm2d(
   num_features,      # Number of channels in the input image
   eps=1e-05,         # A value added to the denominator for numerical stability
   momentum=0.1,      # The value used for the running_mean and running_var computation
   affine=True,       # a boolean value that when set to True, this module has learnable affine parameters)
# N = batch size
# C = number of channels
# H = height of input planes in pixels
# W = width of input planes in pixels
input_tensor = torch.rand(N, C, H, W)
output_tensor = batch_norm_layer(input_tensor) # same shape as input (N, C, H, W)

在各种参数中,你应该了解的主要参数是 num_features,它表示通道数。层的输入是一个四维张量,其中每个索引表示批量大小 (N),通道数 (C),图像高度 (H) 和图像宽度 (W)。

PyTorch 丢弃层

丢弃层帮助模型通过随机将一组值设为零来提取通用特征。这种操作可以防止模型过度拟合训练集。话虽如此,PyTorch 的丢弃层主要通过一个参数 p 控制一个元素被设为零的概率:

drop_out_layer = torch.nn.Dropout2d(
   p=0.5,  # probability of an element to be zeroed )
# N = batch size
# C = number of channels
# H = height of input planes in pixels
# W = width of input planes in pixels
input_tensor = torch.rand(N, C, H, W)
output_tensor = drop_out_layer(input_tensor) # same shape as input (N, C, H, W)

在这个例子中,我们将元素的 50%设为零 (p=0.5)。与批量归一化层类似,torch.nn.Dropout2d 的输入张量大小为 N, C, H, W

PyTorch 卷积层

针对图像处理专门设计的卷积层,使用滑动窗口技术在输入张量上应用卷积操作。在图像处理中,中间数据以 N, C, H, W 大小的四维张量表示,torch.nn.Conv2d 是标准选择:

conv_layer = torch.nn.Conv2d(
   in_channels,         # Number of channels in the input image
   out_channels,        # Number of channels produced by the convolution
   kernel_size,         # Size of the convolving kernel
   stride=1,            # Stride of the convolution
   padding=0,           # Padding added to all four sides of the input.
   dilation=1,          # Spacing between kernel elements)
# N = batch size
# C = number of channels
# H = height of input planes in pixels
# W = width of input planes in pixels
input_tensor = torch.rand(N, C_in, H, W)
output_tensor = conv_layer(input_tensor) # (N, C_out, H_out, W_out)

torch.nn.Conv2d 类的第一个参数 in_channels 表示输入张量中的通道数。第二个参数 out_channels 表示输出张量中的通道数,即滤波器的数量。其他参数 kernel_sizestridepadding 决定了如何对该层进行卷积操作。

PyTorch 循环层

递归层设计用于序列数据。在各种类型的递归层中,本节将介绍torch.nn.RNN,它将多层 Elman递归神经网络RNN)应用于给定的序列(onlinelibrary.wiley.com/doi/abs/10.1207/s15516709cog1402_1)。如果您想尝试不同的递归层,可以参考官方文档:pytorch.org/docs/stable/nn.html#recurrent-layers

# multi-layer Elman RNN with tanh or ReLU non-linearity to an input sequence.
rnn = torch.nn.RNN(
   input_size,                     # The number of expected features in the input x
   hidden_size,                    # The number of features in the hidden state h
   num_layers = 1,                 # Number of recurrent layers
   nonlinearity="tanh",            # The non-linearity to use. Can be either 'tanh' or 'relu'
   bias=True,                      # If False, then the layer does not use bias weights
   batch_first=False,              # If True, then the input and output tensors are provided
                                                                                                 # as (batch, seq, feature) instead of (seq, batch, feature)
             dropout=0,                                                # If non-zero, introduces a Dropout layer on the outputs of each RNN layer
                                                                                                 # except the last layer, with dropout probability equal to dropout
   bidirectional=False,             # If True, becomes a bidirectional RNN)
# N = batch size
# L = sequence length
# D = 2 if bidirectionally, otherwise 1
# H_in = input_size
# H_out = hidden_size
rnn = nn.RNN(H_in, H_out, num_layers)
input_tensor = torch.randn(L, N, H_in)
# H_0 = tensor containing the initial hidden state for each element in the batch
h0 = torch.randn(D * num_layers, N, H_out)
# output_tensor (L, N, D * H_out)
# hn (D * num_layers, N, H_out) 
output_tensor, hn = rnn(input_tensor, h0)

torch.nn.RNN的三个关键参数是input_sizehidden_sizenum_layers。它们分别指的是输入张量中预期的特征数、隐藏状态中的特征数以及要使用的递归层数。为了触发前向传播,您需要传递两个东西,一个输入张量和一个包含初始隐藏状态的张量。

PyTorch 模型训练

在本节中,我们描述了 PL 的模型训练组件。如下面的代码块所示,LightningModule是您必须继承的基类。它的configure_optimizers函数用于定义训练的优化器。然后,实际的训练逻辑在training_step函数中定义:

class SampleModel(LightningModule):
   def configure_optimizers(self):
      """Define optimizer to use"""
      return torch.optim.Adam(self.parameters(), lr=0.02)
   def training_step(self, batch, batch_idx):
      """Define single training iteration"""
      x, y = batch
      y_hat = self(x)
      loss = F.cross_entropy(y_hat, y)
      return loss

验证、预测和测试循环具有类似的函数定义;一个批次被馈送到网络中以计算必要的预测和损失值。收集的数据也可以使用 PL 的内置日志记录系统进行存储和显示。有关详细信息,请参阅官方文档(pytorch-lightning.readthedocs.io/en/latest/common/lightning_module.html):

   def validation_step(self, batch, batch_idx):
      """Define single validation iteration"""
      loss, acc = self._shared_eval_step(batch, batch_idx)
      metrics = {"val_acc": acc, "val_loss": loss}
      self.log_dict(metrics)
      return metrics
   def test_step(self, batch, batch_idx):
      """Define single test iteration"""
      loss, acc = self._shared_eval_step(batch, batch_idx)
      metrics = {"test_acc": acc, "test_loss": loss}
      self.log_dict(metrics)
      return metrics
   def _shared_eval_step(self, batch, batch_idx):
      x, y = batch
      outputs = self(x)
      loss = self.criterion(outputs, targets)
      acc = accuracy(outputs.round(), targets.int())
      return loss, acc
   def predict_step(self, batch, batch_idx, dataloader_idx=0):
      """Compute prediction for the given batch of data"""
      x, y = batch
      y_hat = self(x)
      return y_hat

在幕后,LightningModule执行以下一系列简化的 PyTorch 代码:

model.train()
torch.set_grad_enabled(True)
outs = []
for batch_idx, batch in enumerate(train_dataloader):
   loss = training_step(batch, batch_idx)
   outs.append(loss.detach())
   # clear gradients
   optimizer.zero_grad()
   # backward
   loss.backward()
   # update parameters
   optimizer.step()
   if validate_at_some_point
      model.eval()
      for val_batch_idx, val_batch in enumerate(val_dataloader):
         val_out = model.validation_step(val_batch, val_batch_idx)
         model.train()

LightningDataModuleLightningModule结合在一起,可以简单地实现对测试集的训练和推理如下:

from pytorch_lightning import Trainer
data_module = SampleDataModule()
trainer = Trainer(max_epochs=num_epochs)
model = SampleModel()
trainer.fit(model, data_module)
result = trainer.test()

到目前为止,您应该已经学会了如何使用 PyTorch 设置模型训练。接下来的两节专门介绍了损失函数和优化器,这两个模型训练的主要组成部分。

PyTorch 损失函数

首先,我们将查看 PL 中提供的不同损失函数。本节中的损失函数可以在torch.nn模块中找到。

PyTorch MSE / L2 损失函数

可以使用torch.nn.MSELoss创建 MSE 损失函数。但是,这仅计算平方误差分量,并利用reduction参数提供变体。当reductionNone时,返回计算值。另一方面,当设置为sum时,输出将被累加。为了获得精确的 MSE 损失,必须将 reduction 设置为mean,如下面的代码片段所示:

loss = nn.MSELoss(reduction='mean')
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)

接下来,让我们看看 MAE 损失。

PyTorch MAE / L1 损失函数

MAE(Mean Absolute Error,平均绝对误差)损失函数可以通过torch.nn.L1Loss来实例化。与 MSE 损失函数类似,此函数根据reduction参数计算不同的值:

Loss = nn.L1Loss(reduction='mean')
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)

现在我们可以转向 CE 损失,它在多类分类任务中使用。

PyTorch 交叉熵损失函数

torch.nn.CrossEntropyLoss在训练多类别分类问题的模型时非常有用。正如下面的代码片段所示,此类别还具有reduction参数以计算不同的变体。您还可以使用weightignore_index参数来进一步改变损失的行为,分别对每个类别进行加权并忽略特定索引:

loss = nn.CrossEntropyLoss(reduction="mean")
input = torch.randn(3, 5, requires_grad=True)
target = torch.empty(3, dtype=torch.long).random_(5)
output = loss(input, target)

类似地,我们可以定义 BCE 损失。

PyTorch BCE 损失函数

类似于 CE 损失,PyTorch 将 BCE 损失定义为torch.nn.BCELoss,具有相同的参数集。然而,利用torch.nn.BCELoss与 sigmoid 操作之间的密切关系,PyTorch 提供了torch.nn.BCEWithLogitsLoss,通过在一个类中组合softmax操作和 BCE 损失计算,实现了更高的数值稳定性。其使用方法如下面的代码片段所示:

loss = torch.nn.BCEWithLogitsLoss(reduction="mean")
input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(input, target)

最后,让我们看一下如何构建 PyTorch 中的自定义损失。

PyTorch 自定义损失函数

定义自定义损失函数非常简单。任何使用 PyTorch 操作定义的函数都可以用作损失函数。

下面是使用mean操作符实现的torch.nn.MSELoss的样本实现:

def custom_mse_loss(output, target):
   loss = torch.mean((output - target)**2)
   return loss
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = custom_mse_loss(input, target)

现在,我们将转到 PyTorch 优化器的概述。

PyTorch 优化器

PyTorch 模型训练部分所述,LightningModuleconfigure_optimizers函数指定了训练的优化器。在 PyTorch 中,可以从torch.optim模块中找到预定义的优化器。优化器实例化需要模型参数,可以通过在模型上调用parameters函数来获取,如以下部分所示。

PyTorch SGD 优化器

下面的代码片段实例化了一个 LR 为0.1的 SGD 优化器,并展示了如何实现模型参数更新的单步操作。

torch.optim.SGD内置支持动量和加速,进一步提高了训练性能。可以使用momentumnesterov参数进行配置:

optimizer = torch.optim.SGD(model.parameters(), lr=0.1 momentum=0.9, nesterov=True)

PyTorch Adam 优化器

同样地,可以使用torch.optim.Adam来实例化 Adam 优化器,如下面的代码行所示:

optimizer = torch.optim.Adam(model.parameters(), lr=0.1) 

如果您对 PyTorch 中优化器的工作原理感兴趣,我们建议阅读官方文档:pytorch.org/docs/stable/optim.html

要记住的事情

a. PyTorch 是一种流行的深度学习框架,提供了 GPU 加速的矩阵计算和自动微分功能。PyTorch 因其灵活性、易用性以及在模型训练中的效率而受到欢迎。

b. 为了提高可读性和模块化性,PyTorch 利用名为Dataset的类来进行数据管理,并使用另一个名为DataLoader的类来迭代访问样本。

c. PL 的关键优势来自于LightningModule,它简化了复杂的 PyTorch 代码结构组织为六个部分:计算、训练循环、验证循环、测试循环、预测循环,以及优化器和学习率调度器。

d. PyTorch 和 PL 共享torch.nn模块,用于各种层和损失函数。预定义的优化器可以在torch.optim模块中找到。

在接下来的部分中,我们将看一看另一个深度学习框架,TF。使用 TF 进行的训练设置与使用 PyTorch 的设置非常相似。

在 TF 中实现和训练模型

虽然 PyTorch 偏向于研究项目,TF 更加注重行业应用案例。尽管 PyTorch 的部署功能,如 Torch Serve 和 Torch Mobile 仍处于实验阶段,TF 的部署功能,如 TF Serve 和 TF Lite 已经稳定并且在积极使用中。TF 的第一个版本由 Google Brain 团队于 2011 年推出,并且他们持续更新 TF 以使其更加灵活、用户友好和高效。TF 和 PyTorch 的关键区别最初相差较大,因为 TF 的第一个版本使用静态图。然而,随着版本 2 的推出,情况已经改变,它引入了急切执行,模仿了 PyTorch 中的动态图。TF 版本 2 经常与Keras一起使用,这是一个用于人工神经网络的接口(keras.io)。Keras 允许用户快速开发深度学习模型并运行实验。在接下来的部分中,我们将介绍 TF 的关键组件。

TF 数据加载逻辑

TF 模型可以以多种方式加载数据。你应该了解的一个关键数据操作模块是tf.data,它帮助你构建高效的输入管道。tf.data提供了tf.data.Datasettf.data.TFRecordDataset类,专门设计用于加载不同数据格式的数据集。此外,还有tensorflow_datasetstfds)模块(www.tensorflow.org/datasets/api_docs/python/tfds)和tensorflow_addons模块(www.tensorflow.org/addons),它们进一步简化了许多情况下的数据加载过程。还值得一提的是 TF I/O 包(www.tensorflow.org/io/overview),它扩展了标准 TF 文件系统交互的功能。

无论你将使用哪个包,你都应该考虑创建一个DataLoader类。在这个类中,你将清晰地定义目标数据如何加载以及在训练之前如何预处理。以下代码片段展示了一个加载逻辑的示例实现:

import tensorflow_datasets as tfds
class DataLoader: 
   """ DataLoader class"""
   @staticmethod 
   def load_data(config): 
      return tfds.load(config.data_url)

在前面的例子中,我们使用tfds从外部 URL(config.data_url)加载数据。关于tfds.load的更多信息可以在线找到:www.tensorflow.org/datasets/api_docs/python/tfds/load

数据以各种格式可用。因此,重要的是将其预处理为 TF 模型可以使用的格式,使用tf.data模块提供的功能。因此,让我们看一下如何使用这个包来读取常见格式的数据:

  • 首先,可以按如下方式读取以tfrecord格式存储的数据序列:

    import tensorflow as tf 
    dataset = tf.data.TFRecordDataset(list_of_files)
    
  • 我们可以使用tf.data.Dataset.from_tensor_slices函数从 NumPy 数组创建数据集对象,如下所示:

    dataset = tf.data.Dataset.from_tensor_slices(numpy_array)
    
  • Pandas DataFrames 也可以使用相同的tf.data.Dataset.from_tensor_slices函数加载为数据集:

    dataset = tf.data.Dataset.from_tensor_slices((df_features.values, df_target.values))
    
  • 另一种选项是使用 Python 生成器。以下是一个简单的示例,演示如何使用生成器来提供配对的图像和标签:

    def data_generator(images, labels):
       def fetch_examples(): 
           i = 0 
           while True: 
              example = (images[i], labels[i]) 
              i += 1 
              i %= len(labels) 
              yield example 
           return fetch_examples
    training_dataset = tf.data.Dataset.from_generator(
       data_generator(images, labels),
       output_types=(tf.float32, tf.int32), 
       output_shapes=(tf.TensorShape(features_shape), tf.TensorShape(labels_shape)))
    

如上一段代码片段所示,tf.data.Dataset 提供了内置的数据加载功能,例如批处理、重复和洗牌。这些选项都是不言自明的:批处理创建特定大小的小批量数据,重复允许我们多次迭代数据集,而洗牌则在每个 epoch 中混淆数据条目。

在结束本节之前,我们想提到,使用 Keras 实现的模型可以直接消耗 NumPy 数组和 Pandas DataFrames。

TF 模型定义

类似于 PyTorch 和 PL 如何处理模型定义,TF 提供了多种定义网络架构的方法。首先,我们将看看Keras.Sequential,它链式连接一组层以构建网络。这个类为您处理了层之间的链接,因此您不需要显式地定义它们的连接:

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
input_shape = 50
model = keras.Sequential(
   [
      keras.Input(shape=input_shape),
      layers.Dense(128, activation="relu", name="layer1"),
      layers.Dense(64, activation="relu", name="layer2"),
      layers.Dense(1, activation="sigmoid", name="layer3"),
   ])

在前面的例子中,我们创建了一个模型,包括一个输入层、两个密集层和一个生成单个神经元输出的输出层。这是一个简单的模型,可以用于二元分类。

如果模型定义更复杂,无法按顺序构建,另一种选择是使用keras.Model类,如下面的代码片段所示:

num_classes = 5 
input_1 = layers.Input(50)
input_2 = layers.Input(10)
x_1 = layers.Dense(128, activation="relu", name="layer1x")(input_1)
x_1 = layers.Dense(64, activation="relu", name="layer1_2x")(x_1)
x_2 = layers.Dense(128, activation="relu", name="layer2x")(input_2)
x_2 = layers.Dense(64, activation="relu", name="layer2_1x")(x_2)
x = layers.concatenate([x_1, x_2], name="concatenate")
out = layers.Dense(num_classes, activation="softmax", name="output")(x)
model = keras.Model((input_1,input_2), out)

在这个例子中,我们有两个输入,并进行了不同的计算。这两条路径在最后的串联层中合并,将串联的张量传输到最终的具有五个神经元的密集层中。考虑到最后一层使用了softmax激活函数,这个模型可以用于多类分类。

第三个选项如下,是创建一个继承了keras.Model的类。这个选项给了你最大的灵活性,允许你自定义模型的每个部分和训练过程:

class SimpleANN(keras.Model):
   def __init__(self):
      super().__init__()
      self.dense_1 = layers.Dense(128, activation="relu", name="layer1")
      self.dense_2 = layers.Dense(64, activation="relu", name="layer2")
      self.out = layers.Dense(1, activation="sigmoid", name="output")
   def call(self, inputs):
      x = self.dense_1(inputs)
      x = self.dense_3(x)
      return self.out(x)
model = SimpleANN()

SimpleANN,来自前面的代码,继承自 Keras.Model。在 __init__ 函数中,我们需要使用 tf.keras.layers 模块或基本的 TF 操作来定义网络架构。前向传播逻辑在 call 方法内部定义,就像 PyTorch 中有 forward 方法一样。

当模型被定义为一个独立的类时,您可以将额外的功能链接到该类。在下面的示例中,添加了 build_graph 方法以返回一个 keras.Model 实例,因此您可以例如使用 summary 函数来可视化网络架构作为更简单的表示:

class SimpleANN(keras.Model):
   def __init__(self):
   ...
   def call(self, inputs):
   ...
   def build_graph(self, raw_shape):
      x = tf.keras.layers.Input(shape=raw_shape)
      return keras.Model(inputs=[x], outputs=self.call(x))

现在,让我们看看 TF 如何通过 Keras 提供一组层实现。

TF 深度学习层

如前一节所述,tf.keras.layers 模块提供了一组层实现,您可以用来构建 TF 模型。在本节中,我们将涵盖与我们在 PyTorch 中实现和训练模型 部分描述的相同一组层。此模块中可用的层的完整列表可以在 www.tensorflow.org/api_docs/python/tf/keras/layers 找到。

TF 密集(线性)层

第一个是 tf.keras.layers.Dense,执行线性转换:

tf.keras.layers.Dense(units, activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None, **kwargs)

units 参数定义了密集层中神经元的数量(输出的维度)。如果未定义 activation 参数,则层的输出将原样返回。如下面的代码所示,我们也可以在层定义之外应用 Activation 操作:

X = layers.Dense(128, name="layer2")(input)
x = tf.keras.layers.Activation('relu')(x)

在某些情况下,您需要构建一个自定义层。下面的示例演示了如何使用基本的 TF 操作创建一个密集层,通过继承 tensorflow.keras.layers.Layer 类:

import tensorflow as tf
from tensorflow.keras.layers import Layer
class CustomDenseLayer(Layer):
   def __init__(self, units=32):
      super(SimpleDense, self).__init__()
      self.units = units
   def build(self, input_shape):
      w_init = tf.random_normal_initializer()
      self.w = tf.Variable(name="kernel", initial_value=w_init(shape=(input_shape[-1], self.units),
      dtype='float32'),trainable=True)
      b_init = tf.zeros_initializer()
      self.b = tf.Variable(name="bias",initial_value=b_init(shape=(self.units,), dtype='float32'),trainable=True)
   def call(self, inputs):
      return tf.matmul(inputs, self.w) + self.b

CustomDenseLayer 类的 __init__ 函数中,我们定义输出的维度(units)。然后,在 build 方法中实例化层的状态;我们为层创建并初始化权重和偏置。最后的 call 方法定义了计算本身。对于密集层,它包括将输入与权重相乘并添加偏置。

TF 池化层

tf.keras.layers 提供不同类型的池化层:平均池化、最大池化、全局平均池化和全局最大池化层,用于一维时间数据、二维或三维空间数据。在本节中,我们将展示二维最大池化和平均池化层:

tf.keras.layers.MaxPool2D(
   pool_size=(2, 2), strides=None, padding='valid', data_format=None,
   kwargs)
tf.keras.layers.AveragePooling2D(
   pool_size=(2, 2), strides=None, padding='valid', data_format=None,
   kwargs)

这两个层都使用 pool_size 参数,定义窗口的大小。strides 参数用于定义窗口在池化操作中如何移动。

TF 标准化层

在下面的示例中,我们演示了一个批量标准化层,tf.keras.layers.BatchNormalization

tf.keras.layers.BatchNormalization(
   axis=-1, momentum=0.99, epsilon=0.001, center=True, scale=True,
   beta_initializer='zeros', gamma_initializer='ones',
   moving_mean_initializer='zeros',
   moving_variance_initializer='ones', beta_regularizer=None,
   gamma_regularizer=None, beta_constraint=None, gamma_constraint=None, **kwargs)

该层的输出将具有接近 0 的均值和接近 1 的标准差。有关每个参数的详细信息,请查看 www.tensorflow.org/api_docs/python/tf/keras/layers/BatchNormalization

TF dropout 层

Tf.keras.layers.Dropout 层应用了 dropout,这是一种将随机选择的数值设为零的正则化方法:

tf.keras.layers.Dropout(rate, noise_shape=None, seed=None, **kwargs)

在上述层的实例化中,rate 参数是一个介于 01 之间的浮点值,确定将被丢弃的输入单元的分数。

TF 卷积层

tf.keras.layers 提供了各种卷积层的实现,包括 tf.keras.layers.Conv1Dtf.keras.layers.Conv2Dtf.keras.layers.Conv3D,以及相应的转置卷积层(反卷积层) tf.keras.layers.Conv1DTransposetf.keras.layers.Conv2DTransposetf.keras.layers.Conv3DTranspose

以下代码片段描述了二维卷积层的实例化:

tf.keras.layers.Conv2D(
   filters, kernel_size, strides=(1, 1), padding='valid',
   data_format=None, dilation_rate=(1, 1), groups=1,
   activation=None, use_bias=True,
   kernel_initializer='glorot_uniform',
   bias_initializer='zeros', kernel_regularizer=None,
   bias_regularizer=None, activity_regularizer=None,
   kernel_constraint=None, bias_constraint=None, **kwargs)

在上述层定义中的主要参数是 filterskernel_sizefilters 参数定义了输出的维度,kernel_size 参数定义了二维卷积窗口的大小。有关其他参数,请查看 www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D

TF 循环层

在 Keras 中实现了以下一系列的循环层:LSTM 层,GRU 层,SimpleRNN 层,TimeDistributed 层,Bidirectional 层,ConvLSTM2D 层和 Base RNN 层。

在下面的代码片段中,我们展示了如何实例化 BidirectionalLSTM 层:

model = Sequential()
model.add(Bidirectional(LSTM(10, return_sequences=True), input_shape=(5, 10)))
model.add(Bidirectional(LSTM(10)))
model.add(Dense(5))
model.add(Activation('softmax'))

在上面的例子中,LSTM 层通过 Bidirectional 封装器进行了修改,以向两个隐藏层的两个副本提供初始序列和反向序列。这两层的输出被合并以得到最终的输出。默认情况下,输出是连接的,但是 merge_mode 参数允许我们选择不同的合并选项。输出空间的维度由第一个参数定义。要在每个时间步访问每个输入的隐藏状态,可以启用 return_sequences。更多细节,请查看 www.tensorflow.org/api_docs/python/tf/keras/layers/LSTM

TF 模型训练

对于 Keras 模型,只需在调用带有优化器和损失函数的 compile 函数后,通过在模型上调用 fit 函数即可完成模型训练。fit 函数使用提供的数据集进行指定轮数的训练。

以下代码片段描述了 fit 函数的参数:

model.fit(
   x=None, y=None, batch_size=None, epochs=1,
   verbose='auto', callbacks=None, validation_split=0.0,
   validation_data=None, shuffle=True,
   class_weight=None, sample_weight=None, 
   initial_epoch=0, steps_per_epoch=None,
   validation_steps=None, validation_batch_size=None,
   validation_freq=1, max_queue_size=10, workers=1,
   use_multiprocessing=False)

xy表示输入张量和标签。它们可以以多种格式提供:NumPy 数组、TF 张量、TF 数据集、生成器或tf.keras.utils.experimental.DatasetCreator。除了fit,Keras 模型还具有train_on_batch函数,仅在单个数据批次上执行梯度更新。

在 TF 版本 1 中,训练循环需要计算图编译,而 TF 版本 2 允许我们在不进行任何编译的情况下定义训练逻辑,就像 PyTorch 一样。典型的训练循环如下所示:

Optimizer = tf.keras.optimizers.Adam()
loss_fn = tf.keras.losses.CategoricalCrossentropy()
train_acc_metric = tf.keras.metrics.CategoricalAccuracy()
for epoch in range(epochs):
   for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
       with tf.GradientTape() as tape:
          logits = model(x_batch_train, training=True)
          loss_value = loss_fn(y_batch_train, logits)
       grads = tape.gradient(loss_value, model.trainable_weights)
       optimizer.apply_gradients(zip(grads, model.trainable_weights))
       train_acc_metric.update_state(y, logits)

在上述代码片段中,外部循环遍历各个 epoch,内部循环遍历训练集。前向传播和损失计算在GradientTape的范围内,该范围记录每个批次的自动微分操作。在范围外,优化器使用计算出的梯度更新权重。在上述示例中,TF 函数立即执行操作,而不是像急切执行中那样将操作添加到计算图中。我们想提到的是,如果您使用的是 TF 版本 1,那么需要使用@tf.function装饰器,因为那里需要显式构建计算图。

接下来,我们将研究 TF 中的损失函数。

TF 损失函数

在 TF 中,当模型编译时需要指定损失函数。虽然您可以从头开始构建自定义损失函数,但您可以通过tf.keras.losses模块提供的预定义损失函数来使用 Keras 提供的损失函数。以下示例演示了如何使用 Keras 的损失函数来编译模型:

model.compile(loss=tf.keras.losses.BinaryFocalCrossentropy(gamma=2.0, from_logits=True), ...)

此外,您可以将字符串别名传递给损失参数,如以下代码片段所示:

model.compile(loss='sparse_categorical_crossentropy', ...)

在本节中,我们将解释如何在 TF 中实例化PyTorch 损失函数部分中描述的损失函数。

TF MSE / L2 损失函数

MSE / L2 损失函数可以定义如下(www.tensorflow.org/api_docs/python/tf/keras/losses/MeanSquaredError):

mse = tf.keras.losses.MeanSquaredError()

这是回归中最常用的损失函数,它计算标签和预测之间平方差的平均值。默认设置将计算 MSE。然而,类似于 PyTorch 的实现,我们可以提供一个reduction参数来改变这种行为。例如,如果您希望应用sum操作而不是平均操作,您可以在损失函数中添加reduction=tf.keras.losses.Reduction.SUM。鉴于 PyTorch 中的torch.nn.MSELoss返回原始的平方差,您可以通过将reduction=tf.keras.losses.Reduction.NONE传递给构造函数,在 TF 中获得相同的损失。

接下来,我们将研究 MAE 损失。

TF MAE / L1 损失函数

tf.keras.losses.MeanAbsoluteError是 Keras 中用于 MAE 损失的函数(www.tensorflow.org/api_docs/python/tf/keras/losses/MeanAbsoluteError):

mae = tf.keras.losses.MeanAbsoluteError()

正如其名称所示,此损失计算真实值和预测值之间的绝对差的平均值。它还有一个reduction参数,可以像tf.keras.losses.MeanSquaredError中描述的那样使用。

现在,让我们来看看分类损失,交叉熵损失。

TF 交叉熵损失函数

交叉熵损失计算两个概率分布之间的差异。Keras 提供了tf.keras.losses.CategoricalCrossentropy类,专门用于多类分类(www.tensorflow.org/api_docs/python/tf/keras/losses/CategoricalCrossentropy)。以下代码片段展示了其实例化:

cce = tf.keras.losses.CategoricalCrossentropy()

在 Keras 的情况下,标签需要格式化为独热向量。例如,当目标类是五类中的第一类时,它会是[1, 0, 0, 0, 0]

用于二元分类的交叉熵损失,BCE 损失,也存在。

TF 二元交叉熵损失函数

在二元分类的情况下,标签是01。专门为二元分类设计的损失函数,BCE 损失,可以定义如下(www.tensorflow.org/api_docs/python/tf/keras/losses/BinaryFocalCrossentropy):

loss = tf.keras.losses.BinaryFocalCrossentropy(from_logits=True)

此损失的关键参数是from_logits。当此标志设置为False时,我们需要提供概率,即介于01之间的连续值。当设置为True时,我们需要提供 logits,即介于-无穷大+无穷大之间的值。

最后,让我们看看如何在 TF 中定义自定义损失。

TF 自定义损失函数

要构建一个自定义损失函数,我们需要创建一个接受预测值和标签作为参数并执行所需计算的函数。虽然 TF 的语法只期望这两个参数,但我们可以通过将函数包装到另一个返回损失的函数中来添加一些额外的参数。以下示例展示了如何创建 Huber Loss 作为自定义损失函数:

def custom_huber_loss(threshold=1.0):
   def huber_fn(y_true, y_pred):
       error = y_true - y_pred
       is_small_error = tf.abs(error) < threshold
       squared_loss = tf.square(error) / 2
       linear_loss = threshold * tf.abs(error) - threshold**2 / 2
       return tf.where(is_small_error, squared_loss, linear_loss)
   return huber_fn
model.compile(loss=custom_huber_loss (2.0), optimizer="adam"

另一种选择是创建一个继承tf.keras.losses.Loss类的类。在这种情况下,我们需要实现__init__call方法,如下所示:

class CustomLoss(tf.keras.losses.Loss):
   def __init__(self, threshold=1.0):
      super().__init__()
      self.threshold = threshold
   def call(self, y_true, y_pred):
      error = y_true - y_pred 
      is_small_error = tf.abs(error) < threshold
      squared_loss = tf.square(error) / 2 
      linear_loss = threshold*tf.abs(error) - threshold**2 / 2 
      return tf.where(is_small_error, squared_loss, linear_loss)
model.compile(optimizer="adam", loss=CustomLoss(),

要使用这个损失类,您必须实例化它,并通过一个loss参数将其传递给compile函数,就像本节开头所描述的那样。

TF 优化器

在本节中,我们将描述如何在 TF 中设置不同的优化器来进行模型训练。与前一节中的损失函数类似,Keras 通过tf.keras.optimizers提供了一组优化器。在各种优化器中,我们将在接下来的章节中看到两个主要的优化器,SGD 和 Adam 优化器。

TF SGD 优化器

设计为固定学习率的 SGD 优化器是许多模型中最典型的优化器。以下代码片段描述了如何在 TF 中实例化 SGD 优化器:

tf.keras.optimizers.SGD(
   learning_rate=0.01,
   momentum=0.0,
   nesterov=False,
   name='SGD',
   kwargs)

类似于 PyTorch 的实现,tf.keras.optimizers.SGD还支持使用momentumnesterov参数的增强型 SGD 优化器。

TF Adam 优化器

如“模型训练逻辑”部分所述,Adam 优化器采用自适应学习率。在 TF 中,可以按以下方式实例化:

tf.keras.optimizers.Adam(
   learning_rate=0.001, beta_1=0.9, beta_2=0.999,
   epsilon=1e-07, amsgrad=False, name='Adam', **kwargs)

对于这两种优化器,虽然learning_rate在定义初始学习率时起着最重要的作用,但我们建议您查阅官方文档,以熟悉其他参数:www.tensorflow.org/api_docs/python/tf/keras/optimizers

TF 回调函数

在本节中,我们想简要描述回调函数。这些是在训练的各个阶段执行特定操作的对象。最常用的回调函数是EarlyStoppingModelCheckpointTensorBoard,它们分别在满足特定条件时停止训练、在每个 epoch 后保存模型并可视化训练状态。

这里是一个监控验证损失并在监控的损失停止减少时停止训练的EarlyStopping回调的示例:

tf.keras.callbacks.EarlyStopping(
   monitor='val_loss', min_delta=0.1, patience=2, 
   verbose=0, mode='min', baseline=None, 
   restore_best_weights=False)

min_delta参数定义了被监控数量的最小变化量,以便将变化视为改进,而patience参数定义了在训练停止之前经过的没有改进的 epoch 数量。

通过继承keras.callbacks.Callback可以构建自定义回调函数。通过覆盖其方法可以定义特定事件的逻辑,清晰地描述其绑定的事件:

  • on_train_begin

  • on_train_end

  • on_epoch_begin

  • on_epoch_end

  • on_test_begin

  • on_test_end

  • on_predict_begin

  • on_predict_end

  • on_train_batch_begin

  • on_train_batch_end

  • on_predict_batch_begin

  • on_predict_batch_end

  • on_test_batch_begin

  • 或者on_test_batch_end

对于完整的详细信息,建议您查看www.tensorflow.org/api_docs/python/tf/keras/callbacks/Callback

要记住的事项

a. tf.data允许您构建高效的数据加载逻辑。诸如tfdstensorflow addons或 TF I/O 等包可以用于读取不同格式的数据。

b. TF 通过 Keras 支持三种不同的模型构建方法:顺序模型、函数式模型和子类化模型。

c. 为了简化使用 TF 进行模型开发,tf.keras.layers模块提供了各种层的实现,tf.keras.losses模块包含不同的损失函数,tf.keras.optimizers模块提供了一组标准优化器。

d. Callbacks可以用于在训练的各个阶段执行特定操作。常用的回调包括EarlyStoppingModelCheckpoint

到目前为止,我们已经学习了如何使用最流行的深度学习框架 PyTorch 和 TF 设置 DL 模型训练。在接下来的部分中,我们将探讨我们在本节描述的组件在实际中如何使用。

分解复杂的最新模型实现

即使您已掌握了 TF 和 PyTorch 的基础知识,从头开始设置模型训练可能会让人感到不知所措。幸运的是,这两个框架都有详细的文档和易于跟随的教程:

在这一部分中,我们将介绍一个更复杂的模型,StyleGAN。我们的主要目标是解释如何将前面描述的组件组合起来用于复杂的深度学习项目。关于模型架构和性能的完整描述,我们建议参阅由 NVIDIA 发布的文章,网址为ieeexplore.ieee.org/document/8953766

StyleGAN

StyleGAN 作为生成对抗网络GAN)的一个变体,旨在从潜在代码(随机噪声向量)生成新的图像。其架构可以分解为三个元素:映射网络、生成器和鉴别器。在高层次上,映射网络和生成器共同作用,从一组随机值生成图像。鉴别器在训练过程中发挥了指导生成器生成逼真图像的关键作用。让我们更详细地看看每个组件。

映射网络和生成器

在传统的 GAN 中,生成器设计为直接处理潜在代码,而在 StyleGAN 中,潜在代码首先被馈送到映射网络,如 图 3.5 所示。映射网络的输出然后被馈送到生成器的每个步骤,改变生成图像的风格和细节。生成器从较低分辨率开始,以 4 x 4 或 8 x 8 的张量尺寸构建图像的轮廓。随着生成器处理更大的张量,图像的细节被填充。在最后几层,生成器与 64 x 64 和 1024 x 1024 尺寸的张量交互,构建高分辨率特征:

图 3.5 – StyleGAN 的映射网络(左)和生成器(右)

图 3.5 – StyleGAN 的映射网络(左)和生成器(右)

在上述图中,接受潜在向量 z 并生成 w 的网络是映射网络。右侧的网络是生成器 g,它接受一组噪声向量以及 w。与生成器相比,鉴别器相对简单。图层显示在 图 3.6 中:

图 3.6 – StyleGAN 在 FFHQ 数据集上的 1024 × 1024 分辨率的鉴别器架构

图 3.6 – StyleGAN 在 FFHQ 数据集上的 1024 × 1024 分辨率的鉴别器架构

如前所示的图像所示,鉴别器由多个卷积层块和下采样操作组成。它接收大小为 1024 x 1024 的图像并生成介于 01 之间的数值,描述图像的真实性。

训练 StyleGAN

训练 StyleGAN 需要大量计算,因此需要多个 GPU 才能达到合理的训练时间。估算结果总结在 图 3.7 中:

图 3.7 – 使用 Tesla V100 GPU 训练 StyleGAN 的训练时间

图 3.7 – 使用 Tesla V100 GPU 训练 StyleGAN 的训练时间

因此,如果您想尝试使用 StyleGAN,我们建议按照官方 GitHub 存储库中提供的预训练模型的说明进行操作:github.com/NVlabs/stylegan

在 PyTorch 中的实现

不幸的是,NVIDIA 尚未分享 StyleGAN 在 PyTorch 中的公共实现。相反,他们发布了 StyleGAN2,它与大多数相同组件共享。因此,我们将使用 StyleGAN2 实现作为我们的 PyTorch 示例:github.com/NVlabs/stylegan2-ada-pytorch

所有网络组件都位于 training/network.py 下。三个组件的命名如前所述:MappingNetworkGeneratorDiscriminator

PyTorch 中的映射网络

MappingNetwork 的实现是不言自明的。以下代码片段包含映射网络的核心逻辑:

class MappingNetwork(torch.nn.Module):
   def __init__(self, ...):
       ...
       for idx in range(num_layers):
          in_features = features_list[idx]
          out_features = features_list[idx + 1]
          layer = FullyConnectedLayer(in_features, out_features, activation=activation, lr_multiplier= lr_multiplier) setattr(self, f'fc{idx}', layer)

   def forward(self, z, ...):
       # Embed, normalize, and concat inputs.
       x = normalize_2nd_moment(z.to(torch.float32))

       # Main layers
       for idx in range(self.num_layers):
          layer = getattr(self, f'fc{idx}')
          x = layer(x)
       return x

在这个网络定义中,MappingNetwork也继承了torch.nn.Module。在__init__函数中初始化了必要的FullyConnectedLayer实例。forward方法将潜在向量z传递给每一层。

PyTorch 中的生成器

以下代码片段描述了生成器的实现方式。它包括MappingNetworkSynthesisNetwork,如图 3.5所示:

class Generator(torch.nn.Module):
   def __init__(self, …):
       self.z_dim = z_dim
       self.c_dim = c_dim
       self.w_dim = w_dim
       self.img_resolution = img_resolution
       self.img_channels = img_channels
       self.synthesis = SynthesisNetwork(
          w_dim=w_dim, 
          img_resolution=img_resolution,
          img_channels=img_channels,
          synthesis_kwargs)
       self.num_ws = self.synthesis.num_ws
       self.mapping = MappingNetwork(
          z_dim=z_dim, c_dim=c_dim, w_dim=w_dim,
          num_ws=self.num_ws, **mapping_kwargs)
   def forward(self, z, c, truncation_psi=1, truncation_cutoff=None, **synthesis_kwargs):
       ws = self.mapping(z, c, 
       truncation_psi=truncation_psi, 
       truncation_cutoff=truncation_cutoff)
       img = self.synthesis(ws, **synthesis_kwargs)
       return img

生成器网络Generator也继承了torch.nn.ModuleSynthesisNetworkMappingNetwork__init__函数中被实例化,并在forward函数中按顺序触发。SynthesisNetwork的实现总结如下代码片段:

class SynthesisNetwork(torch.nn.Module):
   def __init__(self, ...):
       for res in self.block_resolutions:
          block = SynthesisBlock(
             in_channels, out_channels, w_dim=w_dim,
             resolution=res, img_channels=img_channels,
             is_last=is_last, use_fp16=use_fp16,
             block_kwargs)
          setattr(self, f'b{res}', block)
       ...
   def forward(self, ws, **block_kwargs):
       ...
       x = img = None
       for res, cur_ws in zip(self.block_resolutions, block_ws):
          block = getattr(self, f'b{res}')
          x, img = block(x, img, cur_ws, **block_kwargs)
       return img

SynthesisNetwork包含多个SynthesisBlockSynthesisBlock接收噪声向量和MappingNetwork的输出,生成最终成为输出图像的张量。

PyTorch 中的判别器

以下代码片段总结了Discriminator的 PyTorch 实现。网络架构遵循图 3.6中描述的结构:

class Discriminator(torch.nn.Module):
   def __init__(self, ...):
       self.block_resolutions = [2 ** i for i in range(self.img_resolution_log2, 2, -1)]
       for res in self.block_resolutions:
          block = DiscriminatorBlock(
              in_channels, tmp_channels, out_channels,
              resolution=res,
              first_layer_idx = cur_layer_idx,
              use_fp16=use_fp16, **block_kwargs, 
              common_kwargs)
          setattr(self, f'b{res}', block)
   def forward(self, img, c, **block_kwargs):
       x = None
       for res in self.block_resolutions:
          block = getattr(self, f'b{res}')
          x, img = block(x, img, **block_kwargs)
       return x

类似于SynthesisNetworkDiscriminator利用DiscriminatorBlock类动态创建一组不同尺寸的卷积层。它们在__init__函数中定义,并且张量在forward函数中按顺序传递给每个块。

PyTorch 中的模型训练逻辑

训练逻辑在training/train_loop.pytraining_loop函数中定义。原始实现包含许多细节。在以下代码片段中,我们将查看与PyTorch 模型训练部分所学内容相符的主要组件:

def training_loop(...):
   ...
training_set_iterator = iter(torch.utils.data.DataLoader(dataset=training_set, sampler=training_set_sampler, batch_size=batch_size//num_gpus, **data_loader_kwargs))
   loss = dnnlib.util.construct_class_by_name(device=device, **ddp_modules, **loss_kwargs) # subclass of training.loss.Loss
   while True:
      # Fetch training data.
      with torch.autograd.profiler.record_function('data_fetch'):
         phase_real_img, phase_real_c = next(training_set_iterator)
      # Execute training phases.
      for phase, phase_gen_z, phase_gen_c in zip(phases, all_gen_z, all_gen_c):
         # Accumulate gradients over multiple rounds.
      for round_idx, (real_img, real_c, gen_z, gen_c) in enumerate(zip(phase_real_img, phase_real_c, phase_gen_z, phase_gen_c)):
         loss.accumulate_gradients(phase=phase.name, real_img=real_img, real_c=real_c, gen_z=gen_z, gen_c=gen_c, sync=sync, gain=gain)
      # Update weights.
      phase.module.requires_grad_(False)
      with torch.autograd.profiler.record_function(phase.name + '_opt'):
         phase.opt.step()

该函数接收各种训练组件的配置,并训练GeneratorDiscriminator。外部循环迭代训练样本,内部循环处理梯度计算和模型参数更新。训练设置由单独的脚本main/train.py配置。

这总结了 PyTorch 实现的结构。尽管存储库由于大量文件而显得庞大,我们已经指导您如何将实现分解为在 PyTorch 中实现和训练模型部分所描述的组件。在接下来的部分中,我们将查看 TF 中的实现。

TF 中的实现

即使官方实现是基于 TF 的(github.com/NVlabs/stylegan),我们将看一个不同的实现,该实现出现在Hands-On Image Generation with TensorFlow: A Practical Guide to Generating Images and Videos Using Deep Learning by Soon Yau Cheong。这个版本基于 TF 版本 2,更符合我们在本书中描述的内容。该实现可以在以下链接找到:github.com/PacktPublishing/Hands-On-Image-Generation-with-TensorFlow-2.0/blob/master/Chapter07/ch7_faster_stylegan.ipynb

类似于前一节中描述的 PyTorch 实现,原始的 TF 实现包括 G_mapping 作为映射网络,G_style 作为生成器,以及 D_basic 作为判别器。

TF 中的映射网络

让我们看一下在以下链接定义的映射网络:github.com/NVlabs/stylegan/blob/1e0d5c781384ef12b50ef20a62fee5d78b38e88f/training/networks_stylegan.py#L384,以及其 TF 版本 2 的实现如下所示:

def Mapping(num_stages, input_shape=512):
   z = Input(shape=(input_shape))
   w = PixelNorm()(z)
   for i in range(8):
      w = DenseBlock(512, lrmul=0.01)(w)
      w = LeakyReLU(0.2)(w)
      w = tf.tile(tf.expand_dims(w, 1), (1,num_stages,1))
   return Model(z, w, name='mapping') 

MappingNetwork 的实现几乎是不言自明的。我们可以看到映射网络从潜在向量 z 构建出向量 w,使用了一个 PixelNorm 自定义层。该自定义层定义如下:

class PixelNorm(Layer):
   def __init__(self, epsilon=1e-8):
      super(PixelNorm, self).__init__()
      self.epsilon = epsilon                
   def call(self, input_tensor):
      return input_tensor / tf.math.sqrt(tf.reduce_mean(input_tensor**2, axis=-1, keepdims=True) + self.epsilon)

正如TF dense (linear) layers部分所述,PixelNorm 继承了 tensorflow.keras.layers.Layer 类,并在 call 函数中定义计算。

Mapping 的其余组件是一组具有 LeakyReLU 激活函数的稠密层。

接下来,我们将看一下生成器网络。

TF 中的生成器

原始代码中的生成器 G_style 由两个网络组成:G_mappingG_synthesis。参见以下链接:github.com/NVlabs/stylegan/blob/1e0d5c781384ef12b50ef20a62fee5d78b38e88f/training/networks_stylegan.py#L299

从仓库中获取的完整实现一开始可能看起来非常复杂。然而,你很快会发现 G_style 只是依次调用 G_mappingG_synthesis

SynthesisNetwork 的实现总结在以下代码片段中:github.com/NVlabs/stylegan/blob/1e0d5c781384ef12b50ef20a62fee5d78b38e88f/training/networks_stylegan.py#L440

在 TF 版本 2 中,生成器的实现如下:

def GenBlock(filter_num, res, input_shape, is_base):
   input_tensor = Input(shape=input_shape, name=f'g_{res}')
   noise = Input(shape=(res, res, 1), name=f'noise_{res}')
   w = Input(shape=512)
   x = input_tensor
   if not is_base:
      x = UpSampling2D((2,2))(x)
      x = ConvBlock(filter_num, 3)(x)
   x = AddNoise()([x, noise])
   x = LeakyReLU(0.2)(x)
   x = InstanceNormalization()(x)
   x = AdaIN()([x, w])
   # Adding noise
   x = ConvBlock(filter_num, 3)(x)
   x = AddNoise()([x, noise])
   x = LeakyReLU(0.2)(x)
   x = InstanceNormalization()(x)                    
   x = AdaIN()([x, w])
   return Model([input_tensor, w, noise], x, name=f'genblock_{res}x{res}')

这个网络遵循了图 3.5中描述的架构;SynthesisNetwork由一组自定义层 AdaInConvBlock 构成。

让我们继续看鉴别器网络。

TF 中的鉴别器

函数 D_basic 实现了 图 3.6 中描述的鉴别器。(github.com/NVlabs/stylegan/blob/1e0d5c781384ef12b50ef20a62fee5d78b38e88f/training/networks_stylegan.py#L562)。由于鉴别器由一组卷积层块组成,D_basic 有一个专用函数 block,根据输入张量大小构建块。函数的核心组件如下:

def block(x, res): # res = 2 … resolution_log2
   with tf.variable_scope('%dx%d' % (2**res, 2**res)):
       x = act(apply_bias(conv2d(x, fmaps=nf(res-1), kernel=3, gain=gain, use_wscale=use_wscale)))
       x = act(apply_bias(conv2d_downscale2d(blur(x), fmaps=nf(res-2), kernel=3, gain=gain, use_wscale=use_wscale, fused_scale=fused_scale)))
   return x

在上述代码中,block 函数处理通过结合卷积和下采样层创建鉴别器中的每个块。D_basic 的剩余逻辑很简单,因为它只是通过将一个块的输出作为下一个块的输入来链式连接一组卷积层块。

TF 中的模型训练逻辑

TF 实现的训练逻辑可以在 train_step 函数中找到。理解实现细节不应该是个难题,因为它们遵循了我们在 TF 模型训练 部分的描述。

总的来说,我们学习了如何在 TF 版本 2 中使用我们在本章中描述的 TF 构建块实现 StyleGAN。

要记住的事情

a. 无论实现的复杂性如何,任何 DL 模型训练实现都可以分解为三个组件(数据加载逻辑、模型定义和模型训练逻辑)。

在这个阶段,您应该了解 StyleGAN 仓库在每个框架中的结构。我们强烈建议您玩弄预训练模型以生成有趣的图像。如果掌握了 StyleGAN,那么跟随 StyleGAN2 (arxiv.org/abs/1912.04958)、StyleGAN3 (arxiv.org/abs/2106.12423) 和 HyperStyle (arxiv.org/abs/2111.15666) 的实现就会很容易。

总结

在本章中,我们探讨了 DL 的灵活性来自何处。DL 使用数学神经元网络来学习数据集中的隐藏模式。训练网络涉及根据训练集更新模型参数的迭代过程,并选择在验证集上表现最佳的模型,以期在测试集上实现最佳性能。

在模型训练中实现重复过程时,许多工程师和研究人员将常见的构建块整合到框架中。我们描述了两个最流行的框架:PyTorch 和 TF。这两个框架的结构方式相似,允许用户使用三个构建块设置模型训练:数据加载逻辑、模型定义和模型训练逻辑。作为本章的最后一个主题,我们分解了 StyleGAN,这是最流行的 GAN 实现之一,以了解这些构建块在实际中的使用方式。

由于深度学习需要大量数据进行成功训练,高效管理数据、模型实现以及各种训练结果对于任何项目的成功至关重要。在接下来的章节中,我们将介绍用于深度学习实验监控的有用工具。

第四章:实验跟踪,模型管理和数据集版本控制

在本章中,我们将介绍一组用于实验跟踪、模型管理和数据集版本控制的实用工具,这些工具可以帮助您有效管理深度学习(DL)项目。本章讨论的工具可以帮助我们跟踪许多实验并更高效地解释结果,从而自然而然地减少运营成本并加速开发周期。通过本章,您将能够亲自体验最流行的工具,并能够为您的项目选择适当的工具集。

在本章中,我们将涵盖以下主要内容:

  • DL 项目跟踪概述

  • 使用 Weights & Biases 进行 DL 项目跟踪

  • 使用 MLflow 和 DVC 进行 DL 项目跟踪

  • 数据集版本控制 – 超越 Weights & Biases, MLflow 和 DVC

技术要求

您可以从本书的 GitHub 仓库下载本章的补充材料,网址为github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_4

DL 项目跟踪概述

训练 DL 模型是一个消耗大量时间和资源的迭代过程。因此,跟踪所有实验并始终有条理地组织它们可以防止我们浪费时间在无需重复训练相似模型的操作上。换句话说,有关所有模型架构及其超参数集合以及实验过程中使用的数据版本的详细记录可以帮助我们从实验中得出正确的结论,这自然而然地有助于项目的成功。

DL 项目跟踪的组成部分

DL 项目跟踪的基本组成部分包括实验跟踪,模型管理和数据集版本控制。让我们详细看看每个组件。

实验跟踪

实验跟踪背后的概念很简单:存储每个实验的描述和动机,以便我们不为了相同目的再次运行一组实验。总体而言,有效的实验跟踪将节省我们运营成本,并允许我们从最小的实验结果集中得出正确的结论。有效实验跟踪的基本方法之一是为每个实验添加唯一标识符。我们需要跟踪每个实验的信息包括项目依赖关系、模型架构定义、使用的参数和评估指标。实验跟踪还包括实时可视化进行中的实验,并能够直观地比较一组实验。例如,如果我们可以检查模型训练过程中每个时期的训练和验证损失,我们可以更快地识别过拟合,从而节省一些资源。此外,通过比较两个实验之间的结果和一组变化,我们可以理解这些变化如何影响模型性能。

模型管理

模型管理不仅仅涵盖实验跟踪,还涵盖了模型的整个生命周期:数据集信息、工件(从训练模型生成的任何数据)、模型的实施、评估指标以及管道信息(如开发、测试、暂存和生产)。模型管理使我们能够快速选择感兴趣的模型,并有效地设置模型可用的环境。

数据集版本控制

DL 项目跟踪的最后一个组成部分是数据集版本控制。在许多项目中,数据集会随时间改变。这些变化可能来自数据模式(数据组织的蓝图)、文件位置,甚至是应用于数据集的筛选器,从而操作底层数据的含义。工业中发现的许多数据集结构复杂,并且通常以多种数据格式存储在多个位置。因此,变化可能比预期的更加显著和难以跟踪。因此,在记录这些变化方面至关重要,以在整个项目中复制一致的结果。

数据集跟踪可以总结如下:将存储为工件的一组数据,在基础数据修改时应成为工件的新版本。话虽如此,每个工件应具有元数据,其中包含关于数据集的重要信息:创建时间、创建者以及与上一版本的差异。

例如,应该如下制定具有数据集版本控制的数据集。数据集应在其名称中具有时间戳:

dataset_<timestamp>
> metadata.json
> img1.png
> img2.png
> img3.png

正如前面所述,元数据应包含关于数据集的关键信息:

{
   "created_by": "Adam"
   "created_on": "2022-01-01"
   "labelled_by": "Bob"
   "number_of_samples": 3
}

请注意,由元数据跟踪的信息集可能因项目而异。

DL 项目跟踪工具

DL 跟踪可以通过多种方式实现,从简单的文本文件中的注释、电子表格,到在 GitHub 或专用网页上保存信息,再到自建平台和外部工具。模型和数据工件可以按原样存储,也可以应用更复杂的方法以避免冗余并提高效率。

DL 项目跟踪领域正在快速增长,并不断引入新工具。因此,为底层项目选择合适的工具并不容易。我们必须考虑业务和技术限制。尽管定价模型是基本的,但其他限制可能由现有的开发设置引入;集成现有工具应该很容易,基础设施必须易于维护。还要考虑 MLOps 团队的工程能力。话虽如此,在选择项目工具时,以下列表将是一个很好的起点。

  • TensorBoard (www.tensorflow.org/tensorboard):

    • TensorFlow 团队开发的一款开源可视化工具。

    • 用于跟踪和可视化实验结果的标准工具。

  • Weights & Biases (wandb.ai):

    • 一款云端服务,提供有效的交互式仪表板,用于可视化和组织实验结果。

    • 服务器可以在本地运行,也可以托管在私有云中。

    • 提供名为 Sweeps 的自动化超参数调整功能。

    • 个人项目免费。定价基于跟踪小时数和存储空间。

  • Neptune (neptune.ai):

    • 用于监视和存储机器学习(ML)实验结果的在线工具。

    • 可轻松集成其他 ML 工具。

    • 以其强大的仪表板而闻名,实时总结实验结果。

  • MLflow (mlflow.org):

    • 一个提供端到端 ML 生命周期管理的开源平台。

    • 它支持基于 Python 和 R 的系统。通常与数据版本控制DVC)结合使用。

  • SageMaker Studio (aws.amazon.com/sagemaker/studio/):

    • 一个基于 Web 的可视化界面,用于管理与 SageMaker 设置的 ML 实验。

    • 该工具允许用户通过提供与 AWS 的其他有用功能简单集成,高效地构建、训练和部署模型。

  • Kubeflow (www.kubeflow.org):

    • 由 Google 设计的开源平台,用于端到端 ML 编排和管理。

    • 也设计用于将 ML 系统高效地部署到各种开发和生产环境中。

  • Valohai (valohai.com):

    • 一款专为自动化机器编排、版本控制和数据管道管理而设计的 DL 管理平台。

    • 它并非免费软件,而是专为企业设计的

    • 它因技术不限和响应迅速的支持团队而日益受欢迎

在各种工具中,我们将介绍两种最常用的设置:Weights & Biases 和 MLflow 结合 DVC。

需记住的事项

a. DL 跟踪的基本组件包括实验跟踪、模型管理和数据集版本控制。近期的 DL 跟踪工具通常具有用户友好的仪表板,总结实验结果。

b. 该领域正在快速发展,并且有许多具有不同优势的工具。选择合适的工具需要理解业务和技术约束。

首先,让我们看看使用 Weights & Biases (W&B) 进行 DL 项目跟踪。

使用 Weights & Biases 进行 DL 项目跟踪

W&B 是一个实验管理平台,为模型和数据提供版本控制。

W&B 提供了一个交互式仪表板,可以嵌入到 Jupyter 笔记本中或用作独立的网页。简单的 Python API 也为简单集成打开了可能性。此外,其功能侧重于简化 DL 实验管理:记录和监控模型和数据版本、超参数值、评估指标、工件和其他相关信息。

W&B 的另一个有趣功能是其内置的超参数搜索功能称为 Sweeps (docs.wandb.ai/guides/sweeps)。可以使用 Python API 轻松设置 Sweeps,并在 W&B 网页上交互比较结果和模型。

最后,W&B 会自动为您创建报告,直观地总结和组织一组实验(docs.wandb.ai/guides/reports)。

总体而言,W&B 的关键功能可以总结如下:

  • 实验跟踪和管理

  • 工件管理

  • 模型评估

  • 模型优化

  • 协同分析

W&B 是一种订阅服务,但个人账户是免费的。

设置 W&B

W&B 拥有 Python API,为多种 DL 框架(包括 TensorFlow 和 PyTorch)提供简单的集成方法。记录的信息,如项目、团队和运行列表,可在线管理和查看,或者在自托管服务器上进行管理。

设置 W&B 的第一步是安装 Python API 并登录 W&B 服务器。您必须预先通过 wandb.ai 创建一个账户:

pip install wandb
wandb login

在您的 Python 代码中,您可以通过以下代码注册一个称为 run-1 的单个实验:

import wandb
run_1 = wandb.init(project="example-DL-Book", name="run-1") 

更具体地说,wandb.init 函数在名为 example-DL-Book 的项目中创建了一个名为 run_1 的新 wandb.Run 实例。如果未提供名称,W&B 将为您生成一个随机的双字名称。如果项目名称为空,W&B 将将您的运行放入 Uncategorized 项目中。wandb.init 的所有参数列在 docs.wandb.ai/ref/python/init,但我们想介绍您最常与之交互的参数:

  • id 为您的运行设置一个唯一的 ID

  • resume 允许您在不创建新运行的情况下恢复实验

  • job_type 允许您将运行分配给特定类型,例如训练、测试、验证、探索或任何其他可以用于分组运行的名称

  • tags 提供了额外的灵活性,用于组织您的运行

当触发 wandb.init 函数时,关于运行的信息将开始出现在 W&B 仪表板上。您可以在 W&B 网页或直接在 Jupyter 笔记本环境中监视仪表板,如下面的截图所示:

Figure 4.1 – Jupyter 笔记本环境中的 W&B 仪表板

图 4.1 – Jupyter 笔记本环境中的 W&B 仪表板

创建运行后,您可以开始记录信息;wandb.log 函数允许您记录任何您想要的数据。例如,您可以通过在训练循环中添加 wandb.log({"custom_loss": custom_loss}) 来记录损失。同样,您可以记录验证损失和任何其他您想要跟踪的详细信息。

有趣的是,W&B 通过为 DL 模型提供内置的日志记录功能,使这个过程变得更加简单。在撰写本文时,您可以找到大多数框架的集成,包括 Keras、PyTorch、PyTorch Lightning、TensorFlow、fast.ai、scikit-learn、SageMaker、Kubeflow、Docker、Databricks 和 Ray Tune(有关详细信息,请参见 docs.wandb.ai/guides/integrations)。

wandb.config 是跟踪模型超参数的绝佳位置。对于来自实验的任何工件,您可以使用 wandb.log_artifact 方法(有关更多详细信息,请参见 docs.wandb.ai/guides/artifacts)。在记录工件时,您需要定义文件路径,然后指定您的工件的名称和类型,如以下代码片段所示:

wandb.log_artifact(file_path, name='new_artifact', type='my_dataset')

然后,您可以重复使用已存储的工件,如下所示:

run = wandb.init(project="example-DL-Book")
artifact = run.use_artifact('example-DL-Book/new_artifact:v0', type='my_dataset')
artifact_dir = artifact.download()

到目前为止,您已经学会了如何为您的项目设置 wandb 并在整个训练过程中单独记录您选择的指标和工件。有趣的是,wandb 为许多深度学习框架提供了自动日志记录。在本章中,我们将更详细地了解如何将 W&B 集成到 Keras 和 PyTorch Lightning (PL) 中。

将 W&B 集成到 Keras 项目中

在 Keras 的情况下,可以通过 WandbCallback 类实现集成。完整版本可以在本书的 GitHub 存储库中找到:

import wandb
from wandb.keras import WandbCallback
from tensorflow import keras
from tensorflow.keras import layers
wandb.init(project="example-DL-Book", name="run-1")
wandb.config = {
   "learning_rate": 0.001,
   "epochs": 50,
   "batch_size": 128
}
model = keras.Sequential()
logging_callback = WandbCallback(log_evaluation=True)
model.fit(
   x=x_train, y=y_train,
   epochs=wandb.config['epochs'],
   batch_size=wandb.config['batch_size'], 
   verbose='auto', 
   validation_data=(x_valid, y_valid),
   callbacks=[logging_callback])

如前一节所述,关于模型的关键信息被记录并在 W&B 仪表板上可用。您可以监视损失、评估指标和超参数。图 4.2 展示了由前述代码自动生成的示例图:

Figure 4.2 – Sample plots generated by W&B from logged metrics

Figure 4.2 – Sample plots generated by W&B from logged metrics

图 4.2 – W&B 根据记录的指标生成的示例图

将 W&B 集成到 PL 项目中类似于将其集成到 Keras 项目中。

将 W&B 集成到 PyTorch Lightning 项目中

对于基于 PL 的项目,W&B 提供了自定义记录器,并隐藏了大部分样板代码。您只需要实例化 WandbLogger 类,并通过 logger 参数将其传递给 Trainer 实例:

import pytorch_lightning as pl
from pytorch_lightning.loggers import WandbLogger
wandb_logger = WandbLogger(project="example-DL-Book")
trainer = Trainer(logger=wandb_logger)
class LitModule(LightningModule):
   def __init__(self, *args, **kwarg):
       self.save_hyperparameters()
   def training_step(self, batch, batch_idx):
       self.log("train/loss", loss)

关于集成的详细说明可在 pytorch-lightning.readthedocs.io/en/stable/extensions/generated/pytorch_lightning.loggers.WandbLogger.html 找到。

需记住的事项

a. W&B 是一个实验管理平台,有助于跟踪模型和数据的不同版本。它还支持存储配置、超参数、数据和模型工件,并提供实时实验跟踪。

b. W&B 安装简便。它为多个 DL 框架提供了内置的集成功能,包括 TensorFlow 和 PyTorch。

c. 可以使用 W&B 进行超参数调优/模型优化。

虽然 W&B 在 DL 项目跟踪领域占据主导地位,但 MLflow 和 DVC 的组合是另一种流行的 DL 项目设置。

使用 MLflow 和 DVC 进行 DL 项目跟踪

MLflow 是一个流行的框架,支持跟踪技术依赖项、模型参数、指标和工件。MLflow 的关键组件如下:

  • 跟踪:每次模型运行时跟踪结果变化

  • 项目:它以可重现的方式打包模型代码

  • 模型:它为未来便捷的部署组织模型工件

  • 模型注册表:它管理 MLflow 模型的完整生命周期

  • 插件:由于提供了灵活的插件,可以轻松集成其他 DL 框架

正如您可能已经注意到的那样,W&B 和 MLflow 之间存在一些相似之处。然而,在 MLflow 的情况下,每个实验都与一组 Git 提交关联。Git 并不会阻止我们保存数据集,但是在数据集较大时会显示出许多限制,即使使用了专为大文件构建的扩展(Git LFS)。因此,MLflow 通常与 DVC 结合使用,DVC 是一个解决 Git 限制的开源版本控制系统。

设置 MLflow

可以使用 pip 安装 MLflow:

pip install mlflow

类似于 W&B,MLflow 还提供了一个 Python API,允许您跟踪超参数(log_param)、评估指标(log_metric)和工件(log_artifacts):

import os
import mlflow
from mlflow import log_metric, log_param, log_artifacts
log_param("epochs", 30)
log_metric("custom", 0.6)
log_metric("custom", 0.75) # metrics can be updated
if not os.path.exists("artifact_dir"):
   os.makedirs("artifact_dir")
with open("artifact_dir/test.txt", "w") as f:
   f.write("simple example")
log_artifacts("artifact_dir")

实验定义可以通过以下代码初始化并标记:

exp_id = mlflow.create_experiment("DLBookModel_1")
exp = mlflow.get_experiment(exp_id)
with mlflow.start_run(experiment_id=exp.experiment_id, run_name='run_1') as run:
   # logging starts here
   mlflow.set_tag('model_name', 'model1_dev')

MLflow 提供了一系列教程,介绍了其 API:www.mlflow.org/docs/latest/tutorials-and-examples/tutorial.html

现在您已经熟悉了 MLflow 的基本用法,我们将描述如何将其集成到 Keras 和 PL 项目中。

将 MLflow 集成到 Keras 项目中

首先,让我们看一下 Keras 的集成。通过 log_model 函数可以记录 Keras 模型的详细信息:

history = keras_model.fit(...)
mlflow.keras.log_model(keras_model, model_dir)

mlflow.kerasmlflow.tensorflow 模块提供了一组 API,用于记录关于 Keras 和 TensorFlow 模型的各种信息。有关更多详细信息,请查看 www.mlflow.org/docs/latest/python_api/index.html

将 MLflow 集成到 PyTorch Lightning 项目中

类似于 W&B 支持 PL 项目的方式,MLflow 也提供了一个 MLFlowLogger 类。这可以传递给 Trainer 实例,用于在 MLflow 中记录模型详细信息:

import pytorch_lightning as pl 
from pytorch_lightning import Trainer
from pytorch_lightning.loggers import MLFlowLogger
mlf_logger = MLFlowLogger(experiment_name="example-DL-Book ", tracking_uri="file:./ml-runs")
trainer = Trainer(logger=mlf_logger)
class DLBookModel(pl.LightningModule):
   def __init__(self):
       super(DLBookModel, self).__init__()
       ...
   def training_step(self, batch, batch_nb):
       loss = self.log("train_loss", loss, on_epoch=True)

在上述代码中,我们传递了一个 MLFlowLogger 实例来替换 PL 的默认记录器。tracking_uri 参数控制记录的数据去向。

关于 PyTorch 集成的其他详细信息可以在官方网站找到:pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.loggers.mlflow.html

使用 DVC 设置 MLflow

要使用 DVC 管理大型数据集,需要使用诸如 pipcondabrew(适用于 macOS 用户)之类的包管理器安装它:

pip install dvc

所有的安装选项可以在 dvc.org/doc/install 找到。

使用 DVC 管理数据集需要按特定顺序执行一组命令:

  1. 第一步是使用 DVC 设置一个 Git 仓库:

    git init
    dvc init
    git commit -m 'initialize repo'
    
  2. 现在,我们需要配置 DVC 的远程存储:

    dvc remote add -d myremote /tmp/dvc-storage
    git commit .dvc/config -m "Added local remote storage"
    
  3. 让我们创建一个样本数据目录,并填充一些示例数据:

    mkdir data
    cp example_data.csv data/
    
  4. 在这个阶段,我们已经准备好开始跟踪数据集了。我们只需要将文件添加到 DVC 中。此操作将创建一个额外的文件 example_data.csv.dvc。此外,example_data.csv 文件会自动添加到 .gitignore 中,使得 Git 不再跟踪原始文件:

    dvc add data/example_data.csv
    
  5. 接下来,您需要提交并上传 example_data.csv.dvc.gitignore 文件。我们将我们的第一个数据集标记为 v1

    git add data/.gitignore data/example_data.csv.dvc
    git commit -m 'data tracking'
    git tag -a 'v1' -m 'test_data'
    dvc push
    
  6. 使用 dvc push 命令后,我们的数据将在远程存储中可用。这意味着我们可以删除本地版本。要恢复 example_data.csv,只需简单地调用 dvc pull

    dvc pull data/example_data.csv.dvc
    
  7. 当修改 example_data.csv 后,我们需要再次添加并推送以更新远程存储中的版本。我们将修改后的数据集标记为 v2

    dvc add data/example_data.csv
    git add data/example_data.csv.dvc
    git commit -m 'data modification description'
    git tag -a 'v2' -m 'modified test_data'
    dvc push
    

执行这些命令后,您将通过 Git 和 DVC 跟踪同一数据集的两个版本:v1v2

接下来,让我们看看如何将 MLflow 与 DVC 结合使用:

import mlflow
import dvc.api
import pandas as pd
data_path='data/example_data.csv'
repo='/Users/BookDL_demo/'
version='v2'
data_url=dvc.api.get_url(path=path, repo=repo, rev=version)
# this will fetch the right version of our data file
data = pd.read_csv(data_url)
# log important information using mlflow
mlflow.start_run()
mlflow.log_param("data_url", data_url)
mlflow.log_artifact(...)

在前面的代码片段中,使用了mlflow.log_artifact来保存有关实验特定列的信息。

总体而言,我们可以通过 MLflow 运行多个实验,使用 DVC 跟踪不同版本的数据集。与 W&B 类似,MLflow 还提供一个网页,我们可以在其中比较我们的实验。您只需在终端中输入以下命令即可:

mlflow ui 

这个命令将启动一个托管在127.0.0.1:5000上的 Web 服务器,以下屏幕截图显示了 MLflow 仪表板:

![图 4.3 – MLflow 仪表板;新运行将显示在页面底部

![图 4.3 – MLflow 仪表板;新运行将显示在页面底部

图 4.3 – MLflow 仪表板;新运行将显示在页面底部

记住的事情

a. MLflow 可以跟踪依赖关系、模型参数、指标和工件。通常与 DVC 结合使用,以实现高效的数据集版本控制。

b. MLflow 可以轻松集成到包括 Keras、TensorFlow 和 PyTorch 在内的 DL 框架中。

c. MLflow 提供交互式可视化,可以同时分析多个实验。

到目前为止,我们已经学习了如何在 W&B、MLflow 和 DVC 中管理 DL 项目。在下一节中,我们将介绍用于数据集版本控制的流行工具。

数据集版本控制 – 超越 Weights & Biases、MLflow 和 DVC

在本章中,我们看到了如何通过 DL 项目跟踪工具管理数据集。在 W&B 的情况下,我们可以使用工件,而在 MLflow 和 DVC 的情况下,DVC 运行在 Git 存储库之上,以跟踪数据集的不同版本,从而解决了 Git 的限制。

是否还有其他对数据集版本控制有用的方法和/或工具?简单的答案是肯定的,但更精确的答案取决于具体的情况。为了做出正确的选择,您必须考虑多个方面,包括成本、易用性和集成难度。在本节中,我们将提及一些我们认为值得探索的工具,如果数据集版本控制是项目的关键组成部分:

  • Neptune (docs.neptune.ai) 是一个用于 MLOps 的元数据存储。Neptune 工件允许在本地或云中存储的数据集上进行版本控制。

  • Delta Lake (delta.io) 是一个在数据湖顶层运行的开源存储抽象。Delta Lake 与 Apache Spark API 配合使用,并使用分布式处理来提高吞吐量和效率。

记住的事情

a. 市场上有许多数据版本控制工具。要选择合适的工具,您必须考虑多个方面,包括成本、易用性和集成难度。

b. 诸如 W&B、MLflow、DVC、Neptune 和 Delta Lake 的工具可以帮助您进行数据集版本控制。

至此,我们介绍了数据集版本控制的流行工具。适合的工具因项目而异。因此,在将任何工具集成到您的项目之前,您必须评估每个工具的利弊。

摘要

由于深度学习项目涉及多次模型训练和评估迭代,有效管理实验、模型和数据集可以帮助团队更快地实现目标。在本章中,我们探讨了两种最流行的深度学习项目跟踪设置:W&B 和与 DVC 集成的 MLflow。这两种设置都内置支持 Keras 和 PyTorch,这两个最流行的深度学习框架。我们还花了一些时间描述了更加强调数据集版本控制的工具:Neptune 和 Delta Lake。请记住,您必须仔细评估每个工具,以选择适合您项目的正确工具。

目前为止,您已经熟悉了构建概念验证和训练必要深度学习模型的框架和流程。从下一章开始,我们将讨论如何通过将深度学习管道的各个组件移至云端来实现规模化。

第二部分:构建一个功能齐全的产品

下一阶段是将概念验证迁移到现有基础设施。在整个过程中,数据处理逻辑和模型的初始版本通常会使用不同的工具和服务重新实现,目的是提高吞吐量和提高效率。在本书中,我们专注于 AWS,这是处理大量数据和昂贵计算的最流行的网络服务。

本部分包括以下章节:

  • 第五章云中的数据准备

  • 第六章高效模型训练

  • 第七章揭秘深度学习模型的秘密

第五章:云中的数据准备

在本章中,我们将学习如何通过利用各种 AWS 云服务在云中设置数据准备。考虑到在数据准备中的抽取、转换和加载(ETL)操作的重要性,我们将深入研究如何以成本效益的方式设置和调度 ETL 作业。我们将涵盖四种不同的设置:在单节点 EC2 实例和 EMR 集群上运行 ETL,然后利用 Glue 和 SageMaker 进行 ETL 作业。本章还将介绍 Apache Spark,这是最流行的 ETL 框架。通过完成本章,您将能够利用所提供的不同设置的优势,并为项目选择合适的工具集。

本章将涵盖以下主要主题:

  • 云中的数据处理

  • Apache Spark 简介

  • 设置单节点 EC2 实例用于 ETL

  • 设置用于 ETL 的 EMR 集群

  • 创建用于 ETL 的 Glue 作业

  • 利用 SageMaker 进行 ETL

技术要求

您可以从本书的 GitHub 存储库下载本章的补充材料:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_5

云中的数据处理

深度学习(DL)项目的成功取决于数据的质量和数量。因此,用于数据准备的系统必须稳定且可扩展,以有效地处理 TB 和 PB 级数据。这通常需要不止一台机器;必须设置一组运行强大的 ETL 引擎的机器集群,以便存储和处理大量数据。

首先,我们想介绍 ETL,即云中数据处理的核心概念。接下来,我们将概述用于数据处理的分布式系统设置。

ETL 简介

在整个 ETL 过程中,数据将从一个或多个来源收集,根据需要转换为不同的形式,并保存在数据存储中。 简而言之,ETL 本身涵盖了整个数据处理管道。ETL 在整个过程中与三种不同类型的数据交互:结构化非结构化半结构化。结构化数据表示具有模式的数据集(例如表),非结构化数据没有明确定义的显式模式(例如文本、图像或 PDF 文件),而半结构化数据在数据本身内部具有部分结构(例如 HTML 或电子邮件)。

流行的 ETL 框架包括Apache Hadoop (hadoop.apache.org),Presto (prestodb.io),Apache Flink (flink.apache.org)和Apache Spark (spark.apache.org)。Hadoop 是最早利用分布式处理优势的数据处理引擎之一。Presto 专门用于处理 SQL 中的数据,而 Apache Flink 则专注于处理流数据。在这四个框架中,Apache Spark 是最流行的工具,因为它可以处理各种数据类型。Apache Spark 利用内存数据处理来增加吞吐量,并提供比 Hadoop 更可扩展的数据处理解决方案。此外,它可以轻松集成其他 ML 和 DL 工具。因此,我们在本书中主要关注 Spark。

数据处理系统架构

为数据处理系统设置环境并不是一项简单的任务,因为它涉及定期获取高端机器、正确链接各种数据处理软件,并确保在发生故障时不丢失数据。因此,许多公司利用云服务,这是一种通过互联网按需提供的各种软件服务。虽然许多公司提供各种云服务,但亚马逊云服务 (AWS)以其稳定且易于使用的服务脱颖而出。

为了让您对现实生活中的数据处理系统有一个更广泛的了解,让我们看一个基于 AWS 服务的样例系统架构。这个系统的核心组件是开源的Apache Spark,执行主要的 ETL 逻辑。一个典型的系统还包括用于调度个别作业、存储数据和可视化处理后数据的组件:

图 5.1 – 数据处理管道的通用架构还包括可视化和实验平台

图 5.1 – 数据处理管道的通用架构,同时包括可视化和实验平台

让我们逐个看看这些组件:

  • 数据存储:数据存储负责保存数据和相关的元数据:

    • Hadoop 分布式文件系统 (HDFS):开源的 HDFS 是一个可按需扩展的分布式文件系统 (hadoop.apache.org)。HDFS 一直是数据存储的传统选择,因为 Apache Spark 和 Apache Hadoop 在 HDFS 上表现最佳。

    • Amazon Simple Storage Service (S3): 这是 AWS 提供的数据存储服务aws.amazon.com/s3)。S3 使用对象和存储桶的概念,其中对象指单个文件,存储桶指对象的容器。对于每个项目或子模块,您可以创建一个存储桶,并为读写操作配置不同的权限。存储桶还可以对数据应用版本控制,跟踪更改记录。

  • ml,成本大约比其他 EC2 实例高 30%到 40%(aws.amazon.com/sagemaker/pricing)。在利用 SageMaker 进行 ETL部分,我们将描述如何在 EC2 实例上设置 SageMaker 进行 ETL 流程。

考虑到需要处理的数据量,正确选择的 ETL 服务以及适当的数据存储选择可以显著提高管道的效率。需要考虑的关键因素包括数据源、数据量、可用的硬件资源和可伸缩性等。

  • Scheduling: 经常需要定期运行 ETL 作业(例如每天、每周或每月),因此需要调度器:

    • AWS Lambda functions: Lambda 函数(aws.amazon.com/lambda)旨在在 EMR 上运行作业,无需提供或管理基础设施。执行时间可以动态配置;作业可以立即运行,也可以计划在不同时间运行。AWS Lambda 函数以无服务器方式运行代码,因此无需维护。如果执行期间出现错误,EMR 集群将自动关闭。

    • Airflow: 调度器在自动化 ETL 过程中发挥重要作用。Airflow(airflow.apache.org是数据工程师使用的最流行的调度框架之一。Airflow 的有向无环图DAG)可用于定期调度管道。Airflow 比 AWS Lambda 函数更常见,用于定期运行 Spark 作业,因为 Airflow 在前面的执行失败时可以轻松地回填数据。

  • Build: Build 是将代码包部署到 AWS 计算资源(如 EMR 或 EC2)或根据预定义规范设置一组 AWS 服务的过程:

    • CloudFormation: CloudFormation 模板(aws.amazon.com/cloudformation帮助以代码形式配置云基础设施。CloudFormation 通常用于执行特定任务,比如创建 EMR 集群、准备具体规格的 S3 存储桶或终止正在运行的 EMR 集群。它有助于标准化重复性任务。

    • Jenkins:Jenkins (www.jenkins.io) 构建用 Java 和 Scala 编写的可执行文件。我们使用 Jenkins 构建 Spark 流水线工件(例如.jar 文件)并部署到 EMR 节点。Jenkins 还利用 CloudFormation 模板以标准化方式执行任务。

  • 数据库(Database):数据存储与数据库的关键区别在于数据库用于存储结构化数据。在这里,我们将讨论两种流行的数据库类型:关系数据库键-值存储数据库。我们将描述它们的区别,并解释适当的使用案例。

    • 关系数据库(Relational databases)关系数据库以表格格式存储带有模式的结构化数据。以结构化方式存储数据的主要优点来自于数据管理;存储的数据值受到严格控制,保持值的一致格式。这使得数据库能够在存储和检索特定数据集时进行额外的优化。ETL 作业通常从一个或多个数据存储服务中读取数据,处理数据,并将处理后的数据存储在关系数据库中,例如MySQL (www.mysql.com) 和 PostgreSQL (www.postgresql.org)。AWS 还提供关系数据库服务,例如Amazon RDS (aws.amazon.com/rds)。

    • 键-值存储数据库(Key-value storage databases):与传统的关系数据库不同,这些是专为高频率读写操作优化的数据库。这些数据库以独特的键-值对方式存储数据。一般来说,数据由一组键和一组值组成,每个键持有各自的属性。许多数据库支持模式,但它们的主要优势在于它们也支持非结构化数据。换句话说,您可以存储任何数据,即使每个数据具有不同的结构。这类数据库的流行例子包括Cassandra (cassandra.apache.org) 和 MongoDB (www.mongodb.com)。有趣的是,AWS 提供了一个称为DynamoDB的键-值存储数据库服务 (aws.amazon.com/dynamodb)。

  • 元数据存储库(Metastore):在某些情况下,最初收集和存储的数据集可能缺少关于自身的任何信息:例如,可能缺少列类型或关于数据源的详细信息。当工程师们管理和处理数据时,这些信息通常对他们有所帮助。因此,工程师们引入了元数据存储库的概念。这是一个存储元数据的仓库。存储为表格的元数据提供了数据指向的位置、模式以及更新历史。

在 AWS 的情况下,Glue Data Catalog 充当元数据存储库的角色,为 S3 提供内置支持。而 Hive (hive.apache.org) 则是一个针对 HDFS 的开源元数据存储库。Hive 的主要优势来自于数据查询、汇总和分析,这些功能天然支持基于类 SQL 语言的交互。

  • 应用程序编程接口 (API) 服务API 端点允许数据科学家和工程师有效地与数据进行交互。例如,可以设置 API 端点以便轻松访问存储在 S3 存储桶中的数据。许多框架专为 API 服务而设计。例如,Flask API (flask.palletsprojects.com) 和 Django (www.djangoproject.com) 框架基于 Python,而 Play 框架 (www.playframework.com) 则经常用于 Scala 项目。

  • 实验平台:在生产中评估系统性能通常通过一种称为 A/B 测试的流行用户体验研究方法来实现。通过部署系统的两个不同版本并比较用户体验,A/B 测试使我们能够了解最近的更改是否对系统产生了积极影响。一般来说,设置 A/B 测试涉及两个组成部分:

    • Rest APIRest API 在处理带有不同参数的请求和返回经过处理的数据方面提供了更大的灵活性。因此,通常会设置一个 Rest API 服务,从数据库或数据存储中聚合必要的数据以供分析目的,并以 JSON 格式提供数据给 A/B 实验平台。

    • A/B 实验平台:数据科学家通常使用一个带有图形用户界面 (GUI) 的应用程序安排各种 A/B 测试实验,并直观地可视化聚合数据进行分析。GrowthBook (www.growthbook.io) 是这类平台的开源示例。

  • 数据可视化工具: 公司内的几个团队和组别(例如市场营销、销售和高管团队)可以从直观地可视化数据中受益。数据可视化工具通常支持创建自定义仪表板,有助于数据分析过程。Tableau (www.tableau.com) 是项目领导者中广受欢迎的工具,但它是专有软件。另一方面,Apache Superset (superset.apache.org) 是一款开源数据可视化工具,支持大多数标准数据库。如果担心管理成本,可以配置 Apache Superset 来读取和绘制使用无服务器数据库(例如 AWS Athena (aws.amazon.com/athena))存储的数据的可视化图表。

  • 身份访问管理 (IAM): IAM 是一种权限系统,用于管理对 AWS 资源的访问。通过 IAM,可以控制用户可以访问的一组资源以及他们可以对提供的资源执行的一组操作。有关 IAM 的更多详细信息,请访问 aws.amazon.com/iam

记住的事情

a. 在整个 ETL 过程中,数据将从一个或多个源收集,根据需要转换为不同的形式,并保存到数据存储或数据库中。

b. Apache Spark 是一款开源的 ETL 引擎,广泛用于处理各种类型的大量数据:结构化、非结构化和半结构化数据。

c. 为数据处理作业设置的典型系统包括各种组件,包括数据存储、数据库、ETL 引擎、数据可视化工具和实验平台。

d. ETL 引擎可以在多种设置中运行 - 单机器、集群、完全托管的云端 ETL 服务以及为深度学习项目设计的端到端服务。

在接下来的部分,我们将介绍 Apache Spark 的关键编程概念,这是最流行的 ETL 工具。

Apache Spark 简介

Apache Spark 是一个开源的数据分析引擎,用于数据处理。最流行的用例是 ETL。作为 Spark 的介绍,我们将涵盖围绕 Spark 的关键概念以及一些常见的 Spark 操作。具体来说,我们将从介绍弹性分布式数据集RDDs)和 DataFrames 开始。然后,我们将讨论用于 ETL 任务的 Spark 基础知识:如何从数据存储加载数据集合,应用各种转换,并存储处理后的数据。Spark 应用可以使用多种编程语言实现:Scala、Java、Python 和 R。在本书中,我们将使用 Python,以便与其他实现保持一致。本节中的代码片段可以在本书的 GitHub 代码库中找到:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_5/spark。我们在示例中使用的数据集包括 Google Scholar 和我们在 第二章 深度学习项目的数据准备 中爬取的 COVID 数据集,以及由纽约时报提供的另一个 COVID 数据集(github.com/nytimes/covid-19-data)。我们将最后一个数据集称为 NY Times COVID。

弹性分布式数据集和 DataFrames

Spark 的独特优势来自于 RDDs,即不可变的分布式数据对象集合。通过利用 RDDs,Spark 能够高效处理利用并行性的数据。Spark 内置的操作基于 RDDs 的并行处理有助于数据处理,即使其中一个或多个处理器失败。当触发 Spark 作业时,输入数据的 RDD 表示会被分割成多个分区,并分发到每个节点进行转换,从而最大化吞吐量。

类似于 pandas 的 DataFrames,Spark 也有 DataFrames 的概念,它们表示关系数据库中的表,具有命名列。DataFrame 也是一个 RDD,因此我们在下一节描述的操作也可以应用于它们。DataFrame 可以从结构化为表格的数据创建,例如 CSV 数据、Hive 中的表或现有的 RDDs。DataFrame 包含 RDD 不提供的模式。因此,RDD 用于非结构化和半结构化数据,而 DataFrame 用于结构化数据。

在 RDDs 和 DataFrames 之间转换

任何 Spark 操作的第一步是创建一个 SparkSession 对象。具体来说,使用 pyspark.sql 中的 SparkSession 模块创建 SparkSession 对象。如下所示,使用该模块中的 getOrCreate 函数创建会话对象。SparkSession 对象是 Spark 应用程序的入口点。它提供了在不同上下文(如 Spark 上下文、Hive 上下文和 SQL 上下文)下与 Spark 应用程序交互的方式:

from pyspark.sql import SparkSession
spark_session = SparkSession.builder\
        .appName("covid_analysis")\
        .getOrCreate()

将 RDD 转换为 DataFrame 很简单。鉴于 RDD 没有任何模式,因此可以如下创建一个没有模式的 DataFrame:

# convert to df without schema
df_ri_freq = rdd_ri_freq.toDF() 

要将 RDD 转换为具有模式的 DataFrame,您需要使用pyspark.sql.types模块中的StructType类。一旦使用StructType方法创建了模式,就可以使用 Spark 会话对象的createDataFrame方法将 RDD 转换为 DataFrame:

from pyspark.sql.types import StructType, StructField, StringType, IntegerType
# rdd for research interest frequency data
rdd_ri_freq = ... 
# convert to df with schema
schema = StructType(
          [StructField("ri", StringType(), False), 
           StructField("frequency", IntegerType(), False)])
df = spark.createDataFrame(rdd_ri_freq, schema)

现在我们已经学会如何在 Python 中设置 Spark 环境,让我们学习如何将数据集加载为 RDD 或 DataFrame。

加载数据

Spark 可以加载存储在各种数据存储中的不同格式的数据。加载存储在 CSV 格式中的数据是 Spark 的基本操作。可以使用spark_session.read.csv函数轻松实现这一点。它将本地或云端(如 S3 桶中)的 CSV 文件读取为 DataFrame。在下面的代码片段中,我们正在加载存储在 S3 中的 Google Scholar 数据。可以使用header选项指示 CSV 文件包含标题行:

# datasets location
google_scholar_dataset_path = "s3a://my-bucket/dataset/dataset_csv/dataset-google-scholar/output.csv"
# load google scholar dataset
df_gs = spark_session. \
        .read \
        .option("header", True) \
        .csv(google_scholar_dataset_path)

下图显示了df_gs.show(n=3)的结果。show函数打印了前n行以及列标题:

图 5.2 – 通过加载 CSV 文件创建的样本 DataFrame

图 5.2 – 通过加载 CSV 文件创建的样本 DataFrame

同样地,可以使用SparkSession模块的read.json函数读取数据存储中的 JSON 文件:

# loada json file
json_file_path="s3a://my-bucket/json/cities.json"
df = spark_session.read.json(json_file_path)

在下一节中,我们将学习如何使用 Spark 操作处理加载的数据。

使用 Spark 操作处理数据

Spark 提供了一组操作,可以将 RDD 转换为不同结构的 RDD。实现 Spark 应用程序是在 RDD 上链接一组 Spark 操作以将数据转换为目标格式的过程。在本节中,我们将讨论最常用的操作 – 即filtermapflatMapreduceByKeytakegroupByjoin

过滤器

在大多数情况下,通常首先应用过滤器以丢弃不必要的数据。对 DataFrame 应用filter方法可以帮助您从给定的 DataFrame 中选择感兴趣的行。在下面的代码片段中,我们使用这种方法仅保留research_interest不为None的行:

# research_interest cannot be None
df_gs_clean = df_gs.filter("research_interest != 'None'")

map

与其他编程语言中的map函数类似,Spark 中的map操作将给定函数应用于每个数据条目。在这里,我们使用map函数仅保留research_interest列:

# we are only interested in research_interest column
rdd_ri = df_gs_clean.rdd.map(lambda x: (x["research_interest"]))

flatMap

flatMap函数在对每个条目应用给定函数后展开 RDD,并返回新的 RDD。在本例中,flatMap操作使用##分隔符拆分每个数据条目,然后创建具有值为1的默认频率的research_interest对:

# raw research_interest data into pairs of research area and a frequency count
rdd_flattened_ri = rdd_ri.flatMap(lambda x: [(w.lower(), 1) for w in x.split('##')])

reduceByKey

reduceByKey基于其键对输入 RDD 进行分组。在这里,我们使用reduceByKey来对频率进行求和,以了解每个research_interest的出现次数:

# The pairs are grouped based on research area and the frequencies are summed up
rdd_ri_freq = rdd_flattened_ri.reduceByKey(add)

take

Spark 的基本操作之一是take。此函数用于从 RDD 中获取前n个元素:

# we are interested in the first 5 entries
rdd_ri_freq_5 = rdd_ri_freq.take(5)

分组操作

分组的概念是将 DataFrame 中相同的数据条目收集到组中,并对这些组执行聚合(例如平均值或求和)。

例如,让我们使用 Moderna COVID 数据集通过groupby操作获取每个司法管辖区(州)分配的平均剂量数。在这里,我们使用sort函数对州级平均剂量进行排序。toDFalias函数可以帮助为新 DataFrame 添加名称:

# calculate average number of 1st corona vaccine per jurisdiction (state)
df_avg_1 = df_covid.groupby("jurisdiction")\
  .agg(F.avg("_1st_dose_allocations")
  .alias("avg"))\
  .sort(F.col("avg").desc())\
  .toDF("state", "avg")

在应用groupby时,可以在单个命令中应用多个聚合(sumavg)。从聚合函数(如F.avgF.sum)创建的列可以使用alias重命名。在以下示例中,正在对 Moderna COVID 数据集执行聚合操作,以获取第一剂和第二剂的平均数和总数:

# At jurisdiction (state) level, calculate at average weekly 1st & 2nd dose vaccine distribution. Similarly calculate sum for 1st and 2nd dose
df_avg = df_covid.groupby(F.lower("jurisdiction").alias("state"))\
  .agg(F.avg("_1st_dose_allocations").alias("avg_1"), \
       F.avg("_2nd_dose_allocations").alias("avg_2"), \
       F.sum("_1st_dose_allocations").alias("sum_1"), \
       F.sum("_2nd_dose_allocations").alias("sum_2")
       ) \
  .sort(F.col("avg_1").desc())

使用groupby函数在州级别执行计算。该数据集总共包含 63 个州,包括某些实体(联邦机构)作为州。

join

join功能有助于组合来自两个 DataFrame 的行。

为了演示如何使用join,我们将 Moderna COVID 数据集与 NY Times COVID 数据集进行连接。在解释任何join操作之前,我们必须像之前处理 Moderna COVID 数据集一样,在 NY Times COVID 数据集上应用聚合。在以下代码片段中,正在应用groupby操作以州级别获取聚合(sum)值,代表总死亡人数和总病例数:

# at jurisdiction (state) level, calculate total number of deaths and total number of cases
df_cases = df_covid2 \
          .groupby(F.lower("state").alias("state")) \
          .agg(F.sum("deaths").alias("sum_deaths"), \
               F.sum("cases").alias("sum_cases"))

Figure 5.3 显示了df_cases.show(n=3)操作的结果,可视化处理后 DataFrame 的前三行:

![Figure 5.3 – 使用 df_inner.show(n=3)操作的输出结果![Figure 5.3 – 聚合结果的前三行 Figure 5.3 – 可视化处理后 DataFrame 的前三行结果现在我们准备演示两种类型的连接:equi-join 和左连接。#### Equi-join(内连接)Equi-join,也称为内连接,是 Spark 中默认的join操作。内连接用于在两个 DataFrame 之间基于共同列值进行连接。在最终的 DataFrame 中,键不匹配的行将被丢弃。在本例中,将应用 equi-join 到state列,作为 Moderna COVID 数据集和 NY Times COVID 数据集之间的共同列。第一步是使用alias为 DataFrame 创建别名。然后,在一个 DataFrame 上调用join函数,同时传递另一个 DataFrame 来定义列关系和连接类型:py# creating an alias for each DataFramedf_moderna = df_avg.alias("df_moderna")df_ny = df_cases.alias("df_ny")df_inner = df_moderna.join(df_ny, F.col("df_moderna.state") == F.col("df_ny.state"), 'inner')下面是df_inner.show(n=3)操作的输出:Figure 5.4 – 使用 df_inner.show(n=3)操作的输出结果

图 5.4 – 使用df_inner.show(n=3)操作的输出

现在,让我们看看另一种类型的连接,左连接。

左连接

左连接是另一种用于数据分析的join操作。左连接返回来自一个 DataFrame 的所有行,不管在另一个 DataFrame 上是否找到匹配项。当join表达式不匹配时,它会为缺失的条目分配null

左连接语法类似于等值连接。唯一的区别在于,在指定连接类型时,您需要使用left关键字而不是inner。左连接获取第一个 DataFrame(df_m)中提到的指定列(df_m.state)的所有值。然后,它试图在第二个提到的 DataFrame(df_ny)中的指定列(df_ny.state)上匹配条目。在本例中,如果某个特定状态在两个 DataFrame 中都出现,则join操作的输出将是该状态及来自两个 DataFrame 的值。如果某个特定状态仅在第一个 DataFrame(df_m)中可用,而不在第二个 DataFrame(df_ny)中,则它将添加该状态及第一个 DataFrame 的值,保留其他条目为null

# join results in all rows from the left table. Missing entries from the right table will result in "null"
df_left = df_moderna.join(df_ny, F.col("df_m.state") == F.col("df_ny.state"), 'left')

使用df_left.show(n=3)命令的输出如下所示:

图 5.5 – 使用操作的输出

图 5.5 – 使用df_inner.show(n=3)操作的输出

尽管 Spark 提供了广泛的操作来涵盖不同的用例,但由于逻辑复杂性,构建自定义操作可能更有用。

使用用户定义函数处理数据

用户定义函数UDF是一种可重复使用的自定义函数,用于对 RDD 执行转换。UDF 函数可以在多个 DataFrame 上重复使用。在本节中,我们将提供一个完整的代码示例,用于使用 UDF 处理 Google Scholar 数据集。

首先,我们想介绍pyspark.sql.function模块,它允许您使用udf方法定义 UDF,并提供各种基于列的操作。pyspark.sql.function还包括用于聚合的函数,如用于计算平均值和总和的avgsum

import pyspark.sql.functions as F

在 Google Scholar 数据集中,data_scienceartificial_intelligencemachine_learning都指向相同的research_interest数据领域,并检查是否可以将任何数据归类为 AI。如果找到匹配项,则在新列中放置1值。否则,将分配0。UDF 的结果使用withColumn方法存储在名为is_artificial_intelligence的新列中。在以下代码片段中,@F.udf注解通知 Spark 该函数是一个 UDF。pyspark.sql.functions中的col方法经常用于将列作为 UDF 的参数传递。在这里,F.col("research_interest")已传递给 UDF is_ai方法,指示该 UDF 应操作的列:

# list of research_interests that are under same domain
lst_ai  = ["data_science", "artificial_intelligence",
           "machine_learning"]
@F.udf
def is_ai(research):
    """ return 1 if research in AI domain else 0"""
    try:
      # split the research interest with delimiter "##"  
      lst_research = [w.lower() for w in str(research).split("##")]
      for res in lst_research:
        # if present in AI domain
        if res in lst_ai:
          return 1
      # not present in AI domain
      return 0
    except:
      return -1
# create a new column "is_artificial_intelligence"
df_gs_new = df_gs.withColumn("is_artificial_intelligence",\ is_ai(F.col("research_interest")))

在处理原始数据后,我们希望将其存储在数据存储中,以便可以为其他目的重复使用。

导出数据

在本节中,我们将学习如何将 DataFrame 保存到 S3 存储桶中。对于 RDD,必须将其转换为 DataFrame 才能适当保存。

通常,数据分析师希望将聚合数据写入 CSV 文件以进行后续操作。要将 DataFrame 导出为 CSV 文件,必须使用 df.write.csv 函数。对于文本值,建议使用 option("quoteAll", True),这将用引号括起每个值。

在以下示例中,我们提供了一个 S3 路径来生成 S3 存储桶中的 CSV 文件。使用 coalesce(1) 以写入单个 CSV 文件而不是多个 CSV 文件:

S3_output_path = "s3a:\\my-bucket\output\vaccine_state_avg.csv"
# writing a DataFrame as a CSVfile
sample_data_frame.\
        .coalesce(1) \
        .write \
        .mode("overwrite") \
        .option("header", True) \
        .option("quoteAll",True) \
        .csv(s3_output_path)

如果要将 DataFrame 保存为 JSON 文件,可以使用 write.json

S3_output_path = "s3a:\\my-bucket\output\vaccine_state_avg.json"
# Writing a DataFrame as a json file
sample_data_frame \
        .write \
        .json(s3_output_path)

此时,您应该看到一个文件已存储在 S3 存储桶中。

需记住的事项

a. RDD 是一个不可变的分布式集合,被分割成多个分区并在集群的不同节点上计算。

b. Spark DataFrame 相当于关系数据库中的表,具有命名列。

c. Spark 提供了一系列操作,可以将一个 RDD 转换为具有不同结构的另一个 RDD。实现 Spark 应用程序是在 RDD 上链接一系列 Spark 操作,将数据转换为目标格式的过程。您可以使用 UDF 构建自定义的 Spark 操作。

在本节中,我们描述了 Apache Spark 的基础知识,这是最常用的 ETL 工具。从下一节开始,我们将讨论如何在云中为 ETL 设置 Spark 作业。首先,让我们看看如何在单个 EC2 实例上运行 ETL。

为 ETL 设置单节点 EC2 实例

EC2 实例可以具有各种 CPU/GPU、内存、存储和网络容量的组合。您可以在官方文档中找到 EC2 的可配置选项:aws.amazon.com/ec2/instance-types

创建 EC2 实例时,可以选择预定义的 Docker 镜像来运行各种项目。这些称为Amazon Machine ImagesAMIs)。例如,有一个安装了 TF 版本 2 用于 DL 项目的镜像,以及一个为通用 ML 项目设置了 Anaconda 的镜像,如下截图所示。有关完整的 AMI 列表,请参阅 docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html

图 5.6 – 选择 EC2 实例的 AMI

图 5.6 – 选择 EC2 实例的 AMI

AWS 提供了用于 DL 项目的Deep Learning AMIsDLAMIs),这些 AMIs 是专为 DL 项目创建的;这些镜像利用不同的 CPU 和 GPU 配置以及不同的计算架构(docs.aws.amazon.com/dlami/latest/devguide/options.html)。

如在 第一章 中提到的 深度学习驱动项目的有效规划,许多数据科学家利用 EC2 实例开发他们的算法,利用动态资源分配的灵活性。创建 EC2 实例并安装 Spark 的步骤如下:

  1. 创建一个 虚拟专用网络VPN),以限制访问 EC2 实例以增强安全性。

  2. 使用 EC2 密钥对创建一个 .pem 密钥。 .pem 文件用于在用户尝试从终端登录 EC2 实例时执行身份验证。

  3. 使用包含所需工具和包的 Docker 镜像创建 EC2 实例。

  4. 添加一个入站规则,允许从本地终端访问新实例。

  5. 使用 SSH 访问 EC2 实例,并使用在 第 2 步 中创建的 .pem 文件。

  6. 启动 Spark shell。

我们为每个步骤提供了详细的描述和屏幕截图,位于 github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_5/ec2

要记住的事情

a. 一个 EC2 实例可以具有不同的 CPU/GPU、内存、存储和网络容量组合。

b. 可以从 AWS Web 控制台上的预定义 Docker 镜像(AMI)中创建 EC2 实例。

接下来,我们将学习如何设置一个运行一组 Spark 工作节点的集群。

为 ETL 设置 EMR 集群

在 DL 的情况下,单个 EC2 实例的计算能力可能不足以进行模型训练或数据处理。因此,通常将一组 EC2 实例放在一起以增加吞吐量。AWS 为此提供了专门的服务:Amazon Elastic MapReduceEMR)。它是一个完全托管的集群平台,提供用于大数据框架(如 Apache Spark 和 Hadoop)的分布式系统。通常,为 ETL 设置的 EMR 集群从 AWS 存储(Amazon S3)读取数据,处理数据,然后将数据写回 AWS 存储。Spark 作业通常用于处理与 S3 交互的 ETL 逻辑。EMR 提供了一个名为 Workspace 的有趣功能,帮助开发人员组织笔记本,并与其他 EMR 用户共享以进行协作工作。

典型的 EMR 设置包括一个主节点和几个核心节点。在多节点集群中,必须至少有一个核心节点。主节点管理运行分布式应用程序(例如 Spark 或 Hadoop)的集群。核心节点由主节点管理,运行数据处理任务并将数据存储在数据存储中(例如 S3 或 HDFS)。

任务节点由主节点管理,是可选的。它们通过在计算过程中引入另一种并行性,提高了集群上运行的分布式应用程序的吞吐量。它们运行数据处理任务,但不将数据存储在数据存储中。

下面的屏幕截图显示了 EMR 集群创建页面。在整个表单中,我们需要提供集群名称、启动模式、EMR 版本、要在集群上运行的应用程序(例如用于数据处理的 Apache Spark 和笔记本的 Jupyter)以及 EC2 实例的规格。DL 的数据处理通常需要高计算能力的实例。在其他情况下,您可以构建具有增加内存限制的集群:

图 5.7 – EMR 集群创建

图 5.7 – EMR 集群创建

图 5.7 – EMR 集群创建

详细步骤如下:

  • 步骤 1:软件和步骤:在这里,您必须选择与软件相关的配置 – 即 EMR 版本和应用程序(Spark、JupyterHub 等)。

  • 步骤 2:硬件:在这里,您必须选择与硬件相关的配置 – 即实例类型、实例数量和 VPN 网络。

  • 步骤 3:通用集群设置:选择集群名称和用于操作日志的 S3 存储桶路径。

  • .pem 文件:

    • .pem 文件只在您想要登录到 EC2 主节点并在 Spark shell 上工作时才需要,就像在单个 EC2 实例的情况下一样。

完成这些步骤后,您需要等待几分钟,直到集群状态变为running。然后,您可以导航到 EMR 集群提供的端点以打开 Jupyter 笔记本。用户名为jovyan,密码为jupyter

我们的 GitHub 存储库提供了这一过程的逐步说明,以及屏幕截图(github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_5/emr)。

需记住的事项

a. EMR 是一个完全托管的集群平台,运行大数据 ETL 框架,如 Apache Spark。

b. 您可以通过 AWS Web 控制台创建具有各种 EC2 实例的 EMR 集群。

EMR 的缺点在于需要明确管理。一个组织通常有专门处理与 EMR 集群相关问题的开发人员小组。不幸的是,如果组织很小,这可能会很难做到。在接下来的部分中,我们将介绍 Glue,它不需要任何显式的集群管理。

创建用于 ETL 的 Glue 作业

AWS Glue(aws.amazon.com/glue)支持以无服务器方式进行数据处理。Glue 的计算资源由 AWS 管理,因此与专用集群(例如 EMR)相比,维护工作量较少。除了资源的最小维护工作外,Glue 还提供额外功能,如内置调度器和 Glue 数据目录,稍后将进行讨论。

首先,让我们学习如何使用 Glue 设置数据处理作业。在开始定义数据处理逻辑之前,您必须创建一个包含 S3 中数据架构的 Glue 数据目录。一旦为输入数据定义了 Glue 数据目录,您可以使用 Glue Python 编辑器定义数据处理逻辑的细节(图 5.8)。该编辑器为您的应用提供了一个基本设置,以减少设置 Glue 作业时的困难:docs.aws.amazon.com/glue/latest/dg/edit-script.html。在这个模板代码的基础上,您将读取 Glue 数据目录作为输入,对其进行处理,并存储处理后的输出。由于 Glue 数据目录与 Spark 高度集成,Glue 作业内的操作通常使用 Spark 完成:

图 5.8 – AWS Glue 作业脚本编辑器

图 5.8 – AWS Glue 作业脚本编辑器

在接下来的章节中,您将学习如何使用存储在 S3 存储桶中的 Google Scholar 数据集设置 Glue 作业。完整的实现可以在 github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_5/glue 找到。

创建 Glue 数据目录

首先,我们将创建一个 Glue 数据目录(见 图 5.9)。Glue 只能读取数据集,其中元数据存储在 Glue 数据目录中。数据目录由数据库组成,这些数据库是以表格形式存储的元数据集合。Glue 提供了一个称为 crawler 的功能,用于创建存储在数据存储中的数据文件的元数据(例如,一个 S3 存储桶):

图 5.9 – 设置爬虫的第一步

图 5.9 – 设置爬虫的第一步

上述截图显示了创建爬虫的第一步。每个步骤的详细信息可以在 docs.aws.amazon.com/glue/latest/dg/add-crawler.html 找到。

设置 Glue 上下文

如果您查看 AWS 为 Glue 提供的模板代码,您会发现已经导入了一些关键包。awsglue.utils 模块中的 getResolvedOptions 帮助利用在运行时传递给 Glue 脚本的参数:

from awsglue.utils import getResolvedOptions
args = getResolvedOptions(sys.argv, ['JOB_NAME'])

对于使用 Spark 的 Glue 作业,必须创建一个 Spark 上下文并将其传递给 GlueContext。可以从 Glue 上下文中访问 Spark 会话对象。可以通过传递 Glue 上下文对象来实例化使用 awsglue.job 模块的 Glue 作业:

from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
# glue_job_google_scholar.py
# spark context
spark_context = SparkContext()
# glue context
glueContext = GlueContext(spark_context)
# spark
spark_session = glueContext.spark_session
# job
job = Job(glueContext)
# initialize job
job.init(args['JOB_NAME'], args)

接下来,我们将学习如何从 Glue 数据目录中读取数据。

读取数据

在本节中,您将学习如何在创建 Glue 表目录后,在 Glue 上下文中读取位于 S3 存储桶中的数据。

在 Glue 中,数据通过称为 DynamicFrame 的特定数据结构从转换到转换传递,它是 Apache Spark DataFrame 的扩展。DynamicFrame 具有自描述特性,不需要任何模式。与 Spark DataFrame 不同,DynamicFrame 的这一额外属性有助于容纳不符合固定模式的数据。可以从 awsglue.dynamicframe 导入所需的库。该包可以轻松地将 DynamicFrame 转换为 Spark DataFrame:

from awsglue.dynamicframe import DynamicFrame

在下面的示例中,我们正在创建一个名为 google_authors 的 Glue 数据目录表,存储在名为 google_scholar 的数据库中。一旦数据库可用,可以使用 glueContext.create_dynamic_frame.from_catalog 读取 google_scholar 数据库中的 google_authors 表,并将其加载为 Glue DynamicFrame:

# glue context
google_authors = glueContext.create_dynamic_frame.from_catalog(
           database="google_scholar",
           table_name="google_authors")

可以使用 toDF 方法将 Glue DynamicFrame 转换为 Spark DataFrame。此转换需要将 Spark 操作应用于数据:

# convert the glue DynamicFrame to Spark DataFrame
google_authors_df = google_authors.toDF()

现在,让我们定义数据处理逻辑。

定义数据处理逻辑

Glue DynamicFrame 可以执行的基本转换由 awsglue.transforms 模块提供。这些转换包括 joinfiltermap 等等(docs.aws.amazon.com/glue/latest/dg/built-in-transforms.html)。您可以类似于Apache Spark 简介部分中所介绍的方式来使用它们:

from awsglue.transforms import *

此外,如果 Glue DynamicFrame 已转换为 Spark DataFrame,则可以将使用 Spark 操作处理数据部分中描述的每个 Spark 操作应用于 Glue 数据。

写入数据

在本节中,我们将学习如何将 Glue DynamicFrame 中的数据写入 S3 存储桶。

给定一个 Glue DynamicFrame,您可以使用 Glue 上下文的 write_dynamic_frame.from_options 将数据存储在指定的 S3 路径中。您需要在作业结束时调用 commit 方法来执行各个操作:

# path for output file
path_s3_write= "s3://google-scholar-csv/write/"
# write to s3 as a CSV file with separator |
glueContext.write_dynamic_frame.from_options(
    frame = dynamic_frame_write,
    connection_type = "s3",
    connection_options = {
            "path": path_s3_write
                         },
    format = "csv",
    format_options={
            "quoteChar": -1,
            "separator": "|"
                   })
job.commit()

对于 Spark DataFrame,必须在存储数据之前将其转换为 DynamicFrame。DynamicFrame.fromDF 函数接受 Spark DataFrame 对象、Glue 上下文对象和新 DynamicFrame 的名称:

# create a DynamicFrame from a Spark DataFrame
dynamic_frame = DynamicFrame.fromDF(df_sort, glueContext, "dynamic_frame")

现在,您可以同时使用 Spark 操作和 Glue 转换来处理您的数据。

需要记住的事项

a. AWS Glue 是一个专为 ETL 操作设计的完全托管服务

b. AWS Glue 是一个无服务器架构,这意味着底层服务器将由 AWS 维护

c. AWS Glue 提供了一个带有 Python 样板代码的内置编辑器。在此编辑器中,您可以定义您的 ETL 逻辑,并利用 Spark。

作为 ETL 的最后设置,我们将看一下 SageMaker。

利用 SageMaker 进行 ETL

在本节中,我们将描述如何使用 SageMaker 设置 ETL 流程(下图显示了 SageMaker 的 Web 控制台)。SageMaker 的主要优势在于它是一个完全托管的基础设施,用于构建、训练和部署 ML 模型。缺点是它比 EMR 和 Glue 更昂贵。

SageMaker Studio 是面向 SageMaker 的基于 Web 的开发环境。SageMaker 的理念是提供一个集成的数据分析管道。使用 SageMaker Studio 可以完成 ML 管道的每个阶段:数据处理、算法设计、作业调度、实验管理、模型开发和训练、创建推断端点、检测数据漂移以及可视化模型性能。SageMaker Studio 的笔记本也可以连接到 EMR 进行计算,但有一些限制;只能使用部分限定的 Docker 镜像(如Data ScienceSparkMagic)(docs.aws.amazon.com/sagemaker/latest/dg/studio-notebooks-emr-cluster.html):

Figure 5.10 – The SageMaker web console

图 5.10 – SageMaker Web 控制台

SageMaker 提供各种预定义的开发环境作为 Docker 镜像。流行的环境包括已经安装了 PyTorch、TF 和 Anaconda 的 DL 项目环境。可以轻松地从 Web 开发环境将笔记本附加到任何这些镜像中,如下面的截图所示:

图 5.11 – 动态更新 SageMaker 笔记本的开发环境

图 5.11 – 动态更新 SageMaker 笔记本的开发环境

创建 ETL 作业的过程可以分解为四个步骤:

  1. 在 SageMaker Studio 内创建一个用户。

  2. 通过选择正确的 Docker 镜像在用户下创建一个笔记本。

  3. 定义数据处理逻辑。

  4. 安排一个作业。

在 SageMaker Web 控制台中,步骤 1步骤 2 只需一键即可完成。步骤 3 可以使用 Spark 设置。要安排作业(步骤 4),首先需要通过pip命令安装run-notebook命令行实用程序:

pip install https://github.com/aws-samples/sagemaker-run-notebook/releases/download/v0.20.0/sagemaker_run_notebook-0.20.0.tar.gz

在讨论安排笔记本的run-notebook命令之前,我们将简要介绍cron命令,它定义了一个调度的格式。如下图所示,使用六个数字表示时间戳。例如,45 22 ** 6*表示每周六晚上 10:45 的调度。*(星号)通配符表示相应单位的每个值:

图 5.12 – Cron 调度格式

图 5.12 – Cron 调度格式

run-notebook命令接受用cron表示的调度和一个笔记本。在以下示例中,notebook.ipynb已安排在 2021 年每天上午 8 点运行:

run-notebook schedule --at "cron(0 8 * * * 2021)" --name nightly notebook.ipynb

我们在 GitHub 仓库的每个步骤中都提供了一组截图:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_5/sagemaker/sagemaker_studio.md

在接下来的章节中,我们将深入研究如何利用 SageMaker 笔记本运行数据处理作业。

创建 SageMaker 笔记本

笔记本实例是运行 Jupyter 笔记本应用程序的 ML 计算实例。SageMaker 将创建此实例以及相关资源。Jupyter 笔记本用于处理数据、训练模型、部署和验证模型。可以在几个步骤内创建笔记本实例。详细描述可在docs.aws.amazon.com/sagemaker/latest/dg/howitworks-create-ws.html找到:

  1. 转到 SageMaker Web 控制台:console.aws.amazon.com/sagemaker。请注意,您需要使用 AWS 凭据登录。

  2. 笔记本实例下,选择创建笔记本实例

  3. 在每个新笔记本上进行pip install tensorflow)。可以在github.com/aws-samples/amazon-sagemaker-notebook-instance-lifecycle-config-samples/tree/master/scripts找到各种示例:

图 5.13 – SageMaker 笔记本的生命周期配置脚本

图 5.13 – SageMaker 笔记本的生命周期配置脚本

虽然直接从 SageMaker 笔记本运行一组操作是一种选择,但 SageMaker 笔记本支持运行明确定义在笔记本外部的数据处理作业,以增加吞吐量和重用性。让我们看看如何从笔记本运行 Spark 作业。

通过 SageMaker 笔记本运行 Spark 作业

一旦笔记本准备就绪,您可以使用sagemaker.processing模块配置 Spark 作业,并使用一组计算资源执行它。SageMaker 提供了PySparkProcessor类,它提供了一个处理 Spark 作业的句柄(sagemaker.readthedocs.io/en/stable/amazon_sagemaker_processing.html#data-processing-with-spark)。其构造函数接受基本的设置详细信息,如作业的名称和 Python 版本。它接受三个参数 - framework_versionpy_versioncontainer_version,这些参数用于固定预构建的 Spark 容器以运行处理作业。可以注册并在image_uri参数上提供自定义映像。image_uri将覆盖framework_versionpy_versioncontainer_version参数:

From sagemaker.processing import PySparkProcessor, ProcessingInput
# ecr image URI
ecr_image_uri = '664544806723.dkr.ecr.eu-central-1.amazonaws.com/linear-learner:latest'
# create PySparkProcessor instance with initial job setup
spark_processor = PySparkProcessor(
    base_job_name="my-sparkjob", # job name
    framework_version="2.4", # tensorflow version
    py_version="py37", # python version
    container_version="1", # container version
    role="myiamrole", # IAM role
    instance_count=2, # ec2 instance count
    instance_type="ml.c5.xlarge", # ec2 instance type
    max_runtime_in_seconds=1200, # maximum run time
    image_uri=ecr_image_uri # ECR image
)

在上述代码中,使用了 PySparkProcessor 类来创建一个 Spark 实例。它接受 base_job_name(作业名称:my-sparkjob)、framework_version(TensorFlow 框架版本:2.0)、py_version(Python 版本:py37)、container_version(容器版本:1)、role(SageMaker 的 IAM 角色:myiamrole)、instance_count(EC2 实例数:2)、instance_type(EC2 实例类型:ml.c5.xlarge)、max_runtime_in_second(超时前的最大运行时间秒数:1200)和 image_url(Docker 镜像的 URL:ecr_image_uri)。

接下来,我们将讨论 PySparkProcessorrun 方法,它通过 Spark 启动提供的脚本:

# input s3 path
path_input = "s3://mybucket/input/"
# output s3 path
path_output = "s3://mybucket/output/"
# run method to execute job
spark_processor.run(
    submit_app="process.py", # processing python script
    arguments=['input', path_input, # input argument for script
               'output', path_output # input argument for script
              ])

在上述代码中,PySparkProcessorrun 方法执行给定的脚本和提供的参数。它接受 submit_app(用 Python 编写的数据处理作业)和参数。在此示例中,我们已经定义了输入数据的位置和输出应存储的位置。

通过 SageMaker 笔记本从自定义容器运行作业

在这一部分中,我们将讨论如何从自定义镜像运行数据处理作业。为此,SageMaker 提供了 sagemaker.processing 模块中的 Processor 类。在本例中,我们将使用 ProcessingInputProcessingOutput 类来分别创建输入和输出对象。这些对象将传递给 Processor 实例的 run 方法。run 方法执行数据处理作业:

# ecr image URI
ecr_image_uri = '664544806723.dkr.ecr.eu-central-1.amazonaws.com/linear-learner:latest'
# input data path
path_data = '/opt/ml/processing/input_data'
# output data path
path_data = '/opt/ml/processing/processed_data'
# s3 path for source
path_source = 's3://mybucket/input'
# s3 path for destination
path_dest = 's3://mybucket/output'
# create Processor instance
processor = Processor(image_uri=ecr_image_uri, # ECR image
               role='myiamrole', # IAM role
               instance_count=1, # instance count
               instance_type="ml.m5.xlarge" # instance type
           )
# calling "run" method of Processor instance
processor.run(inputs=[ProcessingInput(
                 source=path_source, # input source
                 destination=path_data # input destination)],
              outputs=[ProcessingOutput(
                 source=path_data, # output source
                 destination=path_dest # output destination)], ))

在上述代码中,首先创建了一个 Processor 实例。它接受 image_uri(ECR 镜像的 URL 路径:ecr_image_uri)、role(具有访问 ECR 镜像权限的 IAM 角色:myiamrole)、instance_count(EC2 实例计数:1)和 instance_type(EC2 实例类型:ml.m5.xlarge)。Processor 实例的 run 方法可以执行作业。它接受 inputs(作为 ProcessingInput 对象传递的输入数据)和 outputs(作为 ProcessingOutput 对象传递的输出数据)。虽然 Processor 提供了与 PySparkProcessor 类似的一组方法,但主要区别在于 run 函数接受的内容;PySparkProcessor 接受运行 Spark 操作的 Python 脚本,而 Processor 接受支持各种数据处理作业的 Docker 镜像。

对于那些愿意深入了解的人,我们建议阅读 构建自定义处理容器

记住的事情

a. SageMaker 是一个完全托管的基础设施,用于构建、训练和部署 ML 模型。

b. SageMaker 提供一组预定义的开发环境,用户可以根据需要动态更改。

c. SageMaker 笔记本支持通过 sagemaker.processing 模块定义笔记本之外的数据处理作业。

经过对 AWS 中四种最流行的 ETL 工具的介绍,让我们并排比较这四个选项。

比较 AWS 中的 ETL 解决方案

到目前为止,我们已经介绍了四种使用 AWS 设置 ETL 管道的不同方式。在本节中,我们将把这四种设置总结在一张表中(表 5.1)。比较点包括支持无服务器架构、内置调度器的可用性以及支持的 EC2 实例类型的多样性。

支持 单节点 EC2 实例 Glue EMR SageMaker
支持无服务器架构的可用性
内置用于开发人员协作的工作空间的可用性
多种 EC2 实例类型 更多 更少 更多 更多
内置调度器的可用性
内置作业监控 UI 的可用性
内置模型监控的可用性
提供从模型开发到部署的全托管服务
内置可视化器以分析处理过的数据的可用性
内置用于 ETL 逻辑开发的预定义环境的可用性

表 5.1 – 各种数据处理设置的比较 – 单节点 EC2 实例、Glue、EMR 和 SageMaker

正确的设置取决于技术和非技术因素,包括数据来源、数据量、MLOps 的可用性和成本。

需要记住的事情

a. 我们在本章描述的四种 ETL 设置具有独特的优势。

b. 在选择特定设置时,必须考虑各种因素:数据来源、数据量、MLOps 的可用性和成本。

总结

深度学习项目中的一个困难来自于数据量的大小。由于训练深度学习模型需要大量数据,因此数据处理步骤可能会占用大量资源。因此,在本章中,我们学习了如何利用最流行的云服务 AWS 高效处理数千兆字节和百万兆字节的数据。该系统包括调度器、数据存储、数据库、可视化以及用于运行 ETL 逻辑的数据处理工具。

我们额外花费了时间研究 ETL,因为它在数据处理中起着重要作用。我们介绍了 Spark,这是最流行的 ETL 工具,并描述了使用 AWS 设置 ETL 作业的四种不同方式。这四种设置包括使用单节点 EC2 实例、EMR 集群、Glue 和 SageMaker。每种设置都有独特的优势,正确的选择可能因情况而异。这是因为您需要考虑项目的技术和非技术方面。

类似于数据量对数据处理的影响,当训练模型时也会引入多个问题。在下一章中,您将学习如何使用分布式系统高效训练模型。

第六章:高效模型训练

与我们在前一章中扩展数据处理流水线的方式类似,我们可以通过分配更多计算资源来缩短训练深度学习DL)模型所需的时间。在本章中,我们将学习如何配置TensorFlowTF)和PyTorch的训练逻辑,以利用不同机器上多个 CPU 和 GPU 设备。首先,我们将学习 TF 和 PyTorch 如何支持分布式训练,无需任何外部工具。接下来,我们将描述如何利用 SageMaker,因为它专为从云端到端处理 DL 管道而构建。最后,我们将看看专为分布式训练开发的工具:Horovod、Ray 和 Kubeflow。

在本章中,我们将讨论以下主要主题:

  • 在单台机器上训练模型

  • 在集群上训练模型

  • 使用 SageMaker 训练模型

  • 使用 Horovod 训练模型

  • 使用 Ray 训练模型

  • 使用 Kubeflow 训练模型

技术要求

您可以从本书的 GitHub 存储库下载本章的补充材料:https://github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_6。

在单台机器上训练模型

第三章中所述,开发强大的深度学习模型,训练 DL 模型涉及从数据集中提取有意义的模式。当数据集大小较小且模型参数较少时,使用中央处理单元CPU)可能足以训练模型。然而,当使用更大的训练集并且模型包含更多神经元时,DL 模型表现出更好的性能。因此,使用图形处理单元GPU)进行训练已成为标准,因为您可以利用其在矩阵乘法中的大规模并行性。

在 TensorFlow 中利用多个设备进行训练

TF 提供了tf.distribute.Strategy模块,允许您使用多个 GPU 或 CPU 设备进行训练,只需非常简单的代码修改,详见分布式训练tf.distribute.Strategytf.keras.Model.fit完全兼容,以及自定义训练循环,如第三章中的在 TensorFlow 中实现和训练模型部分描述的那样,开发强大的深度学习模型。Keras 的各个组件,包括变量、层、模型、优化器、度量、摘要和检查点,均设计为支持各种tf.distribute.Strategy类,以尽可能简化转向分布式训练。让我们看看tf.distribute.Strategy模块如何使您能够快速修改为多设备上的单机代码集合:

import tensorflow as tf
mirrored_strategy = tf.distribute.MirroredStrategy()
# or 
# mirrored_strategy = tf.distribute.MirroredStrategy(devices=["/gpu:0", "/gpu:1", "/gpu:3"])
# if you want to use only specific devices 
with mirrored_strategy.scope():
    # define your model 
    # …
model.compile(... )
model.fit(... ) 

模型保存后,可以在有或无tf.distribute.Strategy作用域的情况下加载。为了在自定义训练循环中实现分布式训练,您可以参考示例:www.tensorflow.org/tutorials/distribute/custom_training。话虽如此,让我们来回顾一下最常用的策略。我们将涵盖最常见的方法,其中一些超出了单个实例的训练。它们将用于接下来的几节,涵盖在多台机器上进行训练:

  • 提供对tf.keras.Model.fit和自定义训练循环全面支持的策略:

    • MirroredStrategy: 在单台机器上使用多个 GPU 进行同步分布式训练

    • MultiWorkerMirroredStrategy: 在多台机器上进行同步分布式训练(可能使用每台机器上的多个 GPU)。此策略类需要使用已配置TF_CONFIG环境变量的 TF 集群(www.tensorflow.org/guide/distributed_training#TF_CONFIG)。

    • TPUStrategy: 在多个张量处理单元TPU)上进行训练。

  • 具有实验特性的策略(意味着类和方法仍处于开发阶段),适用于tf.keras.Model.fit和自定义训练循环:

    • ParameterServerStrategy: 模型参数在多个工作节点间共享(集群包括工作节点和参数服务器)。每次迭代后,工作节点读取和更新在参数服务器上创建的变量。

    • CentralStorageStrategy: 变量存储在中央存储中,并在每个 GPU 上复制。

  • 我们想要提及的最后一个策略是tf.distribute.OneDeviceStrategy(https://www.tensorflow.org/api_docs/python/tf/distribute/OneDeviceStrategy)。它在单个 GPU 设备上运行训练代码:

    strategy = tf.distribute.OneDeviceStrategy(device="/gpu:0")
    

在上述示例中,我们选择了第一个 GPU("/gpu:0")。

值得一提的是,可以使用tf.distribute.get_strategy函数获取当前的tf.distribute.Strategy对象。您可以使用此函数动态地为您的训练代码更改tf.distribute.Strategy对象,如下面的代码片段所示:

if tf.config.list_physical_devices('GPU'):
    strategy = tf.distribute.MirroredStrategy()
else:  # Use the Default Strategy
    strategy = tf.distribute.get_strategy()

在上述代码中,当 GPU 设备可用时,我们使用tf.distribute.MirroredStrategy,当 GPU 设备不可用时则回退到默认策略。接下来,让我们看一下 PyTorch 提供的功能。

在 PyTorch 中利用多个设备进行训练

要成功训练一个 PyTorch 模型,模型和输入张量需要配置到相同的设备上。如果要使用 GPU 设备,它们需要在训练之前显式地加载到目标 GPU 设备上,可以使用to(device=torch.device('cuda'))cuda()函数:

cpu = torch.device(cpu')
cuda = torch.device('cuda')     # Default CUDA device
cuda0 = torch.device('cuda:0')
x = torch.tensor([1., 2.], device=cuda0)
# x.device is device(type='cuda', index=0)
y = torch.tensor([1., 2.]).cuda()
# y.device is device(type='cuda', index=0)
# transfers a tensor from CPU to GPU 1
a = torch.tensor([1., 2.]).cuda()
# a.device are device(type='cuda', index=1)
# to function of a Tensor instance can be used to move the tensor to different devices
b = torch.tensor([1., 2.]).to(device=cuda)
# b.device are device(type='cuda', index=1)

前述示例展示了在使用 GPU 设备时需要注意的一些关键操作。这是官方 PyTorch 文档中介绍的一部分内容:https://pytorch.org/docs/stable/notes/cuda.html。

然而,为了进行训练设置各个组件可能会很繁琐。因此,Trainergpus参数:

# Train using CPU
Trainer()
# Specify how many GPUs to use
Trainer(gpus=k)
# Specify which GPUs to use
Trainer(gpus=[0, 1])
# To use all available GPUs put -1 or '-1'
Trainer(gpus=-1)

在前述示例中,我们描述了单台机器上的各种训练设置:仅使用 CPU 设备进行训练,使用一组 GPU 设备进行训练以及使用所有 GPU 设备进行训练。

需要记住的事情

a. TF 和 PyTorch 都内置了使用 CPU 和 GPU 设备训练模型的支持。

b. 使用 TF 的tf.distribute.Strategy类可以控制训练过程。在单台机器上训练模型时,可以使用MirroredStrategyOneDeviceStrategy

c. 使用 GPU 设备训练 PyTorch 模型时,需要手动将模型和相关张量加载到同一 GPU 设备上。PL 通过Trainer类处理放置操作,隐藏了大部分样板代码。

在本节中,我们学习了如何在单台机器上利用多个设备。然而,随着单台机器计算能力的限制,已经有很多努力将集群用于训练。

在集群上训练模型

尽管在单台机器上使用多个 GPU 已经大大减少了训练时间,但有些模型仍然非常庞大,需要多天的时间进行训练。增加更多的 GPU 仍然是一种选择,但通常存在物理限制,阻止您充分利用多 GPU 设置的潜力:主板可能仅支持有限数量的 GPU 设备。

幸运的是,许多深度学习框架已经支持在分布式系统上训练模型。尽管在实际实施中存在一些细微差异,但大多数框架都采纳了模型并行数据并行的理念。如下图所示,模型并行将模型的组件分布到多台机器上,而数据并行则将训练集的样本分布到多个设备上:

图 6.1 – 模型并行和数据并行之间的区别

图 6.1 – 模型并行和数据并行之间的区别

在设置分布式系统进行模型训练时,有几个必须注意的细节。首先,集群中的机器需要稳定连接到互联网,因为它们通过网络进行通信。如果不能保证稳定性,集群必须有一种方法来恢复连接问题。理想情况下,分布式系统应该对可用的机器无感知,并且能够在不影响总体进度的情况下添加或删除机器。这样的功能将允许用户动态增加或减少机器数量,以最经济高效的方式进行模型训练。AWS 通过弹性 MapReduce (EMR) 和 弹性容器服务 (ECS) 提供上述功能。

接下来的两个部分,我们将更深入地研究模型并行 ism 和数据并行 ism。

模型并行 ism

在模型并行 ism 的情况下,分布式系统中的每台机器都负责模型的一部分,并管理分配组件的计算。当一个网络太大以至于无法放入单个 GPU 时,通常会考虑这种方法。然而,在实际情况下并不常见,因为 GPU 设备通常有足够的内存来容纳模型,并且设置起来非常复杂。在本节中,我们将描述模型并行 ism 的两种最基本的方法:模型分片 ism模型管道化

模型分片 ism

模型分片 ism 不过是将模型分割成多个计算子图,分布在多个设备上。让我们假设一个简单的基本单层深度神经网络 (DNN) 模型的简单场景(没有并行路径)。模型可以分成几个连续的子图,并且分片配置可以以图形方式表示如下。数据将从带有第一个子图的设备开始顺序流动。每个设备将将计算值传递给下一个子图的设备,直到到达所需的数据为止,设备将保持空闲状态。在此示例中,我们有四个子图:

图 6.2 – 模型分片示例分布图;每个箭头表示一个小批量

图 6.2 – 模型分片示例分布图;每个箭头表示一个小批量

如您所见,模型分片 ism 未充分利用计算资源;设备在等待另一个设备处理其子图。为了解决这个问题,提出了管道化方法。

模型管道化

在模型管道化的情况下,一个小批量被分割成微批次,并按照链式提供给系统,如下图所示:

图 6.3 – 模型管道逻辑图示;每个箭头表示一个小批量

图 6.3 – 模型管道逻辑图示;每个箭头表示一个小批量

然而,模型管道需要反向传播的修改版本。让我们看看如何在模型管道设置中实现单个前向和反向传播。在某些时候,每个设备不仅需要为其子图进行前向计算,还需要进行梯度计算。单个前向和反向传播可以如下实现:

图 6.4 – 模型管道中的单个前向和反向传播

图 6.4 – 模型管道中的单个前向和反向传播

在上图中,我们可以看到每个设备依次运行前向传播和反向传播,以相反的顺序传递计算值给下一个设备。将所有内容汇总在一起,我们得到下面的图表,总结了模型管道的逻辑:

图 6.5 – 基于模型管道的模型并行 ism

图 6.5 – 基于模型管道的模型并行 ism

为了进一步提高训练时间,每个设备都存储其先前计算的值,并在接下来的计算中利用这些值。

在 TensorFlow 中的模型并行 ism

下面的代码片段展示了如何在定义模型架构时将一组层分配给特定设备:

with tf.device('GPU:0'): 
    layer1 = layers.Dense(16, input_dim=8) 
with tf.device('GPU:1'): 
    layer2 = layers.Dense(4, input_dim=16)

如果您想进一步探索 TensorFlow 中的模型并行 ism,我们推荐查看 Mesh TF 存储库(https://github.com/tensorflow/mesh)。

在 PyTorch 中的模型并行 ism

模型并行 ism 仅适用于 PyTorch,并尚未在 PL 中实现。尽管有许多方法可以使用 PyTorch 实现模型并行 ism,但最标准的方法是使用torch.distributed.rpc模块,该模块通过远程过程调用RPC)在机器之间进行通信。基于 RPC 的方法的三个主要特征是远程执行函数或网络(远程执行)、访问和引用远程数据对象(远程引用)以及扩展 PyTorch 跨机器边界的梯度更新功能(分布式梯度更新)。我们将详细信息委托给官方文档https://pytorch.org/docs/stable/rpc.html.

数据并行 ism

数据并行 ism 与模型并行 ism 不同,其目的是通过将数据集分片到集群中的机器来加速训练。每台机器都获得模型的副本,并与其分配的数据集计算梯度。然后,梯度被聚合,并且模型一次全局更新。

在 TensorFlow 中的数据并行 ism

可以通过利用tf.distribute.MultiWorkerMirroredStrategytf.distribute.ParameterServerStrategytf.distribute.CentralStorageStrategy在 TF 中实现数据并行 ism。

我们在 TensorFlow 中利用多个设备进行训练 部分介绍了这些策略,因为特定的 tf.distributed 策略也用于在单个机器内多设备上设置训练。

要使用这些策略,您需要设置一个 TF 集群,其中每台机器可以与其他机器通信。

通常,TF 集群使用 TF_CONFIG 环境变量定义。 TF_CONFIG 只是一个 JSON 字符串,通过定义两个组件来指定集群配置:clustertask。以下 Python 代码展示了如何从 Python 字典生成 TF_CONFIG.json 文件:

tf_config = {
    'cluster': {
        'worker': ['localhost:12345', 'localhost:23456']
    },
    'task': {'type': 'worker', 'index': 0}
}
js_tf = json.dumps(tf_config)
with open("tf_config.json", "w") as outfile:
    outfile.write(js_tf)

关于 TF_CONFIG 的字段和格式,请参阅 https://cloud.google.com/ai-platform/training/docs/distributed-training-details

正如在 TensorFlow 中利用多个设备进行训练 部分演示的,您需要将训练代码放在 tf.distribute.Strategy 范围内。在下面的示例中,我们将展示 tf.distribute.MultiWorkerMirroredStrategy 类的样例用法。

首先,您必须将您的模型实例放在 tf.distribute.MultiWorkerMirroredStrategy 的范围内,如下所示:

strategy = tf.distribute.MultiWorkerMirroredStrategy()
with strategy.scope():
    model = … 

接下来,您需要确保每台机器的 TF_CONFIG 环境变量已正确设置,并运行训练脚本,如下所示:

# On the first node
TF_CONFIG='{"cluster": {"worker": ['localhost:12345', 'localhost:23456']}, "task": {"index": 0, "type": "worker"}}' python training.py
# On the second node
TF_CONFIG='{"cluster": {"worker": ['localhost:12345', 'localhost:23456']}, "task": {"index": 1, "type": "worker"}}' python training.py

要正确保存您的模型,请查看官方文档:https://www.tensorflow.org/tutorials/distribute/multi_worker_with_keras

如果使用自定义训练循环,可以按照 https://www.tensorflow.org/tutorials/distribute/multi_worker_with_ctl 中的说明操作。

PyTorch 中的数据并行

与模型并行不同,数据并行在 PyTorch 和 PL 中都可用。在各种实现中,最标准的功能是 torch.nn.parallel.DistributedDataParallel(DDP)。本节中,我们将主要讨论 PL,因为其主要优势来自于使用数据并行的训练模型的简易性。

要使用数据并行训练模型,您需要修改训练代码以利用底层分布式系统,并使用 torch.distributed.run 模块在每台机器上生成一个进程(pytorch.org/docs/stable/distributed.html)。

下面的代码片段描述了您需要为 ddp 更改的内容。您只需为 Traineraccelerator 参数提供 ddp。当集群中有多台机器时,需要调整 num_nodes 参数:

# train on 8 GPUs (same machine)
trainer = Trainer(gpus=8, accelerator='ddp')
# train on 32 GPUs (4 nodes)
trainer = Trainer(gpus=8, accelerator='ddp', num_nodes=4)

一旦脚本设置完毕,您需要在每台机器上运行以下命令。请记住,MASTER_ADDRMASTER_PORT必须保持一致,因为每个处理器都会使用它们进行通信。另外,NODE_RANK表示机器的索引。换句话说,它必须对每台机器都不同,并且必须从零开始:

python -m torch.distributed.run
    --nnodes=2 # number of nodes you'd like to run with
    --master_addr <MASTER_ADDR>
    --master_port <MASTER_PORT>
    --node_rank <NODE_RANK>
    train.py (--arg1 ... train script args...)

根据官方文档,DDP 的工作原理如下:

  1. 每个节点上的每个 GPU 都会启动一个进程。

  2. 每个进程获取训练集的一个子集。

  3. 每个进程初始化模型。

  4. 每个进程都并行执行前向和后向传播。

  5. 梯度在所有进程之间同步和平均。

  6. 每个进程更新其拥有的模型的权重。

要记住的事情

a. TF 和 PyTorch 提供了使用模型并行 ism 和数据并行 ism 在多台机器上训练 DL 模型的选项。

b. 模型并行将模型分成多个组件,并将它们分布在多台机器上。在 TF 和 PyTorch 中设置模型并行 ism,可以使用Mesh TensorFlow库和torch.distributed.rpc包,分别。

c. 数据并行 ism 将模型复制到每台机器上,并分布小批量数据以进行训练。在 TF 中,可以使用MultiWorkerMirroredStrategyParameterServerStrategyCentralStorageStrategy实现数据并行 ism。PyTorch 中专门用于数据并行 ism 的主要包是torch.nn.parallel.DistributedDataParallel

在本节中,我们学习了如何实现模型训练,其中集群的生命周期得到明确管理。然而,一些工具还管理模型训练的集群。由于它们各自具有不同的优势,您应该理解其差异,以选择适合您开发的正确工具。

首先,我们将查看 SageMaker 的内置功能,以分布式方式训练 DL 模型。

使用 SageMaker 训练模型

正如在第五章在云中利用 SageMaker 进行 ETL部分中提到的,SageMaker 的动机是帮助工程师和研究人员专注于开发高质量的 DL 流水线,而无需担心基础设施管理。SageMaker 为您管理数据存储和计算资源,使您可以利用分布式系统轻松进行模型训练。此外,SageMaker 支持向模型流式传输数据以进行推断、超参数调整和跟踪实验和工件。

SageMaker Studio 是您定义模型逻辑的地方。SageMaker Studio 笔记本允许您快速探索可用数据并设置模型训练逻辑。当模型训练时间过长时,通过对基础架构配置进行少量修改,可以有效地实现扩展使用多个计算资源和找到最佳超参数集。此外,SageMaker 支持在分布式系统上进行超参数调整以利用并行性。

尽管 SageMaker 听起来像深度学习流水线的魔法钥匙,但也有其不足之处。首先是其成本。分配给 SageMaker 的实例比等效的 EC2 实例贵约 40%。其次,您可能会发现并非所有库都在笔记本中可用。换句话说,您可能需要额外的时间来构建和安装所需的库。

设置 SageMaker 的模型训练

现在,你应该能够启动一个笔记本并选择一个预定义的开发环境,因为我们在第五章利用 SageMaker 进行 ETL部分已经涵盖了这些内容。假设您已经处理了原始数据并将处理后的数据存储在数据存储中,我们将在本节中专注于模型训练。使用 SageMaker 进行模型训练可以总结为以下三个步骤:

  1. 如果存储中的处理数据尚未分割为训练、验证和测试集,则必须首先对其进行分割。

  2. 您需要定义模型训练逻辑并指定集群配置。

  3. 最后,您需要训练您的模型并将生成的结果保存回数据存储中。当训练完成时,分配的实例将自动终止。

使用 SageMaker 进行模型训练的关键是sagemaker.estimator.Estimator。它允许您配置训练设置,包括基础架构设置、要使用的 Docker 镜像类型和超参数。以下是您通常会配置的主要参数:

  • role (str): AWS IAM 角色

  • instance_count (int): 用于训练的 SageMaker EC2 实例数量

  • instance_type (str): 用于训练的 SageMaker EC2 实例类型

  • volume_size (int): 用于临时下载训练输入数据的 Amazon 弹性块存储EBS)卷的大小(以 GB 为单位)

  • output_path (str): 训练结果将存储在的 S3 对象

  • use_spot_instances (bool): 指定是否使用 SageMaker 管理的 AWS Spot 实例进行训练的标志

  • checkpoint_s3_uri (str): 训练期间将检查点存储在的 S3 URI

  • hyperparameters (dict): 包含初始超参数集的字典

  • entry_point (str): 运行的 Python 文件路径

  • dependencies (list[str]): 将加载到作业中的目录列表

只要您从 Amazon 弹性容器注册表 (ECR)中选择正确的容器,您就可以为 SageMaker 设置任何训练配置。还存在具有不同 CPU 和 GPU 设备配置的容器。您可以在 http://github.com/aws/deep-learning-containers/blob/master/available_images.md 中找到这些信息。

另外,还存在开源工具包的存储库,旨在帮助在 Amazon SageMaker 上进行 TF 和 PyTorch 模型训练。这些存储库还包含已经安装了必要库(如 TF、PyTorch 和其他构建 SageMaker images 所需的依赖项的 Docker 文件:

最后,我们想提一下,您可以在本地机器上构建和运行容器。如果需要,您还可以更新安装的库。如果进行任何修改,需要将修改后的容器上传到 Amazon ECR,然后才能在sagemaker.estimator.Estimator中使用它。

在接下来的两个部分中,我们将描述训练 TF 和 PyTorch 模型所需的一系列更改。

使用 SageMaker 训练 TensorFlow 模型

SageMaker 为 TF 提供了一个sagemaker.estimator.Estimator类:sagemaker.tensorflow.estimator.TensorFlow (sagemaker.readthedocs.io/en/stable/frameworks/tensorflow/sagemaker.tensorflow.html).

以下示例展示了您需要使用sagemaker.tensorflow.estimator.TensorFlow类编写的包装脚本,以在 SageMaker 上训练 TF 模型:

import sagemaker
from sagemaker.tensorflow import TensorFlow
# Initializes SageMaker session
sagemaker_session = sagemaker.Session()
bucket = 's3://dataset/'
tf_estimator = TensorFlow(entry_point='training_script.py', 
              source_dir='.',
              role=sagemaker.get_execution_role(),
              instance_count=1, 
              instance_type='ml.c5.18xlarge',
              framework_version=tf_version, 
              py_version='py3',
              script_mode=True,
              hyperparameters={'epochs': 30} )

请记住,在训练脚本(train_script.py)的ArgumentParser中,hyperparameters参数的每个键必须有相应的条目定义。在上述示例中,我们仅定义了 epochs ('epochs': 30)。

要启动训练,您需要调用fit函数。如果您的数据集在 S3 桶上,fit函数将如下所示:

tf_estimator.fit({'training': 's3://bucket/training',
                  'validation': 's3://bucket/validation'})   

前面的示例将在由 source_dir 提供的目录中运行 entry_point 参数指定的 training_script.py。实例的详细信息可以在 instance_countinstance_type 参数中找到。训练脚本将在 fit 函数中定义的训练和验证数据集上使用 tf_estimatorhyperparameters 进行运行。

使用 SageMaker 训练 PyTorch 模型

类似于 sagemaker.tensorflow.estimator.TensorFlow,存在 sagemaker.pytorch.PyTorch链接)。您可以按照《在 PyTorch 中实现和训练模型》一节中的描述设置 PyTorch(或 PL)模型的训练,并集成 sagemaker.pytorch.PyTorch,如下面的代码片段所示:

import sagemaker
from sagemaker.pytorch import PyTorch
# Initializes SageMaker session
sagemaker_session = sagemaker.Session()
bucket = 's3://dataset/'
pytorch_estimator = PyTorch(
                      entry_point='train.py',
                      source_dir='.',
                      role=sagemaker.get_execution_role(),
                      framework_version='1.10.0',
                      train_instance_count=1,
                      train_instance_type='ml.c5.18xlarge',
                      hyperparameters={'epochs': 6})
…
pytorch_estimator.fit({
                        'training': bucket+'/training',
                        'validation': bucket+'/validation'})   

使用 PyTorch 估算器的方法与前一节描述的 TF 估算器相同。

这就完成了使用 SageMaker 进行模型训练的基本用法。接下来,我们将学习如何在 SageMaker 中扩展训练作业。我们将讨论使用分布策略进行分布式训练。我们还将介绍如何通过使用具有更低延迟的其他数据存储服务来加快训练速度。

使用 SageMaker 以分布方式训练模型

在 SageMaker 中可以通过使用分布式数据并行库(链接)实现数据并行。

您所需做的就是在创建 sagemaker.estimator.Estimator 实例时启用 dataparallel,如下所示:

distribution = {"smdistributed": {"dataparallel": { "enabled": True}} 

下面的代码片段展示了使用 dataparallel 创建的 TF 估算器。详细信息可以在 https://docs.aws.amazon.com/en_jp/sagemaker/latest/dg/data-parallel-use-api.html 找到:

tf_estimator = TensorFlow(
                 entry_point='training_script.py', 
                 source_dir='.',
                 role=sagemaker.get_execution_role(),
                 instance_count=4, 
                 instance_type='ml.c5.18xlarge',
                 framework_version=tf_version, 
                 py_version='py3',
                 script_mode=True,
                 hyperparameters={'epochs': 30}
                 distributions={'smdistributed':
                 "dataparallel": {"enabled": True}})

对于 PyTorch 估算器,需要进行相同的修改。

SageMaker 支持两种不同的机制来将输入数据传输给底层算法:文件模式和管道模式。默认情况下,SageMaker 使用文件模式,将输入数据下载到用于训练的 EBS 卷中。但是,如果数据量很大,这可能会减慢训练速度。在这种情况下,您可以使用管道模式,它会从 S3(使用 Linux FIFO)流式传输数据,而无需进行额外的复制。

在 TF 的情况下,您可以简单地从 sagemaker-tensorflow 扩展中使用 PipeModeDataset,如 https://github.com/aws/sagemaker-tensorflow-extensions 所示:

from sagemaker_tensorflow import PipeModeDataset
ds = PipeModeDataset(channel='training', record_format='TFRecord') 

然而,使用管道模式训练 PyTorch 模型需要更多的工程化努力。因此,我们将为您指引一个笔记本示例,深入描述每个步骤:github.com/aws/amazon-sagemaker-examples/blob/main/advanced_functionality/pipe_bring_your_own/pipe_bring_your_own.ipynb

分布策略和管道模式应该通过扩展底层计算资源和提高数据传输吞吐量来加速训练。然而,如果它们不足以满足需求,您可以尝试利用另外两种与 SageMaker 兼容的更高效的数据存储服务:亚马逊弹性文件系统EFS)和亚马逊完全托管的共享存储FSx),它是为 Lustre 文件系统而构建的。有关更多详细信息,请分别参阅它们的官方页面:aws.amazon.com/efs/aws.amazon.com/fsx/lustre/

SageMaker 与 Horovod

SageMaker 分布式训练的另一选择是使用Horovod,这是一个基于消息传递接口MPI)原理的免费开源框架,用于分布式 DL 训练。MPI 是一种标准消息传递库,在并行计算架构中被广泛使用。Horovod 假设 MPI 用于工作节点的发现和减少协调。Horovod 还可以利用 Gloo 替代 MPI,这是一个开源的集体通信库。这里是为 Horovod 配置的分布参数示例:

distribution={"mpi": {"enabled":True, 
                        "processes_per_host":2 }}

在前面的代码片段中,我们使用 MPI 实现了机器之间的协调。processes_per_host定义了在每个实例上运行的进程数量。这相当于在 MPI 和 Horovod 中使用-H参数定义进程数量,以控制程序在 MPI 和 Horovod 中的执行。

在下面的代码片段中,我们选择了控制训练脚本执行数量的并行进程数(-np参数)。然后,使用指定的-H参数值将此数目分配到具体的机器上。使用以下命令,每台机器将运行两次train.py。这在每台有两个 GPU 的四台机器的典型设置中是典型的。分配给-H进程的总和不能超过-np值:

mpirun -np 8 -H server1:2,server2:2,server3:2,server4:2 … (other parameters) python train.py  

我们将在下一节中深入讨论 Horovod,讲述如何在由 EC2 实例组成的独立 Horovod 集群上训练 DL 模型。

记住的事情

a. SageMaker 提供了一个优秀的工具,SageMaker Studio,允许您快速进行初始数据探索和训练基线模型。

b. sagemaker.estimator.Estimator对象是使用 SageMaker 训练模型的重要组件。它还支持在一组具有不同 CPU 和 GPU 配置的机器上进行分布式训练。

c. 使用 SageMaker 进行 TF 和 PyTorch 模型训练可以通过专为每种框架设计的估算器来实现。

现在,让我们看看如何在没有 SageMaker 的情况下使用 Horovod 进行分布式模型训练。

使用 Horovod 训练模型

尽管我们在介绍 SageMaker 时介绍了 Horovod,但 Horovod 设计用于仅支持分布式训练(https://horovod.ai/)。它旨在通过为流行的 DL 框架(包括 TensorFlow 和 PyTorch)提供良好的集成来以简单的方式支持分布式训练。

正如在SageMaker 与 Horovod部分中提到的,Horovod 的核心原则基于 MPI 概念,例如 size、rank、local rank、allreduce、allgather、broadcast 和 alltoall(https://horovod.readthedocs.io/en/stable/concepts.html)。

在本节中,我们将学习如何使用 EC2 实例设置 Horovod 集群。然后,我们将描述在 TF 和 PyTorch 脚本中进行的修改,以在 Horovod 集群上训练模型所需的步骤。

设置 Horovod 集群

要使用 EC2 实例设置 Horovod 集群,您必须按照以下步骤进行:

  1. 转到 EC2 实例控制台:https://console.aws.amazon.com/ec2/。

  2. 点击右上角的启动实例按钮。

  3. 选择安装了 TF、PyTorch 和 Horovod 的Deep Learning AMI(Amazon Machine Image)。点击右下角的下一步…按钮。

  4. 为您的训练选择正确的实例类型。您可以选择符合需求的 CPU 或 GPU 实例类型。点击右下角的下一步…按钮:

![图 6.6 – EC2 实例控制台中的实例类型选择图像 B18522_06_06.jpg

图 6.6 – EC2 实例控制台中的实例类型选择

  1. 选择您希望组成 Horovod 集群的实例数量。在这里,您还可以请求 AWS Spot 实例(基于稀疏 EC2 容量的廉价实例,可以被中断,因此仅适合容错任务)。但为简单起见,让我们使用按需资源。

  2. 选择适当的网络和子网设置。在实际操作中,这些信息将由 DevOps 部门提供。

  3. 在同一页上,选择添加实例到放置组添加到新的放置组,输入您想要用于组的名称,并选择cluster作为放置组策略

  4. 在同一页上,提供您的身份和访问管理(IAM)角色,以便您可以访问 S3 存储桶。点击右下角的下一步…按钮。

  5. 为您的实例选择正确的存储大小。在右下角的下一步……按钮。

  6. 为您的实例选择唯一的标签(https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html)。在实际情况中,这些标签可能被用作额外的安全措施,例如使用特定标签终止实例。点击右下角的下一步……按钮。

  7. 创建安全组或选择现有的安全组。同样,您必须与 DevOps 部门沟通以获取适当的信息。在右下角的下一步……按钮。

  8. 审查所有信息并启动。您将被要求提供用于认证的隐私增强邮件PEM)密钥。

完成这些步骤后,所需数量的实例将启动。如果您在步骤 10中没有添加名称标签,则您的实例将没有任何名称。在这种情况下,您可以导航到 EC2 实例控制台并手动更新名称。在写作时,您可以请求称为弹性 IP 的静态 IPv4 地址并将其分配给您的实例(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html)。

最后,确保实例能够无问题地相互通信。您应检查安全组设置,并根据需要为 SSH 和其他流量添加入站规则。

此时,您只需将本地计算机上的 PEM 密钥复制到主 EC2 实例即可。对于 Ubuntu AMI,您可以运行以下命令:

scp -i <your_pem_key_path> ubuntu@<IPv4_Public_IP>:/home/ubuntu/.ssh/ 

现在,您可以使用 SSH 连接到主 EC2 实例。接下来要做的是通过以下命令在 EC2 实例之间设置无密码连接,并在 SSH 命令中提供您的 PEM 密钥:

eval 'ssh-agent'
ssh-add <your_pem_key>

在上述代码片段中,eval命令设置由ssh-agent命令提供的环境变量,而ssh-add命令将 PEM 身份添加到认证代理中。

现在,集群已准备好支持 Horovod!完成后,必须在 Web 控制台上停止或终止集群。否则,将持续收取资源费用。

在接下来的两个部分中,我们将学习如何更改 Horovod 的 TF 和 PyTorch 训练脚本。

为 Horovod 配置 TensorFlow 训练脚本

要使用 Horovod 训练 TF 模型,您需要horovod.tensorflow.keras模块。首先,您需要导入tensorflowhorovod.tensorflow.keras模块。我们将horovod.tensorflow.keras称为hvd。然后,您需要按以下方式初始化 Horovod 集群:

import tensorflow as tf
import horovod.tensorflow.keras as hvd
# Initialize Horovod
hvd.init()

此时,您可以使用hvd.size函数检查集群的大小。Horovod 中的每个进程将被分配一个等级(从 0 到集群大小,即您要运行的进程或要使用的设备的数量),您可以通过hvd.rank函数访问此等级。在每个实例上,每个进程都被分配一个不同的编号,从 0 到该实例上的进程数,称为本地等级(每个实例唯一的数字,但在不同实例之间重复)。可以使用hvd.local_rank函数访问当前进程的本地等级。

您可以使用本地排名来为每个进程固定特定的 GPU 设备。此示例还展示了如何使用tf.config.experimental.set_memory_growth设置 GPU 的内存增长:

gpus = tf.config.experimental.list_physical_devices('GPU')
for gpu in gpus:
    tf.config.experimental.set_memory_growth(gpu, True)
if gpus:
    tf.config.experimental.set_visible_devices(gpus[hvd.local_rank()], 'GPU')

在下面的代码中,我们根据等级来拆分数据,以便每个进程在不同的示例集上训练:

dataset = np.array_split(dataset, hvd.size())[hvd.rank()]

对于模型架构,您可以按照第三章中的在 TensorFlow 中实现和训练模型部分的说明进行操作:

model = …

接下来,您需要配置优化器。在以下示例中,学习率将按照 Horovod 大小进行缩放。此外,优化器需要用 Horovod 优化器包装:

opt = tf.optimizers.Adam(0.001 * hvd.size())
opt = hvd.DistributedOptimizer(opt)

下一步是编译您的模型,并将网络架构定义和优化器放在一起。当您使用早于 v2.2 的 TF 版本调用compile函数时,您需要禁用experimental_run_tf_function,以便 TF 使用hvd.DistributedOptimizer来计算梯度:

model.compile(loss=tf.losses.SparseCategoricalCrossentropy(),
              optimizer=opt,
              metrics=['accuracy'],
              experimental_run_tf_function=False)

您需要配置的另一个组件是回调函数。您需要添加hvd.callbacks.BroadcastGlobalVariablesCallback(0)。这将从等级 0 向所有其他机器和进程广播权重和偏差的初始值。这是确保一致的初始化或正确从检查点恢复训练所必需的:

callbacks=[
    hvd.callbacks.BroadcastGlobalVariablesCallback(0)
]

使用rank可以在特定实例上执行特定操作。例如,通过检查rank是否为 0 (hvd.rank()==0),可以在主节点上记录和保存工件,如下面的代码片段所示:

# Save checkpoints only on the instance with rank 0 to prevent other workers from corrupting them.
If hvd.rank()==0:
    callbacks.append(keras.callbacks.ModelCheckpoint('./checkpoint-{epoch}.h5'))

现在,您可以触发fit函数。以下示例显示了如何使用 Horovod 集群的大小来缩放每个 epoch 的步数。fit函数的消息只会在主节点上可见:

if hvd.rank()==0:
    ver = 1
else:
    ver = 0
model.fit(dataset,
          steps_per_epoch=hvd.size(),
          callbacks=callbacks,
          epochs=num_epochs,
          verbose=ver)

这是您需要更改以在分布式Horovod 中训练 TF 模型的所有内容。您可以在 https://horovod.rea找到完整示例. Keras 版本可在 https://horovod.readthedocs.io/en/stable/keras.html 找到。此外,您可以修改您的训练脚本,使其以容错方式运行:https://horovod.readthedocs.io/en/stable/elastic_include.html。通过此更改,您应该能够使用 AWS Spot 实例并显著降低培训成本。

配置用于 Horovod 的 PyTorch 训练脚本

不幸的是,PL 目前尚无适当的 Horovod 支持文档。因此,在本节中,我们将专注于 PyTorch。与前一节中描述的类似,我们将演示您为 PyTorch 训练脚本进行的代码更改。对于 PyTorch,您需要horovod.torch模块,我们将再次称之为hvd。在以下代码片段中,我们正在导入必要的模块并初始化集群:

import torch
import horovod.torch as hvd
# Initialize Horovod
hvd.init()

如 TF 示例所述,您需要使用本地排名为当前进程绑定 GPU 设备:

torch.cuda.set_device(hvd.local_rank())

训练脚本的其余部分需要进行类似的修改。数据集需要使用torch.utils.data.distributed.DistributedSampler在实例之间分发,并且优化器必须使用hvd.DistributedOptimizer进行包装。主要区别在于hvd.broadcast_parameters(model.state_dict(), root_rank=0),用于广播模型权重。您可以在以下代码片段中找到详细信息:

# Define dataset...
train_dataset = ...
# Partition dataset among workers using DistributedSampler
train_sampler = torch.utils.data.distributed.DistributedSampler(
    train_dataset, num_replicas=hvd.size(), rank=hvd.rank())
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=..., sampler=train_sampler)
# Build model...
model = ...
model.cuda()
optimizer = optim.SGD(model.parameters())
# Add Horovod Distributed Optimizer
optimizer = hvd.DistributedOptimizer(optimizer, named_parameters=model.named_parameters())
# Broadcast parameters from rank 0 to all other processes.
hvd.broadcast_parameters(model.state_dict(), root_rank=0)

现在,您已经准备好训练模型了。训练循环不需要任何修改。您只需将输入张量传递给模型,并通过触发lossbackward函数和optimizerstep函数来触发反向传播。以下代码片段描述了训练逻辑的主要部分:

for epoch in range(num_ephos):
   for batch_idx, (data, target) in enumerate(train_loader):
       optimizer.zero_grad()
       output = model(data)
       loss = F.nll_loss(output, target)
       loss.backward()
       optimizer.step()

完整描述可以在官方 Horovod 文档页面找到:https://horovod.readthedocs.io/en/stable/pytorch.html。

作为使用 Horovod 训练模型部分的最后一个内容,接下来的部分将解释如何使用horovodrunmpirun命令启动模型训练过程。

在 Horovod 集群上训练 DL 模型

Horovod 使用 MPI 原则协调进程之间的工作。要在单台机器上运行四个进程,可以使用以下其中一个命令:

horovodrun -np 4 -H localhost:4 python train.py
mpirun -np 4 python train.py

在两种情况下,-np参数定义了train.py脚本并行运行的次数。-H参数可用于定义每台机器上的进程数(请参见上述示例中的horovodrun命令)。随着我们学习如何在单台机器上运行,可以省略-H,如在mpirun命令中所示。其他mpirun参数在 https://www.open-mpi.org/doc/v4.0/man1/mpirun.1.php#sect6 中描述。

如果未安装 MPI,可以使用 Gloo 运行horovodrun命令。要在localhost上使用 Gloo 运行相同脚本四次(四个进程),只需添加--gloo标志:

horovodrun --gloo -np 4 -H localhost:4 python train.py

扩展到多个实例非常简单。以下命令展示了如何使用horovodrun在四台机器上运行训练脚本:

horovodrun -np 4 -H server1:1,server2:1,server3:1,server4:1 python train.py 

以下命令展示了如何使用mpirun在四台机器上运行训练脚本:

mpirun -np 4 -H server1:1,server2:1,server3:1,server4:1 python train.py

当主节点触发上述任一命令后,您将看到每个实例运行一个训练进程。

需要记住的事情

a. 要使用 Horovod,您需要一个具有节点间开放交叉通信的集群。

b. Horovod 提供了一种简单有效的方法,用于实现 TF 和 PyTorch 的数据并行。

c. 可以使用horovodrunmpirun命令在 Horovod 集群上执行训练脚本。

在下一节中,我们将描述 Ray,另一个流行的用于分布式训练的框架。

使用 Ray 训练模型

Ray 是一个开源的执行框架,用于跨多台机器扩展 Python 工作负载(https://www.ray.io)。Ray 支持以下 Python 工作负载:

Ray 的关键优势在于其集群定义的简单性;您可以定义具有不同类型和来源的机器的集群。例如,Ray 允许您通过混合 AWS EC2 按需实例和具有不同 CPU 和 GPU 配置的 EC2 Spot 实例来构建实例群集(基于每个节点的灵活和弹性资源策略),从而简化了集群的创建和与 DL 框架的集成,使其成为分布式 DL 模型训练过程的有效工具。

首先,我们将学习如何设置 Ray 集群。

设置 Ray 集群

您可以通过两种方式设置 Ray 集群:

  • Ray 集群启动器:Ray 提供的工具,用于利用云服务(包括 AWS、GCP 和 Azure)的实例构建集群。

  • 手动集群构建:所有节点都需要手动连接到 Ray 集群。

Ray 集群包括一个头节点(主节点)和工作节点。形成集群的实例应配置为通过网络互相通信。Ray 实例之间的通信基于传输控制协议TCP)连接,必须打开相应的端口。在接下来的两个部分中,我们将更详细地介绍 Ray 集群启动器和手动集群构建。

使用 Ray Cluster Launcher 设置 Ray 集群

使用 Ray 集群启动器时,需要使用 YAML 文件来配置集群。您可以在 Ray 的 GitHub 仓库中找到多个用于不同配置的示例 YAML 文件:github.com/ray-project/ray/tree/master/python/ray/autoscaler

在本节中,我们将介绍最基本的配置。YAML 文件从集群的基本信息开始,例如集群名称、最大工作节点数和扩展速度,如下所示:

cluster_name: BookDL
max_workers: 5
upscaling_speed: 1.0

接下来,它配置云服务提供商:

provider:
    type: aws
    region: us-east-1
    availability_zone: us-east-1c, us-east-1b, us-east-1a
    cache_stopped_nodes: True 
    ssh_user: ubuntu
    ssh_private_key: /Users/BookDL/.ssh/BookDL.pem

在上述示例中,我们指定了提供者类型(type: aws),并选择将提供实例的区域和可用区(region: us-east-1availability_zone: us-east-1c, us-east-1b, us-east-1a)。然后,我们定义节点在未来是否可以重复使用(cache_stopped_nodes: True)。最后的配置是用户认证(ssh_user:ubuntussh_private_key:/Users/BookDL/.ssh/BookDL.pem)。

接下来需要指定节点配置。首先,我们将从头节点开始:

available_node_types:
    ray.head.default:
        node_config:
            KeyName:"BookDL.pem"

接下来,我们必须设置安全设置。详细设置必须与 DevOps 协商,以监控和保护实例:

            SecurityGroupIds:
                - sg-XXXXX
                - sg-XXXXX
            SubnetIds: [subnet-XXXXX]

下面的配置适用于应使用的实例类型和 AMI:

            InstanceType: m5.8xlarge
            ImageId: ami-09ac68f361e5f4a13

在以下代码片段中,我们提供了存储配置:

            BlockDeviceMappings:
                  - DeviceName: /dev/sda1
                    Ebs:
                    VolumeSize: 580

您可以按如下方式轻松定义 Tags

            TagSpecifications:
                - ResourceType:"instance"
                  Tags:
                      - Key:"Developer"
                        Value:"BookDL"

如果需要,可以为访问特定 S3 存储桶提供 IAM 实例配置文件:

            IamInstanceProfile:
                Arn:arn:aws:iam::XXXXX

YAML 文件的下一个部分,我们需要为工作节点提供配置:

    ray.worker.default:
            min_workers: 2
            max_workers: 4

首先,我们必须指定工作节点的数量(min_workersmax_workers)。然后,我们可以定义类似于我们定义主节点配置的节点配置:

        node_config:
            KeyName: "BookDL.pem"
            SecurityGroupIds:
                - sg-XXXXX
                - sg-XXXXX
            SubnetIds: [subnet-XXXXX]
            InstanceType: p2.8xlarge
            ImageId: ami-09ac68f361e5f4a13
            TagSpecifications:
                - ResourceType: "instance"
                  Tags:
                      - Key: "Developer"
                        Value: "BookDL"
            IamInstanceProfile:
                Arn: arn:aws:iam::XXXXX
            BlockDeviceMappings:
              - DeviceName: /dev/sda1
                Ebs:
                  VolumeSize: 120

此外,您可以在 YAML 文件中指定要在每个节点上运行的一系列 shell 命令:

setup_commands:
    - (stat $HOME/anaconda3/envs/tensorflow2_p38/ &> /dev/null && echo 'export PATH="$HOME/anaconda3/envs/tensorflow2_p38/bin:$PATH"' >> ~/.bashrc) || true
    - source activate tensorflow2_p38 && pip install --upgrade pip
    - pip install awscli
    - pip install Cython
    - pip install -U ray
    - pip install -U ray[rllib] ray[tune] ray
    - pip install mlflow
    - pip install dvc

在这个例子中,我们将在路径中添加tensorflow2_p38以供conda环境使用,激活环境,并使用pip安装一些其他模块。如果您想在头节点或工作节点上运行其他命令,可以分别在head_setup_commandsworker_setup_commands中指定它们。它们将在setup_commands中定义的命令执行后执行。

最后,YAML 文件以启动 Ray 集群的命令结束:

head_start_ray_commands:
    - ray stop
    - source activate tensorflow2_p38 && ray stop
    - ulimit -n 65536; source activate tensorflow2_p38 && ray start --head --port=6379 --object-manager-port=8076 --autoscaling-config=~/ray_bootstrap_config.yaml
worker_start_ray_commands:      
    - ray stop
    - source activate tensorflow2_p38 && ray stop
    - ulimit -n 65536; source activate tensorflow2_p38 && ray start --address=$RAY_HEAD_IP:6379 --object-manager-port=8076

起初,使用 YAML 文件设置 Ray 集群可能看起来很复杂。但是,一旦您习惯了,您会发现为未来项目调整集群设置变得相当简单。此外,它显著减少了启动正确定义的集群所需的时间,因为您可以重用来自先前项目的安全组、子网、标签和 IAM 配置信息。

如果您需要其他详细信息,我们建议您花些时间查阅官方文档:https://docs.ray.io/en/latest/cluster/config.html#cluster-config。

值得一提的是,Ray 集群启动器支持自动缩放和使用实例群,无论是否使用 EC2 Spot 实例。我们在前面的示例中使用了 AMI,但您也可以为您的实例提供特定的 Docker 镜像。通过灵活使用 YAML 配置文件,您可以使用单个文件构建任何集群配置。

正如我们在本节开头提到的,您还可以通过手动添加单独实例来设置 Ray 集群。接下来我们将看看这个选项。

手动设置 Ray 集群

鉴于您拥有一组具有网络连接的机器,第一步是在每台机器上安装 Ray。接下来,您需要更改每台机器的安全设置,以便它们可以相互通信。之后,您需要选择一个节点作为头节点,并在其上运行以下命令:

ray start --head --redis-port=6379  

上述命令建立了 Ray 集群;Redis 服务器(用于集中控制平面)已启动,并且其 IP 地址在终端上打印出来(例如123.45.67.89:6379)。

接下来,您需要在所有其他节点上运行以下命令:

ray start --address=<redis server ip address>

您需要提供的地址是从头节点命令打印出的地址。

现在,您的机器已准备好支持 Ray 应用程序。在手动设置的情况下,需要手动执行以下步骤:启动机器,连接到头节点终端,将训练文件复制到所有节点,并停止机器。让我们看看如何利用 Ray 集群启动器来帮助完成这些任务。

在这个阶段,您应该能够使用 YAML 文件指定所需的 Ray 集群设置。一旦准备好,您可以使用以下命令启动您的第一个 Ray 集群:

ray up your_cluster_setting_file.yaml

要在头节点上获取远程终端,您可以运行以下命令:

ray attach your_cluster_setting_file.yaml

要终止集群,可以使用以下命令:

ray down your_cluster_setting_file.yaml

现在,是时候学习如何在 Ray 集群上执行 DL 模型训练了。

使用 Ray 进行分布式模型训练

Ray 提供 Ray Train 库,通过处理幕后的分布式训练来帮助您专注于定义训练逻辑。Ray Train 支持 TF 和 PyTorch。此外,Ray Datasets 存在,通过分布式数据转换提供分布式数据加载。最后,Ray 通过 Ray Tune 库提供超参数调整功能。

调整 TF 训练逻辑以适应 Ray 类似于我们在 TensorFlow 中的数据并行 ism 部分中描述的方式。主要区别来自 Ray Train 库,它帮助我们设置 TF_CONFIG

调整后的训练逻辑如下所示:

def train_func_distributed():
    per_worker_batch_size = 64
    tf_config = json.loads(os.environ['TF_CONFIG'])
    num_workers = len(tf_config['cluster']['worker'])
    strategy = tf.distribute.MultiWorkerMirroredStrategy()
    global_batch_size = per_worker_batch_size * num_workers
    multi_worker_dataset = dataset(global_batch_size)
    with strategy.scope():
        multi_worker_model = build_and_compile_your_model()
    multi_worker_model.fit(multi_worker_dataset, epochs=20, steps_per_epoch=50)

然后,您可以使用 Ray Trainer 运行训练,如下所示:

import ray
from ray.train import Trainer
ray.init()
trainer = Trainer(backend="tensorflow", num_workers=4, use_gpu=True)
trainer.start()
trainer.run(train_func_distributed)
trainer.shutdown()

在前面的例子中,模型定义与单设备案例类似,唯一的区别在于应使用特定策略进行编译:MultiWorkerMirroredStrategy。数据集在 dataset 函数内部分割,为每个工作节点提供不同的样本集。最后,Trainer 实例处理分布式训练。

使用 Ray 训练 PyTorch 模型 可以通过少量变更来实现。一些示例在 docs.ray.io/en/latest/train/examples.html#pytorch 上呈现。

此外,您还可以将 Ray 与 Horovod 结合使用,利用 Elastic Horovod 以容错的方式进行训练。Ray 将通过简化主机的发现和 orc 串来自动缩放训练过程。我们不会详细介绍,但您可以在 docs.ray.io/en/latest/train/examples/horovod/horovod_example.html 找到一个很好的起点。

需要记住的事情

a. Ray 的主要优势在于其集群定义的简易性。

b. Ray 集群可以通过手动连接每台机器或使用名为 Ray Cluster Launcher 的内置工具来创建。

c. Ray 提供了良好的支持来自动缩放训练过程。它简化了主机的发现和编排。

最后,让我们学习如何使用 Kubeflow 进行分布式训练。

使用 Kubeflow 训练模型

Kubeflow (www.kubeflow.org) 涵盖了模型开发的每个步骤,包括数据探索、预处理、特征提取、模型训练、模型服务、推断和版本控制。通过利用容器和 Kubernetes,Kubeflow 允许您轻松从本地开发环境扩展到生产集群。

如果您的组织已经在使用 Kubernetes生态系统,那么 Kubeflow 可能是您进行分布式训练的首选。

介绍 Kubernetes

Kubernetes 是一个开源的编排平台,用于管理容器化工作负载和服务(kubernetes.io):

  • Kubernetes 有助于持续交付、集成和部署。

  • 它将开发环境与部署环境分离。您可以构建一个容器镜像并同时开发应用程序。

  • 基于容器的方法确保了开发、测试和生产环境的一致性。环境将在桌面计算机或云中保持一致,从而最大限度地减少从一步到另一步所需的修改。

我们假设您已经安装了 Kubeflow 及其所有依赖项,并且运行着一个 Kubernetes 集群。本节中我们将描述的步骤是通用的,适用于任何集群设置 - Minikube(一个本地版本的 Kubernetes)、AWS 弹性 Kubernetes 服务EKS),或者一个多节点的集群。这正是容器化工作负载和服务的美妙之处。您可以在以下网址找到本地 Minikube 安装步骤:minikube.sigs.k8s.io/docs/start/。对于 EKS,我们建议您查阅 AWS 用户指南:docs.aws.amazon.com/eks/latest/userguide/getting-started.html

为 Kubeflow 设置模型训练

第一步是将您的训练代码打包成一个容器。这可以通过一个 Docker 文件实现。取决于您的起始点,您可以使用来自 NVIDIA 容器镜像空间的容器(TF 位于 https://docs.nvidia.com/deeplearning/frameworks/tensorflow-release-notes/running.html 或 PyTorch 在 https://docs.nvidia.com/deeplearning/frameworks/pytorch-release-notes/index.html)或直接从 DL 框架获取容器(TF 位于 https://hub.docker.com/r/tensorflow/tensorflow 或 PyTorch 位于 https://hub.docker.com/r/pytorch/pytorch)。

让我们看一个 TF Docker 文件的示例(kubeflow/tf_example_job):

FROM tensorflow/tensorflow:latest-gpu-jupyter
RUN pip install minio –upgrade
RUN pip install –upgrade pip 
RUN pip install pandas –upgrade 
… 
RUN mkdir -p /opt/kubeflow
COPY train.py /opt/kubeflow
ENTRYPOINT ["python", "/opt/kubeflow/train.py"]

在前述的 Docker 定义中,train.py脚本是一个典型的 TF 训练脚本。

现在,我们假设单机将用于训练。换句话说,它将是一个单容器作业。假设您已经准备好一个 Docker 文件和一个训练脚本,您可以使用以下命令构建您的容器并将其推送到仓库:

docker build -t kubeflow/tf_example_job:1.0
docker push kubeflow/tf_example_job:1.0

我们将使用TFJob,这是 Kubeflow 的一个自定义组件,其中包含一个将TFJob表示为描述容器镜像、训练脚本和执行参数的 YAML 文件。让我们看看一个 YAML 文件,tf_example_job.yaml,其中包含在单台机器上运行的 Kubeflow 模型训练作业:

apiVersion: "kubeflow.org/v1" 
kind: "TFJob"
metadata:
    name: "tf_example_job"
spec:
    tfReplicaSpecs:
        Worker:
            replicas: 1
        restartPolicy: Never
        template:
            specs:
                containers:
                    - name: tensorflow 
                      image: kubeflow/tf_example_job:1.0

API 版本在第一行中定义。然后列出了您自定义资源的类型,kind: "TFJob"metadata字段用于通过提供自定义名称来识别您的作业。集群在tfReplicaSpecs字段中定义。如前面的示例所示,脚本(tf_example_job:1.0)将执行一次(replicas: 1)。

要将定义的TFJob部署到您的集群中,您可以使用kubectl命令,如下所示:

kubectl apply -f tf_example_job.yaml

您可以使用以下命令监视您的作业(使用元数据中定义的名称):

kubectl describe tfjob tf_example_job 

要执行分布式训练,您可以使用带有特定tf.distribute.Strategy的 TF 代码,创建一个新的容器,并修改TFJob。我们将在下一节中查看TFJob所需的更改。

使用 Kubeflow 在分布式环境中训练 TensorFlow 模型

假设我们已经有了来自MultiWorkerMirroredStrategy的 TF 训练代码。为了支持此策略,需要在spec字段中调整tfReplicaSpecs。我们可以通过 YAML 文件定义以下类型的副本:

  • 主节点(主机):编排计算任务

  • 工作节点:运行计算任务

  • 参数服务器:管理模型参数的存储

  • 评估节点:在模型训练期间运行评估

作为最简单的例子,我们将定义一个作为主节点之一的工作节点。参数serverevaluator不是必须的。

让我们看看调整后的 YAML 文件,tf_example_job_dist.yaml,用于分布式 TF 训练:

apiVersion: "kubeflow.org/v1"
kind: "TFJob"
metadata:
    name: "tf_example_job_dist"
spec:
    cleanPodPolicy: None
    tfReplicaSpecs:
        Worker:
            replicas: 4
            restartPolicy: Never
            template:
                specs:
                    containers:
                        - name: tensorflow 
                          image: kubeflow/tf_example_job:1.1

前述的 YAML 文件将基于MultiWorkerMirroredStrategy在新的容器kubeflow/tf_example_job:1.1上运行训练作业。我们可以使用相同的命令将TFJob部署到集群:

kubectl apply -f tf_example_job_dist.yaml

在下一节中,我们将学习如何使用 PyTorch 和 Ray。

使用 Kubeflow 在分布式环境中训练 PyTorch 模型

对于 PyTorch,我们只需将TFJob更改为PyTorchJob,并提供一个 PyTorch 训练脚本。至于训练脚本本身,请参阅PyTorch 中的数据并行 ism部分。如下面的代码片段所示,YAML 文件需要相同的修改:

apiVersion: "kubeflow.org/v1 
kind: "PyTorchJob"
metadata:
    name: "pt_example_job_dist"
spec:
    pytorchReplicaSpecs:
        Master:
            replicas: 1
            restartPolicy: Never
            template:
                specs:
                    containers:
                        - name: pytorch 
                          image: kubeflow/pt_example_job:1.0
        Worker:
            replicas: 5
            restartPolicy: OnFailure
            template:
                specs:
                    containers:
                        - name: pytorch 
                          image: kubeflow/pt_example_job:1.0

在此示例中,我们有一个主节点和五个工作节点的副本。完整详情请查看www.kubeflow.org/docs/components/training/pytorch

需要记住的事项

a. Kubeflow 允许您轻松地从本地开发环境扩展到利用容器和 Kubernetes 的大型集群。

b. TFJobPyTorchJob分别允许您以分布式方式在 Kubeflow 中运行 TF 和 PyTorch 训练作业。

在本节中,我们描述了如何利用 Kubeflow 以分布式方式训练 TF 和 PyTorch 模型。

总结

通过意识到多设备和多机器并行带来的好处,我们学习了多种训练深度学习模型的方法。首先,我们学习了如何在单台机器上利用多个 CPU 和 GPU 设备。然后,我们介绍了如何利用 TF 和 PyTorch 的内置功能以分布式方式进行训练,其中底层集群由显式管理。之后,我们学习了如何使用 SageMaker 进行分布式训练和扩展。最后,最后三节描述了专为分布式训练设计的框架:Horovod、Ray 和 Kubeflow。

在下一章中,我们将介绍模型理解。我们将学习关于模型理解的流行技术,这些技术在训练过程中提供一些关于模型内部运行情况的见解。

第七章:揭示深度学习模型的秘密

到目前为止,我们已经描述了如何构建并高效训练深度学习DL)模型。然而,模型训练通常涉及多次迭代,因为针对特定任务正确配置训练的粗略指导仅存。

在本章中,我们将介绍超参数调优,这是寻找正确训练配置的最标准过程。随着我们指导您完成超参数调优的步骤,我们将介绍用于调优过程的流行搜索算法(网格搜索、随机搜索和贝叶斯优化)。我们还将探讨可解释 AI 领域,即在预测过程中理解模型操作的过程。我们将描述这一领域中的三种最常见技术:置换特征重要性PFI)、SHapley 加法解释SHAP)、局部可解释模型无关解释LIME)。

本章中,我们将涵盖以下主要主题:

  • 使用超参数调优获取最佳性能模型

  • 通过可解释 AI 理解模型行为

技术要求

您可以从本书的 GitHub 存储库下载本章的补充材料,链接为 github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_7

使用超参数调优获取最佳性能模型

如在 第三章 中描述的,开发强大的深度学习模型,获取能够为底层任务提取正确模式的 DL 模型需要适当配置多个组件。尽管构建合适的模型架构通常会引入许多困难,但设置适当的模型训练是大多数人都在努力解决的另一个挑战。

机器学习ML)中,超参数 是指控制学习过程的任何参数。在许多情况下,数据科学家通常关注与模型相关的超参数,例如特定类型层的数量、学习率或优化器类型。然而,超参数还包括数据相关配置,如应用的增强类型和模型训练的抽样策略。将一组超参数进行更改并理解性能变化的迭代过程,以找到目标任务的正确超参数集合,称为超参数调优。确切地说,您将有一组要探索的超参数。对于每次迭代,将会以不同的设置配置一个或多个超参数,并使用调整后的设置训练新模型。经过迭代过程,用于最佳模型的超参数配置将是最终输出。

在本章中,我们将学习用于超参数调整的各种技术和工具。

超参数调整技术

超参数调整技术可以通过选择目标超参数的值的方式而异。在各种技术中,我们将重点介绍最常见的几种:网格搜索随机搜索贝叶斯优化

网格搜索

最基本的方法被称为网格搜索,其中每个可能的值都逐个评估。例如,如果你想探索从 0 到 1 的学习率,增加幅度为 0.25,那么网格搜索将会为每个可能的学习率(0.25、0.5、0.75 和 1)训练模型,并选择生成最佳模型的学习率。

随机搜索

另一方面,随机搜索生成超参数的随机值,并重复训练,直到达到最大实验次数为止。如果我们将前面章节的例子转换为随机搜索,我们必须定义最大实验次数和学习率的边界。在这个例子中,我们将最大实验次数设置为 5,边界设置为 0 到 1。然后,随机搜索将在 0 到 1 之间选择一个随机值,并使用所选的学习率训练模型。这个过程将重复 5 次,选择生成最佳模型的学习率作为超参数调整的输出。

为了帮助理解,以下图表总结了网格搜索和随机搜索之间的区别:

图 7.1 – 网格搜索和随机搜索的差异

图 7.1 – 网格搜索和随机搜索的差异

在上图中,xy表示两个不同的超参数。每个轴上的紫色和绿色图表显示了模型性能随每个超参数变化的情况。

虽然网格搜索和随机搜索都很容易实现,但它们都有一个共同的限制:它们不能保证目标超参数的最佳值。这个问题主要源于在选择下一个要探索的值时没有考虑前面的结果。为了克服这个问题,引入了一种新的搜索算法:贝叶斯优化。

贝叶斯优化

贝叶斯优化的理念很简单:构建一个代理模型,映射超参数与底层模型之间的关系,并在整个超参数调整过程中进行调整,以便我们可以选择一个超参数值,这个超参数值很可能会在后续实验中使我们对关系有更好的理解。利用生成的代理模型,我们可以选择很可能会给我们带来更好模型的超参数值。

有许多构建代理模型的方法。如果我们假设关系可以表示为线性函数,那么代理模型生成过程将简单地是线性回归。实际上,关系要复杂得多,最成功的技术是使用高斯过程回归。在这里,我们假设关系可以用一组正态分布来表示。换句话说,我们选择的每个值都是从多元正态分布中随机选择的。如果我们想详细讨论贝叶斯优化的每个细节,我们将需要引入多个概率和数学术语。我们相信,本节的高层描述和下一节的完整示例足以让您应用使用贝叶斯优化进行超参数调整。如果您想了解贝叶斯优化背后的理论,请访问ieeexplore.ieee.org/abstract/document/7352306

超参数调整工具

由于超参数调整在 ML 项目中起着重要作用,因此存在许多旨在简化此过程的库。以下是一些流行的库:

在各种工具中,我们将关注 Ray Tune,因为我们在《第六章》Chapter 6 Efficient Model TrainingTraining a model using Ray 部分中介绍了如何使用 Ray 进行分布式训练。

使用 Ray Tune 进行超参数调整

作为 Ray 的一部分,一个旨在跨多台机器扩展 Python 工作负载的框架,Ray Tune 专为大规模实验执行和超参数调整而设计。在本节中,我们将为您介绍如何使用 Ray Tune 进行超参数调整的配置和调度。尽管示例旨在抽象表示模型训练功能,但 Ray Tune 的设置和文档足够清晰,以至于 PyTorchTensorFlowTF)的集成自然而然地在本节末尾完成。

首先,我们将介绍 Ray Tune 的基础知识。Ray Tune 的核心功能来自 tune.run 函数,该函数管理所有实验、日志和检查点。tune.run 函数的基本用法如下代码片段所示:

from ray import tune
def tr_function(conf):
    num_iterations = conf["num_it"]
    for i in range(num_iterations):
        … // training logic
        tune.report(mean_accuracy=acc)
tune.run(
    run_or_experiment=tr_function
    conf={"num_it": tune.grid_search([10, 20, 30, 40])})

tune.run 函数接受 run_or_experiment 参数,该参数定义训练逻辑,以及 conf 参数,该参数配置超参数调整。每个超参数在 conf 中提供的搜索函数类型决定了实验的数量。在上述示例中,我们使用了 tune.grid_search([10, 20, 30, 40]),这将启动四个实验,每个实验都会运行为 num_iterations 分配的函数 (tr_function),并使用不同的 num_iterations 值。在 tr_function 中,我们可以通过 conf 参数访问分配的超参数。值得一提的是,Ray Tune 提供了大量的采样方法 (docs.ray.io/en/latest/tune/api_docs/search_space.html#tune-sample-docs)。

Ray Tune 作为 tune.suggest 的一部分整合了许多开源优化库,为超参数调整提供各种先进的搜索算法。流行的算法包括 HyperOpt、Bayesian Optimization、Scitkit-Optimize 和 Optuna。完整列表可以在 docs.ray.io/en/latest/tune/api_docs/suggestion.html 找到。在下面的示例中,我们将描述如何使用 BayesOptSearch,正如其名称所示,它实现了贝叶斯优化:

from ray import tune
from ray.tune.suggest.bayesopt import BayesOptSearch
conf = {"num_it": tune.randint(100, 200)}
bayesopt = BayesOptSearch(metric="mean_accuracy", mode="max")
tune.run(
    run_or_experiment=tr_function
    config = conf,
    search_alg = bayesopt)

在上述代码片段中,我们向 search_alg 参数提供了 BayesOptSearch 的一个实例。该示例将尝试找到 num_iterations,以提供具有最高 mean_accuracy 的模型。

另一个tune.run的关键参数是stop。该参数可以接受一个字典、一个函数或一个Stopper对象,用于定义停止标准。如果是一个字典,键必须是run_or_experiment函数返回结果中的字段之一。如果是一个函数,它应返回一个布尔值,一旦满足停止条件就为True。以下是这两种情况的示例:

# dictionary-based stop
tune.run(tr_function,
        stop={"training_iteration": 20, 
              "mean_accuracy": 0.96})
# function-based stop
def stp_function(trial_id, result):
    return result["training_iteration"] > 20 or
           result["mean_accuracy"] > 0.96
tune.run(tr_function, stop=stp_function)

在基于字典的示例中,每个试验将在完成 10 次迭代或mean_accuracy达到指定值0.96时停止。基于函数的示例实现了相同的逻辑,但使用了stp_function函数。对于stopper类用例,您可以参考docs.ray.io/en/latest/tune/tutorials/tune-stopping.html#stopping-with-a-class

试验是 Ray Tune 的内部数据结构,包含关于每个实验的元数据 (docs.ray.io/en/latest/tune/api_docs/internals.html#trial-objects)。每个试验都有一个唯一的 ID(trial.trial_id),其超参数设置可以通过trial.config进行检查。有趣的是,可以通过tune.runresources_per_trial参数和trial.placement_group_factory为每个试验分配不同规模的机器资源。此外,num_samples参数可用于控制试验的数量。

ray.tune返回的Analysis实例可以获得您实验的摘要。以下代码片段描述了您可以从Analysis实例中检索的一组信息:

# last reported results
df = analysis.results_df
# list of trials
trs = analysis.trials
# max accuracy 
max_acc_df = analysis.dataframe(metric="mean_accuracy", mode="max")
# dict mapping for all trials in the experiment
all_dfs = analysis.trial_dataframes

您还可以从Analysis实例中检索其他有用的信息。完整详情请参阅docs.ray.io/en/latest/tune/api_docs/analysis.html

这完成了 Ray Tune 的核心组件。如果您希望为 PyTorch 或 TF 模型训练集成 Ray Tune,您所需做的就是调整示例中的tr_function,使其在记录相关性能指标的同时训练您的模型。

总体而言,我们已探讨了超参数调整的不同选项。本节介绍的工具应有助于我们有效地找到 DL 模型的最佳配置。

需要记住的事情

a. 获得特定任务的工作 DL 模型需要找到合适的模型架构并使用适当的训练配置。找到最佳组合的过程称为超参数调整。

b. 三种最流行的超参数调整技术是网格搜索、随机搜索和贝叶斯优化。

c. 流行的超参数调整工具包括 Scikit-Optimize、Optuna、Hyperopt、Ray Tune、Bayesian Optimization、MOE、Spearmint、GpyOpt 和 SigOpt。

到目前为止,我们将 DL 模型视为黑匣子。超参数调优涉及搜索未知空间,无法解释模型如何找到潜在模式。在下一节中,我们将探讨研究人员最近致力于理解 DL 灵活性的工作。

通过可解释人工智能理解模型的行为

可解释人工智能是一个非常活跃的研究领域。在商业环境中,理解 AI 模型往往能够轻松带来显著的竞争优势。所谓的黑匣子模型(复杂算法模型),尽管能够带来出色的结果,却因其隐含逻辑而常遭批评。高层管理人员很难完全基于 AI 设计核心业务,因为解释模型和预测并不是一件容易的事情。您如何说服您的业务合作伙伴,表明 AI 模型将始终产生预期结果?您如何确保模型在新数据上仍然有效?模型是如何生成结果的?可解释人工智能帮助我们解答这些问题。

在我们进一步探讨之前,让我们先了解两个重要的概念:可解释性解释性。乍一听可能很相似。可解释性告诉我们为什么特定的输入会产生特定模型的输出:特定变量对结果的影响。解释性超越了可解释性;它不仅关注输入和输出之间的因果关系,还帮助我们理解模型作为一个整体的工作方式,包括其所有子元素。解释性还由透明性、可复现性和可转移性三个基本理念驱动。这意味着我们应该能够完全理解我们的模型所做的事情,数据如何在经过模型时影响模型,并能够重现结果。

可解释人工智能在每个 ML 项目的各个步骤中发挥作用——开发(模型架构的解释和每个超参数的含义)、训练(训练过程中模型的变化)、以及推理(结果解释)。在 DL 模型的情况下,由于网络架构复杂、算法复杂性高,以及在初始化权重、偏置、正则化和超参数优化时使用随机数,因此实现可解释性是困难的。

在本节中,我们将讨论几种常用于增强 DL 模型可信度的方法:置换特征重要性PFI)、特征重要性FI)、SHapley 加性解释SHAP)和局部可解释模型无关解释LIME)。所有这些方法都是模型无关的;它们既可应用于 DL 模型,也可应用于其他支持 ML 模型,常用于设定基线评估指标。

置换特征重要性

神经网络缺乏理解输入特征对预测(模型输出)影响所需的内在属性。然而,有一种称为置换特征重要性PFI)的模型无关方法可以解决这一困难。PFI 的思想来自于输入特征与输出之间的关系:对于与输出变量高度相关的输入特征,改变其值会增加模型的预测误差。如果关系较弱,模型的性能不会受到太多影响。如果关系很强,性能将下降。PFI 经常应用于测试集,以获取对未见数据上模型可解释性的更广泛理解。

PFI 的主要缺点与数据具有一组相关输入特征相关。在这种情况下,即使您改变该组的一个特征,模型的性能也不会改变太多,因为其他特征将保持不变。

深入探讨这个想法,我们可以完全删除该特征并测量模型的性能。这种方法称为特征重要性FI),也称为置换重要性PI)或平均减少精度MDA)。让我们看看如何为任何黑盒模型实现 FI。

特征重要性

在本节中,我们将使用ELI5 Python 包(eli5.readthedocs.io)执行 FI 分析。它在 FI 领域突出的原因是非常简单易用。让我们看一下在 Keras 定义的 TF 模型中使用 ELI5 的最小代码示例(有关模型定义的详细信息,请参见第三章开发一个强大的深度学习模型):

import eli5
from eli5.sklearn import PermutationImportance
def score(self, x, y_true):
    y_pred = model.predict(x)
    return tf.math.sqrt( tf.math.reduce_mean( tf.math.square(y_pred-y_true), axis=-1)) 
perm = PermutationImportance(model, random_state=1, scoring=score).fit(features, labels)
fi_perm=perm.feature_importances_
fi_std=perm.feature_importances_std_

正如您所见,代码几乎是自解释的。首先,我们需要为计算目标评估指标的评分函数创建一个包装器。然后,将tf.keras模型传递给PermutationImportance类的构造函数。fit函数负责处理特征和标签的特征重要性计算。计算完成后,我们可以访问每个特征的平均特征重要性(fi_perm)和置换结果的标准差(fi_std)。以下代码片段展示了如何将置换重要性的结果可视化为条形图:

plt.figure()
for index, row in enumerate(fi_perm):
    plt.bar(index, 
            fi_perm[index], 
            color="b", 
            yerr=fi_std[index], 
            align="center")
plt.show()

如果模型既不基于 scikit-learn 也不基于 Keras,则需要使用permutation_importance.get_score_importance函数。以下代码片段描述了如何在 PyTorch 模型中使用该函数:

import numpy as np
from eli5.permutation_importance import get_score_importances
# A trained PyTorch model
black_box_model = ...
def score(X, y):
    y_pred = black_box_model.predict(X)
    return accuracy_score(y, y_pred)
base_score, score_decreases = get_score_importances(score, X, y)
feature_importances = np.mean(score_decreases, axis=0)

PermutationImportance类不同,get_score_importances函数同时接收评分函数、特征和标签。

接下来,我们将看看SHapley Additive exPlanationsSHAP),这也是一种与模型无关的方法。

SHapley Additive exPlanations(SHAP)

SHAP 是一种解释方法,利用夏普利值来理解给定黑盒模型。我们不会涵盖 SHAP 基于的合作博弈理论,但我们将以高层次来讨论过程。首先,让我们看看 Shapley 值的定义:在不同模拟中,所有可能联盟的平均边际贡献。这究竟意味着什么?假设有四位朋友(f1f2f3f4)共同努力为一个在线游戏获得最高分数。要计算一个人的夏普利值,我们需要计算边际贡献,即该人参与游戏与不参与游戏时分数的差异。这个计算必须针对所有可能的子组(联盟)进行。

让我们仔细看一看。为了计算f1对朋友f2f3f4联合体的边际贡献,我们需要执行以下操作:

  1. 计算所有朋友(f1f2f3f4)生成的分数(s1)。

  2. 计算朋友f2f3f4生成的分数(s2)。

  3. 最后,朋友f1对朋友f2f3f4联合体的边际贡献(v)等于s1-s2

现在,我们需要计算所有子组合的边际贡献(不仅仅是朋友的联合体;即f2f3f4)。这里是每一个可能的组合:

  1. f1无人正在贡献(v1)

  2. f1f2f2(v2)

  3. f1f3f3(v3)

  4. f1f4f4(v4)

  5. f1f2f3f2f3(v5)

  6. f1f2f4f2f4(v6)

  7. f1f3f4f3f4(v7)

  8. f1f2f3f4f2f3f4(v8)

总体而言,f1的夏普利值(SV)为(v1+v2+...+v8) / 8

为了使我们的结果具有统计学意义,我们需要在多次模拟中运行这些计算。您可以看到,如果我们扩展朋友的数量,计算将变得非常复杂,导致计算资源的高消耗。因此,我们使用具体的近似值,产生了不同类型的所谓解释器(夏普利值的近似器),这些解释器可以在shap库中找到(shap.readthedocs.io/en/latest/index.html)。通过比较所有朋友的夏普利值,我们可以找出每个个体对最终分数的贡献。

如果我们回到 DL 模型的解释,我们可以看到朋友们变成了一组特征,而分数则是模型的性能。有了这个理念,让我们来看看 SHAP 解释器,它可以用于 DL 模型:

  • KernelExplainer:这是最流行的方法,也是与模型无关的。它基于局部可解释模型无关解释LIME),我们将在下一节讨论。

  • DeepExplainer:这种方法基于 DeepList 方法,它在特定输入上分解输出(arxiv.org/abs/1704.02685)。

  • GradientExplainer:这种方法基于集成梯度的扩展(arxiv.org/abs/1703.01365)。

例如,我们将展示一个应用于 TF 模型的极简代码示例。完整详情请参阅官方文档 shap-lrjball.readthedocs.io/en/latest/index.html

import shap
# initialize visualization
shap.initjs()
model = … # tf.keras model or PyTorch model (nn.Module) 
explainer = shap.KernelExplainer(model, sampled_data)
shap_values = explainer.shap_values(data, nsamples=300)
shap.force_plot(explainer.expected_value, shap_values, data)
shap.summary_plot(shap_values, sampled_data, feature_names=names, plot_type="bar")

对于 PyTorch 模型,您需要将模型包装在一个包装器中,以将输入和输出转换为正确的类型(f=lambda x: model(torch.autograd.Variable(torch.from_numpy(x))).detach().numpy())。在下面的示例中,我们定义了 KernelExplainer,它接受 DL 模型和 sampled_data 作为输入。接下来,我们使用 explainer.shap_values 函数计算 SHAP 值(Shapley 值的近似值)。在本例中,我们使用 300 个扰动样本来估计给定预测的 SHAP 值。如果我们的 sampled_data 包含 100 个示例,则将执行 100*300 次模型评估。类似地,您可以使用 GradientExplainershap.GradientExplainer(model, sampled_data))或 DeepExplainershap.DeepExplainer(model, sampled_data))。sampled_data 的大小需要足够大,以正确表示分布。在最后几行中,我们使用 shap.force_plot 函数在加性力布局中可视化 SHAP 值,并使用 shap.summary_plot 函数创建全局模型解释图。

现在,让我们看看 LIME 方法。

本地可解释的模型无关解释(LIME)

LIME 是一种训练本地替代模型以解释模型预测的方法。首先,您需要准备一个要解释的模型和一个样本。LIME 使用您的模型从一组扰动数据中收集预测,并将它们与原始样本进行比较,以分配相似性权重(如果预测接近初始样本的预测,则权重较高)。LIME 使用特定数量的特征通过相似性权重在采样数据上拟合一个内在可解释的替代模型。最后,LIME 将替代模型解释视为所选示例的黑盒模型的解释。要执行 LIME 分析,我们可以使用 lime 包(lime-ml.readthedocs.io)。

让我们看一个为 DL 模型设计的示例:

from lime.lime_tabular import LimeTabularExplainer as Lime
from matplotlib import pyplot as plt
expl = Lime(features, mode='classification', class_names=[0, 1])
# explain first sample
exp = expl.explain_instance(x[0], model.predict, num_features=5, top_labels=1)
# show plot
exp.show_in_notebook(show_table=True, show_all=False) 

在前面的示例中,我们正在使用 LimeTabularExplainer 类。构造函数接受一个训练集、特征、类名和模式类型 ('classification')。同样,您可以通过提供 'regression' 模式类型来设置用于回归问题的 LIME。然后,通过展示五个最重要的特征及其影响,我们解释了测试集的第一个预测 (x[0])。最后,我们生成了一个基于计算的 LIME 解释的图表。

要记住的事项

a. 在解释性人工智能中,模型可解释性和解释性是两个关键概念。

b. 解释性人工智能中流行的无关模型技术包括 PFI、FI、SHAP 和 LIME。

c. PFI、FI 和 SHAP 是允许您在本地(单个样本)和全局(一组样本)级别解释您的模型的方法。另一方面,LIME 关注单个样本及其对应的模型预测。

在本节中,我们已经解释了解释性人工智能的概念以及四种最常见的技术:PFI、FI、SHAP 和 LIME。

总结

我们从调整超参数开始了本章。我们描述了用于超参数调整的三种基本搜索算法(网格搜索、随机搜索和贝叶斯优化),并介绍了许多可集成到项目中的工具。在列出的工具中,我们涵盖了 Ray Tune,因为它支持分布式超参数调整,并实现了许多即用的最先进搜索算法。

然后,我们讨论了解释性人工智能。我们解释了最标准的技术(PFI、FI、SHAP 和 LIME),以及它们如何用来发现模型在数据集中每个特征变化时的行为变化。

在下一章中,我们将把焦点转向部署。我们将学习 ONNX,这是一种用于机器学习模型的开放格式,并了解如何将 TF 或 PyTorch 模型转换为 ONNX 模型。

第三部分:部署与维护

在部署深度学习项目过程中经常会面临许多复杂挑战。在许多情况下,部署环境与开发环境不同,这种差异可能会引入各种限制。在本部分中,我们介绍工程师经常遇到的常见问题,并针对每个挑战分享有效的解决方案。在最后一章中,我们描述了深度学习项目的最后阶段,包括评估项目并讨论未来项目的潜在改进。

本部分包括以下章节:

  • 第八章, 简化深度学习模型部署

  • 第九章, 扩展深度学习管道

  • 第十章, 提升推理效率

  • 第十一章, 移动设备上的深度学习

  • 第十二章, 监控生产中的深度学习端点

  • 第十三章, 审查完成的深度学习项目

第八章:简化深度学习模型部署

在生产环境中部署的 深度学习 (DL) 模型通常与训练过程中的模型有所不同。它们通常被增强以处理传入请求,并具有最高性能。然而,目标环境通常过于广泛,因此需要大量定制以涵盖非常不同的部署设置。为了克服这一困难,您可以利用 开放神经网络交换 (ONNX),这是一种用于 ML 模型的标准文件格式。在本章中,我们将介绍如何利用 ONNX 在 DL 框架之间转换 DL 模型,并如何将模型开发过程与部署分离。

在本章中,我们将涵盖以下主要内容:

  • ONNX 简介

  • TensorFlow 和 ONNX 之间的转换

  • PyTorch 和 ONNX 之间的转换

技术要求

您可以从以下 GitHub 链接下载本章的补充材料:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_8

ONNX 简介

您可以使用多种 DL 框架来训练 DL 模型。然而,DL 模型部署中的一个主要困难是这些框架之间缺乏互操作性。例如,PyTorch 和 TensorFlow (TF) 之间的转换引入了许多困难。

在许多情况下,DL 模型还会为部署环境进一步增强,以提高准确性并减少推断延迟,利用底层硬件提供的加速功能。不幸的是,这需要广泛的软件和硬件知识,因为每种类型的硬件为运行应用程序提供不同的加速。用于 DL 的常用硬件包括 中央处理单元 (CPU)、图形处理单元 (GPU)、关联处理单元 (APU)、张量处理单元 (TPU)、现场可编程门阵列 (FPGA)、视觉处理单元 (VPU)、神经处理单元 (NPU) 和 JetsonBoard

此过程不是一次性操作;一旦模型以任何方式更新,可能需要重复此过程。为了减少这一领域的工程工作量,一组工程师共同努力,提出了一种标准化模型组件的中介:.onnx 文件,用于跟踪模型设计及网络内每个操作如何与其他组件链接。.onnx 文件 (github.com/lutzroeder/netron)。以下是一个示例可视化:

图 8.1 – ONNX 文件的 Netron 可视化

图 8.1 – ONNX 文件的 Netron 可视化

如您所见,ONNX 是训练框架和部署环境之间的一层。虽然 ONNX 文件定义了一种交换格式,但也存在支持 ONNX 模型的 ONNX Runtime (ORT),后者支持对 ONNX 模型进行硬件无关的加速优化。换句话说,ONNX 生态系统允许您选择任何 DL 框架进行训练,并使得部署时的硬件特定优化变得轻而易举。

![图 8.2 – ONNX 在深度学习项目中的位置img/B18522_08_02.jpg

图 8.2 – ONNX 在深度学习项目中的位置

总结一下,ONNX 有助于以下任务:

  • 简化不同深度学习框架之间的模型转换

  • 为深度学习模型提供与硬件无关的优化

在接下来的部分中,我们将更详细地了解 ORT。

使用 ONNX Runtime 运行推理

ORT 旨在直接支持使用 ONNX 模型进行训练和推理,无需将其转换为特定框架。然而,训练并不是 ORT 的主要用例,因此我们将专注于推理这一方面,在本节中进行讨论。

ORT 利用不同的硬件加速库,称为 Execution Providers (EPs),以提高各种硬件架构的延迟和准确性。无论模型训练期间使用的 DL 框架和底层硬件如何,ORT 推理代码保持不变。

下面的代码片段是一个 ONNX 推理代码示例。完整详情请查阅 onnxruntime.ai/docs/get-started/with-python.html

import onnxruntime as rt
providers = ['CPUExecutionProvider'] # select desired provider or use rt.get_available_providers()
model = rt.InferenceSession("model.onnx", providers=providers)
onnx_pred = model.run(output_names, {"input": x}) # x is your model's input

InferenceSession 类接受文件名、序列化的 ONNX 模型或 ORT 模型的字节字符串作为输入。在上述示例中,我们指定了一个 ONNX 文件的名称 ("model.onnx")。providers 参数和按优先顺序排列的执行提供者列表(如 CPUExecutionProviderTvmExecutionProviderCUDAExecutionProvider 等)是可选的,但非常重要,因为它们定义了将应用的硬件加速类型。在最后一行,run 函数触发模型预测。run 函数有两个主要参数:output_names(模型输出的名称)和 input_feed(输入字典,包含您希望使用模型进行预测的输入名称和值)。

需要记住的事项

a. ONNX 提供了用于 ML 模型的标准化和跨平台的表示。

b. ONNX 可以用于将一个 DL 框架中实现的模型转换为另一个框架,转换过程需要很少的工作量。

c. ORT 为已部署的模型提供与硬件无关的加速。

在接下来的两节中,我们将看看使用 TF 和 PyTorch 创建 ONNX 模型的过程。

在 TensorFlow 和 ONNX 之间的转换

首先,我们将研究 TF 到 ONNX 的转换。我们将这个过程分解为两步:将 TF 模型转换为 ONNX 模型,以及将 ONNX 模型转换回 TF 模型。

将 TensorFlow 模型转换为 ONNX 模型

tf2onnx 用于将 TF 模型转换为 ONNX 模型 (github.com/onnx/tensorflow-onnx)。此库支持 TF 的两个版本(版本 1 和版本 2)。此外,还支持将模型转换为特定部署的 TF 格式,如 TensorFlow.js 和 TensorFlow Lite。

要将使用 saved_model 模块生成的 TF 模型转换为 ONNX 模型,可以使用 tf2onnx.convert 模块,如下所示:

python -m tf2onnx.convert --saved-model tensorflow_model_path --opset 9 --output model.onnx  

在上述命令中,tensorflow-model-path 指向磁盘上保存的 TF 模型,--output 定义了生成的 ONNX 模型保存的位置,--opset 设置了 ONNX 的 opset,它定义了 ONNX 的版本和操作符 (github.com/onnx/onnx/releases)。如果您的 TF 模型未使用 tf.saved_model.save 函数保存,需要按照以下格式指定输入和输出格式:

# model in checkpoint format
python -m tf2onnx.convert --checkpoint tensorflow-model-meta-file-path --output model.onnx --inputs input0:0,input1:0 --outputs output0:0
# model in graphdef format
python -m tf2onnx.convert --graphdef tensorflow_model_graphdef-file --output model.onnx --inputs input0:0,input1:0 --outputs output0:0 

上述命令描述了 Checkpoint (www.tensorflow.org/api_docs/python/tf/train/Checkpoint) 和 GraphDef (www.tensorflow.org/api_docs/python/tf/compat/v1/GraphDef) 格式模型的转换。关键参数是 --checkpoint--graphdef,它们指示了模型格式以及源模型的位置。

tf2onnx 还提供了一个 Python API,您可以在 github.com/onnx/tensorflow-onnx 找到它。

接下来,我们将看一下如何将 ONNX 模型转换为 TF 模型。

将 ONNX 模型转换为 TensorFlow 模型

虽然 tf2onnx 用于从 TF 到 ONNX 的转换,但 onnx-tensorflow (github.com/onnx/onnx-tensorflow) 用于将 ONNX 模型转换为 TF 模型。它与 tf2onnx 一样,基于终端命令。以下是一个简单的 onnx-tf 命令示例:

onnx-tf convert -i model.onnx -o tensorflow_model_file

在上述命令中,-i 参数用于指定源 .onnx 文件,而 -o 参数用于指定新 TF 模型的输出位置。onnx-tf 命令的其他用例在 github.com/onnx/onnx-tensorflow/blob/main/doc/CLI.md 中有详细说明。

此外,您也可以使用 Python API 执行相同的转换:

import onnx
from onnx_tf.backend import prepare
onnx_model = onnx.load("model.onnx") 
tf_rep = prepare(onnx_model)  
tensorflow-model-file-path = path/to/tensorflow-model
tf_rep.export_graph(tensorflow_model_file_path)

在上述 Python 代码中,使用 onnx.load 函数加载 ONNX 模型,然后使用从 onnx_tf.backend 导入的 prepare 进行调整,最后使用 export_graph 函数将 TF 模型导出并保存到指定位置 (tensorflow_model_file_path)。

需要记住的事情

a. 从 TF 到 ONNX 的转换和从 ONNX 到 TF 的转换分别通过 onnx-tensorflowtf2onnx 完成。

b. onnx-tensorflowtf2onnx 都支持命令行界面和提供 Python API。

接下来,我们将描述在 PyTorch 中如何执行从 ONNX 到 ONNX 的转换。

PyTorch 和 ONNX 之间的转换

在本节中,我们将解释如何将 PyTorch 模型转换为 ONNX 模型,然后再转换回来。在前一节已经覆盖了 TF 和 ONNX 之间的转换,所以通过本节结束时,你应该能够完成 TF 和 PyTorch 之间模型的转换。

将 PyTorch 模型转换为 ONNX 模型

有趣的是,PyTorch 内置支持将其模型导出为 ONNX 模型 (pytorch.org/tutorials/advanced/super_resolution_with_onnxruntime.html)。给定一个模型,你只需要使用 torch.onnx.export 函数,如下面的代码片段所示:

import torch
pytorch_model = ...
# Input to the model
dummy_input = torch.randn(..., requires_grad=True)
onnx_model_path = "model.onnx"
# Export the model
torch.onnx.export(
    pytorch_model,       # model being run
    dummy_input,         # model input (or a tuple for multiple inputs)
    onnx_model_path      # where to save the model (can be a file or file-like object) )

torch.onnx.export 的第一个参数是你想要转换的 PyTorch 模型。第二个参数必须是一个表示虚拟输入的张量。换句话说,这个张量必须是模型期望输入的大小。最后一个参数是 ONNX 模型的本地路径。

触发 torch.onnx.export 函数后,你应该能够看到一个 .onnx 文件生成在你提供的路径下 (onnx_model_path)。

现在,让我们看看如何将一个 ONNX 模型加载为 PyTorch 模型。

将 ONNX 模型转换为 PyTorch 模型。

不幸的是,PyTorch 没有内置支持加载 ONNX 模型的功能。但是,有一个名为 onnx2pytorch 的流行库可用于此转换 (github.com/ToriML/onnx2pytorch)。假设这个库通过 pip 命令安装,下面的代码片段展示了这个转换过程:

import onnx
from onnx2pytorch import ConvertModel
onnx_model = onnx.load("model.onnx")
pytorch_model = ConvertModel(onnx_model)

我们从 onnx2pytorch 模块中需要的关键类是 ConverModel。如前面的代码片段所示,我们将一个 ONNX 模型传递给这个类来生成一个 PyTorch 模型。

需记住的事项

a. PyTorch 内置支持将 PyTorch 模型导出为 ONNX 模型。这个过程涉及到 torch.onnx.export 函数。

b. 将 ONNX 模型导入 PyTorch 环境需要使用 onnx2pytorch 库。

在本节中,我们描述了在 ONNX 和 PyTorch 之间的转换过程。由于我们已经知道如何在 ONNX 和 TF 之间转换模型,所以 TF 和 PyTorch 之间的转换自然而然地进行了。

总结

在本章中,我们介绍了 ONNX,这是一个通用的 ML 模型表示方式。ONNX 的好处主要来自于其模型部署能力,因为它可以通过 ORT 在幕后处理环境特定的优化和转换。ONNX 的另一个优势来自于其互操作性;它可以用来将一个使用某一框架生成的 DL 模型转换为其他框架的模型。在本章中,我们特别介绍了 TensorFlow 和 PyTorch 的转换,因为它们是两个最常见的 DL 框架。

在迈向高效的深度学习模型部署的又一步中,在下一章中,我们将学习如何使用弹性 Kubernetes 服务EKS)和 SageMaker 来建立一个模型推理端点。

第九章:扩展深度学习管道

亚马逊网络服务AWS)在深度学习DL)模型部署方面提供了许多可能性。在本章中,我们将介绍两种最受欢迎的服务,专为将 DL 模型部署为推理端点而设计:弹性 Kubernetes 服务EKS)和SageMaker

在前半部分,我们将描述基于 EKS 的方法。首先,我们将讨论如何为TensorFlowTF)和 PyTorch 模型创建推理端点,并使用 EKS 部署它们。我们还将介绍弹性推理EI)加速器,它可以提高吞吐量同时降低成本。EKS 集群有托管推理端点的 pod 作为 Web 服务器。作为基于 EKS 的部署的最后一个主题,我们将介绍如何根据动态流量扩展这些 pod。

在后半部分,我们将介绍基于 SageMaker 的部署。我们将讨论如何为 TF、PyTorch 和 ONNX 模型创建推理端点。此外,端点将使用亚马逊 SageMaker Neo和 EI 加速器进行优化。然后,我们将设置在 SageMaker 上运行的推理端点的自动扩展。最后,我们将通过描述如何在单个 SageMaker 推理端点中托管多个模型来结束本章。

本章节中,我们将涵盖以下主要主题:

  • 使用弹性 Kubernetes 服务进行推理

  • 使用 SageMaker 进行推理

技术要求

您可以从本书的 GitHub 存储库下载本章的补充材料,网址为github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_9

使用弹性 Kubernetes 服务进行推理

EKS 旨在通过简化复杂的集群管理过程提供用于应用部署的 Kubernetes 集群(aws.amazon.com/eks)。有关创建 EKS 集群的详细步骤,请参阅docs.aws.amazon.com/eks/latest/userguide/create-cluster.html。通常情况下,EKS 集群用于部署任何 Web 服务应用程序,并根据需要进行扩展。EKS 上的推理端点只是处理模型推理请求的 Web 服务应用程序。在本节中,您将学习如何在 EKS 上托管 DL 模型推理端点。

Kubernetes 集群有一个控制平面和一组节点。控制平面根据流入流量的大小进行调度和扩展决策。在调度方面,控制平面管理在给定时间点运行作业的节点。在扩展方面,控制平面根据流入端点的流量大小增加或减少 pod 的大小。EKS 在幕后管理这些组件,以便您可以专注于有效和高效地托管您的服务。

本节首先描述了如何设置 EKS 集群。然后,我们将描述如何使用 TF 和 PyTorch 创建端点,以处理 EKS 集群上的模型推断请求。接下来,我们将讨论 EI 加速器,该加速器提高了推断性能,并降低了成本。最后,我们将介绍一种根据入站流量量动态扩展服务的方法。

准备 EKS 集群

基于 EKS 的模型部署的第一步是创建适当硬件资源的 pod。在本节中,我们将使用 AWS 建议的 GPU Docker 镜像(github.com/aws/deep-learning-containers/blob/master/available_images.md)。这些标准镜像已在 Elastic Container Registry(ECR)注册并可用,ECR 提供了一个安全、可扩展和可靠的 Docker 镜像注册表(aws.amazon.com/ecr)。接下来,我们应该将 NVIDIA 设备插件应用到容器中。此插件使机器学习(ML)操作能够利用底层硬件以实现较低的延迟。关于 NVIDIA 设备插件的更多详细信息,建议阅读 github.com/awslabs/aws-virtual-gpu-device-plugin

在下面的代码片段中,我们将使用 kubectlkubectl 需要提供一个关于集群、用户、命名空间和认证机制信息的 YAML 文件(kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig)。最常见的操作是 kubectl apply,它在 EKS 集群中创建或修改资源:

kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.12/nvidia-device-plugin.yml

在上述用例中,kubectl apply 命令根据 YAML 文件中的规范向 Kubernetes 集群应用 NVIDIA 设备插件。

配置 EKS

YAML 文件用于配置构成 Kubernetes 集群的机器以及集群内运行的应用程序。根据类型,YAML 文件中的配置可以分为两部分:deploymentservice。deployment 部分控制 pod 内运行的应用程序。在本节中,它将用于创建 DL 模型的端点。在 EKS 环境中,运行在一个或多个 pod 上的一组应用程序称为 service。service 部分在集群上创建和配置服务。在整个 service 部分中,我们将为外部连接创建一个唯一的服务 URL,并配置入站流量的负载均衡。

在管理 EKS 集群时,命名空间可以很有用,因为它们在集群内部隔离了一组资源。要创建命名空间,可以简单地使用 kubectl create namespace 终端命令,如下所示:

kubectl create namespace tf-inference

在上述命令中,我们为接下来将在下一节创建的推断端点和服务构建了 tf-inference 命名空间。

使用 TensorFlow 模型在 EKS 上创建推断端点

在这一部分中,我们将描述一个设计用于托管 TF 模型推断端点的 EKS 配置文件(tf.yaml)。端点由 TensorFlow Service 创建,这是一个用于部署 TF 模型的系统(www.tensorflow.org/tfx/guide/serving)。由于我们的主要重点是在 EKS 配置上,我们将简单地假设一个训练有素的 TF 模型已经作为 .pb 文件存储在 S3 上。

首先,让我们看看配置的 Deployment 部分,负责处理端点的创建:

kind: Deployment
  apiVersion: apps/v1
  metadata:
    name: tf-inference # name for the endpoint / deployment
    labels:
      app: demo
      role: master
  spec:
    replicas: 1 # number of pods in the cluster
    selector:
      matchLabels:
        app: demo
        role: master

正如我们所见,配置的 Deployment 部分以 kind: Deployment 开始。在配置的第一部分中,我们提供了有关端点的一些元数据,并通过填写 spec 部分定义了系统设置。

端点的最重要配置在 template 下指定。我们将创建一个端点,可以通过 超文本传输协议HTTP)请求以及 远程过程调用gRPC)请求进行访问。HTTP 是用于网络客户端和服务器的最基本的传输数据协议。构建在 HTTP 之上,gRPC 是一个用于以二进制格式发送请求和接收响应的开源协议:

    template:      
      metadata:
        labels:
          app: demo
          role: master
      spec:
        containers:
        - name: demo
          image: 763104351884.dkr.ecr.us-east-1.amazonaws.com/tensorflow-inference:2.1.0-gpu-py36-cu100-ubuntu18.04 # ECR image for TensorFlow inference
          command:
          - /usr/bin/tensorflow_model_server # start inference endpoint
          args: # arguments for the inference serving
          - --port=9000
          - --rest_api_port=8500
          - --model_name=saved_model
          - --model_base_path=s3://mybucket/models
          ports:
          - name: http
            containerPort: 8500 # HTTP port
          - name: gRPC
            containerPort: 9000 # gRPC port

template 部分下,我们指定了要使用的 ECR 镜像(image: 763104351884.dkr.ecr.us-east-1.amazonaws.com/tensorflow-inference:2.1.0-gpu-py36-cu100-ubuntu18.04)、创建 TF 推断端点的命令(command: /usr/bin/tensorflow_model_server)、TF 服务的参数(args)以及容器的端口配置(ports)。

TF 服务参数包含模型名称(--model_name=saved_model)、模型在 S3 上的位置(--model_base_path=s3://mybucket/models)、用于 HTTP 访问的端口(--rest_api_port=8500)以及用于 gRPC 访问的端口(--port=9000)。ports 下的两个 ContainerPort 配置用于向外部连接暴露端点(containerPort: 8500containerPort: 9000)。

接下来,让我们看一下 YAML 文件的第二部分 – Service 的配置:

kind: Service
  apiVersion: v1
  metadata:
    name: tf-inference # name for the service
    labels:
      app: demo
  spec:
    Ports:
    - name: http-tf-serving
      port: 8500 # HTTP port for the webserver inside the pods
      targetPort: 8500 # HTTP port for access outside the pods
    - name: grpc-tf-serving
      port: 9000 # gRPC port for the webserver inside the pods
      targetPort: 9000 # gRPC port for access outside the pods
    selector:
      app: demo
      role: master
    type: ClusterIP

配置的 Service 部分以 kind: Service 开始。在 name: http-tf-serving 部分下,我们有 port: 8500,指的是用于 HTTP 请求的 TF 服务 Web 服务器在 Pod 内监听的端口。targetPort 指定了用于暴露相应端口的 Pod 使用的端口。我们在 name: grpc-tf-serving 部分下有另一组 gRPC 的端口配置。

要将配置应用于底层集群,您只需将此 YAML 文件提供给 kubectl apply 命令。

接下来,我们将在 EKS 上为 PyTorch 模型创建一个端点。

在 EKS 上使用 PyTorch 模型创建推断端点

在本节中,您将学习如何在 EKS 上创建 PyTorch 模型推断端点。首先,我们想介绍TorchServe,这是一个面向 PyTorch 的开源模型服务框架(pytorch.org/serve)。它旨在简化大规模部署 PyTorch 模型的过程。用于 PyTorch 模型部署的 EKS 配置与前一节描述的 TF 模型部署非常相似。

首先,需要将 PyTorch 模型的.pth文件转换为.mar文件,这是 TorchServe 所需的格式(github.com/pytorch/serve/blob/master/model-archiver/README.md)。可以使用torch-model-archiver包实现此转换。TorchServe 和torch-model-archiver可以通过以下方式使用pip下载和安装:

pip install torchserve torch-model-archiver

当使用torch-model-archiver命令进行转换时,代码如下所示:

torch-model-archiver --model-name archived_model --version 1.0 --serialized-file model.pth --handler run_inference

在前面的代码中,torch-model-archiver命令接受model-name(输出.mar文件的名称,即archived_model)、version(PyTorch 版本 1.0)、serialized-file(输入的 PyTorch .pth文件,即model.pth)和handler(定义 TorchServe 推断逻辑的文件名;即run_inference,指的是名为run_inference.py的文件)。该命令将生成一个archived_model.mar文件,该文件将通过 EKS 上传到 S3 存储桶以供端点托管。

在讨论 EKS 配置之前,我们还想介绍另一个命令,即mxnet-model-server。该命令在 DLAMI 实例中可用,允许您托管一个运行 PyTorch 推断的 Web 服务器以处理传入请求:

mxnet-model-server --start --mms-config /home/model-server/config.properties --models archived_model=https://dlc-samples.s3.amazonaws.com/pytorch/multi-model-server/archived_model.mar

在上面的示例中,mxnet-model-server命令使用start参数创建了通过models参数提供的模型的端点。正如您所看到的,models参数指向了位于 S3 上模型的位置(archived_model=https://dlc-samples.s3.amazonaws.com/pytorch/multi-model-server/archived_model.mar)。模型的输入参数在传递给命令的mms-config参数的/home/model-server/config.properties文件中指定。

现在,我们将讨论必须填写 EKS 配置中的Deployment部分。每个组件可以保持与 TF 模型版本类似。主要区别在于template部分,如下面的代码片段所示:

       containers:
       - name: pytorch-service
         image: "763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-inference:1.3.1-gpu-py36-cu101-ubuntu16.04"
         args:
         - mxnet-model-server
         - --start
         - --mms-config /home/model-server/config.properties
         - --models archived_model=https://dlc-samples.s3.amazonaws.com/pytorch/multi-model-server/archived_model.mar
         ports:
         - name: mms
           containerPort: 8080

在上述代码中,我们使用了一个不同的 Docker 镜像,其中已安装了 PyTorch(image: "763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-inference:1.3.1-gpu-py36-cu101-ubuntu16.04")。配置使用mxnet-model-server命令创建推理端点。我们将在此端点上使用的端口是8080。我们对Service部分所做的唯一更改可以在Ports部分找到;我们必须确保分配了外部端口并连接到端口8080,即端点托管的端口。同样,您可以使用kubectl apply命令来应用更改。

在接下来的部分,我们将描述如何与由 EKS 集群托管的端点进行交互。

与 EKS 上端点的通信

现在我们有了一个运行中的端点,我们将解释如何发送请求并检索推理结果。首先,我们需要使用kubectl get services命令来识别服务的 IP 地址,如下面的代码片段所示:

kubectl get services --all-namespaces -o wide

上述命令将返回服务及其外部 IP 地址的列表:

NAME         TYPE      CLUSTER-IP   EXTERNAL-IP    PORT(S)  AGE 
tf-inference ClusterIP 10.3.xxx.xxx 104.198.xxx.xx 8500/TCP 54s

在此示例中,我们将使用在在 EKS 上使用 TensorFlow 模型创建推理端点部分中创建的tf-inference服务。从kubectl get services的示例输出中,我们可以看到该服务正在运行,并且具有104.198.xxx.xx的外部 IP 地址。要通过 HTTP 访问该服务,您需要将 HTTP 的端口附加到 IP 地址:http://104.198.xxx.xx:8500。如果您有兴趣为 IP 地址创建显式 URL,请访问aws.amazon.com/premiumsupport/knowledge-center/eks-kubernetes-services-cluster

要向端点发送预测请求并接收推理结果,您需要进行 POST 类型的 HTTP 请求。如果您想从终端发送请求,您可以使用以下curl命令:

curl -d demo_input.json -X POST http://104.198.xxx.xx:8500/v1/models/demo:predict

在前述命令中,我们将 JSON 数据(demo_input.json)发送到端点(http://104.198.xxx.xx:8500/v1/models/demo:predict)。输入 JSON 文件demo_input.json包含以下代码片段:

{
    "instances": [1.0, 2.0, 5.0]
}

我们将从端点收到的响应数据也由以下的 JSON 数据组成:

{
    "predictions": [2.5, 3.0, 4.5]
}

您可以在官方文档中找到关于输入和输出 JSON 数据结构的详细解释:www.tensorflow.org/tfx/serving/api_rest

如果您有兴趣使用 gRPC 而不是 HTTP,您可以在aws.amazon.com/blogs/opensource/the-versatility-of-grpc-an-open-source-high-performance-rpc-framework找到详细信息。

恭喜!您已成功为您的模型创建了一个应用程序可以通过网络访问的端点。接下来,我们将介绍 Amazon EI 加速器,它可以降低推理延迟和 EKS 成本。

提高 EKS 端点性能使用 Amazon 弹性推理

在本节中,我们将描述如何使用 EI 加速器创建 EKS 集群,这是一种低成本的 GPU 加速。EI 加速器可以链接到 Amazon EC2 和 Sagemaker 实例或 eia2.* 类型的实例。eia2.* 实例的完整描述可以在 aws.amazon.com/machine-learning/elastic-inference/pricing 找到。

要充分利用 AWS 资源,您还需要使用 AWS Neuron (aws.amazon.com/machine-learning/neuron) 编译您的模型。Neuron 模型的优势在于它们可以利用 Amazon EC2 Inf1 实例。这些类型的机器包含 AWS Inferentia,这是 AWS 专为云中的 ML 设计的定制芯片 (aws.amazon.com/machine-learning/inferentia)。

AWS Neuron SDK 预安装在 AWS DL 容器和 Amazon Machine Images (AMI) 中。在本节中,我们将重点放在 TF 模型上。然而,PyTorch 模型的编译过程与此相同。有关 TF 的详细步骤可以在 docs.aws.amazon.com/dlami/latest/devguide/tutorial-inferentia-tf-neuron.html 找到,PyTorch 的步骤可以在 docs.aws.amazon.com/dlami/latest/devguide/tutorial-inferentia-pytorch-neuron.html 找到。

将 TF 模型编译为 Neuron 模型可以通过使用 TF 的 tf.neuron.saved_model.compile 函数来实现。

import tensorflow as tf
tf.neuron.saved_model.compile(
    tf_model_dir, # input TF model dir 
    neuron_model_dir # output neuron compiled model dir
)

对于此功能,我们只需提供输入模型的位置 (tf_model_dir) 和我们想要存储输出 Neuron 模型的位置 (neuron_model_dir)。正如我们将 TF 模型上传到 S3 存储桶以进行端点创建一样,我们还需要将 Neuron 模型移动到 S3 存储桶。

再次提到,您需要对 EKS 配置进行的更改仅需要在 Deployment 部分的 template 部分完成。以下代码片段描述了配置的更新部分:

       containers:
       - name: neuron-demo
         image: 763104351884.dkr.ecr.us-east-1.amazonaws.com/tensorflow-inference-neuron:1.15.4-neuron-py37-ubuntu18.04
         command:
         - /usr/local/bin/entrypoint.sh
         args:
         - --port=8500
         - --rest_api_port=9000
         - --model_name=neuron_model
         - --model_base_path=s3://mybucket/neuron_model/
         ports:
         - name: http
           containerPort: 8500 # HTTP port
         - name: gRPC
           containerPort: 9000 # gRPC port

从上述配置中我们注意到的第一件事是,它与我们在 在 EKS 上使用 TensorFlow 模型创建推断端点 部分描述的非常相似。区别主要来自于 imagecommandargs 部分。首先,我们需要使用带有 AWS Neuron 和 TensorFlow Serving 应用程序的 DL 容器(image: 763104351884.dkr.ecr.us-east-1.amazonaws.com/tensorflow-inference-neuron:1.15.4-neuron-py37-ubuntu18.04)。接下来,通过 command 键传递模型文件的入口点脚本:/usr/local/bin/entrypoint.sh。入口点脚本用于使用 args 启动 Web 服务器。要从 Neuron 模型创建端点,必须指定存储目标 Neuron 模型的 S3 存储桶作为 model_base_path 参数(--model_base_path=s3://mybucket/neuron_model/)。

要将更改应用到集群,只需将更新后的 YAML 文件传递给 kubectl apply 命令。

最后,我们将看一下 EKS 的自动缩放功能,以提高端点的稳定性。

动态调整 EKS 集群大小使用自动缩放

EKS 集群可以根据流量量自动调整集群大小。水平 Pod 自动缩放的理念是根据传入请求的增加来增加运行应用程序的 Pod 数量。类似地,当传入流量减少时,一些 Pod 将被释放。

通过 kubectl apply 命令部署应用程序后,可以使用 kubectl autoscale 命令设置自动缩放,如下所示:

kubectl autoscale deployment <application-name> --cpu-percent=60 --min=1 --max=10

如前例所示,kubectl autoscale 命令接受 YAML 文件的 Deployment 部分中指定的应用程序名称,cpu-percent(用于调整集群大小的 CPU 百分比阈值),min(要保留的最小 Pod 数),以及 max(要启动的最大 Pod 数)。总结一下,示例命令将根据流量量在 1 到 10 个 Pod 中运行服务,保持 CPU 使用率在 60%。

要记住的事情

a. EKS 旨在通过简化动态流量的复杂集群管理,为应用程序部署提供 Kubernetes 集群。

b. 使用 YAML 文件配置组成 Kubernetes 集群的机器和集群内运行的应用程序。配置的两个部分,DeploymentService,分别控制 Pod 内运行的应用程序,并为底层目标集群配置服务。

c. 可以使用 TF 和 PyTorch 模型在 EKS 集群上创建和托管推断端点。

d. 利用使用 AWS Neuron 编译的模型来利用 EI 加速器,可以提高推断延迟,同时节省 EKS 集群的运营成本。

b. 可以配置 EKS 集群根据流量量动态调整大小。

在这一部分中,我们讨论了基于 EKS 的 TF 和 PyTorch 模型部署。我们描述了如何使用 AWS Neuron 模型和 EI 加速器来提高服务性能。最后,我们介绍了自动扩展以更有效地利用可用资源。在下一部分中,我们将看看另一个 AWS 服务用于托管推理端点:SageMaker。

使用 SageMaker 进行推断

在本节中,您将学习如何使用 SageMaker 而不是 EKS 集群创建端点。首先,我们将描述创建推理端点的与框架无关的方法(即Model类)。然后,我们将查看使用TensorFlowModel和 TF 特定的Estimator类创建 TF 端点的方法。接下来的部分将重点介绍使用PyTorchModel类和 PyTorch 特定的Estimator类创建 PyTorch 模型的端点。此外,我们还将介绍如何从 ONNX 模型构建端点。到此为止,我们应该有一个正在为传入请求运行模型预测的服务。之后,我们将描述如何使用AWS SageMaker Neo和 EI 加速器来提高服务质量。最后,我们将介绍自动扩展并描述如何在单个端点上托管多个模型。

如在《第五章》的在云中利用 SageMaker 进行 ETL部分中所述,SageMaker 提供了一个名为 SageMaker Studio 的内置笔记本环境。我们在本节中包含的代码片段是为在这个笔记本中执行而准备的。

使用 Model 类设置推理端点

一般来说,SageMaker 提供三种不同的类来创建端点。最基本的是Model类,支持各种 DL 框架的模型。另一个选项是使用特定于框架的Model类。最后一个选项是使用Estimator类。在本节中,我们将看看第一种选项,即Model类。

在深入端点创建过程之前,我们需要确保已经适当地准备了必要的组件;为 SageMaker 配置了正确的 IAM 角色,并且训练好的模型应该在 S3 上可用。IAM 角色可以在笔记本中如下准备:

from sagemaker import get_execution_role
from sagemaker import Session
# IAM role of the notebook
role = get_execution_role()
# A Session object for SageMaker
sess = Session()
# default bucket object
bucket = sess.default_bucket()

在上述代码中,已设置了 IAM 访问角色和默认存储桶。要加载 SageMaker 笔记本的当前 IAM 角色,您可以使用sagemaker.get_execution_role函数。要创建一个 SageMaker 会话,您需要为Session类创建一个实例。Session实例的default_bucket方法将创建一个以sagemaker-{region}-{aws-account-id}格式命名的默认存储桶。

在将模型上传到 S3 存储桶之前,模型需要被压缩为.tar文件。以下代码片段描述了如何压缩模型并将压缩后的模型上传到笔记本中的目标存储桶:

import tarfile
model_archive = "model.tar.gz"
with tarfile.open(model_archive, mode="w:gz") as archive:
   archive.add("export", recursive=True) 
# model artifacts uploaded to S3 bucket
model_s3_path = sess.upload_data(path=model_archive, key_prefix="model")

在前面的代码片段中,使用tarfile库执行压缩。Session实例的upload_data方法用于将编译后的模型上传到与 SageMaker 会话链接的 S3 存储桶。

现在,我们准备创建一个Model类的实例。在这个特定的示例中,我们将假设该模型已经使用 TF 训练完成:

from sagemaker.tensorflow.serving import Model
# TF version
tf_framework_version = "2.8"
# Model instance for inference endpoint creation
sm_model = Model(
    model_data=model_s3_path, # S3 path for model
    framework_version=tf_framework_version, # TF version
    role=role) # IAM role of the notebook
predictor = sm_model.deploy(
    initial_instance_count=1, # number of instances used
    instance_type="ml.c5.xlarge")

如前面的代码所示,Model类的构造函数接受model_data(压缩模型文件位于的 S3 路径)、framework_version(TF 的版本)和role(笔记本的 IAM 角色)。Model实例的deploy方法处理实际的端点创建。它接受initial_instance_count(启动端点的实例数)和instance_type(要使用的 EC2 实例类型)。

另外,您可以提供一个指定的image和删除framework_version。在这种情况下,端点将使用为image参数指定的 Docker 镜像创建。它应指向 ECR 上的一个镜像。

接下来,我们将讨论如何使用创建的端点从笔记本触发模型推理。deploy方法将返回一个Predictor实例。如下代码片段所示,您可以通过Predictor实例的predict函数实现这一点。您只需传递表示输入的一些 JSON 数据:

input = {
    "instances": [1.0, 2.0, 5.0]
}
results = predictor.predict(input)

predict函数的输出results是 JSON 数据,我们的示例中如下所示:

{
    "predictions": [2.5, 3.0, 4.5]
}

predict函数支持不同格式的数据,例如 JSON、CSV 和多维数组。如果您需要使用除 JSON 以外的类型,请参考sagemaker.readthedocs.io/en/stable/frameworks/tensorflow/using_tf.html#tensorflow-serving-input-and-output

触发模型推理的另一种选项是使用boto3库中的SageMaker.Client类。SageMaker.Client类是代表 Amazon SageMaker 服务的低级客户端。在以下代码片段中,我们正在创建一个SageMaker.Client实例,并演示如何使用invoke_endpoint方法访问端点:

import boto3
client = boto3.client("runtime.sagemaker")
# SageMaker Inference endpoint name
endpoint_name = "run_model_prediction"
# Payload for inference which consists of the input data
payload = "..."
# SageMaker endpoint called to get HTTP response (inference)
response = client.invoke_endpoint(
   EndpointName=endpoint_name,
   ContentType="text/csv", # content type
   Body=payload # input data to the endpoint)

如前面的代码片段所示,invoke_endpoint方法接受EndpointName(端点名称;即run_model_prediction)、ContentType(输入数据类型;即"text/csv")和Body(用于模型预测的输入数据;即payload)。

实际上,许多公司利用 Amazon API Gateway (aws.amazon.com/api-gateway) 和 AWS Lambda (aws.amazon.com/lambda) 与 SageMaker 端点一起在无服务器架构中通信,以与部署的模型进行交互。有关详细设置,请参阅 aws.amazon.com/blogs/machine-learning/call-an-amazon-sagemaker-model-endpoint-using-amazon-api-gateway-and-aws-lambda

接下来,我们将解释创建端点的框架特定方法。

使用 TensorFlow 设置推断端点

在本节中,我们将描述专为 TF 设计的 Model 类 - TensorFlowModel 类。然后,我们将解释如何使用 TF 特定的 Estimator 类创建端点。本节代码片段的完整版本可以在 github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_9/sagemaker 找到。

使用 TensorFlowModel 类设置 TensorFlow 推断端点

TensorFlowModel 类是为 TF 模型设计的 Model 类。如下代码片段所示,该类可以从 sagemaker.tensorflow 模块导入,并且其使用方式与 Model 类相同:

from sagemaker.tensorflow import TensorFlowModel
# Model instance
sm_model = TensorFlowModel(
   model_data=model_s3_path,
   framework_version=tf_framework_version,
   role=role) # IAM role of the notebook
# Predictor
predictor = sm_model.deploy(
   initial_instance_count=1,
   instance_type="ml.c5.xlarge")

TensorFlowModel 类的构造函数接受与 Model 类相同的参数:上传模型的 S3 路径 (model_s3_path)、TF 框架版本 (Tf_framework_version) 和 SageMaker 的 IAM 角色 (role)。此外,您可以通过提供 entry_point 来提供用于模型推断输入和输出的预处理和后处理的 Python 脚本。在这种情况下,脚本需要命名为 inference.py。更多详情,请参阅 sagemaker.readthedocs.io/en/stable/frameworks/tensorflow/deploying_tensorflow_serving.html#providing-python-scripts-for-pre-post-processing

TensorFlowModel 类作为 Model 的子类,通过 deploy 方法也提供了一个 Predictor 实例。其使用方式与我们在前一节中描述的相同。

接下来,您将学习如何使用 Estimator 类部署您的模型,我们已经在第六章高效模型训练 中介绍了这一类用于 SageMaker 模型训练。

使用 Estimator 类设置 TensorFlow 推断端点

如在Chapter 6Efficient Model TrainingTraining a TensorFlow model using SageMaker部分介绍的那样,SageMaker 提供了支持在 SageMaker 上进行模型训练的Estimator类。同一类也可用于创建和部署推断端点。在下面的代码片段中,我们使用了为 TF 设计的Estimator类,即sagemaker.tensorflow.estimator.TensorFlow,来训练一个 TF 模型,并使用训练后的模型部署一个端点:

from sagemaker.tensorflow.estimator import TensorFlow
# create an estimator
estimator = TensorFlow(
    entry_point="tf-train.py",
    ...,
    instance_count=1,    
    instance_type="ml.c4.xlarge",
    framework_version="2.2",
    py_version="py37" )
# train the model
estimator.fit(inputs)
# deploy the model and returns predictor instance for inference
predictor = estimator.deploy(
    initial_instance_count=1, 
    instance_type="ml.c5.xlarge")

如前面的代码片段所示,sagemaker.tensorflow.estimator.TensorFlow类接受以下参数:entry_point(处理训练的脚本,即"tf-train.py")、instance_count(使用的实例数,即1)、instance_type(实例的类型,即"ml.c4.xlarge")、framework_version(PyTorch 的版本,即"2.2")和py_version(Python 的版本,即"py37")。Estimator实例的fit方法执行模型训练。用于创建和部署端点的关键方法是deploy方法,它基于提供的条件创建和托管一个端点:initial_instance_count1)实例,instance_type"ml.c5.xlarge")。Estimator类的deploy方法返回一个Predictor实例,就像Model类的情况一样。

本节中,我们解释了如何在 SageMaker 上为 TF 模型创建端点。在下一节中,我们将探讨 SageMaker 如何支持 PyTorch 模型。

设置一个 PyTorch 推断端点

本节旨在介绍在 SageMaker 上创建和托管 PyTorch 模型端点的不同方法。首先,我们将介绍为 PyTorch 模型设计的Model类:PyTorchModel类。接着,我们将描述 PyTorch 模型的Estimator类。本节中代码片段的完整实现可以在github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_9/sagemaker/pytorch-inference.ipynb找到。

使用PyTorchModel类设置 PyTorch 推断端点

类似于TensorFlowModel类,专门为 PyTorch 模型设计了一个Model类,即PyTorchModel。可以按以下方式实例化它:

from sagemaker.pytorch import PyTorchModel
model = PyTorchModel(
    entry_point="inference.py",
    source_dir="s3://bucket/model",
    role=role, # IAM role for SageMaker
    model_data=pt_model_data, # model file
    framework_version="1.11.0", # PyTorch version
    py_version="py3", # python version
)

如前面的代码片段所示,构造函数接受entry_point(定义数据的自定义前后处理逻辑)、source_dir(入口点脚本的 S3 路径)、role(SageMaker 的 IAM 角色)、model_data(模型的 S3 路径)、framework_version(PyTorch 的版本)和py_version(Python 的版本)作为参数。

由于PyTorchModel类继承自Model类,它提供了deploy函数,用于创建和部署端点,如使用 Model 类设置 PyTorch 推断端点章节中所述。

接下来,我们将介绍专为 PyTorch 模型设计的Estimator类。

使用 Estimator 类设置 PyTorch 推断端点

如果没有训练好的 PyTorch 模型可用,可以使用sagemaker.pytorch.estimator.PyTorch类来训练和部署模型。训练可以通过fit方法来完成,如Chapter 6章节使用 SageMaker 训练 PyTorch 模型中所述,高效的模型训练。作为Estimator类,sagemaker.pytorch.estimator.PyTorch类提供了与TensorFlow中的Estimator类相同的功能,我们在使用 Estimator 类设置 TensorFlow 推断端点章节中有所涉及。在下面的代码片段中,我们正在为 PyTorch 模型创建一个Estimator实例,训练模型,并创建一个端点:

from sagemaker.pytorch.estimator import PyTorch
# create an estimator
estimator = PyTorch(
    entry_point="pytorch-train.py",
    ...,
    instance_count=1,
    instance_type="ml.c4.xlarge",
    framework_version="1.11",
    py_version="py37")
# train the model
estimator.fit(inputs)
# deploy the model and returns predictor instance for inference
predictor = estimator.deploy(
   initial_instance_count=1, 
   instance_type="ml.c5.xlarge")

如前面的代码片段所示,sagemaker.pytorch.estimator.PyTorch的构造函数接受与为 TF 设计的Estimator类相同的参数集:entry_point(处理训练的脚本;即"pytorch-train.py")、instance_count(要使用的实例数量;即1)、instance_type(EC2 实例的类型;即"ml.c4.xlarge")、framework_version(PyTorch 版本;即"1.11.0")和py_version(Python 版本;即"py37")。模型训练(fit方法)和部署(deploy方法)的方式与使用 Estimator 类设置 TensorFlow 推断端点章节中的前例相同。

在本节中,我们介绍了如何以两种不同的方式部署 PyTorch 模型:使用PyTorchModel类和Estimator类。接下来,我们将学习如何在 SageMaker 上为 ONNX 模型创建端点。

建立一个来自 ONNX 模型的推断端点

如前一章节所述,Chapter 8章节简化深度学习模型部署中,DL 模型通常会转换为开放神经网络交换ONNX)模型进行部署。在本节中,我们将描述如何在 SageMaker 上部署 ONNX 模型。

最标准的方法是使用基础的Model类。如使用 Model 类设置 TensorFlow 推断端点章节中所述,Model类支持各种类型的 DL 模型。幸运的是,它也为 ONNX 模型提供了内置支持:

from sagemaker.model import Model
# Load an ONNX model file for endpoint creation
sm_model= Model(    
    model_data=model_data, # path for an ONNX .tar.gz file
    entry_point="inference.py", # an inference script
    role=role,
    py_version="py3",
    framework="onnx",
    framework_version="1.4.1", # ONNX version
)
# deploy model
predictor = sm_model.deploy(
   initial_instance_count=1, # number of instances to use
   instance_type=ml.c5.xlarge) # instance type for deploy

在上述示例中,我们在 S3 上有一个训练好的 ONNX 模型。在 Model 实例创建中,关键部分来自于 framework="onnx"。我们还需要为 framework_version 提供 ONNX 框架版本。在本例中,我们使用的是 ONNX 框架版本 1.4.0。其他所有内容几乎与之前的示例完全相同。再次强调,deploy 函数用于创建和部署端点;将返回一个 Predictor 实例用于模型预测。

常见做法还包括使用 TensorFlowModelPyTorchModel 类来从 ONNX 模型创建端点。以下代码片段展示了这些用例:

from sagemaker.tensorflow import TensorFlowModel
# Load ONNX model file as a TensorFlowModel
tf_model = TensorFlowModel(    
    model_data=model_data, # path to the ONNX .tar.gz file
    entry_point="tf_inference.py", 
    role=role,
    py_version="py3", # Python version
    framework_version="2.1.1", # TensorFlow version
)
from sagemaker.pytorch import PyTorchModel
# Load ONNX model file as a PyTorchModel
pytorch_model = PyTorchModel(
    model_data=model_data, # path to the ONNX .tar.gz file
    entry_point="pytorch_inference.py",
    role=role,
    py_version="py3", # Python version
    framework_version="1.11.0", # PyTorch version
)

上述代码片段不言自明。这两个类都接受一个 ONNX 模型路径 (model_data),一个推断脚本 (entry_point),一个 IAM 角色 (role),一个 Python 版本 (py_version),以及每个框架的版本 (framework_version)。与 Model 类如何部署端点一样,deploy 方法将从每个模型创建并托管一个端点。

尽管端点允许我们随时获取动态输入数据的模型预测结果,但有些情况下,您需要对存储在 S3 存储桶中的整个输入数据进行推断,而不是逐个馈送它们。因此,我们将看看如何利用 Batch Transform 来满足这一需求。

使用 Batch Transform 处理批量预测请求

我们可以利用 SageMaker 的 Batch Transform 功能(docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html)在一个队列中对大型数据集进行推断。使用 sagemaker.transformer.Transformer 类,您可以在 S3 上对任何数据集进行批量模型预测,而无需持久化端点。具体细节包含在以下代码片段中:

from sagemaker import transformer
bucket_name = "my-bucket" # S3 bucket with data
# location of the input data
input_location = "s3://{}/{}".format(bucket_name, "input_data")
# location where the predictions will be stored
batch_output = "s3://{}/{}".format(bucket_name, "batch-results")
# initialize the transformer object
transformer = transformer.Transformer(
   base_transform_job_name="Batch-Transform", # job name
   model_name=model_name, # Name of the inference endpoint
   max_payload= 5, # maximum payload
   instance_count=1, # instance count to start with
   instance_type="ml.c4.xlarge", # ec2 instance type
   output_path=batch_output # S3 for batch inference output)
# triggers the prediction on the whole dataset
tf_transformer = transformer.transformer(
   input_location, # input S3 path for input data
   content_type="text/csv", # input content type as CSV
   split_type="Line" # split type for input as Line)

如上述代码所示,sagemaker.transformer.Transformer 类接受 base_transformer_job_name(转换作业的作业名称)、model_name(保存推断流水线的模型名称)、max_payload(允许的最大有效载荷,以 MB 为单位)、instance_count(要启动的 EC2 实例数)、instance_type(EC2 实例的类型)和 output_path(存储输出的 S3 路径)。transformer 方法将触发指定数据集上的模型预测。它接受以下参数:input_location(输入数据所在的 S3 路径)、content_type(输入数据的内容类型;即 "text/csv")、以及 split_type(控制如何分割输入数据;使用 "Line" 将数据的每一行作为模型的单独输入)。实际上,许多公司还利用 SageMaker 处理作业(docs.aws.amazon.com/sagemaker/latest/APIReference/API_ProcessingJob.html)执行批量推断,但我们不会详细讨论此事。

到目前为止,我们已经了解了 SageMaker 如何支持托管推断终端以处理实时预测请求,并对在 S3 上静态数据集进行模型预测。接下来,我们将描述如何使用 AWS SageMaker Neo 进一步提高部署模型的推断延迟。

提升 SageMaker 终端性能,利用 AWS SageMaker Neo

在本节中,我们将解释 SageMaker 如何通过利用底层硬件资源(EC2 实例或移动设备)进一步提升应用程序的性能。其思想是使用 AWS SageMaker Neo 编译经过训练的 DL 模型(aws.amazon.com/sagemaker/neo)。编译后,生成的 Neo 模型可以更好地利用底层设备,从而降低推断延迟。AWS SageMaker Neo 支持不同框架的模型(TF、PyTorch、MxNet 和 ONNX),以及各种类型的硬件(操作系统、芯片、架构和加速器)。支持的完整资源列表详见 docs.aws.amazon.com/sagemaker/latest/dg/neo-supported-devices-edge-devices.html

可通过 Model 类的 compile 方法生成 Neo 模型。compile 方法返回一个支持终端创建的 Estimator 实例。让我们看下面的示例以获取详细信息:

# sm_model created from Model
sm_model = Model(...)
# instance type of which the model will be optimized for
instance_family = "ml_c5"
# DL framework
framework = "tensorflow"
compilation_job_name = "tf-compile"
compiled_model_path = "s3:..."
# shape of an input data
data_shape = {"inputs":[1, data.shape[0], data.shape[1]]}
estimator = sm_model.compile(
   target_instance_family=instance_family,
   input_shape=data_shape,
   ob_name=compilation_job_name,
   role=role,
   framework=framework,
   framework_version=tf_framework_version,
   output_path=compiled_model_path)
# deploy the neo model on instances of the target type
predictor = estimator.deploy(
   initial_instance_count=1,
   instance_type=instance_family)

在前述代码中,我们从名为 sm_modelModel 实例开始。我们调用 compile 方法将加载的模型编译为 Neo 模型。以下是参数列表的描述:

  • target_instance_family:优化模型的 EC2 实例类型

  • input_shape:输入数据的形状

  • job_name:编译作业的名称

  • role:编译模型输出的 IAM 角色

  • framework:如 TF 或 PyTorch 的 DL 框架

  • framework_version:要使用的框架版本

  • output_path:编译模型将存储的输出 S3 路径

Estimator实例包括一个deploy函数,用于创建端点。输出是一个Predictor实例,您可以使用它来运行模型预测。在上述示例中,我们优化了我们的模型,以在ml_c5类型的实例上表现最佳。

接下来,我们将描述如何将 EI 加速器集成到运行在 SageMaker 上的端点中。

使用 Amazon Elastic Inference 改善 SageMaker 端点性能

使用 Amazon Elastic Inference 改善 EKS 端点性能部分,我们描述了如何利用 EI 加速器来降低推断端点的运行成本,同时通过利用可用的 GPU 设备来提高推断延迟。在本节中,我们将涵盖 SageMaker 的 EI 加速器集成。

必要的更改非常简单;您只需在触发Model实例的deploy方法时提供accelerator_type即可:

# deploying a Tensorflow/PyTorch/other model files using EI
predictor = sm_model.deploy(
   initial_instance_count=1, # ec2 initial count
   instance_type="ml.m4.xlarge", # ec2 instance type
   accelerator_type="ml.eia2.medium" # accelerator type)

在上述代码中,deploy方法为给定的Model实例创建端点。要将 EI 加速器附加到端点上,您需要在默认参数(initial_instance_countinstance_type)之上指定所需的加速器类型(accelerator_type)。有关在 SageMaker 端点中使用 EI 的完整说明,请参阅docs.aws.amazon.com/sagemaker/latest/dg/ei.html

在接下来的部分,我们将探讨 SageMaker 的自动扩展功能,这使我们能够更好地处理传入流量的变化。

使用自动扩展动态调整 SageMaker 端点的大小

类似于 EKS 集群支持自动扩展根据流量变化自动调整端点的大小,SageMaker 也提供了自动扩展功能。配置自动扩展涉及配置扩展策略,该策略定义了何时进行扩展以及在扩展时创建和销毁多少资源。可以从 SageMaker Web 控制台配置 SageMaker 端点的扩展策略。以下步骤描述了如何在从 SageMaker 笔记本创建的推断端点上配置自动扩展:

  1. 访问 SageMaker Web 控制台,console.aws.amazon.com/sagemaker/,并在左侧导航面板中的推断下点击端点。您可能需要提供凭据以登录。

  2. 接下来,您必须选择要配置的端点名称。在端点运行时设置下,选择需要配置的模型变体。此功能允许您在单个端点中部署多个模型版本,每个版本都会启动一个容器。有关此功能的详细信息,请参见 docs.aws.amazon.com/sagemaker/latest/APIReference/API_runtime_InvokeEndpoint.html

  3. 端点运行时设置下,选择配置自动扩展。这将带您进入配置变体自动扩展页面:

图 9.1 – SageMaker Web 控制台的配置变体自动扩展页面

图 9.1 – SageMaker Web 控制台的配置变体自动扩展页面

  1. 最小实例数字段中输入要维护的最小实例数。最小值为 1。此值定义将始终保留的最小实例数量。

  2. 最大实例数字段中输入要维护的扩展策略的最大实例数。此值定义了在峰值流量时允许的最大实例数量。

  3. 填写SageMakerVariantInvocationsPerInstance字段。每个端点可以在一个或多个 EC2 实例上托管的单个端点中部署多个模型(或模型版本)。SageMakerVariantInvocationsPerInstance定义了每个模型变体每分钟允许的最大调用次数。此值用于负载均衡。有关计算此字段正确数量的详细信息,请参见 docs.aws.amazon.com/sagemaker/latest/dg/endpoint-scaling-loadtest.html

  4. 填写缩放缓冲时间和扩展缓冲时间。这些指示 SageMaker 在检查下一轮缩放之前等待多长时间。

  5. 选择禁用缩放复选框。在流量增加时,作为扩展过程的一部分会启动更多实例。但是,如果在增加后立即减少流量,这些实例可能会在缩小过程中被快速删除。为避免新创建的实例在创建后立即释放,必须选择此复选框。

  6. 单击保存按钮以应用配置。

一旦单击保存按钮,将会将扩展应用于所选的模型变体。SageMaker 将根据传入的流量增加和减少实例数量。有关自动扩展的更多详细信息,请参阅 docs.aws.amazon.com/autoscaling/ec2/userguide/as-instance-termination.html

作为基于 SageMaker 的端点的最后一个主题,我们将描述如何通过单个端点部署多个模型。

在单个 SageMaker 推断端点上托管多个模型

SageMaker 支持通过 多模态端点 (MME) 部署多个模型在一个端点上。在设置 MME 之前,有几件事情需要牢记。首先,建议如果希望保持低延迟,则设置多个端点。其次,容器只能部署来自同一 DL 框架的模型。对于希望托管来自不同框架的模型的用户,建议阅读 docs.amazonaws.cn/en_us/sagemaker/latest/dg/multi-container-direct.html。当模型大小和预期表现相似时,MME 的效果最佳。

以下步骤描述了如何设置 MME:

  1. 使用您的 AWS 凭证访问 SageMaker Web 控制台 console.aws.amazon.com/sagemaker

  2. 在左侧导航面板的 推理 部分下选择 模型。然后,点击右上角的 创建模型 按钮。

  3. 模型名称 字段输入一个值。这将用于在 SageMaker 上下文中唯一标识目标模型。

  4. 选择一个具有 AmazonSageMakerFullAccess IAM 策略的 IAM 角色。

  5. 容器定义部分,选择多个模型选项,并提供推理代码镜像的位置以及模型工件的位置(参见 图 9.2):

图 9.2 – SageMaker Web 控制台的多模态端点配置页面

图 9.2 – SageMaker Web 控制台的多模态端点配置页面

前一个字段用于使用自定义 Docker 镜像部署您的模型(docs.aws.amazon.com/sagemaker/latest/dg/your-algorithms-inference-code.html)。在此字段中,您应提供镜像注册路径,其中镜像位于 Amazon ECR 内。后一个字段指定了模型工件所在的 S3 路径。

  1. 此外,填写 容器主机名 字段。这指定了推理代码镜像将被创建的主机的详细信息。

  2. 在最后选择 创建模型 按钮。

配置了 MME 后,可以使用 boto3 库中的 SageMaker.Client 来测试端点,如下面的代码片段所示:

import boto3
# Sagemaker runtime client instance
runtime_sagemaker_client = boto3.client("sagemaker-runtime")
# send a request to the endpoint targeting  specific model 
response = runtime_sagemaker_client.invoke_endpoint(
   EndpointName="<ENDPOINT_NAME>",
   ContentType="text/csv",
   TargetModel="<MODEL_FILENAME>.tar.gz",
   Body=body)

在上述代码中,SageMaker.Client实例的invoke_endpoint函数向创建的端点发送请求。invoke_endpoint函数接受EndpointName(创建的端点的名称)、ContentType(请求主体中的数据类型)、TargetModel(以.tar.gz格式压缩的模型文件;用于指定请求将调用的目标模型)和BodyContentType中的输入数据)。从调用返回的response变量包含预测结果。有关与端点通信的完整描述,请参阅docs.aws.amazon.com/sagemaker/latest/dg/invoke-multi-model-endpoint.html

记住的事情

a. SageMaker 通过其内置的Model类和Estimator类支持端点创建。这些类支持使用各种 DL 框架(包括 TF、PyTorch 和 ONNX)训练的模型。专为 TF 和 PyTorch 框架设计的Model类也存在:TensorFlowModelPyTorchModel

b. 使用 AWS SageMaker Neo 编译模型后,模型可以更好地利用底层硬件资源,表现出更高的推理性能。

c. SageMaker 可以配置为使用 EI 加速器,从而降低推理端点的运行成本并提高推理延迟。

d. SageMaker 包含自动扩展功能,根据传入流量的量动态调整端点的规模。

e. SageMaker 支持通过 MME 在单个端点上部署多个模型。

在本节中,我们描述了 SageMaker 为部署 DL 模型作为推理端点提供的各种功能。

摘要

在本章中,我们描述了两种最受欢迎的 AWS 服务,专为将 DL 模型部署为推理端点设计:EKS 和 SageMaker。对于这两个选项,我们从最简单的设置开始:使用 TF、PyTorch 或 ONNX 模型创建推理端点。然后,我们解释了如何使用 EI 加速器、AWS Neuron 和 AWS SageMaker Neo 来提高推理端点的性能。我们还介绍了如何设置自动扩展以更有效地处理流量变化。最后,我们讨论了 SageMaker 的 MME 功能,用于在单个推理端点上托管多个模型。

在下一章中,我们将探讨各种模型压缩技术:网络量化、权重共享、网络修剪、知识蒸馏和网络架构搜索。这些技术将进一步提高推理效率。

第十章:提高推理效率

深度学习DL)模型部署在边缘设备上时,推理效率通常令人不满意。这些问题主要源于训练网络的大小,因为它需要大量计算。因此,许多工程师和科学家在将 DL 模型部署到边缘设备上时通常会在速度和准确性之间进行权衡。此外,他们专注于减少模型大小,因为边缘设备通常具有有限的存储空间。

在本章中,我们将介绍一些技术,以改善推理延迟,同时尽可能保持原始性能。首先,我们将讨论网络量化,这是一种通过使用较低精度的数据格式来减小网络尺寸的技术。接下来,我们将谈论权重共享,也被称为权重聚类。这是一个非常有趣的概念,其中少量模型权重值在整个网络中共享,从而减少存储训练模型所需的磁盘空间。我们还将讨论网络修剪,它涉及消除网络内部的不必要连接。虽然这三种技术最受欢迎,但我们还将介绍另外两个有趣的主题:知识蒸馏网络架构搜索。这两种技术通过直接在训练期间修改网络架构来实现模型大小的减小和推理延迟的改进。

在本章中,我们将介绍以下主要内容:

  • 网络量化 – 减少模型参数使用的位数

  • 权重共享 – 减少不同权重值的数量

  • 网络修剪 – 消除网络内部的不必要连接

  • 知识蒸馏 – 通过模仿预测获得更小的网络

  • 网络架构搜索 – 寻找最有效的网络架构

技术要求

您可以从本书的 GitHub 存储库下载本章的补充材料,链接为github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_10

在深入讨论各个技术之前,我们想介绍两个构建在TensorFlowTF)之上的库。第一个是TensorFlow LiteTF Lite),它负责在移动设备、微控制器和其他边缘设备上部署 TF 模型(www.tensorflow.org/lite)。我们将描述的一些技术仅适用于 TF Lite。另一个库称为 TensorFlow Model Optimization Toolkit。此库旨在为 TF 模型提供各种优化技术(www.tensorflow.org/model_optimization)。

网络量化 – 减少模型参数使用的位数

如果我们详细看一下 DL 模型训练,您会注意到模型学习处理噪声输入。换句话说,模型试图为其训练的数据构建一般化,以便即使在传入数据中存在一些噪声时,它也能生成合理的预测。此外,DL 模型在训练后最终会使用特定范围的数值进行推断。基于这种思路,网络量化旨在为这些值使用更简单的表示。

图 10.1 所示,网络量化,也称为模型量化,是将模型与之交互的数值范围重新映射到可以用较少比特表示的数字系统的过程 - 例如,使用 8 位而不是 32 位来表示浮点数。这样的修改在 DL 模型部署中具有额外的优势,因为边缘设备通常不支持基于 32 位浮点数的稳定算术:

图 10.1 – 展示网络量化中从浮点 32 到整数 8 的数字系统重映射的插图

图 10.1 – 展示网络量化中从浮点 32 到整数 8 的数字系统重映射的插图

不幸的是,网络量化涉及的不仅仅是将高精度数字转换为低精度。这是因为 DL 模型推断涉及产生比输入精度更高的数字的算术。在本章中,我们将探讨网络量化中以不同方式克服挑战的各种选项。如果您对了解更多关于网络量化的信息感兴趣,我们推荐阅读 Gholami 等人的《用于高效神经网络推断的量化方法综述》。

网络量化技术可以分为两个领域。第一个是后训练量化,另一个是量化感知训练。前者旨在量化已经训练过的模型,而后者通过以较低精度训练模型来减少由量化过程引起的精度降低。

幸运的是,这两种技术在标准 DL 框架中都可用:TF 和 PyTorch。在接下来的章节中,我们将看看如何在这些框架中执行网络量化。

执行后训练量化

首先,我们将看看 TF 和 PyTorch 如何支持后训练量化。修改非常简单,只需要几行额外的代码。让我们从 TF 开始。

在 TensorFlow 中执行后训练量化

默认情况下,DL 模型在必要的计算和变量中使用 32 位浮点数。在以下示例中,我们将演示动态范围量化,其中仅将固定参数(如权重)量化为使用 16 位而不是 32 位。请注意,您需要安装 TF Lite 来进行 TF 中的后训练量化:

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.float16]
tflite_quant_model = converter.convert()

从量化中,我们获得了一个 TF Lite 模型。在上述代码片段中,我们使用tf.lite.TFLiteConverter.from_saved_model函数加载训练好的 TF 模型,并获取了一个量化的 TF Lite 模型。在触发转换之前,我们需要配置一些东西。首先,我们必须设置量化模型权重的优化策略(converter.optimizations = [tf.lite.Optimize.DEFAULT])。然后,我们需要指定我们希望从量化中获取 16 位权重(converter.target_spec.supported_types = [tf.float16])。实际的量化发生在触发convert函数时。在上述代码中,如果我们不为supported_types指定 16 位浮点类型,我们将使用 8 位整数量化模型。

接下来,我们想介绍全整数量化,其中模型推断的每个组件(输入、激活以及权重)都被量化为较低的精度。对于这种类型的量化,您需要提供一个代表性数据集来估计激活的范围。让我们看下面的示例:

import tensorflow as tf
# A set of data for estimating the range of numbers that the inference requires
representative_dataset = …
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8  # or tf.uint8
converter.inference_output_type = tf.int8  # or tf.uint8
tflite_quant_model = converter.convert()

上述代码几乎是自说明的。再次使用TFLiteConverter类进行量化。首先,我们配置优化策略(converter.optimizations = [tf.lite.Optimize.DEFAULT]),并提供一个代表性数据集(converter.representative_dataset = representative_dataset)。接下来,我们设置 TF 优化以进行整数表示。此外,我们还需要通过配置target_specinference_input_typeinference_output_type来指定输入和输出数据类型。最后一行的convert函数触发量化过程。

TF 中的两种后训练量化类型被详细解释在www.tensorflow.org/model_optimization/guide/quantization/post_training

接下来,我们将看看 PyTorch 如何实现后训练量化。

在 PyTorch 中执行后训练量化

对于 PyTorch,在后训练量化中有两种不同的方法:动态量化静态量化。它们的区别在于量化发生的时间点,并且具有不同的优缺点。在本节中,我们将为每种算法提供高级描述以及代码示例。

动态量化 - 在运行时对模型进行量化

首先,我们将详细介绍动态量化,在 PyTorch 中是可用的最简单形式的量化。这种类型的算法在权重上提前应用量化,而在推断期间动态进行激活的量化。因此,动态量化通常用于模型执行主要受加载权重限制的情况,而计算矩阵乘法不是问题。这种类型的量化通常用于 LSTM 或 Transformer 网络。

给定训练好的模型,可以按如下方式实现动态量化。完整示例可在pytorch.org/tutorials/recipes/recipes/dynamic_quantization.html找到:

import torch
model = …
quantized_model = torch.quantization.quantize_dynamic(
    model,  # the original model
    qconfig_spec={torch.nn.Linear},  # a set of layers to quantize
    dtype=torch.qint8)  # data type which the quantized tensors will be

要应用动态量化,您需要将训练好的模型传递给torch.quantization.quantize_dynamic函数。另外两个参数分别指定要应用量化的模块集(qconfig_spec={torch.nn.Linear})和量化张量的目标数据类型(dtype=torch.qint8)。在此示例中,我们将Linear层量化为 8 位整数。

接下来,让我们看一下静态量化。

静态量化 - 使用代表性数据集确定最佳量化参数

另一种量化方法称为静态量化。像 TF 的完全整数量化一样,这种量化通过使用代表性数据集估计模型与之交互的数字范围,以最小化模型性能下降。

不幸的是,静态量化比动态量化需要更多的编码。首先,您需要在网络前后插入torch.quantization.QuantStubtorch.quantization.DeQuantStub操作,以进行必要的张量转换:

import torch
# A model with few layers
class OriginalModel(torch.nn.Module):
    def __init__(self):
        super(M, self).__init__()
        # QuantStub converts the incoming floating point tensors into a quantized tensor
        self.quant = torch.quantization.QuantStub()
        self.linear = torch.nn.Linear(10, 20)
        # DeQuantStub converts the given quantized tensor into a tensor in floating point
        self.dequant = torch.quantization.DeQuantStub()
    def forward(self, x):
        # using QuantStub and DeQuantStub operations, we can indicate the region for quantization
        # point to quantized in the quantized model
        x = self.quant(x)
        x = self.linear(x)
        x = self.dequant(x)
        return x

在上述网络中,我们有一个单独的Linear层,但在__init__函数中还有两个额外的操作初始化:torch.quantization.QuantStubtorch.quantization.DeQuantStub。前者用于输入张量以指示量化的开始。后者作为forward函数中的最后一个操作以指示量化的结束。以下代码片段描述了静态量化的第一步 - 校准过程:

# model is instantiated and trained
model_fp32 = OriginalModel()
…
# Prepare the model for static quantization
model_fp32.eval()
model_fp32.qconfig = torch.quantization.get_default_qconfig('fbgemm')
model_fp32_prepared = torch.quantization.prepare(model_fp32)
# Determine the best quantization settings by calibrating the model on a representative dataset.
calibration_dataset = …
model_fp32_prepared.eval()
for data, label in calibration_dataset:
    model_fp32_prepared(data)

上述代码片段以训练好的模型model_fp32开头。为了将模型转换为用于校准过程的中间格式,您需要附加一个量化配置(model_fp32.qconfig)并将模型传递给torch.quantization.prepare方法。如果模型推断在服务器实例上运行,则必须将模型的qconfig属性设置为torch.quantization.get_default_qconfig('fbgemm')。如果目标环境是移动设备,则必须向get_default_qconfig函数传入'qnnpack'。通过将代表性数据集传递给生成的模型model_fp32_prepared,可以实现校准过程。

最后一步是将校准模型转换为量化模型:

model_int8 = torch.quantization.convert(model_fp32_prepared)

在上述代码行中,torch.quantization.convert操作量化了校准模型(model_fp32_prepared)并生成了模型的量化版本(model_int8)。

关于静态量化的其他详细信息可在pytorch.org/tutorials/advanced/static_quantization_tutorial.html找到。

在接下来的部分,我们将描述如何在 TF 和 PyTorch 中执行量化感知训练。

执行量化感知训练

后训练量化可以显著减少模型大小。然而,它可能会大幅降低模型的准确性。因此,以下问题产生了:我们能否恢复部分丢失的准确性?这个问题的答案可能是量化感知训练QAT)。在这种情况下,模型在训练之前被量化,以便可以直接使用较低精度的权重和激活进行泛化学习。

首先,让我们看看如何在 TF 中实现这一点。

TensorFlow 中的量化感知训练

TF 通过 TensorFlow Model Optimization Toolkit 提供量化感知训练(QAT)。以下代码片段描述了如何在 TF 中设置 QAT:

import tensorflow_model_optimization as tfmot
# A TF model
model = … 
q_aware_model = tfmot.quantization.keras.quantize_model(model)
q_aware_model.compile(
              optimizer=...,
              loss=...,
              metrics=['accuracy'])
q_aware_model.fit(...)

如您所见,我们使用了 tfmot.quantization.keras.quantize_model 函数来设置 QAT 模型。输出模型需要使用 compile 函数进行编译,并可以使用 fit 函数进行训练,就像普通 TF 模型一样。令人惊讶的是,这就是您所需的全部。训练过的模型已经被量化,并应该提供比后训练量化生成的模型更高的准确性。

欲了解更多详情,请参阅原始文档:www.tensorflow.org/model_optimization/guide/quantization/training_comprehensive_guide

接下来,我们将看一下 PyTorch 的情况。

PyTorch 中的量化感知训练

在 PyTorch 中,QAT 经历了类似的过程。在训练过程中,会对必要的计算进行处理,这些计算会被夹紧和四舍五入,以模拟量化效果。完整的细节可以在 pytorch.org/docs/stable/quantization.html#quantization-aware-training-for-static-quantization 找到。让我们看看如何为 PyTorch 模型设置 QAT。

设置 QAT 的过程几乎与我们在“静态量化 - 使用代表性数据集确定最佳量化参数”部分所经历的过程相同。对于静态量化和 QAT,模型都需要进行相同的修改;需要在模型定义中插入 torch.quantization.QuantStubtorch.quantization.DeQuantStub 操作,以指示量化区域。主要区别来自网络的中间表示,因为 QAT 包括在整个训练过程中更新模型参数。以下代码片段更好地描述了这种差异:

model_fp32 = OriginalModel()
# model must be set to train mode for QAT
model_fp32.train()
model_fp32.qconfig = torch.quantization.get_default_qat_qconfig('fbgemm')
model_fp32_prepared = torch.quantization.prepare_qat(model_fp32_fused)
# train the model
for data, label in train_dataset:
    pred = model_fp32_prepared(data)
    ...
# Generate quantized version of the trained model
model_fp32_prepared.eval()
model_int8 = torch.quantization.convert(model_fp32_prepared)

在前面的示例中,我们使用了在Static quantization – determining optimal quantization parameters using a representative dataset部分定义的相同网络:OriginalModel。模型应处于 QAT(model_fp32.train())的train模式。在这里,我们假设模型将部署在服务器实例上:torch.quantization.get_default_qat_qconfig('fbgemm')。在 QAT 的情况下,模型的中间表示是通过将原始模型传递给 torch.quantization.prepare_qat 函数来创建的。您需要训练中间表示(model_fp32_prepared)而不是原始模型(model_fp32)。完成训练后,您可以使用 torch.quantization.convert 函数生成量化模型。

总体而言,我们研究了 TF 和 PyTorch 如何提供 QAT 以最小化量化对模型精度的降低。

需记住的事项

a. 网络量化是一种简单的技术,通过将处理数字的精度降低来减少推断延迟。

b. 有两种类型的网络量化:后训练量化,将量化应用于已经训练好的模型,以及 QAT,通过低精度训练模型来最小化精度降低。

c. TF 和 PyTorch 支持在训练代码中进行最小修改的后训练量化和 QAT。

在接下来的部分中,我们将看看另一种改善推断延迟的选项:权重共享。

权重共享 - 减少不同权重值的数量

权重共享权重聚类是另一种可以显著减小模型大小的技术。这种技术背后的想法相当简单:让我们将权重聚合成组(或簇),并使用中心值而不是单独的权重值。在这种情况下,我们可以仅存储每个中心点的值,而不是每个权重值。因此,我们可以显著压缩模型大小,并可能加快推断过程。权重共享的关键思想在Figure 10.2中有图形化展示(改编自官方 TF 博客文章关于权重聚类 API: blog.tensorflow.org/2020/08/tensorflow-model-optimization-toolkit-weight-clustering-api.html):

图 10.2 - 权重共享的示例插图

图 10.2 - 权重共享的示例插图

让我们先学习如何在 TF 中执行权重共享,然后再看如何在 PyTorch 中执行相同操作。

在 TensorFlow 中执行权重共享

TF 提供了针对 SequentialFunctional TF 模型的权重共享,通过 TensorFlow Model Optimization Toolkit (www.tensorflow.org/model_optimization/guide/clustering/clustering_example) 实现。

首先,您需要定义如下代码片段所示的聚类配置:

import tensorflow_model_optimization as tfmot
# A trained model to compress
tf_model = ...
CentroidInitialization = tfmot.clustering.keras.CentroidInitialization
clustering_params = {
  'number_of_clusters': 10,
  'cluster_centroids_init': CentroidInitialization.LINEAR
}
clustered_model = tfmot.clustering.keras.cluster_weights(tf_model, **clustering_params)

如您所见,权重聚类涉及tfmot.clustering.keras.cluster_weights函数。 我们需要提供训练好的模型(tf_model)和一个聚类配置(clustering_params)。 聚类配置定义了集群的数量以及如何初始化每个集群。 在此示例中,我们生成了 10 个集群,并使用线性质心初始化(集群质心将均匀分布在最小值和最大值之间)。 可以在www.tensorflow.org/model_optimization/api_docs/python/tfmot/clustering/keras/CentroidInitialization找到其他集群初始化选项。

生成带有聚类权重的模型后,您可以使用tfmot.clustering.keras.strip_clustering函数删除推断期间不需要的所有变量:

final_model = tfmot.clustering.keras.strip_clustering(clustered_model) 

接下来,我们将看看如何在 PyTorch 中执行权重共享。

在 PyTorch 中执行权重共享

不幸的是,PyTorch 不支持权重共享。 相反,我们将提供可能实现的高级描述。 在此示例中,我们将尝试实现描述在Figure 10.2中的操作。 首先,在模型实现中添加一个名为cluster_weights的自定义函数,您可以在训练后调用该函数以对权重进行聚类。 然后,forward方法将需要稍作修改,如下面的代码片段所述:

from torch.nn import Module
class SampleModel(Module):
# in the case of PyTorch Lighting, we inherit pytorch_lightning.LightningModule class
  def __init__(self):
    self.layer = …
    self.weights_cluster = … # cluster index for each weight
    self.weights_mapping = … # mapping from a cluster index to a centroid value
  def forward(self, input):
    if self.training: # in training mode
      output = self.layer(input)
    else: # in eval mode
      # update weights of the self.layer by reassigning each value based on self.weights_cluster and self.weights_mapping
    output = self.layer(input)
    return output
def cluster_weights(self):
  # cluster weights of the layer 
  # construct a mapping from a cluster index to a centroid value and store at self.weights_mapping
  # find cluster index for each weight value and store at self.weights_cluster
  # drop the original weights to reduce the model size
# First, we instantiate a model to train
model = SampleModel()
# train the model
…
# perform weight sharing
model.cluster_weights()
model.eval()

前面的代码应该是自解释的,因为它是带有注释的伪代码,解释了关键操作。 首先,模型被训练,就像是正常模型一样。 当触发cluster_weights函数时,权重被聚类,并且权重共享所需的信息存储在类内部;每个权重的集群索引存储在self.weights_cluster中,并且每个集群的质心值存储在self.weights_mapping中。 当模型处于eval模式时,forward操作使用从self.weights_clusterself.weights_mapping构建的不同权重集。 另外,您可以添加功能以丢弃部署期间不需要的现有权重以减小模型大小。 我们在我们的存储库中提供了完整的实现:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/blob/main/Chapter_10/weight_sharing_pytorch.ipynb.

记住的事情

a. 权重共享通过将不同的权重值分组并用质心值替换来减小模型大小。

b. TF 通过 TensorFlow Model Optimization Toolkit 提供了权重共享,但 PyTorch 不提供任何支持。

接下来,让我们学习另一种流行的技术,称为网络修剪。

网络修剪 - 消除网络内不必要的连接

网络修剪是一种优化过程,可以消除不必要的连接。这种技术可以在训练后应用,但也可以在训练期间应用,从而进一步减少模型精度下降。连接更少意味着需要的权重更少。因此,我们可以减小模型大小以及推断延迟。在接下来的章节中,我们将介绍如何在 TF 和 PyTorch 中应用网络修剪。

希望这个翻译能够满足你的要求!

像模型量化和权重共享一样,TF 的网络修剪可以通过 TensorFlow 模型优化工具包实现。因此,进行网络修剪的第一步是使用以下代码行导入该工具包:

import tensorflow_model_optimization as tfmot

在训练过程中应用网络修剪,您必须使用tfmot.sparsity.keras.prune_low_magnitude函数修改您的模型:

# data and configurations for training
x_train, y_train, x_text, y_test, x_valid, y_valid,  num_examples_train, num_examples_test, num_examples_valid  = …
batch_size = ...
end_step = np.ceil(num_examples_train / batch_size).astype(np.int32) * epochs
# pruning configuration
pruning_params = {
      'pruning_schedule': tfmot.sparsity.keras.PolynomialDecay(initial_sparsity=0.3,
final_sparsity=0.5,
begin_step=0,
end_step=end_step)}
#  Prepare a model that will be pruned
model = ...
model_for_pruning = tfmot.sparsity.keras.prune_low_magnitude(model, **pruning_params)

在前述代码中,我们通过为prune_low_magnitude函数提供模型和一组参数pruning_params来配置网络修剪。正如您所见,我们应用了PolynomialDecay修剪,该修剪通过在训练过程中从特定稀疏性(initial_sparsity)开始构建到目标稀疏性的网络(www.tensorflow.org/model_optimization/api_docs/python/tfmot/sparsity/keras/PolynomialDecay)。正如最后一行所示,prune_low_magnitude函数返回另一个在训练期间执行网络修剪的模型。

在我们查看需要进行训练循环修改之前,我们想介绍另一种修剪配置,即tfmot.sparsity.keras.ConstantSparsitywww.tensorflow.org/model_optimization/api_docs/python/tfmot/sparsity/keras/ConstantSparsity)。该修剪配置通过整个训练过程应用恒定稀疏性修剪。要应用此类型的网络修剪,您可以简单地按照以下代码片段修改pruning_params

pruning_params = {
      'pruning_schedule': tfmot.sparsity.keras.ConstantSparsity(0.5, begin_step=0, frequency=100) }

如下代码片段所示,训练循环需要进行一项额外的修改以进行回调配置;我们需要使用一个 Keras 回调函数,该函数对每个优化器步骤应用修剪 - 即tfmot.sparsity.keras.UpdatePruningStep

model_for_pruning.compile(…)
callbacks = [tfmot.sparsity.keras.UpdatePruningStep()]
model_for_pruning.fit(x_train, y_train,
    batch_size=batch_size, epochs=epochs,     validation_data=(x_valid, y_vallid),
    callbacks=callbacks)

前述代码编译了已准备好进行网络修剪和进行训练的模型。请记住,关键变化来自于为fit函数指定的tfmot.sparsity.keras.UpdatePruningStep回调函数。

最后,你可以通过将模型传递到 tfmot.sparsity.keras.strip_pruning 函数来更新训练过的模型,以仅保留稀疏权重。所有不必要用于模型推理的 tf.Variable 实例都将被丢弃:

final_tf_model = tfmot.sparsity.keras.strip_pruning(model_for_pruning)

提供的示例可以直接应用于 FunctionalSequential TF 模型。要对特定层或模型子集应用修剪,需要进行以下修改:

def apply_pruning_to_dense(layer):
    if isinstance(layer, tf.keras.layers.Dense):
        return tfmot.sparsity.keras.prune_low_magnitude(layer)
    return layer
model_for_pruning = tf.keras.models.clone_model(model, clone_function=apply_pruning_to_dense)

首先,我们定义了一个 apply_pruning_to_dense 包装函数,将 prune_low_magnitude 函数应用于目标层。然后,我们只需将原始模型和 apply_pruning_to_dense 函数传递给 tf.keras.models.clone_model 函数,该函数通过在给定模型上运行提供的函数来生成新模型。

值得一提的是,存在 tfmot.sparsity.keras.PrunableLayer 抽象类,专为自定义网络修剪而设计。关于此类的更多详细信息,请参见 www.tensorflow.org/model_optimization/api_docs/python/tfmot/sparsity/keras/PrunableLayerwww.tensorflow.org/model_optimization/guide/pruning/comprehensive_guide#custom_training_loop

接下来,我们将看一下如何在 PyTorch 中进行修剪。

PyTorch 中的网络修剪

PyTorch 通过 torch.nn.utils.prune 模块支持训练后的网络修剪。给定一个训练好的网络,可以通过将模型传递给 global_unstructured 函数来实现修剪。一旦模型被修剪,就会附加一个二进制掩码,该掩码表示被修剪的参数集合。在 forward 操作之前,掩码被应用于目标参数,从而消除不必要的计算。让我们看一个例子:

# model is instantiated and trained
model = …
parameters_to_prune = (
    (model.conv, 'weight'),
    (model.fc, 'weight')
)
prune.global_unstructured(
    parameters_to_prune,
    pruning_method=prune.L1Unstructured, # L1-norm
    amount=0.2
)

如前面的代码片段所示,global_unstructured 函数的第一个参数定义了将应用修剪的网络组件 (parameters_to_prune)。第二个参数定义了修剪算法 (pruning_method)。最后一个参数 amount 表示要修剪的参数百分比。在本例中,我们基于 L1 范数修剪了连接的最低 20%。如果你对其他算法感兴趣,你可以在 pytorch.org/docs/stable/nn.html#utilities 找到完整列表。

PyTorch 还支持每层修剪以及迭代修剪。你还可以定义一个自定义修剪算法。关于上述功能的详细信息可以在 https://pytorch.org/tutorials/intermediate/pruning_tutorial.html#pruning-tutorial 找到。

需要记住的事情

a. 网络修剪是通过消除网络中不必要的连接来减小模型大小的优化过程。

b. TensorFlow 和 PyTorch 都支持模型级和层级的网络修剪。

在本节中,我们描述了如何消除网络中的不必要连接以提高推理延迟。在下一节中,我们将学习一种名为知识蒸馏的技术,该技术生成一个新模型而不是修改现有模型。

知识蒸馏 – 通过模仿预测获得较小的网络

知识蒸馏的概念最早由希尔顿等人在其题为Distilling the Knowledge in a Neural Network的出版物中于 2015 年首次引入。在分类问题中,Softmax 激活通常作为网络的最后操作,以将每个类别的置信度表示为概率。由于最高概率的类别用于最终预测,因此其他类别的概率被认为不重要。然而,作者认为它们仍包含有意义的信息,代表了模型对输入的解释。例如,如果两个类别在多个样本中报告类似的概率,则这两个类别可能具有许多共同的特征,使得区分两者变得困难。当网络较深时,这些信息变得更加丰富,因为它可以从其已见过的数据中提取更多信息。基于这一思想,作者提出了一种将已训练模型的知识转移到较小尺寸模型的技术:知识蒸馏。

知识蒸馏的过程通常被称为老师与学生分享知识;原始模型称为老师模型,而较小的模型称为学生。如下图所示,学生模型从单个输入构建的两个不同标签中进行训练。一个标签是地面实况标签,称为硬标签。另一个标签称为软标签。软标签是老师模型的输出概率。知识蒸馏的主要贡献来自软标签填补硬标签中缺失信息的能力:

图 10.3 – 知识蒸馏过程概述

图 10.3 – 知识蒸馏过程概述

从许多评估知识蒸馏好处的实验中可以证明,使用较小的网络可以达到可比较的性能。令人惊讶的是,在某些情况下,更简单的网络结构导致正则化,并使学生模型表现优于教师模型。

自该技术首次出现以来,已经引入了许多变体。第一组变体来自于如何定义知识:基于响应的知识(网络输出),基于特征的知识(中间表示),以及基于关系的知识(层次或数据样本之间的关系)。另一组变体集中在如何实现知识传输上:离线蒸馏(从预训练的教师模型训练学生模型),在线蒸馏(在两个模型训练时共享知识),以及自蒸馏(在单个网络内部共享知识)。如果你希望进一步探索这个领域,我们认为由 Gou 等人撰写的名为知识蒸馏:一项调查的论文可能是一个很好的起点。

由于训练设置的复杂性,目前没有一个直接支持知识蒸馏的框架。然而,如果模型网络复杂而输出结构简单,这仍然是一个很好的选择。

要记住的事情

a. 知识蒸馏是一种将训练模型的知识转移到较小模型的技术。

b. 在知识蒸馏中,原始模型称为教师模型,而较小模型称为学生模型。学生模型从两个标签进行训练:地面真实标签和教师模型的输出。

最后,我们介绍了一种修改网络架构以减少模型参数数量的技术:网络架构搜索。

网络架构搜索 – 寻找最有效的网络架构

神经架构搜索(NAS)是为给定问题找到最佳层次结构的过程。由于可能的网络架构搜索空间极其庞大,评估每种可能的网络架构是不可行的。因此,需要一种聪明的方法来识别有前途的网络架构并评估候选者。因此,NAS 方法从三个不同的方面发展:

  • 搜索空间: 如何构建一个合理大小的搜索空间

  • 搜索策略: 如何高效探索搜索空间

  • 性能估算策略: 如何在不完全训练模型的情况下有效估算性能

尽管 NAS 是一个快速发展的研究领域,但针对 TF 和 PyTorch 模型仅有少量工具可用:

简化版 NAS 实现包括从随机层次组织定义搜索空间。然后,我们简单选择表现最佳的模型。为了减少总体搜索时间,我们可以根据特定评估指标应用早停,这将在评估指标不再改变时快速停止训练。这样的设置将 NAS 重新构造为一个超参数调整问题,其中模型架构已成为一个参数。我们可以通过应用以下技术之一进一步改进搜索算法:

  • 贝叶斯优化

  • 强化学习 (RL)

  • 基于梯度的方法

  • 基于层次的方法

如果您想进一步探索这个领域,我们建议自己实施 NAS。首先,您可以利用在第七章中介绍的超参数调整技术。您可以从随机参数搜索或结合早停的贝叶斯优化方法开始。然后,我们建议研究基于 RL 的实现。我们还建议阅读一篇名为神经架构搜索综述:挑战与解决方案的论文,作者是任鹏振等人。

要记住的事情

a. NAS 是找到解决方案的最佳网络架构的过程。

b. NAS 包括三个组成部分:搜索空间、搜索策略和性能估计策略。它涉及评估不同架构的网络并找到最佳架构。

c. NAS 的几个工具包括:Optuna、Syne-Tune、Katib、NNI 和 SigOpt。

在本节中,我们介绍了 NAS 及其如何生成更小网络的方式。

概述

在本章中,我们介绍了一系列技术,可以通过减少模型大小来改善推理延迟。我们介绍了三种最流行的技术,以及在 TF 和 PyTorch 中的完整示例:网络量化、权重共享和网络修剪。我们还描述了通过直接修改网络架构来减少模型大小的技术:知识蒸馏和 NAS。

在下一章中,我们将解释如何在移动设备上部署 TF 和 PyTorch 模型,在这一节描述的技术将会很有用。

第十一章:移动设备上的深度学习

本章将介绍如何在移动设备上部署深度学习DL)模型,这些模型是使用TensorFlowTF)和PyTorch开发的,并使用TensorFlow LiteTF Lite)和PyTorch Mobile分别进行部署。首先,我们将讨论如何将 TF 模型转换为 TF Lite 模型。然后,我们将解释如何将 PyTorch 模型转换为 TorchScript 模型,以便 PyTorch Mobile 可以使用。最后,本章的最后两节将涵盖如何将转换后的模型集成到 Android 和 iOS 应用程序(应用)中。

本章中,我们将涵盖以下主要主题:

  • 为移动设备准备 DL 模型

  • 使用 DL 模型创建 iOS 应用程序

  • 使用 DL 模型创建 Android 应用程序

为移动设备准备 DL 模型

移动设备通过便捷地访问互联网改变了我们日常生活的进行方式;我们许多日常任务都严重依赖移动设备。因此,如果我们能在移动应用中部署 DL 模型,我们应该能够实现更高水平的便利。流行的用例包括不同语言之间的翻译、目标检测和数字识别等。

以下截图展示了一些示例用例:

图 11.1 - 从左到右,列出的应用程序处理植物识别,目标检测和机器翻译,利用 DL 的灵活性

图 11.1 - 从左到右,列出的应用程序处理植物识别、目标检测和机器翻译,利用 DL 的灵活性

移动设备存在许多操作系统OSs)。然而,目前两种 OSs 在移动市场占据主导地位:iOS 和 Android。iOS 是苹果设备(如 iPhone 和 iPad)的操作系统。同样,Android 是由三星和谷歌等公司生产的设备的标准操作系统。在本章中,我们专注于针对这两种主导 OSs 的部署。

不幸的是,TF 和 PyTorch 模型不能直接在移动设备上部署。我们需要将它们转换为可以在移动设备上运行推断逻辑的格式。对于 TF,我们需要一个 TF Lite 模型;我们将首先讨论如何使用tensorflow库将 TF 模型转换为 TF Lite 模型。另一方面,PyTorch 涉及 PyTorch Mobile 框架,该框架只能消耗 TorchScript 模型。在讨论了 TF Lite 转换后,我们将学习如何将 PyTorch 模型转换为 TorchScript 模型。此外,我们还将解释如何优化 PyTorch 模型的特定层,以适应目标移动环境。

值得注意的是,TF 模型或 PyTorch 模型可以转换为开放神经网络交换ONNX)运行时,并部署到移动设备上(onnxruntime.ai/docs/tutorials/mobile)。此外,SageMaker 提供了内置支持,可将 DL 模型加载到边缘设备上:SageMaker Edge Manager(docs.aws.amazon.com/sagemaker/latest/dg/edge-getting-started-step4.html)。

生成 TF Lite 模型

TF Lite 是一个用于在移动设备、微控制器和其他边缘设备上部署模型的库(www.tensorflow.org/lite)。训练好的 TF 模型需要转换为 TF Lite 模型,才能在边缘设备上运行。如下面的代码片段所示,tensorflow 库内置支持将 TF 模型转换为 TF Lite 模型(.tflite 文件):

import tensorflow as tf
# path to the trained TF model
trained_model_dir = "s3://mybucket/tf_model"
# TFLiteConverter class is necessary for the conversion
converter = tf.lite.TFLiteConverter.from_saved_model(trained_model_dir)
tfl_model = converter.convert()
# save the converted model to TF Lite format 
with open('model_name.tflite', 'wb') as f:
  f.write(tfl_model)

在上述 Python 代码中,tf.lite.TFLiteConverter 类的 from_saved_model 函数加载训练好的 TF 模型文件。该类的 convert 方法将加载的 TF 模型转换为 TF Lite 模型。

第十章讨论的那样,提升推理效率,TF Lite 支持各种模型压缩技术。从 TF Lite 中流行的技术包括网络剪枝和网络量化。

接下来,让我们看一下如何将 PyTorch 模型转换为 TorchScript 模型以用于 PyTorch Mobile。

生成 TorchScript 模型

可以使用 PyTorch Mobile 框架在移动设备上运行 PyTorch 模型(pytorch.org/mobile/home/)。类似于 TF 的情况,必须将训练好的 PyTorch 模型转换为 TorchScript 模型,以便使用 PyTorch Mobile 运行模型(pytorch.org/docs/master/jit.html)。TorchScript 模块的主要优势在于能够在 Python 以外的环境(如 C++ 环境)中运行 PyTorch 模块。torch.jit.script 方法将给定 DL 模型的图导出为低级表示,可以在 C++ 环境中执行。有关跨语言支持的完整细节,请参阅pytorch.org/docs/stable/jit_language_reference.html#language-reference。请注意,TorchScript 目前仍处于 beta 状态。

要从 PyTorch 模型获取 TorchScript 模型,需要将训练好的模型传递给 torch.jit.script 函数,如下面的代码片段所示。可以通过 torch.utils.mobile_optimizer 模块的 optimize_for_mobile 方法来进一步优化 TorchScript 模型,以适应移动环境,例如融合 Conv2DBatchNorm 层或者移除不必要的 Dropout 层(详情请参考 pytorch.org/docs/stable/mobile_optimizer.html)。请注意,mobile_optimizer 方法目前仍处于 beta 状态。

import torch
from torch.utils.mobile_optimizer import optimize_for_mobile
# load a trained PyTorch model
saved_model_file = "model.pt"
model = torch.load(saved_model_file)
# the model should be in evaluate mode for dropout and batch normalization layers
model.eval()
# convert the model into a TorchScript model and apply optimization for mobile environment
torchscript_model = torch.jit.script(model)
torchscript_model_optimized = optimize_for_mobile(torchscript_model)
# save the optimized TorchScript model into a .pt file 
torch.jit.save(torchscript_model_optimized, "mobile_optimized.pt")

在上述示例中,我们首先将训练好的模型加载到内存中(torch.load("model.pt"))。模型在进行转换时应处于 eval 模式。接下来,我们使用 torch.jit.script 函数将 PyTorch 模型转换为 TorchScript 模型(torchscript_model)。使用 optimize_for_mobile 方法进一步优化 TorchScript 模型,生成优化后的 TorchScript 模型(torch_script_model_optimized)。最后,可以使用 torch.jit.save 方法将优化后的 TorchScript 模型保存为独立的 .pt 文件(mobile_optimized.pt)。

注意事项

a. 在移动设备上运行 TF 模型涉及 TF Lite 框架。训练好的模型需要转换成 TF Lite 模型。使用 tensorflow.lite 库中的 TFLiteConverter 类来进行转换。

b. 在移动设备上运行 PyTorch 模型涉及 PyTorch Mobile 框架。鉴于 PyTorch Mobile 仅支持 TorchScript 模型,需要使用 torch.jit 库将训练好的模型转换为 TorchScript 模型。

接下来,我们将学习如何将 TF Lite 和 TorchScript 模型集成到 iOS 应用中。

使用 DL 模型创建 iOS 应用

在本节中,我们将讨论如何为 iOS 应用编写 TF Lite 和 TorchScript 模型的推断代码。虽然 Swift 和 Objective-C 是 iOS 的本地语言,可以在一个项目中同时使用,但我们主要关注 Swift 的用例,因为它比 Objective-C 更受欢迎。

如果我们详细解释 iOS 应用开发的每一个步骤,本章将会很冗长。因此,我们将基础内容放在了苹果提供的官方教程中:developer.apple.com/tutorials/app-dev-training

在 iOS 上运行 TF Lite 模型推断

在本节中,我们展示了如何在 iOS 应用程序中加载 TF Lite 模型,使用TensorFlowLiteSwift,这是 TF Lite 的 iOS 本地库(github.com/tensorflow/tensorflow/tree/master/tensorflow/lite/swift)。可以通过 CocoaPods 安装TensorFlowLiteSwift,这是 iOS 应用程序开发的标准包管理器(cocoapods.org)。要在 macOS 上下载 CocoaPods,可以在终端上运行brew install cocoapods命令。每个 iOS 应用程序开发都涉及一个 Podfile,列出了应用程序开发所依赖的库。必须将TensorFlowLiteSwift库添加到此文件中,如以下代码片段所示:

pod 'TensorFlowLiteSwift'

要在 Podfile 中安装所有库,可以运行pod install命令。

下面的步骤描述了如何在您的 iOS 应用程序中加载 TF Lite 模型并运行推理逻辑。有关执行的完整细节,请参阅www.tensorflow.org/lite/guide/inference#load_and_run_a_model_in_swift

  1. 可以使用import关键字加载安装的库:

    import TensorFlowLite
    
  2. 通过提供输入 TF Lite 模型的路径来初始化Interpreter类:

    let interpreter = try Interpreter(modelPath: modelPath) 
    
  3. 为了将输入数据传递给模型,您需要使用self.interpreter.copy方法将输入数据复制到索引为0的输入Tensor对象中:

    let inputData: Data
    inputData = ...
    try self.interpreter.copy(inputData, toInputAt: 0)
    
  4. 一旦输入的Tensor对象准备好,就可以使用self.interpreter.invoke方法运行推理逻辑:

    try self.interpreter.invoke()
    
  5. 可以使用self.interpreter.output检索生成的输出,作为可以进一步使用UnsafeMutableBufferPointer类反序列化为数组的Tensor对象:

    let outputTensor = try self.interpreter.output(at: 0)
    let outputSize = outputTensor.shape.dimensions.reduce(1, {x, y in x * y})
    let outputData = UnsafeMutableBufferPointer<Float32>.allocate(capacity: outputSize)
    outputTensor.data.copyBytes(to: outputData)
    

在本节中,我们学习了如何在 iOS 应用程序中运行 TF Lite 模型推理。接下来,我们将介绍如何在 iOS 应用程序中运行 TorchScript 模型推理。

在 iOS 上运行 TorchScript 模型推理

在这一节中,我们将学习如何在 iOS 应用程序上使用 PyTorch Mobile 部署 TorchScript 模型。我们将从使用TorchModule模块加载训练好的 TorchScript 模型的 Swift 代码片段开始。您需要用于 PyTorch Mobile 的库称为LibTorch_Lite。该库也可通过 CocoaPods 获得。您只需将以下行添加到 Podfile 中:

pod 'LibTorch_Lite', '~>1.10.0'

如上一节所述,您可以运行pod install命令来安装库。

鉴于 TorchScript 模型是为 C++ 设计的,Swift 代码不能直接运行模型推断。为了弥合这一差距,存在 TorchModule 类,它是 torch::jit::mobile::Module 的 Objective-C 包装器。要在应用程序中使用此功能,需要在项目下创建一个名为 TorchBridge 的文件夹,其中包含 TorchModule.mm(Objective-C 实现文件)、TorchModule.h(头文件)和一个命名约定为 -Bridging-Header.h 后缀的桥接头文件(以允许 Swift 加载 Objective-C 库)。完整的示例设置可以在 github.com/pytorch/ios-demo-app/tree/master/HelloWorld/HelloWorld/HelloWorld/TorchBridge 找到。

在接下来的步骤中,我们将展示如何加载 TorchScript 模型并触发模型预测:

  1. 首先,您需要将 TorchModule 类导入到项目中:

    #include "TorchModule.h"
    
  2. 接下来,通过提供 TorchScript 模型文件的路径来实例化 TorchModule

    let modelPath = "model_dir/torchscript_model.pt"
    let module = TorchModule(modelPath: modelPath)
    
  3. TorchModule 类的 predict 方法处理模型推断。需要向 predict 方法提供输入,然后将返回输出。在幕后,predict 方法将通过 Objective-C 包装器调用模型的 forward 函数。以下代码中有所示:

    let inputData: Data
    inputData = ...
    let outputs = module.predict(input: UnsafeMutableRawPointer(&inputData))
    

如果您对推断的幕后实际工作原理感兴趣,建议阅读 pytorch.org/mobile/ios/ 中的 Run inference 部分。

需要记住的事情

a. Swift 和 Objective-C 是开发 iOS 应用程序的标准语言。一个项目可以包含用这两种语言编写的文件。

b. TensorFlowSwift 库是 Swift 的 TF 库。Interpreter 类支持 iOS 上 TF Lite 模型的推断。

c. LibTorch_Lite 库通过 TorchModule 类支持在 iOS 应用程序上进行 TorchScript 模型推断。

接下来,我们将介绍如何在 Android 上运行 TF Lite 和 TorchScript 模型的推断。

使用 DL 模型创建 Android 应用程序

在本节中,我们将讨论 Android 如何支持 TF Lite 和 PyTorch Mobile。Java 和 Java 虚拟机JVM)为 Android 应用程序提供的首选语言(例如 Kotlin)。在本节中,我们将使用 Java。有关 Android 应用程序开发的基础知识,请访问 developer.android.com

我们首先专注于使用 org.tensorflow:tensorflow-lite-support 库在 Android 上运行 TF Lite 模型推断。然后,我们讨论如何使用 org.pytorch:pytorch_android_lite 库运行 TorchScript 模型推断。

在 Android 上运行 TF Lite 模型推断

首先,让我们看看如何使用 Java 在 Android 上运行 TF Lite 模型。使用 org.tensorflow:tensorflow-lite-support 库可以在 Android 应用上部署 TF Lite 模型。该库支持 Java、C++(测试版)和 Swift(测试版)。支持的环境完整列表可在 github.com/tensorflow/tflite-support 找到。

Android 应用开发涉及 Gradle,这是一个管理依赖项的构建自动化工具 (gradle.org)。每个项目都会有一个 .gradle 文件,该文件指定了使用基于 JVM 的语言(如 Groovy 或 Kotlin)的项目规范。在以下代码片段中,我们列出了项目在 dependencies 部分下依赖的库:

dependencies {
     implementation 'org.tensorflow:tensorflow-lite-support:0.3.1'
}

在前面的 Groovy Gradle 代码中,我们已经指定了 org.tensorflow:tensorflow-lite-support 库作为我们的依赖项之一。可以在 docs.gradle.org/current/samples/sample_building_java_applications.html 找到一个示例 Gradle 文件。

在接下来的步骤中,我们将学习如何加载 TF Lite 模型并运行推理逻辑。有关此过程的完整详细信息可以在 www.tensorflow.org/lite/api_docs/java/org/tensorflow/lite/Interpreter 找到:

  1. 首先是导入包含用于 TF Lite 模型推理的 Interpreter 类的 org.tensorflow.lite 库:

    import org.tensorflow.lite.Interpreter;
    
  2. 然后,我们可以通过提供模型路径来实例化 Interpreter 类:

    let tensorflowlite_model_path = "tflitemodel.tflite";
    Interpreter = new Interpreter(tensorflowlite_model_path);
    
  3. Interpreter 类的 run 方法用于运行推理逻辑。它只接受一个 input 类型为 HashMap 的实例,并提供一个类型为 HashMapoutput 实例:

    Map<> input = new HashMap<>();
    Input = ...
    Map<> output = new HashMap<>();
    interpreter.run(input, output);
    

在下一节中,我们将学习如何将 TorchScript 模型加载到 Android 应用中。

在 Android 上运行 TorchScript 模型推理

在本节中,我们将解释如何在 Android 应用中运行 TorchScript 模型。要在 Android 应用中运行 TorchScript 模型推理,您需要使用 org.pytorch:pytorch_android_lite 库提供的 Java 包装器。同样,您可以在 .gradle 文件中指定必需的库,如下面的代码片段所示:

dependencies {
    implementation 'org.pytorch:pytorch_android_lite:1.11'
}

在 Android 应用中运行 TorchScript 模型推理可以通过以下步骤来实现。关键是使用来自 org.pytorch 库的 Module 类,该类在后台调用 C++ 函数进行推理(pytorch.org/javadoc/1.9.0/org/pytorch/Module.html):

  1. 首先,您需要导入 Module 类:

    import org.pytorch.Module;
    
  2. Module 类提供了一个 load 函数,通过加载提供的模型文件创建一个 Module 实例:

    let torchscript_model_path = "model_dir/torchscript_model.pt";
    Module = Module.load(torchscript_model_path);
    
  3. Module 实例的 forward 方法用于运行推理逻辑并生成 org.pytorch.Tensor 类型的输出:

    Tensor outputTensor = module.forward(IValue.from(inputTensor)).toTensor();
    

虽然前面的步骤涵盖了org.pytorch模块的基本用法,您可以在官方文档中找到其他细节:pytorch.org/mobile/android

需要记住的事项

a. Java 和基于 JVM 的语言(例如 Kotlin)是 Android 应用程序的本地语言。

b. org.tensorflow:tensorflow-lite-support库用于在 Android 上部署 TF Lite 模型。Interpreter类实例的run方法处理模型推断。

c. org.pytorch:pytorch_android_lite库专为在 Android 应用程序中运行 TorchScript 模型而设计。Module类的forward方法处理推断逻辑。

完成了在 Android 上部署 DL 模型。现在,您应该能够将任何 TF 和 PyTorch 模型集成到 Android 应用程序中。

总结

在本章中,我们介绍了如何将 TF 和 PyTorch 模型集成到 iOS 和 Android 应用程序中。我们从描述从 TF 模型到 TF Lite 模型的必要转换以及从 PyTorch 模型到 TorchScript 模型开始本章。接下来,我们提供了加载 TF Lite 和 TorchScript 模型并在 iOS 和 Android 上使用加载模型进行推断的完整示例。

在下一章中,我们将学习如何关注部署模型。我们将查看一组用于模型监控的工具,并描述如何有效监控部署在亚马逊弹性 Kubernetes 服务Amazon EKS)和 Amazon SageMaker 上的模型。

第十二章:生产中深度学习端点监控

由于开发和生产设置的差异,一旦部署后深度学习DL)模型的性能保证就变得困难。如果模型行为存在任何差异,必须在合理的时间内捕捉到;否则,会对下游应用产生负面影响。

在本章中,我们的目标是解释生产中监控 DL 模型行为的现有解决方案。我们将首先清楚地描述监控的好处以及保持整个系统稳定运行所需的条件。然后,我们将讨论监控 DL 模型和警报的流行工具。在介绍的各种工具中,我们将深入了解CloudWatch。我们将从 CloudWatch 的基础知识开始,讨论如何将 CloudWatch 集成到运行在SageMakerElastic Kubernetes ServiceEKS)集群上的端点中。

在本章中,我们将覆盖以下主要话题:

  • 生产中 DL 端点监控简介

  • 使用 CloudWatch 进行监控

  • 使用 CloudWatch 监控 SageMaker 端点

  • 使用 CloudWatch 监控 EKS 端点

技术要求

您可以从本书的 GitHub 仓库下载本章的补充材料:github.com/PacktPublishing/Production-Ready-Applied-Deep-Learning/tree/main/Chapter_12

生产中 DL 端点监控简介

我们将从描述部署端点的 DL 模型监控的好处开始本章。理想情况下,我们应该分析与传入数据、传出数据、模型指标和流量相关的信息。监控列出数据的系统可以为我们提供以下好处。

首先,模型的输入和输出信息可以持久化存储在数据存储解决方案中(例如,Simple Storage Service(S3)存储桶),以便理解数据分布。对传入数据和预测的详细分析有助于识别下游流程的潜在改进。例如,监控传入数据可以帮助我们识别模型预测中的偏差。在处理传入请求时,模型可能对特定特征组具有偏见。这些信息可以指导我们在为以下部署训练新模型时应考虑什么。另一个好处来自于模型的可解释性。出于业务目的或法律目的,需要解释模型预测的推理。这涉及到我们在第九章中描述的技术,扩展深度学习管道

我们应该跟踪的另一个关键指标是端点的 吞吐量,这有助于我们提高用户满意度。模型的行为可能会随着传入请求的数量和底层计算机的计算能力而变化。我们可以监控推断延迟与传入流量之间的关系,以构建稳定高效的推断端点供用户使用。

在高层次上,DL 模型的监控可以分为两个领域:端点监控模型监控。在前者领域,我们旨在收集与端点延迟和目标端点的吞吐量相关的数据。后者则专注于改善模型性能;我们需要收集传入数据、预测结果和模型性能,以及推断延迟。虽然许多模型监控用例通过在线方式在运行中的端点实现,但在训练和验证过程中也会以离线方式应用,目的是在部署前了解模型的行为。

在接下来的部分,我们将介绍用于监控 DL 模型的流行工具。

探索监控工具

监控工具主要可以分为两类,具体取决于它们的设计目标:监控工具警报工具。详尽介绍所有工具超出了本书的范围;但我们将简要介绍其中的一些,以解释监控和警报工具的优势。请注意,界限通常不清晰,并且一些工具可能同时支持这两个功能。

让我们先来了解一下监控工具。

Prometheus

Prometheus 是一个开源的监控和警报工具 (prometheus.io)。Prometheus 将应用程序传递的数据存储在本地存储中。它使用时间序列数据库来存储、聚合和检索指标数据,这与监控任务的性质非常匹配。与 Prometheus 的交互涉及使用 Prometheus 查询语言 (PromQL) (prometheus.io/docs/prometheus/latest/querying/basics)。Prometheus 设计用于处理诸如 中央处理单元 (CPU) 使用情况、内存使用情况和延迟等指标。此外,还可以摄取用于监控的自定义指标,例如模型性能或传入和传出数据的分布。

CloudWatch

CloudWatch 是由亚马逊网络服务AWS)设计的监控和可观察性服务 (aws.amazon.com/cloudwatch)。与设置专用的 Prometheus 服务相比,CloudWatch 设置简单,因为它在幕后处理数据存储管理。默认情况下,大多数 AWS 服务如 AWS Lambda 和 EKS 集群使用 CloudWatch 持久化指标以供进一步分析。此外,CloudWatch 可以通过电子邮件或 Slack 消息通知用户关于受监视指标的异常变化。例如,您可以为指标设置阈值,并在其超过或低于预定义阈值时收到通知。有关警报功能的详细信息,请参阅 docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html

Grafana

Grafana 是一个流行的工具,用于可视化从监控工具收集的指标 (grafana.com)。Grafana 可以读取来自 CloudWatch 或 AWS 管理的 Prometheus 的指标数据以进行可视化。关于这些配置的完整描述,建议您阅读 grafana.com/docs/grafana/latest/datasources/aws-cloudwatchdocs.aws.amazon.com/prometheus/latest/userguide/AMP-onboard-query-standalone-grafana.html

Datadog

一种流行的专有解决方案是Datadog (www.datadoghq.com)。这个工具提供了多种监控功能:日志监控,应用性能监控,网络流量监控和实时用户监控。

SageMaker Clarify

SageMaker 内置支持监控由 SageMaker 创建的端点,SageMaker Clarify (aws.amazon.com/sagemaker/clarify)。SageMaker Clarify 带有一个软件开发工具包SDK),有助于理解模型的性能及其在预测中的偏差。有关 SageMaker Clarify 的详细信息,请参阅 docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html

在下一节中,我们将介绍警报工具。

探索警报工具

一个事件是需要后续操作的事件,比如失败的作业或构建。虽然监控工具可以捕获异常变化,但它们通常缺乏事件管理和响应过程的自动化。警报工具通过提供这些功能填补了这一空白。因此,许多公司通常集成明确的警报工具,以及时响应事件。

在本节中,我们将介绍两种最流行的警报工具:PagerDuty 和 Dynatrace。

PagerDuty

作为警报和管理事故响应IR)过程的工具,许多公司集成了PagerDutyPagerDuty)。在基本的警报功能之上,PagerDuty 支持根据事故类型和严重程度分配优先级。PagerDuty 可以从多个流行的监控软件中读取数据,比如Prometheus 和 Datadog(https://aws.amazon.com/blogs/mt/using-amazon-managed-service-for-prometheus-alert-manager-to-receive-alerts-with-pagerduty)。它还可以通过最小的代码更改与 CloudWatch 集成(support.pagerduty.com/docs/aws-cloudwatch-integration-guide)。

Dynatrace

Dynatrace是另一种专有工具,用于监控整个集群或网络和警报事件(Dynatrace)。可以轻松监控正在运行的进程的资源使用情况、流量和响应时间。Dynatrace 具有基于警报配置文件的独特警报系统。这些配置文件定义了系统如何在整个组织中传递通知。Dynatrace 具有内置的推送通知功能,但也可以与其他提供通知功能的系统集成,例如 Slack 和 PagerDuty。

要记住的事情

a. 监控与端点相关的入站数据、出站数据、模型指标和流量量,使我们能够理解端点的行为,并帮助我们识别潜在的改进。

b. Prometheus 是一个开源的监控和警报系统,可用于监控 DL 端点的指标。CloudWatch 是 AWS 的监控服务,专为记录一组数据和跟踪入站和出站流量的异常变化而设计。

c. PagerDuty 是一种流行的警报工具,负责处理事故的完整生命周期。

在本节中,我们讨论了为什么需要对 DL 端点进行监控,并提供了可用工具的列表。在本章的其余部分,我们将详细研究 CloudWatch,这是最常见的监控工具,因为它与 AWS 中的大多数服务都很好地集成(例如 SageMaker)。

使用 CloudWatch 进行监控

首先,我们将介绍 CloudWatch 中的几个关键概念:日志、指标、报警和仪表板。CloudWatch 将摄入的数据以日志或按时间戳组织的指标形式持久化。如其名称所示,日志 指的是程序生命周期中发出的文本数据。另一方面,指标 表示组织的数值数据,如 CPU 或内存利用率。由于指标以有组织的方式存储,CloudWatch 支持从收集的数据中聚合指标并创建直方图。报警 可以设置为在目标指标报告异常变化时发出警报。此外,可以设置仪表板来直观地查看所选指标和已触发的报警。

在以下示例中,我们将描述如何使用 boto3 库中的 CloudWatch 服务客户端记录指标数据。指标数据结构化为字典,包含指标名称、维度和值。维度的概念是捕获有关指标的事实信息。例如,指标名称为 city 可以具有纽约市的值。然后,维度可以捕获特定信息,例如火灾或入室盗窃的每小时计数:

import boto3
# create CloudWatch client using boto3 library
cloudwatch = boto3.client('cloudwatch')
# metrics data to ingest
data_metrics=[
    {
       'MetricName': 'gross_merchandise_value',
       'Dimensions': [
          {
             'Name': 'num_goods_sold',
             'Value': '369'
          } ],
       'Unit': 'None',
       'Value': 900000.0
    } ]
# ingest the data for monitoring 
cloudwatch.put_metric_data(
    MetricData=data_metrics, # data for metrics 
    Namespace='ECOMMERCE/Revenue' # namespace to separate domain/projects)

在前面的代码片段中,我们首先使用 boto3.client 函数为 CloudWatch 创建一个 cloudwatch 服务客户端实例。这个实例将允许我们从 Python 环境与 CloudWatch 进行通信。记录数据的关键方法是 put_metric_data。这个函数的 put_metric_data 方法从 CloudWatch 客户端实例接收 MetricData(要摄入到 CloudWatch 中的目标指标数据:data_metrics)和 Namespace(容器,用于容纳指标数据:ECOMMERCE/Revenue)。不同命名空间的数据被单独管理,以支持高效的聚合。

在这个示例中,data_metrics 指标数据包含一个名为 MetricName 的字段,其值为 gross_merchandise_value,值为 900000.0gross_merchandise_value 的单位被定义为 None。此外,我们还提供了销售商品数 (num_goods_sold) 作为额外的维度信息。

要获取完整的 CloudWatch 概念说明,请参阅 docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html

需要记住的事情

a. CloudWatch 将摄入的数据以日志或按时间戳组织的指标形式持久化。它支持为异常变化设置报警,并通过仪表板提供有效的可视化。

b. 使用 boto3 库可以轻松地将一个指标记录到 CloudWatch。它提供了一个支持通过 put_metric_data 函数进行日志记录的 CloudWatch 服务客户端。

尽管云观察的日志可以像本节描述的那样明确地记录,但 SageMaker 为一些开箱即用的指标提供了内置的日志记录功能。让我们仔细看看它们。

使用 CloudWatch 监视 SageMaker 端点

作为机器学习的端到端服务,SageMaker 是我们实施 DL 项目各个步骤的主要工具之一。在本节中,我们将描述最后一个缺失的部分:监控使用 SageMaker 创建的端点。首先,我们将解释如何设置基于 CloudWatch 的训练监控,其中指标以离线批次报告。接下来,我们将讨论如何监控实时端点。

本节中的代码片段设计用于在 SageMaker Studio 上运行。因此,我们首先需要定义 AWS Identity and Access Management (IAM) 角色和一个会话对象。让我们看看第一个代码片段:

import sagemaker
# IAM role of the notebook
role_exec=sagemaker.get_execution_role()
# a sagemaker session object
sag_sess=sagemaker.session()

在前面的代码片段中,get_execution_role 函数提供笔记本的 IAM 角色。role_execsagemaker.session 提供了 SageMaker sag_sess SageMaker 会话对象,用于作业配置。

在 SageMaker 中监控模型的整个训练过程

在模型训练期间的日志记录涉及 SageMaker 的 Estimator 类。它可以使用 regex 表达式处理打印的消息,并将它们存储为指标。您可以在此处看到一个例子:

import sagemaker
from sagemaker.estimator import Estimator
# regex pattern for capturing error metrics 
reg_pattern_metrics=[
   {'Name':'train:error','Regex':'Train_error=(.*?);'},
   {'Name':'validation:error','Regex':'Valid_error=(.*?)'}]
# Estimator instance for model training
estimator = Estimator(
   image_uri=...,
   role=role_exec,
   sagemaker_session=sag_sess,
   instance_count=...,
   instance_type=...,
   metric_definitions=reg_pattern_metrics)

在前面的代码片段中,我们创建了 estimator,这是用于训练的 Estimator 实例。大多数参数的解释可以在 第六章高效模型训练 中找到。在此示例中,我们正在定义的附加参数是 metric_definitions。我们传递的是 reg_pattern_metrics,它定义了一组 Train_error=(.*?)Valid_error=(.*?),训练和评估日志。匹配给定模式的文本将作为指标持久保存在 CloudWatch 中。有关使用 Estimator 类在整个模型训练过程中进行离线指标记录的完整详情,请参阅 docs.aws.amazon.com/sagemaker/latest/dg/training-metrics.html。我们想要提到的是,特定的训练作业指标(如内存、CPU、图形处理单元 (GPU) 和磁盘利用率)会自动记录,并且您可以通过 CloudWatch 或 SageMaker 控制台监视它们。

监控来自 SageMaker 的实时推断端点

在本节中,我们将描述 SageMaker 的端点基于 CloudWatch 的监控功能。在下面的代码片段中,我们呈现了一个带有 output_handler 函数的样本 inference.py 脚本。该文件被分配为 SageMaker 的 ModelEstimator 类的 entry_point 参数,以定义额外的预处理和后处理逻辑。有关 inference.py 的详细信息,请参阅 第九章扩展深度学习管道output_handler 函数设计用于处理模型预测并使用 print 函数记录度量数据。打印的消息作为日志存储在 CloudWatch 中:

# inference.py
def output_handler(data, context):
    # retrieve the predictions
    results=data.content
    # data that will be ingested to CloudWatch
    data_metrics=[
       {
          'MetricName': 'model_name',
          'Dimensions': [
             {
                'Name': 'classify',
                'Value': results
              } ],
          'Unit': 'None',
          'Value': "classify_applicant_risk"
      } ]
    # print will ingest information into CloudWatch
    print(data_metrics)

在前面的推断代码中,我们首先获得模型预测(results),并为度量数据构建一个字典(data_metrics)。该字典已经具有MetricName 值为 model_name 和名为 classify 的维度。模型预测将被指定为 classify 维度。SageMaker 将收集打印的度量数据并将其输入到 CloudWatch。有关在这种场景下连续模型质量漂移监控的示例方法,请参阅在线说明 sagemaker-examples.readthedocs.io/en/latest/sagemaker_model_monitor/model_quality/model_quality_churn_sdk.html。这页详细解释了如何在这些情况下利用 CloudWatch。

需记住的事项

a. SageMaker 的 Estimator 类在训练期间提供了对基于 CloudWatch 的监控的内置支持。在构造实例时,您需要将一组正则表达式模式传递给 metric_definitions 参数。

b. SageMaker 端点的打印消息将存储为 CloudWatch 日志。因此,我们可以通过记录度量数据的 entry_point 脚本来实现监控。

在本节中,我们解释了 SageMaker 如何支持基于 CloudWatch 的监控。让我们看看 EKS 如何支持推断端点的监控。

使用 CloudWatch 监控 EKS 端点

除了 SageMaker 外,我们还在《第九章》中描述了基于 EKS 的端点,在扩展深度学习流水线中。在本节中,我们描述了 EKS 可用的基于 CloudWatch 的监控。首先,我们将学习如何从容器中记录 EKS 指标以进行监控。接下来,我们将解释如何从 EKS 推断端点记录与模型相关的度量数据。

让我们首先看看如何设置 CloudWatch 来监控 EKS 集群。最简单的方法是在容器中安装 CloudWatch 代理。此外,您还可以安装 Fluent Bit,这是一个开源工具,进一步增强了日志记录过程(www.fluentbit.io)。有关 CloudWatch 代理和 Fluent Bit 的完整说明,请阅读 docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-setup-EKS-quickstart.html

另一个选项是保留由 EKS 控制平面发送的默认指标。这可以通过 EKS Web 控制台轻松启用(docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html)。可以在aws.github.io/aws-eks-best-practices/reliability/docs/controlplane找到 EKS 控制平面发出的完整指标列表。例如,如果您对记录与延迟相关的指标感兴趣,可以使用apiserver_request_duration_seconds*

在模型推断期间记录模型相关的指标,您需要在代码中实例化boto3的 CloudWatch 服务客户端,并显式记录它们。前一节中包含的代码片段,使用 CloudWatch 监视 SageMaker 端点,应该是一个很好的起点。

记住的事情

a. 从一个 EKS 集群记录端点相关的指标可以通过使用 CloudWatch 代理或保留由 EKS 控制平面发送的默认指标来实现。

b. 使用boto3库需要显式记录与模型相关的指标。

作为本节的最后一个主题,我们解释了如何从 EKS 集群将各种指标记录到 CloudWatch。

摘要

本章的目标是解释为什么需要监视运行 DL 模型的端点,并介绍该领域中的流行工具。我们在本章介绍的工具旨在监控端点的信息集并在监控指标发生突变时提供警报。我们涵盖的工具包括 CloudWatch、Prometheus、Grafana、Datadog、SageMaker Clarify、PagerDuty 和 Dynatrace。为了完整起见,我们还介绍了如何将 CloudWatch 集成到 SageMaker 和 EKS 中,以监视端点及模型性能。

在下一章中,作为本书的最后一章,我们将探讨评估已完成项目的过程并讨论潜在的改进。

第十三章:审查已完成的深度学习项目

深度学习 (DL) 项目的最后阶段是审查过程。在规划阶段,已经定义了每个利益相关者的责任,并设定了项目的目标。在此阶段,利益相关者必须再次集合以重新审视责任和目标,评估项目是否按计划执行。这样的过程可以概括为实施后评审 (PIR)。为了进一步指导审查过程,我们还描述了评估完成项目的不同方法,包括但不限于差距分析、预计完成时间分析和可持续性分析。除了项目评估外,还需记录项目的详细信息,并讨论潜在的改进措施,以便下一个项目可以更有效地实现。

在本章中,我们将讨论以下主要主题:

  • 审查 DL 项目

  • 收集未来项目可重复使用的知识、概念和成果

审查 DL 项目

实施后评审 (PIR) 是重新审视项目执行过程的过程。在此过程中,您将比较项目的最终状态与目标状态,并组织当前项目生成的成果以便重复使用。总体而言,这一过程应该使您对项目的成功或失败有一个广泛的理解。此外,它将为您提供明确的指示,说明未来的项目应该如何管理以及如何避免当前项目中的错误。遵循这样的思路,您应该始终考虑当前项目范围之外的更大图景;一个项目可能已经完成,但您获得的见解将可供未来的项目重复利用。

进行实施后评审

PIR 过程包括以下步骤。请记住,该过程可以在交付可交付成果的最终部署之前开始:

  1. 首先,试着回答以下关键问题:项目是否在可用预算和时间内完成?是否成功?

  2. 重新审视关键绩效指标 (KPI) 和项目初期定义的其他度量标准。列出已实现的度量标准,并思考可以进行改进的地方。您可以参考第一章深度学习驱动项目的有效规划,了解各种评估指标的详细信息。

  3. 进行 GAP 分析(www.batimes.com/articles/do-we-need-a-mature-gap-analysis)。这是获得项目绩效详细视角的良好起点。GAP 方法比较部署的 DL 系统的实际绩效与项目规划阶段定义的目标绩效,涵盖所有目标。比较应该引导可能的改进和额外的优化。

  4. 记录利益相关者的意见及其对可能改进的看法。尝试了解利益相关者对项目完成的满意度水平。

  5. 准备详细的成本分析,总结花费的资金与分配的预算。每一项分析必须与每一步骤相关联:开发、部署、监控和维护。尝试找出初步估计错误的地方,并考虑如何在下一个项目中更好地规划细节。

  6. 回顾每个任务所采取的步骤,并理解瓶颈所在。试图识别过程不完美的地方,并讨论如何在未来项目中避免。

  7. 撰写一份简短的文档,总结我们刚才描述的所有要点,并由利益相关者评估。重点关注项目是否成功实现了最初的目标,但也记住 PIR 的目标不仅仅是展示项目的成功。重要的是参与者分享他们在整个项目中学到的东西。

  8. 确保 PIR 文档对组织内的任何人都是可访问的,作为未来项目的参考。

PIR 过程中的关键项目是评估项目的真实价值。我们将在下一节介绍有效项目评估的各种技术。

理解项目的真实价值。

让我们在这个最后阶段看看你应该记住的几个方面。首先,你需要重新审视规划阶段定义的截止日期和估计资源使用情况。这两个因素应该直接影响整个项目的支出。即使你的项目超出了分配的预算或时间表,如果回报大于投入的资源,项目仍可以被认为是成功的。对于一个项目,投资回报率(ROI)可以使用简单的公式计算:

ROI = [(财务价值 - 项目成本) / 项目成本] x 100

比较预期投资回报率(在项目之前计算或基于初步估计)和实际投资回报率应该为项目评估提供额外的视角。

我们将不会涵盖所有可用于指示项目最终状态的绩效指标,因为它们可能有很多:投资回报率(ROI)、收入增长、每位客户收入、利润率、质量成本、进度绩效、客户满意度、客户保留率、生产力、与业务目标对齐水平等等(financesonline.com/10-project-management-success-metrics-to-measure-your-team-performance)。然而,我们想提一下完工预估EAC),这是可以用于项目每个阶段的指标。它用于预测项目的总成本。比较 EAC 和最初预估的完工预算,您将能够审查是否符合最初的成本估算。除了 EAC,建议在整个项目过程中跟踪支出和每个活动的成本偏差。总体而言,此过程的发现将帮助您减少支出并增加利润。

一个项目管理标准,项目管理知识体系PMBOK),将计划价值PV)、挣值EV)和实际成本AC)作为衡量项目绩效的三个关键指标优先考虑(projectmanagementacademy.net)

  • PV,也称为计划工作成本BCWS),仅是在任何给定时间内计划活动的成本估算。它主要用于基准。

  • EV 被称为完成工作的预算成本BCWP),是在一段时间内完成活动预算的总和。比较 EV 和 PV 将指示您是否在资源使用上掌控得当。

  • AC 也被称为实际完成工作的成本ACWP),是完成工作的总成本。跟踪 AC 并将其与计划支出进行比较,将帮助您了解是否在预算内成功完成项目的正确路径。

在深度学习项目中,我们通常评估与模型相关的指标(如精度、召回率和 F1 分数)和与业务相关的指标(如转化率、点击率、用户生命周期价值、用户参与度和运营成本节省)。因此,制定关键目标的定义可能比非深度学习项目更复杂。

除了我们迄今为止涵盖的方面外,您还应考虑项目的可持续性。通过审视可持续性,您将了解项目是否在不影响经济、环境、社会和行政支柱的情况下实现了目标。

要记住的事情

a. 项目的最后一步是理解如何执行项目,并讨论如何在将来更有效地实现它。

b. 在整个 PIR 过程中,您需要审查项目的每个阶段,并对项目的成功或失败有广泛的了解。

c. 在评估整体项目时,KPI 分析、GAP 分析、成本分析、基准比较和 ROI 计算非常有用。

接下来,我们将看看如何有效地组织和分享 PIR 过程中收集到的专业知识。

收集可重复使用的知识、概念和文档,以便未来项目使用。

您的深度学习项目将产生许多可以在将来重复使用的文档。例如,在模型训练中使用的处理数据可以用于其他分析任务,模型实现可以适应其他应用程序,为监控任务设置的基础设施可以重新配置为不同的项目。为了能够重复使用这些文档,您需要正确归档它们,并确保存在足够的文档。让我们看看您可以实施的一些程序,以便在这个过程中使您的生活更轻松:

  1. 为开发环境、数据、实现和模型设定版本标准。这些标准应在项目早期定义,并且所有团队成员都应遵循:

    • 使用 Git(git-scm.com)为代码库添加版本控制。项目可以与 GitHub(github.com)、GitLab(gitlab.com)或 AWS CodeCommit(aws.amazon.com/codecommit)连接,以更好地管理代码库。

    • 为数据和模型设置版本控制。详情请参阅第四章实验跟踪、模型管理和数据集版本控制

    • 为每个项目阶段、环境和资源保留单独的文档,在易于访问的空间中,如ConfluenceSharePointGoogle DriveAsana

  2. 引入编程和文档的标准。确保在代码审查中始终遵循这些标准。请记住始终记录开发环境和关键库依赖的详细信息。利用诸如 Docker 或 Anaconda 之类的虚拟环境工具,以便以可重现的方式保持它们。

  3. 总结在项目中重复使用的关键概念,并确保它们被彻底记录和易于访问。

  4. 持续审查存储信息的状态,以确保在整个项目执行过程中保持其最新状态。

在项目的最后阶段,建议再次审查文档,以填补遗漏的细节。请记住,如果这些资源能处理许多重复的任务,那么你的下一个项目可以更高效地实现。

根据您的地理位置,当交付物消耗敏感数据时,您可能需要遵循特定的法律。常见的包括 GDPR、HIPAA、FCRA、FERPA、GLBA、ECPA、COPPA 和 VPPA (www.nytimes.com/wirecutter/blog/state-of-privacy-laws-in-us/)。项目的最后阶段是确保在任何外部组织审计之前遵循所有法规和合规程序的好时机。

需记住的事项

a. 在项目的最后阶段,您需要回顾所有从项目生成的工件。它们的组织和文档必须重新审视,以便将来可以轻松检索。

b. 建立工件管理流程将使您能够保持其组织良好。例如,定义文档标准并遵循这些标准将有助于您保持资源的一致性。

概要

您已经到达 DL 项目的最终阶段。在本章中,我们描述了您需要遵循的步骤来完成项目。我们首先描述了如何应用 PIR 来评估项目并了解潜在的改进。在这个阶段,您还需要确保从项目中生成的工件被组织和彻底记录,以便它们可以被重复使用在下一个项目中。最后,我们还想提到庆祝活动是 DL 项目的另一个关键组成部分。所有的利益相关者都为完成项目做出了努力。您必须花些时间感谢所有团队成员,并赞扬他们的成就。

在本书中,您学习了如何在高标准下执行 DL 项目。从 DL 的基本概念开始,我们详细描述了 DL 项目的每个阶段,以及您可以使用的各种工具来有效地完成手头的任务。本书强调可扩展性,并解释了如何使用各种云服务进行数据处理和模型训练。总体而言,现在您能够正确估算项目的范围,为给定问题构建一个有效的基于 DL 的解决方案,并适当评估项目的成功。

此时,我们想要感谢您阅读本书。我们很高兴看到本书在人工智能领域理论与应用之间的桥梁。正如我们在撰写本书时对这一领域获得了许多见解一样,我们希望您与我们一起的旅程是一个非凡的学习经验。

posted @ 2024-07-23 14:54  绝不原创的飞龙  阅读(6)  评论(0编辑  收藏  举报