当前位置: 首页 > article >正文

作业3-基于pytorch的非线性模型设计

一、任务描述

        使用BP神经网络和CNN实现对MNITS数据集的识别,并通过修改相关参数,比较各模型的识别准确率。

二、相关配置

        pytorch:2.5.1

        python:3.12

        pycharm:2024.1.2(这个影响不大,版本不要太低就行)

三、数据集介绍

        本次实验使用的数据集为MNIST,该数据集为手写数字0-9的灰度图像。包含60000 张训练集图片和10000 张测试集图片。下图为数据集中的图片示例,该图片像素大小为28*28,单通道灰度图像(像素值范围为 0 到 255)。

四、模型设计

4.1 BP神经网络

        由于输入图像为1*28*28(通道*高*宽)。我们需要将其转换为一维向量,因此该网络具有784(1*28*28=784)个输入特征。我们对这些输入特征,构建了两个隐藏层,每个隐藏层的神经元个数可以由自己设计。当输入特征经过两层隐藏层后,在最终的输出时,我们需要将输出神经元个数设置为10个,这是因为我们的目的是解决一个10分类的问题。

例如:

        假设网络末端10个神经元的输出为:

[-2.3, 0.8, 2.1, -0.5, 1.2, -1.5, -0.9, 0.4, 1.0, -0.2]

        经过Softmax函数后:

[0.02, 0.09, 0.33, 0.04, 0.12, 0.03, 0.05, 0.08, 0.11, 0.07] 

         使用Max函数后,最大概率为0.33,对应索引2,所以最终的预测结果为数字2。

        以下是BP神经网络的代码搭建过程。 

        在搭建网络时,我们使用relu()函数作为神经元的激活函数。该函数只需要判断输入是否大于 0,无需复杂的指数计算,比较适合大规模的神经网络计算。同时在正值区域,梯度(斜率)始终为 1,不会随着深度增加而消失。

relu函数定义:

f(x)=max(0,x)

relu函数图像:

       我们在导入MNIST数据时,若为第一次导入,则需要将download=False改为download=True进行数据集的下载。

        在图像的预处理阶段,我们需要使用transforms.ToTensor()对原始图像的格式进行修改。同时使用transforms.Normalize()函数进行归一化。

        我们调用nn.CrossEntropyLoss()来定义损失函数。

        在这个函数中包含两个步骤:

        1、将模型的输出变换为概率分布(0~1之间,且总和为1)。概率分布公式如下:

p_{i}=\frac{ e^{z_{i}} } { \sum_{j=1}^{C} e^{z_{j}} }

        其中z_{i}是第i类的模型输出,C是类别总数。

        2、对概率的负对数取值并与目标标签计算交叉熵损失。公式如下:

Loss=- \sum_{j=1}^{C} y_{i} log ( p^{i})

        其中y_{i}是目标类别的独热(Ont-Hot)编码。

        同时我们使用Adam优化器来实现参数的更新。传入的参数为网络需要优化的参数以及学习率。这里的学习率lr(Learning Rate)需要设置的小一点。

         在代码主循环中,我们主要按照“前向传播->计算损失值->反向传播->更新参数”这样的流程反复进行。

        在完成N轮训练后,记得将模型的参数保存下来,以便后续使用。

4.2 LeNet神经网络

        对于4.1节提到的BP神经网络,我们还可以在前面加上卷积。笔者的个人理解为,通过卷积后,能提取出原始图像的特征信息,并且能大大减少BP网络的输入特征个数。(对于原始的BP网络来说,一张单通道图像有多少像素点,就要有多少个输入特征)

        对于MNIST数据集,我们使用的是LeNet网络,该是一种经典的卷积神经网络(CNN)架构(网络结构也比较简单,emmmm)。该网络由两个卷积层、两个池化层、三个权连接层组成,具体的网络框架如下图所示。

        在实际代码中,我们只需要在之前BP网络的基础上,在前面增加卷积层和池化层就行了。然而,在卷积部分的设计中我们需要选择合适的参数,这一部分需要自己计算一下。


