卷积神经网络(convolutional neural network, CNN)是一类强大的为处理图像数据而设计的神经网络,是机器学习利用自然图像中一些已知结构的创造性方法。它除了能够高效采样从而获得精确模型,还能够高效地计算。

卷积

对卷积的理解:

  • 一个系统若输入不稳定(ff 函数),输出稳定(gg 函数),则可使用卷积来求系统存量

    f(τ)g(xτ)dτ\int_{-\infty}^{\infty} f(\tau) g(x-\tau) d \tau

  • 周围像素点对当前像素点会产生怎样的影响(如何影响,用gg 函数表示)

  • 一个像素点如何试探周围的像素点,如何筛选图像的特征

在卷积神经网络中,卷积的主要作用即是抽象化图像的内容、压缩数据量、匹配特定的纹理。

互相关运算

在深度学习中,「卷积」实际上表达的运算是互相关运算(cross-correlation)

严格意义下的「卷积」运算中,需要进行卷积核翻转,也就说输入图像索引在增大,卷积核的索引却在减小

暂时忽略通道(第三维)下,二维互相关运算举例如下:

在图像处理中,卷积常作为特征提取的有效方法。一幅图像经卷积操作后得到的结果成为特征映射(Feature Map)

卷积层

卷积层中,输入张量和核张量通过互相关运算,并在添加标量偏置之后产生输出。

卷积层的作用是提取一个局部区域的特征,不同的卷积核相当于不同的特征提取器

对于输入矩阵X:nh×nw\mathbf{X}:n_h \times n_w、卷积核矩阵W:kh×kw\mathbf{W}:k_h \times k_w ,偏差标量bb

则输出矩阵:Y:(nhkh+1)×(nwkw+1)\mathbf{Y}:(n_h-k_h+1) \times (n_w-k_w+1) ,且:

Y=XW+b\mathbf{Y} = \mathbf{X} \star \mathbf{W} + b

卷积层的两个重要性质

  • **局部连接:**第ll层的卷积层中每一个神经元都只和第l1l - 1层中某个局部窗口内的神经元相连,构成一个局部连接网络。
  • **权重共享:**作为参数的卷积核W(l)\mathbf{W}^{(l)} 对于第ll 层的所有神经元都是相同的

使用 PyTorch 提供的 Conv2d() 方法,学习由X\mathbf{X} 生成Y\mathbf{Y} 的卷积核(即已知输入X\mathbf{X} 以及对应的输出Y\mathbf{Y},核是未知的)

Pytorch中的卷积默认是带有偏差标量的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias = False)
# 构造二维卷积层,其输入通道、输出通道大小为1,即图片为黑白,卷积核形状为(1,2)

# 二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度)
# 其中通道数为1,批量数为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))

for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad

填充与步幅

填充(padding) 与 步幅(stride) 是卷积层的超参数

其中,填充能够尽可能地控制输出形状的减少量;步幅,能够解决“大量计算才能得到较小输出”的问题,成倍地减少输出形状。

对于输入图像尺寸为nh×nwn_h \times n_w,卷积核(Filter)尺寸为kh×kwk_h \times k_w,填充尺寸为ph×pwp_h \times p_w,步幅尺寸为sh×sws_h \times s_w,对应输出的 Feature Map 尺寸为:

nh+2phkhsh+1×nw+2pwkwsw+1\lfloor \frac{n_h + 2p_h - k_h}{s_h} + 1 \rfloor \times \lfloor \frac{n_w + 2p_w - k_w}{s_w} + 1 \rfloor

填充

应用多层卷积时,常丢失边缘像素。

为解决该问题,可以使用填充(padding):输入图像的边界填充元素(通常为0),如下举例:

代码实现(使用 PyTorch 的 nn.Conv2d()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
from torch import nn

def comp_conv2d(conv2d, X):
# 在现有的尺寸维度之前,加入通道、批量大小
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 由于输出维度为4维,只需要获取后面2维即可
return Y.reshape(Y.shape[2:])

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
# 对于3*3的卷积核,填充为1即可保证不改变尺寸
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
# torch.Size([8, 8])

当然,还能够进行高度不等于宽度的填充:

1
2
3
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1)) 
comp_conv2d(conv2d, X).shape
# torch.Size([8, 8])

步幅

有时候为了得到较小的输出,需要经过大量层的计算。如给定输入大小为224×224224\times224,在使用5×55\times5 卷积核的情况下,需要 44 层才能将输出降低至4×44\times 4

