4. 量化感知训练(QAT):原理、fake量化节点与精度恢复
聊完PTQ,咱们得聊聊QAT了。
PTQ虽然快,但精度损失有时候就是压不下去。尤其是那些对量化特别敏感的模型——比如MobileNet、EfficientNet这类轻量级网络,或者模型里有很多BN层的结构。我遇到过好几次,PTQ一跑,精度直接掉3个点以上,怎么调校准集都救不回来。这时候,就得请出QAT了。
4.1 QAT的核心原理:让模型“学会”量化误差
QAT的全称是Quantization-Aware Training,翻译过来就是“量化感知训练”。
说白了,它的思路很简单:你不是要量化我吗?我干脆在训练的时候就假装已经被量化了。这样模型在更新权重的时候,就会主动去适应量化带来的误差。
你想想看,传统的训练是浮点数的世界,权重更新得再精细,一量化全给截断了。QAT的做法是——在训练的前向过程中,插入一些“假量化”节点,让模型提前感受一下量化后的“痛”。反向传播的时候,这些假量化节点用直通估计器(STE)来近似梯度,让权重照样能更新。
核心思想:QAT不是在量化后的模型上微调,而是在训练过程中模拟量化行为,让模型参数主动适应量化噪声。
这里有个关键点:QAT的推理过程,和PTQ的推理过程,最终是一样的。区别只在于训练阶段。QAT训练出来的权重,天然就对量化更鲁棒。
4.2 Fake量化节点:到底“假”在哪里?
QAT里最核心的概念,就是fake量化节点。很多初学者一听“fake”就懵了——到底是真量化还是假量化?
我这么解释吧:fake量化节点,做的是真量化的计算,但存的是浮点数的形式。
具体来说,一个fake量化节点会做三件事:
- 量化:把浮点输入缩放到整数范围,再取整。比如把[-1.0, 1.0]映射到[-128, 127]。
- 反量化:把整数再缩放回浮点范围。比如把127映射回1.0。
- 输出:输出的是浮点数,但这个浮点数已经带上了量化误差。
所以,fake量化节点的输出,和真实量化后再反量化的结果,是完全一致的。但因为它输出的是浮点数,所以后续的卷积、全连接层,依然可以用浮点运算来训练。
我的习惯:在插入fake量化节点时,我一般会在每个Conv2D和全连接层的输入和输出都各插一个。权重也要插。这样模型才能全方位感知量化误差。
下面这张图,展示了fake量化节点在训练流程中的位置:
4.3 如何插入Fake量化节点
实际工程中,我们不需要手写fake量化逻辑。主流的框架都提供了现成的API。我以PyTorch为例,说说我的做法。
PyTorch中的QAT实现:
import torch
import torch.nn as nn
import torch.quantization as quant
# 1. 加载预训练浮点模型
model = models.resnet18(pretrained=True)
# 2. 切换到QAT模式
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
model.train()
# 3. 插入fake量化节点
model = quant.prepare_qat(model, inplace=True)
# 4. 正常训练/微调
for epoch in range(num_epochs):
for images, labels in dataloader:
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 5. 训练完成后,转换为量化模型
model.eval()
model = quant.convert(model, inplace=True)
这里有个细节:prepare_qat会在模型的每个需要量化的层前后,自动插入fake量化节点。而convert会把fake量化节点替换成真正的量化/反量化操作,同时把权重也量化成int8。
我曾经踩过的坑:QAT训练时,一定要保持模型在train()模式。因为fake量化节点在训练和推理时的行为不同——训练时它会保留量化误差的分布,推理时则直接使用统计好的量化范围。如果你在训练时用了eval(),那fake量化节点就不会更新量化参数,精度会出问题。
4.4 如何微调恢复精度
插入fake量化节点只是第一步。真正让QAT生效的,是后面的微调过程。我总结了一套比较实用的微调策略:
| 策略 | 具体做法 | 适用场景 |
|---|---|---|
| 学习率调整 | 使用较小的学习率,通常是原训练学习率的1/10到1/100 | 所有场景,尤其是大模型 |
| 冻结BN层 | 在QAT微调初期,冻结BN层的均值和方差 | 模型中有大量BN层时 |
| 分阶段微调 | 先冻结特征提取器,只微调分类头;再解冻全部层 | 迁移学习场景 |
| 知识蒸馏 | 用原始浮点模型作为教师,QAT模型作为学生 | 精度要求极高时 |
我个人最常用的组合是:学习率设为原训练学习率的1/20 + 冻结BN层的前5个epoch。这个组合在大多数CV模型上都能把精度恢复到浮点模型的98%以上。
为什么会这样?因为QAT微调的本质,是让模型在量化噪声下重新找到最优解。学习率太大,模型会震荡;BN层不冻结,量化参数的统计会不稳定。这两个问题一解决,精度自然就回来了。
4.5 QAT vs PTQ:到底选哪个?
很多同学会问:既然QAT精度更高,那为什么不所有模型都用QAT?
原因很简单:QAT需要训练数据,需要GPU时间,需要调参。PTQ只需要几百张图片跑一遍校准,几分钟就搞定了。而QAT可能要跑几个epoch,几个小时甚至几天。
我一般这样选:
- 大模型(ResNet50以上):先试PTQ。如果精度损失在1%以内,直接用PTQ。
- 小模型(MobileNet、ShuffleNet):直接上QAT。这类模型对量化太敏感,PTQ基本救不回来。
- 部署到边缘设备:建议QAT。边缘设备的量化精度要求高,而且模型通常不大,QAT的成本可以接受。
我的经验:QAT并不是万能的。如果模型本身训练得就不够好,或者数据分布有问题,QAT也救不了。先把浮点模型的精度做到极致,再考虑量化的事。
4.6 避坑指南
最后,分享几个我在QAT实战中遇到的坑:
- 量化参数不收敛:检查一下学习率是不是太大了。我遇到过好几次,把学习率从1e-3降到1e-5,量化参数就稳定了。
- 推理时精度比训练时低:大概率是
convert之后,某些层的量化参数没有正确加载。建议在convert前后打印一下模型的权重,看看有没有异常值。 - BN层在推理时失效:QAT训练结束后,记得调用
model.eval()。否则BN层会继续使用训练时的统计量,导致推理结果不对。 - 自定义算子不支持QAT:如果你的模型里有自定义的算子,需要手动注册fake量化节点。这个比较麻烦,我一般建议尽量用框架原生算子。
嗯,QAT的内容差不多就这些。说白了,它就是用一点训练成本,换取更高的量化精度。在工业落地中,QAT几乎是必选项——尤其是当你面对那些对延迟和精度都极其苛刻的场景时。