220 Star 932 Fork 687

GVPMindSpore/mindscience

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
.gitee
.jenkins
MindChemistry
MindEarth
MindElec
MindFlow
applications
cmake
docs
features
solve_pinns_by_mindflow
mindspore_grad_cookbook.ipynb
mindflow
CMakeLists.txt
CONTRIBUTION_CN.md
LICENSE
NOTICE
README.md
README_EN.md
RELEASE.md
RELEASE_CN.md
build.sh
requirements.txt
setup.py
version.txt
MindSPONGE
SciAI
docs
tests
RELEASE.md
.gitignore
CONTRIBUTION.md
LICENSE
NOTICE
OWNERS
README.md
README_CN.md
models.md
models_en.md
requirements.txt
version.txt
克隆/下载
mindspore_grad_cookbook.ipynb 28.71 KB
一键复制 编辑 原始数据 按行查看 历史
czrz 提交于 2年前 . fix typos in autodiff cookbook

MindSpore自动微分快速教程

MindSpore拥有完善的自动微分系统。本文将会借着对自动微分思想的介绍来展示MindSpore自动微分的各项能力,方便读者运用在自己的项目中。

import mindspore as ms
import numpy as np
import mindspore.ops as ops
from mindspore import context
from mindspore import Tensor
from mindspore import grad
from mindspore import dtype as mstype

context.set_context(mode=ms.GRAPH_MODE)
np.random.seed(0)
grad_tanh = grad(ops.tanh)
print(grad_tanh(Tensor(2, mstype.float32)))
0.070650816

grad 的入参为一个函数,返回的是求导后的函数。定义一个Python函数f用来计算数学函数fgrad(f) 就是表达f的Python函数。 grad(f)(x) 就是f(x)的值。

由于 grad 作用在函数上,所以grad也可以用来处理它自己的输出:

print(grad(grad(ops.tanh))(Tensor(2, mstype.float32)))
print(grad(grad(grad(ops.tanh)))(Tensor(2, mstype.float32)))
-0.13621867
0.25265405

下面是一个计算线性回归模型的梯度的例子,首先:

def sigmoid(x):
    return 0.5 * (ops.tanh(x / 2) + 1)

# Outputs probability of a label being true.


def predict(W, b, inputs):
    return sigmoid(ops.inner(inputs, W) + b)


# Build a toy dataset.
inputs = Tensor(np.array([[0.52, 1.12, 0.77],
                          [0.88, -1.08, 0.15],
                          [0.52, 0.06, -1.30],
                          [0.74, -2.49, 1.39]]), ms.float32)
targets = Tensor(np.array([True, True, False, True]))

# Training loss is the negative log-likelihood of the training examples.


def loss(W, b):
    preds = predict(W, b, inputs)
    label_probs = preds * targets + (1 - preds) * (1 - targets)
    return -ops.sum(ops.log(label_probs))


# Initialize random model coefficients
W = Tensor(np.random.rand(3,), ms.float32)
b = Tensor(np.random.rand(), ms.float32)

grad 中使用 grad_position对指定的位置参数进行微分

# Differentiate `loss` with respect to the first positional argument:
W_grad = grad(loss, grad_position=0)(W, b)
print('W_grad', W_grad)

# Since argnums=0 is the default, this does the same thing:
W_grad = grad(loss)(W, b)
print('W_grad', W_grad)

# But we can choose different values too, and drop the keyword:
b_grad = grad(loss, 1)(W, b)
print('b_grad', b_grad)

# Including tuple values
W_grad, b_grad = grad(loss, (0, 1))(W, b)
print('W_grad', W_grad)
print('b_grad', b_grad)
W_grad [-0.5185027  1.5961987 -1.5178145]
W_grad [-0.5185027  1.5961987 -1.5178145]
b_grad -0.49954596
W_grad [-0.5185027  1.5961987 -1.5178145]
b_grad -0.49954596

本质上来说,使用grad_position时,如果f是一个Python函数,那么表达式grad(f, i)就是在求偏微分if.

value_and_grad:同时获得函数值与梯度

value_and_grad可以方便地同时计算函数值和梯度值:

from mindspore import value_and_grad
loss_value, Wb_grad = value_and_grad(loss, (0, 1))(W, b)
print('loss value', loss_value)
print('loss value', loss(W, b))
loss value 2.0792074
loss value 2.0792074

与数值计算结果比较

自动微分可以很直接地与有限微分比较:

# Set a step size for finite differences calculations
eps = 1e-4

# Check b_grad with scalar finite differences
b_grad_numerical = (loss(W, b + eps / 2.) - loss(W, b - eps / 2.)) / eps
print('b_grad_numerical', b_grad_numerical)
print('b_grad_autodiff', grad(loss, 1)(W, b))

# Check W_grad with finite differences in a random direction
# key, subkey = random.split(key)
vec = Tensor(np.random.normal(size=W.shape), mstype.float32)
unitvec = vec / ops.sqrt(ops.inner(vec, vec))
unitvec = unitvec.reshape(W.shape)
W_grad_numerical = (loss(W + eps / 2. * unitvec, b) -
                    loss(W - eps / 2. * unitvec, b)) / eps
print('W_dirderiv_numerical', W_grad_numerical)
print('W_dirderiv_autodiff', ops.inner(grad(loss)(W, b), unitvec))
b_grad_numerical -0.500679
b_grad_autodiff -0.49954596
W_dirderiv_numerical -1.7213821
W_dirderiv_autodiff -1.71724

grad+grad得到Hessian向量积

使用高阶grad可以构造Hessian向量积。(后面我们会用前向模式和反向模式写一个更高效的实现)

Hessian向量积可用来在截断的牛顿共轭梯度法中最小化一个光滑的凸函数,或者用来判断神经网络训练目标的曲率性质。(如 1, 2, 3, 4).

对于一个有着连续二阶导的标量函数(这种函数的Hessian矩阵是对称的)f:RnR,点xRn处的Hessian算子为2f(x)。一个Hessian向量积用来计算映射:

v2f(x)v

其中 vRn

有一个技巧是我们不能实例化整个Hessian矩阵:如果n很大的话(神经网络中可能达到百万或上亿的量级),完整的Hessian矩阵是没法存储的。

幸运的是, grad 提供了一种高效计算Hessian向量积的方式。我们只需要有恒等式:

2f(x)v=[xf(x)v]=g(x)

其中 g(x)=f(x)v 是一个新的标量函数,其表示 fx的梯度与向量v的点乘。这里只涉及对标量函数的向量值的微分,这种情形下 grad 是高效的。

用MindSpore代码,我们可以写出:

def hvp(f, x, v):
    return grad(lambda x: ops.inner(grad(f)(x), v))(x)

这个例子表明我们可以自由的使用词汇闭包,MindSpore都可以正确处理。在后面我会看到Hessian矩阵是怎么被计算出来的,知晓了原理之后我们会同时运用前向模式和反向模式提供一个更高效的写法。

运用 jacfwdjacrev 计算Jacobians 和 Hessians 矩阵

用户可以用 jacfwdjacrev计算Jacobian矩阵:

from mindspore import jacfwd, jacrev

# Isolate the function from the weight matrix to the predictions


def f(W):
    return predict(W, b, inputs)


J = jacfwd(f)(W)
print("jacfwd result, with shape", J.shape)
print(J)

J = jacrev(f)(W)
print("jacrev result, with shape", J.shape)
print(J)
jacfwd result, with shape (4, 3)
[[ 0.05072299  0.10924952  0.07510904]
 [ 0.21355031 -0.26208448  0.03640062]
 [ 0.12973952  0.01496994 -0.3243488 ]
 [ 0.18499702 -0.62249     0.3474944 ]]
jacrev result, with shape (4, 3)
[[ 0.05072299  0.10924952  0.07510904]
 [ 0.21355031 -0.26208448  0.03640062]
 [ 0.12973952  0.01496994 -0.3243488 ]
 [ 0.18499702 -0.62249     0.3474944 ]]

