使用Pytorch实现mnist手写数字识别

写在开头

我其实根本就不想写这方面内容的文章,不过既然人工智能课的结课作业要写项目文档,我就顺便更一下博客而已。(其实说到底就是逮到机会想水博客而已。)没有学过Python,代码来源自网络,整理一下也能方便日后的自己拿出来学习。
下面开始就是作业了。

基于Pytorch的MNIST手写数字识别

摘要

数字识别(Digit Recognition)是计算机从纸质文档,照片,或其他来源接收和理解并识别可读的数字的能力。
根据数字来源的产生方式的不同,目前数字识别问题可以区分为手写体数字识别,印刷体数字识别,光学数字识别,自然场景下的数字识别等,具有很大的实际应用价值。目前比较受到关注的问题主要是手写体数字识别,由于其具有MNIST这种大型标准易用的成熟数据集,简单的0-9数字识别已经被作为计算机视觉领域的入门问题。本文通过介绍手写数字识别的应用,并指出传统研究方法及其不足之处;然后引入深度学习的概念,以卷积神经网络为例,详细介绍卷积神经网络的关键技术特点。最后通过一个实例说明卷积神经网络在手写数字识别方面的应用。(此段来源自网络)

项目背景概述/介绍

MNIST手写数字识别,是一个比较简单的入门项目,就相当于我们在学习编程语言时最开始的Hello World,可以让我们快速地了解构建神经网络的大致过程。
这次我们选择使用PyTorch的神经网络框架。PyTorch是torch的python版本,它是由Facebook开源的神经网络框架。
Torch 是一个经典的对多维矩阵数据进行操作的张量(tensor)库,在机器学习和其他数学密集型应用有广泛应用。与Tensorflow的静态计算图不同,pytorch的计算图是动态的,可以根据计算需要实时改变计算图。但由于Torch语言采用 Lua,导致在国内一直很小众,并逐渐被支持 Python 的 Tensorflow 抢走用户。作为经典机器学习库 Torch 的端口,PyTorch 为 Python 语言使用者提供了舒适的写代码选择。PyTorch的设计追求最少的封装,尽量避免重复造轮子。不像 TensorFlow 中充斥着session、graph、operation、name_scope、variable、tensor、layer等全新的概念,PyTorch 的设计遵循tensor→variable(autograd)→nn.Module 三个由低到高的抽象层次,分别代表高维数组(张量)、自动求导(变量)和神经网络(层/模块),而且这三个抽象之间联系紧密,可以同时进行修改和操作。 简洁的设计带来的另外一个好处就是代码易于理解。PyTorch的源码只有TensorFlow的十分之一左右,更少的抽象、更直观的设计使得PyTorch的源码十分易于阅读。
总的来说,选择Pytorch,是因为它能够在短时间内建立结果,适用于小规模的项目。(其实最主要的是因为老师让我们用Pytorch)

项目实现原理

流程

安装PyTorch -> 安装mnist -> 通过代码下载MNIST训练集 -> 通过代码定义卷积神经网络 -> 通过代码使用MNIST训练集中的训练库和测试库训练出一个模型MNIST.pth -> 通过模型与定义的卷积神经网络进行识别

PyTorch安装

根据自己计算机的实际情况在官网 https://pytorch.org 安装相应的PyTorch
详细安装教程:https://blog.csdn.net/learningpawn/article/details/106531514

安装Python的mnist库

打开cmd,输入pip install mnist
可以通过 pip list 检查自己是否安装相应的库

通过代码进行的操作

通过代码下载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
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
torch.__version__


BATCH_SIZE=512 #大概需要2G的显存
EPOCHS=20 # 总共训练批次
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 让torch判断是否使用GPU,建议使用GPU环境,因为会快很多


#下载训练集
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=BATCH_SIZE, shuffle=True)

#下载测试集
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=BATCH_SIZE, shuffle=True)

#定义卷积神经网络
class ConvNet(nn.Module):
def __init__(self):
super().__init__()
# batch*1*28*28(每次会送入batch个样本,输入通道数1(黑白图像),图像分辨率是28x28)
# 下面的卷积层Conv2d的第一个参数指输入通道数,第二个参数指输出通道数,第三个参数指卷积核的大小
self.conv1 = nn.Conv2d(1, 10, 5) # 输入通道数1,输出通道数10,核的大小5
self.conv2 = nn.Conv2d(10, 20, 3) # 输入通道数10,输出通道数20,核的大小3
# 下面的全连接层Linear的第一个参数指输入通道数,第二个参数指输出通道数
self.fc1 = nn.Linear(20*10*10, 500) # 输入通道数是2000,输出通道数是500
self.fc2 = nn.Linear(500, 10) # 输入通道数是500,输出通道数是10,即10分类
def forward(self,x):
in_size = x.size(0) # 在本例中in_size=512,也就是BATCH_SIZE的值。输入的x可以看成是512*1*28*28的张量。
out = self.conv1(x) # batch*1*28*28 -> batch*10*24*24(28x28的图像经过一次核为5x5的卷积,输出变为24x24)
out = F.relu(out) # batch*10*24*24(激活函数ReLU不改变形状))
out = F.max_pool2d(out, 2, 2) # batch*10*24*24 -> batch*10*12*12(2*2的池化层会减半)
out = self.conv2(out) # batch*10*12*12 -> batch*20*10*10(再卷积一次,核的大小是3)
out = F.relu(out) # batch*20*10*10
out = out.view(in_size, -1) # batch*20*10*10 -> batch*2000(out的第二维是-1,说明是自动推算,本例中第二维是20*10*10)
out = self.fc1(out) # batch*2000 -> batch*500
out = F.relu(out) # batch*500
out = self.fc2(out) # batch*500 -> batch*10
out = F.log_softmax(out, dim=1) # 计算log(softmax(x))
return out



