3、训练后量化(PTQ):动态量化、静态量化、Calibration数据集的选择与构建

各位同学,咱们今天聊聊训练后量化(PTQ)。

说实话,在模型压缩这个领域,PTQ 是我个人用得最多的技术。为什么?因为它省事。你想想看,一个训练好的大模型,动辄几十上百 GB,要重新训一遍?成本太高了。PTQ 就像给模型做一次「瘦身手术」,不动筋骨,只减脂肪。

3.1 动态量化 vs 静态量化:到底选哪个?

先搞清楚概念。量化,说白了就是把模型里的 FP32 参数,映射到 INT8 甚至更低精度。但「怎么映射」这件事,分成了两派。

3.1.1 动态量化

动态量化,我习惯叫它「懒人量化」。为什么?因为它只在推理时,才把权重从 FP32 转成 INT8。激活值呢?还是 FP32,每次计算前临时转一下。

优点很明显:

  • 实现简单——几行代码就能跑起来
  • 无需数据——不需要额外的校准数据集
  • 对精度影响小——因为激活值保留了高精度

缺点也直接:

  • 推理速度提升有限——毕竟每次都要做类型转换
  • 不适合计算密集型场景——比如 Transformer 的注意力层

我的经验:动态量化最适合那些「权重占大头,激活值计算量小」的模型。比如一些简单的 MLP 或者小规模的 LSTM。我在做语音唤醒词检测时,就用动态量化把模型从 50MB 压到 12MB,精度只掉了 0.3%。

3.1.2 静态量化

静态量化就「硬核」多了。它不光量化权重,连激活值也一起量化了。但这里有个关键问题:激活值的范围是动态变化的,你怎么知道它最大是多少?

这就需要用到 Calibration(校准)了。我们拿一批有代表性的数据,跑一遍推理,统计出每一层激活值的 min/max 或者分布,然后「固定」下来。之后推理时,就用这个固定的范围做量化。

优点:

  • 推理速度更快——所有计算都在 INT8 下完成
  • 内存占用更低——激活值也压缩了

缺点:

  • 需要校准数据——而且数据质量直接影响精度
  • 实现复杂——要处理各种边界情况

注意:静态量化不是万能的。我曾经在一个 NLP 模型上试过,校准集选得不好,结果精度直接掉了 5% 以上。后来换了更贴近真实分布的校准数据,才把精度拉回来。

3.2 Calibration 数据集:选对了,事半功倍

说到校准数据集,这是 PTQ 里最容易踩坑的地方。我见过太多人随便拿几百张图片或者几段文本就去校准了,结果模型上线后表现一塌糊涂。

为什么会这样?

因为校准数据集决定了量化时每一层的「缩放因子」和「零点」。如果校准数据不能代表真实推理时的数据分布,那量化后的模型就会「偏科」——对校准数据表现好,对真实数据表现差。

3.2.1 选择原则

我个人总结了三条原则:

  1. 代表性——校准数据必须覆盖真实场景中的各种情况。比如做文本分类,不能只拿新闻语料,还得有社交媒体、技术文档等。
  2. 多样性——不要只用「好样本」,也要用「坏样本」。比如图像分类,模糊的、光照不足的、遮挡的图片都要有。
  3. 适量性——不是越多越好。我一般用 500-2000 条数据就够了。太多了反而可能引入噪声。

小技巧:如果你不确定校准集够不够好,可以做一个「交叉验证」——用不同的校准集分别量化模型,然后在同一个测试集上对比精度。选精度最高的那个校准方案。

3.2.2 构建方法

构建校准数据集,我通常分三步走:

第一步:收集原始数据

从训练集中随机抽取一部分。但注意,不要只抽前 10% 的数据,要打乱后均匀抽取。我习惯用 random.shuffle 后再取。

第二步:数据增强

对抽取的数据做简单的增强。比如文本数据可以加一些拼写错误、同义词替换;图像数据可以做随机裁剪、旋转。这样能让校准集更「皮实」。

第三步:验证分布

跑一遍推理,统计每一层激活值的分布。如果发现某些层的分布和训练时差异很大,说明校准集可能有问题。这时候我会手动检查一下数据。

# 一个简单的校准集构建示例(PyTorch 风格)
import torch
from torch.utils.data import DataLoader, Subset

# 假设 train_dataset 是原始训练集
# 随机抽取 1000 条数据作为校准集
indices = torch.randperm(len(train_dataset))[:1000]
calib_dataset = Subset(train_dataset, indices)
calib_loader = DataLoader(calib_dataset, batch_size=32, shuffle=True)

# 跑一遍推理,收集激活值分布
def collect_activations(model, loader):
    model.eval()
    activations = {}
    with torch.no_grad():
        for batch in loader:
            # 前向传播,同时注册 hook 收集中间层输出
            # ...(具体实现略)
            pass
    return activations

3.3 实战中的坑与避坑指南

嗯,这里我要多说几句。PTQ 看着简单,但实际做起来,坑不少。

坑一:校准集和测试集分布不一致

我曾经做过一个情感分析模型,校准集用的是电影评论,结果上线后用户输入的是产品评价。模型直接「懵」了,准确率从 92% 掉到 78%。后来我重新用产品评价数据做了校准,才恢复正常。

坑二:校准数据量太少

有些人觉得 100 条数据就够了。其实不然。我测试过,100 条和 1000 条校准数据,量化后的模型精度能差 1-2 个百分点。尤其是对于深层网络,数据量太少会导致某些层的激活值范围估计不准。

坑三:忽略了异常值

校准数据里如果混入了异常值(比如全黑图片、全零文本),会导致量化范围被「拉宽」,从而降低精度。我建议在收集校准数据后,先做一轮异常值检测,把明显偏离分布的数据剔除掉。

避坑指南:我曾经在一个 BERT 模型上做 PTQ,校准集选了 500 条新闻文本。结果发现模型在「科技类」新闻上表现很好,但在「娱乐类」上差得离谱。后来一查,原来校准集里 80% 都是科技新闻。所以,一定要保证校准集的类别分布和真实场景一致

3.4 总结一下

PTQ 的核心就三件事:

  • 选对量化方式——动态量化省事但效果有限,静态量化效果好但需要校准
  • 建好校准集——代表性、多样性、适量性,缺一不可
  • 验证再验证——量化后一定要在真实测试集上跑一遍,别偷懒

最后说一句,PTQ 不是银弹。如果你的模型对精度极其敏感(比如医疗诊断、金融风控),那还是老老实实做 QAT(量化感知训练)吧。但如果你只是想快速部署一个 demo 或者对精度要求不那么苛刻,PTQ 绝对是最优解。

好了,这一章就到这里。下一章我们聊聊 QAT,那个更「硬核」的量化方法。