卷积神经网络(Pytorch 08)

一 从全连接层到卷积

卷积网络 主干的基本元素:这包括 卷积层本身填充(padding)和 步幅 (stride)的基本细节、用于在相邻区域汇聚信息的 汇聚层(pooling)、在每一层中多通道(channel)的使用。卷积神经网络 (convolutional neural networks,CNN)是机器学习利用自然图像中一些已知结构的创造性方法。

的多层感知机十分适合处理表格数据,其中 行对应样本列对应特征

1.1 不变性

  1. 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层 应该对相同的图像区域具有相似的反应,即为“平移不变性”。
  2. 局部性(locality):神经网络的前面几层应该 只探索输入图像中的局部区域,而不过度在意图像中相隔 较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。

当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大 的:以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而 现在卷积神经网络通常只需要几百个参数,而且 不需要改变输入或隐藏表示的维数。参数大幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每一层只包含局部的信息。以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到 未知数据中。但如果这偏置与现实不符时,比如当图像不满足平移不变时,我们的模型可能难以拟合我们的训练数据。

小结:

  • 图像的平移不变性使我们以相同的方式处理局部图像,而 不在乎它的位置
  • 局部性意味着计算相应的隐藏 表示只需一小部分局部图像像素
  • 在图像处理中,卷积层通常比全连接层需要更少的参数,但依旧获得高效用的模型。
  • 卷积神经网络(CNN)是一类特殊的神经网络,它可以 包含多个卷积层
  • 多个输入和输出通道使模型在每个空间位置可以获取图像的多方面特征。

1.2 图像卷积

严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是 互相关运算(cross‐correlation),而不是卷积运算。在卷积层中,输入张量和核张量通过互相关运算产生输出张量。

这是因为我们需要足够的空间在图像上“移动”卷积核。稍后,我们将看到 如何通过在图像边界周围填充零 来保证有足够的空间移动卷积核,从而保持输出大小不变。接下来,我们在corr2d函数中实现如上过程,该 函数接受输入张量X和卷积核张量K,并返回输出张量Y。

import torch
from torch import nn
from d2l import torch as d2l

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

验证上述二维互相关运算的输出。

X = 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.]])

1.3 卷积层

卷积层 对输入和卷积核权重 进行互相关运算,并在 添加标量偏置 之后产生输出。所以,卷积层中的两个被训 练的参数是卷积核权重和标量偏置。就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时, 我们也随机初始化卷积核权重。

基于上面定义的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

高度和宽度分别为h和w的卷积核可以被称为h × w卷积或h × w卷积核。

1.4 图像中目标的边缘检测

如下是卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。首先,我们构造 一个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]])
k
# tensor([[ 1., -1.]])

现在,我们对参数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只可以检测垂直边缘,无法检测水平边缘

corr2d(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.]])

1.5 学习卷积核

如果我们只需寻找黑白边缘,那么以上[1, -1]的边缘检测器足以。然而,当有了更复杂数值的卷积核,或者 连续的卷积层时,我们不可能手动设计滤波器。

现在让我们看看是否可以通过仅查看“输入‐输出”对来学习由X生成Y的卷积核。我们先构造一个卷积层,并 将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y与卷积层输出的平方误差,然后计算梯度 来更新卷积核。

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 = 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  3.816
# epoch 4, loss  0.789
# epoch 6, loss  0.193
# epoch 8, loss  0.057
# epoch 10, loss  0.020

在10次迭代之后,误差已经降到足够低。现在我们来看看 我们所学的卷积核的权重张量

conv2d.weight.data.reshape((1, 2))
# tensor([[ 1.0043, -0.9771]])

二 填充和步幅

在前面的例子中,输入的 高度和宽度都为3卷积核的高度和宽度都为2,生成的输出表征的维数 为2 × 2。假设输入形状为n_h × n_w,卷积核形状为k_h × k_w,那么输出形 状将是(n_h − k_h + 1) × (n_w − k_w + 1)。因此,卷积的输出形状取决于输入形状和卷积核的形状

有 时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大 于1所导致的。比如,一个240 × 240像素的图像,经过10层5 × 5的卷积后,将减少到200 × 200像素。如此一 来,原始图像的边界丢失了许多有用信息。而填充是解决此问题最有效的方法;有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余步幅则可以在这类情况下提供帮助。

