本次手写数字识别参考的是Michael Nielsen大佬写的 Neural Networks and Deep Learning 来实现的,由于本书的代码都是由 Python 2.0 编写而成,因此这次我打算使用 Python 3.9 来进行源代码的修改,之后再去实现手写数字识别

期望实现的目标

  • 通过Python3.9实现手写数字识别的基本代码
  • 基于以上代码实现出一个较为精确的模型并产生可视化函数
  • 实现在每次返回后随机抽取9个图片显示,并在上方显示出识别出的数字结果
  • 基于模型的基础上,做出一套可以通过识别自己笔迹,实时输出的手写数字识别系统

准备环境

MNIST数据库

MNIST 数据集可在 获取, 它包含了四个部分:

  • Training set images: train-images-idx3-ubyte.gz (9.9 MB, 解压后 47 MB, 包含 60,000 个样本)
  • Training set labels: train-labels-idx1-ubyte.gz (29 KB, 解压后 60 KB, 包含 60,000 个标签)
  • Test set images: t10k-images-idx3-ubyte.gz (1.6 MB, 解压后 7.8 MB, 包含 10,000 个样本)
  • Test set labels: t10k-labels-idx1-ubyte.gz (5KB, 解压后 10 KB, 包含 10,000 个标签)

和其他的手写数字识别一样,都是用 Training 中的 60000 个样本进行训练,再用 Test 中的 10000 个样本进行验证

Numpy 的 Python 库

如果你没有安装过 Anaconda ,那我强烈建议你去安装,因为真的是太方便了。

如果没有的话也可以在这里下载

神经网络搭建

神经网络代码的核心特征

1
2
3
4
5
6
7
8
9
10
class Network(object):

def __init__(self, sizes):
self.num_layers = len(sizes)
self.sizes = sizes
# 偏置的随机初始化
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
# 权重的随机初始化
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]

代码解释:

size列表

包含各层神经元。假如想要创建一个3层的神经网络,每层分别有2,3,1个神经元,则使用方法如下:

1
net = Network([2,3,1])

np.random.randn()

是Numpy中的一员来⽣成均值为 0,标准差为 1 的高斯分布。这样就可以将我们所设置的神经网络对象,进行 w (权重),b (偏置) 进行随机初始化。

值得注意的是

  1. 这个核心代码,默认情况为,第一层神经元均为输入层,并且不对这些神经元进行任何的偏置处理。
  2. net.weight[1]是一个存储着链接二、三层神经元的权重。

定义S型函数

1
2
def sigmoid(z):
return 1.0/(1.0+np.exp(-z))

PS:注意,当输入z 是⼀个向量或者 Numpy 数组时,Numpy 自动地按元素应用sigmoid() 函数。

np.exp()执行e^x^运算。

添加向前传播的行为方式

1
2
3
def feedforward(self,a):
for b,w in zip(self.biases,self.weights):
a = sigmoid(np.dot(w,a)+b)

目的:为每一层都应用

这个公式

np.dot()函数是为了执行,线性代数中的矩阵乘法。

实现梯度下降学习的SGD方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def sgd(self, training_data, epochs, mini_batch_size, eta, test_data=None):
accuracy_list = []
if test_data:
n_test = len(test_data)
n = len(training_data)
for j in range(epochs):
random.shuffle(training_data)
mini_batches = [
training_data[k:k+mini_batch_size]
for k in range(0, n, mini_batch_size)]
for mini_batch in mini_batches:
# 实现一次梯度下降
self.update_mini_batch(mini_batch, eta)
if test_data:
# 计算正确率
accuracy = self.evaluate(test_data) / n_test
accuracy_list.append(accuracy)
print(f"Epoch {j}: {self.evaluate(test_data)} / {n_test};")
else:
print(f"Epoch {j} complete")

定义参数:

  • self:指代类实例本身。
  • training_data:包含了训练数据及其对应标签的列表。
  • epochs:表示要遍历整个训练数据集的次数。
  • mini_batch_size:表示每次迭代中使用的小批量样本的大小。
  • eta:学习效率,用于控制梯度下降的步长。
  • test_data:可选参数,包含了测试数据及其对应标签的列表。

实现步骤:

  1. 如果提供了测试数据,则计算测试数据的数量n_test
  2. 对于每个epoch,将训练数据随机打乱。
  3. 将打乱后的训练数据分割成小批量样本。
  4. 对于每个小批量样本,使用self.update_mini_batch方法更新神经网络的权重和偏置。
  5. 如果提供了测试数据,则在每个epoch结束时,使用self.evaluate方法计算神经网络在测试数据上的准确率,并打印输出。
  6. 如果没有提供测试数据,则在每个epoch结束时,输出”Epoch {j} complete”。

实现小批量样本的更新

