CV with PyTorch 03 - Train multi-layer convolutional neural network

294 阅读10分钟

训练多层卷积神经网络

一、多层卷积神经网络

在前面的卷积神经网络中,我们了解了卷积滤波器是如何从图像中提取特征的。对于MNIST数据集中的图片(28 * 28的灰度图),我们使用9个大小为 5 * 5 的滤波器,得到了一个9 * 24 * 24 张量

我们可以使用相同的卷积思想,以提取图像中的更高层次的模式。例如,像8和9这样的数字的圆形边缘可以由许多较小的笔画组成。为了识别这些模式,我们可以在第一层结果的基础上再建立一层卷积滤波器。

import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np

from pytorchcv import load_mnist, train, plot_results, plot_convolution, display_dataset
load_mnist(batch_size=128)

1. 池化(合并)层

第一卷积层寻找原始模式,如水平或垂直线。下一级别的卷积层基于原始模式寻找更高层次的模式,如原始形状。更多卷积层可以将这些形状组合成图片的某些部分,最终得到我们需要分类的对象。这也说明了创建提取模式的层次结构。

在应用多层卷积层时,我们还需要应用一个技巧: 减小图像的空间大小。一旦我们检测到滑动窗口中存在水平描边,它发生在哪个确切像素上就不那么重要了。因此,我们可以“缩小”图像的大小,这是使用一个池化层(有两种)完成的:

  1. 均值池化: 采用一个滑动窗口(例如,2x2像素)并计算窗口内的平均值
  2. 最大值池化: 将窗口替换为最大值。最大池是在滑动窗口中旨在检测特定模式的存在。

5-multilayer-convolutions-1.png

因此,在一个典型的 CNN 中,将由几个卷积层组成,其间有池化层,以减少图像的维数。我们还会增加过滤器的数量,因为随着模式变得更加先进,我们需要寻找更多可能的有趣组合。

5-multilayer-convolutions-2.png

