Neural Network(已废弃,见最新文章)
神经网络Neural Network
前言:
最近学习了神经网络的算法, 想要整合一下脑子中所有的想法, 顺便梳理一下思路, 于是写下这篇文章.
本篇文章代码基于c语言, 从最简单的二分类问题入手.
什么是神经网络:
神经网络(Neural Network,NN)一般指人工神经网络(Artificial Neural Network,ANN).
神经网络的名称和结构均受到人脑的启发, 通过模仿生物神经元相互传递信号的方式进行机器学习, 从而让机器掌握与神经结构类似的反应机制.

图1-1

图1-2

图1-3
如同生物神经元有许多输入(树突)一样, 人工神经元也有很多输入信号, 并同时作用到人工神经元上, 生物神经元中大量的突触具有不同的性质和强度, 使得不同的输入的激励作用各不相同, 因此在人工神经元中, 对每一个输入都有一个可变的加权$\omega$, 用于模拟生物神经元中突触的不同连接强度及突触的可变传递特性. 因此我们构建一个由节点层组成的人工神经网络, 包含一个输入层, 一个或多个隐藏层和一个输出层. 每一层称为一个人工神经元, 它们连接到另一个层,具有相关的权重和阈值.

图1-4
正向传播神经网络:
什么是正向传播神经网络:
前向传播(Forward Propagation)就是从Input, 经过一层层的Layer, 不断计算每一层的Z和A, 最后得到输出y的过程.
正向传播是如何进行的:
在图1-4中, 我们向隐藏层输入了已知的参数xi, 使用$z = \omega x + b$($z ,\omega, b$ 均表示向量 $z = [z1,z2…,z3]$)来计算第一层的神经元的值. 然后让每一层通过激活函数, $a = g(z)$ 得到激活层的值, 再将此激活层作为下一层的输入值继续上述过程, 最后到达输出层, 给出预测值.
神经网络的权重在正向传播过程中, 由上一层到下一层的连接权重以及每个神经元之间的边权重共同确定, 这些权重都是在训练中通过反向传播和梯度下降法进行更新的, 在反向传播过程中再讲解.
什么是激活函数:
激活函数(Activation Function)是一种添加到人工神经网络中的函数, 旨在帮助神经网络学习数据中复杂关系. 因为神经网络中每一层的输入输出都是一个线性求和的过程, 下一层的输出只是承接了上一层输入函数的线性变换, 所以如果没有激活函数, 那么无论你构造的神经网络多么复杂, 最后的输出都是输入的线性组合, 纯粹的线性组合并不能够解决更为复杂的问题. 而引入非线性的激活函数之后, 会给神经元引入非线性元素, 使得神经网络可以逼近其他的任何非线性函数, 这样可以使得神经网络应用到更多模型中.
常见的激活函数: Sigmoid激活函数, Relu激活函数, Tanh(双曲正切)激活函数.
下面主要以Sigmoid函数为例:
Sigmoid函数也叫Logistic函数, 用于隐层神经元输出 ,取值范围为(0,1), 它可以将一个实数映射到(0,1)的区间, 可以用来做二分类. 在特征相差比较复杂或是相差不是特别大时效果比较好.
函数的表达式如下:
图像类似一个S形曲线:

图1-4
不同的激活函数有不同的适用范围, 这方面具体的内容在之后的文章中(如果有时间做的话)再做说明.
如何搭建一个正向传播神经网络(以c语言为例):
参数设置:
从零开始搭建一个神经网络, 我们首先要确定神经网络的层数, 每一层神经元的个数.
这里以两层隐藏层为例:
数据的初始化:
对于一个还没经过训练的神经网络, 我们需要先对权重的初始化, 使其附上一定的初值, 再进行正向传播.
由于我们使用的激活函数是Sigmoid函数, 在x过大或者过小的时候会丢失一定的有用信息, 所以我们希望权重的值分布在一个较小的范围内.
在每一步, 权重矩阵乘以来自前一层的激活值. 如果每一层的权重大于1, 并且导致激活值的大小大于1, 当它们被重复乘以多次时,它们就会不断变大,甚至到无穷大. 类似地, 如果权重小于1, 它们将消失为零. 这叫做渐变爆炸和渐变消失问题. 可以类比$ 1.1 ^ {100}$ 和 $ 0.9 ^ {100}$.
接下来介绍Xavier初始化方法:
Xavier初始化的主要思想是在初始化权重时尽可能保持每个神经元输出方差相等. 简单来说, 它会随机选择每个权重值, 并根据某种特定的分布对其进行缩放, 以使分布的方差在反向传播过程中保持不变. 实际上, Xavier初始化隐含了一个假设, 那就是前一层输出和该层输入的方差相等. 通过这种初始化方式, 可以使得每个神经元之间的输出方差尽量一致, 防止梯度消失或梯度爆炸问题的发生. 因此, 使用Xavier初始化可以在训练深度神经网络的时候更容易地实现收敛, 并提高训练速度和模型性能.
当使用Xavier初始化时,我们首先需要确定要初始化的层中神经元的数量num1,以及该层前一层中神经元的数量num2。然后,我们可以使用以下公式来计算权重矩阵的标准差σ:
接着, 我们将权重矩阵样本自一个以0为中心, 标准差为σ的均方分布随机抽取. 这些随机选择的权重值将成为该层权重矩阵的初始值,从而用于神经网络的训练过程. 代码中的hidden代表隐藏层, 也就是A层, 激活层.
代码如下:/* 权重 */
double Hidden_Weight_1[DIMENSION_SIZE * NUM_HIDDEN_1];
double Hidden_Weight_2[NUM_HIDDEN_1 * NUM_HIDDEN_2];
double Output_Weight[NUM_HIDDEN_2 * NUM_OUTPUT];
/* 偏置 */
double Hidden_Bias_1[DIMENSION_SIZE * NUM_HIDDEN_1];
double Hidden_Bias_2[NUM_HIDDEN_1 * NUM_HIDDEN_2];
double Output_Bias[NUM_HIDDEN_2 * NUM_OUTPUT];
/* weights 是要更改的值 count 是weight的数组大小 num1 上一层神经元数量 num2 当前层神经元数量 */
void xavier_init(double* weights, int count, int num1, int num2) {
double stddev = sqrt(2.0 / (num1 + num2));
for (int i = 0; i < count; i++) {
weights[i] = stddev * rand() / RAND_MAX;
}
}
/* 初始化权重和偏置,使用xavier方法 */
void Init_Weight(){
xavier_init(Hidden_Weight_1, DIMENSION_SIZE * NUM_HIDDEN_1, DIMENSION_SIZE, NUM_HIDDEN_1);
xavier_init(Hidden_Weight_2, NUM_HIDDEN_1 * NUM_HIDDEN_2, NUM_HIDDEN_1, NUM_HIDDEN_2);
xavier_init(Output_Weight, NUM_HIDDEN_2 * NUM_OUTPUT, NUM_HIDDEN_2, NUM_OUTPUT);
xavier_init(Hidden_Bias_1, DIMENSION_SIZE * NUM_HIDDEN_1, DIMENSION_SIZE, NUM_HIDDEN_1);
xavier_init(Hidden_Bias_2, NUM_HIDDEN_1 * NUM_HIDDEN_2, NUM_HIDDEN_1, NUM_HIDDEN_2);
xavier_init(Output_Bias, NUM_HIDDEN_2 * NUM_OUTPUT, NUM_HIDDEN_2, NUM_OUTPUT);
}
激活函数:
以Sigmoid函数为例(需要调用头文件 math.h):double Sigmoid(double z){
return 1/(1+exp(-z));
}
正向传播:
按照介绍的正向传播方法进行, 读入上一层的输入, 本层的权重, 计算本一层的激活值, 再作为下一层的输入值继续这个过程.
假设我们已经读取了数据并且存放在*feature里
下面给出代码:/* 先初始化隐藏层与输出层的值都为0 */
double hidden1[NUM_HIDDEN_1] = {0};
double hidden2[NUM_HIDDEN_2] = {0};
double output[NUM_OUTPUT] = {0};
/* 计算正向传播中隐藏层和输出层的值 */
/* layer 需要计算的数组 num1 本层的大小 num2 输入层的大小 input 输入数据 weight 权重 bias 偏置 */
void Caculate_Forward(double *layer,int num1, double *input, int num2, double *weight, double *bias){
for (int i = 0; i < num1; i++){
double sum = 0.0;
for (int j = 0; j < num2; j++){
sum = sum + input[j] * weight[i * num2 + j] + bias[i * num2 + j];
}
layer[i] = Sigmoid(sum);
}
}
之后调用这个函数进行正向传播:
double hidden1[NUM_HIDDEN_1] = {0}; |
这样就得到了最后的预测值.
完整代码在反向传播神经网络5.1.
反向传播神经网络(BP神经网络):
通过了上面的正向传播的过程, 我们得到了最后输出的y值, 假如我们这是一个二元分类问题(即0,1问题), 我们可以根据y的值来判断分类结果是否正确. 如果y值>0.5, 我们可以认为这个分类结果是1, 反之则为0. 但是由于权重是随机创建的, 预测的结果是一片狼藉. 此时我们就会想, 该如何重新设定权重, 让误差越来越小呢? 就是使用误差的反向传播进行权重的更新.
什么是BP神经网络:
反向传播神经网络(Backpropagation Neural Network)是一种应用广泛, 效果良好的人工神经网络模型. 它是一种基于梯度下降的监督学习算法, 在训练数据上通过计算损失函数的梯度, 不断调整神经网络的权重和偏置, 从而使神经网络能够逐步逼近目标函数并达到较高的准确率.
反向传播神经网络的核心思想是误差反向传播. 当我们人类伸手去抓住一件物品的时候, 如果第一次物品太远了或者太重了, 没抓住, 我们就会修改抓的距离或者抓的位置与力度, 从而抓住物品.
简单来说, 反向传播就是这样一个过程. 误差通过反向逐层传播计算, 从而得出每层的误差贡献, 然后利用梯度下降算法来调整每个节点的权重. 这样一步步的反向传递, 最终能够使模型的预测错误降到最小, 从而达到最佳的预测效果.
BP神经网络中的一些数学推导:
前置知识: 复合函数求导的链式法则
已知二元函数$z(x,y) = f(u,v)$, $u,v$又分别是$x,y$的函数,则$z$最终是$x,y$的函数: $ z(x,y) = f(u(x,y),v(x,y)) $
$z$的全微分形式是:
又因为$u,v$是$x,y$的函数, 可以得到$z$关于$x,y$的全微分关系:
最后根据偏导数的定义就得到了:
介绍完了链式法则, 我们可以正式开始误差反向传播的数学推导了.
误差的计算:
神经网络需要一个函数来测度模型的输出值y和真实因变量值之间的差异大小, 一般这种差异被称为残差或者误差. 误差离0越近,说明模型越好.
常用的误差方差:
均方误差
均方误差是各数据偏离真实值差值的平方和 的平均数, 也就是误差平方和的平均数.
其中 $y_i$ 代表真实值, $p_i$ 代表预测值.
这种损失函数通常用在实数值连续变量的回归问题上,并且对于残差较大的情况给予更多的权重。
交叉熵损失函数
交叉熵损失函数经常用于分类问题中, 此外由于交叉熵涉及到计算每个类别的概率, 所以交叉熵几乎每次都和sigmoid(或softmax)函数一起出现.
这里不做详细解释, 直接给出公式:
二分类问题:
其中 $y_i$ 代表真实值, $p_i$ 代表预测值.
我们选择交叉熵损失函数, 在使用反向传播算法更新神经网络的参数时, 我们需要计算输出层的误差, 即损失函数对输出层输入加权和的导数. 由于输出层仅对最后一层(隐层或输出层)的参数有影响, 因此, 输出层的误差可以由下式计算:
我们先计算Sigmoid函数的导数:
我们知道:
可以推导出:
应用到交叉熵函数上, 就得到了(输出层元素就一个p):
接下来详细推导:
我们已知 $p$ 是关于 $w,b,a,z$ 的函数:
我们先对 $p$ 对 $z_j$ 求偏导:
稍加整理, 我们就得到了:
因为我们输出层只有一个值, 所以可以去掉前面的平均和求和:
然后我们可以得到 $L$ 求 $z$ 的偏导:
继续化简:
得到了一个极其简单的式子.
其实在多分类问题中, 我们也会得到一样的式子, 只不过此时的 $p$ 和 $y$ 表示向量.
反向传播:
我们计算得到的误差为$L$, 让我们来观察输出层发生了什么:

