卷积神经网络
从全连接层到卷积
_卷积神经网络_(convolutional neural networks,CNN)是机器学习利用自然图像中一些已知结构的创造性方法。
不变性
_平移不变性_(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。
_局部性_(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。
多层感知机的限制
多层感知机的限制
首先,多层感知机的输入是二维图像
其中
为了方便理解,我们可以认为,无论是输入还是隐藏表示都拥有空间结构。
使用$[\mathbf{X}]{i, j}
$$\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}$$
其中,从
我们只需重新索引下标
平移不变性
现在引用上述的第一个原则:平移不变性。
这意味着检测对象在输入
$$[\mathbf{H}]{i, j} = u + \sum_a\sum_b [\mathbf{V}]{a, b} [\mathbf{X}]_{i+a, j+b}.$$
这就是卷积(convolution)。我们是在使用系数$[\mathbf{V}]{a, b}
局部性
现在引用上述的第二个原则:局部性。如上所述,为了收集用来训练参数$[\mathbf{H}]{i, j}
$$[\mathbf{H}]{i, j} = u + \sum{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Delta} [\mathbf{V}]{a, b} [\mathbf{X}]{i+a, j+b}.$$
:eqlabel:eq_conv-layer
简而言之, :eqref:eq_conv-layer是一个卷积层(convolutional layer),而卷积神经网络是包含卷积层的一类特殊的神经网络。
在深度学习研究社区中,
当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大的:以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而现在卷积神经网络通常只需要几百个参数,而且不需要改变输入或隐藏表示的维数。
参数大幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每一层只包含局部的信息。
以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到未知数据中。
但如果这偏置与现实不符时,比如当图像不满足平移不变时,我们的模型可能难以拟合我们的训练数据。
卷积
在进一步讨论之前,我们先简要回顾一下为什么上面的操作被称为卷积。在数学中,两个函数(比如
也就是说,卷积是当把一个函数“翻转”并移位
当为离散对象时,积分就变成求和。例如,对于由索引为
对于二维张量,则为
:eqlabel:eq_2d-conv-discrete
这看起来类似于 :eqref:eq_conv-layer,但有一个主要区别:这里不是使用eq_conv-layer和 :eqref:eq_2d-conv-discrete之间的符号。我们在 :eqref:eq_conv-layer中的原始定义更正确地描述了互相关(cross-correlation),这个问题将在下一节中讨论。
“沃尔多在哪里”回顾
“沃尔多在哪里”游戏,让我们看看它到底是什么样子。卷积层根据滤波器fig_waldo_mask中所示。我们的目标是学习一个模型,以便探测出在“沃尔多”最可能出现的地方。

:width:400px
:label:fig_waldo_mask
通道
:label:subsec_why-conv-channels
然而这种方法有一个问题:我们忽略了图像一般包含三个通道/三种原色(红色、绿色和蓝色)。
实际上,图像不是二维张量,而是一个由高度、宽度和颜色组成的三维张量,比如包含
前两个轴与像素的空间位置有关,而第三个轴可以看作每个像素的多维表示。
因此,我们将
此外,由于输入图像是三维的,我们的隐藏表示
换句话说,对于每一个空间位置,我们想要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。
因此,我们可以把隐藏表示想象为一系列具有二维张量的通道(channel)。
这些通道有时也被称为特征映射(feature maps),因为每个通道都向后续层提供一组空间化的学习特征。
直观上可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。
为了支持输入
$$[\mathsf{H}]{i,j,d} = \sum{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Delta} \sum_c [\mathsf{V}]{a, b, c, d} [\mathsf{X}]{i+a, j+b, c},$$
:eqlabel:eq_conv-layer-channels
其中隐藏表示
所以, :eqref:eq_conv-layer-channels可以定义具有多个通道的卷积层,而其中
然而,仍有许多问题亟待解决。
例如,图像中是否到处都有存在沃尔多的可能?如何有效地计算输出层?如何选择适当的激活函数?为了训练有效的网络,如何做出合理的网络设计选择?我们将在本章的其它部分讨论这些问题。
小结
- 图像的平移不变性使我们以相同的方式处理局部图像,而不在乎它的位置。
- 局部性意味着计算相应的隐藏表示只需一小部分局部图像像素。
- 在图像处理中,卷积层通常比全连接层需要更少的参数,但依旧获得高效用的模型。
- 卷积神经网络(CNN)是一类特殊的神经网络,它可以包含多个卷积层。
- 多个输入和输出通道使模型在每个空间位置可以获取图像的多方面特征。
练习
- 假设卷积层 :eqref:
eq_conv-layer覆盖的局部区域。在这种情况下,证明卷积内核为每组通道独立地实现一个全连接层。 - 为什么平移不变性可能也不是好主意呢?
- 当从图像边界像素获取隐藏表示时,我们需要思考哪些问题?
- 描述一个类似的音频卷积层的架构。
- 卷积层也适合于文本数据吗?为什么?
- 证明在 :eqref:
eq_2d-conv-discrete中,。
图像卷积
:label:sec_conv_layer
上节我们解析了卷积层的原理,现在我们看看它的实际应用。由于卷积神经网络的设计是用于探索图像数据,本节我们将以图像为例。
互相关运算
严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。
根据 :numref:sec_why-conv中的描述,在卷积层中,输入张量和核张量通过(互相关运算)产生输出张量。
首先,我们暂时忽略通道(第三维)这一情况,看看如何处理二维图像数据和隐藏表示。在 :numref:fig_correlation中,输入是高度为
:label:fig_correlation
在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。
当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。
在如上例子中,输出张量的四个元素由二维互相关运算得到,这个输出高度为
注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1,
而卷积核只与图像中每个大小完全适合的位置进行互相关运算。
所以,**输出大小等于输入大小
这是因为我们需要足够的空间在图像上“移动”卷积核。稍后,我们将看到如何通过在图像边界周围填充零来保证有足够的空间移动卷积核,从而保持输出大小不变。
接下来,我们在corr2d函数中实现如上过程,该函数接受输入张量X和卷积核张量K,并返回输出张量Y。
1 | import torch |
1 | def corr2d(X, K): #@save |
通过 :numref:fig_correlation的输入张量X和卷积核张量K,我们来[验证上述二维互相关运算的输出]。
1 | X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]]) |
1 | tensor([[19., 25.], |
卷积层
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。
所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。
就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。
基于上面定义的corr2d函数[实现二维卷积层]。在__init__构造函数中,将weight和bias声明为两个模型参数。前向传播函数调用corr2d函数并添加偏置。
1 | class Conv2D(nn.Module): |
高度和宽度分别为
我们也将带有
图像中目标的边缘检测
如下是[卷积层的一个简单应用:]通过找到像素变化的位置,来(检测图像中不同颜色的边缘)。
首先,我们构造一个
1 | X = torch.ones((6, 8)) |
1 | tensor([[1., 1., 0., 0., 0., 0., 1., 1.], |
接下来,我们构造一个高度为K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。
1 | K = torch.tensor([[1.0, -1.0]]) |
现在,我们对参数X(输入)和K(卷积核)执行互相关运算。
如下所示,[输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘],其他情况的输出为
1 | Y = corr2d(X, K) |
1 | tensor([[ 0., 1., 0., 0., 0., -1., 0.], |
现在我们将输入的二维图像转置,再进行如上的互相关运算。
其输出如下,之前检测到的垂直边缘消失了。
不出所料,这个[卷积核K只可以检测垂直边缘],无法检测水平边缘。
1 | corr2d(X.t(), K) |
1 | tensor([[0., 0., 0., 0., 0.], |
学习卷积核
如果我们只需寻找黑白边缘,那么以上[1, -1]的边缘检测器足以。然而,当有了更复杂数值的卷积核,或者连续的卷积层时,我们不可能手动设计滤波器。那么我们是否可以[学习由X生成Y的卷积核]呢?
现在让我们看看是否可以通过仅查看“输入-输出”对来学习由X生成Y的卷积核。
我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,我们在此使用内置的二维卷积层,并忽略偏置。
1 | # 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核 |
1 | epoch 2, loss 10.531 |
在
1 | conv2d.weight.data.reshape((1, 2)) |
1 | tensor([[ 1.0010, -0.9739]]) |
细心的读者一定会发现,我们学习到的卷积核权重非常接近我们之前定义的卷积核K。
互相关和卷积
回想一下我们在 :numref:sec_why-conv中观察到的互相关和卷积运算之间的对应关系。
为了得到正式的卷积运算输出,我们需要执行 :eqref:eq_2d-conv-discrete中定义的严格卷积运算,而不是互相关运算。
幸运的是,它们差别不大,我们只需水平和垂直翻转二维卷积核张量,然后对输入张量执行互相关运算。
值得注意的是,由于卷积核是从数据中学习到的,因此无论这些层执行严格的卷积运算还是互相关运算,卷积层的输出都不会受到影响。
为了说明这一点,假设卷积层执行互相关运算并学习 :numref:fig_correlation中的卷积核,该卷积核在这里由矩阵
假设其他条件不变,当这个层执行严格的卷积时,学习的卷积核
也就是说,当卷积层对 :numref:fig_correlation中的输入和fig_correlation中相同的输出。
为了与深度学习文献中的标准术语保持一致,我们将继续把“互相关运算”称为卷积运算,尽管严格地说,它们略有不同。
此外,对于卷积核张量上的权重,我们称其为元素。
特征映射和感受野
如在 :numref:subsec_why-conv-channels中所述, :numref:fig_correlation中输出的卷积层有时被称为特征映射(feature map),因为它可以被视为一个输入映射到下一层的空间维度的转换器。
在卷积神经网络中,对于某一层的任意元素
请注意,感受野可能大于输入的实际大小。让我们用 :numref:fig_correlation为例来解释感受野:
给定
假设之前输出为
在这种情况下,
因此,当一个特征图中的任意元素需要检测更广区域的输入特征时,我们可以构建一个更深的网络。
小结
- 二维卷积层的核心计算是二维互相关运算。最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置。
- 我们可以设计一个卷积核来检测图像的边缘。
- 我们可以从数据中学习卷积核的参数。
- 学习卷积核时,无论用严格卷积运算或互相关运算,卷积层的输出不会受太大影响。
- 当需要检测输入特征中更广区域时,我们可以构建一个更深的卷积网络。
练习
- 构建一个具有对角线边缘的图像
X。- 如果将本节中举例的卷积核
K应用于X,会发生什么情况? - 如果转置
X会发生什么?
X.T将主对角线变为副对角线(从右上到左下) - 如果转置
K会发生什么?
转置卷积核 ≈ 旋转检测方向
- 如果将本节中举例的卷积核
- 在我们创建的
Conv2D自动求导时,有什么错误消息? - 如何通过改变输入张量和卷积核张量,将互相关运算表示为矩阵乘法?
- 手工设计一些卷积核。
- 二阶导数的核的形式是什么?
- 积分的核的形式是什么?
- 得到
次导数的最小核的大小是多少?
填充和步幅
:label:sec_padding
在前面的例子 :numref:fig_correlation中,输入的高度和宽度都为
正如我们在 :numref:sec_conv_layer中所概括的那样,假设输入形状为
因此,卷积的输出形状取决于输入形状和卷积核的形状。
还有什么因素会影响输出的大小呢?本节我们将介绍填充(padding)和步幅(stride)。假设以下情景:
有时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于
有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余。步幅则可以在这类情况下提供帮助。
填充
如上所述,在应用多层卷积时,我们常常丢失边缘像素。
由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。
但随着我们应用许多连续卷积层,累积丢失的像素数就多了。
解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是
例如,在 :numref:img_conv_pad中,我们将
:label:img_conv_pad
通常,如果我们添加
这意味着输出的高度和宽度将分别增加
在许多情况下,我们需要设置
这样可以在构建网络时更容易地预测每个图层的输出形状。假设
如果
卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。
选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
此外,使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X,当满足:
- 卷积核的大小是奇数;
- 所有边的填充行数和列数相同;
- 输出与输入具有相同高度和宽度
则可以得出:输出Y[i, j]是通过以输入X[i, j]为中心,与卷积核进行互相关计算得到的。
比如,在下面的例子中,我们创建一个高度和宽度为3的二维卷积层,并(在所有侧边填充1个像素)。给定高度和宽度为8的输入,则输出的高度和宽度也是8。
1 | import torch |
1 | torch.Size([8, 8]) |
当卷积核的高度和宽度不同时,我们可以[填充不同的高度和宽度],使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。
1 | conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1)) |
1 | torch.Size([8, 8]) |
步幅
在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。
在前面的例子中,我们默认每次滑动一个元素。
但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
我们将每次滑动元素的数量称为步幅(stride)。到目前为止,我们只使用过高度或宽度为
:numref:img_conv_stride是垂直步幅为
着色部分是输出元素以及用于输出计算的输入和内核张量元素:
可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加另一列填充)。
:label:img_conv_stride
通常,当垂直步幅为
如果我们设置了
更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为
下面,我们[将高度和宽度的步幅设置为2],从而将输入的高度和宽度减半。
1 | conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2) |
1 | torch.Size([4, 4]) |
接下来,看(一个稍微复杂的例子)。
1 | conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4)) |
1 | torch.Size([2, 2]) |
为了简洁起见,当输入高度和宽度两侧的填充数量分别为
小结
- 填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
- 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的
( 是一个大于 的整数)。 - 填充和步幅可用于有效地调整数据的维度。
多输入多输出通道
:label:sec_channels
虽然我们在 :numref:subsec_why-conv-channels中描述了构成每个图像的多个通道和多层卷积层。例如彩色图像具有标准的RGB通道来代表红、绿和蓝。
但是到目前为止,我们仅展示了单个输入和单个输出通道的简化例子。
这使得我们可以将输入、卷积核和输出看作二维张量。
当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如 ,每个RGB输入图像具有
多输入通道
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为
然而,当
在 :numref:fig_conv_multi_in中,我们演示了一个具有两个输入通道的二维互相关运算的示例。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素:
:label:fig_conv_multi_in
为了加深理解,我们(实现一下多输入通道互相关运算)。
简而言之,我们所做的就是对每个通道执行互相关操作,然后将结果相加。
1 | import torch |
1 | def corr2d_multi_in(X, K): |
我们可以构造与 :numref:fig_conv_multi_in中的值相对应的输入张量X和核张量K,以(验证互相关运算的输出)。
1 | X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]], |
1 | tensor([[ 56., 72.], |
多输出通道
到目前为止,不论有多少输入通道,我们还只有一个输出通道。然而,正如我们在 :numref:subsec_why-conv-channels中所讨论的,每一层有多个输出通道是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,我们可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。
用
如下所示,我们实现一个[计算多个通道的输出的互相关函数]。
1 | def corr2d_multi_in_out(X, K): |
通过将核张量K与K+1(K中每个元素加K+2连接起来,构造了一个具有
1 | K = torch.stack((K, K + 1, K + 2), 0) |
1 | torch.Size([3, 2, 2, 2]) |
下面,我们对输入张量X与卷积核张量K执行互相关运算。现在的输出包含X和多输入单输出通道的结果一致。
1 | corr2d_multi_in_out(X, K) |
1 | tensor([[[ 56., 72.], |
卷积层
[1x1卷积]
毕竟,卷积的本质是有效提取相邻像素间的相关特征,而
尽管如此,
因为使用了最小窗口,
其实
:numref:fig_conv_1x1展示了使用
这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。
我们可以将
因为这仍然是一个卷积层,所以跨像素的权重是一致的。
同时,
:label:fig_conv_1x1
下面,我们使用全连接层实现
请注意,我们需要对输入和输出的数据形状进行调整。
1 | def corr2d_multi_in_out_1x1(X, K): |
当执行corr2d_multi_in_out。让我们用一些样本数据来验证这一点。
1 | X = torch.normal(0, 1, (3, 3, 3)) |
1 | Y1 = corr2d_multi_in_out_1x1(X, K) |
小结
- 多输入多输出通道可以用来扩展卷积层的模型。
- 当以每像素为基础应用时,
卷积层相当于全连接层。 卷积层通常用于调整网络层的通道数量和控制模型复杂性。
练习
- 假设我们有两个卷积核,大小分别为
和 (中间没有非线性激活函数)。 - 证明运算可以用单次卷积来表示。
- 这个等效的单个卷积核的维数是多少呢?
- 反之亦然吗?
- 假设输入为
,卷积核大小为 ,填充为 ,步幅为 。 - 前向传播的计算成本(乘法和加法)是多少?
- 内存占用是多少?
- 反向传播的内存占用是多少?
- 反向传播的计算成本是多少?
- 如果我们将输入通道
和输出通道 的数量加倍,计算数量会增加多少?如果我们把填充数量翻一番会怎么样? - 如果卷积核的高度和宽度是
,前向传播的计算复杂度是多少? - 本节最后一个示例中的变量
Y1和Y2是否完全相同?为什么? - 当卷积窗口不是
时,如何使用矩阵乘法实现卷积?
汇聚层
:label:sec_pooling
通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。
而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”),所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
此外,当检测较底层的特征时(例如 :numref:sec_conv_layer中所讨论的边缘),我们通常希望这些特征保持某种程度上的平移不变性。例如,如果我们拍摄黑白之间轮廓清晰的图像X,并将整个图像向右移动一个像素,即Z[i, j] = X[i, j + 1],则新图像Z的输出可能大不相同。而在现实中,随着拍摄角度的移动,任何物体几乎不可能发生在同一像素上。即使用三脚架拍摄一个静止的物体,由于快门的移动而引起的相机振动,可能会使所有物体左右移动一个像素(除了高端相机配备了特殊功能来解决这个问题)。
本节将介绍汇聚(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
最大汇聚层和平均汇聚层
与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。
然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。
相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。
在这两种情况下,与互相关运算符一样,汇聚窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。
:label:fig_pooling
:numref:fig_pooling中的输出张量的高度为
汇聚窗口形状为
回到本节开头提到的对象边缘检测示例,现在我们将使用卷积层的输出作为
设置卷积层输入为X,汇聚层输出为Y。
无论X[i, j]和X[i, j + 1]的值相同与否,或X[i, j + 1]和X[i, j + 2]的值相同与否,汇聚层始终输出Y[i, j] = 1。
也就是说,使用
在下面的代码中的pool2d函数,我们(实现汇聚层的前向传播)。
这类似于 :numref:sec_conv_layer中的corr2d函数。
然而,这里我们没有卷积核,输出为输入中每个区域的最大值或平均值。
1 | import torch |
1 | def pool2d(X, pool_size, mode='max'): |
我们可以构建 :numref:fig_pooling中的输入张量X,[验证二维最大汇聚层的输出]。
1 | X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]]) |
1 | tensor([[4., 5.], |
此外,我们还可以(验证平均汇聚层)。
1 | pool2d(X, (2, 2), 'avg') |
1 | tensor([[2., 3.], |
[填充和步幅]
与卷积层一样,汇聚层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。
下面,我们用深度学习框架中内置的二维最大汇聚层,来演示汇聚层中填充和步幅的使用。
我们首先构造了一个输入张量X,它有四个维度,其中样本数和通道数都是1。
1 | X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4)) |
1 | tensor([[[[ 0., 1., 2., 3.], |
默认情况下,(深度学习框架中的步幅与汇聚窗口的大小相同)。
因此,如果我们使用形状为(3, 3)的汇聚窗口,那么默认情况下,我们得到的步幅形状为(3, 3)。
1 | pool2d = nn.MaxPool2d(3) |
1 | tensor([[[[10.]]]]) |
[填充和步幅可以手动设定]。
1 | pool2d = nn.MaxPool2d(3, padding=1, stride=2) |
1 | tensor([[[[ 5., 7.], |
当然,我们可以(设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度)。
1 | pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1)) |
1 | tensor([[[[ 5., 7.], |
多个通道
在处理多通道输入数据时,[汇聚层在每个输入通道上单独运算],而不是像卷积层一样在通道上对输入进行汇总。
这意味着汇聚层的输出通道数与输入通道数相同。
下面,我们将在通道维度上连结张量X和X + 1,以构建具有2个通道的输入。
1 | X = torch.cat((X, X + 1), 1) |
1 | tensor([[[[ 0., 1., 2., 3.], |
如下所示,汇聚后输出通道的数量仍然是2。
1 | pool2d = nn.MaxPool2d(3, padding=1, stride=2) |
1 | tensor([[[[ 5., 7.], |
小结
- 对于给定输入元素,最大汇聚层会输出该窗口内的最大值,平均汇聚层会输出该窗口内的平均值。
- 汇聚层的主要优点之一是减轻卷积层对位置的过度敏感。
- 我们可以指定汇聚层的填充和步幅。
- 使用最大汇聚层以及大于1的步幅,可减少空间维度(如高度和宽度)。
- 汇聚层的输出通道数与输入通道数相同。
练习
- 尝试将平均汇聚层作为卷积层的特殊情况实现。
- 尝试将最大汇聚层作为卷积层的特殊情况实现。
- 假设汇聚层的输入大小为
,则汇聚窗口的形状为 ,填充为 ,步幅为 。这个汇聚层的计算成本是多少? - 为什么最大汇聚层和平均汇聚层的工作方式不同?
- 我们是否需要最小汇聚层?可以用已知函数替换它吗?
- 除了平均汇聚层和最大汇聚层,是否有其它函数可以考虑(提示:回想一下
softmax)?为什么它不流行?
卷积神经网络(LeNet)
:label:sec_lenet
通过之前几节,我们学习了构建一个完整卷积神经网络的所需组件。
回想一下,之前我们将softmax回归模型( :numref:sec_softmax_scratch)和多层感知机模型( :numref:sec_mlp_scratch)应用于Fashion-MNIST数据集中的服装图片。
为了能够应用softmax回归和多层感知机,我们首先将每个大小为
而现在,我们已经掌握了卷积层的处理方法,我们可以在图像中保留空间结构。
同时,用卷积层代替全连接层的另一个好处是:模型更简洁、所需的参数更少。
本节将介绍LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。
这个模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名),目的是识别图像 :cite:LeCun.Bottou.Bengio.ea.1998中的手写数字。
当时,Yann LeCun发表了第一篇通过反向传播成功训练卷积神经网络的研究,这项工作代表了十多年来神经网络研究开发的成果。
当时,LeNet取得了与支持向量机(support vector machines)性能相媲美的成果,成为监督学习的主流方法。
LeNet被广泛用于自动取款机(ATM)机中,帮助识别处理支票的数字。
时至今日,一些自动取款机仍在运行Yann LeCun和他的同事Leon Bottou在上世纪90年代写的代码呢!
LeNet
总体来看,(LeNet(LeNet-5)由两个部分组成:)(卷积编码器和全连接层密集块)
- 卷积编码器:由两个卷积层组成;
- 全连接层密集块:由三个全连接层组成。
该架构如 :numref:img_lenet所示。
:label:
img_lenet
每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均汇聚层。请注意,虽然ReLU和最大汇聚层更有效,但它们在20世纪90年代还没有出现。每个卷积层使用
为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。换言之,我们将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。LeNet的稠密块有三个全连接层,分别有120、84和10个输出。因为我们在执行分类任务,所以输出层的10维对应于最后输出结果的数量。
通过下面的LeNet代码,可以看出用深度学习框架实现此类模型非常简单。我们只需要实例化一个Sequential块并将需要的层连接在一起。
1 | import torch |
我们对原始模型做了一点小改动,去掉了最后一层的高斯激活。除此之外,这个网络与最初的LeNet-5一致。
下面,我们将一个大小为img_lenet_vert一致。
:label:`img_lenet_vert`
1 | X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32) |
1 | Conv2d output shape: torch.Size([1, 6, 28, 28]) |
请注意,在整个卷积块中,与上一层相比,每一层特征的高度和宽度都减小了。
第一个卷积层使用2个像素的填充,来补偿
相反,第二个卷积层没有填充,因此高度和宽度都减少了4个像素。
随着层叠的上升,通道的数量从输入时的1个,增加到第一个卷积层之后的6个,再到第二个卷积层之后的16个。
同时,每个汇聚层的高度和宽度都减半。最后,每个全连接层减少维数,最终输出一个维数与结果分类数相匹配的输出。
模型训练
现在我们已经实现了LeNet,让我们看看[LeNet在Fashion-MNIST数据集上的表现]。
1 | batch_size = 256 |
虽然卷积神经网络的参数较少,但与深度的多层感知机相比,它们的计算成本仍然很高,因为每个参数都参与更多的乘法。
通过使用GPU,可以用它加快训练。
为了进行评估,我们需要[对] :numref:sec_softmax_scratch中描述的(evaluate_accuracy函数进行轻微的修改)。
由于完整的数据集位于内存中,因此在模型使用GPU计算数据集之前,我们需要将其复制到显存中。
1 | def evaluate_accuracy_gpu(net, data_iter, device=None): #@save |
[为了使用GPU,我们还需要一点小改动]。
与 :numref:sec_softmax_scratch中定义的train_epoch_ch3不同,在进行正向和反向传播之前,我们需要将每一小批量数据移动到我们指定的设备(例如GPU)上。
如下所示,训练函数train_ch6也类似于 :numref:sec_softmax_scratch中定义的train_ch3。
由于我们将实现多层神经网络,因此我们将主要使用高级API。
以下训练函数假定从高级API创建的模型作为输入,并进行相应的优化。
我们使用在 :numref:subsec_xavier中介绍的Xavier随机初始化模型参数。
与全连接层一样,我们使用交叉熵损失函数和小批量随机梯度下降。
1 | #@save |
现在,我们[训练和评估LeNet-5模型]。
1 | lr, num_epochs = 0.9, 10 |

小结
- 卷积神经网络(CNN)是一类使用卷积层的网络。
- 在卷积神经网络中,我们组合使用卷积层、非线性激活函数和汇聚层。
- 为了构造高性能的卷积神经网络,我们通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数。
- 在传统的卷积神经网络中,卷积块编码得到的表征在输出之前需由一个或多个全连接层进行处理。
- LeNet是最早发布的卷积神经网络之一。
练习
- 将平均汇聚层替换为最大汇聚层,会发生什么?
- 尝试构建一个基于LeNet的更复杂的网络,以提高其准确性。
- 调整卷积窗口大小。
- 调整输出通道的数量。
- 调整激活函数(如ReLU)。
- 调整卷积层的数量。
- 调整全连接层的数量。
- 调整学习率和其他训练细节(例如,初始化和轮数)。
- 在MNIST数据集上尝试以上改进的网络。
- 显示不同输入(例如毛衣和外套)时,LeNet第一层和第二层的激活值。







