校准数据集优化:如何选对数据,让量化精度不掉

校准数据集,说白了就是量化时用来“摸底”的那几百张图。很多人觉得随便拿点训练数据跑一遍就行,结果量化完模型精度直接崩了。我早期做量化时也踩过这个坑,后来才明白——校准集选得好不好,直接决定了量化后的模型是“能用”还是“废了”。

核心观点:校准集不是训练集的子集,而是推理分布的“采样器”。选对了,INT8 精度损失可以控制在 0.5% 以内;选错了,掉 3-5 个点都很正常。

4.1 代表性校准集:什么样的数据才算“代表”?

我个人的习惯是,先问三个问题:

  • 覆盖度够不够? 每个类别、每种光照、每种角度都得有几张。
  • 分布像不像? 校准集的统计分布要接近真实推理时的数据分布。
  • 极端值有没有? 纯黑图、过曝图、模糊图——这些“边缘案例”必须包含。

举个例子。我在做自动驾驶感知模型量化时,一开始只用了晴天白天的数据做校准。结果量化后的模型在夜间场景下 mAP 掉了 4.2%。后来我把夜间、雨天、逆光的数据各加了 50 张进去,精度损失直接降到 0.8%。

我的经验:校准集最好从验证集中按类别分层采样,每类至少 10-20 张。如果类别数超过 100,每类 5 张也够用。

4.2 校准集大小:不是越多越好,够用就行

很多人觉得校准集越大越准。其实不是。校准集的作用是让量化器“看到”激活值的分布范围,而不是让它记住数据。

我做过一组对比实验,结果如下:

校准集大小 Top-1 精度 (%) 校准耗时 (秒) 备注
32 张 76.12 1.2 分布不稳定,波动大
128 张 76.85 4.8 基本稳定
512 张 76.91 19.3 收益递减
2048 张 76.93 78.5 几乎无提升

看到了吗?从 128 张到 512 张,精度只涨了 0.06%,但耗时翻了 4 倍。从 512 张到 2048 张,精度几乎不动,耗时却翻了 4 倍。所以我的建议是:分类模型用 128-512 张,检测模型用 200-500 张,足够了。

避坑指南:我曾经试过只用 16 张图做校准,结果量化后的模型在某个类别上直接输出全零。后来发现是因为那 16 张图里恰好没有该类别的样本,导致该通道的激活范围没被观测到。所以,校准集必须覆盖所有输出通道的激活范围。

4.3 使用 KL 散度优化校准集分布

好了,现在你知道要选多少张、怎么选了。但还有一个更精细的问题:选出来的数据分布,跟真实推理分布匹配吗?

这里就要用到 KL 散度了。KL 散度衡量的是两个分布之间的差异。我们想要的是:校准集的激活值分布,尽可能接近全量数据的激活值分布。

具体做法分三步:

  1. 收集候选数据:从验证集或生产日志中收集一批候选数据,比如 2000 张。
  2. 计算参考分布:用这 2000 张图跑一遍模型,记录每一层的激活值直方图,作为参考分布 P。
  3. 贪心搜索最优子集:从候选集中选出一个子集 S,使得 S 的激活值分布 Q 与 P 的 KL 散度最小。

代码实现大概是这样的:

import numpy as np
from scipy.special import kl_div

def select_calibration_set(candidates, reference_hist, target_size=256):
    """
    使用 KL 散度贪心选择校准集
    candidates: list of (data, activation_hist)
    reference_hist: 参考分布的直方图
    target_size: 目标校准集大小
    """
    selected = []
    remaining = list(candidates)
    
    for _ in range(target_size):
        best_idx = -1
        best_kl = float('inf')
        
        for i, (data, hist) in enumerate(remaining):
            # 计算当前选中集 + 候选数据的联合分布
            combined_hist = np.mean([h for _, h in selected] + [hist], axis=0)
            kl = np.sum(kl_div(reference_hist, combined_hist))
            
            if kl < best_kl:
                best_kl = kl
                best_idx = i
        
        selected.append(remaining.pop(best_idx))
    
    return [data for data, _ in selected]

嗯,这里要注意:KL 散度计算的是 激活值分布 的差异,不是输入图像的像素分布。因为量化器关心的是每一层输出的数值范围,而不是图像长什么样。

效果对比:我在 ResNet-50 上试过,随机选 256 张校准集,量化后 Top-1 精度 76.85%;用 KL 散度优化后选 256 张,精度 76.91%。虽然只涨了 0.06%,但关键是——方差更小了。随机选 5 次,精度波动 ±0.15%;KL 优化后选 5 次,波动只有 ±0.03%。

4.4 校准集优化的完整流程

把上面这些串起来,我一般按这个流程走:

  1. 收集候选池:从验证集或生产日志中收集 1000-2000 张图,确保覆盖所有类别和场景。
  2. 计算参考分布:用全量候选池跑一遍模型,记录每层的激活值直方图。
  3. KL 散度筛选:用贪心算法选出 256-512 张最优子集。
  4. 验证稳定性:用选出的校准集量化模型,在完整验证集上评估精度。如果波动大,适当增加校准集大小。
  5. 固化校准集:将选出的校准集保存为固定文件,后续每次量化都用同一份。

下面这张图展示了整个流程:

校准集优化流程 候选数据池 1000-2000 张 计算参考分布 激活值直方图 P KL 散度筛选 贪心选子集 验证稳定性 精度波动 < 0.1% 固化校准集 固定 256-512 张 不达标则增加 候选池或调整大小 关键:KL 散度优化的是激活值分布,不是图像像素分布。一定要在模型推理后收集激活值直方图。

小技巧:如果你用的是 PyTorch,可以用 torch.distributions.kl.kl_divergence 直接算 KL 散度,比自己手写快很多。另外,贪心搜索时不用每轮都重新算所有候选,可以缓存上一轮的联合分布,增量更新就行。

4.5 总结一下

校准集优化这件事,说白了就是三个要点:

  • 代表性:覆盖所有类别和场景,别漏掉边缘案例。
  • 大小适中:128-512 张足够,多了浪费计算资源,少了分布不稳定。
  • 分布匹配:用 KL 散度让校准集的激活值分布贴近真实推理分布。

我见过太多团队在量化时随便从训练集里抽 100 张图就开搞,结果精度掉了 2-3 个点还找不到原因。其实花半天时间好好选一下校准集,很多精度问题都能提前避免。嗯,这一步做好了,后面的量化修复工作会轻松很多。