人工神经网络(3)-- 有导师学习

有导师学习算法将一组训练集送入网络,根据网络的实际输出与期望输出间的差别来调整连接权。

反向传播(Back Propagation)算法

1、原理

利用有导师学习算法训练神经网络的本质是一个最优化的过程,也就是要找到最优的权重(weights),使得网络的输出和预期的结果最接近(误差最小)。
现在的问题是,如何根据误差来调整权重?首先可以想到的是使用梯度下降(Gradient Descent)的方法。在每轮训练中,我们计算权重在当前值上的梯度,梯度的正负可以告诉我们该如何调整权重。如果梯度为负,我们就增加权重的值;如果梯度为正,我们就减少权重的值。如果网络的输出和预期完全一致,权重的梯度就是0,也就是说不需要调整。
经典的梯度下降法很有效,但是对于含有隐藏层的神经网络来说就有些行不通了。梯度下降法可以应对那些可以求出误差的情况,比如逻辑回归(我们可以把它看做没有隐层的网络);但是对于含有隐层的神经网络,其中隐层的误差是不存在的!因此不能对它直接应用梯度下降!反向传播算法应运而生,它的基本思想是将误差从末层(输出层)往前传递再应用梯度下降法。传递的过程用到了链式法则,因而可以说反向传播算法是梯度下降法在链式法则中的应用。

根据链式法则,隐藏层误差与输出层误差成正比,比例系数由两层之间的权重决定。也就是说,隐藏层节点与输出节点连接越强,对最终输出误差的影响越大。这是很有道理的!

反转神经网络,将误差作为输入,这就是反向传播

前向传递输入信号直至输出产生误差,反向传播误差信息更新权重矩阵。这两句话很好的形容了信息的流动方向,权重得以在信息双向流动中得到优化。

2、流程模拟

以一个两层神经网络为例,计算其权重的更新过程。假设该神经网络包含两个输入值,一个隐藏节点和一个输出节点,隐藏层和输出层的激活函数都是 sigmoid,如下图所示。(注意:图底部的节点为输入值,图顶部的y为输出值。输入层不计入层数,所以该结构被称为两层神经网络。)

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

$$h = \sum_i w_i x_i = 0.1 \times 0.4 - 0.2 \times 0.3 = -0.02$$

以及隐藏层节点的输出

$$a = f(h) = \mathrm{sigmoid}(-0.02) = 0.495$$

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

$$\hat y = f(W \cdot a) = \mathrm{sigmoid}(0.1 \times 0.495) = 0.512$$

基于该神经网络的输出,就可以使用反向传播来更新各层的权重了。sigmoid 函数的导数$ f’(W \cdot a) = f(W \cdot a) (1 - f(W \cdot a)) $,输出节点的误差对于当前带权输入的偏导项(以下简称”误差带权偏导项”)可表示为

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

现在我们要通过反向传播来计算隐藏节点的偏导项。这里我们把输出节点的偏导项与隐藏层到输出层的权重 W 相乘。隐藏节点的误差带权偏导项可表示为

$$\delta^h_j = \sum_k W_{jk} \delta^o_k f’(h_j)$$

由于本例中只有一个隐藏层节点,就成了

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

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

$$\Delta W = \eta \delta^o a = 0.5 \times 0.122 \times 0.495 = 0.0302$$

输入-隐藏层的权重$w_i$是学习速率乘以隐藏节点误差带权偏导项再乘以输入值。

$$\Delta w_i = \eta \delta^h x_i = (0.5 \times 0.003 \times 0.1, 0.5 \times 0.003 \times 0.3) = (0.00015, 0.00045)$$

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

3、代码实现

权值更新示例:

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
# coding = utf-8
import numpy as np

#========================================================
# 数据准备
#========================================================
# 训练集
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])

#========================================================
# 激活函数
#========================================================

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

#========================================================
# 前向传播
#========================================================

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)

#========================================================
# 反向传递
#========================================================

# 输出层误差
error = target - output
# 输出层误差带权偏导项
output_error_term = error * output * (1 - output)

# 隐藏误差带权偏导项
hidden_error_term = np.dot(output_error_term, weights_hidden_output) * \
hidden_layer_output * (1 - hidden_layer_output)

# 计算隐层到输出层的权值改变
delta_w_h_o = learnrate * output_error_term * hidden_layer_output

# 计算输入层到隐层的权值改变
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)

迭代训练:

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
# coding = utf-8
import numpy as np

#========================================================
# 数据准备
#========================================================
# 训练集
features = None
targets = None

# 测试集
features_test = None
targets_test = None

n_records, n_features = features.shape

#========================================================
# 参数配置
#========================================================

# 隐层点数
n_hidden = 2
# 迭代次数
epochs = 900
# 学习率
learnrate = 0.005

#========================================================
# 初始化权值
#========================================================

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)

#========================================================
# 激活函数
#========================================================

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

#========================================================
# 训练过程
#========================================================
last_loss = None

for e in range(epochs):

w_input_hidden = np.zeros(weights_input_hidden.shape)
w_hidden_output = np.zeros(weights_hidden_output.shape)

for x, y in zip(features, targets):
##=============== 前向传递 ===============##
hidden_input = np.dot(x, weights_input_hidden)
hidden_output = sigmoid(hidden_input)

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

##=============== 反向传播 ===============##
# 输出误差
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)

# 更新权值
w_hidden_output += output_error_term * hidden_output
w_input_hidden += hidden_error_term * x[:, None]

# 权值更新
weights_input_hidden += learnrate * w_input_hidden / n_records
weights_hidden_output += learnrate * w_hidden_output / n_records

# 打印训练集的均方误差
if e % (epochs / 10) == 0:
hidden_output = sigmoid(np.dot(features, 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))
0%