4. 模型量化原理(下):量化感知训练(QAT)与训练后量化(PTQ)实战对比
上一节我们把量化的数学原理和对称/非对称这些基础概念讲透了。今天咱们直接上硬菜——两种主流量化方案的实战对比。说白了,就是回答一个问题:我到底该用PTQ还是QAT?
我个人习惯是,先跑PTQ看看效果。如果精度掉得不多,那就直接用PTQ,省时省力。但如果模型对精度特别敏感,比如人脸识别或者自动驾驶场景,那QAT几乎是绕不开的。嗯,咱们一步步来看。
4.1 训练后量化(PTQ):拿来就用的“快刀手”
PTQ的全称是Post-Training Quantization,翻译过来就是“训练完了再量化”。你模型已经训好了,权重都固定了,我直接拿过来做量化校准。这就像你做好了一锅汤,出锅前再调个味。
PTQ的核心流程:
- 加载预训练好的浮点模型(FP32)
- 准备一小批校准数据(通常几百张图片就够了)
- 跑一遍前向推理,统计每一层激活值的分布
- 根据统计结果计算量化参数(scale和zero_point)
- 把权重和激活值都量化到INT8
我在项目中遇到过最典型的场景:一个分类模型,FP32精度92.3%,直接PTQ量化到INT8后精度掉到了91.8%。只掉了0.5个点,但推理速度翻了将近3倍。这种收益,你说香不香?
关键点:PTQ不需要重新训练模型,也不需要访问原始训练数据(只需要少量校准数据)。这是它最大的优势。
4.2 量化感知训练(QAT):精度保卫战的“终极武器”
QAT就不一样了。它是在训练过程中就“模拟”量化效果,让模型自己去适应量化带来的精度损失。说白了,就是让模型提前知道“我以后要被量化了,你们各层都给我悠着点”。
QAT的核心流程:
- 在浮点模型中插入伪量化节点(FakeQuantize)
- 用原始训练数据继续训练(通常是微调几个epoch)
- 训练过程中,前向传播走量化路径,反向传播走浮点路径
- 训练完成后,移除伪量化节点,导出真正的INT8模型
这里有个细节很多人会忽略:QAT训练时,权重更新用的是浮点梯度,但前向计算用的是量化后的值。这个“伪量化”操作,说白了就是在前向时把浮点数先量化再反量化,模拟出量化误差。
我的经验:QAT一般只需要微调3-5个epoch就够了。我曾经试过一个BERT模型,PTQ掉精度2.3%,QAT微调4个epoch后,精度只掉了0.4%。这个差距在工业界就是“能用”和“不能用”的区别。
4.3 实战对比:PTQ vs QAT,到底选哪个?
咱们直接上表格,一目了然。
| 对比维度 | PTQ(训练后量化) | QAT(量化感知训练) |
|---|---|---|
| 是否需要训练 | 不需要 | 需要微调 |
| 是否需要原始训练数据 | 仅需少量校准数据 | 需要完整训练数据 |
| 精度损失 | 通常0.5%~2% | 通常0.1%~0.5% |
| 部署时间 | 几分钟到几小时 | 几小时到几天 |
| 适用场景 | 大模型、精度不敏感任务 | 小模型、精度敏感任务 |
| 实现复杂度 | 低 | 中高 |
你想想看,如果你的模型有10亿参数,跑一次PTQ可能只需要1小时。但跑一次QAT,光数据加载和训练就得花好几天。所以我的建议是:先PTQ,精度达标就用PTQ;不达标再上QAT。
4.4 代码实战:用PyTorch实现PTQ和QAT
光说不练假把式。咱们直接上代码,看看PTQ和QAT在PyTorch里到底怎么实现。
4.4.1 PTQ实现(训练后量化)
import torch
import torch.quantization as quant
# 加载预训练模型(FP32)
model = torch.load('resnet18_fp32.pth')
model.eval()
# 配置量化后端
model.qconfig = quant.get_default_qconfig('fbgemm')
# 准备量化(插入观察器)
model_prepared = quant.prepare(model, inplace=False)
# 用校准数据跑一遍前向(这里用dummy数据演示)
with torch.no_grad():
for i in range(100):
dummy_input = torch.randn(1, 3, 224, 224)
model_prepared(dummy_input)
# 真正执行量化
model_quantized = quant.convert(model_prepared, inplace=False)
# 保存INT8模型
torch.save(model_quantized.state_dict(), 'resnet18_int8_ptq.pth')
print("PTQ量化完成!")
这段代码看着简单,但有个坑:校准数据一定要有代表性。我曾经见过有人用随机噪声做校准,结果量化后精度直接崩了。校准数据最好是从验证集里随机抽的,能覆盖模型常见的输入分布。
4.4.2 QAT实现(量化感知训练)
import torch
import torch.quantization as quant
# 加载预训练模型
model = torch.load('resnet18_fp32.pth')
model.train()
# 配置QAT
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
# 准备QAT(插入伪量化节点)
model_prepared = quant.prepare_qat(model, inplace=False)
# 继续微调(通常3-5个epoch)
optimizer = torch.optim.SGD(model_prepared.parameters(), lr=1e-4)
criterion = torch.nn.CrossEntropyLoss()
for epoch in range(5):
for images, labels in train_loader:
optimizer.zero_grad()
outputs = model_prepared(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1} 完成")
# 训练完成后,转换为INT8模型
model_prepared.eval()
model_quantized = quant.convert(model_prepared, inplace=False)
# 保存
torch.save(model_quantized.state_dict(), 'resnet18_int8_qat.pth')
print("QAT量化完成!")
避坑指南:我曾经在QAT训练时忘记把模型切换到train模式,结果伪量化节点没生效,微调了10个epoch精度一点没变。记住:QAT训练时用model.train(),转换时用model.eval()。
4.5 核心知识体系:一张图看懂量化方案选择
下面这张图是我自己总结的量化方案选择决策树,帮你快速判断该用PTQ还是QAT。
4.6 实战中的“坑”与“避坑指南”
做量化部署这么多年,我踩过的坑比走过的路还多。挑几个典型的说说。
- 校准数据太少:PTQ时我只用了10张图片做校准,结果量化后模型直接“失明”了。后来改成500张,效果立马正常。建议至少用几百张有代表性的数据。
- QAT学习率太大:我一开始用1e-3的学习率做QAT微调,结果模型直接训飞了。QAT的学习率一般要比原始训练小10倍,1e-4到1e-5比较稳妥。
- 忽略BN层融合:量化前一定要把BN层和卷积层融合,否则量化误差会放大。PyTorch的torch.quantization.fuse_modules可以帮你做这个。
- 硬件差异:同一个INT8模型,在x86上用fbgemm后端,在ARM上用qnnpack后端,精度表现可能不一样。部署前一定要在目标硬件上验证。
我的小技巧:如果你不确定该用PTQ还是QAT,可以先用PTQ跑一遍,然后对比FP32和INT8的精度。如果精度差在1%以内,PTQ就够了。如果超过2%,建议上QAT。这个“1%阈值”是我在多个项目中总结出来的经验值,你可以根据自己的业务场景调整。
4.7 总结:量化不是银弹,但它是性价比最高的优化手段
PTQ和QAT,一个快但精度略低,一个慢但精度高。没有绝对的好坏,只有合不合适的场景。
我个人建议的落地路径是:先PTQ快速验证,精度达标就收工;不达标再上QAT微调。这样既不会在PTQ上浪费太多时间,也不会一上来就投入QAT的高成本。
量化这件事,说白了就是用一点点精度损失,换取数倍的推理加速和内存节省。在AI芯片部署的实战中,这几乎是性价比最高的优化手段了。
核心要点回顾:
- PTQ:不需要训练,仅需校准数据,速度快,精度损失0.5%~2%
- QAT:需要微调,精度损失0.1%~0.5%,适合精度敏感场景
- 先PTQ后QAT,用最小成本换取最大收益
- 校准数据要有代表性,QAT学习率要调小