引言

根据上篇文章介绍的反向传播算法理论,我们今天来手动计算一下。

我们简化下上篇文章中的NN模型(参数和它类似,这里把每个偏差值设成1),使它的隐藏层只有1层。它有两个输入和两个输出。

手撸反向传播算法(附代码)_反向传播代码

我们有初始化权重、偏差和训练用的输入和输出值。

反向传播算法的目的是优化权值手撸反向传播算法(附代码)_反向传播算法_02来最小化损失函数,从而使NN预测的输出和实际输出更匹配。

假设给定一个训练集,它的输入是手撸反向传播算法(附代码)_反向传播Python实现_03,我们想要NN输出手撸反向传播算法(附代码)_反向传播代码_04手撸反向传播算法(附代码)_反向传播算法_05

正向过程(Forward Pass)

首先我们看看这个NN在初始权值和训练数据下的表现。我们会计算出每个隐藏神经元的输入,通过激活函数(这里用​​Sigmoid​​函数)转换成下一层的输入,直到达到输出层计算出最终输出。

先来计算隐藏层手撸反向传播算法(附代码)_神经网络_06的输入,
手撸反向传播算法(附代码)_图解反向传播_07

然后将它代入激活函数(这里用​​Sigmoid​​​函数),得到隐藏层手撸反向传播算法(附代码)_神经网络_06的输出,

手撸反向传播算法(附代码)_反向传播算法_09

同理有

手撸反向传播算法(附代码)_反向传播算法_10

可得手撸反向传播算法(附代码)_反向传播算法_11

这两个输出又可作为它们的下一层的输入,计算输出层手撸反向传播算法(附代码)_反向传播代码_12的输出如下两个步骤:

手撸反向传播算法(附代码)_反向传播代码_13

手撸反向传播算法(附代码)_反向传播代码_14

输出层手撸反向传播算法(附代码)_反向传播代码_15的输出也是同理:

手撸反向传播算法(附代码)_反向传播Python实现_16

手撸反向传播算法(附代码)_图解反向传播_17

可以看到,在初始参数上的输出和我们的目标手撸反向传播算法(附代码)_图解反向传播_18还是有不小距离的。

接下来,我们计算这不小距离到底有多么不小。

计算总误差(Total Error)

我们使用均方误差来计算总误差:

手撸反向传播算法(附代码)_神经网络_19

其中手撸反向传播算法(附代码)_反向传播Python实现_20就是输出层的实际输出,而手撸反向传播算法(附代码)_神经网络_21则是它的期望输出。

比如手撸反向传播算法(附代码)_反向传播代码_12神经元的期望输出是手撸反向传播算法(附代码)_反向传播代码_23,但是实际输出是手撸反向传播算法(附代码)_反向传播代码_24,所以误差是:

手撸反向传播算法(附代码)_反向传播代码_25

使用同样的过程计算出手撸反向传播算法(附代码)_反向传播算法_26:

手撸反向传播算法(附代码)_反向传播代码_27

总误差就是这些误差之和:

手撸反向传播算法(附代码)_图解反向传播_28

这个距离是真的不小啊。

反向过程

反向传播算法的目的是更高效的计算梯度,从而更新参数值,使得总误差更小,也就是使实际输出更贴近我们期望输出。它是作为一个整体去更新整个神经网络的。
反向就是先考虑输出层,然后再考虑它的上一层,并重复这个过程。因此,我们先从输出层开始计算。

输出层

考虑参数手撸反向传播算法(附代码)_神经网络_29,我们想知道手撸反向传播算法(附代码)_神经网络_29的改变会对总误差有多大的影响,即计算
手撸反向传播算法(附代码)_反向传播Python实现_31

手撸反向传播算法(附代码)_神经网络_32

手撸反向传播算法(附代码)_反向传播Python实现_33


我们需要计算这个等式中的每个式子,首先计算手撸反向传播算法(附代码)_反向传播算法_34如何影响总误差

手撸反向传播算法(附代码)_神经网络_35
手撸反向传播算法(附代码)_图解反向传播_36

接下来计算手撸反向传播算法(附代码)_反向传播算法_37

