神经网络实现手写字符识别系统

一:内容介绍

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
Numpy, Sklearn, Scipy 模块

二、实验原理

2.1 人工智能

图灵对于人工智能的定义大家都已耳熟能详,但”是什么构成了智能”至今仍是一个带有争论的话题。计算机科学家们目前将人工智能分成了多个分支,每一个分支都专注于解决一个特定的问题领域,举其中三个有代表性的分支:

基于预定义知识的逻辑与概率推理,比如模糊推理能够帮助一个恒温器根据监测到的温度和湿度决定什么时候开关空调。
启发式搜索,比如在棋类游戏中搜索到走下一子的最优解。
机器学习,比如手写字符识别系统。
简单来说,机器学习的目的就是通过大量数据训练一个能够识别一种或多种模式的系统。训练系统用的数据集合被称作训练集,如果训练集的每个数据条目都打上目标输出值(也就是标签),则该方法称作监督学习,不打标签的则是非监督学习。机器学习中有多种算法能够实现手写字符识别系统,在本课程中我们将基于神经网络实现该系统。

2.2 什么是神经网络

神经网络由能够互相通信的节点构成,赫布理论解释了人体的神经网络是如何通过改变自身的结构和神经连接的强度来记忆某种模式的。而人工智能中的神经网络与此类似。请看下图,最左一列蓝色节点是输入节点,最右列节点是输出节点,中间节点是隐藏节点。该图结构是分层的,隐藏的部分有时候也会分为多个隐藏层。如果使用的层数非常多就会变成我们平常说的深度学习了。

每一层(除了输入层)的节点由前一层的节点加权加相加加偏置向量并经过激活函数得到,公式如下:

其中f是激活函数,b是偏置向量,它们的作用会在之后说明。

这一类拓扑结构的神经网络称作前馈神经网络,因为该结构中不存在回路。有输出反馈给输入的神经网络称作递归神经网络(RNN)。在本课程中我们使用前馈神经网络中经典的BP神经网络来实现手写识别系统。

2.3 如何使用神经网络

很简单,神经网络属于监督学习,那么多半就三件事,决定模型参数,通过数据集训练学习,训练好后就能到分类工具/识别系统用了。数据集可以分为2部分(训练集,验证集),也可以分为3部分(训练集,验证集,测试集),训练集可以看作平时做的习题集(可反复做),系统通过对比习题集的正确答案和自己的解答来不断学习改良自己。测试集可以看作是高考,同一份试卷只能考一次,测试集一般不会透露答案。那么验证集是什么呢?好比多个学生(类比用不同策略训练出的多个神经网络)要参加一个名额只有两三人的比赛,那么就得给他们一套他们没做过的卷子(验证集)来逐出成绩最好的几个人,有时也使用验证集决定模型参数。在本课程中数据集只划分训练集和验证集。

2.4 系统构成

我们的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)是用来测试不同隐藏节点数下的性能,决定隐藏节点数用的。

三、实验步骤

我们将根据系统构成的五部分一一实现,在讲解完每一部分的核心代码后给出完整的文件代码。

3.1 实现用户接口

需要给予用户输入数据、预测、训练的接口,这部分较简单,所以直接给出完整代码。在 ocr.html 中写入如下代码



<html>
<head>
    <script src="ocr.js">script>
head>
<body onload="ocrDemo.onLoadFunction()">
    <div id="main-container" style="text-align: center;">
        <h1>OCR Demoh1>
        <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文件中实现。

3.2 实现客服端

画布设定了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);
    }
}

效果如下图:

3.3 实现服务器端

服务器端由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()

3.4 实现神经网络

如之前所说,我们使用反向传播算法(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])]

3.5 实现神经网络设计脚本

神经网络设计脚本的功能就是决定神经网络使用的隐藏节点的数量,这里我们从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)

3.6下载数据集:

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打开服务器。在页面上写一个数字预测看看:

五、参考资料&延伸阅读

Optical Character Recognition (OCR)
Optical Character Recognition (OCR) 源代码
反向传播神经网络极简入门
Error Backpropagation

你可能感兴趣的:(神经网络)