【youcans动手学模型】LeNet 模型 MNIST 手写数字识别

news2025/1/20 10:49:32

欢迎关注『youcans动手学模型』系列
本专栏内容和资源同步到 GitHub/youcans


【youcans动手学模型】LeNet 模型 MNIST 手写数字识别

    • 1. LeNet5 卷积神经网络模型
      • 1.1 论文简介
      • 1.2 卷积神经网络
      • 1.3 LeNet5 网络
      • 1.4 模型的运行结果
    • 2. 在 PyTorch 中定义 LeNet5 模型类
      • 2.1 使用 nn.Module 定义网络模型类
      • 2.2 使用 Sequential 容器构造模型类
      • 2.3 使用 Sequential 分层构造模型类
    • 3. 基于 LeNet5 模型的 MNIST 手写数字识别
      • 3.1 PyTorch 建立神经网络模型的基本步骤
      • 3.2 加载 MNIST 数据集
      • 3.3 建立 LeNet5 网络模型
      • 3.4 LeNet5 模型训练
      • 3.5 LeNet5 模型保存与加载
      • 3.6 模型推理
    • 4. LeNet5 模型进行MNIST手写数字识别的完整例程


本文用 PyTorch 实现 LeNet5 网络模型,使用 MNIST 数据集训练模型,进行手写数字识别。

1. LeNet5 卷积神经网络模型

Yann LeCun (2018年获得图灵奖)等在 1998 年发表论文“Gradient-Based Learning Applied to Document Recognition”,提出的 LeNet5 模型是卷积神经网络的开山之作,也是深度学习的第一个里程碑。

论文下载地址:下载1,下载2

在这里插入图片描述

Yann LeCun 等在 1989年创造性地提出了 LeNet 卷积神经网络模型,并使用反向传播算法训练模型,解决识别手写邮政编码问题。1990年,LeNet 模型应用于美国邮政局的邮政编码识别系统,错误率仅为 1%,拒绝率约 9%,成为最早实用的手写数字识别系统。经过多年的迭代改进,成为 1998年论文中的 LeNet-5 网络模型,是最早的卷积神经网络模型。虽然今天看来这个网络非常简单,性能也很差,但其原理仍然是各种卷积神经网络的基础。


1.1 论文简介

使用反向传播算法(BP算法)训练多层神经网络,是梯度学习技术的一个最佳范例。对于给定的网络结构,经过简单的预处理,基于梯度的学习算法就可以用来构造一个复杂的决策面,对高维模式特征(例如手写字符)进行分类。本文回顾了用于手写字符识别的各种方法,并对它们在手写数字识别任务中的性能进行了比较。卷积神经网络(CNN)是专门为处理二维图像而设计的,其性能优于其它方法。

实际应用的文档识别系统由多个模块组成,包括字段提取、分割、识别和语言建模。一种新的学习范式,称为图变换网络(GTN),使用基于梯度的方法对这种多模块系统进行全局训练,以获得最优的总体评价指标。本文介绍了两种在线手写识别系统,实验证明了全局训练的优势以及图变换网络的灵活性。

本文描述了一种用于读取银行支票的图变换网络,使用卷积神经网络字符识别器,结合全局训练技术,准确地识别商业和个人支票。该系统已被商业化部署,每天读取数百万张支票。


1.2 卷积神经网络

本文的最大贡献在于开创性地提出了卷积神经网络(CNN),从而开创了深度学习的研究方向。我们首先回顾原文对于卷积神经网络的介绍。

卷积网络结合了三种架构思想,以确保一定程度的移位、缩放和失真的不变性:局部感受野、共享权重(或权值复制)以及空间或时间子采样。

图 2 显示了一个用于识别字符的典型卷积网络,称为 LeNet-5。输入层接收标准化的字符图像,每层的神经元只接收来自于前一层的较小邻域中的输入(注:这是与全连接层 FC 在网络结构上的根本差异)。

将神经元的输入连接到局部感受野的想法,可以追溯到 60年代早期的感知器,当时 Hubel 和 Wiesel (1981年获得诺贝尔奖)在猫的视觉系统中发现局部敏感的选择性神经元。1979年, 福岛邦彦(2021年获得鲍尔奖)提出了 Neocognitron 视觉学习神经模型,使用了局部连接、平均池化和 ReLU 非线性激活函数。1985年,Hinton(2018年获得图灵奖)等提出了反向传播(Back Propagation,BP)梯度学习算法 。

利用局部感受野,神经元可以提取基本的视觉特征,如定向边缘、端点、角点(或其他信号中的类似特征,如语音频谱)。这些特征由后续的网络层组合,以便检测高阶特征。

如前所述,输入的失真或偏移会导致图像特征的位置发生变化。此外,对图像的一部分有用的基本特征检测器可能对整个图像有用。这种知识可以通过在一组单元(其感受野位于图像上的不同位置)使用相同的权重向量来实现。同一层中的神经元共享一组连接权值。

一个卷积层由几个特征图(分别具有不同的权重向量)组成,因此可以在每个位置提取多个特征。例如 LeNet-5 的第一层有 6个特征图。特征图中的每个单元有 25个输入,连接到上一层的 5×5 区域,称为感受野。每个单元有 25个输入,共有 25个权值参数和 1个阈值参数。每个特征图共享一组权值参数,6个特征图共有 6组不同的权值参数,可以提取 6种不同类型的特征。

