神经网络学习笔记——反向传播

1,578 阅读5分钟

这是我参与11月更文挑战的第7天

反向传播 backpropagation —— 一种快速计算代价函数梯度的算法

使⽤矩阵快速计算输出

为了解释问题,首先给出神经网络中权重的清晰定义,如下图所示:

image.png

同样定义bjlb^l_j为第ll层的第jj个神经元的偏置:

image.png

基于上面的定义,可以得到第ll层的第jj个神经元的激活值ajla^l_j(这个值和第l1l-1层的激活值akl1a^{l-1}_k有关):

ajl=σ(kwjklakl1+bjl)a^l_j=\sigma(\sum_kw^l_{jk}a^{l-1}_k+b^l_j)

其中kwjklakl1\sum_kw^l_{jk}a^{l-1}_k中的kk表示l1l-1层中的所有神经元数量,对于每一层ll定义一个权重矩阵wlw^lwlw^l中的元素是l1l-1层到ll层连接的权重,因此wjklw^l_{jk}可以理解为l1l-1层中第kk个神经元到ll层第jj个神经元的连接权重。

上面的公式可以简化为:

al=σ(wlal1+bl)a^l=\sigma(w^la^{l-1}+b^l)

在计算ala^l需要计算中间量zl=wlal1+blz^l=w^la^{l-1}+b^l,将zlz^l称为ll层神经元的带权输入,因此上面的公式又可以简化为al=σ(zl)a^l=\sigma(z^l)

代价函数的两个假设

反向传播算法的目标是计算代价函数CC关于两个参数wwbb的偏导数Cw,Cb\frac{\partial C}{\partial w},\frac{\partial C}{\partial b}。将CC定义为一个二次代价函数:

C=12nxy(x)aL(x)2C=\frac{1}{2n}\sum_x||y(x)-a^L(x)||^2

nn表示训练样本总数,x\sum_x表示遍历了所有样本,y=y(x)y=y(x)是输入xx时对应的期望目标输出,LL表示网络层数,aL=aL(x)a^L=a^L(x)是当输入为xx时网络输出的激活值向量。

为了应用反向传播算法,要对代价函数CC做两个假设:

  • 代价函数可以被写成一个在每个训练样本xx上的代价函数CxC_x的均值C=1nxCxC=\frac{1}{n}\sum_xC_x,其中每个独立样本的代价为Cx=12yaL2C_x=\frac{1}{2}||y-a^L||^2
  • 代价函数可以写成神经网络的输出函数:cost C=C(aL)cost\ C = C(a^L),在这个假设下CxC_x可以进一步写成:
C=12yaL2=12j(yjajL)2C=\frac{1}{2}||y-a^L||^2=\frac{1}{2}\sum_j(y_j-a^L_j)^2

背景知识 hadamard乘积/schur乘积: sts\odot t

sts\odot t表示按元素乘积,(st)j=sjtj(s\odot t)_j=s_jt_j,如下所示: image.png

微分、梯度和梯度下降的关系

四个基本公式

反向传播算法的核心是计算偏导数Cwjkl,Cbjl\frac{\partial C}{\partial w^l_{jk}},\frac{\partial C}{\partial b^l_j},为达目的首先引入中间量δjl\delta^l_j,表示第ll层第jj个神经元上的误差:

δjl=Czjl\delta^l_j=\frac{\partial C}{\partial z^l_j}

这个公式基于一个启发式的认识,即Czjl\frac{\partial C}{\partial z^l_j}是神经元误差的度量。

反向传播基于四个基本公式,这些公式让我们可以计算误差和代价函数梯度。

公式1

输出层误差δL\delta^L的公式:

δjL=Cajlσ(zjL)(1)\delta^L_j=\frac{\partial C}{\partial a^l_j}\sigma'(z^L_j) \tag{1}

公式中Cajl\frac{\partial C}{\partial a^l_j}表示代价CC随着第ll层第jj个神经元输出的激活值的变化而变化的速率σ(zjL)\sigma'(z^L_j)表示激活函数σ\sigmazjLz^L_j处的变化速率。对于二次代价函数C=12j(yjaj)2C=\frac{1}{2}\sum_j(y_j-a_j)^2Cajl=(ajyj)\frac{\partial C}{\partial a^l_j}=(a_j-y_j)

为方便计算将公式(1)重写为矩阵形式:

δL=aCσ(zL)\delta^L=\nabla_aC\odot\sigma'(z^L)

