转载:【AI系统】动手实现 PyTorch 微分

news/2024/12/12 18:17:56/文章来源:https://www.cnblogs.com/xueaigc/p/18603140

这里记录一下使用操作符重载(OO)编程方式的自动微分,其中数学实现模式则是使用反向模式(Reverse Mode),综合起来就叫做反向 OO 实现 AD 啦。

基础知识

下面一起来回顾一下操作符重载和反向模式的一些基本概念,然后一起去尝试着用 Python 去实现 PyTorch 这个 AI 框架中最核心的自动微分机制是如何实现的。

操作符重载 OO

操作符重载:操作符重载或者称运算重载(Operator Overloading,OO),利用现代语言的多态特性(例如 C++/JAVA/Python 等高级语言),使用操作符重载对语言中基本运算表达式的微分规则进行封装。同样,重载后的操作符在运行时会记录所有的操作符和相应的组合关系,最后使用链式法则对上述基本表达式的微分结果进行组合完成自动微分。

在具有多态特性的现代编程语言中,运算符重载提供了实现自动微分的最直接方式,利用了编程语言的第一特性(first class feature),重新定义了微分基本操作语义的能力。

在 C++ 中使用运算符重载实现的流行工具是 ADOL-C(Walther 和 Griewank,2012)。ADOL-C 要求对变量使用启用 AD 的类型,并在 Tape 数据结构中记录变量的算术运算,随后可以在反向模式 AD 计算期间“回放”。Mxyzptlk 库 (Michelotti, 1990) 是 C++ 能够通过前向传播计算任意阶偏导数的另一个例子。

FADBAD++ 库(Bendtsen 和 Stauning,1996 年)使用模板和运算符重载为 C++ 实现自动微分。对于 Python 语言来说,autograd 提供正向和反向模式自动微分,支持高阶导数。在机器学习 ML 或者深度学习 DL 领域,目前 AI 框架中使用操作符重载 OO 的一个典型代表是 PyTorch,其中使用数据结构 Tape 来记录计算流程,在反向模式求解梯度的过程中进行 replay Operator。

下面总结一下操作符重载的一个基本流程:

  • 操作符重载:预定义了特定的数据结构,并对该数据结构重载了相应的基本运算操作符;
  • Tape 记录:程序在实际执行时会将相应表达式的操作类型和输入输出信息记录至特殊数据结构;
  • 遍历微分:得到特殊数据结构后,将对数据结构进行遍历并对其中记录的基本运算操作进行微分;
  • 链式组合:把结果通过链式法则进行组合,完成自动微分。

操作符重载法的优点可以总结如下:

  • 实现简单,只要求语言提供多态的特性能力;
  • 易用性高,重载操作符后跟使用原生语言的编程方式类似。

操作符重载法的缺点可以总结如下:

  • 需要显式的构造特殊数据结构和对特殊数据结构进行大量读写、遍历操作,这些额外数据结构和操作的引入不利于高阶微分的实现;
  • 对于类似 if,while 等控制流表达式,难以通过操作符重载进行微分规则定义。对于这些操作的处理会退化成基本表达式方法中特定函数封装的方式,难以使用语言原生的控制流表达式。

反向模式 Reverse Mode

反向自动微分同样是基于链式法则。仅需要一个前向过程和反向过程,就可以计算所有参数的导数或者梯度。因为需要结合前向和后向两个过程,因此反向自动微分会使用一个特殊的数据结构,来存储计算过程。

而这个特殊的数据结构例如 TensorFlow 或者 MindSpore,则是把所有的操作以一张图的方式存储下来,这张图可以是一个有向无环(DAG)的计算图;而 PyTorch 则是使用 Tape 来记录每一个操作,他们都表达了函数和变量的关系。

反向模式根据从后向前计算,依次得到对每个中间变量节点的偏导数,直到到达自变量节点处,这样就得到了每个输入的偏导数。在每个节点处,根据该节点的后续节点(前向传播中的后续节点)计算其导数值。

整个过程对应于多元复合函数求导时从最外层逐步向内侧求导。这样可以有效地把各个节点的梯度计算解耦开,每次只需要关注计算图中当前节点的梯度计算。

从下图可以看出来,reverse mode 和 forward mode 是一对相反过程,reverse mode 从最终结果开始求导,利用最终输出对每一个节点进行求导。下图虚线就是反向模式。

image

前向和后向两种模式的过程表达如下,表的左列浅色为前向计算函数值的过程,与前向计算时相同,右面列深色为反向计算导数值的过程。

反向模式的计算过程如图所示,其中:

$$ \overline{v_i}=\dfrac{\delta y}{\delta v_i} $$

根据链式求导法则展开有:

$$ \frac{\partial f}{\partial x}=\sum_{k=1}^{N} \frac{\partial f}{\partial v_{k}} \frac{\partial v_{k}}{\partial \boldsymbol{x}} $$