【示例】

        以下图的代码为例,我们来介绍一下如何设置参数。

        由于我们的输入图像为单通道的灰度图像,因此在conv1()中,输入通道in_channels为1(像彩色RGB图像的通道数就为3)。此外,我们使用了16个卷积核(out_channels的值与卷积核的数量相等)对图像进行卷积处理,卷积核kernel_size的大小为5X5,最终会输出16层的特征矩阵。

        对于输出矩阵大小的计算,我们有如下公式(简化版):

N=(W-F+2P)/S+1

        其中,W为输入图片(矩阵)的大小,F(滤波器Filter)为卷积核的大小,S(Stride)为步长,P(Padding)补零的像素数(对称补零,所以为2P)。

        依据以上公式,我们就可以计算出输出矩阵的大小为NXN:

N=(28-5+0)/1+1=24

        在完成conv1()之后,输出的矩阵形式为(16,24,24)。其中16为层数,24为矩阵的大小。

        接着我们对特征矩阵进行第一次池化pool1()(最大下采样),池化核kernel_size的大小为2X2、步距stride为2。因此输出的特征矩阵为(16,12,12)的格式。相当于将原来的特征矩阵长宽缩小一半,但注意,矩阵的层数仍然不变。

        然后我们继续一次卷积conv2()。由于前面conv1()的处理,所以导致在conv2()中输入通道in_channels变为了16。在第二次卷积中,我们使用32个卷积核,因此最终的输出矩阵有32层,输出格式为(32,8,8)

        随后就是进行第二次池化pool2()。池化核kernel_size、步距stridepool1()一样。因此通过所有的卷积和池化后,最终输出的图像矩阵为(32,4,4)

处理方式

处理后输出的图像矩阵格式

(通道数,高,宽)

原始图像(未处理)(1,28,28)

卷积

卷积核数量16,卷积核大小5X5

步距和补零默认都为0

(16,24,24)

池化

池化核大小为2X2,步距为2

(16,12,12)

卷积

卷积核数量32,卷积核大小5X5

步距和补零默认都为0

(32,8,8)

池化

池化核大小为2X2,步距为2

(32,4,4)

        所以最后输入到BP神经网络中的初始特征值个数为32X4X4=512。


五、测试结果

         上图为BP神经网络训练时损失值的变化趋势。其中左图的两个权连接神经元个数分别为16和12,右图为120和84。通过对比我们可以发现,适当增加神经元个数,可以有效加快模型的收敛速度。(这里也可以修改学习率lr,观察收敛速度的变化)

        上表为BP网络、LeNet网络的实验数据。通过数据我们可以发现,LeNet在BP网络的基础上增加卷积和池化层后,对于验证集的准确率有了一定的提升,同时损失值也较小。 这说明增加卷积能有效提取出图像中的特征信息,对提高识别准确率有一定帮助。另外适当增加训练次数,也对准确率有所提升。

六、手写数字的UI搭建

        在这一章,我们将进行UI界面的搭建,利用前面训练好的模型,对我们自己手写的数字进行实时的识别。我们使用Tkinter库进行基础界面的一个搭建。代码(完整代码附在文末了)的大致流程就是,初始化标签、按键、画布等控件。通过点击“识别”按钮开始图像识别,并在控制台和界面上打印出预测结果。最终的界面效果如右图所示。 

        在这一部分工作中,最重要的部分就是图像的预处理了,我们需要确保预测的图像格式与训练的图像格式一致。在下图的预处理代码中。我们首先将获得的图片的大小调整为28X28。然后将原始的白底黑线改为黑底白线,也就是灰度值取反。这一步的目的是为了保证预测图像与训练图像的形式尽可能一样。

        此外,为了使的手写的图像与测试集更加相似,我们在预处理阶段还加入了膨胀操作,在参数设置上,我们选择卷积核kernel为2X2,膨胀次数iterations为1,将数字图像进行了一定的加粗,使得手写的数字的粗细尽可能与测试的数字相同。