为了高效计算或缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素,此时将每次滑动元素的数量称为 步幅(stride),如下举例:高度为 3,宽度为 2的步幅

通常情况下,若(经过填充、普通卷积后的)高度与宽度都能够被步幅整除,则输出形状为:

(nh/sh)×(nw/sw)(n_h / s_h) \times (n_w / s_w)

代码实现:

1
2
3
4
5
X = torch.rand(size=(8, 8))
conv2d = nn.Conv2d(1, 1, kernel_size=3, stride=2)
# 普通卷积后得到的尺寸为(8-3+1)*(8-3+1)
comp_conv2d(conv2d, X).shape
# torch.Size([3, 3])
1
2
3
4
5
X = torch.rand(size=(8, 8))
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
# 填充高宽恰好能够被stride整除,故效果相当于缩小一半
comp_conv2d(conv2d, X).shape
# torch.Size([4, 4])

感受野

感受野(Receptive Field)的定义为:卷积神经网络每一层输出的特征图(feature map)上的像素点映射回输入图像上的区域大小。

也就说,特征图上一点,相对于原图的大小,也是卷积神经网络特征所能看到输入图像的区域。

其计算方式是从最后一层 feature map 开始,从下往上计算,先计算最深层在前一层的感受野,再逐层递推直至第一层

计算公式:$RF_i = (RF_{i + 1} - 1) \times s_i +K_{size_i} $

特别地,最后一层的 feature map 感受野大小RFRF = 卷积核大小KsizeK_{size}

应用:

  • 两个3×33\times 3 的卷积层的感受野与一个5×55 \times 5 的卷积层相同,但参数量更小,在更小的计算量下,增加模型的深度,从而提高其非线性的表达能力

  • 三个3×33\times 3 的卷积层的表达能力与一个7×77 \times 7 卷积层相同

多输入多输出通道

多输入通道

当输入包含多个通道时(如 RGB 输入图像具有3×h×w3\times h \times w 的形状),需要构造一个与输入数据具有相同输入通道数目的卷积核,以便与输入数据进行互相关运算。

每一个通道都应对应一个大小相同但值不同的卷积核,从而使得每个通道能够学习到不同的模式

举例如下:

故,对于输入X:ci×nh×nw\mathbf{X}:c_i\times n_h \times n_w,核W:ci×kh×kw\mathbf{W:c_i\times k_h \times k_w}

则输出Y:mh×mw\mathbf{Y}:m_h \times m_w

即先对每个通道输入的二维张量、卷积核的二维张量进行互相关运算,再对通道求和,得到二维张量

代码实现:

1
2
3
def corr2d_multi_in(X, K): 						
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
# 先遍历每一个输入通道,再将其加在一起

多输出通道

在最流行的神经网络架构中,随着神经网络层数的加深,常常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。

对于输入X:ci×nh×nw\mathbf{X}:c_i \times n_h \times n_w,核W:co×ci×kh×kw\mathbf{W}:c_o\times c_i \times k_h \times k_w

即有多个三维卷积核,每个核生成一个输出通道

则输出Y:co×mh×mw\mathbf{Y}:c_o\times m_h \times m_w

代码实现:

1
2
3
4
5
6
def corr2d_multi_in_out(X, K):
return torch.stack(
# 遍历每一个输出通道,即每一个小k是三维的,此时问题转换为多输入通道
[corr2d_multi_in(X, k) for k in K],
0 # 对这个携带二维张量的列表,按第零维度进行拼接,得到三维张量
)

每个输出通道能够识别特定模式,而输入通道内核识别并组合输入中的模式

⭐️总结:

  • 每个输入通道都有独立的二维卷积核,所有输入通道的互相关运算结果相加,得到一个输出通道结果
  • 每个输出通道都有独立的三维卷积核

1*1 卷积层

Lin, Min, Qiang Chen, and Shuicheng Yan. “Network in network.” arXiv preprint arXiv:1312.4400 (2013).

对于kh=kw=1k_h = k_w = 1 的卷积核,它不识别空间模式,而是融合通道。

相当于具有输入nhnw×cin_h n_w \times c_i 、权重为co×cic_o \times c_i 的全连接层

每个三维卷积核,相当于与输入进行一个加权和

⭐️作用:

  • 升维或降维:Feature Map 大小不变,但深度发生变化(取决于三维卷积核的数量)
  • 跨通道信息交融
  • 减少参数量
  • 增加模型深度,提高非线性表达能力

