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流程时画的。你把它存下来,每次做量化微调前看一眼,能少走很多弯路。

QAT微调全流程 QAT微调全流程 FP32预训练模型 插入Quant/DeQuant桩 prepare_qat() 微调(冻结backbone / 全模型) lr=1e-4, epoch=5~20 convert() → INT8 ⚠️ 关键注意事项 • 先微调,再融合 • Batch Size ≥ 16 • 学习率降为1/10~1/100 ✅ 推荐策略 • 仅量化层微调 • 对称量化(fbgemm) • 验证集监控精度回退

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微调。这个阈值是我在三个不同产线项目里试出来的,你可以根据自己产品的容错率调整。