一文详解用PyTorch解决手写数字问题

您所在的位置:网站首页 mnist手写数字识别原理 一文详解用PyTorch解决手写数字问题

一文详解用PyTorch解决手写数字问题

2023-03-27 21:01| 来源: 网络整理| 查看: 265

1. MNIST 数据集介绍

前面介绍了能够对连续值进行预测的简单线性回归模型,并使用梯度下降算法进行迭代求解。当然深度学习不仅能够处理连续值预测的回归问题,还能够处理预测固定离散值的分类问题。分类问题的一个典型应用就是自动识别图像中物体的种类,手写数字识别是常见的图像识别任务。

为了方便统一测试和评估算法,Yann LeCun 发布了名为 MNIST 的手写数字图片的数据集,MNIST 数据集包含 0~9 共 10 种数字的手写图片,每种数字一共有 7000 张图片,采集自不同书写风格的真实手写图片,一共 70000 张图片。70000 张手写数字图片使用 train_test_split 方法划分为 60000 张训练集(Training Set)和 10000 张测试集(Test Set)。如果将 70000 张手写数字图片全部作为模型的训练集,模型很可能过拟合,模型在训练集上表现很好,但是给模型一个新的数字图片进行预测,模型预测的结果会非常不好。

MNIST 数据集中每张图片都被缩放到 (28 x 28) 的大小,同时只保留了灰度信息。下图为 MNIST 数据集的样例图片。

下图为某个手写数字 8 的图片表示示意图。

我们现在还没有学到将这种图片表示的数字矩阵直接作为输入输入到网络中。简单的方法是将这种数字矩阵的特征图打平成特征向量,打平操作非常简单。比如下面将一个 (2 x 2) 的矩阵的打平成 (4, ) 的向量。

\left[\begin{matrix} 0 & 1 \\ 2 & 3 \end{matrix} \right] => \begin{bmatrix} 0 \\ 1 \\ 2 \\ 3 \end{bmatrix} \\

将 (28 x 28) 的数字矩阵打平成 (784, ) 的特征向量,打平后的特征没有了位置信息。由于特征比较多,如果依然使用 for 循环等进行计算会耗费大量的时间,而使用 Numpy 模块中的矩阵运算可以利用 Numpy 中的并行化操作大幅度提高运算效率。打平后的图片特征为 (784, ) 的向量,如果想要使用矩阵运算需要为向量增加一个维度变成 (1 x 784) 的矩阵,此时的 1 代表的图片的数量,即输入的X = [图片数量, 图片特征]矩阵。

上一小节介绍了简单的线性模型 y = wx + b ,显然一个简单的线性模型是不可能分类手写数字识别任务的,我们通常将几个线性函数进行嵌套:

H_1 = XW_1 + b_1\\ H_2 = H_1W_2 + b_2 \\ H_3 = H_2W_3 + b_3

如果将 X = [1, d_0] 输入到嵌套线性函数中,各个变量的维度变化如下:

[1, d_0] @ [d_0, d_1] + [d_1] = [1, d_1] \\ [1, d_1] @ [d_1, d_2] + [d_2] = [1, d_2] \\ [1, d_2] @ [d_2, d_3] + [d_3] = [1, d_3]

将上面的线性函数结合在一起:

H_3 = \{[XW_1 + b_1]W_2 + b_2 \}W_3 + b_3\\可以发现即使使用多个嵌套的线性函数结果依然是线性函数,但是处理这种复杂的图片识别,光靠线性的关系是不能够学习更深层次的知识,所以我们需要添加非线性的部分:激活函数。本小节使用 ReLU 激活函数,ReLU 也是现在比较流行的激活函数。 加入 ReLU 激活函数的嵌套函数表达式:

H_3 = relu(\{relu([relu(XW_1 + b_1)]W_2 + b_2) \}W_3 + b_3) \\三个非线性模型叠加依然是非线性模型,而且使模型的表达能力进一步增强。

如何将类别标签进行编码呢?

如果将类别标签转换成数字编码,即用一个数字来表示标签信息,此时的输出只需要一个节点就可以表示网络的预测类别,即 d_3 = 1。但是数字编码有一个很大的问题,数字之间存在天然的大小关系,手写数字图片的 0~9 十个类别之间并没有大小关系,但是如果使用数字编码标签信息,可能导致模型迫使去学习 0 < 1 < 2 这种数字大小的关系; 如果将类别标签转码成 one-hot 编码,即用一个包含 0 和 1 的向量来表示标签信息,向量的维度为标签类别的个数,由于手写数字识别的类别为 0~9 的十个类别,此时的输出需要十个节点,即 d_3 = 10。假设某个手写图片属于类别 i,即手写图片中的数字为 i,只需要一个长度为 10 的向量 y,向量 y 的索引号为 i 的元素设置为1,其余位置设置为 0;

