文章翻译自: http://www.codeproject.com/Articles/16650/Neural-Network-for-Recognition-of-Handwritten-Digi

如何在C++中实现一个神经网络类?

主要有四个不同的类需要我们来考虑:

  1. 层 - layers
  2. 层中的神经元 - neurons
  3. 神经元之间的连接 - connections
  4. 连接的权值 - weights

这四类都在下面的代码中体现, 集中应用于第五个类 - 神经网络(neural network)上. 它就像一个容器, 用于和外部交流的接口. 下面的代码大量使用了STL的vector.

  1. // simplified view: some members have been omitted,
  2. // and some signatures have been altered
  3.  
  4. // helpful typedef's
  5.  
  6. typedef std::vector< NNLayer* > VectorLayers;
  7. typedef std::vector< NNWeight* > VectorWeights;
  8. typedef std::vector< NNNeuron* > VectorNeurons;
  9. typedef std::vector< NNConnection > VectorConnections;
  10.  
  11. // Neural Network class
  12.  
  13. class NeuralNetwork
  14. {
  15. public:
  16. NeuralNetwork();
  17. virtual ~NeuralNetwork();
  18.  
  19. void Calculate( double* inputVector, UINT iCount,
  20. double* outputVector = NULL, UINT oCount = );
  21.  
  22. void Backpropagate( double *actualOutput,
  23. double *desiredOutput, UINT count );
  24.  
  25. VectorLayers m_Layers;
  26. };
  27.  
  28. // Layer class
  29.  
  30. class NNLayer
  31. {
  32. public:
  33. NNLayer( LPCTSTR str, NNLayer* pPrev = NULL );
  34. virtual ~NNLayer();
  35.  
  36. void Calculate();
  37.  
  38. void Backpropagate( std::vector< double >& dErr_wrt_dXn /* in */,
  39. std::vector< double >& dErr_wrt_dXnm1 /* out */,
  40. double etaLearningRate );
  41.  
  42. NNLayer* m_pPrevLayer;
  43. VectorNeurons m_Neurons;
  44. VectorWeights m_Weights;
  45. };
  46.  
  47. // Neuron class
  48.  
  49. class NNNeuron
  50. {
  51. public:
  52. NNNeuron( LPCTSTR str );
  53. virtual ~NNNeuron();
  54.  
  55. void AddConnection( UINT iNeuron, UINT iWeight );
  56. void AddConnection( NNConnection const & conn );
  57.  
  58. double output;
  59.  
  60. VectorConnections m_Connections;
  61. };
  62.  
  63. // Connection class
  64.  
  65. class NNConnection
  66. {
  67. public:
  68. NNConnection(UINT neuron = ULONG_MAX, UINT weight = ULONG_MAX);
  69. virtual ~NNConnection();
  70.  
  71. UINT NeuronIndex;
  72. UINT WeightIndex;
  73. };
  74.  
  75. // Weight class
  76.  
  77. class NNWeight
  78. {
  79. public:
  80. NNWeight( LPCTSTR str, double val = 0.0 );
  81. virtual ~NNWeight();
  82.  
  83. double value;
  84. };

类NeuralNetwork存储的是一个指针数组, 这些指针指向NN中的每一层, 即NNLayer. 没有专门的函数来增加层, 只需要使用std::vector::push_back()即可. NeuralNetwork类提供了两个基本的接口, 一个用来得到输出(Calculate), 一个用来训练(Backpropagete).

每一个NNLayer都保存一个指向前一层的指针, 使用这个指针可以获取上一层的输出作为输入. 另外它还保存了一个指针向量, 每个指针指向本层的神经元, 即NNNeuron, 当然, 还有连接的权值NNWeight. 和NeuralNetwork相似, 神经元和权值的增加都是通过std::vector::push_back()方法来执行的. NNLayer层还包含了函数Calculate()来计算神经元的输出, 以及Backpropagate()来训练它们. 实际上, NeuralNetwork类只是简单地调用每层的这些函数来实现上小节所说的2个同名方法.

