反向传播算法简介

2019-08-26
10 min read

本文参考:

  1. 《Python神经网络编程》
  2. http://neuralnetworksanddeeplearning.com/chap2.html

BP (Backpropagation algorithm) 算法所关注的是神经网络中损失函数 C (cost function) 与每一个权重 \(w\) 和偏置 \(b\) 的偏导。BP 不仅仅是一个快速的算法,其同时也为我们提供了一个视角,让我们观察权值和偏置是如何影响网络输出的

BP 算法的目的在于调整各层之间的权重(以及偏置)以减少输出层的误差,训练过程中输出层的输出值与目标值都是已知的,所以输出层的误差可以直接求得。为了减少输出层的误差,我们有两种手段

  1. 调整输出层与前一层神经元之间的权重

    1. 从函数的角度上来讲,以 \(y=f(x)\) (假设 \(x\) 是前一层的输出,\(y\) 为输出层输出)为例,调整权重就是在调整 \(f\),在相同的 \(x\) 前提下修改输出 \(y\) ,在神经网络的训练过程中,对应的是梯度下降法
  2. 调整输出层前一层的输出

    1. 这个过程就是在修改 \(y=f(x)\) 中的 \(x\) ,从而改变输出 \(y\) ,这个过程对应的是误差的反向传播,只有知道了误差才知道如何调整 \(x\)

    2. 在已知输出层误差的前提下,前一层输出应该做怎样的修改也是已知的(无非是增加或减小 \(x\)),可以把这个已知量作为前一层的误差,从而修改前一层与前一层上一层的权重,如此反复可以修改整个网络的权重。这是反向误差传播的核心思想,为了调整上一层的输出,唯一的办法是调整上上层的权重与偏置

      与输出层相连的隐藏层 hx,其每一个节点都和输出层的每一个节点相连,所以 hx 层每个节点输出值的变动会影响输出层的每一个节点。输出层误差反向传播的时候会将输出层所有节点的误差全部反馈给前一层的任意一个节点,这个过程最直白的方法由 \(e_{hx} = W_{hx\_output}^T\cdot e_{output}\) 完成

思考网络模型时把主要关注点放在权重上而非神经元上。神经元的输出是临时的,只有在有输入的时候神经元才会有输出,但权重却会一直存在,无论网络是否载入内存,网络是否在进行前向计算

[TOC]

前置知识

梯度下降算法的基础是下面的公式,核心思想是导数与梯度:

\[v \rightarrow v^{\prime}=v-\eta \nabla C \]

上式中 \(v\) 是需要调整的网络参数。就像很多深度学习教程中讲的那样,在三维空间中一个山谷形状的函数(例如:\(z = x^2+y^2\)),每次依照梯度反方向调整参数,最终会到达谷底(也许不是全局最优解而是一个局部最优解)

根据上面的公式,每次更新参数我们都需要求解每一个参数对于 \(C\) 的导数,特别是使用数值法优化网络,每个导数还需要计算两次函数的输出

\[\frac{\partial y}{\partial x}=\frac{y(x)-y(x-\Delta x)}{\Delta x} \]

海量的计算让使用上面的函数来优化网络变得不切实际,故需要 BP 算法来减少计算量

反向传播示例

层之间权重的调整

以 sigmoid 激活函数为例,输出层与前一层某一个连接权值偏导如下(参考):

\[\frac{\partial E}{\partial w_{j k}}=-\left(t_{k}-o_{k}\right) \cdot \operatorname{sigmoid}\left(\Sigma_{j} w_{j k} \cdot o_{j}\right)\left(1-\operatorname{sigmoid}\left(\Sigma_{j} w_{j k} \cdot o_{j}\right)\right) \cdot o_{j} \]

\(t_k\) 为期望输出;\(o_k\) 为网络输出;\(o_j\) 为前一层第 \(j\) 个元素的输出。权重调整的矩阵形式为:

\[\Delta W_{j, k}=\alpha \cdot E_{k} \cdot O_{k}\left(1-O_{k}\right) \cdot O_{j}^{T} \]

上面公式与下面公式相同,用于权重更新:

\[\text { new } w_{j, k}=\text { old } w_{j, k}-\alpha \cdot \frac{\partial E}{\partial w_{j k}} \]

