C++实现BP神经网络

引言

人工智能机器学习神经网络,这些词语很常见,但其逻辑关系我也是最近才了解清楚:

  • 人工智能:人工智能是计算机科学的一个分支;人工智能研究的一个主要目标是使机器能够胜任一些通常需要人类智能才能完成的复杂工作。百度百科词条-人工智能
  • 机器学习:上面说到使机器胜任人类的工作,那么机器学习就是人工智能的核心,是使计算机具有智能的根本途径。百度百科词条-机器学习
  • 神经网络:这里神经网络一般指人工神经网络,即模仿人脑神经元网络,是机器学习的重要工具。百度百科词条-人工神经网络

神经网络有很多种,本文的关键点在于BP神经网络。BP神经网络是一种监督学习,即对判断结果进行纠正的学习方法。纠错的手段是对误差进行前向传播,对权重和偏置进行修正,直到结果全部符合要求,视为训练完成。
C++实现BP神经网络_第1张图片
上图是最简单的神经元示意,对基本神经网络的权重和偏置进行误差修正,就是BP神经网络,新增参数有err(误差)和eta(步长)。
推荐一个对于激活函数的很好的解释:神经网络激活函数的作用是什么?

串行编程

例程选取了三种鸢尾花的数据,类别分别记为-1,0,1,对其进行模型训练。
下面是串行编程,为方便使用了printf进行输出:

#include
#include

//定义训练用的数据
double iris1[4] = { 5.1, 3.5, 1.4, 0.2 };
double iris2[4] = { 7,   3.2, 4.7, 1.4 };
double iris3[4] = { 6.3, 3.3, 6,   2.5 };

//定义数据的分类
double iris1_class = -1;
double iris2_class = 0;
double iris3_class = 1;

//权重,偏置初始值
//权重按理论应该是随机值,但是为了使结果落在0附近,初始化也在0附近,初始选择好往往可以较少很多循环次数
//这里可以多更改初值,观察最终训练得到的权重
double w[4] = { 0,0,0,0 };
double b = 0;

//求和函数
double sumfunc(double *iris, double *weight, double bias)
{
	double sum = 0;
	for (int i = 0; i < 4; i++) {
		sum += iris[i] * weight[i];
	}
	sum += bias;
	return sum;
}

//激活函数
int active(double sum)
{
	if (sum < -1)
		return -1;
	else if (sum > 1)
		return 1;
	else return 0;
}

//修正参数
double changepara(double iris_class, double output, double *iris, double *weight, double bias)
{
	double err; //计算的误差,用于对权值和偏置的修正
	double eta = 0.01; //步长,在0-1之间
	err = iris_class - output;
	for (int i = 0; i < 4; i++) {
		w[i] += err * eta*iris[i];
	}
	b += eta * err;
	//w是数组用指针可直接修改,b不可以,所以返回b的值
	return b;
}

//主程序
int main()
{
	double sum; //存放加权求和的值
	int output1 = 0, output2 = 0, output3 = 0; //把加权求和的值代入激活函数而得到的输出值
	int count = 0; //训练次数的计数变量
	int flag1 = 0, flag2 = 0, flag3 = 0; //置一表示符合要求
	while (1) {
		sum = sumfunc(iris1, w, b); //代入第一组iris进行计算
		output1 = active(sum);
		if (output1 == iris1_class) //判断输出是否达标,若达标则把标志置1,否则修正权值和偏置
		{
			flag1 = 1;
		}
		else {
			flag1 = 0; //下次循环修正参数后可能不满足1,所以要置零,直到全部符合
			b = changepara(iris1_class, output1, iris1, w, b); //修正参数
		}

		sum = sumfunc(iris2, w, b); //代入第二组iris进行计算
		output2 = active(sum);
		if (output2 == iris2_class)
		{
			flag2 = 1;
		}
		else {
			flag2 = 0;
			b = changepara(iris2_class, output2, iris2, w, b);
		}

		sum = sumfunc(iris3, w, b); //代入第三组iris进行计算
		output3 = active(sum);
		if (output3 == iris3_class)
		{
			flag3 = 1;
		}
		else {
			flag3 = 0;
			b = changepara(iris3_class, output3, iris3, w, b);
		}
		printf("第%d次训练的输出如下:\n", count += 1);//输出训练结果		
		printf("第一组iris属于%d类\n", output1);
		printf("第二组iris属于%d类\n", output2);
		printf("第三组iris属于%d类\n\n", output3);

		if (flag1 == 1 && flag2 == 1 && flag3 == 1)
		{
			break;
		}//如果所有数据都训练达标,则直接跳出循环
	}
	printf("\n\n模型训练完成!\n\n");
	printf("最终训练的权重值为:\n");
	for (int j = 0; j < 4; j++) {
		printf("%.4f\n", w[j]);
	}
	printf("\n最终训练的偏置值为:%.4f\n",b);
	return 0;
}

并行编程

观察整个程序,使用WINAPI创建线程来实现多线程,思考任务的分解:

  • 初始的权值是随机赋值的;
  • 迭代次数是不确定的,以收敛或达到理想输出为止;
  • 学习使用的3组数据没有确定的学习顺序,但权的修正存在关联关系,其权的改正实质上是串行的。因此,需要利用并行技术的同步方法(如事件、临界区等)予以解决。

本文使用临界区来进行不同线程的同步,观察任务可以发现能够进行多线程的部分只有不同组数据的训练,于是对每一组数据创建一个线程,线程调用函数没有发现冲突的问题,其他部分不变。
先说几个重点部分:

  1. 创建线程时进行参数传递,要传递其位置,如(void*)(&flag1)
thread[0] = CreateThread(NULL, 0, judgefunc1, (void*)(&flag1), 0, NULL);
  1. 接受时对参数转化,线程函数将其转化为指针,操作时也要按照指针操作
