4.1.1.8. 模型精度分析与调优

基于几十或上百张校准数据实现浮点模型到定点模型转换的后量化方式,不可避免地会存在一定的精度损失。 但经过大量实际生产经验验证,如果能筛选出最优的量化参数组合,地平线的转换工具在大部分情况下,都可以将精度损失保持在1%以内。

本节先介绍了如何正确地进行模型精度分析,如果通过评估发现不及预期,则可以参考 精度调优 小节的内容尝试调优, 实在无法解决可寻求地平线的技术支持。

4.1.1.8.1. 模型精度分析

在进入到此部分介绍前,我们希望您已经了解如何对一个模型进行精度评测。本节介绍的内容是如何使用模型转换的产出物进行推理。

前文提到模型成功转换的产出物包括以下四个部分:

  • ***_original_float_model.onnx

  • ***_optimized_float_model.onnx

  • ***_calibrated_model.onnx

  • ***_quantized_model.onnx

  • ***.bin

虽然最后的bin模型才是将部署到计算平台的模型,考虑到方便在Ubuntu开发机上完成精度评测, 我们提供了***_quantized_model.onnx完成这个精度评测的过程。 quantized模型已经完成了量化,与最后的bin模型具有一致的精度效果。 使用地平线开发库加载ONNX模型推理的基本流程如下所示,这份示意代码不仅适用于quantized模型, 对original和optimized模型同样适用,根据不同模型的输入类型和layout要求准备数据即可。

# 加载地平线依赖库
from horizon_tc_ui import HB_ONNXRuntime

# 准备模型运行的feed_dict
def prepare_input_dict(input_names):
  feed_dict = dict()
  for input_name in input_names:
      # your_custom_data_prepare代表您的自定义数据
      # 根据输入节点的类型和layout要求准备数据即可
      feed_dict[input_name] = your_custom_data_prepare(input_name)
  return feed_dict

if __name__ == '__main__':
  # 创建推理Session
  sess = HB_ONNXRuntime(model_file='***_quantized_model.onnx')

  # 获取输入节点名称
  input_names = [input.name for input in sess.get_inputs()]
  # 或
  input_names = sess.input_names

  # 获取输出节点名称
  output_names = [output.name for output in sess.get_outputs()]
  # 或
  output_names = sess.output_names

  # 准备模型输入数据
  feed_dict = prepare_input_dict(input_names)
  # 开始模型推理,推理的返回值是一个list,依次与output_names指定名称一一对应
  # 输入图像的类型范围为(RGB/BGR/NV12/YUV444/GRAY)
  outputs = sess.run(output_names, feed_dict, input_offset=128)
  # 输入数据的类型范围为(FEATURE)
  outputs = sess.run_feature(output_names, feed_dict, input_offset=0)

  """
  Modification  history:
    OE 1.3 ~ 1.6
        outputs = sess.run(output_names, feed_dict, input_type_rt=None, float_offset=0)
        outputs = sess.run_feature(output_names, feed_dict, {input_name: "featuremap"}, float_offset=0)
    OE 1.7
        outputs = sess.run(output_names, feed_dict, input_type_rt=None, float_offset=None, input_offset=128)
        outputs = sess.run_feature(output_names, feed_dict, {input_name: "featuremap"}, float_offset=0)
    OE 1.8 ~ 1.9
        outputs = sess.run(output_names, feed_dict, input_offset=128)
        outputs = sess.run_feature(output_names, feed_dict, input_offset=128)

    note: OE 1.5 后架构上的调整,如果更新 OE 需要重新编译模型
  """

上述代码中, input_offset 参数可以不提供,其默认值为128。对于有前处理节点的模型,这里都需要做-128的操作。如果模型输入前并未添加前处理节点,则需要将 input_offset 设置为0。

注解

