卷积神经网络(convolutional neural network)是含有卷积层(convolutional layer)的神经网络。本章中介绍的卷积神经⽹络均使⽤最常⻅的⼆维卷积层。它有⾼和宽两个空间维度,常⽤来处理图像数据。本节中,我们将介绍简单形式的⼆维卷积层的⼯作原理。
1、二维互相关运算
虽然卷积层得名于卷积(convolution)运算,但我们通常在卷积层中使⽤更加直观的互相关(cross- correlation)运算。在⼆维卷积层中,⼀个⼆维输入数组和⼀个二维核(kernel)数组通过互相关运算输出⼀个⼆维数组。我们⽤⼀个具体例子来解释二维互相关运算的含义。如下图所示,输入是⼀个⾼和宽均为3的⼆维数组。我们将该数组的形状记为3 X 3或(3,3)。核数组的高和宽分别为2。该数组在卷积计算中⼜称卷积核或过滤器(filter)。卷积核窗口(又称卷积窗⼝)的形状取决于卷积核的⾼和宽,即 2 X 2。图中的阴影部分为第一个输出元素及其计算所使用的输⼊和核数组元素:0 X 0 + 1 X 1 + 3 X 2 + 4 X 3 = 19
在⼆维互相关运算中,卷积窗口从输⼊数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。当卷积窗口滑动到某⼀位置时,窗⼝中的输⼊子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。上图中输出的4个元素由⼆维互相关运算得出:
下面我们将上述过程实现在 corr2d 函数里。它接受输入数组 X 与核数组 K ,并输出数组 Y
import torch
from torch import nn
def corr2d(X, K): # 本函数已保存在d2lzh_pytorch包中方便以后使用
h, w = K.shape
X, K = X.float(), K.float()
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
我们可以构造图5.1中的输⼊数组 X 、核数组 K 来验证二维互相关运算的输出。
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
K = torch.tensor([[0, 1], [2, 3]])
corr2d(X, K)
tensor([[19., 25.],
[37., 43.]])
2、二维卷积层
⼆维卷积层将输⼊和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。
下面基于 corr2d 函数来实现⼀个⾃定义的二维卷积层。在构造函数 \_\_init\_\_ ⾥我们声明 weight 和 bias 这两个模型参数。前向计算函数 forward 则是直接调用 corr2d 函数再加上偏差。
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super(Conv2D, self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
3、互相关运算和卷积运算的区别与联系
实际上,卷积运算与互相关运算类似。为了得到卷积运算的输出,我们只需将核数组左右翻转并上下翻转,再与输入数组做互相关运算。可见,卷积运算和互相关运算虽然类似,但如果它们使⽤相同的核数组,对于同⼀个输⼊入,输出往往并不相同。
那么,你也许会好奇卷积层为何能使用互相关运算替代卷积运算。其实,在深度学习中核数组都是学出来的:卷积层无论使⽤互相关运算或卷积运算都不影响模型预测时的输出。为了解释这一点,假设卷积层使⽤互相关运算学出图1、中的核数组。设其他条件不不变,使⽤卷积运算学出的核数组即图1、中的核数组按上下、左右翻转。也就是说,图1、中的输⼊与学出的已翻转的核数组再做卷积运算时,依然得到图1、中的输出。
4、卷积神经网络常用的概念
在卷积神经网络中有一些非常重要的概念,比如特征图、感受野、填充、步幅、池化等等。这些概念比较好理解,读者可以自行查阅资料,在这里我主要介绍一下填充、步幅、池化在Pytorch的API怎么使用。
4.1、填充(padding)
padding是指在输入中填充0的意思,在Pytorch中填充主要使通过nn.Conv2d中的padding参数来完成的,可以通过padding的赋值来判断高和宽分别填充的数量,比如padding=1说明在高和宽两侧分别填充1行或列,也就是总共增加了两行和两列,也可以分别对高和宽填充不同数量的0,请参考下面的例子
from torch import nn
# 注意这里是两侧分别填充1行或列,所以在两侧一共填充2行或列
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)
# 使用高为5、宽为3的卷积核。在高和宽两侧的填充数分别为2和1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
4.2、步幅(strade)
strade是指卷积核在卷积的时候,左右或者上下移动的步伐,在Pytorch中步幅主要使通过nn.Conv2d中的stride参数来完成的,比如stride=2,说明每次卷积后都会向右或者向下移动2个元素。
conv2d = nn.Conv2d(1, 1, kernel\_size=3, padding=1, stride=2)
4.3、池化(pooling)
pooling是一种降维思想,为了缓解卷积层对位置的过度敏感性,最常用的就是Maxpooling(最大池化)和Avgpooling(平均池化),最大池化就是取固定窗口大小中最大元素来替代整个窗口的值,平均池化道理类似,就是取固定窗口中所有元素的平均值来代替整个窗口的值。
我们下面来实现最大池化
def pool2d(X, pool_size, mode='max'):
X = X.float()
p_h, p_w = pool_size
Y = torch.zeros(X.shape[0] - p_h + 1, X.shape[1] - p_w + 1)
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
pool2d(X, (2, 2))
tensor([[4., 5.],
[7., 8.]])
在Pytorch中池化有 nn.MaxPool2d((2, 4), padding=(1, 2), stride=(2, 3))来实现
5、多输入通道
当输⼊数据含多个通道时,我们需要构造⼀个输⼊通道数与输⼊数据的通道数相同的卷积核,从而能够与含多通道的输入数据做互相关运算。我们可以举个栗子:
让我们来计算一下阴影部分56是怎么计算的:
(1 X 1 + 2 X 2 + 4 X 3 + 5 X 4) + (0 X 0 + 1 X 1 +3 X 2 + 4 X 3) = 56
下面使用Pytorch函数来实现
def corr2d_multi_in(X, K):
# 沿着X和K的第0维(通道维)分别计算再相加
res = d2l.corr2d(X[0, :, :], K[0, :, :])
for i in range(1, X.shape[0]):
res += d2l.corr2d(X[i, :, :], K[i, :, :])
return res
X = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]])
K = torch.tensor([[[0, 1], [2, 3]], [[1, 2], [3, 4]]])
corr2d_multi_in(X, K)
tensor([[ 56., 72.],
[104., 120.]])
6、多输出通道
上节多输入通道采用了与输入通道相同多的卷积核进行卷积的结果是输出了单通道二维数组,如果要求输出也是多通道,如何计算呢?很显然,我们可以设计一个思维的卷积核进行分别卷积,然后把卷积的结果stack起来就行了。
下面我们来实现一个3通道输出的案例
def corr2d_multi_in_out(X, K):
# 对K的第0维遍历,每次同输入X做互相关计算。所有结果使用stack函数合并在一起
return torch.stack([corr2d_multi_in(X, k) for k in K])
我们将核数组 K 同 K+1 ( K 中每个元素加⼀)和 K+2 连结在⼀起来构造一个输出通道数为3的卷积核
K = torch.stack([K, K + 1, K + 2])
K.shape
torch.Size([3, 2, 2, 2])
下面我们对输入数组 X 与核数组 K 做互相关运算。此时的输出含有3个通道。其中第一个通道的结果与5.1的多输入通道、单输出通道核的计算结果一致
corr2d\_multi\_in\_out(X, K)
tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
7、1 X 1 卷积层
1 X 1 卷积使输⼊和输出具有相同的高和宽。输出中的每个元素来⾃输入中在⾼和宽上相同位置的元素在不同通道之间的按权重累加。假设我们将通道维当作特征维,将⾼和宽维度上的元素当成数据样本,那么卷积层的作用与全连接 层等价。
如下图所示:
下面我们使用全连接层中的矩阵乘法来实现1 X 1卷积。这⾥需要在矩阵乘法运算前后对数据形状做⼀些调整
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.view(c_i, h * w)
K = K.view(c_o, c_i)
Y = torch.mm(K, X) # 全连接层的矩阵乘法
return Y.view(c_o, h, w)
经验证,做1 X 1卷积时,以上函数与之前实现的互相关运算函数 corr2d_multi_in_out 等价。
X = torch.rand(3, 3, 3)
K = torch.rand(2, 3, 1, 1)
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
(Y1 - Y2).norm().item() < 1e-6
True
至此,CNN的一些基本概念就介绍完了,下面主要会介绍一些CNN的经典网络,比如LENET、ALEXNET、VGG、GOOGLENET、RESNET、DENSENET等等。