PyTorch-深度学习快速启动指南-全-
PyTorch 深度学习快速启动指南(全)
原文:
zh.annas-archive.org/md5/59199cdba59917da08494beef7a2e646
译者:飞龙
前言
PyTorch 出人意料地易于学习,并提供高级功能,如支持多处理器、分布式和并行计算。PyTorch 拥有一系列预训练模型,为图像分类提供即插即用的解决方案。PyTorch 提供了最易于入门的前沿深度学习平台之一。它与 Python 编程语言紧密集成,因此对 Python 程序员来说,编写代码似乎是自然而直观的。其独特的动态计算图处理方式意味着 PyTorch 既高效又灵活。
本书适合对象
本书适合任何希望通过 PyTorch 进行深度学习实践的人。本书的目标是通过直接实验为您提供深度学习模型的理解。这本书非常适合那些熟悉 Python、了解一些机器学习基础知识,并希望有效提升技能的人士。本书将重点介绍最重要的功能,并提供实际示例。它假设您具有 Python 的工作知识,并熟悉相关的数学思想,包括线性代数和微分计算。本书提供了足够的理论知识,使您能够快速上手,而不需要严格的数学理解。通过本书,您将掌握深度学习系统的实际知识,并能够应用 PyTorch 模型解决您关心的问题。
本书内容概述
第一章,PyTorch 简介,带您快速上手 PyTorch,演示了在各种平台上的安装方法,并探讨了关键语法元素以及如何导入和使用 PyTorch 中的数据。
第二章,深度学习基础,是对深度学习基础的快速介绍,涵盖了优化、线性网络和神经网络的数学和理论。
第三章,计算图与线性模型,演示了如何计算线性网络的误差梯度,并如何利用它来分类图像。
第四章,卷积网络,讨论了卷积网络的理论及其在图像分类中的应用。
第五章,其他神经网络结构,讨论了循环网络背后的理论,并展示了如何使用它们来预测序列数据。还讨论了长短期记忆网络(LSTMs),并指导您构建语言模型来预测文本。
第六章,深度挖掘 PyTorch,探讨了一些高级功能,如在多处理器和并行环境中使用 PyTorch。您将使用预训练模型构建一个灵活的图像分类解决方案。
为了充分利用本书
本书不假设任何专业知识,只需扎实的一般计算机技能。Python 是一种相对容易(并且非常有用!)的语言,因此如果您的编程背景有限或没有,也不必担心。
本书确实包含一些相对简单的数学知识和一些理论,可能会让一些读者一开始感到困难。深度学习模型是复杂的系统,即使是简单的神经网络的行为也是一个非平凡的练习。幸运的是,PyTorch 作为这些复杂系统的高级框架,因此即使不是专家也可以取得很好的结果。
安装软件很简单,基本上只需要两个软件包:Python 的 Anaconda 发行版和 PyTorch 本身。该软件可以在 Windows 7 和 10,macOS 10.10 或以上版本以及大多数 Linux 版本上运行。它可以在桌面机器或服务器环境中运行。本书中的所有代码都是使用 PyTorch 版本 1.0 和 Python 3 在 Ubuntu 16 上测试的。
下载示例代码文件
您可以从您的帐户在www.packt.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support注册,将文件直接发送至您的邮箱。
您可以按照以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的说明操作。
下载文件后,请确保使用最新版本解压或提取文件夹:
-
Windows 使用 WinRAR/7-Zip
-
Mac 使用 Zipeg/iZip/UnRarX
-
Linux 使用 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Deep-Learning-with-PyTorch-Quick-Start-Guide
。如果代码有更新,将在现有的 GitHub 仓库上更新。
我们还提供来自我们丰富书籍和视频目录的其他代码包,请访问github.com/PacktPublishing/
查看!
下载彩色图片
我们还提供一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789534092_ColorImages.pdf
。
使用的约定
本书中使用了许多文本约定。
CodeInText
:表示文本中的代码词汇,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟网址,用户输入和 Twitter 用户名。例如:"将下载的 WebStorm-10*.dmg
磁盘映像文件挂载为系统中的另一个磁盘。"
代码块设置如下:
import numpy as np
x = np.array([[1,2,3],[4,5,6],[1,2,5]])
y = np.linalg.inv(x)
print (y)
print (np.dot(x,y))
当我们希望引起您对代码块特定部分的注意时,相关行或项设置为粗体:
import numpy as np
x = np.array([[1,2,3],[4,5,6],[1,2,5]])
y = np.linalg.inv(x)
print (y)
print (np.dot(x,y))
粗体:表示新术语,重要词汇或屏幕上看到的单词。例如,菜单或对话框中的字词在文本中显示为这样。例如:"从管理面板中选择系统信息。"
警告或重要说明以这种方式出现。
提示和技巧以这种方式出现。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发送电子邮件至 customercare@packtpub.com
。
勘误:尽管我们已尽一切努力确保内容的准确性,但错误确实会发生。如果您在本书中发现错误,请向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,单击“勘误提交表单”链接并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,请向我们提供位置地址或网站名称。请联系我们,提供材料的链接至 copyright@packt.com
。
如果您有兴趣成为作者:如果您在某个主题上有专业知识,并且有意撰写或为书籍做贡献,请访问 authors.packtpub.com。
评论
请留下您的评论。一旦您阅读并使用了本书,为何不在您购买它的网站上留下一条评论呢?潜在的读者可以通过您的客观意见来做出购买决策,我们在 Packt 可以了解您对我们产品的看法,我们的作者也可以看到您对他们书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packt.com。
第一章:PyTorch 介绍
这是使用 PyTorch 框架进行深度学习的逐步介绍。PyTorch 是进入深度学习的一个绝佳起点,如果你对 Python 有一些了解,那么你会发现 PyTorch 是一个直观、高效和启发性的体验。快速原型设计实验和测试想法的能力是 PyTorch 的核心优势。再加上能够将实验转化为生产可部署资源的可能性,学习曲线挑战将得到丰富的回报。
PyTorch 是理解深度学习概念的相对简单和有趣的方式。你可能会惊讶于解决分类问题(如手写识别和图像分类)所需的代码行数是多么少。尽管说 PyTorch 是易于使用,但不能否认深度学习在很多方面都是困难的。它涉及一些复杂的数学和一些棘手的逻辑难题。然而,这不应该让人们忘记这一企业的有趣和有用部分。毫无疑问,机器学习可以为我们周围的世界提供深刻的见解,并解决重要的问题,但要到达那里可能需要一些工作。
本书试图不是简单地掠过重要的思想,而是以不带行话且简洁的方式解释它们。如果解决复杂的微分方程的想法让你感到心慌,你并不孤单。这可能与高中时期一位脾气暴躁的数学老师愤怒地要求你引用欧拉公式或三角恒等式有关。这是一个问题,因为数学本身应该是有趣的,洞察力并不是通过费力记忆公式而是通过理解关系和基础概念产生的。
深度学习看起来困难的另一个原因是其具有多样化和动态的研究前沿。对于新手来说,这可能会感到困惑,因为它并没有明显的入门点。如果你理解了一些原则并想测试你的想法,那么找到一个合适的工具集可能会是一个令人困惑的任务。开发语言、框架、部署架构等的组合呈现出一个非平凡的决策过程。
机器学习科学已经发展到一个阶段,即已经出现了一组通用算法来解决分类和回归等问题。随后,几个框架被创建出来,以利用这些算法的力量并将它们用于一般问题解决。这意味着入门点已经达到这样一个水平,以至于这些技术现在已经掌握在非计算机科学专业人士手中。各种领域的专家现在可以使用这些思想来推动他们的努力。通过本书,再加上一点奉献精神,你将能够构建和部署有用的深度学习模型,以帮助解决你感兴趣的问题。
在本章中,我们将讨论以下主题:
-
什么是 PyTorch?
-
安装 PyTorch
-
基本操作
-
加载数据
什么是 PyTorch?
PyTorch 是一种用于实验、研究和生产的动态张量深度学习框架。它可以作为 NumPy 的支持 GPU 的替代品,或者作为构建神经网络的灵活高效平台。动态图创建和紧密的 Python 集成使得 PyTorch 在深度学习框架中脱颖而出。
如果你对深度学习生态系统稍有了解,那么像 Theano 和 TensorFlow 这样的框架,或者更高级的衍生框架如 Keras,都是最受欢迎的。PyTorch 是深度学习框架中的相对新进者。尽管如此,它现在已经被 Google、Twitter 和 Facebook 广泛使用。它与其他框架的区别在于,Theano 和 TensorFlow 将计算图编码为静态结构,需要在封闭会话中运行。相反,PyTorch 可以动态实现计算图。对于神经网络来说,这意味着网络在运行时可以更改行为,几乎没有额外开销。而在 TensorFlow 和 Theano 中,要更改行为,实际上需要从头开始重建网络。
这种动态实现是通过一种称为基于“tape”的自动微分过程实现的,允许 PyTorch 表达式自动进行微分。这带来了许多优势。梯度可以即时计算,由于计算图是动态的,因此可以在每次函数调用时更改,允许在循环中和条件调用下以有趣的方式使用。PyTorch 的这种动态行为和极大的灵活性使其成为深度学习实验平台的首选。
PyTorch 的另一个优势是与 Python 语言的紧密集成。对于 Python 编程者来说,它非常直观,并且与 NumPy 和 SciPy 等其他 Python 包无缝交互。PyTorch 非常易于进行实验。它不仅是构建和运行有用模型的理想工具,还是通过直接实验理解深度学习原理的好方式。
如你所料,PyTorch 可以在多个图形处理单元(GPUs)上运行。深度学习算法可能需要大量计算资源,尤其是在处理大型数据集时更是如此。PyTorch 具有强大的 GPU 支持,在进程间智能地共享张量内存。这基本上意味着在 CPU 和 GPU 之间有一种高效且用户友好的方式来分配处理负载。这可以极大地减少测试和运行大型复杂模型所需的时间。
动态图生成,紧密集成 Python 语言,并且相对简单的 API 使得 PyTorch 成为研究和实验的优秀平台。然而,PyTorch 1 之前的版本存在缺陷,阻碍了其在生产环境中的卓越表现。这一不足正在 PyTorch 1 中得到解决。
研究是深度学习的一个重要应用,但越来越多地,深度学习被嵌入到在 Web 上、设备上或机器人中实时运行的应用程序中。这样的应用程序可能会处理成千上万个同时查询并与大规模动态数据交互。尽管 Python 是人类工作中最好的语言之一,但其他语言,如 C++和 Java,通常具有特定的效率和优化。即使构建特定深度学习模型的最佳方式可能是使用 PyTorch,但这可能不是部署它的最佳方式。这不再是一个问题,因为现在使用 PyTorch 1,我们可以导出 Python free 模型的 PyTorch 表示。
这是 Facebook 和 PyTorch 的主要利益相关者与微软合作的结果,创建了开放神经网络交换(ONNX),以帮助开发人员在不同框架之间转换神经网络模型。这导致了 PyTorch 与更适合生产的框架 CAFFE2 的合并。在 CAFFE2 中,模型由纯文本模式表示,使其与语言无关。这意味着它们更容易部署到 Android、iOS 或 Raspberry Pi 设备上。
有了这个想法,PyTorch 版本 1 扩展了其 API,包括生产就绪功能,如优化 Android 和 iPhone 的代码,一个即时(JIT)C++编译器,以及几种方式来创建Python free模型的表示。
总之,PyTorch 具有以下特点:
-
动态图表示
-
与 Python 编程语言紧密集成
-
高低级 API 混合使用
-
在多个 GPU 上实现简单
-
能够构建Python-free模型表示以进行导出和生产
-
使用 Caffe 框架扩展到大规模数据
安装 PyTorch
PyTorch 将在 macOS X、64 位 Linux 和 64 位 Windows 上运行。请注意,Windows 目前不提供(易于)支持 PyTorch 中 GPU 的使用。在安装 PyTorch 之前,您需要在计算机上安装 Python 2.7 或 Python 3.5 / 3.6,并记住为每个 Python 版本安装正确的版本。除非有理由不这样做,建议您安装 Anaconda Python 发行版。这可以从以下地址获取:anaconda.org/anaconda/python
。
Anaconda 包括 PyTorch 的所有依赖项,以及在深度学习中必不可少的技术、数学和科学库。这些将在整本书中使用,因此,除非您想单独安装它们,否则请安装 Anaconda。
以下是我们在本书中将要使用的软件包和工具列表。它们都已经在 Anaconda 中安装好:
-
NumPy
: 主要用于处理多维数组的数学库 -
Matplotlib
: 用于绘图和可视化的库 -
SciPy
: 用于科学和技术计算的软件包 -
Skit-Learn
: 用于机器学习的库 -
Pandas
: 用于处理数据的库。 -
IPython
: 一种笔记本风格的代码编辑器,用于在浏览器中编写和运行代码。
安装了 Anaconda 之后,现在可以安装 PyTorch 了。请访问 PyTorch 网站:pytorch.org/
。
此网站上的安装矩阵相当自解释。只需选择您的操作系统、Python 版本,以及如果您有 GPU,则选择您的 CUDA 版本,然后运行适当的命令。
与往常一样,在安装 PyTorch 之前确保您的操作系统和依赖包是最新的是个好习惯。Anaconda 和 PyTorch 支持 Windows、Linux 和 macOS,尽管 Linux 可能是最常用和最一致的操作系统。在本书中,我将使用 Python 3.7 和 Anaconda 3.6.5 运行在 Linux 上。
本书中的代码是在 Jupyter Notebook 上编写的,这些笔记本可以从该书的网站上获取。
您可以选择在本地自己的计算机上设置 PyTorch 环境,也可以选择在云服务器上远程设置。它们各有利弊。在本地工作的优势在于通常更容易和更快地开始。特别是如果您不熟悉 SSH 和 Linux 终端的话。只需安装 Anaconda 和 PyTorch,您就可以开始了。此外,您可以选择和控制自己的硬件,虽然这是一笔前期成本,但通常在长期来看更便宜。一旦您开始扩展硬件需求,云解决方案可能变得昂贵。在本地工作的另一个优势是可以选择和定制您的集成开发环境(IDE)。事实上,Anaconda 有自己出色的桌面 IDE 称为 Spyder。
在构建自己的深度学习硬件时,需要注意几件事情,您需要 GPU 加速:
-
使用 NVIDIA 兼容 CUDA 的 GPU(例如 GTX 1060 或 GTX 1080)。
-
至少具有 16 个 PCIe 通道的芯片组。
-
至少 16GB 的 RAM。
在云上工作确实提供了从任何计算机工作的灵活性,以及更容易地尝试不同的操作系统、平台和硬件。您还可以更轻松地分享和协作。通常可以廉价开始,每月几美元,甚至免费,但随着项目变得更复杂和数据密集,您需要支付更多的容量。
让我们简要看一下两个云服务器主机的安装过程:Digital Ocean 和 Amazon Web Services。
Digital Ocean。
Digital Ocean 提供了进入云计算的最简单入口之一。它提供可预测的简单付款结构和直观的服务器管理。不幸的是,Digital Ocean 目前不支持 GPU。其功能围绕着droplets(预构建的虚拟专用服务器实例)展开。以下是设置 droplet 所需的步骤:
-
在Digital Ocean注册一个账号。前往
www.digitalocean.com/.
-
点击“创建”按钮并选择“新建 Droplet”。
-
选择 Linux 的 Ubuntu 发行版,并选择两千兆字节或以上的计划。
-
如果需要,选择 CPU 优化。默认值应该足以开始使用。
-
可选择设置公共/私有密钥加密。
-
使用您收到的电子邮件中的信息设置 SSH 客户端(例如 PuTTY)。
-
通过 SSH 客户端连接到您的 Droplet,并
curl
获取最新的 Anaconda 安装程序。您可以在repo.continuum.io/
找到适合您特定环境的安装程序地址。 -
使用以下命令安装 PyTorch:
conda install pytorch torchvision -c pytorch
一旦您启动了您的 Droplet,您可以通过 SSH 客户端访问 Linux 命令。从命令提示符,您可以curl
获取最新的 Anaconda 安装程序,地址是:www.anaconda.com/download/#linux
。
在Digital Ocean 教程部分可以找到完整的逐步指南。
远程连接到 IPython
IPython 是通过 Web 浏览器编辑代码的简便方式。如果您使用桌面电脑,只需启动 IPython 并将浏览器指向localhost:8888
。这是 IPython 服务器 Jupyter 运行的端口。然而,如果您在云服务器上工作,那么通过 SSH 隧道连接到 IPython 是处理代码的常见方式。连接到 IPython 的隧道包括以下步骤:
-
在 SSH 客户端中,将目标端口设置为
localhost:8888
。在 PuTTY 中,转到 Connection | SSH | Tunnels。 -
将源端口设置为大于
8000
以避免与其他服务冲突。点击添加。保存这些设置并打开连接。像往常一样登录到您的 Droplet。 -
通过在服务器实例的命令提示符中键入
jupyter notebook
启动 IPython 服务器。 -
通过将浏览器指向
localhost: source port
,例如localhost:8001
,访问 IPython。 -
启动 IPython 服务器。
请注意,您可能需要一个令牌第一次访问服务器。这可以从您启动 Jupyter 后的命令输出中获取。您可以直接复制此输出中给出的 URL 到浏览器的地址栏,并将端口地址更改为您的本地源端口地址,例如:8001
,或者您可以选择将令牌(token=
后面的部分)粘贴到 Jupyter 启动页面,并将其替换为将来方便使用的密码。现在,您应该能够打开、运行和保存 IPython 笔记本。
亚马逊网络服务(AWS)
AWS 是最初的云计算平台,以其高度可扩展的架构而闻名。它提供了广泛的产品。我们需要开始的是一个 EC2 实例。这可以从 AWS 控制面板的服务选项卡中访问。从那里,选择 EC2,然后启动实例。在这里,您可以选择所需的机器映像。AWS 提供了几种专门用于深度学习的机器映像类型。可以随意尝试任何其中的一个,但我们将在此使用的是适用于 Ubuntu 版本 10 的深度学习 AMI。它预装了 PyTorch 和 TensorFlow 的环境。选择完毕后,您可以选择其他选项。默认的 T2 微型实例,配备 2GB 内存,对于实验来说应该足够了;然而,如果您需要 GPU 加速,您将需要使用 T2 中型实例类型。最后,在启动实例时,系统将提示您创建和下载公共-私有密钥对。然后,您可以使用 SSH 客户端连接到服务器实例,并按照先前的说明进行到 Jupyter Notebook 的隧道。再次检查文档以获取更详细的信息。亚马逊采用按资源付费的模式,因此重要的是您监控您正在使用的资源,以确保不会收到任何不必要或意外的费用。
PyTorch 的基本操作
张量是 PyTorch 的核心工具。如果您了解线性代数,它们相当于矩阵。Torch 张量实际上是numpy.array
对象的扩展。张量是深度学习系统中重要的概念组成部分,因此理解它们的工作原理至关重要。
在我们的第一个示例中,我们将查看大小为 2 x 3 的张量。在 PyTorch 中,我们可以像创建 NumPy 数组一样创建张量。例如,我们可以传递嵌套列表,如下面的代码所示:
在这里,我们创建了两个维度为 2 x 3 的张量。您可以看到,我们创建了一个简单的线性函数(有关线性函数的更多信息,请参见第二章,深度学习基础),并将其应用于x和y,然后打印出结果。我们可以使用以下图表可视化这一点:
正如您可能从线性代数中知道的那样,矩阵乘法和加法是按元素进行的,因此对于x的第一个元素,我们将其写为X[00]。这与Y[00]相乘并加到y的第一个元素上,写为Y[01] = 8,所以f[01] = 4 + 12*。请注意,索引从零开始。
如果你从未见过任何线性代数,不要太担心,因为我们将在《深度学习基础》第二章中进行概述,并且很快你将开始使用 Python 索引。现在,只需将我们的 2 x 3 张量视为其中带有数字的表即可。
默认值初始化
有许多情况下我们需要将 torch 张量初始化为默认值。在这里,我们创建了三个 2 x 3 的张量,分别用零、一和随机浮点数填充它们:
在初始化随机数组时需要考虑的一个重要点是所谓的可重复性种子。看看当你多次运行上述代码时会发生什么。每次都会得到不同的随机数数组。在机器学习中经常需要能够重现结果。我们可以通过使用随机种子来实现这一点。这在下面的代码中进行了演示:
注意,当你多次运行这段代码时,张量的值保持不变。如果删除第一行中的种子,每次运行代码时张量的值将不同。不管你使用什么数字来种子随机数生成器,只要它是一致的,就能实现可重现的结果。
张量和 NumPy 数组之间的转换
将 NumPy 数组转换为张量就像对其执行一个操作一样简单。下面的代码应该清楚地表明这一点:
我们可以看到 torch 张量的类型结果。在许多情况下,我们可以互换地使用 NumPy 数组和张量,并始终确保结果是张量。然而,有时我们需要显式地从数组创建张量。这可以通过 torch.from_numpy
函数来实现:
要从张量转换为 NumPy 数组,只需调用 torch.numpy()
函数:
注意,我们使用了 Python 内置的 type()
函数,如 type(object)
,而不是我们之前使用的 tensor.type()
。NumPy 数组没有 type
属性是另一个重要的理解点。另外一个要注意的是,NumPy 数组和 PyTorch 张量共享同一内存空间。例如,看看当我们像下面的代码演示一样改变一个变量的值时会发生什么:
还要注意,当我们打印一个张量时,它返回一个元组,包含张量本身和其 dtype
,或数据类型属性。这在这里非常重要,因为有些 dtype
的数组不能转换成张量。例如,考虑下面的代码:
这将生成一个错误消息,告诉我们只有支持的dtype
能够被转换为张量。显然,int8
不是其中之一。我们可以通过在传递给torch.from_numpy
之前将我们的int8
数组转换为int64
数组来修复这个问题。我们可以使用numpy.astype
函数来完成,如下面的代码所示:
了解numpy dtype
数组如何转换为 torch dtype
也非常重要。在前面的示例中,numpy int32
转换为IntTensor
。下表列出了 torch dtype
及其对应的numpy
类型:
NumPy 类型 | dtype | Torch 类型 | 描述 |
---|---|---|---|
int64 |
torch.int64 torch.float |
LongTensor |
64 位整数 |
int32 |
torch.int32 torch.int |
IntegerTensor |
32 位有符号整数 |
uint8 |
torch.uint8 |
ByteTensor |
8 位无符号整数 |
float64 double |
torch.float64 torch.double |
DoubleTensor |
64 位浮点数 |
float32 |
torch.float32 torch.float |
FloatTensor |
32 位浮点数 |
torch.int16 torch.short |
ShortTensor |
16 位有符号整数 | |
torch.int8 |
CharTensor |
6 位有符号整数 |
张量的默认dtype
是FloatTensor
;但是,我们可以使用张量的dtype
属性指定特定的数据类型。例如,请看下面的代码:
切片、索引和重塑
torch.Tensor
具有大部分与 NumPy 相同的属性和功能。例如,我们可以像 NumPy 数组一样对张量进行切片和索引:
在这里,我们打印了x
的第一个元素,写作x[0],在第二个示例中,我们打印了x
的第二个元素的一个切片;在这种情况下,x[11]和x[12]。
如果您还没有接触过切片和索引,可能需要再看一次。请注意,索引从0
开始,而不是1
,我们的下标符号保持与此一致。还请注意,切片[1][0:2]
是元素x[10]和x[11],包括x[12]。不包括切片结束索引2
。
我们可以使用view()
函数创建现有张量的重塑副本。以下是三个示例:
(3
,2
)和(6
,1
)很明显,但是第一个示例中的–1
是什么意思?如果您知道需要多少列,但不知道需要多少行,这非常有用。在这里使用–1
告诉 PyTorch 计算所需的行数。在没有其他维度的情况下使用它将创建一个单行张量。如果您不知道输入张量的形状,但知道它需要有三行,您可以像以下示例两样重新编写:
一个重要的操作是交换轴或转置。对于二维张量,我们可以使用 tensor.transpose()
,传递我们想要转置的轴。在这个例子中,原始的 2 x 3 张量变成了一个 3 x 2 张量。行变成了列:
在 PyTorch 中,transpose()
只能同时交换两个轴。我们可以在多个步骤中使用 transpose
;然而,一个更方便的方法是使用 permute()
,传递我们想要交换的轴。以下示例应该能够清楚地说明这一点:
当我们考虑二维张量时,我们可以将它们视为平面表格。当我们移动到更高维时,这种视觉表现变得不可能。我们简单地耗尽了空间维度。深度学习的魔力之一是,数学上所涉及的内容并不太重要。现实世界的特征被编码到数据结构的一个维度中。因此,我们可能处理数千维的张量。虽然这可能令人不安,但大多数可以在二维或三维中说明的想法在更高维中同样适用。
就地操作
理解就地操作和赋值操作之间的区别很重要。例如,当我们使用 transpose(x)
时,会返回一个值,但 x
的值不会改变。在目前的所有示例中,我们一直在执行赋值操作。也就是说,我们将一个变量赋值给操作的结果,或者仅仅将其打印到输出中,就像前面的例子中所示。在任一情况下,原始变量保持不变。或者,我们可能需要就地应用操作。当然,我们可以将一个变量分配给自身,例如 x = x.transpose(0,1)
;然而,更方便的方法是使用就地操作。一般来说,在 PyTorch 中,就地操作的函数名以下划线结尾。例如,查看以下代码示例:
作为另一个例子,这里是我们在本章开始时使用的线性函数,使用就地操作对 y
进行操作:
加载数据
大多数时间你会在深度学习项目上花费在处理数据上,一个深度学习项目失败的主要原因之一是因为糟糕或理解不足的数据。当我们使用众所周知且构建良好的数据集时,这个问题常常被忽视。这里的重点是学习模型。使深度学习模型工作的算法本身就足够复杂,不需要由于某些仅部分了解的东西(如不熟悉的数据集)而增加这种复杂性。现实世界的数据是嘈杂的、不完整的和容易出错的。这些混乱的轴意味着,如果一个深度学习算法在消除代码逻辑错误后仍未给出合理结果,那么糟糕的数据或对数据理解的错误很可能是问题的根源。
因此,暂时搁置我们与数据的斗争,并理解深度学习可以提供宝贵的现实世界见解,我们如何学习深度学习?我们的起点是尽可能消除我们能消除的变量。这可以通过使用众所周知且代表特定问题的数据来实现;例如分类问题。这使我们能够在深度学习任务中有一个起点,同时也有一个测试模型想法的标准。
最著名的数据集之一是手写数字的 MNIST 数据集,通常的任务是正确分类每个数字,从零到九。最好的模型的错误率约为 0.2%。我们可以对任何视觉分类任务应用这个表现良好的模型,并且会得到不同的结果。我们几乎不可能获得接近 0.2%的结果,原因在于数据的差异。理解如何调整深度学习模型以考虑数据中这些有时微妙的差异,是成功的深度学习从业者的关键技能之一。
考虑一个从彩色照片中进行面部识别的图像分类任务。任务仍然是分类,但数据类型和结构的差异决定了模型需要如何改变以考虑这一点。如何做到这一点是机器学习的核心。例如,如果我们处理彩色图像而不是黑白图像,我们将需要额外的两个输入通道。对于每个可能的类别,我们还需要输出通道。在手写分类任务中,我们需要 10 个输出通道,即每个数字一个通道。对于面部识别任务,我们会考虑为每个目标面孔(比如警方数据库中的罪犯)设置一个输出通道。
显然,数据类型和结构是重要考虑因素。图像数据在图像中的结构方式与音频信号或医疗设备输出的方式大不相同。如果我们试图通过声音来分类人名,或者通过症状来分类疾病会怎样?它们都是分类任务;然而,在每种具体情况下,代表每种情况的模型将大不相同。为了在每种情况下构建合适的模型,我们需要深入了解正在使用的数据。
本书不讨论每种数据类型、格式和结构的微妙和细微差别。我们能做的是为您提供有关 PyTorch 数据处理工具、技术和最佳实践的简要见解。深度学习数据集通常非常庞大,在内存中处理它们是一项重要考虑因素。我们需要能够转换数据、批量输出数据、洗牌数据,并在将数据馈送给模型之前执行许多其他操作。由于许多数据集太大,无法将整个数据集加载到内存中,因此我们需要能够执行所有这些操作。在处理数据时,PyTorch 采用对象方法,为每个特定的活动创建类对象。我们将在接下来的部分中更详细地讨论这一点。
PyTorch 数据集加载器
PyTorch 包括几个数据集的数据加载器,帮助您快速入门。torch.dataloader
是用于加载数据集的类。以下是包括的 torch 数据集及其简要描述:
MNIST | 手写数字 1-9。是 NIST 手写字符数据集的一个子集。包含 60,000 张训练图像和 10,000 张测试图像。 |
---|---|
Fashion-MNIST | 用于 MNIST 的一种替代数据集。包含时尚物品的图像;例如 T 恤、裤子、套头衫。 |
EMNIST | 基于 NIST 手写字符,包括字母和数字,并分为 47、26 和 10 类分类问题。 |
COCO | 超过 100,000 张分类为日常物体的图像;例如人、背包和自行车。每张图像可以有多个类别。 |
LSUN | 用于大规模场景图像分类;例如卧室、桥梁、教堂。 |
Imagenet-12 | 大规模视觉识别数据集,包含 120 万张图像和 1000 个类别。使用 ImageFolder 类实现,其中每个类别都在一个文件夹中。 |
CIFAR | 60,000 张低分辨率(32x32)彩色图像,分为 10 个相互排斥的类别;例如飞机、卡车和汽车。 |
STL10 | 类似于 CIFAR,但分辨率更高,并有更多未标记的图像。 |
SVHN | 从谷歌街景获得的 60 万张街道数字图像。用于识别现实世界中的数字。 |
PhotoTour | 学习本地图像描述符。由 126 个补丁组成的灰度图像,附带有描述符文本文件。用于模式识别。 |
这是一个典型的示例,展示了如何将其中一个数据集加载到 PyTorch 中:
CIFAR10
是一个torch.utils.dataset
对象。在这里,我们传递了四个参数。我们指定了一个相对于代码运行位置的根目录,一个布尔值train
,表示我们想要加载的是测试集还是训练集,一个布尔值,如果设置为True
,将检查数据集是否已经下载,如果没有则下载,以及一个可调用的 transform。在这种情况下,我们选择的 transform 是ToTensor()
。这是torchvision.transforms
中的一个内置类,使得该类返回一个 tensor。我们将在本章的后面更详细地讨论 transforms。
可以通过简单的索引查找来获取数据集的内容。我们还可以使用len
函数检查整个数据集的长度。我们也可以按顺序遍历数据集。以下代码演示了这一点:
显示图像
CIFAR10
数据集对象返回一个元组,包含一个图像对象和表示图像标签的数字。从图像数据的大小可以看出,每个样本是一个 3 x 32 x 32 的张量,代表图像中 322 个像素的三种颜色值。需要注意的是,这与matplotlib
使用的格式不完全相同。张量处理的图像格式是[颜色,高度,宽度]
,而numpy
图像的格式是[高度,宽度,颜色]
。要绘制图像,我们需要使用permute()
函数交换轴,或者将其转换为 NumPy 数组并使用transpose
函数。请注意,我们不需要将图像转换为 NumPy 数组,因为matplotlib
将正确显示调整后的张量。以下代码应该能够清楚地表明这一点:
DataLoader
在深度学习模型中,我们可能并不总是希望一次加载一个图像,或者每次以相同顺序加载它们。出于这个原因等等,使用torch.utils.data.DataLoader
对象通常更好。DataLoader
提供了一个多功能迭代器,可以按指定的方式对数据进行采样,例如按批次或随机顺序。它还是在多处理器环境中分配工作线程的便利位置。
在下面的示例中,我们以每批四个样本的方式对数据集进行采样:
这里 DataLoader
返回一个包含两个张量的元组。第一个张量包含批次中所有四个图像的图像数据。第二个张量是图像的标签。每个批次由四个图像标签对或样本组成。在迭代器上调用 next()
生成下一组四个样本。在机器学习术语中,对整个数据集的每次遍历称为一个 epoch。正如我们将看到的,这种技术被广泛用于训练和测试深度学习模型。
创建自定义数据集
Dataset
类是表示数据集的抽象类。它的目的是以一致的方式表示数据集的特定特征。当我们使用不熟悉的数据集时,创建一个 Dataset
对象是理解和表示数据结构的好方法。它与 data loader
类一起使用,以清晰和高效的方式从数据集中抽取样本。下图说明了这些类的使用方式:
我们使用 Dataset
类执行的常见操作包括检查数据的一致性,应用转换方法,将数据分为训练集和测试集,并加载单个样本。
在以下示例中,我们使用一个小型玩具数据集,其中包含被分类为玩具或非玩具的对象图像。这代表了一个简单的图像分类问题,其中模型在一组标记图像上进行训练。深度学习模型需要以一致的方式应用各种转换后的数据。样本可能需要分批抽取并对数据集进行洗牌。拥有表示这些数据任务的框架极大地简化和增强了深度学习模型。
完整的数据集可在 www.vision.caltech.edu/pmoreels/Datasets/Giuseppe_Toys_03/
上找到。
对于本示例,我创建了数据集的一个较小子集,以及一个 labels.csv
文件。这在本书的 GitHub 仓库中的 data/GiuseppeToys
文件夹中可以找到。表示这个数据集的类如下所示:
__init__
函数是我们初始化类的所有属性的地方。因为它仅在我们首次创建实例时调用一次来执行所有操作,所以我们执行所有的日常管理功能,例如读取 CSV 文件,设置变量并检查数据的一致性。我们只执行整个数据集都会发生的操作,因此我们不下载负载(在这个例子中是图像),但是我们确保数据集的关键信息,如目录路径、文件名和数据集标签被存储在变量中。
__len__
函数简单地允许我们在数据集上调用 Python 的内置len()
函数。在这里,我们只需返回标签元组列表的长度,指示数据集中的图像数量。我们希望保持其尽可能简单和可靠,因为我们依赖它正确迭代数据集。
__getitem__
函数是我们在Dataset
类定义中覆盖的内置 Python 函数。这使得Dataset
类具有 Python 序列类型的功能,例如索引和切片的使用。这个方法经常被调用——每当我们进行索引查找时都会调用一次,因此确保它只执行检索样本所需的操作。
要将此功能应用于我们自己的数据集中,我们需要创建我们自定义数据集的实例,如下所示:
转换
除了ToTensor()
转换之外,torchvision
包还包含一些专门用于 Python 图像库图像的转换。我们可以使用compose
函数将多个转换应用于数据集对象,如下所示:
Compose
对象本质上是可以作为单个变量传递给数据集的一系列转换。重要的是要注意图像转换只能应用于 PIL 图像数据,而不能应用于张量。由于在Compose
列表中按照列出的顺序应用转换,因此ToTensor
转换放置在 PIL 转换之前会生成错误。
最后,我们可以通过使用DataLoader
加载带有转换的图像批次来检查所有功能是否正常,就像之前做的那样:
ImageFolder
我们可以看到数据集对象的主要功能是从数据集中获取样本,而DataLoader
函数的功能是向深度学习模型提供样本或样本批次进行评估。在编写自己的数据集对象时,需要考虑的主要事项之一是如何从磁盘上组织的文件中的数据构建可访问内存中的数据结构。我们可能希望组织数据的常见方式是按类别命名的文件夹。假设在这个例子中,我们有三个名为toy
、notoy
和scenes
的文件夹,包含在名为images
的父文件夹中。每个文件夹表示其中包含的文件的标签。我们需要能够加载它们同时保持它们作为单独的标签。幸运的是,有一个专门用于此的类,并且像大多数 PyTorch 中的东西一样,它非常易于使用。这个类是torchvision.datasets.ImageFolder
,使用方法如下:
在data/GiuseppeToys/images
文件夹中,有三个文件夹,toys
、notoys
和scenes
,其中包含以其文件夹名称表示标签的图像。请注意,使用DataLoader
检索的标签由整数表示。由于本示例中有三个文件夹,表示三个标签,DataLoader
返回整数1
到3
,表示图像标签。
连接数据集
很明显,将需要连接数据集——我们可以使用torch.utils.data.ConcatDataset
类来实现这一点。ConcatDataset
接受一个数据集列表,并返回一个连接后的数据集。在以下示例中,我们添加了两个额外的转换,去除蓝色和绿色通道。然后,我们创建了两个应用了这些转换的数据集对象,并最终将这三个数据集连接成一个,如下所示的代码所示:
总结
在本章中,我们介绍了 PyTorch 的一些特性和操作。我们概述了安装平台和流程。您现在应该对张量操作有所了解,并了解如何在 PyTorch 中执行这些操作。您应该清楚地区分了就地操作和通过赋值操作,并且现在应该理解张量索引和切片的基础知识。在本章的后半部分,我们看了如何将数据加载到 PyTorch 中。我们讨论了数据的重要性,以及如何创建表示自定义数据集的dataset
对象。我们查看了 PyTorch 中的内置数据加载器,并讨论了如何使用ImageFolder
对象表示文件夹中的数据。最后,我们看了如何连接数据集。
在下一章中,我们将快速浏览深度学习基础知识及其在机器学习中的地位。我们将带您快速了解涉及的数学概念,包括查看线性系统及其常见解决技术。
第二章:深度学习基础
深度学习通常被认为是机器学习的一个子集,涉及人工神经网络(ANNs)的训练。人工神经网络处于机器学习的前沿。它们有能力解决涉及海量数据的复杂问题。机器学习的许多原则在深度学习中也同样重要,因此我们将在这里花一些时间进行回顾。
在本章中,我们将讨论以下主题:
-
机器学习方法
-
学习任务
-
特征
-
模型
-
人工神经网络
机器学习方法
在一般的机器学习出现之前,如果我们想要构建一个垃圾邮件过滤器,我们可以先编制一个常出现在垃圾邮件中的单词列表。然后垃圾邮件检测器会扫描每封电子邮件,当黑名单单词的数量达到阈值时,该电子邮件将被分类为垃圾邮件。这被称为基于规则的方法,如下图所示:
该方法的问题在于,一旦垃圾邮件的撰写者知道了规则,他们就能够制作避开这一过滤器的邮件。负责维护这个垃圾邮件过滤器的人们将不得不不断更新规则列表。通过机器学习,我们可以有效地自动化这个规则更新过程。我们不再需要撰写一长串规则,而是构建和训练一个模型。作为一个垃圾邮件检测器,它将更加准确,因为它可以分析大量数据。它能够检测到数据中的模式,这在有限的时间内对人类来说是不可能做到的。下图说明了这种方法:
有很多种方法可以用来处理机器学习问题,这些方法大致可以根据以下因素进行分类:
-
模型是否是用标记的训练数据进行训练。这里有几种可能性,包括完全监督、半监督、基于强化的或完全无监督的方法。
-
它们是在线的(即在提供新数据时即时学习),还是使用预先存在的数据进行学习。这被称为批量学习。
-
它们是基于实例的,只是简单地比较新数据与已知数据,还是基于模型的,涉及检测模式并构建预测模型。
这些方法并不是互斥的,大多数算法都是这些方法的组合。例如,构建一个垃圾邮件检测器的典型方式是使用在线的、基于模型的监督学习算法。
学习任务
有几种明显不同的学习任务类型,这些类型部分由它们处理的数据类型定义。基于这一点,我们可以将学习任务分为两大类:
-
无监督学习:数据没有标签,因此算法必须推断变量之间的关系或通过找到相似变量的群集来学习。
-
监督学习:使用标记数据集构建推断函数,该函数可用于预测未标记样本的标签
数据是否有标签对学习算法的构建方式有着预设的影响。
无监督学习
监督学习的一个主要缺点是需要准确标记的数据。大多数现实世界的数据是未标记和非结构化的,这是机器学习和人工智能更广泛努力的主要挑战。无监督学习在发现非结构化数据中的结构方面起重要作用。监督学习和无监督学习之间的区分并不是绝对的。许多无监督算法与监督学习一起使用;例如,在数据只有部分标记或者我们试图找出深度学习模型最重要特征时。
聚类
这是最直接的无监督方法之一。在许多情况下,数据是否有标签并不重要;我们关心的是数据聚集在某些点附近的事实。例如,推荐系统可以利用聚类技术推荐在线商店的电影或书籍。在这里,算法分析客户的购买历史,将其与其他客户进行比较,并根据相似性进行推荐。该算法将客户的使用模式聚类成群组。在任何时候,算法都不知道这些群组是什么;它能够自行解决这个问题。最常用的聚类算法之一是k-means。该算法通过建立基于观察样本均值的聚类中心来运作。
主成分分析
另一种无监督方法,通常与监督学习一起使用的是主成分分析(PCA)。当我们有大量可能相关的特征并且不确定每个特征对确定结果的影响时,可以使用此方法。例如,在天气预测中,我们可以将每个气象观测作为一个特征直接输入模型。这意味着模型需要分析大量数据,其中许多数据是无关的。此外,数据可能相关,因此我们不仅需要考虑单个特征,还需要考虑这些特征如何相互作用。我们需要的是一种工具,能够将这些可能相关和冗余的特征减少到少数几个主成分。PCA 属于一种称为降维的算法,因为它减少了输入数据集的维数。
强化学习
强化学习与其他方法有些不同,并且通常被归类为无监督方法,因为它使用的数据并非以监督方式标记。强化学习可能更接近人类与世界互动和学习的方式。在强化学习中,学习系统被称为一个代理,这个代理通过观察和执行动作与环境互动。每个动作会导致奖励或惩罚。代理必须制定策略或策略,以在时间内最大化奖励并最小化惩罚。强化学习在许多领域有应用,如博弈论和机器人技术,算法必须学习其环境,而无需直接人类提示。
监督学习
在监督学习中,机器学习模型在标记数据集上进行训练。到目前为止,大多数成功的深度学习模型都集中在监督学习任务上。通过监督学习,每个数据实例(如图像或电子邮件)都有两个元素:通常表示为大写X的一组特征,以及用小写y表示的标签。有时,标签也称为目标或答案。
监督学习通常分为两个阶段进行:训练阶段,模型在此阶段学习数据的特征;测试阶段,对未标记数据进行预测。重要的是,模型在不同的数据集上进行训练和测试,因为目标是对新数据进行泛化,而不是精确学习单一数据集的特征。这可能导致常见的过拟合训练集的问题,从而导致在测试数据集上欠拟合的问题。
分类
分类可能是最常见的监督学习任务。基于输入和输出标签的数量,分类问题有几种类型。分类模型的任务是在输入特征中找到模式,并将此模式与一个标签关联起来。模型应该学习数据的区分特征,然后能够预测未标记样本的标签。模型本质上是从训练数据中构建一个推断函数。我们马上会看到这个函数是如何构建的。我们可以区分三种类型的分类模型:
-
二分类:就像我们的玩具示例一样,这涉及区分两个标签。
-
多标签分类:涉及区分两个以上的类别。例如,如果将玩具示例扩展到区分图像中的玩具类型(汽车、卡车、飞机等)。解决多标签分类问题的常见方法是将问题分解为多个二进制问题。
-
Multiple output classification: 每个样本可能有多个输出标签。例如,也许任务是分析场景图像并确定其中的玩具类型。每个图像可以有多种类型的玩具,因此具有多个标签。
评估分类器
您可能认为衡量分类器性能的最佳方法是计算成功预测的比例与总预测量之比。然而,考虑一个关于手写数字数据集的分类任务,目标是所有不是 7 的数字。仅猜测每个样本不是 7 将给出一个成功率,假设数据均匀分布,为 90%。在评估分类器时,我们必须考虑四个变量:
-
TP true positive: 正确识别目标的预测
-
TN true negative: 正确识别非目标的预测
-
FP false positive: 错误识别目标的预测
-
FN false negative: 错误识别非目标的预测
精确率和召回率是常一起用来衡量分类器性能的两个指标。精确率由以下方程定义:
召回率由以下方程定义:
我们可以将这些想法结合在所谓的混淆矩阵中。它被称为混淆矩阵,并不是因为它难以理解,而是因为它总结了分类器混淆目标的情况。下面的图表应该能更清晰地说明这一点:
在确定分类器的成功与否时,我们使用哪种度量或赋予更多权重,实际上取决于应用程序。精确率和召回率之间存在权衡。提高精确率通常会导致召回率降低。例如,增加真阳性的数量通常意味着假阳性率增加。精确率和召回率的合理平衡取决于应用程序的要求。例如,在癌症医学检测中,我们可能需要更高的精确率,因为假阴性意味着癌症实例未被诊断出来,可能导致致命后果。
特征
重要的是要记住,图像检测模型不是看到图像,而是一组像素颜色值,或者在垃圾邮件过滤器中,是电子邮件中的字符集合。这些是模型的原始特征。机器学习的一个重要部分是特征转换。我们已经讨论过的一个特征转换是关于主成分分析的降维。以下是常见的特征转换列表:
-
降维以减少特征数量,使用主成分分析等技术
-
缩放或归一化特征使其处于特定的数值范围内
-
转换特征数据类型(例如,将类别分配给数字)
-
添加随机或生成的数据以增加特征
每个特征被编码到我们输入张量 X 的一个维度上,为了使学习模型尽可能高效,需要尽量减少特征数量。这就是主成分分析和其他降维技术发挥作用的地方。
另一个重要的特征转换是缩放。当特征的比例不同时,大多数机器学习模型表现不佳。用于特征缩放的两种常见技术是:
-
归一化或最小-最大缩放:值被移动和重新缩放到 0 到 1 之间。这是神经网络中最常用的缩放方法。
-
标准化:减去均值并除以方差。这并不将变量限制在特定范围内,但结果分布具有单位方差。
处理文本和类别
当一个特征是一组类别而不是一个数字时,我们该怎么办?假设我们正在构建一个预测房价的模型。该模型的一个特征可能是房屋的外包材料,可能的值包括木材、铁和水泥。我们如何编码这个特征以供深度学习模型使用?显而易见的解决方案是简单地为每个类别分配一个实数:比如木材为 1,铁为 2,水泥为 3。这种表示方法的问题在于它推断类别值是有序的。也就是说,木材和铁比木材和水泥更接近。
避免这种情况的一种解决方案是独热编码。特征值被编码为二进制向量,如下表所示:
木材 | 1 | 0 | 0 |
---|---|---|---|
铁 | 0 | 1 | 0 |
水泥 | 0 | 0 | 1 |
当类别值的数量较少时,这种解决方案效果很好。例如,如果数据是一个文本语料库,我们的任务是自然语言处理,使用独热编码是不实际的。类别值的数量,因此特征向量的长度,是词汇表中单词的数量。在这种情况下,特征向量变得庞大且难以管理。
独热编码使用称为稀疏表示的方法。大多数值为 0。除了不太适合缩放外,独热编码在自然语言处理中还有另一个严重的缺点。它不编码单词的含义或其与其他单词的关系。我们可以使用的一种方法称为密集词嵌入。词汇表中的每个单词由一个实数向量表示,表示特定属性的分数。总体思想是,这个向量编码与当前任务相关的语义信息。例如,如果任务是分析电影评论并根据评论确定电影的类型,我们可以创建如下表所示的词嵌入:
词语 | 戏剧 | 喜剧 | 纪录片 |
---|---|---|---|
有趣 | -4 | 4.5 | 0 |
操作 | 3.5 | 2.5 | 2 |
悬疑 | 4.5 | 1.5 | 3 |
这里,最左边的列列出可能出现在电影评论中的单词。每个单词根据其在相应类型的评论中出现的频率得分。我们可以从分析带标签的电影评论的监督学习任务中构建这样一个表格。然后,可以将训练过的模型应用于非标记的评论,以确定最可能的类型。
模型
在机器学习中,选择模型表示是一项重要任务。到目前为止,我们一直把模型称为黑盒子。一些数据被输入,基于训练,模型进行预测。在我们打开这个黑盒子之前,让我们回顾一些我们需要理解深度学习模型所需的线性代数知识。
线性代数复习
线性代数涉及通过使用矩阵来表示线性方程。在高中教的代数中,我们关注的是标量,也就是单个数字值。我们有方程式,以及操作这些方程式的规则,以便它们可以被评估。当我们使用矩阵而不是标量值时,情况也是如此。让我们回顾一些涉及的概念。
矩阵只是一个数字的矩形数组。我们看到,通过简单地添加每个对应的元素来添加两个矩阵。可以通过简单地将数组中的每个元素乘以标量来将矩阵乘以标量,如下例所示:
这是矩阵加法的一个例子,正如您预期的那样,您可以以相同的方式执行矩阵减法,除了当然,不是添加对应的元素,而是减去它们。请注意,我们只能添加或减去相同大小的矩阵。
另一个常见的矩阵操作是乘以一个标量:
注意我们使用的索引风格:X[ij],其中i表示行,j表示列。在索引方面有两种惯例。这里,我使用的是零索引;即索引从零开始。这是为了与我们在 PyTorch 中索引张量的方式保持一致。请注意,在一些数学文本中,以及取决于您使用的编程语言,索引可能从 1 开始。此外,我们将矩阵的大小或维度称为m乘以n,其中m是行数,n是列数。例如,A和B都是 3 x 2 矩阵。
有一个矩阵的特殊情况称为向量。这只是一个n乘以 1 的矩阵,所以它有一列和任意数量的行,如下例所示:
现在让我们看看如何将一个向量与一个矩阵相乘。在下面的例子中,我们将一个 3 x 2 矩阵与一个大小为 2 的向量相乘:
一个具体的例子可能会使这一点更清楚:
请注意,这里,3 x 2 矩阵的结果是一个 3 向量,在一般情况下,乘以向量的m行矩阵将导致一个m大小的向量。
我们还可以通过结合矩阵向量乘法来将矩阵与其他矩阵相乘,如以下示例所示:
这里:
另一种理解方式是,我们通过将矩阵A与矩阵B的第一列组成的向量相乘,得到矩阵C的第一列。我们通过将矩阵A与从矩阵B的第二列获得的向量相乘,得到矩阵C的第二列。
让我们看一个具体的例子:
重要的是要理解,只有在矩阵A的行数等于矩阵B的列数时,我们才能相乘两个矩阵。结果矩阵的行数始终与矩阵A相同,并且列数与矩阵B相同。请注意,矩阵乘法不是交换的,如下所示:
然而,矩阵乘法是结合的,如下例所示:
矩阵非常有用,因为我们可以用相对简单的方程表示大量操作。对于机器学习来说,有两个矩阵运算特别重要:
-
转置
-
逆矩阵
要转置矩阵,我们只需交换列和行,如以下示例所示:
寻找矩阵的逆矩阵稍微复杂一些。在实数集中,数字 1 扮演单位的角色。也就是说,数字 1 乘以任何其他等于那个数字的数字。几乎每个数字都有一个逆元;也就是说,一个乘以自己等于 1 的数字。例如,2 的逆元是 0.5,因为 2 乘以 0.5 等于 1。矩阵和张量也有类似的概念。单位矩阵沿其主对角线为 1,其他位置为 0,如以下 3 x 3 示例所示:
当我们乘以一个逆矩阵时,单位矩阵是其结果。我们用以下方式表示:
重要的是,我们只能找到方阵的逆矩阵。不要期望手工计算逆矩阵,或者说实际上,任何矩阵操作。这是计算机擅长的。矩阵求逆是一个非平凡的操作,即使对于计算机来说,也是计算密集型的。
线性模型
在机器学习中,我们将遇到的最简单的模型是线性模型。解决线性模型在许多不同的设置中都很重要,并且它们是许多非线性技术的基础。使用线性模型,我们试图将训练数据拟合成一个线性函数,有时称为假设函数。这是通过线性回归的过程完成的。
单变量线性回归的假设函数具有以下形式:
在这里,θ[0] 和 θ[1] 是模型的参数,x 是单个独立变量。对于我们的房价示例,x 可以代表房屋的地板面积大小,h(x) 可以代表预测的房价。
为简单起见,我们将首先看一下只有单变量或单特征的情况。
在下图中,我们展示了许多点,代表着训练数据,并尝试将一条直线拟合到这些点上:
这里,x 是单个特征,θ*[0] 和 θ*[1] 分别表示假设函数的截距和斜率。我们的目标是找到θ*[0] 和 θ*[1] 这两个模型参数的数值,这将使我们在上图中得到最佳拟合直线。在这张图中,θ*[0] 被设为 1,θ*[1] 被设为 0.5。因此,直线的截距是 1,斜率为 0.5。我们可以看到大多数训练点位于直线上方,只有少数数值较低的点位于直线下方。我们可以猜测 θ*[1] 可能略低,因为训练点似乎具有稍陡的斜率。而且 θ*[0] 太高,因为左侧有两个数据点位于直线下方,截距似乎略低于 1。
很明显,我们需要一种正式的方法来找出假设函数中的误差。这是通过所谓的代价函数完成的。代价函数衡量假设函数给出的值与数据实际值之间的总误差。本质上,损失函数对每个点的假设值与实际值之间的距离进行求和。代价函数有时也被称为均方误差(MSE)。其表达式如下:
在这里,hθ 是假设函数对第i个训练样本计算出的值,y^i 是其实际值。差值被平方是为了统计上的便利性,因为它确保结果始终为正。平方还会增加较大差异的权重;也就是说,它对离群值更加重视。将这些平方误差之和除以训练样本数m,可以计算得到均值。此外,将和除以 2 可以使后续的数学运算更为简单。
最后一部分是调整参数值,使得假设函数尽可能地拟合训练数据。我们需要找到能够最小化误差的参数值。
我们可以通过两种方式实现这一点:
-
使用梯度下降迭代训练集并调整参数以最小化成本函数。
-
直接计算模型参数,使用闭合形式方程式
梯度下降
梯度下降是一种通用的优化算法,具有广泛的应用。梯度下降通过迭代调整模型参数来最小化成本函数。梯度下降通过对成本函数进行偏导数来实现。如果我们将成本函数绘制为参数值的函数,它形成一个凸函数,如下图所示:
当我们在图表中从右向左变化 θ 时,成本 J[θ] 减少至最小值,然后上升。目标是在梯度下降的每次迭代中,成本更接近最小值,一旦达到最小值就停止。这是通过以下更新规则实现的:
在这里,α 是学习率,一个可设置的超参数。它被称为超参数,以区别于模型参数 theta。偏导数项是成本函数的斜率,需要分别计算 theta 0 和 theta 1 的值。当导数和因此斜率为正数时,会从 theta 的先前值中减去正值,在图表中从右向左移动。相反,如果斜率为负数,则 theta 增加,从右向左移动。此外,在最小值处,斜率为零,因此梯度下降会停止。这正是我们想要的,因为无论我们从何处开始梯度下降,更新规则都确保将 theta 向最小值移动。
将成本函数代入上述方程,然后分别对 theta 的两个值进行导数运算,结果得到以下两个更新规则,即以下两个方程:
在迭代和后续更新中,θ将收敛到能够最小化成本函数的值,从而得到最佳拟合的直线。有两件事需要考虑。首先是θ的初始化值;也就是说,我们从哪里开始梯度下降。在大多数情况下,随机初始化效果最好。另一件需要考虑的事情是设置学习率α。这是一个介于零和一之间的数字。如果学习率设置得太高,它可能会超过最小值。如果设置得太低,它将需要较长时间收敛。可能需要对使用的特定模型进行一些实验;在深度学习中,通常会使用自适应学习率以获得最佳结果。这是指在梯度下降的每次迭代中学习率通常会变小。
到目前为止,我们讨论的梯度下降类型称为批量梯度下降(BGD)。这是因为每次更新时都使用整个训练集。这意味着随着训练集的增大,批量梯度下降变得越来越慢。另一方面,当特征数量很大时,批量梯度下降的性能更好,因此在训练集较小但特征较多时通常会使用它。
一种替代批量梯度下降的方法是随机梯度下降(SGD)。与使用整个训练集计算梯度不同,SGD 在每次迭代中随机选择一个样本计算梯度。SGD 的优势在于不需要整个训练集一次性存储在内存中,因为每次迭代只处理一个实例。因为随机梯度下降是随机选择样本,其行为比批量梯度下降稍显不规则。使用批量梯度下降时,每次迭代都会平滑地将误差(J[θ])向最小值移动。而使用 SGD 时,并不是每次迭代都能使成本函数更接近最小值。它往往会有些许跳跃,平均而言才会向最小值移动。这意味着它可能会在最小值附近跳动,但在完成迭代时可能并未真正到达最小值。SGD 的随机性在存在多个最小值时可以派上用场,因为它可能会跳出局部最小值找到全局最小值。例如,请考虑以下成本函数:
如果批量梯度下降从局部最小值的右侧开始,它将无法找到全局最小值。幸运的是,线性回归的成本函数始终是一个具有单一最小值的凸函数。然而,并非总是如此,特别是在神经网络中,成本函数可能具有多个局部最小值。
多个特征
在一个实际的例子中,我们会有多个特征,每个特征都有一个关联的参数值需要拟合。我们将多个特征的假设函数写成如下形式:
这里,x[0]称为偏置变量并设为 1,x[1]到x[n]是特征值,n是特征的总数。请注意,我们可以编写假设函数的向量化版本。在这里,θ是参数向量,x是特征向量。
成本函数与单特征情况基本相同;我们只是在求和误差时需要调整梯度下降规则,并明确所需的符号表示。在单特征梯度下降的更新规则中,我们使用了参数值θ[0]和θ[1]的符号表示。对于多特征版本,我们简单地将这些参数的值及其关联的特征包装成向量。参数向量表示为θ[j],其中下标j表示特征,是介于 1 到n之间的整数,n是特征的数量。
每个参数都需要有单独的更新规则。我们可以将这些规则概括如下:
每个参数都有一个更新规则;因此,例如特征j = 1的参数的更新规则如下:
变量x((i))*和*y((i)),如单特征示例中所述,分别指预测值和第i个训练样本的实际值。然而,在多特征情况下,它们不再是单个值,而是向量。值x[j]^((i))指训练样本i的特征j,m是训练集中样本的总数。
正规方程
对于某些线性回归问题,闭式解,也称为正规方程,是寻找θ的最优值的更好方法。如果你了解微积分,那么为了最小化代价函数,你可以找到代价函数的偏导数,对每个θ的值设为零,然后求解每个θ的值。如果你对微积分不熟悉,不用担心;事实证明,我们可以从这些偏导数中推导出正规方程,结果如下方程所示:
你可能会想知道为什么我们需要费心使用梯度下降及其带来的额外复杂性,因为正规方程允许我们在一步中计算参数。原因是求逆矩阵所需的计算工作并不可忽视。当特征矩阵X变得很大时(请记住X是一个包含每个训练样本特征值的矩阵),找到这个矩阵的逆仅需太长时间。尽管梯度下降涉及多次迭代,但对于大数据集来说,仍然比正规方程快。
正规方程的一个优势是,与梯度下降不同,它不要求特征具有相同的尺度。正规方程的另一个优点是不需要选择学习率。
逻辑回归
我们可以使用线性回归模型通过找到分割两个预测类别的决策边界来进行二元分类。一个常见的方法是使用定义如下的sigmoid
函数:
sigmoid
函数的图像如下所示:
sigmoid
函数可以用于假设函数中,以输出概率,如下所示:
在这里,假设函数的输出是在参数化为θ的条件下,y = 1 的概率。为了决定何时预测y = 0或y = 1,我们可以使用以下两条规则:
sigmoid
函数的特性(即在z = 0时有渐近线为0和1,并且在z = 0时的值为0.5),在逻辑回归问题中具有一些吸引人的特性。请注意,决策边界是模型参数的属性,而不是训练集的属性。我们仍然需要调整参数以使成本或误差最小化。为此,我们需要形式化我们已经知道的内容。
我们有一个包含m个样本的训练集,如下所示:
每个训练样本由大小为n的特征向量x组成,其中n是特征数量:
每个训练样本还包括一个值y,对于逻辑回归来说,这个值要么是零,要么是一。我们还有一个逻辑回归的假设函数,可以重写为以下方程:
使用逻辑回归的假设函数,采用与线性回归相同的成本函数,我们通过sigmoid
函数引入了非线性。这意味着成本函数不再是凸函数,因此可能存在多个局部最小值,这对梯度下降来说可能是个问题。事实证明,对于逻辑回归而言,一个良好的函数并且导致凸成本函数的函数如下所示:
我们可以为这两种情况绘制这些函数图。
从先前的图表中可以看出,当标签y等于1而假设值预测为0时,成本趋向无穷大。同样,当y的实际值为0而假设值预测为1时,成本也上升至无穷大。或者,当假设值预测正确的值,即0或1时,成本降至0。这正是逻辑回归所期望的。
现在我们需要应用梯度下降来最小化成本。我们可以将二元分类的逻辑回归成本函数重新写成更简洁的形式,通过以下方程对多个训练样本求和:
最后,我们可以使用以下更新规则更新参数值:
表面上看,这与线性回归的更新规则看起来一样;然而,假设是sigmoid
函数的函数,因此其行为实际上有很大不同。
非线性模型
我们已经看到,仅靠线性模型无法表示非线性的现实世界数据。一个可能的解决方案是向假设函数添加多项式特征。例如,一个立方模型可以用以下方程表示:
在这里,我们需要选择两个派生特征添加到我们的模型中。这些添加的项可以简单地是房屋示例中尺寸特征的平方和立方。
在添加多项式项时,一个重要的考虑因素是特征缩放。该模型中的平方和立方项的尺度会有很大的不同。为了使梯度下降正确工作,必须对这些添加的多项式项进行缩放。
选择多项式项是向模型注入知识的一种方式。例如,仅仅知道房价在相对于房屋面积增大时趋于平稳,就建议添加平方和立方项,以便使数据呈现我们期望的形状。然而,在特征选择中,比如在逻辑回归中,当我们试图预测复杂的多维决策边界时,可能意味着成千上万个多项式项。在这种情况下,线性回归的机制将停滞不前。我们将看到,神经网络为复杂的非线性问题提供了更自动化和强大的解决方案。
人工神经网络
如其名,人工神经网络受其生物对应物的启发,尽管原因可能被误解。一个人工神经元,或者我们将其称为一个单元,在功能和结构上与生物神经元相比都被极大简化了。生物灵感更多地来自这样的洞察力,即大脑中的每个神经元无论是处理声音、视觉还是思考复杂的数学问题,都执行相同的功能。这种单一算法方法基本上是 ANN 的灵感来源。
一个人工神经元,一个单元,执行一个简单的功能。它将其输入相加,并根据激活函数给出输出。人工神经网络的主要好处之一是它们具有高度可扩展性。由于它们由基本单元组成,只需在正确的配置中添加更多单元即可使人工神经网络轻松扩展到大规模、复杂的数据。
人工神经网络(ANNs)的理论已经存在了很长时间,最早是在 1940 年代初提出的。然而,直到最近它们才能够胜过更传统的机器学习技术。这主要有三个原因:
-
算法的改进,特别是实现反向传播(backpropagation),使得 ANN 能够将输出的误差分布到输入层,并相应地调整激活权重
-
大规模数据集的可用性,用于训练人工神经网络
-
处理能力的增加,允许大规模的人工神经网络
感知器
最简单的 ANN 模型之一是感知器,由单个逻辑单元组成。我们可以用以下图表现感知器:
每个输入都与一个权重相关联,并且这些输入被馈送到逻辑单元中。请注意,我们添加了一个偏置特征,x[0] = 1。这个逻辑单元包括两个元素:一个用于求和输入的函数和一个激活函数。如果我们使用sigmoid
作为激活函数,那么我们可以写出以下方程:
请注意,这恰好是我们用于逻辑回归的假设;我们只是将θ换成了w,用来表示逻辑单元中的权重。这些权重与逻辑回归模型的参数完全相同。
要创建一个神经网络,我们将这些逻辑单元连接成层。以下图表示了一个三层神经网络。为了清晰起见,我们省略了偏置单元:
这个简单的人工神经网络包括一个具有三个单元的输入层;一个也有三个单元的隐藏层;最后是一个单一的输出单元。我们使用符号 ai(j) 表示层 j 中单元 i 的激活,并使用 W ^((j)) 表示将层 j 映射到层 j+1 的权重矩阵。利用这个符号表示法,我们可以用以下方程表示三个隐藏单元的激活:
输出单元的激活可以用以下方程表示:
这里,W^((1) ) 是一个 3 x 4 的矩阵,控制输入层、第一层和单个隐藏层之间的函数映射。大小为 1 x 4 的权重矩阵 W^(²) 控制隐藏层与输出层 H 之间的映射。更一般地,一个具有 s[j] 层单元和 s[k] 层单元的网络将具有 s[k] x (s[j]+1) 的大小。例如,对于一个具有五个输入单元和三个在下一个前向层(第二层)的单元的网络,相关的权重矩阵 W^((1)) 将是 3 x 6 的大小。
建立了假设函数后,下一步是制定成本函数来衡量并最终最小化模型的误差。对于分类问题,成本函数几乎与逻辑回归中使用的函数相同。主要的区别在于,使用神经网络时,我们可以添加输出单元以支持多类分类。我们可以将多输出的成本函数写成如下形式:
这里,K 是表示输出类别数量的输出单元数。
最后,我们需要使用反向传播算法来最小化成本函数。基本上,这个算法将误差(成本函数的梯度)从输出单元反向传播到输入单元。为了做到这一点,我们需要计算偏导数。也就是说,我们需要计算以下内容:
这里,l 是层,j 是单元,i 是样本。换句话说,对于每一层中的每一个单元,对于每一个样本,我们需要计算成本函数关于每个参数的偏导数,即梯度。例如,考虑一个具有四层的网络。同时考虑我们正在处理一个单样本。我们需要找到每一层的误差,从输出开始。输出层的误差就是假设的误差:
这是每个单元 j 的误差向量。上标 (4) 表示这是第四层,即输出层。通过一些我们这里不需要担心的复杂数学,可以用以下方程计算两个隐藏层的误差:
在这里 .*
操作符是逐元素向量乘法。请注意,每个方程中都需要下一个前向层的误差向量。也就是说,要计算第三层的误差,需要输出层的误差向量。同样地,要计算第二层的误差,需要第三层的误差向量。
这是单个样本下反向传播的工作方式。要循环遍历整个数据集,我们需要累积每个单元和每个样本的梯度。因此,对于训练集中的每个样本,神经网络执行前向传播以计算隐藏层和输出层的激活。然后,在同一个样本内,也就是在同一个循环内,可以计算输出误差。因此,我们能够依次计算每个前一层的误差,并且神经网络确实这样做,将每个梯度累积在一个矩阵中。循环重新开始,在下一个样本上执行相同的操作,并将这些梯度也累积在误差矩阵中。我们可以写出如下的更新规则:
大写希腊字母 delta 是一个矩阵,它通过将层 l、单元 j 和样本 i 的激活累加起来,然后乘以下一个前向层对于同一样本 i 的相关误差,来存储累积梯度。最后,一旦我们完成整个训练集的一次遍历——一个 epoch——我们可以计算每个参数相对于成本函数的导数:
再次强调,不需要了解这个的正式证明;这只是为了让你对反向传播的机制有一些直观的理解。
总结
在本章中,我们涵盖了大量的内容。如果你对这里呈现的数学内容不太理解,不要担心。我们的目标是让你对一些常见的机器学习算法如何工作有一些直观的认识,而不是完全理解这些算法背后的理论。阅读完本章后,你应该对以下内容有一些了解:
-
机器学习的一般方法,包括了解监督和无监督方法的区别,在线学习和批量学习,以及基于规则与基于模型的学习
-
一些无监督方法及其应用,如聚类和主成分分析
-
分类问题的类型,如二分类、多类分类和多输出分类
-
特征和特征转换
-
线性回归和梯度下降的机制
-
神经网络及反向传播算法的概述
在第三章,计算图与线性模型,我们将运用 PyTorch 应用这些概念。具体来说,我们将展示如何通过构建一个简单的线性模型来找到函数的梯度。您还将通过实现一个简单的神经网络来获得反向传播的实际理解。
第三章:计算图和线性模型
到目前为止,您应该对线性模型和神经网络的理论有所了解,并且对 PyTorch 的基本知识也有所了解。在本章中,我们将通过在 PyTorch 中实现一些 ANN 来整合这些内容。我们将专注于线性模型的实现,并展示如何调整它们以执行多类分类。我们将讨论以下与 PyTorch 相关的主题:
-
autograd
-
计算图
-
线性回归
-
逻辑回归
-
多类分类
autograd
正如我们在上一章看到的,ANN 的许多计算工作涉及计算导数以找到成本函数的梯度。 PyTorch 使用autograd
包对 PyTorch 张量上的操作进行自动微分。为了了解其工作原理,让我们看一个例子:
在上述代码中,我们创建了一个 2 x 3 的 torch 张量,并且重要的是将requires_grad
属性设置为True
。这使得能够计算随后操作的梯度。还要注意我们将dtype
设置为torch.float
,因为这是 PyTorch 用于自动微分的数据类型。我们执行一系列操作,然后取结果的平均值。这返回一个包含单个标量的张量。这通常是autograd
计算前述操作的梯度所需的内容。这些可以是任何一系列操作;重要的是所有这些操作都被记录下来。输入张量a
正在跟踪这些操作,即使有两个中间变量也是如此。为了了解这是如何工作的,让我们写出在与输入张量a
相关的前述代码中执行的操作序列:
在这里,求和并除以六表示对张量a
的六个元素进行平均。对于每个元素ai,分配给张量b
的操作,加二和c
,平方并乘二,这些操作被求和并除以六。
对out张量调用backward()
将计算前一操作的导数。这个导数可以写成以下形式,如果您了解一点微积分,您将很容易确认这一点:
当我们将a的值代入前述方程的右侧时,确实可以得到在前面代码中打印出的a.grad
张量中包含的值。
有时需要在具有requires_grad=True
的张量上执行不需要被跟踪的操作。为了节省内存和计算量,可以将这样的操作包装在with torch.no_grad():
块中。例如,请观察以下代码:
要停止 PyTorch 对张量进行操作的跟踪,可以使用 .detach()
方法。这可以防止未来对操作的跟踪,并将张量从跟踪历史中分离出来:
注意,如果我们尝试第二次计算梯度,例如调用 out.backward()
,将再次生成错误。如果我们确实需要第二次计算梯度,我们需要保留计算图。这通过将 retain_graph
参数设置为 True
来实现。例如,观察以下代码:
注意,第二次调用 backward
会将梯度添加到已经存储在 a.grad
变量中的梯度上。请注意,一旦调用 backward()
而没有将 retain_graph
参数设置为 True
,grad
缓冲区将被释放。
计算图
要更好地理解这一点,让我们看看什么是计算图。我们可以为我们迄今使用的函数绘制如下的图形:
这里,图的叶子表示每一层的输入和参数,输出表示损失。
通常情况下,除非将 retain_graph
设置为 True
,在每个 epoch 的每次迭代中,PyTorch 将创建一个新的计算图。
线性模型
线性模型是理解人工神经网络机制的重要途径。线性回归用于预测连续变量,同时在逻辑回归的情况下用于分类预测类别。神经网络在多类分类中非常有用,因为它们的架构可以自然地适应多个输入和输出。
PyTorch 中的线性回归
让我们看看 PyTorch 如何实现一个简单的线性网络。我们可以使用 autograd
和 backward
手动迭代通过梯度下降。这种不必要的低级方法给我们带来了大量代码,将会很难维护、理解和升级。幸运的是,PyTorch 有一种非常直观的对象方法来构建人工神经网络,使用类来表示模型。我们自定义的模型类继承了构建人工神经网络所需的所有基础机制,使用超类 torch.nn.Module
。以下代码演示了在 PyTorch 中实现模块(在本例中为 linearModel
)的标准方式:
nn.Module
是基类,并且在初始化时通过 super
函数调用。这确保它继承了 nn.Module
中封装的所有功能。我们将一个变量 self.Linear
设置为 nn.Linear
类,这反映了我们正在构建一个线性模型的事实。记住,具有一个独立变量(即一个特征)x
的线性函数可以写成以下形式:
nn.linear
类包含两个可学习变量:bias
和weight
。在我们的单特征模型中,这些是两个参数w[0]和w[1]。当我们训练模型时,这些变量会更新,理想情况下,它们的值接近于最佳拟合线。最后,在前述代码中,我们通过创建变量model
并将其设置为我们的LinearModel
类来实例化模型。
在运行模型之前,我们需要设置学习率、优化器类型以及用于衡量损失的标准。使用以下代码完成此操作:
正如您所见,我们将学习率设为0.01
。这通常是一个很好的起点;如果设置得太高,优化器可能会超过最优点,如果设置得太低,找到最优点可能会花费太长时间。我们将优化器设为随机梯度下降,并传入需要优化的项(在本例中是模型参数),同时设置每个梯度下降步骤要使用的学习率。最后,我们设置损失标准;即用于衡量损失的梯度下降准则,这里我们将其设置为均方误差。
要测试这个线性模型,我们需要向其提供一些数据,为了测试目的,我们创建了一个简单的数据集,x
,由数字1
到10
组成。我们通过在输入值上应用线性变换来创建输出或目标数据。在这里,我们使用线性函数,y = 3*x + 5
。代码如下所示:
请注意,我们需要调整这些张量的形状,以使输入x
和目标y
具有相同的形状。还要注意,我们不需要设置autograd
,因为这一切都由模型类处理。但是,我们需要告诉 PyTorch 输入张量的数据类型为torch.float
,因为默认情况下它会将列表视为整数。
现在我们准备运行线性模型,并且我们通过循环运行它来完成。训练循环包括以下三个步骤:
-
对训练集进行前向传播
-
向后传播以计算损失
-
根据损失函数的梯度更新参数
使用以下代码完成此操作:
我们将epoch
设为1000.
。请记住,每个epoch
代表对训练集的完整遍历。模型的输入被设置为数据集的x
值;在这种情况下,它只是从 1 到 10 的数字序列。我们将标签设置为y
值;在这种情况下,是我们函数计算出的值,2*x + 5
。
重要的是,我们需要清除梯度,以防它们在周期内累积并扭曲模型。这可以通过在每个周期上调用优化器的 zero_grad()
函数来实现。输出张量设置为线性模型的输出,调用 LinearModel
类的前向函数。该模型应用当前参数估计的线性函数,并给出预测输出。
一旦有了输出,我们可以使用均方误差计算损失,比较实际的 y
值与模型计算的值。接下来,可以通过在损失函数上调用 backwards()
方法计算梯度。这确定梯度下降的下一步,使得 step()
函数更新参数值。我们还创建了一个 predicted
变量,用于存储 x
的预测值。稍后在绘制预测值和实际值的图表时将使用它。
为了了解我们的模型是否有效,我们在每个周期打印损失值。注意每次损失都在减少,表明模型按预期运行。确实,当模型完成 1000
个周期时,损失非常小。我们可以通过运行以下代码来打印模型的状态(即参数值):
这里,linear.weight
张量由数值 3.0113
的单个元素组成,而 linear.bias
张量包含值 4.9210
。这与我们通过 y=3x + 5
函数创建线性数据集时使用的 w[0] (5) 和 w[1] (3) 值非常接近。
为了增加一些趣味性,让我们看看当不使用线性函数创建标签,而是将函数中添加一个平方项(例如 y= 3x² + 5
)时会发生什么。我们可以通过将预测值与实际值绘制图表来可视化模型的结果。以下代码展示了结果:
我们使用了 y = 3x2 + 5
函数生成标签。平方项赋予训练集特征曲线,而线性模型的预测则是最佳拟合直线。可以看到,经过 1,000 个周期后,该模型在拟合曲线方面表现相当不错。
保存模型
构建和训练模型后,通常希望保存模型的状态。在像这样训练所需时间微不足道的情况下,这并不重要。然而,对于大型数据集和许多参数,训练可能需要数小时甚至数天才能完成。显然,我们不希望每次需要模型对新数据进行预测时都重新训练模型。为了保存已训练模型的参数,我们只需运行以下代码:
上述代码使用 Python 内置的对象序列化模块 pickle
保存模型。当需要恢复模型时,可以执行以下操作:
注意,为了使这个工作正常,我们需要在内存中保留我们的LinearModel
类,因为我们只保存模型的状态,即模型参数,而不是整个模型。一旦我们恢复了模型,要重新训练模型,我们需要重新加载数据并设置模型的超参数(在本例中是优化器、学习率和标准)。
逻辑回归
一个简单的逻辑回归模型与线性回归的模型看起来并没有太大的区别。以下是逻辑模型的典型类定义:
注意,当我们初始化model
类时,我们仍然使用线性函数。然而,对于逻辑回归,我们需要一个激活函数。在这里,这是在调用forward
时应用的。像往常一样,我们将模型实例化为我们的model
变量。
接下来,我们设置标准和优化器:
我们仍然使用随机梯度下降;然而,我们需要改变损失函数的标准。
对于线性回归,我们使用MSELoss
函数计算均方误差。对于逻辑回归,我们使用介于 0 和 1 之间的概率表示的概率。计算概率的均方误差没有太多意义;相反,常见的技术是使用交叉熵损失或对数损失。在这里,我们使用BCELoss
函数,或者二元交叉熵损失。这背后的理论有点复杂。重要的是理解它本质上是一个对数函数,更好地捕捉了概率的概念。因为它是对数函数,所以当预测概率接近 1 时,对数损失会缓慢减小,给出一个正确的预测。记住,我们试图计算一个错误预测的惩罚。损失必须随着预测值偏离真实值而增加。交叉熵损失对高置信度的预测进行惩罚(即它们接近 1 且是错误的),反之,奖励置信度较低但是正确的预测。
我们可以使用与线性回归相同的代码训练模型,在一个for
循环中运行每个 epoch,进行前向传播计算输出,后向传播计算损失梯度,最后更新参数。
让我们通过创建一个实际示例来更加具体化。假设我们试图通过某种数值度量,比如昆虫翅膀的长度,对昆虫的物种进行分类。我们有以下的训练数据:
这里,x_train
值可以表示翅膀长度(毫米),而y_train
值则表示每个样本的标签;其中一个值表示样本属于目标物种。一旦我们实例化了LogisticModel
类,就可以使用标准的运行代码来运行它。
一旦我们训练好模型,就可以使用一些新数据进行测试:
PyTorch 中的激活函数
使人工神经网络表现出色的一部分诀窍是使用非线性激活函数。最初的想法是简单地使用阶跃函数。在这种情况下,只有当输入超过零时,才会从特定的输出发生。阶跃函数的问题在于它不能被微分,因为它没有定义的梯度。它只由平坦部分组成,并且在零点处不连续。
另一种方法是使用线性激活函数;然而,这将限制我们的输出也成为线性函数。这不是我们想要的,因为我们需要建模高度非线性的现实世界数据。事实证明,我们可以通过使用非线性激活函数将非线性引入我们的网络中。以下是流行激活函数的绘图:
ReLU
,或修正线性单元,通常被认为是最流行的激活函数。尽管在零点不可微,它具有一个特征的肘部,可以使梯度下降跳跃,实际上它的表现非常好。ReLU
函数的一个优点是它计算非常快。此外,它没有最大值;随着其输入的增加,它会继续上升到无穷大。这在某些情况下可能是有利的。
我们已经遇到了sigmoid
函数;它的主要优势在于在所有输入值上都是可微的。这可以帮助在ReLU
函数在梯度下降过程中引起异常行为的情况下使用。与ReLU
不同,sigmoid
函数受到渐近线的约束。这对某些人工神经网络也是有益的。
softmax
函数通常用于多类别分类的输出层。请记住,与多标签分类相比,多类别分类只有一个真正的输出。在这种情况下,我们需要预测的目标尽可能接近 1,而所有其他输出尽可能接近零。softmax
函数是非线性的归一化形式。我们需要对输出进行归一化,以确保我们正在逼近输入数据的概率分布。与简单地将所有输出除以它们的和进行线性归一化不同,softmax
应用了一个非线性的指数函数,增加了离群数据点的影响。这倾向于通过增加对低刺激的反应来增加网络的敏感性。它在计算上比其他激活函数更复杂;然而,事实证明它是sigmoid
函数在多类别分类中的有效泛化。
tanh
激活函数,或双曲正切函数,主要用于二元分类。它在-1
和1
处有渐近线,并且常用作sigmoid
函数的替代,其中强烈负输入值导致sigmoid
输出非常接近零,导致梯度下降陷入困境。在这种情况下,tanh
函数将会负数输出,允许计算有意义的参数。
多类别分类示例
到目前为止,我们一直在使用微不足道的示例来演示 PyTorch 中的核心概念。现在我们准备探索一个更真实的例子。我们将使用的数据集是手写数字从 0 到 9 的MNIST
数据集。任务是正确识别每个样本图像与正确的数字。
我们将构建的分类模型由多层组成,如下图所示:
我们正在处理的图像大小为 28 x 28 像素,每个图像中的每个像素由一个单一数字表示其灰度。这就是为什么我们需要 28 x 28 或 784 个输入到模型中。第一层是具有 10 个输出的线性层,每个标签输出一个结果。这些输出被馈送到softmax
激活层和交叉熵损失层中。这 10 个输出维度代表 10 个可能的类别,即数字零到九。具有最高值的输出表示给定图像的预测标签。
我们首先导入所需的库,以及MNIST
数据集:
现在让我们打印有关MNIST
数据集的一些信息:
函数len
返回数据集中单独项目的数量(在本例中为单个图像)。每个图像都被编码为张量类型,并且每个图像的尺寸为 28 x 28 像素。图像中的每个像素被分配一个单一数字,表示其灰度。
要定义我们的多类别分类模型,我们将使用完全相同的模型定义,即我们用于线性回归的模型定义:
即使最终我们需要执行逻辑回归,但我们以略有不同的方式实现所需的激活和非线性。您会注意到,在模型定义中,由前向函数返回的输出只是一个线性函数。与我们在之前的二元分类示例中使用sigmoid
函数不同,这里我们使用softmax
函数,并分配给损失标准。以下代码设置这些变量并实例化模型:
CrossEntropyLoss()
函数实际上为网络添加了两层:softmax
激活函数和交叉熵损失函数。网络的每个输入都取自图像的一个像素,因此我们的输入维度为 28 x 28 = 784。优化器使用随机梯度下降和学习率为.0001
。
接下来,我们设置了批处理大小,运行模型的epochs
数量,并创建了一个数据加载器对象,以便模型可以迭代数据:
设置批量大小将数据以特定大小的块馈送到模型中。在这里,我们以100
张图像的批量进行馈送。迭代次数(即网络的总前向后向遍历次数)可以通过将数据集的长度除以批量大小,然后乘以epochs
的数量来计算。在本例中,总共有 5 x 60,000/100 = 3,000 次迭代。事实证明,这是处理中等到大型数据集更高效和有效的方法,因为在有限内存中,可能无法加载整个数据。此外,由于每批次训练模型时使用不同的数据子集,因此模型往往会做出更好的预测。将shuffle
设置为True
会在每个epoch
上对数据进行洗牌。
要运行此模型,我们需要创建一个外部循环,该循环通过epochs
进行迭代,并创建一个内部循环,该循环通过每个批次进行迭代。这通过以下代码实现:
这类似于我们迄今为止用于运行所有模型的代码。唯一的区别在于,这里的模型枚举了trainloader
中的每个批次,而不是一次迭代整个数据集。在这里,我们打印出每个epoch
上的损失,并且预期的损失是递减的。
我们可以通过向模型进行前向传播来进行预测:
预测变量的大小为100
乘以10
。这表示批处理中的100
张图像的预测结果。对于每张图像,模型输出一个包含10
个元素的预测张量,其中每个输出表示每个标签的相对强度。以下代码打印出第一个预测张量和实际标签:
如果我们仔细观察之前的输出,我们会看到模型正确预测了标签,因为第二个元素,表示数字1
,具有最高值1.3957
。我们可以通过比较张量中的其他值来看出此预测的相对强度。例如,我们可以看到下一个最强的预测是数字7
,其值为0.9142
。
您会发现模型并非对每个图像都正确,要开始评估和改进我们的模型,我们需要能够衡量其性能。最直接的方法是测量其成功率,即正确结果的比例。为此,我们创建了以下函数:
我们在这里使用字符串推导,首先通过找到每个输出的最大值来创建预测列表。接下来,我们创建一个标签列表以比较这些预测。我们通过比较predict
列表中的每个元素与actual
列表中对应的元素来创建正确值列表。最后,我们通过将正确值的数量除以总预测次数来返回成功率。我们可以通过将输出预测和标签传递给此函数来计算我们模型的成功率:
在这里,我们获得了 83%的成功率。请注意,这是使用模型已经训练过的图像计算得出的。要真正测试模型的性能,我们需要在它之前未见过的图像上进行测试。我们通过以下代码来实现这一点:
在这里,我们使用了整个MNIST
测试集的 10,000 张图像来测试模型。我们从数据加载器对象中创建迭代器,然后加载到两个张量images
和labels
中。接下来,通过传递模型测试图像,我们获得一个输出(这里是一个 10 乘以 10,000 的预测张量)。最后,我们运行SuccessRate
函数,传递输出和标签。其值仅略低于训练集上的成功率,因此我们可以合理地确信这是模型性能的准确度量。
总结
在本章中,我们探索了线性模型并将其应用于线性回归、逻辑回归和多类别分类的任务中。我们看到了自动求导如何计算梯度以及 PyTorch 如何处理计算图。我们构建的多类别分类模型在预测手写数字方面表现合理,但其性能远非最优。最好的深度学习模型能够在这个数据集上接近 100%的准确率。
我们将在第四章,卷积网络中看到如何通过增加更多层和使用卷积网络来提高性能。
第四章:卷积网络
之前,我们构建了几个简单的网络来解决回归和分类问题。这展示了使用 PyTorch 构建人工神经网络所涉及的基本代码结构和概念。
在本章中,我们将通过增加层和使用卷积层来扩展简单的线性模型,解决实际示例中存在的非线性问题。具体来说,我们将涵盖以下主题:
-
超参数和多层网络
-
构建一个简单的基准函数来训练和测试模型
-
卷积网络
超参数和多层网络
现在您了解了构建、训练和测试模型的过程,您将会发现将这些简单网络扩展以提高性能是相对简单的。您会发现我们构建的几乎所有模型基本上都包含以下六个步骤:
-
导入数据并为训练集和测试集创建可迭代的数据加载器对象
-
构建并实例化一个模型类
-
实例化一个损失类
-
实例化一个优化器类
-
训练模型
-
测试模型
当然,一旦我们完成这些步骤,我们会通过调整一组超参数来改进我们的模型并重复这些步骤。值得一提的是,尽管我们通常认为超参数是由人类明确设置的东西,但这些超参数的设置可以部分自动化,就像我们将在学习率的情况下看到的那样。以下是最常见的超参数:
-
梯度下降的学习率
-
运行模型的 epochs 数量
-
非线性激活函数的类型
-
网络的深度,即隐藏层的数量
-
网络的宽度,即每层的神经元数量
-
网络的连接性(例如,卷积网络)
我们已经使用过一些超参数。我们知道学习率,如果设置得太小,将比必要的时间多花费,如果设置得太大,将会过度震荡并表现不稳定。Epochs 的数量是对训练集的完全遍历次数。我们预期随着 epochs 的增加,每个 epoch 的准确率都会提高,考虑到数据集和算法的限制。在某个时刻,准确率将会稳定,再多的 epochs 训练将会是一种资源浪费。如果准确率在前几个 epochs 降低,最可能的原因之一是学习率设置得太高。
激活函数在分类任务中起着关键作用,不同类型的激活的效果可能有些微妙。一般认为 ReLU,或修正线性函数,在最常见的实践数据集上表现最好。这不是说其他激活函数,特别是双曲正切函数或 tanh 函数以及这些函数的变体,如渗漏 ReLU,在某些条件下不能产生更好的结果。
随着深度或层数的增加,我们增强了网络的学习能力,使其能够捕获训练集更复杂的特征。显然,这种增强的能力在很大程度上取决于数据集的大小和复杂性以及任务本身。对于小数据集和相对简单的任务(例如使用 MNIST 进行数字分类),很少的层数(一到两层)就可以得到很好的结果。太多层会浪费资源,并且往往会使网络过拟合或表现不稳定。
当我们来增加宽度,也就是每层单元的数量时,这些说法大多是正确的。增加线性网络的宽度是提升学习能力最有效的方法之一。至于卷积网络,正如我们将看到的那样,不是每个单元都连接到下一个前向层的每个单元;连通性,也就是每层的输入和输出通道数,是至关重要的。我们很快会看到卷积网络,但首先我们需要开发一个框架来测试和评估我们的模型。
对模型进行基准测试
对任何深度学习探索的成功来说,基准测试和评估至关重要。我们将开发一些简单的代码来评估两个关键性能指标:准确率和训练时间。我们将使用以下模型模板:
这个模型是解决 MNIST 问题的最常见和基本的线性模板。你可以看到,在**init**
方法中,我们初始化每一层,通过创建一个赋给 PyTorch nn
对象的类变量。在这里,我们初始化了两个线性函数和一个 ReLU 函数。nn.Linear
函数以28*28
或784
的输入大小开始。这是每个训练图像的尺寸。输出通道或网络的宽度设置为100
。这可以设置为任何值,一般情况下,更高的数字将在计算资源的限制和更宽网络过拟合训练数据的倾向中提供更好的性能。
在forward
方法中,我们创建一个out
变量。你可以看到,out 变量经过一个线性函数、一个 ReLU 函数和另一个线性函数的有序序列处理后返回。这是一个相当典型的网络架构,由交替的线性和非线性层组成。
现在让我们创建另外两个模型,将 ReLU 函数替换为 tanh 和 sigmoid 激活函数。这是 tanh 版本的代码:
你可以看到,我们只是更改了名称,并用nn.Tanh()
函数替换了nn.ReLU()
函数。以完全相同的方式创建第三个模型,用nn.Tanh()
替换nn.Sigmoid()
。不要忘记在超级构造函数中更改名称,并在用于实例化模型的变量中进行更改。还要相应地更改前向函数。
现在,让我们创建一个简单的 benchmark
函数,用来运行并记录每个模型的准确度和训练时间:
希望这个解释清楚了。benchmark
函数接受两个必需的参数:数据和要评估的模型。我们为 epochs 和学习率设置了默认值。我们需要初始化模型,以便可以在同一模型上多次运行它,否则模型参数会累积,扭曲我们的结果。运行的代码与之前的模型使用的代码完全相同。最后,我们打印出准确度和训练时间。这里计算的训练时间实际上只是一个近似值,因为训练时间会受处理器上其他正在进行的操作、内存量和其他我们无法控制的因素的影响。我们只能将此结果用作模型时间性能的相对指标。最后,我们需要一个函数来计算准确率,定义如下:
记得加载训练和测试数据集,并确保它们可以像之前一样被迭代。现在,我们可以运行我们的三个模型并使用类似以下方式进行比较:
我们可以看到,Tanh
和 ReLU
函数的性能显著优于 sigmoid
。对于大多数网络来说,隐藏层上的 ReLU
激活函数在准确度和训练时间上都表现最好。ReLU
激活函数不用于输出层。对于输出层,由于我们需要计算损失,我们使用 softmax
函数。这是损失类的标准,像之前一样,我们使用 CrossEntropy Loss()
,其中包括 softmax
函数。
从这里我们可以改进的几种方法;一个明显的方法就是简单地添加更多层。这通常是通过添加交替的非线性和线性层来完成的。在下面的例子中,我们使用 nn.Sequential
来组织我们的层。在我们的前向层中,我们只需调用序列对象,而不是每个单独的层和函数。这使得我们的代码更加紧凑和易读:
在这里,我们增加了两个更多的层:一个线性层和一个非线性的ReLU
层。设置输入和输出大小尤为重要。在第一个线性层中,输入大小为784
,这是图像的大小。该层的输出是我们选择的,设为100
。因此,第二个线性层的输入必须是100
。这是输出的宽度,卷积核和特征图的数量。第二个线性层的输出是我们选择的,但一般的想法是减少大小,因为我们试图将特征过滤到只有10
个目标类别。为了好玩,创建一些模型,尝试不同的输入和输出大小,记住任何层的输入必须与上一层的输出大小相同。以下是三个模型的输出,我们打印每个隐藏层的输出大小,以便您了解可能的情况:
我们可以根据需要添加尽可能多的层和卷积核,但这并不总是一个好主意。我们在网络中设置输入和输出大小的方式与数据的大小、形状和复杂性密切相关。对于简单的数据集,比如 MNIST,几个线性层就能得到非常好的结果。但是,简单地添加线性层和增加卷积核的数量在捕捉复杂数据集的高度非线性特征时并不有效。
卷积网络
到目前为止,我们在网络中使用了全连接层,其中每个输入单元表示图像中的一个像素。而使用卷积网络时,每个输入单元则被分配一个小的局部感受野。感受野的概念,就像人工神经网络本身一样,是模仿人脑的。1958 年发现,大脑视皮层的神经元对视野中的受刺激有限区域作出反应。更有趣的是,一组神经元仅对某些基本形状作出反应。例如,一组神经元可能对水平线作出反应,而其他组则仅对其他方向的线作出反应。观察到,一组神经元可以具有相同的感受野,但对不同的形状作出反应。还观察到神经元组织成层,深层对更复杂的模式作出反应。这事实上是计算机学习和分类一组图像的一种非常有效的方式。
单个卷积层
卷积层组织得使第一层的单元仅对其相应的感受野作出反应。下一层的每个单元仅连接到第一层的一个小区域,第二个隐藏层的每个单元仅连接到第三层的有限区域,依此类推。通过这种方式,网络可以训练以从前一层中存在的低级特征组装出更高级别的特征。
在实践中,这是通过使用滤波器或卷积核来扫描图像以生成所谓的特征图来实现的。卷积核只是一个大小与接受域相同的矩阵。我们可以将其想象为相机以离散步幅扫描图像。我们通过卷积核矩阵与图像接受域中的值进行逐元素乘法来计算特征图矩阵。然后将结果矩阵求和以计算特征图中的单个数值。卷积核矩阵中的值代表我们希望从图像中提取的特征。这些是我们最终希望模型学习的参数。考虑一个简单的例子,我们试图在图像中检测水平和垂直线条。为简化问题,我们将使用一个输入维度;这可以是黑色,用1表示,或白色,用0表示。请记住,在实践中,这些将是表示灰度或颜色值的缩放和归一化的浮点数。在这里,我们将卷积核设置为 4 x 4 像素,并使用步幅为1进行扫描。步幅简单地是我们移动卷积核的距离,因此步幅为1将卷积核移动一个像素:
一个卷积是对图像的完整扫描,每个卷积生成一个特征图。在每个步幅上,我们对图像的接受域与卷积核进行逐元素乘法,并将结果矩阵求和。
当我们在图像上移动卷积核时,如前图所示,步幅 1 采样左上角,步幅 2 采样左边一个像素,步幅 3 再次采样左边一个像素,依此类推。当我们到达第一行的末尾时,我们需要添加填充像素,因此将值设置为0以便采样图像的边缘。用零填充输入数据称为有效填充。如果我们没有对图像进行填充,特征图的维度将小于原始图像。填充用于确保不丢失原始信息。
理解输入和输出大小、卷积核大小、填充和步幅之间的关系非常重要。它们可以用以下公式来表达:
这里,O = 输出大小,W = 输入高度或宽度,K = 卷积核大小,P = 填充,S = 步幅。请注意,输入高度或宽度假设这两者相同,即输入图像是方形的,而不是矩形的。如果输入图像是矩形的,则需要分别计算宽度和高度的输出值。
填充可以如下计算:
多个卷积核
在每个卷积中,我们可以包含多个内核。卷积中的每个内核生成自己的特征图。内核的数量是输出通道的数量,这也是卷积层生成的特征图数量。我们可以通过使用另一个内核生成更多的特征图。作为练习,请计算以下内核将生成的特征图:
通过堆叠内核或过滤器,并使用不同大小和值的内核,我们可以从图像中提取各种特征。
同时,请记住每个内核不限于一个输入维度。例如,如果我们处理 RGB 彩色图像,则每个内核的输入维度为三。由于我们正在进行逐元素乘法,内核必须与感受野大小相同。当我们有三个维度时,内核需要具有三个输入深度。因此,我们的灰度 2x2 内核对于彩色图像来说变成了一个 2x2x3 矩阵。我们仍然为每个卷积生成单个特征图。我们仍然能够进行逐元素乘法,因为内核大小与感受野相同,只是现在在进行求和时,我们跨三个维度进行求和以获取每个步长所需的单个数字。
正如你可以想象的那样,我们可以以多种方式扫描图像。我们可以更改内核的大小和值,或者更改其步幅,包括填充,甚至包括非连续像素。
要更好地了解一些可能性,请查看 vdumoulin 出色的动画:github.com/vdumoulin/conv_arithmetic/blob/master/README.md
。
多个卷积层
与完全连接的线性层一样,我们可以添加多个卷积层。与线性层一样,同样的限制适用:
-
时间和内存限制(计算负载)
-
倾向于过拟合训练集而不是泛化到测试集
-
需要更大的数据集才能有效工作
适当添加卷积层的好处在于,逐步地,它们能够从数据集中提取更复杂、非线性的特征。
池化层
典型地,卷积层是通过池化层堆叠的。池化层的目的是减少前面卷积生成的特征图的尺寸,但不减少深度。池化层保留 RGB 信息,但压缩空间信息。我们这样做的原因是使内核能够有选择地专注于某些非线性特征。这意味着我们可以通过专注于具有最强影响力的参数来减少计算负载。具有较少参数还可以减少过拟合的倾向。
有三个主要原因导致使用池层来减少输出特征图的维数:
-
通过丢弃不相关的特征来减少计算负载
-
参数数量较少,因此不太可能过拟合数据。
-
能够提取经过某种方式转换的特征,例如来自不同视角的物体图像。
池化层与普通卷积层非常相似,因为它们使用核矩阵或滤波器对图像进行采样。池化层的不同之处在于我们进行了降采样。降采样会减少输入维度。这可以通过增加核的大小或增加步幅或两者同时实现。请查看单卷积层部分的公式以确认这一点。
记住,在卷积中,我们所做的只是在每个步幅上对图像上的两个张量进行乘法运算。卷积中的每个后续步幅采样输入的另一部分。这种采样是通过核与前一卷积层的输出之间的逐元素乘法实现的,包含在特定步幅内。这种采样的结果是一个单一数字。在卷积层中,这个单一数字是逐元素乘法的总和。在池化层中,这个单一数字通常是由逐元素乘法的平均值或最大值生成的。术语平均池化和最大池化指的是这些不同的池化技术。
构建单层 CNN
现在我们应该有足够的理论来构建一个简单的卷积网络并理解其工作原理了。以下是我们可以开始使用的模型类模板:
我们将在 PyTorch 中使用的基本卷积单元是nn.Conv2d
模块。其特征如下所示:
nn.Conv2d(in_channels, outs_channels, kernel_size, stride=1,
padding = 0)
这些参数的值受限于输入数据的大小和上一节讨论的公式。在这个例子中,in_channels
设置为1
。这表示我们的输入图像有一个颜色维度。如果我们使用三通道彩色图像,则应将其设置为3
。out_channel
是卷积核的数量。我们可以将其设置为任何值,但要记住会有计算惩罚,并且改善性能取决于具有更大、更复杂数据集。在本例中,我们将输出通道数设置为16
。输出通道数或卷积核实质上是我们认为可能是目标类别的低级特征的数量。我们将步幅设置为1
,填充设置为2
。这可以确保输出大小与输入大小相同;可以通过将这些值代入单卷积层部分的输出公式来验证。
在__init__
方法中,您会注意到我们实例化了一个卷积层,一个ReLU
激活函数,一个MaxPool2d
层和一个全连接的线性层。这里重要的是理解我们如何推导传递给nn.Linear()
函数的值。这是 MaxPool 层的输出大小。我们可以使用我们的输出公式来计算这个值。我们知道卷积层的输出与输入相同。因为输入图像是方形的,我们可以使用 28 来表示输入,因此也可以表示卷积层的输出大小。我们还知道我们设置了2
的核大小。默认情况下,MaxPool2d
将步幅分配给核大小,并使用隐含的填充。出于实际目的,这意味着当我们对步幅和填充使用默认值时,我们可以简单地通过核大小除以输入,这里是 28。因为我们的核大小是 2,我们可以计算出输出大小为 14。因为我们使用了全连接的线性层,我们需要展平宽度、高度和通道数。我们有 32 个通道,如在nn.Conv2d
的out_channels
参数中设置的那样。因此,输入大小为 16 X 14 X 14。输出大小为 10,因为像线性网络一样,我们使用输出来区分这 10 个类。
模型的forward
函数非常简单。我们只需将out
变量通过卷积层、激活函数、池化层和全连接的线性层。注意,我们需要为线性层调整输入大小。假设批量大小为100
,池化层的输出是一个四维张量:100, 32, 14, 14
。在这里,out.view(out.size(0), -1)
将这个四维张量重塑为一个二维张量:100, 32*14*14
。
为了使这更具体化,让我们训练我们的模型并查看一些变量。我们可以使用几乎相同的代码来训练卷积模型。但是,我们需要在我们的benchmark()
函数中更改一行。由于卷积层可以接受多个输入维度,我们不需要展平输入的宽度和高度。对于以前的线性模型,在我们的运行代码中,我们使用以下内容来展平输入:
outputs= model(images.view(-1, 28*28))
对于我们的卷积层,我们不需要这样做;我们可以简单地将图像传递给模型,如下所示:
outputs = model(images)
在我们之前在本章的bench marking
部分定义的accuracy()
函数中,我们还必须更改这一行。
构建多层 CNN
正如您所期望的那样,我们可以通过添加另一个卷积层来改善这个结果。当我们添加多个层时,将每个层打包成一个序列是很方便的。这就是nn.Sequential
发挥作用的地方:
我们初始化了两个隐藏层和一个全连接的线性输出层。注意传递给Conv2d
实例和线性输出的参数。与以前一样,我们有一个输入维度。从这个维度,我们的卷积层输出16
个特征图或输出通道。
这个图表代表了双层卷积网络:
这应该清楚地展示了我们如何计算输出大小,特别是我们如何推导线性输出层的输入大小。我们知道,使用输出公式,第一个卷积层在最大池化之前的输出大小与输入大小相同,即 28 x 28。由于我们使用了 16 个核或通道,生成了 16 个特征图,进入最大池化层的输入是一个 16 x 28 x 28 的张量。最大池化层使用 2 的核大小、步长 2 和默认的隐式填充,这意味着我们只需将特征图大小除以 2 即可计算最大池化输出大小。这给出了一个输出大小为 16 x 14 x 14。这是第二个卷积层的输入大小。再次使用输出公式,我们可以计算出第二个卷积层在最大池化之前生成了 14 x 14 的特征图,与其输入大小相同。由于我们将核的数量设置为 32,进入第二个最大池化层的输入是一个 32 x 14 x 14 的矩阵。我们的第二个最大池化层与第一个相同,核大小和步长设置为 2,使用默认的隐式填充。再次,我们可以简单地除以 2 来计算输出大小,因此是线性输出层的输入。最后,我们需要将这个矩阵展平为一个维度。因此,线性输出层的输入大小是一个维度为 32 * 7 * 7,即 1,568。和往常一样,我们需要最终线性层的输出大小是类的数量,本例中为 10。
我们可以检查模型参数,看看在运行代码时确实发生了什么:
模型参数包括六个张量。第一个张量是第一个卷积层的参数。它包含16
个核,1
个颜色维度,以及大小为5
的核。接下来的张量是偏置,具有大小为16
的单个维度。列表中的第三个张量是第二个卷积层中的32
个核,16
个输入通道,深度和5 x 5
的核。在最后的线性层中,我们将这些维度展平为10 x 1568
。
批归一化
批量归一化被广泛用于提高神经网络的性能。它通过稳定化层输入的分布来工作。这是通过调整这些输入的均值和方差来实现的。在深度学习研究中,有关批量归一化如此有效的原因的研究者社区存在不确定性是相当典型的。曾经认为这是因为它减少了所谓的内部协变量偏移(ICS)。这指的是由于前面层参数更新的结果而导致的分布变化。批量归一化最初的动机是减少这种偏移。然而,ICS 与性能之间的明确关联尚未得出结论性结果。更近期的研究表明,批量归一化通过平滑化优化的景观。基本上,这意味着梯度下降将更加有效。关于这一点的详细信息可以在 Santurkar 等人的文章批量归一化如何帮助优化中找到,可访问arxiv.org/abs/1805.11604
。
使用nn.BatchNorm2d
函数实现的批量归一化:
这个模型与之前的双层 CNN 完全相同,只是在卷积层的输出上添加了批量归一化。以下是我们迄今为止构建的三个卷积网络的性能打印输出:
摘要
在本章中,我们看到如何改进第三章中开发的简单线性网络,计算图和线性模型。我们可以添加线性层,增加网络宽度,增加我们运行模型的时代数,并调整学习率。然而,线性网络将无法捕捉数据集的非线性特征,在某些时候它们的性能将会达到平台期。另一方面,卷积层使用内核来学习非线性特征。我们看到,使用两个卷积层,MNIST 的性能显著提高。
在下一章中,我们将探讨一些不同的网络架构,包括循环网络和长短期网络。
第五章:其他神经网络架构
循环网络本质上是保持状态的前馈网络。到目前为止,我们看到的所有网络都需要固定大小的输入,比如图像,并给出固定大小的输出,比如特定类别的概率。循环网络不同之处在于,它们接受任意大小的序列作为输入,并产生序列作为输出。此外,网络的隐藏层的内部状态会随着学习的函数和输入的结果而更新。通过这种方式,循环网络记住了它的状态。后续状态是前几个状态的函数。
在本章中,我们将涵盖以下内容:
-
循环网络简介
-
长短期记忆网络
循环网络简介
循环网络在预测时间序列数据方面显示出极强的能力。这对于生物大脑而言是一项基本技能,使我们能够安全驾驶汽车、演奏乐器、躲避捕食者、理解语言以及与动态世界互动。对时间流逝的感知以及事物随时间变化的理解对智能生命至关重要,因此在人工系统中,这种能力的重要性毋庸置疑。
理解时间序列数据的能力在创意工作中也非常重要,循环网络已显示出在作曲旋律、构造语法正确的句子和创建视觉上令人愉悦的图像等方面具有一定的能力。
如我们所见,前馈和卷积网络在诸如静态图像分类等任务中表现出色。然而,处理连续数据,如语音或手写识别、预测股票市场价格或天气预报等任务,需要不同的方法。在这些类型的任务中,输入和输出不再是固定长度的数据,而是任意长度的序列。
循环人工神经元
对于前馈网络中的人工神经元,激活的流动只是从输入到输出。循环人工神经元(RANs)在激活层的输出到其线性输入之间建立了连接,实质上将输出再次加到输入中。RAN 可以在时间上展开:每个后续状态都是前几个状态的函数。因此,可以说 RAN 具有其前几个状态的记忆:
在上述示意图中,左侧的图示了一个单个循环神经元。它将其输入x
与输出y
相加,产生一个新的输出。右侧的图示了相同单元在三个时间步长上的展开。我们可以写出任意给定时间步长输出关于输入的方程如下:
这里,y(t) 是时间 t 的输出向量,x[(t)] 是时间 t 的输入,y[(t-1)] 是前一个时间步的输出,b 是偏置项,Φ 是激活函数,通常是 tanh 或者 RelU。注意,每个单元都有两组权重,w[x] 和 w[y],分别用于输入和输出。这本质上是我们在线性网络中使用的公式,其中添加了一个项来表示输出,反馈到时间 t-1 的输入中。
就像在 CNNs(卷积神经网络)中可以计算整个层的输出一样,使用前述方程的向量化形式在递归网络中也是可能的。以下是递归层的向量化形式:
这里,Y[(t)] 是时间 t 的输出。这是一个大小为 (m, n) 的矩阵,其中 m 是批次中实例的数量,n 是层中单元的数量。X[(t)] 是一个大小为 (m, i) 的矩阵,其中 i 是输入特征的数量。W[x] 是一个大小为 (i, n) 的矩阵,包含当前时间步的输入权重。W[y] 是一个大小为 (n, n) 的矩阵,包含前一个时间步的输出权重。
实现循环网络
因此,我们可以专注于模型,使用我们熟悉的相同数据集。尽管我们处理的是静态图像,但我们可以将每个 28 像素输入大小在 28 个时间步骤上展开,使网络能够对完整图像进行计算:
在前述模型中,我们使用 nn.RNN
类创建了一个具有两个循环层的模型。nn.RNN
类的默认签名如下:
nn.RNN(input_size, hidden_size, num_layers, batch_first=False, nonlinearity = 'tanh'
输入是我们的 28 x 28 MNIST 图像。此模型取每个图像的 28 个像素,将它们展开成 28 个时间步骤,以计算整个批次中所有图像的结果。hidden_size
参数是隐藏层的维度,这是我们选择的。在这里,我们使用大小为 100
。batch_first
参数指定输入和输出张量的预期形状。我们希望其形状为(批次大小,序列长度,特征数)。在本例中,我们期望的输入/输出张量形状是 (100, 28, 28
)。即批次大小、序列长度和每步的特征数;然而,默认情况下,nn.RNN
类使用的是形状为 (序列长度,批次大小,特征数) 的输入/输出张量。设置 batch_first = True
确保输入/输出张量的形状为 (批次大小,序列长度,特征数)。
在forward
方法中,我们初始化了一个用于隐藏层的张量h0
,在模型的每次迭代中更新。这个隐藏张量的形状,表示隐藏状态,是(layers, batch, hidden)的形式。在这个例子中,我们有两层。隐藏状态的第二个维度是批处理大小。请记住,我们使用批处理优先,因此这是输入张量x
的第一个维度,使用x[0]
进行索引。最后一个维度是隐藏大小,在这个例子中,我们设置为100
。
nn.RNN
类要求输入包括输入张量x
和h0
隐藏状态。这就是为什么在forward
方法中,我们传入这两个变量。forward
方法在每次迭代时被调用,更新隐藏状态并给出输出。请记住,迭代次数是每个批次的数据大小除以批次大小乘以数据集大小的结果。
重要的是,正如你所看到的,我们需要使用以下方法对输出进行索引:
out = self.fc(out[:, -1, :])
我们只关注最后一个时间步的输出,因为这是批处理中所有图像的累积知识。如果你记得,输出形状是(batch, sequence, features),在我们的模型中是(100, 28, 100)
。特征的数量简单地等于隐藏维度或隐藏层中的单元数。现在,我们需要所有批次:这就是为什么我们使用冒号作为第一个索引。在这里,-1
表示我们只想要序列的最后一个元素。最后的索引,冒号,表示我们想要所有的特征。因此,我们的输出是序列中最后一个时间步的所有特征,针对整个批次。
我们可以使用几乎相同的训练代码;但是,在调用模型时,我们确实需要重塑输出。请记住,对于线性模型,我们使用以下方法重塑输出:
outputs = model(images.view(-1, 28*28)
对于卷积网络,通过使用nn.CNN
,我们可以传入未展平的图像;而对于循环网络,在使用nn.RNN
时,我们需要输出的形式为(batch, sequence, features)。因此,我们可以使用以下方法重塑输出:
outputs = model(images.view(-1, 28,28))
记住,我们需要在我们的训练代码和测试代码中修改此行。以下是运行三个不同层和隐藏尺寸配置的循环模型的结果打印输出:
要更好地理解这个模型的工作原理,请考虑以下图表,表示我们的具有隐藏大小为100
的两层模型:
在每一个28个时间步骤中,网络接收一个输入,包含100张图像中每张图像的28个像素(特征)。每个时间步骤基本上是一个两层前馈网络。唯一的区别在于每个隐藏层都有额外的输入。这个输入包括前一个时间步骤中相应层的输出。在每个时间步骤,从每个批次中的100张图像中抽取另外28个像素。最后,当批次中的所有图像都被处理完毕时,模型的权重会更新,下一个迭代开始。完成所有迭代后,我们读取输出以获取预测结果。
要更好地理解运行代码时发生的情况,请考虑以下内容:
在这里,我们打印出了一个具有100
隐藏层大小的双层 RNN 模型的权重向量大小。
我们将权重检索为包含10
个张量的列表。第一个大小为[100, 28]
的张量包含隐藏层的输入,100
个单元和输入图像的28
个特征或像素。这是递归网络向量化形式方程中的W[x ]项。接下来的参数组,大小为[100, 100]
,由前述方程中的W[y]项表示,是隐藏层的输出权重,包含每个大小为100
的单元100
。接下来的两个大小为100
的单维张量分别是输入和隐藏层的偏置单元。接着我们有第二层的输入权重、输出权重和偏置。最后,我们有读取层权重,大小为[10, 100]
的张量,用于使用100
个特征进行10
个可能的预测。大小为[10]
的最终单维张量包含读取层的偏置单元。
在下面的代码中,我们复制了模型中递归层在单个图像批次上的操作:
您可以看到我们简单地将trainLoader
数据集对象创建为一个迭代器,并将一个images
变量分配给一个图像批次,就像我们在训练代码中所做的那样。隐藏层h0
需要包含两个张量,每个层次一个。在这些张量中,对于批次中的每个图像,100
个隐藏单元的权重被存储。这解释了为什么我们需要一个三维张量。第一个大小为2
,表示层数,第二个维度大小为100
,来自images.size(0)
,表示批次大小,第三个维度大小为100
,表示隐藏单元数量。然后,我们将重塑后的图像张量和隐藏张量传递给模型。这调用了模型的forward()
函数,进行必要的计算,并返回一个输出张量和一个更新后的隐藏状态张量。
以下确认了这些输出的大小:
这应该帮助您理解为什么我们需要调整images
张量的大小。请注意,输入的特征是每个图像中的28
像素,这些像素在28
个时间步上展开。接下来,让我们将递归层的输出传递给我们的全连接线性层:
您可以看到,这将为输出中的100
个特征的每个特征给出10
个预测。这就是为什么我们只需索引序列中的最后一个元素。请记住,从nn.RNN
输出的大小为(100, 28, 100
)。观察使用-1
索引时此张量的大小会发生什么变化:
这是包含100
个特征的张量,即隐藏单元的输出,每个批次中有100
张图片。这会传递给我们的线性层,以生成每个图像所需的10
个预测。
长短期记忆网络
长短期记忆网络(LSTMS) 是一种特殊类型的 RNN,能够学习长期依赖关系。虽然标准 RNN 在某种程度上可以记住先前的状态,但是它们通过在每个时间步更新隐藏状态的方式来完成这一点。这使得网络能够记住短期依赖关系。隐藏状态作为先前状态的函数,保留了关于这些先前状态的信息。然而,当前状态与先前状态之间的时间步长越多,这个早期状态对当前状态的影响就越小。在当前时间步之前的 10 个时间步的状态上保留的信息要少得多。这样做是尽管较早的时间步可能包含对特定问题或任务具有直接相关性的重要信息。
生物大脑具有出色的记忆长期依赖能力,利用这些依赖形成意义和理解。考虑我们如何跟随电影的情节。我们回忆起电影开始时发生的事件,并且随着情节的发展立即理解它们的相关性。不仅如此,我们还可以通过回忆自己生活中的事件,为故事线赋予相关性和意义。这种有选择地将记忆应用于当前背景的能力,同时过滤掉无关细节,是设计 LSTM 的策略背后的原理。
LSTM 网络试图将这些长期依赖关系整合到人工网络中。它比标准 RNN 复杂得多;然而,它仍基于递归前馈网络,理解这一理论应使您能够理解 LSTM。
下图显示了 LSTM 在单个时间步上的示意图:
与普通的 RNN 一样,每个后续时间步骤都将前一个时间步的隐藏状态 h[t-1] 和数据 x[t][,] 作为其输入。LSTM 还传递在每个时间步骤上计算的细胞状态。您可以看到 h[t-1] 和 x[t] 各自传递给四个单独的线性函数。这些线性函数的每对被求和。LSTM 的核心是这四个门,这些总和被传递到这些门中。首先,我们有 遗忘门。这使用 sigmoid 作为激活函数,并且与 旧细胞状态 进行逐元素乘法。请记住,sigmoid 有效地将 线性 输出值压缩到零到一之间的值。乘以零将有效地消除细胞状态中的特定值,而乘以一将保留该值。遗忘门 实际上决定了传递到下一个时间步的信息。这是通过与 旧细胞状态 的逐元素乘法来实现的。
输入门 和 缩放后的新候选门 共同决定了保留哪些信息。输入门 还使用了 sigmoid 函数,并且乘以 新候选门 的输出,生成一个临时张量,即 缩放后的新候选门 c[2]。请注意,新候选门 使用 tanh 激活函数。记住 tanh 函数的输出值在 -1
和 1
之间。通过这种方式使用 tanh 和 sigmoid 激活函数,即它们的输出进行逐元素乘法,有助于避免梯度消失问题,其中输出变得饱和并且它们的梯度重复接近零,使它们无法执行有意义的计算。通过将 缩放后的新候选门 和 缩放后的旧细胞状态 相加来计算 新细胞状态,从而能够放大输入数据的重要组成部分。
最后一个门,输出门 O,,是另一个 sigmoid。新的细胞状态通过 tanh 函数传递,然后与输出门进行逐元素乘法,以计算 隐藏状态。与标准 RNN 一样,这个 隐藏状态 通过最终的非线性传递,即 sigmoid 和 Softmax 函数,以产生输出。这总体上的效果是增强高能量组分,消除低能量组分,同时减少梯度消失的机会和减少训练集的过拟合。
我们可以将每个 LSTM 门的方程式写成如下形式:
注意,这些方程式与 RNN 的形式完全相同。唯一的区别在于我们需要八个单独的权重张量和八个偏置张量。正是这些额外的权重维度赋予了 LSTM 额外的能力,可以学习和保留输入数据的重要特征,并丢弃不重要的特征。我们可以将某个时间步骤t的线性输出层输出写为以下形式:
实现一个 LSTM
以下是我们将用于 MNIST 的 LSTM 模型类:
注意,nn.LSTM
传递的参数与之前的 RNN 相同。这并不奇怪,因为 LSTM 是一个适用于数据序列的循环网络。请记住输入张量具有形式(batch, sequence, feature)
的轴,因此我们设置batch_first=True
。我们为输出层初始化一个全连接线性层。请注意,在forward
方法中,除了初始化隐藏状态张量h0
,我们还初始化一个用于保存细胞状态的张量c0
。还要记住out
张量包含所有28
个时间步长。对于我们的预测,我们只对序列中的最后一个索引感兴趣。这就是为什么在将其传递给线性输出层之前,我们对out
张量应用了[:, -1, :]
索引。我们可以像之前为 RNN 打印出这个模型的参数一样打印出来:
这些是单层 LSTM 的参数,有100
个隐藏层。这个单层 LSTM 有六组参数。请注意,与 RNN 的情况不同,对于 LSTM,输入和隐藏权重张量的第一个维度大小为400
,表示每个四个 LSTM 门的100
隐藏单元。
第一个参数张量用于输入层,大小为[400,28]
。第一个索引400
对应权重w[1]、w[3]、w[5]和w[7],每个大小为100
,用于输入到指定的100
隐藏单元中。28
是输入中存在的特征或像素数量。接下来的大小为[400,100]
的张量是权重w[2]、w[4]、w[6]和w[8],每个对应100
隐藏单元。接下来的两个大小为[400]
的一维张量是两组偏置单元,b[1]、b[3]、b[5]、b[7] 和 b[2]、b[4]、b[6]、b[8],分别用于每个 LSTM 门。最后,我们有大小为[10, 100]
的输出张量。这是我们的输出大小10
,权重张量w[9]。最后一个大小为[10]
的单维张量是偏置,b9。
构建带门控递归单元的语言模型
为了展示循环网络的灵活性,我们将在本章的最后一节做一些不同的事情。到目前为止,我们一直在使用可能是最常用的测试数据集之一,MNIST。这个数据集具有众所周知的特性,非常适合比较不同类型的模型、测试不同的架构和参数集。然而,有些任务,比如自然语言处理,显然需要完全不同类型的数据集。
到目前为止,我们构建的模型都集中在一个简单的任务上:分类。这是最直接的机器学习任务。为了让你了解其他机器学习任务的风味,并展示循环网络的潜力,我们将构建一个基于字符的预测模型,该模型试图根据前一个字符预测每个后续字符,形成一个学习过的文本体。模型首先学习创建正确的元音—辅音序列、单词,最终生成模仿人类作者构建的形式(但不是意义)的句子和段落。
以下是由 Sean Robertson 和 Pratheek 编写的代码的改编版本,可以在这里找到:github.com/spro/practical-pytorch/blob/master/char-rnn-generation/char-rnn-generation.ipynb
。以下是模型定义:
该模型的目的是在每个时间步骤获取一个输入字符,并输出最可能的下一个字符。在随后的训练中,它开始积累字符序列,模仿训练样本中的文本。我们的输入和输出大小仅仅是输入文本中字符的数量,这作为参数传递给模型进行计算。我们使用nn.Embedding
类初始化一个编码器张量。与我们使用独热编码为每个单词定义唯一索引类似,nn.Embedding
模块将词汇表中的每个单词存储为多维张量。这使我们能够在单词嵌入中编码语义信息。我们需要向nn.Embedding
模块传递一个词汇量大小——这里是输入文本中字符的数量——以及用于编码每个字符的维度——这里是模型的隐藏大小。
我们使用的词嵌入模型基于nn.GRU
模块,或者 GRU。这与我们在前一节使用的 LSTM 模块非常相似。不同之处在于,GRU 是 LSTM 的稍微简化版本。它将输入门和遗忘门合并为单个更新门,并将隐藏状态与单元状态结合起来。结果是,对于许多任务来说,GRU 比 LSTM 更高效。最后,初始化线性输出层以解码 GRU 的输出。在forward
方法中,我们调整输入大小,并通过线性嵌入层、GRU 和最终的线性输出层传递它,返回隐藏状态和输出。
接下来,我们需要导入数据,并初始化包含输入文本的可打印字符和输入文本中字符数的变量。请注意使用unidecode
来删除非 Unicode 字符。如果系统尚未安装此模块,您需要导入并可能在系统上安装它。我们还定义了两个方便的函数:一个函数用于将字符字符串转换为每个 Unicode 字符的整数等效值,另一个函数用于从训练文本中随机采样随机块。random_training_set
函数返回两个张量。inp
张量包含块中的所有字符,不包括最后一个字符。target
张量包含所有偏移一个字符的块中的所有元素,因此包括最后一个字符。例如,如果我们使用块大小为4
,并且该块由 Unicode 字符表示为[41, 27, 29, 51]
,那么inp
张量将是[41, 27, 29]
,target
张量将是[27, 29, 51]
。通过这种方式,目标可以训练模型使用目标数据进行下一个字符的预测:
接下来,我们编写一个评估模型的方法。这是通过逐个字符传递它来完成的:模型输出下一个最有可能字符的多项式概率分布。这样重复操作以构建一个字符序列,并将它们存储在predicted
变量中:
evaluate
函数接受一个temperature
参数,该参数除以输出并找到指数以创建概率分布。temperature
参数的作用是确定每个预测所需的概率水平。对于大于1
的温度值,生成较低概率的字符,生成的文本更随机。对于小于1
的较低温度值,生成较高概率的字符。对于接近0
的温度值,只生成最可能的字符。对于每次迭代,将字符添加到predicted
字符串中,直到达到由predict_len
变量确定的所需长度,并返回predicted
字符串。
下面的函数训练模型:
我们将输入块和目标块传递给它。for
循环在每个块中的每个字符上运行模型一次迭代,更新hidden
状态,并返回每个字符的平均损失。
现在,我们准备实例化并运行模型。这是通过以下代码完成的:
在这里,通常变量被初始化。请注意,我们的优化器没有使用随机梯度下降,而是使用 Adam 优化器。术语 Adam 代表自适应矩估计器。梯度下降使用单一固定学习率来处理所有可学习参数。Adam 优化器使用自适应学习率来维护每个参数的学习率。它可以提高学习效率,特别是在稀疏表示中,例如用于自然语言处理的表示。稀疏表示是那些张量中大部分数值为零的表示,例如单热编码或词嵌入。
一旦我们运行模型,它将打印出预测的文本。起初,文本看起来几乎像是随机的字符序列;然而,在几个训练周期后,模型学会了将文本格式化为类似英语的句子和短语。生成模型是强大的工具,能够帮助我们揭示输入数据中的概率分布。
摘要
在本章中,我们介绍了循环神经网络,并演示了如何在 MNIST 数据集上使用 RNN。RNN 特别适用于处理时间序列数据,因为它们本质上是展开在时间上的前馈网络。这使它们非常适合处理手写和语音识别等数据序列任务。我们还看到了 RNN 的更强大变体,即 LSTM。LSTM 使用四个门来决定传递到下一个时间步的信息,从而使其能够发现数据中的长期依赖关系。最后,在本章中,我们构建了一个简单的语言模型,能够从样本输入文本中生成文本。我们使用的模型基于 GRU。GRU 是 LSTM 的稍简化版本,包含三个门,将 LSTM 的输入门和遗忘门结合在一起。该模型使用概率分布从样本输入生成文本。
在最后一章中,我们将探讨一些 PyTorch 的高级特性,例如在多处理器和分布式环境中使用 PyTorch。我们还看到如何微调 PyTorch 模型,并使用预训练模型进行灵活的图像分类。
第六章:充分利用 PyTorch
到目前为止,你应该能够构建和训练三种不同类型的模型:线性模型、卷积模型和循环模型。你应该对这些模型架构背后的理论和数学有所了解,并能解释它们如何进行预测。卷积网络可能是最研究的深度学习网络,特别是与图像数据相关的情况。当然,卷积网络和循环网络都广泛使用线性层,因此线性网络背后的理论,尤其是线性回归和梯度下降,对所有人工神经网络都是基础的。
迄今为止,我们的讨论相当有限。我们已经研究了一个被广泛研究的问题,比如使用 MNIST 进行分类,以便让你对 PyTorch 的基本构建块有扎实的理解。这最后一章是你在现实世界中使用 PyTorch 的跳板,阅读完后,你应该可以开始自己的深度学习探索。在本章中,我们将讨论以下主题:
-
使用图形处理单元(GPUs)以提高性能
-
优化策略和技术
-
使用预训练模型
多处理器和分布式环境
有各种多处理器和分布式环境的可能性。使用多处理器的最常见原因当然是为了使模型运行更快。将 MNIST 加载到内存中的时间并不重要,因为它只是一个相对较小的数据集,包含 6 万张图像。然而,考虑到我们有吉格或者 TB 级别的数据,或者数据分布在多台服务器上的情况,情况会更加复杂。当考虑在线模型时,数据实时从多台服务器收集的情况更是如此。显然,这些情况都需要某种形式的并行处理能力。
使用 GPU
使模型运行更快的最简单方法是添加 GPU。通过将处理器密集型任务从中央处理单元(CPU)转移到一个或多个 GPU 上,可以显著减少训练时间。PyTorch 使用torch.cuda()
模块与 GPU 进行接口交互。CUDA 是由 NVIDIA 创建的并行计算模型,具有延迟分配功能,因此资源只在需要时分配。由此带来的效率提升是显著的。
PyTorch 使用上下文管理器torch.device()
将张量分配给特定设备。以下截图展示了一个示例:
通常的做法是测试 GPU 的存在并使用以下语义将设备分配给变量:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
字符串"cuda:0"
指的是默认的 GPU 设备。请注意,我们会检查 GPU 设备的存在并将其分配给device
变量。如果 GPU 设备不可用,则会将设备分配给 CPU。这样可以使代码在可能有或没有 GPU 的机器上运行。
考虑我们在第三章中探讨的线性模型,计算图和线性模型。我们可以使用完全相同的模型定义;但是,我们需要在训练代码中做一些改变,以确保处理器密集型操作在 GPU 上执行。一旦创建了我们的device
变量,我们可以将操作分配给该设备。
在我们之前创建的基准函数中,我们需要在初始化模型后添加以下代码行:
model.to(device)
我们还需要确保对图像、标签和输出的操作都在选定的设备上进行。在基准函数的for
循环中,我们进行如下更改:
我们需要对在我们的准确度函数中定义的图像、标签和输出做完全相同的事情,只需将.to(device)
附加到这些张量定义中。一旦做出这些更改,如果在具有 GPU 的系统上运行,速度应明显更快。对于具有四个线性层的模型,此代码运行时间仅稍逾 55 秒,而在我的系统上仅在 CPU 上运行时超过 120 秒。当然,CPU 速度、内存和其他因素会影响运行时间,因此这些基准在不同系统上会有所不同。完全相同的训练代码将适用于逻辑回归模型。同样的修改也适用于我们研究过的其他网络的训练代码。几乎任何东西都可以传输到 GPU,但请注意,每次数据复制到 GPU 时都会产生计算开销,因此除非涉及复杂的计算(例如计算梯度),否则不要不必要地将操作传输到 GPU。
如果您的系统上有多个 GPU 可用,则可以使用nn.DataParallel
来透明地在这些 GPU 上分发操作。这可以简单地使用一个模型包装器来实现,例如,model=torch.nn.DataParallel(model)
。当然,我们也可以采用更细粒度的方法,将特定操作分配给特定设备,如下例所示:
with torch.cuda.device("device:2"): w3=torch.rand(3,3)
PyTorch 有一个特定的内存空间,可用于加速张量传输到 GPU。当张量重复分配到 GPU 时使用此功能。使用pin_memory()
函数实现,例如,w3.pin_memory()
。其中一个主要用途是加速输入数据的加载,这在模型训练周期内反复发生。为此,只需在实例化DataLoader
对象时传递pin_memory=True
参数。
分布式环境
有时,数据和计算资源不可用于单个物理机。这需要在网络上交换张量数据的协议。在分布式环境中,计算可以在不同类型的物理硬件上通过网络进行,有许多考虑因素,例如网络延迟或错误,处理器的可用性,调度和时间问题以及竞争的处理资源。在 ANN 中,计算必须按照一定的顺序进行。幸运的是,PyTorch 使用更高级的接口大部分隐藏了跨机器和处理器网络的每个计算的分配和时间安排的复杂机制。
PyTorch 有两个主要的包,分别处理分布式和并行环境的各个方面。这是除了我们之前讨论的 CUDA 之外。这些包如下:
-
torch.distributed
-
torch.multiprocessing
torch.distributed
使用torch.distributed
可能是最常见的方法。此包提供通信原语,如类,以检查网络中的节点数,确保后端通信协议的可用性,并初始化进程组。它在模块级别上运行。torch.nn.parallel.DistributedDataParallel()
类是一个容器,包装了一个 PyTorch 模型,使其继承torch.distributed
的功能。最常见的用例涉及多个进程,每个进程在其自己的 GPU 上操作,可以是本地的也可以是网络上的。通过以下代码初始化进程组到设备:
torch.distributed.init_process_group(backend='nccl', world_size=4, init_method='...')
这在每个主机上运行。后端指定了要使用的通信协议。NCCL(发音为 nickel)后端通常是最快且最可靠的。请注意,这可能需要安装在您的系统上。world_size
是作业中的进程数,init_method
是指向进程初始化位置和端口的 URL。这可以是网络地址,例如(tcp://...
),也可以是共享文件系统(file://... /...
)。
通过torch.cuda.set_devices(i)
可以设置设备。最后,我们可以使用以下代码短语来分配模型
model = distributedDataParallel(model, device_ids=[i], output_device=i
。这通常用于生成每个进程并将其分配给处理器的初始化函数。这确保每个进程通过使用相同的 IP 地址和端口通过主进程协调。
torch.multiprocessing
torch.multiprocessor
包是 Python 多处理包的替代品,使用方式完全相同,即作为基于进程的线程接口。它通过将 PyTorch 张量放入共享内存并仅发送其句柄到其他进程来扩展 Python 分布式包的方式之一。这是通过 multiprocessing.Queue
对象实现的。一般情况下,多进程是异步执行的;也就是说,特定设备的进程会被入队并在达到队列顶部时执行。每个设备按入队顺序执行进程,并且 PyTorch 在设备间复制时定期同步多个进程。这意味着对于多进程函数的调用者来说,进程是同步进行的。
编写多线程应用程序时的主要困难之一是避免死锁,即两个进程竞争一个资源的情况。这种常见情况是后台线程锁定或导入模块并分叉子进程时发生的。子进程很可能会以损坏的状态启动,导致死锁或其他错误。multiprocessingQueue
类本身会生成多个后台线程来发送、接收和序列化对象,这些线程也可能导致死锁。对于这些情况,可以使用无线程的 multiprocessingQueue.queues.SimpleQueue
。
优化技术
torch.optim
包含多种优化算法,每种算法都有几个参数可以用来微调深度学习模型。优化在深度学习中是一个关键组成部分,因此不同的优化技术对模型的性能可能至关重要。记住,它的作用是基于损失函数的计算梯度来存储和更新参数状态。
优化器算法
除了 SGD 外,PyTorch 中还提供了许多优化算法。以下代码展示了其中一种算法:
optim.Adadelta(params, lr=1.0, rho=0.9, eps=1e-06, weight_decay=0)
Adadelta
算法基于随机梯度下降;然而,不同于每次迭代保持相同的学习率,学习率会随时间调整。Adadelta
算法为每个维度维护单独的动态学习率。这可以使训练更快速和更有效率,因为与计算梯度相比,计算新学习率的开销相当小。Adadelta
算法在各种模型架构、大梯度和分布式环境中表现良好。Adadelta
算法特别适用于大型模型,并且在使用大初始学习率时效果显著。有两个与 Adadelta
相关的超参数我们还没有讨论。rho
用于计算平方梯度的运行平均值,这决定了衰减率。eps
超参数用于提高 Adadelta
的数值稳定性,如下代码所示:
optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0, initial_accumulater_value=0)
Adagrad
算法,或者适应性次梯度方法用于随机优化,是一种技术,它融入了在先前迭代中观察到的训练数据的几何知识。这使得算法能够发现罕见但高度预测的特征。Adagrad
算法使用自适应学习率,为频繁出现的特征赋予较低的学习率,为罕见特征赋予较高的学习率。这样做的效果是找到数据中罕见但重要的特征,并相应地计算每个梯度步长。学习率在每次迭代中对于更频繁出现的特征会更快地减小,对于罕见特征则减小得更慢,这意味着罕见特征在更多迭代中保持较高的学习率。Adagrad
算法通常对于稀疏数据集效果最佳。其应用示例如下代码所示:
optim.Adam(params, lr=0.001, betas(0.9,0.999), eps=1e-08, weight_decay=0, amsgrad=False)
Adam
算法(自适应矩估计)根据梯度的均值和未居中方差(梯度的第一和第二时刻)使用自适应学习率。类似于 Adagrad
,它存储过去平方梯度的平均值。它还存储这些梯度的衰减平均值。它在每次迭代时基于每个维度计算学习率。Adam
算法结合了 Adagrad
的优点,对稀疏梯度效果显著,并且能够在在线和非静态设置中良好工作。请注意,Adam
可以接受一个可选的 beta 参数元组。这些系数用于计算运行平均值及其平方的平均值。当设置 amsgrad
标志为 True
时,会启用 Adam
的一种变体,它结合了梯度的长期记忆。在某些情况下,这有助于收敛,而标准 Adam
算法则可能无法收敛。除了 Adam
算法之外,PyTorch 还包含 Adam
的两个变体。optim.SparseAdam
采用惰性参数更新,仅更新梯度中出现的时刻,并将其应用于参数。这提供了一种更有效的处理稀疏张量(如用于词嵌入的张量)的方法。第二个变体 optim.Adamax
使用无穷范数来计算梯度,理论上降低了对噪声的敏感性。在实践中,选择最佳优化器通常需要经过试验和错误。
下面的代码演示了 optim.RMSprop
优化器:
optim.RMSprop(params, lr=0.01, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0, centered = False)
RMSprop
算法将每个参数的学习率除以该特定参数最近梯度的平方的运行平均值。这确保每次迭代的步长与梯度的尺度相同。这样做可以稳定梯度下降,并减少消失或爆炸梯度的问题。alpha 超参数是一个平滑参数,有助于使网络对噪声具有鲁棒性。其用法可以在下面的代码中看到:
optim.Rprop(params, lr=0.01, etas(0.5,1.2), step_sizes(1e_06,50))
Rprop
算法(弹性反向传播)是一种自适应算法,通过使用每个权重的成本函数的偏导数的符号(而不是大小)来计算权重更新。这些独立地为每个权重计算。Rprop
算法接受一个元组对参数 etas
。这些是乘法因子,根据前一个迭代整体损失函数的导数的符号来增加或减少权重。如果最后一个迭代产生与当前导数相反的符号,则更新乘以元组中的第一个值,称为 etaminus
,一个小于 1 的值,默认为 0.5
。如果当前迭代的符号与上一个相同,则该权重更新乘以元组中的第二个值,称为 etaplis
,一个大于 1
的值,默认为 1.2
。通过这种方式,最小化总误差函数。
学习率调度器
torch.optim.lr_schedular
类作为一个包装器,根据一个特定函数乘以初始学习率来调度学习率。学习率调度器可以分别应用于每个参数组。这可以加快训练时间,因为通常我们能够在训练周期的开始使用更大的学习率,并在优化器接近最小损失时缩小此速率。一旦定义了调度器对象,通常会使用 scheduler.step()
来逐个 epoch 进行步进。PyTorch 提供了许多学习率调度器类,其中最常见的一个如下所示:
optim.lr_schedular.LambdaLR(optimizer, lr_lambda, last_epoch =-1)
此学习率调度器类采用一个函数,该函数乘以每个参数组的初始学习率,并且如果有多个参数组,则作为单个函数或函数列表传递。last_epoch
是最后一个时期的索引,因此默认值 -1
是初始学习率。以下是此类的示例截图,假设我们有两个参数组:
optim.lr_schedular.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)
每 step_size
个 epoch 将学习率按乘法因子 gamma
减少。
optim.lr_schedular.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)
接受一个里程碑列表,以 epoch 数量度量,当学习率减少 gamma
时。milestones
是一个递增的 epoch
索引列表。
参数组
当实例化优化器时,它是以及一些超参数,如学习率。优化器还传递了其他每种优化算法特定的超参数。设置这些超参数的组合可以极大地有助于设置模型的不同部分。通过创建参数组,实质上是一个可以传递给优化器的字典列表,可以实现这一点。
param
变量必须是 torch.tens
的迭代器或指定优化选项默认值的 Python 字典。请注意,参数本身需要指定为有序集合,例如列表,以便在模型运行之间保持参数一致的顺序。
可以将参数指定为参数组。考虑下面截图中显示的代码:
param_groups
函数返回包含权重和优化器超参数的字典列表。我们已经讨论了学习率。SGD 优化器还具有几个其他超参数,可用于微调您的模型。momentum
超参数修改 SGD 算法,帮助加速梯度张量朝向最优点,通常导致更快的收敛。momentum
默认为 0
;然而,使用较高的值,通常在 0.9
左右,往往会导致更快的优化。这在处理嘈杂数据时特别有效。它通过计算数据集的移动平均来工作,有效地平滑数据,从而改善优化效果。dampening
参数可以与 momentum
一起使用作为抑制因子。weight_decay
参数应用 L2 正则化。这向损失函数添加了一个项,效果是缩小参数估计,使模型更简单,更不易过拟合。最后,nestrove
参数基于未来的权重预测计算动量。这使得算法可以向前看,通过计算一个梯度,不是针对当前参数,而是针对近似未来参数。
我们可以使用 param_groups
函数将不同的参数集分配给每个参数组。考虑下面截图中显示的代码:
在这里,我们创建了另一个权重 w2
,并将其分配给一个参数组。请注意,在输出中我们有两组超参数,每个参数组一个。这使我们能够设置特定于权重的超参数,例如允许在网络的每一层中应用不同的选项。我们可以访问每个参数组并更改参数值,使用其列表索引,如下面截图中的代码所示:
预训练模型
图像分类模型的主要困难之一是缺乏标记数据。组装足够大小的标记数据集以很好地训练模型是困难的;这是一项非常耗时且费力的任务。对于 MNIST 来说,这并不是问题,因为这些图像相对简单。它们是灰度的,主要由目标特征组成,没有令人分心的背景特征,所有图像都对齐在同一方式,并且是相同比例的。一个包含 60,000 张图像的小数据集已经足够很好地训练模型。在我们在现实生活中遇到的问题中,很难找到这样一个组织良好且一致的数据集。图像的质量通常是可变的,目标特征可能被遮蔽或扭曲。它们还可以具有广泛可变的尺度和旋转。解决方案是使用一个在非常大的数据集上预训练的模型架构。
PyTorch 包含六种基于卷积网络的模型架构,专为处理分类或回归任务中的图像设计。以下列表详细描述了这些模型:
-
AlexNet:这个模型基于卷积网络,通过并行化操作在处理器之间实现显著的性能改进。其原因在于,卷积层的操作与卷积网络的线性层中发生的操作有所不同。卷积层大约占总计算量的 90%,但只操作 55%的参数。对于完全连接的线性层,情况相反,它们占大约 5%的计算量,但包含大约 95%的参数。AlexNet 使用不同的并行化策略来考虑线性层和卷积层之间的差异。
-
VGG:用于大规模图像识别的非常深度卷积网络(VGG)的基本策略是增加层数的深度,同时对所有卷积层使用一个 3 x 3 的接受领域的非常小过滤器。所有隐藏层都包括 ReLU 非线性,输出层由三个完全连接的线性层和一个 softmax 层组成。VGG 架构提供
vgg11
、vgg13
、vgg16
、vgg19
、vgg11_bn
、vgg13_bn
、vgg16_bn
和vgg19_bn
几种变体。 -
ResNet:虽然非常深的网络可能提供更强的计算能力,但它们很难进行优化和训练。非常深的网络通常会导致梯度消失或爆炸。ResNet 使用残差网络,其中包括快捷跳过连接以跳过某些层。这些跳过层具有可变权重,因此在初始训练阶段,网络有效地折叠成几层,随着训练的进行,随着新特征的学习,层数会扩展。ResNet 提供
resnet18
、resnet34
、resnet50
、resnet101
和resnet152
几种变体。 -
SqueezeNet:SqueezeNet 旨在创建更小、参数更少且更易于在分布式环境中导出和运行的模型。这是通过三种策略实现的。首先,将大多数卷积的感受野从 3 x 3 减少到 1 x 1。其次,将输入通道减少到剩余的 3 x 3 过滤器中。第三,对网络的最终层进行下采样。SqueezeNet 可在
squeezenet1_0
和squeezenet1_1
变体中找到。 -
DenseNet:密集卷积网络与标准 CNN 相比,其中权重从输入传播到输出的每一层,每一层使用所有前一层的特征图作为输入。这导致层之间的连接更短,网络鼓励参数的重复使用。这样可以减少参数数量并增强特征的传播。DenseNet 可在
Densenet121
、Densenet169
和Densenet201
变体中找到。 -
Inception:这种架构使用多种策略来提高性能,包括通过逐渐减少输入和输出之间的维度来减少信息瓶颈,将卷积从较大的感受野因子化为较小的感受野,以及平衡网络的宽度和深度。最新版本是
inception_v3
。重要的是,Inception 要求图像大小为 299 x 299,与其他模型要求的 224 x 224 不同。
这些模型可以通过简单调用其构造函数以随机权重初始化,例如 model = resnet18()
。要初始化预训练模型,请设置布尔值 pretrained= True
,例如 model = resnet18(pretrained=True)
。这将加载预加载的权重数值数据集。这些权重通过在 Imagenet
数据集上训练网络来计算。该数据集包含超过 1400 万张图像和超过 100 个索引。
许多这些模型架构具有多种配置,例如 resnet18
、resnet34
、vgg11
和 vgg13
。这些变体利用层深度、标准化策略和其他超参数的差异。找出哪种最适合特定任务需要进行一些实验。
还要注意,这些模型设计用于处理图像数据,需要 RGB 图像格式为 (3, W, H)
。输入图像需要调整大小为 224 x 224,除了 Inception,它需要大小为 299 x 299 的图像。重要的是,它们需要以非常特定的方式进行归一化。可以通过创建一个 normalize
变量并将其传递给 torch.utils.data.DataLoader
来完成这一点,通常作为 transforms.compose()
对象的一部分。非常重要的是,normalize
变量必须精确地具有以下值:
normalize=transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
这确保了输入图像与它们在 Imagenet
数据集上训练的相同分布。
实现预训练模型
还记得我们在《PyTorch 入门》的第一章中玩过的 Guiseppé玩具数据集吗?现在我们终于有了工具和知识来创建这些数据的分类模型。我们将使用在Imagenet
数据集上预训练的模型来实现这一目标。这被称为迁移学习,因为我们正在将在一个数据集上学到的知识迁移到另一个(通常要小得多的)数据集上进行预测。使用具有预训练权重的网络显著提高了在较小数据集上的性能,而且这是非常容易实现的。在最简单的情况下,我们可以将预训练模型传递给带标签图像的数据,并简单地更改输出特征的数量。请记住,Imagenet
有100
个索引或潜在标签。对于我们的任务,我们希望将图像分类为三类:toy
、notoy
和scenes
。因此,我们需要将输出特征的数量分配为三。
下图中的代码是基于 Sasank Chilamkurthy 的迁移学习教程中的代码进行调整的,可在chsasank.github.io
找到。
首先,我们需要导入数据。这些数据可以从本书的网站(.../toydata
)获取。将该文件解压缩到您的工作目录中。实际上,您可以使用任何您喜欢的图像数据,只要它具有相同的目录结构:即用于训练和验证集的两个子目录,并在这两个目录中,为每个类别建立子目录。您可能还想尝试的其他数据集包括蜜蜂和蚂蚁的 hymenoptera 数据集,可在download.pytorch.org/tutorial/hymenoptera_data.zip
获取,以及来自torchvision/datasets
的 CIFAR-10 数据集,或者更大更具挑战性的植物种子数据集,包含 12 个类别,可在www.kaggle.com/c/plant-seedlings-classification
获取。
我们需要对训练和验证数据集应用单独的数据转换,导入并使数据集可迭代,然后根据下图中的代码将设备分配给 GPU(如果可用):
注意,字典用于存储两个compose
对象列表,以便转换训练和验证集。使用RandomResizedCrop
和RandomHorizontalFlip
转换来增强训练集。对于训练和验证集,图像都被调整大小并居中裁剪,并且应用了上一节讨论的特定标准化值。
数据使用字典推导进行解包。这使用datasets.Imagefolder
类,这是一个通用的数据加载器,用于在数据组织为其类文件夹的情况下使用。在本例中,我们有三个文件夹,NoToy
、Scenes
和SingleToy
,分别对应它们的类。这种目录结构在val
和train
目录中都有。共有 117 张训练图像和 24 张验证图像,分为三个类。
通过调用ImageFolder
的classes
属性,我们可以简单地检索类名,如下面截图中所示的代码所示:
可以使用以下截图中的代码检索一批图像及其类索引:
inputs
张量的大小为(batch, RGB, W,H)
。第一个大小为4
的张量包含0
(NoToy
)、1
(Scenes
)或2
(SingleToy
),表示批次中每个图像的类。可以使用以下列表推导来检索批次中每个图像的类名:
现在,让我们看一下用于训练模型的函数。这与我们先前的训练代码结构类似,但有一些增加。训练分为两个阶段,train
和val
。此外,需要在train
阶段的每个epoch
中调整学习率调度器,如下面截图中的代码所示:
train_model
函数的参数包括模型、损失标准、学习率调度器和 epoch 数。模型权重通过深复制model.state_dict()
来存储。深复制确保所有状态字典的元素都被复制,而不仅仅是引用到best_model_wts
变量中。每个 epoch 分为两个阶段,训练阶段和验证阶段。在验证阶段,使用model.eval()
将模型设置为评估模式。这会改变一些模型层的行为,通常是 dropout 层,将 dropout 概率设置为零以验证完整模型。每个 epoch 打印训练和验证阶段的准确性和损失。完成后打印最佳验证准确性。
在运行训练代码之前,我们需要实例化模型并设置优化器、损失标准和学习率调度器。在这里,我们使用resnet18
模型,如下面截图中的代码所示。这段代码适用于所有resnet
变体,尽管准确性可能有所不同:
该模型使用了所有权重(不包括输出层),这些权重在Imagenet
数据集上进行了训练。我们只需要改变输出层,因为所有隐藏层的权重都处于预训练状态并被冻结。这通过设置输出层为具有输出设置为我们预测的类数的线性层来完成。输出层本质上是我们正在处理的数据集的特征提取器。在输出时,我们尝试提取的特征就是类本身。
我们可以通过简单运行print(model)
来查看模型的结构。最后一层被命名为fc
,因此我们可以通过model.fc
访问该层。这被分配了一个线性层,并传递了输入特征的数量,通过fc.in_features
访问,并且输出类的数量,在这里设置为3
。当我们运行这个模型时,我们能够达到大约 90%的准确率,这实际上是相当令人印象深刻的,考虑到我们使用的小数据集。这是可能的,因为除了最后一层之外,大部分训练都是在一个更大的训练集上完成的。
可以通过对训练代码进行少量更改来使用其他预训练模型,这是一种值得的练习。例如,DenseNet 模型可以直接替换 ResNet,只需将输出层的名称从fc
更改为classifier
,所以我们不再写model.fc
而是写model.classifier
。SqueezeNet、VGG 和 AlexNet 将它们的最后层包装在一个顺序容器中,所以要更改输出fc
层,我们需要经历以下四个步骤:
-
找出输出层中过滤器的数量
-
将顺序对象中的层转换为列表,并移除最后一个元素
-
在列表末尾添加最后一个线性层,指定输出类的数量
-
将列表转换回顺序容器并将其添加到模型类中
对于vgg11
模型,可以使用以下代码来实现这四个步骤:
摘要
现在您已经对深度学习的基础有了理解,应该能够将这些知识应用于您感兴趣的特定学习问题上。在本章中,我们已经开发了一个使用预训练模型进行图像分类的开箱即用解决方案。正如您所见,这个实现相当简单,并且可以应用于您能想到的几乎任何图像分类问题。当然,每种情况下的实际表现将取决于所呈现的图像数量和质量,以及与每个模型和任务相关的超参数的精确调整。
通过简单地使用预训练模型和默认参数,通常可以在大多数图像分类任务上获得非常好的结果。这不需要理论知识,除了安装程序的运行环境。当你调整一些参数时,你会发现可以改善网络的训练时间和/或准确率。例如,你可能已经注意到,增加学习率可能会在少数 epochs 内显著提高模型的性能,但在后续 epochs 中,准确率实际上会下降。这是梯度下降过冲的一个例子,未能找到真正的最优解。要找到最佳的学习率,需要一些梯度下降的知识。
要充分利用 PyTorch,并将其应用于不同的问题领域——如语言处理、物理建模、天气和气候预测等(应用几乎无穷无尽)——你需要对这些算法背后的理论有一定的了解。这不仅可以改进已知任务,如图像分类,还可以让你洞察深度学习在某些情况下的应用,例如输入数据为时间序列,任务是预测下一个序列。阅读本书后,你应该知道解决方案,也就是使用递归网络。你会注意到,我们用来生成文本的模型——即在序列上做出预测的模型——与用来对静态图像数据进行预测的模型有很大不同。但是,为了帮助你洞察特定过程而建立的模型又是什么样的呢?这可能是网站上的电子流量、道路网络上的物理流量、地球的碳氧循环或人类生物系统。这些是深度学习的前沿,具有巨大的潜力去创造好的影响。希望阅读这篇简短介绍让你感到自信,并激发你开始探索其中一些应用。