PyTorch手写数据集MNIST识别(Python功能模块化详细流程+解释)开箱即用

前言

代码基本上给出了注释,MnistModel类的具体解释以及验证结果代码在下面。不求甚解者,可以直接复制代码。

效果

在这里插入图片描述

前提

如果还没有配置好pytorch环境的可以去看Pytorch安装,这一篇就够了,绝不踩坑

训练代码

import torch
import os
import numpy as np
import torch.nn as nn
import torch.optim as optim

from numpy import ndarray
from tqdm import tqdm
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# batch_size(每批处理的数据, 根据性能选择)
BATCH_SIZE = 60
# 选择gpu运行
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 训练次数
EPOCHS = 1
# 构建transform,对图像做处理
my_transforms = transforms.Compose([
    # 将图片转换成PyTorch处理的tensor格式
    transforms.ToTensor(),
    # 进行正则化(对抗过拟合) 0.1307,0.3081分别为官网查得的均值和方差值。
    transforms.Normalize(mean=(0.1307,), std=(0.3081,))
])
# 定义损失函数
loss_function = nn.CrossEntropyLoss()


class MnistModel(nn.Module):
    """
    定义一个继承自nn.Module的手写数字识别模型类
    """

    def __init__(self):
        """
        初始化函数,构建神经网络的各个层
        """
        # 调用父类的初始化函数
        super().__init__()
        # 输入层到隐藏层的全连接层,将28*28的图片展开成1维向量并传入100个神经元
        self.fc1 = nn.Linear(1 * 28 * 28, 100)
        # 激活函数ReLU
        self.relu = nn.ReLU()
        # 隐藏层到输出层的全连接层,将100维的特征向量传入10个神经元,输出10维的向量
        self.fc2 = nn.Linear(100, 10)

    def forward(self, image):
        """
        前向传播函数,计算模型输出结果
        """
        # 将输入图片展开成2维向量
        image_viewed = image.view(-1, 1 * 28 * 28)
        # 输入层到隐藏层的全连接计算
        out_1 = self.fc1(image_viewed)
        # 激活函数ReLU
        fc1 = self.relu(out_1)
        # 隐藏层到输出层的全连接计算
        out_2 = self.fc2(fc1)
        # 返回最终输出结果,即10维的向量
        return out_2


def deal_data() -> tuple:
    """
    划分数据集
    :return:
    """
    # 下载、处理数据集(root=保存路径,train=True代表训练集,False为测试集,download=False代表不需要再下载)
    train_data = datasets.MNIST(root="../MNIST_data",
                                train=True,
                                transform=my_transforms,
                                download=False)
    test_data = datasets.MNIST(root="../MNIST_data",
                               train=False,
                               transform=my_transforms,
                               download=False)

    # 加载数据集(其中shuffle决定的是是否打乱数据,为了提高模型精度选择True打乱。)
    # 训练集
    part_train_loader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True)
    # 测试集
    part_test_loader = DataLoader(test_data, batch_size=BATCH_SIZE, shuffle=True)
    return part_train_loader, part_test_loader


def train_model() -> ndarray:
    """
    训练
    :return:
    """
    total_loss = []
    # 这一步借用了tqdm实现了进度条打印的功能
    dataloader = tqdm(train_loader, total=len(train_loader))
    model.train()  # PyTorch提供的训练方法
    for (data, label) in dataloader:
        # 部署到DEVICE
        data, label = data.to(DEVICE), label.to(DEVICE)
        # 梯度初始化为0
        optimizer.zero_grad()
        # 向前传播
        output = model(data)
        # 计算损失
        loss = loss_function(output, label)
        total_loss.append(loss.item())
        # 反向传播
        loss.backward()
        # 优化器更新
        optimizer.step()

    # 保存模型
    torch.save(model.state_dict(), './models/model.pkl')
    # 保存优化器
    torch.save(optimizer.state_dict(), './models/optimizer.pkl')
    return np.mean(total_loss)


def test_model() -> ndarray:
    """
    测试
    :return:
    """
    # 这一步借用了tqdm实现了进度条打印的功能
    dataloader = tqdm(test_loader, total=len(test_loader))
    # 模型验证
    model.eval()
    # 统计正确率
    succeed = []
    with torch.no_grad():  # 不计算梯度,不反向传播
        for (data, label) in dataloader:
            data, label = data.to(DEVICE), label.to(DEVICE)
            # 测试数据
            output = model(data)
            # 找到概率值最大的下标
            result = output.argmax(dim=1)
            # 累计正确率
            succeed.append(result.eq(label).float().mean().item())

    return np.mean(succeed)


