详解神经网络反向传播算法 Surpass Backpropagation

本文相关代码可以从Backpropagation下载

上篇文章小白也能看懂的BP反向传播算法之Further into Backpropagation中,我们小试牛刀,将反向传播算法运用到了一个两层的神经网络结构中!然后往往实际中的神经网络拥有3层甚至更多层的结构,我们接下来就已一个三层的神经网络结构为例,分析如何运用动态规划来优化反向传播时微分的计算!

Lets get started!!!

如下的网络结构:


image.png

在正式分析神经网络之前,我们先修改一下权重矩阵的表示形式!

让我们以一个符号开始,它代表网络中任意方式的权重信息。我们将使用


image.png

来表示从网络第(l−1)层中第k个神经元指向l层中第j个神经元的连接权重。因此举个例子,下图中的权重就表示从第二层中第四个神经元指向第三层中第二个神经元的权重:


image.png

这个符号起始比较麻烦,的确需要一些努力才能掌握。但是通过努力你会发现它将变得简单和自然。符号中的一个不容易接受的地方就是j和k的位置关系。你可能认为用j来表示输入神经元,k表示输出神经元,而不是实际定义中反过来的方式。我将在下面解释这样做法的原因。

我将使用相似的符号来表示网络中的偏差和激活。明确地,我们使用blj来表示第l层中第j神经元的偏差,用alj来表示第l层中第j神经元的激活。下面的图将展示这些符号:


image.png

有了这些符号,第l层中第j神经元的激活alj就与第(l−1)层中所有激活相关。


image.png

能够用漂亮而简洁的向量格式进行重写


image.png

这个表达式能给我们更多的启发,某一层的激活与上一层的激活是有什么关系:我们只是将权重矩阵应用到激活上,然后再加上一个偏差向量,最后应用σ函数!顺便说一下,这个表达式诱发了前面提到的wljk符号。如果我们使用j来指示输入神经元,k来指示输出神经元,那么我们就需要替换表达式中的权重矩阵 用权重矩阵的转置。虽然这是一个很小的变化,但是烦人的是,我们将失去解释和思考的简单性“应用权重矩阵到激活上。这个全局视图非常简单和简洁(使用了很少的下标),相对于一个神经元到一个神经元的方式。也可以将其想象成一种避免下标混乱,而且还能保持精确的方法。这个表达式在实际中非常有用,因为许多矩阵库都能提供快速的矩阵乘法,向量加法和向量化。

我们间接的计算


image.png

这个值非常有用,我们将zl命名为:网络l层的加权输入。我们将在本章大量的使用加权输入zl。

Hadamard乘积s⊙t

后向传播算法是基于通用的线性代数运算——就像向量加法,矩阵乘向量等等。但是有一个操作平常很少用到。特别的,假设s和t是相同维数的两个向量,那么我们使用s⊙t来表示两个向量元素级的乘法。


image.png

这种元素级的乘法有时叫做 Hadamard乘积或者Schur乘积。我们将把它叫做Hadamard乘积。好的矩阵库一般都能提供Hadamard乘积的快速实施,因此在实施后向传播时候就非常方便。

根据前面多篇文章所学,我们如果要写出第l层j个神经元的加权输入的微分应该不难,就是链式法则求导,如下:


image.png

这里我们假设l层是最后一层,那么此时求出了关于加权输入的微分,就可以继续微分求取关于权重的微分
根据


image.png

所以,不难求出关于权重的微分


image.png

好,现在问题来了,如果我们再往前,求倒数第二层的某个权重,思路也是一致的,也就是要从最后一层一直往回算,为了避免公式太长,我们先求关于第l-1层第j个神经元的加权输入的微分


image.png

然后再根据


image.png

求取权重的微分!

可以看到,只是倒数第二层而已,我们的求取的公式就已经很长了,如果再有个几层,估计就已经爆炸了!

这个时候,就轮到动态规划出场了,动态规划就是在递归的过程中,保存已有的结果,下次计算的时候就不用再算了,可以直接从内存中红取结果。那么我们如何用在这里呢?

仔细观察第l层和l-1层的权重计算公式,我们不难发现,计算第l-1层要经过l层,而在第l-1层中


image.png

我们发现前面两个微分


image.png

不就是第l层的关于加权输入的微分么?也就是说,我们在第l层的权重的微分的计算的时候,就已经计算过这个了,然后在第l-1层的计算的时候还要用到这个。所以我们可以考虑,在第l层计算权重的微分的时候,就把这个值保存下来,这样在后续计算的时候就可以直接用了,这就是动态规划的思想!

我们保存每一层的


image.png

我们给这个值取了个名字叫敏感度矩阵,或者误差矩阵!

image.png

然后我们根据误差矩阵来进行反向传播,首先不难求出误差矩阵的初始值,也就是最后一层的误差矩阵,前面我们已经计算过,直接替换就行


image.png

然后就是关键的动态规划的递推式,这个其实我们也已经在前面求解出来了,参照前面已经分析得出的


image.png

会发现,相邻两层间误差矩阵的关系就是激活函数的微分和权重,我们将其简化成向量的形式就是


image.png

具体的证明如下,也就是链式法则的运用:
首先,


image.png

然后,


image.png

得到,
image.png

代入,
image.png

