李沐动手学深度学习(PyTorch)课程学习笔记第五章:卷积神经网络。
1. 从全连接层到卷积
假设我们有一个足够充分的照片数据集,数据集中是拥有标注的照片,每张照片具有百万级像素,这意味着网络的每次输入都有一百万个维度。即使将隐藏层维度降低到1000,这个全连接层也将有十亿个参数。
假设我们想从一张图片中找到某个物体。合理的假设是:无论哪种方法找到这个物体,都应该和物体的位置无关。卷积神经网络正是将空间不变性(spatial invariance)的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示:
- 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。
- 局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。
2. 图像卷积
严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。
我们可以自己实现如上过程:
1 | import torch |
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。
我们可以基于上面定义的 corr2d
函数实现二维卷积层:
1 | class Conv2D(nn.Module): |
现在来看一下卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。我们假设0为黑色像素,1为白色像素:
1 | X = torch.ones((6, 8)) |
X
的内容如下:
1 | tensor([[1., 1., 0., 0., 0., 0., 1., 1.], |
接下来,我们构造一个高度为1、宽度为2的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零:
1 | K = torch.tensor([[1.0, -1.0]]) |
Y
的内容如下:
1 | tensor([[ 0., 1., 0., 0., 0., -1., 0.], |
现在我们使用 PyTorch 的卷积层尝试通过正确结果 Y
是否能学习出我们之前自己构造出的卷积核参数:
1 | # 构造一个二维卷积层,它具有1个输入通道和1个输出通道,卷积核形状为(1, 2) |
3. 填充和步幅
在应用多层卷积时,我们常常丢失边缘像素。由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。但随着我们应用许多连续卷积层,累积丢失的像素数就多了。解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是0)。
1 | # 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列 |
在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。在前面的例子中,我们默认每次滑动一个元素。但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。我们将每次滑动元素的数量称为步幅(stride)。
1 | conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2) |
4. 多输入多输出通道
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。
为了加深理解,我们实现一下多输入通道互相关运算。简而言之,我们所做的就是对每个通道执行互相关操作,然后将结果相加:
1 | import torch |
到目前为止,不论有多少输入通道,我们还只有一个输出通道。在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,我们可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。
1 | def corr2d_multi_in_out(X, K): |
PS:1*1卷积层通常用于调整网络层的通道数量和控制模型复杂性,其失去了卷积层的特有能力:在高度和宽度维度上,识别相邻元素间相互作用的能力。
5. 汇聚层(池化层)
汇聚层(pooling layer)具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动(从左至右、从上至下),为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。
1 | import torch |
与卷积层一样,汇聚层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。下面,我们用深度学习框架中内置的二维最大汇聚层,来演示汇聚层中填充和步幅的使用。
默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同。因此,如果我们使用形状为 (3, 3)
的汇聚窗口,那么默认情况下,我们得到的步幅形状为 (3, 3)
。
1 | X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4)) |
在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。这意味着汇聚层的输出通道数与输入通道数相同。下面,我们将在通道维度上连结张量 X
和 X + 1
,以构建具有2个通道的输入:
1 | X = torch.cat((X, X + 1), dim=1) |
6. LeNet
本节将介绍 LeNet,它是最早发布的卷积神经网络之一。总体来看,LeNet(LeNet-5)由两个部分组成:
- 卷积编码器:由两个卷积层组成。
- 全连接层密集块:由三个全连接层组成。
每个卷积块中的基本单元是一个卷积层、一个 Sigmoid 激活函数和平均汇聚层。请注意,虽然 ReLU 和最大汇聚层更有效,但它们在20世纪90年代还没有出现。每个卷积层使用5×5卷积核和一个 Sigmoid 激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有6个输出通道,而第二个卷积层有16个输出通道。每个2×2池化操作(步幅2)通过空间下采样将维数减少4倍。卷积的输出形状由批量大小、通道数、高度、宽度决定。
虽然卷积神经网络的参数较少,但与深度的多层感知机相比,它们的计算成本仍然很高,因为每个参数都参与更多的乘法。通过使用 GPU,可以用它加快训练。
1 | import torch |
下一章:现代卷积神经网络。