首页 > 其他分享 >PyTorch-1-x-深度学习指南第二版-全-

PyTorch-1-x-深度学习指南第二版-全-

时间:2024-07-23 15:01:34浏览次数:10  
标签:学习指南 loss nn self PyTorch 深度 validation 我们 size

PyTorch 1.x 深度学习指南第二版(全)

原文:zh.annas-archive.org/md5/3913e248efb5ce909089bb46b2125c26

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

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,并注册以直接通过电子邮件获取文件。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com

  2. 选择“支持”选项卡。

  3. 点击“代码下载”。

  4. 在搜索框中输入书名并按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压软件解压缩文件夹:

  • 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 函数,如 splitlist,将文本转换为标记。”

代码块设置如下:

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)。”

警告或重要提示看起来像这样。

小贴士和技巧看起来像这样。

联系我们

我们非常欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在消息主题中提及书名,并发送电子邮件至 [email protected]

勘误:尽管我们已尽一切努力确保内容的准确性,但错误偶尔也会发生。如果您在本书中发现错误,请向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并填写详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,我们将不胜感激,如果您能提供给我们具体位置或网站名称的信息。请联系我们,发送至 [email protected],并附上材料的链接。

如果您有兴趣成为作者:如果您对某个您专业的主题感兴趣,并且您有意参与撰写或贡献书籍,请访问 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 包可用,您可以使用pipconda构建它。或者,您可以从源代码构建。本书推荐使用 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_featuresout_featuresin_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 神经网络。我们现在可以预测学生的录取状态了吗?不,还不行。首先,我们需要学习可以帮助我们预测录取状态的具体权重:

我们之前定义的神经网络首先随机生成权重。因此,如果我们直接将数据传递给神经网络,我们将得到毫无意义的预测结果。

在神经网络中两个在训练过程中起作用的重要组件是CriterionOptimizer。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 非线性激活

  • 使用深度学习进行图像分类

深入了解神经网络的构建模块

正如我们在前一章中学到的,训练深度学习算法需要以下步骤:

  1. 构建数据管道

  2. 构建网络架构

  3. 使用损失函数评估架构

  4. 使用优化算法优化网络架构权重

在前一章中,网络由使用 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))

上述代码在trainvalid目录内基于类别(猫和狗)创建了文件夹。

我们可以用以下代码对索引进行洗牌:

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的实用类,可以用来加载图像及其关联的标签,当数据以前述格式呈现时。通常的做法是执行以下预处理步骤:

  1. 将所有图像调整为相同的大小。大多数深度学习架构期望图像具有相同的大小。

  2. 使用数据集的均值和标准差进行归一化。

  3. 将图像数据集转换为 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类,它接受数据集并返回图像批次,抽象了批处理中的许多复杂性,例如使用多个工作线程进行变换应用。以下代码将先前的trainvalid数据集转换为数据加载器:

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 架构是一组层,即Conv2dBatchNorm2dMaxPool2d,以特定的方式拼接在一起。所有这些算法都会接受一个名为pretrained的参数。当pretrainedTrue时,算法的权重已经调整到预测 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 数据集构建一个简单而强大的图像分类器。一旦我们建立了这个分类器,我们将逐步分解网络的每个组件。我们将图像分类器的构建分解为以下步骤:

  1. 获取数据

  2. 创建验证数据集

  3. 从头开始构建我们的 CNN 模型

  4. 训练和验证模型

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,它决定了卷积核的大小。一些常用的卷积核大小包括1357。卷积核大小越大,滤波器能够覆盖的区域就越大,因此在早期层中常见到应用大小为79的滤波器对输入数据进行处理。

池化

在卷积层后添加池化层是一种常见的做法,因为它们可以减小特征图的大小,并优化卷积层的输出结果。

池化提供了两个不同的功能:一个是减小要处理的数据大小,另一个是强制算法不要专注于图像中位置的微小变化。例如,人脸检测算法应该能够在图片中检测到人脸,而不管人脸在照片中的位置如何。

让我们看看 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库中提供了一组经过训练的模型。当参数pretrainedTrue时,大多数模型都会接受一个称为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)
  )
)

模型摘要包含两个顺序模型:featuresclassifiersfeatures 顺序模型包含我们将要冻结的层。

冻结层

让我们冻结特征模型的所有层,其中包含卷积块。冻结这些卷积块的权重将阻止这些层的权重更新。由于模型的权重经过训练以识别许多重要特征,我们的算法将能够从第一次迭代开始做同样的事情。使用模型的权重,这些权重最初是为不同用例而训练的能力,称为迁移学习

现在,让我们看看如何冻结层的权重或参数:

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 函数如splitlist来将文本转换为标记。

为了演示分词在字符和单词上的工作方式,让我们考虑一部电影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 函数接收一个单词并将其添加到 word2indexindex2word 中,并增加词汇表的长度,前提是该单词是唯一的。

  • 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 的库,它使得许多过程(如下载、文本向量化和批处理)更加简单。训练情感分类器将涉及以下步骤:

  1. 下载 IMDb 数据并进行文本标记化

  2. 构建词汇表

  3. 生成向量批次

  4. 使用嵌入创建网络模型

  5. 训练模型