if __name__ == "__main__":
    # 模型实例化(传给gpu使用)
    model = MnistModel().to(DEVICE)
    # 优化器:更新模型参数,使训练结果达到最优值
    optimizer = optim.Adam(model.parameters())

    # 加载优化好的模型和优化器继续进行训练
    if os.path.exists('./models/model.pkl'):
        model.load_state_dict(torch.load('./models/model.pkl'))
        optimizer.load_state_dict(torch.load('./models/optimizer.pkl'))

    # 加载数据集
    train_loader, test_loader = deal_data()

    # 训练
    for epoch in range(EPOCHS):
        mean_loss = train_model()
        mean_succeed = test_model()
        print(f"第{
      
      epoch+1}次epoch---损失: {
      
      mean_loss}---成功率: {
      
      mean_succeed}")

MnistModel类

当我们创建一个神经网络模型类时,需要继承 nn.Module 这个父类,并实现其中的 init 和 forward 两个方法。其中,init 方法用于定义模型的结构,forward 方法用于定义模型的前向计算过程。

在这个 MnistModel 类中,init 方法定义了两个线性层和一个 ReLU 激活函数,其中 self.fc1 是输入大小为 784,输出大小为 100 的线性层,self.fc2 是输入大小为 100,输出大小为 10 的线性层。这里的输出大小为 10 是因为手写数字识别任务需要将每张图片分类为 0~9 中的一个数字,因此最终输出结果的维度需要是 10。

在 forward 方法中,首先使用 view 函数将输入图像变换为一维向量,大小为 784。这是因为神经网络中的线性层只能接收一维的输入,所以需要将二维的图像展平为一维向量。接着,将展平后的图像向量传递给第一个线性层 self.fc1 进行线性变换,得到输出 out_1。然后将 out_1 输入到 ReLU 激活函数中进行非线性变换,得到激活后的输出 fc1。最后将 fc1 输入到第二个线性层 self.fc2 中进行线性变换,得到最终输出结果 out_2,其维度为 10,表示每个数字的分类概率。

总体来说,这个 MnistModel 类的结构就是:输入图像 → 展平为一维向量 → 第一个线性层(输出大小为 100) → ReLU 激活函数 → 第二个线性层(输出大小为 10) → 输出分类结果。

问题1-image.view(-1, 1 * 28 * 28) 这个-1是什么意思

在 PyTorch 中,使用 view 函数可以将张量变形为指定的形状。其中,如果我们不确定某一维的大小,可以使用 -1 来表示该维度的大小应该根据其他维度的大小自动计算得出。

在这个例子中,image 是输入的手写数字图像,其大小为 (batch_size, 1, 28, 28),其中 batch_size 表示每次输入的图像数量。在 forward 方法中,我们需要将图像展平为一维向量,大小为 1 * 28 * 28,所以使用 view 函数将其变形为 (batch_size, 1 * 28 * 28)。

由于 batch_size 可能不确定,所以使用 -1 来表示该维度的大小应该根据其他维度的大小自动计算得出,最终的形状就是 (batch_size, 784)。例如,如果 batch_size 为 64,那么展平后的图像向量大小就为 (64, 784)。

问题2-ReLU 激活函数有什么作用,为什么self.fc2后面又不用激活函数了

ReLU(Rectified Linear Unit)激活函数是神经网络中常用的一种非线性激活函数。它的作用是将线性变换后的输出进行非线性映射,增强模型的表达能力,从而提高模型的性能。

具体来说,ReLU 激活函数定义为: f ( x ) = m a x ( 0 , x ) f(x) = max(0, x) f(x)=max(0,x)。其中,当输入 x x x 大于 0 时,输出等于输入 x x x;当输入 x x x 小于等于 0 时,输出为 0。这样可以将所有的负数都映射为 0,使得神经元具有一定的稀疏性,从而减少过拟合的风险。同时,ReLU 激活函数也具有较好的计算性能,因为其只需要比较输入和 0 的大小即可。

