Chapter6 卷积神经网络(CNN)


卷积神经网络(Convolutional Neural Network, CNN)是一种前馈神经网络,是一种专门用来处理具有类似网格结构的数据的神经网络。例如时间序列数据(可以认为是在时间轴上有规律地采样形成的一维网格)和图像数据(可以看作二维的像素网格)。卷积神经网络在诸多应用领域都表现优异。“卷积神经网络”一词表明该网络使用了 卷积(convolution)这种数学运算。卷积是一种特殊的线性运算。 卷积神经网络是指那些至少在网络的一层中使用卷积运算来替代一般的矩阵乘法运算的神经网络

CNN网络主要有三部分构成:卷积层、池化层和全连接层,其中卷积层负责提取图像中的局部特征;池化层用来大幅降低参数数量级(降维);全连接层用来输出想要的结果。下图为一个简单的卷积神经网络架构,由卷积层、池化层、全连接层组成,有些卷积神经网络还包括其他层,如正则化层、高级层等。
在这里插入图片描述
上图的网络经常用于对手写输入数据进行分类,该网络由卷积层(Conv2d)、池化层(MaxPool2d)、和全连接层(Linear)叠加而成。下面使用Pytorch搭建上图的卷积神经网络。

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

class CNNNet(nn.Module):
    def __init__(self):
        super(CNNNet, self).__init__()
        #卷积层
        self.conv1=nn.Conv2d(in_channels=3,out_channels=16,kernel_size=5,stride=1)
        #最大池化层
        self.pool1=nn.MaxPool2d(kernel_size=2,stride=2)
        self.conv2=nn.Conv2d(in_channels=16,out_channels=36,kernel_size=3,stride=1)
        self.pool2=nn.MaxPool2d(kernel_size=2,stride=2)
        #线性层
        self.fc1=nn.Linear(1296,128)
        self.fc2=nn.Linear(128,10)
    def forward(self,x):
        x=self.pool1(F.relu(self.conv1(x)))
        x=self.pool2(F.relu(self.conv2(x)))
        x=x.view(-1,36*6*6)
        x=F.relu(self.fc2(F.relu(self.fc1(x))))
        return x
net = CNNNet()
net
CNNNet(
  (conv1): Conv2d(3, 16, kernel_size=(5, 5), stride=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 36, kernel_size=(3, 3), stride=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=1296, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=10, bias=True)
)

下面将描述卷积神经网络中卷积层和池化层的工作原理,并解释填充、步幅、多通道卷积的含义。这了解完这些基础之后,将探究数个具有代表性的深度卷积神经网络。这些模型包括最早提出的AlexNet、以及后来的使用重复元素的网络VGG、网络中的网络NiN、含并行连接的网络GoogLeNet、残差网络ResNet和稠密连接网络DenseNet。

1、卷积层

卷积神经网络(CNN)是含有卷积层的网络。卷积层是卷积神经网络的核心层,而卷积有时卷积层的核心。虽然卷积层得名于卷积运算,但通常在卷积层使用更加直观的互相关运算(与卷积运算类似)。在二维卷积层中,一个二维输入数组和一个二维核数组通过互相关运算输出一个数组。

下面通过一个例子来解释二维互相关运算的含义。如下图所示,输入是一个高和宽均为3的二维数组。我们将该数组的形状记为3×3或(3,3)。核数组的高和宽分别为2,该数组在卷积运算中又称为 卷积核或过滤器(filter)。卷积核窗口(又称为卷积窗口)的形状取决于卷积核的高和宽,即2×2。下图中的阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:0×0+1×1+3×2+4×3=19。
在这里插入图片描述

在二维互相关运算中,卷积窗口从输入数组的最上方开始,按从左到右、从上往下的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。上图中的输出数组的高和宽都为2,其中,其中的四个元素的二维互相关运算为:
0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 19 1 × 0 + 2 × 1 + 4 × 2 + 5 × 3 = 25 3 × 0 + 4 × 1 + 6 × 2 + 7 × 3 = 37 4 × 0 + 5 × 1 + 7 × 2 + 8 × 3 = 43 0 \times 0+1 \times 1+3 \times 2+4 \times 3=19\\ 1 \times 0+2 \times 1+4 \times 2+5 \times 3=25\\ 3 \times 0+4 \times 1+6 \times 2+7 \times 3=37\\ 4 \times 0+5 \times 1+7 \times 2+8 \times 3=43 0×0+1×1+3×2+4×3=191×0+2×1+4×2+5×3=253×0+4×1+6×2+7×3=374×0+5×1+7×2+8×3=43