我们将在接下来的几节中详细介绍这些步骤。

下载 IMDb 数据并进行文本标记化

对于与计算机视觉相关的应用程序,我们使用了 torchvision 库,它为我们提供了许多实用函数,帮助构建计算机视觉应用程序。同样,还有一个名为 torchtext 的库,它专为与 PyTorch 一起工作而构建,通过提供不同的文本数据加载器和抽象来简化与自然语言处理NLP)相关的许多活动。在撰写本文时,torchtext 并不随标准的 PyTorch 安装一起提供,需要单独安装。您可以在机器的命令行中运行以下代码来安装 torchtext

pip install torchtext

一旦安装完成,我们将能够使用它。 torchtext 下载提供了两个重要的模块,称为 torchtext.datatorchtext.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_sizedevice(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 模型中使用它们。这类似于在计算机视觉应用中使用的迁移学习。通常,使用预训练嵌入涉及以下步骤:

  1. 下载嵌入

  2. 加载模型中的嵌入

  3. 冻结嵌入层权重

让我们详细探讨每个步骤的实现方式。

下载嵌入

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,)

参数向量的值表示要使用的嵌入类。namedim 参数确定可以使用哪些嵌入。我们可以轻松地从 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 不要改变嵌入层的权重是一个两步过程:

  1. requires_grad 属性设置为 False,这告诉 PyTorch 不需要这些权重的梯度。

  2. 删除嵌入层参数传递给优化器。如果不执行此步骤,则优化器会抛出错误,因为它期望所有参数都有梯度。

下面的代码演示了冻结嵌入层权重以及指导优化器不使用这些参数是多么简单:

model.embedding.weight.requires_grad = False
optimizer = optim.SGD([ param for param in model.parameters() if param.requires_grad == True],lr=0.001)

我们通常将所有模型参数传递给优化器,但在之前的代码中,我们传递了 requires_gradTrue 的参数。

我们可以使用这段代码训练模型,并且应该达到类似的准确性。所有这些模型架构都未能充分利用文本的顺序特性。在下一节中,我们探讨了两种流行的技术,即 RNN 和 Conv1D,它们利用了数据的顺序特性。

递归神经网络

RNNs 是我们能够应对分类、序列数据标记、生成文本序列(例如 SwiftKey 键盘应用程序,预测下一个单词)、以及将一种序列转换为另一种序列(比如从法语翻译成英语)等应用中最强大的模型之一。大多数模型架构,如前馈神经网络,并不利用数据的序列性质。例如,我们需要数据来呈现每个示例的特征,以向量形式表示,比如表示句子、段落或文档的所有标记。前馈网络仅设计为一次性查看所有特征并将其映射到输出。让我们看一个文本示例,说明为什么文本的顺序或序列性质很重要。I had cleaned my carI 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 网络,用于构建情感分类器。像往常一样,我们将遵循以下步骤来创建分类器:

  1. 数据准备

  2. 创建批次

  3. 创建网络

  4. 模型训练

我们将在接下来的章节详细讨论这些步骤。

数据准备

我们使用相同的 torchtext 库来下载、分词化和构建 IMDB 数据集的词汇表。在创建 Field 对象时,我们将 batch_first 参数保留为 False。RNN 需要数据的形式为 sequence_lengthbatch_sizefeatures. 用于准备数据集的步骤如下:

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

  • 双向编码器表示来自 TransformersBERT

  • 生成预训练变压器 2GPT-2

语言模型的嵌入

2018 年 2 月,M. Peters 等人发表了深度上下文化的单词表示论文(arxiv.org/abs/1802.05365),介绍了 ELMo。本质上,它证明了语言模型嵌入可以作为目标模型中的特征,如下图所示:

ELMo 使用双向语言模型来学习单词和上下文。正向和反向传递的内部状态在每个单词处被串联起来,以产生一个中间向量。正是模型的双向性质使其获得关于句子中下一个单词和之前单词的信息。

双向编码器表示来自 Transformers

谷歌在 2018 年 11 月发布的后续论文(arxiv.org/pdf/1810.04805.pdf)中提出了双向编码器表示来自 TransformersBERT),它融入了一个注意力机制,学习词与文本之间的上下文关系:

与 ELMo 不同,文本输入是按顺序(从左到右或从右到左)读取的,但是 BERT 会一次性读取整个单词序列。本质上,BERT 是一个经过训练的 Transformer 编码器堆栈。

生成预训练变压器 2

在撰写本文时,OpenAI 的 GPT-2 是设计用于提高生成文本的逼真度和连贯性的最先进的语言模型之一。它是在 2019 年 2 月的论文Language Models are Unsupervised Multi-task Learnersd4mucfpksywv.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 中编译一个自编码器模型:

  1. 首先,导入相关的库:
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
  1. 现在,定义模型参数:
number_epochs = 10
batch_size = 128
learning_rate = 1e-4
  1. 然后,初始化一个函数来转换 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)
  1. 定义自编码器类,用于提供数据并初始化模型:
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)
  1. 定义一个函数,它将在每个 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
  1. 现在在每个 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 向量来实现。在图像的背景下,对图像进行下采样迫使自编码器学习其压缩版本。

编码示例 – 卷积自编码器

在这个例子中,我们将展示如何编译一个卷积自编码器:

  1. 与以前一样,您从 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)
  1. 从这里开始,启动卷积自编码器模型:
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)
  1. 最后,在每个 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 中的采样操作不可微,不能从重构误差中传播梯度。以下图表进一步解释了这一点:

为了克服这一限制,可以使用重参数化技巧。重参数化技巧从单位正态分布中采样ε,将其平移至潜在属性的均值

标签:学习指南,loss,nn,self,PyTorch,深度,validation,我们,size
From: https://www.cnblogs.com/apachecn/p/18318411

相关文章

  • Python-深度学习算法实用指南-全-
    Python深度学习算法实用指南(全)原文:zh.annas-archive.org/md5/844a6ce45a119d3197c33a6b5db2d7b1译者:飞龙协议:CCBY-NC-SA4.0前言深度学习是人工智能领域最受欢迎的领域之一,允许你开发复杂程度各异的多层模型。本书介绍了从基础到高级的流行深度学习算法,并展示了如何使用......
  • Go-深度学习实用指南-全-
    Go深度学习实用指南(全)原文:zh.annas-archive.org/md5/cea3750df3b2566d662a1ec564d1211d译者:飞龙协议:CCBY-NC-SA4.0序言Go是由Google设计的开源编程语言,旨在高效处理大型项目。它使得构建可靠、简单和高效的软件变得简单直接。本书立即进入了在Go语言中实现深度神......
  • PyTorch-现代计算机视觉第二版-二-
    PyTorch现代计算机视觉第二版(二)原文:zh.annas-archive.org/md5/355d709877e6e04dc1540c8ccd0b447d译者:飞龙协议:CCBY-NC-SA4.0第十七章:稳定扩散的应用在前一章中,我们学习了扩散模型的工作原理、稳定扩散的架构以及扩散器-这个库。虽然我们已经了解了生成图像(无条件和条......
  • PyTorch-1-x-强化学习秘籍-全-
    PyTorch1.x强化学习秘籍(全)原文:zh.annas-archive.org/md5/863e6116b9dfbed5ea6521a90f2b5732译者:飞龙协议:CCBY-NC-SA4.0前言强化学习兴起的原因在于它通过学习在环境中采取最优行动来最大化累积奖励的概念,从而革新了自动化。PyTorch1.x强化学习菜谱向您介绍了重要的......
  • PyTorch-1-x-模型训练加速指南-全-
    PyTorch1.x模型训练加速指南(全)原文:zh.annas-archive.org/md5/787ca80dbbc0168b14234d14375188ba译者:飞龙协议:CCBY-NC-SA4.0前言你好!我是一名专注于高性能计算(HPC)的系统分析师和学术教授。是的,你没看错!我不是数据科学家。那么,你可能会想知道我为什么决定写一本关于机器......
  • FIFO最小深度计算
    1、读写没有空闲周期。(fA>fB)    例如,fA=80MHz;fB=50MHz;BurstLength=120;读写之间没有空闲周期,连续读写一个突发长度。解法:        写一个数据需要的时间=1/80MHz=12.5ns;        写一个突发需要的时间=120*12.5ns=1500ns;   ......
  • PyTorch的模型定义方法
    文章目录1、简介2、导包3、设置属性4、构建数据集5、训练函数5.1、初始准备5.2、训练过程5.3、绘制图像6、运行效果7、完整代码......
  • 如何在 PyTorch 中使用类权重和焦点损失来处理多类分类的不平衡数据集
    我正在研究语言任务的多类分类(4类),并使用BERT模型进行分类任务。我正在关注这篇博文NLP的迁移学习:微调BERT用于文本分类我的BERT微调模型返回nn.LogSoftmax(dim=1)我的数据非常不平衡,所以我使用了|||计算类别的权重并使用损失中的权重。......
  • Pytorch从基础数据中学习时出现巨大损失
    我正在尝试制作一个非常基本的机器学习应用程序,基本上我希望模型能够根据我提供的虚拟数据尝试预测明年的每个月,但我遭受了巨大的损失Loss:5206342.5000并且预测值与我的预期相差如此之远Predictedvaluesforeachmonthofnextyear:[-0.043424129486083984,......
  • PyTorch LSTM 模型上的 CrossEntropyLoss,每个时间步一分类
    我正在尝试创建一个LSTM模型来检测时间序列数据中的异常情况。它需要5个输入并产生1个布尔输出(如果检测到异常则为True/False)。异常模式通常连续3-4个时间步长。与大多数LSTM示例不同,它们预测未来数据或对整个数据序列进行分类,我尝试在每个时间步输出True/False检......