带入二次代价函数的偏导计算得到:

δL=(aLy)σ(zL)\delta^L=(a^L-y)\odot\sigma'(z^L)

公式2

使用下一层的误差δl+1\delta^{l+1}来表示当前层的误差δl\delta^l

δl=((wl+1)Tδl+1)σ(zl)(2)\delta^l=((w^{l+1})^T\delta^{l+1})\odot\sigma'(z^l) \tag{2}

公式(2)给出了由下一层误差推导当前层误差的方法,这就是反向传播中反向的含义,即可以将其看作沿着⽹络反向移动误差,通过组合公式(1)和公式(2)可以实现计算任意层的误差。

公式3

代价函数和网络中任意偏置相关的变化率:

Cbjl=δjl(3)\frac{\partial C}{\partial b^l_j}=\delta^l_j \tag{3}

公式(3)证明误差δjl\delta^l_j和偏导数值Cbjl\frac{\partial C}{\partial b^l_j}完全一致

公式4

代价函数和任何一个权重相关的变化率:

Cwjkl=akl1δjl(4)\frac{\partial C}{\partial w^l_{jk}}=a^{l-1}_k\delta^l_j \tag{4}

公式(4)给出了偏导数Cwjkl\frac{\partial C}{\partial w^l_{jk}}的计算方法,它可以简化为:

Cw=ainδout\frac{\partial C}{\partial w}=a_{in}\delta_{out}

aina_{in}表示权重为ww的链路上的输入激活值,δout\delta_{out}表示权重为ww链路上的输出误差,如下图所示: image.png

S型函数的特征令σ\sigma在接近0或1时变化会非常小,这也会让权重学习变得缓慢,对于这样的情形,称为输出神经元已经饱和了,继而权重学习会终⽌(或者学习⾮常缓慢),从上面的公式可以看出学习的速率和σ\sigma'有关。

四个公式的证明

背景知识

多元微积分链式法则公式:

dwdt=i=1nwxidxidt\frac{dw}{dt}=\sum^n_{i=1}\frac{\partial w}{\partial x_i}\frac{dx_i}{dt}

公式1证明

回忆输出误差δ\delta的定义:

δjL=CzjL\delta^L_j=\frac{\partial C}{\partial z^L_j}

基于上述背景知识可以将公式1改写为:

δjL=kCakLakLzjL\delta^L_j =\sum_k \frac{\partial C}{\partial a^L_k}\frac{\partial a^L_k}{\partial z^L_j}

kk表示输出层的所有神经元,第kk个神经元的akLa^L_k只依赖于k=jk=j时的输入权重zjLz^L_j,也就是说kjk \neq jakLzjL\frac{\partial a^L_k}{\partial z^L_j}不存在,基于前面的理论,上面的公式可以化为:

δjL=CajLajLzjL\delta^L_j = \frac{\partial C}{\partial a^L_j}\frac{\partial a^L_j}{\partial z^L_j}

基于ajL=σ(zjL)a^L_j=\sigma(z^L_j)上面公式的右边可以写为σ(zjL)\sigma'(z^L_j),因此可以得到公式1:

δjL=Cajlσ(zjL)(1)\delta^L_j=\frac{\partial C}{\partial a^l_j}\sigma'(z^L_j) \tag{1}

公式2证明

继续延伸误差δ\delta

δjl=Czjl\delta^l_j=\frac{\partial C}{\partial z^l_j}

依旧用链式法则,公式2的核心思想是用下一层误差表示当前误差,因此要想办法引入δkl+1\delta^{l+1}_k,而δkl+1=Czjl+1\delta^{l+1}_k=\frac{\partial C}{\partial z^{l+1}_j},显然要想办法引入zjl+1\partial z^{l+1}_j到误差公式中:

δjl=Czjl=kCzkl+1zkl+1zjl=kzkl+1zjlδkl+1\delta^l_j=\frac{\partial C}{\partial z^l_j} =\sum_k \frac{\partial C}{\partial z^{l+1}_k}\frac{\partial z^{l+1}_k}{\partial z^{l}_j}=\sum_k \frac{\partial z^{l+1}_k}{\partial z^{l}_j} \delta^{l+1}_k

zz的定义可得:

zkl+1=jwkjl+1ajl+bkl+1=jwkjl+1σ(zjl)+bkl+1z^{l+1}_k=\sum_jw^{l+1}_{kj}a^l_j+b^{l+1}_k=\sum_j w^{l+1}_{kj}\sigma(z^l_j)+b^{l+1}_k

