BP神经网络的C++实现

    之前说了组里的任务是手写BP神经网络,上一篇文总结了一下BP神经网络的概念,老实说,总结概念前的一个C++实现版本在总结概念之后重新审视时觉得实在是惨不忍睹,于是今晚回炉重写了。这篇文就来挂我的BP神经网络C++实现。

    老师提出的具体问题是平面对点集的二分类。ACM战过这么多场,写板子早成了习惯,就把通用的BP认真封装了一下:
    头文件里的Data结构体是输入数据的数据结构,可自定义,这里用的是点分类问题的模型,BP.h:

#ifndef _BP_H_
#define _BP_H_

#include <vector>
#include <string>
using namespace std;

/* 数据样本类 */
struct Data
{
	/* 输入参数,包括: */
	/* 三维直角坐标系点的坐标(x, y, z) */
	/* 三维直角坐标系平面a*x + b*y + c*z + d = 0的4个系数 */
	double x[7];
	/* 期望输出即监督值 */
	/* 拟合 */
	// double d[1];
	/* 分类 */
	double d[2];
	/* 数据构造函数 */
	Data();
};

class BP
{
private:
	/* ========== 常数 ========== */
	/* 输入层节点数 */
	int I;
	/* 隐含层神经元数 */
	int H;
	/* 输出层神经元数 */
	int O;
	/* 权重学习速率 */
	double LR;
	/* 偏置学习速率 */
	double LR2;
	/* 学习速率衰减率(每次衰减与当前LR相乘) */
	double LRDecay;
	/* 误差函数收敛阈值 */
	double C;
	/* ========== 容器 ========== */
	/* 训练用数据样本集 */
	vector<Data> trainDS;
	/* 测使用数据集 */
	vector<Data> testDS;
	/* 输入层与隐含层间的全连接权重:w[I(包含一个偏置值)][H] */
	double **w;
	/* w修正值 */
	double *dw;
	/* 隐含层阈值 */
	double *th;
	/* 隐含层输入积累即净激活,也存放之后的激活输出值:u[H] */
	double *u;
	/* 隐含层与输出层间的全连接权重:v[H][O] */
	double **v;
	/* v修正值 */
	double *dv;
	/* 输出层阈值 */
	double *to;
	/* 输出层输入积累即净激活,也存放之后的激活输出值:y[O] */
	double *y;
	/* 拟合标识 */
	bool regression;
	/* ========== 方法 ========== */
	/* 填充训练用数据样本集 */
	void fillTrainDS(int sampleCnt);
	/* 清空训练用数据样本集 */
	void clearTrainDS();
	/* 填充测试使用数据集 */
	void fillTestDS(int sampleCnt);
	/* 清空测试使用数据集 */
	void clearTestDS();
	/* 预测 */
	void forward(int index, bool test = false);
	/* 调整 */
	void backward(int index);
public:
	/* ========== 接口 ========== */
	/*
	 * 类构造函数,初始化BP神经网络结构和训练参数
	 * int _I:		输入参数数目。
	 * int _O:		输出值数目。
	 * int A:		隐含层调整因子(1~10)。
	 * double _LR:		权重学习速率(0.01~0.8)。
	 * double _LR2:	偏置学习速率(0.01~0.8)。
	 * double _LRDecay:	学习速率衰减率(每次衰减与当前LR相乘)。
	 * double _C:		误差函数收敛阈值。
	 * bool regression:	拟合标识。
	 */
	BP(int _I, int _O, int A = 1, double _LR = 0.01, double _LR2 = 0.035, double _LRDecay = 1.0, double _C = 0.01, bool regression = false);
	/* 类析构函数,释放容器分配的堆空间 */
	~BP();
	/* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
	double train(int sampleCnt = 1000, int trainCnt = 100);
	/*
	 * 使用指定数目的样本循环训练。
	 * 误差函数值进入可接受范围判定收敛并停止训练;
	 * 到达最大训练次数时停止训练。
	 * 返回是否收敛。
	 */
	bool trainTillConvergent(int sampleCnt = 1000, int maxEpoch = 1000);
	/* 生成指定数目组数据测试当前神经网络 */
	void testNetwork(int testCnt = 1000);
	/* 保存当前神经网络,即两个权重数组 */
	void saveNetwork(string wPath = "wNetwork", string vPath = "vNetwork");
	/* 载入两个权重数组,还原神经网络 */
	void loadNetwork(string wPath = "wNetwork", string vPath = "vNetwork");
	/* 使用指定数据预测输出值并返回输出值 */
	vector<double> runNetwork(vector<double> x);
};

