1. 简介

给定一组神经元,所以说我们可以通过以神经元节点来构建一个神经网络,不同的神经网络有着不同的网络连接拓扑结构。一种比较直接的拓扑结构是前馈神经网络(Feedforward Neural Network,FNN)。最简单的前馈神经网络是三层的BP神经网络,在前馈神经网络中,各个神经元分别属于不同的层。每一层神经元接受上一层的神经元的信号,并产生信号输出到下一层神经网络中。前馈神经网络包含有输入层、中间隐藏层、输出层。前馈神经网络也叫做多层感知层(Multilayer Perceptron,MLP)。
前馈神经网络的网络结构如下所示:
多层前馈神经网络图片

2. 推导方法

2.1 符号定义与表示

在推导公式之前,我们约定以下的表示符号:

  • L L L:表示神经网络的层数;
  • m ( l ) m^{(l)} m(l):表示第 l l l层神经元的个数;
  • f l ( ⋅ ) f_{l}(\cdot) fl():表示第 l l l层神经元的激活函数;
  • W ( l ) ∈ R m ( l ) × m ( l − 1 ) W^{(l)}\in \mathbb{R}^{m^{(l)}\times m^{(l-1)}} W(l)Rm(l)×m(l1):表示第 l − 1 l-1 l1层到 l l l层的权重矩阵;
  • b ( l ) ∈ R m ( l ) b^{(l)}\in \mathbb{R}^{m^{(l)}} b(l)Rm(l):表示第 l − 1 l-1 l1层到 l l l层的偏置;
  • z ( l ) ∈ R m ( l ) z^{(l)}\in \mathbb{R}^{m^{(l)}} z(l)Rm(l):表示第 l l l层神经元的净输入(净活性值);
  • a ( l ) ∈ R m ( l ) a^{(l)}\in \mathbb{R}^{m^{(l)}} a(l)Rm(l):表示第 l l l层神经元的输出(活性值)。
  • x x x:输入样本向量
  • y ^ \hat y y^:样本向量标签
  • y y y:输出向量

2.2 前向传播过程(forward)

前馈神经网络中每一层的传播过程通过以下公式进行传播:
z ( l ) = W ( l ) ⋅ a ( l − 1 ) + b ( l ) z^{(l)}=W^{(l)}\cdot a^{(l-1)}+b^{(l)} z(l)=W(l)a(l1)+b(l)

a ( l ) = f l ( z ( l ) ) a^{(l)}=f_{l}(z^{(l)}) a(l)=fl(z(l))

上面的两个公式可以合并为
z ( l ) = W ( l ) ⋅ f l − 1 ( z ( l − 1 ) ) + b ( l ) z^{(l)}=W^{(l)}\cdot f_{l-1}(z^{(l-1)})+b^{(l)} z(l)=W(l)fl1(z(l1))+b(l)

或者是
a ( l ) = f l ( W ( l ) ⋅ a ( l − 1 ) + b ( l ) ) a^{(l)}=f_{l}(W^{(l)}\cdot a^{(l-1)}+b^{(l)}) a(l)=fl(W(l)a(l1)+b(l))

所以前馈神经网络通过递推公式逐层传递信息,从而得到最后的网络输出 z ( L ) z^{(L)} z(L)。所以说整个网络可以看作一个符合复合函数 ϕ ( x ; W , b ) \phi(\pmb x;\pmb W,\pmb b) ϕ(xxx;WWW,bbb),其中 a ( 0 ) = x a^{(0)}=\pmb x a(0)=xxx,第 L L L层的输出 z ( L ) = y z^{(L)}=\pmb y z(L)=yyy。其中 W \pmb W WWW b \pmb b bbb分别是网络中的权重矩阵值和偏置矩阵。

2.3 参数学习过程

在深度学习中,模型目标构建的问题一般分为两种问题,即分类学习过程和回归学习过程,其中损失函数在深度学习的过程中有着重要的地位。一般地,在分类学习过程中常常使用交叉熵损失函数,回归学习中常常使用均方差函数损失函数。
训练集 D = { ( x ( n ) , y ^ ( n ) ) } n = 1 N D=\{(x^{(n)},\hat y^{(n)})\}_{n=1}^{N} D={(x(n),y^(n))}n=1N,将样本值 x ( n ) x^{(n)} x(n)输入给前馈神经网络,得到的网络输出为 y ( n ) y^{(n)} y(n),其中在数据集 D D D上的结构化风险函数为:
R ( W , b ) = 1 N ∑ n = 1 N L ( y ( n ) , y ^ ( n ) ) + λ 2 ∣ ∣ W ∣ ∣ F 2 R(W,b)=\frac{1}{N}\sum\limits_{n=1}^{N}L(y^{(n)},\hat y^{(n)})+\frac{\lambda}{2}||W||_{F}^{2} R(W,b)=N1n=1NL(y(n),y^(n))+2λWF2

其中 W W W b b b分别表示网络中所有的权重矩阵和偏置向量;超参数 λ > 0 \lambda>0 λ>0 ∣ ∣ W ∣ ∣ F 2 ||W||_{F}^{2} WF2用来防止过拟合问题。这里的 ∣ ∣ W ∣ ∣ F 2 ||W||_{F}^{2} WF2一般使用 Frobenius \text{Frobenius} Frobenius范数:
∣ ∣ W ∣ ∣ F 2 = ∑ l = 1 L ∑ i = 1 m ( l ) ∑ j = 1 m ( l − 1 ) ( w i j ( l ) ) 2 ||W||_{F}^{2}=\sum\limits_{l=1}^{L}\sum_{i=1}^{m^{(l)}}\sum_{j=1}^{m^{(l-1)}}(w_{ij}^{(l)})^{2} WF2=l=1Li=1m(l)j=1m(l1)(wij(l))2