对于多输入模型:

  • 如果输入input_type均属于(RGB/BGR/NV12/YUV444/GRAY),可以采用 sess.run 方法做推理。

  • 如果输入input_type均属于(FEATURE),可以采用 sess.run_feature 方法做推理。

  • 请注意,目前暂不支持输入 input_type 为混合类型。

此外, your_custom_data_prepare 所代表的输入数据准备过程是最容易出现误操作的部分。 较于您设计&训练原始浮点模型的精度验证过程,我们需要您在数据预处理后将推理输入数据进一步调整, 这些调整主要是数据格式(RGB、NV12等)、数据精度(int8、float32等)和数据排布(NCHW或NHWC)。 至于具体怎么调整,这个是由您在模型转换时设置的 input_type_traininput_layout_traininput_type_rtinput_layout_rt 四个参数共同决定的,其详细规则请参考 转换内部过程解读 部分的介绍。

举个例子,有一个使用ImageNet训练的用于分类的原始浮点模型,它只有一个输入节点。 这个节点接受BGR顺序的三通道图片,输入数据排布为NCHW。原始浮点模型设计&训练阶段,验证集推理前做的数据预处理如下:

  1. 图像长宽等比scale,短边缩放到256。

  2. center_crop 方法获取224x224大小图像。

  3. 按通道减mean。

  4. 数据乘以scale系数。

使用地平线转换这个原始浮点模型时, input_type_train 设置 bgrinput_layout_train 设置 NCHWinput_type_rt 设置 bgrinput_layout_rt 设置 NHWC

根据 转换内部过程解读 部分介绍的规则, ***_quantized_model.onnx接受的输入应该为bgr_128、NCHW排布。 对应到前文的示例代码, your_custom_data_prepare 部分提供的数据处理应该一个这样的过程:

# 本示例使用skimage,如果是opencv会有所区别
# 需要您特别注意的是,transformers中并没有体现减mean和乘scale的处理
# mean和scale操作已经融合到了模型中,参考前文norm_type/mean_values/scale_values配置
def your_custom_data_prepare_sample(image_file):
  # skimage读取图片,已经是NHWC排布
  image = skimage.img_as_float(skimage.io.imread(image_file))
  # 长宽等比scale,短边缩放至256
  image = ShortSideResize(image, short_size=256)
  # CenterCrop获取224x224图像
  image = CenterCrop(image, crop_size=224)
  # skimage读取结果通道顺序为RGB,转换为bgr_128需要的BGR顺序
  image = RGB2BGR(image)
  # 如果原模型是 NCHW 输入(input_type_rt为nv12除外)
  if layout == "NCHW":
    image = HWC2CHW(image)
  # skimage读取数值范围为[0.0,1.0],调整为bgr需要的数值范围
  image = image * 255
  # bgr_128是bgr减去128
  image = image - 128
  #bgr_128使用int8
  image = image.astype(np.int8)

  return image

4.1.1.8.2. 精度调优

基于前文的精度分析工作,如果确定模型的量化精度不符合预期,则主要可分为以下两种情况进行解决:

1.精度有较明显损失(损失大于4%)。

这种问题往往是由于yaml配置不当,校验数据集不均衡等导致的,可以根据我们接下来提供的建议逐一排查。

2.精度损失较小(1.5%~3%)。

排除1导致的精度问题后,如果仍然出现精度有小幅度损失,往往是由于模型自身的敏感性导致,可以使用我们提供的精度调优工具进行调优。

3.在尝试1和2后,如果精度仍无法满足预期,可以尝试使用我们提供的精度debug工具进行进一步尝试。

整体精度问题解决流程示意如下图:

../../../../_images/accuracy_problem.png

一、 精度有明显损失(4%以上)

通常情况下,明显的精度损失往往是由于各种配置不当引起的,我们建议您依次从pipeline、模型转换配置和一致性三个方面检查。

pipeline检查