#训练
def train(model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if(batch_idx+1)%30 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))

#测试
def test(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item() # 将一批的损失相加
pred = output.max(1, keepdim=True)[1] # 找到概率最大的下标
correct += pred.eq(target.view_as(pred)).sum().item()

test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))


if __name__ == '__main__':
model = ConvNet().to(DEVICE)
optimizer = optim.Adam(model.parameters())

for epoch in range(1, EPOCHS + 1):
train(model, DEVICE, train_loader, optimizer, epoch)
test(model, DEVICE, test_loader)


#保存训练完成后的模型
torch.save(model, './MNIST.pth')

以下为测试代码,它将指定进行测试的图片通过opencv转化为灰度图,然后使用前面训练好的模型MNIST.pth和定义好的卷积神经网络进行识别
(opencv安装代码 : pip install python-opencv

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
import torch
from mnist import *
import glob
import cv2
import numpy as np
import torch.nn.functional as F
from torch.autograd import Variable
from torchvision import datasets, transforms
import numpy as np
import torchvision
from skimage import io,transform
from train import *

if __name__ =='__main__':
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = torch.load(r'D:/学习/Python/手写数字识别/MNIST.pth') #加载模型
model = model.to(device)
model.eval() #把模型转为test模式


img = cv2.imread('./numbers/number1.jpg', 0) #以灰度图的方式读取要预测的图片
img = cv2.resize(img, (28, 28))

height,width=img.shape
dst=np.zeros((height,width),np.uint8)
for i in range(height):
for j in range(width):
dst[i,j]=255-img[i,j]

img = dst

img=np.array(img).astype(np.float32)
img=np.expand_dims(img,0)
img=np.expand_dims(img,0)#扩展后,为[1,1,28,28]
img=torch.from_numpy(img)
img = img.to(device)
output=model(Variable(img))
prob = F.softmax(output, dim=1)
prob = Variable(prob)
prob = prob.cpu().numpy() #用GPU的数据训练的模型保存的参数都是gpu形式的,要显示则先要转回cpu,再转回numpy模式
print(prob) #prob是10个分类的概率
pred = np.argmax(prob) #选出概率最大的一个
print("识别结果为:")
print(pred.item())

项目实现结果/分析

以下为最终实现的成果,发现手写数字的粗细程度会影响识别的精度,在一定粗细程度的笔触下写下的数字能够实现高精度的识别。

下图为一定粗细程度笔触写下的数字的实现结果,识别波动几乎为无,而且能够完美识别。
正确实现结果

下图则为过细程度笔触写下的数字的实现结果,可发现识别波动范围比较大,而且出现了识别错误的情况。
错误实现结果

(两图数字皆为博主所写)

要想了解问题为何发生,就得从源头开始查起。
MNIST数据集来自美国国家标准与技术研究所(National Institute of Standards and Technology),简称 (NIST)。
训练集由来自250个不同人手写的数字构成, 其中50%是高中学生, 50%来自人口普查局的工作人员。测试集也是同样比例的手写数字数据。
于是我通过以下代码得到了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
import os
from skimage import io
import torchvision.datasets.mnist as mnist

root="D:/学习/Python/手写数字识别/程序/data/MNIST/raw"
train_set = (
mnist.read_image_file(os.path.join(root, 'train-images-idx3-ubyte')),
mnist.read_label_file(os.path.join(root, 'train-labels-idx1-ubyte'))
)
test_set = (
mnist.read_image_file(os.path.join(root, 't10k-images-idx3-ubyte')),
mnist.read_label_file(os.path.join(root, 't10k-labels-idx1-ubyte'))
)
print("training set :",train_set[0].size())
print("test set :",test_set[0].size())

def convert_to_img(train=True):
if(train):
f=open(root+'train.txt','w')
data_path=root+'/train/'
if(not os.path.exists(data_path)):
os.makedirs(data_path)
for i, (img,label) in enumerate(zip(train_set[0],train_set[1])):
img_path=data_path+str(i)+'.jpg'
io.imsave(img_path,img.numpy())
f.write(img_path+' '+str(label)+'\n')
f.close()
else:
f = open(root + 'test.txt', 'w')
data_path = root + '/test/'
if (not os.path.exists(data_path)):
os.makedirs(data_path)
for i, (img,label) in enumerate(zip(test_set[0],test_set[1])):
img_path = data_path+ str(i) + '.jpg'
io.imsave(img_path, img.numpy())
f.write(img_path + ' ' + str(label) + '\n')
f.close()

convert_to_img(True)#转换训练集
convert_to_img(False)#转换测试集

通过得到的MNIST训练集与测试集中的图片可以看出,虽然他们收集的手写数字皆为不同人所写,但字体的粗细程度几乎相同,或许是使用了同一种笔来进行数据的收集。
这才使得字体的粗细程度会影响识别精度的情况发生。
根据我的初步理解,要想解决这种情况,得丰富数据库,重新进行深度学习。

结束语

通过这次的学习,进一步了解深度学习,也对卷积神经网络有了一个初步的认识。
这个暑假会开展对Python语言的学习,这里的代码算是一个预习了。

文章作者: 牧尾伊織
文章链接: http://example.com/2021/06/27/blog/使用Pytorch实现mnist手写数字识别/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Makiori's blog