BF神经网络(一)算法原理与实现

传送门:
BF神经网络(二)预测共享单车使用情况

一、反向传播算法基本原理

如何让多层神经网络学习呢?我们已了解了使用梯度下降来更新权重,反向传播算法则是它的一个延伸。以一个两层神经网络为例,可以使用链式法则计算输入层-隐藏层间权重的误差。

要使用梯度下降法更新隐藏层的权重,你需要知道各隐藏层节点的误差对最终输出的影响。每层的输出是由两层间的权重决定的,两层之间产生的误差,按权重缩放后在网络中向前传播。既然我们知道输出误差,便可以用权重来反向传播到隐藏层。

例如,输出层每个输出节点$k$的误差是$\delta^o_k$,隐藏节点$j$的误差即为输出误差乘以输出层-隐藏层间的权重矩阵以及梯度。

$$\delta ^h_j=\sum W_{jk}\delta^o_kf’(h_j)$$

然后,梯度下降与之前相同,只是用新的误差:
$$\Delta w_{ij}=\eta \delta^h_jx_i $$

其中 $w_{ij}$ 是输入和隐藏层之间的权重, $x_i$ 是输入值。这个形式可以表示任意层数。权重更新步长等于步长乘以层输出误差再乘以该层的输入值。
$$\Delta w_{qp}=\eta\delta_{output}V_{input}$$
现在,你有了输出误差,$\delta_{output}$,便可以反向传播这些误差了。$V_{input}$ 是该层的输入,比如经过隐藏层激活函数的输出值。

二、范例

1. 单节点误差项


假设我们试着训练一些二进制数据,目标值是 $y=1$。我们从正向传播开始,首先计算输入到隐藏层节点:

$h=\sum_iw_ix_i=0.1 \times 0.4 - 0.2\times 0.3=-0.02 $

以及隐藏层节点的输出:

$a=f(h)=sigmoid(-0.02)=0.495$

然后将其作为输出节点的输入,该神经网络的输出可表示为:

$\hat y=f(W \cdot a)=sigmoid(0.1 \times0.495)=0.512$

基于该神经网络的输出,就可以使用反向传播来更新各层的权重了。sigmoid 函数的导数$\hat y=f(W \cdot a)(1-f(W\cdot a))$,输出节点的误差项(error term)可表示为:

$\delta^o=(y-\hat y)f’(W\cdot a)=(1-0.512)\times0.512\times (1-0.512)=0.122 $

现在我们要通过反向传播来计算隐藏节点的误差项。这里我们把输出节点的误差项与隐藏层到输出层的权重 $W$ 相乘。隐藏节点的误差项$\delta ^h_j=\sum W_{jk}\delta^o_kf’(h_j)$,因为该案例只有一个隐藏节点,这就比较简单了

$\delta ^h=W\delta^of’(h)=0.1 \times 0.122 \times 0.495 \times(1-0.495)=0.003$

有了误差,就可以计算梯度下降步长了。隐藏层-输出层权重更新步长是学习速率乘以输出节点误差再乘以隐藏节点激活值。

$\Delta W=\eta\delta^oa=0.5\times0.122\times 0.495=0.0302 $

然后,输入-隐藏层权重 $w_i$ 是学习速率乘以隐藏节点误差再乘以输入值。

$\Delta w_i=\eta\delta^ox_i=(0.5\times0.003\times0.1,0.5\times0.003\times0.3)=(0.00015,0.00045)$

从这个例子中你可以看到 sigmoid 做激活函数的一个缺点。sigmoid 函数导数的最大值是 0.25,因此输出层的误差被减少了至少 75%,隐藏层的误差被减少了至少 93.75%!如果你的神经网络有很多层,使用 sigmoid 激活函数会很快把靠近输入层的权重步长降为很小的值,该问题称作梯度消失。后面的课程中你会学到在这方面表现更好,也被广泛用于最新神经网络中的其它激活函数。

