使用Pytorch实现手写数字识别
Posted WXiujie123456
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Pytorch实现手写数字识别相关的知识,希望对你有一定的参考价值。
使用Pytor## 标题ch实现手写数字识别
思路和流程分析
- 准备数据,这些需要准备DataLoader
- 构建模型,这里可以使用torch构造一个深层的神经网络
- 模型的训练
- 模型的保存,保存模型,后续持续使用
- 模型的评估,使用测试集,观察模型的好坏
准备训练集和测试集
准备数据集的方法前面已经讲过,但是通过前面的内容可知,调用MNIST返回的结果中图像数据是一个image对象,需要对其进行处理。
为了进行数据的处理,我们需要学习torchvision.transform
的方法
torchvision.transform的图形数据处理API
torchvision.transform.ToTensor
把一个取值范围是[0,255]的PIL.Image
或者shape
为(H,W,C)的numpy.ndarray
,转换成形状为[C,H,W],取值范围是[0,1.0]的torch.FloatTensor
其中(H,W,C)意思为(高,宽,通道数),黑白图片的通道数只有1,其中每个像素点的取值为[0,255],彩色图片的通道数为[R,G,B],每个通道的每个像素点的取值为[0,255],三个通道的颜色互相叠加,形成了各种颜色
实例如下:
from torchvision import transforms
import numpy as np
data = np.random.randint(0,255,size=12)#随机生成12个0-255的数字
img = data.reshape(2,2,3)#将这12个数字的形状改成2 2 3
print(img.shape)
img_tensor = transforms.ToTensor()(img)#转化成tensor
print(img_tensor)
print(img_tensor.shape)
输出如下:
(2, 2, 3)
tensor([[[ 22, 78],
[ 13, 167]],
[[153, 107],
[102, 100]],
[[ 10, 64],
[ 9, 89]]], dtype=torch.int32)
torch.Size([3, 2, 2])
可见,使用transforms.ToTensor()(img)
转化成tensor类型后,这个新对象的形状变成[3,2,2](原来是(2,2,3)),相当于torch.tensor(img).permute(2,0,1)
即:
from torchvision import transforms
import numpy as np
import torch
data = np.random.randint(0,255,size=12)
img = data.reshape(2,2,3)
print(img)
print(img.shape)
img_tensor = transforms.ToTensor()(img)#转化成tensor
print(img_tensor)
print(img_tensor.shape)
img_t = torch.tensor(img)
print(img_t.permute(2,0,1))
print(img_t.permute(2,0,1).shape)
对应输出如下:
[[[154 131 6]
[113 149 7]]
[[ 11 19 163]
[112 111 97]]]
(2, 2, 3)
tensor([[[154, 113],
[ 11, 112]],
[[131, 149],
[ 19, 111]],
[[ 6, 7],
[163, 97]]], dtype=torch.int32)
torch.Size([3, 2, 2])
tensor([[[154, 113],
[ 11, 112]],
[[131, 149],
[ 19, 111]],
[[ 6, 7],
[163, 97]]], dtype=torch.int32)
torch.Size([3, 2, 2])
对应应用于MNIST中:
import torchvision
from torchvision import transforms
dataset = torchvision.datasets.MNIST(root='./data',train=True,download=True,transform=None)
print(dataset[0])
ret = transforms.ToTensor()(dataset[0][0])
print(ret.size())
#print(ret) 输出这个1*28*28的数组,内容太多不展示了
输出如下:
(<PIL.Image.Image image mode=L size=28x28 at 0x19FF2B2C7B8>, 5)
torch.Size([1, 28, 28])
可见通过transforms.ToTensor
方法,把dataset[0]
元组中第一个img对象转换成了[1,28,28]的tensor类型的数组(对应[通道,高,宽])
注意:transforms.ToTensor
对象中有__calll__
方法,所以可以对其示例能传入数据获取结果。
torchvision.transform.Normalize(mean,std)
给定均值:mean,shape(形状)和图片的通道数相同(指的是每个通道的均值)。
方差:std,和图片的通道数相同(指的是每个通道的方差)
将会把Tensor规范化处理,即:Normalize_image = (image - mean) / std
例如:
from torchvision import transforms
import numpy as np
import torchvision
data = np.random.randint(0,255,size=12)
img = data.reshape(2,2,3)
img = transforms.ToTensor()(img) #转化成tensor
print(img)
print('*' * 50)
norm_img = transforms.Normalize((10,10,10),(1,1,1))(img)#进行规范化处理
print(norm_img)
输出如下:
tensor([[[103, 6],
[157, 226]],
[[ 17, 119],
[176, 37]],
[[ 6, 137],
[173, 193]]], dtype=torch.int32)
**************************************************
tensor([[[ 93, -4],
[147, 216]],
[[ 7, 109],
[166, 27]],
[[ -4, 127],
[163, 183]]], dtype=torch.int32)
其中,93=(103-10)/1,10是均值,1是方差
torchvision.transforms.Compose(transforms)
将多个transform组合起来使用
- 传入一个list
- 数据经过list中的每一个方法挨个进行处理
例如:
transforms.Compose([
torchvision.transforms.ToTensor(),#先转化为Tensor
torchvision.transforms.Normalize(mean,std) #再进行正则化
])
准备训练集和测试集的代码实现
from torch.utils.data import DataLoader
from torchvision.transforms import Compose,ToTensor,Normalize
from torchvision.datasets import MNIST
#准备数据集
transform_fn = Compose([ #定义数据处理函数,完成对数据的totorch处理和标准化
ToTensor(),
Normalize(mean=(0.1307,),std=(0.3081,)) #mean 和std的形状要和数据通道数相同
])
dataset = MNIST(root='./data',train=True,transform=transform_fn) #设置数据集
data_Loader = DataLoader(dataset,batch_size=2,shuffle=True)#设置加载器
for i in enumerate(data_Loader):
print(i)
构建模型
补充:全连接层:当前一层的神经元和前一层的神经元相互链接,其核心操作就是y=wx,即矩阵的乘法,实现对前一层数据的变换。
模型的构建使用了一个三层的神经网络,其中包含两个全连接层和一个输出层,第一个全连接层会经过激活函数的处理,将处理后的结果交给下一个全连接层,进行变换后输出结果。
那么在这个模型中有三个地方需要注意:
-
激活函数如何使用
-
每一层数据的形状
-
模型的损失函数
激活函数如何使用
常见的激活函数Relu,它实现对数据中所有的负数置为零,其余0和正数原样输出的效果,它由import torch.nn.functional as F
提供(这个包提供了很多激活函数),F.relu(x)即可对x进行处理。
例如:
b = torch.tensor([-2,-1,0,1,2])
print(F.relu(b))
运行结果:
tensor([0, 0, 0, 1, 2])
模型中数据的形状(【添加形状变化图形】)
- 原数输入数据的形状:[batch_size,1,28,28]
- 进行数据的修改:[batch_size,28*28](全连接层是在进行矩阵的乘法操作)
- 第一个全连接层的输出形状:[batch_size,28],这里的28因个人设定,也可以设置为别的
- 激活函数不会修改数据的形状
- 第二个全连接层的输出形状:[batch_size,10],因为手写数字有十个类别
构建模型的代码如下:
class MnistModel(nn.Module):
def __init__(self):
super(MnistModel,self).__init__()
# 定义f1方法,使用Linear方法,Linear(输入的形状,输出的形状),将输入的28*28输出为28
self.fc1 = nn.Linear(in_features=28*28,out_features=28)
# 将输入的28输出为10,因为预期输出是10个数字
self.fc2 = nn.Linear(28,10)
def forward(self,input):
'''
:param input: [batch_size,1,28*28] 我们获得的原始数据的样子,即input[0]=batchsize,input[1]=1,input[2]=28*28
:return:
'''
#更改形状,view函数相当于resize的功能,将原来的tensor变换成指的维度,input.size(0)指batchsize的值
x = input.view(input.size(0),28*28) #这里实现把形状转化为[batch_size,28*28]
# x = input.view(-1,28*28) #二者实现效果相同
# x = input.view(input.size(0),-1)#实现效果与未注释的那句等价
#进行全连接操作
x = self.fc1(x)
#使用激活函数处理数据,不会使形状发生变化
x = F.relu(x)
#输出层
out = self.fc2(x)
return out
可见,pytorch在构建模型的时候形状上并不会考虑batch_size。
补充知识
view()函数的功能根reshape类似,用来转换size大小。x = x.view(batchsize, -1)中batchsize指转换后有几行,而-1指在不告诉函数有多少列的情况下,根据原tensor数据和batchsize自动分配列数。x = x.view(x.size(0), -1)相当于2x = x.view(batchsize, -1)。
模型的损失函数
需要知道,手写数字识别是一个多分类问题,所谓多分类是对比之前的二分类。
回顾一下二分类:
sigmoid函数表达式如下:
1 1 + e − Z \\frac{1}{1+e^{-Z}} 1+e−Z1
图像如下:
可以看到在趋于正无穷或负无穷时,函数趋近平滑状态,sigmoid函数因为输出范围(0,1),所以二分类的概率常常用这个函数,特点:
值域在0和1之间
函数具有非常好的对称性
函数对输入超过一定范围就会不敏感
现在我们使用多分类应该如何处理呢?
- 多分类应该使用softmax模型,而不是继续使用二分类的sigmoid模型
- softmax和sigmoid的区别在于我们需要去计算样本属于每个类别的概率,需要计算多次,而sigmoid只需要计算一次。
softmax的公式如下:
例如下图:
我们把softmax概率传入对数似然损失的损失函数称为交叉熵损失
在pytorch中有两种方法实现交叉熵损失
criterion = nn.CrossEntropyLoss()
loss = criterion(input.taget)
#对输出值计算softmax和取对数
output = = F.log_softmax(x,dim=-1)
#使用torch中带权损失
loss = F.nll_loss(output,target)
带权损失定义为:
l
n
=
−
∑
w
i
x
i
l_n=-\\sum w_ix_i
ln=−∑wixi
其实就是把log(P)
作为x_i
,把真实值Y
作为权重
模型的训练
训练流程:
- 实例化模型,设置模型为训练模式
- 实例化优化器模型,实例化损失函数
- 获取、遍历dataloader
- 梯度置为0
- 进行前向计算
- 计算损失
- 反向传播
- 更新参数
model = MnistModel()#实例化模型,设置模型为训练模式(默认)
optimizer = Adam(model.parameters(),lr=0.001)#实例化优化器模型
def train(epoch):#epoch 轮的意思
'''实现训练的过程'''
data_loader = get_dataLoader()#获取dataloader
for idx,(input,target) in enumerate(data_loader):
optimizer.zero_grad()#梯度置为零
out_put = model(input)#进行前向计算,调用模型,得到预测值
loss = F.nll_loss(out_put,target)#带权损失
loss.backward()#反向传播(记得梯度置为0),计算梯度
optimizer.step()#梯度更新
if idx%100 == 0:
print(loss.item())
模型的保存和加载
模型的保存
#模型的保存
if idx%100 ==0:
torch.save(model.state_dict(),'./model/model.pkl')
torch.save(optimizer.state_dict(), './model/optimizer.pkl')
模型的加载
if os.path.exists('./model/model.pkl'):#判断路径是否存在
model.load_state_dict(torch.load('./model/model.pkl'))
optimizer.load_state_dict(torch.load('./model/optimizer.pkl'))
模型的评估
评估的过程和训练的过程相似,但是:
- 不需要计算梯度
- 需要收集损失和转化率,用来计算平均损失和平均准确率
- 损失的计算和训练时候损失的计算方法相同
- 准确率的计算:
- 模型的输出为[batch_size]的形状
- 其中最大值的位置就是其预测的目标值(预测值进行过softmax后为概率,softmax中分母都是相同的,分子越大,概率越大)
- 最大值的位置的获取方法可以使用torch.max返回最大值和最大值的位置
- 返回最大值的位置后,和真实值([batch_size])进行对比,相同表示预测成功。
def test():
loss_list = []
acc_list = []
test_dataLoader = get_dataLoader(train=False,batch_size=TEST_BATCH_SIZE)
for idx,(input,target) in enumerate(test_dataLoader):
with torch.no_grad():
output = model(input)
cur_loss = F.nll_loss(output,target)
loss_list.append(cur_loss)
#计算准确率
# output [batch_size] target:[batch_size]
pred = output.max(dim=-1)[-1] #第一个-1表示在最后一个维度(行上)取得最大值,第二个-1表示同时输出对应位置
cur_acc = pred.eq(target).float().mean()
acc_list.append(cur_acc)
print('平均准确率:',np.mean(acc_list),'平均损失',np.mean(loss_list))
Pytorch实现手写识别完整代码
'''
该算法的核心思想是通过对比训练值和测试值中的最大值是否相同,来评估该项目的好坏
'''
import numpy as np
import os
from torch.utils.data import DataLoader
from torchvision.transforms import Compose,ToTensor,Normalize
from torchvision.datasets import MNIST
import torch
import torch.nn.functional as F
import torch.nn as nn
from torch.optim import Adam
BATCH_SIZE = 128
TEST_BATCH_SIZE = 1000
#准备数据集
def get_dataLoader(train = True,batch_size = BATCH_SIZE):
transform_fn = Compose([ # 定义数据处理函数,完成对数据的totorch处理和标准化
ToTensor(),
Normalize(mean=(0.1307,), std=(0.3081,)) # mean 和std的形状要和数据通道数相同
])
dataset = MNIST(root='./data', train=True, transform=transform_fn) # 设置数据集
data_Loader = DataLoader(dataset, batch_size=batch_size, shuffle=True) # 设置加载器
return data_Loader
#构建数据模型
class MnistModel(nn.Module):
def __init__(self):
super(MnistModel,self).__init__()
# 定义f1方法,使用Linear方法,Linear(输入的形状,输出的形状),将输入的28*28输出为28
self.fc1 = nn.Linear(in_features=28*28,out_features=28)
# 将输入的28输出为10,因为预期输出是10个数字
self.fc2 = nn.Linear(28,10)
def forward(self,input):
'''
:param input: [batch_size,1,28*28] 我们获得的原始数据的样子,即input[0]=batchsize,input[1]=1,input[2]=28*28
:return:
'''
#更改形状,view函数相当于resize的功能,将原来的tensor变换成指的维度,input.size(0)指batchsize的值
x = input.view(input.size(0),28*28) #这里实现把形状转化为[batch_size,28*28]
# x = input.view(-1,28*28) #二者实现效果相同
# x = input.view(input.size(0),-1)#实现效果与未注释的那句等价
#进行全连接操作
x = self.fc1(x)
#使用激活函数处理数据,不会使形状发生变化
x = F.relu(x)
#输出层
out = self.fc2(x)
return F.log_softmax(out,dim=-1) #在最后一个维度上进行操作,dim是维度的意思
model = MnistModel()#实例化模型,设置模型为训练模式(默认)
optimizer = Adam(model.parameters(),lr=0.001)#实例化优化器模型
if os.path.exists('./model/model.pkl'):#判断路径是否存在
model.load_state_dict(torch.load('./model/model.pkl'))
optimizer.load_state_dict(torch.load('./model/optimizer.pkl'))
def train(epoch):#epoch 轮的意思
'''实现训练的过程'''
data_loader = get_dataLoader()#获取dataloader
for idx,(input,target) in enumerate(data_loader):
optimizer.zero_grad()#梯度置为零
out_put = model(input)#进行前向计算,调用模型,得到预测值
loss = F.nll_loss(out_put,target)#带权损失
loss.backward()#反向传播(记得梯度置为0),计算梯度
optimizer.step()#梯度更新
# if idx%100 == 0:
# print(loss.item())
#模型的保存
if idx%100 ==0:
torch.save(model.state_dict(),'./model/model.pkl')
torch.save(optimizer.state_dict(), './model/optimizer.pkl')
def test():
loss_list = []
acc_list = []
test_dataLoader = get_dataLoader(train=False,batch_size=TEST_BATCH_SIZE)
for idx,(input,target) in enumerate(test_dataLoader):
with torch.no_grad():
output = model(input)
cur_loss = F.nll_loss(output,target)
loss_list.append(cur_loss)
#计算准确率
# output [batch_size] target:[batch_size]
pred = output.max(dim=-1)[-1] #第一个-1表示在最后一个维度(行上)取得最大值,第二个-1表示同时输出对应位置
cur_acc = pred.eq(target).float().mean()
acc_list.append(cur_acc)
print('平均准确率:',np.mean(acc_list),'平均损失',np.mean(loss_list))
if __name__ == '__main__':
# for i in range(3):#训练三轮
# train(i)
# loader = get_dataLoader(False)
# for input,lable in loader:
# print(lable.size())
# break
test()
for i in range(5):
train(i)
test()
更多Pytorch知识梳理,请参考: pytorch学习笔记
有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系。
以上是关于使用Pytorch实现手写数字识别的主要内容,如果未能解决你的问题,请参考以下文章
AI常用框架和工具丨13. PyTorch实现基于CNN的手写数字识别