精华内容
下载资源
问答
  • 误差与反向传播算法

    千次阅读 2020-06-18 02:49:33
    我们将学习一个能够高效计算权重参数的梯度的方法——误差反向传播法。 5.1 计算图 计算图将计算过程用图形表示出来。这里说的图形是数据结构图,通过多个节点和边表示(连接节点的直线称为“边”)。为了让大家...

    我们介绍了神经网络的学习,并通过数值微分计算了神经网络的权重参数的梯度(严格来说,是损失函数关于权重参数的梯度)。数值微分虽然简单,也容易实现,但缺点是计算上比较费时间。我们将学习一个能够高效计算权重参数的梯度的方法——误差反向传播法。

    5.1 计算图

    计算图将计算过程用图形表示出来。这里说的图形是数据结构图,通过多个节点和边表示(连接节点的直线称为“边”)。为了让大家熟悉计算图,本节先用计算图解一些简单的问题。从这些简单的问题开始,逐步深入,最终抵达误差反向传播法。

    5.1.1 用计算图求解

    现在,我们尝试用计算图解简单的问题。下面我们要看的几个问题都是用心算就能解开的简单问题,这里的目的只是通过它们让大家熟悉计算图。掌握了计算图的使用方法之后,在后面即将看到的复杂计算中它将发挥巨大威力,所以本节请一定学会计算图的使用方法。

    问题 1:太郎在超市买了 2 个 100 日元一个的苹果,消费税是 10%,请计算支付金额。

    计算图通过节点和箭头表示计算过程。节点用○表示,○中是计算的内容。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右传递。用计算图解问题 1,求解过程如图 5-1 所示。

    {90%}

    图 5-1 基于计算图求解的问题 1 的答案

    如图 5-1 所示,开始时,苹果的 100 日元流到“× 2”节点,变成 200 日元,然后被传递给下一个节点。接着,这个 200 日元流向“× 1.1”节点,变成 220 日元。因此,从这个计算图的结果可知,答案为 220 日元。

    虽然图 5-1 中把“× 2”“× 1.1”等作为一个运算整体用○括起来了,不过只用○表示乘法运算“×”也是可行的。此时,如图 5-2 所示,可以将“2”和“1.1”分别作为变量“苹果的个数”和“消费税”标在○外面。

    {95%}

    图 5-2 基于计算图求解的问题 1 的答案:“苹果的个数”和“消费税”作为变量标在○外面

    再看下一题。

    问题 2:太郎在超市买了 2 个苹果、3 个橘子。其中,苹果每个 100 日元,橘子每个 150 日元。消费税是 10%,请计算支付金额。

    同问题 1,我们用计算图来解问题 2,求解过程如图 5-3 所示。

    {95%}

    图 5-3 基于计算图求解的问题 2 的答案

    这个问题中新增了加法节点“+”,用来合计苹果和橘子的金额。构建了计算图后,从左向右进行计算。就像电路中的电流流动一样,计算结果从左向右传递。到达最右边的计算结果后,计算过程就结束了。从图 5-3 中可知,问题 2 的答案为 715 日元。

    综上,用计算图解题的情况下,需要按如下流程进行。

    1. 构建计算图。
    2. 在计算图上,从左向右进行计算。

    这里的第 2 歩“从左向右进行计算”是一种正方向上的传播,简称为正向传播(forward propagation)。正向传播是从计算图出发点到结束点的传播。既然有正向传播这个名称,当然也可以考虑反向(从图上看的话,就是从右向左)的传播。实际上,这种传播称为反向传播(backward propagation)。反向传播将在接下来的导数计算中发挥重要作用。

    5.1.2 局部计算

    计算图的特征是可以通过传递“局部计算”获得最终结果。“局部”这个词的意思是“与自己相关的某个小范围”。局部计算是指,无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果。

    我们用一个具体的例子来说明局部计算。比如,在超市买了 2 个苹果和其他很多东西。此时,可以画出如图 5-4 所示的计算图。

    {95%}

    图 5-4 买了 2 个苹果和其他很多东西的例子

    如图 5-4 所示,假设(经过复杂的计算)购买的其他很多东西总共花费 4000 日元。这里的重点是,各个节点处的计算都是局部计算。这意味着,例如苹果和其他很多东西的求和运算(4000 + 200 → 4200)并不关心 4000 这个数字是如何计算而来的,只要把两个数字相加就可以了。换言之,各个节点处只需进行与自己有关的计算(在这个例子中是对输入的两个数字进行加法运算),不用考虑全局。

    综上,计算图可以集中精力于局部计算。无论全局的计算有多么复杂,各个步骤所要做的就是对象节点的局部计算。虽然局部计算非常简单,但是通过传递它的计算结果,可以获得全局的复杂计算的结果。

    5.1.3 为何用计算图解题

    前面我们用计算图解答了两个问题,那么计算图到底有什么优点呢?一个优点就在于前面所说的局部计算。无论全局是多么复杂的计算,都可以通过局部计算使各个节点致力于简单的计算,从而简化问题。另一个优点是,利用计算图可以将中间的计算结果全部保存起来(比如,计算进行到 2 个苹果时的金额是 200 日元、加上消费税之前的金额 650 日元等)。但是只有这些理由可能还无法令人信服。实际上,使用计算图最大的原因是,可以通过反向传播高效计算导数。

    在介绍计算图的反向传播时,我们再来思考一下问题 1。问题 1 中,我们计算了购买 2 个苹果时加上消费税最终需要支付的金额。这里,假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金额关于苹果的价格的导数”。设苹果的价格为 x,支付金额为 L,则相当于求image.png
    。这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。

    如前所述,“支付金额关于苹果的价格的导数”的值可以通过计算图的反向传播求出来。先来看一下结果,如图 5-5 所示,可以通过计算图的反向传播求导数(关于如何进行反向传播,接下来马上会介绍)。

    {95%}

    图 5-5 基于反向传播的导数的传递

    如图 5-5 所示,反向传播使用与正方向相反的箭头(粗线)表示。反向传播传递“局部导数”,将导数的值写在箭头的下方。在这个例子中,反向传播从右向左传递导数的值(1 → 1.1 → 2.2)。从这个结果中可知,“支付金额关于苹果的价格的导数”的值是 2.2。这意味着,如果苹果的价格上涨 1 日元,最终的支付金额会增加 2.2 日元(严格地讲,如果苹果的价格增加某个微小值,则最终的支付金额将增加那个微小值的 2.2 倍)。

    这里只求了关于苹果的价格的导数,不过“支付金额关于消费税的导数”“支付金额关于苹果的个数的导数”等也都可以用同样的方式算出来。并且,计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以高效地计算多个导数。综上,计算图的优点是,可以通过正向传播和反向传播高效地计算各个变量的导数值。

    5.2 链式法则

    前面介绍的计算图的正向传播将计算结果正向(从左到右)传递,其计算过程是我们日常接触的计算过程,所以感觉上可能比较自然。而反向传播将局部导数向正方向的反方向(从右到左)传递,一开始可能会让人感到困惑。传递这个局部导数的原理,是基于链式法则(chain rule)的。本节将介绍链式法则,并阐明它是如何对应计算图上的反向传播的。

    5.2.1 计算图的反向传播

    话不多说,让我们先来看一个使用计算图的反向传播的例子。假设存在 y = f (x) 的计算,这个计算的反向传播如图 5-6 所示。

    {85%}

    图 5-6 计算图的反向传播:沿着与正方向相反的方向,乘上局部导数

    image.png

    这就是反向传播的计算顺序。通过这样的计算,可以高效地求出导数的值,这是反向传播的要点。那么这是如何实现的呢?我们可以从链式法则的原理进行解释。下面我们就来介绍链式法则。

    5.2.2 什么是链式法则

    介绍链式法则时,我们需要先从复合函数说起。复合函数是由多个函数构成的函数。比如,z=(x+y)2 是由式(5.1)所示的两个式子构成的。

    enter image description here

    链式法则是关于复合函数的导数的性质,定义如下。

    如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。

    image.png

    5.2.3 链式法则和计算图

    现在我们尝试将式(5.4)的链式法则的计算用计算图表示出来。如果用“**2”节点表示平方运算的话,则计算图如图 5-7 所示。

    {88%}

    图 5-7 式(5.4)的计算图:沿着与正方向相反的方向,乘上局部导数后传递

    image.png

    {88%}

    **图 5-8 根据计算图的反向传播的结果

    5.3 反向传播

    上一节介绍了计算图的反向传播是基于链式法则成立的。本节将以“+”和“×”等运算为例,介绍反向传播的结构。

    5.3.1 加法节点的反向传播

    首先来考虑加法节点的反向传播。这里以 z = x + y 为对象,观察它的反向传播。z = x + y 的导数可由下式(解析性地)计算出来。

    enter image description here

    image.png

    {98%}

    图 5-10 加法节点存在于某个最后输出的计算的一部分中。反向传播时,从最右边的输出出发,局部导数从节点向节点反方向传播

    现在来看一个加法的反向传播的具体例子。假设有“10 + 5=15”这一计算,反向传播时,从上游会传来值 1.3。用计算图表示的话,如图 5-11 所示。

    {95%}

    图 5-11 加法节点的反向传播的具体例子

    因为加法节点的反向传播只是将输入信号输出到下一个节点,所以如图 5-11 所示,反向传播将 1.3 向下一个节点传递。

    5.3.2 乘法节点的反向传播

    接下来,我们看一下乘法节点的反向传播。这里我们考虑 z = xy。这个式子的导数用式(5.6)表示。

    enter image description here

    根据式(5.6),可以像图 5-12 那样画计算图。

    {95%}

    图 5-12 乘法的反向传播:左图是正向传播,右图是反向传播

    乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游。翻转值表示一种翻转关系,如图 5-12 所示,正向传播时信号是 x 的话,反向传播时则是 y;正向传播时信号是 y 的话,反向传播时则是 x

    现在我们来看一个具体的例子。比如,假设有“10 × 5 = 50”这一计算,反向传播时,从上游会传来值 1.3。用计算图表示的话,如图 5-13 所示。

    {95%}

    图 5-13 乘法节点的反向传播的具体例子

    因为乘法的反向传播会乘以输入信号的翻转值,所以各自可按 1.3 × 5 = 6.5、1.3 × 10 = 13 计算。另外,加法的反向传播只是将上游的值传给下游,并不需要正向传播的输入信号。但是,乘法的反向传播需要正向传播时的输入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。

    5.3.3 苹果的例子

    再来思考一下本章最开始举的购买苹果的例子(2 个苹果和消费税)。这里要解的问题是苹果的价格、苹果的个数、消费税这 3 个变量各自如何影响最终支付的金额。这个问题相当于求“支付金额关于苹果的价格的导数”“支付金额关于苹果的个数的导数”“支付金额关于消费税的导数”。用计算图的反向传播来解的话,求解过程如图 5-14 所示。

    {95%}

    图 5-14 购买苹果的反向传播的例子

    如前所述,乘法节点的反向传播会将输入信号翻转后传给下游。从图 5-14 的结果可知,苹果的价格的导数是 2.2,苹果的个数的导数是 110,消费税的导数是 200。这可以解释为,如果消费税和苹果的价格增加相同的值,则消费税将对最终价格产生 200 倍大小的影响,苹果的价格将产生 2.2 倍大小的影响。不过,因为这个例子中消费税和苹果的价格的量纲不同,所以才形成了这样的结果(消费税的 1 是 100%,苹果的价格的 1 是 1 日元)。

    最后作为练习,请大家来试着解一下“购买苹果和橘子”的反向传播。在图 5-15 中的方块中填入数字,求各个变量的导数(答案在若干页后)。

    {90%}

    图 5-15 购买苹果和橘子的反向传播的例子:在方块中填入数字,完成反向传播

    5.4 简单层的实现

    本节将用 Python 实现前面的购买苹果的例子。这里,我们把要实现的计算图的乘法节点称为“乘法层”(MulLayer),加法节点称为“加法层”(AddLayer)。

    5.4.1 乘法层的实现

    层的实现中有两个共通的方法(接口)forward()backward()forward() 对应正向传播,backward() 对应反向传播。

    现在来实现乘法层。乘法层作为 MulLayer 类,其实现过程如下所示。

    class MulLayer:
        def __init__(self):
            self.x = None
            self.y = None
    
        def forward(self, x, y):
            self.x = x
            self.y = y
            out = x * y
    
            return out
    
        def backward(self, dout):
            dx = dout * self.y # 翻转x和y
            dy = dout * self.x
    
            return dx, dy
    

    __init__() 中会初始化实例变量 xy,它们用于保存正向传播时的输入值。forward() 接收 xy 两个参数,将它们相乘后输出。backward() 将从上游传来的导数(dout)乘以正向传播的翻转值,然后传给下游。

    上面就是 MulLayer 的实现。现在我们使用 MulLayer 实现前面的购买苹果的例子(2 个苹果和消费税)。上一节中我们使用计算图的正向传播和反向传播,像图 5-16 这样进行了计算。

    {93%}

    图 5-16 购买 2 个苹果

    使用这个乘法层的话,图 5-16 的正向传播可以像下面这样实现。

    apple = 100
    apple_num = 2
    tax = 1.1
    
    # layer
    mul_apple_layer = MulLayer()
    mul_tax_layer = MulLayer()
    
    # forward
    apple_price = mul_apple_layer.forward(apple, apple_num)
    price = mul_tax_layer.forward(apple_price, tax)
    
    print(price) # 220
    

    此外,关于各个变量的导数可由 backward() 求出。

    # backward
    dprice = 1
    dapple_price, dtax = mul_tax_layer.backward(dprice)
    dapple, dapple_num = mul_apple_layer.backward(dapple_price)
    
    print(dapple, dapple_num, dtax) # 2.2 110 200
    

    这里,调用 backward() 的顺序与调用 forward() 的顺序相反。此外,要注意 backward() 的参数中需要输入“关于正向传播时的输出变量的导数”。比如,mul_apple_layer 乘法层在正向传播时会输出 apple_price,在反向传播时,则会将 apple_price 的导数 dapple_price 设为参数。另外,这个程序的运行结果和图 5-16 是一致的。

    5.4.2 加法层的实现

    接下来,我们实现加法节点的加法层,如下所示。

    class AddLayer:
        def __init__(self):
            pass
    
        def forward(self, x, y):
            out = x + y
            return out
    
        def backward(self, dout):
            dx = dout * 1
            dy = dout * 1
            return dx, dy
    

    加法层不需要特意进行初始化,所以 __init__() 中什么也不运行(pass 语句表示“什么也不运行”)。加法层的 forward() 接收 xy 两个参数,将它们相加后输出。backward() 将上游传来的导数(dout)原封不动地传递给下游。

    现在,我们使用加法层和乘法层,实现图 5-17 所示的购买 2 个苹果和 3 个橘子的例子。

    {93%}

    图 5-17 购买 2 个苹果和 3 个橘子

    用 Python 实现图 5-17 的计算图的过程如下所示。

    apple = 100
    apple_num = 2
    orange = 150
    orange_num = 3
    tax = 1.1
    
    # layer
    mul_apple_layer = MulLayer()
    mul_orange_layer = MulLayer()
    add_apple_orange_layer = AddLayer()
    mul_tax_layer = MulLayer()
    
    # forward
    apple_price = mul_apple_layer.forward(apple, apple_num) #(1)
    orange_price = mul_orange_layer.forward(orange, orange_num) #(2)
    all_price = add_apple_orange_layer.forward(apple_price, orange_price) #(3)
    price = mul_tax_layer.forward(all_price, tax) #(4)
    
    # backward
    dprice = 1
    dall_price, dtax = mul_tax_layer.backward(dprice) #(4)
    dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) #(3)
    dorange, dorange_num = mul_orange_layer.backward(dorange_price) #(2)
    dapple, dapple_num = mul_apple_layer.backward(dapple_price) #(1)
    
    print(price) # 715
    print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650
    

    这个实现稍微有一点长,但是每一条命令都很简单。首先,生成必要的层,以合适的顺序调用正向传播的 forward() 方法。然后,用与正向传播相反的顺序调用反向传播的 backward() 方法,就可以求出想要的导数。

    综上,计算图中层的实现(这里是加法层和乘法层)非常简单,使用这些层可以进行复杂的导数计算。下面,我们来实现神经网络中使用的层。

    5.5 激活函数层的实现

    现在,我们将计算图的思路应用到神经网络中。这里,我们把构成神经网络的层实现为一个类。先来实现激活函数的 ReLU 层和 Sigmoid 层。

    5.5.1 ReLU 层

    激活函数 ReLU(Rectified Linear Unit)由下式(5.7)表示。

    {60%}

    通过式(5.7),可以求出 y 关于 x 的导数,如式(5.8)所示。

    {60%}

    在式(5.8)中,如果正向传播时的输入 x 大于 0,则反向传播会将上游的值原封不动地传给下游。反过来,如果正向传播时的 x 小于等于 0,则反向传播中传给下游的信号将停在此处。用计算图表示的话,如图 5-18 所示。

    现在我们来实现 ReLU 层。在神经网络的层的实现中,一般假定 forward()backward() 的参数是 NumPy 数组。另外,实现 ReLU 层的源代码在 common/layers.py 中。

    {95%}

    图 5-18 ReLU 层的计算图

    class Relu:
        def __init__(self):
            self.mask = None
    
        def forward(self, x):
            self.mask = (x <= 0)
            out = x.copy()
            out[self.mask] = 0
    
            return out
    
        def backward(self, dout):
            dout[self.mask] = 0
            dx = dout
    
            return dx
    

    Relu 类有实例变量 mask。这个变量 mask 是由 True/False 构成的 NumPy 数组,它会把正向传播时的输入 x 的元素中小于等于 0 的地方保存为 True,其他地方(大于 0 的元素)保存为 False。如下例所示,mask 变量保存了由 True/False 构成的 NumPy 数组。

    >>> x = np.array( [[1.0, -0.5], [-2.0, 3.0]] )
    >>> print(x)
    [[ 1.  -0.5]
     [-2.   3. ]]
    >>> mask = (x <= 0)
    >>> print(mask)
    [[False  True]
     [ True False]]
    

    如图 5-18 所示,如果正向传播时的输入值小于等于 0,则反向传播的值为 0。因此,反向传播中会使用正向传播时保存的 mask,将从上游传来的 doutmask 中的元素为 True 的地方设为 0。

    5.5.2 Sigmoid 层

    image.png

    image.png

    image.png

    image.png

    现在,我们用 Python 实现 Sigmoid 层。参考图 5-22,可以像下面这样实现。

    class Sigmoid:
        def __init__(self):
            self.out = None
    
        def forward(self, x):
            out = 1 / (1 + np.exp(-x))
            self.out = out
    
            return out
    
        def backward(self, dout):
            dx = dout * (1.0 - self.out) * self.out
    
            return dx
    

    这个实现中,正向传播时将输出保存在了实例变量 out 中。然后,反向传播时,使用该变量 out 进行计算。

    5.6 Affine/Softmax 层的实现

    5.6.1 Affine 层

    神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算(NumPy 中是 np.dot(),具体请参照 3.3 节)。比如,还记得我们用 Python 进行了下面的实现吗?

    >>> X = np.random.rand(2)   # 输入
    >>> W = np.random.rand(2,3) # 权重
    >>> B = np.random.rand(3)   # 偏置
    >>>
    >>> X.shape # (2,)
    >>> W.shape # (2, 3)
    >>> B.shape # (3,)
    >>>
    >>> Y = np.dot(X, W) + B
    

    这里,XWB**** 分别是形状为 (2,)、(2, 3)、(3,) 的多维数组。这样一来,神经元的加权和可以用 Y = np.dot(X, W) + B 计算出来。然后,Y 经过激活函数转换后,传递给下一层。这就是神经网络正向传播的流程。此外,我们来复习一下,矩阵的乘积运算的要点是使对应维度的元素个数一致。比如,如下面的图 5-23 所示,XW 的乘积必须使对应维度的元素个数一致。另外,这里矩阵的形状用 (2, 3) 这样的括号表示(为了和 NumPy 的 shape 属性的输出一致)。

    {65%}

    图 5-23 矩阵的乘积运算中对应维度的元素个数要保持一致

    现在将这里进行的求矩阵的乘积与偏置的和的运算用计算图表示出来。将乘积运算用“dot”节点表示的话,则 np.dot(X, W) + B 的运算可用图 5-24 所示的计算图表示出来。另外,在各个变量的上方标记了它们的形状(比如,计算图上显示了 X 的形状为 (2,),X · W 的形状为 (3,) 等)。

    {85%}

    图 5-24 Affine 层的计算图(注意变量是矩阵,各个变量的上方标记了该变量的形状)

    图 5-24 是比较简单的计算图,不过要注意 XWB 是矩阵(多维数组)。之前我们见到的计算图中各个节点间流动的是标量,而这个例子中各个节点间传播的是矩阵。

    现在我们来考虑图 5-24 的计算图的反向传播。以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同。实际写一下的话,可以得到下式(这里省略了式(5.13)的推导过程)。

    enter image description here

    式(5.13)中 **W**T 的 T 表示转置。转置操作会把 W 的元素 (i, j) 换成元素 (j, i)。用数学式表示的话,可以写成下面这样。

    enter image description here

    如式(5.14)所示,如果 W 的形状是 (2, 3),**W**T 的形状就是 (3, 2)。

    现在,我们根据式(5.13),尝试写出计算图的反向传播,如图 5-25 所示。

    {90%}

    图 5-25 Affine 层的反向传播:注意变量是多维数组。反向传播时各个变量的下方标记了该变量的形状

    image.png

    enter image description here

    image.png

    {90%}

    图 5-26 矩阵的乘积(“dot”节点)的反向传播可以通过组建使矩阵对应维度的元素个数一致的乘积运算而推导出来

    5.6.2 批版本的 Affine 层

    前面介绍的 Affine层的输入 X 是以单个数据为对象的。现在我们考虑 N 个数据一起进行正向传播的情况,也就是批版本的 Affine层。

    先给出批版本的 Affine层的计算图,如图 5-27 所示。

    {93%}

    图 5-27 批版本的 Affine 层的计算图

    image.png

    加上偏置时,需要特别注意。正向传播时,偏置被加到 X · W 的各个数据上。比如,N = 2(数据为 2 个)时,偏置会被分别加到这 2 个数据(各自的计算结果)上,具体的例子如下所示。

    >>> X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
    >>> B = np.array([1, 2, 3])
    >>>
    >>> X_dot_W
    array([[ 0,  0,  0],
           [ 10, 10, 10]])
    >>> X_dot_W + B
    array([[ 1,  2,  3],
           [11, 12, 13]])
    

    正向传播时,偏置会被加到每一个数据(第 1 个、第 2 个……)上。因此,反向传播时,各个数据的反向传播的值需要汇总为偏置的元素。用代码表示的话,如下所示。

    >>> dY = np.array([[1, 2, 3,], [4, 5, 6]])
    >>> dY
    array([[1, 2, 3],
           [4, 5, 6]])
    >>>
    >>> dB = np.sum(dY, axis=0)
    >>> dB
    array([5, 7, 9])
    

    这个例子中,假定数据有 2 个(N = 2)。偏置的反向传播会对这 2 个数据的导数按元素进行求和。因此,这里使用了 np.sum() 对第 0 轴(以数据为单位的轴,axis=0)方向上的元素进行求和。

    综上所述,Affine 的实现如下所示。另外,common/layers.py 中的 Affine 的实现考虑了输入数据为张量(四维数据)的情况,与这里介绍的稍有差别。

    class Affine:
        def __init__(self, W, b):
            self.W = W
            self.b = b
            self.x = None
            self.dW = None
            self.db = None
    
        def forward(self, x):
            self.x = x
            out = np.dot(x, self.W) + self.b
    
            return out
    
        def backward(self, dout):
            dx = np.dot(dout, self.W.T)
            self.dW = np.dot(self.x.T, dout)
            self.db = np.sum(dout, axis=0)
    
            return dx
    

    5.6.3 Softmax-with-Loss 层

    最后介绍一下输出层的 softmax 函数。前面我们提到过,softmax 函数会将输入值正规化之后再输出。比如手写数字识别时,Softmax 层的输出如图 5-28 所示。

    {98%}

    图 5-28 输入图像通过 Affine 层和 ReLU 层进行转换,10 个输入通过 Softmax 层进行正规化。在这个例子中,“0”的得分是 5.3,这个值经过 Softmax 层转换为 0.008(0.8%);“2”的得分是 10.1,被转换为 0.991(99.1%)

    在图 5-28 中,Softmax 层将输入值正规化(将输出值的和调整为 1)之后再输出。另外,因为手写数字识别要进行 10 类分类,所以向Softmax 层的输入也有 10 个。

    下面来实现 Softmax 层。考虑到这里也包含作为损失函数的交叉熵误差(cross entropy error),所以称为“Softmax-with-Loss 层”。Softmax-with-Loss 层(Softmax 函数和交叉熵误差)的计算图如图 5-29 所示。

    {98%}

    图 5-29 Softmax-with-Loss 层的计算图

    可以看到,Softmax-with-Loss 层有些复杂。这里只给出了最终结果,对 Softmax-with-Loss 层的导出过程感兴趣的读者,请参照附录 A。

    图 5-29 的计算图可以简化成图 5-30。

    图 5-30 的计算图中,softmax 函数记为 Softmax 层,交叉熵误差记为 Cross Entropy Error 层。这里假设要进行 3 类分类,从前面的层接收 3 个输入(得分)。如图 5-30 所示,Softmax 层将输入(_a_1, _a_2, _a_3) 正规化,输出(_y_1, _y_2, _y_3)。Cross Entropy Error 层接收 Softmax 的输出(_y_1, _y_2, _y_3) 和教师标签(_t_1, _t_2, _t_3),从这些数据中输出损失 L

    {93%}

    图 5-30 “简易版”的 Softmax-with-Loss 层的计算图

    图 5-30 中要注意的是反向传播的结果。Softmax 层的反向传播得到了(y1-t1, y2-t2, y3-t3)这样“漂亮”的结果。由于(y1, y2, y3)是 Softmax 层的输出,(t1, t2, t3)是监督数据,所以(y1-t1, y2-t2, y3-t3)是 Softmax 层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。

    神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax 的输出)接近教师标签。因此,必须将神经网络的输出与教师标签的误差高效地传递给前面的层。刚刚的(y1-t1, y2-t2, y3-t3)正是 Softmax 层的输出与教师标签的差,直截了当地表示了当前神经网络的输出与教师标签的误差。

    这里考虑一个具体的例子,比如思考教师标签是(0, 1, 0),Softmax 层的输出是 (0.3, 0.2, 0.5) 的情形。因为正确解标签处的概率是 0.2(20%),这个时候的神经网络未能进行正确的识别。此时,Softmax 层的反向传播传递的是 (0.3, -0.8, 0.5) 这样一个大的误差。因为这个大的误差会向前面的层传播,所以 Softmax 层前面的层会从这个大的误差中学习到“大”的内容。

    再举一个例子,比如思考教师标签是 (0, 1, 0),Softmax 层的输出是 (0.01, 0.99, 0) 的情形(这个神经网络识别得相当准确)。此时 Softmax 层的反向传播传递的是 (0.01, -0.01, 0) 这样一个小的误差。这个小的误差也会向前面的层传播,因为误差很小,所以 Softmax 层前面的层学到的内容也很“小”。

    现在来进行 Softmax-with-Loss 层的实现,实现过程如下所示。

    class SoftmaxWithLoss:
        def __init__(self):
            self.loss = None # 损失
            self.y = None    # softmax的输出
            self.t = None    # 监督数据(one-hot vector)
    
        def forward(self, x, t):
            self.t = t
            self.y = softmax(x)
            self.loss = cross_entropy_error(self.y, self.t)
    
            return self.loss
    
        def backward(self, dout=1):
            batch_size = self.t.shape[0]
            dx = (self.y - self.t) / batch_size
    
            return dx
    

    这个实现利用了 3.5.2 节和 4.2.4 节中实现的 softmax()cross_entropy_error() 函数。因此,这里的实现非常简单。请注意反向传播时,将要传播的值除以批的大小(batch_size)后,传递给前面的层的是单个数据的误差。

    5.7 误差反向传播法的实现

    通过像组装乐高积木一样组装上一节中实现的层,可以构建神经网络。本节我们将通过组装已经实现的层来构建神经网络。

    5.7.1 神经网络学习的全貌图

    在进行具体的实现之前,我们再来确认一下神经网络学习的全貌图。神经网络学习的步骤如下所示。

    前提

    神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。神经网络的学习分为下面 4 个步骤。

    步骤 1(mini-batch

    从训练数据中随机选择一部分数据。

    步骤 2(计算梯度)

    计算损失函数关于各个权重参数的梯度。

    步骤 3(更新参数)

    将权重参数沿梯度方向进行微小的更新。

    步骤 4(重复)

    重复步骤 1、步骤 2、步骤 3。

    之前介绍的误差反向传播法会在步骤 2 中出现。上一章中,我们利用数值微分求得了这个梯度。数值微分虽然实现简单,但是计算要耗费较多的时间。和需要花费较多时间的数值微分不同,误差反向传播法可以快速高效地计算梯度。

    5.7.2 对应误差反向传播法的神经网络的实现

    现在来进行神经网络的实现。这里我们要把2层神经网络实现为TwoLayerNet。首先,将这个类的实例变量和方法整理成表 5-1 和表 5-2。

    表 5-1 TwoLayerNet 类的实例变量

    实例变量说明
    params保存神经网络的参数的字典型变量。 params['W1'] 是第 1 层的权重,params['b1'] 是第 1 层的偏置。 params['W2'] 是第 2 层的权重,params['b2'] 是第 2 层的偏置
    layers保存神经网络的层的有序字典型变量。 以 layers['Affine1']layers['ReLu1']layers['Affine2'] 的形式,通过有序字典保存各个层
    lastLayer神经网络的最后一层。本例中为 SoftmaxWithLoss

    表 5-2 TwoLayerNet 类的方法

    方法说明
    __init__(self, input_size, hidden_size, output_size, weight_init_std)进行初始化。 参数从头开始依次是输入层的神经元数、隐藏层的神经元数、输出层的神经元数、初始化权重时的高斯分布的规模
    predict(self, x)进行识别(推理)。 参数 x 是图像数据
    loss(self, x, t)计算损失函数的值。 参数 X 是图像数据、t 是正确解标签
    accuracy(self, x, t)计算识别精度
    numerical_gradient(self, x, t)通过数值微分计算关于权重参数的梯度(同上一章)
    gradient(self, x, t)通过误差反向传播法计算关于权重参数的梯度

    这个类的实现稍微有一点长,但是内容和 4.5 节的学习算法的实现有很多共通的部分,不同点主要在于这里使用了层。通过使用层,获得识别结果的处理(predict())和计算梯度的处理(gradient())只需通过层之间的传递就能完成。下面是 TwoLayerNet 的代码实现。

    import sys, os
    sys.path.append(os.pardir)
    import numpy as np
    from common.layers import *
    from common.gradient import numerical_gradient
    from collections import OrderedDict
    
    class TwoLayerNet:
    
        def \_\_init\_\_(self, input\_size, hidden\_size, output_size,
                     weight\_init\_std=0.01):
            # 初始化权重
            self.params = {}
            self.params\['W1'\] = weight\_init\_std * \
                                np.random.randn(input\_size, hidden\_size)
            self.params\['b1'\] = np.zeros(hidden_size)
            self.params\['W2'\] = weight\_init\_std * \
                                np.random.randn(hidden\_size, output\_size)
            self.params\['b2'\] = np.zeros(output_size)
    
            # 生成层
            **self.layers = OrderedDict()**
            **self.layers\['Affine1'\] = \**
                **Affine(self.params\['W1'\], self.params\['b1'\])**
            **self.layers\['Relu1'\] = Relu()**
            **self.layers\['Affine2'\] = \**
                **Affine(self.params\['W2'\], self.params\['b2'\])**
    
            **self.lastLayer = SoftmaxWithLoss()**
    
        def predict(self, x):
            **for layer in self.layers.values():**
                **x = layer.forward(x)**
    
            return x
    
        # x:输入数据, t:监督数据
        def loss(self, x, t):
            y = self.predict(x)
            return self.lastLayer.forward(y, t)
    
        def accuracy(self, x, t):
            y = self.predict(x)
            y = np.argmax(y, axis=1)
            if t.ndim != 1 : t = np.argmax(t, axis=1)
            accuracy = np.sum(y == t) / float(x.shape\[0\])
            return accuracy
    
        # x:输入数据, t:监督数据
        def numerical_gradient(self, x, t):
            loss_W = lambda W: self.loss(x, t)
    
            grads = {}
            grads\['W1'\] = numerical\_gradient(loss\_W, self.params\['W1'\])
            grads\['b1'\] = numerical\_gradient(loss\_W, self.params\['b1'\])
            grads\['W2'\] = numerical\_gradient(loss\_W, self.params\['W2'\])
            grads\['b2'\] = numerical\_gradient(loss\_W, self.params\['b2'\])
    
            return grads
    
        def gradient(self, x, t):
            **\# forward**
            **self.loss(x, t)**
    
            **\# backward**
            **dout = 1**
            **dout = self.lastLayer.backward(dout)**
    
            **layers = list(self.layers.values())**
            **layers.reverse()**
            **for layer in layers:**
                **dout = layer.backward(dout)**
    
            # 设定
            grads = {}
            grads\['W1'\] = self.layers\['Affine1'\].dW
            grads\['b1'\] = self.layers\['Affine1'\].db
            grads\['W2'\] = self.layers\['Affine2'\].dW
            grads\['b2'\] = self.layers\['Affine2'\].db
    
            return grads
    

    请注意这个实现中的粗体字代码部分,尤其是将神经网络的层保存为 OrderedDict 这一点非常重要。OrderedDict 是有序字典,“有序”是指它可以记住向字典里添加元素的顺序。因此,神经网络的正向传播只需按照添加元素的顺序调用各层的 forward() 方法就可以完成处理,而反向传播只需要按照相反的顺序调用各层即可。因为 Affine 层和 ReLU 层的内部会正确处理正向传播和反向传播,所以这里要做的事情仅仅是以正确的顺序连接各层,再按顺序(或者逆序)调用各层。

    像这样通过将神经网络的组成元素以层的方式实现,可以轻松地构建神经网络。这个用层进行模块化的实现具有很大优点。因为想另外构建一个神经网络(比如 5 层、10 层、20 层……的大的神经网络)时,只需像组装乐高积木那样添加必要的层就可以了。之后,通过各个层内部实现的正向传播和反向传播,就可以正确计算进行识别处理或学习所需的梯度。

    5.7.3 误差反向传播法的梯度确认

    到目前为止,我们介绍了两种求梯度的方法。一种是基于数值微分的方法,另一种是解析性地求解数学式的方法。后一种方法通过使用误差反向传播法,即使存在大量的参数,也可以高效地计算梯度。因此,后文将不再使用耗费时间的数值微分,而是使用误差反向传播法求梯度。

    数值微分的计算很耗费时间,而且如果有误差反向传播法的(正确的)实现的话,就没有必要使用数值微分的实现了。那么数值微分有什么用呢?实际上,在确认误差反向传播法的实现是否正确时,是需要用到数值微分的。

    数值微分的优点是实现简单,因此,一般情况下不太容易出错。而误差反向传播法的实现很复杂,容易出错。所以,经常会比较数值微分的结果和误差反向传播法的结果,以确认误差反向传播法的实现是否正确。确认数值微分求出的梯度结果和误差反向传播法求出的结果是否一致(严格地讲,是非常相近)的操作称为梯度确认(gradient check)。梯度确认的代码实现如下所示。

    import sys, os
    sys.path.append(os.pardir)
    import numpy as np
    from dataset.mnist import load_mnist
    from two_layer_net import TwoLayerNet
    
    # 读入数据
    (x_train, t_train), (x_test, t_test) = \ load_mnist(normalize=True, one_
    hot_label = True)
    
    network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
    
    x_batch = x_train[:3]
    t_batch = t_train[:3]
    
    grad_numerical = network.numerical_gradient(x_batch, t_batch)
    grad_backprop = network.gradient(x_batch, t_batch)
    
    # 求各个权重的绝对误差的平均值
    for key in grad_numerical.keys():
        diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
        print(key + ":" + str(diff))
    

    和以前一样,读入 MNIST 数据集。然后,使用训练数据的一部分,确认数值微分求出的梯度和误差反向传播法求出的梯度的误差。这里误差的计算方法是求各个权重参数中对应元素的差的绝对值,并计算其平均值。运行上面的代码后,会输出如下结果。

    b1:9.70418809871e-13
    W2:8.41139039497e-13
    b2:1.1945999745e-10
    W1:2.2232446644e-13
    

    从这个结果可以看出,通过数值微分和误差反向传播法求出的梯度的差非常小。比如,第 1 层的偏置的误差是 9.7e-130.00000000000097)。这样一来,我们就知道了通过误差反向传播法求出的梯度是正确的,误差反向传播法的实现没有错误。

    5.7.4 使用误差反向传播法的学习

    最后,我们来看一下使用了误差反向传播法的神经网络的学习的实现。和之前的实现相比,不同之处仅在于通过误差反向传播法求梯度这一点。这里只列出了代码,省略了说明。

    import sys, os
    sys.path.append(os.pardir)
    import numpy as np
    from dataset.mnist import load_mnist
    from two\_layer\_net import TwoLayerNet
    
    \# 读入数据
    (x\_train, t\_train), (x\_test, t\_test) = \
        load\_mnist(normalize=True, one\_hot_label=True)
    
    network = TwoLayerNet(input\_size=784, hidden\_size=50, output_size=10)
    
    iters_num = 10000
    train\_size = x\_train.shape\[0\]
    batch_size = 100
    learning_rate = 0.1
    train\_loss\_list = \[\]
    train\_acc\_list = \[\]
    test\_acc\_list = \[\]
    
    iter\_per\_epoch = max(train\_size / batch\_size, 1)
    
    for i in range(iters_num):
        batch\_mask = np.random.choice(train\_size, batch_size)
        x\_batch = x\_train\[batch_mask\]
        t\_batch = t\_train\[batch_mask\]
    
        **\# 通过误差反向传播法求梯度**
        **grad = network.gradient(x\_batch, t\_batch)**
    
        # 更新
        for key in ('W1', 'b1', 'W2', 'b2'):
            network.params\[key\] -= learning_rate * grad\[key\]
    
        loss = network.loss(x\_batch, t\_batch)
        train\_loss\_list.append(loss)
    
        if i % iter\_per\_epoch == 0:
            train\_acc = network.accuracy(x\_train, t_train)
            test\_acc = network.accuracy(x\_test, t_test)
            train\_acc\_list.append(train_acc)
            test\_acc\_list.append(test_acc)
            print(train\_acc, test\_acc)
    
    
    展开全文
  • 关于大型网站技术演进的思考(十八):网站静态化处理—反向代理(10)       反向代理也是一种可以帮助实现网站静态化的重要技术,今天我就来讲讲反向代理这个主题。那么首先我们要了解下什么是反向代理。...

    http://blog.jobbole.com/84669/

     

     

    关于大型网站技术演进的思考(十八):网站静态化处理—反向代理(10

     

     

     

    反向代理也是一种可以帮助实现网站静态化的重要技术,今天我就来讲讲反向代理这个主题。那么首先我们要了解下什么是反向代理。和反向代理相对应的是正向代理,正向代理也就是我们常说的代理服务,正向代理是非常常见的,例如在某些公司里我们想使用互联网,那么我们就得在浏览器里设置一个代理服务器,通过代理服务器我们才能正常使用互联网,而这个代理服务器就是一个正向代理服务器。正向代理更加让人熟悉的使用场景估计还是在FQ技术里的使用,我们使用一个放置在国外的代理服务器来访问那些在国内无法正常访问的网站,这其实也是在使用一个正向代理服务。

    其实不管是正向代理还是反向代理,这两个概念的定义都是以浏览器侧为基准进行的,正向代理是代理浏览器来访问互联网,反向代理是指代理不再是代理浏览器侧了,而是反过来代理浏览器需要访问的应用服务器。那为什么我们要使用正向代理服务器了?答案当然不是为了FQ了,下面我来列举些实例来说明这个问题了。

    例如公司里使用代理服务器主要是为了安全的考虑,很多公司内部都有自己的局域网,一般我们称之为内网,内网里有公司的各种资源,如果公司员工的电脑随意连接到互联网,假如碰到那些别有用心的黑客,通过攻击员工的工作电脑截取了公司重要的文件资料,那样就会造成公司的重大损失,正向代理除了能防范外部的黑客攻击外还能监控和控制公司内部员工将公司重要文件通过互联网传递给不恰当的人,因此公司让员工使用代理上网基本都是出于安全的角度来考虑的。

    正向代理的合理使用还能帮助一些企业提升自己产品的核心竞争力,例如在移动端有一款非常流行的浏览器,它之所以非常受用户的欢迎,是因为使用该浏览器上网速度比其他浏览器明显的快多了,那么这款浏览器是如何做到这点的呢?奥秘就是这家公司为自己的浏览器对应建立一个十分强大的代理服务器集群,用户使用该浏览器访问网站时候用户首先访问的是该公司的代理服务器,而这些代理服务器使用缓存技术缓存了海量的网站信息,再加上使用一些web加速的技术例如CDN技术,这就让该浏览器访问网站的效率明显快于其他浏览器。

    反向代理和正向代理从技术角度上基本上是一致的,区别主要是代理的内容不一样了,反向代理代理的是应用服务器。反向代理技术也基本上是互联网公司的一个标配技术,但是反向代理能否正确使用,能否更进一步的发挥它的实用价值,我觉得并不是所有公司都能做好的,下面我来总结一下反向代理的使用目的吧,具体如下:

    使用目的一:反向代理可以隐藏真实的应用服务器。该目的属于安全的范畴,反向代理隐藏真实的应用服务器,那么就可以让别有用心的黑客很难掌握正确的应用服务器,从而增加黑客的攻击难度。

    使用目的二:反向代理可以实现负载均衡的功能,例如在java的web开发里有一种很简单的实现集群的手段,这个手段就是使用apache加上tomcat的组合,用户请求先到达前置的apache服务器,apache再使用负载均衡策略将请求分配给后台不同的tomcat服务器上。

    使用目的三:反向代理可以起到动态调节应用服务器并发数的目的,一般用作反向代理的服务器都是静态资源服务器,这样的服务器在并发处理能力上要远强于后台的web应用服务器,那么可以通过控制web应用服务器前置的反向代理服务器,这样就可以动态调节后台服务的负载的大小,这个做法的好处可能很多朋友都不太了解,这里我列举个例子,一个网站最需要稳定性的部分是哪个部分呢?很多朋友会说是数据库,的确数据库是最重要的,因为数据库做分布式很难,很容易形成单点故障,要是数据库挂了基本一切都没法玩了,那么除了数据库之外还有别的吗?当然有,那就是用于处理业务的应用服务器了,应用服务器如果做了集群,集群中其中一台服务器挂了其影响面会比数据库挂掉低多了,但是一个网站的做业务处理的应用服务器挂掉,对公司的损失还是很大的,而web应用服务器前面的用作反向代理的静态资源服务器挂掉问题就会小多了,至少不会产生公司业务无法正常完成的事情了,因此当网站负载过高,让过载的请求被反向代理拦截或者阻止,这对应用服务器的稳定性提升有莫大的好处。当然反向代理调节应用服务器的负载水平的用途不仅仅这些,有兴趣的朋友可以在网络上找找相关的介绍。

    使用目的四:反向代理可以缓存静态数据,一般用作反向代理的服务器都是使用像apache或者是ngnix这样的静态资源服务器,因此我们可以把web应用里的静态资源缓存在反向代理服务器上,从而达到提升请求处理的速度问题。反向代理的这个功能就和本系列的主题网站静态化处理切合了。

    分析完反向代理的使用目的后,我们现在将反向代理应用到项目里,这里应用的一个前置限定就是将反向代理应用到网站静态化的处理之上,首先是第一个应用方式,如下图所示:

    第一种反向代理应用方式就是让反向代理和应用服务器一一对应,也就是每台应用服务的部署服务器上都对应部署一台反向代理服务器,这么做有怎样的好处呢?首先我们来讲第一个好处,如果我们将网页做了动静分离,那么反向代理服务器就可以负责对请求中的静态资源访问进行处理,同时反向代理还可以承担动静资源整合的目的。这里要特别说明下,前文里我说道动静资源会因为我们使用的动静策略而发生转化,那么有些动态内容在一定条件被转化为静态资源后,我们可以将这些做了转化的静态资源在服务器上缓存起来,这个时候上图展示的架构模型就会发生变化,如下图所示:

    我们看到反向代理服务器和应用服务器之间会形成一个cache层,反向代理访问cache层的效率会比直接访问应用服务器要高的多,这等于是给应用服务器做了一个加速操作,同时通过缓存我们可以减少应用服务器的运算压力,从而达到提升应用服务器性能的目的。以前有朋友问我这么做会不会增加应用服务器的压力,因为一台服务器上部署了两台可以处理web请求的服务器,那么它们之间一定会有发生冲突的时候,不过我想产生冲突肯定是我们没有很好的处理二者关系所致,所以我们要理清在同一台服务器上部署反向代理和应用服务器后,它们之间的关系到底是怎样的?

    其实反向代理和应用服务器从物理形态角度上它们是两个不同的东西,但是二者在逻辑上其实是一个整体,它们共同完成一个逻辑性的应用服务器的功能,只不过二者因为应用场景不同而形成了一种分工合作的关系,反向代理服务器主要完成对静态资源请求的处理,而应用服务器则是负责业务逻辑的处理,它们最终形成一个强大的合力使得整体的逻辑性应用服务器的性能得到显著的提升。

    除此之外,这个反向代理还可以发挥动态调节应用服务器的并发数的目的,但是上面的技术方案却没有发挥反向代理的负载均衡以及安全性这两个方面的作用。为了让反向代理四个使用目的得到充分的发挥,那么我们该如何来做了?

    方法很简单就是把反向代理的部署地点从应用服务器所在的物理服务器上迁移出来,放到一台独立的物理服务器上,但是这个做法会有性能上的损失,同时还会增加整个技术架构的复杂性。为什么性能会损失呢?因为原来的反向代理服务器和应用服务器部署在同一个物理服务器上,那么它们之间的通讯都是以内存共享的方式进行的,这样的通讯效率是非常高,现在换成了通过网络通讯进行沟通,而网络通讯是IO设备里效率最差,可靠性最差的,因此单独部署反向代理服务器或多或少都会造成一定性能的损失。

    为什么说单独部署反向代理会增加整个网站技术架构的复杂性了?我们把反向代理服务器单独部署,那么单独部署时候我们还会是使用一一对应的策略吗?先不谈这么做,从技术和业务角度的好处和坏处,但从成本这个考虑就是会让很多公司望而却步,因为这个做法就会导致用于部署应用服务器的成本翻倍的增加,而增加的服务器用于反向代理,这样的做法怎么体会都不是觉得物有所值,再说用于反向代理的静态资源服务器本身处理请求的并发能力是普通应用服务器的数百倍,一一对应本身也没有完全发挥反向代理服务器的潜力,因此最好的解决方法就是把反向代理服务器做成一个反向代理服务器集群(zpf:这里你给我解释解释,将反向代理与web服务器分开后,没能充分发挥反向代理的性能,然后解决方法就是部署成反向代理集群?呵呵,一台反向代理性能还能充分发挥,部署成集群,部署成多台反向代理,就能充分发挥性能了?),做成集群问题又来,集群里每台反向代理缓存的数据是不是要保持一个同步了?这就好比处理应用服务器的session同步问题,如果真的这么做会不会导致反向代理服务器上缓存大量使用率不高的数据从而导致缓存的利用率很差,同时同步操作本身也会影响到反向代理集群的性能,所以要设计一个好的反向代理集群是一件十分复杂的事情,其实合理的反向代理集群的做法就是在集群里在进行分组,每个分组应该是和后端的SOA服务相匹配,这个时候反向代理集群的效率才能得到最大的发挥,同时资源利用率也会更加的合理。其实使用反向代理集群方式,也会给生产部署造成麻烦,如果网站进行了静态化处理,那么反向代理需要承担对静态资源的处理操作,这个时候反向代理和对应的应用服务器结合起来才能形成一个完整的应用服务器,但是现在我们将一个完整的逻辑应用服务器分开部署了,那么当我们发布新应用的时候就得面临更加复杂的情况,这就增加了部署和运维的风险和难度。

    我如此批评单独部署反向代理的问题,但是我并不是说这种做法完全不可取(zpf:我有问题:你批评单独部署反向代理,但是你记得你怎么提到单独部署反向代理的吗,你说之前方案没有发挥反向代理的负载均衡以及安全性这两个方面的作用。为了让反向代理四个使用目的得到充分的发挥,才单独部署,然后单独部署会有性能和技术的问题,那我要问题,你批评,就是说你不建立单独部署,那负载均衡与安全性怎么解决?),而是想告诉大家这种做法其实是一种高级的做法,但是也是一个复杂的做法,要做好这个集群是很麻烦的一件事情的,我觉得只有当我们的网站业务量和请求量很大的时候,同时原有方案出现了瓶颈时候可以认真考虑反向代理集群方案的实现,不过将反向代理形成集群会给网站的安全性带来莫大的好处,反向代理可以隐藏后台的应用服务器,这种隐藏就是客户端只需要访问代理服务器即可,应用服务器对外都是以反向代理来展示的,但是如果反向代理和应用服务器一一对应,那么恶意黑客找准了某台反向代理服务器后,对这个反向代理服务器进行反复的攻击,那么这个攻击也就等于攻击与之对应的应用服务器,这就导致反向代理隐藏真实应用服务器的作用就没有得到有效的发挥,而集群这块就可以很好的处理这个问题,不过我们如果觉得使用集群代价太高,我们也有变通的方法,那就是在所有逻辑应用服务器前面再放置一个反向代理服务器,这个反向代理服务器不再承担缓存的功能,而只是用来做负载均衡和安全处理,这样一一对应的策略安全性也可以得到保证,不过如果公司技术能力好可以考虑使用LVS这种软件化的负载均衡技术方案,假如公司还很有钱还可以考虑使用更加高级的硬件负载均衡设备例如F5设备。

    如果我们网站除了使用网站静态化技术还使用了前后端分离技术,当然这个前后端分离技术应该是使用nodejs的前后端分离技术,那么nodejs应该放置在生产部署的什么位置上了?上篇文章里我曾列举了一个nodejs的应用实践场景,在这个实践场景里我曾经提到如果在原有的网站生产架构下引入nodejs会增加一个请求处理环节,而nodejs使用主要是为了满足前后端分离而非增加网站性能,因此增加的环节可能会让请求处理的性能下降,因此我最后提出一种变通手法,就是nodejs项目发布时候编译源代码,然后将编译出的javascript和html文件干脆推移到浏览器端处理,这样就变相形成了前端MVC框架,这个做法总是有点不伦不类的意味,假如我们真的想把nodejs引入到应用生产的网络架构里,我们不希望无端的增加请求处理环节,那么最好是让nodejs服务器替换某个部分。按照这个思路思考,那么我觉得nodejs在生产的引入最好是和反向代理相关,最简单的方式就是让nodejs和反向代理一一对应,这样就可以很好的降低引入nodejs带来的问题,当然复杂点的就是反向代理集群对应的应用服务器应该是nodejs的应用服务器,而不是用来做业务处理的业务级别的应用服务器。

    不管怎么说,我认为在网站静态化方案里我们一定要考虑反向代理的运用,如果静态化技术方案里没有反向代理的身影,那么这个网站静态化处理可能很难达到我们预期的效果。

    好了,今天就写到这里,最后祝大家晚安。

     

    展开全文
  • 思考一个问题 链式法则 计算图的反向传播 链式法则和计算图 加法节点的反向传播 乘法节点的反向传播 “购买水果”问题的反向传播 激活函数(层)的反向传播 激活函数ReLU的反向传播 激活函数Sigmoid的反向...

    目录

    前言

     计算图

    计算图求解示例

    计算图的优点

    反向传播

    思考一个问题

    链式法则

    计算图的反向传播

    链式法则和计算图

    加法节点的反向传播

    乘法节点的反向传播

    “购买水果”问题的反向传播

    激活函数(层)的反向传播

     激活函数ReLU的反向传播

    激活函数Sigmoid的反向传播

    Affine/softmax激活函数的反向传播

    Affine层

    softmax-with-loss层的反向传播

    误差反向传播法的Python实现

    乘法层的Python实现

    加法层的Python实现

    购买水果问题的Python实现

    激活函数层的Python实现

    ReLU层的Python实现

    sigmoid层的Python实现

    Affine层的Python实现

    softmax-with-loss层的Python实现

    误差反向传播法的Python总体实现

    小结


    前言

    计算梯度的传统方法一般采用基于数值微分实现,如下式:

                                                        

    \frac{df\left ( x \right )}{dx}=\lim_{h\rightarrow 0}\frac{f\left ( x+h \right )-f\left ( x \right )}{h}

    虽然数值微分方法比较直观、简单、易于理解,但是计算比较费时间,不适合需频繁计算导数的场合,比如多层神经网络中权重参数的梯度计算。前面我们讲过利用数值微分计算了神经网络中的损失函数关于权重参数的梯度\frac{\partial L}{\partial W},而本专题我们将介绍“误差反向传播法”,实现损失函数关于权重参数梯度\frac{\partial L}{\partial W}的高效计算。本主题将从简单的问题开始,逐步深入,最终直达误差反向传播法。

    理解误差反向传播法,一般有基于数学式的方法和基于计算图的方法。前者比较常见、简洁和严密,但是不直观,因此笔者选择了更加直观、易于理解的计算图来讲解误差反向传播法。

     计算图

     计算图,即将计算过程用图形表示出来,当然,这里的图形指的是具有数据结构(和流程图类似的图形),一般有节点和连接节点的边(线)组成,如图1所示。

    图1 计算图

     图1中圆圈O表示节点,圈内的符号表示计算符号(比如加减乘除),箭头的指向表示节点计算结果的传递方向(图1为从左向右传播,即正向传播),直线上方一般放置中间计算结果(如100)。下面我们基于计算图来分析一个具体的示例。

    计算图求解示例

    问题描述:

    小明在超市买了2个苹果、3个橘子。其中,苹果每个100日元,橘子每个150日元。消费税是10%,请计算支付金额。

    首先,我们采用传统的数学思路来计算支付金额:2个苹果,单价为100日元,因此购买苹果花了200日元;3个橘子,单价150日元,因此,够买橘子花了450日元;因此购买苹果和橘子一共花了650日元,由于消费税是10%,所以支付金额为:650+650·10%=715日元。

    下面我们采用基于计算图来计算支付金额,先直接给出计算图如图2所示,再分析。

    图2  基于计算图计算支付金额

     

    由计算图分析得到的结果和传统方法分析得到的结果一样,均为715日元。现在我们结合图2来分析该计算图,计算图的输入有苹果单价、苹果数量、橘子单价、橘子数量、消费税,中间的计算结果均放置(保存)在直线上面,这里需要强调的是初始输入量的值我们也视为中间结果,比如苹果单价视为输入量(实际等效于一个变量),而100为中间变量(等效于一个实例值)。改图一共有4个节点,其中3节点运算为乘法运算,1个节点运算为加法运算。计算方向为从左至右,这是一种正方向的传播,简称为正向传播。指向节点的箭头可以视为输入,比如上图中输入至加法节点的有两个输入量(苹果总价和橘子总价),当然输入量是没有限制的。图中消费税的中间计算过程为1.1(1+10%),这么做的目的主要是可以直接和水果总价进行乘法运算,当然读者可以自行设计图2中的消费税这一节点。

    我们不难理解,正向传播就是从计算图出发点到结束点的传播,既然有正向传播,那么应该也有反向传播。是的,从右向左的传播就是我们后面将重点关注的反向传播

    计算图的优点

    通过上面给出的计算图求解实际问题的示例可知,计算图的特征是可以通过传递“局部计算”获得最终结果。所谓“局部”,指的是无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果。假设上面的例子中,购买水果总共花费了650日元,我们不关心650日元是通过什么样的计算得到的,只关心把650日元作为该节点的输出并和其他节点进行运算。换句话说,各个节点处只需进行与自己有关的计算,不用考虑全局。无论全局是多么复杂的计算,都可以通过局部计算使各个节点致力于简单的计算,从而简化问题。

    另一个优点是,利用计算图可以将中间的计算结果全部保存起来(比如200、450、650....),为反向传播的计算提供已知数据。

    反向传播

    思考一个问题

    在上面的问题中,我们计算了购买苹果和橘子时加上消费税最终需要支付的金额。假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金额关于苹果价格的导数”。设苹果的价格为x,支付金额为L,则相当于求\frac{\partial L}{\partial x}。这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。

    首先,我们利用传统的数学解题思路来求解,假设苹果价格上涨了\Delta x日元,支付金额增加了\Delta L日元,则有:

                         \Delta L=\left \{ \left ( 100+\Delta x \right )\cdot 2+150\cdot 3 \right \}\cdot 1.1-\left ( 100\cdot 2+150\cdot 3 \right )\cdot 1.1=2.2\Delta x

    通过数学解题思路,我们得到了支付金额关于苹果的价格的导数为2.2,即苹果价格上涨1日元,则最终的支付金额将会增加2.2日元。现在我们先直接给出利用反向传播法分析得到的结果。图中加粗的箭头表示反向传播,箭头下面的结果表示“局部导数”,也就是说,反向传播传递的是导数。从图中可知,支付金额关于苹果单价的导数的值是2.2,这和数学解题思路得到的答案一样。当然,除了求关于苹果的价格的导数,其他的比如支付金额关于消费税的导数、支付金额关于橘子价格的导数等问题也可以采用同样的方式算出来。

    图3  反向传播求支付金额关于苹果单价的导数

    从图3中还可发现,计算中途求得的导数的结果(比如1.1)可以被共享,从而高效地计算多个导数。因此,计算图可以通过正向传播和反向传播高效地计算各个变量的导数值。反向传播传递导数的原理,是基于链式法则。

    链式法则

    反向传播将局部导数从右到左进行传递的原理是基于链式法则,要理解链式法则,我们还得从复合函数说起。复合函数是由多个函数构成的函数。比如z=\left ( x+y \right )^{2}是由下面的两个式子构成的。

                                                             z=t^{2}

                                                             t=x+y                                                                          (1)

    这里,链式法则是关于符合函数的导数的性质,如下:

    如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。

    例如,\frac{\partial z}{\partial x}可以用\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}的乘积表示。即:

                                                                     \frac{\partial z}{\partial x}=\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}

    现在使用链式法则,我们来求式(1)的导数\frac{\partial z}{\partial x}。首先要求它的局部导数:

                                                   \frac{\partial z}{\partial t}=2t

                                                  \frac{\partial t}{\partial x}=1                                                                                     (2)

    所以\frac{\partial z}{\partial x}的导数为:

                                            \frac{\partial z}{\partial x}=\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=2t\cdot 1=2\left ( x+y \right )

    计算图的反向传播

    假设存在y=f\left ( x \right )的计算,则这个计算的反向传播如图4所示。

    图4  计算图的反向传播

    如图所示,反向传播的计算顺序是:将信号E乘以节点的局部导数\frac{\partial y}{\partial x},然后将结果传递给下一个节点。这里所说的局部导数是指正向传播中y=f\left ( x \right )的导数,也就是\frac{\partial y}{\partial x},比如y=f\left ( x \right )=x^{2},则局部导数为\frac{\partial y}{\partial x}=2x。把这个局部导数乘以上游传过来的值(本例中的E),然后传递给前面的节点。(这里给大家说一下,如果是神经网络,那么最上游应该是损失函数)。

    这就是反向传播的计算程序,结合链式法则可以高效地求出多个导数的值。

    链式法则和计算图

    现在我们用计算图的方法把式(1)的链式法则表示出来。如图5所示,这里我们用“**2”表示平方运算。

    图5  式(2)的计算图:沿着与正方向相反的方向,乘上局部导数后传递

    反向传播时,“**2”节点的输入是\frac{\partial z}{\partial z},将其乘以局部导数\frac{\partial z}{\partial t}(因为正向传播时输入是t,输出是z,所以这个节点的局部导数是\frac{\partial z}{\partial t}),然后传递给下一个节点。这里需要提醒的是,反向传播最开始的信号\frac{\partial z}{\partial z}在前面的数学式中没有出现,因为\frac{\partial z}{\partial z}=1。根据链式法则,最左边的反向传播结果\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=\frac{\partial z}{\partial x}成立,对应于“z关于x的导数”。

    现在我们把式(2)的结果代入到图5中,可得\frac{\partial y}{\partial x}=2\left ( x+y \right ),如图6所示。

    图6  反向传播的结果

    到这里,读者也许会生产疑问:反向传播过程中的数字1是怎么得到的,下面的内容将为大家解释这个问题。

    加法节点的反向传播

    加法节点指的是节点运算为加法运算,以 z=x+y为例,则z关于xy的导数为

                                                          \frac{\partial z}{\partial x}=1

                                                         \frac{\partial z}{\partial y}=1

    假设 z=x+y通过某种运算的结果为L,则加法节点的正向传播和反向传播的计算图如下:

    图7 加法节点的反向传播将上游的值原封不动地输出到下游

     我们通过解析性求导,得到z关于xy的导数均为1,因此计算图中,反向传播将上游传过来的导数值(本例中是\frac{\partial L}{\partial z},因为正向传播的输入为z,输出为L)乘以1,然后传向下游。也就是说,加法节点的反向传播只乘以1,所以输入的值会原封不动地流向下一个节点。

    乘法节点的反向传播

    假设有z=xy,则z关于xy的导数为:

                                                                                 \frac{\partial z}{\partial x}=y

                                                                                 \frac{\partial z}{\partial y}=x

    用计算图表示乘法节点的正向传播和反向传播如图8所示。

    图8 乘法节点反向传播将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游

     乘法的反向传播会将上游的值乘以正向传播时的输入信号的'翻转值"后传递给下游。翻转值表示一种翻转关系,正向传播时信号是x的话,反向传播时则是y;正向传播时信号是y的话,反向传播时则是x。这里需要提醒大家的是,加法的反向传播只是将上游的值传递给下游,并不需要正向传播的输入信号。而乘法的反向传播需要正向传播时的输入信号值,因此要实现乘法节点的反向传播时,需要保存正向传播的输入信号。

    “购买水果”问题的反向传播

    现在我们回到前面给出的问题“购买水果,求支付金额”,因为我们已经介绍了加法和乘法的反向传播,所以我们试着来分析“购买水果”的反向传播,即求包括金额关于苹果单价的导数等其他变量的导数。读者只需记住两点:加法的反向传播将上游传递来的值会原封不动地传递给下游;乘法的反向传播会将输入信号翻转后传递给下游。因此“购买水果”的反向传播的计算图如图9所示。

    图9购买水果的反向传播

    可知,苹果的价格的导数为2.2,橘子的价格的导数为3.3(说明橘子的价格的波动比苹果价格的波动对最终的支付金额的影响更大),消费税的导数是650(消费税的1是100%,水果的价格的1是1日元,所以才形成了这么大的消费税的导数)。

    激活函数(层)的反向传播

     激活函数ReLU的反向传播

    激活函数ReLU的表达式如下式(3):

                                           y=\left\{\begin{matrix} x & \left ( x>0 \right )\\ 0& \left ( x\leq 0 \right ) \end{matrix}\right.                                                 (3)

    y关于x的导数如式(4):

                                          \frac{\partial y}{\partial x}=\left\{\begin{matrix} 1 & \left ( x>0 \right )\\ 0& \left ( x\leq 0 \right ) \end{matrix}\right.                                                 (4)

    由式(4)可知,如果正向传播时的输入x大于0,则反向传播会将上游的值原封不动地传递给下游。如果正向传播时的x小于等于0,则反向传播中传给下游的信号将停止在此处,即反向传播的值为0。用计算图表示如图9所示。

    图9 ReLU层的计算图

    激活函数Sigmoid的反向传播

    sigmoid函数的表达式如式(5)所示。

                                                      y=\frac{\mathrm{1} }{\ 1+exp\left ( -x \right )}                                    (5)

    其计算图如图10所示。

    图10 sigmoid函数的计算图

    说明一下,式(5)的计算由局部计算的传播构成,“exp”节点会进行y=exp\left ( x \right )的计算,“/”会进行y=\frac{\mathrm{1} }{\ x}的计算。下面我们来分析图10的计算图的反向传播。

    第一步:

    节点“/”表示y=\frac{\mathrm{1} }{\ x}的计算,则它的导数如式(6)所示。

                                                                        \frac{\partial y}{\partial x}=-\frac{\mathrm{1} }{\ x^{2}}=-y^{2}                                                        (6)

    可知,“/”节点运算时的反向传播会将上游的值乘以-y^{2}(正向传播的输出的平方乘以-1后的值)后,再传给下游。计算图如图11所示。

    图11 除法节点的反向传播的计算图

    第二步:

    “+”节点将上游的值原封不动地传给下游。计算图如图12所示。

    图 12 加法节点的反向传播的计算图

    第三步:

    “exp”节点表示y=exp\left ( x \right ),则它的导数如式(7)所示。

                                                         \frac{\partial y}{\partial x}=exp(x)=y                                                      (7)

    可知,“exp”节点的反向传播将上游的值乘以正向传播时的输出y(这个例子的输出是exp\left ( -x \right ))后,再传给下游。计算图如图13所示。

    图13 指数运算节点的反向传播的计算图

    第四步:

    “x”节点的反向传播将正向传播时的值翻转后做乘法运算,因此计算图如图14所示。

    图14  sigmoid函数的反向传播的计算图

    综上,sigmoid函数的反向传播的输出为\frac{\partial L}{\partial y}y^{2}exp(-x),这个值会传递给下游的节点。我们发现,\frac{\partial L}{\partial y}y^{2}exp(-x)该值可只根据正向传播时的输入x和输出y就可以计算出来。所以,sigmoid函数的反向传播可以简化为如图15所示的计算图。

    图15  sigmoid函数的反向传播的计算图(简洁版)

    简洁后的反向传播可以忽视中间计算过程,因此大幅度提高了计算效率。其实,我们可以对\frac{\partial L}{\partial y}y^{2}exp(-x)作进一步的处理,如式(8)所示。

    因此,sigmoid函数的反向传播只需根据正向传播的输出就能计算出来,这里我们选择图16所示的计算图作为sigmoid函数的反向传播的最终计算图。

    图 16 sigmoid函数的计算图:只需正向传播的输出y计算反向传播

    Affine/softmax激活函数的反向传播

    Affine层

    在前面的专题讲解中,我们介绍了计算加权信号的总和,即输入信号x与权重w的乘积之和,再加上偏置b。在实现过程中,我们利用了矩阵的乘积运算(Numpy库中的np.dot())来计算了神经元(节点)加权和,即Y=np.dot(X,W)+B,然后将Y经激活函数转换后,传递给下一层。这就是神经网络的正向传播的流程。一般地,神经网络的正向传播涉及矩阵的乘积运算(信号的加权和计算)的过程(变换),我们称为Affine层

    Affine层:

    神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换”,它包括一次线性变换和一次平移,分别对应神经网络的加权和运算\sum xw与加偏置预算\left ( \sum xw\right )+b。在这里,我们将进行仿射变换的处理实现为“Affine”层。

    图17为神经网络正向传播的Affine层的计算图,我们需要注意的是,图中的变量均为矩阵形式,所以在进行矩阵运算时,要注意矩阵的形状是否正确。这里我们假设了各变量矩阵的形状,注意这里的计算图中各节点间传递的是矩阵,不是标量。

    图17 Affine层的计算图

    通过Affine层的正向传播,我们如何求它的反向传播呢?在这里我们需要记住两点:第一点是x、w、bwb均为变量,不是常量;第二点是节点中的运算步骤和以标量为对象的计算图相同。因此,我们很容易得到如图18所示的反向传播的计算图。

    图18  Affine层的反向传播

    图18中的反向传播的加法节点将上游传递来的值原封不动地传递给下游。"dot"节点可以看做乘法节点,但又有区别,即它是矩阵乘法,所以在考虑将上游传递来的值乘以正向传播的翻转值的同时,还要注意矩阵的形状。这里我们可以肯定的是:\frac{\partial L}{\partial x}\frac{\partial L}{\partial Y}W的某种乘积关系,而\frac{\partial L}{\partial W}\frac{\partial L}{\partial Y}X的某种乘积关系。因此,仔细分析可知:

    W^{T}W的转置,比如W的形状为(2,3),则W^{T}的形状就是(3,2)。所以图18中Affine层的反向传播的完整的计算图如图19所示:

    图19 Affine层的反向传播的计算图

    当然,这里介绍的Affine层的输入X是以单个数据为对象的,如果我们将N个数据样本(假设数据的特征有2个,则X的形状为(N,2))一起进行正向传播,即批版本的Affine层。那么它的计算图如图20所示。

    图20 批版本的Affine层的计算图

    softmax-with-loss层的反向传播

    神经网络涉及输入信号与权重参数的乘积的加权和(即Affine层)、激活函数、输出层激活函数(softmax)和损失函数(主要使用交叉熵误差)。在这之前,我们已经介绍了Affine层和激活函数的反向传播,下面我们将softmax层和损失函数一起作为对象来分析它们的反向传播的计算图。在这之前,我们以手写数字识别为例,回顾神经网络的推理过程。示意图如图21所示。

    图21 手写数字识别信号传递过程

    图21中,softmax层将输入值正规化(输出值的和调整为1)之后再输出,此外,手写数字识别要进行10类分类,所以向softmax层的输入也有10个。输入图像为“0”,得分为10.1分,经softmax层转换为0.991。

    一般情况下,我们会把softmax层和损失函数一起考虑,由于softmax-with-loss层比较复杂,这里我们直接给出其正向和反向传播的简易计算图如图22所示。具体的分析过程后面我们会专门花一个专题来讲。

    图22 简易版的softmax-with-loss层的计算图

    这里我们重点关注反向传播的结果。softmax层的反向传播得到了(y_{1}-t_{1},y_{2}-t_{2},y_{3}-t_{3})这样漂亮的结果。由于(y_{1},y_{2},y_{3})是softmax层的输出,\left (t_{1},t_{2},t_{3} \right )是监督数据,所以(y_{1}-t_{1},y_{2}-t_{2},y_{3}-t_{3})是softmax层的输出和监督标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。

    神经网络的学习的目的就是通过调整权重参数,使神经网络的输出(softmax层的输出)接近监督标签。因此,必须将神经网络的输出与监督标签的误差高效地传递给前面的层。前面的(y_{1}-t_{1},y_{2}-t_{2},y_{3}-t_{3})直截了当地表示了当前神经网络的输出与监督标签的误差。比如监督标签(0,1,0),softmax层的输出是(0.3,0.2,0.5)。由于正确解标签处的概率是20%,这时候神经网络未能进行正确的识别。此时,softmax层的反向传播传递的是(0.3,-0.8,0.5)这样一个大的误差。这个大的误差会向前面的层传播,所以softmax层前面的层会从这个大的误差中学习到“大”的内容。

    使用交叉熵误差作为softmax函数的损失函数后,反向传播得到(y_{1}-t_{1},y_{2}-t_{2},y_{3}-t_{3})这样漂亮的结果。实际上,这样的结果并不是偶然的,而是为了得到这样的结果,特意设计了交叉熵误差函数。

    误差反向传播法的Python实现

    乘法层的Python实现

    这里我们把乘法节点的计算图用“乘法层”(MulLayer),在Python中用类表示,类中有两个方法(函数),正向传播forward(),和反向传播backward()。代码如下:

    # coding: utf-8
    
    
    class MulLayer:
        def __init__(self):
            self.x = None
            self.y = None
    
        def forward(self, x, y):
            self.x = x
            self.y = y                
            out = x * y
    
            return out
    
        def backward(self, dout):
            dx = dout * self.y     #翻转x和y
            dy = dout * self.x
    
            return dx, dy

    代码中,__init__()会初始化实例变量x和y,它们主要用来保存正向传播时的输入值。forward()接收x和y两个参数,将它们相乘后输出。backward()将从上游传来的导数dout乘以正向传播的翻转值,然后传给下游。

    加法层的Python实现

    # coding: utf-8
    
    
    
    class AddLayer:
        def __init__(self):
            pass
    
        def forward(self, x, y):
            out = x + y
    
            return out
    
        def backward(self, dout):
            dx = dout * 1
            dy = dout * 1
    
            return dx, dy
    

    由于加法节点的反向传播不需要输入值,所以__init()__中无特意执行语句。forward()接收x和y,将它们相加后输出。backword()将上游传来的导数dout原封不动地传递给下游。

    购买水果问题的Python实现

    # coding: utf-8
    
    apple = 100
    apple_num = 2
    orange = 150
    orange_num = 3
    tax = 1.1
    
    # layer
    mul_apple_layer = MulLayer()
    mul_orange_layer = MulLayer()
    add_apple_orange_layer = AddLayer()
    mul_tax_layer = MulLayer()
    
    # forward
    apple_price = mul_apple_layer.forward(apple, apple_num)  # (1)
    orange_price = mul_orange_layer.forward(orange, orange_num)  # (2)
    all_price = add_apple_orange_layer.forward(apple_price, orange_price)  # (3)
    price = mul_tax_layer.forward(all_price, tax)  # (4)
    
    # backward
    dprice = 1
    dall_price, dtax = mul_tax_layer.backward(dprice)  # (4)
    dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)  # (3)
    dorange, dorange_num = mul_orange_layer.backward(dorange_price)  # (2)
    dapple, dapple_num = mul_apple_layer.backward(dapple_price)  # (1)
    
    print("price:", int(price))  #715
    print("dApple:", dapple)     #2.2
    print("dApple_num:", int(dapple_num))  #110
    print("dOrange:", dorange)             #3.3
    print("dOrange_num:", int(dorange_num)) #165
    print("dTax:", dtax)                    #650
    

    激活函数层的Python实现

    ReLU层的Python实现

    # coding: utf-8
    
    
    class Relu:
        def __init__(self):
            self.mask = None
    
        def forward(self, x):
            self.mask = (x <= 0)
            out = x.copy()
            out[self.mask] = 0
    
            return out
    
        def backward(self, dout):
            dout[self.mask] = 0
            dx = dout
    
            return dx

    需要提醒大家的是,神经网络的层的实现中,一般假定forward()和backward()的参数是numPy数组。代码中变量mask是由true/false构成的NumPy数组,它会正向传播时的输入x的元素中小于等于0的地方保存为true,大于0的地方保存为false。

    sigmoid层的Python实现

    class Sigmoid:
        def __init__(self):
            self.out = None
    
        def forward(self, x):
            out = sigmoid(x)
            self.out = out
            return out
    
        def backward(self, dout):
            dx = dout * (1.0 - self.out) * self.out
    
            return dx

    正向传播时将输出保存到了变量out中,反向传播时,使用该变量out进行计算。

    Affine层的Python实现

    class Affine:
        def __init__(self, W, b):
            self.W =W
            self.b = b
            
            self.x = None
            self.original_x_shape = None
            # 权重和偏置参数的导数
            self.dW = None
            self.db = None
    
        def forward(self, x):
            # 对应张量
            self.original_x_shape = x.shape
            x = x.reshape(x.shape[0], -1)
            self.x = x
    
            out = np.dot(self.x, self.W) + self.b
    
            return out
    
        def backward(self, dout):
            dx = np.dot(dout, self.W.T)
            self.dW = np.dot(self.x.T, dout)
            self.db = np.sum(dout, axis=0)
            
            dx = dx.reshape(*self.original_x_shape)  # 还原输入数据的形状(对应张量)
            return dx

    需要注意的是,Affine的实现考虑了输入数据为张量(四维数据)的情况。

    softmax-with-loss层的Python实现

    # coding: utf-8
    import numpy as np 
    
    def softmax(x):
        if x.ndim == 2:
            x = x.T
            x = x - np.max(x, axis=0)
            y = np.exp(x) / np.sum(np.exp(x), axis=0)
            return y.T 
    
        x = x - np.max(x) # 溢出对策
        return np.exp(x) / np.sum(np.exp(x))
    
    def cross_entropy_error(y, t):
        if y.ndim == 1:
            t = t.reshape(1, t.size)
            y = y.reshape(1, y.size)
            
        # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
        if t.size == y.size:
            t = t.argmax(axis=1)
                 
        batch_size = y.shape[0]
        return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
    
    
    class SoftmaxWithLoss:
        def __init__(self):
            self.loss = None
            self.y = None # softmax的输出
            self.t = None # 监督数据
    
        def forward(self, x, t):
            self.t = t
            self.y = softmax(x)
            self.loss = cross_entropy_error(self.y, self.t)
            
            return self.loss
    
        def backward(self, dout=1):
            batch_size = self.t.shape[0]
            if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况
                dx = (self.y - self.t) / batch_size
            else:
                dx = self.y.copy()
                dx[np.arange(batch_size), self.t] -= 1
                dx = dx / batch_size
            
            return dx
    

    误差反向传播法的Python总体实现

    神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。神经网络的学习一般分为以下四个步骤:

    (1)从训练数据中随机选择一部分数据

    (2)计算损失函数关于各个权重参数的梯度(采用误差反向传播法)

    (3)将权重参数沿梯度方向进行微小的更新

    (4) 重复步骤1至步骤3

    下面的代码完成了2层神经网络的实现 

    # coding: utf-8
    import numpy as np
    from collections import OrderedDict
    # coding: utf-8
    import numpy as np
    
    #这里被调用的部分函数可在之前的专题中查找
    
    
    
    
    def numerical_gradient(f, x):
        h = 1e-4 # 0.0001
        grad = np.zeros_like(x)
        
        it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
        while not it.finished:
            idx = it.multi_index
            tmp_val = x[idx]
            x[idx] = float(tmp_val) + h
            fxh1 = f(x) # f(x+h)
            
            x[idx] = tmp_val - h 
            fxh2 = f(x) # f(x-h)
            grad[idx] = (fxh1 - fxh2) / (2*h)
            
            x[idx] = tmp_val # 还原值
            it.iternext()   
            
        return grad
    
    
    class TwoLayerNet:
    
        def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
            # 初始化权重
            self.params = {}
            self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
            self.params['b1'] = np.zeros(hidden_size)
            self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) 
            self.params['b2'] = np.zeros(output_size)
    
            # 生成层
            self.layers = OrderedDict()
            self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
            self.layers['Relu1'] = Relu()
            self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
    
            self.lastLayer = SoftmaxWithLoss()
            
        def predict(self, x):
            for layer in self.layers.values():
                x = layer.forward(x)
            
            return x
            
        # x:输入数据, t:监督数据
        def loss(self, x, t):
            y = self.predict(x)
            return self.lastLayer.forward(y, t)
        
        def accuracy(self, x, t):
            y = self.predict(x)
            y = np.argmax(y, axis=1)
            if t.ndim != 1 : t = np.argmax(t, axis=1)
            
            accuracy = np.sum(y == t) / float(x.shape[0])
            return accuracy
            
        # x:输入数据, t:监督数据
        def numerical_gradient(self, x, t):
            loss_W = lambda W: self.loss(x, t)
            
            grads = {}
            grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
            grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
            grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
            grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
            
            return grads
            
        def gradient(self, x, t):
            # forward
            self.loss(x, t)
    
            # backward
            dout = 1
            dout = self.lastLayer.backward(dout)
            
            layers = list(self.layers.values())
            layers.reverse()
            for layer in layers:
                dout = layer.backward(dout)
    
            # 设定
            grads = {}
            grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
            grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
    
            return grads
    

    代码中使用了OrderDict()函数,它是有序字典,即它可以记住向字典里添加元素的顺序。因此,神经网络的正向传播只需按照添加元素的顺序调用各层的forward()方法就可以完成处理,而反向传播只需要按照相反的顺序调用各层即可。

    我们构造了神经网络之后,就可以进行学习了,在前面的专题我们讲过神经网络的学习,其中介绍了用数值微分的方法求梯度,而这里我们则采用误差反向传播法求梯度。除此之外,程序几乎一样。神经网络的学习的Python实现如下:

    # 读入数据
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
    
    network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
    
    iters_num = 10000
    train_size = x_train.shape[0]
    batch_size = 100
    learning_rate = 0.1
    
    train_loss_list = []
    train_acc_list = []
    test_acc_list = []
    
    iter_per_epoch = max(train_size / batch_size, 1)
    
    for i in range(iters_num):
        batch_mask = np.random.choice(train_size, batch_size)
        x_batch = x_train[batch_mask]
        t_batch = t_train[batch_mask]
        
        # 梯度
        #grad = network.numerical_gradient(x_batch, t_batch)  #之前讲过的数值微分求梯度函数
        grad = network.gradient(x_batch, t_batch)            #误差反向传播法求梯度
        
        # 更新
        for key in ('W1', 'b1', 'W2', 'b2'):
            network.params[key] -= learning_rate * grad[key]
        
        loss = network.loss(x_batch, t_batch)
        train_loss_list.append(loss)
        
        if i % iter_per_epoch == 0:
            train_acc = network.accuracy(x_train, t_train)
            test_acc = network.accuracy(x_test, t_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)
            print(train_acc, test_acc)
    

    小结

     本章我们介绍了计算图,并使用计算图介绍了神经网络的误差反向传播法,并以层为单位实现了神经网络中的处理。通过将数据正向和反向地传播,可以高效地计算权重参数的梯度。

    欢迎关注微信公众号“Python生态智联”,学知识,享生活!

    展开全文
  • 详解反向传播算法--转自知乎

    万次阅读 多人点赞 2017-03-22 16:18:24
    本文转自知乎,作者晓雷。...作为数学系妹子,一直在试图弄懂反向传播算法,先后看了数篇论文、博客,以及著名的“西瓜书”,感觉还是不甚清晰。下面的文章讲解得很详细,图把反向传播过程清晰地展现了出来。

    本文转自知乎,作者晓雷。原文链接:

    https://zhuanlan.zhihu.com/p/25081671 (上篇)

    https://zhuanlan.zhihu.com/p/25416673 (下篇)

    作为数学系妹子,一直在试图弄懂反向传播算法,先后看了数篇论文、博客,以及著名的“西瓜书”,感觉还是不甚清晰。下面的文章讲解得很详细,图把反向传播过程清晰地展现了出来。另外,如果想进一步详细了解反向传播算法,建议斯坦福大学的这篇文章:http://deeplearning.stanford.edu/wiki/index.php/%E5%8F%8D%E5%90%91%E4%BC%A0%E5%AF%BC%E7%AE%97%E6%B3%95

    上篇:

    目录:

    1 用计算图来解释几种求导方法:

    1.1 计算图

    1.2 两种求导模式:前向模式求导( forward-mode differentiation) 反向模式求导(reverse-mode differentiation)

    1.3 反向求导模式(反向传播算法)的重要性

    声明:本文内容来自 Calculus on Computational Graphs: Backpropagation 。算是翻译加上自己理解。水平有限,理解错误欢迎指正。

    反向传播算法(Backpropagation)已经是神经网络模型进行学习的标配。但是有很多问题值得思考一下:

    反向传播算法的作用是什么? 神经网络模型的学习算法一般是SGD。SGD需要用到损失函数C关于各个权重参数w_{jk},b_j的偏导数\frac{ \partial C }{ \partial w_{jk} } , \frac{ \partial C }{ \partial b_j} 。一个模型的参数w,b是非常多的,故而需要反向传播算法快速计算\frac{ \partial C }{ \partial w_{jk} } , \frac{ \partial C }{ \partial b_j} 。也就是说反向传播算法是一种计算偏导数的方法。

    为什么要提出反向传播算法? 在反向传播算法提出之前人们应该想到了使用SGD学习模型,也想到了一些办法求解网络模型的偏导数,但这些算法求解效率比较低,所以提出反向传播算法来更高效的计算偏导数。(那时的网络模型还比较浅只有2-3层,参数少。估计即便不适用反向传播这种高效的算法也能很好的学习。一旦有人想使用更深的网络自然会遇到这个偏导数无法高效计算的问题,提出反向传播也就势在必行了)

    反向传播怎么样实现高效计算偏导数的? 请先回顾一下当初我们学习微积分时是如何计算偏导数的? (链式法则,具体看下面)

    1 用计算图来解释几种求导方法:

    1.1 计算图

    式子 e=(a+b)*(b+1) 可以用如下计算图表达:

    令a=2,b=1则有:

    如何在计算图上表达“求导”呢? 导数的含义是 因变量随自变量的变化率,例如 \frac{\partial y }{\partial x} = 3  表示当x变化1个单位,y会变化3个单位。 微积分中已经学过:加法求导法则是 \frac{\partial}{\partial a}(a+b) = \frac{\partial a}{\partial a} + \frac{\partial b}{\partial a} = 1 乘法求导法则是 \frac{\partial}{\partial u}uv = u\frac{\partial v}{\partial u} + v\frac{\partial u}{\partial u} = v 。 我们在计算图的边上表示导数或偏导数:\frac{ \partial e }{ \partial c } , \frac{ \partial e }{ \partial d }, \frac{ \partial c }{ \partial a }, \frac{ \partial c }{ \partial b }, \frac{ \partial d }{ \partial b } 如下图

    那么 \frac{ \partial e  }{ \partial b } 如何求呢? \frac{\partial c }{ \partial b} = 1 告诉我们1个单位的b变化会引起1个单位的c变换,\frac{\partial e }{ \partial c} = 2告诉我们 1 个单位的c变化会引起2个单位的e变化。所以 \frac{ \partial e  }{ \partial b } =   \frac{ \partial c }{ \partial b } * \frac{ \partial e  }{ \partial c }   = 1*2 =2 吗? 答案必然是错误。因为这样做只考虑到了下图橙色的路径,所有的路径都要考虑:\frac{ \partial e  }{ \partial b } =   \frac{ \partial c }{ \partial b } * \frac{ \partial e  }{ \partial c }  +  \frac{ \partial d  }{ \partial b }  *  \frac{ \partial e  }{ \partial d }  =1*2 + 1 * 3 = 5

    所以上面的求导方法总结为一句话就是: 路径上所有边相乘,所有路径相加。不过这里需要补充一条很有用的合并策略:

    例如:下面的计算图若要计算\frac{\partial Z}{\partial X}就会有9条路径:\frac{\partial Z}{\partial X} = \alpha\delta + \alpha\epsilon + \alpha\zeta + \beta\delta + \beta\epsilon + \beta\zeta + \gamma\delta + \gamma\epsilon + \gamma\zeta

    如果计算图再复杂一些,层数再多一些,路径数量就会呈指数爆炸性增长。但是如果采用合并策略:\frac{\partial Z}{\partial X} = (\alpha + \beta + \gamma)(\delta + \epsilon + \zeta) 就不会出现这种问题。这种策略不是 对每一条路径都求和,而是 “合并同类路径”,“分阶段求解”。先求X对Y的总影响 (\alpha + \beta + \gamma) 再求Y对Z的总影响 (\delta + \epsilon + \zeta) 最后综合在一起。


    1.2 两种求导模式:前向模式求导( forward-mode differentiation) 反向模式求导(reverse-mode differentiation)

    上面提到的求导方法都是前向模式求导( forward-mode differentiation) :从前向后。先求X对Y的总影响 (\alpha + \beta + \gamma) 再乘以Y对Z的总影响 (\delta + \epsilon + \zeta) 。


    另一种,反向模式求导(reverse-mode differentiation) 则是从后向前。先求Y对Z的影响再乘以X对Y的影响。

    前向求导模式追踪一个输入如何影响每一个节点(对每一个节点进行 \frac{\partial}{\partial X}操作)反向求导模式追踪每一个节点如何影响一个输出(对每一个节点进行 \frac{\partial Z}{\partial}操作)。

    1.3 反向求导模式(反向传播算法)的重要性

    让我们再次考虑前面的例子:

    如果用前向求导模式:关于b向前求导一次如果用反向求导模式:向后求导
    前向求导模式只得到了关于输入b的偏导 \frac{\partial e}{\partial b} ,还需要再次求解关于输入a的偏导\frac{\partial e}{\partial a} (运算2遍)。而反向求导一次运算就得到了e对两个输入a,b的偏导\frac{\partial e}{\partial a}, \frac{\partial e}{\partial b} (运算1遍)。上面的比较只看到了2倍的加速。但如果有1亿个输入1个输出,意味着前向求导需要操作1亿遍才得到所有关于输入的偏导,而反向求导则只需一次运算,1亿倍的加速。

    当我们训练神经网络时,把“损失“ 看作 ”权重参数“ 的函数,需要计算”损失“关于每一个”权重参数“的偏导数(然后用梯度下降法学习)。 神经网络的权重参数可以是百万甚至过亿级别。因此 反向求导模式(反向传播算法)可以极大的加速学习。

    参考:


    下篇:

    神经网络结构图:

    示例网络图

    其中C是损失函数,例如C可以取:

    梯度下降(SGD)进行学习时,核心问题是求解损失函数C关于所有网络参数w_{jk},b_j的偏导数\frac{\partial C}{\partial w_{jk}} ,\frac{\partial C}{\partial b_j}  。 根据详解反向传播算法(上) 我们已经知道用反向传播算法可以“一次反向计算”得到损失函数C关于网络中所有参数的偏导数。模仿详解反向传播算法(上) 的推理过程,我们首先画出上面网络图的详细计算图:再看看具体怎么样反向传播求偏导数。

    神经网络计算图

    对应计算图如下:(只展开了最后两层的计算图):

    绿色代表权重参数w_{jk},橙色代表基底参数b_j。可见虽然网络图上只是简单几条线,计算图还是蛮复杂的。

    现在我们在计算图箭头上标出对应的偏导数(只标出了一部分)。

    反向传播四公式


    上面计算图上每一个节点关于前一个节点的偏导数都可以求得,根据求导的链式法则,想要求损失函数C关于某一节点的偏导数,只需要“把该节点每条反向路径上的偏导数做乘积,再求和”即可。(w_{jk},b_j分别对应绿色和橙色的节点)

    现在我们已经可以在计算图上求得损失函数C关于模型参数的偏导数\frac{\partial C}{\partial w_{jk}} ,\frac{\partial C}{\partial b_j}  。但是还不够优雅,反向传播算法要优雅的很多,它通过定义一个损失(\delta_j^l),先逐层向后传播得到每一层节点的损失(\delta_j^l),再通过每一个节点的损失(\delta_j^l)来求解该节点的\frac{\partial C}{\partial w_{jk}} ,\frac{\partial C}{\partial b_j}

    首先记损失函数C关于l层的第j个元素的偏导为:\delta_j^l \equiv \frac{\partial C}{\partial z_j^l}

    最后一层

    对于最后一层(L层)的元素j会有:

    \delta_j^L = \frac{\partial C}{\partial z_j^L}=\frac{\partial C}{\partial a_j^L} \cdot \frac{\partial a_j^L}{\partial z_j^L} = \frac{\partial C}{\partial a_j^L} \cdot \sigma^{'}(z_j^L)

    向量化为:

    \bm \delta^L = \begin{pmatrix} \delta_1^L \\\vdots \\ \delta_j^L \\   \vdots\\  \delta_n^L \end{pmatrix}= \begin{pmatrix} \frac{\partial C}{\partial a_1^L} \cdot \sigma^{'}(z_1^L) \\\vdots \\ \frac{\partial C}{\partial a_j^L} \cdot \sigma^{'}(z_j^L) \\   \vdots\\  \frac{\partial C}{\partial a_n^L} \cdot \sigma^{'}(z_n^L) \end{pmatrix}= \begin{pmatrix} \frac{\partial C}{\partial a_1^L} \\\vdots \\ \frac{\partial C}{\partial a_j^L} \\   \vdots\\  \frac{\partial C}{\partial a_n^L} \end{pmatrix}\odot  \begin{pmatrix} \sigma^{'}(z_1^L) \\\vdots \\ \sigma^{'}(z_j^L) \\   \vdots\\  \sigma^{'}(z_n^L) \end{pmatrix} =  \bm \nabla_aC \odot  \sigma^{'}(\bm z^L) (BP1)

    其中\odot的操作是把两个向量对应元素相乘组成新的元素。

    后一层传播到前一层

    由前面计算图中L和L-1层所标注的偏导数,可得到倒数第一层(L-1)元素j的损失为:(请仔细对照前面的计算图)\delta_j^{L-1} = (\sum_{j=1}^n{\frac{\partial z_j^L}{\partial a_{k}^{L-1}}  \delta_j^L }) \cdot \sigma_{'}(z_j^{L-1}) = (\sum_{j=1}^n{w_{jk}^L \delta_j^L } ) \cdot  \sigma_{'}(z_j^{L-1})  =\begin{pmatrix} w_{1k}^L \cdots w_{jk}^L \cdots  w_{nk}^L\\  \end{pmatrix} \begin{pmatrix} \delta_1^L \\  \vdots \\  \delta_j^L\\\vdots\\\delta_n^L \end{pmatrix}\cdot \sigma^{'}(z_j^{L-1})

    向量化:\delta^{L-1} = ((w^{L})^T\delta^{L} \odot \sigma^{'}(z^{L-1}) )

    这启发我们后一层(l+1层)的损失\delta^{l+1} 如何传播到前一层(l层)得到\delta^l。(只需要把L用l+1替换,L-1l替换)就得到了逐层传播损失的公式:

    \bm \delta^{l} = ((\bm w^{l+1})^T \bm \delta^{l+1} \odot \sigma^{'}(\bm z^{l}) )(BP2)

    关于b_j^l的偏导数

    \frac{\partial C}{\partial b_j^l} =\frac{ \partial C}{ \partial z_j^l} \frac{\partial z_j^l}{\partial b_j^l} = \delta_j^l \cdot 1 = \delta_j^l(BP3)

    向量化:\frac{\partial C}{\partial b^l} =\bm \delta^l


    关于w_{jk}^l的偏导数

    \frac{\partial C}{\partial w_{jk}^l} =\frac{ \partial C}{ \partial z_j^l} \frac{\partial z_j^l}{\partial w_{jk}^l} = \delta_j^l \cdot a_k^{l-1} (BP4)

    向量化:\frac{\partial C}{\partial w_{j\cdot }^l} =\begin{pmatrix}\delta_j^l  a_1^{l-1}  \\  \vdots \\  \delta_j^l  a_k^{l-1} \\\vdots\\\delta_j^l  a_n^{l-1}  \end{pmatrix}=\delta_j^l \cdot\begin{pmatrix}  a_1^{l-1}  \\  \vdots \\    a_k^{l-1} \\\vdots\\  a_n^{l-1}  \end{pmatrix}= \delta_j^l \cdot \bm a^{l-1} \Rightarrow

    \frac{\partial C}{\partial w^l} = \begin{pmatrix}\delta^l_1 \cdot \bm a^{l-1} \\\vdots \\\delta^l_j \cdot \bm a^{l-1}\\\vdots\\\delta^l_n \cdot \bm a^{l-1}\\\end{pmatrix}= \begin{pmatrix}\delta^l_1   \\\vdots \\\delta^l_j \\\vdots\\\delta^l_n \\\end{pmatrix}\cdot \begin{pmatrix} a^{l-1}_1  \cdots a^{l-1}_k \cdots a^{l-1}_n  \end{pmatrix}= \bm \delta^l \cdot (\bm a^{l-1})^T

    至此就得到了反向传播的4个公式:

    图片来自:Neural networks and deep learning
    反向传播算法流程:

    流程图来自:Neural networks and deep learning



    本文主要参考 Neural networks and deep learning,原作者写的也不错,不过个人觉得如果按照计算图会更加直观,基本不需要数学推导过程,用肉眼看图就可以理解反向传播的四个公式。当然前提是计算图要画的清晰明白。花了半天时间来写这篇文章,其中画图花费了80%的时间,尤其是计算图改了N次,仍然可能存在错误,欢迎指正~

    点赞(分享)就是对文章作者的最大鼓励~

    ------下面只是备份下用过的公式,以备后面修改使用 ------------

    a_j^L=\sigma(z_j^L)  a_2^L=\sigma(z_2^L)  z_j^L=\sum_{k=1}^{K}{(w_{jk}^L \cdot a_{k}^{L-1}) } +b_{j}^L z_1^L=\sum_{k=1}^{4}{(w_{1k} \cdot a_k^{L-1}) } +b_{1} z_2^L=\sum_{k=1}^{4}{(w_{2k} \cdot a_k^{L-1}) } +b_{2} \frac{\partial a_j^L}{\partial z_j^L}  = \sigma^{'}(z_j^L) \frac{\partial z_j^L}{\partial b_j^L} = 1 \frac{\partial z_j^L}{\partial w_{jk}^L}  =a_k^{L-1} \frac{\partial z_2^L}{\partial a_{k}^{L-1}}  =w_{2k}^L
    \frac{\partial a_k^{L-1}}{\partial z_k^{L-1}}  = \sigma^{'}(z_k^{L-1}) \frac{\partial z_k^{L-1}}{\partial b_k^{L-1}} = 1 \frac{\partial z_k^{L-1}}{\partial w_{km}^{L-1}}  =a_m^{L-2}

    ------------------------ 备份 end ----------------------------------------------

    展开全文
  • 深度学习---反向传播算法

    千次阅读 2018-12-11 16:30:15
    在介绍反向传播之前,先介绍怎么利用矩阵运算快速的计算神经网络输出。其实在上一章对这一块也提到过,不过不够详细。这里再介绍一下,帮助大家逐渐适应基于矩阵运算的表示方式。 我们先引入一个能够明确表示连接...
  • 由于我把简历放在了GithubPage上了,但是本人又不甘心只能墙外人看到,因此就通过墙内腾讯云服务器,配置了nignx做了反向代理到了我的简历网站。 不过由于墙内的原因,有的时候并不能很好的ping通,甚至出现DNS无法...
  • 前向传播和反向传播 从一个简单的数学表达式开始 1+2=31+2=31+2=3 把常量换成变量,如下 x+y=zx+y=zx+y=z 这是数学表达式的方式 我们再换种计算图的方式,如下 前向传播 反向传播 这是加法,左边是前向传播,右边是...
  • 理解深度学习中的反向传播概述介绍图示计算过程图示求导两种模式计算结果重要性 概述 反向传播主要广泛应用于深度学习中的优化器,是优化器的主要工作原理 介绍 反向传播是使训练深度模型在计算上易于处理的关键算法...
  • 套用古龙武侠小说套路来说,...常用的代理技术分为正向代理、反向代理和透明代理。本文就是针对这三种代理来讲解一些基本原理和具体的适用范围,便于大家更深入理解代理服务技术。 一、正向代理(Forward Proxy) &nbs...
  • 正向代理和反向代理总结

    千次阅读 多人点赞 2019-02-14 16:41:13
     最近工作中用到反向代理,发现网络代理的玩法还真不少,网络背后有很多需要去学习。而在此之前仅仅使用了过代理软件,曾经为了访问google,使用了代理软件,需要在浏览器中配置代理的地址。我只知道有代理这个概念...
  • 误差反向传播法 上一章中,我们介绍了神经网络的学习,并通过数值微分计算了神经网络的权重参数的梯度(严格来说,是损失函数关于权重参数的梯度)。数值微分虽然简单,也容易实现,但缺点是计算上比较费时间。本章...
  • 反向传播算法是神经网络中的重要算法,通过它能够快速计算梯度,进而通过梯度下降实现权重和偏置参数的更新 反向传播算法最初是在20世纪70年代被引入的,但直到1986年大卫·鲁梅尔哈特、杰弗里·辛顿和罗纳德·...
  • 反向传播算法如何工作的?

    千次阅读 2020-05-16 12:16:42
    在本章,我们会解释计算这些梯度的快速算法,也就是反向传播。 反向传播算法最初在 1970 年代被发现,但是这个算法的重要性直到David Rumelhart、Geoffrey Hinton和Ronald Williams的1986年的论文中才被真正认可。...
  • 反向传播算法推导-全连接神经网络

    千次阅读 2018-07-07 10:33:51
    反向传播算法是人工神经网络训练时采用的一种通用方法,在现代深度学习中得到了大规模的应用。全连接神经网络(多层感知器模型,MLP),卷积神经网络(CNN),循环神经网络(RNN)中都有它的实现版本。算法从多元...
  • 今天给大家分享一下,修真院官网JAVA任务三,深度思考中的知识点——nginx服务器有什么作用?什么叫反向代理?为什么要使用反向代理? 视频链接: https://v.qq.com/x/page/y0502c4k5zu.html   undefined_腾讯...
  • 我相信你能看懂这个公式,举个例子,就是第二层的第一个神经元的激活值(值在0-1之间),是由第一层所有神经元的激活值乘上对应的权重矩阵(即每个激活值的重要程度)求和,然后加上第二层第一个神经元的偏置,最后...
  • 前言 低频下,按电池理解二极管即可,但是当高频信号加在二极管两端时,就要考虑二极管的动态特性了。... 我们来看看高频二极管1N4148的反向恢复时间: 4ns 思考 二极管的反向恢复时间跟结电容大小有关系么?
  • 一、预热篇 ... 要理解的主要点:路径上所有边相乘,所有路径...但是有很多问题值得思考一下: 反向传播算法的作用是什么? 神经网络模型的学习算法一般是SGD。SGD需要用到损失函数C关于各个权重参数的偏导数。一个...
  •  反向代理也是一种可以帮助实现网站静态化的重要技术,今天我就来讲讲反向代理这个主题。那么首先我们要了解下什么是反向代理。和反向代理相对应的是正向代理,正向代理也就是我们常说的代理服务,正向代理是非常...
  • 答案是不需要的,自己思考就会明白。   那么反向代理的优点就体现出来了,我不需要配置,而且我不仅只能在一家调用请求,我可以向多个服务端去发出请求。而且反向代理还可以向多台后端服务器进行负载平衡。 ...
  • 最近在看NG的视频的时候,学习反向传播算法时遇到了不小的困难。一是因为NG并没有把太多的精力放在讲解反向传播原理上,可能NG自己也觉得这部分如果要细讲,真的会耗费不少时间。二是NG在开课初就很照顾数学基础差的...
  • 以下根据实际例子使用pytorch编写如何求梯度的简单程序示例,假设输入、输出、损失函数、权重、偏置项如下图所示 以下使用pytorch编写python进行梯度的求解: import torch # 引入torch模块 from torch . ...
  • 文章目录主要代码前向传播反向传播参数更新 用过tensorflow等的都知道,神经网络不同的 Layers 像拼图一样几乎可以随心所欲地拼接。每一个层相当于一个对象,可以实现前向传播、反向传播、参数更新,只需要按顺序...
  • 反向传播算法(UFLDL版)

    2018-01-10 21:02:19
    2.2.1 针对单个样例的反向传播算法 在深入到具体的推导之前,先说一下大体的思路,先说目标吧: 我们是利用整个神经网络的“残差”来计算偏导的,或者说代价函数对参数(权重或者偏置)的偏导可以由当前参数所处...
  • 2.反向传播Backpropagation 2.1反向传播的意义? 2.2反向传播示例 2.2.1误差公式解析 2.2.2误差公式推导 2.3反向传播算法 2.3.1偏导公式 2.3.2算法公式 2.3.3进一步理解 3.梯度检验 4.随机初始化 5.总结...
  • 反向传播是使训练深度模型在计算上易于处理的关键算法。对于现代神经网络,相对于天真的实现,它可以使梯度下降训练的速度提高一千万倍。这是一个模型需要一周的培训和20万年的差异。 除了在深度学习中的应用之外,...
  • 4、反向传播算法 4.1计算图 反向传播算法又称误差反向传播、BP算法( Backpropagation,缩写为BP)。 BP算法的学习过程由正向传播过程和反向传播过程组成。 在正向传播过程中,输入信息通过输入层经隐含层,逐层处理并...
  • 深度学习100问之深入理解Back Propagation(反向传播)

    万次阅读 多人点赞 2019-04-03 10:22:37
    这几天正在看反向传播的原理,最近也经常看到关于反向传播理解的文章,在深度学习的理论中反向传播也是极其重要的,所以就抽出一段时间认真地研究了一下反向传播的原理,以下为参考网上的几篇文章总结得出的。...
  • 思考一下,如果没有SOP ,当请求到跨域的网站时,那个网站就能获取到源网站的一些cookie 信息了,要知道 cookie 里有时候存储的都是用户信息,比如sessionId , 若对方网站是一个恶意网站,很轻松地就能获取到你的...
  • 反向传播算法推导-卷积神经网络

    千次阅读 2018-08-08 11:57:32
    在SIGAI之前的公众号文章“反向传播算法推导-全连接神经网络”中,我们推导了全连接神经网络的反向传播算法。其核心是定义误差项,以及确定误差项的递推公式,再根据误差项得到对权重矩阵、偏置向量的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 16,927
精华内容 6,770
关键字:

反向思考的例子