可以看出,输出数组的大小要小于输入数组,这与卷积核的大小有关。输出数组的大小不仅与卷积核的大小有关,还与后面介绍的步幅与填充的大小有关,输出的大小为多少将在后面详细介绍。由于上图中做运算的时候,步幅为1,填充为0,因此输出的大小为:输入大小-卷积核大小+1。

那卷积运算与互相关运算到底有什么关系哪?

现在大部分深度学习教程以及机器学习库中都把卷积定义为图像矩阵和卷积核的按位点乘,如我们上面介绍的那样。实际上,这种操作叫做互相关运算,而卷积运算时需要把卷积核顺时针旋转180度然后再做互相关运算,如下图为卷积操作。
在这里插入图片描述

然而由于卷积核是从数据中学习得到的,因此无论这些层执行的是严格的卷积运算还是互相关运算,卷积层的输出都不会受到影响。为了与深度学习文献中的标准术语保持一致,将"互相关运算"称为卷积运算。尽管严格来说,他们之间有一点不同。

接下来,将在corr2d函数中实现如上过程,该函数接受输入张量X和卷积核张量K,并返回输出张量Y。

import torch
from torch import nn

#计算二维互相关运算
def corr2d(X,K):
    h,w = K.shape
    #创建输出数组
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    #循环计算输出数组个每个值
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i, j] = (X[i : i+h, j:j+w] * K).sum()
    return Y

通过上图的输入张量X和卷积核张量K,我们来验证上述二维互相关运算的输出。

X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
tensor([[19., 25.],
        [37., 43.]])

二维卷积层将输入和输出做互相关运算,并加上一个变量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常先对卷积核随机初始化,然后再不断迭代卷积核和偏差。

下面是一个基于corr2d函数来实现的一个自定义的二维卷积层。在构造函数里声明weight和bias两个参数。前向计算函数forward则是直接调用corr2d函数再加上偏差。

class Conv2D(nn.Module):
    def __init__(self,kernel_size):
        super(Conv2D, self).__init__()
        self.weight = nn.Parameter(torch.rand(kernel_size))
        self.bias = nn.Parameter(torch.zeros(1))
    def forward(self,x):
        return corr2d(x,self.weight) + self.bias

1.1、卷积核

卷积核是整个卷积过程的核心。比较简单的卷积核或过滤器有Horizontalfilter、VerticalFilter、Sobel Filter等。这些过滤器能够检测图像的水平边缘、垂直边缘、增强图像中心区域权重等。下面几个例子看看它是如何工作的。

  1. 垂直边缘检测。

这个过滤器是3×3的矩阵,其特点是有值的是第1列和第3列,第2列为0.经过这个过滤器作用后,就把原数据垂直边缘检测出来了。如下图所示。

在这里插入图片描述

  1. 水平边缘检测器

这个过滤也是3×3的矩阵,其特点是有值的是第1行和第3行,第2行为0。经过这个过滤器作用之后,就把源数据水平边缘检测出来了。如下图所示。

在这里插入图片描述

  1. 过滤器对图像水平边缘检测、垂直边缘检测的效果图,如下图所示。

在这里插入图片描述

以上这些过滤器是比较简单的,在深度学习中,过滤器的作用不仅在于检测垂直边缘、水平边缘,还需要检测其他边缘特征。

下面使用代码实践一个卷积层的简单应用——检测图像中物体的边缘,即找到像素变化的位置。首相构造一个6×8的图像,它的中间四列为黑(0),其余为白(1)。

X=torch.ones((6,8))
X[:,2:6]=0
X
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.]])

下面构造一个高和宽分别为1和2的卷积核K,当它与输入做互相关运算时,如果相邻元素相同,输出为0,否则输出为非0。

K=torch.tensor([[1.0,-1.0]])
K
tensor([[ 1., -1.]])

下面将输入X与卷积核K进行卷积运算。可以看出,将从白到黑的边缘和从黑到白的边缘分别检测成了1和-1。其余部分的输出全是0。

Y=corr2d(X,K)
Y
tensor([[ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.]])

卷积核类似于标准神经网络中的权重矩阵 W W W W W W需要通过梯度下降算法反复迭代求得。同样在深度学习中,卷积核也是需要通过训练得到的。卷积神经网络主要目的就是计算出这些卷积核的数值。确定好这些卷积核之后,卷积神经网络就可以实现对图像进行特征的检测。

下面看一个例子,使用边缘检测的输入数据X和输出数据Y来学习我们构造的卷积核K。首先构造一个卷积层,将其卷积核初始化为随机张量。接下来在每一次迭代中,使用平均误差来比较Y和卷积层的输出,然后计算梯度来更新卷积核。为了简单,在此使用Pytorch内置的二维卷积层(在后面详细介绍),并忽略了偏置。

