人工智能,机器学习,神经网络,这些词语很常见,但其逻辑关系我也是最近才了解清楚:
神经网络有很多种,本文的关键点在于BP神经网络。BP神经网络是一种监督学习,即对判断结果进行纠正的学习方法。纠错的手段是对误差进行前向传播,对权重和偏置进行修正,直到结果全部符合要求,视为训练完成。
上图是最简单的神经元示意,对基本神经网络的权重和偏置进行误差修正,就是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创建线程来实现多线程,思考任务的分解:
本文使用临界区来进行不同线程的同步,观察任务可以发现能够进行多线程的部分只有不同组数据的训练,于是对每一组数据创建一个线程,线程调用函数没有发现冲突的问题,其他部分不变。
先说几个重点部分:
(void*)(&flag1)
thread[0] = CreateThread(NULL, 0, judgefunc1, (void*)(&flag1), 0, NULL);
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