第十四周:pytorch入门(手写数字辨识)

摘要

本周我使用PyTorch框架成功实现了手写数字识别模型。手写数字识别是计算机视觉领域的重要任务之一,具有广泛的应用前景。在本文中,我将介绍如何使用PyTorch构建一个简单的神经网络模型来识别手写数字。首先,我将解释数据集的准备过程,然后详细讲解模型的架构和参数设置。接下来,我会介绍训练过程,包括损失函数的选择和优化器的使用。最后,我会展示一些测试结果。通过这个项目,可以了解如何使用PyTorch构建和训练一个简单但有效的手写数字识别模型以及对于pytorch中的数据加载、迭代数据集。

Abstract

This week I successfully implemented a handwritten digit recognition model using the PyTorch framework. Handwritten digit recognition is one of the important tasks in the field of computer vision and has a wide range of applications. In this article, I will introduce how to build a simple neural network model to recognize handwritten digits using PyTorch. First, I will explain the process of preparing the dataset, and then explain in detail the architecture and parameter settings of the model. Next, I will introduce the training process, including the choice of loss function and the use of optimizer. Finally, I will show some test results. Through this project, you can understand how to use PyTorch to build and train a simple but effective handwritten digit recognition model as well as for data loading, iterating dataset in pytorch.

1. pytorch中的数据加载

在torch中提供了数据集的基类 torch.utils.data.Dataset,继承这个基类,我们能够非常快速的实现对数据的加载。
torch.utils.data.Dataset 的源码如下:


