神经网络(三)——卷积神经网络(Convolutional Neural Network)

卷积神经网络(Convolutional Neural Network, CNN)是一种专门用于处理具有网格状拓扑数据(如图像)的深度学习模型。CNN在计算机视觉领域表现卓越,广泛应用于图像分类、对象检测、图像分割等任务。

  1. 输入层:接收原始图像数据,通常为三维数组(宽度、高度、通道数)。
  2. 卷积层:对输入图像进行卷积操作,生成特征图。
  3. 激活函数:对卷积结果应用激活函数,如ReLU。
  4. 池化层:对特征图进行池化操作,降低特征图尺寸。
  5. 重复上述卷积层、激活函数和池化层,直到提取出高层次特征。
  6. 全连接层:将高层次特征展平成一维向量,输入到全连接层进行分类。
  7. 输出层:生成最终的分类结果。

一、卷积层

1.1 图像卷积

输入是高度为、宽度为的二维张量(即形状为)。卷积核的高度和宽度都是,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即)。

在如上例子中,输出张量的四个元素由二维互相关运算得到,这个输出高度为、宽度为,如下所示:

注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1,而卷积核只与图像中每个大小完全适合的位置进行互相关运算。所以,输出大小等于输入大小减去卷积核大小,即:

这是因为需要足够的空间在图像上“移动”卷积核。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:])


X = torch.rand(size=(3, 3))
conv2d = nn.Conv2d(1, 1, kernel_size=2)
Y=comp_conv2d(conv2d,X)
Y.shape
1
torch.Size([2, 2])

1.2 填充

在应用多层卷积时,常常丢失边缘像素。由于通常使用小卷积核,因此对于任何单个卷积,可能只会丢失几个像素。但随着应用许多连续卷积层,累积丢失的像素数就多了。解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是)。

输入填充到,那么它的输出就增加为。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素:

通常,如果添加行填充(大约一半在顶部,一半在底部)和列填充(左侧大约一半,右侧一半),则输出形状将为

这意味着输出的高度和宽度将分别增加。在许多情况下,需要设置,使输入和输出具有相同的高度和宽度。这样可以在构建网络时更容易地预测每个图层的输出形状。假设是奇数,将在高度的两侧填充行。如果是偶数,则一种可能性是在输入顶部填充行,在底部填充行。同理,填充宽度的两侧。

使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X,当满足:
1. 卷积核的大小是奇数;
2. 所有边的填充行数和列数相同;
3. 输出与输入具有相同高度和宽度
则可以得出:输出Y[i, j]是通过以输入X[i, j]为中心,与卷积核进行互相关计算得到的。

1
2
3
4
# 请注意,这里每边都填充了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
1
torch.Size([8, 8])
1
2
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
1
torch.Size([8, 8])

1.3 步幅

在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。在前面的例子中,默认每次滑动一个元素。但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。

垂直步幅为 3,水平步幅为 2 的二维互相关运算。

通常,当垂直步幅为、水平步幅为时,输出形状为

如果设置了,则输出形状将简化为。更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为

下面,我们[将高度和宽度的步幅设置为2],从而将输入的高度和宽度减半。

1
2
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
1
torch.Size([4, 4])
1
2
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
1
torch.Size([2, 2])

为了简洁起见,当输入高度和宽度两侧的填充数量分别为时,称之为填充。当时,填充是。同理,当高度和宽度上的步幅分别为时,称之为步幅。特别地,当时,称步幅为。默认情况下,填充为0,步幅为1。在实践中,很少使用不一致的步幅或填充,也就是说,通常有

1.4 多输入通道

当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为,那么卷积核的输入通道数也需要为。如果卷积核的窗口形状是,那么当时,可以把卷积核看作形状为的二维张量。

然而,当时,卷积核的每个输入通道将包含形状为的张量。将这些张量连结在一起可以得到形状为的卷积核。由于输入和卷积核都有个通道,可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将的结果相加)得到二维张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。

演示一个具有两个输入通道的二维互相关运算的示例。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素:

