模型量化基础:什么是量化?对称量化与非对称量化,量化参数(scale, zero-point)

大家好,欢迎来到《端侧模型压缩与量化全流程实战》的第二讲。

今天咱们聊聊量化的核心概念。说实话,量化这个词听起来挺唬人的,但说白了,它就是把模型里那些高精度的数字,比如 float32,换成低精度的整数,比如 int8。你想想看,一个 float32 占 4 个字节,一个 int8 只占 1 个字节,模型体积直接缩到四分之一。这在端侧部署时,简直是救命稻草。

什么是量化?

量化,本质上是一种信息压缩技术。它把连续的、范围很大的数值,映射到离散的、范围很小的整数空间里。我习惯用一个比喻来解释:

想象你有一把尺子,上面有密密麻麻的刻度(float32)。现在你要把它换成一把只有 256 个刻度的尺子(int8)。你肯定要丢掉一些精度,但只要能保证大部分测量结果差不多,那这把新尺子就能用。

在深度学习里,我们量化的对象主要是权重和激活值。权重是模型学到的参数,激活值是每层计算出来的中间结果。把它们从 float32 变成 int8,模型体积变小了,推理速度变快了,代价就是一点点精度损失。

核心要点:量化不是无损的,但好的量化方案能把精度损失控制在 1% 以内,甚至更低。

量化参数:scale 和 zero-point

要实现从 float 到 int 的映射,我们需要两个关键参数:scale(缩放因子)zero-point(零点)

公式很简单:

q = round( r / scale ) + zero-point

其中,r 是原始的浮点数,q 是量化后的整数。scale 决定了每个整数步长代表多大的浮点数范围,zero-point 则负责对齐零点。

反过来,反量化公式是:

r = (q - zero-point) * scale

嗯,这里要注意:scale 必须是正数,zero-point 是一个整数。这两个参数一旦确定,整个量化映射关系就定死了。

我的经验:我在项目中遇到过,有些同学只关注 scale,忽略了 zero-point 的重要性。其实 zero-point 决定了零点偏移,尤其在处理非对称分布的数据时,它非常关键。

对称量化 vs 非对称量化

根据 zero-point 的取值,量化分为两种:对称量化和非对称量化。

对称量化

对称量化里,zero-point 固定为 0。也就是说,浮点数的 0 直接映射到整数的 0。公式简化为:

q = round( r / scale )

这种方式的优点是实现简单,计算效率高。因为 zero-point 为 0,在卷积或全连接层的计算中,可以省去很多偏移量的加减操作。

但缺点也很明显:如果数据的分布不是对称的,比如激活值全是正数,那对称量化会浪费一半的整数表示范围。你想想看,int8 的范围是 [-128, 127],如果数据全是正数,那负数那部分就白白浪费了。

非对称量化

非对称量化允许 zero-point 不为 0。它可以根据数据的实际分布,灵活地调整映射关系。公式就是我们前面提到的:

q = round( r / scale ) + zero-point

这样做的好处是,能充分利用整数的表示范围。比如激活值范围是 [0.0, 6.0],非对称量化可以把它映射到 [0, 255],而对称量化只能映射到 [0, 127] 左右,精度差了一倍。

代价呢?计算稍微复杂一点,因为每次都要处理 zero-point 的偏移。

避坑指南:我曾经在一个项目中,对权重用了对称量化,对激活值用了非对称量化。结果发现,某些层的 zero-point 计算有溢出风险。后来我统一了策略,要么全对称,要么全非对称,除非你非常清楚自己在做什么。

如何选择?

我个人习惯这样选:

  • 权重:通常用对称量化。因为权重的分布一般比较对称,而且对称量化计算快。
  • 激活值:通常用非对称量化。因为激活值经过 ReLU 后全是非负数,非对称量化能更好地利用表示范围。

当然,这不是绝对的。有些框架,比如 TensorRT,默认就用对称量化。你还是要根据实际效果来调。

量化参数的计算

知道了 scale 和 zero-point 的概念,那它们怎么算呢?

假设我们有一组浮点数,范围是 [r_min, r_max]。对于 int8 量化,整数范围是 [q_min, q_max],通常是 [-128, 127] 或 [0, 255]。

scale 的计算公式:

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

zero-point 的计算公式:

zero-point = q_min - round( r_min / scale )

然后对 zero-point 做截断,确保它在 [q_min, q_max] 范围内。

举个例子:假设激活值范围是 [0.0, 6.0],量化到 uint8 [0, 255]。

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

zero-point = 0 - round(0.0 / 0.02353) = 0

你看,因为数据从 0 开始,zero-point 正好是 0,这其实变成了对称量化的一种特例。

量化误差的来源

量化一定会引入误差,主要来自两个方面:

  1. 舍入误差:round 操作会丢失精度。比如 3.14 量化后变成 3,丢了 0.14。
  2. 截断误差:超出量化范围的值会被截断。比如最大值是 10.0,但量化范围只能到 6.0,那 10.0 就被截成 6.0 了。

所以,选择合适的量化范围非常重要。范围选大了,精度不够;范围选小了,截断太多。这需要你在精度和范围之间做权衡。

我的建议:在实际项目中,我会先用校准数据集跑一遍模型,统计每层激活值的分布,然后根据分布选择合适的量化范围。有时候,去掉一些极端 outlier 能显著提升量化精度。

总结一下

量化就是把 float32 变成 int8,核心参数是 scale 和 zero-point。对称量化适合权重,非对称量化适合激活值。选择哪种方案,取决于你的数据分布和计算效率要求。

下一讲,我们会深入量化感知训练(QAT),看看如何在训练过程中模拟量化误差,让模型自己学会适应低精度。到时候我会分享一个我踩过的坑,保证让你印象深刻。