【MindSpore入门教程】 02 自动微分

深度学习等现代AI算法,自动微分技术(Automatic Differentiation,AD)是其中的关键技术。

自动微分是一种介于数值微分与符号微分之间的一种求导方法。自动微分的核心思想是将计算机程序中的运算操作分解为一个有限的基本操作合集,且合集中基本操作的求导规则均为已知的。在完成每一个基本操作的求导后,使用链式求导法则将结果组合得到整体程序的求导结果。
链式求导法则:
( f ∘ g ) ′ ( x ) = f ′ ( g ( x ) ) g ′ ( x ) (1) (f\circ g)^{'}(x)=f^{'}(g(x))g^{'}(x) \tag{1} (fg)(x)=f(g(x))g(x)(1)

自动微分实现的方式可以分为前项自动微分和反向自动微分,MindSpore的反向算子GradOperation实现的是反向自动微分。所以这里主要介绍一下反向自动微分。反向自动微分的求导方向与原函数的求值方向相反,微分结果需要依赖原函数的运行结果。

反向自动微分

我们将被求导的原函数,泛化为具有N输入与M输出的函数F:
( Y 1 , Y 2 , . . . , Y M ) = F ( X 1 , X 2 , . . . , X N ) (2) (Y_{1},Y_{2},...,Y_{M})=F(X_{1},X_{2},...,X_{N}) \tag{2} (Y1,Y2,...,YM)=F(X1,X2,...,XN)(2)

函数 F 的导数本身为一个雅可比矩阵(Jacobian matrix)。
[ ∂ Y 1 ∂ X 1 . . . ∂ Y 1 ∂ X N . . . . . . . . . ∂ Y M ∂ X 1 . . . ∂ Y M ∂ X N ] (3) \begin{split}\left[ \begin{matrix} \frac{\partial Y_{1}}{\partial X_{1}}& ... & \frac{\partial Y_{1}}{\partial X_{N}} \\ ... & ... & ... \\ \frac{\partial Y_{M}}{\partial X_{1}} & ... & \frac{\partial Y_{M}}{\partial X_{N}} \end{matrix} \right] \end{split}\tag{3} X1Y1...X1YM.........XNY1...XNYM (3)

在反向自动微分当中,我们是从输出开始向输入的方向计算的,因此每一次计算我们可以求得某一输出对输入的导数,即雅可比矩阵中的一行。
[ ∂ Y i ∂ X 1 . . . ∂ Y i ∂ X N ] (4) \begin{split}\left[ \begin{matrix} \frac{\partial Y_{i}}{\partial X_{1}}& ... & \frac{\partial Y_{i}}{\partial X_{N}} \\ \end{matrix} \right] \end{split}\tag{4} [X1Yi...XNYi](4)

为了求取该列的值 ∂ Y i ∂ X 1 \frac{\partial Y_{i}}{\partial X_{1}} X1Yi, 自动微分将程序分解为一系列求导规则已知的基本操作,这些基本操作也可以被泛化表达为具有n输入和m输出的函数 f
( y 1 , y 2 , . . . , y m ) = f ( x 1 , x 2 , . . . , x n ) (5) (y_{1},y_{2},...,y_{m})=f(x_{1},x_{2},...,x_{n}) \tag{5} (y1,y2,...,ym)=f(x1,x2,...,xn)(5)

由于我们的已知基础函数 f 求导规则,即其雅可比矩阵是已知的。于是我们可以对 f 计算向量雅可比积(Vjp, Vector-jacobian-product),并应用链式求导法则获得导数结果。
[ ∂ Y i ∂ x 1 . . . ∂ Y i ∂ x N ] = [ ∂ Y i ∂ y 1 . . . ∂ Y i ∂ y m ] [ ∂ y 1 ∂ x 1 . . . ∂ y 1 ∂ x n . . . . . . . . . ∂ y m ∂ x 1 . . . ∂ y m ∂ x n ] (6) \begin{split}\left[ \begin{matrix} \frac{\partial Y_{i}}{\partial x_{1}}& ... & \frac{\partial Y_{i}}{\partial x_{N}} \\ \end{matrix} \right]=\left[ \begin{matrix} \frac{\partial Y_{i}}{\partial y_{1}}& ... & \frac{\partial Y_{i}}{\partial y_{m}} \\ \end{matrix} \right]\left[ \begin{matrix} \frac{\partial y_{1}}{\partial x_{1}}& ... & \frac{\partial y_{1}}{\partial x_{n}} \\ ... & ... & ... \\ \frac{\partial y_{m}}{\partial x_{1}} & ... & \frac{\partial y_{m}}{\partial x_{n}} \end{matrix} \right] \end{split}\tag{6} [x1Yi...xNYi]=[y1Yi...ymYi] x1y1...x1ym.........xny1...xnym (6)
其中, ∂ Y i ∂ y j \frac{\partial Y_{i}}{\partial y_{j}} yjYi ∂ y j ∂ x k \frac{\partial y_{j}}{\partial x_{k}} xkyj 就是基本函数的求导。

GradOperation实现

单算子的反向

为简单说明一个算子的微分,举例正弦函数的微分,已知:
y = f ( x ) = s i n ( x ) y ′ = f ′ ( x ) = c o s ( x ) (7) y= f(x) = sin(x) \tag{7} \\ y'=f'(x) = cos(x) y=f(x)=sin(x)y=f(x)=cos(x)(7)

如下,即是在MindSpore中实现正弦函数的微分。
其中,ops.Cos()是余弦函数,x是输入变量,outsinx(x)计算的结果,即正向输出结果。dout表示的是链式法则中已经计算过得微分。很多基础算子的反向求导,是通过装饰器@bprop_getters.register(xxx)进行关联的。

import mindspore.ops as ops
from mindspore.ops._grad.grad_base import bprop_getters

@bprop_getters.register(ops.Sin)
def get_bprop_sin(self):
    """Grad definition for `Sin` operation."""
    cos = ops.Cos()
    def bprop(x, out, dout):
        dx = dout * cos(x)
        return (dx,)
    return bprop

有了基础算子的反向导数,即可以根据链式法则,组合符合算子的反向。

复合网络的反向

对于一个简单的网络
f ( x ) = c o s ( s i n ( x ) ) (8) f(x) = cos(sin(x)) \tag{8} f(x)=cos(sin(x))(8)
其对输入x进行求导
f ′ ( x ) = − s i n ( s i n ( x ) ) ∗ c o s ( x ) (9) f'(x) = -sin(sin(x)) * cos(x) \tag{9} f(x)=sin(sin(x))cos(x)(9)
首先可以使用MindSpore定义正向的网络f(x) :

from mindspore import nn, ops
from mindspore import Tensor
import numpy as np

class Net(nn.Cell):
    def __init__(self):
        super(Net, self).__init__()
        self.sin = ops.Sin()
        self.cos = ops.Cos()

    def construct(self, x):
        a = self.sin(x)
        out = self.cos(a)
        return out

def forward():
    f = Net()
    x = np.array([1.0, 2.0, 5.0], np.float32)
    y1 = np.cos(np.sin(x))
    x = Tensor(x)
    y2 = f(x)

    print("y1 = ", y1)
    print("y2 = ", y2)

if __name__ == "__main__":
    forward()

# output
#y1 =  [0.6663667  0.6143003  0.57440084]
#y2 =  [0.66636676 0.6143003  0.57440084]

前向计算结果一致。
MindSpore提供了GradOperation高阶函数,用于构造输入函数的反向微分:

from mindspore import nn, ops
from mindspore import Tensor
import numpy as np

class GradNet(nn.Cell):
	def __init__(self, net):
        super(GradNet, self).__init__()
        self.net = net
        self.grad_op = ops.GradOperation()
    def construct(self, x):
        gradient_function = self.grad_op(self.net)
        return gradient_function(x)

def backward():
    f = Net()
    gradf = GradNet(f)
    x = np.array([1.0, 2.0, 5.0], np.float32)
    y1 = -np.sin(np.sin(x)) * np.cos(x)
    x = Tensor(x)
    y2 = gradf(x)

    print("y1 = ", y1)
    print("y2 = ", y2)

if __name__ == "__main__":
    backward()

#output 
#y1 =  [-0.40286243  0.32836995  0.23219854]
#y2 =  [-0.4028624   0.32836998  0.23219854]

可以发现,首先使用GradOperation算子,计算出网络的微分函数gradient_function = self.grad_op(self.net),然后,使用微分函数进行反向计算,结果与预期结果一致。

GradOperation使用

GradOperation有三个输入参数get_all, get_by_list, sens_param,包含多种使用方式。其中,

  • get_all=True, 表示对网络的所有参数(required=True)进行求导。

  • get_by_list=True, 表示对网络中指定的参数进行求导,此时计算网络的微分函数,需要传入参数列表: gradient_function = self.grad_op(self.net, self.params).

  • sens_param=True, 表示配置(关于输出的梯度的)灵敏度,可以手动控制输出梯度值的大小。反向计算时需要向梯度函数传递一个灵敏度值grad_wrt_output。这个输入值必须与正向网络的输出具有相同的形状和类型。使用方式如 gradient_function(x, y, grad_wrt_output)

下面看几个例子。

1. 生成一个梯度函数,该函数返回第一个输入的梯度

from mindspore import Parameter
# f(x,y,z) = (x*z)*y
class Net(nn.Cell):
    def __init__(self):
        super(Net, self).__init__()
        self.matmul = ops.MatMul()
        self.z = Parameter(Tensor(np.array([1.0], np.float32)), name='z')
    def construct(self, x, y):
        x = x * self.z
        out = self.matmul(x, y)
        return out
# df/dx
class GradNetWrtX(nn.Cell):
    def __init__(self, net):
        super(GradNetWrtX, self).__init__()
        self.net = net
        self.grad_op = ops.GradOperation()
    def construct(self, x, y):
        gradient_function = self.grad_op(self.net)
        return gradient_function(x, y)

x = Tensor([[0.5, 0.6, 0.4], [1.2, 1.3, 1.1]], dtype=mstype.float32)
y = Tensor([[0.01, 0.3, 1.1], [0.1, 0.2, 1.3], [2.1, 1.2, 3.3]], dtype=mstype.float32)

grad = GradNetWrtX(Net())(x, y)
print(grad)

# out = (x*z)*y 关于x导数如下
ynp= y.asnumpy().T
a = np.ones((2,3))
out = np.matmul(a, ynp)
print(out)

#output
#[[1.4100001 1.5999999 6.6      ]
# [1.4100001 1.5999999 6.6      ]]
#[[1.41000004 1.59999996 6.5999999 ]
# [1.41000004 1.59999996 6.5999999 ]]

关于矩阵的求导,可以参考矩阵求导公式的数学推导

2. 生成一个梯度函数,该函数返回所有输入的梯度

# df/dx, df/dy
class GradNetWrtXY(nn.Cell):
    def __init__(self, net):
        super(GradNetWrtXY, self).__init__()
        self.net = net
        self.grad_op = ops.GradOperation(get_all=True)
    def construct(self, x, y):
        gradient_function = self.grad_op(self.net)
        return gradient_function(x, y)

x = Tensor([[0.5, 0.6, 0.4], [1.2, 1.3, 1.1]], dtype=mstype.float32)
y = Tensor([[0.01, 0.3, 1.1], [0.1, 0.2, 1.3], [2.1, 1.2, 3.3]], dtype=mstype.float32)
output = GradNetWrtXY(Net())(x, y)
print(output)

# out = (x*z)*y 关于x导数同上
# out = (x*z)*y 关于y导数如下
xnp = x.asnumpy().T
a = np.ones((2,3))
out = np.matmul(xnp, a)
print(out)

#output
#(Tensor(shape=[2, 3], dtype=Float32, value=
#[[1.41000009e+000, 1.59999990e+000, 6.59999990e+000],
# [1.41000009e+000, 1.59999990e+000, 6.59999990e+000]]), 
# Tensor(shape=[3, 3], dtype=Float32, value=
#[[1.70000005e+000, 1.70000005e+000, 1.70000005e+000],
# [1.89999998e+000, 1.89999998e+000, 1.89999998e+000],
# [1.50000000e+000, 1.50000000e+000, 1.50000000e+000]]))
#[[1.70000005 1.70000005 1.70000005]
# [1.89999998 1.89999998 1.89999998]
# [1.50000003 1.50000003 1.50000003]]

关于矩阵的求导,可以参考矩阵求导公式的数学推导

3. 生成一个梯度函数,该函数返回给定参数的梯度

  1. 构造一个带有 get_by_list=True 参数的高阶函数: grad_op = GradOperation(get_by_list=True)
  2. 创建一个 ParameterTuple net 作为GradOperation 的高阶函数的参数输入,ParameterTuple作为参数过滤器决定返回哪个梯度:params = ParameterTuple(net.trainingable_params())
  3. 使用高阶函数,将netparams 作为参数输入 GradOperation 的高阶函数,得到梯度函数: gradient_function = grad_op(net, params)
from mindspore import ParameterTuple
# df/dz
class GradNetWithWrtParams(nn.Cell):
    def __init__(self, net):
        super(GradNetWithWrtParams, self).__init__()
        self.net = net
        self.params = ParameterTuple(net.trainable_params())
        self.grad_op = ops.GradOperation(get_by_list=True)
    def construct(self, x, y):
        gradient_function = self.grad_op(self.net, self.params)
        return gradient_function(x, y)

x = Tensor([[0.5, 0.6, 0.4], [1.2, 1.3, 1.1]], dtype=mstype.float32)
y = Tensor([[0.01, 0.3, 1.1], [0.1, 0.2, 1.3], [2.1, 1.2, 3.3]], dtype=mstype.float32)
output = GradNetWithWrtParams(Net())(x, y)
print(output)

# out = (x*z)*y 关于z导数如下
#output
#(Tensor(shape=[1], dtype=Float32, value= [1.53369999e+001]),)

out = (x*z)*y 矩阵关于z的导数,不知道怎么计算的。有人知道的请多指教。

4. 生成一个梯度函数,该函数返回的梯度乘以一个灵敏度

  1. 构建一个带有 get_all=True, sens_param=True 参数的 GradOperation 高阶函数:grad_op = GradOperation(get_all=True, sens_param=True)
  2. sens_param=True ,定义 grad_wrt_output (关于输出的梯度):grad_wrt_output = Tensor([[0.5, 0.5, 0.5], [0.5, 0.5, 0.5]], dtype=mstype.float32)
  3. 用 net 作为参数输入 ,得到梯度函数:gradient_function = grad_op(net)
  4. net的输入和 sens_param 作为参数调用梯度函数,得到关于所有输入的梯度:gradient_function(x, y, grad_wrt_output)
import mindspore
# sens * df/dx, sens * df/dy, sens * df/dz
class GradNetWrtXYWithSensParam(nn.Cell):
    def __init__(self, net):
        super(GradNetWrtXYWithSensParam, self).__init__()
        self.net = net
        self.grad_op = ops.GradOperation(get_all=True, get_by_list=True, sens_param=True)
        self.params = ParameterTuple(net.trainable_params())
        self.grad_wrt_output = Tensor([[0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], dtype=mindspore.float32)
    def construct(self, x, y):
        gradient_function = self.grad_op(self.net, self.params)
        return gradient_function(x, y, self.grad_wrt_output)
  
x = Tensor([[0.5, 0.6, 0.4], [1.2, 1.3, 1.1]], dtype=mindspore.float32)
y = Tensor([[0.01, 0.3, 1.1], [0.1, 0.2, 1.3], [2.1, 1.2, 3.3]], dtype=mindspore.float32)

output = GradNetWrtXYWithSensParam(Net())(x, y)
print(output)

# out = (x*z)*y 关于y导数如下
xnp = x.asnumpy().T
a = np.ones((2,3))
out = np.matmul(xnp, a)
print(out)

#output
"""
((Tensor(shape=[2, 3], dtype=Float32, value=
[[1.41000003e-001, 1.59999996e-001, 6.59999967e-001],
 [1.41000003e-001, 1.59999996e-001, 6.59999967e-001]]), 
 Tensor(shape=[3, 3], dtype=Float32, value=
[[1.70000002e-001, 1.70000002e-001, 1.70000002e-001],
 [1.89999998e-001, 1.89999998e-001, 1.89999998e-001],
 [1.50000006e-001, 1.50000006e-001, 1.50000006e-001]])), 
 (Tensor(shape=[1], dtype=Float32, value= [1.53369999e+000]),))
[[1.70000005 1.70000005 1.70000005]
 [1.89999998 1.89999998 1.89999998]
 [1.50000003 1.50000003 1.50000003]]
"""

与上面反向计算结果对比,网络反向求解的关于输入x,y和参数z的导数,都乘上了0.1。 这就是灵敏度参数的作用,自定义调节反向梯度的大小。

特别地,在设置了sens_param=True时,一定要记住在gradient_function中输入一个灵敏度Tensor,且其shape大小要求与正向计算的结果保证一致。否则,可能有如下报错信息:

# 错误代码
return gradient_function(x, y)  # 缺少self.grad_wrt_output

# 报错信息
Traceback (most recent call last):
  File "gradoperation.py", line 75, in <module>
    output = GradNetWrtXYWithSensParam(Net())(x, y)
  File "mindspore\lib\site-packages\mindspore\nn\cell.py", line 578, in __call__
    out = self.compile_and_run(*args)
  File "mindspore\lib\site-packages\mindspore\nn\cell.py", line 965, in compile_and_run
    self.compile(*inputs)
  File "mindspore\lib\site-packages\mindspore\nn\cell.py", line 938, in compile
    jit_config_dict=self._jit_config_dict)
  File "mindspore\lib\site-packages\mindspore\common\api.py", line 1134, in compile
    result = self._graph_executor.compile(obj, args_list, phase, self._use_vm_mode())
TypeError: The parameters number of the function is 3, but the number of provided arguments is 2.
FunctionGraph ID : construct.25
NodeInfo: In file gradoperation.py(13)
    def construct(self, x, y):
    
# 错误代码
self.grad_wrt_output = Tensor([[0.1, 0.1], [0.1, 0.1]], dtype=mindspore.float32) # shape与正向输出不一致

# 错误信息
[ERROR] ANALYZER(169608,1,?):2022-8-7 19:43:42 [mindspore\ccsrc\pipeline\jit\static_analysis\async_eval_result.cc:66] HandleException] Exception happened, check the information as below.

