4、训练后量化(PTQ):原理、校准与实战策略
训练后量化,英文叫 Post-Training Quantization,简称 PTQ。说白了,就是模型训练完了,我们直接把它从 FP32 转成 INT8 甚至更低比特。不需要重新训练,成本低,见效快。
我刚开始接触 PTQ 的时候,总觉得这玩意儿有点「玄学」。明明模型精度好好的,一量化就掉点。后来踩的坑多了,才慢慢摸清楚门道。今天我把这些经验掰开揉碎讲给你听。
PTQ 的原理:到底在做什么?
PTQ 的核心就一句话:找到 FP32 数值到 INT8 数值的最佳映射关系。
FP32 的取值范围大约是 ±3.4×10³⁸,而 INT8 只有 -128 到 127。要把一个大范围压缩到小范围,必然有精度损失。关键是怎么让损失最小。
数学上,量化可以表示为:
Q = round( (R / S) + Z )
其中:
- R 是原始的 FP32 值
- S 是缩放因子(Scale)
- Z 是零点偏移(Zero Point)
- Q 是量化后的 INT8 值
反量化就是逆过程:
R ≈ (Q - Z) × S
嗯,这里要注意:S 和 Z 怎么选,直接决定了量化精度。选大了,数值范围覆盖全了但精度粗;选小了,精度细但容易截断。
核心矛盾:量化误差 = 截断误差 + 舍入误差。截断误差来自超出范围的数值被强行截断,舍入误差来自浮点转整数的精度丢失。两者此消彼长,需要找到平衡点。
校准数据集的选择:别随便拿数据
PTQ 需要一个「校准数据集」来统计每层激活值的分布。这个数据集选不好,量化效果直接崩。
我在项目中遇到过一件事:用 ImageNet 的验证集做校准,模型在公开测试集上表现不错。但一部署到客户的实际场景,精度直接掉了 5 个点。为什么?因为校准数据分布和实际数据分布不一致。
选校准数据集,我总结了三条原则:
- 代表性:校准数据必须来自真实部署场景。别偷懒用公开数据集凑数。
- 多样性:覆盖各种边缘情况。比如自动驾驶场景,晴天、雨天、夜晚的数据都要有。
- 适量性:不是越多越好。我一般用 500-2000 张图片就够了。太多反而让校准过程变慢,收益却很小。
我的小技巧:如果场景数据不好收集,可以用训练集的一个子集,但一定要做数据增强。随机裁剪、颜色抖动、加噪声,让校准数据「看起来」更像真实场景。
PTQ 的常见策略:四种主流方法
PTQ 不是只有一种做法。根据你的精度要求和硬件限制,可以选择不同的策略。我按推荐程度从低到高给你排个序。
| 策略 | 原理 | 精度损失 | 适用场景 |
|---|---|---|---|
| Min-Max 量化 | 直接取激活值的 min 和 max 作为量化范围 | 较大 | 对精度不敏感的任务 |
| Percentile 量化 | 去掉一定比例的极端值,用剩余范围量化 | 中等 | 有少量离群值的模型 |
| KL 散度量化 | 最小化 FP32 和 INT8 分布的 KL 散度 | 较小 | 大多数通用场景 |
| 逐通道量化 | 每个通道独立计算 S 和 Z | 最小 | 对精度要求高的场景 |
1. Min-Max 量化
最简单粗暴。直接拿校准数据里每层的最大值和最小值作为量化边界。优点是快,缺点是容易被离群值带偏。我曾经试过一个模型,激活值里有个别像素特别大,结果 Min-Max 量化后大部分数值都挤在 INT8 的 0 附近,精度直接崩了。
2. Percentile 量化
改进版。去掉前 0.1% 或 1% 的极端值,用剩下的范围量化。这样能避免离群值的干扰。我一般先用 Percentile 99.9% 试试,如果精度不够再调低。
3. KL 散度量化
这是目前最主流的方法。NVIDIA TensorRT 默认用的就是它。原理是:把 FP32 的分布和 INT8 的分布做对比,找到让两者 KL 散度最小的阈值。说白了,就是让量化后的分布「看起来最像」原始分布。
# 伪代码示意
def find_optimal_threshold(fp32_distribution):
best_threshold = None
min_kl_divergence = float('inf')
for threshold in candidate_thresholds:
# 截断 FP32 分布
truncated = clip(fp32_distribution, -threshold, threshold)
# 量化到 INT8
quantized = quantize(truncated)
# 反量化回来
dequantized = dequantize(quantized)
# 计算 KL 散度
kl = kl_divergence(fp32_distribution, dequantized)
if kl < min_kl_divergence:
min_kl_divergence = kl
best_threshold = threshold
return best_threshold
4. 逐通道量化
精度最高的方法。每个通道独立计算缩放因子和零点偏移。代价是计算量大了,而且有些硬件不支持。我一般在部署到 GPU 时用这个,CPU 上慎用。
曾经踩过的坑:逐通道量化在卷积层效果很好,但在全连接层上反而可能掉精度。因为全连接层的每个输出通道和所有输入通道相关,独立量化会破坏这种关联性。遇到这种情况,我建议全连接层用逐层量化,卷积层用逐通道量化。
实战中的避坑指南
说了这么多理论,最后分享几个我实际踩过的坑:
- 校准数据量太少:少于 100 张图片,统计出来的分布不可靠。我至少用 500 张。
- 忽略 BN 层的融合:量化前一定要把 BN 层融合到卷积层里。否则 BN 的参数会干扰量化统计。
- 一次性量化所有层:我建议逐层量化,每量化一层就验证一次精度。这样能快速定位是哪一层出了问题。
- 忽视量化后的微调:如果 PTQ 后精度还是不够,可以尝试 QAT(量化感知训练)。但那是另一个话题了。
你想想看,PTQ 其实就是在精度和速度之间做 trade-off。没有银弹,只有最适合你场景的方案。我个人的习惯是:先用 KL 散度量化跑一遍,精度损失在 1% 以内就收工。如果超过 1%,再尝试逐通道量化或者调整校准数据集。
这张图帮你快速决策:精度要求高就走右边,要求不高就走左边。但不管走哪条路,最后一定要验证。我见过太多人量化完就跑,结果部署上去才发现精度不对。
好了,PTQ 的核心内容就这些。记住一句话:校准数据选得好,量化成功一半。另一半靠策略选择和反复验证。
最后一个小建议:如果你用的是 PyTorch,可以试试 torch.ao.quantization 里的 PTQ 工具。TensorRT 的话,直接用 trtexec 的 --int8 选项,它会自动帮你做校准。但不管用哪个工具,理解原理才能用好它。