[CLPR]BP神经网络的C++实现
文章翻译自: http://www.codeproject.com/Articles/16650/Neural-Network-for-Recognition-of-Handwritten-Digi
如何在C++中实现一个神经网络类?
主要有四个不同的类需要我们来考虑:
- 层 - layers
- 层中的神经元 - neurons
- 神经元之间的连接 - connections
- 连接的权值 - weights
这四类都在下面的代码中体现, 集中应用于第五个类 - 神经网络(neural network)上. 它就像一个容器, 用于和外部交流的接口. 下面的代码大量使用了STL的vector.
- // simplified view: some members have been omitted,
- // and some signatures have been altered
- // helpful typedef's
- typedef std::vector< NNLayer* > VectorLayers;
- typedef std::vector< NNWeight* > VectorWeights;
- typedef std::vector< NNNeuron* > VectorNeurons;
- typedef std::vector< NNConnection > VectorConnections;
- // Neural Network class
- class NeuralNetwork
- {
- public:
- NeuralNetwork();
- virtual ~NeuralNetwork();
- void Calculate( double* inputVector, UINT iCount,
- double* outputVector = NULL, UINT oCount = );
- void Backpropagate( double *actualOutput,
- double *desiredOutput, UINT count );
- VectorLayers m_Layers;
- };
- // Layer class
- class NNLayer
- {
- public:
- NNLayer( LPCTSTR str, NNLayer* pPrev = NULL );
- virtual ~NNLayer();
- void Calculate();
- void Backpropagate( std::vector< double >& dErr_wrt_dXn /* in */,
- std::vector< double >& dErr_wrt_dXnm1 /* out */,
- double etaLearningRate );
- NNLayer* m_pPrevLayer;
- VectorNeurons m_Neurons;
- VectorWeights m_Weights;
- };
- // Neuron class
- class NNNeuron
- {
- public:
- NNNeuron( LPCTSTR str );
- virtual ~NNNeuron();
- void AddConnection( UINT iNeuron, UINT iWeight );
- void AddConnection( NNConnection const & conn );
- double output;
- VectorConnections m_Connections;
- };
- // Connection class
- class NNConnection
- {
- public:
- NNConnection(UINT neuron = ULONG_MAX, UINT weight = ULONG_MAX);
- virtual ~NNConnection();
- UINT NeuronIndex;
- UINT WeightIndex;
- };
- // Weight class
- class NNWeight
- {
- public:
- NNWeight( LPCTSTR str, double val = 0.0 );
- virtual ~NNWeight();
- double value;
- };
类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数组.
- // simplified code
- void NeuralNetwork::Calculate(double* inputVector, UINT iCount,
- double* outputVector /* =NULL */,
- UINT oCount /* =0 */)
- {
- VectorLayers::iterator lit = m_Layers.begin();
- VectorNeurons::iterator nit;
- // 第一层是输入层:
- // 直接设置所有的神经元输出为给定的输入向量即可
- if ( lit < m_Layers.end() )
- {
- nit = (*lit)->m_Neurons.begin();
- int count = ;
- ASSERT( iCount == (*lit)->m_Neurons.size() );
- // 输入和神经元个数应当一一对应
- while( ( nit < (*lit)->m_Neurons.end() ) && ( count < iCount ) )
- {
- (*nit)->output = inputVector[ count ];
- nit++;
- count++;
- }
- }
- // 调用Calculate()迭代剩余层
- for( lit++; lit<m_Layers.end(); lit++ )
- {
- (*lit)->Calculate();
- }
- // 使用结果设置每层输出
- if ( outputVector != NULL )
- {
- lit = m_Layers.end();
- lit--;
- nit = (*lit)->m_Neurons.begin();
- for ( int ii=; ii<oCount; ++ii )
- {
- outputVector[ ii ] = (*nit)->output;
- nit++;
- }
- }
- }
在层中的Calculate()函数中, 层会迭代其中的所有神经元, 对于每一个神经元, 它的输出通过前馈公式给出:
这个公式通过迭代每个神经元的所有连接来实现, 获取对应的权重和对应的前一层神经元的输出. 如下:
- // simplified code
- void NNLayer::Calculate()
- {
- ASSERT( m_pPrevLayer != NULL );
- VectorNeurons::iterator nit;
- VectorConnections::iterator cit;
- double dSum;
- for( nit=m_Neurons.begin(); nit<m_Neurons.end(); nit++ )
- {
- NNNeuron& n = *(*nit); // 取引用
- cit = n.m_Connections.begin();
- ASSERT( (*cit).WeightIndex < m_Weights.size() );
- // 第一个权值是偏置
- // 需要忽略它的神经元下标
- dSum = m_Weights[ (*cit).WeightIndex ]->value;
- for ( cit++ ; cit<n.m_Connections.end(); cit++ )
- {
- ASSERT( (*cit).WeightIndex < m_Weights.size() );
- ASSERT( (*cit).NeuronIndex <
- m_pPrevLayer->m_Neurons.size() );
- dSum += ( m_Weights[ (*cit).WeightIndex ]->value ) *
- ( m_pPrevLayer->m_Neurons[
- (*cit).NeuronIndex ]->output );
- }
- n.output = SIGMOID( dSum );
- }
- }
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中.
本层的输出则作为前一层的输入.
- // simplified code
- void NeuralNetwork::Backpropagate(double *actualOutput,
- double *desiredOutput, UINT count)
- {
- // 神经网络的BP过程,
- // 从最后一层迭代向前处理到第一层为止.
- // 首先, 单独计算最后一层,
- // 因为它提供了前一层所需的梯度信息
- // (i.e., dErr_wrt_dXnm1)
- // 变量含义:
- //
- // Err - 整个NN的输出误差
- // Xn - 第n层的输出向量
- // Xnm1 - 前一层的输出向量
- // Wn - 第n层的权值向量
- // Yn - 第n层的激励函数输入值
- // 即, 在应用压缩函数(squashing function)前的权值和// F - 挤压函数: Xn = F(Yn)
- // F' - 压缩函数(squashing function)的梯度
- // 比如, 令 F = tanh,
- // 则 F'(Yn) = 1 - Xn^2, 梯度可以通过输出来计算, 不需要输入信息
- VectorLayers::iterator lit = m_Layers.end() - ; // 取最后一层
- std::vector< double > dErr_wrt_dXlast( (*lit)->m_Neurons.size() ); // 记录后层神经元误差对输入的梯度
- std::vector< std::vector< double > > differentials; //记录每一层输出对输入的梯度
- int iSize = m_Layers.size(); // 层数
- differentials.resize( iSize );
- int ii;
- // 计算最后一层的 dErr_wrt_dXn 来开始整个迭代.
- // 对于标准的MSE方程
- // (比如, 0.5*sumof( (actual-target)^2 ),
- // 梯度表达式就仅仅是期望和实际的差: Xn - Tn
- for ( ii=; ii<(*lit)->m_Neurons.size(); ++ii )
- {
- dErr_wrt_dXlast[ ii ] =
- actualOutput[ ii ] - desiredOutput[ ii ];
- }
- // 保存 Xlast 并分配内存存储剩余的梯度
- differentials[ iSize- ] = dErr_wrt_dXlast; // 最后一层的梯度
- for ( ii=; ii<iSize-; ++ii )
- {
- differentials[ ii ].resize(
- m_Layers[ii]->m_Neurons.size(), 0.0 );
- }
- // 迭代每个层, 包括最后一层但不包括第一层
- // 同时求得每层的BP误差并矫正权值// 返回梯度dErr_wrt_dXnm1用于下一次迭代
- ii = iSize - ;
- for ( lit; lit>m_Layers.begin(); lit--)
- {
- (*lit)->Backpropagate( differentials[ ii ],
- differentials[ ii - ], m_etaLearningRate ); // 调用每一层的BP接口
- --ii;
- }
- differentials.clear();
- }
在NNLayer::Backpropagate()中, 层实现了式3~5, 计算出了梯度. 实现了式6来更新本层的权重. 在下面的代码中, 激励函数的梯度被定义为 DSIGMOID.
- // simplified code
- void NNLayer::Backpropagate( std::vector< double >& dErr_wrt_dXn /* in */,
- std::vector< double >& dErr_wrt_dXnm1 /* out */,
- double etaLearningRate )
- {
- double output;
- // 计算式 (3): dErr_wrt_dYn = F'(Yn) * dErr_wrt_Xn
- for ( ii=; ii<m_Neurons.size(); ++ii ) // 遍历所有神经元
- {
- output = m_Neurons[ ii ]->output; // 神经元输出
- dErr_wrt_dYn[ ii ] = DSIGMOID( output ) * dErr_wrt_dXn[ ii ]; // 误差对输入的梯度
- }
- // 计算式 (4): dErr_wrt_Wn = Xnm1 * dErr_wrt_Yn
- // 对于本层的每个神经元, 遍历前一层的连接
- // 更新对应权值的梯度
- ii = ;
- for ( nit=m_Neurons.begin(); nit<m_Neurons.end(); nit++ ) // 迭代本层所有神经元
- {
- NNNeuron& n = *(*nit); // 取引用
- for ( cit=n.m_Connections.begin(); cit<n.m_Connections.end(); cit++ ) // 遍历每个神经元的后向连接
- {
- kk = (*cit).NeuronIndex; // 连接的前一层神经元标号
- if ( kk == ULONG_MAX ) // 偏置的标号固定为最大整形量
- {
- output = 1.0; // 偏置
- }
- else // 其他情况下 神经元输出等于前一层对应神经元的输出 Xn-1
- {
- output = m_pPrevLayer->m_Neurons[ kk ]->output;
- }
- // 误差对权值的梯度
- // 每次使用对应神经元的误差对输入的梯度
- dErr_wrt_dWn[ (*cit).WeightIndex ] += dErr_wrt_dYn[ ii ] * output;
- }
- ii++;
- }
- // 计算式 (5): dErr_wrt_Xnm1 = Wn * dErr_wrt_dYn,// 需要dErr_wrt_Xn的值来进行前一层的BP
- ii = ;
- for ( nit=m_Neurons.begin(); nit<m_Neurons.end(); nit++ ) // 迭代所有神经元
- {
- NNNeuron& n = *(*nit); // 取引用
- for ( cit=n.m_Connections.begin();
- cit<n.m_Connections.end(); cit++ ) // 遍历每个神经元所有连接
- {
- kk=(*cit).NeuronIndex;
- if ( kk != ULONG_MAX )
- {
- // 排除了ULONG_MAX, 提高了偏置神经元的重要性// 因为我们不能够训练偏置神经元
- nIndex = kk;
- dErr_wrt_dXnm1[ nIndex ] += dErr_wrt_dYn[ ii ] *
- m_Weights[ (*cit).WeightIndex ]->value;
- }
- }
- ii++; // ii 跟踪神经元下标
- }
- // 计算式 (6): 更新权值
- // 在本层使用 dErr_wrt_dW (式4)
- // 以及训练速率eta
- for ( jj=; jj<m_Weights.size(); ++jj )
- {
- oldValue = m_Weights[ jj ]->value;
- newValue = oldValue.dd - etaLearningRate * dErr_wrt_dWn[ jj ];
- m_Weights[ jj ]->value = newValue;
- }
- }
[CLPR]BP神经网络的C++实现的更多相关文章
- BP神经网络原理及python实现
[废话外传]:终于要讲神经网络了,这个让我踏进机器学习大门,让我读研,改变我人生命运的四个字!话说那么一天,我在乱点百度,看到了这样的内容: 看到这么高大上,这么牛逼的定义,怎么能不让我这个技术宅男心 ...
- BP神经网络
秋招刚结束,这俩月没事就学习下斯坦福大学公开课,想学习一下深度学习(这年头不会DL,都不敢说自己懂机器学习),目前学到了神经网络部分,学习起来有点吃力,把之前学的BP(back-progagation ...
- 数据挖掘系列(9)——BP神经网络算法与实践
神经网络曾经很火,有过一段低迷期,现在因为深度学习的原因继续火起来了.神经网络有很多种:前向传输网络.反向传输网络.递归神经网络.卷积神经网络等.本文介绍基本的反向传输神经网络(Backpropaga ...
- BP神经网络推导过程详解
BP算法是一种最有效的多层神经网络学习方法,其主要特点是信号前向传递,而误差后向传播,通过不断调节网络权重值,使得网络的最终输出与期望输出尽可能接近,以达到训练的目的. 一.多层神经网络结构及其描述 ...
- 极简反传(BP)神经网络
一.两层神经网络(感知机) import numpy as np '''极简两层反传(BP)神经网络''' # 样本 X = np.array([[0,0,1],[0,1,1],[1,0,1],[1, ...
- BP神经网络
BP神经网络基本原理 BP神经网络是一种单向传播的多层前向网络,具有三层或多层以上的神经网络结构,其中包含输入层.隐含层和输出层的三层网络应用最为普遍. 网络中的上下层之间实现全连接,而每层神经元之 ...
- BP神经网络学习笔记_附源代码
BP神经网络基本原理: 误差逆传播(back propagation, BP)算法是一种计算单个权值变化引起网络性能变化的较为简单的方法.由于BP算法过程包含从输出节点开始,反向地向第一隐含层(即最接 ...
- 机器学习(一):梯度下降、神经网络、BP神经网络
这几天围绕论文A Neural Probability Language Model 看了一些周边资料,如神经网络.梯度下降算法,然后顺便又延伸温习了一下线性代数.概率论以及求导.总的来说,学到不少知 ...
- 基于Storm 分布式BP神经网络,将神经网络做成实时分布式架构
将神经网络做成实时分布式架构: Storm 分布式BP神经网络: http://bbs.csdn.net/topics/390717623 流式大数据处理的三种框架:Storm,Spark和Sa ...
随机推荐
- 前端面试题之 sum(2)(3) (链式调用,toString,柯里化,数组操作)
写一个函数让下面两个输出结果相同:console.log(sum(2)(3));console.log(sum(2,3)); var sum = (function() { var list = [] ...
- INNODB存储引擎表空间
这片文章主要是对innodb表空间的一些说明: innodb中表空间可以分为以下几种: 系统表空间 独立表空间 undo表空间 临时表空间(temporary tablespace) 通用表空间(ge ...
- 20145106 《Java程序设计》第5周学习总结
教材学习内容总结 个人认为本周的学习在很大程度上是作为之前学习内容的补充.之前编译的程序相信所有人都会失败过,error算是我程序的老主顾了. 第八章名为"异常处理".本章中,我们 ...
- MAC、MII、PHY的关系与区别
嗯,实验室的嵌入式项目需要写设备驱动,我分到了网络驱动的活,写一个适配SylixOS的(这里夸一句,这个真是国内相当不错的嵌入式实时操作系统了)MPC8377的网卡驱动,说实话原来从来没接触过写驱动的 ...
- stm32 Flash读写独立函数[库函数]
一. stm32的FLASH分为 1.主存储块:用于保存具体的程序代码和用户数据,主存储块是以页为单位划分的, 一页大小为1KB.范围为从地址0x08000000开始的128KB内. 2.信息块 ...
- 如何在命令提示符下编译运行含有Package的java文件
这篇是大二自学Java的时候记下的笔记,中午回顾印象笔记的时候意外看到了这篇.看到多年前写下的文字,我想起那时候我对Java的懵懵懂懂,每天晚上在图书馆照着书写书上的示例代码,为一个中文分号绞尽脑汁, ...
- ubuntu 16.04下更换源和pip源【转】
本文转载自:https://blog.csdn.net/weixin_41500849/article/details/80246221 写在前面的话 本文主要内容是更换系统源为清华大学源,更换pyt ...
- Combobox绑定泛型字典时提示“复杂的 DataBinding 接受 IList 或 IListSource 作为数据源”的解决方法
一般情况下我们会将 DataTable 或 DataView 绑定到 Combobox 控件上,这时候进行数据绑定是没有问题的,因为DataTable 和 DataView 都继承了 IList 接口 ...
- javascript之分时函数
在一些开发场景中,我们可能会一次性向文档中注入上千个节点,在短时间内向浏览器中大量添加DOM节点可能会让浏览器吃不消,结果往往会让浏览器卡顿或吃不消,解决方案之一便是使用分时函数(timeChunk) ...
- Rails 5 Test Prescriptions 第7章 double stub mock
https://relishapp.com/rspec/rspec-mocks/v/3-7/docs/basics/test-doubles 你有一个问题,如果想为程序添加一个信用卡程序用于自己挣钱. ...