PyTorch-人工智能基础知识-全-

PyTorch 人工智能基础知识(全)

原文:PyTorch Artificial Intelligence Fundamentals

协议:CC BY-NC-SA 4.0

零、前言

人工智能AI)继续流行并破坏了广泛的领域,但这是一个复杂而艰巨的话题。 在本书中,您将掌握构建深度学习应用的方法,以及如何使用 PyTorch 进行研究和解决实际问题。

本书从张量处理的基础开始,采用基于秘籍的方法,然后介绍了卷积神经网络CNN)和 PyTorch 中的循环神经网络RNN)。 熟悉这些基本网络后,您将使用深度学习来构建医学图像分类器。 接下来,您将使用 TensorBoard 进行可视化。 您还将深入研究生成对抗网络GAN)和深度强化学习DRL),然后最终将模型大规模部署到生产中。 您将发现针对机器学习,深度学习和强化学习中常见问题的解决方案。 您将学习如何在计算机视觉,自然语言处理NLP)中实现 AI 任务并解决实际问题。 其他实际领域。

到本书结尾,您将拥有使用 PyTorch 框架的 AI 中最重要且应用最广泛的技术的基础。

这本书是给谁的

这本 PyTorch 书籍适用于刚刚起步的 AI 工程师以及正在寻找指南以帮助他们有效解决 AI 问题的机器学习工程师,数据科学家和深度学习爱好者。 期望具备 Python 编程语言的工作知识和对机器学习的基本了解。

本书涵盖的内容

第 1 章,“使用 PyTorch 处理张量”介绍 PyTorch 及其安装,然后继续使用 PyTorch 处理张量。

第 2 章,“处理神经网络”全面介绍了开始和训练全连接神经网络的所有要求,从而对基本神经网络的所有组件进行了详尽的解释。 :层,前馈网络,反向传播,损失函数,梯度,权重更新以及使用 CPU/GPU。

第 3 章,“用于计算机视觉的卷积神经网络”首先介绍了用于更高级任务的一类神经网络,即卷积神经网络。 在这里,我们将与 PyTorch 一起探索 TorchVision,训练 CNN 模型,并使用 TensorBoard 可视化其进度。 我们还将介绍与卷积网络构建块相关的各种任务。 卷积神经网络(CNN 或 ConvNet)是一类 DNN,最常用于分析图像。

第 4 章,“用于 NLP 的循环神经网络”探索循环神经网络,并研究 RNN 中的各种修改以及最佳实践。

第 5 章,“迁移学习和 TensorBoard”显示了如何使用经过训练的 ResNet-50 模型训练图像分类器以区分正常和肺炎的胸部 X 射线。 我们将替换分类器,并使用两个输出单元来代表正常和肺炎类别。

第 6 章,“探索生成对抗网络”探索生成对抗网络,以及如何实现 PyTorch 的组件并训练端到端网络。 我们将探索 DCGAN,并通过渐进式 GAN 网络进一步改善 DCGAN 的局限性。

第 7 章,“深度强化学习”可帮助您通过各种秘籍来了解深度 RL。 本章是一系列秘籍和任务,您将在其中利用所需的能力和架构来转变为深度强化学习专家。

第 8 章,“PyTorch 中的生产 AI 模型”,着眼于以两种方式生产 PyTorch 应用。 首先,生成已经训练过的模型,其次,对大型数据集进行分布式训练。 最后,我们将研究各种框架之间的可移植性。

充分利用这本书

需要具备 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 存储库中进行更新。

我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!

下载彩色图像

我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载

使用约定

本书中使用了许多文本约定。

CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“对于 Linux,我们将使用以下pip管理器。”

代码块设置如下:

a = np.ones((2, 3))
a

当我们希望引起您对代码块特定部分的注意时,相关行或项目以粗体显示:

b.shape
torch.Size([2, 3])

任何命令行输入或输出的编写方式如下:

pip3 install https://download.pytorch.org/whl/cu90/torch-1.1.0-cp36-cp36m-win_amd64.whl

粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“标量是单个独立值。”

警告或重要提示如下所示。

提示和技巧如下所示。

栏目

在本书中,您会发现几个经常出现的标题(“准备工作”,“操作步骤”,“工作原理”,“另见”。

要给出有关如何完成秘籍的明确说明,请按以下说明使用这些部分:

准备

本节告诉您在秘籍中会有什么期望,并介绍如何设置秘籍所需的任何软件或任何初步设置。

操作步骤

本节包含遵循秘籍所需的步骤。

工作原理

本节通常包括对上一节中发生的情况的详细说明。

更多

本节包含有关秘籍的其他信息,以使您对秘籍有更多的了解。

另见

本节提供了指向该秘籍其他有用信息的有用链接。

一、使用 PyTorch 使用张量

深度学习是机器学习父领域中的一个子领域,它是受大脑工作启发的一类算法的研究和应用。 给定足够的数据并通过它进行迭代,这些算法可以近似于描述数据的任何函数,并且正确地称为通用函数近似器。 那么 PyTorch 进入这个生态系统的位置是什么?

PyTorch 是 Python 中的一个开源深度学习框架,它使我们能够从研究问题开始,提出原型解决方案,并在开发此解决方案的过程中一直进行到创建分布式计算集群为止。 它使您从研究到生产都可以覆盖。 PyTorch 改编自 Torch,后者是一个科学计算框架,广泛支持机器学习算法,使用 Lua 编写,可为您提供强大的功能(使用 GPU)。 那为什么要用 PyTorch?

PyTorch 与 Python 深度集成,具有命令式风格,使用类似 Python 的语法,并且在 Eager 模式下易于使用且灵活。 它的学习曲线很浅,可以让您专注于功能而不是框架的样板和语法。 Python 命令的纯命令执行将失去很多优化机会,因此,随着即时JIT)编译器的引入,PyTorch 允许过渡到 C++ 运行时的图环境中用于速度,功能和优化的模式。 它得到了来自不同领域的专业人员的大力社区支持,并且与库打交道。 它具有与框架互操作性的本地开放神经网络交换ONNX)支持。 它是分布式的,可扩展到生产环境,与 TensorBoard 集成,并具有出色的文档和 API,您可以轻松编写针对 CPU 和 GPU 的自定义扩展。 我们将在接下来的章节中探索这些以及更多内容。

在本章中,我们将介绍以下秘籍:

  • 安装 PyTorch
  • 在 PyTorch 中创建张量
  • 互操作的 NumPy 桥接
  • 梯度和无梯度
  • 在 PyTorch 中查看张量

技术要求

要完成本章,您需要安装 Python3。 您还将需要任何现代机器,但本章不需要使用支持 GPU 的设备。 如果要利用 GPU 功能,可以使用支持 NVIDIA CUDA 的 GPU。

安装 PyTorch

我们将在本节中安装 PyTorch。

NumPy 是本章必不可少的库,在您安装 PyTorch 时,NumPy 会自动为您安装它的依赖项。 这意味着我们无需显式安装 NumPy。

您可以将 PyTorch 与其他包管理器一起安装,例如 Conda,如这个页面中所述。

要为 Python3 CPU 安装 PyTorch,我们可以使用以下命令:

  • 对于 Linux,我们将使用以下pip管理器:
pip3 install torch==1.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
  • 对于 Windows,我们将使用以下pip管理器:
pip3 install torch==1.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
  • 对于 MacOS,我们将使用以下pip管理器:
pip3 install torch

要为启用 Python3 CUDA 的 GPU 版本安装 PyTorch,可以使用以下命令:

  • 对于 Linux,我们将使用以下pip管理器:
pip3 install torch
  • 对于 Windows,我们将使用以下pip管理器:
pip3 install https://download.pytorch.org/whl/cu90/torch-1.1.0-cp36-cp36m-win_amd64.whl

MacOS 二进制不支持 CUDA,因此如果需要 CUDA,则应从源代码安装它。 您也可以使用其他包管理器进行安装,甚至可以从源代码进行构建。 有关其他包管理器和 Python 版本,请访问这个页面

您可以通过转到 Python 终端并键入以下命令来快速验证安装是否正常:

import torch
import numpy

如果这些导入效果很好,那就很好了!

在 PyTorch 中创建张量

首先让我们了解张量是什么。 标量是单个独立值,一维值数组称为向量,二维值数组称为矩阵,并且任何大于 2D 的值简称为张量。 张量是一个广义的术语,包含标量,向量和矩阵。

标量是 0 阶张量,向量是 1 阶张量,矩阵是 2 阶张量。

以下是各种张量:

  • 标量:这是一个零阶张量。 标量的示例是x₁
  • 向量:这是一阶张量; 以下是向量的示例:

  • 矩阵:这是一个二阶张量。 以下是矩阵的示例:

  • 张量:这些高于二阶张量,如以下示例所示:

这样,我们将继续介绍如何使用张量的秘籍。

操作步骤

在 PyTorch 中有多种创建张量的方法。 我们将在本节中介绍其中一些:

  • 我们可以创建一个具有所有张量的张量,如下所示:
  1. 让我们从导入库开始:
import torch
  1. 我们将使用ones()方法:
torch.ones((2,3))

这将返回一个张量,该张量包含一个张量,并且具有默认的float数据类型,如下所示:

tensor([[1., 1., 1.],
 [1., 1., 1.]])
  • 现在,我们将创建一个仅由整数组成的张量:
  1. 我们将执行与上一秘籍完全相同的操作,但是将添加数据类型(dtype)作为参数:
torch.ones((2,3), dtype=torch.int8)
  1. 这将返回仅由整数组成的张量:
tensor([[1, 1, 1],
 [1, 1, 1]], dtype=torch.int8)
  • 接下来,我们将创建一个仅由整数零组成的张量:
  1. 我们将执行与之前完全相同的操作,但是使用zeros()方法:
torch.zeros((2,3), dtype=torch.int8)
  1. 这将返回仅由整数零组成的张量:
tensor([[0, 0, 0],
 [0, 0, 0]], dtype=torch.int8)
  • 现在,我们将创建一个填充有特定值的张量:
  1. 我们将使用full()方法并传递所需的填充值以及形状:
torch.full((2, 3), 3.141592)
  1. 这将返回具有给定值的张量:
tensor([[3.1416, 3.1416, 3.1416],
 [3.1416, 3.1416, 3.1416]])

请注意,这些值是四舍五入的。

  • 现在,我们将创建一个空张量:
  1. 我们将为此使用empty()方法:
torch.empty((2,3))
  1. 这将返回一个填充有未初始化数据的张量,该张量每次都随每台机器而变化:
tensor([[2.5620e-01, 4.5773e-41, 2.5620e-01],
 [4.5773e-41, 4.4842e-44, 0.0000e+00]])
  • 接下来,我们将根据均匀分布创建张量:
  1. 我们将使用rand()方法:
torch.rand((2,3))
  1. 这将根据[0, 1]的均匀分布绘制具有随机值的张量:
tensor([[0.6714, 0.0930, 0.4395],
 [0.5943, 0.6582, 0.6573]])
  • 我们将创建一个均值为 0 和方差为 1 的张量:
  1. 我们将使用randn()方法:
torch.randn((2,3))
  1. 这将从正态分布(也称为标准正态分布)绘制具有平均值为 0 和方差为 1 的随机值的张量:
tensor([[ 0.3470, -0.4741, 1.2870],
 [ 0.8544, 0.9717, -0.2017]])
  • 接下来,我们将根据给定的值范围创建张量
  1. 我们将使用rand_int()方法,传入下限,上限和形状:
torch.randint(10, 100, (2,3))
  1. 这将返回介于 10 和 100 之间的张量,类似于以下内容:
tensor([[63, 93, 68],
 [93, 58, 29]])
  • 我们现在将根据现有数据创建张量:
  1. 我们将为此使用tensor类:
torch.tensor([[1, 2 ,3], [4, 5, 6]])

这将创建数据的副本并创建张量。 如果要避免进行复制,可以使用torch.as_tensor([[1, 2 ,3], [4, 5, 6]])

  1. 这将返回与数据具有相同数据类型的张量,在这种情况下为整数张量:
tensor([[1, 2, 3],
 [4, 5, 6]])

另外,请注意,如果数据值之一是浮点数,则所有这些值都将转换为浮点数。 但是,如果值之一是字符串,则将引发错误。

  • 接下来,我们将使用另一个张量的属性创建一个张量:
  1. 我们首先为此创建一个参考张量:
a = torch.tensor([[1, 2 ,3], [4, 5, 6]])
  1. 让我们看看张量a的数据类型:
a.dtype
torch.int64
  1. 现在让我们看一下张量的形状:
a.shape
torch.Size([2, 3])
  1. 数据类型和形状符合我们的期望,所以现在让我们创建一个张量b,使其与a的属性匹配,并为此使用torch.*_like格式:
b = torch.ones_like(a)
b

结果为以下输出:

tensor([[1, 1, 1],
 [1, 1, 1]])
  1. 让我们看看张量b的数据类型:
b.dtype
torch.int64
  1. 我们还要看一下张量b的形状:
b.shape
torch.Size([2, 3])
  • 接下来,我们将创建一个与另一个张量类型相似但大小不同的张量:
  1. 我们将使用与上一步相同的张量a,并为此使用torch.new_*格式:
a.new_full((2,2), 3.)
  1. 这将返回以下输出:
tensor([[3, 3],
 [3, 3]])

这些是在 PyTorch 中创建张量的不同方法。

工作原理

在本秘籍中,我们介绍了从各种数据源创建张量的各种方法。 在我们开始探索使用 PyTorch 进行深度学习的概念及其原理之前,必须了解一些最常用的功能来处理数据的基本单位,张量。 我们可以使用torch.tensor()方法创建具有各种值和形状的张量。 我们甚至可以从均匀分布或标准正态分布中得出张量,这对于初始化神经网络以获得最佳表现和训练时间至关重要,所有这些张量都具有默认的torch.FloatTensor 数据类型,并使用dtype更新数据类型。 参数。

.ones()方法创建一个包含给定形状的张量的张量,.zeros()用所有零填充该张量,full()方法将给定形状的张量填充。 .empty()方法创建一个空张量,.rand()[0, 1]的均匀分布中绘制一个具有随机值的张量,.randn()从正态分布中绘制均值为 0 和方差 1 的一个具有随机值的张量 ,也称为标准正态分布。

rand_int()方法从给定范围绘制随机整数,并以给定形状创建张量。 我们可以创建具有其他张量形状的张量,也可以具有所有张量的张量,但是可以使用ones_like()方法创建其他张量的形状和数据类型。 我们可以使用torch.new_*格式创建一个张量,其类型与另一个张量相似,但大小不同。

我们还可以从现有源中获取数据并将其转换为张量,并且存在先进的张量创建技术,这些技术可以减少内存占用并使用现有张量的形状和/或张量的数据类型。

更多

您可以使用shape属性或size()方法找到张量的形状,并使用张量的dtype属性找到数据类型。 您也可以使用torch.numel()来获取张量中的元素总数。

另见

要了解更多信息,请在这个页面上阅读 PyTorch 的官方文档以了解张量创建选项。

探索 NumPy 桥接

NumPy 是 Python 中科学计算的基本包。 它是一个 Python 库,提供多维数组对象和各种派生对象。 除此之外,NumPy 用作通用多维数据的有效容器。 NumPy 允许与各种数据库进行无缝,快速的集成。

NumPy 是标准的 Python 库,用于处理数字数据。 Python 中许多著名的 ML/DS 库,例如 pandas(用于从许多来源读取数据的库)和 scikit-learn(用于读取和写入图像的最重要的 ML 库之一)都使用 NumPy 引擎盖。 例如,在处理表格数据,使用pandas库加载表格数据并将numpy数组移出数据框时,您将处理很多numpy。 读取图像,许多现有库都具有内置的 API,可以将它们读取为numpy数组; 并将numpy数组转换为图像以及文本和其他形式的数据。 同样,它们都使用scikit-learn(机器学习库)支持numpy数组。 如您所见,在numpy数组和 PyTorch 张量之间建立桥梁非常重要。

操作步骤

让我们从导入numpy开始:

  1. 我们将从创建一个numpy数组开始; 为此,让我们导入numpy
import numpy as np

2.我们将创建一个仅由ones组成的numpy数组:

a = np.ones((2, 3))
a

结果为以下输出:

array([[1., 1., 1.],
 [1., 1., 1.]])

3.现在,我们将其转换为 PyTorch 张量:

b = torch.from_numpy(a)
b

结果为以下输出:

tensor([[1., 1., 1.],
 [1., 1., 1.]], dtype=torch.float64)

4.现在,我们将张量转换为numpy数组:

b.numpy()

结果为以下输出:

array([[1., 1., 1.],
 [1., 1., 1.]]) 

有了这个秘籍,我们现在可以在 NumPy 和 Torch 张量之间来回移动了。

工作原理

我们首先导入numpy以创建numpy数组。 然后,我们使用np.ones()创建了仅包含一个数组的numpy数组,并使用from_numpy()方法将其转换为 PyTorch 张量。 然后,我们使用.numpy()方法将张量转换为numpy数组。

在 PyTorch 张量和 NumPy 之间切换非常容易; 实际上,只有两种方法可以实现。 这使得可以获取预测的张量并从 NumPy 转换为图像(使用支持 NumPy 到图像转换的库),并类似地从 NumPy 返回张量。

更多

基础内存在 NumPy 数组和 PyTorch 张量之间共享,因此任何一个更改都会影响另一个。

让我们看一下如何在以下代码块中呈现它:

>>a
array([[1., 1., 1.],
 [1., 1., 1.]]) 
>>b = torch.from_numpy(a)
>>b
tensor([[1., 1., 1.],
 [1., 1., 1.]], dtype=torch.float64) 
>>a*=2
>>a
array([[2., 2., 2.],
       [2., 2., 2.]]) 
>>b
tensor([[2., 2., 2.],
 [2., 2., 2.]], dtype=torch.float64)

我们可以看到numpy的变化也反映在张量中。

另见

要了解更多信息,请单击 NyPy 桥的 PyTorch 官方文档链接

探索梯度

让我们简要介绍一下什么是梯度。 为此,我们需要首先了解什么是梯度下降。 在机器学习问题中,我们提供一个输入和期望的输出对,并要求我们的模型概括给定输入和输出对之间的关​​系。 但是有时模型会得知其预测会与期望的输出相去甚远(这种差异称为损失)。 那么什么是梯度下降?

梯度下降是一种优化算法,用于通过在梯度的负值所定义的最陡下降方向上反复移动来使函数最小化。 我们在训练模型时会使用它,以使损失最小化。 它用于查找使成本或损失函数最小化的函数参数值(机器学习中的系数或权重)。

那么什么是梯度呢? 梯度度量的是当输入改变很小的时候给定函数的输出有多少变化,这与微积分中的导数概念相同。 梯度会计算所有权重相对于误差变化的变化。 梯度是函数的斜率。 较高的坡度意味着坡度更陡,并且模型可以更快地学习。 梯度指向最陡的倾斜方向。 PyTorch 中的Autograd模块在 PyTorch 中执行所有梯度计算。 它是自动差异化的核心 Torch 包。 它使用基于磁带的系统进行自动微分。 在前进阶段,Autograd磁带将记住它执行的所有操作,而在倒退阶段,它将重放它们。

操作步骤

让我们从创建张量开始。

  1. 与我们到目前为止创建的张量不同,我们将添加一个新键,让 PyTorch 知道它需要对以下张量执行梯度计算:
x = torch.full((2,3), 4, requires_grad=True)
x

结果为以下输出:

tensor([[4., 4., 4.],
 [4., 4., 4.]], requires_grad=True)

2.让我们创建另一个张量y,它是从张量a中派生的; 我们将看到这个新张量的输出差异,因为它附有一个梯度函数:

y = 2*x+3
y

结果为以下输出:

tensor([[11., 11., 11.],
 [11., 11., 11.]], grad_fn=<AddBackward0>) 

3.让我们从原始的x开始进一步探索 PyTorch 中的梯度:

x

结果为以下输出:

tensor([[4., 4., 4.],
 [4., 4., 4.]], requires_grad=True)

4.然后,我们将定义y,它比上一个示例稍微复杂一些:

y = (2 * x * 2 + 3)
y

结果为以下输出:

tensor([[35., 35., 35.],
 [35., 35., 35.]], grad_fn=<AddBackward0>)

5.接下来,由于y是张量,我们将针对y上的x计算梯度,并且我们要针对该张量计算梯度。 为此,我们将传递x的形状,该形状与y相同:

y.backward(torch.ones_like(x))

6.现在,让我们使用grad 属性查看x的梯度值:

x.grad

结果为以下输出:

tensor([[16., 16., 16.],
 [16., 16., 16.]])

7.继续本节的非梯度部分,我们可以通过执行以下步骤来关闭代码中特定位置的梯度计算:首先,如果要重新使用张量,请首先使用张量上的requires_grad_()方法 x

>> x.requires_grad

True

>> x.requires_grad_(False) # turning of gradient
>> x.requires_grad

False

8.我们可以从x开始使用.no_grad()方法关闭对梯度计算的跟踪:

>> x = torch.full((2,3), 4,requires_grad=True)
>> x

tensor([[4., 4., 4.],
 [4., 4., 4.]], requires_grad=True)

>> x.requires_grad

True

>> with torch.no_grad():
..         print((x**5+3).requires_grad)

False

有了这个,我们探索了Autograd包的一些功能。

工作原理

我们可以看到,Autograd 可以跟踪操作。 当我们从xy=2*x+3创建张量y时,我们会看到一个梯度函数grad_fn被附加到张量上。

我们首先创建一种新的张量,将require_grad设置为True,然后创建一个张量y,使得y = 2x^2 + 3并发现y具有一个附加到它的不同的梯度函数。 我们还研究了使用requires_grad_(),最后使用了no_grad()

PyTorch 有一个名为autograd的包,可以对张量上的所有操作执行所有跟踪和自动微分。 这是一个按运行定义的框架,这意味着您的反向传播是由代码的运行方式定义的,并且每次迭代都可以不同。 我们利用torch.Tensor类的require_grad属性来确定梯度计算的状态,并在调用.backward()方法后自动计算其.grad属性中的所有梯度和张量的梯度。

我们可以禁用代码之间的梯度计算,也可以暂时禁用对张量的跟踪以进行梯度计算,从而提高计算速度。 禁用计算主要在评估期间使用。

更多

您可以使用torch.set_grad_enabled()方法启用和禁用梯度计算,以及使用detach()方法来将来跟踪计算。 使用grad_fn属性查看附加到张量的梯度函数。

另见

要了解更多信息,可以在这里这里查看官方文档。

在 PyTorch 中查看张量

在使用张量和处理神经网络时,我们经常需要遍历和重新排列张量中的数据,以使张量的尺寸适合架构的需求。 在本节中,我们将探讨 PyTorch 中常见的重排和重塑技术。

在本秘籍中,我们将学习如何使张量看起来像我们想要的样子。

操作步骤

让我们看一下如何改变张量的形状:

  1. 首先,我们将创建一个张量a
>>a = torch.Tensor([1, 2, 3, 4])

2.然后,我们将使用reshape()方法:

>>torch.reshape(a, (2, 2))

结果为以下输出:

tensor([[1., 2.],
 [3., 4.]])

3.接下来,我们将研究resize_()方法:

>>a = torch.Tensor([1, 2, 3, 4, 5, 6])
>>a.shape
torch.Size([6])
>>a.resize_((2, 2))

结果为以下输出:

tensor([[1., 2.],
 [3., 4.]])

4.最常见的方法是view()

>>a = torch.Tensor([1, 2, 3, 4, 5, 6])
>>a.view((2, 3))

结果为以下输出:

tensor([[1., 2., 3.],
 [4., 5., 6.]])
  1. 使用view()方法,您可以选择不提及其中一个尺寸,然后排列其余尺寸,PyTorch 将按以下方式计算缺失尺寸:
>>a.view((2, -1))

结果为以下输出:

tensor([[1., 2., 3.],
 [4., 5., 6.]])

这些是重塑张量的不同方法。

工作原理

在前面的秘籍中,我们基于网络架构操纵了张量以改变其形状,研究了三种不同的方法,每种方法都适用于不同的用例:

  • .reshape()方法:.reshape(a, b)返回一个新张量,该张量具有与原始张量(a, b)相同的数据,因为它会将数据复制到内存的另一部分; .reshape()可以在连续和非连续张量上运行,并且可以返回原始张量的副本或视图。
  • .resize()方法:.resize_(a, b)返回相同的张量,而不创建具有新给定形状的副本。 但是我们应该记住,如果新形状产生的元素少于原始张量,那么它将不会引发任何错误,并且某些元素将从张量中删除,但不会从内存中删除。 如果新形状产生的元素多于原始张量,则新元素将在内存中未初始化而不会引发任何错误。
  • .view()方法:.view(a, b)将返回一个新的张量,其张量与权重为(a, b)的数据相同; .view()只能在连续的张量上运行,并返回与输入相同的存储。

更多

您可以使用另一个张量的尺寸,并使给定的张量类似于该张量的尺寸,而不会影响其中任何一个的实际尺寸。

看下面的代码块:

>>a = torch.Tensor([[1, 2, 3],
                     [4, 5, 6]])
>>a

tensor([[1., 2., 3.],
 [4., 5., 6.]])

>>b = torch.Tensor([4,5,6,7,8,9])
>>b
tensor([4., 5., 6., 7., 8., 9.])
>>b.view_as(a)

tensor([[4., 5., 6.],
 [7., 8., 9.]])

由此可见,张量b呈张量a的形状。

另见

有关更多信息,您可以在这里这里查看文档。

二、与神经网络协作

深度学习是一类机器学习算法,旨在粗略地模拟大脑中的神经元。 神经元从周围神经元的多个输入中获取一个输入并将其求和,如果总和超过某个阈值,则神经元将触发。 每个神经元之间有一个间隙,称为突触。 神经递质化学物质在这些突触中传递信号,这些化学物质的数量和类型将决定神经元输入的强度。 生物神经网络的功能可以通过人工神经网络使用权重,偏差(偏差定义为权重乘以 1 的恒定输入)和激活函数来复制。

