您的位置  > 互联网

高三造轮子的一知半解,你知道吗?

在造轮子之前,一定要广泛查阅资料,自己推演前向传播和反向传播的所有过程。 这个过程需要的知识点是偏导数和链式法则,高中生其实都可以理解(将偏导数理解为求导多元函数中的一个变量的导数,只需将所有其他量视为常数即可) ),但是有点难,直接导致我高三的时候对它一知半解。

首先,入门级实现应该从最简单的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

本课程设计写了相关代码,但与答案中提供的方法不同。 该头文件库中所有网络建立都是通过参数传递+内存分配完成的,并没有使用。 //