您好,欢迎访问代理记账网站
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

C++下实现全连接神经网络

    说明一下环境,该代码由C++实现,但是矩阵运算在C++中没有实现,自己造轮子既浪费时间又无必要,因此采用了Eigen库来进行矩阵操作,其他功能的代码均为自己实现。

一、环境配置

    Eigen矩阵操作方便,本文在VS2015的环境下配置Eigen,具体的Eigen库配置过程不再细说,不了解怎么配置Eigen的话可以看这篇文章https://blog.csdn.net/panpan_jiang1/article/details/79649452,环境就是这样,其余的工作就是新建一个工程,开始编写代码,本文使用Eigen3.3.9版本。

二、网络结构

    网络共有输入层、隐含层、输出层3层,输入层输入维度数10,隐含层输出维度为5,输出层为1,大概结构如下图所示:
在这里插入图片描述
就下来就上具体代码部分:
    最开始是固定的参数部分:

#define InputShape 10
#define Layer1_OutShape 5
#define Layer2_OutShape 1
//数据量
#define DataNum 100
//训练轮数
#define Epcho   2000

    首先,我们定义一个神经网络的类(class)NN,一个神经网络的可训练参数有权值W与阈值B,固定的是学习率(当然有衰减学习率,这里我没有这样做)与输入值与真实(标签)值,因此,在初始化一个神经网络时先将权值阈值随机初始化。
NN的构造函数:

NN::NN(MatrixXf input, MatrixXf y_true, float alph)//初始化权值
{
	this->input = input;
	this->y_true = y_true;
	this->alph = alph;
	this->W1_T = MatrixXf::Random(Layer1_OutShape, InputShape);
	this->W2_T = MatrixXf::Random(Layer2_OutShape, Layer1_OutShape);
	this->B1 = MatrixXf::Zero(Layer1_OutShape,this->input.cols());
	this->B2 = MatrixXf::Zero(Layer2_OutShape, this->input.cols());
}

随后,建立网络的前向传播

MatrixXf NN::ForWard()
{
	this->Z1 = this->W1_T * this->input;
	this->A1 = Leak_ReLu(this->Z1, this->alph);
	this->Z2 = this->W2_T * this->A1;
	this->A2 = Leak_ReLu(this->Z2, this->alph);
	//return MSE(this->y_true, this->A2);
	return this->A2;
}

