Pytorch-卷积神经网络CNN

646 阅读9分钟

CNN基础

卷积

来看一个最简单的例子:边界检测(edge detection),假设有这样的一张图片,大小8×8:

图片中的数字代表该位置的像素值像素值越大,颜色越亮,所以为了示意,把右边小像素的地方画成深色。图的中间两个颜色的分界线就是要检测的边界。可以设计这样的一个 滤波器(filter,也称为kernel),这样的一个**kernel**就是卷积核。

然后,用这个filter,往图片上覆盖一块跟filter一样大的区域之后,对应元素相乘,然后求和。计算一个区域之后,就向其他区域挪动,接着计算,直到把原图片的每一个角落都覆盖到了为止。这个过程就是 “卷积” 。(不用管卷积在数学上到底是指什么运算,只用知道在CNN中是怎么计算的。)这里的“挪动”,就涉及到一个步长了,假如步长是1,那么覆盖了一个地方之后,就挪一格,容易知道,总共可以覆盖6×6个不同的区域。那么,将这6×6个区域的卷积结果,拼成一个矩阵:

这个图片,中间颜色浅,两边颜色深,这说明原图片中间的边界在这里被反映出来了!从上面这个例子中发现,可以通过设计特定的filter,让它去跟图片做卷积,就可以识别出图片中的某些特征,比如边界。上面的例子是检测竖直边界,也可以设计出检测水平边界的,只用把刚刚的filter旋转90°即可。对于其他的特征,理论上只要经过精细的设计,总是可以设计出合适的filter的。

CNN(convolutional neural network),主要就是通过一个个的filter,不断地提取特征,从局部的特征到总体的特征,从而进行图像识别等等功能。 每个filter中的各个数字就是参数,可以通过大量的数据来让机器自己去“学习”这些参数。这就是CNN的原理。

padding 填白

从上面的引子中可以知道,原图像在经过filter卷积之后,变小了,从(8,8)变成了(6,6)。假设再卷一次,那大小就变成了(4,4)了。这样主要有两个问题:

  • 每次卷积,图像都缩小,这样卷不了几次就没了;
  • 相比于图片中间的点,图片边缘的点在卷积中被计算的次数很少。这样的话,边缘的信息就易于丢失。

为了解决这个问题,可以采用**padding**的方法。每次卷积前,先给图片周围都补一圈空白,让卷积之后图片跟原来一样大,同时,原来的边缘也被计算了更多次。

比如把(8,8)的图片给补成(10,10),那么经过(3,3)的filter之后就是(8,8),没有变。把上面这种“让卷积之后的大小不变”的**padding**方式,称为“Same”方式,把不经过任何填白的,称为“Valid” 方式。这个是在使用一些框架的时候需要设置的超参数。

stride 步长

每次卷积后移动的位置就是步长

pooling 池化

**pooling****是为了提取一定区域的主要特征,并减少参数数量,防止模型过拟合。**如下MaxPooling,使用区域最大值,采用了一个2×2的窗口,并取stride=2:

除了MaxPooling还有AveragePooling,顾名思义就是取那个区域的平均值。

多通道(channels)图片卷积

彩色图像,一般都是RGB三个通道(channel)的,因此输入数据的维度一般有三个: (长,宽,通道) 。如一个28×28的RGB图片,维度就是(28,28,3)。前面的引子中,输入图片是2维的(8,8),filter是(3,3),输出也是2维的(6,6)。

如果输入图片是三维的,如是(8,8,3),这个时候filter的维度就要变成(3,3,3)了,它的最后一维要跟输入的channel维度一致。这个时候的卷积,是三个channel的所有元素对应相乘后求和,也就是之前是9个乘积的和,现在是27个乘积的和。因此,输出的维度并不会变化。还是(6,6)。但是一般情况下,会使用多个filters同时卷积,如果同时使用4个filter的话,那么输出的维度则会变为(6,6,4)

  • 输入图片就是X,shape=(8,8,3);
  • 4个filters其实就是第一层神经网络的参数W1,shape=(3,3,3,4),这个4是指有4个filters;
  • 输出就是Z1,shape=(6,6,4);
  • 后面还应该有一个激活函数,如relu,经过激活后Z1变为A1,shape=(6,6,4);

CNN整体结构

CNN包含了3种层(layer):

Convolutional layer(卷积层—CONV)

由滤波器filters和激活函数构成。 一般要设置的超参数包括filters的数量、大小、步长,以及padding是“valid”还是“same”。还包括选择什么激活函数。

Pooling layer (池化层—POOL)

这里面没有参数需要学习,要么是Maxpooling,要么是Averagepooling。需要指定的超参数,包括是Max还是average,窗口大小以及步长。

通常使用的比较多的是Maxpooling,而且一般取大小为(2,2),步长为2的filter,这样,经过pooling之后,输入的长宽都会缩小2倍,channels不变。

Fully Connected layer(全连接层—FC)

全连接层,这里要指定的超参数无非就是神经元的数量,以及激活函数。

上面这个CNN的结构可以用:X→CONV(relu)→MAXPOOL→CONV(relu)→FC(relu)→FC(softmax)→Y来表示。这里需要说明的是,在经过数次卷积和池化之后,最后会先将多维的数据进行“扁平化”, 也就是把 (height,width,channel) 的数据压缩成长度为 height × width × channel 的一维数组,然后再与 FC层连接,这之后就跟普通的神经网络无异了。可以从图中看到,随着网络的深入,图像越来越小,但是channels却越来越大了。在图中的表示就是长方体面对我们的面积越来越小,但是长度却越来越长了。

pytorch中的CNN

卷积模块Conv

卷积模块:Conv1d、Conv2d,Conv3d