每个NNNeuron保存了一个连接数组, 使用这个数组可以使得神经元能够获取输入. 使用NNNeuron::AddConnection()来增加一个Connection, 输入神经元的标号和权值的标号, 从而建立一个NNConnection对象, 并将它push_back()到神经元保存的连接数组中. 每个神经元同样保存着它自己的输出值(double). NNConnection和NNWeight类分别存储了一些信息.

你可能疑惑, 为何权值和连接要分开定义? 根据上述的原理, 每个连接都有一个权值, 为何不直接将它们放在一个类里?

原因是: 权值经常被连接共享.

实际上, 在卷积神经网络中就是共享连接的权值的. 所以, 举例来说, 就算一层可能有几百个神经元, 权值却可能只有几十个. 通过分离这两个概念, 这种共享可以很轻易地实现.


前向传递

前向传递是指所有的神经元基于接收的输入, 计算输出的过程.

在代码中, 这个过程通过调用NeuralNetwork::Calculate()来实现. NeuralNetwork::Calculate()直接设置输入层的神经元的值, 随后迭代剩下的层, 调用每一层的NNLayer::Calculate(). 这就是所谓的前向传递的串行实现方式. 串行计算并非是实现前向传递的唯一方法, 但它是最直接的. 下面是一个简化后的代码, 输入一个代表输入数据的C数组和一个代表输出数据的C数组.

  1. // simplified code
  2.  
  3. void NeuralNetwork::Calculate(double* inputVector, UINT iCount,
  4. double* outputVector /* =NULL */,
  5. UINT oCount /* =0 */)
  6.  
  7. {
  8. VectorLayers::iterator lit = m_Layers.begin();
  9. VectorNeurons::iterator nit;
  10.  
  11. // 第一层是输入层:
  12. // 直接设置所有的神经元输出为给定的输入向量即可
  13.  
  14. if ( lit < m_Layers.end() )
  15. {
  16. nit = (*lit)->m_Neurons.begin();
  17. int count = ;
  18.  
  19. ASSERT( iCount == (*lit)->m_Neurons.size() );
  20. // 输入和神经元个数应当一一对应
  21.  
  22. while( ( nit < (*lit)->m_Neurons.end() ) && ( count < iCount ) )
  23. {
  24. (*nit)->output = inputVector[ count ];
  25. nit++;
  26. count++;
  27. }
  28. }
  29.  
  30. // 调用Calculate()迭代剩余层
  31.  
  32. for( lit++; lit<m_Layers.end(); lit++ )
  33. {
  34. (*lit)->Calculate();
  35. }
  36.  
  37. // 使用结果设置每层输出
  38.  
  39. if ( outputVector != NULL )
  40. {
  41. lit = m_Layers.end();
  42. lit--;
  43.  
  44. nit = (*lit)->m_Neurons.begin();
  45.  
  46. for ( int ii=; ii<oCount; ++ii )
  47. {
  48. outputVector[ ii ] = (*nit)->output;
  49. nit++;
  50. }
  51. }
  52. }

在层中的Calculate()函数中, 层会迭代其中的所有神经元, 对于每一个神经元, 它的输出通过前馈公式给出: 

