神经网络前向后向传播推导及实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gao158190523/article/details/72963615

本文主要是BP网络的前后向传播较详细推导,以及C++实现,记下来也方便后面的回顾,也希望对关系细节的读者也一丝帮助。如果有不对的地方,请指正。

BP图模型:

这里写图片描述

网络中单个激活单元:

这里写图片描述


  • 上图定了隐层中的激活单元,该隐层激活单元中含有一个偏置项b。相关运算如图所示,符号右上角角标为单元在网络中的层好,结合代码实现时,网络激活单元之间的权重一般保存在前一层的单元中。

这里有两点注意:

  1. 输入层的单元中没有偏置项b,但有权值项w。
  2. 输出层的单元中没有权值项w,但有偏置项b。

相关符号定义:

符号 意义
m 样本数
nl 网络的总层数
Ll l
W(l)ij l 层的j单元与 l+1 的i单元之间的权值(weight)
b(l)i l 层第i个单元的偏置项
Sl 表示第 l 层的节点数
a(l)i 表示第 l 层第i单元的激活值
f(x) sigmoid函数: f(x)=11+ex
z(l)i 表示第 l 层第i单元的输入
hwb(x) 表示整个网络对输入x的输出结果,等价于 a(nl)

损失函数(带2范式正则):

J(W,b)=1mi=1mJ(W,b;xi,yi)+λ2l=1nl1i=1Slj=1Sl+1(W(l)ji)2

其中,
J(W,b;x,y)=12hwb(x)y2

我们优化所有权值和偏置就是通过最小化损失函数来实现的,通过对损失函数计算各权值和偏置的梯度,然后沿着各自梯度的反方向走,就可以让损失函数慢慢变小,由于神经网络不是的损失函数不是严格凸函数,所以并不能保证找到全局最优解。我们首先就要计算各权值和偏置关于损失函数的梯度。

前向传播

Tip : 这里需要先初始化各单元中的权值和偏置项的值,权值可以按照标准正态分布去产生,也可以使用其它方式产生,但最好不要偷懒而给所有权值赋上相同的值,这样会导致极慢的收敛速度,有兴趣的读者可以修改下面的程序自己试下。

  1. 输入层向隐层的前向传播:
    • 向输入层输入数据X,第二层第i个激活单元相关计算:
      该激活单元的输入: z(2)i=j=1S1W(1)ija(1)j+b(2)i ,其中 a(1)i=Xi
      该激活单元的输入: a(2)i=f(z(2)i)=11+ez(2)i
      计算完输入层的前向传播后就可以计算隐层间的传播了。
  2. 隐层间的传播:
    • 上一层的隐层输出值作为当前隐层的输入值:
      l 层第i个激活单元输入: z(l)i=j=1Sl1W(l1)ija(l1)j+b(l)i
      l 层第i个激活单元输出: a(l)i=f(z(l)i)=11+ez(l)i
      隐层按照从低向高的顺序依次计算各层的激活单元,依次计算各层: l=3,4,...Snl1
  3. 输出层的传播:
    • 传播到最后的输出层:
      Snl 层第i个激活单元输入: z(nl)i=j=1Snl1W(nl1)ija(nl1)j+b(nl)i
      Snl 层第i个激活单元最终的输出: a(nl)i=f(z(nl)i)=11+ez(nl)i
      其中 a(nl)i 就是神经网络最终的输出。

反向传播

Tip : 如果直接对损失函数中位于第一层的权值求导,会发现无从下手,因为后面的所有层的激活单元都直接或间接的包含了第一层的权值,这是一种嵌套的关系(数学函数嵌套,类似于斐波那契数列),第一层的权值被嵌套的最深。换言之,约靠后的激活单元,其权值在损失函数中嵌套的就越浅。既然这样,那我们可以先从靠后的权值下手,比如倒数第二层的权值 Wnl1 ,在损失函数中,就是最顶层的(输出层没有权值)。为了后期计算的方便,我们从后向前对各层各激活单元的输入变量 Zli 求导。后在对 Wli 求导就显得非常简单了,这里主要是求导的链式法则起到了关键作用(题外话:RNN中的LSTM单元也是通过这种思想求解,可以将时间t比作层数来从后向前计算)。

  1. 反向传播之输出层:
    δnli=J(W,b;x,y)Znli=12hwb(x)y2Znli=12anliy2Znli=12f(Znli)y2Znli=(anliyi)f(Znli)Znli=(anliyi)f(Znli)(1f(Znli))
  2. 反向传播之隐藏层和输入层:
    这里假设我们已经将后一层的 δl+1i 计算出来了(实际上输出层(最后一层)上的 δnli 已经根据上面的公式计算出来了),下面计算来计算 δli

