摘要

 
 

​ 在如今大数据与互联网蓬勃发展的年代,人工智能技术被推到了风口浪尖。

​ 但人工智能并不仅仅是深度学习,深度学习只是人工智能的一种。随着这门技术的发展,很多深度学习的库如春笋般涌现。知名的有tensorflowpytorchkera等。但使用了这些库了之后就无法关注到深度学习的底层逻辑。也有进程占用内存巨大,无法便捷地利用python爬虫等诸多缺点。本项目通过训练EMNIST数据集提供的位图来达到识别手写字母的效果。仅仅使用到了numpy库,更加轻巧,可以嵌入与于服务器,手机等终端设备中。

研究背景

​ 本文受到了斋藤康毅所著 《深度学习入门:基于Python的理论与实现》一书的影响。想要做一个可以识别所有的英文字母和数字的系统。

研究过程

选择数据集

​ 在深度学习中,一个好的数据集是成功的关键。起初,我们想使用The Chars74K dataset来进行深度学习,但这个数据集数量不够,而且图片形状大小不一。

​ 后来我们使用了EMNIST数据集(Extended MNIST),EMNIST数据集有很多优点,第一是它所有的数据都大小相同,方便操作;且都是用灰度值表示的,方便学习。而且数据量庞大,方便训练和测试。

EMNIST数据集分类方式:
  • By_Class : 共 814255 张,62 类,与 NIST 相比重新划分类训练集与测试机的图片数
  • By_Merge: 共 814255 张,47 类, 与 NIST 相比重新划分类训练集与测试机的图片数
  • Balanced : 共 131600 张,47 类, 每一类都包含了相同的数据,每一类训练集 2400 张,测试集 400 张
  • Digits :共 28000 张,10 类,每一类包含相同数量数据,每一类训练集 24000 张,测试集 4000 张
  • Letters : 共 145600 张,26 类,每一类包含相同数据,每一类训练集5600 张,测试集 800 张
  • MNIST : 共 70000 张,10 类,每一类包含相同数量数据(注:这里虽然数目和分类都一样,但是图片的处理方式不一样,EMNIST 的 MNIST 子集数字占的比重更大)

该数据集已在github上开源,当然,gitee上也有相应的clone版本。
 
 
 
 

处理数据

打开gzip文件

EMNIST数据集下载完成后是gzip格式的,我们使用了python自带的gzip库进行处理:

import gzip # 导入gzip库
# 读取数据的函数
def _load_img(file_name):
    file_path = dataset_dir + "/" + file_name

    print("Converting " + file_name + " to NumPy Array ...")
    # 使用gzip打开gzip文件
    with gzip.open(file_path, 'rb') as f:
        data = np.frombuffer(f.read(), np.uint8, offset=16)
    data = data.reshape(-1, img_size)
    print("Done")

    return data

 
 
 
 

为数据做标签

为了方便数据的读取,可以把图片贴上标签:

def _label(file_name):
    file_path = dataset_dir + "/" + file_name

    print("Converting " + file_name + " to NumPy Array ...")
    with gzip.open(file_path, 'rb') as f:
        labels = np.frombuffer(f.read(), np.uint8, offset=8)
    print("Done")

    return labels

 
 
 
 

转变成one-hot-label模式

# 转变成one-hot-label模式,便于计算损失。
# 比如:8--->[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
def _change_one_hot_label(X):
    T = np.zeros((X.size, 47))# 一共47种,47个输出
    for idx, row in enumerate(T):
        row[X[idx]] = 1

    return T

 
 
 
 

创建pkl文件

pkl文件是python用来保存数据时使用的后缀名。这里我们用它来保存权重数据。

先要import一下pickle库:

import pickle

定义init_emnist函数:

def init_emnist():
    dataset = _convert_numpy()
    print("Creating pickle file ...")
    with open(save_file, 'wb') as f:
        pickle.dump(dataset, f, -1)
    print("Done!")

 
 
 
 

load_emnist主函数

# 读入EMNIST数据集
def load_emnist(normalize=True, flatten=True, one_hot_label=False):

    # 创建好一个pkl了,以后就不用创建了
    if not os.path.exists(save_file):
        init_emnist()

    with open(save_file, 'rb') as f:
        dataset = pickle.load(f)

    # normalize : 将图像的像素值正规化为0.0~1.0
    if normalize:
        for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].astype(np.float32)
            dataset[key] /= 255.0

    # one_hot_label为True的情况下,标签作为one-hot数组返回
    if one_hot_label:
        dataset['train_label'] = _change_one_hot_label(dataset['train_label'])
        dataset['test_label'] = _change_one_hot_label(dataset['test_label'])

    # 判断是否将图像展开为一维数组(flatten) 
    if not flatten:
        for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].reshape(-1, 1, 28, 28)

    # 返回数据:(训练图像, 训练标签), (测试图像, 测试标签)
    return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label'])

 
 
 
 