2.1 填充

在应用多层卷积时,我们常常丢失边缘像素。由于我们通常使用小卷积核,因此对于任何单个卷 积,我们可能只会丢失几个像素。但随着我们应用许多连续卷积层,累积丢失的像素数就多了。解决这个问 题的简单方法即为填充(padding):在输入图像的边界填充元素通常填充元素是0)。例如,我们将3 × 3输入填充到5 × 5,那么它的输出就增加为4 × 4。阴影部分是第一个输出元素以及用于输出计算 的输入和核张量元素:0 × 0 + 0 × 1 + 0 × 2 + 0 × 3 = 0,外部填充一圈0。

卷积神经网络中 卷积核的高度和宽度通常为奇数,例如1、3、5或7。选择奇数的好处是,保持空间维度的同 时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。

比如,在下面的例子中,我们创建 一个高度和宽度为3的二维卷积层,并在所有侧边填充1个像素给定高度和宽度为8的输入,则输出的高度和宽度也是8

import torch
from torch import nn

def comp_conv2d(conv2d, x):
    x = x.reshape((1, 1) + x.shape)
    y = conv2d(x)
    return y.reshape(y.shape[2:])

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
# conv2d = nn.Conv2d(1, 1, kernel_size=5, padding=2)
x = torch.rand(size=(8, 8))
comp_conv2d(conv2d, x).shape
# torch.Size([8, 8])

当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在 如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1

conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, x).shape
# torch.Size([8, 8])

2.2 步幅

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

下面示例垂直步幅为3水平步幅为2的二维互相关运算。着色部分是输出元素以及用于 输出计算的输入和内核张量元素:0 × 0 + 0 × 1 + 1 × 2 + 2 × 3 = 8、0 × 0 + 6 × 1 + 0 × 2 + 0 × 3 = 6。 可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加 另一列填充)。

x = torch.rand(size=(5, 5))
conv2d = nn.Conv2d(1, 1, kernel_size=2, stride=3, padding=0)
comp_conv2d(conv2d, x).shape
# torch.Size([2, 2])

看一个稍微复杂的例子:

x = torch.rand(size=(8, 8))
conv2d = 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 时,我们称之为 填充(p_h, p_w)。当ph = pw = p时,填充是p。同理,当高度和宽度上的步幅分别为s_h和s_w时,我们称之为 步幅(s_h, s_w)。特别地,当sh = sw = s时, 我们称步幅为s。默认情况下,填充为0,步幅为1。在实践中,我们 很少使用不一致的步幅或填充,也就是说, 我们 通常有p_h = p_w和s_h = s_w

  • 填充 可以增加输出的高度和宽度。这 常用来使输出与输入具有相同的高和宽
  • 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的1/n(n是一个大于1的整数)。
  • 填充和步幅可用于有效地调整数据的维度

三 多输入多输出通道

当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有3 × h × w的 形状。我们将这个大小为3的轴称为 通道(channel)维度。本节将更深入地研究具有多输入和多输出通道的 卷积核。

3.1 多输入通道

当输入包含多个通道时,需要构造一个 与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。

为了加深理解,我们实现一下多输入通道互相关运算。简而言之,我们所做的就是 对每个通道执行互相关操作,然后将结果相加

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))

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.]])

3.2 多输出通道

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

实现一个计算多个通道的输出的互相关函数。

def corr2d_multi_in_out(X, K):
    # 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
    # 最后将所有结果都叠加在一起
    return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

K = torch.stack((K, K + 1, K + 2), 0)
K.shape
# torch.Size([3, 2, 2, 2])

通过将核张量K与K+1(K中每个元素加1)和K+2连接起来,构造了一个具有3个输出通道的卷积核。 

corr2d_multi_in_out(X, K)

# tensor([[[ 56.,  72.],
#          [104., 120.]],

#         [[ 76., 100.],
#          [148., 172.]],

#         [[ 96., 128.],
#          [192., 224.]]])

3.3 1 × 1 卷积层

1 × 1卷积,即kh = kw = 1,看起来似乎没有多大意义。毕竟,卷积的本质是有效提取相邻像素间的相关特 征,而1 × 1卷积显然没有此作用。尽管如此,1 × 1仍然十分流行,经常包含在复杂深层网络的设计中。下面, 让我们详细地解读一下它的实际作用。

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

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