使用 one-hot 编码类别标签没有使用数字编码中的问题,所以通常类别标签使用这种 one-hot 编码的方式。

有了这些准备接下来就可以使用梯度下降算法进行迭代求解,由于标签采用 one-hot 编码方式,预测输出 H_3 和真实标签 y 都是一个十维的向量,我们需要找到使得 H_3 和 y 之间距离最小的参数 W, b,衡量两个向量之间距离最简单的方式是使用欧式距离:\sum(H_3 - y)^2,此时的损失函数 L = \sum(H_3 - y)^2。需要注意参数 W, b 包含了:

第一个非线性函数的 W_1, b_1第二个非线性函数的 W_2, b_2第三个非线性函数的 W_3, b_32. MNIST 数据集实战

下面来简单回顾上一小节的嵌套非线性模型:

H_1 = relu(XW_1 + b_1)H_2 = relu(H1W_2 + b_2)H_3 = f(H_2W_3 + b_3), 模型最后一层的激活函数不会是 relu 激活函数,需要根据你的具体任务来选择合适的激活函数。比如使用二分类的 Sigmoid 或多分类的 SoftMax(当然多个二分类也可以用于处理多分类)。由于这里只是简单的演示整个训练流程,所以为了简单本小节最后一层不添加任何激活函数。

对 MNIST 手写数字识别进行分类大致分为四个步骤,这四个步骤也是训练大多数深度学习模型的基本步骤:

加载数据集(Load data)构建模型(Build Model)训练(Train)测试(Test)

不过在这之前我们需要构建一个 utils.py 文件,其中包含着三个工具方法:

plot_curve(loss_list) 方法绘制损失函数曲线;plot_image(x, label, name)方法显示 6 张手写数字图片以及对应的数字标签;one_hot(label, depth = 10)方法将 0~9 的数字编码标签转换为 one-hot 编码的标签。比如将数字编码 5 转换为 one-hot 编码为 [0,0,0,0,1,0,0,0,0,0](由于此时假设为十个类别,因此 one-hot 编码后的向量维度为 10 维)。import torch from matplotlib import pyplot as plt def plot_curve(loss_list): """ 根据存放loss值的列表绘制曲线 """ plt.plot(range(len(loss_list)), loss_list, color = 'blue') # 添加图例并放置在右上角 plt.legend(['train_loss'], loc = 'upper right') plt.xlabel('step') # 设置横坐标轴名称 plt.ylabel('train_loss') # 设置纵坐标轴名称 plt.show() def plot_image(x, label, name): """ 显示6张手写数字图片以及对应的数字标签 """ for i in range(6): plt.subplot(2, 3, i + 1) plt.tight_layout() plt.imshow(x[i][0] * 0.3081 + 0.1307, cmap='gray', interpolation='none') plt.title("{}: {}".format(name, label[i].item())) plt.xticks([]) plt.yticks([]) plt.show() def one_hot(label, depth = 10): ''' 将数字编码标签label转换为one-hot编码y ''' y = torch.zeros(label.size(0), depth) idx = torch.LongTensor(label).view(-1, 1) y.scatter_(dim = 1, index = idx, value = 1) return y2.1 加载数据集

MNIST 是比较重要和经典的数据集,目前常用的机器学习和深度学习框架都内置了 MNIST 数据集,通过几行代码就可以自动下载、管理以及加载 MNIST 数据集。基于 PyTorch 有很多工具集,比如:处理自然语言的 torchtext,处理音频的 torchaudio 和 处理图像视频的 torchvision,这些工具集可以独立于 PyTorch 的使用。MNIST 数据集属于图像,我们可以在 torchvision.datasets 包中加载 MNIST。加载的 MNIST 数据集是 ndarray 数组类型,因此我们需要将其转换成 Tensor。实验证明输入数据在 0 附近均匀分布,神经网络模型会有所提升(在本小节的神经网络模型架构下,对数据进行标准化准确率能够提升 10%),因此我们还需要对 MNIST 数据集进行标准化的转换,torchvision.transforms 包提供了这些转换方法。

import torchvision train_data = torchvision.datasets.MNIST('mnist_data', train=True, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize( (0.1307,), (0.3081,)) ])) print(len(train_data)) # 60000 # 训练集中的第1张手写数字图片以及对应的标签 X_train_0, label_train_0 = train_data[0] print(X_train_0.shape) # torch.Size([1, 28, 28]) print(label_train_0) # 5

