《动手学深度学习》学习笔记 第6章 卷积神经网络

本系列为《动手学深度学习》学习笔记
书籍链接:动手学深度学习

笔记是从第四章开始,前面三章为基础知道,有需要的可以自己去看看

关于本系列笔记: 书里为了让读者更好的理解,有大篇幅的描述性的文字,内容很多,笔记只保留主要内容,同时也是对之前知识的查漏补缺

6. 卷积神经网络

6.1 从全连接层到卷积

6.1.1 不变性

设计适合于计算机视觉的神经网络架构。

  1. 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层
    应该对相同的图像区域具有相似的反应,即为“平移不变性”。
  2. 局部性(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+kl[W]i,j,k,l[X]k,l=[U]i,j+ab[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+ab[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:RdR)之间的“卷积”被定义为:
( f ∗ g ) ( x ) = Z f ( z ) g ( x − z ) d z . (f ∗ g)(x) = Zf(z)g(x − z)dz. (fg)(x)=Zf(z)g(xz)dz.
  当为离散对象时,积分就变成求和。

6.1.4 “沃尔多在哪里”回顾

《动手学深度学习》学习笔记 第6章 卷积神经网络_第1张图片
通道
  可以把隐藏表示想象为一系列具有二维张量的通道(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),而不是卷积运算。
《动手学深度学习》学习笔记 第6章 卷积神经网络_第2张图片
  (0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 19.)

《动手学深度学习》学习笔记 第6章 卷积神经网络_第3张图片
  输出⼤小略小于输⼊⼤小。这是因为卷积核的宽度和⾼度⼤于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) (nhkh+1)×(nwkw+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)
《动手学深度学习》学习笔记 第6章 卷积神经网络_第4张图片

  • 卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。

  选择奇数的好处是:保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。

  • 此外,使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X,当满足:
  1. 卷积核的大小是奇数;
  2. 所有边的填充行数和列数相同;
  3. 输出与输入具有相同高度和宽度则可以得出:输出 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⌋. ⌊(nhkh+ph+sh)/sh×⌊(nwkw+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 多输入多输出通道

  当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。
《动手学深度学习》学习笔记 第6章 卷积神经网络_第5张图片

	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个输出值。

《动手学深度学习》学习笔记 第6章 卷积神经网络_第6张图片
  下面,我们使用全连接层实现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章 卷积神经网络_第7张图片
  图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章 卷积神经网络_第8张图片
  图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一致。
《动手学深度学习》学习笔记 第6章 卷积神经网络_第9张图片

	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

《动手学深度学习》学习笔记 第6章 卷积神经网络_第10张图片

你可能感兴趣的:(《手动深度学习》笔记,深度学习,学习,笔记)