我们使用全连接层实现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))

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 × 1卷积层相当于全连接层。
  • 1 × 1卷积层 通常用于调整网络层的通道数量和控制模型复杂性

四 汇聚层 (池化层)

当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。

而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”),所以我们 最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层

本节将介绍汇聚(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性

4.1 最大汇聚层和平均汇聚层

与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动, 为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。池运算是确定性的,我们通常计算汇聚窗口中所有元素的 最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。

汇聚窗口形状为 2 × 2 的最大汇聚层。着色部分是第一个输出元素,以及用于计算这个输出的输入元素: max(0, 1, 3, 4) = 4。

在下面的代码中的pool2d函数,我们实现汇聚层的前向传播。

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

验证二维 最大汇聚层 的输出:

X = 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.]])

验证 平均汇聚层

pool2d(X, (2, 2), 'avg')
# tensor([[2., 3.],
#         [5., 6.]])

4.2 填充和步幅

与卷积层一样,汇聚层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。 下面,我们用深度学习框架中内置的二维最大汇聚层,来演示 汇聚层中填充和步幅的使用。我们首先构造了 一个输入张量X,它有四个维度,其中样本数和通道数都是1。

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.]]]])

深度学习框架中的步幅与汇聚窗口的大小相同。因此,如果我们 使用形状为(3, 3)的汇聚窗口, 那么默认情况下,我们得到的 步幅形状为(3, 3)

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

填充和步幅可以手动设定

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.]]]])

4.3 多个通道

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

X = 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

pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)

# tensor([[[[ 5.,  7.],
#           [13., 15.]],

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

小结:

  • 对于给定输入元素,最大汇聚层会输出该窗口内的最大值,平均汇聚层会输出该窗口内的平均值。
  • 汇聚层的主要优点之一是 减轻卷积层对位置的过度敏感
  • 我们可以指定汇聚层的填充和步幅。
  • 使用最大汇聚层以及大于1的步幅,可减少空间维度(如高度和宽度)。
  • 汇聚层的输出通道数与输入通道数相同

五 卷积神经网络 (LeNet)

本节将介绍LeNet,它是最早发布的卷积神经网络之一。

总体来看,LeNet(LeNet‐5)由两个部分组成:

  • 卷积编码器:由 两个卷积层组成;
  • 全连接层密集块:由 三个全连接层 组成。

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

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

LeNet 代码:

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。通过在每一层打印输出的形状,我们可 以检查模型。

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])

请注意,在整个卷积块中,与上一层相比,每一层特征的高度和宽度都减小了第一个卷积层使用2个像素的 填充,来补偿5 × 5卷积核导致的特征减少。相反,第二个卷积层没有填充,因此高度和宽度都减少了4个像 素。随着层叠的上升,通道的数量从输入时的1个,增加到第一个卷积层之后的6个,再到第二个卷积层之后 的16个。同时,每个汇聚层的高度和宽度都减半。最后,每个全连接层减少维数,最终输出一个维数与结果 分类数相匹配的输出

5.1 模型训练

现在我们已经实现了LeNet,让我们看看LeNet在Fashion‐MNIST数据集上的表现。

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

由于完整的数据集位于 内存中,因此在模型使用GPU计算数据集之前,我们需要将其复制到显存中。

def evaluate_accuracy_gpu(net, data_iter, device=None):  #@save
    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]

以下训练函数假定从高级API创建的模型作为输入,并进行相应的优化。我们使用 Xavier随机初始化模型参数。与全连接层一样,我们使用 交叉熵损失函数和小批量随机梯度 下降

#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    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}, test acc {test_acc: .3f}')
    print(f'{metric[2] * num_epochs / timer.sum(): .1f} examples / sec 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.477, train acc  0.820, test acc  0.814

小结:

  • 在卷积神经网络中,我们组合使用 卷积层、非线性激活函数和汇聚层
  • 为了构造高性能的卷积神经网络,我们通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数
  • 在传统的卷积神经网络中,卷积块编码得到的表征在 输出之前需由一个或多个全连接层进行处理
  • LeNet是最早发布的卷积神经网络之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值