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

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

在 PyTorch 中使用 LeNet5 模型进行手写数字识别
推荐:将NSDT场景编辑器加入你的3D工具链
3D工具集:NSDT简石数字孪生

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

完成本章后,您将了解:

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

概述

这篇文章分为五个部分;它们是:

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

MNIST手写数字识别问题

MNIST问题是一个经典的问题,可以证明卷积神经网络的强大功能。MNIST数据集由Yann LeCun,Corinna Cortes和Christopher Bges开发,用于评估手写数字分类问题的机器学习模型。该数据集由美国国家标准与技术研究院 (NIST) 提供的许多扫描文档数据集构建而成。这就是数据集名称的来源,即修改后的 NIST 或 MNIST 数据集。

数字图像取自各种扫描文档,大小标准化并居中。这使其成为评估模型的出色数据集,使开发人员能够以最少的数据清理或准备工作专注于机器学习。每个图像都是一个 28×28 像素的正方形(总共 784 像素)的灰度。数据集的标准拆分用于评估和比较模型,其中 60,000 张图像用于训练模型,另一组 10,000 张图像用于测试模型。

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

在 PyTorch 中加载 MNIST 数据集

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

1
2
3
4
5
6
7
8
9
10
import matplotlib.pyplot as plt
import torchvision
 
train = torchvision.datasets.MNIST('./data', train=True, download=True)
 
fig, ax = plt.subplots(4, 4, sharex=True, sharey=True)
for i in range(4):
    for j in range(4):
        ax[i][j].imshow(train.data[4*i+j], cmap="gray")
plt.show()

具有多层感知器的基线模型

你真的需要一个像卷积神经网络这样的复杂模型来获得MNIST的最佳结果吗?使用具有单个隐藏层的非常简单的神经网络模型可以获得良好的结果。在本节中,您将创建一个简单的多层感知器模型,其精度为 99.81%。您将以此作为基线,与更复杂的卷积神经网络模型进行比较。首先,让我们检查一下数据的外观:

1
2
3
4
5
6
7
8
9
10
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
 
# Load MNIST data
train = torchvision.datasets.MNIST('data', train=True, download=True)
test = torchvision.datasets.MNIST('data', train=True, download=True)
print(train.data.shape, train.targets.shape)
print(test.data.shape, test.targets.shape)

您应该看到:

1
2
torch.Size([60000, 28, 28]) torch.Size([60000])
torch.Size([10000, 28, 28]) torch.Size([10000])

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

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

在下文中,您将转换数据集,将其转换为浮点型,并通过缩放浮点值对其进行规范化,并且可以在下一步中轻松对其进行规范化。

1
2
3
4
5
# each sample becomes a vector of values 0-1
X_train = train.data.reshape(-1, 784).float() / 255.0
y_train = train.targets
X_test = test.data.reshape(-1, 784).float() / 255.0
y_test = test.targets

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

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