#构造一个二维卷积层,它具有一个输出通道和形状为(1,2)的卷积核
conv2d=nn.Conv2d(1,1,kernel_size=(1,2),bias=False)

#这个二维卷积层使用四维输入和输出格式(批量大小,通道,高度,宽度)
#其中批量大小和通道大小都为1
X = X.reshape((1,1,6,8))
Y = Y.reshape((1,1,6,7))
#学习率
lr = 3e-2
#开始训练
for i in range(10):
    Y_hat = conv2d(X)
    l = (Y_hat - Y)**2
    conv2d.zero_grad()
    l.sum().backward()
    #更新卷积核的权重
    conv2d.weight.data[:] -= lr * conv2d.weight.grad
    if (i + 1) % 2 == 0:
        print(f'epoch {i+1}, loss {l.sum():.3f}')
epoch 2, loss 7.387
epoch 4, loss 1.752
epoch 6, loss 0.504
epoch 8, loss 0.171
epoch 10, loss 0.064

在次迭代之后,误差已经降到足够低。现在我们来看看我们所学的卷积核的权重张量。

conv2d.weight.data.reshape((1, 2))
tensor([[ 0.9628, -1.0133]])

可以看出,学习到的卷积核权重非常接近我们之前定义的卷积核K。

1.2、步幅

在卷积运算中,卷积窗口从输入数组的最上方开始,按照从左到右、从上往下的顺序,每次滑动的行数和列数被称为步幅(strides),在图像中就是跳过的像素个数,上面的例子中,卷积窗口每次只移动一格,故参数strides=1。这个参数也可以是其他数字,在行和列的不同移动方向上步幅也可以不同,例如往左移动2个步幅,而往下移动3个步幅。下图为每次移动两个步幅的操作。

在这里插入图片描述

在卷积窗口移动的过程中,其值始终是不变的,都是卷积核的值。也可以说,卷积核的值在整个过程中都是共享的,所以又把卷积核的值称为共享变量。卷积神经网络采用参数共享的方法大大降低了参数的数量。

在进行卷积的过程中,有许许多多的卷积核,不同的卷积核提取不同的特征。

参数strides是卷积神经网络中的一个重要参数,在用Pytorch具体实现中,strides参数格式为单个整数或者是两个整数的元组。

在上图中,卷积窗口如何在继续往右移动两格,卷积窗口部分将在输入矩阵之外,如下图所示。此时就需要使用填充(Padding)

在这里插入图片描述

1.3、填充

如上所示,在卷积窗口部分在输入矩阵之外的时候,就无法进行卷积运算,因此这部分的输入将会被抛弃,因此会丢失一部分数据。而往往在边界部分的数据会带有更多的信息,解决这一问题的简单方法为填充(padding):在输入图像的边界填充元素(通常填充元素0)。如下图,将3×3输入填充到5×5,那么它的输出就变为4×4。

在这里插入图片描述

下面看一下,在添加步幅和填充后,输入矩阵在经过卷积操作后,输出矩阵的大小应该是多少。设填充的圈数为 p p p,输入数据的大小为 n n n,卷积核的大小为 f f f,步幅的大小为 s s s,则经过卷积操作后输出的大小为:
⌊ n + 2 p − f s ⌋ + 1 \lfloor \frac{n+2p-f}{s} \rfloor +1 sn+2pf+1
从公式中看出,卷积操作时,不能被整除的时候采用了向下取整。原因是:卷积核必须完整处于图像中或者填充之后的图像区域内才能输出相应的结果。

1.4、多通道上的卷积

上面介绍的卷积操作输入数据和卷积核都是单个通道的,因此在图形的角度讲来说都是灰色图像,并没有考虑彩色图片的情况。在实际情况中,输入数据或者中间特征图都是多通道的,如彩色图片都是3通道的,即R、G、B通道。多通道的卷积与单通道的卷积运算基本一致,对于多通道的数据,其对应的卷积核的通道数与输入数据是相同的。过程是将每个单通道与对应的卷积核进行卷积运算求和,然后将每个通道的和相加,得到一个输出元素,所有输出元素组合在一起形成输出矩阵。

在下图中,演示了一个具有两个输入通道的卷积运算的示例。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素:
( 1 × 1 + 2 × 2 + 4 × 3 + 5 × 4 ) + ( 0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 ) = 56 (1 \times 1+2 \times 2+4 \times 3+5 \times 4)+(0 \times 0+1 \times 1+3 \times 2+4 \times 3)=56 (1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56

在这里插入图片描述

为了加深理解,我们实现一下多输入通道卷积运算。 简而言之,我们所做的就是对每个通道执行卷积操作,然后将结果相加。

import torch

#计算二维卷积
def corr2d(X,K):
    h,w = K.shape
    #创建输出数组
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    #循环计算输出数组个每个值
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i, j] = (X[i : i+h, j:j+w] * K).sum()
    return Y

