一 从全连接层到卷积
卷积网络 主干的基本元素:这包括 卷积层本身、填充(padding)和 步幅 (stride)的基本细节、用于在相邻区域汇聚信息的 汇聚层(pooling)、在每一层中多通道(channel)的使用。卷积神经网络 (convolutional neural networks,CNN)是机器学习利用自然图像中一些已知结构的创造性方法。
的多层感知机十分适合处理表格数据,其中 行对应样本,列对应特征。
1.1 不变性
- 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层 应该对相同的图像区域具有相似的反应,即为“平移不变性”。
- 局部性(locality):神经网络的前面几层应该 只探索输入图像中的局部区域,而不过度在意图像中相隔 较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。
当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大 的:以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而 现在卷积神经网络通常只需要几百个参数,而且 不需要改变输入或隐藏表示的维数。参数大幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每一层只包含局部的信息。以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到 未知数据中。但如果这偏置与现实不符时,比如当图像不满足平移不变时,我们的模型可能难以拟合我们的训练数据。
小结:
- 图像的平移不变性使我们以相同的方式处理局部图像,而 不在乎它的位置。
- 局部性意味着计算相应的隐藏 表示只需一小部分局部图像像素。
- 在图像处理中,卷积层通常比全连接层需要更少的参数,但依旧获得高效用的模型。
- 卷积神经网络(CNN)是一类特殊的神经网络,它可以 包含多个卷积层。
- 多个输入和输出通道使模型在每个空间位置可以获取图像的多方面特征。
1.2 图像卷积
严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是 互相关运算(cross‐correlation),而不是卷积运算。在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
这是因为我们需要足够的空间在图像上“移动”卷积核。稍后,我们将看到 如何通过在图像边界周围填充零 来保证有足够的空间移动卷积核,从而保持输出大小不变。接下来,我们在corr2d函数中实现如上过程,该 函数接受输入张量X和卷积核张量K,并返回输出张量Y。
import torch
from torch import nn
from d2l import torch as d2l
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
验证上述二维互相关运算的输出。
X = torch.tensor([[0.0, 1.0, 2.0],
[3.0, 4.0, 5.0],
[6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0],
[2.0, 3.0]])
corr2d(X, K)
# tensor([[19., 25.],
# [37., 43.]])
1.3 卷积层
卷积层 对输入和卷积核权重 进行互相关运算,并在 添加标量偏置 之后产生输出。所以,卷积层中的两个被训 练的参数是卷积核权重和标量偏置。就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时, 我们也随机初始化卷积核权重。
基于上面定义的corr2d函数实现二维卷积层。在__init__构造函数中,将 weight和bias声明为两个模型参数。前向传播函数调用corr2d函数并添加偏置。
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
高度和宽度分别为h和w的卷积核可以被称为h × w卷积或h × w卷积核。
1.4 图像中目标的边缘检测
如下是卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。首先,我们构造 一个6 × 8像素的黑白图像。中间四列为黑色(0),其余像素为白色(1)。
x = torch.ones((6, 8))
x[:, 2:6] = 0
x
# tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
# [1., 1., 0., 0., 0., 0., 1., 1.],
# [1., 1., 0., 0., 0., 0., 1., 1.],
# [1., 1., 0., 0., 0., 0., 1., 1.],
# [1., 1., 0., 0., 0., 0., 1., 1.],
# [1., 1., 0., 0., 0., 0., 1., 1.]])
接下来,我们构造一个 高度为1、宽度为2 的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则 输出为零,否则输出为非零。
k = torch.tensor([[1.0, -1.0]])
k
# tensor([[ 1., -1.]])
现在,我们对参数X(输入)和K(卷积核)执行互相关运算。如下所示,输出Y中的1代表从白色到黑色的边 缘,‐1代表从黑色到白色的边缘,其他情况的输出为0。
y = corr2d(x, k)
y
# tensor([[ 0., 1., 0., 0., 0., -1., 0.],
# [ 0., 1., 0., 0., 0., -1., 0.],
# [ 0., 1., 0., 0., 0., -1., 0.],
# [ 0., 1., 0., 0., 0., -1., 0.],
# [ 0., 1., 0., 0., 0., -1., 0.],
# [ 0., 1., 0., 0., 0., -1., 0.]])
现在我们将输入的二维图像转置,再进行如上的互相关运算。其输出如下,之前检测到的垂直边缘消失了。 不出所料,这个卷积核K只可以检测垂直边缘,无法检测水平边缘。
corr2d(x.t(), k)
# tensor([[0., 0., 0., 0., 0.],
# [0., 0., 0., 0., 0.],
# [0., 0., 0., 0., 0.],
# [0., 0., 0., 0., 0.],
# [0., 0., 0., 0., 0.],
# [0., 0., 0., 0., 0.],
# [0., 0., 0., 0., 0.],
# [0., 0., 0., 0., 0.]])
1.5 学习卷积核
如果我们只需寻找黑白边缘,那么以上[1, -1]的边缘检测器足以。然而,当有了更复杂数值的卷积核,或者 连续的卷积层时,我们不可能手动设计滤波器。
现在让我们看看是否可以通过仅查看“输入‐输出”对来学习由X生成Y的卷积核。我们先构造一个卷积层,并 将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y与卷积层输出的平方误差,然后计算梯度 来更新卷积核。
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
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}')
# epoch 2, loss 3.816
# epoch 4, loss 0.789
# epoch 6, loss 0.193
# epoch 8, loss 0.057
# epoch 10, loss 0.020
在10次迭代之后,误差已经降到足够低。现在我们来看看 我们所学的卷积核的权重张量。
conv2d.weight.data.reshape((1, 2))
# tensor([[ 1.0043, -0.9771]])