技术背景

在MindSpore深度学习框架中,我们可以使用mindspore.grad对函数式编程的函数直接计算自动微分,也可以使用mindspore.ops.GradOperation求解Cell类的梯度dout。本文所介绍的mindspore.ops.InsertGradientOf是一个对dout进一步进行处理的算子,类似于在Cell类中自定义一个bprop函数,不改变前向传播输出的结果,只改变反向传播的结果。

测试场景

我们使用一个简单的函数\(f(x,y)=xy^2,\frac{\partial f}{\partial x}=y^2\)来测试一下MindSpore中的自动微分,以及InsertGradientOf算子对梯度的操作。

import numpy as np
from mindspore import Tensor, ops, grad
# 定义Clip函数的上下界
a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32))
# Clip反向传播dx结果
def clip_gradient(dx):
ret = dx
if ret > a:
ret = a
if ret < b:
ret = b
return ret
# 生成一个计算图的节点
clip = ops.InsertGradientOf(clip_gradient)
# 主函数
func = lambda x, y: x * y ** 2
# 带Clip的主函数
def clip_func(x, y):
x = clip(x)
c = func(x, y)
return c
# 带Clip主函数的前向传播
def f(x, y):
return clip_func(x, y)
# 带Clip主函数的反向传播
def fd(x, y):
return grad(clip_func)(x, y)
# 给定x,y的数值
x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([2]).astype(np.float32))
# 分别计算Clip前后的主函数的前向传播与反向传播
print("forward: ", func(x, y))
print("backward: ", grad(func)(x, y))
print("clip forward: ", f(x, y))
print("clip backward:", fd(x, y))

输出结果为:

forward:  [-8.] # x*y**2
backward: [4.] # y**2
clip forward: [-8.] # x*y**2
clip backward: [1.1] # clip(y**2)

需要注意的是,虽然我们最终clip的时候操作的是\(\frac{\partial f}{\partial x}=y^2\),但是在函数实现时,clip函数应该施加在\(x\)上面,而不是\(y\)上面,这表示对\(x\)的反向传播进行操作。

InsertGradientOf成员函数

bprop是MindSpore框架中Cell类的一个关于计算反向传播的函数,可以用于计算和处理梯度值。但是有一个比较偏的问题是,bprop的函数输入与construct函数的输入要求要一致,如果参数数量对不上,就会报错。关于这一点,其实torch里面处理的方案会更直观一些,可以参考这篇博客中的两个Issue。而MindSpore中要实现类似的功能,就需要依赖于这个InsertGradientOf算子。先看一个使用bprop处理Clip梯度的示例:

import numpy as np
from mindspore import Tensor, grad, nn
# 定义Clip参数
a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32))
# Clip函数
def clip_gradient(dx):
ret = dx
if ret > a:
ret = a
if ret < b:
ret = b
return ret
# 需要被Clip的Cell类
class Net(nn.Cell):
# 使用bprop处理梯度
def bprop(self, x, y, out, dout):
return clip_gradient(y**2)
def construct(self, x, y):
return x * y ** 2 x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([2]).astype(np.float32))
net = Net()
print (net(x, y))
print (grad(net)(x, y))

输出结果为:

[-8.]
[1.1]

这里还是比较容易理解的,我们手动推导了一个\(\frac{\partial f}{\partial x}=y^2\),那么就可以把\(y\)参数传给bprop函数,然后计算\(y^2\),最后再计算clip。但是这个方案要求传入到bprop函数的参数是完整的,如果参数匹配不上就会报错:

import numpy as np
from mindspore import Tensor, ops, grad, nn a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32)) def clip_gradient(dx):
ret = dx
if ret > a:
ret = a
if ret < b:
ret = b
return ret clip = ops.InsertGradientOf(clip_gradient) class Net(nn.Cell):
def bprop(self, y, out, dout):
return clip_gradient(y ** 2)
def construct(self, x, y):
return x * y ** 2 x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([2]).astype(np.float32)) net = Net()
print (net(x, y))
print (grad(net)(x, y))

这里给bprop函数传入的参数跟construct函数是对不齐的,那么计算梯度时就会出现这样的报错:

[-8.]
Traceback (most recent call last):
File "test_insert_gradient.py", line 83, in <module>
print (grad(net)(x, y))
File "/home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages/mindspore/ops/composite/base.py", line 622, in after_grad
return grad_(fn_, weights, grad_position)(*args, **kwargs)
File "/home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages/mindspore/common/api.py", line 131, in wrapper
results = fn(*arg, **kwargs)
File "/home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages/mindspore/ops/composite/base.py", line 601, in after_grad
res = self._pynative_forward_run(fn, grad_, weights, args, kwargs)
File "/home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages/mindspore/ops/composite/base.py", line 658, in _pynative_forward_run
outputs = fn(*args, **new_kwargs)
File "/home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages/mindspore/nn/cell.py", line 693, in __call__
raise err
File "/home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages/mindspore/nn/cell.py", line 690, in __call__
_pynative_executor.end_graph(self, output, *args, **kwargs)
File "/home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages/mindspore/common/api.py", line 1264, in end_graph
self._executor.end_graph(obj, output, *args, *(kwargs.values()))
TypeError: Size of bprop func inputs[1] is not equal to the size of cell inputs[2] ----------------------------------------------------
- C++ Call Stack: (For framework developers)
----------------------------------------------------
mindspore/ccsrc/pipeline/pynative/grad/grad.cc:837 GetCustomBpropPrim

但是我们知道,\(\frac{\partial f}{\partial x}=g(y)\)是一个只跟\(y\)有关的函数,其实不用传入\(x\)参数也应该要可以计算其梯度值。

接下来考虑,如果在Cell类外定义一个InsertGradientOf算子构建的函数,那么也可以在Cell类里面使用:

import numpy as np
from mindspore import Tensor, ops, grad, nn a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32)) def clip_gradient(dx):
ret = dx
if ret > a:
ret = a
if ret < b:
ret = b
return ret clip = ops.InsertGradientOf(clip_gradient) class Net(nn.Cell):
def construct(self, x, y):
return clip(x) * y ** 2 x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([2]).astype(np.float32)) net = Net()
print (net(x, y))
print (grad(net)(x, y))

输出结果为:

[-8.]
[1.1]

这个计算结果是对的,不过我们需要的是这个clip函数最好也能够调用到类本身的一些属性和成员变量,而InsertGradientOf算子也支持对成员函数进行处理:

import numpy as np
from mindspore import Tensor, ops, grad, nn a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32)) class Net(nn.Cell):
def __init__(self):
super().__init__()
self.clip = ops.InsertGradientOf(self.back)
# 把Clip定义成一个成员函数
def back(self, y):
ret = y
if ret > a:
ret = a
if ret < b:
ret = b
return ret
def construct(self, x, y):
return self.clip(x) * y ** 2 x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([0.8]).astype(np.float32)) net = Net()
print (net(x, y))
print (grad(net)(x, y))

这里输出的结果为:

[-1.2800001]
[0.64000005]

因为\(y^2=0.64\),未触发边界Clip的条件,因此这里正常输出\(\frac{\partial f}{\partial x}=y^2\),如果稍微调整下输入的\(y\),触发了边界条件,那么梯度就会被Clip:

import numpy as np
from mindspore import Tensor, ops, grad, nn a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32)) class Net(nn.Cell):
def __init__(self):
super().__init__()
self.clip = ops.InsertGradientOf(self.back)
def back(self, y):
ret = y
if ret > a:
ret = a
if ret < b:
ret = b
return ret
def construct(self, x, y):
return self.clip(x) * y ** 2 x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([2]).astype(np.float32)) net = Net()
print (net(x, y))
print (grad(net)(x, y))

计算结果为:

[-8.]
[1.1]

当然了,如果我们直接返回一个跟\(x\)、\(y\)都无关的参数作为梯度也是可以的:

import numpy as np
from mindspore import Tensor, ops, grad, nn a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32)) class Net(nn.Cell):
def __init__(self):
super().__init__()
self.clip = ops.InsertGradientOf(self.back)
def back(self, dx):
return 100.
def construct(self, x, y):
return self.clip(x) * y ** 2 x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([2]).astype(np.float32)) net = Net()
print (net(x, y))
print (grad(net)(x, y))

输出结果为:

[-8.]
100.0

如果要再传一些偏置参数到\(x\)的梯度中,例如令\(g=\frac{\partial f}{\partial x}+z\),而这个参数\(z\)一般都是通过construct函数直接传进Cell类的。此时可用的思路是,把这些额外的变量存到类的属性里面,通过读取成员变量再加载到梯度操作函数中:

