神经网络基本原理、误差逆传播BP算法公式推导与多层神经网络的Python实现

本文深入介绍了神经网络的基本组件,包括神经元和感知机,探讨了多层前馈网络的结构与误差逆传播算法。针对过拟合问题,提出了早停和正则化策略。通过Python实现了一个多层神经网络,展示了前向传播、损失计算、反向传播和参数更新的过程。最后,以电离层数据集为例,展示了神经网络在实际问题中的应用,包括数据预处理、模型训练和性能评估。
摘要由CSDN通过智能技术生成

一. 神经元

神经网络最广泛的定义:神经网络是具有适应性的简单单元组成的广泛并行互连的网络,它的组织能够模拟生物神经系统对真实世界物体作出的交互反应。

这个“简单单元”就是神经网络中最基本的成分——神经元(neuron),1943年抽象出沿用至今的M-P神经元模型。神经元接受到其他n个神经元传递过来的输入信号,这些信号通过带权重的连接进行传递,神经元将接收的总输入值与阈值进行比较,在经过激活函数处理产生输出。激活函数一般使用sigmoid函数,它具有光滑连续、处处可导的优点,可以将在较大范围内变化的输入值挤压到(0,1)范围内。

二. 感知机

接下来就要研究由基本单元组成的复合状态了。感知机由两层神经元组成:输入层和输出层,但是只有输出层神经元进行激活函数处理(只有一层功能神经元)。

1. 感知机学习规则

给定训练集,权重 w i w_i wi 和阈值 θ \theta θ 可通过学习得到,对样例 ( x , y ) (x,y) (x,y) ,若输出为 y ^ \hat{y} y^ ,则进行如下调整:
w i ← w i + Δ w i w_i\gets w_i+\Delta w_i wiwi+Δwi Δ w i = η ( y − y ^ ) x i \Delta w_i=\eta(y-\hat{y})x_i Δwi=η(yy^)xi 其中, η ∈ ( 0 , 1 ) \eta\in(0,1) η(0,1) 指学习率, ( y − y ^ ) (y-\hat{y}) (yy^)是误差,以误差作为信号。为什么这样一种规则能实现学习的效果呢?因为这种规则能够在一次次的迭代中使得 ( y − y ^ ) (y-\hat{y}) (yy^)减小,分情况具体说明如下:

( 1 ) x i > 0 (1)x_i>0 1xi>0 的情形
     i f    y − y ^ > 0 , \ \ \ \ if\ \ y-\hat{y}>0,     if  yy^>0, Δ w i > 0 ,   w i ↑ ,   y ^ = ∑ w i x i ↑ ,   y − y ^ ↓ , \Delta w_i>0,\ w_i\uparrow,\ \hat{y}=\sum w_ix_i\uparrow,\ y-\hat{y}\downarrow, Δwi>0, wi, y^=wixi, yy^, 即误差减小.
     i f    y − y ^ < 0 , \ \ \ \ if\ \ y-\hat{y}<0,     if  yy^<0, Δ w i < 0 ,   w i ↓ ,   y ^ = ∑ w i x i ↓ ,   y − y ^ ↑ , \Delta w_i<0,\ w_i\downarrow,\ \hat{y}=\sum w_ix_i\downarrow,\ y-\hat{y}\uparrow, Δwi<0, wi, y^=wixi, yy^, 即误差减小.
( 2 ) x i < 0 (2)x_i<0 2xi<0 的情形
     i f    y − y ^ > 0 , \ \ \ \ if\ \ y-\hat{y}>0,     if  yy^>0, Δ w i < 0 ,   w i ↓ ,   y ^ = ∑ w i x i ↑ ,   y − y ^ ↓ , \Delta w_i<0,\ w_i\downarrow,\ \hat{y}=\sum w_ix_i\uparrow,\ y-\hat{y}\downarrow, Δwi<0, wi, y^=wixi, yy^, 即误差减小.
     i f    y − y ^ < 0 , \ \ \ \ if\ \ y-\hat{y}<0,     if  yy^<0, Δ w i > 0 ,   w i ↑ ,   y ^ = ∑ w i x i ↓ ,   y − y ^ ↑ , \Delta w_i>0,\ w_i\uparrow,\ \hat{y}=\sum w_ix_i\downarrow,\ y-\hat{y}\uparrow, Δwi>0, wi, y^=wixi, yy^, 即误差减小.

2. 感知机学习能力

学习能力有限,只能处理线性可分问题(线性可分:可以用线性超平面进行划分),此时感知机学习过程一定会收敛并求得权向量。对于非线性可分问题,则需要使用多层功能神经元,否则学习过程会发生震荡。
.

三. 多层前馈神经网络

