反向传播神经网络






4.79/5 (40投票s)
一个 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 日。