这两个函数得到的结果应该是一样的,二者只是实现方式不通: jacfwd 使用的是前向模式的自动微分,在比较"高"的Jacobian矩阵上较高效。 jacrev 使用的是反向模式,在"宽"的矩阵上更高效。对于比较方正的矩阵, jacfwd 效果稍好于jacrev

关于前向模式和反向模式的更多信息,请继续阅读!

使用一种组合的方式计算dense的Hessian矩阵:

def hessian(f):
    return jacfwd(jacrev(f))


H = hessian(f)(W)
print("hessian, with shape", H.shape)
print(H)
hessian, with shape (4, 3, 3)
[[[-2.0597292e-02 -4.4363402e-02 -3.0499836e-02]
  [-4.4363398e-02 -9.5551945e-02 -6.5691955e-02]
  [-3.0499836e-02 -6.5691963e-02 -4.5163218e-02]]

 [[-3.2176636e-02  3.9489504e-02 -5.4846536e-03]
  [ 3.9489508e-02 -4.8464395e-02  6.7311660e-03]
  [-5.4846536e-03  6.7311660e-03 -9.3488418e-04]]

 [[-3.0198938e-03 -3.4844928e-04  7.5497343e-03]
  [-3.4844928e-04 -4.0205687e-05  8.7112316e-04]
  [ 7.5497343e-03  8.7112322e-04 -1.8874336e-02]]

 [[-5.4928247e-04  1.8482616e-03 -1.0317604e-03]
  [ 1.8482613e-03 -6.2191500e-03  3.4717342e-03]
  [-1.0317604e-03  3.4717345e-03 -1.9380364e-03]]]

这里的shape是合理的:f:RnRm, 在点 xRn 上,会有shape

  • f(x)Rm, fx 处的值,
  • f(x)Rm×n, x 处的Jacobian矩阵,
  • 2f(x)Rm×n×n, x 处的Hessian矩阵

jacfwd(jacrev(f))jacrev(jacfwd(f)) 或者二者任意的组合皆可实现一个hessian矩阵,只是 forward+reverse一般情况下是效率最高的方式。 这是因为里面一层的Jacobian计算经常会有针对宽Jacobian矩阵的微分(比如loss function f:RnR),在外面那一层的Jacobian 计算 通常是微分一个正方矩阵(因为会有f:RnRn),这时forward-mode速度更快。

深入理解两个基本的自动微分函数

Jacobian向量积 (JVPs, 前向模式自动微分)

MindSpore对前向和反向的自动微分都提供了高效且泛用性强的实现。我们熟悉的 grad 是基于反向模式实现的,不过为了理解二者的区别,我们需要一点数学背景。

JVPs的数学背景

从数学的角度看,给定一个函数 f:RnRmf 在输入点 xRn 的Jacobian矩阵可被记作 f(x),通常型如 Rm×Rn:

f(x)Rm×n.

我们可以将 f(x) 视为线性映射,把在点 xf 定义域上的正切空间( 其实就是 Rn 的一份拷贝)映射到了在点 f(x)f 陪域上的正切空间(Rm 的拷贝)。

f(x):RnRm.

这个映射又被称作 fx前推映射。Jacobian矩阵只是这个线性映射在标准情况下的矩阵形式。

如果我们不拘泥于一个特定的点 x,那么函数 f 可被视为先取一个输入点然后返回那个点上的Jacobian线性映射:

f:RnRnRm.

尤其是,做反curring时,给定输入 xRn 和切向量 vRn,返回一个输出切向量 Rm。我们把从 (x,v) 到输出切向量的映射称之为 Jacobian向量积,写作:

(x,v)f(x)v

MindSpore中的JVP

回到Python代码上,MindSpore的 jvp 函数模拟了上述转换。 给定一个Python函数 f, MindSpore的 jvp 可以得到一个表达 (x,v)(f(x),f(x)v) 的函数

from mindspore import jvp

# Isolate the function from the weight matrix to the predictions


def f(W):
    return predict(W, b, inputs)


v = Tensor(np.random.normal(size=W.shape), mstype.float32)
# Push forward the vector `v` along `f` evaluated at `W`
y, u = jvp(f, (W), (v))
print(y, u)
[0.89045584 0.5856106  0.52238137 0.5020062 ] [ 0.01188576  0.00967572 -0.15435933  0.17893277]