δli=J(W,b;x,y)Zli=(12j=1Snl(anljyj)2)Zli=k=1Sl+1(12j=1Snl(anljyj)2)Zl+1k×Zl+1kZli=k=1Sl+1(δl+1k×Zl+1kZli)=k=1Sl+1δl+1k×(t=1Sl(Wlktalt)+bl+1k)Zli=k=1Sl+1δl+1k×(t=1Sl(Wlktf(Zlt))+bl+1k)Zli=k=1Sl+1(δl+1k×Wlki×f(Zli)Zli)=k=1Sl+1(δl+1k×Wlki×f(Zli)×(1f(Zli)))=f(Zli)×(1f(Zli))×k=1Sl+1(δl+1k×Wlki)

计算各权值和偏置的梯度:

更新策略:对于每一个样本 (xi,yi) ,先使用前向传播计算出各激活单元的输出值 ali ,再通过反向传播,计算出各激活单元的输入关于单个样本对应损失函数的偏导 δli ,之后就可以是用下面的公式对所有样本求出每个权值和偏置的偏导,并使用优化算法对其更新,这里使用的是梯度下降,也可以使用SGD等优化算法。

  • 权值关于损失函数的梯度:

    J(W,b)W(l)ji=1mt=1mJ(W,b;xt,yt)W(l)ji+λW(l)ji=1mt=1mJ(W,b;xt,yt)Zl+1j×Zl+1jW(l)ji+λW(l)ji=1mt=1mδl+1j×Zl+1jW(l)ji+λW(l)ji=1mt=1mδl+1j×(k=1Sl(Wljkalk)+bli)W(l)ji+λW(l)ji=1mt=1m(δl+1j×ali)+λW(l)ji

  • 偏置关于损失函数的梯度:

J(W,b)bli=1mt=1mJ(W,b;xt,yt)bli=1mt=1mJ(W,b;xt,yt)Zl+1j×Zl+1jbli=1mt=1mδl+1j×Zl+1jbli=1mt=1mδl+1j×(k=1Sl(Wljkalk)+bli)bli=1mt=1m(δl+1j)

程序中相关变量解释:

变量名 代表含义
value 对应单元的激活值: a(l)i
rightout 样本真实值: yi
error 损失误差: 1mi=1mJ(W,b;xi,yi)
delta 单个样本偏差: J(W,b;xi,yi)W(l)ji
wDeltaSum 整个样本偏差和,即最终的权值偏差: J(W,b)W(l)ji

下面是c++版实现程序(没有使用正则):

  • 注:程序实现的是对XOR运算的拟合

BpNet.h:

#include<iostream>
#include<cmath>
#include<vector>
#include<stdlib.h>
#include<time.h>

using namespace std;

#define innode 2            //输入节点数
#define hidenode 4          //隐含节点数
#define hidelayer 1         //隐含层数
#define outnode 1           //输出节点数
#define learningRate 0.9    //学习速率,alpha

//产生随机数
inline double get_11Random()
{
    return ((2.0*(double)rand()/RAND_MAX) - 1);
}

//sigmoid函数
inline double sigmoid(double x)
{
    double ans = 1.0/(1+exp(-x));
    return ans;
}

/*输入层节点
//1.value:      固定输入值
//2.weight:     面对第一层隐含层每个节点都有权值
//3.wDeltaSum:  面对第一层隐含层每个节点权值的delta值积累
*/
typedef struct inputNode
{
    double value;
    vector<double> weight,wDeltaSum;
}inputNode;

/*输出层节点
  1.value:      节点当前值
  2.delta:      与正确输出值之间的delta值
  3.rightout:   正确输出值
  4.bias:       偏移量
  5.bDeltaSum:  bias的delta值的积累,每个节点一个
 */
typedef struct outputNode
{
    double value, delta, rightout, bias, bDeltaSum;
}outputNode;