至此,一个完美的反向传播算法基本上已经大功告成了!
还差最后一丢丢,就是已经有了每一层的敏感度矩阵,也就是每一层关于加权输入的微分,最后再计算我们需要的每一层关于权重和偏置的微分,自然也是手到擒来,直接利用微分求导:
关于权重,


image.png

关于偏置,


image.png

后向传播算法

后向传播等式给我们提供了一种计算代价函数梯度的方法。让我们用算法显示的写出它们:

  1. 输入x:为输入层设置对应的激活a1。
  2. 向前反馈:对于每一层l=1,2,3...,计算
    image.png

    image.png
  3. 输出层误差δL:计算向量
    image.png
  4. 后向传播误差:对每一层l=L-1,L-2,...2计算
    image.png
  5. 输出:代价函数的梯度为
    image.png

检查这个算法,你能看到为什么它被叫作后向传播。我们从最末一层开始向后计算各层误差δl。看起来将网络向后传播很奇怪。但是如果你再想一下后向传播的证明,向后移动就是因为代价是网络输出的函数。为了理解代价是如何跟随早期的权重和偏差进行的改变,我们需要不断的应用链式规则,向后来获取有用的表达式。

随机梯度下降的后向传播算法

就像我上面描述那样,后向传播算法计算了某一个样本的代价函数的梯度C=Cx。实际上,通用的方式是将后向传播算法与随机梯度下降算法合并,我们便能计算许多训练样本的梯度。特别的,给出一小批训练样本mm,下面算法将基于小批训练样本进行梯度下降学习步骤:

  1. 输入训练样本集合

  2. 对于每一个训练样本x: 设置对应的输入激活,执行以下步骤(参照前文的后向传播算法):

    • 向前反馈:对于每一层l=1,2,3...,计算
      image.png

      image.png
    • 输出层误差δL:计算向量
      image.png
    • 后向传播误差:对每一层l=L-1,L-2,...2计算
      image.png
  3. 梯度下降:按照更新法则更新权重和偏置


    image.png
image.png

当然,为了实施随机梯度下降,你也许要一个外部循环来产生小批量的训练样本,还需要一个外部循环来实施更多的训练代。我们将为了简化暂且忽略掉这些。

后向传播的实施代码

抽象上理解了后向传播算法,我们就能根据以上算法,实现一个完整的神经网络的后向传播的算法了!

# %load network.py

"""
network.py
~~~~~~~~~~
IT WORKS

A module to implement the stochastic gradient descent learning
algorithm for a feedforward neural network.  Gradients are calculated
using backpropagation.  Note that I have focused on making the code
simple, easily readable, and easily modifiable.  It is not optimized,
and omits many desirable features.
"""

#### Libraries
# Standard library
import random

# Third-party libraries
import numpy as np

class Network(object):

    def __init__(self, sizes):
        """The list ``sizes`` contains the number of neurons in the
        respective layers of the network.  For example, if the list
        was [2, 3, 1] then it would be a three-layer network, with the
        first layer containing 2 neurons, the second layer 3 neurons,
        and the third layer 1 neuron.  The biases and weights for the
        network are initialized randomly, using a Gaussian
        distribution with mean 0, and variance 1.  Note that the first
        layer is assumed to be an input layer, and by convention we
        won't set any biases for those neurons, since biases are only
        ever used in computing the outputs from later layers."""
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]

    def feedforward(self, a):
        """Return the output of the network if ``a`` is input."""
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w, a)+b)
        return a

    def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        """Train the neural network using mini-batch stochastic
        gradient descent.  The ``training_data`` is a list of tuples
        ``(x, y)`` representing the training inputs and the desired
        outputs.  The other non-optional parameters are
        self-explanatory.  If ``test_data`` is provided then the
        network will be evaluated against the test data after each
        epoch, and partial progress printed out.  This is useful for
        tracking progress, but slows things down substantially."""

        training_data = list(training_data)
        n = len(training_data)

        if test_data:
            test_data = list(test_data)
            n_test = len(test_data)

        for j in range(epochs):
            random.shuffle(training_data)
            mini_batches = [
                training_data[k:k+mini_batch_size]
                for k in range(0, n, mini_batch_size)]
            for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta)
            if test_data:
                print("Epoch {} : {} / {}".format(j,self.evaluate(test_data),n_test));
            else:
                print("Epoch {} complete".format(j))

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

    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 range(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 evaluate(self, test_data):
        """Return the number of test inputs for which the neural
        network outputs the correct result. Note that the neural
        network's output is assumed to be the index of whichever
        neuron in the final layer has the highest activation."""
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_results)

    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)

#### Miscellaneous functions
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))

以上代码实现了一个完整的神经网络的类,里面包括前向传播,结合小批量随机梯度法实现的后向传播,可以直接应用于神经网络问题的求解!

写在最后

终于,我们从微分开始,讲到链式法则,从单个简单的神经元到嵌套神经元,再到两层的神经网络,最后到多层的神经网络,从微分结合链式法则的暴力进行反向传播的计算,到引入动态规划的计算,引入敏感度函数,真正理解了神经网络的反向传播算法!希望能对读者理解神经网络的反向传播有一定的帮助

Further reading

本文相关代码可以从Backpropagation下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容