梯度求导
自动微分接口
正向网络构建完成之后,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_position
和weights
共同决定要输出哪些值的梯度,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)问题。
详情请参考梯度累积。