23. 深度学习 - 多维向量自动求导

news2024/11/18 13:37:11

茶桁的AI秘籍 核心能力 23

Hi, 你好。我是茶桁。

前面几节课中,我们从最初的理解神经网络,到讲解函数,多层神经网络,拓朴排序以及自动求导。 可以说,最难的部分已经过去了,这节课到了我们来收尾的阶段,没错,生长了这么久,终于到迎接成果的时候了。

好,让我们开始。

我们还是用上一节课的代码:21.ipynb

我们上一节课中,实现了自动计算的部分。

for node in sorted_nodes[::-1]:
    print('\n{}'.format(node.name))
    node.backward()

结果我就不打印了,节省篇幅。

那我们到这一步之后,咱们就已经获得了偏导,现在要考虑的问题就是去更新它,去优化它的值。

learning_rate = 1e-5

for node in sorted_nodes:
    node.value = node.value + -1 * node.gradients[node] * learning_rate

node的值去更新,就应该等于它本身的值加上一个-1乘以它的偏导在乘以一个learning_rate, 我们对这个是不是已经很熟悉了?我们从第8节线性回归的时候就一直在接触这个公式。

只不过在这个地方,x, y的值也要更新吗? 它们的值是不应该去更新的,那要更新的应该是k, b的值。

那么在这个地方该怎么办呢?其实很简单,我们添加一个判断就可以了:

for node in sorted_nodes:
    if node.is_trainable:
        node.value = node.value + -1 * node.gradients[node] * learning_rate

然后我们给之前定义的类上加一个变量用于判断。

class Node:
    def __init__(..., is_trainable=False):
        ...
        self.is_trainable = is_trainable

在这里我们默认是不可以训练的,只有少数的一些是需要训练的。

然后我们在初始化的部分把这个定义的值加上:

node_k = Placeholder(name='k', is_trainable=True)
node_b = Placeholder(name='b', is_trainable=True)

对了,我们还需要将Placeholder做些改变:

class Placeholder(Node):
    def __init__(..., is_trainable=False):
        Node.__init__(.., is_trainable=is_trainable)
        ...
    ...

这就意味着,运行for循环的时候只有k和b的值会更新,我们再加几句话:

for node in sorted_nodes:
    if node.is_trainable:
        ...
        cmp = 'large' if node.gradients[node] > 0 else 'small'
        print('{}的值{},需要更新。'.format(node.name, cmp))

---
k的值small,需要更新。
b的值small,需要更新。

我们现在将forward, backward和optimize的三个循环封装乘三个方法:

def forward(graph_sorted_nodes):
    # Forward
    for node in sorted_nodes:
        node.forward()

def backward(graph_sorted_nodes):
    # Backward
    for node in sorted_nodes[::-1]:
        print('\n{}'.format(node.name))
        node.backward()

def optimize(graph_sorted_nodes, learning_rate=1e-3):
    # optimize
    for node in sorted_nodes:
        if node.is_trainable:
            node.value = node.value + -1 * node.gradients[node] * learning_rate
            cmp = 'large' if node.gradients[node] > 0 else 'small'
            print('{}的值{},需要更新。'.format(node.name, cmp))

然后我们再来定义一个epoch方法,将forward和backward放进去一起执行:

def run_one_epoch(graph_sorted_nodes):
    forward(graph_sorted_nodes)
    backward(graph_sorted_nodes)

这样,我们完成一次完整的求值-求导-更新,就可以写成这样:

run_one_epoch(sorted_nodes)
optimize(sorted_nodes)

为了更好的观察,我们将所有的print都删掉,然后在backward方法中写一个观察loss的打印函数:

def backward(graph_sorted_nodes):
    # Backward
    for node in sorted_nodes[::-1]:
        if isinstance(node, Loss):
            print('loss value: {}'.format(node.value))
        node.backward()

然后我们来对刚才完整的过程做个循环:

# 完整的一次求值-求导-更新:
for _ in range(10):
    run_one_epoch(sorted_nodes)
    optimize(sorted_nodes, learning_rate=1e-1)

---
loss value: 0.12023025149136042
loss value: 0.11090709486917472
loss value: 0.10118818479676453
loss value: 0.09120180962480523
loss value: 0.08111466190584131
loss value: 0.0711246044819575
loss value: 0.061446239826641165
loss value: 0.05229053883349982
loss value: 0.043842158831920566
loss value: 0.036239620745126