2. 多节点误差项时的情况

大多数时候我们需要创建多个隐藏层多几个节点,因此针对这种情况,现在在更新权重时,我们需要考虑隐藏层 每个节点 的误差 $\delta _j$

$\Delta w_{ij}=\eta\delta_jx_i$

首先,会有不同数量的输入和隐藏节点,所以试图把误差与输入当作行向量来乘会报错。

另外,现在$w_{ij}$是一个矩阵,所以右侧对应也应该有跟左侧同样的维度。幸运的是,NumPy 这些都能搞定。如果你用一个列向量数组和一个行向量数组相乘,它会把列向量的第一个元素与行向量的每个元素相乘,组成一个新的二维数组的第一行。列向量的每一个元素依次重复该过程,最后你会得到一个二维数组,形状是 (len(column_vector),len(row_vector))

hidden_error*inputs[:,None]
array([[ -8.24195994e-04,  -2.71771975e-04,   1.29713395e-03],
       [ -2.87777394e-04,  -9.48922722e-05,   4.52909055e-04],
       [  6.44605731e-04,   2.12553536e-04,  -1.01449168e-03],
       [  0.00000000e+00,   0.00000000e+00,  -0.00000000e+00],
       [  0.00000000e+00,   0.00000000e+00,  -0.00000000e+00],
       [  0.00000000e+00,   0.00000000e+00,  -0.00000000e+00]])

这正好是我们计算权重更新的步长的方式。跟以前一样,如果你的输入是一个一行的二维数组,也可以用 hidden_error*inputs.T,但是如果 inputs 是一维数组,就不行了(这时可以使用上述方法hidden_error*inputs[:,None])。

三、反向传播练习

接下来你将用代码来实现一次两个权重的反向传播更新。我们提供了正向传播的代码,你来实现反向传播的部分。

要做的事

  • 计算网络输出误差
  • 计算输出层误差项
  • 用反向传播计算隐藏层误差项
  • 计算反向传播误差的权重更新步长
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
import numpy as np


def sigmoid(x):
"""
Calculate sigmoid
"""
return 1 / (1 + np.exp(-x))


x = np.array([0.5, 0.1, -0.2])
target = 0.6
learnrate = 0.5

weights_input_hidden = np.array([[0.5, -0.6],
[0.1, -0.2],
[0.1, 0.7]])

weights_hidden_output = np.array([0.1, -0.3])

## Forward pass
hidden_layer_input = np.dot(x, weights_input_hidden)
hidden_layer_output = sigmoid(hidden_layer_input)

output_layer_in = np.dot(hidden_layer_output, weights_hidden_output)
output = sigmoid(output_layer_in)

## Backwards pass
## TODO: Calculate output error
error = target - output

# TODO: Calculate error term for output layer
output_error_term = error * output * (1 - output)

# TODO: Calculate error term for hidden layer
hidden_error_term = np.dot(output_error_term, weights_hidden_output) * \
hidden_layer_output * (1 - hidden_layer_output)

# TODO: Calculate change in weights for hidden layer to output layer
delta_w_h_o = learnrate * output_error_term * hidden_layer_output

# TODO: Calculate change in weights for input layer to hidden layer
delta_w_i_h = learnrate * hidden_error_term * x[:, None]

print('Change in weights for hidden layer to output layer:')
print(delta_w_h_o)
print('Change in weights for input layer to hidden layer:')
print(delta_w_i_h)
Change in weights for hidden layer to output layer:
[0.00804047 0.00555918]
Change in weights for input layer to hidden layer:
[[ 1.77005547e-04 -5.11178506e-04]
 [ 3.54011093e-05 -1.02235701e-04]
 [-7.08022187e-05  2.04471402e-04]]

Nice job!  That's right!

四、实现反向传播算法

现在我们知道输出层的误差是

$$\delta _k=(y_k-\hat y_k)f’(a_k)$$

隐藏层误差为:

$$\delta j=\sum[w{jk}\delta_k]f’(h_j)$$
现在我们只考虑一个简单神经网络,它只有一个隐藏层和一个输出节点。这是通过反向传播更新权重的算法概述:

1) 初始化权重。为了简单起见,这里初始化为0

  • 输入到隐藏册的权重$\Delta w_{ij}=0$
  • 隐藏层到输出层权重$\Delta W_j=0$

2) 对训练数据中的每个点执行

  • 让它正向通过网络,计算出$\hat y$
  • 计算输出节点的误差梯度 $\delta^o=(y-\hat y)f’(z)$这里$z=\sum _jW_ja_j$是输出节点的输入。
  • 误差传播到隐藏层 $\delta^h_j=\delta^oW_jf’(h_j)$
  • 更新权重步长:
    • $\Delta W_j=\Delta W_j+\delta^oa_j$
    • $\Delta w_{ij}=\Delta w_{ij}+\delta_j^ha_i$

3) 更新权重,其中$\eta $是学习率,$m$ 是数据点的数量:

  • $W_j=W_j+\eta\Delta W_j/m$
  • $w_{ij}=w_{ij}+\eta\Delta w_{ij}/m$

4) 重复以上过程$e$代。

代码实现例子

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
import numpy as np
import pandas as pd

admissions = pd.read_csv('binary.csv')

data = pd.concat([admissions, pd.get_dummies(admissions['rank'], prefix='rank')], axis=1)
data = data.drop('rank', axis=1)

for field in ['gre', 'gpa']:
mean, std = data[field].mean(), data[field].std()
data.loc[:,field] = (data[field]-mean)/std

np.random.seed(21)
sample = np.random.choice(data.index, size=int(len(data)*0.9), replace=False)
data, test_data = data.ix[sample], data.drop(sample)

features, targets = data.drop('admit', axis=1), data['admit']
features_test, targets_test = test_data.drop('admit', axis=1), test_data['admit']

#=================================

def sigmoid(x):
"""
Calculate sigmoid
"""
return 1 / (1 + np.exp(-x))

n_hidden = 2 # number of hidden units
epochs = 900
learnrate = 0.005

n_records, n_features = features.shape
last_loss = None

weights_input_hidden = np.random.normal(scale=1 / n_features ** .5,
size=(n_features, n_hidden))
weights_hidden_output = np.random.normal(scale=1 / n_features ** .5,
size=n_hidden)

for e in range(epochs):
del_w_input_hidden = np.zeros(weights_input_hidden.shape)
del_w_hidden_output = np.zeros(weights_hidden_output.shape)
for x, y in zip(features.values, targets):

hidden_input = np.dot(x, weights_input_hidden)
hidden_output = sigmoid(hidden_input)

output = sigmoid(np.dot(hidden_output,
weights_hidden_output))

error = y - output

output_error_term = error * output * (1 - output)

hidden_error = np.dot(output_error_term, weights_hidden_output)

hidden_error_term = hidden_error * hidden_output * (1 - hidden_output)

del_w_hidden_output += output_error_term * hidden_output
del_w_input_hidden += hidden_error_term * x[:, None]

weights_input_hidden += learnrate * del_w_input_hidden / n_records
weights_hidden_output += learnrate * del_w_hidden_output / n_records

if e % (epochs / 10) == 0:
hidden_output = sigmoid(np.dot(x, weights_input_hidden))
out = sigmoid(np.dot(hidden_output,
weights_hidden_output))
loss = np.mean((out - targets) ** 2)

if last_loss and last_loss < loss:
print("Train loss: ", loss, " WARNING - Loss Increasing")
else:
print("Train loss: ", loss)
last_loss = loss

hidden = sigmoid(np.dot(features_test, weights_input_hidden))
out = sigmoid(np.dot(hidden, weights_hidden_output))
predictions = out > 0.5
accuracy = np.mean(predictions == targets_test)
print("Prediction accuracy: {:.3f}".format(accuracy))