使用 PyTorch 中的 LeNet5 模型进行手写数字识别

深度学习技术能力的流行演示是图像数据中的目标识别。机器学习和深度学习目标识别的“hello world”是手写数字识别的 MNIST 数据集。在这篇文章中,您将学习如何在 PyTorch 中开发一个深度学习模型,以在 MNIST 手写数字识别任务上实现接近最先进的性能。完成本章后,您将了解:

  • 如何使用 torchvision 加载 MNIST 数据集
  • 如何开发和评估 MNIST 问题的基线神经网络模型
  • 如何实现和评估 MNIST 的简单卷积神经网络
  • 如何实现 MNIST 的最先进深度学习模型

通过我的《用PyTorch进行深度学习》一书来启动你的项目。它提供了包含可用代码自学教程


让我们开始吧。

使用 PyTorch 中的 LeNet5 模型进行手写数字识别
图片来源:Johnny Wong。保留部分权利。

概述

本文分为五个部分,它们是:

  • MNIST 手写数字识别问题
  • 在 PyTorch 中加载 MNIST 数据集
  • 基于多层感知器的基线模型
  • 用于 MNIST 的简单卷积神经网络
  • 用于 MNIST 的 LeNet5

MNIST 手写数字识别问题

MNIST 问题是一个经典的例子,可以展示卷积神经网络的强大能力。MNIST 数据集由 Yann LeCun、Corinna Cortes 和 Christopher Burges 开发,用于评估机器学习模型在手写数字分类问题上的性能。该数据集是从国家标准与技术研究院(NIST)提供的多个扫描文档数据集中构建的。这就是数据集名称的由来,即 Modified NIST 或 MNIST 数据集。

数字图像取自各种扫描文档,经过大小标准化和居中处理。这使其成为评估模型的优秀数据集,允许开发人员专注于机器学习,而无需进行最少的数据清理或准备。每张图像都是 28×28 像素的正方形(总共 784 像素)灰度图像。数据集的标准划分用于评估和比较模型,其中 60,000 张图像用于训练模型,另外 10,000 张图像用于测试。

这个问题的目标是识别图像上的数字。有十个数字(0到9)或十个类别需要预测。最先进的预测精度达到了 99.8% 的水平,这是通过大型卷积神经网络实现的。

想开始使用PyTorch进行深度学习吗?

立即参加我的免费电子邮件速成课程(附示例代码)。

点击注册,同时获得该课程的免费PDF电子书版本。

在 PyTorch 中加载 MNIST 数据集

torchvision 库是 PyTorch 的一个姊妹项目,它为计算机视觉任务提供专门的函数。torchvision 中有一个函数可以下载 MNIST 数据集供 PyTorch 使用。该数据集在第一次调用此函数时下载并本地存储,因此您将来无需再次下载。下面是一个小脚本,用于下载和可视化 MNIST 数据集训练子集中的前 16 张图像。

基于多层感知器的基线模型

您是否真的需要像卷积神经网络这样复杂的模型才能在 MNIST 上获得最佳结果?使用一个简单的单隐藏层神经网络模型就可以获得不错的结果。在本节中,您将创建一个简单的多层感知器模型,其准确率达到 99.81%。您将以此作为基线,与更复杂的卷积神经网络模型进行比较。首先,让我们看看数据是什么样的。

您应该看到

训练数据集的结构是一个三维数组,包含实例、图像高度和图像宽度。对于多层感知器模型,您必须将图像缩小为像素向量。在这种情况下,28×28 大小的图像将是 784 像素的输入向量。您可以使用 reshape() 函数轻松完成此转换。

像素值是介于 0 到 255 之间的灰度值。在使用神经网络模型时,对输入值进行缩放几乎总是一个好主意。由于比例是众所周知且表现良好的,因此您可以通过将每个值除以最大值 255,非常快速地将像素值归一化到 0 到 1 的范围。

在以下步骤中,您将转换数据集,将其转换为浮点数,并通过缩放浮点值对其进行归一化,并在下一步中轻松进行归一化。

输出目标 y_trainy_test 是 0 到 9 之间的整数形式的标签。这是一个多类别分类问题。您可以将这些标签转换为独热编码,或者像本例一样保留它们为整数标签。您将使用交叉熵函数评估模型性能,PyTorch 的交叉熵函数实现可以应用于独热编码目标或整数标签目标。

现在,您已准备好创建简单的神经网络模型。您将在 PyTorch Module 类中定义模型。

该模型是一个简单的神经网络,包含一个隐藏层,其神经元数量与输入(784个)相同。隐藏层中的神经元使用 ReLU 激活函数。该模型的输出是 **logits**,这意味着它们是可以使用 softmax 函数转换为类似概率值的实数。您无需显式应用 softmax 函数,因为交叉熵函数将为您完成此操作。

