BP神经网络从理论到应用(一):C++实现

121 篇文章 40 订阅

为什么基于数学原理的程序会那么难以实现?因为缺乏一定的训练。事实上,基于数学定理的算法并不比「数据结构与算法」的算法复杂更多(前提是对数学原理的深入理解),它们往往并不需要高级的数据结构作为支撑(除非算法本身即是基于图、基于树、基于hash散列),也不要高级的语法特性完成某一功能,算法所赖以实现的基石仍是基本的循环分判断等机制。

为什么基于数学原理的程序会那么难以阅读?因为缺乏程序语言向数学公式的转换的训练,还是缺乏训练。(迷失在茫茫的记号(notation)、变量(variant)之海,不明白其中的变量所代表的数学意义,etc)。

demo演示

学习任何一门知识或者技术的方法,还应当遵循马克思哲学体系关于认识论的部分,即:认识的两次飞跃,从感性认识上升为理性认识,再从理论回归实践。两个过程互为因果,循环往复,交替向前,直至建立起对一门科学对一个领域全面而深入的认识。
这个观点是怎么来的呢,是看「晓说」来的,高晓松老师(晓松老师也是借鉴「诗经」的创作手法,赋比兴,「关关雎鸠,在河之洲,窈窕淑女,君子好逑」,悲伤还是欢愉,看门见山,直抒胸臆)。在讲台湾,将日本,将欧洲,都会先说感官,衣食住行,国民性,这些浅表的,然后庖丁解牛,深入内部,探究其根源,而不是一上来就是历史、就是深刻、就是宏观叙事,一来容易接受,二则也较为有趣。

神经网络拓扑结构示意图:
这里写图片描述
该网络的拓扑结构为3-3-2,3 inputs,2 outputs。
所适配的训练样本集自然(3个输入值,2个输出值),根据不同的样本,反复更新神经元之间的权值,所要学习的参数(也即权值)的个数为(3+1)*3+(3+1)*2,加1表示bias neuron(其输出值为1)也参与向下一层(layer)的传播。

神经网络的执行流程

这里我们以一个小应用(神经网络解异或(xor)问题)来说明神经网络的基本处理流程,建立起感官认识。
首先generate如下格式的数据集(用以训练):

topology: 2 4 1 // 代表神经网络拓扑结构,即该神经网络共三层(three layers),每一层神经元的个数分别为2,4,和1个。
in: 0 1 // 输入为0、1时,输出为1,等四种情况,即为异或问题
out: 1
in: 0 1
out: 1
in: 1 0
out: 1

程序读取该训练集,分别对每一个样本进行训练(input values -> forward propagation 前向,target values -> backward propagation 后向),实现权值的更新(整个训练过程得到的就是神经元之间的权值)。

while(!trainData.isEof())       // trainData是输入文件流
{
    vector<double> inputVals, targetVals;
    trainData.getNextInputs(inputVals);
    myNet.feedWard(inputVals);
    trainData.getTargetOutputs(targetVals)
    myNet.backProg(targetVals);
}

下图为每次的训练误差(RMS:Root Mean Square)随训练样本增加的变化情况:
这里写图片描述

算法流程及规格

常用记号

为了下文表述问题的方便,这里先引入一些记号(notation),记号并不多,也不复杂,刚好作为类结构设计(面向对象)Neuron中的成员变量(member variables):
- Wlij l1 层第 i 个神经元(neuron)到第l层第 j 个神经元的权值