1
2
3
4
5
6
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]]]])
conv2d = nn.Conv2d(2,1, kernel_size=2)
conv2d.weight=nn.Parameter(K)
conv2d(X)
1
2
tensor([[[[ 55.6891,  71.6891],
[103.6891, 119.6891]]]], grad_fn=<ConvolutionBackward0>)

1.5 多输出通道

每一层有多个输出通道是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,通常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。

分别表示输入和输出通道的数目,并让为卷积核的高度和宽度。为了获得多个通道的输出,可以为每个输出通道创建一个形状为的卷积核张量,这样卷积核的形状是。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。

1
2
3
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
K = torch.stack((K, K + 1, K + 2), 0)
K.shape
1
torch.Size([3, 2, 2, 2])
1
2
3
conv2d = nn.Conv2d(2,3, kernel_size=2)
conv2d.weight=nn.Parameter(K)
conv2d(X)
1
2
3
4
5
6
7
8
tensor([[[[ 56.1745,  72.1745],
[104.1745, 120.1745]],

[[ 75.7571, 99.7571],
[147.7571, 171.7571]],

[[ 95.7574, 127.7574],
[191.7574, 223.7574]]]], grad_fn=<ConvolutionBackward0>)

1.6 卷积层

卷积,即,看起来似乎没有多大意义。毕竟,卷积的本质是有效提取相邻像素间的相关特征,而卷积显然没有此作用。尽管如此,仍然十分流行,经常包含在复杂深层网络的设计中。

因为使用了最小窗口,卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。其实卷积的唯一计算发生在通道上。

下图展示了使用卷积核与个输入通道和个输出通道的互相关计算。这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。可以将卷积层看作在每个像素位置应用的全连接层,以个输入值转换为个输出值。因为这仍然是一个卷积层,所以跨像素的权重是一致的。同时,卷积层需要的权重维度为,再额外加上一个偏置。

下面,使用全连接层实现卷积。需要对输入和输出的数据形状进行调整。

1
2
3
4
5
6
X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))

conv2d = nn.Conv2d(1,2, kernel_size=1)
conv2d.weight=nn.Parameter(K)
conv2d(X)
1
2
3
4
5
6
7
tensor([[[-0.2232,  0.5647,  1.6974],
[-0.9593, 1.2693, 2.0304],
[ 2.3791, -0.7228, -2.2729]],

[[-0.0278, -2.8085, 0.1661],
[-2.9471, 0.8498, -1.2249],
[ 1.5648, -1.7783, -1.0291]]], grad_fn=<SqueezeBackward1>)

1.7 卷积类型 [1]

1.7.1 普通卷积

普通卷积的操作分成3个维度,在空间维度(H和W维度)是共享卷积核权重滑窗相乘求和(融合空间信息),在输入通道维度是每一个通道使用不同的卷积核参数并对输入通道维度求和(融合通道信息),在输出通道维度操作方式是并行堆叠(多种),有多少个卷积核就有多少个输出通道

1.7.2 空洞卷积

和普通卷积相比,空洞卷积可以在保持较小参数规模的条件下增大感受野,常用于图像分割领域。其缺点是可能产生网格效应,即有些像素被空洞漏过无法利用到,可以通过使用不同膨胀因子的空洞卷积的组合来克服该问题,参考文章:https://developer.orbbec.com.cn/v/blog_detail/892

1.7.3 分组卷积

和普通卷积相比,分组卷积将输入通道分成g组,卷积核也分成对应的g组,每个卷积核只在其对应的那组输入通道上做卷积,最后将g组结果堆叠拼接。由于每个卷积核只需要在全部输入通道的个通道上做卷积,参数量降低为普通卷积的。分组卷积要求输入通道和输出通道数都是g的整数倍。参考文章:https://zhuanlan.zhihu.com/p/65377955

1.7.4 深度可分离卷积

深度可分离卷积的思想是先用(输入通道数)的分组卷积逐通道作用融合空间信息,再用n(输出通道数)个1乘1卷积融合通道信息。 其参数量为 , 相比普通卷积的参数量 显著减小

1.7.5 转置卷积