pipeline是指您完成数据准备、模型推理、后处理、精度评测Metric的全过程。 在以往的实际问题跟进经验中,我们发现这些部分在原始浮点模型训练阶段中有变动,却没有及时更新到模型转换的精度验证过程来是比较常见的情况。

模型转换配置检查

  • input_type_rtinput_type_train 该参数用来区分转后混合异构模型与原始浮点模型需要的数据格式,需要认真检查是否符合预期,尤其是BGR和RGB通道顺序是否正确。

  • norm_typemean_valuesscale_values 等参数是否配置正确。通过转换配置可以直接在模型中插入mean和scale操作节点, 需要确认是否对校验/测试图片进行了重复的mean和scale操作。重复预处理是错误的易发区。

数据处理一致性检查

  • skimage.readopencv.imread 是两种常用图片读取方法,这两种方法在输出的范围和格式上都有所区别。 使用 skimage 的图片读取,得到的是RGB通道顺序,取值范围为0~1,数值类型为float;而使用 opencv,得到的是BGR通道顺序,取值范围为0~255,数据类型为uint8。

  • 在校准数据准备阶段、给应用程序准备应用样本时,我们常使用numpy的tofile序列化数据。这种方式不会保存shape和类型信息, 在加载时都需要手动指定,需要您确保这些文件的序列化和反序列化过程的数据类型、数据尺寸和数据排布等信息都是一致的。

  • 推荐您在地平线工具链使用过程中,依然使用原始浮点模型训练验证阶段依赖的数据处理库。 对于鲁棒性较差的模型,不同库实现的功能resize、crop等典型功能都可能引起扰动,进而影响模型精度。

  • 校验图片集是否合理设置。校准图片集数量应该在百张左右,同时最好可以覆盖到数据分布的各种场合,例如在多任务或多分类时,校验图片集可以覆盖到各个预测分支或者各个类别。 同时避免偏离数据分布的异常图片(过曝光等)。

  • 使用 ***_original_float_model.onnx再验证一遍精度,正常情况下,这个模型的精度应该是与原始浮点模型精度保持小数点后三到五位对齐。 如果验证发现不满足这种对齐程度,则表明您的数据处理需要再仔细检查。

二、 较小精度损失提升

一般情况下,为降低模型精度调优的难度,我们建议您在转换配置中使用的是自动参数搜索功能。 如果发现自动搜索的精度结果仍与预期有一定的差距,较于原始浮点模型的精度损失在1.5%到3%范围左右。 可以分别尝试使用以下建议提高精度:

  • 尝试在配置转换中手动指定 calibration_type ,可以先选择 mix ,如果最终精度仍不符合预期,再尝试 kl / max

  • 尝试在配置转换中启用 per_channel

  • calibration_type 设定为 max 时, 同时配置 max_percentile 参数分别为 0.999990.999950.99990.99950.999 进行尝试。

三、 精度debug工具

在尝试了一和二中提供的方法后,如果您的精度仍无法满足预期,为了方便您定位问题,我们提供了精度debug工具用于协助您定位问题。 该工具能够协助您对校准模型进行节点粒度的量化误差分析,快速定位出现精度异常的节点。 工具的详细介绍及使用方法您可参考 精度debug工具 章节。