以下是神经单元的示意图:

神经网络所看到的只是数字集,它试图识别数据中的模式。 通过训练,神经网络学会识别输入中的模式。 但是,某些特定的架构在应用于特定类别的问题时比其他结构具有更好的表现。 一个简单的神经网络架构由三种层组成:输入层,输出层和隐藏层。 当隐藏层不止一个时,它称为深度神经网络

以下是深度神经网络的表示:

在前面的图中,圆圈代表神经元,或用深度学习的术语代表作为计算单元的节点。 边缘代表节点之间的连接,并保持两个节点之间的连接权重(突触强度)。

在本章中,以下秘籍将使我们开始使用神经网络:

  • 定义神经网络类
  • 创建一个全连接网络
  • 定义损失函数
  • 实现优化器
  • 实现丢弃
  • 实现函数式 API

技术要求

在本章中,我们将开始处理图像数据并学习全连接神经网络如何工作。 PyTorch 有一个名为torchvision的补充库,我们将在开始秘籍之前进行安装。

您可以对torchvision使用以下pip安装命令:

pip install torchvision

对于其他安装方法,您可以访问这个页面。 上一章“使用 PyTorch 张量”的其余依赖关系保持不变。

定义神经网络类

在本秘籍中,我们将从了解torchvision的一些重要函数开始,这些函数使它能够处理图像数据并进行处理。 然后,我们将通过定义一个类来定义神经网络的基本架构,并查看可用于此的模块和方法。 在本秘籍中,我们将专注于全连接神经网络类。 它的属性是各个层,其目的是对各种类型的衣服进行分类。

我们将使用 Fashion–MNIST 数据集。 这是 Zalando 文章图片的数据集,包括 6 万个示例的训练集和 10,000 个示例的测试集。 我们将拍摄一张尺寸为28 x 28的灰度图像,并将其转换为 784 的向量。

以下是来自数据集的示例:

现在,我们将研究定义网络所需采取的步骤。

操作步骤

让我们定义我们的网络:

1.我们将从设置torchtorchvision导入开始:

>>import torch
>>from torch import nn
>>from torchvision import datasets, transforms

2.接下来,我们将定义用于图像数据预处理的转换:

>>transform = transforms.Compose([transforms.ToTensor(),
                  transforms.Normalize((0.5,), (0.5,)),
               ])

3.让我们定义batch_size,将我们的数据集划分为多个小块,以供输入模型:

>>batch_size = 64

4.接下来,我们将从torchvision中提取数据集并应用转换并创建批量。 为此,我们将首先创建一个训练数据集:

>>trainset = datasets.FashionMNIST('~/.pytorch/F_MNIST_data/', download=True, train=True, transform=transform)
>>trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)

5.现在,让我们创建testset

>>testset = datasets.FashionMNIST('~/.pytorch/F_MNIST_data/', download=True, train=False, transform=transform)
>>testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=True)

6.现在我们的主要任务是定义神经网络类,该类必须是nn.Module的子类:

>>class FashionNetwork(nn.Module):

7.接下来,我们为该类定义init方法:

>>def __init__(self):
      super().__init__()

8.我们需要在init中为我们的模型定义层。 第一个隐藏层如下所示:

>>self.hidden1 = nn.Linear(784, 256)

9.现在,我们将定义第二个隐藏层:

>>self.hidden2 = nn.Linear(256, 128)

10.然后,我们将定义输出层:

>>self.output = nn.Linear(128, 10)

11.我们将为最后一层定义 softmax 激活:

>>self.softmax = nn.Softmax(dim=1)

12.最后,我们将在内层中定义激活函数:

>>self.activation = nn.ReLU()

通过这些步骤,我们已经完成了网络单元。

工作原理

在本秘籍中,我们开始使用torchvisiontorchvision中有工具来支持与视觉有关的任务。 有一个名为transforms的模块可以帮助完成许多图像预处理任务。 对于我们正在处理的特殊情况,一个由28 x 28灰度像素组成的图像,我们首先需要从图像中读取并使用transforms.ToTensor()变换将其转换为张量。 然后,我们分别将像素值的平均值和标准差设为 0.5 和 0.5,以便模型更易于训练; 为此,我们使用transforms.Normalize((0.5,),(0.5,))。 我们将所有转换与transform.Compose()结合在一起。

准备好转换后,我们定义了合适的批量大小。 较高的批量大小意味着该模型具有较少的训练步骤并且学习速度更快,而较高的批量大小会导致对内存的高要求。

TorchVision 的datasets模块附带了许多受欢迎的数据集; 如果机器上没有它,它将为您下载,传递转换并将数据转换为所需的格式以供模型训练。 在我们的案例中,数据集带有训练和测试集,并相应地加载它们。 我们使用torch.utils.data.DataLoader将处理后的数据分批加载,并进行其他操作,例如打乱和加载到正确的设备(CPU 或 GPU)。

我们可以用任何名称定义模型类,但是重要的是它是nn.Module的子类并具有super().__init__(),该类为模型提供了许多有用的方法和属性,并保留了架构的知识。

我们使用nn.Linear()通过输入和输出尺寸来定义全连接层。 我们将 softmax 层用于最后一层输出,因为有 10 个输出类。 我们在输出层之前的层中使用 ReLU 激活来学习数据中的非线性。 hidden1层采用 784 个输入单元,并给出 256 个输出单元。 hidden2短语输出 128 个单元,输出层有 10 个输出单元,代表 10 个输出类别。 softmax 层将激活转换为概率,以便沿维度 1 加 1。

更多

我们可以使用另一种方法来使用nn.Sequential()定义模型并传递所需的层,而无需定义类。 还有其他一些可应用于输入图像的转换,我们将在后续章节中进行探讨。

另见

您可以在这个页面上查看有关转换的更多详细信息,还可以在这个页面上了解有关定义模型类的更多信息。

创建一个全连接网络

在本秘籍中,我们将扩展在先前秘籍“定义神经网络类”中定义的类。 在“定义神经网络类”秘籍中,我们仅创建了所需架构的组件; 现在我们将把所有这些部分捆绑在一起,以建立一个明智的网络。 我们各层的进度将从 784 个单元增加到 256 个,然后是 128 个,最后是 10 个单元的输出层。

在本秘籍中,我们将使用类的构造器中定义的组件来研究网络架构。 然后,我们将完成网络类定义并创建其对象。

操作步骤

我们将继续上一节中的类定义,并在其上进行扩展:

1.让我们从类中的forward()方法开始,传入输入:

>>def forward(self, x):

2.现在,将输入移动到具有 256 个节点的第一个隐藏层:

>>x = self.hidden1(x)

3.接下来,我们通过激活函数传递第一个隐藏层的输出,在本例中为 ReLU:

>>x = self.activation(x)

4.我们将对第二层(具有 128 个节点)重复相同的操作,并将其传递给 ReLU:

>>x = self.hidden2(x)
>>x = self.activation(x)

5.现在,我们传递最后一个输出层,其中包含 10 个输出类:

>>x = self.output(x)

6.然后,我们将使用softmax函数推送输出:

>>output = self.softmax(x)

7.最后,我们返回输出张量:

>>return output

8.然后,我们将创建网络对象:

>>model = FashionNetwork()

9.让我们快速看一下我们的模型:

>>print(model)
>FashionNetwork(
       (hidden1): Linear(in_features=784, out_features=256, bias=True)
       (hidden2): Linear(in_features=256, out_features=128, bias=True)
       (output): Linear(in_features=128, out_features=10, bias=True)
       (softmax): Softmax()
       (activation): ReLU()
 )

现在,我们已经为 Fashion-MNIST 数据集完成了神经网络模型。

工作原理

在秘籍中,通过建立前向网络来完成网络,其中我们将构造器中定义的网络组件捆绑在一起。 用nn.Module定义的网络需要定义forward()方法。 它采用输入张量,并按照正向方法中定义的操作顺序,将其通过网络类中__init__()方法中定义的网络组件。

当传递输入时,将引用模型对象的名称自动调用 forward 方法。 nn.Module自动创建将在正向方法中使用的权重和偏差张量。 线性单元本身定义了线性函数,例如xW + B; 要具有非线性函数,我们需要插入非线性激活函数,在这里我们使用最流行的激活函数之一 ReLU,尽管您可以在 PyTorch 中使用其他可用的激活函数。

我们的输入层有 784 个单元(从28 x 28像素开始),第一层具有 ReLU 激活的 256 个单元,然后具有 ReLU 激活的 128 个单元,最后有 softmax 激活的 10 个单元。 我们通过 softmax 压缩最终层输出的原因是因为我们希望有 1 个输出类的概率高于所有其他类,并且输出概率之和应等于 1。softmax 函数的参数dim=1是为了确保在输出的各列中采用 softmax。 然后,我们使用模型类创建一个对象,并使用print(model)打印该类的详细信息。

更多

我们可以在不使用nn.Sequential模块定义网络类的情况下定义网络架构,并且尽管__init__中的序列无关紧要,但重要的是要确保forward方法中的操作顺序正确排序。 。 您可以使用nn.Tanh进行 tanh 激活。 您可以使用model.hidden.weightmodel.hidden.bias从模型对象访问权重和偏差张量。

另见

您可以在这个页面上查看nn.Modulenn.Sequential的官方文档。

定义损失函数

机器学习模型在接受训练时,可能会在预测输出和实际输出之间存在一些偏差,这种差异称为模型的误差。 让我们计算该误差的函数称为损失函数误差函数。 此函数提供了一种度量标准,用于评估所有可能的解决方案并选择最优化的模型。 损失函数必须能够将模型的所有属性减少到一个单一的数字,以便该损失函数值的改善代表更好的模型。

在本秘籍中,我们将使用 PyTorch 中可用的损失函数为我们的时装数据集定义损失函数。

操作步骤

让我们定义损失函数:

  1. 首先,我们将从网络构造器中的__init__方法开始,将现有的网络架构修改为softmax而不是softmax的输出日志:
>>self.log_softmax = nn.LogSoftmax()

2.接下来,我们将对神经网络的forward方法进行相同的更改:

>>output = self.log_softmax(x)

3.现在,我们的新类如下所示:

>>class FashionNetwork(nn.Module):
      def __init__(self):
          super().__init__()
          self.hidden1 = nn.Linear(784, 256)
          self.hidden2 = nn.Linear(256, 128)
          self.output = nn.Linear(128, 10)
          self.log_softmax = nn.LogSoftmax()
          self.activation = nn.ReLU()
      def forward(self, x):
          x = self.hidden1(x)
          x = self.activation(x)
          x = self.hidden2(x)
          x = self.activation(x)
          x = self.output(x)
          output = self.log_softmax(x)
          return output

4.我们定义模型对象如下:

>>model = FashionNetwork()
>>model
>>FashionNetwork(
     (hidden1): Linear(in_features=784, out_features=256, bias=True)
     (hidden2): Linear(in_features=256, out_features=128, bias=True)
     (output): Linear(in_features=128, out_features=10, bias=True)
     (log_softmax): LogSoftmax()
     (activation): ReLU()
 )

5.现在,我们将定义损失函数; 我们将为此使用负对数似然损失:

>criterion = nn.NLLLoss()

现在我们已经准备好损失函数。

工作原理

在此秘籍中,我们用对数 softmax 替换了 softmax,以便随后可以使用概率对数而不是概率的对数,这具有很好的理论解释。 这样做有多种原因,包括改进的数值表现和梯度优化。 在训练可能具有计算挑战性和昂贵成本的模型时,这些优势非常重要。 此外,当它没有预测正确的类别时,它具有很高的惩罚作用。

因此,在处理对数 softmax 时,我们会使用负对数似然率,因为 softmax 不兼容。 在n类数之间的分类中很有用。 该对数将确保我们不会处理 0 到 1 之间的非常小的值,而负值将确保小于 1 的概率的对数非零。 我们的目标是减少这种负的对数损失误差函数。 在 PyTorch 中,损失函数称为标准,因此我们将损失函数命名为criterion

更多

我们可以提供一个可选参数weight,它必须是一维张量,该张量为每个输出类分配权重以处理不平衡的训练集。

另见

您可以在这个页面上查看更多损失函数的官方文档。

实现优化器

在本秘籍中,我们将学习优化器。 在先前的秘籍“定义损失函数”中,我们谈到了误差和误差函数,并了解到,为了获得一个好的模型,我们需要最小化计算出的误差。 反向传播是神经网络从误差中学习的一种方法。 误差用于修改权重,以使误差最小化。 优化函数负责修改权重以减少误差。 优化函数计算相对于权重的误差的偏导数。 导数显示正斜率的方向,因此我们需要反转梯度的方向。 优化器函数将模型参数和损失函数结合在一起,以迭代方式修改模型参数以减少模型误差。 可以考虑将优化器摆在模型权重的基础上,根据模型与实际输出的预测差异来获得最佳模型,而损失函数则通过指示优化器是对还是错来充当指导。

学习率是优化器的超参数,它控制权重的更新量。 学习速度确保了权重不会大量更新,从而使算法根本无法收敛,并且误差越来越大。 然而,与此同时,权重的更新不应太低,以至于永远需要花费成本函数/误差函数的最小值。

下面显示了学习率的影响:

在本秘籍中,我们将学习如何在 PyTorch 中使用优化器函数,以及一些常见的优化函数以及如何处理学习率。

操作步骤

在本节中,我们从上一节中保留代码的位置开始,在定义条件的那一点:

1.我们将从导入optim模块开始:

>>from torch import optim

2.接下来,我们将创建一个optimizer对象。 我们将使用Adam优化器并传递模型参数:

>>optimizer = optim.Adam(model.parameters())

3.要检查优化器的默认设置,可以执行以下操作:

>>optimizer.defaults
>>{'lr': 0.001,
 'betas': (0.9, 0.999),
 'eps': 1e-08,
 'weight_decay': 0,
 'amsgrad': False}

4.您还可以将学习率添加为附加参数:

>>optimizer = optim.Adam(model.parameters(), lr=3e-3)

5.现在,我们将开始训练我们的模型,从周期数开始:

>>epoch = 10

6.然后,我们将开始循环:

>>for _ in range(epoch):

7.我们将running_loss初始化为0

>>running_loss = 0

8.我们将在训练图像加载器中遍历每个图像,这在本章前面的秘籍“定义神经网络类”中定义:

>>for image, label in trainloader:

9.然后,我们将梯度重置为零:

>>optimizer.zero_grad()

10.接下来,我们将重塑图像:

>>image = image.view(image.shape[0],-1)

11.然后,我们从模型中获得预测:

>>pred = model(image)

12.然后我们计算损失/误差:

>>loss = criterion(pred, label)

13.然后,对损失调用.backward()方法:

>>loss.backward()

14.然后,在优化器上调用.step()方法:

>>optimizer.step()

15.然后附加运行损失:

>>running_loss += loss.item()

16.最后,我们将在每个周期之后打印损失:

>>else:
    >>print(f'Training loss: {running_loss/len(trainloader):.4f}')

以下是示例输出:

Training loss: 0.4978
Training loss: 0.3851
Training loss: 0.3498
Training loss: 0.3278
Training loss: 0.3098
Training loss: 0.2980
Training loss: 0.2871
Training loss: 0.2798
Training loss: 0.2717
Training loss: 0.2596

现在我们已经完成了训练。

工作原理

在本秘籍中,我们首先使用Adam优化器定义优化器,然后为优化器设置学习率,并查看默认参数。 我们设置一个周期10,并为每个周期开始迭代,在每次迭代中将running_loss设置为 0,并在该周期内对每个图像进行迭代(模型看到数据集的次数)。 我们首先使用.zero_grad()方法清除梯度。 PyTorch 在每次反向传播时都会累积梯度,这在某些情况下很有用,因此将其导入以将梯度归零,以正确更新模型参数。

接下来,我们通过将每批 64 幅图像(每幅图像由28 x 28像素组成)展平到 784 来重塑图像,从而将张量形状从64 x 28 x 28更改为64 x 784,因为我们的模型期望这种输入形状。 接下来,我们将此输入发送到模型,并从模型中获得该批量的输出预测,然后将其传递给损失函数,也称为criterion; 在那里,它评估了预测类与实际类之间的差异。

loss.backward()函数计算了梯度(即,误差相对于权重的偏导数),我们调用了optimizer.step()函数来更新模型的权重,以适应评估的误差。 .item()方法从单个元素张量中拉出标量,因此使用loss.item()从批量中获得error的标量值,将其累加到所有批量的损失中,最后在周期末尾打印损失。

更多

我们可以使用称为closure的回调函数作为.step(closure)的参数来计算损失并通过传入函数作为参数来更新权重。 您还可以探索 PyTorch 提供的其他优化器函数,例如 Adadelta,Adagrad,SGD 等。

另见

您可以在这个页面上了解有关优化程序的更多信息。

实现丢弃

在本秘籍中,我们将研究实现丢弃。 在训练神经网络模型或一般任何机器学习模型时,我们可能会遇到的一种较常见的现象是过拟合。 当模型学习提供给训练的数据而不是在求解空间上进行泛化时,就会发生过拟合,也就是说,模型学习的是训练数据的细微细节和噪声,而不是掌握全局,因此在效果上表现不佳。 新数据。 正则化是防止模型过拟合的过程。

使用丢弃是神经网络中最流行的正则化技术之一,在这种技术中,训练时会关闭随机选择的神经元,也就是说,神经元的作用会暂时从正向传播中移除,而反向传播不会影响权重 ,因此没有一个神经元或神经元子集能获得模型的所有决定力; 相反,所有神经元都被迫为预测做出积极贡献。

丢弃可以直观地理解为创建大量集成模型,学习在一个模型的大定义下捕获各种特征。

在本秘籍中,我们将研究如何在模型定义中添加缺失,以通过防止过拟合来改善整体模型表现。 应当记住,丢弃仅在训练时才适用; 但是,在测试和实际预测期间,我们希望所有神经元都做出贡献。

操作步骤

在本节中,我们将学习如何为我们的初始模型类FashionNetwork添加丢弃:

1.我们将从初始模型定义开始:

>>class FashionNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden1 = nn.Linear(784, 256)
        self.hidden2 = nn.Linear(256, 128)
        self.output = nn.Linear(128, 10)
        self.log_softmax = nn.LogSoftmax()
        self.activation = nn.ReLU()
    def forward(self, x):
        x = self.hidden1(x)
        x = self.activation(x)
        x = self.hidden2(x)
        x = self.activation(x)
        x = self.output(x)
        output = self.log_softmax(x)
        return output

2.然后,我们将为模型__init__添加一个丢弃:

>>self.drop = nn.Dropout(p=0.25)

我们更新的__init__()如下所示:

>>def __init__(self):
        super().__init__()
        self.hidden1 = nn.Linear(784, 256)
        self.hidden2 = nn.Linear(256, 128)
        self.output = nn.Linear(128, 10)
        self.log_softmax = nn.LogSoftmax()
        self.activation = nn.ReLU()
        self.drop = nn.Dropout(p=0.25)

3.现在,我们将在forward()方法中添加丢弃:

>>def forward(self, x):
        x = self.hidden1(x)
        x = self.activation(x)
        x = self.drop(x)
        x = self.hidden2(x)
        x = self.activation(x)
        x = self.drop(x)
        x = self.output(x)
        output = self.log_softmax(x)
        return output

现在,我们有了一个带有丢弃的网络。

工作原理

在此秘籍中,我们更改了__init__()方法,以 0.25 的丢弃率添加了该丢弃层,这意味着将应用该丢弃层中 25% 的神经元将被随机关闭。 然后,我们编辑forward函数,将其应用于其中具有 256 个单元的第一个隐藏层,然后对第二个层(具有 128 个单元)应用该滤除。 在完成激活函数之后,我们在两个层中都应用了激活。 我们必须牢记,必须仅在隐藏层上应用丢弃,以防止我们丢失输入数据和丢失输出。

更多

我们可以通过调用model.eval()禁用退出,并使用model.train().启用退出

另见

您可以在这个页面上了解有关丢弃的更多信息。

实现函数式 API

在本秘籍中,我们将探索 PyTorch 中的函数式 API。 这样做将使我们能够编写更简洁的网络架构和组件。 我们将研究函数式 API,并使用函数式 API 定义模型或模型的一部分。

操作步骤

在以下步骤中,我们使用现有的神经网络类定义,然后使用函数式 API 重写它:

1.我们将首先进行导入:

>>import torch.nn.functional as F

2.然后,我们用F.relu()F.log_softmax()定义我们的FashionNetwork类:

>>class FashionNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden1 = nn.Linear(784,256)
        self.hidden2 = nn.Linear(256,128)
        self.output = nn.Linear(128,10)

    def forward(self,x):
        x = F.relu(self.hidden1(x))
        x = F.relu(self.hidden2(x))
        x = F.log_softmax(self.output(x))
        return x

我们使用函数式 API 重新定义了模型

工作原理

在此秘籍中,我们定义了与以前完全相同的网络,但是用function.relufunction.log_softmax代替了激活函数和对数 softmax,这使我们的代码看起来更加简洁明了。

更多

您可以通过使用functional.linear()functional.dropout()来控制丢弃,从而对线性层使用函数式 API,但是必须注意传递模型状态以指示其处于训练还是评估/预测模式。

另见

您可以通过这个页面了解更多有关函数式 API 的信息。

三、用于计算机视觉的卷积神经网络

在本章中,我们将学习卷积神经网络CNN)。 这是与前几章讨论的神经网络不同的一类。 CNN 在计算机视觉领域已经取得了巨大的成功,随着我们对它们的了解越来越多,我们将能够理解其中的原因。

CNN 是一种特殊的网络,可以将图像作为张量接收。 彩色图像由红色,绿色和蓝色三个颜色通道组成,称为 RGB。 我们将这些二维矩阵通道堆叠起来以形成彩色图像; 每个通道的值变化会产生不同的颜色。 CNN 将图像作为三个独立的堆叠颜色层,一个层放在另一个层上。

图像从附近的一个设置像素中获得其含义,但是单个像素不能保存有关整个图像的太多信息。 在也称为密集层的全连接神经网络中,一层中的每个节点都连接到下一层中的每个其他节点。 CNN 利用像素之间的空间结构来减少两层之间的连接数,从而显着提高训练速度,同时减少模型参数。

这是显示全连接网络的图像:

将上一张图像与下一张图像进行比较,后者显示了一个卷积网络:

CNN 使用过滤器从输入图像中拾取特征; 具有足够数量的过滤器的 CNN 可以检测图像中的各种特征。 随着我们越来越向后一层移动,这些过滤器在检测复杂特征方面变得越来越复杂。 卷积网络使用这些过滤器并逐一映射它们以创建特征出现的映射。

在本章中,我们将介绍以下秘籍:

  • 探索卷积
  • 探索池化
  • 探索转换
  • 执行数据扩充
  • 加载图像数据
  • 定义 CNN 架构
  • 训练图像分类器

技术要求

在本章中,您将需要在上一章中安装的 TorchVision。 您最好在支持 GPU 的计算机上运行这些秘籍中的代码。

探索卷积

卷积是 CNN 中的一个组成部分。 它们被定义为 CNN 中的一层。 在卷积层中,我们将过滤器矩阵从左到右,从上到下在整个图像矩阵上滑动,然后取过滤器的点积,此斑块将过滤器的尺寸跨过图像通道。 如果两个矩阵在相同位置具有较高的值,则点积的输出将较高,反之亦然。 点积的输出是标量值,该标量值标识图像中的像素模式和由过滤器表示的像素模式之间的相关性。 不同的过滤器会以不同的复杂度从图像中检测不同的特征。

我们需要了解 CNN 的另外两个关键元素,如下所示:

  • 跨步:这是在图像的下一个小块上使用过滤器应用卷积网络之前,我们水平和垂直移动的像素数。
  • 填充:这是我们在卷积时应用于图像边缘的策略,具体取决于我们是在卷积后要保持张量的尺寸不变还是仅在过滤器适合的情况下对输入图像应用卷积。 如果要保持尺寸不变,则需要对边缘进行零填充,以使原始尺寸在卷积后与输出匹配。 这称为相同填充。 但是,如果我们不想保留原始尺寸,则会将过滤器无法完全容纳的位置截断,这称为有效填充

这是这两个填充的示意图:

下图显示了有效填充的示例:

在本秘籍中,我们将学习如何在 PyTorch 中使用卷积神经网络。

操作步骤

在此秘籍中,我们将探讨卷积:

  1. 首先,我们将导入所需的割炬modules
        >>import torch
        >>import torch.nn as nn
  1. 接下来,我们将 2D 卷积应用于图像:
        >>nn.Conv2d(3, 16, 3)

这将创建以下卷积层:

Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1))
  1. 然后,我们在图像的边缘添加所需大小的填充:
        >>nn.Conv2d(3, 16, 3, padding=1)

这将创建以下卷积层:

Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  1. 然后,我们可以使用以下代码创建一个非平方核(过滤器):
        >>nn.Conv2d(3, 16, (3,4), padding=1)

这将创建以下卷积层:

Conv2d(3, 16, kernel_size=(3, 4), stride=(1, 1), padding=(1, 1))
  1. 然后,我们可以使用以下代码将步幅添加到卷积中:
        >>nn.Conv2d(3, 16, 3, stride=2)

这将创建以下卷积层:

Conv2d(3, 16, kernel_size=(3, 3), stride=(2, 2))
  1. 我们在水平和垂直方向上的步幅和填充量可能不相等:
>>nn.Conv2d(3, 16, (3,4), stride=(3,3), padding=(1,2))

这将创建以下卷积层:

Conv2d(3, 16, kernel_size=(3, 4), stride=(3, 3), padding=(1, 2))

通过此秘籍,我们学习了如何在 PyTorch 中使用卷积。

工作原理

