Neural Network based on Eorr Back Propagation典型BP网络c++实现

参考资料:人工神经网络-韩力群PPT
   
    看了一些关于基于神经网络的语言模型, 与传统语言模型相比, 除了计算量让人有点不满意之外, 不需要额外的平滑算法, 感觉它们的效果让人惊讶。 这些网络里面都能看到BP的影子, 可以说BP网络是最基本的, 掌握扎实了, 对其他结构理解会更深刻, 于是早在学习语言模型之前我自己曾经用c++写过一个简单的BP网络,虽然功能简单,只有最基本的三层结构,但让自己对误差反传理解的更深刻。那个时候自己还没开始写博客, 现在把以前的代码放上来吧, 那个时候写代码没考虑任何优化,达到功能即可,所以很多模块没有做成函数,也没有考虑如何加速网络,比如矩阵相乘,我用的就是最老实的多重循环来计算。最后利用这个网络给了一个简单的例子——用这个BP网络来计算异或运算。这样更容易理解,网络先进行训练,由人为给定异或运算的样本规则,然后让其自行学习,最后是测试模块(那个时候我写的函数叫work,当时不知道准确的叫法应该是test).
    本文不对BP网络做推导,因为网上随便一搜就很多,下面是一个典型的三层结构,图来自人工神经网络-韩力群PPT


下面是代码,代码组织方式是按照标准的公式推导的,以前写这个代码时参照了网上的,但现在确实找不到是哪儿的了,就没办法引用了。下面直接上代码,因为注释的非常详细,所以过程就不做解释了。

首先BP网络的结构定义在BpNet.h,内容如下:
#include <iostream>  
#include <fstream>  
#include <ctime>  
#include <cmath>  
  
using namespace std;  
  
/*常量区域*/  
#define N 4                                         //样本的个数  
#define IN 2                                        //输入层神经元个数  
#define HN 2                                        //隐层神经元个数  
#define ON 1                                        //输出层神经元个数  
  
//定义存放学习样本的结构  
class StudyData  
{  
public:  
    float input[IN];                                //输入样本  
    float teach[ON];                                //期望输出(教师信号)  
      
    StudyData();  
    virtual ~StudyData();  
};  
  
//定义BP神经网络的类结构  
class BpNet    
{  
public:  
    void GetOutput();                                   //从键盘输入数据,输出进行保存  
    void Work(char *weight, char *threshold);           //将训练好的数据进行工作  
    double GetSumErr();                                 //得到总误差  
                                                        //进行训练  
    void Train(char *sampleFileName, char *weight, char *threshold);  
    void ReadWeight(char *weight, char *threshold);     //读取权值阈值到神经网络中  
    void SaveBpNet(char *weight, char *threshold);      //保存权值  
    void UpdateWeight(int m);                           //更新权值  
    void ErrorSignal(int m);                            //算误差信号  
    void NetInputOutput(int m);                         //算各层输出输入  
    void GetTrainingData(char *sampleFileName);         //从外存获取样本集  
    void StartShow(void);                                 
  
    StudyData studyData[N];                             //存放多组学习样本的数组  
    float W[HN][IN];                                    //输入层到隐层权值数组  
    float V[ON][HN];                                    //隐层到输出层权值数组  
    float HU_HN[HN];                                    //隐层神经元阈值数组  
    float HU_ON[ON];                                    //输出层神经元阈值数组  
    float IN_HN[HN];                                    //隐层的输入  
    float OUT_HN[HN];                                   //隐层的输出  
    float IN_ON[ON];                                    //输出层的输入  
    float OUT_ON[ON];                                   //输出层的输出  
    float E[N];                                         //样本组误差数组,每个分量为一组样本的误差  
    float stuRate1;                                     //输出层至隐层学习效率  
    float stuRate2;                                     //隐层至输入层学习效率  
    float errSignalON[ON];                              //δk,输出层的误差信号数组  
    float errSignalHN[HN];                              //δj,隐层的误差信号数组  
  
    BpNet();  
    virtual ~BpNet();  
};  

其次是BpNet.h的实现,如下:

#include "stdafx.h"   
#include "BpNet.h"   
  
//////////////////////////////////////////////////////////////////////   
// Construction/Destruction   
//////////////////////////////////////////////////////////////////////   
                                  