您将使用随机梯度下降算法(学习率设置为 0.01)来优化此模型。训练循环如下:

MNIST 数据集很小。这个例子应该在一分钟内完成,输出如下。这个简单的网络可以产生 92% 的准确率。

下面是上述 MNIST 数据集上的多层感知器分类的完整代码。

用于 MNIST 的简单卷积神经网络

既然您已经了解了如何使用多层感知器模型对 MNIST 数据集进行分类。让我们继续尝试卷积神经网络模型。在本节中,您将为 MNIST 创建一个简单的 CNN,该 CNN 将演示如何使用现代 CNN 实现的所有方面,包括卷积层、池化层和 dropout 层。

在 PyTorch 中,卷积层应该作用于图像。图像的张量应该是像素值,其维度为 (样本数、通道数、高度、宽度),但是当您使用 PIL 等库加载图像时,像素通常以 (高度、宽度、通道数) 的维度数组形式呈现。转换成正确的张量格式可以通过 torchvision 库中的转换函数完成。

您需要使用 DataLoader,因为转换是在从 DataLoader 读取数据时应用的。

接下来,定义您的神经网络模型。卷积神经网络比标准多层感知器更复杂,因此您将首先使用一个简单的结构,该结构使用了实现最先进结果的所有元素。下面总结了网络架构。

  1. 第一个隐藏层是卷积层,nn.Conv2d()。该层将灰度图像转换为 10 个特征图,滤波器大小为 5×5,并使用 ReLU 激活函数。这是期望具有上述结构图像的输入层。
  2. 接下来是一个使用最大池化的池化层,nn.MaxPool2d()。它配置为 2×2 的池化大小,步幅为 1。它的作用是在每个通道的 2×2 像素块中取最大值,并将该值分配给输出像素。结果是每个通道 27×27 像素的特征图。
  3. 下一层是使用 Dropout 的正则化层,nn.Dropout()。它被配置为随机排除层中 20% 的神经元,以减少过拟合。
  4. 接下来是一个使用 nn.Flatten 将 2D 矩阵数据转换为向量的层。它的输入有 10 个通道,每个通道的特征图大小为 27×27。该层允许输出由标准的、全连接层处理。
  5. 接下来是一个具有128个神经元的全连接层。使用了ReLU激活函数。
  6. 最后,输出层有十个神经元,对应十个类别。您可以通过在其上应用 softmax 函数将输出转换为类似概率的预测。

该模型使用交叉熵损失和 Adam 优化算法进行训练。其实现如下:

运行上述代码需要几分钟,并产生以下结果:

结果不是最好的,但这展示了卷积层的工作原理。

下面是使用简单卷积网络的完整代码。

用于 MNIST 的 LeNet5

前面的模型只有一个卷积层。当然,您可以添加更多层以构建更深的模型。神经网络中卷积层有效性的最早演示之一是“LeNet5”模型。该模型旨在解决 MNIST 分类问题。它有三个卷积层和两个全连接层,共同构成模型中的五个可训练层,正如其名称所示。

在它被开发的时候,使用双曲正切函数作为激活函数是很常见的。因此在这里也使用了它。该模型实现如下:

与之前的模型相比,LeNet5 没有 Dropout 层(因为 Dropout 层是在 LeNet5 诞生几年后才发明的),并且使用平均池化而不是最大池化(即,对于一个 2×2 像素的补丁,它取像素值的平均值而不是最大值)。但 LeNet5 模型最显著的特点是它使用步幅和填充来将图像大小从 28×28 像素缩小到 1×1 像素,同时将通道数从一个(灰度)增加到 120。

填充意味着在图像边界添加值为 0 的像素,使其稍微变大。如果没有填充,卷积层的输出将小于其输入。步幅参数控制滤波器应移动多少才能在输出中生成下一个像素。通常为 1 以保持相同大小。如果大于 1,则输出是输入的**下采样**。因此,您在 LeNet5 模型中看到,池化层中使用步幅 2 将 28×28 像素的图像转换为 14×14。

训练这个模型与训练之前的卷积神经网络模型相同,如下所示:

运行此代码,您可能会看到:

在这里,我们实现了超过 98% 的准确率。

以下是完整的代码。

MNIST 资源

MNIST 数据集经过深入研究。以下是您可能希望查阅的一些额外资源。

总结