这个公式通过迭代每个神经元的所有连接来实现, 获取对应的权重和对应的前一层神经元的输出. 如下:

  1. // simplified code
  2.  
  3. void NNLayer::Calculate()
  4. {
  5. ASSERT( m_pPrevLayer != NULL );
  6.  
  7. VectorNeurons::iterator nit;
  8. VectorConnections::iterator cit;
  9.  
  10. double dSum;
  11.  
  12. for( nit=m_Neurons.begin(); nit<m_Neurons.end(); nit++ )
  13. {
  14. NNNeuron& n = *(*nit); // 取引用
  15.  
  16. cit = n.m_Connections.begin();
  17.  
  18. ASSERT( (*cit).WeightIndex < m_Weights.size() );
  19.  
  20. // 第一个权值是偏置
  21. // 需要忽略它的神经元下标
  22.  
  23. dSum = m_Weights[ (*cit).WeightIndex ]->value;
  24.  
  25. for ( cit++ ; cit<n.m_Connections.end(); cit++ )
  26. {
  27. ASSERT( (*cit).WeightIndex < m_Weights.size() );
  28. ASSERT( (*cit).NeuronIndex <
  29. m_pPrevLayer->m_Neurons.size() );
  30.  
  31. dSum += ( m_Weights[ (*cit).WeightIndex ]->value ) *
  32. ( m_pPrevLayer->m_Neurons[
  33. (*cit).NeuronIndex ]->output );
  34. }
  35.  
  36. n.output = SIGMOID( dSum );
  37.  
  38. }
  39.  
  40. }

SIGMOID是一个宏定义, 用于计算激励函数.


反向传播

BP是从最后一层向前移动的一个迭代过程. 假设在每一层我们都知道了它的输出误差. 如果我们知道输出误差, 那么修正权值来减少这个误差就不难. 问题是我们只能观测到最后一层的误差.

BP给出了一种通过当前层输出计算前一层的输出误差的方法. 它是一种迭代的过程: 从最后一层开始, 计算最后一层权值的修正, 然后计算前一层的输出误差, 反复.

BP的公式在下面. 代码中就用到了这个公式. 距离来说, 第一个公式告诉了我们如何去计算误差EP对于激励值yi的第n层的偏导数. 代码中, 这个变量名为dErr_wrt_dYn[ ii ].

对于最后一层神经元的输出, 计算一个单输入图像模式的误差偏导的方法如下:

(equation 1)

其中, 是对于模式P再第n层的误差, 是最后一层的期望输出, 是最后一层的实际输出.

给定上式, 我们可以得到偏导表达式:

(equation 2)

式2给出了BP过程的起始值. 我们使用这个数值作为式2的右值从而计算偏导的值. 使用偏导的值, 我们可以计算权值的修正量, 通过应用下式:

(equation 3), 其中是激励函数的导数.

(equation 4)

使用式2和式3, 我们可以计算前一层的误差, 使用下式5:

(equation 5)

从式5中获取的值又可以立刻用作前一层的起始值. 这是BP的核心所在.

式4中获取的值告诉我们该如何去修正权值, 按照下式:

(equation 6)

其中eta是学习速率, 常用值是0.0005, 并随着训练减小.

本代码中, 上述等式在NeuralNetwork::Backpropagate()中实现. 输入实际上是神经网络的实际输出和期望输出. 使用这两个输入, NeuralNetwork::Backpropagate()计算式2的值并迭代所有的层, 从最后一层一直迭代到第一层. 对于每层, 都调用了NNLayer::Backpropagate(). 输入是梯度值, 输出则是式5.

这些梯度都保存在一个两维数组differentials中.