按照Haskell类型风格, 有:

jvp :: (a -> b) -> a -> T a -> (b, T b)

在这里,我们用 T a 表示 a 切空间的类型。简而言之, jvp 的参数有 a -> b类型函数,、 a 类型的值和T a切向量。返回的是b类型的值和T b 类型的切向量。

jvp的计算方式与原始函数很相似,但它与每个a类型的原始值配对,并推送T a类型的切线值。对于每个原始函数会应用的原始数值操作,jvp转换后的函数为该原始函数执行一个 "JVP规则",既对原始值进行评估,又在这些原始值上应用原始的JVP。

这种计算策略对计算的复杂度有直接的影响:因为在计算JVP的过程中不用存储任何东西,所以空间开销和计算的深度完全无关。除此之外, jvp 转换过的函数FLOP开销约是原函数的3倍 (一份来自原函数的计算,比如 sin(x); 一份来自线性化,如 cos(x);还有一份来自于将线性化函数施加在向量上,如 cos_x * v)。 换句话说,对于固定的点 x,我们计算 vf(x)v 和计算 f的边际成本是相近的。

这里的空间复杂度看起来很有说服力,但我们在机器学习中并不经常见到前向模式。

为了回答这个问题,首先假设要用JVP构建一个完整的Jacobian矩阵。如果我们是对一个one-hot切向量用了JVP,结果反映的是Jacobian矩阵的一列,对应填入的非零项。所以我们是可以通过一次构建一列的方式构建一个完整的Jacobian矩阵的,而且每一列的开销和一次函数计算差不多。这就意味这对于"高"的Jaocbian矩阵来说比较合算,但对于"宽"的就较为低效。

如果在机器学习中做基于梯度的优化,你可能想要最小化损失函数,这个损失函数以 Rn 为参数,返回一个标量值R。 这就意味着该函数的Jacobian矩阵会很宽了:f(x)R1×n,一般我们会认为和梯度向量 f(x)Rn 一样。一次一列地构建这个矩阵,而且每列的FLOP和原函数计算一次的开销差不多,这个开销当然是不小的。尤其是,对于训练神经网络来说,损失函数 fn 可以达到上亿的量级,这就更暴露出前向模式的问题了。

为了解决这种问题,就需要反向模式了。

向量Jacobian 积 (VJP, 反向模式自动微分)

和前向模式的一次一列的方式不同,反向模式的构造方式是一次一行。

VJPs 的数学背景

首先考虑有 f:RnRm。 其VJP表达为:

(x,v)vf(x),

其中 vfx 的余切空间(Rm 的同构)。严谨来说,v 是线性映射 v:RmRvf(x) 指的是复合函数 vf(x),在 f(x):RnRm 时成立。 不过通常 v 都可以视为 Rm 中的向量,这两个写法基本可以互换。

有了这些说明后,我们把VJP的线性部分视为JVP线性部分的转置(或伴随、共轭):

(x,v)f(x)Tv.

对点 x,有:

f(x)T:RmRn.

对余切空间的映射通常称为 fx拉回。理解的关键在于拉回会从形似输出 f 的形式得到形似输入 f 的形式,就像线性函数转置一样。

MindSpore中使用VJP

MindSpore vjp 以一个python函数 f 为输入,返回表示 VJP (x,v)(f(x),vTf(x)) 的函数。

from mindspore import vjp

# Isolate the function from the weight matrix to the predictions


def f(W):
    return predict(W, b, inputs)


y, vjp_fun = vjp(f, W)

u = Tensor(np.random.normal(size=y.shape), mstype.float32)

# Pull back the covector `u` along `f` evaluated at `W`
v = vjp_fun(u)
[ 0.6064372  -1.1690241   0.32237193]

仿照 Haskell-like type signatures, 有

vjp :: (a -> b) -> a -> (b, CT b -> CT a)

其中,我们用CT a来表示a的余切空间的类型。换句话说,vjp将一个a -> b类型的函数和一个a类型的点作为参数,并返回一个由b类型的值和CT b -> CT a类型的线性映射组成的对。

