第六章-卷积神经网络

为什么要有所谓卷积神经网络

初衷：图像中本就拥有丰富的结构，而这些结构可以被人类和机器学习模型使用。通过使用这些结构，可以降低训练的参数量，提高模型的准确性。

譬如在一张图片里做物体寻找的任务，一个比较合理的假设是物体位置具有空间不变性。卷积神经网络正是将空间不变性（spatial invariance）的这一概念系统化，从而基于这个模型使用较少的参数来学习有用的表示。

可以归纳有以下两个特点：

平移不变性（translation invariance）：不管检测对象出现在图像中的哪个位置，神经网络的前面几层应该对相同的图像区域具有相似的反应，即为 “平移不变性”。

局部性（locality）：神经网络的前面几层应该只探索输入图像中的局部区域，而不过度在意图像中相隔较远区域的关系，这就是 “局部性” 原则。最终，可以聚合这些局部特征，以在整个图像级别进行预测。

数学表示

H\mathbf{H}H 是图像矩阵 X\mathbf{X}X 的隐藏表示。U\mathbf{U}U 是偏置。W\mathbf{W}W 是四阶权重张量。

索引 (i,j)(i, j)(i,j) 处表示在此位置的像素。于是全连接层中的隐藏表示可以推导如下：

Copy[H]i,j=[U]i,j+∑k∑l[W]i,j,k,l[X]k,l=[U]i,j+∑a∑b[V]i,j,a,b[X]i+a,j+b.\begin{aligned} \left[\mathbf{H}\right]_{i, j} &= [\mathbf{U}]_{i, j} + \sum_k \sum_l[\mathsf{W}]_{i, j, k, l} [\mathbf{X}]_{k, l}\\ &= [\mathbf{U}]_{i, j} + \sum_a \sum_b [\mathsf{V}]_{i, j, a, b} [\mathbf{X}]_{i+a, j+b}.\end{aligned}[H]i,j​​=[U]i,j​+k∑​l∑​[W]i,j,k,l​[X]k,l​=[U]i,j​+a∑​b∑​[V]i,j,a,b​[X]i+a,j+b​.​

其中 W\mathsf{W}W 到 V\mathsf{V}V 只不过是把求和 “中心” 移动到了 (i,j)(i, j)(i,j) 处而已。

平移不变性 意味着 U\mathsf{U}U 和 V\mathsf{V}V 不依赖于 (i,j)(i, j)(i,j) 的值。 这点很好理解，在不同的 (i,j)(i, j)(i,j) 处，若 X\mathbf{X}X 相同（物品一样），那么得到的隐藏表示也应该一样。 故：

