看了很多BP神经网络原理,也看到很多python的代码,我就想用C++来徒手撕一撕。
因为我习惯用C++写程序了,不妨就用C++啃下这个神经网络吧。
BP(back propagation)神经网络是最基础的神经网络,很多变种,例如RNN,LSTM,GRU,HNN等,都基于这个神经网络的训练过程。
BP包括三个神经元层,输入神经元,隐藏层神经元,输出神经元。上一层神经元的输出会被下一层的神经元“选择性地”以一定强度接受,并且进行下一步的传递,这里就有权重和偏置来影响。而输出会有激励函数来加以限制,将数据压缩在一定的范围。这里我选择的激励函数是最基础的sigmoid
sigmoid=1/(1+exp(-x))
exp()函数在cmath库中有,表示e^x
而sigmoid的导数dif-sigmoid
dif-sigmoid=sigmoid(x)*(1-sigmoid(x))
#include<bits/stdc++.h>
#define inum 10
#define hnum 20
#define onum 10
using namespace std;
double input[inum],expect[onum],error,n=0.5;
struct hidden
{
double in,out;
double w[inum],bia;
}hide[hnum];
struct outputlayer
{
double in,out;
double w[hnum],bia;
double tr;
}output[onum];
double f(double x)
{
return 1.0/(1.0+exp(-x));
}
double dif(double x)
{
double trans=f(x);
return trans*(1-trans);
}
不难看出我在开头这些乱七八糟的定义里面把东西都定全了
inum输入层神经元数,hnum隐藏层神经元数,onum输出层神经元数,通常是符合hnum=random(1~10)+根号下inum*onum(打不出根号蛋疼)
expect就是期望得到的结果,最后在反向传播时有用
n是学习率,error是误差,f是激励函数,dif就是它的导数了
在定义hide和output时用了结构体,有的人觉得不一定需要这样,也可以直接用数组,二维数组来代替。我是觉得这样比较直观吧,于是就这么写了。
必然是要有in输入,out输出,w权重,bia偏置的,in和out都要参与运算,所以不可少,尤其是in,这个要参与反向传播。
首先是要初始化权重和偏置的,对吧。
void init()
{
error=10;
srand(unsigned(time(NULL)));
for(int i=0;i<hnum;i++)
{
hide[i].bia=(rand()%10)/200.0;
for(int j=0;j<inum;j++)
hide[i].w[j]=(rand()%10)/200.0;
}
for(int i=0;i<onum;i++)
{
output[i].bia=(rand()%10)/200.0;
for(int j=0;j<hnum;j++)
output[i].w[j]=(rand()%10)/200.0;
}
}
先把error给整成了10,主要是下一步运算的时候进while不要出问题。然后采用了随机数生成,种子是时间,不过这个有缺陷,因为。。。。。。基本上下面生成的随机数都是一样的,因为电脑在一秒钟内能运算太多了。。。希望有大佬能帮帮我解决这个问题。(不过对运算影响不是特别大啦,不过这种权重和偏置设定也可以手动调,或者采用遗传算法,选择出最佳的初始设定)
主函数内容大概如此,灵活应变吧,这里只是稍微做个演示
int main()
{
init();
for(int i=0;i<inum;i++)cin>>input[i];
for(int i=0;i<onum;i++)cin>>expect[i];
while(error>=0.01)
{
mainwork();
cout<<error<<endl;
}
for(int i=0;i<onum;i++)cout<<output[i].out<<" ";
return 0;
}
最后我要看看训练结果如何,就让他输出output[i].out了。在实际训练中,训练集肯定是不止一组的,一定要记住,训练的时候把整个训练集过下来才能算一次训练,如果一组训练完再进行下一组训练,那么BP神经网络比较蠢,最后只能逼近最后一组数据。(想想为什么)
接下来就是主要运算过程啦!
首先前向传播
hide[i].in=sigma(hide[i].w[j]*input[j])+hide[i].bia
hide[i].out=sigmoid(hide[i].in)
output[i].in+=sigmoid(output[i].w[j]*hide[j].out)+output[i].bia
output[i].out=sigmoid(output[i].in)
for(int i=0;i<hnum;i++)
{
hide[i].in=0;
for(int j=0;j<inum;j++)
hide[i].in+=hide[i].w[j]*input[j];
hide[i].in+=hide[i].bia;
hide[i].out=f(hide[i].in);
}
for(int i=0;i<onum;i++)
{
output[i].in=0;
for(int j=0;j<hnum;j++)
output[i].in+=output[i].w[j]*hide[j].out;
output[i].in+=output[i].bia;
output[i].out=f(output[i].in);
}
然后算一下误差
error=0;
for(int i=0;i<onum;i++)
{
double trans=expect[i]-output[i].out;
error+=trans*trans;
}
error*=0.5;
最后开始反向传播,具体反向传播的求偏导过程不赘述。
tr用于储存对expect求output[i].bia的偏导(其实是对output[i].in的求偏导,因为bia求偏导时前面乘常数1,所以没有变化),这样在进行下一步求偏导的时候可以减少运算时间
for(int i=0;i<onum;i++)
output[i].tr=(expect[i]-output[i].out)*dif(output[i].in);
接下来是要先训练隐藏层的权重和偏置的,因为先训练输出层的权重会影响到训练隐藏层的权重(隐藏层权重偏导数内含有输出层的权重)
for(int i=0;i<hnum;i++)
{
double trans=0;
for(int j=0;j<onum;j++)
trans+=output[j].tr*output[j].w[i];
trans*=dif(hide[i].in);
hide[i].bia+=n*trans;
for(int j=0;j<inum;j++)
hide[i].w[j]+=n*trans*input[j];
}
最后攻克输出层训练,一次训练结束,但是error值可能并没有满足要求(我在while里面设定了这个条件)
for(int i=0;i<onum;i++)
{
output[i].bia+=n*output[i].tr;
for(int j=0;j<hnum;j++)
output[i].w[j]+=n*output[i].tr*hide[j].out;
}
对整个运算过程包装一下,放入mainwork()函数中~
void mainwork()
{
for(int i=0;i<hnum;i++)
{
hide[i].in=0;
for(int j=0;j<inum;j++)
hide[i].in+=hide[i].w[j]*input[j];
hide[i].in+=hide[i].bia;
hide[i].out=f(hide[i].in);
}
for(int i=0;i<onum;i++)
{
output[i].in=0;
for(int j=0;j<hnum;j++)
output[i].in+=output[i].w[j]*hide[j].out;
output[i].in+=output[i].bia;
output[i].out=f(output[i].in);
}
error=0;
for(int i=0;i<onum;i++)
{
double trans=expect[i]-output[i].out;
error+=trans*trans;
}
error*=0.5;
for(int i=0;i<onum;i++)
output[i].tr=(expect[i]-output[i].out)*dif(output[i].in);
for(int i=0;i<hnum;i++)
{
double trans=0;
for(int j=0;j<onum;j++)
trans+=output[j].tr*output[j].w[i];
trans*=dif(hide[i].in);
hide[i].bia+=n*trans;
for(int j=0;j<inum;j++)
hide[i].w[j]+=n*trans*input[j];
}
for(int i=0;i<onum;i++)
{
output[i].bia+=n*output[i].tr;
for(int j=0;j<hnum;j++)
output[i].w[j]+=n*output[i].tr*hide[j].out;
}
}
如果对反向传播不了解的话,可以参考下面的两个链接
还有一个是另外一个大佬的C++实现,里面有更好的解说过程