MindSpore自动微分小技巧

技术背景

基于链式法则的自动微分技术,是大多数深度学习框架中所支持的核心功能,旨在更加快速的进行梯度计算,并且可以绕开符号微分的表达式爆炸问题和手动微分的困难推导问题。本文主要基于MindSpore框架,记录一下几种自动微分的使用技巧。MindSpore版本信息:

bash 复制代码
Name: mindspore
Version: 2.2.13
Summary: MindSpore is a new open source deep learning training/inference framework that could be used for mobile, edge and cloud scenarios.
Home-page: https://www.mindspore.cn
Author: The MindSpore Authors
Author-email: contact@mindspore.cn
License: Apache 2.0
Location: /home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages
Requires: packaging, pillow, protobuf, asttokens, numpy, psutil, scipy, astunparse
Required-by: 

函数微分

首先我们定义一个\(E\)的函数表达形式:

\[E=x\cdot z+y \]

其中包含有三个输入变量,写成python函数就是

python 复制代码
def fE(x, y, z):
    return x*z+y

那么在mindspore中对这个函数进行自动微分,只需要使用grad函数即可:

python 复制代码
import mindspore as ms
from mindspore import ops, Tensor
gfE = ops.grad(fE)
x = Tensor([2.0], ms.float32)
y = Tensor([5.0], ms.float32)
print (gfE(x, y, Tensor([3.], ms.float32)))
# [3.]

从这个输出结果来说,我们发现默认的配置求得的导数为:\(\frac{\partial E}{\partial x}\)。假如说我们需要计算\(\frac{\partial E}{\partial y}\)和\(\frac{\partial E}{\partial z}\)的话,只需要在函数中配置一下求导位置即可:

python 复制代码
gfE = ops.grad(fE, grad_position=(2, ))
print (gfE(x, y, Tensor([3.], ms.float32)))
# [2.]
gfE = ops.grad(fE, grad_position=(0, 1, 2))
print (gfE(x, y, Tensor([3.], ms.float32)))
# (Tensor(shape=[1], dtype=Float32, value= [ 3.00000000e+00]), 
#  Tensor(shape=[1], dtype=Float32, value= [ 1.00000000e+00]), 
#  Tensor(shape=[1], dtype=Float32, value= [ 2.00000000e+00]))

如果得到的导数只有一项,那么返回的结果是一个Tensor。而如果是针对多个输入的求导,得到的结果是一个Tuple类型。

类求导

这里就抛开面向过程和面向对象的优劣,单纯讨论如何对MindSpore中的一个Cell类进行求导。MindSpore的Cell类的基础结构是这样的:

python 复制代码
class Net(nn.Cell):
    def __init__(self, z):
        super().__init__()
        self.z = z
    def construct(self, x, y):
        return x + y

但是在Cell类里面会多出来一个超参数的概念,也就是上面这个函数中的z。如果是从深度学习的角度来划分参数,可以大致的划分成参数和超参数,超参数大多数时候是初始化之后可以保持不变的内容,而参数是每一次迭代计算都要参与的。如果是从变量的角度来划分参数,则主要可以分为可变参数(Parameter)和不可变参数(Tensor)。之所以要这样划分,是因为在求导上的形式会有所差异。这里我们先把函数E写成Cell类的形式:

python 复制代码
import mindspore as ms
from mindspore import nn, Parameter, Tensor
class E(nn.Cell):
    def __init__(self):
        super(E, self).__init__()
        self.z = Parameter(Tensor([3.], ms.float32), requires_grad=True, name='z')
    def construct(self, x, y):
        return x*self.z+y

在这个示例中,我们把z作为一个可变参数,x和y作为不可变的输入参数。类似于函数求导的形式,我们也可以用grad函数直接对这个类的对象进行求导:

python 复制代码
gE = ops.grad(nt)
print (gE(x, y))
# [3.]
gE = ops.grad(nt, grad_position=(0, 1))
print (gE(x, y))
# (Tensor(shape=[1], dtype=Float32, value= [ 3.00000000e+00]), 
#  Tensor(shape=[1], dtype=Float32, value= [ 1.00000000e+00]))

但是这里就发现了一个问题,这种形式的求导只能对不可变的输入参数进行求导,而我们在初始化对象时用到的可变参数,不在这个求导函数所支持的范围内。不过不用担心,MindSpore内部也支持了一些可以对Cell类进行求导的类,例如GradOperation。这里先看一个GradOperation的普通使用方法:

python 复制代码
nt = E()
grad_net = ops.GradOperation(get_all=True)(nt)
print (grad_net(x, y))
# (Tensor(shape=[1], dtype=Float32, value= [ 3.00000000e+00]),
#  Tensor(shape=[1], dtype=Float32, value= [ 1.00000000e+00]))

我们配置一个get_all参数,表示对所有的不可变参量进行求导。如果不加任何配置,那么默认是只对第一个不可变参量进行求导:

python 复制代码
nt = E()
grad_net = ops.GradOperation()(nt)
print (grad_net(x, y))
# [3.]

但是不同于grad函数,GradOperation类支持get_by_list参数,这样我们就可以配置其中的一些可变参量作为求导对象:

python 复制代码
nt = E()
grad_net = ops.GradOperation(get_by_list=True)(nt, nt.z)
print (grad_net(x, y))
# [2.]

这里就表示计算了\(\frac{\partial E}{\partial z}\)的值。

总结概要

不同于符号微分和手动微分,基于链式法则的自动微分不仅有极高的速度,还不需要去手动推导微分,在深度学习领域有非常广泛的应用。本文主要通过几个案例,分别介绍了一下在MindSpore深度学习框架中,如何使用grad函数和GradOperation类,分别对函数和类进行自动微分计算。

版权声明

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

作者ID:DechinPhy

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

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