PyTorch 深度学习编程(一)
原文:Programming Pytorch for Deep Learning
译者:飞龙
前言
当今世界的深度学习
你好,欢迎!本书将通过 PyTorch 这个由 Facebook 于 2017 年发布的开源库来介绍深度学习。除非你过去几年一直把头埋在地里,否则你一定会注意到神经网络如今无处不在。它们已经从计算机科学中人们学习后却不做任何事情的非常酷的部分,变成了我们每天随身携带的手机中,用来改善我们的图片或听取我们的语音指令。我们的电子邮件软件读取我们的邮件并生成上下文相关的回复,我们的扬声器倾听我们,汽车自动驾驶,计算机终于在围棋上战胜了人类。我们还看到这项技术被用于更邪恶的目的,在威权国家,神经网络支持的哨兵可以从人群中识别出面孔,并决定是否应该逮捕他们。
尽管感觉一切发生得如此迅速,但神经网络和深度学习的概念早已存在很久。证明这样一个网络可以作为一种以近似方式替代任何数学函数的方式运行的证据,这是神经网络可以被训练用于许多不同任务的基础,可以追溯到 1989 年,而卷积神经网络在 90 年代后期就被用来识别支票上的数字了。这一切时间里一直在建立坚实的基础,那么为什么感觉在过去的 10 年里发生了爆炸呢?
有很多原因,但其中最主要的原因必须是图形处理单元(GPU)性能的激增以及它们日益可负担的价格。最初设计用于游戏的 GPU 需要每秒执行数以百万计的矩阵运算,以便在您的游戏机或 PC 上渲染所有多边形,这些操作是标准 CPU 无法优化的。2009 年的一篇论文,“使用图形处理器进行大规模深度无监督学习”由 Rajat Raina 等人指出,训练神经网络也是基于执行大量矩阵运算,因此这些附加的图形卡可以用于加速训练,同时使更大、更深的神经网络架构首次变得可行。其他重要技术,如Dropout(我们将在第三章中讨论),也在过去的十年中被引入,作为不仅加速训练而且使训练更泛化的方法(这样网络不仅学会识别训练数据,还会遇到我们将在下一章中遇到的过拟合问题)。在过去几年里,公司们已经将这种基于 GPU 的方法推向了一个新的水平,谷歌创建了他们所描述的张量处理单元(TPUs),这些设备专门用于尽可能快地执行深度学习,并且甚至作为谷歌云生态系统的一部分向普通公众提供。
过去十年来,追踪深度学习的进展的另一种方式是通过 ImageNet 竞赛。ImageNet 是一个包含超过 1400 万张图片的庞大数据库,手动标记为 20000 个类别,对于机器学习目的来说,ImageNet 是一个标记数据的宝库。自 2010 年以来,每年的 ImageNet 大规模视觉识别挑战赛一直试图测试所有参与者对数据库的 1000 个类别子集的处理能力,直到 2012 年,挑战的错误率一直在 25%左右。然而,那一年,一个深度卷积神经网络以 16%的错误率赢得了比赛,远远超过了所有其他参赛者。随着接下来的几年,错误率不断下降,直到 2015 年,ResNet 架构获得了 3.6%的结果,超过了 ImageNet 上的平均人类表现(5%)。我们被超越了。
但深度学习究竟是什么,我需要博士学位才能理解吗?
深度学习的定义通常比启发性更令人困惑。一种定义深度学习的方式是说,深度学习是一种利用多个和众多层的非线性变换逐渐从原始输入中提取特征的机器学习技术。这是正确的,但并没有真正帮助,对吧?我更喜欢将其描述为一种通过提供输入和期望输出来解决问题的技术,并让计算机找到解决方案,通常使用神经网络。
深度学习中吓倒很多人的一件事是数学。看看这个领域的任何论文,你将会看到几乎无法理解的大量符号,到处都是希腊字母,你可能会吓得四处奔跑。事实是:在大多数情况下,你不需要成为数学天才来使用深度学习技术。实际上,对于技术的大多数日常基本用途,你根本不需要了解太多,要真正理解正在发生的事情(正如你将在第二章中看到的那样),你只需要稍微努力一下,理解你可能在高中学到的概念。所以不要太害怕数学。到第三章结束时,你将能够用几行代码组建一个图像分类器,与 2015 年最优秀的人才所能提供的相媲美。
PyTorch
正如我在开头提到的,PyTorch 是 Facebook 提供的一个开源工具,可以在 Python 中编写深度学习代码。它有两个来源。首先,也许并不奇怪,鉴于其名称,它从 Torch 中获得了许多功能和概念,Torch 是一个基于 Lua 的神经网络库,可以追溯到 2002 年。它的另一个主要来源是 Chainer,于 2015 年在日本创建。Chainer 是最早提供了一种急切的差异化方法而不是定义静态图的神经网络库之一,这种方法允许在创建、训练和操作网络时具有更大的灵活性。Torch 的遗产加上 Chainer 的思想使得 PyTorch 在过去几年中变得流行。
该库还配备了一些模块,可帮助处理文本、图像和音频(torchtext、torchvision和torchaudio),以及流行架构的内置变体,如 ResNet(可下载权重以提供对迁移学习等技术的帮助,你将在第四章中看到)。
除了 Facebook 之外,PyTorch 在工业界得到了快速的接受,包括 Twitter、Salesforce、Uber 和 NVIDIA 等公司在其深度学习工作中以各种方式使用它。啊,但我感觉到有一个问题要来了……
那么 TensorFlow 呢?
是的,让我们来谈谈那只角落里的相当大的、带有 Google 标志的大象。PyTorch 提供了什么,TensorFlow 没有的?为什么你应该学习 PyTorch 呢?
答案是传统的 TensorFlow 与 PyTorch 的工作方式不同,这对于代码和调试有重大影响。在 TensorFlow 中,您使用库来构建神经网络架构的图表示,然后在该图上执行操作,这发生在 TensorFlow 库内部。这种声明式编程方法与 Python 更为命令式的范式有些不符,这意味着 Python TensorFlow 程序可能看起来和感觉有些奇怪和难以理解。另一个问题是静态图声明可能会使在训练和推断时动态修改架构变得更加复杂和充满样板代码,而不像 PyTorch 的方法那样简单。
出于这些原因,PyTorch 在面向研究的社区中变得流行。在过去一年中,提交给国际学习表示会议的论文中提到PyTorch的数量增加了 200%,提到TensorFlow的论文数量几乎同样增加。PyTorch 绝对会持续存在。
然而,在更近期的 TensorFlow 版本中,一项名为eager execution的新功能已被添加到库中,使其能够类似于 PyTorch 工作,并且将是 TensorFlow 2.0 中推广的范式。但由于在谷歌之外的资源帮助您学习这种与 PyTorch 类似的工作方法的资源稀缺,再加上您需要多年的工作经验来理解另一种范式,以便充分利用该库。
但这一切都不应让您对 TensorFlow 产生负面看法;它仍然是一个经过行业验证的库,得到了全球最大公司之一的支持。PyTorch(当然,由全球另一家最大公司支持)是我会说,更简化和专注于深度学习和微分编程的方法。因为它不必继续支持旧的、陈旧的 API,所以在 PyTorch 中教学和变得高效比在 TensorFlow 中更容易。
Keras 在其中的位置如何?有很多好问题!Keras 是一个高级深度学习库,最初支持 Theano 和 TensorFlow,现在也支持某些其他框架,如 Apache MXNet。它提供了一些功能,如训练、验证和测试循环,这些功能在低级框架中留给开发人员自己实现,以及构建神经网络架构的简单方法。它对 TensorFlow 的推广做出了巨大贡献,现在已经成为 TensorFlow 本身的一部分(作为tf.keras),同时仍然是一个独立的项目。相比之下,PyTorch 在原始 TensorFlow 和 Keras 之间有些中间地带;我们将不得不编写自己的训练和推断例程,但创建神经网络几乎和 Keras 一样简单(我会说 PyTorch 的创建和重用架构方法对于 Python 开发人员来说比某些 Keras 的魔法更合乎逻辑)。
正如您在本书中所看到的,尽管 PyTorch 在更多面向研究的职位中很常见,但随着 PyTorch 1.0 的出现,它完全适用于生产用例。
本书使用的约定
本书使用以下排版约定:
斜体
指示新术语、URL、电子邮件地址、文件名和文件扩展名。
等宽
用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
等宽粗体
显示用户应该按照字面输入的命令或其他文本。
等宽斜体
显示应由用户提供值或由上下文确定值替换的文本。
提示
此元素表示提示或建议。
注意
此元素表示一般说明。
警告
此元素表示警告或注意事项。
使用代码示例
可下载补充材料(包括代码示例和练习)请访问https://oreil.ly/pytorch-github。
这本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们以获得许可。例如,编写一个程序使用本书中的几个代码块不需要许可。出售或分发包含 O'Reilly 图书示例的 CD-ROM 需要许可。引用本书并引用示例代码回答问题不需要许可。将本书中大量示例代码整合到产品文档中需要许可。
我们感谢但不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Ian Pointer(O'Reilly)的《深度学习 PyTorch 编程》。2019 年 Ian Pointer 著,978-1-492-04535-9。”
如果您认为您使用的代码示例超出了合理使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。
致谢
衷心感谢我的编辑 Melissa Potter,我的家人和 Tammy Edlund 在使这本书成为可能的过程中提供的所有帮助。还要感谢在写作过程中提供宝贵反馈的技术审阅人员,包括 Phil Rhodes、David Mertz、Charles Givre、Dominic Monn、Ankur Patel 和 Sarah Nagy。
请参见 George Cybenko(1989)的“由 Sigmoidal 函数的叠加逼近”。
请注意,PyTorch 从 Chainer 借鉴了一些想法,但没有实际代码。
第一章:开始使用 PyTorch
在本章中,我们设置了使用 PyTorch 所需的一切。一旦我们完成了这一步,随后的每一章都将在这个初始基础上构建,因此很重要我们做对。这导致我们的第一个基本问题:您应该构建一个自定义深度学习计算机,还是只使用众多可用的基于云的资源之一?
构建自定义深度学习机器
在深度学习中,有一种冲动,想要为自己的计算需求建造一个庞然大物。您可以花费数天时间查看不同类型的显卡,了解可能的 CPU 选择提供的内存通道,购买最佳类型的内存,以及购买多大的 SSD 驱动器以尽可能快地访问磁盘。我并不是在宣称自己对此免疫;几年前,我花了一个月的时间列出零件清单,在我的餐桌上组装了一台新电脑。
我的建议,特别是对于新手来说,是:不要这样做。您可以轻松地在一台您可能不会经常使用的机器上花费数千美元。相反,我建议您通过使用云资源(无论是亚马逊网络服务、谷歌云还是微软 Azure)来阅读本书,然后再考虑是否需要为自己建造一台机器,如果您觉得需要一台全天候运行的单机。您不需要在硬件上进行巨额投资来运行本书中的任何代码。
您可能永远不需要为自己建造一台定制机器。有一个甜蜜的点,如果您知道您的计算总是会受限于一台机器(最多几个 GPU),那么建造一个定制机器可能会更便宜。然而,如果您的计算开始需要跨越多台机器和 GPU,云再次变得有吸引力。考虑到组装一台定制机器的成本,我建议您在深入之前三思而后行。
如果我还没有劝阻您自己组装机器,接下来的部分将提供您需要做的建议。
GPU
每个深度学习盒子的核心,GPU,将为大多数 PyTorch 的计算提供动力,并且很可能是您机器中最昂贵的组件。近年来,由于它们在挖掘比特币等加密货币中的使用,GPU 的价格已经上涨,供应量也在减少。幸运的是,这个泡沫似乎正在消退,GPU 的供应量又变得更加充裕。
在撰写本文时,我建议选择 NVIDIA GeForce RTX 2080 Ti。如果想要更便宜的选项,可以选择 1080 Ti(尽管如果您因预算原因考虑选择 1080 Ti,我再次建议您考虑云选项)。尽管 AMD 制造的 GPU 卡确实存在,但它们在 PyTorch 中的支持目前还不够好,无法推荐除 NVIDIA 卡以外的其他选择。但请留意他们的 ROCm 技术,这将最终使它们成为 GPU 领域的可信替代品。
CPU/主板
您可能会选择 Z370 系列主板。许多人会告诉您,CPU 对于深度学习并不重要,只要有强大的 GPU,您可以使用速度较低的 CPU。但根据我的经验,CPU 往往会成为瓶颈,尤其在处理增强数据时。
RAM
更多的 RAM 是好的,因为这意味着您可以将更多数据保存在内存中,而不必访问速度慢得多的磁盘存储(尤其是在训练阶段非常重要)。您应该至少考虑为您的机器配备 64GB DDR4 内存。
存储
自定义机箱的存储应该分为两类:首先,一个 M2 接口固态硬盘(SSD)——尽可能大——用于存储您正在积极工作的项目时保持尽可能快的访问速度的热数据。对于第二类存储,添加一个 4TB 串行 ATA(SATA)驱动器,用于您当前不在积极工作的数据,并根据需要转移到热和冷存储。
我建议您查看PCPartPicker来浏览其他人的深度学习机器(您还可以看到所有奇怪和疯狂的机箱设计想法!)。您将了解到机器零件清单和相关价格,这些价格可能会大幅波动,尤其是 GPU 卡的价格。
现在您已经查看了本地的物理机器选项,是时候转向云端了。
云端深度学习
好了,那么为什么云端选项更好呢?尤其是如果您已经查看了亚马逊网络服务(AWS)的定价方案,并计算出构建深度学习机器将在六个月内收回成本?想一想:如果您刚开始,您不会在这六个月内全天候使用那台机器。您就是不会。这意味着您可以关闭云端机器,并支付存储数据的几分钱。
如果您刚开始,您不需要立即使用 NVIDIA 的庞大 Tesla V100 卡连接到您的云实例。您可以从更便宜的(有时甚至是免费的)基于 K80 的实例开始,然后在准备好时升级到更强大的卡。这比在自定义盒子上购买基本 GPU 卡并升级到 2080Ti 便宜一点。此外,如果您想在单个实例中添加八张 V100 卡,您只需点击几下即可。试试用您自己的硬件做到这一点。
另一个问题是维护。如果您养成良好的习惯,定期重新创建云实例(最好每次回来进行实验时都重新开始),您几乎总是会有一个最新的机器。如果您有自己的机器,更新就取决于您。这就是我承认我有自己的定制深度学习机器的地方,我忽视了它上面的 Ubuntu 安装很长时间,结果是它不再接收支持的更新,最终花了一整天的时间来让系统恢复到可以再次接收更新的状态。令人尴尬。
无论如何,您已经决定转向云端。万岁!接下来:选择哪个提供商?
Google Colaboratory
但等等——在我们查看提供商之前,如果您根本不想做任何工作怎么办?不想建立一台机器或者不想费心设置云端实例?哪里有真正懒惰的选择?谷歌为您提供了正确的东西。Colaboratory(或Colab)是一个大多数免费、无需安装的定制 Jupyter Notebook 环境。您需要一个谷歌账号来设置您自己的笔记本。图 1-1 显示了在 Colab 中创建的笔记本的屏幕截图。
Colab 之所以成为深度学习的绝佳方式,是因为它包含了预安装的 TensorFlow 和 PyTorch 版本,因此您无需进行任何设置,只需键入import torch,每个用户都可以免费获得长达 12 小时的连续运行时间的 NVIDIA T4 GPU。免费的。从这个角度来看,实证研究表明,您在训练时大约可以获得 1080 Ti 速度的一半,但内存额外增加了 5GB,因此您可以存储更大的模型。它还提供了连接到更近期的 GPU 和谷歌定制的 TPU 硬件的能力,但您几乎可以使用 Colab 免费完成本书中的每个示例。因此,我建议一开始就使用 Colab,并随后根据需要决定是否扩展到专用云实例和/或您自己的个人深度学习服务器。
图 1-1. 谷歌 Colab(实验室)
Colab 是零工作量的方法,但你可能想要对安装方式或在云端实例上获取安全外壳(SSH)访问有更多控制,因此让我们看看主要云服务提供商提供了什么。
云服务提供商
三大云服务提供商(亚马逊网络服务、谷歌云平台和微软的 Azure)都提供基于 GPU 的实例(也称为虚拟机或VMs)和官方镜像以部署在这些实例上。它们提供了一切你需要的,无需自己安装驱动程序和 Python 库即可运行。让我们看看每个提供商提供了什么。
亚马逊网络服务
AWS,云市场的 800 磅大猩猩,乐意满足你的 GPU 需求,并提供 P2 和 P3 实例类型来帮助你。(G3 实例类型更多用于实际的基于图形的应用程序,如视频编码,所以我们这里不涉及。)P2 实例使用较旧的 NVIDIA K80 卡(最多可以连接 16 个到一个实例),而 P3 实例使用快速的 NVIDIA V100 卡(如果你敢的话,你可以在一个实例上连接八个)。
如果你要使用 AWS,我建议选择p2.xlarge类。在撰写本书时,这将花费你每小时仅 90 美分,并为你提供足够的计算能力来完成示例。当你开始参加一些有挑战性的 Kaggle 比赛时,你可能会想升级到 P3 类。
在 AWS 上创建并运行深度学习框非常容易:
-
登录到 AWS 控制台。
-
选择 EC2 并点击启动实例。
-
搜索深度学习 AMI(Ubuntu)选项并选择它。
-
选择
p2.xlarge作为你的实例类型。 -
启动实例,可以创建新的密钥对或重用现有的密钥对。
-
通过使用 SSH 连接并将本地机器上的端口 8888 重定向到实例来连接到实例:
ssh-Llocalhost:8888:localhost:8888\ -i*`your``.pem``filename`*ubuntu@*`your``instance``DNS`* -
通过输入
**jupyter notebook**来启动 Jupyter Notebook。复制生成的 URL 并粘贴到浏览器中以访问 Jupyter。
记得在不使用时关闭你的实例!你可以通过在 Web 界面中右键单击实例并选择关闭选项来实现这一点。这将关闭实例,并在实例不运行时不会向你收费。然而,即使实例关闭,你仍会被收取为其分配的存储空间费用,所以请注意。要完全删除实例和存储,请选择终止选项。
Azure
与 AWS 一样,Azure 提供了一些更便宜的基于 K80 的实例和更昂贵的 Tesla V100 实例。Azure 还提供基于较旧的 P100 硬件的实例,作为其他两种之间的中间点。同样,我建议本书使用单个 K80(NC6)的实例类型,这也每小时花费 90 美分,并根据需要转移到其他 NC、NCv2(P100)或 NCv3(V100)类型。
以下是如何在 Azure 中设置 VM:
-
登录到 Azure 门户,并在 Azure Marketplace 中找到 Data Science Virtual Machine 镜像。
-
点击立即获取按钮。
-
填写 VM 的详细信息(为其命名,选择 SSD 磁盘而不是 HDD,一个 SSH 用户名/密码,将实例计费到的订阅,以及将位置设置为最接近你的提供 NC 实例类型的地点)。
-
点击创建选项。实例应该在大约五分钟内被配置。
-
你可以使用指定给该实例的公共域名系统(DNS)名称的用户名/密码来使用 SSH。
-
当实例被配置时,Jupyter Notebook 应该运行;导航至http://
实例的 DNS 名称:8000并使用你用于 SSH 登录的用户名/密码组合登录。
谷歌云平台
除了像亚马逊和 Azure 一样提供 K80、P100 和 V100 支持的实例外,Google Cloud Platform(GCP)还为那些具有巨大数据和计算需求的人提供了上述的 TPUs。您不需要本书中的 TPUs,它们价格昂贵,但它们将与 PyTorch 1.0 一起使用,因此不要认为您必须使用 TensorFlow 才能利用它们,如果您有一个需要使用它们的项目。
开始使用 Google Cloud 也非常简单:
-
在 GCP Marketplace 上搜索 Deep Learning VM。
-
在 Compute Engine 上点击启动。
-
为实例命名并将其分配给您最近的区域。
-
将机器类型设置为 8 个 vCPU。
-
将 GPU 设置为 1 K80。
-
确保在框架部分中选择 PyTorch 1.0。
-
选择“第一次启动时自动安装 NVIDIA GPU?”复选框。
-
将启动磁盘设置为 SSD 持久磁盘。
-
单击部署选项。虚拟机将需要大约 5 分钟才能完全部署。
-
要连接到实例上的 Jupyter,请确保您已登录到
gcloud中的正确项目,并发出以下命令:gcloud compute ssh _INSTANCE_NAME_ -- -L 8080:localhost:8080
Google Cloud 的费用大约为每小时 70 美分,是三家主要云服务提供商中最便宜的。
应该使用哪个云服务提供商?
如果没有任何事情吸引您,我建议使用 Google Cloud Platform(GCP);这是最便宜的选择,如果需要,您可以扩展到使用 TPUs,比 AWS 或 Azure 提供的灵活性更大。但如果您已经在另外两个平台中的一个上拥有资源,那么在这些环境中运行将完全没问题。
一旦您的云实例运行起来,您将能够登录到其 Jupyter Notebook 的副本,所以下面让我们来看看。
使用 Jupyter Notebook
如果您以前没有接触过它,这里是关于 Jupyter Notebook 的简介:这个基于浏览器的环境允许您将实时代码与文本、图像和可视化混合在一起,已经成为全球数据科学家的事实标准工具之一。在 Jupyter 中创建的笔记本可以轻松共享;实际上,您会在本书中的所有笔记本中找到。您可以在图 1-2 中看到 Jupyter Notebook 的截图。
在本书中,我们不会使用 Jupyter 的任何高级功能;您只需要知道如何创建一个新的笔记本,以及 Shift-Enter 如何运行单元格的内容。但如果您以前从未使用过它,我建议在进入第二章之前浏览Jupyter 文档。
图 1-2. Jupyter Notebook
在我们开始使用 PyTorch 之前,我们将讨论最后一件事:如何手动安装所有内容。
从头开始安装 PyTorch
也许您想对软件有更多控制,而不是使用之前提供的云镜像之一。或者您需要特定版本的 PyTorch 来运行您的代码。或者,尽管我发出了所有警告,您真的想要在地下室中安装那台设备。让我们看看如何在 Linux 服务器上通用安装 PyTorch。
警告
您可以使用 Python 2.x与 PyTorch 一起使用,但我强烈建议不要这样做。尽管 Python 2.x到 3.x的升级已经进行了十多年,但越来越多的软件包开始放弃对 Python 2.x的支持。因此,除非有充分理由,确保您的系统正在运行 Python 3。
下载 CUDA
尽管 PyTorch 可以完全在 CPU 模式下运行,但在大多数情况下,需要 GPU 支持的 PyTorch 才能实现实际用途,因此我们需要 GPU 支持。这相当简单;假设您有一张 NVIDIA 卡,这是由他们的 Compute Unified Device Architecture(CUDA)API 提供的。下载适合您 Linux 版本的适当软件包格式并安装软件包。
对于 Red Hat Enterprise Linux(RHEL)7:
sudo rpm -i cuda-repo-rhel7-10-0local-10.0.130-410.48-1.0-1.x86_64.rpm
sudo yum clean all
sudo yum install cuda
对于 Ubuntu 18.04:
sudo dpkg -i cuda-repo-ubuntu1804-10-0-local-10.0.130-410.48_1.0-1_amd64.deb
sudo apt-key add /var/cuda-repo-<version>/7fa2af80.pub
sudo apt-get update
sudo apt-get install cuda
Anaconda
Python 有各种打包系统,所有这些系统都有好坏之分。与 PyTorch 的开发人员一样,我建议您安装 Anaconda,这是一个专门为数据科学家提供最佳软件包分发的打包系统。与 CUDA 一样,它相当容易安装。
前往Anaconda并选择适合您机器的安装文件。因为这是一个通过 shell 脚本在您的系统上执行的大型存档,我建议您在下载的文件上运行md5sum并将其与签名列表进行比对,然后再使用bash Anaconda3-VERSION-Linux-x86_64.sh执行以确保您机器上的签名与网页上的签名匹配。这可以确保下载的文件没有被篡改,并且可以安全地在您的系统上运行。脚本将提供有关将要安装的位置的几个提示;除非有充分的理由,否则请接受默认设置。
注意
您可能会想:“我能在我的 MacBook 上做这个吗?”遗憾的是,如今大多数 Mac 都配备有 Intel 或 AMD GPU,实际上不支持在 GPU 加速模式下运行 PyTorch。我建议您使用 Colab 或云服务提供商,而不是尝试在本地使用 Mac。
最后,PyTorch!(和 Jupyter Notebook)
现在您已经安装了 Anaconda,使用 PyTorch 很简单:
conda install pytorch torchvision -c pytorch
这将安装 PyTorch 和我们在接下来的几章中使用的torchvision库,用于创建与图像一起工作的深度学习架构。Anaconda 还为我们安装了 Jupyter Notebook,因此我们可以通过启动它来开始:
jupyter notebook
在浏览器中前往http://YOUR-IP-ADDRESS:8888,创建一个新的笔记本,并输入以下内容:
import torch
print(torch.cuda.is_available())
print(torch.rand(2,2))
这应该产生类似于这样的输出:
True
0.6040 0.6647
0.9286 0.4210
[torch.FloatTensor of size 2x2]
如果cuda.is_available()返回False,则需要调试您的 CUDA 安装,以便 PyTorch 可以看到您的显卡。在您的实例上,张量的值将不同。
但这个张量是什么?张量几乎是 PyTorch 中的一切,因此您需要知道它们是什么以及它们可以为您做什么。
张量
张量既是数字的容器,也是定义在产生新张量之间的张量之间的转换规则的集合。对于我们来说,将张量视为多维数组可能是最容易的。每个张量都有一个与其维度空间对应的秩。一个简单的标量(例如,1)可以表示为秩为 0 的张量,一个向量是秩为 1 的,一个n×n矩阵是秩为 2 的,依此类推。在前面的示例中,我们使用torch.rand()创建了一个具有随机值的秩为 2 的张量。我们也可以从列表中创建它们:
x = torch.tensor([[0,0,1],[1,1,1],[0,0,0]])
x
>tensor([[0, 0, 1],
[1, 1, 1],
[0, 0, 0]])
我们可以通过使用标准的 Python 索引在张量中更改元素:
x[0][0] = 5
>tensor([[5, 0, 1],
[1, 1, 1],
[0, 0, 0]])
您可以使用特殊的创建函数生成特定类型的张量。特别是,ones()和zeroes()将分别生成填充有 1 和 0 的张量:
torch.zeros(2,2)
> tensor([[0., 0.],
[0., 0.]])
您可以使用张量执行标准的数学运算(例如,将两个张量相加):
tensor.ones(1,2) + tensor.ones(1,2)
> tensor([[2., 2.]])
如果您有一个秩为 0 的张量,可以使用item()提取值:
torch.rand(1).item()
> 0.34106671810150146
张量可以存在于 CPU 或 GPU 上,并且可以通过使用to()函数在设备之间进行复制:
cpu_tensor = tensor.rand(2)
cpu_tensor.device
> device(type='cpu')
gpu_tensor = cpu_tensor.to("cuda")
gpu_tensor.device
> device(type='cuda', index=0)
张量操作
如果您查看PyTorch 文档,您会发现有很多函数可以应用于张量——从查找最大元素到应用傅立叶变换等。在本书中,您不需要了解所有这些函数来将图像、文本和音频转换为张量并对其进行操作,但您需要了解一些。我强烈建议您在完成本书后浏览文档。现在我们将逐一介绍将在接下来的章节中使用的所有函数。
首先,我们经常需要找到张量中的最大项以及包含最大值的索引(因为这通常对应于神经网络在最终预测中决定的类)。这可以通过max()和argmax()函数来实现。我们还可以使用item()从 1D 张量中提取标准的 Python 值。
torch.rand(2,2).max()
> tensor(0.4726)
torch.rand(2,2).max().item()
> 0.8649941086769104
有时,我们可能想要改变张量的类型;例如,从LongTensor到FloatTensor。我们可以使用to()来实现:
long_tensor = torch.tensor([[0,0,1],[1,1,1],[0,0,0]])
long_tensor.type()
> 'torch.LongTensor'
float_tensor = torch.tensor([[0,0,1],[1,1,1],[0,0,0]]).to(dtype=torch.float32)
float_tensor.type()
> 'torch.FloatTensor'
大多数在张量上操作并返回张量的函数会创建一个新的张量来存储结果。然而,如果你想节省内存,可以查看是否定义了一个原地函数,它的名称应该与原始函数相同,但在末尾加上下划线(_)。
random_tensor = torch.rand(2,2)
random_tensor.log2()
>tensor([[-1.9001, -1.5013],
[-1.8836, -0.5320]])
random_tensor.log2_()
> tensor([[-1.9001, -1.5013],
[-1.8836, -0.5320]])
另一个常见的操作是重塑张量。这通常是因为你的神经网络层可能需要一个与你当前要输入的形状略有不同的输入形状。例如,手写数字的 Modified National Institute of Standards and Technology (MNIST)数据集是一组 28×28 的图像,但它的打包方式是长度为 784 的数组。为了使用我们正在构建的网络,我们需要将它们转换回 1×28×28 的张量(前导的 1 是通道数——通常是红、绿和蓝,但由于 MNIST 数字只是灰度的,我们只有一个通道)。我们可以使用view()或reshape()来实现:
flat_tensor = torch.rand(784)
viewed_tensor = flat_tensor.view(1,28,28)
viewed_tensor.shape
> torch.Size([1, 28, 28])
reshaped_tensor = flat_tensor.reshape(1,28,28)
reshaped_tensor.shape
> torch.Size([1, 28, 28])
请注意,重塑后的张量形状必须与原始张量的总元素数相同。如果你尝试flat_tensor.reshape(3,28,28),你会看到这样的错误:
RuntimeError Traceback (most recent call last)
<ipython-input-26-774c70ba5c08> in <module>()
----> 1 flat_tensor.reshape(3,28,28)
RuntimeError: shape '[3, 28, 28]' is invalid for input of size 784
现在你可能想知道view()和reshape()之间的区别是什么。答案是view()作为原始张量的视图操作,所以如果底层数据发生变化,视图也会发生变化(反之亦然)。然而,如果所需的视图不是连续的,view()可能会抛出错误;也就是说,如果它不与从头开始创建的具有所需形状的新张量共享相同的内存块。如果发生这种情况,你必须在使用view()之前调用tensor.contiguous()。然而,reshape()在幕后完成所有这些工作,所以一般来说,我建议使用reshape()而不是view()。
最后,你可能需要重新排列张量的维度。你可能会在处理图像时遇到这种情况,图像通常以[height, width, channel]的张量形式存储,但 PyTorch 更喜欢以[channel, height, width]的形式处理。你可以使用permute()来以一种相当简单的方式处理这些:
hwc_tensor = torch.rand(640, 480, 3)
chw_tensor = hwc_tensor.permute(2,0,1)
chw_tensor.shape
> torch.Size([3, 640, 480])
在这里,我们刚刚对一个[640,480,3]的张量应用了permute,参数是张量维度的索引,所以我们希望最终维度(由于从零开始索引,是 2)在张量的前面,后面是剩下的两个维度按照原始顺序。
张量广播
从 NumPy 借鉴的广播允许你在张量和较小张量之间执行操作。如果从它们的尾部维度开始向后看,你可以在两个张量之间进行广播:
-
两个维度相等。
-
一个维度是 1。
在我们使用广播时,它有效是因为 1 有一个维度是 1,而且没有其他维度,1 可以扩展到另一个张量。如果我们尝试将一个[2,2]张量加到一个[3,3]张量上,我们会得到这样的错误消息:
The size of tensor a (2) must match the size of
tensor b (3) at non-singleton dimension 1
但是我们可以毫无问题地将一个[1,3]张量加到一个[3,3]张量上。广播是一个方便的小功能,可以增加代码的简洁性,并且通常比手动扩展张量更快。
关于张量的一切你需要开始的内容就到这里了!我们将在书中后面遇到其他一些操作,但这已经足够让你深入第二章了。
结论
无论是在云端还是在本地机器上,您现在应该已经安装了 PyTorch。我已经介绍了该库的基本构建模块,张量,您已经简要了解了 Jupyter Notebook。这就是您开始的全部所需!在下一章中,您将利用到目前为止所见的一切来开始构建神经网络和对图像进行分类,所以在继续之前,请确保您对张量和 Jupyter 感到舒适。
进一步阅读
第二章:使用 PyTorch 进行图像分类
在设置 PyTorch 之后,深度学习教材通常会在做任何有趣的事情之前向你抛出一堆行话。我尽量将其减少到最低限度,并通过一个例子来解释,尽管这个例子可以在你更熟悉使用 PyTorch 的过程中轻松扩展。我们在整本书中使用这个例子来演示如何调试模型(第七章)或将其部署到生产环境(第八章)。
从现在开始直到第四章结束,我们将构建一个图像分类器。神经网络通常用作图像分类器;网络被给予一张图片,并被问到对我们来说是一个简单的问题:“这是什么?”
让我们开始构建我们的 PyTorch 应用程序。
我们的分类问题
在这里,我们构建一个简单的分类器,可以区分鱼和猫之间的区别。我们将不断迭代设计和构建模型的过程,使其变得更加准确。
图 2-1 和 2-2 展示了一条鱼和一只猫的全貌。我不确定这条鱼是否有名字,但这只猫叫 Helvetica。
让我们从讨论传统分类中涉及的挑战开始。
图 2-1. 一条鱼!
图 2-2. 盒子里的 Helvetica
传统挑战
你会如何编写一个程序来区分鱼和猫?也许你会编写一组规则,描述猫有尾巴,或者鱼有鳞片,并将这些规则应用于图像以确定你看到的是什么。但这需要时间、精力和技能。另外,如果你遇到像曼克斯猫这样的东西会发生什么;虽然它显然是一只猫,但它没有尾巴。
你可以看到这些规则只会变得越来越复杂,以描述所有可能的情况。此外,我承认我在图形编程方面非常糟糕,所以不得不手动编写所有这些规则的想法让我感到恐惧。
我们追求的是一个函数,给定一张图片的输入,返回猫或鱼。对于我们来说,通过详细列出所有标准来构建这个函数是困难的。但深度学习基本上让计算机做所有那些我们刚刚谈到的规则的艰苦工作——只要我们创建一个结构,给网络大量数据,并让它找出是否得到了正确答案的方法。这就是我们要做的。在这个过程中,你将学习如何使用 PyTorch 的一些关键概念。
但首先,数据
首先,我们需要数据。需要多少数据?这取决于情况。对于任何深度学习技术都需要大量数据来训练神经网络的想法并不一定正确,正如你将在第四章中看到的那样。然而,现在我们将从头开始训练,这通常需要大量数据。我们需要很多鱼和猫的图片。
现在,我们可以花一些时间从 Google 图像搜索等地方下载许多图片,但在这种情况下,我们有一个捷径:一个用于训练神经网络的标准图像集合,称为ImageNet。它包含超过 1400 万张图片和 20000 个图像类别。这是所有图像分类器用来评判自己的标准。所以我从那里获取图片,但如果你愿意,可以自行下载其他图片。
除了数据,PyTorch 还需要一种确定什么是猫和什么是鱼的方法。这对我们来说很容易,但对计算机来说有点困难(这也是我们首次构建程序的原因!)。我们使用附加到数据的标签,以这种方式进行训练称为监督学习。(当您无法访问任何标签时,您必须使用无监督学习方法进行训练,这可能并不令人惊讶。)
现在,如果我们使用 ImageNet 数据,它的标签对我们来说并不是那么有用,因为它们包含了对我们来说太多的信息。tabby cat或trout这样的标签,在计算机看来,与cat或fish是分开的。我们需要重新标记这些。因为 ImageNet 是如此庞大的图像集合,我已经整理了一份图像 URL 和标签的列表供鱼类和猫类使用。
您可以在该目录中运行download.py脚本,它将从 URL 下载图像并将其放置在适当的位置进行训练。重新标记很简单;脚本将猫的图片存储在train/cat目录中,将鱼的图片存储在train/fish目录中。如果您不想使用下载脚本,只需创建这些目录并将适当的图片放在正确的位置。现在我们有了数据,但我们需要将其转换为 PyTorch 可以理解的格式。
PyTorch 和数据加载器
加载和转换数据为训练准备的格式通常会成为数据科学中吸收我们太多时间的领域之一。PyTorch 已经发展了与数据交互的标准约定,使得与之一起工作变得相当一致,无论您是在处理图像、文本还是音频。
与数据交互的两个主要约定是数据集和数据加载器。数据集是一个 Python 类,允许我们访问我们提供给神经网络的数据。数据加载器是将数据从数据集传送到网络的工具。(这可能包括信息,例如,有多少个工作进程正在将数据传送到网络中?或我们一次传入多少张图片?)
让我们先看看数据集。无论数据集包含图像、音频、文本、3D 景观、股市信息还是其他任何内容,只要满足这个抽象的 Python 类,就可以与 PyTorch 进行交互:
class Dataset(object):
def __getitem__(self, index):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
这是相当直接的:我们必须实现一个返回数据集大小的方法(len),并实现一个可以检索数据集中项目的方法,返回一个(*label*,*tensor*)对。这是由数据加载器调用的,因为它正在将数据推送到神经网络进行训练。因此,我们必须编写一个getitem的主体,它可以获取图像并将其转换为张量,然后返回该张量和标签,以便 PyTorch 可以对其进行操作。这很好,但你可以想象到这种情况经常发生,所以也许 PyTorch 可以让事情变得更容易?
构建训练数据集
torchvision包含一个名为ImageFolder的类,几乎为我们做了一切,只要我们的图像结构中每个目录都是一个标签(例如,所有猫都在一个名为cat的目录中)。对于我们的猫和鱼的示例,这是您需要的:
import torchvision
from torchvision import transforms
train_data_path = "./train/"
transforms = transforms.Compose([
transforms.Resize(64),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225] )
])
train_data = torchvision.datasets.ImageFolder
(root=train_data_path,transform=transforms)
这里发生了更多的事情,因为torchvision还允许您指定一系列将应用于图像的转换,然后将其馈送到神经网络之前。默认转换是将图像数据转换为张量(在前面的代码中看到的transforms.ToTensor()方法),但我们还在做一些其他可能不太明显的事情。
首先,GPU 被设计为快速执行标准大小的计算。但我们可能有许多分辨率的图像。为了提高我们的处理性能,我们通过Resize(64)转换将每个传入的图像缩放到相同的分辨率 64×64。然后我们将图像转换为张量,最后我们将张量归一化到一组特定的均值和标准差点周围。
归一化很重要,因为当输入通过神经网络的层时会发生大量的乘法运算;保持输入值在 0 和 1 之间可以防止值在训练阶段变得过大(称为梯度爆炸问题)。这种神奇的化身只是 ImageNet 数据集作为整体的均值和标准差。你可以专门为这个猫和鱼子集计算它,但这些值已经足够好了。(如果你在完全不同的数据集上工作,你将不得不计算那个均值和偏差,尽管许多人只是使用这些 ImageNet 常数并报告可接受的结果。)
可组合的转换还允许我们轻松地进行图像旋转和扭曲以进行数据增强,我们将在第四章中回到这个话题。
注意
在这个例子中,我们将图像调整为 64×64。我做出了这个任意选择,以便使我们即将到来的第一个网络的计算变得快速。大多数现有的架构在第三章中使用 224×224 或 299×299 作为图像输入。一般来说,输入尺寸越大,网络学习的数据就越多。另一方面,你通常可以将更小的图像批次适应到 GPU 的内存中。
我们对数据集还没有完成。但是为什么我们需要不止一个训练数据集呢?
构建验证和测试数据集
我们的训练数据已经设置好了,但我们需要为我们的验证数据重复相同的步骤。这里有什么区别?深度学习(实际上所有机器学习)的一个危险是过拟合的概念:你的模型在训练过的内容上表现得非常好,但无法推广到它没有见过的例子。所以它看到一张猫的图片,除非所有其他猫的图片都与那张图片非常相似,否则模型不认为它是一只猫,尽管它显然是一只猫。为了防止我们的网络这样做,我们在download.py中下载了一个验证集,其中包含一系列不在训练集中出现的猫和鱼的图片。在每个训练周期(也称为epoch)结束时,我们会与这个集合进行比较,以确保我们的网络没有出错。但不用担心,这段代码非常简单,因为它只是稍微更改了一些变量名的早期代码:
val_data_path = "./val/"
val_data = torchvision.datasets.ImageFolder(root=val_data_path,
transform=transforms)
我们只是重新使用了transforms链,而不必再次定义它。
除了验证集,我们还应该创建一个测试集。这用于在所有训练完成后测试模型:
test_data_path = "./test/"
test_data = torchvision.datasets.ImageFolder(root=test_data_path,
transform=transforms)
区分数据集类型可能有点困惑,所以我编制了一张表来指示哪个数据集用于模型训练的哪个部分;请参见表 2-1。
表 2-1. 数据集类型
| 训练集 | 用于训练过程中更新模型的数据集 |
|---|---|
| 验证集 | 用于评估模型在问题领域中的泛化能力,而不是适应训练数据;不直接用于更新模型 |
| 测试集 | 在训练完成后提供最终评估模型性能的最终数据集 |
然后我们可以用几行 Python 代码构建我们的数据加载器:
batch_size=64
train_data_loader = data.DataLoader(train_data, batch_size=batch_size)
val_data_loader = data.DataLoader(val_data, batch_size=batch_size)
test_data_loader = data.DataLoader(test_data, batch_size=batch_size)
从这段代码中需要注意的新内容是batch_size。这告诉我们在训练和更新之前有多少图像会通过网络。理论上,我们可以将batch_size设置为测试集和训练集中图像的数量,以便网络在更新之前看到每个图像。实际上,我们通常不这样做,因为较小的批次(在文献中更常被称为小批量)需要比存储数据集中每个图像的所有信息更少的内存,并且较小的批次大小会使训练更快,因为我们更快地更新我们的网络。
默认情况下,PyTorch 的数据加载器设置为batch_size为 1。你几乎肯定会想要更改这个值。虽然我在这里选择了 64,但你可能想要尝试一下,看看你可以使用多大的小批量而不会耗尽 GPU 的内存。你可能还想尝试一些额外的参数:你可以指定数据集如何被采样,是否在每次运行时对整个集合进行洗牌,以及使用多少个工作进程来从数据集中提取数据。所有这些都可以在PyTorch 文档中找到。
这涵盖了将数据导入 PyTorch,所以现在让我们介绍一个简单的神经网络来开始对我们的图像进行分类。
最后,一个神经网络!
我们将从最简单的深度学习网络开始:一个输入层,用于处理输入张量(我们的图像);一个输出层,其大小将是输出类别数量(2)的大小;以及它们之间的一个隐藏层。在我们的第一个示例中,我们将使用全连接层。图 2-3 展示了一个具有三个节点的输入层,三个节点的隐藏层和两个节点输出的样子。
图 2-3. 一个简单的神经网络
正如你所看到的,在这个全连接的例子中,每一层中的每个节点都会影响到下一层中的每个节点,并且每个连接都有一个权重,它决定了从该节点传入下一层的信号的强度。当我们训练网络时,这些权重通常会从随机初始化中更新。当一个输入通过网络时,我们(或 PyTorch)可以简单地将该层的权重和偏置进行矩阵乘法,然后将结果传递到下一个函数中,该结果会经过一个激活函数,这只是一种在我们的系统中插入非线性的方法。
激活函数
激活函数听起来很复杂,但你在文献中最常见的激活函数是ReLU,或者修正线性单元。这再次听起来很复杂!但事实证明,它只是实现max(0,x)的函数,所以如果输入是负数,则结果为 0,如果x是正数,则结果就是输入(x)。简单!
你可能会遇到的另一个激活函数是softmax,在数学上稍微复杂一些。基本上它会产生一组介于 0 和 1 之间的值,加起来等于 1(概率!),并且加权这些值以夸大差异——也就是说,它会在向量中产生一个比其他所有值都高的结果。你经常会看到它被用在分类网络的末尾,以确保网络对输入属于哪个类别做出明确的预测。
有了所有这些构建块,我们可以开始构建我们的第一个神经网络。
创建一个网络
在 PyTorch 中创建一个网络是一个非常 Pythonic 的事情。我们从一个名为torch.nn.Network的类继承,并填写__init__和forward方法:
class SimpleNet(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(12288, 84)
self.fc2 = nn.Linear(84, 50)
self.fc3 = nn.Linear(50,2)
def forward(self):
x = x.view(-1, 12288)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = F.softmax(self.fc3(x))
return x
simplenet = SimpleNet()
同样,这并不太复杂。我们在init()中进行任何所需的设置,这种情况下调用我们的超类构造函数和三个全连接层(在 PyTorch 中称为Linear,而不是 Keras 中的Dense)。forward()方法描述了数据如何在网络中流动,无论是在训练还是进行预测(推理)。首先,我们必须将图像中的 3D 张量(x和y加上三通道的颜色信息—红色、绿色、蓝色)转换为 1D 张量,以便将其馈送到第一个Linear层中,我们使用view()来实现这一点。从那里,您可以看到我们按顺序应用层和激活函数,最后返回softmax输出以给出我们对该图像的预测。
隐藏层中的数字有些是任意的,除了最终层的输出是 2,与我们的两类猫或鱼相匹配。一般来说,您希望在层中的数据在向下堆栈时压缩。如果一个层要将 50 个输入传递到 100 个输出,那么网络可能会通过简单地将 50 个连接传递给 100 个输出中的 50 个来学习,并认为其工作完成。通过减小输出相对于输入的大小,我们迫使网络的这部分学习使用更少的资源来学习原始输入的表示,这希望意味着它提取了一些对我们要解决的问题重要的图像特征;例如,学习识别鳍或尾巴。
我们有一个预测,我们可以将其与原始图像的实际标签进行比较,以查看预测是否正确。但是我们需要一种让 PyTorch 能够量化预测是正确还是错误,以及有多错误或正确的方法。这由损失函数处理。
损失函数
损失函数是有效深度学习解决方案的关键组成部分之一。PyTorch 使用损失函数来确定如何更新网络以达到期望的结果。
损失函数可以是您想要的复杂或简单。PyTorch 配备了一个全面的损失函数集合,涵盖了您可能会遇到的大多数应用程序,当然,如果您有一个非常自定义的领域,您也可以编写自己的损失函数。在我们的情况下,我们将使用一个名为CrossEntropyLoss的内置损失函数,这是推荐用于多类别分类任务的,就像我们在这里所做的那样。您可能会遇到的另一个损失函数是MSELoss,这是一个标准的均方损失,您可能在进行数值预测时使用。
要注意的一件事是,CrossEntropyLoss还将softmax()作为其操作的一部分,因此我们的forward()方法变为以下内容:
def forward(self):
# Convert to 1D vector
x = x.view(-1, 12288)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
现在让我们看看在训练循环期间神经网络的层如何更新。
优化
训练网络涉及通过网络传递数据,使用损失函数确定预测和实际标签之间的差异,然后使用该信息来更新网络的权重,以尽可能使损失函数返回尽可能小的损失。为了对神经网络进行更新,我们使用一个优化器。
如果我们只有一个权重,我们可以绘制损失值与权重值的图表,它可能看起来像图 2-4。
图 2-4。损失的二维图
如果我们从一个随机位置开始,用 X 标记,将我们的权重值放在 x 轴上,损失函数放在 y 轴上,我们需要到曲线的最低点找到我们的最佳解决方案。我们可以通过改变权重的值来移动,这将给我们一个新的损失函数值。要知道我们正在做出的移动有多好,我们可以根据曲线的梯度进行检查。可视化优化器的一种常见方法是像滚动大理石一样,试图找到一系列山谷中的最低点(或最小值)。如果我们将视图扩展到两个参数,创建一个如图 2-5 所示的 3D 图,这可能更清晰。
图 2-5。损失的 3D 图
在这种情况下,我们可以在每个点检查所有潜在移动的梯度,并选择使我们在山下移动最多的那个。
但是,您需要注意一些问题。首先是陷入局部最小值的危险,这些区域看起来像是损失曲线最浅的部分,如果我们检查梯度,但实际上在其他地方存在更浅的区域。如果我们回到图 2-4 中的 1D 曲线,我们可以看到如果通过短跳下陷入左侧的最小值,我们永远不会有离开该位置的理由。如果我们采取巨大的跳跃,我们可能会发现自己进入通往实际最低点的路径,但由于我们一直跳得太大,我们一直在到处弹跳。
我们的跳跃大小被称为学习率,通常是需要调整的关键参数,以便使您的网络学习正确和高效。您将在第四章中看到确定良好学习率的方法,但现在,您将尝试不同的值:尝试从 0.001 开始。正如刚才提到的,较大的学习率会导致网络在训练过程中到处反弹,并且不会收敛到一组良好的权重上。
至于局部最小值问题,我们对获取所有可能梯度进行了轻微修改,并在批处理期间指示样本随机梯度。称为随机梯度下降(SGD),这是优化神经网络和其他机器学习技术的传统方法。但是还有其他优化器可用,事实上对于深度学习来说更可取。PyTorch 提供了 SGD 和其他优化器,如 AdaGrad 和 RMSProp,以及 Adam,我们将在本书的大部分内容中使用的优化器。
Adam 的一个关键改进(RMSProp 和 AdaGrad 也是如此)是它为每个参数使用一个学习率,并根据这些参数的变化速率调整该学习率。它保持梯度和这些梯度的平方的指数衰减列表,并使用这些来缩放 Adam 正在使用的全局学习率。经验表明,Adam 在深度学习网络中优于大多数其他优化器,但您可以将 Adam 替换为 SGD 或 RMSProp 或另一个优化器,以查看是否使用不同的技术能够为您的特定应用程序提供更快更好的训练。
创建基于 Adam 的优化器很简单。我们调用optim.Adam()并传入网络的权重(通过simplenet.parameters()获得)和我们示例的学习率 0.001:
import torch.optim as optim
optimizer = optim.Adam(simplenet.parameters(), lr=0.001)
优化器是拼图的最后一块,所以我们终于可以开始训练我们的网络了。
训练
这是我们完整的训练循环,将迄今为止看到的所有内容结合起来训练网络。我们将其编写为一个函数,以便可以将诸如损失函数和优化器之类的部分作为参数传递。目前看起来相当通用:
for epoch in range(epochs):
for batch in train_loader:
optimizer.zero_grad()
input, target = batch
output = model(input)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
这是相当简单的,但你应该注意几点。我们在循环的每次迭代中从训练集中取一个批次,这由我们的数据加载器处理。然后我们通过模型运行这些数据,并计算出期望输出的损失。为了计算梯度,我们在模型上调用backward()方法。optimizer.step()方法随后使用这些梯度来执行我们在前一节中讨论过的权重调整。
然而,zero_grad()调用是在做什么呢?事实证明,默认情况下计算的梯度会累积,这意味着如果我们在批次迭代结束时不将梯度清零,下一个批次将不得不处理这个批次的梯度以及自己的梯度,接下来的批次将不得不处理前两个批次的梯度,依此类推。这并不有用,因为我们希望在每次迭代中只查看当前批次的梯度进行优化。我们使用zero_grad()确保在我们完成循环后将它们重置为零。
这是训练循环的抽象版本,但在写完我们的完整函数之前,我们还需要解决一些问题。
使其在 GPU 上运行
到目前为止,如果你运行了任何代码,你可能已经注意到它并不那么快。那么那块闪亮的 GPU 呢,它就坐在我们云端实例上(或者我们在桌面上组装的非常昂贵的机器上)?PyTorch 默认使用 CPU 进行计算。为了利用 GPU,我们需要通过显式地使用to()方法将输入张量和模型本身移动到 GPU 上。这里有一个将SimpleNet复制到 GPU 的示例:
if torch.cuda.is_available():
device = torch.device("cuda")
else
device = torch.device("cpu")
model.to(device)
在这里,如果 PyTorch 报告有 GPU 可用,我们将模型复制到 GPU 上,否则保持模型在 CPU 上。通过使用这种构造,我们可以确定 GPU 是否在我们的代码开始时可用,并在程序的其余部分中使用tensor|model.to(device),确信它会到达正确的位置。
注意
在早期版本的 PyTorch 中,你会使用cuda()方法将数据复制到 GPU 上。如果在查看其他人的代码时遇到这个方法,只需注意它与to()做的是相同的事情!
这就是训练所需的所有步骤。我们快要完成了!
将所有内容整合在一起
在本章中,你已经看到了许多不同的代码片段,让我们整合它们。我们将它们放在一起,创建一个通用的训练方法,接受一个模型,以及训练和验证数据,还有学习率和批次大小选项,并对该模型进行训练。我们将在本书的其余部分中使用这段代码:
def train(model, optimizer, loss_fn, train_loader, val_loader,
epochs=20, device="cpu"):
for epoch in range(epochs):
training_loss = 0.0
valid_loss = 0.0
model.train()
for batch in train_loader:
optimizer.zero_grad()
inputs, target = batch
inputs = inputs.to(device)
target = targets.to(device)
output = model(inputs)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
training_loss += loss.data.item()
training_loss /= len(train_iterator)
model.eval()
num_correct = 0
num_examples = 0
for batch in val_loader:
inputs, targets = batch
inputs = inputs.to(device)
output = model(inputs)
targets = targets.to(device)
loss = loss_fn(output,targets)
valid_loss += loss.data.item()
correct = torch.eq(torch.max(F.softmax(output), dim=1)[1],
target).view(-1)
num_correct += torch.sum(correct).item()
num_examples += correct.shape[0]
valid_loss /= len(valid_iterator)
print('Epoch: {}, Training Loss: {:.2f},
Validation Loss: {:.2f},
accuracy = {:.2f}'.format(epoch, training_loss,
valid_loss, num_correct / num_examples))
这是我们的训练函数,我们可以通过传入所需的参数来启动训练:
train(simplenet, optimizer, torch.nn.CrossEntropyLoss(),
train_data_loader, test_data_loader,device)
网络将训练 20 个 epochs(你可以通过向train()传入一个值来调整这个值),并且你应该在每个 epoch 结束时得到模型在验证集上的准确性打印输出。
你已经训练了你的第一个神经网络——恭喜!现在你可以用它进行预测,让我们看看如何做到这一点。
进行预测
在本章的开头,我说过我们将制作一个神经网络,可以对图像进行分类,判断是猫还是鱼。我们现在已经训练了一个可以做到这一点的网络,但是我们如何使用它来为单个图像生成预测呢?这里有一段快速的 Python 代码,它将从文件系统加载一张图像,并打印出我们的网络是说“猫”还是“鱼”:
from PIL import Image
labels = ['cat','fish']
img = Image.open(FILENAME)
img = transforms(img)
img = img.unsqueeze(0)
prediction = simplenet(img)
prediction = prediction.argmax()
print(labels[prediction])
大部分代码都很简单;我们重用了之前制作的转换流水线,将图像转换为神经网络所需的正确形式。然而,因为我们的网络使用批次,实际上它期望一个 4D 张量,第一个维度表示批次中的不同图像。我们没有批次,但我们可以通过使用unsqueeze(0)创建一个长度为 1 的批次,这会在我们的张量前面添加一个新的维度。
获取预测就像将我们的batch传递到模型中一样简单。然后我们必须找出具有更高概率的类别。在这种情况下,我们可以简单地将张量转换为数组并比较两个元素,但通常情况下不止这两个元素。幸运的是,PyTorch 提供了argmax()函数,它返回张量中最高值的索引。然后我们使用该索引来索引我们的标签数组并打印出我们的预测。作为练习,使用前面的代码作为基础,在本章开头创建的测试集上进行预测。您不需要使用unsqueeze(),因为您从test_data_loader中获取批次。
这就是您现在需要了解的有关进行预测的全部内容;在第八章中,我们将为生产使用加固事项时再次回顾这一点。
除了进行预测,我们可能希望能够在将来的任何时间点重新加载模型,使用我们训练好的参数,因此让我们看看如何在 PyTorch 中完成这个任务。
模型保存
如果您对模型的性能感到满意或因任何原因需要停止,您可以使用torch.save()方法将模型的当前状态保存为 Python 的pickle格式。相反,您可以使用torch.load()方法加载先前保存的模型迭代。
因此,保存我们当前的参数和模型结构将像这样工作:
torch.save(simplenet, "/tmp/simplenet")
我们可以按以下方式重新加载:
simplenet = torch.load("/tmp/simplenet")
这将模型的参数和结构都存储到文件中。如果以后更改模型的结构,这可能会成为一个问题。因此,更常见的做法是保存模型的state_dict。这是一个标准的 Pythondict,其中包含模型中每个层的参数映射。保存state_dict看起来像这样:
torch.save(model.state_dict(), PATH)
要恢复,首先创建模型的一个实例,然后使用load_state_dict。对于SimpleNet:
simplenet = SimpleNet()
simplenet_state_dict = torch.load("/tmp/simplenet")
simplenet.load_state_dict(simplenet_state_dict)
这里的好处是,如果以某种方式扩展了模型,可以向load_state_dict提供一个strict=False参数,该参数将参数分配给模型中存在的层,但如果加载的state_dict中的层缺失或添加到模型的当前结构中,则不会失败。因为它只是一个普通的 Pythondict,您可以更改键名称以适应您的模型,如果您从完全不同的模型中提取参数,这可能会很方便。
在训练运行期间可以将模型保存到磁盘,并在另一个时间点重新加载,以便可以在离开的地方继续训练。当使用像 Google Colab 这样的工具时,这非常有用,它让您在大约 12 小时内持续访问 GPU。通过跟踪时间,您可以在截止日期之前保存模型,并在新的 12 小时会话中继续训练。
结论
您已经快速浏览了神经网络的基础知识,并学会了如何使用 PyTorch 对其进行训练,对其他图像进行预测,并将模型保存/恢复到磁盘。
在阅读下一章之前,尝试一下我们在这里创建的SimpleNet架构。调整Linear层中的参数数量,也许添加一两个额外的层。查看 PyTorch 中提供的各种激活函数,并将ReLU替换为其他函数。看看如果调整学习率或将优化器从 Adam 切换到其他选项(也许尝试普通的 SGD),训练会发生什么变化。也许改变批量大小和图像在前向传递开始时被转换为 1D 张量的初始大小。许多深度学习工作仍处于手工调整阶段;学习率是手动调整的,直到网络被适当训练,因此了解所有移动部件如何相互作用是很重要的。
你可能对SimpleNet架构的准确性有些失望,但不用担心!第三章将引入卷积神经网络,带来明显的改进,取代我们目前使用的非常简单的网络。
进一步阅读
-
《Adam:一种随机优化方法》(2014)作者 Diederik P. Kingma 和 Jimmy Ba
-
《梯度下降优化算法概述》(2016)作者 Sebstian Ruder
第三章:卷积神经网络
在第二章中尝试使用全连接神经网络后,您可能注意到了一些问题。如果您尝试添加更多层或大幅增加参数数量,您几乎肯定会在 GPU 上耗尽内存。此外,训练时间很长,准确率也不尽如人意,尤其考虑到深度学习的炒作。到底发生了什么呢?
确实,全连接或(前馈)网络可以作为通用逼近器,但理论并没有说明训练它成为您真正想要的函数逼近器需要多长时间。但我们可以做得更好,尤其是对于图像。在本章中,您将了解卷积神经网络(CNNs)以及它们如何构成当今最准确的图像分类器的基础(我们会详细看一些)。我们为我们的鱼与猫应用程序构建了一个基于卷积的新架构,并展示它比我们在上一章中所做的更快速和更准确。让我们开始吧!
我们的第一个卷积模型
这一次,我将首先分享最终的模型架构,然后讨论所有新的部分。正如我在第二章中提到的,我们创建的训练方法与模型无关,因此您可以先测试这个模型,然后再回来了解解释!
class CNNNet(nn.Module):
def __init__(self, num_classes=2):
super(CNNNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 192, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(),
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(),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(),
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
第一件要注意的事情是使用nn.Sequential()。这使我们能够创建一系列层。当我们在forward()中使用这些链中的一个时,输入会依次通过每个层的数组元素。您可以使用这个方法将模型分解为更合理的安排。在这个网络中,我们有两个链:features块和classifier。让我们看看我们正在引入的新层,从Conv2d开始。
卷积
Conv2d层是2D 卷积。如果我们有一个灰度图像,它由一个数组组成,x像素宽,y像素高,每个条目的值表示它是黑色、白色还是介于两者之间(我们假设是 8 位图像,因此每个值可以从 0 到 255 变化)。对于这个例子,我们看一个 4 像素高宽的小方形图像:
10 11 9 3 2 123 4 0 45 237 23 99 20 67 22 255
接下来我们介绍一种叫做filter或卷积核的东西。这是另一个矩阵,很可能更小,我们将它拖过我们的图像。这是我们的 2×2 filter:
1 0 1 0
为了产生我们的输出,我们取较小的 filter 并将其传递到原始输入上,就像放大镜放在一张纸上一样。从左上角开始,我们的第一个计算如下:
10 11 2 123 1 0 1 0
我们所做的就是将矩阵中的每个元素与另一个矩阵中的对应成员相乘,并求和结果:(10 × 1) + (11 × 0) + (2 × 1) + (123 × 0) = 12。做完这个之后,我们将滤波器移动并重新开始。但是我们应该移动滤波器多少?在这种情况下,我们将滤波器移动 2 个单位,这意味着我们的第二次计算是:
9 3 4 0 1 0 1 0
这给我们一个输出为 13。现在我们将滤波器向下移动并向左移动,重复这个过程,给出这个最终结果(或特征图):
12 13 65 45
在图 3-1 中,您可以看到这是如何以图形方式工作的,一个 3×3 卷积核被拖动到一个 4×4 张量上,并产生一个 2×2 的输出(尽管每个部分基于九个元素而不是我们第一个示例中的四个)。
![3x3 卷积核在 4x4 输入上的操作
图 3-1。3×3 卷积核在 4×4 输入上的操作
卷积层将有许多这样的滤波器,这些滤波器的值是由网络的训练填充的,该层中的所有滤波器共享相同的偏置值。让我们回到如何调用Conv2d层并看看我们可以设置的其他选项:
nn.Conv2d(in_channels,out_channels, kernel_size, stride, padding)
in_channels是我们在该层接收的输入通道数。在网络的开始,我们将 RGB 图像作为输入,因此输入通道数为三。out_channels是输出通道数,对应于卷积层中的滤波器数量。接下来是kernel_size,描述了滤波器的高度和宽度。¹ 这可以是一个指定正方形的单个标量(例如,在第一个卷积层中,我们设置了一个 11×11 的滤波器),或者您可以使用一个元组(例如(3,5)表示一个 3×5 的滤波器)。
接下来的两个参数似乎无害,但它们可能对网络的下游层产生重大影响,甚至影响该特定层最终查看的内容。stride表示我们在调整滤波器到新位置时在输入上移动多少步。在我们的示例中,我们最终得到步幅为 2,这使得特征图的大小是输入的一半。但我们也可以使用步幅为 1 移动,这将给我们一个 4×4 的特征图输出,与输入的大小相同。我们还可以传入一个元组*(a,b),允许我们在每一步上移动a个单位横向和b*个单位纵向。现在,您可能想知道,当它到达末尾时会发生什么。让我们看看。如果我们以步幅 1 拖动我们的滤波器,最终会到达这一点:
3 ? 0 ?
我们的输入中没有足够的元素来进行完整的卷积。那么会发生什么?这就是padding参数发挥作用的地方。如果我们给出padding值为 1,我们的输入看起来有点像这样:
0 0 0 0 0 0 0 10 11 9 3 0 0 2 123 4 0 0 0 45 237 23 99 0 0 20 67 22 255 0 0 0 0 0 0 0
现在当我们到达边缘时,我们的过滤器覆盖的值如下:
3 0 0 0
如果不设置填充,PyTorch 在输入的最后几列中遇到的任何边缘情况都会被简单地丢弃。您需要适当设置填充。与stride和kernel_size一样,您也可以传入一个height×weight填充的元组,而不是填充相同的单个数字。
这就是我们模型中的Conv2d层在做的事情。但是那些MaxPool2d层呢?
池化
与卷积层一起,您经常会看到池化层。这些层将网络的分辨率从前一个输入层降低,这使得我们在较低层中有更少的参数。这种压缩导致计算速度更快,有助于防止网络过拟合。
在我们的模型中,我们使用了一个核大小为 3,步长为 2 的MaxPool2d。让我们通过一个示例来看看它是如何工作的。这是一个 5×3 的输入:
1 2 1 4 1 5 6 1 2 5 5 0 0 9 6
使用 3×3 的核大小和步长 2,我们从池化中得到两个 3×3 的张量:
1 2 1 5 6 1 5 0 01 4 1 1 2 5 0 9 6
在MaxPool中,我们从这些张量中取最大值,得到一个输出张量为[6,9]。就像在卷积层中一样,MaxPool有一个padding选项,可以在张量周围创建一个零值边界,以防步长超出张量窗口。
正如你可以想象的那样,除了从内核中取最大值之外,你还可以使用其他函数进行池化。一个流行的替代方法是取张量值的平均值,这样允许所有张量数据都参与到池中,而不仅仅是max情况下的一个值(如果你考虑一幅图像,你可以想象你可能想要考虑像素的最近邻)。此外,PyTorch 提供了AdaptiveMaxPool和AdaptiveAvgPool层,它们独立于传入输入张量的维度工作(例如,我们的模型中有一个AdaptiveAvgPool)。我建议在构建模型架构时使用这些,而不是标准的MaxPool或AvgPool层,因为它们允许你创建可以处理不同输入维度的架构;在处理不同数据集时这很方便。
我们还有一个新组件要讨论,这个组件非常简单但对训练非常重要。
Dropout
神经网络的一个经常出现的问题是它们倾向于过拟合训练数据,深度学习领域正在进行大量工作,以确定允许网络学习和泛化到非训练数据的方法,而不仅仅是学习如何对训练输入做出响应。Dropout 层是一个极其简单但重要的方法,它易于理解且有效:如果我们在训练周期内不训练网络中的一组随机节点会怎样?因为它们不会被更新,它们就不会有机会过拟合输入数据,而且因为是随机的,每个训练周期将忽略不同的输入选择,这应该进一步帮助泛化。
在我们示例 CNN 网络中,默认情况下,Dropout 层的初始化为0.5,意味着输入张量的 50%会被随机置零。如果你想将其更改为 20%,请在初始化调用中添加p参数:Dropout(p=0.2)。
注意
Dropout 应该只在训练期间发生。如果在推理时发生,你会失去网络推理能力的一部分,这不是我们想要的!幸运的是,PyTorch 的Dropout实现会根据你运行的模式来确定,并在推理时通过Dropout层传递所有数据。
在查看了我们的小型 CNN 模型并深入研究了层类型之后,让我们看看过去十年中制作的其他模型。
CNN 架构的历史
尽管 CNN 模型已经存在几十年了(例如,LeNet-5 在 1990 年代末用于支票上的数字识别),但直到 GPU 变得广泛可用,深度 CNN 网络才变得实用。即使是在那时,深度学习网络开始压倒所有其他现有方法在图像分类中的应用也仅有七年。在本节中,我们将回顾过去几年的一些 CNN 学习里程碑,并探讨一些新技术。
AlexNet
AlexNet 在许多方面改变了一切。它于 2012 年发布,并在当年的 ImageNet 竞赛中以 15.3%的前五错误率摧毁了所有其他参赛作品(第二名的前五错误率为 26.2%,这让你了解了它比其他最先进方法好多少)。AlexNet 是最早引入MaxPool和Dropout概念的架构之一,甚至推广了当时不太知名的ReLU激活函数。它是最早证明许多层次在 GPU 上训练是可能且高效的架构之一。虽然它不再是最先进的,但仍然是深度学习历史上的重要里程碑。
AlexNet 架构是什么样的?啊哈,是时候让你知道一个小秘密了。我们在本章中迄今为止一直在使用的网络?就是 AlexNet。惊喜!这就是为什么我们使用标准的MaxPool2d而不是AdaptiveMaxPool2d,以匹配原始的 AlexNet 定义。
Inception/GoogLeNet
让我们直接跳到 2014 年 ImageNet 比赛的获胜者。GoogLeNet 架构引入了Inception模块,解决了 AlexNet 的一些缺陷。在该网络中,卷积层的卷积核被固定在某个分辨率上。我们可能期望图像在宏观和微观尺度上都有重要的细节。使用较大的卷积核可能更容易确定一个对象是否是汽车,但要确定它是 SUV 还是掀背车可能需要一个较小的卷积核。而要确定车型,我们可能需要一个更小的卷积核来识别标志和徽标等细节。
Inception 网络代替了在同一输入上运行一系列不同尺寸的卷积,并将所有滤波器连接在一起传递到下一层。不过,在执行任何操作之前,它会进行一个 1×1 的卷积作为瓶颈,压缩输入张量,这意味着 3×3 和 5×5 的卷积核操作的过滤器数量比如果没有 1×1 卷积存在时要少。你可以在图 3-2 中看到一个 Inception 模块的示例。
图 3-2。一个 Inception 模块
原始的 GoogLeNet 架构使用了九个这样的模块堆叠在一起,形成一个深度网络。尽管深度较大,但总体参数比 AlexNet 少,同时提供了一个 6.67%的前五名错误率,接近人类的表现。
VGG
2014 年 ImageNet 的第二名是来自牛津大学的 Visual Geometry Group(VGG)网络。与 GoogLeNet 相比,VGG 是一个更简单的卷积层堆叠。在最终分类层之前,它展示了简单深度架构的强大之处(在 VGG-16 配置中获得了 8.8%的前五名错误率)。图 3-3 展示了 VGG-16 从头到尾的层。
VGG 方法的缺点是最终的全连接层使网络膨胀到一个庞大的尺寸,与 GoogLeNet 的 700 万参数相比,达到了 1.38 亿参数。尽管如此,VGG 网络在深度学习领域仍然非常受欢迎,因为它的构造更简单,训练权重早期可用。你经常会看到它在样式转移应用中使用(例如,将照片转换为梵高的画作),因为它的卷积滤波器的组合似乎捕捉到了这种信息,这种信息比更复杂的网络更容易观察。
图 3-3。VGG-16
ResNet
一年后,微软的 ResNet 架构在 ImageNet 2015 比赛中获得了 ResNet-152 变体的 4.49%和集成模型的 3.57%的前五名得分(在这一点上基本超越了人类的能力)。ResNet 带来的创新是改进了 Inception 风格的层叠层次结构方法,其中每个层叠执行通常的 CNN 操作,但还将传入的输入添加到块的输出中,如图 3-4 所示。
这种设置的优势在于每个块将原始输入传递到下一层,允许训练数据的“信号”在比 VGG 或 Inception 更深的网络中传递。(在深度网络中的权重变化的损失被称为梯度消失,因为在训练过程中反向传播的梯度变化趋于零。)
图 3-4。一个 ResNet 块
其他架构也是可用的!
自 2015 年以来,许多其他架构已经逐步提高了在 ImageNet 上的准确性,例如 DenseNet(ResNet 思想的延伸,允许构建 1,000 层的庞大架构),但也有很多工作致力于创建像 SqueezeNet 和 MobileNet 这样的架构,它们提供了合理的准确性,但与 VGG、ResNet 或 Inception 等架构相比,它们要小得多。
另一个重要的研究领域是让神经网络开始设计神经网络。到目前为止,最成功的尝试当然来自 Google,他们的 AutoML 系统生成了一个名为NASNet的架构,在 ImageNet 上的前五错误率为 3.8%,这是我在 2019 年初写这篇文章时的最新技术水平(还有另一个来自 Google 的自动生成架构称为PNAS)。事实上,ImageNet 比赛的组织者已经决定停止在这个领域进行进一步的比赛,因为这些架构已经超越了人类的能力水平。
这将我们带到了这本书出版时的最新技术水平,所以让我们看看我们如何可以使用这些模型而不是定义我们自己的。
在 PyTorch 中使用预训练模型
显然,每次想使用一个模型都要定义一个模型将是一件麻烦事,特别是一旦你远离 AlexNet,所以 PyTorch 在torchvision库中默认提供了许多最受欢迎的模型。对于 AlexNet,你只需要这样做:
import torchvision.models as models
alexnet = models.alexnet(num_classes=2)
VGG、ResNet、Inception、DenseNet 和 SqueezeNet 变体的定义也是可用的。这给了你模型的定义,但你也可以进一步调用models.alexnet(pretrained=True)来下载 AlexNet 的预训练权重,让你可以立即用它进行分类,无需额外的训练。(但正如你将在下一章中看到的那样,你可能需要进行一些额外的训练来提高你特定数据集上的准确性。)
话虽如此,至少建立自己的模型一次是有必要的,这样你就能感受到它们如何组合在一起。这是一个很好的练习,在 PyTorch 中构建模型架构的方法,当然你也可以与提供的模型进行比较,以确保你所构建的与实际定义相匹配。但是你如何找出那个结构是什么呢?
检查模型的结构
如果你对其中一个模型是如何构建的感到好奇,有一个简单的方法可以让 PyTorch 帮助你。例如,这里是整个 ResNet-18 架构的一个示例,我们只需调用以下内容:
print(model)
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3),
bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1,
dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2),
padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2),
bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2),
padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2),
bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2),
padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2),
bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)
在这一章中,你几乎没有看到什么新东西,除了BatchNorm2d。让我们看看其中一个层中的作用。
BatchNorm
BatchNorm,即批量归一化,是一个简单的层,它的生活中只有一个任务:使用两个学习参数(意味着它将与网络的其余部分一起训练)来尝试确保通过网络的每个小批量具有以零为中心的均值和方差为 1。你可能会问为什么我们需要这样做,当我们已经通过使用第二章中的变换链对输入进行了归一化。对于较小的网络,BatchNorm确实不太有用,但随着它们变得更大,任何一层对另一层的影响,比如说 20 层之后,可能会很大,因为重复的乘法,你可能会得到消失或爆炸的梯度,这两者对训练过程都是致命的。BatchNorm层确保即使你使用像 ResNet-152 这样的模型,你网络内部的乘法也不会失控。
您可能会想:如果我们的网络中有BatchNorm,为什么在训练循环的转换链中还要对输入进行归一化呢?毕竟,BatchNorm不应该为我们做这项工作吗?答案是是的,您可以这样做!但网络将需要更长的时间来学习如何控制输入,因为它们将不得不自己发现初始转换,这将使训练时间更长。
我建议您实例化我们到目前为止讨论过的所有架构,并使用print(model)来查看它们使用的层以及操作发生的顺序。之后,还有另一个关键问题:我应该使用这些架构中的哪一个?
您应该使用哪个模型?
没有帮助的答案是,自然是哪个对您最有效!但让我们深入一点。首先,尽管我建议您目前尝试 NASNet 和 PNAS 架构,但我不会全力推荐它们,尽管它们在 ImageNet 上取得了令人印象深刻的结果。它们在操作中可能会消耗大量内存,并且迁移学习技术(您将在第四章中了解到)与人工构建的架构(包括 ResNet)相比并不那么有效。
我建议您在Kaggle上浏览基于图像的比赛,这是一个举办数百个数据科学比赛的网站,看看获胜作品在使用什么。很可能您会看到一堆基于 ResNet 的集成模型。就我个人而言,我喜欢并使用 ResNet 架构,因为它们提供了良好的准确性,并且很容易从 ResNet-34 模型开始尝试实验,然后转向更大的 ResNet(更现实地说,使用不同 ResNet 架构的集成模型,就像微软在 2015 年 ImageNet 比赛中使用的那样),一旦我觉得有所希望。
在结束本章之前,我有一些关于下载预训练模型的最新消息。
模型一站式购物:PyTorch Hub
PyTorch 世界最近的一项公告提供了另一种获取模型的途径:PyTorch Hub。这将成为未来获取任何已发布模型的中心位置,无论是用于处理图像、文本、音频、视频还是其他任何类型的数据。要以这种方式获取模型,您可以使用torch.hub模块:
model = torch.hub.load('pytorch/vision', 'resnet50', pretrained=True)
第一个参数指向一个 GitHub 所有者和存储库(字符串中还可以包含可选的标签/分支标识符);第二个是请求的模型(在本例中为resnet50);最后一个指示是否下载预训练权重。您还可以使用torch.hub.list('pytorch/vision')来发现该存储库中可供下载的所有模型。
PyTorch Hub 是 2019 年中新推出的,所以在我写这篇文章时可用的模型数量并不多,但我预计到年底它将成为一个流行的模型分发和下载方式。本章中的所有模型都可以通过 PytorchHub 中的pytorch/vision存储库加载,所以可以随意使用这种加载过程,而不是torchvision.models。
结论
在这一章中,您已经快速了解了基于 CNN 的神经网络是如何工作的,包括Dropout、MaxPool和BatchNorm等特性。您还看了当今工业中最流行的架构。在继续下一章之前,尝试一下我们讨论过的架构,看看它们之间的比较。(不要忘记,您不需要训练它们!只需下载权重并测试模型。)
我们将通过使用这些预训练模型作为我们猫对鱼问题的自定义解决方案的起点来结束我们对计算机视觉的探讨,这将使用迁移学习。
进一步阅读
-
AlexNet: “使用深度卷积神经网络进行 ImageNet 分类” 作者:Alex Krizhevsky 等人(2012 年)
-
VGG: “用于大规模图像识别的非常深的卷积网络” 作者:Karen Simonyan 和 Andrew Zisserman(2014 年)
-
Inception: “使用卷积进行更深层次的研究” 作者:Christian Szegedy 等人(2014 年)
-
ResNet: “用于图像识别的深度残差学习” 作者:Kaiming He 等人(2015 年)
-
NASNet: “学习可迁移的架构以实现可扩展的图像识别” 作者:Barret Zoph 等人(2017 年)
¹ 在文献中,核和滤波器往往可以互换使用。如果您有图形处理经验,核可能更熟悉,但我更喜欢滤波器。