2、量化基础回顾:线性量化与非线性量化、对称量化与非对称量化、量化参数(scale, zero_point)详解
好,咱们正式开始聊量化。说实话,很多同学一上来就盯着各种量化算法看,结果连最基础的参数都没搞明白。我当年刚入行时也踩过这个坑——模型量化完精度掉得一塌糊涂,debug 了三天才发现是 zero_point 算错了。所以今天咱们把地基打牢,把 scale 和 zero_point 这两个家伙彻底搞透。
2.1 量化到底在做什么?
量化,说白了就是把连续的浮点数(比如 float32)映射到离散的整数(比如 int8)。你想想看,一个 float32 的权重占 4 个字节,换成 int8 只占 1 个字节,模型体积直接缩到四分之一。但代价是什么?精度损失。所以我们要做的就是在压缩率和精度之间找平衡。
我个人习惯把量化理解成「用更少的格子去装同样多的东西」。原来你有 2^32 个格子,现在只有 2^8 = 256 个格子。怎么把原来那些值塞进这 256 个格子里?这就是量化的核心问题。
2.2 线性量化 vs 非线性量化
先看第一个分类:线性量化和非线性量化。
线性量化,就是映射关系是一条直线。公式很简单:
q = round(r / scale) + zero_point
其中 r 是原始浮点值,q 是量化后的整数值。scale 和 zero_point 是我们要确定的参数。这种量化方式计算简单,硬件实现也快,所以绝大多数工业级部署都用它。
非线性量化呢?映射关系不是直线,比如对数量化、查表量化。我记得在某个语音模型项目中,尝试过对数量化,精度确实比线性量化好一点,但推理速度慢了将近一倍。为什么?因为非线性量化在硬件上没法直接用整数运算加速,得查表或者做特殊处理。
核心结论:实际工程中,99% 的情况用线性量化就够了。非线性量化更多是学术研究或者特殊场景(比如极低比特量化)才会考虑。
2.3 对称量化 vs 非对称量化
这是第二个分类,也是面试高频题。
对称量化,意思是量化后的整数范围关于 0 对称。比如 int8 的范围是 [-128, 127],对称量化会让浮点数的 0 映射到整数的 0。这时候 zero_point 固定为 0,公式简化为:
q = round(r / scale)
这样做的好处是什么?计算更简单,不需要考虑 zero_point 的偏移。但坏处也很明显——如果浮点数的分布不对称(比如 ReLU 后的激活值全是非负的),对称量化会浪费一半的量化范围。
非对称量化,就是允许 zero_point 不为 0。浮点数的 0 可以映射到任意整数。公式就是前面那个完整的:
q = round(r / scale) + zero_point
我在项目中遇到过这样一个坑:用对称量化去量化 MobileNet 的激活值,精度直接掉了 2 个点。后来换成非对称量化,精度就回来了。为什么?因为 MobileNet 的激活值经过 ReLU6 后,分布集中在 [0, 6] 这个区间,对称量化硬要把范围扩展到 [-6, 6],白白浪费了一半的表示能力。
经验之谈:权重通常用对称量化(因为权重分布一般接近 0 对称),激活值建议用非对称量化(因为 ReLU 后全是非负的)。当然,这也不是绝对的,具体还得看你的模型结构。
2.4 量化参数详解:scale 和 zero_point
好,重头戏来了。scale 和 zero_point 到底怎么算?
scale(缩放因子):决定了每个量化步长代表多大的浮点数范围。公式是:
scale = (r_max - r_min) / (q_max - q_min)
其中 r_max 和 r_min 是浮点数的最大值和最小值,q_max 和 q_min 是量化整数的范围边界。比如 int8 的 q_max=127, q_min=-128。
zero_point(零点偏移):保证浮点数的 0 在量化后能被精确表示。公式是:
zero_point = round(-r_min / scale) + q_min
或者更直观的理解:zero_point 就是浮点数 0 映射到的那个整数。
我建议你记住一个关键点:zero_point 必须是整数。因为量化后的值 q 是整数,所以 zero_point 也得是整数。这就意味着 round 操作会引入误差。嗯,这里要注意,这个误差在极端情况下可能会放大。
曾经踩过的坑:我在量化一个检测模型时,发现某个通道的 r_min 和 r_max 非常接近(比如 0.001 到 0.002),导致 scale 极小。结果 zero_point 计算时 round 误差被放大,推理结果全乱了。后来我加了一个最小 scale 的约束,才解决这个问题。
2.5 量化参数的计算示例
光说不练假把式。咱们手算一个例子。
假设有一组浮点激活值:[-1.5, 0.0, 3.2, 5.1, 2.8],要量化到 uint8(范围 0~255)。
第一步:找范围。r_min = -1.5,r_max = 5.1。
第二步:算 scale。
scale = (5.1 - (-1.5)) / (255 - 0) = 6.6 / 255 ≈ 0.02588
第三步:算 zero_point。
zero_point = round(-(-1.5) / 0.02588) + 0 = round(57.96) = 58
第四步:验证一下。浮点数 0 映射到整数 58,没问题。浮点数 5.1 映射到:
q = round(5.1 / 0.02588) + 58 = round(197.06) + 58 = 197 + 58 = 255
完美落在边界上。
关键点:实际部署时,scale 和 zero_point 是每个张量(或者每个通道)单独计算的。不同层的 scale 可能差好几个数量级,这很正常。
2.6 量化误差的来源
量化为什么会有精度损失?说白了就两个原因:
- 截断误差:浮点数值超出 [r_min, r_max] 范围时,会被强行截断到边界值。比如上面例子中,如果来了个 6.0,量化后还是 255。
- 舍入误差:round 操作会把浮点数映射到最近的整数,这个舍入过程会丢失信息。
我记得在某个项目中,模型量化后精度掉了 3 个点。排查后发现是某个层的激活值分布有长尾,r_max 设得太大,导致大部分值都挤在几个量化格子里。后来改用百分位法(比如取 99.9% 的分位数作为 r_max),精度就回来了。
避坑指南:不要直接用 min/max 作为量化范围。尤其是激活值,偶尔会有 outlier(异常值),会严重拉大量化范围。我建议用百分位法或者 KL 散度校准法来确定范围。
2.7 小结
今天咱们把量化的基础概念捋了一遍。线性量化是主流,对称量化适合权重,非对称量化适合激活值。scale 和 zero_point 的计算其实不复杂,但细节决定成败。下一章咱们会讲量化感知训练(QAT),到时候这些参数会在训练过程中动态调整,那才是真正的实战环节。
记住一句话:量化不是简单的「压缩」,而是「重新编码」。理解了这个,后面的内容就好办了。