0%

[DL] PyTorch 折桂 11:使用全连接网络进行手写数字识别

光说不练假把式,现在我们已经积累了那么多的 PyTorch 知识,让我们实践一下吧!

本文从简单的手写数字识别入手,参考了若干文章:

1. PyTotch 使用总览

使用 PyTorch 进行深度学习的步骤主要分以下七步:

  1. 准备数据,包括数据的预处理和封装;
  2. 模型搭建;
  3. 选择损失函数;
  4. 选择优化器;
  5. 迭代训练;
  6. 评估模型;
  7. 保存模型。 其实第 3 - 5 步反而是最简单的,复杂的地方主要集中在第 1、6 步上。在本文中,我们将使用 PyTorch 搭建一个全连接神经网络,用来识别 MNIST 数据框中的手写数字。本文不涉及 GPU 的使用。

2. PyTorch实践

2.0 载入必须的库

1
2
3
4
5
6
7
import numpy as np
import matplotlib.pyplot as plt

import torch
import torchvision
from torchvision import datasets, transforms
from torch import nn, optim

2.1 准备数据

首先简单介绍一下我们使用的数据库。MNIST 数据库由美国国家标准和科技局推出,包含了 70000 张手写的 0 - 9 的图片,每个数字 7000 张。训练集 60000 张,测试集 10000 张。每张图片都经过预处理,转换成了 28*28 尺寸的一维黑白图像。

2.1.1 获取数据

我们从 torchvision.datasets 获取数据:

1
2
3
4
5
6
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)),
])

trainset = datasets.MNIST('./data', download=True, train=True, transform=transform)
testset = datasets.MNIST('./data', download=True, train=False, transform=transform)

transforms 库在下载图像数据时会对数据进行处理。transforms.ToTensor() 将一个维度为 (H x W x C) 的 RGB 文件转换为一个维度为 (C x H x W) 的张量,数值范围从 [0, 255] 转换为 [0, 1]transforms.Normalize() 将数据进行标准化处理,使其满足正态分布。transforms.Compose() 将所有转换打包。

设置好下载时的预处理方式,我们就可以下载数据了。第一个参数 './data' 指定了保存的地址,第三个参数 train 的值 TrueFalse 分别对应了训练集和测试集。

2.1.2 封装数据

我们不能把 60000 个图片一次全部给神经网络,需要按照 batch 的尺寸分批给。有时候在给之前还要进行随机选择。关于封装数据,请见前文《[DL] PyTorch 折桂 5:PyTorch 模块总览 & torch.utils.data》。这一次我们设置 batch size 为 64.

1
2
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=True)

2.1.3 exploratory data analysis (EDA)

拿到数据以后很重要的步骤是对数据进行基本的了解,包括数量、维度等等。

1
2
3
4
>>> len(trainset)
60000
>>> len(testset)
10000

接下来对图片进行可视化:

1
2
3
4
5
6
7
8
def show_batch(batch):
im = torchvision.utils.make_grid(batch)
plt.imshow(np.transpose(im.numpy(), (1, 2, 0)))

dataiter = iter(trainloader)
images, labels = dataiter.next()

show_batch(images)


查看图片的尺寸:

1
2
>>> images[0].shape
torch.Size([1, 28, 28])

2.2 搭建模型

搭建模型有两种方法:简单但稍欠灵活性的 nn.Sequential 和相反的模块化搭建方法。因为后续还会有实战,这次我们仅仅搭建一个最简单的一层全连接网络。关于搭建模型使用的 nn.Module 的详情请看 《[DL] PyTorch 折桂 6:torch.nn.Module》

首先来看如何使用 nn.Sequential

1
2
model = nn.Sequential(nn.Linear(28\*28, 10),
nn.LogSoftmax(dim=1))

我们也可以使用模块化方式搭建模型,与 nn.Sequential 方法搭建的模型时等价的:

1
2
3
4
5
6
7
8
9
10
11
12
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc = nn.Linear(28*28, 10)
self.softmax = nn.LogSoftmax(dim=1)

def forward(self, x):
x = self.fc(x)
x = self.softmax(x)
return x

model = Net() # 模块化构建神经网络需要先实例化

因为全连接层使用矩阵乘法进行运算,输入应该是一个一维向量,而且输入的最后一维的维度要与全连接层的入度相同。所以我们需要先把一个图片打平(后面训练的时候做),然后将打平后的长度作为全连接层的入度。对于一个分类模型来说,全连接层的出度是分类的数量。因为我们想对 0 - 9 一共 10 个数字进行分类,所以出度为 10。

