在 PyTorch 中构建卷积神经网络

神经网络由相互连接的层构建而成。有许多不同类型的层。对于与图像相关的应用程序,您始终可以找到卷积层。它是一个参数很少的层,但应用于大尺寸输入。它之所以强大,是因为它可以保留图像的空间结构。因此,它被用于在计算机视觉神经网络上产生最先进的结果。在这篇文章中,您将了解卷积层及其构建的网络。

在 PyTorch 中构建卷积神经网络

推荐:将NSDT场景编辑器加入你的3D工具链

3D工具集:NSDT简石数字孪生

在 PyTorch 中构建卷积神经网络

完成这篇文章后,您将知道:

  • 什么是卷积层和池化层
  • 它们如何在神经网络中组合在一起
  • 如何使用卷积层的神经网络

概述

这篇文章分为四个部分;他们是

  • 卷积神经网络的案例
  • 卷积神经网络的构建块
  • 卷积神经网络示例
  • 特征图中有什么?

卷积神经网络的案例

让我们考虑制作一个神经网络来处理灰度图像作为输入,这是计算机视觉深度学习中最简单的用例。

灰度图像是一个像素数组。每个像素通常是一个介于 0 到 255 之间的值。大小为 32×32 的图像将具有 1024 像素。将其作为神经网络的输入意味着第一层至少有 1024 个输入权重。

查看像素值对理解图片几乎没有用处,因为数据隐藏在空间结构中(例如,图片上是否有水平线或垂直线)。因此,传统的神经网络将发现很难从图像输入中找出信息。

卷积神经网络是利用卷积层来保留像素的空间信息。它了解相邻像素的相似程度并生成特征表示。卷积层从图片中看到的东西在某种程度上与失真是不变的。例如,卷积神经网络可以预测相同的结果,即使输入图像的颜色发生了变化、旋转或重新缩放。此外,卷积层的权重较小,因此更容易训练。

卷积神经网络的构建块

卷积神经网络最简单的用例是分类。您会发现它包含三种类型的图层:

  1. 卷积层
  2. 池化层
  3. 全连接层

卷积层上的神经元称为过滤器。通常它是图像应用中的二维卷积层。过滤器是应用于输入图像像素的 2D 补丁(例如,2×3 像素)。这个3D贴片的大小也称为感受野,这意味着它一次可以看到图像的多大部分。

卷积层的滤波器是与输入像素相乘,然后求和结果。此结果是输出处的一个像素值。过滤器将在输入图像周围移动以填充输出中的所有像素值。通常将多个过滤器应用于同一输入,从而产生多个输出张量。这些输出张量称为该层生成的特征图。它们作为一个张量堆叠在一起,并作为输入传递到下一层。

应用于二维输入以创建特征映射的筛选器示例

卷积层的输出称为特征图,因为它通常了解输入图像的特征。例如,位置是否有垂直线。从像素中学习特征有助于在更高层次上理解图像。多个卷积层堆叠在一起,以便从较低级别的细节推断出更高级别的特征。

池化层是对前一层的特征图进行下采样。它通常在卷积层之后使用,以巩固学习的特征。它可以压缩和概化特征表示。池化层也有一个感受野,通常是取感受野上所有值的平均值(平均池化)或最大值(最大池化)。

全连接层通常是网络中的最后一层。它是将先前的卷积层和池化层合并的特征作为输入来生成预测。可能有多个完全连接的层堆叠在一起。在分类的情况下,您通常会看到最终全连接层的输出与 softmax 函数一起应用以生成类似概率的分类。

想开始使用 PyTorch 进行深度学习吗?

立即参加我的免费电子邮件速成课程(带有示例代码)。

单击以注册并获得该课程的免费PDF电子书版本。

下载您的免费迷你课程

卷积神经网络示例

以下是对 CIFAR-10 数据集进行图像分类的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
 
transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
 
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
 
batch_size = 32
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)
testloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)
 
