量化类型:PTQ 与 QAT 的正面交锋

聊到模型量化,大家最先接触的两个概念就是 PTQ 和 QAT。说白了,它们就是两种不同的「减肥」策略。PTQ 是事后减肥,模型训练完了再动手;QAT 是带着减肥目标去训练,让模型从一开始就适应「瘦子」的体型。

我个人习惯把 PTQ 比作「快刀斩乱麻」,而 QAT 更像是「慢工出细活」。今天咱们就掰开揉碎了,看看这两兄弟到底差在哪。

一、PTQ:拿来就用的「事后诸葛亮」

Post-Training Quantization,名字很直白——训练后量化。你模型已经训好了,权重、激活值都固定了,这时候我拿一把「量化剪刀」咔嚓一下,把 FP32 的数值剪成 INT8。

PTQ 的核心流程:

  1. 加载预训练好的 FP32 模型
  2. 准备一小部分校准数据(几百张图片就够了)
  3. 跑一遍前向推理,统计每层激活值的范围(min/max)
  4. 根据统计结果计算量化参数(scale, zero_point)
  5. 把权重和激活值量化到 INT8

嗯,这里要注意:PTQ 不需要反向传播,也不需要重新训练。所以它快得惊人。我在项目中遇到过客户要求「今天给方案,明天上板子」,这种情况下 PTQ 就是救命稻草。

PTQ 的典型代码(PyTorch 示例):

import torch
from torch.quantization import quantize_dynamic

# 加载预训练模型
model = torch.load('resnet18_fp32.pth')
model.eval()

# 动态量化(仅量化权重)
quantized_model = quantize_dynamic(
    model, 
    {torch.nn.Linear, torch.nn.Conv2d},
    dtype=torch.qint8
)

# 保存量化模型
torch.save(quantized_model.state_dict(), 'resnet18_int8.pt')

你看,代码就这么几行。但 PTQ 有个致命弱点——精度损失。尤其是当模型本身很小(比如 MobileNet)或者任务对精度极其敏感(比如目标检测的边框回归)时,PTQ 掉点可能让你怀疑人生。

我曾经踩过的坑: 有一次用 PTQ 量化一个语义分割模型,校准集只用了 100 张图。结果量化后 mIoU 从 0.78 掉到了 0.52。后来发现是激活值分布有长尾,校准集没覆盖到。所以 PTQ 的校准数据一定要有代表性,别偷懒。

二、QAT:带着「镣铐」跳舞

Quantization-Aware Training,量化感知训练。这名字起得好——训练过程中就「感知」到量化这件事。说白了,就是在训练时模拟量化带来的误差,让模型学会「将错就错」。

QAT 的核心流程:

  1. 在 FP32 模型中插入伪量化节点(FakeQuantize)
  2. 用这些节点模拟量化-反量化的过程(前向时量化,反向时直通估计)
  3. 用训练数据继续微调模型(通常需要几个 epoch)
  4. 训练完成后,移除伪量化节点,导出真正的 INT8 模型

你想想看,QAT 相当于让模型在「戴着镣铐」的情况下练跑步。虽然训练时慢一点,但真正上场(推理)时,它已经习惯了 INT8 的精度限制。

QAT 的典型代码(PyTorch 示例):

import torch
from torch.quantization import prepare_qat, convert

# 加载预训练模型
model = torch.load('resnet18_fp32.pth')
model.train()

# 准备 QAT 配置
model.qconfig = torch.quantization.get_default_qat_qconfig('fbgemm')
model = prepare_qat(model)

# 微调几个 epoch
for epoch in range(5):
    for images, labels in train_loader:
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

# 转换为量化模型
model.eval()
quantized_model = convert(model)
torch.save(quantized_model.state_dict(), 'resnet18_qat_int8.pt')

这里有个关键点:QAT 的反向传播用的是 Straight-Through Estimator(STE)。因为量化操作本身不可导,STE 的做法是「前向时该量化就量化,反向时假装量化不存在,直接把梯度传过去」。这招虽然粗糙,但实践证明很管用。

我的个人经验: QAT 微调时,学习率要调小。我一般用原始训练学习率的 1/10 到 1/100。太大了模型会「忘记」之前学到的知识,太小了又学不动量化带来的误差补偿。另外,BN 层的统计量在 QAT 中最好重新计算,别用预训练时的 running mean/var。

三、PTQ vs QAT:一张表说清楚

对比维度 PTQ QAT
是否需要训练 不需要 需要微调
所需数据 少量校准集(100-500 张) 完整训练集(或子集)
耗时 几分钟到几小时 几小时到几天
精度损失 通常 1%-5% 通常 <1%
适用场景 大模型、快速部署、精度不敏感 小模型、精度敏感、边缘设备
实现难度 低(几行代码) 中(需调参)

说白了,PTQ 适合「先跑起来再说」的场景,QAT 适合「必须保住精度」的场景。我个人的选择标准很简单:如果 PTQ 后精度掉点超过 2%,我就上 QAT。

四、实战中的选择策略

在实际项目中,我一般按这个顺序来:

  1. 先试 PTQ:用默认配置跑一遍,看看精度掉多少
  2. 优化 PTQ:如果掉点严重,试试不同的校准方法(比如 percentile、KL 散度)
  3. 上 QAT:如果 PTQ 优化后还不行,那就老老实实做 QAT 微调
  4. 混合策略:有些层对量化敏感(比如第一层、最后一层),可以保持 FP16,其他层用 INT8

避坑指南: 我曾经在一个语音识别模型上,PTQ 掉点 3%,QAT 微调了 10 个 epoch 才追回来。但后来发现,其实只需要对 attention 层做 QAT,其他层 PTQ 就够了。所以别一股脑全上 QAT,先分析每层的量化敏感度。

另外,边缘设备上还有个现实问题:有些芯片的 NPU 只支持对称量化(symmetric quantization),而 PTQ 默认可能用非对称。这时候 QAT 的优势就出来了——你可以在训练时直接指定量化方案,确保部署时无缝对接。

五、总结一下

PTQ 和 QAT 不是非此即彼的关系。我见过很多团队的做法是:先用 PTQ 快速验证可行性,再用 QAT 做精度调优。毕竟在工业界,时间就是金钱。

最后说一句:别迷信 QAT。如果你的模型本身训练得就不够好(比如过拟合、欠拟合),QAT 也救不了你。先把 FP32 模型做到极致,再考虑量化的事。

嗯,下一章咱们聊聊具体的量化算法——对称量化 vs 非对称量化,以及它们在不同硬件上的表现。到时候我会分享一个在树莓派上踩过的坑,保证让你印象深刻。