这种运算相当于卷积,卷积核就是特征图所使用的连接权值,因此称为卷积网络。


1.3 LeNet5 网络

最初的 LeNet 网络采用 5层网络,包括 2个卷积层、2个池化层(汇聚层)和 1个全连接层,网络结构如下:

  1. 输入层为 28×28 的单通道图像。

  2. C1 卷积层:4个 5×5 卷积核,得到 4 个 24×24 特征图。

  3. S1 池化层: 2×2 的平均池化层,图像高宽减半,得到 4 个 12×12 特征图。

  4. C2 卷积层:12个 5×5 卷积核,得到 12 个 8×8 特征图。

  5. S2 池化层:2×2 的平均池化层,图像高宽减半,,得到 12 个4×4 特征图。

  6. FC 全连接层: 全连接隐藏层,使用 sigmoid 函数。

LeNet-5 网络是 LeNet 网络的改进版,采用 7层网络,包括 3个卷积层、2个池化层和 2个全连接层。

在这里插入图片描述

  1. 输入层为 32×32 的单通道图像。
  2. C1 卷积层:6个 5×5 卷积核,156个训练参数,得到 6个 28×28 特征图。
  3. S2 池化层:2×2 的最大池化层,图像高宽减半,12个训练参数,得到 6个 14×14 特征图。
  4. C3 卷积层:16个 5×5 卷积核,1516个训练参数,得到 16个 10×10 特征图。
  5. S4 池化层:2×2 的最大池化层,图像高宽减半,32个训练参数,得到 16个 5×5 特征图。
  6. C5 卷积层:120个 5×5 卷积核,48120个训练参数,得到 120个 1×1 的特征图,相当于全连接。
  7. F6 全连接层:由 84个神经元组成的隐藏层,10164个训练参数,使用 sigmoid 函数。
  8. F7 输出层:由10 个 RBF 神经元组成的输出层。

特殊地,C3 层与S2层不是全部连接,而是按照作者选取的方式连接,以减少计算和提取更多特征。但在后来的研究中,随着网络越来越复杂,通常不使用这种人工选择方法。

在这里插入图片描述


1.4 模型的运行结果

在这里插入图片描述


2. 在 PyTorch 中定义 LeNet5 模型类

2.1 使用 nn.Module 定义网络模型类

PyTorch 通过 torch.nn 模块提供了高阶的 API,可以从头开始构建网络。

使用 PyTorch 构造神经网络模型,需要运用__call__()__init__()方法定义模型类 Class。nn.Module 是所有神经网络单元(neural network modules)的基类。

PyTorch在 nn.Module 中实现了__call__()方法,在 __call__() 方法中调用 forward 函数。__init__()方法是类的初始化函数,类似于C++的构造函数。

LeNet 模型类的例程如下:

import torch.nn as nn
import torch.nn.functional as F

# 定义 LeNet5 模型类 1
class LeNet5v1(nn.Module):
    def __init__(self):
        super(LeNet5v1, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, 5, padding='same')  # C1: 输入 1,输出 6,卷积核 5x5
        self.pool1 = nn.AvgPool2d(2, 2)  # S2: 卷积核 2x2,步长 2
        self.conv2 = nn.Conv2d(6, 16, 5)  # C3: 输入 6,输出 16,卷积核 5x5
        self.pool2 = nn.AvgPool2d(2, 2)  # S4: 卷积核 2x2,步长 2
        self.flatten = nn.Flatten()  # 展平为一维
        self.linear1 = nn.Linear(400, 120)  # C5: 输入 400,输出 120
        self.linear2 = nn.Linear(120, 84)  # F6: 输入 120,输出 84
        self.linear3 = nn.Linear(84, 10)  # F7: 输入 84,输出 10

    def forward(self, x):
        x = F.relu(self.conv1(x))  # (1,28,28) -> (6,28,28)
        x = self.pool1(x)  # (1,28,28) -> (6,14,14)
        x = F.relu(self.conv2(x))  # (6,14,14) -> (16,10,10)
        x = self.pool2(x)  # (16,10,10) -> (16,5,5)
        x = self.flatten(x)  # (16,5,5) -> (400)
        x = F.relu(self.linear1(x))  # (400) -> (120)
        x = F.relu(self.linear2(x))  # (120) -> (84)
        x = self.linear3(x)  # (84) -> (10)
        return x

使用 print 可以输出 LeNet 模型的结构如下:

LeNet5(
  (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=same)
  (pool1): AvgPool2d(kernel_size=2, stride=2, padding=0)
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (pool2): AvgPool2d(kernel_size=2, stride=2, padding=0)
  (flatten): Flatten(start_dim=1, end_dim=-1)
  (linear1): Linear(in_features=400, out_features=120, bias=True)
  (linear2): Linear(in_features=120, out_features=84, bias=True)
  (linear3): Linear(in_features=84, out_features=10, bias=True)
)

在 LeNet5v1 模型类中,激活函数并没有体现在模型结构中,而是在 forward 函数前向计算时实现的。