有了学习准则和训练样本,网络参数可以通过使用梯度下降的算法来进行学习。其中在梯度下降方法的每次迭代过程中,第 l l l层的参数 W ( l ) W^{(l)} W(l) b ( l ) b^{(l)} b(l)参数更新方式为
W ( l ) = W ( l ) − α ∂ R ( W , b ) ∂ W ( l ) = W ( l ) − α ( 1 N ∑ n = 1 N ∂ L ( y ( n ) , y ^ ( n ) ) ∂ W ( l ) + λ W ( l ) ) W^{(l)}=W^{(l)}-\alpha\frac{\partial R(W,b)}{\partial W^{(l)}}\\ =W^{(l)}-\alpha(\frac{1}{N}\sum\limits_{n=1}^{N}\frac{\partial L(y^{(n)},\hat y^{(n)})}{\partial W^{(l)}}+\lambda W^{(l)}) W(l)=W(l)αW(l)R(W,b)=W(l)α(N1n=1NW(l)L(y(n),y^(n))+λW(l))

b ( l ) = b ( l ) − α ∂ R ( W , b ) ∂ b ( l ) = b ( l ) − α ( 1 N ∑ n = 1 N ∂ L ( y ( n ) , y ^ ( n ) ) ∂ b ( l ) ) b^{(l)}=b^{(l)}-\alpha\frac{\partial R(W,b)}{\partial b^{(l)}} \\=b^{(l)}-\alpha(\frac{1}{N}\sum\limits_{n=1}^{N}\frac{\partial L(y^{(n)},\hat y^{(n)})}{\partial b^{(l)}}) b(l)=b(l)αb(l)R(W,b)=b(l)α(N1n=1Nb(l)L(y(n),y^(n)))

交叉熵损失函数
对于样本 ( x , y ) (x,y) (x,y),其交叉熵损失函数为
L ( y , y ^ ) = − y ^ T log y L(y,\hat y)=-\hat y^{T}\text{log}y L(y,y^)=y^Tlogy

其中 y ^ ∈ { 0 , 1 } \hat y\in \{0,1\} y^{0,1}为对应标签的 one-hot \text{one-hot} one-hot向量表示。
均方差损失函数
L ( y , y ^ ) = ( y − y ^ ) T ( y − y ^ ) L(y,\hat y)=(y-\hat y)^{T}(y-\hat y) L(y,y^)=(yy^)T(yy^)

其中 y ^ \hat y y^为函数的标签向量。
梯度下降算法需要计算损失函数对参数的偏导数,如果通过链式法则逐一对每个参数进行求导比较低效。神经网络训练的后向传播过程中经常使用反向传播算法来高效地计算梯度。

2.4 后向传播过程(backward)

假设采用随机梯度下降进行神经网络参数学习,现在给定一个样本 ( x , y ^ ) (x,\hat y) (x,y^),将其输入到神经网络模型中,得到的网络输出为 y y y。假设损失函数为 L ( y , y ^ ) L(y,\hat y) L(y,y^),要进行参数学习就必须计算损失函数中关于每一个参数的导数。
一般地,现在对于第 l l l层中的参数 W ( l ) W^{(l)} W(l) b ( l ) b^{(l)} b(l)计算参数。由于 ∂ L ( y , y ^ ) ∂ W ( l ) \frac{\partial L(y,\hat y)}{\partial W^{(l)}} W(l)L(y,y^)的计算涉及向量对矩阵的微分,十分繁琐,故而我们求导数 ∂ L ( y , y ^ ) ∂ W i j ( l ) \frac{\partial L(y,\hat y)}{\partial W_{ij}^{(l)}} Wij(l)L(y,y^)以及导数 ∂ L ( y , y ^ ) ∂ b i l \frac{\partial L(y,\hat y)}{\partial b_{i}^{l}} bilL(y,y^)。根据链式法则,则有以下的求导公式
∂ L ( y , y ^ ) ∂ W i j ( l ) = ∂ z ( l ) ∂ W i j ( l ) ∂ L ( y , y ^ ) ∂ z ( l ) \frac{\partial L(y,\hat y)}{\partial W_{ij}^{(l)}}=\frac{\partial z^{(l)}}{\partial W_{ij}^{(l)}}\frac{\partial L(y,\hat y)}{\partial z^{(l)}} Wij(l)L(y,y^)=Wij(l)z(l)z(l)L(y,y^)

∂ L ( y , y ^ ) ∂ b i ( l ) = ∂ z ( l ) ∂ b i ( l ) ∂ L ( y , y ^ ) ∂ z ( l ) \frac{\partial L(y,\hat y)}{\partial b_{i}^{(l)}}=\frac{\partial z^{(l)}}{\partial b_{i}^{(l)}}\frac{\partial L(y,\hat y)}{\partial z^{(l)}} bi(l)L(y,y^)=bi(l)z(l)z(l)L(y,y^)