def corr2d_multi_in(X, K):
    # 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
    return sum(corr2d(x, k) for x, k in zip(X, K))

构造与上图中的值相对应的输入张量X和核张量K,以验证多通道卷积的输出。

X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])

corr2d_multi_in(X, K)
tensor([[ 56.,  72.],
        [104., 120.]])

为了实现检测更多的图像特征,可以增加更多的卷积核组,如下图所示就是两组卷积核 W 0 W_0 W0 W 1 W_1 W1。输入为7×7×3为三通道数据,经过两个3×3×3的卷积核(步幅为2),得到了3×3×2的输出(因为有两个卷积核组,因此输出通道数为2)。由下图可以看出,对输入添加了大小为1的padding,也就是在输入元素的周围补了一圈0。padding对于图像边缘的特征提取很有帮助,可以防止信息丢失。输出的通道数与卷积核组的数量有关。

在这里插入图片描述

卷积核的通道数与输入特征图的通道数一样,输出特征图的通道数与卷积核组的数量一样。

下面实现一个计算多个卷积核组对多个通道的输入进行卷积操作,得到多个通道输出的卷积运算过程。

def corr2d_multi_in_out(X, K):
    # 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
    # 最后将所有结果都叠加在一起
    return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

通过将多组多通道卷积核K与K+1(K中每个元素加)和K+2连接起来,构造了一个具有个输出通道的卷积核。

K = torch.stack((K, K + 1, K + 2), 0)
K.shape
torch.Size([3, 2, 2, 2])

下面,我们对输入张量X与卷积核张量K执行卷积运算。现在的输出包含个通道,第一个通道的结果与先前输入张量X和多输入单输出通道的结果一致。

corr2d_multi_in_out(X, K)
tensor([[[ 56.,  72.],
         [104., 120.]],

        [[ 76., 100.],
         [148., 172.]],

        [[ 96., 128.],
         [192., 224.]]])

1.5、激活函数

卷积神经网络与标准的神经网络类似,为保证其非线性,也需要使用激活函数,即在卷积运算后,把输出值另加偏置,输入到激活函数,然后作为下一层的输入,如下图所示:

在这里插入图片描述

常用的激活函数有:sigmoid、relu、tanh等,这些激活函数可看之前的章节。

1.6、Pytorch中的卷积函数

卷积函数是构建神经网络的重要支架,Pytorch中有nn.Conv1dnn.Conv2dnn.Conv3d三个函数进行卷积操作。这三个的区别是我们要处理的数据是几维的,比如当我们对一个句子进行卷积操作时,一个句子序列数一维的,因此就使用nn.Conv1d。而对于图片,它是一个二维的物体,因此我们就需要使用nn.Conv2d,在对三维的物体进行卷积操作的使用就需要使用nn.Conv3d。由于卷积神经网络经常用于处理图像,所有这里详细介绍nn.Conv2d这个函数。

torch.nn.Conv2d的格式为:

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None)
  1. 主要参数说明:
  • in_channels (int):输入数据中的通道数量。
  • out_channels (int):卷积操作产生的通道数。
  • kernel_size (int or tuple):卷积核的大小。
  • stride (int or tuple, optional):卷积的步幅。默认值:1。
  • padding (int, tuple or str, optional):在输入的所有四边添加的填充。默认值:0。
  • padding_mode (string, optional):‘zeros’, ‘reflect’, ‘replicate’或’circular’. 默认:‘zeros’。
  • dilation (int or tuple, optional):卷积核元素之间的间距。默认值:1。
  • groups (int, optional):控制输入和输出之间的连接。group=1,输出是所有的输入的卷积;group=2,此时相当于有并排的两个卷积层,每个卷积层计算输入通道的一般,并且产生的输出是输出通道的一般,随后将这两个输出连接起来。
  • bias (bool, optional): 如果是 “True”,则在输出中增加一个可学习的偏置。默认值:True。
  1. 输入输出形状
  • input: ( N , C i n , H i n , W i n ) (N,C_{in},H_{in},W_{in}) (N,Cin,Hin,Win) N为数据的个数,C为通道数,H为高度,W为宽度。
  • output: ( N , C o u t , H o u t , W o u t ) (N,C_{out},H_{out},W_{out}) (N,Cout,Hout,Wout)

