手写数字识别实战

一、思路和流程分析
  1. 准备数据,需要准备DataLoader
  2. 构建模型,这里可以使用torch构造一个深层的神经网络
  3. 训练模型
  4. 保存模型
  5. 使用测试集进行模型评估
二、准备训练集和测试集

为了进行数据的处理,需要介绍torchvision.transforms方法

1. torchvision.transforms.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],彩色图片有3个通道(R,G,B),每个通道的像素点取值范围为[0,255],三个通道的颜色叠加后形成各种颜色

	from torchvision import transforms
	import numpy as np
	
	img = np.random.randint(0, 255, size=12).reshape(2,2,3)
    print(img.shape)
    # 将(H,W,C)的numpy.ndarray转换成(C,H,W)的tensor
    img_tensor = transforms.ToTensor()(img)  
    print(img_tensor)
    print(img_tensor.shape)

在这里插入图片描述

2. torchvision.transforms.Normalize(mean, std)

根据均值、方差进行规范化处理,即:

input[channel] = (input[channel] - mean[channel]) / std[channel]

    img = np.random.randint(0, 255, size=12).reshape(2,2,3)
    img = transforms.ToTensor()(img)
    print(img)
    print('-'*100)
    norm_img = transforms.Normalize(mean = (0.5, 0.5, 0.5), std = (0.5, 0.5, 0.5))(img)
    print(norm_img)
3. torchvision.transforms.Compose(transforms)

作用:将多个transform组合使用

transform.Compose([
	torchvision.transforms.ToTensor(),  # 转为tensor
	torchvision.transforms.Normalize(mean, std)  # 标准化
])
4. 准备训练集

下载数据集:

	from torchvision.datasets import MNIST
    MNIST(root='./data', train=True, download=True)

准备数据集:

def getDataLoader(istrain=True, batch_size=TRAIN_BATCH_SIZE):
    dataset = MNIST(
        root='./data',
        train=istrain,
        transform=Compose([
            ToTensor(),
            Normalize(mean=(0.1307,), std=(0.3081,))
        ])
    )
    # dataLoader里的一个元素为一个列表,列表包含特征值和目标值
    # 特征值形状为BATCH_SIZE * 1 * 28 * 28
    # 目标值表示此手写图片代表的数字
    dataLoader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
    return dataLoader

batch_size=2时,打印dataLoader里其中一个元素,以及特征值的形状:

在这里插入图片描述

三、构建模型

补充:

全连接层: 当前一层的神经元和前一层的神经元相互连接,其核心操作就是 y = w x y=wx y=wx,即矩阵乘法,实现对前一层数据的变换

模型的构建使用了一个四层的神经网络,其中包括一个输入层,两个全连接层和一个输出层,第一个全连接层经过激活函数的处理,将处理后的结果交给下一个全连接层,进行变换后输出结果

在这个模型中需要注意:

  1. 激活函数的使用
  2. 每一层数据的形状
  3. 模型的损失函数
3.1 激活函数的使用

激活函数由torch.nn.functional提供

在这里插入图片描述

3.2 模型中数据的形状
  1. 原始输入数据的形状:batch_size * 1 * 28 * 28
  2. 进行形状的修改:batch_size * 28 * 28
  3. 第一个全连接层的输出形状:batch_size * 28
  4. 激活函数不会修改数据的形状
  5. 第二个全连接层的输出形状:batch_size * 10,因为手写数字有10个类别

构建模型的代码如下:

# 构建模型
class MnistModel(nn.Module):
    def __init__(self):
        super(MnistModel, self).__init__()
        self.full_conn1 = nn.Linear(1*28*28, 50)  # 全连接层的形状
        self.full_conn2 = nn.Linear(50, 10)  # 10个类别


    def forward(self, input):
        """
        :param input:[batch_size, 1, 28, 28]
        :return:
        """
        # 1. 修改形状
        x = input.view([input.size(0), 1*28*28])
        # 2. 第一层全连接
        x = self.full_conn1(x)
        # 3. 激活函数处理
        x = F.relu(x)
        # 4. 第二层全连接
        out = self.full_conn2(x)
        return out
3.3 模型的损失函数

首先,我们需要明确,当前我们手写字体识别的问题是一个多分类的问题,所谓多分类对比的是之前学习的二分类