这里 “多层” 指不止一层功能神经元,“前馈” 指每层神经元与下一层全互连,且不存在同层、跨层连接,网络拓扑结构上不存在环或回路。

最简单的情况如图5.6(a)所示,一共有三层:输入层、隐层、输出层,也叫单隐层神经网络,但是只有隐层和输出层有激活函数处理。神经网络根据训练数据调整神经元之间的连接权和每个功能神经元的阈值。

至此,总结一下提到的模型与结构:

模型总层数功能神经元层数
神经元一层(单个)一层(因为是有激活函数处理的)
感知机两层一层(输入层没有激活函数处理)
多层网络多层总层数-1(只有输入层没有激活函数处理)

四. 误差逆传播算法

误差逆传播算法(backpropagation BP算法)是迄今最成功的的神经网络算法。现实任务中使用神经网络时,大多使用BP算法进行训练。其过程简单描述为:先将输入实例提供给输入层神经元,然后逐层将信号前传,直到产生输出层的结果;然后计算输出层的误差,再将误差逆向传播至隐层神经元;最后根据隐层神经元的误差来对连接权和阈值进行调整,该迭代过程循环进行,直到达到某些停止条件位置。

接下来是进行数学描述和公式推导,这里的符号都和西瓜书保持一致,我尽量在自己理解的基础上用清晰易懂的方式表达出来。

输入层一共d个结点(d个属性): x 1 , . . . , x i , . . . , x d x_1,...,x_i,...,x_d x1,...,xi,...,xd
隐层一共q个结点: b 1 , . . . , b h , . . . , b q b_1,...,b_h,...,b_q b1,...,bh,...,bq
输出层一共l个结点: y 1 , . . . , y j , . . . , y l y_1,...,y_j,...,y_l y1,...,yj,...,yl
符号含义
D = ( x i , y i ) i = 1 , . . . , k , . . . , m D={(x_i,y_i})\quad i=1,...,k,...,m D=(xi,yi)i=1,...,k,...,m数据集 (一共m个样本)
θ j \theta_j θj j j j个输出结点的阈值
γ h \gamma_h γh h h h个隐层结点的阈值
v i h v_{ih} vih i i i个输入结点和第 h h h个隐层结点之间的权重
w h j w_{hj} whj h h h个隐层结点和第 j j j个输出结点之间的权重
α h = ∑ i = 1 d v i h x i \alpha_h=\sum_{i=1}^dv_{ih}x_i αh=i=1dvihxi h h h个隐层结点接收的输入
b h = f ( α h − γ h ) b_h=f(\alpha_h-\gamma_h) bh=f(αhγh) h h h个隐层结点的输出(经激活函数处理)
β j = ∑ h = 1 q w h j b h \beta_j=\sum_{h=1}^qw_{hj}b_h βj=h=1qwhjbh j j j个输出结点接收的输入
y ^ j k = f ( β j − θ j ) \hat{y}_j^k=f(\beta_j-\theta_j) y^jk=f(βjθj) j j j个输出结点的输出(k指第k个样例)

采用均方误差,第k个样例的损失函数记为 E k = 1 2 ∑ j = 1 l ( y ^ j k − y j k ) 2 E_k=\frac{1}{2}\sum_{j=1}^l(\hat{y}_j^k-y_j^k)^2 Ek=21j=1l(y^jkyjk)2

此时,共有 ( d + l + 1 ) q + l (d+l+1)q+l (d+l+1)q+l 个参数需要确定(输入–隐层d×q个权重,隐层–输出q×l个权重,q+l 个阈值).

从输入起始层层计算得到输出,并计算损失函数值之后,就要进行误差的逆向传播了。采用广义的感知机学习规则对任意参数 v v v 进行更新: v ← v + Δ v v\gets v+\Delta v vv+Δv基于梯度下降策略,以目标的负梯度方向对参数进行调整。因为梯度是函数值增大最快的方向,负梯度就是下降最快的方向,而我们要使损失函数最小。所以,梳理一下,接下来要做的就是求 E E E 对神经网络中每一个 θ , γ , v , w \theta,\gamma,v,w θ,γ,v,w 一共 ( d + l + 1 ) q + l (d+l+1)q+l (d+l+1)q+l 个数分别求偏导,并更新它们的值。