在这个例子中,self.fc1 和 self.fc2 都是线性层,即进行矩阵乘法和加法运算的操作,不具有非线性映射的能力。因此,在两个线性层之间添加 ReLU 激活函数可以增强模型的表达能力,提高分类性能。而在最后一个线性层 self.fc2 后面没有添加激活函数的原因是,这个线性层的输出已经是最终的分类结果,不需要再进行非线性映射了。最终的分类结果需要输入到损失函数中进行计算和优化,而不需要再进行任何的激活函数操作。

class MnistModel(nn.Module):
    """
    定义一个继承自nn.Module的手写数字识别模型类
    """

    def __init__(self):
        """
        初始化函数,构建神经网络的各个层
        """
        # 调用父类的初始化函数
        super().__init__()
        # 输入层到隐藏层的全连接层,将28*28的图片展开成1维向量并传入100个神经元
        self.fc1 = nn.Linear(1 * 28 * 28, 100)
        # 激活函数ReLU
        self.relu = nn.ReLU()
        # 隐藏层到输出层的全连接层,将100维的特征向量传入10个神经元,输出10维的向量
        self.fc2 = nn.Linear(100, 10)

    def forward(self, image):
        """
        前向传播函数,计算模型输出结果
        """
        # 将输入图片展开成2维张量
        image_viewed = image.view(-1, 1 * 28 * 28)
        # 输入层到隐藏层的全连接计算
        out_1 = self.fc1(image_viewed)
        # 激活函数ReLU
        fc1 = self.relu(out_1)
        # 隐藏层到输出层的全连接计算
        out_2 = self.fc2(fc1)
        # 返回最终输出结果,即10维的向量
        return out_2

验证代码

import torch

from torchvision import transforms
from torch import nn, load, no_grad
from PIL import Image

# 选择gpu运行
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 构建transform,对图像做处理
my_transforms = transforms.Compose([
    # 讲图片转换为1通道
    transforms.Grayscale(1),
    # 将图片转换成PyTorch处理的tensor格式
    transforms.ToTensor(),
    # 进行正则化(对抗过拟合) 0.1307,0.3081分别为官网查得的均值和方差值。
    transforms.Normalize(mean=(0.1307,), std=(0.3081,))
])


# 全连接层
class MnistModel(nn.Module):
    """
    定义一个继承自nn.Module的手写数字识别模型类
    """

    def __init__(self):
        """
        初始化函数,构建神经网络的各个层
        """
        # 调用父类的初始化函数
        super().__init__()
        # 输入层到隐藏层的全连接层,将28*28的图片展开成1维向量并传入100个神经元
        self.fc1 = nn.Linear(1 * 28 * 28, 100)
        # 激活函数ReLU
        self.relu = nn.ReLU()
        # 隐藏层到输出层的全连接层,将100维的特征向量传入10个神经元,输出10维的向量
        self.fc2 = nn.Linear(100, 10)

    def forward(self, image):
        """
        前向传播函数,计算模型输出结果
        """
        # 将输入图片展开成2维张量
        image_viewed = image.view(-1, 1 * 28 * 28)
        # 输入层到隐藏层的全连接计算
        out_1 = self.fc1(image_viewed)
        # 激活函数ReLU
        fc1 = self.relu(out_1)
        # 隐藏层到输出层的全连接计算
        out_2 = self.fc2(fc1)
        # 返回最终输出结果,即10维的向量
        return out_2


if __name__ == "__main__":
    # 模型实例化(传给gpu使用)
    model = MnistModel().to(DEVICE)
    # 加载模型
    model.load_state_dict(load('./models/model.pkl'))
    # 打开图片
    image = Image.open('./models/2.jpg')
    # 对图片进行转换成PyTorch处理的tensor格式,并移动到模型所在设备上
    image = my_transforms(image).to(DEVICE)
    # 预测
    model.eval()

    # 不进行梯度计算
    with no_grad():
        # 获取结果
        out_put = model(image)
        out_put.max(dim=1)
        # 对模型输出结果out_put在维度1上取最大值。因为模型输出结果是10维的向量,所以维度1就是指在这个10维的向量上取最大值。
        # .indices是获取最大值的索引,也就是预测的结果。
        result = out_put.max(dim=1).indices
        print(result.item())

借鉴

ChatGPT
猿人学-安然导师
手把手实战PyTorch手写数据集MNIST识别项目全流程

猜你喜欢

转载自blog.csdn.net/qq_41866988/article/details/129821219