在造轮子之前,一定要广泛查阅资料,自己推演前向传播和反向传播的所有过程。 这个过程需要的知识点是偏导数和链式法则,高中生其实都可以理解(将偏导数理解为求导多元函数中的一个变量的导数,只需将所有其他量视为常数即可) ),但是有点难,直接导致我高三的时候对它一知半解。
首先,入门级实现应该从最简单的BP开始。 公式等内容不再一一重复。 网上资料很多,个人草稿推演的内容早已失传。 确认理解后,就可以开始用C++编写BP轮了。
首先是各种激活函数。 一开始你可能只需要知道激活函数,但随着学习的进展,了解其他激活函数是有必要的。
就激励函数而言,需要写的是函数本身及其导函数。 tanh、elu等激发函数需要使用exp()函数,该函数需要cmath头文件。 您可以选择您喜欢的 IDE,以更方便的为准。 高中的时候就参加过NOIP,用过Dev,但是直到现在才转用它(这个只能算是一个工作台)。
正值
其实tanh在cmath库里有写好的函数,可以直接调用。
ReLU
relu 使用三元运算符非常舒服。 elu和leaky relu也可以这样玩。 不过我更喜欢leaky relu,因为relu很容易出现神经元死亡(无论神经元接收到什么样的数据,它的和都是负数,那么这个神经元就没有输出,无法在逆向过程中使用。更新权重,取决于根据您自己的推导)。
以上是激活函数的一些示例。 下面是编写神经元的示例。
写神经元的时候,可以简单的用二维数组来写,直接进行矩阵运算即可。 不过当时我没有接触过线性代数,所以后期我使用了一种非常直观但效率低下的方法。
一般使用
首先定义输入神经元的数量、隐藏层神经元的数量和输出神经元的数量,分别对应于 。 如果你觉得这种写法有点长,你可以将其缩短为INUM、HNUM、ONUM等,你觉得舒服就写吧。
然后它们分别用于定义隐藏层神经元和输出层神经元。 显然,C++中的这个类感觉就像是拍蚊子的大炮。 当然,还是那句话,你怎么舒服就怎么写。 如果你觉得我写的有点重复,可以借鉴一下。
灵活编写并可重用
那么 bp 必须具有的一些变量/常量是:
const常量,大小可以自己定
input[INUM]用于存储输入数据(单批次),[ONUM]用于存储期望数据(单批次)
error,用于记录单个批次的误差并对所有批次的误差进行求和。 记得将其初始化为一个较大的值,这是进入训练循环的必要条件。
[HNUM]、[ONUM]是结构体数组,后面正向和反向写入过程中都需要用到。
以上所有变量都是全局变量。
最基本的需求功能
()用于检查神经网络数据是否存在。 如果不存在,则执行 INIT() 并将输出数据保存为文件。 INIT()是初始化函数。
INIT函数,第一句先确定随机数种子
INIT() 函数必须首先开始生成随机数。 srand() plus (time(NULL)) 很好用,需要头文件 ctime.h 在下面的语句中,初始化每个神经元的sum和bia。 具体内容根据需要而定。 您可以灵活地编辑它。 我这里只给出一个大的框架。
话不多说,显然是用于数据输出和读取的,用于保存几个epoch后的数据文件,避免下次打开时重复训练。
使用and来读取和输出,需要头文件。 上图中,双引号内填写文件名(文件在本文件夹中)或绝对地址(文件在其他地方)。
()函数用于读取训练集,首先=0,然后循环训练集中的每个batch,读取输入sum,然后调用Calc()函数进入前向传播阶段,然后调用()进入这个误差计算阶段,在这个计算中给误差赋值一个值。 然后执行()反向传播,后面+=error。
预览
Calc() 中的前向传播基本上是一个循环。 我不需要多说了吧?
()也是如此,()也是如此,所以写的东西中,绝大多数语句都是循环语句。
*0.5比/2快得多,特别是当它需要很多步时
把整个过程写下来。 。 。 这是我个人的偏好。 如果你看不懂或者觉得效率低下,可以自己写。 不管怎样,能实现功能才是关键!
(有一个小trick,上图中没有体现出来,一般bia的增量为2**diff,个人测试效果还不错)
基本上在main里写一些调用内容
那么在C++中,如果数据中出现Inf,那么下面很有可能会出现NaN,然后循环就会被动停止,向你输出包含一堆NaN的垃圾数据。 为了避免这种情况,C++实际上有一个可以检测Inf和NaN的宏。
isnan()和isinf()是cmath/math.h库中的宏,可以直接调用来确定
至此,我就讲解完了简单BP的编写技巧。 如果要写的深入的话,框架其实也差不多。 我以后可能更新的内容基本上都是基于这个框架体系的。 我希望它能有所帮助。 尽管你可能无法接受我的没有矩阵运算的写法,但这就是一个用C++从头开始造轮子的例子。 我希望它能鼓励你。
粘贴下面的代码。 当然你不能直接复制并使用它。 你必须自己修改它。
#include
#include
#include
#define INUM 2
#define HNUM 5
#define ONUM 2
using namespace std;
template <const int NUM>
struct neuron
{
double w[NUM],bia,diff;
double in,out;
};
neuron<HNUM> hide[HNUM];
neuron<ONUM> output[ONUM];
const double learningrate=0.1;
double input[INUM];
double expect[ONUM];
double sigma_error=1e8;
double error=1e8;
double sigmoid(double x)
{
return 1.0/(1.0+exp(-x));
}
double diffsigmoid(double x)
{
x=1.0/(1.0+exp(-x));
return x*(1-x);
}
double tanh(double x)
{
return (exp(x)-exp(-x))/(exp(x)+exp(-x));
}
double difftanh(double x)
{
x=tanh(x);
return 1-x*x;
}
double relu(double x)
{
return x>0? x:0;
}
double diffrelu(double x)
{
return x>0? 1:0;
}
void TxtCheck();
void INIT();
void Datain();
void Dataout();
void Mainwork();
void Calc();
void ErrorCalc();
void Training();
int main()
{
int epoch=0;
TxtCheck();
while(sigma_error>0.001)
{
epoch++;
Mainwork();
if(epoch%(一个数)==0)
Dataout();
//也可以写其他操作
}
Dataout();
return 0;
}
void INIT()
{
srand(unsigned(time(NULL)));
/*statement*/
return;
}
void Datain()
{
ifstream fin(" ");
fin>>...
fin.close();
}
void Dataout()
{
ofstream fout(" ");
fout<<...
fout.close();
}
void Mainwork()
{
ifstream fin("数据集");
sigma_error=0;
for(int b=0;b<batch_size;b++)
{
/*处理batch数据,读入input和expect*/
Calc();
ErrorCalc();
Training();
sigma_error+=error;
}
fin.close();
return;
}
void Calc()
{
for(int i=0;i<HNUM;i++)
{
hide[i].in=0;
hide[i].in+=hide[i].bia;
for(int j=0;j<INUM;j++)
hide[i].in+=input[j]*hide[i].w[j];
hide[i].out=sigmoid(hide[i].in);
}
/*
other statements
*/
}
void ErrorCalc()
{
double trans;
error=0;
for(int i=0;i<ONUM;i++)
{
trans=output[i].out-expect[i];
error+=trans*trans;
}
error*=0.5;
}
void Training()
{
for(int i=0;i<ONUM;i++)
output[i].diff=(expect[i]-output[i].out)*diffsigmoid(output[i].in);
//负号直接舍弃,因为整个传递过程这里的负号不带来影响
//而且在最后更新数据的时候也不需要再*(-1)
for(int i=0;i<HNUM;i++)
{
hide[i].diff=0;
for(int j=0;j<ONUM;j++)
hide[i].diff+=output[j].diff*output[j].w[i];
hide[i].diff*=diffsigmoid(hide[i].in);
}
for(int i=0;i<ONUM;i++)
{
output[i].bia+=learningrate*output[i].diff;
for(int j=0;j<HNUM;j++)
output[i].w[j]+=learningrate*output[i].diff*hide[j].out;
}
for(int i=0;i<HNUM;i++)
{
hide[i].bia+=learningrate*hide[i].diff;
for(int j=0;j<INUM;j++)
hide[i].w[j]+=learningrate*hide[i].diff*input[j];
}
return;
}
更新于 2019/3/14 21:59
最近进入深度学习,自动编码器是必不可少的,所以我将这部分添加到了LSTM模型中。 由于最初的架构,循环较多,代码量较大。 不过,您可以复制之前的代码,然后稍微修改一下,然后粘贴下来。 有时间的话我会分享我的RNN和LSTM的东西。
更新于2019年3月15日
函数功能的大致结构如前面所写,现在我们来说说其他神经元单元的设计和使用。 我是做NLP自然语言处理的。 自然语言处理必然离不开RNN、LSTM、GRU等基本单元。 按照上面的思路,写出RNN和LSTM应该不难,但是变成了如下:
#define MAXTIME 100
template <const int InputNum,const int HideNum,const int Maxtime>
struct rnn_neuron
{
double wi[InputNum],wh[HideNum];
double bia,diff[Maxtime];
double in[Maxtime],out[Maxtime];
};
template <const int InputNum,const int Maxtime>
struct nor_neuron
{
double w[InputNum],bia,diff[Maxtime];
double in[Maxtime],out[Maxtime];
};
const double learningrate=0.1;
rnn_neuron<INUM,HNUM,MAXTIME> hide[HNUM];
nor_neuron<HNUM,MAXTIME> output[ONUM];
double input[INUM][MAXTIME];
double expect[ONUM][MAXTIME];
double sigma_error=1e8;
double error=1e8;
可见这个东西已经出现了。 这个辅助量用来记录时间序列中每个时间点的数据,因为每个数据在最终的BPTT过程中都是需要的。 rnn中的wi是输入端的权重,wh是前一次隐藏层输出的权重。
但这样写有一个缺陷。 diff记录了该单元在t时刻的训练增量。 显然,直接遍历所有时间并将增量按顺序分配给数据是不可行的。 因为在每个时刻内,增量的幅度可能很小,甚至可能达到1e-8或更小(在很长的时间序列中,可以达到1e-20的水平)。 如果直接赋值给数据的话,就相当于给数据加了0,失去了精度。
例如:x=0.1,y=1e-10;
x+y之后,结果仍然是0.1,显然精度丢失了。
所以为了避免这个问题,我们还需要再添加一个来累加所有时刻的diff,并一起赋值给数据。 但是,如果这样做,则每次都必须对每个数据(包括权重)都这样做,因为一开始找到的 diff 是 bia 的偏导数。 如果直接将它们全部相加,则只能得到 bia 的偏导数。
然后
template <const int InputNum,const int HideNum,const int Maxtime>
struct rnn_neuron
{
double wi[InputNum],wh[HideNum],sigmawi[InputNum],sigmawh[HideNum];
double bia,diff[Maxtime],sigmabia;
double in[Maxtime],out[Maxtime];
};
template <const int InputNum,const int Maxtime>
struct nor_neuron
{
double w[InputNum],bia,diff[Maxtime],sigmaw[InputNum],sigmabia;
double in[Maxtime],out[Maxtime];
};
就变成这样了。
同理,lstm也是同样的思路,但是数据更多了,随着数据量的增加,训练速度明显会变得很慢(真是一个重大的改变!)
template <const int Inputnum,const int Hidenum,const int Maxtime>
struct LSTM_neuron
{
double cell[Maxtime];
double out[Maxtime];
double fog_in[Maxtime],fog_out[Maxtime],fog_bia,fog_wi[InputNum],fog_wh[HideNum],fog_diff[Maxtime];
double sig_in[Maxtime],sig_out[Maxtime],sig_bia,sig_wi[InputNum],sig_wh[HideNum],sig_diff[Maxtime];
double tan_in[Maxtime],tan_out[Maxtime],tan_bia,tan_wi[InputNum],tan_wh[HideNum],tan_diff[Maxtime];
double out_in[Maxtime],out_out[Maxtime],out_bia,out_wi[InputNum],out_wh[HideNum],out_diff[Maxtime];
double fog_transbia,fog_transwi[InputNum],fog_transwh[HideNum];
double sig_transbia,sig_transwi[InputNum],sig_transwh[HideNum];
double tan_transbia,tan_transwi[InputNum],tan_transwh[HideNum];
double out_transbia,out_transwi[InputNum],out_transwh[HideNum];
};
那么rnn和lstm的Calc()和()函数必须重写!
下一步就是使用这些单元来编写一些模型,然后将测试后的模型进行打包。
我们以初始BP为例。 这个想法其实很简单。 我们已经有了一个定义的 BP 神经元。 然后我们用它来创建一个类并包含一些函数。 bp.h用于put和class
/*bp.h header file by ValK*/
/* 2019/3/15 15:25 */
#ifndef __BP_H__
#define __BP_H__
#include
#include
#include
#include
#include
#include
using namespace std;
template <const int NUM>
struct neuron
{
double w[NUM],bia,diff;
double in,out;
};
class ActivateFunction
{
public:
double sigmoid(double x)
{
return 1.0/(1.0+exp(-x));
}
double diffsigmoid(double x)
{
x=1.0/(1.0+exp(-x));
return x*(1-x);
}
double tanh(double x)
{
return (exp(x)-exp(-x))/(exp(x)+exp(-x));
}
double difftanh(double x)
{
x=tanh(x);
return 1-x*x;
}
double relu(double x)
{
return x>0? x:0;
}
double diffrelu(double x)
{
return x>0? 1:0;
}
};
ActivateFunction fun;
template<const int INUM,const int HNUM,const int ONUM>
class bp_neural_network
{
private:
neuron<HNUM> hide[HNUM];
neuron<ONUM> output[ONUM];
double learningrate;
double input[INUM];
double expect[ONUM];
int batch_size;
double sigma_error;
double error;
public:
int epoch;
void TxtCheck()
{
if(!fopen("data.ai","r"))
{
INIT();
Dataout();
}
if(!fopen("trainingdata.txt","r"))
{
cout<<"Cannot open file:trainingdata.txt"<<endl;
cout<<"Programme exited with an unexpected error"<<endl;
exit(0);
}
}
bp_neural_network()
{
epoch=0;
sigma_error=1e8;
error=1e8;
batch_size=1;
learningrate=0.01;
TxtCheck();
}
void SetBatch(int Batch)
{
batch_size=Batch;
return;
}
void INIT()
{
srand(unsigned(time(NULL)));
/*statement*/
return;
}
void Datain()
{
ifstream fin("data.ai");
/*statement*/
fin.close();
}
void Dataout()
{
ofstream fout("data.ai");
/*statement*/
fout.close();
}
void Mainwork()
{
ifstream fin("trainingdata.txt");
sigma_error=0;
for(int b=0;b<batch_size;b++)
{
/*处理batch数据,读入input和expect*/
Calc();
ErrorCalc();
Training();
sigma_error+=error;
}
fin.close();
return;
}
void Calc()
{
for(int i=0;i<HNUM;i++)
{
hide[i].in=hide[i].bia;
for(int j=0;j<INUM;j++)
hide[i].in+=input[j]*hide[i].w[j];
hide[i].out=fun.sigmoid(hide[i].in);
}
for(int i=0;i<ONUM;i++)
{
output[i].in=output[i].bia;
for(int j=0;j<HNUM;j++)
output[i].in+=hide[j].out*output[i].w[j];
output[i].out=fun.sigmoid(output[i].in);
}
}
void ErrorCalc()
{
double trans;
error=0;
for(int i=0;i<ONUM;i++)
{
trans=output[i].out-expect[i];
error+=trans*trans;
}
error*=0.5;
}
void Training()
{
for(int i=0;i<ONUM;i++)
output[i].diff=(expect[i]-output[i].out)*fun.diffsigmoid(output[i].in);
//负号直接舍弃,因为整个传递过程这里的负号不带来影响
//而且在最后更新数据的时候也不需要再*(-1)
for(int i=0;i<HNUM;i++)
{
hide[i].diff=0;
for(int j=0;j<ONUM;j++)
hide[i].diff+=output[j].diff*output[j].w[i];
hide[i].diff*=fun.diffsigmoid(hide[i].in);
}
for(int i=0;i<ONUM;i++)
{
output[i].bia+=learningrate*output[i].diff;
for(int j=0;j<HNUM;j++)
output[i].w[j]+=learningrate*output[i].diff*hide[j].out;
}
for(int i=0;i<HNUM;i++)
{
hide[i].bia+=learningrate*hide[i].diff;
for(int j=0;j<INUM;j++)
hide[i].w[j]+=learningrate*hide[i].diff*input[j];
}
return;
}
};
#endif
这初始的三个参数是建立网络所必需的参数。 这个想法在编写其他包时非常重要。
是单位,包括基本bp神经元所需的数据,类包括一些需要用到的激励函数。
为了节省时间,有些功能我就不写太多了。 设计构造函数的时候,你可以自己创新,想怎么写就怎么写。 这里的构造函数首先初始化epoch、error等。 (我被迫将函数内容直接写到类中...教授看到会骂我的)
通常建议您不要封装函数。 。 因为bp可能会用来处理各种问题,所以为了保证灵活性,最好自己写在外面,想加什么功能就加什么。
写一个小bug(错误),看看运行是否正常:
没问题,因为我没有训练集,所以我在构造函数中判断了,直接退出程序。
更新内容基本结束啦~
更新于2019.5.14
本课程设计写了相关代码,但与答案中提供的方法不同。 该头文件库中所有网络建立都是通过参数传递+内存分配完成的,并没有使用。 //