可以看到loss在一点点的下降。当然,这样循环10次我们还能观察出来,但是我们如果要成百上千次的去计算它,这样可就不行了, 那我们需要将history存下来,然后用图来显示出来:

loss_history = []
for _ in range(100):
    ...
    _loss_node = sorted_nodes[-1]
    assert isinstance(_loss_node, Loss)
    loss_history.append(_loss_node.value)
    optimize(sorted_nodes, learning_rate=1e-1)

plt.plot(loss_history)

Alt text

我们现在可以验证一下,我们拟合的yhat和真实的y之间差距有多大,首先我们当然是要获取到每个值的下标,然后用sigmoid函数来算一下:

sorted_nodes

---
[k, y, x, b, Linear, Sigmoid, Loss]

通过下标来进行计算, k是0, x是2, b是3, y是1:

def sigmoid(x):
    return 1/(1+np.exp(-x))

# k*x+b
sigmoid_x = sorted_nodes[0].value * sorted_nodes[2].value + sorted_nodes[3].value
print(sigmoid(sigmoid_x))

# y
print(sorted_nodes[1].value)

---
0.891165479601981
0.8988713384533658

可以看到,非常的接近。那说明我们拟合的情况还是不错的。

好,这里总结一下,就是我们有了拓朴排序,就能向前去计算它的值,通过向前计算的值就可以向后计算它的值。那现在其实我们已经完成了一个mini的深度学习框架的核心内容,咱们能够定义节点,能够前向传播运算,能够反向传播运算,能更新梯度了。

那接下来是不是就结束了呢?很遗憾,并没有,接着咱们还要考虑如何处理多维数据。咱们现在看到的数据都是x、k、b的输入,也就是都是一维的。

然而咱们真实世界中大多数场景下其实都是多维度的,其实都是多维数组。那么多维数组的还需要更新些什么,和现在有什么区别呢?

我们来接着往后看,因为基本上写法和现在这些几乎完全一样,那我也就不这么细致的讲了。

为了和之前代码做一个区分,所以我将多维向量计算的代码从新开了个文件,放在了23.ipynb里,小伙伴可以去下载到本地研习。

那么多维和现在最大的区别在哪里呢?就在于计算的时候,我们就要用到矩阵运算了。只是值变成了矩阵,运算变成的了矩阵运算。好,我们从Node开始来改动它,没什么变化的地方我就直接用...来省略了:

class Node:
    def __init__(self, input=[]):
        ...
    def forward(self):
        raise NotImplemented
    def backward(self):
        raise NotImplemented

class Placeholder(Node):
    def __init__(self):
        Node.__init__(self)
    def forward(self, value=None):
        ...
    def backward(self):
        self.gradients = {self:0}
        for n in self.outputs:
            grad_cost = n.gradients[self]
            self.gradients[self] = grad_cost * 1

class Linear(Node):
    def __init__(self, x, k, b):
        ...
    def forward(self):
        ...
    def backward(self):
        self.gradients = {n: np.zeros_like(n.value) for n in self.inputs}

        for n in self.outputs:
            grad_cost = n.gradients[self]

            self.gradients[self.inputs[0]] = np.dot(grad_cost, self.inputs[1].value.T)
            self.gradients[self.inputs[1]] = np.dot(self.inputs[0].value.T, grad_cost)
            self.gradients[self.inputs[2]] = np.sum(grad_cost, axis=0, keepdims=False)

class Sigmoid(Node):
    def __init__(self, node):
        Node.__init__(self, [node])
    def _sigmoid(self, x):
        ...
    def forward(self):
        ...

    def backward(self):
        self.partial = self._sigmoid(self.x) * (1 - self._sigmoid(self.x))
        self.gradients = {n: np.zeros_like(n.value) for n in self.inputs}
        for n in self.outputs:
            grad_cost = n.gradients[self]  
            self.gradients[self.inputs[0]] = grad_cost * self.partial

class MSE(Node): # 也就是之前的Loss类
    def __init__(self, y, a):
        Node.__init__(self, [y, a])
    def forward(self):
        y = self.inputs[0].value.reshape(-1, 1)
        a = self.inputs[1].value.reshape(-1, 1)
        assert(y.shape == a.shape)
        self.m = self.inputs[0].value.shape[0]
        self.diff = y - a
        self.value = np.mean(self.diff**2)
    def backward(self):
        self.gradients[self.inputs[0]] = (2 / self.m) * self.diff
        self.gradients[self.inputs[1]] = (-2 / self.m) * self.diff

