模型量化基础:从浮点到整数的“压缩艺术”

各位同学,今天我们聊聊模型量化。说实话,这是模型压缩里最“立竿见影”的一招。我最早接触量化是在一个移动端部署项目里,模型跑在手机上又慢又烫,量化之后体积直接砍掉 75%,速度翻了两倍多。那一刻我就知道——这玩意儿,必须吃透。

什么是量化?

量化,说白了就是把模型里的浮点数(通常是 FP32)用更低位宽的整数(比如 INT8)来表示。你想想看,一个 FP32 的权重占 4 个字节,换成 INT8 只要 1 个字节,内存直接降到四分之一。

但这里有个关键问题:精度不能丢太多。量化不是简单的“四舍五入”,它有一套映射机制,把连续的浮点数值域映射到离散的整数空间。

核心公式(记住它):

量化:q = round(r / scale) + zero_point

反量化:r = (q - zero_point) × scale

其中 r 是原始浮点值,q 是量化后的整数值。

嗯,这里要注意:scale 和 zero_point 就是量化的两个核心参数。后面我们会详细拆解它们是怎么算出来的。

对称量化 vs 非对称量化

这两种方式,我在项目里都踩过坑。先看它们的区别:

对比维度 对称量化 非对称量化
zero_point 固定为 0 根据数据分布计算
映射范围 [-max, +max] → [-128, 127] [min, max] → [0, 255]
适用场景 权重(通常对称分布) 激活值(通常非对称分布)
计算复杂度 低(少一个偏移量) 稍高(需要处理 zero_point)
精度损失 对非对称分布数据较大 更贴合实际分布,精度更好

对称量化

对称量化,就是让浮点数的 0 映射到整数的 0。所以 zero_point = 0,公式简化为:

q = round(r / scale)
r = q × scale

我习惯在量化权重时用对称量化。为什么?因为神经网络的权重通常围绕 0 对称分布,正负值差不多。这时候用对称量化,不会浪费整数表示范围。

我的经验: 如果你量化的是卷积层的权重,优先试对称量化。我在一个 ResNet-50 项目里对比过,对称量化比非对称量化在权重上只差了 0.1% 的精度,但计算量少了 15%。

非对称量化

非对称量化就灵活多了。它允许浮点数的 0 映射到任意整数位置。公式里 zero_point 不再是 0,而是根据数据的最小值和最大值算出来的。

为什么需要非对称?你想想看,激活值(比如 ReLU 的输出)全是非负数,范围是 [0, 6.0]。如果用对称量化,那 [-6.0, 6.0] 的范围里有一半是浪费的。非对称量化可以把 [0, 6.0] 完整映射到 [0, 255],精度直接翻倍。

我曾经踩过的坑: 有一次量化一个 MobileNet 的激活值,我偷懒用了对称量化,结果精度掉了 5%。后来换成非对称量化,精度只掉了 0.8%。记住:激活值一定要用非对称量化,除非你确认它的分布是对称的。

量化参数的计算原理

好,现在我们来拆解 scale 和 zero_point 到底怎么算。这是量化的“心脏”。

scale 的计算

scale 就是“步长”,表示一个整数单位代表多大的浮点数。公式很简单:

scale = (r_max - r_min) / (q_max - q_min)

对于 INT8:

  • 对称量化:q_max = 127,q_min = -128
  • 非对称量化:q_max = 255,q_min = 0

举个例子。假设某层激活值的范围是 [0.0, 6.0],用非对称 INT8 量化:

scale = (6.0 - 0.0) / (255 - 0) = 6.0 / 255 ≈ 0.02353

这意味着每个整数步长代表约 0.02353 的浮点数。量化后的值每差 1,原始值就差 0.02353。

zero_point 的计算

zero_point 是浮点 0 映射到的整数值。它的作用是让量化后的整数能正确表示浮点 0。

计算公式:

zero_point = round(q_min - r_min / scale)

还是上面的例子:

zero_point = round(0 - 0.0 / 0.02353) = round(0) = 0

因为最小值是 0,所以 zero_point 也是 0。但如果数据范围是 [-3.0, 7.0]:

scale = (7.0 - (-3.0)) / 255 = 10.0 / 255 ≈ 0.03922
zero_point = round(0 - (-3.0) / 0.03922) = round(76.5) = 77

这时候浮点 0 映射到整数 77。反量化时,r = (q - 77) × 0.03922,当 q=77 时,r=0,完美。

关键点: zero_point 必须是一个整数,而且要在 [q_min, q_max] 范围内。如果计算出来超出范围,需要截断。我在一个项目里遇到过 zero_point 算出来是 -1(INT8 非对称范围是 [0,255]),直接截断到 0,精度影响不大。

量化过程的完整流程

下面这张图展示了从浮点模型到量化模型的完整流程:

量化流程框架图 浮点模型 (FP32) 收集数据分布 计算 scale/zero_point 量化权重 量化激活值 校准 (Calibration) 量化模型 (INT8) 校准阶段使用少量验证数据确定激活值的 min/max,从而计算 scale 和 zero_point

量化误差的来源

量化不是无损的。误差主要来自两个地方:

  1. 截断误差: 当浮点值超出量化范围时,会被截断到边界值。比如 INT8 范围是 [-128, 127],如果某个权重是 130,量化后变成 127,信息就丢了。
  2. 舍入误差: 量化公式里的 round() 操作会引入舍入误差。比如 scale=0.1,r=0.15,量化后 q=round(1.5)=2,反量化后 r'=0.2,误差 0.05。

我的建议: 量化后一定要做精度验证。我一般会跑一遍验证集,对比量化前后的精度差。如果精度下降超过 1%,我会考虑改用更大的校准集,或者对某些敏感层保持 FP32。

总结

量化就是把 FP32 模型压缩成 INT8 甚至更低比特的过程。核心就三个东西:scale、zero_point 和映射公式。对称量化适合权重,非对称量化适合激活值。计算参数时,先确定数据范围,再算 scale,最后算 zero_point。

嗯,这一章的内容就到这儿。量化是模型压缩的“基本功”,后面我们会讲怎么在代码里实现它,以及如何应对精度损失。记住:量化不是玄学,是数学。