3、模型量化基础(下):训练后量化(PTQ)实战、量化感知训练(QAT)入门、量化误差分析与调试

好,咱们接着聊。上一节我们把量化的理论框架搭起来了,这一节直接上硬菜——动手做。我会带你走一遍训练后量化(PTQ)的完整流程,再入门一下量化感知训练(QAT),最后聊聊怎么分析那些让人头疼的量化误差。

说实话,PTQ 是我在项目里用得最多的方法。为什么?因为它快。你模型都训好了,直接拿来量化,不需要重新训练。但快归快,坑也不少。我踩过的坑,今天一并告诉你。

3.1 训练后量化(PTQ)实战

PTQ 的核心思路很简单:用一小部分校准数据,统计出每层激活值的分布,然后算出最佳的缩放因子和零点。说白了,就是给模型做一次「静态分析」。

我个人习惯用 PyTorch 自带的量化工具,配合 Intel 的 IPEX 或者 NVIDIA 的 TensorRT。下面给一个最基础的 PTQ 流程,你一看就懂。

import torch
import torch.quantization as quant

# 1. 加载预训练模型(假设是浮点模型)
model = MyModel().eval()

# 2. 设置量化配置(这里用最常用的 per-tensor, symmetric)
model.qconfig = quant.get_default_qconfig('fbgemm')

# 3. 准备量化(插入观察器)
model_prepared = quant.prepare(model, inplace=False)

# 4. 用校准数据跑一遍(关键!)
with torch.no_grad():
    for images in calibration_loader:
        model_prepared(images)

# 5. 转换为量化模型
model_quantized = quant.convert(model_prepared, inplace=False)

# 6. 保存
torch.jit.save(torch.jit.script(model_quantized), 'model_quantized.pt')

嗯,这里要注意:校准数据千万别用训练集,最好用验证集里随机抽的几百张。我见过有人直接用训练集做校准,结果量化后精度反而比浮点模型还高——这其实是过拟合了,部署后一测真实数据就露馅。

我的小技巧: 校准数据量一般取 100~500 张就够了。太多反而让统计值偏向训练集分布,太少又覆盖不了激活值的动态范围。我通常取 200 张,效果最稳。

3.2 量化感知训练(QAT)入门

PTQ 虽然快,但有时候精度掉得厉害。尤其是那些对数值敏感的小模型,比如 MobileNet、SqueezeNet,量化后可能直接掉 2~3 个点。这时候就得请出 QAT 了。

QAT 的原理其实不复杂:在训练过程中模拟量化误差,让模型自己去适应。你想想看,模型在训练时就「知道」自己将来要被量化,它就会学得「皮实」一点。

我在项目中遇到过最典型的场景:一个语音唤醒模型,PTQ 后唤醒率从 95% 掉到 82%,完全没法用。后来上了 QAT,只训了 5 个 epoch,唤醒率就回到了 93%。

# QAT 流程(基于 PyTorch)
model = MyModel().train()

# 设置 QAT 配置(注意和 PTQ 不同)
model.qconfig = quant.get_default_qat_qconfig('fbgemm')

# 准备 QAT
model_prepared = quant.prepare_qat(model, inplace=False)

# 正常训练(但学习率要调小,我一般用原学习率的 1/10)
optimizer = torch.optim.SGD(model_prepared.parameters(), lr=1e-4)
for epoch in range(5):
    for images, labels in train_loader:
        output = model_prepared(images)
        loss = criterion(output, labels)
        loss.backward()
        optimizer.step()

# 最后转换为量化模型
model_quantized = quant.convert(model_prepared, inplace=False)
避坑指南: 我曾经犯过一个低级错误——QAT 训练完后忘了调用 convert(),直接拿 prepared 模型去部署。结果推理速度比浮点模型还慢,因为那些 FakeQuantize 节点还在做模拟量化。记住:QAT 训练完一定要 convert!

3.3 量化误差分析与调试

量化完了,精度掉了,怎么办?别慌,先分析误差来源。我总结了一套「三步定位法」,屡试不爽。

3.3.1 第一步:逐层对比输出

把浮点模型和量化模型的每一层输出都拿出来对比。看哪一层的误差最大。误差可以用余弦相似度或者均方误差来衡量。

def compare_layer_outputs(float_model, quant_model, input_tensor):
    # 注册 hook 来获取中间层输出
    float_outputs = {}
    quant_outputs = {}
    
    def hook_fn(name, storage):
        def hook(module, input, output):
            storage[name] = output.detach()
        return hook
    
    # 对每一层注册 hook
    for name, module in float_model.named_modules():
        module.register_forward_hook(hook_fn(name, float_outputs))
    for name, module in quant_model.named_modules():
        module.register_forward_hook(hook_fn(name, quant_outputs))
    
    # 前向传播
    float_model(input_tensor)
    quant_model(input_tensor)
    
    # 计算每层的余弦相似度
    for name in float_outputs:
        cos_sim = torch.nn.functional.cosine_similarity(
            float_outputs[name].flatten(), 
            quant_outputs[name].flatten(), 
            dim=0
        )
        print(f'Layer {name}: cosine similarity = {cos_sim.item():.4f}')

你会发现,误差通常集中在某几层。我遇到最多的是第一层卷积和最后一层全连接。第一层是因为输入是 RGB 图像,数值范围大;最后一层是因为输出 logits 的分布很稀疏。

3.3.2 第二步:检查量化参数

如果某层误差大,看看它的缩放因子和零点是不是合理。有时候校准数据没选好,导致缩放因子偏大或偏小。

问题现象 可能原因 解决办法
某层输出全为 0 缩放因子太小,所有值都被截断到 0 改用 per-channel 量化,或调整校准数据
某层输出饱和(全是 127 或 -128) 缩放因子太大,值被截断到边界 改用更保守的校准方法(如 percentile)
精度整体下降 激活值分布不均匀 尝试不同的量化粒度(per-tensor vs per-channel)

3.3.3 第三步:针对性修复

定位到问题层后,有几种修复手段:

  • 混合精度量化: 对敏感层保持 FP16 或 FP32,其他层用 INT8。我有个项目就是这么干的,精度只掉了 0.1%,速度还快了 3 倍。
  • 调整校准策略: 默认用 min-max 校准,可以试试 percentile(比如取 99.9% 分位点),能有效抑制 outlier 的影响。
  • 增加 QAT 训练轮数: 有时候 5 个 epoch 不够,试试 10 个。但别太多,否则模型会过拟合到量化噪声上。
核心总结: PTQ 是首选,快且简单;如果精度不够,上 QAT;如果还不行,用混合精度。误差分析时,逐层对比 + 检查量化参数,基本能解决 90% 的问题。

好了,这一节的内容就到这儿。量化这东西,光看理论总觉得玄乎,动手做一遍就全明白了。下一节我们会讲更高级的量化技术——比如权值共享量化、二值化网络,到时候再聊。