多层感知机十分适合处理表格数据,其中行对应样本,列对应特征。然而对于高维感知数据,这种缺少结构的网络可能会变得不实用。在多层感知机图像分类例子里,通过将图像数据展平成一维向量,再将数据送入一个全连接的多层感知机中,忽略了每个图像的空间结构信息。全连接网络参数量极大,卷积神经网络需要的参数少于全连接架构的网络,而且卷积也很容易用GPU并行计算。因此卷积神经网络除了能够高效地采样从而获得精确的模型, 还能够高效地计算。
卷积神经网络不变性
假设想从一张图片中找到某个物体
在二维互相关运算中,卷积窗口从输入张量的左上⻆开始,从左到右、从上到下滑动。当卷积窗口滑动到新 一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值。
注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1,而卷积核只与图像中每个大小完全适合的位置进行互相关运算。输出大小即:
( n h − k h + 1 ) × ( n w − k w + 1 ) (n_h-k_h+1)\times(n_w-k_w+1) (nh−kh+1)×(nw−kw+1)
卷积层对输入和卷积核权重进行互相关运算,添加标量偏置之后输出。
# 自定义计算二维互相关运算
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
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__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
通过“输入-输出”对学习由X
生成Y
的卷积核。先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,比较Y
与卷积层输出的平方误差,然后计算梯度来更新卷积核。
# 构造一个二维卷积层,它具有1个输出通道和形状为(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}')
conv2d.weight.data.reshape((1, 2))
卷积层可以视为一个输入映射到下一层的空间维度的转换器,因此被称为特征映射(feature map)。
在CNN中,对于某一层的任意元素 x x x,其感受野(receptive field)是指在前向传播期间可能影响 x x x计算的所有元素(来自所有先前层)。注意,感受野的覆盖率可能大于某层输入的实际区域大小。
当一个特征图中的任意元素需要检测更广区域的输入特征时,可以构建一个更深的网络。
多层卷积操作之后,输出会远小于输入大小,原始图像的边界会丢失了许多有用信息。解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充0)。
当添加 p h p_h ph行进行填充(大约一半在顶部,一半在底部)和 p w p_w pw列进行填充(左侧大约一半,右侧一半),则输出形状将为:
( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1) (nh−kh+ph+1)×(nw−kw+pw+1)输出的高度和宽度将分别增加 p h p_h ph和 p w p_w pw。
通常,设置 p h = k h − 1 \color{Red}p_h=k_h-1 ph=kh−1和 p w = k w − 1 \color{Red}p_w=k_w-1 pw=kw−1,使输入和输出具有相同的高度和宽度。
当 k h k_h kh是奇数,在高度的两侧填充 p h / 2 p_h/2 ph/2行;当 k h k_h kh是偶数,则一种可能性是在输入顶部填充 ⌈ p h / 2 ⌉ \lceil p_h/2\rceil ⌈ph/2⌉(向上取整)行,在底部填充 ⌊ p h / 2 ⌋ \lfloor p_h/2\rfloor ⌊ph/2⌋(向下取整)行。填充宽度的两侧同理。
卷积神经网络中卷积核的高度和宽度通常为奇数
,选择奇数的好处是,保持空间维度的同时,可以填充相同数量的行和列。
import torch
from torch import nn
X = torch.rand(size=(8, 8))
# 这里的(1,1)表示批量大小和通道数都是1
X = X.reshape((1, 1) + X.shape)
# 创建高度和宽度为3的二维卷积层,并在所有侧边填充1个像素
# padding=1表示左右上下各填充1行或1列,共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
Y = conv2d(X)
# 省略前两个维度:批量大小和通道
Y.reshape(Y.shape[2:]).shape
当垂直步幅为 s h s_h sh、水平步幅为 s w s_w sw时,输出形状为
⌊ n h − k h + p h s h + 1 ⌋ × ⌊ n w − k w + p w s w + 1 ⌋ \lfloor\frac{n_h-k_h+p_h}{s_h}+1\rfloor \times \lfloor\frac{n_w-k_w+p_w}{s_w}+1\rfloor ⌊shnh−kh+ph+1⌋×⌊swnw−kw+pw+1⌋如果设置 p h = k h − 1 p_h=k_h-1 ph=kh−1和 p w = k w − 1 p_w=k_w-1 pw=kw−1,则输出形状将简化为 ⌊ n h − 1 s h + 1 ⌋ × ⌊ n w − 1 s w + 1 ⌋ \lfloor\frac{n_h-1}{s_h}+1\rfloor \times \lfloor\frac{n_w-1}{s_w}+1\rfloor ⌊shnh−1+1⌋×⌊swnw−1+1⌋。更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 ( n h / s h ) × ( n w / s w ) (n_h/s_h) \times (n_w/s_w) (nh/sh)×(nw/sw)。
# 高度和宽度的步幅设置为2
nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
# 当输入高度和宽度两侧的填充数量分别为p_h和p_w时,填充padding=(p_h,p_w)
# 当高度和宽度上的步幅分别为s_h和s_w时,步幅stride=(s_h,s_w)
nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。
假设输入的通道数为 c i c_i ci,那么卷积核的输入通道数也需要为 c i c_i ci。当 c i > 1 c_i>1 ci>1时,可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将 c i c_i ci的结果相加)得到二维张量。
import torch
from d2l import torch as d2l
# X:输入张量,K:核张量
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(d2l.corr2d(x, k) for x, k in zip(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)
在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观来说,可以将每个通道看作是对不同特征的响应。
用 c i c_i ci和 c o c_o co分别表示输入和输出通道的数目, k h k_h kh和 k w k_w kw为卷积核的高度和宽度。为了获得多个通道的输出,卷积核的形状是 c o × c i × k h × k w c_o\times c_i\times k_h\times k_w co×ci×kh×kw。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。
计算多个通道的输出的互相关函数
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
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]]])
# 将核张量K与K+1(K中每个元素加 1)和K+2连接起来,构造了一个具有3个输出通道的卷积核
K = torch.stack((K, K + 1, K + 2), 0)
corr2d_multi_in_out(X, K)
上图展示了 1 × 1 1\times 1 1×1卷积核与 3 3 3个输入通道和 2 2 2个输出通道的互相关计算。此时输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。可以将 1 × 1 1\times 1 1×1卷积层看作是在每个像素位置应用的全连接层,以 c i c_i ci个输入值转换为 c o c_o co个输出值。因为这仍然是一个卷积层,所以跨像素的权重是一致的。同时, 1 × 1 1\times 1 1×1卷积层需要的权重维度为 c o × c i c_o\times c_i co×ci,再额外加上一个偏差。
常见的有平均值池化层(average pooling))和最大池化层(maximum pooling),如上图所示,求着色区域的平均值或最大值。
池化层函数
import torch
from torch import nn
def pool2d(X, pool_size, mode='max'):
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')
默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同。比如,使用(3, 3)的汇聚窗口,默认情况下,步幅形状为(3, 3)。
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
pool2d = nn.MaxPool2d(3)
# 设定一个任意大小的矩形汇聚窗口
# pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)