一.项目介绍
1.概述
识别手写数字项目是我们学习深度学习的基础项目,非常适合刚起步的初学者实践的好项目。此项目采用五层卷积神经网络作为网络模型,准确率高达99%以上。同时此项目还设计了一个GUI界面,能够快速识别现场手写数字,快来试试吧 ~ ~
2.运行效果图:
图1:这里可以看到准确率高达99%
图2:测试实况
图3:抗干扰测试
3.你能学到什么
通过自己亲手撸完代码后,我觉得自己收获还是很大的,我在这里做一个小总结:
a.了解卷积网络的基本构架:例如本项目采用了2个卷积层,2个ReLu激活层,2个最大池化层,1个全连接层。
b.对于模型训练的基本套路有了一个基本的了解:数据准备、选择模型架构、初始化模型、选择损失函数(详见【机器学习】损失函数的选择总结-CSDN博客)、选择优化方法、训练模型、模型评估、调参和优化、保存模型。(详细总结请见本人另一篇博客基于经典网络架构训练图像分类模型(超详细批注和完整思路!!!)-CSDN博客)
c.简单了解如何封装网络模型、数据加载器和模型评估函数。
d.了解如何使用GPU训练数据集,以及如何对训练好的模型进行保存和加载使用。
e.初步体验设计一个简单的GUI界面。
二.代码展示
train:(这个文件用来训练模型并保存效果最好的模型文件“best_module.pth”)
# 创作者:余潇
# 创作时间:2023-10-28 17:46
import time
import torch
from torchvision import transforms
from torch.utils.data import DataLoader
import torchvision
import torch.nn as nn
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv = nn.Sequential(
# [BATCH_SIZE, 1, 28, 28]
nn.Conv2d(1, 32, 5, 1, 2),
# [BATCH_SIZE, 32, 28, 28]
nn.ReLU(),
nn.MaxPool2d(2),
# [BATCH_SIZE, 32, 14, 14]
nn.Conv2d(32, 64, 5, 1, 2),
# [BATCH_SIZE, 64, 14, 14]
nn.ReLU(),
nn.MaxPool2d(2)
# [BATCH_SIZE, 64, 7, 7]
)
self.fc = nn.Linear(64 * 7 * 7, 10)
def forward(self, x):
x = self.conv(x)
x = x.view(x.size(0), -1)
y = self.fc(x)
return y
def get_data_loader(is_train):
to_tensor = transforms.Compose([transforms.ToTensor()]) # 数据集被转换为张量
data_set = torchvision.datasets.MNIST("", is_train, transform=to_tensor, download=True) # 加载 MNIST 数据集
return DataLoader(data_set, batch_size=15, shuffle=True) # 按照每批 15 个样本进行随机打乱
# evaluate函数用于评估模型的性能
def evaluate(test_data, net):
correct_num = 0
total_num = 0
with torch.no_grad(): # 禁用梯度计算(torch.no_grad()):因为在评估阶段我们不需要计算梯度,所以可以通过这个上下文管理器来禁用梯度计算,以提高运行效率。
for inputs, labels in test_data:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = net(inputs.view(-1, 1, 28, 28)) # 将输入数据的形状重塑为[batch_size, 1, 28, 28],其中1表示通道数,因为灰度图像只有一个通道。
loss_func = torch.nn.CrossEntropyLoss()
loss_func = loss_func.to(device)
loss = loss_func(outputs, labels)
outputs = net.forward(inputs.view(-1, 1, 28, 28))
for i, output in enumerate(outputs): # torch.argmax(output) 返回的是输出张量 output 中最大元素的索引,这个索引可以被视为模型的预测类别
if torch.argmax(output) == labels[i]:
correct_num += 1
total_num += 1
accuracy = correct_num / total_num
return accuracy, loss
# 是否用GPU训练
train_on_gpu = torch.cuda.is_available()
if not train_on_gpu:
print('CUDA is not available. Training on CPU ...')
else:
print('CUDA is available! Training on GPU ...')
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
net = Net()
net = net.to(device)
def main(net):
best_acc = 0
train_data = get_data_loader(is_train=True)
test_data = get_data_loader(is_train=False)
since = time.time()
print("initial accuracyL:{}".format(evaluate(test_data, net)[0]))
optimizer = torch.optim.Adam(net.parameters(), lr=0.001) # 创建了一个Adam优化器(torch.optim.Adam),用于更新神经网络的参数。学习率设置为0.001。
for epoch in range(10):
print("----------第{}轮训练开始----------".format(epoch))
for inputs, labels in train_data:
inputs = inputs.to(device)
labels = labels.to(device)
net.zero_grad() # 将神经网络模型 net 中的梯度清零
outputs = net.forward(inputs.view(-1, 1, 28, 28))
loss_func = torch.nn.CrossEntropyLoss() # 使用负对数似然损失(Negative Log-Likelihood Loss),它通常用于多类别分类问题
loss_func = loss_func.to(device)
loss = loss_func(outputs, labels)
loss.backward() # 反向传播的开始,用于计算如何更新模型参数以减小损失。
optimizer.step() # 使用优化器 optimizer 来更新模型的权重和偏置,以最小化损失。
now = time.time()
use_time = now - since
print("Epoch:{}, Accuracy:{}, Time:{}min {:.2f}s, Loss:{:.6f}".format(epoch, evaluate(test_data, net)[0],
use_time // 60,
use_time % 60,
evaluate(test_data, net)[1]))
epoch_acc = evaluate(test_data, net)[0]
if epoch_acc > best_acc:
best_acc = epoch_acc
best_model = net.state_dict() # 保留模型参数
torch.save(best_model, "best_model.pth")
if __name__ == "__main__":
main(net)
main:(这个文件主要用来设计GUI界面)
import tkinter as tk
import numpy as np
from PIL import Image, ImageDraw
import torch
import train
from tkinter import font
# 加载训练好的模型
model = train.Net()
model.load_state_dict(torch.load('best_model.pth', map_location=torch.device('cpu')))
model.eval()
# 创建画布和绘图工具
canvas_width = 280
canvas_height = 280
def clear_canvas():
canvas.delete('all') # 删除画布上的所有元素
img_draw.rectangle([(20, 0), (300, 280)], fill='black') # 在图像上绘制一个黑色的矩形覆盖原有内容
output_label.config(text='')
def draw(event):
x, y = event.x, event.y
canvas.create_text(x, y, text='●', font='Helvetica 20', fill='white') # 将椭圆填充颜色设为白色
img_draw.ellipse([(x - 5, y - 5), (x + 5, y + 5)], fill='white') # 将椭圆描边颜色设为白色
def recognize_digit():
# 从画布中获取图像,调整尺寸为模型所需的大小
image = img.crop((0, 0, 300, 300)).resize((28, 28)).convert('L')
# 数据预处理
input_data = np.array(image).reshape((1, 1, 28, 28)).astype('float32')
input_data = input_data / 255.0 # 将像素值缩放到0到1之间
input_tensor = torch.from_numpy(input_data)
# 使用模型进行预测
with torch.no_grad():
output = model(input_tensor)
prediction = output.argmax(dim=1).item()
result_font = font.Font(size=15)
# 显示预测结果
output_label.config(text=f'\n识别结果: {prediction}', font=result_font)
root = tk.Tk()
root.title("手写数字识别")
# 计算窗口位置
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
window_width = 320
window_height = 350
x_position = int((screen_width - window_width) / 2)
y_position = int((screen_height - window_height) / 2)
root.geometry(f"{window_width}x{window_height}+{x_position}+{y_position}")
canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg='black')
# 固定窗口大小
root.resizable(False, False)
root.geometry(f"{window_width}x{window_height}+{x_position}+{y_position}")
canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg='black')
canvas.pack()
img = Image.new('RGB', (canvas_width, canvas_height), 'black')
img_draw = ImageDraw.Draw(img)
canvas.bind('<B1-Motion>', draw)
recognize_button = tk.Button(root, text="识别", command=recognize_digit, width=10, height=2)
recognize_button.pack(side=tk.LEFT, padx=10, pady=10)
clear_button = tk.Button(root, text="清除", command=clear_canvas, width=10, height=2)
clear_button.pack(side=tk.RIGHT, padx=10, pady=10)
output_label = tk.Label(root, text='', font=('Arial', 20))
output_label.pack()
root.mainloop()