自学内容网 自学内容网

LeNet (经典卷积神经网络) + 代码实现 ——笔记2.10《动手学深度学习》

目录

0. 前言

1. LeNet

2. 模型训练

3. 小结


0. 前言

:label:sec_lenet

通过之前几节,我们学习了构建一个完整卷积神经网络的所需组件。 回想一下,之前我们将softmax回归模型( :numref:sec_softmax_scratch)和多层感知机模型( :numref:sec_mlp_scratch)应用于Fashion-MNIST数据集中的服装图片。 为了能够应用softmax回归和多层感知机,我们首先将每个大小为28×28的图像展平为一个784维的固定长度的一维向量,然后用全连接层对其进行处理。 而现在,我们已经掌握了卷积层的处理方法,我们可以在图像中保留空间结构。 同时,用卷积层代替全连接层的另一个好处是:模型更简洁、所需的参数更少。

本节将介绍LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。 这个模型是由AT&T贝尔实验室的研究员Yann LeCun(杨立昆 - Facebook首席人工智能科学家、纽约大学教授)在1989年提出的(并以其命名),目的是识别图像 :cite:LeCun.Bottou.Bengio.ea.1998中的手写数字。 当时,Yann LeCun 发表了第一篇通过反向传播成功训练卷积神经网络的研究,这项工作代表了十多年来神经网络研究开发的成果。

当时,LeNet取得了与支持向量机(support vector machines)性能相媲美的成果,成为监督学习的主流方法。 LeNet被广泛用于自动取款机(ATM)机中,帮助识别处理支票的数字。 时至今日,一些自动取款机仍在运行Yann LeCun和他的同事Leon Bottou在上世纪90年代写的代码呢!LeNet在邮递行业识别手写邮编上,也有广泛应用。

1. LeNet

总体来看,(LeNet(LeNet-5)由两个部分组成:)(卷积编码器和全连接层密集块)

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

该架构如 :numref:img_lenet所示。

:label:img_lenet

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

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

通过下面的LeNet代码,可以看出用深度学习框架实现此类模型非常简单。我们只需要实例化一个Sequential块并将需要的层连接在一起。

In [1]:

import torch
from torch import nn
from d2l import torch as d2l
​
# 这个net网络的设计,是LeNet论文的核心
net = nn.Sequential(  # 输入是的形状是(n,1,28,28),n是批量batch,1是通道,28×28是高和宽(行和列)
    nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),  # Conv2d默认步幅stride=1
    nn.AvgPool2d(kernel_size=2, stride=2),  # 均值池化层
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),  # 为了得到非线性,两个Conv2d都加入激活函数Sigmoid()
    nn.AvgPool2d(kernel_size=2, stride=2),  # 输出都是4D的张量,实际形状是(n,16,5,5),n指的是样本数
    nn.Flatten(),  # nn.Flatten()保持最外层第0维(批量维n),将其余维度的张量展平为一维张量,方便作为全连接输入
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),  # 16 * 5 * 5 = 400是输入元素个数,120是输出元素个数
    nn.Linear(120, 84), nn.Sigmoid(),  # 1×120个输入,1×84个输出;还得nn.Sigmoid()激活一下
    nn.Linear(84, 10))  # 最后降到10个输出,对应着Fashion-MNIST数据集的10个(分类)类别

我们对原始模型做了一点小改动,去掉了最后一层的高斯激活。除此之外,这个网络与最初的LeNet-5一致。

下面,我们将一个大小为28×28的单通道(黑白)图像通过LeNet。通过在每一层打印输出的形状,我们可以[检查模型],以确保其操作与我们期望的 :numref:img_lenet_vert一致。

:label:img_lenet_vert

In [2]:

X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32) # 随机输入张量;这里的含义是有 1 个样本
                                                          # 1个通道(MNIST数据集是灰度图像),图像大小为28x28
# 遍历神经网络中的每一层 —— 方便看到每一层的输入到输出的形状变化
for layer in net:  # 咱前面通过nn.Sequential()把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])
  • 可以观察到,每一层卷积,都增加了通道数,缩小了层的尺寸,实际信息增多了。
  • 全连接也在降低元素量,400降到120,84,最后降到10。

卷积神经网络的核心思想,就是:

  • 不断增加通道数,缩小层的尺寸(压缩高宽),提炼出有价值的空间模式,即识别(学习)空间的Pattern,最后通过mlp(多层感知机)把所有识别出来的模式拿出来,训练得到最后的输出(识别与分类)。

这个思想到今天还在CNN中使用,只是具体实现的细节会有所不同,有所优化。

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

2. 模型训练

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

In [3]:

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

虽然卷积神经网络的参数较少,但与深度的多层感知机相比,它们的计算成本仍然很高,因为每个参数都参与更多的乘法。 通过使用GPU,可以用它加快训练。

为了进行评估,我们需要[] :numref:sec_softmax_scratch中描述的(evaluate_accuracy函数进行轻微的修改)。《动手学深度学习》笔记1.5——Softmax 回归→损失函数→图片分类数据集_softmax 回归 + 损失函数-CSDN博客

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

In [4]:

def evaluate_accuracy_gpu(net, data_iter, device=None): #@save    # 这个函数后面会复用
    """使用GPU计算模型在数据集上的精度"""  # 因此这里用if来识别net是从零实现的,还是torch.nn定义的
    if isinstance(net, nn.Module):  # 如果net是用torch.nn定义的
        net.eval()  # 设置为评估模式
        if not device:  # 如果传入参数里没有给定device是GPU还是CPU
            device = next(iter(net.parameters())).device  # 获取模型参数所在的设备(如CPU或特定的GPU)
    # 用累加器Accumulator()来累加:正确预测的数量,总预测的数量
    metric = d2l.Accumulator(2)  # Accumulator实例中创建了2个变量,分别用于存储正确预测的数量和预测的总数量
    with torch.no_grad():
        for X, y in data_iter:
            if isinstance(X, list):  # 如果是list(列表)
                # BERT微调所需的(之后将介绍)
                X = [x.to(device) for x in X]
            else:
                X = X.to(device)  # 便把X挪到 GPU or CPU 上
            y = y.to(device)  
            metric.add(d2l.accuracy(net(X), y), y.numel())  # y.numel(): 总预测的数量
            # 使用Accumulator()类中的add方法分别将正确预测数和总预测数在metric[0]和metric[1]中累加
            # accuracy()也是之前在softmax_scratch(从零实现)里定义过的,将预测结果与真实结果比较
    return metric[0] / metric[1]  # 正确率

[为了使用GPU,我们还需要一点小改动]。 与 :numref:sec_softmax_scratch中定义的train_epoch_ch3不同,在进行正向和反向传播之前,我们需要将每一小批量数据移动到我们指定的设备(例如GPU)上。

如下所示,训练函数train_ch6也类似于 :numref:sec_softmax_scratch中定义的train_ch3。 由于我们将实现多层神经网络,因此我们将主要使用高级API。 以下训练函数假定从高级API创建的模型作为输入,并进行相应的优化。 我们使用在 :numref:subsec_xavier中介绍的Xavier随机初始化模型参数。 与全连接层一样,我们使用交叉熵损失函数和小批量随机梯度下降。

In [5]:


#@save # 后面基本上都用这个函数训练模型,保存到d2l库中
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    """用GPU训练模型(在第六章定义)"""  # 和train_ch3没有本质区别(只是增添了用gpu训练的功能)
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:  # 如果是全连接层,或卷积层上的权重
            nn.init.xavier_uniform_(m.weight)  # xavier根据输入和输出的神经元初始化权重,防止梯度消失/爆炸
    net.apply(init_weights)  # apply指的是对net里每一个参数m,都run一下函数init_weights(m)
    print('training on', device)  # 打印一下,看看是不是真的在gpu上跑;以防在CPU上跑,半天不出结果
    # ※ 与train_ch3的不同点 1 ※
    net.to(device)  # 把net里的参数都搬到gpu的内存上※
    optimizer = torch.optim.SGD(net.parameters(), lr=lr)  # 随机梯度下降,创建实例optimizer
    loss = nn.CrossEntropyLoss()  # 多分类问题,用交叉熵,与softmax没区别
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],  # Animator输出动画效果,不重要,不细讲
                            legend=['train loss', 'train acc', 'test acc'])
    timer, num_batches = d2l.Timer(), len(train_iter)  # d2l.Timer()库用于测量代码执行的时间
    for epoch in range(num_epochs):  # 对每个epoch迭代
        # 用累加器创建一个[0.,0.,0.]包含三个元素的变量,分别存储:
        metric = d2l.Accumulator(3)  # 训练损失之和,训练准确率之和,样本数
        net.train()
        for i, (X, y) in enumerate(train_iter):  # 每次迭代拿一个batch出来
            timer.start()  # 开始计时
            optimizer.zero_grad()  # 初始梯度设为 0
            # ※ 与train_ch3的不同点 2 ※
            X, y = X.to(device), y.to(device)  # 挪到gpu上※
            y_hat = net(X)  # nn.Sequential()定义的net的前向操作
            l = loss(y_hat, y)  # 计算损失
            l.backward()  # 计算梯度
            optimizer.step()  # 用torch.optim.SGD中的step(),结合之前传入的lr,来更新权重参数
            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)  # 调用评估正确率函数_gpu版
        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模型]。

In [6]:

lr, num_epochs = 1.8, 18  # 原论文超参数是“lr, num_epochs = 0.9, 10”
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.333, train acc 0.874, test acc 0.856
50517.8 examples/sec on cuda:0

  • 看结果两根虚线,test和train没有明显的gap,没有太多 over fitting(过拟合),意味着 Underfitting(欠拟合),就是说模型不够强。
  • 55498.4 examples/sec,速度很快,Fashion-MNIST数据集一共就50000个样本,一下子就能扫完;这里看上去没那么快主要是算acc(正确率)扫一遍数据做验证,以及打印图的时候python的性能限制不高效,出图慢。
  • 对比mlp, LeNet的过拟合会比较少,因为LeNet模型比较小。
  • 还可以调参,课本里原先的超参数“lr, num_epochs = 0.9, 10”是沿用杨立昆1989年在原论文里用的,在当时的MNIST数据集上是合理的,但在Fashion-MNIST上会有很多不合理,Fashion-MNIST比MNIST更难,超参数可以调大一些,比如“lr, num_epochs = 1.8, 18”。

3. 小结

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

原文地址:https://blog.csdn.net/weixin_57972634/article/details/143085140

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!