在公式推导之前还要做一个准备,这里选取的激活函数是sigmoid函数,它有一个重要的性质,其对x求导的结果可由其本身来表示,这将方便后面的推导,即 s i g m o i d : f ( x ) = 1 1 + e − x sigmoid:f(x)=\dfrac{1}{1+e^{-x}} sigmoid:f(x)=1+ex1 f ( x ) ′ = f ( x ) ( 1 − f ( x ) ) f(x)^{'}=f(x)(1-f(x)) f(x)=f(x)(1f(x)) 首先计算E对【输出层与隐层之间的权重】及【输出层结点阈值】的导数,利用复合函数链式求导法则:

∂ E ∂ w h j = ∂ E ∂ y ^ j   ∂ y ^ j ∂ β j   ∂ β j ∂ w h j = ( y ^ j − y j ) y ^ j ( 1 − y ^ j ) b h \dfrac{\partial E}{\partial w_{hj}}=\dfrac{\partial E}{\partial \hat{y}_j}\ \dfrac{\partial \hat{y}_j}{\partial\beta_j}\ \dfrac{\partial\beta_j}{\partial w_{hj}}=(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j)b_h whjE=y^jE βjy^j whjβj=(y^jyj)y^j(1y^j)bh

∂ E ∂ θ j = ∂ E ∂ y ^ j   ∂ y ^ j ∂ θ j = ( y ^ j − y j ) y ^ j ( 1 − y ^ j ) ( − 1 ) \dfrac{\partial E}{\partial \theta_j}=\dfrac{\partial E}{\partial \hat{y}_j}\ \dfrac{\partial \hat{y}_j}{\partial\theta_j}=(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j)(-1) θjE=y^jE θjy^j=(y^jyj)y^j(1y^j)(1)

接下来计算E对【隐层与输入层之间的权重】和【隐层结点阈值】的导数,这个要稍微复杂一些,因为还要加上求和号。

∂ E ∂ v i h = ∂ E ∂ b h   ∂ b h ∂ α h   ∂ α h ∂ v i h = b h ( 1 − b h )   x i ∑ j = 1 l ( y ^ j − y j ) y ^ j ( 1 − y ^ j ) w h j \dfrac{\partial E}{\partial v_{ih}}=\dfrac{\partial E}{\partial b_h} \ \dfrac{\partial b_h}{\partial \alpha_h}\ \dfrac{\partial \alpha_h}{\partial v_{ih}}=b_h(1-b_h)\ x_i\sum_{j=1}^l(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j)w_{hj} vihE=bhE αhbh vihαh=bh(1bh) xij=1l(y^jyj)y^j(1y^j)whj

∂ E ∂ γ h = ∂ E ∂ b h   ∂ b h ∂ γ h = − b h ( 1 − b h ) ∑ j = 1 l ( y ^ j − y j ) y ^ j ( 1 − y ^ j ) w h j \dfrac{\partial E}{\partial \gamma_{h}}=\dfrac{\partial E}{\partial b_h} \ \dfrac{\partial b_h}{\partial \gamma_h}=-b_h(1-b_h)\sum_{j=1}^l(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j)w_{hj} γhE=bhE γhbh=bh(1bh)j=1l(y^jyj)y^j(1y^j)whj

w h j w_{hj} whj为例进行参数更新, Δ w h j = − η ∂ E ∂ w h j ,   w h j ← w h j + Δ w h j \Delta w_{hj}=-\eta\dfrac{\partial E}{\partial w_{hj}}, \ w_{hj}\gets w_{hj}+\Delta w_{hj} Δwhj=ηwhjE, whjwhj+Δwhj


以上介绍的是标准BP算法,每次仅针对一个样例更新连接权和阈值,参数更新非常频繁。累计BP 则要最小化训练集D上的累计误差 E = 1 m ∑ k = 1 m E k E=\frac{1}{m}\sum_{k=1}^mE_k E=m1k=1mEk,它在读取整个训练集D一遍后才对参数进行更新。

五. 过拟合问题

只需一个包含足够多神经元的隐层,多层前馈网络就能以任意精度逼近任意复杂度的连续函数,但隐层结点数的选取并没有一个标准的方法,通常通过试错法来决定。由于其强大的表示能力,BP神经网络常常会遭遇过拟合,主要缓解策略有以下两种:

  • 早停(early stopping):将数据集划分为训练集和验证集,验证集用来估计误差,若训练集误差降低而验证集误差升高,则停止训练,并返回具有最小验证集误差的连接权和阈值。
  • 正则化(regularization):其基本思想是在误差目标函数中增加一个用于描述网络复杂度的部分,例如将误差目标函数改写为: E = λ 1 m ∑ k = 1 m E k + ( 1 − λ ) ∑ i w i 2 E=\lambda\dfrac{1}{m}\sum_{k=1}^mE_k+(1-\lambda)\sum_iw_i^2 E=λm1k=1mEk+(1λ)iwi2 其中, λ ∈ ( 0 , 1 ) \lambda\in(0,1) λ(0,1)用于对经验误差和网络复杂度这两项进行折中,对过于复杂的网络进行“惩罚”,使得训练过程偏好比较小的连接权和阈值,使网络输出更加“光滑”,从而缓解过拟合。 λ \lambda λ 通常用交叉验证法进行估计。

六. 全局最小与局部最小

局部极小解:参数空间中某个点,其邻域点的误差函数值均不小于该点的函数值。
全局最小解:参数空间中所有点的误差函数值均不小于该点的函数值。
● 可能有多个局部极小,但只有一个全局最小。
● 全局最小一定是局部极小,局部极小不一定是全局最小。

基于梯度的搜索 是最广泛的参数寻优方法。每次迭代过程中,计算误差函数在当前点的梯度,因为负梯度是函数值下降最快的方向,因此沿负梯度方向搜索最优解。若误差函数在当前点梯度为0,则达到局部极小。若误差函数只有一个局部极小,则该局部极小就是全局最小值,若不止一个局部极小,那就不能保证我们找到的是全局最小。所以要采取一定的策略“跳出”局部极小。

  • 多次初始化:以多组不同的参数值初始化多个神经网络,这相当于从多个不同的初始点开始搜索。
  • 模拟退火(simulated annealing):在每一步都有一定几率接受比当前解更差的结果,从而有助于跳出“局部极小”。为了保持算法稳定,这个几率要随着时间的推移而降低。
  • 随机梯度下降:在计算梯度时加入了随机因素,因此即便先入局部最小点,它计算出的梯度仍可能不为零。

七. Python实现

这里参考了网上可以找到的代码,可以实现多层神经网络,自己指定网络层数和各层结点数,我根据自己的理解添加了一些说明和解释,并对实际运行中出错的部分进行了一些修改。
为了更好的理解代码,需要对代码中使用的符号及其含义进行熟悉,虽然神经网络的整个过程在西瓜书的符号体系里理明白了,但是在启用另一套符号的时候还是会有些乱。这里进行一些说明:中括号内的上标 表示神经网络的层数,如 w [ L ] w^{[L]} w[L] 表示第L层的权重矩阵, A [ L ] A^{[L]} A[L] 表示第L层的输出值。小括号内的上标 表示第几个样本,如 w [ L ] ( i ) w^{[L](i)} w[L](i)表示第 i 个样本第L层的权重矩阵。代码中使用的一些numpy库函数在最后进行了一下总结。

0.激活函数

一般在输出层使用sigmoid,各隐层使用relu。这里简单介绍一下引入relu(线性修正函数)的原因:引入relu函数的原因

第一,采用sigmoid等函数,算激活函数时(指数运算),计算量大,反向传播求误差梯度时,求导涉及除法,计算量相对大,而采用Relu激活函数,整个过程的计算量节省很多。
第二,对于深层网络,sigmoid函数反向传播时,很容易就会出现 梯度消失 的情况(在sigmoid接近饱和区时,变换太缓慢,导数趋于0,这种情况会造成信息丢失),从而无法完成深层网络的训练。
第三,ReLu会使一部分神经元的输出为0,这样就造成了 网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生。

def relu(Z):
    A = np.maximum(0,Z)
    assert(A.shape == Z.shape)
    cache = Z
    return A, cache

# relu的反向求导过程
def relu_backward(dA, cache):
    Z = cache
    dZ = np.array(dA, copy=True)

    dZ[Z <= 0] = 0
    assert (dZ.shape == Z.shape)
    return dZ
    
def sigmoid(Z):
    A = 1/(1+np.exp(-Z))
    cache = Z
    return A, cache

def sigmoid_backward(dA, cache):
    Z = cache
    s = 1/(1+np.exp(-Z))
    dZ = np.multiply(np.multiply(dA , s) , (1-s))
    assert (dZ.shape == Z.shape)
    return dZ

1.初始化

输入:列表 layer_dims,用来指定各层的结点数
输出:输出各层参数,以字典形式储存。

首先,考虑参数矩阵的形状。 输入层的结点数应为属性的个数,且无需进行参数初始化,每一层的权值矩阵行数为本层结点数,列数为前一层结点数;偏置矩阵行数为本层结点数,由于矩阵运算的广播功能而只需一列即可。因此,对于第L层,其参数矩阵形状如下: w [ L ] : n [ L ] × n [ L − 1 ] w^{[L]}:n^{[L]} ×n^{[L-1]} w[L]:n[L]×n[L1] b [ L ] : n [ L ] × 1 b^{[L]}:n^{[L]} ×1 b[L]:n[L]×1

然后,考虑矩阵内的数值。 对于参数w,用 np.random.randn()生成一个高斯分布的数,由sigmoid函数图像可知, z = wx + b 很大或很小时,曲线的斜率很小,这就会导致梯度下降的速度非常慢,不利于快速达到局部最优值,而当 w 绝对值很大时,z 绝对值也会很大,所以 w 的绝对值应该尽量小,以保证在初期有较快的梯度下降速度。因此,需要乘上一个很小的数比如 0.01,以限制它的范围。

对于参数 b,它的初始化对神经网络没有较大的影响,因此可以直接设置为0,采用 np.zeros()方法生成特定形状的全零矩阵。

def initialize_parameters_deep(layer_dims):
    #np.random.seed(3)       # layer_dims列表类型,存放每一层的结点数,包括输入层
    parameters = {}          # 用字典存储参数W和b
    L = len(layer_dims) - 1  # 神经网络的层数,不考虑输入层,例如双隐层 L=3

    # 初始化第1到第L层结点的参数,第0层为输入层X
    for l in range(1, L + 1):                                                                 # 假设 40 10 5 1
        # 第l层的权值矩阵的形状为 layer_dims[l] * layer_dims[l - 1]
        # 举例而言w1就是输入层(第0层)和第一层之间的权重,b1是第一层的偏置,因此循环是从1开始
        # randn函数返回一个或一组样本,具有标准正态分布,返回值为指定维度的array
        parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l - 1]) * 0.01   
        parameters['b' + str(l)] = np.zeros((layer_dims[l], 1))                               
		# assert是对表达式布尔值的判断,要求表达式计算值必须为真,这里用来确认矩阵的形状是正确的
        assert (parameters['W' + str(l)].shape == (layer_dims[l], layer_dims[l - 1]))
        assert (parameters['b' + str(l)].shape == (layer_dims[l], 1))
    return parameters