初始化

这里需要用到os库

import os

这里定义了by_what_type变量,key_file数组,save_filedataset_dir都是用来储存路径变量的。

by_what_type = "bymerge"    # 就是我们用的分类,如果要用别的可以改成"byclass"等

key_file = {
    'train_img': 'emnist-' + by_what_type + '-train-images-idx3-ubyte.gz',
    'train_label': 'emnist-' + by_what_type + '-train-labels-idx1-ubyte.gz',
    'test_img': 'emnist-' + by_what_type + '-test-images-idx3-ubyte.gz',
    'test_label': 'emnist-' + by_what_type + '-test-labels-idx1-ubyte.gz'
}

dataset_dir = os.path.dirname(os.path.abspath(__file__))
save_file = dataset_dir + "/emnist-" + by_what_type + ".pkl"

train_num = 60000
test_num = 10000
img_dim = (1, 28, 28)   
img_size = 784

 
 
 
 

训练数据

 
 

函数的选择

 
 

隐藏层使用的激活函数

我们选择的是sigmoid函数,这个函数在深度学习中很常用。

数学表达式如下:

h(x) = \frac{1}{1 + e^{-x}}(e\approx2.718)

函数图像大概是这样的:

sigmoid函数的实现可以使用numpy库,也可以直接使用2.718这个数字来代替常数e。

先导入numpy库:

import numpy as np

函数实现:

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

但我们使用不是上面的实现,而是效率更快的反向传播求导:

def sigmoid_grad(x):
    return (1.0 - sigmoid(x)) * sigmoid(x)

 
 
 
 

输出层所用的激活函数

输出层的激活函数我们使用的是softmax函数,

输出层所用的激活函数,要根据求解问题的性质决定。一般地,回
归问题可以使用恒等函数,二元分类问题可以使用sigmoid 函数,
多元分类问题可以使用softmax 函数。

我们的项目是手写字母识别系统,属于多元分类问题,所以使用softmax函数是比较恰当的。

其数学表达式如下:

y_k = \frac{e^{a_k}}{\sum\limits_{i=1}^{n}e^{a_i}}(e\approx2.718)

函数实现:

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T

    x = x - np.max(x)  # 溢出对策
    return np.exp(x) / np.sum(np.exp(x))

 
 
 
 

损失函数

损失函数我们使用的是交叉熵误差函数(cross entropy error)。

其数学表达式如下:

E = - \sum\limits_{k}^{}t_k\log_ey_k

tk即正确解的标签的值。我们在处理数据时,将数据转变为了one-hot-label模式。也就是说,除了正确解标签之外,其他的标签均为0。所以,交叉熵误差的值是由正确解标签所对应的输出结果决定的。

以下是交叉熵误差函数的实现:

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
    if t.size == y.size:
        t = t.argmax(axis=1)

    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

 
 
 
 

神经网络的实现

这里是整个项目的核心代码,分为初始化前向处理计算损失计算识别精度计算权重参数的梯度五个部分。接下来会逐一介绍。
 
 
 
 

初始化

初始化的数据全部储存在了一个变量params里。这个变量本质上是一个字典。

params有四个键,分别为:1,b1,W2,b2。

W1的值对是是一个Numpy数组,储存着第一层的权重参数。第一层也就是隐藏层,输入层是没有权重和偏置的。b1以Numpy数组的形式储存着第一层的所有偏置。

同理,W2就是第二层的权重参数,b1就是第二层的偏置。注:第二层就是输出层。

我们在确定params变量的初始值时,使用了Numpy。

具体代码如下:

np.random.randn(input_size, hidden_size)

np.random.randn()函数返回具有标准正态分布的一组随机数据。

比如:

>>> import numpy as np
np.random.rand(101,1) # 返回一组长度为101的一维随机数组
array([[0.09121784],
       [0.16222607],
       [0.95502866],
       [0.35076647],
            ……
       [0.88607282],
       [0.11430214],
       [0.66273171],
       [0.28294524],
       [0.88311454],
       [0.56603641]])
>>> 

为了更直观地表示,我将这组数据坐标化,再用曲线链接,生成如下图形:

可以看出,Numpy返回的数据是随机的,而且在0,1之间。

由此可以得到权重和偏置,再通过后期学习提高精确度。

这里是具体的代码:

def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
   # 初始化权重
   self.params = {}
   self.params['W1'] = weight_init_std * \
       np.random.randn(input_size, hidden_size)
   self.params['b1'] = np.zeros(hidden_size)
   self.params['W2'] = weight_init_std * \
       np.random.randn(hidden_size, output_size)
   self.params['b2'] = np.zeros(output_size)

 
 
 
 

前向处理

神经网络层与层之间的传递是通过矩阵来实现的。

我们通过矩阵的点乘来实现输入层乘上权重的运算。

代码如下:

def predict(self, x):
    W1, W2 = self.params['W1'], self.params['W2']
    b1, b2 = self.params['b1'], self.params['b2']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    y = softmax(a2)

    return y

其中(W_1)是一个长784,宽100的矩阵,而(W_2)是一个长100,宽47的矩阵。

具体可以通过以下代码获知:

print(self.params['W1'].shape)

 
 
 
 

计算损失

这其实只需要调用前面的交叉商误差函数即可计算损失。

def loss(self, x, t):
    y = self.predict(x)

    return cross_entropy_error(y, t)

 
 
 
 

计算识别精度

计算精度可以让我们非常直观地看到学习的效果。

def accuracy(self, x, t):
    y = self.predict(x)
    y = np.argmax(y, axis=1)
    t = np.argmax(t, axis=1)

    accuracy = np.sum(y == t) / float(x.shape[0])
    return accuracy

Numpy的argmax函数的功能就是返回数组中最大值的索引值。axis参数是对该数组进行降维操作。我们的训练数据有697932个,因此y就是一个长697932,宽47的二维数组。asix=1就是将该数组转变为一个长697932的一维数组。float(x.shape[0])就是x的长度。

 
 
 
 

计算权重参数的梯度
def gradient(self, x, t):
    W1, W2 = self.params['W1'], self.params['W2']
    b1, b2 = self.params['b1'], self.params['b2']
    grads = {}

    batch_num = x.shape[0]

    # 前向处理
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    y = softmax(a2)

    # 反向传播
    dy = (y - t) / batch_num
    grads['W2'] = np.dot(z1.T, dy)
    grads['b2'] = np.sum(dy, axis=0)

    da1 = np.dot(dy, W2.T)
    dz1 = sigmoid_grad(a1) * da1
    grads['W1'] = np.dot(x.T, dz1)
    grads['b1'] = np.sum(dz1, axis=0)

    return grads

计算梯度分为前向传播和反向传播两个部分。

前向传播的原理和上文predict方法实现基本相同。

反向传播(英语:,缩写为BP)是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法。该方法对网络中所有权重计算损失函数的梯度。这个梯度会反馈给最优化方法,用来更新权值以最小化损失函数。

材料来自维基百科

反向传播其实就是更新权重的过程。在三层神经网络中,先计算 (\delta W_1) 对于所有隐藏层到输出层的权值和 (\delta W_2) 对于所有输入层到隐藏层的权值。再更新网络权值直到所有样本正确分类或满足其他停止标准。

至此,二层神经网络的类中的方法以介绍完毕。接下来是主函数。

 
 
 
 

主函数

主函数就是程序执行的部分,分为 读入数据初始化更新参数绘制图形 四个部分。
 
 
 
 

读入数据

读入数据的过程只要调用load_emnist函数即可。

(x_train, t_train), (x_test, t_test) = load_emnist(
    normalize = True, one_hot_label = True) # 启用正规化和one_hot_label模式

 
 
 
 

初始化
network = TwoLayerNet(784, 100, 47) #设定输入层,隐藏层和输出层的大小

iters_num = 10000  # 适当设定循环的次数
train_size = x_train.shape[0] # 训练数据697932
batch_size = 1000 # 1000个为一组
learning_rate = 1 # 学习率(这里是1)

train_loss_list = [] # 训练的损失
train_acc_list = [] # 训练准确率
test_acc_list = [] # 测试准确率

iter_per_epoch = 680 # 定义每次epoch的大小

这里初始化了各个变量。其中iters_numtrain_sizebatch_sizelearning_rate四个变量是我们后期主要修改的地方。

 
 
 
 

更新参数
for i in range(iters_num):
    # 随机抽取训练数据和测试数据
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    # 计算梯度
    grad = network.gradient(x_batch, t_batch)
    # 更新参数
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
    # 计算损失
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    # 每次epoch时计算精确度
    if i % iter_per_epoch == 0:
        # 计算训练精确度
        train_acc = network.accuracy(x_train, t_train)
        train_acc_list.append(train_acc)
        # 计算测试精确度
        test_acc = network.accuracy(x_test, t_test)
        test_acc_list.append(test_acc)
        # 在控制台输出精确度的大小
        print("train acc, test acc | " +
              str(train_acc) + ", " + str(test_acc))

 
 
 
 