根据以往的实际生产经验,以上策略已经可以应对各种实际问题。 如果经过以上尝试仍然未能解决您的问题,欢迎在地平线唯一官方技术社区(https://developer.horizon.ai)发帖与我们取得联系, 我们将根据您的具体问题提供更具针对性的指导建议。

4.1.1.8.3. 使用QAT量化感知训练方案进一步提升模型精度

如果通过上述分析,并没有发现任何配置上的问题,但是精度仍不能满足要求,则可能是PTQ本身的限制。 这时候我们可以改用QAT的方式来对模型进行量化。

本小节内容对QAT方案进行详细介绍:

  • 首先,关于量化 介绍量化的概念和两种量化方法;

  • 其次,关于模型转换 介绍地平线模型转换、原始浮点模型和混合异构模型的概念;

  • 接着,在理解了以上一些概念后,关于模型量化编译流程 一小节内容,让您理解地平线PTQ和QAT方案的关系,便于您可以在不同情况下选择更合适的模型处理方案;

  • 最后,QAT模型量化编译 则是为您引入通过QAT方案完成量化模型编译的介绍。

4.1.1.8.3.1. 关于量化

目前在GPU上训练的模型大部分都是浮点模型,即参数使用的是float类型存储。 地平线BPU架构的计算平台使用的是int8的计算精度(业内计算平台的通用精度),能运行定点量化模型。 那么 从训练出的浮点精度转为定点模型的过程,我们叫做量化。

量化方法有两种,分别为

  • 后量化(post training quantization,PTQ)

    先训练浮点模型,然后使用校准图片计算量化参数,将浮点模型转为量化模型。 该方法简单、快捷,但将浮点模型直接转为量化模型难免会有一些量化损失。

    注解

    关于PTQ模型的量化和编译流程, 模型量化与编译 已为您做出了详细介绍。

  • 量化感知训练(quantization aware training,QAT)

    在浮点训练的时候,就先对浮点模型结构进行干预,增加量化误差,使得模型能够感知到量化带来的损失。 该方法需要用户在全量训练集上重新训练,能有效地降低量化部署的量化误差。 一些社区框架都提供QAT方案,例如pytorch的eager mode方案、pytorch的fx graph方案、tf-lite量化方案等。

    注解

    QAT训练与浮点训练的关系

    QAT训练是一种finetune方法,最好是在浮点结果已经拟合的情况下,再用QAT方法提升量化精度。 即用户的训练分为了两个步骤,先训练浮点模型,将模型精度提升到满意的指标;再通过QAT训练,提升量化精度。

    为了让模型更好的感知到量化误差,QAT训练需要使用全量的训练数据集。 训练轮数和模型难度相关,大约是原来的浮点训练的1/10。 因为是在浮点模型上finetune,所以QAT训练的学习率尽量和浮点模型的最后几个epoch一致。

4.1.1.8.3.2. 关于模型转换

模型转换是指将原始浮点模型转换为地平线混合异构模型的过程。 其中会包括模型前处理节点修改、原始模型图优化、模型量化和上板模型编译等过程。

原始浮点模型 (文中部分地方也称为浮点模型)是指您通过TensorFlow/PyTorch等等DL框架训练得到的可用模型,这个模型的计算精度为float32; 目前我们的QAT方案中,训练工具基于Pytorch开发,因此只支持Pytorch格式的模型。 PTQ方案只支持Caffe&ONNX模型格式,因此对于TensorFlow/PyTorch等格式的模型,需要先通过转换到ONNX模型后,才能够被地平线的工具进行量化&编译。

混合异构模型 是一种适合在地平线计算平台上运行的模型格式,之所以被称为异构模型是因为它能够支持模型同时在ARM CPU和BPU上执行。 由于在BPU上的运算速度会远大于CPU上的速度,因此会尽可能的将算子放在BPU上运算。 对于BPU上暂时不支持的算子,则会放在CPU上进行运算。

4.1.1.8.3.3. 关于模型量化编译流程

正常的模型量化编译流程如下图所示:

../../../../_images/qat_compile_flow.png

小技巧

由于PTQ方式的使用代价小,因此推荐用户首先尝试该方法进行模型量化编译。 若尝试并调优后的模型精度依然无法满足要求,则可以再改为尝试QAT方案。

4.1.1.8.3.4. QAT模型量化编译介绍

Horizon Plugin Pytorch参考了PyTorch官方的量化接口和思路,Plugin采用的是Quantization Aware Training(QAT)方案, 因此建议用户先阅读 PyTorch官方文档 中和QAT相关部分。

更详细的关于Horizon Plugin Pytorch的介绍,您可以参考 量化感知训练(QAT) 章节。