图4-1
图中黑线部分执行的是正向传播, 对于激活层到输出层, 我们有函数关系:
而对于普通层到激活层, 我们有函数关系:
那么, 此时我们对$L$求关于$z$的偏导数(注意Sigmoid的求导符号):
我们设
根据导数的定义, 有
所以我们可以发现, 如果 $\delta$ 越大, $\Delta L$ 越大, 也就表示误差越大. 因此就可以说 $\delta$ 是 $\Delta L$ 的一种反映,是误差的度量。
继续推导, 我们可以得到以下四个式子(以下公式中的 $a,b,z,\omega,L$ 均先看作向量):
输出层的误差
误差的反向传播(相邻两层误差之间的递推关系):
误差与权重的关系:
误差与偏置的关系:
接下来将详细推导后三个公式:
误差的反向传播:

图4-2
我们首先来看 $z^{i}_1$ 与 $z^{i-1}_j$ 的关系:
所以有:
稍加整理, 我们就得到了:
误差与权重的关系:
先取 $z^{i}_1$ 观察, 有如下式子:
所以有:
稍加整理, 我们就得到了:
注意, 这里的结果是一个矩阵, $\omega_j$ 对应第 $j$ 列.
误差与偏置的关系:
同 4.3.2.2:
这里的 $\delta^i_1$ 是一个向量, 每一行的值都是 $\delta^i_1$.
稍加整理, 我们就得到了:
这里的 $\delta^i$ 是一个矩阵, $b_j$ 对应第 $j$ 列.
这样误差反向传播过程中的基本实现方法已经解释清楚了, 如有纰缪, 欢迎指出讨论.
权重和偏置的更新:
得到了每一层的误差, 我们就需要根据误差来修改本层的权重和偏置, 使得误差越来越小, 在神经网络中最常用的就是梯度下降法.
简单来说, 梯度下降的过程就是不断地沿着目标函数下降(梯度方向), 朝着更小的目标函数值靠拢的过程. 在每一次迭代中, 都会根据函数的当前位置求出该位置的梯度, 并将该梯度乘以一个较小的学习率作为步长, 再以负梯度方向为基础方向进行调整目标函数, 并更新参数, 从而逐渐达到最优解.

