Back Propagation Neural Network

Back Propagation Neural Network

模型架构

BP 神经网络是一种多层的前馈神经网络,其主要的特点是:信号是前向传播的,而误差是反向传播的。BP 神经网络的过程主要 分为两个阶段,第一阶段是信号的前向传播,从输入层经过隐含层,最后到达输出层;第二阶段是误差的反向传播,从输出层 到隐含层,最后到输入层,依次调节隐含层到输出层的权重和偏置,输入层到隐含层的权重和偏置。其架构如图所示
BP 神经网络

前向传播

在训练网络之前,我们需要随机初始化权重和偏置,对每一个权重取[-1,1]的一个随机实数,每一个偏置取[0,1]的一个随机实数,之后就开始进行前向传播。前向传播比较简单,只要按照模型的方法一层一层的计算即可,具体公式如下

\[I_j = \sum_i{w_{ij}O_i} + b_j \qquad O_j = \frac{1}{1+\exp^{-I_j}}\]

其中\(I_j\)是第\(j\)层网络的输入,\(O_j\)是第\(j\)层网络的输出,\(w_{ij}\)是第\(i\)层到第\(j\)层的连接权重

后向传播

后向传播从最后一层即输出层开始,我们训练神经网络作分类的目的往往是希望最后一层的输出能够描述数据记录的类别,比如对于一个二分类的问题,我们常常用两个神经单元作为输出层,如果输出层的第一个神经单元的输出值比第二个神经单元大,我们认为这个数据记录属于第一类,否则属于第二类。对于一个随机初始化参数的网络是不能够很好地描述数据集,因此需要对参数进行调整。这个调整过程就是通过误差的后向传播来完成的。误差\(e = |\mathbf{o} - \mathbf{y}|_2\),其中\(\mathbf{o}\)是输出向量,\(\mathbf{y}\)是目标向量。因此在数据集\(\mathcal{D}\)中,损失函数\(\mathcal{J}\)可定义为

\[\mathcal{J}(w,b) = \frac{1}{2}\sum_{i=1}^{|\mathcal{D}|}e_i\]

其中\(|\mathcal{D}|\)表示数据集\(\mathcal{D}\)的样本总数,\(e_i\)表示第\(i\)个样本的误差值。 通过权值更新公式 \((w,b) := (w,b) - \alpha(\nabla_{(w,b)}\mathcal{J}(w,b))\) 更新权值即可。具体细节的推导比较复杂,所以只写到这一步,通过阅读代码也能很好地理解后向传播的具体过程。 这里给出我觉得不错的教程

训练终止条件

每一轮训练都使用数据集的所有记录,但什么时候停止,停止条件有下面两种:

  1. 设置最大迭代次数,比如使用数据集迭代 100 次后停止训练
  2. 计算训练集在网络上的预测准确率,达到一定门限值后停止训练

这次算法使用的是第一种方法,后续如果时间充足的话再去实现第二种。

代码实现

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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#! /usr/bin python

from __future__ import print_function
from __future__ import division
import random
import numpy as np
import cPickle as pkl

__author__ = 'wangzx'

class Network(object):

def __init__(self, sizes):
"""``sizes``是 list 类型,第 i 个值表示第 i 层所包含的神经元个数。
例如,如果 sizes=[2,3,1],那么就表示一个三层网络,第一层有 2 个神经元,
第二层有 3 个神经元,最后一层有 1 个神经元。网络的偏置 b 和权值 w 使用标准高斯
分布来初始化。需要注意的一点是网络的第一层表示输入层,最后一层表示输出层
"""

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:])]

def feedforward(self, a):
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a / np.sum(a)

def SGD(self, training_data, epochs, mini_batch_size, eta,
test_data=None):

"""使用 mini-batch stochastic gradient descent 方法训练网络。
``training_data``是一个列表元组``(x,y)``,代表训练的输入和目标输出。
如果提供``test_data``的话,在每一个 epoch 完成之后会用模型计算这个数据相对应的
输出。这个可以让我们更清楚整个训练情况,不过会降低训练的速度。
"""