我们知道手撸反向传播算法(附代码)_神经网络_38

所以 手撸反向传播算法(附代码)_反向传播代码_39

最后是最简单的手撸反向传播算法(附代码)_神经网络_40

手撸反向传播算法(附代码)_反向传播Python实现_41
手撸反向传播算法(附代码)_反向传播代码_42

最后放到一起得:

手撸反向传播算法(附代码)_反向传播Python实现_43

通常一般定义 手撸反向传播算法(附代码)_图解反向传播_44
因此 手撸反向传播算法(附代码)_神经网络_45

为了减少误差,通常需要减少当前权值,如下:

手撸反向传播算法(附代码)_图解反向传播_46

学习率手撸反向传播算法(附代码)_图解反向传播_47一般取值手撸反向传播算法(附代码)_反向传播算法_48,当然是需要根据实际情况去调整的。

同理可算得手撸反向传播算法(附代码)_反向传播算法_49:
手撸反向传播算法(附代码)_神经网络_50
手撸反向传播算法(附代码)_反向传播Python实现_51
手撸反向传播算法(附代码)_反向传播代码_52

但还未结束,我们只是更新了输出层的参数,还要继续往前,更新隐藏层的参数。

隐藏层

首先来更新手撸反向传播算法(附代码)_图解反向传播_53:
手撸反向传播算法(附代码)_图解反向传播_54

我们要用和更新输出层参数类似的步骤来更新隐藏层的参数,但有一点不同的是,每个隐藏层的神经元都影响了多个输出层(或下一层)神经元的输出。手撸反向传播算法(附代码)_反向传播算法_55同时影响了手撸反向传播算法(附代码)_反向传播算法_34手撸反向传播算法(附代码)_反向传播算法_57,因此计算手撸反向传播算法(附代码)_反向传播代码_58需要将输出层的两个神经元都考虑在内:

手撸反向传播算法(附代码)_反向传播代码_59

手撸反向传播算法(附代码)_反向传播代码_60开始:

手撸反向传播算法(附代码)_反向传播代码_61

其实我们上面已经算过手撸反向传播算法(附代码)_反向传播算法_62了:

手撸反向传播算法(附代码)_反向传播Python实现_63

并且手撸反向传播算法(附代码)_图解反向传播_64:

手撸反向传播算法(附代码)_图解反向传播_65
手撸反向传播算法(附代码)_反向传播Python实现_66

也就是:

手撸反向传播算法(附代码)_反向传播算法_67

同理,可得手撸反向传播算法(附代码)_反向传播代码_68

因此:

手撸反向传播算法(附代码)_反向传播Python实现_69

现在我们已经知道了手撸反向传播算法(附代码)_反向传播Python实现_70,我们还需要计算手撸反向传播算法(附代码)_反向传播Python实现_71手撸反向传播算法(附代码)_图解反向传播_72:

手撸反向传播算法(附代码)_反向传播Python实现_73
手撸反向传播算法(附代码)_反向传播Python实现_74

手撸反向传播算法(附代码)_神经网络_75

手撸反向传播算法(附代码)_图解反向传播_76

最后,总式子就可以计算了:

手撸反向传播算法(附代码)_反向传播代码_77

接下来,就可以更新手撸反向传播算法(附代码)_图解反向传播_53了:

手撸反向传播算法(附代码)_神经网络_79

同理算得手撸反向传播算法(附代码)_反向传播Python实现_80:

手撸反向传播算法(附代码)_反向传播代码_81
手撸反向传播算法(附代码)_神经网络_82
手撸反向传播算法(附代码)_反向传播算法_83

终于,我们更新完一轮权值了!接下来用原始输入值来测试更新权值后的总误差是多少?经计算误差是:手撸反向传播算法(附代码)_神经网络_84 还记得未更新前的总误差吗? 手撸反向传播算法(附代码)_反向传播Python实现_85

但是在我们执行10000次更新权值的过程后:

>times=9999, lrate=0.500, error=0.000
[0.011851540581436764, 0.9878060737917571]