/*BpNet构造函数,对变量进行初始化*/   
BpNet::BpNet()  
{  
    srand(time(NULL));                                   //随机数种子   
  
     int  i, j;  
     for  (i=0; i<HN; i++)                              //输入层到隐层权值初始化为0附近的小数   
    {     
         for  (j=0; j<IN; j++)  
        {  
            W[i][j] = ( float )(((rand()/32767.0)*2-1)/2);  
        }  
    }  
  
     for  (i=0; i<ON; i++)                              //隐层到输出层权值初始化为0附近的小数   
    {     
         for  (j=0; j<HN; j++)  
        {  
            V[i][j] = ( float )(((rand()/32767.0)*2-1)/2);  
        }  
    }  
  
     for  (i=0; i<HN; i++)                              //阈值初始化   
    {  
        HU_HN[i] = 1.0;  
    }  
  
     for  (i=0; i<ON; i++)                              //阈值初始化   
    {  
        HU_ON[i] = 1.0;  
    }  
  
    stuRate1 = 0.5;                                      //输出层到隐层的学习率初始化   
    stuRate2 = 0.5;                                      //隐层到输入层的学习率初始化   
  
     //测试权值的输出   
/*  
    for (i=0; i<HN; i++)  
    {  
        for (j=0; j<IN; j++)  
        {  
            cout << W[i][j] << " ";  
        }  
        cout << endl;  
    }  
 
    cout << endl;  
    for (i=0; i<ON; i++)  
    {  
        for (j=0; j<HN; j++)  
        {  
            cout << V[i][j] << " ";  
        }  
        cout << endl;  
    }*/   
}  
  
/*BpNet析构函数,对动态申请内存进行释放*/   
BpNet::~BpNet()  
{  
  
}  
  
/*程序开始*/   
void  BpNet::StartShow()  
{  
    cout << endl;  
    cout <<  "*************BP算法的使用程序(C++版)********"  << endl << endl;  
}  
  
/**从文件中读取样本, 并显示到屏幕上以便于进行核对**/   
void  BpNet::GetTrainingData( char  *sampleFileName)  
{  
    ifstream infile(sampleFileName, ios::in);            //打开样本文件   
     if  (!infile)                                         //如果打开失败   
    {  
        cerr <<  "打开样本文件出错"  << endl;  
        exit(1);  
    }  
      
     int  i, j;                                            //循环下标   
  
     float  temp;  
      
     for  (i=0; i<N; i++)                                   //i表示每一趟读取一个样本   
    {  
         for  (j=0; j<IN+ON; j++)            
        {  
             if  (!(infile >> temp))                         //从文件读入一个数   
            {  
                cerr <<  "读入文件元素过程中出错,或以达到文件结尾"  << endl;  
            }  
  
             if  (j > (IN-1))                               //读入教师信号   
            {             
                studyData[i].teach[j-IN] = temp;  
            }  
             else                                          //读入样本输入   
            {  
                studyData[i].input[j] = temp;  
            }  
        }  
    }  
  
    infile.close();                                      //关闭文件   
  
     /*文件中的样本信息输出到屏幕*/   
    cout <<  "从指定文件中成功载入"    
         << N * (IN + ON)  
         <<  "个数据,显示如下:"  << endl << endl;  
  
     for  (i=0; i<IN; i++)                              //进行排版   
    {  
        cout <<  "输入值"  <<  "   " ;  
    }  
     for  (i=0; i<ON; i++)  
    {  
        cout <<  "期望值"  <<  "   " ;  
    }  
    cout << endl;  
     for  (i=0; i<N; i++)  
    {  
         for  (j=0; j<IN+ON; j++)  
        {  
             if  (j > (IN-1))                               //输出教师信号   
            {             
                cout <<studyData[i].teach[j-IN] <<  "         " ;  
            }  
             else                                          //输出样本输入   
            {  
                cout <<studyData[i].input[j] <<  "        " ;  
            }  
        }  
        cout << endl;  
    }  
}  
  
StudyData::StudyData()  
{  
  
}  
  
StudyData::~StudyData()  
{  
  
}  
  
