全连接神经网络之反向传播算法原理推导

简介

在上一篇文章《手写一个全连接神经网络用于MNIST数据集》中,我们使用少于100行代码实现了一个3层的全连接网络,并且在MNIST数据集上取得了95%以上的准确率。相信读过这篇文章的读者对全连接网络如何使用梯度下降算法来学习自身的权值和偏置的原理已经有所了解(如果你没读过,建议先看一下再阅读本文)。但是上篇文章留下了一个问题,就是我们没有讨论如何计算损失函数的梯度,在本文中,我会详细解释如何计算这些梯度。

本文会涉及到较多的数学公式,要求读者了解微积分的基本知识,尤其是链式法则。


反向传播概览

先引用一下维基百科中对于反向传播的定义:

反向传播(英语:Backpropagation,缩写为BP)是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法。该方法对网络中所有权重计算损失函数的梯度。这个梯度会反馈给最优化方法,用来更新权值以最小化损失函数。

反向传播的核心是对损失函数C关于任何权重w(或偏置b)的偏导数\partial C/\partial w(或\partial C / \partial b)的表达式。这个表达式告诉我们在改变权重和偏置时,损失函数变化的快慢。由于C是一个叠加了多种运算的多元函数,所以对网络的某一层的某一个权重w_i的偏导数可能会变得很复杂,不过这也让我们直观的看到了某一个权重w_i的变化究竟会如何改变网络的行为。


神经网络参数的表示

通过上一篇文章,我们知道神经网络是由多个S型神经元构成的,每个S型神经元有输入x,权重w,偏置b,以及输出z。那么当它们组合在一起形成复杂的神经网络结构的时候,参数会变得异常的多。这个时候我们需要先约定一下如何来表示这些参数,以便数学描述和基于矩阵的运算。先以一个简单的神经网络结构为例:


可以看到上述网络一共有3层,第一层是输入层,第二层是隐层,第三层是输出层。接下来我会在这张图上标注权重w,偏置b,激活值a等参数,并且会引入若干个符号来表示这些参数。

w_{jk}^l表示从(l-1)^{th}层的k^{th}个神经元到l^{th}层的j^{th}个神经元上的权重。这个表示看起来有点奇怪,也不容易理解,因为这个表达式里面虽然带有一个l,但是实际上它表示的是第l-1层到第l层的之间的权重关系。以上图的蓝色箭头所指的线段为例,w_{24}^3代表的含义是,第2层的第4个神经元到第3层的第2个神经元之间的权重。

其实如果你仔细看过上一篇文章中的代码,你会发现权重向量w和偏置向量b只是第二层和第三层才会有,而输入层是没有这些参数的。在上图中,我们是用带箭头的线段来表示的权重,故w^l代表的并不是第l层指出去的线段,而指的是第l-1层指向它的线段,这个有点违反直觉,但是接下来你会看到这么做的好处。

b_j^l表示在第l^{th}层的第j^{th}个神经元的偏置,如上图b^2_{3}表示第2层的第3个神经元上的偏置。
a_j^l表示在第l^{th}层的第j^{th}个神经元的激活值,如上图a^2_{4}代表第2层的第4个神经元上的激活值。

有了这些表示,l^{th}层的第j^{th}个神经元的激活值a^l_{j}就和(l-1)^{th}层的激活值a^{l-1}通过方程关联起来了:

其中求和是在(l-1)^{th}层的全部k个神经元之间进行的。为了用矩阵的形式重写这个表达式,我们对每一层l都定义了一个权重矩阵w^l。权重矩阵w^l的元素是连接到第l层的神经元的权重(即指向第l层神经元的全部箭头)。同理,对每一层定义一个偏置向量b^l,向量中的每一个元素就是b^l_k。最后定义每一层的激活向量a^l,向量中的每个元素是a^l_j
也许你还不明白这个怎么计算的,让我以上图的a^2_4来描述一下它的计算过程。我用橙色的线段来代表所有指向第2层第4个神经元的权重,可以看到一共有3条线段指向了它,其代表的权重分别是w^2_{41}w^2_{42}w^2_{43},由于这是第一层,故它的激活向量就是输入向量xb_4^2代表的是这个神经元的偏置值,那么完整的计算过程描述如下:
a^2_4 = \sigma (w^2_{41}*a^1_1 + w^2_{42}*a^1_2 + w^2_{43}*a^1_3 + b_4^2)