import numpy as np
from mindspore import Tensor, ops, grad, nn a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32)) class Net(nn.Cell):
def __init__(self):
super().__init__()
self.clip = ops.InsertGradientOf(self.back)
self.z = 0.
def back(self, dx):
ret = dx
if ret > a:
ret = a
if ret < b:
ret = b
return ret + self.z
def construct(self, x, y, z=0.):
self.z = z
return self.clip(x) * y ** 2 x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([2]).astype(np.float32)) net = Net()
print (net(x, y, z=-1))
print (grad(net)(x, y, z=-1))

输出结果为:

[-8.]
[0.10000002]

这就实现了给梯度修饰函数传参的功能。

优先级问题

凡是有冲突的操作,就必然有一个优先级的顺序。bprop函数是用本地的方法去计算一个梯度值,而InsertGradientOf算子是对某一个变量的梯度值进行处理。因此当这两个函数同时被用于处理一个梯度值时,就需要看看谁的优先级更高:

import numpy as np
from mindspore import Tensor, ops, grad, nn a = Tensor(np.array([1.1]).astype(np.float32))
b = Tensor(np.array([0.1]).astype(np.float32)) class Net(nn.Cell):
def __init__(self):
super().__init__()
self.clip = ops.InsertGradientOf(self.back)
def back(self, y):
ret = y
if ret > a:
ret = a
if ret < b:
ret = b
return ret
def bprop(self, x, y, out, dout):
return 100.
def construct(self, x, y):
return self.clip(x) * y ** 2 x = Tensor(np.array([-2]).astype(np.float32))
y = Tensor(np.array([2]).astype(np.float32)) net = Net()
print (net(x, y))
print (grad(net)(x, y))

在这个案例中,clip函数还是对梯度做一个截断,而bprop函数则是直接返回一个梯度值。那么最终执行的输出结果为:

[-8.]
100.0

这个结果表明,bprop函数的执行优先级要高于InsertGradientOf算子。

总结概要

这篇文章主要介绍了mindspore深度学习框架中基于InsertGradientOf算子的进阶梯度操作。InsertGradientOf算子的功能跟此前介绍过的bprop功能有些类似,也是自定义梯度,但bprop更倾向于计算梯度,而InsertGradientOf算子更倾向于修改梯度,这里介绍了一些比较详细的测试案例。

版权声明

本文首发链接为:https://www.cnblogs.com/dechinphy/p/InsertGradientOf.html

作者ID:DechinPhy

更多原著文章:https://www.cnblogs.com/dechinphy/

请博主喝咖啡:https://www.cnblogs.com/dechinphy/gallery/image/379634.html

