为什么要有所谓卷积神经网络#
初衷:图像中本就拥有丰富的结构,而这些结构可以被人类和机器学习模型使用。通过使用这些结构,可以降低训练的参数量,提高模型的准确性。
譬如在一张图片里做物体寻找的任务,一个比较合理的假设是物体位置具有空间不变性。卷积神经网络正是将空间不变性(spatial invariance)的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。
可以归纳有以下两个特点:
- 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为 “平移不变性”。
- 局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是 “局部性” 原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。
数学表示#
是图像矩阵 的隐藏表示。 是偏置。 是四阶权重张量。
索引 处表示在此位置的像素。于是全连接层中的隐藏表示可以推导如下:
其中 到 只不过是把求和 “中心” 移动到了 处而已。
平移不变性 意味着 和 不依赖于 的值。 这点很好理解,在不同的 处,若 相同(物品一样),那么得到的隐藏表示也应该一样。 故:
局部性 意味着我们可以把在某个范围之外的信息屏蔽,即在 之外的 设置为 0
称上式为一个卷积层。 被称为卷积核(convolution kernel)或者滤波器(filter)。
简单地喵两句卷积层。
卷积层所表达的运算其实是互相关运算(cross-correlation)
在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
真正的卷积运算长这样(但其实无所谓):
实际上,图像是一个由高度、宽度和颜色组成的三维张量,比如包含个像素。
也采用三维张量。
换句话说,对于每一个空间位置,我们想要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。
因此,我们可以把隐藏表示想象为一系列具有二维张量的 通道(channel)。
这些通道有时也被称为 特征映射(feature maps),因为每个通道都向后续层提供一组空间化的学习特征。
直观上可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。
为了支持输入 和隐藏表示 中的多个通道,我们可以在 中添加第四个坐标,即 。综上所述,
其中隐藏表示 中的索引 表示输出通道,而随后的输出将继续以三维张量 作为输入进入下一个卷积层。
于是可以定义具有多个通道的卷积层,而其中 是该卷积层的权重。
代码练习#
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
二维卷积层
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
简单的示例如下
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)是指在前向传播期间可能影响
计算的所有元素(来自所有先前层)。
感受野可能大于输入的实际大小。当一个特征图中的任意元素需要检测更广区域的输入特征时,我们可以构建一个更深的网络。
填充和步幅#
卷积的输出形状取决于输入形状和卷积核的形状。连续应用多次卷积之后得到的输出远小于输入。这样的副作用就是图像边缘的信息丢失。
可在边界填充(冗余)信息,来影响。
另外,对于分辨率过高的冗余信息,可通过步幅来改进(一次走多步)。
这样,填充之后输出维度 变成
卷积神经网络中卷积核的高度和宽度通常为奇数,例如 1、3、5 或 7。 选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
常常设 ,使输入和输出具有相同的高度和宽度。
这样可以在构建网络时更容易地预测每个图层的输出形状。假设 是奇数,我们将在高度的两侧填充 行。
如果 是偶数,则一种可能性是在输入顶部填充 行,在底部填充 行。同理,我们填充宽度的两侧。
步幅
每次滑动元素的数量称为步幅(stride)。
通常,当垂直步幅为 、水平步幅为 时,输出形状为
特别地, , 时,输出形状为
更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 。
代码练习#
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
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 默认是除后向上取整。
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 的轴称为 通道(channel)维度。
几个通道一般就要构造几个卷积核。得到一个 的卷积核,互相关之后再求和。
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)