从零编写一个手写数字识别神经网络

image.png

我们要设计并训练一个3层的神经网络:

image.png

这个神经网络会以数字图像作为输入,经过神经网络的计算,就会识别出图像中的数字是几,从而实现数字图像的分类。

在这个过程中,重点讲解3个块内容:

  1. 神经网络的设计和实现
  2. 训练数据的准备和处理
  3. 模型的训练和测试流程。

1.神经网络的设计和实现

输入数据长成什么样?

为了设计一个处理数字图像的神经网络,首先要弄清楚输入图像的大小和格式。

image.png

我们要处理的图片是,28*28像素的灰色单通道图像。

这样的灰色图像,包括了28*28=784个数据点。

每次在处理数字图像时,输入给神经网络的,就是这784个数据点。

在将它输入给神经网络前,这个2828的二维图片向量,会被展平为1784大小的一维线性向量:

image.png

比如这张图,左侧代表了28*28个像素对应的图像;

右侧是一个展平后的一维向量,包括了x0到x783,一共784个像素点。

这样这个向量才能被神经网络的输入层所接收和处理。

输入层如何设计?

我们会使用一个3层神经网络,来处理图片对应的向量x:

image.png

输入层需要接收784维的图片向量x。

图中的红色箭头,就代表了数据的输入。

x中的每一个维度的数据,都有一个神经元来接收。

因此,输入层就要包含784个神经元。

隐藏层如何设计?
image.png

隐藏层用于特征提取,它将输入的特征向量,处理为更高级的特征向量。

由于手写数字图像并不复杂,这里就将隐藏层的神经元个数,设置为256。

256就是个经验值,大家也可以设置为128、512,甚至999。

对于手写数字这个问题,并没有太大影响。

这样输入层与隐藏层之间,就会有一个784*256大小的线性层。

它可以将一个784维的输入向量,转换为256维的输出向量。

该输出向量会继续向前传播,到达输出层。

输出层如何设计?

由于最终要将数字图像,识别为0到9,10种可能的数字;

因此,输出层需要定义10个神经元,对应这10种数字。

256维的向量,再经过隐藏层和输出层之间的线性层计算后,就得到了10维的输出结果。

image.png

这个10维的向量,就是代表了10个数字的预测得分。

不要忘了还得有 softmax 层!

为了继续得到10个数字的预测概率,我们还要将输出层的输出,输入到 softmax 层。

image.png

softmax层会将10维的向量,转换为10个概率值,p0到p9。

每个概率值,都对应一个数字,也就是输入图片,是某一个数字的可能性。

另外,p0到p9这10个概率值,相加到一起的总和是1。

这是由softmax函数的性质决定的。

神经网络的代码怎么写?
import torch
from torch import nn
#定义神经网络Network
class Network(nn.Module):
  def __init__(self):
    super().__init__()
    #线性层1,输入层和隐藏层之间的线性层
    self.layerl=nn.Linear(784,256)
    #线性层2,隐藏层和输出层之间的线性层
    self.layer2 =nn.Linear(256,10)
    #在前向传播,forward函数中,输入为图像x
    def forward(self,x):
        x=x.view(-1,28*28) #使用view函数,将x展平
        x=self.layer1(x )#将x输入至layer1
        x= torch.relu(x) #使用relu激活
        return self.layer2(x) #输入至layer2计算结果

首先,定义神经网络Network。

init函数中:

定义两个线性层layer1layer2

layer1layer2分别是输入层和隐藏层、隐藏层和输出层之间的线性层。

它们的大小分别是784*256256*10

也就是右侧图中,红色标记的layer1layer2

在前向传播,forward函数中:

函数的输入为图像x

这个x就是1个或者多个,28*28像素数字图像。

在函数中,需要先将输入的图像x,使用view函数,将x展平。

也就是将n*28*28的数据,展平成n*784的数据。

然后将x输入至layer1, 接着使用relu激活;

最后输入至layer2计算结果,再返回。

另外,需要注意的是:

代码没有在forward中直接定义softmax层,

这是因为后面会使用CrossEntropyLoss损失函数。

在这个损失函数中,会实现softmax的计算。

2.训练数据的准备和处理

如果想要理解一个模型,我们要先理解给它输入的数据。

理解了数据定义和读取,再去看模型,会事半功倍。