一般的卷积操作后会让特征图尺寸变小,但转置卷积(也被称为反卷积)可以实现相反的效果,即放大特征图尺寸。对两种方式理解转置卷积,第一种方式是转置卷积是一种特殊的卷积,通过设置合适的padding的大小来恢复特征图尺寸。第二种理解基于卷积运算的矩阵乘法表示方法,转置卷积相当于将卷积核对应的表示矩阵做转置,然后乘上输出特征图压平的一维向量,即可恢复原始输入特征图的大小。 参考文章:https://zhuanlan.zhihu.com/p/115070523

暂时忽略通道,从基本的转置卷积开始,设步幅为1且没有填充。假设有一个的输入张量和一个的卷积核。以步幅为1滑动卷积核窗口,每行次,每列次,共产生个中间结果。每个中间结果都是一个的张量,初始化为0。为了计算每个中间张量,输入张量中的每个元素都要乘以卷积核,从而使所得的张量替换中间张量的一部分。请注意,每个中间张量被替换部分的位置与输入张量中元素的位置相对应。最后,所有中间结果相加以获得最终结果。

下图解释了如何为的输入张量计算卷积核为的转置卷积。

1
2
3
4
5
6
X = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, bias=False)
tconv.weight.data = K
tconv(X)
1
2
3
tensor([[[[ 0.,  0.,  1.],
[ 0., 4., 6.],
[ 4., 12., 9.]]]], grad_fn=<ConvolutionBackward0>)

填充、步幅和多通道

与常规卷积不同,在转置卷积中,填充被应用于的输出(常规卷积将填充应用于输入)。例如,当将高和宽两侧的填充数指定为1时,转置卷积的输出中将删除第一和最后的行与列。

1
2
3
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, padding=1, bias=False)
tconv.weight.data = K
tconv(X)
1
tensor([[[[4.]]]], grad_fn=<ConvolutionBackward0>)

在转置卷积中,步幅被指定为中间结果(输出),而不是输入。使用上图中相同输入和卷积核张量,将步幅从1更改为2会增加中间张量的高和权重,因此输出张量在下图中。

以下代码可以验证步幅为2的转置卷积的输出。

1
2
3
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, stride=2, bias=False)
tconv.weight.data = K
tconv(X)
1
2
3
4
tensor([[[[0., 0., 0., 1.],
[0., 0., 2., 3.],
[0., 2., 0., 3.],
[4., 6., 6., 9.]]]], grad_fn=<ConvolutionBackward0>)

对于多个输入和输出通道,转置卷积与常规卷积以相同方式运作。假设输入有个通道,且转置卷积为每个输入通道分配了一个的卷积核张量。当指定多个输出通道时,每个输出通道将有一个的卷积核。

同样,如果将代入卷积层来输出,并创建一个与具有相同的超参数、但输出通道数量是中通道数的转置卷积层,那么的形状将与相同。
下面的示例可以解释这一点。

1
2
3
4
X = torch.rand(size=(1, 10, 16, 16))
conv = nn.Conv2d(10, 20, kernel_size=5, padding=2, stride=3)
tconv = nn.ConvTranspose2d(20, 10, kernel_size=5, padding=2, stride=3)
tconv(conv(X)).shape == X.shape
1
True

抽象来看,给定输入向量和权重矩阵,卷积的前向传播函数可以通过将其输入与权重矩阵相乘并输出向量来实现。由于反向传播遵循链式法则和,卷积的反向传播函数可以通过将其输入与转置的权重矩阵相乘来实现。因此,转置卷积层能够交换卷积层的正向传播函数和反向传播函数:它的正向传播和反向传播函数将输入向量分别与相乘。可以使用矩阵乘法来实现卷积。转置卷积层能够交换卷积层的正向传播函数和反向传播函数。

二、池化层(pooling)

