• 22. 深度学习 - 自动求导


    在这里插入图片描述

    Hi,你好。我是茶桁。

    咱们接着上节课内容继续讲,我们上节课已经了解了拓朴排序的原理,并且简单的模拟实现了。我们这节课就来开始将其中的内容变成具体的计算过程。

    linear, sigmoidloss这三个函数的值具体该如何计算呢?

    我们现在似乎大脑已经有了一个起比较模糊的印象,可以通过它的输入来计算它的点。

    让我们先把最初的父类Node改造一下:

    
    class Node():
        def __init__(self, inputs=[], name=None):
            ...
            self.value = None
        
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后再复制出一个,和Placeholder一样,我们需要继承Node,并且改写这个方法自己独有的内容:

    class Linear(Node):
        def __init__(self, x, k, b, name=None):
            Node.__init__(self, inputs=[x, k, b], name=name)
    
        def forward(self):
            x, k, b = self.inputs[0], self.inputs[1], self.inputs[2]
            self.value = k.value * x.value + b.value
            print('我是{}, 我没有人类爸爸,需要自己计算结果{}'.format(self.name, self.value))
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们新定义的这个类叫Linear, 它会接收x, k, b。它继承了Node。这个里面的forward该如何计算呢? 我们需要每一个节点都需要一个值,一个变量,因为我们初始化的时候接收的x,k,b都赋值到了inputs里,这里我们将其取出来就行了,然后就是线性方程的公式k*x+b,赋值到它自己的value上。

    然后接着呢,就轮到Sigmoid了,一样的,我们定义一个子类来继承Node:

    class Sigmoid(Node):
        def __init__(self, x, name=None):
            Node.__init__(self, inputs=[x], name=name)
            self.x = self.inputs[0]
    
        def _sigmoid(self, x):
            return 1/(1+np.exp(-x))
    
        def forward(self):
            self.value = self._sigmoid(self.x.value)
            print('我是{}, 我自己计算了结果{}'.format(self.name, self.value))
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Sigmoid函数只接收一个参数,就是x,其公式为1/(1+e^{-x}),我们在这里定义一个新的方法来计算,然后在forward里把传入的x取出来,再将其送到这个方法里进行计算,最后将结果返回给它自己的value。

    那下面自然是Loss函数了,方式也是一模一样:

    class Loss(Node):
        def __init__(self, y, yhat, name=None):
            Node.__init__(self, inputs = [y, yhat], name=name)
            self.y = self.inputs[0]
            self.yhat = self.inputs[1]
    
        def forward(self):
            y_v = np.array(self.y.value)
            yhat_v = np.array(self.y_hat.value)
            self.value = np.mean((y.value - yhat.value) ** 2)
            print('我是{}, 我自己计算了结果{}'.format(self.name, self.value))
    
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    那我们这里定义成Loss其实并不确切,因为我们虽然喊它是损失函数,但是其实损失函数的种类也非常多。而这里,我们用的MSE。所以我们应该定义为MSE,不过为了避免歧义,这里还是沿用Loss好了。

    定义完类之后,我们参数调用的类名也就需要改一下了:

    ...
    node_linear = Linear(x=node_x, k=node_k, b=node_b, name='linear')
    node_sigmoid = Sigmoid(x=node_linear, name='sigmoid')
    node_loss = Loss(y=node_y, yhat=node_sigmoid, name='loss')
    
    • 1
    • 2
    • 3
    • 4

    好,这个时候我们基本完成了,计算之前让我们先看一下sorted_node:

    sorted_node
    
    ---
    [Placeholder: y,
     Placeholder: k,
     Placeholder: x,
     Placeholder: b,
     Linear: Linear,
     Sigmoid: Sigmoid,
     MSE: Loss]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    没有问题,我们现在可以模拟神经网络的计算过程了:

    for node in sorted_nodes:
        node.forward()
    
    ---
    我是x, 我已经被人类爸爸赋值为3
    我是b, 我已经被人类爸爸赋值为0.3737660632429008
    我是k, 我已经被人类爸爸赋值为0.35915077292816744
    我是y, 我已经被人类爸爸赋值为0.6087876106387002
    我是Linear, 我没有人类爸爸,需要自己计算结果1.4512183820274032
    我是Sigmoid, 我没有人类爸爸,需要自己计算结果0.8101858733432837
    我是Loss, 我没有人类爸爸,需要自己计算结果0.04056126022042443
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    咱们这个整个过程就像是数学老师推公式一样,因为这个比较复杂。你不了解这个过程就求解不出来。

    这就是为什么我一直坚持要手写代码的原因。c+v大法确实好,但是肯定是学的不够深刻。表面的东西懂了,但是更具体的为什么不清楚。

    我们可以看到,我们现在已经将Linear、Sigmoid和Loss都将值计算出来了。那我们现在已经实现了从x到loss的前向传播

    现在我们有了loss,那就又要回到我们之前机器学习要做的事情了,就是将损失函数loss的值降低。

    之前咱们讲过,要将loss的值减小,那我们就需要求它的偏导,我们前面课程的求导公式这个时候就需要拿过来了。

    然后我们需要做的事情并不是完成求导就好了,而是要实现「链式求导」。

    那从Loss开始反向传播的时候该做些什么?先让我们把“口号”喊出来:

    class Node:
        def __init__(...):
            ...
        ...
        def backward(self):
            for n in self.inputs:
                print('获取∂{} / ∂{}'.format(self.name, n.name))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样修改一下Node, 然后在其中假如一个反向传播的方法,将口号喊出来。

    然后我们来看一下口号喊的如何,用[::-1]来实现反向获取:

    for node in sorted_nodes[::-1]:
        node.backward()
    
    ---
    获取∂Loss / ∂y
    获取∂Loss / ∂Sigmoid
    获取∂Sigmoid / ∂Linear
    获取∂Linear / ∂x
    获取∂Linear / ∂k
    获取∂Linear / ∂b
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这样看着似乎不是太直观,我们再将node的名称加上去来看就明白很多:

    for node in sorted_nodes[::-1]:
        print(node.name)
        node.backward()
    ---
    Loss
    获取∂Loss / ∂y
    获取∂Loss / ∂Sigmoid
    Sigmoid
    获取∂Sigmoid / ∂Linear
    Linear
    获取∂Linear / ∂x
    获取∂Linear / ∂k
    获取∂Linear / ∂b
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    最后的k, y, x, b我就用…代替了,主要是函数。

    那我们就清楚的看到,Loss获取了两个偏导,然后传到了Sigmoid, Sigmoid获取到一个,再传到Linear,获取了三个。那现在其实我们只要把这些值能乘起来就可以了。我们要计算步骤都有了,只需要把它乘起来就行了。

    我们先是需要一个变量,用于存储Loss对某个值的偏导

    class Node:
        def __init__(...):
            ...
            self.gradients = dict()
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后我们倒着来看, 先来看Loss:

    class Loss(Node):
        ...
        def backward(self):
            self.gradients[self.inputs[0]] = '∂{}/∂{}'.format(self.name, self.inputs[0].name)
            self.gradients[self.inputs[1]] = '∂{}/∂{}'.format(self.name, self.inputs[1].name)
            print('[0]: {}'.format(self.gradients[self.inputs[0]]))
            print('[1]: {}'.format(self.gradients[self.inputs[1]]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    眼尖的小伙伴应该看出来了,我现在依然还是现在里面进行「喊口号」的动作。主要是先来看一下过程。

    刚才每个node都有一个gradients,它代表的是对某个节点的偏导。

    现在这个节点self就是loss,然后我们self.inputs[0]就是y, self.inputs[1]就是yhat, 也就是node_sigmoid。那么我们现在这个self.gradients[self.inputs[n]]其实就分别是∂loss/∂y∂loss/∂yhat,我们把对的值分别赋值给它们。

    然后我们再来看Sigmoid:

    class Sigmoid(Node):
        ...
    
        def backward(self):
            self.gradients[self.inputs[0]] = '∂{}/∂{}'.format(self.name, self.inputs[0].name)
            print('[0]: {}'.format(self.gradients[self.inputs[0]]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们依次来看哈,这个时候的self就是Sigmoid了,这个时候的sigmoid.inputs[0]应该是Linear对吧,然后我们整个self.gradients[self.inputs[0]]自然就应该是∂sigmoid/∂linear

    我们继续,这个时候self.outputs[0]就是loss, loss.gradients[self]那自然就应该是输出过来的∂loss/∂sigmoid,然后呢,我们需要将这两个部分乘起来:

    def backward(self):
        self.gradients[self.inputs[0]] = '*'.join([self.outputs[0].gradients[self], '∂{}/∂{}'.format(self.name, self.inputs[0].name)])
        print('[0]: {}'.format(self.gradients[self.inputs[0]]))
    
    • 1
    • 2
    • 3

    接着,我们就需要来看看Linear了:

    def backward(self):
        self.gradients[self.inputs[0]] = '*'.join([self.outputs[0].gradients[self], '∂{}/∂{}'.format(self.name, self.inputs[0].name)])
        self.gradients[self.inputs[1]] = '*'.join([self.outputs[0].gradients[self], '∂{}/∂{}'.format(self.name, self.inputs[1].name)])
        self.gradients[self.inputs[2]] = '*'.join([self.outputs[0].gradients[self], '∂{}/∂{}'.format(self.name, self.inputs[2].name)])
        print('[0]: {}'.format(self.gradients[self.inputs[0]]))
        print('[1]: {}'.format(self.gradients[self.inputs[1]]))
        print('[2]: {}'.format(self.gradients[self.inputs[2]]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    和上面的分析一样,我们先来看三个inputs[n]的部分,self在这里是linear了,这里的self.inputs[n]分别应该是x, k, b对吧,那么它们就应该分别是linear.gradients[x], linear.gradients[k]linear.gradients[b], 也就是∂linear/∂x,∂linear/∂k, ∂linear/∂b

    那反过来,outputs就应该反向来找,那么self.outputs[0]这会儿就应该是sigmoid。sigmoid.gradients[self]就是前一个输出过来的∂loss/∂sigmoid * ∂sigmoid/∂linear, 那后面以此的[1]和[2]我们也就应该明白了。

    然后后面分别是∂linear/∂x,∂linear/∂k, ∂linear/∂b。一样,我们将它们用乘号连接起来。

    公式就应该是:

    ∂ l o s s ∂ s i g m o i d ⋅ ∂ s i g m o i d ∂ l i n e a r ⋅ ∂ l i n e a r ∂ x ∂ l o s s ∂ s i g m o i d ⋅ ∂ s i g m o i d ∂ l i n e a r ⋅ ∂ l i n e a r ∂ k ∂ l o s s ∂ s i g m o i d ⋅ ∂ s i g m o i d ∂ l i n e a r ⋅ ∂ l i n e a r ∂ b

    losssigmoidsigmoidlinearlinearxlosssigmoidsigmoidlinearlinearklosssigmoidsigmoidlinearlinearb" role="presentation" style="position: relative;">losssigmoidsigmoidlinearlinearxlosssigmoidsigmoidlinearlinearklosssigmoidsigmoidlinearlinearb
    sigmoidlosslinearsigmoidxlinearsigmoidlosslinearsigmoidklinearsigmoidlosslinearsigmoidblinear

    那同理,我们还需要写一下Placeholder

    def Placeholder(Node):
        ...
        def backward(self):
            print('我获取了我自己的gradients: {}'.format(self.outputs[0].gradients[self]))
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5

    好,我们来看下我们模拟的情况如何,看看它们是否都如期喊口号了, 结合我们之前的前向传播的结果,我们一起来看:

    for node in sorted_nodes:
        node.forward()
        
    for node in sorted_nodes[::-1]:
        print('\n{}'.format(node.name))
        node.backward()
    
    ---
    Loss
    [0]: ∂Loss/∂y
    [1]: ∂Loss/∂Sigmoid
    
    Sigmoid
    [0]: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear
    
    Linear
    [0]: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂x
    [1]: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂k
    [2]: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂b
    
    k
    我获取了我自己的gradients: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂k
    
    b
    我获取了我自己的gradients: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂b
    
    x
    我获取了我自己的gradients: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂x
    
    y
    我获取了我自己的gradients: ∂Loss/∂y
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    好,观察下来没问题,那我们现在还剩下最后一步。就是将这些口号替换成真正的计算的值, 其实很简单,就是将我们之前学习过并写过的函数替换进去就可以了:

    class Linear(Node):
        ...
        def backward(self):
            x, k, b = self.inputs[0], self.inputs[1], self.inputs[2]
            self.gradients[self.inputs[0]] = self.outputs[0].gradients[self] * k.value
            self.gradients[self.inputs[1]] = self.outputs[0].gradients[self] * x.value
            self.gradients[self.inputs[2]] = self.outputs[0].gradients[self] * 1
            ...
    
    class Sigmoid(Node):
        ...
        def backward(self):
            self.value = self._sigmoid(self.x.value)
            self.gradients[self.inputs[0]] = self.outputs[0].gradients[self] * self.value * (1 - self.value)
            ...
    
    class Loss(Node):
        ...
        def backward(self):
            y_v = self.y.value
            yhat_v = self.y_hat.value
            self.gradients[self.inputs[0]] = 2*np.mean(y_v - yhat_v)
            self.gradients[self.inputs[1]] = -2*np.mean(y_v - yhat_v)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    那我们来看下真正计算的结果是怎样的:

    for node in sorted_nodes[::-1]:
        print('\n{}'.format(node.name))
        node.backward()
    
    ---
    Loss
    ∂Loss/∂y: -0.402796525409167
    ∂Loss/∂Sigmoid: 0.402796525409167
    
    Sigmoid
    ∂Sigmoid/∂Linear: 0.06194395247945269
    
    Linear
    ∂Linear/∂x: 0.02224721841122111
    ∂Linear/∂k: 0.18583185743835806
    ∂Linear/∂b: 0.06194395247945269
    
    y
    gradients: -0.402796525409167
    
    k
    gradients: 0.18583185743835806
    
    b
    gradients: 0.06194395247945269
    
    x
    gradients: 0.02224721841122111
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    好,到这里,我们就实现了前向传播和反向传播,让程序自动计算出了它们的偏导值。

    不过我们整个动作还没有结束,就是我们需要将loss降低到最小才可以。

    那我们下节课,就来完成这一步。

  • 相关阅读:
    杂题——1097: 蛇行矩阵
    c# 多线程处理
    OCR表格识别—(一)
    Python爬虫实战:从API获取数据
    python面试题常用语句
    Linux/Windows中创建共享文件夹
    【Spring】MyBatis(缓存机制、二级缓存、插件)面试题
    在window上安装redis 如何安装redis-server
    C++ 图解二叉树非递归后序 + 实战力扣题
    NX二次开发-UFUN获取显示在NX交互界面的对象UF_OBJ_is_displayable
  • 原文地址:https://blog.csdn.net/ivandoo/article/details/134491182