在本秘籍中,我们研究了创建 2D 卷积的多种方法,其中第一个参数是给定输入图像中的通道数,对于彩色图像,通道数将为3,对于灰度图像将为1。 第二个参数是输出通道的数量,换句话说,就是我们要从给定层获得的过滤器的数量。 第三个参数是核大小(即核大小),或者是要使用过滤器卷积的图像的补丁大小。

然后,我们创建了一个Con2d对象,并将输入传递到 2D 卷积层以获取输出。 使用nn.Conv2d(3, 16, 3),我们创建了一个卷积层,该卷积层接受 3 个通道的输入并输出 16 个通道。 该层的大小为3 x 3的正方形核,其高度和宽度的默认跨度为 1。 我们可以使用padding参数添加填充,该参数可以具有整数或元组值。 在这里,整数值将为高度和宽度创建相同的填充,而元组值将为高度和宽度创建不同的填充-这对于核大小和跨度都是正确的。

更多

通过将padding参数设置为0(默认设置),可以有效填充。 您还可以通过更改padding_mode参数将零填充更改为圆形填充。 您可以使用bias布尔参数(默认为True)添加或删除偏差。

另见

您可以在这个页面了解有关 PyTorch 卷积的其他参数。

探索池化

现在,我们进入 CNN 的下一个关键层-池化层。 到目前为止,我们一直在处理图像而不改变帧的空间尺寸(考虑相同的填充); 相反,我们一直在增加通道/过滤器的数量。 池化层用于减小输入的空间尺寸,并保留其深度。 当我们从 CNN 的初始层移到后面的层时,我们希望在图像中识别出比实际逐个像素信息更多的概念意义,因此我们想从输入和抛出中识别并保留剩下的关键信息。 池化层可以帮助我们做到这一点。

这是最大池化的示意图:

这是使用池化层的主要原因:

  • 减少计算数量:通过减少输入的空间尺寸而不会损失过滤器,我们可以获得更好的计算表现,因此我们减少了训练所需的时间以及计算资源。
  • 防止过拟合:随着空间尺寸的减小,我们减少了模型具有的参数数量,从而降低了模型的复杂性并有助于我们更好地概括。
  • 位置不变性:这使 CNN 可以捕获图像中的特征,而不管特征在给定图像中的位置。 假设我们正在尝试建立一个分类器来检测芒果。 芒果位于图像的中心,左上角,右下角还是图像中的任何位置都没关系-需要对其进行检测。 池化层可以帮助我们。

池的类型很多,例如最大池,平均池,和池等。 但是,最大池化是最受欢迎的。 以与处理卷积层相同的方式,我们将定义一个窗口并在该窗口中应用所需的池化操作。 我们将根据层的跨度水平和垂直地滑动窗口。

操作步骤

在此秘籍中,我们将研究如何在 PyTorch 中实现池化层:

  1. 首先,让我们进行导入:
    >>import torch
    >>import torch.nn as nn
  1. 然后,我们使用nn模块中定义的池类,如下所示:
    >>max_pool = nn.MaxPool2d(3, stride=1)
  1. 现在,让我们定义一个张量来执行池化:
    >>a = torch.FloatTensor(3,5,5).random_(0, 10)
    >>a

这给我们以下输出:


    tensor([[[2., 8., 6., 8., 3.],
 [6., 6., 7., 6., 6.],
 [2., 0., 8., 8., 8.],
 [2., 0., 3., 5., 7.],
 [9., 7., 8., 2., 1.]],

 [[1., 8., 6., 7., 3.],
 [0., 1., 2., 9., 4.],
 [1., 2., 5., 0., 1.],
 [8., 2., 8., 3., 1.],
 [5., 4., 0., 5., 2.]],

 [[1., 6., 2., 6., 1.],
 [4., 0., 0., 6., 6.],
 [4., 2., 2., 3., 2.],
 [1., 0., 1., 7., 1.],
 [8., 1., 0., 5., 4.]]])
  1. 现在,我们将池化应用于张量:
    >>max_pool(a)

这给我们以下输出:

 tensor([[[8., 8., 8.],
 [8., 8., 8.],
 [9., 8., 8.]],

 [[8., 9., 9.],
 [8., 9., 9.],
 [8., 8., 8.]],

 [[6., 6., 6.],
 [4., 7., 7.],
 [8., 7., 7.]]])
  1. 现在,我们可以尝试以类似方式进行平均池化:
    >>avg_pool = nn.AvgPool2d(3, stride=1)
  1. 然后,我们像以前一样应用平均池化:
     >>avg_pool(a) 

这给我们以下输出:

 tensor([[[5.0000, 6.3333, 6.6667],
 [3.7778, 4.7778, 6.4444],
 [4.3333, 4.5556, 5.5556]],

 [[2.8889, 4.4444, 4.1111],
 [3.2222, 3.5556, 3.6667],
 [3.8889, 3.2222, 2.7778]],

 [[2.3333, 3.0000, 3.1111],
 [1.5556, 2.3333, 3.1111],
 [2.1111, 2.3333, 2.7778]]])

通过此秘籍,我们了解了 PyTorch 中的池化操作。

工作原理

在前面的代码中,我们研究了一个张量的示例,以了解实际的池化层。 我们使用大小为3 x 3的方形核。池的第一个应用发生在[0,0,0][0,3,3]的面片上。 由于步幅为 1,因此下一个要操作的音色为[0,0,1][0,3,4]。 一旦碰到水平端,就对下面的张量进行运算。 nn.MaxPool2d(3, stride=1)nn.AvgPool2d(3, stride=1)都创建了大小为3x3的最大和平均池方核,步幅为1,将其应用于随机张量a

更多

在本秘籍中,我们研究了正方形核,但是我们可以选择使用非正方形核并大步前进,就像我们进行卷积一样。 还有另一种流行的池化方法,称为全局平均池化,可以通过输入的维数通过平均池化来实现。 例如avg_pool2d(a, a.size()[2:]0)

另见

您可以在这个页面上找到有关池和各种池的更多信息。

探索转换

PyTorch 无法直接处理图像像素,需要将其内容作为张量。 为了解决这个问题,torchvision是一个专门处理视觉和图像相关任务的库,提供了一个名为transform的模块,该模块提供了将像素转换为张量,标准化标准缩放等的 API。 在本秘籍中,我们将探索转换模块中的各种方法。 因此,您需要安装torchvision才能阅读此秘籍。

操作步骤

在本节中,我们将探讨torchvision中的各种转换:

  1. 我们将从导入torchvision开始:
    >>from torchvision import transforms
  1. 让我们根据图像创建张量:
    >>transforms.ToTensor()
  1. 接下来,让我们标准化图像张量:
>>transforms.Normalize((0.5,),(0.5,))
  1. 要调整图像大小,我们将使用以下方法:
    >>transforms.Resize(10)

我们还可以使用以下内容:

>>transforms.Resize((10,10))
  1. 然后,我们使用转换来裁剪图像:
    >>transforms.CenterCrop(10)

我们还可以使用以下内容:

    >>transforms.CenterCrop((10, 10))
  1. 我们可以使用转换来填充图像张量:
>>transforms.Pad(1, 0)

我们还可以使用以下内容:

>>transforms.Pad((1, 2), 1)

如果愿意,我们还可以执行以下操作:

>>transforms.Pad((1, 2, 2, 3), padding_mode='reflect')
  1. 然后,我们链接多个转换:
>>transforms.Compose([
     transforms.CenterCrop(10),
     transforms.ToTensor(),
  ])

在此秘籍中,我们了解了torchvision中使用的一些转换。

工作原理

在前面的代码段中,我们研究了torchvision中可用的各种转换。 这些使我们可以获取输入图像并将其格式化为所需尺寸和属性的张量,然后将其输入到割炬模型中。 我们研究的第一种方法是toTensor()方法,该方法将给定的输入图像转换为张量。 然后我们可以使用Normalize()方法对该输入图像张量进行归一化。 Normalize()方法采用两个元组,其中第一个元组是输入图像中每个通道的均值序列,第二个元组是每个通道的标准差序列。

此外,我们可以使用Resize()方法将给定图像的大小调整为所需尺寸,如果给定整数,则将其与较小边缘的长度匹配,如果给定元组,则将其与图像的高度和宽度匹配。 在某些情况下,有关图像的关键信息位于其中心,在这种情况下,可以裁剪并仅考虑给定图像的中心; 为此,您可以使用CenterCrop()方法。 然后,我们传入一个整数以从中心裁剪一个正方形,或将与高度和宽度匹配的序列传递给CenterCrop()

另一个重要任务是填充图像以匹配特定尺寸。 为此,我们使用Pad()方法。 我们将填充大小作为整数表示,用于在所有面上进行均等大小的填充,或者将序列作为由两个元素组成的序列,用于填充大小分别对应于左/右和上/下。 此外,我们可以将左侧,顶部,右侧和底部的填充大小作为由四个元素组成的序列传递。 然后,我们将填充值作为整数提供,如果它是三个元素的元组,则分别用作 R,G 和 B 通道的填充值。 除此之外,Pad()方法还具有padding_mode参数,该参数具有以下可能性:

  • constant:使用提供的填充值来填充
  • edge:在图像边缘使用数值来填充
  • reflect:使用图像反射来填充,边缘像素除外
  • symmetric:使用图像反射来填充,包括边缘像素

最后,我们研究了Compose()转换,该转换通过将一系列转换对象作为参数传递来组合各种转换以构建转换管道。

更多

transforms.functional模块中有用于转换的函数式 API。 它们通过提供对转换的细粒度控制来帮助我们建立复杂的转换管道。

还有其他有用的转换,例如灰度转换,它使用Grayscale()作为输出通道数作为参数。 我们将在下一部分中探索更多的转换。

另见

您可以在这个页面上了解有关函数式转换的更多信息。

执行数据扩充

在本秘籍中,我们将学习有关使用火炬进行数据扩充的知识。 数据扩充是深度学习和计算机视觉中的一项重要技术。 对于任何涉及深度学习或计算机视觉的模型,可用的数据量对于查看模型的表现至关重要。 数据扩充可防止模型记住有限数量的数据,而不是对观察到的数据进行概括。 数据扩充通过从原始图像创建变量而不实际收集新数据来增加用于训练模型的数据的多样性。

通常,光量,亮度,方向或颜色变化不会影响模型所做的推断。 但是,当模型在现实世界中部署时,输入数据可能会有这些变化。 对于模型来说,知道其做出的决定必须相对于输入中的这些变化是不变的,这很有用,因此数据扩充可以提高模型的表现。 在本秘籍中,我们将使用 PyTorch 的transform模块执行数据扩充。

操作步骤

为了充分利用此秘籍,您应该完成“探索转换”秘籍,因为此秘籍是我们对转换工作的延续。 在本秘籍中,我们将介绍一些我们可以使用torchvision中的transform模块执行的流行数据扩充:

  1. 我们将从导入torchvision开始:
>>import torchvision
  1. 然后,我们将随机裁剪图像的一部分:
>>transforms.RandomCrop(10)

我们还可以使用以下内容:

>>transforms.RandomCrop((10,20))
  1. 我们可以使用以下方法水平翻转图像:
>>transforms.RandomHorizontalFlip(p=0.3)
  1. 我们也可以垂直翻转它:
>>transforms.RandomVerticalFlip(p=0.3)
  1. 尝试添加亮度,对比度,饱和度和色调变化:
>>transforms.ColorJitter(0.25, 0.25, 0.25, 0.25)
  1. 接下来,让我们添加旋转变化:
>>transforms.RandomRotation(10)
  1. 最后,我们将构成所有转换:
>>transforms.Compose([
     transforms.RandomRotation(10),
     transforms.ToTensor(),
])

在此秘籍中,我们在数据上创建了转换以从现有数据创建更多数据。

工作原理

在本秘籍中,我们了解了如何通过执行对手头问题有意义的某些转换来为数据添加变化。 选择正确的数据扩充以模仿我们在现实生活中会遇到的图像变化时,我们必须小心。 例如,在构建汽车分类器时,有意义的是使用颜色和亮度的变化来增加数据,或者水平翻转汽车图像,等等。 但是,除非我们要解决汽车上下颠倒的问题,否则使用垂直翻转的汽车图像来增强数据是没有意义的。

在此秘籍中,我们尝试在随机位置裁剪图像,以便如果无法获得对象的整个图像,但无法获得一部分,则我们的模型将能够检测到该对象。 我们应该将裁剪后的图像大小包括为整数或具有特定高度和宽度的元组。 然后,我们水平翻转图像,并随机传递了水平翻转和垂直翻转的概率。 然后,我们使用ColorJitter()方法在图像的颜色,对比度,饱和度和色调上创建了变化。

我们通过设置参数来控制每种颜色的变化量,其中颜色,对比度和饱和度在[max(0, 1-parameter), 1 + parameter]值之间变化,而色相在[-hue, hue]之间,其中色调介于 0 到 0.5 之间。 我们还向图像添加了随机旋转,并提供了最大旋转角度。 最后,选择正确的数据扩充策略后,将其添加到transforms.compose()中。

更多

我们还可以自定义定义图像数据所需的转换。 为此,我们将使用transforms.Lambda()并将函数或 lambda 传递给所需的自定义转换。

另见

您可以在这个页面了解有关仿射变换等其他变换的信息。

加载图像数据

在本秘籍中,我们将研究如何将图像数据从文件加载到张量中。 在本秘籍中,我们将使用 CIFAR-10 数据集,该数据集由数据集中 10 个类别中的每个类别的 60,000 个32 x 32像素彩色图像组成。 这些类别是飞机,汽车,鸟,猫,鹿,狗,青蛙,马,船和卡车。

准备

我们将使用torchvision加载数据。 CIFAR-10 可以作为torchvision中的数据集使用。 您应该已经安装了torchvision。 如果没有,则可以使用以下代码进行安装:

pip install torchvision==0.x.x

有了这个设置,我们很高兴选择这个秘籍。

操作步骤

在此秘籍中,我们将在 PyTorch 中加载 CIFAR-10 数据集:

  1. 我们将从torchvision导入datasets模块:
>>from torchvision import datasets
  1. 然后,我们将导入transforms模块:
>>from torchvision import transforms
  1. 然后,我们将创建一个转换管道:
>>transformations = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(20),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))
])
  1. 接下来,我们将使用数据集模块创建训练数据集:
>>train_data = datasets.CIFAR10('CIFAR10', train=True, download=True, transform=transformations)
  1. 同样,我们将创建一个测试数据集:
>>test_data = datasets.CIFAR10('CIFAR10', train=False, download=True, transform=transformations)
  1. 现在,我们可以检查训练和测试数据集的长度:
>>len(train_data), len(test_data)
(50000, 10000)
  1. 现在,我们将从训练集中创建一个验证集; 为此,我们将从torch模块进行导入:
>>from torch.utils.data.sampler import SubsetRandomSampler
  1. 我们将选择 20% 的训练数据作为验证数据:
>>validation_size = 0.2
  1. 现在,我们将导入numpy
>>import numpy as np
  1. 然后,我们将获得训练数据的大小:
>>training_size = len(train_data)
  1. 接下来,我们创建一个索引列表:
>>indices = list(range(training_size))
  1. 然后,我们将重新整理索引列表:
>>np.random.shuffle(indices)
  1. 之后,我们将获得索引以拆分验证和训练数据集:
>>index_split = int(np.floor(training_size * validation_size))
  1. 然后,我们将获得训练和验证集索引:
>>validation_indices, training_indices = indices[:index_split], indices[index_split:]
  1. 现在,我们将使用来自割炬的子集随机采样器:
>>training_sample = SubsetRandomSampler(training_indices)
>>validation_sample = SubsetRandomSampler(validation_indices)
  1. 接下来,我们将数据集分成多个批量。 我们将批量大小设置为 16:
>>batch_size = 16
  1. 然后,我们将在 PyTorch 中使用dataloader模块将数据批量加载:
>>from torch.utils.data.dataloader import DataLoader
  1. 然后,我们将创建训练,验证和测试数据集批量:
>>train_loader = DataLoader(train_data, batch_size=batch_size, sampler=training_sample)
>>valid_loader = DataLoader(train_data, batch_size=batch_size, sampler=validation_sample)
>>test_loader = DataLoader(train_data, batch_size=batch_size)

这样,我们已经加载了数据并对其进行了预处理,以便可以将其发送到模型进行训练。

工作原理

在此秘籍中,我们使用了 PyTorch 中的datasets模块来获取 CIFAR10 数据集。 然后,我们定义了对数据集中的图像有意义的转换,这些图像是与 10 个不同类别相对应的动物的图像。 我们对某些图像进行了水平翻转,并随机对某些图像进行了旋转,范围为 -20 至 20 度。

但是,我们没有添加垂直翻转,因为我们预计在评估阶段不会有动物的上下颠倒的图像输入到模型中。 之后,我们使用ToTensor()变换将图像转换为张量。 准备好张量后,我们使用Normalize()变换分别为红色,绿色和蓝色通道中的每一个设置了均值和标准差。 之后,我们在数据集中使用CIFAR10()方法来使用 CIFAR10 数据集。 然后,将download参数设置为True,以便如果根目录CIFAR10(第一个参数)中不存在数据集,则将其下载并保存在该目录中。

对于训练数据,我们将train参数设置为True,并使用transform参数传递了要应用于数据的转换。 这使我们能够动态创建图像而无需显式创建新图像。 现在,为了准备测试数据,我们将train参数设置为False。 我们将训练和测试数据集的大小分别设置为 50,000 和 10,000。 然后,我们使用训练集的 20%(由validation_size定义)从训练集准备了验证集。 我们随机选择了 20% 的训练集来创建验证集,以使验证集不会偏向特定类别的动物。 然后,我们使用训练集的大小,并使用 Python 中的range()准备了索引列表。

然后,我们使用numpy中的random.shuffle()方法对索引列表进行混排。 指标列表随机化后,我们将指标的前 20% 移至验证集,将其余 80% 的指标移至训练集。 我们通过将原始训练量乘以原始训练集的百分比用作验证集来找到分割索引。 我们使用split_index进行拆分。 然后,我们使用torch.utils.data中的SubsetRandomSampler()方法从给定的索引列表中随机抽取元素,而不进行替换。 最后,我们使用DataLoader()组合了数据集和采样器,以对数据集进行迭代。 然后,我们将数据加载器用于训练,验证和测试集,以在训练模型时对数据进行迭代。

更多

DataLoader()模块中还有许多函数-例如,DataLoader()可用于多进程数据加载,而num_workers控制在加载数据时要使用的子进程数。 在我们的示例中,我们使用了默认值0,这意味着数据已在主进程中加载​​,这对于小型数据集是理想的选择,并为我们提供了更具可读性的错误跟踪。

另见

您可以在这个页面上了解有关数据加载工具的更多信息。

定义 CNN 架构

到目前为止,在本章中,我们一直在研究 CNN 的不同组成部分,以及如何将数据集中的数据加载到可以馈入 CNN 模型的格式中。 在本秘籍中,我们将通过到目前为止已经看到的完成模型的组件来定义 CNN 模型架构。 这与我们在第 2 章,“处理神经网络”中介绍的全连接神经网络非常相似。 为了更好地理解此秘籍,从第 2 章,“处理神经网络”修改全连接神经网络的模型定义将是一个好主意。 我们将在 CIFAR10 数据集上建立图像分类模型,我们在“加载图像数据”秘籍中对此进行了讨论。

操作步骤

我们将在此秘籍中完成模型类的定义:

1.首先,我们将导入nn.Moduletorch函数式 API:

>>import torch.nn as nn
>>import torch.nn.functional as F

2.然后,我们将编写一个继承自nn.Module的类:

>>class CNN(nn.Module):

3.然后我们将定义我们的__init__()

>>def __init__(self):
    super().__init__()

4.现在,我们将在__init__()中定义我们的卷积层:

    self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
    self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
    self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
    self.pool = nn.MaxPool2d(2, 2)
    self.linear1 = nn.Linear(64 * 4 * 4, 512)
    self.linear2 = nn.Linear(512, 10)
    self.dropout = nn.Dropout(p=0.3)

5.我们的下一步是编写forward()方法:

>>def forward(self, x):
    x = self.pool(F.relu(self.conv1(x)))
    x = self.pool(F.relu(self.conv2(x)))
    x = self.pool(F.relu(self.conv3(x)))
    x = x.view(-1, 64 * 4 * 4)
    x = self.dropout(x)
    x = F.relu(self.linear1(x))
    x = self.dropout(x)
    x = self.linear2(x)
    return x

6.现在我们的 CNN 类已经完成,我们可以实例化我们的模型类:

>>model = CNN()
>>model
CNN(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (linear1): Linear(in_features=1024, out_features=512, bias=True)
  (linear2): Linear(in_features=512, out_features=10, bias=True)
  (dropout): Dropout(p=0.3)
)

在本秘籍中,我们已经完成了模型定义。

工作原理

此秘籍的工作方式与第 2 章,“处理神经网络”时非常相似,当我们研究一个全连接神经网络时。 我们从__init__()方法和父类的构造器开始,定义了从 PyTorch 中的nn.Module继承的 CNN 类。 之后,我们通过传入与每一层相关的参数来定义 CNN 中的各个层。 对于我们的第一卷积层,输入通道的数量为 3(RGB),输出通道的数量定义为 16,其平方核大小为 3。第二卷积层采用上一层的张量,并具有 16 个输入通道和 32 个输出通道,核尺寸为3 x 3。类似地,第三卷积层具有 32 个输入通道和 64 个输出通道,核尺寸为3 x 3。 我们还需要一个最大池化层,并使用 2 的核大小和 2 的步幅。我们使用.view()将张量的三个维度展平为一个维度,以便可以将其传递到全连接网络中。 view函数中的 -1 通过确保view函数之前和之后的元素数量保持相同(在本例中为批量大小)来确保将正确的尺寸自动分配给该尺寸。

对于第一个全连接层,我们有 1,024 个输入(通过将最大池后的64 x 4 x 4张量展平而获得)和 512 个输出。 对于最后一个全连接层,我们有 512 个输入和 10 个输出,代表输出类别的数量。 我们还为全连接层定义了一个丢弃层,概率为 0.3。

接下来,我们定义forward()方法,将__init__()方法中定义的组件连接在一起。 因此,输入批量的 16 个张量(每个张量为32 x 32 x 3)经过第一个卷积层,然后经过 ReLU,然后是最大合并层,以形成尺寸为16 x 16 x 16的输出张量,然后通过第二个卷积层,然后是 ReLU 和最大池化层,输出的尺寸为8 x 8 x 32,然后是第三个卷积层,然后是 ReLU 和最大池化层,尺寸为4 x 4 x 64。此后,我们将图像展平为 1,024 个元素的向量,并将其通过丢弃层传递到第一个全连接层,提供 512 个输出,然后是 ReLU 和丢弃,在最后一个全连接层中,以提供所需的输出数量,本例中为 10。

然后,我们从 CNN 类实例化该模型并打印该模型。

更多

您可以尝试不同的配置以用于丢弃,卷积和池化层,甚至可以更改每种类型的层数。

另见

您可以在这个页面上看到使用 CNN 训练 CIFAR10 的不同模型。

训练图像分类器

现在我们已经定义了模型,接下来的主要步骤是使用手头的数据训练该模型。 这将与我们在第 2 章,“处理神经网络”和我们全连接神经网络中进行的训练非常相似。 在本秘籍中,我们将完成训练图像分类器的工作。 如果您在完成本秘籍之前,先阅读了第 2 章的“实现优化器”秘籍,那将非常有用。

操作步骤

让我们通过以下秘籍完成模型的训练:

1.首先,我们将导入torchtorch.optim

>>import torch
>>import torch.nn as nn
>>import torch.optim as optim

2.然后,我们将检查运行模型所需的设备:

>>device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
>>device.type
'cuda'

3.然后,我们将模型移至可用设备:

>>model = model.to(device)

4.接下来,我们添加交叉熵损失:

>>criterion = nn.CrossEntropyLoss()

5.然后,添加优化器:

>>optimizer = optim.SGD(model.parameters(), lr=0.01)

6.现在,我们将通过设置周期数来开始训练循环:

>>n_epochs = 30
>>for epoch in range(1, n_epochs+1):
    train_loss = 0.0
    valid_loss = 0.0

7.然后,在循环中将模型设置为训练模式:

model.train()

8.然后,我们遍历每批:

for batch_idx, (data, target) in enumerate(train_loader):
    data, target = data.to(device), target.to(device)
    optimizer.zero_grad()

9.然后,我们在循环中将数据传递给模型:

output = model(data)

10.接下来,我们得到了损失:

loss = criterion(output, target)

11.然后,我们反向传播:

loss.backward()

12.接下来,我们更新模型参数:

optimizer.step()

13.然后,我们更新总损失:

train_loss += loss.item()*data.size(0)

14.然后,我们将模型切换到评估模式,退出训练批量循环:

model.eval()

15.然后,我们遍历验证集批量:

for batch_idx, (data, target) in enumerate(valid_loader):
    data, target = data.to(device), target.to(device)

16.接下来,我们获得模型输出和损失,就像在“步骤 9”,“步骤 10”和“步骤 13”中所做的那样:

 output = model(data)
 loss = criterion(output, target)
 valid_loss += loss.item()*data.size(0)

17.然后,我们将计算每个周期的损失:

train_loss = train_loss/len(train_loader.sampler)
valid_loss = valid_loss/len(valid_loader.sampler)

18.最后,我们在每个周期打印模型表现:

    print(f'| Epoch: {epoch:02} | Train Loss: {train_loss:.3f} | Val. Loss: {valid_loss:.3f} |')

19.这是您将看到的输出示例:


