小白入门:手写数字识别MNIST实践

写在前面的话:

本篇博客的内容是由学校老师教授后的个人总结(选修课),需要python基础,需要安装pytorch,PIL,cv2等包,这些当然都可以在csdn上找到教学。由于还未系统学习深度学习等课程,以下内容如有出错,恳请各位大佬批评指正。当然,这也代表你即使是个小白,也可以按部就班的和我一起尝试本次实践。

首先,我们要讲一下本次实践的内容:

1.获取数据集

2.读取数据集

3.搭建神经网络模型

4.定义损失函数

5.定义优化器来优化参数

6.模型训练

7.模型检测

但我并非由上到下顺序讲解,而是根据之前老师的讲解顺序来,也许老师考虑了我们还是小白。

第一课:学习搭建全连接神经网络

全连接神经网络(Fully - Connected Neural Network,FCN)是神经网络中较为基础的一种结构。在这种网络中,相邻两层的神经元之间全部相互连接,即前一层的每个神经元都与后一层的每个神经元有连接权重。

以上是官方定义,下面插图帮助理解

  

在本次实践中,输入层是从数字图片中所读取的信息,而每一层,神经元先对输入进行加权求和

 由输入到输出的过程就是前向传播,它的结果表示输入数据经过我们搭建的模型所得到的结果,在本次实践中,我们输入由0到9的图片的读取参数,最后得到它识别的结果。那么你现在必定有一个问题,就是最后的结果怎么能提高正确率。其实这主要是由w,b两个参数决定,而怎么得到这两个参数的更优值来使得结果准确率较高,之后的课程会讲,此处我们只要理解什么是全连接神经网络和前向传播即可。

我们首先要了解来两个函数nn.Linear()和nn.ReLU(),nn是torch中的模块,非常有用。

nn.Linear 是 PyTorch 中用于实现全连接层(也叫线性层)的模块,用于线性变换,此处需要一点点的矩阵乘法的知识。就是我们输入的是矩阵(或列向量),要乘以一个权重矩阵w(不是一个数)

 我们知道了nn.Linear是用于构建全连接神经网络,那nn.Relu是什么?答案是激活函数。下面是关于激活函数的作用:激活函数是神经网络中不可或缺的组成部分,它的核心作用是为网络引入非线性特性,从而使神经网络能够学习复杂的函数映射。

我的理解就是如果没有激活函数,那么即使再多层也只是线性函数,就是y=kx+b嵌套再多层也是一次函数(类比)

它本身很简单:把负数变成0,正数不变

那么我们先正式写代码:

import torch
from torch import nn#导入库

下面我们创建一个类,方便以后导入和创建实例。

#神经网络搭建部分
class MNIST_Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(784, 256)
        self.layer2 = nn.Linear(256, 256)
        self.layer3 = nn.Linear(256, 10)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = x.view(-1, 784)
        x = self.layer1(x)
        x = self.relu(x)
        x = self.layer2(x)
        x = self.relu(x)
        x = self.layer3(x)
        return x

首先我们要注意到我们创建的MNIST_Net继承了nn.Module,这里面有很多常用函数,这里有ai的解释:nn.Module 是所有神经网络模块的基类,也是构建自定义神经网络的核心工具。通过继承 nn.Module,可以方便地组织和管理神经网络的层、参数以及前向传播逻辑。此处我们主要是利用它的前向传播。

我们可以看到这个类有两部分,一个是必写的__init__函数,类似于构造函数,其中有三层和一个激活函数作为成员变量,在forward中会用到,你也可以多写几层自己试一试,但后面好像还是要改回来,有我们后面的代码有关。

x = x.view(-1, 784)解释一下这一句,这是把x变成784列。这是因为输入层要求784,必须对应,-1表示前面由总数/784决定行数。

代码尝试运行:

if __name__=='__main__':
    net=MNIST_Net()
    x=torch.ones(28,28)
    y=net(x)
    print(y)

 x=torch.ones(28,28)表示一个28*28的全为1的二维张量,理解为矩阵应该也行。x =torch.randn(1, 784),不过这是生成随机的数了。由于此时我们没有训练,此处的w和b都是随机的,结果也随机。

