自动求导
mindspore.ops
模块提供的GradOperation
接口可以生成网络模型的梯度。本文主要介绍如何使用GradOperation
接口进行一阶、二阶求导,以及如何停止计算梯度。
更多求导接口相关信息可参考API文档。
一阶求导
计算一阶导数方法:mindspore.ops.GradOperation()
,其中参数使用方式为:
get_all
:为False
时,只会对第一个输入求导;为True
时,会对所有输入求导。get_by_list:
为False
时,不会对权重求导;为True
时,会对权重求导。sens_param
:对网络的输出值做缩放以改变最终梯度,故其维度与输出维度保持一致;
下面我们先使用MatMul算子构建自定义网络模型Net
,再对其进行一阶求导,通过这样一个例子对GradOperation
接口的使用方式做简单介绍,即公式:
首先我们要定义网络模型Net
、输入x
和输入y
:
[1]:
import numpy as np
import mindspore.nn as nn
import mindspore.ops as ops
from mindspore import Tensor
from mindspore import ParameterTuple, Parameter
from mindspore import dtype as mstype
# 定义输入x和y
x = Tensor([[0.8, 0.6, 0.2], [1.8, 1.3, 1.1]], dtype=mstype.float32)
y = Tensor([[0.11, 3.3, 1.1], [1.1, 0.2, 1.4], [1.1, 2.2, 0.3]], dtype=mstype.float32)
class Net(nn.Cell):
"""定义矩阵相乘网络Net"""
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
对输入进行求导
对输入值进行求导,代码如下:
[2]:
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)
output = GradNetWrtX(Net())(x, y)
print(output)
[[4.5099998 2.7 3.6000001]
[4.5099998 2.7 3.6000001]]
接下来我们对上面的结果做一个解释。为便于分析,我们把上面的输入x
、y
以及权重z
表示成如下形式:
x = Tensor([[x1, x2, x3], [x4, x5, x6]])
y = Tensor([[y1, y2, y3], [y4, y5, y6], [y7, y8, y9]])
z = Tensor([z])
根据MatMul算子定义可得前向结果:
梯度计算时由于MindSpore采用的是Reverse自动微分机制,会对输出结果求和后再对输入x
求导:
求和公式:
求导公式:
计算结果:
若考虑对
x
、y
输入求导,只需在GradNetWrtX
中设置self.grad_op = GradOperation(get_all=True)
。
对权重进行求导
对权重进行求导,示例代码如下:
[3]:
class GradNetWrtZ(nn.Cell):
"""定义网络权重的一阶求导"""
def __init__(self, net):
super(GradNetWrtZ, 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)
output = GradNetWrtZ(Net())(x, y)
print(output[0])
[21.536]
下面我们通过公式对上面的结果做一个解释。对权重的求导公式为:
计算结果:
梯度值缩放
可以通过sens_param
参数控制梯度值的缩放:
[4]:
class GradNetWrtN(nn.Cell):
"""定义网络的一阶求导,控制梯度值缩放"""
def __init__(self, net):
super(GradNetWrtN, self).__init__()
self.net = net
self.grad_op = ops.GradOperation(sens_param=True)
# 定义梯度值缩放
self.grad_wrt_output = Tensor([[0.1, 0.6, 0.2], [0.8, 1.3, 1.1]], dtype=mstype.float32)
def construct(self, x, y):
gradient_function = self.grad_op(self.net)
return gradient_function(x, y, self.grad_wrt_output)
output = GradNetWrtN(Net())(x, y)
print(output)
[[2.211 0.51 1.49 ]
[5.588 2.68 4.07 ]]
为了方便对上面的结果进行解释,我们把self.grad_wrt_output
记作如下形式:
self.grad_wrt_output = Tensor([[s1, s2, s3], [s4, s5, s6]])
缩放后的输出值为原输出值与self.grad_wrt_output
对应元素的乘积,公式为:
求导公式变为输出值总和对x
的每个元素求导:
计算结果:
停止计算梯度
我们可以使用stop_gradient
来停止计算指定算子的梯度,从而消除该算子对梯度的影响。
在上面一阶求导使用的矩阵相乘网络模型的基础上,我们再增加一个算子out2
并禁止计算其梯度,得到自定义网络Net2
,然后看一下对输入的求导结果情况。
示例代码如下:
[5]:
class Net(nn.Cell):
def __init__(self):
super(Net, self).__init__()
self.matmul = ops.MatMul()
def construct(self, x, y):
out1 = self.matmul(x, y)
out2 = self.matmul(x, y)
out2 = ops.stop_gradient(out2) # 停止计算out2算子的梯度
out = out1 + out2
return out
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)
output = GradNetWrtX(Net())(x, y)
print(output)
[[4.5099998 2.7 3.6000001]
[4.5099998 2.7 3.6000001]]
从上面的打印可以看出,由于对out2
设置了stop_gradient
, 所以out2
没有对梯度计算有任何的贡献,其输出结果与未加out2
算子时一致。
下面我们删除out2 = stop_gradient(out2)
,再来看一下输出结果。示例代码为:
[6]:
class Net(nn.Cell):
def __init__(self):
super(Net, self).__init__()
self.matmul = ops.MatMul()
def construct(self, x, y):
out1 = self.matmul(x, y)
out2 = self.matmul(x, y)
# out2 = stop_gradient(out2)
out = out1 + out2
return out
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)
output = GradNetWrtX(Net())(x, y)
print(output)
[[9.0199995 5.4 7.2000003]
[9.0199995 5.4 7.2000003]]
打印结果可以看出,在我们把out2
算子的梯度也计算进去之后,由于out2
和out1
算子完全相同,因此它们产生的梯度也完全相同,所以我们可以看到,结果中每一项的值都变为了原来的两倍(存在精度误差)。
高阶求导
高阶微分在AI支持科学计算、二阶优化等领域均有应用。如分子动力学模拟中,利用神经网络训练势能时,损失函数中需计算神经网络输出对输入的导数,则反向传播便存在损失函数对输入、权重的二阶交叉导数。
此外,AI求解微分方程(如PINNs方法)还会存在输出对输入的二阶导数。又如二阶优化中,为了能够让神经网络快速收敛,牛顿法等需计算损失函数对权重的二阶导数。
MindSpore可通过多次求导的方式支持高阶导数,下面通过几类例子展开阐述。
单输入单输出高阶导数
例如Sin算子,其公式为:
其一阶导数是:
其二阶导数为:
其二阶导数(-Sin)实现如下:
[9]:
import numpy as np
import mindspore.nn as nn
import mindspore.ops as ops
from mindspore import Tensor
class Net(nn.Cell):
"""定义基于Sin算子的网络模型"""
def __init__(self):
super(Net, self).__init__()
self.sin = ops.Sin()
def construct(self, x):
out = self.sin(x)
return out
class Grad(nn.Cell):
"""一阶求导"""
def __init__(self, network):
super(Grad, self).__init__()
self.grad = ops.GradOperation()
self.network = network
def construct(self, x):
gout = self.grad(self.network)(x)
return gout
class GradSec(nn.Cell):
"""二阶求导"""
def __init__(self, network):
super(GradSec, self).__init__()
self.grad = ops.GradOperation()
self.network = network
def construct(self, x):
gout = self.grad(self.network)(x)
return gout
x_train = Tensor(np.array([3.1415926]), dtype=mstype.float32)
net = Net()
firstgrad = Grad(net)
secondgrad = GradSec(firstgrad)
output = secondgrad(x_train)
# 打印结果
result = np.around(output.asnumpy(), decimals=2)
print(result)
[-0.]
从上面的打印结果可以看出,-sin(3.1415926)
的值接近于0
。
由于不同计算平台的精度可能存在差异,因此本章节中的代码在不同平台上的执行结果会存在微小的差别。