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

ops/2024/11/28 15:36:20/

一、任务描述

        使用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/ops/137392.html

相关文章

模拟算法实例讲解:从理论到实践的编程之旅

目录 1、模拟算法简介 2、替换所有问号 3、提莫攻击 4、Z字形变换 5、外观数列 6、数青蛙 1、模拟算法简介 模拟算法是一种基本的算法设计方法&#xff0c;它的核心思想是按照问题描述的规则&#xff0c;逐步模拟问题的发展过程&#xff0c;从而得到问题的解决方案。这种…

【halcon】Metrology工具系列之 get_metrology_object_model_contour

get_metrology_object_model_contour (Operator) Name get_metrology_object_model_contour — 在图像坐标中查询测量对象的模型轮廓。 Signature get_metrology_object_model_contour( : Contour : MetrologyHandle, Index, Resolution : )Description get_metrology_obj…

【Flink-scala】DataStream编程模型之 窗口的划分-时间概念-窗口计算程序

DataStream编程模型之 窗口的划分-时间概念-窗口计算程序 1. 窗口的划分 1.1 窗口分为&#xff1a;基于时间的窗口 和 基于数量的窗口 基于时间的窗口&#xff1a;基于起始时间戳 和终止时间戳来决定窗口的大小 基于数量的窗口&#xff1a;根据固定的数量定义窗口 的大小 这…

RK3568平台开发系列讲解(DMA篇)DMA engine使用

🚀返回专栏总目录 文章目录 一、申请DMA channel二、配置DMA channel的参数三、获取传输描述(tx descriptor)四、启动传输沉淀、分享、成长,让自己和他人都能有所收获!😄 📢DMA子系统下有一个帮助测试的测试驱动(drivers/dma/dmatest.c), 从这个测试驱动入手我们了解…

简单图论农场派对

题目 2406: 信息学奥赛一本通T1497-农场派对 时间限制: 2s 内存限制: 192MB 提交: 40 解决: 13 题目描述 原题来自&#xff1a;USACO 2007 Feb. Silver N(1≤N≤1000) 头牛要去参加一场在编号为 x(1≤x≤N) 的牛的农场举行的派对。有 M(1≤M≤100000) 条有向道路&#xff0c;每…

C++笔记之单例模式与静态方法的使用辨析及代码规范

C++笔记之单例模式与静态方法的使用辨析及代码规范 code review! 文章目录 C++笔记之单例模式与静态方法的使用辨析及代码规范一.示例代码二.讲解2.1.代码规范2.1.1.单例模式实现2.1.2.静态方法实现2.1.3.单例模式结合静态方法2.2.总结一.示例代码 // 使用 set 方法设置值(通…

IT成长之路-ubuntu驱动篇

历时3天的蹂躏&#xff0c;总结驱动安装全面教程。 步骤一、安装gcc、g和make包 #脚本更新 sudo apt-get update #编译gcc sudo apt-get install gcc #编译g sudo apt-get install g #编译make sudo apt-get install make 注意&#xff1a; gcc、g版本可能会导致显卡驱动安…

离散化/C++ STL编码+set去重

先只做离散 #include<iostream> #include<algorithm> #include<vector> #include<set> using namespace std; int main() {int N 0;cin >> N;vector<int> old;set<int> ne;int temp;for (int i 0; i < N; i) {cin >> te…