PyTorch中卷积模块主要包括3个,即分别为1维卷积Conv1d、2维卷积Conv2d和3维卷积Conv3d,其中Conv2d即是最常用于图像数据的二维卷积,也是最早出现的模块;Conv1d则可用于时序数据中的卷积。

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)
  • in_channels:输入层的图像数据通道数
  • out_channels:输出层的图像数据通道数
  • kernel_size:卷积核尺寸,既可以是标量,代表一个正方形的卷积核;也可以是一个二元组,分别代表长和宽
  • stride:卷积核滑动的步幅,默认情况下为1,即逐像素点移动,若设置大于1的数值,则可以实现跨步移动的效果
  • padding:边缘填充的层数,默认为0,表示对原始图片数据不做填充。如果取值大于0,例如padding1,则在原始图片数据的外圈添加一圈0值,注意这里是添加一圈,填充后的图片尺寸为原尺寸长和宽均+2。

Conv2d可以看做是一个特殊的神经网络层,所以其本质上是将一个输入的tensor变换为另一个tensor,其中输入和输出tensor的尺寸即含义分别如下:

  • input:batch × in_channels × height × width
  • output:  batch × output_channels × height × width

即输入和输出tensor主要是图像通道数上的改变,图像的高和宽的大小则要取决于kernel、padding、stride和dilation四个参数的综合作用

池化模块****MaxPool

池化模块:MaxPool1d、MaxPool2d,MaxPool3d

池化模块在PyTorch中主要内置了最大池化和平均池化,每种池化又可细分为一维、二维和三维池化层。

torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
  • kernel_size(int or tuple) - max pooling的窗口大小,
  • stride(int or tuple, optional) - max pooling的窗口移动的步长。默认值是kernel_size
  • padding(int or tuple, optional) - 输入的每一条边补充0的层数
  • dilation(int or tuple, optional) – 一个控制窗口中元素步幅的参数
  • return_indices - 如果等于True,会返回输出最大值的序号,对于上采样操作会有帮助
  • ceil_mode - 如果等于True,计算输出信号大小的时候,会使用向上取整,代替默认的向下取整的操作

CNN图片分类简单实例

选用LeNet5对手写数字分类任务加以尝试,首先是mnist数据集的准备,可直接使用torchvision包在线下载:

from torchvision import datasets
from torch.utils.data import DataLoader, TensorDataset

train = datasets.MNIST('data/', download=True, train=True)
test = datasets.MNIST('data/', download=True, train=False)

X_train = train.data.unsqueeze(1)/255.0
y_train = train.targets
trainloader = DataLoader(TensorDataset(X_train, y_train), batch_size=256, shuffle=True)

X_test = test.data.unsqueeze(1)/255.0
y_test = test.targets

然后是LeNet5的网络模型(torchvision中内置了部分经典模型,但LeNet5由于比较简单,不在其中)

import torch
from torch import nn

class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, 5, padding=2) # 二维矩阵,通道数1,卷积输出通道数6,最后卷积成5*5矩阵图片
        self.pool1 = nn.MaxPool2d((2, 2))
        self.conv2 = nn.Conv2d(6, 16, 5) # 通道数6→16,卷积成5*5矩阵图片
        self.pool2 = nn.MaxPool2d((2, 2))
        self.fc1 = nn.Linear(16*5*5, 120)# 通道数16*图片大小5*5
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x)) # 前向传播过程,激活函数
        x = self.pool1(x)
        x = F.relu(self.conv2(x))
        x = self.pool2(x)
        x = x.view(len(x), -1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

最后是模型的训练过程:

model = LeNet5()
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss()

for epoch in trange(10):
    for X, y in trainloader:
        pred = model(X)
        loss = criterion(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    with torch.no_grad():
        y_pred = model(X_train)
        acc_train = (y_pred.argmax(dim=1) == y_train).float().mean().item()
        y_pred = model(X_test)
        acc_test = (y_pred.argmax(dim=1) == y_test).float().mean().item()
        print(epoch, acc_train, acc_test)
### 训练结果 ###
 10%|████████▎                                                                          | 1/10 [00:28<04:12, 28.05s/it]
0 0.9379666447639465 0.9406999945640564
 20%|████████████████▌                                                                  | 2/10 [00:56<03:48, 28.54s/it]
1 0.9663333296775818 0.9685999751091003
 30%|████████████████████████▉                                                          | 3/10 [01:25<03:21, 28.76s/it]
2 0.975350022315979 0.9771000146865845
 40%|█████████████████████████████████▏                                                 | 4/10 [01:54<02:52, 28.78s/it]
3 0.9786166548728943 0.9787999987602234
 50%|█████████████████████████████████████████▌                                         | 5/10 [02:22<02:22, 28.43s/it]
4 0.9850000143051147 0.9853000044822693
 60%|█████████████████████████████████████████████████▊                                 | 6/10 [02:51<01:53, 28.49s/it]
5 0.9855666756629944 0.9843999743461609
 70%|██████████████████████████████████████████████████████████                         | 7/10 [03:20<01:26, 28.78s/it]
6 0.9882833361625671 0.9873999953269958
 80%|██████████████████████████████████████████████████████████████████▍                | 8/10 [03:51<00:58, 29.37s/it]
7 0.9877333045005798 0.9872000217437744
 90%|██████████████████████████████████████████████████████████████████████████▋        | 9/10 [04:21<00:29, 29.54s/it]
8 0.9905833601951599 0.9896000027656555
100%|██████████████████████████████████████████████████████████████████████████████████| 10/10 [04:49<00:00, 28.93s/it]
9 0.9918666481971741 0.9886000156402588