void  BpNet::NetInputOutput( int  m)  
{  
     //求BP网络的神经网络各层进输入输出   
     //断言参数m合法 参数m表示第m组测试样本   
  
     int  i, j;  
     float  sum = 0.0;                      
  
     for  (i=0; i<HN; i++)                              //求隐层的净输入输出   
    {  
         for  (j=0; j<IN; j++)  
        {  
            sum += W[i][j] * studyData[m].input[j];      //算隐层第i个神经元不包含阈值的输入   
        }                                     
        IN_HN[i] = sum + HU_HN[i];                       //算隐层第i个神经元的净输入   
        OUT_HN[i] = 1.0 / (1.0 + exp(-IN_HN[i]));        //算隐层第i个神经元的输出   
    }  
  
    sum = 0.0;  
     for  (i=0; i<ON; i++)                              //求输出层的输入输出   
    {  
         for  (j=0; j<HN; j++)  
        {  
            sum += V[i][j] * OUT_HN[j];                  //算输出层第i个神经元的输入(不含阈值)   
        }  
        IN_ON[i] = sum + HU_ON[i];                       //算输出层第i个神经元的净输入   
        OUT_ON[i] = 1.0 / (1.0 + exp(-IN_ON[i]));        //算输出层第i个神经元的输出   
    }  
  
     //测试输入输出 打印到屏幕上   
/*  
    for (i=0; i<ON; i++)  
    {  
        cout << "输出层输出" << OUT_ON[i] << endl;  
    }  
    for (i=0; i<HN; i++)  
    {  
        cout << "隐层输入" << IN_HN[i] << endl;  
    }*/   
}  
  
void  BpNet::ErrorSignal( int  m)  
{  
     //算误差信号δk,δj   
     //断言m合法,m表示第m组样本   
  
     int  k;  
     float  absErr[ON];                                    //期望-输出即绝对误差   
     float  sqrErr = 0.0;                                  //误差平方和   
  
     for  (k=0; k<ON; k++)                               
    {  
        absErr[k] = studyData[m].teach[k] - OUT_ON[k];   //算绝对误差   
                                                         //算输出层的误差信号   
        errSignalON[k] = absErr[k] * OUT_ON[k] * (1.0-OUT_ON[k]);     
        sqrErr += absErr[k] * absErr[k];                 //算第m组样本的误差平方和     
    }  
    E[m] = sqrErr / 2;                                   //算第m组样本的总误差   
      
     int  j;                                               //下面算隐层的误差信号δj   
     float  sum;  
  
     for  (j=0; j<HN; j++)                               
    {  
        sum = 0.0;  
         for  (k=0; k<ON; k++)  
        {  
            sum += errSignalON[k] * V[k][j];  
        }  
        errSignalHN[j] = sum * OUT_HN[j] * (1-OUT_HN[j]); //得到隐层的误差信号   
    }  
  
}  
  
void  BpNet::UpdateWeight( int  m)  
{  
     //更新两层权值,阈值   
     //断言m合法,m表示第m组测试样本   
  
     int  i, j;  
     float  deltaWeight;                                   //权值的改变量   
  
     for  (i=0; i<ON; i++)                              //更新隐层到输出层权值   
    {  
         for  (j=0; j<HN; j++)  
        {  
            deltaWeight = stuRate1 * errSignalON[i] * OUT_HN[j]; //计算权值的改变量   
            V[i][j] += deltaWeight;                      //更新权值         
        }  
        HU_ON[i] += stuRate1 * errSignalON[i];           //更新输出层阈值   
    }  
  
     for  (i=0; i<HN; i++)                              //调整隐层到输出层的权值   
    {  
         for  (j=0; j<IN; j++)  
        {  
                                                         //权值改变量   
            deltaWeight = stuRate2 * errSignalHN[i] * studyData[m].input[j];  
            W[i][j] += deltaWeight;                      //更新权值   
        }  
        HU_HN[i] += stuRate2 * errSignalHN[i];           //更新隐层阈值   
    }  
}  
  
