作业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函数定义:
relu函数图像:
我们在导入MNIST数据时,若为第一次导入,则需要将download=False改为download=True进行数据集的下载。
在图像的预处理阶段,我们需要使用transforms.ToTensor()对原始图像的格式进行修改。同时使用transforms.Normalize()函数进行归一化。
我们调用nn.CrossEntropyLoss()来定义损失函数。
在这个函数中包含两个步骤:
1、将模型的输出变换为概率分布(0~1之间,且总和为1)。概率分布公式如下:
其中是第i类的模型输出,C是类别总数。
2、对概率的负对数取值并与目标标签计算交叉熵损失。公式如下:
其中是目标类别的独热(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层的特征矩阵。
对于输出矩阵大小的计算,我们有如下公式(简化版):
其中,W为输入图片(矩阵)的大小,F(滤波器Filter)为卷积核的大小,S(Stride)为步长,P(Padding)补零的像素数(对称补零,所以为2P)。
依据以上公式,我们就可以计算出输出矩阵的大小为NXN:
在完成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、步距stride与pool1()一样。因此通过所有的卷积和池化后,最终输出的图像矩阵为(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站教程https://www.bilibili.com/video/BV187411T7Ye?spm_id_from=333.788.videopod.sections&vd_source=e8f452a07f36bcbcdce084be68194906 在此感谢这位UP主的视频讲解,同时这个视频合集里还包含了很多网络代码介绍,例如VGG、GoogLeNet、ResNet等,大家也可以去康康。
UP主的Github仓库链接https://github.com/WZMIAOMIAO/deep-learning-for-image-processing
九、感悟
MNIST这个数据集还是较为简单,BP网络和LeNet网络分类准确率还是比较高的。但是当处理一些图像内容较为复杂、彩色图像RGB的数据集时,用这些传统的网络就比较吃力了。在笔者先前的测试下,准确率只有20%~40%。后续可以尝试使用一些其他更为复杂的网络进行测试。
此外,对于pytorch提供的相关函数,笔者也只是简单了解了一下他的使用方法,并未对函数内部本身进行研究,如果后续想要改进模型的话,还是需要我们对底层代码进行深入挖掘的。
2024-11-18-20:12,收货颇丰
原文地址:https://blog.csdn.net/m0_72300717/article/details/143817262
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!