模型量化基础:从浮点到整数的“压缩艺术”
各位同学,今天我们聊聊模型量化。说实话,这是模型压缩里最“立竿见影”的一招。我最早接触量化是在一个移动端部署项目里,模型跑在手机上又慢又烫,量化之后体积直接砍掉 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,精度影响不大。
量化过程的完整流程
下面这张图展示了从浮点模型到量化模型的完整流程:
量化误差的来源
量化不是无损的。误差主要来自两个地方:
- 截断误差: 当浮点值超出量化范围时,会被截断到边界值。比如 INT8 范围是 [-128, 127],如果某个权重是 130,量化后变成 127,信息就丢了。
- 舍入误差: 量化公式里的
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。
嗯,这一章的内容就到这儿。量化是模型压缩的“基本功”,后面我们会讲怎么在代码里实现它,以及如何应对精度损失。记住:量化不是玄学,是数学。