4. 量化精度调优

量化,说白了就是把模型从浮点数世界拽到整数世界。这个过程一定会丢精度,但怎么丢得少、丢得可控,就是咱们今天要聊的核心。

我刚开始做端侧部署时,觉得量化就是个除法加取整。结果第一次把 MobileNet 量化到 INT8 后,精度直接掉了 5 个点,模型在手机上跑得飞快,但识别结果完全没法用。嗯,从那以后我才真正开始研究量化校准这件事。

4.1 校准数据与校准流程

量化校准的第一步,是收集激活值的分布。你需要一小部分有代表性的数据,跑一遍前向推理,把每一层的激活值统计下来。

  • 校准集大小:我个人习惯用 500~2000 张图片,太少分布不准,太多浪费时间。
  • 数据代表性:必须覆盖真实场景。我见过有人用 ImageNet 校准集去量化一个工业缺陷检测模型,结果部署后精度崩了——因为分布完全不一样。
  • 逐层统计:每一层的激活值范围差异很大,不能混在一起算。
我的经验:校准数据不要做数据增强。你只需要原始分布,不需要模拟噪声。增强后的数据会让校准出来的阈值偏大,反而降低精度。

4.2 KL 散度校准:最小化信息损失

KL 散度校准是目前用得最广的方法,TensorRT 和很多 NPU 工具链都在用。它的核心思想很简单:找一个阈值,把原始浮点分布截断后量化,让量化后的分布和原始分布尽可能相似。

为什么不是直接取 min/max?因为浮点分布往往有长尾。直接取 min/max 会把整个动态范围拉得很大,导致有效数值区间被压缩,精度损失反而更大。

KL 散度的计算过程大致如下:

  1. 统计激活值的直方图(通常 2048 个 bin)。
  2. 从某个候选阈值开始,把直方图截断并量化到 256 个 bin。
  3. 计算量化后分布与原始分布的 KL 散度。
  4. 遍历所有候选阈值,选 KL 散度最小的那个。
# 伪代码示意
def kl_calibrate(hist, bin_edges, num_bins=2048):
    best_kl = float('inf')
    best_threshold = 0
    for threshold_idx in range(128, num_bins):
        # 截断直方图
        clipped_hist = hist[:threshold_idx].copy()
        # 量化到 256 个 bin
        quantized_hist = quantize(clipped_hist, 256)
        # 计算 KL 散度
        kl = compute_kl(clipped_hist, quantized_hist)
        if kl < best_kl:
            best_kl = kl
            best_threshold = bin_edges[threshold_idx]
    return best_threshold
避坑指南:我曾经在某个模型上发现 KL 散度校准出来的阈值特别大,导致精度下降。后来排查发现,校准集中有少量异常值把直方图尾巴拉得很长。解决办法是:在校准前先做一次异常值裁剪,去掉 top 0.01% 的极端值。

4.3 MSE 最小化校准:直接优化数值误差

KL 散度关注的是分布相似性,而 MSE 最小化更直接——它追求量化前后的数值误差最小。

我个人更喜欢 MSE 校准,尤其是在回归任务或对数值精度敏感的模型上。它的计算更直观:

def mse_calibrate(tensor, num_bits=8):
    best_mse = float('inf')
    best_scale = 0
    min_val, max_val = tensor.min(), tensor.max()
    # 在 min/max 之间搜索最优 scale
    for scale in np.linspace(0.1, max_val, 1000):
        quantized = quantize(tensor, scale, num_bits)
        mse = np.mean((tensor - quantized) ** 2)
        if mse < best_mse:
            best_mse = mse
            best_scale = scale
    return best_scale

MSE 校准的优点是稳定,缺点是计算量比 KL 散度大。不过现在硬件快了,这点开销可以接受。

校准方法 优化目标 适用场景 计算开销
KL 散度 分布相似性 分类任务、分布长尾明显 中等
MSE 最小化 数值误差 回归任务、检测任务 较高
Min-Max 无 简单基线 低
百分比截断 去除极端值 分布有异常值 低

4.4 每通道量化 vs 每层量化

这是量化精度调优里最容易忽略但又影响巨大的选择。

  • 每层量化(Per-Layer):整层共用一个 scale 和 zero_point。优点是计算简单,硬件实现容易。缺点是如果层内不同通道的数值范围差异大,精度损失会很明显。
  • 每通道量化(Per-Channel):每个通道独立计算 scale 和 zero_point。精度更高,但计算量也更大。

我做过一个对比实验:在 MobileNetV2 上,每层量化掉 3 个点,每通道量化只掉 0.5 个点。差距非常明显。

我的建议:对于权重,尽量用每通道量化。对于激活值,如果硬件支持就用每通道,不支持就用每层加 KL 散度校准,效果也还行。

4.5 量化误差分析与补偿

量化误差不是均匀分布的。有些层对量化敏感,有些层几乎无感。你需要找到那些敏感层,单独处理。

我常用的分析方法:

  1. 逐层对比浮点和量化后的输出,计算 SNR(信噪比)或 MSE。
  2. 把 SNR 最低的 top 5 层标记为敏感层。
  3. 对敏感层做特殊处理:比如保留 FP16、使用更大的校准集、或者做量化感知训练。

补偿手段主要有三种:

  • 偏置校正:量化误差会导致输出偏置漂移,统计后减去这个偏置。
  • 权重均衡:调整相邻层的权重分布,让量化更均匀。这个技巧在 Depthwise 卷积层特别有效。
  • 混合精度:敏感层用 FP16,其他层用 INT8。我有个项目就是靠这个把精度从掉 2% 拉回到只掉 0.3%。
避坑指南:我曾经在某个检测模型上做偏置校正,结果精度反而下降了。后来发现是因为校正数据太少,统计出来的偏置不准确。偏置校正至少需要 1000 张以上的校准数据,否则不如不做。

4.6 量化精度调优流程总览

下面这张图是我在实际项目中总结的量化精度调优流程,你可以把它当作一个检查清单:

量化精度调优流程 收集校准数据 选择校准方法(KL/MSE) 确定量化粒度(每层/每通道) 逐层误差分析(SNR/MSE) 误差补偿(偏置/均衡/混合精度) 关键检查点 • 校准集是否覆盖真实分布? • 是否有异常值需要裁剪? • 校准集大小是否足够? 常见陷阱 • KL 散度对异常值敏感 • 每层量化忽略通道差异 补偿优先级 1. 偏置校正(最简单) 2. 权重均衡(Depthwise 层) 3. 混合精度(最后手段)

这个流程我用了很多项目,基本能覆盖 90% 的量化精度问题。记住一个原则:先校准,再分析,最后补偿。不要一上来就上混合精度,那是最后的手段。

我的习惯:每次量化完,我都会保留一份逐层的误差分析报告。下次遇到类似模型,直接参考历史数据,能省不少时间。