七、完整代码

7.1  BP网络模型

# 导入一些包
import torch.nn as nn
import torch.nn.functional as fun


# 这个类中实现两个方法
class BPNet(nn.Module):
    def __init__(self):
        super(BPNet, self).__init__()

        # 三个权连接层(一维向量,所以需要展开)
        # 依据上一层,进行展开(1 * 28 * 28)
        self.fc1 = nn.Linear(1 * 28 * 28, 120)  # (1 * 28 * 28)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)  # 最后的“10”,就是输出有几个类别

    def forward(self, x):
        x = x.view(-1, 1 * 28 * 28)  # 输出(1 * 28 * 28)
        x = fun.relu(self.fc1(x))    # 输出(120)
        x = fun.relu(self.fc2(x))    # 输出(84)
        x = self.fc3(x)              # 输出(10)
        return x

7.2  BP网络训练代码

# 导入一些包
import torch
import torch.nn as nn
from bp_model import BPNet
import torch.optim as optim
from torchvision import datasets, transforms


def main():
    # 图像预处理
    transform = transforms.Compose([
        transforms.ToTensor(),  # 将原图像的格式【高X宽X通道】修改为【通道X高X宽】。像素点范围修改为[0, 1]
        transforms.Normalize((0.5,), (0.5,))  # 归一化,像素点范围修改为[-1, 1]
    ])  # 对图像格式进行转换

    # 下载并加载MNIST训练和测试数据集,如果你为第一次使用,则需要将download的值改为True
    train_dataset = datasets.MNIST(root='./data_set', train=True, download=False, transform=transform)  # 获取训练集
    test_dataset = datasets.MNIST(root='./data_set', train=False, download=False, transform=transform)  # 获取数据集

    # 将数据集加载到DataLoader中
    train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)  # 训练集一批为64张
    test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=10000, shuffle=False)

    test_data_iter = iter(test_loader)  # 转化成可迭代的迭代器
    test_image, test_label = next(test_data_iter)  # 获得一批数据

    net = BPNet()
    loss_function = nn.CrossEntropyLoss()  # 定义一个交叉熵损失函数
    optimizer = optim.Adam(net.parameters(), lr=0.001)  # 使用 Adam 优化器来更新模型参数,lr为学习率

    for epoch in range(5):  # 模型一共训练10次
        running_loss = 0.0
        for step, data in enumerate(train_loader, start=0):  # 每个批次进行遍历,同时跟踪当前的步数,step为当前的批次
            inputs, labels = data  # 获取一个批次的数据

            optimizer.zero_grad()  # 梯度值清零,直接更新,防止累加

            outputs = net(inputs)  # 前向传播
            loss = loss_function(outputs, labels)
            loss.backward()  # 依据损失值,进行反向传播
            optimizer.step()  # 更新参数

            running_loss += loss.item()  # 当前批次的损失值进行累加

            if step % 500 == 499:  # 每500次打印一次数据
                with torch.no_grad():  # 禁用梯度计算
                    outputs = net(test_image)
                    predict_y = torch.max(outputs, dim=1)[1]  # 获取最大预测率,并返回其索引(对应的标签)
                    accuracy = torch.eq(predict_y, test_label).sum().item() / test_label.size(0)
                    # torch.eq() 判断标签是否相等
                    # .sum() 计算True的个数。例如[True, False, True, True],则返回值为3
                    # test_label.size(0) 获取测试集的总样本数量
                    print('[%d, %5d] train_loss: %.8f  test_accuracy: %.3f' %
                          (epoch + 1, step + 1, running_loss/500, accuracy))
                    running_loss = 0.0  # 清零

    print('Finished Training')

    save_path = './BP_MNSIT.pth'
    torch.save(net.state_dict(), save_path)  # 将模型的参数进行保存