#endif

    类实现源码里的Data结构体的构造函数是输入数据的处理和问题模型的建立,可自定义,这里用的是点分类问题的数据处理,BP.cpp:

#include "BP.h"
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <fstream>
using namespace std;

/* 数据构造函数 */
Data::Data()
{
	/* 归一参数 */
	for(int i = 1 ; i < 8 ; i++)
	{
		x[i] = rand()/(double)RAND_MAX;
	}
	/* 实际参数 */
	double _x = x[1]*20 - 10;
	double y = x[2]*20 - 10;
	double z = x[3]*20 - 10;
	double a = x[4]*20 - 10;
	double b = x[5]*20 - 10;
	double c = x[6]*20 - 10;
	double _d = x[7]*20 - 10;
	/* 拟合 */
	// d[0] = z - (a*_x + b*y + _d)/(-1*c);
	/* 分类 */
	d[0] = z > (a*_x + b*y + _d)/(-1*c) ? 1 : 0;
	d[1] = d[0] ? 0 : 1;
}

/* 填充训练用数据样本集 */
void BP::fillTrainDS(int sampleCnt)
{
	while(sampleCnt--)
	{
		trainDS.push_back(Data());
	}
}

/* 清空训练用数据样本集 */
void BP::clearTrainDS()
{
	vector<Data> v;
	trainDS.swap(v);
}

/* 填充测试使用数据集 */
void BP::fillTestDS(int sampleCnt)
{
	while(sampleCnt--)
	{
		testDS.push_back(Data());
	}
}

/* 清空测试使用数据集 */
void BP::clearTestDS()
{
	vector<Data> v;
	testDS.swap(v);
}

/* 预测 */
void BP::forward(int index, bool test)
{
	/* 隐含层 */
	for(int i = 0 ; i < H ; i++)
	{
		u[i] = 0;
		for(int j = 0 ; j < I ; j++)
		{
			double x = test ? testDS[index].x[j] : trainDS[index].x[j];
			/* 积累输入 */
			u[i] += w[j][i]*x;
		}
		u[i] += th[i];
		/* Sigmoid函数作为激活函数 */
		u[i] = 1 / (1 + exp(-1*u[i]));
	}
	/* 输出层 */
	for(int i = 0 ; i < O ; i++)
	{
		y[i] = 0;
		for(int j = 0 ; j < H ; j++)
		{
			/* 积累输入 */
			y[i] += v[j][i]*u[j];
		}
		y[i] += to[i];
		/* 分类:Sigmoid函数作为激活函数 */
		if (!regression)
		{
			y[i] = 1 / (1 + exp(-1*y[i]));
		}
	}
}

/* 调整 */
void BP::backward(int index)
{
	/* 计算隐含层与输出层间权重调整值 */
	for(int i = 0 ; i < O ; i++)
	{
		/* 拟合:计算输出层学习误差 */
		if (regression)
		{
			dv[i] = y[i] - trainDS[index].d[i];
		}
		/* 分类:计算输出层学习误差 */
		else
		{
			dv[i] = (y[i] - trainDS[index].d[i])*y[i]*(1 - y[i]);
		}
	}
	/* 计算输入层与隐含层间权重调整值 */
	double t;
	for(int i = 0 ; i < H ; i++)
	{
		t = 0;
		for(int j = 0 ; j < O ; j++)
		{
			t += dv[j]*v[i][j];
		}
		dw[i] = t*u[i]*(1 - u[i]);
	}
	/* 调整隐含层与输出层间权重 */
	for(int i = 0 ; i < H ; i++)
	{
		for(int j = 0 ; j < O ; j++)
		{
			v[i][j] -= LR*dv[j]*u[i];
		}
	}
	/* 调整输出层偏置 */
	for(int i = 0 ; i < O ; i++)
	{
		to[i] -= LR2*dv[i];
	}
	/* 调整输入层与隐含层间权重 */
	for(int i = 0 ; i < I ; i++)
	{
		for(int j = 0 ; j < H ; j++)
		{
			w[i][j] -= LR*dw[j]*trainDS[index].x[i];
		}
	}
	/* 调整隐含层偏置 */
	for(int i = 0 ; i < H ; i++)
	{
		th[i] -= LR2*dw[i];
	}
}

