22. 深度学习 - 自动求导

news2024/10/7 20:26:15

在这里插入图片描述

Hi,你好。我是茶桁。

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

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

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

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


class Node():
    def __init__(self, inputs=[], name=None):
        ...
        self.value = None
    
    ...

然后再复制出一个,和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))
    ...

我们新定义的这个类叫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))
    ...

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

    ...

那我们这里定义成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')

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

sorted_node

---
[Placeholder: y,
 Placeholder: k,
 Placeholder: x,
 Placeholder: b,
 Linear: Linear,
 Sigmoid: Sigmoid,
 MSE: Loss]

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

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

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

这就是为什么我一直坚持要手写代码的原因。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))

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

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

for node in sorted_nodes[::-1]:
    node.backward()

---
获取∂Loss / ∂y
获取∂Loss / ∂Sigmoid
获取∂Sigmoid / ∂Linear
获取∂Linear / ∂x
获取∂Linear / ∂k
获取∂Linear / ∂b

这样看着似乎不是太直观,我们再将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
...

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

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

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

class Node:
    def __init__(...):
        ...
        self.gradients = dict()
    ...

然后我们倒着来看, 先来看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]]))

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

刚才每个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]]))

我们依次来看哈,这个时候的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]]))

接着,我们就需要来看看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]]))

和上面的分析一样,我们先来看三个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 \begin{align*} \frac{\partial loss}{\partial sigmoid} \cdot \frac{\partial sigmoid}{\partial linear} \cdot \frac{\partial linear}{\partial x} \\ \frac{\partial loss}{\partial sigmoid} \cdot \frac{\partial sigmoid}{\partial linear} \cdot \frac{\partial linear}{\partial k} \\ \frac{\partial loss}{\partial sigmoid} \cdot \frac{\partial sigmoid}{\partial linear} \cdot \frac{\partial linear}{\partial b} \\ \end{align*} sigmoidlosslinearsigmoidxlinearsigmoidlosslinearsigmoidklinearsigmoidlosslinearsigmoidblinear

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

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

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

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

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

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)

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1226892.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Spring Boot结合JustAuth实现支付宝、微信、微博扫码登录功能

使用Spring Boot结合JustAuth实现支付宝、微信、微博扫码登录功能 在使用Spring Boot结合JustAuth实现支付宝、微信、微博扫码登录功能之前,需要先确保已经配置好Spring Boot项目,并且添加了JustAuth的依赖。你可以在项目的pom.xml文件中添加如下依赖&a…

麦克风阵列入门

文章引注: http://t.csdnimg.cn/QP7uC 一、麦克风阵列的定义 所谓麦克风阵列其实就是一个声音采集的系统,该系统使用多个麦克风采集来自于不同空间方向的声音。麦克风按照指定要求排列后,加上相应的算法(排列算法)就可…

2 Redis的高级数据结构

1、Bitmaps 首先,最经典的应用场景就是用户日活的统计,比如说签到等。 字段串:“dbydc”,根据对应的ASCII表,最后可以得到对应的二进制,如图所示 一个字符占8位(bit),…

【GCN】GCN学习笔记一

谱域图卷积 卷积 卷积定义离散空间的卷积 图卷积简介 卷积定理谱域图卷积实现思路如何定义图上的傅里叶变换拉普拉斯矩阵 (Laplacian Matrix)拉普拉斯矩阵的性质拉普拉斯矩阵的谱分解拉普拉斯矩阵与拉普拉斯算子 图傅里叶变换 图上的信号表示经典傅里叶变…

常见Web安全

一.Web安全概述 以下是百度百科对于web安全的解释: Web安全,计算机术语,随着Web2.0、社交网络、微博等等一系列新型的互联网产品的诞生,基于Web环境的互联网应用越来越广泛,企业信息化的过程中各种应用都架设在Web平台…

性格懦弱怎么办?如何改变懦弱的性格?

性格懦弱是一个比较常见的话题了,懦弱带来的苦恼和困扰,深深影响着我们的生活,人际关系,以及事业的发展。然后如何摆脱懦弱,却并非易事,尤其是对于成年人来说,这种懦弱的性格特征,已…

