光说不练假把式,现在我们已经积累了那么多的 PyTorch 知识,让我们实践一下吧!
本文从简单的手写数字识别入手,参考了若干文章:
- Handwritten Digit Recognition Using PyTorch — Intro To Neural Networks
- Building Your First PyTorch Solution
1. PyTotch 使用总览
使用 PyTorch 进行深度学习的步骤主要分以下七步:
- 准备数据,包括数据的预处理和封装;
- 模型搭建;
- 选择损失函数;
- 选择优化器;
- 迭代训练;
- 评估模型;
- 保存模型。 其实第 3 - 5 步反而是最简单的,复杂的地方主要集中在第 1、6 步上。在本文中,我们将使用 PyTorch 搭建一个全连接神经网络,用来识别 MNIST 数据框中的手写数字。本文不涉及 GPU 的使用。
2. PyTorch实践
2.0 载入必须的库
1 | import numpy as np |
2.1 准备数据
首先简单介绍一下我们使用的数据库。MNIST 数据库由美国国家标准和科技局推出,包含了 70000 张手写的 0 - 9 的图片,每个数字 7000 张。训练集 60000 张,测试集 10000 张。每张图片都经过预处理,转换成了 28*28 尺寸的一维黑白图像。
2.1.1 获取数据
我们从 torchvision.datasets
获取数据:
1 | transform = transforms.Compose([transforms.ToTensor(), |
transforms
库在下载图像数据时会对数据进行处理。transforms.ToTensor()
将一个维度为 (H x W x C)
的 RGB 文件转换为一个维度为 (C x H x W)
的张量,数值范围从 [0, 255]
转换为 [0, 1]
。transforms.Normalize()
将数据进行标准化处理,使其满足正态分布。transforms.Compose()
将所有转换打包。
设置好下载时的预处理方式,我们就可以下载数据了。第一个参数 './data'
指定了保存的地址,第三个参数 train
的值 True
和 False
分别对应了训练集和测试集。
2.1.2 封装数据
我们不能把 60000 个图片一次全部给神经网络,需要按照 batch 的尺寸分批给。有时候在给之前还要进行随机选择。关于封装数据,请见前文《[DL] PyTorch 折桂 5:PyTorch 模块总览 & torch.utils.data》。这一次我们设置 batch size 为 64.
1 | trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True) |
2.1.3 exploratory data analysis (EDA)
拿到数据以后很重要的步骤是对数据进行基本的了解,包括数量、维度等等。
1 | len(trainset) |
接下来对图片进行可视化:
1 | def show_batch(batch): |
查看图片的尺寸:
1 | 0].shape images[ |
2.2 搭建模型
搭建模型有两种方法:简单但稍欠灵活性的 nn.Sequential
和相反的模块化搭建方法。因为后续还会有实战,这次我们仅仅搭建一个最简单的一层全连接网络。关于搭建模型使用的 nn.Module
的详情请看 《[DL] PyTorch 折桂 6:torch.nn.Module》。
首先来看如何使用 nn.Sequential
:
1 | model = nn.Sequential(nn.Linear(28\*28, 10), |
我们也可以使用模块化方式搭建模型,与 nn.Sequential
方法搭建的模型时等价的:
1 | class Net(nn.Module): |
因为全连接层使用矩阵乘法进行运算,输入应该是一个一维向量,而且输入的最后一维的维度要与全连接层的入度相同。所以我们需要先把一个图片打平(后面训练的时候做),然后将打平后的长度作为全连接层的入度。对于一个分类模型来说,全连接层的出度是分类的数量。因为我们想对 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 | for e in range(epochs): |
我们设置 epochs = 15
运行一下:
1 | Epoch 0 - Training loss: 0.46956403385093215 |
可以看到,模型似乎在学习,在第 10 个 epoch 稳定。
2.6 评估模型
PyTorch 没有 TensorFlow 方便的评估功能,所有评估都要手工定义。
1 | correct_count, all_count = 0, 0 |
详情见代码评论。这里只说一点:在测试的时候我们不需要模型进行更新,关闭模型更新的方法除了代码里的 with torch.no_grad()
以外,还可以使用 model.eval()
。
我们看一下测试结果:
1 | Number Of Images Tested = 10000 |
我们的模型仅仅使用了一个全连接层就获得了 92.2%的准确率,如果我们加入多个全连接层并且使用 dropout 等方法,准确率可以轻松超过 97%。
2.7 保存模型
PyTorch 的模型文件的扩展名一般是 pt
或 pth
。
1 | torch.save(model, './my_mnist_model.pt') |