| Epoch: 01 | Train Loss: 2.027 | Val. Loss: 1.784 |
| Epoch: 02 | Train Loss: 1.640 | Val. Loss: 1.507 |
| Epoch: 03 | Train Loss: 1.483 | Val. Loss: 1.383 |
| Epoch: 04 | Train Loss: 1.380 | Val. Loss: 1.284 |
| Epoch: 05 | Train Loss: 1.312 | Val. Loss: 1.235 |
| Epoch: 06 | Train Loss: 1.251 | Val. Loss: 1.170 |
| Epoch: 07 | Train Loss: 1.198 | Val. Loss: 1.144 |
| Epoch: 08 | Train Loss: 1.162 | Val. Loss: 1.090 |
| Epoch: 09 | Train Loss: 1.123 | Val. Loss: 1.047 |
| Epoch: 10 | Train Loss: 1.088 | Val. Loss: 1.075 |
| Epoch: 11 | Train Loss: 1.061 | Val. Loss: 1.010 |
| Epoch: 12 | Train Loss: 1.035 | Val. Loss: 0.966 |
| Epoch: 13 | Train Loss: 1.012 | Val. Loss: 0.950 |
| Epoch: 14 | Train Loss: 0.991 | Val. Loss: 0.912 |
| Epoch: 15 | Train Loss: 0.971 | Val. Loss: 0.912 |
| Epoch: 16 | Train Loss: 0.946 | Val. Loss: 0.883 |
| Epoch: 17 | Train Loss: 0.931 | Val. Loss: 0.906 |
| Epoch: 18 | Train Loss: 0.913 | Val. Loss: 0.869 |
| Epoch: 19 | Train Loss: 0.896 | Val. Loss: 0.840 |
| Epoch: 20 | Train Loss: 0.885 | Val. Loss: 0.847 |
| Epoch: 21 | Train Loss: 0.873 | Val. Loss: 0.809 |
| Epoch: 22 | Train Loss: 0.855 | Val. Loss: 0.835 |
| Epoch: 23 | Train Loss: 0.847 | Val. Loss: 0.811 |
| Epoch: 24 | Train Loss: 0.834 | Val. Loss: 0.826 |
| Epoch: 25 | Train Loss: 0.823 | Val. Loss: 0.795 |
| Epoch: 26 | Train Loss: 0.810 | Val. Loss: 0.776 |
| Epoch: 27 | Train Loss: 0.800 | Val. Loss: 0.759 |
| Epoch: 28 | Train Loss: 0.795 | Val. Loss: 0.767 |
| Epoch: 29 | Train Loss: 0.786 | Val. Loss: 0.789 |
| Epoch: 30 | Train Loss: 0.773 | Val. Loss: 0.754 |

有了这个秘籍,我们就完成了图像分类器的训练。

工作原理

在此秘籍中,我们找到并训练了我们的模型。 为此,我们进行了导入,首先要确定模型并将模型分配给我们在计算机上拥有的适当设备。 我们使用model.to(device)方法移动模型,这比使用model.cuda()model.cpu()更为优雅。

然后,我们定义了损失函数,也称为criterion。 由于这是一个分类问题,因此我们使用了交叉熵损失。 然后,我们选择 SGD 优化器在反向传播时更新模型权重,学习率为 0.01,并使用model.parameters()传入模型参数。 然后,我们将模型运行了 30 个周期,尽管我们可以选择任何合理的数目来执行此操作。 在循环中,我们将训练和验证损失重置为 0,并将模型设置为训练模式,然后遍历训练数据集中的每个批量。 我们首先将批量移至设备,这样,如果我们的 GPU 内存有限,则并非所有数据都不会完全加载到 GPU 内存中。 然后,我们将输入张量传递到模型中,并获取输出,并将其传递到损失函数中,以评估预测标签和真实标签之间的差异。

此后,我们使用loss.backward()进行了反向传播,并使用optimizer.step()步骤更新了模型权重。 然后,我们使用总历时损失来汇总批量中的损失。 然后,我们使用model.eval()将模型转换为评估模型,因为该模型的表现需要在验证集上进行评估,并且该模型在此阶段中不会学习,因此我们也需要关闭退出项。 遍历验证批量,我们获得了模型输出,并在整个周期累积了验证批量之间的损失。 此后,我们格式化了模型表现,以查看每个周期模型的变化。 我们注意到,模型训练和验证损失随时间的推移而减少,这表明模型正在学习。

更多

我们已经运行了训练过的模型,我们需要根据保持数据或测试数据(即该模型尚未看到的数据)评估模型。 通过这样做,我们可以评估模型的真实表现。 为此,您将必须进入模型测试批量,并且对于每个批量,必须执行_, prediction = torch.max(output, 1)将 softmax 概率转换为实际预测,并使用prediction.eq(target.data.view_as(prediction))将预测与真实输出标签进行比较,其中我们确保预测张量和输出张量的尺寸相同。 这将返回一个张量,其中匹配的张量为 1,不匹配的张量为 0。 我们可以使用它来计算每个批量中模型的准确率,并将其汇总到整个测试数据集中。

另见

您可以在这个页面上看到测试模型的示例实现。

四、用于 NLP 的循环神经网络

在本章中,我们将处理循环神经网络RNNs),这是一种专门处理序列数据或时变数据的神经网络。 利用卷积神经网络,我们处理在空间上彼此相关的数据点,其中一组像素值保存有关图像的信息。 但是请考虑一下节奏,节奏是由一段时间内一系列变化的声音信号形成的。 数据点彼此之间具有时间关系。 在循环神经网络中,神经元之间的连接在时间序列上形成有向图,表现出时间动态行为。 传统的前馈网络不存储先前的输入; 但是,RNN 使用存储单元来记住先前的输入,因此根据到目前为止的输入顺序来处理当前输入。

在本章中,我们将介绍以下秘籍:

  • 分词
  • 创建字段
  • 开发数据集
  • 开发迭代器
  • 探索单词嵌入
  • 建立 LSTM 网络
  • 多层 LSTM
  • 双向 LSTM

RNN 介绍

这是 RNN 的示意图:

图 1:循环神经网络

在上图中,我们可以看到 RNN 中的输入,输出和循环本身。 RNN 是为信息的持久性而设计的,而循环组件则可以实现这一点。

这是“图 1”的扩展版本:

图 2:RNN 的展开图

在上图中,我们可以看到 RNN 的展开图,其中一个步骤的信息被馈送到下一个步骤,从而创建同一网络的多个副本,所有这些信息都封装在循环循环中。 循环神经网络接受输入并给出输出,但是该输出不仅取决于给定实例上的输入,还取决于提供给网络的输入的整个历史,网络会在数学上记住这些输入。

循环神经网络能够接收可变大小的输入序列并产生可变大小的输出序列,从而执行与全连接神经网络中固定数量的计算相反的可变数量的计算。 进一步的 RNN 允许信息持久化,并在接收到的输入之间共享该信息。 在给定实例上生成的输出基于到目前为止所看到的所有输入的历史记录。

这是 LSTM 的示意图:

图 3:LSTM

长短期记忆LSTM)网络是一种循环神经网络,是 RNN 之上的一种进步。

这是双向 LSTM:

图 4:双向 LSTM

这是多层 LSTM:

图 5:多层 LSTM

双向 LSTM 和多层 LSTM 是对基本 LSTM 网络架构的改进。

技术要求

在本章中,我们需要设置 PyTorch。 我们将使用torchtext,它是一个专门的库,用于处理与 PyTorch 联合工作的语言任务。

我们可以使用以下pip命令安装torchtext

pip install torchtext

至此,我们完成了本章所需的设置。

分词

在处理自然语言处理任务时,我们采用文本语料库并将其分解为较小的单元。 在本秘籍中,我们将句子分解为单个单词,其中每个单词代表一个意思,其他单词与它附近的其他单词一起表达一个句子的意图。 计算机只能理解数字,因此为这些单词分配了唯一的整数值来表示单词。 将句子分解为标记的过程称为分词。 在本秘籍中,我们将执行单词分词。

操作步骤

在本秘籍中,我们将编写一个标记器,将在本章的“创建字段”部分中使用:

  1. 我们将首先编写一个简单的 lambda 函数:
>>tokenizer = lambda words: words.split()
  1. 然后,我们将测试tokenizer()函数:
>>tokenizer("This is a test for tokenizer")
['This', 'is', 'a', 'test', 'for', 'tokenizer']

在此秘籍中,我们成功实现了单词分词。

工作原理

在此秘籍中,我们编写了一个简单的分词器 lambda 函数,该函数可用于英语和类似英语的语言。 我们使用单词之间的空格来标记句子。 然后,我们通过将句子传递给tokenizer()函数来测试标记器。 我们将在下一个秘籍中使用此标记生成器来创建字段。

更多

我们还可以使用nltk库对句子进行分词:

>>from nltk.tokenize import word_tokenize
>>word_tokenize("This is a test for tokenizer")
['This', 'is', 'a', 'test', 'for', 'tokenizer']

此外,还有其他类型的分词,例如字符串分词,其中涉及将字符串分词为子字符串。

另见

可以在这个页面上探索使用nltk对字符串进行的各种分词。

创建字段

在本秘籍中,我们将探索字段,这些字段与torchvision中可用的工具一样,使处理自然语言数据变得容易。 字段让我们定义数据类型,并通过指定要对数据执行的一组操作来帮助我们从文本数据中创建张量。 Field类使我们可以执行常见的文本处理任务,并掌握手边的数据词汇。

在本秘籍中,我们将研究如何使用Field类定义各种文本处理任务。

操作步骤

在本秘籍中,我们将探讨使用字段的各种示例:

  1. 我们将从导入开始:
>>from torchtext.data import Field
  1. 为了进行情感分析,我们将为评论定义Field对象:
>>Review = Field(sequential=True, tokenize=tokenizer, lower=True)
  1. 然后,我们为标签定义字段:
>>Label = Field(sequential=False, use_vocab=False)
  1. 我们可以在输入字符串的开头和结尾添加标记:
>>SequenceField = Field(tokenize=tokenizer, init_token='<sos>', eos_token='<eos>', lower=True)
  1. 我们可以将序列设置为固定长度:
>>SequenceField = Field(tokenize=tokenizer, init_token='<sos>', eos_token='<eos>', lower=True, fix_length=50)
  1. 我们可以设置一个未知的标记:
>>SequenceField = Field(tokenize=tokenizer, init_token='<sos>', eos_token='<eos>', unk_token='<unk>')
  1. 我们可以将批量维度设置为第一维度:
>>SequenceField = Field(tokenize=tokenizer, init_token='<sos>', eos_token='<eos>', unk_token='<unk>', batch_first=True)

通过此秘籍,我们探索了可用于在torchtext中创建字段的不同方法。

工作原理

在此秘籍中,我们根据手头的特定任务,使用field类对给定的输入文本执行了各种文本处理任务。 在审阅分类的示例中,在review字段中,我们将sequential参数设置为True,因为它是序列数据。 对于标签字段,我们将其设置为False,因为它们不是顺序的。 我们可以将文本设置为小写,这样就不会根据标记的大小写为相同的单词分配单独的标记 ID。 在评论分类的情况下,这不会影响含义; 这可以通过将lower设置为True来实现。

对于数字字段,我们将use_vocab设置为False,这是我们为审阅标签所做的,因为我们假设标签的负数值为0,正数的值为1。 我们从分词部分传递了tokenizer函数作为tokenize参数; 我们甚至可以通过设置tokenize="spacy"使用spacy的标记器。 对于某些任务,例如,使用Sequence对模型进行排序,我们可能需要特殊的标记来指示序列的开始和结束。 可以通过设置init_tokeneos_token参数轻松完成。 这对于序列到序列的模型是正确的,并且如果在模型评估期间用于训练模型的词汇表中不存在标记(词汇表外),则可以使用自定义标记来替换这些标记, 设置unk_token参数。

然后,将batch_first设置为True,以使输出张量的第一维为批量维,如果fix_length参数设置为整​​数值,则使用此字段将对输入设置固定长度。

更多

我们可以为特定于某种语言的分词设置用于分词的语言,该语言支持 spacy 支持的语言。 我们可以使用pad_token参数设置自定义填充标记。 我们可以在分词之后但在数字化之前使用该字段定义将应用于示例的处理管道,在数字化之后但使用preprocessingpostprocessing参数将数字转换为张量之前,我们可以执行相同的操作。 stop_words参数可用于删除在预处理时需要删除的标记。 此外,还有一种专门用于标签字段的字段类型LabelField,它可以代替普通字段使用。

另见

您可以通过这个页面了解更多有关字段的信息。

开发数据集

在本秘籍中,我们将研究读取文本数据并使用各种数据源。torchtext可以从文本文件,CSV/TSV 文件,JSON 文件和目录中读取数据,并将其转换为数据集。 数据集是已预处理的数据块,可读取到内存中,并可由其他数据结构使用。

准备

我们将使用此秘籍的新闻分类数据集,您可以从这里下载该数据集。

.csv文件中包含以下几列:

  • id
  • content
  • Business
  • SciTech
  • Sports
  • World

操作步骤

在此秘籍中,我们将读取有毒评论数据集,该数据集存储为一组.csv文件:

  1. 我们将从导入开始:
>>from torchtext.data import TabularDataset
  1. 我们将选择训练列:
>>train_datafields = [("id", None),
                 ("content", Review), ("Business", Label),
                 ("SciTech", Label), ("Sports", Label),
                 ("World", Label)]
  1. 然后,我们将选择测试列:
>>test_datafields = [("id", None),
                  ("content", Review)]
  1. 然后,我们将阅读训练和验证.csv文件:
>>train, valid = TabularDataset.splits(path='NewsClassification', 
                                    train='train.csv',
                                    valid='valid.csv',
                                    format='csv',
                                    skip_header=True,
                                    fields=train_datafields)
  1. 接下来,我们将读取测试.csv文件:
>>test = TabularDataset(path="NewsClassification/test.csv",
                    format='csv',
                    skip_header=True,
                    fields=test_datafields)
  1. 然后,我们将建立词汇表:
>>Review.build_vocab(train, min_freq=2)

通过此秘籍,我们定义了数据集的格式。

工作原理

我们使用torchtext中的TabularDataset模块读取 CSV 文件,该文件也可以用于读取 TSV,JSON 和 Python 字典中的输入,这些字典定义了列的数据集。 然后,我们定义了一个元组数组,其中每个元组都是一对列和Field对象(定义要应用的文本转换),但是在最终数据集中不需要特定的列。 然后,我们将在 ID 列中看到的该列的相应Field对象设置为None

在此秘籍中,我们使用了新闻分类数据集。 我们在其中一个文本列中应用了Review字段,并在其余列中应用了Label字段。 对于test_datafield,我们将具有新闻内容,因此对于content列,我们应用了Review字段,并完全删除了id列。 然后,我们在TabularDataset中使用splits方法,并将其传递到训练和验证文件所在的根文件夹路径中。 我们还使用trainvalid参数传入了训练和验证文件的文件名。

我们将文件格式指定为csv,并通过将skip_header设置为True以及fields参数中的必需列来删除标题行,并且对数据集进行了相同的测试。 最后,我们在Fields对象中调用了build_vocab()方法,以建立可能的单词库,在数据集中的出现次数最少为两次。 不在词汇表中的单词将在验证和测试集中分配一个未知标签。

更多

您可以使用Vocab模块在torchtext中构建词汇表。 除了TabularDataset之外,还有其他类型的数据集可以使用,具体取决于手头的 NLP 任务-例如,对于语言翻译任务,我们可以使用TranslationDataset类。

另见

您可以在这个页面上了解有关数据集的更多信息。

开发迭代器

迭代器用于从数据集中加载一批数据。 它们提供了使加载数据和将数据移动到适当设备的方法更加容易。 我们可以使用这些迭代器对象遍历周期时对数据进行迭代。 在本秘籍中,我们将从数据集中开发这些迭代器。 您将需要完成“开发数据集”秘籍中的步骤,因为我们将在此处使用该秘籍中的Dataset对象。

操作步骤

在此秘籍中,我们将数据集转换为迭代器,以便在每个周期准备好要迭代的适当批量:

  1. 我们将从导入开始:
>>from torchtext.data import BucketIterator
>>import torch
  1. 然后,我们将定义批量大小:
>>BATCH_SIZE = 128
  1. 然后,我们将确定可用的设备:
>>device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  1. 接下来,我们将使用BucketIterator创建数据桶。
>>train_iter, valid_iter, test_iter = BucketIterator.splits(
                                     (train, valid, test),
                                     batch_size=BATCH_SIZE,
                                     device=device,
                                     sort_key=lambda x: len(x.comment_text), 
                                     sort_within_batch=False)

通过此秘籍,我们为训练,测试和验证数据集创建了迭代器。

工作原理

我们使用迭代器来构建训练,测试和验证批量,并将数据集移动到适当的 CPU 或 GPU 设备中。 Iterators使执行这些任务变得非常优雅。 我们使用了称为BucketIterator的专门迭代器类,该类将输入序列分组为相似长度的序列,并自动对其进行随机排序。 我们定义了批量大小,并找到了机器上可用的设备。

然后,我们使用BucketIteratorsplits方法创建训练,测试和验证迭代器。 我们将sort_within_batch参数设置为False,如果使用pack_padded_sequence则将其设置为True,这将阻止 LSTM 看到输入序列的填充部分。 当True使用sort_key参数时,它将按降序对批量中的序列进行排序。

更多

还有其他类型的迭代器可用。 一个简单的迭代器从Dataset对象中加载一批数据,而BPTTIterator定义了用于语言建模任务的迭代器,其中一对序列彼此相距一个时间步长。

另见

您可以在这个页面上找到有关迭代器参数的更多信息。

探索单词嵌入

单词嵌入是单词的学习表示。 它们是单词的密集表示,其中为每个单词分配一个向量,即预定义向量空间中的实值向量,而不是数字标识符。 例如,一个单词将表示为n维向量[x[1], x[2], ..., x[n]]-例如,语料库中的单词book可能表示为[0.22, 0.242, ..., ..., 1.234]而不是[0, 0, 1, ..., 0]的单引号表示。

数字表示只是单词的表示; 但是,单词嵌入是标记的表示,其中该表示也包含标记/单词的含义。 模型从单词出现的上下文中获悉了此含义。 在词嵌入中,具有相似含义的词具有相似的表示形式,我们可以对这些词向量执行向量算术,如下所示:

在这里,我们能够从king向量中减去man向量,并将其与woman向量相加,所得向量将接近该向量Queen的表示形式。 我们将在本秘籍中探讨此实现。

操作步骤

在此秘籍中,我们将对torchtext使用预训练的嵌入:

  1. 我们将从导入开始:
>>from torchtext import vocab
  1. 然后,我们将继续加载嵌入向量:
>>vec = vocab.Vectors('glove.6B.100d.txt', cache='./vec/glove_embedding/', url='http://nlp.stanford.edu/data/glove.6B.zip')
  1. 我们可以通过将预训练向量应用于字段对象来构建词汇表:
>>Review.build_vocab(train, min_freq=2, vectors=vec)

通过此秘籍,我们已加载了预训练的单词嵌入。

工作原理

torchtext具有用于处理嵌入的vocab模块。 我们可以通过在此秘籍中提及需要的嵌入名称来下载预训练的嵌入。 我们使用了预先训练的 GloVe(GloVe 是词向量技术)模型,该模型使用 60 亿个​​标记和 100 个嵌入维向量glove.6B.50d进行训练。

然后,我们从缓存位置加载向量。 如果所需的嵌入不在高速缓存中,那么它将自动从 URL 下载并作为嵌入向量传递。 然后,我们使用Review字段对象的build_vocab方法从这些预训练的嵌入中建立词汇表,然后将其添加到训练数据的词汇表中。

更多

我们还可以使用训练数据中的预训练嵌入词汇表-例如,我们可以使用使用 gensim 创建的嵌入向量作为嵌入向量。 我们还可以使用torch.nn模块创建嵌入; 我们将在下一个秘籍中了解如何执行此操作。

另见

您可以在这个页面上了解有关嵌入的更多信息。

构建 LSTM 网络

长短期记忆LSTM)网络是一种循环神经网络,具有内部门控,有助于更好地保持信息。 这些门是微小的神经网络,用于控制何时需要保存信息以及何时可以擦除或忘记信息。 RNN 受梯度消失和爆炸的影响,因此很难学习长期依赖关系。 LSTM 可以抵抗爆炸和梯度消失,尽管在数学上仍然可行。

操作步骤

在此秘籍中,我们将定义 LSTM 分类器:

  1. 我们将从导入开始:
>>import torch.nn as nn
  1. 我们将命名为LSTMClassifier类:
>>class LSTMClassifier(nn.Module):
  1. 然后,我们添加嵌入层:
>>def __init__(self, embedding_dim, hidden_dim, output_dim, dropout):
            super().__init__()
            self.embedding = nn.Embedding(len(Review.vocab), embedding_dim)
  1. 然后,我们添加 LSTM 层:
self.rnn = nn.LSTM(embedding_dim, hidden_dim)
  1. 然后,我们添加一个全连接层:
self.fc = nn.Linear(hidden_dim, output_dim)
  1. 接下来,我们定义丢弃层:
self.dropout = nn.Dropout(dropout)
  1. 然后,我们为 LSTM 分类器定义正向方法:
>>def forward(self, x):
  1. 接下来,我们输入嵌入层:
x = self.embedding(x)
  1. 然后,我们将嵌入层的输出传递到 LSTM 中:
output, (hidden, cell) = self.rnn(x)
  1. 然后,我们应用dropout
hidden = self.dropout(hidden)
  1. 最后,我们将输出传递给线性层:
return self.fc(hidden)
  1. 我们将定义超参数如下:
>>EMBEDDING_DIM = 100
>>HIDDEN_DIM = 256
>>OUTPUT_DIM = 1
>>DROPOUT = 0.5
  1. 最后,我们创建一个model对象:
>>model = LSTMClassifier(EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, DROPOUT)

通过此秘籍,我们创建了一个 LSTM 模型。

工作原理

我们使用torch.nn模块创建了从torch.nn.Module继承的模型类LSTMClassifier,并初始化了基类构造器。 然后,我们定义嵌入层,其中输入维与词汇量大小相同,输出为嵌入维,然后将嵌入层输出传递到 LSTM 层,其中输入维为嵌入维,然后定义隐藏状态维度。

然后,我们定义了全连接层和丢弃层。 接下来,我们定义forward()方法,该方法接受输入序列,并将其传递给嵌入层,从而产生尺寸为embedding_dim的输出,该输出是输入序列的嵌入向量。 然后将这个字向量传递到 LSTM 层,该层输出三个状态-输出状态,隐藏状态和单元状态。

隐藏状态张量保存了到目前为止 LSTM 所见过的所有序列的信息,因此我们采用了隐藏状态,应用了dropout,并将其通过全连接层传递给最终输出向量,其大小等于类数。 例如,对于有毒评论数据集,输出类别的数量为六; 但是,对于具有两个状态(正向和负向)的情感分析器,我们甚至可以考虑只具有一个输出,以便1代表积极情感,0代表消极情感。

更多

对于具有两个以上状态的毒性审查任务,我们将使用CrossEntropyLoss(),对于仅具有一个输出的情感分析器,我们将使用BCEWithLogitsLoss()。 其余训练与我们在第 3 章,“用于计算机视觉的卷积神经网络”中看到的内容相同,我们在其中训练了卷积神经网络。

另见

您可以在这个页面上了解有关 LSTM 的更多信息。

您可以在这个页面了解有关消失和梯度爆炸的更多信息。

多层 LSTM

我们在先前的秘籍中介绍了简单的 LSTM。 在本秘籍中,我们将为多层 LSTM 升级该简单的 LSTM 定义。 您需要完成“构建 LSTM 网络”秘籍才能了解此秘籍。

操作步骤

该秘籍是基于 LSTM 秘籍的修改。

  1. 首先,我们将更新该类的__init__()
>>def __init__(self, embedding_dim, hidden_dim, output_dim, dropout, num_layers):
  1. 然后,我们将num_layers参数添加到 LSTM 定义中:
self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=num_layers)

我们的类定义应如下所示:

class MultiLSTMClassifier(nn.Module):
    def __init__(self, embedding_dim, hidden_dim, output_dim, dropout, num_layers):
        self.embedding = nn.Embedding(len(Review.vocab), embedding_dim)
        self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=num_layers)
        self.fc = nn.Linear(hidden_dim, output_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        x = self.embedding(x)
        output, (hidden, cell) = self.rnn(x)
        hidden = self.dropout(hidden)
        return self.fc(hidden[-1])
  1. 接下来,我们向超参数添加层数量:
>>NUM_LAYERS = 2
  1. 最后,我们创建模型对象:
>>model = MultiLSTMClassifier(EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, DROPOUT, NUM_LAYERS)

通过此秘籍,我们修改了用于多层 LSTM 的网络。

工作原理

在此秘籍中,我们在构造器中添加了num_layers和参数以控制模型中 LSTM 的层数,并将其作为关键字参数num_layers传递给 LSTM 定义。

然后,在forward()方法中,由于隐藏状态的形状是[num_layers * num_directions, batch, hidden_dim](默认情况下num_direction1),因此我们仅使用hidden[-1]从最后一个 LSTM 层获取了隐藏状态。 这意味着hidden[-1]给出了最后一层的隐藏状态。 通过这样做,我们可以选择num_layers作为超参数。 来自较低层的隐藏状态输出作为较高状态的输入传递。

更多

在本秘籍中,我们仅考虑了最后一个 LSTM 层的隐藏状态。 但是,可能存在使用所有隐藏层的复杂架构。 有一个dropout参数,可用于在多层 LSTM 的各层之间施加丢弃。

另见

您可以在这个页面上了解有关多层 LSTM 的更多信息。

双向 LSTM

该秘籍以多层 LSTM 秘籍为基础。 在正常的 LSTM 中,LSTM 会从头到尾读取输入序列。 但是,在双向 LSTM 中,有第二个 LSTM 从最后到第一个读取序列,即反向 RNN。 当当前时间戳的预测取决于序列中进一步输入时,这种类型的 LSTM 可以提高模型表现。 考虑示例“我看过漫画”和“我昨天看过漫画”。 在这种情况下,基于将来出现的标记,相同的标记(即read)具有不同的含义。 我们将在本秘籍中探讨其实现。

准备

该秘籍建立在“多层 LSTM”秘籍的基础上,因此在尝试该秘籍之前,请务必先完成该秘籍。

操作步骤

在此秘籍中,我们将修改“多层 LSTM”秘籍中的类定义,使其成为双向 LSTM:

  1. 我们将bidirectional参数设置为True
self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=num_layers, bidirectional=True)
  1. 然后,我们将更改全连接层的输入尺寸:
self.fc = nn.Linear(2*hidden_dim, output_dim)
  1. 然后,将输入更新为全连接层,如下所示:
hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1))
return self.fc(hidden.squeeze(0))

现在,类定义如下所示:

class BiLSTMClassifier(nn.Module):
    def __init__(self, embedding_dim, hidden_dim, output_dim, dropout, num_layers):
        self.embedding = nn.Embedding(len(Review.vocab), embedding_dim)
        self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=num_layers, bidirectional=True)
        self.fc = nn.Linear(2*hidden_dim, output_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        x = self.embedding(x)
        output, (hidden, cell) = self.rnn(x)
        hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1))
        return self.fc(hidden.squeeze(0))
  1. 然后,我们创建模型对象:
>>model = BiLSTMClassifier(EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, DROPOUT, NUM_LAYERS)

通过此秘籍,我们修改了网络,使其现在是双向 LSTM。

工作原理

在此秘籍中,我们在 LSTM 定义中将bidirectional标志设置为True。 我们将前向和后向 LSTM 的隐藏状态连接起来,并将它们传递到全连接层中。 因此,全连接层的输入尺寸增加了一倍,以适应前向和后向隐藏状态张量。

forward()方法中,我们使用torch.cat()连接了向前和向后隐藏状态,并使用了向前和向后 LSTM 的最后一个隐藏状态。 在 PyTorch 中,隐藏状态堆叠为[forward_layer_0, backward_layer_0, forward_layer_1, backward_layer_1, ..., forward_layer_n, backward_layer_n],因此所需的张量为hidden[-2,:,:], hidden[-1,:,:]。 连接后,在挤出额外的尺寸后,我们将隐藏的向量传递到全连接层中。

更多

我们选择了最后的前向和后向隐藏状态并将它们连接起来,这就是我们选择的架构。 但是,根据手头的任务,我们可以选择任何或所有隐藏状态。

另见

您可以在这个页面上了解有关多层 LSTM 的更多信息。

五、迁移学习和 TensorBoard

迁移学习是深度学习中的一个重要概念,它使我们可以将深度学习用于各种日常任务。 这是一种机器学习技术,其中针对任务训练的模型被重用以创建用于类似任务的新模型。 我们采用在大型数据集上训练的模型,并将其知识迁移到较小的数据集。 对于具有卷积神经网络CNN)的计算机视觉任务,我们冻结了网络的早期卷积层,仅训练了最后几层。 早期的卷积层提取适用于整个图像的通用低级特征,以检测边缘,图案和梯度,而稍后的层识别图像中的特定特征,并且特定于数据集。

在本章中,我们将训练图像分类器以区分正常患者和肺炎患者的胸部 X 光,并使用经过训练的 ResNet-50 模型进行迁移学习。 我们将替换分类器,并有两个输出单元代表正常和肺炎分类。

我们将在以下阶段完成迁移学习任务:

  1. 加载在 ImageNet 数据集上训练的预训练的 ResNet-50 模型。
  2. 在模型的较低卷积层中冻结参数(权重)。
  3. 用多层可训练参数替换分类器。
  4. 在可用于任务的训练数据上训练分类器层。
  5. 微调超参数并根据需要解冻更多层。

在本章中,我们将介绍以下秘籍:

  • 调整预训练模型
  • 实现模型训练
  • 实现模型测试
  • 加载数据集
  • 定义 TensorBoard 写入器
  • 训练模型和解冻层

技术要求

要完成此秘籍,我们需要 Torch 1.2 或更高版本,强烈建议我们使用支持 CUDA 的设备。

调整预训练模型

在本秘籍中,我们将采用经过预训练的 ResNet 模型,并修改最后一层以适合我们所需的输出。 与 ImageNet 数据集中用于训练 ResNet-50 模型的类的数量相比,我们只需要两个类。 我们将修改 ResNet 模型的最后一个池化层和全连接分类器。 我们将进一步将模型的训练限制为仅添加新添加的分类器单元,并且将保留所有其余层以免更新权重。 这称为冻结模型。 让我们看一下如何实现秘籍。

准备

此秘籍要求我们下载特定的数据集。 我们将从这里获取数据集。 为了完成此秘籍,您的 PyTorch 安装应为 1.2 或更高版本,强烈建议您使用支持 CUDA 的设备。

操作步骤

在本秘籍中,我们将训练我们的神经网络,并从在 ImageNet 数据集上训练的预训练模型 ResNet-50 开始:

  1. 现在,我们将从导入开始编写 Python 代码:
>>import torch
>>import torch.nn as nn
>>import numpy as np
>>import torch.optim as optim
>>from torchvision import transforms, datasets, models, utils
>>import time
>>import numpy as np
>>from torchsummary import summary
>>from torch.utils.data import DataLoader

2.定义AdaptiveConcatPool2d子模块:

>>class AdaptiveConcatPool2d(nn.Module):
    def __init__(self, sz=None):
        super().__init__()
        sz = sz or (1,1)
        self.ap = nn.AdaptiveAvgPool2d(sz)
        self.mp = nn.AdaptiveMaxPool2d(sz)
    def forward(self, x): 
        return torch.cat([self.mp(x), self.ap(x)], 1)

3.定义一个函数以获取模型:

>>def get_model():
    model = models.resnet50(pretrained=True)

4.现在,我们将冻结模型:

for param in model.parameters():
    param.requires_grad = False

5.现在,我们将替换 ResNet 的最后两层并返回模型:

model.avgpool = AdaptiveConcatPool2d()
model.fc = nn.Sequential(
    nn.Flatten(),
    nn.BatchNorm1d(4096),
    nn.Dropout(0.5),
    nn.Linear(4096, 512),
    nn.ReLU(),
    nn.BatchNorm1d(512),
    nn.Dropout(p=0.5),
    nn.Linear(512, 2),
    nn.LogSoftmax(dim=1)
)
return model

有了这个秘籍,我们已经准备好要获取模型的函数。

工作原理

在此秘籍中,我们定义了一个子模块AdaptiveConcatPool2d,该子模块在平均 2D 池和最大 2D 池之间执行级联,以便从卷积层到具有最大特征信息的全连接层的平滑过渡。

然后,我们定义了get_model()函数,该函数首先下载 ResNet-50 模型(本地不可用)并冻结该模型的权重。 通过冻结权重,较低的卷积层不会更新。 然后,我们用AdaptiveConcatPool2d层替换了平均池化层,并为两个可用的类添加了具有两个输出单元的全连接分类器。 我们最终返回了带有冻结的 ResNet 层的模型。

实现模型训练

在本秘籍中,我们将实现一个在单个周期内训练模型的函数。 此函数进一步记录模型的训练指标并将其绘制到 TensorBoard 上。 我们将传入模型,训练数据,优化器和模型训练准则,并将返回训练损失。

操作步骤

我们将在此秘籍中实现训练函数:

  1. 定义训练函数:
>>def train(model, device, train_loader, criterion, optimizer, epoch, writer):
    model.train()

    total_loss = 0   
  1. 迭代训练数据并更新模型权重:

    for batch_id, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)

        optimizer.zero_grad()
        preds = model(data)
        loss = criterion(preds, target)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
  1. 记录并返回训练损失:

    writer.add_scalar('Train Loss', total_loss/len(train_loader), epoch)
    writer.flush()

    return total_loss/len(train_loader)

通过此秘籍,我们完成了训练函数。

工作原理

在此秘籍中,我们定义了执行训练周期的函数。 在开始训练过程之前,我们使用.train()将模型设置为训练模式,并将训练损失设置为0。 然后,我们遍历训练数据并将输入数据点及其对应的标签移到可用设备(CPU 或 GPU)上。

然后,我们清除梯度,进行模型预测,然后将其传递给准则以确定训练损失。 然后,我们对损失进行了反向传播,并更新了模型的权重。 由于模型是冻结的,因此它仅更新模型中分类器的权重。

最后,我们使用 TensorBoard 中SummaryWriter对象中的SummaryWriter对象中的add_scalar()方法在 TensorBoard 中记录了训练指标-我们在其中传递了一个标签,一个标量值和一个计数器,在本例中为周期数 。

实现模型测试

在此秘籍中,我们将定义一个函数以在一个周期内根据验证数据测试模型。 此函数还将测试指标记录到 TensorBoard 上。 我们还将添加工具函数,通过绘制图像并以可读的方式标记这些图像来记录模型中的一些错误分类。

操作步骤

在本秘籍中,我们将实现模型测试以及工具函数:

  1. 首先,我们将定义一个将张量转换为图像的函数:
>>inv_normalize = transforms.Normalize(
        mean=[-0.485/0.229, -0.456/0.224, -0.406/0.225],
        std=[1/0.229, 1/0.224, 1/0.255]
    )
  1. 然后,我们将定义一个函数来记录错误分类的图像:
>>def misclassified_images(pred, writer, target, data, output, epoch, count=10):
    misclassified = (pred != target.data)
    for index, image_tensor in enumerate(data[misclassified][:count]):
        img_name = '{}->Predict-{}x{}-Actual'.format(
                epoch,
                LABEL[pred[misclassified].tolist()[index]],
                LABEL[target.data[misclassified].tolist()[index]], 
            )

        writer.add_image(img_name, inv_normalize(image_tensor), epoch)
  1. 现在,我们将具有一个用于记录指标的函数:
>>def test(model, device, test_loader, criterion, epoch, writer):
    model.eval()

    total_loss, correct = 0, 0

    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            total_loss += criterion(output, target).item()
            pred = output.data.max(1)[1]
            correct += pred.eq(target.data).cpu().sum()

            misclassified_images(pred, writer, target, data, output, epoch)

    total_loss /= len(test_loader)
    accuracy = 100\. * correct / len(test_loader.dataset)
  1. 现在,我们在 TensorBoard 上记录测试指标:

    writer.add_scalar('Test Loss', total_loss, epoch)
    writer.add_scalar('Accuracy', accuracy, epoch)
    writer.flush()

    return total_loss, accuracy

通过此秘籍,我们完成了测试函数。

工作原理

在此秘籍中,我们编写了逆归一化函数以撤消在将图像转换为具有 ImageNet 统计信息的张量时建立的归一化。 我们还定义了misclassified_images()方法,用于记录预测错误的图像。 然后使用SummaryWriter对象中的add_image()方法将分类错误的图像添加到 TensorBoard 中,该方法将输入图像名称,图像和计数器。

然后,我们定义了test()方法,该方法在模型的验证数据集上运行验证,并使用add_scalar()方法记录测试损失和准确率,就像在训练函数中一样。 最后,我们在验证数据集上返回了测试损失和模型准确率。

加载数据集

在此秘籍中,我们将加载肺炎数据集并将其转换为张量。 该模型需要张量形式的数据,因此我们将需要对图像进行预处理以为其提供所需的数据。 我们将执行数据扩充以增加数据集的大小。 在将其输入模型之前,我们还将根据 ImageNet 数据集执行图像归一化。

操作步骤

在此秘籍中,我们将加载数据集:

  1. 首先,我们将定义转换:
>>image_transforms = {

以下代码显示了训练集转换:


    'train':
    transforms.Compose([
        transforms.RandomResizedCrop(size=300, scale=(0.8, 1.1)),
        transforms.RandomRotation(degrees=10),
        transforms.ColorJitter(0.4, 0.4, 0.4),
        transforms.RandomHorizontalFlip(),
        transforms.CenterCrop(size=256), # Image net standards
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225]) # Imagenet standards
    ]),

以下代码显示了验证集转换:


    'val':
    transforms.Compose([
        transforms.Resize(size=300),
        transforms.CenterCrop(size=256),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),

以下代码显示了测试集转换:


    'test':
    transforms.Compose([
        transforms.Resize(size=300),
        transforms.CenterCrop(size=256),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}
  1. 然后,我们将定义图像路径,批量大小,日志路径和保存模型的路径:
>>datadir = '../input/chest-xray-pneumonia/chest_xray/chest_xray/'
>>traindir = datadir + 'train/'
>>validdir = datadir + 'test/'
>>testdir = datadir + 'val/'
>>model_path = "model.pth"
>>batch_size = 128
>>PATH_to_log_dir = 'logdir/'
  1. 接下来,让我们从文件夹中加载图像:
>>data = {
    'train':
    datasets.ImageFolder(root=traindir, transform=image_transforms['train']),
    'val':
    datasets.ImageFolder(root=validdir, transform=image_transforms['val']),
    'test':
    datasets.ImageFolder(root=testdir, transform=image_transforms['test'])
}
  1. 现在,我们将创建迭代器:
>>dataloaders = {
    'train': DataLoader(data['train'], batch_size=batch_size, shuffle=True),
    'val': DataLoader(data['val'], batch_size=batch_size, shuffle=True),
    'test': DataLoader(data['test'], batch_size=batch_size, shuffle=True)
}
  1. 然后,我们将构建类标签:
>>LABEL = dict((v,k) for k,v in data['train'].class_to_idx.items())

有了这个秘籍,我们就可以准备好数据集。

工作原理

在此秘籍中,我们定义了训练,验证和测试数据集中图像所需的转换。 根据数据集选择变换,并且归一化变换中的值来自 ImageNet 统计信息。 然后,我们定义了数据集路径,模型名称,批量大小和日志目录。 然后,我们使用datasets.ImageFolder()方法根据文件夹名称加载数据,并为每个数据集创建一个迭代器。

请注意,我们翻转了验证数据集和测试数据集目录。 这是因为给定数据集的验证数据集确实很小,因此我们将测试数据集用于验证数据集。

我们还使用DataLoader为我们的训练,测试和验证数据集创建了迭代器。 然后,我们创建了一个LABEL常量,这是一个字典,将分类器输出索引映射到类名。

定义 TensorBoard 编写器

在此秘籍中,我们将创建一个写入 TensorBoard 的对象。 我们使用SummaryWriter对象写入 TensorBoard。 我们可以使用 TensorBoard 编写标量值,绘图图和绘图图像以及其他。 我们将定义一个返回 TensorBoard SummaryWriter对象以记录我们的模型指标的函数。

准备

此秘籍要求我们安装 TensorBoard 库。

我们需要安装 TensorBoard 夜间版本:

pip install tb-nightly

这样,我们准备实现该秘籍。

操作步骤

在此秘籍中,我们将创建编写器对象以将数据登录到 TensorBoard。

  1. 我们将在命令行中输入以下内容来加载 TensorBoard:
tensorboard --logdir=log_dir/ --port 6006

您可以通过转到http://localhost:6006/在浏览器上访问 TensorBoard。

  1. 接下来,我们将导入 TensorBoard:
>>from torch.utils.TensorBoard import SummaryWriter
  1. 然后,我们将定义一个函数来获取 TensorBoard 编写器:
>>def tb_writer():
      timestr = time.strftime("%Y%m%d_%H%M%S")
      writer = SummaryWriter(PATH_to_log_dir + timestr)
      return writer
  1. 然后,我们将创建一个图像网格以可视化数据集中的图像:
>>writer = tb_writer()
>>dataiter = iter(dataloaders['train'])
>>images, labels = dataiter.next()
>>grid = utils.make_grid([inv_normalize(image) for image in images[:32]])
>>writer.add_image('X-Ray grid', grid, 0)
>>writer.flush()

这样,我们就为 TensorBoard 做好了准备。

工作原理

在本秘籍中,我们首先从命令行加载 TensorBoard 以从logdir/目录中读取。 即使目录不存在,TensorBoard 也不抛出错误,而是等待目录出现,因此传递正确的目录很重要。

然后,我们将 TensorBoard 导入代码中,并定义了tb_writer()函数,该函数返回一个新的SummaryWriter对象。 我们向writer对象传递了 TensorBoard 日志要保存到的目录名称,并在时间戳timestr的帮助下,确保每个编写器都有一个唯一的目录来写入。

然后,我们使用utils.make_grid()创建了一个图像网格以查看训练数据集中的样本图像,并使用add_image()方法将其传递给 TensorBoard。 我们使用iter(dataloaders['train'])提取了图像,然后选择了 32 个图像的样本并进行反归一化,然后制作了网格。 我们还使用写入器中的flush()方法将缓冲区数据写入存储器。

训练模型和解冻层

在本秘籍中,我们将为数据集的预定义迭代次数完成模型训练。 我们将在模型训练期间保存最佳模型。 一旦针对给定的周期数训练了模型,我们将使用最佳模型的权重加载模型。 然后,我们将解冻模型的先前冻结的 ResNet 层,并训练模型以较低的学习率微调权重。

操作步骤

在本秘籍中,我们将完成模型训练。

  1. 将模型移至可用设备:
>>device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
>>model = get_model().to(device)
  1. 定义条件和优化器:
>>criterion = nn.NLLLoss()
>>optimizer = optim.Adam(model.parameters())
  1. 我们将定义一个函数来跨周期训练我们的模型:
>>def train_epochs(model, device, dataloaders, criterion, optimizer,epochs, writer):
    print('{0:>20} | {1:>20} | {2:>20} | {3:>20} |'.format('Epoch','Training Loss','Test Loss', 'Accuracy'))

    best_score = np.Inf

    for epoch in epochs:

        train_loss = train(model, device, dataloaders['train'], criterion, optimizer, epoch, writer)

       test_loss, accuracy = test(model, device, dataloaders['val'], criterion, epoch, writer)

        if test_loss < best_score:
            best_score = test_loss
            torch.save(model.state_dict(), model_path)

        print('{0:>20} | {1:>20} | {2:>20} | {3:>20.2f}% |'.format(epoch,train_loss,test_loss, accuracy))

        writer.flush()
  1. 现在,我们将训练冻结模型:
>>train_epochs(model, device, dataloaders, criterion, optimizer, range(0,10), writer)
>>writer.close()

这是一个示例输出:

Epoch | Training Loss | Test Loss    | Accuracy |
 0    |  0.3617607994 | 0.4970110774 |  79.17%  |
 1    |   0.233799973 | 0.3870731115 |  84.78%  |
 2    |  0.2014380195 |   0.37044851 |  85.26%  |
 3    |   0.190022166 |  0.362625807 |  86.86%  |
 4    |   0.176903085 |   0.40945090 |  85.90%  |
 5    |   0.163670904 | 0.3690894782 |  86.86%  |
 6    |  0.1607481929 |  0.418265098 |  84.46%  |
 7    |  0.1615160162 | 0.4016072392 |  85.58%  |
 8    |  0.1519727988 |  0.481940734 |  84.13%  |
 9    |  0.1441831755 |  0.433110350 |  84.46%  |
  1. 我们将定义一个函数来解冻模型:
>>def unfreeze(model):
    for param in model.parameters():
        param.requires_grad = True
  1. 到目前为止,我们将加载最佳模型并解冻模型:
>>model.load_state_dict(torch.load(model_path))
>>unfreeze(model)
  1. 我们将更新优化器以降低学习速度:
>>optimizer = optim.Adam(model.parameters(), lr=1e-6)
  1. 我们将训练未冻结的模型:
>>writer = tb_writer()
>>train_epochs(model, device, dataloaders, criterion, optimizer, range(9,14), writer)

这是一个示例输出:

Epoch |        Training Loss |            Test Loss |  Accuracy  |
    9 |  0.15972968554351388 |  0.41342413425445557 |   85.10%   |
   10 |   0.1224460500042613 |   0.3801746487617493 |   86.54%   |
   11 |   0.1217333177422605 |  0.37790409922599794 |   87.18%   |
   12 |  0.11098722713749583 |   0.3712982594966888 |   87.98%   |
   13 |  0.09877484373566581 |  0.41088773012161256 |   86.70%   |
   14 |  0.09256085244620718 |   0.3181425631046295 |   89.42%   |
  1. 我们最终将关闭 TensorBoard 编写器:
>>writer.close()

在以下屏幕截图中,我们可以看到训练 TensorBoard 中查看的模型后生成的图:

图 1:TensorBoard 主仪表板

这是我们创建的图像网格:

图 2:图像网格

这是一个预示肺炎的误分类示例,但这实际上是正常的:

图 3:预计的肺炎,实际上是正常的

这是一个预测正常的误分类示例,但实际上是肺炎:

图 4:预测的正常,实际上是肺炎

这是显示我们的训练损失随时间减少的图:

图 5:训练损失

这是显示我们的测试损失随时间减少的图:

图 6:测试损失

以下图表显示了我们的精度随着时间的推移而提高:

图 7:准确率得分

通过此秘籍,我们已经训练了模型并在 TensorBoard 中可视化了其结果。

工作原理

在此秘籍中,我们创建了模型并将其移至可用设备,并分别使用负对数损失和 Adam 作为我们的标准和优化器。 train_epochs()方法用于在定义的周期范围内训练模型。 在每个周期结束时,我们使用writer.flush()方法来确保所有未决事件已写入磁盘。 最后,我们使用writer.close()刷新关闭编写器。 我们还在此函数中保存了最佳模型,以供日后重新加载。

然后,我们从到目前为止的训练中重新加载了最佳模型,并对其进行了冻结以进行微调。 解冻后,所有模型参数均可用于训练。 我们将优化器设置为较低的学习率,对该未冻结的模型进行了更多的训练,并记录了模型的表现。 我们看到,通过微调,我们的模型表现更好。

从 TensorBoard 图中,我们看到冻结模型中的指标为橙色,解冻后的指标为蓝色,这表明模型解冻后的表现有所提高。 然后,我们从训练数据中采样了图像网格图,并从各个周期对分类错误的示例进行了采样。

我们看到训练和测试损失在各个周期都减少了,准确率提高了。

更多

在此秘籍中,我们可以进一步编写一个函数来确定测试数据集指标,使用add_histogram()方法将直方图添加到 TensorBoard 中,并使用其他预训练的网络训练模型。

另见

有关更多详细信息,请参阅以下内容:

  • 您可以在这个页面上了解有关微调的更多信息。
  • 您可以在这个页面上了解另一个迁移学习示例。
  • 您可以通过这里探索 TensorBoard 函数。

六、探索生成对抗网络

生成对抗网络GAN)是一种机器学习技术,其中同时训练两种模型:一种专门用于创建伪造数据,另一种专门用于区分真实数据和伪造数据。 真实数据。 术语生成反映了以下事实:这些神经网络用于创建新数据,而术语对抗来自以下事实:两个模型相互竞争,从而提高了生成的数据的质量。

GAN 中的两个模型称为生成器和判别器,其中生成器负责创建数据,判别器接收数据并将其分类为真实数据或由生成器生成。 生成器的目标是创建与训练集中的真实数据没有区别的数据样本。

我们可以用一个类比来理解 GAN 的概念,即犯罪分子(产生者)想要伪造金钱,而侦探(判别器)则试图抓住他。 假币的外观越真实,侦探在检测到假币时就必须越好,越高效,这意味着伪钞的质量必须提高到足以使侦探无法发现。

生成器从判别器的分类反馈中学习。 判别器的目标是确定其输入是真实的(来自训练数据集)还是伪造的(来自生成器),因此,每当辨别器将虚假图像分类为真实的错误时,生成器都会获得表现良好的正反馈。 相反,每当判别器正确捕捉到生成器生成的图像为伪造图像时,生成器就会收到需要改进的反馈。

判别器基本上是一个分类器,并且像任何分类器一样,它从真实标签(它在这种情况下是真实的或假的)中了解到其预测有多远。 因此,随着生成器在生成逼真的数据方面变得更好,判别器在从真实标签中辨别伪造品方面也必须变得更好。 这样,两个网络都可以同时改善。

从概念上讲,生成器必须能够从训练示例中捕获真实数据的特征,以使其生成的样本与真实数据无法区分。 生成器自己学习创建模式(而不是识别图像分类问题中的模式)。 通常,生成器的输入通常是随机数的向量。

让我们看一下 GAN 的以下架构图:

在此图中,有一个数据源,其中包含训练图像x,生成器必须捕获其属性并重新创建。 生成器接收随机向量z,该向量充当生成器创建伪图像的种子。 生成器获取种子并生成x*图像,判别器从真实和虚假图像中获取图像,并输出给定​​输入为真实的概率(假设真实图像用 1 表示,伪图像用 0 表示)。 然后,我们获得分类误差,并使用它来迭代训练判别器和生成器。 判别器的目的是使分类误差最小,而生成器的目的是使分类误差最大化。

从理论上讲,生成器和判别器达到平衡,其中生成器已捕获了生成的伪图像中真实图像的所有特征,而从进一步的训练中没有任何收获。 类似地,判别器只能以 50% 的概率猜测图像是伪造的还是真实的,因为这两个图像就其性质而言完全无法区分。 在那个状态下,GAN 已经收敛。 然而,实际上,这种状态很难实现。 在本章中,我们将探讨 GAN 的概念以及 PyTorch 中各种 GAN 的实现。

在本章中,我们将介绍以下秘籍:

  • 创建一个 DCGAN 生成器
  • 创建 DCGAN 判别器
  • 训练 DCGAN 模型
  • 可视化 DCGAN 结果
  • 使用 PyTorch Hub 运行 PGGAN

技术要求

强烈建议,对于本章中实现的秘籍,代码应在具有 NVIDIA GPU 且启用了 CUDA 和 CUDNN 的计算机上运行,​​因为本章中的秘籍需要大量计算。

创建一个 DCGAN 生成器

在本秘籍及其后续秘籍中,我们将实现 DCGAN。 DCGAN 代表“深度卷积 GAN”; 与原始 GAN 相比,它们有很大的改进。 在 DCGAN 中,我们使用卷积神经网络,而不是原始 GAN 中的全连接网络。 在第 3 章,“用于计算机视觉的卷积神经网络”中,我们看到了第 2 章“处理神经网络”中全连接分类器是什么样,是该领域的一项改进; DCGAN 与原始 GAN 的情况相同。 在 DCGAN 中,我们将使用批量归一化,这是一种技术,它可以将作为输入输入到下一层的层的输出归一化。 批量归一化允许网络的每一层独立于其他层进行学习,从而减少了协变量偏移。

批量归一化是通过缩放来实现的,以使平均值为 0,方差为 1。在此秘籍中,我们将生成类似于 MNIST 数据集的手写数字,其中包含来自噪声向量的数据。 我们将扩展此噪声向量,将其转换为 2D 矩阵,最后将其转换为28 x 28黑白图像。 为了增加高度和宽度,我们必须执行与卷积操作相反的操作,这称为反卷积。 我们将在使用卷积执行分类任务时执行此操作。 在执行反卷积时,我们将增加高度和宽度,同时减少通道数。

以下是我们的 DCGAN 生成器的架构图:

请注意,我们将使用第 3 章,“用于计算机视觉的卷积神经网络”中的概念,因此再次阅读这些秘籍将非常有用。

操作步骤

在此秘籍中,我们将实现 GAN 网络的生成器端:

1.我们将从导入开始:

>import torch
>>import torch.nn as nn
>>import torchvision.transforms as transforms

2.然后,我们将定义转换:

>>transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, ), (0.5, )),
])

