一、原理
神经网络(Neural Networks)是一种模仿生物神经系统的机器学习算法。该算法的提出最早可追述至上个世纪四十年代,这几乎与电子计算机的历史同步。但它的发展并非一帆风顺,也经历了初创阶段—黄金阶段—停滞阶段—复兴阶段,直到目前的高速发展阶段。年初由Google公司开发的神经网络围棋——AlphaGo击败世界围棋冠军李世石,使神经网络技术更是受到世人的注目,因为它的意义要远大于1997年IBM的超级计算机——深蓝击败国际象棋大师卡斯帕罗夫。
图1 神经元
与生物神经系统相似,人工神经网络也是由若干个神经元构成。如图1所示,x1、x2、…xn为该神经元的输入,y为该神经元的输出。显然,不同的输入对神经元的作用是不同的,因此用权值w1、w2、…wn来表示这种影响程度的不同。神经元内部包括两个部分,第一个部分是对输入的加权求和,第二个部分是对求和的结果进行“激活”,得到输出。加权求和的公式为:
(1)
式中,b为偏移量,该偏移量也可以定义为输入恒为1的权值w0,即权值也包括偏移量,因此式1可以改写为:
(2)
激活的公式为:
(3)
式中,f(·)称为激活函数。激活函数可以有多种形式,如线性函数:
(4)
阈值函数:
(5)
Sigmoid函数:
(6)
对称Sigmoid函数:
(7)
双曲正切函数:
(8)
高斯函数:
(9)
式7和式9中的α和β均为函数的系数。
前馈神经网络是神经网络的一种,也是最常用的一种神经网络。它包括一个输入层,一个输出层和若干个隐含层,因此具有该种拓扑结构的神经网络又称为多层感知器(MLP)。如图2所示,该MLP包括一个输入层,一个输出层和一个隐含层,其中某一层的神经元只能通过一个方向连接到下一层的神经元。
图2 前馈神经网络
对于MLP,我们可以用Backprop(backward propagation oferrors,误差的反向传播,简称BP)算法实现它的建模,该算法具有结构简单、易于实现等特点。
Backprop算法是一种监督的机器学习算法,输入层的神经元数量一般为样本的特征属性的数量,输出层的神经元的数量一般为样本的所有的可能目标值的数量,如果是分类问题,则为样本的分类数量,因此,与其他机器学习算法不同,在MLP中,样本对应的响应值应该是一个相量,相量的维数与输出层的神经元的数量一致。而隐含层的层数以及各层神经元的数量则根据实际情况进行选取。Backprop算法的核心思想是:通过前向通路(箭头的方向)得到误差,再把该误差反向传播实现权值w的修正。
MLP的误差可以用平方误差函数来进行表示。设某个样本x对应的目标值为t,样本x有n个特征属性,即x={ x1, x2,…,xn},目标值t有J种可能的值,即t={ t1, t2,…,tJ},因此该MLP的输入层(即第一层)一共有n个神经元,输出层(即第L层,设MLP一共有L层)一共有J个神经元。设样本x经过前向通路得到的最终输出为y={ y1L,y2L,…,yJL },输出y的下标表示y所在层的神经元索引,上标表示y所在的层,则该样本的平方误差为:
(10)
之所以式10中的平方误差函数要除以2,是为了便于后面的求导运算,因为它并不影响误差的变化趋势。
显然,MLP算法的目标就是使E最小。由图1可知,式10中的yjL是由上一层(即第L-1层)所有神经元的输出经加权激活后得到,而第L-1层神经元的输出又由第L-2层的所有神经元的输出经加权激活后得到,因此可以说误差E是全体权值w的函数,通过改变权值w,就可达到使误差E最小的目的。
Backprop算法是一种迭代的方法,也就是我们不必通过一次改变权值w来达到使E最小的目的,我们只需渐进的减小E即可。w和E的关系可以形象的比作山坡,山的高度是误差,平面的维度空间是权值,山坡越陡(误差大),平面维度空间的变化(权值的变化)就越大,权值的变化与误差有关,而当权值改变时,误差就要重新计算。这样两者相互作用,即不断迭代,直到误差小于某个值(即收敛)为止。该方法就是我们常用的梯度下降法。
误差E对权值w的导数为w的变化率,即:
(11)
式中,η表示学习效率,它的取值在0和1之间,它起到控制收敛速度和准确性的作用。如果η过大,导致振荡,则很难收敛,如果η过小,则需要更长的时间收敛。为了改变因η选取的不好而带来的问题,又引入了被称为“动量(momentum)”的参数μ,则w的变化率改写为:
(12)
式中,t表示当前,t-1表示上一次,而t+1表示下一次,因此本次的w变化率不仅与E的导数有关,还与上次w的变化率有关。参数μ的作用是提供了一些惯性,使之平滑权值的随机波动。由Δw更新当前的权值w,即:
(13)
式13所表示的更新权值的过程是从输出层经过隐含层,向输入层逐层推进的过程,也就是误差反向传播的过程,这也就是Backprop算法名称的由来。当所有的权值更新完后,再由前向通路计算得到误差E,此时就完成了一次迭代。
下面我们就来讲解如何由前向通路得到误差E,又如何计算w的变化率,及由反向通路更新w的过程。
设wkhl表示第l层的第k个神经元与第l-1层的第h个神经元之间连接时的权值。由式1和式3可以,第l层的第k个神经元的输出ykl为:
(14)
式中,ukl表示第l层的第k个神经元的加权和,bkl表示第l层的第k个神经元的偏移量,即
(15)
这里我们设第l层一共有K个神经元,第l-1层一共有H个神经元。则第l层的所有K个神经元的加权和ul可以比较方便的用矩阵的形式表示:
(16)
则第l层的所有K个神经元输出yl为:
(17)
我们把每层的输出yl级联在一起,就构成了MLP的前向通路,则当一个样本x添加到MLP的输入层时,经过层层计算,最终得到了MLP的输出yL,把yL带入到式10,就得到了该样本x的误差。
前向通路的计算过程总结为:样本经过权值的计算后,再经过激活函数得到输出,最终得到误差,即
(18)
下面我们计算权值的变化率,即对误差求导。由链式法则可知,样本x的误差E对权值wkhl的偏导数可以表示为:
(19)
由上式可知,要想得到误差的导数,只需要计算等式右侧的三个偏导即可。上式中右侧的前两个偏导又可以定义为δkl,即
(20)
由式1可以得到式19右侧的第三个偏导:
(21)
式21表示求导的结果为第l-1层的第h个神经元的输出,如果第l-1层为输入层,则yhl-1为样本的第h个特征属性值xh。
式19右侧的第二个偏导为:
(22)
式22中的f(·)就是式4至式9给出的激活函数。式4所表示的函数的导数为:
(23)
式6所表示的函数的导数为:
(24)
式7所表示的函数的导数为:
(25)
式8所表示的函数的导数为:
(26)
式9所表示的函数的导数为:
(27)
下面计算式19右侧的第一个偏导。当ykl为输出层的输出时,即yjL,该项的偏导很简单:
(28)
因此,基于输出层的权值wjhL的误差导数,即式19为:
(29)
其中,
(30)
而当ykl为MLP内部神经元的输出时,要想计算式19右侧的第一个偏导就略显复杂。这是因为我们不知道内部神经元的输出误差,而我们只知道输出层的误差,所以我们需要把内部神经元误差传递到输出层。很显然,所有接受到前面神经元输出的后续神经元都会受到该神经元误差的影响,而后续的神经元都直接或间接的与前面神经元连接,因此内部所有神经元的误差最终都会传递到输出层的所有神经元上。
设yhl为中间第l层的第h个神经元的输出,则
(31)
式中,E(ukl+1)表示误差E是ukl+1的函数。式31表明,第l层的第h个神经元的输出误差传递到了第l+1层内的所有K个神经元内,则
(32)
由式15可以得到式32中右侧第二个偏导为:
(33)
由式20可以得到式32中右侧第一个偏导
(34)
因此,基于中间层的权值wkhl的误差导数,即式19为:
(35)
因为我们是通过反向传播的方式计算式19,即先得到最后层(即输出层)的结果,再计算倒数第二层,以此类推,所以在计算第l层误差导数时,事先一定会得到第l+1层的δkl+1,从而保证式35能够顺利计算。现在我们把式29和式35写在一起,完整的得到误差导数:
(36)
由式36就得到了基于wkhl的误差导数,再把该结果带入式11或式12中就得到了该权值的变化率,最后由式13就得到了更新后的权值。在反向传播的过程中,所有权值都经过了上述计算后,就得了更新后的所有权值。用新得到的权值计算下一个样本,以此类推,直至所有样本都经过MLP计算为止,此时就完成了所有样本的一次迭代过程。在每次迭代完成后,我们可以比较两次迭代的误差大小,如果两个误差之差满足我们的设计要求,则可以终止迭代,否则继续下次迭代。该方法也称作在线方法,因为样本是一个一个的进入MLP,每完成一个样本的计算就更新一次权值。为了增加鲁棒性,在每次迭代之前,可以把全体样本打乱顺序,这样每次迭代的过程中提取样本的顺序就会不相同。除了在线方法,还有一种方法称为批量方法,即把所有样本的误差累加在一起,用该累加误差计算误差的导数,进而得到权值的变化率。
在完成上述计算的过程之前,首先要解决的问题就是初始化权值,即第一次权值如何选择。一般的做法是随机选择很小的值作为初始权值,但这样做收敛较慢。比较好的方法是采用Nguyen-Widrow算法初始化权值。它的基本思想是每个神经元都有属于自己的一个区间范围,通过初始化权值就可以限制它的区间位置,当改变权值时,该神经元也只是在自己的区间范围内变化,因此该方法可以大大提高收敛速度。
Nguyen-Widrow算法初始化MLP权值的方法为:对于所有连接输出层的权值和偏移量,初始值为一个在正负1之间的随机数。对于中间层的权值,初始为:
(37)
式中,vh是一个在正负1之间的随机数,H为第l-1层神经元的数量。而中间层的偏移量初始为:
(38)
式中,vk是一个在正负1之间的随机数,K为第l层神经元的数量,而G为:
(39)
以上我们介绍了经典的Backprop算法,该算法还是有一些不足之处。首先是它的学习效率η是需要我们事先确定好;另外权值的变化是基于误差梯度的变化率,虽然这点乍一看,似乎没有问题,但我们不敢保证它永远正确有效。为此Riedmiller等人提出了RPROP算法(resilient backpropagation),用以改善Backprop算法。
RPROP算法的权值变化率并不是基于误差梯度的变化率,而是基于它的符号:
(40)
式中,Δ(t)为:
(41)
式中,常数η+必须大于1,常数η-必须在0和1之间。式40和式41中的∂E/∂w由式36得到。
关于式40和式41,还有一些问题需要解决,那就是Δ(t)的初始值和它的变化范围。Riedmiller等人已经给出了Δ(0)初始化为0.1是比较正确的选择,而Δmax(t)=50,Δmin(t)=10-6可以有效的防止溢出。
二、源码分析
Opencv的神经网络实现了MLP算法,具体为BACKPROP算法和RPROP算法两种,BACKPROP算法使用的是在线方法,RPROP算法使用的是批量方法。
结构CvANN_MLP_TrainParams表示MLP训练算法所需的参数,它的构造函数为:
CvANN_MLP_TrainParams::CvANN_MLP_TrainParams()
{
//表示训练迭代的终止条件,默认为迭代次数(大于1000)和权值变化率(小于0.01)
term_crit = cvTermCriteria( CV_TERMCRIT_ITER + CV_TERMCRIT_EPS, 1000, 0.01 );
//具体应用的MLP算法,默认为RPROP
train_method = RPROP;
// bp_dw_scale为式13中的η,bp_moment_scale为式13中的μ
bp_dw_scale = bp_moment_scale = 0.1;
//RPROP算法所需的参数(式40和式41),依次为Δ(0)、η+、η-、Δmin(t)、Δmax(t)
rp_dw0 = 0.1; rp_dw_plus = 1.2; rp_dw_minus = 0.5;
rp_dw_min = FLT_EPSILON; rp_dw_max = 50.;
}
CvANN_MLP_TrainParams::CvANN_MLP_TrainParams( CvTermCriteria _term_crit,
int _train_method,
double _param1, double _param2 )
{
term_crit = _term_crit;
train_method = _train_method;
bp_dw_scale = bp_moment_scale = 0.1;
rp_dw0 = 1.; rp_dw_plus = 1.2; rp_dw_minus = 0.5;
rp_dw_min = FLT_EPSILON; rp_dw_max = 50.;
if( train_method == RPROP ) //RPROP算法
{
rp_dw0 = _param1; //输入参数_param1表示Δ(0)
if( rp_dw0 < FLT_EPSILON ) //Δ(0)不能太小
rp_dw0 = 1.;
rp_dw_min = _param2; //输入参数_param2表示Δmin(t)
rp_dw_min = MAX( rp_dw_min, 0 ); //Δmin(t)不能小于0
}
else if( train_method == BACKPROP ) //BACKPROP算法
{
bp_dw_scale = _param1; //输入参数_param1表示η
//确保η在一个合理的范围内
if( bp_dw_scale <= 0 )
bp_dw_scale = 0.1;
bp_dw_scale = MAX( bp_dw_scale, 1e-3 );
bp_dw_scale = MIN( bp_dw_scale, 1 );
bp_moment_scale = _param2; //输入参数_param2表示μ
//确保μ在一个合理的范围内
if( bp_moment_scale < 0 )
bp_moment_scale = 0.1;
bp_moment_scale = MIN( bp_moment_scale, 1 );
}
//如果输入参数_train_method为除了RPROP和BACKPROP以外的值,则程序给出的算法为RPROP
else
train_method = RPROP;
}
类CvANN_MLP实现了MLP模型,不像其他的机器学习模型,MLP模型的构建和训练是由两个不同步骤来完成的。我们先给出CvANN_MLP类的一个构造函数:
CvANN_MLP::CvANN_MLP( const CvMat* _layer_sizes,
int _activ_func,
double _f_param1, double _f_param2 )
//_layer_sizes用整型相量的形式表示MLP中的一共有几层(包括一个输入层,一个输出层,以及若干个隐含层),每层有多少个神经元,相量的维数表示层数,相量元素的值表示对应层的神经元的数量
//_activ_func表示激活函数的类型,可以为CvANN_MLP::IDENTITY,CvANN_MLP::SIGMOID_SYM或CvANN_MLP::GAUSSIAN,分别表示线性激活函数(式4),对称