反向传播算法是神经网络的核心与精髓,在神经网络算法中有着举足轻重的地位。
用通俗的话说,所谓的反向传播算法,就是复合函数的链式求导法则的一个强大应用,而且实际上的应用比起理论上的推导强大得多。本节将主要介绍反向传播算法的一个简单模型的推导,虽然模型简单,但是这个简单的模型是其应用广泛的基础。
机器学习在理论上可以看作是统计学在计算机科学上的一个应用。在统计学上,一个非常重要的内容就是拟合和预测,即基于以往的数据,建立光滑的曲线模型实现数据结果与数据变量的对应关系。
深度学习为统计学的应用,同样是为了这个目的,寻找结果与影响因素的一一对应关系,只不过样本点由狭义的 x 和 y 扩展到向量、矩阵等广义的对应点。此时,由于数据的复杂性,因此对应关系模型的复杂度也随之增加,而不能使用一个简单的函数表达。
数学上通过建立复杂的高次多元函数解决复杂模型拟合的问题,但是大多数都失败,因为过于复杂的函数式是无法进行求解的,也就是其公式的获取是不可能的。
基于前人的研究,科研工作人员发现可以通过神经网络来表示一一对应关系,而神经网络本质就是一个多元复合函数,通过增加神经网络的层次和神经单元可以更好地表达函数的复合关系。
图4-13是多层神经网络的图像表达方式,通过设置输入层、隐藏层与输出层可以形成一个多元函数,用于求解相关问题。
图4-13 多层神经网络
通过数学表达式将多层神经网络模型表达出来,公式如下:
其中 x 是输入数值,而 w 是相邻神经元之间的权重,也就是神经网络在训练过程中需要学习的参数。与线性回归类似的是,神经网络学习同样需要一个损失函数,训练目标通过调整每个权重值 w 来使得损失函数最小。前面在讲解梯度下降算法的时候已经讲过,如果权重过大或者指数过大,那么直接求解系数是一件不可能的事情,因此梯度下降算法是求解权重问题的比较好的方法。
在前面梯度下降算法的介绍中,没有对其背后的原理做出更为详细的介绍。实际上,梯度下降算法就是链式法则的一个具体应用,如果把前面公式中的损失函数以向量的形式表示为:
那么其梯度向量为:
可以看到,其实所谓的梯度向量就是求出函数在每个向量上的偏导数之和。这也是链式法则擅长处理的问题。
下面以 e =( a + b )×( b +1),其中 a = 2, b = 1为例,计算其偏导数,如图4-14所示。
图4-14 计算偏导数
本例中为了求得最终值 e 对各个点的梯度,需要将各个点与 e 联系在一起,例如期望求得 e 对输入点 a 的梯度,则只需求得:
这样就把 e 与 a 的梯度联系在一起了,同理可得:
用图表示如图4-15所示。
图4-15 链式法则的应用
这样做的好处是显而易见的,求 e 对 a 的偏导数只要建立一个 e 到 a 的路径,图中经过 c ,那么通过相关的求导链接就可以得到所需要的值。对于求 e 对 b 的偏导数,也只需要建立所有 e 到 b 的路径中的求导路径,从而获得需要的值。
在求导过程中,可能有读者已经注意到,如果拉长了求导过程或者增加了其中的单元,就会大大增加其中的计算过程,即很多偏导数的求导过程会被反复计算,因此在实际应用中对于权值达到十万或者百万以上的神经网络来说,这样的重复冗余所导致的计算量是很大的。
同样是为了求得对权重的更新,反馈神经网络算法将训练误差 E 看作以权重向量每个元素为变量的高维函数,通过不断更新权重寻找训练误差的最低点,按误差函数梯度下降的方向更新权值。
提示: 反馈神经网络算法的具体计算公式在本小节后半部分进行推导。
首先求得最后的输出层与真实值之间的差距,如图4-16所示。
图4-16 反馈神经网络最终误差的计算
之后以计算出的测量值与真实值为起点,反向传播到上一个节点,并计算出节点的误差值,如图4-17所示。
图4-17 反馈神经网络输出层误差的反向传播
以后将计算出的节点误差重新设置为起点,依次向后传播误差,如图4-18所示。
图4-18 反馈神经网络隐藏层误差的反向传播
图4-18 反馈神经网络隐藏层误差的反向传播(续)
注意: 对于隐藏层,误差并不是像输出层一样由单个节点确定的,而是由多个节点确定的,因此对它的计算要求得所有的误差值之和。
通俗地解释,一般情况下误差的产生是由于输入值与权重的计算产生了错误,而输入值往往是固定不变的,因此对于误差的调节,需要对权重进行更新。而权重的更新又是以输入值与真实值的偏差为基础的,当最终层的输出误差被反向一层一层地传递回来后,每个节点都会被相应地分配适合其所处的神经网络地位的误差,即只需要更新其所需承担的误差量,如图4-19所示。
图4-19 反馈神经网络权重的更新
即在每一层,需要维护输出对当前层的微分值,该微分值相当于被复用于之前每一层中权值的微分计算。因此,空间复杂度没有变化。同时,也没有重复计算,每一个微分值都在之后的迭代中使用。
下面介绍公式的推导。公式的推导需要使用一些高等数学的知识,因此读者可以自由选择学习。
首先进行算法的分析,前面已经讲过,对于反馈神经网络算法,主要需要知道输出值与真实值之间的差值。
· 对于输出层单元,误差项是真实值与模型计算值之间的差值。
· 对于隐藏层单元,由于缺少直接的目标值来计算隐藏层单元的误差,因此需要以间接的方式来计算隐藏层的误差项,对受隐藏层单元影响的每一个单元的误差进行加权求和。
· 权值的更新方面,主要依靠学习速率、该权值对应的输入以及单元的误差项。
对于前向传播的值传递,隐藏层输出值定义如下:
其中
X
i
是当前节点的输入值,
是连接到此节点的权重,
是输出值。
f
是当前阶段的激活函数,
为当前节点的输入值经过计算后被激活的值。
而对于输出层,定义如下:
其中
hk
W
为输入的权重,
为将节点输入数据经过计算后的激活值作为输入值。这里将所有输入值进行权重计算后求得的值作为神经网络的最后输出值
a
k
。
与前向传播类似,首先需要定义两个值:
δ
k
与
:
其中
δ
k
为输出层的误差项,其计算值为真实值与模型计算值之间的差值。
Y
是计算值,
T
是真实值。
为输出层的误差。
提示:
对于
δ
k
与
来说,无论定义在哪个位置,都可以看作当前的输出值对于输入值的梯度计算。
通过前面的分析可以知道,所谓的神经网络反馈算法,就是逐层地将最终误差进行分解,即每一层只与下一层打交道,如图4-20所示。那么,据此可以假设每一层均为输出层的前一个层级,通过计算前一个层级与输出层的误差得到权重的更新。
图4-20 权重的逐层反向传导
因此,反馈神经网络计算公式定义为:
即当前层的输出值对误差的梯度可以通过下一层的误差与权重和输入值的梯度乘积获得。若公式
中的
δ
k
为输出层,则可以通过
求得,若
δ
k
为非输出层,则可使用逐层反馈的方式求得。
提示:
这里一定要注意,对于
δ
k
与
来说,其计算结果都是当前的输出值对于输入值的梯度计算,是权重更新过程中一个非常重要的数据计算内容。
也可以将前面的公式表示为:
可以看到,通过更为泛化的公式,可以把当前层的输出对输入的梯度计算转换成求下一个层级的梯度计算值。
反馈神经网络计算的目的是对权重的更新,因此与梯度下降算法类似,其更新可以仿照梯度下降对权值的更新公式:
即:
其中
ji
表示反向传播时对应的节点系数,通过对
的计算,就可以更新对应的权重值。
W
ji
的计算公式如上所示。而对于没有推导的
b
ji
,其推导过程与
W
ji
类似,但是在推导过程中输入值是被消去的,请读者自行学习。
现在回到反馈神经网络的函数:
对于此公式中的
和
以及需要计算的目标
δ
l
已经做了较为详尽的解释。但是一直没有对
做出介绍。
回到前面生物神经元的图示,传递进来的电信号通过神经元进行传递,由于神经元的突触强弱是有一定的敏感度的,因此只会对超过一定范围的信号进行反馈,即这个电信号必须大于某个阈值,神经元才会被激活引起后续的传递。
在训练模型中同样需要设置神经元的阈值,即神经元被激活的频率用于传递相应的信息,模型中这种能够确定是否为当前神经元节点的函数被称为激活函数,如图4-21所示。
图4-21 激活函数示意图
激活函数代表了生物神经元中接收到的信号的强度,目前应用范围较广的是Sigmoid函数。因为其在运行过程中只接收一个值,所以输出也是一个经过公式计算的值,且其输出值的范围为0~1。
其图形如图4-22所示。
图4-22 Sigmoid激活函数图
而其倒函数的求法也较为简单,即:
换一种表示方式为:
Sigmoid输入一个实值的数,之后将其压缩到0~1。特别是对于较大值的负数被映射成0,而大的正数被映射成1。
顺便讲一下,Sigmoid函数在神经网络模型中占据了很长时间的统治地位,但是目前已经不常使用,主要原因是其非常容易区域饱和,当输入非常大或者非常小的时候,Sigmoid会产生一个平缓区域,其中的梯度值几乎为0,而这又会造成在梯度传播过程中产生接近0的传播梯度。这样在后续的传播过程中会造成梯度消散的现象,因此并不适合现代的神经网络模型使用。
除此之外,近年来涌现出了大量新的激活函数模型,例如Maxout、Tanh和ReLU模型,这些都是为了解决传统的Sigmoid模型在更深程度的神经网络所产生的各种不良影响。
提示: Sigmoid函数的具体使用和影响会在第5章详细介绍。
经过前面的介绍,读者应该对神经网络的算法和描述有了一定的理解,本小节将使用Python代码实现一个反馈神经网络。
为了简化起见,这里的神经网络被设置成三层,即只有一个输入层、一个隐藏层以及最终的输出层。
(1)辅助函数的确定:
def rand(a, b): return (b - a) * random.random() + a def make_matrix(m,n,fill=0.0): mat = [] for i in range(m): mat.append([fill] * n) return mat def sigmoid(x): return 1.0 / (1.0 + math.exp(-x)) def sigmod_derivate(x): return x * (1 - x)
代码首先定义了随机值,使用random包中的random函数生成了一系列随机数,之后的make_matrix函数生成了相对应的矩阵。sigmoid和sigmod_derivate分别是激活函数和激活函数的导函数。这也是前文所定义的内容。
(2)进入BP神经网络类的正式定义,类的定义需要对数据进行内容的设定。
def __init__(self): self.input_n = 0 self.hidden_n = 0 self.output_n = 0 self.input_cells = [] self.hidden_cells = [] self.output_cells = [] self.input_weights = [] self.output_weights = []
init函数是数据内容的初始化,即在其中设置了输入层、隐藏层以及输出层中节点的个数;各个cell数据是各个层中节点的数值;weights数据代表各个层的权重。
(3)使用setup函数对init函数中设定的数据进行初始化。
def setup(self,ni,nh,no): self.input_n = ni + 1 self.hidden_n = nh self.output_n = no self.input_cells = [1.0] * self.input_n self.hidden_cells = [1.0] * self.hidden_n self.output_cells = [1.0] * self.output_n self.input_weights = make_matrix(self.input_n,self.hidden_n) self.output_weights = make_matrix(self.hidden_n,self.output_n) # random activate for i in range(self.input_n): for h in range(self.hidden_n): self.input_weights[i][h] = rand(-0.2, 0.2) for h in range(self.hidden_n): for o in range(self.output_n): self.output_weights[h][o] = rand(-2.0, 2.0)
需要注意,输入层节点的个数被设置成ni+1,这是由于其中包含bias偏置数;各个节点与1.0相乘是初始化节点的数值;各个层的权重值根据输入层、隐藏层以及输出层中节点的个数被初始化并被赋值。
(4)定义完各个层的数目后,下面进入正式的神经网络内容的定义。首先是对神经网络前向的计算。
def predict(self,inputs): for i in range(self.input_n - 1): self.input_cells[i] = inputs[i] for j in range(self.hidden_n): total = 0.0 for i in range(self.input_n): total += self.input_cells[i] * self.input_weights[i][j] self.hidden_cells[j] = sigmoid(total) for k in range(self.output_n): total = 0.0 for j in range(self.hidden_n): total += self.hidden_cells[j] * self.output_weights[j][k] self.output_cells[k] = sigmoid(total) return self.output_cells[:]
代码段中将数据输入函数中,通过隐藏层和输出层的计算,最终以数组的形式输出。案例的完整代码如下。
【程序4-3】
import numpy as np import math import random def rand(a, b): return (b - a) * random.random() + a def make_matrix(m,n,fill=0.0): mat = [] for i in range(m): mat.append([fill] * n) return mat def sigmoid(x): return 1.0 / (1.0 + math.exp(-x)) def sigmod_derivate(x): return x * (1 - x) class BPNeuralNetwork: def __init__(self): self.input_n = 0 self.hidden_n = 0 self.output_n = 0 self.input_cells = [] self.hidden_cells = [] self.output_cells = [] self.input_weights = [] self.output_weights = [] def setup(self,ni,nh,no): self.input_n = ni + 1 self.hidden_n = nh self.output_n = no self.input_cells = [1.0] * self.input_n self.hidden_cells = [1.0] * self.hidden_n self.output_cells = [1.0] * self.output_n self.input_weights = make_matrix(self.input_n,self.hidden_n) self.output_weights = make_matrix(self.hidden_n,self.output_n) # random activate for i in range(self.input_n): for h in range(self.hidden_n): self.input_weights[i][h] = rand(-0.2, 0.2) for h in range(self.hidden_n): for o in range(self.output_n): self.output_weights[h][o] = rand(-2.0, 2.0) def predict(self,inputs): for i in range(self.input_n - 1): self.input_cells[i] = inputs[i] for j in range(self.hidden_n): total = 0.0 for i in range(self.input_n): total += self.input_cells[i] * self.input_weights[i][j] self.hidden_cells[j] = sigmoid(total) for k in range(self.output_n): total = 0.0 for j in range(self.hidden_n): total += self.hidden_cells[j] * self.output_weights[j][k] self.output_cells[k] = sigmoid(total) return self.output_cells[:] def back_propagate(self,case,label,learn): self.predict(case) #计算输出层的误差 output_deltas = [0.0] * self.output_n for k in range(self.output_n): error = label[k] - self.output_cells[k] output_deltas[k] = sigmod_derivate(self.output_cells[k]) * error #计算隐藏层的误差 hidden_deltas = [0.0] * self.hidden_n for j in range(self.hidden_n): error = 0.0 for k in range(self.output_n): error += output_deltas[k] * self.output_weights[j][k] hidden_deltas[j] = sigmod_derivate(self.hidden_cells[j]) * error #更新输出层的权重 for j in range(self.hidden_n): for k in range(self.output_n): self.output_weights[j][k] += learn * output_deltas[k] * self.hidden_cells[j] #更新隐藏层的权重 for i in range(self.input_n): for j in range(self.hidden_n): self.input_weights[i][j] += learn * hidden_deltas[j] * self.input_cells[i] error = 0 for o in range(len(label)): error += 0.5 * (label[o] - self.output_cells[o]) ** 2 return error def train(self,cases,labels,limit = 100,learn = 0.05): for i in range(limit): error = 0 for i in range(len(cases)): label = labels[i] case = cases[i] error += self.back_propagate(case, label, learn) pass def test(self): cases = [ [0, 0], [0, 1], [1, 0], [1, 1], ] labels = [[0], [1], [1], [0]] self.setup(2, 5, 1) self.train(cases, labels, 10000, 0.05) for case in cases: print(self.predict(case)) if __name__ == '__main__': nn = BPNeuralNetwork() nn.test()