在这篇文章中,您发现了 MNIST 手写数字识别问题以及使用 Keras 库在 Python 中开发的深度学习模型,这些模型能够取得出色的结果。通过本章的学习,您了解了:

  • 如何使用 torchvision 在 PyTorch 中加载 MNIST 数据集
  • 如何将 MNIST 数据集转换为 PyTorch 张量以供卷积神经网络使用
  • 如何使用 PyTorch 为 MNIST 创建卷积神经网络模型
  • 如何实现用于 MNIST 分类的 LeNet5 模型

开始使用PyTorch进行深度学习!

Deep Learning with PyTorch

学习如何构建深度学习模型

...使用新发布的PyTorch 2.0库

在我的新电子书中探索如何实现
使用 PyTorch进行深度学习

它提供了包含数百个可用代码自学教程,让你从新手变成专家。它将使你掌握:
张量操作训练评估超参数优化等等...

通过动手练习开启你的深度学习之旅


查看内容

使用 PyTorch 中的 LeNet5 模型进行手写数字识别的 4 条回复

  1. Patrick Wood 2023 年 5 月 31 日 下午 1:45 #

    我认为测试数据集的 train 应该是 False。

    • Eduardo Chico 2023 年 11 月 12 日 上午 7:18 #

      哈哈,我来这里正是为了提出同样的看法

  2. Michael Hogg 2024 年 12 月 20 日 上午 8:54 #

    感谢这篇文章。

    关于数据转换的一个问题
    transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0,), (128,)),
    ])

    torchvision 文档中说 ToTensor 将图像数据从 [0,255] 范围转换为 [0.0,1.0]。这很好,但是应用 Normalize((0,), (128,)) 就没有意义了,因为这意味着除以 128。

    我见过其他建议使用
    transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0.5,), (0.5,)),
    ])

    这将把图像数据缩放到 [-1,1]。

    • James Carmichael 2024 年 12 月 20 日 上午 9:54 #

      你好 Michael…你的观察完全正确,所提供的转换管道中 Normalize((0,), (128,)) 的使用与数据归一化的通常意图不符。

      以下是详细说明:

      ### ToTensor() 的作用
      ToTensor() 转换将图像数据从 PIL 图像或 NumPy 数组格式的范围 [0, 255] 转换为 PyTorch 张量,范围为 [0.0, 1.0]

      ### Normalize(mean, std) 的作用
      Normalize((mean,), (std,)) 应用转换
      \[
      \text{output} = \frac{\text{input} – \text{mean}}{\text{std}}
      \]
      其中
      mean 是要减去的平均值。
      std 是要除以的标准差值。

      ### 为什么 Normalize((0,), (128,)) 没有意义
      由于数据已经通过 ToTensor() 缩放到 [0.0, 1.0],因此应用 Normalize((0,), (128,)) 意味着将缩放后的值除以 128。这很可能是对归一化过程的错误或误解。相反,您通常会通过使用适当的 meanstd 值将数据归一化,使其均值为 0.0,标准差为 1.0

      ### 推荐的 Normalize((0.5,), (0.5,))
      – 使用 Normalize((0.5,), (0.5,)) 将数据从 [0.0, 1.0] 缩放到 [-1.0, 1.0]
      – 首先,减去 0.5 将数据平移到 [-0.5, 0.5]
      – 然后,除以 0.5 将其缩放到 [-1.0, 1.0]

      这对于输入数据在 [-1, 1] 范围内表现良好的模型来说是一种常见做法。

      ### LeNet-5 的正确转换
      如果您遵循标准的 LeNet-5 实现,最好使用:
      python
      transform = torchvision.transforms.Compose([
      torchvision.transforms.ToTensor(),
      torchvision.transforms.Normalize((0.5,), (0.5,)), # 将数据缩放到 [-1, 1]
      ])

      或者,如果您希望基于数据集的统计数据(例如 MNIST)进行零均值和单位方差归一化,您可以使用:
      python
      transform = torchvision.transforms.Compose([
      torchvision.transforms.ToTensor(),
      torchvision.transforms.Normalize((0.1307,), (0.3081,)), # MNIST 的均值和标准差
      ])

      这些统计数据(mean=0.1307std=0.3081)是根据 MNIST 数据集本身计算得出的,并在实践中被广泛使用。

      ### 总结
      – **Normalize((0,), (128,))** 是不正确的,除非有特定的理由要除以 128(在这里不太可能)。
      – 使用 **Normalize((0.5,), (0.5,))** 进行缩放到 [-1, 1],或者如果需要零均值、单位方差归一化,则计算特定于数据集的 meanstd
      – 根据您使用的 LeNet-5 实现的输入期望选择归一化方法。

发表回复

Machine Learning Mastery 是 Guiding Tech Media 的一部分,Guiding Tech Media 是一家领先的数字媒体出版商,专注于帮助人们了解技术。访问我们的公司网站以了解更多关于我们的使命和团队的信息。