class CIFAR10Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=(3,3), stride=1, padding=1)
        self.act1 = nn.ReLU()
        self.drop1 = nn.Dropout(0.3)
 
        self.conv2 = nn.Conv2d(32, 32, kernel_size=(3,3), stride=1, padding=1)
        self.act2 = nn.ReLU()
        self.pool2 = nn.MaxPool2d(kernel_size=(2, 2))
 
        self.flat = nn.Flatten()
 
        self.fc3 = nn.Linear(8192, 512)
        self.act3 = nn.ReLU()
        self.drop3 = nn.Dropout(0.5)
 
        self.fc4 = nn.Linear(512, 10)
 
    def forward(self, x):
        # input 3x32x32, output 32x32x32
        x = self.act1(self.conv1(x))
        x = self.drop1(x)
        # input 32x32x32, output 32x32x32
        x = self.act2(self.conv2(x))
        # input 32x32x32, output 32x16x16
        x = self.pool2(x)
        # input 32x16x16, output 8192
        x = self.flat(x)
        # input 8192, output 512
        x = self.act3(self.fc3(x))
        x = self.drop3(x)
        # input 512, output 10
        x = self.fc4(x)
        return x
 
model = CIFAR10Model()
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
 
n_epochs = 20
for epoch in range(n_epochs):
    for inputs, labels in trainloader:
        # forward, backward, and then weight update
        y_pred = model(inputs)
        loss = loss_fn(y_pred, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
 
    acc = 0
    count = 0
    for inputs, labels in testloader:
        y_pred = model(inputs)
        acc += (torch.argmax(y_pred, 1) == labels).float().sum()
        count += len(labels)
    acc /= count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))
 
torch.save(model.state_dict(), "cifar10model.pth")

CIFAR-10 数据集提供 32×32 像素的 RGB 颜色图像(即 3 个颜色通道)。有 10 个类,以整数 0 到 9 标记。每当您使用图像的 PyTorch 神经网络模型时,您都会发现姊妹库很有用。在上面,你用它从互联网上下载了 CIFAR-10 数据集,并将其转换为 PyTorch 张量:torchvision

1
2
3
4
5
...
transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
 
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)

您还使用了 PyTorch 中的 来帮助创建用于训练的批处理。训练是使用随机梯度下降来优化模型的交叉熵损失。它是一个分类模型,因此分类的准确性比交叉熵更直观,交叉熵是在每个纪元结束时计算的,通过将输出 logit 中的最大值与数据集的标签进行比较:DataLoader

1
2
...
acc += (torch.argmax(y_pred, 1) == labels).float().sum()

运行上面的程序来训练网络需要时间。该网络应该能够在分类中达到70%以上的准确率。

在网络中,图像分类通常在早期阶段由卷积层组成,辍学层和池化层交错。然后,在稍后阶段,卷积层的输出被展平并由一些全连接层处理。

特征图中有什么?

上面定义的网络中有两个卷积层。它们都定义为内核大小为 3×3,因此它一次查看 9 个像素以生成一个输出像素。请注意,第一个卷积层将RGB图像作为输入。因此,每个像素都有三个通道。第二个卷积层采用具有 32 个通道的特征图作为输入。它看到的每个“像素”将有 32 个值。因此,第二卷积层具有更多的参数,即使它们具有相同的感受野。

让我们看看特征图中有什么。假设我们从训练集中选择一个输入样本:

1
2
3
4
import matplotlib.pyplot as plt
 
plt.imshow(trainset.data[7])
plt.show()

您应该看到这是一匹马的图像,32×32像素,带有RGB通道:

首先,您需要将其转换为 PyTorch 张量,并使其成为一个图像的批处理。PyTorch 模型期望每个图像作为(通道、高度、宽度)格式的张量,但您读取的数据采用 (高度、宽度、通道) 格式。如果您用于将图像转换为 PyTorch 张量,则此格式转换会自动完成。否则,您需要在使用前排列尺寸。torchvision

之后,将其传递到模型的第一个卷积层并捕获输出。你需要告诉 PyTorch 这个计算不需要梯度,因为你不打算优化模型权重:

1
2
3
4
X = torch.tensor([trainset.data[7]], dtype=torch.float32).permute(0,3,1,2)
model.eval()
with torch.no_grad():
    feature_maps = model.conv1(X)