1
2
3
4
5
6
7
8
9
10
11
class Baseline(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(784, 784)
        self.act1 = nn.ReLU()
        self.layer2 = nn.Linear(784, 10)
        
    def forward(self, x):
        x = self.act1(self.layer1(x))
        x = self.layer2(x)
        return x

该模型是一个简单的神经网络,具有一个隐藏层,其神经元数量与输入相同(784)。整流器激活功能用于隐藏层中的神经元。该模型的输出是 logits,这意味着它们是实数,可以使用 softmax 函数转换为类似概率的值。您没有显式应用 softmax 函数,因为交叉熵函数会为您执行此操作。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
model = Baseline()
 
optimizer = optim.SGD(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
loader = torch.utils.data.DataLoader(list(zip(X_train, y_train)), shuffle=True, batch_size=100)
 
n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in loader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    y_pred = model(X_test)
    acc = (torch.argmax(y_pred, 1) == y_test).float().mean()
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

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

1
2
3
4
5
6
7
8
9
10
Epoch 0: model accuracy 84.11%
Epoch 1: model accuracy 87.53%
Epoch 2: model accuracy 89.01%
Epoch 3: model accuracy 89.76%
Epoch 4: model accuracy 90.29%
Epoch 5: model accuracy 90.69%
Epoch 6: model accuracy 91.10%
Epoch 7: model accuracy 91.48%
Epoch 8: model accuracy 91.74%
Epoch 9: model accuracy 91.96%

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
 
# Load MNIST data
train = torchvision.datasets.MNIST('data', train=True, download=True)
test = torchvision.datasets.MNIST('data', train=True, download=True)
 
# each sample becomes a vector of values 0-1
X_train = train.data.reshape(-1, 784).float() / 255.0
y_train = train.targets
X_test = test.data.reshape(-1, 784).float() / 255.0
y_test = test.targets
 
class Baseline(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(784, 784)
        self.act1 = nn.ReLU()
        self.layer2 = nn.Linear(784, 10)
        
    def forward(self, x):
        x = self.act1(self.layer1(x))
        x = self.layer2(x)
        return x
    
model = Baseline()
 
optimizer = optim.SGD(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
loader = torch.utils.data.DataLoader(list(zip(X_train, y_train)), shuffle=True, batch_size=100)
 
n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in loader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    y_pred = model(X_test)
    acc = (torch.argmax(y_pred, 1) == y_test).float().mean()
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

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

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

在PyTorch中,卷积层应该处理图像。图像的张量应该是具有尺寸(样本、通道、高度、宽度)的像素值,但是当您使用 PIL 等库加载图像时,像素通常显示为维度数组(高度、宽度、通道)。可以使用库中的转换完成到正确张量格式的转换。torchvision

1
2
3
4
5
6
7
8
9
...
transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0,), (128,)),
])
train = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
test = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(train, shuffle=True, batch_size=100)
testloader = torch.utils.data.DataLoader(test, shuffle=True, batch_size=100)

您需要使用,因为从 .DataLoaderDataLoader

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(1, 10, kernel_size=5, stride=1, padding=2)
        self.relu1 = nn.ReLU()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=1)
        self.dropout = nn.Dropout(0.2)
        self.flat = nn.Flatten()
        self.fc = nn.Linear(27*27*10, 128)
        self.relu2 = nn.ReLU()
        self.output = nn.Linear(128, 10)
        
    def forward(self, x):
        x = self.relu1(self.conv(x))
        x = self.pool(x)
        x = self.dropout(x)
        x = self.relu2(self.fc(self.flat(x)))
        x = self.output(x)
        return x
    
model = CNN()
 
optimizer = optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
 
n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in trainloader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    acc = 0
    count = 0
    for X_batch, y_batch in testloader:
        y_pred = model(X_batch)
        acc += (torch.argmax(y_pred, 1) == y_batch).float().sum()
        count += len(y_batch)
    acc = acc / count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

运行上述程序需要几分钟,并生成以下内容:

1
2
3
4
5
6
7
8
9
10
Epoch 0: model accuracy 81.74%
Epoch 1: model accuracy 85.38%
Epoch 2: model accuracy 86.37%
Epoch 3: model accuracy 87.75%
Epoch 4: model accuracy 88.00%
Epoch 5: model accuracy 88.17%
Epoch 6: model accuracy 88.81%
Epoch 7: model accuracy 88.34%
Epoch 8: model accuracy 88.86%
Epoch 9: model accuracy 88.75%

这不是最好的结果,但这证明了卷积层是如何工作的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
 
# Load MNIST data
transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0,), (128,)),
])
train = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
test = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(train, shuffle=True, batch_size=100)
testloader = torch.utils.data.DataLoader(test, shuffle=True, batch_size=100)
 
class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(1, 10, kernel_size=5, stride=1, padding=2)
        self.relu1 = nn.ReLU()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=1)
        self.dropout = nn.Dropout(0.2)
        self.flat = nn.Flatten()
        self.fc = nn.Linear(27*27*10, 128)
        self.relu2 = nn.ReLU()
        self.output = nn.Linear(128, 10)
        
    def forward(self, x):
        x = self.relu1(self.conv(x))
        x = self.pool(x)
        x = self.dropout(x)
        x = self.relu2(self.fc(self.flat(x)))
        x = self.output(x)
        return x
    
model = CNN()
 
optimizer = optim.Adam(model.parameters())
loss_fn = nn.CrossEntropyLoss()
 
n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in trainloader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    acc = 0
    count = 0
    for X_batch, y_batch in testloader:
        y_pred = model(X_batch)
        acc += (torch.argmax(y_pred, 1) == y_batch).float().sum()
        count += len(y_batch)
    acc = acc / count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