计算zkl+1zjl \frac{\partial z^{l+1}_k}{\partial z^{l}_j}得到:

zkl+1zjl=wkjl+1σ(zjl) \frac{\partial z^{l+1}_k}{\partial z^{l}_j}=w^{l+1}_{kj}\sigma'(z^l_j)

计算过程可以用数学归纳法证明,过程如下:

j=1j=1时:

zkl+1z1l=wk1l+1σ(z1l)\frac{\partial z^{l+1}_k}{\partial z^{l}_1}=w^{l+1}_{k1}\sigma'(z^l_1)

j=2j=2时:

zkl+1z2l=wk2l+1σ(z2l)\frac{\partial z^{l+1}_k}{\partial z^{l}_2}=w^{l+1}_{k2}\sigma'(z^l_2)

j=mj=m时:

zkl+1zml=wkml+1σ(zml)\frac{\partial z^{l+1}_k}{\partial z^{l}_m}=w^{l+1}_{km}\sigma'(z^l_m)

证明完毕

综合上述公式可以得到公式2:

δjl=kwkjl+1δkl+1σ(zjl)=((wl+1)Tδl+1)σ(zl)\delta^l_j=\sum_k w^{l+1}_{kj}\delta^{l+1}_k\sigma'(z^l_j)= ((w^{l+1})^T\delta^{l+1})\odot\sigma'(z^l)

公式3的证明

Cbjl=Czjlzjlbjl=δjl×1\frac{\partial C}{\partial b^l_j}=\frac{\partial C}{\partial z^l_j}\frac{\partial z^l_j}{\partial b^l_j}=\delta^l_j \times 1

公式4证明

Cwjkl=Czjlzjlwjkl=δjl(kwjklakl1+bjl)wjkl=akl1δjl\frac{\partial C}{\partial w^l_{jk}}=\frac{\partial C}{\partial z^l_{j}}\frac{\partial z^l_{j}}{\partial w^l_{jk}}=\delta^l_j\frac{\partial (\sum_k w^l_{jk}a^{l-1}_k+b^l_j)}{\partial w^l_{jk}}=a^{l-1}_k\delta^l_j

反向传播算法过程

  • 输入x:为输入层设置激活值a1a^1
  • 前向传播:对于从第二层开始的l=2,3,4...,Ll=2,3,4...,L计算中间值zl=wlal1+blz^l=w^la^{l-1}+b^lal=σ(zl)a^l=\sigma(z^l)
  • 输出层误差δL\delta^L:计算最后一层(输出层)误差δL=aCσ(zL)\delta^L=\nabla_aC\odot\sigma'(z^L)
  • 反向误差传播:基于δL\delta^L逆推前面层的误差(l=L1,L2,...,2l=L-1,L-2,...,2),计算δl=((wl+1)Tδl+1)σ(zl)\delta^l=((w^{l+1})^T\delta^{l+1})\odot\sigma'(z^l)
  • 输出:计算代价函数梯度,即Cbjl=δjl\frac{\partial C}{\partial b^l_j}=\delta^l_jCwjkl=akl1δjl\frac{\partial C}{\partial w^l_{jk}}=a^{l-1}_k\delta^l_j

在反向传播算法的基础上应用梯度下降方法更新参数过程:

image.png

算法的实现

# 反向传播算法
    # 返回一个元祖,nabla_b, nabla_w表示损失函数C_x的梯度
    def backprop(self,x,y):
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # feedforward 前向传播
        #设置输入层激活值activation=a^1
        #activations 用来存储每层的激活值
        activation = x
        activations = [x]
        # zs用来存储每一层的中间值z
        zs = [] #
        for b, w in list(zip(self.biases, self.weights)):
            # 中间值z的计算
            z = np.dot(w, activation) + b
            zs.append(z)
            # 基于z计算激活值
            activation = sigmoid(z)
            activations.append(activation)
        # backward pass 反向传播
        # cost_derivative 计算网络输出值和期望输出的差值 即nabla C
        # sigmoid_prime返回sigmoid函数的导数
        # 计算误差delta
        delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
        # 计算参数b和w的梯度
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        # 从后往前(反向)逆推前面层的误差delta
        for l in range(2, self.num_layers):
            # 反向因此此处是-l
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l + 1].transpose(), delta) * sp
            # 存储对应层的nabla b和 nabla w
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
        return nabla_b, nabla_w