可以看出,左侧是源程序分解后得到的基本操作集合,而右侧则是每一个基本操作根据已知的求导规则和链式法则由下至上计算的求导结果。

反向操作符重载实现

下面的代码主要介绍反向模式自动微分的实现。目的是通过了解 PyTorch 的 auto diff 实现,来了解到上面复杂的反向操作符重载实现自动微分的原理,值的主要的是千万不要在乎这是 MindSpore 的实现还是 TensorFlow 版的实现(实际上都不是哈)。

首先,需要通过 typing 库导入一些辅助函数。

from typing import List, NamedTuple, Callable, Dict, Optional_name = 1
def fresh_name():global _namename = f'v{_name}'_name += 1return name

fresh_name 用于打印跟 tape 相关的变量,并用 _name 来记录是第几个变量。

为了能够更好滴理解反向模式自动微分的实现,实现代码过程中不依赖 PyTorch 的 autograd。代码中添加了变量类 Variable 来跟踪计算梯度,并添加了梯度函数 grad() 来计算梯度。

对于标量损失 l 来说,程序中计算的每个张量 x 的值,都会计算值 dl/dX。反向模式从 dl/dl=1 开始,使用偏导数和链式规则向后传播导数,例如:

$$
dl/dx*dx/dy=dl/dy
$$

下面就是具体的实现过程,首先我们所有的操作都是通过 Python 进行操作符重载的,而操作符重载,通过 Variable 来封装跟踪计算的 Tensor。每个变量都有一个全局唯一的名称 fresh_name,因此可以在字典中跟踪该变量的梯度。为了便于理解,__init__ 有时会提供此名称作为参数。否则,每次都会生成一个新的临时值。

为了适配上面图中的简单计算,这里面只提供了乘、加、减、sin、log 五种计算方式。

class Variable:def __init__(self, value, name=None):self.value = valueself.name = name or fresh_name()def __repr__(self):return repr(self.value)# We need to start with some tensors whose values were not computed# inside the autograd. This function constructs leaf nodes. @staticmethoddef constant(value, name=None):var = Variable(value, name)print(f'{var.name} = {value}')return var# Multiplication of a Variable, tracking gradientsdef __mul__(self, other):return ops_mul(self, other)def __add__(self, other):return ops_add(self, other)def __sub__(self, other):return ops_sub(self, other)def sin(self):return ops_sin(self)def log(self):return ops_log(self)

接下来需要跟踪 Variable 所有计算,以便向后应用链式规则。那么数据结构 Tape 有助于实现这一点。

class Tape(NamedTuple):inputs : List[str]outputs : List[str]# apply chain rulepropagate : 'Callable[List[Variable], List[Variable]]'

输入 inputs 和输出 outputs 是原始计算的输入和输出变量的唯一名称。反向传播使用链式规则,将函数的输出梯度传播给输入。其输入为 dL/dOutputs,输出为 dL/dinput。Tape 只是一个记录所有计算的累积 List 列表。

下面提供了一种重置 Tape 的方法 reset_tape,方便运行多次自动微分,每次自动微分过程都会产生 Tape List。

gradient_tape : List[Tape] = []# reset tape
def reset_tape():global _name_name = 1gradient_tape.clear()

现在来看看具体运算操作符是如何定义的,以乘法为例子啦,首先需要计算正向结果并创建一个新变量来表示,也就是 x = Variable(self.value * other.value)。然后定义了反向传播闭包 propagate,使用链规则来反向支撑梯度。

