第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 流程,你可以直接参考:

  1. 准备校准集:从验证集中随机抽取 1000 张图像,确保覆盖不同场景
  2. 前向推理收集统计量:逐层记录激活值的 min/max 或直方图
  3. 计算量化参数:对权重用对称 MinMax,对激活值用非对称 KL 散度
  4. 量化权重:直接转换,不需要校准数据
  5. 量化激活值:插入 Q/DQ 节点,模拟量化效果
  6. 验证精度:用测试集评估量化后的 PSNR/SSIM
  7. 逐层调试:如果精度不达标,定位问题层,改用更高精度(如 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 的完整流程和关键决策点:

训练后量化(PTQ)核心流程 校准数据集 前向推理收集激活分布 量化参数计算(Scale / Zero Point) 对称量化(权重) 非对称量化(激活值) 生成 INT8 量化模型 校准集需覆盖真实场景分布 离群点处理:KL散度校准

4.6 常见问题与避坑

最后分享几个我踩过的坑:

  • 校准集过小:少于 100 张时,量化参数不稳定,每次量化结果都不一样。我建议至少 500 张
  • 忽略 BN 层融合:量化前一定要先把 BN 层融合进卷积层,否则量化误差会叠加。这个顺序不能错
  • 逐通道量化 vs 逐层量化:对于卷积层,逐通道量化(per-channel)精度更高,但有些硬件不支持。我一般先用逐层量化快速验证,最后再切到逐通道量化做最终优化
  • 量化敏感层:模型的第一层和最后一层对量化最敏感。我习惯对第一层和最后一层保留 FP16,其他层用 INT8

重要提醒:量化后的模型一定要在真实硬件上测试。我在 PC 上模拟量化精度损失只有 0.2%,但部署到某款 DSP 上后,精度损失直接飙到 1.8%。原因是 DSP 的 INT8 乘法器实现和模拟器有差异。所以,永远以硬件实测结果为准。

PTQ 是端侧部署的入门必修课。掌握了校准集选择、量化参数计算、对称/非对称量化的取舍,你就能应对 80% 以上的量化场景。剩下的 20%,靠的是经验和调试——嗯,这部分我们后面章节再细聊。