池化层(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。与卷积层类似,池化层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口遍历的每个位置计算一个输出。 然而,不同于卷积层中的输入与卷积核之间的互相关计算,池化层不包含参数。 通常计算池化窗口中所有元素的最大值或平均值。这些操作分别称为最大池化层(Max pooling)和平均池化层(average pooling)。

池化窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在池化窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大池化层还是平均池化层。

输出张量的高度为,宽度为。这四个元素为每个池化窗口中的最大值:

池化窗口形状为的池化层称为池化层,池化操作称为池化。

Max Pooling

1
2
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X
1
2
pool2d = nn.MaxPool2d(2)
pool2d(X)
1
2
tensor([[[[ 5.,  7.],
[13., 15.]]]])

Avg Pooling

1
2
pool2d = nn.AvgPool2d(2)
pool2d(X)
1
2
tensor([[[[ 2.5000,  4.5000],
[10.5000, 12.5000]]]])

2.1 填充和步幅

与卷积层一样,池化层也可以改变输出形状。和以前一样,可以通过填充和步幅以获得所需的输出形状。下面,用深度学习框架中内置的二维最大池化层,来演示池化层中填充和步幅的使用。

深度学习框架中的步幅与池化窗口的大小相同

1
2
pool2d = nn.MaxPool2d(3)
pool2d(X)
1
tensor([[[[10.]]]])

填充和步幅可以手动设定

1
2
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
1
2
tensor([[[[ 5.,  7.],
[13., 15.]]]])

设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度

1
2
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)
1
2
tensor([[[[ 5.,  7.],
[13., 15.]]]])

2.2 多个通道

在处理多通道输入数据时,[池化层在每个输入通道上单独运算],而不是像卷积层一样在通道上对输入进行汇总。这意味着池化层的输出通道数与输入通道数相同。下面,将在通道维度上连结张量XX + 1,以构建具有2个通道的输入。

1
2
X = torch.cat((X, X + 1), 1)
X
1
2
3
4
5
6
7
8
9
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
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
1
2
3
4
5
tensor([[[[ 5.,  7.],
[13., 15.]],

[[ 6., 8.],
[14., 16.]]]])

三、上采样和下采样

缩小图像(或称为下采样(subsampled)或降采样(downsampled))的主要目的有两个:

  1. 使得图像符合显示区域的大小;
  2. 生成对应图像的缩略图。

的主要目的是:放大原图像,从而可以显示在更高分辨率的显示设备上。

3.1 下采样

下采样(subsampled)也称为降采样(downsampled)对图像进行了缩小,下采样的过程是一个信息损失的过程。

通常使用以下方法:

  1. 使用步幅为2的池化层:例如Max-pooling和Average-pooling,目前通常使用Max-pooling,因为他计算简单而且能够更好的保留纹理特征。池化层是为了降低特征维度
  2. 使用步幅为2的卷积层:卷积过程使图像变小是为了提取特征。池化层是不可学习的,使用可学习卷积层来代替pooling可以得到更好的效果

下采样主要目的是为了使得图像符合显示区域的大小,生成对应图像的缩略图。主要两个作用:

  • 一是减少计算量,防止过拟合;
  • 二是增大感受野,使得后面的卷积核能够学到更加全局的信息。

3.2 上采样

上采样(upsampling)或称为图像插值(interpolating),主要目的是放大原图像。一般有三种方式:

  1. 插值,一般使用的是双线性插值,因为效果最好,虽然计算上比其他插值方式复杂,但是相对于卷积计算可以说不值一提,其他插值方式还有最近邻插值、三线性插值等;

  2. 转置卷积又或是说反卷积(Transpose Conv),通过对输入feature map间隔填充0,再进行标准的卷积计算,可以使得输出feature map的尺寸比输入更大;

  3. Up-Pooling - Max Unpooling && Avg Unpooling --Max Unpooling,在对称的max pooling位置记录最大值的索引位置,然后在unpooling阶段时将对应的值放置到原先最大值位置,其余位置补0;

而UnSampling阶段没有使用MaxPooling时的位置信息,而是直接将内容复制来扩充Feature Map。

3.2.1 最邻近元法

这种方法最简单,不需要计算,即在待求像素的四个邻像素中,选取距离待求像素最近的邻像素的灰度值赋给待求像素。

