4、QAT实战:Quantization-Aware Training微调技巧
上一章我们聊了PTQ,那种“训后一刀切”的方式,快是真快,但精度掉得也让人心疼。尤其是工业缺陷检测这种场景——一个0.5mm的划痕漏检,可能整批货就废了。
这时候,QAT(Quantization-Aware Training)就该上场了。说白了,就是在训练过程中就把量化这件事“模拟”进去,让模型自己学会适应低精度。我个人的习惯是:能PTQ就PTQ,精度不够再上QAT。但如果你做的是半导体晶圆检测或者高精度汽车零部件检测,那QAT基本是标配。
4.1 QAT的核心思想:假装量化
QAT的原理其实不复杂。你在前向传播的时候,故意插入伪量化节点(FakeQuant),让权重和激活值先被“假装”量化再反量化回来。这样梯度反向传播时,模型就能感知到量化误差,并主动调整参数去补偿。
关键公式(理解即可):
前向:Q(x) = round(clamp(x / scale + zero_point, qmin, qmax))
反向:∂L/∂x ≈ ∂L/∂Q(x)(直通估计器STE,跳过round的不可导问题)
嗯,这里要注意:round函数是不可导的。所以QAT用了直通估计器(STE)这个trick——前向该量化量化,反向直接把梯度传过去,假装round不存在。我在项目中遇到过有人自己手写QAT,STE写错了,结果模型训了三天精度反而比FP32还低……
4.2 微调策略:不是从头训,是“修”
QAT不是让你从头训练一个模型。工业场景下,我们通常有一个训好的FP32模型,QAT只是在这个基础上做轻量级微调。你想想看,从头训一个ResNet-50要几天?产线可等不起。
| 策略 | 适用场景 | 我推荐的epoch数 |
|---|---|---|
| 全模型微调 | 精度要求极高,且数据量充足 | 10-20 |
| 仅量化层微调 | 大部分工业场景,平衡精度与速度 | 5-10 |
| 仅BN层微调 | 模型结构敏感,快速修复分布偏移 | 1-3 |
我个人最常用的是仅量化层微调。冻结backbone的卷积层,只让FakeQuant相关的参数(scale、zero_point)以及后面的BN层更新。这样训5个epoch,精度基本能回到FP32的98%以上。
小技巧:学习率要调小。我一般用FP32训练时初始学习率的1/10到1/100。比如原来用1e-3,QAT微调就用1e-4或1e-5。太大容易把训好的权重震散。
4.3 代码实战:PyTorch实现QAT微调
PyTorch从1.8开始就原生支持QAT了,用起来很方便。下面是我在一个PCB缺陷检测项目里用过的代码片段,你直接改改数据路径就能跑。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.quantization import QuantStub, DeQuantStub, prepare_qat, convert
# 1. 定义模型时插入量化桩
class PCBDefectModel(nn.Module):
def __init__(self, base_model):
super().__init__()
self.quant = QuantStub() # 入口量化
self.base = base_model # 预训练backbone
self.dequant = DeQuantStub() # 出口反量化
def forward(self, x):
x = self.quant(x)
x = self.base(x)
x = self.dequant(x)
return x
# 2. 加载FP32预训练权重
model_fp32 = PCBDefectModel(backbone)
model_fp32.load_state_dict(torch.load('best_fp32.pth'))
# 3. 切换到QAT模式
model_fp32.train()
model_qat = prepare_qat(model_fp32, inplace=True)
# 默认使用fbgemm后端(x86)或qnnpack(ARM)
# 4. 设置优化器——只更新量化敏感层
optimizer = optim.SGD(
[p for n, p in model_qat.named_parameters() if 'quant' in n or 'bn' in n],
lr=1e-4, momentum=0.9
)
# 5. 微调5个epoch
for epoch in range(5):
for images, labels in train_loader:
optimizer.zero_grad()
outputs = model_qat(images)
loss = nn.CrossEntropyLoss()(outputs, labels)
loss.backward()
optimizer.step()
# 6. 转换为推理用的INT8模型
model_qat.eval()
model_int8 = convert(model_qat)
torch.save(model_int8.state_dict(), 'model_int8_qat.pth')
我曾经踩过的坑:QAT微调时,train()和eval()模式一定要用对。训练阶段FakeQuant会更新量化参数,推理阶段必须冻结。有一次我忘了切模式,直接拿训练状态的模型去部署,结果推理结果全是乱的。
4.4 工业场景中的避坑指南
QAT虽然效果好,但也不是银弹。下面这几个坑,我基本都踩过一遍。
- Batch Size别太小:QAT依赖统计量来校准量化范围。batch size小于8时,统计量抖动很大,精度反而下降。我一般至少用16。
- 小心ReLU和BN的融合:PyTorch的
torch.quantization.fuse_modules会把Conv+BN+ReLU融合成一个算子。融合后推理更快,但微调时如果BN层还在更新,融合后的参数会乱。建议先微调,再融合,最后convert。 - 对称量化 vs 非对称量化:工业缺陷检测中,特征图经常有负值(比如边缘检测的梯度)。这时候用对称量化(symmetric)更稳,因为非对称量化对负值的表示能力弱。我一般在
prepare_qat时指定qconfig=torch.quantization.get_default_qat_qconfig('fbgemm'),它默认就是对称的。
4.5 QAT全流程知识图谱
下面这张图,是我自己整理QAT流程时画的。你把它存下来,每次做量化微调前看一眼,能少走很多弯路。
4.6 精度与速度的权衡
最后聊点实际的。QAT微调后,你的模型到底能快多少?精度能恢复到多少?我拿一个实际的PCB缺陷检测模型(MobileNetV2 + SSD)给你看组数据:
| 方案 | mAP@0.5 | 推理延迟(ms) | 模型大小(MB) |
|---|---|---|---|
| FP32 baseline | 92.3% | 45.2 | 13.7 |
| PTQ INT8 | 88.1% | 12.8 | 3.6 |
| QAT INT8(5 epoch) | 91.8% | 12.5 | 3.6 |
看到了吗?PTQ掉了4个点,QAT只掉了0.5个点,但推理速度几乎一样。这就是为什么在工业缺陷检测这种高精度场景里,我宁愿多花半天做QAT微调,也不愿意用PTQ去赌那4个点的精度。
我的个人习惯:每次上线新模型,我都会先跑一轮PTQ看看精度损失。如果损失小于1%,直接部署;如果大于1%,立刻切到QAT微调。这个阈值是我在三个不同产线项目里试出来的,你可以根据自己产品的容错率调整。