LeNet5 for MNIST

以前的模型只有一个卷积层。当然,您可以添加更多内容以制作更深层次的模型。卷积层在神经网络中有效性的最早证明之一是“LeNet5”模型。该模型旨在解决MNIST分类问题。它有三个卷积层和两个全连接层,构成了模型中的五个可训练层,顾名思义。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2)
        self.act1 = nn.Tanh()
        self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
 
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0)
        self.act2 = nn.Tanh()
        self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
 
        self.conv3 = nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0)
        self.act3 = nn.Tanh()
 
        self.flat = nn.Flatten()
        self.fc1 = nn.Linear(1*1*120, 84)
        self.act4 = nn.Tanh()
        self.fc2 = nn.Linear(84, 10)
        
    def forward(self, x):
        # input 1x28x28, output 6x28x28
        x = self.act1(self.conv1(x))
        # input 6x28x28, output 6x14x14
        x = self.pool1(x)
        # input 6x14x14, output 16x10x10
        x = self.act2(self.conv2(x))
        # input 16x10x10, output 16x5x5
        x = self.pool2(x)
        # input 16x5x5, output 120x1x1
        x = self.act3(self.conv3(x))
        # input 120x1x1, output 84
        x = self.act4(self.fc1(self.flat(x)))
        # input 84, output 10
        x = self.fc2(x)
        return x

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
model = LeNet5()
 
optimizer = optim.Adam(model.parameters())
loss_fn = nn.CrossEntropyLoss()
 
n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in trainloader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    acc = 0
    count = 0
    for X_batch, y_batch in testloader:
        y_pred = model(X_batch)
        acc += (torch.argmax(y_pred, 1) == y_batch).float().sum()
        count += len(y_batch)
    acc = acc / count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

运行此操作,您可能会看到:

1
2
3
4
5
6
7
8
9
10
Epoch 0: model accuracy 89.46%
Epoch 1: model accuracy 93.14%
Epoch 2: model accuracy 94.69%
Epoch 3: model accuracy 95.84%
Epoch 4: model accuracy 96.43%
Epoch 5: model accuracy 96.99%
Epoch 6: model accuracy 97.14%
Epoch 7: model accuracy 97.66%
Epoch 8: model accuracy 98.05%
Epoch 9: model accuracy 98.22%

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

以下是完整的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
 
# Load MNIST data
transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0,), (128,)),
])
train = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
test = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(train, shuffle=True, batch_size=100)
testloader = torch.utils.data.DataLoader(test, shuffle=True, batch_size=100)
 
class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2)
        self.act1 = nn.Tanh()
        self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
 
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0)
        self.act2 = nn.Tanh()
        self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
 
        self.conv3 = nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0)
        self.act3 = nn.Tanh()
 
        self.flat = nn.Flatten()
        self.fc1 = nn.Linear(1*1*120, 84)
        self.act4 = nn.Tanh()
        self.fc2 = nn.Linear(84, 10)
        
    def forward(self, x):
        # input 1x28x28, output 6x28x28
        x = self.act1(self.conv1(x))
        # input 6x28x28, output 6x14x14
        x = self.pool1(x)
        # input 6x14x14, output 16x10x10
        x = self.act2(self.conv2(x))
        # input 16x10x10, output 16x5x5
        x = self.pool2(x)
        # input 16x5x5, output 120x1x1
        x = self.act3(self.conv3(x))
        # input 120x1x1, output 84
        x = self.act4(self.fc1(self.flat(x)))
        # input 84, output 10
        x = self.fc2(x)
        return x
 
model = LeNet5()
 
optimizer = optim.Adam(model.parameters())
loss_fn = nn.CrossEntropyLoss()
 
n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in trainloader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    acc = 0
    count = 0
    for X_batch, y_batch in testloader:
        y_pred = model(X_batch)
        acc += (torch.argmax(y_pred, 1) == y_batch).float().sum()
        count += len(y_batch)
    acc = acc / count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

3D建模学习工作室 翻译整理,转载请注明出处!

NSDT场景编辑器 | NSDT 数字孪生 | GLTF在线编辑器 | 3D模型在线转换 | UnrealSynth虚幻合成数据生成器 | 3D模型自动纹理化工具
2023 power by nsdt©鄂ICP备2023000829号