类完成之后,我们还有一些其他的方法:

def forward_and_backward(graph): # run_one_epoch
    for n in graph:
        n.forward()
    for n in  graph[::-1]:
        n.backward()
def toplogic(graph):
    ...
def convert_feed_dict_to_graph(feed_dict):
    ...
# 将sorted_nodes赋值从新定义了一个方法
def topological_sort_feed_dict(feed_dict):
    graph = convert_feed_dict_to_graph(feed_dict)
    return toplogic(graph)

def optimize(trainables, learning_rate=1e-2):
    for node in trainables:
        node.value += -1 * learning_rate * node.gradients[node]

这样就完成了。可以发现基本上代码没有什么变动,变化比较大的都是各个类中的backward方法,因为要将其变成使用矩阵运算。

我们来尝试着用一下这个多维算法,我们还是用波士顿房价的那个数据来做一下尝试:

X_ = data['data']
y_ = data['target']

# Normalize data
X_ = (X_ - np.mean(X_, axis=0)) / np.std(X_, axis=0)

n_features = X_.shape[1]
n_hidden = 10
W1_ = np.random.randn(n_features, n_hidden)
b1_ = np.zeros(n_hidden)
W2_ = np.random.randn(n_hidden, 1)
b2_ = np.zeros(1)

# Neural network
X, y = Placeholder(), Placeholder()
W1, b1 = Placeholder(), Placeholder()
W2, b2 = Placeholder(), Placeholder()

l1 = Linear(X, W1, b1)
s1 = Sigmoid(l1)
l2 = Linear(s1, W2, b2)
cost = MSE(y, l2)

feed_dict = {
    X: X_,
    y: y_,
    W1: W1_,
    b1: b1_,
    W2: W2_,
    b2: b2_
}

epochs = 5000
# Total number of examples
m = X_.shape[0]
batch_size = 16
steps_per_epoch = m // batch_size

graph = topological_sort_feed_dict(feed_dict)
trainables = [W1, b1, W2, b2]

print("Total number of examples = {}".format(m))

我们在中间定义了l1, s1, l2, cost, 分别来实例化四个类。然后我们就需要根据数据来进行迭代计算了,定义一个losses来保存历史数据:

losses = []

epochs = 100

for i in range(epochs):
    loss = 0
    for j in range(steps_per_epoch):
        # Step 1
        X_batch, y_batch = resample(X_, y_, n_samples=batch_size)

        X.value = X_batch
        y.value = y_batch

        # Step 2
        forward_and_backward(graph) # set output node not important.

        # Step 3
        rate = 1e-2
    
        optimize(trainables, rate)

        loss += graph[-1].value
    
    if i % 100 == 0: 
        print("Epoch: {}, Loss: {:.3f}".format(i+1, loss/steps_per_epoch))
        losses.append(loss/steps_per_epoch)

---
Epoch: 1, Loss: 194.170
...
Epoch: 4901, Loss: 3.137

可以看到它loss下降的非常快,还记得咱们刚开始的时候在训练波士顿房价数据的时候,那个loss下降到多少? 最低是不是就下降到在第一节课的时候我们的lose最多下降到了多少47.34对吧?那现在呢?直接下降到了3,这是为什么? 因为我们的维度多了,维度多了它就准确了。这说明什么? 说明大家去谈恋爱的时候,不要盯着对象的一个方面,多方面考察,才能知道这个人是否合适。

好,现在看起来效果是很好,但是我们想知道到底拟合出来的什么函数,那怎么办?咱们把这个维度降低成三维空间就可以看了。

现在咱们这个波士顿的所有数据实际上是一个15维的数据,15维的数据你根本看不了,咱们现在只要把x这个里边取一点值,在这个里边稍微把值给它变一下。

X_ = dataframe[['RM', 'LSTAT']]
y_ = data['target']

在咱们之前的课程中对其进行计算的时候就分析过,RM和LSTAT是影响最大的两个特征,我们还是来用这个。然后我们将刚才的代码从新运行一遍:

losses = []

for i in tqdm_notebook(range(epochs)):
    ...

---
Epoch: 1, Loss: 150.122
...
Epoch: 4901, Loss: 16.181

这次下降的就没上次好了。

现在我们可视化一下这个三维空间来看看:

from mpl_toolkits.mplot3d import Axes3D

predicate_results = []
for rm, ls in X_.values:
    X.value = np.array([[rm, ls]])
    forward_and_backward(graph)
    predicate_results.append(graph[-2].value[0][0])

%matplotlib widget

fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(111, projection='3d')

X_ = dataframe[['RM', 'LSTAT']].values[:, 0]
Y_ = dataframe[['RM', 'LSTAT']].values[:, 1]

Z = predicate_results

rm_and_lstp_price = ax.plot_trisurf(X_, Y_, Z, color='green')

ax.set_xlabel('RM')
ax.set_ylabel('% of lower state')
ax.set_zlabel('Predicated-Price')

然后我们就能看到一个数据的三维图形,因为我们开启了widget, 所以可以进行拖动。

Alt text

Alt text

从图形上看,确实符合房间越多,低收入人群越少,房价越高的特性。

那现在计算机确实帮我们自动的去找到了一个函数,这个函数到底怎么设置咱们都不用关心,它自动就给你求解出来,这个就是深度学习的意义。咱们经过这一系列写出来的东西其实就已经能够做到。

我觉得这个真的有一种数学之美,它从最简单的东西出发,最后做成了这样一个复杂的东西。确实很深其,并且还都在我们的掌握之中。

好,大家下来以后记得要多多自己敲代码,多分析其中的一些过程和原理。

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

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

相关文章

搜维尔科技:Faceware面部捕捉最佳实践!

视频源和分辨率: 我们的软件针对 RGB 彩色素材进行了优化,不支持使用红外摄像机。 我们建议视频分辨率为 720p 和 1080p。低于 720p 的分辨率可能会对跟踪质量产生负面影响,而高于 1080p 的分辨率会导致存储要求和传输时间增加,而…

【数字化转型方法论读书笔记】-数据中台角色解读

一千个读者,就有一千个哈姆雷特。同样,数据中台对于企业内部不同角色的价值也不同,下面分别从董事长、CEO、 CTO/CIO、IT 架构师、数据分析师这 5 个角色的视角详细解读数据中台。 1、董事长视角下的数据中台 在数字经济时代,企业…

机器学习【00】pycharm使用远程服务器

我们使用conda在服务器上创建虚拟环境,远程使用pycharm进行编程 pycharm版本2023.1.3 一.首先在服务器上创建虚拟环境 注:anaconda的安装可以参考ubuntu系统miniconda的安装 conda create --name tac python3.7二.pycharm 连接 点击add interpreter …

Android系统调试工具大全:解密adb、dumpsys、procrank等神器

Android系统调试工具大全:解密adb、dumpsys、procrank等神器 引言 Android开发中,调试是一个非常重要的环节,本文将介绍一些常用的Android系统调试工具,包括adb、logcat、procrank、dumpsys、dmesg、top、free、df、trace、pm、…

利用GenericMenu创建上下文菜单或下拉菜单

使用GenericMenu 创建自定义上下文菜单和下拉菜单丰富自己的编辑器功能。 GenericMenu 介绍 变量 allowDuplicateNames 允许菜单具有多个同名的菜单项。 公共函数 AddDisabledItem 向菜单添加已禁用的项。 AddItem 向菜单添加一个项。 AddSeparator 向菜单添加一个分隔符项…

缓存雪崩、击穿、穿透及解决方案_保证缓存和数据库一致性

文章目录 缓存雪崩、击穿、穿透1.缓存雪崩造成缓存雪崩解决缓存雪崩 2. 缓存击穿造成缓存击穿解决缓存击穿 3.缓存穿透造成缓存穿透解决缓存穿透 更新数据时,如何保证数据库和缓存的一致性?1. 先更新数据库?先更新缓存?解决方案 2…

PTA 六度空间

“六度空间”理论又称作“六度分隔(Six Degrees of Separation)”理论。这个理论可以通俗地阐述为:“你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过五个人你就能够认识任何一个陌生人。”如图1所示…

Jmeter 压测保姆级入门教程

1、Jmeter本地安装 1.1、下载安装 软件下载地址: https://mirrors.tuna.tsinghua.edu.cn/apache/jmeter/binaries/ 选择一个压缩包下载即可 然后解压缩后进入bin目录直接执行命令jmeter即可启动 1.2 修改语言 默认是英文的,修改中文,点击…

使用原生js通过ajax实现服务器渲染的简单代码和个人改进