/*隐含层节点
  1.value:      节点当前值
  2.delta:      BP推导出的delta值
  3.bias:       偏移量
  4.bDeltaSum:  bias的delta值的积累,每个节点一个
  5.weight:     面对下一层每个节点都有的权值
  6.wDeltaSum:  weight的delta值的积累,面对下一层每个节点各自积累
 */
typedef struct hiddenNode
{
    double value, delta, bias, bDeltaSum;
    vector<double> weight, wDeltaSum;
}hiddenNode;

//单个样本
typedef struct sample
{
    vector<double> in, out;
}sample;

//BP神经网络
class BpNet
{
    public:
        BpNet();        //构造函数
        void forwardPropagationEpoc();      //单个样本前向传播
        void backPropagationEpoc();         //单个样本后向传播
        void training (static vector<sample> sampleGroup, double threshold);    //更新weight,bias
        void predict(vector<sample>& testGroup);            //神经网络预测
        void setInput(static vector<double> sampleIn);      //设置学习样本输入
        void setOutput(static vector<double> sampleOut);    //设置学习样本输出
        double error;
        inputNode* inputLayer[innode];                  //输入层
        outputNode* outputLayer[outnode];               //输出层
        hiddenNode* hiddenLayer[hidelayer][hidenode];   //隐含层
};

BpNet.cpp:

#include "BpNet.h"
using namespace std;

BpNet::BpNet()
{
    srand((unsigned)time(NULL));
    error = 100.f;

    //初始化输入层
    for(int i = 0; i< innode; i++)
    {
        inputLayer[i] = new inputNode();
        for(int j = 0; j < hidenode; j++)
        {
            inputLayer[i]->weight.push_back(get_11Random());
            inputLayer[i]->wDeltaSum.push_back(0.f);
        }
    }

    //初始化隐藏层
    for ( int i = 0; i < hidelayer; i++)
    {
        if ( i ==hidelayer - 1)
        {
            for(int j = 0;j < hidenode; j++ )
            {
                hiddenLayer[i][j] = new hiddenNode();
                hiddenLayer[i][j]->bias = get_11Random();
                for (int k = 0;k < outnode; k++)
                {
                    hiddenLayer[i][j]->weight.push_back(get_11Random());
                    hiddenLayer[i][j]->wDeltaSum.push_back(0.f);
                }
            }
        }
        else
        {
            for (int j =0; j < hidenode; j++)
            {
                hiddenLayer[i][j] = new hiddenNode();
                hiddenLayer[i][j]->bias = get_11Random();
                for (int k = 0; k < hidenode; k++)
                {
                    hiddenLayer[i][j]->weight.push_back(get_11Random());
                    hiddenLayer[i][j]->wDeltaSum.push_back(0.f);
                }
            }
        }

    }

    //初始化输出层
    for ( int i = 0; i < outnode; i++)
    {
        outputLayer[i] = new outputNode();
        outputLayer[i]->bias = get_11Random();
    }
}

void BpNet::forwardPropagationEpoc()
{
    //forward propagation on hidden layer
    for ( int i = 0; i < hidelayer; i++)
    {
        if (i == 0 )
        {
            for ( int j = 0; j < hidenode; j++)
            {
                double sum = 0.f;
                for ( int k = 0; k < innode; k++)
                {
                    sum += inputLayer[k]->value * inputLayer[k]->weight[j];
                }
                sum += hiddenLayer[i][j]->bias;
                hiddenLayer[i][j]->value = sigmoid(sum);
            }
        }
        else
        {
            for ( int j = 0; j < hidenode; j++)
            {
                double sum = 0.f;
                for ( int k = 0; k < hidenode; k++)
                {
                    sum += hiddenLayer[i-1][k]->value*hiddenLayer[i-1][k]->weight[j];
                }
                sum += hiddenLayer[i][j]->bias;
                hiddenLayer[i][j]->value = sigmoid(sum);
            }
        }
    }

    //forward propagation on output layer
    for ( int i = 0; i < outnode; i++)
    {
        double sum = 0.f;
        for( int j = 0; j < hidenode; j++)
        {
            sum += hiddenLayer[hidelayer - 1][j]->value * hiddenLayer[hidelayer - 1][j]->weight[i];
        }
        sum += outputLayer[i]->bias;
        outputLayer[i]->value = sigmoid(sum);
    }

}