特征图在一个张量中。您可以使用 matplotlib 可视化它们:

1
2
3
4
5
fig, ax = plt.subplots(4, 8, sharex=True, sharey=True, figsize=(16,8))
for i in range(0, 32):
    row, col = i//8, i%8
    ax[row][col].imshow(feature_maps[0][i])
plt.show()

您可能会看到以下内容:

您可以看到它们被称为特征图,因为它们突出显示了输入图像中的某些特征。使用小窗口(在本例中为 3×3 像素过滤器)标识要素。输入图像有三个颜色通道。每个通道都应用了不同的滤波器,并且它们的结果将合并为输出要素。

类似地,您可以从第二个卷积层的输出中显示特征图,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
X = torch.tensor([trainset.data[7]], dtype=torch.float32).permute(0,3,1,2)
 
model.eval()
with torch.no_grad():
    feature_maps = model.act1(model.conv1(X))
    feature_maps = model.drop1(feature_maps)
    feature_maps = model.conv2(feature_maps)
 
fig, ax = plt.subplots(4, 8, sharex=True, sharey=True, figsize=(16,8))
for i in range(0, 32):
    row, col = i//8, i%8
    ax[row][col].imshow(feature_maps[0][i])
plt.show()

其中显示:

与第一卷积层的输出相比,来自第二卷积层的特征图看起来更模糊且更抽象。但这些对于模型识别对象更有用。

将所有内容放在一起,下面的代码加载上一节中保存的模型并生成特征图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
 
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True)
 
class CIFAR10Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=(3,3), stride=1, padding=1)
        self.act1 = nn.ReLU()
        self.drop1 = nn.Dropout(0.3)
 
        self.conv2 = nn.Conv2d(32, 32, kernel_size=(3,3), stride=1, padding=1)
        self.act2 = nn.ReLU()
        self.pool2 = nn.MaxPool2d(kernel_size=(2, 2))
 
        self.flat = nn.Flatten()
 
        self.fc3 = nn.Linear(8192, 512)
        self.act3 = nn.ReLU()
        self.drop3 = nn.Dropout(0.5)
 
        self.fc4 = nn.Linear(512, 10)
 
    def forward(self, x):
        # input 3x32x32, output 32x32x32
        x = self.act1(self.conv1(x))
        x = self.drop1(x)
        # input 32x32x32, output 32x32x32
        x = self.act2(self.conv2(x))
        # input 32x32x32, output 32x16x16
        x = self.pool2(x)
        # input 32x16x16, output 8192
        x = self.flat(x)
        # input 8192, output 512
        x = self.act3(self.fc3(x))
        x = self.drop3(x)
        # input 512, output 10
        x = self.fc4(x)
        return x
 
model = CIFAR10Model()
model.load_state_dict(torch.load("cifar10model.pth"))
 
plt.imshow(trainset.data[7])
plt.show()
 
X = torch.tensor([trainset.data[7]], dtype=torch.float32).permute(0,3,1,2)
model.eval()
with torch.no_grad():
    feature_maps = model.conv1(X)
fig, ax = plt.subplots(4, 8, sharex=True, sharey=True, figsize=(16,8))
for i in range(0, 32):
    row, col = i//8, i%8
    ax[row][col].imshow(feature_maps[0][i])
plt.show()
 
with torch.no_grad():
    feature_maps = model.act1(model.conv1(X))
    feature_maps = model.drop1(feature_maps)
    feature_maps = model.conv2(feature_maps)
fig, ax = plt.subplots(4, 8, sharex=True, sharey=True, figsize=(16,8))
for i in range(0, 32):
    row, col = i//8, i%8
    ax[row][col].imshow(feature_maps[0][i])
plt.show()

3D建模学习工作室 翻译整理,转载请注明出处!

NSDT场景编辑器 | NSDT 数字孪生 | GLTF在线编辑器 | 3D模型在线转换 | UnrealSynth虚幻合成数据生成器 | 3D模型自动纹理化工具
2023 power by nsdt©鄂ICP备2023000829号