Copy[H]i,j=u+∑a∑b[V]a,b[X]i+a,j+b.[\mathbf{H}]_{i, j} = u + \sum_a\sum_b [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.[H]i,j​=u+a∑​b∑​[V]a,b​[X]i+a,j+b​.

局部性 意味着我们可以把在某个范围之外的信息屏蔽，即在 [a,b][a,b][a,b] 之外的 V\mathbf{V}V 设置为 0

Copy[H]i,j=u+∑a=−Δ1Δ1∑b=−Δ2Δ2[V]a,b[X]i+a,j+b.[\mathbf{H}]_{i, j} = u + \sum_{a = -\Delta_1}^{\Delta_1} \sum_{b = -\Delta_2}^{\Delta_2} [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.[H]i,j​=u+a=−Δ1​∑Δ1​​b=−Δ2​∑Δ2​​[V]a,b​[X]i+a,j+b​.

称上式为一个卷积层。V\mathbf{V}V 被称为卷积核（convolution kernel）或者滤波器（filter）。

简单地喵两句卷积层。
卷积层所表达的运算其实是互相关运算（cross-correlation）
在卷积层中，输入张量和核张量通过互相关运算产生输出张量。
真正的卷积运算长这样（但其实无所谓）：

Copy(f∗g)(x)=∫f(z)g(x−z)dz.(f∗g)(i)=∑af(a)g(i−a).(f∗g)(i,j)=∑a∑bf(a,b)g(i−a,j−b).\begin{aligned} (f * g)(\mathbf{x}) &= \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}.\\ (f * g)(i) &= \sum_a f(a) g(i-a).\\ (f * g)(i, j) &= \sum_a\sum_b f(a, b) g(i-a, j-b). \end{aligned}(f∗g)(x)(f∗g)(i)(f∗g)(i,j)​=∫f(z)g(x−z)dz.=a∑​f(a)g(i−a).=a∑​b∑​f(a,b)g(i−a,j−b).​

实际上，图像是一个由高度、宽度和颜色组成的三维张量，比如包含1024×1024×31024 \times 1024 \times 31024×1024×3个像素。
[X]i,j→[X]i,j,k[V]a,b→[V]a,b,c[\mathsf{X}]_{i, j}\to[\mathsf{X}]_{i, j, k}\quad [\mathbf{V}]_{a,b}\to [\mathsf{V}]_{a,b,c}[X]i,j​→[X]i,j,k​[V]a,b​→[V]a,b,c​

H\mathsf{H}H 也采用三维张量。
换句话说，对于每一个空间位置，我们想要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。
因此，我们可以把隐藏表示想象为一系列具有二维张量的 通道（channel）。
这些通道有时也被称为 特征映射（feature maps），因为每个通道都向后续层提供一组空间化的学习特征。
直观上可以想象在靠近输入的底层，一些通道专门识别边缘，而一些通道专门识别纹理。

为了支持输入 X\mathsf{X}X 和隐藏表示 H\mathsf{H}H 中的多个通道，我们可以在 V\mathsf{V}V 中添加第四个坐标，即 [V]a,b,c,d[\mathsf{V}]_{a, b, c, d}[V]a,b,c,d​。综上所述，

Copy[H]i,j,d=∑a=−Δ1Δ1∑b=−Δ2Δ2∑c[V]a,b,c,d[X]i+a,j+b,c,[\mathsf{H}]_{i,j,d} = \sum_{a = -\Delta_1}^{\Delta_1} \sum_{b = -\Delta_2}^{\Delta_2} \sum_c [\mathsf{V}]_{a, b, c, d} [\mathsf{X}]_{i+a, j+b, c},[H]i,j,d​=a=−Δ1​∑Δ1​​b=−Δ2​∑Δ2​​c∑​[V]a,b,c,d​[X]i+a,j+b,c​,

其中隐藏表示 H\mathsf{H}H 中的索引 ddd 表示输出通道，而随后的输出将继续以三维张量 H\mathsf{H}H 作为输入进入下一个卷积层。
于是可以定义具有多个通道的卷积层，而其中 V\mathsf{V}V 是该卷积层的权重。

(nh−kh+1)×(nw−kw+1).(n_h-k_h+1) \times (n_w-k_w+1).(nh​−kh​+1)×(nw​−kw​+1).

代码练习

Copy
def corr2d(X, K):
"""二维互相关运算

Args:
X (Tensor): 二维矩阵
K (Tensor): Kernel

Returns:
Tensor: 二维互相关运算结果
"""
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]):
# Hadamard product(Schur product, entrywise)
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y

二维卷积层

Copy
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

简单的示例如下

Copy
import torch
import torch.nn as nn
import torch.optim as optim

conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
X = X.reshape((1, 1, 10, 12))
Y = Y.reshape((1, 1, 10, 11))

lr = 1e-1

optimizer = optim.SGD([conv2d.weight], lr=lr)

for epoch in range(100):
Y_hat = conv2d(X)
l = nn.MSELoss()(Y_hat, Y)

optimizer.zero_grad()
l.backward()
optimizer.step()

print(f"epoch {epoch+1}, loss {l.item():.3f}")

