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/err…,选择您的书籍,点击勘误提交表单链接,并填写详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法副本,我们将不胜感激,如果您能提供给我们具体位置或网站名称的信息。请联系我们,发送至 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 内部运行情况的不同技术。
在下一章中,我们将学习如何使用循环神经网络处理序列数据。