本层的输出则作为前一层的输入.

  1. // simplified code
  2. void NeuralNetwork::Backpropagate(double *actualOutput,
  3. double *desiredOutput, UINT count)
  4. {
  5. // 神经网络的BP过程,
  6. // 从最后一层迭代向前处理到第一层为止.
  7. // 首先, 单独计算最后一层,
  8. // 因为它提供了前一层所需的梯度信息
  9. // (i.e., dErr_wrt_dXnm1)
  10.  
  11. // 变量含义:
  12. //
  13. // Err - 整个NN的输出误差
  14. // Xn - 第n层的输出向量
  15. // Xnm1 - 前一层的输出向量
  16. // Wn - 第n层的权值向量
  17. // Yn - 第n层的激励函数输入值
  18. // 即, 在应用压缩函数(squashing function)前的权值和// F - 挤压函数: Xn = F(Yn)
  19. // F' - 压缩函数(squashing function)的梯度
  20. // 比如, 令 F = tanh,
  21. // 则 F'(Yn) = 1 - Xn^2, 梯度可以通过输出来计算, 不需要输入信息
  22.  
  23. VectorLayers::iterator lit = m_Layers.end() - ; // 取最后一层
  24.  
  25. std::vector< double > dErr_wrt_dXlast( (*lit)->m_Neurons.size() ); // 记录后层神经元误差对输入的梯度
  26. std::vector< std::vector< double > > differentials; //记录每一层输出对输入的梯度
  27.  
  28. int iSize = m_Layers.size(); // 层数
  29.  
  30. differentials.resize( iSize );
  31.  
  32. int ii;
  33.  
  34. // 计算最后一层的 dErr_wrt_dXn 来开始整个迭代.
  35. // 对于标准的MSE方程
  36. // (比如, 0.5*sumof( (actual-target)^2 ),
  37. // 梯度表达式就仅仅是期望和实际的差: Xn - Tn
  38.  
  39. for ( ii=; ii<(*lit)->m_Neurons.size(); ++ii )
  40. {
  41. dErr_wrt_dXlast[ ii ] =
  42. actualOutput[ ii ] - desiredOutput[ ii ];
  43. }
  44.  
  45. // 保存 Xlast 并分配内存存储剩余的梯度
  46.  
  47. differentials[ iSize- ] = dErr_wrt_dXlast; // 最后一层的梯度
  48.  
  49. for ( ii=; ii<iSize-; ++ii )
  50. {
  51. differentials[ ii ].resize(
  52. m_Layers[ii]->m_Neurons.size(), 0.0 );
  53. }
  54.  
  55. // 迭代每个层, 包括最后一层但不包括第一层
  56. // 同时求得每层的BP误差并矫正权值// 返回梯度dErr_wrt_dXnm1用于下一次迭代
  57.  
  58. ii = iSize - ;
  59. for ( lit; lit>m_Layers.begin(); lit--)
  60. {
  61. (*lit)->Backpropagate( differentials[ ii ],
  62. differentials[ ii - ], m_etaLearningRate ); // 调用每一层的BP接口
  63. --ii;
  64. }
  65.  
  66. differentials.clear();
  67. }