在 torchvision.datasets 中有很多类似 MNIST 的数据集,下面来简单介绍 torchvision.datasets.MNIST 中的一些参数:

'mnist_data':MNIST 数据集所在的文件夹,我直接设置在当前路径。如果你也传入 'mnist_data',你会在当前路径下发现一个 mnist_data 的文件夹;train = True:可选参数。如果设置为 True,则从 ./mnist_data/MNIST/processed/training.pt 中加载训练集(使用 len(train_data) 可以看出共有 60000 张手写数字图片)。如果设置为 False,则从 ./mnist_data/MNIST/processed/test.pt 中加载测试集;download = True:可选参数。如果设置为 True,且路径下没有 MNIST 数据集,则会从网络上下载 MNIST 数据集,如果路径下已经存在 MNIST 数据集,则不会再次下载;transform = torchvision.transforms.Compose:transform 进行数据的预处理操作: ToTensor:将 ndarray 数组转换为 Tensor 数据类型;Normalize:进行数据的标准化,即减去均值除以方差,此时均值 0.1307 和方差 0.3081 是 MNIST 数据集计算好的数据,直接使用即可;

加载完了 MNIST 数据集中的训练集,我们可以设置 train = False 来加载 10000 张测试集。

import torchvision test_data = torchvision.datasets.MNIST('mnist_data', train = False, download = True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize( (0.1307,), (0.3081,)) ])) print(len(test_data)) # 10000 # 测试集中的第1张手写数字图片以及对应的标签 X_test_0, label_test_0 = test_data[0] print(X_test_0.shape) # torch.Size([1, 28, 28]) print(label_test_0) # 7

至此 60000 张训练集以及 10000 张测试集都加载进来了,不过我们通常使用更为方便的数据集加载器 DataLoader,DataLoader 结合了数据集和取样器,提供了多个线程处理数据集,并且里面提供了很多方便处理数据集的功能。DataLoader 在 torch.utils.data 包下。

import torch import utils # 加载我们自己写的工具类 batch_size = 512 train_loader = torch.utils.data.DataLoader(train_data, batch_size = batch_size, # batch_size shuffle = True) # 是否打乱数据集 test_loader = torch.utils.data.DataLoader(test_data, batch_size = batch_size, # 测试集只用于验证模型性能不需要打乱数据集 shuffle = False) # 迭代器加载数据集,每次都加载batch_size个 # X: [batch_size, channel, width, hight] # label: 数字编码 X, label = next(iter(train_loader)) print(X.shape, label.shape, X.min(), label.max()) # torch.Size([512, 1, 28, 28]) torch.Size([512]) tensor(-0.4242) tensor(9) utils.plot_image(X, label, 'image sample')2.2 构建模型

自定义一个模型可以通过继承 torch.nn.Moudle 类来实现,在 __init__ 构造函数中来定义声明模型中的各个层,在 forward 方法中构建各个层的连接关系实现模型前向传播的过程。在 PyTorch 这种高级的深度学习框架中帮我们实现了很多常见的网络层以及激活函数。PyTorch 中的网络层通常在 torch.nn 包下,而激活函数通常在 torch.nn.functional 包下。

from torch import nn from torch.nn import functional as F # 2. 构建模型 class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 使用PyTorch提供的Linear线性层 self.fc1 = nn.Linear(28 * 28, 256) self.fc2 = nn.Linear(256, 64) self.fc3 = nn.Linear(64, 10) def forward(self, X): # X: [batch_size, 1, 28, 28] # H1 = relu(XW1 + b1) X = F.relu(self.fc1(X)) # H2 = relu(H1W2 + b2) X = F.relu(self.fc2(X)) # H3 = H2W3 + b3 X = self.fc3(X) return X

此时构建的是一个四层的全连接神经网络,由于我们将 (28 x 28) 的手写数字图片像素矩阵打平成了 (784, ) 的特征向量,并且将对应的数字标签转换成了 one-hot 十个维度的向量,因此全连接神经网络的输入层和输出层的节点数都是固定的分别为 784 和 10。通常我们把输入层和输出层之外的层称为隐藏层,而隐藏层的层数以及每一层的节点个数都是需要我们人为指定的超参数。