vector<double> m_outputVals; 
  • slj:加权和(weighted sum), 或者通俗地说就是最终的的得分值score

    • xlj :对上一层的加权和weighted sum转换后的神经元的输出值
    • m_outputVals = Neuron::transferFcn(weighted_sum);
      • δlj :其定义式为 δlj=enslj ,
      double m_gradient;

      传递函数(transfer function)设计:

      算法流程

      神经网络的全部难点正在于反向传播计算权值梯度 δlj

      这里先总体、定性地说明神经网络的算法流程。
      1. Initialization:初始化各层(layer)神经元(neuron)的权重(weight, Wlij );
      2. Feed forward:前向更新各层各神经元的输入( xli );

      slj=i=0dl1Wlijxl1i

      xlj=tanh(slj)

      3. Back Propagation:后向更新各个神经元的权值梯度(delta weight, δli ),已知神经元共 L
      δli={k=0dl11δl+1kWljk}tanh(slj)

      又根据 en=(ynxL1)2 ,可知对于输出层来说:
      δLj=2(ynxL1)tanh(slj)

      4. Update:更新权重 WlijWlij+ηxl1iδlj ,更新的顺序应当是从后往前,也即更新倒数第二层向最后一层的权重,然后倒数第三层向倒数第二层的权重,这本身也符合权值扩散的方向(总不能反过来,第二层向第一层,第三层向第二层)。

      Cpp实现

      好的数据结构设计是算法完成的一半。

      类结构设计

      UML类图
      这里写图片描述

      核心代码段分析

      Net 类构造

      Net::Net(const vector<unsigned> &topology)
      {
          unsigned numLayers = topology.size();
          for (unsigned layerNum = 0; layerNum < numLayers; ++layerNum) {
              m_layers.push_back(Layer());
              unsigned numOutputs = layerNum == topology.size() - 1 ? 0 : topology[layerNum + 1];
      
          //  never forget to add a bias neuron in each layer.
          for (unsigned neuronNum = 0; neuronNum <= topology[layerNum]; ++neuronNum) {
                      m_layers.back().push_back(Neuron(numOutputs, neuronNum));
                      cout << "Made a Neuron!" << endl;
                  }
          // output value of bias neuron in each layer equals 1.0
          m_layers.back().back().setOutputVal(1.0);
          }
      }

      Neuron类构造

      Neuron::Neuron(unsigned numOutputs, unsigned myIndex):m_myIdx(myIdx)
      {
          for (unsigned c = 0; c < numOutputs; ++c) 
              m_outputWeights.push_back(Neuron::rndomWeight());
      }

      Neuron::feedForward() 前向

      void Neuron::feedForward(const Layer &prevLayer)
      {
          double weighted_sum = 0.0;
      
          for (unsigned n = 0; n < prevLayer.size(); ++n) {
              weighted_sum += prevLayer[n].getOutputVal() *
                  prevLayer[n].m_outputWeights[m_myIdx];
          }
      
          m_outputVal = Neuron::transferFunction(weighted_sum );
      }

      Net::backProg()

      void Net::backProp(const vector<double> &targetVals)
      {
          //1. Calculate overall net error(RMS of output neuron errors)
      
          Layer &outputLayer = m_layers.back();
          m_error = 0.0;
      
          for (unsigned n = 0; n < outputLayer.size() - 1; ++n) {
              double delta = targetVals[n] - outputLayer[n].getOutputVal();
              m_error += delta * delta;
          }
          m_error /= outputLayer.size() - 1; // get average error squared
          m_error = sqrt(m_error); // RMS
      
          //2. Implement a recent average measurement
      
              m_recentAverageError =
              (m_recentAverageError * m_recentAverageSmoothingFactor + m_error)
              / (m_recentAverageSmoothingFactor + 1.0);
      
          //3. Calculate output layer gradients
      
              for (unsigned n = 0; n < outputLayer.size() - 1; ++n) {
                  outputLayer[n].calcOutputGradients(targetVals[n]);
              }
      
          //4. Calculate hidden layer gradients
      
              for (unsigned layerNum = m_layers.size() - 2; layerNum > 0; --layerNum) {
                  Layer &hiddenLayer = m_layers[layerNum];
                  Layer &nextLayer = m_layers[layerNum + 1];
      
                  for (unsigned n = 0; n < hiddenLayer.size(); ++n) {
                      hiddenLayer[n].calcHiddenGradients(nextLayer);
                  }
              }
      
          //5. For all layers from outputs to first hidden layer,
          //  update connection weights
      
              for (unsigned layerNum = m_layers.size() - 1; layerNum > 0; --layerNum) {
                  Layer &layer = m_layers[layerNum];
                  Layer &prevLayer = m_layers[layerNum - 1];
      
                  for (unsigned n = 0; n < layer.size() - 1; ++n) {
                      layer[n].updateInputWeights(prevLayer);
                  }
              }
      }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五道口纳什

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值