技术背景

在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. C++ STL 容器和算法:详解和实例演示

    C++ STL(标准模板库)提供了一组丰富的容器和算法,使得开发者能够更加高效地编写程序.本文将介绍STL中的一些常用容器和算法. 容器 vector vector是一个动态数组,可以在运行时调整大小 ...

  2. Pandas通用函数和运算

    Pandas继承了Numpy的运算功能,可以快速对每个元素进行运算,即包括基本运算(加减乘除等),也包括复杂运算(三角函数.指数函数和对数函数等). 通用函数使用 apply和applymap app ...

  3. c# 托管和非托管资源-详解

    前言 引用:带你复习c# 托管和非托管资源_C#教程_脚本之家 (jb51.net) c# 托管和非托管比较重要,因为这涉及到资源的释放. 现在只要在计算机上运行的,无论玩出什么花来,整个什么概念,逃 ...

  4. 数据库SQL(MSSQLSERVER)服务启动错误代码3414

    昨天永和客户联系我,说他们的前台系统报错了,给我发了报错的图片.看到错误的第一眼就知道是数据库出问题了,连不上sql Server. 虽然知道是数据库出问题了,但是刚开始的时候没有打开SQL Serv ...

  5. 千字干货分享:一文教你ABI增强分析,BI的未来就在这里!

    自2017年以来,智能概念开始出现,各类商业智能BI应用的使用门槛逐渐降低,商业智能BI制造商主要竞争增强分析的能力.<2020年Gartner分析与BI平台魔法象限报告>指出,2020年 ...

  6. 单链表实现增删查改等操作(python版本)

    单向链表 : SingleLinkedList 节点类属性: 数据域:value 节点存储的数据 地址域:next 指向下一个节点的地址 链表类属性: 头指针:head 指向链表的第一个节点的地址 尾 ...

  7. Python中2种常用数据可视化库:Bokeh和Altair

    本文分享自华为云社区<探究数据可视化:Bokeh vs. Altair>,作者:柠檬味拥抱. 在数据科学和数据分析领域,数据可视化是一种强大的工具,可以帮助我们更好地理解数据.发现模式和趋 ...

  8. Signalr断线重连机制

    前言 Signalr 即时消息发布到服务器后发现链接老是自动断开,导致无法发送广播后面百度搜了一下,signalr有个超时的机制 解决办法(js) //链接到自己的hub var connection ...

  9. easyx的使用 鼠标交互(3.1)

    本文学习于B站,进行借鉴学习记录: 视频链接:鼠标操作(新版)_哔哩哔哩_bilibili 初始化调用文件头不再使用#include<graphics.h>,选择调用#include< ...

  10. PolarDB-X 源码解读:事务的一生

    简介: 本文将主要解读 PolarDB-X 中事务部分的相关代码,着重解读事务的一生在计算节点(CN)中的关键代码:从开始.执行.到最后提交这一整个生命周期. 概述 本文将主要解读 PolarDB-X ...