3. 量化原理(下):量化误差分析
上一章我们聊了量化的基本数学原理,说白了就是把 float32 的连续值映射到 int8 的离散格子。但映射这件事,天生就有信息损失。你想想看,一个 32 位的数压缩到 8 位,怎么可能不丢精度?
这一章我们就来直面这个问题。我会从误差的源头讲起,然后聊怎么用 KL 散度去评估校准质量,最后给出两种最常用的校准方法。嗯,都是我在项目里踩过坑之后才真正理解的东西。
3.1 量化误差的两个源头
量化误差其实就两种:截断误差和舍入误差。听起来简单,但很多人搞混。
3.1.1 截断误差(Clipping Error)
截断误差发生在你设定量化范围的时候。比如你选了 [-128, 127] 这个范围,但实际权重里有个 150 的值。那 150 就只能被截断到 127。这个差值就是截断误差。
我在项目中遇到过一件事:一个 MobileNetV2 模型,量化后精度直接掉了 5 个点。排查了半天,发现是某个层的激活值分布特别宽,有个别离群点把整个量化范围撑大了。结果大部分正常值都被压缩到很小的区间,精度全丢了。
关键认知:截断误差主要影响离群点。如果你的数据分布很集中,但有几个离群值,那截断误差反而小——因为你把离群点截掉了,保护了主体分布。
3.1.2 舍入误差(Rounding Error)
舍入误差发生在你从 float 映射到 int 的时候。比如 scale 是 0.1,那 float 值 3.14 映射到 int 就是 31(因为 3.14/0.1=31.4,四舍五入到 31)。反量化回来是 3.1,误差 0.04。
舍入误差是均匀分布的,范围在 [-0.5*scale, 0.5*scale] 之间。你想想看,scale 越小,舍入误差就越小。但 scale 小意味着量化范围窄,又容易引入截断误差。这就是一对矛盾。
| 误差类型 | 产生原因 | 特点 | 控制方法 |
|---|---|---|---|
| 截断误差 | 超出量化范围的值被截断 | 非均匀,集中在尾部 | 选择合适的量化范围 |
| 舍入误差 | 浮点到整数的映射精度损失 | 均匀分布,与 scale 正相关 | 减小 scale(但会增大截断风险) |
注意:很多人以为量化误差就是舍入误差。其实在 int8 量化中,截断误差往往比舍入误差大得多。我见过一个团队花了两周优化舍入方案,结果精度没提升——因为他们的截断阈值设得太差了。
3.2 KL 散度:量化质量的度量尺
怎么判断一个量化方案好不好?直观上,我们希望量化后的分布和原始分布尽可能接近。KL 散度就是用来衡量这个「接近程度」的。
3.2.1 什么是 KL 散度
KL 散度,全称 Kullback-Leibler divergence,也叫相对熵。它衡量的是用一个分布 Q 去近似另一个分布 P 时,信息损失了多少。
公式长这样:
D_KL(P || Q) = Σ P(x) * log(P(x) / Q(x))
说白了,就是看两个分布的重叠程度。重叠越多,KL 散度越小,量化质量越好。
我建议你在选择校准方法时,把 KL 散度作为核心指标。但要注意,KL 散度不是对称的——D_KL(P||Q) 和 D_KL(Q||P) 不一样。在量化场景下,我们通常用原始分布作为 P,量化分布作为 Q。
3.2.2 校准数据集的选择
校准数据集是用来确定量化参数的。你想想看,如果校准集选得不好,量化参数就定不准,推理时精度肯定崩。
我个人习惯遵循三个原则:
- 代表性:校准集要覆盖模型在实际场景中可能遇到的各种输入。比如做人脸识别,校准集里不能全是正脸,侧脸、遮挡、光照变化都要有。
- 多样性:不要只用 100 张相似的图片。我见过有人用 ImageNet 的 1000 张子集做校准,结果模型在真实场景下精度掉了 3 个点——因为校准集太「干净」了。
- 规模适中:不是越多越好。一般来说,500-2000 张图片就够。太多反而让校准过程变慢,而且收益递减。
小技巧:如果你不确定校准集选得好不好,可以跑一个快速验证:用校准集和测试集分别计算 KL 散度。如果两者差异很大,说明校准集可能有问题。
3.3 MinMax 与 Percentile 校准方法
好了,理论讲完了,我们来点实际的。校准方法说白了就是怎么确定量化范围 [min, max]。
3.3.1 MinMax 校准
MinMax 是最简单的方法。直接取校准集中所有数据的实际最小值和最大值作为量化范围。
min_val = min(calibration_data)
max_val = max(calibration_data)
scale = (max_val - min_val) / 255
zero_point = round(-min_val / scale)
优点很明显:实现简单,计算快。缺点也很致命:对离群点极其敏感。
我记得有一次,一个 ResNet-50 模型用 MinMax 校准后,精度从 76.3% 掉到了 72.1%。查了半天,发现校准集里有一张图片的某个像素值特别大,把整个量化范围撑到了 [-3.2, 4.8]。实际上 99% 的数据都在 [-1.0, 1.5] 之间。这就是典型的离群点污染。
避坑指南:我曾经在部署一个检测模型时,直接用 MinMax 校准,结果模型输出全是 NaN。后来发现是某个层的权重有一个异常大的值,导致 scale 计算溢出。从那以后,我每次用 MinMax 都会先检查数据分布,看看有没有明显的离群点。
3.3.2 Percentile 校准
Percentile 校准就是为了解决 MinMax 的离群点问题。它不取实际的最小最大值,而是取某个百分位上的值。
比如你设 percentile=99.9,那就取所有数据中排在 99.9% 位置的值作为 max,0.1% 位置的值作为 min。这样就把最极端的离群点给过滤掉了。
min_val = percentile(calibration_data, 0.1) # 0.1% 分位
max_val = percentile(calibration_data, 99.9) # 99.9% 分位
scale = (max_val - min_val) / 255
zero_point = round(-min_val / scale)
Percentile 的关键是选对百分位。选太高(比如 99.99%),效果和 MinMax 差不多;选太低(比如 95%),又会把太多正常值截断掉。
我个人的经验是:
- 权重:用 99.9% 或 99.99%。权重分布通常比较集中,离群点很少。
- 激活值:用 99.5% 到 99.9%。激活值分布更宽,需要更激进地过滤离群点。
- 特殊情况:如果模型对精度特别敏感(比如医疗影像),可以试试 99.99%,甚至回到 MinMax。
| 校准方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| MinMax | 数据分布集中,无离群点 | 实现简单,无参数 | 对离群点敏感 |
| Percentile | 存在少量离群点 | 鲁棒性好,可调节 | 需要调参,可能过度截断 |
3.4 实战建议:怎么选校准方法
说了这么多,你可能会问:到底该用哪种?
我的建议是分三步走:
- 先跑 MinMax:作为 baseline,看看精度损失多少。
- 检查分布:画出校准数据的直方图,看看有没有离群点。如果有,用 Percentile。
- 调参验证:用 KL 散度评估不同百分位的效果,选 KL 散度最小的那个。
嗯,这套流程我在三个项目里验证过,效果都还不错。当然,如果你用的是 TensorRT 或 ONNX Runtime,它们内部已经集成了这些校准方法,你只需要设置参数就行。但理解原理还是很重要——至少出了问题你知道该往哪个方向排查。
核心总结:量化误差不可避免,但我们可以控制。截断误差和舍入误差是一对 trade-off,KL 散度是衡量量化质量的好工具,MinMax 简单但脆弱,Percentile 鲁棒但需要调参。校准数据集选得好,量化就成功了一半。
下一章我会讲更进阶的量化方法,包括逐通道量化和对称/非对称量化的选择。到时候我会分享一个我在部署 YOLOv5 时遇到的坑——嗯,那个坑让我加班到凌晨三点。