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的实现在BpNet.cpp,内容如下:
// BpNet.cpp: implementation of the BpNet class.
//
//////////////////////////////////////////////////////////////////////

#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;
}

最后的测试结果图如下:

技术分享

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。