65.9K
CodeProject 正在变化。 阅读更多。
Home

反向传播神经网络

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (40投票s)

2006年3月27日

MIT

5分钟阅读

viewsIcon

698056

downloadIcon

8328

一个 C++ 类,实现了反向传播算法神经网络,支持任意数量的层/神经元。

引言

CBackProp 类封装了一个前馈神经网络和一个用于训练它的反向传播算法。本文档面向已经对神经网络和反向传播算法有所了解的读者。如果您不熟悉这些概念,我建议先阅读一些相关资料。

背景

这是我大学最后学期时的一个学术项目的一部分,我需要为不同的数据集找到隐藏层和学习参数的最佳数量和大小。确定神经网络的数据结构并让反向传播算法正常工作并不容易。本文档的目的是为了节省他人付出同样的努力。

在此有一点免责声明……本文档描述的是该算法的一个简化实现,并未充分阐述该算法。包含的代码有很多改进的空间(例如添加异常处理:-),并且许多步骤需要比我包含的更多的推理,例如,我为参数选择的值,以及层数/每层的神经元数量是为了演示用法,可能不是最优的。要了解更多信息,我建议访问

使用代码

通常,使用涉及以下步骤

  • 使用 CBackProp::CBackProp(int nl,int *sz,double b,double a) 创建网络
  • 应用反向传播算法 - 通过将输入和期望输出传递给 void CBackProp::bpgt(double *in,double *tgt) 来训练网络,直到 均方误差(通过 CBackProp::double mse(double *tgt) 获得)降低到可接受的值。
  • 使用训练好的网络通过 void CBackProp::ffwd(double *in) 前馈 输入数据来做出预测。

以下是对我包含的示例程序的描述。

一次一步…

设定目标

我们将尝试教会我们的网络破解二元的 **A XOR B XOR C**。XOR 是一个显而易见的选择,它不是线性可分的,因此需要隐藏层,并且不能由单个感知器学习。

训练数据集由多个记录组成,每个记录包含输入到网络的字段,然后是期望输出的字段。在此示例中,它是三个输入 + 一个期望输出。

// prepare XOR training data
double data[][4]={//    I  XOR  I  XOR  I   =   O
                  //--------------------------------
                        0,      0,      0,      0,
                        0,      0,      1,      1,
                        0,      1,      0,      1,
                        0,      1,      1,      0,
                        1,      0,      0,      1,
                        1,      0,      1,      0,
                        1,      1,      0,      0,
                        1,      1,      1,      1 };

配置

接下来,我们需要指定神经网络的合适结构,即它应该有多少个隐藏层以及每个层中有多少个神经元。然后,我们为其他参数指定合适的值:**学习率** - beta,我们可能还想指定**动量** - alpha(此项是可选的),以及**阈值** - thresh(目标均方误差,一旦达到则停止训练,否则继续训练 num_iter 次)。

让我们定义一个具有 4 个层,分别有 3、3、3 和 1 个神经元的网络。由于第一层是输入层,即输入参数的占位符,它必须与输入参数的数量相同,而最后一层是输出层,其大小必须与输出的数量相同 - 在我们的示例中,它们是 3 和 1。中间的其他层称为隐藏层

int numLayers = 4, lSz[4] = {3,3,3,1};
double beta = 0.2, alpha = 0.1, thresh = 0.00001;
long num_iter = 500000;

创建网络

CBackProp *bp = new CBackProp(numLayers, lSz, beta, alpha);

培训

for (long i=0; i < num_iter ; i++)
{
    bp->bpgt(data[i%8], &data[i%8][3]);

    if( bp->mse(&data[i%8][3]) < thresh) 
        break; // mse < threshold - we are done training!!!
}

让我们测试它的智慧

我们准备测试数据,在这里它与训练数据减去期望输出相同。