在NNLayer::Backpropagate()中, 层实现了式3~5, 计算出了梯度. 实现了式6来更新本层的权重. 在下面的代码中, 激励函数的梯度被定义为 DSIGMOID.

  1. // simplified code
  2.  
  3. void NNLayer::Backpropagate( std::vector< double >& dErr_wrt_dXn /* in */,
  4. std::vector< double >& dErr_wrt_dXnm1 /* out */,
  5. double etaLearningRate )
  6. {
  7. double output;
  8.  
  9. // 计算式 (3): dErr_wrt_dYn = F'(Yn) * dErr_wrt_Xn
  10.  
  11. for ( ii=; ii<m_Neurons.size(); ++ii ) // 遍历所有神经元
  12. {
  13. output = m_Neurons[ ii ]->output; // 神经元输出
  14.  
  15. dErr_wrt_dYn[ ii ] = DSIGMOID( output ) * dErr_wrt_dXn[ ii ]; // 误差对输入的梯度
  16. }
  17.  
  18. // 计算式 (4): dErr_wrt_Wn = Xnm1 * dErr_wrt_Yn
  19. // 对于本层的每个神经元, 遍历前一层的连接
  20. // 更新对应权值的梯度
  21.  
  22. ii = ;
  23. for ( nit=m_Neurons.begin(); nit<m_Neurons.end(); nit++ ) // 迭代本层所有神经元
  24. {
  25. NNNeuron& n = *(*nit); // 取引用
  26.  
  27. for ( cit=n.m_Connections.begin(); cit<n.m_Connections.end(); cit++ ) // 遍历每个神经元的后向连接
  28. {
  29. kk = (*cit).NeuronIndex; // 连接的前一层神经元标号
  30. if ( kk == ULONG_MAX ) // 偏置的标号固定为最大整形量
  31. {
  32. output = 1.0; // 偏置
  33. }
  34. else // 其他情况下 神经元输出等于前一层对应神经元的输出 Xn-1
  35. {
  36. output = m_pPrevLayer->m_Neurons[ kk ]->output;
  37. }
  38. // 误差对权值的梯度
  39.   // 每次使用对应神经元的误差对输入的梯度
  40. dErr_wrt_dWn[ (*cit).WeightIndex ] += dErr_wrt_dYn[ ii ] * output;
  41. }
  42.  
  43. ii++;
  44. }
  45.  
  46. // 计算式 (5): dErr_wrt_Xnm1 = Wn * dErr_wrt_dYn,// 需要dErr_wrt_Xn的值来进行前一层的BP
  47.  
  48. ii = ;
  49. for ( nit=m_Neurons.begin(); nit<m_Neurons.end(); nit++ ) // 迭代所有神经元
  50. {
  51. NNNeuron& n = *(*nit); // 取引用
  52.  
  53. for ( cit=n.m_Connections.begin();
  54. cit<n.m_Connections.end(); cit++ ) // 遍历每个神经元所有连接
  55. {
  56. kk=(*cit).NeuronIndex;
  57. if ( kk != ULONG_MAX )
  58. {
  59. // 排除了ULONG_MAX, 提高了偏置神经元的重要性// 因为我们不能够训练偏置神经元
  60.  
  61. nIndex = kk;
  62.  
  63. dErr_wrt_dXnm1[ nIndex ] += dErr_wrt_dYn[ ii ] *
  64. m_Weights[ (*cit).WeightIndex ]->value;
  65. }
  66.  
  67. }
  68.  
  69. ii++; // ii 跟踪神经元下标
  70.  
  71. }
  72.  
  73. // 计算式 (6): 更新权值
  74. // 在本层使用 dErr_wrt_dW (式4)
  75. // 以及训练速率eta
  76.  
  77. for ( jj=; jj<m_Weights.size(); ++jj )
  78. {
  79. oldValue = m_Weights[ jj ]->value;
  80. newValue = oldValue.dd - etaLearningRate * dErr_wrt_dWn[ jj ];
  81. m_Weights[ jj ]->value = newValue;
  82. }
  83. }