训练数据哪里来?

手写数字识别的训练数据,可以直接使用 MNIS 数据集,这个数据集可以从torchvision.datasets中获取。

image.png

我会将数据分别保存到train和test两个目录中,其中:

image.png

这四个文件分别为:
①训练样本的图像(60000个)
②对应训练样本上每一张图像上数字的标签(0~9)(60000个)
③测试样本的图像(10000个)
④对应测试样本上每一张图像上数字的标签(0~9)(10000个)

它们分别用来模型的训练和测试。

在train和test,这两个目录中,都包括了10个子目录:

image.png

子目录的名字就对应了图像中的数字。

例如,在名为3的文件夹中,就保存了数字3的图像。

其中图像的名称是随机的字符串签名。

如何处理和读取这些数据?

完成数据的准备后,实现数据的读取功能,我会基于这一部分的代码进行讲解。

初学者在学习这一部分时,只要知道大致的数据处理流程就可以了。

数据的处理包括三块内容。

第1步,图像数据预处理:
from torchvision import transforms
from torchvision import datasets
from torch.utils.dataimport DataLoader
#初学者在学习这一部分时,只要知道大致的数据处理流程就可以了
if __name__ == '__main__':
  #实现图像的预处理pipeline
  transform=transforms.Compose([
    transforms.Grayscale(num output channels=1),# 转换为单通道灰度图
    transforms.ToTensor() #转换为张量
  ])

需要实现图像的预处理pipeline,transform。

它包括了将图像转为灰度图和转张量两个功能。

这一步可以简单的理解为,将数组数据处理为训练时所用的张量数据。

第2步,构建数据集对象:

数据集对象的作用,就是用来整体操作训练数据,可以更方便的访问这些数据。