2.前向传播

输入:数据集 X 和各层参数,
输出:前向传播过程最终的预测值 AL,并将相关参数放入缓存 caches 中以备后续
使用,其中 cashes 是一个长度等于网络层数的列表,列表中的每个元素存放着每
一层的 A [ L − 1 ] , w [ L ] , b [ L ] , z [ L ] A^{[L-1]},w^{[L]},b^{[L]},z^{[L]} A[L1],w[L],b[L],z[L].

每一层的前向传播过程都可概括为两个步骤:
(1)计算 z [ L ] = w [ L ] A [ L − 1 ] + b [ L ] z^{[L]}=w^{[L]}A^{[L-1]}+b^{[L]} z[L]=w[L]A[L1]+b[L],即 linear_forward()
(2)代入激活函数中, A [ L ] = f ( z [ L ] ) A^{[L]}=f(z^{[L]}) A[L]=f(z[L]),即 linear_activation_forward()

在执行这两个步骤时,需要将 A [ L − 1 ] , w [ L ] , b [ L ] , z [ L ] A^{[L-1]},w^{[L]},b^{[L]},z^{[L]} A[L1],w[L],b[L],z[L]存起来,以待反向传播时使用。

对于第 1~L-1 层,使用 relu()函数作为激活函数,运算简单计算速度快,可以防止梯度消失,sigmoid 的导数只有在 0 附近的时候有比较好的激活性,在正负饱和区的梯度都接近于 0。但是对于第 L 层,则使用 sigmoid()函数,sigmoid()函数的输出值是[0,1],可以作为概率。

