BP神经网络的实现

这可能是第二简单的手写BP神经网络教程


本科生毕业设计想出来一个题目,用CNN来手写体识别。出了题目之后,发现对NN的知识都忘光了,现在重新手写NN来再学一次。

什么是神经网络(Neural Network)

计算机处理数字计算十分简单,然而处理图像数据却十分困难。通过利用生物学的设计构建一个使计算机能处理高维数据的模型,从而产生了神经网络这样的技术。
假设有这样一台机器,接受一个问题进行思考,然后得出答案。对于人类而言就是这样一个过程,对于计算机来说。这样的过程变成了:输入->计算->输出。但是从流程上看起来他们是相似的。


生物大脑的基本单元是神经元,虽然有各种形式,但是他们的本质就是将电信号从一端传递到另一端,从一个神经元传递到另一个神经元。然而,每个神经元的输入信号可能是单个,也可能是多个。有时候单个信号的输入并不能很好的解决某些问题(例如异或情况的分类)。


但是与传统函数的输入输出不同,神经元对于微小的噪声信号直接忽略,只有在输入信号高于阈值(threshold)才会产生输出。因此多个信号输入之后进行累加求和,当求和总量达到某个阈值后,神经元被激活,产生输出结果。这就是神经元。

关于阈值的判断,需要一个名为激活函数(activtion function)的模型来对信号进行处理。激活函数的种类十分多,普遍比较多的是Sigmoid函数。

这个函数相比较于阶跃函数比较平滑。
通过多个神经元之间的连接,构造一种名为神经网络的模型方法,对于计算机识别高维度的数据而言具有良好的适用性。

神经网络的工作原理

传统的神经网络分为三层,输入层(input layer)隐藏层(hidden layer)输出层(output layer)。隐藏层可以有多层网络构建,每层之间依靠连接权值进行连接。

正向传值

现在开始考虑正向传值计算,即从输入->输入层->隐藏层->输出层的过程。从单个神经元开始分析,从输入层开始,当输入值进入输入层时,输入值就只是单纯赋值给输入层节点,即输入值=输入单元,无需进行计算。
从输入层进入隐藏层开始,引入了权值的概念。即输入*权值的过程。

再将得到的值经过激活函数进行调节,这样就能得到隐藏层每个节点的输入结果。将得到的结果放入输出层再次进行计算,这样子我们就得到了一个完整的正向传播过程。
为了计算方便,引入矩阵来进行计算。令输入层数据为,输入层权值矩阵为。则进入隐藏层的和值为:

再将这个和值通过对应的激活函数进行激活得到隐藏层的输出,在这里我们使用Sigmoid函数作为激活函数(此时计算出来的为值向量,长度为隐藏层神经元的个数),输出值为:

然后将隐藏层输出值放入到输出层重复以上步骤输出,得到输出矩阵

反向传播(BackPropagation)

单次的正向传播得出的结果和实际结果相差可能会很大,现在需要做的就是使用这个误差来调整神经网络本身,进而改进输出值。
误差值的修正,是通过权值来进行实现的。当我们得到正向计算矩阵之后,可以求得输出误差,通过来自多个节点的权重进行反馈传播,这里还是要用到矩阵的计算,首先将隐藏输出系数矩阵进行转置,然后和输出误差点乘,公式如下:

使用这样的方式是将误差分割,按照权值的比例进行分配给不同的神经元。虽然也有其他的分配方式(如均匀分配),但是按权分配是比较合理的做法。

更新权重

虽然得到了每个神经元的误差值,但是还是没有解决如何更新权值的问题。在这里我们使用最简明且有效的方法:梯度下降(Gradient Descent)。关于梯度下降的过程,在手写线性回归的实现那章当中我有写,这里就不再描述了。
我们的目标是求得的极小值。这里使用链式法则并将每个项展开。

sigmoid函数求导过程十分复杂,这里不赘述。将上式化简得:

这里将公式化为了输入-隐藏层的下标,隐藏-输出层权值更新同理。

得到了修正值之后,设定学习率,然后得到更新权值如下:

虽然一次更新的量十分微小,但是训练集量足够预测效果将十分优秀。
以上基本就是BP神经网络的工作原理,接下来我们使用python手写一个BP神经网络,并使用mnist手写体训练集来测试效果。

编码实现

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
class NeuralN:
def __init__(self,input_nodes,hidden_nodes,output_nodes,learning_rate):
self.inodes = input_nodes
self.hnodes = hidden_nodes
self.onodes = output_nodes

self.lr = learning_rate