if test_data: n_test = len(test_data)
n = len(training_data)
for j in xrange(epochs):
random.shuffle(training_data)
mini_batches = [
training_data[k:k+mini_batch_size]
for k in xrange(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``是一个元组列表``(x,y)``,每一项是一个训练数据,分别代表
训练输入和目标输出,``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)]

def backprop(self, x, y):
"""返回损失函数 C_x 的梯度(nabla_b, nabla_w). ``nabla_b``表示偏置 b 的梯度,``nabla_w``表示
权重 w 的梯度。需要注意一点,返回的是每一层的梯度,也就是说返回值是一个 list,list 的每一项代表
网络其中一层的梯度"""

nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward
activation = x
activations = [x] # list to store all the activations, layer by layer
zs = [] # list to store all the z vectors, layer by layer
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
delta = self.cost_derivative(activations[-1], y) * \
sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())

# 下面这一部分可能有点晦涩,这里使用了 python 的索引特性:允许使用负数进行索引
# 因为最后一层是输出层,作为后向传播的初始阶段,在上面做了初始化,所以从倒数
# 第二层开始
for l in xrange(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):
"""返回预测准确的样本个数"""
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)

def cost_derivative(self, output_activations, y):
# 因为 y 是一个常数值,要转为 one-hot 编码
t = np.zeros((self.sizes[-1], 1))
t[y, 0] = 1
#print("output activation is {0}".format(output_activations))
#print("y is {0}".format(t))
# 使用最小二乘估计,损失函数导数如下
return (output_activations-t)

#### Miscellaneous functions
def sigmoid(z):
"""sigmoid 函数."""
return 1.0/(1.0+np.exp(-z))

def sigmoid_prime(z):
"""sigmoid 函数的导数."""
return sigmoid(z)*(1-sigmoid(z))

def test():
feat = pkl.load(open("data/train_X.pkl", 'rb'))
feat = (feat - np.min(feat, axis=0)) / \
(np.max(feat, axis=0) - np.min(feat, axis=0) + 1e-3)
y = pkl.load(open("data/train_Y.pkl", 'rb'))
train_data = [(np.asarray(f).reshape((-1,1)), t) for f, t in zip(feat, y)]
test_feat = pkl.load(open("data/test_X.pkl", 'rb'))
test_feat = (test_feat - np.min(test_feat, axis=0)) / \
(np.max(test_feat, axis=0) - np.min(test_feat, axis=0) + 1e-3)
test_y = pkl.load(open("data/test_Y.pkl", 'rb'))
test_data = [(np.asarray(f).reshape((-1,1)), t) for f, t in zip(test_feat, test_y)]
n_sample, ndim = feat.shape
bp = Network([ndim, 50, 2])

print("Begin fit training data")
bp.SGD(train_data, 50, 50, 0.02, test_data)

score = bp.evaluate(test_data) / len(test_data)
print("Test accuracy is %f" % score)


def test_mnist():
path = '/home/wangzx/usr/share/data/mnist2/'
train_X = pkl.load(open(path + 'mnist_train_X.pkl'))
train_y = pkl.load(open(path + 'mnist_train_y.pkl')).ravel()
test_X = pkl.load(open(path + 'mnist_test_X.pkl'))
test_y = pkl.load(open(path + 'mnist_test_y.pkl')).ravel()
train_data = [(np.asarray(f).reshape((-1,1)), t) for f, t in zip(train_X, train_y)]
test_data = [(np.asarray(f).reshape((-1,1)), t) for f, t in zip(test_X, test_y)]
n_sample, ndim = train_X.shape
bp = Network([ndim, 10])

print("Begin fit training data")
bp.SGD(train_data, 50, 50, 0.1, test_data)

score = bp.evaluate(test_data) / len(test_data)
print("Test accuracy is %f" % score)
if __name__ == "__main__":
#test()
#test_mnist()
pass

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器