反向传播是多数神经网络进行参数更新的基本方法,它的本质是巧妙利用了高数中的链式法则,下面对这一方法进行推导:
(1)符号说明
:神经元的激活函数
:神经网络的权重向量
:神经网络的偏置向量
:某层的输入向量
:某层的输出向量
(2)损失函数
假设神经网络的损失函数为
,那么定义其损失函数为:
其中,
为期望的输出值,
是神经网络的预测输出值。
方向传播算法是通过改变网络中的权重参数
和偏置
来改变损失函数的方法。
(3)4个基本方程推导
1. 输出层误差
首先定义第
层的第
个神经元的误差为:
在这里可能很多人会觉得不能理解,具体可以参考这篇文章 http://neuralnetworksanddeeplearning.com/chap2.html
根据定义,对损失函数求导可得:
其中
度量了损失函数在神经元输出的函数的变化程度,
则度量了激活函数在神经元加权输入处的变化程度。
对输出层误差向量化,得到输出层的误差公式BP1,如下:
2. 第
层误差
非输出层的误差依赖于下一层的误差,也就是网络第
层的误差依赖于第
层的误差,则有:
上式利用了链式法则,前一次的输出可以作用于后一层的输入,又因为:
所以有:
带入之前的算是可以得到网络第
层的第
个神经元误差为:
向量化后可以得到网络中第1层的误差公式BP2:
公式BP2充分体现了误差反向传播的特点,只要知道
层的误差就可以推导出
层的误差,以此类推,最后能够从网络的输出层倒推直到输入层的误差,这就是误差反向传播的含义,另外从误差传播的公式中我们可以注意到含有激活函数的导师,一般我们采用的sigmoid的激活函数的导数最大为0.25,每反向传播一次梯度最多保存25%,因此当神经网络隐藏层增多后会产生梯度消失问题。
3. 损失函数关于偏置
的偏导
同理,根据链式求导法则可得:
又因为:
带入得到:
最终损失函数在网络中任意偏置等于该神经元上的误差,可以通过误差直接得到偏置的倒数。
4. 损失函数关于权值
的偏导
从公式中可以知道损失函数关于权重的偏导,连接了第1层第j个神经元的误差以及上一层第i个神经元的输出。
至此反向传播的4个基本公式都已经推导完毕。
(4)反向传播算法的实现代码示例
import numpy as np
import pprint
pp = pprint.PrettyPrinter(indent=4)
# 定义神经网络的模型架构 [input, hidden, output]
network_sizes = [3,4,2]
# 初始化该神经网络的参数
sizes = network_sizes
num_layers = len(sizes)
biases = [np.random.randn(h, 1) for h in sizes[1:]]
weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
def loss_der(network_y, real_y):
"""
返回损失函数的偏导,损失函数使用 MSE
L = 1/2(network_y-real_y)^2
delta_L = network_y-real_y
"""
return (network_y - real_y)
def sigmoid(z):
"""激活函数使用 sigmoid."""
return 1.0 / (1.0 + np.exp(-z))
def sigmoid_der(z):
"""sigmoid函数的导数 derivative of sigmoid."""
return sigmoid(z) * (1 - sigmoid(z))
def backprop(x, y):
"""
根据损失函数 C通过反向传播算法返回
"""
"""Return a tuple "(nabla_b, nabla_w)" representing the
gradient for the cost function C_x. "nabla_b" and
"nabla_w" are layer-by-layer lists of numpy arrays, similar
to "self.biases" and "self.weights"."""
# 初始化网络参数的导数 权重w的偏导和偏置b的偏导
delta_w = [np.zeros(w.shape) for w in weights]
delta_b = [np.zeros(b.shape) for b in biases]
# 向前传播 feed forward
activation = x # 把输入的数据作为第一次激活值
activations = [x] # 存储网络的激活值
zs = [] # 存储网络的加权输入值 (z=wx+b)
for w, b in zip(weights, biases):
z = np.dot(w, activation) + b
activation = sigmoid(z)
activations.append(activation)
zs.append(z)
# 反向传播 back propagation
# BP1 计算输出层误差
delta_L = loss_der(activations[-1], y) * sigmoid_der(zs[-1])
# BP3 损失函数在输出层关于偏置的偏导
delta_b[-1] = delta_L
# BP4 损失函数在输出层关于权值的偏导
delta_w[-1] = np.dot(delta_L, activations[-2].transpose())
delta_l = delta_L
for l in range(2, num_layers):
# BP2 计算第l层误差
z = zs[-l]
sp = sigmoid_der(z)
delta_l = np.dot(weights[-l + 1].transpose(), delta_l) * sp
# BP3 损失函数在l层关于偏置的偏导
delta_b[-l] = delta_l
# BP4 损失函数在l层关于权值的偏导
delta_w[-l] = np.dot(delta_l, activations[-l - 1].transpose())
return (delta_w, delta_b)
training_x = np.random.rand(3).reshape(3,1)
training_y = np.array([0, 1]).reshape(2,1)
print("training data x:\n{},\n training data y:\n{}".format(training_x, training_y))
backprop(training_x, training_y)
输出的结果为:
training data x:
[[0.69316998]
[0.16969983]
[0.51637141]],
training data y:
[[0]
[1]]
([array([[-0.00122639, -0.00030024, -0.00091359],
[-0.0252308 , -0.00617693, -0.01879548],
[-0.0039671 , -0.00097121, -0.00295526],
[ 0.01921057, 0.00470307, 0.01431076]]),
array([[ 0.05610549, 0.02035511, 0.06627431, 0.01132567],
[-0.11372293, -0.04125875, -0.13433461, -0.02295656]])],
[array([[-0.00176925],
[-0.03639915],
[-0.00572312],
[ 0.02771408]]), array([[ 0.07169137],
[-0.14531471]])])
参考:
《深度学习原理与实践》第2章