BP神经网络原理的粗浅解释以及python实现

来源:互联网 发布:手机淘宝不能搜索宝贝 编辑:程序博客网 时间:2024/05/17 04:56

本文仅为博主对BP神经网络的个人理解,特别在矩阵部分自觉有些东西还不是理解太透彻,希望能与大家就各个不明晰的点进行交流。


1.BP网络模型的两个理解出发点


从生物学角度,神经网络可以认为是对人脑真正神经工作的一种简化模型。外界的每一次刺激,都会激发与感受器(视觉感受器,听觉感受器······)相连的若干神经元产生电位变化,这种变换又随着神经元的连接一层层传递下去,最终来到大脑皮层。将这一过程抽象化后,就构成了BP神经网络里那个经典的图像:



而外界每一次不同的刺激,激活的神经元们都不会完全相同,这可对应于每次训练传入不同样本;某些神经元之间经常发生电位变换传递,这两个神经元之间的连接就会加强,反之减弱,这可对应于连接weight的设置;神经元的电位变化传递是非线性的,即上一个神经元的电位变化如果超过一定阈值,即使再增加,下一个神经元的电位变化也不会跟着增加,而是维持在一个水平,或是增加幅度非常小,这可对应于激活函数;每个神经元即使没被上一个激活,自身也不是完全静默的,这可对应于神经元的偏置值。
激活输入层的一批节点,“电位”的变化最终会反映到输出节点上,而我们若是希望给定特定的输入组合能反映一个我们预期的输出组合,我们就需要像巴普洛夫的条件反射实验一样,对网络得到正确结果时给予奖励,错误结果时给予惩罚。具体到施行方法,则是计算某节点输出与预期输出差距,并回溯该节点的输出是由哪些上层神经元的输入共同给予的,如果输出偏大,则降低那些上层到自身连接的权重,反之增加,一层层往前推。在这个过程中还要注意,1由于正向传递时每层间存在一个非线性的激活函数,所以反向推进时这个函数要对应到激活函数的导函数(代码中计算delta部分类似(1-value)*value这种形式),2一个单位的误差要对应多大程度的权重修改力度(学习率)。整个反向推进的过程就是前馈法。


从数学角度,BP神经网络则可以看做是(传入一个样本时)一个向量在高维空间进行非线性变换(激活函数的存在引入非线性)到另一个向量。1989年Robert Hecht-Nielsen证明了对于任何闭区间内的一个连续函数都可以用一个隐含层的BP网络来逼近。对于这点的理解,可以参考下面这个神经网络中常见的公式:

这里将每层间的连接权重写成了矩阵形式,对应到一开始那个点线图,wij的每一行就是上一层的每一个节点,而每行(以第一行为例)的w11,w12....w1n分别对应上层第一个节点到下层n个节点的连接权重,如果对线性代数还记得一些,就会明白这个线性变换的过程(推荐B站的这个视频:【双字/合集】“线性代数的本质”系列合集

忽略偏置bj,将sj值域用激活函数映射到(0,1)区间,就完成了下层节点value的一次计算。
在前馈过程中,我们还会经常接触到一个名词,叫梯度下降法。其实梯度下降法只是一个手段,最终目的是调整weight和bias使得神经网络尽快尽准确的达到理想中的状态,而达到这种状态的标志就是实际输出与预期输出的误差很小。我们在高中就知道,求一个函数最小值的通用手段就是求导,求导数等于0的点就是极大/小值点(高中范围)。虽然对神经网络这么一个复杂的,高维度的巨型函数没法直接算导数为0点,但我们至少可以揪住这个巨型函数的某一个维度,在这个维度上求导(偏导),并看看当前所处的位置是在一个下坡上(当前导数值<0)还是在上坡上(当前导数值>0),在下坡上就接着往下走,在上坡上就往回退,尽量往最低点走。对每个维度都这么处理,多处理几次就能使整体的误差达到极小值(可能不是最小值,这也是神经网络的缺陷之一,而且也有可能本维度这么走是往下在另一个维度却是往上走了),公式如下,详细的数学推导可见此BP神经网络

(隐藏-输出层)


(输入-隐藏层)

然而有时候,神经网络要拟合更复杂的函数,或是对更复杂的样点分布情况进行分类(想象一下,一缸水中有一只猫,你可能需要一个猫型的闭合面才能完美的将猫和水分开),这时候增加隐藏层层数就变得有意义起来,但隐藏层数过多也会带来残差传递能力不够的问题(或许可以类比为长途信号的衰减?)


2.对标准BP网络的改进

1)自适应学习率