根据链式求导规则,我们只需要计算三个偏导数: ∂ z ( l ) ∂ W i j ( l ) \frac{\partial z^{(l)}}{\partial W_{ij}^{(l)}} Wij(l)z(l) ∂ z ( l ) ∂ b ( l ) \frac{\partial z^{(l)}}{\partial b^{(l)}} b(l)z(l) ∂ L ( y , y ^ ) ∂ z ( l ) \frac{\partial L(y,\hat y)}{\partial z^{(l)}} z(l)L(y,y^),即可以求出这些导数,下面来进行计算这三个偏导数。

  • 计算偏导数 ∂ z ( l ) ∂ W i j ( l ) \frac{\partial z^{(l)}}{\partial W_{ij}^{(l)}} Wij(l)z(l)
    一般地,对于第 l l l层有
    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)
    那么偏导数即为
    ∂ z ( l ) ∂ W i j ( l ) = [ ∂ z 1 ( l ) ∂ W i j ( l ) , . . . , ∂ z i ( l ) ∂ W i j ( l ) , . . . , ∂ z m ( l ) ( l ) ∂ W i j ( l ) ] = [ 0 , . . . , ∂ ( w i ( l ) a ( l − 1 ) + b i ( l ) ) ∂ W i j ( l ) , . . . , 0 ] = [ 0 , . . . , a j ( l − 1 ) , . . . , 0 ] = I i ( a j ( l − 1 ) ) ∈ R m ( l ) \frac{\partial z^{(l)}}{\partial W_{ij}^{(l)}}=[\frac{\partial z_{1}^{(l)}}{\partial W_{ij}^{(l)}},...,\frac{\partial z_{i}^{(l)}}{\partial W_{ij}^{(l)}},...,\frac{\partial z_{m^{(l)}}^{(l)}}{\partial W_{ij}^{(l)}}]\\ =[0,...,\frac{\partial (w_{i}^{(l)}a^{(l-1)}+b_{i}^{(l)})}{\partial W_{ij}^{(l)}},...,0]\\ =[0,...,a_{j}^{(l-1)},...,0]\\ =\mathbb{I}_{i}(a_{j}^{(l-1)})\in \mathbb{R}^{m^{(l)}} Wij(l)z(l)=[Wij(l)z1(l),...,Wij(l)zi(l),...,Wij(l)zm(l)(l)]=[0,...,Wij(l)(wi(l)a(l1)+bi(l)),...,0]=[0,...,aj(l1),...,0]=Ii(aj(l1))Rm(l)

