基于C++的 RNN/lstm神经网络算法(不调用外源库)

目前玩深度学习的小伙伴,上来就是使用现有的深度学习框架(TensorFlow,keras,pytorch,caffe),增加网络层,就像搭积木似的,看似方便,实则有时不利于个人能力发展,要知道现在公司需要的算法工程师,不仅仅只是会搭积木(这种工作,入门几个月的人就可以干了),而是要深入底层,能优化代码,能自己搭。

本文章适合以下几类人:

1)初学者,了解神经网络的推理和参数更新过程

2)会使用深度学习框架但不知道代码的实现

3)想提升自己的代码能力
 

RNN基本知识介绍:

用之前的输出和当前的输入作为新的输入输入带神经网络

代码实现(让程序自己学会是否需要进位,从而学会加法)

void RNN::train()
{
	int epoch, i, j, k, m, p;
	vector<double*> layer_1_vector;      //保存隐藏层
	vector<double> layer_2_delta;        //保存误差关于Layer 2 输出值的偏导

	for (epoch = 0; epoch<10000; epoch++)  //训练次数
	{
		double e = 0.0;  //误差
		for (i = 0; i<layer_1_vector.size(); i++)
			delete layer_1_vector[i];
		layer_1_vector.clear();
		layer_2_delta.clear();

		int d[binary_dim];                    //保存每次生成的预测值
		memset(d, 0, sizeof(d));

		int a_int = (int)randval(largest_number / 2.0);  //随机生成一个加数 a
		int a[binary_dim];
		int2binary(a_int, a);                 //转为二进制数

		int b_int = (int)randval(largest_number / 2.0);  //随机生成另一个加数 b
		int b[binary_dim];
		int2binary(b_int, b);                 //转为二进制数

		int c_int = a_int + b_int;            //真实的和 c
		int c[binary_dim];
		int2binary(c_int, c);                 //转为二进制数

		double *layer_1 = new double[hidenode];
		for (i = 0; i<hidenode; i++)         //在0时刻是没有之前的隐含层的,所以初始化一个全为0的
			layer_1[i] = 0;
		layer_1_vector.push_back(layer_1);

		//正向传播
		for (p = 0; p<binary_dim; p++)           //循环遍历二进制数组,从最低位开始
		{
			layer_0[0] = a[p];
			layer_0[1] = b[p];
			double y = (double)c[p];          //实际值
			layer_1 = new double[hidenode];   //当前隐含层

			for (j = 0; j<hidenode; j++)
			{
				//输入层传播到隐含层
				double o1 = 0.0;
				for (m = 0; m<innode; m++)
					o1 += layer_0[m] * w[m][j];

				//之前的隐含层传播到现在的隐含层
				double *layer_1_pre = layer_1_vector.back();
				for (m = 0; m<hidenode; m++)
					o1 += layer_1_pre[m] * wh[m][j];

				layer_1[j] = sigmoid(o1);      //隐藏层各单元输出
			}

			for (k = 0; k<outnode; k++)
			{
				//隐藏层传播到输出层
				double o2 = 0.0;
				for (j = 0; j<hidenode; j++)
					o2 += layer_1[j] * w1[j][k];
				layer_2[k] = sigmoid(o2);          //输出层各单元输出
			}

			d[p] = (int)floor(layer_2[0] + 0.5);   //记录预测值
			layer_1_vector.push_back(layer_1);     //保存隐藏层,以便下次计算

												   //保存标准误差关于输出层的偏导
			layer_2_delta.push_back((y - layer_2[0]) * dsigmoid(layer_2[0]));
			e += fabs(y - layer_2[0]);          //误差
		}

		//误差反向传播

		//隐含层偏差,通过当前之后一个时间点的隐含层误差和当前输出层的误差计算
		double *layer_1_delta = new double[hidenode];
		double *layer_1_future_delta = new double[hidenode];   //当前时间之后的一个隐藏层误差
		for (j = 0; j<hidenode; j++)
			layer_1_future_delta[j] = 0;
		for (p = binary_dim - 1; p >= 0; p--)
		{
			layer_0[0] = a[p];
			layer_0[1] = b[p];

			layer_1 = layer_1_vector[p + 1];     //当前隐藏层
			double *layer_1_pre = layer_1_vector[p];   //前一个隐藏层

			for (k = 0; k<outnode; k++)  //对于网络中每个输出单元,更新权值
			{
				//更新隐含层和输出层之间的连接权
				for (j = 0; j<hidenode; j++)
					w1[j][k] += alpha * layer_2_delta[p] * layer_1[j];
			}

			for (j = 0; j<hidenode; j++) //对于网络中每个隐藏单元,计算误差项,并更新权值
			{
				layer_1_delta[j] = 0.0;
				for (k = 0; k<outnode; k++)
					layer_1_delta[j] += layer_2_delta[p] * w1[j][k];
				for (k = 0; k<hidenode; k++)
					layer_1_delta[j] += layer_1_future_delta[k] * wh[j][k];

				//隐含层的校正误差
				layer_1_delta[j] = layer_1_delta[j] * dsigmoid(layer_1[j]);

				//更新输入层和隐含层之间的连接权
				for (k = 0; k<innode; k++)
					w[k][j] += alpha * layer_1_delta[j] * layer_0[k];

				//更新前一个隐含层和现在隐含层之间的权值
				for (k = 0; k<hidenode; k++)
					wh[k][j] += alpha * layer_1_delta[j] * layer_1_pre[k];
			}

			if (p == binary_dim - 1)
				delete layer_1_future_delta;
			layer_1_future_delta = layer_1_delta;
		}
		delete layer_1_future_delta;

		if (epoch % 1000 == 0)
		{
			cout << "error:" << e << endl;
			cout << "pred:";
			for (k = binary_dim - 1; k >= 0; k--)
				cout << d[k];
			cout << endl;

			cout << "true:";
			for (k = binary_dim - 1; k >= 0; k--)
				cout << c[k];
			cout << endl;

			int out = 0;
			for (k = binary_dim - 1; k >= 0; k--)
				out += d[k] * pow(2, k);
			cout << a_int << " + " << b_int << " = " << out << endl << endl;
		}
	}
}