```python
class Dataset(object):
	def __getitem__(self, index):
		raise NotImplementedError
	
	def __len__(self):
		raise NotImplementedError

	def __add__(self, other):
		return ConcatDataset([self, other])

可知: 我们需要在自定义的数据集中继承 Dataset 类,同时还需要实现两个方法:

  1. __ len __ 方法,能够实现通过全局的 len() 方法获取其中的元素个数
  2. __ getitem __ 方法,能够通过传入索引的方式获取数据,例如通过 dataset[i] 获取其中的第 i 条数据

2. 数据加载案例(Dataset)

下面通过一个例子来看看如何使用 Dataset 来加载数据。

数据来源:http://archive.ics.uci.edu/dataset/228/sms+spam+collection

数据介绍:SMS Spam Collection 是用于骚扰短信识别的经典数据集,完全来自真实短信内容。每行完整记录一条短信内容,每行开头通过ham和spam标识正确短信和骚扰短信。

数据实例:
在这里插入图片描述
之后对 Dataset 进行实例化,可以获取其中的数据,实现如下:

import torch
from torch.utils.data import Dataset

data_path = r"C:\Users\10245\Desktop\pytorch练习\数据集实例\data\SMSSpamCollection"

# 完成数据集
class MyDataset(Dataset):
    def __init__(self):
        self.lines = open(data_path, encoding= 'utf-8').readlines()

    def __getitem__(self, index):
        # 获取索引对应位置的一条数据
        cur_line = self.lines[index].strip()
        label = cur_line[:4].strip()
        content = cur_line[4:].strip()
        return label, content

    def __len__(self):
        # 返回数据的总数量
        return len(self.lines)

if __name__ == '__main__':
    my_dataset = MyDataset()
    print(my_dataset[0])
    print(len(my_dataset))

输出如下:
在这里插入图片描述

但是在深度学习中,数据量通常非常多,非常大的,如此大的数据量,不可能一次性的在模型中进行向前的计算和反向传播,经常我们会对整个数据进行随机的打乱顺序,把数据处理成一个个的batch,同时还会对数据进行预处理。

3. 迭代数据集(DataLoader)

使用上述的方法能够进行数据的读取,但是其中还有很多内容没有实现:

  • 批处理数据(Batching the data)
  • 打乱数据(Shuffling the data)
  • 使用多线程 multiprocessing 并行加载数据

在pytorch中 torch.utils.data.DataLoader 提供了上述的所用方法
DataLoader 的使用方法示例:

import torch
from torch.utils.data import Dataset,DataLoader

data_path = r"C:\Users\10245\Desktop\pytorch练习\数据集实例\data\SMSSpamCollection"

# 完成数据集
class MyDataset(Dataset):
    def __init__(self):
        self.lines = open(data_path, encoding= 'utf-8').readlines()

    def __getitem__(self, index):
        # 获取索引对应位置的一条数据
        cur_line = self.lines[index].strip()
        label = cur_line[:4].strip()
        content = cur_line[4:].strip()
        return label, content

    def __len__(self):
        # 返回数据的总数量
        return len(self.lines)

my_dataset = MyDataset()
data_loader = DataLoader(dataset=my_dataset, batch_size=2, shuffle=True)
if __name__ == '__main__':
    for i in data_loader:
        print(i)

其中参数含义:

  1. dataset:提前定义的 dataset 的实例
  2. batch_size:传入数据的 batch 的大小,常用128,256等
  3. shuffle:bool类型,表示是否在每次获取数据的时候提前打乱数据
  4. num_workers:加载数据的线程数

输出如下:
在这里插入图片描述

4. pytorch自带的数据集

pytorch中自带的数据集由两个上层api提供,分别是 torchvision 和 torchtext。
其中:

  • torchvision 提供了对图片数据处理相关的api和数据
    数据位置:torchvision.datasets,例如:torchvision.datasets.MNIST(手写数字图片数据)
  • torchtext 提供了对文本数据处理相关的API和数据
    数据位置:torchtext.datasets,例如:torchtext.datasets.IMDB(电影评论文本数据)

下面我们以MNIST手写数字为例,来看看pytorch如何加载其中自带的数据集。
使用方法和之前一样:

  1. 准备好 Dataset 实例
  2. 把dataset交给dataloader打乱顺序,组成batch

torchvision.datasets中的数据集类都是继承自Dataset,这意味着直接对torchvision.datasets.MNIST进行实例化就可以得到Dataset的实例,但是MNIST API中的参数需要注意一下:

torchvision.datasets.MNIST(root='/files/', train=True, download=True, transform=)
  1. root:表示数据存放的位置
  2. train:bool类型,表示使用训练集的数据还是测试集的数据
  3. download:bool类型,表示是否需要下载数据到root目录
  4. transform:实现对图片的处理函数

5. MNIST数据集的介绍

数据集的原始地址:http://yann.lecun.com/exdb/mnist/

MNIST是由 Yann LeCun 等人提供的免费的图像识别的数据集,其中包括60000个训练样本和10000个测试样本,其中图的尺寸已经进行了标准化处理,都是黑白图像,大小为28*28

执行代码,下载数据,观察数据类型:

import torch
from torchvision.datasets import MNIST

minst = MNIST(root="./data", train=True, download=False)
print(minst)

print(dataset[0])

下载的数据如下:
在这里插入图片描述
在这里插入图片描述

6. 使用pytorch实现手写数字识别

6.1 思路

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

6.2 MNIST数据的处理

准备数据集的方法前面已经了解过了,但是通过前面的内容可知,调用MNIST返回的结果中图形数据是一个image对象,需要对其进行处理。

为了进行数据的处理,接下来我们了解 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],彩色图片的通道数为(R,G,B),每个通道的每个像素点的取值为 [0,255],三个通道的颜色相互叠加,形成了各种颜色。

示例如下:

from torchvision import transforms
import numpy as np

minst = MNIST(root="./data", train=True, download=False)
data = np.random.randint(0,255,size=12)
img = data.reshape(2,2,3)  
print(img)
print(img.shape)
img_tensor = transforms.ToTensor()(img)
print(img_tensor)		
print(img_tensor.shape)	

运行结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/7545c813d59b47758ff308a46ab25c0e.png

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

给定均值:mean,shape和图片的通道数相同(指的是每个通道的均值),方差:std,和图片的通道数相同(指的是每个通道的方差),将会把 Tensor 规范化处理。

即:Normalized_image = (image - mean) / std

例如:

import torchvision
from torchvision import transforms
import numpy as np

data = np.random.randint(0, 255, size=12)
img = data.reshape(2, 2, 3)
img = transforms.ToTensor()(img).float()
print(img)
print("*" * 100)

norm_img = transforms.Normalize((10, 10, 10), (1, 1, 1))(img)
print(norm_img)

运行结果:
在这里插入图片描述

3). torchvision.transforms.Compose(transforms)

将多个transform组合起来使用

transforms.Compose([
	torchvision.transforms.ToTensor()	#先转化为Tensor
	torchvision.transforms.Normalize(mean,std)   #再进行正则化
])

6.3 准备MNIST数据集的Dataset和DataLoader

导入项目依赖包

from torchvision.datasets import MNIST
from torchvision.transforms import Compose,Normalize,ToTensor
from torch.utils.data import DataLoader

准备训练集

# 准备数据集,其中0.1307,0.3081为MNIST数据的均值和标准差,这样操作能够对其进行标准化
# 因为MNIST只有一个通道(黑白图片),所以元组中只有一个值
def get_dataloader(train=True):
    transform_fn = Compose([
        ToTensor(),
        Normalize(mean=(0.1307),std=(0.3081))
    ])
    dataset = MNIST(root='./data', train=train, transform=transform_fn)
    # 准备数据迭代器
    data_loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
    return data_loader

6.4 构建模型

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

那么在这个模型中有三个地方需要我们注意:

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

6.4.1 激活函数的使用

Relu激活函数是由 import torch.nn.functional as F 提供,F.relu(x) 即可对 x 进行处理。

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

  1. 原始输入数据的形状:[batch_size, 1, 28, 28]
  2. 进行形状的修改:[batch_size, 28*28],(全连接层是在进行矩阵乘法的操作)
  3. 第一个全连接层的输出形状:[batch_size, 28]
  4. 激活函数不会修改数据的形状
  5. 第二个全连接层的输出形状:[batch_size, 10],因为手写数字有10个类别

构建模型的代码如下:

import torch.nn.functional as F
import torch.nn as nn

class MnistModel(nn.Module):
    def __init__(self):
        super(MnistModel, self).__init__()
        self.fc1 = nn.Linear(1*28*28, 28)
        self.fc2 = nn.Linear(28, 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.fc1(x)
        # 3. 进行激活函数处理,形状没有变化
        F.relu(x)
        # 4. 输出层
        out = self.fc2(x)
        return out

6.5 模型的损失函数

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

回顾我们之前所学习的内容,我们在逻辑回归中,我们使用 sigmod 进行计算对数似然损失,以此来定义我们的二分类的损失。

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

那么在多分类的过程中我们应该怎么做呢?

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

例如下图:
在这里插入图片描述
在这里插入图片描述
假如加入softmax之前的输出结果是 2.3,4.1,5.6,那么经过softmax之后的结果是什么呢?
Y 1 = e 2.3 e 2.3 + e 4.1 + e 5.6 Y_{1} = \frac{e^{2.3}}{e^{2.3}+e^{4.1}+e^{5.6}} Y1=e2.3+e4.1+e5.6e2.3 Y 2 = e 4.1 e 2.3 + e 4.1 + e 5.6 Y_{2} = \frac{e^{4.1}}{e^{2.3}+e^{4.1}+e^{5.6}} Y2=e2.3+e4.1+e5.6e4.1 Y 3 = e 5.6 e 2.3 + e 4.1 + e 5.6 Y_{3} = \frac{e^{5.6}}{e^{2.3}+e^{4.1}+e^{5.6}} Y3=e2.3+e4.1+e5.6e5.6对于这个softmax输出的结果,是在[0,1]区间,我们可以把它当作概率。和前面二分类的损失一样,多分类的损失只需要再把这个结果进行对数似然损失的计算即可,即: J = − ∑ Y l o g ( P ) J=-\sum Ylog(P) J=Ylog(P)其中 P = e z j ∑ k = 1 K e z k P=\frac{e^{z_{j}}}{ {\textstyle \sum_{k=1}^{K}} e^{z_{k}}} P=k=1Kezkezj(也就是上式我们计算的 Y 1 , Y 2 , Y 3 Y_1,Y_2,Y_3 Y1,Y2,Y3), Y Y Y表示真实值,最后,会计算每个样本的损失,即上式的平均值,我们把softmax概率传入对数似然损失得到的损失函数称为交叉熵损失。

在pytorch中有两种方法实现交叉熵损失:

方法1

criterion = nn.CrossEntropyLoss()
loss = criterion(input, target)

方法2

# 1. 对输出值计算softmax和取对数
output = F.log_softmax(x, dim=-1)
# 2. 使用torch中带权损失
loss = F.nll_loss(output, target)

带权损失定义为: l n = − ∑ w i x i l_{n}=-\sum w_{i}x_{i} ln=wixi,其实就是把 l o g ( P ) log(P) log(P) 作为 x x x,把真实值 Y Y Y 作为权重。

6.6 模型的训练

训练的流程:

  1. 实例化模型,设置模型为训练模式
  2. 实例化优化器类,实例化损失函数
  3. 获取并遍历dataloader
  4. 梯度置0
  5. 进行前向计算
  6. 计算损失
  7. 反向传播
  8. 更新参数

代码如下:

model = MnistModel()
optimizer = Adam(model.parameters(), lr=0.001)
def train(epoch):
    """
    实现训练的过程
    :param epoch: 训练的轮数
    :return:
    """
    data_loader = get_dataloader()
    for idx, (input,target) in enumerate(data_loader):
        optimizer.zero_grad()   # 梯度置0
        output = model(input)   # 调用模型,得到预测值
        loss = F.nll_loss(output, target)   # 得到损失
        loss.backward()         # 反向传播
        optimizer.step()        # 梯度的更新
        if idx%10 == 0:
            print(epoch,idx,loss.item())

6.7 模型的保存和加载

模型的保存

torch.save(mnist_net.state_dict()), "model/mnist_net.pt") # 保存模型参数
torch.save(optimizer.state_dict(), "results/mnist_optimizer.pt") # 保存优化器参数

模型的加载

mnist_net.load_state_dict(torch.load("model/mnist_net.pt"))
optimizer.load_state_dict(torch.load("results/mnist_optimizer.pt")) 

6.8 模型的评估

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

  1. 不需要计算梯度
  2. 需要收集损失和准确率,用来计算平均损失和平均准确率
  3. 损失的计算和训练计算方法相同
  4. 准确率的计算
    • 模型的输出为[batch_size, 10]的形状
    • 其中最大值的位置就是其预测的目标值(预测值进行过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, 10] target: [batch_size, 1]
            pred = output.max(dim=-1)[-1]
            cur_acc = pred.eq(target).float().mean()
            acc_list.append(cur_acc)
    print("平均准确率,平均损失",np.mean(acc_list),np.mean(loss_list))

在这里插入图片描述

7. 完整代码

在这里插入图片描述

# 使用pytorch完成手写数字识别
from torchvision.datasets import MNIST
from torchvision.transforms import Compose,Normalize,ToTensor
from torch.utils.data import DataLoader
from torch.optim import Adam
import torch.nn.functional as F
import torch.nn as nn
import torch
import os
import numpy as np

BATCH_SIZE = 128
TEST_BATCH_SIZE = 1000
# 1. 准备数据集
def get_dataloader(train=True, batch_size=BATCH_SIZE):
    transform_fn = Compose([
        ToTensor(),
        Normalize(mean=(0.1307),std=(0.3081))
    ])
    dataset = MNIST(root='./data', train=train, transform=transform_fn)
    data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
    return data_loader

# 2. 构建模型
class MnistModel(nn.Module):
    def __init__(self):
        super(MnistModel, self).__init__()
        self.fc1 = nn.Linear(1*28*28, 28)
        self.fc2 = nn.Linear(28, 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.fc1(x)
        # 3. 进行激活函数处理,形状没有变化
        F.relu(x)
        # 4. 输出层
        out = self.fc2(x)
        return F.log_softmax(out, dim=-1)

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):
    """
    实现训练的过程
    :param epoch: 训练的轮数
    :return:
    """
    data_loader = get_dataloader()
    for idx, (input,target) in enumerate(data_loader):
        optimizer.zero_grad()   # 梯度置0
        output = model(input)   # 调用模型,得到预测值
        loss = F.nll_loss(output, target)   # 得到损失
        loss.backward()         # 反向传播
        optimizer.step()        # 梯度的更新
        if idx%100 == 0:
            print(epoch,idx,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, 10] target: [batch_size, 1]
            pred = output.max(dim=-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(10):
        train(i)
    test()

总结

通过实现手写数字识别案例,让我了解了如何去将之前学习的理论进行实现,也让我对pytorch中常用的API有了初步的了解,以及对一个案例,该如何去部署。下周我将通过pytorch实现深度学习网络,如CNN、RNN的构建,同时下周再继续巩固机器学习的基础。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

@默然

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

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

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

打赏作者

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

抵扣说明:

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

余额充值