去年在学习Stanford的ML课程的时候整理过一篇BP神经网络原理的解析,链接地址,不过没有对它的code实现作太多的研究,只是用MATLAB的工具箱做了实验。

Jeremy Lin

具体的原理性资料可以参考:

[1] BP神经网络解析          地址

[2] Tom M.Mitchell 机器学习教程  地址


BP网络算法流程:

c语言 库 神经网络 bp神经网络c语言代码_神经网络

从上面的算法流程可以看出来,BP神经网络的步骤并不多,如果你之前就了解BP神经网络,那么上面的流程应该还是可以很顺利地看懂的,如果之前没学习过,那么我推荐你好好参看我上面推荐的两个Ref。


BPNN的结构体

要开始具体编写BP神经网络(以下简称BPNN)的步骤之前,你需要定义好BPNN的结构,这里面用结构体实现,自然你完全可以用类实现。


typedef struct 
{
  int input_n;                  // 网络输入个数
  int hidden_n;                 // 隐藏层单元个数
  int output_n;                 // 网络输出个数

  double *input_units;          // 网络输入单元
  double *hidden_units;         // 隐藏层单元
  double *output_units;         // 网络输出单元

  double *hidden_delta;         // 隐藏层单元error
  double *output_delta;         // 网络输出单元error

  double *target;               // 目标输出单元

  double **input_weights;       // 从输入层到隐藏层的权值向量
  double **hidden_weights;      // 从隐藏层到输出层的权值向量
  
                                // 冲量项    
  double **input_prev_weights;  // 从输入层到隐藏层的冲量项向量
  double **hidden_prev_weights; // 从隐藏层到输出层的冲量项向量
} BPNN;



创建BPNN

在上面的算法流程中的第一步是创建n_in个输入,n_hidden个隐藏层单元,n_out个输出单元的网络。然后是初始化网络的权值为较小的随机数。

在讲如何创建网络之前,我更愿意谈谈如何生成随机数。嘿,那不是直接用rand()吗?Yes,当然是用这个函数,但是,我们可以花点时间谈下rand()是如何生成随机数。在Meitu实习的末尾,我突然想了解随机数到底是如何生成的。那时候我第一步是自然是Google了,结果搜索关键词不够cool,得到的结果自然是用rand()生成了,哈,可是我想要的是随机数生成的原理啊。当然,最后知道了,那就是用线性同余法生成随机数。大家如果如果有兴趣的话,可以参考我提供的Refs[3],具体我不想在这里说了。

[3] 计算机程序设计艺术(卷2)  Chapter 3 Random Numbers 

现在进入原题:


BPNN *bpnn_create(int n_in, int n_hidden, int n_out)
{
  // 创建BPNN
  BPNN *newnet;
  newnet = bpnn_internal_create(n_in, n_hidden, n_out);
  
  // 初始化BPNN的网络权值
  bpnn_randomize_weights(newnet->input_weights, n_in, n_hidden);
  bpnn_randomize_weights(newnet->hidden_weights, n_hidden, n_out);
  // 初始化BPNN的冲量
  bpnn_zero_weights(newnet->input_prev_weights, n_in, n_hidden);
  bpnn_zero_weights(newnet->hidden_prev_weights, n_hidden, n_out);

  return (newnet);
}



在这里,我们主要使用bpnn_internal_create()函数创建BPNN:


BPNN *bpnn_internal_create(int n_in, int n_hidden, int n_out)
{
  BPNN *newnet;

  newnet = (BPNN *) malloc (sizeof (BPNN));
  if (newnet == NULL) {
    printf("BPNN_CREATE: Couldn't allocate neural network\n");
    return (NULL);
  }

  newnet->input_n = n_in;
  newnet->hidden_n = n_hidden;
  newnet->output_n = n_out;
  newnet->input_units = alloc_1d_dbl(n_in + 1);
  newnet->hidden_units = alloc_1d_dbl(n_hidden + 1);
  newnet->output_units = alloc_1d_dbl(n_out + 1);

  newnet->hidden_delta = alloc_1d_dbl(n_hidden + 1);
  newnet->output_delta = alloc_1d_dbl(n_out + 1);
  newnet->target = alloc_1d_dbl(n_out + 1);

  newnet->input_weights = alloc_2d_dbl(n_in + 1, n_hidden + 1);
  newnet->hidden_weights = alloc_2d_dbl(n_hidden + 1, n_out + 1);

  newnet->input_prev_weights = alloc_2d_dbl(n_in + 1, n_hidden + 1);
  newnet->hidden_prev_weights = alloc_2d_dbl(n_hidden + 1, n_out + 1);

  return (newnet);
}



在上面各个单元的内存分配主要是通过如下两个函数实现的:


// 一维数组空间分配
double *alloc_1d_dbl(int n)
{
  double *newA;

  newA = (double *) malloc ((unsigned) (n * sizeof (double)));
  if (newA == NULL) 
  {
    printf("ALLOC_1D_DBL: Couldn't allocate array of doubles\n");
    return (NULL);
  }
  return (newA);
}


