3. 训练后量化(PTQ):三种主流方法深度拆解

训练后量化,说白了就是模型训练完了,我们再动手把权重和激活值从 FP32 转成 INT8。好处很明显——不用重新训练,省时省力。但问题也来了:精度损失怎么控制?

我这些年经手过不少量化项目,从手机端的视觉模型到边缘侧的语音模型,PTQ 几乎是必走的一步。今天咱们就聊聊三种最常用的方法:MinMax、KL 散度、均方误差(MSE)。

嗯,先看一张总览图,心里有个谱。

训练后量化(PTQ)三种方法对比 训练后量化(PTQ) — 三种核心方法 PTQ 量化方法 MinMax 量化 KL 散度量化 MSE 量化 特点 • 简单粗暴 • 易受异常值影响 • 计算量最小 特点 • 最小化信息损失 • 适合非均匀分布 • 需要校准数据 特点 • 最小化重建误差 • 对异常值鲁棒 • 计算量最大

3.1 MinMax 量化:最直接,但别太天真

MinMax 的思路很简单:找到张量里的最大值和最小值,然后线性映射到 INT8 的范围 [-128, 127] 或 [0, 255]。

公式长这样:

scale = (max - min) / 255
zero_point = round(-min / scale)
quantized_value = round(value / scale) + zero_point

听起来完美,对吧?但我得提醒你——异常值是 MinMax 的噩梦。

我曾经踩过的坑: 有一次量化一个语音模型,某个权重层有个离群值 127.3,其他值都在 [-3.2, 3.1] 之间。MinMax 一算,scale 被拉得巨大,结果 INT8 表示里大部分值都挤在 0 附近,精度直接掉了 5 个点。后来换成 MSE 才救回来。

所以,MinMax 适合什么场景?

  • 数据分布均匀,没有明显异常值
  • 激活值范围稳定,比如 ReLU 后的输出
  • 快速原型验证,先跑通再优化

我个人习惯在项目初期先用 MinMax 跑一遍,看看精度损失有多大。如果损失在 1% 以内,那就直接用,省事。

3.2 KL 散度量化:让分布更接近

KL 散度量化,核心思想是:找到最优的截断阈值,让量化后的分布和原始分布尽可能相似。

你想想看,MinMax 把整个范围都保留了,但如果两端是长尾噪声,其实没必要。KL 散度会帮你「砍掉」那些不重要的尾巴。

具体做法:

  1. 收集校准数据,统计激活值的直方图
  2. 尝试不同的截断阈值 T
  3. 对每个 T,计算量化前后的 KL 散度
  4. 选 KL 散度最小的那个 T

关键点: KL 散度量化需要校准数据集。我一般从训练集里随机抽 500~1000 张图片或 1000 条语音片段就够了。太多反而慢,太少统计不准。

伪代码大概这样:

def kl_quantization(tensor, num_bins=2048):
    # 1. 统计直方图
    hist, bin_edges = np.histogram(tensor, bins=num_bins)
    # 2. 遍历可能的截断位置
    best_kl = float('inf')
    best_threshold = None
    for i in range(128, num_bins):  # 至少保留128个bin
        # 截断
        clipped_hist = hist[:i].copy()
        # 量化到128个bin
        quant_hist = np.zeros(128)
        # ... 重采样逻辑
        # 计算KL散度
        kl = compute_kl(clipped_hist, quant_hist)
        if kl < best_kl:
            best_kl = kl
            best_threshold = bin_edges[i]
    return best_threshold
小技巧: 实际工程中,NVIDIA 的 TensorRT 用的就是 KL 散度量化。如果你用 GPU 部署,直接调 TensorRT 的 calibrator 就行,不用自己写。

KL 散度量化的优势在哪?它对非均匀分布特别友好。比如某些层的激活值集中在 0 附近,但有个小尾巴,KL 散度会主动把尾巴截掉,保留主要信息。

3.3 MSE 量化:最小化误差,最稳妥

MSE 量化,说白了就是:找一个 scale 和 zero_point,让量化前后的均方误差最小。

公式:

MSE = mean((x - x_quant)²)

其中 x 是原始浮点值,x_quant 是反量化后的值。

为什么说它稳妥?因为它直接优化了「重建误差」。你想想看,量化本质上是有损压缩,MSE 就是让这个损失最小化。

我遇到过的情况:

  • 某个卷积层的权重分布是双峰分布,MinMax 和 KL 散度都表现不好
  • 换成 MSE 量化,手动搜索最优 scale,精度恢复了 2.3%

MSE 量化的搜索过程:

def mse_quantization(tensor, search_range=1000):
    min_val, max_val = tensor.min(), tensor.max()
    best_scale = None
    best_mse = float('inf')
    # 网格搜索 scale
    for i in range(search_range):
        scale = (max_val - min_val) / 255 * (i / search_range)
        # 量化 + 反量化
        quant = np.round(tensor / scale).clip(-128, 127)
        dequant = quant * scale
        mse = np.mean((tensor - dequant) ** 2)
        if mse < best_mse:
            best_mse = mse
            best_scale = scale
    return best_scale
注意: 网格搜索虽然有效,但计算量大。我一般先用粗粒度搜一遍,找到大致范围,再细粒度搜。这样能省 10 倍时间。

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

直接上表格,一目了然:

方法 计算量 精度保留 异常值鲁棒性 推荐场景
MinMax 极低 一般 差 快速验证、均匀分布
KL 散度 中等 较好 中等 非均匀分布、激活值量化
MSE 较高 最好 好 权重量化、精度敏感场景

3.5 我的实战建议

做了这么多量化项目,我总结了一套「三步走」策略:

  1. 先用 MinMax 跑 baseline,看看精度损失。如果损失 < 0.5%,直接收工。
  2. 如果损失大,换 KL 散度,特别是量化激活值时。校准数据选 500 条左右,跑一遍。
  3. 还不行?上 MSE,重点量化权重层。注意网格搜索的步长,别太粗也别太细。
避坑指南: 我曾经在量化一个 Transformer 模型时,发现某几层的 KL 散度结果特别差。后来逐层排查,发现是 LayerNorm 后的分布太窄,KL 散度找不到好的截断点。换成 MSE 后,问题解决。所以,不同层用不同方法,别一刀切。

嗯,PTQ 的三种方法就聊到这儿。MinMax 简单直接,KL 散度适合激活值,MSE 最稳妥但计算量大。实际项目中,我建议你灵活组合,别死磕一种方法。