2.2 使用 Sequential 容器构造模型类

nn.Sequential() 是一个有序的容器,该类按照传入构造器的顺序,将多个构造函数依次添加到计算图中执行。通过 Sequential 可以构建序列化的模块,使得网络模块的层次更加清晰,便于构造大型和复杂的网络模型。

简单地,以参数列表方式将 LeNet5 模型中的各网络层顺序添加到 Sequential 容器,在 init 方法中直接定义一个model,简化 forward 方法 。

# 定义 LeNet5 模型类 2
class LeNet5v2(nn.Module):
    def __init__(self):
        super(LeNet5v2, self).__init__()
        self.model = nn.Sequential(  # 顺序容器
            nn.Conv2d(1, 6, 5, padding='same'),  # C1: 输入 1,输出 6,卷积核 5x5
            nn.ReLU(),  # 激活函数
            nn.AvgPool2d(2, 2),  # S2: 卷积核 2x2,步长 2
            nn.Conv2d(6, 16, 5),  # C3: 输入 6,输出 16,卷积核 5x5
            nn.ReLU(),  # 激活函数
            nn.AvgPool2d(2, 2),  # S4: 卷积核 2x2,步长 2
            nn.Flatten(),  # 展平为一维
            nn.Linear(400, 120),  # C5: 输入 400,输出 120
            nn.ReLU(),  # 激活函数
            nn.Linear(120, 84),  # F6: 输入 120,输出 84
            nn.ReLU(),  # 激活函数
            nn.Linear(84, 10)  # F7: 输入 84,输出 10
        )

    def forward(self, x):
        x = self.model(x)
        return x

使用 print 可以输出 LeNet v2模型的结构如下:

LeNet5v2(
  (model): Sequential(
    (0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=same)
    (1): ReLU()
    (2): AvgPool2d(kernel_size=2, stride=2, padding=0)
    (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
    (4): ReLU()
    (5): AvgPool2d(kernel_size=2, stride=2, padding=0)
    (6): Flatten(start_dim=1, end_dim=-1)
    (7): Linear(in_features=400, out_features=120, bias=True)
    (8): ReLU()
    (9): Linear(in_features=120, out_features=84, bias=True)
    (10): ReLU()
    (11): Linear(in_features=84, out_features=10, bias=True)
  )
)

在 LeNet5v2 模型类中,激活函数直接体现在模型结构中,模型结构更加清晰完整。


2.3 使用 Sequential 分层构造模型类

通常把卷积、池化和非线性激活函数组合起来,作为一个网络层使用。通过 Sequential 可以逐层构造网络,也可以访问指定的层,并通过 parameters、weights 等参数显示网络的参数和权重。

用 Sequential 容器构造每个网络层,定义 LeNet5 模型类如下。

# 定义 LeNet5 网络结构 3
class LeNet5v3(nn.Module):
    def __init__(self):
        super(LeNet5v3, self).__init__()  # 调用父类的构造函数
        # 卷积池化层
        self.conv_pool1 = nn.Sequential(
            nn.Conv2d(1, 6, 5, padding=2),  # C1: 输入 1,输出 6,卷积核 5x5,填充 2
            nn.ReLU(),  # ReLU 激活函数
            nn.AvgPool2d(2, stride=2)  # S2: 卷积核 2x2,步长 2
        )
        self.conv_pool2 = nn.Sequential(
            nn.Conv2d(6, 16, 5),  # C3: 输入 6,输出 16,卷积核 5x5
            nn.ReLU(),  # ReLU 激活函数
            nn.AvgPool2d(2, stride=2)  # S2: 卷积核 2x2,步长 2
        )
        # 全连接层
        self.fc1 = nn.Sequential(
            nn.Linear(16*5*5, 120),
            nn.ReLU()
        )
        self.fc2 = nn.Sequential(
            nn.Linear(120, 84),
            nn.ReLU()
        )
        # 输出层
        self.out = nn.Sequential(
            nn.Linear(84, 10)
        )

    def forward(self, x):
        x = self.conv_pool1(x)  # (1,28,28) -> (6,14,14)
        x = self.conv_pool2(x)  # (6,14,14) -> (16,5,5)
        x = x.view(x.size(0), -1)  # (16,5,5) -> (400), 展平为一维
        x = self.fc1(x)  # (400) -> (120)
        x = self.fc2(x)  # (120) -> (84)
        x = self.out(x)  # (84) -> (10)
        return x

使用 print 可以输出 LeNet v3模型的结构如下:

LeNet5(
  (conv_pool1): Sequential(
    (0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (1): ReLU()
    (2): AvgPool2d(kernel_size=2, stride=2, padding=0)
  )
  (conv_pool2): Sequential(
    (0): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
    (1): ReLU()
    (2): AvgPool2d(kernel_size=2, stride=2, padding=0)
  )
  (fc1): Sequential(
    (0): Linear(in_features=400, out_features=120, bias=True)
    (1): ReLU()
  )
  (fc2): Sequential(
    (0): Linear(in_features=120, out_features=84, bias=True)
    (1): ReLU()
  )
  (out): Sequential(
    (0): Linear(in_features=84, out_features=10, bias=True)
  )
)

3. 基于 LeNet5 模型的 MNIST 手写数字识别

3.1 PyTorch 建立神经网络模型的基本步骤

使用 PyTorch 建立、训练和使用神经网络模型的基本步骤如下。

  1. 准备数据集(Prepare dataset):加载数据集,对数据进行预处理。
  2. 建立模型(Design the model):实例化模型类,定义损失函数和优化器,确定模型结构和训练方法。
  3. 模型训练(Model trainning):使用训练数据集对模型进行训练,确定模型参数。
  4. 模型推理(Model inferring):使用训练好的模型进行推理,对输入数据预测输出结果。
  5. 模型保存与加载(Model saving/loading):保存训练好的模型,以便以后使用或部署。

以下按此步骤讲解 LeNet5模型的例程。


3.2 加载 MNIST 数据集

通用数据集的样本结构均衡、信息高效,而且组织规范、易于处理。使用通用的数据集训练神经网络,不仅可以提高工作效率,而且便于评估模型性能。

PyTorch 提供了一些常用的图像数据集,预加载在 torchvision.datasets 类中。torchvision 模块实现神经网络所需的核心类和方法, torchvision.datasets 包含流行的数据集、模型架构和常用的图像转换方法。

MNIST 数据集是经典的手写体数字数据集,内容是 0~9 的手写数字,图像是大小为 28*28 的单通道灰度图像。训练集包含 60000 张图像,测试集包含 10000 张图像。

MNIST 数据集可以从官网下载:http://yann.lecun.com/exdb/mnist/ 后使用,也可以使用 datasets 类自动加载(如果本地路径没有该文件则自动下载)。

下载数据集时,使用预定义的 transform 方法进行数据预处理,包括使用 MNIST 数据集的均值和方差对样本数据进行标准化处理,将数据格式转换为张量。注意MNIST是单通道图像,因此均值和方差也是单通道。

大型训练数据集不能一次性加载全部样本来训练,可以使用 Dataloader 类自动加载数据。Dataloader 是一个迭代器,基本功能是传入一个 Dataset 对象,根据参数 batch_size 生成一个 batch 的数据。

    # (1) 将[0,1]的 PILImage 转换为[-1,1]的Tensor
    transform = transforms.Compose([  # Transform Compose of the image
        transforms.ToTensor(),  # 将图像转换为张量 Tensor
        transforms.Normalize(mean=(0.1307,), std=(0.3081,))])  # 标准化

    # (2) 加载 MNIST 数据集
    batch_size = 64
    # 加载 MNIST 数据集, 如果 root 路径加载失败, 则自动在线下载
    # 加载 MNIST 训练数据集, 50000张训练图片
    train_set = torchvision.datasets.MNIST(root='../dataset', train=True,
                                          download=True, transform=transform)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size,
                                              shuffle=True, num_workers=2)

    # 加载 MNIST 验证数据集, 10000张验证图片
    test_set = torchvision.datasets.MNIST(root='../dataset', train=False,
                                          download=True, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=1024,
                                             shuffle=False, num_workers=2)      

3.3 建立 LeNet5 网络模型

建立一个 LeNet5 网络模型进行训练,包括三个步骤:

  • 实例化 LeNet5 模型对象;
  • 设置训练的损失函数;
  • 设置训练的优化器。

torch.nn.functional 模块提供了各种内置损失函数,本例使用交叉熵损失函数 CrossEntropyLoss。

torch.optim 模块提供了各种优化方法,本例使用 Adam 优化器。注意要将 model 的参数 model.parameters() 传给优化器对象,以便优化器扫描需要优化的参数。

    # (3) 实例化 LeNet-5 网络模型
    model = LeNet5()  # 实例化 LeNet-5 网络模型
    print(model)

    criterion = nn.CrossEntropyLoss()  # 交叉熵损失函数
    optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)  # SGD 随机梯度下降优化器

使用 print 可以输出 LeNet 模型的结构如下:

LeNet5(
  (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=same)
  (pool1): AvgPool2d(kernel_size=2, stride=2, padding=0)
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (pool2): AvgPool2d(kernel_size=2, stride=2, padding=0)
  (flatten): Flatten(start_dim=1, end_dim=-1)
  (linear1): Linear(in_features=400, out_features=120, bias=True)
  (linear2): Linear(in_features=120, out_features=84, bias=True)
  (linear3): Linear(in_features=84, out_features=10, bias=True)
)

3.4 LeNet5 模型训练

PyTorch 模型训练的基本步骤是:

  1. 前馈计算模型的输出值;
  2. 计算损失函数值;
  3. 计算权重 weight 和偏差 bias 的梯度;
  4. 根据梯度值调整模型参数;
  5. 将梯度重置为 0(用于下一循环)。

在模型训练过程中,可以使用验证集数据评价训练过程中的模型精度,以便控制训练过程。模型验证就是用验证数据进行模型推理,前向计算得到模型输出,但不反向计算模型误差,因此需要设置 torch.no_grad()。

