对MLP(BP神经网络)反向传播公式的理解

虽然用了很久的神经网络编程框架了(不过也隔了很长时间没接触),但反向传播公式怎么来的还是记不住。于是今天就手动推导了一下。

先附一张图

下面是一张反向传播公式图。第一个公式是计算最后一层误差,第二个公式是反向地推公式,第三四个是根据误差推导 b 和 W 的更新公式。

不过这几个公式其实是简化后的写法,真正的写法使用的是 雅克比矩阵,这个留到最后讨论一下。

在这里插入图片描述

(不过这个误差好像是对 zi 而不是 ai 的,而且用对z的求导表示误差似乎是业界通识,还是记它比较好。但我之后用 C 对 a 的导数表示要进行反向传播的误差,原理一样。只要知道了原理,推 z 的就不是难事)

预定义变量

(以下所有向量都为列向量)

对第 i 层网络,权重(weight)为 Wi,偏置(bias)为 bi,激活函数为 f(假设是softmax),输入为 a[i-1],输出为 ai,其中 (Wi*a[i-1]+bi) 习惯上称为 zi
a i = f ( W i a i − 1 + b i ) = f ( z i ) a_i = f(W_{i} a_{i-1}+b_i)=f(z_i) ai=f(Wiai1+bi)=f(zi)
对于最后一层,输出为 a_last
a l a s t = N e u r a l N e t w o r k ( a 0 ) , 即 整 个 网 络 的 输 出 a_{last}=NeuralNetwork(a_0),即整个网络的输出 alast=NeuralNetwork(a0)
代价为 C(一个标量),标签为y,代价函数为 Cost(y,a_last)
C = C o s t ( y , a l a s t ) C=Cost(y,a_{last}) C=Cost(y,alast)

推导

