手写数字识别画板(qt)

一、前言

  • 环境介绍
    • 系统:win10
    • 语言:python3.7
    • 框架:pytorch1.1
  • 内容概要
    • 神经网络分类实验(对应于机器学习的聚类算法Kmeans)
    • 手写数字集识别(神经网络+工程)

二、神经网络对数据分类

2.1 网络数据生成

  • 步骤一:

先随机生成一些数据,数据截图如下:

1567940707870

  • 步骤二

通过两个相交的函数将数据分为4类,分类结果如下:

1567941617881

分割函数如下设计:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class TrainData(data.Dataset):
def __init__(self):
super(TrainData,self).__init__()
self.TrainData = self.generateTrainData()

def __getitem__(self, item):
return torch.Tensor(self.TrainData[item,0:2]), torch.Tensor([self.TrainData[item, 2]])

def __len__(self):
return len(self.TrainData)

def generateTrainData(self):
outData = []
centerPoint = [[3,3],[8,5],[2,7]]
for i in range(300):
rand_x = np.random.random_sample(1)[0] * 10
rand_y = np.random.random_sample(1)[0] * 10
# label = 3
# for index,(x,y) in enumerate(centerPoint):
# if np.sqrt(np.power(rand_x - x,2) + np.power(rand_y-y,2)) <= 1.5:
# label = index

x1 = 0.1 * np.power(rand_x, 2)
x2 = 0.1 * np.power(rand_x - 10, 2)

if (rand_y <= x2) and (rand_y >= x1):
label = 0
elif (rand_y <= x2) and (rand_y <= x1):
label = 1
elif (rand_y >= x2) and (rand_y <= x1):
label = 2
else:
label = 3

outData.append([rand_x, rand_y, label])

outData = np.array(outData)

plt.scatter(outData[outData[:, 2] == 0, 0], outData[outData[:, 2] == 0, 1], c="r")
plt.scatter(outData[outData[:, 2] == 1, 0], outData[outData[:, 2] == 1, 1], c="g")
plt.scatter(outData[outData[:, 2] == 2, 0], outData[outData[:, 2] == 2, 1], c="b")
plt.scatter(outData[outData[:, 2] == 3, 0], outData[outData[:, 2] == 3, 1])
plt.show()

return outData

2.2 网络设计

网络分为4层,为什么要4层!!随意吧,反正越多效果越好,越多学习速率越慢。网络层数3层也行,重要是激励函数,激励函数我使用了4种:

  • 无激励函数:函数学习速度快,但是不理想,因为没有激励函数,输出只不过是输入的线性组合,发挥不出网络的威力。
  • Sigmoid激励函数:按道理这里应该使用Sigmoid或者softmax激励函数,他们的输出都是0~1,适合用于处理分类问题(逻辑回归),但是这里为了熟悉线性回归,使用了以下两个激励函数,可以看出需要的神经元数量还是相当的多。
  • relu激励函数:学习速度快,学习的结果不错。他和无激励函数的区别就是它可以选择性的使得一些神经元失去活性,更加类似于人的大脑。函数表示为:return max(x,0)
  • Softplus激励函数:学习速度略慢与relu,学习结果比relu更加圆滑。数学表达式为:$log(1+e^x)$

既然relu和Softplus都可以,选择就出现了。如果面对比较深的网络选择使用relu,浅的网络选择使用Softplus。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class net(nn.Module):
def __init__(self):
super(net,self).__init__()

self.layer1 = nn.Linear(2,200)
self.active1 = nn.Softplus()

self.layer2 = nn.Linear(200,200)
self.active2 = nn.Softplus()

self.layer3 = nn.Linear(200,4)

def forward(self, x):
x = self.layer1(x)
# x = F.relu(x)

x = self.active1(x)
x = self.layer2(x)
# x = F.relu(x)
x = self.active2(x)
x = self.layer3(x)

return x

2.3 训练数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def setparamsLr(model,lr):  # 修改学习速率
params = []
params_dict = dict(model.named_parameters())

for key,value in params_dict.items():
if "weight" in key:
params.append({'params': value, 'lr': lr})
elif "bias" in key:
params.append({'params': value,'lr': lr})
return params

learning_rate = 0.005# 学习速率
num_epoches = 1000 # 迭代次数

if __name__ == '__main__':
# 网络构建
module = net()

# 数据生成
allData = TrainData()
trainData = DataLoader(allData, batch_size = 5, shuffle = True)
dataLen = trainData.__len__()

# 损失函数设计, 默认会对batch求平均
Loss_func = nn.CrossEntropyLoss()

optimizer = torch.optim.SGD(setparamsLr(module, learning_rate), lr=learning_rate)