我们在二分类问题中使用sigmoid进行计算对数似然损失

  • 在二分类中,正类的概率为 p ( x ) = 1 1 + e − x = e x 1 + e x p(x)=\frac{1}{1+e^{-x}}=\frac{e^x}{1+e^x} p(x)=1+ex1=1+exex,那么负类的概率直接为 1 − p ( x ) 1-p(x) 1p(x)
  • 将这个结果进行计算对数似然损失 − ∑ y l o g ( p ( x ) ) -\sum ylog(p(x)) ylog(p(x))就可得到最终的损失

那么在多分类问题中我们应该怎么做?

  • 多分类和二分类中唯一的区别是我们不能再使用sigmoid函数来计算当前样本属于哪个类别的概率,而应该使用softmax函数
  • softmaxsigmoid的区别在于我们需要去计算样本属于哪个类别,需要计算多次,而sigmoid只需要计算一次

s o f t m a x softmax softmax公式: σ ( z ) j = e z j ∑ k = 1 K e z k \sigma(z)_j=\frac{e^{z_j}}{\sum_{k=1}^K}e^{z_k} σ(z)j=k=1Kezjezk

在这里插入图片描述
假设softmax的输入是2、3、5,那么经过softmax之后的结果如下:
在这里插入图片描述
这个输出结果介于[0,1]之间,我们可以把它当作概率

和前面的二分类的损失一样,多分类的损失只需要再把这个结果进行对数似然损失的计算即可:

− ∑ y l o g ( P ) -\sum ylog(P) ylog(P),其中 P = e z j ∑ k = 1 K e z k P=\frac{e^{z_j}}{\sum_{k=1}^K}e^{z_k} P=k=1Kezjezk y y y 表示真实值

最后,计算每个样本的损失,即上式的平均值。我们把softmax概率传入对数似然函数得到的损失函数称为交叉熵损失

在pytorch中,有两种方式实现 交叉熵损失

	criterion = nn.CrossEntropyLoss()
    loss = criterion(input, target)
	# 对输出值计算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,其实就是把 l o g ( P ) log(P) log(P)作为 x i x_i xi,把真实值 y y y 作为权重 w i w_i wi

四、训练模型

训练的流程:

  1. 实例化模型,设置为训练模式
  2. 实例化优化器类,实例化损失函数
  3. 获取、遍历dataLoader
  4. 置梯度为0
  5. 前向传播
  6. 计算损失
  7. 反向传播
  8. 更新参数
# 训练模型
def train(epoch, model, optimizer):
    # 获取数据加载器
    dataLoader = getDataLoader()
    # 开始训练
    for i in range(epoch):
        for index, (input, target) in enumerate(dataLoader):
            # 梯度置 0
            optimizer.zero_grad()
            # 调用模型,得到预测值。会调用类方法forward
            output = model(input)
            # 计算交叉熵损失
            loss = F.nll_loss(output, target)
            # 反向传播
            loss.backward()
            # 梯度更新
            optimizer.step()
            if index % 100 == 0:
                print(loss.item())
                torch.save(model.state_dict(), './MnistModel/model.pkl')
                torch.save(optimizer.state_dict(), './MnistModel/optimizer.pkl')

保存模型

torch.save(model.state_dict(), './MnistModel/model.pkl')
torch.save(optimizer.state_dict(), './MnistModel/optimizer.pkl')

加载模型

model.load_state_dict(torch.load('./MnistModel/model.pkl'))
optimizer.load_state_dict(torch.load('./MnistModel/optimizer.pkl'))  
四、评估模型

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

  1. 无需计算梯度
  2. 需要收集损失和准确率,用于计算平均损失和平均准确率
  3. 损失的计算和训练时候损失的计算方法相同
  4. 准确率的计算
  • 模型的输出为[batch_size,10]的形状
  • 其中最大值的位置就是其预测的目标值(预测值进行过softmax后为概率,softmax中分母相同,分子越大,概率越大)
  • 最大值的位置获取最大值可以用torch.max,同时返回最大值和最小值的位置
  • 返回最大值位置后,和真实值([batch_size])进行对比,相同表示成功
def test(model):
    loss_list = []
    accu_list = []
    testDataLoader = getDataLoader(istrain=False, batch_size=TEST_BATCH_SIZE)
    for feature, target in testDataLoader:
        # 无需跟踪梯度,所以包含在torch.no_grad()内
        with torch.no_grad():
            output = model(feature)  # [batch_size, 10]
            cur_loss = F.nll_loss(output, target)
            loss_list.append(cur_loss)
            # 计算准确率
            # output是存放的batch_size*10的概率,每列对应预测的一个数值
            # 每一行表示一张图片,预测一个数据,找出每行10个概率中最大的概率
            # 此概率的位置对应的数值,便是预测的数据
            # max函数返回最大的数值,以及对应的位置
            pre = output.max(dim=-1)[-1]
            cur_accu = pre.eq(target).float().mean()
            accu_list.append(cur_accu)
    print('平均准确率={:.2f}%,平均损失={:.2f}%'.format(100*np.mean(accu_list), 100*np.mean(loss_list)))

