说明一下环境,该代码由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=W1T∗input,
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)={Zalph∗ZZ>=0Z<0
alph的值默认0.1。
隐含层到输出层:
Z
2
=
W
2
T
∗
A
1
Z_{2}=W_{2}^{T}*A_{1}
Z2=W2T∗A1,
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′=W−alph∗dWdloss(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=W−alph∗dW1dJ
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=0100−1MSE(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=dA2dJ∗dZ2dA2∗dW2dZ2
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=W2T∗A1,这个导数学过矩阵求导的话应该知道导数是
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=W2−alph∗dW2dJ这样操作就好。
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=dA2dJ∗dZ2dA2∗dA1dZ2∗dZ1dA1∗dW1dZ1
关于阈值
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=dA2dJ∗dZ2dA2
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=dA2dJ∗dZ2dA2∗dA1dZ2∗dZ1dA1
都是在 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,使用与转载请注明出处