使用 PyTorch 进行模型训练的例程如下。

    # (4) 训练 LeNet-5 网络模型
    epoch_list = []  # 记录训练轮次
    loss_list = []  # 记录训练集的损失值
    accu_list = []  # 记录验证集的准确率
    num_epochs = 50  # 训练轮次
    for epoch in range(num_epochs):  # 训练轮次 epoch
        running_loss = 0.0  # 每个 epoch 的累加损失值清零
        for step, data in enumerate(train_loader, start=0):  # 迭代器加载数据
            inputs, labels = data  # inputs: [batch, 1, 28, 28] labels: [batch]

            optimizer.zero_grad()  # 损失梯度清零
            outputs = model(inputs)  # 前向传播, [batch, 10]
            loss = criterion(outputs, labels)  # 计算损失函数
            loss.backward()  # 反向传播
            optimizer.step()  # 参数更新

            # 累加训练损失值
            running_loss += loss.item()
            if step%100==99:  # 每 100 个 step 打印一次训练信息
                print("epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))

        # 计算验证集的预测准确率
        with torch.no_grad():  # 验证过程, 不计算损失函数梯度
            outputs_valid = model(valid_images)  # 对验证集进行模型推理 [batch, 10]
        # loss_valid = criterion(outputs_valid, valid_labels)  # 计算验证集损失函数
        pred_labels = torch.max(outputs_valid, dim=1)[1]  # 模型预测的类别 [batch]
        accuracy = torch.eq(pred_labels, valid_labels).sum().item() / valid_size * 100  # 计算准确率
        print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))

        # 记录训练过程的统计数据
        epoch_list.append(epoch)  # 记录迭代次数
        loss_list.append(running_loss)  # 记录训练集上的损失函数
        accu_list.append(accuracy)  # 记录验证集上的损失函数值

程序运行结果如下:

Epoch 0: train loss=1334.7479, accuracy=85.50%
Epoch 1: train loss=318.6170, accuracy=91.30%
Epoch 2: train loss=219.7319, accuracy=94.20%
Epoch 3: train loss=168.5048, accuracy=95.50%

Epoch 47: train loss=10.6612, accuracy=98.80%
Epoch 48: train loss=10.9356, accuracy=98.70%
Epoch 49: train loss=9.7699, accuracy=98.70%

经过 10 轮训练,使用验证集中的 1000 张图片进行验证,模型准确率就接近 98%。继续训练,可以进一步降低训练损失函数值,但验证集的准确率保持在 98~99%。从下图的训练集损失函数和验证集准确率曲线可以看出,对训练集进行 20 轮左右的训练,就可以得到比较满意的模型参数。

在这里插入图片描述


3.5 LeNet5 模型保存与加载

模型训练好以后,将模型保存起来,以便下次使用。PyTorch 中模型保存主要有两种方式,一是保存模型权值,二是保存整个模型。本例使用 model.state_dict() 方法以字典形式返回模型权值,torch.save() 方法将权值字典序列化到磁盘,将模型保存为 .pth 文件。

    # (5) 保存 LeNet5 网络模型
    model_path = "../models/LeNet_MNIST2.pth"
    torch.save(model.state_dict(), model_path)

使用训练好的模型,首先要实例化模型类,然后调用 load_state_dict() 方法加载模型的权值参数。

    # 以下模型加载和模型推理,可以是另一个独立的程序
    # (6) 加载 LeNet5 网络模型进行推理
    # 加载 LeNet 预训练模型
    model_new = LeNet5()  # 实例化 LeNet-5 网络模型
    model_path = "../models/LeNet_MNIST1.pth"
    model_new.load_state_dict(torch.load(model_path))
    model_new.eval()  # 模型推理模式

需要特别注意的是:

(1)PyTorch 中的 .pth 文件只保存了模型的权值参数,而没有模型的结构信息,因此必须先实例化模型对象,再加载模型参数。

(2)模型对象必须与模型参数严格对应,才能正常使用。注意即使都是 LeNet5 模型,模型类的具体定义也可能有细微的区别。如果从一个来源获取模型类的定义,从另一个来源获取模型参数文件,就很容易造成模型结构与参数不能匹配。

(3)无论从 PyTorch 模型仓库加载的模型和参数,或从其它来源获取的预训练模型,或自己训练得到的模型,模型加载的方法都是相同的,也都要注意模型结构与参数的匹配问题。


3.6 模型推理

使用加载的 LeNet5 模型,输入新的图片进行模型推理,可以由模型输出结果确定输入图片所属的类别。

使用测试集数据进行模型推理,可以计算得到测试模型的准确率。注意模型验证集与模型检验集不能交叉使用,但为了简化例程在本程序中未做区分。

    # 模型检测
    model_new.eval()  # 模型推理模式
    correct = 0
    total = 0
    for data in test_loader:  # 迭代器加载测试数据集
        inputs, labels = data
        outputs = model_new(inputs)
        labels_pred = torch.max(outputs, dim=1)[1]  # 模型预测的类别 [batch]
        # _, labels_pred = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += torch.eq(labels_pred, labels).sum().item()
    accuracy = 100. * correct / total
    print("Test accuracy={:.2f}%".format(accuracy))

使用测试集进行模型推理,测试模型准确率为 98.75%。

Test accuracy=98.75%