学习率过低,在初期会拖慢神经网络的收敛速度;学习率过高,在收敛末期会会导致震荡(可以这样理解,一个人想从山丘下到地面,但他一步就是1000米,结果一步就又蹿上了另一个山丘,下次还是如此,于是就在两个山丘之间跑来跑去)。于是让学习率动态设置:本轮的总体误差比上轮小,则稍微增大学习率;本轮的总体误差比上轮大,则稍微减小学习率。

2)动量项

在修改连接weight的时候,相邻的两轮修改方案常常不是完全独立的,于是我们让上一轮的weight修改情况适当的指导本轮,这就引入了动量项,传统的动量项形式如下:



前一项是传统的梯度下降法,后一项的α为动量因子,取0~1的值。
如果将上式写成这个形式:y[n] = -ηx[n] + αy[n-1],学过数字信号处理的朋友可能会觉得很熟悉,这是一阶数字低通滤波器的差分方程形式,再联系之前提到的,神经网络的收敛就是不断下坡,如果坡路上有凹凸不平的坑包,就可能陷在某个局部最低点,而用低通滤波器滤去这些“高频”噪声,可使坡路变得平缓起来。
项目中采用的是改进的变动量项算法,形式为:



详细内容请阅读此文献带动量项的BP神经网络收敛性分析

3)其他

惩罚函数可在误差的基础上,加入对网络复杂程度的表示(正则项),具体可参看带惩罚项的BP神经网络训练算法的收敛性 。还有自适应学习率的具体调整方案,隐含层节点数的经验公式,初始参数的归一化,对梯度下降法改进的共轭梯度法,拟牛顿法等等。


3.BP神经网络的Python实现

1)项目代码

