深度学习入门:3.手写数字分类

这一章将基于Pytorch在只用全连接网络情况下实现手写数字识别,让大家基本了解怎么实现模型训练和预测使用。完整代码在最下面。

一.MNIST数据集

1.数据集介绍

MNIST 数据集包含 60,000 个训练样本和 10,000 个测试样本,每个样本都是 28x28 像素的灰度图像,表示一个 0 到 9 的手写数字。每个图像都有一个对应的标签,表示图像中的数字。样例如下:

尽管手写数字识别是一个复杂的任务,但 MNIST 数据集相对简单,因为所有图像都是经过归一化和中心化的,且背景干净,没有复杂的背景或噪声。在实现分类上也比较容易,模型收敛较快,就算用CPU训练模型也可以在短时间内收敛。

2.MNIST数据集下载与预处理

trainset =torchvision.datasets.MNIST('./data/', train=True, download=True,    #训练集下载
                               transform=torchvision.transforms.Compose([
                                   torchvision.transforms.ToTensor(),       #转换数据类型为Tensor
                                   torchvision.transforms.Normalize(
                                       (0.1307,), (0.3081,))    #数据标准化
                               ]))

#------------------------------------------------------------------#
#       将训练集再划分为训练集和测试集(训练集:测试集=4:1)    #
#------------------------------------------------------------------#
train_size = len(trainset)
indices = list(range(train_size))

# 划分索引
split = int(0.8 * train_size)
train_indices, val_indices = indices[:split], indices[split:]

# 创建训练集和验证集的子集
trainset_subset = torch.utils.data.Subset(trainset, train_indices)
valset_subset = torch.utils.data.Subset(trainset, val_indices)

# 创建数据加载器
train_loader = torch.utils.data.DataLoader(trainset_subset, batch_size=batch_size_train, shuffle=True)
eval_loader = torch.utils.data.DataLoader(valset_subset, batch_size=batch_size_eval, shuffle=False)


test_loader = torch.utils.data.DataLoader(
    torchvision.datasets.MNIST('./data/', train=False, download=True,   #测试集下载
                               transform=torchvision.transforms.Compose([
                                   torchvision.transforms.ToTensor(),
                                   torchvision.transforms.Normalize(
                                       (0.1307,), (0.3081,))
                               ])),
    batch_size=batch_size_test, shuffle=True)

Pytorch已经提供了MNIST数据集,只要调用相关代码进行下载调用即可,下载后还需要进行预处理才能训练模型,需要将数据集的数据类型转换为张量;再将数据标准化,加快模型收敛速度。数据标准化所用的均值0.1307和标准差0.3081这些系数是数据提供方计算好的数据。为了找到最佳模型,将原本训练集分成训练集和验证集(训练集:验证集=4:1)。

二、神经网络模型

注:本章只是让大家简单了解模型的训练和使用,神经网络模型只使用了全连接层,并未加入卷积神经网络。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(784, 196)
        self.fc2 = nn.Linear(196, 49)
        self.fc3 = nn.Linear(49, 10)
    def forward(self, x):
        x = x.view(-1, 784)  # 降维
        x = F.relu(self.fc1(x))  # 激活函数relu
        x = F.dropout(x, training=self.training)  #dropout正则化,降低过拟合
        x = self.fc2(x)
        x = self.fc3(x)
        return F.log_softmax(x)

如上代码所示,构建的神经网络模型主要包括三个全连接层(fc1,fc2,fc3)。由于输入张量的维度为(28,28,1),全连接层只能处理一维向量,所以需要将张量进行降维处理。使用x.view(-1,784)进行降维(28*28*1=784)。

激活函数作用:

  1. 非线性化:神经网络中的线性组合(如权重乘以输入加上偏置)本身是线性的。为了捕获数据中的非线性关系,需要引入非线性元素,这就是激活函数的作用。
  2. 增强模型的表达能力:通过引入非线性,激活函数允许神经网络学习复杂的数据表示和模式。
  3. 控制输出范围:某些激活函数(如Sigmoid和Tanh)将输出限制在特定的范围内,这有助于标准化输出,并可能使某些优化问题更容易解决。
  4. 引入稀疏性:某些激活函数(如ReLU及其变体)在输入为负时输出为零,这有助于引入稀疏性,即许多神经元的输出为零。这可以提高计算效率,并可能有助于防止过拟合。
  5. 优化梯度传播:某些激活函数(如ReLU)在输入为正时具有恒定的梯度,这有助于缓解梯度消失问题,特别是在深度网络中。