从测试集中选取几张图片,或者读取新的手写数字图片(注意格式转换和图片大小),输入图片进行模型推理,也可以识别输入图片中的数字。

     # (7) 模型推理识别手写数字
    # imgs, labels = next(iter(test_loader))  # 用 next 返回一个批次的数据
    # print(imgs.shape, labels.shape)  # torch.Size([64, 1, 28, 28])

    plt.figure(figsize=(8, 5))
    plt.suptitle("Inferring using LeNet-5 Model")
    for i, img in enumerate(imgs[:10]):
        out = model_new(imgs[i].unsqueeze(0))  # 增加维度,[1, 1, 28, 28]
        pred = torch.max(out, dim=1)[1]  # 模型预测的类别 torch.Size([1])
        plt.subplot(2, 5, i+1)
        imgNP = img.squeeze().numpy()  # 删除维度,转换为 numpy 数组
        plt.imshow(imgNP, cmap='gray')  # 绘制第 i 张图片
        plt.title("{:d}".format(pred.item()))
        plt.axis('off')
    plt.tight_layout()
    plt.show()

手写数字识别的结果如下。

在这里插入图片描述


4. LeNet5 模型进行MNIST手写数字识别的完整例程

本文的完整例程如下。

# Beginner_LeNet_MNIST_1.py
# LeNet-5 model for beginner with PyTorch
# 经典模型: LeNet 模型 MNIST 手写数字识别
# Copyright: youcans@qq.com
# Crated: Huang Shan, 2023/05/12

# _*_coding:utf-8_*_
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from matplotlib import pyplot as plt

# 定义 LeNet5 网络结构
class LeNet5(nn.Module):
    def __init__(self):
        super(LeNet5, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, 5, padding='same')  # C1: 输入 1,输出 6,卷积核 5x5
        self.pool1 = nn.AvgPool2d(2, 2)  # S2: 卷积核 2x2,步长 2
        self.conv2 = nn.Conv2d(6, 16, 5)  # C3: 输入 6,输出 16,卷积核 5x5
        self.pool2 = nn.AvgPool2d(2, 2)  # S4: 卷积核 2x2,步长 2
        self.flatten = nn.Flatten()  # 展平为一维
        self.linear1 = nn.Linear(400, 120)  # C5: 输入 400,输出 120
        self.linear2 = nn.Linear(120, 84)  # F6: 输入 120,输出 84
        self.linear3 = nn.Linear(84, 10)  # F7: 输入 84,输出 10

    def forward(self, x):
        x = F.relu(self.conv1(x))  # (1,28,28) -> (6,28,28)
        x = self.pool1(x)  # (1,28,28) -> (6,14,14)
        x = F.relu(self.conv2(x))  # (6,14,14) -> (16,10,10)
        x = self.pool2(x)  # (16,10,10) -> (16,5,5)
        x = self.flatten(x)  # (16,5,5) -> (400)
        x = F.relu(self.linear1(x))  # (400) -> (120)
        x = F.relu(self.linear2(x))  # (120) -> (84)
        x = self.linear3(x)  # (84) -> (10)
        return x