#!/usr/bin/env python3# -*- coding: utf-8 -*-"""Created on Tue Jan 10 16:40:45 2017@author: liu"""import randomimport numpy as npimport math#初始化是为各连接赋随机weight值,以及bias值(满足标准高斯分布)def randomweight():    return np.random.randn()#激活函数使用sigmoid函数,形状大概是个S型def sigmoid(x):    return 1 / (1+math.exp(-x))    #计算两个向量的余弦相似度def cos(vector1,vector2):    dot_product = 0    normA = 0    normB = 0    for a,b in zip(vector1,vector2):        dot_product += a*b        normA += a**2        normB += b**2    if normA == 0 or normB==0:        return 0    else:        return (dot_product / (math.sqrt(normA*normB)))    '''变量解释weight:为列表形式,长度为下一层的节点数wdeltasum, bdeltasum:本bp网络采用的是批量学习,即一次传过去多个样本,再进行一次反向推进,于是就必须记录这几个样本的delta和lastchange:动量项的本次调整需要上次调整的信息记录'''      class inputnode:    def __init__(self, weight, lastchange, value=0, wdeltasum=0):        self.value = value        self.weight = weight        self.lastchange = lastchange        self.wdeltasum = wdeltasum        class outputnode:    def __init__(self, value=None, rightvalue=None, delta=None, bias=None, bdeltasum=0):        self.value = value        self.rightvalue = rightvalue        self.delta = delta        self.bias = bias        self.bdeltasum = bdeltasum        class hiddennode:    def __init__(self, weight, lastchange, value=0, delta=None, bias=None, bdeltasum=0, wdeltasum=[]):        self.value = value        self.delta = delta        self.bias = bias        self.bdeltasum = bdeltasum        self.weight = weight        self.lastchange = lastchange        self.wdeltasum = wdeltasum#初始化时建立好所有的节点和神经元以及连接'''变量解释hnodenum:隐藏层每层的神经元数量hlayernum:隐藏层层数elelist:所有特征的列表,其长度也即输入层节点数onodenum:输出层节点数errorsum:每轮训练后的总体误差,是判断是否收敛的标志'''      class neutralnet:    def __init__(self, hnodenum, hlayernum, elelist, onodenum):        self.inlayer = []        self.hiddenlayer = []        self.outlayer = []        self.hnodenum = hnodenum        self.hlayernum = hlayernum        self.elelist = elelist #其实这里并不需要传整个特征列表进来,传特征数量就够了        self.onodenum = onodenum        self.errorsum = 100 #初始化时设一个极大值就可以        for item in self.elelist:            weight = []; wdeltasumn = []; lastchange = []            for k in range(self.hnodenum):                weight.append(randomweight()/len(elelist))                lastchange.append(0)                wdeltasumn.append(0)            self.inlayer.append(inputnode(weight, lastchange, wdeltasum=wdeltasumn))                    lastlayer = []        for layerindex in range(self.hlayernum):            if layerindex == self.hlayernum-1:                for nodeindex in range(self.hnodenum):                    weight = []; wdeltasumn = []; lastchange = []                    for outindex in range(self.onodenum):                        weight.append(randomweight()/onodenum)                        lastchange.append(0)                        wdeltasumn.append(0)                    newhiddennode = hiddennode(weight, lastchange, bias=randomweight()/hnodenum, wdeltasum=wdeltasumn)                    lastlayer.append(newhiddennode)            else:                thislayer = []                for nodeindex in range(self.hnodenum):                    weight = []; wdeltasumn = []; lastchange = []                    for nlnodeindex in range(self.hnodenum):                        weight.append(randomweight()/hnodenum)                        lastchange.append(0)                        wdeltasumn.append(0)                    newhiddennode = hiddennode(weight, lastchange, bias=randomweight()/hnodenum, wdeltasum=wdeltasumn)                    thislayer.append(newhiddennode)                self.hiddenlayer.append(thislayer)        self.hiddenlayer.append(lastlayer)                        for outindex in range(self.onodenum):            newoutnode = outputnode(bias=randomweight()/onodenum)            self.outlayer.append(newoutnode)    '''    变量解释    sum:隐藏层及输出层的每个节点都需计算,为节点所在层的上一层,=sigmoid{求和(每个上层点value*上层点到该点weight)+该点bias}    bias:偏置,隐藏层及输出层的每个节点均具有    '''        def fPropagation(self):        for layerindex in range(self.hlayernum):            if layerindex == 0:                i = 0                for hiddennode in self.hiddenlayer[layerindex]:                    sum = 0;                    for innodeindex in range(len(self.elelist)):                        sum += self.inlayer[innodeindex].value * self.inlayer[innodeindex].weight[i]                    i += 1                    sum += hiddennode.bias                    hiddennode.value = sigmoid(sum)            else:                i = 0                for hiddennode in self.hiddenlayer[layerindex]:                    sum = 0                    for hnodeindex in range(self.hnodenum):                        sum += self.hiddenlayer[layerindex-1][hnodeindex].value *\                         self.hiddenlayer[layerindex-1][hnodeindex].weight[i]                    i += 1                    sum += hiddennode.bias                    hiddennode.value = sigmoid(sum)        i = 0                   for outindex in range(self.onodenum):            sum = 0            for hiddennode in self.hiddenlayer[self.hlayernum-1]:                sum += hiddennode.value * hiddennode.weight[i]            i += 1            sum += self.outlayer[outindex].bias            self.outlayer[outindex].value = sigmoid(sum)    '''    变量解释    delta=δ*tanh,对应的就是梯度下降法里的求偏导    temp:输出值与理想输出值之差,注意不要取绝对值,只有保留符号才能使weight向正确的方向调整    sum:除输出层外,输入层和隐藏层每个节点计算方法为 求和(本层该节点到下层各节点weight*下层对应节点delta) ,意义是从下层反向传来的delta加权和    wdeltasum:列表类型,输入层和隐藏层每个节点均具备,为 该节点value*下一层对应节点delta,起到对weight的反向修正    bdeltasum:输出层和隐藏层每个节点均具备,其值只与节点自身的delta有关,起到对bias的反向修正    wdeltasum和bdeltasum在每轮训练前都会请空,而在一轮训练的多个样本中是不断累加的,虽然最后对weight等量的修正值已经除以了样本数,但我们提倡多轮训练小样本,而不是一次传进所有训练样本    '''        def bPropagation(self):        for outnode in self.outlayer:            temp = outnode.rightvalue - outnode.value            self.errorsum += temp**2/2            delta = temp * (1-outnode.value) * outnode.value             outnode.delta = delta                for layerindex in reversed(range(self.hlayernum)):            if layerindex == self.hlayernum-1:                for hiddennode in self.hiddenlayer[layerindex]:                    sum = 0; i = 0                    for outnode in self.outlayer:                        sum += outnode.delta * hiddennode.weight[i]                        i += 1                    hiddennode.delta = sum * (1-hiddennode.value) * hiddennode.value            else:                for hiddennode in self.hiddenlayer[layerindex]:                    sum = 0; i = 0                    for nlnode in self.hiddenlayer[layerindex+1]:                        sum += nlnode.delta * hiddennode.weight[i]                        i += 1                    hiddennode.delta = sum * (1-hiddennode.value) * hiddennode.value        for inputnode in self.inlayer:            for hnodeindex in range(self.hnodenum):                inputnode.wdeltasum[hnodeindex] += self.hiddenlayer[0][hnodeindex].delta * inputnode.value        for layerindex in range(self.hlayernum):            if layerindex == self.hlayernum-1:                for hiddennode in self.hiddenlayer[layerindex]:                    hiddennode.bdeltasum += hiddennode.delta                    for outindex in range(self.onodenum):                        hiddennode.wdeltasum[outindex] += self.outlayer[outindex].delta * hiddennode.value            else:                for hiddennode in self.hiddenlayer[layerindex]:                    hiddennode.bdeltasum += hiddennode.delta                    for hnodeindex in range(self.hnodenum):                        hiddennode.wdeltasum[hnodeindex] += self.hiddenlayer[layerindex+1][hnodeindex].delta * hiddennode.value                    for outnode in self.outlayer:            outnode.bdeltasum += outnode.delta        #自适应学习率调整,总体误差减小了则可适当增大学习率,加快收敛速度;相反则减小学习率以免出现震荡    def autoadjustlr(self, learningrate, errorsum, upfactor=1.05, downfactor=0.95):        if errorsum > self.errorsum:            learningrate *= upfactor        else:            learningrate *= downfactor        return learningrate    '''    变量解释    learningrate:学习率,可以认为是每次对weight和bias的修正大力程度    samplelistin:全部样本的集合,建议每次训练时从中随机挑一小部分(smallsamplenum)进行训练,多训练几次(circlenum)    derivlist:储存该节点本次梯度下降(即常规BP网络反向传播)对weight的改变    lastchange:储存该节点当前weight与上一轮weight之差的列表    Ps:本轮weight的最终变化Δw(n)是derivlist的部分加上α(1+cosθ)Δw(n-1),即动量项    '''        def training(self, samplelistin, threshold, learningrate=0.5, smallsamplenum=3, alpha=0.2):        circletimes = 500; j=0                #while self.errorsum > threshold:        for j in range(circletimes):            print("this is %d circle, and error is %f" % (j,self.errorsum)); #j += 1            samplepartlist = []            errorsum = self.errorsum            self.errorsum = 0            for chance in range(smallsamplenum):                samplepartlist.append(random.choice(samplelistin))                        for hnodes in self.hiddenlayer:                for hiddennode in hnodes:                    hiddennode.wdeltasum = [0]*len(hiddennode.wdeltasum)                    hiddennode.bdeltasum = 0                        for sample in samplepartlist:                i = 0                for inputnode in self.inlayer:                    inputnode.value = sample[0][i]; i += 1                    inputnode.wdeltasum = [0]*self.hnodenum                k = 0                for outputnode in self.outlayer:                    outputnode.rightvalue = sample[1][k]; k += 1                    outputnode.bdeltasum = 0                self.fPropagation()                self.bPropagation()                            learningrate = self.autoadjustlr(learningrate, errorsum)            for inputnode in self.inlayer:                derivlist = []                for hnodeindex in range(self.hnodenum):                    derivchange = inputnode.wdeltasum[hnodeindex] / smallsamplenum                    derivlist.append(derivchange)                costheta = cos(derivlist, inputnode.lastchange)                inputnode.lastchange = list(learningrate*np.array(derivlist) + alpha*(1+costheta)*np.array(inputnode.lastchange))                inputnode.weight = list(np.array(inputnode.weight)+np.array(inputnode.lastchange))                            for layerindex in range(self.hlayernum):                if layerindex == self.hlayernum-1:                    for hiddennode in self.hiddenlayer[layerindex]:                        derivlist = []                        hiddennode.bias += learningrate * hiddennode.bdeltasum / smallsamplenum                        for outindex in range(self.onodenum):                            derivchange = hiddennode.wdeltasum[outindex] / smallsamplenum                            derivlist.append(derivchange)                        costheta = cos(derivlist, hiddennode.lastchange)                        hiddennode.lastchange = list(learningrate*np.array(derivlist) + alpha*(1+costheta)*np.array(hiddennode.lastchange))                        hiddennode.weight = list(np.array(hiddennode.weight)+np.array(hiddennode.lastchange))                        else:                    for hiddennode in self.hiddenlayer[layerindex]:                        derivlist = []                        hiddennode.bias += learningrate * hiddennode.bdeltasum / smallsamplenum                        for hnodeindex in range(self.hnodenum):                            derivchange = hiddennode.wdeltasum[hnodeindex] / smallsamplenum                            derivlist.append(derivchange)                        costheta = cos(derivlist, hiddennode.lastchange)                        hiddennode.lastchange  = list(learningrate*np.array(derivlist) + alpha*(1+costheta)*np.array(hiddennode.lastchange))                        hiddennode.weight = list(np.array(hiddennode.weight)+np.array(hiddennode.lastchange))                        for outputnode in self.outlayer:                change = outputnode.bdeltasum / smallsamplenum                outputnode.bias += learningrate * change        def predict(self, onesample):        i = 0        for inputnode in self.inlayer:            inputnode.value = onesample[i]; i += 1        self.fPropagation()        return self.outlayer #返回一个列表,长度为输出节点数目


