PyTorch-1-x-深度学习指南第二版-全-
PyTorch 1.x 深度学习指南第二版(全)
原文:
zh.annas-archive.org/md5/3913e248efb5ce909089bb46b2125c26
译者:飞龙
前言
PyTorch 因其易用性、高效性以及更符合 Python 开发方式而吸引了深度学习研究人员和数据科学专业人员的关注。本书将帮助您快速掌握 PyTorch 这一最尖端的深度学习库。
在第二版中,您将了解使用 PyTorch 1.x 库的新功能和提供的各种基础构建模块,以推动现代深度学习的发展。您将学习如何使用卷积神经网络(CNNs)、循环神经网络(RNNs)和长短期记忆网络(LSTM)解决实际问题。接着,您将掌握各种最先进的现代深度学习架构的概念,如 ResNet、DenseNet 和 Inception。您将学习如何将神经网络应用于计算机视觉、自然语言处理(NLP)等各个领域。您将了解如何使用 PyTorch 构建、训练和扩展模型,并深入探讨生成网络和自编码器等复杂神经网络。此外,您还将了解 GPU 计算以及如何利用 GPU 进行大规模计算。最后,您将学习如何使用基于深度学习的架构解决迁移学习和强化学习问题。
在本书的最后,您将能够轻松在 PyTorch 中实现深度学习应用。
本书适合谁
本书适合希望使用 PyTorch 1.x 探索深度学习算法的数据科学家和机器学习工程师。那些希望迁移到 PyTorch 1.x 的人会发现本书富有洞见。为了充分利用本书,具备 Python 编程的工作知识和一些机器学习知识将非常有帮助。
本书内容涵盖了什么
第一章,使用 PyTorch 开始深度学习,介绍了深度学习、机器学习和人工智能的历史。本章涵盖了它们与神经科学以及统计学、信息理论、概率论和线性代数等科学领域的关系。
第二章,神经网络的构建模块,涵盖了使用 PyTorch 理解和欣赏神经网络所需的各种数学概念。
第三章,深入探讨神经网络,向您展示如何将神经网络应用于各种现实场景。
第四章,计算机视觉中的深度学习,涵盖了现代 CNN 架构的各种构建模块。
第五章,使用序列数据进行自然语言处理,向您展示如何处理序列数据,特别是文本数据,并教您如何创建网络模型。
第六章,实现自编码器,通过自编码器的介绍介绍了半监督学习算法的概念。还涵盖了如何使用受限玻尔兹曼机理解数据的概率分布。
第七章,生成对抗网络的应用,展示了如何构建能够生成文本和图像的生成模型。
第八章,现代网络架构下的迁移学习,介绍了现代架构如 ResNet、Inception、DenseNet 和 Seq2Seq,并展示了如何使用预训练权重进行迁移学习。
第九章,深度强化学习,从强化学习的基本介绍开始,包括代理、状态、动作、奖励和策略的覆盖。还包括基于深度学习的强化学习问题的实用代码,如 Deep Q 网络、策略梯度方法和演员-评论家模型。
第十章,接下来做什么?,快速概述了本书涵盖的内容,并提供了如何跟上领域最新进展的信息。
要充分利用这本书
熟悉 Python 将会很有帮助。
下载示例代码文件
您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.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-1.x
。如果代码有更新,将在现有的 GitHub 仓库中更新。
我们还提供了来自我们丰富图书和视频目录的其他代码包,都可以在github.com/PacktPublishing/
查看!
下载彩色图像
我们还提供了一份包含本书中使用的屏幕截图/图表的彩色图像的 PDF 文件。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781838553005_ColorImages.pdf
。
使用的约定
本书中使用了许多文本约定。
CodeInText
:指示文本中的代码词汇,数据库表名,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。例如:“让我们使用简单的 Python 函数,如 split
和 list
,将文本转换为标记。”
代码块设置如下:
toy_story_review = "Just perfect. Script, character, animation....this manages to break free of the yoke of 'children's movie' to simply be one of the best movies of the 90's, full-stop."
print(list(toy_story_review))
当我们希望引起您对代码块特定部分的注意时,相关行或项将加粗显示:
['J', 'u', 's', 't', ' ', 'p', 'e', 'r', 'f', 'e', 'c', 't', '.', ' ', 'S', 'c', 'r', 'i', 'p', 't', ',', ' ', 'c', 'h', 'a', 'r', 'a', 'c', 't', 'e', 'r', ',', ' ', 'a', 'n', 'i', 'm', 'a', 't', 'i', 'o', 'n', '.', '.', '.', '.', 't', 'h', 'i', 's', ' ', 'm', 'a', 'n', 'a', 'g', 'e', 's', ' ', 't', 'o', ' ', 'b', 'r', 'e', 'a', 'k', ' ', 'f', 'r', 'e', 'e', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', 'y', 'o', 'k', 'e', ' ', 'o', 'f', ' ', "'", 'c', 'h', 'i', 'l', 'd', 'r', 'e', 'n', "'", 's', ' ', 'm', 'o', 'v', 'i', 'e', "'", ' ', 't', 'o', ' ', 's', 'i', 'm', 'p', 'l', 'y', ' ', 'b', 'e', ' ', 'o', 'n', 'e', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', 'b', 'e', 's', 't', ' ', 'm', 'o', 'v', 'i', 'e', 's', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', '9', '0', "'", 's', ',', ' ', 'f', 'u', 'l', 'l', '-', 's', 't', 'o', 'p', '.']
任何命令行输入或输出都写成以下格式:
pip install torchtext
粗体:表示新术语,重要词汇或屏幕上显示的词语。例如,菜单或对话框中的词语在文本中显示为这样。这是一个例子:“我们将帮助您理解递归神经网络(RNNs)。”
警告或重要提示看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们非常欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在消息主题中提及书名,并发送电子邮件至 customercare@packtpub.com
。
勘误:尽管我们已尽一切努力确保内容的准确性,但错误偶尔也会发生。如果您在本书中发现错误,请向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并填写详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法副本,我们将不胜感激,如果您能提供给我们具体位置或网站名称的信息。请联系我们,发送至 copyright@packt.com
,并附上材料的链接。
如果您有兴趣成为作者:如果您对某个您专业的主题感兴趣,并且您有意参与撰写或贡献书籍,请访问 authors.packtpub.com。
评论
请留下您的评论。一旦您阅读并使用了本书,请为您购买的网站留下评论,以便潜在读者可以看到并使用您的客观意见来做出购买决策,我们在 Packt 可以了解您对我们产品的看法,而我们的作者可以看到您对他们书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packt.com。
第一部分:构建 PyTorch 1.x 深度学习的基础模块
在本节中,你将会介绍深度学习的概念以及各种深度学习框架。
本节包含以下章节:
-
第一章,使用 PyTorch 入门深度学习
-
第二章,神经网络的构建模块
第一章:使用 PyTorch 入门深度学习
深度学习(DL)已经彻底改变了一个又一个行业。安德鲁·吴曾在 Twitter 上著名地描述它如下:
"人工智能是新的电力!"
电力改变了无数行业;现在,人工智能(AI)也将如此。
AI 和 DL 被用作同义词,但两者之间存在实质性的区别。让我们揭开行业术语的神秘面纱,这样作为从业者的你就能够区分信号和噪音。
在本章中,我们将涵盖 AI 的以下不同部分:
-
探索人工智能
-
在现实世界中的机器学习
-
深度学习的应用
-
深度学习框架
-
设置 PyTorch 1.x
探索人工智能
每天都有无数篇讨论 AI 的文章发表。过去两年这一趋势有所增加。网络上有许多关于 AI 的定义,我最喜欢的是智能任务的自动化,通常由人类执行。
AI 的历史
自从你拿起这本书,你可能已经对 AI 的最近热潮有所了解。但一切都始于约翰·麦卡锡,当时是达特茅斯学院的年轻助理教授,他在 1995 年创造了术语人工智能,并将其定义为涉及智能机器科学和工程的领域。这掀起了 AI 的第一波浪潮,主要由符号推理驱动;其成果令人惊叹不已。在此期间开发的 AI 能够阅读和解决高中代数问题[STUDENT]、证明几何定理[SAINT]以及学习英语语言[SHRDLU]。符号推理是复杂规则嵌套在 if-then 语句中的使用。
然而,在这个时代最有前途的工作是感知器,由 Frank Rosenblatt 于 1958 年引入。感知器与后来发现的智能优化技术结合,为我们今天所知的深度学习奠定了基础。
AI 并非一帆风顺,由于初期发现过度宣称以及缺乏数据和计算能力,领域内的资金显著减少。然而,机器学习(ML)在九十年代初的突出表现扭转了这一趋势,并在该领域引发了极大兴趣。首先,我们需要了解 ML 的范式及其与 DL 的关系。
在现实世界中的机器学习
ML 是 AI 的一个子领域,利用算法和统计技术执行任务,无需任何明确的指令,而是依赖于数据中的统计模式。
要构建成功的机器学习模型,我们需要为 ML 算法提供标记数据。这种方法的成功在很大程度上依赖于可用的数据和计算能力,以便能够使用大量数据。
那么,为什么要用 DL?
大多数 ML 算法在结构化数据上表现良好,比如销售预测、推荐系统和营销个性化。对于任何 ML 算法来说,特征工程都是一个重要因素,数据科学家需要花费大量时间探索可能对 ML 算法有高预测力的特征。在某些领域,如计算机视觉和自然语言处理(NLP),特征工程具有挑战性,因为对于一个任务重要的特征可能对其他任务效果不佳。这就是 DL 的优势所在——算法本身在非线性空间中工程化特征,使其对特定任务至关重要。
当数据稀缺时,传统的 ML 算法仍然优于 DL 方法,但随着数据增加,传统机器学习算法的性能往往会趋于平稳,而深度学习算法则往往会显著优于其他学习策略。
以下图示展示了 DL 与 ML 和 AI 的关系:
总结一下,DL 是机器学习的一个子领域;特征工程是算法非线性地探索其空间的地方。
深度学习的应用
DL 是 21 世纪最重要创新的中心,从检测肿瘤的误差率低于放射科医生到自动驾驶汽车。让我们快速看一些 DL 应用。
文字自动翻译图像
2015 年谷歌的一篇博客详细介绍了谷歌团队如何从图像中翻译文本。以下图片展示了相关步骤:
首先,DL 算法用于执行光学字符识别(OCR)并识别图像中的文本。随后,另一个 DL 算法用于将文本从源语言翻译到选择的语言。我们今天看到的机器翻译的改进归因于从传统方法转向 DL。
自动驾驶车辆中的目标检测
特斯拉在 2019 年向投资者深入介绍了他们的自动驾驶系统,提到了他们如何使用深度神经网络从车辆摄像头中检测物体。该算法的输出被特斯拉开发的专有自动驾驶策略所使用。
前面的图片是一个目标检测深度学习网络的输出。它从视觉图像中捕获的语义信息对于自动驾驶任务至关重要。
深度学习框架
以前编写深度学习算法的代码非常困难,因为编写学习步骤的代码(涉及复杂导数链的链接)非常容易出错且冗长。深度学习框架使用巧妙的启发式算法自动计算这些复杂导数。选择这种启发式显著改变了这些框架的工作方式。以下图表显示了当前的深度学习框架生态系统:
TensorFlow 是最流行的深度学习框架,但 PyTorch 的简洁和实用性使得深度学习研究对许多人更加可接近。让我们看看为什么使用 PyTorch 可以显著加速我们的深度学习研究和开发时间。
为什么选择 PyTorch?
TensorFlow 使用定义然后运行的范式来计算复杂的链式导数,而 PyTorch 则使用更聪明的定义即运行范式。让我们通过查看下面的图像深入探讨这个问题,我们将计算系列1 + 1 / 2 + 1 / 4 + 1 / 8 ...的总和,结果应该是 2:
我们可以立即看到,在 PyTorch 中编写操作的代码是多么简洁和简单。在更复杂的场景中,这种差异更加显著。
作为特斯拉人工智能部门的负责人和当前计算机视觉领域最重要的思想领袖之一,Andrej Karpathy 发推文说:“我现在已经使用 PyTorch 几个月了,感觉从未如此之好。我更有精力了。我的皮肤更清爽了。我的视力也有所改善。” PyTorch 绝对使得编写深度学习代码的人们生活更加美好。
这种定义即运行的范式除了创建更清晰和简单的代码之外还有许多其他优点。调试也变得极其容易,你当前用于调试 Python 代码的所有工具也同样适用于 PyTorch。这是一个重大优势,因为随着网络变得越来越复杂,轻松调试您的网络将是救命稻草。
PyTorch v1.x 的新功能有哪些?
PyTorch 1.x 在其灵活性上有所扩展,并试图将研究和生产能力统一到一个框架中。Caffe2,一个生产级深度学习框架,已集成到 PyTorch 中,使我们能够将 PyTorch 模型部署到移动操作系统和高性能 C++服务中。PyTorch v1.0 还原生支持将模型导出为 ONNX 格式,这使得 PyTorch 模型可以导入其他深度学习框架。对于 PyTorch 开发者来说,现在真是令人兴奋的时刻!
CPU 与 GPU
CPU 具有较少但更强大的计算核心,而 GPU 具有大量的性能较低的核心。CPU 更适合顺序任务,而 GPU 适合具有显著并行性的任务。总之,CPU 可以执行大型的顺序指令,但在并行执行少量指令方面不如 GPU,后者可以并行执行数百个小指令:
在使用 DL 时,我们将执行大量线性代数操作,这些操作更适合于 GPU,并且可以显著提升神经网络训练所需的时间。
什么是 CUDA?
CUDA 是由 NVIDIA 开发的框架,允许我们在图形处理单元(GPU)上进行通用计算。它是用 C++编写的广泛使用的框架,允许我们编写在 GPU 上运行的通用程序。几乎所有深度学习框架都利用 CUDA 在 GPU 上执行指令。
我们应该使用哪些 GPU?
由于大多数深度学习框架,包括 PyTorch,使用 NVIDIA 的 CUDA 框架,强烈建议您购买和使用 NVIDIA GPU 进行深度学习。让我们快速比较几个 NVIDIA GPU 型号:
如果没有 GPU,你该怎么办?
有很多云服务,如 Azure、AWS 和 GCP,提供预装有 GPU 和所有必要深度学习软件的实例。FloydHub 是在云中运行深度学习模型的好工具。然而,您绝对应该了解的最重要的工具是 Google 的 Colaboratory,它提供高性能的 GPU 免费供您运行深度学习模型。
设置 PyTorch v1.x
在本书中,我们将使用 Anaconda Distribution 进行 Python 和 PyTorch 1.x 开发。您可以通过访问官方 PyTorch 网站(pytorch.org/get-started/locally/
)根据您当前的配置执行相关命令来跟随代码。
安装 PyTorch
PyTorch 作为 Python 包可用,您可以使用pip
或conda
构建它。或者,您可以从源代码构建。本书推荐使用 Anaconda Python 3 发行版。要安装 Anaconda,请参考 Anaconda 官方文档 conda.io/docs/user-guide/install/index.html
。本书的所有示例将作为 Jupyter Notebooks 提供在该书的 GitHub 存储库中。我强烈建议您使用 Jupyter Notebook,因为它允许您进行交互式实验。如果您已经安装了 Anaconda Python,则可以继续执行以下 PyTorch 安装说明。
对于基于 GPU 的安装和 Cuda 8,请使用以下命令:
conda install pytorch torchvision cuda80 -c soumith
对于基于 GPU 的安装和 Cuda 7.5,请使用以下命令:
conda install pytorch torchvision -c soumith
对于非基于 GPU 的安装,请使用以下命令:
conda install pytorch torchvision -c soumith
在撰写本文时,PyTorch 不支持 Windows 机器,因此您可以尝试虚拟机(VM)或 Docker 镜像。
总结
在这一章中,我们学习了人工智能的历史,为什么使用深度学习,深度学习生态系统中的多个框架,PyTorch 为何是一个重要工具,为何我们在深度学习中使用 GPU,并且如何设置 PyTorch v1.0。
在下一章中,我们将深入研究神经网络的构建模块,并学习如何编写 PyTorch 代码来进行训练。
第二章:神经网络的基本构建模块
理解神经网络的基本构建模块,如张量、张量操作和梯度下降,对于构建复杂的神经网络至关重要。在本章中,我们将对神经网络进行一般性概述,同时深入探讨 PyTorch API 的基础。神经网络的原始想法受到人脑中的生物神经元的启发,但在撰写本文时,二者之间的相似性仅仅是表面的,对这两个系统的任何比较可能导致对其中任何一个系统的错误假设。因此,我们不会深究这两个系统之间的相似之处,而是直接深入探讨用于 AI 中的神经网络的解剖学。
在本章中,我们将涵盖以下主题:
-
什么是神经网络?
-
在 PyTorch 中构建神经网络
-
理解 PyTorch 张量
-
理解张量操作
什么是神经网络?
简而言之,神经网络是一种学习输入变量与其关联目标变量之间关系的算法。例如,如果您有一个数据集,其中包含学生的 GPA、GRE 分数、大学排名以及学生的录取状态,我们可以使用神经网络来预测学生在给定其 GPA、GRE 分数和大学排名的情况下的录取状态(目标变量):
在前述图中,每个箭头代表一个权重。这些权重是从训练数据的实例中学习到的,{ ( (x1, y1), (x2, y2),..., (xm, ym) ) },以便从操作中创建的复合特征能够预测学生的录取状态。
例如,网络可以学习 GPA/GRE 在院校排名中的重要性,如下图所示:
理解神经网络的结构
神经网络中的操作由两个基础计算构建。一个是权重向量与其对应的输入变量向量之间的点积,另一个是将产品转换为非线性空间的函数。我们将在下一章节学习几种这些函数的类型。
让我们进一步分解:第一个点积学习到一个混合概念,因为它创建了依赖于每个输入变量重要性的输入变量的混合。将这些重要特征传递到非线性函数中允许我们构建比仅使用传统线性组合更强大的输出:
通过这些操作作为构建模块,我们可以构建健壮的神经网络。让我们来详细分析之前的神经网络示例;神经网络学习关于特征的信息,这些特征反过来是目标变量的最佳预测因子。因此,神经网络的每一层都学习到可以帮助神经网络更好地预测目标变量的特征:
在前面的图表中,我们可以看到如何使用神经网络来预测房屋的价格。
在 PyTorch 中构建神经网络
让我们从在 PyTorch 中构建一个神经网络开始,它将帮助我们预测大学生的录取状态。在 PyTorch 中有两种构建神经网络的方式。首先,我们可以使用更简单的torch.nn.Sequential
类,它允许我们将我们期望的神经网络操作序列作为参数传递给实例化我们的网络。
另一种方式,这是一种更复杂、更强大但优雅的方法,是将我们的神经网络定义为从torch.nn.Module
类继承的类:
我们将利用这两种模式来构建我们的神经网络,这两种模式都是由 PyTorch API 定义的。
PyTorch 顺序神经网络
神经网络中所有常用的操作都在torch.nn
模块中可用。因此,我们需要从引入所需模块开始:
import torch
import torch.nn as nn
现在,让我们看看如何使用torch.nn.Sequential
类构建神经网络。我们使用在torch.nn
模块中定义的操作,并按顺序将它们作为参数传递给torch.nn.Sequential
类,以实例化我们的神经网络。在我们导入操作之后,我们的神经网络代码应该如下所示:
My_neuralnet = nn.Sequential(operationOne,operationTwo…)
在构建神经网络时最常用的操作是nn.Linear()
操作。它接受两个参数:in_features
和out_features
。in_features
参数是输入的大小。在我们的情况下,我们有三个输入特征:GPA、GRE 和大学排名。out_features
参数是输出的大小,对于我们来说是两个,因为我们想要从输入中学习两个特征,以帮助我们预测学生的录取状态。本质上,nn.Linear(in_features, out_features)
操作接受输入并创建权重向量以执行点积。
在我们的情况下,nn.Linear(in_features = 3, out_features = 2)
会创建两个向量:[w11, w12, w13] 和 [w21, w22, w23]。当输入 [xGRE, xGPA, xrank] 被传递到神经网络时,我们将创建一个包含两个输出的向量 [h1, h2],其结果为 [w11 . xGRE + w12 . xGPA + w13 . xrank , w21 . xGRE + w22 . xGPA + w23 . xrank]。
当你想要继续向你的神经网络中添加更多层时,这种模式会继续下游。下图显示了被转换为nn.Linear()
操作后的神经网络结构:
很好!但是添加更多的线性操作并不能充分利用神经网络的能力。我们还必须使用几种非线性函数之一将这些输出转换为非线性空间。这些函数的类型以及每个函数的优点和缺点将在下一章节中更详细地描述。现在,让我们使用其中一种最常用的非线性函数之一,即 修正线性单元,也称为 ReLU。PyTorch 通过调用 nn.ReLU()
提供了一个内置的 ReLU 操作符。以下图展示了非线性函数如何分类或解决线性函数失败的学习问题:
最后,为了获得我们的预测结果,我们需要将输出压缩到 0 到 1 之间。这些状态分别指非录取和录取。Sigmoid 函数,如下图所示,是将连续量转换为介于 0 和 1 之间的值最常用的函数。在 PyTorch 中,我们只需调用 nn.Sigmoid()
操作:
现在,让我们在 PyTorch 中将我们的神经网络代码整合起来,以便获得一个结构如下图所示的网络:
执行这个操作的代码如下:
import torch
import torch.nn as nn
my_neuralnet = nn.Sequential(nn.Linear(3,2),
nn.ReLU(),
nn.Linear(2, 1),
nn.Sigmoid())
就是这样!在 PyTorch 中组合一个神经网络就是这么简单。my_neuralnet
Python 对象包含了我们的神经网络。稍后我们将看看如何使用它。现在,让我们看看如何使用基于定义从 nn.Module
类继承的类的更高级 API 来构建神经网络。
使用 nn.Module
构建 PyTorch 神经网络
使用 nn.Module
类定义神经网络也是简单而优雅的。它通过定义一个将继承自 nn.Module
类并重写两个方法的类开始:__init__()
和 forward()
方法。__init__()
方法应包含我们期望的神经网络层中的操作。另一方面,forward()
方法应描述数据通过这些期望的层操作的流动。因此,代码的结构应类似于以下内容:
class MyNeuralNet(nn.Module):
# define the __init__() method
def __init__(self, other_features_for_initialization):
# Initialize Operations for Layers
# define the forward() method
def forward(self, x):
# Describe the flow of data through the layers
让我们更详细地了解这种模式。class
关键字帮助定义一个 Python 类,后面跟着你想要为你的类使用的任意名称。在这种情况下,它是 MyNeuralNet
。然后,括号中传递的参数是我们当前定义的类将继承的类。因此,我们始终从 MyNeuralNet(nn.Module)
类开始。
self
是传递给类中定义的每个方法的任意第一个参数。它表示类的实例,并可用于访问类中定义的属性和方法。
__init__()
方法是 Python 类中的一个保留方法。它也被称为构造函数。每当实例化类的对象时,__init__()
方法中包装的代码将被运行。这帮助我们一旦实例化了我们的神经网络类的对象,就设置好所有的神经网络操作。
需要注意的一点是,一旦我们在神经网络类内部定义了 __init__()
方法,我们就无法访问 nn.Module
类的 __init__()
方法中定义的所有代码了。幸运的是,Python 的 super()
函数可以帮助我们运行 nn.Module
类中的 __init__()
方法中的代码。我们只需要在新的 __init__()
方法的第一行中使用 super()
函数。使用 super()
函数来访问 __init__()
方法非常简单;我们只需使用 super(NameOfClass, self).__init__()
。在我们的情况下,这将是 super(MyNeuralNet, self).__init__()
。
现在我们知道如何编写我们的 __init__()
方法的第一行代码,让我们看看我们需要在 __init__()
方法的定义中包含哪些其他代码。我们必须将 PyTorch 中定义的操作存储为 self
的属性。在我们的情况下,我们有两个 nn.Linear
操作:一个从输入变量到神经网络层中的两个节点,另一个从这些节点到输出节点。因此,我们的 __init__()
方法如下所示:
class MyNeuralNet(nn.Module):
def __init__(self):
super(MyNeuralNet, self).__init__()
self.operationOne = nn.Linear(3, 2)
self.operationTwo = nn.Linear(2, 1)
在上述代码中,我们将所需神经网络的操作存储为 self
的属性。您应该习惯将 PyTorch 中的操作存储为 self
中的属性。我们用来执行此操作的模式如下:
self.desiredOperation = PyTorchOperation
然而,在上述代码中存在一个明显的错误:nn.Linear
的输入是硬编码的,因此如果输入大小发生变化,我们就必须重新编写我们的神经网络类。因此,在实例化对象时,使用变量名而不是硬编码是一个很好的做法,并将它们作为参数传递。代码如下所示:
def __init__(self, input_size, n_nodes, output_size):
super(MyNerualNet, self).__init__()
self.operationOne = nn.Linear(input_size, n_nodes)
self.operationTwo = nn.Linear(n_nodes, output_size)
现在,让我们深入了解 forward()
方法的实现。此方法接受两个参数:self
参数和任意的 x
参数,这是我们实际数据的占位符。
我们已经看过 nn.ReLU
操作,但 PyTorch 中还有更方便的函数接口,允许我们更好地描述数据流。需要注意的是,这些函数等效物不能在 Sequential API 中使用。我们的第一项工作是将数据传递给由 x
参数表示的神经网络中的第一个操作。在 PyTorch 中,将数据传递给我们网络中的第一个操作就像简单地使用 self.operationOne(x)
一样。
然后,使用 PyTorch 的功能接口,我们可以通过torch.nn.functional.relu.self.operationOne(x)
将此操作的输出传递给非线性 ReLU 函数。让我们把一切都放在一起,并定义forward()
方法。重要的是要记住最终输出必须伴随着return
关键字:
def forward(self, x):
x = self.operationOne(x)
x = nn.functional.relu(x)
x = self.operationTwo(x)
output = nn.functional.sigmoid(x)
return output
现在,让我们进行精加工和编译,以便使用基于类的 API 在 PyTorch 中定义我们的神经网络。以下代码展示了您在开源社区中找到的大部分 PyTorch 代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
class MyNeuralNet(nn.Module):
def __init__(self, input_size, n_nodes, output_size):
super(MyNeuralNet, self).__init__()
self.operationOne = nn.Linear(input_size, n_nodes)
self.operationTwo = nn.Linear(n_nodes, output_size)
def forward(self, x):
x = F.relu(self.operationOne(x)
x = self.operationTwo(x)
x = F.sigmoid(x)
return x
最后,为了访问我们的神经网络,我们必须实例化MyNeuralNet
类的对象。我们可以这样做:
my_network = MyNeuralNet(input_size = 3, n_nodes = 2, output_size = 1)
现在,我们可以通过my_network
Python 变量访问我们想要的神经网络。我们已经构建了我们的神经网络,那么接下来呢?它现在能预测学生的录取状态吗?不行。但我们会到达那里。在此之前,我们需要了解如何在 PyTorch 中表示数据,以便我们的神经网络能够理解。这就是 PyTorch 张量发挥作用的地方。
理解 PyTorch 张量
PyTorch 张量是驱动 PyTorch 计算的引擎。如果您之前有使用 Numpy 的经验,理解 PyTorch 张量将会轻而易举。大多数您在 Numpy 数组中学到的模式可以转换为 PyTorch 张量。
张量是数据容器,是向量和矩阵的广义表示。向量是一阶张量,因为它只有一个轴,看起来像[x1, x2, x3..]。矩阵是二阶张量,它有两个轴,看起来像[[x11, x12, x13..], [x21, x22, x23..]]。另一方面,标量是零阶张量,只包含单个元素,如 x1。这在下图中显示:
我们可以立即观察到,我们的数据集,其中包含 GPA、GRE、排名和录取状态列,以及各种观察行,可以表示为二阶张量:
让我们快速看一下如何从 Python 列表创建 PyTorch 张量:
import torch
first_order_tensor = torch.tensor([1, 2, 3])
print(first_order_tensor)
#tensor([1, 2, 3])
访问该容器中的元素也很简单,索引从 0 开始,以 n - 1 结束,其中 n 是容器中的元素数目:
print(first_order_tensor[0])
#tensor(1)
tensor(1)
,我们之前打印过的,是一个零阶张量。访问多个元素类似于在 NumPy 和 Python 中的方式,其中 0:2 提取从索引 0 开始的元素,但不包括索引 2 处的元素:
print(first_order_tensor[0:2])
#tensor([1, 2])
如果你想访问从特定索引开始的张量的所有元素,你可以使用 k:,其中 k 是你想提取的第一个元素的索引:
print(first_order_tensor[1:])
#tensor([2, 3])
现在,让我们了解一下二阶张量的工作原理:
second_order_tensor = torch.tensor([ [ 11, 22, 33 ],
[ 21, 22, 23 ]
])
print(second_order_tensor)
#tensor([[11, 12, 13],
[21, 22, 23]])
从一个二阶张量中访问元素稍微复杂一些。现在,让我们从之前创建的张量中访问元素 12。重要的是将二阶张量视为由两个一阶张量构成的张量,例如,[[一阶张量], [一阶张量]]。元素 12 位于第一个一阶张量内部,并且在该张量内部位于第二个位置,即索引 1。因此,我们可以使用[0, 1]来访问元素 22,其中 0 描述了一阶张量的索引,1 描述了一阶张量内部元素的索引:
print(second_order_tensor[0, 1])
#tensor(12)
现在,让我们做一个小的思维练习:如何从我们创建的张量中访问第 23 个元素?是的!你是对的!我们可以使用[1, 2]来访问它。
对于更高维度的张量,这个模式同样适用。需要注意的是,你需要使用的索引位置数目等于张量的阶数。让我们来做一个四阶张量的练习!
在我们开始之前,让我们想象一个四阶张量;它必须由三阶张量组成。因此,它看起来应该类似于[[张量的三阶],[张量的三阶],[张量的三阶]…]。每个这些三阶张量必须依次由二阶张量组成,看起来像[[张量的二阶],[张量的二阶],[张量的二阶],…],依此类推。
在这里,你会找到一个四阶张量。为了便于可视化,它已经得到了合理的间隔。在这个练习中,我们需要访问元素 1112, 1221, 2122 和 2221:
fourth_order_tensor = torch.tensor(
[
[
[
[1111, 1112],
[1121, 1122]
],
[
[1211, 1212],
[1221, 1222]
]
],
[
[
[2111, 2112],
[2121, 2122]
],
[
[2211, 2212],
[2221, 2222]
]
]
])
在这里,张量由两个三阶张量组成,每个张量都有两个二阶张量,而每个二阶张量又包含两个一阶张量。让我们看看如何访问元素 2122;其余的留给你在空闲时间里完成。元素 2122 包含在我们原始张量的第二个三阶张量中[[张量的三阶], [张量的三阶]]。所以,第一个索引位置是 1。接下来在三阶张量中,我们想要的元素在第一个二阶张量内[[二阶张量], [二阶张量]]。因此,第二个索引位置是 0。在二阶张量内部,我们想要的元素在第二个一阶张量中[[张量的一阶], [*张量的一阶]],所以索引位置是 1。最后,在一阶张量中,我们想要的元素是第二个元素[2121, 2122],索引位置是 1。当我们把这些放在一起时,我们可以使用fourth_order_tensor[1, 0, 1, 1]
来索引元素 2122。
理解张量的形状和重塑张量
现在我们知道如何从张量中访问元素,理解张量形状就很容易了。所有 PyTorch 张量都有一个 size()
方法,描述了张量在每个轴上的尺寸。零阶张量,即标量,没有任何轴,因此没有可量化的尺寸。让我们看一下 PyTorch 中几个张量的尺寸:
my_tensor = torch.tensor([1, 2, 3, 4, 5])
print(my_tensor.size())
# torch.Size([5])
由于张量沿着第一个轴有五个元素,张量的尺寸是 [5]:
my_tensor = torch.tensor([[11, 12, 13], [21, 22, 23]])
print(my_tensor.size())
# torch.Size([2, 3])
由于二阶张量包含两个一阶张量,第一个轴的尺寸是 2,每个一阶张量包含 3 个标量元素,第二个轴的尺寸是 3。因此,张量的尺寸是 [2, 3]。
这种模式可以扩展到更高阶的张量。让我们完成一个关于在前一小节中创建的 fourth_order_tensor
的快速练习。有两个三阶张量,每个三阶张量有两个一阶张量,这些一阶张量又包含两个一阶张量,每个一阶张量包含两个标量元素。因此,张量的尺寸是 [2, 2, 2, 2]:
print(fourth_order_tensor.size())
# torch.Size([2, 2, 2, 2])
现在我们了解了张量的尺寸,我们可以使用 torch.rand()
快速生成具有所需形状的随机元素张量。在本书的后续部分中,我们还会看到其他生成张量的方法。在你的张量中创建的元素可能与这里看到的不同:
random_tensor = torch.rand([4, 2])
print(random_tensor)
#tensor([[0.9449, 0.6247],
[0.1689, 0.4221],
[0.9565, 0.0504],
[0.5897, 0.9584]])
有时你可能希望重塑张量,即将张量中的元素移动到不同的轴上。我们使用 .view()
方法来重塑张量。让我们深入一个快速的例子,展示如何在 PyTorch 中完成这个操作:
random_tensor.view([2, 4])
#tensor([[0.9449, 0.6247, 0.1689, 0.4221],
[0.9565, 0.0504, 0.5897, 0.9584]])
需要注意的是,这不是一个原地操作,并且原始的 random_tensor
仍然是尺寸为 [4, 2] 的。你需要将返回的值赋值给变量以存储结果。有时,当你有很多轴时,可以使用 -1 让 PyTorch 计算特定轴的尺寸:
random_tensor = torch.rand([4, 2, 4])
random_tensor.view([2, 4, -1])
#tensor([[[0.1751, 0.2434, 0.9390, 0.4585],
[0.5018, 0.5252, 0.8161, 0.9712],
[0.7042, 0.4778, 0.2127, 0.3466],
[0.6339, 0.4634, 0.8473, 0.8062]],
[[0.3456, 0.0725, 0.0054, 0.4665],
[0.9140, 0.2361, 0.4009, 0.4276],
[0.3073, 0.9668, 0.0215, 0.5560],
[0.4939, 0.6692, 0.9476, 0.7543]]])
random_tensor.view([2, -1, 4])
#tensor([[[0.1751, 0.2434, 0.9390, 0.4585],
[0.5018, 0.5252, 0.8161, 0.9712],
[0.7042, 0.4778, 0.2127, 0.3466],
[0.6339, 0.4634, 0.8473, 0.8062]],
[[0.3456, 0.0725, 0.0054, 0.4665],
[0.9140, 0.2361, 0.4009, 0.4276],
[0.3073, 0.9668, 0.0215, 0.5560],
[0.4939, 0.6692, 0.9476, 0.7543]]])
理解张量操作
到目前为止,我们已经看过了基本的张量属性,但是使它们如此特殊的是它们执行向量化操作的能力,这对于高效的神经网络非常重要。让我们快速看一下 PyTorch 中可用的一些张量操作。
加法、减法、乘法和除法操作是按元素执行的:
让我们快速看一下这些操作:
x = torch.tensor([5, 3])
y = torch.tensor([3, 2])
torch.add(x, y)
# tensor([8, 5])
torch.sub(x, y)
# tensor([2, 1])
torch.mul(x, y)
# tensor([15, 6])
你还可以使用 +、-、* 和 / 运算符在 torch 张量上执行这些操作:
x + y
# tensor([8, 5])
让我们快速看一下 torch 张量中的矩阵乘法,可以使用 torch.matmul()
或 @
运算符来执行:
torch.matmul(x, y)
# tensor(21)
x @ y
# tensor(21)
有一个特定的原因,为什么我们还没有对两个张量执行除法操作。现在让我们来做这个操作:
torch.div(x, y)
# tensor([1, 1])
什么?那怎么可能?5 / 3 应该约为 1.667,而 3 / 2 应该是 1.5。但为什么我们得到tensor([1, 1])
作为结果?如果你猜到这是因为张量中存储的元素的数据类型,那你绝对是对的!
理解 PyTorch 中的张量类型
PyTorch 张量只能存储单一数据类型的元素。PyTorch 中还定义了需要特定数据类型的方法。因此,了解 PyTorch 张量可以存储的数据类型非常重要。根据 PyTorch 文档,以下是 PyTorch 张量可以存储的数据类型:
每个 PyTorch 张量都有一个dtype
属性。让我们来看看之前创建的张量的dtype
:
x.dtype
# torch.int64
y.dtype
# torch.int64
在这里,我们可以看到我们创建的张量中存储的元素的数据类型是 int64。因此,元素之间执行的除法是整数除法!
通过在torch.tensor()
中传递dtype
参数,让我们重新创建具有 32 位浮点元素的 PyTorch 张量:
x_float = torch.tensor([5, 3], dtype = torch.float32)
y_float = torch.tensor([3, 2], dtype = torch.float32)
print(x_float / y_float)
# tensor([1.6667, 1.5000])
你也可以使用torch.FloatTensor()
或前述截图中tensor
列下的其他名称,直接创建所需类型的张量。你也可以使用.type()
方法将张量转换为其他数据类型:
torch.FloatTensor([5, 3])
# tensor([5., 3.])
x.type(torch.DoubleTensor)
# tensor([5., 3.], dtype=torch.float64)
将我们的数据集作为 PyTorch 张量导入
现在,让我们将admit_status.csv
数据集作为 PyTorch 张量导入,以便我们可以将其馈送到我们的神经网络中。为了导入我们的数据集,我们将使用 Python 中的 NumPy 库。我们将要处理的数据集如下图所示:
当我们导入数据集时,我们不想导入第一行,即列名。我们将使用 NumPy 库中的np.genfromtext()
来将数据读取为一个 numpy 数组:
import numpy as np
admit_data = np.genfromtxt('../datasets/admit_status.csv',
delimiter = ',', skip_header = 1)
print(admit_data)
这将给我们以下输出:
我们可以使用torch.from_numpy()
直接将 numpy 数组导入为 PyTorch 张量:
admit_tensor = torch.from_numpy(admit_data)
print(admit_tensor)
这将给我们以下输出:
在 PyTorch 中训练神经网络
我们已经将数据作为 PyTorch 张量,也有了 PyTorch 神经网络。我们现在可以预测学生的录取状态了吗?不,还不行。首先,我们需要学习可以帮助我们预测录取状态的具体权重:
我们之前定义的神经网络首先随机生成权重。因此,如果我们直接将数据传递给神经网络,我们将得到毫无意义的预测结果。
在神经网络中两个在训练过程中起作用的重要组件是Criterion和Optimizer。Criterion 生成一个损失分数,该分数与神经网络的预测与真实目标值之间的差距成正比,即我们的情况下是录取状态。
优化器使用这个分数来调整神经网络中的权重,使网络的预测尽可能接近真实值。
优化器使用 Criterion 的损失分数来更新神经网络的权重的迭代过程被称为神经网络的训练阶段。现在,我们可以训练我们的神经网络。
在继续训练我们的神经网络之前,我们必须将数据集分割为输入 x
和目标 y
:
x_train = admit_tensor[:300, 1:]
y_train = admit_tensor[:300, 0]
x_test = admit_tensor[300:, 1:]
y_test = admit_tensor[300:, 0]
我们需要创建 Criterion 和 Optimizer 的实例,以便训练我们的神经网络。PyTorch 中内置了多个 Criterion,可以从 torch.nn
模块中访问。在这种情况下,我们将使用 BCELoss()
,也被称为二进制交叉熵损失,用于二元分类:
criterion = nn.BCELoss()
在 PyTorch 中,torch.optim
模块内置了几种优化器。在这里,我们将使用SGD 优化器,也被称为随机梯度下降优化器。该优化器接受神经网络的参数或权重作为参数,并可以通过在之前创建的神经网络实例上使用 parameters()
方法来访问:
optimizer = torch.optim.SGD(my_network.parameters(), lr=0.01)
我们必须编写一个循环,迭代更新权重的过程。首先,我们需要传递数据以从神经网络中获取预测结果。这非常简单:我们只需将输入数据作为参数传递给神经网络实例,使用 y_pred = my_neuralnet(x_train)
。然后,我们需要计算损失分数,通过将神经网络的预测结果和真实值传递给 Criterion 来得到 loss_score = criterion(y_pred, y_train)
。
在继续更新神经网络的权重之前,清除累积的梯度非常重要,可以通过在优化器上使用 zero_grad()
方法来实现。然后,我们使用计算的 loss_score
上的 backward()
方法执行反向传播步骤。最后,使用优化器上的 step()
方法更新参数或权重。
所有之前的逻辑必须放在一个循环中,我们在训练过程中迭代,直到我们的网络学习到最佳参数。因此,让我们将所有内容整合成可运行的代码:
for epoch in range(100):
# Forward Propagation
y_pred = my_network(x_train)
# Compute and print loss
loss_score = criterion(y_pred, y_train)
print('epoch: ', epoch,' loss: ', loss.item())
# Zero the gradients
optimizer.zero_grad()
# perform a backward pass (backpropagation)
loss_score.backward()
# Update the parameters
optimizer.step()
大功告成!我们已经训练好了我们的神经网络,它已准备好进行预测。在下一章中,我们将深入探讨神经网络中使用的各种非线性函数,验证神经网络学到的内容,并深入探讨构建强大神经网络的理念。
摘要
在本章中,我们探讨了 PyTorch 提供的各种数据结构和操作。我们使用 PyTorch 的基本模块实现了几个组件。在数据准备阶段,我们创建了张量,这些张量将被我们的算法使用。我们的网络架构是一个模型,它将学习预测用户在我们的 Wondermovies 平台上平均花费的时间。我们使用损失函数来检查我们模型的标准,并使用optimize
函数来调整模型的可学习参数,使其表现更好。
我们还看到了 PyTorch 如何通过抽象化几个复杂性,使我们能够更轻松地创建数据管道,而不需要我们并行化和增强数据。
在下一章中,我们将深入探讨神经网络和深度学习算法的工作原理。我们将探索各种内置的 PyTorch 模块,用于构建网络架构、损失函数和优化。我们还将学习如何在真实世界的数据集上使用它们。
第三章:第二节:深入深度学习。
在本节中,你将学习如何将神经网络应用于各种实际场景。
本节包括以下章节:
-
第三章,深入探讨神经网络
-
第四章,计算机视觉的深度学习
-
第五章,使用序列数据进行自然语言处理
第四章:深入探讨神经网络
在本章中,我们将探索用于解决实际问题的深度学习架构的不同模块。在前一章中,我们使用 PyTorch 的低级操作来构建模块,如网络架构、损失函数和优化器。在本章中,我们将探讨神经网络的重要组件以及 PyTorch 通过提供大量高级功能来抽象掉许多复杂性。在本章的最后,我们将构建解决实际问题的算法,如回归、二分类和多类分类。
在本章中,我们将讨论以下主题:
-
深入探讨神经网络的各种构建模块
-
非线性激活
-
PyTorch 非线性激活
-
使用深度学习进行图像分类
深入了解神经网络的构建模块
正如我们在前一章中学到的,训练深度学习算法需要以下步骤:
-
构建数据管道
-
构建网络架构
-
使用损失函数评估架构
-
使用优化算法优化网络架构权重
在前一章中,网络由使用 PyTorch 数值操作构建的简单线性模型组成。虽然使用数值操作构建一个虚拟问题的神经架构更容易,但是当我们尝试构建解决不同领域(如计算机视觉和自然语言处理(NLP))复杂问题所需的架构时,情况很快变得复杂起来。
大多数深度学习框架,如 PyTorch、TensorFlow 和 Apache MXNet,提供了抽象了许多复杂性的高级功能。这些高级功能在深度学习框架中被称为层。它们接受输入数据,应用类似于我们在前一章看到的转换,并输出数据。为了解决现实世界的问题,深度学习架构由 1 到 150 个或更多层组成。抽象化低级操作和训练深度学习算法看起来像以下的图示:
任何深度学习训练都涉及获取数据,构建架构(通常意味着组合一堆层),使用损失函数评估模型的准确性,然后通过优化网络权重来优化算法。在探讨解决一些实际问题之前,我们将了解 PyTorch 提供的用于构建层、损失函数和优化器的高级抽象。
层 - 神经网络的基本组件
在本章的其余部分,我们将遇到不同类型的层。首先,让我们试着理解最重要的层之一,线性层,它正是我们在上一章网络架构中所做的事情。线性层应用线性变换:
它之所以强大,是因为我们在上一章中编写的整个函数可以用一行代码来表示,如下所示:
from torch.nn import Linear
linear_layer = Linear(in_features=5,out_features=3,bias=True)
在上述代码中,linear_layer
函数将接受一个大小为 5 的张量,并在应用线性变换后输出一个大小为 3 的张量。让我们看一个如何做到这一点的简单示例:
inp = Variable(torch.randn(1,5))
linear_layer(inp)
我们可以通过权重访问层的可训练参数:
Linear_layer.weight
这将得到以下输出:
以同样的方式,我们可以使用bias
属性访问层的可训练参数:
linear_layer.bias
这将得到以下输出:
在不同框架中,线性层有不同的称呼,如稠密或全连接层。用于解决真实用例的深度学习架构通常包含多个层。在 PyTorch 中,我们可以通过将一个层的输出传递给另一个层来简单实现:
linear_layer = Linear(5,3)
linear_layer_2 = Linear(3,2)
linear_layer_2(linear_layer(inp))
这将得到以下输出:
每一层都有其自己的可学习参数。使用多层的想法是,每一层将学习某种模式,后续层将在此基础上构建。但是仅将线性层堆叠在一起存在问题,因为它们无法学习超出简单线性层表示的任何新内容。让我们通过一个简单的例子来看看,为什么将多个线性层堆叠在一起是没有意义的。
假设我们有两个线性层,具有以下权重:
层 | 权重 1 |
---|---|
层 1 | 3.0 |
层 2 | 2.0 |
具有两个不同层的上述架构可以简单地表示为具有不同层的单层。因此,仅仅堆叠多个线性层不会帮助我们的算法学到任何新内容。有时,这可能不太清晰,因此我们可以用以下数学公式来可视化架构:
为了解决这个问题,我们有不同的非线性函数,可以帮助学习不同的关系,而不仅仅是线性关系。
在深度学习中有许多不同的非线性函数。PyTorch 将这些非线性功能提供为层,我们可以像使用线性层一样使用它们。
一些流行的非线性函数如下:
-
Sigmoid
-
Tanh
-
ReLU
-
Leaky ReLU
非线性激活函数
非线性激活函数是将输入进行数学转换并产生输出的函数。在实践中,我们会遇到几种非线性操作。我们将介绍一些流行的非线性激活函数。
Sigmoid
Sigmoid 激活函数有一个简单的数学形式,如下所示:
Sigmoid 函数直观地将实数取值并输出一个在 0 到 1 之间的数。对于较大的负数,它接近于 0;对于较大的正数,它接近于 1。以下图表示不同 sigmoid 函数的输出:
历史上,sigmoid 函数在不同架构中被广泛使用,但近年来,它已经不再流行,因为它有一个主要缺点。当 sigmoid 函数的输出接近 0 或 1 时,前面层的梯度接近于 0,因此前一层的可学习参数的梯度也接近于 0,权重很少被调整,导致死神经元。
Tanh
Tanh 非线性函数将一个实数压扁到 -1 和 1 的范围内。当 tanh 输出接近 -1 和 1 的极端值时,也会面临梯度饱和的问题。但与 sigmoid 不同的是,tanh 的输出是以零为中心的:
ReLU
近年来,ReLU 变得越来越流行;我们几乎可以在任何现代架构中找到其使用或其变体的使用。它有一个简单的数学表达式:
简单来说,ReLU 将任何负数输入压扁为 0,并保留正数不变。我们可以将 ReLU 函数可视化如下:
使用 ReLU 的一些优缺点如下:
-
它帮助优化器更快地找到正确的权重集。更具技术性地说,它加快了随机梯度下降的收敛速度。
-
它在计算上廉价,因为我们只是进行阈值处理,而不像 sigmoid 和 tanh 函数那样进行任何计算。
-
ReLU 有一个缺点:在反向传播过程中,当大梯度通过时,它经常变得不响应;这些被称为死神经元,可以通过仔细选择学习率来控制。我们将在讨论不同调整学习率方法时讨论如何选择学习率,在 第四章,《计算机视觉的深度学习》中。
Leaky ReLU
Leaky ReLU 是解决“死亡问题”的一种尝试,而不是饱和到 0,而是饱和到一个非常小的数,例如 0.001。对于某些用例,此激活函数提供了比其他激活函数更好的性能,但不是一致的。
PyTorch 非线性激活
PyTorch 已经为我们实现了大多数常见的非线性激活函数,并且可以像任何其他层一样使用。让我们快速看一下如何在 PyTorch 中使用 ReLU 函数的示例:
example_data = Variable(torch.Tensor([[10,2,-1,-1]]))
example_relu = ReLU()
example_relu(example_data)
这将导致以下输出:
在前面的例子中,我们取一个具有两个正值和两个负值的张量,并对其应用 ReLU 函数,将负数阈值设置为 0,并保留正数。
现在我们已经涵盖了构建网络架构所需的大部分细节,让我们构建一个可以用来解决实际问题的深度学习架构。在前一章中,我们使用了一种简单的方法,这样我们可以专注于深度学习算法的工作方式。我们不再使用那种风格来构建我们的架构;相反,我们将按照 PyTorch 中预期的方式构建架构。
PyTorch 构建深度学习算法的方式
PyTorch 中的所有网络都是作为类实现的,子类化一个名为nn.Module
的 PyTorch 类,并应该实现__init__
和forward
方法。在init
函数中,我们初始化任何层,例如我们在前一节中介绍的线性层。在forward
方法中,我们将输入数据传递到我们在init
方法中初始化的层中,并返回最终输出。非线性函数通常直接在forward
函数中使用,有些也在init
方法中使用。以下代码片段显示了如何在 PyTorch 中实现深度学习架构:
class NeuralNetwork(nn.Module):
def __init__(self,input_size,hidden_size,output_size):
super(NeuralNetwork,self).__init__()
self.layer1 = nn.Linear(input_size,hidden_size)
self.layer2 = nn.Linear(hidden_size,output_size)
def __forward__(self,input):
out = self.layer1(input)
out = nn.ReLU(out)
out = self.layer2(out)
return out
如果您是 Python 新手,则可能难以理解前面的一些代码,但它所做的只是继承一个父类并在其中实现两种方法。在 Python 中,我们通过将父类作为参数传递给类名来进行子类化。init
方法在 Python 中充当构造函数,super
用于将子类的参数传递给父类,而在我们的情况下是nn.Module
。
不同机器学习问题的模型架构
我们正在解决的问题类型将主要决定我们将使用哪些层,从线性层到用于顺序数据的长短期记忆(LSTM)层。根据您尝试解决的问题类型,确定您的最后一层。通常有三种问题我们使用任何机器学习或深度学习算法来解决。让我们看看最后一层会是什么样子:
-
对于回归问题,例如预测 T 恤销售价格,我们将使用最后一层作为输出为 1 的线性层,输出连续值。
-
要将给定图像分类为 T 恤或衬衫,您将使用 Sigmoid 激活函数,因为它输出接近于 1 或 0 的值,这通常称为二元分类问题。
-
对于多类别分类问题,例如分类一幅图像是 T 恤、牛仔裤、衬衫还是连衣裙,我们会在网络末端使用 softmax 层。让我们尝试直观理解 softmax 的作用,而不深入讨论其数学原理。它从前一层的线性层获取输入,并为一定数量的示例输出概率。在我们的例子中,它将被训练以预测每种类型图像的四个概率。请记住,所有这些概率总是加起来等于 1。
损失函数
一旦我们定义了网络架构,我们还剩下两个重要步骤。一个是计算我们的网络在执行回归、分类等特定任务时的表现如何,另一个是优化权重。
优化器(梯度下降)通常接受一个标量值,因此我们的损失函数应生成一个标量值,在训练过程中需要最小化它。在某些情况下,比如预测道路上障碍物的位置并将其分类为行人或其他物体,可能需要使用两个或更多个损失函数。即使在这种情况下,我们也需要将这些损失组合成单个标量以便优化器进行最小化。我们将在第八章,现代网络架构下的迁移学习中详细讨论如何将多个损失组合成单个标量的实际示例。
在前一章中,我们定义了自己的损失函数。PyTorch 提供了几种常用损失函数的实现。让我们看看用于回归和分类的损失函数。
对于回归问题,常用的损失函数是均方误差(MSE)。这是我们在前面章节中实现的相同损失函数。我们可以使用 PyTorch 中实现的损失函数,如下所示:
loss = nn.MSELoss()
input = Variable(torch.randn(2, 6), requires_grad=True)
target = Variable(torch.randn(2, 6))
output = loss(input, target)
output.backward()
对于分类问题,我们使用交叉熵损失。在深入探讨交叉熵数学之前,让我们先了解一下交叉熵损失的作用。它计算分类网络的损失,预测的概率应该总和为 1,就像我们的 softmax 层一样。当预测的概率与正确概率偏离时,交叉熵损失会增加。例如,如果我们的分类算法预测某图像是猫的概率为 0.1,但实际上是熊猫,那么交叉熵损失将会较高。如果预测接近实际标签,则交叉熵损失会较低。
让我们看一个 Python 代码中如何实际发生的示例实现:
def cross_entropy_function(true_label, prediction):
if true_label == 1:
return -log(prediction)
else:
return -log(1 - prediction)
要在分类问题中使用交叉熵损失,我们真的不需要担心内部发生了什么——我们只需要记住,当我们的预测糟糕时,损失会很高,而当预测良好时,损失会很低。PyTorch 为我们提供了损失的实现,我们可以使用,如下所示:
loss = nn.CrossEntropyLoss()
input = Variable(torch.randn(2, 6), requires_grad=True)
target = Variable(torch.LongTensor(2).random_(6))
output = loss(input, target)
output.backward()
PyTorch 中的一些其他损失函数如下:
L1 损失 | 主要用作正则化项;我们将在第四章,计算机视觉深度学习中进一步讨论它 |
---|---|
均方误差损失 | 用作回归问题的损失函数 |
交叉熵损失 | 用于二元和多类分类问题 |
负对数似然损失 | 用于分类问题,并允许我们使用特定的权重来处理不平衡数据集 |
二维负对数似然损失 | 用于像素级分类,主要用于与图像分割相关的问题 |
优化网络架构
一旦计算了网络的损失,我们将优化权重以减少损失,从而提高算法的准确性。为了简单起见,让我们将这些优化器看作黑盒子,它们接收损失函数和所有可学习参数,并微调它们以改善我们的性能。PyTorch 提供了大部分深度学习中常用的优化器。如果您想探索这些优化器内部发生的事情,并且具有数学背景,我强烈推荐以下博客:
PyTorch 提供的一些优化器如下:
-
ASGD
-
Adadelta
-
Adagrad
-
Adam
-
Adamax
-
LBFGS
-
RMSprop
-
Rprop
-
SGD
-
SparseAdam
我们将详细讨论一些算法在第四章,计算机视觉深度学习中的细节,包括一些优点和权衡。让我们走过创建任何优化器中的一些重要步骤:
sgd_optimizer = optim.SGD(model.parameters(), lr = 0.01)
在前面的示例中,我们创建了一个 SGD 优化器,它以您网络的所有可学习参数作为第一个参数,并且一个学习率作为决定可学习参数变化比率的参数。在第四章,计算机视觉深度学习中,我们将更详细地讨论学习率和动量,这是优化器的一个重要参数。一旦创建了优化器对象,我们需要在循环内调用 zero_grad()
,因为参数将积累在前一个优化器调用中创建的梯度:
for input, target in dataset:
sgd_optimizer.zero_grad()
output = model(input)
loss = loss_fn(output, target)
loss.backward()
sgd_optimizer.step()
一旦我们在损失函数上调用 backward,它将计算梯度(可学习参数需要变化的量),我们再调用 optimizer.step()
,这将实际地改变我们的可学习参数。
现在我们已经涵盖了大多数需要帮助计算机看到或识别图像的组件。让我们构建一个复杂的深度学习模型,能够区分狗和猫,将所有理论付诸实践。
使用深度学习进行图像分类
解决任何实际问题的最重要步骤是获取数据。为了在本章中测试我们的深度学习算法,我们将使用由名为ardamavi
的用户在 GitHub 仓库提供的数据集。我们将在第四章中再次使用此数据集,计算机视觉的深度学习,将涵盖卷积神经网络(CNNs)和一些可以用来提高图像识别模型性能的高级技术。
您可以从以下链接下载数据:github.com/ardamavi/Dog-Cat-Classifier/tree/master/Data/Train_Data
。数据集包含猫和狗的图像。在实施算法之前,需要执行数据预处理和创建训练、验证和测试拆分等重要步骤。
大多数框架使得在提供以下格式的图像和标签时更容易读取图像并对其进行标记。这意味着每个类别应该有其图像的单独文件夹。在这里,所有猫图像应该在cat
文件夹中,而狗图像应该在dog
文件夹中:
Python 使得将数据放入正确格式变得很容易。让我们快速查看一下代码,然后我们将详细讨论其中的重要部分:
path = 'Dog-Cat-Classifier/Data/Train_Data/'
#Read all the files inside our folder.
dog_files = [f for f in glob.glob('Dog-Cat-Classifier/Data/Train_Data/dog/*.jpg')]
cat_files = [f for f in glob.glob('Dog-Cat-Classifier/Data/Train_Data/cat/*.jpg')]
files = dog_files + cat_files
print(f'Total no of images {len(files)}')
no_of_images = len(files)
创建一个可以用来创建验证数据集的洗牌索引:
shuffle = np.random.permutation(no_of_images)
创建一个验证目录来保存训练和验证图像:
os.mkdir(os.path.join(path,'train'))
os.mkdir(os.path.join(path,'valid'))
Create directories with label names.
for t in ['train','valid']:
for folder in ['dog/','cat/']:
os.mkdir(os.path.join(path,t,folder))
将少量图像副本复制到验证文件夹中:
for i in shuffle[:250]:
folder = files[i].split('/')[-2].split('.')[0]
image = files[i].split('/')[-1]
os.rename(files[i],os.path.join(path,'valid',folder,image))
将少量图像副本复制到训练文件夹中:
for i in shuffle[250:]:
folder = files[i].split('/')[-2].split('.')[0]
image = files[i].split('/')[-1]
os.rename(files[i],os.path.join(path,'train',folder,image))
上述所有代码所做的就是检索所有文件并选择一些图像样本来创建测试和验证集。它将所有图像分成猫和狗两个类别。创建单独的验证集是一种常见且重要的做法,因为在训练的数据上测试算法是不公平的。为了创建数据集,我们创建一个以洗牌顺序排列的数字列表,该列表的范围是图像长度。洗牌的数字充当我们选择一堆图像来创建数据集的索引。让我们详细讨论代码的每个部分。
我们使用glob
方法返回特定路径中的所有文件:
dog_files = [f for f in glob.glob('Dog-Cat-Classifier/Data/Train_Data/dog/*.jpg')]
cat_files = [f for f in glob.glob('Dog-Cat-Classifier/Data/Train_Data/cat/*.jpg')]
当图像数量庞大时,我们也可以使用iglob
,它返回一个迭代器,而不是将名称加载到内存中。在我们的情况下,我们处理的图像体积较小,可以轻松放入内存,因此不是必需的。
我们可以使用以下代码对文件进行洗牌:
shuffle = np.random.permutation(no_of_images)
前面的代码以洗牌顺序返回 0 到 1,399 范围内的数字,我们将使用这些数字作为选择图像子集的索引来创建数据集。
我们可以创建如下的测试和验证代码:
os.mkdir(os.path.join(path,'train'))
os.mkdir(os.path.join(path,'valid'))
for t in ['train','valid']:
for folder in ['dog/','cat/']:
os.mkdir(os.path.join(path,t,folder))
上述代码在train
和valid
目录内基于类别(猫和狗)创建了文件夹。
我们可以用以下代码对索引进行洗牌:
for i in shuffle[:250]:
folder = files[i].split('/')[-2].split('.')[0]
image = files[i].split('/')[-1]
os.rename(files[i],os.path.join(path,'valid',folder,image))
在上述代码中,我们使用打乱的索引随机选取了 250 张不同的图像作为验证集。对于训练数据,我们类似地对train
目录中的图像进行分组。
现在数据格式已经就绪,让我们快速看看如何将图像加载为 PyTorch 张量。
将数据加载到 PyTorch 张量中
PyTorch 的torchvision.datasets
包提供了一个名为ImageFolder
的实用类,可以用来加载图像及其关联的标签,当数据以前述格式呈现时。通常的做法是执行以下预处理步骤:
-
将所有图像调整为相同的大小。大多数深度学习架构期望图像具有相同的大小。
-
使用数据集的均值和标准差进行归一化。
-
将图像数据集转换为 PyTorch 张量。
PyTorch 通过在transforms
模块中提供许多实用函数,使得这些预处理步骤更加简单。对于我们的示例,让我们应用三个转换:
-
缩放到 256 x 256 像素大小
-
转换为 PyTorch 张量
-
标准化数据(我们将在下一节讨论如何得到均值和标准差)
下面的代码演示了如何应用转换并使用ImageFolder
类加载图像:
transform = transforms.Compose([transforms.Resize((224,224))
,transforms.ToTensor()
,transforms.Normalize([0.12, 0.11, 0.40], [0.89, 0.21, 0.12])])
train = ImageFolder('Dog-Cat-Classifier/Data/Train_Data/train/',transform)
valid = ImageFolder('Dog-Cat-Classifier/Data/Train_Data/valid/',transform)
train
对象保存了数据集中的所有图像和相关标签。它包含两个重要属性:一个提供了类别与数据集中使用的相关索引之间的映射,另一个提供了类别列表:
-
train.class_to_idx - {'cat': 0, 'dog': 1}
-
train.classes - ['cat', 'dog']
可视化加载到张量中的数据通常是一种最佳实践。为了可视化张量,我们必须重塑张量并对值进行反归一化。以下函数为我们完成了这些操作:
import matplotlib.pyplot as plt
def imshow(inp):
"""Imshow for Tensor."""
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([0.12, 0.12, 0.40])
std = np.array([0.22, 0.20, 0.20])
inp = std * inp + mean
inp = np.clip(inp, 0, 1)
plt.imshow(inp)
现在我们可以将张量传递给前面的imshow
函数,将其转换为图像:
imshow(train[30][0])
上述代码生成了以下输出:
加载 PyTorch 张量作为批次
在深度学习或机器学习中,对图像样本进行批处理是常见的做法,因为现代图形处理单元(GPU)和 CPU 优化了对图像批次的快速操作。批大小通常取决于使用的 GPU 类型。每个 GPU 都有自己的内存,可以从 2 GB 到 12 GB 不等,有时商业 GPU 的内存更多。PyTorch 提供了DataLoader
类,它接受数据集并返回图像批次,抽象了批处理中的许多复杂性,例如使用多个工作线程进行变换应用。以下代码将先前的train
和valid
数据集转换为数据加载器:
train_data_generator = torch.utils.data.DataLoader(train,shuffle=True,batch_size=64,num_workers=8)
valid_data_generator = torch.utils.data.DataLoader(valid,batch_size=64,num_workers=8)
DataLoader
类为我们提供了许多选项,其中一些最常用的选项如下:
-
shuffle
:当为 true 时,这会在每次数据加载器调用时重新排列图像。 -
num_workers
:这负责并行化。通常建议在您的机器上使用少于可用核心数的工作线程。
构建网络架构
对于大多数实际用例,特别是在计算机视觉领域,我们很少自己构建架构。有不同的架构可以快速用来解决我们的实际问题。在我们的示例中,我们将使用一种名为ResNet的流行深度学习算法,该算法在 2015 年赢得了不同竞赛(如 ImageNet)的第一名。
为了更简单地理解,让我们假设这个算法是一堆不同的 PyTorch 层仔细地组合在一起,而不是关注这个算法内部发生了什么。当我们学习 CNN 时,我们将看到 ResNet 算法的一些关键构建块。PyTorch 通过在torchvision.models
模块中提供这些流行算法使得使用它们变得更加容易。因此,对于这个示例,让我们快速看一下如何使用这个算法,然后逐行走过每一行代码:
pretrained_resnet = models.resnet18(pretrained=True)
number_features = pretrained_resnet.fc.in_features
pretrained_resnet.fc = nn.Linear(number_features, 4)
models.resnet18(pretrained = True)
对象创建了一个算法实例,它是一组 PyTorch 层。我们可以通过打印pretrained_resnet
快速查看 ResNet 算法的构成。该算法的一个小部分如下截图所示(我没有包含完整的算法,因为它可能运行数页):
正如我们所看到的,ResNet 架构是一组层,即Conv2d
、BatchNorm2d
和MaxPool2d
,以特定的方式拼接在一起。所有这些算法都会接受一个名为pretrained
的参数。当pretrained
为True
时,算法的权重已经调整到预测 ImageNet 分类问题(包括汽车、船、鱼、猫和狗)的 1000 个不同类别的特定点。这些权重被存储并与我们用于用例的模型共享。算法在使用经过微调的权重启动时通常会表现更好,而不是使用随机权重启动。因此,对于我们的用例,我们将从预训练权重开始。
ResNet 算法不能直接使用,因为它是训练用于预测 1000 个类别中的一个。对于我们的用例,我们需要预测狗和猫中的其中一个类别。为了实现这一点,我们取 ResNet 模型的最后一层,这是一个线性层,并将输出特征更改为4
,如下面的代码所示:
pretrained_resnet.fc = nn.Linear(number_features, 4)
如果您在基于 GPU 的机器上运行此算法,则为了使算法在 GPU 上运行,我们在模型上调用cuda
方法。强烈建议您在支持 GPU 的机器上运行这些程序;可以轻松地为少于一美元的费用启动一个带 GPU 的云实例。以下代码片段的最后一行告诉 PyTorch 在 GPU 上运行代码:
if is_cuda:
pretrained_resnet = pretrained_resnet.cuda()
训练模型
在前几节中,我们创建了一些DataLoader
实例和算法。现在让我们训练模型。为此,我们需要一个损失函数和一个优化器:
learning_rate = 0.005
criterion = nn.CrossEntropyLoss()
fit_optimizer = optim.SGD(pretrained_resnet.parameters(), lr=0.005, momentum=0.6)
exp_learning_rate_scheduler = lr_scheduler.StepLR(fit_optimizer, step_size=2, gamma=0.05)
在上述代码中,我们基于CrossEntropyLoss
创建了我们的损失函数,并基于SGD
创建了优化器。StepLR
函数有助于动态调整学习率。我们将讨论不同可用的策略来调整学习率,详见第四章,计算机视觉的深度学习。
下面的train_my_model
函数接收一个模型,并通过运行多个 epoch 来调整算法的权重以减少损失:
def train_my_model(model, criterion, optimizer, scheduler, number_epochs=20):
since = time.time()
best_model_weights = model.state_dict()
best_accuracy = 0.0
for epoch in range(number_epochs):
print('Epoch {}/{}'.format(epoch, number_epochs - 1))
print('-' * 10)
每个 epoch 都有训练和验证阶段:
for each_phase in ['train', 'valid']:
if each_phase == 'train':
scheduler.step()
model.train(True)
else:
model.train(False)
running_loss = 0.0
running_corrects = 0
迭代数据:
for data in dataloaders[each_phase]:
input_data, label_data = data
if torch.cuda.is_available():
input_data = Variable(inputs.cuda())
label_data = Variable(labels.cuda())
else:
input_data, label_data = Variable(input_data), Variable(label_data)
optimizer.zero_grad()
outputs = model(input_data)
_, preds = torch.max(outputs.data, 1)
loss = criterion(outputs, label_data)
if each_phase == 'train':
loss.backward()
optimizer.step()
running_loss += loss.data[0]
running_corrects += torch.sum(preds == label_data.data)
epoch_loss = running_loss / dataset_sizes[each_phase]
epoch_acc = running_corrects / dataset_sizes[each_phase]
print('{} Loss: {:.4f} Acc: {:.4f}'.format(each_phase, epoch_loss, epoch_acc))
if each_phase == 'valid' and epoch_acc > best_acc:
best_accuracy = epoch_acc
best_model_weights = model.state_dict()
print()
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_accuracy))
model.load_state_dict(best_model_weights)
return model
函数可以按以下方式运行:
train_my_model(pretrained_resnet, criterion, fit_optimizer, exp_learning_rate_scheduler, number_epochs=20)
前述函数执行以下操作:
-
它通过模型传递图像并计算损失。
-
训练阶段进行反向传播。在验证/测试阶段,不调整权重。
-
损失在每个 epoch 中跨批次累积。
-
存储了最佳模型并打印了验证准确率。
在运行了 20 个 epoch 后,上述模型的验证准确率达到了 87%。
在接下来的章节中,我们将学习更高级的技术,帮助我们以更快的方式训练更准确的模型。前面的模型在 Titan X GPU 上运行大约花费了 30 分钟。我们将涵盖不同的技术,有助于加快模型的训练速度。
概要
在本章中,我们探讨了在 PyTorch 中神经网络的完整生命周期,从构建不同类型的层,添加激活函数,计算交叉熵损失,到最终优化网络性能(即通过 SGD 优化器调整层的权重)。
我们研究了如何将流行的 ResNet 架构应用于二元或多类分类问题。
在此过程中,我们试图解决真实世界的图像分类问题,将猫图像分类为猫,狗图像分类为狗。这些知识可以应用于分类不同类别/实体的类别,例如分类鱼的物种,识别不同品种的狗,分类植物苗,将宫颈癌分为类型 1、类型 2 和类型 3,以及更多。
在下一章中,我们将深入学习机器学习的基础知识。
第五章:深度学习用于计算机视觉
在第三章中,深入探讨神经网络,我们使用了一种名为ResNet的流行卷积神经网络(CNN)架构构建了一个图像分类器,但我们将这个模型当作黑盒子使用。在本章中,我们将探索如何从头开始构建架构来解决图像分类问题,这是最常见的用例之一。我们还将学习如何使用迁移学习,这将帮助我们使用非常小的数据集构建图像分类器。除了学习如何使用 CNN,我们还将探索这些卷积网络学习到了什么。
在本章中,我们将涵盖卷积网络的重要构建模块。本章将涵盖以下重要主题:
-
神经网络介绍
-
从头开始构建 CNN 模型
-
创建和探索 VGG16 模型
-
计算预卷积特征
-
理解 CNN 模型学习的内容
-
可视化 CNN 层的权重
神经网络介绍
在过去几年中,CNN 在图像识别、目标检测、分割以及计算机视觉领域的许多其他领域中变得流行起来。尽管在自然语言处理(NLP)领域中尚不常用,但它们也变得流行起来。完全连接层和卷积层之间的根本区别在于中间层中权重连接的方式。让我们看看以下图表,展示了完全连接或线性层的工作原理:
在计算机视觉中使用线性层或完全连接层的最大挑战之一是它们丢失了所有空间信息,并且在使用完全连接层时权重的复杂性太大。例如,当我们将 224 像素图像表示为平面数组时,我们将得到 150,528(224 x 224 x 3 通道)。当图像被展平时,我们失去了所有的空间信息。让我们看看简化版本的 CNN 是什么样子:
所有的卷积层只是在图像上应用称为过滤器的权重窗口。在我们试图详细理解卷积和其他构建模块之前,让我们为 MNIST 数据集构建一个简单而强大的图像分类器。一旦我们建立了这个分类器,我们将逐步分解网络的每个组件。我们将图像分类器的构建分解为以下步骤:
-
获取数据
-
创建验证数据集
-
从头开始构建我们的 CNN 模型
-
训练和验证模型
MNIST - 获取数据
MNIST 数据集包含 60,000 个手写数字(从 0 到 9)用于训练和 10,000 张图像用于测试。PyTorch 的torchvision
库为我们提供了一个 MNIST 数据集,它下载数据并以可直接使用的格式提供。让我们使用 MNIST 函数将数据集下载到本地并将其包装到DataLoader
中。我们将使用torchvision
转换将数据转换为 PyTorch 张量并进行数据标准化。以下代码将处理下载数据,将数据包装到DataLoader
中,并进行数据标准化:
transformation = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.14,), (0.32,))])
training_dataset = datasets.MNIST('dataset/',train=True,transform=transformation,
download=True) test_dataset =
datasets.MNIST('dataset/',train=False,transform=transformation, download=True)
training_loader = torch.utils.data.DataLoader(training_dataset,batch_size=32,shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset,batch_size=32,shuffle=True)
前面的代码为我们提供了用于训练和测试数据集的DataLoader
变量。让我们可视化几张图像,以便了解我们正在处理的内容。以下代码将帮助我们可视化 MNIST 图像:
def plot_img(image):
image = image.numpy()[0] mean = 0.1307
std = 0.3081
image = ((mean * image) + std) plt.imshow(image,cmap='gray')
现在,我们可以传递plot_img
方法来可视化我们的数据集。我们将使用以下代码从DataLoader
变量中获取一批记录,并绘制图像:
sample_data = next(iter(training_loader)) plot_img(sample_data[0][1]) plot_img(sample_data[0][2])
图像可以如下进行可视化:
从头开始构建 CNN 模型
在这一部分,我们将从头开始构建自己的架构。我们的网络架构将包含不同层的组合,如下所示:
-
Conv2d
-
MaxPool2d
-
修正线性单元 (ReLU)
-
视图
-
线性层
让我们看一下我们打算实现的架构的图示表示:
让我们在 PyTorch 中实现这个架构,然后逐步了解每个单独的层的作用:
class Network(nn.Module): def init (self):
super(). init ()
self.conv1 = nn.Conv2d(1, 10, kernel_size=3)
self.conv2 = nn.Conv2d(10, 20, kernel_size=3) self.conv2_drop = nn.Dropout2d()
self.fullyconnected1 = nn.Linear(320, 50) self.fullyconnected2 = nn.Linear(50, 10)
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) x = x.view(-1, 320)
x = F.relu(self.fullyconnected1(x))
x = F.dropout(x, training=self.training) x = self.fullyconnected2(x)
return F.log_softmax(x)
让我们详细了解每一层的作用。
Conv2d
Conv2d 负责在我们的 MNIST 图像上应用卷积滤波器。让我们尝试理解如何在一维数组上应用卷积,然后学习如何在图像上应用二维卷积。看看以下图表。在这里,我们将对长度为 7 的张量应用一个Conv1d大小为 3 的滤波器(或核):
底部的框代表我们的输入张量,共有七个值,而连接的框代表我们应用三个大小的卷积滤波器后的输出。在图像的右上角,三个框代表Conv1d层的权重和参数。卷积滤波器像窗口一样应用,并且通过跳过一个值来移动到以下值。要跳过的值的数量称为步幅,默认设置为 1。让我们写下第一个和最后一个输出的计算方式来理解输出值是如何被计算出来的:
输出 1 –> (-0.5209 x 0.2286) + (-0.0147 x 2.4488) + (-0.321 x -0.9498)
输出 5 –> (-0.5209 x -0.6791) + (-0.0147 x -0.6535) + (-0.321 x 0.6437)
现在,我们应该清楚卷积是做什么的了。它通过根据步长的值移动一个滤波器(或卷积核),也就是一组权重,来对输入进行处理。在前面的示例中,我们每次移动我们的滤波器一个点。如果步长值为 2,那么我们将一次移动两个点。让我们看一个 PyTorch 的实现来理解它是如何工作的:
conv = nn.Conv1d(1,1,3,bias=False)
sample = torch.randn(1,1,7)
conv(Variable(sample))
#Check the weights of our convolution filter by
conv.weight
还有另一个重要的参数叫做填充(padding),通常与卷积一起使用。如前面的例子所示,如果滤波器未应用到数据的末尾,即当数据不足以进行步长时,它就会停止。填充通过向张量的两端添加零来防止这种情况。让我们看一个一维填充如何工作的示例:
在上述图中,我们使用了一个填充为 2 且步长为 1 的Conv1d层。让我们看看 Conv2d 在图像上的工作原理。
在我们了解 Conv2d 如何工作之前,我强烈建议您查看一个了不起的博客(setosa.io/ev/image-kernels/
),其中包含卷积如何工作的实时演示。在您花几分钟玩弄演示之后,继续阅读。
让我们来理解演示中发生了什么。在图像的中心框中,我们有两组不同的数字:一组在方框中表示,另一组在方框下方。方框中表示的是像素值,正如演示中左侧照片上的白色框所示。方框下方标记的数字是用于锐化图像的滤波器(或卷积核)值。这些数字是特意挑选出来执行特定的任务。在这种情况下,它们是用来锐化图像的。就像我们之前的例子一样,我们进行逐元素乘法并将所有值求和,以生成右侧图像中像素的值。生成的值由图像右侧的白色框突出显示。
尽管在此示例中卷积核中的值是手动挑选的,在 CNN 中,我们不手动挑选这些值;相反,我们随机初始化它们,并让梯度下降和反向传播调整卷积核的值。学习到的卷积核将负责识别不同的特征,如线条、曲线和眼睛。看看以下截图,我们可以看到一个数字矩阵并了解卷积是如何工作的:
在上述屏幕截图中,我们假设 6 x 6 矩阵表示一幅图像,并应用了大小为 3 x 3 的卷积滤波器。然后,我们展示了如何生成输出。为了保持简单,我们只计算了矩阵的突出部分。输出通过执行以下计算生成:
输出 –> 0.86 x 0 + -0.92 x 0 + -0.61 x 1 + -0.32 x -1 + -1.69 x -1 + ........
Conv2d 函数中使用的另一个重要参数是kernel_size
,它决定了卷积核的大小。一些常用的卷积核大小包括1、3、5和7。卷积核大小越大,滤波器能够覆盖的区域就越大,因此在早期层中常见到应用大小为7或9的滤波器对输入数据进行处理。
池化
在卷积层后添加池化层是一种常见的做法,因为它们可以减小特征图的大小,并优化卷积层的输出结果。
池化提供了两个不同的功能:一个是减小要处理的数据大小,另一个是强制算法不要专注于图像中位置的微小变化。例如,人脸检测算法应该能够在图片中检测到人脸,而不管人脸在照片中的位置如何。
让我们看看 MaxPool2d 是如何工作的。它也使用与卷积相同的核大小和步幅的概念。与卷积不同的是,它不具有任何权重,只是作用于前一层每个滤波器生成的数据。如果核大小为2 x 2,则它会在图像中考虑该大小,并选择该区域的最大值。让我们看一下下面的图表,这将清楚地解释 MaxPool2d 的工作原理:
左侧的方框包含特征图的值。在应用最大池化后,输出存储在方框的右侧。让我们通过写出第一行输出中数值的计算来查看输出是如何计算的:
另一种常用的池化技术是平均池化。最大函数被替换为平均函数。下图解释了平均池化的工作原理:
在这个示例中,我们不是取四个值的最大值,而是取这四个值的平均值。让我们写下计算过程,以便更容易理解:
非线性激活 - ReLU
在应用最大池化或平均池化后,通常在卷积层后添加非线性层是一种常见且最佳的做法。大多数网络架构倾向于使用 ReLU 或不同变体的 ReLU。无论我们选择哪种非线性函数,它都会应用于特征图的每个元素。为了使其更直观,让我们看一个示例,在该示例中,我们在应用了最大池化和平均池化的同一特征图上应用 ReLU:
视图
对于图像分类问题,在大多数网络的最后使用全连接或线性层是一种常见做法。这里,我们使用的是二维卷积,它以一个数字矩阵作为输入,并输出另一个数字矩阵。要应用线性层,我们需要展平矩阵,即将二维张量展平为一维向量。以下图展示了 view 函数的工作原理:
让我们看一下在我们的网络中使用的代码,它确实如此:
x.view(-1, 320)
正如我们之前看到的,view 方法将把一个n维张量展平成一个一维张量。在我们的网络中,每个图像的第一维是输入数据。在批处理后,输入数据的维度将为32 x 1 x 28 x 28,其中第一个数字32表示有32张大小为28高、28宽、1通道的图像,因为这是一张黑白图像。在展平时,我们不希望展平或混合不同图像的数据。因此,我们传递给 view 函数的第一个参数将指示 PyTorch 避免在第一维上展平数据。以下图展示了其工作原理:
在上图中,我们有大小为2 x 1 x 2 x 2的数据;在应用 view 函数后,它将其转换为大小为2 x 1 x 4的张量。让我们看另一个例子,这次我们没有提到- 1:
如果我们忘记了指明要展平的维度,可能会导致意外的结果,因此在这一步要特别小心。
线性层
当我们将数据从二维张量转换为一维张量后,我们通过一个线性层,然后是一个非线性激活层来处理数据。在我们的架构中,我们有两个线性层,一个后面跟着 ReLU,另一个后面跟着log_softmax
函数,用于预测给定图像中包含的数字。
训练模型
要训练模型,我们需要遵循与之前的狗和猫图像分类问题相同的过程。以下代码片段训练我们的模型,使用提供的数据集:
def fit_model(epoch,model,data_loader,phase='training',volatile=False): if phase == 'training':
model.train()
if phase == 'validation': model.eval() volatile=True
running_loss = 0.0
running_correct = 0
for batch_idx , (data,target) in enumerate(data_loader): if is_cuda:
data,target = data.cuda(),target.cuda()
data , target = Variable(data,volatile),Variable(target) if phase == 'training':
optimizer.zero_grad() output = model(data)
loss = F.null_loss(output,target) running_loss +=
F.null_loss(output,target,size_average=False).data[0] predictions = output.data.max(dim=1,keepdim=True)[1]
running_correct += preds.eq(target.data.view_as(predictions)).cpu().sum() if phase == 'training':
loss.backward() optimizer.step()
loss = running_loss/len(data_loader.dataset)
accuracy = 100\. * running_correct/len(data_loader.dataset) print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is
{running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}') return loss,accuracy
这种方法在训练和验证时有不同的逻辑。主要有两个原因使用不同的模式:
-
在训练模式下,dropout 会移除一定比例的数值,而这种情况在验证或测试阶段不应该发生。
-
在训练模式下,我们计算梯度并改变模型的参数值,但在测试或验证阶段不需要反向传播。
在前一个函数中,大部分代码都是不言自明的。在函数的最后,我们返回该特定 epoch 模型的损失和准确度。
让我们通过前面的函数对模型进行 20 次迭代,并绘制训练和验证的损失和准确率,以了解我们的网络表现如何。以下代码运行 fit
方法对训练和测试数据集进行 20 次迭代:
model = Network() if is_cuda:
model.cuda()
optimizer = optim.SGD(model.parameters(),lr=0.01,momentum=0.5) training_losses , training_accuracy = [],[]
validation_losses , validation_accuracy = [],[] for epoch in range(1,20):
epoch_loss, epoch_accuracy = fit(epoch,model,training_loader,phase='training')
validation_epoch_loss , validation_epoch_accuracy = fit(epoch,model,test_loader,phase='validation')
training_losses.append(epoch_loss) training_accuracy.append(epoch_accuracy) validation_losses.append(validation_epoch_loss) validation_accuracy.append(validation_epoch_accuracy)
以下代码绘制了训练和测试的损失:
plt.plot(range(1,len(training_losses)+1),training_losses,'bo',label = 'training loss')
plt.plot(range(1,len(validation_losses)+1),validation_losses,'r',label = 'validation loss')
plt.legend()
前面的代码生成了以下图表:
以下代码绘制了训练和测试的准确率:
plt.plot(range(1,len(training_accuracy)+1),training_accuracy,'bo',label = 'train accuracy')
plt.plot(range(1,len(validation_accuracy)+1),validation_accuracy,'r',label = 'val accuracy')
plt.legend()
前面的代码生成了以下图表:
在第 20 个 epoch 结束时,我们实现了 98.9% 的测试准确率。我们已经让我们的简单卷积模型运行,并几乎达到了最新的结果。让我们看看当我们尝试在我们的狗与猫数据集上使用相同的网络架构时会发生什么。我们将使用上一章节第三章神经网络的基本构建块中的数据,以及来自 MNIST 示例的架构,并进行一些小的更改。一旦我们训练了模型,我们可以评估它,以了解我们的简单架构表现如何。
从头开始分类狗和猫 - CNN
我们将使用相同的架构,但会进行一些小的更改,如下所列:
-
第一线性层的输入维度需要改变,因为我们的猫和狗图像的尺寸是 256, 256。
-
我们将添加另一个线性层,以使模型能够更灵活地学习。
让我们看一下实现网络架构的代码:
class Network(nn.Module): def init (self):
super(). init ()
self.conv1 = nn.Conv2d(3, 10, kernel_size=3) self.conv2 = nn.Conv2d(10, 20, kernel_size=3) self.conv2_drop = nn.Dropout2d()
self.fc1 = nn.Linear(56180, 500) self.fc2 = nn.Linear(500,50) self.fc3 = nn.Linear(50, 2)
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) x = x.view(x.size(0),-1)
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training) x = F.relu(self.fc2(x))
x = F.dropout(x,training=self.training) x = self.fc3(x)
return F.log_softmax(x,dim=1)
我们将使用与 MNIST 示例相同的训练函数,因此我不会在此处包含代码。然而,让我们看一下在对模型进行 20 次迭代训练时生成的图表。
训练集和验证集的损失绘制如下:
训练集和验证集的准确率绘制如下:
从这些图表中可以看出,训练损失在每次迭代中都在减少,但验证损失却变得更糟。准确率在训练过程中也在增加,但几乎在 75% 时趋于饱和。这是一个明显的例子,显示模型没有泛化能力。在接下来的部分,我们将看一下另一种称为迁移学习的技术,它可以帮助我们训练更精确的模型,并提供可以加快训练的技巧。
使用迁移学习对狗和猫进行分类
迁移学习是能够在类似数据集上重复使用已训练的算法,而无需从头开始训练它。我们人类在认识新图像时并不通过分析成千上万张类似的图像来学习。我们只需理解不同的特征,这些特征实际上能够区分一个特定的动物,比如狐狸,与狗的不同之处。我们不需要通过理解线条、眼睛和其他更小的特征来学习什么是狐狸。因此,我们将学习如何利用预训练模型,用极少量的数据建立最先进的图像分类器。
CNN 架构的前几层专注于更小的特征,例如线条或曲线的外观。CNN 后面几层的过滤器学习更高级的特征,例如眼睛和手指,最后几层学习识别确切的类别。预训练模型是一种在类似数据集上训练的算法。大多数流行的算法都是在流行的 ImageNet 数据集上预训练,以识别 1000 个不同的类别。这样的预训练模型将有调整后的滤波器权重,用于识别各种模式。因此,让我们了解如何利用这些预训练权重。我们将研究一个名为VGG16的算法,它是早期在 ImageNet 竞赛中取得成功的算法之一。尽管有更现代的算法,但这种算法仍然很受欢迎,因为它简单易懂,适合用于迁移学习。让我们先看看 VGG16 模型的架构,然后试着理解这个架构以及如何用它来训练我们的图像分类器:
VGG16 架构包含五个 VGG 块。一个块由卷积层、非线性激活函数和最大池化函数组成。所有的算法参数都被调整以达到在分类 1000 个类别时的最先进结果。该算法接受以批次形式的输入数据,并且数据被 ImageNet 数据集的均值和标准差进行了归一化。
在迁移学习中,我们尝试通过冻结大部分层的学习参数来捕捉算法学到的内容。通常,只微调网络的最后几层是一个良好的实践。在这个例子中,我们将仅训练最后几个线性层,保持卷积层不变,因为卷积特征学习的特征大多适用于所有种类的图像问题,这些图像具有相似的属性。让我们使用迁移学习来训练一个 VGG16 模型,用于狗和猫的分类。接下来的章节中,我们将详细介绍实现的步骤。
创建和探索 VGG16 模型
PyTorch 在其torchvision
库中提供了一组经过训练的模型。当参数pretrained
为True
时,大多数模型都会接受一个称为pretrained
的参数,它会下载为解决ImageNet分类问题而调整的权重。我们可以使用以下代码创建一个 VGG16 模型:
from torchvision import models
vgg = models.vgg16(pretrained=True)
现在,我们已经有了我们的 VGG16 模型和所有预训练的权重准备好使用。当第一次运行代码时,根据您的互联网速度,可能需要几分钟。权重的大小可能约为 500 MB。我们可以通过打印来快速查看 VGG16 模型。当我们使用现代架构时,了解这些网络如何实现实际上非常有用。让我们看看这个模型:
VGG (
(features): Sequential (
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU (inplace)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU (inplace)
(4): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU (inplace)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU (inplace)
(9): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
(10):Conv2d(128, 256, kernel_size=(3, 3), stride=(1,1), padding=(1, 1))
(11): ReLU (inplace)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU (inplace)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU (inplace)
(16): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU (inplace)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU (inplace)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU (inplace)
(23): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1,1))
(25): ReLU (inplace)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU (inplace)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU (inplace)
(30): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
)
(classifier): Sequential (
(0): Linear (25088 -> 4096)
(1): ReLU (inplace)
(2): Dropout (p = 0.5)
(3): Linear (4096 -> 4096)
(4): ReLU (inplace)
(5): Dropout (p = 0.5)
(6): Linear (4096 -> 1000)
)
)
模型摘要包含两个顺序模型:features
和 classifiers
。 features
顺序模型包含我们将要冻结的层。
冻结层
让我们冻结特征模型的所有层,其中包含卷积块。冻结这些卷积块的权重将阻止这些层的权重更新。由于模型的权重经过训练以识别许多重要特征,我们的算法将能够从第一次迭代开始做同样的事情。使用模型的权重,这些权重最初是为不同用例而训练的能力,称为迁移学习。
现在,让我们看看如何冻结层的权重或参数:
for param in vgg.features.parameters(): param.requires_grad = False
此代码防止优化器更新权重。
微调 VGG16
VGG16 模型已经训练用于分类 1000 个类别,但尚未训练用于狗和猫的分类。因此,我们需要将最后一层的输出特征从 1000 更改为 2。我们可以使用以下代码来实现这一点:
vgg.classifier[6].out_features = 2
vgg.classifier
函数使我们可以访问顺序模型内的所有层,第六个元素将包含最后一层。当我们训练 VGG16 模型时,只需要训练分类器参数。因此,我们只传递classifier.parameters
给优化器,如下所示:
optimizer = optim.SGD(vgg.classifier.parameters(),lr=0.0001,momentum=0.5)
训练 VGG16 模型
到目前为止,我们已经创建了模型和优化器。由于我们使用的是狗与猫数据集,我们可以使用相同的数据加载器和训练函数来训练我们的模型。请记住:当我们训练模型时,只有分类器内部的参数会发生变化。以下代码片段将训练模型 20 个 epoch,从而达到 98.45%的验证精度:
training_losses , training_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,20):
epoch_loss, epoch_accuracy =
fit(epoch,vgg,training_data_loader,phase='training')
validation_epoch_loss , validation_epoch_accuracy =
fit(epoch,vgg,valid_data_loader,phase='validation')
training_losses.append(epoch_loss)
training_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
让我们可视化训练和验证损失:
让我们可视化训练和验证精度:
我们可以应用一些技巧,如数据增强,并尝试不同的 dropout 值,以提高模型的泛化能力。以下代码片段将 VGG 分类器模块中的 dropout 值从 0.5 更改为 0.2,并训练模型:
for layer in vgg.classifier.children(): if(type(layer) == nn.Dropout):
layer.p = 0.2
#Training
training_losses , training_accuracy = [],[] validation_losses , validation_accuracy = [],[]
for epoch in range(1,3):
epoch_loss, epoch_accuracy =
fit(epoch,vgg,training_data_loader,phase='training')
validation_epoch_loss , validation_epoch_accuracy =
fit(epoch,vgg,valid_data_loader,phase='validation')
training_losses.append(epoch_loss)
training_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
将模型训练几个 epoch 后,我注意到略有改善;您可以尝试调整不同的 dropout 值,看看是否可以获得更好的结果。我们可以使用另一个重要的技巧来改善模型的泛化能力,即增加数据或进行数据增强。我们可以通过随机水平翻转图像或将图像旋转一小角度来执行数据增强。torchvision transforms
模块提供了不同的功能来执行数据增强,并且它们是动态的,每个 epoch 都会变化。我们可以使用以下代码实现数据增强:
training_transform =transforms.Compose([transforms.Resize((224,224)),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(0.2),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.32, 0.406], [0.229, 0.224, 0.225])
])
train = ImageFolder('dogsandcats/train/',training_transform) valid = ImageFolder('dogsandcats/valid/',simple_transform)
#Training
training_losses , training_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,3):
epoch_loss, epoch_accuracy = fit(epoch,vgg,training_data_loader,phase='training')
validation_epoch_loss , validation_epoch_accuracy = fit(epoch,vgg,valid_data_loader,phase='validation')
training_losses.append(epoch_loss)
training_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
上述代码的输出如下:
#Results
training loss is 0.041 and training accuracy is 22657/23000 98.51 validation loss is 0.043 and validation accuracy is 1969/2000 98.45 training loss is 0.04 and training accuracy is 22697/23000 98.68 validation loss is 0.043 and validation accuracy is 1970/2000 98.5
使用增强数据训练模型使模型的准确性提高了 0.1%,只需运行两个 epoch;我们可以再运行几个 epoch 来进一步提高。如果您在阅读本书时训练这些模型,您会意识到每个 epoch 的训练时间可能超过几分钟,这取决于您正在使用的 GPU。让我们看看一种技术,可以使每个 epoch 的训练时间缩短到几秒钟。
计算预卷积特征
当我们冻结卷积层和训练模型时,完全连接层或稠密层(vgg.classifier
)的输入始终保持不变。为了更好地理解这一点,让我们将卷积块——在我们的示例中是 vgg.features
块——视为一个具有学习权重且在训练过程中不会改变的函数。因此,计算卷积特征并存储它们将有助于提高训练速度。训练模型的时间将减少,因为我们只需计算这些特征一次,而不是在每个 epoch 都计算一次。
让我们通过可视化理解并实现它:
第一个框描述了通常的训练方式,因为我们在每个 epoch 计算卷积特征,尽管值不变,因此可能会很慢。在底部框中,我们只计算一次卷积特征,然后仅训练线性层。为了计算预卷积特征,我们需要通过卷积块传递所有训练数据,并将它们存储起来。为此,我们需要选择 VGG 模型的卷积块。幸运的是,PyTorch 实现的 VGG16 有两个序列模型,因此只需选择第一个序列模型的特征即可。以下代码为我们执行此操作:
vgg = models.vgg16(pretrained=True) vgg = vgg.cuda()
features = vgg.features
training_data_loader = torch.utils.data.DataLoader(train,batch_size=32,num_workers=3,shuffle=False)
valid_data_loader = torch.utils.data.DataLoader(valid,batch_size=32,num_workers=3,shuffle=False)
def preconvfeat(dataset,model):
conv_features = []
labels_list = []
for data in dataset:
inputs,labels = data
if is_cuda:
inputs , labels = inputs.cuda(),labels.cuda()
inputs , labels = Variable(inputs),Variable(labels)
output = model(inputs)
conv_features.extend(output.data.cpu().numpy())
labels_list.extend(labels.data.cpu().numpy())
conv_features = np.concatenate([[feat] for feat in conv_features])
return (conv_features,labels_list)
conv_feat_train,labels_train = preconvfeat(training_data_loader,features) conv_feat_val,labels_val = preconvfeat(valid_data_loader,features)
在前面的代码中,preconvfeat
方法接收数据集和 vgg
模型,并返回卷积特征以及相关的标签。其余的代码与前面的示例中用来创建数据加载器和数据集的代码类似。
一旦我们获得了训练集和验证集的卷积特征,我们可以创建一个 PyTorch 数据集和 DataLoader
类,这将简化我们的训练过程。以下代码创建了用于我们卷积特征的数据集和 DataLoader
:
class CustomDataset(Dataset):
def init (self,feat,labels): self.conv_feat = feat self.labels = labels
def len (self):
return len(self.conv_feat) def getitem (self,idx):
return self.conv_feat[idx],self.labels[idx]
training_feat_dataset = CustomDataset(conv_feat_train,labels_train) validation_feat_dataset = CustomDataset(conv_feat_val,labels_val)
training_feat_loader = DataLoader(training_feat_dataset,batch_size=64,shuffle=True)
validation_feat_loader = DataLoader(validation_feat_dataset,batch_size=64,shuffle=True)
由于我们有了新的数据加载器,它们生成了一批批的卷积特征和标签,我们可以使用在其他示例中使用过的相同训练函数。现在,我们将使用vgg.classifier
作为模型来创建优化器和拟合方法。以下代码训练分类器模块以识别狗和猫。在 Titan X GPU 上,每个 epoch 不到五秒,否则可能需要几分钟:
training_losses , training_accuracy = [],[] validation_losses , validation_accuracy = [],[]
for epoch in range(1,20): epoch_loss, epoch_accuracy =
fit_numpy(epoch,vgg.classifier,training_feat_loader,phase='training') validation_epoch_loss , validation_epoch_accuracy =
fit_numpy(epoch,vgg.classifier,validation_feat_loader,phase='validation') training_losses.append(epoch_loss) training_accuracy.append(epoch_accuracy) validation_losses.append(validation_epoch_loss) validation_accuracy.append(validation_epoch_accuracy)
理解 CNN 模型学习的内容
深度学习模型通常被认为是不可解释的。然而,有不同的技术可以帮助我们解释这些模型内部发生的情况。对于图像,卷积层学习到的特征是可解释的。在本节中,我们将探索两种流行的技术,以便理解卷积层。
从中间层可视化输出
可视化中间层的输出将帮助我们理解输入图像在不同层之间的转换方式。通常,每个层的输出称为激活。为此,我们应该提取中间层的输出,可以通过不同的方式完成。PyTorch 提供了一种称为register_forward_hook
的方法,允许我们传递一个函数来提取特定层的输出。
默认情况下,PyTorch 模型只存储最后一层的输出,以便更有效地使用内存。因此,在检查中间层的激活输出之前,让我们学习如何从模型中提取输出。看看以下代码片段,它提取了输出。我们将逐步分析以理解发生了什么:
vgg = models.vgg16(pretrained=True).cuda()
class LayerActivations(): features=None
def init (self,model,layer_num):
self.hook = model[layer_num].register_forward_hook(self.hook_fn) def hook_fn(self,module,input,output):
self.features = output.cpu() def remove(self):
self.hook.remove()
conv_out = LayerActivations(vgg.features,0) o = vgg(Variable(img.cuda())) conv_out.remove()
act = conv_out.features
我们首先创建一个预训练的 VGG 模型,从中提取特定层的输出。LayerActivations
类指示 PyTorch 将该层的输出存储在features
变量中。让我们逐个了解LayerActivations
类内的每个函数。
_init_
函数以模型和需要从中提取输出的层的层编号作为参数。我们在该层上调用register_forward_hook
方法并传入一个函数。当 PyTorch 进行前向传播时,即当图像通过各层时,PyTorch 调用传递给register_forward_hook
方法的函数。该方法返回一个句柄,可以用于注销传递给register_forward_hook
方法的函数。
register_forward_hook
方法将三个值传递给我们传递给它的函数。module
参数允许我们访问层本身。第二个参数是input
,它指的是流经该层的数据。第三个参数是output
,允许我们访问转换后的输入或层的激活。我们将features
变量的输出存储在LayerActivations
类中。
第三个函数从_init_
函数中获取钩子并取消注册函数。现在,我们可以传递模型和我们正在寻找的激活层的层数。让我们看看为不同层次的以下图像创建的激活:
让我们可视化第一个卷积层生成的一些激活以及用于此目的的代码:
fig = plt.figure(figsize=(20,50)) fig.subplots_adjust(left=0,right=1,bottom=0,top=0.8,hspace=0,
wspace=0.2)
for i in range(30):
ax = fig.add_subplot(12,5,i+1,xticks=[],yticks=[]) ax.imshow(act[0][i])
让我们可视化第五个卷积层生成的一些激活:
让我们来看看最后的 CNN 层:
通过查看不同层生成的内容,我们可以看到早期层次检测线条和边缘,而最终层次则倾向于学习高级特征,不太可解释。
在我们继续可视化权重之前,让我们学习一下 ReLU 层之后的特征映射或激活在表现上是如何的。因此,让我们可视化第二层的输出。
如果您快速查看上述图像的第二行第五幅图像,它看起来像是滤波器在检测图像中的眼睛。当模型表现不佳时,这些可视化技巧可以帮助我们理解模型为何可能无法工作。
可视化 CNN 层的权重
获取特定层次的模型权重很简单。所有模型权重都可以通过state_dict
函数访问。state_dict
函数返回一个字典,其中keys
为层,weights
为其值。以下代码演示了如何提取特定层的权重并可视化它们:
vgg.state_dict().keys()
cnn_weights = vgg.state_dict()['features.0.weight'].cpu()
上述代码给我们提供了以下输出:
每个框代表一个大小为3 x 3的滤波器的权重。每个滤波器都经过训练,用于识别图像中的特定模式。
总结
在本章中,我们学习了如何使用卷积神经网络构建图像分类器,以及如何使用预训练模型。我们探讨了通过使用预卷积特征加速训练过程的技巧。我们还研究了了解 CNN 内部运行情况的不同技术。
在下一章中,我们将学习如何使用循环神经网络处理序列数据。
第六章:序列数据的自然语言处理
在本章中,我们将看到不同的文本数据表示形式,这些形式对构建深度学习模型非常有用。我们将帮助您理解循环神经网络(RNNs)。本章将涵盖不同的 RNN 实现,如长短期记忆(LSTM)和门控循环单元(GRU),它们支持大多数文本和序列数据的深度学习模型。我们将研究文本数据的不同表示及其对构建深度学习模型的用处。此外,本章还将讨论可用于序列数据的一维卷积。
可以使用 RNN 构建的一些应用程序包括:
-
文档分类器:识别推文或评论的情感,分类新闻文章
-
序列到序列学习:用于诸如语言翻译、将英语转换为法语等任务
-
时间序列预测:根据前几天的销售记录预测商店的销售情况
本章将涵盖以下主题:
-
处理文本数据
-
通过构建情感分类器训练嵌入
-
使用预训练的词嵌入
-
递归神经网络
-
使用 LSTM 解决文本分类问题
-
序列数据上的卷积网络
-
语言建模
处理文本数据
文本是最常用的序列数据类型之一。文本数据可以看作是字符序列或单词序列。对于大多数问题,将文本视为单词序列是很常见的。深度学习的顺序模型,如 RNN 及其变种,能够从文本数据中学习重要的模式,以解决以下领域的问题:
-
自然语言理解
-
文档分类
-
情感分类
这些顺序模型也是各种系统的重要构建块,例如问答(QA)系统。
尽管这些模型在构建这些应用程序中非常有用,但由于其固有的复杂性,它们并不理解人类语言。这些顺序模型能够成功地发现有用的模式,然后用于执行不同的任务。将深度学习应用于文本是一个快速增长的领域,每个月都会出现许多新技术。我们将涵盖大多数现代深度学习应用程序的基本组件。
深度学习模型与其他机器学习模型一样,并不理解文本,因此我们需要将文本转换为数值表示。将文本转换为数值表示的过程称为向量化,可以通过以下不同方式完成:
-
将文本转换为单词,并将每个单词表示为向量
-
将文本转换为字符,并将每个字符表示为向量
-
创建单词的 n-gram,并将它们表示为向量
文本数据可以被分解为这些表示之一。文本的每个较小单元称为标记,将文本分解为标记的过程称为分词。Python 中有许多强大的库可以帮助我们进行分词。一旦我们将文本数据转换为标记,我们接下来需要将每个标记映射到一个向量。独热编码和词嵌入是将标记映射到向量的两种最流行的方法。以下图表总结了将文本转换为向量表示的步骤:
让我们更详细地了解分词、n-gram 表示和向量化。
分词
给定一个句子,将其分割为字符或单词称为分词。有一些库,比如 spaCy,提供了复杂的分词解决方案。让我们使用简单的 Python 函数如split
和list
来将文本转换为标记。
为了演示分词在字符和单词上的工作方式,让我们考虑一部电影Toy Story的简短评论。我们将使用以下文本:
Just perfect. Script, character, animation....this manages to break free of the yoke of 'children's movie' to simply be one of the best movies of the 90's, full-stop.
将文本转换为字符
Python 的list
函数接受一个字符串并将其转换为单个字符的列表。这完成了将文本转换为字符的任务。以下代码块展示了所使用的代码及其结果:
toy_story_review = "Just perfect. Script, character, animation....this manages to break free of the yoke of 'children's movie' to simply be one of the best movies of the 90's, full-stop."
print(list(toy_story_review))
结果如下:
['J', 'u', 's', 't', ' ', 'p', 'e', 'r', 'f', 'e', 'c', 't', '.', ' ', 'S', 'c', 'r', 'i', 'p', 't', ',', ' ', 'c', 'h', 'a', 'r', 'a', 'c', 't', 'e', 'r', ',', ' ', 'a', 'n', 'i', 'm', 'a', 't', 'i', 'o', 'n', '.', '.', '.', '.', 't', 'h', 'i', 's', ' ', 'm', 'a', 'n', 'a', 'g', 'e', 's', ' ', 't', 'o', ' ', 'b', 'r', 'e', 'a', 'k', ' ', 'f', 'r', 'e', 'e', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', 'y', 'o', 'k', 'e', ' ', 'o', 'f', ' ', "'", 'c', 'h', 'i', 'l', 'd', 'r', 'e', 'n', "'", 's', ' ', 'm', 'o', 'v', 'i', 'e', "'", ' ', 't', 'o', ' ', 's', 'i', 'm', 'p', 'l', 'y', ' ', 'b', 'e', ' ', 'o', 'n', 'e', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', 'b', 'e', 's', 't', ' ', 'm', 'o', 'v', 'i', 'e', 's', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', '9', '0', "'", 's', ',', ' ', 'f', 'u', 'l', 'l', '-', 's', 't', 'o', 'p', '.']
这个结果展示了我们简单的 Python 函数如何将文本转换为标记。
将文本转换为单词
我们将使用 Python 字符串对象中提供的split
函数来将文本分割成单词。split
函数接受一个参数,基于这个参数将文本分割成标记。在我们的示例中,我们将使用空格作为分隔符。以下代码块演示了如何使用 Python 的split
函数将文本转换为单词:
print(list(toy_story_review.split()))
这将产生以下输出:
['Just', 'perfect.', 'Script,', 'character,', 'animation....this', 'manages', 'to', 'break', 'free', 'of', 'the', 'yoke', 'of', "'children's", "movie'", 'to', 'simply', 'be', 'one', 'of', 'the', 'best', 'movies', 'of', 'the', "90's,", 'full-stop.']
在上述代码中,我们没有使用任何分隔符;默认情况下,split
函数在空格上分割。
N-gram 表示
我们看到文本如何表示为字符和单词。有时,查看两个、三个或更多单词一起的情况非常有用。N-grams是从给定文本中提取的单词组。在一个 n-gram 中,n表示可以一起使用的单词数。让我们看一个双字母词(n=2)的例子。我们使用 Python 的nltk
包为toy_story_review
生成了一个双字母词。以下代码块展示了双字母词的结果以及生成它的代码:
from nltk import ngrams
print(list(ngrams(toy_story_review.split(),2)))
这将产生以下输出:
[('Just', 'perfect.'), ('perfect.', 'Script,'), ('Script,', 'character,'), ('character,', 'animation....this'), ('animation....this', 'manages'), ('manages', 'to'), ('to', 'break'), ('break', 'free'), ('free', 'of'), ('of', 'the'), ('the', 'yoke'), ('yoke', 'of'), ('of', "'children's"), ("'children's", "movie'"), ("movie'", 'to'), ('to', 'simply'), ('simply', 'be'), ('be', 'one'), ('one', 'of'), ('of', 'the'), ('the', 'best'), ('best', 'movies'), ('movies', 'of'), ('of', 'the'), ('the', "90's,"), ("90's,", 'full-stop.')]
ngrams
函数接受一个单词序列作为其第一个参数,以及要分组的单词数作为第二个参数。以下代码块展示了三元组表示的样例以及用于生成它的代码:
print(list(ngrams(toy_story_review.split(),3)))
这将产生以下输出:
[('Just', 'perfect.', 'Script,'), ('perfect.', 'Script,', 'character,'), ('Script,', 'character,', 'animation....this'), ('character,', 'animation....this', 'manages'), ('animation....this', 'manages', 'to'), ('manages', 'to', 'break'), ('to', 'break', 'free'), ('break', 'free', 'of'), ('free', 'of', 'the'), ('of', 'the', 'yoke'), ('the', 'yoke', 'of'), ('yoke', 'of', "'children's"), ('of', "'children's", "movie'"), ("'children's", "movie'", 'to'), ("movie'", 'to', 'simply'), ('to', 'simply', 'be'), ('simply', 'be', 'one'), ('be', 'one', 'of'), ('one', 'of', 'the'), ('of', 'the', 'best'), ('the', 'best', 'movies'), ('best', 'movies', 'of'), ('movies', 'of', 'the'), ('of', 'the', "90's,"), ('the', "90's,", 'full-stop.')]
在上述代码中唯一改变的是n值,即函数的第二个参数。
许多监督学习机器学习模型,例如朴素贝叶斯,使用 n-gram 来改进其特征空间。n-gram 也用于拼写校正和文本摘要任务。
n-gram 表示的一个挑战是失去文本的顺序性质。它通常与浅层机器学习模型一起使用。这种技术在深度学习中很少使用,因为像 RNN 和 Conv1D 这样的架构会自动学习这些表示。
向量化
有两种常用方法将生成的标记映射到数字向量,称为 one-hot 编码和词嵌入。让我们通过编写一个简单的 Python 程序来理解如何将标记转换为这些向量表示。我们还将讨论每种方法的各种优缺点。
One-hot 编码
在 one-hot 编码中,每个标记由长度为 N 的向量表示,其中 N 是词汇表的大小。词汇表是文档中唯一单词的总数。让我们以一个简单的句子为例,观察每个标记如何表示为 one-hot 编码向量。以下是句子及其相关标记表示:
每天一个苹果,医生远离我说道医生。
以前面的句子为例,其 one-hot 编码可以表示为以下表格格式:
An | 100000000 |
---|---|
apple | 10000000 |
a | 1000000 |
day | 100000 |
keeps | 10000 |
doctor | 1000 |
away | 100 |
said | 10 |
the | 1 |
该表格描述了标记及其 one-hot 编码表示。向量长度为九,因为句子中有九个唯一单词。许多机器学习库已经简化了创建 one-hot 编码变量的过程。我们将编写自己的实现以便更容易理解,并可以使用相同的实现来构建后续示例所需的其他特性。以下代码包含一个Dictionary
类,其中包含创建唯一单词字典的功能,以及返回特定单词的 one-hot 编码向量的函数。让我们看一下代码,然后逐个功能进行解释:
class Dictionary(object):
def init (self):
self.word2index = {}
self.index2word = []
self.length = 0
def add_word(self,word):
if word not in self.index2word:
self.indexword.append(word)
self.word2index[word] = self.length + 1
self.length += 1
return self.word2index[word]
def len (self):
return len(self.index2word)
def onehot_encoded(self,word):
vec = np.zeros(self.length)
vec[self.word2index[word]] = 1
return vec
上述代码提供了三个重要功能:
-
初始化函数
init
创建一个word2index
字典,它将存储所有唯一单词及其索引。index2word
列表存储所有唯一单词,length
变量包含我们文档中的唯一单词总数。 -
add_word
函数接收一个单词并将其添加到word2index
和index2word
中,并增加词汇表的长度,前提是该单词是唯一的。 -
onehot_encoded
函数接收一个单词并返回一个长度为 N 的向量,全零,除了单词的索引处为一。如果传递的单词索引为二,则向量在索引为二处的值为一,其余所有值为零。
当我们定义了我们的Dictionary
类后,让我们在我们的toy_story_review
数据上使用它。下面的代码演示了如何构建word2index
字典以及如何调用我们的onehot_encoded
函数:
dic = Dictionary()
for tok in toy_story_review.split(): dic.add_word(tok)
print(dic.word2index)
使用单热表示的一个挑战是数据过于稀疏,而且随着词汇表中唯一单词数量的增加,向量的大小会迅速增长。另一个局限是单热没有表现词语之间的内部关系。因此,单热表示在深度学习中很少使用。
单词嵌入
单词嵌入是在由深度学习算法解决的问题中代表文本数据的一种非常流行的方式。单词嵌入提供了填充有浮点数的单词的密集表示。向量维度根据词汇量的大小而变化。在训练阶段通常使用维度大小为 50、100、256、300 或者有时 1000 的单词嵌入。维度大小是一个我们需要在训练阶段调整的超参数。
如果我们尝试用单热表示表示大小为 20,000 的词汇表,那么我们最终会得到 20,000 x 20,000 个数字,其中大多数将为零。相同的词汇表可以用 20,000 x 维度大小的单词嵌入表示,其中维度大小可以是 10、50、300 等等。
创建单词嵌入的一种方法是为每个标记开始时使用随机数生成密集向量,然后训练一个模型,如文档分类器,用于情感分类。代表标记的浮点数将被调整,以便语义上更接近的单词具有类似的表示。为了理解它,让我们看下面的图表,我们在其中绘制了五部电影的二维图上的单词嵌入向量:
前面的图表显示了如何调整密集向量,以便使语义上相似的单词之间的距离更小。由于电影标题如寻找尼莫、玩具总动员和超人特工队都是虚构的卡通电影,因此这些单词的嵌入更接近。另一方面,电影泰坦尼克号的嵌入远离卡通片,更接近电影恋恋笔记本,因为它们都是浪漫电影。
当你的数据量太少时,学习单词嵌入可能不可行,在这些情况下,我们可以使用由其他机器学习算法训练的单词嵌入。从其他任务生成的嵌入称为预训练单词嵌入。我们将学习如何构建自己的单词嵌入并使用预训练单词嵌入。
通过构建情感分类器来训练单词嵌入
在上一节中,我们简要介绍了词嵌入而没有实现它。在本节中,我们将下载一个名为 IMDb 的数据集,其中包含评论,并构建一个情感分类器,计算评论情感是积极的、消极的还是未知的。在构建过程中,我们还将对 IMDb 数据集中出现的单词进行词嵌入训练。
我们将使用一个名为 torchtext
的库,它使得许多过程(如下载、文本向量化和批处理)更加简单。训练情感分类器将涉及以下步骤:
-
下载 IMDb 数据并进行文本标记化
-
构建词汇表
-
生成向量批次
-
使用嵌入创建网络模型
-
训练模型
我们将在接下来的几节中详细介绍这些步骤。
下载 IMDb 数据并进行文本标记化
对于与计算机视觉相关的应用程序,我们使用了 torchvision
库,它为我们提供了许多实用函数,帮助构建计算机视觉应用程序。同样,还有一个名为 torchtext
的库,它专为与 PyTorch 一起工作而构建,通过提供不同的文本数据加载器和抽象来简化与自然语言处理(NLP)相关的许多活动。在撰写本文时,torchtext
并不随标准的 PyTorch 安装一起提供,需要单独安装。您可以在机器的命令行中运行以下代码来安装 torchtext
:
pip install torchtext
一旦安装完成,我们将能够使用它。 torchtext
下载提供了两个重要的模块,称为 torchtext.data
和 torchtext.datasets
。
我们可以从以下链接下载 IMDb 电影数据集:
grouplens.org/datasets/movielens/
使用 torchtext.data
进行标记化
torchtext.data
实例定义了一个 Field
类,它帮助我们定义数据的读取和标记化方式。让我们看下面的例子,我们将用它来准备我们的 IMDb 数据集:
from torchtext import data
text = data.Field(lower=True, batch_first=True,fix_length=20)
label = data.Field(sequential=False)
在上面的代码中,我们定义了两个 Field
对象,一个用于实际文本,另一个用于标签数据。对于实际文本,我们希望 torchtext
将所有文本转换为小写,标记化文本并将其修剪到最大长度为 20。如果我们正在构建用于生产环境的应用程序,可能会将长度固定为更大的数字。但是,对于示例,这样做效果很好。Field
构造函数还接受另一个名为 tokenize
的参数,默认使用 str.split
函数。我们也可以指定 spaCy
或任何其他分词器作为参数。对于我们的示例,我们将坚持使用 str.split
。
使用 torchtext.datasets
进行标记化
torchtext.datasets
实例提供了使用不同数据集的包装器,如 IMDb、TREC(问题分类)、语言建模(WikiText-2)和其他几个数据集。我们将使用torch.datasets
下载 IMDb 数据集并将其分割为训练集和测试集。以下代码执行此操作,第一次运行时可能需要几分钟(取决于您的宽带连接),因为它从互联网下载 IMDb 数据集:
train, test = datasets.IMDB.splits(text, label)
先前数据集的IMDB
类抽象了下载、分词和将数据库分为训练集和测试集的所有复杂性。train.fields
下载包含一个字典,其中TEXT
是键,LABEL
是值。让我们看看train.fields
及其每个元素train
包含的内容:
print('train.fields', train.fields)
这导致以下输出:
#Results
train.fields {'text': <torchtext.data.field.Field object at 0x1129db160>, 'label': <torchtext.data.field.Field object at 0x1129db1d0>}
类似地,训练数据集的方差如下:
print(vars(train[0]))
这导致以下输出:
#Results
vars(train[0]) {'text': ['for', 'a', 'movie', 'that', 'gets', 'no', 'respect', 'there', 'sure', 'are', 'a', 'lot', 'of', 'memorable', 'quotes', 'listed', 'for', 'this', 'gem.', 'imagine', 'a', 'movie', 'where', 'joe', 'piscopo', 'is', 'actually', 'funny!', 'maureen', 'stapleton', 'is', 'a', 'scene', 'stealer.', 'the', 'moroni', 'character', 'is', 'an', 'absolute', 'scream.', 'watch', 'for', 'alan', '"the', 'skipper"', 'hale', 'jr.', 'as', 'a', 'police', 'sgt.'], 'label': 'pos'}
我们可以从这些结果中看到,单个元素包含一个字段和文本,以及表示文本的所有标记,以及一个包含文本标签的label
字段。现在我们已经准备好进行 IMDb 数据集的批处理。
构建词汇表
当我们为toy_story_review
创建了一种一热编码时,我们创建了一个word2index
字典,它被称为词汇表,因为它包含了文档中唯一单词的所有详细信息。torchtext
实例使这一切变得更加容易。一旦数据加载完成,我们可以调用build_vocab
并传递必要的参数来构建数据的词汇表。以下代码展示了词汇表是如何构建的:
text.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
label.build_vocab(train)
在上述代码中,我们传递了需要构建词汇表的train
对象,并要求它使用维度为300
的预训练嵌入来初始化向量。build_vocab
对象只是下载并创建稍后在使用预训练权重训练情感分类器时将使用的维度。max_size
实例限制了词汇表中单词的数量,而min_freq
删除了任何出现次数不到 10 次的单词,其中10
是可配置的。
一旦词汇表建立完成,我们可以获取不同的值,如频率、词索引和每个单词的向量表示。以下代码演示了如何访问这些值:
print(text.vocab.freqs)
这导致以下输出:
# A sample result
Counter({"i'm": 4174,
'not': 28597,
'tired': 328,
'to': 133967,
'say': 4392,
'this': 69714,
'is': 104171,
'one': 22480,
'of': 144462,
'the': 322198,
以下代码演示了如何访问结果:
print(text.vocab.vectors)
这导致以下输出:
#Results displaying the 300 dimension vector for each word.
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.0466 0.2132 -0.0074 ... 0.0091 -0.2099 0.0539
... ... ...
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.7724 -0.1800 0.2072 ... 0.6736 0.2263 -0.2919
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
[torch.FloatTensor of size 10002x300]
类似地,我们将打印包含单词及其索引的字典的值如下:
print(TEXT.vocab.stoi)
这导致以下输出:
# Sample results
defaultdict(<function torchtext.vocab._default_unk_index>,
{'<unk>': 0,
'<pad>': 1,
'the': 2,
'a': 3,
'and': 4,
'of': 5,
'to': 6,
'is': 7,
'in': 8,
'i': 9,
'this': 10,
'that': 11,
'it': 12,
stoi
值提供了访问包含单词及其索引的字典。
生成向量的批次
torchtext
下载提供了BucketIterator
,它有助于批处理所有文本并用单词的索引号替换单词。BucketIterator
实例带有许多有用的参数,例如batch_size
、device
(GPU 或 CPU)和shuffle
(数据是否需要洗牌)。以下代码演示了如何创建为训练和测试数据集生成批次的迭代器:
train_iter, test_iter = data.BucketIterator.splits((train, test), batch_size=128, device=-1,shuffle=True)
#device = -1 represents cpu , if you want gpu leave it to None.
前面的代码为训练和测试数据集提供了一个BucketIterator
对象。下面的代码展示了如何创建一个批次并显示批次的结果:
batch = next(iter(train_iter)) batch.text
这导致以下输出:
#Results
Variable containing:
5128 427 19 ... 1688 0 542
58 2 0 ... 2 0 1352
0 9 14 ... 2676 96 9
... ... ...
129 1181 648 ... 45 0 2
6484 0 627 ... 381 5 2
748 0 5052 ... 18 6660 9827
[torch.LongTensor of size 128x20]
我们将按以下方式打印标签:
batch.label
这导致以下输出:
#Results
Variable containing:
2
1
2
1
2
1
1
1
[torch.LongTensor of size 128]
根据前面代码块的结果,我们可以看到文本数据如何转换为大小为(batch_size
* fix_len
)的矩阵,即(128 x 20)。
创建一个带有嵌入的网络模型
我们之前简要讨论了词嵌入。在本节中,我们将词嵌入作为网络架构的一部分创建,并训练整个模型以预测每个评论的情感。在训练结束时,我们将得到一个情感分类器模型以及 IMDB 数据集的词嵌入。以下代码演示了如何创建一个网络架构来使用词嵌入来预测情感:
class EmbeddingNetwork(nn.Module):
def init(self,emb_size,hidden_size1,hidden_size2=400):
super(). init ()
self.embedding = nn.Embedding(emb_size,hidden_size1)
self.fc = nn.Linear(hidden_size2,3)
def forward(self,x):
embeds = self.embedding(x).view(x.size(0),-1)
out = self.fc(embeds)
return F.log_softmax(out,dim=-1)
在上述代码中,EmbeddingNetwork
创建了情感分类的模型。在_init_
函数内部,我们初始化了nn.Embedding
类的一个对象,它接受两个参数,即词汇量的大小和我们希望为每个单词创建的维度。由于我们限制了唯一单词的数量,词汇量大小将为 10,000,并且我们可以从一个小的嵌入大小 10 开始。在快速运行程序时,使用小的嵌入大小是有用的,但是当您尝试构建用于生产系统的应用程序时,请使用较大的嵌入。我们还有一个线性层,将单词嵌入映射到类别(积极、消极或未知)。
前向函数确定如何处理输入数据。对于批次大小为 32 和句子最大长度为 20 个单词,我们将会得到形状为 32 x 20 的输入。第一个嵌入层充当查找表,用相应的嵌入向量替换每个单词。对于嵌入维度为 10,输出变为 32 x 20 x 10,因为每个单词被其对应的嵌入所替换。view()
函数将会展平来自嵌入层的结果。传递给 view 的第一个参数将保持该维度不变。
在我们的情况下,我们不希望将来自不同批次的数据合并,因此我们保留第一维并展平张量中的其余值。应用view()
函数后,张量形状变为 32 x 200。一个密集层将展平的嵌入映射到类别的数量。一旦网络定义好了,我们可以像往常一样训练网络。
请记住,在这个网络中,我们失去了文本的顺序性,我们只是将文本视为一袋词语。
训练模型
训练模型非常类似于我们构建图像分类器时所看到的,因此我们将使用相同的函数。我们通过模型传递数据批次,计算输出和损失,然后优化模型权重,包括嵌入权重。以下代码执行此操作:
def fit(epoch,model,data_loader,phase='training',volatile=False):
if phase == 'training':
model.train()
if phase == 'validation':
model.evaluation()
volatile=True
running_loss = 0.0
running_correct = 0
现在我们对数据集进行迭代:
for batch_idx , batch in enumerate(data_loader):
text, target = batch.text , batch.label
if is_cuda:
text,target = text.cuda(),target.cuda()
if phase == 'training':
optimizer.zero_grad()
output = model(text)
loss = F.nll_loss(output,target)
running_loss += F.nll_loss(output,target,size_average=False).data[0]
predictions = output.data.max(dim=1,keepdim=True)[1]
running_correct += predictions.eq(target.data.view_as(predictions)).cpu().sum()
if phase == 'training':
loss.backward()
optimizer.step()
loss = running_loss/len(data_loader.dataset)
accuracy = 100\. * running_correct/len(data_loader.dataset)
print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}').format(loss,accuracy)
从这里开始,我们可以在每个 epoch 上训练模型:
train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
train_iter.repeat = False
test_iter.repeat = False
for epoch in range(1,10):
epoch_loss, epoch_accuracy = fit(epoch,model,train_iter,phase='training')
validation_epoch_loss, validation_epoch_accuracy = fit(epoch,model,test_iter,phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
在上述代码中,我们通过传递用于批处理数据的BucketIterator
对象来调用fit
方法。默认情况下,迭代器不会停止生成批次,因此我们必须将BucketIterator
对象的 repeat 变量设置为False
。如果不将 repeat 变量设置为False
,那么fit
函数将无限运行。在大约 10 个 epoch 的训练后,模型达到了约 70%的验证准确率。现在您已经学会了通过构建情感分类器训练词嵌入,让我们在下一节中学习如何使用预训练词嵌入。
使用预训练词嵌入
预训练词嵌入在我们工作于特定领域时非常有用,例如医学和制造业,在这些领域中我们有大量数据可以用来训练嵌入。当我们的数据很少且不能有意义地训练嵌入时,我们可以使用在不同数据语料库(如维基百科、Google 新闻和 Twitter 推文)上训练的嵌入。许多团队都有使用不同方法训练的开源词嵌入。在本节中,我们将探讨torchtext
如何使使用不同词嵌入更加容易,以及如何在我们的 PyTorch 模型中使用它们。这类似于在计算机视觉应用中使用的迁移学习。通常,使用预训练嵌入涉及以下步骤:
-
下载嵌入
-
加载模型中的嵌入
-
冻结嵌入层权重
让我们详细探讨每个步骤的实现方式。
下载嵌入
torchtext
库在下载嵌入和将其映射到正确单词时,抽象掉了许多复杂性。torchtext
库在vocab
模块中提供了三个类,即 GloVe、FastText、CharNGram,它们简化了下载嵌入和映射到我们词汇表的过程。每个类都提供了在不同数据集上训练的不同嵌入,并使用不同技术。让我们看一些不同的提供的嵌入:
-
charngram.100d
-
fasttext.en.300d
-
fasttext.simple.300d
-
glove.42B.300d
-
glove.840B.300d
-
glove.twitter.27B.25d
-
glove.twitter.27B.50d
-
glove.twitter.27B.100d
-
`glove.twitter.27B.200d`
-
glove.6B.50d
-
glove.6B.100d
-
glove.6B.200d
-
glove.6B.300d
Field
对象的build_vocab
方法接受一个用于嵌入的参数。以下代码解释了如何下载嵌入:
from torchtext.vocab import GloVe
TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train,)
参数向量的值表示要使用的嵌入类。name
和 dim
参数确定可以使用哪些嵌入。我们可以轻松地从 vocab
对象中访问嵌入。下面的代码演示了它,以及结果将如何显示:
TEXT.vocab.vectors
这导致以下输出:
#Output
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.0000 0.0000 0.0000 ... 0.0000 0.0000 0.0000
0.0466 0.2132 -0.0074 ... 0.0091 -0.2099 0.0539
... ... ...
[torch.FloatTensor of size 10002x300]
现在我们已经下载并将嵌入映射到我们的词汇表中。让我们了解如何在 PyTorch 模型中使用它们。
在模型中加载嵌入
vectors
变量返回一个形状为 vocab_size
x dimensions
的 torch 张量,其中包含预训练的嵌入。我们必须将嵌入分配给我们嵌入层的权重。我们可以通过访问嵌入层的权重来赋值嵌入的权重,如以下代码所示:
model.embedding.weight.data = TEXT.vocab.vectors
model
下载代表我们网络的对象,embedding
代表嵌入层。由于我们使用了新维度的嵌入层,线性层的输入将会有所变化。下面的代码展示了新的架构,与我们之前训练嵌入时使用的架构类似:
class EmbeddingNetwork(nn.Module):
def init (self,embedding_size,hidden_size1,hidden_size2=400): super(). init ()
self.embedding = nn.Embedding(embedding_size,hidden_size1) self.fc1 = nn.Linear(hidden_size2,3)
def forward(self,x):
embeds = self.embedding(x).view(x.size(0),-1) out = self.fc1(embeddings)
return F.log_softmax(out,dim=-1)
model = EmbeddingNetwork(len(TEXT.vocab.stoi),300,12000)
一旦加载了嵌入,我们必须确保在训练期间不改变嵌入权重。让我们讨论如何实现这一点。
冻结嵌入层的权重
要告诉 PyTorch 不要改变嵌入层的权重是一个两步过程:
-
将
requires_grad
属性设置为False
,这告诉 PyTorch 不需要这些权重的梯度。 -
删除嵌入层参数传递给优化器。如果不执行此步骤,则优化器会抛出错误,因为它期望所有参数都有梯度。
下面的代码演示了冻结嵌入层权重以及指导优化器不使用这些参数是多么简单:
model.embedding.weight.requires_grad = False
optimizer = optim.SGD([ param for param in model.parameters() if param.requires_grad == True],lr=0.001)
我们通常将所有模型参数传递给优化器,但在之前的代码中,我们传递了 requires_grad
为 True
的参数。
我们可以使用这段代码训练模型,并且应该达到类似的准确性。所有这些模型架构都未能充分利用文本的顺序特性。在下一节中,我们探讨了两种流行的技术,即 RNN 和 Conv1D,它们利用了数据的顺序特性。
递归神经网络
RNNs 是我们能够应对分类、序列数据标记、生成文本序列(例如 SwiftKey 键盘应用程序,预测下一个单词)、以及将一种序列转换为另一种序列(比如从法语翻译成英语)等应用中最强大的模型之一。大多数模型架构,如前馈神经网络,并不利用数据的序列性质。例如,我们需要数据来呈现每个示例的特征,以向量形式表示,比如表示句子、段落或文档的所有标记。前馈网络仅设计为一次性查看所有特征并将其映射到输出。让我们看一个文本示例,说明为什么文本的顺序或序列性质很重要。I had cleaned my car 和 I had my car cleaned 是两个英文句子,它们有相同的单词集合,但考虑单词顺序时它们的含义不同。
在大多数现代语言中,人类通过从左到右阅读单词并构建一个强大的模型来理解文本数据的各种内容。RNN 类似地通过逐次查看文本中的每个单词来工作。RNN 也是一种神经网络,其中有一个特殊的层,该层循环处理数据而不是一次性处理所有数据。由于 RNN 可以处理序列数据,我们可以使用不同长度的向量并生成不同长度的输出。以下图表提供了一些不同的表示方式:
前面的图是来自有关 RNN 的著名博客之一(karpathy.github. io/2015/05/21/rnn-effectiveness
),作者 Andrej Karpathy 在其中讲述了如何使用 Python 从零开始构建 RNN 并将其用作序列生成器。
通过示例理解 RNN 的工作方式
让我们假设我们已经构建了一个 RNN 模型,并试图理解它提供的功能。一旦我们理解了 RNN 的功能,我们再探讨 RNN 内部发生的事情。
让我们将 Toy Story 的评论作为 RNN 模型的输入。我们正在查看的示例文本是 Just perfect. Script, character, animation....this manages to break free....。我们从将第一个单词 just 传递给我们的模型开始,模型生成两种不同的东西:一个状态向量和一个输出向量。状态向量在模型处理评论中的下一个单词时被传递,并生成一个新的状态向量。我们只考虑模型在最后一个序列期间生成的输出。以下图表总结了这一点:
上述图示演示了以下内容:
-
通过展开文本输入和图像来理解 RNN 的工作方式
-
状态如何递归地传递给同一个模型
到目前为止,您已经对 RNN 的工作有了一定了解,但不知道其具体工作原理。在我们深入研究其工作原理之前,让我们看一下展示我们所学内容更详细的代码片段。我们仍将视 RNN 为一个黑箱:
rnn = RNN(input_size, hidden_size,output_size)
for i in range(len(toy_story_review):
output, hidden = rnn(toy_story_review[i], hidden)
在上述代码中,hidden
变量表示状态向量,有时称为隐藏状态。现在,我们应该对 RNN 的使用有所了解了。接下来,让我们看一下实现 RNN 并理解 RNN 内部发生了什么的代码。以下代码包含 RNN
类:
import torch.nn as nn
from torch.autograd import Variable
class RNN(nn.Module):
def init (self, input_size, hidden_size, output_size):
super(RNN, self). init ()
self.hidden_size = hidden_size
self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
self.i2o = nn.Linear(input_size + hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, input, hidden):
combined = torch.cat((input, hidden), 1)
hidden = self.i2h(combined)
output = self.i2o(combined)
output = self.softmax(output)
return output, hidden
def initHidden(self):
return Variable(torch.zeros(1, self.hidden_size))
除了上述代码中的 RNN 一词外,其他内容听起来与我们在前几章中使用的内容非常相似,因为 PyTorch 隐藏了很多反向传播的复杂性。让我们详细查看 __init__
函数和 forward
函数,了解其中发生了什么。
__init__
函数初始化了两个线性层,一个用于计算输出,另一个用于计算状态或隐藏向量。
forward
函数将输入向量和隐藏向量组合,并通过两个线性层传递,生成输出向量和隐藏状态。对于输出层,我们应用 log_softmax
函数。
initHidden
函数有助于在第一次调用 RNN 时创建没有状态的隐藏向量。让我们通过下面的图示来直观了解 RNN 类的功能:
上述图示展示了 RNN 的工作原理。
RNN 的概念有时在第一次接触时可能难以理解,因此我强烈推荐阅读以下链接提供的一些令人惊叹的博客:karpathy.github.io/2015/05/21/rnn-effectiveness/
和 colah.github.io/posts/2015-08-Understanding-LSTMs/.
在下一节中,我们将学习如何使用称为 LSTM 的 RNN 变体构建 IMDB 数据集上的情感分类器。
使用 LSTM 解决文本分类问题
RNN 在构建实际应用中非常流行,例如语言翻译、文本分类等多种顺序问题。然而,在现实中,我们很少使用简单版本的 RNN,比如我们在前一节中看到的那种。简单版本的 RNN 存在问题,如处理大序列时的梯度消失和梯度爆炸。在大多数实际问题中,使用诸如 LSTM 或 GRU 等 RNN 变体,这些变体解决了普通 RNN 的限制,并且能更好地处理顺序数据。我们将尝试理解 LSTM 的工作原理,并基于 LSTM 构建网络,解决 IMDB 数据集上的文本分类问题。
长期依赖
理论上,RNN 应该从历史数据中学习所有必需的依赖关系,以建立下一个事件的上下文。例如,我们试图预测句子“The clouds are in the sky.”中的最后一个单词。RNN 可以预测,因为信息(clouds)仅在几个单词之后。让我们再来看一个长段落,依赖关系不需要那么紧密,我们想要预测其中的最后一个单词。这个句子是:“I am born in Chennai a city in Tamilnadu. Did schooling in different states of India and I speak...”在实践中,传统的 RNN 版本很难记住前面序列中发生的上下文。LSTMs 及其他 RNN 的不同变体通过在 LSTM 内部添加不同的神经网络来解决这个问题,稍后这些网络会决定可以记住多少或者可以记住什么数据。
LSTM 网络
LSTMs 是一种特殊类型的 RNN,能够学习长期依赖关系。它们于 1997 年引入,并在最近几年因可用数据和硬件的进步而变得流行。它们在各种问题上表现出色,并被广泛应用。
LSTMs 通过设计来避免长期依赖问题,自然而然地记住信息长时间。在 RNN 中,我们看到它们在序列的每个元素上重复自己。在标准 RNN 中,重复模块将具有类似于单个线性层的简单结构。
下图显示了一个简单的循环神经网络是如何重复自身的:
在 LSTM 内部,我们没有使用简单的线性层,而是在 LSTM 内部有更小的网络,这些网络执行独立的工作。下图展示了 LSTM 内部的情况:
图片来源:colah.github.io/posts/2015-08-Understanding-LSTMs/
(由 Christopher Olah 绘制的图表)
在上述图中第二个框中,每个小矩形(黄色)框代表一个 PyTorch 层,圆圈代表一个元素矩阵或向量的加法,而合并线表示两个向量正在被串联。好处在于,我们无需手动实现所有这些。大多数现代深度学习框架提供了一个抽象,可以处理 LSTM 内部的所有功能。PyTorch 提供了nn.LSTM
层内部所有功能的抽象,我们可以像使用任何其他层一样使用它。
LSTM 中最重要的是单元状态,它通过前面图表中的所有迭代表示为跨单元的水平线。 LSTM 内的多个网络控制信息如何在单元状态之间传播。 LSTM 中的第一步(由符号 σ 表示的小网络)是决定从单元状态中丢弃哪些信息。该网络称为遗忘门,并且具有 sigmoid 作为激活函数,输出每个元素在单元状态中的取值介于 0 和 1 之间。该网络(PyTorch 层)用以下公式表示:
网络中的值决定了哪些值将保留在单元状态中,哪些将被丢弃。下一步是决定我们将添加到单元状态中的信息。这有两部分组成:一个称为输入门的 sigmoid 层,它决定要更新的值,以及一个创建新值添加到单元状态的 tanh 层。数学表示如下:
在下一步中,我们将输入门和 tanh 生成的两个值组合起来。现在,我们可以通过遗忘门与其和 Ct 乘积之和的逐元素乘法来更新单元状态,如下公式所示:
最后,我们需要决定输出,这将是单元状态的筛选版本。 LSTM 有不同的版本,大多数都采用类似的原理。作为开发人员或数据科学家,我们很少需要担心 LSTM 内部发生了什么。
如果您想更深入了解它们,请阅读以下博客链接,以非常直观的方式涵盖了许多理论内容。
查看 Christopher Olah 的关于 LSTM 的精彩博客(colah.github.io/posts/2015-08-Understanding-LSTMs
),以及 Brandon Rohrer 的另一篇博客(brohrer.github.io/how_rnns_lstm_work.html
),他在一个很棒的视频中解释了 LSTM。
既然我们理解了 LSTM,让我们实现一个 PyTorch 网络,用于构建情感分类器。像往常一样,我们将遵循以下步骤来创建分类器:
-
数据准备
-
创建批次
-
创建网络
-
模型训练
我们将在接下来的章节详细讨论这些步骤。
数据准备
我们使用相同的 torchtext
库来下载、分词化和构建 IMDB 数据集的词汇表。在创建 Field
对象时,我们将 batch_first
参数保留为 False
。RNN 需要数据的形式为 sequence_length
、batch_size
和 features.
用于准备数据集的步骤如下:
TEXT = data.Field(lower=True,fix_length=200,batch_first=False)
LABEL = data.Field(sequential=False,)
train, test = IMDB.splits(TEXT, LABEL)
TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train,)
创建批次
我们使用 torchtext BucketIterator
函数创建批次,批次的大小将是序列长度和批次大小。对于我们的情况,大小将是 [200, 32],其中 200 是序列长度,32 是批次大小。
以下是用于批处理的代码:
train_iter, test_iter = data.BucketIterator.splits((train, test), batch_size=32, device=-1)
train_iter.repeat = False
test_iter.repeat = False
创建网络
让我们看一下代码,然后逐步理解。您可能会对代码看起来多么熟悉感到惊讶:
class IMDBRnn(nn.Module):
def init (self,vocab,hidden_size,n_cat,bs=1,nl=2):
super(). init ()
self.hidden_size = hidden_size
self.bs = bs
self.nl = nl
self.e = nn.Embedding(n_vocab,hidden_size)
self.rnn = nn.LSTM(hidden_size,hidden_size,nl)
self.fc2 = nn.Linear(hidden_size,n_cat)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self,inp):
bs = inp.size()[1]
if bs != self.bs:
self.bs = bs
e_out = self.e(inp)
h0 = c0 = Variable(e_out.data.new(*(self.nl,self.bs,self.hidden_size)).zero_())
rnn_o,_ = self.rnn(e_out,(h0,c0))
rnn_o = rnn_o[-1]
fc = F.dropout(self.fc2(rnn_o),p=0.8)
return self.softmax(fc)
init
方法创建一个大小为词汇表大小和 hidden_size
的嵌入层。它还创建了一个 LSTM 和一个线性层。最后一层是一个 LogSoftmax
层,用于将线性层的结果转换为概率。
在forward
函数中,我们传入大小为 [200, 32] 的输入数据,经过嵌入层处理,批次中的每个标记都被嵌入取代,大小变为 [200, 32, 100],其中 100 是嵌入维度。LSTM 层接收嵌入层的输出和两个隐藏变量。这些隐藏变量应与嵌入输出的类型相同,它们的大小应为 [num_layers
, batch_size
, hidden_size
]。LSTM 按顺序处理数据,并生成形状为 [Sequence_length
, batch_size
, hidden_size
] 的输出,其中每个序列索引表示该序列的输出。在这种情况下,我们只取最后一个序列的输出,其形状为 [batch_size
, hidden_dim
],并将其传递给线性层,将其映射到输出类别。由于模型容易过拟合,添加一个 dropout 层。您可以调整 dropout 的概率。
训练模型
网络创建完成后,我们可以使用与之前示例中相同的代码训练模型。以下是训练模型的代码:
model = IMDBRnn(n_vocab,n_hidden,3,bs=32)
model = model.cuda()
optimizer = optim.Adam(model.parameters(),lr=1e-3)
def fit(epoch,model,data_loader,phase='training',volatile=False):
if phase == 'training':
model.train()
if phase == 'validation':
model.eval()
volatile=True
running_loss = 0.0
running_correct = 0
for batch_idx , batch in enumerate(data_loader):
text , target = batch.text , batch.label
if is_cuda:
text,target = text.cuda(),target.cuda()
if phase == 'training':
optimizer.zero_grad()
output = model(text)
loss = F.nll_loss(output,target)
running_loss += F.nll_loss(output,target,size_average=False).data[0]
preds = output.data.max(dim=1,keepdim=True)[1]
running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
if phase == 'training':
loss.backward()
optimizer.step()
loss = running_loss/len(data_loader.dataset)
accuracy = 100\. * running_correct/len(data_loader.dataset)
print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}') return loss,accuracy
train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,5):
epoch_loss, epoch_accuracy =
fit(epoch,model,train_iter,phase='training')
validation_epoch_loss , validation_epoch_accuracy =
fit(epoch,model,test_iter,phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
下面是训练模型的结果:
training loss is 0.7 and training accuracy is 12564/25000 50.26
validation loss is 0.7 and validation accuracy is 12500/25000 50.0
training loss is 0.66 and training accuracy is 14931/25000 59.72
validation loss is 0.57 and validation accuracy is 17766/25000 71.06
training loss is 0.43 and training accuracy is 20229/25000 80.92
validation loss is 0.4 and validation accuracy is 20446/25000 81.78
training loss is 0.3 and training accuracy is 22026/25000 88.1
validation loss is 0.37 and validation accuracy is 21009/25000 84.04
对模型进行四个 epoch 的训练得到了 84% 的准确率。再训练更多 epoch 导致过拟合,因为损失开始增加。我们可以尝试一些技术,如减小隐藏维度、增加序列长度和使用较小的学习率来进一步提高准确性。
我们还将探讨如何在序列数据上使用一维卷积。
序列数据上的卷积网络
我们通过学习 第四章 深度学习在计算机视觉中的应用 中图像中 CNN 如何通过学习图像特征来解决计算机视觉问题。在图像中,CNN 通过在高度和宽度上进行卷积来工作。同样地,时间可以被视为卷积特征。一维卷积有时比 RNN 更好,并且计算成本更低。在过去几年中,像 Facebook 这样的公司展示了在音频生成和机器翻译方面的成功。在本节中,我们将学习如何使用 CNN 构建文本分类解决方案。
理解序列数据的一维卷积
在第四章,计算机视觉的深度学习,我们已经看到如何从训练数据中学习二维权重。这些权重在图像上移动以生成不同的激活。同样,一维卷积激活在训练我们的文本分类器时也是通过移动这些权重来学习模式。以下图示解释了一维卷积的工作原理:
对 IMDB 数据集上的文本分类器进行训练时,我们将按照使用 LSTMs 构建分类器时遵循的相同步骤进行操作。唯一改变的是,我们使用batch_first =
True
,而不像我们的 LSTM 网络那样。所以,让我们看看网络、训练代码以及其结果。
创建网络
让我们先看看网络架构,然后逐步看代码:
class IMDBCnn(nn.Module):
def
__init__(self,vocab,hidden_size,n_cat,bs=1,kernel_size=3,max_len=200): super().__init__()
self.hidden_size = hidden_size
self.bs = bs
self.e = nn.Embedding(n_vocab,hidden_size)
self.cnn = nn.Conv1d(max_len,hidden_size,kernel_size)
self.avg = nn.AdaptiveAvgPool1d(10)
self.fc = nn.Linear(1000,n_cat)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self,inp):
bs = inp.size()[0]
if bs != self.bs:
self.bs = bs
e_out = self.e(inp)
cnn_o = self.cnn(e_out)
cnn_avg = self.avg(cnn_o)
cnn_avg = cnn_avg.view(self.bs,-1)
fc = F.dropout(self.fc(cnn_avg),p=0.5)
return self.softmax(fc)
在前面的代码中,我们不再使用 LSTM 层,而是使用了Conv1d
层和AdaptiveAvgPool1d
层。卷积层接受序列长度作为其输入大小,输出大小为隐藏大小,核大小为三。由于我们必须改变线性层的维度,所以每次我们尝试使用不同长度运行时,我们使用AdaptiveAvgpool1d
层,它接受任何大小的输入并生成给定大小的输出。因此,我们可以使用一个大小固定的线性层。代码的其余部分与大多数网络架构中看到的相似。
训练模型
模型的训练步骤与前面的示例相同。让我们看看调用fit
方法和生成的结果的代码:
train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,5):
epoch_loss, epoch_accuracy =
fit(epoch,model,train_iter,phase='training')
validation_epoch_loss , validation_epoch_accuracy = fit(epoch,model,test_iter,phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
我们对模型进行了四个 epoch 的训练,得到了大约 83%的准确率。以下是运行模型的结果:
training loss is 0.59 and training accuracy is 16724/25000 66.9
validation loss is 0.45 and validation accuracy is 19687/25000 78.75
training loss is 0.38 and training accuracy is 20876/25000 83.5
validation loss is 0.4 and validation accuracy is 20618/25000 82.47
training loss is 0.28 and training accuracy is 22109/25000 88.44
validation loss is 0.41 and validation accuracy is 20713/25000 82.85
training loss is 0.22 and training accuracy is 22820/25000 91.28
validation loss is 0.44 and validation accuracy is 20641/25000 82.56
自从三个 epoch 后验证损失开始增加,我停止了模型的运行。我们可以尝试几件事来改进结果,例如使用预训练权重、添加另一个卷积层以及在卷积之间尝试使用MaxPool1d
层。我把这些尝试留给你来测试是否有助于提高准确性。现在我们已经学习了处理序列数据的各种神经网络,让我们在下一节中看看语言建模。
语言建模
语言建模是在给定前几个单词的情况下预测下一个单词的任务。生成这种顺序数据的能力在许多不同领域都有应用,如下所示:
-
图像字幕
-
语音识别
-
语言翻译
-
自动邮件回复
-
写故事、新闻文章、诗歌等
最初,这一领域的关注点主要集中在 RNNs,特别是 LSTMs 上。然而,自 2017 年引入 Transformer 架构(arxiv.org/pdf/1706.03762.pdf
)后,在 NLP 任务中变得普遍。此后出现了许多 Transformer 的修改版本,其中一些我们将在本章中介绍。
预训练模型
近年来,预训练模型在 NLP 任务中的使用引起了广泛关注。使用预训练语言模型的一个关键优势是它们能够用更少的数据学习。这些模型特别适用于标记数据稀缺的语言,因为它们只需要标记数据。
2015 年,戴安哲和 Q.V.勒在题为半监督序列学习的论文中首次提出了用于序列学习的预训练模型(arxiv.org/abs/1511.01432
)。然而,直到最近,它们才被证明在广泛的任务中具有益处。现在我们将考虑近年来这一领域中一些值得注意的最新进展,其中包括但不限于以下内容:
-
语言模型的嵌入(ELMo)
-
双向编码器表示来自 Transformers(BERT)
-
生成预训练变压器 2(GPT-2)
语言模型的嵌入
2018 年 2 月,M. Peters 等人发表了深度上下文化的单词表示论文(arxiv.org/abs/1802.05365
),介绍了 ELMo。本质上,它证明了语言模型嵌入可以作为目标模型中的特征,如下图所示:
ELMo 使用双向语言模型来学习单词和上下文。正向和反向传递的内部状态在每个单词处被串联起来,以产生一个中间向量。正是模型的双向性质使其获得关于句子中下一个单词和之前单词的信息。
双向编码器表示来自 Transformers
谷歌在 2018 年 11 月发布的后续论文(arxiv.org/pdf/1810.04805.pdf
)中提出了双向编码器表示来自 Transformers(BERT),它融入了一个注意力机制,学习词与文本之间的上下文关系:
与 ELMo 不同,文本输入是按顺序(从左到右或从右到左)读取的,但是 BERT 会一次性读取整个单词序列。本质上,BERT 是一个经过训练的 Transformer 编码器堆栈。
生成预训练变压器 2
在撰写本文时,OpenAI 的 GPT-2 是设计用于提高生成文本的逼真度和连贯性的最先进的语言模型之一。它是在 2019 年 2 月的论文Language Models are Unsupervised Multi-task Learners(d4mucfpksywv.cloudfront.net/better-language-models/language_models_are_unsupervised_multitask_learners.pdf
)中介绍的。它被训练用于预测 800 万个网页(总共 40GB 的文本),参数达到 15 亿个,是 BERT 的四倍多。以下是 OpenAI 关于 GPT-2 的说法:
GPT-2 生成连贯的段落文本,在许多语言建模基准上取得了最先进的性能,并在基本阅读理解、机器翻译、问题回答和摘要等方面表现出色,所有这些都没有经过特定任务的训练。
最初,OpenAI 表示他们不会发布数据集、代码或完整的 GPT-2 模型权重。这是因为他们担心这些内容会被用于大规模生成欺骗性、偏见性或滥用性语言。这些模型应用于恶意目的的示例如下:
-
逼真的假新闻文章
-
在线实际模仿其他人
-
可能发布在社交媒体上的滥用或伪造内容
-
自动生产垃圾邮件或钓鱼内容
然后团队决定分阶段发布模型,以便让人们有时间评估社会影响并在每个阶段发布后评估其影响。
PyTorch 实现
有一个来自开发者 Hugging Face 的流行 GitHub 仓库,其中实现了基于 PyTorch 的 BERT 和 GPT-2。可以在以下网址找到该仓库:github.com/huggingface/pytorch-pretrained-BERT
。该仓库最早于 2018 年 11 月发布,并允许用户从自己的数据中生成句子。它还包括多种可用于测试不同模型在不同任务(如问题回答、标记分类和序列分类)中应用效果的类。
下面的代码片段演示了如何从 GitHub 仓库中的代码使用 GPT-2 模型生成文本。首先,我们导入相关的库并初始化预训练信息如下:
import torch
from torch.nn import functional as F
from pytorch_pretrained_bert import GPT2Tokenizer, GPT2LMHeadModel
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
gtp2model = GPT2LMHeadModel.from_pretrained('gpt2')
在这个例子中,我们将模型提供 'We like unicorns because they'
这个句子,然后它生成如下所示的词语:
input_text = tokenizer.encode('We like unicorns because they')
input, past = torch.tensor([input_text]), None
for _ in range(25):
logits, past = gtp2model(input, past=past)
input = torch.multinomial(F.softmax(logits[:, -1]), 1)
input_text.append(input.item())
以下是输出:
GPT-2 游乐场
还有另一个有用的 GitHub 存储库来自开发者 ilopezfr,可以在以下链接找到:github.com/ilopezfr/gpt-2
。它还提供了一个 Google Colab 的笔记本,允许用户与 OpenAI GPT-2 模型进行交互和实验(colab.research.google.com/github/ilopezfr/gpt-2/blob/master/gpt-2-playground_.ipynb
)。
下面是游乐场不同部分的一些示例:
- 文本完成部分:
- 问答部分:
- 翻译部分:
摘要
在本章中,我们学习了不同的技术来表示深度学习中的文本数据。我们学习了如何在处理不同领域时使用预训练的词嵌入和我们自己训练的嵌入。我们使用 LSTM 和一维卷积构建了文本分类器。我们还了解了如何使用最先进的语言建模架构生成文本。
在下一章中,我们将学习如何训练深度学习算法来生成时尚图像、新图像,并生成文本。
第三部分:理解深度学习中的现代架构
在本节中,你将熟悉深度学习中各种现代架构。
本节包含以下章节:
-
第六章,实现自编码器
-
第七章,使用生成对抗网络
-
第八章,使用现代网络架构进行迁移学习
-
第九章,深度强化学习
-
第十章,接下来做什么?
第七章:实现自编码器
本章讨论了半监督学习算法的概念,通过引入自编码器,然后进入受限玻尔兹曼机(RBMs)和深度信念网络(DBNs),以理解数据的概率分布。本章将概述这些算法如何应用于一些实际问题。还将提供在 PyTorch 中实现的编码示例。
自编码器是一种无监督学习技术。它可以接收无标签的数据集,并通过建模来重建原始输入,将问题建模为无监督学习,而不是监督学习。自编码器的目标是使输入与输出尽可能相似。
具体来说,本章将涵盖以下主题:
-
自编码器及其应用概述
-
瓶颈和损失函数
-
不同类型的自编码器
-
受限玻尔兹曼机
-
深度信念网络
自编码器的应用
自编码器属于表征学习,用于找到输入的压缩表示。它们由编码器和解码器组成。以下图示显示了自编码器的结构:
自编码器的应用示例包括以下几种:
-
数据去噪
-
数据可视化的降维
-
图像生成
-
插值文本
瓶颈和损失函数
自编码器对网络施加了一个瓶颈,强制使原始输入的知识表示被压缩。如果没有瓶颈的话,网络将简单地学会记忆输入值。因此,这意味着模型在未见数据上的泛化能力不会很好:
为了使模型能够检测到信号,我们需要它对输入具有敏感性,但不能简单地记住它们,而在未见数据上预测效果不佳。为了确定最优权衡,我们需要构建一个损失/成本函数:
有一些常用的自编码器架构,用于施加这两个约束条件,并确保在两者之间有最优的权衡。
编码示例 - 标准自编码器
在本例中,我们将展示如何在 PyTorch 中编译一个自编码器模型:
- 首先,导入相关的库:
import os
from torch import nn
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.datasets import MNIST
from torchvision.utils import save_image
- 现在,定义模型参数:
number_epochs = 10
batch_size = 128
learning_rate = 1e-4
- 然后,初始化一个函数来转换 MNIST 数据集中的图像:
transform_image = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
dataset = MNIST('./data', transform=transform_image)
data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
- 定义自编码器类,用于提供数据并初始化模型:
class autoencoder_model(nn.Module):
def __init__(self):
super(autoencoder_model, self).__init__()
self.encoder = nn.Sequential(
nn.Linear(28 * 28, 128),
nn.ReLU(True),
nn.Linear(128, 64),
nn.ReLU(True), nn.Linear(64, 12), nn.ReLU(True), nn.Linear(12, 3))
self.decoder = nn.Sequential(
nn.Linear(3, 12),
nn.ReLU(True),
nn.Linear(12, 64),
nn.ReLU(True),
nn.Linear(64, 128),
nn.ReLU(True), nn.Linear(128, 28 * 28), nn.Tanh())
def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return x
model = autoencoder_model()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(
model.parameters(), lr=learning_rate, weight_decay=1e-5)
- 定义一个函数,它将在每个 epoch 后从模型输出图像:
def to_image(x):
x = 0.5 * (x + 1)
x = x.clamp(0, 1)
x = x.view(x.size(0), 1, 28, 28)
return x
- 现在在每个 epoch 上运行模型并查看重建图像的结果:
for epoch in range(number_epochs):
for data in data_loader:
image, i = data
image = image.view(image.size(0), -1)
image = Variable(image)
# Forward pass
output = model(image)
loss = criterion(output, image)
# Backward pass
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('Epoch [{}/{}], Loss:{:.4f}'.format(epoch + 1, number_epochs, loss.data[0]))
if epoch % 10 == 0:
pic = to_image(output.cpu().data)
save_image(pic, './mlp_img/image_{}.png'.format(epoch))
torch.save(model.state_dict(), './sim_autoencoder.pth')
这将产生以下输出:
以下图片显示了每个 epoch 的自编码器输出:
随着经过的 epoch 越来越多,图像变得越来越清晰,因为模型继续学习。
卷积自编码器
自编码器可以使用卷积而不是全连接层。这可以通过使用 3D 向量而不是 1D 向量来实现。在图像的背景下,对图像进行下采样迫使自编码器学习其压缩版本。
编码示例 – 卷积自编码器
在这个例子中,我们将展示如何编译一个卷积自编码器:
- 与以前一样,您从 MNIST 数据集获取训练和测试数据集,并定义模型参数:
number_epochs = 10
batch_size = 128
learning_rate = 1e-4
transform_image = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
dataset = MNIST('./data', transform=transform_image)
data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
- 从这里开始,启动卷积自编码器模型:
class conv_autoencoder(nn.Module):
def __init__(self):
super(conv_autoencoder, self).__init__()
self.encoder = nn.Sequential(
nn.Conv2d(1, 16, 3, stride=3, padding=1),
nn.ReLU(True),
nn.MaxPool2d(2, stride=2),
nn.Conv2d(16, 8, 3, stride=2, padding=1),
nn.ReLU(True),
nn.MaxPool2d(2, stride=1)
)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(8, 16, 3, stride=2),
nn.ReLU(True),
nn.ConvTranspose2d(16, 8, 5, stride=3, padding=1),
nn.ReLU(True),
nn.ConvTranspose2d(8, 1, 2, stride=2, padding=1),
nn.Tanh()
)
def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return x
model = conv_autoencoder()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5)
- 最后,在每个 epoch 运行模型同时保存输出图像以供参考:
for epoch in range(number_epochs):
for data in data_loader:
img, i = data
img = Variable(img)
# Forward pass
output = model(img)
loss = criterion(output, img)
# Backward pass
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Print results
print('epoch [{}/{}], loss:{:.4f}'
.format(epoch+1, number_epochs, loss.data[0]))
if epoch % 10 == 0:
pic = to_image(output.cpu().data)
save_image(pic, './dc_img/image_{}.png'.format(epoch))
torch.save(model.state_dict(), './convolutional_autoencoder.pth')
我们可以在代码中提到的文件夹中,每个 epoch 后查看保存的图像。
去噪自编码器
去噪编码器故意向网络的输入添加噪声。这些自编码器实质上创建了数据的损坏副本。通过这样做,这有助于编码器学习输入数据中的潜在表示,使其更具普适性:
这个损坏的图像与其他标准自编码器一样被送入网络:
正如我们所见,原始输入中添加了噪声,编码器对输入进行编码并将其发送到解码器,解码器然后将嘈杂的输入解码为清理后的输出。因此,我们已经看过自编码器可以用于的各种应用。现在我们将看看一种特定类型的自编码器,即变分自编码器(VAE)。
变分自编码器
VAEs 与我们迄今考虑过的标准自编码器不同,因为它们以概率方式描述潜在空间中的观察结果,而不是确定性方式。每个潜在属性的概率分布被输出,而不是单个值。
标准自编码器在现实世界中的应用有些受限,因为它们只在您想要复制输入的数据时才真正有用。由于 VAEs 是生成模型,它们可以应用于您不希望输出与输入相同的数据的情况。
让我们在现实世界的背景下考虑这个问题。当在面部数据集上训练自编码器模型时,您希望它能学习潜在属性,比如一个人是否微笑,他们的肤色,是否戴眼镜等等:
正如在前面的图中所示,标准自编码器将这些潜在属性表示为离散值。
如果我们允许每个特征在可能值的范围内而不是单个值内,我们可以使用 VAEs 以概率术语描述属性:
前面的图示了我们如何将一个人是否微笑表示为离散值或概率分布。
每个潜在属性的分布是从图像中采样的,以生成用作解码器模型输入的向量:
如下图所示,输出两个向量:
其中一个描述平均值,另一个描述分布的方差。
训练 VAE
在训练期间,我们使用反向传播计算网络中每个参数与整体损失的关系。
标准自动编码器使用反向传播来在网络权重上重建损失值。由于 VAE 中的采样操作不可微,不能从重构误差中传播梯度。以下图表进一步解释了这一点:
为了克服这一限制,可以使用重参数化技巧。重参数化技巧从单位正态分布中采样ε,将其平移至潜在属性的均值𝜇,并按潜在属性的方差𝜎进行缩放:
这将采样过程从梯度流中移除,因为现在它位于网络之外。因此,采样过程不依赖于网络中的任何东西。现在我们可以优化分布的参数,同时保持从中随机采样的能力:
我们可以通过均值𝜇和协方差矩阵∑对其进行变换,因为每个属性的分布是高斯分布:
这里,ε ~ N(0,1)。
现在我们可以使用简单的反向传播来训练模型,并引入重参数化技巧:
如前面的图表所示,我们已经训练了自动编码器以平滑图像。
编码示例 - VAE
要在 PyTorch 中编写 VAE,我们可以像在之前的示例中那样加载库和数据集。从这里,我们可以定义 VAE 类:
class VariationalAutoEncoder(nn.Module):
def __init__(self):
super(VariationalAutoEncoder, self).__init__()
self.fc1 = nn.Linear(784, 400)
self.fc21 = nn.Linear(400, 20)
self.fc22 = nn.Linear(400, 20)
self.fc3 = nn.Linear(20, 400)
self.fc4 = nn.Linear(400, 784)
def encode_function(self, x):
h1 = F.relu(self.fc1(x))
return self.fc21(h1), self.fc22(h1)
def reparametrize(self, mu, logvar):
std = logvar.mul(0.5).exp_()
if torch.cuda.is_available():
eps = torch.cuda.FloatTensor(std.size()).normal_()
else:
eps = torch.FloatTensor(std.size()).normal_()
eps = Variable(eps)
return eps.mul(std).add_(mu)
def decode_function(self, z):
h3 = F.relu(self.fc3(z))
return F.sigmoid(self.fc4(h3))
def forward(self, x):
mu, logvar = self.encode_function(x)
z = self.reparametrize(mu, logvar)
return self.decode_function(z), mu, logvar
然后,我们使用 KL 散度来定义损失函数,并初始化模型:
def loss_function(reconstruction_x, x, mu, latent_log_variance):
"""
reconstruction_x: generating images
x: original images
mu: latent mean
"""
BCE = reconstruction_function(reconstruction_x, x)
# KL loss = 0.5 * sum(1 + log(sigma²) - mu² - sigma²)
KLD_aspect = mu.pow(2).add_(latent_log_variance.exp()).mul_(-1).add_(1).add_(logvar)
KLD = torch.sum(KLD_aspect).mul_(-0.5)
# KL divergence
return BCE + KLD
optimizer = optim.Adam(model.parameters(), lr=1e-4)
从这里,我们可以运行模型的每个时期并保存输出:
for epoch in range(number_epochs):
model.train()
train_loss = 0
for batch_idx, data in enumerate(data_loader):
img, _ = data
img = img.view(img.size(0), -1)
img = Variable(img)
if torch.cuda.is_available():
img = img.cuda()
optimizer.zero_grad()
recon_batch, mu, logvar = model(img)
loss = loss_function(recon_batch, img, mu, logvar)
loss.backward()
train_loss += loss.data[0]
optimizer.step()
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch,
batch_idx * len(img),
len(data_loader.dataset), 100\. * batch_idx / len(data_loader),
loss.data[0] / len(img)))
print('Epoch: {} Average loss: {:.4f}'.format(epoch, train_loss / len(data_loader.dataset)))
if epoch % 10 == 0:
save = to_image(recon_batch.cpu().data)
save_image(save, './vae_img/image_{}.png'.format(epoch))
torch.save(model.state_dict(), './vae.pth')
现在我们已经看过各种自动编码器及其如何编译它们,让我们学习如何在推荐系统中实现它们。
受限玻尔兹曼机
RBM是一种广泛用于协同过滤、特征提取、主题建模和降维等任务的算法。它们可以无监督地学习数据集中的模式。
例如,如果你观看电影并说出你是否喜欢它,我们可以使用一个RBM来帮助我们确定你做出这个决定的原因。
RBM 的目标是最小化能量,由以下公式定义,其依赖于可见/输入状态、隐藏状态、权重和偏置的配置:
RBM 是 DBN 的基本构建块的两层网络。RBM 的第一层是神经元的可见/输入层,第二层是隐藏层的神经元:
RBM 将输入从可见层翻译成一组数字。通过几次前向和后向传递,该数字然后被翻译回重构输入。在 RBM 中的限制是同一层中的节点不连接。
从训练数据集中的每个节点的低级特征被馈送到可见层的每个节点。在图像分类的情况下,每个节点将为图像中每个像素接收一个像素值:
通过网络跟踪一个像素,输入x被隐藏层的权重乘以,然后加上偏置。然后,这被输入到激活函数中,产生输出,这实质上是通过它传递的信号强度,给定输入x,如下图所示:
在隐藏层的每个节点,来自每个像素值的x被单独的权重乘以。然后将这些乘积求和,并添加偏置。然后将其输出通过激活函数,产生该单个节点的输出:
在每个时刻,RBM 处于某种状态,这指的是可见v和隐藏h层中神经元的值。这种状态的概率可以由以下联合分布函数给出:
这里,Z 是分区函数,是对所有可能的可见和隐藏向量对的求和。
训练 RBM
在训练期间,RBM 执行两个主要步骤:
- 吉布斯采样:训练过程的第一步使用吉布斯采样,它重复以下过程k次:
-
给定输入向量的隐藏向量的概率;预测隐藏值。
-
给定隐藏向量的输入向量的概率;预测输入值。从这里,我们获得另一个输入向量,该向量是从原始输入值重新创建的。
- 对比散度:RBM 通过对比散度调整它们的权重。在此过程中,可见节点的权重是随机生成的,并用于生成隐藏节点。然后,隐藏节点再使用相同的权重重构可见节点。用于重构可见节点的权重在整个过程中是相同的。但是生成的节点不同,因为它们之间没有连接。
一旦 RBM 训练完成,它基本上能够表达两件事情:
-
输入数据特征之间的相互关系
-
在识别模式时哪些特征最重要
理论示例 - RBM 推荐系统
在电影的背景下,我们可以使用 RBM 揭示一组代表它们类型的潜在因素,从而确定一个人喜欢哪种电影类型。例如,如果我们要求某人告诉我们他们看过哪些电影以及是否喜欢,我们可以将它们表示为二进制输入(1 或 0)到 RBM 中。对于那些他们没看过或没告诉我们的电影,我们需要分配一个值为-1,这样网络在训练时可以识别并忽略它们的关联权重。
让我们考虑一个示例,用户喜欢老妈妈,我来了,宿醉和伴娘,不喜欢尖叫或心理,还没有看过霍比特人。根据这些输入,RBM 可能识别出三个隐藏因子:喜剧、恐怖和奇幻,这些因子对应于电影的类型:
对于每个隐藏神经元,RBM 分配了给定输入神经元的隐藏神经元的概率。神经元的最终二进制值是通过从伯努利分布中抽样得到的。
在上面的例子中,代表喜剧类型的唯一隐藏神经元变得活跃。因此,给定输入到 RBM 的电影评分,它预测用户最喜欢喜剧电影。
对于已训练的 RBM 来说,要预测用户尚未看过的电影,基于他们的喜好,RBM 使用可见神经元给定隐藏神经元的概率。它从伯努利分布中进行抽样,以确定哪个可见神经元可以变为活跃状态。
编码示例 - RBM 推荐系统
继续在电影的背景下,我们将展示如何使用 PyTorch 库构建一个 RBM 推荐系统的示例。该示例的目标是训练一个模型来确定用户是否会喜欢一部电影。
在这个示例中,我们使用了 MovieLens 数据集(grouplens.org/datasets/movielens/
),包含 100 万条评分,这个数据集由明尼苏达大学的 GroupLens 研究组创建:
- 首先,下载数据集。可以通过终端命令完成如下操作:
wget -O moviedataset.zip http://files.grouplens.org/datasets/movielens/ml-1m.zip
unzip -o moviedataset.zip -d ./data
unzip -o moviedataset.zip -d ./data
- 现在导入我们将要使用的库:
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.optim as optim
import torch.utils.data
from torch.autograd import Variable
- 然后导入数据:
movies = pd.read_csv('ml-1m/movies.dat', sep = '::', header = None, engine = 'python', encoding = 'latin-1')
users = pd.read_csv('ml-1m/users.dat', sep = '::', header = None, engine = 'python', encoding = 'latin-1')
ratings = pd.read_csv('ml-1m/ratings.dat', sep = '::', header = None, engine = 'python', encoding = 'latin-1')
以下截图展示了我们数据集的结构:
- 准备测试和训练数据集:
training_dataset = pd.read_csv('ml-100k/u1.base', delimiter = '\t')
training_dataset = np.array(training_set, dtype = 'int')
test_dataset = pd.read_csv('ml-100k/u1.test', delimiter = '\t')
test_dataset = np.array(test_dataset, dtype = 'int')
- 现在我们需要准备一个包含用户评分的矩阵。该矩阵将以用户为行,电影为列。零用于表示用户未对特定电影评分的情况。我们定义
no_users
和no_movies
变量,然后考虑训练和测试数据集中的最大值如下:
no_users = int(max(max(training_dataset[:,0]), max(test_dataset[:,0])))
no_movies = int(max(max(training_dataset[:,1]), max(test_dataset[:,1])))
- 现在我们定义一个名为
convert_dataset
的函数,将数据集转换为矩阵。它通过创建一个循环来运行数据集,并获取特定用户评分的所有电影及该用户的评分。因为用户没有评级过的电影有许多,所以我们首先创建一个全零矩阵:
def convert_dataset(data):
converted_data = []
for id_users in range(1, no_users + 1):
id_movies = data[:,1][data[:,0] == id_users]
id_ratings = data[:,2][data[:,0] == id_users]
movie_ratings = np.zeros(no_movies)
ratings[id_movies - 1] = id_ratings
converted_data.append(list(movie_ratings))
return converted_data
training_dataset = convert_dataset(training_dataset)
test_dataset = convert_dataset(test_dataset)
- 现在我们使用
FloatTensor
实用程序将数据转换为 Torch 张量。这将把数据集转换为 PyTorch 数组:
training_dataset = torch.FloatTensor(training_dataset)
test_dataset = torch.FloatTensor(test_dataset)
- 在这个例子中,我们想要进行二元分类,即用户是否喜欢这部电影。因此,我们将评分转换为零和一。但是首先,我们将现有的零替换为 -1,以表示用户从未评级过的电影:
training_dataset[training_dataset == 0] = -1
training_dataset[training_dataset == 1] = 0
training_dataset[training_dataset == 2] = 0
training_dataset[training_dataset >= 3] = 1
test_dataset[test_dataset == 0] = -1
test_dataset[test_dataset == 1] = 0
test_dataset[test_dataset == 2] = 0
test_dataset[test_dataset >= 3] = 1
- 现在,我们需要创建一个类来定义 RBM 的架构。该类通过使用随机正态分布初始化权重和偏置。还定义了两种类型的偏置,其中
a
是给定可见节点时隐藏节点的概率,b
是给定隐藏节点时可见节点的概率。该类创建了一个sample_hidden_nodes
函数,它以x
作为参数并表示可见神经元。从这里,我们计算给定v
的h
的概率,其中h
和v
分别表示隐藏和可见节点。这代表了 S 型激活函数。它计算为权重向量和x
的乘积加上偏置a
。由于我们考虑的是二元分类模型,我们返回隐藏神经元的伯努利样本。从这里,我们创建一个sample_visible_function
函数,它将对可见节点进行采样。最后,我们创建训练函数。它接受包含电影评分的输入向量、k 次采样后获得的可见节点、概率向量以及 k 次采样后的隐藏节点的概率:
class RBM():
def __init__(self, num_visible_nodes, num_hidden_nodes):
self.W = torch.randn(num_hidden_nodes, num_visible_nodes)
self.a = torch.randn(1, num_hidden_nodes)
self.b = torch.randn(1, num_visible_nodes)
def sample_hidden_nodes(self, x):
wx = torch.mm(x, self.W.t())
activation = wx + self.a.expand_as(wx)
p_h_given_v = torch.sigmoid(activation)
return p_h_given_v, torch.bernoulli(p_h_given_v)
def sample_visible_nodes(self, y):
wy = torch.mm(y, self.W)
activation = wy + self.b.expand_as(wy)
p_v_given_h = torch.sigmoid(activation)
return p_v_given_h, torch.bernoulli(p_v_given_h)
def train(self, v0, vk, ph0, phk):
self.W += torch.mm(v0.t(), ph0) - torch.mm(vk.t(), phk)
self.b += torch.sum((v0 - vk), 0)
self.a += torch.sum((ph0 - phk), 0)
- 现在我们定义我们的模型参数:
num_visible_nodes = len(training_dataset[0])
num_hidden_nodes = 200
batch_size = 100
rbm = RBM(num_visible_nodes, num_hidden_nodes)
- 从这里,我们可以为每个 epoch 训练模型:
nb_epoch = 10
for epoch in range(1, nb_epoch + 1):
train_loss = 0
s = 0.
for id_user in range(0, nb_users - batch_size, batch_size):
vk = training_dataset[id_user:id_user+batch_size]
v0 = training_dataset[id_user:id_user+batch_size]
ph0,_ = rbm.sample_hidden_nodes(v0)
for k in range(10):
_,hk = rbm.sample_hidden_nodes(vk)
_,vk = rbm.sample_visible_nodes(hk)
vk[v0<0] = v0[v0<0]
phk,_ = rbm.sample_hidden_nodes(vk)
rbm.train(v0, vk, ph0, phk)
train_loss += torch.mean(torch.abs(v0[v0>=0] - vk[v0>=0]))
s += 1.
print('epoch: '+str(epoch)+' loss: '+str(train_loss/s))
我们可以在训练过程中绘制跨 epoch 的错误:
这可以帮助我们确定应该运行多少个 epoch 进行训练。显示在六个 epoch 后,改进的性能率下降,因此我们应该考虑在这个阶段停止训练。
我们已经看到了在 RBM 中实现推荐系统的编码示例,现在让我们简要地浏览一下 DBN 架构。
DBN 架构
DBN 是一个多层信念网络,每一层都是一个叠加的 RBM。除了 DBN 的第一层和最后一层之外,每一层既作为其前面节点的隐藏层,又作为其后节点的输入层:
DBN 中的两个层通过权重矩阵连接。DBN 的顶部两层是无向的,它们之间形成对称连接,形成联想存储器。较低的两层直接连接到上面的层。方向感将联想存储器转换为观察变量:
DBN 的两个最显著特性如下:
-
DBN 通过高效的逐层过程学习自顶向下的生成权重。这些权重决定了一个层中的变量如何依赖于上面的层。
-
训练完成后,可以通过单个自下而上的传递推断每层隐藏变量的值。传递从底层的可见数据向量开始,并使用其生成权重相反方向。
联合配置网络的概率在可见层和隐藏层之间的联合配置网络的能量依赖于所有其他联合配置网络的能量:
一旦 RBMs 堆栈完成了 DBN 的预训练阶段,就可以使用前向网络进行微调阶段,从而创建分类器或在无监督学习场景中简单地帮助聚类无标签数据。
微调
微调的目标是找到层间权重的最优值。它微调原始特征,以获得更精确的类边界。为了帮助模型将模式和特征关联到数据集,使用了一个小的标记数据集。
微调可以作为随机的自下而上传递应用,然后用于调整自上而下的权重。一旦达到顶层,递归被应用于顶层。为了进一步微调,我们可以进行随机的自上而下传递,并调整自下而上的权重。
总结
在本章中,我们解释了自编码器及其不同的变体。在整个章节中,我们提供了一些编码示例,展示它们如何应用于 MNIST 数据集。后来我们介绍了受限玻尔兹曼机,并解释了如何将其开发成深度玻尔兹曼机,同时提供了额外的示例。
在下一章中,我们将介绍生成对抗网络,并展示它们如何用于生成图像和文本。
进一步阅读
进一步的信息请参考以下内容:
-
变分自编码器教程:
arxiv.org/abs/1606.05908
-
CS598LAZ – 变分自编码器:
slazebni.cs.illinois.edu/spring17/lec12_vae.pdf
-
自编码变分贝叶斯:
arxiv.org/abs/1312.6114
-
深度信念网快速学习算法:
www.cs.toronto.edu/~fritz/absps/ncfast.pdf
-
训练受限玻尔兹曼机:简介:
www.sciencedirect.com/science/article/abs/pii/S0031320313002495
-
深度玻尔兹曼机:
proceedings.mlr.press/v5/salakhutdinov09a/salakhutdinov09a.pdf
-
训练受限玻尔兹曼机实用指南:
www.cs.toronto.edu/~hinton/absps/guideTR.pdf
-
深度信念网络:
link.springer.com/chapter/10.1007/978-3-319-06938-8_8
-
实战神经网络:
www.amazon.co.uk/Hands-Neural-Networks-neural-network-ebook/dp/B07SKDSGB6/
第八章:使用生成对抗网络进行工作
我们在前几章中看到的所有示例都集中在解决分类或回归等问题上。对于理解深度学习如何发展以解决无监督学习问题,本章非常有趣且重要。
在本章中,我们将训练网络学习如何创建以下内容:
-
基于内容和特定艺术风格的图像,通常称为风格转移
-
使用特定类型的生成对抗网络(GAN)生成新人脸。
这些技术构成了深度学习领域正在进行的大部分先进研究的基础。深入研究每个子领域的具体细节,如 GAN 和语言建模,超出了本书的范围,它们值得单独的一本书来探讨。我们将学习它们的一般工作原理以及在 PyTorch 中构建它们的过程。
本章将涵盖以下主题:
-
神经风格转移
-
介绍生成对抗网络
-
DCGANs
神经风格转移
我们人类以不同精度和复杂度生成艺术作品。尽管创建艺术的过程可能非常复杂,但可以看作是两个最重要因素的结合,即要画什么和如何画。要画什么受到我们周围所见的启发,而如何画也会受到我们周围某些事物的影响。从艺术家的角度来看,这可能是一种过于简化的看法,但对于理解如何使用深度学习算法创建艺术作品非常有用。
我们将训练一个深度学习算法,从一幅图像中提取内容,然后根据特定的艺术风格进行绘制。如果你是艺术家或者从事创意行业,你可以直接利用最近几年来进行的令人惊叹的研究来改进这一过程,并在你所工作的领域内创造出有趣的东西。即使你不是,它仍然可以向你介绍生成模型的领域,其中网络生成新的内容。
让我们从高层次理解神经风格转移的工作,并深入探讨相关细节,以及构建它所需的 PyTorch 代码。风格转移算法提供了一个内容图像(C)和一个风格图像(S)——算法必须生成一个新图像(O),其中包含来自内容图像的内容和来自风格图像的风格。这种神经风格转移的过程是由 Leon Gates 等人在 2015 年的论文《艺术风格的神经算法》中介绍的(arxiv.org/pdf/1508.06576.pdf
)。以下是我们将使用的内容图像(C):
以下是风格图像(S):
前述图片来源:来自葛饰北斋的《神奈川冲浪里》(commons.wikimedia.org/wiki/File:The_Great_Wave_off_Kanagawa.jpg
)
这是我们将得到的结果图片:
当你理解卷积神经网络(CNNs)的工作原理时,风格转移背后的思想变得清晰。 当 CNNs 被用于目标识别时,训练的早期层学习非常通用的信息,如线条,曲线和形状。 CNN 的最后几层捕捉图像的更高级概念,如眼睛,建筑物和树木。 因此,类似图像的最后几层的值往往更接近。 我们采用相同的概念并应用于内容损失。 内容图像和生成图像的最后一层应该类似,并且我们使用均方误差(MSE)来计算相似性。 我们使用优化算法降低损失值。
在 CNN 中,通过称为 Gram 矩阵的技术通常捕获图像的风格。 Gram 矩阵计算跨多个层捕获的特征图之间的相关性。 Gram 矩阵提供了计算风格的一种方法。类似风格的图像具有 Gram 矩阵的类似值。风格损失还使用风格图像的 Gram 矩阵与生成图像之间的均方误差(MSE)来计算。
我们将使用预训练的 VGG19 模型,该模型提供在 TorchVision 模型中。 训练样式转移模型所需的步骤与任何其他深度学习模型相似,唯一不同的是计算损失比分类或回归模型更复杂。 神经风格算法的训练可以分解为以下步骤:
-
加载数据。
-
创建一个 VGG19 模型。
-
定义内容损失。
-
定义风格损失。
-
从 VGG 模型中提取跨层的损失。
-
创建优化器。
-
训练 - 生成类似于内容图像的图像和类似于样式图像的样式。
加载数据
加载数据类似于我们在第三章“深入神经网络”的解决图像分类问题中看到的。 我们将使用预训练的 VGG 模型,因此必须使用与预训练模型相同的值对图像进行归一化处理。
以下代码显示了如何实现此目标。 代码大部分是不言自明的,因为我们在前几章中已经详细讨论过它:
image_size = 512
is_cuda = torch.cuda.is_available()
preprocessing = transforms.Compose([transforms.Resize(image_size),
transforms.ToTensor(),
transforms.Lambda(lambda x: x[torch.LongTensor([2,1,0])]),
transforms.Normalize(mean=[0.40760392, 0.45795686, 0.48501961],
std=[1,1,1]),
transforms.Lambda(lambda x: x.mul_(255)),
])
processing = transforms.Compose([transforms.Lambda(lambda x: x.mul_(1./255)),
transforms.Normalize(mean=[-0.40760392, -0.45795686, -0.48501961],
std=[1,1,1]),
transforms.Lambda(lambda x: x[torch.LongTensor([2,1,0])]),
])
postprocess = transforms.Compose([transforms.ToPILImage()])
def postprocess_b(tensor):
t = processing(tensor)
t[t>1] = 1
t[t<0] = 0
img = postprocess(t)
return img
在此代码中,我们定义了三个功能:preprocess
执行所有必需的预处理,并使用与训练 VGG 模型时相同的标准化值。模型的输出需要被归一化回其原始值;processing
函数执行所需的处理。生成的模型可能超出接受值的范围,postprocess_b
函数将所有大于一的值限制为一,并将小于零的值限制为零。
现在我们定义 loader
函数,它加载图像,应用 preprocessing
转换,并将其转换为变量:
def loader(image_name):
image = Image.open(image_name)
image = Variable(preprocessing(image))
# fake batch dimension required to fit network's input dimensions
image = image.unsqueeze(0)
return image
以下函数加载样式和内容图像:
style_image = loader("Images/style_image.jpg")
content_image = loader("Images/content_image.jpg")
我们可以使用噪声(随机数)创建图像,也可以使用相同的内容图像。在这种情况下,我们将使用内容图像。以下代码创建内容图像:
output_image = Variable(content_image.data.clone(),requires_grad=True)
我们将使用优化器来调整 output_image
变量的值,以使图像更接近内容图像和样式图像。出于这个原因,我们要求 PyTorch 通过提及 requires_grad=True
来保持梯度。
创建 VGG 模型
我们将从 torchvisions.models
中加载预训练模型。我们将仅使用此模型来提取特征,并且 PyTorch 的 VGG 模型是这样定义的:所有卷积块位于特征模块中,全连接或线性层位于分类器模块中。由于我们不会训练 VGG 模型中的任何权重或参数,因此我们还将冻结该模型,如下面的代码所示:
vgg = vgg19(pretrained=True).features
for param in vgg.parameters():
param.requires_grad = False
在此代码中,我们创建了一个 VGG 模型,仅使用其卷积块,并冻结了模型的所有参数,因为我们将仅用它来提取特征。
内容损失
内容损失是输入图像和输出图像之间的距离。其目的是保持图像的原始内容。它是在通过网络传递两个图像并提取特定层的输出后计算的 MSE。我们通过使用 register_forward_hook
功能从 VGG 中提取中间层的输出来实现,传入内容图像和要优化的图像。
我们根据这些层的输出计算得到的 MSE,如下面的代码所述:
target_layer = dummy_fn(content_img)
noise_layer = dummy_fn(noise_img)
criterion = nn.MSELoss()
content_loss = criterion(target_layer,noise_layer)
我们将在接下来的部分为此代码实现 dummy_fn
。现在我们知道的是,dummy_fn
函数通过传递图像返回特定层的输出。我们通过将内容图像和噪声图像传递给 MSE 损失函数来传递生成的输出。
样式损失
样式损失是跨多个层计算的。样式损失是每个特征图生成的 Gram 矩阵的 MSE。Gram 矩阵表示其特征的相关值。让我们通过以下图表和代码实现来理解 Gram 矩阵的工作原理。
以下表格显示了维度为 [2, 3, 3, 3] 的特征映射的输出,具有列属性 Batch_size,Channels 和 Values:
要计算 Gram 矩阵,我们展平每个通道的所有值,然后通过与其转置相乘来找到其相关性,如下表所示:
我们做的所有工作就是将所有值按照每个通道展平为单个向量或张量。以下代码实现了这一点:
class GramMatrix(nn.Module):
def forward(self,input):
b,c,h,w = input.size()
features = input.view(b,c,h*w)
gram_matrix = torch.bmm(features,features.transpose(1,2))
gram_matrix.div_(h*w)
return gram_matrix
我们将 GramMatrix
函数实现为另一个 PyTorch 模块,具有 forward
函数,以便像 PyTorch 层一样使用它。在这一行中,我们从输入图像中提取不同的维度:
b,c,h,w = input.size()
这里,b
表示批次,c
表示过滤器或通道,h
表示高度,w
表示宽度。在下一步中,我们将使用以下代码保持批次和通道维度不变,并在高度和宽度维度上展平所有值,如前面的图示所示:
features = input.view(b,c,h*w)
通过将其转置向量与其展平值相乘来计算 Gram 矩阵。我们可以使用 PyTorch 提供的批次矩阵乘法函数 torch.bmm()
来实现,如下代码所示:
gram_matrix = torch.bmm(features,features.transpose(1,2))
我们完成了通过将其除以元素数量来规范 Gram 矩阵值的工作。这可以防止某个具有大量值的特征映射主导得分。一旦计算了 GramMatrix
,就可以简单地计算风格损失,这在以下代码中实现:
class StyleLoss(nn.Module):
def forward(self,inputs,targets):
out = nn.MSELoss()(GramMatrix()(inputs),targets)
return (out)
StyleLoss
类被实现为另一个 PyTorch 层。它计算输入 GramMatrix
值与风格图像 GramMatrix
值之间的均方误差(MSE)。
提取损失
就像我们使用 register_forward_hook()
函数提取卷积层的激活一样,我们可以提取不同卷积层的损失,以计算风格损失和内容损失。在这种情况下的一个区别是,我们需要提取多个层的输出而不是一个层的输出。以下类集成了所需的更改:
class LayerActivations():
features=[]
def __init__(self,model,layer_numbers):
self.hooks = []
for layer_num in layer_numbers:
self.hooks.append(model[layer_numbers].register_forward_hook(self.hook_fn))
def hook_fn(self,module,input,output):
self.features.append(output)
def remove(self):
for hook in self.hooks:
hook.remove()
__init__
方法接受我们需要调用 register_forward_hook
方法的模型和需要提取输出的层编号。__init__
方法中的 for
循环遍历层编号并注册所需的前向钩子,用于提取输出。
传递给 register_forward_hook
方法的 hook_fn
函数在注册 hook_fn
函数的层之后由 PyTorch 调用。在函数内部,我们捕获输出并将其存储在特征数组中。
当我们不想捕获输出时,需要调用 remove 函数一次。忘记调用 remove 方法可能会导致内存不足异常,因为所有输出都会累积。
让我们写另一个实用函数,可以提取用于样式和内容图像的输出。以下函数执行相同操作:
def extract_layers(layers,image,model=None):
la = LayerActivations(model,layers)
la.features = []
out = model(image)
la.remove()
return la.features
在 extract_layers
函数内部,我们通过向模型和层编号传递来创建 LayerActivations
类的对象。特征列表可能包含来自先前运行的输出,因此我们将其重新初始化为空列表。然后我们通过模型传递图像,并且我们不会使用输出。我们更关心的是特征数组中生成的输出。我们调用 remove 方法来从模型中删除所有已注册的钩子,并返回特征。以下代码展示了我们提取样式和内容图像所需目标的方法:
content_targets = extract_layers(content_layers,content_img,model=vgg)
style_targets = extract_layers(style_layers,style_img,model=vgg)
一旦我们提取了目标,我们需要将输出从创建它们的图中分离出来。请记住,所有这些输出都是 PyTorch 变量,它们保留了它们创建方式的信息。但是,对于我们的情况,我们只关注输出值,而不是图形,因为我们不会更新样式图像或内容图像。以下代码展示了这一技术:
content_targets = [t.detach() for t in content_targets]
style_targets = [GramMatrix()(t).detach() for t in style_targets]
一旦我们分离了,让我们把所有的目标添加到一个列表中。以下代码展示了这一技术:
targets = style_targets + content_targets
在计算样式损失和内容损失时,我们传递了称为内容层和样式层的两个列表。不同的层选择将影响生成图像的质量。让我们选择与论文作者提到的相同层。以下代码显示了我们在这里使用的层的选择:
style_layers = [1,6,11,20,25]
content_layers = [21]
loss_layers = style_layers + content_layers
优化器期望最小化一个单一的标量数量。为了获得单一标量值,我们将所有到达不同层的损失相加起来。习惯上,对这些损失进行加权和是常见做法,而我们选择与 GitHub 仓库中论文实现中使用的相同权重。我们的实现是作者实现的一个稍微修改的版本。以下代码描述了使用的权重,这些权重是通过所选层中的过滤器数量计算得出的:
style_weights = [1e3/n**2 for n in [64,128,256,512,512]]
content_weights = [1e0]
weights = style_weights + content_weights
要进行可视化,我们可以打印 VGG 层。花一分钟观察我们选择了哪些层,并尝试不同的层组合。我们将使用以下代码来打印 VGG 层:
print(vgg)
这导致以下输出:
#Results
Sequential(
(0): Conv2d (3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace)
(2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace)
(4): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
(5): Conv2d (64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace)
(7): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace)
(9): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
(10): Conv2d (128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace)
(12): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace)
(14): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace)
(16): Conv2d (256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(17): ReLU(inplace)
(18): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
(19): Conv2d (256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace)
(21): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace)
(23): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(24): ReLU(inplace)
(25): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(26): ReLU(inplace)
(27): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
(28): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace)
(30): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(31): ReLU(inplace)
(32): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(33): ReLU(inplace)
(34): Conv2d (512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(35): ReLU(inplace)
(36): MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1))
)
我们必须定义损失函数和优化器来生成艺术图像。我们将在以下部分中初始化它们两个。
为每个层创建损失函数
我们已经将损失函数定义为 PyTorch 层。因此,让我们为不同的样式损失和内容损失创建损失层。以下代码定义了这个函数:
loss_fns = [StyleLoss()] * len(style_layers) + [nn.MSELoss()] * len(content_layers)
loss_fns
函数是一个列表,包含一堆基于创建的数组长度的样式损失对象和内容损失对象。
创建优化器
通常,我们传递网络的参数,如 VGG 的参数进行训练。但在本例中,我们将 VGG 模型用作特征提取器,因此不能传递 VGG 的参数。在这里,我们只提供将优化以使图像具有所需内容和风格的opt_img
变量的参数。以下代码创建优化器以优化其值:
optimizer = optim.LBFGS([output_image]);
现在我们已经准备好所有训练组件。
训练模型
与我们到目前为止训练过的其他模型相比,训练方法有所不同。在这里,我们需要在多个层级计算损失,并且每次调用优化器时,都会改变输入图像,使其内容和风格接近目标的内容和风格。让我们看一下用于训练的代码,然后我们将逐步介绍训练的重要步骤:
maximum_iterations = 500
show_iteration-1 = 50
n_iter=[0]
optimizer = optim.LBFGS([output_image]);
n_iteration=[0]
while n_iteration[0] <= maximum_iterations:
def closure():
optimizer.zero_grad()
out = extract_layers(loss_layers,output_image,model=vgg)
layer_losses = [weights[a] * loss_fnsa for a,A in enumerate(out)]
loss = sum(layer_losses)
loss.backward()
n_iteration[0]+=1
if n_iteration[0]%show_iteration == (show_iteration-1):
print('Iteration: %d, loss: %f'%(n_iteration[0]+1, loss.data[0]))
return loss
optimizer.step(closure)
我们正在运行 500 次迭代的训练循环。对于每次迭代,我们使用我们的extract_layers
函数计算来自 VGG 模型不同层的输出。在这种情况下,唯一改变的是output_image
的值,它将包含我们的样式图像。一旦计算出输出,我们通过迭代输出并将它们传递给相应的损失函数,同时传递它们的相应目标来计算损失。我们总结所有的损失并调用反向传播函数。在闭包函数的末尾,返回损失。对于max_iterations
,同时调用闭包方法和optimizer.step
方法。如果您在 GPU 上运行,可能需要几分钟才能运行;如果您在 CPU 上运行,请尝试减小图像的大小以加快运行速度。
在运行了 500 个 epochs 之后,在我的设备上生成的图像如下所示。尝试不同的内容和风格的组合来生成有趣的图像:
在接下来的部分中,让我们使用深度卷积生成对抗网络(DCGANs)生成人脸。
引入 GANs
GANs 是由 Ian Goodfellow 于 2014 年引入,并变得非常流行。最近 GAN 研究取得了许多重要进展,以下时间轴显示了 GAN 研究中一些最显著的进展和关键发展:
在本章中,我们将专注于 DCGAN 的 PyTorch 实现。然而,有一个非常有用的 GitHub 仓库提供了一堆 PyTorch 实现示例,包括时间轴上显示的 GAN 以及其他模型。可以通过以下链接访问:github.com/eriklindernoren/PyTorch-GAN
。
GAN 通过训练两个深度神经网络——生成器和鉴别器来解决无监督学习问题,它们相互竞争。在训练过程中,两者最终都变得更擅长执行它们所执行的任务。
GAN 可以用一个造假者(生成器)和警察(鉴别器)的案例直观理解。最初,造假者向警察展示假钱。警察识别出它是假的,并解释给造假者为什么是假的。造假者根据收到的反馈制造新的假钱。警察发现它是假的,并告诉造假者为什么是假的。重复进行大量次数,直到造假者能够制造出警察无法识别的假钱。在 GAN 场景中,我们最终得到一个生成器,生成的假图像非常类似于真实图像,而分类器变得擅长识别真伪。
GAN 是伪造者网络和专家网络的结合体,每个网络都经过训练以击败另一个。生成器网络以随机向量作为输入,并生成合成图像。鉴别器网络接收输入图像,并预测图像是真实的还是伪造的。我们向鉴别器网络传递真实图像或伪造图像。
生成器网络被训练生成图像,并欺骗鉴别器网络认为它们是真实的。鉴别器网络也在不断改进,以免受骗,因为我们在训练时传递反馈。
以下图表描述了 GAN 模型的架构:
虽然 GAN 的理念在理论上听起来很简单,但训练一个真正有效的 GAN 模型非常困难,因为需要并行训练两个深度神经网络。
DCGAN 是早期展示如何构建一个可以自我学习并生成有意义图像的 GAN 模型之一。您可以在这里了解更多信息:arxiv.org/pdf/1511.06434.pdf
。我们将逐步讲解这种架构的每个组成部分以及背后的一些推理,以及如何在 PyTorch 中实现它。
DCGAN
在本节中,我们将根据我在前面信息框中提到的 DCGAN 论文实现 GAN 架构的不同部分。训练 DCGAN 的一些重要部分包括以下内容:
-
生成器网络,将固定维度的潜在向量(数字列表)映射到某些形状的图像。在我们的实现中,形状是(3, 64, 64)。
-
鉴别器网络,以生成器生成的图像或来自实际数据集的图像作为输入,并映射到评估输入图像是否真实或伪造的分数。
-
定义生成器和鉴别器的损失函数。
-
定义一个优化器。
让我们详细探讨每个部分。这一实现提供了更详细的解释,说明了在 PyTorch GitHub 存储库中提供的代码:github.com/pytorch/examples/tree/master/dcgan.
定义生成器网络
生成器网络将固定维度的随机向量作为输入,并对其应用一组转置卷积、批量归一化和 ReLU 激活函数,生成所需尺寸的图像。在深入研究生成器实现之前,让我们先来定义转置卷积和批量归一化。
转置卷积
转置卷积也称为分数步幅卷积。它们的工作方式与卷积相反。直观地说,它们试图计算如何将输入向量映射到更高的维度。
让我们看看下面的图表以更好地理解它:
此图表被引用在 Theano 文档中(另一个流行的深度学习框架—deeplearning.net/software/theano/tutorial/conv_arithmetic.html
)。如果你想更深入地了解步幅卷积的工作原理,我强烈推荐你阅读这篇文章。对我们而言重要的是,它有助于将向量转换为所需维度的张量,并且我们可以通过反向传播训练核的值。
批量归一化
我们已经多次观察到,所有传递给机器学习或深度学习算法的特征都经过了归一化处理;即,通过从数据中减去均值来将特征的值居中到零,并通过将数据除以其标准差来给数据一个单位标准差。通常我们会使用 PyTorch 的torchvision.Normalize
方法来实现这一点。以下代码展示了一个例子:
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
在我们所见的所有示例中,数据在进入神经网络之前都进行了归一化;不能保证中间层得到归一化的输入。下图展示了神经网络中间层未获得归一化数据的情况:
批量归一化充当一个中间函数或层,当训练过程中的均值和方差随时间变化时,它会归一化中间数据。批量归一化是由 Ioffe 和 Szegedy 在 2015 年提出的(arxiv.org/abs/1502.03167
)。批量归一化在训练和验证或测试期间表现不同。训练期间,会计算批次数据的均值和方差。验证和测试期间则使用全局值。我们只需理解它归一化了中间数据。使用批量归一化的一些关键优势包括以下几点:
-
改善了网络中的梯度流,从而帮助我们构建更深的网络
-
允许更高的学习率
-
减少了初始化的强依赖
-
作为正则化的一种形式,并减少了对丢弃的依赖
大多数现代架构,如 ResNet 和 Inception,在它们的架构中广泛使用批标准化。我们将在下一章节深入探讨这些架构。批标准化层是在卷积层或线性/全连接层之后引入的,如下图所示:
到目前为止,我们对生成器网络的关键组成部分有了直观的理解。
生成器
让我们快速浏览以下生成器网络代码,然后讨论生成器网络的关键特性:
class _net_generator(nn.Module):
def __init__(self):
super(_net_generator, self).__init__()
self.main = nn.Sequential(
nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
nn.Tanh()
)
def forward(self, input):
output = self.main(input)
return output
net_generator = _net_generator()
net_generator.apply(weights_inititialisation)
print(net_generator)
在我们看到的大多数代码示例中,我们使用了一系列不同的层,然后在前向方法中定义数据的流动。在生成器网络中,我们在__init__
方法中定义了层和数据的流动,使用了顺序模型。该模型接收大小为 nz 的张量作为输入,然后将其传递给转置卷积以映射输入到需要生成的图像大小。前向函数将输入传递给顺序模块并返回输出。生成器网络的最后一层是一个 tanh 层,限制了网络可以生成的值的范围。
我们不再使用相同的随机权重初始化模型,而是根据论文中定义的权重初始化模型。以下是权重初始化代码:
def weights_inititialisation(m):
class_name = m.__class__.__name__
if class_name.find('Conv') != -1:
m.weight.data.normal_(0.0, 0.02)
elif class_name.find('BatchNorm') != -1:
m.weight.data.normal_(1.0, 0.02)
m.bias.data.fill_(0)
我们通过将函数传递给生成器对象net_generator
来调用权重函数。每一层都会传递给该函数;如果该层是卷积层,我们会以不同的方式初始化权重,如果是BatchNorm
层,则会稍有不同。我们使用以下代码在网络对象上调用该函数:
net_generator.apply(weights_inititialisation)
定义鉴别器网络
让我们快速浏览一下以下鉴别器网络代码,然后讨论鉴别器网络的关键特性:
class _net_discriminator(nn.Module):
def __init__(self):
super(_net_discriminator, self).__init__()
self.main = nn.Sequential(
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, input):
output = self.main(input)
return output.view(-1, 1).squeeze(1)
net_discriminator = _net_discriminator()
net_discriminator.apply(weights_inititialisation)
print(net_discriminator)
在前述网络中有两个重要点,即使用 Leaky ReLU 作为激活函数,以及在最后使用 sigmoid 作为激活层。首先,让我们了解一下 Leaky ReLU 是什么。
Leaky ReLU 是为了解决 ReLU 激活函数中的“神经元死亡”问题。当输入为负数时,Leaky ReLU 不会返回零,而是输出一个非常小的数值,如 0.001。论文中显示,使用 Leaky ReLU 可以提高鉴别器的效率。
另一个重要的区别是在鉴别器末端不使用全连接层。通常会看到最后的全连接层被全局平均池化替换。但使用全局平均池化会降低收敛速度(构建准确分类器所需的迭代次数)。最后的卷积层被展平并传递给 sigmoid 层。
除了这两个区别外,该网络的其余部分与我们在书中看到的其他图像分类器网络类似。
定义损失和优化器
我们将在以下代码中定义二元交叉熵损失和两个优化器,一个用于生成器,另一个用于鉴别器。
criterion = nn.BCELoss()
optimizer_discriminator = optim.Adam(net_discriminator.parameters(), lr, betas=(beta1, 0.95))
optimizer_generator = optim.Adam(net_generator.parameters(), lr, betas=(beta1, 0.95))
到目前为止,这与我们在所有先前示例中看到的非常相似。让我们探索如何训练生成器和鉴别器。
训练鉴别器
鉴别器网络的损失取决于其在真实图像上的表现以及其在生成器网络生成的假图像上的表现。损失可以定义如下:
因此,我们需要使用真实图像和生成器网络生成的假图像来训练鉴别器。
使用真实图像训练鉴别器
让我们将一些真实图像直接作为信息传递给训练鉴别器。
首先,我们将查看执行相同操作的代码,然后探索其重要特征:
output = net_discriminator(inputv)
err_discriminator_real = criterion(output, labelv)
err_discriminator_real.backward()
在前面的代码中,我们计算了鉴别器图像所需的损失和梯度。inputv
和 labelv
值表示 CIFAR10 数据集中的输入图像和标签,对于真实图像标签为 1。这很简单明了,与我们对其他图像分类器网络所做的工作类似。
使用假图像训练鉴别器
现在传递一些随机图像来训练鉴别器。
让我们看一下相关代码,然后探索其重要特征:
fake = net_generator(noisev)
output = net_discriminator(fake.detach())
err_discriminator_fake = criterion(output, labelv)
err_discriminator_fake.backward()
optimizer_discriminator.step()
此代码中的第一行传递了一个大小为 100 的向量,生成器网络(net_generator
)生成一张图像。我们将图像传递给鉴别器,以便其识别图像是真实的还是假的。我们不希望生成器得到训练,因为鉴别器正在训练中。因此,我们通过在其变量上调用 detach
方法来从其图中移除假图像。一旦计算出所有梯度,我们调用优化器来训练鉴别器。
训练生成器网络
让我们看一下用于训练生成器网络的以下代码,然后探索其重要特征:
net_generator.zero_grad()
labelv = Variable(label.fill_(real_label)) # fake labels are real for generator cost
output = net_discriminator(fake)
err_generator = criterion(output, labelv)
err_generator.backward()
optimizer_generator.step()
看起来与我们在训练假图像上训练鉴别器时做的很相似,除了一些关键的不同之处。我们传递了生成器创建的相同假图像,但这次我们没有从生成它的图中分离它,因为我们希望训练生成器。我们计算损失(err_generator
)并计算梯度。然后我们调用生成器优化器,因为我们只想训练生成器,并在生成器生成略微逼真图像之前重复整个过程多次。
训练完整网络
我们已经看了 GAN 训练的各个部分。让我们总结如下,并查看用于训练我们创建的 GAN 网络的完整代码:
-
用真实图像训练鉴别器网络
-
用假图像训练鉴别器网络
-
优化鉴别器
-
根据鉴别器的反馈训练生成器
-
仅优化生成器网络
我们将使用以下代码来训练网络:
for epoch in range(niter):
for i, data in enumerate(dataloader, 0):
# train with real
net_discriminator.zero_grad()
real_cpu, _ = data
batch_size = real_cpu.size(0)
if torch.cuda.is_available():
real_cpu = real_cpu.cuda()
input.resize_as_(real_cpu).copy_(real_cpu)
label.resize_(batch_size).fill_(real_label)
inputv = Variable(input)
labelv = Variable(label)
output = net_discriminator(inputv)
err_discriminator_real = criterion(output, labelv)
err_discriminator_real.backward()
D_x = output.data.mean()
noise.resize_(batch_size, nz, 1, 1).normal_(0, 1)
noisev = Variable(noise)
fake = net_generator(noisev)
labelv = Variable(label.fill_(fake_label))
output = net_discriminator(fake.detach())
err_discriminator_fake = criterion(output, labelv)
err_discriminator_fake.backward()
D_G_z1 = output.data.mean()
err_discriminator = err_discriminator_real + err_discriminator_fake
optimizer_discriminator.step()
net_generator.zero_grad()
labelv = Variable(label.fill_(real_label)) # fake labels are real for generator cost
output = net_discriminator(fake)
err_generator = criterion(output, labelv)
err_generator.backward()
D_G_z2 = output.data.mean()
optimizer_generator.step()
print('[%d/%d][%d/%d] Loss_Discriminator: %.4f Loss_Generator: %.4f D(x): %.4f D(G(z)): %.4f / %.4f'
% (epoch, niter, i, len(dataloader),
err_discriminator.data[0], err_generator.data[0], D_x, D_G_z1, D_G_z2))
if i % 100 == 0:
vutils.save_image(real_cpu,
'%s/real_samples.png' % outf,
normalize=True)
fake = net_generator(fixed_noise)
vutils.save_image(fake.data,
'%s/fake_samples_epoch_%03d.png' % (outf, epoch),
normalize=True)
函数 vutils.save_image
将接收一个张量并保存为图像。如果提供了一个图像的小批量,则将它们保存为图像网格。
在接下来的章节中,我们将看看生成的图像和真实图像的样子。
检查生成的图像
因此,让我们比较生成的图像和真实图像。
生成的图像如下所示:
实际图像如下所示:
比较这两组图像,我们可以看到我们的 GAN 能够学习如何生成图像。
总结
在本章中,我们介绍了如何训练能够使用生成网络生成艺术风格转换的深度学习算法。我们还学习了如何使用 GAN 和 DCGAN 生成新图像。在 DCGAN 中,我们探索了使用真实和虚假图像来训练鉴别器,并检查了生成的图像。除了训练生成新图像外,我们还有一个鉴别器,可以用于分类问题。当有限的标记数据可用时,鉴别器学习有关图像的重要特征,这些特征可以用于分类任务。当有限的标记数据时,我们可以训练一个 GAN,它将给我们一个分类器,可以用来提取特征,然后可以在其上构建一个分类器模块。
在下一章中,我们将介绍一些现代架构,如 ResNet 和 Inception,用于构建更好的计算机视觉模型,以及用于构建语言翻译和图像字幕的序列到序列模型。
第九章:使用现代网络架构进行迁移学习
在上一章中,我们探讨了如何利用深度学习算法创建艺术图像、基于现有数据集生成新图像以及生成文本。在本章中,我们将介绍驱动现代计算机视觉应用和自然语言系统的不同网络架构。我们还将覆盖如何在这些模型中应用迁移学习。
迁移学习是机器学习中的一种方法,其中一个为特定任务开发的模型被重用于另一个任务。例如,如果我们想学习如何驾驶摩托车,但我们已经知道如何驾驶汽车,我们会将关于驾驶汽车的知识转移到新任务,而不是从头开始。
要将这种知识从一个任务转移到另一个任务,网络中的一些层需要被冻结。冻结一层意味着在训练期间不会更新该层的权重。迁移学习的好处在于,它可以通过重复使用预训练模型所学到的知识来加快开发和训练新模型的时间,从而加速结果的产生。
本章将讨论的一些架构如下:
-
残差网络 (ResNet)
-
Inception
-
DenseNet
-
编码器-解码器架构
本章将涵盖以下主题:
-
现代网络架构
-
密集连接卷积网络 – DenseNet
-
模型集成
-
编码器-解码器架构
现代网络架构
当深度学习模型无法学习时,我们最好的做法之一是向模型添加更多的层。随着层数的增加,模型的准确性会提高,然后开始饱和。然而,超过一定数量的层会引入一些挑战,例如梯度消失或梯度爆炸。通过精心初始化权重和引入中间的规范化层,部分解决了这个问题。现代架构,如 ResNet 和 Inception,通过引入不同的技术,如残差连接,试图解决这个问题。
ResNet
ResNet 首次在 2015 年由 Kaiming He 等人在名为《深度残差学习用于图像识别》的论文中提出(arxiv.org/pdf/1512.03385.pdf
)。它使我们能够训练成千上万层并实现高性能。ResNet 的核心概念是引入一个跳过一个或多个层的身份快捷连接。下图展示了 ResNet 的工作原理:
此身份映射没有任何参数。它只是将前一层的输出添加到下一层的输入中。然而,有时候,x 和 F(x)将不具有相同的维度。卷积操作通常会缩小图像的空间分辨率。例如,对 32 x 32 图像进行 3 x 3 卷积会得到一个 30 x 30 图像。此身份映射被线性投影W乘以,以扩展捷径的通道以匹配残差。因此,需要将输入 x 和 F(x)结合起来创建下一层的输入:
以下代码演示了 PyTorch 中简单 ResNet 块的样子:
class ResNetBlock(nn.Module):
def __init__(self,in_channels,output_channels,stride):
super().__init__()
self.convolutional_1 = nn.Conv2d(input_channels,output_channels,kernel_size=3,stride=stride,padding=1,bias=False)
self.bn1 = nn.BatchNorm2d(output_channels)
self.convolutional_2 = nn.Conv2d(output_channels,output_channels,kernel_size=3,stride=stride,padding=1,bias=False)
self.bn2 = nn.BatchNorm2d(output_channels)
self.stride = stride
def forward(self,x):
residual = x
out = self.convolutional_1(x)
out = F.relu(self.bn1(out),inplace=True)
out = self.convolutional_2(out)
out = self.bn2(out)
out += residual
return F.relu(out)
ResNetBasicBlock
类包含一个init
方法,用于初始化各种层次,如卷积层和批量归一化。前向方法几乎与我们到目前为止看到的相同,只是在返回之前将输入添加到层次的输出中。
PyTorch 的torchvision
包提供了一个即插即用的 ResNet 模型,具有不同的层次结构。以下是一些可用的不同模型:
-
ResNet-18
-
ResNet-34
-
ResNet-50
-
ResNet-101
-
ResNet-152
我们还可以将这些模型之一用于迁移学习。torchvision
实例允许我们简单地创建其中一个模型并像以下代码中所示使用它:
from torchvision.models import resnet18
resnet_model = resnet18(pretrained=False)
以下图表展示了一个 34 层的 ResNet 模型的样子:
在这里,我们可以看到该网络由多个 ResNet 块组成。与 VGG 等模型相比,这些现代网络的一个关键优势是它们需要很少的参数,因为它们避免使用需要大量参数训练的全连接层。
现在,我们将在狗和猫的数据集上训练一个 ResNet 模型。我们将使用在第三章中使用的数据,深入神经网络,并将基于从 ResNet 计算得到的特征快速训练一个模型。像往常一样,我们将按照以下步骤训练模型:
-
创建 PyTorch 数据集。
-
创建训练和验证加载器。
-
创建 ResNet 模型。
-
提取卷积特征。
-
为预卷积特征创建自定义 PyTorch 数据集类和加载器。
-
创建一个简单的线性模型。
-
训练和验证模型。
完成后,我们将重复这些步骤用于 Inception 和 DenseNet。最后,我们将探索集成技术,将这些强大的模型组合成一个新模型。
创建 PyTorch 数据集
首先,我们需要创建一个包含所有基本变换的变换对象,并使用ImageFolder
函数从我们在第三章中创建的数据目录加载图像。在以下代码中,我们创建数据集:
transform_data = transforms.Compose([
transforms.Resize((299,299)),
tansforms.ToTensor(),
transforms.Normalize([0.30, 0.40, 0.40], [0.20, 0.20, 0.20])
])
train_dataset = ImageFolder('../Chapter03/Dog-Cat-Classifier/Data/Train_Data/train/',transform=transform_data)
validation_dataset = ImageFolder('../Chapter03/Dog-Cat-Classifier/Data/Train_Data/valid/',transform=transform_data)
classes=2
到目前为止,前面大部分代码将是不言自明的。
创建用于训练和验证的加载器
我们使用 PyTorch 加载器加载数据集提供的批量数据,以及其所有优势,如数据洗牌和使用多线程,以加快进程速度。以下代码展示了这一点:
training_data_loader = DataLoader(train_dataset,batch_size=32,shuffle=False,num_workers=4)
validation_data_loader = DataLoader(validation_dataset,batch_size=32,shuffle=False,num_workers=4)
我们在计算预卷积特征时需要保持数据的确切顺序。当允许数据被洗牌时,我们将无法保持标签。因此,请确保shuffle
为False
;否则,需要在代码内部处理所需的逻辑。
创建一个 ResNet 模型
在这里,我们将考虑一个创建 ResNet 模型的编码示例。首先,我们初始化预训练的resnet34
模型:
resnet_model = resnet34(pretrained=True)
然后,我们丢弃最后一个线性层:
m = nn.Sequential(*list(resnet_model.children())[:-1])
一旦模型创建完成,我们将requires_grad
参数设为False
,这样 PyTorch 就不必维护用于保存梯度的任何空间:
for p in resnet_model.parameters():
p.requires_grad = False
提取卷积特征
在这里,我们通过模型传递来自训练和验证数据加载器的数据,并将结果存储在列表中以供进一步计算。通过计算预卷积特征,我们可以节省大量训练模型的时间,因为我们不会在每次迭代中计算这些特征:
# Stores the labels of the train data
training_data_labels = []
# Stores the pre convoluted features of the train data
training_features = []
迭代通过训练数据,并使用以下代码存储计算得到的特征和标签:
for d,la in training_data_loader:
o = m(Variable(d))
o = o.view(o.size(0),-1)
training_data_labels.extend(la)
training_features.extend(o.data)
对于验证数据,迭代通过验证数据,并使用以下代码存储计算得到的特征和标签:
validation_data_labels = []
validation_features = []
for d,la in validation_data_loader:
o = m(Variable(d))
o = o.view(o.size(0),-1)
validation_data_labels.extend(la)
validation_features.extend(o.data)
创建用于预卷积特征的自定义 PyTorch 数据集类和加载器
现在我们已经计算出了预卷积特征,我们需要创建一个自定义数据集,以便从中选择数据。在这里,我们将为预卷积特征创建一个自定义数据集和加载器:
class FeaturesDataset(Dataset):
def __init__(self,features_list,labels_list):
self.features_list = features_list
self.labels_list = labels_list
def __getitem__(self,index):
return (self.features_lst[index],self.labels_list[index])
def __len__(self):
return len(self.labels_list)
#Creating dataset for train and validation
train_features_dataset = FeaturesDataset(training_features,training_data_labels)
validation_features_dataset = FeaturesDataset(validation_features,validation_data_labels)
一旦创建了用于预卷积特征的自定义数据集,我们可以使用DataLoader
函数,如下所示:
train_features_loader = DataLoader(train_features_dataset,batch_size=64,shuffle=True)
validation_features_loader = DataLoader(validation_features_dataset,batch_size=64)
这将为训练和验证创建一个数据加载器。
创建一个简单的线性模型
现在,我们需要创建一个简单的线性模型,将预卷积特征映射到相应的类别。在这个例子中,有两个类别(狗和猫):
class FullyConnectedLinearModel(nn.Module):
def __init__(self,input_size,output_size):
super().__init__()
self.fc = nn.Linear(input_size,output_size)
def forward(self,inp):
out = self.fc(inp)
return out
fully_connected_in_size = 8192
fc = FullyConnectedLinearModel(fully_connected_in_size,classes)
if is_cuda:
fc = fc.cuda()
现在,我们准备训练我们的新模型并验证数据集。
训练和验证模型
以下代码展示了我们如何训练模型。请注意,fit
函数与 第三章 中讨论的 深入神经网络 的相同。
train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,20):
epoch_loss, epoch_accuracy = fit(epoch,fc,train_features_loader,phase='training')
validation_epoch_loss , validation_epoch_accuracy = fit(epoch,fc,validation_features_loader,phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
Inception
Inception 网络是 CNN 分类器发展中的一个重要里程碑,因为它在速度和准确性上都有所提高。Inception 有许多版本,其中一些最著名的版本包括以下几种:
-
Inception v1 (
arxiv.org/pdf/1409.4842v1.pdf
),通常称为 GoogLeNet -
Inception v2 和 v2 (
arxiv.org/pdf/1512.00567v3.pdf
) -
Inception v4 和 Inception-ResNet (
arxiv.org/pdf/1602.07261.pdf
)
以下图表显示了朴素 Inception 网络的结构(v1):
图片来源:arxiv.org/pdf/1409.4842.pdf
在这里,不同大小的卷积应用于输入,并且所有这些层的输出被连接在一起。这是一个 Inception 模块的最简单版本。还有另一种变体的 Inception 块,我们在通过 3 x 3 和 5 x 5 卷积之前会先通过 1 x 1 卷积来降低维度。1 x 1 卷积用于降低计算瓶颈。1 x 1 卷积一次查看一个值,跨通道。例如,在输入大小为 100 x 64 x 64 上使用 10 x 1 x 1 的滤波器将导致 10 x 64 x 64 的输出。以下图表显示了带有降维的 Inception 块:
图片来源:arxiv.org/pdf/1409.4842.pdf
现在,让我们看一下 PyTorch 中上述 Inception 块的示例:
class BasicConvolutional2d(nn.Module):
def __init__(self, input_channels, output_channels, **kwargs):
super(BasicConv2d, self).__init__()
self.conv = nn.Conv2d(input_channels, output_channels, bias=False, **kwargs)
self.bn = nn.BatchNorm2d(output_channels)
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
return F.relu(x, inplace=True)
class InceptionBlock(nn.Module):
def __init__(self, input_channels, pool_features):
super().__init__()
self.inception_branch_1x1 = BasicConv2d(input_channels, 64, kernel_size=1)
self.inception_branch_5x5_1 = BasicConv2d(input_channels, 48, kernel_size=1)
self.inception_branch_5x5_2 = BasicConv2d(48, 64, kernel_size=5, padding=2)
self.inception_branch_3x3dbl_1 = BasicConv2d(input_channels, 64, kernel_size=1)
self.inception_branch_3x3dbl_2 = BasicConv2d(64, 96, kernel_size=3, padding=1)
self.inception_branch_pool = BasicConv2d(input_channels, pool_features, kernel_size=1)
def forward(self, x):
inception_branch_1x1 = self.inception_branch1x1(x)
inception_branch_5x5 = self.inception_branch_5x5_1(x)
inception_branch_5x5 = self.inception_branch_5x5_2(branch5x5)
inception_branch_3x3dbl = self.inception_branch_3x3dbl_1(x)
inception_branch_3x3dbl = self.inception_branch_3x3dbl_2(inception_branch3x3dbl)
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
branch_pool = self.branch_pool(branch_pool)
outputs = [inception_branch_1x1, inception_branch_5x5, inception_branch_3x3dbl, inception_branch_pool]
return torch.cat(outputs, 1)
上述代码包含两个类:BasicConv2d
和 InceptionBasicBlock
。BasicConv2d
充当自定义层,将二维卷积层、批量归一化和 ReLU 层应用于输入。当我们有重复的代码结构时,创建新的层是很好的做法,以使代码看起来更优雅。
InceptionBasicBlock
类实现了第二个 Inception 图表中的内容。让我们逐个查看每个较小片段,并试图理解其如何实现:
inception_branch_1x1 = self.inception_branch_1x1(x)
在上述代码中,通过应用一个 1 x 1 卷积块来转换输入:
inception_branch_5x5 = self.inception_branch_5x5_1(x)
inception_branch_5x5 = self.inception_branch_5x5_2(inception_branch5x5)
在上述代码中,我们通过应用一个 1 x 1 卷积块,然后是一个 5 x 5 卷积块来转换输入:
inception_branch_3x3dbl = self.inception_branch_3x3dbl_1(x)
inception_branch_3x3dbl = self.inception_branch_3x3dbl_2(inception_branch3x3dbl)
在上述代码中,我们通过应用一个 1 x 1 卷积块,然后是一个 3 x 3 卷积块来转换输入:
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
branch_pool = self.branch_pool(branch_pool)
在上述代码中,我们应用了平均池化以及一个 1 x 1 卷积块。最后,我们将所有结果连接在一起。一个 Inception 网络由多个 Inception 块组成。以下图表显示了 Inception 架构的外观:
Inception 架构
torchvision
包含一个可以像使用 ResNet 网络一样使用的 Inception 网络。对初始 Inception 块进行了许多改进,PyTorch 提供的当前实现是 Inception v3。让我们看看如何从 torchvision
使用 Inception v3 模型来计算预计算特征。我们不会再次介绍数据加载过程,因为我们将使用与 创建一个 ResNet 模型 部分相同的数据加载器。我们将查看以下重要主题:
-
创建一个 Inception 模型
-
使用
register_forward_hook
提取卷积特征 -
为卷积特征创建一个新的数据集
-
创建一个全连接模型
-
训练和验证模型
创建一个 Inception 模型
Inception v3 模型有两个分支,每个分支生成一个输出,在原始模型训练中,我们会合并损失,就像风格迁移一样。目前,我们只关心使用一个分支计算 Inception 的预卷积特征。详细说明超出本书的范围。如果您有兴趣了解更多如何工作的内容,则查阅论文和 Inception 模型的源代码 (github.com/pytorch/vision/blob/master/torchvision/models/inception.py
) 会有所帮助。我们可以通过将 aux_logits
参数设置为 False
来禁用其中一个分支。下面的代码解释了如何创建模型以及如何将 aux_logits
参数设置为 False
:
inception_model = inception_v3(pretrained=True)
inception_model.aux_logits = False
if is_cuda:
inception_model = inception_model.cuda()
从 Inception 模型中提取卷积特征并不简单,因此我们将使用 register_forward_hook
函数来提取激活值。
使用 register_forward_hook
提取卷积特征
我们将使用与计算风格迁移激活值相同的技术。以下是 LayerActivations
类的代码,进行了一些小的修改,因为我们只关心提取特定层的输出:
class LayerActivations():
features=[]
def __init__(self,model):
self.features = []
self.hook = model.register_forward_hook(self.hook_function)
def hook_function(self,module,input,output):
self.features.extend(output.view(output.size(0),-1).cpu().data)
def remove(self):
self.hook.remove()
除了 hook
函数外,其余代码与我们用于风格迁移的代码类似。因为我们捕获了所有图像的输出并将它们存储起来,所以不能将数据保存在图形处理单元(GPU)内存中。因此,我们需要从 GPU 和 CPU 提取张量并仅存储张量而不是 Variable
。我们将它们重新转换为张量,因为数据加载器只能处理张量。在以下代码中,我们使用 LayerActivations
对象从 Inception 模型的最后一层提取输出,跳过了平均池化层、dropout 层和线性层。我们跳过平均池化层以避免在数据中丢失有用信息:
# Create LayerActivations object to store the output of inception model at a particular layer.
train_features = LayerActivations(inception_model.Mixed_7c)
train_labels = []
# Passing all the data through the model , as a side effect the outputs will get stored
# in the features list of the LayerActivations object.
for da,la in train_loader:
_ = inception_model(Variable(da.cuda()))
train_labels.extend(la)
train_features.remove()
# Repeat the same process for validation dataset .
validation_features = LayerActivations(inception_model.Mixed_7c)
validation_labels = []
for da,la in validation_loader:
_ = inception_model(Variable(da.cuda()))
validation_labels.extend(la)
validation_features.remove()
让我们创建所需的新卷积特征数据集和加载器。
创建用于卷积特征的新数据集
我们可以使用相同的FeaturesDataset
类来创建新的数据集和数据加载器。在以下代码中,我们正在创建数据集和加载器:
#Dataset for pre computed features for train and validation data sets
train_feat_dset = FeaturesDataset(train_features.features,train_labels)
validation_feat_dset = FeaturesDataset(validation_features.features,validation_labels)
#Data loaders for pre computed features for train and validation data sets
train_feat_loader = DataLoader(train_feat_dset,batch_size=64,shuffle=True)
validation_feat_loader = DataLoader(validation_feat_dset,batch_size=64)
让我们创建一个新模型,我们可以在预卷积特征上训练。
创建一个全连接模型
一个简单的模型可能会导致过拟合,因此让我们在模型中包含 dropout。Dropout 将帮助我们避免过拟合。在以下代码中,我们正在创建我们的模型:
class FullyConnectedModel(nn.Module):
def __init__(self,input_size,output_size,training=True):
super().__init__()
self.fully_connected = nn.Linear(input_size,output_size)
def forward(self,input):
output = F.dropout(input, training=self.training)
output = self.fully_connected(output)
return output
# The size of the output from the selected convolution feature
fc_in_size = 131072
fc = FullyConnectedModel(fc_in_size,classes)
if is_cuda:
fc = fc.cuda()
一旦模型创建完成,我们就可以开始训练模型。
训练和验证模型
在这里,我们将使用与我们的 ResNet 示例中相同的拟合和训练逻辑。我们只会看一下训练代码和它返回的结果:
for epoch in range(1,10):
epoch_loss, epoch_accuracy = fit(epoch,fc,train_feat_loader,phase='training')
validation_epoch_loss , validation_epoch_accuracy = fit(epoch,fc,validation_feat_loader,phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
这将产生以下输出:
training loss is 0.78 and training accuracy is 22825/23000 99.24
validation loss is 5.3 and validation accuracy is 1947/2000 97.35
training loss is 0.84 and training accuracy is 22829/23000 99.26
validation loss is 5.1 and validation accuracy is 1952/2000 97.6
training loss is 0.69 and training accuracy is 22843/23000 99.32
validation loss is 5.1 and validation accuracy is 1951/2000 97.55
training loss is 0.58 and training accuracy is 22852/23000 99.36
validation loss is 4.9 and validation accuracy is 1953/2000 97.65
training loss is 0.67 and training accuracy is 22862/23000 99.4
validation loss is 4.9 and validation accuracy is 1955/2000 97.75
training loss is 0.54 and training accuracy is 22870/23000 99.43
validation loss is 4.8 and validation accuracy is 1953/2000 97.65
training loss is 0.56 and training accuracy is 22856/23000 99.37
validation loss is 4.8 and validation accuracy is 1955/2000 97.75
training loss is 0.7 and training accuracy is 22841/23000 99.31
validation loss is 4.8 and validation accuracy is 1956/2000 97.8
training loss is 0.47 and training accuracy is 22880/23000 99.48
validation loss is 4.7 and validation accuracy is 1956/2000 97.8
查看结果,Inception 模型在训练数据集上达到了 99%的准确率,在验证数据集上达到了 97.8%的准确率。由于我们预先计算并保存了所有特征在内存中,所以训练模型只需不到几分钟。如果您在运行程序时遇到内存不足的问题,则可能需要避免在内存中保存特征。
在下一节中,我们将看到另一个有趣的架构,DenseNet,这在过去一年中变得非常流行。
密集连接卷积网络 – DenseNet
一些最成功和最流行的架构,如 ResNet 和 Inception,显示了更深更宽网络的重要性。ResNet 使用快捷连接来构建更深的网络。DenseNet 通过允许从每一层到后续层的连接,即我们可以接收来自前一层的所有特征映射的层,将这一点推到了一个全新的水平。符号上看,它会如下所示:
以下图表描述了一个五层密集块的外观:
torchvision
还有一个 DenseNet 的实现(github.com/pytorch/vision/blob/master/torchvision/models/densenet.py
)。让我们看一下它的两个主要功能,即_DenseBlock
和_DenseLayer
。
_DenseBlock
对象
让我们来看看_DenseBlock
的代码,然后逐步解析它:
class _DenseBlock(nn.Sequential):
def __init__(self, number_layers, number_input_features, bn_size, growth_rate, drop_rate):
super(_DenseBlock, self).__init__()
for i in range(number_layers):
layer = _DenseLayer(number_input_features + i * growth_rate, growth_rate, bn_size, drop_rate)
self.add_module('denselayer%d' % (i + 1), layer)
_DenseBlock
是一个顺序模块,在这里我们按顺序添加层。基于块中的层数(number_layers
),我们添加相应数量的_DenseLayer
对象,并赋予一个名称。所有的魔法都发生在_DenseLayer
对象内部。让我们看看DenseLayer
对象内部发生了什么。
_DenseLayer
对象
学习特定网络工作方式的一种方法是查看源代码。PyTorch 的实现非常干净,大多数情况下很容易阅读。让我们来看看 _DenseLayer
的实现:
class _DenseLayer(nn.Sequential):
def __init__(self, number_input_features, growth_rate, bn_size, drop_rate):
super(_DenseLayer, self).__init__()
self.add_module('norm.1', nn.BatchNorm2d(number_input_features)),
self.add_module('relu.1', nn.ReLU(inplace=True)),
self.add_module('conv.1', nn.Conv2d(number_input_features, bn_size *
growth_rate, kernel_size=1, stride=1, bias=False)),
self.add_module('norm.2', nn.BatchNorm2d(bn_size * growth_rate)),
self.add_module('relu.2', nn.ReLU(inplace=True)),
self.add_module('conv.2', nn.Conv2d(bn_size * growth_rate, growth_rate,
kernel_size=3, stride=1, padding=1, bias=False)),
self.drop_rate = drop_rate
def forward(self, x):
new_features = super(_DenseLayer, self).forward(x)
if self.drop_rate > 0:
new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)
return torch.cat([x, new_features], 1)
如果您对 Python 中的继承还不熟悉,那么前面的代码可能看起来不直观。_DenseLayer
对象是 nn.Sequential
的子类;让我们看看每个方法内部发生了什么。
在 __init__
方法中,我们添加了所有需要传递给输入数据的层。这与我们之前看到的所有其他网络架构非常相似。
forward
方法中的魔法发生在这里。我们将输入传递给超类 nn.Sequential
的 forward
方法。让我们看看序列类 forward
方法中发生了什么(github.com/pytorch/pytorch/blob/409b1c8319ecde4bd62fcf98d0a6658ae7a4ab23/torch/nn/modules/container.py
):
def forward(self, input):
for module in self._modules.values():
input = module(input)
return input
输入通过之前添加到序列块中的所有层,并将输出连接到输入。这个过程在块中所需的层数中重复进行。
现在我们理解了 DenseNet 块的工作原理,让我们探索如何使用 DenseNet 计算预卷积特征并在其上构建分类器模型。在高层次上,DenseNet 的实现类似于 VGG 的实现。DenseNet 的实现还有一个特征模块,其中包含所有的稠密块,以及一个分类器模块,其中包含全连接模型。在本节中,我们将按照以下步骤构建模型,但将跳过与 Inception 和 ResNet 相似的大部分部分,例如创建数据加载器和数据集。
我们将详细讨论以下步骤:
-
创建 DenseNet 模型
-
提取 DenseNet 特征
-
创建数据集和加载器
-
创建全连接模型并对其进行训练
到目前为止,大多数代码都是不言自明的。
创建 DenseNet 模型
Torchvision 提供了预训练的 DenseNet 模型,具有不同的层选项(121、169、201 和 161)。在这里,我们选择了具有 121 层的模型。正如我们之前提到的,DenseNet 模型有两个模块:features
(包含稠密块)和 classifier
(全连接块)。由于我们将 DenseNet 用作图像特征提取器,我们只会使用 features
模块:
densenet_model = densenet121(pretrained=True).features
if is_cuda:
densenet_model = densenet_model.cuda()
for p in densenet_model.parameters():
p.requires_grad = False
让我们从图像中提取 DenseNet 特征。
提取 DenseNet 特征
这个过程类似于我们对 Inception 所做的操作,只是我们没有使用 register_forward_hook
来提取特征。以下代码展示了如何从图像中提取 DenseNet 特征:
#For training data
train_labels = []
train_features = []
#code to store densenet features for train dataset.
for d,la in train_loader:
o = densenet_model(Variable(d.cuda()))
o = o.view(o.size(0),-1)
train_labels.extend(la)
train_features.extend(o.cpu().data)
#For validation data
validation_labels = []
validation_features = []
#Code to store densenet features for validation dataset.
for d,la in validation_loader:
o = densenet_model(Variable(d.cuda()))
o = o.view(o.size(0),-1)
validation_labels.extend(la)
validation_features.extend(o.cpu().data)
前面的代码与我们看到的 Inception 和 ResNet 类似。
创建数据集和加载器
我们将使用我们为 ResNet 创建的相同FeaturesDataset
类,并用它来为训练和验证数据集创建数据加载器。我们将使用以下代码来实现:
# Create dataset for train and validation convolution features
train_feat_dset = FeaturesDataset(train_features,train_labels)
validation_feat_dset = FeaturesDataset(validation_features,validation_labels)
# Create data loaders for batching the train and validation datasets
train_feat_loader = DataLoader(train_feat_dset,batch_size=64,shuffle=True,drop_last=True)
validation_feat_loader = DataLoader(validation_feat_dset,batch_size=64)
现在,是时候创建模型并训练它了。
创建一个全连接模型并训练它
现在,我们将使用一个简单的线性模型,类似于我们在 ResNet 和 Inception 中使用的模型,来训练模型。以下代码展示了我们将用于训练模型的网络架构:
class FullyConnectedModel(nn.Module):
def __init__(self,input_size,output_size):
super().__init__()
self.fc = nn.Linear(input_size,output_size)
def forward(self,input):
output = self.fc(input)
return output
fc = FullyConnectedModel(fc_in_size,classes)
if is_cuda:
fc = fc.cuda()
我们将使用相同的fit
方法来训练前述模型。下面的代码片段显示了训练代码及其结果:
train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,10):
epoch_loss, epoch_accuracy = fit(epoch,fc,train_feat_loader,phase='training')
validation_epoch_loss , validation_epoch_accuracy = fit(epoch,fc,validation_feat_loader,phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
前述代码的结果如下:
training loss is 0.057 and training accuracy is 22506/23000 97.85
validation loss is 0.034 and validation accuracy is 1978/2000 98.9
training loss is 0.0059 and training accuracy is 22953/23000 99.8
validation loss is 0.028 and validation accuracy is 1981/2000 99.05
training loss is 0.0016 and training accuracy is 22974/23000 99.89
validation loss is 0.022 and validation accuracy is 1983/2000 99.15
training loss is 0.00064 and training accuracy is 22976/23000 99.9
validation loss is 0.023 and validation accuracy is 1983/2000 99.15
training loss is 0.00043 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1983/2000 99.15
training loss is 0.00033 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1984/2000 99.2
training loss is 0.00025 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1984/2000 99.2
training loss is 0.0002 and training accuracy is 22976/23000 99.9
validation loss is 0.025 and validation accuracy is 1985/2000 99.25
training loss is 0.00016 and training accuracy is 22976/23000 99.9
validation loss is 0.024 and validation accuracy is 1986/2000 99.3
前述算法能够达到最高 99%的训练精度和 99%的验证精度。由于您可能创建的验证数据集可能具有不同的图像,因此您的结果可能会有所不同。
DenseNet 的一些优点如下:
-
它显著减少了所需的参数数量。
-
它缓解了梯度消失问题。
-
它鼓励特征重用。
在接下来的部分中,我们将探讨如何构建一个模型,结合使用 ResNet、Inception 和 DenseNet 计算的卷积特征的优势。
模型集成
有时我们需要尝试结合多个模型来构建一个非常强大的模型。我们可以使用许多技术来构建集成模型。在本节中,我们将学习如何使用由三个不同模型(ResNet、Inception 和 DenseNet)生成的特征来组合输出,以构建一个强大的模型。我们将使用本章中其他示例中使用的相同数据集。
集成模型的架构如下:
前面的图表显示了我们将在集成模型中执行的操作,可以总结如下步骤:
-
创建三个模型。
-
使用创建的模型提取图像特征。
-
创建一个自定义数据集,返回所有三个模型的特征以及标签。
-
创建一个与前面图表中显示的架构类似的模型。
-
训练和验证模型。
让我们详细探讨每个步骤。
创建模型
让我们创建所有三个所需的模型,如以下代码块所示。
创建 ResNet 模型的代码如下:
resnet_model = resnet34(pretrained=True)
if is_cuda:
resnet_model = resnet_model.cuda()
resnet_model = nn.Sequential(*list(resnet_model.children())[:-1])
for p in resnet_model.parameters():
p.requires_grad = False
Inception 模型的代码如下:
inception_model = inception_v3(pretrained=True)
inception_model.aux_logits = False
if is_cuda:
inception_model = inception_model.cuda()
for p in inception_model.parameters():
p.requires_grad = False
DenseNet 模型的代码如下:
densenet_model = densenet121(pretrained=True).features
if is_cuda:
densenet_model = densenet_model.cuda()
for p in densenet_model.parameters():
p.requires_grad = False
现在我们已经有了所有的模型,让我们从图像中提取特征。
提取图像特征
在这里,我们将结合我们在本章中各个算法中单独看到的所有逻辑。
ResNet 的代码如下:
train_labels = []
train_resnet_features = []
for d,la in train_loader:
o = resnet_model(Variable(d.cuda()))
o = o.view(o.size(0),-1)
train_labels.extend(la)
train_resnet_features.extend(o.cpu().data)
validation_labels = []
validation_resnet_features = []
for d,la in validation_loader:
o = resnet_model(Variable(d.cuda()))
o = o.view(o.size(0),-1)
validation_labels.extend(la)
validation_resnet_features.extend(o.cpu().data)
Inception 的代码如下:
train_inception_features = LayerActivations(inception_model.Mixed_7c)
for da,la in train_loader:
_ = inception_model(Variable(da.cuda()))
train_inception_features.remove()
validation_inception_features = LayerActivations(inception_model.Mixed_7c)
for da,la in validation_loader:
_ = inception_model(Variable(da.cuda()))
validation_inception_features.remove()
DenseNet 的代码如下:
train_densenet_features = []
for d,la in train_loader:
o = densnet_model(Variable(d.cuda()))
o = o.view(o.size(0),-1)
train_densenet_features.extend(o.cpu().data)
validation_densenet_features = []
for d,la in validation_loader:
o = densnet_model(Variable(d.cuda()))
o = o.view(o.size(0),-1)
validation_densenet_features.extend(o.cpu().data)
现在,我们使用所有模型创建了图像特征。如果您遇到内存问题,则可以删除一个模型或停止存储训练速度较慢的特征。如果您在运行 CUDA 实例,则可以选择更强大的实例。
创建自定义数据集及其数据加载器
由于 FeaturesDataset
类仅开发用于挑选来自单个模型的输出,因此我们无法使用它。由于这一点,以下实现包含对 FeaturesDataset
类所做的轻微更改,以便我们可以容纳所有三个生成的特征:
class FeaturesDataset(Dataset):
def __init__(self,featlst1,featlst2,featlst3,labellst):
self.featlst1 = featlst1
self.featlst2 = featlst2
self.featlst3 = featlst3
self.labellst = labellst
def __getitem__(self,index):
return (self.featlst1[index],self.featlst2[index],self.featlst3[index],self.labellst[index])
def __len__(self):
return len(self.labellst)
train_feat_dset = FeaturesDataset(train_resnet_features,train_inception_features.features,train_densenet_features,train_labels)
validation_feat_dset = FeaturesDataset(validation_resnet_features,validation_inception_features.features,validation_densenet_features,validation_labels)
在这里,我们对 __init__
方法进行了更改,以便我们可以存储从不同模型生成的所有特征。我们还改变了 __getitem__
方法,以便我们可以检索图像的特征和标签。使用 FeatureDataset
类,我们为训练和验证数据创建了数据集实例。创建数据集后,我们可以使用相同的数据加载器来批处理数据,如下面的代码所示:
train_feat_loader = DataLoader(train_feat_dset,batch_size=64,shuffle=True)
validation_feat_loader = DataLoader(validation_feat_dset,batch_size=64)
创建集成模型
现在,我们需要创建一个像我们之前看到的架构图一样工作的模型。以下代码实现了这一点:
class EnsembleModel(nn.Module):
def __init__(self,output_size,training=True):
super().__init__()
self.fully_connected1 = nn.Linear(8192,512)
self.fully_connected2 = nn.Linear(131072,512)
self.fully_connected3 = nn.Linear(82944,512)
self.fully_connected4 = nn.Linear(512,output_size)
def forward(self,input1,input2,input3):
output1 = self.fully_connected1(F.dropout(input1,training=self.training))
output2 = self.fully_connected2(F.dropout(input2,training=self.training))
output3 = self.fully_connected3(F.dropout(input3,training=self.training))
output = output1 + output2 + output3
output = self.fully_connected4(F.dropout(out,training=self.training))
return output
em = EnsembleModel(2)
if is_cuda:
em = em.cuda()
在上述代码中,我们创建了三个线性层,这些层将由不同模型生成的特征作为输入。我们将这三个线性层的所有输出相加,并将它们传递到另一个线性层,将它们映射到所需的类别。为了防止模型过拟合,我们使用了 dropout。
训练和验证模型
我们需要对 fit
方法进行一些小的更改,以适应从数据加载器生成的三个输入值。以下代码实现了新的 fit
函数:
def fit(epoch,model,data_loader,phase='training',volatile=False):
if phase == 'training':
model.train()
if phase == 'validation':
model.eval()
volatile=True
running_loss = 0.0
running_correct = 0
for batch_idx , (data1,data2,data3,target) in enumerate(data_loader):
if is_cuda:
data1,data2,data3,target = data1.cuda(),data2.cuda(),data3.cuda(),target.cuda()
data1,data2,data3,target = Variable(data1,volatile),Variable(data2,volatile),Variable(data3,volatile),Variable(target)
if phase == 'training':
optimizer.zero_grad()
output = model(data1,data2,data3)
loss = F.cross_entropy(output,target)
running_loss += F.cross_entropy(output,target,size_average=False).data[0]
preds = output.data.max(dim=1,keepdim=True)[1]
running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
if phase == 'training':
loss.backward()
optimizer.step()
loss = running_loss/len(data_loader.dataset)
accuracy = 100\. * running_correct/len(data_loader.dataset)
print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
return loss,accuracy
正如您所看到的,大部分代码保持不变,除了加载器返回三个输入和一个标签。因此,我们必须对功能进行更改,这是不言自明的。
以下是训练代码:
train_losses , train_accuracy = [],[]
validation_losses , validation_accuracy = [],[]
for epoch in range(1,10):
epoch_loss, epoch_accuracy = fit(epoch,em,trn_feat_loader,phase='training')
validation_epoch_loss , validation_epoch_accuracy = fit(epoch,em,validation_feat_loader,phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
validation_losses.append(validation_epoch_loss)
validation_accuracy.append(validation_epoch_accuracy)
上述代码的结果如下:
training loss is 7.2e+01 and training accuracy is 21359/23000 92.87
validation loss is 6.5e+01 and validation accuracy is 1968/2000 98.4
training loss is 9.4e+01 and training accuracy is 22539/23000 98.0
validation loss is 1.1e+02 and validation accuracy is 1980/2000 99.0
training loss is 1e+02 and training accuracy is 22714/23000 98.76
validation loss is 1.4e+02 and validation accuracy is 1976/2000 98.8
training loss is 7.3e+01 and training accuracy is 22825/23000 99.24
validation loss is 1.6e+02 and validation accuracy is 1979/2000 98.95
training loss is 7.2e+01 and training accuracy is 22845/23000 99.33
validation loss is 2e+02 and validation accuracy is 1984/2000 99.2
training loss is 1.1e+02 and training accuracy is 22862/23000 99.4
validation loss is 4.1e+02 and validation accuracy is 1975/2000 98.75
training loss is 1.3e+02 and training accuracy is 22851/23000 99.35
validation loss is 4.2e+02 and validation accuracy is 1981/2000 99.05
training loss is 2e+02 and training accuracy is 22845/23000 99.33
validation loss is 6.1e+02 and validation accuracy is 1982/2000 99.1
training loss is 1e+02 and training accuracy is 22917/23000 99.64
validation loss is 5.3e+02 and validation accuracy is 1986/2000 99.3
集成模型实现了 99.6%的训练准确率和 99.3%的验证准确率。虽然集成模型功能强大,但计算成本高昂。它们是解决 Kaggle 等竞赛中问题时的好技术。
编码器-解码器架构
在本书中,我们看到的几乎所有深度学习算法都擅长于学习如何将训练数据映射到其对应的标签。我们不能直接将它们用于需要从序列学习并生成另一个序列或图像的任务。一些示例应用如下:
-
语言翻译
-
图像字幕
-
图像生成 (
seq2img
) -
语音识别
-
问答
大多数这些问题可以看作是序列到序列映射的形式,并且可以使用一类称为编码器-解码器架构的家族来解决。在本节中,我们将了解这些架构背后的直觉。我们不会深入研究这些网络的实现,因为它们需要更详细的学习。
在高层次上,编码器-解码器架构如下所示:
编码器通常是一个循环神经网络(RNN)(用于序列数据)或者卷积神经网络(CNN)(用于图像),接收图像或序列并将其转换为一个固定长度的向量,编码了所有信息。解码器是另一个 RNN 或 CNN,它学习解码编码器生成的向量,并生成新的数据序列。以下图表显示了图像字幕系统中编码器-解码器架构的外观:
图像来源:arxiv.org/pdf/1411.4555.pdf
现在,让我们看看图像字幕系统中编码器和解码器架构内部发生了什么。
编码器
对于图像字幕系统,我们应该使用训练好的架构,比如 ResNet 或 Inception,从图像中提取特征。就像我们为集成模型所做的那样,我们可以通过使用线性层输出一个固定长度的向量,然后使该线性层可训练。
解码器
解码器是一个长短期记忆(LSTM)层,用于为图像生成字幕。为了构建一个简单的模型,我们可以将编码器嵌入作为 LSTM 的输入。然而,对于解码器来说,学习起来可能会有挑战;因此,常见的做法是在解码器的每个步骤中提供编码器嵌入。直观地说,解码器学习生成一系列文本,最好地描述给定图像的字幕。
具有注意力机制的编码器-解码器
在 2017 年,阿希什·瓦斯瓦尼和合作者发表了一篇名为Attention Is All You Need的论文(arxiv.org/pdf/1706.03762.pdf
),该论文引入了注意力机制。在每个时间步,注意力网络计算像素的权重。它考虑到迄今为止已生成的单词序列,并输出接下来应该描述什么:
在上面的例子中,我们可以看到 LSTM 保留信息的能力可以帮助它学习在“一个男人”之后逻辑地写入“正在抱着一只狗”。
摘要
在本章中,我们探讨了一些现代架构,如 ResNet、Inception 和 DenseNet。我们还探讨了如何利用这些模型进行迁移学习和集成,并介绍了编码器-解码器架构,这种架构驱动了许多系统,如语言翻译系统。
在接下来的章节中,我们将深入探讨深度强化学习,并学习模型如何应用于解决现实世界中的问题。我们还将看看一些 PyTorch 实现,这些实现可以帮助实现这一目标。
第十章:深度强化学习
本章从基本介绍强化学习(RL)开始,包括代理、状态、动作、奖励和策略。它扩展到基于深度学习(DL)的架构,用于解决 RL 问题,如策略梯度方法、深度 Q 网络和演员-评论家模型。本章将解释如何使用这些深度学习架构及其手动代码在 OpenAI Gym 环境中解决序列决策问题。
具体而言,将涵盖以下内容:
-
强化学习简介
-
使用深度学习解决强化学习问题
-
策略梯度和 PyTorch 中的代码演示
-
深度 Q 网络和 PyTorch 中的代码演示
-
Actor-critic 网络和 PyTorch 中的代码演示
-
强化学习在现实世界中的应用
强化学习简介
强化学习是机器学习的一个分支,其中代理程序学习在给定环境中的最佳行为方式。代理程序执行某些动作并观察奖励/结果。它学习将情况映射到行动的过程,以最大化奖励。
强化学习过程可以建模为一个迭代循环,并可以表示为称为马尔可夫决策过程(MDP)的数学框架。以下步骤概述了该过程的进行:
-
强化学习代理从环境中接收状态(s[0])。
-
强化学习代理根据当前状态(s[0])采取行动(a[0])。在此阶段,由于代理没有关于可能获得的奖励的先前知识,它采取的行动是随机的。
-
第一次行动发生后,代理现在可以被认为处于状态s[1]。
-
此时,环境向代理提供奖励(r[1])。
这一循环持续重复;它输出一个状态和动作序列,并观察奖励。
该过程本质上是一种学习 MDP 策略的算法:
每个时间步的累积奖励与给定行动相关的表示如下:
在某些应用中,可能有利于更多地重视及时收到的奖励。例如,今天收到£100 比 5 年后收到要好。为了纳入这一点,引入一个折扣因子𝛾是很常见的。
累积折扣奖励表示如下:
强化学习中的一个重要考虑因素是奖励可能是不频繁和延迟的。在存在长时间延迟奖励的情况下,追溯哪些动作序列导致了奖励可能是具有挑战性的。
基于模型的强化学习
基于模型的强化学习模拟环境的行为。它预测采取行动后的下一个状态。可以用概率分布的形式数学表示如下:
在此,p表示模型,x是状态,a是控制或动作。
这个概念可以通过考虑平衡杆示例来进行演示。目标是让附着在小车上的杆保持竖直,代理可以决定两种可能的动作之间的选择:将小车向左移动或将小车向右移动。在下面的截图中,P 模拟了采取行动后杆的角度:
下图描述了下一时间步中θ的概率分布输出:
在这种情况下,模型描述了物理定律;然而,模型可能是任何应用的依据。另一个例子是模型可以建立在国际象棋游戏的规则上。
基于模型的强化学习的核心概念是使用模型和成本函数来定位最佳路径的行动或者说状态和行动的轨迹,𝝉:
基于模型的算法的缺点是,随着状态空间和动作空间的扩大,它们可能变得不切实际。
无模型强化学习
无模型算法依赖试验和错误来更新其知识。因此,它们不需要空间来存储所有状态和动作的组合。策略梯度、值学习或其他无模型强化学习方法用于找到一个最大化奖励的最佳行动策略。无模型和基于模型方法的一个关键区别在于,无模型方法在真实环境中行动学习。
比较基于策略和离策略
策略定义了代理如何行动;它告诉代理在每个状态下应该如何行动。每个强化学习算法必须遵循某种策略来决定其行为方式。
代理试图学习的策略函数可以表示如下,其中θ是参数向量,s是特定状态,a是一个动作:
基于策略的代理学习值(期望的折扣奖励),基于当前动作并源于当前策略。离策略学习值则基于从另一策略(如贪婪策略,如我们接下来介绍的 Q-learning 中)获得的动作。
Q-learning
Q-learning 是一种无模型强化学习算法,通过创建一个表格来计算每个状态下每个动作的最大预期未来奖励。它被认为是离策略的,因为 Q-learning 函数从当前策略之外的动作中学习。
当进行 Q-learning 时,会创建一个 Q-表格,其中列代表可能的动作,行代表状态。Q-表格中每个单元格的值将是给定状态和动作的最大预期未来奖励:
每个 Q-表格分数将是从最佳策略中获取的动作的最大预期未来奖励。Q-learning 算法用于学习 Q-表格中的每个值。
Q 函数(或动作值函数)接受两个输入:状态和奖励。Q 函数返回该状态下执行该动作的预期未来奖励。它可以如下表示:
Q 函数基本上通过滚动 Q 表来查找与当前状态相关的行和与动作相关的列。从这里,它返回相应的预期未来奖励 Q 值。
考虑到在下图中展示的倒立摆示例中。在当前状态下,向左移动应该比向右移动具有更高的 Q 值:
随着环境的探索,Q 表将被更新以提供更好的近似值。Q 学习算法的过程如下:
-
初始化 Q 表。
-
根据当前的 Q 值估计,选择当前状态(s)中的一个动作。
-
执行一个动作(a)。
-
奖励(r)被测量。
-
使用贝尔曼方程更新 Q 值。
贝尔曼方程如下所示:
步骤 2-5 重复执行,直到达到最大的回合数或手动停止训练。
Q 学习算法可以表示为以下方程:
值方法
值学习是许多强化学习方法的关键构建模块。值函数 V(s) 表示代理所处状态的好坏程度。它等于从状态 s 开始代理预期的总奖励。总预期奖励取决于代理通过选择动作执行的策略。如果代理使用给定策略(𝛑)选择其动作,则相应的值函数由以下公式给出:
在倒立摆示例中考虑到这一点,我们可以利用杆子保持直立的时间长度来衡量奖励。在下面的截图中,与状态 s2 相比,状态 s1 杆子保持直立的概率更高。因此,对于大多数策略而言,状态 s1 的值函数可能更高(即更高的期望未来奖励):
存在一个优化的值函数,其对所有状态都具有比其他函数更高的值,可以表示为以下形式:
存在一个与最优值函数对应的最优策略:
可以通过几种不同的方式找到最优策略。这称为策略评估。
值迭代
值迭代是一个通过迭代改进V(s)估计来计算最优状态值函数的过程。首先,算法将V(s)初始化为随机值。然后,它重复更新Q(s, a)和V(s)的值,直到它们收敛。值迭代收敛到最优值。
下面是值迭代的伪代码:
编码示例 – 值迭代
为了说明这一点,我们将使用 OpenAI Gym 中的 Frozen Lake 环境作为示例。在这个环境中,玩家需要想象自己站在一个部分冻结的湖面上。目标是从起点 S 移动到终点 G 而不掉入洞中:
网格上的字母代表以下内容:
-
S:起点,安全
-
F:冻结表面,安全
-
H:洞,不安全
-
G:目标
代理可以采取四种可能的移动:左、右、下、上,分别表示为 0、1、2、3。因此,有 16 种可能的状态(4 x 4)。对于每个 H 状态,代理会收到-1 的奖励,并在达到目标时收到+1 的奖励。
要在代码中实现值迭代,我们首先导入希望使用的相关库,并初始化FrozenLake
环境:
import gym
import numpy as np
import time, pickle, os
env = gym.make('FrozenLake-v0')
现在,我们为变量赋值:
# Epsilon for an epsilon greedy approach
epsilon = 0.95
total_episodes = 1000
# Maximum number of steps to be run for every episode
maximum_steps = 100
learning_rate = 0.75
# The discount factor
gamma = 0.96
从这里开始,我们初始化 Q 表,其中env.observation_space.n
是状态数,env.action_space.n
是动作数。
Q = np.zeros((env.observation_space.n, env.action_space.n))
定义代理选择动作和学习的函数:
def select_action(state):
action=0
if np.random.uniform(0, 1) < epsilon:
# If the random number sampled is smaller than epsilon then a random action is chosen.
action = env.action_space.sample()
else:
# If the random number sampled is greater than epsilon then we choose an action having the maximum value in the Q-table
action = np.argmax(Q[state, :])
return action
def agent_learn(state, state_next, reward, action):
predict = Q[state, action]
target = reward + gamma * np.max(Q[state_next, :])
Q[state, action] = Q[state, action] + learning_rate * (target - predict)
从这里开始,我们可以开始运行回合并将 Q 表导出到一个 pickle 文件中:
for episode in range(total_episodes):
state = env.reset()
t = 0
while t < maximum_steps:
env.render()
action = select_action(state)
state_next, reward, done, info = env.step(action)
agent_learn(state, state_next, reward, action)
state = state_next
t += 1
if done:
break
time.sleep(0.1)
print(Q)
with open("QTable_FrozenLake.pkl", 'wb') as f:
pickle.dump(Q, f)
我们可以通过运行前面的代码来看到这个过程:
策略方法
在基于策略的强化学习中,目标是找到能做出最有益决策的策略,可以数学表示如下:
强化学习中的策略可以是确定性的,也可以是随机的:
随机策略输出的是概率分布而不是单一的离散值:
我们可以将这个目标数学化地表示如下:
策略迭代
在值迭代过程中,有时最优策略在值函数之前收敛,因为它只关心找到最优策略。还可以执行另一个称为策略迭代的算法来达到最优策略。这是在每次策略评估之后,下一个策略基于值函数直到策略收敛的过程。以下图示展示了策略迭代过程:
因此,策略迭代算法保证收敛到最优策略,而值迭代算法则不一定。
以下是策略迭代的伪代码:
编码示例 - 策略迭代
在这里,我们考虑使用之前介绍的 Frozen Lake 环境的编码示例。
首先,导入相关的库:
import numpy as np
import gym
from gym import wrappers
现在,我们定义函数来运行一个 episode 并返回奖励:
def run_episode_return_reward(environment, policy, gamma_value = 1.0, render = False):
""" Runs an episode and return the total reward """
obs = environment.reset()
total_reward = 0
step_index = 0
while True:
if render:
environment.render()
obs, reward, done , _ = environment.step(int(policy[obs]))
total_reward += (gamma_value ** step_index * reward)
step_index += 1
if done:
break
return total_reward
从这里,我们可以定义评估策略的函数:
def evaluate_policy(environment, policy, gamma_value = 1.0, n = 200):
model_scores = [run_episode_return_reward(environment, policy, gamma_value, False) for _ in range(n)]
return np.mean(model_scores)
然后,定义提取策略的函数:
def extract_policy(v, gamma_value = 1.0):
""" Extract the policy for a given value function """
policy = np.zeros(environment.nS)
for s in range(environment.nS):
q_sa = np.zeros(environment.nA)
for a in range(environment.nA):
q_sa[a] = sum([p * (r + gamma_value * v[s_]) for p, s_, r, _ in environment.P[s][a]])
policy[s] = np.argmax(q_sa)
return policy
最后,定义计算策略的函数:
def compute_policy_v(environment, policy, gamma_value=1.0):
""" Iteratively evaluate the value-function under policy.
Alternatively, we could formulate a set of linear equations in terms of v[s]
and solve them to find the value function.
"""
v = np.zeros(environment.nS)
eps = 1e-10
while True:
prev_v = np.copy(v)
for s in range(environment.nS):
policy_a = policy[s]
v[s] = sum([p * (r + gamma_value * prev_v[s_]) for p, s_, r, _ in environment.P[s][policy_a]])
if (np.sum((np.fabs(prev_v - v))) <= eps):
# value converged
break
return v
从这里,我们可以在 Frozen Lake 环境上运行策略迭代:
env_name = 'FrozenLake-v0'
environment = gym.make(env_name)
optimal_policy = policy_iteration(environment, gamma_value = 1.0)
scores = evaluate_policy(environment, optimal_policy, gamma_value = 1.0)
print('Average scores = ', np.mean(scores))
我们观察到它在 步骤 5 处收敛:
价值迭代与策略迭代的比较
当代理假定对其在环境中的行为影响有一些先验知识时,可以使用价值迭代和策略迭代算法。这些算法假定已知马尔可夫决策过程(MDP)模型。然而,策略迭代通常更加计算效率高,因为它往往需要更少的迭代次数才能收敛。
策略梯度算法
策略梯度也是解决强化学习问题的一种方法,旨在直接建模和优化策略:
在策略梯度中,采取以下步骤:
-
代理观察环境的状态 (s)。
-
代理根据他们对状态 (s) 的本能(即策略π)采取行动 u。
-
代理移动并且环境改变;形成一个新的状态。
-
代理根据观察到的环境状态进一步采取行动。
-
在运动轨迹(τ)之后,代理根据所获得的总奖励 R(τ) 调整其本能。
策略梯度定理如下:
预期奖励的导数是策略π[θ]的对数梯度与奖励乘积的期望。
编码示例 - 策略梯度算法
在本例中,我们使用名为 CartPole 的 OpenAI 环境,目标是让连接到小车上的杆尽可能长时间保持直立。代理在每个时间步长内保持杆平衡时会获得奖励。如果杆倒下,那么这一集结束:
在任何时刻,小车和杆都处于一个状态 s。该状态由四个元素的向量表示,即杆角度、杆速度、小车位置和小车速度。代理可以选择两种可能的动作:向左移动小车或向右移动小车。
策略梯度采取小步骤,并根据与步骤相关的奖励更新策略。这样做可以训练代理,而无需在环境中为每对状态和动作映射价值。
在这个例子中,我们将应用一种称为蒙特卡洛策略梯度的技术。使用这种方法,代理将根据获得的奖励在每个 episode 结束时更新策略。
我们首先导入我们计划使用的相关库:
import gym
import numpy as np
from tqdm import tqdm, trange
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.autograd import Variable
from torch.distributions import Categorical
现在,我们定义一个前馈神经网络,包含一个隐藏层,有 128 个神经元和 0.5 的 dropout。我们使用 Adam 作为优化器,学习率为 0.02。使用 dropout 显著提高了策略的性能:
class PolicyGradient(nn.Module):
def __init__(self):
super(PolicyGradient, self).__init__()
# Define the action space and state space
self.action_space = env.action_space.n
self.state_space = env.observation_space.shape[0]
self.l1 = nn.Linear(self.state_space, 128, bias=False)
self.l2 = nn.Linear(128, self.action_space, bias=False)
self.gamma_value = gamma_value
# Episode policy and reward history
self.history_policy = Variable(torch.Tensor())
self.reward_episode = []
# Overall reward and loss history
self.history_reward = []
self.history_loss = []
def forward(self, x):
model = torch.nn.Sequential(
self.l1,
nn.Dropout(p=0.5),
nn.ReLU(),
self.l2,
nn.Softmax(dim=-1)
)
return model(x)
policy = PolicyGradient()
optimizer = optim.Adam(policy.parameters(), lr=l_rate)
现在,我们定义一个choose_action
函数。这个函数根据策略分布选择动作,使用了 PyTorch 分布包。策略返回一个数组,表示动作空间上每个可能动作的概率。在我们的例子中,可以是向左移动或向右移动,因此输出可能是[0.1, 0.9]。根据这些概率选择动作,记录历史并返回动作:
def choose_action(state):
# Run the policy model and choose an action based on the probabilities in state
state = torch.from_numpy(state).type(torch.FloatTensor)
state = policy(Variable(state))
c = Categorical(state)
action = c.sample()
if policy.history_policy.dim() != 0:
try:
policy.history_policy = torch.cat([policy.history_policy, c.log_prob(action)])
except:
policy.history_policy = (c.log_prob(action))
else:
policy.history_policy = (c.log_prob(action))
return action
要更新策略,我们从 Q 函数(动作值函数)中取样。回想一下,这是通过遵循策略 π 在状态中采取行动来预期的回报。我们可以使用每个时间步长的策略梯度来计算,其中在杆保持垂直的每个步骤都有 1 的奖励。我们使用长期奖励 (vt),这是整个 episode 期间所有未来奖励的折现总和。因此,episode 越长,当前状态-动作对的奖励越大,其中 gamma 是折现因子。
折现奖励向量表示如下:
例如,如果一个 episode 持续 4 个时间步,每步的奖励将分别是 [4.90, 3.94, 2.97, 1.99]。从这里,我们可以通过减去均值并除以标准差来缩放奖励向量。
在每个 episode 结束后,我们应用蒙特卡洛策略梯度来改进策略,如下所示:
然后,这个策略乘以奖励值,输入优化器,使用随机梯度下降更新神经网络的权重。
以下函数定义了我们如何在代码中更新策略:
def update_policy():
R = 0
rewards = []
# Discount future rewards back to the present using gamma
for r in policy.reward_episode[::-1]:
R = r + policy.gamma_value * R
rewards.insert(0,R)
# Scale rewards
rewards = torch.FloatTensor(rewards)
x = np.finfo(np.float32).eps
x = np.array(x)
x = torch.from_numpy(x)
rewards = (rewards - rewards.mean()) / (rewards.std() + x)
# Calculate the loss loss
loss = (torch.sum(torch.mul(policy.history_policy, Variable(rewards)).mul(-1), -1))
# Update the weights of the network
optimizer.zero_grad()
loss.backward()
optimizer.step()
#Save and intialize episode history counters
policy.history_loss.append(loss.data[0])
policy.history_reward.append(np.sum(policy.reward_episode))
policy.history_policy = Variable(torch.Tensor())
policy.reward_episode= []
在这里,我们定义主策略训练循环。在每个训练 episode 的每一步,选择一个动作并记录新状态和奖励。在每个 episode 结束时调用update_policy
函数,将 episode 历史传入神经网络:
def main_function(episodes):
running_total_reward = 50
for e in range(episodes):
# Reset the environment and record the starting state
state = env.reset()
done = False
for time in range(1000):
action = choose_action(state)
# Step through environment using chosen action
state, reward, done, _ = env.step(action.data.item())
# Save reward
policy.reward_episode.append(reward)
if done:
break
# Used to determine when the environment is solved.
running_total_reward = (running_total_reward * 0.99) + (time * 0.01)
update_policy()
if e % 50 == 0:
print('Episode number {}, Last length: {:5d}, Average length: {:.2f}'.format(e, time, running_total_reward))
if running_total_reward > env.spec.reward_threshold:
print("Solved! Running reward is now {} and the last episode runs to {} time steps!".format(running_total_reward, time))
break
episodes = 2000
main_function(episodes)
深度 Q 网络
深度 Q 网络 (DQNs) 结合了深度学习和强化学习,在多个不同的应用中学习,尤其是在电子游戏中。让我们考虑一个简化的游戏示例,在迷宫中有一只老鼠,目标是让老鼠尽可能多地吃奶酪。老鼠吃的奶酪越多,游戏得分就越高:
在这个例子中,RL 术语如下:
-
代理:由计算机控制的老鼠
-
状态:游戏中的当前时刻
-
动作:老鼠做出的决策(向左、向右、向上或向下移动)
-
奖励:游戏中的分数/老鼠吃掉的奶酪数量,换句话说,代理试图最大化的值
DQN 使用 Q-learning 来学习给定状态的最佳动作。它们使用卷积神经网络作为 Q-learning 函数的函数逼近器。ConvNets 使用卷积层来查找空间特征,例如老鼠当前在网格中的位置。这意味着代理只需学习数百万而不是数十亿种不同的游戏状态的 Q 值:
学习鼠迷宫游戏时 DQN 架构的示例如下:
-
当前状态(迷宫屏幕)作为输入输入到 DQN 中。
-
输入通过卷积层传递,以找出图像中的空间模式。请注意,这里没有使用池化,因为在建模电脑游戏时知道空间位置很重要。
-
卷积层的输出被馈送到全连接线性层。
-
线性层的输出给出了 DQN 在当前状态下采取行动的概率(向上、向下、向左或向右)。
DQN 损失函数
DQN 需要一种损失函数以提高得分。该函数可以数学表示如下:
是 Q 网络选择要采取的动作。目标网络是用作地面真实值的近似值。如果我们考虑这样一个情况,即 Q 网络预测在特定状态下正确行动是向左移动的概率为 80%,而目标网络建议向左移动,我们可以通过反向传播调整 Q 网络的参数,使其更有可能在该状态下预测“向左移动”。换句话说,我们通过 DQN 反向传播损失并调整 Q 网络的权重,以减少总体损失。损失方程旨在使移动的概率更接近于 100%的确定性选择。
经验回放
一个经验包括当前状态、动作、奖励和下一个状态。代理获得的每个经验都记录在经验回放内存中。从回放内存中随机抽取一个经验来训练网络。
与传统的 Q-learning 相比,经验回放具有一些关键优势。其中一个优势是,由于每个经验可能被用来多次训练 DQN 的神经网络,因此具有更高的数据效率。另一个优势是,在它学习到经验之后,下一个样本的训练是由当前参数决定的。如果我们在迷宫的例子中考虑这一点,如果下一个最佳行动是向左移动,那么训练样本将主要来自屏幕左侧。这种行为可能导致 DQN 陷入局部最小值。通过引入经验回放,用于训练 DQN 的经验来源于时间的许多不同点,从而平滑学习过程并帮助避免性能不佳。
编码示例 - DQN
在这个例子中,我们将再次考虑来自 OpenAI Gym 的CartPole-v0
环境。
首先,我们创建一个类,它将允许我们在训练 DQN 时引入经验回放。这本质上存储了智能体观察到的转换。通过采样过程,构建一个批次的转换是不相关的:
transition_type = namedtuple('transition_type',
('state', 'action', 'next_state', 'reward'))
class ExperienceReplayMemory(object):
def __init__(self, model_capacity):
self.model_capacity = model_capacity
self.environment_memory = []
self.pole_position = 0
def push(self, *args):
"""Saves a transition."""
if len(self.environment_memory) < self.model_capacity:
self.environment_memory.append(None)
self.environment_memory[self.pole_position] = transition_type(*args)
self.pole_position = (self.pole_position + 1) % self.model_capacity
def sample(self, batch_size):
return random.sample(self.environment_memory, batch_size)
def __len__(self):
return len(self.environment_memory)
定义 ConvNet 模型,其中当前和先前的屏幕补丁之间的差异被馈送进去。模型有两个输出—Q(s,left)和 Q(s,right)。网络试图预测在给定当前输入时采取行动的预期奖励/回报:
class DQNAlgorithm(nn.Module):
def __init__(self, h, w, outputs):
super(DQNAlgorithm, self).__init__()
self.conv_layer1 = nn.Conv2d(3, 8, kernel_size=5, stride=2)
self.batch_norm1 = nn.BatchNorm2d(8)
self.conv_layer2 = nn.Conv2d(8, 32, kernel_size=5, stride=2)
self.batch_norm2 = nn.BatchNorm2d(32)
self.conv_layer3 = nn.Conv2d(32, 32, kernel_size=5, stride=2)
self.batch_norm3 = nn.BatchNorm2d(32)
# The number of linear input connections depends on the number of conv2d layers
def conv2d_layer_size_out(size, kernel_size = 5, stride = 2):
return (size - (kernel_size - 1) - 1) // stride + 1
convw = conv2d_layer_size_out(conv2d_layer_size_out(conv2d_layer_size_out(w)))
convh = conv2d_layer_size_out(conv2d_layer_size_out(conv2d_layer_size_out(h)))
linear_input_size = convw * convh * 32
self.head = nn.Linear(linear_input_size, outputs)
# Determines next action during optimisation
def forward(self, x):
x = F.relu(self.batch_norm1(self.conv_layer1(x)))
x = F.relu(self.batch_norm2(self.conv_layer2(x)))
x = F.relu(self.batch_norm3(self.conv_layer3(x)))
return self.head(x.view(x.size(0), -1))
设置模型的超参数以及一些用于训练的实用程序:
BATCH_SIZE = 128
GAMMA_VALUE = 0.95
EPISODE_START = 0.9
EPISODE_END = 0.05
EPISODE_DECAY = 200
TARGET_UPDATE = 20
init_screen = get_screen()
dummy_1, dummy_2, height_screen, width_screen = init_screen.shape
number_actions = environment.action_space.n
policy_network = DQNAlgorithm(height_screen, width_screen, number_actions).to(device)
target_network = DQNAlgorithm(height_screen, width_screen, number_actions).to(device)
target_network.load_state_dict(policy_network.state_dict())
target_network.eval()
optimizer = optim.RMSprop(policy_network.parameters())
memory = ExperienceReplayMemory(1000)
steps_done = 0
def choose_action(state):
global steps_done
sample = random.random()
episode_threshold = EPISODE_END + (EPISODE_START - EPISODE_END) * \
math.exp(-1\. * steps_done / EPISODE_DECAY)
steps_done += 1
if sample > episode_threshold:
with torch.no_grad():
return policy_network(state).max(1)[1].view(1, 1)
else:
return torch.tensor([[random.randrange(number_actions)]], device=device, dtype=torch.long)
durations_per_episode = []
def plot_durations():
plt.figure(2)
plt.clf()
durations_timestep = torch.tensor(durations_per_episode, dtype=torch.float)
plt.title('Training in progress...')
plt.xlabel('Episode')
plt.ylabel('Duration')
plt.plot(durations_timestep.numpy())
if len(durations_timestep) >= 50:
mean_values = durations_per_episode.unfold(0, 100, 1).mean(1).view(-1)
mean_values = torch.cat((torch.zeros(99), mean_values))
plt.plot(mean_values.numpy())
plt.pause(0.001)
plt.show()
最后,我们有训练模型的代码。此函数执行优化的单步。首先,它对批次进行采样并将所有张量连接成一个单一张量。它计算Q(st,at)和V(st+1)=maxaQ(st+1,a),并将它们结合成一个损失。根据定义,如果s是终端状态,则设置V(s)=0。我们还使用目标网络来计算V(st+1)以提高稳定性:
def optimize_model():
if len(memory) < BATCH_SIZE:
return
transitions_memory = memory.sample(BATCH_SIZE)
batch = Transition(*zip(*transitions_memory))
计算非最终状态的掩码。之后,我们将批次元素连接起来:
not_final_mask = torch.tensor(tuple(map(lambda x: x is not None,
batch.next_state)), device=device, dtype=torch.uint8)
not_final_next_states = torch.cat([x for x in batch.next_state if x is not None])
state_b = torch.cat(batch.state)
action_b = torch.cat(batch.action)
reward_b = torch.cat(batch.reward)
计算Q(s_t, a),然后选择所采取的动作列:
state_action_values = policy_network(state_b).gather(1, action_b)
next_state_values = torch.zeros(BATCH_SIZE, device=device)
next_state_values[not_final_mask] = target_net(not_final_next_states).max(1)[0].detach()
我们计算预期的 Q 值:
expected_state_action_values = (next_state_values * GAMMA_VALUE) + reward_b
然后我们计算 Huber 损失函数:
hb_loss = F.smooth_l1_loss(state_action_values, expected_state_action_values.unsqueeze(1))
现在,我们优化模型:
optimizer.zero_grad()
hb_loss.backward()
for param in policy_network.parameters():
param.grad.data.clamp_(-1, 1)
optimizer.step()
number_episodes = 100
for i in range(number_episodes):
environment.reset()
last_screen = get_screen()
current_screen = get_screen()
current_state = current_screen - last_screen
for t in count():
# Here we both select and perform an action
action = choose_action(current_state)
_, reward, done, _ = environment.step(action.item())
reward = torch.tensor([reward], device=device)
现在,我们观察新状态:
last_screen = current_screen
current_screen = get_screen()
if not done:
next_state = current_screen - last_screen
else:
next_state = None
我们将转换存储在内存中:
memory.push(current_state, action, next_state, reward)
# Move to the next state
current_state = next_state
让我们执行优化的一步(在目标网络上):
optimize_model()
if done:
durations_per_episode.append(t + 1)
plot_durations()
break
更新目标网络;复制所有 DQN 中的权重和偏置:
if i % TARGET_UPDATE == 0:
target_network.load_state_dict(policy_network.state_dict())
print('Complete')
environment.render()
environment.close()
plt.ioff()
plt.show()
这输出一些可视化,以便了解模型在训练过程中的表现:
以下图表总结了此编码示例中模型的操作:
双重深度 Q 学习
双深度 Q 学习通常比纯深度 Q 网络表现更好。深度 Q 学习的一个常见问题是,有时代理可以学习到不切实际地高的行动价值,因为它包括对估计行动价值的最大化步骤。这倾向于偏爱过高估计的值而不是低估值。如果过高估计不均匀且不集中在我们希望更多了解的状态上,则可能会对结果策略的质量产生负面影响。
双 Q 学习的思想是减少这些过高估计。它通过将目标中的 max 操作分解为行动选择和行动评估来实现。在纯深度 Q 网络实现中,行动选择和行动评估是耦合的。它使用目标网络来选择行动,并同时估计行动的质量。
我们正在使用目标网络来选择行动,并同时估计行动的质量。双 Q 学习本质上试图将这两个过程解耦。
在双 Q 学习中,时序差分(TD)目标如下所示:
新的 TD 目标的计算可以总结为以下步骤:
-
Q 网络使用下一个状态s'来计算在状态s'中每个可能的行动a的质量Q(s',a)。
-
应用于Q(s',a)的
argmax
操作选择了属于最高质量的行动*a**(行动选择)。 -
行动的质量 Q(s',a),属于行动a**,被选为目标的计算。
双 Q 学习的过程可以如下图所示。AI 代理处于初始状态s,基于一些先前的计算,它知道该状态中可能的两个行动a[1]和a[2]的质量Q(s, a[1])和Q(s, a[2])。然后代理决定采取行动a[1]并进入状态s':
演员-评论家方法
演员-评论家方法旨在结合值和基于策略的方法的优势,同时消除它们的缺点:
演员-评论家的基本思想是将模型分为两部分:一部分用于根据状态计算行动,另一部分用于生成行动的 Q 值。
演员是一个神经网络,将状态作为输入并输出最佳行动。通过学习最优策略,它控制代理的行为方式。评论家通过计算价值函数评估行动。换句话说,演员尝试优化策略,评论家尝试优化价值。这两个模型随着时间的推移在各自的角色上得到改进,因此整体架构的学习效率高于单独使用这两种方法:
这两个模型本质上是相互竞争的。这种方法在机器学习领域越来越流行;例如,在生成对抗网络中也有这种情况。
演员的角色性质是探索性的。它经常尝试新事物并探索环境。评论者的角色是要么批评,要么赞扬演员的行动。演员接受这些反馈并相应地调整其行为。随着演员获得越来越多的反馈,它在决定采取哪些行动时变得越来越好。
就像神经网络一样,演员可以是一个函数逼近器,其任务是为给定的状态生成最佳动作。例如,这可以是一个完全连接的或卷积神经网络。评论者也是一个函数逼近器,它接收环境和演员的动作作为输入。它连接这些输入并输出动作值(Q 值)。
这两个网络分别进行训练,并使用梯度上升而不是下降来更新它们的权重,因为它旨在确定全局最大值而不是最小值。权重在每个步骤而不是在每个策略梯度末尾更新。
演员评论已被证明能够学习复杂的环境,并已在许多二维和三维电脑游戏中使用,例如超级马里奥和Doom。
编码示例 - 演员评论模型
在这里,我们将考虑一个在 PyTorch 中的编码实现示例。首先,我们定义ActorCritic
类:
HistoricalAction = namedtuple('HistoricalAction', ['log_prob', 'value'])
class ActorCritic(nn.Module):
def __init__(self):
super(ActorCritic, self).__init__()
self.linear = nn.Linear(4, 128)
self.head_action = nn.Linear(128, 2)
self.head_value = nn.Linear(128, 1)
self.historical_actions = []
self.rewards = []
def forward(self, x):
x = F.relu(self.linear(x))
scores_actions = self.head_action(x)
state_values = self.head_value(x)
return F.softmax(scores_actions, dim=-1), state_values
现在,我们初始化模型:
ac_model = ActorCritic()
optimizer = optim.Adam(ac_model.parameters(), lr=3e-2)
eps = np.finfo(np.float32).eps.item()
定义一个基于状态选择最佳动作的函数:
def choose_action(current_state):
current_state = torch.from_numpy(current_state).float()
probabilities, state_value = ac_model(current_state)
m = Categorical(probabilities)
action = m.sample()
ac_model.historical_actions.append(HistoricalAction(m.log_prob(action), state_value))
return action.item()
从这里开始,我们需要定义计算总回报并考虑损失函数的函数:
def end_episode():
R = 0
historical_actions = ac_model.historical_actions
losses_policy = []
losses_value = []
returns = []
for r in ac_model.rewards[::-1]:
R = r + gamma * R
returns.insert(0, R)
returns = torch.tensor(returns)
returns = (returns - returns.mean()) / (returns.std() + eps)
for (log_prob, value), R in zip(historical_actions, returns):
advantage = R - value.item()
losses_policy.append(-log_prob * advantage)
losses_value.append(F.smooth_l1_loss(value, torch.tensor([R])))
optimizer.zero_grad()
loss = torch.stack(losses_policy).sum() + torch.stack(losses_value).sum()
loss.backward()
optimizer.step()
del ac_model.rewards[:]
del ac_model.historical_actions[:]
最后,我们可以训练模型并查看其表现:
running_reward = 10
for i_episode in count(1):
current_state, ep_reward = environment.reset(), 0
for t in range(1, 10000):
action = choose_action(current_state)
current_state, reward, done, _ = environment.step(action)
if render:
environment.render()
ac_model.rewards.append(reward)
ep_reward += reward
if done:
break
running_reward = 0.05 * ep_reward + (1 - 0.05) * running_reward
end_episode()
if i_episode % log_interval == 0:
print('Episode number {}\tLast reward: {:.2f}\tAverage reward: {:.2f}'.format(
i_episode, ep_reward, running_reward))
if running_reward > environment.spec.reward_threshold:
print("Solved! Running reward is {} and "
"the last episode runs to {} time steps!".format(running_reward, t))
break
这将给出以下输出:
异步演员评论算法
异步优势演员评论或A3C是由谷歌的 DeepMind 提出的一种算法。该算法已被证明优于其他算法。
在 A3C 中,有多个代理实例,每个代理实例在其自己的独立环境中进行不同的初始化。每个个体代理开始采取行动,并通过强化学习过程来收集自己独特的经验。然后,这些独特的经验用于更新全局神经网络。这个全局神经网络被所有代理共享,它影响所有代理的行动,每个代理的每个新经验都提高了整体网络的速度:
名称中的优势术语是指状态的预期平均值与该状态的行动相比是否有改进的价值。优势公式如下:
A (s,a) = Q(s,a) - V(s)
实际应用
强化学习方法已被应用于解决现实世界中多种领域的问题。在这里,我们考虑了其中一些例子。
- 机器人技术: 在机器人领域应用强化学习的工作已经取得了显著进展。如今,制造设施充斥着执行各种任务的机器人,其基础是强化学习方法:
- 交通信号灯控制: 在论文基于强化学习的多智能体网络交通信号控制系统中,研究人员设计了一个交通信号灯控制器来解决拥堵问题,表现优异,超过了其他方法:
- 个性化推荐: 强化学习已经应用于新闻推荐系统中,以应对新闻快速变化的特点,用户的注意力不集中,仅凭点击率无法反映用户的留存率:
- 生成图像: 对于将强化学习与其他深度学习架构结合进行研究已经有很多成果。DeepMind 展示了使用生成模型和强化学习成功生成图像的能力:
总结
在本章中,我们首先介绍了强化学习的基础,并介绍了一些在真实场景中表现出超越人类能力的先进算法。同时,我们还展示了这些算法如何在 PyTorch 中实现。
在接下来的最后一章中,将概述本书内容,并提供如何保持与数据科学领域最新进展的技巧。
进一步阅读
请参考以下链接获取更多信息:
-
深度强化学习简要概述:
arxiv.org/pdf/1708.05866.pdf
-
使用深度强化学习玩 Atari 游戏:
www.cs.toronto.edu/~vmnih/docs/dqn.pdf
-
双 Q 学习的深度强化学习:
arxiv.org/pdf/1509.06461.pdf
-
深度强化学习中的连续控制:
arxiv.org/pdf/1509.02971.pdf
-
深度强化学习中的异步方法:
arxiv.org/pdf/1602.01783.pdf
-
软演员-评论家算法:基于最大熵深度强化学习的离策略方法:
arxiv.org/pdf/1801.01290.pdf
-
基于强化学习的多智能体网络交通信号控制系统:
web.eecs.utk.edu/~itamar/Papers/IET_ITS_2010.pdf
-
端到端训练深度视觉运动策略:
arxiv.org/pdf/1504.00702.pdf
-
DRN: 一种用于新闻推荐的深度强化学习框架:
www.personal.psu.edu/~gjz5038/paper/www2018_reinforceRec/www2018_reinforceRec.pdf
-
使用增强对抗学习合成图像程序:
arxiv.org/pdf/1804.01118.pdf
第十一章:接下来做什么?
恭喜你完成了这本书!请给自己一个大大的掌声!感谢您阅读本书,真诚希望它能帮助您在未来的道路上,无论是作为数据科学家、机器学习工程师,还是人工智能领域众多不断发展的职称之一。到现在为止,您应该已经对 PyTorch API 及其在计算机视觉、自然语言处理和强化学习等多个任务中的应用有了牢固的掌握。然而,这绝不是您开始的旅程的终点,而是一个美好未来之路的开端!
接下来做什么?
在本章中,我们将看一下下一个逻辑步骤序列,以便在书中已经取得的进展基础上继续前进。然而,首先让我们退一步,看看到目前为止学到的所有工具和技术,理解你所学到的一切如何融入更深入的学习背景中。
书籍概述
以下项目符号列表将帮助您理解书中每章的要点,并可以作为快速指南,将我们在整本书中学到的内容置于上下文中:
-
AI、神经网络和深度学习的历史。目前使用的各种深度学习框架。PyTorch 的重要性和必要性。PyTorch v1.0 的改进。GPU 与 CPU 的对比。使用 CUDA 并行化张量计算。
-
神经网络的构建模块:网络如何学习表示?我们看了 PyTorch
tensors
、tensor operations
、nn.module
、torch optim
,以及 PyTorch 定义运行时动态 DAG 的工作原理。 -
我们学习了神经网络训练中涉及的不同流程,如 PyTorch 数据集用于数据准备,数据加载器用于批量张量处理,
torch.nn
包用于创建网络架构,以及使用 PyTorch 损失函数和优化器。我们还讨论了处理过拟合的不同技术,如 dropout、l1 和 l2 正则化,以及使用批归一化。 -
我们学习了卷积神经网络(CNNs)的不同构建模块,还学习了迁移学习,帮助我们使用预训练模型。我们还看到了一些技术,如使用预先计算的特征,有助于减少训练模型所需的时间。
-
我们学习了词嵌入(word embedding)及其在文本分类问题中的应用。我们还探讨了如何使用预训练的词嵌入。我们研究了循环神经网络(RNN)及其变体,例如长短期记忆网络(LSTM),以及如何将它们应用于文本分类问题。
-
在本章中,我们研究了使用自编码器进行半监督学习来去噪数据的想法,以及使用变分自编码器生成新图像。
-
我们探讨了生成模型,并学习了如何使用 PyTorch 进行艺术风格转移,以及使用生成对抗网络(GAN)创建新的 CIFAR 图像。我们还探讨了语言建模技术,可以用于生成新文本或创建特定领域的嵌入。
-
我们探讨了现代架构,如 ResNet、Inception、DenseNet 和编码-解码架构。我们还看到了这些模型如何用于迁移学习。我们还通过组合所有这些模型构建了一个集成模型。
-
最后,我们研究了如何利用强化学习来训练模型在不确定的顺序环境中做出决策。我们研究了各种深度强化学习策略,如深度 Q 学习、基于策略的方法和演员-评论家模型。我们使用了 OpenAI gym 环境来使用深度强化学习解决著名的倒立摆问题。
阅读和实施研究论文
深度学习是一个不断发展的领域,跟上领域中的最新发展将显著影响您在所在团队以及整个领域中的贡献能力。
研究论文起初可能看起来是充满术语且难以理解的混乱文本,但是不断努力阅读和实施这些算法将极大提升您的能力。我最喜欢的论文及其对应代码的存储库之一是 paperswithcode (paperswithcode.com/sota
):
paperswithcode 网站
您应该查找关于 CV、NLP、强化学习、语音等人工智能各种任务的最新研究论文和代码。每周至少阅读一篇论文,并通过下载源代码实现代码,将帮助您跟上该领域的最新发展。
探索有趣的想法
我们在本书中学到的大部分概念构成了由深度学习驱动的现代应用的基础。在本节中,我们将看看与计算机视觉和自然语言处理(NLP)相关的不同有趣的项目。
对象检测
本书中的所有示例都有助于您检测给定图像是否为这种(猫)或那种(狗)。但是,要解决现实世界中的一些问题,您可能需要识别图像中的不同对象,例如这里展示的对象:
对象检测算法的输出
此图显示了对象检测算法的输出,该算法正在检测一些对象,如一只美丽的狗和猫。就像有现成的图像分类算法一样,有许多令人惊奇的算法可以帮助构建物体识别系统。以下是一些重要的算法和物体检测的论文列表:
-
单次多框检测器 (SSD):
arxiv.org/abs/1512.02325
-
Faster RCNN:
arxiv.org/abs/1506.01497
-
YOLO2:
arxiv.org/abs/1612.08242
图像分割
假设你正在建筑物的露台上读这本书。你看到了什么?你能勾勒出你看到的东西吗?如果你是个好艺术家,不像我,那么你可能画了几栋建筑物、树木、鸟类以及周围更多有趣的东西的轮廓。图像分割算法试图捕捉类似的东西。给定一幅图像,它们为每个像素生成预测,识别每个像素属于哪个类别。以下图像显示了图像分割算法识别的内容:
图像分割算法的输出
以下是您可能想探索的一些重要图像分割算法:
-
R-CNN:
arxiv.org/abs/1311.2524
-
Fast R-CNN:
arxiv.org/abs/1504.08083
-
Faster R-CNN:
arxiv.org/abs/1506.01497
-
Mask R-CNN:
arxiv.org/abs/1703.06870
PyTorch 中的 OpenNMT
开源神经机器翻译 (OpenNMT) (github.com/OpenNMT/OpenNMT-py
) 项目有助于构建许多由编码器-解码器架构驱动的应用程序。您可以构建的一些应用包括翻译系统、文本摘要和图像到文本的转换。
Allen NLP
Allen NLP 是基于 PyTorch 的开源项目,可以更轻松地完成许多 NLP 任务。有一个演示页面 (demo.allennlp.org/machinecomprehension
),你应该看看以了解可以用 Allen NLP 构建什么。
fast.ai – 再次让神经网络变得不那么酷
我最喜欢了解深度学习的地方之一,也是灵感的伟大来源,是由两位来自fast.ai
的出色导师 Jeremy Howard 和 Rachel Thomas 组织的一个 MOOC,他们的唯一动机是使深度学习对所有人都可访问。为了他们课程的一个新版本,他们在 PyTorch 之上建立了一个令人难以置信的框架 (github.com/fastai/fastai
),使构建应用程序变得更加简单和快速。如果你还没有开始他们的课程,我强烈建议你开始。探索fast.ai
框架的构建将为您提供许多强大技术的深入了解。
开放神经网络交换
开放神经网络交换 (ONNX) (onnx.ai/
) 是通向开放生态系统的第一步,使您能够在项目发展中选择合适的工具。ONNX 提供了深度学习模型的开源格式。它定义了可扩展的计算图模型以及内置运算符和标准数据类型的定义。Caffe2、PyTorch、Microsoft Cognitive Toolkit、Apache MXNet 等工具正在开发 ONNX 支持。这个项目可以帮助产品化 PyTorch 模型。
如何保持自己的更新
社交媒体平台,特别是 Twitter,帮助您在领域内保持更新。有许多人可以关注。如果你不确定从哪里开始,我建议你关注 Jeremy Howard (twitter.com/jeremyphoward
) 和他可能关注的任何有趣的人。通过这样做,你将促使 Twitter 的推荐系统为你工作。另一个需要关注的重要 Twitter 账号是 PyTorch (twitter.com/PyTorch
)。PyTorch 背后的出色人才有一些很棒的内容在分享。如果你正在寻找研究论文,那么请查看www.arxiv-sanity.com/
,那里有许多聪明的研究人员发布他们的论文。关于 PyTorch 的更多学习资源包括其教程 (pytorch.org/tutorials/
),其源代码 (github.com/pytorch/pytorch
) 和其文档 (pytorch.org/docs/0.3.0/
)。
概要
深度学习和 PyTorch 还有很多值得探索的内容。PyTorch 是一个相对较新的框架,在撰写本章节时已有 3 年历史。还有很多东西可以学习和探索,祝你学习愉快。一切顺利!