The function call stack (See file 'rank_0\om/analyze_fail.dat' for more details. Get instructions about `analyze_fail.dat` at https://www.mindspore.cn/search?inputValue=analyze_fail.dat):
# 0 In file gradoperation.py(70)
        return gradient_function(x, y, self.grad_wrt_output)
               ^
# 1 In file mindspore\lib\site-packages\mindspore\ops\_grad\grad_math_ops.py(250)
        if ta:
# 2 In file mindspore\lib\site-packages\mindspore\ops\_grad\grad_math_ops.py(253)
            dx = mul1(dout, w)
                 ^

Traceback (most recent call last):
  File "gradoperation.py", line 75, in <module>
    output = GradNetWrtXYWithSensParam(Net())(x, y)
  File "mindspore\lib\site-packages\mindspore\nn\cell.py", line 578, in __call__
    out = self.compile_and_run(*args)
  File "mindspore\lib\site-packages\mindspore\nn\cell.py", line 965, in compile_and_run
    self.compile(*inputs)
  File "mindspore\lib\site-packages\mindspore\nn\cell.py", line 938, in compile
    jit_config_dict=self._jit_config_dict)
  File "mindspore\lib\site-packages\mindspore\common\api.py", line 1134, in compile
    result = self._graph_executor.compile(obj, args_list, phase, self._use_vm_mode())
  File "mindspore\lib\site-packages\mindspore\ops\primitive.py", line 467, in __check__
    fn(*(x[track] for x in args))
  File "mindspore\lib\site-packages\mindspore\ops\operations\math_ops.py", line 1436, in check_shape
    raise ValueError(f"For '{cls_name}', the input dimensions must be equal, but got 'x1_col': {x1_col} "
ValueError: For 'MatMul', the input dimensions must be equal, but got 'x1_col': 2 and 'x2_row': 3. And 'x' shape [2, 2](transpose_a=False), 'y' shape [3, 3](transpose_b=True).

至此,我们熟悉了MindSpore自动微分的设计原理,对使用方式也有了初步的了解。

参考资料

[1] mindspore.ops.GradOperation

[2] MindSpore 函数式微分编程

[3] Baydin, A.G. et al., 2018. Automatic differentiation in machine learning: A survey. arXiv.org. Available at: https://arxiv.org/abs/1502.05767

今年的华为开发者大会 HDC 2020 上,除了**昇腾、鲲鹏等自研芯片硬件平台**之外,最令人期待的就是**深度学习框架 MindSpore 的开源**了。今天上午,华为 MindSpore **首席科学家陈雷**在活动中宣布这款产品正式开源,我们终于可以在开放平台上一睹它的真面目。 本文是根据机器之心报道的MindSpore 的开源介绍而整理的.md笔记 作为一款支持**端、边、云独立/协同的统一训练和推理框架,华为希望通过这款完整的软件堆栈,实现**一次性算子开发、一致的开发和调试体验**,以此帮助开发者实现**一次开发,应用在所有设备上平滑迁移**的能力。 三大创新能力:新编程范式,执行模式和协作方式 由**自动微分自动并行、数据处理**等功能构成 开发算法即代码、运行高效、部署态灵活**的**特点**, 三层核心:从下往上分别是**后端运行时、计算图引擎及前端表示层**。 最大特点:采用了业界最新的 **Source-to-Source 自动微分**,它能**利用编译器及编程语言的底层技术**,进一步**优化以支持更好的微分表达**。主流深度学习框架中主要有**三种自动微分技术,才用的不是静态计算图、动态计算图,而是基于**源码**转换:该技术源以**函数式编程框架**为基础,以**即时编译(JIT)**的方式**在中间表达(编译过程中程序的表达形式)上做自动微分变换**,支持**复杂控制流场景、高阶函数和闭包**。 MindSpore 主要概念就是张量、算子、单元和模型 其代码有两个比较突出的亮点:计算图的调整,动态图与静态图可以一行代码切换;自动并行特性,我们写的串行代码,只需要多加一行就能完成自动并行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值