更过关于卷积层的函数可以参考网址:https://pytorch.org/docs/stable/nn.html#convolution-layers

2、池化层

池化(Pooling)又称下采样,通过卷积层获得图像的特征后,理论上可以直接使用这些特征训练分类器。但是,这样做会面临巨大的计算挑战,而且很容易产生过拟合的现象。为了进一步降低网络训练参数以及模型的过拟合程度,就要对卷积层进行池化(Pooling)处理。常用的池化方式通常有3种:

  • 最大池化(Max Pooling):选择Pooling窗口中的最大值作为采样值。
  • 均值池化(Mean Pooling):将Pooling窗口中的所有值相加取平均,以平均值作为采样值。
  • 全局最大(或均值)池化:与平常最大或最小池化相对而言,全局池化是对整个特征图的池化。

这三种池化方法,可用下图来描述。
在这里插入图片描述

池化层在CNN中可用来减小尺寸,提高运算速度及减少噪声影响,让各特征更具有健壮性。池化层比卷积层更简单,他没有卷积运算,只是在池化窗口滑动区域内去最大值或者平均值。而池化的作用则体现在降采样:保留显著特征、降低特征维度、增大感受野。深度网络越往后面越能捕捉到物体的语义信息,这种语义信息值建立在较大的感受野基础上的。

2.1、局部池化

我们通常使用最大或平均池化,是在特征图上以窗口的形式进行滑动,操作为取窗口内的最大值或平均值作为结果,经操作后,特征图降采样,减少了过拟合。

在Pytorch中,最大池化使用nn.MaxPool2d,平均池化使用nn.AvgPool2d。在实际应用中,最大池化比其他池化更常用,在此介绍最大池化,具体格式为:

torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)

参数说明:

  • kernel_size – 池化窗口大小,一般是[height,width],如果两者相等,可以是一个数字,如kernel_size=3。
  • stride –窗口在每一个维度上滑动的步长,一般是[stride_h,stride_w],如果两者相等,可以是一个数字,如stride=1。
  • padding – 填充,和卷积类似。
  • dilation – 卷积对输入数据的空间间隔。
  • return_indices – 是否返回最大值对应的下标。
  • ceil_mode – 使用一些方块代替层结构。
  1. 输入输出形状
  • input: ( N , C , H i n , W i n ) (N,C,H_{in},W_{in}) (N,C,Hin,Win) N为数据的个数,C为通道数,H为高度,W为宽度。
  • output: ( N , C , H o u t , W o u t ) (N,C,H_{out},W_{out}) (N,C,Hout,Wout)

池化后的特征图的计算方式与卷积类似,设填充的圈数为 p p p,输入数据的大小为 n n n,池化窗口的大小为 f f f,步幅的大小为 s s s

⌊ n + 2 p − f s ⌋ + 1 \lfloor \frac{n+2p-f}{s} \rfloor +1 sn+2pf+1

实例代码:

import torch
from torch import nn

#池化窗口为正方形 size=3,stride=2
m1=nn.MaxPool2d(kernel_size=3,stride=2)
#池化窗口为非正方形
m2=nn.MaxPool2d(kernel_size=(3,2),stride=(2,1))
#输入(个数,通道数,高,宽)
input=torch.randn(20,16,50,32)
out1=m1(input)
out2=m2(input)

#高:(50-3)/2+1=24
#宽:(32-3)/2+1=15
print(out1.shape)
#高:(50-3)/2+1=24
#宽:(32-2)/1+1=31
print(out2.shape)
torch.Size([20, 16, 24, 15])
torch.Size([20, 16, 24, 31])

2.2、全局池化

与局部池化相对的是全局池化,全局池化也分最大和平均池化,所谓的全局是针对整个特征图。下面以全局平均池化为例,全局平均池化(Global Average Pooling, GAP),不以池化窗口的大小进行均值化,而是以特征图为单位进行均值化,即一个特征图输出一个值。

下面看看全局池化主要使用在什么地方:

如下图左边有四个特征图,先用一个全连接层将4个特征图展平为一个向量,然后再通过一个全连接层输出为4个分类节点。GAP可以将这两个操作合二为一。可以把GAP视为一个特殊的均值池化层,只不过池化窗口的大小和整个特征图一样大,就是求每张特征图所有像素的均值,输出一个数据值,这样4个特征图就会输出4个数据,这4个数据组成一个分类向量。

在这里插入图片描述