for epoch in range(num_epoches):
loss_avg = 0
num = 0 # 记录数据匹配的个数,可以判断网络是否可以学习,大致判断学习效果
module.train()
for index, (testData, label) in enumerate(trainData):
data = torch.tensor(testData, requires_grad=True).float()
label = torch.tensor(label[:,0]).long()

out = module(data)
loss = Loss_func(out, label)

optimizer.zero_grad()
loss.backward()
optimizer.step()

loss_avg = loss_avg + loss.data.numpy()
if label[0] == torch.max(out.data[0],0)[1]:
num = num + 1
print(num)
print("epoch is :",epoch, " loss_avg is : ", loss_avg / dataLen)

2.4 测试结果

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.eval()
test = []
for x in range(50):
for y in range(50):
out = module(torch.tensor([x/5,y/5]).float()) #
outindex = torch.max(out.data, 0)[1]
test.append([x/5,y/5,outindex])

test = np.array(test)

plt.clf()
plt.axis([-1,11,-1,11])
plt.scatter(test[test[:, 2] == 0, 0], test[test[:, 2] == 0, 1], c="r")
plt.scatter(test[test[:, 2] == 1, 0], test[test[:, 2] == 1, 1], c="g")
plt.scatter(test[test[:, 2] == 2, 0], test[test[:, 2] == 2, 1], c="b")
plt.scatter(test[test[:, 2] == 3, 0], test[test[:, 2] == 3, 1])
plt.pause(0.1)

从下图可以看出,学校效果不错

1568103449464

再随机生成一些数据,测试网络

1568103068271

1568103046579

2.5 思考

神经网络分类的网络搭建和神经网络拟合在设计上面的区别

三、卷积神经网络

这里不讲图片卷积的基础原理,推荐看《数字图像处理》——冈萨雷斯。本章以对图片去噪为例子。

3.1 传统去噪方法

这里使用椒盐噪声为例,什么是椒盐噪声,就是图片上面被纯黑色和纯白色干扰的图片。

原图片如下:

1568277152338

加上椒盐噪声:

1568277480315

使用中值滤波器滤波之后:

1568279408867

仿真代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import numpy as np
import cv2

def sp_noise(image,prob):
'''
添加椒盐噪声
prob:噪声比例
'''
output = np.zeros(image.shape,np.uint8)
thres = 1 - prob
for i in range(image.shape[0]):
for j in range(image.shape[1]):
rdn = np.random.random()
if rdn < prob:
output[i][j] = 0
elif rdn > thres:
output[i][j] = 255
else:
output[i][j] = image[i][j]
return output

def m_filter(im, x, y, step = 3):
sum_s=[]
for k in range(-int(step/2),int(step/2)+1):
for m in range(-int(step/2),int(step/2)+1):
if ((x + k) < 0) or ((y+m) <= 0) or ((x + k) > (im.shape[0] - 1)) or ((y+m) > (im.shape[1] - 1)):
sum_s.append(0)
else:
sum_s.append(im[x+k][y+m])
sum_s.sort()
return sum_s[(int(step*step/2)+1)]

img = cv2.imread("lena.bmp",cv2.IMREAD_GRAYSCALE) # 单色图
img = sp_noise(img,0.02) # 加噪声
im_copy = np.copy(img) # 复制数组

step= 3
for i in range(0,img.shape[0]): # 开始滤波
for j in range(0,img.shape[1]):
im_copy[i][j]=m_filter(img,i,j)

cv2.imshow("lena",im_copy) # 显示图片
cv2.imshow("lena2",img)
cv2.waitKey(0)

好像并不能很好的解释卷积,不管了,沾到一点边即可。

3.2 神经网络去噪方法

主要步骤如下:

  • 数据集(由于搜集数据集太过麻烦,所以。。。)
  • 网络设计(设计一个隐藏层就好)
  • 损失函数设计(应该没有自带的损失函数了)

开始实现:

具体:略

四、手写数字集实现

已经写过了,详情请参考以下内容:

主要步骤如下:

  • 数据集(官方有,pytorch直接通过网络下载)
  • 网络设计(使用成熟的2层卷积网络,3层线性网络,这是成熟的google网络好像)
  • 损失函数设计(使用自带的交叉损失函数)

4.1 数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torchvision

original_TrainData = torchvision.datasets.MNIST(
root='./mnist/',
train=True,
transform=torchvision.transforms.ToTensor(),
download=True,
)

# 获得测试数据,本地没有就会通过网络下载
original_TestData = torchvision.datasets.MNIST(
root='./mnist/',
train=False, # 下载测试集
transform=torchvision.transforms.ToTensor(), # 输出转换为Tensor类型
download=True, # 如果本地没有,则下载
)

查看数据集代码:

1
2
3
4
5
6
import matplotlib.pyplot as plt
for D,L in train_data:
print(L[0])
print(D[0][0])
plt.imshow(D[0][0],cmap="gray")
plt.show()

训练数据截图

1569916569792

1569916581238

1569916619250

4.2 网络设计

这里使用的是现有的网络模型LeNet1 ,注意每一层的输出尺寸,容易写错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
def __init__(self):
super(Net,self).__init__()

# 卷积层,输入一张图片,输出6张,滤波器为5*5大小,cuda表示使用GPU计算
self.conv1 = nn.Conv2d(1,6,5)
self.conv2 = nn.Conv2d(6,16,5)
self.fc1 = nn.Linear(16 * 4 * 4,120)
self.fc2 = nn.Linear(120,84)
self.fc3 = nn.Linear(84,10)

# 继承来自nn.Module的接口,必须实现,不能改名。
# max_pool2d,池化函数,用来把图像缩小一半
# relu 神经元激励函数,y = max(x,0)
def forward(self, x):
x = F.max_pool2d(F.relu(self.conv1(x)),(2,2))
x = F.max_pool2d(F.relu(self.conv2(x)),(2,2))

x = x.view(-1, 16 * 4 * 4) # 类似于reshape功能,重塑张量形状
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

4.3 开始训练并测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from torch.utils.data import DataLoader
import torch

# 定义模块
model = Net().cuda()

# 定义损失函数,其实就是一个计算公式
criterion = nn.CrossEntropyLoss()

# 定义梯度下降算法,把model内的参数交给他
optimizer = torch.optim.SGD(model.parameters(),lr=0.01,momentum = 0.9)

for num_epoche in range(20):
train_data = DataLoader(dataset=original_TrainData,batch_size = 20,shuffle = True) # 打乱数据,处理格式

model.train(mode=True) # 设置为训练模式

for index, (data,lable) in enumerate(train_data):
# data torch.Size([60, 1, 28, 28])
D = torch.tensor(data,requires_grad=True).cuda() # cuda表示放在GPU计算
L = torch.tensor(lable).cuda() # cuda表示放在GPU计算

out = model(D)
loss = criterion(out,L) # loss 是一个值,不是向量
optimizer.zero_grad() # 清除上一次的梯度,不然这次就会叠加
loss.backward() # 进行反向梯度计算
optimizer.step() # 更新参数

model.eval() # 设置网络为评估模式
eval_loss = 0 # 保存平均损失
num_count = 0 # 保存正确识别到的图片数量
test_data = DataLoader(dataset=original_TestData,batch_size = 20,shuffle = True)
for index, (data,lable) in enumerate(test_data):
D = torch.tensor(data).cuda()
L = torch.tensor(lable).cuda()

out = model(D)
loss = criterion(out,L) # 计算损失,可以使用print输出
eval_loss += loss.data.item() * L.size(0) # loss.data.item()是mini-batch平均值

pred = torch.max(out,1)[1] # 返回每一行中最大值的那个元素,且返回其索引。如果是0,则返回每列最大值
num_count += (pred == L).sum() # 计算有多少个,这种方法只支持troch.tensor类型

acc = num_count.float() / 10000
eval_loss = eval_loss / 10000
print("num_epoche:%2d,num_count:%5d, acc: %6.4f, eval_loss:%6.4f"%(num_epoche,num_count,acc,eval_loss))

运行,从打印结果可以看出,准确率可以达到98%左右。

五、制作手写窗口

第四章写的是学习手写数字,这一章写应用,目标设计如下图所示:

5.1 程序结构

整个程序设计分为三个部分:

  • 界面设计(略讲,我也不懂)
    • 画板功能实现
      • 重写widget内的paintEvent函数
      • 图层管理
    • 按键功能实现(信号与槽)
    • 界面排版
    • 界面渲染
  • 图像分割
    • 简单分割
      • 获取X坐标
      • 获取Y坐标
      • 添加Padding(因为训练集有Padding,没办法)
    • 精确分割
      • 图像二值化
      • 宽度优先遍历
      • 深度优先遍历
  • 图像识别
    • 数据集获取
    • 网络搭建
    • 训练部分
    • 测试部分

5.2 界面设计

代码已经上传至github

5.2.1 基础入门知识

推荐视频教程

5.2.2 画板功能实现

Qt的窗口是有多个widget(小部件)组成的,widget里面可以放置另一个widget,不停的叠加下去。对于每一个widget来说,都会有一个paintEvent函数来负责绘制这个widget的样子,比如按钮、输入栏这些。paintEvent函数会在需要显示界面或者更新的时候被自动触发,比如初始化,和切换窗口的时候,也可以通过self.update()主动触发。