#使用ImageFolder所数,读取数据文件夹,构建数据集dataset
#这个函数会将保存数据的文件夹的名字,作为数据的标签,组织数据
#例如,对于名字为"3"的文件夹
#会将“3"就会作为文件夹中图像数据的标签,和图像配对,用于后续的训练,使用起来非常的方便
train_dataset = datasets,ImageFolder(root='./mnist_images/train', transform=transform)
test_dataset= datasets,ImageFolder(root='./mnist_images/test',transform=transform)
#打印它们的长度
print("train dataset length:" ,len(train dataset)
print("test dataset length:", len(test dataset))
#程序输出:
#train_dataset length:60000
#test_dataset length:10000

具体来说,使用ImageFolder函数,读取数据文件夹,构建数据集dataset。

这个函数会将保存数据的文件夹的名字,作为数据的标签,组织数据。

例如,对于名字为“3”的文件夹,就会将“3”就会作为文件夹中的图像数据的标签。

标签和图像配对,用于后续的训练,ImageFolder使用起来非常方便。

这里我们分别读取训练数据文件夹train和测试数据文件夹test;

这样会得到train_dataset和test_dataset,两个数据集对象。

如果我们此时运行程序,会打印出它们的长度;

会看到,train_dataset是60000,test_dataset是10000。

这就代表了在训练集有60000个数据,测试集中有10000个数据。

第3步,小批量加载数据:

小批量加载数据直接和模型的训练有关。

小批量的数据读取,是训练各类深度学习模型的前提!

以下是创建小批量读取器dataloader的样例代码:

使用train loader,
实现小批量的数据读取
#这里设置小批量的大小,batch size=64。也就是每个批次,包括64个数据
train_loader = Dataloader(train_dataset, batch_size=64,shuffle=True)
#打印train loader的长度
print("train_loader length:", len(train loader))
#60000个训练数据,如果每个小批量,读入64个样本,那么60000个数据会被分成938组
#计算938*64=60032,这说明最后一组,会不够64个数据
#程序输出:
#train_loader length:938

我们会使用train_loader,实现小批量的数据读取。

这里设置小批量的大小,batch_size=64。

也就是每个批次,包括64个数据,一次计算64个数据的梯度!

这时如果运行程序,会打印train_loader的长度,然后看到结果是938。

大家可以想一想,938是怎么来的?

具体来说,60000个训练数据,如果每个小批量,读入64个样本;

那么60000个数据会被分成938组。

我们可以计算938*64=60032,不足60000;

这就说明最后一组,会不够64个数据。

大家可以再想一想,最后一组有多少个数据呢?

小批量的遍历数据,是训练的关键前提

我们可以通过循环遍历train_loader来获取每个小批量数据。

#循环遍历train loader
#每一次循环,都会取出64个图像数据,作为一个小批量batch
for batch_idx,(data,label)in enumerate(train_loader):
  if batch_idx=3: #打印前3个batch观絮
    break
  print("batch idx:",batch_idx)
  print("data.shape:",,data.shape) #图片的尺寸
  print("label:",label.shape)#图像中的数字
  print(label)
#程序输出:
#batch idx: 0
#data.shape: torch.size([64,1,28,28])
#label: torch.Size([64])
#tensor([6,8,0,7,8,9,8,3,6,6,7,0,2,2,6,6,8,0,5,0,2,8,2,0,
#8,5,9,6,7,5,2,2,7,6,3,3,6,2,0,3,2,2,9,2,2,6,3,0,
#4, 6, 3, 5,7,5,4,8,5,1,2,2,1,2,8, 1])
#...

这里的每一次循环,都会取出64个图像数据,作为一个小批量batch。

此时如果,打印前3个batch观察,可以看到数据的尺寸data.shape64*1*28*28

它表示了每组数据包括64个图像, 每个图像有1个灰色通道,图像的尺寸是28*28。

接着打印图像的标签label,可以看到64个图片对应的数字,其中保存的数值是0到9,对应了10个数字。

3.模型的训练和测试

实际上,对于训练一个深度学习模型,训练后再测试这个深度学习模型;

这两个过程,都是定式。

也就是,无论你训练的模型简单还是复杂,是前馈神经网络还是Transformer,都是哪几个步骤。

当然,对于一些特殊的神经网络,可能会做一些专门的训练优化,但本质还是那几个步骤,主要理解“什么是训练模型的定式”。

相同的数据读入步骤

关于模型的训练,前半部分是图像数据的读入。

import torch
from torch import nn
from torch import optim
from model import Network
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
if __name == '__main__':
  #图像的预处理
  transform=transforms.Compose([
    transforms.Grayscale(num_output_channels=1) #转换为单通道灰度图
    transforms.ToTensor() #转换为张量
  ])
  #读入并构造数据集
  train_dataset = datasets,ImageFolder(root='./mnist_images/train', transform=transform)
  print("train_dataset length:",len(train_dataset))
  #小批量的数据读入
  train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
  print("train_loader length:" ,len(train loader))
#程序输出:
#train_datasetlength:60000
#train_loaderlength:938

包括:

  1. 图像的预处理transform
  2. 读入并构造数据集train_dataset
  3. 使用train_loader进行小批量的数据读入。
创建核心对象(变量)

在使用Pytorch训练模型时,需要创建三个核心对象(变量)。

大家要记住,无论训练哪种深度学习模型;

下面说的这三个对象,都要创建!

#在使用pytorch训练模型时,需要创建三个对象:
model= Network()  #1.模型本身,它就是我们设计的神经网络
optimizer = optim.Adam(model.parameters())  2.优化器,优化模型中的参数
criterion = nn.CrossEntropyLoss()  #3.损失函数,分类问题,使用交叉熵损失误差

第1个是:模型本身model,它就是我们设计的神经网络。

第2个是:优化器optimizer,它用来优化模型中的参数。

初学的时候,直接使用Adam优化器就可以了。

第3个是:损失函数criterion,对于分类问题,就直接使用CrossEntropyLoss,交叉熵损失误差,这一点也不需要纠结。

进入模型的循环迭代

模型的循环迭代,同样是定式!迭代深度学习模型,就是两层循环。

这两层循环,分别是:

  • 表示训练轮数的外层循环
  • 表示梯度下降的内层循环
#进入模型的迭代循环
for epoch in range(10): #外层循环,代表了整个训练数据集的遍历次数
  #整个训练集要循环多少轮,是10次、20次或者100次都是可能的,
  #内存循环使用train loader,进行小批量的数据读取
  for batch_idx,(data,label) in enumerate(train_loader):
    #内层每循环一次,就会进行一次梯度下降算法
    #包括了5个步骤:
    output= model(data)#1.计算神经网络的前向传播结果
    loss = criterion(output,label) # 2.计算output和标签label之间的损失loss
    loss.backward() #3.使用backward计算梯度
    optimizer.step() #4.使用optimizer.step更新参数
    optimizer.zero_grad() #5.将梯度清零
    #这5个步骤,是使用pytorch框架训练模型的定式,初学的时候,先记住就可以了
    #每迭代100个小批量,就打印一次模型的损失,观察训练的过程
    if batch_idx % 100 = 0:
      print(f"Epoch {epoch +1}/10"
            f"|Batch {batch_idx}/{len(train_loader)}"
            f"|Loss:{loss.item():.4f}")
torch.save(model.state_dict(),'mnist.pth')#保存模型
#Epoch 1/10|Batch 0/938 Loss: 2.3257
#Epoch 1/10|Batch 100/938|Loss: 0.4863
#Epoch 1/10|Batch 200/938Loss:0.2337
#Epoch 1/10|Batch 300/938|Loss: 0.1560

具体来说:

外层循环,代表了整个训练数据集的遍历次数。

整个训练集要循环多少轮,是10次、20次或者100次都是可能的。

这里根据经验,设置为10次。

内层循环使用train_loader,进行小批量的数据读取。

内层循环,每循环一次,就会进行一次梯度下降算法。

梯度下降算法

内层循环所包含的梯度下降算法,包括了5个步骤。

这5个步骤,又是使用pytorch框架训练模型的定式。

具体来说:

1)计算神经网络的前向传播结果output。

