关于深度学习:MindSpore踩坑昇腾上的Cosine误差

20次阅读

共计 4111 个字符,预计需要花费 11 分钟才能阅读完成。

这两天遇到一个很经典的 issue,为啥说经典呢,因为这是一个可能体现框架研发人员和算法工程师认知重大错位的典型案例。先放链接,有趣味的同学能够去看下全过程。

【AICC】CosineDecayLR 余弦学习率实现形式强转 float32 类型计算(目前测试源码只能用 fp32),导致呈现负值学习率影响模型最初收敛!!!

写模型的老师 (maybe 学生?) 用了三个感叹号来表白心田的不爽。我先来简略形容一下这个问题:

P.Cos()(Tensor(math.pi, mstype.float32))

result: -1.000004

用于计算余弦函数的 Cos 算子,后果会有误差,失常状况下 cos(pi) = -1.0,然而用 MindSpore 计算失去的后果多了一个 -4e-6。个别状况下,如果在网络里用到 Cos 算子,其实影响也不大,然而。。。。这个问题是产生在 CosineDecayLR,也就是利用余弦函数动静调整学习率,这时候,就会呈现问题:

P.Cos()(Tensor(math.pi, mstype.float32)) + 1.0

result: -4.053116e-06

这个时候影响就十分大了,入门常识问题,学习率不能为正数,否则梯度更新会是反方向。此外,个别如 BERT 这样的模型,学习率的数量级在 1e- 5 左右,能够看到这个误差就会重大影响梯度降落了。

算子精度误差达标 = 性能正确?
既然问题很大,要怎么解决呢?我大略简述一下 issue 创建者和专家回复过程。

issue 创建者:CosineDecayLR 余弦学习率呈现负值学习率影响模型最初收敛!!!误差是 -4.053116e-06。
专家:百万分之 4 的误差,满足算子正当的计算误差范畴
issue 创建者:然而导致我学习率为负值之后,我模型梯度更新方向反了,loss 从稳固逐步升高 ……
这也是我说这是典型的起因,从硬件芯片到驱动使能再到算子库和框架,其实每一个层级的角度是不同的,所以对于昇腾(或者说 CANN)而言,算子的精度误差在正当范畴内,这个算子是能够验收公布的。这时候,如果默认其正确,而后交由下层封装(即 MindSpore)也是没有问题的。

然而!!!深度学习框架的研发和测试如果没有充沛的背景常识(其实是常识),就会呈现这样的问题。

显然,算子精度误差达标,绝不会等价于性能正确,像 CosineDecayLR 这样的 API 应该正当验证边界条件可能触发的问题。再次再次再次吐槽一遍,AI 框架开发者要有深度学习根底!

GPU 和 Ascend 上的正 / 余弦函数误差解决
回到问题自身,既然余弦函数有误差,正弦函数也得看看。而后我又在 GPU 上跑了一下,发现一个乏味的景象。

Ascend:

P.Cos()(Tensor(math.pi, mstype.float32))

result: -1.000004

P.Sin()(Tensor(math.pi, mstype.float32))

result: 0.0

GPU:

P.Cos()(Tensor(math.pi, mstype.float32))

result: -1.0

P.Sin()(Tensor(math.pi, mstype.float32))

result: -8.7423e-08

这个后果就很回味无穷了,Ascend 上 Sin 是是没有精度误差的,GPU 刚好相同。为了确认不是 MindSpore 的问题,我又用 Pytorch 跑了一下:

torch.cos(torch.tensor(math.pi))

result: -1.

torch.sin(torch.tensor(math.pi))

result: -8.7423e-08

能够明确 Pytorch 同样存在误差,然而 GPU 上应该对 Cos 做了解决。思考到个别 Cos 的应用场景更多(构建网络、学习率甚至权重初始化),这个解决也就能够了解了。而 Ascend 上 Sin 是无误差的,与 GPU 刚好相同,不晓得是出于什么起因的思考。然而从 MindSpore 跨平台应用而言,同样的 CosineDecayLR 代码,在这个时候会造成微小差别是毫无疑问的。