self.wih = np.random.normal(0.0,pow(self.hnodes,-0.5),(self.hnodes,self.inodes))
self.who = np.random.normal(0.0,pow(self.onodes,-0.5),(self.onodes,self.hnodes))

self.activtion_fun = lambda x: special.expit(x)

def train(self,inputs_list,targets_list):
inputs = np.array(inputs_list, ndmin=2).T
targets = np.array(targets_list,ndmin=2).T

hidden_inputs = np.dot(self.wih,inputs)
hidden_outputs = self.activtion_fun(hidden_inputs)
# print(hidden_outputs.shape)
final_inputs = np.dot(self.who,hidden_outputs)
final_outputs = self.activtion_fun(final_inputs)

outputs_error = targets - final_outputs

hidden_error = np.dot(self.who.T,outputs_error)

# print(hidden_error.shape)
print((hidden_error * hidden_outputs * (1 - hidden_outputs)).shape)
# print('+++++++++++++++++')
print(np.transpose(inputs).shape)
print('+++++++++++++++++')
# print(np.dot((outputs_error * final_outputs * (1 - final_outputs)),np.transpose(hidden_outputs)))

self.who += self.lr * np.dot((outputs_error * final_outputs * (1.0 - final_outputs)), \
np.transpose(hidden_outputs))

self.wih += self.lr * np.dot((hidden_error * hidden_outputs * (1.0 - hidden_outputs)), \
np.transpose(inputs))
pass

def query(self,inputs_list):
inputs = np.array(inputs_list, ndmin=2).T

hidden_inputs = np.dot(self.wih,inputs)

hidden_outputs = self.activtion_fun(hidden_inputs)

final_inputs = np.dot(self.who,hidden_outputs)

final_outputs = self.activtion_fun(final_inputs)

return final_outputs

这里是BP神经网络的基本框架。读取的数据是手写数据集,每个图像都是28x28的矩阵,训练集包含了70000个手写体数据。这里的不能使用有序的数据进行训练,不然就会取值趋于最后一个特定值。(之前使用了sklearn自带的数据集,调了一下午QAQ)下面我们开始加载数据,调用框架。

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
input_nodes = 784
hidden_nodes = 100
output_nodes = 10
learning_rate = 0.3

n = NeuralN(input_nodes,hidden_nodes,output_nodes,learning_rate)

training_data_file = open("./mnist_train.csv",'r')
training_data_list = training_data_file.readlines()
training_data_file.close()

for record in training_data_list:
all_values = record.split(',')
inputs = (np.asfarray(all_values[1:])/255*0.99)+0.01
targets = np.zeros(output_nodes) + 0.01
targets[int(all_values[0])] = 0.99
n.train(inputs,targets)

# train_list = X[:10000,:]
# target_list = Y[:10000]
# i = 0
# for record in train_list:
# # print(record.shape)
# inputs_list = (np.asfarray(record[:]/255*0.99)+0.01)
# # print(inputs_list.shape)
# targets = np.zeros(output_nodes) + 0.01
# print(target_list[i])
# targets[int(target_list[i])] = 0.99
# i += 1
# print(i)
# print(targets)
# n.train(inputs_list,targets)
# print()

训练完成之后我们开始预测。读取测试集,使用计分表进行积分,输出正确率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
test_data_file = open("./mnist_test.csv",'r')
test_data_list = test_data_file.readlines()
test_data_file.close()

scorecard =[]

for record in test_data_list:
all_values = record.split(',')
correct_label = int(all_values[0])
print(correct_label,'correct label')
inputs = (np.asfarray(all_values[1:])/255*0.99)+0.01
outputs = n.query(inputs)
label = np.argmax(outputs)
print(label,'net answer')
if label == correct_label:
scorecard.append(1)
else:
scorecard.append(0)

scorecard_arr = np.array(scorecard)
print(scorecard_arr.sum()/scorecard_arr.size)

最后正确率在93左右。

总结

整个手写基本参考《python神经网络编程》。这本书基本上详细讲明了BP神经网络和python基本开发方法,这篇博客是在这本书的基础上加上自身的学习经验所作的一个简单总结。因为时间有限,书中很多内容都没有好好总结(公式推导,预测问题,矩阵计算,权重初始随机等)。还有很多可以优化的地方,如学习率优化,隐藏层节点数优化等等。在这之后,卷积神经网络(Convolutional Neural Networks, CNN)是对于BP神经网络而言,在手写体识别具有更好的效果。毕业设计的题目也是使用CNN来进行手写体识别,所以说下一步就开始手写CNN试试。

参考链接

《python神经网络编程》