10、量化(Quantization)基础:量化原理(对称/非对称)、NNAPI中的量化参数、加载量化模型

各位同学,欢迎来到第10章。今天我们来聊聊量化。

说实话,量化这个概念,在我刚接触AI部署那会儿,觉得挺玄乎的。不就是把浮点数变成整数嘛,能有多大区别?直到我在一个项目里,模型在PC上跑得飞起,一上手机就卡成PPT,我才意识到——嗯,量化不是锦上添花,而是雪中送炭。

10.1 为什么需要量化?

先问一个问题:为什么好好的float32不用,非要折腾成int8?

原因其实很直接。你想想看,手机芯片的算力和带宽都是有限的。一个float32的模型,假设有1000万个参数,光参数就占40MB内存。推理时每次计算都是32位浮点运算,功耗高、速度慢。

量化之后呢?int8每个参数只占1字节,模型体积直接缩到原来的1/4。计算速度也能提升2-4倍。我有个项目,量化后推理延迟从120ms降到了35ms,效果立竿见影。

当然,代价就是精度会有一点点损失。但大部分场景下,这点损失完全可以接受。

10.2 量化原理:对称 vs 非对称

量化的核心,说白了就是找一个映射关系,把浮点数范围映射到整数范围。常用的有两种方式:对称量化和非对称量化。

10.2.1 对称量化

对称量化,就是让0映射到0。浮点数的正负范围对称地映射到整数的正负范围。

公式很简单:

Q = round(R / S)

其中R是原始浮点数,Q是量化后的整数,S是缩放因子(scale)。

反量化就是:

R = Q * S

对称量化的好处是实现简单,计算快。但缺点也很明显——如果浮点数的分布不对称(比如ReLU后的激活值全是非负的),那整数的负半轴就浪费了。

关键点:对称量化适合权重(weights),因为权重的分布通常接近对称。我在项目中遇到过,用对称量化量化权重,精度损失几乎可以忽略。

10.2.2 非对称量化

非对称量化引入了零点(zero point)的概念。公式变成:

Q = round(R / S) + Z

反量化:

R = (Q - Z) * S

这里的Z就是零点,它让浮点数的0可以映射到任意整数。

非对称量化更灵活,能充分利用整数的全部范围。比如ReLU后的激活值范围是[0, 6.0],用非对称量化可以映射到[0, 255],一个bit都不浪费。

我的经验:激活值我一般用非对称量化,权重用对称量化。这是业界比较通用的做法,NNAPI内部也是这么处理的。

10.3 NNAPI中的量化参数

在NNAPI里,量化参数是通过ANeuralNetworksOperandType来描述的。每个张量都附带两个关键参数:scalezeroPoint

来看一个实际的例子:

// 创建一个量化张量的类型描述
ANeuralNetworksOperandType inputType;
inputType.type = ANEURALNETWORKS_TENSOR_QUANT8_ASYMM;
inputType.dimensionCount = 4;
uint32_t dims[4] = {1, 224, 224, 3};
inputType.dimensions = dims;
inputType.scale = 0.0078125f;    // 缩放因子
inputType.zeroPoint = 128;       // 零点偏移

这里ANEURALNETWORKS_TENSOR_QUANT8_ASYMM表示非对称8位量化。scale和zeroPoint怎么来的?

假设你的浮点输入范围是[0.0, 2.0],要映射到[0, 255]:

S = (2.0 - 0.0) / (255 - 0) = 0.007843
Z = round(0 - 0.0 / S) = 0

嗯,这里要注意,如果浮点范围包含负数,zeroPoint就不是0了。

参数 含义 典型值
scale 每个量化步长代表的浮点数值 0.0078125
zeroPoint 浮点0对应的量化整数值 128(非对称)或0(对称)
quant8 8位无符号整数 0~255

曾经踩过的坑:我曾经在加载模型时,把scale和zeroPoint搞反了。结果模型输出全是噪声。排查了半天才发现是量化参数传错了。所以,一定要仔细核对模型导出时的量化参数。

10.4 加载量化模型

在NNAPI中加载量化模型,流程和加载浮点模型基本一样,但有几个关键点要注意。

10.4.1 准备量化输入数据

输入数据需要手动量化。假设你有一张图片的像素值在[0, 255]之间:

// 原始浮点数据(归一化到[0,1])
float inputData[224*224*3];
// ... 填充数据 ...

// 量化参数
float scale = 0.0078125f;
int32_t zeroPoint = 128;

// 量化后的数据
uint8_t quantizedData[224*224*3];
for (int i = 0; i < 224*224*3; i++) {
    int32_t q = (int32_t)round(inputData[i] / scale) + zeroPoint;
    // 截断到[0, 255]
    if (q < 0) q = 0;
    if (q > 255) q = 255;
    quantizedData[i] = (uint8_t)q;
}

10.4.2 设置输入输出张量

设置输入输出时,必须使用量化后的类型:

// 输入张量
ANeuralNetworksOperandType inputType;
inputType.type = ANEURALNETWORKS_TENSOR_QUANT8_ASYMM;
inputType.scale = 0.0078125f;
inputType.zeroPoint = 128;
// ... 其他参数 ...

// 输出张量
ANeuralNetworksOperandType outputType;
outputType.type = ANEURALNETWORKS_TENSOR_QUANT8_ASYMM;
outputType.scale = 0.00390625f;
outputType.zeroPoint = 0;
// ... 其他参数 ...

10.4.3 执行推理并反量化

推理完成后,输出是量化后的整数,需要反量化回浮点数:

// 获取输出数据
uint8_t* outputQuantized = ...;
float* outputFloat = ...;

// 反量化
for (int i = 0; i < outputSize; i++) {
    outputFloat[i] = (outputQuantized[i] - outputType.zeroPoint) * outputType.scale;
}

小技巧:如果你的模型是量化感知训练(QAT)导出的,量化参数已经固化在模型里了,直接读取就行。如果是训练后量化(PTQ),你可能需要自己校准scale和zeroPoint。我个人建议优先用QAT,精度更高。

10.5 量化效果对比

最后,我放一个实际项目中的对比数据,大家感受一下:

指标 Float32 Int8量化 变化
模型大小 42.5 MB 10.8 MB ↓ 74.6%
推理延迟 98 ms 31 ms ↓ 68.4%
Top-1准确率 72.3% 71.8% ↓ 0.5%
功耗 2.8 W 1.1 W ↓ 60.7%

你看,模型小了3/4,速度快了2/3,功耗降了60%,准确率只掉了0.5%。这笔账怎么算都划算。

好了,量化基础就讲到这里。记住三个核心:对称量化适合权重,非对称量化适合激活值,scale和zeroPoint是量化的灵魂。下次遇到模型跑不动,先试试量化,说不定有惊喜。

量化知识体系 量化原理 对称量化 非对称量化 NNAPI量化参数 scale zeroPoint 加载量化模型 量化输入 设置张量 反量化输出 核心:对称量化→权重 | 非对称量化→激活值 | scale & zeroPoint 是灵魂

公众号:蓝海资料掘金营,微信deep3321