[CLPR]BP神经网络的C++实现的更多相关文章

  1. BP神经网络原理及python实现

    [废话外传]:终于要讲神经网络了,这个让我踏进机器学习大门,让我读研,改变我人生命运的四个字!话说那么一天,我在乱点百度,看到了这样的内容: 看到这么高大上,这么牛逼的定义,怎么能不让我这个技术宅男心 ...

  2. BP神经网络

    秋招刚结束,这俩月没事就学习下斯坦福大学公开课,想学习一下深度学习(这年头不会DL,都不敢说自己懂机器学习),目前学到了神经网络部分,学习起来有点吃力,把之前学的BP(back-progagation ...

  3. 数据挖掘系列(9)——BP神经网络算法与实践

    神经网络曾经很火,有过一段低迷期,现在因为深度学习的原因继续火起来了.神经网络有很多种:前向传输网络.反向传输网络.递归神经网络.卷积神经网络等.本文介绍基本的反向传输神经网络(Backpropaga ...

  4. BP神经网络推导过程详解

    BP算法是一种最有效的多层神经网络学习方法,其主要特点是信号前向传递,而误差后向传播,通过不断调节网络权重值,使得网络的最终输出与期望输出尽可能接近,以达到训练的目的. 一.多层神经网络结构及其描述 ...

  5. 极简反传(BP)神经网络

    一.两层神经网络(感知机) import numpy as np '''极简两层反传(BP)神经网络''' # 样本 X = np.array([[0,0,1],[0,1,1],[1,0,1],[1, ...

  6.  BP神经网络

     BP神经网络基本原理 BP神经网络是一种单向传播的多层前向网络,具有三层或多层以上的神经网络结构,其中包含输入层.隐含层和输出层的三层网络应用最为普遍. 网络中的上下层之间实现全连接,而每层神经元之 ...

  7. BP神经网络学习笔记_附源代码

    BP神经网络基本原理: 误差逆传播(back propagation, BP)算法是一种计算单个权值变化引起网络性能变化的较为简单的方法.由于BP算法过程包含从输出节点开始,反向地向第一隐含层(即最接 ...

  8. 机器学习(一):梯度下降、神经网络、BP神经网络

    这几天围绕论文A Neural Probability Language Model 看了一些周边资料,如神经网络.梯度下降算法,然后顺便又延伸温习了一下线性代数.概率论以及求导.总的来说,学到不少知 ...

  9. 基于Storm 分布式BP神经网络,将神经网络做成实时分布式架构

    将神经网络做成实时分布式架构: Storm 分布式BP神经网络:    http://bbs.csdn.net/topics/390717623 流式大数据处理的三种框架:Storm,Spark和Sa ...

随机推荐

  1. 前端面试题之 sum(2)(3) (链式调用,toString,柯里化,数组操作)

    写一个函数让下面两个输出结果相同:console.log(sum(2)(3));console.log(sum(2,3)); var sum = (function() { var list = [] ...

  2. INNODB存储引擎表空间

    这片文章主要是对innodb表空间的一些说明: innodb中表空间可以分为以下几种: 系统表空间 独立表空间 undo表空间 临时表空间(temporary tablespace) 通用表空间(ge ...

  3. 20145106 《Java程序设计》第5周学习总结

    教材学习内容总结 个人认为本周的学习在很大程度上是作为之前学习内容的补充.之前编译的程序相信所有人都会失败过,error算是我程序的老主顾了. 第八章名为"异常处理".本章中,我们 ...

  4. MAC、MII、PHY的关系与区别

    嗯,实验室的嵌入式项目需要写设备驱动,我分到了网络驱动的活,写一个适配SylixOS的(这里夸一句,这个真是国内相当不错的嵌入式实时操作系统了)MPC8377的网卡驱动,说实话原来从来没接触过写驱动的 ...

  5. stm32 Flash读写独立函数[库函数]

    一. stm32的FLASH分为 1.主存储块:用于保存具体的程序代码和用户数据,主存储块是以页为单位划分的, 一页大小为1KB.范围为从地址0x08000000开始的128KB内. 2.信息块   ...

  6. 如何在命令提示符下编译运行含有Package的java文件

    这篇是大二自学Java的时候记下的笔记,中午回顾印象笔记的时候意外看到了这篇.看到多年前写下的文字,我想起那时候我对Java的懵懵懂懂,每天晚上在图书馆照着书写书上的示例代码,为一个中文分号绞尽脑汁, ...

  7. ubuntu 16.04下更换源和pip源【转】

    本文转载自:https://blog.csdn.net/weixin_41500849/article/details/80246221 写在前面的话 本文主要内容是更换系统源为清华大学源,更换pyt ...

  8. Combobox绑定泛型字典时提示“复杂的 DataBinding 接受 IList 或 IListSource 作为数据源”的解决方法

    一般情况下我们会将 DataTable 或 DataView 绑定到 Combobox 控件上,这时候进行数据绑定是没有问题的,因为DataTable 和 DataView 都继承了 IList 接口 ...

  9. javascript之分时函数

    在一些开发场景中,我们可能会一次性向文档中注入上千个节点,在短时间内向浏览器中大量添加DOM节点可能会让浏览器吃不消,结果往往会让浏览器卡顿或吃不消,解决方案之一便是使用分时函数(timeChunk) ...

  10. Rails 5 Test Prescriptions 第7章 double stub mock

    https://relishapp.com/rspec/rspec-mocks/v/3-7/docs/basics/test-doubles 你有一个问题,如果想为程序添加一个信用卡程序用于自己挣钱. ...