[TOC]

概述

量化背景

  • 随着神经网络层数的不断加深,网络体积与计算量也在不断增大。
    对于实时性要求较高的模型计算,及移动端的ai需求,设备算力可能不足以支持运算需求。
    对模型的权重进行量化可以有效的加速运算,减少资源开销。

低比特量化技术

  1. 为什么CNN可以使用量化模型做推理
    • 由于pooling层、激活函数层(如relu),早期的DropOut层的存在,CNN网络结构对于噪声是不敏感的。
      因此在卷积神经网络模型的前向计算时,可以使用低精度的数值类型(float16/int8)来表示模型训练所使用的float32浮点数值。
  2. 量化的收益
    • 以int8量化为例,量化后的模型体积将为量化前的1/4,减少了存储压力及访存压力
    • 定点运算相较于浮点运算,模型运算速度提升明显
  3. 量化的问题与难点
    1. 核心是精度损失
      • 低比特计算会带来精度的损失
        • 量化对于模型权重分布的描述不能完全准确
        • 相较于分类任务,回归任务精度损失将更大
        • 深层网络会带来更大的精度损失
        • DepthWise对于量化不友好
        1. 同时也需考虑软硬件支持
        • 不同硬件支持的低比特指令不一致
        • 不同神经网络框架提供的量化方法不一致

以NCNN为例的int8量化

  • 与Nvidia的TensorRT一致,ncnn采用无需retrain的直接量化方案

  • 量化原理:目的是将float32的卷积操作转换为int8的卷积操作,这样计算量就变为原来的1/4
    对于每一层的网络参数,float32与int8的对应原理如下图所示

  • 也就是将一个网络层的权重值按照minmax范围,按照一定比例,映射到-127127范围内
    公式如下
    FP32 Tensor (T) = scale * 8-bit Tensor(t) + FP32_bias (b)
    可见ncnn,ncnn仅对weights做量化,并未对bias做量化

  • ncnn Conv int8计算流程,如下图

    image-20210721200509794

    ​ ncnn首先将输入(bottom_blob)在线量化为INT8,再与离线量化为INT8的weights进行卷积运算。
    然后将运算结果反量化为float32,再和未经量化的bias相加,得到输出(top_blob)。

  • 量化 quantize

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    网络输入与权重的量化公式
    bottom_blob(int8) = bottom_blob_scale(int8) * bottom_blob_fp32
    weight_blob(int8) = weight_data_scale(int8) * weight_blob_fp32
    // ncnn float2int8代码
    static inline signed char float2int8(float v)
    {
    int int32 = round(v);
    if (int32 > 127) return 127;
    if (int32 < -128) return -128;
    return (signed char)int32;
    }
    1
    2
    3
    权重是不会变化的,因此weight_blob是离线量化的。  
    而输入是会变化的,所以bottom_blob是在线量化的,
    但使用的bottom_blob_int8_scale是根据小批量样本离线生成的。
    1
    2
    3
    4
    前向推理:
    bottom_blob(fp32) * weight_blob(fp32) =
    bottom_blob(int8) * weight_blob(int8)
    / (bottom_blob_scale(int8) * weight_data_scale(int8))
  • 反量化 dequantize

    1
    2
    // 反量化因子
    dequantize_scale = 1 / bottom_blob_scale(int8) * weight_data_scale(int8))
    1
    2
    3
    4
    5
    // 前向推理
    bottom_blob(fp32) * weight_data(fp32) = bottom_blob(int8) * weight_blob(int8) * dequantize_scale
    // 前向推理可以分为两步:
    inner_blob(int32) = bottom_blob(int8) * weight_blob(int8)
    inner_blob(fp32) = inner_blob(int32) * dequantize_scale
  • requantize
    当Conv1后紧随Conv2时,NCNN会进行requantize的操作,也就是在得到Conv1的int32输出后,
    会直接帮Conv2做量化,而不会输出Conv1反量化的fp32结果,节省了一次内存读写。
    流程如下图:

    image-20210721195810590

    1
    2
    3
      设Conv1的反量化Scale = 1 / (s1 * s2), Conv2的输入量化scale = s3
    则requantize的计算公式为:
    Conv2_input(int8) = (Conv1_Inner(int32) * (1 / s1*s2) + Conv1_Bias) * s3
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 代码实现:
    for (int i=0; i<size; i++)
    {
    ptr[i] = float2int8(((intptr[i] * scale_in) + bias) * scale_out);
    if (fusion_relu && ptr[i] < 0)
    ptr[i] = 0;
    }
    // requantize不再存储Conv1反量化的结果,而是直接算出Conv2的量化结果存储,省去了一次读写
    // Relu激活函数也被合并入requantize过程中

NCNN量化算法

  • 如何将一个layer的权重值映射到-127127之间?如果使用min-max映射的话,可知如果数据分布并不均匀,映射到-127127后,有效表示范围将会更少,会使得精度损失很大。
    那么往往使用下图方式:

    image-20210721195855952
  • 找到一个阈值T,将大于T或小于-T的数据都映射到极值(127,-127)上,与主成分降维思想类似,丢弃无用的高频细节,将映射注意力集中在分布密集区域,从而寻求更优的量化映射。

  • 那么如何寻找最优的阈值T呢,NCNN与NVIDIA的做法一致,采用KL-divergence,也就是相对熵,来对量化前后的数据分布差异程度做衡量。
    相对熵公式
    image-20210721200024788

    事件q:(int8)分布的概率,事件p:(fp32)分布的概率

    ​ 对于int值,计算概率分布时候可以求hist直方图,但对于float数据集合,需要界定边界以对数据归类,NVIDIA采用分bin的方式,将float32的数据分为2048个bin。

    NVIDIA运算流程如下:

    image-20210721200217056

    首先准备一个校准数据集,然后对每一层:

    • 收集激活值的直方图
    • 基于不同的阈值产生不同的量化分布
    • 计算每个量化分布与原分布的相对熵,选择熵最小的一个,其阈值作为选定阈值。
  • NVIDIA校准算法(计算输入的scale),NCNN基本一致:

    image-20210721200217056

    • 在校准数据集P上,对float32的数据分布取2048个bin, 从128开始,尝试对其截断
    • for i in range(128, 2048):
      • 将截断区外的值全部求和
      • 截断区外累积的和加到截断样本P的最后一个值上
      • 求得样本P截断后的概率分布
      • 创建样本Q,其值为对样本P截断后的量化值
      • 将Q的长度拓展到i,使得和样本P截断后的长度一致
      • 求得样本Q的概率分布
      • 求截断样本P、Q的KL散度
    • 不断尝试截断样本P,构造截断P和Q,并计算二者相对熵,并找到最小相对熵时的截断长度m
    • 最终求出最优的scale:
      1
      2
      3
      interval = max_value / 2048
      T = (m + 0.5) * interval
      Scale = T / 127

NCNN量化操作

  1. 生成量化表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ./ncnn2table --images=<> --param=<> --bin=<> --output=<> --size=<> --thread=<> --mean=<> --normal=<>
    --images: 量化样本路径
    --param: ncnn模型参数文件
    --bin: ncnn模型文件
    --output: 生成量化表路径
    --size: 输入样本shape
    --mean: 输入样本均值
    --normal: 输入样本方差
    --thread: 启用线程数
  • ncnn2table默认只接受三通道输入图片,需手动更改以接受单通道输入
  • 归一化方式可以修改以适配场景
  1. 量化模型
    1
    ./ncnn2int8 [in_param] [in_bin] [out_param] [out_bin] [calibration_tablel]