2)计算output和标签label之间的损失loss。

3)使用backward计算梯度。

4)使用optimizer.step更新参数。

5)最后将梯度清零。

另外,我们每迭代100个小批量,就打印一次模型的损失,观察训练的过程。

运行程序,就会观察到,模型的损失loss,不断变小。

最后使用torch.save保存模型,模型名字为mnist.pth。

这个“mnist.pth”就是我们最后得到的神经网络模型;

将来再进行数字图片的预测时,就要用它来识别图像。

模型的测试过程

完成模型训练后,需要对模型进行测试。

测试的流程与训练差不多,我们要测试出模型的效果。

测试的过程,也相当于模型的“使用过程”了。

前面是类似的数据读入和模型定义:

from model import Network
from torchvision import transforms
from torchvision import datasets
import torch
if __name__ == '__main__':
  transform=transforms.Compose([
    transforms.Grayscale(num_output_channels=1),
    transforms.ToTensor()
  )]
  #读取测试数据集
  test_dataset = datasets,ImageFolder (root='./mnist images/test', transform=transform)
  print("test dataset length:",len(test_dataset))
  model=Network()#定义神经网络模型
  model,load state_dict(torch.load('mnist.pth')) #加载刚刚训练好的模型文件

首先需要读取测试数据集test_dataset。

然后定义神经网络模型,并加载刚刚训练好的模型文件mnist.pth。

然后是遍历测试数据集,进行预测,统计正确率:

right = 0 #保存正确识别的数量
for i,(x,y) in enumerate(test_dataset):
  output= model(x) #将其中的数据x输入到模型
  predict = output,argmax(1).tem()# 选择概率最大标签的作为预测结果
  #对比预测值predict和真实标签y
  if predict == y:
    right += 1
  else:
    #将识别错误的样例打印了出来
    img_path =test_dataset.samples[i][0]
    print(f"wrong case:predict ={predict) y={y} img_path = {img_path}")
#计算出测试效果
sample_num=len(testdataset)
acc = right * 1.0 / sample_num
print("test accuracy = %d / %d = %.3lf" % (right,sample_num,acc))
#程序输出:
#wrongcase: predict=3 y=9 img_path=./mnist_images/test\9\fda99ed05e16f012.png
#wrong case: predict=7 y=9 img_path=./mnist_images/test\9\fe063alecae8f4cb.png
#wrong case: predict=4 y=9 img_path=./mnist_images/test\9fff8b146d27a7c02.png
#test accuracy=9779/10000=0.978

定义变量right,保存正确识别的数量。

遍历test_dataset,将其中的数据x输入到模型model中,计算结果output。

然后从output中,使用argmax,选择概率最大标签的作为预测结果,保存到predict。

接着对比预测值predict和真实标签y。

这里将识别错误的样本打印了出来。

可以看到错误case的预测值predict、真实值y和文件路径。

最终计算出的测试效果为0.978。

也就是10000个数据,有9779个数据识别正确。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容