本系列为《动手学深度学习》学习笔记
书籍链接:动手学深度学习
笔记是从第四章开始,前面三章为基础知道,有需要的可以自己去看看
关于本系列笔记: 书里为了让读者更好的理解,有大篇幅的描述性的文字,内容很多,笔记只保留主要内容,同时也是对之前知识的查漏补缺
《动手学深度学习》学习笔记 第4章 多层感知机
《动手学深度学习》学习笔记 第5章 深度学习计算
《动手学深度学习》学习笔记 第6章 卷积神经网络
《动手学深度学习》学习笔记 第7章 现代卷积神经网络
6. 卷积神经网络
6.1 从全连接层到卷积
6.1.1 不变性
设计适合于计算机视觉的神经网络架构。
- 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层
应该对相同的图像区域具有相似的反应,即为“平移不变性”。 - 局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔
较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。
6.1.2 多层感知机的限制
多层感知机的输入是二维图像 X X X,其隐藏表示 H H H在数学上是一个矩阵,在代码中表示为二维张量。
使用 [ X ] i , j [X]_{i,j} [X]i,j和 [ H ] i , j [H]_{i,j} [H]i,j分别表示输入图像和隐藏表示中位置 ( i , j ) {(i,j)} (i,j)处的像素。( [ X ] i , j [X]_{i,j} [X]i,j和 [ H ] i , j [H]_{i,j} [H]i,j是一样大的(感知机,全连接层))
假设
U
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}+\sum_{k}\sum_{l}{[W]_{i,j,k,l}[X]_{k,l}} = [U]_{i,j}+\sum_{a}\sum_{b}{[V]_{i,j,a,b}[X]_{i+a,j+b}}
[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
上式中 [ V ] i , j , a , b = [ W ] i + a , j + b [V]_{i,j,a,b}=[W]_{i+a,j+b} [V]i,j,a,b=[W]i+a,j+b,索引a和b通过在正偏移和负偏移之间移动覆盖了整个图像。
对于隐藏表示中任意给定位置 ( i , j ) {(i,j)} (i,j)处的像素值 [ X ] i , j [X]_{i,j} [X]i,j,可以通过在 X X X中以 ( i , j ) {(i,j)} (i,j)为中心对像素进行加权求和得到,加权使用的权重为 [ V ] i , j , a , b [V]_{i,j,a,b} [V]i,j,a,b( 到这里 [ V ] i , j , a , b [V]_{i,j,a,b} [V]i,j,a,b表示的还是跟 X X X一样大的张量,还不能理解为卷积 )。
平移不变性:
V
V
V和
U
U
U实际上不依赖于
(
i
,
j
)
(i, j)
(i,j)的值,即
[
V
]
i
,
j
,
a
,
b
=
[V]
a
,
b
[V]_{i,j,a,b }= \textbf{[V]}_{a,b}
[V]i,j,a,b=[V]a,b。并且U是一个常数,比如u。因此,可以简化
H
H
H定义为:
[
H
]
i
,
j
=
u
+
∑
a
∑
b
[V]
a
,
b
[
X
]
i
+
a
,
j
+
b
[H]_{i,j} = u+\sum_{a}\sum_{b}{\textbf{[V]}_{a,b}[X]_{i+a,j+b}}
[H]i,j=u+a∑b∑[V]a,b[X]i+a,j+b
这就是卷积(convolution)。使用系数
[V]
a
,
b
\textbf{[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
[H]_{i,j}
[H]i,j。
局部性:
如上所述,为了收集用来训练参数
[
H
]
i
,
j
[H]_{i,j}
[H]i,j的相关信息,不应偏离
到距
(
i
,
j
)
(i, j)
(i,j)很远的地方。这意味着在
∣
a
∣
>
∆
|a| > ∆
∣a∣>∆或
∣
b
∣
>
∆
|b| > ∆
∣b∣>∆的范围之外,我们可以设置
[V]
a
,
b
=
0
\textbf{[V]}_{a,b} = 0
[V]a,b=0。因此,将
[
H
]
i
,
j
[H]_{i,j}
[H]i,j重写为:
[
H
]
i
,
j
=
u
+
∑
a
=
−
∆
∆
∑
b
=
−
∆
∆
[V]
a
,
b
[
X
]
i
+
a
,
j
+
b
[H]_{i,j} = u+\sum_{a=−∆}^{∆}\sum_{b=−∆}^{∆}{\textbf{[V]}_{a,b}[X]_{i+a,j+b}}
[H]i,j=u+a=−∆∑∆b=−∆∑∆[V]a,b[X]i+a,j+b
简而言之,(6.1.3)是一个卷积层(convolutional layer)
在深度学习研究社区中,V被称为卷积核(convolution kernel)或者滤波器(filter),亦或简单地称之为该卷积层的权重,通常该权重是可学习的参数。
6.1.3 卷积
在进一步讨论之前,我们先简要回顾一下为什么上面的操作被称为卷积。在数学中,两个函数(比如
f
,
g
:
R
d
→
R
f, g :R^d → R
f,g:Rd→R)之间的“卷积”被定义为:
(
f
∗
g
)
(
x
)
=
Z
f
(
z
)
g
(
x
−
z
)
d
z
.
(f ∗ g)(x) = Zf(z)g(x − z)dz.
(f∗g)(x)=Zf(z)g(x−z)dz.
当为离散对象时,积分就变成求和。
6.1.4 “沃尔多在哪里”回顾
通道
可以把隐藏表示想象为一系列具有二维张量的通道(channel)。这些通道有时也被称为特征映射(feature maps),因为每个通道都向后续层提供一组空间化的学习特征。(直观上可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。)
为了支持输入 X X X和隐藏表示 H H H中的多个通道,我们可以在V中添加第四个坐标,即 [ V ] a , b , c , d [V]_{a,b,c,d} [V]a,b,c,d。综上所述
[
H
]
i
,
j
,
d
=
∑
a
=
−
∆
∆
∑
b
=
−
∆
∆
[
V
]
a
,
b
,
c
,
d
[
X
]
i
+
a
,
j
+
b
,
c
[H]_{i,j,d} = \sum_{a=−∆}^{∆}\sum_{b=−∆}^{∆}{[V]_{a,b,c,d}[X]_{i+a,j+b,c}}
[H]i,j,d=a=−∆∑∆b=−∆∑∆[V]a,b,c,d[X]i+a,j+b,c
其中隐藏表示
H
H
H中的索引,
d
d
d表示输出通道,而随后的输出将继续以三维张量
H
H
H作为输入进入下一个卷积层。
6.2 图像卷积
6.2.1 互相关运算
严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross‐correlation),而不是卷积运算。
(0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 19.)
输出⼤小略小于输⼊⼤小。这是因为卷积核的宽度和⾼度⼤于1,而卷积核只与图像中每个⼤小完全适合的位置进⾏互相关运算。
所以,输出⼤小等于输⼊⼤小
n
h
×
n
w
n_h × n_w
nh×nw减去卷积核⼤小
k
h
×
k
w
k_h × k_w
kh×kw即:
(
n
h
−
k
h
+
1
)
×
(
n
w
−
k
w
+
1
)
(n_h-k_h+1 )× (n_w-k_w+1)
(nh−kh+1)×(nw−kw+1)
在corr2d函数中实现如上过程,该函数接受输⼊张量
X
X
X和卷积核张量
K
K
K,并返回输出张量
Y
Y
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
6.2.2 卷积层
基于上⾯定义的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
6.2.3 图像中目标的边缘检测
如下是卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。
构建卷积核:构造一个6 × 8像素的黑白图像。中间四列为黑色(0),其余像素为白色(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的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。
K = torch.tensor([[1.0, -1.0]])
现在,对参数X(输入) 和K(卷积核) 执行互相关运算。如下所示,
- 输出Y中的1代表从白色到黑色的边缘,
- ‐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.]])
现在将输入的二维图像转置,再进行如上的互相关运算。
其输出如下,之前检测到的垂直边缘消失了。
不出所料,这个卷积核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.]])
6.2.4 学习卷积核
是否可以学习由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}')
在10次迭代之后,误差已经降到足够低。(大家可以自己试试)
6.2.5 互相关和卷积
它们差别不大,只需水平和垂直翻转二维卷积核张量,然后对输入张量执行互相关运算。
为了与深度学习文献中的标准术语保持一致,将继续把“互相关运算”称为卷积运算,尽管严格地说,它们略有不同。
此外,对于卷积核张量上的权重,我们称其为元素。
6.2.6 特征映射和感受野
中输出的卷积层有时被称为特征映射(feature map),因为它可以被视为一个输入映射到下一层的空间维度的转换器。
在卷积神经网络中,对于某一层的任意元素x,其感受野(receptive field)是指在前向传播期间可能影响x计算的所有元素(来自所有先前层)。
6.3 填充和步幅
6.3.1 填充
如上所述,在应用多层卷积时,我们常常丢失边缘像素。随着应用许多连续卷积层,累积丢失的像素数就多了。解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是0)。
- 卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。
选择奇数的好处是:保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
- 此外,使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X,当满足:
- 卷积核的大小是奇数;
- 所有边的填充行数和列数相同;
- 输出与输入具有相同高度和宽度则可以得出:输出 Y [ i , j ] Y[i, j] Y[i,j]是通过以输入 X [ i , j ] X[i, j] X[i,j]为中心,与卷积核进行互相关计算得到的。
例:创建一个高度和宽度为3的二维卷积层,并在所有侧边填充1个像素。给定高度和宽度为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
6.3.2 步幅
将每次滑动元素的数量称为步幅(stride)。
当垂直步幅为
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
⌋
.
⌊(n_h − k_h + p_h + s_h)/s_h⌋ × ⌊(n_w − k_w + p_w + s_w)/s_w⌋.
⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋.
将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半。
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
看一个稍微复杂的例子。
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
6.4 多输入多输出通道
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。
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))
corr2
实现过程见前文:6.2.1 互相关运算
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.4.2 多输出通道
为了获得多个通道的输出,可以为每个输出通道创建一个形状为 c i × k h × k w c_i × k_h × k_w ci×kh×kw的卷积核张量,这样卷积核的形状是 c o × c i × k h × k w c_o × c_i × k_h × 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)
6.4.3 1 × 1 卷积层
因为使用了最小窗口,1 × 1卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。其实1 × 1卷积的唯一计算发生在通道上。
下图展示了使用1×1卷积核与3个输入通道和2个输出通道的互相关计算。
可以将1 × 1卷积层看作在每个像素位置应用的全连接层,以 c i c_i ci个输入值转换为 c o c_o co个输出值。
下面,我们使用全连接层实现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))
6.5 汇聚层
通常当处理图像时,希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。
本节将介绍汇聚(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
6.5.1 最大汇聚层和平均汇聚层
与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。
然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。
池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。
在汇聚窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。
图6.5.1: 汇聚窗口形状为 2 × 2 的最大汇聚层。着色部分是第一个输出元素,以及用于计算这个输出的输入元素: max(0, 1, 3, 4) = 4.
在下面的代码中的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
6.5.2 填充和步幅
与卷积层一样,汇聚层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。
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.]]]])
默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同。
填充和步幅可以手动设定。
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.]]]])
6.5.3 多个通道
在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。这意味着汇聚层的输出通道数与输入通道数相同。
6.6 卷积神经网络(LeNet)
本节将介绍LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。这个模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名),目的是识别图像(LeCun et al., 1998)中的手写数字。当时,Yann LeCun发表了第一篇通过反向传播成功训练卷积神经网络的研究,这项工作代表了十多年来神经网络研究开发的成果。
6.6.1 LeNet
总体来看,LeNet(LeNet‐5)由两个部分组成:
- 卷积编码器:由两个卷积层组成;
- 全连接层密集块:由三个全连接层组成。
该架构如 图6.6.1所示。
图6.6.1: LeNet中的数据流。输入是手写数字,输出为10种可能结果的概率。
为了将卷积块的输出传递给稠密块,必须在小批量中展平每个样本。换之,将这个四维输入转换成全连接层所期望的二维输入。
这里的二维表示第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。
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的单通道(黑白)图像通过LeNet。通过在每一层打印输出的形状,可以检查模型,以确保其操作与期望的 图6.6.2一致。
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])
6.6.2 模型训练
现在已经实现了LeNet,看看LeNet在Fashion‐MNIST数据集上的表现。
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
为了进行评估,需要对 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,我们还需要一点小改动。需要将每一小批量数据移动到指定的设备(例如GPU)上。
如下所示,由于将实现多层神经网络,因此将主要使用高级API。以下训练函数假定从高级API创建的模型作为输入,并进行相应的优化。
使用在4.8.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.469, train acc 0.823, test acc 0.779
55296.6 examples/sec on cuda:0