if __name__ == '__main__':
    main()

7.3  LeNet网络模型

# 导入一些包
import torch.nn as nn
import torch.nn.functional as fun


# 这个类中实现两个方法
class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()

        self.conv1 = nn.Conv2d(1, 16, 5)   # 定义第1个卷积层
        self.pool1 = nn.MaxPool2d(2, 2)  # 池化,缩小一半

        self.conv2 = nn.Conv2d(16, 32, 5)  # 定义第2个卷积层
        self.pool2 = nn.MaxPool2d(2, 2)  # 池化,再缩小一半

        # 三个权连接层(一维向量,所以需要展开)
        # 依据上一层,进行展开(32 * 4 * 4)
        self.fc1 = nn.Linear(32 * 4 * 4, 120)   # (32 * 4 * 4, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)  # 最后的“10”,就是输出有几个类别

    # 这里使用relu作为激活函数
    def forward(self, x):
        x = fun.relu(self.conv1(x))   # 输入(1, 28, 28) 输出(16, 24, 24)
        x = self.pool1(x)             # 输出(16, 12, 12)
        x = fun.relu(self.conv2(x))   # 输出(32, 8, 8)
        x = self.pool2(x)             # 输出(32, 4, 4)

        x = x.view(-1, 32 * 4 * 4)    # 输出(32 * 4 * 4)
        x = fun.relu(self.fc1(x))     # 输出(120)
        x = fun.relu(self.fc2(x))     # 输出(84)
        x = self.fc3(x)               # 输出(10)
        return x

7.4  LeNet网络训练代码

# 导入一些包
import torch
import torch.nn as nn
from model import LeNet
import torch.optim as optim
from torchvision import datasets, transforms


def main():
    # 图像预处理
    transform = transforms.Compose([
        transforms.ToTensor(),  # 将原图像的格式【高X宽X通道】修改为【通道X高X宽】。像素点范围修改为[0, 1]
        transforms.Normalize((0.5,), (0.5,))  # 归一化,像素点范围修改为[-1, 1]
    ])  # 对图像格式进行转换

    # 下载并加载MNIST训练和测试数据集
    train_dataset = datasets.MNIST(root='./data_set', train=True, download=False, transform=transform)  # 获取训练集
    test_dataset = datasets.MNIST(root='./data_set', train=False, download=False, transform=transform)  # 获取数据集

    # 将数据集加载到DataLoader中
    train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)  # 训练集一批为64张
    test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=10000, shuffle=False)

    test_data_iter = iter(test_loader)  # 转化成可迭代的迭代器
    test_image, test_label = next(test_data_iter)  # 获得一批数据

    net = LeNet()
    loss_function = nn.CrossEntropyLoss()  # 定义一个交叉熵损失函数
    optimizer = optim.Adam(net.parameters(), lr=0.001)  # 使用 Adam 优化器来更新模型参数,lr为学习率

    for epoch in range(5):  # 模型一共训练10次
        running_loss = 0.0
        for step, data in enumerate(train_loader, start=0):  # 每个批次进行遍历,同时跟踪当前的步数,step为当前的批次
            inputs, labels = data  # 获取一个批次的数据
            optimizer.zero_grad()  # 梯度值清零,直接更新,防止累加

            outputs = net(inputs)  # 前向传播
            loss = loss_function(outputs, labels)
            loss.backward()  # 依据损失值,进行反向传播
            optimizer.step()  # 更新参数
            running_loss += loss.item()  # 当前批次的损失值进行累加

            if step % 500 == 499:  # 每500次打印一次数据
                with torch.no_grad():  # 禁用梯度计算
                    outputs = net(test_image)
                    predict_y = torch.max(outputs, dim=1)[1]  # 获取最大预测率,并返回其索引(对应的标签)
                    accuracy = torch.eq(predict_y, test_label).sum().item() / test_label.size(0)
                    # torch.eq() 判断标签是否相等
                    # .sum() 计算True的个数。例如[True, False, True, True],则返回值为3
                    # test_label.size(0) 获取测试集的总样本数量
                    print('[%d, %5d] train_loss: %.8f  test_accuracy: %.3f' %
                          (epoch + 1, step + 1, running_loss/500, accuracy))
                    running_loss = 0.0

    print('Finished Training')

    save_path = './Lenet_mnist_test.pth'
    torch.save(net.state_dict(), save_path)  # 将模型的参数进行保存


