反向传播算法(BackPropagation)

你可以在这里阅读 上一篇

感谢英文原版在线书籍,这是我学习机器学习过程中感觉非常适合新手入门的一本书。鉴于知识分享的精神,我希望能将其翻译过来,并分享给所有想了解机器学习的人,本人翻译水平有限,欢迎读者提出问题和发现错误,更欢迎大牛的指导。因为篇幅较长,文章将会分为多个部分进行,感兴趣的可以关注我的文集,文章会持续更新。


在上一篇文章中,我们看到了神经网络如何通过梯度下降算法学习调整它的权重和b值,但是我们省略了如何计算损失函数的梯度。在这一篇文章中,我将会介绍一种计算梯度的快速算法:反向传播算法(BackPropagation)。

这篇文章会涉及很多数学,如果你觉得没有兴趣或者很迷惑,可以跳过,那样你可以直接把反向传播当成一个黑盒来使用。但是有什么值得我们花时间来学习这些细节呢?

理由当然是:理解。反向传播的核心是通过改变对于网络中的权重w和偏移b,计算损失函数C的偏导数∂C/∂w,从而求出损失变化的极值(如果这句话看不懂,我建议你去先看看微积分)。这个表达式告诉我们当我们改变权重w和偏移b,损失函数变化的速度。虽然它有时很复杂,但是它可以对每个元素有一个自然的,直观的解释,它实际给了我们一个详细的修改权重和b值来改变网络整体行为的办法。这是非常值得研究的。

热身:一种基于矩阵的快速计算神经网络输出的方法


在讨论反向传播之前,让我们先用一种基于矩阵的快速传播方法来预测一个神经网络的输出。这有助于我们熟悉反向传播中的符号。

定义符号:

这个符号代表第(l-1)层中第K个神经元和第l层中第j个神经元之间的权重。例如下图:

定义符号:

代表第 l 层中第 j 个神经元的偏移值b。

代表第 l 层中第 j 个神经元的输出激活a(activation)。如下图所示:

由以上所有符号得出:第l层第j个神经元的激活等于:

上式括号中表示的是:第(l-1)层(前一层)中的所有k个神经元到第(l)层(后一层)第j个神经元的权重和第(l-1)层所有k个神经元的输出乘积的和,加上第l层第j个神经元的偏移值(阀值)。相信看到这里,这些符号的意义你都会很明确了。

我们向量化公式(23),用σ(v)表示。例如,方程式f(x)=x^2,向量化就是:

向量化的f会对每一个向量进行求平方操作。

所以公式(23)被向量化可以表示为下式:

这个表达式给我们提供了一个更为全局的思考方法:一层中的激活与前一层中的激活如何相关:我们只是将权重矩阵应用于前一层激活,然后添加偏移向量(b),最后应用到σ函数。 比我们现在采用的神经元 – 神经元更简单,更简洁(至少应用了更少的索引!)。 该表达式在实践中也是有用的,因为大多数矩阵库提供了实现矩阵乘法,矢量加法和矢量化的快速方法。 实际上,上一章中的代码隐含地使用这个表达式来计算网络的行为。

我们定义公式:

为第l层神经元的加权输入。如果将其写成分量样式:

即代表第j层神经元的激活函数对第l层的加权输入。

关于损失函数的两个假设


反向传播的目标是通过计算损失函数C相对于网络中的权重w或偏差b的偏导数∂C/∂w和∂C/∂b来实现的。 为此,我们需要对损失函数的形式做两个假设。 在阐述这些假设之前,我们参照一个损失函数示例。 我们将使用上一节中的二次损失函数(c.f. Equation(6))。 在上一节的符号中,二次损失函数的形式是:

其中:n是训练样例的总数; x是每一个训练样本;y(x)是期望输出;L是网络层数,a^L(x)是当x输入是网络的输出。

第一个假设是:用损失函数的平均数表示在每一个训练样例上的损失。

我们做这个假设的原因是,我们求偏导数∂Cx/∂w 和 ∂Cx/∂b是针对每一个样例进行的。

第二个假设是将神经网络的输出表示成损失函数:

例如二次损失函数可以满足这个要求,因为二次损失函数对于单一的输入x可以写成:

Hadamard 积,s⊙t


反向传播要基于常见的线性代数运算,像矩阵的加、乘等等。但是有一种运算是不常见的。例如,s和t是两个相同维度的向量,我们使用s⊙t表示两个向量按元素的积(elementwise product),如:

可用如下形式表示:

这种按元素相乘的积叫做Hadamard乘积。

反向传播背后重要的四个方程


反向传播算法的目的是通过改变(权重w和偏移b)来改善神经网络损失函数。最终是通过求偏导数(∂C/∂w)和(∂C/∂b)来衡量的。为此我们先定义一个中间量:δ(l,j),代表 l 层第 j 个神经元的误差。我们将会利用反向传播来计算这个误差。首先为了理解这个中间量,我们想象一下在神经网络中有一个小恶魔:

这个恶魔站在第l层的第j个神经元,当有输入过来的时候,它改变了一点加权输入(上面定义的z):

造成了神经元输出的改变:

最终,造成了损失函数的改变:

现在假设这个恶魔是个好恶魔,试图帮助你提高输出的准确率。它尝试帮你调节∆z,使得输出误差最小。由上面的想象,我们通过下式定义第l层第j个神经元的误差:

作为惯例,把上式中的j去掉,表示第l层的误差向量(δ^l)。反向传播为我们提供了计算它的方法,并通过:∂C/∂w 和∂C/∂b将误差和真实数据结合起来。

反向传播基于四个基本方程。 这些方程一起给我们提供了一种计算误差δ和损失函数梯度的方法。 不过要注意的是:你不应该期望一下子就理解这四个方程式。 这样的期望可能会让你失望。 因为反向传播方程非常丰富,以至于深入了解它们需要相当长的时间和耐心。

这里先提供一个预览的方式,我们将会在后面章节给出更详细的描述。这里会给出一些简单的证明,这有助于我们对他们的理解。

输出层误差的等式:

表达式右边的第一部分表示:第j个神经元输出改变时损失C改变的速度(斜率)。如果当神经元输出改变已经不能太显著地改变C时,那么误差就会很小,这正是我们期望的。右边的第二个式子表示:测量激活函数σ在加权输入变化时的变化速率。

将BP1以向量的形势写出来就是:

等号右边第一部分表示一个向量,你可以把它看作损失C对输出偏导数的向量矩阵,很容易看出来(BP1a)和(BP1)是相等的。因为这个原因,从现在起我们将用BP1代替上式。我们参照在二次均方误差方程中的例子来理解,其损失函数为:

可推导出:

所以:

代入方程(BP1a)得出:

正如你看到的,表达式中的每一部分都是向量格式,所以就能很容易的用Numpy来计算。

用(l + 1)层的误差表示第(l)层的误差

你可以把它看做:误差会随着网络进行传递。

与偏差b相关的误差变化:

通过BP1和BP2,我们能简单表示BP3:

与权重相关的误差变化:

简洁格式:

a(in)可理解成权重w的输入激活, δ(out)可理解成权重w输入时神经元的误差。图示:

等式中a的值很小接近0时,公式值就会很小,此时我们说这个权重下学习很慢,意味着它不能很快的改变梯度。

让我们从输出层的角度观察这4个公式,看一下公式BP1,再回想一下上一节中的sigmoid函数的图,当函数接近0或1的时候就会变的非常水平。这时:

如果输出神经元是低激活(≈0)或高激活(≈1),权重调节就是缓慢学习的。这种情况下就说,输出神经元饱和,权重停止学习,对于输出神经元的偏差b,也有类似的说法。这种方法应用在BP2上也是类似的。

总结一下,我们已经知道,如果输入神经元是低激活的,或者如果输出神经元饱和,即高或低激活,则权重学习效率将会很低。

注意:以上四个公式我希望读者可以自己证明出来,这能给你带来更深入的理解。这可能会花很多时间,但是这是挺有必要的。

简单证明一下上看的式子:

四个式子都是可以用多元微积分的链式法则求出来的。如果你熟悉链式法则,我希望你可以自己证明。

  • BP1
    我们知道误差表达式为:

根据微积分链式法则:

上式为输出层k个神经元的和,当然,输出层第k个神经元的激活输出a仅仅依赖于输出层第j个神经元的加权输入z(j)。所以除了这一项,其他项都消失了,则:

而a=σ(z),所以:

  • BP2

这是表示相邻两层的误差关系的式子,由式子(36)可得:

由链式法则可得:

然后已知的是:

对其求偏导数可得:

代入(42)可得:

这就是式子二的分量形势。

关于式子三和四我希望读者可以自己证明,根据链式法则这也是相对容易的。

我们从最后一层开始计算误差向量,然后向后逐层计算误差。

实际中,因为有很多训练集,我们经常需要结合随机梯度下降和反向传播算法来计算梯度。例如使用我们第一章提到的mini-batch数据时:

  • 输入训练数据
  • 依照下面的步骤,为每个训练数据设置激活a(x,1):

    • 为每一层 l=2,3,…,L计算a和z:
    • 反向传播计算每一层误差:
  • 梯度下降,对每一层更新w和b:

此外,你需要一个外部循环mini-batches遍历整个训练数据集。

反向传播代码

理解了反向传播的抽象概念,我们来理解一下上节的代码。还记得在Network类中有update_mini_batch和backprop方法,这两个方法是直接翻译的上面的公式,update_mini_batch方法是通过计算当前训练集(mini_batch)的梯度更新权重(w)和偏移值(b):

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)这一行执行。这一行通过反向传播方法计算b和w相对于C的偏导数。这个方法如下:

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))

反向传播算法是什么意思?

让我们想象一下网络中权重有一点小小的改变:

这个小小的改变会造成相应神经元激活输出的改变:

然后会造成下一层所有输出激活的改变:

最终,将会影响到损失函数:

通过公式可以表示成:

我们知道一层中激活改变会造成下一层中输出改变,这里先把目光聚焦于下一层中的某一个的改变。

结合公式(48):

可以想象,当有很多层时(每一层假设都有受影响的输出):

这代表这某个权重改变经过网络某一条路径最终对误差的影响,当然网络中有很多的路径,我们需要计算所有的总和:

结合(47):

计算c相对于某个权重的变化速率,公式可看出每一层的激活相对于下一层激活的偏导数都是速率因子。体现在图上:

这只是提供一种启发式的思考,希望会对你理解反向传播有帮助。

发表评论

关闭菜单