其中 w i ( l ) w_{i}^{(l)} wi(l)为权重矩阵 W ( l ) W^{(l)} W(l)的第 i i i行, I i ( a j ( l − 1 ) ) \mathbb{I}_{i}(a_{j}^{(l-1)}) Ii(aj(l1))表示第 i i i个元素为 a j ( l − 1 ) a_{j}^{(l-1)} aj(l1),其余为 0 0 0的行向量。

  • 计算偏导数 ∂ z ( l ) ∂ b i ( l ) \frac{\partial z^{(l)}}{\partial b_{i}^{(l)}} bi(l)z(l)
    由于 z ( l ) z^{(l)} z(l) b ( l ) b^{(l)} b(l)的函数关系为
    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)
    因此偏导数为
    ∂ z ( l ) ∂ b ( l ) = I m ( l ) ∈ R m ( l ) × m ( l ) \frac{\partial z^{(l)}}{\partial b^{(l)}}=I_{m^{(l)}}\in \mathbb{R}^{m^{(l)}\times m^{(l)}} b(l)z(l)=Im(l)Rm(l)×m(l)
    其中 I m ( l ) I_{m^{(l)}} Im(l) m ( l ) × m ( l ) m^{(l)}\times m^{(l)} m(l)×m(l)的单位矩阵。
  • 计算偏导数 ∂ L ( y , y ^ ) ∂ z ( l ) \frac{\partial L(y,\hat y)}{\partial z^{(l)}} z(l)L(y,y^)
    这个偏导数表示第 l l l层神经元对最终损失的影响,也反映了最终损失对第 l l l层神经元的敏感程度,故而一般称为第 l l l层神经元的误差项,用 δ ( l ) \delta^{(l)} δ(l)来表示:
    δ ( l ) = ∂ L ( y , y ^ ) ∂ z ( l ) ∈ R m ( l ) \delta^{(l)}=\frac{\partial L(y,\hat y)}{\partial z^{(l)}}\in \mathbb{R}^{m^{(l)}} δ(l)=z(l)L(y,y^)Rm(l)
    由于 z ( l + 1 ) = W ( l + 1 ) a ( l ) + b ( l + 1 ) z^{(l+1)}=W^{(l+1)}a^{(l)}+b^{(l+1)} z(l+1)=W(l+1)a(l)+b(l+1),则有
    ∂ z ( l + 1 ) ∂ a ( l ) = ( W ( l + 1 ) ) T \frac{\partial z^{(l+1)}}{\partial a^{(l)}}=(W^{(l+1)})^{T} a(l)z(l+1)=(W(l+1))T
    a ( l ) = f l ( z ( l ) ) a^{(l)}=f_{l}(z^{(l)}) a(l)=fl(z(l)),所以有
    ∂ a ( l ) ∂ z ( l ) = ∂ f l ( z ( l ) ) ∂ z ( l ) = f l ′ ( z ( l ) ) \frac{\partial a^{(l)}}{\partial z^{(l)}}=\frac{\partial f_{l}(z^{(l)})}{\partial z^{(l)}}=f_{l}^{'}(z^{(l)}) z(l)a(l)=z(l)fl(z(l))=fl(z(l))
    根据链式法则,第 l l l层的误差项为
    δ ( l ) = ∂ L ( y , y ^ ) ∂ z ( l ) = ∂ a ( l ) ∂ z ( l ) ⋅ ∂ z ( l + 1 ) ∂ a ( l ) ⋅ ∂ L ( y , y ^ ) ∂ z ( l + 1 ) = diag ( f l ′ ( z ( l ) ) ) ⊙ ( ( W ( l + 1 ) ) T δ ( l + 1 ) ) \delta^{(l)}=\frac{\partial L(y,\hat y)}{\partial z^{(l)}}\\ =\frac{\partial a^{(l)}}{\partial z^{(l)}}\cdot\frac{\partial z^{(l+1)}}{\partial a^{(l)}}\cdot\frac{\partial L(y,\hat y)}{\partial z^{(l+1)}}\\ =\text{diag}(f_{l}^{'}(z^{(l)}))\odot((W^{(l+1)})^{T}\delta^{(l+1)}) δ(l)=z(l)L(y,y^)=z(l)a(l)a(l)z(l+1)z(l+1)L(y,y^)=diag(fl(z(l)))((W(l+1))Tδ(l+1))
    其中 ⊙ \odot 是向量的点积运算符,表示矩阵中的对应元素相乘。

计算完上面三个偏导数之后,则有下面的求导公式
∂ L ( y , y ^ ) ∂ w i j ( l ) = I i ( a j ( l − 1 ) ) δ ( l ) = δ i ( l ) a j ( l − 1 ) \frac{\partial L(y,\hat y)}{\partial w_{ij}^{(l)}}=\mathbb{I}_{i}(a_{j}^{(l-1)})\delta^{(l)}=\delta_{i}^{(l)}a_{j}^{(l-1)} wij(l)L(y,y^)=Ii(aj(l1))δ(l)=δi(l)aj(l1)
所以得到 L ( y , y ^ ) L(y,\hat y) L(y,y^)关于第 l l l层权重 W ( l ) W^{(l)} W(l)的梯度值
∂ L ( y , y ^ ) ∂ W ( l ) = δ ( l ) ( a ( l − 1 ) ) T \frac{\partial L(y,\hat y)}{\partial W^{(l)}}=\delta^{(l)}(a^{(l-1)})^{T} W(l)L(y,y^)=δ(l)(a(l1))T
同样, L ( y , y ^ ) L(y,\hat y) L(y,y^)关于第 l l l层偏置 b ( l ) b^{(l)} b(l)的梯度
∂ L ( y , y ^ ) ∂ b ( l ) = δ ( l ) \frac{\partial L(y,\hat y)}{\partial b^{(l)}}=\delta^{(l)} b(l)L(y,y^)=δ(l)
初始值为其输入值
z m ( 0 ) = x z^{m^{(0)}}=x zm(0)=x

对于不同的问题,会有不同的损失函数。下面笔者使用均方差函数以及交叉熵函数来进行数学推导。

对于一般回归问题中的均方差函数,在最后一层中,令 y = z ( L ) y=z^{(L)} y=z(L)
δ ( L ) = ∂ L ( y , y ^ ) ∂ z ( L ) = ∂ ( ( y − y ^ ) T ( y − y ^ ) ) ∂ y = 2 ( y − y ^ ) \delta^{(L)}=\frac{\partial L(y,\hat y)}{\partial z^{(L)}}=\frac{\partial ((y-\hat y)^{T}(y-\hat y))}{\partial y}=2(y-\hat y) δ(L)=z(L)L(y,y^)=y((yy^)T(yy^))=2(yy^)

对于一般的分类问题中的交叉熵损失函数,在设计深层次神经网络过程中,最后一层中,我们会添加softmax函数。对于向量 z ( L ) z^{(L)} z(L),则必有输出向量 r r r
r = softmax ( z ( L ) ) r=\text{softmax}(z^{(L)}) r=softmax(z(L))

其中的每一个元素都有
r i = e z i ( L ) ∑ k = 1 m ( L ) e z k ( L ) r_{i}=\frac{e^{z_{i}^{(L)}}}{\sum\limits_{k=1}^{m^{(L)}}e^{z_{k}^{(L)}}} ri=k=1m(L)ezk(L)ezi(L)
在求导矩阵中,根据 i i i j j j是否相等来进行求导:
i = j i=j i=j情况: ∂ r i ∂ z i ( L ) = e z i ( L ) ⋅ 1 ∑ k = 1 m ( L ) e z k ( L ) + e z i ( L ) ⋅ − e z i ( L ) ( ∑ k = 1 m ( L ) e z k ( L ) ) 2 = e z i ( L ) ∑ k = 1 m ( L ) e z k ( L ) ⋅ ( 1 − e z i ( L ) ∑ k = 1 m ( L ) e z k ( L ) ) = r i ( 1 − r i ) \frac{\partial r_{i}}{\partial z_{i}^{(L)}}=e^{z_{i}^{(L)}}\cdot \frac{1}{\sum\limits_{k=1}^{m^{(L)}}e^{z_{k}^{(L)}}}+e^{z_{i}^{(L)}}\cdot\frac{-e^{z_{i}^{(L)}}}{(\sum\limits_{k=1}^{m^{(L)}}e^{z_{k}^{(L)}})^{2}}\\ =\frac{e^{z_{i}^{(L)}}}{\sum\limits_{k=1}^{m^{(L)}}e^{z_{k}^{(L)}}}\cdot(1-\frac{e^{z_{i}^{(L)}}}{\sum\limits_{k=1}^{m^{(L)}}e^{z_{k}^{(L)}}})\\ =r_{i}(1-r_{i}) zi(L)ri=ezi(L)k=1m(L)ezk(L)1+ezi(L)(k=1m(L)ezk(L))2ezi(L)=k=1m(L)ezk(L)ezi(L)(1k=1m(L)ezk(L)ezi(L))=ri(1ri)

i ≠ j i\neq j i=j情况: ∂ r i ∂ z j ( L ) = e z i ( L ) ⋅ − e z j ( L ) ( ∑ k = 1 m ( L ) e z k ( L ) ) 2 = − e z i ( L ) ∑ k = 1 m ( L ) e z k ( L ) ⋅ e z j ( L ) ∑ k = 1 m ( L ) e z k ( L ) = − r i r j \frac{\partial r_{i}}{\partial z_{j}^{(L)}}=e^{z_{i}^{(L)}}\cdot \frac{-e^{z_{j}^{(L)}}}{(\sum\limits_{k=1}^{m^{(L)}}e^{z_{k}^{(L)}})^{2}}\\ =-\frac{e^{z_{i}^{(L)}}}{{\sum\limits_{k=1}^{m^{(L)}}e^{z_{k}^{(L)}}}}\cdot \frac{e^{z_{j}^{(L)}}}{\sum\limits_{k=1}^{m^{(L)}}e^{z_{k}^{(L)}}}\\ =-r_{i}r_{j} zj(L)ri=ezi(L)(k=1m(L)ezk(L))2ezj(L)=k=1m(L)ezk(L)ezi(L)k=1m(L)ezk(L)ezj(L)=rirj

∂ L ( r , y ^ ) ∂ r i = − y ^ i r i \frac{\partial L(r,\hat y)}{\partial r_{i}}=-\frac{\hat y_{i}}{r_{i}} riL(r,y^)=riy^i

交叉熵函数
δ i ( L ) = ∂ L ( r , y ^ ) ∂ z i ( L ) = ∑ j = 1 m ( L ) ∂ L ( r , y ^ ) ∂ r j ⋅ ∂ r j ∂ z i ( L ) = ∑ j ≠ i m ( L ) ∂ L ( r , y ^ ) ∂ r j ⋅ ∂ r j ∂ z i ( L ) + ∂ L ( r , y ^ ) ∂ r i ⋅ ∂ r i ∂ z i ( L ) \delta_{i}^{(L)}=\frac{\partial L(r,\hat y)}{\partial z_{i}^{(L)}}=\sum_{j=1}^{m^{(L)}}\frac{\partial L(r,\hat y)}{\partial r_{j}}\cdot{\frac{\partial r_{j}}{\partial z_{i}^{(L)}}}\\ =\sum_{j\neq i}^{m^{(L)}}\frac{\partial L(r,\hat y)}{\partial r_{j}}\cdot{\frac{\partial r_{j}}{\partial z_{i}^{(L)}}}+\frac{\partial L(r,\hat y)}{\partial r_{i}}\cdot{\frac{\partial r_{i}}{\partial z_{i}^{(L)}}} δi(L)=zi(L)L(r,y^)=j=1m(L)rjL(r,y^)zi(L)rj=j=im(L)rjL(r,y^)zi(L)rj+riL(r,y^)zi(L)ri

所以,当 i ≠ j i\neq j i=j时候:
∑ j ≠ i m ( L ) ∂ L ( r , y ^ ) ∂ r j ⋅ ∂ r j ∂ z i ( L ) = ∑ j ≠ i m ( L ) [ − y ^ j r j ( − r i r j ) ] \sum_{j\neq i}^{m^{(L)}}\frac{\partial L(r,\hat y)}{\partial r_{j}}\cdot{\frac{\partial r_{j}}{\partial z_{i}^{(L)}}}=\sum_{j\neq i}^{m^{(L)}}[-\frac{\hat y_{j}}{r_{j}}(-r_{i}r_{j})] j=im(L)rjL(r,y^)zi(L)rj=j=im(L)[rjy^j(rirj)]

i = j i=j i=j时候
∂ L ( r , y ^ ) ∂ r i ⋅ ∂ r i ∂ z i ( L ) = − y ^ i r i [ r i ( 1 − r i ) ] \frac{\partial L(r,\hat y)}{\partial r_{i}}\cdot{\frac{\partial r_{i}}{\partial z_{i}^{(L)}}}=-\frac{\hat y_{i}}{r_{i}}[r_{i}(1-r_{i})] riL(r,y^)zi(L)ri=riy^i[ri(1ri)]

所以
δ i ( L ) = ∂ L ( r , y ^ ) ∂ z i ( L ) = ∑ j = 1 m ( L ) ∂ L ( r , y ^ ) ∂ r j ⋅ ∂ r j ∂ z i ( L ) = ∑ j ≠ i m ( L ) [ − y ^ j r j ( − r i r j ) ] + { − y ^ i r i [ r i ( 1 − r i ) ] } = r i { ∑ j ≠ i m ( L ) [ − y ^ j r j ( − r j ) ] − y ^ i r i ( 1 − r i ) } = r i { ∑ j ≠ i m ( L ) y ^ j − y ^ i r i + y ^ i } = r i ( 1 − y ^ i r i ) = r i − y ^ i \delta_{i}^{(L)}=\frac{\partial L(r,\hat y)}{\partial z_{i}^{(L)}}=\sum_{j=1}^{m^{(L)}}\frac{\partial L(r,\hat y)}{\partial r_{j}}\cdot{\frac{\partial r_{j}}{\partial z_{i}^{(L)}}}\\ =\sum_{j\neq i}^{m^{(L)}}[-\frac{\hat y_{j}}{r_{j}}(-r_{i}r_{j})]+\{-\frac{\hat y_{i}}{r_{i}}[r_{i}(1-r_{i})]\}\\ =r_{i}\{\sum_{j\neq i}^{m^{(L)}}[-\frac{\hat y_{j}}{r_{j}}(-r_{j})]-\frac{\hat y_{i}}{r_{i}}(1-r_{i})\}\\ =r_{i}\{\sum_{j\neq i}^{m^{(L)}}\hat y_{j}-\frac{\hat y_{i}}{r_{i}}+\hat y_{i}\}\\ =r_{i}(1-\frac{\hat y_{i}}{r_{i}})=r_{i}-\hat y_{i} δi(L)=zi(L)L(r,y^)=j=1m(L)rjL(r,y^)zi(L)rj=j=im(L)[rjy^j(rirj)]+{riy^i[ri(1ri)]}=ri{j=im(L)[rjy^j(rj)]riy^i(1ri)}=ri{j=im(L)y^jriy^i+y^i}=ri(1riy^i)=riy^i

用矩阵表示为
δ ( L ) = r − y ^ \delta^{(L)}=r-\hat y δ(L)=ry^

2.5 算法详细过程

神经网络训练过程的算法如下所示
输入:训练集 D = { x ( n ) , y ( n ) } n = 1 N D=\{x^{(n)},y^{(n)}\}_{n=1}^{N} D={x(n),y(n)}n=1N,验证集 V V V,学习率 α \alpha α,正则化系数 λ \lambda λ,网络层数目 L L L,神经网络数量 m ( l ) , 1 ≤ l ≤ L m^{(l)},1\leq l \leq L m(l),1lL

  1. 随机初始化 W W W b b b
  2. repeat
    ① 对训练集 D D D中选取样本 ( x ( n ) , y ( n ) ) (x^{(n)},y^{(n)}) (x(n),y(n))
    ② 前馈计算每一层的净输入 z ( l ) z^{(l)} z(l)以及激活值 a ( l ) a^{(l)} a(l),直到最后一层;
    ③ 反向传播计算每一层的误差 δ ( l ) \delta^{(l)} δ(l)
    // 计算每一层的导数
    ∀ l , ∂ L ( y ( n ) , y ^ ( n ) ) ∂ W ( l ) = δ ( l ) ( a ( l − 1 ) ) T \forall l,\frac{\partial L(y^{(n)},\hat y^{(n)})}{\partial W^{(l)}}=\delta^{(l)}(a^{(l-1)})^{T} l,W(l)L(y(n),y^(n))=δ(l)(a(l1))T
    ∀ l , ∂ L ( y ( n ) , y ^ ( n ) ) ∂ b ( l ) = δ ( l ) \forall l,\frac{\partial L(y^{(n)},\hat y^{(n)})}{\partial b^{(l)}}=\delta^{(l)} l,b(l)L(y(n),y^(n))=δ(l)
    // 更新参数
    W ( l ) ← W ( l ) − α ( ∂ L ( y ( n ) , y ^ ( n ) ) ∂ W ( l ) + λ W ( l ) ) W^{(l)}\leftarrow W^{(l)}-\alpha(\frac{\partial L(y^{(n)},\hat y^{(n)})}{\partial W^{(l)}}+\lambda W^{(l)}) W(l)W(l)α(W(l)L(y(n),y^(n))+λW(l))
    b ( l ) ← b ( l ) − α ∂ L ( y ( n ) , y ^ ( n ) ) ∂ b ( l ) b^{(l)}\leftarrow b^{(l)}-\alpha\frac{\partial L(y^{(n)},\hat y^{(n)})}{\partial b^{(l)}} b(l)b(l)αb(l)L(y(n),y^(n))
  3. end
  4. until 神经网络模型在验证集 V V V上的错误率不再下降;

输出:权重矩阵 W W W和偏置 b b b

3. 前馈神经网络的实现

我们现在对上述推导过程的前馈网络进行一些实现。我们这里使用python中的numpy库、Lua中的Torch7库、MATLAB实现前馈神经网络,这些函数库中包含有丰富的科学计算库。另外,笔者尝试使用Go、java和Julia语言再一次实现前馈神经网络。用这些不同的计算机语言实现前馈神经网络的目的是位比较不同语言中运算速度等。
下面是使用numpy实现前馈神经网络库核心函数,主要包括有forward函数、backward函数以及梯度更新操作。详细的代码见笔者github上的项目:numpy实现前馈神经网络

import numpy as np
import time
class Relu:
    def __init__(self):
        self.name = "sigmoid"
    def forward(self,input):
        return np.maximum(input, 0)
    def diff(self,input):
        def inner_diff_relu(x):
            if x >= 0:
                return 1
            else:
                return 0
        inner_diff_relu = np.vectorize(inner_diff_relu)
        return inner_diff_relu(input)
class Sigmoid:
    def __init__(self):
        self.name = "sigmoid"
        def sigmoid(x):
            return 1 / (1 + np.exp(-x))
        self.sigmoid = sigmoid
    def forward(self,input):
        return 1/(1+np.exp(-input))
    def diff(self,input):
        return np.multiply(self.sigmoid(input),1-self.sigmoid(input))
class Tanh:
    def __init__(self):
        self.name = "tanh"
    def forward(self,input):
        return np.tanh(input)
    def diff(self,input):
        return 1 - np.multiply(np.tanh(input), np.tanh(input))
class Linear:
    def __init__(self,in_dim,out_dim):
        self.in_dim = in_dim
        self.out_dim = out_dim
        #initialize the weight and bais
        self.weight = np.random.rand(in_dim,out_dim) # size:(in_dim,out_dim)
        self.bais = np.random.rand(out_dim) # size:(1,out_dim)
    def forward(self,input):
        batch = input.shape[0]
        out = np.dot(input,self.weight) + np.broadcast_to(self.bais,(batch,self.out_dim))
        return out
    def __call__(self, input):
        return self.forward(input)
    def __str__(self):
        return "Linear Layer:\n(in_dim=%d,out_dim=%d)"%(self.in_dim,self.out_dim)
class BatchNormalize:
    def __init__(self,dtype = "normalize"):
        self.dtype = dtype
    def forward(self,input):
        if self.dtype == "normalize":
            return BatchNormalize._normalize(input)
        elif self.dtype == "maxmin":
            return BatchNormalize._maxmin(input)
        else:
            raise ValueError("Error for normalization type %s"%str(self.dtype))
    @staticmethod
    def _normalize(input):
        for k in range(len(input)):
            input[k] = (input[k] - input[k].mean())/input[k].std()
        return input
    @staticmethod
    def _maxmin(input):
        for k in range(len(input)):
            input[k] = (input[k] - input[k].min())/(input[k].max()-input[k].min())
        return input
class DNNNet_Regression:
    def __init__(self,dim_list,act_func=None,batchnormalize = False,learning_rate=0.3,lambd = 0,name =None):
        self.dim_list = dim_list
        self.hidden_list = []
        self.num_layers = len(dim_list)-1
        self.batchFlag = batchnormalize
        self.learning_rate = learning_rate
        self.lambd = lambd
        if name is None:
            self.name = time.strftime("%Y%m%d%H%M", time.localtime(time.time()))
        else:
            self.name = name
        if act_func is None:
            self.act_class = self.select_act("relu")()
        else:
            self.act_class = self.select_act(act_func)()
        if self.batchFlag:
            self.batch_nomal = BatchNormalize(dtype= "maxmin")
        for k in range(self.num_layers):
            fc = Linear(self.dim_list[k],self.dim_list[k+1])
            self.hidden_list.append(fc)
    def select_act(self,name):
        if name == "sigmoid":
            return Sigmoid
        elif name == "tanh":
            return Tanh
        elif name == "relu":
            return Relu
        else:
            raise TypeError("Unknown type %s"%str(name))
    def forward(self,input):
        out = input
        for k in range(self.num_layers):
            if self.batchFlag:
                out = self.batch_nomal.forward(out)
            hidden = self.act_class.forward(out)
            out = self.hidden_list[k].forward(hidden)
        return out
    def backward(self,input,target):
        weight_grad_list = []
        bais_grad_list = []
        output = self.forward(input)
        delta_l = 2*(output-target)
        for k in range(self.num_layers-1,-1,-1):
            out = input
            for j in range(k):
                if self.batchFlag:
                    out = self.batch_nomal.forward(out)
                hidden = self.act_class.forward(out)
                out = self.hidden_list[j].forward(hidden)
            delta_W = np.dot(out.T,delta_l)
            delta_b = delta_l
            # update the next delta_l
            delta_l = np.multiply(self.act_class.diff(out),np.dot(delta_l,self.hidden_list[k].weight.T))
            # save the gradient
            weight_grad_list.append(delta_W)
            bais_grad_list.append(delta_b)
        return weight_grad_list,bais_grad_list
    def batch_backward(self,input,target):
        batch = input.shape[0]
        for k in range(batch):
            weight_grad_list,bais_grad_list = self.backward(input[k],target[k])
            self.update_grad(weight_grad_list,bais_grad_list)
    def loss(self,output,target):
        out = np.square(target-output)
        return np.sum(out)/len(out)
    def update_grad(self,weight_grad_list,bais_grad_list):
        N = self.dim_list[self.num_layers]
        for k in range(self.num_layers):
            self.hidden_list[k].weight = self.hidden_list[k].weight - self.learning_rate*(weight_grad_list[self.num_layers-k-1]/N+self.lambd*self.hidden_list[k].weight)
            self.hidden_list[k].bais = self.hidden_list[k].bais - self.learning_rate*bais_grad_list[self.num_layers-k-1]/N

numpy实现的神经网络中主要的问题是,对于Relu函数网络训练结果较好,但是对于Sigmoid函数以及Tanh函数来说,训练结果比较差,主要存在的问题是梯度爆炸问题难以控制。其次,对于归一化层的添加,训练结果也存在梯度爆炸问题。
下面是Torch7库实现前馈神经网络,详细代码见项目Torch7实现前馈神经网络

require("torch")
model_package = {}
local Linear = torch.class("model_package.Linear")
local DNNNet = torch.class("model_package.DNNNet")
local Sigmoid = torch.class("model_package.Sigmoid")
local Relu = torch.class("model_package.Relu")
local Tanh = torch.class("model_package.Tanh")
function Sigmoid:__init()
    self.name = "sigmoid"
end
function Sigmoid:forward(input)
    return torch.sigmoid(input)
end
function Sigmoid:diff(x)
    return torch.cmul(torch.sigmoid(x),(1-torch.sigmoid(x)))
end

function Tanh:__init()
    self.name = "tanh"
end
function Tanh:forward(input)
    return torch.tanh(input)
end
function Tanh:diff(input)
    return 1- torch.pow(torch.tanh(input),2)
end

function Relu:__init()
    self.name = "relu"
end
function Relu:forward(input)
    output = input:clone()
    output:apply(function(x)
        if x>0 then
            return x
        else
            return 0
        end
    end)
    return output
end
function Relu:diff(input)
    output = input:clone()
    output:apply(function(x)
        if x>=0 then
            return 1.0
        else
            return 0.0
        end
    end)
    return output
end

function Linear:__init(in_dim,out_dim)
    self.in_dim = in_dim
    self.out_dim = out_dim

    self.weight = torch.rand(in_dim,out_dim)
    self.bais = torch.rand(out_dim)
end
function Linear:forward(input)
    if input:dim() == 2 then
        batch = input:size()[1]
        bais = torch.Tensor(batch,self.out_dim)
        for k =1,batch do
            bais[k] = self.bais
        end
        output = torch.mm(input,self.weight) + bais
    elseif input:dim() == 1 then
        output = torch.mv(self.weight:t(),input) + self.bais
    else
        error("the dim do not match!",2)
    end
    return output
end
function DNNNet:__init(dim_list,learning_rate,lambda,act_func)
    self.dim_list = dim_list
    self.hid_list = {}
    self.num_layers = #dim_list - 1
    self.learning_rate = learning_rate or 0.3
    self.lambda = lambda or 0.0
    for k=1,self.num_layers do
        self.hid_list[k] = model_package.Linear(dim_list[k],dim_list[k+1])
    end
    if act_func == nil then
        
        self.act_class = self:select_act("sigmoid")()
    else
        self.act_class = self:select_act(act_func)()
    end
end
function DNNNet:select_act(name)
    if name == "sigmoid" then
        return model_package.Sigmoid
    elseif name == "relu" then
        return model_package.Relu
    elseif name == "tanh" then
        return model_package.Tanh
    else
        error("Error activate function: ".. tostring(name),2)
    end
end

function DNNNet:forward(input)
    output = input
    for k =1,self.num_layers do
        hidden = self.act_class:forward(output)
        output = self.hid_list[k]:forward(hidden)
    end
    return output
end
function DNNNet:backward(input,target)
    weight_grad_list = {}
    bais_grad_list = {}
    output = self:forward(input)
    delta_l = 2*(output -target)
    for k =self.num_layers,1,-1 do
        output = input
        for j=1,k-1 do
            hidden = self.act_class:forward(output)
            output = self.hid_list[j]:forward(hidden)
        end
        delta_W = torch.ger(output,delta_l)
        delta_b = delta_l:clone()
        delta_l = torch.cmul(self.act_class:diff(output),torch.mv(self.hid_list[k].weight,delta_l))
        table.insert(weight_grad_list,delta_W)
        table.insert(bais_grad_list,delta_b)
    end
    return weight_grad_list,bais_grad_list
end
function DNNNet:batch_backward(input,target)
    batch = input:size()[1]
    for k =1,batch do
        weight_grad_list,bais_grad_list = self:backward(input[k],target[k])
        self:update_grad(weight_grad_list,bais_grad_list)
        os.exit()
    end
end

function DNNNet:update_grad(weight_grad_list,bais_grad_list)
    N = self.dim_list[#self.dim_list]
    for k =1,self.num_layers do
        self.hid_list[k].weight = self.hid_list[k].weight - self.learning_rate*(weight_grad_list[self.num_layers-k+1]/N+self.lambda*self.hid_list[k].weight)
        self.hid_list[k].bais = self.hid_list[k].bais - self.learning_rate*bais_grad_list[self.num_layers-k+1]/N
    end
end

function DNNNet:loss(output,target)
    return torch.sum(output-target)
end

下面是使用go、java、C++语言实现前馈神经网络的例子,由于这两个语言中没有实现矩阵乘法,所以首先实现这些基础矩阵乘法。详细代码见以下项目
C++语言实现前馈神经网络
Java语言实现前馈神经网络
C语言使用CUDA实现前馈神经网络加速
Julia语言实现前馈神经网络
Go语言实现前馈神经网络

4. 实验设计与试验结果

实验中我们使用到的是葡萄酒评测数据集。其中这些数据集分为白葡萄酒数据集(4898条)和红葡萄酒评测数据集(1599条),数据中分为11个输入数据项作为输入数据,1个数据项作为输出。11个数据项分别为:固定酸度,挥发性酸度,柠檬酸,残留糖,氯化物,游离二氧化硫,总二氧化硫,密度,pH值,硫酸盐,酒精。输出变量为葡萄酒的质量值(分数为0到10之间)。两个数据集中,我们采用了70%的数据作为训练数据集,15%作为测试数据集,15%作为验证测试集。
实验中,我们测试了几次神经网络的层数以及节点个数数量,实验结果如下所示:

每层隐藏层的节点个数 激活函数 学习率 正则化参数 实验测试集误差值(红葡萄酒) 实验测试集误差值(白葡萄酒)
50,30,20,10,5 sigmoid 0.1 0.1 0.789623 0.724354
50,30,20,10,5 relu 0.1 0.1 0.663542 1.028760
50,30,20,10,5 tanh 0.1 0.1 0.663542 Nan
50,30,20,10,5 tanh 0.01 0.1 0.603452 0.830197
50,30,10,5 tanh 0.01 0.1 0.667059 0.812876
50,10,5 relu 0.01 0.1 0.691021 0.823464
10 relu 0.01 0.1 0.681109 0.808402
10 tanh 0.01 0.1 0.592218 0.754554
10 sigmoid 0.01 0.1 0.613808 0.757545
50,100,60,30,20,10 tanh 0.01 0.1 0.723089 0.753538
50,100,60,30,20,10 relu 0.01 0.1 0.682197 0.748096
50,100,60,30,20,10 sigmoid 0.01 0.1 0.633969 0.771930
10,30,5 relu 0.001 0.1 0.656096 0.839657
10,30,5 sigmoid 0.001 0.1 0.792913 0.840292
10,30,5 tanh 0.001 0.1 0.630493 0.805885

实验中测试集误差值越小表明实验模型越符合具体实际数据值。由表中的数据可知,学习率大概取值在 0.01 ∼ 0.2 0.01\sim 0.2 0.010.2之间较为合适,神经网络层数必须在一个合适的值求解出的值比较符合数据结果。另外,在实验过程中,使用Sigmoid、tanh 函数容易出现梯度爆炸现象,relu 这种现象少了一些,但是不足的问题是,relu函数平滑性不是很好,容易出现一种称为dead neuron的问题。总体来说,实验比较成功。

5. 小结

本小结中详细推导了多层深度神经网络中的一些基本的数学原理,最基础的算法是随机梯度下降算法,这是必须要掌握的内容,后续的内容笔者会介绍一些其他类型的基本神经网络。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