1
2
3
4
5
6
7
8
9
10
11
12
def update_mini_batch(self, mini_batch, eta):
# 存储偏置和权重的梯度
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# 分别计算每个样本的偏导数
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
# 函数实现
self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]

具体来说,该函数接收一个mini_batch(一个由训练样本和对应的标签组成的元组列表)、一个学习率eta作为输入,然后计算出该小批次样本的梯度并更新权重和偏置。

该函数首先初始化了两个空列表nabla_bnabla_w,用于存储偏置和权重的梯度。然后使用backprop方法计算每个样本的偏导数,并将它们累加到nabla_bnabla_w中。

接下来,该函数使用更新后的权重和偏置的公式,具体来说,对于每个权重和偏置,函数将其减去一个学习率(eta)乘以其对应的梯度(nabla_w和nabla_b),并除以mini_batch的大小

反向传播 定义backprop()

是一种反向传播的算法,一种快速计算代价函数梯度的方法。在本函数中具体用来计算每个样本的偏置与权重的偏导数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def backprop(self, x, y):
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# 前向传播
activation = x
activations = [x]
zs = []
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# 反向传播
delta = self.cost_derivative(activations[-1], y) * \sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
for l in range(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)

输入$x$ 和 $y$,并返回一个元组 (nabla_b, nabla_w),表示代价函数 C_x 的梯度。其中 nabla_bnabla_w 是逐层存储的 Numpy 数组列表,与 self.biasesself.weights 类似。

首先,该函数进行前向传播计算,计算出每层的激活值(activations[])和加权输入值(zs[]),然后,函数计算输出层的误差(即 $\delta$),并使用误差反向传播算法来计算每层的误差。应用如上公式

对于每一层,误差计算需要使用前一层的误差来计算,并且在计算时还需要使用该层的加权输入值和激活函数的导数。最后,函数返回 nabla_bnabla_w,表示代价函数相对于每个偏置和权重的梯度。

神经网络评估

1
2
3
4
5
6
def evaluate(self, test_data):
#函数获取预测值中最大值所对应的类别标签,并将其与真实标签 y 组成元组
test_results = [(np.argmax(self.feedforward(x)), y)
for (x, y) in test_data]
#将相同的标签存入列表中
return sum(int(x == y) for (x, y) in test_results)

np.argmax()

numpy.argmax(a, axis=None, out=None)

函数功能,返回最大值的索引。

若axis=1,表明按行比较,输出每行中最大值的索引,若axis=0,则输出每列中最大值的索引。

test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data]:将测试数据中的每个样本 x 通过神经网络前向传播得到预测值,使用 np.argmax() 函数获取预测值中最大值所对应的类别标签,并将其与真实标签 y 组成元组,存入列表 test_results 中。

return sum(int(x == y) for (x, y) in test_results):遍历 test_results 列表,如果预测值 x 与真实标签 y 相同,则将其转换为整数 1,否则为整数 0,最后将所有转换后的整数相加,得到正确分类的样本数,作为模型在测试数据上的准确率进行返回。

计算输出激活的偏导数向量

1
2
def cost_derivative(self, output_activations, y):
return (output_activations-y)

实现可视化函数

1
2
3
4
5
6
def plot_accuracy(self, accuracy_list):
plt.plot(range(1, len(accuracy_list) + 1), accuracy_list)
plt.title('Accuracy during Training')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.show()

导入MNIST数据库

将下列代码单独存放在一个.py文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import pickle
import gzip
import numpy as np


# 读取文件
def load_data():
# 下方修改文件路径
f = gzip.open('./data/mnist.pkl.gz', 'rb')
training_data, validation_data, test_data = pickle.load(f, encoding='bytes')
f.close()
return training_data, validation_data, test_data


# 输出文件各部分信息
def load_data_wrapper():
tr_d, va_d, te_d = load_data()
training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
training_results = [vectorized_result(y) for y in tr_d[1]]
training_data = list(zip(training_inputs, training_results))
validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
validation_data = list(zip(validation_inputs, va_d[1]))
test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
test_data = list(zip(test_inputs, te_d[1]))
return training_data, validation_data, test_data


def vectorized_result(j):
e = np.zeros((10, 1))
e[j] = 1.0
return e

结果展示

result_1.1

小结

本次实现的手写识别有几点不足:

  1. 没能实现显示特征图和预测结果将其输出。(这点会在之后的学习中,进行增加)
  2. 没能实现手写及时输出。(是未来可以学习去实现的一个目标,会在之后的学习当中去完善)

出现以上几点问题的原因主要还是,学习的东西知之甚少,不能将一些功能写出,并为自己所用。

个人感觉现在的已有代码身上,对增加测试数据和保存有些麻烦,之后会在空闲时间,通过使用pytorch,重新再写一份手写数字识别,并争取能够实现本次没能实现的两个目标。