4、训练后量化(PTQ)原理:校准数据集、KL散度与熵、Min-Max与Percentile校准方法
各位同学,今天我们聊聊PTQ。训练后量化,说白了就是模型训练完了,我们再动手把它从FP32压缩到INT8。你想想看,这比QAT省事多了,不用重新训练,也不用动你的训练代码。我在项目里遇到的大部分场景,其实PTQ就够用了。
但PTQ也不是随便跑个脚本就能搞定的。核心问题就一个:怎么找到最优的量化参数?也就是那个scale和zero_point。找得不对,模型精度直接崩给你看。今天我就把PTQ里最关键的几个校准方法掰开揉碎了讲清楚。
4.1 校准数据集:PTQ的“试金石”
PTQ的第一步,你得准备一个校准数据集。这个数据集不是用来训练的,而是用来“观察”模型激活值分布的。
我个人习惯,校准集一般从训练集里抽个几百到几千张图片。数量不用太多,500到1000张通常就够。但有一点必须注意:校准集必须能代表真实数据分布。你拿猫的图片去校准一个做车牌识别的模型,那结果肯定翻车。
校准的过程是这样的:我们把校准数据喂给模型,跑一遍前向推理。在这个过程中,我们记录下每一层激活值的分布。有了这些分布数据,我们才能去算量化参数。
4.2 Min-Max校准:最简单,也最“脆弱”
Min-Max方法,名字就说明了一切。它直接取激活值的最小值和最大值,作为量化范围。
公式很简单:
scale = (max_val - min_val) / (Qmax - Qmin)
zero_point = Qmin - min_val / scale // 通常取整
嗯,这里要注意。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:剪得太多,信息丢失;剪得太少,异常值还在。
4.4 KL散度与熵:信息论视角下的最优校准
终于到了重头戏。KL散度方法,是NVIDIA TensorRT里默认使用的校准方法。它的核心思想是:找一个量化后的分布,让它和原始FP32分布的“信息损失”最小。
这个“信息损失”就是用KL散度来衡量的。KL散度越小,说明两个分布越接近,量化损失越小。
具体怎么做呢?我简单描述一下流程:
- 收集激活值的直方图(比如2048个bin)
- 从某个阈值开始,把直方图“截断”
- 将截断后的分布量化到INT8的256个bin
- 计算原始分布和量化后分布的KL散度
- 遍历所有可能的阈值,找到KL散度最小的那个
你可能会问,为什么要截断?因为INT8只有256个值,我们得把FP32的分布“压缩”进去。截断掉那些长尾部分,反而能让主体部分量化得更精细。
下面是一个简化的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,省事
好了,PTQ的校准方法就讲到这里。下一章我们会聊量化粒度——到底是逐层量化好,还是逐通道量化好?这里面也有不少门道。到时候见。