使用全局平均池化代替CNN中传统的全连接层。在使用卷积层的识别任务中,全局平均池化能够为一个特定的类别生成一个特征图。
GAP的有时在于:各个类别与特征图逐渐的联系更加直观,特征图被转化为分类概率也更加容易,因为在GAP中没有参数需要调,所以能够避免过拟合问题。GAP汇总了空间信息,因此对输入的空间转换鲁棒性更强。所以目前卷积神经网络中最后几个全连接层,大都用GAP替换。

虽然在Pytorch中没有对应名称的池化层,但是可以用自适应池化层(nn.AdaptiveAvgPool2d(1)或nn.AdaptiveMaxPool2d(1))来实现。自适应池化层的一般格式为:

torch.nn.AdaptiveMaxPool2d(output_size, return_indices=False)
#输出大小为(5,7)
m1=nn.AdaptiveMaxPool2d((5,7))
input1=torch.randn(1,1,8,9)
out1=m1(input1)

#输出大小为(1)
m2=nn.AdaptiveMaxPool2d(1)
input2=torch.randn(1,1,8,9)
out2=m2(input2)
out1,out2
(tensor([[[[ 0.7005,  0.7005,  0.8201,  0.8201, -0.3679, -0.5045,  0.0699],
           [ 0.7005,  1.5801,  1.6334,  1.6334,  0.5873,  1.3667,  1.3667],
           [ 0.7747,  1.0073,  1.4952,  1.4952,  0.5873,  1.3667,  1.3667],
           [ 0.7747,  1.0073,  1.2745,  1.2745,  0.9278,  0.9278,  2.8169],
           [ 1.0026,  0.3357,  1.3508,  1.3904,  1.3279,  1.3279, -0.2348]]]]),
 tensor([[[[2.1225]]]]))

Adaptive Pooling输出张量的大小都是给定的output_size。例如输入大小为(1,64,8,9),设定输出大小为(5,7),通过Adaptive层后,可以得到大小为(1,64,5,7)的张量。因此如果使用全局池化的时候,可以将输出的大小设置为1,这样就可以获得全局平均池化或者全局最大池化后的值。

更过关于池化层的函数可以参考网址:https://pytorch.org/docs/stable/nn.html#pooling-layers

3、Pytorch搭建CNN实现CIFAR-10多分类

在下面,使用Pytorch搭建一个简单的卷积神经网络,利用CIFAR-10数据集对网络进行训练。

CIFAR-10数据集由10个类别的60000个32x32彩色图像组成,每个类别包含6000个图像。有50000个训练图像10000个测试图像。

数据集分为五个训练批次和一个测试批次,每个批次有 10000 张图像。测试批次恰好包含来自每个类别的1000个随机选择的图像。训练批次以随机顺序包含剩余图像,但一些训练批次可能包含来自一个类的图像多于另一个。在它们之间,训练批次恰好包含来自每个类别的5000张图像。

以下是数据集中的类,以及每个类的10张随机图像:

在这里插入图片描述

3.1、数据加载与可视化

1、首先加载CIFAR-10数据集,在这里采用Pytorch提供的数据集加载工具torchvision,同时对数据进行预处理。创建load_data_CIFAR10函数用于加载数据集。

#导入相关的包
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from d2l import torch as d2l
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline


#用于加载CIFAR-10数据集
def load_data_CIFAR10(batch_size):
    #对数据进行预处理,在这里对图像做了转为tensor数据并进行归一化两个操作。
    trans = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
    ])
    #加载数据集,返回的类型为Dataset
    trainset = torchvision.datasets.CIFAR10(root='./cifar10',train=True,transform=trans,download=True)
    testset = torchvision.datasets.CIFAR10(root='./cifar10',train=False,transform=trans,download=True)
    #返回的数据类型为DataLoader,便于后面在训练的时候进行批量训练
    return (data.DataLoader(trainset,batch_size,shuffle=True,num_workers=0),
            data.DataLoader(testset,batch_size,shuffle=False,num_workers=0))

2、下面对数据集中的部分数据进行可视化,以便于更直观的查看数据集,加载数据的时候,批次大小设置为4。

batch_size=4
trainloader,testloader = load_data_CIFAR10(batch_size)

classes = ('plane','car','bird','cat','deer','dog','frog','horse','ship','truck')
def imshow(img):
    img = img / 2 + 0.5#反归一化
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg,(1,2,0)))
    plt.show()
