一、使用 PyTorch 使用张量
深度学习是机器学习父领域中的一个子领域,它是受大脑工作启发的一类算法的研究和应用。 给定足够的数据并通过它进行迭代,这些算法可以近似于描述数据的任何函数,并且正确地称为通用函数近似器。 那么 PyTorch 进入这个生态系统的位置是什么?
PyTorch 是 Python 中的一个开源深度学习框架,它使我们能够从研究问题开始,提出原型解决方案,并在开发此解决方案的过程中一直进行到创建分布式计算集群为止。 它使您从研究到生产都可以覆盖。 PyTorch 改编自 Torch,后者是一个科学计算框架,广泛支持机器学习算法,使用 Lua 编写,可为您提供强大的功能(使用 GPU)。 那为什么要用 PyTorch?
PyTorch 与 Python 深度集成,具有命令式风格,使用类似 Python 的语法,并且在 Eager 模式下易于使用且灵活。 它的学习曲线很浅,可以让您专注于功能而不是框架的样板和语法。 Python 命令的纯命令执行将失去很多优化机会,因此,随着即时(JIT)编译器的引入,PyTorch 允许过渡到 C++ 运行时的图环境中用于速度,功能和优化的模式。 它得到了来自不同领域的专业人员的大力社区支持,并且与库打交道。 它具有与框架互操作性的本地开放神经网络交换(ONNX)支持。 它是分布式的,可扩展到生产环境,与 TensorBoard 集成,并具有出色的文档和 API,您可以轻松编写针对 CPU 和 GPU 的自定义扩展。 我们将在接下来的章节中探索这些以及更多内容。
在本章中,我们将介绍以下秘籍:
要完成本章,您需要安装 Python3。 您还将需要任何现代机器,但本章不需要使用支持 GPU 的设备。 如果要利用 GPU 功能,可以使用支持 NVIDIA CUDA 的 GPU。
我们将在本节中安装 PyTorch。
NumPy 是本章必不可少的库,在您安装 PyTorch 时,NumPy 会自动为您安装它的依赖项。 这意味着我们无需显式安装 NumPy。
您可以将 PyTorch 与其他包管理器一起安装,例如 Conda,如这个页面中所述。
要为 Python3 CPU 安装 PyTorch,我们可以使用以下命令:
pip
管理器:pip3 install torch==1.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
pip
管理器:pip3 install torch==1.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
pip
管理器:pip3 install torch
要为启用 Python3 CUDA 的 GPU 版本安装 PyTorch,可以使用以下命令:
pip
管理器:pip3 install torch
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
如果这些导入效果很好,那就很好了!
首先让我们了解张量是什么。 标量是单个独立值,一维值数组称为向量,二维值数组称为矩阵,并且任何大于 2D 的值简称为张量。 张量是一个广义的术语,包含标量,向量和矩阵。
标量是 0 阶张量,向量是 1 阶张量,矩阵是 2 阶张量。
以下是各种张量:
x[1]
。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2RHdcalX-1681785960487)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/bc4636e9-4110-4074-8ac5-420cdab8ce74.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-78kkZ5T0-1681785960488)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/dc9d1b48-fc56-4787-8afe-ff809ea12284.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y39TQZcs-1681785960488)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/ccc2bd36-f510-4adf-ac38-c7c185d40e53.png)]
这样,我们将继续介绍如何使用张量的秘籍。
在 PyTorch 中有多种创建张量的方法。 我们将在本节中介绍其中一些:
import torch
ones()
方法:torch.ones((2,3))
这将返回一个张量,该张量包含一个张量,并且具有默认的float
数据类型,如下所示:
tensor([[1., 1., 1.],
[1., 1., 1.]])
dtype
)作为参数:torch.ones((2,3), dtype=torch.int8)
tensor([[1, 1, 1],
[1, 1, 1]], dtype=torch.int8)
zeros()
方法:torch.zeros((2,3), dtype=torch.int8)
tensor([[0, 0, 0],
[0, 0, 0]], dtype=torch.int8)
full()
方法并传递所需的填充值以及形状:torch.full((2, 3), 3.141592)
tensor([[3.1416, 3.1416, 3.1416],
[3.1416, 3.1416, 3.1416]])
请注意,这些值是四舍五入的。
empty()
方法:torch.empty((2,3))
tensor([[2.5620e-01, 4.5773e-41, 2.5620e-01],
[4.5773e-41, 4.4842e-44, 0.0000e+00]])
rand()
方法:torch.rand((2,3))
[0, 1]
的均匀分布绘制具有随机值的张量:tensor([[0.6714, 0.0930, 0.4395],
[0.5943, 0.6582, 0.6573]])
randn()
方法:torch.randn((2,3))
tensor([[ 0.3470, -0.4741, 1.2870],
[ 0.8544, 0.9717, -0.2017]])
rand_int()
方法,传入下限,上限和形状:torch.randint(10, 100, (2,3))
tensor([[63, 93, 68],
[93, 58, 29]])
tensor
类:torch.tensor([[1, 2 ,3], [4, 5, 6]])
这将创建数据的副本并创建张量。 如果要避免进行复制,可以使用torch.as_tensor([[1, 2 ,3], [4, 5, 6]])
。
tensor([[1, 2, 3],
[4, 5, 6]])
另外,请注意,如果数据值之一是浮点数,则所有这些值都将转换为浮点数。 但是,如果值之一是字符串,则将引发错误。
a = torch.tensor([[1, 2 ,3], [4, 5, 6]])
a
的数据类型:a.dtype
torch.int64
a.shape
torch.Size([2, 3])
b
,使其与a
的属性匹配,并为此使用torch.*_like
格式:b = torch.ones_like(a)
b
结果为以下输出:
tensor([[1, 1, 1],
[1, 1, 1]])
b
的数据类型:b.dtype
torch.int64
b
的形状:b.shape
torch.Size([2, 3])
a
,并为此使用torch.new_*
格式:a.new_full((2,2), 3.)
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 是 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
开始:
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
磁带将记住它执行的所有操作,而在倒退阶段,它将重放它们。
让我们从创建张量开始。
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 可以跟踪操作。 当我们从x
,y=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 中常见的重排和重塑技术。
在本秘籍中,我们将学习如何使张量看起来像我们想要的样子。
让我们看一下如何改变张量的形状:
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.]])
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 的恒定输入)和激活函数来复制。
以下是神经单元的示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L876wFAe-1681785960489)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/d6e62091-b4e4-489a-b2e8-926df04945ed.png)]
神经网络所看到的只是数字集,它试图识别数据中的模式。 通过训练,神经网络学会识别输入中的模式。 但是,某些特定的架构在应用于特定类别的问题时比其他结构具有更好的表现。 一个简单的神经网络架构由三种层组成:输入层,输出层和隐藏层。 当隐藏层不止一个时,它称为深度神经网络。
以下是深度神经网络的表示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mCcL9RII-1681785960489)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/61ece4e2-7ca2-42bd-8663-1c221607aacc.png)]
在前面的图中,圆圈代表神经元,或用深度学习的术语代表作为计算单元的节点。 边缘代表节点之间的连接,并保持两个节点之间的连接权重(突触强度)。
在本章中,以下秘籍将使我们开始使用神经网络:
在本章中,我们将开始处理图像数据并学习全连接神经网络如何工作。 PyTorch 有一个名为torchvision
的补充库,我们将在开始秘籍之前进行安装。
您可以对torchvision
使用以下pip
安装命令:
pip install torchvision
对于其他安装方法,您可以访问这个页面。 上一章“使用 PyTorch 张量”的其余依赖关系保持不变。
在本秘籍中,我们将从了解torchvision
的一些重要函数开始,这些函数使它能够处理图像数据并进行处理。 然后,我们将通过定义一个类来定义神经网络的基本架构,并查看可用于此的模块和方法。 在本秘籍中,我们将专注于全连接神经网络类。 它的属性是各个层,其目的是对各种类型的衣服进行分类。
我们将使用 Fashion–MNIST 数据集。 这是 Zalando 文章图片的数据集,包括 6 万个示例的训练集和 10,000 个示例的测试集。 我们将拍摄一张尺寸为28 x 28
的灰度图像,并将其转换为 784 的向量。
以下是来自数据集的示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IIJMTmNB-1681785960489)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/48bccba8-198a-4518-aa52-317ebaf65fc2.png)]
现在,我们将研究定义网络所需采取的步骤。
让我们定义我们的网络:
1.我们将从设置torch
和torchvision
导入开始:
>>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()
通过这些步骤,我们已经完成了网络单元。
在本秘籍中,我们开始使用torchvision
。 torchvision
中有工具来支持与视觉有关的任务。 有一个名为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.weight
和model.hidden.bias
从模型对象访问权重和偏差张量。
您可以在这个页面上查看nn.Module
和nn.Sequential
的官方文档。
机器学习模型在接受训练时,可能会在预测输出和实际输出之间存在一些偏差,这种差异称为模型的误差。 让我们计算该误差的函数称为损失函数或误差函数。 此函数提供了一种度量标准,用于评估所有可能的解决方案并选择最优化的模型。 损失函数必须能够将模型的所有属性减少到一个单一的数字,以便该损失函数值的改善代表更好的模型。
在本秘籍中,我们将使用 PyTorch 中可用的损失函数为我们的时装数据集定义损失函数。
让我们定义损失函数:
__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
,它必须是一维张量,该张量为每个输出类分配权重以处理不平衡的训练集。
您可以在这个页面上查看更多损失函数的官方文档。
在本秘籍中,我们将学习优化器。 在先前的秘籍“定义损失函数”中,我们谈到了误差和误差函数,并了解到,为了获得一个好的模型,我们需要最小化计算出的误差。 反向传播是神经网络从误差中学习的一种方法。 误差用于修改权重,以使误差最小化。 优化函数负责修改权重以减少误差。 优化函数计算相对于权重的误差的偏导数。 导数显示正斜率的方向,因此我们需要反转梯度的方向。 优化器函数将模型参数和损失函数结合在一起,以迭代方式修改模型参数以减少模型误差。 可以考虑将优化器摆在模型权重的基础上,根据模型与实际输出的预测差异来获得最佳模型,而损失函数则通过指示优化器是对还是错来充当指导。
学习率是优化器的超参数,它控制权重的更新量。 学习速度确保了权重不会大量更新,从而使算法根本无法收敛,并且误差越来越大。 然而,与此同时,权重的更新不应太低,以至于永远需要花费成本函数/误差函数的最小值。
下面显示了学习率的影响:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lxpFw1hn-1681785960490)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/ca32e442-d198-424e-bc49-52451f5a458b.png)]
在本秘籍中,我们将学习如何在 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().
启用退出
您可以在这个页面上了解有关丢弃的更多信息。
在本秘籍中,我们将探索 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.relu
和function.log_softmax
代替了激活函数和对数 softmax,这使我们的代码看起来更加简洁明了。
您可以通过使用functional.linear()
和functional.dropout()
来控制丢弃,从而对线性层使用函数式 API,但是必须注意传递模型状态以指示其处于训练还是评估/预测模式。
您可以通过这个页面了解更多有关函数式 API 的信息。
在本章中,我们将学习卷积神经网络(CNN)。 这是与前几章讨论的神经网络不同的一类。 CNN 在计算机视觉领域已经取得了巨大的成功,随着我们对它们的了解越来越多,我们将能够理解其中的原因。
CNN 是一种特殊的网络,可以将图像作为张量接收。 彩色图像由红色,绿色和蓝色三个颜色通道组成,称为 RGB。 我们将这些二维矩阵通道堆叠起来以形成彩色图像; 每个通道的值变化会产生不同的颜色。 CNN 将图像作为三个独立的堆叠颜色层,一个层放在另一个层上。
图像从附近的一个设置像素中获得其含义,但是单个像素不能保存有关整个图像的太多信息。 在也称为密集层的全连接神经网络中,一层中的每个节点都连接到下一层中的每个其他节点。 CNN 利用像素之间的空间结构来减少两层之间的连接数,从而显着提高训练速度,同时减少模型参数。
这是显示全连接网络的图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r62hdlCm-1681785960490)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/06998e32-12e3-44d9-9100-99d43f341b71.png)]
将上一张图像与下一张图像进行比较,后者显示了一个卷积网络:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmFb8EhS-1681785960490)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/ae0349d2-67ef-4b56-b530-7c349847db18.png)]
CNN 使用过滤器从输入图像中拾取特征; 具有足够数量的过滤器的 CNN 可以检测图像中的各种特征。 随着我们越来越向后一层移动,这些过滤器在检测复杂特征方面变得越来越复杂。 卷积网络使用这些过滤器并逐一映射它们以创建特征出现的映射。
在本章中,我们将介绍以下秘籍:
在本章中,您将需要在上一章中安装的 TorchVision。 您最好在支持 GPU 的计算机上运行这些秘籍中的代码。
卷积是 CNN 中的一个组成部分。 它们被定义为 CNN 中的一层。 在卷积层中,我们将过滤器矩阵从左到右,从上到下在整个图像矩阵上滑动,然后取过滤器的点积,此斑块将过滤器的尺寸跨过图像通道。 如果两个矩阵在相同位置具有较高的值,则点积的输出将较高,反之亦然。 点积的输出是标量值,该标量值标识图像中的像素模式和由过滤器表示的像素模式之间的相关性。 不同的过滤器会以不同的复杂度从图像中检测不同的特征。
我们需要了解 CNN 的另外两个关键元素,如下所示:
这是这两个填充的示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RkaEmVVm-1681785960491)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/b148a21b-3863-4c64-b9bd-5f711b1f6caf.png)]
下图显示了有效填充的示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iDjsb1Hj-1681785960491)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/097e8ce0-c2bc-4f7b-b97f-8a2952f84f2f.png)]
在本秘籍中,我们将学习如何在 PyTorch 中使用卷积神经网络。
在此秘籍中,我们将探讨卷积:
modules
: >>import torch
>>import torch.nn as nn
>>nn.Conv2d(3, 16, 3)
这将创建以下卷积层:
Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1))
>>nn.Conv2d(3, 16, 3, padding=1)
这将创建以下卷积层:
Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
>>nn.Conv2d(3, 16, (3,4), padding=1)
这将创建以下卷积层:
Conv2d(3, 16, kernel_size=(3, 4), stride=(1, 1), padding=(1, 1))
>>nn.Conv2d(3, 16, 3, stride=2)
这将创建以下卷积层:
Conv2d(3, 16, kernel_size=(3, 3), stride=(2, 2))
>>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 的初始层移到后面的层时,我们希望在图像中识别出比实际逐个像素信息更多的概念意义,因此我们想从输入和抛出中识别并保留剩下的关键信息。 池化层可以帮助我们做到这一点。
这是最大池化的示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xgyZJ4NJ-1681785960491)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/07171c56-ad0e-436e-9f67-bcdeb8f91cc1.png)]
这是使用池化层的主要原因:
池的类型很多,例如最大池,平均池,和池等。 但是,最大池化是最受欢迎的。 以与处理卷积层相同的方式,我们将定义一个窗口并在该窗口中应用所需的池化操作。 我们将根据层的跨度水平和垂直地滑动窗口。
在此秘籍中,我们将研究如何在 PyTorch 中实现池化层:
>>import torch
>>import torch.nn as nn
nn
模块中定义的池类,如下所示: >>max_pool = nn.MaxPool2d(3, stride=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.]]])
>>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.]]])
>>avg_pool = nn.AvgPool2d(3, stride=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
中的各种转换:
torchvision
开始: >>from torchvision import transforms
>>transforms.ToTensor()
>>transforms.Normalize((0.5,),(0.5,))
>>transforms.Resize(10)
我们还可以使用以下内容:
>>transforms.Resize((10,10))
>>transforms.CenterCrop(10)
我们还可以使用以下内容:
>>transforms.CenterCrop((10, 10))
>>transforms.Pad(1, 0)
我们还可以使用以下内容:
>>transforms.Pad((1, 2), 1)
如果愿意,我们还可以执行以下操作:
>>transforms.Pad((1, 2, 2, 3), padding_mode='reflect')
>>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
模块执行的流行数据扩充:
torchvision
开始:>>import torchvision
>>transforms.RandomCrop(10)
我们还可以使用以下内容:
>>transforms.RandomCrop((10,20))
>>transforms.RandomHorizontalFlip(p=0.3)
>>transforms.RandomVerticalFlip(p=0.3)
>>transforms.ColorJitter(0.25, 0.25, 0.25, 0.25)
>>transforms.RandomRotation(10)
>>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 数据集:
torchvision
导入datasets
模块:>>from torchvision import datasets
transforms
模块:>>from torchvision import transforms
>>transformations = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(20),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))
])
>>train_data = datasets.CIFAR10('CIFAR10', train=True, download=True, transform=transformations)
>>test_data = datasets.CIFAR10('CIFAR10', train=False, download=True, transform=transformations)
>>len(train_data), len(test_data)
(50000, 10000)
torch
模块进行导入:>>from torch.utils.data.sampler import SubsetRandomSampler
>>validation_size = 0.2
numpy
:>>import numpy as np
>>training_size = len(train_data)
>>indices = list(range(training_size))
>>np.random.shuffle(indices)
>>index_split = int(np.floor(training_size * validation_size))
>>validation_indices, training_indices = indices[:index_split], indices[index_split:]
>>training_sample = SubsetRandomSampler(training_indices)
>>validation_sample = SubsetRandomSampler(validation_indices)
>>batch_size = 16
dataloader
模块将数据批量加载:>>from torch.utils.data.dataloader import DataLoader
>>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 模型架构。 这与我们在第 2 章,“处理神经网络”中介绍的全连接神经网络非常相似。 为了更好地理解此秘籍,从第 2 章,“处理神经网络”修改全连接神经网络的模型定义将是一个好主意。 我们将在 CIFAR10 数据集上建立图像分类模型,我们在“加载图像数据”秘籍中对此进行了讨论。
我们将在此秘籍中完成模型类的定义:
1.首先,我们将导入nn.Module
和torch
函数式 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.首先,我们将导入torch
和torch.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。 我们可以使用它来计算每个批量中模型的准确率,并将其汇总到整个测试数据集中。
您可以在这个页面上看到测试模型的示例实现。
在本章中,我们将处理循环神经网络(RNNs),这是一种专门处理序列数据或时变数据的神经网络。 利用卷积神经网络,我们处理在空间上彼此相关的数据点,其中一组像素值保存有关图像的信息。 但是请考虑一下节奏,节奏是由一段时间内一系列变化的声音信号形成的。 数据点彼此之间具有时间关系。 在循环神经网络中,神经元之间的连接在时间序列上形成有向图,表现出时间动态行为。 传统的前馈网络不存储先前的输入; 但是,RNN 使用存储单元来记住先前的输入,因此根据到目前为止的输入顺序来处理当前输入。
在本章中,我们将介绍以下秘籍:
这是 RNN 的示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uzJsllJY-1681785960492)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/616ddb15-b5e8-4d9f-b8fb-e5147173866f.png)]
图 1:循环神经网络
在上图中,我们可以看到 RNN 中的输入,输出和循环本身。 RNN 是为信息的持久性而设计的,而循环组件则可以实现这一点。
这是“图 1”的扩展版本:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2WOWQO63-1681785960492)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/a503c507-a800-47b7-ba2a-8ed34c3c4a5e.png)]
图 2:RNN 的展开图
在上图中,我们可以看到 RNN 的展开图,其中一个步骤的信息被馈送到下一个步骤,从而创建同一网络的多个副本,所有这些信息都封装在循环循环中。 循环神经网络接受输入并给出输出,但是该输出不仅取决于给定实例上的输入,还取决于提供给网络的输入的整个历史,网络会在数学上记住这些输入。
循环神经网络能够接收可变大小的输入序列并产生可变大小的输出序列,从而执行与全连接神经网络中固定数量的计算相反的可变数量的计算。 进一步的 RNN 允许信息持久化,并在接收到的输入之间共享该信息。 在给定实例上生成的输出基于到目前为止所看到的所有输入的历史记录。
这是 LSTM 的示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9STGB93q-1681785960492)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/11252b15-33f0-4c1f-b91e-f2af4acfb6b5.png)]
图 3:LSTM
长短期记忆(LSTM)网络是一种循环神经网络,是 RNN 之上的一种进步。
这是双向 LSTM:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UFCjU7jK-1681785960493)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/5d6dbdf3-445c-42f8-848e-f9f4fcf09e55.png)]
图 4:双向 LSTM
这是多层 LSTM:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LfclppPX-1681785960493)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/5148d1fd-5c3f-4d7a-84c2-39645c43af31.png)]
图 5:多层 LSTM
双向 LSTM 和多层 LSTM 是对基本 LSTM 网络架构的改进。
在本章中,我们需要设置 PyTorch。 我们将使用torchtext
,它是一个专门的库,用于处理与 PyTorch 联合工作的语言任务。
我们可以使用以下pip
命令安装torchtext
:
pip install torchtext
至此,我们完成了本章所需的设置。
在处理自然语言处理任务时,我们采用文本语料库并将其分解为较小的单元。 在本秘籍中,我们将句子分解为单个单词,其中每个单词代表一个意思,其他单词与它附近的其他单词一起表达一个句子的意图。 计算机只能理解数字,因此为这些单词分配了唯一的整数值来表示单词。 将句子分解为标记的过程称为分词。 在本秘籍中,我们将执行单词分词。
在本秘籍中,我们将编写一个标记器,将在本章的“创建字段”部分中使用:
>>tokenizer = lambda words: words.split()
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
类定义各种文本处理任务。
在本秘籍中,我们将探讨使用字段的各种示例:
>>from torchtext.data import Field
Field
对象:>>Review = Field(sequential=True, tokenize=tokenizer, lower=True)
>>Label = Field(sequential=False, use_vocab=False)
>>SequenceField = Field(tokenize=tokenizer, init_token='<sos>', eos_token='<eos>', lower=True)
>>SequenceField = Field(tokenize=tokenizer, init_token='<sos>', eos_token='<eos>', lower=True, fix_length=50)
>>SequenceField = Field(tokenize=tokenizer, init_token='<sos>', eos_token='<eos>', unk_token='<unk>')
>>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_token
和eos_token
参数轻松完成。 这对于序列到序列的模型是正确的,并且如果在模型评估期间用于训练模型的词汇表中不存在标记(词汇表外),则可以使用自定义标记来替换这些标记, 设置unk_token
参数。
然后,将batch_first
设置为True
,以使输出张量的第一维为批量维,如果fix_length
参数设置为整数值,则使用此字段将对输入设置固定长度。
我们可以为特定于某种语言的分词设置用于分词的语言,该语言支持 spacy 支持的语言。 我们可以使用pad_token
参数设置自定义填充标记。 我们可以在分词之后但在数字化之前使用该字段定义将应用于示例的处理管道,在数字化之后但使用preprocessing
和postprocessing
参数将数字转换为张量之前,我们可以执行相同的操作。 stop_words
参数可用于删除在预处理时需要删除的标记。 此外,还有一种专门用于标签字段的字段类型LabelField
,它可以代替普通字段使用。
您可以通过这个页面了解更多有关字段的信息。
在本秘籍中,我们将研究读取文本数据并使用各种数据源。torchtext
可以从文本文件,CSV/TSV 文件,JSON 文件和目录中读取数据,并将其转换为数据集。 数据集是已预处理的数据块,可读取到内存中,并可由其他数据结构使用。
我们将使用此秘籍的新闻分类数据集,您可以从这里下载该数据集。
.csv
文件中包含以下几列:
id
content
Business
SciTech
Sports
World
在此秘籍中,我们将读取有毒评论数据集,该数据集存储为一组.csv
文件:
>>from torchtext.data import TabularDataset
>>train_datafields = [("id", None),
("content", Review), ("Business", Label),
("SciTech", Label), ("Sports", Label),
("World", Label)]
>>test_datafields = [("id", None),
("content", Review)]
.csv
文件:>>train, valid = TabularDataset.splits(path='NewsClassification',
train='train.csv',
valid='valid.csv',
format='csv',
skip_header=True,
fields=train_datafields)
.csv
文件:>>test = TabularDataset(path="NewsClassification/test.csv",
format='csv',
skip_header=True,
fields=test_datafields)
>>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
方法,并将其传递到训练和验证文件所在的根文件夹路径中。 我们还使用train
和valid
参数传入了训练和验证文件的文件名。
我们将文件格式指定为csv
,并通过将skip_header
设置为True
以及fields
参数中的必需列来删除标题行,并且对数据集进行了相同的测试。 最后,我们在Fields
对象中调用了build_vocab()
方法,以建立可能的单词库,在数据集中的出现次数最少为两次。 不在词汇表中的单词将在验证和测试集中分配一个未知标签。
您可以使用Vocab
模块在torchtext
中构建词汇表。 除了TabularDataset
之外,还有其他类型的数据集可以使用,具体取决于手头的 NLP 任务-例如,对于语言翻译任务,我们可以使用TranslationDataset
类。
您可以在这个页面上了解有关数据集的更多信息。
迭代器用于从数据集中加载一批数据。 它们提供了使加载数据和将数据移动到适当设备的方法更加容易。 我们可以使用这些迭代器对象遍历周期时对数据进行迭代。 在本秘籍中,我们将从数据集中开发这些迭代器。 您将需要完成“开发数据集”秘籍中的步骤,因为我们将在此处使用该秘籍中的Dataset
对象。
在此秘籍中,我们将数据集转换为迭代器,以便在每个周期准备好要迭代的适当批量:
>>from torchtext.data import BucketIterator
>>import torch
>>BATCH_SIZE = 128
>>device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
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
的专门迭代器类,该类将输入序列分组为相似长度的序列,并自动对其进行随机排序。 我们定义了批量大小,并找到了机器上可用的设备。
然后,我们使用BucketIterator
的splits
方法创建训练,测试和验证迭代器。 我们将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]
的单引号表示。
数字表示只是单词的表示; 但是,单词嵌入是标记的表示,其中该表示也包含标记/单词的含义。 模型从单词出现的上下文中获悉了此含义。 在词嵌入中,具有相似含义的词具有相似的表示形式,我们可以对这些词向量执行向量算术,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HB2FORhe-1681785960493)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/c4fc9f6e-bc00-4e90-a9e6-00637c82c64d.png)]
在这里,我们能够从king
向量中减去man
向量,并将其与woman
向量相加,所得向量将接近该向量Queen
的表示形式。 我们将在本秘籍中探讨此实现。
在此秘籍中,我们将对torchtext
使用预训练的嵌入:
>>from torchtext import vocab
>>vec = vocab.Vectors('glove.6B.100d.txt', cache='./vec/glove_embedding/', url='http://nlp.stanford.edu/data/glove.6B.zip')
>>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)网络是一种循环神经网络,具有内部门控,有助于更好地保持信息。 这些门是微小的神经网络,用于控制何时需要保存信息以及何时可以擦除或忘记信息。 RNN 受梯度消失和爆炸的影响,因此很难学习长期依赖关系。 LSTM 可以抵抗爆炸和梯度消失,尽管在数学上仍然可行。
在此秘籍中,我们将定义 LSTM 分类器:
>>import torch.nn as nn
LSTMClassifier
类:>>class LSTMClassifier(nn.Module):
>>def __init__(self, embedding_dim, hidden_dim, output_dim, dropout):
super().__init__()
self.embedding = nn.Embedding(len(Review.vocab), embedding_dim)
self.rnn = nn.LSTM(embedding_dim, hidden_dim)
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)
dropout
:hidden = self.dropout(hidden)
return self.fc(hidden)
>>EMBEDDING_DIM = 100
>>HIDDEN_DIM = 256
>>OUTPUT_DIM = 1
>>DROPOUT = 0.5
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 秘籍的修改。
__init__()
:>>def __init__(self, embedding_dim, hidden_dim, output_dim, dropout, num_layers):
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])
>>NUM_LAYERS = 2
>>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_direction
是1
),因此我们仅使用hidden[-1]
从最后一个 LSTM 层获取了隐藏状态。 这意味着hidden[-1]
给出了最后一层的隐藏状态。 通过这样做,我们可以选择num_layers
作为超参数。 来自较低层的隐藏状态输出作为较高状态的输入传递。
在本秘籍中,我们仅考虑了最后一个 LSTM 层的隐藏状态。 但是,可能存在使用所有隐藏层的复杂架构。 有一个dropout
参数,可用于在多层 LSTM 的各层之间施加丢弃。
您可以在这个页面上了解有关多层 LSTM 的更多信息。
该秘籍以多层 LSTM 秘籍为基础。 在正常的 LSTM 中,LSTM 会从头到尾读取输入序列。 但是,在双向 LSTM 中,有第二个 LSTM 从最后到第一个读取序列,即反向 RNN。 当当前时间戳的预测取决于序列中进一步输入时,这种类型的 LSTM 可以提高模型表现。 考虑示例“我看过漫画”和“我昨天看过漫画”。 在这种情况下,基于将来出现的标记,相同的标记(即read
)具有不同的含义。 我们将在本秘籍中探讨其实现。
该秘籍建立在“多层 LSTM”秘籍的基础上,因此在尝试该秘籍之前,请务必先完成该秘籍。
在此秘籍中,我们将修改“多层 LSTM”秘籍中的类定义,使其成为双向 LSTM:
bidirectional
参数设置为True
:self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=num_layers, bidirectional=True)
self.fc = nn.Linear(2*hidden_dim, output_dim)
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))
>>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 的更多信息。
迁移学习是深度学习中的一个重要概念,它使我们可以将深度学习用于各种日常任务。 这是一种机器学习技术,其中针对任务训练的模型被重用以创建用于类似任务的新模型。 我们采用在大型数据集上训练的模型,并将其知识迁移到较小的数据集。 对于具有卷积神经网络(CNN)的计算机视觉任务,我们冻结了网络的早期卷积层,仅训练了最后几层。 早期的卷积层提取适用于整个图像的通用低级特征,以检测边缘,图案和梯度,而稍后的层识别图像中的特定特征,并且特定于数据集。
在本章中,我们将训练图像分类器以区分正常患者和肺炎患者的胸部 X 光,并使用经过训练的 ResNet-50 模型进行迁移学习。 我们将替换分类器,并有两个输出单元代表正常和肺炎分类。
我们将在以下阶段完成迁移学习任务:
在本章中,我们将介绍以下秘籍:
要完成此秘籍,我们需要 Torch 1.2 或更高版本,强烈建议我们使用支持 CUDA 的设备。
在本秘籍中,我们将采用经过预训练的 ResNet 模型,并修改最后一层以适合我们所需的输出。 与 ImageNet 数据集中用于训练 ResNet-50 模型的类的数量相比,我们只需要两个类。 我们将修改 ResNet 模型的最后一个池化层和全连接分类器。 我们将进一步将模型的训练限制为仅添加新添加的分类器单元,并且将保留所有其余层以免更新权重。 这称为冻结模型。 让我们看一下如何实现秘籍。
此秘籍要求我们下载特定的数据集。 我们将从这里获取数据集。 为了完成此秘籍,您的 PyTorch 安装应为 1.2 或更高版本,强烈建议您使用支持 CUDA 的设备。
在本秘籍中,我们将训练我们的神经网络,并从在 ImageNet 数据集上训练的预训练模型 ResNet-50 开始:
>>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 上。 我们将传入模型,训练数据,优化器和模型训练准则,并将返回训练损失。
我们将在此秘籍中实现训练函数:
>>def train(model, device, train_loader, criterion, optimizer, epoch, writer):
model.train()
total_loss = 0
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()
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 上。 我们还将添加工具函数,通过绘制图像并以可读的方式标记这些图像来记录模型中的一些错误分类。
在本秘籍中,我们将实现模型测试以及工具函数:
>>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]
)
>>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)
>>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)
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 数据集执行图像归一化。
在此秘籍中,我们将加载数据集:
>>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])
]),
}
>>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/'
>>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'])
}
>>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)
}
>>LABEL = dict((v,k) for k,v in data['train'].class_to_idx.items())
有了这个秘籍,我们就可以准备好数据集。
在此秘籍中,我们定义了训练,验证和测试数据集中图像所需的转换。 根据数据集选择变换,并且归一化变换中的值来自 ImageNet 统计信息。 然后,我们定义了数据集路径,模型名称,批量大小和日志目录。 然后,我们使用datasets.ImageFolder()
方法根据文件夹名称加载数据,并为每个数据集创建一个迭代器。
请注意,我们翻转了验证数据集和测试数据集目录。 这是因为给定数据集的验证数据集确实很小,因此我们将测试数据集用于验证数据集。
我们还使用DataLoader
为我们的训练,测试和验证数据集创建了迭代器。 然后,我们创建了一个LABEL
常量,这是一个字典,将分类器输出索引映射到类名。
在此秘籍中,我们将创建一个写入 TensorBoard 的对象。 我们使用SummaryWriter
对象写入 TensorBoard。 我们可以使用 TensorBoard 编写标量值,绘图图和绘图图像以及其他。 我们将定义一个返回 TensorBoard SummaryWriter
对象以记录我们的模型指标的函数。
此秘籍要求我们安装 TensorBoard 库。
我们需要安装 TensorBoard 夜间版本:
pip install tb-nightly
这样,我们准备实现该秘籍。
在此秘籍中,我们将创建编写器对象以将数据登录到 TensorBoard。
tensorboard --logdir=log_dir/ --port 6006
您可以通过转到http://localhost:6006/
在浏览器上访问 TensorBoard。
>>from torch.utils.TensorBoard import SummaryWriter
>>def tb_writer():
timestr = time.strftime("%Y%m%d_%H%M%S")
writer = SummaryWriter(PATH_to_log_dir + timestr)
return writer
>>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 层,并训练模型以较低的学习率微调权重。
在本秘籍中,我们将完成模型训练。
>>device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
>>model = get_model().to(device)
>>criterion = nn.NLLLoss()
>>optimizer = optim.Adam(model.parameters())
>>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()
>>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% |
>>def unfreeze(model):
for param in model.parameters():
param.requires_grad = True
>>model.load_state_dict(torch.load(model_path))
>>unfreeze(model)
>>optimizer = optim.Adam(model.parameters(), lr=1e-6)
>>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% |
>>writer.close()
在以下屏幕截图中,我们可以看到训练 TensorBoard 中查看的模型后生成的图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wiy1RJMw-1681785960494)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/dc1e4acc-1e1d-4f51-9f65-759e60b2d923.png)]
图 1:TensorBoard 主仪表板
这是我们创建的图像网格:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YbHB1Kfs-1681785960494)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/ad38f1c4-c40d-4a81-873f-769fb7eb97d1.png)]
图 2:图像网格
这是一个预示肺炎的误分类示例,但这实际上是正常的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CPsIesrs-1681785960494)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/e4991ef7-4d56-4d48-b0d4-ca3e390364b8.png)]
图 3:预计的肺炎,实际上是正常的
这是一个预测正常的误分类示例,但实际上是肺炎:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5yDAe6mK-1681785960494)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/d9a5e2c2-c006-4d12-949d-a729f49a86eb.png)]
图 4:预测的正常,实际上是肺炎
这是显示我们的训练损失随时间减少的图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z15LHRSe-1681785960495)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/bc088a69-91a6-4b3c-b5c8-aa997eb9a256.png)]
图 5:训练损失
这是显示我们的测试损失随时间减少的图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2nPFJ2n1-1681785960495)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/95de82df-c218-4340-b743-2dd9f50bc858.png)]
图 6:测试损失
以下图表显示了我们的精度随着时间的推移而提高:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wICIHxZI-1681785960495)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/pt-ai-fund/img/fec7751c-9387-4092-ad3f-98ade9667ec3.png)]
图 7:准确率得分
通过此秘籍,我们已经训练了模型并在 TensorBoard 中可视化了其结果。
在此秘籍中,我们创建了模型并将其移至可用设备,并分别使用负对数损失和 Adam 作为我们的标准和优化器。 train_epochs()
方法用于在定义的周期范围内训练模型。 在每个周期结束时,我们使用writer.flush()
方法来确保所有未决事件已写入磁盘。 最后,我们使用writer.close()
刷新关闭编写器。 我们还在此函数中保存了最佳模型,以供日后重新加载。
然后,我们从到目前为止的训练中重新加载了最佳模型,并对其进行了冻结以进行微调。 解冻后,所有模型参数均可用于训练。 我们将优化器设置为较低的学习率,对该未冻结的模型进行了更多的训练,并记录了模型的表现。 我们看到,通过微调,我们的模型表现更好。
从 TensorBoard 图中,我们看到冻结模型中的指标为橙色,解冻后的指标为蓝色,这表明模型解冻后的表现有所提高。 然后,我们从训练数据中采样了图像网格图,并从各个周期对分类错误的示例进行了采样。
我们看到训练和测试损失在各个周期都减少了,准确率提高了。
在此秘籍中,我们可以进一步编写一个函数来确定测试数据集指标,使用add_histogram()
方法将直方图添加到 TensorBoard 中,并使用其他预训练的网络训练模型。
有关更多详细信息,请参阅以下内容: