梯度求导

下载Notebook 下载样例代码 查看源文件

自动微分接口

正向网络构建完成之后,MindSpore提供了自动微分的接口用以计算模型的梯度结果。 在自动求导的教程中,对各种梯度计算的场景做了一些介绍。

MindSpore求梯度的接口目前有三种:

mindspore.grad

mindspore.grad这个API有四个可以配置的参数:

  • fn (Union[Cell, Function]) - 待求导的函数或网络(Cell)。

  • grad_position (Union[NoneType, int, tuple[int]]) - 指定求导输入位置的索引,默认值:0。

  • weights (Union[ParameterTuple, Parameter, list[Parameter]]) - 训练网络中需要返回梯度的网络参数,默认值:None。

  • has_aux (bool) - 是否返回辅助参数的标志。若为True, fn 输出数量必须超过一个,其中只有 fn 第一个输出参与求导,其他输出值将直接返回。默认值:False。

其中grad_positionweights共同决定要输出哪些值的梯度,has_aux在有多个输出时配置对第一个输入求梯度还是全部输出求梯度。

grad_position

weights

output

0

None

第一个输入的梯度

1

None

第二个输入的梯度

(0, 1)

None

(第一个输入的梯度, 第二个输入的梯度)

None

weights

(weights的梯度)

0

weights

(第一个输入的梯度), (weights的梯度)

(0, 1)

weights

(第一个输入的梯度, 第二个输入的梯度), (weights的梯度)

None

None

报错

下面实际运行一个示例,看下具体是怎么用的。

首先,构造一个带参数的网络,这个网络有两个输出loss和logits,其中loss是我们用于求梯度的输出。

[1]:
import mindspore as ms
from mindspore import nn

class Net(nn.Cell):
    def __init__(self, in_channel, out_channel):
        super(Net, self).__init__()
        self.fc = nn.Dense(in_channel, out_channel, has_bias=False)
        self.loss = nn.MSELoss()

    def construct(self, x, y):
        logits = self.fc(x).squeeze()
        loss = self.loss(logits, y)
        return loss, logits


net = Net(3, 1)
net.fc.weight.set_data(ms.Tensor([[2, 3, 4]], ms.float32))   # 给全连接的weight设置固定值

print("=== weight ===")
for param in net.trainable_params():
    print("name:", param.name, "data:", param.data.asnumpy())
x = ms.Tensor([[1, 2, 3]], ms.float32)
y = ms.Tensor(19, ms.float32)

loss, logits = net(x, y)
print("=== output ===")
print(loss, logits)
=== weight ===
name: fc.weight data: [[2. 3. 4.]]
=== output ===
1.0 20.0
[2]:
# 对第一个输入求梯度

print("=== grads 1 ===")
grad_func = ms.grad(net, grad_position=0, weights=None, has_aux=True)
grad, logit = grad_func(x, y)
print("grad", grad)
print("logit", logit)
=== grads 1 ===
grad [[4. 6. 8.]]
logit (Tensor(shape=[], dtype=Float32, value= 20),)
[3]:
# 对第二个输入求梯度

print("=== grads 2 ===")
grad_func = ms.grad(net, grad_position=1, weights=None, has_aux=True)
grad, logit = grad_func(x, y)
print("grad", grad)
print("logit", logit)
=== grads 2 ===
grad -2.0
logit (Tensor(shape=[], dtype=Float32, value= 20),)
[4]:
# 对多个输入求梯度

print("=== grads 3 ===")
grad_func = ms.grad(net, grad_position=(0, 1), weights=None, has_aux=True)
grad, logit = grad_func(x, y)
print("grad", grad)
print("logit", logit)
=== grads 3 ===
grad (Tensor(shape=[1, 3], dtype=Float32, value=
[[4.00000000e+000, 6.00000000e+000, 8.00000000e+000]]), Tensor(shape=[], dtype=Float32, value= -2))
logit (Tensor(shape=[], dtype=Float32, value= 20),)
[5]:
# 对weights求梯度

print("=== grads 4 ===")
grad_func = ms.grad(net, grad_position=None, weights=net.trainable_params(), has_aux=True)
grad, logit = grad_func(x, y)
print("grad", grad)
print("logits", logit)
=== grads 4 ===
grad (Tensor(shape=[1, 3], dtype=Float32, value=
[[2.00000000e+000, 4.00000000e+000, 6.00000000e+000]]),)
logits (Tensor(shape=[], dtype=Float32, value= 20),)
[6]:
# 对第一个输入和weights求梯度

print("=== grads 5 ===")
grad_func = ms.grad(net, grad_position=0, weights=net.trainable_params(), has_aux=True)
grad, logit = grad_func(x, y)
print("grad", grad)
print("logit", logit)
=== grads 5 ===
grad (Tensor(shape=[1, 3], dtype=Float32, value=
[[4.00000000e+000, 6.00000000e+000, 8.00000000e+000]]), (Tensor(shape=[1, 3], dtype=Float32, value=
[[2.00000000e+000, 4.00000000e+000, 6.00000000e+000]]),))
logit (Tensor(shape=[], dtype=Float32, value= 20),)
[7]:
# 对多个输入和weights求梯度

print("=== grads 6 ===")
grad_func = ms.grad(net, grad_position=(0, 1), weights=net.trainable_params(), has_aux=True)
grad, logit = grad_func(x, y)
print("grad", grad)
print("logit", logit)
=== grads 6 ===
grad ((Tensor(shape=[1, 3], dtype=Float32, value=
[[4.00000000e+000, 6.00000000e+000, 8.00000000e+000]]), Tensor(shape=[], dtype=Float32, value= -2)), (Tensor(shape=[1, 3], dtype=Float32, value=
[[2.00000000e+000, 4.00000000e+000, 6.00000000e+000]]),))
logit (Tensor(shape=[], dtype=Float32, value= 20),)
[8]:
# has_aux=False的场景

print("=== grads 7 ===")
grad_func = ms.grad(net, grad_position=0, weights=None, has_aux=False)
grad = grad_func(x, y)  # 只有一个输出
print("grad", grad)
=== grads 7 ===
grad [[ 6.  9. 12.]]

has_aux=False的场景实际上等价于两个输出相加作为求梯度的输出:

[9]:
class Net2(nn.Cell):
    def __init__(self, in_channel, out_channel):
        super().__init__()
        self.fc = nn.Dense(in_channel, out_channel, has_bias=False)
        self.loss = nn.MSELoss()

    def construct(self, x, y):
        logits = self.fc(x).squeeze()
        loss = self.loss(logits, y)
        return loss + logits

net2 = Net2(3, 1)
net2.fc.weight.set_data(ms.Tensor([[2, 3, 4]], ms.float32))   # 给全连接的weight设置固定值
grads = ms.grad(net2, grad_position=0, weights=None, has_aux=False)
grad = grads(x, y)  # 只有一个输出
print("grad", grad)
grad [[ 6.  9. 12.]]
# grad_position=None, weights=None

print("=== grads 8 ===")
grad_func = ms.grad(net, grad_position=None, weights=None, has_aux=True)
grad, logit = grad_func(x, y)
print("grad", grad)
print("logit", logit)

# === grads 8 ===
# ValueError: `grad_position` and `weight` can not be None at the same time.

mindspore.value_and_grad

mindspore.value_and_grad这个接口和上面的grad的参数是一样的,只不过这个接口可以一次性计算网络的正向结果和梯度。

grad_position

weights

output

0

None

(网络的输出, 第一个输入的梯度)

1

None

(网络的输出, 第二个输入的梯度)

(0, 1)

None

(网络的输出, (第一个输入的梯度, 第二个输入的梯度))

None

weights

(网络的输出, (weights的梯度))

0

weights

(网络的输出, ((第一个输入的梯度), (weights的梯度)))

(0, 1)

weights

(网络的输出, ((第一个输入的梯度, 第二个输入的梯度), (weights的梯度)))

None

None

报错

[11]:
print("=== value and grad ===")
value_and_grad_func = ms.value_and_grad(net, grad_position=(0, 1), weights=net.trainable_params(), has_aux=True)
value, grad = value_and_grad_func(x, y)
print("value", value)
print("grad", grad)
=== value and grad ===
value (Tensor(shape=[], dtype=Float32, value= 1), Tensor(shape=[], dtype=Float32, value= 20))
grad ((Tensor(shape=[1, 3], dtype=Float32, value=
[[4.00000000e+000, 6.00000000e+000, 8.00000000e+000]]), Tensor(shape=[], dtype=Float32, value= -2)), (Tensor(shape=[1, 3], dtype=Float32, value=
[[2.00000000e+000, 4.00000000e+000, 6.00000000e+000]]),))

mindspore.ops.GradOperation

mindspore.ops.GradOperation一个高阶函数,为输入函数生成梯度函数。

由 GradOperation 高阶函数生成的梯度函数可以通过构造参数自定义。

这个函数和grad的功能差不多,当前版本不推荐使用,详情请参考API内描述。

loss scale

由于在混合精度的场景,在求梯度的过程中可能会遇到梯度下溢,一般我们会使用loss scale配套梯度求导使用。

在Ascend上因为Conv、Sort、TopK等算子只能是float16的,MatMul由于性能问题最好也是float16的,所以建议loss scale操作作为网络训练的标配。Ascend 上只支持float16的算子列表

溢出可以通过MindSpore Insight的调试器或者dump数据获取到溢出算子信息。

一般溢出表现为loss Nan/INF,loss突然变得很大等。

[12]:
from mindspore.amp import StaticLossScaler, all_finite

loss_scale = StaticLossScaler(1024.)  # 静态lossscale

def forward_fn(x, y):
    loss, logits = net(x, y)
    print("loss", loss)
    loss = loss_scale.scale(loss)
    return loss, logits

value_and_grad_func = ms.value_and_grad(forward_fn, grad_position=None, weights=net.trainable_params(), has_aux=True)
(loss, logits), grad = value_and_grad_func(x, y)
print("=== loss scale ===")
print("loss", loss)
print("grad", grad)
print("=== unscale ===")
loss = loss_scale.unscale(loss)
grad = loss_scale.unscale(grad)
print("loss", loss)
print("grad", grad)

# 检查是否溢出,无溢出的话返回True
state = all_finite(grad)
print(state)
loss 1.0
=== loss scale ===
loss 1024.0
grad (Tensor(shape=[1, 3], dtype=Float32, value=
[[2.04800000e+003, 4.09600000e+003, 6.14400000e+003]]),)
=== unscale ===
loss 1.0
grad (Tensor(shape=[1, 3], dtype=Float32, value=
[[2.00000000e+000, 4.00000000e+000, 6.00000000e+000]]),)
True

loss scale的原理非常简单,通过给loss乘一个比较大的值,通过梯度的链式传导,在计算梯度的链路上乘一个比较大的值,防止在梯度反向传播过程中过小而出现精度问题。

在计算完梯度之后,需要把loss和梯度除回原来的值,保证整个计算过程正确。

最后一般需要使用all_finite来判断下是否有溢出,如果没有溢出的话就可以使用优化器进行参数更新了。

梯度裁剪

当训练过程中遇到梯度爆炸或者梯度特别大,训练不稳定的情况,可以考虑添加梯度裁剪,这里对常用的使用global_norm进行梯度裁剪的场景举例说明:

[13]:
from mindspore import ops

grad = ops.clip_by_global_norm(grad)

梯度累积

梯度累积是一种训练神经网络的数据样本按Batch拆分为几个小Batch的方式,然后按顺序计算,用以解决由于内存不足,导致Batch size过大,神经网络无法训练或者网络模型过大无法加载的OOM(Out Of Memory)问题。

详情请参考梯度累积