CosineDecayLR 的解决 (躲避) 计划
计划 1
依据 @用什么名字没那么重要 的倡议,间接 clip 数值更适合,不会呈现误差问题。

代码如下:

import mindspore.ops as P
import mindspore.common.dtype as mstype
from mindspore import context
from mindspore.nn.learning_rate_schedule import LearningRateSchedule

class CosineDecayLR(LearningRateSchedule):

def __init__(self, min_lr, max_lr, decay_steps):
    super(CosineDecayLR, self).__init__()
    if not isinstance(min_lr, float):
        raise TypeError("For'CosineDecayLR', the argument'min_lr'must be type of float,"
                        "but got'min_lr'type: {}.".format(type(min_lr)))
    if min_lr >= max_lr:
        raise ValueError("For'CosineDecayLR', the'max_lr'should be greater than the'min_lr',"
                         "but got'max_lr'value: {},'min_lr'value: {}.".format(max_lr, min_lr))
    self.min_lr = min_lr
    self.max_lr = max_lr
    self.decay_steps = decay_steps
    self.math_pi = math.pi
    self.delta = 0.5 * (max_lr - min_lr)
    self.cos = P.Cos()
    self.min = P.Minimum()
    self.max = P.Maximum()
    self.cast = P.Cast()

def construct(self, global_step):
    p = self.cast(self.min(global_step, self.decay_steps), mstype.float32)
    return self.min_lr + self.delta * self.max((1.0 + self.cos(self.math_pi * (p / self.decay_steps))), 0.0)

计划 2
有了后面的剖析,其实从前端角度解决或躲避就比较简单了,既然 Sin 算子不会呈现误差,那就间接应用 Sin 代替 Cos 即可:

cos(a) = sin(a + pi/2)

公式也很简略,间接革新一下 CosineDecayLR 源码即可。

import mindspore.ops as P
import mindspore.common.dtype as mstype
from mindspore import context
from mindspore.nn.learning_rate_schedule import LearningRateSchedule

class CosineDecayLR(LearningRateSchedule):

def __init__(self, min_lr, max_lr, decay_steps):
    super(CosineDecayLR, self).__init__()
    if not isinstance(min_lr, float):
        raise TypeError("For'CosineDecayLR', the argument'min_lr'must be type of float,"
                        "but got'min_lr'type: {}.".format(type(min_lr)))
    if min_lr >= max_lr:
        raise ValueError("For'CosineDecayLR', the'max_lr'should be greater than the'min_lr',"
                         "but got'max_lr'value: {},'min_lr'value: {}.".format(max_lr, min_lr))
    self.min_lr = min_lr
    self.max_lr = max_lr
    self.decay_steps = decay_steps
    self.math_pi = math.pi
    self.delta = 0.5 * (max_lr - min_lr)
    self.cos = P.Cos()
    self.sin = P.Sin()
    self.min = P.Minimum()
    self.cast = P.Cast()
    self.is_ascend = context.get_context("device_target") == "Ascend"

def construct(self, global_step):
    p = self.cast(self.min(global_step, self.decay_steps), mstype.float32)
    if self.is_ascend:
        return self.min_lr + self.delta * (1.0 + self.sin(self.math_pi * (p / self.decay_steps + 0.5)))
    return self.min_lr + self.delta * (1.0 + self.cos(self.math_pi * (p / self.decay_steps)))

通过实测,

P.Cos()(Tensor(math.pi, mstype.float32))

result: -1.000004

P.Sin()(Tensor(math.pi * (1 + 0.5), mstype.float32))

result: -0.9999996

尽管也有误差,然而不会呈现 cos(pi) + 1.0 < 0.0 的状况了,因而学习率不会呈现负值,梯度更新不会反向。然而精度问题还在,而且框架研发人员和算法工程师认知重大错位的问题值得更加器重。

以上。

正文完
 0