0. 前言
本章介绍的卷积神经网络(convolutional neural network,CNN)是一类强大的、为处理图像数据而设计的神经网络。 基于卷积神经网络架构的模型在计算机视觉领域中已经占主导地位,当今几乎所有的图像识别、目标检测或语义分割相关的学术竞赛和商业应用都以这种方法为基础。
对应实践:https://github.com/silenceZheng66/deep_learning/blob/master/d2l/0x07.ipynb
在之前的学习中,我们将图像数据(二维像素网格,对于黑白图像来说每个像素只是1个数值,而彩色图像则有多个)展平为一维向量后送入MLP中,但这种方式忽略了每个图像的空间结构信息。最优的方式是利用先验知识,即利用相近像素之间的相互关联性,从图像数据中学习得到有效的模型。
现代卷积神经网络的设计得益于生物学、群论和一系列的补充实验。 卷积神经网络需要的参数少于全连接架构的网络,而且卷积也很容易用GPU并行计算。 卷积神经网络除了能够高效地采样从而获得精确的模型,还能够高效地计算。 即使在通常使用循环神经网络的一维序列结构任务上(例如音频、文本和时间序列分析),卷积神经网络也越来越受欢迎。 通过对卷积神经网络一些巧妙的调整,也使它们在图结构数据和推荐系统中发挥作用。
本章的主要内容:
- 构成所有卷积网络主干的基本元素
- 卷积层本身
- 填充(padding)和步幅(stride)的基本细节
- 用于在相邻区域池化信息的池化层(pooling)
- 在每一层中多通道(channel)的使用
- 有关现代卷积网络架构的仔细讨论
- 一个完整的、可运行的LeNet模型:这是第一个成功应用的卷积神经网络,比现代深度学习兴起时间还要早
0.1. 结论
- 图像的平移不变性使我们以相同的方式处理局部图像,而不在乎它的位置。
- 局部性意味着计算相应的隐藏表示只需一小部分局部图像像素。
- 在图像处理中,卷积层通常比全连接层需要更少的参数,但依旧获得高效用的模型。
- 卷积神经网络(CNN)是一类特殊的神经网络,它可以包含多个卷积层。
- 多个输入和输出通道使模型在每个空间位置可以获取图像的多方面特征。
- 二维卷积层的核心计算是二维互相关运算。最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置。
- 可以设计一个卷积核来检测图像的边缘。
- 可以从数据中学习卷积核的参数。
- 学习卷积核时,无论用严格卷积运算或互相关运算,卷积层的输出不会受太大影响。
- 当需要检测输入特征中更广区域时,可以构建一个更深的卷积网络。
- 填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
- 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的$1/n$($n$是一个大于的整数)。
- 填充和步幅可用于有效地调整数据的维度。
- 多输入多输出通道可以用来扩展卷积层的模型。
- 当以每像素为基础应用时,$1 \times 1$卷积层相当于全连接层。
- $1 \times 1$卷积层通常用于调整网络层的通道数量和控制模型复杂性。
- 对于给定输入元素,最大池化层会输出该窗口内的最大值,平均池化层会输出该窗口内的平均值。
- 池化层的主要优点之一是减轻卷积层对位置的过度敏感。可以指定池化层的填充和步幅。
- 使用最大池化层以及大于1的步幅,可减少空间维度(如高度和宽度)。
- 池化层的输出通道数与输入通道数相同。
- 卷积神经网络(CNN)是一类使用卷积层的网络。
- CNN中组合使用卷积层、非线性激活函数和汇聚层。
- 为了构造高性能的卷积神经网络,通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数。
- 在传统的卷积神经网络中,卷积块编码得到的表征在输出之前需由一个或多个全连接层进行处理。
- LeNet是最早发布的卷积神经网络之一。
1. 从全连接层到卷积
多层感知机十分适合处理表格数据,其中行对应样本,列对应特征。 对于表格数据,我们寻找的模式可能涉及特征之间的交互,但是我们不能预先假设任何与特征交互相关的先验结构。 此时,多层感知机可能是最好的选择,然而对于高维感知数据,这种缺少结构的网络可能会变得不实用。
例如,在之前猫狗分类的例子中:假设我们有一个足够充分的照片数据集,数据集中是拥有标注的照片,每张照片具有百万级像素,这意味着网络的每次输入都有一百万个维度。 即使将隐藏层维度降低到1000,这个全连接层也将有$10^6 \times 10^3 = 10^9$个参数。这难以训练且需要大量样本进行拟合。
如今人类和机器都能很好地区分猫和狗,是因为图像中本就拥有丰富的结构,而这些结构可以被人类和机器学习模型使用。 卷积神经网络(convolutional neural networks,CNN)是机器学习利用自然图像中一些已知结构的创造性方法。
1.1. 不变性
假设想从一张图片中找到某个物体。 合理的假设是:无论哪种方法找到这个物体,都应该和物体的位置无关。 理想情况下的系统应该能够利用常识:猪通常不在天上飞,飞机通常不在水里游泳。 但是,如果一只猪出现在图片顶部,系统还是应该认出它。
在沃尔多游戏中包含了许多充斥着活动的混乱场景,而沃尔多通常潜伏在一些不太可能的位置,读者的目标就是找出他。 沃尔多的样子并不取决于他潜藏的地方,因此我们可以使用一个“沃尔多检测器”扫描图像。 该检测器将图像分割成多个区域,并为每个区域包含沃尔多的可能性打分。 卷积神经网络正是将空间不变性(spatial invariance)的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。
总结一下适合于计算机视觉的神经网络架构原则:
- 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。
- 局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。
1.2. 多层感知机的限制
首先, 多层感知机的输入是二维图像 $\mathbf{X}$, 其隐藏表示 $\mathbf{H}$ 在数学上是一个矩阵, 在代码中表示为二维张量。其中 $\mathbf{X}$ 和 $\mathbf{H}$ 具有相同的形状。即输入和隐藏表示都拥有空间结构。
用$[\mathbf{X}]_{i, j}$ 和 $[\mathbf{H}]_{i, j}$ 分别表示输入图像和隐藏表示中位置 $(i, j)$ 处的像素。为了使每个隐藏神经元都能接收到每个输入像素的信息, 我们将参数从权重矩阵替换为四阶权重张量$\mathbf{W}$。假设 $\mathbf{U}$ 包含偏置参数, 可以将全连接层形式化地表示为
其中, 从W到V的转换只是形式上的转换, 因为在这两个四阶张量的元素之间存在一一对应的关系。只需重新索引下标 $(k, l)$, 使 $k=i+a 、 l=j+b$, 由此可得 $[\mathrm{V}]_{i, j, a, b}=[\mathrm{W}]_{i, j, i+a, j+b}$ 。索引 $a$ 和 $b$ 通过在正偏移和负偏移之间移动覆盖了整个图像。对于隐藏表示中任意给定位置 $(i, j)$ 处的像素值 $[\mathbf{H}]_{i, j}$, 可以通过在 $x$ 中以 $(i, j)$ 为中心对像素进行加权求和得到, 加权使用的权重为 $[\mathrm{V}]_{i, j, a, b}$ 。
1.2.1. 平移不变性
引用上述的第一个原则:平移不变性。这意味着检测对象在输入 $\mathbf{X}$ 中的平移, 应该仅导致隐藏表示 $\mathbf{H}$ 中的平移。也就是$\mathrm{V}$ 和 $\mathbf{U}$ 实际上不依赖于 $(i, j)$ 的值, 即 $[\mathbf{V}]_{i, j, a, b}=[\mathbf{V}]_{a, b}$ 。并且 $\mathbf{U}$ 是一个常数, 比如 $u$ 。 故可以简化 $\mathbf{H}$ 定义为:
这就是卷积 (convolution)。我们是在使用系数 $[\mathbf{V}]_{a, b}$ 对位置 $(i, j)$ 附近的像素 $(i+a, j+b)$ 进行加权得到 $[\mathbf{H}]_{i, j}$ 。 $[\mathbf{V}]_{a, b}$ 的系数比 $[\mathbf{V}]_{i, j, a, b}$ 少很多, 因为前者不再依赖于图像中的位置,这是显著的进步!
1.2.2. 局部性
引用上述的第二个原则:局部性。为了收集用来训练参数 $[\mathbf{H}]_{i, j}$ 的相关信息, 我们不应偏离到距 $(i, j)$ 很远的地方。这意味着在 $|a|>\Delta$ 或 $|b|>\Delta$ 的范围之外, 我们可以设置 $[\mathbf{V}]_{a, b}=0$ 。因此, 我们可以将 $[\mathbf{H}]_{i, j}$ 重写为
上式是一个卷积层 (convolutional layer) ,而卷积神经网络是包含卷积层的一类特殊的神经网络。在深度学习研究社区中, $\mathbf{V}$ 被称为卷积核 (convolution kernel)或者滤波器(filter),亦或简单地称之为该卷积层的权重, 通常该权重是可学习的参数。
当图像处理的局部区域很小时, 卷积神经网络与多层感知机的训练差异可能是巨大的: 以前, 多层感知机可能需要数十亿个参数来表示网络中的一层, 而现在卷积神经网络通常只需要几百个参数, 而且不需要改变输入或隐藏表示的维数。
参数大幅减少的代价是, 现在的特征是平移不变的, 并且当确定每个隐藏活性值时, 每一层只包含局部的信息。以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时, 我们就能得到样本有效的模型, 并且这些模型能很好地泛化到末知数据中。不符时, 如图像不满足平移不变时, 模型可能难以拟合训练数据。
1.3. 卷积
为什么上面的操作被称为卷积?
在数学中, 两个函数(比如 $\left.f, g: \mathbb{R}^{d} \rightarrow \mathbb{R}\right)$ 之间的 “卷积”被定义为
卷积是当把一个函数”翻转”并移位 $\mathbf{x}$ 时, 测量 $f$ 和 $g$ 之间的重叠。当为离散对象时, 积分就变成求和。例如:对于由 索引为 $\mathbb{Z}$ 的、平方可和的、无限维向量集合中抽取的向量,我们得到以下定义:
对于二维张量, 则为 $f$ 的索引 $(a, b)$ 和 $g$ 的索引 $(i-a, j-b)$ 上的对应加和:
这看起来类似于1.2.2中的卷积层公式, 只有一个主要区别:这里不是使用 $(i+a, j+b)$, 而是使用差值。但这种区别是表面的, 因为我们总是可以对应两式之间的符号。我们在中的原始定义更正确地描述了互相关 (cross-correlation)。
1.4. 沃尔多游戏回顾
回到“沃尔多在哪里”游戏,卷积层根据滤波器$V$选取给定大小的窗口,并加权处理图片,如下图所示。我们的目标是学习一个模型,以便探测出在“沃尔多”最可能出现的地方。
1.4.1. 通道
这种方法有一个问题: 忽略了图像一般包含三个通道/三种原色(红色、绿色和蓝色)。实际上图像不是二维张量, 而是一个由高度、宽度和颜色组成的三维张量, 比如包含 $1024 \times 1024 \times 3$ 个像素。前两个轴与像素的空间位置有关, 而第三个轴可以看作是每个像素的多维表示。因此, 我们将X索引为 $[\mathrm{X}]_{i, j, k}$ 。由此卷积相应地调整为 $[\mathbf{V}]_{a, b, c}$,而不是 $[\mathbf{V}]_{a, b}$ 。
由于输入图像是三维的, 隐藏表示$\mathbf{H}$也最好采用三维张量。也就是对于每一个空间位置, 采用一组隐藏表示而不是单个。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。
把隐藏表示想象为一系列具有二维张量的通道(channel),也被称为特征映射(feature maps),因为每个通道都向后续层提供一组空间化的学习特征。直观上你可以想象在靠近输入的底层, 一些通道专门识别边缘, 而一些通道专门识别纹理。
为了支持输入$\mathbf{X}$和隐藏表示 $\mathrm{H}$ 中的多个通道, 可以在V中添加第四个坐标, 即 $[\mathrm{V}]_{a, b, c, d}$ 综上所述:
其中隐藏表示$\mathbf{H}$中的索引 $d$ 表示输出通道, 而随后的输出将继续以三维张量 $\mathrm{H}$ 作为输入进入下一个卷积层。所以上式可以定义具有多个通道的卷积层,其中V是该卷积层的权重。
然而仍有许多问题需要解决。例如,图像中是否到处都有存在沃尔多的可能? 如何有效地计算输出层? 如何选择适当的激活函数? 为了训练有效的网络,如何做出合理的网络设计选择? 后续节会讨论这些。
2. 图像卷积
以图像为例,探索卷积神经网络的实际应用。
2.1. 互相关运算
严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。 在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
首先暂时忽略通道(第三维)这一情况, 看看如何处理二维图像数据和隐藏表示。下图中, 输入是高度为3 、宽度为3的二维张量(形状为 $3 \times 3$ )。卷积核的高度和宽度都是 2 , 而卷积核窗口(或卷积窗口,即输入处的窗口)的形状由内核的高度和宽度决定 $($ 即 $2 \times 2 )$。
图中的阴影部分是第一个输出元素,以及用于计算输出的输入张量元素和核张量元素。
在二维互相关运算中, 卷积窗口从输入张量的左上角开始, 从左到右、从上到下滑动。当卷积窗口滑动到新一个位置时, 包含在该窗口中的部分张量与卷积核张量进行按元素相乘, 得到的张量再求和得到一个单一的标量值, 由此我们得出了这一位置的 输出张量值。在如上例子中, 输出张量的四个元素由二维互相关运算得到, 这个输出高度为 2 、宽度为 2 , 如下所示:
输出大小略小于输入大小是因为卷积核的宽度和高度大于1, 并且卷积核只与图像中每个大小完全适合的位置进行互相关运算。所以,输出大小等于输入大小 $n_{h} \times n_{w}$ 减去卷积核大小 $k_{h} \times k_{w}$, 即:
这是因为需要足够的空间在图像上“移动”卷积核。稍后将看到如何通过在图像边界周围填充零来保证有足够的空间移动卷积核, 从而保持输出大小不变。接下来, 我们在 $\operatorname{corr} 2 \mathrm{~d}$ 函数中实现如上过程, 该函数接受输入张量X和卷积核张量K, 并返回输出张量Y。1
2
3
4
5
6
7
8
9
10
11
12
13import torch
from torch import nn
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
验证上述二维互相关运算的输出:1
2
3
4
5
6
7X = 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.]])
2.2. 卷积层
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。 基于卷积层训练模型时,同样随机初始化卷积核权重。
基于上面定义的corr2d函数实现二维卷积层。在init构造函数中,将weight和bias声明为两个模型参数。前向传播函数调用corr2d函数并添加偏置。1
2
3
4
5
6
7
8class 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 \times w$ 卷积或 $h \times w$ 卷积核。 我们也将带有 $h \times w$ 卷积核的卷积层称为 $h \times w$ 卷积层。
2.3. 图像中目标的边缘检测
卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。
首先构造一个$6 \times 8$像素的黑白图像。中间四列为黑色,其余像素为白色。1
2
3
4
5
6
7
8
9
10
11X = 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。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。1
K = torch.tensor([[1.0, -1.0]])
对参数X(输入)和K(卷积核)执行互相关运算。 如下所示,输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为0。1
2
3
4
5
6
7
8
9
10Y = 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只可以检测垂直边缘,无法检测水平边缘。1
2
3
4
5
6
7
8
9
10
11corr2d(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.]])
2.4. 学习卷积核
如果只需寻找黑白边缘,那么以上[1, -1]的边缘检测器足以。然而当有了更复杂数值的卷积核,或者连续的卷积层时,我们不可能手动设计滤波器。
我们需要学习由X生成Y的卷积核,即根据输入和输出,让程序学习应该使用什么样的卷积核。
现在看看是否可以通过仅查看“输入-输出”对来学习由X生成Y的卷积核。
先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核。简单起见在此使用内置的二维卷积层,并忽略偏置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27# 构造一个二维卷积层,它具有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 1.618
epoch 4, loss 0.298
epoch 6, loss 0.061
epoch 8, loss 0.015
epoch 10, loss 0.004
10次迭代之后,误差已经降到足够低。1
2
3
4conv2d.weight.data.reshape((1, 2))
输出:
tensor([[ 0.9879, -0.9993]])
结果非常接近之前的卷积核K。
2.5. 互相关和卷积
基于互相关运算和卷积运算之间的对应关系。 为了得到正式的卷积运算输出,我们需要执行中定义的严格卷积运算,而不是互相关运算。 但好在它们差别不大,只需水平和垂直翻转二维卷积核张量,然后对输入张量执行互相关运算。
由于卷积核是从数据中学习到的,因此无论这些层执行严格的卷积运算还是互相关运算,卷积层的输出都不会受到影响。假设卷积层执行互相关运算并学习之前例子中的卷积核,该卷积核在这里由矩阵$\mathbf{K}$表示。 假设其他条件不变,当这个层执行严格的卷积时,学习的卷积核$\mathbf{K}’$在水平和垂直翻转之后将与$\mathbf{K}$相同。 也就是说,当卷积层对例子中的输入和$\mathbf{K}’$执行严格卷积运算时,将得到与互相关运算相同的输出。
在深度学习文献中,将继续把“互相关运算”称为卷积运算,尽管它们略有不同。 对于卷积核张量上的权重,我们称其为元素。
2.6. 特征映射和感受域
输出的卷积层有时被称为特征映射 (feature map) ,因为它可以被视为一个输入映射到下一层的空间维度的转换器。在卷积神经网络中, 对于某一层的任意元素 $x$, 其感受域 (receptive field) 是指在前向传播期间可能影响 $x$ 计算的所有元素(来自所有先前层)。
感受野可能大于输入的实际大小。用2.1的图为例来解释:给定 $2 \times 2$ 卷积核, 阴影输出元素值19的感受域是输入阴影部分的四个元素。假设之前输出为 $\mathbf{Y}$, 其大小为 $2 \times 2$, 现在我们在其后附加一个卷积层, 该卷积层以 $\mathbf{Y}$ 为输入, 输出单个元素 $z$ 。此时$z$ 的感受域包括 $\mathbf{Y}$ 的所有四个元素, 以及最初所有九个输入元素。
重点来啦,根据这一特质,当一个特征图中的任意元素需要检测更广区域的输入特征时,可以构建一个更深的网络。
PS:实在受不了感受野这翻译,浅动一个字改成感受域吧。
3. 填充和步幅
现在已经知道假设输入形状为 $n_{h} \times n_{w}$, 卷积核形状为 $k_{h} \times k_{w}$, 那么输出形状将是 $\left(n_{h}-k_{h}+1\right) \times\left(n_{w}-k_{w}+1\right)$ 。 即卷积的输出形状取决于输入形状和卷积核的形状。
本节将介绍填充(padding)和步幅(stride)。假设以下情景:有时在应用了连续的卷积之后, 最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于 1 所导致的。比如, 一个 $240 \times 240$ 像素的图像, 经过 10 层 $5 \times 5$ 的卷积后, 将减少到 $200 \times 200$ 像素。这会导致原始图像边界上的有用信息被丢弃,填充是解决此问题最有效的方法。有时可能希望大幅降低图像的宽度和高度。例如原始的输入分辨率十分冗余,步幅可以在这类情况下提供帮助。
3.1. 填充(padding)
在应用多层卷积时,常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,可能只会丢失几个像素。 但随着应用许多连续卷积层,累积丢失的像素数就多了。 解决这个问题的简单方法为填充(padding):在输入图像的边界填充元素(通常填充元素是)。
以上节中的例子来说,将$3 \times 3$输入填充到$5 \times 5$,那么它的输出就增加为$4 \times 4$。如下图,阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素:
通常, 如果添加 $p_{h}$ 行填充(大约一半在顶部, 一半在底部)和 $p_{w}$ 列填充(左侧大约一半, 右侧一半), 则输出形状将为
即输出的高度和宽度将分别增加 $p_{h}$ 和 $p_{w}$ 。
在许多情况下, 我们需要设置 $p_{h}=k_{h}-1$ 和 $p_{w}=k_{w}-1$, 使输入和输出具有相同的高度和宽度。这样可以在构建网络时更容易地预测每个图层的输出形状。假设 $k_{h}$ 是奇数, 将在高度的两侧填充 $p_{h} / 2$ 行。如果 $k_{h}$ 是偶数, 则可以在输入顶部填充 $\left\lceil p_{h} / 2\right\rceil$ 行, 在底部填充 $\left\lfloor p_{h} / 2\right\rfloor$ 行。然后对宽度两侧按同样的道理填充。
卷积神经网络中卷积核的高度和宽度通常为奇数, 例如 $1、3、5、7$。选择奇数的好处是在保持空间维度的同时,可以在顶部和底部填充相同数量的行, 在左侧和右侧填充相同数量的列。
使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X, 当满足:1. 卷积核的大小是奇数;2. 所有边的填充行数和列数相同; 3. 输出与输入具有相同高度和宽度 则可以得出:输出 $Y[i, j]$ 是通过以输入 $[i, j]$ 为中心, 与卷积核进行互相关计算得到的。
下例中, 创建一个高度和宽度为3的二维卷积层, 并在所有侧边填充1个像素。给定高度和宽度为8的输入, 则输出的高度和宽度也是8。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import 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。1
2
3
4
5conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
输出:
torch.Size([8, 8])
3.2. 步幅(stride)
在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 在前面的例子中默认每次滑动一个元素。 但有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
通常将每次滑动元素的数量称为步幅(stride)。 下图是垂直步幅为3,水平步幅为2的二维互相关运算。 着色部分是输出元素以及用于输出计算的输入和内核张量元素:$0\times0+0\times1+1\times2+2\times3=8$、$0\times0+6\times1+0\times2+0\times3=6$。
可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口。
通常, 当垂直步幅为 $s_{h}$ 、水平步幅为 $s_{w}$ 时, 输出形状为
如果设置了 $p_{h}=k_{h}-1$ 和 $p_{w}=k_{w}-1$, 则输出形状将简化为 $\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$ 。如果在此基础上,输入的高度和宽度可以被垂直和水平步幅整除, 则输出形状将为 $\left(n_{h} / s_{h}\right) \times\left(n_{w} / s_{w}\right)$ 。
下面将高度和宽度的步幅设置为 2 , 从而将输入的高度和宽度减半。1
2
3
4
5conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
输出:
torch.Size([4, 4])
另一个复杂点的例子,只填充列:1
2
3
4
5conv2d = 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_{w}$ 时, 我们称之为填充 $\left(p_{h}, p_{w}\right)$ 。当 $p_{h}=p_{w}=p$ 时, 填充是 $p_{\text {。 }}$
默认情况下, 填充为 0 , 步幅为 1 。
在实践中很少使用不一致的步幅或填充, 通常有 $p_{h}=p_{w}$ 和 $s_{h}=s_{w}$ 。
4. 多输入多输出通道
目前为止展示的单个输入和单个输出通道的简单例子,使得我们可以将输入、卷积核和输出看作二维张量。
当添加通道时,输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有$3\times h\times w$的形状。我们将这个大小为$3$的轴称为通道(channel)维度。
4.1. 多输入通道
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为$c_i$,那么卷积核的输入通道数也需要为$c_i$。
如果卷积核的窗口形状是$k_h\times k_w$,当$c_i=1$时,可以把卷积核看作形状为$k_h\times k_w$的二维张量。
当$c_i>1$时,卷积核的每个输入通道将包含形状为$k_h\times k_w$的张量。将这些张量$c_i$连结在一起可以得到形状为$c_i\times k_h\times k_w$的卷积核。
输入和卷积核都有$c_i$个通道,所以可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将$c_i$的结果相加)得到二维张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。
下图演示了一个具有两个输入通道的二维互相关运算的示例。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素:
实现一下多输入通道互相关运算,所做的就是对每个通道执行互相关操作,然后将结果相加。1
2
3
4
5
6import torch
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
# 前面定义的corr2d函数
return sum(corr2d(x, k) for x, k in zip(X, K))
构造上图中的例子,验证互相关运算的输出:1
2
3
4
5
6
7
8
9X = 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.]])
4.2. 多输出通道
目前为止还只有一个输出通道,但每一层有多个输出通道是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,经常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,可以将每个通道看作是对不同特征的响应,但他们不是互相独立,而是一个不可分割的整体。因为每个通道不是独立学习的,而是为了共同使用而优化的,多输出通道并不仅是学习多个单通道的检测器。
用$c_i$和$c_o$分别表示输入和输出通道的数目,并让$k_h$和$k_w$为卷积核的高度和宽度。为了获得多个通道的输出,可以为每个输出通道创建一个形状为$c_i\times k_h\times k_w$的卷积核张量,这样卷积核的形状是$c_o\times c_i\times k_h\times k_w$。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。
下面实现一个计算多个通道的输出的互相关函数:1
2
3
4def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起,stack
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
通过将核张量K与K+1(K中每个元素加1)和K+2连接起来,构造了一个具有3个输出通道的卷积核:1
2
3
4
5K = torch.stack((K, K + 1, K + 2), 0)
K.shape
输出:
torch.Size([3, 2, 2, 2])
此时,卷积核K的形状为$3\times2\times2\times2$,可以想作是3片2通道的2维张量。然后对输入张量X与卷积核张量K执行互相关运算。现在的输出包含3个通道,第一个通道的结果与先前输入张量X和多输入单输出通道的结果一致:1
2
3
4
5
6
7
8
9
10
11corr2d_multi_in_out(X, K)
输出:
tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
4.3. $1\times1$卷积层
$1 \times 1$卷积,即$k_h = k_w = 1$,看起来似乎没有多大意义。
毕竟,卷积的本质是有效提取相邻像素间的相关特征,而$1 \times 1$卷积并没有这种作用。
尽管如此,$1 \times 1$卷积核仍然十分流行,经常包含在复杂深层网络的设计中。下面详细地解读一下它的实际作用。
因为使用了最小窗口,$1\times 1$卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。
其实,$1\times 1$卷积的唯一计算发生在通道上。
下图展示了使用$1\times 1$卷积核与$3$个输入通道和$2$个输出通道的互相关计算。
这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。对于图像来说,实际上是对每个像素点,在不同的通道上进行线性组合(信息整合),且保留了图片的原有平面结构,完成升维或降维的功能。
可以将$1 \times 1$卷积层看作是在每个像素位置应用的全连接层,以$c_i$个输入值转换为$c_o$个输出值。也就是说$1 \times 1$卷积核的一个作用是调整通道数,类比多层感知机中的全连接层,调整输入和输出的大小。
这仍然是一个卷积层,所以跨像素的权重是一致的。同时,$1\times 1$卷积层需要的权重维度为$c_o\times c_i$,再额外加上一个偏置。
使用全连接层实现$1 \times 1$卷积,注意需要对输入和输出的数据形状进行调整:1
2
3
4
5
6
7
8
9# X是输入,K是卷积核
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 \times 1$卷积运算时,上述函数相当于先前实现的互相关函数corr2d_multi_in_out:1
2
3
4
5
6
7
8# 注意这里的 X 和 K 的形状,在reshape后的矩阵乘法应用了广播机制
# 3行9列的X 和 2行3列的K 进行矩阵乘法,将K扩展为9行3列进行运算
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
综上,$1 \times 1$卷积层通常用于调整网络层的通道数量和控制模型复杂性。
5. 池化层
当处理图像时,通常希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受域(输入)就越大。
而机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”),所以最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
此外,当检测较底层的特征时(例如边缘检测),通常希望这些特征保持某种程度上的平移不变性。例如,如果拍摄黑白之间轮廓清晰的图像X,并将整个图像向右移动一个像素,即Z[i, j] = X[i, j + 1],则新图像Z的输出可能大不相同。而在现实中,随着拍摄角度的移动,任何物体不可能出现在同一位置上。即使用三脚架拍摄一个静止的物体,由于快门的移动而引起的相机振动,可能会使所有物体左右移动一个像素。
本节介绍池化(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
5.1. 最大池化层和平均池化层
与卷积层类似,池化层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为池化窗口)遍历的每个位置计算一个输出。 但不同于卷积层中的输入与卷积核之间的互相关计算,池化层不包含参数。 相反,池运算是确定性的,我们通常计算池化窗口中所有元素的最大值或平均值。这些操作分别称为最大池化层(maximum pooling)和平均池化层(average pooling)。
在这两种情况下,与互相关运算符一样,池化窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在池化窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大池化层还是平均池化层。
着色部分是第一个输出元素,以及用于计算这个输出的输入元素:$\max(0, 1, 3, 4)=4$
池化窗口形状为$p \times q$的池化层称为$p \times q$池化层,池化操作称为$p \times q$池化。
回到开头提到的对象边缘检测示例,现在我们将使用卷积层的输出作为$2\times 2$最大池化的输入。
设置卷积层输入为X
,池化层输出为Y
。
无论是移动X[i, j]
到X[i, j + 1]
,或移动X[i, j]
到X[i + 1, j]
,池化层始终输出Y[i, j] = 1
。
也就是说,使用$2\times 2$最大池化层,即使在高度或宽度上移动一个元素,卷积层仍然可以识别到模式。
在下面的代码中的pool2d
函数,实现了池化层的正向传播。这类似于之前实现的corr2d
函数。
但这里我们没有卷积核,输出为输入中每个区域的最大值或平均值。1
2
3
4
5
6
7
8
9
10
11
12
13import torch
from torch import nn
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
验证二维最大池化层的输出:1
2
3
4
5
6X = 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.]])
验证平均池化层:1
2
3
4
5pool2d(X, (2, 2), 'avg')
输出:
tensor([[2., 3.],
[5., 6.]])
5.2. 填充和步幅
与卷积层一样,池化层也可以通过填充和步幅以获得所需的输出形状。 下面用深度学习框架中内置的二维最大池化层,来演示池化层中填充和步幅的使用。 首先构造了一个输入张量X,它有四个维度,其中样本数和通道数都是1。1
2
3
4
5
6
7
8X = 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)。1
2
3
4
5pool2d = nn.MaxPool2d(3)
pool2d(X)
输出:
tensor([[[[10.]]]])
可以设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度。1
2
3
4
5
6pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)
输出:
tensor([[[[ 5., 7.],
[13., 15.]]]])
5.3. 多个通道
在处理多通道输入数据时,池化层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着池化层的输出通道数与输入通道数相同。 下面在通道维度上连结张量X和X + 1,以构建具有2个通道的输入。1
2
3
4
5
6
7
8
9
10
11
12
13X = 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:1
2
3
4
5
6
7
8
9pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
输出:
tensor([[[[ 5., 7.],
[13., 15.]],
[[ 6., 8.],
[14., 16.]]]])
6. 卷积神经网络(LeNet)
现在,对于Fashion-MNIST数据集中的服装图片,我们已经掌握了卷积层的处理方法,可以在图像中保留空间结构(不需要像MLP中一样展平)。 同时,用卷积层代替全连接层的另一个好处是:模型更简洁、所需的参数更少。
本节将介绍LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。 这个模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名),目的是识别图像中的手写数字。 当时,Yann LeCun发表了第一篇通过反向传播成功训练卷积神经网络的研究,这项工作代表了十多年来神经网络研究开发的成果。
LeNet在当时取得了与支持向量机(support vector machines, SVM)性能相媲美的成果,成为监督学习的主流方法。 LeNet被广泛用于自动取款机(ATM)机中,帮助识别处理支票的数字。 时至今日,一些自动取款机仍在运行Yann LeCun和他的同事Leon Bottou在上世纪90年代写的代码!
6.1. LeNet
总体来看,LeNet(LeNet-5)由两个部分组成:
- 卷积编码器:由两个卷积层组成;
- 全连接层密集块:由三个全连接层组成。
该架构如下所示(LeNet中的数据流。输入是手写数字,输出为10种可能结果的概率):
每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均池化层。虽然ReLU和最大池化层更有效,但它们在20世纪90年代还没有出现。
每个卷积层使用$5\times 5$卷积核和一个sigmoid激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有6个输出通道,而第二个卷积层有16个输出通道。每个$2\times2$池操作(步幅2)通过空间下采样将维数减少4倍。卷积的输出形状由批量大小、通道数、高度、宽度决定。
为了将卷积块的输出传递给稠密块,必须在小批量中展平每个样本。也就是将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。LeNet的稠密块有三个全连接层,分别有120、84和10个输出。因为我们在执行分类任务,所以输出层的10维对应于最后输出结果的数量。
用深度学习框架实现此类模型非常简单,只需要实例化一个Sequential
块并将需要的层连接在一起。1
2
3
4
5
6
7
8
9
10
11
12
13
14import torch
from torch import nn
net = nn.Sequential(
# 通道数 1 -> 6
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
# 通道数 6 -> 16
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 \times 28$的单通道(黑白)图像通过LeNet。通过在每一层打印输出的形状可以检查模型,以确保其操作与我们期望的一致。
1 | X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32) |
注意,在整个卷积块中,与上一层相比,每一层特征的高度和宽度都减小了。 第一个卷积层使用2个像素的填充,来补偿$5 \times 5$卷积核导致的特征减少。 相反,第二个卷积层没有填充,因此高度和宽度都减少了4个像素。 随着层叠的上升,通道的数量从输入时的1个,增加到第一个卷积层之后的6个,再到第二个卷积层之后的16个。 同时,每个池化层的高度和宽度都减半。最后,每个全连接层减少维数,最终输出一个维数与结果分类数相匹配的输出。
6.2. 模型训练
现在已经实现了LeNet,看看它在Fashion-MNIST数据集上的表现:1
2
3batch_size = 256
# 省略了load_data_fashion_mnist的实现
train_iter, test_iter = load_data_fashion_mnist(batch_size=batch_size)
虽然卷积神经网络的参数较少,但与深度的多层感知机相比,它们的计算成本仍然很高,因为每个参数都参与更多的乘法。 可以使用GPU加快训练。
由于完整的数据集位于内存中,因此在模型使用GPU计算数据集之前需要将其复制到显存中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21def evaluate_accuracy_gpu(net, data_iter, device=None):
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
# 省略了Accumulator的实现
metric = 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)
# 省略了accuracy的实现
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
训练方面,在进行正向和反向传播之前,我们需要将每一小批量数据移动到指定的设备(例如GPU)上。
以下训练函数假定从高级API创建的模型作为输入,并进行相应的优化,使用Xavier随机初始化模型参数。 与全连接层一样,使用交叉熵损失函数和小批量随机梯度下降。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42def 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实现
animator = Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
# 省略了Timer实现
timer, num_batches = Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = 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], 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模型(M1芯片GPU):1
2lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, torch.device('mps'))
关于这一部分的完整代码在实践中可以找到。