3.然后,我们将提供该设备:

>>device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

3.现在,我们将定义生成器类:

>>class Generator_model(nn.Module):
    def __init__(self, z_dim):
         super().__init__()

4.然后,我们将定义生成器的单元:

 self.fc = nn.Linear(z_dim, 256 * 7 * 7)
 self.gen = nn.Sequential(
            nn.ConvTranspose2d(256, 128, 4, 2, 1),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.01),
            nn.ConvTranspose2d(128, 64, 3, 1, 1),
            nn.BatchNorm2d(64),
            nn.LeakyReLU(0.01),
            nn.ConvTranspose2d(64, 1, 4, 2, 1),
            nn.Tanh()
        )

5.现在我们将定义forward方法:

def forward(self, input):
    x = self.fc(input)
    x = x.view(-1, 256, 7, 7)
    return self.gen(x)

6.最后,我们将为生成器模型创建对象:

>>generator = Generator_model(z_dim).to(device)

完成此过程后,我们已经准备好 DCGAN 生成器。

工作原理

在此秘籍中,我们进行了变换以将图像转换为张量并对其进行归一化,就像在第 3 章,“用于计算机视觉的卷积神经网络”中所做的一样。 然后,我们确定了机器上的设备:CPU 或 GPU。 然后,我们定义了从nn.Module类继承的Generator_model类,就像在所有以前的架构中所做的一样。

在构造器中,我们传递了z_dim参数,这是我们的噪声向量大小。 然后,我们定义了一个全连接单元self.fc,我们将噪声向量传递给该单元,并为其提供了256 * 7 * 7输出。 然后,我们定义了一个称为self.gennn.Sequential单元,其中包含用于定义生成器的关键组件。 我们使用 PyTorch 中提供的nn.ConvTranspose2dnn.BatchNorm2dnn.LeakyReLU使用一组反卷积,批量规范化和激活层。 ConvTranspose2d接受输入通道,输出通道,核大小,步幅和填充等参数。 BatchNorm2d接受上一层的特征/通道数作为其参数,而 LeakyReLU 接受负斜率的角度。

与 ReLU 不同,LeakyReLU 允许传递小的梯度信号以获取负值。 它使来自判别器的梯度流入生成器。 我们在输出层中使用了 tanh 激活,但是从 DCGAN 论文中我们观察到,使用有界激活可以使模型学会快速饱和并覆盖训练分布的色彩空间。 tanh 的对称性在这里可能是一个优势,因为网络应该以对称方式处理较深的颜色和较浅的颜色。

让我们看一下forward方法的工作方式。 z_dim维度的输入噪声向量经过全连接层以提供 12544 输出。 然后,我们将 12544 输出调整为256 x 7 x 7,其中 256 是通道数。 256 x 7 x 7张量然后通过反卷积层以提供128 x 14 x 14输出,然后通过具有 128 个特征和泄漏 ReLU 的Batchnorm层。 128 x 14 x 14然后在第二次反卷积中转换为64 x 14 x 14张量,在第三次反卷积中变为1 x 28 x 28张量; 这些只是我们需要的尺寸。 然后,我们创建生成器对象并将其移动到设备。

另见

您可以通过这个页面了解更多有关 DCGAN 的信息。

创建 DCGAN 判别器

在本秘籍中,我们将探讨 GAN 网络的鉴别方。 基本上,判别器是在两个类别之间进行分类的分类器,即根据给定图像是来自数据集的真实图像还是由生成器网络生成的伪图像。 正是基于来自判别器网络的反馈,生成器学会了创建更好的图像,以试图使判别器误以为来自生成器的图像是真实的。 现在,在 DCGAN 中,将使用卷积神经网络构建判别器。

以下是我们的判别器的架构图:

准备好

在本秘籍中,我们将严重依赖第 3 章,“用于计算机视觉的卷积神经网络”的秘籍,因此最好快速浏览第 3 章,“用于计算机视觉的卷积神经网络”。

操作步骤

在此秘籍中,我们将构建 GAN 的判别器端:

1.我们将从导入开始:

>>import torch.nn.functional as F

2.然后,我们将定义判别器类:

>>class Discriminator_model(nn.Module):
    def __init__(self):
        super().__init__()

3.接下来,我们定义鉴别单元:

self.disc = nn.Sequential(
            nn.Conv2d(1, 32, 3, 2, 1),
            nn.LeakyReLU(0.01),
            nn.Conv2d(32, 64, 3, 2, 1),
            nn.BatchNorm2d(64),
            nn.LeakyReLU(0.01),
            nn.Conv2d(64, 128, 3, 2, 1),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.01)
        )

4.然后定义最后一个全连接层:

self.fc = nn.Linear(2048, 1)

5.然后定义forward()方法:

def forward(self, input):
        x = self.disc(input)
        return F.sigmoid(self.fc(x.view(-1, 2048)))

6.然后我们创建鉴别对象:

>>discriminator = Discriminator_model().to(device)

现在我们已经准备好判别器。

工作原理

在本秘籍中,我们定义了一个分类器; 使用nn.Sequential()定义卷积,激活和批量规范化单元的数组; 并且还定义了最后一个全连接层,该层采用平坦的张量并给出通过 Sigmoid 层的单个输出。 由于只有两个类,因此我们最后使用了 Sigmoid 层。 输入是尺寸为1 x 28 x 28的图像张量,并经过第一卷积单元以给出尺寸为32 x 14 x 14的输出张量。 第二个卷积层使它成为64 x 7 x 7张量,然后从那里变成128 x 4 x 4。 之后,我们将拉平并使张量穿过全连接层。

另见

您可以在这个页面上了解有关 DCGAN 的信息。

训练 DCGAN 模型

我们在前两个秘籍中定义了生成器,即“创建 DCGan 生成器”和“创建 DCGAN 判别器”。 在本秘籍中,我们将继续训练 GAN 模型。 请记住,生成器的目标是创建与数据集尽可能相似的图像,而判别器的目标是区分真实图像和生成的图像。 从理论上讲,生成器将捕获数据集中图像的所有特征,并且无法学习更多信息,而判别器只能猜测图像是真实的还是生成的。 在本秘籍中,我们将通过整合到目前为止已经创建的生成器和判别器来完成 DCGANs 模型的训练。

准备好

我们将使用torchsummary库来查看我们的模型层,它们的输出形状和它们的参数。 为此,我们将使用以下命令安装该库:

pip install torchsummary

准备好此安装​​后,我们将继续进行下一步。

操作步骤

在此秘籍中,我们将完成 GAN 训练:

  1. 首先导入:
>>from torchsummary import summary
>>import torch.optim as optim
>>import torchvision.utils as vutils
  1. 然后我们初始化生成器和判别器的权重:
>>def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        nn.init.normal_(m.weight.data, 0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        nn.init.constant_(m.bias.data, 0)
>>generator.apply(weights_init)
>>discriminator.apply(weights_init)
  1. 然后我们打印生成器的摘要:
>>summary(generator, (100, ))

这给我们以下输出:

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Linear-1                [-1, 12544]       1,266,944
   ConvTranspose2d-2          [-1, 128, 14, 14]         524,416
       BatchNorm2d-3          [-1, 128, 14, 14]             256
         LeakyReLU-4          [-1, 128, 14, 14]               0
   ConvTranspose2d-5           [-1, 64, 14, 14]          73,792
       BatchNorm2d-6           [-1, 64, 14, 14]             128
         LeakyReLU-7           [-1, 64, 14, 14]               0
   ConvTranspose2d-8            [-1, 1, 28, 28]           1,025
              Tanh-9            [-1, 1, 28, 28]               0
================================================================
Total params: 1,866,561
Trainable params: 1,866,561
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.97
Params size (MB): 7.12
Estimated Total Size (MB): 8.09
----------------------------------------------------------------

  1. 然后,我们将打印判别器摘要:
>>summary(discriminator, (1, 28, 28))

这给我们以下输出:

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 32, 14, 14]             320
         LeakyReLU-2           [-1, 32, 14, 14]               0
            Conv2d-3             [-1, 64, 7, 7]          18,496
       BatchNorm2d-4             [-1, 64, 7, 7]             128
         LeakyReLU-5             [-1, 64, 7, 7]               0
            Conv2d-6            [-1, 128, 4, 4]          73,856
       BatchNorm2d-7            [-1, 128, 4, 4]             256
         LeakyReLU-8            [-1, 128, 4, 4]               0
            Linear-9                    [-1, 1]           2,049
================================================================
Total params: 95,105
Trainable params: 95,105
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.21
Params size (MB): 0.36
Estimated Total Size (MB): 0.58
----------------------------------------------------------------
  1. 接下来,我们将定义损失函数:
>>criterion = nn.BCELoss()
  1. 我们还将创建一个固定的噪声:
>>fixed_noise = torch.randn(64, z_dim, device=device)
  1. 现在,我们将定义优化器函数:
>>doptimizer = optim.Adam(discriminator.parameters())
>>goptimizer = optim.Adam(generator.parameters())
  1. 然后,我们将为判别器设置标签:
>>real_label, fake_label = 1, 0
  1. 我们还将准备存储我们训练中的指标:
>>image_list = []
>>g_losses = []
>>d_losses = []
>>iterations = 0
>>num_epochs = 50
  1. 现在,我们开始训练循环:
>>for epoch in range(num_epochs):
  1. 然后我们遍历数据:
print(f'Epoch : | {epoch+1:03} / {num_epochs:03} |')
for i, data in enumerate(train_loader):
  1. 然后,我们通过清除梯度来开始训练判别器:
discriminator.zero_grad()
  1. 然后我们获取图像:
real_images = data[0].to(device)
size = real_images.size(0)
  1. 然后,我们为这些图像创建标签:
label = torch.full((size,), real_label, device=device)
  1. 接下来,我们得到判别器输出:
d_output = discriminator(real_images).view(-1)
  1. 然后我们计算判别器误差:
derror_real = criterion(d_output, label)
  1. 接下来,我们计算梯度:
derror_real.backward()
  1. 现在我们将创建一个噪声向量:
noise = torch.randn(size, z_dim, device=device)
  1. 接下来,我们将噪声向量传递给生成器:
fake_images = generator(noise)
  1. 然后,我们为生成的图像创建标签:
label.fill_(0)
  1. 然后我们将它们传递给判别器:
d_output = discriminator(fake_images.detach()).view(-1)
  1. 接下来,我们得到误差和梯度:
derror_fake = criterion(d_output, label)
derror_fake.backward()
derror_total = derror_real + derror_fake
  1. 然后,我们更新判别器权重:
doptimizer.step()
  1. 我们通过清除梯度开始训练生成器:
generator.zero_grad()
  1. 然后,我们将标签从假更改为真:
label.fill_(1)
  1. 接下来,我们得到判别器输出:
d_output = discriminator(fake_images).view(-1)
  1. 然后我们计算生成器损失和梯度并更新生成器权重:
gerror = criterion(d_output, label)
gerror.backward()
goptimizer.step()
  1. 然后我们保存了损失:
if i % 50 == 0:
    print(f'| {i:03} / {len(train_loader):03} | G Loss: {gerror.item():.3f} | D Loss: {derror_total.item():.3f} |')
    g_losses.append(gerror.item())
    d_losses.append(derror_total.item())
  1. 然后,我们从固定噪声中保存图像:
if (iterations % 500 == 0) or ((epoch == num_epochs-1) and (i == len(train_loader)-1)):
    with torch.no_grad():
        fake_images = generator(fixed_noise).detach().cpu()
        image_list.append(vutils.make_grid(fake_images, padding=2, normalize=True))

iterations += 1

以下是示例输出:

Epoch : | 001 / 050 |
| 000 / 469 | G Loss: 1.939 | D Loss: 1.432 |
| 050 / 469 | G Loss: 3.920 | D Loss: 0.266 |
| 100 / 469 | G Loss: 3.900 | D Loss: 0.406 |
| 150 / 469 | G Loss: 3.260 | D Loss: 0.230 |
| 200 / 469 | G Loss: 3.856 | D Loss: 0.556 |
| 250 / 469 | G Loss: 4.097 | D Loss: 0.123 |
| 300 / 469 | G Loss: 2.377 | D Loss: 0.416 |
| 350 / 469 | G Loss: 2.984 | D Loss: 0.416 |
| 400 / 469 | G Loss: 3.262 | D Loss: 0.140 |
| 450 / 469 | G Loss: 3.469 | D Loss: 0.849 |
Epoch : | 002 / 050 |
| 000 / 469 | G Loss: 2.057 | D Loss: 0.484 |
| 050 / 469 | G Loss: 2.108 | D Loss: 0.435 |
| 100 / 469 | G Loss: 1.714 | D Loss: 0.862 |
| 150 / 469 | G Loss: 3.902 | D Loss: 0.199 |
| 200 / 469 | G Loss: 3.869 | D Loss: 0.086 |
| 250 / 469 | G Loss: 2.390 | D Loss: 0.208 |
| 300 / 469 | G Loss: 3.008 | D Loss: 0.586 |
| 350 / 469 | G Loss: 4.662 | D Loss: 0.074 |
| 400 / 469 | G Loss: 3.353 | D Loss: 0.368 |
| 450 / 469 | G Loss: 5.080 | D Loss: 0.110 |
Epoch : | 003 / 050 |
| 000 / 469 | G Loss: 7.159 | D Loss: 0.008 |
| 050 / 469 | G Loss: 5.087 | D Loss: 0.056 |
| 100 / 469 | G Loss: 4.232 | D Loss: 0.184 |
| 150 / 469 | G Loss: 5.037 | D Loss: 0.141 |
| 200 / 469 | G Loss: 5.636 | D Loss: 0.570 |
| 250 / 469 | G Loss: 3.624 | D Loss: 0.304 |
| 300 / 469 | G Loss: 4.291 | D Loss: 0.214 |
| 350 / 469 | G Loss: 2.901 | D Loss: 0.247 |
| 400 / 469 | G Loss: 3.703 | D Loss: 0.643 |
| 450 / 469 | G Loss: 1.149 | D Loss: 1.035 |
Epoch : | 004 / 050 |
| 000 / 469 | G Loss: 3.317 | D Loss: 0.202 |
| 050 / 469 | G Loss: 2.990 | D Loss: 0.350 |
| 100 / 469 | G Loss: 2.680 | D Loss: 0.162 |
| 150 / 469 | G Loss: 2.934 | D Loss: 0.391 |
| 200 / 469 | G Loss: 3.736 | D Loss: 0.215 |
| 250 / 469 | G Loss: 3.601 | D Loss: 0.199 |
| 300 / 469 | G Loss: 4.288 | D Loss: 0.164 |
| 350 / 469 | G Loss: 2.978 | D Loss: 0.086 |
| 400 / 469 | G Loss: 3.827 | D Loss: 0.189 |
| 450 / 469 | G Loss: 4.283 | D Loss: 0.216 |
Epoch : | 005 / 050 |
| 000 / 469 | G Loss: 4.456 | D Loss: 0.250 |
| 050 / 469 | G Loss: 4.886 | D Loss: 0.160 |
| 100 / 469 | G Loss: 1.844 | D Loss: 0.447 |
| 150 / 469 | G Loss: 3.680 | D Loss: 0.505 |
| 200 / 469 | G Loss: 4.428 | D Loss: 0.200 |
| 250 / 469 | G Loss: 4.270 | D Loss: 0.222 |
| 300 / 469 | G Loss: 4.617 | D Loss: 0.102 |
| 350 / 469 | G Loss: 3.920 | D Loss: 0.092 |
| 400 / 469 | G Loss: 4.010 | D Loss: 0.392 |
| 450 / 469 | G Loss: 1.705 | D Loss: 0.651 |

至此,我们已经完成了 DCGAN 的训练。

工作原理

我们从weights_init函数开始,该函数用于从均值 0 和标准差 0.02 的正态分布中随机初始化所有权重。 初始化模型后,函数将模型作为输入,并重新初始化所有卷积,卷积转置和批归一化层。

然后,我们使用torchsummary库打印模型的摘要; 为此,我们将模型与输入维一起传递。 方便地查看我们所有的输出尺寸是否正确,并检查每一层中参数的数量和大小。 接下来,我们定义损失函数,并使用二进制交叉熵损失,因为只有一个输出具有两个可能的状态,分别表示图像是真实的 1 还是伪造的 0。

我们还创建了固定噪声,用于可视化 GAN 模型在迭代过程中的改进。 我们使用 ADAM 优化器分别更新了生成器和判别器goptimizerdoptimizer的权重。 然后,我们进行了准备以存储一些模型指标,以查看模型在迭代过程中的变化,然后开始训练循环。

我们遍历了每个小批量并开始训练判别器。 我们仅从 MNIST 数据集中获取图像,然后使用real_images = data[0].to(device)将其移动到设备中; 由于图像全部来自 MNIST 数据集,因此我们知道它们是真实的,因此我们创建了与小批量相同大小的标签向量,并用真实图像标签 1 进行填充。然后,将这些真实图像传递到判别器来预测,然后使用此预测从准则中得出误差derror_real并计算梯度。 然后,我们创建了相等数量的噪声向量,并将它们传递给生成器以生成图像,然后将这些生成的图像传递给判别器以获取预测,然后从准则derror_fake中获取误差。 然后,我们再次进行此操作以计算和累积梯度。 然后,我们从真实图像和伪图像中获得了误差之和,以得出总的判别器误差,并更新判别器的权重。

然后,我们开始训练生成器,并且生成器应该能够欺骗判别器。 生成器必须纠正判别器正确预测生成的图像为假的情况。 因此,只要来自判别器的预测将生成的图像标记为伪造,就会增加生成器损失gerror。 然后,我们计算梯度并更新生成器权重。

然后,我们定期显示模型指标,并保存固定噪声生成器生成的图像,以可视化模型在各个周期的表现。

更多

您可以利用网络的超参数发挥更多的作用-例如,您可以对判别器优化器使用与生成器不同的学习率,或者对判别器的每次更新训练生成器两次或三次。

另见

您可以在 这个页面这个页面上看到训练和架构 DCGAN 的另一个示例。

可视化 DCGAN 结果

以前我们已经看到,使用 GAN,生成器和判别器相互竞争,并且这样做可以产生越来越好的图像。 但是,从理论上讲,它们达到了生成器已捕获真实图像的所有特征的程度,并且生成器无法学习。 同样,判别器只能猜测给定图像是真实的还是伪造的,成功机会为 50/50。 在这一点上,据说 GAN 已经收敛。

现在,任何方面的改善都会导致另一面的结果降低,这是零和状态或纳什平衡; 但是在实践中很难实现,因为生成器和判别器都在不断变化,因此检查 GAN 表现的最佳方法是通过图形和图表。 在本秘籍中,我们将快速了解可视化。

准备好

对于此秘籍,您必须具有matplotlibnumpy库,可以使用pip如下安装它们:

pip install matplotlib
pip install numpy

安装这些工具后,我们将继续进行操作。

操作步骤

在此秘籍中,我们将快速绘制 GAN 中的图形和图像:

  1. 我们将从导入开始:
>>import matplotlib.pyplot as plt
>>import numpy as np
  1. 然后,我们将添加图的大小和标题:
>>plt.figure(figsize=(10,5))
>>plt.title("Generator and Discriminator Loss During Training")
  1. 接下来,我们添加生成器和判别器损失:
>>plt.plot(g_losses,label="Generator")
>>plt.plot(d_losses,label="Discriminator")
  1. 然后,我们添加xy轴标签:
>>plt.xlabel("iterations")
>>plt.ylabel("Loss")
  1. 接下来,我们为图添加图例:
>>plt.legend()
  1. 最后,我们显示该图:
>>plt.show()

这将给出以下输出:

  1. 然后,我们遍历image_list中的图像并显示它们:
>>for image in image_list:
    plt.imshow(np.transpose(image,(1,2,0)))
    plt.show()

结果为以下输出:

在这里,我们看到了 DCGAN 生成的手写图像。

工作原理

在本秘籍中,我们使用 matplotlib 绘制图形和图像。 我们使用figure()title()方法设置图形尺寸和标题,然后使用plot()方法绘制生成器和判别器损失。 我们还使用xlabelylabel方法添加了xy标签。 我们还使用legend()方法为图添加了图例,最后使用show()方法显示了图。

我们遍历训练期间保存在image_list中的图像,并使用 NumPy 的transpose()方法以所需顺序固定图像的尺寸。 image_list中的图像是使用torchvision.util.make_grid()方法生成的,我们根据噪声向量创建了生成图像的网格。

更多

您可以使用其他库(例如 plotly 和 seaborn)来绘制和美化图形。

另见

您可以在这个页面上看到 DCGAN 的可视化效果和动画。

使用 PyTorch Hub 运行 PGGAN

在本秘籍中,我们将研究渐进 GANPGGAN),与 DCGAN 相比它们是高级 GAN,并且能够生成逼真的图像。 PGGAN 分多个阶段训练 GAN 网络。 它具有z的潜在特征,并使用两个反卷积层生成4×4图像。 在判别器方面,网络使用两个卷积层训练生成的4 x 4图像。 网络稳定后,它会在判别器中再增加两个卷积层以将图像上采样到8 x 8,再增加两个卷积层以对图像下采样。

经过 9 个这样的序列后,生成器将生成1024 x 1024个图像。 PGGAN 的渐进式训练策略相对于常规 GAN 具有优势,因为它可以加快并稳定训练。 之所以如此,是因为大多数训练都是在较低的分辨率下进行的,而在网络达到各个阶段的稳定性之后,会逐渐发展为较高的分辨率。

以下是 PGGAN 的抽象表示形式:

PGGAN 的关键创新可以总结如下:

  • 高分辨率层中的逐渐增长和平滑淡化:它从低分辨率卷积变为高分辨率卷积,而不是立即跳变分辨率,而是通过参数平滑地淡化了具有更高分辨率的 nedonew 层α(介于 0 和 1 之间)的大小,用于控制我们使用旧的还是放大的较大输出。
  • 小批量标准差:我们计算判别器的统计信息,即生成器生成或来自真实数据的小批量中所有像素的标准差。 现在,判别器需要了解,如果要评估的批量中图像的标准差较低,则该图像很可能是伪造的,因为真实数据的方差更高。 因此,生成器必须增加所生成样本的方差,以欺骗判别器。
  • 均衡的学习率:所有权重(w)被归一化(w')在某个范围内,以便w' = w / c的常数c对于每一层来说都是不同的,具体取决于权重矩阵的形状。
  • 逐像素特征归一化:对每个像素中的特征向量进行归一化,因为批量规范最适合大型微型批量,并且占用大量内存。

Nvidia 最初执行 PGGAN 的过程要花一到两个月的时间。 但是,为了让我们看到 PGGAN 的表现,我们将使用 PyTorch Hub,它是使用 PyTorch 构建的预训练模型的存储库。

准备

为了使割炬轮毂正常工作,您需要拥有 PyTorch 版本 1.1.0 或更高版本。

操作步骤

在此秘籍中,我们将从火炬中心运行 PGGAN:

  1. 首先,我们将设置导入:
>>import torch
>>import matplotlib.pyplot as plt
>>import torchvision
  1. 然后我们检查 GPU:
>>use_gpu = True if torch.cuda.is_available() else False
  1. 接下来,我们加载预训练的 PGGAN 模型:
>>model = torch.hub.load('facebookresearch/pytorch_GAN_zoo:hub', 'PGAN', 
                       model_name='celebAHQ-512',
                       pretrained=True, 
                       useGPU=use_gpu)
  1. 然后我们产生噪音:
>>num_images = 5
>>noise, _ = model.buildNoiseData(num_images)
  1. 接下来,我们获得生成器输出
>>with torch.no_grad():
    generated_images = model.test(noise)
  1. 最后,我们制作图像网格并显示它:
>>grid = torchvision.utils.make_grid(generated_images.clamp(min=-1, max=1), scale_each=True, normalize=True)
>>plt.imshow(grid.permute(1, 2, 0).cpu().numpy())

在这里,我们看到了从 PGGAN 生成的面部图像:

通过前面的步骤,我们学习了如何使用 PyTorch Hub 运行 PGGAN。

工作原理

在此秘籍中,我们加载了在celebAHQ数据集上训练的 PGGAN 预训练模型; 为此,我们使用了torch.hub中的load()方法。 然后,我们定义了创建和生成尺寸为num_images x 512的噪声向量所需的图像数量,因为此模型使用大小为 512 的噪声向量进行训练,所有噪声向量都由buildNoiseData()方法内部处理,它在模型对象中可用。

model.test()方法生成了我们用来制作网格的图像。 钳位方法将所有值限制在minmax定义的范围内。 .cpu()方法将生成的图像移至 CPU,我们使用permute固定尺寸。 最后,plt.imshow()显示了我们创建的网格。

更多

您可以在这里探索完整的 PGGAN 实现。

另见

您可以在这个页面上了解有关割炬轮毂的更多信息。

您可以在这里这里

七、深度强化学习

在本章中,我们将探索神经网络NNs)在使用 PyTorch 进行强化学习RL)上的应用。