根据下面两条,可以求出 C 对所有 a_i 的偏导数
C = ∂ C o s t ( y , a l a s t ) ∂ a l a s t 【 最 后 一 层 】 C= \frac{\partial{Cost(y,a_{last})}}{\partial{a_{last}}} 【最后一层】 C=alastCost(y,alast)
∂ C ∂ a i − 1 = ∂ C ∂ a i ∂ a i ∂ z i ∂ z i ∂ a i − 1 = W T ( ∂ C ∂ a i ⊙ f ′ ( z i ) ) 【 已 知 ∂ C ∂ a i , 求 ∂ C ∂ a i − 1 】 \frac{\partial{C}}{\partial{a_{i-1}}} = \frac{\partial{C}}{\partial{a_{i}}} \frac{\partial{a_i}}{\partial{z_i}} \frac{\partial{z_i}}{\partial{a_{i-1}}} =W^{T} (\frac{\partial{C}}{\partial{a_{i}}} \odot f^{'}(z_i)) 【已知 \frac{\partial{C}}{\partial{a_{i}}} ,求 \frac{\partial{C}}{\partial{a_{i-1}}}】 ai1C=aiCziaiai1zi=WT(aiCf(zi))aiCai1C
实际上这只是一种简化,之后会提到。简单记住公式就行,下面是两点技巧用于记住公式

  • 注意上面公式中矩阵求导后的矩阵形状以及乘法顺序问题,要保证矩阵乘法规则成立,且最后结果向量要和 a[i-1] 维数一致。知道这点技巧后,即使对矩阵求导规则不熟练也能把乘的顺序猜出一二。

  • 激活函数导数部分 f ′ ( z i ) f^{'}(z_i) f(zi)采取的是“逐值相乘”( ⊙ \odot ),这并不违和,因为计算 f ( W i a i − 1 + b i ) f(W_{i} a_{i-1}+b_i) f(Wiai1+bi)时也是逐值计算的。

知道了 ∂ C ∂ a i \frac{\partial{C}}{\partial{a_{i}}} aiC后,求 ∂ C ∂ W i \frac{\partial{C}}{\partial{W_{i}}} WiC ∂ C ∂ b i \frac{\partial{C}}{\partial{b_{i}}} biC就很容易了。
∂ C ∂ W i = ∂ C ∂ a i ∂ a i ∂ z i ∂ z i ∂ W i = ( ∂ C ∂ a i ⊙ f ′ ( z i ) ) a i − 1 T \frac{\partial{C}}{\partial{W_{i}}} = \frac{\partial{C}}{\partial{a_{i}}} \frac{\partial{a_{i}}}{\partial{z_{i}}} \frac{\partial{z_i}}{\partial{W_{i}}} = (\frac{\partial{C}}{\partial{a_{i}}} \odot f^{'}(z_i))a_{i-1}^T WiC=aiCziaiWizi=(aiCf(zi))ai1T
∂ C ∂ b i = ∂ C ∂ a i ∂ a i ∂ z i ∂ z i ∂ b i = ∂ C ∂ a i ⊙ f ′ ( z i ) \frac{\partial{C}}{\partial{b_{i}}} = \frac{\partial{C}}{\partial{a_{i}}} \frac{\partial{a_{i}}}{\partial{z_{i}}} \frac{\partial{z_i}}{\partial{b_{i}}} = \frac{\partial{C}}{\partial{a_{i}}} \odot f^{'}(z_i) biC=aiCziaibizi=aiCf(zi)

使用

假设学习率为 lr(learning rate),目标是使得 C 最小化,则可按照下式更新参数:
W i : = W i − l r ∂ C ∂ W i b i : = b i − l r ∂ C ∂ b i W_i := W_i - lr \frac{\partial{C}}{\partial{W_{i}}} \\ b_i := b_i - lr \frac{\partial{C}}{\partial{b_{i}}} Wi:=WilrWiCbi:=bilrbiC
不过实际写代码的时候,这个 lr 应该放哪呢?

其实 l r ∂ C ∂ W i lr\frac{\partial{C}}{\partial{W_{i}}} lrWiC按照上面链式法则的思路可以写成 ( l r ∂ C ∂ a l a s t ) ∂ a l a s t ∂ W i (lr\frac{\partial{C}}{\partial{a_{last}}})\frac{\partial{a_{last}}}{\partial{W_{i}}} (lralastC)Wialast

也就说,只要计算了 ( l r ∂ C ∂ a l a s t ) (lr\frac{\partial{C}}{\partial{a_{last}}}) (lralastC),lr 会随着反向传播影响到所有层,所以只要在最后结果上使用 lr 就行。

举个例子:
平 方 损 失 函 数 : C = C o s t ( y , a ) = ( y − a ) T ( y − a ) 平方损失函数: C = Cost(y,a) = (y-a)^{T}(y-a) :C=Cost(y,a)=(ya)T(ya)
∂ C ∂ a = 2 ( y − a ) \frac{\partial{C}}{\partial{a}}=2(y-a) aC=2(ya)
则 Δ a = l r × 2 ( y − a ) , 这 个 2 在 编 程 的 时 候 可 以 不 用 管 则 \Delta a = lr\times2(y-a),这个2在编程的时候可以不用管 Δa=lr×2(ya)2
W i : = W i − Δ a ∂ a ∂ W i b i : = b i − Δ a ∂ a ∂ b i W_i := W_i -\Delta a\frac{\partial{a}}{\partial{W_{i}}} \\ b_i := b_i - \Delta a\frac{\partial{a}}{\partial{b_{i}}} Wi:=WiΔaWiabi:=biΔabia

为什么说之前推导的公式是被简化过的?

为什么说之前推导的公式是被简化过的?

回想一下之前的“推导过程”,那些矩阵求导之后出现的矩阵乘法顺序问题,以及“逐值相乘”(百度百科里说叫“哈达马积”:矩阵乘法)看起来相当别扭。我看了很多博客里没有关于这点的解释,好像从上一步跳到下一步是理所当然的。我以前就是被这一点所蒙蔽,从来没有真正理解那些公式的真正含义。

(╯°Д°)╯︵┻━┻

直到我看到这篇关于softmax的博客: Softmax函数及其导数

实际上向量对向量求导的结果是一个 雅克比矩阵(雅克比矩阵是导数的推广,非对角线上的元素表示的是一种耦合),由于 softmax 或者 relu 函数不存在 i项与j项 的耦合,求出来的雅可比矩阵一定是一个对角矩阵,是一种特殊情况,做矩阵乘法时候和上面说的逐值相乘结果一样。不如就写成一个一维向量,存矩阵对角线元素,不仅省内存,还能提高效率。所以 ∂ a i ∂ z i = f ′ ( z i ) \frac{\partial{a_i}}{\partial{z_i}}=f^{'}(z_i) ziai=f(zi)在做运算的时候可以写成一维向量,而且这么一改,为了满足运算规则,很多地方都得改,甚至在公式里加了“哈达马积”—— 反正我看的一头雾水。

不过像 softmax 这种 i 项和所有项相关的存在耦合,这时候就得老老实实求出雅克比矩阵,并使用运算量很大的矩阵乘法,顶多找一些技巧优化效率。

如果使用雅克比矩阵表示的话,这些反向传播的公式推导过程会变得非常容易理解,因为只涉及到普通的矩阵加法与乘法(当然如果还老想着“哈达马积”那套公式可能绕不过来)。

但是完全按照它来计算会变得非常耗时。举个例子感受一下:
[外链图片转存失败(img-6sM8OFCX-1565431552285)(3BF29C69FAB949B0AE0667EF126C88C0)]

这是 z = Wx, z 对 W 的导数。其中 z 是 T 维向量,x 是 N 维向量,W 是 NxT 维矩阵。把 W 按照主序展开成一维向量,z 对 W 进行求导就是一个 TxNT 的雅克比矩阵。

如果让我按照它来编程,肯定会慢的怀疑人生(╯°Д°)╯︵┻━┻,除非找到可以大幅简化的技巧。

简化对推导产生影响的例子

T 维向量对 N 维矩阵求导会得到 TxN 矩阵;T 维向量对 TxN 矩阵求导会得到 TxNT 矩阵。根据这两点来分析下这个公式:

在这里插入图片描述

dC/dW 是常量对矩阵求导,所以和 W 尺寸一样。所以乍看之下,没有任何问题。(下式中替换 a 位置的是 dz/dW)
∂ C ∂ W i = ∂ C ∂ z i ∂ z i ∂ W i \frac{\partial{C}}{\partial{W_{i}}} = \frac{\partial{C}}{\partial{z_{i}}} \frac{\partial{z_i}}{\partial{W_{i}}} WiC=ziCWizi
但是考虑到 ∂ z i ∂ W i \frac{\partial{z_i}}{\partial{W_{i}}} Wizi`或者 ∂ a i ∂ W i \frac{\partial{a_i}}{\partial{W_{i}}} Wiai是 TxNT 矩阵后,上面BP4公式就怎么都让人想不通了。

先按照 Softmax函数及其导数 中的思路想一下。

在这里插入图片描述
在这里插入图片描述

(其中S是激活函数softmax,g是矩阵乘法那一步)

Dg 是 TxNT 雅克比矩阵,而且Dg 大多数为0,仅当i=k时 D i j g k D_{ij}g{k} Dijgk不为0。如果把 [x1,x2,x3…xN]看成矩阵元素,记为 v,Dg 可以写成一个元素为行向量的对角矩阵,每个对角元素都是[x1,x2,x3…xN]。

在这里插入图片描述

v = [ x 1 x 2 ⋯ x N ] T D g = [ v T ⋱ v T ] v = \left[ \begin{matrix} x_1& x_2 & \cdots &x_N \end{matrix} \right]^T \\ Dg = \left[ \begin{matrix} v^T& & \\ & \ddots & \\ & & v^T \end{matrix} \right] v=[x1x2xN]TDg=vTvT

假设一个 v0 是一个 T 为向量,[x1,x2,x3…xN] 记为 v,且都视为列向量。
细心算一下就能发现,下式恰好成立:
v 0 T × [ v T ⋱ v T ] = = 一 维 展 开 ( v 0 × v T ) v_0^T \times \left[ \begin{matrix} v^T& & \\ & \ddots & \\ & & v^T \end{matrix} \right] == 一维展开(v_0 \times v^T) v0T×vTvT==(v0×vT)
所以Dg可以被替换成a,最后简化成了 BP4 公式。其他几个公式意思差不多。

PS:

[ v k 1 v k 2 ⋯ v k n ] × [ v j 1 T ⋱ v j 1 T ] = [ v k 1 × v j 1 T ⋯ v k n × v j 1 T ] ( 所 有 v 都 是 列 向 量 ) \left[ \begin{matrix} v_{k_1}& v_{k_2} & \cdots &v_{k_n} \end{matrix} \right] \times \left[ \begin{matrix} v_{j_1}^T& & \\ & \ddots & \\ & & v_{j_1}^T \end{matrix} \right] =\left[ \begin{matrix} v_{k_1} \times v_{j_1}^T & \cdots &v_{k_n} \times v_{j_1}^T \end{matrix} \right] (所有 v 都是列向量) [vk1vk2vkn]×vj1Tvj1T=[vk1×vj1Tvkn×vj1T](v)

附上用 scala 写的全连接层代码

import breeze.linalg.{DenseMatrix, DenseVector}
import breeze.stats.distributions.Gaussian

/**
  * 全连接层
  * @param inputSize 输入端维数
  * @param outputSize 输出端维数
  * @param fn 激活函数
  */
class FullConnectedLayer(inputSize: Int, outputSize: Int, fn: ActivationFunction) {
  var weight = DenseMatrix.rand[Double](this.outputSize, this.inputSize, Gaussian(0.0, 1.0))
  var bias = DenseVector.rand[Double](this.outputSize, Gaussian(0.0, 1.0))
  var inputVec: DenseVector[Double] = null //保留forward的输入
  var z:DenseVector[Double] = null //保留forward过程中z的计算结果

  /**
    * 前向计算
    * @param x 输入向量 a[i]
    * @return 激活结果 a[i+1]
    */
  def forward(x: DenseVector[Double]): DenseVector[Double] = {
    this.inputVec = x
    this.z = this.bias + (this.weight * this.inputVec)
    fn.forward(this.z)
  }

  /**
    * 反向传播
    * @param deltaNext delta_a[i+1]
    * @return delta_a[i]
    */
  def backward(deltaNext: DenseVector[Double]): DenseVector[Double] = {
    assert(this.inputVec != null && this.z != null)
    val fnDerivate = fn.derivate(this.z)
    val deltaZ = deltaNext * fnDerivate
    val deltaW = deltaZ * this.inputVec.t
    val deltaB = deltaZ
    val delta = this.weight.t * deltaZ
    this.weight += deltaW
    this.bias += deltaB
    delta
  }
}

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值