总误差成了手撸反向传播算法(附代码)_神经网络_86,输出是手撸反向传播算法(附代码)_反向传播Python实现_87手撸反向传播算法(附代码)_反向传播代码_88

和期望输出手撸反向传播算法(附代码)_反向传播代码_23以及手撸反向传播算法(附代码)_反向传播算法_05比是不是十分接近了。

(上面同理计算可得的地方其实我都是用代码算的,下面就贴出代码)

反向传播代码

# -*- coding: utf-8 -*
from math import exp


# 计算z
def calculate_z(weights, inputs):
z = weights[-1] # 偏差b # z = x₁w₁ + x₂w₂ + ... + b
for i in range(len(weights) - 1):
z += weights[i] * inputs[i]
return z


# 激活函数Sigmoid: σ(z) = 1 / (1 + e^(-z))
def active(z):
return 1.0 / (1.0 + exp(-z))


# 正向传播过程
def forward_pass(network, row):
inputs = row
for layer in network:
new_inputs = []
for neuron in layer:
z = calculate_z(neuron['weights'], inputs) # 计算z
neuron['output'] = active(z) # 代入激活函数得到输出 并保存到 output key中
new_inputs.append(neuron['output'])
inputs = new_inputs
return inputs


# sigmoid函数的导数 : 𝑑σ/𝑑z = σ(1-σ)
def active_derivative(output):
return output * (1.0 - output)


# 反向传播过程
def back_pass(network, expected):
for i in reversed(range(len(network))):
layer = network[i] # 从输出层开始
errors = []
if i != len(network) - 1: # 如果非输出层
for j in range(len(layer)): # 遍历所有神经元
error = 0.0
# 计算 ∂Etotal/∂a = ∂Eo₁/∂ah₁ + ∂Eo₂/∂ah₁ ...
for neuron in network[i + 1]: # 下一层的神经元
error += (neuron['weights'][j] * neuron['delta']) # 反向传播
errors.append(error)
else: # 如果是输出层
for j in range(len(layer)):
neuron = layer[j] # 遍历输出的神经元
errors.append(neuron['output'] - expected[j]) # -(target - output) 计算∂E/∂a

for j in range(len(layer)):
neuron = layer[j]
neuron['delta'] = errors[j] * active_derivative(neuron['output']) # δ = ∂E/∂zⱼ = ∂E/∂yⱼ * daⱼ/dzⱼ


# 更新参数
def update_weights(network, row, l_rate):
for i in range(len(network)):
inputs = row[:-1] # 去掉数据集中最后的类别标签
if i != 0:
inputs = [neuron['output'] for neuron in network[i - 1]] # 如果不是输入层,则更新该层的输入为上一层的输出
for neuron in network[i]:
for j in range(len(inputs)): # 入参的数量也就是权值参数的数量
neuron['weights'][j] -= l_rate * neuron['delta'] * inputs[j] # wⱼ = wⱼ - α * δ * xⱼ
neuron['weights'][-1] -= l_rate * neuron['delta'] * 1 # 同时更新了偏差b,如果更新了偏差,结果会更好


def total_error(outputs, expected):
sum_error = 0.0
for i in range(len(expected)):
sum_error += (expected[i] - outputs[i]) ** 2
return sum_error / 2.0


def test():
network = [[{'weights': [1, -2, 1]},
{'weights': [-1, 1, 1]}],
[{'weights': [2, -2, 1]},
{'weights': [-2, -1, 1]}]]

dataset = [[1, -1, None]]
n_inputs = len(dataset[0]) - 1
expected = [0.01, 0.99]
l_rate = 0.5

for times in range(10000):
sum_error = 0.0
for row in dataset:
outputs = forward_pass(network, row)
sum_error += total_error(outputs, expected)
back_pass(network, expected)
update_weights(network, row, l_rate)
print('>times=%d, lrate=%.3f, error=%.3f' % (times, l_rate, sum_error))

outputs = forward_pass(network, dataset[0])
print(outputs)


if __name__ == '__main__':
test()

有任何问题欢迎留言探讨。

参考

  1. 深度学习入门之反向传播算法
  2. ​a-step-by-step-backpropagation-example​
  3. ​李宏毅 深度学习​