BP神经网络实现手写数字识别Python实现,带GUI手写画板


在B站看了一个机器学习基础的视频( 链接)后,发现到资料里面有一个用BP神经网络对手写数字进行分类的模型。有一天晚上躺在床上,突然灵感一来,何不把这个样本改造成一个真正可以进行手写数字识别的程序?用手机查了一下tkinter画图(对tkinter比较熟),稍微看了一下感觉应该可以实现,就下决定把它做出来,一方面也可以巩固学到的知识。一共花了20多个小时,80%的时候花在调教tkinter上了。。。

GitHub下载链接:点我
CSDN资源中心下载链接:点我



下面就是整个程序的内容。

需要导入的模块

import tkinter
from tkinter import *
from tkinter import ttk, Frame, Tk, messagebox, Menu
from PIL import Image, ImageDraw
import pickle
import csv
import numpy as np
from sklearn.datasets import load_digits
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split

BP神经网络模型

这个模型有3层神经网络,每层神经元个数分别为64,100,50 。
数据集来自于sklearn自带的手写数字数据集。
数据集的每个样本有64个特征,这是由8x8的图片提取每一个像素的灰度值得来的。每一个样本最后还有一个手动添加的偏置值。
每个样本的标签是手写图片对应的数字,即0~9 。通过one-hot编码后,变成10个输出。(刚接触神经网络,对于这一点的其实我不是很理解,10个输出和1个输出具体有什么差别?)
所以神经网络的输入是65个值,输出是10个值。

def sigmoid(x):
    return 1/(1+np.exp(-x))

def dsigmoid(x):
    return x*(1-x)

class NeuralNetwork:
    def __init__(self,layers):  # (64,100,50,10)
        # 权值的初始化,范围-1到1
        self.U = np.random.random((layers[0]+1,layers[1]+1))*2-1
        self.V = np.random.random((layers[1]+1,layers[2]+1))*2-1
        self.W = np.random.random((layers[2]+1,layers[3]))*2-1
        
    def train(self,X,y,X_test,y_test,lr=0.11,epochs=10000):
        # 添加偏置
        temp = np.ones([X.shape[0],X.shape[1]+1])
        temp[:,0:-1] = X  # 最后一列都是1
        X = temp
        
        for n in range(epochs+1):
            i = np.random.randint(X.shape[0]) # 随机选取一个数据
            x = [X[i]]
            x = np.atleast_2d(x)  # 转为2维数据 (1, 65)

            L0 = sigmoid(np.dot(x,self.U))
            L1 = sigmoid(np.dot(L0,self.V))  # 隐层输出
            L2 = sigmoid(np.dot(L1,self.W))  # 输出层输出
            
            L2_delta = (y[i]-L2)*dsigmoid(L2)
            L1_delta= L2_delta.dot(self.W.T)*dsigmoid(L1)
            L0_delta= L1_delta.dot(self.V.T)*dsigmoid(L0)
            
            self.W += lr*L1.T.dot(L2_delta)
            self.V += lr*L0.T.dot(L1_delta)
            self.U += lr*x.T.dot(L0_delta)
            
            #每训练1000次预测一次准确率
            if n%1000==0:
                predictions = []
                for j in range(X_test.shape[0]):
                    o = self.predict(X_test[j])
                    predictions.append(np.argmax(o)) # 获取预测结果
                self.accuracy = np.mean(np.equal(predictions,y_test))
                print('epoch:',n,'accuracy:',self.accuracy)
        
    def predict(self,x):
        #添加偏置
        temp = np.ones(x.shape[0]+1)
        temp[0:-1] = x
        x = temp
        x = np.atleast_2d(x) # 转为2维数据
        
        L0 = sigmoid(np.dot(x,self.U))
        L1 = sigmoid(np.dot(L0,self.V))  # 隐层输出
        L2 = sigmoid(np.dot(L1,self.W))  # 输出层输出
        
        return L2

用tkinter编写用于手写输入的画板

接下来的是,用tkinter做一个手写输入的GUI界面。这样才算是一个完整的手写输入识别程序。

