深度学习实验(三)
实验三 卷积神经网络 LeNet
一、实验目的
卷积综述
互相关运算
严格地说,卷积层所表达的运算可以被更准确地描述为互相关运算 (cross-correlation)。
根据 中的描述,在卷积层中,输入张量和核张量通过(互相关运算)产生输出张量。
首先,我们暂时忽略通道(第三维)这一情况,看看如何处理二维图像数据和隐藏表示。输入是高度为 333 、宽度为 333 的二维张量(即形状为 3×33 \times 33×3 )。卷积核的高度和宽度都是 222 ,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即 2×22 \times 22×2 )。
在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。
当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。
在如上例子中,输出张量的四个元素由二维互相关运算得到,这个输出高度为 222 、宽度为 222 ,如下所示:
0×0+1×1+3×2+4×3=19,1×0+2×1+4×2+5×3=25,3×0+4×1+6×2+7×3=37,4×0+5×1+7×2+8×3=43. 0\times0+1\times1+3\times2+4\times3=19,\\ 1\times0+2\times1+4\times2+5\times3=25,\\ 3\times0+4\times1+6\times2+7\times3=37,\\ 4\times0+5\times1+7\times2+8\times3=43. 0×0+1×1+3×2+4×3=19,1×0+2×1+4×2+5×3=25,3×0+4×1+6×2+7×3=37,4×0+5×1+7×2+8×3=43.
注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1,
而卷积核只与图像中每个大小完全适合的位置进行互相关运算。
所以,输出大小等于输入大小 nh×nwn_h \times n_wnh×nw 减去卷积核大小 kh×kwk_h \times k_wkh×kw,即:
(nh−kh+1)×(nw−kw+1) (n_h-k_h+1) \times (n_w-k_w+1) (nh−kh+1)×(nw−kw+1)
这是因为我们需要足够的空间在图像上“移动”卷积核。稍后,我们将看到如何通过在图像边界周围填充零来有保证足够的空间来移动内核,从而保持输出大小不变。
卷积层
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。
所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。
互相关和卷积
值得注意的是,由于卷积核是从数据中学习到的,因此无论这些层执行严格的卷积运算还是互相关运算,卷积层的输出都不会受到影响。
为了说明这一点,假设卷积层执行互相关运算并学习,该卷积核在这里由矩阵 K\mathbf{K}K 表示。
假设其他条件不变,当这个层执行严格的卷积时,学习的卷积核 K′\mathbf{K}'K′ 在水平和垂直翻转之后将与 K\mathbf{K}K 相同。
也就是说,当卷积层对输入和 K′\mathbf{K}'K′ 执行严格卷积运算时,将得到与互相关运算中相同的输出。
为了与深度学习文献中的标准术语保持一致,我们将继续把“互相关运算”称为卷积运算,尽管严格地说,它们略有不同。
此外,对于卷积核张量上的权重,我们称其为元素。
特征映射和感受野
在CNN中,对于某一层的任意元素 xxx ,其 感受野 (Receptive Field)是指在前向传播期间可能影响 xxx 计算的所有元素(来自所有先前层)。
给定 2×22 \times 22×2 卷积核,阴影输出元素值 191919 的接收域是输入阴影部分的四个元素。
假设之前输出为 Y\mathbf{Y}Y ,其大小为 2×22 \times 22×2 ,现在我们在其后附加一个卷积层,该卷积层以 Y\mathbf{Y}Y 为输入,输出单个元素 zzz。
在这种情况下, Y\mathbf{Y}Y 上的 zzz 的接收字段包括 Y\mathbf{Y}Y 的所有四个元素,而输入的感受野包括最初所有九个输入元素。
因此,当一个特征图中的任意元素需要检测更广区域的输入特征时,我们可以构建一个更深的网络。
填充、步幅综述
在前面的例子 中,输入的高度和宽度都为333,卷积核的高度和宽度都为 222 ,生成的输出表征的维数为 2×22\times22×2。
假设输入形状为 nh×nwn_h\times n_wnh×nw,卷积核形状为 kh×kwk_h\times k_wkh×kw,那么输出形状将是 (nh−kh+1)×(nw−kw+1)(n_h-k_h+1) \times (n_w-k_w+1)(nh−kh+1)×(nw−kw+1)。
因此,卷积的输出形状取决于输入形状和卷积核的形状。
还有什么因素会影响输出的大小呢?本节我们将介绍 填充(padding)和 步幅 (stride)。假设以下情景:
-
有时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于 111 所导致的。比如,一个 240×240240 \times 240240×240 像素的图像,经过 101010 层 5×55 \times 55×5 的卷积后,将减少到 200×200200 \times 200200×200 像素。如此一来,原始图像的边界丢失了许多有用信息。 而填充 是解决此问题最有效的方法。
-
有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余。 步幅则可以在这类情况下提供帮助。
填充
如上所述,在应用多层卷积时,我们常常丢失边缘像素。
由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。
但随着我们应用许多连续卷积层,累积丢失的像素数就多了。
解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是 000 )。
我们将 3×33 \times 33×3 输入填充到 5×55 \times 55×5,那么它的输出就增加为 4×44 \times 44×4。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素:
0×0+0×1+0×2+0×3=0 0\times0+0\times1+0\times2+0\times3=0 0×0+0×1+0×2+0×3=0
通常,如果我们添加 php_hph 行填充(大约一半在顶部,一半在底部)和 pwp_wpw 列填充(左侧大约一半,右侧半),则输出形状将为
(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)
这意味着输出的高度和宽度将分别增加 php_hph 和 pwp_wpw。
在许多情况下,我们需要设置 ph=kh−1p_h=k_h-1ph=kh−1 和 pw=kw−1p_w=k_w-1pw=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⌋ 行。同理,我们填充宽度的两侧。
卷积神经网络中卷积核的高度和宽度通常为奇数,例如 1、3、5 或 7。
选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
此外,使用奇数核和填充也提供了书写上的便利。对于任何二维张量 X,当满足:
- 内核的大小是奇数;
- 所有边的填充行数和列数相同;
- 输出与输入具有相同高度和宽度
则可以得出:输出Y[i, j]是通过以输入X[i, j]为中心,与卷积核进行互相关计算。
步幅
在计算互相关时,卷积窗口从输入张量的左上角开始,向下和向右滑动。
在前面的例子中,我们默认每次滑动一个元素。
但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
我们将每次滑动元素的数量称为 步幅 (stride)。到目前为止,我们只使用过高度或宽度为 111 的步幅,那么如何使用较大的步幅呢?
通常,当垂直步幅为 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)。
通道综述
多输入通道
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数目的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为 cic_ici,那么卷积核的输入通道数也需要为 cic_ici 。如果卷积核的窗口形状是 kh×kwk_h\times k_wkh×kw,那么当 ci=1c_i=1ci=1 时,我们可以把卷积核看作形状为 kh×kwk_h\times k_wkh×kw 的二维张量。
然而,当 ci>1c_i>1ci>1 时,我们卷积核的每个输入通道将包含形状为 kh×kwk_h\times k_wkh×kw 的张量。将这些张量 cic_ici 连结在一起可以得到形状为 ci×kh×kwc_i\times k_h\times k_wci×kh×kw 的卷积核。由于输入和卷积核都有 cic_ici 个通道,我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将 cic_ici 的结果相加)得到二维张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。
多输出通道
到目前为止,不论有多少输入通道,我们还只有一个输出通道。然而,每一层有多个输出通道是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,我们可以将每个通道看作是对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。
用 cic_ici 和 coc_oco 分别表示输入和输出通道的数目,并让 khk_hkh 和 kwk_wkw 为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为 ci×kh×kwc_i\times k_h\times k_wci×kh×kw 的卷积核张量,这样卷积核的形状是 co×ci×kh×kwc_o\times c_i\times k_h\times k_wco×ci×kh×kw。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。
1×11\times 11×1 卷积
1×11 \times 11×1 卷积,即 kh=kw=1k_h = k_w = 1kh=kw=1,看起来似乎没有多大意义。毕竟,卷积的本质是有效提取相邻像素间的相关特征,而 1×11 \times 11×1 卷积显然没有此作用。尽管如此,1×11 \times 11×1 仍然十分流行,时常包含在复杂深层网络的设计中。下面,让我们详细地解读一下它的实际作用。
因为使用了最小窗口,1×11\times 11×1 卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。而 1×11\times 11×1 卷积的唯一计算发生在通道上。这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。
我们可以将 1×11\times 11×1 卷积层看作是在每个像素位置应用的全连接层,以 cic_ici 个输入值转换为 coc_oco 个输出值。因为这仍然是一个卷积层,所以跨像素的权重是一致的。同时,1×11\times 11×1 卷积层需要的权重维度为 co×cic_o\times c_ico×ci ,再额外加上一个偏置。
池化层综述
通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率,聚集信息,这样的随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”), 所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
例如,如果我们拍摄黑白之间轮廓清晰的图像 X,并将整个图像向右移动一个像素,即 Z[i, j] = X[i, j + 1],则新图像 Z 的输出可能大不相同。而在现实中,随着拍摄角度的移动,任何物体几乎不可能发生在同一像素上。即使用三脚架拍摄一个静止的物体,由于快门的移动而引起的相机振动,可能会使所有物体左右移动一个像素(除了高端相机配备了特殊功能来解决这个问题)。
本节将尝试引入池化 (pooling) 的概念解决相关问题。
最大池化层和平均池化层
与卷积层类似,池化层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为 池化窗口)遍历的每个位置计算一个输出。
然而,不同于卷积层中的输入与卷积核之间的互相关计算,池化层不包含参数。
相反,池运算符是确定性的,我们通常计算池化窗口中所有元素的最大值或平均值。这些操作分别称为 最大池化层 (maximum pooling)和 平均池化层(average pooling)。
在这两种情况下,与互相关运算符一样,池化窗口从输入张量的左上角开始,从左到右、从上到下的在输入张量内滑动。在池化窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值,具体取决于是使用了最大池化层还是平均池化层。
max(0,1,3,4)=4,max(1,2,4,5)=5,max(3,4,6,7)=7,max(4,5,7,8)=8. \max(0, 1, 3, 4)=4,\\ \max(1, 2, 4, 5)=5,\\ \max(3, 4, 6, 7)=7,\\ \max(4, 5, 7, 8)=8.\\ max(0,1,3,4)=4,max(1,2,4,5)=5,max(3,4,6,7)=7,max(4,5,7,8)=8.
池化窗口形状为 p×qp \times qp×q 的池化层称为 p×qp \times qp×q 池化层,池化操作称为 p×qp \times qp×q 池化。
设置卷积层输入为 X,池化层输出为 Y。
无论 X[i, j] 和 X[i, j + 1] 的值是否不同,或 X[i, j + 1] 和 X[i, j + 2] 的值是否不同,池化层始终输出 Y[i, j] = 1。
也就是说,使用 2×22\times 22×2 最大池化层,即使在高度或宽度上移动一个元素,卷积层仍然可以识别到模式。
填充和步幅
与卷积层一样,池化层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。
多个通道
在处理多通道输入数据时,池化层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。这意味着池化层的输出通道数与输入通道数相同。
LeNet 综述
在本节中,我们将介绍 LeNet
之前我们将 softmax 回归模型和多层感知机模型应用于 Fashion-MNIST 数据集中的服装图片上。
为了能够应用 softmax 回归和多层感知机,我们首先将每个大小为 28×2828\times2828×28 的图像展平为一个 784 固定长度的一维向量,然后用全连接层对其进行处理。而现在,我们已经掌握了卷积层的处理方法,我们可以在图像中保留空间结构。同时,用卷积层代替全连接层的另一个好处是:更简洁的模型所需的参数更少。
总体来看,(LeNet(LeNet-5)由两个部分组成:
- 卷积编码器:由两个卷积层组成;
- 全连接层密集块:由三个全连接层组成。
每个卷积块中的基本单元是一个卷积层、一个 sigmoid 激活函数和平均池化层。请注意,虽然 ReLU 和最大池化层更有效,但它们在20世纪90年代还没有出现。每个卷积层使用 5×55\times 55×5 卷积核,这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有 6 个输出通道,而第二个卷积层有 16 个输出通道。每个 2×22\times22×2 池操作(步骤2)通过空间下采样将维数减少 4 倍。卷积的输出形状由批量大小、通道数、高度、宽度决定。
为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。换言之,我们将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。LeNet 的稠密块有三个全连接层,分别有 120、84 和 10 个输出。因为我们仍在执行分类,所以输出层的 10 维对应于最后输出结果的数量。
二、实验要求
基于任意一种深度学习框架,对相关理论进行代码实现。
三、实验步骤
图像卷积
我们在 corr2d 函数中实现如上过程,该函数接受输入张量 X 和卷积核张量 K ,并返回输出张量 Y 。
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): #@save
"""计算二维互相关运算。"""
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 和卷积核张量 K ,我们来验证上述二维互相关运算的输出。
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)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XlJLx0jv-1636467159486)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\1.jpg)]
卷积层
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。
所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。
就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重
基于上面定义的 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
高度和宽度分别为 hhh 和 www的卷积核可以被称为 h×wh \times wh×w 卷积或 h×wh \times wh×w 卷积核。
我们也将带有 h×wh \times wh×w 卷积核的卷积层称为 h×wh \times wh×w 卷积层
图像中目标的边缘检测
如下是卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。
首先,我们构造一个 6×86\times 86×8 像素的黑白图像。中间四列为黑色(000),其余像素为白色(111)。
X = torch.ones((6, 8))
X[:, 2:6] = 0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HwrRrH25-1636467159490)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\2.jpg)]
接下来,我们构造一个高度为 111 、宽度为 222 的卷积核 K 。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。
K = torch.tensor([[1.0, -1.0]])
现在,我们对参数 X (输入)和 K (卷积核)执行互相关运算。
如下所示,[输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘],其他情况的输出为 000
Y = corr2d(X, K)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5I3RAFzz-1636467159492)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\3.jpg)]
现在我们将输入的二维图像转置,再进行如上的互相关运算。
其输出如下,之前检测到的垂直边缘消失了。
不出所料,这个[卷积核K只可以检测垂直边缘],无法检测水平边缘。
corr2d(X.t(), K)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PjnBuPwM-1636467159493)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\3.jpg)]
学习卷积核
如果我们只需寻找黑白边缘,那么以上 [1, -1] 的边缘检测器足以。然而,当有了更复杂数值的卷积核,或者连续的卷积层时,我们不可能手动设计过滤器。那么我们是否可以学习由X生成Y的卷积核呢?
现在让我们看看是否可以通过仅查看“输入-输出”对来了解由 X 生成 Y 的卷积核。
我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较 Y 与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,我们在此使用内置的二维卷积层,并忽略偏置。
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y)**2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'batch {i+1}, loss {l.sum():.3f}')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NzE1c4rJ-1636467159495)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\5.jpg)]
在 101010 次迭代之后,误差已经降到足够低。现在我们来看看我们所学的卷积核的权重张量。
conv2d.weight.data.reshape((1, 2))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PM7xxVMr-1636467159496)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\6.jpg)]
细心的你一定会发现,我们学习到的卷积核权重非常接近我们之前定义的卷积核 K 。
填充和步幅
填充
比如,在下面的例子中,我们创建一个高度和宽度为3的二维卷积层,并(在所有侧边填充1个像素)。给定高度和宽度为 888 的输入,则输出的高度和宽度也是 888。
import torch
from torch import nn
# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
# 这里的(1,1)表示批量大小和通道数都是1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 省略前两个维度:批量大小和通道
return Y.reshape(Y.shape[2:])
# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cx4tKbdo-1636467159496)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\7.jpg)]
当卷积内核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YZzHE3gj-1636467159497)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\8.jpg)]
步幅
下面,我们将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半。
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jeeP1Nfy-1636467159498)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\9.jpg)]
接下来,看一个稍微复杂的例子。
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WyMabCTJ-1636467159498)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\10.jpg)]洁起见,当输入高度和宽度两侧的填充数量分别为 php_hph 和 pwp_wpw 时,我们称之为填充 (ph,pw)(p_h, p_w)(ph,pw)。当 ph=pw=pp_h = p_w = pph=pw=p 时,填充是 ppp。同理,当高度和宽度上的步幅分别为 shs_hsh 和 sws_wsw 时,我们称之为步幅 (sh,sw)(s_h, s_w)(sh,sw)。当时的步幅为 sh=sw=ss_h = s_w = ssh=sw=s 时,步幅为 sss。默认情况下,填充为 000,步幅为 111。在实践中,我们很少使用不一致的步幅或填充,也就是说,我们通常有 ph=pwp_h = p_wph=pw 和 sh=sws_h = s_wsh=sw。
多输入多输出通道
目前为止,我们仅展示了单个输入和单个输出通道的简化例子,这使得我们可以将输入、卷积核和输出看作二维张量。
当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有 3×h×w3\times h\times w3×h×w 的形状。我们将这个大小为 333 的轴称为 通道(channel) 维度。在本节中,我们将更深入地研究具有多输入和多输出通道的卷积核。
多输入通道
为了加深理解,我们将实现一下多输入通道互相关运算。
简而言之,我们所做的就是对每个通道执行互相关操作,然后将结果相加。
import torch
from d2l import torch as d2l
def corr2d_multi_in(X, K):
# 先遍历 “X” 和 “K” 的第0个维度(通道维度),再把它们加在一起
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)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1jsX5Km-1636467159499)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\11.jpg)]
多输出通道
如下所示,我们实现一个计算多个通道的输出的互相关函数。
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cmybl1et-1636467159502)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\12.jpg)]
通过将核张量 K 与 K+1 ( K 中每个元素加 111 )和 K+2 连接起来,构造了一个具有333个输出通道的卷积核。
K = torch.stack((K, K + 1, K + 2), 0)
K.shape
下面,我们对输入张量 X 与卷积核张量 K 执行互相关运算。现在的输出包含 333 个通道,第一个通道的结果与先前输入张量 X 和多输入单输出通道的结果一致。
corr2d_multi_in_out(X, K)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sKfBD4LD-1636467159503)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\13.jpg)]
1×11\times 11×1 卷积层
下面,我们使用全连接层实现 1×11 \times 11×1 卷积。
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
# 全连接层中的矩阵乘法
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
当执行 1×11\times 11×1 卷积运算时,上述函数相当于先前实现的互相关函数corr2d_multi_in_out。让我们用一些样本数据来验证这一点。
X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
池化层
最大池化层和平均池化层
在下面的代码中的 pool2d 函数,我们实现池化层的正向传播
import torch
from torch import nn
from d2l import torch as d2l
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i:i + p_h, j:j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i:i + p_h, j:j + p_w].mean()
return Y
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1sOLleCu-1636467159505)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\14.jpg)]
此外,我们还可以验证平均池化层。
pool2d(X, (2, 2), 'avg')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4IYZGOAm-1636467159506)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\15.jpg)]
填充和步幅
下面,我们用深度学习框架中内置的二维最大池化层,来演示池化层中填充和步幅的使用。
我们首先构造了一个输入张量 X,它有四个维度,其中样本数和通道数都是 1。
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RPbNkQKx-1636467159507)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\16.jpg)]
默认情况下,深度学习框架中的步幅与池化窗口的大小相同*。因此,如果我们使用形状为 (3, 3) 的池化窗口,那么默认情况下,我们得到的步幅形状为 (3, 3)。
pool2d = nn.MaxPool2d(3)
pool2d(X)
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kjx1CkjH-1636467159508)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\17.jpg)]
当然,我们可以设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度。
pool2d = nn.MaxPool2d((2, 3), padding=(1, 1), stride=(2, 3))
pool2d(X)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ibN50LZt-1636467159509)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\18.jpg)]
多个通道
下面,我们将在通道维度上连结张量 X 和 X + 1,以构建具有 2 个通道的输入。
X = torch.cat((X, X + 1), 1)
如下所示,池化后输出通道的数量仍然是 2。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpYitq2m-1636467159510)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\19.jpg)]
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fviJZ1NY-1636467159511)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\20.jpg)]
LeNet
LeNet 构建
通过下面的 LeNet 代码。我们只需要实例化一个 Sequential 块并将需要的层连接在一起,就能实现 LeNet 的主要功能。
import torch
from d2l import torch as d2l
from torch import nn
class Reshape(torch.nn.Module):
def forward(self, x):
return x.view(-1, 1, 28, 28)
net = torch.nn.Sequential(Reshape(), nn.Conv2d(1, 6, kernel_size=5,
padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(), nn.Linear(84, 10))
我们对原始模型做了一点小改动,去掉了最后一层的高斯激活。除此之外,这个网络与最初的 LeNet-5 一致。
下面,我们将一个大小为 28×2828 \times 2828×28 的单通道(黑白)图像通过 LeNet。 通过在每一层打印输出的形状,我们可以检查模型。
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape: \t', X.shape)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2WNMzCRI-1636467159512)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\21.jpg)]
在整个卷积块中,与上一层相比,每一层特征的高度和宽度都减小了。第一个卷积层使用 2 个像素的填充,来补偿 5×55 \times 55×5 卷积核导致的特征减少。第二个卷积层没有填充,因此高度和宽度都减少了 4 个像素。
随着层叠的上升,通道的数量从输入时的 1 个,增加到第一个卷积层之后的 6 个,再到第二个卷积层之后的 16 个。同时,每个池化层的高度和宽度都减半。最后,每个全连接层减少维数,最终输出一个维数与结果分类数相匹配的输出。
模型训练
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度。"""
if isinstance(net, torch.nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""Train a model with a GPU (defined in Chapter 6)."""
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,范例数
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
运行与评估
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W1y305qC-1636467159512)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\24.jpg)]
四、实验结论
- 二维卷积层的核心计算是二维互相关运算。最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置。我们可以从数据中学习卷积核的参数。当需要检测输入特征中更广区域时,我们可以构建一个更深的卷积网络;
- 填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽;步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 1/n1/n1/n( nnn 是一个大于 111 的整数);填充和步幅可用于有效地调整数据的维度;
- 多输入多输出通道可以用来扩展卷积层的模型,当以每像素为基础应用时,1×11\times 11×1 卷积层相当于全连接层,1×11\times 11×1 卷积层通常用于调整网络层的通道数量和控制模型复杂性;
- 卷积神经网络(CNN)是一类使用卷积层的网络,我们组合使用卷积层、非线性激活函数和池化层构建它。为了构造高性能的卷积神经网络,我们通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数。在传统的卷积神经网络中,卷积块编码得到的表征在输出之前需由一个或多个全连接层进行处理。
五、实验拓展
-
将平均池化层替换为最⼤池化层,会发⽣什么?
修改代码如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YyHoEA6R-1636467159513)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\23.jpg)]
训练结果如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-soqYHSSL-1636467159514)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\23.jpg)]
-
尝试构建⼀个基于LeNet 的更复杂的⽹络,以提⾼其准确性。
- 调整卷积窗口⼤小。
- 调整输出通道的数量。
- 调整激活函数(如ReLU)。
- 调整卷积层的数量。
- 调整全连接层的数量。
- 调整学习率和其他训练细节(例如,初始化和周期数)。
-
在MNIST 数据集上尝试以上改进的⽹络。
可以看到在调整后学习效果有非常显著的提升
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yU7hCTii-1636467159514)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验3\22.jpg)]
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)