RL 是人工智能AI)的领域,与我们在前面各章中介绍的其他机器学习格式不同。 RL 是机器学习算法的子类,它通过最大化环境中的奖励来进行学习。 当问题涉及决策或采取行动时,这些算法很有用。

在本章中,我们将介绍以下秘籍:

  • OpenAI Gym 简介– CartPole
  • DQN 简介
  • 实现 DQN 类
  • 训练 DQN
  • 深度 GA 简介
  • 生成智能体
  • 选择智能体
  • 使智能体突变
  • 训练深度 GA

深入了解 RL

智能体是任何 RL 问题的核心。 它是 RL 算法的一部分,它处理输入信息以执行操作。 它探索和利用重复试验中的知识,以学习如何最大化奖励。 智能体必须处理的场景称为环境,而动作是智能体可以在给定环境中进行的动作。 采取行动后从环境中获得的回报称为奖励,智能体根据当前状态决定使用下一行动的行动过程称为策略。 相对于短期奖励,带有折扣的预期长期回报称为Q 值与该值相似,但具有附加的当前动作参数。

现在,我们已经了解了 RL 的上下文,让我们理解为什么我们应该使用深度 RLDRL)而不是 RL。 DRL 将 RL 与深度学习相结合,后者使用 NN 解决 RL 问题。

深度学习算法可以学习抽象出环境状态的细节,然后学习状态的重要特征。 由于深度学习算法仅具有有限数量的参数,因此我们可以使用它将可能的状态压缩为更少的状态,然后使用该新表示形式来选择一个动作。

RL 解决方案涉及将反复试验的结果存储在查找表中,当环境变得越来越复杂时,查找表将变得非常庞大。 深度神经网络可能会学会识别程序员自己必须使用查找表方法手动设计的相同高级特征。

深度学习是 RL 最近取得突破的背后。 它们展现了代表性的功能,效率,灵活性,并为眼前的问题提供了简化的解决方案。

OpenAI Gym 简介 – CartPole

在本秘籍中,我们将实现两种不同的 RL 算法。 我们将需要一个环境来运行我们的算法,以便我们创建的模型将获得最大的回报。

我们将使用 OpenAI 的体育馆库,该库是我们可以用来训练模型的环境的集合。 我们将专注于称为Cartpole-v1的特定环境。

杆子是一个倒立的摆锤,其重心高于其枢轴点。 为了控制倒立摆的不稳定位置,我们必须将枢轴点移动到质心下方。 目的是施加适当的力,以使柱杆在枢轴点上保持平衡。

下图显示了 OpenAI Gym 的 Cartpole:

图 1:OpenAI Gym– Cartpole

撑杆通过未激活的接头连接到推车,该接头在水平轨道上自由移动。 摆锤垂直于水平轨道开始。 目的是通过施加力 +1 和 -1 来防止跌落。

当杆与垂直位置的夹角超过 15 度时,或者手推车从中心移出的单位超过 2.4 个单位时,则认为手推车未能达到目标。

对于杆保持直立的每个时间步,都会获得+1 的奖励。 现在我们有了上下文,我们将尝试解决 OpenAI Gym 的 Cartpole 问题的代码。

准备

首先,我们需要安装gym。 我们将使用pip管理器安装它:

pip install gym

现在我们已经安装了gym,让我们跳入gym库。

操作步骤

在本秘籍中,我们将了解棘突环境。 请按照以下步骤操作:

  1. 我们将从导入gym模块开始:
>>import gym
  1. 接下来,我们将创建环境:
>>env = gym.make('CartPole-v0')
  1. 接下来,我们将重置环境:
>>env.reset()
  1. 现在,我们将渲染环境:
>>for _ in range(1000):
    env.render()
    action = env.action_space.sample()
    observation, reward, done, info = env.step(action)
    if done:
        env.reset()
>>env.close()

这将为我们提供以下输出:

图 2:柱极环境的输出

  1. 获取可以执行的可能动作的数量:
>>env.action_space.n
2
  1. 获取观察状态参数的数量:
>>env.observation_space.shape[0]
4

这样,您将看到一个窗口,该窗口显示不稳定的枢轴,并且枢轴点随机移动。

工作原理

在本秘籍中,我们探讨了 OpenAI Gym 的一些功能。 我们首先使用Cartpole-v0创建一个环境,该环境的最高可得分为 200,然后环境终止。 我们使用env.reset()命令将环境置于初始状态,即线杆处于直立位置。 然后,我们开始执行 1000 个步骤的循环,其中使用render()渲染了当前环境的开始,并且使用env.action_space.sample()为当前状态选择了一个随机动作。 然后,我们将选定的动作传递到环境的step方法中。 step方法告诉我们在对环境的当前状态执行当前操作时环境发生了什么。 step方法返回以下内容:

  • 观察:这是一个对象,它告诉我们有关环境的新状态。 该对象特定于我们选择的环境。
  • 奖励:这给了我们所选动作所获得的奖励。 如果是直杆,则直杆直立时每个时间步长为 1.0,否则为 0.0。
  • 完成:这是一个布尔值,它告诉我们环境是否已达到最终状态,可能是由于手头的任务失败而导致的(如果是小刀杆,则是当刀杆无法保持直立位置时) ,或者从完成手头的任务开始(对于小偷来说,就是达到最大时间步长 200 的时间)。
  • 信息:对调试有用的诊断信息。

在循环中,每当极点翻倒时,我们都会将环境重置为其初始状态。

最后,我们关闭了环境。 我们使用env.action_space.n查看了环境中可能采取的行动,并使用env.observation_space.shape[0]查看了处于观察状态的参数。 现在我们已经了解了环境,我们可以开始实现各种深度 RL 算法。

更多

您可以通过更改环境名称来尝试其他环境。 试用Cartpole-v1对您来说将是有益的。

另见

您可以在这个页面上了解有关 OpenAI Gym 的更多信息。

DQN 介绍

在进入下一个秘籍之前,让我们快速看一下 DQN。

DQN 是一种 RL 技术,旨在为给定的观察选择最佳的动作。 有一个 Q 值,它是与每个可能观察到的每个可能动作相关联的给定移动的质量。 在传统的 RL 算法中,此 Q 值来自 Q 表,该表是一个查找表,其中是包含 Q 值的表。 通过反复玩游戏并使用奖励更新表来迭代更新此查找表。 q 学习算法学习要在此表中填充的最佳值。 我们可以简单地查看给定状态的表并选择具有最大 Q 值的动作,以最大程度地赢得游戏。

Q 值可以更新如下:

新的 Q 值是两个部分的总和。 第一部分是(1 - 学习率) * 旧 Q 值,它是多少旧值将被记住。 学习率为 0 时,不会学习任何新内容,学习率为 1 时,所有旧值都将被忘记。

第二部分是学习率*(即时行动奖励+最优未来值的折算估计),其中学习值是即时奖励加上最优未来值的折算估计。 未来奖励的重要性由折扣系数决定。

通过深度 Q 学习,我们可以使用深度神经网络来预测动作的 Q 值,并使用深度神经网络来选择动作,而不是使用 Q 表查找给定状态下具有最大可能 Q 值的动作。 给定动作的最大 Q 值。

操作步骤

在本秘籍中,我们将为卡特彼勒问题定义神经网络模型。 请按照以下步骤操作:

  1. 首先,我们将导入torch
>>import torch
>>import torch.nn as nn
  1. 接下来,定义一个函数来返回模型:
def cartpole_model(observation_space, action_space):
    return nn.Sequential(
        nn.Linear(observation_space, 24),
        nn.ReLU(),
        nn.Linear(24, 24),
        nn.ReLU(),
        nn.Linear(24, action_space)
    )

此函数返回模型。

工作原理

在此秘籍中,我们定义了一个名为cartpole_model的函数,该函数接受observation_ spaceaction_space参数,并返回一个神经网络模型。 在这里,我们使用了torch.nnnn.Linearnn.ReLU中的Sequential模块来完成模型。 给定一个观察值,我们使用该模型来训练和预测每个动作的 Q 值。

更多

我们还可以训练将状态作为图像并学习从图像预测 Q 值的模型。 完成此操作后,我们将使用nn.Conv2d()来使用卷积神经网络。

另见

您可以在这个页面上查看替代架构。

实现 DQN 类

在本秘籍中,我们将使用神经网络完成 DQN。 为此,我们将执行一些关键任务,包括创建目标和策略网络,损失函数和网络优化器,存储学习过程的状态和奖励,预测行为,经验回放以及控制学习过程。 探索率。

准备

在完成本秘籍之前,您应该完成本章的“OpenAI Gym 介绍 – Cartpole”秘籍,以便设置gym包。

操作步骤

在本秘籍中,我们将研究可用于执行 DQN 的所有关键功能。 按着这些次序:

  1. 我们将从必要的导入开始:
>>import random
>>from collections import deque
>>import numpy as np
>>import torch.optim as optim
  1. 接下来,我们将定义 DQN 类:
>>class DQN:
  1. 然后,我们将定义构造器:
>>def __init__(self, observation_space, action_space):
        self.exploration_rate = MAX_EXPLORE
        self.action_space = action_space
        self.observation_space = observation_space
        self.memory = deque(maxlen=MEMORY_LEN)
  1. 接下来,我们将定义target_netpolicy_net
self.target_net = cartpole_model(self.observation_space, self.action_space)
self.policy_net = cartpole_model(self.observation_space, self.action_space)
  1. 现在,我们将复制权重:
self.target_net.load_state_dict(self.policy_net.state_dict())
self.target_net.eval()
  1. 在这里,我们定义损失函数,优化器和限制标志:
self.criterion = nn.MSELoss()
self.optimizer = optim.Adam(self.policy_net.parameters())

self.explore_limit = False
  1. 接下来,我们将定义load_memory方法:
>>def load_memory(self, state, action, reward, next_state, terminal):
        self.memory.append((state, action, reward, next_state, terminal))
  1. 现在,我们将定义predict_action方法:
>>def predict_action(self, state):
        random_number = np.random.rand()

        if random_number < self.exploration_rate:
            return random.randrange(self.action_space)

        q_values = self.target_net(state).detach().numpy()
        return np.argmax(q_values[0])
  1. 现在,我们将跳至experience_replay方法:
>>def experience_replay(self):
        if len(self.memory) < BATCH_SIZE:
            return

        batch = random.sample(self.memory, BATCH_SIZE)
  1. 现在,让我们使用批量更新 Q 值:
for state, action, reward, next_state, terminal in batch:
    q_update = reward
    if not terminal:
        q_update = reward + GAMMA * self.target_net(next_state).max(axis=1)[0] 

    q_values = self.target_net(state)
    q_values[0][action] = q_update
  1. 接下来,我们计算损失并更新权重:
loss = self.criterion(self.policy_net(state), q_values)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
  1. 我们还将更新探索率:
if not self.explore_limit:
    self.exploration_rate *= EXPLORE_DECAY
    if self.exploration_rate < MIN_EXPLORE:
        self.exploration_rate = MIN_EXPLORE
        self.explore_limit = True

至此,我们完成了 DQN 课程。

工作原理

在本秘籍中,我们完成了 DQN 类,并添加了所有必需的函数来训练 DQN。 在构造器中,我们初始化了探索的初始状态,观察空间和动作空间,然后定义了一个存储单元来保存 DQN 的经验。 我们创建了称为policy_nettarget_net的软骨模型的两个实例。 我们需要两个网络,因为在训练的每个步骤中,Q 网络的值都会移动,并且如果我们使用不断变化的目标值来调整我们的网络,则该网络可能会由于陷入此变化的目标与估计的 Q 值之间的反馈回路而变得不稳定。 网络值。 如果发生这种情况,值估计将失去控制。 因此,我们使用了两个网络并将target_net保持在eval模式。 然后,我们使用MSELoss()作为损失函数以及Adam优化器来更新权重。

load_memory()方法中,我们从环境中存储了状态,操作,奖励,下一个状态和终端,以用于训练网络。 我们使用的下一个方法是predict_action。 在此方法中,我们使用np.random.rand()选择了random_number,这为我们提供了[0,1)的值。 如果此random_number小于当前的exploration_rate,则我们选择一个随机动作,该动作由exploration_rate控制。 这就是我们合并探索的方式。 但是,如果random_number大于exploration_rate,则target_net会预测q_values并选择具有最大 Q 值的动作。

最后,我们实现了experience_replay方法。 在这里,我们等待数据点的数量至少为BATCH_SIZE,然后从内存中随机采样一批。 这使它可以从一系列不同的观察中学习,而不是从一系列紧密相关的观察中学习。 遍历批量时,我们使用target_net根据 Q 值更新公式更新了 Q 值。 然后,我们根据新 Q 值与policy_net预测的 Q 值之间的误差训练了policy_net。 之后,通过将其乘以探索衰减量逐渐降低探索速率,直到获得最低探索速率。 我们这样做是因为,在训练的初始阶段,我们希望智能体进行更多探索; 但是,随着训练的进行,我们希望算法能够收敛。 至此,我们已经完成了 DQN 类的所有函数。

更多

您可以在 DQN 类中添加一种方法,以用策略网络更新目标网络的权重。

另见

您可以在这个页面上的 Keras 中查看此实现。

训练 DQN

在此秘籍中,我们将完成训练 DQN 增强算法的过程,并在训练模型后可视化我们的小车。 我们将使用 DQN 类来预测操作并将该操作应用于环境以获得奖励。 此秘籍的目的是使奖励最大化。 我们将使用经验回放来训练我们的模型以预测 Q 值。

操作步骤

在本秘籍中,我们将继续使用“DQN 类”秘籍。 按着这些次序:

  1. 首先,我们需要导入gym
>>import gym
  1. 接下来,我们需要初始化常量和环境:
>>ENV_NAME = "CartPole-v1"
>>BATCH_SIZE = 20
>>GAMMA = 0.95
>>LEARNING_RATE = 0.001
>>MAX_EXPLORE = 1.0
>>MIN_EXPLORE = 0.01
>>EXPLORE_DECAY = 0.995
>>MEMORY_LEN = 1_000_000
>>UPDATE_FREQ = 10
  1. 现在,我们需要初始化环境和 DQN:
>>env = gym.make(ENV_NAME)
>>observation_space = env.observation_space.shape[0]
>>action_space = env.action_space.n
>>dqn = DQN(observation_space, action_space)
  1. 现在,让我们开始训练循环:
>>for i in range(100):
    state = env.reset()
    state = np.reshape(state, [1, observation_space])
    state = torch.from_numpy(state).float()
  1. 接下来,我们需要逐步了解环境:
score = 0
while True:
    score += 1
    action = dqn.predict_action(state)
    next_state, reward, terminal, info = env.step(action)
  1. 在这里,我们必须找到下一个状态:
next_state = torch.from_numpy(np.reshape(next_state, [1, observation_space])).float()
dqn.load_memory(state, action, reward, next_state, terminal)
state = next_state
  1. 我们将使用以下代码结束无限循环:
if terminal:
    print(f'| {i+1:02} | {dqn.exploration_rate:.4f} | {score:03} |')
    break    
  1. 接下来,我们需要执行一次经验回放:
dqn.experience_replay()
  1. 接下来,更新权重:
if steps%UPDATE_FREQ == 0:
    dqn.target_net.load_state_dict(dqn.policy_net.state_dict())

以下代码块显示了一些示例输出:

| Run | Exploration Rate | Score |
| 001 |       0.9416     | 032   |
| 002 |       0.8956     | 011   |
| 003 |       0.8061     | 022   |
| 004 |       0.7477     | 016   |
| 005 |       0.6936     | 016   |
| 006 |       0.6498     | 014   |
| 007 |       0.5371     | 039   |
.
.
| 072 |       0.0100     | 256   |
| 073 |       0.0100     | 227   |
| 074 |       0.0100     | 225   |
| 075 |       0.0100     | 238   |
| 076 |       0.0100     | 154   |
| 077 |       0.0100     | 285   |
.
.
.
  1. 现在,我们将定义一个函数,该函数将可视化小车的表现:
>>def play_agent(dqn, env):
    observation = env.reset()
    total_reward=0

我们需要使用以下代码来迭代最多 500 个步骤:


    for _ in range(500):
        env.render()
        observation = torch.tensor(observation).type('torch.FloatTensor').view(1,-1)
        q_values = dqn.target_net(observation).detach().numpy()
        action = np.argmax(q_values[0])
        new_observation, reward, done, _ = env.step(action)
        total_reward += reward
        observation = new_observation

        if(done):
            break

最后,关闭环境:


    env.close()
    print("Rewards: ",total_reward)

调用play_agent()函数:

>>play_agent(dqn, env)
Rewards: 160.0

这样,我们就对 DQN 进行了训练和可视化。

工作原理

在本秘籍中,我们首先使用超参数导入并初始化环境。 然后,我们创建了DQN类的实例,并开始训练循环,重置环境,并对状态数组进行整形,以便可以将其作为浮点张量输入到模型中。 然后,我们开始了一个无限循环,当env.step()方法的返回值将terminal设置为True时终止。 predict_action()方法预测在给定环境当前状态下要采取的措施。 然后,我们使用环境的step()方法应用了此操作。 我们采用step方法返回的下一个状态,并将其从numpy转换为torch.FloatTensor,并保存了环境参数。 我们一次又一次地将此新状态传递给模型。 我们还每隔几步就将权重从我们的策略网复制到目标网。

最后,我们编写了一个简单的play_agent函数来可视化平衡并在重置环境后运行循环。 我们要求目标网络预测每个可能动作的 Q 值,选择具有最高 Q 值的动作,并使用step()将其导入环境。 之后,我们不断增加奖励。 此函数返回柱状体直立的时间步数和柱状体执行平衡动作的视频。

更多

您可以编写一个函数来绘制算法的表现,并且仅在模型的得分始终在 450-500 之间时才停止训练过程。

另见

您可以在这里的 Keras 中看到此实现。

您可以在这个页面上看到替代实现。

Deep GA 简介

在本秘籍中,我们将探讨深层遗传算法Deep GA),并向您展示当将其应用于 RL 时,它是基于梯度方法的竞争替代品。 我们将使用一组随机生成的网络,而不是使用梯度下降来修改其权重的随机生成的网络,而是创建一个一代,然后对其在给定环境中的表现进行评估。 请注意,一代人中的某些网络将比其他人表现更好。 我们将选择表现最佳的网络,并将其保留给下一代网络。 然后,我们将通过复制它们来创建下一代并对其权重进行随机修改。 由于我们将选择权重较小的最佳网络,因此网络的整体表现将不断提高。

在本秘籍中,我们将定义模型的网络维度,该网络维度将用于预测给定状态时应采取的措施。

操作步骤

在本秘籍中,我们将完成网络模型定义。 您需要安装 OpenAI 的体育馆库。 按着这些次序:

  1. 我们将从导入开始:
>>import torch.nn as nn
>>import torch
  1. 现在,让我们定义一个返回神经网络的函数:
>>def cartpole_model(observation_space, action_space):
    return nn.Sequential(
        nn.Linear(observation_space, 128),
        nn.ReLU(),
        nn.Linear(128, action_space),
        nn.Softmax(dim=1)
    )

这样,我们就完成了模型的定义。

工作原理

在此秘籍中,函数处于观察状态,并通过两个线性层和一个 ReLU 单元(在隐藏层中有 128 个单元)运行它。 最后一层的输出通过 softmax 函数传递,以将激活转换为概率,并选择概率最高的动作。

更多

对于复杂模型,您还可以具有多个层和不同数量的单元。

另见

您也可以将复杂的网络与卷积层一起使用。 在这个页面中显示了一个示例。

生成智能体

在本秘籍中,我们将着眼于创建一组智能体以开始我们的进化过程,然后初始化这些智能体的权重。 我们将使用这些智能体来评估模型的表现并生成下一代智能体。

操作步骤

在此秘籍中,我们将创建给定数量的智能体。 按着这些次序:

  1. 首先,我们将定义一个函数来初始化智能体的权重:
>>def init_weight(module):
    if((type(module) == nn.Linear)):
            nn.init.xavier_uniform_(module.weight.data)
            module.bias.data.fill_(0.00)
  1. 现在,我们将定义一个将创建智能体的函数:
>>def create_agents(num_agents, observation_space, action_space):
      agents = []
  1. 接下来,我们将创建num_agents个智能体:
for _ in range(num_agents):
    agent = cartpole_model(observation_space, action_space)
    agent.apply(init_weight)
  1. 我们将关闭智能体的每个层的梯度:
for param in agent.parameters():
    param.requires_grad = False

agent.eval()
agents.append(agent)
  1. 最后,我们将返回智能体:
   return agents

现在,我们的智能体已准备好进行评估。

工作原理

在此秘籍中,我们编写了两个函数-第一个函数初始化模型层的权重。 对于模型权重,我们使用torch.nn.init中的xavier_uniform并用0填充偏差。 第二个函数创建num_agents个智能体,并使用cartpole_model()函数返回它们。 我们使用init_weight初始化权重。 然后,对于模型的参数,我们禁用梯度计算,将智能体设置为eval()模式,然后返回所有智能体。

另见

您可以在这个页面上找到有关其他初始化方法的信息。

选择智能体

在本秘籍中,我们将基于适应度函数着眼于智能体选择,在我们的案例中,这意味着在平衡卡特波勒方面得分很高。 这意味着我们将传播得分最高的智能体,而忽略其余的智能体。 我们将评估给定一代中的每个智能体,并多次评估它们,以确保奖励不是偶然的。 最后,我们将使用每个智能体的平均分数来确定表现最佳的智能体。

操作步骤

在本秘籍中,我们将编写用于评估智能体,多次评估以及按给定序列评估所有智能体的函数。 按着这些次序:

  1. 我们将从导入开始:
>>import numpy as np
  1. 接下来,我们需要定义一个函数来评估智能体的表现:
>>def eval_agent(agent, env):
    observation = env.reset()
  1. 接下来,我们需要在最大可能的时间步长内运行循环:
total_reward = 0
for _ in range(MAX_STEP):
    observation = torch.tensor(observation).type('torch.FloatTensor').view(1,-1)
    action_probablity = agent(observation).detach().numpy()[0]
    action = np.random.choice(range(env.action_space.n), 1, p=action_probablity).item()
    next_observation, reward, terminal, _ = env.step(action)
    total_reward += reward
    observation = next_observation

    if terminal:
        break
return total_reward
  1. 然后,我们需要定义智能体的平均得分:
>>def agent_score(agent, env, runs):
    score = 0
    for _ in range(runs):
        score += eval_agent(agent, env)

    return score/runs 
  1. 最后,我们评估所有智能体的分数:
>>def all_agent_score(agents, env, runs):
    agents_score = []
    for agent in agents:
        agents_score.append(agent_score(agent, env, runs))

    return agents_score

现在,我们的函数已准备好进行评估。

工作原理

在本秘籍中,我们完成了深度遗传算法的一些关键函数。 我们研究了三个不同的函数-第一个函数eval_agent()与我们在“训练 DQN”秘籍中看到的函数非常相似,其中我们使用了智能体,该智能体是一种神经网络模型,它预测要采取的动作,并执行到MAX_STEP(对于cartpole-v1为 500)或终端为True并返回分数的动作。

然后,我们使用第二个函数agent_score()返回指定数量runs之上的平均分数,并返回该平均分数,以确保模型的随机表现不佳。 最后一个函数all_agent_score()仅循环遍历一代中的所有智能体,并获得一代中所有智能体的平均分数。

使智能体突变

在本秘籍中,我们将介绍使智能体突变。 在从给定的一代中选择表现最佳的模型之后,然后再创建下一代智能体,我们将对这些选定智能体的权重进行轻微的随机变化,这使智能体可以探索更多区域以获得更好的回报,就像生物进化的工作原理一样。

操作步骤

在此秘籍中,我们将识别精英智能体并向这些智能体添加突变。 按着这些次序:

  1. 首先,我们将导入copynumpy模块:
>>import copy
>>import numpy
  1. 接下来,我们将定义mutation函数:
>>def mutation(agent):
    child_agent = copy.deepcopy(agent)
  1. 接下来,我们将遍历智能体的参数:
for param in agent.parameters():
    mutation_noise = torch.randn_like(param) * MUTATION_POWER
  1. 然后,我们将变异噪声添加到参数中:
    param += mutation_noise
return child_agent
  1. 现在,定义elite函数:
>>def elite(agents, top_parents_id, env, elite_id=None, top=10):
    selected_elites = top_parents_id[:top]
    if elite_id:
        selected_elites.append(elite_id)

    top_score = np.NINF
    top_id = None
  1. 接下来,找到elite智能体:
for agent_id in selected_elites:
    score = agent_score(agents[agent_id], env, runs=5)
    if score > top_score:
       top_score = score
       top_id = agent_id

return copy.deepcopy(agents[top_id])
  1. 获取子智能体:
>>def child_agents(agents, top_parents_id, env, elite_id=None):
    children = []

    agent_count = len(agents)-1    
    selected_agents_id = np.random.choice(top_parents_id, agent_count)
    selected_agents = [agents[id] for id in selected_agents_id]
    child_agents = [mutate(agent) for agent in selected_agents]

    child_agents.append(elite(agents, top_parents_id, env))
    elite_id = len(child_agents)-1

    return child_agents, elite_id
  1. 获取顶级父级:
>>def top_parents(scores, num_top_parents):
    return np.argsort(rewards)[::-1][:num_top_parents]

在这里,我们定义了识别精英智能体的函数和为智能体增加噪音的函数。

工作原理

在本秘籍中,我们研究了四个不同的函数-mutation函数为一个智能体创建一个重复项,并为每个参数附加一个受MUTATION_POWER限制的小的随机值。 rand_like方法从间隔[0, 1)上以与param相同的大小从均匀分布返回具有随机值的张量。 最后,该函数返回突变的子智能体。 接下来,我们看到elite函数返回表现最佳的智能体中最佳智能体的副本。 在elite函数中,我们重新评估智能体以确保得分最高的智能体被选为精英,并作为子智能体传递给下一代。

child_agent函数生成的子智能体数量与上一代相同,其中一个子智能体是elite函数的精英智能体,其余子智能体则使用np.random.choice随机选择。 selected_agents保留了表现最佳的选定智能体的列表。 在[mutate(agent) for agent in selected_agents]步骤中,使用mutation函数对得分最高的智能体进行突变。

然后,我们将精英智能体附加到下一代智能体。 最后,top_parent函数返回一代中表现最高的智能体的索引。

训练深度 GA

在本秘籍中,我们将完成深度遗传算法的演化,并可视化执行平衡操作的关键。 我们将使用在本章的秘籍中了解到的所有函数,并针对给定的代数运行这些函数。 这将创建智能体,获取他们的分数,选择表现最佳的智能体,并将其突变为下一代。 在几代人中,我们将看到智能体的分数增加。

操作步骤

按着这些次序:

  1. 首先,我们将导入gym
>>import gym
  1. 接下来,我们将声明超参数:
>>ENV_NAME = "CartPole-v1"
>>MAX_STEP = 500
>>MUTATION_POWER = 0.02
>>num_agents = 500
>>num_top_parents = 20
>>generations = 25
>>elite_agent = None
  1. 之后,我们将创建环境并禁用梯度计算:
>>torch.set_grad_enabled(False)
>>env = gym.make(ENV_NAME)
  1. 现在,创建智能体:
>>agents = create_agents(num_agents, env.observation_space.shape[0], env.action_space.n)
  1. 接下来,遍历几代:
>>print(f'| Generation | Score |')
>>for gen in range(generations):

现在,我们可以评估智能体:

      rewards = all_agent_score(agents, env, 3)

通过这样做,我们得到了最好的智能体:

      top_parents_id = top_parents(rewards, num_top_parents)

反过来,这将创建下一代:

      agents, elite_agent = child_agents(agents, top_parents_id, env, elite_agent)
      print(f'| {gen+1:03} | {np.mean([rewards[i] for i in top_parents_id[:5]]):.4f} |')

以下代码块显示了示例输出:

| Generation |     Score       |
|    001     |    47.0667      |
|    002     |    47.3333      |
|    003     |    55.7333      |
|    004     |    58.2667      |
|    005     |    65.3333      |
|    006     |    88.0000      |
|    007     |    105.5333     |
|    008     |    117.4000     |
|    009     |    109.4000     |
|    010     |    137.6667     |
|    011     |    150.3333     |
|    012     |    168.6000     |
|    013     |    176.2667     |
|    014     |    248.0667     |
|    015     |    281.6667     |
|    016     |    327.9333     |
|    017     |    363.5333     |
|    018     |    375.4000     |
|    019     |    387.0000     |
|    020     |    432.2000     |
|    021     |    454.6000     |
|    022     |    445.9333     |
|    023     |    463.7333     |
|    024     |    482.1333     |
|    025     |    496.2000     |
  1. 最后,我们将形象化地表现出:
>>def play_agent(agent, env):

    observation = env.reset()
    total_reward=0

    for _ in range(MAX_STEP):
        env.render()
        observation = torch.tensor(observation).type('torch.FloatTensor').view(1,-1)
        output_probabilities = agent(observation).detach().numpy()[0]
        action = np.random.choice(range(2), 1, p=output_probabilities).item()
        new_observation, reward, done, _ = env.step(action)
        total_reward += reward
        observation = new_observation

        if(done):
            break

    env.close()
    print("Rewards: ",total_reward)

>>play_agent(agents[num_agents-1],env)
Rewards: 350.0

至此,我们已经完成了 DGA 的训练和可视化。

工作原理

在此秘籍中,我们改进了深度遗传算法。 根据论文《深度神经进化:遗传算法是用于训练深度神经网络以进行强化学习的一种竞争选择》,我们将超参数MUTATION_POWER设置为0.02。 我们不需要使用 PyTorch 来进行梯度计算,因为我们不需要依靠梯度下降来改善模型和创建环境。

然后,我们创建了一些智能体,以开始我们的进化,并通过遍历generations将它们运行了预定的代数,在其中我们获得了每一代所有智能体的奖励。 然后,我们选择了得分最高的父母,并通过child_agent函数传递这些父母指数以获得下一代。 之后,我们打印出前 5 个得分的平均值。

最后,我们使用了与“训练 DQN”秘籍中相同的play_agent函数,并进行了较小的修改,以补偿模型预测值的差异。 在这里,我们使用精英模型来显示卡特彼勒的表现,每一代之后。 位于智能体列表的末尾。 这是使用play_agent函数完成的。

更多

您可以控制深度遗传算法的各种超参数,以查看表现差异并将分数存储到图表中。

另见

您可以在这里这里阅读更多有关 DGA 的信息。

八、在 PyTorch 中生产 AI 模型

在本章中,我们将学习如何将 PyTorch 模型预测用于实际问题。 PyTorch 已从研究工具发展为可用于生产的框架,在本章中,我们将探讨一些使 PyTorch 可用于生产的功能。 部署模型意味着将模型提供给最终用户或系统。 为此,您可能需要满足多个要求,例如能够通过 Web 访问预测,快速进行预测以降低延迟或确保与其他深度学习框架的互操作性,以便开发人员可以使用正确的工具作为目标。 项目在发展。 所有这些确保了从研究到生产的更快过渡。

在本章中,我们将介绍以下秘籍:

  • 使用 Flask 部署模型
  • 创建一个 TorchScript
  • 导出至 ONNX

技术要求

使用 Python 3.6 中的 PyTorch 1.3 已完成了本章的所有秘籍。

使用 Flask 部署模型

在本秘籍中,我们将使用 Flask 微框架部署图像分类器。 我们使用 Flask 的原因是因为它是一个易于使用的微框架,可用于构建 RESTful 微服务,它是一个非常流行的框架,并且有据可查。 我们将部署一个使用 Densenet-161 预训练模型构建的图像分类器模型,以完成此秘籍。

准备

我们将需要为此秘籍安装 Flask。 使用pip管理器安装flask

pip install flask

这样,我们就可以开始了。

操作步骤

我们将把这个秘籍分成多个文件。 请按照以下步骤操作:

  1. 创建一个名为image_classifier.py的文件。
  2. 现在,我们需要进行导入:
>>import io
>>import torch
>>from torchvision import models
>>from PIL import Image
>>import torchvision.transforms as transforms
>>import json
  1. 读取包含类名称的.json文件:
>>with open('idx_class.json') as f:
    idx_class = json.load(f)
  1. 定义create_model函数:
>>def create_model():

    model_path = "densenet161.pth"
    model = models.densenet161(pretrained=True)
    model.load_state_dict(torch.load(model_path, map_location='cpu'), strict=False)
    model.eval()
    return model
  1. 定义image_transformer函数:
>>def image_transformer(image_data):
    transform = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406],
                             std=[0.229, 0.224, 0.225])
    ])

    image = Image.open(io.BytesIO(image_data))
    return transform(image).unsqueeze(0)
  1. 接下来,我们需要定义predict_image函数:
>>def predict_image(model, image_data):

    image_tensor = image_transformer(image_data)
    output = model(image_tensor)
    _, prediction = output.max(1)
    object_index = prediction.item()

    return idx_class[object_index]
  1. 现在,我们将创建imageapp.py
  2. 首先,我们将导入所需的模块和 Flask:
>>from flask import Flask, request, jsonify
>>from image_classifier import create_model, predict_image
  1. 现在,我们将创建一个 Flask 应用和分类器模型:
>>app = Flask(__name__)
>>model = create_model()
  1. 现在,让我们创建路由:
>>@app.route('/predict', methods=['POST'])
  1. 接下来,我们将编写一个将在此路由上调用的函数:
>>@app.route('/predict', methods=['POST'])
>>def predicted():
      if 'image' not in request.files:
          return jsonify({'error': 'Image not found'}), 400

      image = request.files['image'].read()
      object_name = predict_image(model, image)

      return jsonify({'object_name' : object_name})
  1. 最后,如果运行imageapp.py,我们将启动 Flask 应用:
>>if __name__ == '__main__':
    app.run(debug=True)
  1. 接下来,您需要使用以下命令运行 Flask 应用:
python imageapp.py

通过运行此命令,Flask 服务器将启动并运行。 您应该可以通过http://127.0.0.1:5000/访问应用的 URL 并发送POST请求。

  1. 使用邮差工具,您可以检查 API。 这是图像的示例响应:

现在,我们将检查 API 的响应:

{
 "object_name": "scorpion"
 }

这将为我们提供以下输出:

在此秘籍中,我们使用 Flask 进行了简单的应用部署。

工作原理

在本秘籍中,我们使用 Flask Python 框架部署了使用 RESTful API 进行推理的模型。 我们首先创建image_classifier.py并从idx_class.json加载类名。

在此文件中,第一个函数加载预先训练的densenet161模型,该模型在 ImageNet 数据集中具有 1,000 个类别的模型上进行训练; 我们将模型设置为评估模式,然后返回模型。 第二个函数将给定的输入图像转换为张量并对其进行变换。 我们使用了PIL中的Image模块来读取图像数据。 第三个函数通过将给定图像转换为张量并将其传递到模型中来进行预测。 这将返回图像中对象的名称。

然后,我们切换到imageapp.py文件,在这里我们使用 Flask 创建了 Web 应用。 在这里,我们使用app = Flask(__name__)创建了 Flask 应用,并使用create_model函数创建了模型。 此后,我们创建了一个名为/predict的路由,该路由使用我们创建的应用实例接收了一个POST请求。 然后,我们定义了predicted函数,该函数将在调用/predict URL 时被调用。 request.files在 POST 请求中保存文件。 在这里,我们检查了是否使用发布参数名称image上传了图像文件。

最后,我们将此图像数据传递到我们先前定义的predict_image函数中。 Flask 中的jsonify方法可确保响应为.json格式。 app.run(debug=True)启动 Flask 服务器并处理请求。

更多

在本秘籍中,我们使用debug=True将调试模式设置为开启,这在生产中不建议使用。 Flask 服务器的功能不足以支持生产负载。 相反,应使用gunicornnginx进行正确的部署。

另见

您可以在以下位置阅读有关 Flask 的更多信息

您可以通过以下网址了解有关 gunicorn Nginx 部署的信息

您可以在以下位置查看替代实现

创建一个 TorchScript

TorchScript 为最初用 PyTorch 编写的模型提供中间表示。 这样,您就可以在高性能环境(例如 C++)中运行模型。 TorchScript 通过 PyTorch 代码创建模型的可序列化和优化版本。 使用 TorchScript 编写的代码可以加载到进程中,而无需任何 Python 依赖项。 TorchScript 提供了可用于捕获模型定义的工具,而 PyTorch 具有动态和灵活的特性,因此足以支持此定义。 可以通过两种方式创建 TorchScript:跟踪或使用脚本编译器。 在本秘籍中,我们将使用跟踪和脚本编译器将 PyTorch 模型转换为 TorchScript。

操作步骤

在此秘籍中,我们将创建一个 TorchScript。 请按照以下步骤操作:

  1. 首先,我们将编写一个简单的网络:
>>import torch
>>import torch.nn as nn
>>class MyCell(torch.nn.Module):
      def __init__(self):
          super(MyCell, self).__init__()
          self.linear = torch.nn.Linear(4, 4)

      def forward(self, x, h):
          new_h = torch.tanh(self.linear(x) + h)
          return new_h
  1. 接下来,我们将根据模型类创建一个模型:
>>my_cell = MyCell()
  1. 然后,我们将生成两个随机张量传递给模型:
>>x, h = torch.rand(4, 4), torch.rand(4, 4)
  1. 接下来,我们可以jit.trace
>>traced_cell = torch.jit.trace(my_cell, (x, h))
>>traced_cell

TracedModule[MyCell](
  original_name=MyCell
  (linear): TracedModule[Linear](original_name=Linear)
)
  1. 然后,将张量传递给traced_cell
>>traced_cell(x, h)

tensor([[ 0.4238, -0.0524, 0.5719, 0.4747],
        [-0.0059, -0.3625, 0.2658, 0.7130],
        [ 0.4532, 0.6390, 0.6385, 0.6584]],
       grad_fn=<DifferentiableGraphBackward>)
  1. 我们可以使用以下代码访问图:
>>traced_cell.graph

graph(%self : ClassType<MyCell>,
      %input : Float(3, 4),
      %h : Float(3, 4)):
  %1 : ClassType<Linear> = prim::GetAttr[name="linear"](%self)
  %weight : Tensor = prim::GetAttr[name="weight"](%1)
  %bias : Tensor = prim::GetAttr[name="bias"](%1)
  %6 : Float(4, 4) = aten::t(%weight), scope: MyCell/Linear[linear] # /home/<user>/.local/lib/python3.6/site-packages/torch/nn/functional.py:1370:0
  %7 : int = prim::Constant[value=1](), scope: MyCell/Linear[linear] # /home/<user>/.local/lib/python3.6/site-packages/torch/nn/functional.py:1370:0
  %8 : int = prim::Constant[value=1](), scope: MyCell/Linear[linear] # /home/<user>/.local/lib/python3.6/site-packages/torch/nn/functional.py:1370:0
  %9 : Float(3, 4) = aten::addmm(%bias, %input, %6, %7, %8), scope: MyCell/Linear[linear] # /home/<user>/.local/lib/python3.6/site-packages/torch/nn/functional.py:1370:0
  %10 : int = prim::Constant[value=1](), scope: MyCell # <ipython-input-2-c6e2cd8665ee>:7:0
  %11 : Float(3, 4) = aten::add(%9, %h, %10), scope: MyCell # <ipython-input-2-c6e2cd8665ee>:7:0
  %12 : Float(3, 4) = aten::tanh(%11), scope: MyCell # <ipython-input-2-c6e2cd8665ee>:7:0
  return (%12)

对于可读的版本,我们可以使用以下命令:

>>traced_cell.code

import __torch__
import __torch__.torch.nn.modules.linear
def forward(self,
            input: Tensor,
            h: Tensor) -> Tensor:
    _0 = self.linear
    weight = _0.weight
    bias = _0.bias
    _1 = torch.addmm(bias, input, torch.t(weight), beta=1, alpha=1)
    return torch.tanh(torch.add(_1, h, alpha=1))

现在,让我们探索脚本编译器。 按着这些次序:

  1. 首先,我们将定义一个具有控制流程的子模块:
>>class MyDecisionGate(torch.nn.Module):
    def forward(self, x):
        if x.sum() > 0:
            return x
        else:
            return -x
  1. 然后,我们将在模型定义中使用此子模块:
>>class MyCell(torch.nn.Module):
    def __init__(self, dg):
        super(MyCell, self).__init__()
        self.dg = dg
        self.linear = torch.nn.Linear(4, 4)

    def forward(self, x, h):
        new_h = torch.tanh(self.dg(self.linear(x)) + h)
        return new_h
  1. 根据定义创建模型:
>>my_cell = MyCell(MyDecisionGate())
  1. 现在,我们将执行跟踪:
>>traced_cell = torch.jit.trace(my_cell, (x, h))
>>traced_cell.code

import __torch__.___torch_mangle_0
import __torch__
import __torch__.torch.nn.modules.linear.___torch_mangle_1
def forward(self,
    input: Tensor,
    h: Tensor) -> Tensor:
  _0 = self.linear
  weight = _0.weight
  bias = _0.bias
  x = torch.addmm(bias, input, torch.t(weight), beta=1, alpha=1)
  _1 = torch.tanh(torch.add(torch.neg(x), h, alpha=1))
  return _1

接下来,我们将使用jit.script将其转换为 TorchScript:

>>scripted_gate = torch.jit.script(MyDecisionGate())
>>my_cell = MyCell(scripted_gate)
>>traced_cell = torch.jit.script(my_cell)
>>print(traced_cell.code)

import __torch__.___torch_mangle_3
import __torch__.___torch_mangle_2
import __torch__.torch.nn.modules.linear.___torch_mangle_4
def forward(self,
    x: Tensor,
    h: Tensor) -> Tensor:
  _0 = self.linear
  _1 = _0.weight
  _2 = _0.bias
  if torch.eq(torch.dim(x), 2):
    _3 = torch.__isnot__(_2, None)
  else:
    _3 = False
  if _3:
    bias = ops.prim.unchecked_unwrap_optional(_2)
    ret = torch.addmm(bias, x, torch.t(_1), beta=1, alpha=1)
  else:
    output = torch.matmul(x, torch.t(_1))
    if torch.__isnot__(_2, None):
      bias0 = ops.prim.unchecked_unwrap_optional(_2)
      output0 = torch.add_(output, bias0, alpha=1)
    else:
      output0 = output
    ret = output0
  _4 = torch.gt(torch.sum(ret, dtype=None), 0)
  if bool(_4):
    _5 = ret
  else:
    _5 = torch.neg(ret)
  return torch.tanh(torch.add(_5, h, alpha=1))

这样,我们研究了创建 TorchScript 的两种不同方法。

工作原理

在本秘籍中,我们使用了跟踪方法来创建 TorchScript。 我们定义了一个简单的模块MyCell转换为Torchscript,并创建了两个采样张量xh传递给网络模块的正向方法。 然后,我们使用jit.trace跟踪 Python 代码并创建 TorchScript。

我们使用跟踪将PyTorch模型转换为 TorchScript,并传递了我们的模型实例。 jit.trace通过在模块的前向方法内跟踪模型评估中的操作来创建torch.jit.ScriptModule对象。 jit.trace运行网络模块,记录运行该模块时发生的操作,并创建torch.jit.ScriptModule对象的实例。 TorchScript 以中间表示形式(在深度学习中称为图)记录其定义。 然后,我们检查了具有.graph属性的图,并使用.code生成了更具可读性的版本,这是代码的 Python 语法解释。

然后,我们探索了使用脚本编译器创建 TorchScript 的下一种方法。 为此,我们使用以下代码定义了具有控制流的子模块:

>>class MyDecisionGate(torch.nn.Module):
    def forward(self, x):
        if x.sum() > 0:
            return x
        else:
            return -x

我们在MyCell模块中使用了以下子模块:

my_cell = MyCell(MyDecisionGate())

使用跟踪方法,我们失去了控制流,因为通过跟踪,我们运行了代码,记录了操作,并构造了一个ScriptModule对象,该对象擦除了诸如控制流之类的东西。 可以在以下代码中看到:

>>traced_cell = torch.jit.trace(my_cell, (x, h))
>>traced_cell.code

因此,我们使用jit.script保留了控制流。 首先,我们在子模块对象上运行jit.script,如下所示:

>>scripted_gate = torch.jit.script(MyDecisionGate())

然后,我们创建MyCell对象并使用jit.script运行它:

>>my_cell = MyCell(scripted_gate)
>>traced_cell = torch.jit.script(my_cell)

当使用print(traced_cell.code)打印 TorchScript 代码时,我们看到仍然保留了控制流。

更多

我们可以将跟踪和脚本编写方法混合在一起。

另见

您可以在以下网址找到有关混合跟踪和脚本的更多信息

导出至 ONNX

在本秘籍中,我们将介绍如何将 PyTorch 模型导出到开放神经网络交换ONNX),该模型为深度学习和传统机器学习模型提供了一种开源格式。 它定义了一个可扩展的计算图模型,以及内置的运算符和标准数据类型。

ONNX 得到了广泛的支持,可以在许多框架,工具和硬件中找到,因为它可以实现不同框架之间的互操作性,并可以实现从研究到生产的过渡。

准备

对于此秘籍,我们需要安装 ONNX,可以使用以下命令进行安装:

pip install onnx

这样,我们就可以进行秘籍了。

对于此秘籍,我们还将需要在第 3 章,“用于计算机视觉的卷积神经网络”中在CIFAR-10上训练的模型的训练权重。

操作步骤

在本秘籍中,我们将 CIFAR-1o 模型导出为 ONNX 格式,并使用onnxruntime运行它。 请按照以下步骤操作:

  1. 我们将从导入开始:
>>import onnx
>>import onnxruntime
>>import torch.nn as nn
>>import torch
>>import torch.nn.functional as F
>>import numpy as np
  1. 接下来,我们将定义模型类:
>>class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.linear1 = nn.Linear(64 * 4 * 4, 512)
        self.linear2 = nn.Linear(512, 10) 
        self.dropout = nn.Dropout(p=0.3)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = self.pool(F.relu(self.conv3(x)))
        x = x.view(-1, 64 * 4 * 4)
        x = self.dropout(x)
        x = F.relu(self.linear1(x))
        x = self.dropout(x)
        x = self.linear2(x)
        return x
  1. 然后,我们将创建模型对象并从我们的训练中加载权重:
>>model = CNN()
>>model.load_state_dict(torch.load("cifar10.pth"))
<All keys matched successfully>
  1. 接下来,我们将模型设置为评估模式:
>>model.eval()

CNN(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (linear1): Linear(in_features=1024, out_features=512, bias=True)
  (linear2): Linear(in_features=512, out_features=10, bias=True)
  (dropout): Dropout(p=0.3, inplace=False)
)
  1. 现在,我们将创建一个随机变量:
>>x = torch.randn(1, 3, 32, 32, requires_grad=True)
  1. 接下来,我们将获得随机变量x的输出:
>>model_out = model(x)
  1. 之后,我们将模型及其权重导出到onnx模型中:
>torch.onnx.export(model,
                 x,
                 "cifar.onnx",
                 export_params=True,
                 opset_version=10, 
                 do_constant_folding=True, 
                 input_names = ['input'], 
                 output_names = ['output'],
                 dynamic_axes={'input' : {0 : 'batch_size'},
                               'output' : {0 : 'batch_size'}})
  1. 接下来,我们将加载并检查onnx模型:
>>onnx_model = onnx.load("cifar.onnx")
>>onnx.checker.check_model(onnx_model)
  1. 我们将onnx加载到 ONNX 运行时中:
>>ort_session = onnxruntime.InferenceSession("cifar.onnx")
  1. 现在,定义to_numpy()函数:
>>def to_numpy(tensor):
    return tensor.detach().cpu().numpy() if tensor.requires_grad else tensor.cpu().numpy()
  1. 在这里,我们将把输入变量x传递到 ONNX 运行时:
>>ort_inputs = {ort_session.get_inputs()[0].name: to_numpy(x)}
>>ort_outs = ort_session.run(None, ort_inputs)
  1. 最后,我们将检查模型和onnx模型的输出是否相等:
>>np.testing.assert_allclose(to_numpy(model_out), ort_outs[0], rtol=1e-03, atol=1e-05)

通过此秘籍,我们已导出为onnx格式,并使用 ONNX 运行时以onnx格式运行了模型。

工作原理

在此秘籍中,我们将正常的 PyTorch 模型导出为 ONNX 格式,并使用 ONNX 运行时运行onnx模型。 为此,我们采用了权重模型。 在这里,我们使用了第 3 章“卷积神经网络”的 CIFAR-10 模型,用于计算机视觉*。 我们从训练中使用了模型的权重,并将模型设置为评估模式,以进行快速,轻便的计算。

然后,我们使用了一个随机变量,其形状与输入张量的形状相同,在本例中为三通道32 x 32像素图像。 我们将此随机输入传递到模型中并获得输出。 然后,我们使用输出将其与模型的 ONNX 版本中的模型进行比较。

在 PyTorch 中使用跟踪或脚本导出模型。 在本秘籍中,我们在torch.onnx.export()的帮助下使用了跟踪。 跟踪跟踪用于获取输出的操作。 这就是为什么我们提供x的原因-因此可以进行跟踪。 x必须具有正确的类型和大小。 输入尺寸在导出的 ONNX 图中固定为所有输入尺寸,我们必须指定所有动态轴。 在此秘籍中,我们使用第一维的输入导出模型,将批量大小设置为 1,并在torch.onnx.export()dynamic_axes参数中将第一维指定为动态。

第一个参数是 PyTorch 模型,第二个参数是随机变量。 然后,我们有了onnx格式的路径; export_params用于将训练后的参数权重存储在模型文件中; opset_versiononnx导出版本; do_constant_folding用于执行常量折叠以进行优化; input_names是模型的输入名称,output_names是模型的输出名称。 然后,我们加载了导出的onnx模型,并检查了模型结构并使用onnx.checker.check_model(onnx_model)验证了架构。 通过检查模型版本,图的结构,节点及其输入和输出来验证 ONNX 图。

然后,我们将模型加载到onnx运行时中,并为模型创建一个推理会话。 创建会话后,我们使用run() API 评估了模型,其中第一个参数是输出名称的列表,第二个参数是输入字典。 此调用的输出是计算 ONNX 运行时之后模型输出的列表。 最后,我们使用numpy.testing.assert_allclose()比较了 PyTorch 模型和onnx模型的输出值,如果两个对象不等于期望的公差,则会提高AssertionError

更多

我们可以导出onnx模型,加载其他受支持的框架,并使用torch.onnx.export()中的其他参数配置导出。

另见

您可以在以下位置阅读有关 ONNX 的更多信息

您可以在以下位置阅读有关 Python ONNX 运行时的更多信息

posted @ 2026-03-25 10:34  布客飞龙II  阅读(0)  评论(0)    收藏  举报