double testData[][3]={ //  I  XOR  I  XOR  I  =  ?
                        //----------------------
                            0,      0,      0,
                            0,      0,      1,
                            0,      1,      0,
                            0,      1,      1,
                            1,      0,      0,
                            1,      0,      1,
                            1,      1,      0,
                            1,      1,      1};

现在,使用训练好的网络对我们的测试数据进行预测……

for ( i = 0 ; i < 8 ; i++ )
{
    bp->ffwd(testData[i]);
    cout << testData[i][0]<< "  " 
         << testData[i][1]<< "  "  
         << testData[i][2]<< "  " 
         << bp->Out(0) << endl;
}

现在一探究竟

神经网络存储

我认为下面的代码注释很充分,而且是自 explanatory 的……

class CBackProp{

//      output of each neuron
        double **out;

//      delta error value for each neuron
        double **delta;

//      3-D array to store weights for each neuron
        double ***weight;

//      no of layers in net including input layer
        int numl;

//      array of numl elements to store size of each layer
        int *lsize;

//      learning rate
        double beta;

//      momentum
        double alpha;

//      storage for weight-change made in previous epoch
        double ***prevDwt;

//      sigmoid function
        double sigmoid(double in);

public:

        ~CBackProp();

//      initializes and allocates memory
        CBackProp(int nl,int *sz,double b,double a);

//      backpropogates error for one set of input
        void bpgt(double *in,double *tgt);

//      feed forwards activations for one set of inputs
        void ffwd(double *in);

//      returns mean square error of the net
        double mse(double *tgt);

//      returns i'th output of the net
        double Out(int i) const;
};

一些替代实现为层/神经元/连接定义了一个单独的类,然后将它们组合起来形成一个神经网络。虽然这绝对是一个更简洁的方法,但我决定使用 double ***double ** 来存储权重和输出等,通过分配所需的精确内存量,因为

  • 它在实现学习算法时提供了便利,例如,对于 (i-1)th 层 jth 神经元和 ith 层 kth 神经元之间的连接的权重,我个人更喜欢 w[i][k][j] (而不是像 net.layer[i].neuron[k].getWeight(j))。ith 层 jth 神经元的输出是 out[i][j],依此类推。
  • 我感受到的另一个优势是选择任意数量和大小的层的灵活性。
// initializes and allocates memory
CBackProp::CBackProp(int nl,int *sz,double b,double a):beta(b),alpha(a)
{

// Note that the following are unused,
//
// delta[0]
// weight[0]
// prevDwt[0]

//  I did this intentionally to maintain
//  consistency in numbering the layers.
//  Since for a net having n layers,
//  input layer is referred to as 0th layer,
//  first hidden layer as 1st layer
//  and the nth layer as output layer. And 
//  first (0th) layer just stores the inputs
//  hence there is no delta or weight
//  values associated to it.


    //    set no of layers and their sizes
    numl=nl;
    lsize=new int[numl];

    for(int i=0;i<numl;i++){
        lsize[i]=sz[i];
    }

    //    allocate memory for output of each neuron
    out = new double*[numl];

    for( i=0;i<numl;i++){
        out[i]=new double[lsize[i]];
    }

    //    allocate memory for delta
    delta = new double*[numl];

    for(i=1;i<numl;i++){
        delta[i]=new double[lsize[i]];
    }

    //    allocate memory for weights
    weight = new double**[numl];

    for(i=1;i<numl;i++){
        weight[i]=new double*[lsize[i]];
    }
    for(i=1;i<numl;i++){
        for(int j=0;j<lsize[i];j++){
            weight[i][j]=new double[lsize[i-1]+1];
        }
    }

    //    allocate memory for previous weights
    prevDwt = new double**[numl];

    for(i=1;i<numl;i++){
        prevDwt[i]=new double*[lsize[i]];

    }
    for(i=1;i<numl;i++){
        for(int j=0;j<lsize[i];j++){
            prevDwt[i][j]=new double[lsize[i-1]+1];
        }
    }

    //    seed and assign random weights
    srand((unsigned)(time(NULL)));
    for(i=1;i<numl;i++)
        for(int j=0;j<lsize[i];j++)
            for(int k=0;k<lsize[i-1]+1;k++)
                weight[i][j][k]=(double)(rand())/(RAND_MAX/2) - 1;

    //    initialize previous weights to 0 for first iteration
    for(i=1;i<numl;i++)
        for(int j=0;j<lsize[i];j++)
            for(int k=0;k<lsize[i-1]+1;k++)
                prevDwt[i][j][k]=(double)0.0;
}

