第二章:反向传播算法
上一章,我们看到了神经网络是如何通过梯队下降算法学习权重和偏移量。不过,我们跳过了如何计算成本函数导数这部分。这确实是一块不小的缺失。本章我将介绍一个计算此类导数的快速算法,名字叫反向传播。
反向传播算法最早在1970年代就引入了,但直至1980年代,那篇由 David Rumelhart, Geoffrey Hinton, and Ronald Williams发表的著名论文之后,才名声鹊起。这篇论文讲述了几种神经网络,用到的反向传播算法要比之前所有的方法在学习上都快很多。这样就使得通过神经网络解决许多之前无解的问题成为可能。如今,反向传播是神经网络学习中的顶梁柱。 相对于本书其它章节,这一章将会涉及到较多的数学知识。如果你对数学不感冒,你可能想跳过这一章节。毕竟,把反向传播当做一个黑盒,这样它的细节你完全可以不用关心。为什么要花时间学习呢? 原因当然是为了更深入的理解。反向传播算法的核心是一个偏导表达式∂C/∂w,它是代价函数C对所有的权重w(或者偏移量b)进行求导。这个等式告诉我们,在改变w或b的时候,代价函数的变化有多快。表达式虽然有些复杂,也有它美丽的一面,每个元素都有自然和直观的解释。所以,反向传播算法不仅仅是一个快速的学习方法。它也可以帮助我们了解,如何通过改变权重和偏移量来影响网络整体行为这个过程的细节。所以还是值得学习一下他的细节。 虽然如此,如果你想跳过这个章节,直接开始下一章节,也没问题。即使你把反向传播算法当做一个黑盒来理解,你在学习本书剩余部分的时候也没有任何问题。这本书稍后的部分可能会提到本章的一些结果。不过,对你应该没有障碍。
热身:用一个快速的基于矩阵的算法,来计算神经网络的输出
探讨反向传播算法之前,我们先热热身,先介绍一个快速的基于矩阵的方法,用它来计算神经网络的输出。实际上在上一张快结束的时候,我们已经看到这个算法的大概了,现在我们了解一下它的细节。这对于习惯反向传播算法中的各种符号是一个不错的方法。 我们从一个比较难的符号开始,他代表了第(l-1)层的第k个的神经元到第l层的第j个神经元之间连接的权重。例如,下图展示的是第二层中第四个神经元到第三层第三个神经元之间连接的权重。
一开始,看起来有些难懂,确实需要花些时间来掌握它。不过,花些功夫你就会发现它其实很简单和直观了。可能你会认为,用j来描述输入神经元用k描述输出神经元会更加直观一些。稍后我会解释为什么会是这样。
在标记网络中的偏移量和激活值时,我们会使用相同的逻辑。表示第l层的第j个神经元的偏移量,表示第l层第j个神经元的激活值。下图是一个例子:
基于这些标记,第l层第j个神经元的激活值和第l-1层神经元的激活值有关系,等式如下:
$$
这里对视对l-1层的所有k个神经元求和。为了使用向量重写这个等式,我们把定义为第l层的权重矩阵。权重向量wl的单个实体是连接l层所有神经元的权重。也就是说第j行第k列的权重是{jk}^l。类似,对于每一层的偏移量b,我们定义向量。你可以已经猜测出来了,偏移量向量的每一个实体为,它代表的是第l层的每一个偏移量。最后,我们定义激活值向量,它的每一个元素为。
我们要重写等式23的最后一个元素是向量化其中的函数,例如σ。上一章我们简单提到过,这里回顾一下。想法是对向量中每一个元素执行函数,使用来标记。也就是,的每一个组成部分。举个例子,如果函数。 向量形式的f函数效果如下:
$$\begin{pmatrix}
\begin{bmatrix}
2 \
3
\end{bmatrix}
\end{pmatrix}
=
\begin{bmatrix} f(2)\ f(3) \end{bmatrix}
=
\begin{bmatrix} 4\ 9 \end{bmatrix}, \qquad\qquad\qquad (24) $$
向量化的f函数会对向量中的每一个元素执行平方计算。
有了这些标记,等式(23)可以被重写为如下等式:
a_l = \sigma (w^la^{l-1} + b_l) \qquad\qquad\qquad (25)a_la^{l-1}$$中计算得来的:我们把l-1层的激活值乘以对应的权重,然后加上对应的第l层的偏移量。最后在计算σ函数。整体视角经常要比单个神经元视角看起来更简洁和容易。它可以帮助我们避免去理解可恶的角标,同时对算法保持精确的理解。向量化的表达式在实际操作中也非常有用,因为大多数矩阵库都提供了快速的矩阵的乘法加法和向量化。实际上,上一章的代码中,在计算网络行为的时候,我们已经悄悄使用了这个表达式。
使用等式25计算的时候,我们顺便计算了中间值 ≡ 。这个等式很有用,还是值得命名一下的:我们称为l层神经元的加权输入。本章中,我们会多次使用这个加权输入。等式25有时会以加权输入的形式书写,。 值得提到的一点是的元素模式为: 。 也就是说的元素它是l层第j个神经元激活函数的加权输入.
代价函数相关的两个假设
反向传播算法的目标是计算代价函数相对于任何一个权重和偏移量的的偏导数(∂C/∂w) 和 (∂C/∂b)。为了解释反向传播算法是如何工作的,我们需要做两个假设。我们举一个代价函数的作为例子。上一章在等式6中,我们提到过这个平方代价函数:
C=\frac{1}{2n}\sum_{x}^{ }\left | y(x)-a^L(x) \right |^2 \qquad\qquad\qquad (26)
n是训练集的数量,sum是对所有的训练集x求和,y=y(x)是期望的输出。 L代表神经网络的层级。 = 是当网络的输入是x时,对应的激活输出。 所以,为了执行反向传播算法,我应该对代价函数C做什么假设呢?第一个假设为代价函数可以写作每一个训练数据x对应代价函数求和的平均值:.这样,对于单个训练样本的代价函数为。这个假设对于本书中我们提到的其它所有代价函数都成立。
这么做假设的原因是,反向传播算法事实上是对每一个训练样本求偏导数 以及。然后, 以及 实际是所有训练样本计算之后的平均值。基于这些假设,假设训练样本x是确定的,去掉下标x,就可以把写作C。我们最终还是引入了x了,不过把它当做一个恼人的标记好了。
我们对代价函数所做的第二个假设也可以通过神经网络的输出函数的形式表达:
例如,二次代价函数符合这个要求,这样,单个训练样本的二次代价函数就可以写作:
C = \frac {1}{2} \left | y - aL \right |\ ^2 = \frac{1}{2} \sum{j}{ }(y_j - a_j^L)^2, \qquad \qquad \qquad (27) a_L$$的函数,而把y当做是函数中的一个参数就可以了。
哈达玛积(点积),$$s \odot t$$
反向传播算法基于一个通用的线性代数运算—-向量乘以矩阵,就像向量加法那样。 不过没矩阵加法那么常用。具体来讲,加入s和t是两个维度相同的向量。 我们使用记做两个向量的点积。的元素化写法为.下面是一个例子:
$$ \begin{bmatrix} 1\ 2 \end{bmatrix}
\odot
\begin{bmatrix} 3\ 4 \end{bmatrix} =
\begin{bmatrix} 13\ 24 \end{bmatrix}
=
\begin{bmatrix} 3\ 8 \end{bmatrix}
\qquad \qquad \qquad (28) $$
这种点乘有时也成为哈达玛积或者舒尔积。我们将称之为哈达玛积。优秀的矩阵库通常会提供高效的哈达玛积的计算,这对于计算反向传播算法会非常有帮助。
反向传播算法背后的四个基本等式
反向传播算法的意图是理解如何通过改变权重和偏移量来改变成本函数。最终,这就意味着计算偏导数以及。为了计算这个结果,我们首先引入一个中间值,。代表第l层第j个神经元的误差。反向传播算法会给出以及各计算误差的过程。然后,他们吧与{jk}^l 以及 关联起来。 为了理解误差是如何产生的,想想一下在神经网络中有一个小怪物:
小怪物坐在第l层的第j个神经元上。当输入神经元来到的时候,小怪物把神经元的运算搞的乱七八糟。它对神经元加权输入做了细微的改变 。所以,神经元的输出从变为了。 这个改变通过前向传播算法往神经网络的后续层级传播,最终造成了成本函数也发生了改变.
假如这个小怪物是个好的小怪物,它帮助你改善了代价函数,例如,它帮助你找到了一个使得代价函数变小了。假如的值非常大(无论是正值还是负值),那么小怪物通过选择与符号相反的,减少了代价函数一大块。相反,如果接近于0,就意味着小怪物没办法通过扰动加权输入改善成本函数。这样,小怪物就可以说神经元已经十分趋近最优化了。所以,基于经验判断,就是神经元误差的度量值。 基于这个故事,我们把l层神经元j的误差以下面的公式来定义:
习惯上,我们用\sigma^l来标记l层相关误差的向量。反向传播算法提供了一个方法,计算每一层的误差\sigma^l,然后把这些误差与实际参数(\frac{\partial C}{\partial w{jk}^l}以及\frac{\partial C}{\partial b_j^l}的量关联起来。
你看会琢磨为什么小恶魔会改变权重输入z_j^l。可能更加直观的是,小恶魔去改变输出激活值a_j^l,所以,我们使用\frac{\partial C}{\partial a_j^l}作为误差的度量。实际上,如果你这么做,结果会和下面讨论的内容十分相似。但是事实证明,这样的话,讲解反向传播算法在代数上就会变得有些复杂了。所以,我们仍然使用 \delta_j^l ≡ \frac{\partial C}{\partial z_j^l} 来度量误差。
解决这个问题的计划:反向传播算法基于四个基本的等式,通过它们就可以计算出来误差\delta ^l以及代价函数的导数。接下来我会把他们列出来。需要提醒你的是,不要想着能够瞬间理解这些等式。如果抱有这个期望,你会会有些沮丧。实际上,当你翻看更深一些的时候,会发现反向传播算法相关的等式相当丰富,理解起来需要花费不少的时间和耐心。不过好消息是,花费的这些耐心经常会给你带来回报。这节的探讨只是个开始,目的是帮助更深入地理解这些等式。
先简要介绍一下,在本章剩余部分,我将如何介绍这些等式更深入地细节:我将简短证明一下这些等式,以帮助大家理解为什么这些等式是成立的。然后,我们会用伪代码以算法的形式重写这些等式,这样将可以看到这些伪代码是可以被真正可以运行的Python代码实现。最后,我们将画出一个直观的示意图来解释这些反向传播算法到底是什么意思,以及人们是如何一步一步发现他们的。这个过程中,我们将时不时返回到这四个基本等式。随着你对这些等是理解的加深,你看到这些等式时会舒服一些,甚至会觉得它们很自然美丽。
输出层误差等式,\delta ^L: \delta^L的元素形式如下:
\delta^L = \frac{\partial C}{\partial a_j^L} \sigma{ }{ }’(z_j^L). \qquad \qquad \qquad (BP1) 这是一个非常自然的表达式。右侧的第一项,\frac {\partial C}{\partial a_j^L} 代表的是作为第j层输出激活值的函数,代价变化的速度。如果,例如 C和某一具体的输出神经元j依赖性不强,那么\delta_j^L就会比较小,这正是我们所期望的。右侧的第二个元素\sigma’(z_j^L)则代表激活函数在z_j^L处变化的速度。
请注意等式BP中的所有元素都很容易计算得来。在计算网络的行为时我们计算会得出z_j^L,有了它之后就不难计算得到\sigma{ }{ }’(z_j^L)。\frac{\partial C}{\partial a_j^L}的形式取决于代价函数的选取。不过,基于我们前面选取的代价函数,计算\frac{\partial C}{\partial a_j^L}就很简单了。例如,我们选取二次代价函数,C=\frac {1}{2}\sum_j(y_j - a_j^L)^2,所以\frac{\partial C}{\partial a_j^L} = (a_j^L - y_j),显然也是容易计算得来的。
等式(BP1)是\delta ^L的元素化表示。它是一个完美的表达式,不过我们想要的是反向传播算法是矩阵形式的。所以我们以矩阵形式重写它:
\delta ^L = \triangledown_a C \odot \sigma’(z^L). \qquad \qquad \qquad (BP1a) 这里\triangledown_a C定义为一下向量,它的元素是偏导数\frac {\partial C}{\partial a_j^L}。你可以把\triangledown_a C 想象为相对于输出激活值C的变化率。很容易看出来等式(BP1)和等式(BP1a)是相等的。所以,我们使用(BP1)来代表两个等式。例如,在二次代价函数中,\triangledown_a C = (a^L-y),所以,(BP1)的全部矩阵形式就变为:
$$\delta^L = (a^L) \odot \sigma’(z^L). \qquad \qquad \qquad (30) $$
这个等式中的每个元素都使用了漂亮的向量,当使用Numpy之类的库时,会十分容易计算。
一个误差\delta ^l与下一层误差\delta ^{l+1}之间关系的等式:
\delta^l = (w^{l+1})^T \odot \sigma’ (z^l) \qquad \qquad \qquad (BP2) 这里(w^{l+1})^T是l+1层权重矩阵w^{l+1}的转置矩阵。这个等式看起来有些复杂,不过每个元素都比较清晰易懂。假设我们知道了l+1层的误差\delta ^{l+1},然后我们对l+1层的权重矩阵进行转置(w^{l+1})^T,我们可以直观认为这是把误差在网络中往后传递。这样我们就有办法来衡量第l层的误差了。然后我们进行哈达玛德点乘\odot \delta{ }{ }’(z^l),就得到了第l层的误差\delta ^l,同时也是l层的加权输入,这样,我们通过激活函数就把误差传递到了l层。
把等式(BP1)和 (BP2)合并就可以计算网络中所有层的误差\delta^l。我们用等式(BP1)开始,计算得到了\delta^L,然后利用等式(BP2)计算得到了\delta^{L-1},在此应用等式(BP2)得到\delta^{L-2},等等,继续下去,就得以在网络中向后传播。
代价函数相对于偏移量的变化率: 等式如下:
\frac {\partial C}{\partial b_j^l} = \delta_j^l \qquad \qquad \qquad (BP3)
误差\delta_j^l 刚好等于代价函数的变化率 \frac {\partial C}{\partial b_j^l}。这是个好消息,因为(BP1)(BP2)已经告诉我们如何计算\delta_j^l 。我们可以将(BP3)重写为:
\frac {\partial C}{\partial b} = \delta \qquad \qquad \qquad (31)
可以理解为\delta和偏移量b取决于同一个神经元。
代价函数相对于网络中任意一个权重的变化率:
$$\frac {\partial C}{\partial w_{jk}^l} = a_k^{l-1}\delta_j^l. \qquad \qquad \qquad (BP4) $$
由于\delta^l 和 a^{l-1} 我们已经知道如何计算,这个公式告诉我们通过\delta^l和a^{l-1}可以计算得出\frac {\partial C}{\partial w_{jk}^l}。减少角标后,这个等式可以重写为:
\frac {\partial C}{\partial w} = a_{in}\delta {out}. \qquad \qquad \qquad (32)
这里,a{in}是权重w的输入侧神经元的激活值,而\delta{out}是权重w输出侧神经元的误差,两个神经元通过权重w链接,如下图: 这样,基于等式32我们知道,如果a{in}很小,a{in} \approx 0 那么导数\frac {\partial C}{\partial w} 的值也会很小。如果是这样,我们就认为这个权重学习很慢,也就是说它在梯度下降中变化不大。换句话说,由 BP4我们就可以知道地激活值神经元的加权输出学习比较慢。
通过(BP1)-(BP4),我们还可以得出其它推论。我们先从输出层来看,以(BP1)中的\sigma{ }{ }’(z_j^L)举例。回忆一下上一章中的sigmoid函数图,当\sigma(z_j^L)趋近于0或者1的时候,\sigma函数变得很平坦。所以,此时\sigma{ }{ }’(z_j^l) \approx 0。 我们可以推导出的结论是,当输出层神经元的激活值过高(\approx 1)或者过低(\approx 0)时,最后一层的权重将会学习得很慢。这种情况下,我们可以说神经元已经饱和,结果是权重的学习停止了(或者很慢)。对于输出神经元的偏移量的学习来说,同样适用。
前面的几层我们可以得到类似的推论。例如,注意到等式(BP2)中的\delta{ }{ }’(z^l)。这意味着当神经元接近饱和的时候,\delta_j^l会变得很小。这就意味着饱和神经元的所有加权输入的学习都很慢。
总结一下,我们已经了解到,当输入神经元的激活值比较低或者输出神经元接近饱和(无论是接近于1,过高,还是接近于0,过低),权重的学习就会变慢。
尽管上述结论并未带来惊喜,不过对于我们了解神经网络的学习原理也很有帮助。不仅如此,我们可以将这个逻辑推广开来。事实证明,这四个基础等式对于任何激活函数都是使用的,不仅仅是标准的sigmoid函数(原因是,这些等并未涉及\delta的任何属性)。 所以,当设计带有我们所期望学习属性的特殊激活函数时,我们也可以用这些等式。举个例子,加入我们打算选取一个非sigmoid激活函数\sigma,它的导数\sigma{ }{ }’永远为正,并且永远不趋于0。这样就可以避免标准sigmoid神经元的学习变慢的问题。稍后这本书中我们将会见到一个对激活函数进行此类修正的例子。记住这四个等式(BP1)-(BP4)可以帮助我们理解为什么会尝试这些改变,以及这些改变产生的影响是什么。
问题
反向传播算法的另外一种表达方式: 我们提到反向传播算法(主要是(BP1)和(BP2))使用到了哈达吗积。其实,基于传统的矩阵乘法,还有另外一个方法,或许有些读者可能会得多一些启发。
(BP1)可以重写为
\delta ^L = \sum{ }{ }’(z^L)\triangledown_a C, \qquad \qquad \qquad (33) 这里\sum{ }{ }’(z^L)是一个方阵,它的对角线元素是值\sigma{ }{ }’(z_j^L),并且不在对角线上的元素都为零。注意到这个矩阵与\triangledown_a C通过传统矩阵乘法相乘。
(BP2)可以重写为
$$ \delta ^l = \sum'(z^l)((w^{l+1})^T\delta^{l+1}). \qquad \qquad \qquad (34)
$$
- 结合(1)和(2)可以得出:
\delta ^l = \sum { }{ } ‘(z^l)(w^{l+1})^T … \sum{ }{ }’z^{L-1}(w^L)^T\sum{ }{ }’(z^L)\triangledown_a C \qquad \qquad \qquad (34)
对于熟悉矩阵乘法的读者来说,这么表达或许要比(BP1)和(BP2)跟容易理解。我仍然采用(BP1)和(BP2)的原因是,它们在实现数据计算的时候速度会更快一些。
四个等式的证明(可选)
现在我们来证明一下这四个基础等式。这四个等式都是多变量微积分基于链式法则计算的结果。如果你不反感链式法则,我强烈鼓励你在往下读之前自己尝试着做一些微积分。
我们从(BP1)开始,它是输出误差\delta ^L的表达式。我们回顾一下这个等式:
$$\delta_j ^L = \frac {\partial C}{\partial z_j^L}. \qquad \qquad \qquad (36) $$ 应用链式法则,我们可以把等式重写为如下形似:
\delta_j ^L = \sum_k \frac {\partial C}{\partial a_k^L}\frac {\partial a_k^L}{\partial z_j^L}. \qquad \qquad \qquad (37) 这里sum是针对所有输出层的神经元k。当然,当k=j的时候,第K个神经元的输出激活值a_k^L仅仅取决于第j层神经元的加权输入z_j^L。当k \neq j时,\frac {\partial a_k^L}{\partial z_j^L}消失了。所以可以简化为
\delta_j^L = \frac {\partial C}{\partial a_j^L} \frac {\partial a_j^L}{\partial z_j^L}. \qquad \qquad \qquad (38)
考虑到a_j^L = \sigma(z_j^L),等式右侧第二项可以重写为\sigma’(z_j^L),于是等式变为:
$$\delta_j ^L = \frac {\partial C}{\partial a_k^L} \sigma’(z_j^L) . \qquad \qquad \qquad (39) $$ 这个刚好是(BP1)的元素形式。
下面,我们将证明(BP2),给定了下一层的误差\delta {L+1}来计算本层的误差\delta L。我们需要将\delta_k^l = \frac {\partial C}{\partial z_j^l}写为\delta_k^{l+1} = \frac {\partial C}{\partial z_k^{l+1}}。可以通过链式规则:
\delta_k^l = \frac {\partial C}{\partial z_j^l} \qquad \qquad \qquad (40) = \sum_k \frac {\partial C}{\partial z_k^{l+1}} \frac {\partial z_k^{l+1}}{\partial z_j^l} \qquad \qquad \qquad (41) = \sum_k \frac {\partial z_k^{l+1}}{\partial z_j^l} \delta_k^{l+1}, \qquad \qquad \qquad (42) 最后一行,我们交换了等式右侧的两项,而且取代了\delta _k^{l+1}。为了评估第一行的第一项,注意到:
$$zk^{l+1} = \sum_jw{kj}^{l+1}aj^l+b_k^{l+1} = \sum_jw{kj}^{l+1}\sigma (z_j^l)+b_k^{l+1}. \qquad \qquad \qquad (43) $$ 做微分,可以得到:
$$\frac {\partial zk^{l+1}}{\partial z_j^l} = w{kj}^{l+1} \sigma’(z_j^l). \qquad \qquad \qquad (44) $$ 回头替换掉等式42我们得到:
$$\deltaj^l = \sum _k w{kj}^{l+1}\delta _k^{l+1}\sigma’(z_j^l). \qquad \qquad \qquad (45) $$ 这恰好是等式BP2的元素形态。
最后两个等式,BP3和、BP4,证明方法和上面两个等式类似,就留给大家作为练习吧。
练习
. 证明等式BP3和BP4
这样,四个基础等式的证明就完成了,看起来有些复杂,其实只用小心运用链式规则,就可以得出来。虽然不够简洁,我们可以把反向传播算法当做是计算代价函数导数的一种方式,它通过系统性地对多变量运用链式规则来完成计算。
反向传播算法
反向传播算法等式为我们提供了一个计算代价函数导数的方法。我们用一个明确的形式来表述他们:
- 输入 $$x:$$ 输入层对应的激活函数$$a^1$$
2.前向传播 对于每一个 l= 2,3,…,L,计算z^l = w^la^{l-1}+b^l 以及 a^l = \sigma(z^l).
输出值误差 \delta^L:计算向量\delta^l = \triangledown_aC \odot \sigma’(z^L).$$
计算后向传播误差: 对于每一个l=L-1,L-2,…,2,计算\delta^l = ((w^{l+1})^T\delta^{l+1})\odot\sigma’(z^l)
输出: 代价函数的导数为:\frac{\partial C}{\partial w_{jk}^l} = a_k^{l-1} 以及 \frac {\partial C}{\partial b_j^l} = \delta _j^l.
检查以下这个公式,我们局可以看到 为什么它叫做反向传播了。我们从最后一层开始,反向计算误差向量\delta^l。我们沿着网络的反方向进行计算看起来有些奇怪。不过,回想一下反向传播算法的证明过程, 代价其实是网络输出结果的函数,而反向移动则是这一事实的结果。为了理解代价是如何随着早期的权重和偏移量变化的,我们需要反复应用链式规则,从网络的反方向逐步获取可用的表达式。
练习
. 单个改变了的神经元的反向传播算法 假如在前向网络中我们修改了一个神经元,结果是神经元的输出为f(\sum_jw_jx_j+b),这里f不是sigmoid函数。此时我们改如何修改反向传播算法呢?
. 线性神经元的反向传播算法 在神经网络中,将常用的非线性\sigma函数用\sigma(z)=z来代替。重写反向传播算法。
如前所述,反向传播算法计算了每一个训练样本的代价函数,C=C_x。实际应用中,通常会把反向传播与学习函数例如随机梯度下降结合起来,这样就需要计算许多训练样本的导数。特别地,给定一个由m个样本组成的迷你小队,下列算法计算了基于这个迷你小队的梯度下降学习步骤。
输入一组训练集
对于每一个训练集 x:设定对应的输入激活值为a^{x,1},执行下列步骤: 前向传播: 对于每一个l=2,3,…,L,计算z^{x,l} = w^la^{l-1}+b^l 以及 a^{x,l} = \sigma(z^{x,l}).
输出误差: \delta^ {x,L}:计算向量\delta^ {x,L} = \triangledown _aC_x\odot \sigma’(z{x,l}).
反向传播计算误差: 对于每一个l=L-1,L-2,…,2,计算\delta^{x,l} = ((w^{l+1})^T\delta^{x,l+1})\odot \sigma’(z^{x,l})
梯度下降: 对于每一个l=L-1,L-2,…,2,根据等式w^l\rightarrow w^l-\frac{\eta}{m}\sum_x \delta^{x,l}(a^{x,l-1})^T更新权重值。根据等式b^l\rightarrow b^l - \frac{\eta}{m}\sum_x\delta{x,l}更新偏移量。
当然,为了实现梯度下降算法,你需要一个外部的循环来生成训练集的迷你小队,这个外部循环就形成了训练的世代,这里暂时略过。
反向传播算法代码
理解了反向传播算法的概念,我们现在就可以理解上一章中的实现反向传播算法的代码了。回顾一下上一章,这些代码包含在update_mini_batch以及backpro方法中,他们都在network类里面。把本章中讲述的算法直接翻译一下,就是这些方法的代码了。通过计算当前mini-batch训练集的梯度,update_mini_batch方法更新了network中的权重和偏移量。
class Network(object):
...
def update_mini_batch(self, mini_batch, eta):
"""Update the network's weights and biases by applying
gradient descent using backpropagation to a single mini batch.
The "mini_batch" is a list of tuples "(x, y)", and "eta"
is the learning rate."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
self.weights = [w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]
大部分工作是由deltanabla_b, delta_nabla_w = self.backprop(x, y)完成的,它使用了backpro方法来计算偏导数\frac {\partial C_x}{\partial b_j^l} 以及 \frac{\partial C_x}{\partial w{jk}^l} 。backprop方法正是依照上一节我们介绍的算法运算的。只有一个很小的改变:在使用标注层的时候,我们使用了稍微不同的下标。目的是为了利用Python的特性,名为列表倒序,用来从列表的尾部开始计数。例如l[-3]代表的是列表l的倒数第三个值。backprop的代码列在了下面,其中包含了一些辅助函数,用来计算\sigma函数及其导数\sigma’,以及代价函数的导数。基于前面讲到结论,你应该可以自己弄明白这些代码了。如果仍然有些困难,你可以咨询代码的原始介绍
class Network(object):
...
def backprop(self, x, y):
"""Return a tuple "(nabla_b, nabla_w)" representing the
gradient for the cost function C_x. "nabla_b" and
"nabla_w" are layer-by-layer lists of numpy arrays, similar
to "self.biases" and "self.weights"."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward
activation = x
activations = [x] # list to store all the activations, layer by layer
zs = [] # list to store all the z vectors, layer by layer
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
delta = self.cost_derivative(activations[-1], y) * \
sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# Note that the variable l in the loop below is used a little
# differently to the notation in Chapter 2 of the book. Here,
# l = 1 means the last layer of neurons, l = 2 is the
# second-last layer, and so on. It's a renumbering of the
# scheme in the book, used here to take advantage of the fact
# that Python can use negative indices in lists.
for l in xrange(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)
...
def cost_derivative(self, output_activations, y):
"""Return the vector of partial derivatives \partial C_x /
\partial a for the output activations."""
return (output_activations-y)
def sigmoid(z):
"""The sigmoid function."""
return 1.0/(1.0+np.exp(-z))
def sigmoid_prime(z):
"""Derivative of the sigmoid function."""
return sigmoid(z)*(1-sigmoid(z))
问题
. 完全基于矩阵的迷你小队反向传播算法
我们通过多个迷你小队来实现随机梯度下降算法,其实通过修改反向传播算法,可以通过这些迷你小队同时计算所有训练样本的梯度。方法是,我们以样本矩阵X=[x_1x_2…X_m]而不是样本向量x来开始计算,矩阵的列是迷你小队向量。我们将它乘以权重矩阵,加上合适的偏移量矩阵,然后在所有的地方应用sigmoid函数,这样就可以完成前向传播的计算。反向传播的计算是类似的。请明确写出这种方法下反向传播算法的伪代码。然后,据此修改network.py代码来完成完全基于矩阵的算法。这么做的好处是可以完全利用线性代数的现代库。结果是,他要比循环计算mini-batch要快很多。(在我的便携电脑中,例如,在运行上一章提到的MNIST分类问题时,大约加速了2倍。)实际应用中,所有的反向传播算法的库都是在使用完全的基于矩阵的方法,或者它的变种。
为什么反向传播算法是一个快的算法
为什么反向传播算法是一个快的算法呢?为了回答这个问题,我们来考虑另外一种计算导数的方法。想象一下,在神经网络研究的初期,大约1950或1960年代,你是世界上第一个尝试试用梯度下降算法进行学习的人。你需要找到一个方法来计算代价函数的导数。你回顾了一下你微积分的知识,并且打算使用链式规则来计算导数。折腾了一阵子后,你发现代数太复杂了,有些气馁。所以,你试图找其他的途径。你打算把当做是代价函数C=C(w)的唯一变量(稍后我们再讨论偏移量)。你把权重编号为w_1,W_2,…,对于权重w_j你打算计算\frac{\partial C}{\partial w_j}。一个容易想到的办法是使用约等式:
\frac{\partial C}{\partial w_j} \approx \frac{C(w+\epsilon e_j)-C(w)}{\epsilon} \qquad \qquad \qquad (46) $$
这里\epsilon > 0是一个很小的正数,e_j是第j个方向的单位向量。换句话说,我们可以通过两个稍微不同的w_j的代价C来估计\frac{\partial C}{\partial w_j}。然后应用等式(46). 运用同样的方法,我们可以计算得出\frac{\partial C}{\partial b}。
这个方法看起来很有大有希望。理解起来简单,实现起来也非常容易,几行代码就可以了。当然,比起利用链式规则来计算还靠谱。不行的是,尽管看起来很有希望,在用代码实现的时候,你就会发现它极端慢。为了理解其中的原因,假设你的神经网络中有100万个权重,对于每一个权重w_j,为了计算\frac{\partial C}{\partial w_j}, 我们都需要计算一遍C(w + \epsilon e_j)。这就意味着为了计算出导数,我们需要计算100万次代价函数,这就需要网络进行100万次的前向传播(对于每一个训练样本)。我们还需要计算C(w),所以,总数是100万+1次网络前向传播的计算。
反向传播算法的聪明之处在于,它只需要网络进行一次前向传播和一次反向传播,就可以同时计算出所有的导数\frac{\partial C}{\partial w_j} 。总体来说,反向传播的耗费的算力和两次前向传播相当,而基于等式(46)的算法,则需要100万加1次前向传播。所有,尽管反向传播算法看起来比等式(46)复杂,实际上,却快了许多。
人们在1986年第一次使用这个加速的方法,它大大地扩大了神经网络能够解决的问题的范围,从此,人们开始大量使用神经网络。当然,反向传播算法并不是万能的。也就是在1980年后期,人们就遇到了它的瓶颈,特别是试图使用反向传播算法计训练深度神经网络的时候,这些网络往往具有非常多的隐藏层。稍后这本书中,我们将介绍使用现代的计算机并配以更聪明的方法,是如何在深度神经网络中使用反向传播算法的。
反向传播算法:从总体来看
如我之前解释的那样反向传播算法留下了两个秘密。第一,算法到底在做什么?我们已经构建了一个误差从输出结果向后传递的图景。但是,我们可以更深入一些,针对这些矩阵和向量的乘法,建立一个更为直观的印象?第二个秘密是,人们当初是如何发现反向传播算法的?当然你可以理解算法的各个步骤,甚至能够对算法的证明而已了如指掌。但这不意味着你你理解得足够深入,你可以第一时间发现这个算法。是否存在有一个合理的线索,可以引导你发现反向传播算法?这一节,我们将揭示这两个秘密。
为了改善我们对算法机制的直观感受,让我们想想我们对网络中的一个权重w{jk}^l做了一个小的变化\Delta w{jk}^l:
这些改变一层一层传递下去,一直到最后一层,这就影响到了代价函数:
代价函数发生的变化\Delta C和权重发生的变化\Delta w_{jk}^l之间的关系如下:
\Delta C \approx \frac{\Delta C}{\Delta w_{jk}^l}\Delta w{jk}^l. \qquad \qquad \qquad (47)
这就意味着只要仔细跟踪w{jk}^l一个小的变化随着向前传播,了解到它是如何影响C的,就可以计算出\frac{\partial C}{\partial w{jk}^l}。这样,我们通过仔细列出传播中涉及到的所有表达式,配以容易计算的数值,就能够计算出来\frac{\partial C}{\partial w_{jk}^l}。
我们试着做一下,权重的细微变化\Delta w_{jk}^l造成了l层第j个神经元的激活值a_j^l的细微变化\Delta a_j^l,如下等式:
\Delta aj^l \approx \frac{\partial a_j^l}{\partial w{jk}^l} \Delta w{jk}^l. \qquad \qquad \qquad (48)
激活值的变化\Delta a_j^l会造成后续层所有神经元激活值的变化。例如第l+1层。我们据其中的一个受影响的神经元a_q^{l+1}作为例子,
实际上,它将引起如下变化:
\Delta a_q^{l+1} \approx \frac{\partial a_q^{l+1}}{\partial a_j^l}\Delta a_j^l. \qquad \qquad \qquad (49)
代入等式48中的表达式,可以得到:
\Delta aq^{l+1} \approx \frac{\partial a_q^{l+1}}{\partial a_j^l}\frac{\partial a_j^l}{\partial w{jk}^l}\Delta w_{jk}^l \qquad \qquad \qquad (50)
当然,\Delta aq^{l+1}也会引起其后层级神经元激活值的变化。事实上,w{jk}^l的改变引起了其后层级激活值的改变,这样一层一层传递下去,最终改变了代价函数,所以,我们可以将这个过程理解为因w_{jk}^l改变引起的C变化。如果这个过程经过的激活值为a_j^l,a_q^{l+1},…,a_n^{l-1},a_m^L,那么结果表达式为:
\Delta C \approx \frac{\partial C}{\partial am^L} \frac{\partial a_m^L}{\partial a_n^{L-1}} \frac{\partial a_n^{L-1}}{\partial a_p^{L-2}}…\frac{\partial a_q^{l+1}}{\partial a_j^l}\frac{\partial a_j^l}{\partial w{jk}^l} \Delta w_{jk}^l, \qquad \qquad \qquad (51)
也就是,对于经过的每一个神经元,我们都挑选了一个诸如\frac{\partial a}{\partial a}的式子,包括最后一个frac{\partial C}{\partial a_m^L}。等式(51)说明的是,一个激活值变化,通过某一条路径层层往后传递,最终引起的C值的变化。当然,会有许多路径会引起C值的变化,这里只是其中的一个路径。计算对C值总体的变化可行的,只要将介于这个变化的权重及最终代价值之间所有的路径求和就可以了:
\Delta C \approx \sum{mnp…q} \frac{\partial C}{\partial a_m^L} \frac{\partial a_m^L}{\partial a_n^{L-1}} \frac{\partial a_n^{L-1}}{\partial a_p^{L-2}}…\frac{\partial a_q^{l+1}}{\partial a_j^l}\frac{\partial a_j^l}{\partial w{jk}^l} \Delta w_{jk}^l, \qquad \qquad \qquad (52)
这里我们将所有可能的路径经过的中间神经元进行了求和。对比等式(47),我们可以看到:
\frac{\partial C}{\partial w{jk}^l} = \sum{mnp…q} \frac{\partial C}{\partial am^L} \frac{\partial a_m^L}{\partial a_n^{L-1}} \frac{\partial a_n^{L-1}}{\partial a_p^{L-2}}…\frac{\partial a_q^{l+1}}{\partial a_j^l}\frac{\partial a_j^l}{\partial w{jk}^l}, \qquad \qquad \qquad (53)
等式(53)看起来有些复杂,不过他却很直观。
我们通过网络中的权重来计算C的变化率,等式告诉我们,网络当中两个神经元之间的边缘(edge)与比例因子(rate factor)有关,它恰好是其中一个神经元机的激活值相对于另外一个神经元激活值的偏导数。从第一个权重到第一个神经元的边缘包含一个比例因子\frac{\partial aj^l}{\partial w{jk}^l}。一条路径的比例因子恰好是这条路径所有比例因子的产品???。而且,整体改变率\frac{\partial C}{\partial w_{jk}^l}刚好是从初始权重到最终代价函数之间所有路径比例因子之和。这个过程中的一条路径,可以示例如下:
以上所述,其实是经验式的论点,提供了一种思考方法,可以帮助我们理解在网络中,如果对权重进行一个小小的改动,会发生什么。我来画一个轮廓线,你可以用来做更深入的探讨。首先,通过等式(53)你可以得到所有单个偏导数的等式,通过一点微积分,还是很容易做到的。完事儿后,你可以试着弄清楚不用角标只用矩阵乘法来重写这个求和。你会发现这很让人心烦,需要大量的坚持和耐心,不过倒不是特别难以理解。做完所有这些,然后尽量简化,你就会发现,你得到的正是反向传播算法。所以,你可以把反向传播算法当做是一种计算这些路径所有比例因子之和的过程。或者换一种稍微不同一些的说法是,反向传播算法是一种聪明的方法,用来跟踪权重(偏移量)的小扰动是如何在网络中传播,到达输出层并最终影响代价函数。
我不打算在这里详细讲述这部分,这些细节工作繁杂需要非常细心。如果你想挑战一下自我,你可以去尝试一下,弄不好你真的很喜欢呢。即使你对此无感,我也希望你能据此了解更多反向传播算法是如何工作的。
那其它的秘密呢?比如,反向传播算法当初是如何被发现的?如果你按照我刚才的概述,你就可以验证反向传播算法。不幸的是,整个验证的过程比较长而且要比本章开头的描述更为复杂。 所以,简短的验证是如何发现的呢?当你写出了长验证的所有细节之后,你会发现仍然有几个明显简化会忍不住要完成。等你做完简化,得到了更短的验证,写出来。然后,又有几个明显的简化跳了到你面前。你继续简化。一直下去,经过几轮简化,你就会发现我们之前证明的等式。简短,但有些难以理解。所有建立等式时用到的路标都被去掉了。这点,请你相信我。在早期,证明的起源确实没有什么高级的秘密可言。只是基于这一节我提到的证明,进行许多艰苦的简化而已。