如上图所示,新增在A区内的像素就用左上角的像素点来赋值,其余三个区域同理。虽然最邻近元法计算量较小,但可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能会出现明显的锯齿状。

3.2.2 双线性插值法

线性插值

假设已知坐标 ,要得到 区间内某一位置在直线上的值。根据图中所示,得到

由于值已知,所以可以从公式得到 y 的值

已知的过程与以上过程相同,只是要进行交换。

双线性插值法

双线性插值,又称为双线性内插。在数学上,双线性插值是对线性插值在二维直角网格上的扩展,用于对双变量函数(例如)进行插值。其核心思想是在两个方向分别进行一次线性插值。

红色的数据点与待插值得到的绿色点

假如想得到未知函数在点的值,假设我们已知函数,,, 及四个点的值。

首先在方向进行线性插值,得到

然后在方向进行线性插值,得到

注意此处如果先在方向插值、再在方向插值,其结果与按照上述顺序双线性插值的结果是一样的。

3.2.3 三线性插值

表示在下方一个方格点,表示在上方的一个方格点,对于是同样的意思。表示在较小相关坐标的差值.

首先,沿着x轴方向插值

然后再沿着y轴插值

最后再沿着z轴插值

上述操作可以形象化如下:首先,找到围绕需要插值点立方体的八个角。 这些角的值为,,,,,,
,

接下来,在之间进行线性插值来找到来找到来找到来找到

现在在之间进行插值来找到来找到。 最后,通过的线性插值计算值

四、卷积神经网络(LeNet)

LeNet是最早发布的卷积神经网络之一,LeNet(LeNet-5)由两个部分组成:

  • 卷积编码器:由两个卷积层组成;
  • 全连接层密集块:由三个全连接层组成。
LeNet中的数据流。输入是手写数字,输出为10种可能结果的概率。

每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均池化。请注意,虽然ReLU和最大池化更有效,但它们在20世纪90年代还没有出现。每个卷积层使用卷积核和一个sigmoid激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有6个输出通道,而第二个卷积层有16个输出通道。每个池操作(步幅2)通过空间下采样将维数减少4倍。卷积的输出形状由批量大小、通道数、高度、宽度决定。

为了将卷积块的输出传递给稠密块,必须在小批量中展平每个样本。换言之,将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量表示。LeNet的稠密块有三个全连接层,分别有120、84和10个输出。因为我们在执行分类任务,所以输出层的10维对应于最后输出结果的数量。

用深度学习框架实现此类模型非常简单。只需要实例化一个Sequential块并将需要的层连接在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
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。通过在每一层打印输出的形状,可以通过[检查模型],以确保其操作与期望的一致。

LeNet 的简化版。
1
2
3
4
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)
1
2
3
4
5
6
7
8
9
10
11
12
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])

加载数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def load_data_fashion_mnist(batch_size, resize=None):  
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=4),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=4))

batch_size=256
train_iter, test_iter = load_data_fashion_mnist(batch_size=batch_size)

评估函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def 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
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
with torch.no_grad():
for X, y in data_iter:
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]

训练

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
42
43
44
45
46
47
48
def train6(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)}')

def try_gpu(i=0):
"""如果存在,则返回gpu(i),否则返回cpu()"""
if torch.cuda.device_count() >= i + 1:
return torch.device(f'cuda:{i}')
return torch.device('cpu')

lr, num_epochs = 0.9, 10
train6(net, train_iter, test_iter, num_epochs, lr, try_gpu())
1
2
loss 0.472, train acc 0.822, test acc 0.775
116769.4 examples/sec on cuda:0

参考

  1. 李沐-动手学深度学习第二版
  2. 反卷积(Deconvolution)、上采样(UNSampling)与上池化(UnPooling)
  3. 上采样(UnSampling) 和 下采样(DownSampling)是啥?
  4. 上采样和下采样
  5. 线性插值
  6. 双线性插值
  7. Trilinear interpolation

神经网络(三)——卷积神经网络(Convolutional Neural Network)
https://mztchaoqun.com.cn/posts/D56_CNN/
作者
mztchaoqun
发布于
2025年1月10日
许可协议