2)测试程序

if __name__ == '__main__':'''样本输入格式:每个样本为一个元组,第一项是归一化的输入节点激活项,为一个列表,长度为输入节点个数,要激活的点赋值1;第二项为一个列表,长度为输出节点个数,要激活的点赋值1测试程序为XOR异或运算,所以输入归一化就免了,输出节点只有一个,输出0或1'''    sampleputin = [([1, 0], [1]), ([1, 1], [0]), ([0, 0], [0]), ([0, 1], [1])]    elelist = ['0', '1']    hlayernum = 1    innodenum = len(elelist)    hnodenum =  int(math.sqrt(innodenum+1)+5) #隐藏层节点数目经验公式    onodenum = 1    b_pnet = neutralnet(hnodenum, hlayernum, elelist, onodenum)    b_pnet.training(sampleputin, threshold=0.01, smallsamplenum=2) #判断网络收敛可以看总误差是否已经降到了阈值以下,也可以设置训练一定次数后就结束           

3)测试结果

......this is 700 circle, and error is 0.053928this is 701 circle, and error is 0.014101this is 702 circle, and error is 0.054981this is 703 circle, and error is 0.031104this is 704 circle, and error is 0.032143this is 705 circle, and error is 0.045292this is 706 circle, and error is 0.025953this is 707 circle, and error is 0.053093this is 708 circle, and error is 0.014126this is 709 circle, and error is 0.023661this is 710 circle, and error is 0.040556this is 711 circle, and error is 0.019283this is 712 circle, and error is 0.033991>>> b_pnet.predict([1,1])[0].value0.19105864593071376>>> b_pnet.predict([1,0])[0].value0.9019077960132104

预测结果基本正确

0 0