void BpNet::backPropagationEpoc()
{
    // backward propagation on output layer
    // -- comput delta

    for ( int i = 0; i < outnode; i++)
    {
        double temp = fabs(outputLayer[i]->value-outputLayer[i]->rightout);
        error += temp * temp / 2;
        outputLayer[i]->delta = (outputLayer[i]->value - outputLayer[i]->rightout)*(1-outputLayer[i]->value)*outputLayer[i]->value;

    }

    // backward propagation on hidden layer
    // compute delta
    for ( int i = hidelayer - 1; i >= 0; i--)
    {
        if ( i == hidelayer - 1)
        {
            for ( int j = 0; j < hidenode; j++)
            {
                double sum = 0.f;
                for ( int k = 0; k < outnode; k++)
                {
                    sum += outputLayer[k]->delta * hiddenLayer[i][j]->weight[k];
                }
                hiddenLayer[i][j]->delta = sum*hiddenLayer[i][j]->value*(1 - hiddenLayer[i][j]->value);
            }
        }
        else
        {
            for ( int j = 0; j < hidenode; j++)
            {
                double sum = 0.f;
                for ( int k = 0; k < hidenode; k++)
                {
                    sum += hiddenLayer[i + 1][k]->delta * hiddenLayer[i][j]->weight[k];
                }
                hiddenLayer[i][j]->delta = sum * hiddenLayer[i][j]->value*(1-hiddenLayer[i][j]->value);
            }
        }

    }

    // backward propagation on input layer
    // update weight delta sum
    for ( int i = 0; i < innode; i++)
    {
        for ( int j = 0; j < hidenode; j++)
        {
            inputLayer[i]->wDeltaSum[j] += inputLayer[i]->value*hiddenLayer[0][j]->delta;

        }
    }

    // backward propagation on hidden layer
    // update weight delta sum and bias delta sum
    // 计算偏导数
    for ( int i = 0; i < hidelayer; i++)
    {
        if ( i == hidelayer - 1)
        {
            for ( int j = 0; j < hidenode; j++)
            {
                hiddenLayer[i][j]->bDeltaSum += hiddenLayer[i][j]->delta;
                for ( int k = 0; k < outnode; k++)
                {
                    hiddenLayer[i][j]->wDeltaSum[k] += hiddenLayer[i][j]->value * outputLayer[k]->delta;
                }
            }
        }
        else
        {
            for (int j = 0; j < hidenode; j++)
            {
                hiddenLayer[i][j]->bDeltaSum += hiddenLayer[i][j]->delta;
                for ( int k = 0; k < hidenode; k++)
                {
                    hiddenLayer[i][j]->wDeltaSum[k] += hiddenLayer[i][j]->value * hiddenLayer[i+1][k]->delta;
                }
            }
        }

    }

    // backward propagation on output layer
    // update bias delta sum
    for ( int i = 0; i < outnode; i++)
    {
        outputLayer[i]->bDeltaSum += outputLayer[i]->delta;
    }

}

void BpNet::training(static vector<sample> sampleGroup, double threshold)
{
    int sampleNum = sampleGroup.size();

    while ( error > threshold)
    {
        cout << "training error:"<<error<<endl;
        error = 0.f;
        //initialize delta sum
        for ( int i = 0; i < innode; i++)
        {
            inputLayer[i]->wDeltaSum.assign(inputLayer[i]->wDeltaSum.size(), 0.f);
        }
        for ( int i = 0; i < hidelayer; i++)
        {
            for ( int j = 0; j < hidenode; j++)
            {
                hiddenLayer[i][j]->wDeltaSum.assign(hiddenLayer[i][j]->wDeltaSum.size(), 0.f);
                hiddenLayer[i][j]->bDeltaSum = 0.f;
            }

        }
        for ( int i = 0; i < outnode; i++)
        {
            outputLayer[i]->bDeltaSum = 0.f;
        }

        // start training
        for( int iter = 0; iter < sampleNum; iter++)
        {
            // initial data of input and output
            setInput(sampleGroup[iter].in);
            setOutput(sampleGroup[iter].out);
            // forward and backward propagation
            // compute delta
            forwardPropagationEpoc();
            backPropagationEpoc();
        }

        // deltasum had computed over! then update weight and bias

        // backward propagation on input layer
        // update weight with Gradient descent
        for ( int i = 0; i < innode; i++)
        {
            for ( int j = 0; j < hidenode; j++)
            {
                inputLayer[i]->weight[j] -= learningRate * inputLayer[i]->wDeltaSum[j] /sampleNum;
            }
        }

        // backward propagation on hidden layer
        // update weight and bias
        for ( int i = 0; i < hidelayer; i++)
        {
            if( i == hidelayer -1)
            {
                for ( int j = 0;  j < hidenode; j++)
                {
                    // update bias
                    hiddenLayer[i][j]->bias -= learningRate * hiddenLayer[i][j]->bDeltaSum/sampleNum;
                    // update weight
                    for ( int k = 0; k < outnode; k++)
                    {
                        hiddenLayer[i][j]->weight[k] -= learningRate * hiddenLayer[i][j]->wDeltaSum[k]/sampleNum;
                    }
                }
            }
            else
            {
                for ( int j = 0; j < hidenode; j++)
                {
                    // update bias
                    hiddenLayer[i][j]->bias -= learningRate * hiddenLayer[i][j]->bDeltaSum/sampleNum;
                    // update weight
                    for ( int k = 0; k < hidenode; k++)
                    {
                        hiddenLayer[i][j]->weight[k] -= learningRate * hiddenLayer[i][j]->wDeltaSum[k]/sampleNum;
                    }
                }
            }
        }

        // backward propagation on output layer
        // udate bias
        for ( int i = 0; i < outnode; i++)
        {
            outputLayer[i]->bias -= learningRate * outputLayer[i]->bDeltaSum/sampleNum;
        }

    }

}

