3、训练后量化(PTQ):Calibration数据集选择与KL散度调优实战
训练后量化,说白了就是模型训练完了,我们再动手把它从FP32压缩到INT8。这活儿看着简单,但坑不少。我最早做PTQ的时候,觉得随便找几张图跑一遍校准就完事了,结果模型精度直接掉了两三个点,找了好久才发现是Calibration数据集没选对。
今天我们就聊聊PTQ里最关键的两个环节:Calibration数据集怎么选,以及KL散度怎么调。这两个点搞定了,PTQ基本就稳了。
3.1 什么是Calibration?为什么它这么重要?
Calibration,直译过来就是“校准”。在PTQ里,它的任务是:用一小批数据跑一遍模型,统计出每一层激活值的分布范围。然后量化器根据这个范围,决定怎么把FP32的数值映射到INT8的256个离散值上。
你想想看,如果校准数据选得不对,统计出来的范围就偏了。比如你拿了一堆全是白天场景的图片去校准,模型对夜晚场景的激活值就可能被截断,精度自然就崩了。
核心结论:Calibration数据集的质量,直接决定了PTQ的精度上限。数据集选得好,INT8模型精度可以接近FP32;选得不好,掉点2-3%是常事。
3.2 Calibration数据集的选择策略
我个人习惯把Calibration数据集的选择分成三个维度来考虑:代表性、多样性、数量。
| 维度 | 说明 | 常见误区 |
|---|---|---|
| 代表性 | 校准数据必须覆盖真实部署场景的分布 | 只用训练集做校准,忽略了部署时的数据偏移 |
| 多样性 | 包含不同光照、角度、背景、类别等变化 | 校准集全是同类样本,导致某些通道量化误差大 |
| 数量 | 一般500-2000张图片足够,太多反而过拟合 | 以为越多越好,结果校准时间翻倍,精度没提升 |
我在项目中遇到过一种情况:用训练集的随机子集做校准,效果还行,但一换到实际场景的测试集,精度就掉。后来我改用验证集+少量真实场景数据混合校准,问题就解决了。
小技巧:如果你不确定校准集选得好不好,可以跑一次“校准集分布 vs 真实部署分布”的对比。用直方图看一眼激活值的分布是否接近,心里就有底了。
3.3 KL散度:量化误差的“度量尺”
校准数据选好了,接下来就是怎么用这些数据去确定量化阈值。这里就要请出KL散度了。
KL散度,全称Kullback-Leibler divergence,用来衡量两个概率分布之间的差异。在量化里,我们用它来比较原始FP32激活值的分布和量化后INT8激活值的分布。KL散度越小,说明量化损失越小。
具体做法是:
- 用校准数据跑一遍模型,收集每一层的激活值直方图(FP32)。
- 尝试不同的量化阈值(比如从最大值往下砍一点),计算量化后的分布。
- 计算原始分布和量化后分布的KL散度。
- 选择KL散度最小的那个阈值。
嗯,这里要注意:KL散度不是对称的。我们关心的是量化后的分布如何“逼近”原始分布,所以方向是D_KL(原始 || 量化)。
3.4 KL散度调优实战:代码与参数
下面我贴一段核心代码,展示如何用Python实现KL散度校准。这是我在NVIDIA TensorRT源码基础上简化过的版本,逻辑是一样的。
import numpy as np
def kl_divergence(p, q):
"""计算KL散度 D_KL(p || q)"""
p = np.asarray(p, dtype=np.float32)
q = np.asarray(q, dtype=np.float32)
# 避免除零和log(0)
p = p / np.sum(p) + 1e-10
q = q / np.sum(q) + 1e-10
return np.sum(p * np.log(p / q))
def calibrate_threshold(hist, bin_edges, num_bins=2048):
"""
通过KL散度寻找最优量化阈值
hist: 激活值直方图
bin_edges: 直方图边界
"""
# 将直方图归一化为概率分布
hist = hist.astype(np.float32)
hist = hist / np.sum(hist)
best_threshold = bin_edges[-1]
min_kl = float('inf')
# 从高到低尝试不同的截断位置
for i in range(num_bins // 2, num_bins):
# 截断直方图
truncated_hist = hist[:i].copy()
# 将截断部分累加到最后一个bin
truncated_hist[-1] += np.sum(hist[i:])
# 量化到128个bin(INT8正半轴)
quantized_hist = np.zeros(128)
bin_size = i / 128
for j in range(128):
start = int(j * bin_size)
end = int((j + 1) * bin_size)
if end > i:
end = i
quantized_hist[j] = np.sum(truncated_hist[start:end])
# 反量化回原始bin数
dequantized_hist = np.zeros(i)
for j in range(128):
start = int(j * bin_size)
end = int((j + 1) * bin_size)
if end > i:
end = i
dequantized_hist[start:end] = quantized_hist[j] / (end - start)
# 计算KL散度
kl = kl_divergence(truncated_hist, dequantized_hist)
if kl < min_kl:
min_kl = kl
best_threshold = bin_edges[i]
return best_threshold, min_kl
避坑指南:我曾经在调KL散度时忽略了一个细节——直方图的bin数量。bin太少,分布信息丢失严重,KL散度算出来不准;bin太多,计算量暴增,而且容易过拟合。我一般用2048个bin,效果比较稳定。
3.5 实战中的调优经验
光有代码还不够,实际调优时还有几个点要注意:
- 逐层校准 vs 全局校准:我建议逐层做KL散度校准。每层的激活值分布差异很大,全局用一个阈值会损失精度。
- 对称量化 vs 非对称量化:对于ReLU这类激活函数,输出都是非负的,用非对称量化更合适。KL散度校准在非对称量化下效果更好。
- 校准集数量:我试过从100张到5000张图片,发现1000张左右是个甜点。太少统计不稳定,太多边际收益递减。
为什么会这样?因为KL散度本质上是在找“信息损失最小”的截断点。校准集太小,直方图稀疏,KL散度容易找到假的最优解;校准集太大,直方图过于平滑,反而模糊了关键区域的细节。
3.6 知识结构图:PTQ校准与KL散度调优流程
下面这张图总结了整个流程,方便你对照理解。
3.7 总结:PTQ校准的核心要点
最后,我把今天的内容浓缩成几个关键点,方便你记:
- 校准集选不好,后面全白费。代表性、多样性、数量三者缺一不可。
- KL散度是量化误差的度量尺。用它来找最优阈值,比简单用最大值或百分比截断靠谱得多。
- 逐层校准,别偷懒。每层的分布不一样,统一阈值会损失精度。
- 校准集数量1000张左右足够。太多太少都不好。
嗯,PTQ这块内容其实挺深的。今天讲的Calibration和KL散度,是PTQ里最核心的两个环节。搞懂了它们,你就能在精度和性能之间找到那个最优的平衡点。