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