void BpNet::predict(vector<sample>& testGroup)
{
     int testNum = testGroup.size();
     for(int iter = 0; iter < testNum; iter++)
     {
         testGroup[iter].out.clear();
         setInput(testGroup[iter].in);
         forwardPropagationEpoc();
         for ( int i = 0; i< outnode; i++)
         {
             testGroup[iter].out.push_back(outputLayer[i]->value);
         }

     }

}

void BpNet::setInput(static vector<double> sampleIn)
{
    for ( int i = 0; i < innode; i++)
    {
        inputLayer[i]->value = sampleIn[i];
    }
}

void BpNet::setOutput(static vector<double> sampleOut)
{
    for ( int i = 0; i < outnode; i++)
    {
        outputLayer[i]->rightout = sampleOut[i];
    }
}


int main()
{
    BpNet testNet;

    //学习样本
    vector<double> samplein[4];
    vector<double> sampleout[4];

    samplein[0].push_back(0);
    samplein[0].push_back(0);
    sampleout[0].push_back(0);

    samplein[1].push_back(0);
    samplein[1].push_back(1);
    sampleout[1].push_back(1);

    samplein[2].push_back(1);
    samplein[2].push_back(0);
    sampleout[2].push_back(1);

    samplein[3].push_back(1);
    samplein[3].push_back(1);
    sampleout[3].push_back(0);

    sample sampleInOut[4];
    for ( int i = 0; i < 4; i++)
    {
        sampleInOut[i].in = samplein[i];
        sampleInOut[i].out = sampleout[i];
    }
    vector<sample> sampleGroup(sampleInOut, sampleInOut + 4);
    testNet.training(sampleGroup, 0.0001);

    //测试数据
    vector<double> testin[4];
    vector<double> testout[4];
    testin[0].push_back(0.1);
    testin[0].push_back(0.2);
    testin[1].push_back(0.15);
    testin[1].push_back(0.9);
    testin[2].push_back(1.1);
    testin[2].push_back(0.01);
    testin[3].push_back(0.88);
    testin[3].push_back(1.03);

    sample testInOut[4];
    for ( int i = 0; i < 4; i++)
    {
        testInOut[i].in = testin[i];
    }
    vector<sample> testGroup(testInOut, testInOut + 4);

    //预测测试数据,并输出结果
    testNet.predict(testGroup);
    for ( int i = 0; i < testGroup.size(); i++)
    {
        for ( int j = 0; j < testGroup[i].in.size(); j ++)
        {
            cout << testGroup[i].in[j] << "\t";
        }
        cout << "--- prediction:";
        for ( int j = 0; j < testGroup[i].out.size(); j++)
        {
            cout << testGroup[i].out[j] << "\t";
        }
        cout << endl;
    }

    system("pause");
    return 0;
}

本文为作者原创,转载请注明出处,谢谢!

参考

猜你喜欢

转载自blog.csdn.net/gao158190523/article/details/72963615