3. 均匀量化与非线性量化:对称量化、非对称量化、对数量化、查表量化
好,咱们今天聊聊量化。这玩意儿在嵌入式部署里,可以说是“兵家必争之地”。模型再大,算力再强,量化做不好,上板子就是白搭。我个人习惯把量化分成两大类:均匀量化和非线性量化。说白了,就是看你怎么把连续的浮点数,映射到离散的整数点上。
3.1 均匀量化:简单粗暴,但够用
均匀量化,顾名思义,就是量化步长是固定的。你想想看,就像拿一把刻度均匀的尺子去量东西。这把尺子的最小刻度,就是我们的量化步长(Scale)。
均匀量化里,又分两种:对称量化和非对称量化。我刚开始接触时,也经常搞混。后来在项目里踩过坑,才真正理解它们的区别。
3.1.1 对称量化
对称量化,核心思想就是让零点(Zero Point)对齐到浮点数的0。也就是说,浮点数的0,量化后一定是整数0。这样做的好处是啥?计算简单啊!
公式长这样:
// 对称量化公式
// r 是浮点数,q 是量化后的整数,S 是缩放因子
// r = S * q
// 量化过程
S = max(|r_max|, |r_min|) / (2^(n-1) - 1)
q = round(r / S)
// 反量化过程
r = S * q
你看,没有零点偏移,乘除法就搞定了。我在做MobileNet部署时,就特别喜欢用对称量化。因为MobileNet的权重和激活值分布,通常比较对称,用对称量化几乎不掉点。
3.1.2 非对称量化
非对称量化就灵活多了。它允许零点不在0上,可以偏移。公式里多了一个Zero Point(Z)。
// 非对称量化公式
// r = S * (q - Z)
// 量化过程
S = (r_max - r_min) / (2^n - 1)
Z = round(-r_min / S)
q = round(r / S + Z)
// 反量化过程
r = S * (q - Z)
为什么要用非对称?因为很多模型的激活值,比如ReLU后的输出,全是非负的。这时候你用对称量化,有一半的量化范围就浪费了。非对称量化可以把整个量化范围,都用来表示正数,精度自然就高了。
3.2 非线性量化:精度不够,花样来凑
均匀量化虽然简单,但有个硬伤:它对大数值和小数值的精度是一样的。但实际中,我们往往更关心小数值的精度。比如权重里,很多值都集中在0附近。这时候,非线性量化就派上用场了。
3.2.1 对数量化
对数量化,说白了,就是在对数域里做量化。它的步长不是固定的,而是随着数值增大而增大。这样,小数值的精度就高了。
公式大概这样:
// 对数量化(以2为底)
// q = round(log2(|r|))
// 反量化时:r = sign(r) * 2^q
// 实际实现时,通常用查表法
// 因为2的幂次运算,在硬件上可以转换成移位操作
对数量化有个好处:乘法可以变成加法。因为 log(a * b) = log(a) + log(b)。这在硬件上,能省不少乘法器。我记得在某个语音唤醒词项目里,就用过对数量化,把模型大小压到了原来的1/4,功耗也降了不少。
3.2.2 查表量化
查表量化,可以说是非线性量化的“终极形态”。它不限制你用什么映射关系,你甚至可以手工设计。只要把映射关系存成一张表,量化时查表就行。
举个例子,你想把浮点数映射到0-255的整数,但希望小数值的量化步长更小。你可以这样设计:
// 查表量化示例
// 假设我们设计一个映射表,让小数值的步长更小
// 1. 设计映射关系(比如:q = floor(sqrt(r) * 16))
// 2. 预计算好一张表:float_to_int[float_index] = q
// 3. 量化时,直接查表
// 反量化时,也需要一张表:int_to_float[q] = r
// 实际部署时,这两张表通常放在Flash里
查表量化的好处是灵活,你可以针对你的模型,设计最优的映射。但坏处也明显:需要额外的存储空间来存表。而且,如果表太大,查表速度也会受影响。
3.3 如何选择?
说了这么多,到底该用哪种?我一般按这个思路来:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 权重分布对称(如分类网络) | 对称量化 | 计算简单,精度损失小 |
| 激活值非负(如ReLU后) | 非对称量化 | 充分利用量化范围,精度更高 |
| 数值分布范围广,小值重要 | 对数量化 | 小值精度高,乘法变加法 |
| 特殊分布,精度要求极高 | 查表量化 | 最灵活,可定制 |
嗯,这里要注意一点:没有银弹。每种量化方法都有它的适用场景。我建议你在项目初期,先用均匀量化(对称或非对称)试试水。如果精度达标,就别折腾了。如果精度差那么一点,再考虑非线性量化。
最后,送大家一句话:量化不是目的,部署才是。别为了量化而量化,一切以最终效果为准。