LSTM基本知识介绍:

LSTM 是解决短时记忆问题的解决方案,它们具有称为“门”的内部机制,可以调节信息流,这些“门”可以知道序列中哪些重要的数据是需要保留,而哪些是要删除的。 随后,它可以沿着长链序列传递相关信息以进行预测。

代码实现(使用lstm网络来学习z=x^2-xy+y^2这一函数,x、y作为训练特征,计算出来的z作为训练标签调用train函数进行训练)

/*
训练网络
参数:
trainSet、训练特征集
labelSet、训练标签集
epoche、迭代次数
verification、验证集的比例
stopThreshold、提前停止阈值,当两次迭代结果的变化小于此阈值则停止
*/
void Lstm::train(vector<DataType*> trainSet, vector<DataType*> labelSet, int epoche, double verification, double stopThreshold){
	if(trainSet.size()<=0 || labelSet.size()<=0 || trainSet.size()!=labelSet.size()){
		cout<<"data set error!"<<endl;
		return;
	}


    _verification = 0;
    if(verification>0 && verification<0.5){
        _verification = verification;
    }else{
        cout<<"verification rate is invalid."<<endl;
    }

	double lastTrainRmse = 0.0;
	double lastVerRmse = 0.0;
    _LEARNING_RATE = LEARNING_RATE;//开始训练前初始化学习率 适用于SGD优化器

    //计算验证集的平均值
    double verificationAvg = 0.0;
    if(_verification>0){
        int verLen = _verification*labelSet.size();
        FOR(i, verLen){
        	verificationAvg += labelSet[labelSet.size()-verLen+i][0];
        }
        verificationAvg /= verLen;
        verificationAvg = verificationAvg<0?-verificationAvg:verificationAvg;
        cout<<"---------------avg="<<verificationAvg<<endl;
    }

    Deltas *deltaSet = new Deltas(_inNodeNum, _hideNodeNum, _outNodeNum);
    cout<<"deltaset inited. start trainning."<<endl;
	FOR(e, epoche){	
		//每次epoche清除单元状态
		resetStates();
		//正向传播
		forward(trainSet, labelSet);
		//反向计算误差并计算偏导数
        deltaSet->resetDelta();//重置每个权值的偏导数值
		backward(trainSet, deltaSet);
		//根据偏导数更新权重
		optimize(deltaSet, e);

		//验证前清除单元状态
		resetStates();
		double trainRmse = trainLoss(trainSet, labelSet);
		double verRmse = verificationLoss(trainSet, labelSet);
		// cout<<"epoche:"<<e<<"|rmse:"<<trainRmse<<endl;
		if(e>0 && abs(trainRmse-lastTrainRmse) < stopThreshold){//变化足够小
			cout<<"train rmse got tiny diff, stop in epoche:"<<e<<endl;
			break;
		}

		if(e>0 && verRmse!=0 && (verRmse-lastVerRmse)>(verificationAvg*0.025)){//验证集准确率大幅下降则停止 0.03~0.04(84.792)
			// cout<<"verification rmse ascend too much:"<<verRmse-lastVerRmse<<", stop in epoche:"<<e<<endl;
			// cout<<"verification rmse ascend or got tiny diff, stop in epoche:"<<e<<endl;
			break;
		}

		lastTrainRmse = trainRmse;
		lastVerRmse = verRmse;
	}
    deltaSet->~Deltas();
    deltaSet = NULL;
}

运行过程

用到的软件是vs2010以上的版本都可以,不用额外配置什么,没调包,会用这个软件进行c++开发,就会使用这个软件

由于程序不调用任何外源库,所以读者可以看清楚每一个算法的原理,要想学好神经网络,必须打好基础,不要好高骛远,另外,程序都是有备注,应该很好理解的,实在不懂,可以来问店主

代码的下载路径:基于C++的 RNN/lstm神经网络算法(不调用外源库)

有问题可以私信或者留言,有问必答
 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值