class Window(Frame):
    
    def __init__(self, master= None):
        super().__init__()
        self.master = master
        self.init_window()
        
        # 记录最后绘制图形的id
        self.lastDraw = 0
        
        # 前景色
        self.foreColor = '#000000'
        self.backColor = '#FFFFFF'
        
        #控制是否允许画图的变量,1:允许,0:不允许
        self.yesno = tkinter.IntVar(value=0)
        #控制画图类型的变量 
        self.what = tkinter.IntVar(value=1)

        # 记录鼠标位置的变量
        self.X = tkinter.IntVar(value=0)
        self.Y = tkinter.IntVar(value=0)

        self.samples = np.array([])  # 保存手写数字的样本
        self.labels = np.array([])  # 保存样本对应的数字标签

        
    def init_window(self):
        self.master.title('手写数字识别demo')
        
        menubar = Menu(self.master)
        self.master.config(menu=menubar)
        
        menu = Menu(menubar)
        menu.add_command(label="训练新的模型", command=self.retrainning)
        menu.add_command(label="训练新的模型(含新样本)", command=self.train_with_newsample)
        menu.add_command(label="关于", command=self.aboutme)
        menubar.add_cascade(label="菜单", menu=menu)

        self.frame_info = ttk.LabelFrame(self.master, text='Info: ' )
        self.frame_info.place(x=15,y=0)
        self.infoLabel = ttk.Label(self.frame_info, text="使用提示: 写字→载入模型→数字识别→保存样本(可选)",anchor="center",font=("微软雅黑",9))
        self.infoLabel.pack(fill=tkinter.BOTH, expand=tkinter.YES)
        
        self.frame_pad = ttk.LabelFrame(self.master, text="写字区")
        self.frame_pad.place(x=10, y=50, width=200, height=200)
        
        # 创建画布
        image = tkinter.PhotoImage()
        self.canvas = tkinter.Canvas(self.frame_pad, bg='white', width=200, height=200)
        self.canvas.create_image(120, 120, image=image)
        self.canvas.bind('<B1-Motion>', self.onLeftButtonMove)
        self.canvas.bind('<Button-1>', self.onLeftButtonDown)
        self.canvas.bind('<ButtonRelease-1>', self.onLeftButtonUp)
        self.canvas.pack(fill=tkinter.BOTH, expand=tkinter.YES)
        self.base = Image.new("RGB", (200, 200), (255,255,255))
        self.d = ImageDraw.Draw(self.base)
        
        action_frame = ttk.Frame(root)
        action_frame.place(x=225,y=65,width=70,height=150)
        button_cl = ttk.Button(action_frame, text="重写", command=self.Clear)        
        button_cl.pack(pady=5)
        button_start = ttk.Button(action_frame, text="载入模型", command=self.load_model)
        button_start.pack(pady=15)
        button_reg = ttk.Button(action_frame, text="数字识别", command=self.predict)
        button_reg.pack(pady=5)  
       
        self.frame2 = ttk.LabelFrame(self.master, text="数字识别结果")
        self.frame2.place(x=320, y=50, width=150, height=150)
        image2 = tkinter.PhotoImage()
        self.canvas2 = tkinter.Canvas(self.frame2, bg='white', width=200, height=200)
        self.canvas2.create_image(120, 120, image=image2)
        self.canvas2.pack(fill=tkinter.BOTH, expand=tkinter.YES)
        
        self.label = ttk.Label(self.master, text="输入数字作\n为样本标签",anchor="center",font=("微软雅黑",10))
        self.label.place(x=290,y=215,width=120,height=60)
        self.numEntry = Entry(self.master)
        self.numEntry.place(x=250,y=230,width=50,height=30)
        button_save = ttk.Button(self.master, text="保存样本", command=self.saveSample)
        button_save.place(x=400,y=230,width=70,height=30)


    # 按住鼠标左键移动,画图
    def onLeftButtonMove(self,event):
        # global lastDraw
        if self.yesno.get()==0:
            return
        if self.what.get()==1:
            #使用当前选择的前景色绘制曲线
            # canvas.create_line(X.get(), Y.get(), event.x, event.y, width=8, fill=foreColor)
            self.canvas.create_oval(self.X.get(), self.Y.get(), event.x, event.y, width=8, fill=self.foreColor)
            self.d.line([self.X.get(), self.Y.get(), event.x, event.y],
                    width=8,
                    fill='black')

            self.X.set(event.x)
            self.Y.set(event.y)
        
        # 鼠标左键单击,允许画图
    def onLeftButtonDown(self,event):
        self.yesno.set(1)
        self.X.set(event.x)
        self.Y.set(event.y)
        if self.what.get()==4:
            self.canvas.create_text(event.x, event.y, text=text)

    # 鼠标左键抬起,不允许画图
    def onLeftButtonUp(self,event):
        self.yesno.set(0)
        self.lastDraw = 0
        
    # 添清除
    def Clear(self):
        # pillow的img对象重新画成白色的
        self.d.rectangle([0,0,200,200],fill='white')
        
        # 删除tkinter canvas的所有对象
        for item in self.canvas.find_all():
            self.canvas.delete(item)
        for item in self.canvas2.find_all():
            self.canvas2.delete(item)
            
        self.label['text'] = "输入数字作\n为样本标签"
        self.label['foreground'] = ['black']

    # 训练模型
    def trainning(self,newSample=None):
        digits = load_digits()  # 载入数据
        X = digits.data  # 数据
        y = digits.target  # 标签
        # 输入数据归一化
        X -= X.min()
        X /= X.max()

        if newSample is not None:
            X = np.concatenate((X, newSample[:,:-1]))
            y = np.concatenate((y, newSample[:,-1].astype(int)))
            
        nm = NeuralNetwork([64,100,50,10]) # 创建网络

        X_train,X_test,y_train,y_test = train_test_split(X,y)
        # labels_train = LabelBinarizer().fit_transform(y_train)
        # labels_test = LabelBinarizer().fit_transform(y_test)
        # 用numpy进行onehot得到的是浮点数,须要修改一下数据格式。
        labels_train = np.eye(10)[y_train].astype(np.int16)
        labels_test = np.eye(10)[y_test].astype(np.int16)

        print('start')
        nm.train(X_train,labels_train,X_test,y_test,epochs=20000)
        print('end')
        
        return nm


    def train_with_newsample(self):
        try:
            new_samples = np.genfromtxt('mysamples.csv', delimiter=',')
            
            if len(new_samples) > 5:
                self.model = self.trainning(newSample=new_samples)
            else:
                self.infoLabel['text'] = '提示:mysamples.csv文件没有足够的新样本'
                self.infoLabel['foreground'] = ['red']
        except:
            self.infoLabel['text'] = '提示:当前目录未找到mysamples.csv文件,或数据格式有误!'
            self.infoLabel['foreground'] = ['red']
        
        
    def retrainning(self):
        print('retrainning...')
        self.infoLabel['text'] = '提示:正在训练新的模型。。。'
        self.infoLabel['foreground'] = ['blue']
        self.model = self.trainning()
            
        with open('nmModel.pkl', 'wb') as pkl:
            pickle.dump(self.model, pkl, pickle.HIGHEST_PROTOCOL)
            self.infoLabel['text'] = '提示:新模型训练完成!'
            self.infoLabel['foreground'] = ['blue']

        
    def load_model(self):
        try:
            pkl = open('nmModel.pkl', 'rb')
            self.model = pickle.load(pkl)
            
        except:
            self.infoLabel['text'] = '提示:未找到本地模型,正在训练新的模型。'
            self.infoLabel['foreground'] = ['red']
            self.model = self.trainning()
            
            with open('nmModel.pkl', 'wb') as pkl:
                pickle.dump(self.model, pkl, pickle.HIGHEST_PROTOCOL)
        finally:
            self.infoLabel['text'] = '提示:模型加载完成'
            self.infoLabel['foreground'] = ['blue']
            

    def predict(self):
        
        preproces = self.pre_job()
        if not preproces:
            return
        
        for item in self.canvas2.find_all():
            self.canvas2.delete(item)

        try:
            result = self.model.predict(self.test)
        except AttributeError:
            self.canvas2.create_text(18, 65,
            text = '模型未加载\n或加载失败\n请重载模型',
            font = ("微软雅黑", 16, "bold"),
            fill= "red",
            anchor = W,
            justify = LEFT)

            return
        
        print(np.argmax(result))
        
        titleFont = ("微软雅黑", 50, "bold")
        self.canvas2.create_text(45, 65,
            text = np.argmax(result),
            font = titleFont,
            fill= "Turquoise",
            anchor = W,
            justify = LEFT)

    def pre_job(self):
        img = self.base
        x,y = img.size
        img = img.convert('L')
        raw_data = img.load()
        
        "这里有点奇怪,横纵颠倒了?"
        "行列和横纵坐标"
        L = [[raw_data[j, i] for j in range(img.size[0])] for i in range(img.size[1])]
        L_arry = np.array(L)
        print(L_arry.shape)
        
        row_member = L_arry.sum(axis=1) < 245*img.size[0]
        col_member = L_arry.sum(axis=0) < 245*img.size[1]

        # 图片裁剪的边缘
        r_cs = row_member.cumsum()
        
        if r_cs.max() < 2:
            # 过滤少于2行非白色像素,即没有画数字的情况
            self.canvas2.create_text(15, 38,
            text = '请先在写字\n区写数字',
            font = ("微软雅黑", 18, "bold"),
            fill= "red",
            anchor = W,
            justify = LEFT)
            
            return
            
        y_min =np.argwhere(r_cs == 1)[0,0] - 1 # 第一个非纯白的列
        y_max = r_cs.argmax() + 1

        c_cs = col_member.cumsum()
        x_min = np.argwhere(c_cs == 1)[0,0] - 1
        x_max = c_cs.argmax() + 1
        
        # 要裁剪成矩形,需要检查一下横竖边
        x_len = x_max - x_min
        y_len = y_max - y_min
        if y_len - x_len > 0:
            x_min = x_min - int(1/2 * (y_len - x_len))
            if x_min < 0:
                x_min = 0
            x_max = x_min + y_len
        elif y_len - x_len < 0:
            y_min = y_min - int(1/2 * (x_len - y_len))
            if y_min < 0:
                y_min = 0
            y_max = y_min + x_len
            
        new = img.crop((x_min, y_min, x_max, y_max))
        print(new.size)
        
        new = new.resize((8, 8))  # 裁剪成和训练数据一样的尺寸
        new_data = new.load()
        new_array = np.array([[new_data[j, i] for j in range(8)] for i in range(8)])
        print(new_array.shape)
        
        test = (255 - new_array) / 255
        
        self.test = np.r_[test.ravel()]
            
        print(self.test)

        return 'Done'


    def saveSample(self):
        target = self.numEntry.get()
        print(type(target))
        
        try:
            print(self.test.shape)
        except:
            preproces = self.pre_job()
            if not preproces:
                return
        
        if target.isdigit():
            print(self.test.shape)
            if self.test.shape != (64,):
                self.label['text'] = '样本数据格式\n有误,请重试'
                self.label['foreground'] = ['red']
                return
            
            print(target)
            self.label['foreground'] = ['black']
            
            with open('mysamples.csv','a', newline='') as csvfile:
                # obj = self.test.append(int(target))
                obj = self.test.tolist()
                obj.append(int(target))
                print(obj)
                writer = csv.writer(csvfile)
                writer.writerow(obj)
                print('Save!')
                
                self.label['text'] = '保存成功'
                self.label['foreground'] = ['blue']

        else:
            self.label['text'] = '先输入数字作\n为样本的标签'
            self.label['foreground'] = ['red']

            
    def aboutme(self):
        messagebox.showinfo("关于","\n\n基于神经网络和sklearn的digit数据集编写的手写数字识别demo\n\n"
                                   "Recognizer for handwritten numeral-v0.01\n\n"
                                   "                                         Copyright by 查尔Char\n"
                                   "                                          All Rights Reserved")

   

最后再加上主程序运行的声明,就完成了。

if __name__ == '__main__':
    root = Tk()
    sw = root.winfo_screenwidth()
    sh = root.winfo_screenheight()
    ww = 500
    wh = 300
    x = (sw-ww) / 2 - 100
    y = 200
    root.geometry("%dx%d+%d+%d" % (ww, wh, x, y))
    app = Window(root)
    root.mainloop()

程序运行的效果截图


二层神经网络的效果

在这里插入图片描述
在这里插入图片描述



三层神经网络的效果(和两层的效果感觉差别不大)


在这里插入图片描述
在这里插入图片描述


初步测试,感觉7较难识别正确,很大概率会被识别为3 。

当然,这神经网络是一个很简单的系统,还有sklearn里面对于图像的处理我也不清楚,只是按照个人理解进行转换,正确率比较低也可能跟我数据转换有关系。
在这里插入图片描述

在这里插入图片描述


菜单里面放了一个把新的手写样本加入到训练数据里的选项。方便对错误率比较高的数字进行训练。
通过简单的修改,这个程序也可以变成识别手写英文字母的(大概?)。

在这里插入图片描述

  • 10
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值