// 二维数组空间分配
double **alloc_2d_dbl(int m, int n)
{
  int i;
  double **newA2;

  newA2 = (double **) malloc ((unsigned) (m * sizeof (double *)));
  if (newA2 == NULL) 
  {
    printf("ALLOC_2D_DBL: Couldn't allocate array of dbl ptrs\n");
    return (NULL);
  }

  for (i = 0; i < m; i++) 
  {
    newA2[i] = alloc_1d_dbl(n);
  }
  return (newA2);
}



那么接下来就是BPNN的网络权值初始化了:


#define BIGRND 0x7fffffff

double drnd()
{
  return ((double) rand() / (double) BIGRND);
}

double dpn1()
{
  return ((drnd() * 2.0) - 1.0);
}
void bpnn_randomize_weights(double **w, int m, int n)
{
  int i, j;

  for (i = 0; i <= m; i++) 
  {
    for (j = 0; j <= n; j++) 
  {
      w[i][j] = dpn1();
    }
  }
}




BPNN的训练

好了,前戏已做,现在开始训练:


void bpnn_train(BPNN *net, double eta, double momentum, double *eo, double *eh)
{
  int in, hid, out;
  double out_err, hid_err;

  in  = net->input_n;
  hid = net->hidden_n;
  out = net->output_n;

  // 前向传播
  bpnn_layerforward(net->input_units, net->hidden_units,
      net->input_weights, in, hid);
  bpnn_layerforward(net->hidden_units, net->output_units,
      net->hidden_weights, hid, out);

  // 计算误差
  bpnn_output_error(net->output_delta, net->target, net->output_units,
      out, &out_err);
  bpnn_hidden_error(net->hidden_delta, hid, net->output_delta, out,
      net->hidden_weights, net->hidden_units, &hid_err);
  *eo = out_err;
  *eh = hid_err;

  // 调整权值向量
  bpnn_adjust_weights(net->output_delta, out, net->hidden_units, hid,
      net->hidden_weights, net->hidden_prev_weights, eta, momentum);
  bpnn_adjust_weights(net->hidden_delta, hid, net->input_units, in,
      net->input_weights, net->input_prev_weights, eta, momentum);
}



下面是上面各个函数的具体实现:


// 激活函数
double squash(double x)
{
  return (1.0 / (1.0 + exp(-x)));
}

// 前向函数
void bpnn_layerforward(double *l1,double *l2, double **conn, int n1, int n2)
{
  double sum;
  int j, k;

  // 设置偏置
  l1[0] = 1.0;

  for (j = 1; j <= n2; j++) 
  {
    sum = 0.0;
    for (k = 0; k <= n1; k++) 
    {
      sum += conn[k][j] * l1[k];
    }
    l2[j] = squash(sum);
  }
}

void bpnn_output_error(double *delta, double *target, double *output, int nj, double *err)
{
  int j;
  double o, t, errsum;

  errsum = 0.0;
  for (j = 1; j <= nj; j++) 
  {
    o = output[j];
    t = target[j];
    delta[j] = o * (1.0 - o) * (t - o);
    errsum += ABS(delta[j]);
  }
  *err = errsum;
}


void bpnn_hidden_error(double *delta_h, int nh, double *delta_o, int no, double **who, double *hidden, double *err)
{
  int j, k;
  double h, sum, errsum;

  errsum = 0.0;
  for (j = 1; j <= nh; j++) 
  {
    h = hidden[j];
    sum = 0.0;
    for (k = 1; k <= no; k++) 
    {
      sum += delta_o[k] * who[j][k];
    }
    delta_h[j] = h * (1.0 - h) * sum;
    errsum += ABS(delta_h[j]);
  }
  *err = errsum;
}


void bpnn_adjust_weights(double *delta, int ndelta, double *ly, int nly, double **w,
           double **oldw, double eta,double momentum)
{
  double new_dw;
  int k, j;

  ly[0] = 1.0;
  for (j = 1; j <= ndelta; j++) 
  {
    for (k = 0; k <= nly; k++) 
    {
      new_dw = ((eta * delta[j] * ly[k]) + (momentum * oldw[k][j]));
      w[k][j] += new_dw;
      oldw[k][j] = new_dw;
    }
  }
}

void bpnn_feedforward(BPNN *net)
{
  int in, hid, out;

  in = net->input_n;
  hid = net->hidden_n;
  out = net->output_n;

  bpnn_layerforward(net->input_units, net->hidden_units,
      net->input_weights, in, hid);
  bpnn_layerforward(net->hidden_units, net->output_units,
      net->hidden_weights, hid, out);
}



到现在为止,BPNN的主要函数算是介绍完毕了。

不过,还有一个东西还没说,那就是所谓的终止条件。从推导BP的过程来说,我们很容易想到的一种终止条件自然是:直到对训练样例的误差E降低到某个预定义的阈值之下。但是,这其实并不是一个好的想法,因为BP算法容易过度拟合训练样例,降低对于其他实例的泛化精度。而为解决这个问题一个最常用的方法是在训练数据外再为算法提供一个验证数据集(validation dataset)。算法在使用训练数据集合驱动梯度下降搜索的同时,监视对于这个验证数据集合的误差。