if __name__ == '__main__':
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(device)

    # (1) 将[0,1]的 PILImage 转换为[-1,1]的Tensor
    transform = transforms.Compose([  # Transform Compose of the image
        transforms.ToTensor(),  # 将图像转换为张量 Tensor
        transforms.Normalize(mean=(0.1307,), std=(0.3081,))])  # 标准化

    # (2) 加载 MNIST 数据集
    batch_size = 64
    # 加载 MNIST 数据集, 如果 root 路径加载失败, 则自动在线下载
    # 加载 MNIST 训练数据集, 50000张训练图片
    train_set = torchvision.datasets.MNIST(root='../dataset', train=True,
                                          download=True, transform=transform)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size,
                                              shuffle=True, num_workers=2)
    # 加载 MNIST 验证数据集, 10000张验证图片
    test_set = torchvision.datasets.MNIST(root='../dataset', train=False,
                                          download=True, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=1000,
                                             shuffle=False, num_workers=2)
    # 创建生成器,用 next 获取一个批次的数据
    valid_data_iter = iter(test_loader)  # _SingleProcessDataLoaderIter 对象
    valid_images, valid_labels = next(valid_data_iter)  # val_image: [batch, 1, 28, 28] val_label: [batch]
    valid_size = valid_labels.size(0)  # 验证数据集大小,1000
    print(valid_images.shape, valid_labels.shape)  # torch.Size([1000, 1, 28, 28]) torch.Size([1000]

    # (3) 实例化 LeNet-5 网络模型
    model = LeNet5()  # 实例化 LeNet-5 网络模型
    print(model)

    criterion = nn.CrossEntropyLoss()  # 交叉熵损失函数
    optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)  # SGD 随机梯度下降优化器

    # (4) 训练 LeNet-5 网络模型
    epoch_list = []  # 记录训练轮次
    loss_list = []  # 记录训练集的损失值
    accu_list = []  # 记录验证集的准确率
    num_epochs = 50  # 训练轮次
    for epoch in range(num_epochs):  # 训练轮次 epoch
        running_loss = 0.0  # 每个 epoch 的累加损失值清零
        for step, data in enumerate(train_loader, start=0):  # 迭代器加载数据
            inputs, labels = data  # inputs: [batch, 1, 28, 28] labels: [batch]

            optimizer.zero_grad()  # 损失梯度清零
            outputs = model(inputs)  # 前向传播, [batch, 10]
            loss = criterion(outputs, labels)  # 计算损失函数
            loss.backward()  # 反向传播
            optimizer.step()  # 参数更新

            # 累加训练损失值
            running_loss += loss.item()
            # if step%100==99:  # 每 100 个 step 打印一次训练信息
            #     print("epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))

        # 计算验证集的预测准确率
        with torch.no_grad():  # 验证过程, 不计算损失函数梯度
            outputs_valid = model(valid_images)  # 对验证集进行模型推理 [batch, 10]
            # loss_valid = criterion(outputs_valid, valid_labels)  # 计算验证集损失函数
            pred_labels = torch.max(outputs_valid, dim=1)[1]  # 模型预测的类别 [batch]
            accuracy = torch.eq(pred_labels, valid_labels).sum().item() / valid_size * 100  # 计算准确率

        # 记录训练过程的统计数据
        epoch_list.append(epoch)  # 记录迭代次数
        loss_list.append(running_loss)  # 记录训练集上的损失函数
        accu_list.append(accuracy)  # 记录验证集上的损失函数值
        print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))

    # 训练结果可视化
    plt.figure(figsize=(11, 5))
    plt.suptitle("LeNet-5 Model in MNIST")
    plt.subplot(121), plt.title("Train loss")
    plt.plot(epoch_list, loss_list)
    plt.xlabel('epoch'), plt.ylabel('loss')
    plt.subplot(122), plt.title("Valid accuracy")
    plt.plot(epoch_list, accu_list)
    plt.xlabel('epoch'), plt.ylabel('accuracy')
    plt.show()

    # (5) 保存 LeNet5 网络模型
    model_path = "../models/LeNet_MNIST1.pth"
    torch.save(model.state_dict(), model_path)  # 保存模型权值
    
    # # 以下模型加载和模型推理,可以是另一个独立的程序
    # # (6) 加载 LeNet5 网络模型进行推理
    # # 加载 LeNet 预训练模型
    # model_new = LeNet5()  # 实例化 LeNet-5 网络模型
    # model_path = "../models/LeNet_MNIST1.pth"
    # model_new.load_state_dict(torch.load(model_path))
    # model_new.eval()  # 模型推理模式
    #
    # # 模型推理
    # correct = 0
    # total = 0
    # for data in test_loader:  # 迭代器加载测试数据集
    #     inputs, labels = data
    #     outputs = model_new(inputs)
    #     labels_pred = torch.max(outputs, dim=1)[1]  # 模型预测的类别 [batch]
    #     # _, labels_pred = torch.max(outputs.data, 1)
    #     total += labels.size(0)
    #     correct += torch.eq(labels_pred, labels).sum().item()
    # accuracy = 100. * correct / total
    # print("Test accuracy={:.2f}%".format(accuracy))

【本节完】

参考文献:

  1. Yann LeCun, Gradient-based learning applied to document recognition, 1998
  2. https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html

【本节完】


版权声明:
欢迎关注『youcans动手学模型』系列
转发请注明原文链接:
【youcans动手学模型】LeNet 模型 MNIST 手写数字识别
Copyright 2023 youcans, XUPT
Crated:2023-05-16


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/532198.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何设计一个合格的高并发秒杀系统

一、前言 在前面的文章中,详细阐述了建设秒杀系统的目标与存在的挑战,并且简单罗列了如何应对这些挑战的方式。本章,就详细阐述对秒杀系统存在挑战的应对之道,最终构建出兼具高并发、高性能和高可用的秒杀系统。心中不仅了解建设…

中原银行:数据资产管理与运营实践

关 注gzh"大数据食铁兽",了解更多的银行大数据案例 案例简介 中原银行整合内外部数据资源,以业务价值为导向,创新面向敏捷组织的AgileData数据治理方法论,依托工具化、自动化、智能化的治理手段与运营方式,…

CSS--移动web基础

01-移动 Web 基础 谷歌模拟器 模拟移动设备,方便查看页面效果 屏幕分辨率 分类: 物理分辨率:硬件分辨率(出厂设置)逻辑分辨率:软件 / 驱动设置 结论:制作网页参考 逻辑分辨率 视口 作用&a…

CVE-2023-32233 Linux kernel

0x01 漏洞介绍 近日,研究人员发现了Linux内核的NetFilter框架中的新漏洞(CVE-2023-32233)。该漏洞可被本地用户用于将权限提升为root,并完全控制系统。问题的根源在于tfilter nf_tables是如何处理批处理请求的,经过身…

科研热点|严整“打招呼”, 国自然基金项目评审请托行为禁止清单来了~

为严整“打招呼”顽疾,5月16日,国家自然科学基金委员会网站公告征求《国家自然科学基金项目评审请托行为禁止清单(征求意见稿)》公众意见。意见稿分别对科研人员、依托单位、评审专家、自然科学基金委工作人员等列出禁止行为清单。…

怎么建企业网站?这份指南告诉你需要知道的事项