前馈

此函数更新每个神经元的输出值。从第一个隐藏层开始,它获取每个神经元的输入,通过首先计算输入的加权和,然后应用 Sigmoid 函数来找到输出(o),并将其向前传递到下一层,直到输出层被更新。

其中

// feed forward one set of input
void CBackProp::ffwd(double *in)
{
        double sum;

 // assign content to input layer

        for(int i=0;i < lsize[0];i++)
                out[0][i]=in[i];


// assign output(activation) value
// to each neuron usng sigmoid func

        // For each layer
        for(i=1;i < numl;i++){
                // For each neuron in current layer    
                for(int j=0;j < lsize[i];j++){
                        sum=0.0;
                        // For input from each neuron in preceding layer
                        for(int k=0;k < lsize[i-1];k++){
                                // Apply weight to inputs and add to sum
                                sum+= out[i-1][k]*weight[i][j][k];    
                        }
                        // Apply bias
                        sum+=weight[i][j][lsize[i-1]];                
                        // Apply sigmoid function
                        out[i][j]=sigmoid(sum);                        
                }
        }
}

反向传播…

该算法实现在函数 void CBackProp::bpgt(double *in,double *tgt) 中。以下是反向传播输出层误差直到第一个隐藏层的各种步骤。

void CBackProp::bpgt(double *in,double *tgt)
{
    double sum;

首先,我们调用 void CBackProp::ffwd(double *in) 来更新每个神经元的输出值。此函数接收网络的输入并找到每个神经元的输出。

其中

ffwd(in);

下一步是找到输出层的 delta。

for(int i=0;i < lsize[numl-1];i++){
   delta[numl-1][i]=out[numl-1][i]*
     (1-out[numl-1][i])*(tgt[i]-out[numl-1][i]);
}

然后找到隐藏层的 delta…

for(i=numl-2;i>0;i--){
    for(int j=0;j < lsize[i];j++){
        sum=0.0;
        for(int k=0;k < lsize[i+1];k++){
                sum+=delta[i+1][k]*weight[i+1][k][j];
        }
        delta[i][j]=out[i][j]*(1-out[i][j])*sum;
    }
}

应用动量(如果 alpha=0 则无效)。

for(i=1;i < numl;i++){
    for(int j=0;j < lsize[i];j++){
        for(int k=0;k < lsize[i-1];k++){
                weight[i][j][k]+=alpha*prevDwt[i][j][k];
        }
        weight[i][j][lsize[i-1]]+=alpha*prevDwt[i][j][lsize[i-1]];
    }
}

最后,通过找到权重的修正来调整权重。

然后应用修正。

for(i=1;i < numl;i++){
    for(int j=0;j < lsize[i];j++){
        for(int k=0;k < lsize[i-1];k++){
                prevDwt[i][j][k]=beta*delta[i][j]*out[i-1][k];
                weight[i][j][k]+=prevDwt[i][j][k];
        }
        prevDwt[i][j][lsize[i-1]]=beta*delta[i][j];
        weight[i][j][lsize[i-1]]+=prevDwt[i][j][lsize[i-1]];
    }
}

网络学得怎么样?

均方误差被用作衡量神经网络学习好坏的指标。

正如在示例 XOR 程序中所显示的,我们应用上述步骤,直到达到令人满意低的误差水平。CBackProp::double mse(double *tgt) 返回的就是这个值。

历史

  • 创建日期:2006 年 3 月 25 日。
© . All rights reserved.