4、激活量化与校准:校准数据集的选择,KL散度与MinMax校准方法
聊到激活量化,我得先坦白一件事。
几年前我第一次做模型量化时,天真地以为只要把权重和激活值都转成INT8就完事了。结果模型跑起来,精度直接崩了——分类任务从85%掉到30%。我当时就懵了。后来才发现,问题出在激活值的分布上。权重还好说,但激活值这东西,不同层、不同输入,分布差异太大了。
所以今天咱们重点聊两个事:校准数据集怎么选,以及KL散度和MinMax这两种校准方法。
4.1 校准数据集:选对了,量化就成功了一半
激活量化需要先跑一遍推理,收集每层激活值的统计信息。这个过程就叫「校准」。校准用的数据集,直接决定了量化参数的准确性。
我个人习惯,校准数据集至少要满足三个条件:
- 代表性:数据分布要和真实场景一致。比如你做图像分类,校准集里不能全是猫,得包含各类物体。
- 多样性:覆盖尽可能多的激活值范围。我见过有人只用100张图片做校准,结果模型在边缘场景下直接崩了。
- 适量性:不是越多越好。一般来说,500~2000张图片就够用了。太多反而增加校准时间,收益却微乎其微。
另外,校准数据要尽量覆盖激活值的极端情况。比如ReLU层的输出,大部分是0,但偶尔会有很大的值。如果校准集里没有这些大值,量化时就会把它们截断,造成精度损失。
4.2 MinMax校准:简单直接,但有坑
MinMax是最直观的校准方法。说白了,就是找到激活值的最小值和最大值,然后线性映射到INT8的[-128, 127]范围。
公式很简单:
scale = (max - min) / 255
zero_point = -round(min / scale)
嗯,这里要注意:MinMax对异常值非常敏感。你想想看,如果激活值里有一个离群点,比如某个神经元输出了1000,其他值都在0~10之间。那MinMax会把整个量化范围拉宽,导致大部分值的精度都浪费了。
所以,MinMax适合激活值分布比较均匀、没有明显离群点的情况。比如一些简单的CNN模型,激活值分布很规整,用MinMax完全没问题。
4.3 KL散度校准:更聪明,也更复杂
KL散度校准,说白了就是找一个阈值,把激活值截断到某个范围,使得截断后的分布和原始分布尽可能相似。
为什么需要截断?因为INT8只有256个离散值,而激活值可能是连续的。如果直接映射,那些分布很长的尾巴会浪费大量量化精度。截断掉尾巴,反而能提高主体部分的精度。
具体做法是这样的:
- 收集激活值的直方图(比如2048个bin)。
- 从某个阈值开始,尝试截断直方图的右边部分。
- 把截断后的直方图量化到128个bin(INT8的正半轴)。
- 计算原始分布和量化后分布的KL散度。
- 遍历所有可能的阈值,找到KL散度最小的那个。
代码实现大致如下:
def calibrate_kl(hist, bin_edges, num_bins=128):
# hist: 原始直方图
# bin_edges: 直方图的边界
# num_bins: INT8的bin数量
best_kl = float('inf')
best_threshold = 0
for threshold in range(num_bins, len(hist)):
# 截断直方图
truncated_hist = hist[:threshold].copy()
truncated_hist[-1] += hist[threshold:].sum()
# 量化到num_bins个bin
quantized_hist = np.zeros(num_bins)
bin_size = threshold / num_bins
for i in range(num_bins):
start = int(i * bin_size)
end = int((i + 1) * bin_size)
quantized_hist[i] = truncated_hist[start:end].sum()
# 归一化
truncated_hist = truncated_hist / truncated_hist.sum()
quantized_hist = quantized_hist / quantized_hist.sum()
# 计算KL散度
kl = (truncated_hist * np.log(truncated_hist / quantized_hist)).sum()
if kl < best_kl:
best_kl = kl
best_threshold = threshold
return bin_edges[best_threshold]
你可能会问:为什么KL散度比MinMax好?
原因很简单:KL散度会主动舍弃那些不重要的尾巴。对于大部分神经网络来说,激活值的分布是类似高斯分布的,中间密集、两边稀疏。KL散度会找到一个平衡点,把稀疏的尾巴截掉,把宝贵的量化精度留给中间的主体部分。
| 方法 | ResNet-50 Top-1 | BERT F1 | YOLOv5 mAP |
|---|---|---|---|
| FP32 | 76.13% | 88.5% | 50.4% |
| MinMax INT8 | 74.82% | 85.1% | 48.7% |
| KL散度 INT8 | 75.91% | 87.8% | 49.9% |
从表格能看出来,KL散度在大部分场景下都比MinMax好。尤其是BERT这种对精度敏感的模型,差距更明显。
4.4 如何选择?我的建议
说了这么多,到底该用哪种方法?
- 如果你追求简单、快速:用MinMax。适合那些激活值分布很规整的模型,比如MobileNet、SqueezeNet。
- 如果你在意精度:用KL散度。适合大模型、精度敏感的场景,比如BERT、YOLO。
- 如果你不确定:两种都试一下。我一般会在校准后跑一遍验证集,对比精度损失。哪个好就用哪个。
最后说一句:校准数据集的质量,比校准方法更重要。我见过有人用KL散度但校准集选得不好,结果还不如MinMax。所以,先把校准数据集搞对,再谈方法优化。
嗯,今天就聊到这儿。下一节咱们聊聊权重量化,那个坑更多,到时候再细说。