在本章中,我们将学习卷积神经网络(CNN)。 这是与前几章讨论的神经网络不同的一类。 CNN 在计算机视觉领域已经取得了巨大的成功,随着我们对它们的了解越来越多,我们将能够理解其中的原因。
CNN 是一种特殊的网络,可以将图像作为张量接收。 彩色图像由红色,绿色和蓝色三个颜色通道组成,称为 RGB。 我们将这些二维矩阵通道堆叠起来以形成彩色图像; 每个通道的值变化会产生不同的颜色。 CNN 将图像作为三个独立的堆叠颜色层,一个层放在另一个层上。
图像从附近的一个设置像素中获得其含义,但是单个像素不能保存有关整个图像的太多信息。 在也称为密集层的全连接神经网络中,一层中的每个节点都连接到下一层中的每个其他节点。 CNN 利用像素之间的空间结构来减少两层之间的连接数,从而显着提高训练速度,同时减少模型参数。
这是显示全连接网络的图像:
将上一张图像与下一张图像进行比较,后者显示了一个卷积网络:
CNN 使用过滤器从输入图像中拾取特征; 具有足够数量的过滤器的 CNN 可以检测图像中的各种特征。 随着我们越来越向后一层移动,这些过滤器在检测复杂特征方面变得越来越复杂。 卷积网络使用这些过滤器并逐一映射它们以创建特征出现的映射。
在本章中,我们将介绍以下秘籍:
- 探索卷积
- 探索池化
- 探索转换
- 执行数据扩充
- 加载图像数据
- 定义 CNN 架构
- 训练图像分类器
在本章中,您将需要在上一章中安装的 TorchVision。 您最好在支持 GPU 的计算机上运行这些秘籍中的代码。
卷积是 CNN 中的一个组成部分。 它们被定义为 CNN 中的一层。 在卷积层中,我们将过滤器矩阵从左到右,从上到下在整个图像矩阵上滑动,然后取过滤器的点积,此斑块将过滤器的尺寸跨过图像通道。 如果两个矩阵在相同位置具有较高的值,则点积的输出将较高,反之亦然。 点积的输出是标量值,该标量值标识图像中的像素模式和由过滤器表示的像素模式之间的相关性。 不同的过滤器会以不同的复杂度从图像中检测不同的特征。
我们需要了解 CNN 的另外两个关键元素,如下所示:
- 跨步:这是在图像的下一个小块上使用过滤器应用卷积网络之前,我们水平和垂直移动的像素数。
- 填充:这是我们在卷积时应用于图像边缘的策略,具体取决于我们是在卷积后要保持张量的尺寸不变还是仅在过滤器适合的情况下对输入图像应用卷积。 如果要保持尺寸不变,则需要对边缘进行零填充,以使原始尺寸在卷积后与输出匹配。 这称为相同填充。 但是,如果我们不想保留原始尺寸,则会将过滤器无法完全容纳的位置截断,这称为有效填充。
这是这两个填充的示意图:
下图显示了有效填充的示例:
在本秘籍中,我们将学习如何在 PyTorch 中使用卷积神经网络。
在此秘籍中,我们将探讨卷积:
- 首先,我们将导入所需的割炬
modules
:
>>import torch
>>import torch.nn as nn
- 接下来,我们将 2D 卷积应用于图像:
>>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 的初始层移到后面的层时,我们希望在图像中识别出比实际逐个像素信息更多的概念意义,因此我们想从输入和抛出中识别并保留剩下的关键信息。 池化层可以帮助我们做到这一点。
这是最大池化的示意图:
这是使用池化层的主要原因:
- 减少计算数量:通过减少输入的空间尺寸而不会损失过滤器,我们可以获得更好的计算表现,因此我们减少了训练所需的时间以及计算资源。
- 防止过拟合:随着空间尺寸的减小,我们减少了模型具有的参数数量,从而降低了模型的复杂性并有助于我们更好地概括。
- 位置不变性:这使 CNN 可以捕获图像中的特征,而不管特征在给定图像中的位置。 假设我们正在尝试建立一个分类器来检测芒果。 芒果位于图像的中心,左上角,右下角还是图像中的任何位置都没关系-需要对其进行检测。 池化层可以帮助我们。
池的类型很多,例如最大池,平均池,和池等。 但是,最大池化是最受欢迎的。 以与处理卷积层相同的方式,我们将定义一个窗口并在该窗口中应用所需的池化操作。 我们将根据层的跨度水平和垂直地滑动窗口。
在此秘籍中,我们将研究如何在 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
- 我们将选择 20% 的训练数据作为验证数据:
>>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)
- 接下来,我们将数据集分成多个批量。 我们将批量大小设置为 16:
>>batch_size = 16
- 然后,我们将在 PyTorch 中使用
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。 我们可以使用它来计算每个批量中模型的准确率,并将其汇总到整个测试数据集中。
您可以在这个页面上看到测试模型的示例实现。