• MindSpore梯度进阶操作


    技术背景

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

    测试场景

    我们使用一个简单的函数f(x,y)=xy2,fx=y2来测试一下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的时候操作的是fx=y2,但是在函数实现时,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]
    

    这里还是比较容易理解的,我们手动推导了一个fx=y2,那么就可以把y参数传给bprop函数,然后计算y2,最后再计算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
    

    但是我们知道,fx=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]
    

    因为y2=0.64,未触发边界Clip的条件,因此这里正常输出fx=y2,如果稍微调整下输入的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]
    

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

    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=fx+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

  • 相关阅读:
    科技“蝶变”,两轮电动车下一个五年的“新动力”
    基子Android的自助洗衣店预约系统的设计与实现
    外观缺陷检测原理
    Codejock Toolkit工具包专业版
    Windows OpenGL ES 图像色调
    Android Camera FW 里的requestId和frameId
    SwiftUI 6.0(iOS 18)新容器视图修改器漫谈
    《机械工程基础》复习题
    深入JVM:全面解析GC调优
    肠道重要菌属——嗜胆菌属 (Bilophila)喜欢脂肪、耐胆汁的促炎菌
  • 原文地址:https://www.cnblogs.com/dechinphy/p/18194324/InsertGradientOf