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

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



本文用 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: [email protected]
# 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


猜你喜欢

转载自blog.csdn.net/youcans/article/details/130699395
今日推荐