轉載實驗樓: https://www.shiyanlou.com/courses/593/labs/1966/document
項目代碼: https://github.com/dahu1/ocr
先試試手感: http://115.159.157.136:3000/ocr.html
一、實驗介紹
1.1 課程來源
本課程核心部分來自《500 lines or less》項目,作者是來自 Mozilla 的工程師 Marina Samuel,這是她的個人主頁:http://www.marinasamuel.com/ 。項目代碼使用 MIT 協議,項目文檔使用 http://creativecommons.org/licenses/by/3.0/legalcode 協議。
課程內容在原文檔基礎上做了稍許修改,增加了部分原理介紹,步驟的拆解分析及源代碼註釋。
1.2 實驗內容
本課程最終將基於BP神經網絡實現一個手寫字符識別系統,系統會在服務器啓動時自動讀入訓練好的神經網絡文件,如果文件不存在,則讀入數據集開始訓練,用戶可以通過在html
頁面上手寫數字發送給服務器來得到識別結果。
1.3 實驗知識點
本課程項目完成過程中,我們將學習:
- 什麼是神經網絡
- 在客戶端(瀏覽器)完成手寫數據的輸入與請求的發送
- 在服務器端根據請求調用神經網絡模塊並給出響應
- 實現BP神經網絡
1.4 實驗環境
- python2.7
- Xfce終端
- Numpy, Sklearn, Scipy 模塊(請確認電腦上安裝有這些模塊,如果沒有請在命令行中
sudo pip
安裝相應的模塊)
1.5 適合人羣
本課程難度爲中等,屬於中級級別課程,適合具有Python基礎的用戶,並且對機器學習有一定了解。
二、實驗原理
人工智能
圖靈對於人工智能的定義大家都已耳熟能詳,但"是什麼構成了智能"至今仍是一個帶有爭論的話題。計算機科學家們目前將人工智能分成了多個分支,每一個分支都專注於解決一個特定的問題領域,舉其中三個有代表性的分支:
- 基於預定義知識的邏輯與概率推理,比如模糊推理能夠幫助一個恆溫器根據監測到的溫度和溼度決定什麼時候開關空調。
- 啓發式搜索,比如在棋類遊戲中搜索到走下一子的最優解。
- 機器學習,比如手寫字符識別系統。
簡單來說,機器學習的目的就是通過大量數據訓練一個能夠識別一種或多種模式的系統。訓練系統用的數據集合被稱作訓練集,如果訓練集的每個數據條目都打上目標輸出值(也就是標籤),則該方法稱作監督學習,不打標籤的則是非監督學習。機器學習中有多種算法能夠實現手寫字符識別系統,在本課程中我們將基於神經網絡實現該系統。
什麼是神經網絡
神經網絡由能夠互相通信的節點構成,赫布理論解釋了人體的神經網絡是如何通過改變自身的結構和神經連接的強度來記憶某種模式的。而人工智能中的神經網絡與此類似。請看下圖,最左一列藍色節點是輸入節點,最右列節點是輸出節點,中間節點是隱藏節點。該圖結構是分層的,隱藏的部分有時候也會分爲多個隱藏層。如果使用的層數非常多就會變成我們平常說的深度學習了。
每一層(除了輸入層)的節點由前一層的節點加權加相加加偏置向量並經過激活函數得到,公式如下:
其中f
是激活函數,b
是偏置向量,它們的作用會在之後說明。
這一類拓撲結構的神經網絡稱作前饋神經網絡,因爲該結構中不存在迴路。有輸出反饋給輸入的神經網絡稱作遞歸神經網絡(RNN)。在本課程中我們使用前饋神經網絡中經典的BP神經網絡來實現手寫識別系統。
如何使用神經網絡
很簡單,神經網絡屬於監督學習,那麼多半就三件事,決定模型參數,通過數據集訓練學習,訓練好後就能到分類工具/識別系統用了。數據集可以分爲2部分(訓練集,驗證集),也可以分爲3部分(訓練集,驗證集,測試集),訓練集可以看作平時做的習題集(可反覆做),系統通過對比習題集的正確答案和自己的解答來不斷學習改良自己。測試集可以看作是高考,同一份試卷只能考一次,測試集一般不會透露答案。那麼驗證集是什麼呢?好比多個學生(類比用不同策略訓練出的多個神經網絡)要參加一個名額只有兩三人的比賽,那麼就得給他們一套他們沒做過的卷子(驗證集)來逐出成績最好的幾個人,有時也使用驗證集決定模型參數。在本課程中數據集只劃分訓練集和驗證集。
系統構成
我們的OCR系統分爲5部分,分別寫在5個文件中:
- 客戶端(
ocr.js
) - 服務器(
server.py
) - 用戶接口(
ocr.html
) - 神經網絡(
ocr.py
) - 神經網絡設計腳本(
neural_network_design.py
)
用戶接口(ocr.html
)是一個html
頁面,用戶在canvas
上寫數字,之後點擊選擇訓練或是預測。客戶端(ocr.js
)將收集到的手寫數字組合成一個數組發送給服務器端(server.py
)處理,服務器調用神經網絡模塊(ocr.py
),它會在初始化時通過已有的數據集訓練一個神經網絡,神經網絡的信息會被保存在文件中,等之後再一次啓動時使用。最後,神經網絡設計腳本(neural_network_design.py
)是用來測試不同隱藏節點數下的性能,決定隱藏節點數用的。
三、開發準備
打開終端,進入 Code
目錄,創建 ocr
文件夾,
並將其作爲我們的工作目錄。
$ cd Code
$ mkdir ocr && cd ocr
四、項目文件結構
五、實驗步驟
我們將根據系統構成的五部分一一實現,在講解完每一部分的核心代碼後給出完整的文件代碼。
實現用戶接口
需要給予用戶輸入數據、預測、訓練的接口,這部分較簡單,所以直接給出完整代碼。在 ocr.html
中寫入如下代碼。
<!-- 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 BaseHTTPServer
import json
from ocr import OCRNeuralNetwork
import numpy as np
import 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))
return
if __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)
這裏說明一下會用到的每一個矩陣/向量及其形狀:
變量名 | 描述 | 形狀 |
---|---|---|
y0 | 輸入層 | 1 * 400 |
theta1 | 輸入-隱藏層權值矩陣 | 隱藏層節點數 * 400 |
input_layer_bias | 輸入-隱藏層偏置向量 | 隱藏層節點數 * 1 |
y1 | 隱藏層 | 隱藏層節點數 * 1 |
theta2 | 隱藏-輸出層權值矩陣 | 10 * 隱藏層節點數 |
hidden_layer_bias | 隱藏-輸出層偏置向量 | 10 * 1 |
y2 | 輸出層 | 10 * 1 |
第二步:前向傳播
前向傳播就是輸入數據通過一層一層計算到達輸出層得到輸出結果,輸出層會有10個節點分別代表0~9,哪一個節點的輸出值最大就作爲我們的預測結果。還記得前面說的激發函數嗎?一般用sigmoid
函數作爲激發函數。
# sigmoid激發函數
def _sigmoid_scalar(self, z):
return 1 / (1 + math.e ** -z)
它長這樣:
可以將實數範圍的數字映射到(0, 1),S型的形狀也很理想,最關鍵是導數可直接得到。反向傳播神經網絡極簡入門裏有更具體的說明。
使用numpy
的vectorize
能得到標量函數的向量化版本,這樣就能直接處理向量了:
self.sigmoid = np.vectorize(self._sigmoid_scalar)
前向傳播的代碼:
y1 = np.dot(np.mat(self.theta1), np.mat(data['y0']).T)
sum1 = y1 + np.mat(self.input_layer_bias)
y1 = self.sigmoid(sum1)
y2 = np.dot(np.array(self.theta2), y1)
y2 = np.add(y2, self.hidden_layer_bias)
y2 = self.sigmoid(y2)
第三步:反向傳播
第三步是訓練的關鍵,它需要通過計算誤差率然後系統根據誤差改變網絡的權值矩陣和偏置向量。通過訓練數據的標籤我們得到actual_vals
用來和輸出層相減得到誤差率output_errors
,輸出層的誤差只能用來改進上一層,想要改進上上一層就需要計算上一層的輸出誤差,公式原理還是請看反向傳播神經網絡極簡入門。
actual_vals = [0] * 10
actual_vals[data['label']] = 1
output_errors = np.mat(actual_vals).T - np.mat(y2)
hidden_errors = np.multiply(np.dot(np.mat(self.theta2).T, output_errors),
self.sigmoid_prime(sum1))
其中sigmoid_prime
的作用就是先sigmoid
再求導數。
更新權重矩陣與偏執向量:
self.theta1 += self.LEARNING_RATE * np.dot(np.mat(hidden_errors),
np.mat(data['y0']))
self.theta2 += self.LEARNING_RATE * np.dot(np.mat(output_errors),
np.mat(y1).T)
self.hidden_layer_bias += self.LEARNING_RATE * output_errors
self.input_layer_bias += self.LEARNING_RATE * hidden_errors
LEARNING_RATE
是學習步進,這裏我們設置成0.1
,步子大雖然學得快,但也容易扭到,步子小得到的結果會更精準。
預測的代碼就相當於前向傳播:
def predict(self, test):
y1 = np.dot(np.mat(self.theta1), np.mat(test).T)
y1 = y1 + np.mat(self.input_layer_bias) # Add the bias
y1 = self.sigmoid(y1)
y2 = np.dot(np.array(self.theta2), y1)
y2 = np.add(y2, self.hidden_layer_bias) # Add the bias
y2 = self.sigmoid(y2)
results = y2.T.tolist()[0]
return results.index(max(results))
ocr.py
的完整代碼如下:
# -*- coding: UTF-8 -*-
import csv
import numpy as np
from numpy import matrix
from math import pow
from collections import namedtuple
import math
import random
import os
import json
class OCRNeuralNetwork:
LEARNING_RATE = 0.1
WIDTH_IN_PIXELS = 20
# 保存神經網絡的文件路徑
NN_FILE_PATH = 'nn.json'
def __init__(self, num_hidden_nodes, data_matrix, data_labels, training_indices, use_file=True):
# sigmoid函數
self.sigmoid = np.vectorize(self._sigmoid_scalar)
# sigmoid求導函數
self.sigmoid_prime = np.vectorize(self._sigmoid_prime_scalar)
# 決定了要不要導入nn.json
self._use_file = use_file
# 數據集
self.data_matrix = data_matrix
self.data_labels = data_labels
if (not os.path.isfile(OCRNeuralNetwork.NN_FILE_PATH) or not use_file):
# 初始化神經網絡
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)
# 訓練並保存
TrainData = namedtuple('TrainData', ['y0', 'label'])
self.train([TrainData(self.data_matrix[i], int(self.data_labels[i])) for i in training_indices])
self.save()
else:
# 如果nn.json存在則加載
self._load()
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)]
def _sigmoid_scalar(self, z):
return 1 / (1 + math.e ** -z)
def _sigmoid_prime_scalar(self, z):
return self.sigmoid(z) * (1 - self.sigmoid(z))
def train(self, training_data_array):
for data in training_data_array:
# 前向傳播得到結果向量
y1 = np.dot(np.mat(self.theta1), np.mat(data.y0).T)
sum1 = y1 + np.mat(self.input_layer_bias)
y1 = self.sigmoid(sum1)
y2 = np.dot(np.array(self.theta2), y1)
y2 = np.add(y2, self.hidden_layer_bias)
y2 = self.sigmoid(y2)
# 後向傳播得到誤差向量
actual_vals = [0] * 10
actual_vals[data.label] = 1
output_errors = np.mat(actual_vals).T - np.mat(y2)
hidden_errors = np.multiply(np.dot(np.mat(self.theta2).T, output_errors), self.sigmoid_prime(sum1))
# 更新權重矩陣與偏置向量
self.theta1 += self.LEARNING_RATE * np.dot(np.mat(hidden_errors), np.mat(data.y0))
self.theta2 += self.LEARNING_RATE * np.dot(np.mat(output_errors), np.mat(y1).T)
self.hidden_layer_bias += self.LEARNING_RATE * output_errors
self.input_layer_bias += self.LEARNING_RATE * hidden_errors
def predict(self, test):
y1 = np.dot(np.mat(self.theta1), np.mat(test).T)
y1 = y1 + np.mat(self.input_layer_bias) # Add the bias
y1 = self.sigmoid(y1)
y2 = np.dot(np.array(self.theta2), y1)
y2 = np.add(y2, self.hidden_layer_bias) # Add the bias
y2 = self.sigmoid(y2)
results = y2.T.tolist()[0]
return results.index(max(results))
def save(self):
if not self._use_file:
return
json_neural_network = {
"theta1":[np_mat.tolist()[0] for np_mat in self.theta1],
"theta2":[np_mat.tolist()[0] for np_mat in self.theta2],
"b1":self.input_layer_bias[0].tolist()[0],
"b2":self.hidden_layer_bias[0].tolist()[0]
};
with open(OCRNeuralNetwork.NN_FILE_PATH,'w') as nnFile:
json.dump(json_neural_network, nnFile)
def _load(self):
if not self._use_file:
return
with open(OCRNeuralNetwork.NN_FILE_PATH) as nnFile:
nn = json.load(nnFile)
self.theta1 = [np.array(li) for li in nn['theta1']]
self.theta2 = [np.array(li) for li in nn['theta2']]
self.input_layer_bias = [np.array(nn['b1'][0])]
self.hidden_layer_bias = [np.array(nn['b2'][0])]
實現神經網絡設計腳本
神經網絡設計腳本的功能就是決定神經網絡使用的隱藏節點的數量,這裏我們從5個節點開始增長,每次增加5個,到50個爲止,打印性能進行比較,neural_network_design.py
完整代碼如下:
# -*- coding: UTF-8 -*-
import numpy as np
from ocr import OCRNeuralNetwork
from sklearn.cross_validation import train_test_split
def test(data_matrix, data_labels, test_indices, nn):
correct_guess_count = 0
for i in test_indices:
test = data_matrix[i]
prediction = nn.predict(test)
if data_labels[i] == prediction:
correct_guess_count += 1
return correct_guess_count / float(len(test_indices))
data_matrix = np.loadtxt(open('data.csv', 'rb'), delimiter = ',').tolist()
data_labels = np.loadtxt(open('dataLabels.csv', 'rb')).tolist()
# Create training and testing sets.
train_indices, test_indices = train_test_split(list(range(5000)))
print "PERFORMANCE"
print "-----------"
for i in xrange(5, 50, 5):
nn = OCRNeuralNetwork(i, data_matrix, data_labels, train_indices, False)
performance = str(test(data_matrix, data_labels, test_indices, nn))
print "{i} Hidden Nodes: {val}".format(i=i, val=performance)
下載數據集
wget http://labfile.oss.aliyuncs.com/courses/593/data.csv
wget http://labfile.oss.aliyuncs.com/courses/593/dataLabels.csv
運行腳本查看結果(注意每次初始化時的參數是隨機的,訓練順序也是隨機的,所以每個人的訓練結果應該是不一樣的):
PERFORMANCE
-----------
5 Hidden Nodes: 0.7792
10 Hidden Nodes: 0.8704
15 Hidden Nodes: 0.8808
20 Hidden Nodes: 0.8864
25 Hidden Nodes: 0.8808
30 Hidden Nodes: 0.888
35 Hidden Nodes: 0.8904
40 Hidden Nodes: 0.8896
45 Hidden Nodes: 0.8928
通過輸出我們判斷15個隱藏節點可能是最優的。從10到15增加了1%的精確度,之後需要再增加20個節點纔能有如此的增長,但同時也會大大地增加了計算量,因此15個節點性價比最高。當然不追求性價比電腦性能也夠用的話還是選擇準確度最高的節點數爲好。
五、實驗結果
輸入python server.py
打開服務器。在頁面上寫一個數字預測看看:
六、實驗總結
在本課中我們基於BP神經網絡實現了一個簡單的手寫字符識別系統。雖然它只能識別數字,雖然性能也非常一般,但它是一個起點,從這裏出發可以看到更多有趣的事物和好玩的花樣,高級一點比如AlphaGo,更接近生活一點比如商品識別,比如語言翻譯,比如訓練一個打馬里奧系列遊戲很厲害的AI(MarI/O),比如把一張初音未來的圖放大幾倍仍然清晰(waifu2x),比如預測下一年的考研題型(等待有人填補這個空缺)。