def L_model_forward(X, parameters):
    caches = []
    A = X
    L = len(parameters) // 2    # W和b是成对出现的,所以层数是参数的组数除以2
    for l in range(1, L):
        A, cache = linear_activation_forward(A_prev=A, W=parameters["W" + str(l)], b=parameters["b" + str(l)],
                                             activation="relu")
        caches.append(cache)
    AL, cache = linear_activation_forward(A_prev=A, W=parameters["W" + str(L)], b=parameters["b" + str(L)],
                                          activation="sigmoid")
    caches.append(cache)
    assert (AL.shape == (1, X.shape[1]))
    return AL, caches   # AL是最后的预测值,caches存了每一层的z和a w b
    
def linear_forward(A, W, b):   # 前向 线性 传播 即计算z
    Z = np.dot(W, A) + b
    assert (Z.shape == (W.shape[0], A.shape[1]))
    cache = (A, W, b)
    return Z, cache
    
def linear_activation_forward(A_prev, W, b, activation): # 计算 a
    if activation == "sigmoid":
        Z, linear_cache = linear_forward(A_prev, W, b)
        A, activation_cache = sigmoid(Z)
    elif activation == "relu":
        Z, linear_cache = linear_forward(A_prev, W, b)
        A, activation_cache = relu(Z)
    else:
        A=[]
        linear_cache=[]
        activation_cache=[]
    assert (A.shape == (W.shape[0], A_prev.shape[1]))
    cache = (linear_cache, activation_cache)
    return A, cache

