4、模型量化入门:为什么需要量化、量化原理(对称/非对称)、量化参数计算
各位同学,欢迎来到第四章。
前面几章我们聊了怎么在Cortex-M上搭环境、跑裸机程序。但说实话,那都是热身。真正让神经网络在MCU上跑起来的关键,就是今天要讲的——模型量化。
我刚开始做嵌入式AI那会儿,也天真地以为,把训练好的模型直接扔到芯片上就能跑。结果呢?一个几MB的模型,Cortex-M4的RAM才128KB,根本塞不进去。后来我才明白,不量化,寸步难行。
4.1 为什么需要量化?
先问大家一个问题:你训练好的模型,参数是什么类型的?
绝大多数是float32,也就是32位浮点数。一个参数占4个字节。一个100万参数的模型,光权重就要4MB。而Cortex-M系列的SRAM,通常只有几十到几百KB。Flash大一点,但也不够塞。
更关键的是,Cortex-M4/M7虽然有FPU(浮点运算单元),但跑浮点乘加的速度,远不如跑整数运算。而且很多低成本的M0/M3,压根没有FPU。你让它们跑浮点,那速度简直没法看。
所以,量化的核心目的就两个:
- 压缩模型体积:把float32变成int8,体积直接缩小4倍。
- 加速推理速度:整数运算比浮点快得多,而且能利用SIMD指令(比如SMLAD)一次算好几个。
我在一个项目里,把MobileNetV1从float32量化到int8,模型从4.2MB降到了1.1MB。在STM32F407上跑一次推理,从原来的2.3秒降到了0.6秒。效果立竿见影。
量化不是可选项,而是必选项。 想在Cortex-M上跑神经网络,量化是绕不开的一步。
4.2 量化原理:对称 vs 非对称
量化说白了,就是用一个低精度的整数,去近似表示一个高精度的浮点数。最常见的做法,就是把float32映射到int8(-128 ~ 127)或者uint8(0 ~ 255)。
怎么映射?有两种主流方法:对称量化和非对称量化。
4.2.1 对称量化
对称量化,顾名思义,就是零点(zero-point)在0的位置。也就是说,浮点数的0,映射到整数的0。
它的映射公式很简单:
q = round(r / scale)
其中,r是浮点数,q是量化后的整数,scale是缩放因子。
反量化公式:
r = q * scale
你看,没有零点偏移,计算起来非常干净。我个人的习惯是,如果模型的权重分布比较对称(比如经过BatchNorm后的激活值),我会优先用对称量化。
但对称量化有个问题:如果浮点数的范围是[-1.0, 100.0],那么对称量化会把范围映射到[-128, 127]。但实际数据大部分都在正数区域,负数区域几乎没用,这就浪费了int8的表示能力。
4.2.2 非对称量化
非对称量化引入了零点偏移(zero-point),让映射更灵活。
公式如下:
q = round(r / scale) + zero_point
反量化:
r = (q - zero_point) * scale
这里的zero_point是一个整数,它对应浮点数中的0。比如,浮点数范围是[-1.0, 100.0],我们可以把-1.0映射到0,100.0映射到255。那么zero_point就是0,scale就是(100.0 - (-1.0)) / (255 - 0) ≈ 0.396。
非对称量化能更好地利用整数的全部范围。尤其是对于ReLU后的激活值(全是非负数),用非对称量化映射到uint8,效率很高。
我的建议: 权重通常用对称量化(int8),激活值通常用非对称量化(uint8)。这是TensorFlow Lite和CMSIS-NN的默认做法,也是经过大量实践验证的。
4.3 量化参数计算
量化参数就是scale和zero_point。怎么算?我们一步步来。
4.3.1 计算scale和zero_point
假设我们有一组浮点数,最小值是r_min,最大值是r_max。我们要映射到整数范围[q_min, q_max]。
对于int8,q_min = -128,q_max = 127。
对于uint8,q_min = 0,q_max = 255。
计算公式:
scale = (r_max - r_min) / (q_max - q_min)
zero_point = round(q_min - r_min / scale)
注意,zero_point必须是一个整数,而且要在[q_min, q_max]范围内。
举个例子:
假设激活值范围是[0.0, 6.0],我们要量化到uint8。
scale = (6.0 - 0.0) / (255 - 0) = 6.0 / 255 ≈ 0.02353
zero_point = round(0 - 0.0 / 0.02353) = round(0) = 0
所以,量化参数就是scale=0.02353,zero_point=0。
再举一个对称量化的例子:
权重范围是[-1.5, 1.5],量化到int8。
scale = (1.5 - (-1.5)) / (127 - (-128)) = 3.0 / 255 ≈ 0.01176
zero_point = round(-128 - (-1.5) / 0.01176) = round(-128 + 127.55) = round(-0.45) = 0
你看,对称量化下,zero_point就是0。
4.3.2 量化后的矩阵乘法
量化不只是把参数存成整数,推理时的计算也要在整数域进行。以矩阵乘法为例:
假设我们有两个浮点矩阵A和B,量化后得到qA和qB,对应的scale和zero_point分别是scaleA, zpA和scaleB, zpB。
浮点矩阵乘法:
C = A * B
量化后的计算:
qC = zpC + (scaleA * scaleB / scaleC) * (qA - zpA) * (qB - zpB)
这个公式看着复杂,但实际实现时,我们会把(scaleA * scaleB / scaleC)这个浮点数预先算好,变成一个定点数。然后整个计算就变成了整数乘加。
嗯,这里要注意:量化推理的精度,很大程度上取决于scale和zero_point的选取。 如果范围选得太宽,精度损失大;选得太窄,容易溢出。
我曾经踩过的坑: 有一次我偷懒,直接用训练集的全局最大最小值来计算scale。结果推理时遇到一个异常大的激活值,直接导致量化后的int8溢出,输出全乱套了。后来我改用校准集(calibration dataset)来统计范围,并且加了饱和截断(比如只保留99.9%的数据范围),才解决了问题。
4.4 实战中的量化策略
在实际项目中,量化通常有两种方式:
- 训练后量化(Post-Training Quantization, PTQ):模型训练完后,用一小部分校准数据统计范围,然后直接量化。简单快速,适合大多数场景。
- 量化感知训练(Quantization-Aware Training, QAT):在训练过程中模拟量化误差,让模型学会适应低精度。精度更高,但需要重新训练。
我个人建议,对于Cortex-M上的部署,优先尝试PTQ。如果精度损失在1%以内,直接用。如果损失太大,再考虑QAT。
另外,CMSIS-NN库已经对int8和uint8的量化推理做了深度优化。你只需要提供量化后的权重和激活值,它就能高效地跑起来。
| 量化类型 | 适用场景 | 精度损失 | 工作量 |
|---|---|---|---|
| 对称量化(int8) | 权重、对称分布的激活值 | 较小 | 低 |
| 非对称量化(uint8) | ReLU后的激活值 | 更小 | 低 |
| 训练后量化(PTQ) | 快速部署 | 1%~3% | 低 |
| 量化感知训练(QAT) | 高精度要求 | <1% | 高 |
好了,这一章的内容就到这里。量化是嵌入式AI的基石,理解了它,你就能看懂后面CMSIS-NN的代码了。下一章,我们手把手教你如何用CMSIS-NN实现一个量化后的卷积层。