PyTorch-口袋参考-全-
PyTorch 口袋参考(全)
译者:飞龙
前言
我们生活在激动人心的时代!我们中的一些人有幸经历了技术的巨大进步——个人计算机的发明,互联网的兴起,手机的普及以及社交媒体的出现。现在,人工智能领域正在发生重大突破!
看到并参与这种变革是令人兴奋的。我认为我们才刚刚开始,想到未来十年世界可能会发生怎样的变化,这是令人惊奇的。我们能够生活在这个时代并参与人工智能的扩展,这是多么伟大的事情?
毫无疑问,PyTorch 已经实现了一些深度学习和人工智能领域的最好进展。它是免费下载和使用的,任何拥有计算机或互联网连接的人都可以运行人工智能实验。除了像这样更全面的参考资料外,还有许多免费和廉价的培训课程、博客文章和教程可以帮助您。任何人都可以开始使用 PyTorch 进行机器学习和人工智能。
谁应该阅读这本书
这本书是为对机器学习和人工智能感兴趣的初学者和高级用户编写的。最好具有一些编写 Python 代码的经验以及对数据科学和机器学习的基本理解。
如果您刚开始学习机器学习,这本书将帮助您学习 PyTorch 的基础知识并提供一些简单的示例。如果您已经使用其他框架,如 TensorFlow、Caffe2 或 MXNet,这本书将帮助您熟悉 PyTorch 的 API 和编程思维方式,以便扩展您的技能。
如果您已经使用 PyTorch 一段时间,这本书将帮助您扩展对加速和优化等高级主题的知识,并在您日常开发中使用 PyTorch 时提供快速参考资源。
我为什么写这本书
学习和掌握 PyTorch 可能会非常令人兴奋。有很多东西可以探索!当我开始学习 PyTorch 时,我希望有一个资源可以教会我一切。我想要一些东西,可以让我对 PyTorch 提供的内容有一个很好的高层次了解,但也可以在我需要深入了解时提供示例和足够的细节。
有一些关于 PyTorch 的很好的书籍和课程,但它们通常侧重于张量和深度学习模型的训练。PyTorch 的在线文档也非常好,并提供了许多细节和示例;然而,我发现使用它通常很麻烦。我不断地不得不点击以学习或谷歌我需要知道的内容。我需要一本书放在桌子上,我可以标记并在编码时作为参考。
我的目标是这将是您的终极 PyTorch 参考资料。除了通读以获取对 PyTorch 资源的高层次理解之外,我希望您为您的开发工作标记关键部分并将其放在桌子上。这样,如果您忘记了某些内容,您可以立即得到答案。如果您更喜欢电子书或在线书籍,您可以在线收藏本书。无论您如何使用,我希望这本书能帮助您用 PyTorch 创建一些令人惊人的新技术!
浏览本书
如果您刚开始学习 PyTorch,您应该从第一章开始,并按顺序阅读每一章。这些章节从初学者到高级主题逐渐展开。如果您已经有一些 PyTorch 经验,您可能想跳到您最感兴趣的主题。不要忘记查看关于 PyTorch 生态系统的第八章。您一定会发现一些新东西!
这本书大致组织如下:
-
第一章简要介绍了 PyTorch,帮助您设置开发环境,并提供一个有趣的示例供您尝试。
-
第二章介绍了张量,PyTorch 的基本构建块。这是 PyTorch 中一切的基础。
-
第三章全面介绍了您如何使用 PyTorch 进行深度学习,而第四章提供了示例参考设计,让您可以看到 PyTorch 的实际应用。
-
第五章和第六章涵盖了更高级的主题。第五章向您展示了如何自定义 PyTorch 组件以适应您自己的工作,而第六章则向您展示了如何加速训练并优化您的模型。
-
第七章向您展示如何通过本地机器、云服务器和移动或边缘设备将 PyTorch 部署到生产环境。
-
第八章引导您下一步,介绍 PyTorch 生态系统,描述流行的软件包,并列出额外的培训资源。
本书中使用的约定
本书使用以下印刷约定:
斜体
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
等宽字体
用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
等宽粗体
显示用户应该按照字面意义输入的命令或其他文本。此外,在表格中,粗体用于强调函数。
等宽斜体
显示应该用用户提供的值或上下文确定的值替换的文本。此外,在表格中列出的转换目前不受 TorchScript 支持。
使用代码示例
补充材料(代码示例、练习等)可在https://github.com/joe-papa/pytorch-book下载。
如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需征得我们的许可。例如,编写一个使用本书中几个代码块的程序不需要许可。销售或分发 O'Reilly 图书中的示例需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书中大量示例代码合并到产品文档中需要许可。
我们感谢,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“PyTorch 口袋参考 作者 Joe Papa(O'Reilly)。版权所有 2021 年 Mobile Insights Technology Group,LLC,978-1-492-09000-7。”
如果您认为您对代码示例的使用超出了合理使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。
致谢
作为读者,我经常在阅读其他作者的致谢时感到惊讶。写一本书并不是一件小事,写一本好书需要许多人的支持。阅读致谢是一个不断提醒我们不能独自完成的过程。
我感谢我的朋友 Matt Kirk 的支持和鼓励,多年前在 O'Reilly 会议上认识他。他对个人发展的共同热情激励着我创作图书和课程,帮助他人充分发挥个人和职业潜力。在疫情期间,我们每周的 Zoom 聊天和自助项目确实帮助我保持理智。没有 Matt,这本书就不可能完成。
我要感谢 Rebecca Novack 建议这个项目并给我机会,以及 O'Reilly 的工作人员让这个项目得以实现。
写一本书需要努力,但写一本好书需要关心读者的专注审阅者。我要感谢 Mike Drob、Axel Sirota 和 Jeff Bleiel 花时间审查这本书并提供无数建议。Mike 的建议增加了许多实用资源,否则我可能会忽视。他确保我们使用的是最先进的工具和您在在线文档中找不到的最佳实践。
Axel 对细节的关注令人难以置信。我感谢他对本书代码和技术细节的审查和努力。Jeff 是一位出色的编辑。我感谢他对本书的顺序和流程提出的建议。他显著帮助我成为一个更好的作者。
PyTorch 真正是一个社区项目。我感谢 Facebook 和超过 1700 名贡献者开发了这个机器学习框架。我特别要感谢那些创建文档和教程来帮助像我这样的人快速学习 PyTorch 的人。
对我帮助最大的一些人包括 Suraj Subramanian、Seth Juarez、Cassie Breviu、Dmitry Soshnikov、Ari Bornstein、Soumith Chintala、Justin Johnson、Jeremy Howard、Rachel Thomas、Francisco Ingham、Sasank Chilamkurthy、Nathan Inkawhich、Sean Robertson、Ben Trevett、Avinash Sajjanshetty、James Reed、Michael Suo、Michela Paganini、Shen Li、Séb Arnold、Rohan Varma、Pritam Damania、Jeff Tang,以及关于 PyTorch 主题的无数博主和 YouTuber。
我感谢 Manbir Gulati 介绍我认识 PyTorch,感谢 Rob Miller 给我机会领导 PyTorch 的 AI 项目。我也感谢与我的朋友 Isaac Privitera 分享这本书的深度学习思想。
当然,没有我妈妈 Grace 的辛勤工作和奉献精神,我在生活中无法取得任何成就,她带领我们从不起眼的开始,给了我和我哥哥一个生活的机会。我每天都在想念她。
特别感谢我的哥哥文尼,在完成家庭项目时给予了很大帮助,让我有更多时间写作。我感激我的继父卢,在我写书时给予的鼓励。我还要感谢我的孩子们,萨凡娜、卡罗琳和乔治,在爸爸工作时耐心理解。
最后,我想感谢我的妻子艾米莉。她一直无限支持我的想法和梦想。当我着手写这本书的任务时,当然又一次依靠了她。在疫情期间照顾我们的三个孩子并承担新的责任是一项艰巨的任务。
然而,她一直是我完成写作所需的支持。事实上,在写这本书的过程中,我们发现我们正在期待第四个孩子的到来!我的妻子总是带着微笑和笑话(通常是拿我开玩笑),我很爱她。
第一章:PyTorch 简介
PyTorch 是最受欢迎的深度学习 Python 库之一,广泛被人工智能研究社区使用。许多开发人员和研究人员使用 PyTorch 来加速深度学习研究实验和原型设计。
在本章中,我将简要介绍 PyTorch 是什么以及使其受欢迎的一些特点。我还将向您展示如何在本地机器和云端安装和设置 PyTorch 开发环境。通过本章的学习,您将能够验证 PyTorch 已正确安装并运行一个简单的 PyTorch 程序。
什么是 PyTorch?
PyTorch 库主要由 Facebook 的人工智能研究实验室(FAIR)开发,是一款免费的开源软件,拥有超过 1700 名贡献者。它允许您轻松运行基于数组的计算,在 Python 中构建动态神经网络,并进行自动微分,具有强大的图形处理单元(GPU)加速——这些都是深度学习研究所需的重要功能。尽管有些人用它来加速张量计算,但大多数人用它来进行深度学习开发。
PyTorch 的简单和灵活接口使快速实验成为可能。您可以加载数据,应用转换,并用几行代码构建模型。然后,您可以灵活地编写定制的训练、验证和测试循环,并轻松部署训练好的模型。
它拥有强大的生态系统和庞大的用户社区,包括斯坦福大学等大学和优步、英伟达和 Salesforce 等公司。2019 年,PyTorch 在机器学习和深度学习会议论文中占据主导地位:69%的计算机视觉与模式识别(CVPR)会议论文使用 PyTorch,超过 75%的计算语言学协会(ACL)和北美 ACL 分会(NAACL)使用它,超过 50%的学习表示国际会议(ICLR)和国际机器学习会议(ICML)也使用它。GitHub 上有超过 60000 个与 PyTorch 相关的存储库。
许多开发人员和研究人员使用 PyTorch 来加速深度学习研究实验和原型设计。其简单的 Python API、GPU 支持和灵活性使其成为学术和商业研究机构中的热门选择。自 2018 年开源以来,PyTorch 已经发布了稳定版本,并可以轻松安装在 Windows、Mac 和 Linux 操作系统上。该框架继续迅速扩展,现在可以方便地部署到云端和移动平台的生产环境中。
为什么使用 PyTorch?
如果您正在学习机器学习、进行深度学习研究或构建人工智能系统,您可能需要使用一个深度学习框架。深度学习框架使得执行常见任务如数据加载、预处理、模型设计、训练和部署变得容易。由于其简单性、灵活性和 Python 接口,PyTorch 已经在学术和研究社区中变得非常流行。以下是学习和使用 PyTorch 的一些原因:
PyTorch 很受欢迎
许多公司和研究机构将 PyTorch 作为他们的主要深度学习框架。事实上,一些公司已经在 PyTorch 的基础上构建了他们的自定义机器学习工具。因此,PyTorch 技能需求量大。
PyTorch 得到所有主要云平台的支持,如亚马逊网络服务(AWS)、谷歌云平台(GCP)、微软 Azure 和阿里云
您可以快速启动一个预装有 PyTorch 的虚拟机,进行无摩擦的开发。您可以使用预构建的 Docker 镜像,在云 GPU 平台上进行大规模训练,并在生产规模上运行模型。
PyTorch 得到 Google Colaboratory 和 Kaggle Kernels 的支持
您可以在浏览器中运行 PyTorch 代码,无需安装或配置。您可以通过在内核中直接运行 PyTorch 来参加 Kaggle 竞赛。
PyTorch 是成熟和稳定的
PyTorch 定期维护,现在已经超过 1.8 版本。
PyTorch 支持 CPU、GPU、TPU 和并行处理
您可以使用 GPU 和 TPU 加速训练和推断。张量处理单元(TPUs)是由 Google 开发的人工智能加速的应用特定集成电路(ASIC)芯片,旨在为 NN 硬件加速提供替代 GPU 的选择。通过并行处理,您可以在 CPU 上应用预处理,同时在 GPU 或 TPU 上训练模型。
PyTorch 支持分布式训练
您可以在多台机器上的多个 GPU 上训练神经网络。
PyTorch 支持部署到生产环境
借助新的 TorchScript 和 TorchServe 功能,您可以轻松将模型部署到包括云服务器在内的生产环境中。
PyTorch 开始支持移动部署
尽管目前仍处于实验阶段,但您现在可以将模型部署到 iOS 和 Android 设备上。
PyTorch 拥有庞大的生态系统和一套开源库
诸如 Torchvision、fastai 和 PyTorch Lightning 等库扩展了功能并支持特定领域,如自然语言处理(NLP)和计算机视觉。
PyTorch 还具有 C++前端
尽管本书将重点放在 Python 接口上,但 PyTorch 也支持前端 C++接口。如果您需要构建高性能、低延迟或裸机应用程序,可以使用相同的设计和架构在 C++中编写,就像使用 Python API 一样。
PyTorch 原生支持开放神经网络交换(ONNX)格式
您可以轻松将模型导出为 ONNX 格式,并在 ONNX 兼容的平台、运行时或可视化器中使用它们。
PyTorch 拥有庞大的开发者社区和用户论坛
PyTorch 论坛上有超过 38,000 名用户,通过访问PyTorch 讨论论坛很容易获得支持或发布问题。
入门
如果您熟悉 PyTorch,可能已经安装并设置了开发环境。如果没有,我将在本节中向您展示一些选项。开始的最快方式是使用 Google Colaboratory(或Colab)。Google Colab 是一个免费的基于云的开发环境,类似于 Jupyter Notebook,并已安装了 PyTorch。Colab 提供免费的有限 GPU 支持,并与 Google Drive 接口良好,可用于保存和共享笔记本。
如果您没有互联网访问,或者想在自己的硬件上运行 PyTorch 代码,那么我将向您展示如何在本地机器上安装 PyTorch。您可以在 Windows、Linux 和 macOS 操作系统上安装 PyTorch。我建议您拥有 NVIDIA GPU 进行加速,但不是必需的。
最后,您可能希望使用 AWS、Azure 或 GCP 等云平台开发 PyTorch 代码。如果您想使用云平台,我将向您展示在每个平台上快速入门的选项。
在 Google Colaboratory 中运行
使用 Google Colab,您可以在浏览器中编写和执行 Python 和 PyTorch 代码。您可以直接将文件保存到 Google Drive 帐户,并轻松与他人共享您的工作。要开始,请访问Google Colab 网站,如图 1-1 所示。

图 1-1. Google Colaboratory 欢迎页面
如果您已经登录到您的 Google 帐户,将会弹出一个窗口。单击右下角的“新笔记本”。如果弹出窗口未出现,请单击“文件”,然后从菜单中选择“新笔记本”。您将被提示登录或创建 Google 帐户,如图 1-2 所示。

图 1-2. Google 登录
验证您的配置,导入 PyTorch 库,打印已安装的版本,并检查是否正在使用 GPU,如图 1-3 所示。

图 1-3. 在 Google Colaboratory 中验证 PyTorch 安装
默认情况下,我们的 Colab 笔记本不使用 GPU。您需要从运行时菜单中选择更改运行时类型,然后从“硬件加速器”下拉菜单中选择 GPU 并单击保存,如图 1-4 所示。

图 1-4. 在 Google Colaboratory 中使用 GPU
现在再次运行单元格,选择单元格并按 Shift-Enter。您应该看到is_available()的输出为True,如图 1-5 所示。

图 1-5. 在 Google Colaboratory 中验证 GPU 是否激活
注意
Google 提供了一个付费版本称为 Colab Pro,提供更快的 GPU、更长的运行时间和更多内存。对于本书中的示例,免费版本的 Colab 应该足够了。
现在您已经验证了 PyTorch 已安装,并且您也知道版本。您还验证了您有一个可用的 GPU,并且正确安装和运行了适当的驱动程序。接下来,我将向您展示如何在本地机器上验证您的 PyTorch。
在本地计算机上运行
在某些情况下,您可能希望在本地机器或自己的服务器上安装 PyTorch。例如,您可能希望使用本地存储,或者使用自己的 GPU 或更快的 GPU 硬件,或者您可能没有互联网访问。运行 PyTorch 不需要 GPU,但需要 GPU 加速才能运行。我建议使用 NVIDIA GPU,因为 PyTorch 与用于 GPU 支持的 Compute Unified Device Architecture(CUDA)驱动程序紧密相关。
警告
首先检查您的 GPU 和 CUDA 版本!PyTorch 仅支持特定的 GPU 和 CUDA 版本,许多 Mac 电脑使用非 NVIDIA GPU。如果您使用的是 Mac,请通过单击菜单栏上的苹果图标,选择“关于本机”,然后单击“显示”选项卡来验证您是否有 NVIDIA GPU。如果您在 Mac 上看到 NVIDIA GPU 并希望使用它,您将需要从头开始构建 PyTorch。如果您没有看到 NVIDIA GPU,则应使用 PyTorch 的仅 CPU 版本或选择另一台具有不同操作系统的计算机。
PyTorch 网站提供了一个方便的浏览器工具用于安装,如图 1-6 所示。选择最新的稳定版本,您的操作系统,您喜欢的 Python 包管理器(推荐使用 Conda),Python 语言和您的 CUDA 版本。执行命令行并按照您的配置的说明进行操作。请注意先决条件、安装说明和验证方法。

图 1-6. PyTorch 在线安装配置工具
您应该能够在您喜欢的 IDE(Jupyter Notebook、Microsoft Visual Studio Code、PyCharm、Spyder 等)或终端中运行验证代码片段。图 1-7 显示了如何在 Mac 终端上验证 PyTorch 的正确版本是否已安装。相同的命令也可以用于在 Windows 或 Linux 终端中验证。

图 1-7. 使用 Mac 终端验证 PyTorch
在云平台上运行
如果您熟悉 AWS、GCP 或 Azure 等云平台,您可以在云中运行 PyTorch。云平台为训练和部署深度学习模型提供强大的硬件和基础设施。请记住,使用云服务,特别是 GPU 实例,会产生额外的费用。要开始,请按照感兴趣的平台的在线 PyTorch 云设置指南中的说明进行操作。
设置云环境超出了本书的范围,但我将总结可用的选项。每个平台都提供虚拟机实例以及托管服务来支持 PyTorch 开发。
在 AWS 上运行
AWS 提供多种在云中运行 PyTorch 的选项。如果您更喜欢全面托管服务,可以使用 AWS SageMaker,或者如果您更喜欢管理自己的基础架构,可以使用 AWS 深度学习 Amazon 机器映像(AMI)或容器:
Amazon SageMaker
这是一个全面托管的服务,用于训练和部署模型。您可以从仪表板运行 Jupyter 笔记本,并使用 SageMaker Python SDK 在云中训练和部署模型。您可以在专用 GPU 实例上运行您的笔记本。
AWS 深度学习 AMI
这些是预配置的虚拟机环境。您可以选择 Conda AMI,其中预先安装了许多库(包括 PyTorch),或者如果您更喜欢一个干净的环境来设置私有存储库或自定义构建,可以使用基本 AMI。
AWS 深度学习容器
这些是预先安装了 PyTorch 的 Docker 镜像。它们使您可以跳过从头开始构建和优化环境的过程,主要用于部署。
有关如何入门的更详细信息,请查看“在 AWS 上开始使用 PyTorch”说明。
在 Microsoft Azure 上运行
Azure 还提供多种在云中运行 PyTorch 的选项。您可以使用名为 Azure Machine Learning 的全面托管服务开发 PyTorch 模型,或者如果您更喜欢管理自己的基础架构,可以运行数据科学虚拟机(DSVMs):
Azure Machine Learning
这是一个用于构建和部署模型的企业级机器学习服务。它包括拖放设计器和 MLOps 功能,可与现有的 DevOps 流程集成。
DSVMs
这些是预配置的虚拟机环境。它们预先安装了 PyTorch 和其他深度学习框架以及开发工具,如 Jupyter Notebook 和 VS Code。
有关如何入门的更详细信息,请查看Azure Machine Learning 文档。
在 Google 云平台上运行
GCP 还提供多种在云中运行 PyTorch 的选项。您可以使用名为 AI 平台笔记本的托管服务开发 PyTorch 模型,或者如果您更喜欢管理自己的基础架构,可以运行深度学习 VM 镜像:
AI 平台笔记本
这是一个托管服务,其集成的 JupyterLab 环境允许您创建预配置的 GPU 实例。
深度学习 VM 镜像
这些是预配置的虚拟机环境。它们预先安装了 PyTorch 和其他深度学习框架以及开发工具。
有关如何入门的更详细信息,请查看 Google Cloud 的“AI 和机器学习产品”说明。
验证您的 PyTorch 环境
无论您使用 Colab、本地计算机还是您喜爱的云平台,您都应该验证 PyTorch 是否已正确安装,并检查是否有 GPU 可用。您已经在 Colab 中看到了如何执行此操作。要验证 PyTorch 是否已正确安装,请使用以下代码片段。该代码导入 PyTorch 库,打印版本,并检查是否有 GPU 可用:
import torch
print(torch.__version__)
print(torch.cuda.is_available())
警告
您使用import torch导入库,而不是import pytorch。PyTorch 最初基于torch库,这是一个基于 C 和 Lua 编程语言的开源机器学习框架。保持库命名为torch允许 Torch 代码与更高效的 PyTorch 实现重用。
一个有趣的例子
现在您已经验证了您的环境是否正确配置,让我们编写一个有趣的例子,展示 PyTorch 的一些特性,并演示机器学习中的最佳实践。在这个例子中,我们将构建一个经典的图像分类器,尝试根据 1,000 个可能的类别或选择来识别图像的内容。
您可以从本书的 GitHub 存储库访问此示例并跟随。尝试在 Google Colab、本地计算机或 AWS、Azure 或 GCP 等云平台上运行代码。不用担心理解机器学习的所有概念。我们将在本书中更详细地介绍它们。
注意
在实践中,您将在代码开头导入所有必要的库。然而,在这个例子中,我们将在使用时导入库,这样您就可以看到每个任务需要哪些库。
首先,让我们选择一个我们想要分类的图像。在这个例子中,我们将选择一杯美味的新鲜热咖啡。使用以下代码将咖啡图像下载到您的本地环境:
import urllib.request
url = url = 'https://pytorch.tips/coffee'
fpath = 'coffee.jpg'
urllib.request.urlretrieve(url, fpath)
请注意,代码使用urllib库的urlretrieve()函数从网络获取图像。我们通过指定fpath将文件重命名为coffee.jpg。
接下来,我们使用 Pillow 库(PIL)读取我们的本地图像:
import matplotlib.pyplot as plt
from PIL import Image
img = Image.open('coffee.jpg')
plt.imshow(img)
图 1-8 展示了我们的图像是什么样子的。我们可以使用matplotlib的imshow()函数在我们的系统上显示图像,就像前面的代码所示的那样。

图 1-8。分类器的输入图像
请注意我们还没有使用 PyTorch。这里就是事情变得令人兴奋的地方。接下来,我们将把我们的图像传递给一个预训练的图像分类神经网络(NN)—但在这之前,我们需要预处理我们的图像。在机器学习中,预处理数据是非常常见的,因为 NN 期望输入满足某些要求。
在我们的示例中,图像数据是一个 RGB 1600 × 1200 像素的 JPEG 格式图像。我们需要应用一系列预处理步骤,称为转换,将图像转换为 NN 的正确格式。我们使用 Torchvision 在下面的代码中实现这一点:
import torch
from torchvision import transforms
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])])
img_tensor = transform(img)
print(type(img_tensor), img_tensor.shape)
# out:
# <class 'torch.tensor'> torch.Size([3, 224, 224])
我们使用Compose()转换来定义一系列用于预处理我们的图像的转换。首先,我们需要调整大小并裁剪图像以适应 NN。图像目前是 PIL 格式,因为我们之前是这样读取的。但是我们的 NN 需要一个张量输入,所以我们将 PIL 图像转换为张量。
张量是 PyTorch 中的基本数据对象,我们将在整个下一章中探索它们。您可以将张量视为 NumPy 数组或带有许多额外功能的数值数组。现在,我们只需将我们的图像转换为一个数字的张量数组,使其准备好。
我们应用了另一个叫做Normalize()的转换,来重新缩放像素值的范围在 0 和 1 之间。均值和标准差(std)的值是基于用于训练模型的数据预先计算的。对图像进行归一化可以提高分类器的准确性。
最后,我们调用transform(img)来将所有的转换应用到图像上。正如你所看到的,img_tensor是一个 3 × 224 × 224 的torch.Tensor,代表着一个 3 通道、224 × 224 像素的图像。
高效的机器学习过程会批处理数据,我们的模型会期望一批数据。然而,我们只有一张图像,所以我们需要创建一个大小为 1 的批次,如下面的代码所示:
batch = img_tensor.unsqueeze(0)
print(batch.shape)
# out: torch.Size([1, 3, 224, 224])
我们使用 PyTorch 的unsqueeze()函数向我们的张量添加一个维度,并创建一个大小为 1 的批次。现在我们有一个大小为 1 × 3 × 224 × 224 的张量,代表一个批次大小为 1 和 3 通道(RGB)的 224 × 224 像素。PyTorch 提供了许多有用的函数,比如unsqueeze()来操作张量,我们将在下一章中探索其中许多函数。
现在我们的图像已经准备好用于我们的分类器 NN 了!我们将使用一个名为 AlexNet 的著名图像分类器。AlexNet 在 2012 年的 ImageNet 大规模视觉识别挑战赛中获胜。使用 Torchvision 很容易加载这个模型,如下面的代码所示:
from torchvision import models
model = models.alexnet(pretrained=True)
我们将在这里使用一个预训练的模型,所以不需要训练它。AlexNet 模型已经使用数百万张图像进行了预训练,并且在分类图像方面表现得相当不错。让我们传入我们的图像,看看它的表现:
device = "cuda" if torch.cuda.is_available() else "cpu"
print(device)
# out(results will vary): cpu
model.eval()
model.to(device)
y = model(batch.to(device))
print(y.shape)
# out: torch.Size([1, 1000])
GPU 加速是 PyTorch 的一个关键优势。在第一行中,我们使用 PyTorch 的cuda.is_available()函数来查看我们的机器是否有 GPU。这是 PyTorch 代码中非常常见的一行,我们将在第二章和第六章进一步探讨 GPU。我们只对一个图像进行分类,所以这里不需要 GPU,但如果我们有一个巨大的批次,使用 GPU 可能会加快速度。
model.eval()函数配置我们的 AlexNet 模型进行推断或预测(与训练相对)。模型的某些组件仅在训练期间使用,我们不希望在这里使用它们。使用model.to(device)和batch.to(device)将我们的模型和输入数据发送到 GPU(如果可用),执行model(batch.to(device))运行我们的分类器。
输出y包含一个批次的 1,000 个输出。由于我们的批次只包含一个图像,第一维是1,而类的数量是1000,每个类有一个值。值越高,图像包含该类的可能性就越大。以下代码找到获胜的类:
y_max, index = torch.max(y,1)
print(index, y_max)
# out: tensor([967]) tensor([22.3059],
# grad_fn=<MaxBackward0>)
使用 PyTorch 的max()函数,我们看到索引为 967 的类具有最高值 22.3059,因此是获胜者。但是,我们不知道类 967 代表什么。让我们加载包含类名的文件并找出:
url = 'https://pytorch.tips/imagenet-labels'
fpath = 'imagenet_class_labels.txt'
urllib.request.urlretrieve(url, fpath)
with open('imagenet_class_labels.txt') as f:
classes = [line.strip() for line in f.readlines()]
print(classes[967])
# out: 967: 'espresso',
就像我们之前做的那样,我们使用urlretrieve()下载包含每个类描述的文本文件。然后,我们使用readlines()读取文件,并创建一个包含类名的列表。当我们print(classes[967])时,它显示类 967 是espresso!
使用 PyTorch 的softmax()函数,我们可以将输出值转换为概率:
prob = torch.nn.functional.softmax(y, dim=1)[0] * 100
print(classes[index[0]], prob[index[0]].item())
#967: 'espresso', 87.85208892822266
要打印索引处的概率,我们使用 PyTorch 的tensor.item()方法。item()方法经常被使用,并返回张量中包含的数值。结果显示,模型有 87.85%的把握这是一张浓缩咖啡的图像。
我们可以使用 PyTorch 的sort()函数对输出概率进行排序,并查看前五个:
_, indices = torch.sort(y, descending=True)
for idx in indices[0][:5]:
print(classes[idx], prob[idx].item())
# out:
# 967: 'espresso', 87.85208892822266
# 968: 'cup', 7.28359317779541
# 504: 'coffee mug', 4.33521032333374
# 925: 'consomme', 0.36686763167381287
# 960: 'chocolate sauce, chocolate syrup',
# 0.09037172049283981
我们看到模型预测图像是espresso的概率为 87.85%。它还以 7.28%的概率预测cup,以 4.3%的概率预测coffee mug,但它似乎非常确信图像是一杯浓缩咖啡。
您可能现在感觉需要一杯浓缩咖啡。在那个示例中,我们涵盖了很多内容!实际上,实现所有这些的核心代码要短得多。假设您已经下载了文件,您只需要运行以下代码来使用 AlexNet 对图像进行分类:
import torch
from torchvision import transforms, models
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])])
img_tensor = transform(img)
batch = img_tensor.unsqueeze(0)
model = models.alexnet(pretrained=True)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.eval()
model.to(device)
y = model(batch.to(device))
prob = torch.nn.functional.softmax(y, dim=1)[0] * 100
_, indices = torch.sort(y, descending=True)
for idx in indices[0][:5]:
print(classes[idx], prob[idx].item())
这就是如何使用 PyTorch 构建图像分类器。尝试通过模型运行自己的图像,并查看它们的分类情况。还可以尝试在另一个平台上完成示例。例如,如果您使用 Colab 运行代码,请尝试在本地或云中运行它。
恭喜,您已经验证了您的环境已正确配置,并且可以执行 PyTorch 代码!我们将在本书的其余部分更深入地探讨每个主题。在下一章中,我们将探讨 PyTorch 的基础知识,并提供张量及其操作的快速参考。
第二章:张量
在深入了解 PyTorch 开发世界之前,熟悉 PyTorch 中的基本数据结构torch.Tensor是很重要的。通过理解张量,您将了解 PyTorch 如何处理和存储数据,由于深度学习基本上是浮点数的收集和操作,理解张量将帮助您了解 PyTorch 如何为深度学习实现更高级的功能。此外,在预处理输入数据或在模型开发过程中操作输出数据时,您可能经常使用张量操作。
本章作为理解张量和在代码中实现张量函数的快速参考。我将从描述张量是什么开始,并向您展示如何使用函数来创建、操作和加速 GPU 上的张量操作的一些简单示例。接下来,我们将更广泛地查看创建张量和执行数学操作的 API,以便您可以快速查阅一份全面的张量功能列表。在每个部分中,我们将探讨一些更重要的函数,识别常见的陷阱,并检查它们的使用中的关键点。
张量是什么?
在 PyTorch 中,张量是一种用于存储和操作数据的数据结构。与 NumPy 数组类似,张量是一个包含单一数据类型元素的多维数组。张量可以用来表示标量、向量、矩阵和n维数组,并且是从torch.Tensor类派生的。然而,张量不仅仅是数字数组。从torch.Tensor类创建或实例化张量对象使我们可以访问一组内置的类属性和操作或类方法,提供了一套强大的内置功能。本章详细描述了这些属性和操作。
张量还包括一些附加优势,使它们比 NumPy 数组更适合用于深度学习计算。首先,使用 GPU 加速可以显著加快张量操作的速度。其次,可以使用分布式处理在多个 CPU 和 GPU 上以及跨多个服务器上存储和操作张量。第三,张量跟踪它们的图计算,正如我们将在“自动微分(Autograd)”中看到的,这在实现深度学习库中非常重要。
为了进一步解释张量实际上是什么以及如何使用它,我将从一个简单示例开始,创建一些张量并执行一个张量操作。
简单 CPU 示例
这里有一个简单的示例,创建一个张量,执行一个张量操作,并在张量本身上使用一个内置方法。默认情况下,张量数据类型将从输入数据类型派生,并且张量将分配到 CPU 设备。首先,我们导入 PyTorch 库,然后我们从二维列表创建两个张量x和y。接下来,我们将这两个张量相加,并将结果存储在z中。我们可以在这里使用+运算符,因为torch.Tensor类支持运算符重载。最后,我们打印新的张量z,我们可以看到它是x和y的矩阵和,并打印z的大小。注意,z本身是一个张量对象,size()方法用于返回其矩阵维度,即 2×3:
import torch
x = torch.tensor([[1,2,3],[4,5,6]])
y = torch.tensor([[7,8,9],[10,11,12]])
z = x + y
print(z)
# out:
# tensor([[ 8, 10, 12],
# [14, 16, 18]])
print(z.size())
# out: torch.Size([2, 3])
注意
在旧代码中可能会看到使用torch.Tensor()(大写 T)构造函数。这是torch.FloatTensor默认张量类型的别名。您应该改用torch.tensor()来创建您的张量。
简单 GPU 示例
由于在 GPU 上加速张量操作是张量优于 NumPy 数组的主要优势,我将向您展示一个简单的示例。这是上一节中的相同示例,但在这里,如果有 GPU 设备,我们将将张量移动到 GPU 设备。请注意,输出张量也分配给了 GPU。您可以使用设备属性(例如z.device)来双重检查张量所在的位置。
在第一行中,torch.cuda.is_available()函数将在您的机器支持 GPU 时返回True。这是一种方便的编写更健壮代码的方式,可以在存在 GPU 时加速运行,但在没有 GPU 时也可以在 CPU 上运行。在输出中,device='cuda:0'表示正在使用第一个 GPU。如果您的机器包含多个 GPU,您还可以控制使用哪个 GPU:
device = "cuda" if torch.cuda.is_available()
else "cpu"
x = torch.tensor([[1,2,3],[4,5,6]],
device=device)
y = torch.tensor([[7,8,9],[10,11,12]],
device=device)
z = x + y
print(z)
# out:
# tensor([[ 8, 10, 12],
# [14, 16, 18]], device='cuda:0')
print(z.size())
# out: torch.Size([2, 3])
print(z.device)
# out: cuda:0
在 CPU 和 GPU 之间移动张量
前面的代码使用torch.tensor()在特定设备上创建张量;然而,更常见的是将现有张量移动到设备上,即如果有 GPU 设备的话,通常是 GPU。您可以使用torch.to()方法来实现。当通过张量操作创建新张量时,PyTorch 会在相同设备上创建新张量。在下面的代码中,z位于 GPU 上,因为x和y位于 GPU 上。张量z通过torch.to("cpu")移回 CPU 进行进一步处理。还请注意,操作中的所有张量必须在同一设备上。如果x在 GPU 上,而y在 CPU 上,我们将会收到错误:
device = "cuda" if torch.cuda.is_available()
else "cpu"
x = x.to(device)
y = y.to(device)
z = x + y
z = z.to("cpu")
# out:
# tensor([[ 8, 10, 12],
# [14, 16, 18]])
注意
您可以直接使用字符串作为设备参数,而不是设备对象。以下都是等效的:
-
device="cuda" -
device=torch.device("cuda") -
device="cuda:0" -
device=torch.device("cuda:0")
创建张量
前一节展示了创建张量的简单方法;然而,还有许多其他方法可以实现。您可以从现有的数字数据中创建张量,也可以创建随机抽样。张量可以从存储在类似数组结构中的现有数据(如列表、元组、标量或序列化数据文件)以及 NumPy 数组中创建。
以下代码说明了创建张量的一些常见方法。首先,它展示了如何使用torch.tensor()从列表创建张量。此方法也可用于从其他数据结构(如元组、集合或 NumPy 数组)创建张量:
import numpy
# Created from preexisting arrays
w = torch.tensor([1,2,3]) # ①
w = torch.tensor((1,2,3)) # ②
w = torch.tensor(numpy.array([1,2,3])) # ③
# Initialized by size
w = torch.empty(100,200) # ④
w = torch.zeros(100,200) # ⑤
w = torch.ones(100,200) # ⑥
①
从列表中
②
从元组中
③
从 NumPy 数组中
④
未初始化;元素值不可预测
⑤
所有元素初始化为 0.0
⑥
所有元素初始化为 1.0
如前面的代码示例所示,您还可以使用torch.empty()、torch.ones()和torch.zeros()等函数创建和初始化张量,并指定所需的大小。
如果您想要使用随机值初始化张量,PyTorch 支持一组强大的函数,例如torch.rand()、torch.randn()和torch.randint(),如下面的代码所示:
# Initialized by size with random values
w = torch.rand(100,200) # ①
w = torch.randn(100,200) # ②
w = torch.randint(5,10,(100,200)) # ③
# Initialized with specified data type or device
w = torch.empty((100,200), dtype=torch.float64,
device="cuda")
# Initialized to have the same size, data type,
# and device as another tensor
x = torch.empty_like(w)
①
创建一个 100×200 的张量,元素来自区间 0,1)上的均匀分布。
![2 元素是均值为 0、方差为 1 的正态分布随机数。// # ③
元素是介于 5 和 10 之间的随机整数。
在初始化时,您可以像前面的代码示例中所示指定数据类型和设备(即 CPU 或 GPU)。此外,示例展示了如何使用 PyTorch 创建具有与其他张量相同属性但使用不同数据初始化的张量。带有_like后缀的函数,如torch.empty_like()和torch.ones_like(),返回具有与另一个张量相同大小、数据类型和设备的张量,但初始化方式不同(参见“从随机样本创建张量”)。
注意
一些旧函数,如from_numpy()和as_tensor(),在实践中已被torch.tensor()构造函数取代,该构造函数可用于处理所有情况。
表 2-1 列出了用于创建张量的 PyTorch 函数。您应该使用torch命名空间下的每个函数,例如torch.empty()。您可以通过访问PyTorch 张量文档获取更多详细信息。
表 2-1. 张量创建函数
| 函数 | 描述 |
|---|---|
torch.**tensor**(*data, dtype=None, device=None, requires_grad=False, pin_memory=False*) |
从现有数据结构创建张量 |
torch.**empty**(**size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
根据内存中值的随机状态创建未初始化元素的张量 |
torch.**zeros**(**size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
创建一个所有元素初始化为 0.0 的张量 |
torch.**ones**(**size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
创建一个所有元素初始化为 1.0 的张量 |
torch.**arange**(*start=0, end, step=1, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
使用公共步长值在范围内创建值的一维张量 |
torch.**linspace**(*start, end, steps=100, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
在start和end之间创建线性间隔点的一维张量 |
torch.**logspace**(*start, end, steps=100, base=10.0, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
在start和end之间创建对数间隔点的一维张量 |
torch.**eye**(*n, m=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
创建一个对角线为 1,其他位置为 0 的二维张量 |
torch.**full**(*size, fill_value, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
创建一个填充了fill_value的张量 |
torch.**load**(*f*) |
从序列化的 pickle 文件中加载张量 |
torch.**save**(*f*) |
将张量保存到序列化的 pickle 文件中 |
PyTorch 文档包含了创建张量的完整函数列表,以及如何使用它们的更详细解释。在创建张量时,请记住一些常见的陷阱和额外的见解:
-
大多数创建函数都接受可选的
dtype和device参数,因此您可以在创建时设置这些参数。 -
使用
torch.arange()而不是已弃用的torch.range()函数。当步长已知时,请使用torch.arange()。当元素数量已知时,请使用torch.linspace()。 -
您可以使用
torch.tensor()从类似数组的结构(如列表、NumPy 数组、元组和集合)创建张量。要将现有张量转换为 NumPy 数组和列表,分别使用torch.numpy()和torch.tolist()函数。
张量属性
PyTorch 受欢迎的一个特点是它非常符合 Python 风格且面向对象。由于张量是自己的数据类型,因此可以读取张量对象本身的属性。现在您可以创建张量了,通过访问它们的属性,可以快速查找有关它们的信息是很有用的。假设x是一个张量,您可以按如下方式访问x的几个属性:
x.dtype
指示张量的数据类型(请参见表 2-2 列出的 PyTorch 数据类型列表)
x.device
指示张量的设备位置(例如,CPU 或 GPU 内存)
x.shape
显示张量的维度
x.ndim
标识张量的维数或秩
x.requires_grad
一个布尔属性,指示张量是否跟踪图计算(参见“自动微分(Autograd)”)
x.grad
如果requires_grad为True,则包含实际的梯度
x.grad_fn
如果requires_grad为True,则存储使用的图计算函数
x.s_cuda,x.is_sparse,x.is_quantized,x.is_leaf,x.is_mkldnn
指示张量是否满足某些条件的布尔属性
x.layout
指示张量在内存中的布局方式
请记但,当访问对象属性时,不要像调用类方法那样包括括号(())(例如,使用x.shape,而不是x.shape())。
数据类型
在深度学习开发中,了解数据及其计算所使用的数据类型非常重要。因此,在创建张量时,应该控制所使用的数据类型。如前所述,所有张量元素具有相同的数据类型。您可以在创建张量时使用dtype参数指定数据类型,或者可以使用适当的转换方法或to()方法将张量转换为新的dtype,如下面的代码所示:
# Specify the data type at creation using dtype
w = torch.tensor([1,2,3], dtype=torch.float32)
# Use the casting method to cast to a new data type
w.int() # w remains a float32 after the cast
w = w.int() # w changes to an int32 after the cast
# Use the to() method to cast to a new type
w = w.to(torch.float64) # ①
w = w.to(dtype=torch.float64) # ②
# Python automatically converts data types during
# operations
x = torch.tensor([1,2,3], dtype=torch.int32)
y = torch.tensor([1,2,3], dtype=torch.float32)
z = x + y # ③
print(z.dtype)
# out: torch.float32
①
传入数据类型。
②
直接使用dtype定义数据类型。
③
Python 会自动将x转换为float32,并将z返回为float32。
请注意,转换和to()方法不会改变张量的数据类型,除非重新分配张量。此外,在执行混合数据类型的操作时,PyTorch 会自动将张量转换为适当的类型。
大多数张量创建函数允许您在创建时使用dtype参数指定数据类型。在设置dtype或转换张量时,请记住使用torch命名空间(例如,使用torch.int64,而不仅仅是int64)。
表 2-2 列出了 PyTorch 中所有可用的数据类型。每种数据类型都会导致不同的张量类,具体取决于张量的设备。相应的张量类分别显示在 CPU 和 GPU 的最右两列中。
表 2-2. 张量数据类型
| 数据类型 | dtype | CPU 张量 | GPU 张量 |
|---|---|---|---|
| 32 位浮点数(默认) | torch.float32或torch.float |
torch.FloatTensor |
torch.cuda.FloatTensor |
| 64 位浮点数 | torch.float64或torch.double |
torch.DoubleTensor |
torch.cuda.DoubleTensor |
| 16 位浮点数 | torch.float16或torch.half |
torch.HalfTensor |
torch.cuda.HalfTensor |
| 8 位整数(无符号) | torch.uint8 |
torch.ByteTensor |
torch.cuda.ByteTensor |
| 8 位整数(有符号) | torch.int8 |
torch.CharTensor |
torch.cuda.CharTensor |
| 16 位整数(有符号) | torch.int16或torch.short |
torch.ShortTensor |
torch.cuda.ShortTensor |
| 32 位整数(有符号) | torch.int32或torch.int |
torch.IntTensor |
torch.cuda.IntTensor |
| 64 位整数(有符号) | torch.int64或torch.long |
torch.LongTensor |
torch.cuda.LongTensor |
| 布尔值 | torch.bool |
torch.BoolTensor |
torch.cuda.BoolTensor |
注意
为了减少空间复杂度,有时您可能希望重用内存并使用就地操作覆盖张量值。要执行就地操作,请在函数名称后附加下划线(_)后缀。例如,函数y.add_(x)将x添加到y,但结果将存储在y中。
从随机样本创建张量
在深度学习开发过程中经常需要创建随机数据。有时您需要将权重初始化为随机值或创建具有指定分布的随机输入。PyTorch 支持一组非常强大的函数,您可以使用这些函数从随机数据创建张量。
与其他创建函数一样,您可以在创建张量时指定 dtype 和 device。表 2-3 列出了一些随机抽样函数的示例。
表 2-3. 随机抽样函数
| 函数 | 描述 |
|---|---|
torch.rand(**size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
从区间[0 到 1]上的均匀分布中选择随机值 |
torch.randn(**size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
从均值为零方差为单位的标准正态分布中选择随机值 |
torch.normal(*mean, std, *, generator=None, out=None*) |
从具有指定均值和方差的正态分布中选择随机数 |
torch.randint(*low=0, high, size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False*) |
在指定的低值和高值之间生成均匀分布的随机整数 |
torch.randperm(*n, out=None, dtype=torch.int64, layout=torch.strided, device=None, requires_grad=False*) |
创建从 0 到n-1 的整数的随机排列 |
torch.bernoulli(*input, *, generator=None, out=None*) |
从伯努利分布中绘制二进制随机数(0 或 1) |
torch.multinomial(*input, num_samples, replacement=False, *, generator=None, out=None*) |
根据多项分布中的权重从列表中选择一个随机数 |
提示
您还可以创建从更高级分布(如柯西分布、指数分布、几何分布和对数正态分布)中抽样的值张量。为此,使用torch.empty()创建张量,并对分布(例如柯西分布)应用就地函数。请记住,就地方法使用下划线后缀。例如,x = torch.empty([10,5]).cauchy_()创建一个从柯西分布中抽取的随机数张量。
像其他张量一样创建张量
您可能希望创建并初始化一个具有与另一个张量相似属性的张量,包括dtype、device和layout属性,以便进行计算。许多张量创建操作都有一个相似性函数,允许您轻松地执行此操作。相似性函数将具有后缀_like。例如,torch.empty_like(tensor_a)将创建一个具有tensor_a的dtype、device和layout属性的空张量。一些相似性函数的示例包括empty_like()、zeros_like()、ones_like()、full_like()、rand_like()、randn_like()和rand_int_like()。
张量操作
现在您了解如何创建张量,让我们探索您可以对其执行的操作。PyTorch 支持一组强大的张量操作,允许您访问和转换张量数据。
首先我将描述如何访问数据的部分,操作它们的元素,并组合张量以形成新的张量。然后我将向您展示如何执行简单的计算以及高级的数学计算,通常在恒定时间内。PyTorch 提供了许多内置函数。在创建自己的函数之前检查可用的函数是很有用的。
索引、切片、组合和拆分张量
创建张量后,您可能希望访问数据的部分并组合或拆分张量以形成新张量。以下代码演示了如何执行这些类型的操作。您可以像切片和索引 NumPy 数组一样切片和索引张量,如以下代码的前几行所示。请注意,即使数组只有一个元素,索引和切片也会返回张量。在传递给print()等其他函数时,您需要使用item()函数将单个元素张量转换为 Python 值:
x = torch.tensor([[1,2],[3,4],[5,6],[7,8]])
print(x)
# out:
# tensor([[1, 2],
# [3, 4],
# [5, 6],
# [7, 8]])
# Indexing, returns a tensor
print(x[1,1])
# out: tensor(4)
# Indexing, returns a value as a Python number
print(x[1,1].item())
# out: 4
在下面的代码中,我们可以看到我们可以使用与用于切片 Python 列表和 NumPy 数组相同的[*start*:*end*:*step*]格式执行切片。我们还可以使用布尔索引来提取满足某些条件的数据部分,如下所示:
# Slicing
print(x[:2,1])
# out: tensor([2, 4])
# Boolean indexing
# Only keep elements less than 5
print(x[x<5])
# out: tensor([1, 2, 3, 4])
PyTorch 还支持转置和重塑数组,如下面的代码所示:
# Transpose array; x.t() or x.T can be used
print(x.t())
# tensor([[1, 3, 5, 7],
# [2, 4, 6, 8]])
# Change shape; usually view() is preferred over
# reshape()
print(x.view((2,4)))
# tensor([[1, 3, 5, 7],
# [2, 4, 6, 8]])
您还可以使用torch.stack()和torch.unbind()等函数组合或拆分张量,如下面的代码所示:
# Combining tensors
y = torch.stack((x, x))
print(y)
# out:
# tensor([[[1, 2],
# [3, 4],
# [5, 6],
# [7, 8]],
# [[1, 2],
# [3, 4],
# [5, 6],
# [7, 8]]])
# Splitting tensors
a,b = x.unbind(dim=1)
print(a,b)
# out:
# tensor([1, 3, 5, 7]); tensor([2, 4, 6, 8])
PyTorch 提供了一组强大的内置函数,可用于以不同方式访问、拆分和组合张量。表 2-4 列出了一些常用的用于操作张量元素的函数。
表 2-4。索引、切片、组合和拆分操作
| 函数 | 描述 |
|---|---|
torch.cat() |
在给定维度中连接给定序列的张量。 |
torch.chunk() |
将张量分成特定数量的块。每个块都是输入张量的视图。 |
torch.gather() |
沿着由维度指定的轴收集值。 |
torch.index_select() |
使用索引中的条目沿着维度索引输入张量的新张量,索引是LongTensor。 |
torch.masked_select() |
根据布尔掩码(BoolTensor)索引输入张量的新 1D 张量。 |
torch.narrow() |
返回输入张量的窄版本的张量。 |
torch.nonzero() |
返回非零元素的索引。 |
torch.reshape() |
返回一个与输入张量具有相同数据和元素数量但形状不同的张量。使用view()而不是确保张量不被复制。 |
torch.split() |
将张量分成块。每个块都是原始张量的视图或子分区。 |
torch.squeeze() |
返回一个去除输入张量所有尺寸为 1 的维度的张量。 |
torch.stack() |
沿新维度连接一系列张量。 |
torch.t() |
期望输入为 2D 张量并转置维度 0 和 1。 |
torch.take() |
在切片不连续时返回指定索引处的张量。 |
torch.transpose() |
仅转置指定的维度。 |
torch.unbind() |
通过返回已删除维度的元组来移除张量维度。 |
torch.unsqueeze() |
返回一个在指定位置插入大小为 1 的维度的新张量。 |
torch.where() |
根据指定条件从两个张量中的一个返回所选元素的张量。 |
其中一些函数可能看起来多余。然而,重要的是要记住以下关键区别和最佳实践:
-
item()是一个重要且常用的函数,用于从包含单个值的张量返回 Python 数字。 -
在大多数情况下,用
view()代替reshape()来重新塑造张量。使用reshape()可能会导致张量被复制,这取决于其在内存中的布局。view()确保不会被复制。 -
使用
x.T或x.t()是转置 1D 或 2D 张量的简单方法。处理多维张量时,请使用transpose()。 -
torch.squeeze()函数在深度学习中经常用于去除未使用的维度。例如,使用squeeze()可以将包含单个图像的图像批次从 4D 减少到 3D。 -
torch.unsqueeze()函数在深度学习中经常用于添加大小为 1 的维度。由于大多数 PyTorch 模型期望批量数据作为输入,当您只有一个数据样本时,可以应用unsqueeze()。例如,您可以将一个 3D 图像传递给torch.unsqueeze()以创建一个图像批次。
注意
PyTorch 在本质上非常符合 Python 的特性。与大多数 Python 类一样,一些 PyTorch 函数可以直接在张量上使用内置方法,例如x.size()。
其他函数直接使用torch命名空间调用。这些函数以张量作为输入,就像在torch.save(x, 'tensor.pt')中的x一样。
数学张量操作
深度学习开发在很大程度上基于数学计算,因此 PyTorch 支持非常强大的内置数学函数集。无论您是创建新的数据转换、自定义损失函数还是构建自己的优化算法,您都可以通过 PyTorch 提供的数学函数加快研究和开发速度。
本节的目的是快速概述 PyTorch 中许多可用的数学函数,以便您可以快速了解当前存在的内容,并在需要时找到适当的函数。
PyTorch 支持许多不同类型的数学函数,包括逐点操作、缩减函数、比较计算以及线性代数操作,以及频谱和其他数学计算。我们将首先看一下有用的数学操作的第一类是逐点操作。逐点操作在张量中的每个点上执行操作,并返回一个新的张量。
它们对于舍入和截断以及三角和逻辑操作非常有用。默认情况下,这些函数将创建一个新的张量或使用由out参数传递的张量。如果要执行原地操作,请记得在函数名称后附加下划线。
表 2-5 列出了一些常用的逐点操作。
表 2-5. 逐点操作
| 操作类型 | 示例函数 |
|---|---|
| 基本数学 | add(), div(), mul(), neg(), reciprocal(), true_divide() |
| 截断 | ceil(), clamp(), floor(), floor_divide(), fmod(), frac(), lerp(), remainder(), round(), sigmoid(), trunc() |
| 复数 | abs(), angle(), conj(), imag(), real() |
| 三角函数 | acos(), asin(), atan(), cos(), cosh(), deg2rad(), rad2deg(), sin(), sinh(), tan(), tanh() |
| 指数和对数 | exp(), expm1(), log(), log10(), log1p(), log2(), logaddexp(), pow(), rsqrt(), sqrt(), square() |
| 逻辑 | logical_and(), logical_not(), logical_or(), logical_xor() |
| 累积数学 | addcdiv(), addcmul() |
| 位运算符 | bitwise_not(), bitwise_and(), bitwise_or(), bitwise_xor() |
| 错误函数 | erf(), erfc(), erfinv() |
| 伽玛函数 | digamma(), lgamma(), mvlgamma(), polygamma() |
使用 Python 提示或参考 PyTorch 文档以获取有关函数使用的详细信息。请注意,true_divide()首先将张量数据转换为浮点数,应在将整数除以以获得真实除法结果时使用。
注意
大多数张量操作可以使用三种不同的语法。张量支持运算符重载,因此您可以直接使用运算符,例如z = x + y。虽然您也可以使用 PyTorch 函数如torch.add()来执行相同的操作,但这较少见。最后,您可以使用下划线(_)后缀执行原地操作。函数y.add_(x)可以实现相同的结果,但它们将存储在y中。
第二类数学函数是 缩减操作。 缩减操作将一堆数字减少到一个数字或一组较小的数字。 也就是说,它们减少了张量的 维度 或 秩。 缩减操作包括查找最大值或最小值以及许多统计计算的函数,例如查找平均值或标准差。
这些操作在深度学习中经常使用。 例如,深度学习分类通常使用 argmax() 函数将 softmax 输出缩减为主导类。
表 2-6 列出了一些常用的缩减操作。
表 2-6. 缩减操作
| 函数 | 描述 |
|---|---|
torch.argmax(input, dim, keepdim=False, out=None) |
返回所有元素中最大值的索引,或者如果指定了维度,则只返回一个维度上的索引 |
torch.argmin(input, dim, keepdim=False, out=None) |
返回所有元素中最小值的索引,或者如果指定了维度,则只返回一个维度上的索引 |
torch.dist(input, dim, keepdim=False, out=None) |
计算两个张量的 p-范数 |
torch.logsumexp(input, dim, keepdim=False, out=None) |
计算给定维度中输入张量的每行的指数和的对数 |
torch.mean(input, dim, keepdim=False, out=None) |
计算所有元素的平均值,或者如果指定了维度,则只计算一个维度上的平均值 |
torch.median(input, dim, keepdim=False, out=None) |
计算所有元素的中位数或中间值,或者如果指定了维度,则只计算一个维度上的中位数 |
torch.mode(input, dim, keepdim=False, out=None) |
计算所有元素的众数或最频繁出现的值,或者如果指定了维度,则只计算一个维度上的值 |
torch.norm(input, p='fro', dim=None, keepdim=False, out=None, dtype=None) |
计算所有元素的矩阵或向量范数,或者如果指定了维度,则只计算一个维度上的范数 |
torch.prod(input, dim, keepdim=False, dtype=None) |
计算所有元素的乘积,或者如果指定了维度,则只计算输入张量的每行的乘积 |
torch.std(input, dim, keepdim=False, out=None) |
计算所有元素的标准差,或者如果指定了维度,则只计算一个维度上的标准差 |
torch.std_mean(input, unbiased=True) |
计算所有元素的标准差和平均值,或者如果指定了维度,则只计算一个维度上的标准差和平均值 |
torch.sum(input, dim, keepdim=False, out=None) |
计算所有元素的和,或者如果指定了维度,则只计算一个维度上的和 |
torch.unique(input, dim, keepdim=False, out=None) |
在整个张量中删除重复项,或者如果指定了维度,则只删除一个维度上的重复项 |
torch.unique_consecutive(input, dim, keepdim=False, out=None) |
类似于 torch.unique(),但仅删除连续的重复项 |
torch.var(input, dim, keepdim=False, out=None) |
计算所有元素的方差,或者如果指定了维度,则只计算一个维度上的方差 |
torch.var_mean(input, dim, keepdim=False, out=None) |
计算所有元素的平均值和方差,或者如果指定了维度,则只计算一个维度上的平均值和方差 |
请注意,许多这些函数接受 dim 参数,该参数指定多维张量的缩减维度。 这类似于 NumPy 中的 axis 参数。 默认情况下,当未指定 dim 时,缩减会跨所有维度进行。 指定 dim = 1 将在每行上计算操作。 例如,torch.mean(x,1) 将计算张量 x 中每行的平均值。
提示
将方法链接在一起是常见的。 例如,torch.rand(2,2).max().item() 创建一个 2 × 2 的随机浮点数张量,找到最大值,并从结果张量中返回值本身。
接下来,我们将看一下 PyTorch 的比较函数。比较函数通常比较张量中的所有值,或将一个张量的值与另一个张量的值进行比较。它们可以根据每个元素的值返回一个充满布尔值的张量,例如torch.eq()或torch.is_boolean()。还有一些函数可以找到最大或最小值,对张量值进行排序,返回张量元素的顶部子集等。
| torch.svd() | 执行奇异值分解 |
表 2-7. 比较操作
| 操作类型 | 示例函数 |
|---|---|
| 将张量与其他张量进行比较 | eq(), ge(), gt(), le(), lt(), ne() 或 ==, >, >=, <, <=, !=, 分别 |
| 测试张量状态或条件 | isclose(), isfinite(), isinf(), isnan() |
| 返回整个张量的单个布尔值 | allclose(), equal() |
| 查找整个张量或沿给定维度的值 | argsort(), kthvalue(), max(), min(), sort(), topk() |
比较函数似乎很简单;然而,有一些关键点需要记住。常见的陷阱包括以下内容:
-
torch.eq()函数或==返回一个相同大小的张量,每个元素都有一个布尔结果。torch.equal()函数测试张量是否具有相同的大小,如果张量中的所有元素都相等,则返回一个单个布尔值。 -
函数
torch.allclose()也会返回一个单个布尔值,如果所有元素都接近指定值。
接下来我们将看一下线性代数函数。线性代数函数促进矩阵运算,对于深度学习计算非常重要。
许多计算,包括梯度下降和优化算法,使用线性代数来实现它们的计算。PyTorch 支持一组强大的内置线性代数操作,其中许多基于基本线性代数子程序(BLAS)和线性代数包(LAPACK)标准化库。
表 2-8 列出了一些常用的线性代数操作。
表 2-8. 线性代数操作
| 函数 | 描述 |
|---|---|
torch.matmul() |
计算两个张量的矩阵乘积;支持广播 |
torch.chain_matmul() |
计算N个张量的矩阵乘积 |
torch.mm() |
计算两个张量的矩阵乘积(如果需要广播,请使用matmul()) |
torch.addmm() |
计算两个张量的矩阵乘积并将其添加到输入中 |
torch.bmm() |
计算一批矩阵乘积 |
torch.addbmm() |
计算一批矩阵乘积并将其添加到输入中 |
torch.baddbmm() |
计算一批矩阵乘积并将其添加到输入批次 |
torch.mv() |
计算矩阵和向量的乘积 |
torch.addmv() |
计算矩阵和向量的乘积并将其添加到输入中 |
torch.matrix_power |
返回张量的n次幂(对于方阵) |
torch.eig() |
找到实方阵的特征值和特征向量 |
torch.inverse() |
计算方阵的逆 |
torch.det() |
计算矩阵或一批矩阵的行列式 |
torch.logdet() |
计算矩阵或一批矩阵的对数行列式 |
torch.dot() |
计算两个张量的内积 |
torch.addr() |
计算两个张量的外积并将其添加到输入中 |
torch.solve() |
返回线性方程组的解 |
| 表 2-7 列出了一些常用的比较函数供参考。 | |
torch.pca_lowrank() |
执行线性主成分分析 |
torch.cholesky() |
计算 Cholesky 分解 |
torch.cholesky_inverse() |
计算对称正定矩阵的逆并返回 Cholesky 因子 |
torch.cholesky_solve() |
使用 Cholesky 因子解线性方程组 |
表 2-8 中的函数范围从矩阵乘法和批量计算函数到求解器。重要的是要指出,矩阵乘法与torch.mul()或*运算符的逐点乘法不同。
本书不涵盖完整的线性代数研究,但在进行特征降维或开发自定义深度学习算法时,您可能会发现访问一些线性代数函数很有用。请参阅PyTorch 线性代数文档以获取可用函数的完整列表以及如何使用它们的更多详细信息。
我们将考虑的最后一类数学运算是光谱和其他数学运算。根据感兴趣的领域,这些函数可能对数据转换或分析有用。例如,光谱运算如快速傅里叶变换(FFT)在计算机视觉或数字信号处理应用中可能起重要作用。
表 2-9 列出了一些用于频谱分析和其他数学运算的内置操作。
表 2-9. 光谱和其他数学运算
| 操作类型 | 示例函数 |
|---|---|
| 快速、逆、短时傅里叶变换 | fft(), ifft(), stft() |
| 实到复 FFT 和复到实逆 FFT(IFFT) | rfft(), irfft() |
| 窗口算法 | bartlett_window(), blackman_window(), hamming_window(), hann_window() |
| 直方图和箱计数 | histc(), bincount() |
| 累积操作 | cummax(), cummin(), cumprod(), cumsum(), trace()(对角线之和),einsum()(使用爱因斯坦求和的乘积之和) |
| 标准化函数 | cdist(), renorm() |
| 叉积、点积和笛卡尔积 | cross(), tensordot(), cartesian_prod() |
| 创建对角张量的函数,其元素为输入张量的元素 | diag(), diag_embed(), diag_flat(), diagonal() |
| 爱因斯坦求和 | einsum() |
| 矩阵降维和重构函数 | flatten(), flip(), rot90(), repeat_interleave(), meshgrid(), roll(), combinations() |
| 返回下三角形或上三角形及其索引的函数 | tril(), tril_indices, triu(), triu_indices() |
自动微分(Autograd)
一个函数,backward(),值得在自己的子节中调用,因为它是 PyTorch 在深度学习开发中如此强大的原因。backward()函数使用 PyTorch 的自动微分包torch.autograd,根据链式法则对张量进行微分和计算梯度。
这是自动微分的一个简单示例。我们定义一个函数,f = sum(x²),其中 x 是一个变量矩阵。如果我们想要找到矩阵中每个变量的 df / dx,我们需要为张量x设置requires_grad = True标志,如下面的代码所示:
x = torch.tensor([[1,2,3],[4,5,6]],
dtype=torch.float, requires_grad=True)
print(x)
# out:
# tensor([[1., 2., 3.],
# [4., 5., 6.]], requires_grad=True)
f = x.pow(2).sum()
print(f)
# tensor(91., grad_fn=<SumBackward0>)
f.backward()
print(x.grad) # df/dx = 2x
# tensor([[ 2., 4., 6.],
# [ 8., 10., 12.]])
f.backward()函数对f进行微分,并将df / dx存储在x.grad属性中。对微积分微分方程的快速回顾将告诉我们f相对于x的导数,df / dx = 2x。对x的值评估df / dx的结果显示为输出。
注意
只有浮点dtype的张量可以需要梯度。
训练神经网络需要我们在反向传播中计算权重梯度。随着我们的神经网络变得更深更复杂,这个功能可以自动化复杂的计算。有关 autograd 工作原理的更多信息,请参阅Autograd 教程。
本章提供了一个快速参考,用于创建张量和执行操作。现在您已经对张量有了良好的基础,我们将重点讨论如何使用张量和 PyTorch 来进行深度学习研究。在下一章中,我们将回顾深度学习开发过程,然后开始编写代码。
第三章:使用 PyTorch 进行深度学习开发
现在您的开发环境已经运行,并且对张量及其操作有了很好的理解,我们可以开始使用 PyTorch 开发和部署深度学习模型。本章提供了基本 NN 开发过程和执行所需的 PyTorch 代码的快速参考。
首先我们将回顾整体过程,然后深入每个阶段,查看一些实现每个功能的示例 PyTorch 代码。我们将在第二章学到的基础上,将数据加载到张量中,并应用数据转换,将张量转换为模型的合适输入。
您将构建一个深度学习模型,并使用常见的训练循环结构对模型进行训练。然后,您将测试模型的性能,并调整超参数以改善结果和训练速度。最后,我们将探讨将模型部署到原型系统或生产环境的方法。在每个阶段,我将提供常用的 PyTorch 代码作为您开发自己的深度学习模型的参考。
本书的未来章节将提供更多示例,并涵盖更高级的主题,如定制、优化、加速、分布式训练和高级部署。现在,我们将专注于基本 NN 开发过程。
整体过程
尽管每个人构建深度学习模型的方式都不同,但整个过程基本上是相同的。无论您是使用带标签数据进行监督学习,使用无标签数据进行无监督学习,还是使用两者混合的半监督学习,都会使用基本的流程来训练、测试和部署您的深度学习模型。我假设您对深度学习模型开发有一定了解,但在开始之前,让我们回顾一下基本的深度学习训练过程。然后我将展示如何在 PyTorch 中实现这个过程。
图 3-1 展示了深度学习开发中最常见的任务。第一阶段是数据准备阶段,在这个阶段,我们将从外部来源加载数据,并将其转换为适合模型训练的格式。这些数据可以是图像、视频、语音录音、音频文件、文本、一般的表格数据,或者它们的任意组合。
首先,我们加载这些数据,并将其转换为张量形式的数值。这些张量将在模型训练阶段作为输入;然而,在传入之前,这些张量通常会通过转换进行预处理,并分组成批次以提高训练性能。因此,数据准备阶段将通用数据转换为可以传入 NN 模型的张量批次。
接下来,在模型实验和开发阶段,我们将设计一个 NN 模型,使用训练数据训练模型,测试其性能,并优化我们的超参数以提高性能到期望水平。为此,我们将将数据集分为三部分:一部分用于训练,一部分用于验证,一部分用于测试。我们将设计一个 NN 模型,并使用训练数据训练其参数。PyTorch 在torch.nn模块中提供了优雅设计的模块和类,帮助您创建和训练您的 NN。我们将从众多内置的 PyTorch 函数中定义损失函数和优化器。然后,我们将执行反向传播,并在训练循环中更新模型参数。
图 3-1. 基本深度学习开发过程
在每个 epoch 内,我们还将通过传入验证数据来验证我们的模型,衡量性能,并可能调整超参数。最后,我们将通过传入测试数据来测试我们的模型,并根据未知数据的性能来衡量模型的表现。在实践中,验证和测试循环可能是可选的,但我们在这里展示它们以确保完整性。
深度学习模型开发的最后阶段是模型部署阶段。在这个阶段,我们有一个完全训练好的模型——那么我们该怎么办呢?如果您是进行实验的深度学习研究科学家,您可能只想将模型保存到文件中,以便进一步研究和实验,或者您可能希望通过 PyTorch Hub 等存储库提供对其的访问。您还可以将其部署到边缘设备或本地服务器,以演示原型或概念验证。
另一方面,如果您是软件开发人员或系统工程师,您可能希望将模型部署到产品或服务中。在这种情况下,您可以将模型部署到云服务器上的生产环境,或将其部署到边缘设备或手机上。在部署经过训练的模型时,模型通常需要额外的后处理。例如,您可能要对一批图像进行分类,但只想报告最有信心的结果。模型部署阶段还处理从模型的输出值到最终解决方案所需的任何后处理。
现在我们已经探讨了整个开发过程,让我们深入每个部分,展示 PyTorch 如何帮助您开发深度学习模型。
数据准备
深度学习开发的第一阶段始于数据准备。在这个阶段,我们获取数据来训练和测试我们的 NN 模型,并将其转换为数字张量,以便我们的 PyTorch 模型可以处理。数据集的大小和数据本身对于开发良好的模型很重要;然而,生成良好的数据集超出了本书的范围。
在本节中,我将假设您已经确定数据是好的,因此我将重点介绍如何使用 PyTorch 的内置功能加载数据、应用转换并对数据进行批处理。首先我将展示如何使用torchvision包准备图像数据,然后我们将探索 PyTorch 资源以准备其他类型的数据。
数据加载
PyTorch 提供了强大的内置类和实用程序,如Dataset、DataLoader和Sampler类,用于加载各种类型的数据。Dataset类定义了如何从文件或数据源访问和预处理数据。Sampler类定义了如何从数据集中采样数据以创建批次,而DataLoader类将数据集与采样器结合在一起,允许您迭代一组批次。
PyTorch 库如 Torchvision 和 Torchtext 还提供支持专门数据的类,如计算机视觉和自然语言数据。torchvision.datasets模块是如何利用内置类加载数据的一个很好的例子。torchvision.datasets模块提供了许多子类来从流行的学术数据集加载图像数据。
其中一个流行的数据集是 CIFAR-10。CIFAR-10 数据集是由 Alex Krizhevsky、Vinod Nair 和 Geoffrey Hinton 在为加拿大高级研究所(CIFAR)进行研究时收集的。它包含 50,000 个训练图像和 10,000 个测试图像,涵盖了 10 种可能的对象:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。以下代码展示了如何使用 CIFAR-10 创建一个训练数据集:
from torchvision.datasets import CIFAR10
train_data = CIFAR10(root="./train/",
train=True,
download=True)
train参数确定我们加载训练数据还是测试数据,将download设置为True将为我们下载数据(如果我们还没有)。
让我们探索train_data数据集对象。我们可以使用其方法和属性访问有关数据集的信息,如下面的代码所示:
print(train_data) # ①
# out:
# Dataset CIFAR10
# Number of datapoints: 50000
# Root location: ./train/
# Split: Train
print(len(train_data)) # ②
# out: 50000
print(train_data.data.shape) # ndarray # ③
# out: (50000, 32, 32, 3)
print(train_data.targets) # list # ④
# out: [6, 9, ..., 1, 1]
print(train_data.classes) # ⑤
# out: ['airplane', 'automobile', 'bird',
# 'cat', 'deer', 'dog', 'frog',
# 'horse', 'ship', 'truck']
print(train_data.class_to_idx) # ⑥
# out:
# {'airplane': 0, 'automobile': 1, 'bird': 2,
# 'cat': 3, 'deer': 4, 'dog': 5, 'frog': 6,
# 'horse': 7, 'ship': 8, 'truck': 9}
①
打印对象会返回其一般信息。
②
使用len()检查数据样本的数量。
③
数据是一个包含 50,000 个 32×32 像素彩色图像的 NumPy 数组。
④
目标是一个包含 50,000 个数据标签的列表。
⑤
你可以使用classes将数值标签映射到类名。
⑥
你可以使用class_to_idx将类名映射到索引值。
让我们仔细看看train_data数据集的数据和标签。我们可以使用索引访问数据样本,如下面的代码所示:
print(type(train_data[0]))
# out: <class 'tuple'>
print(len(train_data[0]))
# out: 2
data, label = train_data[0]
如代码中所示,train_data[0]返回一个包含两个元素的元组——数据和标签。让我们先检查数据:
print(type(data))
# out: <class 'PIL.Image.Image'>
print(data)
# out:
# <PIL.Image.Image image mode=RGB
# size=32x32 at 0x7FA61-D6F1748>
数据由一个 PIL 图像对象组成。PIL 是一种常见的图像格式,使用 Pillow 库以高度×宽度×通道的格式存储图像像素值。彩色图像有三个通道(RGB)分别为红色、绿色和蓝色。了解数据格式很重要,因为如果模型期望不同的格式,我们可能需要转换这种格式(稍后会详细介绍)。
图 3-2 显示了 PIL 图像。由于分辨率只有 32×32,所以有点模糊,但你能猜出是什么吗?

图 3-2. 示例图像
让我们检查标签:
print(type(label))
# out: <class 'int'>
print(label)
# out: 6
print(train_data.classes[label])
# out: frog
在代码中,label是一个表示图像类别的整数值(例如,飞机、狗等)。我们可以使用classes属性查看索引 6 对应于青蛙。
我们还可以将测试数据加载到另一个名为test_data的数据集对象中。更改根文件夹并将train标志设置为False即可,如下面的代码所示:
test_data = CIFAR10(root="./test/",
train=False,
download=True)
print(test_data)
# out:
# Dataset CIFAR10
# Number of datapoints: 10000
# Root location: ./test/
# Split: Test
print(len(test_data))
# out: 10000
print(test_data.data.shape) # ndarray
# out: (10000, 32, 32, 3)
test_data数据集与train_data数据集类似。但是测试数据集中只有 10,000 张图像。尝试访问数据集类的一些方法和test_data数据集上的属性。
数据转换
在数据加载步骤中,我们从数据源中提取数据并创建包含有关数据集和数据本身信息的数据集对象。但是,在将数据传递到 NN 模型进行训练和测试之前,数据可能需要进行调整。例如,数据值可能需要归一化以帮助训练,进行增强以创建更大的数据集,或者从一种对象类型转换为张量。
这些调整是通过应用transforms来完成的。在 PyTorch 中使用 transforms 的美妙之处在于你可以定义一系列 transforms 并在访问数据时应用它。稍后在第五章中,你将看到如何在 CPU 上并行应用 transforms,同时在 GPU 上进行训练。
在下面的代码示例中,我们将定义我们的 transforms 并使用这些 transforms 创建我们的train_data数据集:
from torchvision import transforms
train_transforms = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(
mean=(0.4914, 0.4822, 0.4465), # ①
std=(0.2023, 0.1994, 0.2010))])
train_data = CIFAR10(root="./train/",
train=True,
download=True,
transform=train_transforms) # ②
①
这里的均值和标准差值是根据数据集本身预先确定的。
②
创建数据集时设置transform参数。
我们使用transforms.Compose()类定义一组 transforms。这个类接受一个 transforms 列表并按顺序应用它们。这里我们随机裁剪和翻转图像,将它们转换为张量,并将张量值归一化为预定的均值和标准差。
transforms 在实例化数据集类时传递,并成为数据集对象的一部分。每当访问数据集对象时都会应用 transforms,返回一个由转换后的数据组成的新结果。
我们可以通过打印数据集或其transforms属性来查看 transforms,如下面的代码所示:
print(train_data)
# out:
# Dataset CIFAR10
# Number of datapoints: 50000
# Root location: ./train/
# Split: Train
# StandardTransform
# Transform: Compose(
# RandomCrop(size=(32, 32),
# padding=4)
# RandomHorizontalFlip(p=0.5)
# ToTensor()
# Normalize(
# mean=(0.4914, 0.4822, 0.4465),
# std=(0.2023, 0.1994, 0.201))
# )
print(train_data.transforms)
# out:
# StandardTransform
# Transform: Compose(
# RandomCrop(size=(32, 32),
# padding=4)
# RandomHorizontalFlip(p=0.5)
# ToTensor()
# Normalize(
# mean=(0.4914, 0.4822, 0.4465),
# std=(0.2023, 0.1994, 0.201))
我们可以使用索引访问数据,如下一个代码块所示。PyTorch 在访问数据时会自动应用 transforms,因此输出数据将与之前看到的不同:
data, label = train_data[0]
print(type(data))
# out: <class 'torch.Tensor'>
print(data.size())
# out: torch.Size([3, 32, 32])
print(data)
# out:
# tensor([[[-0.1416, ..., -2.4291],
# [-0.0060, ..., -2.4291],
# [-0.7426, ..., -2.4291],
# ...,
# [ 0.5100, ..., -2.2214],
# [-2.2214, ..., -2.2214],
# [-2.2214, ..., -2.2214]]])
如你所见,数据输出现在是一个大小为 3×32×32 的张量。它也已经被随机裁剪、水平翻转和归一化。图 3-3 显示了应用 transforms 后的图像。

图 3-3。变换后的图像
颜色可能看起来奇怪是因为归一化,但这实际上有助于神经网络模型更好地对图像进行分类。
我们可以为测试定义不同的变换集,并将其应用于我们的测试数据。在测试数据的情况下,我们不希望裁剪或翻转图像,但我们确实需要将图像转换为张量并对张量值进行归一化,如下所示:
test_transforms = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
(0.4914, 0.4822, 0.4465),
(0.2023, 0.1994, 0.2010))])
test_data = torchvision.datasets.CIFAR10(
root="./test/",
train=False,
transform=test_transforms)
print(test_data)
# out:
# Dataset CIFAR10
# Number of datapoints: 10000
# Root location: ./test/
# Split: Test
# StandardTransform
# Transform: Compose(
# ToTensor()
# Normalize(
# mean=(0.4914, 0.4822, 0.4465),
# std=(0.2023, 0.1994, 0.201)))
数据批处理
现在我们已经定义了变换并创建了数据集,我们可以逐个访问数据样本。然而,当训练模型时,您将希望在每次迭代中传递小批量的数据,正如我们将在“模型开发”中看到的。将数据分批不仅可以实现更高效的训练,还可以利用 GPU 的并行性加速训练。
批处理可以很容易地使用torch.utils.data.DataLoader类实现。让我们从 Torchvision 如何使用这个类的示例开始,然后我们将更详细地介绍它。
在下面的代码中,我们为train_data创建一个数据加载器,可以用来加载一批样本并应用我们的变换:
trainloader = torch.utils.data.DataLoader(
train_data,
batch_size=16,
shuffle=True)
我们使用批量大小为 16 个样本,并对数据集进行洗牌,以便数据加载器检索数据的随机抽样。
数据加载器对象结合了数据集和采样器,并为给定数据集提供了一个可迭代的对象。换句话说,您的训练循环可以使用此对象对数据集进行抽样,并一次一个批次地应用变换,而不是一次性地对整个数据集应用变换。这在训练和测试模型时显著提高了效率和速度。
以下代码显示了如何从trainloader中检索一批样本:
data_batch, labels_batch = next(iter(trainloader))
print(data_batch.size())
# out: torch.Size([16, 3, 32, 32])
print(labels_batch.size())
# out: torch.Size([16])
我们需要使用iter()将trainloader转换为迭代器,然后使用next()再次迭代数据。这仅在访问一个批次时才是必要的。正如我们将在后面看到的,我们的训练循环将直接访问数据加载器,而无需使用iter()和next()。检查数据和标签的大小后,我们看到它们返回大小为 16 的批次。
我们可以为我们的test_data数据集创建一个数据加载器,如下所示:
testloader = torch.utils.data.DataLoader(
test_data,
batch_size=16,
shuffle=False)
在这里,我们将shuffle设置为False,因为通常不需要对测试数据进行洗牌,研究人员希望看到可重复的测试结果。
通用数据准备(torch.utils.data)
到目前为止,我已经向您展示了如何使用 Torchvision 加载、转换和批处理图像数据。然而,您也可以使用 PyTorch 准备其他类型的数据。PyTorch 库如 Torchtext 和 Torchaudio 为文本和音频数据提供了数据集和数据加载器类,新的外部库也在不断开发中。
PyTorch 还提供了一个名为torch.utils.data的子模块,您可以使用它来创建自己的数据集和数据加载器类,就像您在 Torchvision 中看到的那样。它包括Dataset、Sampler和DataLoader类。
数据集类
PyTorch 支持映射和可迭代样式的数据集类。映射样式数据集源自抽象类torch.utils.data.Dataset。它实现了getitem()和len()函数,并表示从(可能是非整数)索引/键到数据样本的映射。例如,当使用dataset[idx]访问这样的数据集时,可以从磁盘上的文件夹中读取第 idx 个图像及其对应的标签。映射样式数据集比可迭代样式数据集更常用,所有表示由键或数据样本制成的映射的数据集都应该使用这个子类。
提示
创建自己的数据集类的最简单方法是子类化映射样式的torch.utils.data.Dataset类,并使用自己的代码重写getitem()和len()函数。
所有子类都应该重写getitem(),它为给定键获取数据样本。子类也可以选择重写len(),它返回数据集的大小,由许多Sampler实现和DataLoader的默认选项使用。
另一方面,可迭代样式数据集派生自torch.utils.data.IterableDataset抽象类。它实现了iter()协议,并表示数据样本的可迭代。当从数据库或远程服务器读取数据以及实时生成数据时,通常使用这种类型的数据集。当随机读取昂贵或不确定时,以及批次大小取决于获取的数据时,可迭代数据集非常有用。
PyTorch 的torch.utils.data子模块还提供了数据集操作,用于转换、组合或拆分数据集对象。这些操作包括以下内容:
TensorDataset(*tensors*)
从张量创建数据集对象
ConcatDataset(*datasets*)
从多个数据集创建数据集
ChainDataset(*datasets*)
多个IterableDatasets链接
Subset(*dataset*, *indices*)
从指定索引创建数据集的子集
采样器类
除了数据集类,PyTorch 还提供了采样器类,它们提供了一种迭代数据集样本索引的方法。采样器派生自torch.utils.data.Sampler基类。
每个Sampler子类都需要实现一个iter()方法,以提供迭代数据元素索引的方法,以及一个返回迭代器长度的len()方法。表 3-1 提供了可用采样器的列表供参考。
表 3-1. 数据集采样器(torch.utils.data)
| Sampler | 描述 |
|---|---|
SequentialSampler(data_source) |
按顺序采样数据 |
RandomSampler(data_source, replacement=False, num_samples=None, generator=None) |
随机采样数据 |
SubsetRandomSampler(indices, generator=None) |
从数据集的子集中随机采样数据 |
WeightedRandomSampler(weights, num_samples, replacement=True, generator=None) |
从加权分布中随机采样 |
BatchSampler(sampler, batch_size, drop_last) |
返回一批样本 |
distributed.DistributedSampler(dataset, num_replicas=None, rank=None, shuffle=True, seed=0) |
在分布式数据集上采样 |
通常不直接使用采样器。它们通常传递给数据加载器,以定义数据加载器对数据集进行采样的方式。
DataLoader 类
Dataset类返回一个包含数据和数据信息的数据集对象。Sampler类以指定或随机的方式返回实际数据本身。DataLoader类将数据集与采样器结合起来,并返回一个可迭代对象。
数据集和采样器对象不是可迭代的,这意味着您不能在它们上运行for循环。数据加载器对象解决了这个问题。我们在本章前面的 CIFAR-10 示例中使用DataLoader类构建了一个数据加载器对象。以下是DataLoader的原型:
torch.utils.data.DataLoader(
dataset,
batch_size=1,
shuffle=False,
sampler=None,
batch_sampler=None,
num_workers=0,
collate_fn=None,
pin_memory=False,
drop_last=False,
timeout=0,
worker_init_fn=None,
multiprocessing_context=None,
generator=None)
dataset、batch_size、shuffle和sampler参数是最常用的。num_workers参数通常用于增加生成批次的 CPU 进程数量。其余参数仅用于高级情况。
如果您编写自己的数据集类,您只需要调用内置的DataLoader来为您的数据生成一个可迭代对象。无需从头开始创建数据加载器类。
本节提供了 PyTorch 数据准备功能的快速参考。现在您了解了如何使用 PyTorch 加载、转换和批处理数据,可以开始使用您的数据来开发和训练深度学习。
模型开发
大多数研究和开发都集中在开发新颖的深度学习模型上。模型开发过程包括几个步骤。在这一点上,我假设您已经创建了良好的数据集,并已经准备好让模型处理。
过程中的第一步是模型设计,您将设计一个或多个模型架构并初始化模型的参数(例如权重和偏差)。通常的做法是从现有设计开始,然后修改它或创建自己的设计。我将在本节中向您展示如何做这两种操作。
下一步是训练。在训练过程中,您将通过模型传递训练数据,测量误差或损失,并调整参数以改善结果。
在验证过程中,您将测量模型在未在训练中使用的验证数据上的性能。这有助于防止过拟合,即模型在训练数据上表现良好,但不能泛化到其他输入数据。
最后,模型开发过程通常以测试结束。测试是指您测量经过训练的模型在之前未见数据上的性能。本节提供了如何在 PyTorch 中完成模型开发的步骤和子步骤的快速参考。
模型设计
在过去的十年中,模型设计研究在所有行业和领域都有了显著的扩展。每年都会有成千上万篇论文涉及计算机视觉、自然语言处理、语音识别和音频处理等领域,以解决早期癌症检测等问题,并创新出自动驾驶汽车等新技术。因此,根据您要解决的问题,可以选择许多不同类型的模型架构。您甚至可以创建一些自己的模型!
使用现有和预训练模型
大多数用户开始模型开发时会选择一个现有的模型。也许您想要从现有设计开始,进行轻微修改或尝试小的改进,然后再设计自己的架构。您还可以使用已经用大量数据训练过的现有模型或模型部分。
PyTorch 提供了许多资源来利用现有的模型设计和预训练的神经网络。一个示例资源是基于 PyTorch 的torchvision库,用于计算机视觉。torchvision.models子包含有不同任务的模型定义,包括图像分类、像素级语义分割、目标检测、实例分割、人体关键点检测和视频分类。
假设我们想要在设计中使用著名的 VGG16 模型。VGG16(也称为 OxfordNet)是一种卷积神经网络架构,以牛津大学的视觉几何组命名,他们开发了这个模型。它在 2014 年提交到大规模视觉识别挑战,并在 ImageNet 上取得了 92.7%的前 5 测试准确率,ImageNet 是一个包含 1400 万手工注释图像的非常庞大的数据集。
我们可以轻松地创建一个预训练的 VGG16 模型,如下面的代码所示:
from torchvision import models
vgg16 = models.vgg16(pretrained=True)
默认情况下,模型将是未经训练的,并且具有随机初始化的权重。但是,在我们的情况下,我们希望使用预训练模型,因此我们设置pretrained = True。这将下载在 ImageNet 数据集上预训练的权重,并使用这些值初始化我们模型的权重。
您可以通过打印模型来查看 VGG16 模型中包含的层序列。VGG16 模型由三部分组成:features、avgpool和classifier。这里无法打印所有层,所以我们只打印classifier部分:
print(vgg16.classifier)
# out:
# Sequential(
# (0): Linear(in_features=25088,
# out_features=4096, bias=True)
# (1): ReLU(inplace=True)
# (2): Dropout(p=0.5, inplace=False)
# (3): Linear(in_features=4096,
# out_features=4096, bias=True)
# (4): ReLU(inplace=True)
# (5): Dropout(p=0.5, inplace=False)
# (6): Linear(in_features=4096,
# out_features=1000, bias=True)
# )
Linear、ReLU和Dropout是torch.nn模块。torch.nn用于创建神经网络层、激活函数、损失函数和其他神经网络组件。现在不要太担心它;我们将在下一节中详细介绍。
有许多著名的未经训练和经过预训练的模型可用,包括 AlexNet、VGG、ResNet、Inception 和 MobileNet 等。请参考Torchvision 模型文档获取完整的模型列表以及有关它们使用的详细信息。
PyTorch Hub 是另一个用于现有和预训练 PyTorch 模型的优秀资源。您可以使用torch.hub.load()API 从另一个存储库加载模型。以下代码显示了如何从 PyTorch Hub 加载模型:
waveglow = torch.hub.load(
'nvidia/DeepLearningExamples:torchhub',
'nvidia_waveglow')
在这里,我们加载一个名为 WaveGlow 的模型,用于从 NVIDIA DeepLearningExamples 存储库生成语音。
您可以在主 PyTorch Hub 网站找到 PyTorch Hub 存储库的列表。要探索特定存储库的所有可用 API 端点,您可以在存储库上使用torch.hub.list()函数,如下面的代码所示:
torch.hub.list(
'nvidia/DeepLearningExamples:torchhub')
# out:
# ['checkpoint_from_distributed',
# 'nvidia_ncf',
# 'nvidia_ssd',
# 'nvidia_ssd_processing_utils',
# 'nvidia_tacotron2',
# 'nvidia_waveglow',
# 'unwrap_distributed']
这列出了nvidia/DeepLearningExamples:torchhub存储库中所有可用的模型,包括 WaveGlow、Tacotron 2、SSD 等。尝试在支持 PyTorch Hub 的其他存储库上使用hub.list(),看看您可以找到哪些其他现有模型。
从 Python 库(如 Torchvision)和通过 PyTorch Hub 从存储库加载现有和预训练模型,可以让您在自己的工作中建立在以前的研究基础上。在本章后面,我将向您展示如何将您的模型部署到包和存储库中,以便其他人可以访问或基于您自己的研究和开发。
PyTorch NN 模块(torch.nn)
PyTorch 最强大的功能之一是其 Python 模块torch.nn,它使得设计和尝试新模型变得容易。以下代码说明了如何使用torch.nn创建一个简单模型。在这个例子中,我们将创建一个名为 SimpleNet 的全连接模型。它包括一个输入层、一个隐藏层和一个输出层,接收 2,048 个输入值并返回 2 个用于分类的输出值:
import torch.nn as nn
import torch.nn.functional as F
class SimpleNet(nn.Module):
def __init__(self): # ①
super(SimpleNet, self).__init__() # ②
self.fc1 = nn.Linear(2048, 256)
self.fc2 = nn.Linear(256, 64)
self.fc3 = nn.Linear(64,2)
def forward(self, x): # ③
x = x.view(-1, 2048)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = F.softmax(self.fc3(x),dim=1)
return x
①
通常将层创建为类属性
②
调用基类的__init__()函数来初始化参数
③
需要定义模型如何处理数据
在 PyTorch 中创建模型被认为是非常“Pythonic”的,意味着它以首选的 Python 方式创建对象。我们首先创建一个名为SimpleNet的新子类,它继承自nn.Module类,然后我们定义__init__()和forward()方法。__init__()函数初始化模型参数,而forward()函数定义了数据如何通过我们的模型传递。
在__init__()中,我们调用super()函数来执行父nn.Module类的__init__()方法以初始化类参数。然后我们使用nn.Linear模块定义一些层。
forward()函数定义了数据如何通过网络传递。在forward()函数中,我们首先使用view()将输入重塑为一个包含 2,048 个元素的向量,然后我们通过每一层处理输入并应用relu()激活函数。最后,我们应用softmax()函数并返回输出。
警告
PyTorch 使用术语module来描述 NN 层或块。Python 使用这个术语来描述一个可以导入的库包。在本书中,我将坚持使用 PyTorch 的用法,并使用术语Python 模块来描述 Python 库模块。
到目前为止,我们已经定义了 SimpleNet 模型中包含的层或模块,它们是如何连接的,以及参数是如何初始化的(通过super().init())。
以下代码显示了如何通过实例化名为simplenet的模型对象来创建模型:
simplenet = SimpleNet() # ①
print(simplenet)
# out:
# SimpleNet(
# (fc1): Linear(in_features=2048,
# out_features=256, bias=True)
# (fc2): Linear(in_features=256,
# out_features=64, bias=True)
# (fc3): Linear(in_features=64,
# out_features=2, bias=True)
# )
input = torch.rand(2048)
output = simplenet(input) # ②
①
实例化或创建模型。
②
通过模型运行数据(前向传递)。
如果我们打印模型,我们可以看到它的结构。执行我们的模型就像调用模型对象作为函数一样简单。我们传入输入,模型运行前向传递并返回输出。
这个简单的模型展示了在模型设计过程中需要做出的以下决策:
模块定义
您将如何定义您的 NN 的层?您将如何将这些层组合成构建块?在这个例子中,我们选择了三个线性或全连接层。
激活函数
您将在每个层或模块的末尾使用哪些激活函数?在这个例子中,我们选择在输入和隐藏层使用relu激活,在输出层使用softmax。
模块连接
您的模块将如何连接在一起?在这个例子中,我们选择简单地按顺序连接每个线性层。
输出选择
将返回什么输出值和格式?在这个例子中,我们从softmax()函数返回两个值。
这种范式的简单性、灵活性和 Python 风格是 PyTorch 在深度学习研究中如此受欢迎的原因。PyTorch 的torch.nn Python 模块包括用于创建 NN 模型设计所需的构建块、层和激活函数的类。让我们来看看 PyTorch 中可用的不同类型的构建块。
表 3-2 提供了一个NN 容器列表。您可以使用容器类来创建更高级别的构建块集。例如,您可以使用Sequential在一个块中创建一系列层。
表 3-2. PyTorch NN 容器
| 类 | 描述 |
|---|---|
Module |
所有 NN 模块的基类 |
Sequential |
一个顺序容器 |
ModuleList |
一个以列表形式保存子模块的容器 |
ModuleDict |
一个以字典形式保存子模块的容器 |
ParameterList |
一个以列表形式保存参数的容器 |
ParameterDict |
一个以字典形式保存参数的容器 |
注意
nn.Module是所有 NN 构建块的基类。您的 NN 可能由单个模块或包含其他模块的多个模块组成,这些模块也可能包含模块,从而创建构建块的层次结构。
表 3-3 列出了torch.nn支持的一些线性层。Linear通常用于全连接层。
表 3-3. PyTorch NN 线性层
| 类 | 描述 |
|---|---|
nn.Identity |
一个占位符身份运算符,不受参数影响 |
nn.Linear |
将线性变换应用于传入数据的层 |
nn.Bilinear |
将双线性变换应用于传入数据的层 |
表 3-4 列出了torch.nn支持的几种卷积层。卷积层在深度学习中经常用于在各个阶段对数据应用滤波器。正如您在表中看到的,PyTorch 内置支持 1D、2D 和 3D 卷积以及转置和折叠变体。
表 3-4. PyTorch NN 卷积层
| 类 | 描述 |
|---|---|
nn.Conv1d |
在由多个输入平面组成的输入信号上应用 1D 卷积 |
nn.Conv2d |
在由多个输入平面组成的输入信号上应用 2D 卷积 |
nn.Conv3d |
在由多个输入平面组成的输入信号上应用 3D 卷积 |
nn.ConvTranspose1d |
在由多个输入平面组成的输入图像上应用 1D 转置卷积运算符 |
nn.ConvTranspose2d |
在由多个输入平面组成的输入图像上应用 2D 转置卷积运算符 |
nn.ConvTranspose3d |
对由多个输入平面组成的输入图像应用 3D 转置卷积运算符 |
nn.Unfold |
从批量输入张量中提取滑动本地块 |
nn.Fold |
将滑动本地块的数组组合成一个大的包含张量 |
表 3-5 显示了torch.nn中可用的池化层。池化通常用于下采样或减少输出层的复杂性。PyTorch 支持 1D、2D 和 3D 池化以及最大或平均池化方法,包括它们的自适应变体。
表 3-5. PyTorch NN 池化层
| 类 | 描述 |
|---|---|
nn.MaxPool1d |
对由多个输入平面组成的输入信号应用 1D 最大池化 |
nn.MaxPool2d |
对由多个输入平面组成的输入信号应用 2D 最大池化 |
nn.MaxPool3d |
对由多个输入平面组成的输入信号应用 3D 最大池化 |
nn.MaxUnpool1d |
计算MaxPool1d的部分逆操作 |
nn.MaxUnpool2d |
计算MaxPool2d的部分逆操作 |
nn.MaxUnpool3d |
计算MaxPool3d的部分逆操作 |
nn.AvgPool1d |
对由多个输入平面组成的输入信号应用 1D 平均池化 |
nn.AvgPool2d |
对由多个输入平面组成的输入信号应用 2D 平均池化 |
nn.AvgPool3d |
对由多个输入平面组成的输入信号应用 3D 平均池化 |
nn.FractionalMaxPool2d |
对由多个输入平面组成的输入信号应用 2D 分数最大池化 |
nn.LPPool1d |
对由多个输入平面组成的输入信号应用 1D 幂平均池化 |
nn.LPPool2d |
对由多个输入平面组成的输入信号应用 2D 幂平均池化 |
nn.AdaptiveMaxPool1d |
对由多个输入平面组成的输入信号应用 1D 自适应最大池化 |
nn.AdaptiveMaxPool2d |
对由多个输入平面组成的输入信号应用 2D 自适应最大池化 |
nn.AdaptiveMaxPool3d |
对由多个输入平面组成的输入信号应用 3D 自适应最大池化 |
nn.AdaptiveAvgPool1d |
对由多个输入平面组成的输入信号应用 1D 自适应平均池化 |
nn.AdaptiveAvgPool2d |
对由多个输入平面组成的输入信号应用 2D 自适应平均池化 |
nn.AdaptiveAvgPool3d |
对由多个输入平面组成的输入信号应用 3D 自适应平均池化 |
表 3-6 列出了可用的填充层。填充在图层输出增加尺寸时填充缺失数据。PyTorch 支持 1D、2D 和 3D 填充,并可以使用反射、复制、零或常数填充数据。
表 3-6. PyTorch NN 填充层
| 类 | 描述 |
|---|---|
nn.ReflectionPad1d |
使用输入边界的反射填充输入张量 |
nn.ReflectionPad2d |
使用输入边界的反射填充输入张量的 2D 输入 |
nn.ReplicationPad1d |
使用输入边界的复制填充输入张量 |
nn.ReplicationPad2d |
使用输入边界的复制填充输入张量的 2D 输入 |
nn.ReplicationPad3d |
使用输入边界的复制填充输入张量的 3D 输入 |
nn.ZeroPad2d |
使用零填充输入张量的边界 |
nn.ConstantPad1d |
使用常数值填充输入张量的边界 |
nn.ConstantPad2d |
使用常数值填充 2D 输入的边界 |
nn.ConstantPad3d |
使用常数值填充 3D 输入的边界 |
表 3-7 列出了dropout的可用层。Dropout 通常用于减少复杂性、加快训练速度,并引入一些正则化以防止过拟合。PyTorch 支持 1D、2D 和 3D 层的 dropout,并提供对 alpha dropout 的支持。
表 3-7. PyTorch NN dropout 层
| 类 | 描述 |
|---|---|
nn.Dropout |
在训练期间,使用来自伯努利分布的样本,以概率p随机将输入张量的一些元素归零 |
nn.Dropout2d |
对 2D 输入随机归零整个通道 |
nn.Dropout3d |
对 3D 输入随机归零整个通道 |
nn.AlphaDropout |
对输入应用 alpha dropout |
表 3-8 提供了支持归一化的类列表。在某些层之间执行归一化以防止梯度消失或爆炸,通过保持中间层输入在一定范围内来实现。它还可以帮助加快训练过程。PyTorch 支持 1D、2D 和 3D 输入的归一化,并提供批次、实例、组和同步归一化等归一化方法。
表 3-8. PyTorch NN 归一化层
| 类 | 描述 |
|---|---|
nn.BatchNorm1d |
对 2D 或 3D 输入(带有可选额外通道维度的 1D 输入的小批量)应用批次归一化,如论文“通过减少内部协变量转移加速深度网络训练的批次归一化”中所述 |
nn.BatchNorm2d |
对 4D 输入(带有额外通道维度的 2D 输入的小批量)应用批次归一化,如论文“批次归一化”中所述 |
nn.BatchNorm3d |
对 5D 输入(带有额外通道维度的 3D 输入的小批量)应用批次归一化,如论文“批次归一化”中所述 |
nn.GroupNorm |
对输入的小批量应用组归一化,如论文“组归一化”中所述 |
nn.SyncBatchNorm |
对n维输入(带有额外通道维度的[n–2]D 输入的小批量)应用批次归一化,如论文“批次归一化”中所述 |
nn.InstanceNorm1d |
对 3D 输入(带有可选额外通道维度的 1D 输入的小批量)应用实例归一化,如论文“实例归一化:快速风格化的缺失成分”中所述 |
nn.InstanceNorm2d |
对 4D 输入(带有额外通道维度的 2D 输入的小批量)应用实例归一化,如论文“实例归一化”中所述 |
nn.InstanceNorm3d |
对 5D 输入(带有额外通道维度的 3D 输入的小批量)应用实例归一化,如论文“实例归一化”中所述 |
nn.LayerNorm |
对输入的小批量应用层归一化,如论文“层归一化”中所述 |
nn.LocalResponseNorm |
对由多个输入平面组成的输入信号应用局部响应归一化,其中通道占据第二维 |
表 3-9 显示了用于循环神经网络(RNN)的循环层。RNN 经常用于处理时间序列或基于序列的数据。PyTorch 内置支持 RNN、长短期记忆(LSTM)和门控循环单元(GRU)层,以及用于 RNN、LSTM 和 GRU 单元的类。
表 3-9. PyTorch NN 循环层
| 类 | 描述 |
|---|---|
nn.RNNBase |
RNN 基类 |
nn.RNN |
应用多层 Elman RNN(使用\Tanh 或 ReLU 非线性)到输入序列的层 |
nn.LSTM |
应用多层 LSTM RNN 到输入序列的层 |
nn.GRU |
应用多层 GRU RNN 到输入序列的层 |
nn.RNNCell |
具有 tanh 或 ReLU 非线性的 Elman RNN 单元 |
nn.LSTMCell |
一个 LSTM 单元 |
nn.GRUCell |
一个 GRU 单元 |
表 3-10 列出了用于变压器网络的变压器层。变压器网络通常被认为是处理序列数据的最先进技术。PyTorch 支持完整的Transformer模型类,还提供了以堆栈和层格式提供的Encoder和Decoder子模块。
表 3-10. PyTorch NN 变压器层
| 类 | 描述 |
|---|---|
nn.Transformer |
一个变压器模型 |
nn.TransformerEncoder |
N个编码器层的堆叠 |
nn.TransformerDecoder |
N个解码器层的堆叠 |
nn.TransformerEncoderLayer |
由自我注意(attn)和前馈网络组成的层 |
nn.TransformerDecoderLayer |
由自我注意、多头注意和前馈网络组成的层 |
表 3-11 包含了一系列稀疏层。PyTorch 提供了对文本数据嵌入的内置支持,以及用于余弦相似度和两两距离的稀疏层,这些在推荐引擎算法中经常使用。
表 3-11. PyTorch NN 稀疏层和距离函数
| 类 | 描述 |
|---|---|
nn.Embedding |
存储固定字典和大小的嵌入 |
nn.EmbeddingBag |
计算“包”嵌入的和或平均值,而不实例化中间嵌入 |
nn.CosineSimilarity |
返回沿一个维度计算的x[1]和x[2]之间的余弦相似度 |
nn.PairwiseDistance |
使用p-范数计算向量v[1]和v[2]之间的批次两两距离 |
表 3-12 包含了支持计算机视觉的视觉层列表。它们包括用于洗牌像素和执行多种上采样算法的层。
表 3-12. PyTorch NN 视觉层
| 类 | 描述 |
|---|---|
nn.PixelShuffle |
将形状为(∗, , H, W)的张量重新排列为形状为(∗, C, , )的张量 |
nn.Upsample |
上采样给定的多通道 1D(时间)、2D(空间)或 3D(体积)数据 |
nn.UpsamplingNearest2d |
对由多个输入通道组成的输入信号应用 2D 最近邻上采样 |
nn.UpsamplingBilinear2d |
对由多个输入通道组成的输入信号应用 2D 双线性上采样 |
表 3-13 提供了torch.nn中所有激活函数的列表。激活函数通常应用于层输出,以引入模型中的非线性。PyTorch 支持传统的激活函数,如 sigmoid、tanh、softmax 和 ReLU,以及最近的函数,如 leaky ReLU。随着研究人员设计和应用新的激活函数,更多的函数正在被添加到其中。
表 3-13. PyTorch NN 非线性激活
| 类 | 描述 |
|---|---|
nn.ELU |
逐元素应用指数线性单元函数 |
nn.Hardshrink |
逐元素应用硬收缩函数 |
nn.Hardsigmoid |
逐元素应用硬 sigmoid 函数 |
nn.Hardtanh |
逐元素应用 hardtanh 函数 |
nn.Hardswish |
逐元素应用 hardswish 函数 |
nn.LeakyReLU |
逐元素应用泄漏修正线性单元函数 |
nn.LogSigmoid |
逐元素应用对数 sigmoid 函数 |
nn.MultiheadAttention |
允许模型同时关注来自不同表示子空间的信息 |
nn.PReLU |
逐元素应用参数化修正线性单元函数 |
nn.ReLU |
逐元素应用修正线性单元函数 |
nn.ReLU6 |
应用带有最大值的修正线性单元函数 |
nn.RReLU |
逐元素应用随机泄漏修正线性单元函数 |
nn.SELU |
逐元素应用缩放指数线性单元函数 |
nn.CELU |
逐元素应用连续可微指数线性单元函数 |
nn.GELU |
应用高斯误差线性单元函数 |
nn.Sigmoid |
逐元素应用 sigmoid 函数 |
nn.Softplus |
逐元素应用 softplus 函数 |
nn.Softshrink |
逐元素应用软收缩函数 |
nn.Softsign |
逐元素应用 softsign 函数 |
nn.Tanh |
逐元素应用双曲正切函数 |
nn.Tanhshrink |
逐元素应用带收缩的双曲正切函数 |
nn.Threshold |
设定输入张量的每个元素的阈值 |
nn.Softmin |
将 softmin 函数应用于n维输入张量,以便将n维输出张量的元素重新缩放到[0,1]范围,并总和为 1 |
nn.Softmax |
将 softmax 函数应用于n维输入张量,以便将n维输出张量的元素重新缩放到[0,1]范围,并总和为 1 |
nn.Softmax2d |
将 softmax 函数应用于每个空间位置的特征 |
nn.LogSoftmax |
将 log(softmax(x))函数应用于n维输入张量 |
nn.AdaptiveLogSoftmaxWithLoss |
提供了一个高效的 softmax 近似,如 Edouard Grave 等人在“Efficient Softmax Approximation for GPUs”中描述的 |
正如您所看到的,PyTorch 的torch.nn模块支持一组强大的 NN 层和激活函数。您可以使用其类来创建从简单的顺序模型到复杂的多层次网络、生成对抗网络(GANs)、变换器网络、RNN 等各种模型。
现在您已经知道如何设计您的模型,让我们探讨如何使用 PyTorch 训练和测试您自己的 NN 模型设计。
训练
在模型设计过程中,您定义了 NN 模块、它们的参数以及它们之间的连接方式。在 PyTorch 中,您的模型设计被实现为一个从torch.nn.Module类派生的模型对象。您可以调用该对象将数据传递到模型中,并根据模型架构和其参数的当前值生成输出。
模型开发的下一步是使用训练数据训练您的模型。训练模型仅涉及估计模型的参数、传递数据和调整参数以获得对数据一般建模更准确的表示。
换句话说,您设置参数为某些值,通过数据,然后将模型的输出与真实输出进行比较以测量误差。目标是改变参数并重复该过程,直到误差最小化且模型的输出与真实输出相同。
基本训练循环
PyTorch 相对于其他机器学习框架的一个关键优势是其灵活性,特别是在创建自定义训练循环时。在本章中,我们将探讨一个常用于监督学习的基本训练循环。
在这个例子中,我们将使用本章前面使用过的 CIFAR-10 数据集训练 LeNet5 模型。LeNet5 模型是由 Yann LeCun 及其团队在 1990 年代在贝尔实验室开发的一个简单的卷积 NN,用于分类手写数字。(当时我并不知道,我实际上在新泽西州霍尔姆德尔的同一栋建筑物中为贝尔实验室工作,而这项工作正在进行中。)
可以使用以下代码创建现代化的 LeNet5 模型版本:
from torch import nn
import torch.nn.functional as F
class LeNet5(nn.Module): # ①
def __init__(self):
super(LeNet5, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = F.max_pool2d(F.relu(self.conv1(x)),
(2, 2))
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(-1,
int(x.nelement() / x.shape[0]))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
device = ('cuda' if torch.cuda.is_available()
else 'cpu') # ②
model = LeNet5().to(device=device) # ③
①
定义模型类。
②
如果有 GPU 可用,请使用。
③
创建模型并将其移动到 GPU(如果可用)。
如前面的代码所示,我们的 LeNet5 模型使用了两个卷积层和三个全连接或线性层。它已经通过最大池化和 ReLU 激活进行了现代化。在这个例子中,我们还将利用 GPU 进行训练,以加快训练速度。在这里,我们创建了名为model的模型对象。
接下来,我们需要定义损失函数(也称为标准)和优化器算法。损失函数确定我们如何衡量模型的性能,并计算预测与真相之间的损失或错误。我们将尝试通过调整模型参数来最小化损失。优化器定义了我们在训练过程中如何更新模型参数。
为了定义损失函数和优化器,我们使用torch.optim和torch.nn包,如下面的代码所示:
from torch import optim
from torch import nn
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), # ①
lr=0.001,
momentum=0.9)
①
确保传入model.parameters()作为您的模型。
在这个例子中,我们使用CrossEntropyLoss()函数和随机梯度下降(SGD)优化器。交叉熵损失经常用于分类问题。SGD 算法也常用作优化器函数。选择损失函数和优化器超出了本书的范围;但是,我们将在本章后面检查许多内置的 PyTorch 损失函数和优化器。
警告
PyTorch 优化器要求您使用parameters()方法传入模型参数(即model.parameters())。忘记()是一个常见错误。
以下 PyTorch 代码演示了基本的训练循环:
N_EPOCHS = 10
for epoch in range(N_EPOCHS): # ①
epoch_loss = 0.0
for inputs, labels in trainloader:
inputs = inputs.to(device) # ②
labels = labels.to(device)
optimizer.zero_grad() # ③
outputs = model(inputs) # ④
loss = criterion(outputs, labels) # ⑤
loss.backward() # ⑥
optimizer.step() # ⑦
epoch_loss += loss.item() # ⑧
print("Epoch: {} Loss: {}".format(epoch,
epoch_loss/len(trainloader)))
# out: (results will vary and make take minutes)
# Epoch: 0 Loss: 1.8982970092773437
# Epoch: 1 Loss: 1.6062103009033204
# Epoch: 2 Loss: 1.484384165763855
# Epoch: 3 Loss: 1.3944422281837463
# Epoch: 4 Loss: 1.334191104450226
# Epoch: 5 Loss: 1.2834235876464843
# Epoch: 6 Loss: 1.2407222446250916
# Epoch: 7 Loss: 1.2081411465930938
# Epoch: 8 Loss: 1.1832368299865723
# Epoch: 9 Loss: 1.1534993273162841
①
外部训练循环;循环 10 个 epochs。
②
如果可用,将输入和标签移动到 GPU。
③
在每次反向传播之前将梯度清零,否则它们会累积。
④
执行前向传播。
⑤
计算损失。
⑥
执行反向传播;计算梯度。
⑦
根据梯度调整参数。
⑧
累积批量损失,以便我们可以在整个 epoch 上进行平均。
训练循环由两个循环组成。在外部循环中,我们将在每次迭代或 epoch 中处理整个训练数据集。然而,我们不会等到处理完整个数据集后再更新模型参数,而是一次处理一个较小的数据批次。内部循环遍历每个批次。
警告
默认情况下,PyTorch 在每次调用loss.backward()(即反向传播)时累积梯度。这在训练某些类型的 NNs(如 RNNs)时很方便;然而,对于卷积神经网络(CNNs)来说并不理想。在大多数情况下,您需要调用optimizer.zero_grad()来将梯度归零,以便在进行反向传播之前优化器正确更新模型参数。
对于每个批次,我们将批次(称为inputs)传递到模型中。它运行前向传播并返回计算的输出。接下来,我们使用criterion()将模型输出(称为outputs)与训练数据集中的真实值(称为labels)进行比较,以计算错误或损失。
接下来,我们调整模型参数(即 NN 的权重和偏差)以减少损失。为此,我们首先使用loss.backward()执行反向传播来计算梯度,然后使用optimizer.step()运行优化器来根据计算的梯度更新参数。
这是训练 NN 模型使用的基本过程。实现可能有所不同,但您可以在创建自己的训练循环时使用此示例作为快速参考。在设计训练循环时,您需要决定如何处理或分批数据,使用什么损失函数以及运行什么优化算法。
您可以使用 PyTorch 内置的损失函数和优化算法之一,也可以创建自己的。
损失函数
PyTorch 在torch.nn Python 模块中包含许多内置的损失函数。表 3-14 提供了可用损失函数的列表。
表 3-14. 损失函数
| 损失函数 | 描述 |
|---|---|
nn.L1Loss() |
创建一个标准,用于测量输入x和目标y中每个元素的平均绝对误差(MAE) |
nn.MSELoss() |
创建一个标准,测量输入x和目标y中每个元素的均方误差(平方 L2 范数) |
nn.CrossEntropyLoss() |
将nn.LogSoftmax()和nn.NLLLoss()结合在一个类中 |
nn.CTCLoss() |
计算连接主义时间分类损失 |
nn.NLLLoss() |
计算负对数似然损失 |
nn.PoissonNLLLoss() |
使用泊松分布计算目标的负对数似然损失 |
nn.KLDivLoss() |
用于测量 Kullback-Leibler 散度损失 |
nn.BCELoss() |
创建一个标准,测量目标和输出之间的二元交叉熵 |
nn.BCEWithLogitsLoss() |
将 sigmoid 层和nn.BCELoss()结合在一个类中 |
nn.MarginRankingLoss() |
创建一个标准,用于在给定输入x¹、x²(两个 1D 小批量张量)和标签 1D 小批量张量y(包含 1 或-1)时测量损失 |
nn.HingeEmbeddingLoss() |
在给定输入张量x和标签张量y(包含 1 或-1)时测量损失 |
nn.MultiLabelMarginLoss() |
创建一个标准,用于优化多类分类的铰链损失(即基于边界的损失),输入为x(一个 2D 小批量张量)和输出y(一个目标类别索引的 2D 张量) |
nn.SmoothL1Loss() |
创建一个标准,如果绝对元素误差低于 1,则使用平方项,否则使用 L1 项 |
nn.SoftMarginLoss() |
创建一个标准,优化输入张量x和目标张量y(包含 1 或-1)之间的两类分类逻辑损失 |
nn.MultiLabelSoftMarginLoss() |
创建一个标准,基于最大熵优化多标签一对所有损失 |
nn.CosineEmbeddingLoss() |
创建一个标准,给定输入张量x¹、x²和标记为 1 或-1 的张量y时测量损失 |
nn.MultiMarginLoss() |
创建一个标准,优化多类分类的铰链损失 |
nn.TripletMarginLoss() |
创建一个标准,给定输入张量x¹、x²、x³和大于 0 的边界值时测量三元组损失 |
警告
CrossEntropyLoss()函数包括 softmax 计算,通常在 NN 分类器模型的最后一步执行。在使用CrossEntropyLoss()时,不要在模型定义的输出层中包含Softmax()。
优化算法
PyTorch 还在torch.optim Python 子模块中包含许多内置的优化器算法。表 3-15 列出了可用的优化器算法及其描述。
表 3-15. 优化器算法
| Algorithm | 描述 |
|---|---|
Adadelta() |
自适应学习率方法 |
Adagrad() |
自适应梯度算法 |
Adam() |
随机优化方法 |
AdamW() |
一种 Adam 变体,提出于“解耦权重衰减正则化” |
SparseAdam() |
适用于稀疏张量的 Adam 版本 |
Adamax() |
基于无穷范数的 Adam 变体 |
ASGD() |
平均随机梯度下降 |
LBFGS() |
BFGS 算法的有限内存实现,受minFunc启发 |
RMSprop() |
均方根传播 |
Rprop() |
弹性反向传播 |
SGD() |
随机梯度下降 |
torch.optim Python 子模块支持大多数常用算法。接口足够通用,因此将来也可以轻松集成新算法。访问torch.optim 文档以获取有关如何配置算法和调整学习率的更多详细信息。
验证
现在我们已经训练了我们的模型并尝试最小化损失,我们如何评估其性能?我们如何知道我们的模型将泛化并与以前未见过的数据一起工作?
模型开发通常包括验证和测试循环,以确保不会发生过拟合,并且模型将针对未见数据表现良好。让我们首先讨论验证。在这里,我将为您提供一个如何使用 PyTorch 将验证添加到训练循环中的快速参考。
通常,我们会保留一部分训练数据用于验证。验证数据不会用于训练 NN;相反,我们将在每个时代结束时使用它来测试模型的性能。
在训练模型时进行验证是一个好的实践。在调整超参数时通常会执行验证。例如,也许我们想在五个时代后降低学习率。
在执行验证之前,我们需要将训练数据集分成训练数据集和验证数据集,如下所示:
from torch.utils.data import random_split
train_set, val_set = random_split(
train_data,
[40000, 10000])
trainloader = torch.utils.data.DataLoader(
train_set,
batch_size=16,
shuffle=True)
valloader = torch.utils.data.DataLoader(
val_set,
batch_size=16,
shuffle=True)
print(len(trainloader))
# out: 2500
print(len(valloader))
# out: 625
我们使用torch.utils.data中的random_split()函数,将我们的 50,000 个训练图像中的 10,000 个保留用于验证。一旦创建了train_set和val_set,我们为每个创建数据加载器。
然后我们定义我们的模型、损失函数(或标准)和优化器,如下所示:
from torch import optim
from torch import nn
model = LeNet5().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(),
lr=0.001,
momentum=0.9)
以下代码显示了先前的基本训练示例,并添加了验证:
N_EPOCHS = 10
for epoch in range(N_EPOCHS):
# Training
train_loss = 0.0
model.train() # ①
for inputs, labels in trainloader:
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
# Validation
val_loss = 0.0
model.eval() # ②
for inputs, labels in valloader:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels)
val_loss += loss.item()
print(
"Epoch: {} Train Loss: {} Val Loss: {}".format(
epoch,
train_loss/len(trainloader),
val_loss/len(valloader)))
①
为训练配置模型。
②
为测试配置模型。
在处理训练数据后,每个时代都会进行验证。在验证期间,模型会传递尚未用于训练且尚未被模型看到的数据。我们只在验证期间执行前向传递。
注意
在模型对象上运行.train()或.eval()方法会将模型分别置于训练或测试模式。只有在模型在训练和评估时操作不同的情况下才需要调用这些方法。例如,训练中使用了 dropout 和批量归一化,但在验证或测试中没有使用。在循环中调用.train()和.eval()是一个好的实践。
如果验证数据的损失减少,那么模型表现良好。然而,如果训练损失减少而验证损失没有减少,那么模型很可能出现过拟合。查看前一个训练循环的结果。您应该有类似以下结果:
# out: (results may vary and take a few minutes)
# Epoch: 0 Train Loss: 1.987607608 Val Loss: 1.740786979
# Epoch: 1 Train Loss: 1.649753892 Val Loss: 1.587019552
# Epoch: 2 Train Loss: 1.511723689 Val Loss: 1.435539366
# Epoch: 3 Train Loss: 1.408525426 Val Loss: 1.361453659
# Epoch: 4 Train Loss: 1.339505518 Val Loss: 1.293459154
# Epoch: 5 Train Loss: 1.290560259 Val Loss: 1.245048282
# Epoch: 6 Train Loss: 1.259268565 Val Loss: 1.285989610
# Epoch: 7 Train Loss: 1.235161985 Val Loss: 1.253840940
# Epoch: 8 Train Loss: 1.207051850 Val Loss: 1.215700019
# Epoch: 9 Train Loss: 1.189215132 Val Loss: 1.183332257
正如您所看到的,我们的模型训练良好,似乎没有过拟合,因为训练损失和验证损失都在减少。如果我们训练模型更多的 epochs,我们可能会获得更好的结果。
尽管如此,我们还没有完成。我们的模型可能仍然存在过拟合的问题。我们可能只是在选择超参数时运气好,导致验证结果良好。为了进一步测试是否存在过拟合,我们将一些测试数据通过我们的模型运行。
模型在训练期间从未见过测试数据,测试数据也没有对超参数产生任何影响。让我们看看我们在测试数据集上的表现如何。
测试
CIFAR-10 提供了自己的测试数据集,我们在本章前面创建了test_data和一个 testloader。让我们通过我们的测试循环运行测试数据,如下所示的代码:
num_correct = 0.0
for x_test_batch, y_test_batch in testloader:
model.eval() # ①
y_test_batch = y_test_batch.to(device)
x_test_batch = x_test_batch.to(device)
y_pred_batch = model(x_test_batch) # ②
_, predicted = torch.max(y_pred_batch, 1) # ③
num_correct += (predicted ==
y_test_batch).float().sum() # ④
accuracy = num_correct/(len(testloader) \
*testloader.batch_size) # ⑤
print(len(testloader), testloader.batch_size)
# out: 625 16
print("Test Accuracy: {}".format(accuracy))
# out: Test Accuracy: 0.6322000026702881
①
将模型设置为测试模式。
②
预测每个批次的结果。
③
选择具有最高概率的类索引。
④
将预测与真实标签进行比较并计算正确预测的数量。
⑤
计算正确预测的百分比(准确率)。
我们在训练 10 个 epochs 后的初始测试结果显示,在测试数据上的准确率为 63%。这不是一个坏的开始;看看您是否可以通过训练更多 epochs 来提高准确率。
现在您知道如何使用 PyTorch 创建训练、验证和测试循环。在创建自己的循环时,可以随时使用此代码作为参考。
现在您已经有了一个完全训练好的模型,让我们探索在模型部署阶段可以做些什么。
模型部署
根据您的目标,有许多选项可用于保存或部署您的训练模型。如果您正在进行深度学习研究,您可能希望以一种可以重复实验或稍后访问以进行演示和发表论文的方式保存模型。您还可以希望将模型发布为像 Torchvision 这样的 Python 软件包的一部分,或将其发布到 PyTorch Hub 这样的存储库,以便其他研究人员可以访问您的工作。
在开发方面,您可能希望将训练好的 NN 模型部署到生产环境中,或将模型与产品或服务集成。这可能是一个原型系统、边缘设备或移动设备。您还可以将其部署到本地生产服务器或提供系统可以使用的 API 端点的云服务器。无论您的目标是什么,PyTorch 都提供了能力来帮助您按照您的意愿部署模型。
保存模型
最简单的事情之一是保存训练好的模型以备将来使用。当您想对新输入运行模型时,您只需加载它并使用新值调用模型。
以下代码演示了保存和加载训练模型的推荐方法。它使用state_dict()方法,该方法创建一个字典对象,将每个层映射到其参数张量。换句话说,我们只需要保存模型的学习参数。我们已经在模型类中定义了模型的设计,因此不需要保存架构。当我们加载模型时,我们使用构造函数创建一个“空白模型”,然后使用load_state_dict()为每个层设置参数:
torch.save(model.state_dict(), "./lenet5_model.pt")
model = LeNet5().to(device)
model.load_state_dict(torch.load("./lenet5_model.pt"))
请注意,load_state_dict()需要一个字典对象,而不是一个保存的state_dict对象的路径。在将其传递给load_state_dict()之前,您必须使用torch.load()对保存的state_dict文件进行反序列化。
注意
一个常见的 PyTorch 约定是使用.pt或.pth文件扩展名保存模型。
您也可以使用torch.save(PATH)和model = torch.load(PATH)保存和加载整个模型。尽管这更直观,但不建议这样做,因为序列化过程与用于定义模型类的确切文件路径和目录结构绑定。如果您重构类代码并尝试在其他项目中加载模型,您的代码可能会出现问题。相反,保存和加载state_dict对象将为您提供更多灵活性,以便稍后恢复模型。
部署到 PyTorch Hub
PyTorch Hub 是一个预训练模型存储库,旨在促进研究的可重复性。在本章的前面,我向您展示了如何从 PyTorch Hub 加载预先存在的或预训练的模型。现在,我将向您展示如何通过添加一个简单的hubconf.py文件将您的预训练模型(包括模型定义和预训练权重)发布到 GitHub 存储库。hubconf.py文件定义了代码依赖关系,并为 PyTorch API 提供一个或多个端点。
在大多数情况下,只需导入正确的函数就足够了,但您也可以明确定义入口点。以下代码显示了如何使用 VGG16 端点从 PyTorch Hub 加载模型:
import torch
vgg16 = torch.hub.load('pytorch/vision',
'vgg16', pretrained=True)
现在,如果您已经创建了 VGG16 并希望将其部署到 PyTorch Hub,您只需要在存储库的根目录中包含以下hubconf.py文件。hubconf.py配置文件将torch设置为依赖项。此文件中定义的任何函数都将充当端点,因此只需导入 VGG16 函数即可完成任务:
dependencies = ['torch']
from torchvision.models.vgg import vgg16
如果您想明确定义端点,可以编写如下代码中的函数:
dependencies = ['torch']
from torchvision.models.vgg import vgg16 as _vgg16
# vgg16 is the name of the entrypoint
def vgg16(pretrained=False, **kwargs):
""" # This docstring shows up in hub.help():
VGG16 model
pretrained (bool): kwargs,
load pretrained weights into the model
"""
# Call the model; load pretrained weights
model = _vgg16(pretrained=pretrained, **kwargs)
return model
就是这样!全世界的研究人员将因为能够轻松从 PyTorch Hub 加载您的预训练模型而感到高兴。
部署到生产环境
将模型保存到文件和存储库可能在进行研究时是可以的;然而,为了解决大多数问题,我们必须将我们的模型集成到产品和服务中。这通常被称为“部署到生产环境”。有许多方法可以做到这一点,PyTorch 具有内置功能来支持它们。部署到生产环境是一个全面的主题,将在第七章中深入讨论。
本章涵盖了很多内容,探讨了深度学习开发过程,并提供了一个快速参考,介绍了 PyTorch 在实现每个步骤时的能力。下一章将介绍更多的参考设计,您可以在涉及迁移学习、情感分析和生成学习的项目中使用。
第四章:神经网络开发参考设计
在上一章中,我们以高层次介绍了 NN 开发过程,并学习了如何在 PyTorch 中实现每个阶段。该章节中的示例侧重于使用 CIFAR-10 数据集和简单的全连接网络解决图像分类问题。CIFAR-10 图像分类是一个很好的学术示例,用来说明 NN 开发过程,但在使用 PyTorch 开发深度学习模型时还有很多内容。
本章介绍了一些用于 PyTorch 中 NN 开发的参考设计。参考设计是代码示例,您可以将其用作解决类似类型问题的参考。
确实,本章中的参考设计仅仅触及了深度学习的可能性表面;然而,我将尝试为您提供足够多样性的内容,以帮助您开发自己的解决方案。我们将使用三个示例来处理各种数据,设计不同的模型架构,并探索学习过程的其他方法。
第一个示例使用 PyTorch 执行迁移学习,使用小数据集和预训练网络对蜜蜂和蚂蚁的图像进行分类。第二个示例使用 PyTorch 执行情感分析,使用文本数据训练一个 NLP 模型,预测电影评论的积极或消极情感。第三个示例使用 PyTorch 展示生成学习,通过训练生成对抗网络(GAN)生成服装的图像。
在每个示例中,我将提供 PyTorch 代码,以便您可以将本章作为快速参考,用于编写自己设计的代码。让我们开始看看 PyTorch 如何使用迁移学习解决计算机视觉问题。
使用迁移学习进行图像分类
图像分类的主题已经深入研究,许多著名的模型,如之前看到的 AlexNet 和 VGG 模型,都可以通过 PyTorch 轻松获得。然而,这些模型是使用 ImageNet 数据集进行训练的。虽然 ImageNet 包含 1,000 个不同的图像类别,但可能不包含您需要解决的图像分类问题的类别。
在这种情况下,您可以应用迁移学习,这是一个过程,通过在一个更小的新图像数据集上微调预训练模型。在下一个示例中,我们将训练一个模型来对蜜蜂和蚂蚁的图像进行分类——这些类别不包含在 ImageNet 中。蜜蜂和蚂蚁看起来非常相似,很难区分。
为了训练我们的新分类器,我们将微调另一个著名的模型 ResNet18,通过加载预训练模型并使用 120 张新的蜜蜂和蚂蚁的训练图像进行训练——与 ImageNet 中数百万张图像相比,这是一个更小的数据集。
数据处理
让我们从加载数据,定义转换和配置数据加载器以进行批量采样开始。与之前一样,我们将利用 Torchvision 库中的函数来创建数据集,加载数据并应用数据转换。
首先让我们导入本示例所需的库:
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import torchvision
from torchvision import datasets, models
from torchvision import transforms
然后我们将下载用于训练和验证的数据:
from io import BytesIO
from urllib.request import urlopen
from zipfile import ZipFile
zipurl = 'https://pytorch.tips/bee-zip'
with urlopen(zipurl) as zipresp:
with ZipFile(BytesIO(zipresp.read())) as zfile:
zfile.extractall('./data')
在这里,我们使用io、urlib和zipfile库来下载并解压文件到本地文件系统。运行前面的代码后,您应该在本地data/文件夹中拥有您的训练和验证图像。它们分别位于data/hymenoptera_data/train和data/hymenoptera_data/val中。
接下来我们定义我们的转换,加载数据,并配置我们的批采样器。
首先我们定义我们的转换:
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(
[0.485, 0.456,0.406],
[0.229, 0.224, 0.225])])
val_transforms = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
[0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
请注意,我们在训练时随机调整、裁剪和翻转图像,但在验证时不这样做。Normalize转换中使用的“魔术”数字是预先计算的均值和标准差。
现在让我们定义数据集:
train_dataset = datasets.ImageFolder(
root='data/hymenoptera_data/train',
transform=train_transforms)
val_dataset = datasets.ImageFolder(
root='data/hymenoptera_data/val',
transform=val_transforms)
在前面的代码中,我们使用 ImageFolder 数据集从我们的数据文件夹中提取图像,并将转换设置为我们之前定义的转换。接下来,我们为批量迭代定义我们的数据加载器:
train_loader = torch.utils.data.DataLoader(
train_dataset,
batch_size=4,
shuffle=True,
num_workers=4)
val_loader = torch.utils.data.DataLoader(
val_dataset,
batch_size=4,
shuffle=True,
num_workers=4)
我们使用批量大小为 4,并将num_workers设置为4以配置四个 CPU 进程来处理并行处理。
现在我们已经准备好了训练和验证数据,我们可以设计我们的模型。
模型设计
在这个例子中,我们将使用一个已经在 ImageNet 数据上进行了预训练的 ResNet18 模型。然而,ResNet18 被设计用来检测 1,000 个类别,在我们的情况下,我们只需要 2 个类别——蜜蜂和蚂蚁。我们可以修改最后一层以检测 2 个类别,而不是 1,000 个,如下面的代码所示:
model = models.resnet18(pretrained=True)
print(model.fc)
# out:
# Linear(in_features=512, out_features=1000, bias=True)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 2)
print(model.fc)
# out:
# Linear(in_features=512, out_features=2, bias=True)
我们首先使用函数torchvision.models.resnet18()加载一个预训练的 ResNet18 模型。接下来,我们通过model.fc.in_features读取最后一层之前的特征数量。然后,我们通过直接将model.fc设置为具有两个输出的全连接层来更改最后一层。
我们将使用预训练模型作为起点,并用新数据微调其参数。由于我们替换了最后的线性层,它的参数现在是随机初始化的。
现在我们有一个 ResNet18 模型,所有权重都是在 ImageNet 图像上进行了预训练,除了最后一层。接下来,我们需要用蜜蜂和蚂蚁的图像训练我们的模型。
提示
Torchvision 为计算机视觉和图像处理提供了许多著名的预训练模型,包括以下内容:
-
AlexNet
-
VGG
-
ResNet
-
SqueezeNet
-
DenseNet
-
Inception v3
-
GoogLeNet
-
ShuffleNet v2
-
MobileNet v2
-
ResNeXt
-
Wide ResNet
-
MNASNet
要获取更多信息,请探索torchvision.models类或访问Torchvision models documentation。
训练和验证
在微调我们的模型之前,让我们用以下代码配置我们的训练:
from torch.optim.lr_scheduler import StepLR
device = torch.device("cuda:0" if
torch.cuda.is_available() else "cpu") # ①
model = model.to(device)
criterion = nn.CrossEntropyLoss() # ②
optimizer = optim.SGD(model.parameters(),
lr=0.001,
momentum=0.9) # ③
exp_lr_scheduler = StepLR(optimizer,
step_size=7,
gamma=0.1) # ④
①
如果有的话,将模型移动到 GPU 上。
②
定义我们的损失函数。
③
定义我们的优化器算法。
④
使用学习率调度器。
代码应该看起来很熟悉,除了学习率调度器。在这里,我们将使用 PyTorch 中的调度器来在几个周期后调整 SGD 优化器的学习率。使用学习率调度器将帮助我们的神经网络在训练过程中更精确地调整权重。
以下代码展示了整个训练循环,包括验证:
num_epochs=25
for epoch in range(num_epochs):
model.train() # ①
running_loss = 0.0
running_corrects = 0
for inputs, labels in train_loader:
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
_, preds = torch.max(outputs,1)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()/inputs.size(0)
running_corrects += \
torch.sum(preds == labels.data) \
/inputs.size(0)
exp_lr_scheduler.step() # ②
train_epoch_loss = \
running_loss / len(train_loader)
train_epoch_acc = \
running_corrects / len(train_loader)
model.eval() # ③
running_loss = 0.0
running_corrects = 0
for inputs, labels in val_loader:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs,1)
loss = criterion(outputs, labels)
running_loss += loss.item()/inputs.size(0)
running_corrects += \
torch.sum(preds == labels.data) \
/inputs.size(0)
epoch_loss = running_loss / len(val_loader)
epoch_acc = \
running_corrects.double() / len(val_loader)
print("Train: Loss: {:.4f} Acc: {:.4f}"
" Val: Loss: {:.4f}"
" Acc: {:.4f}".format(train_epoch_loss,
train_epoch_acc,
epoch_loss,
epoch_acc))
①
训练循环。
②
为下一个训练周期调整学习率的计划。
③
验证循环。
我们应该看到训练和验证损失减少,而准确率提高。结果可能会有一些波动。
测试和部署
让我们通过将模型保存到文件来测试我们的模型并部署它。为了测试我们的模型,我们将显示一批图像,并展示我们的模型如何对它们进行分类,如下面的代码所示:
import matplotlib.pyplot as plt
def imshow(inp, title=None): # ①
inp = inp.numpy().transpose((1, 2, 0)) # ②
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inp = std * inp + mean # ③
inp = np.clip(inp, 0, 1)
plt.imshow(inp)
if title is not None:
plt.title(title)
inputs, classes = next(iter(val_loader)) # ④
out = torchvision.utils.make_grid(inputs)
class_names = val_dataset.classes
outputs = model(inputs.to(device)) # ⑤
_, preds = torch.max(outputs,1) # ⑥
imshow(out, title=[class_names[x] for x in preds]) # ⑦
①
定义一个新的函数来绘制我们的张量图像。
②
切换从 C × H × W 到 H × W × C 的图像格式以进行绘图。
③
撤销我们在转换过程中进行的归一化,以便正确查看图像。
④
从我们的验证数据集中获取一批图像。
⑤
使用我们微调的 ResNet18 进行分类。
⑥
选择“获胜”类别。
⑦
显示输入图像及其预测类别。
由于我们有一个如此小的数据集,我们只需通过可视化输出来测试模型,以确保图像与标签匹配。图 4-1 展示了一个测试示例。由于val_loader将返回一个随机抽样的图像批次,您的结果会有所不同。

图 4-1。图像分类结果
完成后,我们保存模型:
torch.save(model.state_dict(), "./resnet18.pt")
您可以将此参考设计用于迁移学习的其他情况,不仅限于图像分类,还包括其他类型的数据。只要您能找到一个合适的预训练模型,您就可以修改模型,并仅使用少量数据重新训练部分模型。
这个例子是基于 Sasank Chilamkurthy 的"计算机视觉迁移学习教程"。您可以在教程中找到更多细节。
接下来,我们将进入 NLP 领域,探索一个处理文本数据的参考设计。
使用 Torchtext 进行情感分析
另一个流行的深度学习应用是情感分析,人们通过对一段文本数据进行分类来判断情感。在这个例子中,我们将训练一个 NN 来预测一部电影评论是积极的还是消极的,使用著名的互联网电影数据库(IMDb)数据集。对 IMDb 数据进行情感分析是学习 NLP 的常见初学者示例。
数据处理
IMDb 数据集包含来自 IMDb 的 25,000 条电影评论,这些评论被标记为情感(例如,积极或消极)。PyTorch 项目包括一个名为Torchtext的库,提供了在文本数据上执行深度学习的便利功能。为了开始我们的示例参考设计,我们将使用 Torchtext 来加载和预处理 IMDb 数据集。
在加载数据集之前,我们将定义一个名为generate_bigrams()的函数,用于预处理我们的文本评论数据。我们将用于此示例的模型计算输入句子的n-gram,并将其附加到末尾。我们将使用 bi-grams,即出现在句子中的单词或标记对。
以下代码展示了我们的预处理函数generate_bigrams(),并提供了它的工作示例:
def generate_bigrams(x):
n_grams = set(zip(*[x[i:] for i in range(2)]))
for n_gram in n_grams:
x.append(' '.join(n_gram))
return x
generate_bigrams([
'This', 'movie', 'is', 'awesome'])
# out:
# ['This', 'movie', 'is', 'awesome', 'This movie',
# 'movie is', 'is awesome']
现在我们已经定义了我们的预处理函数,我们可以构建我们的 IMDb 数据集,如下所示的代码:
from torchtext.datasets import IMDB
from torch.utils.data.dataset import random_split
train_iter, test_iter = IMDB(
split=('train', 'test')) # ①
train_dataset = list(train_iter) # ②
test_data = list(test_iter)
num_train = int(len(train_dataset) * 0.70)
train_data, valid_data = \
random_split(train_dataset,
[num_train,
len(train_dataset) - num_train]) # ③
①
从 IMDb 数据集加载数据。
②
将迭代器重新定义为列表。
③
将训练数据分为两组,70%用于训练,30%用于验证。
在代码中,我们使用IMDB类加载训练和测试数据集。然后我们使用random_split()函数将训练数据分成两个较小的集合,用于训练和验证。
警告
在运行代码时,请确保您至少使用了 Torchtext 0.9,因为 Torchtext API 在 PyTorch 1.8 中发生了重大变化。
让我们快速查看数据:
print(len(train_data), len(valid_data),
len(test_data))
# out:17500 7500 25000
data_index = 21
print(train_data[data_index][0])
# out: (your results may vary)
# pos
print(train_data[data_index][1])
# out: (your results may vary)
# ['This', 'film', 'moved', 'me', 'beyond', ...
如您所见,我们的数据集包括 17,500 条评论用于训练,7,500 条用于验证,25,000 条用于测试。我们还打印了第 21 条评论及其情感,如输出所示。拆分是随机抽样的,因此您的结果可能会有所不同。
接下来,我们需要将文本数据转换为数字数据,以便 NN 可以处理它。我们通过创建预处理函数和数据管道来实现这一点。数据管道将使用我们的generate_bigrams()函数、一个标记器和一个词汇表,如下所示的代码:
from torchtext.data.utils import get_tokenizer
from collections import Counter
from torchtext.vocab import Vocab
tokenizer = get_tokenizer('spacy') # ①
counter = Counter()
for (label, line) in train_data:
counter.update(generate_bigrams(
tokenizer(line))) # ②
vocab = Vocab(counter,
max_size = 25000,
vectors = "glove.6B.100d",
unk_init = torch.Tensor.normal_,) # ③
①
定义我们的分词器(如何分割文本)。
②
列出我们训练数据中使用的所有标记,并计算每个标记出现的次数。
③
创建一个词汇表(可能标记的列表)并定义如何将标记转换为数字。
在代码中,我们定义了将文本转换为张量的指令。对于评论文本,我们指定spaCy作为分词器。spaCy 是一个流行的 Python 包,用于自然语言处理,并包含自己的分词器。分词器将文本分解为词和标点等组件。
我们还创建了一个词汇表和一个嵌入。词汇表只是我们可以使用的一组单词。如果我们在电影评论中发现一个词不在词汇表中,我们将该词设置为一个特殊的单词“未知”。我们将我们的字典限制在 25,000 个单词,远远小于英语语言中的所有单词。
我们还指定了我们的词汇向量,这导致我们下载了一个名为 GloVe(全局词向量表示)的预训练嵌入,具有 100 个维度。下载 GloVe 数据并创建词汇表可能需要几分钟。
嵌入是将单词或一系列单词映射到数值向量的方法。定义词汇表和嵌入是一个复杂的话题,超出了本书的范围。在这个例子中,我们将从训练数据中构建一个词汇表,并下载流行的预训练的 GloVe 嵌入。
现在我们已经定义了我们的分词器和词汇表,我们可以为评论和标签文本数据构建我们的数据管道,如以下代码所示:
text_pipeline = lambda x: [vocab[token]
for token in generate_bigrams(tokenizer(x))]
label_pipeline = lambda x: 1 if x=='pos' else 0
print(text_pipeline('the movie was horrible'))
# out:
print(label_pipeline('neg'))
# out:
我们使用lambda函数通过管道传递文本数据,以便 PyTorch 数据加载器可以将每个文本评论转换为一个 100 元素的向量。
现在我们已经定义了我们的数据集和预处理,我们可以创建我们的数据加载器。我们的数据加载器从数据集的采样中加载数据批次,并预处理数据,如以下代码所示:
from torch.utils.data import DataLoader
from torch.nn.utils.rnn import pad_sequence
device = torch.device("cuda" if
torch.cuda.is_available() else "cpu")
def collate_batch(batch):
label_list, text_list = [], []
for (_label, _text) in batch:
label_list.append(label_pipeline(_label))
processed_text = torch.tensor(
text_pipeline(_text))
text_list.append(processed_text)
return (torch.tensor(label_list,
dtype=torch.float64).to(device),
pad_sequence(text_list,
padding_value=1.0).to(device))
batch_size = 64
def batch_sampler():
indices = [(i, len(tokenizer(s[1])))
for i, s in enumerate(train_dataset)]
random.shuffle(indices)
pooled_indices = []
# create pool of indices with similar lengths
for i in range(0, len(indices), batch_size * 100):
pooled_indices.extend(sorted(
indices[i:i + batch_size * 100], key=lambda x: x[1]))
pooled_indices = [x[0] for x in pooled_indices]
# yield indices for current batch
for i in range(0, len(pooled_indices),
batch_size):
yield pooled_indices[i:i + batch_size]
BATCH_SIZE = 64
train_dataloader = DataLoader(train_data,
# batch_sampler=batch_sampler(),
collate_fn=collate_batch,
batch_size=BATCH_SIZE,
shuffle=True)
# collate_fn=collate_batch)
valid_dataloader = DataLoader(valid_data,
batch_size=BATCH_SIZE,
shuffle=True,
collate_fn=collate_batch)
test_dataloader = DataLoader(test_data,
batch_size=BATCH_SIZE,
shuffle=True,
collate_fn=collate_batch)
在代码中,我们将批处理大小设置为 64,并在有 GPU 时使用。我们还定义了一个名为collate_batch()的整理函数,并将其传递给我们的数据加载器以执行我们的数据管道。
现在我们已经配置了我们的管道和数据加载器,让我们定义我们的模型。
模型设计
在这个例子中,我们将使用一种称为 FastText 的模型,该模型来自 Armand Joulin 等人的论文“高效文本分类的技巧袋”。虽然许多情感分析模型使用 RNN,但这个模型使用了一种更简单的方法。
以下代码实现了 FastText 模型:
import torch.nn as nn
import torch.nn.functional as F
class FastText(nn.Module):
def __init__(self,
vocab_size,
embedding_dim,
output_dim,
pad_idx):
super().__init__()
self.embedding = nn.Embedding(
vocab_size,
embedding_dim,
padding_idx=pad_idx)
self.fc = nn.Linear(embedding_dim,
output_dim)
def forward(self, text):
embedded = self.embedding(text)
embedded = embedded.permute(1, 0, 2)
pooled = F.avg_pool2d(
embedded,
(embedded.shape[1], 1)).squeeze(1)
return self.fc(pooled)
正如您所看到的,该模型使用nn.Embedded层为每个单词计算单词嵌入,然后使用avg_pool2d()函数计算所有单词嵌入的平均值。最后,它通过一个线性层传递平均值。有关此模型的更多详细信息,请参考论文。
让我们使用以下代码构建我们的模型及其适当的参数:
model = FastText(
vocab_size = len(vocab),
embedding_dim = 100,
output_dim = 1,
pad_idx = vocab['<PAD>'])
我们不会从头开始训练我们的嵌入层,而是使用预训练的嵌入来初始化层的权重。这个过程类似于我们在“使用迁移学习进行图像分类”示例中使用预训练权重的方式:
pretrained_embeddings = vocab.vectors # ①
model.embedding.weight.data.copy_(
pretrained_embeddings) # ②
EMBEDDING_DIM = 100
unk_idx = vocab['<UNK>'] # ③
pad_idx = vocab['<PAD>']
model.embedding.weight.data[unk_idx] = \
torch.zeros(EMBEDDING_DIM) # ④
model.embedding.weight.data[pad_idx] = \
torch.zeros(EMBEDDING_DIM)
①
从我们的词汇表中加载预训练的嵌入。
②
初始化嵌入层的权重。
③
将未知标记的嵌入权重初始化为零。
④
将填充标记的嵌入权重初始化为零。
现在它已经正确初始化,我们可以训练我们的模型。
训练和验证
训练和验证过程应该看起来很熟悉。它类似于我们在先前示例中使用的过程。首先,我们配置我们的损失函数和优化算法,如下所示:
import torch.optim as optim
optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()
model = model.to(device)
criterion = criterion.to(device)
在这个例子中,我们使用 Adam 优化器和BCEWithLogitsLoss()损失函数。Adam 优化器是 SGD 的替代品,在处理稀疏或嘈杂梯度时表现更好。BCEWithLogitsLoss()函数通常用于二元分类。我们还将我们的模型移到 GPU(如果可用)。
接下来,我们运行我们的训练和验证循环,如下所示:
for epoch in range(5):
epoch_loss = 0
epoch_acc = 0
model.train()
for label, text, _ in train_dataloader:
optimizer.zero_grad()
predictions = model(text).squeeze(1)
loss = criterion(predictions, label)
rounded_preds = torch.round(
torch.sigmoid(predictions))
correct = \
(rounded_preds == label).float()
acc = correct.sum() / len(correct)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
epoch_acc += acc.item()
print("Epoch %d Train: Loss: %.4f Acc: %.4f" %
(epoch,
epoch_loss / len(train_dataloader),
epoch_acc / len(train_dataloader)))
epoch_loss = 0
epoch_acc = 0
model.eval()
with torch.no_grad():
for label, text, _ in valid_dataloader:
predictions = model(text).squeeze(1)
loss = criterion(predictions, label)
rounded_preds = torch.round(
torch.sigmoid(predictions))
correct = \
(rounded_preds == label).float()
acc = correct.sum() / len(correct)
epoch_loss += loss.item()
epoch_acc += acc.item()
print("Epoch %d Valid: Loss: %.4f Acc: %.4f" %
(epoch,
epoch_loss / len(valid_dataloader),
epoch_acc / len(valid_dataloader)))
# out: (your results may vary)
# Epoch 0 Train: Loss: 0.6523 Acc: 0.7165
# Epoch 0 Valid: Loss: 0.5259 Acc: 0.7474
# Epoch 1 Train: Loss: 0.5935 Acc: 0.7765
# Epoch 1 Valid: Loss: 0.4571 Acc: 0.7933
# Epoch 2 Train: Loss: 0.5230 Acc: 0.8257
# Epoch 2 Valid: Loss: 0.4103 Acc: 0.8245
# Epoch 3 Train: Loss: 0.4559 Acc: 0.8598
# Epoch 3 Valid: Loss: 0.3828 Acc: 0.8549
# Epoch 4 Train: Loss: 0.4004 Acc: 0.8813
# Epoch 4 Valid: Loss: 0.3781 Acc: 0.8675
只需进行五次训练周期,我们应该看到验证准确率在 85-90%左右。让我们看看我们的模型在测试数据集上的表现如何。
测试和部署
早些时候,我们基于 IMDb 测试数据集构建了我们的test_iterator。请记住,测试数据集中的数据没有用于训练或验证。
我们的测试循环如下所示:
test_loss = 0
test_acc = 0
model.eval() # ①
with torch.no_grad(): # ①
for label, text, _ in test_dataloader:
predictions = model(text).squeeze(1)
loss = criterion(predictions, label)
rounded_preds = torch.round(
torch.sigmoid(predictions))
correct = \
(rounded_preds == label).float()
acc = correct.sum() / len(correct)
test_loss += loss.item()
test_acc += acc.item()
print("Test: Loss: %.4f Acc: %.4f" %
(test_loss / len(test_dataloader),
test_acc / len(test_dataloader)))
# out: (your results will vary)
# Test: Loss: 0.3821 Acc: 0.8599
①
对于这个模型来说并不是必需的,但是是一个好的实践。
在前面的代码中,我们一次处理一个批次,并在整个测试数据集上累积准确率。您应该在测试集上获得 85-90%的准确率。
接下来,我们将使用以下代码预测我们自己评论的情感:
import spacy
nlp = spacy.load('en_core_web_sm')
def predict_sentiment(model, sentence):
model.eval()
text = torch.tensor(text_pipeline(
sentence)).unsqueeze(1).to(device)
prediction = torch.sigmoid(model(text))
return prediction.item()
sentiment = predict_sentiment(model,
"Don't waste your time")
print(sentiment)
# out: 4.763594888613835e-34
sentiment = predict_sentiment(model,
"You gotta see this movie!")
print(sentiment)
# out: 0.941755473613739
接近 0 的结果对应于负面评论,而接近 1 的输出表示积极的评论。正如您所看到的,模型正确预测了样本评论的情感。尝试用一些您自己的电影评论来测试它!
最后,我们将保存我们的模型以供部署,如下所示:
torch.save(model.state_dict(), 'fasttext-model.pt')
在这个例子中,您学会了如何预处理文本数据并为情感分析设计了一个 FastText 模型。您还训练了模型,评估了其性能,并保存了模型以供部署。您可以使用这个设计模式和参考代码来解决自己工作中的其他情感分析问题。
这个例子是基于 Ben Trevett 的“更快的情感分析”教程。您可以在他的PyTorch 情感分析 GitHub 存储库中找到更多详细信息和其他优秀的 Torchtext 教程。
让我们继续我们的最终参考设计,我们将使用深度学习和 PyTorch 生成图像数据。
生成学习——使用 DCGAN 生成 Fashion-MNIST 图像
深度学习中最有趣的领域之一是生成学习,其中神经网络用于创建数据。有时,这些神经网络可以创建图像、音乐、文本和时间序列数据,以至于很难区分真实数据和生成数据之间的区别。生成学习用于创建不存在的人和地方的图像,增加图像分辨率,预测视频中的帧,增加数据集,生成新闻文章,以及转换艺术和音乐的风格。
在这一部分,我将向您展示如何使用 PyTorch 进行生成学习。开发过程类似于先前的示例;然而,在这里,我们将使用一种无监督的方法,其中数据没有标记。
此外,我们将设计和训练一个 GAN,这与先前示例中的模型和训练循环有很大不同。测试和评估 GAN 也涉及到稍微不同的过程。总体开发顺序与第二章中的过程一致,但每个部分都将是生成学习的独特部分。
在这个例子中,我们将训练一个 GAN 来生成类似于 Fashion-MNIST 数据集中使用的训练图像的图像。Fashion-MNIST 是一个用于图像分类的流行学术数据集,包括服装的图像。让我们访问 Fashion-MNIST 数据,看看这些图像是什么样子,然后我们将根据我们看到的内容创建一些合成图像。
数据处理
与用于监督学习的模型不同,那里模型学习数据和标签之间的关系,生成模型旨在学习训练数据的分布,以便生成类似于手头训练数据的数据。因此,在这个例子中,我们只需要训练数据,因为如果我们构建一个好的模型并训练足够长的时间,模型应该开始产生良好的合成数据。
首先让我们导入所需的库,定义一些常量,并设置我们的设备:
import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
CODING_SIZE = 100
BATCH_SIZE = 32
IMAGE_SIZE = 64
device = torch.device("cuda:0" if
torch.cuda.is_available() else "cpu")
以下代码加载训练数据,定义了转换操作,并创建了一个用于批量迭代的数据加载器:
transform = transforms.Compose([
transforms.Resize(IMAGE_SIZE),
transforms.ToTensor(),
])
dataset = datasets.FashionMNIST(
'./',
train=True,
download=True,
transform=transform)
dataloader = DataLoader(
dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=8)
这段代码应该对你来说很熟悉。我们再次使用 Torchvision 函数来定义转换、创建数据集,并设置一个数据加载器,该加载器将对数据集进行采样,应用转换,并为我们的模型返回一批图像。
我们可以使用以下代码显示一批图像:
from torchvision.utils import make_grid
import matplotlib.pyplot as plt
data_batch, labels_batch = next(iter(dataloader))
grid_img = make_grid(data_batch, nrow=8)
plt.imshow(grid_img.permute(1, 2, 0))
Torchvision 提供了一个很好的实用工具叫做make_grid来显示一组图像。图 4-2 展示了一个 Fashion-MNIST 图像的示例批次。

图 4-2. Fashion-MNIST 图像
让我们看看我们将用于数据生成任务的模型。
模型设计
为了生成新的图像数据,我们将使用 GAN。GAN 模型的目标是基于训练数据的分布生成“假”数据。GAN 通过两个不同的模块实现这一目标:生成器和鉴别器。
生成器的工作是生成看起来真实的假图像。鉴别器的工作是正确识别图像是否为假的。尽管 GAN 的设计超出了本书的范围,但我将提供一个使用深度卷积 GAN(DCGAN)的示例参考设计。
注意
GAN 首次在 Ian Goodfellow 等人于 2014 年发表的著名论文中描述,标题为“生成对抗网络”。Alec Radford 等人在 2015 年的论文中提出了构建更稳定的卷积 GAN 的指导方针,标题为“使用深度卷积生成对抗网络进行无监督表示学习”。本例中使用的 DCGAN 在这篇论文中有描述。
生成器被设计为从一个包含 100 个随机值的输入向量创建图像。以下是代码:
import torch.nn as nn
class Generator(nn.Module):
def __init__(self, coding_sz):
super(Generator, self).__init__()
self.net = nn.Sequential(
nn.ConvTranspose2d(coding_sz,
1024, 4, 1, 0),
nn.BatchNorm2d(1024),
nn.ReLU(),
nn.ConvTranspose2d(1024,
512, 4, 2, 1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.ConvTranspose2d(512,
256, 4, 2, 1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.ConvTranspose2d(256,
128, 4, 2, 1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.ConvTranspose2d(128,
1, 4, 2, 1),
nn.Tanh()
)
def forward(self, input):
return self.net(input)
netG = Generator(CODING_SIZE).to(device)
这个示例生成器使用 2D 卷积转置层与批量归一化和 ReLU 激活。这些层在__init__()函数中定义。它的工作方式类似于我们的图像分类模型,只是顺序相反。
也就是说,它不是将图像缩小为较小的表示,而是从一个随机向量创建完整的图像。我们还将Generator模块实例化为netG。
接下来,我们创建Discriminator模块,如下所示的代码:
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator,
self).__init__()
self.net = nn.Sequential(
nn.Conv2d(1, 128, 4, 2, 1),
nn.LeakyReLU(0.2),
nn.Conv2d(128, 256, 4, 2, 1),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2),
nn.Conv2d(256, 512, 4, 2, 1),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2),
nn.Conv2d(512, 1024, 4, 2, 1),
nn.BatchNorm2d(1024),
nn.LeakyReLU(0.2),
nn.Conv2d(1024, 1, 4, 1, 0),
nn.Sigmoid()
)
def forward(self, input):
return self.net(input)
netD = Discriminator().to(device)
鉴别器是一个二元分类网络,确定输入图像是真实的概率。这个示例鉴别器 NN 使用 2D 卷积层与批量归一化和泄漏 ReLU 激活函数。我们将Discriminator实例化为netD。
DCGAN 论文的作者发现,初始化权重有助于提高性能,如下所示的代码:
def weights_init(m):
classname = m.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(m.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
netG.apply(weights_init)
netD.apply(weights_init)
现在我们已经设计好了两个模块,我们可以设置并训练 GAN。
训练
训练 GAN 比之前的训练示例要复杂一些。在每个时代中,我们将首先用真实数据批次训练鉴别器,然后使用生成器创建一个假批次,然后用生成的假数据批次训练鉴别器。最后,我们将训练生成器 NN 以生成更好的假数据。
这是一个很好的例子,展示了 PyTorch 在创建自定义训练循环时的强大功能。它提供了灵活性,可以轻松开发和实现新的想法。
在开始训练之前,我们需要定义用于训练生成器和鉴别器的损失函数和优化器:
from torch import optim
criterion = nn.BCELoss()
optimizerG = optim.Adam(netG.parameters(),
lr=0.0002,
betas=(0.5, 0.999))
optimizerD = optim.Adam(netD.parameters(),
lr=0.0001,
betas=(0.5, 0.999))
在前面的代码中,我们为真实与假图像定义了一个标签。然后我们使用二元交叉熵(BCE)损失函数,这是用于二元分类的常用函数。请记住,鉴别器通过将图像分类为真实或假来执行二元分类。我们使用常用的 Adam 优化器来更新模型参数。
让我们为真实和假标签定义值,并创建用于计算损失的张量:
real_labels = torch.full((BATCH_SIZE,),
1.,
dtype=torch.float,
device=device)
fake_labels = torch.full((BATCH_SIZE,),
0.,
dtype=torch.float,
device=device)
在开始训练之前,我们将创建用于存储错误的列表,并定义一个测试向量以后显示结果:
G_losses = []
D_losses = []
D_real = []
D_fake = []
z = torch.randn((
BATCH_SIZE, 100)).view(-1, 100, 1, 1).to(device)
test_out_images = []
现在我们可以执行训练循环。如果 GAN 是稳定的,随着更多时代的训练,它应该会改进。以下是训练循环的代码:
N_EPOCHS = 5
for epoch in range(N_EPOCHS):
print(f'Epoch: {epoch}')
for i, batch in enumerate(dataloader):
if (i%200==0):
print(f'batch: {i} of {len(dataloader)}')
# Train Discriminator with an all-real batch.
netD.zero_grad()
real_images = batch[0].to(device) *2. - 1.
output = netD(real_images).view(-1) # ①
errD_real = criterion(output, real_labels)
D_x = output.mean().item()
# Train Discriminator with an all-fake batch.
noise = torch.randn((BATCH_SIZE,
CODING_SIZE))
noise = noise.view(-1,100,1,1).to(device)
fake_images = netG(noise)
output = netD(fake_images).view(-1) # ②
errD_fake = criterion(output, fake_labels)
D_G_z1 = output.mean().item()
errD = errD_real + errD_fake
errD.backward(retain_graph=True) # ③
optimizerD.step()
# Train Generator to generate better fakes.
netG.zero_grad()
output = netD(fake_images).view(-1) # ④
errG = criterion(output, real_labels) # ⑤
errG.backward() # ⑥
D_G_z2 = output.mean().item()
optimizerG.step()
# Save losses for plotting later.
G_losses.append(errG.item())
D_losses.append(errD.item())
D_real.append(D_x)
D_fake.append(D_G_z2)
test_images = netG(z).to('cpu').detach() # ⑦
test_out_images.append(test_images)
①
将真实图像传递给“鉴别器”。
②
将假图像传递给“鉴别器”。
③
运行反向传播并更新“鉴别器”。
④
将假图像传递给更新后的“鉴别器”。
⑤
“生成器”的损失基于“鉴别器”错误的情况。
⑥
运行反向传播并更新“生成器”。
⑦
创建一批图像并在每个时代后保存它们。
与之前的示例一样,我们循环遍历所有数据,每次一个批次,在每个时代使用数据加载器。首先,我们用一批真实图像训练鉴别器,以便它可以计算输出,计算损失并计算梯度。然后我们用一批假图像训练鉴别器。
假图像是由生成器从随机值向量创建的。再次,我们计算鉴别器输出,计算损失并计算梯度。接下来,我们添加所有真实和所有假批次的梯度,并应用反向传播。
我们使用刚训练的鉴别器从相同的假数据计算输出,并计算生成器的损失或错误。利用这个损失,我们计算梯度并在生成器本身上应用反向传播。
最后,我们将跟踪每个时代后的损失,以查看 GAN 的训练是否持续改进和稳定。图 4-3 显示了生成器和鉴别器在训练过程中的损失曲线。

图 4-3. GAN 训练曲线
损失曲线绘制了每个批次在所有时代中的生成器和鉴别器损失,因此损失会根据批次的计算损失而波动。不过我们可以看到,两种情况下的损失都从训练开始时减少了。如果我们训练更多时代,我们会期待这些损失值接近零。
总的来说,GAN 很难训练,学习率、beta 和其他优化器超参数可能会产生重大影响。
让我们检查鉴别器在每个批次上所有时代的平均结果,如图 4-4 所示。

图 4-4. 鉴别器结果
如果 GAN 完美的话,鉴别器将无法正确识别假图像为假或真实图像为真,我们期望在这两种情况下平均误差为 0.5。结果显示有些批次接近 0.5,但我们肯定可以做得更好。
现在我们已经训练了我们的网络,让我们看看它在创建服装的假图像方面表现如何。
测试和部署
在监督学习中,我们通常留出一个未用于训练或验证模型的测试数据集。在生成式学习中,生成器没有生成标签。我们可以将生成的图像传递给 Fashion-MNIST 分类器,但除非我们手动标记输出,否则我们无法知道错误是由分类器还是 GAN 引起的。
现在,让我们通过比较第一个时代的结果和最后一个时代生成的图像来测试和评估我们的 GAN。我们为测试创建一个名为z的测试向量,并在我们的训练循环代码中使用每个时代末尾计算的生成器结果。
图 4-5 显示了第一个时代生成的图像,而图 4-6 显示了仅训练五个时代后的结果。

图 4-5. 生成器结果(第一个时代)

图 4-6. 生成器结果(最后一个时代)
您可以看到生成器有所改进。看看第二行末尾的靴子或第三行末尾的衬衫。我们的 GAN 并不完美,但在只经过五个时代后似乎有所改善。训练更多时代或改进我们的设计可能会产生更好的结果。
最后,我们可以保存我们训练好的模型以供部署,并使用以下代码生成更多合成的 Fashion-MNIST 图像:
torch.save(netG.state_dict(), './gan.pt')
我们通过设计和训练一个 GAN 来扩展了我们的 PyTorch 深度学习能力,在这个生成式学习参考设计中。您可以使用这个参考设计来创建和训练其他 GAN 模型,并测试它们生成新数据的性能。
在本章中,我们涵盖了更多示例,展示了使用 PyTorch 的各种数据处理、模型设计和训练方法,但是如果您有一个新颖的、创新的 NN 的惊人想法呢?或者如果您想出了一个新的优化算法或损失函数,以前没有人见过的呢?在下一章中,我将向您展示如何创建自己的自定义模块和函数,以便扩展您的深度学习研究并尝试新的想法。
第五章:自定义 PyTorch
到目前为止,您一直在使用内置的 PyTorch 类、函数和库来设计和训练各种预定义模型、模型层和激活函数。但是,如果您有一个新颖的想法或正在进行前沿的深度学习研究怎么办?也许您发明了一个全新的层架构或激活函数。也许您开发了一个新的优化算法或一个以前从未见过的特殊损失函数。
在本章中,我将向您展示如何在 PyTorch 中创建自定义的深度学习组件和算法。我们将首先探讨如何创建自定义层和激活函数,然后看看如何将这些组件组合成自定义模型架构。接下来,我将向您展示如何创建自定义的损失函数和优化算法。最后,我们将看看如何创建用于训练、验证和测试的自定义循环。
PyTorch 提供了灵活性:您可以扩展现有库,也可以将自定义内容组合到自己的库或包中。通过创建自定义组件,您可以解决新的深度学习问题,加快训练速度,并发现执行深度学习的创新方法。
让我们开始创建一些自定义深度学习层和激活函数。
自定义层和激活
PyTorch 提供了一套广泛的内置层和激活函数。然而,PyTorch 如此受欢迎,尤其是在研究社区中,是因为创建自定义层和激活如此简单。这样做的能力可以促进实验并加速您的研究。
如果我们查看 PyTorch 源代码,我们会看到层和激活是使用功能定义和类实现创建的。功能定义指定基于输入创建输出的方式。它在nn.functional模块中定义。类实现用于创建调用此函数的对象,但它还包括从nn.Module类派生的附加功能。
例如,让我们看看全连接的nn.Linear层是如何实现的。以下代码显示了功能定义nn.functional.linear()的简化版本:
import torch
def linear(input, weight, bias=None):
if input.dim() == 2 and bias is not None:
# fused op is marginally faster
ret = torch.addmm(bias, input, weight.t())
else:
output = input.matmul(weight.t())
if bias is not None:
output += bias
ret = output
return ret
linear()函数将输入张量乘以权重矩阵,可选择添加偏置向量,并将结果返回为张量。您可以看到代码针对性能进行了优化。当输入具有两个维度且没有偏置时,应使用融合矩阵加函数torch.addmm(),因为在这种情况下速度更快。
将数学计算保留在单独的功能定义中有一个好处,即将优化与层nn.Module分开。功能定义也可以在一般编写代码时作为独立函数使用。
然而,我们通常会使用nn.Module类来对我们的神经网络进行子类化。当我们创建一个nn.Module子类时,我们获得了nn.Module对象的所有内置优势。在这种情况下,我们从nn.Module派生nn.Linear类,如下面的代码所示:
import torch.nn as nn
from torch import Tensor
class Linear(nn.Module):
def __init__(self, in_features,
out_features, bias): # ①
super(Linear, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.weight = Parameter(
torch.Tensor(out_features,
in_features))
if bias:
self.bias = Parameter(
torch.Tensor(out_features))
else:
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
init.kaiming_uniform_(self.weight,
a=math.sqrt(5))
if self.bias is not None:
fan_in, _ = \
init._calculate_fan_in_and_fan_out(
self.weight)
bound = 1 / math.sqrt(fan_in)
init.uniform_(self.bias, -bound, bound)
def forward(self, input: Tensor) -> Tensor: # ②
return F.linear(input,
self.weight,
self.bias) # ③
①
初始化输入和输出大小、权重和偏置。
②
定义前向传递。
③
使用linear()的功能定义。
nn.Linear代码包括任何nn.Module子类所需的两种方法。一种是__init__(),它初始化类属性,即在这种情况下的输入、输出、权重和偏置。另一种是forward()方法,它定义了前向传递期间的处理。
如前面的代码所示,forward()方法经常调用与层相关的nn.functional定义。这种约定在 PyTorch 代码中经常使用于层。
创建自定义层的约定是首先创建一个实现数学运算的函数,然后创建一个nn.Module子类,该子类使用这个函数来实现层类。使用这种方法可以很容易地在 PyTorch 模型开发中尝试新的层设计。
自定义层示例(复杂线性)
接下来,我们将看看如何创建一个自定义层。在这个例子中,我们将为一种特殊类型的数字——复数创建自己的线性层。复数经常在物理学和信号处理中使用,由一对数字组成——一个“实”部分和一个“虚”部分。这两个部分都是浮点数。
PyTorch 正在添加对复杂数据类型的支持;然而,在撰写本书时,它们仍处于测试阶段。因此,我们将使用两个浮点张量来实现它们,一个用于实部,一个用于虚部。
在这种情况下,输入、权重、偏置和输出都将是复数,并且将由两个张量组成,而不是一个。复数乘法得到以下方程(其中 j 是复数 ):
首先,我们将创建一个复杂线性层的函数版本,如下面的代码所示:
def complex_linear(in_r, in_i, w_r, w_i, b_i, b_r):
out_r = (in_r.matmul(w_r.t())
- in_i.matmul(w_i.t()) + b_r)
out_i = (in_r.matmul(w_i.t())
- in_i.matmul(w_r.t()) + b_i)
return out_r, out_i
如你所见,该函数将复杂乘法公式应用于张量数组。接下来,我们根据nn.Module创建ComplexLinear的类版本,如下面的代码所示:
class ComplexLinear(nn.Module):
def __init__(self, in_features, out_features):
super(Linear, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.weight_r = \
Parameter(torch.randn(out_features,
in_features))
self.weight_i = \
Parameter(torch.randn(out_features,
in_features))
self.bias_r = Parameter(
torch.randn(out_features))
self.bias_i = Parameter(
torch.randn(out_features))
def forward(self, in_r, in_i):
return F.complex_linear(in_r, in_i,
self.weight_r, self.weight_i,
self.bias_r, self.bias_i)
在我们的类中,我们在__init__()函数中为实部和虚部定义了单独的权重和偏置。请注意,in_features和out_features的选项数量不会改变,因为实部和虚部的数量是相同的。我们的forward()函数只是调用我们的复杂乘法和加法操作的函数定义。
请注意,我们也可以使用 PyTorch 现有的nn.Linear层来构建我们的层,如下面的代码所示:
class ComplexLinearSimple(nn.Module):
def __init__(self, in_features, out_features):
super(ComplexLinearSimple, self).__init__()
self.fc_r = Linear(in_features,
out_features)
self.fc_i = Linear(in_features,
out_features)
def forward(self,in_r, in_i):
return (self.fc_r(in_r) - self.fc_i(in_i),
self.fc_r(in_i)+self.fc_i(in_r))
在这段代码中,我们可以免费获得nn.Linear的所有附加好处,而无需实现新的函数定义。当你创建自己的自定义层时,检查 PyTorch 的内置层,看看是否可以重用现有的类。
即使这个例子非常简单,你也可以使用相同的方法来创建更复杂的层。此外,相同的方法也可以用来创建自定义激活函数。
激活函数与 NN 层非常相似,它们通过对一组输入执行数学运算来返回输出。它们的不同之处在于,操作是逐元素执行的,并且不包括在训练过程中调整的权重和偏置等参数。因此,激活函数可以仅使用函数版本执行。
例如,让我们来看看 ReLU 激活函数。ReLU 函数对于负值为零,对于正值为线性:
def my_relu(input, thresh=0.0):
return torch.where(
input > thresh,
input,
torch.zeros_like(input))
当激活函数具有可配置参数时,通常会创建一个类版本。我们可以通过创建一个 ReLU 类来添加调整 ReLU 函数的阈值和值的功能,如下所示:
class MyReLU(nn.Module):
def __init__(self, thresh = 0.0):
super(MyReLU, self).__init__()
self.thresh = thresh
def forward(self, input):
return my_relu(input, self.thresh)
在构建 NN 时,通常使用激活函数的函数版本,但如果有的话也可以使用类版本。以下代码片段展示了如何使用torch.nn中包含的 ReLU 激活的两个版本。
这是函数版本:
import torch.nn.functional as F # ①
class SimpleNet(nn.Module):
def __init__(self, D_in, H, D_out):
super(SimpleNet, self).__init__()
self.fc1 = nn.Linear(D_in, H)
self.fc2 = nn.Linear(H, D_out)
def forward(self, x):
x = F.relu(self.fc1(x)) # ②
return self.fc2(x)
①
导入函数包的常见方式。
②
这里使用了 ReLU 的函数版本。
这是类版本:
class SimpleNet(nn.Module):
def __init__(self, D_in, H, D_out):
super(SimpleNet, self).__init__()
self.net = nn.Sequential( # ①
nn.Linear(D_in, H),
nn.ReLU(), # ②
nn.Linear(H, D_out)
)
def forward(self, x):
return self.net(x)
①
我们使用nn.Sequential()因为所有组件都是类。
②
我们正在使用 ReLU 的类版本。
自定义激活函数示例(Complex ReLU)
我们可以创建自己的自定义 ComplexReLU 激活函数来处理我们之前创建的ComplexLinear层中的复数值。以下代码展示了函数版本和类版本:
def complex_relu(in_r, in_i): # ①
return (F.relu(in_r), F.relu(in_i))
class ComplexReLU(nn.Module): # ②
def __init__(self):
super(ComplexReLU, self).__init__()
def forward(self, in_r, in_i):
return complex_relu(in_r, in_i)
①
函数版本
②
类版本
现在您已经学会了如何创建自己的层和激活函数,让我们看看如何创建自己的自定义模型架构。
自定义模型架构
在第二章和第三章中,我们使用了内置模型并从内置 PyTorch 层创建了自己的模型。在本节中,我们将探讨如何创建类似于torchvision.models的模型库,并构建灵活的模型类,根据用户提供的配置参数调整架构。
torchvision.models包提供了一个AlexNet模型类和一个alexnet()便利函数来方便其使用。让我们先看看AlexNet类:
class AlexNet(nn.Module):
def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11,
stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 192, kernel_size=5,
padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3,
padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3,
padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3,
padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
与所有层、激活函数和模型一样,AlexNet类派生自nn.Module类。AlexNet类是如何创建和组合子模块成为 NN 的一个很好的例子。
该库定义了三个子网络——features、avgpool和classifier。每个子网络由 PyTorch 层和激活函数组成,并按顺序连接。AlexNet 的forward()函数描述了前向传播;即输入如何被处理以形成输出。
在这种情况下,PyTorch 的torchvision.models代码提供了一个方便的函数alexnet()来实例化或创建模型并提供一些选项。这里的选项是pretrained和progress;它们确定是否加载具有预训练参数的模型以及是否显示进度条:
from torch.hub import load_state_dict_from_url
model_urls = {
'alexnet':
'https://pytorch.tips/alexnet-download',
}
def alexnet(pretrained=False,
progress=True, **kwargs):
model = AlexNet(**kwargs)
if pretrained:
state_dict = load_state_dict_from_url(
model_urls['alexnet'],
progress=progress)
model.load_state_dict(state_dict)
return model
**kwargs参数允许您向 AlexNet 模型传递其他选项。在这种情况下,您可以使用alexnet(n_classes = 10)将类别数更改为 10。该函数将使用n_classes = 10实例化 AlexNet 模型并返回模型对象。如果pretrained为True,函数将从指定的 URL 加载权重。
通过采用类似的方法,您可以创建自己的模型架构。创建一个从nn.Module派生的顶级模型。定义您的__init__()和forward()函数,并根据子网络、层和激活函数实现您的 NN。您的子网络、层和激活函数甚至可以是您自己创建的自定义的。
如您所见,nn.Module类使创建自定义模型变得容易。除了Module类外,torch.nn包还包括内置的损失函数。让我们看看如何创建自己的损失函数。
自定义损失函数
如果您回忆一下第三章,在训练 NN 模型之前,我们需要定义损失函数。损失函数或成本函数定义了我们在训练过程中希望通过调整模型权重来最小化的度量。
起初,损失函数可能看起来只是一个函数定义,但请记住,损失函数是 NN 模块参数的函数。
因此,损失函数实际上就像是一个额外的层,将 NN 的输出作为输入,并产生一个度量作为其输出。当我们进行反向传播时,我们是在损失函数上进行反向传播,而不是在 NN 上。
这使我们能够直接调用该类来计算给定 NN 输出和真实值的损失。然后我们可以一次计算所有 NN 参数的梯度,即进行反向传播。以下代码展示了如何在代码中实现这一点:
loss_fcn = nn.MSELoss() # ①
loss = loss_fcn(outputs, targets)
loss.backward()
①
有时称为criterion
首先我们实例化损失函数本身,然后调用该函数,传入输出(来自我们的模型)和目标值(来自我们的数据)。最后,我们调用backward()方法进行反向传播,并计算所有模型参数相对于损失的梯度。
与之前讨论的层类似,损失函数使用功能定义和从nn.Module类派生的类实现来实现。
mse_loss的功能定义和类实现的简化版本如下所示:
def mse_loss(input, target):
return ((input-target)**2).mean()
class MSELoss(nn.Module):
def __init__(self):
super(MSELoss, self).__init__()
def forward(self, input, target):
return F.mse_loss(input, target)
让我们创建自己的损失函数,复数的 MSE 损失。为了创建自定义损失函数,我们首先定义一个数学上描述损失函数的功能定义。然后我们将创建损失函数类,如下所示:
def complex_mse_loss(input_r, input_i,
target_r, target_i):
return (((input_r-target_r)**2).mean(),
((input_i-target_i)**2).mean())
class ComplexMSELoss(nn.Module):
def __init__(self, real_only=False):
super(ComplexMSELoss, self).__init__()
self.real_only = real_only
def forward(self, input_r, input_i,
target_r, target_i):
if (self.real_only):
return F.mse_loss(input_r, target_r)
else:
return complex_mse_loss(
input_r, input_i,
target_r, target_i)
这次,我们在类中创建了一个名为real_only的可选设置。当我们使用real_only = True实例化损失函数时,将使用mse_loss()函数而不是complex_mse_loss()函数。
正如您所看到的,PyTorch 在构建自定义模型架构和损失函数方面提供了出色的灵活性。在进行训练之前,还有一个函数可以自定义:优化器。让我们看看如何创建自定义优化器。
自定义优化器算法
优化器在训练 NN 模型中起着重要作用。优化器是在训练过程中更新模型参数的算法。当我们使用loss.backward()进行反向传播时,我们确定参数应该增加还是减少以最小化损失。优化器使用梯度来确定在每一步中参数应该改变多少并进行相应的更改。
PyTorch 有自己的子模块称为torch.optim,其中包含许多内置的优化器算法,正如我们在第三章中所看到的。要创建一个优化器,我们传入我们模型的参数和任何特定于优化器的选项。例如,以下代码创建了一个学习率为 0.01 和动量值为 0.9 的 SGD 优化器:
from torch import optim
optimizer = optim.SGD(model.parameters(),
lr=0.01, momentum=0.9)
在 PyTorch 中,我们还可以为不同的参数指定不同的选项。当您想要为模型的不同层指定不同的学习率时,这是很有用的。每组参数称为参数组。我们可以使用字典指定不同的选项,如下所示:
optim.SGD([
{'params':
model.features.parameters()},
{'params':
model.classifier.parameters(),
'lr': 1e-3}
], lr=1e-2, momentum=0.9)
假设我们正在使用 AlexNet 模型,上述代码将分类器层的学习率设置为1e-3,并使用默认学习率1e-2来训练特征层。
PyTorch 提供了一个torch.optim.Optimizer基类,以便轻松创建自定义优化器。以下是Optimizer基类的简化版本:
from collections import defaultdict
class Optimizer(object):
def __init__(self, params, defaults):
self.defaults = defaults
self.state = defaultdict(dict) # ①
self.param_groups = [] # ②
param_groups = list(params)
if len(param_groups) == 0:
raise ValueError(
"""optimizer got an
empty parameter list""")
if not isinstance(param_groups[0], dict):
param_groups = [{'params': param_groups}]
for param_group in param_groups:
self.add_param_group(param_group)
def __getstate__(self):
return {
'defaults': self.defaults,
'state': self.state,
'param_groups': self.param_groups,
}
def __setstate__(self, state):
self.__dict__.update(state)
def zero_grad(self): # ③
for group in self.param_groups:
for p in group['params']:
if p.grad is not None:
p.grad.detach_()
p.grad.zero_()
def step(self, closure): # ④
raise NotImplementedError
①
根据需要定义state。
②
根据需要定义 param_groups。
③
根据需要定义 zero_grad()。
④
您需要编写自己的 step()。
优化器有两个主要属性或组件:state 和 param_groups。state 属性是一个字典,可以在不同的优化器之间变化。它主要用于在每次调用 step() 函数之间维护值。param_groups 属性也是一个字典。它包含参数本身以及每个组的相关选项。
Optimizer 基类中的重要方法是 zero_grad() 和 step()。zero_grad() 方法用于在每次训练迭代期间将梯度归零或重置。step() 方法用于执行优化器算法,计算每个参数的变化,并更新模型对象中的参数。zero_grad() 方法已经为您实现。但是,当创建自定义优化器时,您必须创建自己的 step() 方法。
让我们通过创建我们自己简单版本的 SGD 来演示这个过程。我们的 SDG 优化器将有一个选项——学习率(LR)。在每个优化器步骤中,我们将梯度乘以 LR,并将其添加到参数中(即,调整模型的权重):
from torch.optim import Optimizer
class SimpleSGD(Optimizer):
def __init__(self, params, lr='required'):
if lr is not 'required' and lr < 0.0:
raise ValueError(
"Invalid learning rate: {}".format(lr))
defaults = dict(lr=lr)
super(SimpleSGD, self).__init__(
params, defaults)
def step(self):
for group in self.param_groups:
for p in group['params']:
if p.grad is None:
continue
d_p = p.grad
p.add_(d_p, alpha=-group['lr'])
return
__init__() 函数设置默认选项值,并根据输入参数初始化参数组。请注意,我们不必编写任何代码来执行此操作,因为 super(SGD, self).init(params, defaults) 调用基类初始化方法。我们真正需要做的是编写 step() 方法。对于每个参数组,我们首先将参数乘以组的 LR,然后从参数本身减去该乘积。这通过调用 p.add_(d_p, alpha=-group['lr']) 完成。
以下是我们如何使用新优化器的示例:
optimizer = SimpleSGD(model.parameters(),
lr=0.001)
我们还可以使用以下代码为模型的不同层定义不同的学习率。在这里,我们假设再次使用 AlexNet 作为模型,其中包含名为 feature 和 classifier 的层:
optimizer = SimpleSGD([
{'params':
model.features.parameters()},
{'params':
model.classifier.parameters(),
'lr': 1e-3}
], lr=1e-2)
现在您可以为训练模型创建自己的优化器了,让我们看看如何创建自己的自定义训练、验证和测试循环。
自定义训练、验证和测试循环
在整本书中,我们一直在使用自定义训练、验证和测试循环。这是因为在 PyTorch 中,所有训练、验证和测试循环都是由程序员手动创建的。
与 Keras 不同,没有 fit() 或 eval() 方法来执行循环。相反,PyTorch 要求您编写自己的循环。在许多情况下,这实际上是一个好处,因为您希望控制训练过程中发生的事情。
实际上,在“生成学习—使用 DCGAN 生成 Fashion-MNIST 图像”中的参考设计演示了如何创建更复杂的训练循环。
在本节中,我们将探讨编写循环的传统方式,并讨论开发人员定制循环的常见方式。让我们回顾一些用于训练、验证和测试循环的常用代码:
for epoch in range(n_epochs):
# Training
for data in train_dataloader:
input, targets = data
optimizer.zero_grad()
output = model(input)
train_loss = criterion(output, targets)
train_loss.backward()
optimizer.step()
# Validation
with torch.no_grad():
for input, targets in val_dataloader:
output = model(input)
val_loss = criterion(output, targets)
# Testing
with torch.no_grad():
for input, targets in test_dataloader:
output = model(input)
test_loss = criterion(output, targets)
这段代码应该看起来很熟悉,因为我们在整本书中经常使用它。我们假设 n_epochs、model、criterion、optimizer、train_、val_ 和 test_dataloader 已经定义。对于每个时期,我们执行训练和验证循环。训练循环逐批次处理每个批次,将批次输入通过模型,并计算损失。然后我们执行反向传播来计算梯度,并执行优化器来更新模型的参数。
验证循环禁用梯度计算,并逐批次将验证数据通过网络传递。测试循环逐批次将测试数据通过模型传递,并计算测试数据的损失。
让我们为我们的循环添加一些额外的功能。可能性是无限的,但这个示例将演示一些简单的任务,比如打印信息、重新配置模型以及在训练过程中调整超参数。让我们走一遍以下代码,看看如何实现这一点:
for epoch in range(n_epochs):
total_train_loss = 0.0 # ①
total_val_loss = 0.0 # ①
if (epoch == epoch//2):
optimizer = optim.SGD(model.parameters(),
lr=0.001) # ②
# Training
model.train() # ③
for data in train_dataloader:
input, targets = data
optimizer.zero_grad()
output = model(input)
train_loss = criterion(output, targets)
train_loss.backward()
optimizer.step()
total_train_loss += train_loss # ①
# Validation
model.eval() # ③
with torch.no_grad():
for input, targets in val_dataloader:
output = model(input)
val_loss = criterion(output, targets)
total_val_loss += val_loss # ①
print("""Epoch: {}
Train Loss: {}
Val Loss {}""".format(
epoch, total_train_loss,
total_val_loss)) # ①
# Testing
model.eval()
with torch.no_grad():
for input, targets in test_dataloader:
output = model(input)
test_loss = criterion(output, targets)
①
打印 epoch、训练和验证损失的示例
②
重新配置模型的示例(最佳实践)
③
修改训练过程中的超参数示例
在上述代码中,我们添加了一些变量来跟踪运行的训练和验证损失,并在每个 epoch 打印它们。接下来,我们使用 train() 或 eval() 方法来配置模型进行训练或评估。这仅适用于模型的 forward() 函数在训练和评估时表现不同的情况。
例如,一些模型可能在训练过程中使用 dropout,但在验证或测试过程中不应用 dropout。在这种情况下,我们可以通过调用 model.train() 或 model.eval() 来重新配置模型,然后执行它。
最后,我们在训练过程中修改了优化器中的学习率。这使我们能够在一半的 epoch 训练后以更快的速度训练,同时在微调参数更新之后。
这个示例是如何自定义您的循环的简单演示。训练、验证和测试循环可能会更加复杂,因为您同时训练多个网络、使用多模态数据,或设计更复杂的网络,甚至可以训练其他网络。PyTorch 提供了设计用于训练、验证和测试的特殊和创新过程的灵活性。
提示
PyTorch Lightning 是一个第三方 PyTorch 包,提供了用于训练、验证和测试循环的样板模板。该包提供了一个框架,允许您创建自定义循环,而无需为每个模型实现重复输入样板代码。我们将在第八章中讨论 PyTorch Lightning。您也可以在PyTorch Lightning 网站上找到更多信息。
在本章中,您学习了如何为在 PyTorch 中开发深度学习模型创建自定义组件。随着您的模型变得越来越复杂,您可能会发现您需要训练模型的时间可能会变得相当长——也许是几天甚至几周。在下一章中,您将看到如何利用内置的 PyTorch 能力来加速和优化您的训练过程,从而显著减少整体模型开发时间。
第六章:PyTorch 加速和优化
在前几章中,您学习了如何使用 PyTorch 的内置功能,并通过创建自己的自定义组件来扩展这些功能,从而使您能够快速设计新模型和算法来训练它们。
然而,当处理非常大的数据集或更复杂的模型时,将模型训练在单个 CPU 或 GPU 上可能需要非常长的时间——可能需要几天甚至几周才能获得初步结果。训练时间更长可能会变得令人沮丧,特别是当您想要使用不同的超参数配置进行许多实验时。
在本章中,我们将探讨使用 PyTorch 加速和优化模型开发的最新技术。首先,我们将看看如何使用张量处理单元(TPU)而不是 GPU 设备,并考虑在使用 TPU 时可以提高性能的情况。接下来,我将向您展示如何使用 PyTorch 的内置功能进行并行处理和分布式训练。这将为跨多个 GPU 和多台机器训练模型提供一个快速参考,以便在更多硬件资源可用时快速扩展您的训练。在探索加速训练的方法之后,我们将看看如何使用高级技术(如超参数调整、量化和剪枝)来优化您的模型。
本章还将提供参考代码,以便轻松入门,并提供我们使用的关键软件包和库的参考资料。一旦您创建了自己的模型和训练循环,您可以返回到本章获取有关如何加速和优化训练过程的提示。
让我们开始探讨如何在 TPU 上运行您的模型。
在 TPU 上的 PyTorch
随着深度学习和人工智能的不断部署,公司正在开发定制硬件芯片或 ASIC,旨在优化硬件中的模型性能。谷歌开发了自己的用于神经网络加速的 ASIC,称为 TPU。由于 TPU 是为神经网络设计的,它没有 GPU 的一些缺点,GPU 是为图形处理而设计的。谷歌的 TPU 现在可以作为谷歌云 TPU 的一部分供您使用。您还可以在 Google Colab 上运行 TPU。
在前几章中,我向您展示了如何使用 GPU 测试和训练您的深度模型。如果您的用例符合以下条件,您应该继续使用 CPU 和 GPU 进行训练:
-
您有小型或中型模型以及小批量大小。
-
您的模型训练时间不长。
-
数据的进出是您的主要瓶颈。
-
您的计算经常是分支的或主要是逐元素完成的,或者您使用稀疏内存访问。
-
您需要使用高精度。双精度不适合在 TPU 上使用。
另一方面,有几个原因可能会导致您希望使用 TPU 而不是 GPU 进行训练。TPU 在执行密集向量和矩阵计算方面非常快速。它们针对特定工作负载进行了优化。如果您的用例符合以下情况,您应该强烈考虑使用 TPU:
-
您的模型主要由矩阵计算组成。
-
您的模型训练时间很长。
-
您希望在 TPU 上运行整个训练循环的多次迭代。
在 TPU 上运行与在 CPU 或 GPU 上运行非常相似。让我们回顾一下如何在 GPU 上训练模型的以下代码:
device = torch.device("cuda" if
torch.cuda.is_available() else "cpu") # ①
model.to(device) # ②
for epoch in range(n_epochs):
for data in trainloader:
input, labels = data
input = input.to(device) # ③
labels = labels.to(device) # ③
optimizer.zero_grad()
output = model(input)
loss = criterion(input, labels)
loss.backward()
optimizer.step()
①
如果有 GPU 可用,请将设备配置为 GPU。
②
将模型发送到设备。
③
将输入和标签发送到 GPU。
换句话说,我们将模型、输入和标签移至 GPU,其余工作由系统完成。在 TPU 上训练网络几乎与在 GPU 上训练相同,只是您需要使用PyTorch/XLA(加速线性代数)包,因为 TPU 目前不受 PyTorch 原生支持。
让我们在 Google Colab 上使用 Cloud TPU 训练我们的模型。打开一个新的 Colab 笔记本,并从运行时菜单中选择更改运行时类型。然后从“硬件加速器”下拉菜单中选择 TPU,如图 6-1 所示。Google Colab 提供免费的 Cloud TPU 系统,包括远程 CPU 主机和每个具有两个核心的四个 TPU 芯片。

图 6-1。在 Google Colab 中使用 TPU
由于 Colab 默认未安装 PyTorch/XLA,我们需要首先安装它,使用以下命令。这将安装最新的“夜间”版本,但如果需要,您可以选择其他版本:
!curl 'https://raw.githubusercontent.com/pytorch' \
'/xla/master/contrib/scripts/env-setup.py' \
-o pytorch-xla-env-setup.py
!python pytorch-xla-env-setup.py --version "nightly" # ①
<1>这些是打算在笔记本中运行的命令。在命令行上运行时,请省略“!”。
安装 PyTorch/XLA 后,我们可以导入该软件包并将数据移动到 TPU:
import torch_xla.core.xla_model as xm
device = xm.xla_device()
请注意,我们这里不使用torch.cuda.is_available(),因为它仅适用于 GPU。不幸的是,TPU 没有is_available()方法。如果您的环境未配置为 TPU,您将收到错误消息。
设备设置完成后,其余代码完全相同:
model.to(device)
for epoch in range(n_epochs):
for data in trainloader:
input, labels = data
input = input.to(device)
labels = labels.to(device)
optimizer.zero_grad()
output = model(input)
loss = criterion(input, labels)
loss.backward()
optimizer.step()
print(output.device) # ①
# out: xla:1
①
如果 Colab 配置为 TPU,您应该看到 xla:1。
PyTorch/XLA 是一个用于 XLA 操作的通用库,可能支持除 TPU 之外的其他专用 ASIC。有关 PyTorch/XLA 的更多信息,请访问PyTorch/XLA GitHub 存储库。
在 TPU 上运行仍然存在许多限制,GPU 支持更加普遍。因此,大多数 PyTorch 开发人员将首先使用单个 GPU 对其代码进行基准测试,然后再探索使用单个 TPU 或多个 GPU 加速其代码。
我们已经在本书的前面部分介绍了如何使用单个 GPU。在下一节中,我将向您展示如何在具有多个 GPU 的机器上训练您的模型。
在多个 GPU 上的 PyTorch(单台机器)
在加速训练和开发时,充分利用您可用的硬件资源非常重要。如果您有一台本地计算机或网络服务器可以访问多个 GPU,本节将向您展示如何充分利用系统上的 GPU。此外,您可能希望通过在单个实例上使用云 GPU 来扩展 GPU 资源。这通常是在考虑分布式训练方法之前的第一级扩展。
在多个 GPU 上运行代码通常称为并行处理。并行处理有两种方法:数据并行处理和模型并行处理。在数据并行处理期间,数据批次在多个 GPU 之间分割,而每个 GPU 运行模型的副本。在模型并行处理期间,模型在多个 GPU 之间分割,数据批次被管道传送到每个部分。
数据并行处理在实践中更常用。模型并行处理通常保留用于模型不适合单个 GPU 的情况。我将在本节中向您展示如何执行这两种类型的处理。
数据并行处理
图 6-2 说明了数据并行处理的工作原理。在此过程中,每个数据批次被分成N部分(N是主机上可用的 GPU 数量)。N通常是 2 的幂。每个 GPU 持有模型的副本,并且为批次的每个部分计算梯度和损失。在每次迭代结束时,梯度和损失被合并。这种方法适用于较大的批次大小和模型适合单个 GPU 的用例。
PyTorch 可以使用单进程,多线程方法或使用多进程方法来实现数据并行处理。单进程,多线程方法只需要一行额外的代码,但在许多情况下性能不佳。

图 6-2。数据并行处理
不幸的是,由于 Python 的全局解释器锁(GIL)在线程之间的争用、模型的每次迭代复制以及输入散布和输出收集引入的额外开销,多线程性能较差。您可能想尝试这种方法,因为它非常简单,但在大多数情况下,您可能会使用多进程方法。
使用 nn.DataParallel 的多线程方法
PyTorch 的nn模块原生支持多线程的数据并行处理。您只需要在将模型发送到 GPU 之前将其包装在nn.DataParallel中,如下面的代码所示。在这里,我们假设您已经实例化了您的模型:
if torch.cuda.device_count() > 1:
print("This machine has",
torch.cuda.device_count(),
"GPUs available.")
model = nn.DataParallel(model)
model.to("cuda")
首先,我们检查确保我们有多个 GPU,然后我们使用nn.DataParallel()在将模型发送到 GPU 之前设置数据并行处理。
这种多线程方法是在多个 GPU 上运行的最简单方式;然而,多进程方法通常在单台机器上表现更好。此外,多进程方法也可以用于跨多台机器运行,我们将在本章后面看到。
使用 DDP 的多进程方法(首选)
最好使用多进程方法在多个 GPU 上训练您的模型。PyTorch 通过其nn.parallel.DistributedDataProcessing模块支持这一点。分布式数据处理(DDP)可以在单台机器上的多个进程或跨多台机器的多个进程中使用。我们将从单台机器开始。
有四个步骤需要修改您的代码:
-
使用torch.distributed初始化一个进程组。
-
使用torch.nn.to()创建一个本地模型。
-
使用torch.nn.parallel将模型包装在 DDP 中。
-
使用torch.multiprocessing生成进程。
以下代码演示了如何将您的模型转换为 DDP 训练。我们将其分解为步骤。首先,导入必要的库:
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
import torch.nn as nn
import torch.optim as optim
from torch.nn.parallel \
import DistributedDataParallel as DDP
请注意,我们正在使用三个新库—torch.distributed、torch.multiprocessing和torch.nn.parallel。以下代码向您展示如何创建一个分布式训练循环:
def dist_training_loop(rank,
world_size,
dataloader,
model,
loss_fn,
optimizer):
dist.init_process_group("gloo",
rank=rank,
world_size=world_size) # ①
model = model.to(rank) # ②
ddp_model = DDP(model,
device_ids=[rank]) # ③
optimizer = optimizer(
ddp_model.parameters(),
lr=0.001)
for epochs in range(n_epochs):
for input, labels in dataloader:
input = input.to(rank)
labels = labels.to(rank) # ④
optimizer.zero_grad()
outputs = ddp_model(input) # ⑤
loss = loss_fn(outputs, labels)
loss.backward()
optimizer.step()
dist.destroy_process_group()
①
使用world_size进程设置一个进程组。
②
将模型移动到 ID 为rank的 GPU。
③
将模型包装在 DDP 中。
④
将输入和标签移动到 ID 为rank的 GPU。
⑤
调用 DDP 模型进行前向传递。
DDP 将模型状态从rank0进程广播到所有其他进程,因此我们不必担心不同进程具有具有不同初始化权重的模型。
DDP 处理了低级别的进程间通信,使您可以将模型视为本地模型。在反向传播过程中,当loss.backward()返回时,DDP 会自动同步梯度并将同步的梯度张量放在params.grad中。
现在我们已经定义了进程,我们需要使用spawn()函数创建这些进程,如下面的代码所示:
if __name__=="__main__":
world_size = 2
mp.spawn(dist_training_loop,
args=(world_size,),
nprocs=world_size,
join=True)
在这里,我们将代码作为main运行,生成两个进程,每个进程都有自己的 GPU。这就是如何在单台机器上的多个 GPU 上运行数据并行处理。
警告
GPU 设备不能在进程之间共享。
如果您的模型不适合单个 GPU 或者使用较小的批量大小,您可以考虑使用模型并行处理而不是数据并行处理。接下来我们将看看这个。
模型并行处理
图 6-3 展示了模型并行处理的工作原理。在这个过程中,模型被分割到同一台机器上的 N 个 GPU 中。如果我们按顺序处理数据批次,下一个 GPU 将始终等待前一个 GPU 完成,这违背了并行处理的目的。因此,我们需要对数据处理进行流水线处理,以便每个 GPU 在任何给定时刻都在运行。当我们对数据进行流水线处理时,只有前 N 个批次按顺序运行,然后每个后续运行会激活所有 GPU。

图 6-3. 模型并行处理
实现模型并行处理并不像数据并行处理那样简单,它需要您重新编写模型。您需要定义模型如何跨多个 GPU 分割以及数据在前向传递中如何进行流水线处理。通常通过为模型编写一个子类,具有特定数量的 GPU 的多 GPU 实现来完成这一点。
以下代码演示了 AlexNet 的双 GPU 实现:
class TwoGPUAlexNet(AlexNet):
def __init__(self):
super(ModelParallelAlexNet, self).__init__(
num_classes=num_classes,
*args,
**kwargs)
self.features.to('cuda:0')
self.avgpool.to('cuda:0')
self.classifier.to('cuda:1')
self.split_size = split_size
def forward(self, x):
splits = iter(x.split(self.split_size,
dim=0))
s_next = next(splits)
s_prev = self.seq1(s_next).to('cuda:1')
ret = []
for s_next in splits:
s_prev = self.seq2(s_prev) # ①
ret.append(self.fc(
s_prev.view(s_prev.size(0), -1)))
s_prev = self.seq1(s_next).to('cuda:1') # ②
s_prev = self.seq2(s_prev)
ret.append(self.fc(
s_prev.view(s_prev.size(0), -1)))
return torch.cat(ret)
①
s_prev 在 cuda:1 上运行。
②
s_next 在 cuda:0 上运行,可以与 s_prev 并行运行。
因为我们从 AlexNet 类派生一个子类,我们继承了它的模型结构,所以不需要创建我们自己的层。相反,我们需要描述模型的哪些部分放在 GPU0 上,哪些部分放在 GPU1 上。然后我们需要在 forward() 方法中通过每个 GPU 管道传递数据来实现 GPU 流水线。当训练模型时,您需要将标签放在最后一个 GPU 上,如下面的代码所示:
model = TwoGPUAlexNet()
loss_fn = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.001)
for epochs in range(n_epochs):
for input, labels in dataloader;
input = input.to("cuda:0")
labels = labels.to("cuda:1") # ①
optimizer.zero_grad()
outputs = model(input)
loss_fn(outputs, labels).backward()
optimizer.step()
①
将输入发送到 GPU0,将标签发送到 GPU1。
如您所见,训练循环需要更改一行代码,以确保标签位于最后一个 GPU 上,因为在计算损失之前输出将位于那里。
数据并行处理和模型并行处理是利用多个 GPU 进行加速训练的两种有效范式。如果我们能够将这两种方法结合起来并取得更好的结果,那将是多么美妙呢?让我们看看如何实现结合的方法。
结合数据并行处理和模型并行处理
您可以将数据并行处理与模型并行处理结合起来,以进一步提高性能。在这种情况下,您将使用 DDP 包装您的模型,将数据批次分发给多个进程。每个进程将使用多个 GPU,并且您的模型将被分割到每个 GPU 中。
我们只需要做两个更改。
-
将我们的多 GPU 模型类更改为接受设备作为输入。
-
在前向传递期间省略设置输出设备。DDP 将确定输入和输出数据的放置位置。
以下代码显示了如何修改多 GPU 模型:
class Simple2GPUModel(nn.Module):
def __init__(self, dev0, dev1):
super(Simple2GPUModel,
self).__init__()
self.dev0 = dev0
self.dev1 = dev1
self.net1 = torch.nn.Linear(
10, 10).to(dev0)
self.relu = torch.nn.ReLU()
self.net2 = torch.nn.Linear(
10, 5).to(dev1)
def forward(self, x):
x = x.to(self.dev0)
x = self.relu(self.net1(x))
x = x.to(self.dev1)
return self.net2(x)
在 __init__() 构造函数中,我们传入 GPU 设备对象 dev0 和 dev1,并描述模型的哪些部分位于哪些 GPU 中。这使我们能够在不同进程上实例化新模型,每个模型都有两个 GPU。forward() 方法在模型的适当位置将数据从一个 GPU 移动到下一个 GPU。
以下代码显示了训练循环的更改:
def model_parallel_training(rank, world_size):
print(f"Running DDP with a model parallel")
setup(rank, world_size)
# set up mp_model and devices for this process
dev0 = rank * 2
dev1 = rank * 2 + 1
mp_model = Simple2GPUModel(dev0, dev1)
ddp_mp_model = DDP(mp_model) # ①
loss_fn = nn.MSELoss()
optimizer = optim.SGD(
ddp_mp_model.parameters(), lr=0.001)
for epochs in range(n_epochs):
for input, labels in dataloader:
input = input.to(dev0),
labels = labels,to(dev1) # ②
optimizer.zero_grad()
outputs = ddp_mp_model(input) # ③
loss = loss_fn(outputs, labels)
loss.backward()
optimizer.step()
cleanup()
①
将模型包装在 DDP 中。
②
将输入和标签移动到适当的设备 ID。
③
输出在 dev1 上。
总之,当在多个 GPU 上使用 PyTorch 时,您有几个选项。您可以使用本节中的参考代码来实现数据并行、模型并行或组合并行处理,以加速模型训练和推断。到目前为止,我们只讨论了单台机器或云实例上的多个 GPU。
在许多情况下,在单台机器上的多个 GPU 上进行并行处理可以将训练时间减少一半或更多,您只需要升级 GPU 卡或利用更大的云 GPU 实例。但是,如果您正在训练非常复杂的模型或使用极其大型的数据集,您可能希望使用多台机器或云实例来加速训练。
好消息是,在多台机器上使用 DDP 与在单台机器上使用 DDP 并没有太大的区别。下一节将展示如何实现这一点。
分布式训练(多台机器)
如果在单台机器上训练您的 NN 模型不能满足您的需求,并且您可以访问一组服务器集群,您可以使用 PyTorch 的分布式处理能力将训练扩展到多台机器。PyTorch 的分布式子包 torch.distributed 提供了丰富的功能集,以适应各种训练架构和硬件平台。
torch.distributed 子包由三个组件组成:DDP、基于 RPC 的分布式训练(RPC)和集体通信(c10d)。我们在上一节中使用了 DDP 在单台机器上运行多个进程,它最适合数据并行处理范式。RPC 是为支持更一般的训练架构而创建的,并且可以用于除数据并行处理范式之外的分布式架构。
c10d 组件是一个用于在进程之间传输张量的通信库。c10d 被 DDP 和 RPC 组件用作后端,PyTorch 提供了 c10d API,因此您可以在自定义分布式应用中使用它。
在本书中,我们将重点介绍使用 DDP 进行分布式训练。但是,如果您有更高级的用例,您可能希望使用 RPC 或 c10d。您可以通过阅读 PyTorch 文档 了解更多信息。
对于使用 DDP 进行分布式训练,我们将遵循与在单台机器上使用多个进程相同的 DDP 过程。但是,在这种情况下,我们将在单独的机器或实例上运行每个进程。
要在多台机器上运行,我们使用一个指定配置的启动脚本来运行 DDP。启动脚本包含在 torch.distributed 中,并且可以按照以下代码执行。假设您有两个节点,节点 0 和节点 1。节点 0 是主节点,IP 地址为 192.168.1.1,空闲端口为 1234。在节点 0 上,您将运行以下脚本:
>>> python -m torch.distributed.launch
--nproc_per_node=NUM_GPUS
--nnodes=2
--node_rank=0 # ①
--master_addr="192.168.1.1"
--master_port=1234
TRAINING_SCRIPT.py (--arg1 --arg2 --arg3)
①
node_rank 被设置为节点 0。
在节点 1 上,您将运行下一个脚本。请注意,此节点的等级是 1:
>>> python -m torch.distributed.launch
--nproc_per_node=NUM_GPUS
--nnodes=2
--node_rank=1 # ①
--master_addr="192.168.1.1"
--master_port=1234
TRAINING_SCRIPT.py (--arg1 --arg2 --arg3)
①
node_rank 被设置为节点 1。
如果您想探索此脚本中的可选参数,请运行以下命令:
>>> python -m torch.distributed.launch --help
请记住,如果您不使用 DDP 范式,您应该考虑为您的用例使用 RPC 或 c10d API。并行处理和分布式训练可以显著加快模型性能并减少开发时间。在下一节中,我们将考虑通过实施优化模型本身的技术来改善 NN 性能的其他方法。
模型优化
模型优化是一个关注 NN 模型的基础实现以及它们如何训练的高级主题。随着这一领域的研究不断发展,PyTorch 已经为模型优化添加了各种功能。在本节中,我们将探讨三个优化领域——超参数调整、量化和剪枝,并为您提供参考代码,供您在自己的设计中使用。
超参数调整
深度学习模型开发通常涉及选择许多用于设计模型和训练模型的变量。这些变量称为超参数,可以包括架构变体,如层数、层深度和核大小,以及可选阶段,如池化或批量归一化。超参数还可能包括损失函数或优化参数的变体,例如 LR 或权重衰减率。
在这一部分,我将向您展示如何使用一个名为 Ray Tune 的包来管理您的超参数优化。研究人员通常会手动测试一小组超参数。然而,Ray Tune 允许您配置您的超参数,并确定哪些设置对性能最佳。
Ray Tune 支持最先进的超参数搜索算法和分布式训练。它不断更新新功能。让我们看看如何使用 Ray Tune 进行超参数调整。
还记得我们在第三章中为图像分类训练的 LeNet5 模型吗?让我们尝试不同的模型配置和训练参数,看看我们是否可以使用超参数调整来改进我们的模型。
为了使用 Ray Tune,我们需要对我们的模型进行以下更改:
-
定义我们的超参数及其搜索空间。
-
编写一个函数来封装我们的训练循环。
-
运行 Ray Tune 超参数调整。
让我们重新定义我们的模型,以便我们可以配置全连接层中节点的数量,如下面的代码所示:
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self, nodes_1=120, nodes_2=84):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, nodes_1) # ①
self.fc2 = nn.Linear(nodes_1, nodes_2) # ②
self.fc3 = nn.Linear(nodes_2, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
①
配置fc1中的节点。
②
配置fc2中的节点。
到目前为止,我们有两个超参数,nodes_1和nodes_2。让我们还定义另外两个超参数,lr和batch_size,这样我们就可以在训练中改变学习率和批量大小。
在下面的代码中,我们导入ray包并定义超参数配置:
from ray import tune
import numpy as np
config = {
"nodes_1": tune.sample_from(
lambda _: 2 ** np.random.randint(2, 9)),
"nodes_2": tune.sample_from(
lambda _: 2 ** np.random.randint(2, 9)),
"lr": tune.loguniform(1e-4, 1e-1),
"batch_size": tune.choice([2, 4, 8, 16])
}
在每次运行期间,这些参数的值是从指定的搜索空间中选择的。您可以使用方法tune.sample_from()和一个lambda函数来定义搜索空间,或者您可以使用内置的采样函数。在这种情况下,layer_1和layer_2分别使用sample_from()从2到9中随机选择一个值。
lr和batch_size使用内置函数,其中lr被随机选择为从 1e-4 到 1e-1 的双精度数,batch_size被随机选择为2、4、8或16中的一个。
接下来,我们需要将我们的训练循环封装到一个函数中,该函数以配置字典作为输入。这个训练循环函数将被 Ray Tune 调用。
在编写我们的训练循环之前,让我们定义一个函数来加载 CIFAR-10 数据,这样我们可以在训练期间重复使用来自同一目录的数据。下面的代码类似于我们在第三章中使用的数据加载代码:
import torch
import torchvision
from torchvision import transforms
def load_data(data_dir="./data"):
train_transforms = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(
(0.4914, 0.4822, 0.4465),
(0.2023, 0.1994, 0.2010))])
test_transforms = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
(0.4914, 0.4822, 0.4465),
(0.2023, 0.1994, 0.2010))])
trainset = torchvision.datasets.CIFAR10(
root=data_dir, train=True,
download=True, transform=train_transforms)
testset = torchvision.datasets.CIFAR10(
root=data_dir, train=False,
download=True, transform=test_transforms)
return trainset, testset
现在我们可以将训练循环封装成一个函数,train_model(),如下面的代码所示。这是一个大段的代码;但是,这应该对您来说很熟悉:
from torch import optim
from torch import nn
from torch.utils.data import random_split
def train_model(config):
device = torch.device("cuda" if
torch.cuda.is_available() else "cpu")
model = Net(config['nodes_1'],
config['nodes_2']).to(device=device) # ①
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(),
lr=config['lr'],
momentum=0.9) # ②
trainset, testset = load_data()
test_abs = int(len(trainset) * 0.8)
train_subset, val_subset = random_split(
trainset,
[test_abs, len(trainset) - test_abs])
trainloader = torch.utils.data.DataLoader(
train_subset,
batch_size=int(config["batch_size"]),
shuffle=True) # ③
valloader = torch.utils.data.DataLoader(
val_subset,
batch_size=int(config["batch_size"]),
shuffle=True) # ③
for epoch in range(10):
train_loss = 0.0
epoch_steps = 0
for data in trainloader:
inputs, labels = data
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
val_loss = 0.0
total = 0
correct = 0
for data in valloader:
with torch.no_grad():
inputs, labels = data
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, predicted = torch.max(
outputs.data, 1)
total += labels.size(0)
correct += \
(predicted == labels).sum().item()
loss = criterion(outputs, labels)
val_loss += loss.cpu().numpy()
print(f'epoch: {epoch} ',
f'train_loss: ',
f'{train_loss/len(trainloader)}',
f'val_loss: ',
f'{val_loss/len(valloader)}',
f'val_acc: {correct/total}')
tune.report(loss=(val_loss / len(valloader)),
accuracy=correct / total)
①
使模型层可配置。
②
使学习率可配置。
③
使批量大小可配置。
接下来我们想要运行 Ray Tune,但首先我们需要确定我们想要使用的调度程序和报告程序。调度程序确定 Ray Tune 如何搜索和选择超参数,而报告程序指定我们希望如何查看结果。让我们在下面的代码中设置它们:
from ray.tune import CLIReporter
from ray.tune.schedulers import ASHAScheduler
scheduler = ASHAScheduler(
metric="loss",
mode="min",
max_t=10,
grace_period=1,
reduction_factor=2)
reporter = CLIReporter(
metric_columns=["loss",
"accuracy",
"training_iteration"])
对于调度器,我们将使用异步连续减半算法(ASHA)进行超参数搜索,并指示它最小化损失。对于报告器,我们将配置一个 CLI 报告器,以便在每次运行时在 CLI 上报告损失、准确性、训练迭代和选择的超参数。
最后,我们可以使用以下代码中显示的run()方法运行 Ray Tune:
from functools import partial
result = tune.run(
partial(train_model),
resources_per_trial={"cpu": 2, "gpu": 1},
config=config,
num_samples=10,
scheduler=scheduler,
progress_reporter=reporter)
我们提供资源并指定配置。我们传入我们的配置字典,指定样本或运行的数量,并传入我们的scheduler和reporter函数。
Ray Tune 将报告结果。get_best_trial()方法返回一个包含有关最佳试验信息的对象。我们可以打印出产生最佳结果的超参数设置,如下面的代码所示:
best_trial = result.get_best_trial(
"loss", "min", "last")
print("Best trial config: {}".format(
best_trial.config))
print("Best trial final validation loss:",
"{}".format(
best_trial.last_result["loss"]))
print("Best trial final validation accuracy:",
"{}".format(
best_trial.last_result["accuracy"]))
您可能会发现 Ray Tune API 的其他功能有用。表 6-1 列出了tune.schedulers中可用的调度器。
表 6-1. Ray Tune 调度器
| 调度方法 | 描述 |
|---|---|
| ASHA | 运行异步连续减半算法的调度器 |
| HyperBand | 运行 HyperBand 早停算法的调度器 |
| 中位数停止规则 | 基于中位数停止规则的调度器,如“Google Vizier: A Service for Black-Box Optimization”中所述。 |
| 基于人口的训练 | 基于人口训练算法的调度器 |
| 基于人口的训练重放 | 重放人口训练运行的调度器 |
| BOHB | 使用贝叶斯优化和 HyperBand 的调度器 |
| FIFOScheduler | 简单的调度器,按提交顺序运行试验 |
| TrialScheduler | 基于试验的调度器 |
| Shim 实例化 | 基于提供的字符串的调度器 |
更多信息可以在Ray Tune 文档中找到。正如您所看到的,Ray Tune 具有丰富的功能集,但也有其他支持 PyTorch 的超参数包。这些包括Allegro Trains和Optuna。
通过找到最佳设置,超参数调整可以显著提高 NN 模型的性能。接下来,我们将探讨另一种优化模型的技术:量化。
量化
NNs 实现为计算图,它们的计算通常使用 32 位(或在某些情况下,64 位)浮点数。然而,我们可以使我们的计算使用低精度数字,并通过应用量化仍然实现可比较的结果。
量化是指使用低精度数据进行计算和访问内存的技术。这些技术可以减小模型大小,减少内存带宽,并由于内存带宽节省和使用 int8 算术进行更快的推断而执行更快的计算。
一种快速的量化方法是将所有计算精度减半。让我们再次考虑我们的 LeNet5 模型示例,如下面的代码所示:
import torch
from torch import nn
import torch.nn.functional as F
class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = F.max_pool2d(
F.relu(self.conv1(x)), (2, 2))
x = F.max_pool2d(
F.relu(self.conv2(x)), 2)
x = x.view(-1,
int(x.nelement() / x.shape[0]))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
model = LeNet5()
默认情况下,所有计算和内存都实现为 float32。我们可以使用以下代码检查模型参数的数据类型:
for n, p in model.named_parameters():
print(n, ": ", p.dtype)
# out:
# conv1.weight : torch.float32
# conv1.bias : torch.float32
# conv2.weight : torch.float32
# conv2.bias : torch.float32
# fc1.weight : torch.float32
# fc1.bias : torch.float32
# fc2.weight : torch.float32
# fc2.bias : torch.float32
# fc3.weight : torch.float32
# fc3.bias : torch.float32
如预期,我们的数据类型是 float32。然而,我们可以使用half()方法在一行代码中将模型减少到半精度:
model = model.half()
for n, p in model.named_parameters():
print(n, ": ", p.dtype)
# out:
# conv1.weight : torch.float16
# conv1.bias : torch.float16
# conv2.weight : torch.float16
# conv2.bias : torch.float16
# fc1.weight : torch.float16
# fc1.bias : torch.float16
# fc2.weight : torch.float16
# fc2.bias : torch.float16
# fc3.weight : torch.float16
# fc3.bias : torch.float16
现在我们的计算和内存值是 float16。使用half()通常是量化模型的一种快速简单方法。值得一试,看看性能是否适合您的用例。
然而,在许多情况下,我们不希望以相同方式量化每个计算,并且我们可能需要将量化超出 float16 值。对于这些其他情况,PyTorch 提供了三种额外的量化模式:动态量化、训练后静态量化和量化感知训练(QAT)。
当权重的计算或内存带宽限制吞吐量时,使用动态量化。这通常适用于 LSTM、RNN、双向编码器表示来自变压器(BERT)或变压器网络。当激活的内存带宽限制吞吐量时,通常适用于 CNN 的静态量化。当静态量化无法满足精度要求时,使用 QAT。
让我们为每种类型提供一些参考代码。所有类型将权重转换为 int8。它们在处理激活和内存访问方面有所不同。
动态量化是最简单的类型。它会将激活即时转换为 int8。计算使用高效的 int8 值,但激活以浮点格式读取和写入内存。
以下代码向您展示了如何使用动态量化量化模型:
import torch.quantization
quantized_model = \
torch.quantization.quantize_dynamic(
model,
{torch.nn.Linear},
dtype=torch.qint8)
我们所需做的就是传入我们的模型并指定量化层和量化级别。
警告
量化取决于用于运行量化模型的后端。目前,量化运算符仅在以下后端中支持 CPU 推断:x86(fbgemm)和 ARM(qnnpack)。然而,量化感知训练在完全浮点数上进行,并且可以在 GPU 或 CPU 上运行。
后训练静态量化可通过观察训练期间不同激活的分布,并决定在推断时如何量化这些激活来进一步降低延迟。这种类型的量化允许我们在操作之间传递量化值,而无需在内存中来回转换浮点数和整数:
static_quant_model = LeNet5()
static_quant_model.qconfig = \
torch.quantization.get_default_qconfig('fbgemm')
torch.quantization.prepare(
static_quant_model, inplace=True)
torch.quantization.convert(
static_quant_model, inplace=True)
后训练静态量化需要配置和训练以准备使用。我们配置后端以使用 x86(fbgemm),并调用torch.quantization.prepare来插入观察器以校准模型并收集统计信息。然后我们将模型转换为量化版本。
量化感知训练通常会产生最佳精度。在这种情况下,所有权重和激活在训练的前向和后向传递期间都被“伪量化”。浮点值四舍五入为 int8 等效值,但计算仍然以浮点数进行。也就是说,在训练期间进行量化时,权重调整是“知道”的。以下代码显示了如何使用 QAT 量化模型:
qat_model = LeNet5()
qat_mode.qconfig = \
torch.quantization.get_default_qat_qconfig('fbgemm')
torch.quantization.prepare_qat(
qat_model, inplace=True)
torch.quantization.convert(
qat_model, inplace=True)
再次,我们需要配置后端并准备模型,然后调用convert()来量化模型。
PyTorch 的量化功能正在不断发展,目前处于测试阶段。请参考PyTorch 文档获取有关如何使用量化包的最新信息。
修剪
现代深度学习模型可能具有数百万个参数,并且可能难以部署。但是,模型是过度参数化的,参数通常可以减少而几乎不影响准确性或模型性能。修剪是一种通过最小影响性能来减少模型参数数量的技术。这使您可以部署具有更少内存、更低功耗和减少硬件资源的模型。
修剪模型示例
修剪可以应用于nn.module。由于nn.module可能包含单个层、多个层或整个模型,因此可以将修剪应用于单个层、多个层或整个模型本身。让我们考虑我们的 LeNet5 模型示例:
from torch import nn
import torch.nn.functional as F
class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = F.max_pool2d(
F.relu(self.conv1(x)), (2, 2))
x = F.max_pool2d(
F.relu(self.conv2(x)), 2)
x = x.view(-1,
int(x.nelement() / x.shape[0]))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
我们的 LeNet5 模型有五个子模块——conv1、conv2、fc1、fc2和fc3。模型参数包括其权重和偏差,可以使用named_parameters()方法显示。让我们看看conv1层的参数:
device = torch.device("cuda" if
torch.cuda.is_available() else "cpu")
model = LeNet5().to(device)
print(list(model.conv1.named_parameters()))
# out:
# [('weight', Parameter containing:
# tensor([[[[0.0560, 0.0066, ..., 0.0183, 0.0783]]]],
# device='cuda:0',
# requires_grad=True)),
# ('bias', Parameter containing:
# tensor([0.0754, -0.0356, ..., -0.0111, 0.0984],
# device='cuda:0',
# requires_grad=True))]
本地和全局修剪
本地修剪是指我们仅修剪模型的特定部分。通过这种技术,我们可以将本地修剪应用于单个层或模块。只需调用修剪方法,传入层,并设置其选项如下代码所示:
import torch.nn.utils.prune as prune
prune.random_unstructured(model.conv1,
name="weight",
amount=0.25)
此示例将随机非结构化修剪应用于我们模型中conv1层中名为weight的参数。这只修剪了权重参数。我们也可以使用以下代码修剪偏置参数:
prune.random_unstructured(model.conv1,
name="bias",
amount=0.25)
修剪可以进行迭代应用,因此您可以使用不同维度上的其他修剪方法进一步修剪相同的参数。
您可以以不同方式修剪模块和参数。例如,您可能希望按模块或层类型修剪,并将修剪应用于卷积层和线性层的方式不同。以下代码演示了一种方法:
model = LeNet5().to(device)
for name, module in model.named_modules():
if isinstance(module, torch.nn.Conv2d):
prune.random_unstructured(module,
name='weight',
amount=0.3) # ①
elif isinstance(module, torch.nn.Linear):
prune.random_unstructured(module,
name='weight',
amount=0.5) # ②
①
通过 30%修剪所有 2D 卷积层。
②
通过 50%修剪所有线性层。
另一个使用修剪 API 的方法是应用全局修剪,即我们将修剪方法应用于整个模型。例如,我们可以全局修剪我们模型参数的 25%,这可能会导致每个层的不同修剪率。以下代码演示了一种应用全局修剪的方法:
model = LeNet5().to(device)
parameters_to_prune = (
(model.conv1, 'weight'),
(model.conv2, 'weight'),
(model.fc1, 'weight'),
(model.fc2, 'weight'),
(model.fc3, 'weight'),
)
prune.global_unstructured(
parameters_to_prune,
pruning_method=prune.L1Unstructured,
amount=0.25)
在这里我们修剪整个模型中所有参数的 25%。
修剪 API
PyTorch 在其torch.nn.utils.prune模块中提供了对修剪的内置支持。表 6-2 列出了修剪 API 中可用的函数。
表 6-2. 修剪函数
| 函数 | 描述 |
|---|---|
is_pruned(*module*) |
检查模块是否已修剪 |
remove(*module*, *name*) |
从模块中删除修剪重参数化和从前向钩子中删除修剪方法 |
custom_from_mask(*module*, *name*, *mask*) |
通过应用mask中的预先计算的掩码,修剪与module中名为name的参数对应的张量 |
global_unstructured(*params*, *pruning_method*) |
通过应用指定的pruning_method,全局修剪与params中所有参数对应的张量 |
ln_structured(*module*, *name*, *amount*, *n*, *dim*) |
通过移除指定dim上具有最低 Ln-范数的(当前未修剪的)通道,修剪与module中名为name的参数对应的张量中的指定amount |
random_structured(*module*, *name*, *amount*, *dim*) |
通过随机选择指定dim上的通道,移除与module中名为name的参数对应的张量中的指定amount(当前未修剪的)通道 |
l1_unstructured(*module*, *name*, *amount*) |
通过移除具有最低 L1-范数的指定amount(当前未修剪的)单元,修剪与module中名为name的参数对应的张量 |
random_unstructured(*module*, *name*, *amount*) |
通过随机选择指定amount的(当前未修剪的)单元,修剪与module中名为name的参数对应的张量 |
自定义修剪方法
如果找不到适合您需求的修剪方法,您可以创建自己的修剪方法。为此,请从torch.nn.utils.prune中提供的BasePruningMethod类创建一个子类。在大多数情况下,您可以将call()、apply_mask()、apply()、prune()和remove()方法保持原样。
但是,您需要编写自己的__init__()构造函数和compute_mask()方法来描述您的修剪方法如何计算掩码。此外,您需要指定修剪类型(structured、unstructured或global)。以下代码显示了一个示例:
class MyPruningMethod(prune.BasePruningMethod):
PRUNING_TYPE = 'unstructured'
def compute_mask(self, t, default_mask):
mask = default_mask.clone()
mask.view(-1)[::2] = 0
return mask
def my_unstructured(module, name):
MyPruningMethod.apply(module, name)
return module
首先我们定义类。此示例根据compute_mask()中的代码定义了每隔一个参数进行修剪。PRUNING_TYPE用于配置修剪类型为unstructured。然后我们包含并应用一个实例化该方法的函数。您可以按以下方式将此修剪应用于您的模型:
model = LeNet5().to(device)
my_unstructured(model.fc1, name='bias')
您现在已经创建了自己的自定义修剪方法,并可以在本地或全局应用它。
本章向您展示了如何使用 PyTorch 加速培训并优化模型。下一步是将您的模型和创新部署到世界上。在下一章中,您将学习如何将您的模型部署到云端、移动设备和边缘设备,并且我将提供一些参考代码来构建快速应用程序,展示您的设计。
第七章:将 PyTorch 部署到生产环境
到目前为止,本书大部分内容都集中在模型设计和训练上。早期章节向您展示了如何使用 PyTorch 的内置能力设计您的模型,并创建自定义的 NN 模块、损失函数、优化器和其他算法。在上一章中,我们看到了如何使用分布式训练和模型优化来加速模型训练时间,并最大程度地减少运行模型所需的资源。
到目前为止,您已经拥有创建一些经过良好训练、尖端的 NN 模型所需的一切,但不要让您的创新孤立存在。现在是时候通过应用程序将您的模型部署到世界上了。
过去,从研究到生产是一项具有挑战性的任务,需要一个软件工程团队将 PyTorch 模型移至一个框架并将其集成到(通常非 Python)生产环境中。如今,PyTorch 包括内置工具和外部库,支持快速部署到各种生产环境。
在本章中,我们专注于将您的模型部署到推理而非训练,并探讨如何将经过训练的 PyTorch 模型部署到各种应用程序中。首先,我将描述 PyTorch 内置的各种能力和工具,供您用于部署。像 TorchServe 和 TorchScript 这样的工具使您能够轻松地将 PyTorch 模型部署到云端、移动设备或边缘设备。
根据应用程序和环境,您可能有多种部署选项,每种选项都有其自身的权衡。我将向您展示如何在多个云端和边缘环境中部署您的 PyTorch 模型的示例。您将学习如何部署到用于开发和生产规模的 Web 服务器,iOS 和 Android 移动设备,以及基于 ARM 处理器、GPU 和现场可编程门阵列(FPGA)硬件的物联网(IoT)设备。
本章还将提供参考代码,包括我们使用的关键 API 和库的引用,以便轻松入门。当您需要部署模型时,您可以参考本章进行快速查阅,以便在云端或移动环境中展示您的应用程序。
让我们开始回顾 PyTorch 提供的资源,以帮助您部署您的模型。
PyTorch 部署工具和库
PyTorch 包括内置工具和能力,以便将您的模型部署到生产环境和边缘设备。在本节中,我们将探索这些工具,并在本章的其余部分将它们应用于各种环境中。
PyTorch 的部署能力包括其自然的 Python API,以及 TorchServe、TorchScript、ONNX 和移动库。由于 PyTorch 的自然 API 基于 Python,PyTorch 模型可以在任何支持 Python 的环境中直接部署。
表 7-1 总结了可用于部署的各种资源,并指示如何适当地使用每种资源。
表 7-1. 部署 PyTorch 资源
| 资源 | 使用 |
|---|---|
| Python API | 进行快速原型设计、训练和实验;编写 Python 运行时程序。 |
| TorchScript | 提高性能和可移植性(例如,在 C++中加载和运行模型);编写非 Python 运行时或严格的延迟和性能要求。 |
| TorchServe | 一个快速的生产环境工具,具有模型存储、A/B 测试、监控和 RESTful API。 |
| ONNX | 部署到具有 ONNX 运行时或 FPGA 设备的系统。 |
| 移动库 | 部署到 iOS 和 Android 设备。 |
以下各节为每个部署资源提供参考和一些示例代码。在每种情况下,我们将使用相同的示例模型,接下来将对其进行描述。
常见示例模型
对于本章提供的每个部署资源示例和应用程序,以及参考代码,我们将使用相同的模型。对于我们的示例,我们将使用一个使用 ImageNet 数据预训练的 VGG16 模型来部署一个图像分类器。这样,每个部分都可以专注于使用的部署方法,而不是模型本身。对于每种方法,您可以用自己的模型替换 VGG16 模型,并按照相同的工作流程来实现您自己设计的结果。
以下代码实例化了本章中将要使用的模型:
from torchvision.models import vgg16
model = vgg16(pretrained=True)
我们之前使用过 VGG16 模型。为了让您了解模型的复杂性,让我们使用以下代码打印出可训练参数的数量:
import numpy as np
model_parameters = filter(lambda p:
p.requires_grad, model.parameters())
params = sum([np.prod(p.size()) for
p in model_parameters])
print(params)
# out: 138357544
VGG16 模型有 138,357,544 个可训练参数。当我们逐步进行每种方法时,请记住在这个复杂性水平上的性能。您可以将其用作比较您模型复杂性的粗略基准。
在实例化 VGG16 模型后,将其部署到 Python 应用程序中需要很少的工作。事实上,在之前的章节中测试我们的模型时,我们已经这样做了。在我们进入其他方法之前,让我们再次回顾一下这个过程。
Python API
Python API 并不是一个新资源。这是我们在整本书中一直在使用的资源。我在这里提到它是为了指出您可以在不更改代码的情况下部署您的 PyTorch 模型。在这种情况下,您只需从任何 Python 应用程序中以评估模式调用您的模型,如下面的代码所示:
import system
import torch
if __name__ == "__main__":
model = MyModel()
model.load_state_dict(torch.load(PATH))
model.eval()
outputs = model(inputs)
print(outputs)
该代码加载模型,传入输入,并打印输出。这是一个简单的独立 Python 应用程序。您将看到如何在本章后面使用 RESTful API 和 Flask 将模型部署到 Python Web 服务器。使用 Flask Web 服务器,您可以构建一个快速的浏览器应用程序,展示您模型的能力。
Python 并不总是在生产环境中使用,因为它的性能较慢,而且缺乏真正的多线程。如果您的生产环境使用另一种语言(例如 C++、Java、Rust 或 Go),您可以将您的模型转换为 TorchScript 代码。
TorchScript
TorchScript 是一种序列化和优化 PyTorch 模型代码的方式,使得 PyTorch 模型可以在非 Python 运行环境中保存和执行,而不依赖于 Python。TorchScript 通常用于在 C++中运行 PyTorch 模型,并与支持 C++绑定的任何语言一起使用。
TorchScript 代表了一个 PyTorch 模型的格式,可以被 TorchScript 编译器理解、编译和序列化。TorchScript 编译器创建了一个序列化的优化版本的您的模型,可以在 C++应用程序中使用。要在 C++中加载您的 TorchScript 模型,您将使用名为LibTorch的 PyTorch C++ API 库。
有两种方法可以将您的 PyTorch 模型转换为 TorchScript。第一种称为tracing,这是一个过程,在这个过程中,您传入一个示例输入并使用一行代码进行转换。在大多数情况下使用。第二种称为scripting,当您的模型具有更复杂的控制代码时使用。例如,如果您的模型具有依赖于输入本身的条件if语句,您将需要使用 scripting。让我们看一下每种情况的一些参考代码。
由于我们的 VGG16 示例模型没有任何控制流,我们可以使用跟踪来将我们的模型转换为 TorchScript,如下面的代码所示:
import torch
model = vgg16(pretrained=True)
example_input = torch.rand(1, 3, 224, 224)
torchscript_model = torch.jit.trace(model,
example_input)
torchscript_model.save("traced_vgg16_model.pt")
该代码创建了一个 Python 可调用模型torchscript_model,可以使用正常的 PyTorch 方法进行评估,例如output = torchscript_model(inputs)。一旦我们保存了模型,就可以在 C++应用程序中使用它。
注意
在 PyTorch 中评估模型的“正常”方法通常被称为eager mode,因为这是开发中评估模型的最快方法。
如果我们的模型使用了控制流,我们将需要使用注释方法将其转换为 TorchScript。让我们考虑以下模型:
import torch.nn as nn
class ControlFlowModel(nn.Module):
def __init__(self, N):
super(ControlFlowModel, self).__init__()
self.fc = nn.Linear(N,100)
def forward(self, input):
if input.sum() > 0:
output = input
else:
output = -input
return output
model = ControlFlowModel(10)
torchcript_model = torch.jit.script(model)
torchscript_model.save("scripted_vgg16_model.pt")
在这个例子中,ControlFlowModel的输出和权重取决于输入值。在这种情况下,我们需要使用torch.jit.script(),然后我们可以像跟踪一样将模型保存到 TorchScript 中。
现在我们可以在 C++应用程序中使用我们的模型,如下所示的 C++代码:
include <torch/script.h>
#include <iostream>
#include <memory>
int main(int argc, const char* argv[]) {
if (argc != 2) {
std::cerr << "usage: example-app" >> \
"*`<path-to-exported-script-module>`*\n";
return -1;
}
torch::jit::script::Module model;
model = torch::jit::load(argv[1]);
std::vector<torch::jit::IValue> inputs;
inputs.push_back( \
torch::ones({1, 3, 224, 224}));
at::Tensor output = model.forward(inputs).toTensor();
std::cout \
<< output.slice(/*dim=*/1, \
/*start=*/0, /*end=*/5) \
<< '\n';
}
}
我们将 TorchScript 模块的文件名传递给程序,并使用torch::jit::load()加载模型。然后我们创建一个示例输入向量,将其通过我们的 TorchScript 模型运行,并将输出转换为张量,打印到stdout。
TorchScript API 提供了额外的函数来支持将模型转换为 TorchScript。表 7-2 列出了支持的函数。
表 7-2. TorchScript API 函数
| 函数 | 描述 |
|---|---|
script(obj[, optimize, _frames_up, _rcb]) |
检查源代码,使用TorchScript编译器将其编译为TorchScript代码,并返回一个ScriptModule或ScriptFunction |
trace(func, example_inputs[, optimize, ...]) |
跟踪一个函数并返回一个可执行的或者ScriptFunction,将使用即时编译进行优化 |
script_if_tracing(fn) |
在跟踪期间首次调用fn时编译 |
trace_module(mod, inputs[, optimize, ...]) |
跟踪一个模块并返回一个可执行的ScriptModule,将使用即时编译进行优化 |
fork(func, *args, **kwargs) |
创建一个异步任务,执行func并引用此执行结果的值 |
wait(future) |
强制完成一个torch.jit.Future[T]异步任务,返回任务的结果 |
ScriptModule() |
将脚本封装成 C++ torch::jit::Module |
ScriptFunction() |
与ScriptModule()相同,但表示一个单独的函数,没有任何属性或参数 |
freeze(mod[, preserved_attrs]) |
克隆一个ScriptModule,并尝试将克隆模块的子模块、参数和属性内联为TorchScript IR 图中的常量 |
save(m, f[, _extra_files]) |
保存模块的离线版本,用于在单独的进程中使用 |
load(f[, map_location, _extra_files]) |
加载之前使用torch.jit.save()保存的ScriptModule或ScriptFunction |
ignore([drop]) |
指示编译器忽略一个函数或方法,并保留为 Python 函数 |
unused(fn) |
指示编译器忽略一个函数或方法,并替换为引发异常 |
isinstance(obj, target_type) |
在 TorchScript 中提供容器类型细化 |
在本节中,我们使用 TorchScript 来提高模型在 C++应用程序或绑定到 C++的语言中的性能。然而,规模化部署 PyTorch 模型需要额外的功能,比如打包模型、配置运行环境、暴露 API 端点、日志和监控,以及管理多个模型版本。幸运的是,PyTorch 提供了一个名为 TorchServe 的工具,以便于这些任务,并快速部署您的模型进行规模推理。
TorchServe
TorchServe 是一个开源的模型服务框架,可以轻松部署训练好的 PyTorch 模型。它由 AWS 工程师开发,并于 2020 年 4 月与 Facebook 联合发布,目前由 AWS 积极维护。TorchServe 支持部署模型到生产环境所需的所有功能,包括多模型服务、模型版本控制用于 A/B 测试、日志和监控指标、以及与其他系统集成的 RESTful API。图 7-1 展示了 TorchServe 的工作原理。

图 7-1. TorchServe 架构
客户端应用程序通过多个 API 与 TorchServe 进行交互。推理 API 提供主要的推理请求和预测。客户端应用程序通过 RESTful API 请求发送输入数据,并接收预测结果。管理 API 允许您注册和管理已部署的模型。您可以注册、注销、设置默认模型、配置 A/B 测试、检查状态,并为模型指定工作人员数量。指标 API 允许您监视每个模型的性能。
TorchServe 运行所有模型实例并捕获服务器日志。它处理前端 API 并管理模型存储到磁盘。TorchServe 还为常见应用程序提供了许多默认处理程序,如目标检测和文本分类。处理程序负责将 API 中的数据转换为模型将处理的格式。这有助于加快部署速度,因为您不必为这些常见应用程序编写自定义代码。
警告
TorchServe 是实验性的,可能会发生变化。
要通过 TorchServe 部署您的模型,您需要遵循几个步骤。首先,您需要安装 TorchServe 的工具。然后,您将使用模型存档工具打包您的模型。一旦您的模型被存档,您将运行 TorchServe Web 服务器。一旦 Web 服务器运行,您可以使用其 API 请求预测,管理您的模型,执行监控,或访问服务器日志。让我们看看如何执行每个步骤。
安装 TorchServe 和 torch-model-archiver
AWS 在 Amazon SageMaker 或 Amazon EC2 实例中提供了预安装的 TorchServe 机器。如果您使用不同的云提供商,请在开始之前检查是否存在预安装实例。如果您使用本地服务器或需要安装 TorchServe,请参阅TorchServe 安装说明。
尝试的一个简单方法是使用 conda 或 pip 进行安装,如下所示:
$ `conda` `install` `torchserve` `torch-model-archiver` `-c` `pytorch`
$ `pip` `install` `torchserve` `torch-model-archiver`
如果遇到问题,请参考上述链接中的 TorchServe 安装说明。
打包模型存档
TorchServe 有能力将所有模型工件打包到单个模型存档文件中。为此,我们将使用我们在上一步中安装的 torch-model-archiver 命令行工具。它将模型检查点以及 state_dict 打包到一个 .mar 文件中,TorchServe 服务器将使用该文件来提供模型服务。
您可以使用 torch-model-archiver 来存档您的 TorchScript 模型以及标准的 “eager 模式” 实现,如下所示。
对于 TorchScript 模型,命令行如下:
$ `torch``-``model``-``archiver` `-``-``model``-``name` `vgg16`
`-``-``version` `1.0` `-``-``serialized``-``file` `model``.``pt` `-``-``handler`
`image_classifier`
我们将模型设置为我们的示例 VGG16 模型,并使用保存的序列化文件 model.pt。在这种情况下,我们也可以使用默认的 image_classifier 处理程序。
对于标准的 eager 模式模型,我们将使用以下命令:
$ `torch``-``model``-``archiver` `-``-``model``-``name` `vgg16`
`-``-``version` `1.0` `-``-``model``-``file` `model``.``py` `-``-``serialized``-``file` `model``.``pt`
`-``-``handler` `image_classifier`
这与之前的命令类似,但我们还需要指定模型文件 model.py。
torch-model-archiver 工具的完整选项集如下所示:
$ `torch-model-archiver` `-h`
usage: torch-model-archiver [-h]
--model-name MODEL_NAME
--version MODEL_VERSION_NUMBER
--model-file MODEL_FILE_PATH
--serialized-file MODEL_SERIALIZED_PATH
--handler HANDLER
[--runtime {python,python2,python3}]
[--export-path EXPORT_PATH] [-f]
[--requirements-file]
表 7-3. 模型存档工具选项
| 选项 | 描述 |
|---|---|
-h, --help |
帮助消息。显示帮助消息后,程序将退出。 |
--model-name *MODEL_NAME* |
导出的模型名称。导出的文件将命名为 --export-path,则将保存在当前工作目录中,否则将保存在导出路径下。 |
--serialized-file *SERIALIZED_FILE* |
指向包含 state_dict 的 .pt 或 .pth 文件的路径,对于 eager 模式,或者包含可执行 ScriptModule 的路径,对于 TorchScript。 |
--model-file *MODEL_FILE* |
指向包含模型架构的 Python 文件的路径。对于 eager 模式模型,此参数是必需的。模型架构文件必须只包含一个从 torch.nn.modules 扩展的类定义。 |
--handler *HANDLER* |
TorchServe的默认处理程序名称或处理自定义TorchServe推理逻辑的 Python 文件路径。 |
--extra-files *EXTRA_FILES* |
逗号分隔的额外依赖文件的路径。 |
--runtime *{python, python2, python3}* |
运行时指定要在其上运行推理代码的语言。默认运行时为RuntimeType.PYTHON,但目前支持以下运行时:python,python2和python3。 |
--export-path *EXPORT_PATH* |
导出的 .mar 文件将保存在的路径。这是一个可选参数。如果未指定--export-path,文件将保存在当前工作目录中。 |
--archive-format *{tgz, no-archive, default}* |
存档模型工件的格式。tgz以no-archive在default以 |
-f,--force |
当指定-f或--force标志时,将覆盖具有与--model-name中提供的相同名称的现有.mar文件,该文件位于--export-path指定的路径中。 |
-v, --version |
模型的版本。 |
-r, --requirements-file |
指定包含要由TorchServe安装的模型特定 Python 包列表的requirements.txt文件的路径,以实现无缝模型服务。 |
我们可以将模型存档.mar文件保存在/models文件夹中。我们将使用这个作为我们的模型存储。接下来,让我们运行 TorchServe Web 服务器。
运行 TorchServe
TorchServe 包括一个从命令行运行的内置 Web 服务器。它将一个或多个 PyTorch 模型包装在一组 REST API 中,并提供控件以配置端口、主机和日志记录。以下命令启动 Web 服务器,其中所有模型都位于/models文件夹中的模型存储中:
$ `torchserve` `--model-store` `/models` `--start`
`--models` `all`
完整的选项集显示在表 7-4 中。
表 7-4. TorchServe 选项
| 选项 | 描述 |
|---|---|
--model-store +MODEL_STORE+ +(mandatory)+ |
指定模型存储位置,可以加载模型 |
-h, --help |
显示帮助消息并退出 |
-v, --version |
返回 TorchServe 的版本 |
--start |
启动模型服务器 |
--stop |
停止模型服务器 |
--ts-config +TS_CONFIG+ |
指示 TorchServe 的配置文件 |
--models +MODEL_PATH1 MODEL_NAME=MODEL_PATH2… [MODEL_PATH1 MODEL_NAME=MODEL_PATH2… …]+ |
指示要使用[model_name=]model_location格式加载的模型;位置可以是 HTTP URL、模型存档文件或包含模型存档文件的目录在MODEL_STORE中 |
--log-config +LOG_CONFIG+ |
指示 TorchServe 的 log4j 配置文件 |
--ncs, --no-config-snapshots |
禁用快照功能 |
现在 TorchServe Web 服务器正在运行,您可以使用推理 API 发送数据并请求预测。
请求预测
您可以使用推理 API 传递数据并请求预测。推理 API 在端口 8080 上侦听,默认情况下仅从本地主机访问。要更改默认设置,请参阅TorchServe 文档。要从服务器获取预测,我们使用推理 API 的Service.Predictions gRPC API,并通过 REST 调用到/predictions/<model_name>,如下面的命令行中使用curl所示:
$c`url` `http://localhost:8080/predictions/vgg16`
`-T` `hot_dog.jpg`
代码假设我们有一个图像文件hot_dog.jpg. JSON 格式的响应看起来像这样:
{
"class": "n02175045 hot dog",
"probability": 0.788482002828
}
您还可以使用推理 API 进行健康检查,使用以下请求:
$ `curl` `http://localhost:8080/ping`
如果服务器正在运行,响应将如下所示:
{
"health": "healthy!"
}
要查看推理 API 的完整列表,请使用以下命令:
$ `curl` `-X` `OPTIONS` `http://localhost:8080`
日志记录和监控
您可以使用 Metrics API 配置指标,并在部署时监视和记录模型的性能。Metrics API 监听端口 8082,默认情况下仅从本地主机访问,但在配置 TorchServe 服务器时可以更改默认设置。以下命令说明如何访问指标:
$ `curl` `http://127.0.0.1:8082/metrics`
# HELP ts_inference_latency_microseconds # Cumulative inference
# TYPE ts_inference_latency_microseconds counter ts_inference_latency_microseconds{
uuid="d5f84dfb-fae8-4f92-b217-2f385ca7470b",...
ts_inference_latency_microseconds{
uuid="d5f84dfb-fae8-4f92-b217-2f385ca7470b",model_name="noop"...
# HELP ts_inference_requests_total Total number of inference ...
# TYPE ts_inference_requests_total counter ts_inference_requests_total{
uuid="d5f84dfb-fae8-4f92-b217-2f385ca7470b",...
ts_inference_requests_total{
uuid="d5f84dfb-fae8-4f92-b217-2f385ca7470b",model_name="noop"...
# HELP ts_queue_latency_microseconds Cumulative queue duration ...
# TYPE ts_queue_latency_microseconds counter ts_queue_latency_microseconds{
uuid="d5f84dfb-fae8-4f92-b217-2f385ca7470b",...
ts_queue_latency_microseconds{
uuid="d5f84dfb-fae8-4f92-b217-2f385ca7470b",model_name="noop"...
默认的指标端点返回 Prometheus 格式的指标。Prometheus 是一个免费软件应用程序,用于事件监控和警报,它使用 HTTP 拉模型记录实时指标到时间序列数据库中。您可以使用curl请求查询指标,或者将 Prometheus 服务器指向端点并使用 Grafana 进行仪表板。有关更多详细信息,请参阅Metrics API 文档。
指标被记录到文件中。TorchServe 还支持其他类型的服务器日志记录,包括访问日志和 TorchServe 日志。访问日志记录推理请求以及完成请求所需的时间。根据properties文件的定义,访问日志被收集在<log_location>/access_log.log文件中。TorchServe 日志收集来自 TorchServe 及其后端工作人员的所有日志。
TorchServe 支持超出默认设置的指标和日志记录功能。指标和日志可以以许多不同的方式进行配置。此外,您可以创建自定义日志。有关 TorchServe 的指标和日志自定义以及其他高级功能的更多信息,请参阅TorchServe 文档。
注意
NVIDIA Triton 推理服务器变得越来越受欢迎,也用于在生产环境中规模部署 AI 模型。尽管不是 PyTorch 项目的一部分,但在部署到 NVIDIA GPU 时,您可能希望考虑 Triton 推理服务器作为 TorchServe 的替代方案。
Triton 推理服务器是开源软件,可以从本地存储、GCP 或 AWS S3 加载模型。Triton 支持在单个或多个 GPU 上运行多个模型,低延迟和共享内存,以及模型集成。Triton 相对于 TorchServe 的一些可能优势包括:
-
Triton 已经不再是测试版。
-
这是在 NVIDIA 硬件上进行推理的最快方式(常见)。
-
它可以使用
int4量化。 -
您可以直接从 PyTorch 转换而无需 ONNX。
作为 Docker 容器提供,Triton 推理服务器还与 Kubernetes 集成,用于编排、指标和自动扩展。有关更多信息,请访问NVIDIA Triton 推理服务器文档。
ONNX
如果您的平台不支持 PyTorch,并且无法在部署中使用 TorchScript/C++或 TorchServe,那么您的部署平台可能支持 Open Neural Network Exchange(ONNX)格式。ONNX 格式定义了一组通用操作符和通用文件格式,以便深度学习工程师可以在各种框架、工具、运行时和编译器之间使用模型。
ONNX 是由 Facebook 和 Microsoft 开发的,旨在允许 PyTorch 和其他框架(如 Caffe2 和 Microsoft 认知工具包(CTK))之间的模型互操作性。目前,ONNX 由多家供应商的推理运行时支持,包括 Cadence Systems、Habana、Intel AI、NVIDIA、Qualcomm、腾讯、Windows 和 Xilinx。
一个示例用例是在 Xilinx FPGA 设备上进行边缘部署。FPGA 设备是可以使用特定逻辑编程的定制芯片。它们被边缘设备用于低延迟或高性能应用,如视频。如果您想将您的新创新模型部署到 FPGA 设备上,您首先需要将其转换为 ONNX 格式,然后使用 Xilinx FPGA 开发工具生成带有您模型实现的 FPGA 图像。
让我们看一个如何将模型导出为 ONNX 的示例,再次使用我们的 VGG16 模型。ONNX 导出器可以使用追踪或脚本。我们在 TorchScript 的早期部分学习了关于追踪和脚本的内容。我们可以通过简单地提供模型和一个示例输入来使用追踪。以下代码显示了我们如何使用追踪将我们的 VGG16 模型导出为 ONNX:
model = vgg16(pretrained=True)
example_input = torch.rand(1, 3, 224, 224)
onnx_model = torch.onnx.export(model,
example_input,
"vgg16.onnx")
我们定义一个示例输入并调用torch.onnx.export()。生成的文件vgg16.onnx是一个包含我们导出的 VGG16 模型的网络结构和参数的二进制 protobuf 文件。
如果我们想要验证我们的模型是否正确转换为 ONNX,我们可以使用 ONNX 检查器,如下所示:
import onnx
model = onnx.load("vgg16.onnx")
onnx.checker.check_model(model)
onnx.helper.printable_graph(model.graph)
此代码使用 Python ONNX 库加载模型,运行检查器,并打印出模型的可读版本。在运行代码之前,您可能需要安装 ONNX 库,使用conda或pip。
要了解更多关于转换为 ONNX 或在 ONNX 运行时中运行的信息,请查看 PyTorch 网站上的ONNX 教程。
除了 TorchScript、TorchServe 和 ONNX 之外,还正在开发更多工具来支持 PyTorch 模型部署。让我们考虑一些用于将模型部署到移动平台的工具。
移动库
Android 和 iPhone 设备不断发展,并在其定制芯片组中添加对深度学习加速的本机支持。此外,由于需要减少延迟、保护隐私以及在应用程序中与深度学习模型无缝交互的增长需求,部署到移动设备变得更加复杂。这是因为移动运行时可能与开发人员使用的训练环境有显著不同,导致在移动部署过程中出现错误和挑战。
PyTorch Mobile 解决了这些挑战,并提供了一个从训练到移动部署的端到端工作流程。PyTorch Mobile 可用于 iOS、Android 和 Linux,并提供用于移动应用程序所需的预处理和集成任务的 API。基本工作流程如图 7-2 所示。

图 7-2. PyTorch 移动工作流程
您可以像通常在 PyTorch 中设计模型一样开始。然后,您可以对模型进行量化,以减少其复杂性,同时最小程度地降低性能。随后,您可以使用追踪或脚本转换为 TorchScript,并使用torch.utils优化模型以适用于移动设备。接下来,保存您的模型并使用适当的移动库进行部署。Android 使用 Maven PyTorch 库,iOS 使用 CocoPods 与 LibTorch pod。
警告
PyTorch Mobile 仍在开发中,可能会发生变化。
有关 PyTorch Mobile 的最新详细信息,请参考PyTorch Mobile 文档。
现在我们已经探讨了一些 PyTorch 工具,用于部署我们的模型,让我们看一些参考应用程序和代码,用于部署到云端和边缘设备。首先,我将向您展示如何使用 Flask 构建开发 Web 服务器。
部署到 Flask 应用程序
在部署到全面生产之前,您可能希望将模型部署到开发 Web 服务器。这使您能够将深度学习算法与其他系统集成,并快速构建原型以演示您的新模型。使用 Python 使用 Flask 构建开发服务器的最简单方法之一。
Flask 是一个用 Python 编写的简单微型 Web 框架。它被称为“微”框架,因为它不包括数据库抽象层、表单验证、上传处理、各种身份验证技术或其他可能由其他库提供的内容。我们不会在本书中深入讨论 Flask,但我会向您展示如何使用 Flask 在 Python 中部署您的模型。
我们还将公开一个 REST API,以便其他应用程序可以传入数据并接收预测。在以下示例中,我们将部署我们预训练的 VGG16 模型并对图像进行分类。首先,我们将定义我们的 API 端点、请求类型和响应类型。我们的 API 端点将位于/predict,接受 POST 请求(包括图像文件)。响应将以 JSON 格式返回,并包含来自 ImageNet 数据集的class_id和class_name。
让我们创建我们的主要 Flask 文件,称为app.py。首先我们将导入所需的包:
import io
import json
from torchvision import models
import torchvision.transforms as transforms
from PIL import Image
from flask import Flask, jsonify, request
我们将使用io将字节转换为图像,使用json处理 JSON 格式数据。我们将使用torchvision创建我们的 VGG16 模型,并将图像数据转换为适合我们模型的格式。最后,我们导入Flask、jsonnify和request来处理 API 请求和响应。
在创建我们的 Web 服务器之前,让我们定义一个get_prediction()函数,该函数读取图像数据,预处理它,将其传递到我们的模型,并返回图像类别:
import json
imagenet_class_index = json.load(
open("./imagenet_class_index.json"))
model = models.vgg16(pretrained=True)
image_transforms = transforms.Compose(
[transforms.Resize(255),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
[0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
def get_prediction(image_bytes):
image = Image.open(io.BytesIO(image_bytes))
tensor = image_transforms(image)
outputs = model(tensor)
_, y = outputs.max(1)
predicted_idx = str(y.item())
return imagenet_class_index[predicted_idx]
由于我们的模型将返回一个表示类别的数字,我们需要一个查找表将此数字转换为类别名称。我们通过读取 JSON 转换文件创建一个名为imagenet_class_index的字典。然后,我们实例化我们的 VGG16 模型,并定义我们的图像转换,以预处理一个 PIL 图像,将其调整大小、中心裁剪、转换为张量并进行归一化。在将图像发送到我们的模型之前,这些步骤是必需的。
我们的get_prediction()函数基于接收到的字节创建一个 PIL 图像对象,并应用所需的图像转换以创建输入张量。接下来,我们执行前向传递(或模型推断),并找到具有最高概率的类别y。最后,我们使用输出类值查找类名。
现在我们有了预处理图像、通过我们的模型传递图像并返回预测类别的代码,我们可以创建我们的 Flask Web 服务器和端点,并部署我们的模型,如下所示的代码:
app = Flask(__name__)
@app.route('/predict', methods=['POST'])
def predict():
if request.method == 'POST':
file = request.files['file']
img_bytes = file.read()
class_id, class_name = \
get_prediction(image_bytes=img_bytes)
return jsonify({'class_id': class_id,
'class_name': class_name})
我们的 Web 服务器对象称为app。我们已经创建了它,但它还没有运行。我们将我们的端点设置为/predict,并配置它以处理 POST 请求。当 Web 服务器接收到 POST 请求时,它将执行predict()函数,读取图像,获取预测,并以 JSON 格式返回图像类别。
就是这样!现在我们只需要添加以下代码,以便在执行app.py时运行 Web 服务器:
if __name__ == '__main__':
app.run()
要测试我们的 Web 服务器,可以按照以下方式运行它:
>>> FLASK_ENV=development FLASK_APP=app.py flask run
我们可以使用一个简单的 Python 文件和requests库发送图像:
import requests
resp = requests.post(
"http://localhost:5000/predict",
files={"file": open('cat.jpg','rb')})
print(resp.json())
>>> {"class_id": "n02124075", "class_name": "Egyptian_cat"}
在这个例子中,我们在本地机器的端口 5000(localhost:5000)上运行一个 Web 服务器。您可能希望在 Google Colab 中运行开发 Web 服务器,以利用云 GPU。接下来我将向您展示如何做到这一点。
Colab Flask 应用程序
也许您一直在 Colab 中开发您的 PyTorch 模型,以利用其快速开发或其 GPU。Colab 提供了一个虚拟机(VM),它将其localhost路由到我们机器的本地主机。要将其暴露到公共 URL,我们可以使用一个名为ngrok的库。
首先在 Colab 中安装ngrok:
!pip install flask-ngrok
要使用ngrok运行我们的 Flask 应用程序,我们只需要添加两行代码,如下注释所示:
from flask_ngrok import run_with_ngrok # ①
@app.route("/")
def home():
return "<h1>Running Flask on Google Colab!</h1>"
app.run()
app = Flask(__name__)
run_with_ngrok(app) # ②
@app.route('/predict', methods=['POST'])
def predict():
if request.method == 'POST':
file = request.files['file']
img_bytes = file.read()
class_id, class_name = \
get_prediction(image_bytes=img_bytes)
return jsonify({'class_id': class_id,
'class_name': class_name})
app.run() # ③
①
导入ngrok库。
②
当应用程序运行时启动ngrok。
③
由于我们在 Colab 中运行,我们不需要检查main。
我已经省略了其他导入和get_prediction()函数,因为它们没有改变。现在您可以在 Colab 中运行开发 Web 服务器,以便更快地进行原型设计。ngrok库为在 Colab 中运行的服务器提供了安全的 URL;当运行 Flask 应用程序时,您将在 Colab 笔记本输出中找到 URL。例如,以下输出显示 URL 为http://c0c97117ba27.ngrok.io:
* Serving Flask app "__main__" (lazy loading)
* Environment: production
WARNING: This is a development server.
Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Running on http://c0c97117ba27.ngrok.io
* Traffic stats available on http://127.0.0.1:4040
127.0.0.1 - - [08/Dec/2020 20:46:05] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [08/Dec/2020 20:46:05]
"GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [08/Dec/2020 20:46:06] "GET / HTTP/1.1" 200 -
再次,您可以发送带有图像的 POST 请求来测试 Web 服务器。您可以在本地或另一个 Colab 笔记本中运行以下代码:
import requests
resp = requests.post(
"http://c0c97117ba27.ngrok.io/predict",
files={"file": open('cat.jpg','rb')})
print(resp.json())
# out :
# {"class_id": "n02124075",
# "class_name": "Egyptian_cat"}
请注意 URL 已更改。在 Flask 应用程序中部署您的模型是快速测试和使用 REST API 的get_prediction()函数的好方法。但是,在这里我们的 Flask 应用程序用作开发 Web 服务器,而不是用于生产部署。在大规模部署模型时,您将需要解决模型管理、A/B 测试、监控、日志记录和其他任务等问题,以确保您的模型服务器正常工作。要在大规模生产环境中部署,我们将使用 TorchServe。
使用 TorchServe 部署到云端
在此示例中,我们将将 VGG16 图像分类器部署到生产环境。假设我们的公司制作了一个软件工具,将根据图像中出现的对象对零售产品图像集进行分类。该公司正在迅速发展,现在支持数百万家每天使用该工具的小型企业。
作为机器学习工程团队的一部分,您需要将模型部署到生产环境,并提供一个简单的 REST API,软件工具将使用该 API 对其图像进行分类。因为我们希望尽快部署某些东西,所以我们将在 AWS EC2 实例中使用 Docker 容器。
使用 Docker 快速入门
TorchServe 提供了脚本,可以基于各种平台和选项创建 Docker 镜像。运行 Docker 容器可以消除重新安装运行 TorchServe 所需的所有依赖项的需要。此外,我们可以通过使用 Kubernetes 旋转多个 Docker 容器来扩展我们的模型推理。首先,我们必须根据我们在 EC2 实例上拥有的资源创建 Docker 镜像。
第一步是克隆 TorchServe 存储库并导航到Docker文件夹,使用以下命令:
$ git clone https://github.com/pytorch/serve.git
cd serve/docker
接下来,我们需要将 VGG16 模型归档添加到 Docker 镜像中。我们通过向下载存档模型文件并将其保存在/home/model-server/目录中的 Dockerfile 添加以下行来实现这一点:
$ curl -o /home/model-server/vgg16.pth \
https://download.pytorch.org/models/vgg16.pth
现在我们可以运行build_image.sh脚本,创建一个安装了公共二进制文件的 Docker 镜像。由于我们在带有 GPU 的 EC2 实例上运行,我们将使用-g标志,如下所示:
$ ./build_image.sh -g
您可以运行./build_image.sh -h查看其他选项。
一旦我们创建了 Docker 镜像,我们可以使用以下命令运行容器:
$ docker run --rm -it --gpus '"device=1"' \
-p 8080:8080 -p 8081:8081 -p 8082:8082 \
-p 7070:7070 -p 7071:7071 \
pytorch/torchserve:latest-gpu
这个命令将启动容器,将 8080/81/82 和 7070/71 端口暴露给外部世界的本地主机。它使用一个带有最新 CUDA 版本的 GPU。
现在我们的 TorchServe Docker 容器正在运行。我们公司的软件工具可以通过将图像文件发送到ourip.com/predict来发送推理请求,并可以通过 JSON 接收图像分类。
有关在 Docker 中运行 TorchServe 的更多详细信息,请参阅TorchServe Docker 文档。要了解有关 TorchServe 的更多信息,请访问TorchServe 存储库。
现在,您可以使用 Flask 进行开发或使用 TorchServe 进行生产,将模型部署到本地计算机和云服务器。这对于原型设计和通过 REST API 与其他应用程序集成非常有用。接下来,您将扩展部署能力,将模型部署到云之外:在下一节中,我们将探讨如何将模型部署到移动设备和其他边缘设备。
部署到移动和边缘
边缘设备通常是与用户或环境直接交互并在设备上直接运行机器学习计算的(通常是小型)硬件系统,而不是在云中的集中服务器上运行。一些边缘设备的例子包括手机和平板电脑,智能手表和心率监测器等可穿戴设备,以及工业传感器和家用恒温器等其他物联网设备。有一个越来越大的需求在边缘设备上运行深度学习算法,以保护隐私,减少数据传输,最小化延迟,并支持实时的新交互式用例。
首先我们将探讨如何在 iOS 和 Android 移动设备上部署 PyTorch 模型,然后我们将涵盖其他边缘设备。PyTorch 对边缘部署的支持有限但在不断增长。这些部分将提供一些参考代码,帮助您开始使用 PyTorch Mobile。
iOS
根据苹果公司的数据,截至 2021 年 1 月,全球有超过 16.5 亿活跃的 iOS 设备。随着每个新模型和定制处理单元的推出,对机器学习硬件加速的支持不断增长。学习如何将 PyTorch 模型部署到 iOS 为您打开了许多机会,可以基于深度学习创建 iOS 应用程序。
要将模型部署到 iOS 设备,您需要学习如何使用 Xcode 等开发工具创建 iOS 应用程序。我们不会在本书中涵盖 iOS 开发,但您可以在PyTorch iOS 示例应用 GitHub 存储库中找到“Hello, World”程序和示例代码,以帮助您构建您的应用程序。
让我们描述将我们的 VGG16 网络部署到 iOS 应用程序的工作流程。iOS 将使用 PyTorch C++ API 与我们的模型进行接口,因此我们需要首先将我们的模型转换并保存为 TorchScript。然后我们将在 Objective-C 中包装 C++函数,以便 iOS Swift 代码可以访问 API。我们将使用 Swift 加载和预处理图像,然后将图像数据传入我们的模型以预测其类别。
首先我们将使用追踪将我们的 VGG16 模型转换为 TorchScript 并保存为model.pt,如下面的代码所示:
import torch
import torchvision
from torch.utils.mobile_optimizer \ import optimize_for_mobile
model = torchvision.models.vgg16(pretrained=True)
model.eval()
example = torch.rand(1, 3, 224, 224) # ①
traced_script_module = \
torch.jit.trace(model, example) # ②
torchscript_model_optimized = \
optimize_for_mobile(traced_script_module) # ③
torchscript_model_optimized.save("model.pt") # ④
①
使用随机数据定义example。
②
将model转换为 TorchScript。
③
优化代码的新步骤。
④
保存模型。
如前所述,使用追踪需要定义一个示例输入,我们使用随机数据来做到这一点。然后我们使用torch.jit.trace()将模型转换为 TorchScript。然后我们添加一个新步骤,使用torch.utils.mobile_optimizer包为移动平台优化 TorchScript 代码。最后,我们将模型保存到名为model.pt的文件中。
现在我们需要编写我们的 Swift iOS 应用程序。我们的 iOS 应用程序将使用 PyTorch C++库,我们可以通过 CocoaPods 安装如下:
$ pod install
然后我们需要编写一些 Swift 代码来加载一个示例图像。您可以在将来通过访问设备上的相机或照片来改进这一点,但现在我们将保持简单:
let image = UIImage(named: "image.jpg")! \
imageView.image = image
let resizedImage = image.resized(
to: CGSize(width: 224, height: 224))
guard var pixelBuffer = resizedImage.normalized()
else return
在这里,我们将图像调整大小为 224×224 像素,并运行一个函数来规范化图像数据。
接下来,我们将加载并实例化我们的模型到我们的 iOS 应用程序中,如下面的代码所示:
private lazy var module: TorchModule = {
if let filePath = Bundle.main.path(
forResource: "model", ofType: "pt"),
let module = TorchModule(
fileAtPath: filePath) {
return module
} else {
fatalError("Can't find the model file!")
}
}()
iOS 是用 Swift 编写的,Swift 无法与 C++进行接口,因此我们需要使用一个 Objective-C 类TorchModule作为torch::jit::script::Module的包装器。
现在我们的模型已加载,我们可以通过将预处理的图像数据传入我们的模型并运行预测来预测图像的类别,如下面的代码所示:
guard let outputs = module.predict(image:
UnsafeMutableRawPointer(&pixelBuffer))
else {
return
}
在内部,predict() Objective-C 包装器调用 C++ forward()函数如下:
at::Tensor tensor = torch::from_blob(
imageBuffer, {1, 3, 224, 224}, at::kFloat);
torch::autograd::AutoGradMode guard(false);
auto outputTensor = _impl.forward(
{tensor}).toTensor();
float* floatBuffer =
outputTensor.data_ptr<float>();
当您运行示例应用程序时,您应该看到类似于图 7-3 的输出,用于示例图像文件。
这个图像分类示例只是对为 iOS 设备编码能力的一个小表示。对于更高级的用例,您仍然可以遵循相同的流程:转换并保存为 TorchScript,创建一个 Objective-C 包装器,预处理输入,并调用您的predict()函数。接下来,我们将为部署 PyTorch 到 Android 移动设备遵循类似的流程。

图 7-3. iOS 示例
Android
Android 移动设备在全球范围内也被广泛使用,据估计,2021 年初移动设备的市场份额超过 70%。这意味着也有巨大的机会将 PyTorch 模型部署到 Android 设备上。
Android 使用 PyTorch Android API,您需要安装 Android 开发工具来构建示例应用程序。使用 Android Studio,您将能够安装 Android 本机开发工具包(NDK)和软件开发工具包(SDK)。我们不会在本书中涵盖 Android 开发,但您可以在PyTorch Android 示例 GitHub 存储库中找到“Hello, World”程序和示例代码,以帮助您构建您的应用程序。
在 Android 设备上部署 PyTorch 模型的工作流程与我们用于 iOS 的过程非常相似。我们仍然需要将我们的模型转换为 TorchScript 以便与 PyTorch Android API 一起使用。然而,由于 API 本身支持加载和运行我们的 TorchScript 模型,我们无需像在 iOS 中那样将其包装在 C++代码中。相反,我们将使用 Java 编写一个 Android 应用程序,该应用程序加载和预处理图像文件,将其传递给我们的模型进行推理,并返回结果。
让我们将 VGG16 模型部署到 Android。首先,我们将模型转换为 TorchScript,就像我们为 iOS 所做的那样,如下面的代码所示:
import torch
import torchvision
from torch.utils.mobile_optimizer \
import optimize_for_mobile
model = torchvision.models.vgg16(pretrained=True)
model.eval()
example = torch.rand(1, 3, 224, 224)
traced_script_module = \
torch.jit.trace(model, example)
torchscript_model_optimized = \
optimize_for_mobile(traced_script_module)
torchscript_model_optimized.save("model.pt")
我们使用torch.jit.trace()进行跟踪将模型转换为 TorchScript。然后,我们添加一个新步骤,使用torch.utils.mobile_optimizer包为移动平台优化 TorchScript 代码。最后,我们将模型保存到名为model.pt的文件中。
接下来,我们使用 Java 创建我们的 Android 应用程序。我们通过将以下代码添加到build.gradle将 PyTorch Android API 添加到我们的应用程序作为 Gradle 依赖项:
repositories {
jcenter()
}
dependencies {
implementation
'org.pytorch:pytorch_android:1.4.0'
implementation
'org.pytorch:pytorch_android_torchvision:1.4.0'
}
接下来,我们编写我们的 Android 应用程序。我们首先加载图像并使用以下代码对其进行预处理:
Bitmap bitmap = \
BitmapFactory.decodeStream(
getAssets().open("image.jpg"));
Tensor inputTensor = \
TensorImageUtils.bitmapToFloat32Tensor(
bitmap,
TensorImageUtils.TORCHVISION_NORM_MEAN_RGB,
TensorImageUtils.TORCHVISION_NORM_STD_RGB);
现在我们有了我们的图像,我们可以预测它的类别,但首先我们必须加载我们的模型,如下所示:
Module module = Module.load(
assetFilePath(this, "model.pt"));
然后我们可以运行推理来预测图像的类别,并使用以下代码处理结果:
Tensor outputTensor = module.forward(
IValue.from(inputTensor)).toTensor();
float[] scores = \
outputTensor.getDataAsFloatArray();
float maxScore = -Float.MAX_VALUE;
int maxScoreIdx = -1;
for (int i = 0; i < scores.length; i++) {
if (scores[i] > maxScore) {
maxScore = scores[i];
maxScoreIdx = i;
}
}
String className = \
ImageNetClasses.IMAGENET_CLASSES[maxScoreIdx];
这个工作流程可以用于更高级的用例。您可以使用设备上的相机或照片或其他 Android 传感器来创建更复杂的应用程序。有关更多 PyTorch Android 演示应用程序,请访问PyTorch Android 演示应用程序 GitHub 存储库。
其他边缘设备
运行 iOS 或 Android 的移动设备代表一种边缘设备,但还有许多可以执行深度学习算法的设备。边缘设备通常使用定制硬件构建用于特定应用。其他边缘设备的示例包括传感器、视频设备、医疗监视器、软件定义无线电、恒温器、农业机械和制造传感器以检测缺陷。
大多数边缘设备包括计算机处理器、GPU、FPGA 或其他定制 ASIC 计算机芯片,能够运行深度学习模型。那么,如何将 PyTorch 模型部署到这些边缘设备呢?这取决于设备上使用了哪些处理组件。让我们探讨一些常用芯片的想法:
CPU
如果您的边缘设备使用 CPU,如英特尔或 AMD 处理器,PyTorch 可以在 Python 和 C++中使用 TorchScript 和 C++前端 API 部署。移动和边缘 CPU 芯片组通常经过优化以最小化功耗,并且边缘设备上的内存可能更有限。在部署之前通过修剪或量化优化您的模型以最小化运行推理所需的功耗和内存可能是值得的。
ARMs
ARM 处理器是一类具有简化指令集的计算机处理器。它们通常以比英特尔或 AMD CPU 更低的功耗和时钟速度运行,并可以包含在片上系统(SoCs)中。除了处理器,SoCs 芯片通常还包括其他电子设备,如可编程 FPGA 逻辑或 GPU。目前正在开发在 ARM 设备上运行 PyTorch 的 Linux。
微控制器
微控制器是通常用于非常简单控制任务的非常有限的处理器。一些流行的微控制器包括 Arduino 和 Beaglebone 处理器。由于可用资源有限,对微控制器的支持有限。
GPUs
边缘设备可能包括 GPU 芯片。NVIDIA GPU 是最广泛支持的 GPU,但其他公司(如 AMD 和英特尔)也制造 GPU 芯片。NVIDIA 在其 GPU 开发套件中支持 PyTorch,包括其 Jetson Nano、Xavier 和 NX 板。
FPGAs
PyTorch 模型可以部署到许多 FPGA 设备,包括赛灵思(最近被 AMD 收购)和英特尔 FPGA 设备系列。这两个平台都不支持直接的 PyTorch 部署;但是它们支持 ONNX 格式。典型的方法是将 PyTorch 模型转换为 ONNX,并使用 FPGA 开发工具从 ONNX 模型创建 FPGA 逻辑。
TPUs
谷歌的 TPU 芯片也正在部署到边缘设备上。PyTorch 通过 XLA 库支持,如“在 TPU 上的 PyTorch”中所述。将模型部署到使用 TPU 的边缘设备可以使您使用 XLA 库进行推理。
ASICs
许多公司正在开发自己的定制芯片或 ASIC,以高度优化和高效的方式实现模型设计。部署 PyTorch 模型的能力将严重依赖于定制 ASIC 芯片设计和开发工具所支持的功能。在某些情况下,如果 ASIC 支持,您可以使用 PyTorch/XLA 库。
当部署 PyTorch 模型到边缘设备时,请考虑系统上可用的处理组件。根据可用的芯片,研究利用 C++前端 API、利用 TorchScript、将模型转换为 ONNX 格式,或访问 PyTorch XLA 库以部署模型的选项。
在本章中,您学习了如何使用标准的 Python API、TorchScript/C++、TorchServe、ONNX 和 PyTorch 移动库来部署您的模型进行推理。本章还提供了参考代码,以在本地开发服务器或云中的生产环境中使用 Flask 和 TorchServe,以及在 iOS 和 Android 设备上部署 PyTorch 模型。
PyTorch 支持一个庞大、活跃的有用工具生态系统,用于模型开发和部署。我们将在下一章中探讨这个生态系统,该章还提供了一些最受欢迎的 PyTorch 工具的参考代码。
第八章:PyTorch 生态系统和其他资源
在之前的章节中,您已经学会了使用 PyTorch 设计和部署深度学习模型所需的一切。您已经学会了如何在不同平台上构建、训练、测试和加速您的模型,以及如何将这些模型部署到云端和边缘设备。正如您所见,PyTorch 在开发和部署环境中都具有强大的功能,并且高度可扩展,允许您创建符合您需求的定制化。
为了总结这个参考指南,我们将探索 PyTorch 生态系统、其他支持库和额外资源。PyTorch 生态系统是 PyTorch 最强大的优势之一。它提供了丰富的项目、工具、模型、库和平台,用于探索人工智能并加速您的人工智能开发。
PyTorch 生态系统包括由研究人员、第三方供应商和 PyTorch 社区创建的项目和库。这些项目得到了 PyTorch 团队的认可,以确保它们的质量和实用性。
此外,PyTorch 项目还包括支持特定领域的其他库,包括用于计算机视觉的 Torchvision 和用于 NLP 的 Torchtext。PyTorch 还支持其他包,如 TensorBoard 用于可视化,还有大量的学习资源供进一步研究,如 Papers with Code 和 PyTorch Academy。
在本章中,我们将首先概述 PyTorch 生态系统及其支持的项目和工具的高级视图。然后,我们将深入了解一些最强大和流行的资源,提供关于它们的使用和 API 的参考资料。最后,我将向您展示如何通过各种教程、书籍、课程和其他培训资源进一步学习。
让我们开始看看生态系统提供了什么。
PyTorch 生态系统
截至 2021 年初,PyTorch 生态系统拥有超过 50 个库和项目,这个列表还在不断增长。其中一些是特定领域的项目,例如专门用于计算机视觉或 NLP 解决方案的项目。其他项目,如 PyTorch Lightning 和 fastai,提供了编写简洁代码的框架,而像 PySyft 和 Crypten 这样的项目支持安全性和隐私性。还有支持强化学习、游戏模型、模型可解释性和加速的项目。在本节中,我们将探索包含在 PyTorch 生态系统中的项目。
表 8-1 提供了支持计算机视觉应用的生态系统项目列表。
表 8-1. 计算机视觉项目
| 项目 | 描述 |
|---|---|
| Torchvision | PyTorch 的计算机视觉库,提供常见的转换、模型和实用程序,以支持计算机视觉应用(https://pytorch.tips/torchvision) |
| Detectron2 | Facebook 的目标检测和分割平台(https://pytorch.tips/detectron2) |
| Albumentations | 图像增强库(https://pytorch.tips/albumentations) |
| PyTorch3D | 用于 3D 计算机视觉的可重用组件集合(https://pytorch.tips/pytorch3d) |
| Kornia | 用于计算机视觉的可微分模块库(https://pytorch.tips/kornia) |
| MONAI | 用于医疗影像深度学习的框架(https://pytorch.tips/monai) |
| TorchIO | 用于 3D 医学图像的工具包(https://pytorch.tips/torchio) |
Torchvision 是计算机视觉应用中最强大的库之一,包含在 PyTorch 项目中。它也由 PyTorch 开发团队维护。我们将在本章后面更详细地介绍 Torchvision API。
PyTorch3D 和 TorchIO 为 3D 成像提供了额外支持,而 TorchIO 和 MONAI 专注于医学成像应用。Detectron2 是一个强大的物体检测平台。如果您正在进行计算机视觉研究和开发,这些扩展可能有助于加速您的结果。
与计算机视觉一样,过去十年来在 NLP 研究中取得了重大进展,NLP 应用也得到了 PyTorch 的良好支持。
表 8-2 提供了支持NLP 和基于音频应用的生态系统项目列表。
表 8-2. NLP 和音频项目
| 项目 | 描述 |
|---|---|
| Torchtext | PyTorch 的自然语言处理和文本处理库(https://pytorch.tips/torchtext) |
| Flair | NLP 的简单框架(https://pytorch.tips/flair) |
| AllenNLP | 用于设计和评估 NLP 模型的库(https://pytorch.tips/allennlp) |
| ParlAI | 用于共享、训练和测试对话模型的框架(https://pytorch.tips/parlai) |
| NeMo | 用于会话 AI 的工具包(https://pytorch.tips/nemo) |
| PyTorch NLP | NLP 的基本工具(https://pytorch.tips/pytorchnlp) |
| Translate | Facebook 的机器翻译平台(https://pytorch.tips/translate) |
| TorchAudio | PyTorch 的音频预处理库(https://pytorch.tips/torchaudio) |
与 Torchvision 一样,Torchtext 作为 PyTorch 项目的一部分包含在内,并由 PyTorch 开发团队维护。Torchtext 为处理文本数据和开发基于 NLP 的模型提供了强大的功能。
Flair、AllenNLP 和 PyTorch NLP 为基于文本的处理和 NLP 模型开发提供了额外功能。ParlAI 和 NeMo 提供了开发对话和会话 AI 系统的工具,而 Translate 专注于机器翻译。
TorchAudio 提供处理音频文件(如语音和音乐)的功能。
强化学习和游戏也是研究的快速增长领域,有工具支持它们使用 PyTorch。
表 8-3 提供了支持游戏和强化学习应用的生态系统项目列表。
表 8-3. 游戏和强化学习项目
| 项目 | 描述 |
|---|---|
| ELF | 用于在游戏环境中训练和测试算法的项目(https://pytorch.tips/elf) |
| PFRL | 深度强化学习算法库(https://pytorch.tips/pfrl) |
ELF(广泛、轻量级、灵活的游戏研究平台)是 Facebook 开发的开源项目,重新实现了像 AlphaGoZero 和 AlphaZero 这样的游戏算法。PFRL(首选强化学习)是由 Preferred Networks 开发的基于 PyTorch 的开源深度强化学习库,是 Chainer 和 ChainerRL 的创造者。它可以用来创建强化学习的基线算法。PFRL 目前为 11 个基于原始研究论文的关键深度强化学习算法提供了可重现性脚本。
正如您在本书中所看到的,PyTorch 是一个高度可定制的框架。这种特性有时会导致需要为常见任务经常编写相同的样板代码。为了帮助开发人员更快地编写代码并消除样板代码的需要,几个 PyTorch 项目提供了高级编程 API 或与其他高级框架(如 scikit-learn)的兼容性。
表 8-4 提供了支持高级编程的生态系统项目列表。
表 8-4. 高级编程项目
| 项目 | 描述 |
|---|---|
| fastai | 简化使用现代实践进行训练的库(https://pytorch.tips/fastai) |
| PyTorch Lightning | 可定制的类 Keras ML 库,消除样板代码(https://pytorch.tips/lightning) |
| Ignite | 用于编写紧凑、功能齐全的训练循环的库(https://pytorch.tips/ignite) |
| Catalyst | 用于紧凑强化学习流水线的框架(https://pytorch.tips/catalyst) |
| skorch | 提供与 scikit-learn 兼容的 PyTorch(https://pytorch.tips/skorch) |
| Hydra | 用于配置复杂应用程序的框架(https://pytorch.tips/hydra) |
| higher | 促进复杂元学习算法实现的库(https://pytorch.tips/higher) |
| Poutyne | 用于样板代码的类 Keras 框架(https://pytorch.tips/poutyne) |
Fastai 是建立在 PyTorch 上的研究和学习框架。它有全面的文档,并自早期提供了 PyTorch 的高级 API。您可以通过查阅其文档和免费的在线课程或阅读 Jeremy Howard 和 Sylvain Gugger(O'Reilly)合著的书籍使用 fastai 和 PyTorch 进行编码的深度学习来快速掌握该框架。
PyTorch Lightning 也已成为 PyTorch 非常受欢迎的高级编程 API 之一。它为训练、验证和测试循环提供了所有必要的样板代码,同时允许您轻松添加自定义方法。
Ignite 和 Catalyst 也是流行的高级框架,而 skorch 和 Poutyne 分别提供了类似于 scikit-learn 和 Keras 的接口。Hydra 和 higher 用于简化复杂应用程序的配置。
除了高级框架外,生态系统中还有支持硬件加速和优化推理的软件包。
表 8-5 提供了支持推理加速应用程序的生态系统项目列表。
表 8-5。推理项目
| 项目 | 描述 |
|---|---|
| Glow | 用于硬件加速的 ML 编译器(https://pytorch.tips/glow) |
| Hummingbird | 编译经过训练的模型以实现更快推理的库(https://pytorch.tips/hummingbird) |
Glow 是用于硬件加速器的机器学习编译器和执行引擎,可以用作高级深度学习框架的后端。该编译器允许进行最先进的优化和神经网络图的代码生成。Hummingbird 是由微软开发的开源项目,是一个库,用于将经过训练的传统 ML 模型编译为张量计算,并无缝地利用 PyTorch 加速传统 ML 模型。
除了加速推理外,PyTorch 生态系统还包含用于加速训练和使用分布式训练优化模型的项目。
表 8-6 提供了支持分布式训练和模型优化的生态系统项目列表。
表 8-6。分布式训练和模型优化项目
| 项目 | 描述 |
|---|---|
| Ray | 用于构建和运行分布式应用程序的快速、简单框架(https://pytorch.tips/ray) |
| Horovod | 用于 TensorFlow、Keras、PyTorch 和 Apache MXNet 的分布式深度学习训练框架(https://pytorch.tips/horovod) |
| DeepSpeed | 优化库(https://pytorch.tips/deepspeed) |
| Optuna | 自动化超参数搜索和优化(https://pytorch.tips/optuna) |
| Polyaxon | 用于构建、训练和监控大规模深度学习应用程序的平台(https://pytorch.tips/polyaxon) |
| Determined | 使用共享 GPU 和协作训练模型的平台(https://pytorch.tips/determined) |
| Allegro Trains | 包含深度学习实验管理器、版本控制和机器学习操作的库(https://pytorch.tips/allegro) |
Ray 是一个用于构建分布式应用程序的 Python API,并打包了其他库以加速机器学习工作负载。我们在第六章中使用了其中一个软件包 Ray Tune 来在分布式系统上调整超参数。Ray 是一个非常强大的软件包,还可以支持可扩展的强化学习、分布式训练和可扩展的服务。Horovod 是另一个分布式框架。它专注于分布式训练,并可与 Ray 一起使用。
DeepSpeed、Optuna 和 Allegro Trains 还支持超参数调优和模型优化。Polyaxon 可用于规模化训练和监控模型,而 Determined 专注于共享 GPU 以加速训练。
随着 PyTorch 的流行,已经开发了许多专门的软件包来支持特定领域和特定工具。这些工具中的许多旨在改进模型或数据的预处理。
表 8-7 提供了支持建模和数据处理的生态系统项目列表。
表 8-7. 建模和数据处理项目
| 项目 | 描述 |
|---|---|
| TensorBoard | TensorBoard 的数据和模型可视化工具已集成到 PyTorch 中(https://pytorch.tips/pytorch-tensorboard) |
| PyTorch Geometric | 用于 PyTorch 的几何深度学习扩展库(https://pytorch.tips/geometric) |
| Pyro | 灵活且可扩展的深度概率建模(https://pytorch.tips/pyro) |
| Deep Graph Library (DGL) | 用于实现图神经网络的库(https://pytorch.tips/dgl) |
| MMF | Facebook 的多模型深度学习(视觉和语言)模块化框架(https://pytorch.tips/mmf) |
| GPyTorch | 用于创建可扩展高斯过程模型的库(https://pytorch.tips/gpytorch) |
| BoTorch | 用于贝叶斯优化的库(https://pytorch.tips/botorch) |
| Torch Points 3D | 用于非结构化 3D 空间数据的框架(https://pytorch.tips/torchpoints3d) |
| TensorLy | 用于张量方法和深度张量神经网络的高级 API(https://pytorch.tips/tensorly)(https://pytorch.tips/advertorch) |
| BaaL | 从贝叶斯理论中实现主动学习(https://pytorch.tips/baal) |
| PennyLane | 量子机器学习库(https://pytorch.tips/pennylane) |
TensorBoard 是为 TensorFlow 开发的非常流行的可视化工具,也可以用于 PyTorch。我们将在本章后面介绍这个工具及其 PyTorch API。
PyTorch Geometric、Pyro、GPyTorch、BoTorch 和 BaaL 都支持不同类型的建模,如几何建模、概率建模、高斯建模和贝叶斯优化。
Facebook 的 MMF 是一个功能丰富的多模态建模软件包,而 Torch Points 3D 可用于对通用的 3D 空间数据进行建模。
PyTorch 作为一个工具的成熟和稳定性体现在用于支持安全和隐私的软件包的出现。随着法规要求系统在这些领域合规,安全和隐私问题变得更加重要。
表 8-8 提供了支持安全性和隐私性的生态系统项目列表。
表 8-8. 安全和隐私项目
| 项目 | 描述 |
|---|---|
| AdverTorch | 用于对抗性示例和防御攻击的模块 |
| PySyft | 用于模型加密和隐私的库(https://pytorch.tips/pysyft) |
| Opacus | 用于训练具有差分隐私的模型的库(https://pytorch.tips/opacus) |
| CrypTen | 隐私保护 ML 的框架(https://pytorch.tips/crypten) |
PySyft、Opacus 和 CrypTen 是支持安全性和隐私性的 PyTorch 包。它们添加了保护和加密模型以及用于创建模型的数据的功能。
通常,深度学习似乎是一个黑匣子,开发人员不知道模型为什么做出决策。然而,如今,这种缺乏透明度已不再可接受:人们越来越意识到公司及其高管必须对其算法的公平性和运作负责。模型可解释性对于研究人员、开发人员和公司高管来说很重要,以了解模型为何产生其结果。
表 8-9 显示了支持模型 可解释性的生态系统项目。
表 8-9. 模型可解释性项目
| 项目 | 描述 |
|---|---|
| Captum | 用于模型可解释性的库(https://pytorch.tips/captum) |
| 视觉归因 | 用于模型可解释性的最新视觉归因方法的 PyTorch 实现(https://pytorch.tips/visual-attribution) |
目前,Captum 是支持模型可解释性的首要 PyTorch 项目。视觉归因包对解释计算机视觉模型和识别图像显著性很有用。随着领域的扩展,更多的项目肯定会进入这个领域。
正如您所看到的,PyTorch 生态系统包括广泛的开源项目,可以在许多不同的方面帮助您。也许您正在开展一个可以使其他研究人员受益的项目。如果您希望将您的项目纳入官方 PyTorch 生态系统,请访问PyTorch 生态系统申请页面。
在考虑应用程序时,PyTorch 团队寻找符合以下要求的项目:
-
您的项目使用 PyTorch 来改善用户体验、添加新功能或加快训练/推理速度。
-
您的项目稳定、维护良好,并包含足够的基础设施、文档和技术支持。
生态系统不断增长。要获取最新的项目列表,请访问PyTorch 生态系统网站。要向我们更新书中的新项目,请发送电子邮件至作者邮箱 jpapa@joepapa.ai。
接下来,我们将更深入地了解一些 PyTorch 项目的支持工具和库。显然,我们无法在本书中涵盖所有可用的库和工具,但在接下来的章节中,我们将探索一些最受欢迎和有用的库,以帮助您更深入地了解它们的 API 和用法。
Torchvision 用于图像和视频
我们在本书中使用了 Torchvision,它是计算机视觉研究中最强大和有用的 PyTorch 库之一。从技术上讲,Torchvision 包是 PyTorch 项目的一部分。它包括一系列流行的数据集、模型架构和常见的图像转换。
数据集和 I/O
Torchvision 提供了大量的数据集。它们包含在torchvision.datasets库中,可以通过创建数据集对象来访问,如下面的代码所示:
import torchvision
train_data = torchvision.datasets.CIFAR10(
root=".",
train=True,
transform=None,
download=True)
您只需调用构造函数并传入适当的选项。此代码使用训练数据从 CIFAR-10 数据集创建数据集对象,不使用任何转换。它会在当前目录中查找数据集文件,如果文件不存在,它将下载它们。
表 8-10 提供了 Torchvision 提供的数据集的全面列表。
表 8-10。Torchvision 数据集
| 数据集 | 描述 |
|---|---|
| CelebA | 大规模人脸属性数据集,包含超过 200,000 张名人图像,每张图像有 40 个属性注释。 |
| CIFAR-10 | CIFAR-10 数据集包含 60,000 个 32×32 彩色图像,分为 10 个类别,分为 50,000 个训练图像和 10,000 个测试图像。还提供了包含 100 个类别的 CIFAR-100 数据集。 |
| Cityscapes | 包含来自 50 个不同城市街景记录的视频序列的大规模数据集,带有注释。 |
| COCO | 大规模目标检测、分割和字幕数据集。 |
| DatasetFolder | 用于从文件夹结构中创建任何数据集。 |
| EMNIST | MNIST 的手写字母扩展。 |
| FakeData | 一个返回随机生成图像作为 PIL 图像的虚假数据集。 |
| Fashion-MNIST | Zalando 服装图像数据集,符合 MNIST 格式(60,000 个训练示例,10,000 个测试示例,28×28 灰度图像,10 个类别)。 |
| Flickr | Flickr 8,000 张图像数据集。 |
| HMDB51 | 大型人体运动视频序列数据库。 |
| ImageFolder | 用于从文件夹结构中创建图像数据集。 |
| ImageNet | 包含 14,197,122 张图像和 21,841 个单词短语的图像分类数据集。 |
| Kinetics-400 | 大规模动作识别视频数据集,包含 650,000 个持续 10 秒的视频剪辑,涵盖高达 700 个人类动作类别,如演奏乐器、握手和拥抱。 |
| KMNIST | Kuzushiji-MNIST,MNIST 数据集的替代品(70,000 个 28×28 灰度图像),其中每个字符代表平假名的 10 行之一。 |
| LSUN | 每个 10 个场景类别和 20 个对象类别的一百万标记图像。 |
| MNIST | 手写的单个数字,28×28 灰度图像,有 60,000 个训练和 10,000 个测试样本。 |
| Omniglot | 由 50 种不同字母表的 1,623 个不同手写字符生成的数据集。 |
| PhotoTour | 包含 1,024×1,024 位图图像的照片旅游数据集,每个图像包含一个 16×16 的图像块数组。 |
| Places365 | 包含 400 多个独特场景类别的 10,000,000 张图像数据集,每个类别有 5,000 至 30,000 张训练图像。 |
| QMNIST | Facebook 的项目,从 NIST 特殊数据库 19 中找到的原始数据生成 MNIST 数据集。 |
| SBD | 包含 11,355 张图像的语义分割注释的语义边界数据集。 |
| SBU | Stony Brook 大学(SBU)标题照片数据集,包含超过 1,000,000 张带标题的图像。 |
| STL10 | 用于无监督学习的类似于 CIFAR-10 的数据集。96×96 彩色图像的 10 个类别,包括 5,000 个训练图像,8,000 个测试图像和 100,000 个未标记图像。 |
| SVHN | 街景房屋号码数据集,类似于 MNIST,但是在自然场景彩色图像中有 10 倍的数据。 |
| UCF101 | 包含来自 101 个动作类别的 13,320 个视频的动作识别数据集。 |
| USPS | 包含 16×16 手写文本图像的数据集,有 10 个类别,7,291 个训练图像和 2,007 个测试图像。 |
| VOC | 用于目标类别识别的 PASCAL 视觉对象类别图像数据集。2012 年版本有 20 个类别,11,530 个训练/验证图像,27,450 个感兴趣区域(ROI)标注对象和 6,929 个分割。 |
Torchvision 不断添加更多数据集。要获取最新列表,请访问Torchvision 文档。
模型
Torchvision 还提供了一个广泛的模型列表,包括模块架构和预训练权重(如果有的话)。通过调用相应的构造函数,可以轻松创建模型对象,如下所示:
import torchvision
model = torchvision.models.vgg16(pretrained=False)
这段代码创建了一个带有随机权重的 VGG16 模型,因为没有使用预训练权重。通过使用类似的构造函数并设置适当的参数,可以实例化许多不同的计算机视觉模型。Torchvision 使用 PyTorch 的torch.utils.model_zoo提供预训练模型。可以通过传递pretrained=True来构建这些模型。
表 8-11 提供了 Torchvision 中包含的模型的全面列表,按类别分类。这些模型在研究界广为人知,表中包含了与每个模型相关的研究论文的参考文献。
表 8-11. Torchvision 模型
| 模型 | 论文 |
|---|---|
| 分类 | |
| AlexNet | “用于并行化卷积神经网络的一个奇怪技巧,” 作者:Alex Krizhevsky |
| VGG | “用于大规模图像识别的非常深度卷积网络,” 作者:Karen Simonyan 和 Andrew Zisserman |
| ResNet | “用于图像识别的深度残差学习,” 作者:Kaiming He 等 |
| SqueezeNet | “SqueezeNet: AlexNet 级别的准确性,参数减少 50 倍,模型大小<0.5MB,” 作者:Forrest N. Iandola 等 |
| DenseNet | “密集连接的卷积网络,” 作者:Gao Huang 等 |
| Inception v3 | “重新思考计算机视觉中的 Inception 架构,” 作者:Christian Szegedy 等 |
| GoogLeNet | “使用卷积深入研究,” 作者:Christian Szegedy 等 |
| ShuffleNet v2 | “ShuffleNet V2: 高效 CNN 架构设计的实用指南,” 作者:马宁宁等 |
| MobileNet v2 | “MobileNetV2: 反向残差和线性瓶颈,” 作者:Mark Sandler 等 |
| ResNeXt | “用于深度神经网络的聚合残差变换,” 作者:Saining Xie 等 |
| Wide ResNet | “宽残差网络,” 作者:Sergey Zagoruyko 和 Nikos Komodakis |
| MNASNet | “MnasNet: 面向移动设备的神经架构搜索,” 作者:Mingxing Tan 等 |
| 语义分割 | |
| FCN ResNet50 | “用于语义分割的全卷积网络,” 作者:Jonathan Long 等 |
| FCN ResNet101 | 参见上文 |
| DeepLabV3 ResNet50 | “重新思考空洞卷积用于语义图像分割,” 作者:Liang-Chieh Chen 等 |
| DeepLabV3 ResNet101 | 参见上文 |
| 目标检测 | |
| Faster R-CNN ResNet-50 | “FPNFaster R-CNN: 实时目标检测与区域建议网络,” 作者:Shaoqing Ren 等 |
| Mask R-CNN ResNet-50 FPN | “Mask R-CNN,” 作者:Kaiming He 等 |
| 视频分类 | |
| ResNet 3D 18 | “仔细研究时空卷积用于动作识别,” 作者:Du Tran 等 |
| ResNet MC 18 | 参见上文 |
| ResNet (2+1)D | 参见上文 |
Torchvision 还在不断添加新的计算机视觉模型。要获取最新列表,请访问Torchvision 文档。
变换、操作和实用程序
Torchvision 还提供了一套全面的变换、操作和实用程序集合,以帮助图像预处理和数据准备。应用变换的常见方法是形成一组变换的组合,并将这个transforms对象传递给数据集构造函数,如下面的代码所示:
from torchvision import transforms, datasets
train_transforms = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
(0.4914, 0.4822, 0.4465),
(0.2023, 0.1994, 0.2010)),
])
train_data = datasets.CIFAR10(
root=".",
train=True,
transform=train_transforms)
在这里,我们创建了一个复合变换,使用ToTensor()将数据转换为张量,然后使用预定的均值和标准差对图像数据进行归一化。将transform参数设置为train_transforms对象会配置数据集在访问数据时应用一系列变换。
表 8-12 提供了torchvision.transforms中可用转换的完整列表。在本表和表 8-13 中以斜体显示的转换目前不受 TorchScript 支持。
表 8-12. Torchvision 转换
| 转换 | 描述 |
|---|---|
| 操作转换 | |
Compose() |
基于其他转换序列创建一个转换 |
CenterCrop(size) |
以给定大小在中心裁剪图像 |
ColorJitter(brightness=0, contrast=0, saturation=0, hue=0) |
随机改变图像的亮度、对比度、饱和度和色调 |
FiveCrop(size) |
将图像裁剪成四个角和中心裁剪 |
Grayscale(num_output_channels=1) |
将彩色图像转换为灰度图像 |
Pad(padding, fill=0, padding_mode=constant) |
使用给定值填充图像的边缘 |
RandomAffine(degrees, translate=None, scale=None, shear=None, resample=0, fillcolor=0) |
随机应用仿射变换 |
RandomApply(transforms, p=0.5) |
以给定概率随机应用一系列转换 |
RandomCrop(size, padding=None, pad_if_needed=False, fill=0, padding_mode=constant) |
在随机位置裁剪图像 |
RandomGrayscale(p=0.1) |
以给定概率随机将图像转换为灰度图像 |
RandomHorizontalFlip(p=0.5) |
以给定概率随机水平翻转图像 |
RandomPerspective(distortion_scale=0.5, p=0.5, interpolation=2, fill=0) |
应用随机透视变换 |
RandomResizedCrop(size, scale=(0.08, 1.0), ratio=(0.75, 1.3333333333333333), interpolation=2) |
使用随机大小和长宽比调整图像 |
RandomRotation(degrees, resample=False, expand=False, center=None, fill=None) |
随机旋转图像 |
RandomVerticalFlip(p=0.5) |
以给定概率随机垂直翻转图像 |
Resize(size, interpolation=2) |
将图像调整为随机大小 |
TenCrop(size, vertical_flip=False) |
将图像裁剪成四个角和中心裁剪,并额外提供每个的翻转版本 |
GaussianBlur(kernel_size, sigma=(0.1, 2.0)) |
使用随机核应用高斯模糊 |
| 转换转换 | |
ToPILImage(mode=None) |
将张量或numpy.ndarray转换为 PIL 图像 |
ToTensor() |
将 PIL 图像或ndarray转换为张量 |
| 通用转换 | |
Lambda(lambda) |
将用户定义的lambda作为转换应用 |
大多数转换可以在张量或 PIL 格式的图像上进行操作,其形状为[..., C, H, W],其中...表示任意数量的前导维度。然而,一些转换只能在 PIL 图像或张量图像数据上操作。
在表 8-13 中列出的转换仅适用于 PIL 图像。这些转换目前不受 TorchScript 支持。
表 8-13. Torchvision 仅支持 PIL 的转换
| 转换 | 描述 |
|---|---|
RandomChoice(transforms) |
从列表中随机选择一个转换应用 |
RandomOrder(transforms) |
以随机顺序应用一系列转换 |
在表 8-14 中列出的转换仅适用于张量图像。
表 8-14. Torchvision 仅支持张量的转换
| 转换 | 描述 |
|---|---|
LinearTransformation(transformation_matrix, mean_vector) |
根据离线计算的方形变换矩阵和mean_vector对张量图像应用线性变换。 |
Normalize(mean, std, inplace=False) |
使用给定的均值和标准差对张量图像进行归一化。 |
RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False) |
随机选择一个矩形区域并擦除其像素。 |
ConvertImageDtype(dtype: torch.dtype) |
将张量图像转换为新的数据类型,并自动缩放其值以匹配该类型 |
注意
在为 C++使用脚本转换时,请使用torch.nn.Sequential()而不是torchvision.transforms.Compose()。以下代码显示了一个示例:
>>> transforms = torch.nn.Sequential(
transforms.CenterCrop(10),
transforms.Normalize(
(0.485, 0.456, 0.406), (0.229, 0.224,
0.225)),
)
>>> scripted_transforms = torch.jit.script(transforms)
在前面的表中列出的许多转换包含用于指定参数的随机数生成器。例如,RandomResizedCrop()将图像裁剪为随机大小和长宽比。
Torchvision 还提供了功能性转换作为torchvision.transforms.functional包的一部分。您可以使用这些转换来执行具有您选择的特定参数集的转换。例如,您可以调用torchvision.transforms.functional.adjust_brightness()来调整一个或多个图像的亮度。
表 8-15 提供了支持的功能性转换列表。
表 8-15。Torchvision 功能性转换
| 功能性转换和实用程序 |
|---|
adjust_brightness(img: torch.Tensor, brightness_factor: float) |
adjust_contrast(img: torch.Tensor, contrast_factor: float) |
adjust_gamma(img: torch.Tensor, gamma: float, gain: float = 1) |
adjust_hue(img: torch.Tensor, hue_factor: float) → torch.Tensor |
adjust_saturation(img: torch.Tensor, saturation_factor: float) |
affine(img: torch.Tensor, angle: float, translate: List[int], scale: float, shear: List[float], resample: int = 0, fillcolor: Optional[int] = None) |
center_crop(img: torch.Tensor, output_size: List[int]) |
convert_image_dtype(image: torch.Tensor, dtype: torch.dtype = torch.float32) |
crop(img: torch.Tensor, top: int, left: int, height: int, width: int) |
erase(img: torch.Tensor, i: int, j: int, h: int, w: int, v: torch.Tensor, inplace: bool = False) |
five_crop(img: torch.Tensor, size: List[int]) |
gaussian_blur(img: torch.Tensor, kernel_size: List[int], sigma: Optional[List[float]] = None) |
hflip(img: torch.Tensor) |
normalize(tensor: torch.Tensor, mean: List[float], std: List[float], inplace: bool = False) |
pad(img: torch.Tensor, padding: List[int], fill: int = 0, padding_mode: str = constant) |
perspective(img: torch.Tensor, startpoints: List[List[int]], endpoints: List[List[int]], interpolation: int = 2, fill: Optional[int] = None) |
pil_to_tensor(pic) |
resize(img: torch.Tensor, size: List[int], interpolation: int = 2) |
resized_crop(img: torch.Tensor, top: int, left: int, height: int, width: int, size: List[int], interpolation: int = 2) |
rgb_to_grayscale(img: torch.Tensor, num_output_channels: int = 1) |
rotate(img: torch.Tensor, angle: float, resample: int = 0, expand: bool = False, center: Optional[List[int]] = None, fill: Optional[int] = None) |
ten_crop(img: torch.Tensor, size: List[int], vertical_flip: bool = False) |
to_grayscale(img, num_output_channels=1) |
to_pil_image(pic, mode=None) |
to_tensor(pic) |
vflip(img: torch.Tensor) |
utils.save_image(tensor: Union[torch.Tensor, List[torch.Tensor]], fp: Union[str, pathlib.Path, BinaryIO], nrow: int = 8, padding: int = 2, normalize: bool = False, range: Optional[Tuple[int, int]] = None, scale_each: bool = False, pad_value: int = 0, format: Optional[str] = None) |
utils.make_grid(tensor: Union[torch.Tensor, List[torch.Tensor]], nrow: int = 8, padding: int = 2, normalize: bool = False, range: Optional[Tuple[int, int]] = None, scale_each: bool = False, pad_value: int = 0) |
如上表所示,Torchvision 提供了一组强大的功能操作,可用于处理图像数据。每个操作都有自己的一组参数,用于强大的控制。
此外,Torchvision 提供了用于简化 I/O 和操作的函数。表 8-16 列出了其中一些函数。
表 8-16. Torchvision 的 I/O 和操作函数
| 函数 |
|---|
| 视频 |
io.read_video(filename: str, start_pts: int = 0, end_pts: Optional[float] = None, pts_unit: str = pts) |
io.read_video_timestamps9filename: str, pts_unit: str = pts) |
io.write_video9filename: str, video_array: torch.Tensor, _fps: float, video_codec: str = *libx264*, options: Optional[Dict[str, Any]] = None) |
| 细粒度视频 |
io.VideoReader(path, stream=video) |
| 图像 |
io.decode_image(input: torch.Tensor) |
io.encode_jpeg(input: torch.Tensor, quality: int = 75) |
io.read_image(path: str) |
io.write_jpeg(input: torch.Tensor, filename: str, quality: int = 75) |
io.encode_png(input: torch.Tensor, compression_level: int = 6) |
io.write_png(input: torch.Tensor, filename: str, compression_level: int = 6) |
上述函数是为了让您能够快速读取和写入多种格式的视频和图像文件。它们让您能够加快图像和视频处理的速度,而无需从头开始编写这些函数。
如您所见,Torchvision 是一个功能丰富、得到良好支持且成熟的 PyTorch 包。本节提供了 Torchvision API 的快速参考。在下一节中,我们将探索另一个用于 NLP 和文本应用的流行 PyTorch 包 Torchtext。
Torchtext 用于 NLP
Torchtext 包含一系列用于数据处理的实用工具和流行的 NLP 数据集。Torchtext API 与 Torchvision API 略有不同,但整体方法是相同的。
创建数据集对象
首先创建一个数据集,并描述一个预处理流水线,就像我们在 Torchvision 转换中所做的那样。Torchtext 提供了一组知名数据集。例如,我们可以加载 IMDb 数据集,如下面的代码所示:
from torchtext.datasets import IMDB
train_iter, test_iter = \
IMDB(split=('train', 'test'))
next(train_iter)
# out:
# ('neg',
# 'I rented I AM CURIOUS-YELLOW ...)
我们自动创建一个迭代器,并可以使用next()访问数据。
警告
Torchtext 在 PyTorch 1.8 中显著改变了其 API。如果本节中的代码返回错误,您可能需要升级 PyTorch 的版本。
预处理数据
Torchtext 还提供了预处理文本和创建数据管道的功能。预处理任务可能包括定义分词器、词汇表和数值嵌入。
在新的 Torchtext API 中,您可以使用data.get_tokenizer()函数访问不同的分词器,如下面的代码所示:
from torchtext.data.utils \
import get_tokenizer
tokenizer = get_tokenizer('basic_english')
在新 API 中创建词汇表也是灵活的。您可以直接使用Vocab类构建词汇表,如下面的代码所示:
from collections import Counter
from torchtext.vocab import Vocab
train_iter = IMDB(split='train')
counter = Counter()
for (label, line) in train_iter:
counter.update(tokenizer(line))
vocab = Vocab(counter,
min_freq=10,
specials=('<unk>',
'<BOS>',
'<EOS>',
'<PAD>'))
如您所见,我们可以设置min_freq来指定词汇表中的截止频率。我们还可以将特殊符号分配给特殊符号,比如<BOS>和<EOS>,如Vocab类的构造函数所示。
另一个有用的功能是为文本和标签定义转换,如下面的代码所示:
text_transform = lambda x: [vocab['<BOS>']] \
+ [vocab[token] \
for token in tokenizer(x)] + [vocab['<EOS>']]
label_transform = lambda x: 1 \
if x == 'pos' else 0
print(text_transform("programming is awesome"))
# out: [1, 8320, 12, 1156, 2]
我们将文本字符串传递给我们的转换,然后使用词汇表和分词器对数据进行预处理。
创建用于批处理的数据加载器
现在我们已经加载并预处理了数据,最后一步是创建一个数据加载器,以从数据集中对数据进行采样和批处理。我们可以使用以下代码创建一个数据加载器:
from torch.utils.data import DataLoader
train_iter = IMDB(split='train')
train_dataloader = DataLoader(
list(train_iter),
batch_size=8,
shuffle=True)
# for text, label in train_dataloader
您可能会注意到,这段代码与我们在 Torchvision 中创建数据加载器的代码相似。我们不是传入数据集对象,而是将train_iter转换为list()传入。DataLoader()构造函数还接受batch_sampler和collate_fcn参数(在上述代码中未显示;请参阅文档),因此您可以自定义数据集的采样和整理方式。创建数据加载器后,使用它来训练您的模型,如上述代码注释所示。
Torchtext 具有许多有用的功能。让我们探索 API 中提供的内容。
数据(torchtext.data)
torchtext.data API 提供了在 PyTorch 中创建基于文本的数据集对象的函数。表 8-17 列出了torchtext.data中可用的函数。
表 8-17. Torchtext 数据
| 函数 | 描述 |
|---|---|
torchtext.data.utils |
|
get_tokenizer(tokenizer, language=en) |
为字符串句子生成一个分词器函数 |
ngrams_iterator(token_list, ngrams) |
返回一个迭代器,产生给定标记及其 ngrams |
torchtext.data.functional |
|
generate_sp_model(filename, vocab_size=20000, model_type=unigram, model_prefix=m_user) |
训练一个SentencePiece分词器 |
load_sp_model(spm) |
从文件加载一个SentencePiece模型 |
sentencepiece_numericalizer(sp_model) |
创建一个生成器,接受文本句子并根据SentencePiece模型输出相应的标识符 |
sentencepiece_tokenizer(sp_model) |
创建一个生成器,接受文本句子并根据SentencePiece模型输出相应的标记 |
custom_replace(replace_pattern) |
作为一个转换器,将文本字符串转换 |
simple_space_split(iterator) |
作为一个转换器,通过空格分割文本字符串 |
numericalize_tokens_from_iterator(vocab, iterator, removed_tokens=None) |
从一个标记迭代器中产生一个标识符列表,使用vocab |
torchtext.data.metrics |
|
bleu_score(candidate_corpus, references_corpus, max_n=4, weights=[0.25, 0.25, 0.25, 0.25]) |
计算候选翻译语料库和参考翻译语料库之间的 BLEU 分数 |
正如您所看到的,torchtext.data子模块支持根据字段创建数据集对象的函数,以及加载、预处理和迭代批次。接下来让我们看看 Torchtext 库中提供的 NLP 数据集有哪些。
数据集(torchtext.datasets)
Torchtext 支持从流行论文和研究中加载数据集。您可以找到用于语言建模、情感分析、文本分类、问题分类、蕴涵、机器翻译、序列标记、问题回答和无监督学习的数据集。
表 8-18 提供了 Torchtext 中包含的数据集的全面列表。
表 8-18. Torchtext 数据集
| 函数 | 描述 |
|---|---|
| 文本分类 | |
TextClassificationDataset(vocab, data, labels) |
通用文本分类数据集 |
IMDB(root=*.data*, split=(*train*, *test*)) |
包含来自 IMDb 的 50,000 条评论,标记为正面或负面的二元情感分析数据集 |
AG_NEWS(root=*.data*, split=(*train*, *test*)) |
包含四个主题标记的新闻文章数据集 |
SogouNews(root=*.data*, split=(*train*, *test*)) |
包含五个主题标记的新闻文章数据集 |
DBpedia(root=*.data*, split=(*train*, *test*)) |
包含 14 个类别标记的新闻文章数据集 |
YelpReviewPolarity(root=*.data*, split=(*train*, *test*)) |
包含 50 万条 Yelp 评论的二元分类数据集 |
YelpReviewFull(root=*.data*, split=(*train*, *test*)) |
包含 50 万条 Yelp 评论的数据集,具有细粒度(五类)分类 |
YahooAnswers(root=*.data*, split=(*train*, *test*)) |
包含 10 个不同类别的 Yahoo 答案的数据集 |
AmazonReviewPolarity(root=*.data*, split=(*train*, *test*)) |
包含亚马逊评论的数据集,具有二元分类 |
AmazonReviewFull(root=*.data*, split=(*train*, *test*)) |
包含亚马逊评论的数据集,具有细粒度(五类)分类 |
| 语言建模 | |
LanguageModelingDataset(path, text_field, newline_eos=True, encoding=*utf-8*, **kwargs) |
通用语言建模数据集类 |
WikiText2(root=*.data*, split=(*train*, *valid*, *test*)) |
WikiText 长期依赖语言建模数据集,从维基百科上经过验证的“优秀”和“精选”文章中提取的超过 1 亿个标记的集合 |
WikiText103(root=*.data*, split=(*train*, *valid*, *test*)) |
更大的 WikiText 数据集 |
PennTreebank(root=*.data*, split=(*train*, *valid*, *test*)) |
最初为词性(POS)标记创建的相对较小的数据集 |
| 机器翻译 | |
TranslationDataset(path, exts, fields, **kwargs) |
通用翻译数据集类 |
IWSLT2016(root=*.data*, split=(*train*, *valid*, *test*), language_pair=(*de*, *en*)*,* *valid_set=tst2013, test_set=tst2014**)` |
国际口语翻译会议(IWSLT)2016 TED 演讲翻译任务 |
IWSLT2017(root=*.data*, split=(*train*, *valid*, *test*), language_pair=(*de*, *en*)) |
国际口语翻译会议(IWSLT)2017 TED 演讲翻译任务 |
| 序列标记 | |
SequenceTaggingDataset(path, fields, encoding=*utf-8*, separator=*t*, **kwargs) |
通用序列标记数据集类 |
UDPOS(root=*.data*, split=(*train*, *valid*, *test*)) |
通用依存关系版本 2 词性标记数据 |
CoNLL2000Chunking(root=*.data*, split=(*train*, *test*)) |
下载和加载 Conference on Computational Natural Language Learning (CoNLL) 2000 分块数据集的命令 |
| 问答 | |
SQuAD1(root=*.data*, split=(*train*, *dev*)) |
创建斯坦福问答数据集(SQuAD)1.0 数据集,这是一个由众包工作者在一组维基百科文章上提出的问题的阅读理解数据集 |
SQuAD2(root=.data, split=(train, dev)) |
创建斯坦福问答数据集(SQuAD)2.0 数据集,该数据集通过添加超过 5 万个无法回答的问题扩展了 1.0 数据集 |
Torchtext 开发人员始终在添加新的数据集。要获取最新列表,请访问Torchtext 数据集文档。
加载数据后,无论是来自现有数据集还是您创建的数据集,您都需要在训练模型和运行推理之前将文本数据转换为数值数据。为此,我们使用提供映射以执行这些转换的词汇表和词嵌入。接下来,我们将检查用于支持词汇表的 Torchtext 函数。
词汇表(torchtext.vocab)
Torchtext 提供了通用类和特定类来支持流行的词汇表。表 8-19 提供了torchtext.vocab中的类列表,以支持词汇表的创建和使用。
表 8-19. Torchtext 词汇表
| 功能 | 描述 |
|---|---|
| 词汇表类 | |
Vocab(counter, max_size=None, min_freq=1, specials=(<unk>, <pad>), vectors=None, unk_init=None, vectors_cache=None, specials_first=True) |
定义将用于数值化字段的词汇表对象 |
SubwordVocab(counter, max_size=None, specials=<pad>, vectors=None, unk_init=<method zero_of torch._C._TensorBase objects>) |
从collections.Counter创建一个revtok子词汇表 |
Vectors(name, cache=None, url=None, unk_init=None, max_vectors=None) |
用于词向量嵌入的通用类 |
| 预训练词嵌入 | |
GloVe(name=*840B*, dim=300, **kwargs) |
全局向量(GloVe)模型,用于分布式词表示,由斯坦福大学开发 |
FastText(language=en, **kwargs) |
294 种语言的预训练词嵌入,由 Facebook 的 AI 研究实验室创建 |
CharNGram(**kwargs) |
CharNGram 嵌入,一种学习基于字符的组合模型以嵌入文本序列的简单方法 |
| 杂项 | |
build_vocab_from_iterator(iterator, num_lines=None) |
通过循环遍历迭代器构建词汇表 |
正如您所看到的,Torchtext 提供了一套强大的功能,支持基于文本的建模和 NLP 研究。欲了解更多信息,请访问Torchtext 文档。
无论您是为 NLP、计算机视觉或其他领域开发深度学习模型,能够在开发过程中可视化模型、数据和性能指标是很有帮助的。在下一节中,我们将探索另一个强大的用于可视化的包,称为 TensorBoard。
用于可视化的 TensorBoard
TensorBoard 是一个可视化工具包,包含在 PyTorch 的主要竞争深度学习框架 TensorFlow 中。PyTorch 没有开发自己的可视化工具包,而是与 TensorBoard 集成,并原生地利用其可视化能力。
使用 TensorBoard,您可以可视化学习曲线、标量数据、模型架构、权重分布和 3D 数据嵌入,以及跟踪超参数实验结果。本节将向您展示如何在 PyTorch 中使用 TensorBoard,并提供 TensorBoard API 的参考。
TensorBoard 应用程序在本地或远程服务器上运行,显示和用户界面在浏览器中运行。我们还可以在 Jupyter Notebook 或 Google Colab 中运行 TensorBoard。
我将在本书中使用 Colab 来演示 TensorBoard 的功能,但在本地或远程云中运行它的过程非常类似。Colab 预装了 TensorBoard,您可以直接在单元格中使用魔术命令运行它,如下所示的代码:
%load_ext tensorboard
%tensorboard --logdir ./runs/
首先我们加载tensorboard扩展,然后运行tensorboard并指定保存事件文件的日志目录。事件文件保存了来自 PyTorch 的数据,将在 TensorBoard 应用程序中显示。
由于我们尚未创建任何事件文件,您将看到一个空的显示,如图 8-1 所示。

图 8-1. TensorBoard 应用程序
通过单击右上角菜单中 INACTIVE 旁边的箭头,您将看到可能的显示选项卡。一个常用的显示选项卡是 SCALARS 选项卡。此选项卡可以显示随时间变化的任何标量值。我们经常使用 SCALARS 显示来查看损失和准确率训练曲线。让我们看看如何在您的 PyTorch 代码中保存标量值以供 TensorBoard 使用。
注意
PyTorch 与 TensorBoard 的集成最初是由一个名为 TensorBoardX 的开源项目实现的。自那时起,TensorBoard 支持已集成到 PyTorch 项目中,作为torch.utils.tensorboard包,并由 PyTorch 开发团队积极维护。
首先让我们导入 PyTorch 的 TensorBoard 接口,并设置 PyTorch 以便与 TensorBoard 一起使用,如下所示的代码:
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter() # ①
①
默认情况下,写入器将输出到./runs/目录。
我们只需从 PyTorch 的tensorboard包中导入SummaryWriter类,并实例化一个SummaryWriter对象。要将数据写入 TensorBoard,我们只需要调用SummaryWriter对象的方法。在模型训练时保存我们的损失数值,我们使用add_scalar()方法,如下面的代码所示:
N_EPOCHS = 10
for epoch in range(N_EPOCHS):
epoch_loss = 0.0
for inputs, labels in trainloader:
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
print("Epoch: {} Loss: {}".format(epoch,
epoch_loss/len(trainloader)))
writer.add_scalar('Loss/train',
epoch_loss/len(trainloader), epoch) # ①
①
将loss.item()作为事件记录到tensorboard。
这只是一个示例训练循环。您可以假设model已经被定义,trainloader已经被创建。代码不仅在每个 epoch 打印损失,还将其记录到tensorboard事件中。我们可以刷新之前单元格中的 TensorBoard 应用程序,或者使用%tensorboard命令创建另一个单元格。
使用 SCALARS 查看学习曲线
TensorBoard 提供了绘制一个或多个标量值随时间变化的能力。在深度学习开发中,这对于显示模型训练时的指标非常有用。通过查看损失或准确率等指标,您可以轻松地看到您的模型训练是否稳定并持续改进。
图 8-2 展示了使用 TensorBoard 显示学习曲线的示例。
您可以通过滑动平滑因子与显示进行交互,也可以通过将鼠标悬停在绘图上查看每个 epoch 的曲线。TensorBoard 允许您应用平滑处理以消除不稳定性并显示整体进展。

图 8-2. TensorBoard 学习曲线
使用 GRAPHS 查看模型架构
TensorBoard 的另一个有用功能是使用图形可视化您的深度学习模型。要将图形保存到事件文件中,我们将使用add_graph()方法,如下面的代码所示:
model = vgg16(preTrained=True)
writer.add_graph(model)
在这段代码中,我们实例化了一个 VGG16 模型,并将模型写入事件文件。我们可以通过刷新现有的 TensorBoard 单元格或创建一个新的单元格来显示模型图。图 8-3 展示了 TensorBoard 中的图形可视化工具。

图 8-3. TensorBoard 模型图
该图是交互式的。您可以单击每个模块并展开以查看底层模块。这个工具对于理解现有模型并验证您的模型图是否符合其预期设计非常有用。
使用图像、文本和投影仪的数据
您还可以使用 TensorBoard 查看不同类型的数据,例如图像、文本和 3D 嵌入。在这些情况下,您将分别使用add_image()、add_text()和add_projection()方法将数据写入事件文件。
图 8-4 显示了来自 Fashion-MNIST 数据集的一批图像数据。
通过检查图像数据的批次,您可以验证数据是否符合预期,或者识别数据或结果中的错误。TensorBoard 还提供了监听音频数据、显示文本数据以及查看多维数据或数据嵌入的 3D 投影的能力。

图 8-4. TensorBoard 图像显示
使用 DISTRIBUTIONS 和 HISTOGRAMS 查看权重分布
TensorBoard 的另一个有用功能是显示分布和直方图。这使您可以查看大量数据以验证预期行为或识别问题。
模型开发中的一个常见任务是确保避免梯度消失问题。当模型权重变为零或接近零时,梯度消失就会发生。当这种情况发生时,神经元基本上会死亡,无法再更新。
如果我们可视化我们的权重分布,很容易看到大部分权重值已经达到零。
图 8-5 展示了 TensorBoard 中的 DISTRIBUTIONS 选项卡。在这里,我们可以检查我们的权重值的分布。
如您在图 8-5 中所见,TensorBoard 可以以 3D 显示分布,因此很容易看到分布随时间或每个时期如何变化。

图 8-5. TensorBoard 权重分布
具有 HPARAMS 的超参数
在运行深度学习实验时,很容易迷失不同超参数集的跟踪,以尝试假设。TensorBoard 提供了一种在每次实验期间跟踪超参数值并将值及其结果制表的方法。
图 8-6 显示了我们如何跟踪实验及其相应的超参数和结果的示例。
在 HPARAMS 选项卡中,您可以以表格视图、平行坐标视图或散点图矩阵视图查看结果。每个实验由其会话组名称、超参数(如丢失百分比和优化器算法)以及结果指标(如准确性)标识。HPARAMS 表格可帮助您跟踪实验和结果。
当您完成向 TensorBoard 事件文件写入数据时,应使用close()方法,如下所示:
writer.close()
这将调用析构函数并释放用于摘要写入器的任何内存。

图 8-6. TensorBoard 超参数跟踪
TensorBoard API
PyTorch TensorBoard API 非常简单。它作为torch.utils.tensorboard的一部分包含在torch.utils包中。表 8-20 显示了用于将 PyTorch 与 TensorBoard 接口的函数的全面列表。
表 8-20. PyTorch TensorBoard API
| 方法 | 描述 |
|---|---|
SummaryWriter(log_dir=None, comment='', purge_step=None, max_queue=10, flush_secs=120, filename_suffix='') |
创建SummaryWriter对象 |
flush() |
将事件文件刷新到磁盘;确保所有待处理事件都已写入磁盘 |
close() |
释放SummaryWriter对象并关闭事件文件 |
add_scalar(tag, scalar_value, global_step=None, walltime=None) |
将标量写入事件文件 |
add_scalars(main_tag, tag_scalar_dict, global_step=None, walltime=None) |
将多个标量写入事件文件以在同一图中显示多个标量 |
add_custom_scalars(layout) |
通过收集标量中的图表标签创建特殊图表 |
add_histogram(tag, values, global_step=None, bins=tensorflow, walltime=None, max_bins=None) |
为直方图显示写入数据 |
add_image(tag, img_tensor, global_step=None, walltime=None, dataformats=CHW) |
写入图像数据 |
add_images(tag, img_tensor, global_step=None, walltime=None, dataformats=NCHW) |
将多个图像写入同一显示 |
add_figure(tag, figure, global_step=None, close=True, walltime=None) |
将matplotlib类型的图绘制为图像 |
add_video(tag, vid_tensor, global_step=None, fps=4, walltime=None``) |
写入视频 |
add_audio(tag, snd_tensor, global_step=None, sample_rate=44100, walltime=None) |
将音频文件写入事件摘要 |
add_text(tag, text_string, global_step=None, walltime=None) |
将文本数据写入摘要 |
add_graph(model, input_to_model=None, verbose=False) |
将模型图或计算图写入摘要 |
add_embedding(mat, metadata=None, label_img=None, global_step=None, tag=default, metadata_header=None) |
将嵌入投影仪数据写入摘要 |
add_pr_curve(tag, labels, predictions, global_step=None, num_thresholds=127, weights=None, walltime=None) |
在不同阈值下写入精度/召回率曲线 |
add_mesh(tag, vertices, colors=None, faces=None, config_dict=None, global_step=None, walltime=None) |
将网格或 3D 点云添加到 TensorBoard |
add_hparams(hparam_dict, metric_dict, hparam_domain_discrete=None, run_name=None):向 TensorBoard 中添加一组超参数以进行比较。 |
如表 8-20 所示,API 很简单。您可以使用SummaryWriter()、flush()和close()方法来管理写入对象,并使用其他函数向 TensorBoard 事件文件添加数据。
有关 TensorBoard PyTorch API 的更多详细信息,请访问 TensorBoard API 文档。有关如何使用 TensorBoard 应用程序本身的更多详细信息,请访问 TensorBoard 文档。
TensorBoard 通过提供可视化工具解决了在 PyTorch 中开发深度学习模型时的一个主要挑战。另一个主要挑战是跟上最新研究和最先进的解决方案。研究人员经常需要重现结果并利用代码来对比自己的设计。在接下来的部分中,我们将探讨 Papers with Code,这是一个您可以使用的资源来解决这个问题。
Papers with Code
Papers with Code(PwC)是一个网站,它整理了机器学习研究论文及其相应的代码,这些代码通常是用 PyTorch 编写的。PwC 允许您轻松重现实验并扩展当前研究,该网站还允许您找到给定机器学习主题的表现最佳的研究论文。例如,想要找到最佳的图像分类模型及其代码吗?只需点击图像分类瓷砖,您将看到研究领域的摘要以及 GitHub 上相应论文和代码的基准和链接。图 8-7 展示了图像分类的示例列表。
“Papers With Code Image Classification Listing”图片
图 8-7. Papers with Code
PwC 并不是一个专门的 PyTorch 项目;然而,PwC 提供的大多数代码都使用 PyTorch。它可能有助于您了解当前最先进的研究并解决您在深度学习和人工智能方面的问题。在 PwC 网站上探索更多。
额外的 PyTorch 资源
阅读完这本书后,您应该对 PyTorch 及其功能有很好的理解。然而,总是有新的方面可以探索和实践。在本节中,我将提供一些额外资源的列表,您可以查看以了解更多信息,并提升您在 PyTorch 中的技能。
教程
PyTorch 网站提供了大量的文档和教程。如果您正在寻找更多的代码示例,这个资源是一个很好的起点。图 8-8 展示了 PyTorch 教程网站,您可以选择标签来帮助您找到感兴趣的教程。
“PyTorch 教程网站”图片
图 8-8. PyTorch 教程
该网站包括一个 60 分钟的闪电战、PyTorch 食谱、教程和 PyTorch 备忘单。大多数代码和教程都可以在 GitHub 上找到,并且可以在 VS Code、Jupyter Notebook 和 Colab 中运行。
60 分钟闪电战是一个很好的起点,可以帮助您恢复技能或复习 PyTorch 的基础知识。PyTorch 食谱是关于如何使用特定 PyTorch 功能的简短、可操作的示例。PyTorch 教程比食谱稍长,由多个步骤组成,以实现或演示一个结果。
目前,您可以找到与以下主题相关的教程:
-
音频
-
最佳实践
-
C++
-
CUDA
-
扩展 PyTorch
-
FX
-
前端 API
-
入门
-
图像/视频
-
可解释性
-
内存格式
-
移动
-
模型优化
-
并行和分布式训练
-
生产
-
性能分析
-
量化
-
强化学习
-
TensorBoard
-
文本
-
TorchScript
PyTorch 团队不断添加新资源,这个列表肯定会发生变化。有关更多信息和最新教程,请访问 PyTorch 教程网站。
书籍
教程是学习的好方法,但也许您更喜欢阅读有关 PyTorch 的更多信息,并从多位作者的不同视角获得不同观点。表 8-21 提供了与 PyTorch 相关的其他书籍列表。
表 8-21. PyTorch 书籍
| 书籍 | 出版商,年份 | 摘要 |
|---|---|---|
| 云原生机器学习 by Carl Osipov | Manning, 2021 | 学习如何在 AWS 上部署 PyTorch 模型 |
| 使用 fastai 和 PyTorch 进行编码人员的深度学习 by Jeremy Howard 和 Sylvain Gugger | O’Reilly, 2020 | 学习如何在没有博士学位的情况下构建人工智能应用程序 |
| 使用 PyTorch 进行深度学习 by Eli Stevens 等 | Manning, 2019 | 学习如何使用 Python 工具构建、训练和调整神经网络 |
| 使用 PyTorch 进行深度学习 by Vishnu Subramanian | Packt, 2018 | 学习如何使用 PyTorch 构建神经网络模型 |
| 使用 PyTorch 1.x 进行实用生成对抗网络 by John Hany 和 Greg Walters | Packt, 2019 | 学习如何使用 Python 实现下一代神经网络,构建强大的 GAN 模型 |
| Hands-On Natural Language Processing with PyTorch 1.x by Thomas Dop | Packt, 2020 | 学习如何利用深度学习和自然语言处理技术构建智能的人工智能驱动的语言应用程序 |
| 使用 PyTorch 1.0 进行实用神经网络 by Vihar Kurama | Packt, 2019 | 学习如何在 PyTorch 中实现深度学习架构 |
| 使用 PyTorch 进行自然语言处理 by Delip Rao 和 Brian McMahan | O’Reilly, 2019 | 学习如何利用深度学习构建智能语言应用程序 |
| 使用 PyTorch 进行实用深度学习 by Nihkil Ketkar | Apress, 2020 | 学习如何使用 Python 优化 GAN |
| 为深度学习编程 PyTorch by Ian Pointer | O’Reilly, 2019 | 学习如何创建和部署深度学习应用程序 |
| PyTorch 人工智能基础 by Jibin Mathew | Packt, 2020 | 学习如何设计、构建和部署自己的 PyTorch 1.x AI 模型 |
| PyTorch 食谱 by Pradeepta Mishra | Apress, 2019 | 学习如何在 PyTorch 中解决问题 |
在线课程和现场培训
如果您更喜欢在线视频课程和现场培训研讨会,您可以选择扩展您的 PyTorch 知识和技能的选项。您可以继续从 PyTorch Academy、Udemy、Coursera、Udacity、Skillshare、DataCamp、Pluralsight、edX、O’Reilly Learning 和 LinkedIn Learning 等在线讲师那里学习。一些课程是免费的,而其他课程需要付费或订阅。
表 8-22 列出了撰写时可用的 PyTorch 在线课程的选择。
表 8-22. PyTorch 课程
| 课程 | 讲师 | 平台 |
|---|---|---|
| 开始使用 PyTorch 开发 | Joe Papa | PyTorch Academy |
| PyTorch 基础 | Joe Papa | PyTorch Academy |
| 高级 PyTorch | Joe Papa | PyTorch Academy |
| 使用 PyTorch 进行深度学习入门 | Ismail Elezi | DataCamp |
| PyTorch 基础 | Janani Ravi | Pluralsight |
| 使用 PyTorch 的深度神经网络 | IBM | Coursera |
| 用于机器学习的 PyTorch 基础 | IBM | edX |
| 使用 PyTorch 进行深度学习入门 | Facebook AI | Udacity |
| PyTorch:深度学习和人工智能 | 懒惰的程序员 | Udemy |
| 用于深度学习和计算机视觉的 PyTorch | Rayan Slim 等 | Udemy |
| PyTorch 入门 | Dan We | Skillshare |
| PyTorch 基础培训:深度学习 | Jonathan Fernandes | LinkedIn Learning |
| 使用 PyTorch 介绍深度学习 | Goku Mohandas 和 Alfredo Canziani | O’Reilly Learning |
本章提供了扩展您学习、研究和开发 PyTorch 的资源。您可以将这些材料作为 PyTorch 项目和 PyTorch 生态系统中众多软件包的快速参考。当您希望扩展您的技能和知识时,可以返回本章,获取其他培训材料的想法。
恭喜您完成了这本书!您已经走了很长一段路,掌握了张量,理解了模型开发过程,并探索了使用 PyTorch 的参考设计。此外,您还学会了如何定制 PyTorch,创建自己的特性,加速训练,优化模型,并将您的神经网络部署到云端和边缘设备。最后,我们探索了 PyTorch 生态系统,调查了关键软件包如 Torchvision、Torchtext 和 TensorBoard,并了解了通过教程、书籍和在线课程扩展知识的其他方法。
无论您将来要处理什么项目,我希望您能一次又一次地返回这本书。我也希望您继续扩展您的技能,并掌握 PyTorch 的能力,开发创新的新深度学习工具和系统。不要让您的新知识和技能消失。去构建一些有趣的东西,在世界上产生影响!
让我知道您创造了什么!我希望在PyTorch Academy的课程中见到您,并随时通过电子邮件(jpapa@joepapa.ai)、Twitter(@JoePapaAI)或 LinkedIn(@MrJoePapa)联系我。


浙公网安备 33010602011771号