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

server/2024/11/29 7:24:51/

一、任务描述

        使用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, transformsdef main():# 图像预处理transform = transforms.Compose([transforms.ToTensor(),  # 将原图像的格式【高X宽X通道】修改为【通道X高X宽】。像素点范围修改为[0, 1]transforms.Normalize((0.5,), (0.5,))  # 归一化,像素点范围修改为[-1, 1]])  # 对图像格式进行转换# 下载并加载MNIST训练和测试数据集,如果你为第一次使用,则需要将download的值改为Truetrain_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.0for 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, transformsdef 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.0for 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.0print('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 transformsdef 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 = Noneself.photo = Noneself.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.yself.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)  # 调整图片大小NEARESTself.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.ppmy.cn/server/145840.html

相关文章

C语言解决空瓶换水问题:高效算法与实现

标题&#xff1a;C语言解决空瓶换水问题&#xff1a;高效算法与实现 一、问题描述 在一个饮料促销活动中&#xff0c;你可以通过空瓶换水的方式免费获得更多的水&#xff1a;3个空瓶可以换1瓶水。喝完这瓶水后&#xff0c;空瓶会再次变为空瓶。假设你最初拥有一定数量的空瓶&a…

泷羽sec-云技术

基础之云技术 声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec…

【高等数学学习记录】微分中值定理

一、知识点 &#xff08;一&#xff09;罗尔定理 费马引理 设函数 f ( x ) f(x) f(x) 在点 x 0 x_0 x0​ 的某邻域 U ( x 0 ) U(x_0) U(x0​) 内有定义&#xff0c;并且在 x 0 x_0 x0​ 处可导&#xff0c;如果对任意的 x ∈ U ( x 0 ) x\in U(x_0) x∈U(x0​) &#xff0…

项目介绍和游戏搭建(拼图小游戏)

1. &#xff08;1&#xff09; import javax.swing.*;public class GameJFrame extends JFrame {//游戏主界面&#xff0c;游戏的所有逻辑public GameJFrame(){this.setSize(603,680);this.setVisible(true);//true是展示&#xff0c;flase是隐藏} } &#xff08;2&#xff…

【优选算法】位运算

目录 常见位运算总结1、基础位运算2、给一个数n&#xff0c;确定它的二进制位的第x位上是0还是13、将一个数n的二进制位的第x位改成14、将一个数n的二进制位的第x位改成05、位图的思想6、提取一个数n的二进制位中最右侧的17、将一个数n的二进制位中最右侧的1变为08、位运算的优…

linux一键部署apache脚本

分享一下自己制作的一键部署apache脚本&#xff1a; 脚本已和当前文章绑定&#xff0c;请移步下载&#xff08;免费&#xff01;免费&#xff01;免费&#xff01;&#xff09; &#xff08;单纯的分享&#xff01;&#xff09; 步骤&#xff1a; 将文件/内容上传到终端中 …

kubernetes volcano 客户端

kubernetes 客户端 package mainimport ("context""fmt""os"appsv1 "k8s.io/api/apps/v1"corev1 "k8s.io/api/core/v1"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/util/intst…

当前就业形势下C++方向后端开发学习指南

文章目录 1. C后端开发的职业方向1.1 C的应用领域1.2 后端开发的职业选择 2. 当前就业形势分析2.1 C开发者的市场需求2.2 C开发者的薪资水平 3. 学习路线3.1 入门阶段&#xff1a;掌握基础知识3.2 进阶阶段&#xff1a;掌握后端开发的核心技术3.2.1 数据库与C3.2.2 网络编程 3.…