VJP一个优良的性质在于VJP是按行构建Jacobian矩阵, (x,v)(f(x),vTf(x)) 的FLOP仅为计算 f 的三倍左右。而且计算 f:RnR 的梯度,我们只需要一次VJP就够了。这就是为什么 grad 在大的神经网络中做梯度优化依然高效。

不过还有一点需要考虑一下: 尽管 FLOP开销不高,VJP的空间复杂度是随计算深度上升而上升的。而且实现上通常比前向模式复杂。

反向模式的更多说明请参阅 this tutorial video from the Deep Learning Summer School in 2017.

VJP计算梯度向量

可以用VJP得到梯度向量:

from mindspore import vjp

context.set_context(mode=ms.PYNATIVE_MODE)


def vgrad(f, x):
    y, vjp_fn = vjp(f, x)
    return vjp_fn(ops.ones(y.shape))[0]


print(vgrad(lambda x: 3*x**2, ops.ones((2, 2))))
[[6. 6.]
 [6. 6.]]

用前向和反向模式得到Hessian向量积

仅用反向模式得到Hessian向量积的实现:

def hvp(f, x, v):
    return grad(lambda x: ops.inner(grad(f)(x), v))(x)

通过组合使用前反向的方法我们可以得到更高效的实现。

设有待微分函数 f:RnR , 在点 xRn 线性化函数,并有向量 vRn。 Hessian向量积函数为:

(x,v)2f(x)v

构造helper function g:RnRn,定义为 f 的导数(或梯度), 即 g(x)=f(x)。使用一次JVP,便得到:

(x,v)g(x)v=2f(x)v

用代码写作:

from mindspore import jvp, grad

# forward-over-reverse


def hvp(f, primals, tangents):
    return jvp(grad(f), primals, tangents)[1]

在这里我们不需要 ops.inner,该 hvp 函数对任何shape的数组都成立。

以下是该函数的一个样例:

def f(X):
    return ops.sum(ops.tanh(X)**2)


X = Tensor(np.random.normal(size=(30, 40)), mstype.float32)
V = Tensor(np.random.normal(size=(30, 40)), mstype.float32)

ans1 = hvp(f, (X), (V))
ans2 = ms.numpy.tensordot(hessian(f)(X), V, 2)

print(np.allclose(ans1.numpy(), ans2.numpy(), 1e-4, 1e-4))
True

你也可以考虑写一种先前向后反向的方式:

# reverse-over-forward
def hvp_revfwd(f, primals, tangents):
    def g(primals):
        return jvp(f, primals, tangents)[1]
    return grad(g)(primals)

不过这就不是很高效了,因为前向模式比反向模式的开销低一些,而且由于外层微分算子计算量比内层的要大,继续在外层用前向模式反而更好:

# reverse-over-reverse, only works for single arguments
context.set_context(mode=ms.PYNATIVE_MODE)


def hvp_revrev(f, primals, tangents):
    x = primals
    v = tangents
    return grad(lambda x: ops.inner(grad(f)(x), v))(x)


print("Forward over reverse")
%timeit - n10 - r3 hvp(f, (X), (V))
print("Reverse over forward")
%timeit - n10 - r3 hvp_revfwd(f, (X), (V))
print("Reverse over reverse")
%timeit - n10 - r3 hvp_revrev(f, (X), (V))
print("Naive full Hessian materialization")
%timeit - n10 - r3 ms.numpy.tensordot(hessian(f)(X), V, 2)
Forward over reverse
297 ms ± 9.5 ms per loop (mean ± std. dev. of 3 runs, 10 loops each)
Reverse over forward
2.48 ms ± 257 µs per loop (mean ± std. dev. of 3 runs, 10 loops each)
Reverse over reverse
4.44 ms ± 51.9 µs per loop (mean ± std. dev. of 3 runs, 10 loops each)
Naive full Hessian materialization
1.23 s ± 13.6 ms per loop (mean ± std. dev. of 3 runs, 10 loops each)
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/mindspore/mindscience.git
git@gitee.com:mindspore/mindscience.git
mindspore
mindscience
mindscience
master

搜索帮助