上面的公式(1)还是太麻烦了,我们注意到(1)式中的参数wab其实都是向量,故我们可以将其改写成矩阵形式,如下:

这个式子看着更加简洁,它描述了第l层的激活值a^l与第l-1层的激活值a^{l-1}之间的关系。我们只需要将本层的权重矩阵作用在上一层的激活向量上,然后加上本层的偏置向量,最后通过\sigma函数,便得到了本层的激活向量。
如果将(2)式写的更详细一点,其实我们是先得到了中间量z^l = w^l a^{l-1} + b^l,然后通过\sigma函数。我们称z^l为第l层神经元的带权输入。故为了本文后面描述方便,我们也会将(2)式写成以下形式:

注意z^l是一个向量,代表了第l层的带权输入。它的每一个元素是z^l_j = \sum_k w_{jk}^l a_k^{l-1} + b_j^l,其中z^l_j就是第l层的第j个神经元的激活函数的带权输入。

注意,在本文中,只带了上标而没有带下标的表达式,都是指的向量。


损失函数

还记得我们在上一篇文章中,使用了均方差损失函数,定义如下:

其中n是训练样本的总数,求和运算遍历了每个训练样本xy(x)是每个样本对应的标签,L代表网络的层数,a^L代表的是输入为x时网络输出的激活向量(在MNIST任务中,输出是一个10维的向量)。
在上式中,对于一个特定的输入样本集xny(x)都是固定的,所以我们可以将C看做是a^L的函数。
本文将会继续使用此损失函数来描述如何进行反向传播算法的应用。


Hadamard乘积

反向传播算法基于常规的线性代数运算 —— 诸如向量加法,向量矩阵乘法等。但是有一个运算不大常⻅。特别地,假设st是两个同样维度的向量。那么我们使用s ⊙ t来表示按元素的乘积。所以s ⊙ t的元素就是(s ⊙ t)_j = s_jt_j。举个例子如下:

这种类型的按元素乘法有时候被称为Hadamard乘积,具体定义可以参考百度百科


反向传播

定义神经元误差

反向传播其实是对权重和偏置变化影响损失函数过程的理解,最终的目的就是计算偏导数\partial C/ \partial w^l_{jk }\partial C / \partial b^l_j。为了计算这些值,我们首先引入一个中间量\delta ^l_j,我们称之为在第l^{th}层的第j^{th}个神经元上的误差。

为了理解误差是如何定义的,假设在神经网络上有一个调皮⻤:


这个调皮鬼在第l层的第j^{th}个神经元上。当输入进来的时候,这个调皮鬼对这个输入增加了很小的变化\Delta z^l_j,使得神经元输出由原本的\sigma (z^l_j)变成了\sigma (z^l_j + \Delta z^l_j)。这个变化会依次向网络后面的层进行传播,最终导致整个损失函数产生\frac {\partial C }{\partial z^l_j} \Delta z^l_j的变化(具体可以参考全微分的定义)。
现在加入这个调皮鬼改邪归正了,并且试着帮你优化损失函数,它试着找到可以让损失函数更小的\Delta z^l_j。假设\frac {\partial C}{\partial z^l_j}有一个很大的值,或正或负。那么这个调皮鬼可以通过选择适当的\Delta z^l_j来降低损失函数的值。相反,如果\frac {\partial C}{\partial z^l_j}接近0,那么无论怎么调整\Delta z^l_j都不能改善太多损失函数的值。因此,在调皮鬼看来,这时神经元已经接近最优了。所以这里有一种启发式的认识,\frac {\partial C}{\partial z^l_j}可以认为是神经元误差的度量。

按照上面的描述,我们定义第l层的第j^{th}个神经元上的误差\delta ^l_j为:
\delta ^l_j = \frac {\partial C}{\partial z^l_j} \tag 5

反向传播的四个方程

反向传播基于4个基本方程,这些方程指明了计算误差\delta ^l和损失函数梯度的方法。先列举出来:

1. 输出层误差的方程