**应用举例:**GoogLeNet

每一个Inception块中由四条并行路径组成:1×13×35×51\times 1、3\times 3、5\times 5的卷积层,以及3×33\times 3的汇聚层,从不同的空间中提取信息。

  • 每一分支,若不使用1×11\times 1 卷积层,乘法运算量(FeatureMap元素个数 × 感受野)将会非常庞大
  • 使用1×11\times 1 卷积层对输入层进行降维(大小没变、深度减少),从而大大减少参数数量

池化(汇聚)

类似于卷积层,池化层除了有一个固定形状的窗口(即池化窗口),能够根据步幅大小在输入的所有区域上滑动。当然,它也有填充操作

池化窗口从输入张量的左上角开始,从左至右、从上至下地在输入张量内滑动。在池化窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值,具体取决于使用了最大池化层(maximum pooling),还是平均池化层(average pooling)。

池化(pooling)的双重目的:

  • 降低卷积层对位置的敏感性

  • 降低对空间降采样(downsampling)表示的敏感性

    降采样,可以理解为“从样本里再选样本”的过程。

池化窗口形状为p×qp\times q 的池化层,其池化操作称为p×qp \times q 池化。

最大池化操作举例如下:

池化层没有可学习的参数,并且在每个输入通道中应用池化层(即:汇聚层在每个输入通道上单独运算,而无需在通道上对输入进行汇总),由此获得相应的输出通道

因此,池化层的输出通道数量,等于输入通道数量

GAP

Lin, Min, Qiang Chen, and Shuicheng Yan. “Network in network.” arXiv preprint arXiv:1312.4400 (2013).

GAP(Global Average Pooling):将每一张 Feature Map 计算所有像素点的均值,输出对应的特征点,将这些特征点组成最后的特征向量

简单来说,就是每一个通道选出一个代表出来

目的:替代卷积神经网络中传统的全连接层,减少参数量

优点:

  • 通过强制使特征图和类别之间相对应,对于卷积结构更来说这个转换更自然。
  • 没有参数用于优化,避免了过拟合

自适应池化

自适应(Adaptive):只需要给定输入数据和输出数据的大小,自适应算法就能够自动地帮助计算核的大小和每次移动的步长。

  • torch.nn.AdaptiveAvgPool2d(output_size):提供二维的自适应平均池化操作,传入的是输出尺寸

代码实现

从零实现池化层的正向传播:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from torch import nn
from d2l import torch as d2l

def pool2d(X, pool_size, mode='max'):
# 假设X为单通道
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.0, 1.0, 2.0],
[3.0, 4.0, 5.0],
[6.0, 7.0, 8.0]])
pool2d(X, (2, 2)), pool2d(X, (2, 2), 'avg')
# 验证最大池化层以及平均池化层
1
2
3
4
(tensor([[4., 5.],
[7., 8.]]),
tensor([[2., 3.],
[5., 6.]]))

使用 PyTorch 实现池化层:

1
2
3
4
5
6
7
X = torch.arange(16, dtype = torch.float32).reshape((1, 1, 4, 4))

# MaxPool2d的默认步幅与池化窗口大小相等,意味着两次遍历的窗口是不重叠的
pool2d = nn.MaxPool2d(3) # 3*3窗口
pool2d_b = nn.MaxPool2d(3, padding=1, stride=2)

X, pool2d(X), pool2d_b(X)
1
2
3
4
5
6
7
(tensor([[[[ 0.,  1.,  2.,  3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]]]]),
tensor([[[[10.]]]]),
tensor([[[[ 5., 7.],
[13., 15.]]]]))

对于多通道输入,池化层分别对每个输入通道进行运算:

1
2
3
X = torch.cat((X, X+1), 1) # 拼接为双通道,第一个通道为X,第二个通道为X+1(即X每个元素均加1)
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
X, pool2d(X)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(tensor([[[[ 0.,  1.,  2.,  3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]],

[[ 1., 2., 3., 4.],
[ 5., 6., 7., 8.],
[ 9., 10., 11., 12.],
[13., 14., 15., 16.]]]]),
tensor([[[[ 5., 7.],
[13., 15.]],

[[ 6., 8.],
[14., 16.]]]]))

卷积网络的整体结构

典型的卷积网络是由卷积层、池化层、全连接层交叉堆叠而成

拓展资料

CNN可视化:https://poloclub.github.io/cnn-explainer/