MindSpore梯度进阶操作的更多相关文章

  1. [原创]Scala学习:数组的基本操作,数组进阶操作,多维数组

    1.Scala中提供了一种数据结构-数组,其中存储相同类型的元素的固定大小的连续集合.数组用于存储数据的集合,但它往往是更加有用认为数组作为相同类型的变量的集合 2 声明数组变量: 要使用的程序的数组 ...

  2. Django中的ORM进阶操作

    Django中的ORM进阶操作 Django中是通过ORM来操作数据库的,通过ORM可以很easy的实现与数据库的交互.但是仍然有几种操作是非常绕也特别容易混淆的.于是,针对这一块,来一个分类总结吧. ...

  3. Django之Models进阶操作(字段属性)

    字段属性详细介绍 一.字段 AutoField(Field) - int自增列,必须填入参数 primary_key=True BigAutoField(AutoField) - bigint自增列, ...

  4. 第11章:sed进阶操作

    第11章:sed进阶操作 sed是一个很好的文件处理工具,本身是一个管道命令,主要是以行为单位进行处理,可以将数据行进行替换.删除.新增.选取等特定工作,下面先了解一下sed的用法 sed命令行格式为 ...

  5. 第10章:awk进阶操作

    第10章:awk进阶操作 在第4章:查找与替换简单的讲解了awk的使用,本章介绍详细讲解awk的使用.awk是一个强大的文本分析工具,简单的说awk就是把文件逐行的读入, 以空格为默认分隔符将每行切片 ...

  6. [学习笔记]LCT进阶操作

    LCT总结——应用篇(附题单)(LCT) 一般都是维护链的操作.split即可搞定. 进阶操作的话,处理好辅助树和原树的关系即可搞定. 其实,最大的区别就是,splay随便转,辅助树形态变了,但是原树 ...

  7. Django 之models进阶操作

    到目前为止,当我们的程序涉及到数据库相关操作时,我们一般都会这么搞: 创建数据库,设计表结构和字段 使用 MySQLdb 来连接数据库,并编写数据访问层代码 业务逻辑层去调用数据访问层执行数据库操作 ...

  8. Django 数据库进阶操作

    基本操作 # 增 # # models.Tb1.objects.create(c1='xx', c2='oo') 增加一条数据,可以接受字典类型数据 **kwargs # obj = models.T ...

  9. web框架-(七)Django补充---models进阶操作及modelform操作

    通过之前的课程我们可以对于Django的models进行简单的操作,今天了解下进阶操作和modelform: 1. Models进阶操作 1.1 字段操作 AutoField(Field) - int ...

  10. 03- Linux目录与文件进阶操作

    第三课 目录与文件进阶操作cat (一次性全部输出文件内容)   cat -n 文件名 显示行号   cat -b 文件名 空行不显示行号less (分页显示,可以上下翻页,光标键上下键,PageUp ...

随机推荐

  1. JVM—对象的创建流程与内存分配

    JVM-对象的创建流程与内存分配 创建流程 对象创建的流程图如下: 对象的内存分配方式 内存分配的方式有两种: 指针碰撞(Bump the Pointer) 空闲列表(Free List) 分配方式 ...

  2. 熊磊:成功移植 OpenHarmony 到多套开发板,是最开心的事

    编者按:在 OpenHarmony 生态发展过程中,涌现了大批优秀的代码贡献者,本专题旨在表彰贡献.分享经验,文中内容来自嘉宾访谈,不代表 OpenHarmony 工作委员会观点. 熊磊 华为技术有限 ...

  3. Drop 、Delete、Truncate的区别是什么

    Drop .Delete.Truncate 的区别是什么? DROP 删除表结构和数据,truncate 和 delete 只删除数据 truncate 操作,表和索引所占用的空间会恢复到初始大小:d ...

  4. VIM YouCompleteMe(ycm) 对于Python3第三方库的自动补全【部分解决】

    VIM YouCompleteMe(ycm) 对于Python3第三方库的自动补全[部分解决] Python3 学习笔记 问题:VIM 用YouCompleteMe(ycm)自动补全插件时,只能支持P ...

  5. k8s 深入篇———— docker 镜像是什么[二]

    前言 简单介绍一下docker的镜像. 正文 前面讲到了容器的工作原理了(namespace 限制了时间, cgroup限制了资源),知道docker 历史的也知道,docker 之所以能够称为容器大 ...

  6. 重新点亮linux 命令树————网络配置的查看[十一三]

    前言 简单整理一下网络配置. 正文 通过ifconfig 查看. 这个就是ip地址. 网卡mac地址. 还有一块信息非常重要: 这个io开头的信息,这里面就是我们127.0.0.1的信息. 那么就来演 ...

  7. mysql 重新整理——索引优化explain字段介绍二 [十]

    前言 紧接上文. 正文 type type字段有如下类型: 1.all 2.index 3.rang 4.ref 5.eq_ref 6.const,system 7.null 最好到最差的顺序为: s ...

  8. 数据分布算法:hash+ 一致性 hash + redis cluster 的 hash slot

    讲解分布式数据存储的核心算法,数据分布的算法 hash 算法 -> 一致性 hash 算法(memcached) -> redis cluster 的 hash slot 算法 用不同的算 ...

  9. 使用树莓派远程控制灯继电器开关,dht11温湿度网页显示,树莓派物联网

    前段时间使用esp8266搞了个智能家居,通过网页控制,但是没有办法实现远程控制,只能局域网控制,因为我没有平台,使用机制云等平台还需要开发app 使用chatgpt生成的main.py程序 from ...

  10. Linux下的常见基本指令

    pwd //显示当前用户所在的路径 ls //显示当前路径下的文件名或者目录名称 ls-l //显示当前路径下的文件或者目录的更详细的属性信息 cd 一个目录路径 //进入一个目录,进去后,可以用pw ...