/*
 * 类构造函数,初始化BP神经网络结构和训练参数
 * int _I:		输入参数数目,包括偏置值对应的参数-1。
 * int _O:		输出值数目。
 * int A:		隐含层调整因子(1~10)。
 * double _LR:		权重学习速率(0.01~0.8)。
 * double _LR2:	偏置学习速率(0.01~0.8)。
 * double _LRDecay:	学习速率衰减率(每次衰减与当前LR相乘)。
 * double _C:		误差函数收敛阈值。
 * bool regression:	拟合标识。
 */
BP::BP(int _I, int _O, int A, double _LR, double _LR2, double _LRDecay, double _C, bool _regression)
{
	/* ========== 初始化常数 ========== */
	I = _I;
	H = ceil(sqrt(_I + _O)) + A;
	O = _O;
	LR = _LR;
	LR2 = _LR2;
	LRDecay = _LRDecay;
	C = _C;
	regression = _regression;
	/* ========== 初始化容器 ========== */
	srand((unsigned)time(NULL));
	/* 初始化w */
	w = new double*[I];
	for(int i = 0 ; i < I ; i++)
	{
		w[i] = new double[H];
		for(int j = 0 ; j < H ; j++)
		{
			w[i][j] = rand()/(double)RAND_MAX;
		}
	}
	/* 初始化dw */
	dw = new double[H];
	/* 初始化th */
	th = new double[H];
	for(int i = 0 ; i < H ; i++)
	{
		th[i] = rand()/(double)RAND_MAX;
	}
	/* 初始化u */
	u = new double[H];
	/* 初始化v */
	v = new double*[H];
	for(int i = 0 ; i < H ; i++)
	{
		v[i] = new double[O];
		for(int j = 0 ; j < O ; j++)
		{
			v[i][j] = rand()/(double)RAND_MAX;
		}
	}
	/* 初始化dv */
	dv = new double[O];
	/* 初始化to */
	to = new double[O];
	for(int i = 0 ; i < O ; i++)
	{
		to[i] = rand()/(double)RAND_MAX;
	}
	/* 初始化y */
	y = new double[O];
}

/* 类析构函数,释放容器分配的堆空间 */
BP::~BP()
{
	/* 释放w */
	for(int i = 0 ; i < I ; i++)
	{
		delete []w[i];
	}
	delete []w;
	/* 释放dw */
	delete []dw;
	/* 释放th */
	delete []th;
	/* 释放u */
	delete []u;
	/* 释放v */
	for(int i = 0 ; i < H ; i++)
	{
		delete []v[i];
	}
	delete []v;
	/* 释放dv */
	delete []dv;
	/* 释放to */
	delete []to;
	/* 释放y */
	delete []y;
}

/* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
double BP::train(int sampleCnt, int trainCnt)
{
	fillTrainDS(sampleCnt);
	double e;
	while(trainCnt--)
	{
		e = 0;
		for(int i = 0 ; i < trainDS.size() ; i++)
		{
			/* 预测 */
			forward(i);
			/* 误差积累 */
			for(int j = 0 ; j < O ; j++)
			{
				e += pow(y[j] - trainDS[i].d[j], 2.0);
			}
			/* 调整 */
			backward(i);
		}
		e /= 2*sampleCnt;
		/* 学习速率衰减 */
		if (LR > 0.01)
		{
			LR *= LRDecay;
		}
	}
	clearTrainDS();
	return e;
}

/*
 * 使用指定数目的样本循环训练。
 * 误差函数值进入可接受范围判定收敛并停止训练;
 * 到达最大训练次数时停止训练。
 * 返回是否收敛。
 */
bool BP::trainTillConvergent(int sampleCnt, int maxEpoch)
{
	fillTrainDS(sampleCnt);
	double e;
	for(;;)
	{
		e = 0;
		for(int i = 0 ; i < trainDS.size() ; i++, maxEpoch--)
		{
			if (!maxEpoch)
			{
				clearTrainDS();
				return false;
			}
			/* 预测 */
			forward(i);
			/* 误差积累 */
			for(int j = 0 ; j < O ; j++)
			{
				e += pow(y[j] - trainDS[i].d[j], 2.0);
			}
			/* 调整 */
			backward(i);
		}
		/* 判定收敛,中止训练 */
		if (e/(2*sampleCnt) < C)
		{
			clearTrainDS();
			return true;
		}
		/* 学习速率衰减 */
		if (LR > 0.01)
		{
			LR *= LRDecay;
		}
	}
	clearTrainDS();
	return false;
}

