mindspore模型训练—混合精度算法

举报
leid_lzu 发表于 2021/11/02 13:16:32 2021/11/02
【摘要】     **概述**:深度学习模型的计算任务分为训练和推理.训练往往是放在云端或者超算集群中,利用GPU强大的浮点计算能力,来完成网络模型参数的学习过程.一般来说训练时,计算资源往往非常充足,基本上受限于显存资源/多节点扩展/通讯库效率的问题。相对于训练过程,推理往往被应用于终端设备,如手机,计算资源/功耗都收到严格的限制,为了解决这样的问题,提出了很多不同的方法来减少模型的大小以及所需的计...

    **概述**:深度学习模型的计算任务分为训练和推理.训练往往是放在云端或者超算集群中,利用GPU强大的浮点计算能力,来完成网络模型参数的学习过程.一般来说训练时,计算资源往往非常充足,基本上受限于显存资源/多节点扩展/通讯库效率的问题。相对于训练过程,推理往往被应用于终端设备,如手机,计算资源/功耗都收到严格的限制,为了解决这样的问题,提出了很多不同的方法来减少模型的大小以及所需的计算资源/存储资源。模型压缩除了剪枝以外,还有一个方法就是降低模型参数的数值精度。随着网络深度的加大,带来的参数数量也呈现指数级增长,如何将最终学习好的网络模型塞入到终端设备有限的空间中是目前很多性能优良的网络真正应用到日常生活中的一大阻碍。
    大多数的深度学习模型使用的是32位单精度浮点数(FP32)来进行训练,而混合精度训练的方法中则增加了通过16位浮点数(FP16)进行深度学习模型训练,从而减少了训练深度学习模型所需的内存,同时由于FP16的运算比FP32运算更快,从而也进一步提高了硬件效率。
    **混合精度训练方法**是通过混合使用单精度和半精度数据格式来加速深度神经网络训练的过程,同时保持了单精度训练所能达到的网络精度。即在尽可能减少精度损失的情况下利用半精度浮点数加速训练。
    使用FP16即半精度浮点数存储权重和梯度。在减少占用内存的同时起到了加速训练的效果。混合精度训练能够加速计算过程,同时减少内存使用和存取,并使得在特定的硬件上可以训练更大的模型或batch size。
    **缺陷:**
    训练采用低精度数据在一定程度上会造成精度损失,详情如下:
    一个FP16数据占据两个字节,其中1位符号位,5位指数位,10位有效精度,取值范围是5.96× 10−8 ~ 65504,而FP32则是1.4×10-45 ~ 3.4×1038。从FP16的范围可以看出,用FP16代替原FP32神经网络计算的最大问题就是精度损失。
    **MindSpore新版本可支持图神经网络的训练**,最典型的GCN和GAT网络在Cora和Citeseer数据集上做混合精度训练,对于mindspore混合精度模型训练而言,主要分为自动混合精度和手动混合精度,具体计算流程如下(图片来源https://zhuanlan.zhihu.com/p/352746002)

(1)自动混合精度
    使用自动混合精度,需要调用相应的接口,将待训练网络和优化器作为输入传进去;该接口会将整张网络的算子转换成FP16算子(除BatchNorm算子和Loss涉及到的算子外)。可以使用amp接口和Model接口两种方式实现混合精度。
    使用mindspore\train\amp.build_train_network接口封装网络模型、优化器和损失函数,设置level参数,在该步骤中,MindSpore会将有需要的算子自动进行类型转换。

def build_train_network(network, optimizer, loss_fn=None, level='O0', **kwargs):
    """
    Build the mixed precision training cell automatically.
    自动构建混合精度训练单元。
    Args:
        network (Cell): 网络的定义
        loss_fn (Union[None, Cell]): loss_fn的定义。如果没有,则“网络”中应该有损失。
            Default: None.
        optimizer (Optimizer): 优化器更新参数。
        level (str): Supports ["O0", "O2", "O3", "auto"]. Default: "O0".
            - O0: 无改变
            - O2: 将网络转换为float16,保持batchnorm和'loss_fn'(如果设置)为float32类型,
            使用动态损耗量表。
            - O3: 转换网络为float16, 附加属性 'keep_batchnorm_fp32=False'.
            - auto: Set to level to recommended level in different devices. Set level to O2 on GPU, Set
              level to O3 Ascend. The recommended level is choose by the export experience, cannot
              always generalize. User should specify the level for special network.
              在不同的设备中将“级别”设置为 建议 的级别。在GPU上将级别设置为O2,建议在ascend级别使用O3。
              建议的级别由导出经验选择,不能总是一概而论。用户应指定特殊网络的级别。
            O2 is recommended on GPU, O3 is recommended on Ascend.

        cast_model_type (:class:`mindspore.dtype`): 支持 `mstype.float16` or `mstype.float32`.
            If set to `mstype.float16`, use `float16` mode to train. If set, 覆盖级别设置。
        keep_batchnorm_fp32 (bool): Keep Batchnorm run in `float32`. If set, 覆盖级别设置。
            Only `cast_model_type` is `float16`, `keep_batchnorm_fp32` 将生效。
        loss_scale_manager (Union[None, LossScaleManager]): 如果没有,则不按比例计算损失,否则
                                    通过“LossCaleManager”衡量损失。如果已设置,则覆盖级别设置。
    """
    validator.check_value_type('network', network, nn.Cell)
    #检查network类型是否为nn.cell
    validator.check_value_type('optimizer', optimizer, (nn.Optimizer, acc.FreezeOpt))
    validator.check('level', level, "", ['O0', 'O2', 'O3', "auto"], Rel.IN)
    #同理
    if level == "auto":
        device_target = context.get_context('device_target')
        #get_context() 定义于 mindspore\mindspore\context.py
        #根据输入键获取上下文属性值。
        if device_target == "GPU":
            level = "O2"
        elif device_target == "Ascend":
            level = "O3"
        else:
            raise ValueError("Level `auto` only support when `device_target` is GPU or Ascend.")
    _check_kwargs(kwargs)
    #检查参数
    config = dict(_config_level[level], **kwargs)
    #**kwargs常用于有key值的输入
    '''
   def test(**kwargs):
       print(kwargs)
       keys = kwargs.keys()
       value = kwargs.values()
       print(keys)
       print(value)
   test(a=1,b=2,c=3,d=4)
   # 输出值分别为
   # {'a': 1, 'b': 2, 'c': 3, 'd': 4}
   # dict_keys(['a', 'b', 'c', 'd'])
   # dict_values([1, 2, 3, 4])
       '''
    config = edict(config)
    #from easydict import EasyDict as edict
    #EasyDict可以让你像访问属性一样访问dict里的变量
    '''
#实例如下:
from easydict import EasyDict as edict
easy = edict(d = {'foo':3, 'bar':{'x':1, 'y':2}}) # 将普通的字典传入到edict()
print(easy['foo'])  # 这是传统的方法
print(easy.foo)  # 这是我们使用easydict输出二者结果是一样的,但是可以更为方便的使用字典了
print(easy.bar.x)  # 我们也是可以很方便的使用字典中字典的元素了
    '''

    if config.cast_model_type == mstype.float16:
        network.to_float(mstype.float16)#若格式不符则进行格式转换

        if config.keep_batchnorm_fp32:#config已经转化为EasyDict 如果对应keep_batchnorm_fp32属性参数为真
            _do_keep_batchnorm_fp32(network)

    if loss_fn:
        network = _add_loss_network(network, loss_fn, config.cast_model_type)

    if _get_parallel_mode() in (ParallelMode.SEMI_AUTO_PARALLEL, ParallelMode.AUTO_PARALLEL):
        network = _VirtualDatasetCell(network)

    loss_scale = 1.0
    if config.loss_scale_manager is not None:
        loss_scale_manager = config.loss_scale_manager
        loss_scale = loss_scale_manager.get_loss_scale()
        update_cell = loss_scale_manager.get_update_cell()
        if update_cell is not None:
            # 只有cpu不支持 `TrainOneStepWithLossScaleCell` 控制流
            if not context.get_context("enable_ge") and context.get_context("device_target") == "CPU":
                raise ValueError("Only `loss_scale_manager=None` and "
                                 "`loss_scale_manager=FixedLossScaleManager(drop_overflow_update=False)`"
                                 "are supported in current version. If you use `O2` option, please"
                                 "use `loss_scale_manager=None` or `FixedLossScaleManager`")
            network = nn.TrainOneStepWithLossScaleCell(network, optimizer,
                                                       scale_sense=update_cell).set_train()
            return network
    network = nn.TrainOneStepCell(network, optimizer, loss_scale).set_train()
    #TrainOneStepCell继承nn.Cell,包含大量网络训练的库函数的类
    '''
    set_train():model参数默认为true
    将单元格设置为训练模式。单元本身和所有子单元将设置为训练模式。
    具有不同构造的层对于训练和预测,例如“BatchNorm”,将通过此属性区分分支。
    如果设置为True时,将执行训练分支,否则将执行另一个分支。
    '''
    return network
```



(2)手动混合精度
    MindSpore还支持手动混合精度。假定在网络中只有一个Dense Layer要用FP32计算,其他Layer都用FP16计算。混合精度配置以Cell为粒度,Cell默认是FP32类型。
(3)约束条件:
    使用混合精度时,只能由自动微分功能生成反向网络,不能由用户自定义生成反向网络,否则可能会导致MindSpore产生数据格式不匹配的异常信息。
![混合精度训练概述图](/api/attachments/368994)

    定义于mindspore\mindspore\train\amp.py中的三种规范

_config_level = {#初始化三种配置级别
    "O0": {
        "keep_batchnorm_fp32": False,#是否遵守fp32规范
        "cast_model_type" :mstype.float32,
        "loss_scale_manager": None},
    "O2": {
        "keep_batchnorm_fp32": True,
        "cast_model_type": mstype.float16,#半精度
        "loss_scale_manager": DynamicLossScaleManager()},
    "O3": {
        "keep_batchnorm_fp32": False,
        "cast_model_type": mstype.float16,
        "loss_scale_manager": None}}


**混合精度训练实现关键技术:**
    第一项关键技术被称为“混合精密钥匙”(mixed precision key)。在MT模型中仍然保留FP32格式的主副本,将FP16用于正向和反向传播,优化器中的梯度更新将被添加到主FP32副本当中,该FP32副本被简化为一个FP16副本在训练期间使用,这个过程在每次训练迭代中重复,直至模型收敛且足以恢复损失的精度,从而达到较低内存使用、内存带宽压力更低和更快速执行的优点。
    第二种关键技术则是“损耗缩放”(loss-scaling)。该技术可以够恢复一些小数值的梯度。在训练期间,一些权重梯度具有非常小的指数,其FP16格式可能会变为零。为了克服这个问题,我们使用缩放因子在反向传播开始时缩放损失,通过连锁规则,梯度也逐渐扩大,并在FP16中可表示。在将其更新应用于权重之前,梯度确实需要缩小;而为了恢复某些型号的精度损失,必须进行损耗调整。
    MindSpore主要运用的是第二种,所定义抽象类函数(mindspore\mindspore\train\loss_scale_manager.py)详情如下:

"""损失规模管理器抽象类。"""

from .._checkparam import Validator as validator#常用于参数验证
from .. import nn


class LossScaleManager:
    """损失规模管理器抽象类"""
    def get_loss_scale(self):
        """获取损失比例值。"""

    def update_loss_scale(self, overflow):
        """
        更新损失比例值。

        Args:
            overflow (bool): 是否溢出.
        """
    def get_update_cell(self):
        """Get the loss scaling update logic cell.获取损耗缩放更新逻辑单元。"""


class FixedLossScaleManager(LossScaleManager):
    """
    Fixed loss-scale manager固定损失规模管理器.

    Args:
        loss_scale (float): Loss scale. Note that if `drop_overflow_update` is set to False, the value of `loss_scale`
            in optimizer that you used need to be set to the same value as here. Default: 128.0.
        损失规模。请注意,如果“drop_overflow_update”设置为False,则需要将优化器中使用的“loss_scale”的值设置为与此处相同的值。 
        drop_overflow_update (bool): Whether to execute optimizer if there is an overflow. If True, the optimizer will
            not executed when overflow occurs. Default: True.
            出现溢出时,是否执行优化器。如果为True,则优化器发生溢出时将不执行。

    Examples:
        >>> from mindspore import Model, nn, FixedLossScaleManager
        >>>
        >>> net = Net()#初始化网络
        >>> #1) Drop the parameter update if there is an overflow如果出现溢出,请删除参数更新 
        >>> loss_scale_manager = FixedLossScaleManager()#固定损失规模管理器
        >>> optim = nn.Momentum(params=net.trainable_params(), learning_rate=0.1, momentum=0.9)
        >>> model = Model(net, loss_scale_manager=loss_scale_manager, optimizer=optim)#模型初始化
        >>>
        >>> #2) Execute parameter update even if overflow occurs
        >>> loss_scale = 1024
        >>> loss_scale_manager = FixedLossScaleManager(loss_scale, False)
        >>> optim = nn.Momentum(params=net.trainable_params(), learning_rate=0.1, momentum=0.9, loss_scale=loss_scale)
        >>> model = Model(net, loss_scale_manager=loss_scale_manager, optimizer=optim)
    """
    def __init__(self, loss_scale=128.0, drop_overflow_update=True):
        if loss_scale < 1:
            raise ValueError("loss_scale must be at least 1, "
                             "but got loss_scale {}".format(loss_scale))
        self._loss_scale = loss_scale
        self._drop_overflow_update = drop_overflow_update

    def get_loss_scale(self):
        """获取损耗缩放值"""
        return self._loss_scale

    def get_drop_overflow_update(self):
        """获取当出现溢出时是否删除优化器更新的标志。"""
        return self._drop_overflow_update

    def update_loss_scale(self, overflow):
        """
        更新损耗缩放值.
        Args参数:
            overflow (bool): 是否溢出的标志.
        """

    def get_update_cell(self):
        "Returns the cell返回单元格 for `TrainOneStepWithLossScaleCell`"
        if not self._drop_overflow_update:
            return None
        return nn.FixedLossScaleUpdateCell(self._loss_scale)


class DynamicLossScaleManager(LossScaleManager):
    """
    动态损失规模管理器。

    Args:
        init_loss_scale (float): Initialize loss scale初始化损失规模(比例). Default: 2**24.
        scale_factor (int): Coefficient of increase and decrease增减系数. Default: 2.
        scale_window (int): Maximum continuous normal steps when there is no overflow无溢出时的最大连续正常步数. Default: 2000.

    Examples:
        >>> from mindspore import Model, nn
        >>> from mindspore.train.loss_scale_manager import DynamicLossScaleManager
        >>>
        >>> net = Net()#初始化网络
        >>> loss_scale_manager = DynamicLossScaleManager()#实例化
        >>> optim = nn.Momentum(params=net.trainable_params(), learning_rate=0.1, momentum=0.9)
        >>> model = Model(net, loss_scale_manager=loss_scale_manager, optimizer=optim)
    """
    def __init__(self,
                 init_loss_scale=2 ** 24,
                 scale_factor=2,
                 scale_window=2000):
        if init_loss_scale < 1.0:
            raise ValueError("Loss scale value should be > 1")
        self.loss_scale = init_loss_scale
        validator.check_positive_int(scale_window, "scale_window", self.__class__.__name__)
        self.scale_window = scale_window
        if scale_factor <= 0:
            raise ValueError("Scale factor should be > 1")
        self.scale_factor = scale_factor
        self.increase_ratio = scale_factor
        self.decrease_ratio = 1 / scale_factor
        self.cur_iter = 1
        self.last_overflow_iter = 0
        self.bad_step_max = 1000
        self.bad_step = 0

    def get_loss_scale(self):
        """Get loss scale value."""
        return self.loss_scale

    def update_loss_scale(self, overflow):
        """
        Update loss scale value.

        Args:
            overflow: Boolean. Whether it overflows.布尔值 1/0
        """
        if overflow:
            self.loss_scale = max(self.loss_scale * self.decrease_ratio, 1)
            self.last_overflow_iter = self.cur_iter
            self.bad_step += 1
        else:
            if (self.cur_iter - self.last_overflow_iter) % self.scale_window == 0:
                self.loss_scale *= self.increase_ratio
            self.bad_step = 0

        if self.bad_step > self.bad_step_max:
            raise RuntimeError("Dynamic loss scale Continuous overflow ", self.bad_step, " times")

        self.cur_iter += 1

    def get_drop_overflow_update(self):
        """Get the flag whether to drop optimizer update when there is an overflow."""
        return True

    def get_update_cell(self):
        "Returns the cell for `TrainOneStepWithLossScaleCell`"
        return nn.DynamicLossScaleUpdateCell(self.loss_scale, self.scale_factor, self.scale_window)


**MindSpore相关新特性举例**:
**多跳知识推理问答(TPRR)**:TPRR是华为泊松实验室与华为MindSpore团队提出的解决开放域多跳问题的通用模型。相比于传统问答仅需从单个文档中检索答案,多跳知识推理问答需要从多个佐证文档得到最终答案,并返回问题到答案的推理链。TPRR基于MindSpore混合精度特性,可以高效地完成多跳问答推理过程。
全路径建模:TPRR模型在多跳问题推理链的每一个环节中基于全部推理路径的条件概率建模,模型以“全局视角”进行知识推理。动态样本选取:TPRR模型采用动态样本的建模方式,通过更强的对比学习提升模型多跳问答的能力

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。