分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow
也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!
神经网络实现手写字符识别系统
一、课程介绍
1. 课程来源
本课程核心部分来自《500 lines or less》项目,作者是来自 Mozilla 的工程师 Marina Samuel,这是她的个人主页:http://www.marinasamuel.com/ 。项目代码使用 MIT 协议,项目文档使用http://creativecommons.org/licenses/by/3.0/legalcode 协议。
课程内容在原文档基础上做了稍许修改,增加了部分原理介绍,步骤的拆解分析及源代码注释。
2. 内容简介
本课程最终将基于BP神经网络实现一个手写字符识别系统,系统会在服务器启动时自动读入训练好的神经网络文件,如果文件不存在,则读入数据集开始训练,用户可以通过在html
页面上手写数字发送给服务器来得到识别结果。
3. 课程知识点
本课程项目完成过程中,我们将学习:
- 什么是神经网络
- 在客户端(浏览器)完成手写数据的输入与请求的发送
- 在服务器端根据请求调用神经网络模块并给出响应
- 实现BP神经网络
二、实验环境
打开终端,进入 Code
目录,创建 ocr
文件夹, 并将其作为我们的工作目录。
$ cd Code$ mkdir ocr && cd ocr
三、实验原理
人工智能
图灵对于人工智能的定义大家都已耳熟能详,但"是什么构成了智能"至今仍是一个带有争论的话题。计算机科学家们目前将人工智能分成了多个分支,每一个分支都专注于解决一个特定的问题领域,举其中三个有代表性的分支:
- 基于预定义知识的逻辑与概率推理,比如模糊推理能够帮助一个恒温器根据监测到的温度和湿度决定什么时候开关空调。
- 启发式搜索,比如在棋类游戏中搜索到走下一子的最优解。
- 机器学习,比如手写字符识别系统。
简单来说,机器学习的目的就是通过大量数据训练一个能够识别一种或多种模式的系统。训练系统用的数据集合被称作训练集,如果训练集的每个数据条目都打上目标输出值(也就是标签),则该方法称作监督学习,不打标签的则是非监督学习。机器学习中有多种算法能够实现手写字符识别系统,在本课程中我们将基于神经网络实现该系统。
什么是神经网络
神经网络由能够互相通信的节点构成,赫布理论解释了人体的神经网络是如何通过改变自身的结构和神经连接的强度来记忆某种模式的。而人工智能中的神经网络与此类似。请看下图,最左一列蓝色节点是输入节点,最右列节点是输出节点,中间节点是隐藏节点。该图结构是分层的,隐藏的部分有时候也会分为多个隐藏层。如果使用的层数非常多就会变成我们平常说的深度学习了。
每一层(除了输入层)的节点由前一层的节点加权加相加加偏置向量并经过激活函数得到,公式如下:
其中f
是激活函数,b
是偏置向量,它们的作用会在之后说明。
这一类拓扑结构的神经网络称作前馈神经网络,因为该结构中不存在回路。有输出反馈给输入的神经网络称作递归神经网络(RNN)。在本课程中我们使用前馈神经网络中经典的BP神经网络来实现手写识别系统。
如何使用神经网络
很简单,神经网络属于监督学习,那么多半就三件事,决定模型参数,通过数据集训练学习,训练好后就能到分类工具/识别系统用了。数据集可以分为2部分(训练集,验证集),也可以分为3部分(训练集,验证集,测试集),训练集可以看作平时做的习题集(可反复做),系统通过对比习题集的正确答案和自己的解答来不断学习改良自己。测试集可以看作是高考,同一份试卷只能考一次,测试集一般不会透露答案。那么验证集是什么呢?好比多个学生(类比用不同策略训练出的多个神经网络)要参加一个名额只有两三人的比赛,那么就得给他们一套他们没做过的卷子(验证集)来逐出成绩最好的几个人,有时也使用验证集决定模型参数。在本课程中数据集只划分训练集和验证集。
系统构成
我们的OCR系统分为5部分,分别写在5个文件中:
- 客户端(
ocr.js
) - 服务器(
server.py
) - 用户接口(
ocr.html
) - 神经网络(
ocr.py
) - 神经网络设计脚本(
neural_network_design.py
)
用户接口(ocr.html
)是一个html
页面,用户在canvans
上写数字,之后点击选择训练或是预测。客户端(ocr.js
)将收集到的手写数字组合成一个数组发送给服务器端(server.py
)处理,服务器调用神经网络模块(ocr.py
),它会在初始化时通过已有的数据集训练一个神经网络,神经网络的信息会被保存在文件中,等之后再一次启动时使用。最后,神经网络设计脚本(neural_network_design.py
)是用来测试不同隐藏节点数下的性能,决定隐藏节点数用的。
四、实验步骤
我们将根据系统构成的五部分一一实现,在讲解完每一部分的核心代码后给出完整的文件代码。
实现用户接口
需要给予用户输入数据、预测、训练的接口,这部分较简单,所以直接给出完整代码:
<!-- index.html --><!DOCTYPE html><html><head> <script src="ocr.js"></script></head><body onload="ocrDemo.onLoadFunction()"> <div id="main-container" style="text-align: center;"> <h1>OCR Demo</h1> <canvas id="canvas" width="200" height="200"></canvas> <form name="input"> <p>Digit: <input id="digit" type="text"> </p> <input type="button" value="Train" onclick="ocrDemo.train()"> <input type="button" value="Test" onclick="ocrDemo.test()"> <input type="button" value="Reset" onclick="ocrDemo.resetCanvas();"/> </form> </div></body></html>
开一个服务器看一下页面效果:
python -m SimpleHTTPServer 3000
打开浏览器地址栏输入localhost:3000
页面效果如下图:
手写输入等主要的客户端逻辑需要在ocr.js
文件中实现。
实现客服端
画布设定了200*200,但我们并不需要200*200这么精确的输入数据,20*20就很合适。
var ocrDemo = { CANVAS_WIDTH: 200, TRANSLATED_WIDTH: 20, PIXEL_WIDTH: 10, // TRANSLATED_WIDTH = CANVAS_WIDTH / PIXEL_WIDTH
在画布上加上网格辅助输入和查看:
drawGrid: function(ctx) { for (var x = this.PIXEL_WIDTH, y = this.PIXEL_WIDTH; x < this.CANVAS_WIDTH; x += this.PIXEL_WIDTH, y += this.PIXEL_WIDTH) { ctx.strokeStyle = this.BLUE; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, this.CANVAS_WIDTH); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(this.CANVAS_WIDTH, y); ctx.stroke(); } },
我们使用一维数组来存储手写输入,0代表黑色(背景色),1代表白色(笔刷色)。
手写输入与存储的代码:
onMouseMove: function(e, ctx, canvas) { if (!canvas.isDrawing) { return; } this.fillSquare(ctx, e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop); }, onMouseDown: function(e, ctx, canvas) { canvas.isDrawing = true; this.fillSquare(ctx, e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop); }, onMouseUp: function(e) { canvas.isDrawing = false; }, fillSquare: function(ctx, x, y) { var xPixel = Math.floor(x / this.PIXEL_WIDTH); var yPixel = Math.floor(y / this.PIXEL_WIDTH); //在这里存储输入 this.data[((xPixel - 1) * this.TRANSLATED_WIDTH + yPixel) - 1] = 1; ctx.fillStyle = '#ffffff'; //白色 ctx.fillRect(xPixel * this.PIXEL_WIDTH, yPixel * this.PIXEL_WIDTH, this.PIXEL_WIDTH, this.PIXEL_WIDTH); },
下面完成在客户端点击训练键时触发的函数。
当客户端的训练数据到达一定数量时,就一次性传给服务器端给神经网络训练用:
train: function() { var digitVal = document.getElementById("digit").value; // 如果没有输入标签或者没有手写输入就报错 if (!digitVal || this.data.indexOf(1) < 0) { alert("Please type and draw a digit value in order to train the network"); return; } // 将训练数据加到客户端训练集中 this.trainArray.push({"y0": this.data, "label": parseInt(digitVal)}); this.trainingRequestCount++; // 训练数据到达指定的量时就发送给服务器端 if (this.trainingRequestCount == this.BATCH_SIZE) { alert("Sending training data to server..."); var json = { trainArray: this.trainArray, train: true }; this.sendData(json); // 清空客户端训练集 this.trainingRequestCount = 0; this.trainArray = []; } },
为什么要设置BATCH_SIZE
呢?这是为了防止服务器在短时间内处理过多请求而降低了服务器的性能。
接着完成在客户端点击测试键(也就是预测)时触发的函数:
test: function() { if (this.data.indexOf(1) < 0) { alert("Please draw a digit in order to test the network"); return; } var json = { image: this.data, predict: true }; this.sendData(json); },
最后,我们需要处理在客户端接收到的响应,这里只需处理预测结果的响应:
receiveResponse: function(xmlHttp) { if (xmlHttp.status != 200) { alert("Server returned status " + xmlHttp.status); return; } var responseJSON = JSON.parse(xmlHttp.responseText); if (xmlHttp.responseText && responseJSON.type == "test") { alert("The neural network predicts you wrote a \'" + responseJSON.result + '\''); } }, onError: function(e) { alert("Error occurred while connecting to server: " + e.target.statusText); },
ocr.js
的完整代码如下:
var ocrDemo = { CANVAS_WIDTH: 200, TRANSLATED_WIDTH: 20, PIXEL_WIDTH: 10, // TRANSLATED_WIDTH = CANVAS_WIDTH / PIXEL_WIDTH BATCH_SIZE: 1, // 服务器端参数 PORT: "9000", HOST: "http://localhost", // 颜色变量 BLACK: "#000000", BLUE: "#0000ff", // 客户端训练数据集 trainArray: [], trainingRequestCount: 0, onLoadFunction: function() { this.resetCanvas(); }, resetCanvas: function() { var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); this.data = []; ctx.fillStyle = this.BLACK; ctx.fillRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_WIDTH); var matrixSize = 400; while (matrixSize--) this.data.push(0); this.drawGrid(ctx); // 绑定事件操作 canvas.onmousemove = function(e) { this.onMouseMove(e, ctx, canvas) }.bind(this); canvas.onmousedown = function(e) { this.onMouseDown(e, ctx, canvas) }.bind(this); canvas.onmouseup = function(e) { this.onMouseUp(e, ctx) }.bind(this); }, drawGrid: function(ctx) { for (var x = this.PIXEL_WIDTH, y = this.PIXEL_WIDTH; x < this.CANVAS_WIDTH; x += this.PIXEL_WIDTH, y += this.PIXEL_WIDTH) { ctx.strokeStyle = this.BLUE; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, this.CANVAS_WIDTH); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(this.CANVAS_WIDTH, y); ctx.stroke(); } }, onMouseMove: function(e, ctx, canvas) { if (!canvas.isDrawing) { return; } this.fillSquare(ctx, e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop); }, onMouseDown: function(e, ctx, canvas) { canvas.isDrawing = true; this.fillSquare(ctx, e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop); }, onMouseUp: function(e) { canvas.isDrawing = false; }, fillSquare: function(ctx, x, y) { var xPixel = Math.floor(x / this.PIXEL_WIDTH); var yPixel = Math.floor(y / this.PIXEL_WIDTH); // 存储手写输入数据 this.data[((xPixel - 1) * this.TRANSLATED_WIDTH + yPixel) - 1] = 1; ctx.fillStyle = '#ffffff'; ctx.fillRect(xPixel * this.PIXEL_WIDTH, yPixel * this.PIXEL_WIDTH, this.PIXEL_WIDTH, this.PIXEL_WIDTH); }, train: function() { var digitVal = document.getElementById("digit").value; if (!digitVal || this.data.indexOf(1) < 0) { alert("Please type and draw a digit value in order to train the network"); return; } // 将数据加入客户端训练数据集 this.trainArray.push({"y0": this.data, "label": parseInt(digitVal)}); this.trainingRequestCount++; // 将客服端训练数据集发送给服务器端 if (this.trainingRequestCount == this.BATCH_SIZE) { alert("Sending training data to server..."); var json = { trainArray: this.trainArray, train: true }; this.sendData(json); this.trainingRequestCount = 0; this.trainArray = []; } }, // 发送预测请求 test: function() { if (this.data.indexOf(1) < 0) { alert("Please draw a digit in order to test the network"); return; } var json = { image: this.data, predict: true }; this.sendData(json); }, // 处理服务器响应 receiveResponse: function(xmlHttp) { if (xmlHttp.status != 200) { alert("Server returned status " + xmlHttp.status); return; } var responseJSON = JSON.parse(xmlHttp.responseText); if (xmlHttp.responseText && responseJSON.type == "test") { alert("The neural network predicts you wrote a \'" + responseJSON.result + '\''); } }, onError: function(e) { alert("Error occurred while connecting to server: " + e.target.statusText); }, sendData: function(json) { var xmlHttp = new XMLHttpRequest(); xmlHttp.open('POST', this.HOST + ":" + this.PORT, false); xmlHttp.onload = function() { this.receiveResponse(xmlHttp); }.bind(this); xmlHttp.onerror = function() { this.onError(xmlHttp) }.bind(this); var msg = JSON.stringify(json); xmlHttp.setRequestHeader('Content-length', msg.length); xmlHttp.setRequestHeader("Connection", "close"); xmlHttp.send(msg); }}
效果如下图:
实现服务器端
服务器端由Python
标准库BaseHTTPServer
实现,我们接收从客户端发来的训练或是预测请求,使用POST
报文,由于逻辑简单,方便起见,两种请求就发给同一个URL了,在实际生产中还是分开比较好。
完整代码如下:
# -*- coding: UTF-8 -*-import BaseHTTPServerimport jsonfrom ocr import OCRNeuralNetworkimport numpy as npimport random#服务器端配置HOST_NAME = 'localhost'PORT_NUMBER = 9000#这个值是通过运行神经网络设计脚本得到的最优值HIDDEN_NODE_COUNT = 15# 加载数据集data_matrix = np.loadtxt(open('data.csv', 'rb'), delimiter = ',')data_labels = np.loadtxt(open('dataLabels.csv', 'rb'))# 转换成list类型data_matrix = data_matrix.tolist()data_labels = data_labels.tolist()# 数据集一共5000个数据,train_indice存储用来训练的数据的序号train_indice = range(5000)# 打乱训练顺序random.shuffle(train_indice)nn = OCRNeuralNetwork(HIDDEN_NODE_COUNT, data_matrix, data_labels, train_indice);class JSONHandler(BaseHTTPServer.BaseHTTPRequestHandler): """处理接收到的POST请求""" def do_POST(self): response_code = 200 response = "" var_len = int(self.headers.get('Content-Length')) content = self.rfile.read(var_len); payload = json.loads(content); # 如果是训练请求,训练然后保存训练完的神经网络 if payload.get('train'): nn.train(payload['trainArray']) nn.save() # 如果是预测请求,返回预测值 elif payload.get('predict'): try: print nn.predict(data_matrix[0]) response = {"type":"test", "result":str(nn.predict(payload['image']))} except: response_code = 500 else: response_code = 400 self.send_response(response_code) self.send_header("Content-type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() if response: self.wfile.write(json.dumps(response)) returnif __name__ == '__main__': server_class = BaseHTTPServer.HTTPServer; httpd = server_class((HOST_NAME, PORT_NUMBER), JSONHandler) try: #启动服务器 httpd.serve_forever() except KeyboardInterrupt: pass else: print "Unexpected server exception occurred." finally: httpd.server_close()
实现神经网络
如之前所说,我们使用反向传播算法(Backpropagation)来训练神经网络,算法背后的原理推导推荐阅读这篇博文:反向传播神经网络极简入门
算法主要分为三个步骤:
第一步:初始化神经网络
一般将所有权值与偏置量置为(-1,1)范围内的随机数,在我们这个例子中,使用(-0.06,0.06)这个范围,输入层到隐藏层的权值存储在矩阵theta1
中,偏置量存在input_layer_bias
中,隐藏层到输出层则分别存在theta2
与hidden_layer_bias
中。
创建随机矩阵的代码如下,注意输出的矩阵是以size_out
为行,size_in
为列。可能你会想为什么不是size_in
在左边。你可以这么想,一般都是待处理的输入放在右边,处理操作(矩阵)放在左边。
def _rand_initialize_weights(self, size_in, size_out): return [((x * 0.12) - 0.06) for x in np.random.rand(size_out, size_in)]
初始化权值矩阵与偏置向量:
self.theta1 = self._rand_initialize_weights(400, num_hidden_nodes)self.theta2 = self._rand_initialize_weights(num_hidden_nodes, 10)self.input_layer_bias = self._rand_initialize_weights(1, num_hidden_nodes)self.hidden_layer_bias = self._rand_initialize_weights(1, 10)
这里说明一下会用到的每一个矩阵/向量及其形状:
第二步:前向传播
前向传播就是输入数据通过一层一层计算到达输出层得到输出结果,输出层会有10个节点分别代表0~9,哪一个节点的输出值最大就作为我们的预测结果。还记得前面说的激发函数吗?一般用sigmoid
函数作为激发函数。
# sigmoid激发函数def _sigmoid_scalar(