/* 生成指定数目组数据测试当前神经网络 */
void BP::testNetwork(int testCnt)
{
	fillTestDS(testCnt);
	double e = 0;
	for(int i = 0 ; i < testCnt ; i++)
	{
		forward(i, true);
		for(int j = 0 ; j < O ; j++)
		{
			e += pow(y[j] - testDS[i].d[j], 2.0);
		}
	}
	cout << testCnt << "组数据测试预测值相对期望值方差为:" << e/(2*testCnt) << endl;
	clearTestDS();
}

/* 保存当前神经网络,即两个权重数组 */
void BP::saveNetwork(string wPath, string vPath)
{
	ofstream fout_w(wPath);
	if(fout_w == NULL)
	{
		cout << "打开文件失败" << endl;
		return;
	}
	for(int i = 0 ; i < I ; i++)
	{
		for(int j = 0 ; j < H ; j++)
		{
			fout_w << w[i][j] << '\t';
		}
		fout_w << '\n';
	}
	fout_w.close();
	ofstream fout_v(vPath);
	if(fout_v == NULL)
	{
		cout << "打开文件失败" << endl;
		return;
	}
	for(int i = 0 ; i < H ; i++)
	{
		for(int j = 0 ; j < O ; j++)
		{
			fout_v << v[i][j] << '\t';
		}
		fout_v << '\n';
	}
	fout_v.close();
}

/* 载入两个权重数组,还原神经网络 */
void BP::loadNetwork(string wPath, string vPath)
{
	ifstream fin_w(wPath);
	if(fin_w == NULL)
	{
		cout << "打开文件失败" << endl;
		return;
	}
	for(int i = 0 ; i < I ; i++)
	{
		for(int j = 0 ; j < H ; j++)
		{
			fin_w >> w[i][j];
		}
	}
	fin_w.close();
	ifstream fin_v(vPath);
	if(fin_v == NULL)
	{
		cout << "打开文件失败" << endl;
		return;
	}
	for(int i = 0 ; i < H ; i++)
	{
		for(int j = 0 ; j < O ; j++)
		{
			fin_v >> v[i][j];
		}
	}
	fin_v.close();
}

/* 使用指定数据预测输出值并返回输出值 */
vector<double> BP::runNetwork(vector<double> x)
{
	/* 隐含层 */
	for(int i = 0 ; i < H ; i++)
	{
		u[i] = 0;
		for(int j = 0 ; j < x.size() ; j++)
		{
			/* 积累输入 */
			u[i] += w[j][i]*x[j];
		}
		u[i] += th[i];
		/* Sigmoid函数作为激活函数 */
		u[i] = 1 / (1 + exp(-1*u[i]));
	}
	vector<double> o;
	double y;
	/* 输出层 */
	for(int i = 0 ; i < O ; i++)
	{
		y = 0;
		for(int j = 0 ; j < H ; j++)
		{
			/* 积累输入 */
			y += v[j][i]*u[j];
		}
		y += to[i];
		/* 分类:Sigmoid函数作为激活函数 */
		if (!regression)
		{
			y = 1 / (1 + exp(-1*y));
		}
		o.push_back(y);
	}
	return o;
}

    类的测试方法里无非是根据实际问题初始化类,做一系列的训练,调整参数,慢慢提高预测精度,test.cpp:

#include "BP.h"
#include <iostream>

using namespace std;