dropout正则化,对某一层使用dropout,就是在训练过程中随机将该层的一些输出特征舍弃(即舍弃的特征值设置为0),有效的降低模型的过拟合问题。

网络结构如下:

三、优化器和损失函数

优化器:使用SGD优化器,在PyTorch中,optim.SGD 是用于实现随机梯度下降(Stochastic Gradient Descent, SGD)优化算法的类。这个类用于更新和计算模型(如神经网络)的参数,以便在训练过程中最小化损失函数。

损失函数:负对数似然损失(Negative Log Likelihood Loss,简称NLL Loss)是用于分类任务的一种损失函数,尤其是在处理具有softmax或log-softmax输出的神经网络时。它的基本思想是衡量模型预测的类别概率分布与真实类别概率分布之间的差异。

假设有 N 个样本,每个样本的真实标签为 yi​,对应的模型预测概率为 pyi​​,则平均负对数似然损失可以表示为:

L = -\frac{1}{N} \sum_{i=1}^{N} \log(p_{y_i})

四、训练模型

训练模型可以选择CPU或者GPU训练,CPU训练速度比GPU慢很多,但数据集和模型规模都比较小,CPU训练也不会花费太长时间。若使用GPU训练模型,需要先将输入和模型加载到GPU上,才能使用CUDA加速训练。

def train(epoch,epochs):
    #训练模型
    train_loss=0
    network.train()
    pbar = tqdm(total=len(train_loader), desc=f'Epoch {epoch + 1}/{epochs}', mininterval=0.3)
    for batch_idx, (data, target) in enumerate(train_loader): #批次,输入数据,标签
        data = data.to(device)
        target=target.to(device)
        optimizer.zero_grad()   #清空优化器中的梯度
        output = network(data)  #前向传播,获得当前模型的预测值
        loss = F.nll_loss(output, target) #真实值和预测值之前的损失
        loss.backward() #反向传播,计算损失函数关于模型中参数梯度
        optimizer.step() #更新模型中参数
        #输出当前训练轮次,批次,损失等
        train_loss +=loss.item()
        '''torch.save(network.state_dict(), './model.pth')
        torch.save(optimizer.state_dict(), './optimizer.pth')'''
        pbar.set_postfix(**{'train loss'  : train_loss/(batch_idx+1)})
        pbar.update(1)
    return train_loss/(batch_idx+1)

def eval(epoch,epochs):
    #测试模型
    network.eval()
    pbar = tqdm(total=len(eval_loader), desc=f'Epoch {epoch + 1}/{epochs}', mininterval=0.3)
    eval_loss = 0
    with torch.no_grad(): #仅测试模型,禁用梯度计算
        for batch_idx, (data, target) in enumerate(eval_loader):
            data=data.to(device)
            target=target.to(device)
            output = network(data)
            eval_loss += F.nll_loss(output, target).item()
            pbar.set_postfix(**{'eval loss': eval_loss / (batch_idx + 1)})
            pbar.update(1)
    return eval_loss/(batch_idx + 1)

训练模型代码如上,训练集用于训练模型,验证集用于评估模型在未见过的数据上的性能,从而帮助选择最佳的模型架构和超参数。最后将在验证集上损失最小的模型权重保存下来。

五、测试模型

测试集是模型在训练和验证过程中从未见过的数据,通过在测试集上评估模型,我们可以获得对模型泛化能力的真实估计,即模型对未见数据的预测能力。