int *flag1 = (int*)para;
*flag1 = 1;

除此之外,按照通常的思路写下并行程序,将权重和偏置都改为100,步长为0.01,将训练次数增加,观察和串行的时间差别:

#include
#include
#include
#include

CRITICAL_SECTION cs;
HANDLE thread[3];
int flag1 = 0, flag2 = 0, flag3 = 0;
int output1 = 0, output2 = 0, output3 = 0;

//定义训练用的数据
double iris1[4] = { 5.1, 3.5, 1.4, 0.2 };
double iris2[4] = { 7,   3.2, 4.7, 1.4 };
double iris3[4] = { 6.3, 3.3, 6,   2.5 };

//定义数据的分类
double iris1_class = -1;
double iris2_class = 0;
double iris3_class = 1;

//权重,偏置初始值
double w[4] = { 100,100,100,100 };
double b = 100;

//求和函数
double sumfunc(double *iris, double *weight, double bias)
{
	double sum = 0;
	for (int i = 0; i < 4; i++) {
		sum += iris[i] * weight[i];
	}
	sum += bias;
	return sum;
}

//激活函数
int active(double sum)
{
	if (sum < -1)
		return -1;
	else if (sum > 1)
		return 1;
	else return 0;
}

//修正参数
double changepara(double iris_class, double output, double *iris, double *weight, double bias)
{
	double err; //计算的误差,用于对权值和偏置的修正
	double eta = 0.01; //步长,在0-1之间
	err = iris_class - output;
	for (int i = 0; i < 4; i++) {
		w[i] += err * eta*iris[i];
	}
	b += eta * err;
	//w是数组用指针可直接修改,b不可以,所以返回b的值
	return b;
}

//以下是3个线程,对三组数据进行训练
DWORD WINAPI judgefunc1(LPVOID para) {
	EnterCriticalSection(&cs);
	//将接受参数转换成int类型的指针并赋值给*flag1
	int *flag1 = (int*)para;
	double sum1 = 0;
	int output1;
	sum1 = sumfunc(iris1, w, b);
	output1 = active(sum1);
	if (output1 == iris1_class) //判断输出是否达标,若达标则把标志置1,否则修正权值和偏置
	{
		//注意对flag进行更改时,此时flag为指针,取其值进行更改,下同
		*flag1 = 1;
	}
	else {
		*flag1 = 0; //下次循环修正参数后可能不满足1,所以要置零,直到全部符合
		b = changepara(iris1_class, output1, iris1, w, b); //修正参数
	}
	//输出一次训练的结果
	printf("第一组iris属于%d类\n", output1);
	LeaveCriticalSection(&cs);
	return 0;
}

DWORD WINAPI judgefunc2(LPVOID para) {
	EnterCriticalSection(&cs);
	int *flag2 = (int*)para;
	double sum2 = 0;
	int output2;
	sum2 = sumfunc(iris2, w, b);
	output2 = active(sum2);
	if (output2 == iris2_class)
	{
		*flag2 = 1;
	}
	else {
		*flag2 = 0;
		b = changepara(iris2_class, output2, iris2, w, b);
	}
	printf("第二组iris属于%d类\n", output2);
	LeaveCriticalSection(&cs);
	return 0;
}

DWORD WINAPI judgefunc3(LPVOID para) {
	EnterCriticalSection(&cs);
	int *flag3 = (int*)para;
	double sum3 = 0;
	int output3;
	sum3 = sumfunc(iris3, w, b);
	output3 = active(sum3);
	if (output3 == iris3_class)
	{
		*flag3 = 1;
	}
	else {
		*flag3 = 0;
		b = changepara(iris3_class, output3, iris3, w, b);
	}
	printf("第三组iris属于%d类\n", output3);
	LeaveCriticalSection(&cs);
	return 0;
}

//主程序
int main()
{	
	int count = 0; //训练次数的计数变量
	while (1) {
		printf("\n第%d次训练的输出如下:\n", count += 1);//输出训练结果
		InitializeCriticalSection(&cs); //初始化临界区
		thread[0] = CreateThread(NULL, 0, judgefunc1, (void*)(&flag1), 0, NULL);
		thread[1] = CreateThread(NULL, 0, judgefunc2, (void*)(&flag2), 0, NULL);
		thread[2] = CreateThread(NULL, 0, judgefunc3, (void*)(&flag3), 0, NULL);

		//以下用于观察每次训练后权重和偏置的值
		//Sleep(100);
		//printf("\n本次训练的权重值为:\n");
		//for (int j = 0; j < 4; j++) {
		//	printf("%.4f\n", w[j]);
		//}
		//printf("本次训练的偏置值为:%.4f\n", b);

		WaitForMultipleObjects(3, thread, TRUE, INFINITE);
		DeleteCriticalSection(&cs); //删除临界区

		if (flag1 == 1 && flag2 == 1 && flag3 == 1)
		{
			break;
		}//如果所有数据都训练达标,则直接跳出循环
	}
	printf("\n\n模型训练完成!\n\n");
	printf("最终训练的权重值为:\n");
	for (int j = 0; j < 4; j++) {
		printf("%.4f\n", w[j]);
	}
	printf("\n最终训练的偏置值为:%.4f\n",b);
	return 0;
}

最终输出的结果(部分截图):

991次训练的输出如下:
第一组iris属于-1类
第二组iris属于1类
第三组iris属于1类

第992次训练的输出如下:
第一组iris属于-1类
第二组iris属于0类
第三组iris属于1类


模型训练完成!

最终训练的权重值为:
-52.6530
11.1330
30.5610
82.8600

最终训练的偏置值为:73.7600

你可能感兴趣的:(多核架构与系统设计)