void  BpNet::SaveBpNet( char  *weight,  char  *threshold)  
{  
     //保存权值到指定的文件名weight内   
     //保存阈值到指定的文件名threshold内   
  
    ofstream outfile(weight, ios::out);                  //打开保存权值的文件   
     if  (!outfile)  
    {  
        cerr <<  "打开权值文件失败"  << endl;   
        exit(1);  
    }  
      
     int  i, j;  
     for  (i=0; i<HN; i++)                              //输入层到隐层的权值写入   
    {  
         for  (j=0; j<IN; j++)  
        {  
            outfile << W[i][j] <<  " " ;  
        }  
    }  
  
     for  (i=0; i<ON; i++)                              //将输出层到隐层的权值写入   
    {  
         for  (j=0; j<HN; j++)  
        {  
            outfile << V[i][j] <<  " " ;                    
        }  
    }  
    outfile.close();                                     //关闭文件   
  
    ofstream outfile2(threshold, ios::out);              //打开阈值文件   
     if  (!outfile2)  
    {  
        cerr <<  "阈值文件打开失败"  << endl;  
        exit(1);  
    }  
  
     for  (i=0; i<HN; i++)                              //写入隐层的阈值   
    {  
        outfile2 << HU_HN[i] <<  " " ;  
    }  
     for  (i=0; i<ON; i++)                              //写入输出层的阈值   
    {  
        outfile2 << HU_ON[i] <<  " " ;  
    }  
    outfile2.close();                                    //关闭文件   
}  
  
void  BpNet::ReadWeight( char  *weight,  char  *threshold)  
{  
     //从训练好网络中读取权值、阈值等   
  
    ifstream infile1(weight, ios::in);                   //打开文件   
     if  (!infile1)  
    {  
        cerr <<  "打开权值文件失败"  << endl;  
        exit(1);  
    }  
      
     int  i, j;  
     float  temp;  
     for  (i=0; i<HN; i++)                              //读取输入层权值到隐层权值   
    {  
         for  (j=0; j<IN; j++)  
        {  
             if  (infile1 >> temp)  
            {  
                W[i][j] = temp;  
            }  
        }  
    }  
  
     for  (i=0; i<ON; i++)                              //读取输出层到隐层的权值   
    {  
         for  (j=0; j<HN; j++)  
        {  
             if  (infile1 >> temp)  
            {  
                V[i][j] = temp;   
            }  
        }  
    }  
    infile1.close();                                     //关闭权值文件   
  
    ifstream infile2(threshold, ios::in);                //打开阈值文件       
     if  (!infile2)  
    {  
        cerr <<  "阈值文件打开失败"  << endl;  
        exit(1);  
    }  
  
     for  (i=0; i<HN; i++)                              //读取隐层阈值   
    {  
         if  (infile2 >> temp)  
        {  
            HU_HN[i] = temp;  
         /*  cout << "隐层的阈值是" << HU_HN[i] << endl;*/   
        }     
    }  
     for  (i=0; i<ON; i++)                              //读取输出层阈值   
    {  
         if  (infile2 >> temp)  
        {  
            HU_ON[i] = temp;  
         /*  cout << "输出层的阈值是" << HU_ON[i] << endl;*/   
        }  
    }  
}  
  
double  BpNet::GetSumErr()  
{  
     //求总误差,即所有样本的总误差   
     int  m;  
     double  sumErr = 0.0;  
     for  (m=0; m<N; m++)  
    {  
        sumErr += E[m];  
    }  
  
     return  sumErr;  
}  
  
void  BpNet::Train( char  *sampleFileName,  char  *weight,  char  *threshold)  
{  
     //对给定文件名sampleFileName提取样本数据进行训练   
     //训练达到一定的精度后将权值存于weight   
     //阈值存于threshold内   
     int  limitStudyTimes = 400000;                        //限定内的学习次数   
     long   int  studyFileTimes = 0;                         //学习文件次数,即对sample整个文件学习一次,算一次   
     long   int  studySampleTimes = 0;                       //样本学习次数,一组样本学习一次算一次   
     double  minErr = 0.000001;                            //最小的学习误差,达到这个精度后训练完毕   
     double  sumErr;                                       //由实际训练得到的样本总误差   
     int  m;                                               //表示sample文件第m组样本   
  
    StartShow();                                         //程序开始界面   
    cout <<  "现在是训练模式···"  << endl;  
    GetTrainingData(sampleFileName);                     //读取样本数据到内存   
     do    
    {  
        studyFileTimes++;  
         for  (m=0; m<N; m++)                               //N组样本进行学习训练,循环完毕即一个文件学习完毕   
        {  
            NetInputOutput(m);                           //算网络内部输入输出   
            ErrorSignal(m);                              //算误差信号   
            UpdateWeight(m);                             //调整权值   
        }  
  
        sumErr = GetSumErr();                            //所有样本的总误差     
  
         if  (studyFileTimes > limitStudyTimes)             //超出学习的限定次数,估计收敛不了了,在下去就是无限循环,强制停止   
        {  
            cout <<  "超出限定的学习次数,超时了,强制停止程序"  << endl;  
             break ;  
        }  
        cout <<  "正在进行的训练次数: " << studyFileTimes <<  "\r" ;  
  
  
    }  while  (sumErr > minErr);  
  
    SaveBpNet(weight, threshold);                        //保存权值、阈值   
  
    cout <<  "学习次数是:"  << studyFileTimes << endl;  
    cout <<  "最后误差是"  << sumErr << endl;  
    cout <<  "权值文件"  << weight <<  "已成功保存"  << endl;  
    cout <<  "阈值文件"  << threshold <<  "已成功保存"  << endl;  
}  
  
  
  
