摘要
在如今大数据与互联网蓬勃发展的年代,人工智能技术被推到了风口浪尖。
但人工智能并不仅仅是深度学习,深度学习只是人工智能的一种。随着这门技术的发展,很多深度学习的库如春笋般涌现。知名的有tensorflow
,pytorch
,kera
等。但使用了这些库了之后就无法关注到深度学习的底层逻辑。也有进程占用内存巨大,无法便捷地利用python
爬虫等诸多缺点。本项目通过训练EMNIST
数据集提供的位图来达到识别手写字母的效果。仅仅使用到了numpy
库,更加轻巧,可以嵌入与于服务器,手机等终端设备中。
研究背景
本文受到了斋藤康毅所著 《深度学习入门:基于Python的理论与实现》一书的影响。想要做一个可以识别所有的英文字母和数字的系统。
研究过程
选择数据集
在深度学习中,一个好的数据集是成功的关键。起初,我们想使用The Chars74K dataset来进行深度学习,但这个数据集数量不够,而且图片形状大小不一。
后来我们使用了EMNIST
数据集(Extended MNIST),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_file
和dataset_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_num
,train_size
,batch_size
,learning_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)
但是我们看到的却是:
很显然,这个图片被旋转了。而且不仅仅是旋转,还翻折了90度。
我们是这么解决的:
# 将图片翻转回来
def img_show(img):
pil_img = Image.fromarray(np.uint8(img)).transpose(
Image.FLIP_LEFT_RIGHT).rotate(90)
pil_img.show()
这样,我们就可以很直观地感受到它的正确率了。
成果描述
目前,我们已做到精确度为85%左右,但是离“智能”还差了很多。人类的识别率可以达到98%以上,而人工智能的目的就是模拟人类的大脑。所以路还很长,我们还需要努力优化。
有待解决的问题
有待解决的问题已经很明显了。
- 精确度的问题
- 移动端,网页端的嵌入问题
- 识别图像的清晰度问题
- 识别图像的灰度问题
参考文献
-
《深度学习入门·基于python的理论与实现》日·斋藤康毅 著 陆宇杰 译 2018.7版
-
斯坦福2017季CS231n深度视觉识别课程视频(by Fei-Fei Li, Justin Johnson, Serena Yeung)
代码和工具
- Python 3.7.4
- Clang 4.0.1
- Sublime Text
- 我们的代码已经push到了gitee,代码地址
本文使用markdown标记语言编写,已上传至个人博客。源代码可以到gitee中查看。
0 条评论