结合(3)式,我们简单证明一下第一个方程:

因为上式中的求和是在输出层的所有k个神经元上运行的,由于这里是求损失函数C对第L层的第j个神经元的输出激活值求导,故当k \ne j时, \partial a^L_k / \partial z^L_j都为0。

上式右边第一个项\partial C / \partial a^L_j表示损失函数C随着j^{th}输出激活值的变化而变化的速度。假如C不依赖特定的神经元j,那么\delta ^L_j就会比较小,这也是我们期望的效果。右边第二项\delta ' (z^L_j)表达的是在激活函数\sigmaz^L_j处的变化速度。
对于第一项\partial C / \partial a^L_j,它依赖特定的损失函数C的形式,如果我们使用均方差损失函数,那么其实很容易就可以算出来,如下:

如果我们令\nabla _aC代表损失函数C对激活值向量a的偏导数向量,那么(6)式可以被写成矩阵的形式:

(7)式就是反向传播4个方程中的第一个方程。对于均方差损失函数,\nabla _aC = (a^L - y),所以(7)式可以写成如下形式:

写成上述向量的形式是为了方便使用numpy之类的库进行矩阵计算。

2. 使用下一层的误差\delta ^{l+1}来更新当前层的误差\delta ^{l}

第一个方程描述了输出层的误差\delta ^L,那么如何求得前一层的误差\delta ^{L-1}呢?我们还是可以从(5)式出发,对其运用链式法则,如下:

又根据上文,我们知道z^l_j = \sum_k w_{jk}^l a_k^{l-1} + b_j^la^l= \sigma(z^l),因此可以得到:

上式对z^l_j微分的结果如下:

将式(11)带入式子(9)得:

将式(12)写成向量的形式,得到第二个方程:

其中(w^{l+1})^T代表第(l+1)^{th}层的权重矩阵的转置。这个公式看起来挺复杂,但是每一项都有具体的意义。假如第(l+1)^{th}层的误差是\delta ^{l+1},当我们使用同一层的转置权重矩阵(w^{l+1})^T去乘以它时,直观感觉可以认为它是沿着网络反向地移动误差,这给了我们度量在第l^{th}层输出的误差方法(还记得上文,我们使用带箭头的线段来表明权重么,这里这么做,相当于把第l层第j个神经元指向第l+1层的所有神经元的箭头线段全部逆向了)。
接着我们进行Hadamard乘积运算,这会让误差通过第l层的激活函数反向传递回来,并给出在第l层的带权输入误差\delta

我估计你看到这里会很懵逼,我当时学习的时候也是非常迷惑,感觉脑子一团糟,不过我将会以第二层第4个神经元的误差\delta ^2_4为例,来展示误差的反向传播过程,根据式(12),可以得出以下计算过程:

下面用图例展示了误差是如何从第三层的神经元反向传播到第二层的:

其实这个也很符合直觉,因为第2层第4个神经元连接到了第3层的全部神经元,故误差反向传播的时候,应该是与其连接的所有神经元的误差之和传递给此神经元。

有了前两个方程之后,我们就可以计算任何层的误差\delta ^L。首先使用方程(7)计算当前层误差\delta ^L,然后使用式(13)来计算得到\delta ^{L-1},以此类推,直到反向传播完整个网络。

3. 损失函数关于任意偏置b^l_j的变化率

由上面内容可知,z^l_j = \sum_k w_{jk}^l a_k^{l-1} + b_j^lz^l_jb^l_j求偏导得\frac {\partial z^l_j}{\partial b^l_j} = 1。则有:

故可知,误差\delta ^l_j和偏导数\partial C / \partial b^l_j完全一致。
写成向量的形式如下:

其中\delta和偏置b都是针对同一个神经元。

4. 损失函数对于任意一个权重的变化率

由上面内容可知,z^l_j = \sum_k w_{jk}^l a_k^{l-1} + b_j^lz^l_jw^l_{jk}求偏导得\frac {\partial z^l_j}{\partial w^l_{jk}} = a^{l-1}_k。又:

上式可以简化成更少下标的表示:

其中a_{in}是上一层的激活输出向量,\delta _{out}是当前层的误差,可以使用下图来描述:


上图说明,当上一层的激活值a_{in}很小的时候,梯度\partial C / \partial w也会很小,这意味着在梯度下降算法过程中,这个权重不会改变很多。这样导致的问题就是来自较低激活值的神经元的权重学习会非常缓慢。
另外观察一下前两个方程,可以看到它们的表达式中都包含\sigma ' (z^l_k),即要计算\sigma函数的导数。回忆一下上一篇文章中Sigmoid函数的图像,可以看到,当输入非常大或者非常小的时候,\sigma函数变得非常平坦,即导数趋近于0,这会导致梯度消失的问题,后面会专门写文章讨论。
总结一下,如果输入神经元激活值很低,或者神经元输出已经饱和了,那么权重学习的过程会很慢。

反向传播的算法流程

反向传播的4个方程给出了一种计算损失函数梯度的方法,下面用算法描述出来:

  1. 输入x,为输入层设置对应的激活值a^1
  2. 前向传播: 对每一层l=2,3,4,...,L,计算相应的权重输入z^L = w^la^l + b^la^l = \sigma (z^l)
  3. 输出层的误差\delta ^L: 计算向量\delta ^L = \nabla C \ ⊙ \ \sigma ' (z^L)
  4. 误差反向传播:对于每一层l=L-1,L-2,...,2,计算\delta^l = ((w^{l+1})^T \delta ^{l+1}) ⊙ \sigma ' (z^L)
  5. 输出: 损失函数的梯度分别由\frac {\partial C}{\partial w^l_{ji}} = a^{l-1}_k \delta ^L_j\frac {\partial C}{\partial b^l_j} = \delta ^L_j得出。

很多人在一开始学习的时候,分不清梯度下降和反向传播之间的关系,我最开始也是。不过从上面的流程中应该可以了解一点。梯度下降法是一种优化算法,中心思想是沿着目标函数梯度的方向更新参数值以希望达到目标函数最小(或最大)。而这个算法中需要计算目标函数的梯度,那么反向传播就是在深度学习中计算梯度的一种方式。

代码解析

这里只列举了代码中反向传播部分,若需完整代码,请在https://github.com/HeartbreakSurvivor/FCN下载。
我对代码中关键步骤,都添加了详细的注释,读者再对着文章内容,应该能够看明白。

def update_mini_batch(self, mini_batch, eta):
    """
    通过小批量随机梯度下降以及反向传播来更新神经网络的权重和偏置向量
    :param mini_batch: 随机选择的小批量
    :param eta: 学习率
    """
    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:
        # 反向传播算法,运用链式法则求得对b和w的偏导
        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):
    """
    反向传播算法,计算损失对w和b的梯度
    :param x: 训练数据x
    :param y: 训练数据x对应的标签
    :return: 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]
    # 前向传播,计算网络的输出
    activation = x
    # 一层一层存储全部激活值的列表
    activations = [x]
    # 一层一层第存储全部的z向量,即带权输入
    zs = []
    for b, w in zip(self._biases, self._weights):
        # 利用 z = wt*x+b 依次计算网络的输出
        z = np.dot(w, activation) + b
        zs.append(z)
        # 将每个神经元的输出z通过激活函数sigmoid
        activation = sigmoid(z)
        # 将激活值放入列表中暂存
        activations.append(activation)
    # 反向传播过程

    # 首先计算输出层的误差delta L
    delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
    # 反向存储 损失函数C对b的偏导数
    nabla_b[-1] = delta
    # 反向存储 损失函数C对w的偏导数
    nabla_w[-1] = np.dot(delta, activations[-2].transpose())
    # 从第二层开始,依次计算每一层的神经元的偏导数 
    for l in range(2, self._num_layers):
        z = zs[-l]
        sp = sigmoid_prime(z)
        # 更新得到前一层的误差delta
        delta = np.dot(self._weights[-l + 1].transpose(), delta) * sp
        # 保存损失喊出C对b的偏导数,它就等于误差delta
        nabla_b[-l] = delta
        # 根据第4个方程,计算损失函数C对w的偏导数
        nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
    # 返回每一层神经元的对b和w的偏导数 
    return (nabla_b, nabla_w)

参考

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

推荐阅读更多精彩内容