更直观地展示一下这里的过程,假设现在是两个隐层的网络,则前向传播过程最终得到的是: A L = A [ 3 ] = s i g m o i d ( W [ 3 ] A [ 2 ] + b [ 3 ] ) AL=A^{[3]}=sigmoid(W^{[3]}A^{[2]}+b^{[3]}) AL=A[3]=sigmoid(W[3]A[2]+b[3]) c a c h e s = [ ( ( X , W [ 1 ] , b [ 1 ] ) , Z [ 1 ] ) , ( ( A [ 1 ] , W [ 2 ] , b [ 2 ] ) , Z [ 2 ] ) , ( ( A [ 2 ] , W [ 3 ] , b [ 3 ] ) , Z [ 3 ] ) ] caches=\Bigg[\Big((X,W^{[1]},b^{[1]}),Z^{[1]}\Big),\\\quad\quad\quad\quad\quad\Big((A^{[1]},W^{[2]},b^{[2]}),Z^{[2]}\Big),\\ \quad\quad\quad\quad\quad\Big((A^{[2]},W^{[3]},b^{[3]}),Z^{[3]}\Big) \Bigg] caches=[((X,W[1],b[1]),Z[1]),((A[1],W[2],b[2]),Z[2]),((A[2],W[3],b[3]),Z[3])]cashes是个列表,列表的每个元素都是一个元组,每个元组里有两个元素,一个是元组,另一个是单个的数值。

3.计算损失函数

损失函数使用交叉熵,交叉熵在一般情况下更容易收敛到一个更好的解。此处计算数据集中所有样本的交叉熵,再求其平均值,反映数据集整体的特征,目标是数据集总体的损失函数值最小。其计算公式为: l o s s   f u n c t i o n = − 1 m ∑ i = 1 m ( y ( i ) l o g ( a [ L ] ( i ) ) + ( 1 − y ( i ) ) l o g ( 1 − a [ L ] ( i ) ) loss\ function=-\dfrac{1}{m}\sum_{i=1}^m(y^{(i)}log(a^{[L](i)})+(1-y^{(i)})log(1-a^{[L](i)}) loss function=m1i=1m(y(i)log(a[L](i))+(1y(i))log(1a[L](i))此处 a [ L ] a^{[L]} a[L] 就是最终的预测值,也即前文公式推导中的 y ^ \hat{y} y^ ,也即代码中的变量AL。

这里说明一下交叉熵如何度量预测误差:只考虑第 i 个样本,交叉熵尽可能小,意味着求和号后面的部分要尽可能大,分别看真实标签为0和1的情况

(1)若 y ( i ) = 0 y^{(i)}=0 y(i)=0, 则式子化简为 l o g ( 1 − a [ L ] ( i ) ) , a [ L ] ( i ) log(1-a^{[L](i)}),a^{[L](i)} log(1a[L](i))a[L](i) 越小,则该式越大,加个负号后就越小,而 a [ L ] ( i ) a^{[L](i)} a[L](i)的取值由于sigmoid的作用而被限制在0~1,因此 a [ L ] ( i ) a^{[L](i)} a[L](i) 越接近真实标签 0,交叉熵的值就越小。

(2)若 y ( i ) = 1 y^{(i)}=1 y(i)=1, 则式子化简为 l o g ( a [ L ] ( i ) ) , a [ L ] ( i ) log(a^{[L](i)}),a^{[L](i)} log(a[L](i))a[L](i) 越大,则该式越大,加个负号后就越小,此时 a [ L ] ( i ) a^{[L](i)} a[L](i) 越接近真实标签 1,交叉熵的值就越小。

def compute_cost(AL, Y):
    m = Y.shape[1]     # m即数据集中的训练样本数
    cost = -1 / m * np.sum(np.multiply(Y , np.log(AL)) + np.multiply((1 - Y) , np.log(1 - AL)))
    cost = np.squeeze(cost)
    assert (cost.shape == ())
    return cost
    '''注意区别np.dot和np.multiple~'''

4.反向传播

输入:最终预测值 AL, caches,真实值 Y
输出: dA ,dW ,db ,并以字典形式存储。

反向传播的完整过程在 L_model_backward() 中,但需要用到def linear_backward() 以及 def linear_activation_backward(),这是将需要频繁进行的过程另外写一个函数,提高代码的可读性。

因为这里用的交叉熵损失函数,因此再写一下数学计算过程: ∂ L ∂ y ^ ( i ) = − y ( i ) y ^ ( i ) + 1 − y ( i ) 1 − y ^ ( i ) \dfrac{\partial L}{\partial \hat{y}^{(i)}}=-\dfrac{y^{(i)}}{ \hat{y}^{(i)}}+\dfrac{1-y^{(i)}}{1- \hat{y}^{(i)}} y^(i)L=y^(i)y(i)+1y^(i)1y(i)这里上标 i 即第 i 个训练样本,在python实现时进行了向量化处理,若有500个样例,在输出结点个数为1时,y就是一个1*500的矩阵。

def L_model_backward(AL, Y, caches):
    grads = {}       # 存放个参数梯度的字典
    L = len(caches)  # 神经网络层数
    m = AL.shape[1]  
    Y = Y.reshape(AL.shape)

    dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))
    current_cache = caches[L - 1]
    grads["dA" + str(L - 1)], grads["dW" + str(L)], grads["db" + str(L)] = linear_activation_backward(dAL,current_cache,activation='sigmoid')   
                                                                      
	for l in reversed(range(1,L)):
        current_cache = caches[l - 1]  # caches的下标从0开始,而网络层从1开始,所以第L层的缓存对应caches[l-1]
        dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads["dA" + str(l)], current_cache, "relu")
        grads["dA" + str(l - 1)] = dA_prev_temp
        grads["dW" + str(l)] = dW_temp
        grads["db" + str(l)] = db_temp
    return grads