文章目录 前文提要代码实现主要参考服务器渲染实现逻辑网页呈现效果 代码分段讲解提要html的body部分css部分js部分xhr.open函数AJAX-onreadystatechange事件function函数简写方法附件功能:选中行 高亮 代码全文 前文提要 本文仅做个人学习记录,如有错误…

Apache服务Rwrite功能使用

Rewrite也称为规则重写,主要功能是实现浏览器访问时,URL的跳转。其正则表达式是基于Perl语言。要使用rewrite功能,Apache服务器需要添加rewrite模块。如果使用源码编译安装,–enable-rewrite。有了rewrite模块后,需要在…

怎么快速卸载office365

怎么快速卸载office365 根据官网提供的两种解决方案即点即用或MSIMicrosoft Store 根据官网提供的两种解决方案 官网地址:https://support.microsoft.com/zh-cn/office/%E4%BB%8E-pc-%E5%8D%B8%E8%BD%BD-office-9dd49b83-264a-477a-8fcc-2fdf5dbf61d8#OfficeVersio…

visionOS空间计算实战开发教程Day 6 拖拽和点击

在之前的学习中我们在空间中添加了3D模型,但在初始摆放后就无法再对其进行移动或做出修改。本节我们在​​Day 5​​显示和隐藏的基础上让我们模型可以实现拖拽效果,同时对纯色的立方体实现点击随机换色的功能。 首先是入口文件,无需做出改变…

单元测试-java.lang.NullPointerException

报错信息 java.lang.NullPointerException 空指针异常 空对象引用 来源 对Controller层进行单元测试,解决完Spring上下文报错后继续报错。 解决 在测试方法执行前要为字段完成对象的注入,否则就报空指针异常。 测试例子 public class SysUserContr…

若依框架参数验证

文章目录 一、前端触发参数校验异常1.前端页面2.前端代码 二、后端触发参数校验异常1.前端页面2.后端报错 三、后端自定义参数验证1.添加注解2.触发后端校验 一、前端触发参数校验异常 1.前端页面 输入不符合校验规则的值来触发 2.前端代码 校验规则数组 表单的元素 修…

RTT打印在分区跳转后无法打印问题

场景: RTT打印仅占用JLINK的带宽,比串口传输更快更简洁,同时RTT可以使用jscope对代码里面的变量实时绘图显示波形,而采用串口打印波形无法实时打印。同时可以保存原始数据到本地进行分析,RTT在各方面完胜串口。 问题描…

【深度学习】Transformer简介

近年来,Transformer模型在自然语言处理(NLP)领域中横扫千军,以BERT、GPT为代表的模型屡屡屠榜,目前已经成为了该领域的标准模型。同时,在计算机视觉等领域中,Transformer模型也逐渐得到了重视&a…

CVE-2023-27524:Apache Superset未授权访问漏洞复现

文章目录 ​Apache Superset 未授权访问漏洞(CVE-2023-27524)复现0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.漏洞复现 0x06 修复建议 ​Apache Superset 未授权访问漏洞(CVE-2023-27524)复现 0x01 前言 免责声明:请勿利用文…

【追求卓越04】数据结构--栈与队列

引导 今天我们开始学习栈与队列的内容,我觉得栈并不难,所以篇幅也就不会那么多了。在虚拟空间中,栈是用户空间中的一种数据结构,它主要用于保存局部变量。那么问题来了,为什么用栈来保存局部变量,不用别的数…

【数据结构(四)】前缀、中缀、后缀表达式(逆波兰表达式)和逆波兰计算器的代码实现(2)

文章目录 1. 前缀表达式(波兰表达式)1.1. 前缀表达式的计算机求值 2. 中缀表达式3. 后缀表达式(逆波兰表达式)3.1. 后缀表达式的计算机求值3.2. 逆波兰计算器的实现 4. 中缀表达式 转 后缀表达式4.1. 思路分析4.2. 代码实现 5. 逆波兰计算器的完整版 1. 前缀表达式(波兰表达式)…

如何搭建Zblog网站并通过内网穿透将个人博客发布到公网

文章目录 1. 前言2. Z-blog网站搭建2.1 XAMPP环境设置2.2 Z-blog安装2.3 Z-blog网页测试2.4 Cpolar安装和注册 3. 本地网页发布3.1. Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 想要成为一个合格的技术宅或程序员,自己搭建网站制作网页是绕…