写在前面

  1. 教材链接:Neural Networks and Deep Learning

  2. 本机系统:macOS Catalina 10.15.6,python3.8.3

  3. 本博客源代码:handwritten_digit_recognition_python3

  4. python2(也是教材官方的github库):neural-networks-and-deep-learning

  5. 需要的第三方库:

    1
    2
    3
    import numpy as np
    import gzip
    import pickle

一、数据导入

mnist_loader.py包中,将data目录下的mnist.pkl.gz压缩包解压缩,并以一定的格式传递给python进行计算。

1
2
3
4
5
6
7
8
9
def load_data():
'''
将MNIST文件解压并打开,存储到变量中。
:return:
'''
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

使用gzip.open()命令将.gz文件以二进制只读的格式打开,再用pickle.load()函数将数据存入变量中。

注意,此时各个变量的格式:

  • training_data:元祖的形式(x,y),其中x为一个有50000个元素的numpy矩阵,每一个元素又分别有784个元素,代表28*28的单个MNIST图像。y也是一个有50000个元素的numpy矩阵,每一个元素为MNIST图像所代表的digit数字,范围从0到9.
  • validation_data、test_data的形式与上述training_data相同,但只包含10000幅图像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def load_data_wrapper():
'''
将解压后得到的数据转化成更适合python计算的格式
'''
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

转换后各个变量的格式:

  • training_data:一个长度为50000的列表,列表中的每一个元素为元祖(x,y),其中,x是一个包含了输入图片的784维的numpy矩阵,y是一个10维的numpy矩阵,表示该数字的期待输出。

  • validation_data/test_data:一个长度为10000的列表,列表中的每一个元素为元祖(x,y),其中,x是一个包含了输入图片的784维的numpy矩阵,y是MNIST图像所代表的digit数字,范围从0到9。

1
2
3
4
5
6
7
8
def vectorized_result(j):
'''
:param j: 输入图像表示的数字
:return:
'''
e = np.zeros((10, 1))
e[j] = 1.0
return e

一个辅助函数,返回一个10维的numpy矩阵。

二、网络学习

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import random
import numpy as np

'''
定义一个神经网络类
'''
class Network(object):
def __init__(self, sizes):
self.num_layers = len(sizes)
self.sizes = sizes
self.bias = [np.random.randn(y, 1) for y in sizes[1:]] # randn返回的是一个0,1之间的随机小数,格式是y行1列。
self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]

def feedforward(self, a):
'''
:param a: 神经网络的输入
:return a: 输出层
'''
for b, w in zip(self.bias, self.weights):
a = sigmoid(np.dot(w, a) + b)
return a

def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
'''
随机梯度下降算法,将训练数据分成相同大小的mini_batch,在每个batch不断迭代得到权重和偏置。
:param training_data: 训练数据
:param epochs: 迭代周期
:param mini_batch_size: 一个mini_batch的大小
:param eta: 学习速率
:param test_data: 测试数据
:return: None
'''
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:
print("Epoch {0} :{1}/{2}".format(j, self.evaluate(test_data), n_test))
else:
print("Epoch {0} complete.".format(j))

def update_mini_batch(self, mini_batch, eta):
'''
在每一个mini_batch内的每一个数据使用反向传播算法,得到权重和偏置的在一个mini_batch内的更新。
:param mini_batch: 一个用来训练网络的数据batch,由测试数据打乱后分割而成。
:param eta: 学习速率
:return: None
'''
nabla_b = [np.zeros(b.shape) for b in self.bias]
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.bias = [b - (eta / len(mini_batch) * nb) for b, nb in zip(self.bias, nabla_b)]
self.weights = [w - (eta / len(mini_batch) * nw) for w, nw in zip(self.weights, nabla_w)]

