Linux系统上常用软件集锦
[TOC]
概述
量化背景
- 随着神经网络层数的不断加深,网络体积与计算量也在不断增大。
对于实时性要求较高的模型计算,及移动端的ai需求,设备算力可能不足以支持运算需求。
对模型的权重进行量化可以有效的加速运算,减少资源开销。
低比特量化技术
- 为什么CNN可以使用量化模型做推理
- 由于pooling层、激活函数层(如relu),早期的DropOut层的存在,CNN网络结构对于噪声是不敏感的。
因此在卷积神经网络模型的前向计算时,可以使用低精度的数值类型(float16/int8)来表示模型训练所使用的float32浮点数值。
- 由于pooling层、激活函数层(如relu),早期的DropOut层的存在,CNN网络结构对于噪声是不敏感的。
- 量化的收益
- 以int8量化为例,量化后的模型体积将为量化前的1/4,减少了存储压力及访存压力
- 定点运算相较于浮点运算,模型运算速度提升明显
- 量化的问题与难点
- 核心是精度损失
- 低比特计算会带来精度的损失
- 量化对于模型权重分布的描述不能完全准确
- 相较于分类任务,回归任务精度损失将更大
- 深层网络会带来更大的精度损失
- DepthWise对于量化不友好
- 同时也需考虑软硬件支持
- 不同硬件支持的低比特指令不一致
- 不同神经网络框架提供的量化方法不一致
- 低比特计算会带来精度的损失
- 核心是精度损失
以NCNN为例的int8量化
与Nvidia的TensorRT一致,ncnn采用无需retrain的直接量化方案
量化原理:目的是将float32的卷积操作转换为int8的卷积操作,这样计算量就变为原来的1/4
对于每一层的网络参数,float32与int8的对应原理如下图所示也就是将一个网络层的权重值按照min
max范围,按照一定比例,映射到-127127范围内
公式如下:FP32 Tensor (T) = scale * 8-bit Tensor(t) + FP32_bias (b)
可见ncnn,ncnn仅对weights做量化,并未对bias做量化ncnn Conv int8计算流程,如下图
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_scalerequantize
当Conv1后紧随Conv2时,NCNN会进行requantize的操作,也就是在得到Conv1的int32输出后,
会直接帮Conv2做量化,而不会输出Conv1反量化的fp32结果,节省了一次内存读写。
流程如下图:1
2
3设Conv1的反量化Scale = 1 / (s1 * s2), Conv2的输入量化scale = s3
则requantize的计算公式为:
Conv2_input(int8) = (Conv1_Inner(int32) * (1 / s1*s2) + Conv1_Bias) * s31
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的权重值映射到-127
127之间?如果使用min-max映射的话,可知如果数据分布并不均匀,映射到-127127后,有效表示范围将会更少,会使得精度损失很大。
那么往往使用下图方式:找到一个阈值T,将大于T或小于-T的数据都映射到极值(127,-127)上,与主成分降维思想类似,丢弃无用的高频细节,将映射注意力集中在分布密集区域,从而寻求更优的量化映射。
那么如何寻找最优的阈值T呢,NCNN与NVIDIA的做法一致,采用KL-divergence,也就是相对熵,来对量化前后的数据分布差异程度做衡量。
相对熵公式事件q:(int8)分布的概率,事件p:(fp32)分布的概率
对于int值,计算概率分布时候可以求hist直方图,但对于float数据集合,需要界定边界以对数据归类,NVIDIA采用分bin的方式,将float32的数据分为2048个bin。
NVIDIA运算流程如下:
首先准备一个校准数据集,然后对每一层:
- 收集激活值的直方图
- 基于不同的阈值产生不同的量化分布
- 计算每个量化分布与原分布的相对熵,选择熵最小的一个,其阈值作为选定阈值。
NVIDIA校准算法(计算输入的scale),NCNN基本一致:
- 在校准数据集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
3interval = max_value / 2048
T = (m + 0.5) * interval
Scale = T / 127
NCNN量化操作
- 生成量化表
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
./ncnn2int8 [in_param] [in_bin] [out_param] [out_bin] [calibration_tablel]