误差反向传播之后,获得误差的那一层可以看作是输出层,不断的反向传播误差可以调整所有层之间的权重

误差的反向传播

本文一直在重复,优化神经网络就是在调整权重和偏置值,最后一层的权重和偏置值的调整方法很明显,因为最后一层的损失值很好计算,网络输出减去期望就可以得到最后一层的误差。最后一层之外那些层的权值如何调整呢?如果想调整这些层的权值,我们需要获得这些层的输出误差。因为初始条件下我们只知道最后一层的误差,所以我们需要将最后一层的误差往其他层传播,其他层在获得这些误差之后就可以调整权值了,这是**“反向”**的由来

最简单的误差反向传播方法:

\[e_{hx} = W_{hx\_output}^T\cdot e_{output} \]

\(e_{hx}\) 是与输出层相连的隐藏层误差;\(e_{output}\) 是输出层的误差,这是已知的;\(W_{hx\_output}\) 是输出层与其相连的隐藏层的权重矩阵,误差反向传播的时候需要转置并与输出层误差相乘从而得上一层隐藏层误差

上面的公式比较直白,直接按照权重将输出层误差加权转化为前一层每一个神经元的误差,没有考虑激活函数的链式导数,但在整体趋势上,上面公式可以降低网络输出层的误差,实现了网络优化的目的

从后向前依次将隐藏层看作输出层并利用上面提及的权重调整与误差传播方法可以优化整个网络的权重与偏置

下文有一个考虑更为细致的误差反向传播方法,也是网络优化中最常用的误差反向传播方法:

\[\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma^{'}(z^l) \]

\(l\) 层对网络的影响(\(\delta^l\))由第 \(l+1\) 层对网络的影响乘以这两层之间的权值,这其实就是导数的链式法则

激活函数

感知机可以认为是第一代神经网络,感知机神经网络使用的激活函数是阶跃函数,阶跃函数的特点是微小的输入可能造成输出巨大的变化(翻转),从而不利于网络的学习;

只能处理线性问题是感知机的另一个特点,因为这个特性感知机在发现之后沉寂了很久一段时间,直到新的激活函数加入到网络中,比如 sigmoid 等激活函数,新激活函数为网络引入了处处可导的特性,为上面的 BP 算法提供了应用基础

利用矩阵实现网络计算

先介绍一种网络权重的数学标记法:\(w_{jk}^l\),这个数学标记表示神经网络中第 \(l\) 层的第 \(j\) 个元素和第 \(l-1\) 层第 \(k\) 个元素之间的权重。同样,\(b_j^l\) 表示网络第 \(l\) 层第 \(j\) 个元素的偏置值,\(a_j^l\) 表示 \(l\) 层第 \(j\) 个元素的激活函数输出值。利用这种数学标记法,\(a_j^l\) 可以表示为:

\[a_j^l = \sigma(\sum_k w_{jk}^l a_k^{l-1} + b_j^l) \tag{1} \]

其中 \(\sigma(x)\) 为神经元的激活函数,使用矩阵形式表示上述表达式:

\[a^l = \sigma(w^l a^{l-1} + b^l) \tag{2} \]

定义 \(z^l = w^l a^{l-1} + b^l\) 为神经元激活函数的输入值则可以将上面表达式\((2)\) 表示为:

\[a^l = \sigma(z^l) \tag{3} \]

Hadamard 积,\(s \odot t \)

Hadamard 积(哈达玛积)表示矩阵按对应元素做乘法:\((s \odot t)_j = s_j t_j\)

损失函数的两个特点

为了使 BP 算法正常运行,损失函数需要满足两个条件。在给出这两个条件前,我们先介绍一种常用的均方差损失函数,如式\((4)\)所示:

\[C = \frac 1{2n} \sum_x \Vert y(x) - a^L(x) \Vert ^2 \tag{4} \]

表达式 \((4)\) 中变量分别为:\(n\) 是训练网络的样本个数;\(y(x)\) 是训练样本 \(x\) 的期望值(也就是样本的标签值);\(L\) 表示网络的层数;\(a^L = a^L(x)\) 是网络在输入为 \(x\) 时输出层的输出

现在描述我们对损失函数的要求。首先,损失函数可以写成所有训练样本损失值均值的形式:\(C = \frac {1}{n} \sum_x C_x\)

我们做上面的要求是因为训练的过程中我们常常使用批训练的方式,而不是每次只使用一个样本训练网络。批训练之后我们求当前批次样本损失值的平均数来更新权重和偏置,所以损失函数要满足叠加定理

其次,损失函数可以使用网络输出层的输出作为参数:\(C = C(a^L)\)\(a^L\) 是网络输出层的输出,如果不满足这个要求我们将不能定量分析网络的性能(因为无法计算网络的损失值)。以均方差损失函数为例,当样本为 \(x\) 时,网络的损失值为:

\[C = \frac 1 2 \Vert y-a^L \Vert ^2 = \frac 1 2 \sum_j(y_j-a_j^L)^2 \tag{5} \]

上式中所有元素的值都是已知的,\(y\) 是标签、\(a_j^L\) 是网络输出层的输出

BP 算法所依赖的四个方程

参考资料:http://neuralnetworksanddeeplearning.com/chap2.html

BP 算法用于计算网络中权值与偏置关于网络损失值的偏导,也就是计算:\(\frac{\partial C}{\partial w_{jk}^l}\)\(\frac{\partial C}{\partial b_j^l}\) 。在计算偏导前我们先引入一个中间变量 \(\delta_j^l\),这个变量表示网络第 \(l\) 层第 \(j\) 个神经元激活函数的输入值(\(z_j^l\))对整个网络损失的影响。BP 算法可以帮我们计算出 \(\delta_j^l\) ,然后我们就可以通过 \(\delta_j^l\) 得到\(\frac{\partial C}{\partial w_{jk}^l}\)\(\frac{\partial C}{\partial b_j^l}\)

如果\(\frac {\partial C} {\partial z_j^l}\) 的值不为0,那么调整 \( z_j^l = z_j^l - \eta \frac {\partial C} {\partial z_j^l} \Delta z_j^l\),将减小整个网络的损失值(\(\eta\) 是学习率,是个比较小的小数)。如果 \(\frac {\partial C} {\partial z_j^l}\) 的值为0,因为导数为 0,再大的调整对网络都没有影响(这里暂不考虑舍入误差)

我们定义 \(\delta_j^l\) 如下:

\[{\delta}_j^l \equiv \frac {\partial C} {\partial z_j^l} \tag{6} \]

\((6)\)的矩阵表示方法为:\(\delta ^l\)。BP 算法可以帮助我们计算网络中每一层的 \(\delta ^l\)

BP 算法所依赖的四个方程总结如下,后面依次解释

  1. 输出层损失计算: \(\delta^{L}=\nabla_{a} C \odot \sigma^{\prime}\left(z^{L}\right)\)
  2. 上一层损失值计算:\(\delta^{l}=\left(\left(w^{l+1}\right)^{T} \delta^{l+1}\right) \odot \sigma^{\prime}\left(z^{l}\right)\)
  3. 偏置对网络的影响:\(\frac{\partial C}{\partial b_{j}^{L}}=\delta_{j}^{l}\)
  4. 权值对网络的影响:\(\frac{\partial C}{\partial w_{j k}^{\prime}}=a_{k}^{l-1} \delta_{j}^{l}\)

BP1:输出层损失值计算

网络输出层 \(\delta ^L\)的计算方式为:

\[\delta_j^L = \frac {\partial C}{\partial a_j^L} \sigma{'}(z_j^l)\tag{BP1} \]

BP1 等号右侧偏导部分表示网络输出层最后的输出对网络损失的影响强度,\(\sigma\) 的导数表示这个神经元输入对整个网络损失的影响强度(下有证明)

需要注意的是对于最后一层而言,BP1 的结果是很容易计算的。\(\frac {\partial C}{\partial a_j^L}\)依赖于损失函数的形式。举个例子,如果我们使用式\((5)\)中的均方差作为最终的损失函数,那么\(\frac {\partial C}{\partial a_j^L} = (a_j^L - y_j)\)

以矩阵的形式表示\((BP1)\)

\[\delta^L = \nabla _aC \odot \sigma^{'}(z^L) \tag{BP1a} \]

如果损失函数还是均方差的话,那么 \(\nabla _aC = (a^L-y)\),从而可得:

\[\delta^L = (a^L-y) \odot \sigma^{'}(z^L) \tag{8} \]

BP2:使用 \(\delta ^l\) 计算 \(\delta ^{l-1}\)

先给出公式:

\[\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma^{'}(z^l)\tag{BP2} \]

\((w^{l+1})^T\)是网络第 \((l+1)\) 层权值矩阵的转置。结合\((BP1)\)\((BP2)\),我们可以计算出网络中所有层的\(\delta ^l\)

如果将神经网络的每一层都看做一个函数 \(y = f(x)\)\(x\) 为当前层的输入, \(y\) 是当前层的输出,则输入对输出影响的强度即为函数的导数:\(\frac {d y}{d x} = f^{'}(x)\)

神经网络一般有很多层,每一层的输入都是前一层的输出(这里只考虑简单的网络,每一层的输入只和相邻的前一层相关),那么一个含有两个隐藏层的网络可以用函数 \(y = f(g(x))\) 表示。其中 \(x\) 是整个网络的输入,\(g\) 表示第一层网络,\(f\) 表示第二层网络,\(y\) 为整个网络的输出。

在已知第二层网络输入的前提下,\(\delta ^{layer 2} = f^{'}(in_{layer 2 input})\)。在已知网络第一层输入 \(x\) 的前提下,求 \(\delta ^{layer 1}\)需要使用微积分中的链式求导法则,即:

\[\delta ^{layer 1} = f^{'}(g(x))g^{'}(x) \tag{11} \]

\((11)\)中所包含的思想和式\((10)\)是相同的,在已知 \(x\) 的前提下 \(g(x)\) 也是已知的。因为 \(f\)\(g\) 的函数形式是已知的故其导数形式也是已知的。

综上所述,所有层的 \(\delta ^l\)都是可以通过链式求导法则进行计算的

BP3:偏置值对网络损失值的影响

网络中偏置值 \(b_j^l\)的变化对网络损失值的影响可以使用如下表达式进行计算:

\[\frac {\partial C}{\partial b_j^l} = \delta_j^l\tag{BP3} \]

结合\((7)\)可证式\((BP3)\)

\[\frac {\partial C}{\partial b_j^l}= \frac {\partial C}{\partial (\sum_k w_{jk}^l a_{k}^{l-1} + b_j^l)} = \frac {\partial C}{\partial (z_j^l)} = \delta_j^l \tag{12} \]

从式\((BP3)\)可知,我们可以使用 \(\delta_j^l\) 来计算偏置值关于损失函数的梯度

BP4:权值对网络损失值的影响

\[\frac {\partial C}{\partial w_{jk}^l} = a_k^{l-1}\delta_j^l\tag{BP4} \]

\((BP4)\)告诉我们,我们可以使用前一层网络的输出和 \(\delta_j^l\) 来计算权值关于损失函数的梯度,而这些值都是已知的

观察上面几个方程,对于输出层而言,如果 \(z_j^L\) 非常大且我们使用的激活函数为 \(sigmoid(x) = \frac {1}{1-e^{-x}} \),那么\(\sigma^{'} \approx 0\),此时\(\delta ^L \approx 0\),网络是无法更新权重与偏置的,即网络失去了学习能力

随着网络层数的增加,位于左侧的层其权值与偏置也将非常难以更新,因为 \(a_k^{l-1}\delta_j^l\) 值向左传播的过程中会越来越接近于0。因此,好的激活函数对网络的训练是有益的,而且网络的层数也不是越多越好,跨层连接(如ResNet)对网络的训练也是有益的

反向传播算法的实现

上小节中的四个方程向我们提供了一个计算网络权值和偏置关于网络损失值梯度的方法,下面描述下这个方法的具体过程:

  1. 输入 \(x\) :设置输入层的值
  2. 前向传导:每一层的输入和输出分别为:\(z^l = w^l a^{l-1} + b^l\)\(a^l = \sigma(z^l)\)
  3. \(\delta ^L\) ,输出层关于损失值的梯度:\(\delta ^L = \nabla_a \odot \sigma^{'}(z^L)\)
  4. 反向传播误差: \(\delta^{l} = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^{l})\)
  5. 获得权值和偏置关于损失函数的梯度:\(\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_j\)\(\frac{\partial C}{\partial b^l_j} = \delta^l_j\)
Previous TC++PL