4、训练后量化(PTQ)原理:校准数据集、KL散度与熵、Min-Max与Percentile校准方法

各位同学,今天我们聊聊PTQ。训练后量化,说白了就是模型训练完了,我们再动手把它从FP32压缩到INT8。你想想看,这比QAT省事多了,不用重新训练,也不用动你的训练代码。我在项目里遇到的大部分场景,其实PTQ就够用了。

但PTQ也不是随便跑个脚本就能搞定的。核心问题就一个:怎么找到最优的量化参数?也就是那个scale和zero_point。找得不对,模型精度直接崩给你看。今天我就把PTQ里最关键的几个校准方法掰开揉碎了讲清楚。

4.1 校准数据集:PTQ的“试金石”

PTQ的第一步,你得准备一个校准数据集。这个数据集不是用来训练的,而是用来“观察”模型激活值分布的。

我个人习惯,校准集一般从训练集里抽个几百到几千张图片。数量不用太多,500到1000张通常就够。但有一点必须注意:校准集必须能代表真实数据分布。你拿猫的图片去校准一个做车牌识别的模型,那结果肯定翻车。

避坑指南: 我曾经在一个OCR项目里,偷懒只用了100张图片做校准。结果量化后模型在复杂场景下直接崩了。后来换成500张,覆盖了不同光照和字体,精度才稳住。校准集的质量,直接决定了PTQ的成败。

校准的过程是这样的:我们把校准数据喂给模型,跑一遍前向推理。在这个过程中,我们记录下每一层激活值的分布。有了这些分布数据,我们才能去算量化参数。

4.2 Min-Max校准:最简单,也最“脆弱”

Min-Max方法,名字就说明了一切。它直接取激活值的最小值和最大值,作为量化范围。

公式很简单:

scale = (max_val - min_val) / (Qmax - Qmin)
zero_point = Qmin - min_val / scale  // 通常取整

嗯,这里要注意。Min-Max的优点是计算快,实现简单。但缺点也很明显——它对异常值极其敏感

你想想看,如果激活值里有一个离群点,比如某个值特别大,那整个量化范围就被这个点撑大了。结果就是,大部分正常的激活值只能分配到很少的量化区间,精度损失惨重。

我的经验: 在大多数卷积神经网络里,Min-Max的效果其实还行。但在Transformer或者有残差连接的模型里,我建议你慎用。那些偶尔出现的极端激活值,会让Min-Max直接翻车。

4.3 Percentile校准:给Min-Max“剪枝”

Percentile方法,说白了就是给Min-Max加了个“保险”。我们不取真正的最大值,而是取一个百分位点。比如99.9%分位点。

什么意思呢?就是把所有激活值排个序,取第99.9%位置的那个值作为最大值。这样,那0.1%的极端值就被我们“剪掉”了。

代码实现也很直观:

import numpy as np

def percentile_calibration(activation_values, percentile=99.9):
    min_val = np.percentile(activation_values, 100 - percentile)
    max_val = np.percentile(activation_values, percentile)
    return min_val, max_val

Percentile的好处是鲁棒性更强。你剪掉的那部分异常值,对最终精度影响通常很小。但这里有个trade-off:剪得太多,信息丢失;剪得太少,异常值还在

小技巧: 我个人习惯,对于激活值,用99.9%或99.99%的分位点。对于权重,我通常直接用Min-Max,因为权重的分布相对稳定,异常值少。

4.4 KL散度与熵:信息论视角下的最优校准

终于到了重头戏。KL散度方法,是NVIDIA TensorRT里默认使用的校准方法。它的核心思想是:找一个量化后的分布,让它和原始FP32分布的“信息损失”最小

这个“信息损失”就是用KL散度来衡量的。KL散度越小,说明两个分布越接近,量化损失越小。

具体怎么做呢?我简单描述一下流程:

  1. 收集激活值的直方图(比如2048个bin)
  2. 从某个阈值开始,把直方图“截断”
  3. 将截断后的分布量化到INT8的256个bin
  4. 计算原始分布和量化后分布的KL散度
  5. 遍历所有可能的阈值,找到KL散度最小的那个

你可能会问,为什么要截断?因为INT8只有256个值,我们得把FP32的分布“压缩”进去。截断掉那些长尾部分,反而能让主体部分量化得更精细。

核心理解: KL散度方法本质上是在找一个最优的“截断点”。这个点平衡了两件事:一是截断带来的信息丢失,二是量化精度提升带来的收益。我在项目中对比过,KL散度方法通常比Min-Max和Percentile都要好,尤其是在精度敏感的模型上。

下面是一个简化的KL散度校准伪代码,帮你理解核心逻辑:

def kl_calibration(hist, bin_edges):
    # hist: 原始分布的直方图
    # bin_edges: 直方图的边界
    
    best_kl = float('inf')
    best_threshold = 0
    
    for threshold in range(128, 2048):  # 遍历可能的截断点
        # 截断分布
        clipped_hist = hist[:threshold].copy()
        clipped_hist[-1] += sum(hist[threshold:])
        
        # 量化到INT8的256个bin
        quantized_hist = np.zeros(256)
        # ... 重采样逻辑 ...
        
        # 计算KL散度
        kl = compute_kl(clipped_hist, quantized_hist)
        
        if kl < best_kl:
            best_kl = kl
            best_threshold = threshold
    
    return bin_edges[best_threshold]

4.5 三种方法对比:什么时候用哪个?

我把这三种方法的特点整理成了表格,方便你对比:

方法 计算复杂度 鲁棒性 适用场景
Min-Max 低(对异常值敏感) 权重量化、分布稳定的激活层
Percentile 大多数CNN模型,快速实验
KL散度 精度敏感模型、Transformer、生产环境

我个人在项目里的选择策略是这样的:

  • 快速验证阶段:先用Percentile(99.9%)跑一遍,看看精度损失在不在可接受范围内
  • 生产部署阶段:必须上KL散度,虽然慢一点,但精度最有保障
  • 权重量化:直接用Min-Max,省事
重要提醒: 不管你用哪种方法,校准完成后一定要做精度验证。用你的验证集跑一遍量化后的模型,对比和FP32模型的精度差异。如果精度损失超过1%,建议换校准方法,或者考虑上QAT。

好了,PTQ的校准方法就讲到这里。下一章我们会聊量化粒度——到底是逐层量化好,还是逐通道量化好?这里面也有不少门道。到时候见。