def ops_mul(self, other):# forwardx = Variable(self.value * other.value)print(f'{x.name} = {self.name} * {other.name}')# backwarddef propagate(dl_doutputs):dl_dx, = dl_doutputsdx_dself = other # partial derivate of r = self*otherdx_dother = self # partial derivate of r = self*otherdl_dself = dl_dx * dx_dselfdl_dother = dl_dx * dx_dotherdl_dinputs = [dl_dself, dl_dother]return dl_dinputs# record the input and output of the optape = Tape(inputs=[self.name, other.name], outputs=[x.name], propagate=propagate)gradient_tape.append(tape)return x
def ops_add(self, other):x = Variable(self.value + other.value)print(f'{x.name} = {self.name} + {other.name}')def propagate(dl_doutputs):dl_dx, = dl_doutputsdx_dself = Variable(1.)dx_dother = Variable(1.)dl_dself = dl_dx * dx_dselfdl_dother = dl_dx * dx_dotherreturn [dl_dself, dl_dother]# record the input and output of the optape = Tape(inputs=[self.name, other.name], outputs=[x.name], propagate=propagate)gradient_tape.append(tape)return xdef ops_sub(self, other):x = Variable(self.value - other.value)print(f'{x.name} = {self.name} - {other.name}')def propagate(dl_doutputs):dl_dx, = dl_doutputsdx_dself = Variable(1.)dx_dother = Variable(-1.)dl_dself = dl_dx * dx_dselfdl_dother = dl_dx * dx_dotherreturn [dl_dself, dl_dother]# record the input and output of the optape = Tape(inputs=[self.name, other.name], outputs=[x.name], propagate=propagate)gradient_tape.append(tape)return xdef ops_sin(self):x = Variable(np.sin(self.value))print(f'{x.name} = sin({self.name})')def propagate(dl_doutputs):dl_dx, = dl_doutputsdx_dself = Variable(np.cos(self.value))dl_dself = dl_dx * dx_dselfreturn [dl_dself]# record the input and output of the optape = Tape(inputs=[self.name], outputs=[x.name], propagate=propagate)gradient_tape.append(tape)return xdef ops_log(self):x = Variable(np.log(self.value))print(f'{x.name} = log({self.name})')def propagate(dl_doutputs):dl_dx, = dl_doutputsdx_dself = Variable(1 / self.value)dl_dself = dl_dx * dx_dselfreturn [dl_dself]# record the input and output of the optape = Tape(inputs=[self.name], outputs=[x.name], propagate=propagate)gradient_tape.append(tape)return x

grad 是将变量运算放在一起的梯度函数,函数的输入是 l 和对应的梯度结果 results。

def grad(l, results):dl_d = {} # map dL/dX for all values Xdl_d[l.name] = Variable(1.)print("dl_d", dl_d)def gather_grad(entries):return [dl_d[entry] if entry in dl_d else None for entry in entries]for entry in reversed(gradient_tape):print(entry)dl_doutputs = gather_grad(entry.outputs)dl_dinputs = entry.propagate(dl_doutputs)for input, dl_dinput in zip(entry.inputs, dl_dinputs):if input not in dl_d:dl_d[input] = dl_dinputelse:dl_d[input] += dl_dinputfor name, value in dl_d.items():print(f'd{l.name}_d{name} = {value.name}')return gather_grad(result.name for result in results)

以下面公式为例:

$$ f(x_{1},x_{2})=ln(x_{1})+x_{1}*x_{2}−sin(x_{2}) $$

因为是基于操作符重载 OO 的方式进行计算,因此在初始化自变量 x 和 y 的值需要使用变量 Variable 来初始化,然后通过代码 f = Variable.log(x) + x * y - Variable.sin(y) 来实现。

reset_tape()x = Variable.constant(2., name='v-1')
y = Variable.constant(5., name='v0')f = Variable.log(x) + x * y - Variable.sin(y)
print(f)
    v-1 = 2.0v0 = 5.0v1 = log(v-1)v2 = v-1 * v0v3 = v1 + v2v4 = sin(v0)v5 = v3 - v411.652071455223084

print(f) 可以看到是下面图中的左边正向运算,计算出前向的结果。下面的代码 grad(f, [x, y]) 就是利用前向最终的结果,通过 Tape 一个个反向的求解。得到最后的结果啦。

image

dx, dy = grad(f, [x, y])
print("dx", dx)
print("dy", dy)
    dl_d {'v5': 1.0}Tape(inputs=['v3', 'v4'], outputs=['v5'], propagate=<function ops_sub.<locals>.propagate at 0x7fd7a2c8c0d0>)v9 = v6 * v7v10 = v6 * v8Tape(inputs=['v0'], outputs=['v4'], propagate=<function ops_sin.<locals>.propagate at 0x7fd7a2c8c378>)v12 = v10 * v11Tape(inputs=['v1', 'v2'], outputs=['v3'], propagate=<function ops_add.<locals>.propagate at 0x7fd7a234e7b8>)v15 = v9 * v13v16 = v9 * v14Tape(inputs=['v-1', 'v0'], outputs=['v2'], propagate=<function ops_mul.<locals>.propagate at 0x7fd7a3982ae8>)v17 = v16 * v0v18 = v16 * v-1v19 = v12 + v18Tape(inputs=['v-1'], outputs=['v1'], propagate=<function ops_log.<locals>.propagate at 0x7fd7a3982c80>)v21 = v15 * v20v22 = v17 + v21dv5_dv5 = v6dv5_dv3 = v9dv5_dv4 = v10dv5_dv0 = v19dv5_dv1 = v15dv5_dv2 = v16dv5_dv-1 = v22dx 5.5dy 1.7163378145367738

如果您想了解更多AI知识,与AI专业人士交流,请立即访问昇腾社区官方网站https://www.hiascend.com/或者深入研读《AI系统:原理与架构》一书,这里汇聚了海量的AI学习资源和实践课程,为您的AI技术成长提供强劲动力。不仅如此,您还有机会投身于全国昇腾AI创新大赛和昇腾AI开发者创享日等盛事,发现AI世界的无限奥秘~
转载自:| https://www.cnblogs.com/ZOMI/articles/18562818 | header |
| ---------------------------------------------- | ------ |
| | |

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