def linear_backward(dZ, cache):
    A_prev, W, b = cache
    m = A_prev.shape[1]

    dW = np.dot(dZ, A_prev.T) / m
    db = np.sum(dZ, axis=1) / m
    db=np.mat(db)

    db=db.reshape(db.shape[1],1)
    dA_prev = np.dot(W.T, dZ)

    assert (dA_prev.shape == A_prev.shape)
    assert (dW.shape == W.shape)
    assert (db.shape == b.shape)
    return dA_prev, dW, db

def linear_activation_backward(dA, cache, activation):
    linear_cache, activation_cache = cache

    if activation == "relu":
        dZ = relu_backward(dA, activation_cache)  # dA乘上dA对Z的导数,得到dZ
        dA_prev, dW, db = linear_backward(dZ=dZ, cache=linear_cache)
        return dA_prev, dW, db
    elif activation == "sigmoid":
        dZ = sigmoid_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ=dZ, cache=linear_cache)
        return dA_prev, dW, db

5.更新参数

输入:目前的参数,反向传播过程求得的梯度和学习率
输出:更新后的参数

通过for循环对每一层的参数进行更新: W [ L ] = W [ L ] − α d W [ L ] W^{[L]}=W^{[L]}-\alpha dW^{[L]} W[L]=W[L]αdW[L] b [ L ] = b [ L ] − α d b [ L ] b^{[L]}=b^{[L]}-\alpha db^{[L]} b[L]=b[L]αdb[L]

def update_parameters(parameters, grads, learning_rate):
    L = len(parameters) // 2  # 神经网络的层数
    for l in range(L):
        parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * grads["dW" + str(l + 1)]
        parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * grads["db" + str(l + 1)]
    return parameters

核心代码到这里就结束了,而在具体的数据集中,通常还要根据数据的特点编写预处理、划分数据集的相关代码,利用以上函数搭建神经网络,设置学习率、隐层个数、隐层结点数、迭代次数,训练结束后进行预测,并对模型性能进行评估。

八. 应用实践

我选择了UCI的电离层数据集进行神经网络的实际应用。这是一个经典数据集,数据集中有 34 个自变量,且均为连续值属性, 1 个因变量代表电离层的结构类型:g代表“好”,b代表“坏”,总共有 351 个观测值。我将因变量值“g”和“b”转换为1和0,并另存为csv文件,训练神经网络进行二分类。

首先,读取数据,并初步了解数据集的情况。 通过以下语句的输出结果,发现该数据集已经进行了数据归一化,每一个属性的值都在0-1之间,但第二列的数值全为0,没有信息价值,故后续将其删去。且这是一个类别不平衡的数据集,正例有225个,反例有126个。

data = pd.read_csv(r'C:\Users\Lenovo\Desktop\数据集\电离层.csv',header=None) # 不需要表头
print(data)                        # 输出数据
print(data.describe().transpose()) # 输出描述信息,有无缺失值
print(data[34].value_counts())     # 输出两个类别的样本数(正负例)正负例极其不平衡
'''
data是dataframe类型的数据;
header=None是说csv文件的第一行就是数据了而不是列标题;
数据一共35列,而标号是从0开始的,故data[34]是最后一列,表示类别标签
但是后面我删了一列,所以一共就只有34列了
'''

第二步,划分训练集和测试集。