绘制图形

绘制图形我们使用的是matplotlib库。

先import:

# import numpy as np # matplotlib高度依赖Numpy
import matplotlib.pyplot as plt

绘制精确度随epoch变化的曲线:

x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

研究过程中遇到的问题

数据被解压,如何恢复为gzip格式

在Linux和Mac系统中可以使用gzip命令解决:

 gzip ./emnist-bymerge-train-images-idx3-ubyte

显示出指定的图像问题

首先,我们找到了emnist-bymerge-mapping.txt文件,里面的内容大致如下:

编号 ASCII
0 48
1 49
2 50
3 51
4 52
5 53
6 54
7 55
8 56
9 57
10 65
11 66
12 67
13 68
14 69
15 70
16 71
17 72
18 73
19 74
20 75
21 76
22 77
23 78
24 79
25 80
26 81
27 82
28 83
29 84
30 85
31 86
32 87
33 88
34 89
35 90
36 97
37 98
38 100
39 101
40 102
41 103
42 104
43 110
44 113
45 114
46 116

可以看出,这列表列出了数字和字母所对应的ASCII码值。

所以,我们可以定义一个字典:

d = {0: 48, 1: 49, 2: 50, 3: 51, 4: 52, 5: 53, 6: 54, 7: 55, 8: 56, 9: 57, 10: 65, 11: 66, 12: 67, 13: 68, 14: 69, 15: 70, 16: 71, 17: 72, 18: 73, 19: 74, 20: 75, 21: 76, 22: 77, 23: 78, 24: 79, 25: 80, 26: 81, 27: 82, 28: 83, 29: 84, 30: 85, 31: 86, 32: 87, 33: 88, 34: 89, 35: 90, 36: 97, 37: 98, 38: 100, 39: 101, 40: 102, 41: 103, 42: 104, 43: 110, 44: 113, 45: 114, 46: 116}

调用权重数值:

with open("weight.pkl", 'rb') as f:
    print("loading files...")
    network = pickle.load(f)

计算并找出最大概率的结果

index = 5

(x_train1, t_train1), (_, _) = load_emnist(normalize=False)
(x_train2, t_train2), (_, _) = load_emnist(one_hot_label=True)

result = list(network.predict(x_train2[index]))
largest_result = np.argmax(result)  # 最大的结果

把神经网络计算出来的结果和答案输出

img = x_train1[index]
label = t_train1[index]
print("神经网络输出结果为:", chr(d[largest_result]))
print("答案为:", chr(d[label]))

img = img.reshape(28, 28)  # 把图像的形状变为原来的尺寸
img_show(img)

但是我们看到的却是:

revolved-picture

很显然,这个图片被旋转了。而且不仅仅是旋转,还翻折了90度。

我们是这么解决的:

# 将图片翻转回来
def img_show(img):
    pil_img = Image.fromarray(np.uint8(img)).transpose(
        Image.FLIP_LEFT_RIGHT).rotate(90)
    pil_img.show()

这样,我们就可以很直观地感受到它的正确率了。

 
 
 
 
 
 

成果描述

目前,我们已做到精确度为85%左右,但是离“智能”还差了很多。人类的识别率可以达到98%以上,而人工智能的目的就是模拟人类的大脑。所以路还很长,我们还需要努力优化。

有待解决的问题

有待解决的问题已经很明显了。

  • 精确度的问题
  • 移动端,网页端的嵌入问题
  • 识别图像的清晰度问题
  • 识别图像的灰度问题

 
 

参考文献

 
 

  1. 《深度学习入门·基于python的理论与实现》日·斋藤康毅 著 陆宇杰 译 2018.7版

  2. 斯坦福2017季CS231n深度视觉识别课程视频(by Fei-Fei Li, Justin Johnson, Serena Yeung)

  3. Emnist | Teansorflow DataSets

  4. The Emnist DataSet | NIST

代码和工具

  • Python 3.7.4
  • Clang 4.0.1
  • Sublime Text
  • 我们的代码已经push到了gitee,代码地址

 
 

本文使用markdown标记语言编写,已上传至个人博客。源代码可以到gitee中查看。

 
 
 
 
 
 


头像

huzongyu

本人擅长Ai、Fw、Fl、Br、Ae、Pr、Id、Ps等软件的安装与卸载; 精通CSS、JavaScript、PHP、ASP、C、C++、C#、Java、Ruby、Perl、Lisp、python、Objective-C、ActionScript、Pascal等单词的拼写; 熟悉Windows、Linux、Mac、Android、IOS、WP8等系统的开关机。

0 条评论

发表评论

邮箱地址不会被公开。 必填项已用*标注

error: 禁止转载