随着数字化时代的到来,企业网站已经成为了企业发展过程中不可或缺的一部分。而对于初创企业来说,怎么建企业网站可能会面临许多挑战和问题。本文将介绍如何利用建站工具来轻松建立自己的企业网站。 第一步:选择适合你的模板 建站工具提供了…

EasyImage简单图床 - 快速搭建私人图床云盘同时远程访问

文章目录 1.前言2. EasyImage网站搭建2.1. EasyImage下载和安装2.2. EasyImage网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2 Cpolar内网穿透本地设置 4. 公网访问测试5. 结语 转发自CSDN远程内网穿透的文章:私人图床 - 本地快速搭建简单的E…

LeetCode·每日一题·1335. 工作计划的最低难度·动态规划

作者:小迅 链接:https://leetcode.cn/problems/minimum-difficulty-of-a-job-schedule/solutions/2271898/dong-tai-gui-hua-zhu-shi-chao-ji-xiang-x-4elt/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者…

出学校干了 3 年外包,人废了···

如果不是女朋友和我提分手,我估计现在还没醒悟。 大专生,19年通过校招进入湖南某软件公司,干了接近4年的功能测试。今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经…

Python—练习题

文档结构 练习题 练习题 1、使用一行代码实现给定列表的奇偶数分离;list_a [11, 22, 45,17,19,21,76,34,28,59] 答案:此处使用 列表推导式实现; >>> >>> list_a [11, 22, 45,17,19,21,76,34,28,59] >>> part…

JWT(Json Web Token)的原理、渗透与防御

(关于JWT kid安全部分后期整理完毕再进行更新~2023.05.16) JWT的原理、渗透与防御 目录 JWT的原理、渗透与防御含义原理JWT的起源传统session认证问题token与session区别JWT的结构与内容 JWT的攻击和渗透敏感信息泄露空密钥破解密钥爆破CVE-2019-7644 J…

BFT 最前线 |文心一言推出内测专用独立App,谷歌发布 AI 网络安全套件,腾讯机器人业务新突破:自研灵巧手与机械臂首次亮相

文 | BFT机器人 01 科大讯飞将发布星火认知大模型 百度聊天机器人文心一言已经启动邀测一个多月。现在网友发现,文心一言已经推出了内测专用独立App,目前仅提供安卓版。用户可以通过键盘打字输入问题,也可以通过语音方式输入问题&#xff0c…

JavaScript简介及常用语法

简介 JavaScript 是互联网上最流行的脚本语言,这门语言可用于 HTML 和 web ,更可广泛用于服务器、 PC 、 笔记本电脑、平板电脑和智能手机等设备。 JavaScript 是一种轻量级的编程语言。 JavaScript 是可插入 HTML 页面的编程代码。 JavaScrip…

基于 KONOS 编写一个部门级的前端框架

01 什么是 konos 现在对于前端框架的定义越来越广泛了,在前端工程化中的某一个环节的特定方案,都可泛称为一个前端框架。 konos 是一个插件化的前端框架基座,如果你对 umi 有所了解的话,可以把它当作一个没有任何功能的 umi core …

RocketMQ单机环境部署

文章目录 1. 前置条件2. 下载源码3. 编译源码4. Rocket MQ启动JVM参数配置5. 启动RocketMQ6. 安装DashBoard7. 测试RocketMQ9. 查看dashboard情况 不太懂RocketMQ基本概念的看我 这篇博客 1. 前置条件 (看我这篇博客,注意版本要改成3.2.*,m…

神经网络:Zero2Hero 2

Zero → \to → Hero : 2 接上篇,Zero → \to → Hero : 1,进一步的扩展模型: 增加输入字符序列的长度,通过多个字符预测下一个字符的概率分布增加模型的深度,通过多层的MLP来学习和预测字符的生成概率增加嵌入层&…

深度学习04-CNN经典模型

简介 卷积神经网络(CNN)是深度学习中非常重要的一种网络结构,它可以处理图像、文本、语音等各种类型的数据。以下是CNN的前4个经典模型 LeNet-5 LeNet-5是由Yann LeCun等人于1998年提出的,是第一个成功应用于手写数字识别的卷积…

【数据结构】线性表之链表

目录 前言一、链表的定义二、链表的分类1. 单向和双向2. 带头和不带头3. 循环和不循环4. 常用(无头单向非循环链表和带头双向循环链表) 三、无头单向非循环链表的接口及实现1. 单链表的接口2. 接口的实现 四、带头双向循环链表接口的及实现1. 双向链表的…

磺酸基-Cy5 羧酸Sulfo-Cy5 COOH分子式C32H37N2KO8S2

Sulfo CY5 COOH是一种有机化合物,属于荧光染料。它具有荧光、稳定、水溶性等特点,因此被应用于分析化学、生物技术、药物研发等领域。Sulfo CY5 COOH的分子式为C32H37N2KO8S2,分子量为680.87。它的荧光波长为670nm,可以通过荧光显…

如何在AD中添加自定义材料单模板

AD默认的材料单格式和常用的格式有点区别,为了减少在材料单格式编辑的工作,决定添加自定义模板到AD的模板中。 1.查找AD模板的安装位置 在AD菜单Reports中,找到“Bill of materials”菜单, 点击后,弹出的窗口中包含了…