def backprop(self, x, y):
'''
反向传播算法,利用四个公式,得到一个单独数据输入时,权重和偏置的改变
:param x: 输入的数据
:param y: 期望输出
:return:
'''
nabla_b = [np.zeros(b.shape) for b in self.bias]
nabla_w = [np.zeros(w.shape) for w in self.weights]
activation = x
activations = [x]
zs = []
for b, w in zip(self.bias, 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

def evaluate(self, test_data):
'''
得到正确分类的个数
:param test_data: 测试数据
:return:
'''
test_result = [(np.argmax(self.feedforward(x)), y) for x, y in test_data]
return sum(int(x == y) for x, y in test_result)

def cost_derivative(self,output_activations,y):
'''
得到代价函数的导数
:param output_activations: 输出激活值
:param y: 期望输出
:return:
'''
return output_activations-y

def sigmoid(z):
'''
S型神经元的格式
:param z: 中间值
:return:
'''
return 1.0 / (1.0 + np.exp(-z))


def sigmoid_prime(z):
'''
S型神经元的导数
:param z:
:return:
'''
return sigmoid(z) * (1 - sigmoid(z))

每个函数的具体作用都写在了函数前的注释中,此处不再赘述。

再来看看main.py里面的内容。

1
2
3
4
5
6
7
import network
import mnist_loader
if __name__ == '__main__':
print("begin!")
training_data,validation_data,test_data=mnist_loader.load_data_wrapper()
net=network.Network([784,30,10])
net.SGD(training_data,30,10,3.0,test_data=test_data)

main函数是python程序运行的主函数,将剩下两个python程序导入后,给各个data赋值,定义一个[784,30,10]的神经网络,最后调用SGD来完成计算。

最终结果是,在我的三十个迭代周期中,分辨准确率的最大值达到了95.2%。

三、步骤总结

使用神经网络视线手写数字识别大概分以下几个步骤。

1.数据导入

使用一个单独的程序将官方提供的MNIST数据导入python程序,以供我们训练使用。

2.定义网络

此处使用的是一个[784,30,10]的神经网络,隐藏层只有30个神经元,学习速率3.0,选取30个迭代周期,每个batch的大小设置为10。

此处的超参数也可以自己设定。

3.随机梯度下降

将训练数据分成相同大小的mini_batch,在每个batch不断迭代得到权重和偏置,称为随机梯度下降算法。

具体来说,在每一个迭代周期内:

  • 将training_data打乱,按照mini_batch的步长将training_data分成不同的mini_batchs。

  • 在每个mini_batch内,一个一个的输入训练数据,使用反向传播算法,将得到的关于权重和偏置的改变累加起来,最后利用公式

    $$w^{l} \rightarrow w^{l}-\frac{\eta}{m} \sum_{x} \delta^{x, l}\left(a^{x, l-1}\right)^{T}$$

    $$b^{l} \rightarrow b^{l}-\frac{\eta}{m} \sum_{x} \delta^{x, l})$$

    获得在一个mini_batch内的权重和偏置的改变。

  • 遍历完所有的mini_batch,得到一次迭代后w和b的改变。

  • 将test_data输入到刚刚训练好的网络中,得到准确率。

  • 继续下一次迭代。

4.反向传播算法

对于一个单独的元祖输入(x,y),其中x是一个包含了输入图片的784维的numpy矩阵,y是一个10维的numpy矩阵,表示该数字的期待输出,反向传播算法的具体做法如下:

  • 第一层的激活值a^1^即为输入x。
  • 根据公式,使用网络原本的w和b,算出每一层的中间值z和激活值a。
  • 计算输出层误差:$\delta^{x, L}=\nabla_{a} C_{x} \odot \sigma^{\prime}\left(z^{x, L}\right)$,其中$\nabla_{a} C_{x}=a^{L}-y$
  • 反向传播,计算其他层的误差:$\delta^{l}=\left(\left(w^{l+1}\right)^{T} \delta^{l+1}\right) \odot \sigma^{\prime}\left(z^{l}\right)$
  • 得到代价函数的梯度下降公式:$\frac{\partial C}{\partial w_{j k}^{l}}=a_{k}^{l-1} \delta_{j}^{l}$,$\frac{\partial C}{\partial b_{j }^{l}}=\delta_{j}^{l}$