TensorFlow官方教程《Neural Networks and Deep Learning》译(第二章)
反向传播算法(BackPropagation algorithm)的工作原理
在上一章节中, 我们看到了神经网络是如何利用梯度下降算法从数据中学习权重值 weights 和 偏倚量值 biases ,完成“学习” 的过程。 但是, 在算法原理的解释中, 有一个环节被跳过了: 我们没有讨论如何计算成本函数(cost function) 的梯度值。 这是很重要的一个环节! 在这一章, 我们会解释一个快速计算这种梯度的算法, 名为“反向传播算法”(BackPropagation algorithm)。
反向传播算法原本是 19世纪70年代提出的, 但是其重要性直到 1986 年的一篇由(David Rumelhart , Geoffrey Hinton, Ronald Williams 三人所写的一篇著名论文发布以后才真正被世人意识到。这篇论文描述了几个神经网络在使用了反向传播算法以后, 学习速度比之前有显著提升, 使得用这些神经网络来解决以前看上去不可解的问题成为可能。 如今, 反向传播算法已经成为了神经网络进行学习的“核心骨干”算法。
本章节与本书的其余章节相比涉及了更多数学上的内容。 如果你对数学不是很热衷, 你可能会想要跳过这一章节, 然后把反向传播算法当做一个忽略其内部细节的黑盒来使用。 但是为什么我们要花费时间来学习这些细节呢?
原因当然是为了“弄懂”。 反向传播算法 核心是一个成本函数
虽然说了上面这一大堆, 如果你依旧只是想略读这一章,或是直接跳过这一章, 也是ok的。 本书的其余章节在写作时, 已经考虑到了你有可能跳过本章, 直接将反向传播算法当做一个黑盒的情况。所以即使你不略过了本章, 你依旧可以理解后续章节的主要结论, 尽管你可能无法完全跟的上其中的推导过程。
热身: 一个快速的基于矩阵计算神经网络输出的方法
在讨论反向传播算法前, 让我用一个基于矩阵计算神将网络输出的算法热个身。 事实上, 在上一章结尾, 我们已经初步见识过这一算法了, 但是当时我快速的一带而过了, 所以值得再重新探究一下其中的细节。 这是一个让我们逐渐熟悉反向传播算法中所使用数学记号的很好的方式。
让我们从一个让人感到含糊不清的指代神经网络众多权重的记号开始。 我们会用
这个记号乍一看是让人很不习惯的, 它确实需要花费一些功夫来掌握。 但是经过较小的努力之后, 你会发现这个记号变得简单和自然起来。 这个记号的一个古怪之处在于
我们使用一个类似的记号来表示神经网络的偏倚量(biases)和**结果( activations) 。我们用
利用这些记号, 第
上面等式中的
等式 (23) 中最后一个需要重写的部分就是把
向量化后的
有了这些记号, 等式 (23) 就可以被重写为非常漂亮可以被重写为漂亮且紧凑的向量形式:
这个表达式给了我们一个从全局角度思考一层神经元的**结果是如何与前一层神经元的**结果进行关联的:* 在**结果上乘以权重矩阵, 然后与偏倚量向量相加, 然后再应用
*注释:
这种全局的视角与之前以神经元为单位的思考更简单, 更精炼(同时涉及到更少的上下标)。 把它当做是一种避免繁琐上下标, 且不失准确性的表达技巧。 这种表达式在实践中也很有用, 因为大部分的矩阵运算库提供了矩阵乘法, 向量加法, 向量化的快速实现方法。 事实上, 上一章节的代码
已经隐含地使用了上面那个表达式来计算神经网络的行为。
在使用表达式【25】来计算
两个我们所需的关于成本函数的假设
反向传播函数的目标是计算成本函数
我们会使用上一张提到的二次成本函数作为示例(等式【6】)。 在上一节的记号中, 二次成本函数有如下的形式:
其中:
好的, 那么我们为了成本函数
我们之所以需要这个假设, 是因为反向传播让我们做的实际上是计算单个输入的偏导数
我们所需要做的第二个假设是, 成本可以被写作神经网络输出的一个函数计算结果:
例如, 二次成本函数就满足这个要求, 因为单个输入
而且因此, 也就是**结果输出的一个函数。 当然, 这个成本函数的结果也取决于目标输出
Hadmard 积, s⊙t
f反向传播算法是基于常见的线性代数运算的- 如向量加法, 向量与矩阵的乘法等等。 但是, 有一个运算符没有那么常见。 具体而言, 假设
的内容就是
这种按元素相乘的乘法有时被称为 Hadmard(哈达玛达 )乘积 或是 Schur 积, 这个运算符在实现反向传播算法时会显得很好用。
反向传播算法背后的四个基本等式
反向传播算法帮我们理解神经网络中权重和偏倚量的变动是如何影响成本函数结果变化的。 最终, 是通过计算偏导数
为了理解这个偏差值是如何被定义的, 先想象我们的神经网络中有一个恶魔:
这个小恶魔坐在第
。
译者注: 最后的这个改变量是一个偏微分表达式, 忘记的同学可以复习一下大学课本。
现在这个恶魔变成了一个好的恶魔, 并且试图帮助你来改善成本值, 也就是说, 他们将试着找到一个
受到这个故事的启发, 我们定义第
按照我们的惯例, 我们用
你可能在想,为什么小恶魔会更改权重化输入
【】注释: 在类似 MNIST 类似的分类问题中, 术语 “误差”有时被用来表示分类失败率。 例如, 如果神经网络正确的识别了 96%的数字,那么误差就是 4% 。 显示这和我们的误差向量
攻击计划: 反向传播算法是基于 4 个基础等式。 这些等式合起来给了我们一个计算误差
这里先预览一下我们在本章后面的部分将要深入钻研方程的路线:我会给一个等式的简短证明,以解释这些等式为什么成立; 我会将这些等式以伪代码的算法形式再次列出, 看这些伪代码如何被实现为真正的, 可运行的 python 代码。 然后, 在这一章的最后一节, 我会构建一个反向传播算法等式含义的直觉图, 以及一个怎样能够从无到有的发现这个等式。 沿着这条路线, 我们将会反复地审视这四个基础等式, 而你会不断加深对它们的理解, 最终你会熟悉习惯这四个等式,甚至可能发觉这四个等式是漂亮且自然的。
首先是一个关于输出层误差量的等式
这是一个非常自然的等式, 等式右侧的
注意到 (BP1 ) 中的每个部分都是易于计算的。 具体来说, 我们在计算神经网络的行为时, 会计算
译者注: 又需要复习一下高数里的微分部分。
等式 (BP1) 是一个关于
这里,
如你所见, 这个表达式中的每个部分都有一个非常简洁的向量形式, 而且很容易使用类似 Numpy 的库进行计算。
等式 (BP2) 是是关于误差量
其中,
译者注: 这里补充一个例子, 来说明为什么是反向移动了一层。
下面假设我们有一个神经网络结构,
从上例可以看出,
然后我们把误差反向移动后的结果与
通过合并(BP_2) 和 (BP_1) 两个等式, 我们可以计算出任意一层的误差量 。 我们可以先用(BP_1) 算出
等式(BP3) 是一个 以神经网络中任意偏倚量为参照的成本函数改变速率 有关的等式:
这个等式说明, 偏差量
等式中, 误差
等式(BP4) 是一个 以神经网络中任意权重为参照是成本函数的改变速率 相关的等式:
这个等式告诉我们如何利用我们已经知道怎么计算的
这个等式中,
等式 (32) 有一个很好的推论是, 当**量
沿着这四大基本方程, 很有很多其他的结论。 然我们从输出层开始, 考虑等式(BP1) 中的
当
从前面的神经元层中, 我们也可以获得类似的见解。具体来说, 可以注意观察一下 等式(BP2)中的
注: 之所以说有可能会变小, 是因为考虑到
总结一下, 我们从之前的分析中了解到, 无论是输入神经元处于低**态 或是输出神经元处于饱和态(低**态或高**态), 一个权重的学习过程都会变缓慢。
这些发现都没什么让人感到惊讶的地方。 他们依旧只是帮我们改善我们头脑里神经网络的模型, 以更好地理解神经网络是如何学习的。 进一步, 我们可以把这类的推理进行推广。 之前列出的四个基本等式不仅针对标准的 S型函数成立, 对任意类型的**函数也是成立的(这是因为, 等式的证明过程中没有使用任何独属于 S型函数的特殊性质, 这个证明过程我们之后会看到)。 因此我们可以使用这些等式来设计一些具备我们所期待性质的**函数。 下面通过一个例子来进行说明, 假设我们选择了一个(非S型)**函数
问题:
修改反向传播函数的等式: 我已经使用 Hadmard 乘积的方式展示了反向传播函数的等式(BP1 和 BP2)。 这种展示形式可能让不熟悉 Hadmard 乘积符号的人感到不习惯。 其实还有一种使用矩阵乘法表示的方法, 一些读者可能会对此更加熟悉。
(1) 尝试证明 (BP1) 可以被重写为下面的形式:
其中
- (2)尝试证明(BP2) 可以被重写为
δl=Σ′(zl)(wl+1)Tδl+1.(34) - (3) 把 (1)和 (2) 的结果合并证明:
对于熟悉矩阵乘法的读者来说, 这些等式可能比 (BP1) 和 (BP2) 更容易理解。 而我专注于 (BP1) 和 (BP2) 的原因是, 这种表示方法的数学实现起来更快。
四个基础等式的证明(选读)
我们现在来证明四个基础等式(BP1)- ( BP4 ). 这四个等式都是多变量微积分链式法则的结果。 如果你熟悉链式法则的话, 我强力建议你在阅读前尝试自行推导。
让我们从等式(BP1) 开始, 该等式给我们提供了一个关于输出误差的表达式
应用链式法则, 我们可以将上面的偏导数重新表达为与输出变量有关的形式。
其中, 求和函数是针对输出层的所有神经元 k 进行的。 显然, 当 k = j 时, 第 k 个神经元的输出**量
回想起
这恰恰就是(BP1) 的成分表达形式。
下面我们将会证明 (BP2), 该等式为我们提供了前后两层的误差量
在最后一行, 我们交换了 (41) 中两项的位置, 然后根据
进行微分以后, 我们得到:
这恰恰就是 (BP2) 的成分形式。
我们最后想要证明的等式是 (BP3) 和 (BP4) , 这些也是使用链式法则证明的, 和前两个证明的方法类似。 我把后面的证明留给读者作为一个小练习。
$$练习题
- 证明等式(BP3) 和 (BP 4)
至此就完成了反向传播的四个基本等式的证明。 证明过程看上去好像比较复杂, 但是它确实只是应用链式法则的结果。 说的啰嗦一点, 我们可以把反向传播看做是通过应用多变量微积分的链式法则来计算成本函数梯度的一种方式。 这真的就是反向传播的全部了。 其余的都是细节。
反向传播算法
反向传播算法给我们提供了一种计算成本函数梯度的方法, 让我们把这个过程用算法形式描述出来:
-
输入
x :设置输入层的**量a1 -
前向反馈 : 针对每一层
l=2,3,...,L 计算zl=wlal−1+bl 和al=σ(zl) . -
输出误差量
δL : 计算向量δL=∇aC⊙σ′(zL) -
反向传播误差量: 对每一层
l=L−1,L−2,…,2 , 计算δl=((wl+1)Tδl+1)⊙σ′(zl) -
输出: 成本函数的梯度可以通过计算
∂C∂wljk=al−1kδlj 和∂C∂blj=δlj 得到。
审视这个算法, 你会发现为什么它被称为“反向传播”, 我们是从最后一层开始反向计算误差向量
练习题
-
单个神经元被修改过后的反向传播:
- 假设我们在前向反馈网络中修改单一的一个神经元, 使得该神经元的输出为
f(∑jwjxj+b) , 其中f 是某个非 S 形函数。 这种情况下, 我们该如何修改反向传播算法呢?
- 假设我们在前向反馈网络中修改单一的一个神经元, 使得该神经元的输出为
-
线性神经元网络的反向传播:
- 假设我们把非线性的
σ 函数替换为线性函数σ(z)=z , 请为这种情形重写反向传播算法。
- 假设我们把非线性的
正如我之前所描述的, 反向传播算法计算针对单一的一个训练输入计算了成本函数的梯度,
- 输入一个训练抽样的集合
-
对每一个训练抽样 x : 设置对应的输入**量
ax,1 , 并且进行下面的步骤:-
前向反馈: 针对每一层
l=2,3,...,L 计算zx,l=wlax,l−1+bl 和ax,l=σ(zx,l) . -
输出误差量
δx,L : 计算向量δx,L=∇aC⊙σ′(zx,L) -
反向传播误差量: 对每一层
l=L−1,L−2,…,2 , 计算δx,l=((wl+1)Tδx,l+1)⊙σ′(zx,l)
-
前向反馈: 针对每一层
-
梯度下降: 针对每一层
l=L,L−1,…,2 , 根据规则wl→wl−ηm∑xδx,l(ax,l−1)T 和bl→bl−ηm∑xδx,l
当然, 为了实现梯度下降算法, 你还需要在上面算法的外层嵌套一个对迷你集的遍历循环, 然后在更外层套一个游历多训练世代(epoch) 的循环。 为了间接, 这些循环被省略了。
反向传播算法的代码
在抽象地理解了反向传播算法后, 我们现在可以理解上一章用于实现反向传播算法的代码了。 回想上一章节包含在 Network
类中的 update_mini_batch
和 backprop
方法。 这些代码是上面所描述的算法的一个直译实现。 具体而言, update_mini_batch
方法通过计算当前 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)]
大部分的工作都被该行代码:delta_nabla_b, delta_nabla_w = self.backprop(x, y)
完成了, 该行代码应用了 backprop
方法来算出偏导数
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=[x1x2…xm] 开始, 其中的每一列就对应着迷你集中的每一个输入向量。 我们通过乘以权重矩阵, 然后加上一个对应的偏移量矩阵, 然后在每一个项上应用 S 形函数。 现在请你把这个方法的伪代码写出来。 然后修改network.py
使得它用这个完全基于矩阵的方法来进行计算。 这种方法的好处是, 它可以完全利用现代的线性代数代码实现库, 加速对于迷你集的循环。 (在我的笔记本电脑上, 对于上一章我们所解决的 MNIST 的分类问题可以加速1/2) 。 实践中, 所有正式的反向传播算法库都会使用这种完全基于矩阵的方法, 或是该方法的变种形式。
在哪种意义上, 反向传播算法是一个快速的算法?
在哪种意义上, 反向传播算法是一个快速的算法? 为了回答这个问题, 让我考虑另外一种计算梯度的方法。 想象现在是神经网络研究的早期, 可能是19世纪50-60 年代, 你是世界上第一个考虑用梯度下降方法来进行学习的人!但是为了实现这个想法, 你需要计算成本函数的梯度。 你回想了你关于微积分的只是, 然后决定尝试看你是否能用链式法则来计算梯度。 但是在做了一些尝试后, 代数等式变得复杂起来, 你有一些沮丧。 于是你试图寻找另外一种方法。 你决定把成本当做一个仅仅有关权重的方程
其中,
这个方法看上去非常有前景。 概念上足够简单, 极度易于实现,仅仅几行代码就能实现。 当然, 这看上去比链式法则来计算梯度更有诱惑力!
不幸的是, 这个方法虽然好像很有前景, 但是当你用代码实现后会发现它极度缓慢。 为了理解它为什么很慢, 想象我们的神经网络中有一千万哥权重。 那么针对每一个不同的权重
反向传播的聪明之处在于, 它可以使我们仅仅用一次网络的前向迭代计算和紧随其后的一次反向迭代计算, 就同时计算所有的偏导数
这个加速方法是1986年第一次被完全接受, 之后极大地扩展了神经网络可以解决的问题范围。 这样, 又反过来促进一大批人使用神经网络。 当然, 反向传播不是一个灵丹妙药。 甚至在19世纪80年代, 人们就遭遇了该方法的瓶颈, 尤其是当人们视图用这个方法训练深度神经网络(有很多隐藏层的神经网络)时。 在这本书的后面,我们会看到现代计算机和一些聪明的想法是如何使得用反向传播训练深度神经网络成为可能的。
反向传播: 整体图景
正如我之前解释过的, 反向传播算法有两个神秘之处。
第一, 这个算法到底做了什么。 我们已经了解了误差量从输出被反向传播的过程。 但是, 我们能进一步深入, 对我们所做的这一系列矩阵和向量乘法构建出更多的直觉性认识吗?
第二, 一个人如何才能在没有先行者参考的情况下, 发现这个算法。 理解算法的步骤, 或甚至理解算法的证明过程是一回事, 理解一个问题,然后作为第一个人独立发现解决这个问题的算法又是另一回事。 有没有一条可行的推理线索可以指引你发现反向传播算法?
在这一小节, 我会解答这两个谜题
为了改善我们对反向传播算法工作内容的直觉性理解, 让我们想象我们对神经网络中的某个权重
这会导致对应的神经元的**量发生改变:
然后这会进一步会导致下一层所有的神经元**量都发生变化:
这些改变会进一步导致下一层的变化, 依次类推直到影响最后一层的输出, 然后影响到成本函数值的变化。
成本函数值的改变量
这给我们提供了一个思路, 可以通过仔细地观察
让我们来试着实现它。 改变量
**量的改变
事实上, 它会导致如下的变化:
用等式 (48) 进行代入得到
当然, 改变量
其中, 我们为每一个经过的神经元都选用了
其中, 我们加总了所有所有可能的神经元序列组合。 和公式 (47) 进行对比, 我们可以得到:
现在等式(53) 看起来比较复杂了。 然而, 它却有一个很好的直觉性解释。 我们在计算成本 C 相对于神经网络中的一个权重变化率, 而这个等式告诉我们, 神经网络中的两个神经元之间的每一条边(权重)都和一个因子存在某种联系, 这个因子就是其中一个神经元的**量相对于另外一个**量的偏导数。 而连接两个神经元的路径上的第一条边的因子比较特殊, 是
刚才所展示的只是一个启发性质的论证, 一种用于思考对权重进行微调后会发生什么的方法。 现在让我们勾勒出一条线索, 使你可以进一步深化这个论证。
首先, 你可以推到中等式(53)中每一个偏导数的表达式, 这用于一点微分学知识就能做到。 完成了之后, 你可以尝试这思考如何把这个包含众多下标的求和公式写成矩阵相乘的形式。 这件事真的做起来会乏味, 还需要一些毅力, 但不需要超凡的洞察力。 在完成了所有这些后, 试着对其进行尽可能的简化, 你会发现你最终就得到了反向传播算法。 然后你就可以把反向传播算法当做一个为所有路径求因子和的方式。 或者, 换种说法, 反向传播算法是一种追踪权重(偏倚量)的变动如何一步步扩散至输出, 最后影响成本的方法。
现在, 我并不打算一步步展示这个过程。 它非常散乱且需要极度的细心去完成好每个细节。 如果你有兴趣挑战一下, 你可能会享受这个尝试的过程。 如果你不感兴趣, 那么我希望这个思考过程可以为你提供一些反向传播算法工作原理的深入认识。
现在讨论另一个谜题, 反向传播算法是如何被第一个人发现的呢?事实上, 如果你沿着我刚才所描述的方法进行尝试, 就会发现反向传播算法的证明方法。 不幸的是, 这个证明非常长, 且比我本章之前的描述要冗长许多。 那么, 问题就来了, 短的那个证明是如何被发现的。 事实上, 当你写下来长的证明方式的所有细节后, 你会发现其中有好几处非常明显的可以简化的地方。 你进行这些简化, 会得到一个更短的证明。 然后把它写下来, 你会进一步发现可以简化的地方, 你再次重复。 在经过几次重复操作后, 你就会得到我们先前看到的, 简短的, 但是有一些晦涩的证明, 晦涩的原因是简化的过程相当于去除了你一路推导的路标。 我想请你相信我,要推导出之前的证明, 真的不存在什么神秘之处, 它就是对我之前所勾勒的证明过程做了很多简化工作而已。