所以我这里创建一个widget,通过mouseMoveEvent捕捉鼠标移动事件,记录移动的点。根据记录的点复写其paintEvent函数,实现画板功能。

除此之外,在本程序中,还设置了三个图层,分别用来显示原始图像、处理的图像、标记信息,方便界面操作。

5.2.3 按键功能实现

(只介绍按键功能,和本次项目无关,重新写时间太长了)

  • 界面创建一个按钮
  • 添加信号槽
  • 编写实现代码
  • 创建应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from tempUI.untitled import  Ui_Form
from PyQt5.QtWidgets import QWidget,QApplication
import sys

class test(QWidget,Ui_Form):
def __init__(self):
super(test,self).__init__()
self.setupUi(self)

def Button_test(self):
print("hello word")

if __name__ == "__main__":
app = QApplication(sys.argv) # 必须创建一个应用空间,由它来负责程序的运行和信号的调用
ex = test() # 创建一个widget
ex.show() # 显示
sys.exit(app.exec_()) # 捕捉程序退出信息
5.2.4 界面的排版

(只介绍按键功能,和本次项目无关,重新写时间太长了)

直接拖进去的空间不太好看,可以使用布局进行简单排版

(这里应该有实际操作演示)

1569921299111

1569921393259

可以看到控件在对齐方面已经没有问题

5.2.5 界面渲染

(只介绍按键功能,和本次项目无关,重新写时间太长了)

这里操作类似于html的css语法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
QWidget{
background-color: rgb(237, 248, 255);
border:1px solid rgb(180, 180, 180);
}

QPushButton{
color:rgb(0,0,0);
background-color:#acffaf;
border-radius:10px;
border:0px solid rgb(180, 180, 180);
}

QPushButton:hover{
background-color:#c4ffdf;
}

QPushButton:pressed{
background-color:#84aeb4;
border: None;
}

QCheckBox
{
border:0px;
height:24px;
font-size:14px;
border-radius:2px;
}

QComboBox{
background: rgb(230,230,230);
border-radius: 4px;
}
QComboBox::hover{
color:green;
border-color:rgb(50,180,40);
}
QComboBox::drop-down{
width: 25px;
border: none;
background: transparent;
}

python加载css语法:

1
2
with open("css/mainwindow.css","r") as file_css:
self.setStyleSheet(file_css.read())

结果显示:

1569921707270

根据上面的原理,优化后的界面如下:

1569922126225

5.3 数字分割

5.3.1 简单分割

要实现数字识别,首先要对画板的数字进行分割,找到每个数字的位置。这里采用比较粗糙的识别方式,先对x方向的像素进行统计,统计图像如下,同理在对y方向进行统计,即可以得到每一个图像的位置。

1569915681058

1569158120051

就是先确定x坐标,再确定y坐标,就可以定位数字的位置。定位到位置后添加padding后生成测试图片:

未添加padding的图片:

1569922390272

1569922374960

添加padding的图片:

1569922451971

1569922440344

思考:为什么要添加padding

但是这种方法当数字挨的比较近之后,就会显示识别失误的问题,如下:

1569922217382

5.3.2 精确分割
  • 图论遍历
    类似于图论的遍历
  • 深度优先遍历
    程序使用递归,结构简单,好理解。但是当碰到较大的图的时候,递归深度过高会导致堆栈溢出。故而对于未知深度的递归程序不建议使用。
  • 宽度优先遍历
    可以解决深度优先遍历出现的问题。

深度优先遍历代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def dfs(self, image, x, y):
if x > self.width or y > self.heigh or x < 0 or y < 0:
return
if image[y, x] != 255:
return

self.Spitmode_of_2_point.append([x, y])
image[y, x] = 0

self.dfs(image, x, y + 1)
self.dfs(image, x, y - 1)
self.dfs(image, x + 1, y)
self.dfs(image, x - 1, y)
self.dfs(image, x - 1, y - 1)
self.dfs(image, x + 1, y - 1)
self.dfs(image, x - 1, y + 1)
self.dfs(image, x + 1, y + 1)

宽度优先遍历代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def bfs(self, image, x, y):
queue_point = []

queue_point.append([y, x])
while queue_point != []:
y, x = queue_point.pop()
self.Spitmode_of_2_point.append([x, y])
image[y, x] = 0
for y_index in [-1,0,1]:
for x_index in [-1,0,1]:
if (y + y_index) >= 0 and (y + y_index) <= self.heigh and (x + x_index) >= 0 and (
x + x_index) <= self.width:
if image[y + y_index, x + x_index] == 255:
queue_point.append([y + y_index, x + x_index])

数字书写不再受到从左到右的限制

1570107719488

5.4 图像识别

见第4章

参考文献

1. LeNet-5详解
-------------本文结束感谢您的阅读-------------
0%