def test():
    #如果已经训练好了权重,模型直接加载权重文件进行测试#
    model_test=Net()
    model_test.load_state_dict(torch.load('model.pth'))
    model_test.eval()
    model_test=model_test.to(device)
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data=data.to(device)
            target=target.to(device)
            output = model_test(data)
            test_loss += F.nll_loss(output, target, size_average=False).item()
            pred = output.data.max(1, keepdim=True)[1]  #模型的分类结果
            correct += pred.eq(target.data.view_as(pred)).sum() #模型的分类结果和真实类别对比,统计正确的个数
    test_loss /= len(test_loader.dataset)
    print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))


    #------------------------------------------------------------------------------------------------------------#
    #                展示一些测试样本,直接用cpu运行,如果继续用cuda后续还需要将结果转到cpu再转numpy数据类型,非常麻烦           #
    # ----------------------------------------------------------------------------------------------------------#
    model_test=model_test.to('cpu')
    examples = enumerate(test_loader)
    batch_idx, (example_data, example_targets) = next(examples)
    with torch.no_grad():
        output = model_test(example_data)
    for i in range(6):
        plt.subplot(2, 3, i + 1)
        plt.tight_layout()
        plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
        plt.title("Prediction: {}".format(output.data.max(1, keepdim=True)[1][i].item()))
        plt.xticks([])
        plt.yticks([])
    plt.show()

最后几个样本的预测结果如下。

完整代码如下

import numpy as np
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
from tqdm import tqdm

batch_size_train = 64    #训练模型时,一个批次的数据(当前代表64个MNIST图片),用cuda的可以相应将参数设置大些
# 意味着在每次权重更新时,你会从训练集中选择64个样本(也称为一个“批次”)来计算梯度,并使用这些梯度来更新模型的权重。
batch_size_eval = 200   #测试模型时,一次批次的数据(当前代表200个MNIST图片)
batch_size_test = 200  #测试模型时,一次批次的数据(当前代表200个MNIST图片)
learning_rate = 0.01 #学习率
momentum = 0.5  #使用optim.SGD(随机梯度下降)优化器时,momentum是一个重要的参数。它代表了动量(Momentum)的大小,是动量优化算法中的一个关键概念。
log_interval = 10
random_seed = 1
torch.manual_seed(random_seed) #用于设置随机数生成器种子(seed)的函数。设置种子可以确保在每次运行代码时,与随机数生成相关的操作(如权重初始化、数据打乱等)都会生成相同的随机数序列,从而使结果具有可复现性。

trainset =torchvision.datasets.MNIST('./data/', train=True, download=True,    #训练集下载
                               transform=torchvision.transforms.Compose([
                                   torchvision.transforms.ToTensor(),       #转换数据类型为Tensor
                                   torchvision.transforms.Normalize(
                                       (0.1307,), (0.3081,))    #数据标准化
                               ]))

#------------------------------------------------------------------#
#       将训练集再划分为训练集和测试集(训练集:测试集=4:1)    #
#------------------------------------------------------------------#
train_size = len(trainset)
indices = list(range(train_size))

# 划分索引
split = int(0.8 * train_size)
train_indices, val_indices = indices[:split], indices[split:]

# 创建训练集和验证集的子集
trainset_subset = torch.utils.data.Subset(trainset, train_indices)
valset_subset = torch.utils.data.Subset(trainset, val_indices)

# 创建数据加载器
train_loader = torch.utils.data.DataLoader(trainset_subset, batch_size=64, shuffle=True)
eval_loader = torch.utils.data.DataLoader(valset_subset, batch_size=64, shuffle=False)



test_loader = torch.utils.data.DataLoader(
    torchvision.datasets.MNIST('./data/', train=False, download=True,   #测试集下载
                               transform=torchvision.transforms.Compose([
                                   torchvision.transforms.ToTensor(),
                                   torchvision.transforms.Normalize(
                                       (0.1307,), (0.3081,))
                               ])),
    batch_size=batch_size_test, shuffle=True)


def show_MNIST():
    #展示MNIST图片
    examples = enumerate(test_loader)
    batch_idx, (example_data, example_targets) = next(examples)
    fig = plt.figure()
    for i in range(6):
        plt.subplot(2, 3, i + 1)
        plt.tight_layout()
        plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
        plt.title("Ground Truth: {}".format(example_targets[i]))
        plt.xticks([])
        plt.yticks([])
    plt.show()


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(784, 196)
        self.fc2 = nn.Linear(196, 49)
        self.fc3 = nn.Linear(49, 10)
    def forward(self, x):
        x = x.view(-1, 784)  # 降维
        x = F.relu(self.fc1(x))  # 激活函数relu
        x = F.dropout(x, training=self.training)  #dropout正则化,降低过拟合
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return F.log_softmax(x,dim=1)