2.3 训练模型import utils # 加载我们自己写的工具类 from torch import optim # 其中包含各种优化算法 epochs = 3 # 迭代的轮数 train_loss = [] # 用于存储训练过程中的损失值,方便可视化 net = Net() # 实例化模型 # SGD随机梯度下降法 optimizer = optim.SGD(net.parameters(), lr = 0.01, momentum = 0.9) for epoch in range(epochs): # 对整个数据集迭代3遍 # 每一次for循环都会获取batch_size个样本 for batch_idx, (X_train, label_train) in enumerate(train_loader): # X_train.shape = torch.Size([batch_size, 1, 28, 28]) # label_train.shape = torch.Size([batch_size]) # 根据前面的学习,需要: # 1. X_train打平成[batch_size, 784](X_train.shape[0] = batch_size) X_train = X_train.reshape(X_train.shape[0], 28 * 28) # 2. label_train数字编码转换为one_hot编码 y_train = utils.one_hot(label_train) # 前向传播过程 # X_train: [batch_size, 784] -> out: [batch_size, 10] out = net(X_train) # 计算当前损失值,由于输出节点没有使用任何激活函数 # 因此使用简单的均方差MSE loss = F.mse_loss(out, y_train) # 由于PyTorch会把计算的梯度值进行累加,因此每次循环需要将梯度值置为0 optimizer.zero_grad() # 计算loss关于net.parameters()的梯度 loss.backward() # 使用梯度下降算法更新net.parameters()参数值: # theta' = theta - 学习率 * 梯度 optimizer.step() train_loss.append(loss) # 为了可视化,保存当前loss值 if batch_idx % 10 == 0: # 每隔10个batch打印一次 print("epoch: ", epoch, "batch_idx: ", batch_idx, "loss: ", loss.item()) ''' epoch: 0 batch_idx: 0 loss: 0.12745174765586853 epoch: 0 batch_idx: 10 loss: 0.09659640491008759 ... epoch: 2 batch_idx: 100 loss: 0.03306906297802925 epoch: 2 batch_idx: 110 loss: 0.03607671707868576 '''

每一步都有非常详细的代码注释,这里不再过多赘述。**torch.optim 包中实现了各种优化算法,SGD 是随机梯度下降法,**简单来看看 torch.optim.SGD(net.parameters(), lr = 0.01, momentum = 0.9) 中的三个参数:

net.parameters():模型网络中的所有待优化参数,由于使用 PyTorch 提供的 Linear 层,其中的优化参数都为我们定义好了。如果使用我们自己定义的层,需要在定义待优化参数的时候将 required_grad 参数指定为 True;lr = 0.01:指定学习率为 0.01;momentum = 0.9:动量因子,简单来说给梯度一个冲量帮助跳出局部极小值点或者一些梯度等于 0 的点。具体可以看推荐阅读中的文章;

为了可视化将训练过程中的 loss 值保存在 train_loss 列表中,只需要调用我们自己实现的工具类中的 utils.plot_curve(train_loss) 方法即可绘制训练过程中的 loss 值曲线。

utils.plot_curve(train_loss)2.4 测试模型

接下来用测试集来对训练好的模型进行评估。评估模型非常简单,只需要将测试集中的手写数字图片矩阵打平之后输入到训练好的模型中,对于每个测试集样本,模型都会输出一个十维的向量,使用 argmax 方法输出十维向量 10 个值中最大值所在位置的索引。

# 使用测试集来评估训练好的模型 total_correct = 0 for X_test, label_test in test_loader: # 打平X_test X_test = X_test.reshape(X_test.shape[0], 28 * 28) out = net(X_test) # 训练好的模型前向传播过程 # 获取out中[batch_size, 10]中10个值的最大值所在位置的索引 # out:[batch_size, 10] => pred: [b] pred = out.argmax(dim = 1) # 获取预测正确的样本个数,由于pred为最大值所在位置的索引, # 因此不需要将label_test转换为one_hot编码 # 当tensor为标量的时候,tensor.item()可以将其转换为ndarray类型 correct = pred.eq(label_test).sum().float().item() total_correct += correct # 所有预测正确的样本个数 # 获取整个测试集的样本个数 total_num = len(test_loader.dataset) acc = total_correct / total_num print("test acc: ", acc) # 0.8837

最终模型的准确率为:0.8837。仅仅是一个四层网络就能够达到 88% 的准确率,可见深度学习的强大,当然这并不是 MNIST 手写数字识别所能达到的最高准确率,我们可以调整神经网络的层数、每层的神经元个数或者使用卷积神经网络等等以达到更高的准确率。

参考:1.深度学习与PyTorch入门实战2.《TensoFlow深度学习》3.《Python深度学习基于PyTorch》



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3