第二课:创建和读取数据集 

首先,我们来学习如何读取一张图片,可以用PIL中的Image.open(),或者cv2中的cv2.imread,现在你可以下载我的资源里的压缩包,然后解压。

注意:必须解压,就是一会要复制的路径里不能有压缩包

 示例:

im=cv2.imread(r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST\0\542.jpg',0)
print(im)

结果: 

此处你不用和我用一致的地址,找对应图片,复制上面的地址即可。

 ok,到这里你就知道了怎么读取一张图,但是这个压缩包有几万张图片,那么你不可能像上面一样一张一张地读取,下面我们来介绍怎么一次读取一个文件里的所有图片。

代码:

data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'
classes = os.listdir(data_root)

for c in classes:
    img_names = os.listdir(os.path.join(data_root, c))
    for img_name in img_names:
        img_num += 1
        test_img = Image.open(os.path.join(data_root, c, img_name))      
       # 用 PIL 打开图片

解释: data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'这是我们要访问的文件夹,此处r是因为\是转义字符,会报错,必须加r或者用/,又或者\\。

此时data_root数据类型是字符串,os.listdir(data_root),这个os是不用下载自带的包。这个方法作用是把文件夹中所有子文件夹名称存入一个列表并返回。

os.path.join(data_root, c)是把路径合并,此时可以进一步把其中的子文件名称全部读取,然后再合并最后一次。所以我们通过for循环遍历和这两个方法实现了我们的需求。

但是,老师接下来说我们这样写还是略显麻烦,最好能写成一个类方便以后调用。并且还有

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

这两个库可以使用,这两个库在后面有用

import torch
from torch import nn
from PIL import Image
import os
from MNIST_Net import MNIST_Net
import torchvision.transforms as transforms
from torch.utils.data import Dataset
from torch.utils.data import DataLoader

#创建一个类用于读取一个路径下的所有文件,并存储信息
class MNIST_Data(Dataset):
    def __init__(self, data_root):
        self.data_root = data_root
        self.dataset = []
        classes = os.listdir(self.data_root)
        for c in classes:
            img_names = os.listdir(os.path.join(data_root, c))
            for img_name in img_names:
                path = os.path.join(data_root, c, img_name)
                label = int(c)#类别标签,此前c是字符串
                self.dataset.append((path, label))#将数据对(路径,标签)不断加入列表
                #分开写是为减小所耗费的存储空间,当存储图片信息时,最好存储路径
    def __getitem__(self, idx):
        path, label = self.dataset[idx]#读取路径,标签
        img = Image.open(path)
        my_trans = transforms.ToTensor()
        img = my_trans(img)
        return img, label

    def __len__(self):
        return len(self.dataset)#返回文件数

此处我们只用了Dataset后面具体使用时,会有相应的解释。

第三课:测试

现在,我们已经学会读取数据集和搭建模型,我们先跳过剩下训练步骤,用老师给的训练好的文件,测试一下,相关文件也可以在我的资源找到。

代码:

import torch
from torch import nn
from PIL import Image
import os
from MNIST_Net import MNIST_Net
import torchvision.transforms as transforms

if __name__ == '__main__':
    net = MNIST_Net()
    net.load_state_dict(torch.load('fcn_mnist_ckpt.pt', map_location='cpu'))

    data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'
    classes = os.listdir(data_root)

    score_all = 0
    img_num = 0

    for c in classes:
        img_names = os.listdir(os.path.join(data_root, c))
        for img_name in img_names:
            img_num += 1
            test_img = Image.open(os.path.join(data_root, c, img_name))      
               # 用 PIL 打开图片
            my_trans = transforms.ToTensor()
            test_img = my_trans(test_img)

            score = net(test_img)
            softmax = nn.Softmax(dim=1)
            score = softmax(score)
            answer = torch.argmax(score, dim=1)
            score_all += float(answer[0].numpy() == int(c))

        test_score_all = score_all / img_num
        print(f'{c} Test score: {test_score_all:.2f}')

 这里我们主要解释这几句,毕竟前面都差不多讲了:

net = MNIST_Net()
net.load_state_dict(torch.load('fcn_mnist_ckpt.pt', map_location='cpu'))
    score = net(test_img)
    softmax = nn.Softmax(dim=1)
    score = softmax(score)
    answer = torch.argmax(score, dim=1)
    score_all += float(answer[0].numpy() == int(c))

test_score_all = score_all / img_num
print(f'{c} Test score: {test_score_all:.2f}')

net = MNIST_Net()是我们之前搭建的神经网络模型的实例化,net.load_state_dict(torch.load('fcn_mnist_ckpt.pt', map_location='cpu'))这里你肯定会疑惑为啥有这个方法load_state_dict(),不是只写了两个方法吗,别忘了我们继承了nn.Module。它的主要作用就是导入我们需要的参数,这个文件名为fcn_mnist_ckpt.pt,用cpu运行。这里的fcn_mnist_ckpt.pt要放在代码同一文件夹下。

如图:

score = net(test_img)这是模型运行结果,是一列数,共十个,数越大,越说明代表对应图片是它所在位置。其实由于我们已经训练好了模型, softmax = nn.Softmax(dim=1),这一句话理论上可以不用,这里还是解释一下,反正下面要用。

主要就是利用指数函数的非负性,保证每个数对应概率非负,且相加为1。dim=1表示列。

answer = torch.argmax(score, dim=1)这里表示找出每列最大值对应的索引,这就是我们识别的图片结果,score_all += float(answer[0].numpy() == int(c))表示识别正确加一分。

test_score_all = score_all / img_num表示识别准确率。

结果:

我们再使用之前写好的类MNIST_Data来让我们的代码更加简洁:

import torch
from torch import nn
from MNIST_Data import MNIST_Data
from PIL import Image
import os
from MNIST_Net import MNIST_Net
import torchvision.transforms as transforms
from torch.utils.data import Dataset
from torch.utils.data import DataLoader

#创建一个类用于读取一个路径下的所有文件,并存储信息
class MNIST_Data(Dataset):
    def __init__(self, data_root):
        self.data_root = data_root
        self.dataset = []
        classes = os.listdir(self.data_root)
        for c in classes:
            img_names = os.listdir(os.path.join(data_root, c))
            for img_name in img_names:
                path = os.path.join(data_root, c, img_name)
                label = int(c)#类别标签,此前c是字符串
                self.dataset.append((path, label))#将数据对(路径,标签)不断加入列表

    def __getitem__(self, idx):
        path, label = self.dataset[idx]#读取路径,标签
        img = Image.open(path)
        my_trans = transforms.ToTensor()
        img = my_trans(img)
        return img, label

    def __len__(self):
        return len(self.dataset)#返回文件数

if __name__ == '__main__':
    test_data_root =  r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'
    #构建测试集
    test_data = MNIST_Data(test_data_root)
    test_dataloader = DataLoader(dataset=test_data, batch_size=1, shuffle=False)

    net = MNIST_Net()
    net.load_state_dict(torch.load('fcn_mnist_ckpt.pt', map_location='cpu'))
    #从文件fcn_mnist_ckpt.pt加载模型权重,
    # 将权重映射到 CPU 设备(无论模型原本在哪个设备上训练),将加载的权重应
    #用到当前的net模型实例中

    test_score_all = 0
    for img, label in test_dataloader:
        score = net(img)
        softmax = nn.Softmax(dim=1)
        score = softmax(score)
        answer = torch.argmax(score, dim=1)
        score_batch = torch.eq(answer, label).float().sum()#一组之和
        test_score_all = test_score_all + score_batch
    test_score_all = test_score_all / len(test_data)#整个数据集的准确率
    print('Test score: %.2f'%test_score_all)

这里主要解释一下Dataset,DataLoader的作用

通过对比之前我们用两个for循环写的代码,这里最大的区别在于将数据集以数据对的形式进行了存储,方便管理,继承Dataset是因为方法与DataLoader有关,DataLoader是一个迭代器,用于批量加载和处理Dataset中的数据。这里

test_data = MNIST_Data(test_data_root)是将测试点数据集构建完成,
DataLoader(dataset=test_data, batch_size=1, shuffle=False)是表示对应数据集以批次为1,不打乱构造一个迭代器,然后我们就可以从这个迭代器里直接获取图像对应的张量和标签。再把张量像之前一样处理即可。

运行结果:

第四课:自己训练模型,优化参数

我们先来了解一下反向传播,又称后向传播,它对参数优化,减小误差很有帮助。顾名思义,就是前向传播的反过来。

反向传播(Backpropagation)是一种在人工神经网络中用于计算梯度的算法,主要用于训练神经网络,以下是关于它的详细介绍:

  • 反向传播算法基于链式法则,将误差从输出层反向传播到输入层,以计算每个神经元的权重梯度。具体来说,它首先计算输出层的误差,然后根据误差对输出层的权重和偏置进行调整。接着,将误差反向传播到隐藏层,计算隐藏层的误差,并对隐藏层的权重和偏置进行调整。这个过程不断重复,直到误差传播到输入层。

按照我的理解就是一个多元函数原本是以x1,x1。。。xn为变量,然后计算结果发现有误差,再以w1,w2。。。wn,b1,b2。。。bn为主元,进行微分,向使损失函数降低的方向前进。(梯度是方向函数max)

这里多讲一下:ai的看法

你的理解基本正确。


以损失函数(比如均方误差等)作为衡量误差大小的指标,损失函数是关于权重 w 和偏置 b 的函数。根据链式法则对损失函数关于权重 w 和偏置 b 求微分(也就是计算梯度),然后沿着梯度的反方向(使得损失函数降低的方向)来更新权重和偏置,以此来不断优化神经网络的性能,使得网络的输出尽可能地接近真实值。

这里我们提到了误差,又称损失,那么我们要怎么计算损失呢,需要一个损失函数。这里采用的是交叉熵损失函数。

代码实现:

loss_ce = nn.CrossEntropyLoss()

手动代码:

 def cross_entropy(y_hat, y):
         return -torch.log(y_hat[range(len(y_hat)), y])

有了损失函数,下一步是优化器用来优化参数

代码:

 # 定义优化器
    opt = torch.optim.SGD(net.parameters(), lr=0.01)  # learning rate 学习率

 学习率(Learning Rate) 是一个至关重要的超参数,它控制着模型参数(权重和偏置)在每次迭代中根据梯度更新的步长大小,这是一个比较难的地方,暂时不需要掌握,理解成更新速度有关即可,但不能太大太小。

运用优化器

opt.zero_grad()  # 清空梯度
loss.backward()  # 反向传播
opt.step()  # 进行一步更新

手动代码:

def sgd(params, lr, batch_size):
         with torch.no_grad():
             for param in params:
                 param -= lr * param.grad / batch_size
                 param.grad.zero_()

其实这里一样就是上面的三句注解,with torch.no_grad()表示梯度清零,因为pytorch默认梯度累加,要避免之前计算的影响就要梯度清零。下面for循环就是参数更新:

  • aram.grad: 当前批次计算得到的梯度。
  • / batch_size: 归一化处理,因为 PyTorch 中损失函数默认对一个批次内的样本取平均,而梯度通常是累积的,所以需要除以批量大小。
  • lr * ...: 缩放梯度,控制更新步长。
  • param -= ...: 原地更新参数,向梯度反方向移动。

所以通过以上代码我们就可以完成训练

'''准备大量数据(训练测试数据),构建模型,训练模型'''
import os
import torch
import torch.nn as nn
from MNIST_Net import MNIST_Net
from matplotlib import pyplot as plt
from MNIST_Data import MNIST_Data
from torch.utils.data import Dataset, DataLoader

os.environ["KMP_DUPLICATE_LIB_OK"]= 'TRUE'

if __name__ == '__main__':
    # 训练集目录位置
    train_data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TRAIN'
    # 构建训练集dataloader
    train_data = MNIST_Data(train_data_root)
    train_dataloader = DataLoader(dataset=train_data, batch_size=8, shuffle=True)

    # 测试集目录位置
    test_data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'
    # 构建测试集dataloader
    test_data = MNIST_Data(test_data_root)
    test_dataloader = DataLoader(dataset=test_data, batch_size=1, shuffle=False)

    # 从MNIST_Net类中实例化一个网络, 命名为 net
    net = MNIST_Net()

    # 定义损失函数
    loss_ce = nn.CrossEntropyLoss()
    # 定义优化器
    opt = torch.optim.SGD(net.parameters(), lr=0.01)  # learning rate 学习率

    epoch = 3  # 整个训练数据集 遍历的 轮数

    # 初始化两个列表,报出每轮的loss和score,以便训练结束时用plot显示
    train_loss_epoch = []
    test_score_epoch = []
    # 开始训练
    for e in range(epoch):
        for img, label in train_dataloader:  # 从dataloader中遍历图片和标签
            score = net(img)  # 预测结果
            loss = loss_ce(score, label)  # 预测值和标签计算损失
            opt.zero_grad()  # 清空梯度
            loss.backward()  # 反向传播
            opt.step()  # 进行一步更新

           

那么我们需要检测我们的训练结果:

    for e in range(epoch):
        loss_all = 0
        for img, label in train_dataloader:  # 从dataloader中遍历图片和标签
            score = net(img)  # 预测结果
            loss = loss_ce(score, label)  # 预测值和标签计算损失
            opt.zero_grad()  # 清空梯度
            loss.backward()  # 反向传播
            opt.step()  # 进行一步更新

            loss_all += loss
        train_loss_epoch.append(loss_all.detach().numpy() / len(train_dataloader))  # 保存本轮loss
        if not os.path.exists('train_checkpoint'):
            os.mkdir('train_checkpoint')
        torch.save(net.state_dict(), 'train_checkpoint/mnist_ckpt_%d.pth' % e)  # 保存本轮权重checkpoint

        test_score_all = 0
        for img, label in test_dataloader:
            score = net(img)  # 输入测试图像,此时net会自动调用执行forward函数的过程,返回网络的输出结果
            softmax = nn.Softmax(dim=1)
            score = softmax(score)  # 利用 softmax 将 网络输出结果转化为是分类的概率向量
            answer = torch.argmax(score, dim=1)  # 利用 argmax,找到score中最大值的位置
            score_batch = torch.eq(answer, label).float().sum()  # 计算结果是否和类别标签一致
            test_score_all = test_score_all + score_batch
        test_score_all = test_score_all / len(test_data)  # 计算平均值
        test_score_epoch.append(test_score_all)  # 保存本轮score

        print('Epoch %d, Train loss: %.2f, Test score: %.2f' \
              % (e, loss_all / len(train_dataloader), test_score_all))

if not os.path.exists('train_checkpoint'):
            os.mkdir('train_checkpoint')
        torch.save(net.state_dict(), 'train_checkpoint/mnist_ckpt_%d.pth' % e)  # 保存本轮权重checkpoint

这几句解释一下,就是创建了一个文件夹以二进制的形式用来保存你的 每轮循环完后的参数,这样你就有了我之前给你的参数了。

注意:

opt = torch.optim.SGD(net.parameters(), lr=0.01)

这句代码建立了net中参数和优化器的联系,所以我们接下来每一轮都是用更新过的参数来检验

运行结果:

可以看到损失逐渐降低,准确率逐步提高。

最后的代码实用来画图可视化的,可以运行看看。

感觉自己初学者水平还是不够,有很多地方不太正确还望见谅。

最后,说一下,就是你可以试着要ai帮你写一段代码来用于你进行手写数字图片,在用程序识别,可以再不懂的人面前装一装(狗头) 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值