# 调用sklearn库可以很方便的实现数据划分
from sklearn.model_selection import train_test_split
X = data.drop(columns=33)
Y = data[33]
x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# 不使用第三方库
x_train=[]
x_test=[]
y_train=[]
y_test=[]

series=random.sample(range(0,351),X.shape[0]//5) # 生成一组随机数,大小为数据个数的20%,数据集对应标号的数据为测试集

for i in range(0,351):
    if i in series:
        x_test.append(X.loc[i].tolist())
        y_test.append(int(Y.loc[i]))
    else:
        x_train.append(X.loc[i].tolist())
        y_train.append(int(Y.loc[i]))
# 都转换为矩阵类型,方便后续处理
# 每一列是一个样例的各个属性值,每一行是一个属性在不同样例中的所有取值,和一般常用的表示不太一样
x_train=np.mat(x_train).T
x_test=np.mat(x_test).T
y_train=np.mat(y_train)
y_test=np.mat(y_test)

第三步,搭建神经网络进行训练。

def Nueral_Network(x_train,x_test,y_train,y_test,learning_rate=0.1,iterations=2000):
    n_input = x_train.shape[0]     # 输入层结点个数
    layer_dims = [n_input,5,5,1]   # 存放各层结点数的列表
    parameters = initialize_parameters_deep(layer_dims)
    
    # 存放数据用于后续绘图直观展示准确率变化情况
    times=[] # 训练次数
    a=[] #训练准确率
    b=[] #测试准确率
    c=[] #代价函数

    for i in range(0,iterations):
        AL,caches = L_model_forward(x_train,parameters)
        cost = compute_cost(AL,y_train)
        grads = L_model_backward(AL,y_train,caches)
        parameters = update_parameters(parameters,grads,learning_rate)
        if i % 50==0:
            train_result,train_acc=predict(x_train,y_train,parameters)
            test_result,test_acc=predict(x_test,y_test,parameters)
            print("迭代第%i次,代价函数为:%f,训练准确率:%.6f,测试准确率:%.6f"%(i,cost,train_acc,test_acc))

            times.append(i)
            a.append(train_acc)
            b.append(test_acc)
            c.append(cost)
            
    return  times,a,b,c

plt.xlabel("iterations")
plt.ylabel("loss")
plt.plot(times,c)
plt.savefig('C:\\Users\\Lenovo\\Desktop\\数据集\\loss.png')
plt.clf()

plt.xlabel("iterations")
plt.ylabel("accuracy")
plt.plot(times, a, label='train')
plt.plot(times, b, label='test')
plt.legend() # 显示图例
plt.savefig('C:\\Users\\Lenovo\\Desktop\\数据集\\accuracy.png')
plt.clf()

在上面的代码块中用到predict函数,该函数只进行前向过程,用给定的参数计算最终预测值,并和真实值比对,返回预测结果和准确率。

def predict(x,y,parameters):
    AL, caches = L_model_forward(x, parameters)
    predict_result = []
    for i in range(0,AL.shape[1]):
        if AL[0,i]>=0.5:
            predict_result.append(1)
        else:
            predict_result.append(0)
    count = 0
    for i in range(0, len(predict_result)):
        if y[0, i] == predict_result[i]:
            count = count + 1
    acc = count / len(predict_result)
    return predict_result,acc

设置学习率0.1,迭代3000次,两个隐层的结点数都为5,最终运行结果如下图所示:
迭代第0次,代价函数为:0.692724,训练准确率:0.704626,测试准确率:0.714286
迭代第50次,代价函数为:0.656887,训练准确率:0.633452,测试准确率:0.671429
……
迭代第1000次,代价函数为:0.046090,训练准确率:0.985765,测试准确率:0.928571
……
迭代第2950次,代价函数为:0.011886,训练准确率:0.996441,测试准确率:0.957143

九. 代码所涉及的numpy函数总结

这不是本篇的重点,因此写得很简略,就当加深个印象,大概知道有哪些功能~

(1) np.exp(x)         e的x次方
(2) np.dot(x,y)       矩阵乘法(点乘)
(3) np.multiply(x,y)  两个同形状的矩阵对应位置相乘
(4) np.divide(x,y)    两个同形状的矩阵对应位置相除
(5) np.mat(x)         将x转换为矩阵类型
(6) np.max(x)         找出矩阵中最大的数值
(7) np.maximum(x,y)   逐元素比较两个矩阵中对应位置的大小 并取大的值 最终返回一个新的矩阵  
(8) np.zeros()        返回给定形状的数组用0填充
(9) np.random.randn() 返回给定形状的数组 填充值满足标准正态
(10)np.sum()          求所有元素和或行/列和
(11)np.squeeze()      从数组的形状中合并单维度条目,即把shape中为1的维度去掉
  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值