前向传播的数学原理描述就比较简单了,
    输入层到隐含层:
                                         Z 1 = W 1 T ∗ i n p u t Z_{1}=W_{1}^{T}*input Z1=W1Tinput,

                                         A 1 = L e a k y _ R e L u ( Z 1 ) A_{1}=Leaky\_ReLu(Z_{1}) A1=Leaky_ReLu(Z1)

                     L e a k y _ R e L u ( Z ) = { Z Z > = 0 a l p h ∗ Z Z < 0 Leaky\_ReLu(Z)=\left\{\begin{matrix} Z & Z>=0 & \\ alph*Z & Z<0 & \end{matrix}\right. Leaky_ReLu(Z)={ZalphZZ>=0Z<0
    alph的值默认0.1。
    隐含层到输出层:
                                         Z 2 = W 2 T ∗ A 1 Z_{2}=W_{2}^{T}*A_{1} Z2=W2TA1,

                                         A 2 = L e a k y _ R e L u ( Z 2 ) A_{2}=Leaky\_ReLu(Z_{2}) A2=Leaky_ReLu(Z2)

重头戏来了,反向传播过程,先贴代码^_^

float NN::BackWard()
{
	int rows_temp, cols_temp;//临时行列变量
	//Abount Layer2 work start!!
	this->dJ_dA2 = 2 * (this->A2 - this->y_true);
	this->dA2_dZ2 = MatrixXf::Ones(this->Z2.rows(),this->Z2.cols());
	for (rows_temp = 0; rows_temp < this->Z2.rows(); ++rows_temp)
	{
		for (cols_temp = 0; cols_temp < this->Z2.cols(); ++cols_temp)
		{
			this->dA2_dZ2(rows_temp, cols_temp) = this->Z2(rows_temp, cols_temp) >= 0 ? 1.0 : this->alph;
		}
	}
	this->dZ2_dW2 = this->A1.transpose();
	this->dW2 = this->dJ_dA2.cwiseProduct(this->dA2_dZ2)*this->dZ2_dW2/DataNum;
	this->dB2 = this->dJ_dA2.cwiseProduct(this->dA2_dZ2) / DataNum;
	//Abount Layer2 work end!!

	//Abount Layer1 work start!!
	this->dZ2_dA1 = this->W2_T.transpose();
	this->dA1_Z1 = MatrixXf::Ones(this->Z1.rows(), this->Z1.cols());
	for (rows_temp = 0; rows_temp < this->Z1.rows(); ++rows_temp)
	{
		for (cols_temp = 0; cols_temp < this->Z1.cols(); ++cols_temp)
		{
			this->dA1_Z1(rows_temp, cols_temp) = this->Z1(rows_temp, cols_temp) >= 0 ? 1.0 : this->alph;
		}
	}

	this->dZ1_W1 = this->input.transpose();
	this->dW1 = this->dA1_Z1.cwiseProduct(this->dZ2_dA1 * this->dJ_dA2.cwiseProduct(this->dA2_dZ2))*this->dZ1_W1 / DataNum;
	this->dB1 = this->dA1_Z1.cwiseProduct(this->dZ2_dA1 * this->dJ_dA2.cwiseProduct(this->dA2_dZ2)) / DataNum;
	//Abount Layer1 work end!!
	//调整学习参数
	this->W2_T = this->W2_T - this->alph*this->dW2;
	this->W1_T = this->W1_T - this->alph*this->dW1;
	this->B2 = this->B2 - this->alph*this->dB2;
	this->B1 = this->B1 - this->alph*this->dB1;
	return MSE(this->y_true,this->ForWard());
}

    只看代码一定会晕菜,先放一会,让我解释下整体过程:NN(神经网络)初始化好之后,给它喂入数据,NN会给出预测值y_pred,y_pred一定会跟真实值y_true有误差,用简单的均方误差(MSE)公式根据y_pred与y_true会计算出一个损失值loss,NN就是基于loss根据一定算法调整权值与阈值,目标是使得loss变小、趋向于0,这样y_pred就会与y_true变得一样。
    好的,那该如何调整呢?简单的,我们想到了梯度下降法,上图先
在这里插入图片描述
简化下问题,我们的目的是调整W使得loss(W)变小,那么,W就可以根据loss函数在W处的梯度调整

                                                     W 1 ′ = W − a l p h ∗ d l o s s ( W ) d W W_{1}^{'}=W-alph*\frac{d loss(W)}{dW} W1=WalphdWdloss(W)
这样loss不就小了吗?
所以, W 1 , W 2 , B 1 , B 2 W_{1} ,W_{2} ,B_{1} ,B_{2} W1,W2,B1,B2都是这么调整,这里只举一个例子:

                                                     W 1 = W − a l p h ∗ d J d W 1 W_{1}=W-alph*\frac{d J}{dW_{1}} W1=WalphdW1dJ
J是代价函数,注意,这里是代价函数,不是损失函数,代价函数是损失函数在每个样本上的损失值和再求平均。以MSE和本文的数据为例,y_true与y_pred都是1行100列的向量(矩阵),J就为:

                         J ( y _ t r u e , y _ p r e d ) = ∑ i = 0 100 − 1 M S E ( y _ t r u e ( 0 , i ) , y _ p r e d ( 0 , i ) ) / 100 J(y\_true,y\_pred)=\sum_{i=0}^{100 - 1}MSE(y\_true(0,i),y\_pred(0,i))/100 J(y_true,y_pred)=i=01001MSE(y_true(0,i),y_pred(0,i))/100

J是关于y_pred与y_true的函数。
OK,这里我们就要求 d J d W 1 \frac{d J}{dW_{1}} dW1dJ了,这里需要用一点大学知识链式求导法则,或者,你就直接看做分式的化简的反向操作:

                                             d J W 2 = d J d A 2 ∗ d A 2 d Z 2 ∗ d Z 2 d W 2 \frac{dJ}{W_{2}}=\frac{dJ}{dA_{2}}*\frac{dA_{2}}{dZ_{2}}*\frac{dZ_{2}}{dW_{2}} W2dJ=dA2dJdZ2dA2dW2dZ2

                                        J/W2 ==>>(J/A2) * (A2/Z2) *(Z2/W2)

A 2 A_{2} A2就是y_pred,J又是关于y_pred与y_true的函数,y_true是定值,所以这个求导结果就非常简单了,直接给答案,求导结果是2*(y_pred-y_true)。
A 2 = L e a k y _ R e L u ( Z 2 ) A_{2}=Leaky\_ReLu(Z_{2}) A2=Leaky_ReLu(Z2),导数很明显是分段的, Z 2 > = 0 Z_{2}>=0 Z2>=0,导数为1,反之,导数为alph。
Z 2 = W 2 T ∗ A 1 Z_{2}=W_{2}^{T}*A_{1} Z2=W2TA1,这个导数学过矩阵求导的话应该知道导数是 A 1 A_{1} A1的转置。
这样的话 d J W 2 \frac{dJ}{W_{2}} W2dJ就求出来了,只要 W 2 = W 2 − a l p h ∗ d J d W 2 W_{2}=W_{2}-alph*\frac{d J}{dW_{2}} W2=W2alphdW2dJ这样操作就好。

OK,这样的话关于 W 1 W_{1} W1的调整正也同理

                                                 d J W 1 = d J d A 2 ∗ d A 2 d Z 2 ∗ d Z 2 d A 1 ∗ d A 1 d Z 1 ∗ d Z 1 d W 1 \frac{dJ}{W_{1}}=\frac{dJ}{dA_{2}}*\frac{dA_{2}}{dZ_{2}}*\frac{dZ_{2}}{dA_{1}}*\frac{dA_{1}}{dZ_{1}}*\frac{dZ_{1}}{dW_{1}} W1dJ=dA2dJdZ2dA2dA1dZ2dZ1dA1dW1dZ1

关于阈值 B 1 B_{1} B1 B 2 B_{2} B2,
                                                             d J B 2 = d J d A 2 ∗ d A 2 d Z 2 \frac{dJ}{B_{2}}=\frac{dJ}{dA_{2}}*\frac{dA_{2}}{dZ_{2}} B2dJ=dA2dJdZ2dA2

                                                     d J B 1 = d J d A 2 ∗ d A 2 d Z 2 ∗ d Z 2 d A 1 ∗ d A 1 d Z 1 \frac{dJ}{B_{1}}=\frac{dJ}{dA_{2}}*\frac{dA_{2}}{dZ_{2}}*\frac{dZ_{2}}{dA_{1}}*\frac{dA_{1}}{dZ_{1}} B1dJ=dA2dJdZ2dA2dA1dZ2dZ1dA1

都是在 W W W的计算过程计算了,直接拿来用就好。于是,给出NN的定义

class NN
{
	public:
		NN(MatrixXf input, MatrixXf y_true, float alph);
		MatrixXf ForWard();
		float BackWard();
	private:
		//神经网络的输入值、输出的真实值、学习率等较为固定的值
		MatrixXf input;
		MatrixXf y_true;
		float alph;
		//神经网络待学习参数
		MatrixXf W1_T;
		MatrixXf B1;
		MatrixXf W2_T;
		MatrixXf B2;
		//神经网络中间参数
		MatrixXf Z1;       
		MatrixXf A1;
		MatrixXf Z2;
		MatrixXf A2;//==out
		//反向传播所用参数
		MatrixXf dW2;
		MatrixXf dB2;
		MatrixXf dW1;
		MatrixXf dB1;
		//对Layer2所需参数
		MatrixXf dJ_dA2;
		MatrixXf dA2_dZ2;
		MatrixXf dZ2_dW2;
		MatrixXf dZ2_dB2;
		//对Layer1所需参数
		MatrixXf dZ2_dA1;
		MatrixXf dA1_Z1;
		MatrixXf dZ1_W1;
};

PS:用的是BGSD(批量梯度下降法)。

三、数据集说明

    数据集是用Eigen库的Random随机生成10行100列的数据(Random是生成-1到1的均匀分布数字),每一列是一条数据,真实值(标签)是这样生成的,每一条的数据和>0:标签为1,反之,标签为0.
代码:

MatrixXf input_data,y_true,y_pred;
	int rows_temp,cols_temp;//行列数临时变量
	input_data = MatrixXf::Random(InputShape, DataNum);
	y_true = MatrixXf::Zero(Layer2_OutShape, DataNum);
	for (cols_temp = 0; cols_temp < DataNum; ++cols_temp)
	{
		y_true(0, cols_temp) = input_data.col(cols_temp).sum() > 0 ? 1.0:0.0;
	}

四、运行结果

贴图,有图有真相:
先是训练过程的部分loss变化情况
在这里插入图片描述
可以看到,loss在稳步下降。
2000迭代后:
在这里插入图片描述
预测结果与真实结果对比,认为正确的判断条件是:y_true是1,对应的y_pred>0.5,反之y_true是0,对应的y_pred<0.5。
在这里插入图片描述
最终准确率95%
在这里插入图片描述
写了好几天,搞了好几个版本,终于把功能实现了,从来都是知易行难【抱拳】。代码上传到这里了https://github.com/tian0zhi/Neural-Network-By-C-Plus-Plus,使用与转载请注明出处


分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进