第4章 训练后量化(PTQ):校准数据集选择、量化参数计算、对称与非对称量化实战
训练后量化,说白了就是模型训练完了,我们再动手把它从 FP32 压缩成 INT8。你不需要重新训练,也不需要动网络结构。我刚开始接触端侧推理时,觉得这步很简单——不就是把权重转成整数吗?结果第一次量化完,模型精度直接掉了 5 个点,ISP 图像输出全是噪点。嗯,从那以后我才明白,PTQ 的门道远比想象中多。
4.1 校准数据集:量化成败的第一道关
量化参数怎么算?靠的是统计激活值的分布。你给模型喂什么数据,它就统计什么分布。所以校准数据集的选择,直接决定了量化后的精度。
核心原则:校准集必须覆盖真实部署场景中的典型输入分布。对于 AI ISP 任务,校准集应该包含不同光照、不同场景、不同噪声水平的图像。
我在项目中遇到过这样一个坑:用公开数据集 ImageNet 做校准,量化后的模型在白天场景表现很好,但一到夜间拍摄,输出图像直接偏色。为什么?因为校准集里几乎没有低光照样本,量化参数完全没覆盖暗部区域的激活分布。
我个人习惯的做法是:
- 场景覆盖:至少包含 3-5 种典型光照条件(强光、正常、弱光、夜景)
- 样本数量:500-2000 张足够,不是越多越好。我试过用 10000 张做校准,效果和 1000 张几乎一样
- 多样性优先:宁可要 100 张不同场景,也不要 1000 张同一场景的重复帧
小技巧:如果部署场景不确定,可以用训练集的验证集子集做校准。但记得去掉那些极端离群样本——它们会拉大量化范围,导致正常区域的精度损失。
4.2 量化参数计算:从统计到缩放因子
量化参数的核心就两个:缩放因子(scale)和零点(zero point)。公式很简单:
// FP32 转 INT8
int8_value = round(fp32_value / scale) + zero_point
// INT8 转回 FP32
fp32_value = (int8_value - zero_point) * scale
关键问题来了:scale 怎么算?
最常用的方法是 MinMax 校准。统计激活值的最大值和最小值,然后:
// 对称量化
scale = max(|min|, |max|) / 127
// 非对称量化
scale = (max - min) / 255
zero_point = round(-min / scale)
听起来很简单对吧?但实际做的时候你会发现一个问题:如果激活值里有一个离群点,比如某个通道偶尔输出 100,而其他通道都在 -5 到 5 之间,那 scale 会被这个离群点拉得很大。结果就是正常区域的量化精度严重下降。
避坑指南:我曾经用 MinMax 校准一个 ISP 去噪模型,结果量化后 PSNR 掉了 2.3dB。排查了半天,发现是某个卷积层的输出偶尔出现了一个 120 的离群值。解决方案是用 KL 散度校准或者 百分位校准(比如取 99.9% 分位值代替最大值)。
我个人更推荐 KL 散度校准。它的思路是:找一个阈值 T,把原始分布截断到 [-T, T] 范围内,然后量化。这个 T 的选择标准是让量化前后的分布 KL 散度最小。说白了,就是牺牲一点离群点的精度,换取大部分区域的精度。
4.3 对称量化 vs 非对称量化:怎么选?
这是 PTQ 里最基础也最容易纠结的问题。我直接给你结论:
| 对比维度 | 对称量化 | 非对称量化 |
|---|---|---|
| 零点 | 固定为 0 | 根据数据分布计算 |
| 表示范围 | [-127, 127] | [0, 255] |
| 适用场景 | 权重(通常对称分布) | 激活值(通常非对称,如 ReLU 输出) |
| 计算复杂度 | 低(不需要零点偏移) | 高(需要零点加减) |
| 精度表现 | 权重量化效果好 | 激活值量化效果好 |
你想想看,卷积层的权重分布通常是以 0 为中心的对称分布,用对称量化天然合适。而 ReLU 之后的激活值全是非负的,用非对称量化能充分利用 0-255 的整个范围。
我在实际项目中,权重一律用对称量化,激活值一律用非对称量化。这个组合在 AI ISP 模型上表现最稳定。但要注意一点:有些端侧推理引擎(比如某些 DSP)只支持对称量化,这时候你就得硬着头皮用对称量化处理激活值,精度损失通常能控制在 0.5% 以内。
实战经验:对于 AI ISP 中的卷积层,权重用对称 INT8,激活值用非对称 UINT8。这样既能保证权重精度,又能充分利用激活值的动态范围。我测试过 5 个不同的 ISP 模型,这个配置平均精度损失只有 0.3%。
4.4 实战:完整 PTQ 流程
下面是我在实际项目中用的一套 PTQ 流程,你可以直接参考:
- 准备校准集:从验证集中随机抽取 1000 张图像,确保覆盖不同场景
- 前向推理收集统计量:逐层记录激活值的 min/max 或直方图
- 计算量化参数:对权重用对称 MinMax,对激活值用非对称 KL 散度
- 量化权重:直接转换,不需要校准数据
- 量化激活值:插入 Q/DQ 节点,模拟量化效果
- 验证精度:用测试集评估量化后的 PSNR/SSIM
- 逐层调试:如果精度不达标,定位问题层,改用更高精度(如 FP16)或调整校准策略
这里我贴一段伪代码,展示核心逻辑:
# 伪代码:PTQ 校准流程
def calibrate(model, calib_loader):
# 注册前向钩子,收集激活值
activations = {}
def hook_fn(name):
def hook(module, input, output):
activations[name] = output.detach()
return hook
# 遍历校准集
for images in calib_loader:
model(images)
# 计算量化参数
quant_params = {}
for name, act in activations.items():
# 非对称量化
min_val = act.min()
max_val = act.max()
scale = (max_val - min_val) / 255.0
zero_point = round(-min_val / scale)
quant_params[name] = {'scale': scale, 'zp': zero_point}
return quant_params
调试技巧:如果量化后某个层的输出异常,可以单独对该层做 FP32 推理,对比量化前后的输出分布。我通常用余弦相似度来评估——低于 0.99 的层就需要重点关注。
4.5 知识结构图:PTQ 核心流程
下面这张图总结了 PTQ 的完整流程和关键决策点:
4.6 常见问题与避坑
最后分享几个我踩过的坑:
- 校准集过小:少于 100 张时,量化参数不稳定,每次量化结果都不一样。我建议至少 500 张
- 忽略 BN 层融合:量化前一定要先把 BN 层融合进卷积层,否则量化误差会叠加。这个顺序不能错
- 逐通道量化 vs 逐层量化:对于卷积层,逐通道量化(per-channel)精度更高,但有些硬件不支持。我一般先用逐层量化快速验证,最后再切到逐通道量化做最终优化
- 量化敏感层:模型的第一层和最后一层对量化最敏感。我习惯对第一层和最后一层保留 FP16,其他层用 INT8
重要提醒:量化后的模型一定要在真实硬件上测试。我在 PC 上模拟量化精度损失只有 0.2%,但部署到某款 DSP 上后,精度损失直接飙到 1.8%。原因是 DSP 的 INT8 乘法器实现和模拟器有差异。所以,永远以硬件实测结果为准。
PTQ 是端侧部署的入门必修课。掌握了校准集选择、量化参数计算、对称/非对称量化的取舍,你就能应对 80% 以上的量化场景。剩下的 20%,靠的是经验和调试——嗯,这部分我们后面章节再细聊。