模型量化基础:量化的数学原理与工程实践
量化这个话题,说实话,是嵌入式AI里最让我又爱又恨的技术。爱的是它带来的速度提升太明显了,恨的是稍不注意精度就崩了。今天咱们就把量化的底裤扒干净,看看它到底是怎么工作的。
一、为什么需要量化?
先问个问题:你手上的MCU或者NPU,跑float32模型能跑多快?我猜大概率是慢得让人抓狂。我做过一个项目,在Cortex-M4上跑一个简单的分类模型,float32版本推理一次要800ms,量化到int8后直接降到120ms。这差距,你想想看。
量化的核心思想很简单:用更少的比特数来表示模型参数和中间激活值。说白了,就是把float32的权重和激活值,映射到int8甚至int4的整数空间里。这样做的好处有两个:
- 模型体积缩小:int8比float32小4倍,模型加载更快
- 推理速度提升:整数运算比浮点运算快得多,而且能利用硬件加速单元
关键数据对比:在STM32H743上测试MobileNetV1,float32推理耗时450ms,int8量化后仅需95ms,速度提升约4.7倍。而精度从70.2%下降到68.9%,只掉了1.3个百分点。这个trade-off,我觉得很值。
二、量化的数学原理
量化本质上是一个映射过程。我们需要把浮点数范围映射到整数范围。这里有两种主流方案:对称量化和非对称量化。
2.1 对称量化
对称量化,名字就说明了特点——以零点为中心,正负对称。映射公式是这样的:
// 对称量化公式
scale = max(|x_min|, |x_max|) / (2^(n-1) - 1)
quantized_value = round(x / scale)
// 反量化公式
dequantized_value = quantized_value * scale
举个例子,假设你的权重范围是[-1.5, 1.5],要量化到int8(范围-127到127):
scale = 1.5 / 127 ≈ 0.0118
// 浮点数0.5量化后:
q = round(0.5 / 0.0118) = round(42.37) = 42
// 反量化回来:
d = 42 * 0.0118 = 0.4956
嗯,这里要注意:对称量化有个隐含假设——数据分布是对称的。但我在实际项目中遇到过,ReLU后的激活值全是非负的,这时候用对称量化就会浪费一半的整数范围。怎么办?看非对称量化。
2.2 非对称量化
非对称量化引入了零点偏移(zero point),可以灵活处理任意分布的数据:
// 非对称量化公式
scale = (x_max - x_min) / (q_max - q_min)
zero_point = round(q_min - x_min / scale)
quantized_value = round(x / scale + zero_point)
// 反量化
dequantized_value = (quantized_value - zero_point) * scale
还是刚才那个ReLU后的激活值,范围是[0.0, 3.2],量化到uint8(0到255):
scale = (3.2 - 0.0) / (255 - 0) = 0.01255
zero_point = round(0 - 0.0 / 0.01255) = 0
// 浮点数1.5量化后:
q = round(1.5 / 0.01255 + 0) = round(119.5) = 120
我的经验:对于权重,我习惯用对称量化,因为训练好的权重通常分布比较对称。对于激活值,尤其是ReLU后的,非对称量化效果更好。曾经有个项目,激活值用对称量化精度掉了3%,换成非对称量化只掉了0.5%。
三、量化对精度的影响
量化一定会损失精度,这是物理定律决定的。但损失多少,取决于几个因素:
| 影响因素 | 影响程度 | 说明 |
|---|---|---|
| 量化位宽 | 高 | int8比int4精度好很多,int4需要特殊处理 |
| 数据分布 | 中 | 分布越均匀,量化损失越小 |
| 量化粒度 | 高 | per-channel量化比per-tensor好 |
| 校准数据集 | 中 | 校准集要能代表真实数据分布 |
我遇到过最惨的一次,模型量化后精度从92%掉到了67%。排查了半天,发现是某个层的激活值范围特别大(-100到100),而其他层只有-5到5。用全局scale量化,小值直接被截断成0了。后来改成per-channel量化,精度回到了89%。
避坑指南:我曾经在量化一个检测模型时,发现某些通道的权重范围差异巨大。如果使用per-tensor量化,那些小范围的通道几乎被完全抹平。后来我改用per-channel量化,每个输出通道单独计算scale和zero_point,精度才保住。所以,对于卷积层,强烈建议使用per-channel量化。
四、量化对推理速度的提升
量化为什么能提速?说白了就三点:
- 内存带宽降低:int8数据量是float32的1/4,内存搬运时间大幅减少
- 计算效率提升:整数乘法器比浮点乘法器快,而且很多芯片有SIMD指令加速
- 缓存命中率提高:模型变小了,更多参数能塞进缓存
我在一个实际项目里测过,结果是这样的:
// 在RK3588上测试YOLOv5s
// float16推理:35ms
// int8量化推理:12ms
// 速度提升:约2.9倍
// 精度变化:mAP从0.723降到0.708
你可能会问:为什么不是理论上的4倍?因为还有反量化操作的开销。每次卷积计算前,需要把int8权重和激活值反量化回浮点,或者用整数运算模拟浮点计算。这个开销会吃掉一部分性能提升。
优化技巧:我习惯把卷积层的权重和偏置提前量化好,推理时只做整数运算,最后再统一反量化。这样能减少反复量化的开销。另外,尽量使用硬件支持的量化格式,比如有些NPU只支持对称量化,你用非对称量化反而会慢。
五、量化方法的选择建议
说了这么多,到底该怎么选?我个人的经验是这样的:
- 权重量化:优先用对称量化 + per-channel,简单高效
- 激活值量化:用非对称量化 + per-tensor,因为激活值分布变化大
- 偏置量化:用int32或float32,精度敏感,别省这点空间
- 量化位宽:新手先从int8开始,int4是进阶玩法
最后说一句:量化不是银弹。有些模型天生对量化敏感,比如含有大量BatchNorm层的模型。我建议你先做小规模实验,量化一个子网络看看精度变化,再决定是否全盘量化。嗯,这个习惯帮我避免了好几次翻车。