if __name__ == '__main__':
    main()

 7.5  手写数字界面搭建代码

# 导入一些包
import cv2
import torch
import numpy as np
import tkinter as tk
from tkinter import *
from model import LeNet
from PIL import Image, ImageDraw, ImageTk
import torchvision.transforms as transforms


def my_predict(img):
    transform = transforms.Compose([
        transforms.ToTensor(),  # 将原图像的格式【高X宽X通道】修改为【通道X高X宽】。像素点范围修改为[0, 1]
        transforms.Normalize((0.5,), (0.5,))  # 归一化,像素点范围修改为[-1, 1]
    ])

    classes = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')  # 分类的几个类别

    net = LeNet()
    net.load_state_dict(torch.load('Lenet_mnist_test.pth', weights_only=True))  # 添加权重文件

    img = transform(img)  # 调整格式并进行归一化
    img = torch.unsqueeze(img, dim=0)  # [N, C, H, W]

    with torch.no_grad():  # 禁用梯度计算
        outputs = net(img)  # 对输入的图像进行预测
        predict = torch.max(outputs, dim=1)[1].numpy()  # 寻找预测概率最大的

    print("预测您手写的数字为:"+classes[int(predict.item())])  # 打印预测结果
    return predict


# 创建 Tkinter 界面
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.label2 = None
        self.photo = None
        self.title("手写数字识别")   # 定义窗口标题
        self.geometry("560x400")  # 设置窗口大小

        # 创建画布
        self.canvas = tk.Canvas(self, width=200, height=200, bg="black")
        self.canvas.place(x=50, y=50)
        self.canvas.bind("<B1-Motion>", self.paint)  # 绑定鼠标事件的代码,用于捕捉用户在画布上拖动鼠标时的行为

        # 标签显示识别结果
        self.label = tk.Label(self, text="鼠标画图并点击“识别”按钮", font=("黑体", 15))
        self.label.place(x=150, y=15)

        # 添加“清屏”按钮
        self.clear_button = tk.Button(self, text="清屏", width=10, height=2,
                                      font=("黑体", 12), command=self.clear_canvas)
        self.clear_button.place(x=170, y=280)
        # 添加“识别”按钮
        self.predict_button = tk.Button(self, text="识别", width=10, height=2,
                                        font=("黑体", 12), command=self.predict_digit)
        self.predict_button.place(x=290, y=280)

        # 初始化画布
        self.image = Image.new("L", (200, 200), 255)
        self.draw = ImageDraw.Draw(self.image)

    def paint(self, event):
        # 在画布上绘制数字
        x, y = event.x, event.y
        self.canvas.create_oval(x, y, x+8, y+8, fill="white", width=20,
                                outline="white")
        self.draw.ellipse([x, y, x+8, y+8], fill=0)  # 记录到 PIL 图像中

    def clear_canvas(self):
        # 清空画布
        self.canvas.delete("all")
        self.image = Image.new("L", (200, 200), 255)
        self.draw = ImageDraw.Draw(self.image)

    def predict_digit(self):

        img = self.image.resize((28, 28), Image.BILINEAR)  # 将画布图像处理为模型输入格式并预测

        img = np.array(img)  # 将图像转换为numpy数组
        img = 255 - img  # 图像灰度值取反(白图变黑图)
        img = Image.fromarray(img)  # 将反转后的numpy数组转换回图像
        img.save("before_dilate.jpg")  # 保存为.jpg格式

        img = np.array(img)
        kernel = np.ones((2, 2), np.uint8)
        img = cv2.dilate(img, kernel, iterations=1)  # 执行膨胀操作
        img = Image.fromarray(img)
        img.save("after_dilate.jpg")  # 保存为.jpg格式

        # 创建标签并显示图片
        image = img.resize((200, 200), Image.NEAREST)  # 调整图片大小NEAREST
        self.photo = ImageTk.PhotoImage(image)
        self.label2 = tk.Label(self, image=self.photo)
        self.label2.place(x=300, y=50)
        self.label = tk.Label(self, text="鼠标画图并点击“识别”按钮", font=("黑体", 15))
        self.label.place(x=150, y=15)

        # 标签显示识别结果
        predicted = my_predict(img)  # 进行图片预测
        self.label.config(text=f"识别结果:{predicted.item()}", font=("黑体", 15))
        self.label.place(x=220, y=350)