#随机获取一个批次的训练数据
dataiter = iter(trainloader)
images,labels = dataiter.next()
#显显示图像
#torchvision.utils.make_grid(tensor, nrow=8, padding=2, normalize=False, range=None, scale_each=False)
#将一小batch图片变为一张图。nrow表示每行多少张图片的数量。
imshow(torchvision.utils.make_grid(images))
#打印标签
print(' '.join('%5s'% classes[labels[j]] for j in range(4)))
Files already downloaded and verified
Files already downloaded and verified

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XfxZoHdv-1649772209924)(https://img-rep01.oss-cn-beijing.aliyuncs.com/img/202204121403827.svg#pic_center)]

在这里插入图片描述

truck   dog horse   car

3.2、模型搭建

下面搭建卷积神经网络,该卷积神经网络有两个卷积层,每个卷积层后有一个最大池化层,最后使用两个线性层输出最终的分类结果,激活函数使用relu激活函数。

#构建网络模型 含有两个卷积层和连个池化层
class CNNNet(nn.Module):
    def __init__(self):
        super(CNNNet, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=3,out_channels=16,kernel_size=5,stride=1)
        self.pool1 = nn.MaxPool2d(kernel_size=2,stride=2)
        self.conv2 = nn.Conv2d(in_channels=16,out_channels=36,kernel_size=3,stride=1)
        self.pool2 = nn.MaxPool2d(kernel_size=2,stride=2)
        self.fc1 = nn.Linear(1296,128)
        self.fc2 = nn.Linear(128,10)
    def forward(self,x):
        x = self.pool1(F.relu(self.conv1(x)))
        x = self.pool2(F.relu(self.conv2(x)))
        x = x.reshape(-1,36*6*6)
        x=F.relu(self.fc1(x))
        x=self.fc2(x)
        return x

定义函数try_gpu,自定义训练过程中所使用的的设备,如果没有则使用CPU进行训练。

#如果存在,则返回gpu(i),否则返回cpu
def try_gpu(i=0):
    if torch.cuda.device_count()>=i+1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

实例化网络,并将其放入GPU中。

net = CNNNet()
net = net.to(try_gpu())
net
CNNNet(
  (conv1): Conv2d(3, 16, kernel_size=(5, 5), stride=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 36, kernel_size=(3, 3), stride=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=1296, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=10, bias=True)
)

3.3、模型训练v1(未整合版本)

数据集和网络定义好之后,下面就可以对网络进行训练了。

首先对网络中的权重进行初始化,在下面定义了好几种初始化的方法,它们均是从某一分布中抽取数值对模型的参数进行初始化,具体的解释可以查看Pytorch的官网。

#初始化参数
for m in net.modules():
    if isinstance(m,nn.Conv2d) or isinstance(m,nn.Linear):
        nn.init.xavier_uniform_(m.weight)

定义损失函数和优化器:

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(),lr=0.001,momentum=0.9)

下面对模型进行训练:

for epoch in range(10):
    running_loss = 0.0
    for i,data in enumerate(trainloader):
        inputs, labels = data
        inputs, labels = inputs.to(try_gpu()),labels.to(try_gpu())
        #权重参数梯度清零
        optimizer.zero_grad()
        
        #正向及反向传播
        outputs = net(inputs)
        loss = criterion(outputs,labels)
        loss.backward()
        optimizer.step()
        
        #显式损失值
        running_loss += loss.item()
        if i % 2000 == 1999:
            print('[%d,%5d] loss: %.3f' %(epoch+1,i+1,running_loss/2000))
            running_loss = 0.0
print('Finish Training')
[1, 2000] loss: 1.805
[1, 4000] loss: 1.528
[1, 6000] loss: 1.446
[1, 8000] loss: 1.377
[1,10000] loss: 1.306

[10, 2000] loss: 0.328
[10, 4000] loss: 0.374
[10, 6000] loss: 0.398
[10, 8000] loss: 0.430
[10,10000] loss: 0.446
[10,12000] loss: 0.460
Finish Training

模型评估。使用训练好的模型对测试集进行评估,可见结果只有 0.6556,并不是很理想。后续会介绍其他的卷积神经网络对该数据集进行训练,会获得更好的结果。

#测试模型
correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
        images,labels = data
        images,labels = images.to(try_gpu()),labels.to(try_gpu())
        outputs = net(images)
        _,predicted = torch.max(outputs,1)
        total += labels.size(0)
        correct +=(predicted == labels).sum().item()

    print(correct/total)
0.6556

对数据集的每个类别进行评估,得到每个类别的准确度。

#各种类别的准确率
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images,labels = data
        images, labels = images.to(try_gpu()), labels.to(try_gpu())
        outputs = net(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] +=1
for i in range(10):
    print('Accuracy of %5s : %2d %%' %(classes[i],100*class_correct[i]/class_total[i]))