重点说一下 nn.LogSoftmax。softmax 是分类任务中常用的手段,将目标值转化为范围为 $(0,1)$ 之间的,所有值的和为 1 的概率分布。因为 softmax 的计算公式为 $\frac{e^{x_i}}{\sum e^{x_i}}$,如果 $x$ 过小会导致它的概率极小,超过 Python 的数据精度而为 0,所以我们一般对概率分布取对数,将概率分布转化为 $(-\infty,0)$ 的分布。nn.LogSoftmax 就是进行这个运算的的类。nn.LogSoftmax 对应的损失函数为 nn.NLLLoss。因为我们要对第二维进行似然估计,所以明确 dim=1

关于损失函数的具体介绍请看《[DL] PyTorch 折桂 9:损失函数》

2.3 损失函数

上面已经提到了,如果使用 nn.LogSoftmax 作为模型的输出,损失函数应该使用 nn.NLLLoss。这里不多赘述。

1
criterion = nn.NLLLoss()

2.4 优化器

《[DL] PyTorch 折桂 10:torch.optim》 提到,通常我们可以无脑选择 torch.optim.Adam。但是 MNIST 手写数字识别是一个非常简单的任务,使用 SGD 足矣,这次我们使用 torch.optim.SGD

1
optimizer = optim.SGD(model.parameters(), lr=0.003, momentum=0.9)

2.5 迭代训练

每一次的训练的流程如下:

  1. 优化器的导数记录清零;
  2. 使用模型得到预测值;
  3. 使用损失函数计算预测值与真实值之间的损失;
  4. 反向传播;
  5. 更新权重。

因为优化器里的导数是累积的,在每一轮训练中都要执行第一步,在第四步前还是第五步后无所谓。此外可以根据需要加入进度报告。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for e in range(epochs):
running_loss = 0
for images, labels in trainloader:
images = images.view(images.shape[0], -1) # 打平数据

optimizer.zero_grad() # 导数清零
output = model(images) # 得到预测值
loss = criterion(output, labels) # 计算损失

loss.backward() # 反向传播
optimizer.step() # 优化权重

running_loss += loss.item()
else:
print("Epoch {} - Training loss: {}".format(e, running_loss/len(trainloader)))

我们设置 epochs = 15 运行一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Epoch 0 - Training loss: 0.46956403385093215
Epoch 1 - Training loss: 0.33383476238515075
Epoch 2 - Training loss: 0.31380205746017287
Epoch 3 - Training loss: 0.3029081499509847
Epoch 4 - Training loss: 0.2956352831442346
Epoch 5 - Training loss: 0.2905418651063305
Epoch 6 - Training loss: 0.2873595496103453
Epoch 7 - Training loss: 0.2838163320173714
Epoch 8 - Training loss: 0.2816906003777915
Epoch 9 - Training loss: 0.27968987264930567
Epoch 10 - Training loss: 0.27738782898512987
Epoch 11 - Training loss: 0.2752566468248616
Epoch 12 - Training loss: 0.27330243247134217
Epoch 13 - Training loss: 0.2733802362513949
Epoch 14 - Training loss: 0.27021837964463336

可以看到,模型似乎在学习,在第 10 个 epoch 稳定。

2.6 评估模型

PyTorch 没有 TensorFlow 方便的评估功能,所有评估都要手工定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
correct_count, all_count = 0, 0

for images,labels in valloader:
for i in range(len(labels)):
img = images[i].view(1, 784) # 取出第 i 个元素

with torch.no_grad(): # 关闭求导功能
logps = model(img) # 获得预测值


ps = torch.exp(logps) # 将对数去掉
pred_label = torch.argmax(ps[0]) # 获得最大概率的标签
true_label = labels[i] # 获得真实数据的标签

if(true_label == pred_label): # 如果预测与真实值相同则加 1
correct_count += 1

all_count += 1

print("Number Of Images Tested =", all_count)
print("Model Accuracy =", (correct_count/all_count))

详情见代码评论。这里只说一点:在测试的时候我们不需要模型进行更新,关闭模型更新的方法除了代码里的 with torch.no_grad() 以外,还可以使用 model.eval()
我们看一下测试结果:

1
2
Number Of Images Tested = 10000
Model Accuracy = 0.9222

我们的模型仅仅使用了一个全连接层就获得了 92.2%的准确率,如果我们加入多个全连接层并且使用 dropout 等方法,准确率可以轻松超过 97%。

2.7 保存模型

PyTorch 的模型文件的扩展名一般是 ptpth

1
torch.save(model, './my_mnist_model.pt')

欢迎关注我的其它发布渠道