在这里插入图片描述
完整代码:

import os
import numpy as np
import torch
import torch.nn.functional as F
from torch import nn
from torch.optim import Adam
from torchvision.datasets import MNIST
from torch.utils.data import Dataset, DataLoader
from torchvision.transforms import ToTensor, Normalize, Compose


TRAIN_BATCH_SIZE = 256
TEST_BATCH_SIZE = 256*4
IS_TRAIN = False

# 准备数据集
def getDataLoader(istrain=True, batch_size=TRAIN_BATCH_SIZE):
    dataset = MNIST(
        root='./data',
        train=istrain,
        transform=Compose([
            ToTensor(),
            Normalize(mean=(0.1307,), std=(0.3081,))
        ])
    )
    # dataLoader里的元素为特征值和目标值
    # 特征值形状为BATCH_SIZE * 1 * 28 * 28
    # 目标值表示此手写图片代表的数字
    dataLoader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
    return dataLoader


# 构建模型
class MnistModel(nn.Module):
    def __init__(self):
        super(MnistModel, self).__init__()
        self.full_conn1 = nn.Linear(1*28*28, 50)  # 全连接层的形状
        self.full_conn2 = nn.Linear(50, 10)  # 10个类别

    def forward(self, input):
        """
        :param input:[batch_size, 1, 28, 28]
        :return:
        """
        # 1. 修改形状
        x = input.view([input.size(0), 1*28*28])
        # 2. 第一层全连接
        x = self.full_conn1(x)
        # 3. 激活函数处理
        x = F.relu(x)
        # 4. 第二层全连接
        out = self.full_conn2(x)
        # 必须指定在哪个维度上进行softmax操作,dim为-1表示在最后一个维度操作
        return F.log_softmax(out, dim=-1)


# 训练模型
def train(epoch, model, optimizer):
    # 获取数据加载器
    dataLoader = getDataLoader()
    # 开始训练
    for i in range(epoch):
        for index, (feature, target) in enumerate(dataLoader):
            # 梯度置0
            optimizer.zero_grad()
            # 调用模型,得到预测值。会调用类方法forward
            output = model(feature)
            # 计算交叉熵损失
            loss = F.nll_loss(output, target)
            # 反向传播
            loss.backward()
            # 梯度更新
            optimizer.step()
            if index % 100 == 0:
                print(loss.item())
                torch.save(model.state_dict(), './MnistModel/model.pkl')
                torch.save(optimizer.state_dict(), './MnistModel/optimizer.pkl')


def test(model):
    loss_list = []
    accu_list = []
    testDataLoader = getDataLoader(istrain=False, batch_size=TEST_BATCH_SIZE)
    for feature, target in testDataLoader:
        # 无需跟踪梯度,所以包含在torch.no_grad()内
        with torch.no_grad():
            output = model(feature)  # [batch_size, 10]
            cur_loss = F.nll_loss(output, target)
            loss_list.append(cur_loss)
            # 计算准确率
            # output是存放的batch_size*10的概率,每列对应预测的一个数值
            # 每一行表示一张图片,预测一个数据,找出每行10个概率中最大的概率
            # 此概率的位置对应的数值,便是预测的数据
            # max函数返回最大的数值,以及对应的位置
            pre = output.max(dim=-1)[-1]
            cur_accu = pre.eq(target).float().mean()
            accu_list.append(cur_accu)
    print('平均准确率={:.2f}%,平均损失={:.2f}%'.format(100*np.mean(accu_list), 100*np.mean(loss_list)))


def main():
    # 实例化模型
    model = MnistModel()
    # 实例化优化器
    optimizer = Adam(model.parameters(), lr=1e-3)
    # 从文件加载模型、优化器
    if os.path.exists('./MnistModel/model.pkl'):
        model.load_state_dict(torch.load('./MnistModel/model.pkl'))
        optimizer.load_state_dict(torch.load('./MnistModel/optimizer.pkl'))
    # 训练100轮
    epoch = 100
    # 传入模型,优化器,开始训练
    if IS_TRAIN:
        train(epoch, model, optimizer)
    else:
        test(model)


if __name__ == '__main__':
    main()

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bugcoder-9905

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值