Accuracy of plane : 75 %
Accuracy of   car : 69 %
Accuracy of  bird : 45 %
Accuracy of   cat : 42 %
Accuracy of  deer : 59 %
Accuracy of   dog : 49 %
Accuracy of  frog : 76 %
Accuracy of horse : 77 %
Accuracy of  ship : 77 %
Accuracy of truck : 82 %

模型的训练和评估过程就完成了

3.4、模型训练v2(整合版本)

上面代码将模型参数初始化、损失函数和优化器的定义、模型的训练与评估的过程给分开了。由于后面会对其他的卷积神经网络进行搭建和训练,为了把重心放到模型的搭建上,因此将模型参数初始化,模型训练与评估过程进行整合到一个函数中。

下面对模型的训练过程进行整合,以方便后面只把重点放到各个卷积神经网络的搭建上面。定义个三个函数,分别是accuracyevaluate_accuracy_gputrain_CNNaccuracy函数的作用是计算正确预测的数量;evaluate_accuracy_gpu是一个可以在GPU上评估模型的函数;train_CNN则是对模型的训练过程,并对训练过程进行可视化。在对模型训练的时候,只需要调用train_CNN函数,因此训练过程比较方便。

在训练过程中,对优化器和批量的大小进行了修改,得到了稍微好一点的结果。

#计算正确预测的数量
def accuracy(y_hat,y):
    #如果输出的格式与标签不一致,提取输出中每一行最大的值,这里标签不是one_hot
    if len(y_hat.shape)>1 and y_hat.shape[1]>1:
        y_hat = y_hat.argmax(axis=1)
    #类型转为一样,逐条对比是否一样
    cmp = y_hat.type(y.dtype) == y
    #返回预测整数的数量
    return float(cmp.type(y.dtype).sum())


#定义一个可以在GPU上评估模型的函数
def evaluate_accuracy_gpu(net,data_iter,device=None):
    if isinstance(net,torch.nn.Module):
        #设为评估模式
        net.eval()
        #device为空,查看模型的参数使用的什么设备,后续评估将使用该设备
        if not device:
            device = next(iter(net.parameters())).device
    #用于记录数据 正确预测的数量,总预测的数量
    metric = d2l.Accumulator(2)
    for X,y in data_iter:
        if isinstance(X,list):
            #BERT微调用的
            X = [x.to(device) for x in X]
        else:
            X = X.to(device)
        y = y.to(device)
        metric.add(accuracy(net(X),y),y.numel())
    return metric[0]/metric[1]

def train_CNN(net,train_iter,test_iter,num_epochs,lr,device):
    #初始化参数
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight)
    #会递归地将函数init_weight应用到父模块的每个子模块submodule,也包括model这个父模块自身
    net.apply(init_weights)
    #将模型放入设备中
    net.to(device)

    #优化器,与上面不用的是,这里使用了Adam优化器
    optimizer = torch.optim.Adam(net.parameters(),lr=lr,betas=(0.9,0.99))
    #损失函数
    loss = nn.CrossEntropyLoss()
    #绘图函数
    animator = d2l.Animator(xlabel='epoch',xlim=[1,num_epochs],legend=['train loss','train acc','test acc'])
    timer,num_batchs = d2l.Timer(),len(train_iter)
    for epoch in range(num_epochs):
        #训练损失和,训练准确率和,数量
        metric = d2l.Accumulator(3)
        net.train()
        for i,(X,y) in enumerate(train_iter):
            timer.start()
            #清空梯度
            optimizer.zero_grad()
            X,y = X.to(device),y.to(device)
            y_hat = net(X)
            l = loss(y_hat,y)
            l.backward()
            optimizer.step()
            with torch.no_grad():
                metric.add(l*X.shape[0],accuracy(y_hat,y),X.shape[0])
            timer.stop()
            train_l = metric[0]/metric[2]
            train_acc = metric[1]/metric[2]
            if (i + 1) % (num_batchs // 5) == 0 or i == num_batchs - 1:
                animator.add(epoch + (i + 1) / num_batchs, (train_l,train_acc,None))
        test_acc = evaluate_accuracy_gpu(net,test_iter)
        animator.add(epoch+1,(None,None,test_acc))
    print(f'loss {train_l:.3f},train acc {train_acc:.3f},test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} example/sec on {str(device)}')

下面使用这三个函数对模型进行训练,由于上面对实例化的模型已经训练好了,这里重新对数据进行加载,模型定义等工作。

lr,num_epochs,batch_size = 0.001,10,200
trainloader1,testloader1 = load_data_CIFAR10(batch_size)
net = CNNNet()
train_CNN(net,trainloader1,testloader1,num_epochs,lr,try_gpu())
loss 0.598,train acc 0.791,test acc 0.697
5993.9 example/sec on cuda:0

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值