4、量化理论基础:对称量化与非对称量化、量化参数(scale/zero_point)、量化公式推导与反量化过程
好,咱们进入第四章。这一章是纯理论,但也是后面所有实战的根基。我见过不少同学,代码跑得飞起,一问他 scale 怎么算出来的,立马卡壳。嗯,这不行。量化参数搞不清楚,后面调精度、修 bug 就会像无头苍蝇。
说白了,量化就是把连续的浮点数,映射到离散的整数空间。为什么要这么干?因为端侧芯片的算力有限,内存也金贵。FP32 的模型跑在手机上,功耗和延迟都扛不住。INT8 计算,速度快 2-4 倍,内存省 75%。代价呢?精度会掉一点。我们的任务就是让这个代价最小化。
4.1 对称量化 vs 非对称量化
先看两种最基本的量化策略。我个人习惯,先看名字猜含义——对称,就是正负范围对称;非对称,就是可以不对称。
4.1.1 对称量化
对称量化,映射关系是 real_value = scale * quantized_value。注意,这里没有 zero_point。也就是说,浮点数的 0,量化后还是 0。正负范围完全对称。
举个例子,假设浮点范围是 [-10.0, 10.0],量化到 INT8(范围 -128 到 127)。那么 scale 就是 (10.0 - (-10.0)) / (127 - (-128)) = 20.0 / 255 ≈ 0.07843。量化后的值就是浮点数除以 scale,再四舍五入。
核心特点:
- zero_point 固定为 0
- 实现简单,计算量小
- 如果浮点分布不对称(比如全是正数),会浪费一半的量化范围
我在项目中遇到过,用对称量化量化 ReLU 后的激活值。ReLU 输出全是非负的,但对称量化硬生生把负半轴也分配了码字。结果就是精度损失比预期大。后来我改用了非对称量化,效果立竿见影。
4.1.2 非对称量化
非对称量化,公式是 real_value = scale * (quantized_value - zero_point)。这里 zero_point 可以不为 0。它允许我们灵活地偏移量化范围,让浮点数的 0 映射到任意整数。
还是刚才的例子,浮点范围 [0.0, 10.0],量化到 INT8。这时候 scale = (10.0 - 0.0) / (127 - (-128)) = 10.0 / 255 ≈ 0.03922。zero_point 呢?我们希望浮点 0 映射到 INT8 的 0,所以 zero_point = 0 - (0.0 / scale) = 0。但等等,如果浮点范围是 [2.0, 12.0] 呢?
这时候 scale = (12.0 - 2.0) / 255 ≈ 0.03922。zero_point = 0 - (2.0 / 0.03922) ≈ -51。也就是说,浮点 0 映射到 INT8 的 -51。你看,zero_point 把整个量化窗口平移了。
我的建议:
对于权重,我通常用对称量化。因为训练好的权重分布往往接近对称。对于激活值,尤其是经过 ReLU 的,我强烈建议用非对称量化。别问为什么,问就是血的教训。
4.2 量化参数:scale 和 zero_point
这两个参数,是量化的灵魂。scale 决定了步长,zero_point 决定了偏移。我们详细拆解一下。
4.2.1 scale(缩放因子)
scale 是一个浮点数,表示量化后的每个整数步长,对应多少浮点数值。公式是:
scale = (r_max - r_min) / (q_max - q_min)
其中 r_max 和 r_min 是浮点数的最大值和最小值,q_max 和 q_min 是量化整数的最大值和最小值(比如 INT8 就是 127 和 -128)。
scale 越小,量化精度越高。但 scale 太小,可能导致量化后的整数范围溢出。这是个 trade-off。
4.2.2 zero_point(零点)
zero_point 是一个整数,表示浮点数 0 映射到哪个量化整数。公式是:
zero_point = q_min - (r_min / scale)
注意,zero_point 需要四舍五入到整数。而且,对于对称量化,zero_point 强制为 0。
避坑指南:
我曾经在量化一个模型时,发现推理结果全错。排查了半天,发现是 zero_point 计算时没有做 clamp。INT8 的 zero_point 必须在 [-128, 127] 范围内。如果计算出来是 -130,必须截断到 -128。否则反量化时直接崩掉。
4.3 量化公式推导与反量化过程
这部分,我建议你拿纸笔跟着推一遍。别嫌麻烦,推完你就再也不会忘了。
4.3.1 量化公式推导
我们想要一个线性映射:
r = S * (q - Z)
其中 r 是浮点数,q 是量化整数,S 是 scale,Z 是 zero_point。
已知两个端点:
- 当 r = r_min 时,q = q_min
- 当 r = r_max 时,q = q_max
代入公式:
r_min = S * (q_min - Z)
r_max = S * (q_max - Z)
两式相减:
r_max - r_min = S * (q_max - q_min)
S = (r_max - r_min) / (q_max - q_min)
然后求 Z:
Z = q_min - (r_min / S)
嗯,就是这么简单。但实际工程中,r_min 和 r_max 怎么选?是取训练集的最大最小值,还是取百分位?这属于校准方法,后面章节会细讲。
4.3.2 反量化过程
反量化,就是把量化后的整数,还原回浮点数。公式就是量化公式的逆运算:
r = S * (q - Z)
注意,反量化后的浮点数,和原始浮点数是有误差的。这个误差叫量化误差,主要来自四舍五入。
举个例子,假设 S=0.1,Z=0,原始浮点 r=3.14159。量化:q = round(3.14159 / 0.1) = round(31.4159) = 31。反量化:r' = 0.1 * (31 - 0) = 3.1。误差 = 3.14159 - 3.1 = 0.04159。
你看,误差就这么产生了。量化位宽越小,S 越大,误差越大。这就是为什么 INT4 比 INT8 精度差的原因。
关键点总结:
- 量化:q = round(r / S) + Z
- 反量化:r = S * (q - Z)
- 对称量化:Z = 0
- 非对称量化:Z ≠ 0
4.4 知识体系结构图
下面这张图,是我自己画的知识结构。你看一眼,就能把本章内容串起来。
4.5 实战中的选择建议
说了这么多理论,最后给点实际建议。你在端侧芯片上做量化推理时,怎么选?
| 场景 | 推荐量化方式 | 原因 |
|---|---|---|
| 权重(Weight) | 对称量化 | 权重分布通常对称,省去 zero_point 计算 |
| 激活值(Activation) | 非对称量化 | ReLU 后全为正数,非对称不浪费码字 |
| 偏置(Bias) | FP32 或 INT32 | 偏置范围大,量化后精度损失严重 |
| 硬件有特殊指令 | 按硬件要求 | 有些芯片只支持对称量化,硬上非对称反而慢 |
个人经验:
我刚开始做量化时,总想着把所有层都量化到 INT8。后来发现,某些敏感层(比如第一层和最后一层)量化后精度掉得厉害。我的做法是:对这些层保持 FP32,其他层用 INT8。这叫混合精度量化,后面章节会详细展开。
好了,这一章的内容就到这里。量化理论是基础中的基础,你把它吃透了,后面看代码、调精度都会轻松很多。记住,scale 和 zero_point 不是随便算的,每一步都有物理意义。下次遇到量化 bug,先检查这两个参数对不对。
公众号:蓝海资料掘金营,微信deep3321