关于缓存和数据库一致性问题的深入研究

如何保证缓存和数据库一致性,这是一个老生常谈的话题了。 但很多人对这个问题,依旧有很多疑惑: 到底是更新缓存还是删缓存?到底选择先更新数据库,再删除缓存,还是先删除缓存,再更新数据库&…

算法设计与分析复习--贪心(一)

文章目录 上一篇贪心的性质活动安排问题贪心背包问题最优装载哈夫曼编码下一篇 上一篇 算法设计与分析复习–动态规划 贪心的性质 贪心和动态规划都要求问题具有最优子结构; 可用贪心方法时,动态规划可能不适用 可用动态规划方法时,贪心方法…

C/C++关于main函数参数问题

文章目录 前言不带参数的main带参数的main为什么会有带参数的main总结 前言 每次写C/C程序,基本上就是一个int main(){return 0;}。但是后来在linux里面涉及到很多带参数的main函数,我一直不太理解,这里就写篇博客记录一下。 不带参数的main…

10、背景分离 —— 大津算法

上一节学习了通过一些传统计算机视觉算法,比如Canny算法来完成一个图片的边缘检测,从而可以区分出图像的边缘。 今天再看一个视觉中更常见的应用,那就是把图片的前景和背景的分离。 前景和背景 先看看什么是前景什么是背景。 在图像处理和计算机视觉中,"前景"…

SpringCloudAlibaba系列之Nacos服务注册与发现

目录 说明 认识注册中心 Nacos架构图 Nacos服务注册与发现实现原理总览 SpringCloud服务注册规范 服务注册 心跳机制与健康检查 服务发现 主流服务注册中心对比 小小收获 说明 本篇文章主要目的是从头到尾比较粗粒度的分析Nacos作为注册中心的一些实现,很…

AcWing 3. 完全背包问题 学习笔记

有 N� 种物品和一个容量是 V� 的背包,每种物品都有无限件可用。 第 i� 种物品的体积是 vi��,价值是 wi��。 求解将哪些物品装入背包,可使这些物品的总体积不…

Elasticsearch中的语义检索

一、传统检索的背景痛点 和传统的基于关键词的匹配方式不同,语义检索,利用大模型,将文本内容映射到神经网络空间,最终记忆token做检索。 例如想要搜索中国首都,例如数据集中,只有一篇文章在描述北京&#x…

Zabbix实现故障自愈

一、简介 Zabbix agent 可以运行被动检查和主动检查。 在被动检查模式中 agent 应答数据请求。Zabbix server(或 proxy)询求数据,例如 CPU load,然后 Zabbix agent 返还结果。 主动检查处理过程将相对复杂。Agent 必须首先从 Z…

优卡特脸爱云一脸通智慧管理平台权限绕过漏洞复现(CVE-2023-6099)

0x01 产品简介 脸爱云一脸通智慧管理平台是一套功能强大,运行稳定,操作简单方便,用户界面美观,轻松统计数据的一脸通系统。无需安装,只需在后台配置即可在浏览器登录。 功能包括:系统管理中心、人员信息管理…

[qemu逃逸] XNUCA2019-vexx

前言 这题没有去符合, 题目本身不算难. 用户名: root 密码: goodluck 设备逆向 题目没有去符合, 所以其实没啥好讲了, 就列一些笔者认为关键的地方 这里的定义了两块 mmio 内存区. 然后看下设备实例结构体: 可以看到 QEMUTimer, 所以多半就是劫持 dma_timer 了. 漏洞点在…

使用Qt实现多人聊天工作室

目录 1、项目背景 2、技术分析 3、架构设计 3、1 服务器架构 3.1.1 模块划分 3.1.2 模块之间的交互 3、2 客户端架构 3.2.1 模块划分 3.2.2 模块之间交互 4、实现过程 4、1 功能实现 4.1.1 用户登录注册功能​编辑 4.1.2 用户主界面功能 4、2 设计实现 4.2.1 登录…