在卷积神经网络中，对于某一层的任意元素
，其感受野（receptive field）是指在前向传播期间可能影响
计算的所有元素（来自所有先前层）。

感受野可能大于输入的实际大小。当一个特征图中的任意元素需要检测更广区域的输入特征时，我们可以构建一个更深的网络。

填充和步幅

卷积的输出形状取决于输入形状和卷积核的形状。连续应用多次卷积之后得到的输出远小于输入。这样的副作用就是图像边缘的信息丢失。
可在边界填充（冗余）信息，来影响。
另外，对于分辨率过高的冗余信息，可通过步幅来改进（一次走多步）。

这样，填充之后输出维度 (nh−kh+1)×(nw−kw+1).(n_h-k_h+1) \times (n_w-k_w+1).(nh​−kh​+1)×(nw​−kw​+1). 变成
(nh−kh+ph+1)×(nw−kw+pw+1)。(n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1)。(nh​−kh​+ph​+1)×(nw​−kw​+pw​+1)。

卷积神经网络中卷积核的高度和宽度通常为奇数，例如 1、3、5 或 7。 选择奇数的好处是，保持空间维度的同时，我们可以在顶部和底部填充相同数量的行，在左侧和右侧填充相同数量的列。

常常设 ph=kh−1, pw=kw−1p_h=k_h-1, \,p_w=k_w-1ph​=kh​−1,pw​=kw​−1，使输入和输出具有相同的高度和宽度。
这样可以在构建网络时更容易地预测每个图层的输出形状。假设 khk_hkh​ 是奇数，我们将在高度的两侧填充 ph/2p_h/2ph​/2 行。
如果 khk_hkh​ 是偶数，则一种可能性是在输入顶部填充 ⌈ph/2⌉\lceil p_h/2\rceil⌈ph​/2⌉ 行，在底部填充 ⌊ph/2⌋\lfloor p_h/2\rfloor⌊ph​/2⌋ 行。同理，我们填充宽度的两侧。

步幅

每次滑动元素的数量称为步幅（stride）。
通常，当垂直步幅为 shs_hsh​、水平步幅为 sws_wsw​ 时，输出形状为
⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋.\lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor.⌊(nh​−kh​+ph​+sh​)/sh​⌋×⌊(nw​−kw​+pw​+sw​)/sw​⌋.

特别地，ph=kh−1p_h=k_h-1ph​=kh​−1 ， pw=kw−1p_w=k_w-1pw​=kw​−1 时，输出形状为
⌊(nh+sh−1)/sh⌋×⌊(nw+sw−1)/sw⌋\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor⌊(nh​+sh​−1)/sh​⌋×⌊(nw​+sw​−1)/sw​⌋

更进一步，如果输入的高度和宽度可以被垂直和水平步幅整除，则输出形状将为 (nh/sh)×(nw/sw)(n_h/s_h) \times (n_w/s_w)(nh​/sh​)×(nw​/sw​)。

代码练习

Copy
def comp_conv2d(conv2d, X):
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
return Y.reshape(Y.shape[2:])

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape

Copy
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

注意到 (parameter) padding: _size_2_t | str，padding 是高和宽的两侧分别填充的元素数量。

另外，nn 的 padding 默认是除后向上取整。

Copy
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1))
comp_conv2d(conv2d, X).shape
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 2))
comp_conv2d(conv2d, X).shape

多输入多输出通道

例如，每个 RGB 输入图像具有 3×h×w3\times h\times w3×h×w 的形状。我们将这个大小为 3 的轴称为 通道（channel）维度。

几个通道一般就要构造几个卷积核。得到一个 ci×kh×kwc_i\times k_h\times k_wci​×kh​×kw​ 的卷积核，互相关之后再求和。

Copy
def corr2d_multi_in(X, K):
"""多通道二维卷积

Args:
X (Tensor): 输入
K (Tensor): 核

Returns:
Tensor: 结果
"""
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)