Reference:
文章跳转:
在前面的章节中,我们遇到过图像数据。这种数据的每个样本都由一个二维像素网格组成,每个像素可能是一个或者多个数值,取决于是黑白还是彩色图像。到目前为止,我们处理这类结构丰富的数据的方式还不够有效。我们仅仅通过将图像数据展平成一维向量而忽略了每个图像的空间结构信息,再将数据送入一个全连接的多层感知机中。因为这些网络特征元素的顺序是不变的,因此最优的结果是利用先验知识,即利用相近像素之间的相互关联性,从图像数据中学习得到有效的模型。
本章介绍的卷积神经网络(convolutional neural network,CNN)
是一类强大的、为处理图像数据而设计的神经网络。基于卷积神经网络架构的模型在计算机视觉领域中已经占主导地位,当今几乎所有的图像识别、目标检测或语义分割相关的学术竞赛和商业应用都以这种方法为基础。
现代卷积神经网络的设计得益于生物学、群论和一系列的补充实验。卷积神经网络需要的参数少于全连接架构的网络,而且卷积也很容易用GPU并行计算。因此卷积神经网络除了能够高效地采样从而获得精确的模型,还能够高效地计算。久而久之,从业人员越来越多地使用卷积神经网络。即使在通常使用循环神经网络的一维序列结构任务上(例如音频、文本和时间序列分析),卷积神经网络也越来越受欢迎。通过对卷积神经网络一些巧妙的调整,也使它们在图结构数据和推荐系统中发挥作用。
在本章的开始,我们将介绍构成所有卷积网络主干的基本元素。这包括卷积层本身、填充(padding)
和步幅(stride)
的基本细节、用于在相邻区域汇聚信息的汇聚层(pooling)
、在每一层中多通道(channel)
的使用,以及有关现代卷积网络架构的仔细讨论。在本章的最后,我们将介绍一个完整的、可运行的LeNet模型:这是第一个成功应用的卷积神经网络,比现代深度学习兴起时间还要早。在下一章中,我们将深入研究一些流行的、相对较新的卷积神经网络架构的完整实现,这些网络架构涵盖了现代从业者通常使用的大多数经典技术。
我们之前讨论的多层感知机十分适合处理表格数据,其中行对应样本,列对应特征。对于表格数据,我们寻找的模式可能涉及特征之间的交互,但是我们不能预先假设任何与特征交互相关的先验结构。此时,多层感知机可能是最好的选择,然而对于高维感知数据,这种缺少结构的网络可能会变得不实用。
例如,在之前猫狗分类的例子中:假设我们有一个足够充分的照片数据集,数据集中是拥有标注的照片,每张照片具有百万级像素,这意味着网络的每次输入都有一百万个维度。即使将隐藏层维度降低到1000,这个全连接层也将有 1 0 6 × 1 0 3 = 1 0 9 10^6\times 10^3=10^9 106×103=109 个参数。想要训练这个模型将不可实现,因为需要有大量的GPU、分布式优化训练的经验和超乎常人的耐心。
有些读者可能会反对这个观点,认为要求百万像素的分辨率可能不是必要的。然而,即使分辨率减小为十万像素,使用1000个隐藏单元的隐藏层也可能不足以学习到良好的图像特征,在真实的系统中我们仍然需要数十亿个参数。此外,拟合如此多的参数还需要收集大量的数据。然而,如今人类和机器都能很好地区分猫和狗:这是因为图像中本就拥有丰富的结构,而这些结构可以被人类和机器学习模型使用。卷积神经网络(convolutional neural networks,CNN)
是机器学习利用自然图像中一些已知结构的创造性方法。
想象一下,假设你想从一张图片中找到某个物体。合理的假设是:无论哪种方法找到这个物体,都应该和物体的位置无关。 理想情况下,我们的系统应该能够利用常识:猪通常不在天上飞,飞机通常不在水里游泳。但是,如果一只猪出现在图片顶部,我们还是应该认出它。我们可以从下图中儿童游戏”沃尔多在哪里”中得到灵感:在这个游戏中包含了许多充斥着活动的混乱场景,而沃尔多通常潜伏在一些不太可能的位置,读者的目标就是找出他。尽管沃尔多的装扮很有特点,但是在眼花缭乱的场景中找到他也如大海捞针。然而沃尔多的样子并不取决于他潜藏的地方,因此我们可以使用一个“沃尔多检测器”扫描图像。该检测器将图像分割成多个区域,并为每个区域包含沃尔多的可能性打分。卷积神经网络正是将空间不变性(spatial invariance)
的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。
现在,我们将上述想法总结一下,从而帮助我们设计适合于计算机视觉的神经网络架构:
平移不变性(translation invariance)
:不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。局部性(locality)
:神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。让我们看看这些原则是如何转化为数学表示的。
首先,多层感知机的输入是二维图像 X \mathbf{X} X,其隐藏表示 H \mathbf{H} H 在数学上是一个矩阵,在代码中表示为二维张量。其中 X \mathbf{X} X 和 H \mathbf{H} H 具有相同的形状。为了方便理解,我们可以认为,无论是输入还是隐藏表示都拥有空间结构。
使用 [ X ] i , j [\mathbf{X}]_{i, j} [X]i,j 和 [ H ] i , j [\mathbf{H}]_{i, j} [H]i,j 分别表示输入图像和隐藏表示中位置 ( i , j ) (i, j) (i,j) 处的像素。为了使每个隐藏神经元都能接收到每个输入像素的信息,我们将参数从权重矩阵(如同我们先前在多层感知机中所做的那样)替换为四阶权重张量 W \mathbf{W} W。假设 U \mathbf{U} U 包含偏置参数,我们可以将全连接层形式化地表示为:
[ H ] i , j = [ U ] i , j + ∑ k ∑ l [ W ] i , j , k , l [ X ] k , l = [ U ] i , j + ∑ a ∑ b [ V ] i , j , a , b [ X ] i + a , j + b \begin{aligned} {[\mathbf{H}]_{i, j} } &=[\mathbf{U}]_{i, j}+\sum_k \sum_l[\mathbf{W}]_{i, j, k, l}[\mathbf{X}]_{k, l} \\ &=[\mathbf{U}]_{i, j}+\sum_a \sum_b[\mathbf{V}]_{i, j, a, b}[\mathbf{X}]_{i+a, j+b} \end{aligned} [H]i,j=[U]i,j+k∑l∑[W]i,j,k,l[X]k,l=[U]i,j+a∑b∑[V]i,j,a,b[X]i+a,j+b
其中,从 W \mathbf{W} W 到 V \mathbf{V} V 的转换只是形式上的转换,因为在这两个四阶张量的元素之间存在一一对应的关系。我们只需重新索引下标 ( k , l ) (k, l) (k,l),使 k = i + a k=i+a k=i+a 、 l = j + b l=j+b l=j+b,由此可得 [ V ] i , j , a , b = [ W ] i , j , i + a , j + b [\mathrm{V}]_{i, j, a, b}=[\mathrm{W}]_{i, j, i+a, j+b } [V]i,j,a,b=[W]i,j,i+a,j+b。索引 a a a 和 b b b 通过在正偏移和负偏移之间移动覆盖了整个图像。对于隐藏表示中任意给定位置 ( i , j ) (i, j) (i,j) 处的像素值 [ H ] i , j [\mathbf{H}]_{i, j} [H]i,j,可以通过在 x x x 中以 ( i , j ) (i, j) (i,j) 为中心对像素进行加权求和得到,加权使用的权重为 [ V ] i , j , a , b [\mathrm{V}]_{i, j, a, b } [V]i,j,a,b。
现在引用上述的第一个原则:平移不变性。这意味着检测对象在输入 X \mathbf{X} X 中的平移,应该仅导致隐藏表示 H \mathbf{H} H 中的平移。也就是说, V \mathbf{V} V 和 U \mathbf{U} U 实际上不依赖于 ( i , j ) (i, j) (i,j) 的值,即 [ V ] i , j , a , b = [ V ] a , b [\mathrm{V}]_{i, j, a, b}=[\mathbf{V}]_{a, b} [V]i,j,a,b=[V]a,b 。并且 U \mathbf{U} U 是一个常数,比如 u u u。因此,我们可以简化 H \mathbf{H} H 定义为:
[ H ] i , j = u + ∑ a ∑ b [ V ] a , b [ X ] i + a , j + b . [\mathbf{H}]_{i, j}=u+\sum_a \sum_b[\mathbf{V}]_{a, b}[\mathbf{X}]_{i+a, j+b} . [H]i,j=u+a∑b∑[V]a,b[X]i+a,j+b.
这就是卷积 (convolution)
。我们是在使用系数 [ V ] a , b [\mathbf{V}]_{a, b} [V]a,b 对位置 ( i , j ) (i, j) (i,j) 附近的像素 ( i + a , j + b ) (i+a, j+b) (i+a,j+b) 进行加权得到 [ H ] i , j [\mathbf{H}]_{i, j} [H]i,j 。注意, [ V ] a , b [\mathbf{V}]_{a, b} [V]a,b 的系数比 [ V ] i , j , a , b [\mathrm{V}]_{i, j, a, b} [V]i,j,a,b 少很多,因为前者不再依赖于图像中的位置。这就是显著的进步!
现在引用上述的第二个原则:局部性。如上所述,为了收集用来训练参数 [ H ] i , j [\mathbf{H}]_{i, j} [H]i,j 的相关信息,我们不应偏离到距 ( i , j ) (i, j) (i,j) 很远的地方。这意味着在 ∣ a ∣ > Δ |a|>\Delta ∣a∣>Δ 或 ∣ b ∣ > Δ |b|>\Delta ∣b∣>Δ 的范围之外,我们可以设置 [ V ] a , b = 0 [\mathbf{V}]_{a, b}=0 [V]a,b=0 。因此,我们可以将 [ H ] i , j [\mathbf{H}]_{i, j} [H]i,j 重写为
[ H ] i , j = u + ∑ a = − Δ Δ ∑ b = − Δ Δ [ V ] a , b [ X ] i + a , j + b . [\mathbf{H}]_{i, j}=u+\sum_{a=-\Delta}^{\Delta} \sum_{b=-\Delta}^{\Delta}[\mathbf{V}]_{a, b}[\mathbf{X}]_{i+a, j+b} . [H]i,j=u+a=−Δ∑Δb=−Δ∑Δ[V]a,b[X]i+a,j+b.
简而言之,上式是一个卷积层 (convolutional layer)
,而卷积神经网络是包含卷积层的一类特殊的神经网络。在深度学习研究社区中, V \mathbf{V} V 被称为卷积核 (convolution kernel)
或者滤波器(filter)
,亦或简单地称之为该卷积层的权重
,通常该权重是可学习的参数。当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大的:以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而现在卷积神经网络通常只需要几百个参数,而且不需要改变输入或隐藏表示的维数。参数大幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每一层只包含局部的信息。以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到末知数据中。但如果这偏置与现实不符时,比如当图像不满足平移不变时,我们的模型可能难以拟合我们的训练数据。
在进一步讨论之前,我们先简要回顾一下为什么上面的操作被称为卷积
。在数学中,两个函数(比如 f , g : R d → R ) \left.f, g: \mathbb{R}^d \rightarrow \mathbb{R}\right) f,g:Rd→R) 之间的“卷积”被定义为:
( f ∗ g ) ( x ) = ∫ f ( z ) g ( x − z ) d z . (f * g)(\mathbf{x})=\int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d \mathbf{z} . (f∗g)(x)=∫f(z)g(x−z)dz.
也就是说,卷积是当把一个函数 "翻转"并移位 x \mathbf{x} x 时,测量 f f f 和 g g g 之间的重叠。当为离散对象时,积分就变成求和。例如:对于由索引为 Z \mathbb{Z} Z 的、平方可和的、无限维向量集合中抽取的向量,我们得到以下定义:
( f ∗ g ) ( i ) = ∑ a f ( a ) g ( i − a ) . (f * g)(i)=\sum_a f(a) g(i-a) . (f∗g)(i)=a∑f(a)g(i−a).
对于二维张量,则为 f f f 的索引 ( a , b ) (a, b) (a,b) 和 g g g 的索引 ( i − a , j − b ) (i-a, j-b) (i−a,j−b) 上的对应加和:
( f ∗ g ) ( i , j ) = ∑ a ∑ b f ( a , b ) g ( i − a , j − b ) . (f * g)(i, j)=\sum_a \sum_b f(a, b) g(i-a, j-b) . (f∗g)(i,j)=a∑b∑f(a,b)g(i−a,j−b).
这看起来类似于 (6.1.3),但有一个主要区别:这里不是使用 ( i + a , j + b ) (i+a, j+b) (i+a,j+b),而是使用差值。然而,这种区别是表面的,因为我们总是可以匹配 (6.1.3)和 (6.1.6)之间的符号。我们在 (6.1.3)中的 原始定义更正确地描述了互相关 (cross-correlation)
,这个问题将在下一节中讨论。
回到上面的“沃尔多在哪里”游戏,让我们看看它到底是什么样子。卷积层根据滤波器 V \mathbf{V} V 选取给定大小的窗口,并加权处理图片,如下图中所示。我们的目标是学习一个模型,以便探测出在“沃尔多”最可能出现的地方。
然而这种方法有一个问题:我们忽略了图像一般包含三个通道/三种原色(红色、绿色和蓝色)。实际上,图像不是二维张量,而是一个由高度、宽度和颜色组成的三维张量,比如包含 1024 × 1024 × 3 1024 \times 1024 \times 3 1024×1024×3 个像素。前两个轴与像素的空间位置有关,而第三个轴可以看作是每个像素的多维表示。因此,我们将X索引为 [ X ] i , j , k [\mathbf{X}]_{i, j, k} [X]i,j,k 。由此卷积相应地调整为 [ V ] a , b , c [\mathbf{V}]_{a, b, c} [V]a,b,c,而不是 [ V ] a , b [\mathbf{V}]_{a, b} [V]a,b。
此外,由于输入图像是三维的,我们的隐藏表示H也最好采用三维张量。换句话说,对于每一个空间位置,我们想要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。因此,我们可以把隐藏表示想象为一系列具有二维张量的通道(channel)
。这些通道有时也被称为特征映射(feature maps)
,因为每个通道都向后续层提供一组空间化的学习特征。直观上你可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。
为了支持输入 X \mathrm{X} X 和隐藏表示 H \mathrm{H} H 中的多个通道,我们可以在 V \mathrm{V} V 中添加第四个坐标,即 [ V ] a , b , c , d [\mathrm{V}]_{a, b, c, d} [V]a,b,c,d。综上所述,
[ H ] i , j , d = ∑ a = − Δ Δ ∑ b = − Δ Δ ∑ c [ V ] a , b , c , d [ X ] i + a , j + b , c [\mathrm{H}]_{i, j, d}=\sum_{a=-\Delta}^{\Delta} \sum_{b=-\Delta}^{\Delta} \sum_c[\mathrm{V}]_{a, b, c, d}[\mathrm{X}]_{i+a, j+b, c} [H]i,j,d=a=−Δ∑Δb=−Δ∑Δc∑[V]a,b,c,d[X]i+a,j+b,c
其中隐藏表示 H \mathrm{H} H 的索引 d d d 表示输出通道,而随后的输出将继续以三维张量 H \mathrm{H} H 作为输入进入下一个卷积层。所以,上式可以定义具有多个通道的卷积层,而其中 V \mathrm{V} V 是该卷积层的权重。
然而,仍有许多问题凾待解决。例如,图像中是否到处都有存在沃尔多的可能? 如何有效地计算输出层? 如何选择适当的激活函数?为了训练有效的网络,如何做出合理的网络设计选择? 我们将在 本章的其它部分讨论这些问题。
卷积神经网络(CNN)
是一类特殊的神经网络,它可以包含多个卷积层。上节我们解析了卷积层的原理,现在我们看看它的实际应用。由于卷积神经网络的设计是用于探索图像数据,本节我们将以图像为例。
严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation)
,而不是卷积运算。根据上一节中的描述,在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
首先,我们暂时忽略通道(第三维)这一情况,看看如何处理二维图像数据和隐藏表示。在下图中,输入是高度为 3 3 3、宽度为 3 3 3的二维张量(即形状为 3 × 3 3\times 3 3×3)。卷积核的高度和宽度都是 2 2 2,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即 2 × 2 2\times 2 2×2)。
在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。 在如上例子中,输出张量的四个元素由二维互相关运算得到,这个输出高度为 2 2 2、宽度为 2 2 2,如下所示:
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 \begin{array}{l} 0 \times 0+1 \times 1+3 \times 2+4 \times 3=19 \\ 1 \times 0+2 \times 1+4 \times 2+5 \times 3=25 \\ 3 \times 0+4 \times 1+6 \times 2+7 \times 3=37 \\ 4 \times 0+5 \times 1+7 \times 2+8 \times 3=43 \end{array} 0×0+1×1+3×2+4×3=191×0+2×1+4×2+5×3=253×0+4×1+6×2+7×3=374×0+5×1+7×2+8×3=43
注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于 1,而卷积核只与图像中每个大小完全适合的位置进行互相关运算。所以,输出大小等于输入大小 n h × n w n_h \times n_w nh×nw 减去卷积核大小 k h × k w k_h \times k_w kh×kw, 即:
( n h − k h + 1 ) × ( n w − k w + 1 ) . \left(n_h-k_h+1\right) \times\left(n_w-k_w+1\right) . (nh−kh+1)×(nw−kw+1).
这是因为我们需要足够的空间在图像上“移动”卷积核。稍后,我们将看到如何通过在图像边界周围填充零来保证有足够的空间移动卷积核,从而保持输出大小不变。接下来,我们在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)
tensor([[19., 25.],
[37., 43.]])
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。
基于上面定义的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 h h和 w w w的卷积核可以被称为 h × w h\times w h×w卷积或 h × w h\times w h×w卷积核。 我们也将带有 h × w h\times w h×w卷积核的卷积层称为 h × w h\times w h×w卷积层。
如下是卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。首先,我们构造一个 6 × 8 6\times 8 6×8像素的黑白图像。中间四列为黑色( 0 0 0),其余像素为白色( 1 1 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 1 1、宽度为 2 2 2的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。
K = torch.tensor([[1.0, -1.0]])
现在,我们对参数X(输入)和K(卷积核)执行互相关运算。如下所示,输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为 0 0 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, -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))
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 4.735
epoch 4, loss 0.874
epoch 6, loss 0.179
epoch 8, loss 0.043
epoch 10, loss 0.013
在 10 10 10次迭代之后,误差已经降到足够低。现在我们来看看我们所学的卷积核的权重张量。
conv2d.weight.data.reshape((1, 2))
tensor([[ 0.9990, -0.9791]])
细心的你一定会发现,我们学习到的卷积核权重非常接近我们之前定义的卷积核K。
回想一下我们在第一节中观察到的互相关和卷积运算之间的对应关系。为了得到正式的卷积运算输出,我们需要执行 ( f ∗ g ) ( i , j ) = ∑ a ∑ b f ( a , b ) g ( i − a , j − b ) (f * g)(i, j)=\sum_a \sum_b f(a, b) g(i-a, j-b) (f∗g)(i,j)=∑a∑bf(a,b)g(i−a,j−b) 中定义的严格卷积运算,而不是互相关运算。幸运的是,它们差别不大,我们只需水平和垂直翻转二维卷积核张量,然后对输入张量执行互相关运算。
值得注意的是,由于卷积核是从数据中学习到的,因此无论这些层执行严格的卷积运算还是互相关运算,卷积层的输出都不会受到影响。为了说明这一点,假设卷积层执行互相关运算并学习 图6.2.1中的卷积核,该卷积核在这里由矩阵 K \mathbf{K} K 表示。假设其他条件不变,当这个层执行严格的卷积时,学习的卷积核 K ′ \mathbf{K'} K′在水平和垂直翻转之后将与 K \mathbf{K} K相同。 也就是说,当卷积层对 图6.2.1中的输入和 K ′ \mathbf{K'} K′执行严格卷积运算时,将得到与互相关运算 图6.2.1中相同的输出。
为了与深度学习文献中的标准术语保持一致,我们将继续把“互相关运算”称为卷积运算,尽管严格地说,它们略有不同。此外,对于卷积核张量上的权重,我们称其为元素
。
如在 6.1.4.1节中所述,图6.2.1中输出的卷积层有时被称为特征映射(feature map)
,因为它可以被视为一个输入映射到下一层的空间维度的转换器。在卷积神经网络中,对于某一层的任意元素 x x x,其感受野(receptive field)
是指在前向传播期间可能影响 x x x 计算的所有元素(来自所有先前层)。
请注意,感受野可能大于输入的实际大小。让我们用 图6.2.1为例来解释感受野:给定 2 × 2 2\times 2 2×2 卷积核,阴影输出元素值 19 19 19的感受野是输入阴影部分的四个元素。假设之前输出为 Y \mathbf{Y} Y,其大小为 2 × 2 2\times 2 2×2,现在我们在其后附加一个卷积层,该卷积层以 Y \mathbf{Y} Y 为输入,输出单个元素 z z z。在这种情况下, Y \mathbf{Y} Y 上的 z z z 的感受野包括 Y \mathbf{Y} Y 的所有四个元素,而输入的感受野包括最初所有九个输入元素。因此,当一个特征图中的任意元素需要检测更广区域的输入特征时,我们可以构建一个更深的网络。
在前面的例子 图6.2.1中,输入的高度和宽度都为 3 3 3,卷积核的高度和宽度都为 2 2 2,生成的输出表征的维数为 2 × 2 2\times2 2×2。正如我们在上一节中所概括的那样,假设输入形状为 n h × n w n_h\times n_w nh×nw,卷积核形状为 k h × k w k_h\times k_w kh×kw,那么输出形状将是 ( n h − k h + 1 ) × ( n w − k w + 1 ) (n_h-k_h+1)\times(n_w-k_w+1) (nh−kh+1)×(nw−kw+1)。因此,卷积的输出形状取决于输入形状和卷积核的形状。
还有什么因素会影响输出的大小呢?本节我们将介绍填充(padding)
和步幅(stride)
。假设以下情景:有时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于 1 1 1 所导致的。比如,一个 240 × 240 240\times240 240×240像素的图像,经过 10 10 10层 5 × 5 5\times5 5×5的卷积后,将减少到 200 × 200 200\times200 200×200像素。如此一来,原始图像的边界丢失了许多有用信息。而填充
是解决此问题最有效的方法。有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余。步幅
则可以在这类情况下提供帮助。
如上所述,在应用多层卷积时,我们常常丢失边缘像素。由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。解决这个问题的简单方法即为填充(padding)
:在输入图像的边界填充元素(通常填充元素是 0 0 0)。例如,在下图中,我们将 3 × 3 3\times3 3×3输入填充到 5 × 5 5\times5 5×5,那么它的输出就增加为 4 × 4 4\times4 4×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。
通常,如果我们添加 p h p_h ph 行填充 (大约一半在顶部,一半在底部) 和 p w p_w pw 列填充 (左侧大约一半, 右 侧一半),则输出形状将为:
( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) 。 \left(n_h-k_h+p_h+1\right) \times\left(n_w-k_w+p_w+1\right) 。 (nh−kh+ph+1)×(nw−kw+pw+1)。
这意味着输出的高度和宽度将分别增加 p h p_h ph 和 p w p_w pw 。
在许多情况下,我们需要设置 p h = k h − 1 p_h=k_h-1 ph=kh−1 和 p w = k w − 1 p_w=k_w-1 pw=kw−1,使输入和输出具有相同的高度和宽度。这样可以在构建网络时更容易地预测每个图层的输出形状。假设 k h k_h kh 是奇数,我们将在高度的两侧填充 p h / 2 p_h / 2 ph/2 行。如果 k h k_h kh 是偶数,则一种可能性是在输入顶部填充 ⌈ p h / 2 ⌉ \left\lceil p_h / 2\right\rceil ⌈ph/2⌉ 行,在底部填充 ⌊ p h / 2 ⌋ \left\lfloor p_h / 2\right\rfloor ⌊ph/2⌋ 行。同理,我们填充宽度的两侧。
卷积神经网络中卷积核的高度和宽度通常为奇数,例如 1 1 1、 3 3 3、 5 5 5 或 7 7 7。选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
此外,使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量 X \mathrm{X} X,当满足:
比如,在下面的例子中,我们创建一个高度和宽度为 3 3 3 的二维卷积层,并在所有侧边填充 1 1 1 个像素。给定高度和宽度为 8 8 8 的输入,则输出的高度和宽度也是 8 8 8。
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
torch.Size([8, 8])
当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
torch.Size([8, 8])
在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。在前面的例子中,我们默认每次滑动一个元素。但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
我们将每次滑动元素的数量称为步幅(stride)
。到目前为止,我们只使用过高度或宽度为 1 1 1 的步幅,那么如何使用较大的步幅呢?下图是垂直步幅为 3 3 3,水平步幅为 2 2 2 的二维互相关运算。着色部分是输出元素以及用于输出计算的输入和内核张量元素: 0 × 0 + 0 × 1 + 1 × 2 + 2 × 3 = 8 0\times0+0\times1+1\times2+2\times3=8 0×0+0×1+1×2+2×3=8、 0 × 0 + 6 × 1 + 0 × 2 + 0 × 3 = 6 0\times0+6\times1+0\times2+0\times3=6 0×0+6×1+0×2+0×3=6。
可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加另一列填充)。
通常,当垂直步幅为 s h s_h sh、水平步幅为 s w s_w sw 时,输出形状为
⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / s w ⌋ . \left\lfloor\left(n_h-k_h+p_h+s_h\right) / s_h\right\rfloor \times\left\lfloor\left(n_w-k_w+p_w+s_w\right) / s_w\right\rfloor . ⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋.
如果我们设置了 p h = k h − 1 p_h=k_h-1 ph=kh−1 和 p w = k w − 1 p_w=k_w-1 pw=kw−1,则输出形状将简化为 ⌊ ( n h + s h − 1 ) / s h ⌋ × ⌊ ( n w + s w − 1 ) / s w ⌋ \left\lfloor\left(n_h+s_h-1\right) / s_h\right\rfloor \times\left\lfloor\left(n_w+s_w-1\right) / s_w\right\rfloor ⌊(nh+sh−1)/sh⌋×⌊(nw+sw−1)/sw⌋。更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 ( n h / s h ) × ( n w / s w ) \left(n_h / s_h\right) \times\left(n_w / s_w\right) (nh/sh)×(nw/sw)。
下面,我们将高度和宽度的步幅设置为 2,从而将输入的高度和宽度减半。
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
torch.Size([4, 4])
接下来,看一个稍微复杂的例子。
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
torch.Size([2, 2])
为了简洁起见,当输入高度和宽度两侧的填充数量分别为 p h p_h ph 和 p w p_w pw 时,我们称之为填充 ( p h , p w ) \left(p_h, p_w\right) (ph,pw)。当 p h = p w = p p_h=p_w=p ph=pw=p 时,填充是 p p p。同理,当高度和宽度上的步幅分别为 s h s_h sh和 s w s_w sw时,我们称之为步幅 ( s h , s w ) \left(s_h, s_w\right) (sh,sw)。特别地,当 s h = s w = s s_h=s_w=s sh=sw=s 时,我们称步幅为 s 。 s_{\text {。 }} s。 默认情况下,填充为 0,步幅为 1。在实践中,我们很少使用不一致的步幅或填充,也就是说,我们通常有 p h = p w p_h=p_w ph=pw 和 s h = s w s_h=s_w sh=sw。
虽然我们在 6.1.4.1节中描述了构成每个图像的多个通道和多层卷积层。例如彩色图像具有标准的RGB通道来代表红、绿和蓝。 但是到目前为止,我们仅展示了单个输入和单个输出通道的简化例子。 这使得我们可以将输入、卷积核和输出看作二维张量。
当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有 3 × h × w 3\times h\times w 3×h×w 的形状。我们将这个大小为 3 3 3的轴称为通道(channel)
维度。在本节中,我们将更深入地研究具有多输入和多输出通道的卷积核。
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为 c i c_i ci,那么卷积核的输入通道数也需要为 c i ∘ c_{i \circ} ci∘ 如果卷积核的窗 口形状是 k h × k w k_h \times k_w kh×kw,那么当 c i = 1 c_i=1 ci=1 时,我们可以把卷积核看作形状为 k h × k w k_h \times k_w kh×kw 的二维张量。
然而, 当 c i > 1 c_i>1 ci>1 时,我们卷积核的每个输入通道将包含形状为 k h × k w k_h \times k_w kh×kw 的张量。将这些张量 c i c_i ci 连结在一起可以得到形状为 c i × k h × k w c_i \times k_h \times k_w ci×kh×kw 的卷积核。由于输入和卷积核都有 c i c_i ci 个通道,我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将 c i c_i ci 的结果相加)得到二维张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。
在下图中,我们演示了一个具有两个输入通道的二维互相关运算的示例。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素:
( 1 × 1 + 2 × 2 + 4 × 3 + 5 × 4 ) + ( 0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 ) = 56 (1 \times 1+2 \times 2+4 \times 3+5 \times 4)+(0 \times 0+1 \times 1+3 \times 2+4 \times 3)=56 (1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56
为了加深理解,我们实现一下多输入通道互相关运算。简而言之,我们所做的就是对每个通道执行互相关操作,然后将结果相加。
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和核张量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)
tensor([[ 56., 72.],
[104., 120.]])
到目前为止,不论有多少输入通道,我们还只有一个输出通道。然而,正如我们在 6.1.4.1节中所讨论的,每一层有多个输出通道是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,我们可以将每个通道看作是对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。
用 c i c_i ci和 c o c_o co分别表示输入和输出通道的数目,并让 k h k_h kh和 k w k_w kw为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为 c i × k h × k w c_i\times k_h\times k_w ci×kh×kw的卷积核张量,这样卷积核的形状是 c o × c i × k h × k w c_o\times c_i\times k_h\times k_w co×ci×kh×kw。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。
如下所示,我们实现一个计算多个通道的输出的互相关函数。
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
通过将核张量K与K+1(K中每个元素加 1 1 1)和K+2连接起来,构造了一个具有 3 3 3个输出通道的卷积核。
K = torch.stack((K, K + 1, K + 2), 0)
K.shape
torch.Size([3, 2, 2, 2])
下面,我们对输入张量X与卷积核张量K执行互相关运算。现在的输出包含 3 3 3个通道,第一个通道的结果与先前输入张量X和多输入单输出通道的结果一致。
corr2d_multi_in_out(X, K)
tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
1 × 1 1 \times 1 1×1 卷积,即 k h = k w = 1 k_h=k_w=1 kh=kw=1,看起来似乎没有多大意义。毕竟,卷积的本质是有效提取相邻像素间的相关特征,而 1 × 1 1 \times 1 1×1 卷积显然没有此作用。尽管如此, 1 × 1 1 \times 1 1×1 仍然十分流行,经常包含在复杂深层网络的设计中。下面,让我们详细地解读一下它的实际作用。
因为使用了最小窗口, 1 × 1 1 \times 1 1×1 卷积失去了卷积层的特有能力---------在高度和宽度维度上,识别相邻元素间相互作用的能力。其实 1 × 1 1 \times 1 1×1 卷积的唯一计算发生在通道上。
下图展示了使用 1 × 1 1 \times 1 1×1 卷积核与 3 个输入通道和 2 个输出通道的互相关计算。这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。我们可以将 1 × 1 1 \times 1 1×1 卷积层看作是在每个像素位置应用的全连接层,以 c i c_i ci 个输入值转换为 c o c_o co 个输出值。因为这仍然是一个卷积层,所以跨像素的权重是一致的。同时, 1 × 1 1 \times 1 1×1 卷积层需要的权重维度为 c o × c i c_o \times c_i co×ci,再额外加上一个偏置。
下面,我们使用全连接层实现 1 × 1 1\times1 1×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 × 1 1\times1 1×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
通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。
而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”),所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
此外,当检测较底层的特征时(例如 6.2节中所讨论的边缘),我们通常希望这些特征保持某种程度上的平移不变性。例如,如果我们拍摄黑白之间轮廓清晰的图像X,并将整个图像向右移动一个像素,即Z[i, j] = X[i, j + 1],则新图像Z的输出可能大不相同。而在现实中,随着拍摄角度的移动,任何物体几乎不可能发生在同一像素上。即使用三脚架拍摄一个静止的物体,由于快门的移动而引起的相机振动,可能会使所有物体左右移动一个像素(除了高端相机配备了特殊功能来解决这个问题)。
本节将介绍汇聚/池化层(pooling)层
,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。 然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)
和平均汇聚层(average pooling)
。
在这两种情况下,与互相关运算符一样,汇聚窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。
上图中的输出张量的高度为 2 2 2,宽度为 2 2 2。这四个元素为每个汇聚窗口中的最大值:
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 × q p\times q p×q 的汇聚层称为 p × q p\times q p×q 汇聚层,汇聚操作称为 p × q p\times q p×q 汇聚。
回到本节开头提到的对象边缘检测示例,现在我们将使用卷积层的输出作为 2 × 2 2\times 2 2×2 最大汇聚的输入。 设置卷积层输入为X,汇聚层输出为Y。 无论X[i, j]和X[i, j + 1]的值相同与否,或X[i, j + 1]和X[i, j + 2]的值相同与否,汇聚层始终输出Y[i, j] = 1。 也就是说,使用 2 × 2 2\times 2 2×2 最大汇聚层,即使在高度或宽度上移动一个元素,卷积层仍然可以识别到模式。
在下面的代码中的pool2d函数,我们实现汇聚层的前向传播。这类似于 6.2节中的corr2d函数。然而,这里我们没有卷积核,输出为输入中每个区域的最大值或平均值。
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,验证二维最大汇聚层的输出。
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))
tensor([[4., 5.],
[7., 8.]])
此外,我们还可以验证平均汇聚层。
pool2d(X, (2, 2), 'avg')
tensor([[2., 3.],
[5., 6.]])
与卷积层一样,汇聚层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。 下面,我们用深度学习框架中内置的二维最大汇聚层,来演示汇聚层中填充和步幅的使用。 我们首先构造了一个输入张量X,它有四个维度,其中样本数和通道数都是1。
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X
tensor([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]]]])
默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同。因此,如果我们使用形状为(3, 3)的汇聚窗口,那么默认情况下,我们得到的步幅形状为(3, 3)。
pool2d = nn.MaxPool2d(3)
pool2d(X)
tensor([[[[10.]]]])
填充和步幅可以手动设定。
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
tensor([[[[ 5., 7.],
[13., 15.]]]])
当然,我们可以设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度。
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)
tensor([[[[ 5., 7.],
[13., 15.]]]])
在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。这意味着汇聚层的输出通道数与输入通道数相同。下面,我们将在通道维度上连结张量X和X + 1,以构建具有2个通道的输入。
X = torch.cat((X, X + 1), 1)
X
tensor([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]],
[[ 1., 2., 3., 4.],
[ 5., 6., 7., 8.],
[ 9., 10., 11., 12.],
[13., 14., 15., 16.]]]])
如下所示,汇聚后输出通道的数量仍然是2。
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
tensor([[[[ 5., 7.],
[13., 15.]],
[[ 6., 8.],
[14., 16.]]]])
通过之前几节,我们学习了构建一个完整卷积神经网络的所需组件。回想一下,之前我们将 softmax回归模型 和 多层感知机模型 应用于Fashion-MNIST数据集中的服装图片。为了能够应用softmax回归和多层感知机,我们首先将每个大小为 28 × 28 28\times 28 28×28 的图像展平为一个784维的固定长度的一维向量,然后用全连接层对其进行处理。而现在,我们已经掌握了卷积层的处理方法,我们可以在图像中保留空间结构。同时,用卷积层代替全连接层的另一个好处是:模型更简洁、所需的参数更少。
在本节中,我们将介绍LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。这个模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名),目的是识别图像 [LeCun et al., 1998]中的手写数字。当时,Yann LeCun发表了第一篇通过反向传播成功训练卷积神经网络的研究,这项工作代表了十多年来神经网络研究开发的成果。
当时,LeNet取得了与支持向量机(support vector machines)
性能相媲美的成果,成为监督学习的主流方法。LeNet被广泛用于自动取款机(ATM)机中,帮助识别处理支票的数字。时至今日,一些自动取款机仍在运行Yann LeCun和他的同事Leon Bottou在上世纪90年代写的代码呢!
总体来看,LeNet(LeNet-5)由两个部分组成:
每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均汇聚层。请注意,虽然ReLU和最大汇聚层更有效,但它们在20世纪90年代还没有出现。每个卷积层使用 5 × 5 5\times5 5×5 卷积核和一个sigmoid激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有6个输出通道,而第二个卷积层有16个输出通道。每个 2 × 2 2\times2 2×2 池操作(步幅2)通过空间下采样将维数减少4倍。卷积的输出形状由批量大小、通道数、高度、宽度决定。
为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。换言之,我们将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。LeNet的稠密块有三个全连接层,分别有120、84和10个输出。因为我们在执行分类任务,所以输出层的10维对应于最后输出结果的数量。
通过下面的LeNet代码,你会相信用深度学习框架实现此类模型非常简单。我们只需要实例化一个Sequential块并将需要的层连接在一起。
import torch
from torch import nn
from d2l import torch as d2l
net = nn.Sequential(
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 × 28 28\times 28 28×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)
Conv2d output shape: torch.Size([1, 6, 28, 28])
Sigmoid output shape: torch.Size([1, 6, 28, 28])
AvgPool2d output shape: torch.Size([1, 6, 14, 14])
Conv2d output shape: torch.Size([1, 16, 10, 10])
Sigmoid output shape: torch.Size([1, 16, 10, 10])
AvgPool2d output shape: torch.Size([1, 16, 5, 5])
Flatten output shape: torch.Size([1, 400])
Linear output shape: torch.Size([1, 120])
Sigmoid output shape: torch.Size([1, 120])
Linear output shape: torch.Size([1, 84])
Sigmoid output shape: torch.Size([1, 84])
Linear output shape: torch.Size([1, 10])
请注意,在整个卷积块中,与上一层相比,每一层特征的高度和宽度都减小了。第一个卷积层使用2个像素的填充,来补偿 5 × 5 5\times 5 5×5 卷积核导致的特征减少。相反,第二个卷积层没有填充,因此高度和宽度都减少了4个像素。随着层叠的上升,通道的数量从输入时的1个,增加到第一个卷积层之后的6个,再到第二个卷积层之后的16个。同时,每个汇聚层的高度和宽度都减半。最后,每个全连接层减少维数,最终输出一个维数与结果分类数相匹配的输出。
现在我们已经实现了LeNet,让我们看看LeNet在Fashion-MNIST数据集上的表现。
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
虽然卷积神经网络的参数较少,但与深度的多层感知机相比,它们的计算成本仍然很高,因为每个参数都参与更多的乘法。 如果你有机会使用GPU,可以用它加快训练。
为了进行评估,我们需要对 3.6节中描述的evaluate_accuracy函数进行轻微的修改。 由于完整的数据集位于内存中,因此在模型使用GPU计算数据集之前,我们需要将其复制到显存中。
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
with torch.no_grad():
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]
为了使用GPU,我们还需要一点小改动。 与 3.6节中定义的train_epoch_ch3不同,在进行正向和反向传播之前,我们需要将每一小批量数据移动到我们指定的设备(例如GPU)上。
如下所示,训练函数train_ch6也类似于 3.6节中定义的train_ch3。由于我们将实现多层神经网络,因此我们将主要使用高级API。以下训练函数假定从高级API创建的模型作为输入,并进行相应的优化。我们使用在 4.8.2.2节中介绍的Xavier随机初始化模型参数。与全连接层一样,我们使用交叉熵损失函数和小批量随机梯度下降。
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型(在第六章定义)"""
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)}')
现在,我们训练和评估LeNet-5模型。
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.468, train acc 0.824, test acc 0.806
83857.2 examples/sec on cuda:0