使用Pytorch实现手写数字识别

文章目录

    • 使用Pytorch实现手写数字识别
      • 思路和流程分析
      • 准备训练集和测试集
        • torchvision.transform的图形数据处理API
          • `torchvision.transform.ToTensor`
          • `torchvision.transform.Normalize(mean,std)`
          • `torchvision.transforms.Compose(transforms)`
          • 准备训练集和测试集的代码实现
      • 构建模型
        • 激活函数如何使用
        • 模型中数据的形状(【添加形状变化图形】)
        • 模型的损失函数
        • 模型的训练
        • 模型的保存和加载
          • 模型的保存
          • 模型的加载
        • 模型的评估
        • Pytorch实现手写识别完整代码

使用Pytorch实现手写数字识别

思路和流程分析

  1. 准备数据,这些需要准备DataLoader
  2. 构建模型,这里可以使用torch构造一个深层的神经网络
  3. 模型的训练
  4. 模型的保存,保存模型,后续持续使用
  5. 模型的评估,使用测试集,观察模型的好坏

准备训练集和测试集

准备数据集的方法前面已经讲过,但是通过前面的内容可知,调用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的数组,内容太多不展示了

输出如下:

(, 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组合起来使用

  1. 传入一个list
  2. 数据经过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])

模型中数据的形状(【添加形状变化图形】)

  1. 原数输入数据的形状:[batch_size,1,28,28]
  2. 进行数据的修改:[batch_size,28*28](全连接层是在进行矩阵的乘法操作)
  3. 第一个全连接层的输出形状:[batch_size,28],这里的28因个人设定,也可以设置为别的
  4. 激活函数不会修改数据的形状
  5. 第二个全连接层的输出形状:[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)。

模型的损失函数

需要知道,手写数字识别是一个多分类问题,所谓多分类是对比之前的二分类。

回顾一下二分类:
使用Pytorch实现手写数字识别_第1张图片

sigmoid函数表达式如下:
1 1 + e − Z \frac{1}{1+e^{-Z}} 1+eZ1

图像如下:

使用Pytorch实现手写数字识别_第2张图片

可以看到在趋于正无穷或负无穷时,函数趋近平滑状态,sigmoid函数因为输出范围(0,1),所以二分类的概率常常用这个函数,特点:

  1. 值域在0和1之间

  2. 函数具有非常好的对称性

  3. 函数对输入超过一定范围就会不敏感

现在我们使用多分类应该如何处理呢?

  • 多分类应该使用softmax模型,而不是继续使用二分类的sigmoid模型
  • softmax和sigmoid的区别在于我们需要去计算样本属于每个类别的概率,需要计算多次,而sigmoid只需要计算一次。

softmax的公式如下:在这里插入图片描述

例如下图:

使用Pytorch实现手写数字识别_第3张图片

使用Pytorch实现手写数字识别_第4张图片

我们把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作为权重

模型的训练

训练流程:

  1. 实例化模型,设置模型为训练模式
  2. 实例化优化器模型,实例化损失函数
  3. 获取、遍历dataloader
  4. 梯度置为0
  5. 进行前向计算
  6. 计算损失
  7. 反向传播
  8. 更新参数
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'))

模型的评估

评估的过程和训练的过程相似,但是:

  1. 不需要计算梯度
  2. 需要收集损失和转化率,用来计算平均损失和平均准确率
  3. 损失的计算和训练时候损失的计算方法相同
  4. 准确率的计算:
    • 模型的输出为[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,pytorch,深度学习,神经网络)