主要参考学习资料:
《动手学深度学习》阿斯顿·张 等 著
【动手学深度学习 PyTorch版】哔哩哔哩@跟李牧学AI
由于本系列一开始跳过了第一章引言部分,因此系列编号比书本章节编号提前。现改为和书本统一(因为之前自己的原始笔记也是按照书本章节编的,每次发布都要修改有些麻烦)。
概述
- 由于多层感知机应用于图像会导致数以亿计的参数,因此卷积神经网络被提出。
- 卷积层的操作基于输入与卷积核的互相关运算。
- 卷积核的大小、步幅和图像的填充都会影响输出的大小。
- 一张图像一般具有多个通道,为输入和输出添加了一个维度。
- 汇聚层进一步降低了卷积层对位置的过度敏感。
- LeNet是最早发布的卷积神经网络之一。
- 下一章将介绍更多现代卷积神经网络。
6.1 从全连接层到卷积
(本节公式理解有困难可直接看下一节图像卷积)
6.1.1 多层感知机的限制
设计适合于计算机视觉的神经网络架构应该符合以下两个原则:
- 平移不变性:不管检测对象出现在图像中的哪个位置,神经网络的前几层应该对相同的图像区域具有相似的反应。
- 局部性:神经网络的前几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,最终可以聚合这些局部特征以在整张图像级进行预测。
在多层感知机中,我们将二维图像 X \boldsymbol X X展平为一维向量 x \boldsymbol x x作为输入,相应地得到一个一维向量 h \boldsymbol h h作为输出(隐藏表示)。权重矩阵 W \boldsymbol W W的元素 w i , j w_{i,j} wi,j表示将输入向量的第 j j j个元素映射到输出向量的第 i i i个元素的权重。
为了体现输入和输出的空间结构,方便应用开头的两个原则,我们仍将它们看作是二维的,于是参数从权重矩阵替换为四阶权重张量 W \mathsf W W,其中元素 w i , j , k , l w_{i,j,k,l} wi,j,k,l表示将输入位置 ( k , l ) (k,l) (k,l)处的元素映射到输出位置 ( i , j ) (i,j) (i,j)处的元素的权重。假设 U \boldsymbol 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 h_{i,j}=u_{i,j}+\displaystyle\sum_k\sum_lw_{i,j,k,l}x_{k,l}=u_{i,j}+\displaystyle\sum_a\sum_bv_{i,j,a,b}x_{i+a,j+b} hi,j=ui,j+k∑l∑wi,j,k,lxk,l=ui,j+a∑b∑vi,j,a,bxi+a,j+b
其中 W \mathsf W W到 V \mathsf V V只是下标上作了参数代换,以此来引出卷积层。可以理解为看待 X \boldsymbol X X中元素的视角从其本身的绝对位置转换到了与目标输出元素坐标的相对位置。
1.平移不变性
现在引用平移不变性原则,即对象在 X \boldsymbol X X中的平移仅引起隐藏表示 H \boldsymbol H H中的平移,故计算 h i , j h_{i,j} hi,j的参数应该与 i i i、 j j j无关:
h i , j = u + ∑ a ∑ b v a , b x i + a , i + b h_{i,j}=u+\displaystyle\sum_a\sum_bv_{a,b}x_{i+a,i+b} hi,j=u+a∑b∑va,bxi+a,i+b
这就是卷积层中的卷积,这一步转化大大减少了神经网络所需的参数。
2.局部性
现在引用局部性原则,即我们不应该使用距离 ( i , j ) (i,j) (i,j)很远的位置来训练计算 h i , j h_{i,j} hi,j的参数,故通过限制 a a a、 b b b的取值范围来限制选取输入元素 x i + a , i + b x_{i+a,i+b} xi+a,i+b的范围:
h i , j = u + ∑ a = − Δ Δ ∑ b = − Δ Δ v a , b x i + a , j + b h_{i,j}=u+\displaystyle\sum^\Delta_{a=-\Delta}\sum^\Delta_{b=-\Delta}v_{a,b}x_{i+a,j+b} hi,j=u+a=−Δ∑Δb=−Δ∑Δva,bxi+a,j+b
这就是卷积层,其中新的权重矩阵 V \boldsymbol V V称为卷积核或滤波器,而卷积神经网络是包含卷积层的一类特殊神经网络。
更通俗地说,卷积层的特点就是让层中的每个神经元只关注图像中自己负责的一个局部。
卷积神经网络的参数大幅减少,但其代价是假设特征平移不变,并且在确定每个隐藏激活值时,每一层只包含局部的信息。
6.1.2 卷积
上述操作称为卷积的理由来源于数学中定义的卷积运算:
( f ∗ g ) ( x ) = ∫ f ( z ) g ( x − z ) d z \displaystyle(f*g)(\boldsymbol x)=\int f(\boldsymbol z)g(\boldsymbol x-\boldsymbol z)\mathrm d\boldsymbol z (f∗g)(x)=∫f(z)g(x−z)dz
当计算对象为离散的时,积分就变成求和:
( f ∗ g ) ( i ) = ∑ a f ( a ) g ( i − a ) (f*g)(i)=\displaystyle\sum_af(a)g(i-a) (f∗g)(i)=a∑f(a)g(i−a)
将其进一步拓展到二维:
( f ∗ g ) ( i , j ) = ∑ a ∑ b f ( a , b ) g ( i − a , j − b ) (f*g)(i,j)=\displaystyle\sum_a\sum_bf(a,b)g(i-a,j-b) (f∗g)(i,j)=a∑b∑f(a,b)g(i−a,j−b)
最终得到的式子与神经网络中的卷积操作类似,但区别是第二个函数/元素的索引。严格来说,卷积层中的卷积是一种错误叫法,实际上其表达的运算为互相关,运算符号为 ⊗ \otimes ⊗。
6.1.3 通道
前面为了简化问题,我们假定图像的每个像素可以用一个标量表示,由此将图像转化为二维张量。但一般情况下,图像包含3个通道(RGB三原色),每个通道都是一个二维张量,因此实际上图像是一个由高度、宽度和颜色通道组成的三维张量,所以我们对 X \mathsf X X和 H \mathsf H H均采用三维索引,同时卷积可以通过四维索引来进一步表示从 X \mathsf X X的通道 c c c到 H \mathsf H H的通道 d d d的映射关系:
h i , j , d = ∑ a = − Δ Δ ∑ b = − Δ Δ ∑ c v a , b , c , d x i + a , j + b , c h_{i,j,d}=\displaystyle\sum^\Delta_{a=-\Delta}\sum^{\Delta}_{b=-\Delta}\sum_cv_{a,b,c,d}x_{i+a,j+b,c} hi,j,d=a=−Δ∑Δb=−Δ∑Δc∑va,b,c,dxi+a,j+b,c
隐藏表示 H \mathsf H H中的通道也称为特征映射,因为每个通道都向后续层提供一组空间化的学习特征。直观上可以想象在靠近输出的底层,一些通道专门识别边缘,一些通道专门识别纹理。
6.2 图像卷积
6.2.1 互相关运算
暂时忽略表示通道的第三维,我们以二维图像数据为例更直观地理解一下上一节的互相关运算是如何操作的。
假设输入是一个 3 × 3 3\times3 3×3的矩阵,我们用 2 × 2 2\times2 2×2的卷积核窗口去套输入矩阵上 2 × 2 2\times2 2×2的区域,每个区域会通过按元素乘法并求和的方法得到一个输出。遍历整个输入矩阵,我们能套到左上角、右上角、左下角和右下角的 4 4 4个 2 × 2 2\times2 2×2的区域,这 4 4 4个区域最终输出一个 2 × 2 2\times2 2×2的矩阵。所有计算如下:
0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 19 0\times0+1\times1+3\times2+4\times3=19 0×0+1×1+3×2+4×3=19
1 × 0 + 2 × 1 + 4 × 2 + 5 × 3 = 25 1\times0+2\times1+4\times2+5\times3=25 1×0+2×1+4×2+5×3=25
3 × 0 + 4 × 1 + 6 × 2 + 7 × 3 = 37 3\times0+4\times1+6\times2+7\times3=37 3×0+4×1+6×2+7×3=37
4 × 0 + 5 × 1 + 7 × 2 + 8 × 3 = 43 4\times0+5\times1+7\times2+8\times3=43 4×0+5×1+7×2+8×3=43
假设输入大小为 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_h-k_h+1 nh−kh+1次,从左侧到右侧需水平移动 n w − k w + 1 n_w-k_w+1 nw−kw+1次,因此输出大小为:
( 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)
用代码实现互相关运算:
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K):
"""输入张量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
6.2.2 卷积层
对于卷积层来说,被训练的参数为卷积核和标量偏置,而超参数为卷积核的大小。在训练基于卷积层的模型时,我们随机初始化卷积核权重。
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
6.2.3 图像中目标的边缘检测
卷积核的一个简单应用是边缘检测:通过找到像素变化的位置来检测图像中不同颜色的边缘。我们以一个 6 × 8 6\times8 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 × 2 1\times2 1×2的卷积核 K \mathsf K K,当进行互相关运算时,若水平相邻两元素相同则输出零,否则输出非零:
K = torch.tensor([[1.0, -1.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.]])
可见在从左到右的方向上, Y \mathsf Y Y中的1表示从白色到黑色的边缘, − 1 -1 −1表示从黑色到白色的边缘。
6.2.4 学习卷积核
通过内置的二维卷积层,我们来学习上述目标检测例子中的卷积核,损失函数采用 Y \mathsf Y Y与输出的平方误差:
#构造具有1个输入通道和1个输出通道、卷积核大小为(1, 2)的卷积层,为简单起见不带偏置
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
#该二维卷积层使用四维输入输出格式(批量大小、通道、高度、宽度)
X = X.reshape(1, 1, 6, 8)
Y = Y.reshape(1, 1, 6, 7)
#学习率
lr = 0.03
for i in range(10):
Y_hat = conv2d(X)
l = (Y - Y_hat) ** 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}')
print(conv2d.weight.data.reshape(1, 2))
运行结果:
epoch 2, loss 6.546
epoch 4, loss 2.196
epoch 6, loss 0.818
epoch 8, loss 0.322
epoch 10, loss 0.129
tensor([[ 0.9557, -1.0296]])
可见学习到的卷积核与上一小节定义的卷积核 K \mathsf K K十分接近。
6.2.6 感受野
对于某一层的任意元素,其感受野是指在前向传播期间可能影响其计算的所有元素。例如本节最初的例子中输出矩阵每个元素的感受野是输入矩阵中对应的 4 4 4个元素。若在输出矩阵后再作用一个卷积核大小为 2 × 2 2\times2 2×2的卷积层,则最终得到的单个输出元素的感受野除了包含输出矩阵中的 4 4 4个元素外,还包含输入矩阵中的 9 9 9个元素。
对于更深的网络,感受野的大小甚至可能大于输入的实际大小。
6.3 填充和步幅
除了输入和卷积核的大小,填充和步幅也会影响输出的大小。
6.3.1 填充
在应用多层卷积时,边缘像素的信息会在前向传播的过程中逐渐丢失。解决该问题的简单方法是填充:在输入图像的边缘填充元素(通常为 0 0 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 ) (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1) (nh−kh+ph+1)×(nw−kw+pw+1)
通常我们设置 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使输入和输出的形状相同,这样构建网络时可以更容易地预测每个层的输出形状。宽度和高度的两侧会设置相等的填充数目,如果 p h p_h ph或 p w p_w pw为奇数则尽量在两侧均匀分配。因此卷积核的高度和宽度通常为奇数来使 p h p_h ph或 p w p_w pw为偶数。
pytorch卷积层的填充使用padding关键字参数配置:
#高度和宽度两侧边各自的填充分别为2和1
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
6.3.2 步幅
在前面的例子中,我们默认卷积核每次滑动一个元素。有时候为了高效计算或者缩减采样次数,卷积核可以每次滑动多个元素。每次滑动元素的数量称为步幅。
在 5 × 5 5\times5 5×5的输入中,使用 2 × 2 2\times2 2×2的卷积核和高度为 3 3 3、宽度为 2 2 2的步幅,卷积层的计算过程如下:
如果垂直步幅为 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 ⌋ \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⌋
如果我们设置 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 ⌋ \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⌋
更进一步,如果输入的高度和宽度可以被垂直步幅和水平步幅整除,则输出形状为:
( n h / s h ) × ( n w / s w ) (n_h/s_h)\times(n_w/s_w) (nh/sh)×(nw/sw)
pytorch卷积层的填充使用stride关键字参数配置:
#垂直步幅和水平步幅分别为3和4
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1), stride=(3, 4))
6.4 多输入多输出通道
6.4.1 多输入通道
在通道维度为多输入单输出的情况下,需要构造一个具有与输入数据相同输入通道数的卷积核。每个通道的输入和卷积核各自进行互相关运算,再对所有通道的运算结果求和得到输出:
import torch
from d2l import torch as d2l
def corr2d_multi_in(X, K):
#按第0个维度(通道维度)遍历X和K进行互相关运算再求和,其中x和k是X和K每个通道下的二维张量
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
6.4.2 多输出通道
在通道维度为多输入多输出的情况下,需要构造与输出通道同等数量的三维卷积核,每个卷积核以上述方式生成一个通道的输出张量。
def corr2d_multi_in_out(X, K):
#遍历四维张量K每个通道的三维卷积核k与X进行多输入单输出互相关运算
#再将每个通道的二维运算结果在第0维上堆叠成三维输出张量
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
6.4.3 1 × 1 1\times1 1×1卷积层
k h = k w = 1 k_h=k_w=1 kh=kw=1的 1 × 1 1\times1 1×1卷积在复杂深层网络的设计中很流行,它不识别空间模式,只融合通道信息。下图使用两个三维卷积核将 3 3 3个输入通道转化为 2 2 2个输出通道:
1 × 1 1\times1 1×1卷积相当于将每个像素看作一个样本,而每个通道看作一种特征,以此对每个像素位置应用一次全连接层,得到每个像素的单通道输出。多通道输出则相当于应用了多次权重不同的全连接层。该全连接层的输入形状为 n h n w × c i n_hn_w\times c_i nhnw×ci,权重矩阵形状为 c o × c i c_o\times c_i co×ci,输出形状为 n h n w × c o n_hn_w\times c_o nhnw×co。
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)
总结一下,对于多输入多输出的卷积层来说:
- 输入 X \boldsymbol X X: c i × n h × n w c_i\times n_h\times n_w ci×nh×nw
- 核 W \boldsymbol W W: c o × c i × k h × k w c_o\times c_i\times k_h\times k_w co×ci×kh×kw
- 偏置 B \boldsymbol B B: c o × c i c_o\times c_i co×ci
- 输出 Y \boldsymbol Y Y: c o × m h × m w c_o\times m_h\times m_w co×mh×mw
Y = X ⊗ W + B \boldsymbol Y=\boldsymbol X\otimes\boldsymbol W+\boldsymbol B Y=X⊗W+B
其中 c o c_o co和 c i c_i ci的表示可以更通俗地理解为将通道 i i i映射到通道 o o o的参数。
6.5 汇聚层
汇聚层具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
6.5.1 最大汇聚和平均汇聚
与卷积层类似,汇聚层的运算也依赖一个固定形状的窗口,该窗口根据其步幅大小再输入的所有区域上滑动,为窗口遍历的每个位置计算一个输出。不同的是,汇聚成不包含学习参数,我们通常计算汇聚窗口中所有元素的最大值和平均值,分别称为最大汇聚和平均汇聚。
通过求最值或平均,即使图像的像素在一定范围内移动,或者随机抽去图像中的一部分行和列,对最终输出也不会产生显著影响。
汇聚窗口形状为 p × q p\times q p×q的汇聚层称为 p × q p\times q p×q汇聚层,其操作称为 p × q p\times q p×q汇聚。汇聚层也称为池化层。
汇聚层的代码实现除了计算输出的操作,其余与卷积层类似:
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
6.5.2 填充和步幅
与卷积层一样,汇聚层也可以通过填充和步幅获得所需的输出形状。以最大汇聚为例,pytorch通过padding和stride关键字参数对填充和步幅进行设置:
#当参数只传入一个整数时,设定的形状为高宽相等的方阵
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
6.5.3 多通道
在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不像卷积层一样将通道进行融合,因此汇聚层的输出通道数和输入通道数相同。
6.6 卷积神经网络(LeNet)
LeNet是最早发布的卷积神经网络之一,在1989年以识别手写数字为目的被AT&T贝尔实验室的研究员Yann LeCun提出。
6.6.1 LeNet
LeNet-5由以下两个部分组成:
- 卷积编码器:由两个卷积层组成。
- 全连接层稠密块:由三个全连接层组成。
每个卷积层使用 5 × 5 5\times5 5×5卷积核和一个sigmoid激活函数,并紧随一个步幅为 2 2 2的 2 × 2 2\times2 2×2平均汇聚操作。第一个卷积层有 6 6 6个输出通道,第二个卷积层有 16 16 16个输出通道。虽然ReLU和最大汇聚层更有效,但它们在20世纪90年代还没有出现。
为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。稠密块的三个全连接层分别有 120 120 120、 84 84 84和 10 10 10个输出,输出层的 10 10 10维分别是数字被识别为 0 ∼ 9 0\sim9 0∼9的概率。
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一致。
6.6.2 模型训练
本章的模型训练的函数中包括了计算设备的选择。
batch_size = 256
#使用Fashion-MNIST数据集
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
#使用GPU计算模型在数据集上的精度
def evaluate_accuracy_gpu(net, data_iter, device=None):
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:
#将数据移动到指定的计算设备
#X可能为list或tensor,需分情况操作
if isinstance(X, list):
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训练模型
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
#采用Xavier均匀分布初始化
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', 'text acc'])
#timer用于计算训练效率
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]
#每20%的batch或最后一个batch更新训练曲线
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())
plt.show()
训练结果:
loss 0.457, train acc 0.829, test acc 0.822
37214.7 examples/sec on cuda:0