4、模型量化原理:FP32 vs INT8 vs INT4,量化误差的来源,对称量化与非对称量化
说到边缘AI部署,量化是个绕不开的话题。我刚开始接触这块时,也觉得量化就是把32位浮点数砍成8位整数,简单得很。结果第一次在STM32上跑量化模型,精度直接掉了5个点,当时就懵了。后来才明白,量化这件事,门道比想象中深得多。
4.1 为什么非要用INT8和INT4?
先说说FP32。FP32是32位浮点数,精度高,动态范围大。但问题也明显——占内存,算得慢。在边缘设备上,比如Cortex-M系列芯片,FP32的乘加运算一次要好几周期,内存带宽也跟不上。
INT8就不一样了。8位整数,内存占用只有FP32的四分之一。计算速度呢?我实测过,在ARM的DSP指令集上,INT8的矩阵乘法比FP32快3到5倍。INT4更夸张,内存再砍一半,速度还能再提一截。
你想想看,一个2MB的模型,量化成INT8就变成512KB。这在资源紧张的MCU上,差别就是能不能跑得动的问题。
核心收益:
- 内存占用:FP32 → INT8 减少75%,INT4 减少87.5%
- 计算速度:INT8 比 FP32 快3-5倍,INT4 更快但精度损失更大
- 功耗:整数运算比浮点运算省电得多
4.2 量化到底是怎么做的?
量化的本质,就是把一个连续的数值范围映射到离散的整数集合上。说白了,就是找个缩放因子,把浮点数乘一下再取整。
公式其实不复杂:
量化值 = round(浮点值 / 缩放因子) + 零点偏移
反量化就是反过来:
浮点值 = (量化值 - 零点偏移) × 缩放因子
嗯,这里要注意,缩放因子和零点偏移怎么选,直接决定了量化误差的大小。我在项目中遇到过,有人直接用最大值最小值来算缩放因子,结果模型精度崩了。为什么?因为有个别离群点把范围拉得太宽,导致大部分数值的量化精度不够。
4.3 对称量化 vs 非对称量化
这两种方式,说白了就是零点偏移要不要的问题。
对称量化
对称量化把零点固定在0。也就是说,浮点数的正负范围对称地映射到整数的正负范围。比如INT8,范围是[-128, 127],零点就是0。
好处是计算简单,没有零点偏移的额外开销。坏处呢?如果数据分布不对称,比如ReLU后的激活值全是非负的,那对称量化就会浪费一半的整数范围。
// 对称量化示例
float scale = max(abs(min_val), abs(max_val)) / 127.0f;
int8_t quantized = round(float_val / scale);
非对称量化
非对称量化允许零点偏移。这样就能把浮点数的实际范围完整映射到整数范围上,不浪费任何编码空间。
我个人的习惯是,权重用对称量化,激活值用非对称量化。为什么?权重通常分布比较对称,而激活值经过ReLU后全是非负的,非对称量化能更好地利用INT8的256个等级。
// 非对称量化示例
float scale = (max_val - min_val) / 255.0f;
int zero_point = round(-min_val / scale);
uint8_t quantized = round(float_val / scale) + zero_point;
我的经验: 在MobileNet这类轻量网络上,对称量化通常够用。但在YOLO这种检测网络里,激活值分布很复杂,非对称量化能多保住1-2个点的mAP。具体选哪种,建议先跑个校准集看看数据分布。
4.4 量化误差的来源
量化不是无损的,误差来源主要有三个:
| 误差类型 | 原因 | 影响程度 |
|---|---|---|
| 截断误差 | 超出量化范围的数值被截断 | 大,尤其是有离群点时 |
| 舍入误差 | 浮点数到整数的取整操作 | 中等,通常可控 |
| 精度损失 | 量化步长导致的数值分辨率下降 | 小到中等,取决于位宽 |
我曾经在一个语音识别模型上踩过坑。模型里有个层的权重范围是[-0.001, 0.001],但有个离群点是0.5。用对称量化时,缩放因子被这个离群点拉大了500倍,导致大部分权重量化后都变成了0。模型直接废了。
后来怎么解决的?我用了per-channel量化,每个输出通道单独算缩放因子。这样离群点只影响它自己的通道,其他通道的精度保住了。
避坑指南: 我曾经在量化一个BERT模型时,发现INT8精度掉了3个点。排查了半天,发现是LayerNorm层的输入范围太大,量化后信息丢失严重。解决方案是把LayerNorm留在FP32计算,或者用INT16做中间计算。记住,不是所有层都适合量化到INT8。
4.5 INT4量化:更极致的压缩
INT4量化,每个数值只占4位。内存再减一半,但精度损失也更明显。我一般在资源极度受限的场景才用,比如耳机里的语音唤醒模型。
INT4的挑战在于:
- 量化步长更大,精度损失更严重
- 硬件支持有限,很多芯片没有INT4的指令
- 需要更精细的校准策略
我的做法是,先用INT8量化,然后对某些层做INT4。比如全连接层可以试试INT4,但卷积层最好保留INT8。这样能在精度和压缩率之间找到平衡。
4.6 量化感知训练
如果后训练量化效果不好,那就得上量化感知训练(QAT)了。说白了,就是在训练过程中模拟量化操作,让模型自己去适应量化误差。
QAT的核心是直通估计器(STE)。前向传播时做量化,反向传播时假装量化不存在,梯度直接通过。这样模型就能学会在量化条件下保持精度。
# 伪代码:量化感知训练中的前向传播
def forward(x, scale, zero_point):
# 量化
x_q = torch.round(x / scale + zero_point)
# 截断到INT8范围
x_q = torch.clamp(x_q, -128, 127)
# 反量化,用于后续计算
x_dq = (x_q - zero_point) * scale
return x_dq
我建议,如果模型精度要求高,或者模型本身比较敏感(比如检测、分割任务),直接用QAT。虽然训练时间会多花20%-30%,但换来的是几乎无损的INT8模型,值了。
4.7 总结一下
量化不是简单的数值转换,而是一个系统工程。选对称还是非对称,用INT8还是INT4,要不要做QAT,都得根据你的硬件、模型和精度要求来定。
我个人习惯的流程是:先跑后训练量化看看效果,如果精度损失在1%以内就直接用。超过1%就试试per-channel量化或者混合精度。还不行就上QAT。这套流程在十几个项目里都验证过,挺稳的。
最后说一句,量化工具现在很成熟了,TensorRT、ONNX Runtime、TFLite都支持。但工具只是工具,理解原理才能用好它。下次遇到量化精度问题,你知道该从哪里排查了吧?