引言
根据上篇文章介绍的反向传播算法理论,我们今天来手动计算一下。
我们简化下上篇文章中的NN模型(参数和它类似,这里把每个偏差值设成1),使它的隐藏层只有1层。它有两个输入和两个输出。

我们有初始化权重、偏差和训练用的输入和输出值。
反向传播算法的目的是优化权值
来最小化损失函数,从而使NN预测的输出和实际输出更匹配。
假设给定一个训练集,它的输入是
,我们想要NN输出
和
正向过程(Forward Pass)
首先我们看看这个NN在初始权值和训练数据下的表现。我们会计算出每个隐藏神经元的输入,通过激活函数(这里用Sigmoid函数)转换成下一层的输入,直到达到输出层计算出最终输出。
先来计算隐藏层
的输入,

然后将它代入激活函数(这里用Sigmoid函数),得到隐藏层
的输出,
,
同理有

可得
这两个输出又可作为它们的下一层的输入,计算输出层
的输出如下两个步骤:


输出层
的输出也是同理:


可以看到,在初始参数上的输出和我们的目标
还是有不小距离的。
接下来,我们计算这不小距离到底有多么不小。
计算总误差(Total Error)
我们使用均方误差来计算总误差:

其中
就是输出层的实际输出,而
则是它的期望输出。
比如
神经元的期望输出是
,但是实际输出是
,所以误差是:

使用同样的过程计算出
:

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

这个距离是真的不小啊。
反向过程
反向传播算法的目的是更高效的计算梯度,从而更新参数值,使得总误差更小,也就是使实际输出更贴近我们期望输出。它是作为一个整体去更新整个神经网络的。
反向就是先考虑输出层,然后再考虑它的上一层,并重复这个过程。因此,我们先从输出层开始计算。
输出层
考虑参数
,我们想知道
的改变会对总误差有多大的影响,即计算
。


我们需要计算这个等式中的每个式子,首先计算
如何影响总误差


接下来计算
我们知道
所以 
最后是最简单的


最后放到一起得:

通常一般定义 
因此 
为了减少误差,通常需要减少当前权值,如下:

学习率
一般取值
,当然是需要根据实际情况去调整的。
同理可算得
:



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

我们要用和更新输出层参数类似的步骤来更新隐藏层的参数,但有一点不同的是,每个隐藏层的神经元都影响了多个输出层(或下一层)神经元的输出。
同时影响了
和
,因此计算
需要将输出层的两个神经元都考虑在内:

从
开始:

其实我们上面已经算过
了:

并且
:


也就是:

同理,可得
因此:

现在我们已经知道了
,我们还需要计算
和
:




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

接下来,就可以更新
了:

同理算得
:



终于,我们更新完一轮权值了!接下来用原始输入值来测试更新权值后的总误差是多少?经计算误差是:
还记得未更新前的总误差吗? 
但是在我们执行10000次更新权值的过程后:
>times=9999, lrate=0.500, error=0.000
[0.011851540581436764, 0.9878060737917571]
总误差成了
,输出是
和
和期望输出
以及
比是不是十分接近了。
(上面同理计算可得的地方其实我都是用代码算的,下面就贴出代码)
反向传播代码
# -*- 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()
有任何问题欢迎留言探讨。
参考
- 深度学习入门之反向传播算法
- a-step-by-step-backpropagation-example
- 李宏毅 深度学习