✍面向读者:软件工程师、架构师、IT人士、设计人员等
✍所属专栏:人工智能工具实践
目录
深度学习技术能力的一个流行演示是图像数据中的对象识别。机器学习和深度学习的对象识别的“hello world”是用于手写数字识别的 MNIST 数据集。在这篇文章中,您将了解如何开发深度学习模型,以在 PyTorch 中的 MNIST 手写数字识别任务中实现近乎最先进的性能。完成本章后,您将了解:
- 如何使用 torchvision 加载 MNIST 数据集
- 如何开发和评估 MNIST 问题的基线神经网络模型
- 如何为 MNIST 实现和评估简单的卷积神经网络
- 如何为 MNIST 实现最先进的深度学习模型
概述
这篇文章分为五个部分;他们是:
- MNIST 手写数字识别问题
- 在 PyTorch 中加载 MNIST 数据集
- 具有多层感知器的基线模型
- MNIST 的简单卷积神经网络
- MNIST 的 LeNet5
MNIST 手写数字识别问题
MNIST 问题是一个可以展示卷积神经网络威力的经典问题。MNIST 数据集由 Yann LeCun、Corinna Cortes 和 Christopher Burges 开发,用于评估手写数字分类问题的机器学习模型。该数据集是根据美国国家标准与技术研究院 (NIST) 提供的许多扫描文档数据集构建的。这就是数据集名称的由来:修改后的 NIST 或 MNIST 数据集。
数字图像取自各种扫描文档,尺寸标准化并居中。这使其成为评估模型的绝佳数据集,使开发人员能够专注于机器学习,而只需最少的数据清理或准备。每个图像都是一个 28×28 像素的灰度正方形(总共 784 像素)。数据集的标准分割用于评估和比较模型,其中使用 60,000 张图像来训练模型,并使用一组单独的 10,000 张图像来测试模型。
该问题的目标是识别图像上的数字。有十个数字(0 到 9)或十个类别需要预测。最先进的预测精度为 99.8%,通过大型卷积神经网络实现。
在 PyTorch 中加载 MNIST 数据集
该torchvision
库是 PyTorch 的姊妹项目,为计算机视觉任务提供专门的函数。其中有一个函数torchvision
可以下载 MNIST 数据集以与 PyTorch 一起使用。第一次调用该函数时会下载数据集并存储在本地,因此以后无需再次下载。下面是一个小脚本,用于下载和可视化 MNIST 数据集训练子集中的前 16 张图像。
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%。您将使用它作为与更复杂的卷积神经网络模型进行比较的基线。首先,让我们检查一下数据是什么样的:
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)
你应该看到:
torch.Size([60000, 28, 28]) torch.Size([60000])
torch.Size([10000, 28, 28]) torch.Size([10000])
训练数据集的结构为实例、图像高度和图像宽度的 3 维数组。对于多层感知器模型,您必须将图像简化为像素向量。在这种情况下,28×28 大小的图像将是 784 像素的输入向量。您可以使用该reshape()
函数轻松完成此转换。
像素值的灰度值在 0 到 255 之间。使用神经网络模型时,对输入值进行一些缩放几乎总是一个好主意。由于该比例是众所周知的并且表现良好,因此您可以通过将每个值除以最大值 255 来非常快速地将像素值标准化到范围 0 和 1。
接下来,您将转换数据集,转换为浮点,并通过缩放浮点值来标准化它们,您可以在下一步中轻松地标准化它们。
# 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
输出目标y_train
和y_test
是 0 到 9 整数形式的标签。这是一个多类分类问题。您可以将这些标签转换为 one-hot 编码,或者将它们保留为整数标签,就像本例一样。您将使用交叉熵函数来评估模型性能,交叉熵函数的 PyTorch 实现可以应用于 one-hot 编码目标或整数标记目标。
您现在已准备好创建简单的神经网络模型。您将在 PyTorchModule
类中定义您的模型。
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)来优化该模型。训练循环如下:
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% 的准确率。
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 数据集多层感知器分类的完整代码。
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
。
...
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)
您需要使用DataLoader
,因为当您从 读取数据时会应用转换DataLoader
。
接下来,定义您的神经网络模型。卷积神经网络比标准多层感知器更复杂,因此您将从使用一个简单的结构开始,该结构使用所有元素来获得最先进的结果。下面总结了网络架构。
- 第一个隐藏层是卷积层,
nn.Conv2d()
。该层将灰度图像转换为 10 个特征图,滤波器尺寸为 5×5,并具有 ReLU 激活函数。这是需要具有上述结构的图像的输入层。 - 接下来是一个池化层,它采用最大值
nn.MaxPool2d()
。它配置的池大小为 2×2,步幅为 1。它的作用是获取每个通道 2×2 像素块中的最大值并将该值分配给输出像素。结果是每个通道有 27×27 像素的特征图。 - 下一层是使用 dropout 的正则化层
nn.Dropout()
。它被配置为随机排除层中 20% 的神经元,以减少过度拟合。 - 接下来是使用 将 2D 矩阵数据转换为向量的层
nn.Flatten
。其输入有 10 个通道,每个通道的特征图大小为 27×27。该层允许由标准的全连接层处理输出。 - 接下来是一个包含 128 个神经元的全连接层。使用ReLU激活函数。
- 最后,输出层有十个神经元用于十个类别。您可以通过对输出应用 softmax 函数将其转换为类似概率的预测。
该模型使用交叉熵损失和 Adam 优化算法进行训练。其实现方式如下:
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))
运行上面的代码需要几分钟并产生以下结果:
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%
这不是最好的结果,但这展示了卷积层的工作原理。
下面是使用简单卷积网络的完整代码。
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))
MNIST 的 LeNet5
之前的模型只有一个卷积层。当然,您可以添加更多内容来制作更深的模型。“LeNet5”模型是最早证明神经网络中卷积层有效性的模型之一。该模型是为了解决 MNIST 分类问题而开发的。正如其名称所示,它具有三个卷积层和两个全连接层,构成模型中的五个可训练层。
在它开发时,使用双曲正切函数作为激活很常见。因此这里使用它。该模型的实现如下:
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 像素的 patch,它取像素的平均值)值而不是取最大值)。但LeNet5模型最显着的特点是它使用strides和padding将图像尺寸从28×28像素减小到1×1像素,同时将通道数从1(灰度)增加到120。
填充是指在图像的边界处添加值为 0 的像素,使其变大一些。如果没有填充,卷积层的输出将小于其输入。步幅参数控制滤波器应移动多少以产生输出中的下一个像素。通常为1以保持相同的大小。如果大于 1,则输出是输入的下采样。因此,您可以在 LeNet5 模型中看到,池化层中使用了步幅 2,以将 28×28 像素图像转换为 14×14。
训练该模型与训练之前的卷积网络模型相同,如下:
...
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))
运行这个你可能会看到:
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%。
以下是完整代码。
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))
MNIST 上的资源
MNIST 数据集得到了很好的研究。以下是您可能想要查看的一些其他资源。
- Yann LeCun、Corinna Cortes 和 Christopher JC Burges。MNIST 手写数字数据库。
- 罗德里戈·贝南森。该图像的类别是什么?分类数据集结果,2016 年。
- 数字识别器:使用著名的 MNIST 数据学习计算机视觉基础知识。卡格尔。
- 休伯特·艾希纳。JavaScript 中用于手写数字识别的神经网络。
概括
在这篇文章中,您发现了 MNIST 手写数字识别问题以及使用 Keras 库在 Python 中开发的深度学习模型,这些模型能够取得出色的结果。通过本章的学习,您了解到:
- 如何使用 torchvision 在 PyTorch 中加载 MNIST 数据集
- 如何将 MNIST 数据集转换为 PyTorch 张量以供卷积神经网络使用
- 如何使用 PyTorch 为 MNIST 创建卷积神经网络模型
- 如何实现MNIST分类的LeNet5模型