转载:【AI系统】计算图原理

在前面的文章曾经提到过,目前主流的 AI 框架都选择使用计算图来抽象神经网络计算表达,通过通用的数据结构(张量)来理解、表达和执行神经网络模型,通过计算图可以把 AI 系统化的问题形象地表示出来。 本文将会以 AI 概念落地的时候,遇到的一些问题与挑战,因此引出了计算图…

转载:【AI系统】计算图基本介绍

在 AI 框架发展的最近一个阶段,技术上主要以计算图来描述神经网络。前期实践最终催生出了工业级 AI:TensorFlow 和 PyTorch,这一时期同时伴随着如 Chainer、DyNet、CNTK、PaddlePaddle、JAX 等激发了框架设计灵感的诸多实验课程。 TensorFlow 和 PyTorch,特别是 PyTorch 代…

转载:【AI系统】什么是微分

自动微分(Automatic Differentiation,AD)是一种对计算机程序进行高效准确求导的技术,一直被广泛应用于计算流体力学、大气科学、工业设计仿真优化等领域。 近年来,机器学习技术的兴起也驱动着对自动微分技术的研究进入一个新的阶段。随着自动微分和其他微分技术研究的深入…

转载:【AI系统】Auto-Tuning 原理

在硬件平台驱动算子运行需要使用各种优化方式来提高性能,然而传统的手工编写算子库面临各种窘境,衍生出了自动生成高性能算子的的方式,称为自动调优。在本文我们首先分析传统算子库面临的挑战,之后介绍基于 TVM 的业界领先的三个自动调优系统。 高性能算子挑战 DNN 部署的硬…

转载:【AI系统】微分计算模式

上一篇文章简单了解计算机中常用几种微分方式。本文将深入介绍 AI 框架离不开的核心功能:自动微分。 而自动微分则是分为前向微分和后向微分两种实现模式,不同的实现模式有不同的机制和计算逻辑,而无论哪种模式都离不开雅克比矩阵,所以我们也会深入了解一下雅克比矩阵的原理…

转载:【AI系统】AI 框架之争

在前面的内容主要是讲述了 AI 框架在数学上对自动微分进行表达和处理,最后表示称为开发者和应用程序都能很好地去编写深度学习中神经网络的工具和库,整体流程如下所示:除了要回答最核心的数学表示原理以外,实际上 AI 框架还要思考和解决许多问题,如 AI 框架如何对实际的神…

转载:【AI系统】指令和存储优化

除了应用极广的循环优化,在 AI 编译器底层还存在指令和存储这两种不同优化。 指令优化 指令优化依赖于硬件提供的特殊加速计算指令。这些指令,如向量化和张量化,能够显著提高计算密度和执行效率。向量化允许我们并行处理数据,而张量化则进一步扩展了这一概念,通过将数据组…

转载:【AI系统】算子循环优化

在具体硬件执行计算的时候,实际会大量地使用 for 等循环指令不断地去读取不同的数据执行重复的指令(SIMT/SIMD),因此循环优化主要是为了提升数据的局部性或者计算的并行性,从而提升整体算子性能,当然这二者都需要 AI 芯片硬件的支持。 循环优化挑战 数据局部性 数据的局部…

数字组合转字母删除二叉树节点字符串相乘打家劫舍ii无序数组第k大 无序数组前k大两个有序数组合并中文数字转换为整数最大连续子数组和零钱凑数

一、数字串转换为字符串 1-26个数字分别代表26个字符(A-z)输入"12326〞就可以拆分为【1,2,3,2,6】、(12, 3, 2, 6]. [1, 23, 2, 6]【1,23,26】、【12,3,26】等,将每种组合转成成对应字母输出,输出所有可能的结果返回所有可能的转换结果// 将数字串转换成字母串// 将数…

PDFgear强大的PDF转换编辑工具

点击上方蓝字睿共享资源关注我 前言 PDFgear是一个很好用的PDF软件,它不仅能看PDF文件,还能把PDF转换成其他格式的文件,或者编辑它们。而且,它还有很多实用的功能,比如你可以在PDF上签名、填写表单等等。这个软件的界面看起来很舒服,用起来也很简单,不管你是新手还是老手…

使用python进行PostgreSQL 数据库连接

使用python进行PostgreSQL 数据库连接 PostgreSQL 数据库是最常用的关系型数据库之一,最吸引人的一点是它作为开源数据库且具有可拓展性,能够提供丰富的应用。运用python可以很简单的建立PostgreSQL 数据库连接,其中最受欢迎的就是psycopg。 1. 安装psycopg2 Psycopy是针对p…