void  BpNet::Work( char  *weight,  char  *threshold)  
{  
     //将训练完毕后得到的权值用于工作   
     //从键盘输入数据,由神经网络输出结果   
  
    ReadWeight(weight, threshold);                       //读取文件中训练完毕的权值阈值到神经网络中   
     char  flag;                                           //控制是否继续输入   
     int  i;  
  
    cout <<  "现在是工作模式···"  << endl;  
     while  (1)  
    {  
        GetOutput();                                     //从键盘输入数据,神经网络的输出存于OUT_ON[]中   
                  
         for  (i=0; i<ON; i++)  
        {  
            cout <<  "输出为:"  << OUT_ON[i] <<  " " ;  
        }  
        cout << endl;  
          
        cout <<  "你是否想要继续输入Y/N"  << endl;  
        cin >> flag;  
         if  ( ( 'n' ==flag) || ( 'N' ==flag) )  
        {  
             break ;  
        }  
    }  
}  
  
void  BpNet::GetOutput()  
{  
     //从键盘输入数据,神经网络的输出存于ON[]中   
      
     int  i, j;  
     float  sum = 0.0;  
     float  input[IN];  
      
    cout <<  "请输入神经网络的"  << IN <<  "个输入数据:(以空格隔开)"  << endl;  
     for  (i=0; i<IN; i++)                              //输入数据   
    {  
        cin >> input[i];  
    }  
      
     for  (i=0; i<HN; i++)                              //求隐层的净输入输出   
    {  
         for  (j=0; j<IN; j++)  
        {         
            sum += W[i][j] * input[j];                   //算隐层第i个神经元不包含阈值的输入   
        }                                     
        IN_HN[i] = sum + HU_HN[i];                       //算隐层第i个神经元的净输入   
        OUT_HN[i] = 1.0 / (1.0 + exp(-IN_HN[i]));        //算隐层第i个神经元的输出   
    }  
      
    sum = 0.0;  
     for  (i=0; i<ON; i++)                              //求输出层的输入输出   
    {  
         for  (j=0; j<HN; j++)  
        {  
            sum += V[i][j] * OUT_HN[j];                  //算输出层第i个神经元的输入(不含阈值)   
        }  
        IN_ON[i] = sum + HU_ON[i];                       //算输出层第i个神经元的净输入   
        OUT_ON[i] = 1.0 / (1.0 + exp(-IN_ON[i]));        //算输出层第i个神经元的输出   
    }  
}  
最后是main函数的内容,用来做一个异或运算的测试,内容如下:
sample1.txt内容如下:
0 0 0
0 1 1
1 0 1
1 1 0
表示异或运算的规则
#include "stdafx.h"   
#include "BpNet.h"   
  
int  main( int  argc,  char * argv[])  
{  
     /*BP分为两大阶段的函数:用于训练的函数Train(),用于工作的函数Work()*/   
    BpNet bp;  
  
     /*对亦或样本进行训练*/   
  
//  bp.Train("sample1.txt", "weight1.txt", "yuzhi1.txt");   
  
     /*对亦或样本训练结果进行工作测试*/   
  
    bp.Work( "weight1.txt" , "yuzhi1.txt" );  
  
     return  0;  
}  

最后的测试结果图如下:



猜你喜欢

转载自blog.csdn.net/a635661820/article/details/44733779