# 运行应用
app = App()
app.mainloop()

八、参考资料

B站教程icon-default.png?t=O83Ahttps://www.bilibili.com/video/BV187411T7Ye?spm_id_from=333.788.videopod.sections&vd_source=e8f452a07f36bcbcdce084be68194906        在此感谢这位UP主的视频讲解,同时这个视频合集里还包含了很多网络代码介绍,例如VGG、GoogLeNet、ResNet等,大家也可以去康康。

UP主的Github仓库链接icon-default.png?t=O83Ahttps://github.com/WZMIAOMIAO/deep-learning-for-image-processing

九、感悟

        MNIST这个数据集还是较为简单,BP网络和LeNet网络分类准确率还是比较高的。但是当处理一些图像内容较为复杂、彩色图像RGB的数据集时,用这些传统的网络就比较吃力了。在笔者先前的测试下,准确率只有20%~40%。后续可以尝试使用一些其他更为复杂的网络进行测试。

        此外,对于pytorch提供的相关函数,笔者也只是简单了解了一下他的使用方法,并未对函数内部本身进行研究,如果后续想要改进模型的话,还是需要我们对底层代码进行深入挖掘的。

2024-11-18-20:12,收货颇丰


http://www.kler.cn/a/413488.html

相关文章:

  • 【计算机网络】多路转接之epoll
  • 使用phpStudy小皮面板模拟后端服务器,搭建H5网站运行生产环境
  • 【2022-数学二】历年真题-2022年-简答题(17-20)
  • 网络安全拟态防御技术
  • 在 PyTorch 训练中使用 `tqdm` 显示进度条
  • css—轮播图实现
  • 理解B+树
  • 芯科科技率先支持Matter 1.4,推动智能家居迈向新高度
  • Android 常用命令和工具解析之Trace相关
  • SpringBoot技术在欢迪迈手机商城中的应用
  • 【CLIP】2: semantic-text2image-search前后端调试
  • 实时数仓Kappa架构:从入门到实战
  • [so]实现Linux 程序使用指定的 .so 库,而不是系统的库
  • 网路协议解说
  • 主键、外键和索引之间的区别?
  • 如何处理python爬虫ip被封
  • SQL for XML
  • 微信小程序录音、停止录音、上传录音、播放录音
  • 【深入理解RabbitMQ】七大工作模式
  • 解锁 Vue 项目中 TSX 配置与应用简单攻略
  • YOLOv8 代码训练与中文字体配置教程(Linux、Windows通用)
  • MyBatis事务管理-附案例代码
  • Redis(概念、IO模型、多路选择算法、安装和启停)
  • 2024年wordpress、d-link等相关的多个cve漏洞poc
  • 【MySQL】表的操作(增删查改)
  • Oracle 中的表 ID(OBJECT_ID)段 ID(DATA_OBJECT_ID)