图4-3-1

图4-3-2
图片来源于网络
梯度下降的基本形式就是:
其中, $\alpha$ 代表学习率(learning rate), $J$ 是损失函数.
在反向传播神经网络中, 我们对权重与偏置进行梯度下降, 使得损失函数最小. 就有如下的变式:
如何构建一个反向传播神经网络(以c语言为例):
正向传播神经网络的搭建:
反向传播神经网络需要先使用正向传播神经网络模型进行输出值的计算, 再通过误差的反向传播进行权重更新, 从而实现网络学习.
正向传播神经网络的搭建方法这里不再赘述,直接上代码:
/* 权重 */
double Hidden_Weight_1[DIMENSION_SIZE * NUM_HIDDEN_1];
double Hidden_Weight_2[NUM_HIDDEN_1 * NUM_HIDDEN_2];
double Output_Weight[NUM_HIDDEN_2 * NUM_OUTPUT];
/* 偏置 */
double Hidden_Bias_1[DIMENSION_SIZE * NUM_HIDDEN_1];
double Hidden_Bias_2[NUM_HIDDEN_1 * NUM_HIDDEN_2];
double Output_Bias[NUM_HIDDEN_2 * NUM_OUTPUT];
/* weights 是要更改的权重 count 是weight的数组大小 num1 上一层神经元数量 num2 当前层神经元数量 */
void xavier_init(double* weights, int count, int num1, int num2) {
double stddev = sqrt(2.0 / (num1 + num2));
for (int i = 0; i < count; i++) {
weights[i] = stddev * rand() / RAND_MAX;
}
}
double Sigmoid(double z){
return 1/(1+exp(-z));
}
/* 初始化权重和偏置,使用xavier方法 */
void Init_Weight(){
xavier_init(Hidden_Weight_1, DIMENSION_SIZE * NUM_HIDDEN_1, DIMENSION_SIZE, NUM_HIDDEN_1);
xavier_init(Hidden_Weight_2, NUM_HIDDEN_1 * NUM_HIDDEN_2, NUM_HIDDEN_1, NUM_HIDDEN_2);
xavier_init(Output_Weight, NUM_HIDDEN_2 * NUM_OUTPUT, NUM_HIDDEN_2, NUM_OUTPUT);
xavier_init(Hidden_Bias_1, DIMENSION_SIZE * NUM_HIDDEN_1, DIMENSION_SIZE, NUM_HIDDEN_1);
xavier_init(Hidden_Bias_2, NUM_HIDDEN_1 * NUM_HIDDEN_2, NUM_HIDDEN_1, NUM_HIDDEN_2);
xavier_init(Output_Bias, NUM_HIDDEN_2 * NUM_OUTPUT, NUM_HIDDEN_2, NUM_OUTPUT);
}
/* 计算正向传播中隐藏层和输出层的值 */
/* layer 需要计算的数组 num1 本层的大小 num2 输入层的大小 input 输入数据 weight 权重 bias 偏置 */
void Caculate_Forward(double *layer,int num1, double *input, int num2, double *weight, double *bias){
for (int i = 0; i < num1; i++){
double sum = 0.0;
for (int j = 0; j < num2; j++){
sum = sum + input[j] * weight[i * num2 + j] + bias[i * num2 + j];
}
layer[i] = Sigmoid(sum);
}
}
/* 正向传播 */
void Forward(){
double hidden1[NUM_HIDDEN_1] = {0};
double hidden2[NUM_HIDDEN_2] = {0};
double output[NUM_OUTPUT] = {0};
Caculate_Forward(hidden1, NUM_HIDDEN_1, feature, DIMENSION_SIZE,Hidden_Weight_1, Hidden_Bias_1);
Caculate_Forward(hidden2, NUM_HIDDEN_2, hidden1, NUM_HIDDEN_1, Hidden_Weight_2, Hidden_Bias_2);
Caculate_Forward(output, NUM_OUTPUT, hidden2, NUM_HIDDEN_2, Output_Weight , Output_Bias );
}
计算每一神经元误差对于权重与函数的导数:
Sigmoid_derivative的表达式已经在4.3给出.
给出这部分的代码:double Sigmoid_derivative(double z){
return Sigmoid(z) * (1 - Sigmoid(z));
}
/* 计算误差grad相当于error度量 */
/* layer_grad 需要计算的数组 layer num1 本层的大小 num2 输入层的大小 input 输入数据 weight 权重*/
void Caculate_grad(double *layer_grad, double *layer,int num1, double *input, int num2, double *weight){
for (int i = 0; i < num1; i++){
double sum = 0.0;
for (int j = 0; j < num2; j++){
// weight里的值要注意
sum += input[j] * weight[j * num1 + i];
}
layer_grad[i] = sum * Sigmoid_derivative(layer[i]);
}
}
更新每一神经元的权重和偏置:
我们需要提前定义学习率的大小:
/* 更新权重和偏置 */
/* weight 需要更新的权重 bias 需要更新的偏置 num1 本层大小 layer_gard 本层的偏导值 beforelayer 前一层的值 num2 前一层的大小*/
void Updata_Weight(double *weights, double *bias, int num1, double *layer_grad, double *beforelayer, int num2){
for (int i = 0; i < num1; i++){
for (int j = 0; j < num2; j++){
weights[i * num2 + j] = weights[i * num2 + j] - ALPHA * layer_grad[i] * beforelayer[j];
bias[i * num2 + j] = bias[i * num2 + j] - ALPHA * layer_grad[i];
}
}
}
反向传播:
将5.1和5.2与5.3结合, 我们就得到了反向传播函数:
有一点需要注意, 反向传播中需要使用正向传播计算得到的隐藏层值, 所以我将正向传播中定义的隐藏层放在外层先定义.void Backward(){
double hidden1[NUM_HIDDEN_1] = {0};
double hidden2[NUM_HIDDEN_2] = {0};
double output[NUM_OUTPUT] = {0};
Forward();
double hidden1_grad[NUM_HIDDEN_1] = {0};
double hidden2_grad[NUM_HIDDEN_2] = {0};
double output_grad[NUM_OUTPUT] = {0};
Caculate_error(hidden2_grad, hidden2, NUM_HIDDEN_2, output_grad,NUM_OUTPUT, Output_Weight );
Caculate_error(hidden1_grad, hidden1, NUM_HIDDEN_1, hidden2_gradNUM_HIDDEN_2, Hidden_Weight_2);
/* 更新权重和偏置 */
Updata_Weight(Output_Weight, Output_Bias, NUM_OUTPUT, output_grad,hidden2, NUM_HIDDEN_2 );
Updata_Weight(Hidden_Weight_2, Hidden_Bias_2, NUM_HIDDEN_2, hidden2_gradhidden1, NUM_HIDDEN_1 );
Updata_Weight(Hidden_Weight_1, Hidden_Bias_1, NUM_HIDDEN_1, hidden1_gradfeature, DIMENSION_SIZE);
}
多次迭代:
仅仅一次的更新权重肯定无法得到最优解, 所以我们需要多次迭代才能得到最优解:
/* size 为读入数据的大小 */
void Interation(int size){
for (int k = 0; k < NUM_ITERATIONS; k++){
for (int n = 0; n < size; n++){
Backward();
}
}
}
到此, BP神经网络就已经被我们构建完成了.
结语:
神经网络最基础的知识就讲解清楚了,下一篇文章准备写神经网络在多分类问题与多标签问题上的解决方法,等有时间再慢慢理吧.