network = Net()
optimizer = optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum)  #优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") #检测电脑是否能使用cuda训练,不行则使用cpu
network=network.to(device)

def train(epoch,epochs):
    #训练模型
    train_loss=0
    network.train()
    pbar = tqdm(total=len(train_loader), desc=f'Epoch {epoch + 1}/{epochs}', mininterval=0.3)
    for batch_idx, (data, target) in enumerate(train_loader): #批次,输入数据,标签
        data = data.to(device)
        target=target.to(device)
        optimizer.zero_grad()   #清空优化器中的梯度
        output = network(data)  #前向传播,获得当前模型的预测值
        loss = F.nll_loss(output, target) #真实值和预测值之前的损失
        loss.backward() #反向传播,计算损失函数关于模型中参数梯度
        optimizer.step() #更新模型中参数
        #输出当前训练轮次,批次,损失等
        train_loss +=loss.item()
        '''torch.save(network.state_dict(), './model.pth')
        torch.save(optimizer.state_dict(), './optimizer.pth')'''
        pbar.set_postfix(**{'train loss'  : train_loss/(batch_idx+1)})
        pbar.update(1)
    return train_loss/(batch_idx+1)

def eval(epoch,epochs):
    #测试模型
    network.eval()
    pbar = tqdm(total=len(eval_loader), desc=f'Epoch {epoch + 1}/{epochs}', mininterval=0.3)
    eval_loss = 0
    with torch.no_grad(): #仅测试模型,禁用梯度计算
        for batch_idx, (data, target) in enumerate(eval_loader):
            data=data.to(device)
            target=target.to(device)
            output = network(data)
            eval_loss += F.nll_loss(output, target).item()
            pbar.set_postfix(**{'eval loss': eval_loss / (batch_idx + 1)})
            pbar.update(1)
    return eval_loss/(batch_idx + 1)


def model_fit(epochs):
    best_loss=1e7
    for epoch in range(epochs):
        train_loss=train(epoch,epochs)
        eval_loss=eval(epoch,epochs)
        print('\nEpoch: {}\tTrain Loss: {:.6f}\tEval Loss: {:.6f}'.format(epoch+1,train_loss,eval_loss))
        if eval_loss<best_loss:
            best_loss=eval_loss
            torch.save(network.state_dict(), 'model.pth')

def test():
    #如果已经训练好了权重,模型直接加载权重文件进行测试#
    model_test=Net()
    model_test.load_state_dict(torch.load('model.pth'))
    model_test.eval()
    model_test=model_test.to(device)
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data=data.to(device)
            target=target.to(device)
            output = model_test(data)
            test_loss += F.nll_loss(output, target, size_average=False).item()
            pred = output.data.max(1, keepdim=True)[1]  #模型的分类结果
            correct += pred.eq(target.data.view_as(pred)).sum() #模型的分类结果和真实类别对比,统计正确的个数
    test_loss /= len(test_loader.dataset)
    print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))


    #------------------------------------------------------------------------------------------------------------#
    #                展示一些测试样本,直接用cpu运行,如果继续用cuda后续还需要将结果转到cpu再转numpy数据类型,非常麻烦           #
    # ----------------------------------------------------------------------------------------------------------#
    model_test=model_test.to('cpu')
    examples = enumerate(test_loader)
    batch_idx, (example_data, example_targets) = next(examples)
    with torch.no_grad():
        output = model_test(example_data)
    for i in range(6):
        plt.subplot(2, 3, i + 1)
        plt.tight_layout()
        plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
        plt.title("Prediction: {}".format(output.data.max(1, keepdim=True)[1][i].item()))
        plt.xticks([])
        plt.yticks([])
    plt.show()


if __name__=="__main__":
    #model_fit(100) #训练100轮
    test() #测试最终保存的模型


  • 21
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值