先卷积后池化,为何输出为此?

  • 注意:
  1. 池化滤波器的步长和卷积滤波器步长不一致。池化滤波器的步长和长宽保持一致,而卷积滤波器的步长是 1。所以 28 * 28 经过卷积滤波器得到的结果为:26 * 26,先将原图像除以对应的池化滤波器宽高,然后再减去对应的(w//2, h//2) 得到 12 * 12 (多出的一行一列不要了)
  2. 可以看出第二个卷积层的输入张量为 10 * 12 * 12,经过 20 个 2 * 2 的卷积滤波器后得到的张量应该为 20 * 10 * 10, 再经过最大值池化层(步长为2),得到 20 * 4 * 4 (多出的一行一列不要了)

由于空间维数的减少和特征/过滤器维数的增加,这种体系结构也称为金字塔体系结构。

class MultiLayerCNN(nn.Module):
    def __init__(self):
        super(MultiLayerCNN, self).__init__()
        # 参数分别为输入图像通道数,使用的卷积滤波器个数,卷积核大小
        self.conv1 = nn.Conv2d(1, 10, 5)
        # 最大值池化大小
        self.pool = nn.MaxPool2d(2, 2)
        # 第二个卷积层
        self.conv2 = nn.Conv2d(10, 20, 5)
        # 定义稠密层
        self.fc = nn.Linear(320,10)

    def forward(self, x):
        # 第一层卷积池化
        x = self.pool(nn.functional.relu(self.conv1(x)))
        # 第二层卷积池化
        x = self.pool(nn.functional.relu(self.conv2(x)))
        # 没有使用 Flatten 函数将张量展开
        x = x.view(-1, 320)
        # 使用log_softmax函数对稠密层的结果进行处理
        x = nn.functional.log_softmax(self.fc(x),dim=1)
        return x

net = MultiLayerCNN()
summary(net,input_size=(1,1,28,28))
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
MultiLayerCNN                            --                        --
├─Conv2d: 1-1                            [1, 10, 24, 24]           260
├─MaxPool2d: 1-2                         [1, 10, 12, 12]           --
├─Conv2d: 1-3                            [1, 20, 8, 8]             5,020
├─MaxPool2d: 1-4                         [1, 20, 4, 4]             --
├─Linear: 1-5                            [1, 10]                   3,210
==========================================================================================
Total params: 8,490
Trainable params: 8,490
Non-trainable params: 0
Total mult-adds (M): 0.47
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.06
Params size (MB): 0.03
Estimated Total Size (MB): 0.09
==========================================================================================

上面定义多层卷积时的要点:

  1. 我们不使用Flatten层,而是使用视图函数将forward前向函数中的张量展开,类似于 numpy 中的reshape函数。因为扁平化层没有可训练的权重(无需更新迭代),所以我们不需要在类中创建一个单独的层实例——我们只需要使用一个来自 torch.nn.functional 名称空间的函数。
  2. 我们在模型中只使用了一个池层实例,这也是因为它不包含任何可训练的参数,因此一个实例可以被有效地重用。上面仅仅定义了一个最大池化层,但是也完成了池化的功能。
  3. 可训练参数的数量(约8.5 k)明显小于以前的情况(在感知器中为80k,在单层 CNN 中为50k)。这是因为 卷积层通常参数很少,与输入图像的大小无关。此外,由于合并,在应用最终的密集层之前,图像的维度显著降低。少量的参数对我们的模型有积极的影响,因为它有助于防止过度拟合,即使在较小的数据集上也可以有效防止过拟合。
hist = train(net,train_loader,test_loader,epochs=5)
Epoch  0, Train acc=0.952, Val acc=0.982, Train loss=0.001, Val loss=0.000
Epoch  1, Train acc=0.983, Val acc=0.981, Train loss=0.000, Val loss=0.000
Epoch  2, Train acc=0.985, Val acc=0.984, Train loss=0.000, Val loss=0.000
Epoch  3, Train acc=0.986, Val acc=0.984, Train loss=0.000, Val loss=0.000
Epoch  4, Train acc=0.987, Val acc=0.985, Train loss=0.000, Val loss=0.000

可以看出,我们的模型获得更高的精确度,更快的迭代速度(只需要1到2个Epoch)。这意味着复杂的网络架构只需要更少的时间便能明确任务所在,并从我们的图像中提取通用模式。

二、使用来自 cifar-10数据集的真实图像

虽然我们的手写数字识别问题可能看起来像一个小儿科,但我们现在已经准备好做一些更严肃的事情了。 让我们探索更高级的不同物体图片数据集,称为 CIFAR-10。 它包含 60k的 32x32 彩色图像,分为 10 个类别。

transform = torchvision.transforms.Compose(
    # 转化为张量
    [torchvision.transforms.ToTensor(),
     # 进行规范化
     torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]
)

# 导入训练集
trainset = torchvision.datasets.CIFAR10(
    root='./data', 
    train=True, 
    download=True, 
    transform=transform
)
# 定义训练集载入器,便于成批处理
trainloader = torch.utils.data.DataLoader(
    trainset, 
    batch_size=14, 
    shuffle=True
)

# 导入训练集
testset = torchvision.datasets.CIFAR10(
    root='./data', 
    train=False, 
    download=True, 
    transform=transform
)

# 定义测试集载入器,便于成批处理
testloader = torch.utils.data.DataLoader(
    testset, 
    batch_size=14, 
    shuffle=False
)

# 定义各分类
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data\cifar-10-python.tar.gz



  0%|          | 0/170498071 [00:00<?, ?it/s]


Extracting ./data\cifar-10-python.tar.gz to ./data
Files already downloaded and verified
display_dataset(trainset,classes=classes)

output_19_0.png

CIFAR-10 的一个著名架构称为 LeNet,由 Yann LeCun 提出。 它遵循我们上面概述的相同原则。 但是,由于所有图像都是彩色的,因此输入张量大小为 3 × 32 × 32 ,并且在颜色维度上也应用了 5 × 5 卷积滤波器 - 这意味着卷积核矩阵的大小为 3×5×5 (三维)。

我们还对这个模型进行了更多的简化——我们不使用 log _ softmax 作为输出激活函数,只返回最后一个完全连接层的输出。在这种情况下,我们可以使用CrossEntropyLoss损失函数对模型进行优化。

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.conv3 = nn.Conv2d(16,120,5)
        # 此处没有使用view函数进行扁平化
        self.flat = nn.Flatten()
        self.fc1 = nn.Linear(120,64)
        self.fc2 = nn.Linear(64,10)

    def forward(self, x):
        x = self.pool(nn.functional.relu(self.conv1(x)))
        x = self.pool(nn.functional.relu(self.conv2(x)))
        x = nn.functional.relu(self.conv3(x))
        x = self.flat(x)
        x = nn.functional.relu(self.fc1(x))
        x = self.fc2(x)
        return x

net = LeNet()

summary(net,input_size=(1,3,32,32))
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
LeNet                                    --                        --
├─Conv2d: 1-1                            [1, 6, 28, 28]            456
├─MaxPool2d: 1-2                         [1, 6, 14, 14]            --
├─Conv2d: 1-3                            [1, 16, 10, 10]           2,416
├─MaxPool2d: 1-4                         [1, 16, 5, 5]             --
├─Conv2d: 1-5                            [1, 120, 1, 1]            48,120
├─Flatten: 1-6                           [1, 120]                  --
├─Linear: 1-7                            [1, 64]                   7,744
├─Linear: 1-8                            [1, 10]                   650
==========================================================================================
Total params: 59,386
Trainable params: 59,386
Non-trainable params: 0
Total mult-adds (M): 0.66
==========================================================================================
Input size (MB): 0.01
Forward/backward pass size (MB): 0.05
Params size (MB): 0.24
Estimated Total Size (MB): 0.30
==========================================================================================

训练这个网络将需要大量的时间,最好是在支持 gpu 的计算机上完成。

  • 注意: 为了获得更好的训练效果,我们可能需要对一些训练参数进行试验,比如学习率。因此,我们在这里显式定义了一个随机梯度下降优化器(SGD) ,并传递训练参数。你可以调整这些参数,观察它们是如何影响训练的。
opt = torch.optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
hist = train(net, trainloader, testloader, epochs=3, optimizer=opt, loss_fn=nn.CrossEntropyLoss())
Epoch  0, Train acc=0.253, Val acc=0.382, Train loss=0.145, Val loss=0.123
Epoch  1, Train acc=0.437, Val acc=0.478, Train loss=0.110, Val loss=0.103
Epoch  2, Train acc=0.506, Val acc=0.536, Train loss=0.097, Val loss=0.093

我们用三个Epoch的训练所能达到的准确性似乎并不是很高。然而,请记住,盲目猜测(不使用模型,瞎猜)只能给我们10% 的准确率,而且我们的问题实际上要比 MNIST 数字分类困难得多。在如此短的训练时间内达到50% 以上的准确率似乎是一个很好的成就。

三、小结:

在这个单元中,我们学习了计算机视觉神经网络背后的主要概念——卷积网络。后续的支持图像分类、图像目标检测、甚至图像生成网络的现实架构都是基于 cnn 的,只是增加了更多的层次和一些额外的训练技巧罢了。

四、错误小结

1. 我们应用哪些层来大幅降低多层 CNN 中的空间维度?

池化层(如 MaxPooling 或 AveragePooling)用于降低维度,通常降低二分之一

2. 在网络的卷积基与最终线性分类器之间使用哪一层?

Flatten平展用于将空间张量重塑为线性向量,而池化层池化层用于降低卷积层之间的维度(也就是身处两个卷积层之间)

3. 多通道卷积核

  1. 这里就要涉及到“卷积核”和“filter”这两个术语的区别。在只有一个通道的情况下,“卷积核”就相当于“filter”,这两个概念是可以互换的。但在一般情况下,它们是两个完全不同的概念。每个“filter”实际上恰好是“卷积核”的一个集合,在当前层,每个通道都对应一个卷积核,且这个卷积核是独一无二的。

  2. 多通道卷积的计算过程:将矩阵与滤波器对应的每一个通道进行卷积运算,最后相加,形成一个单通道输出,加上偏置项后,我们得到了一个最终的单通道输出。如果存在多个filter,这时我们可以把这些最终的单通道输出组合成一个总输出。

  3. 这里我们还需要注意一些问题——滤波器的通道数、输出特征图的通道数。

    • 某一层滤波器的通道数 = 上一层特征图的通道数。我们输入一张 6 * 6 * 3 的RGB图片,那么滤波器( 3 * 3 * 3 )也要有三个通道。

    • 某一层输出特征图的通道数 = 当前层滤波器的个数。如上图所示,当只有一个filter(多个卷积核)时,输出特征图( 4 * 4 )的通道数为1(将所有卷积核结果叠加);当有2个filter时,输出特征图(2 * 4 * 4 )的通道数为2。