int main()
{
	/*
	 * 类构造函数,初始化BP神经网络结构和训练参数
	 * int _I:		输入参数数目,包括偏置值对应的参数-1。
	 * int _O:		输出值数目。
	 * int A:		隐含层调整因子(1~10)。
	 * double _LR:		学习速率(0.01~0.8)。
	 * double _LR2:	偏置学习速率(0.01~0.8)。
	 * double _LRDecay:	学习速率衰减率(每次衰减与当前LR相乘)。
	 * double _C:		误差函数收敛阈值。
	 * bool regression:	拟合标识。
	 */
	BP o(7, 2, 8, 0.2, 0.02, 0.99, 3.2e-2, false);

	// o.loadNetwork();

	/* 使用指定数目的样本训练指定数目次循环,返回最后的误差函数值 */
	for(int i = 0 ; i < 20 ; i++)
	{
		cout << "误差函数值:" << o.train(1000, 2e3) << endl;
	}

	/*
	 * 使用指定数目的样本循环训练。
	 * 误差函数值进入可接受范围判定收敛并停止训练;
	 * 到达最大训练次数时停止训练。
	 * 返回是否收敛。
	 */
	// bool success = o.trainTillConvergent(1000, 1e5*1000);
	// cout << "收敛:" << (success ? "success" : "fail") << endl;

	o.testNetwork(1000);

	o.saveNetwork();

	vector<double> X;
	double x = 0.1;
	double y = 0.1;
	double z = 0.1;
	double a = 1;
	double b = 1;
	double c = 1;
	double d = -1;
	X.push_back((x + 10)/20.0);
	X.push_back((y + 10)/20.0);
	X.push_back((z + 10)/20.0);
	X.push_back((a + 10)/20.0);
	X.push_back((b + 10)/20.0);
	X.push_back((c + 10)/20.0);
	X.push_back((d + 10)/20.0);
	vector<double> Y = o.runNetwork(X);
	/* 拟合 */
	// cout << "期望值:" << z - (a*x + b*y + d)/(-c) << endl;
	// cout << "输出值:" << Y[0] << endl;
	/* 分类 */
	cout << "期望值:" << (z > (a*x + b*y + d)/(-c) ? "1\t0" : "0\t1") << endl;
	cout << "输出值:" << Y[0] << '\t' << Y[1] << endl;

	return 0;
}

    点分类问题我先尝试了函数拟合方式,训练情况很糟糕,动辄上十万的方差。后来改用分类方式,降低训练要求,方差立马降下来了。下午例会就要做报告了,我没有训练到充分收敛,但是离平面比较远的点判断正确率已经很高了。下面提供平面分类点集问题当前训练进度下,输入层与隐含层间权重数组、隐含层与输出层间权重数组的保存文件。

    输入层与隐含层间权重数组wNetwork:

0.15513	0.56829	0.461073	0.27552	0.44408	0.144657	0.95798	0.335522	0.325092	0.254423	0.79217
-1.06417	-0.0241895	3.99915	0.228275	-0.483658	6.68402	-1.61574	0.753689	4.14272	-4.58112	-3.09481
3.70442	-3.7423	0.0852013	-0.564758	-4.99619	1.29436	-4.19077	-1.75579	-0.568722	-2.57748	1.20149
-0.311858	2.12485	0.494471	-3.32854	-0.393046	1.12112	-0.585242	-3.45739	-0.687711	-1.37346	-0.860532
1.29214	0.74987	4.41025	-1.37666	0.268284	6.72309	2.0978	-1.8259	4.20105	-6.15165	5.04569
-3.76331	-3.22432	-0.450262	1.77063	-5.89358	3.40051	3.64122	1.30879	-0.98291	-0.931413	-1.72719
9.53636	9.64562	9.82743	13.4779	-12.3668	12.5147	8.37658	6.32437	-10.263	-13.8476	-5.31933

    隐含层与输出层间权重数组vNetwork:

31.4711	-31.5094
27.536	-27.5713
-25.7692	25.7999
-27.4329	27.467
-17.6709	17.6933
-18.464	17.5204
25.8167	-25.8471
-23.2151	23.2434
27.3533	-27.387
26.9126	-26.9519
-24.7905	24.8209

    这个类我还做过a+b问题的函数拟合,收敛非常顺利,精度很高。
    后来,例会上提任务的博士说他明明是要我们寻找分割点集的平面,我把问题泛化成寻找点与平面位置关系规则的问题了,能用泛化能力不太强的BP整的差不多收敛也是不容易。我表示生无可恋……

    读者注意一下,上一篇BP网络的理论介绍中,隐层与输入层之间的权重调整值的推导有一些问题,少乘了一个wij。推导已经改过来了,但是比较忙没空改代码,有需要的同学请自己参照上一篇博文修改一下代码。OTZ


BP神经网络的C++实现
https://blog.bipedalbit.net/2015/10/11/BP神经网络的C-实现/
作者
Bipedal Bit
发布于
2015年10月11日
许可协议