4、量化感知训练(QAT):模拟量化、直通估计器(STE)、QAT训练流程与技巧

各位同学,咱们今天聊点硬核的——量化感知训练。说白了,就是让模型在训练阶段就「提前适应」量化后的低精度生活。你想想看,一个模型在FP32的温柔乡里训练得好好的,突然被扔到INT8的硬板床上,能不闹情绪吗?QAT就是解决这个问题的。

4.1 为什么需要QAT?

先说说我个人的血泪史。几年前我做一个小模型部署到手机端,直接用了训练后量化(PTQ)。结果呢?精度掉了3个点,模型直接变智障。后来我才明白——PTQ是「事后补救」,QAT是「事前预防」

QAT的核心思想很简单:在训练过程中模拟量化误差,让模型自己学会「抗噪」。这样部署时,模型已经习惯了低精度的「痛」,精度损失自然就小了。

关键区别:

  • PTQ:训练完再量化,像给成年人换鞋码
  • QAT:训练时就量化,像给小孩穿可调节的鞋

4.2 模拟量化(Fake Quantization)

模拟量化,名字听着玄乎,其实就干一件事:在前向传播时假装量化,反向传播时假装没量化

具体怎么做?我们来看一个典型的模拟量化操作:

def fake_quantize(x, scale, zero_point, num_bits=8):
    # 1. 量化:将浮点数映射到整数
    x_int = torch.round(x / scale + zero_point)
    # 2. 截断:限制在整数范围内
    x_int = torch.clamp(x_int, 0, 2**num_bits - 1)
    # 3. 反量化:转回浮点数(但精度已经损失了)
    x_fake = (x_int - zero_point) * scale
    return x_fake

嗯,这里要注意:模拟量化输出的还是浮点数,但数值已经「脏」了——它包含了量化误差。模型在训练时看到的就是这种「带噪声」的数据,自然就学会了适应。

4.3 直通估计器(STE)

好,问题来了。模拟量化里有个torch.round()操作,这玩意儿导数几乎处处为0。你想想看,梯度传到这里就断了,模型还怎么学?

这时候STE就登场了。它的想法很粗暴:前向传播该量化就量化,反向传播假装量化不存在。说白了,就是把round()的梯度直接「直通」过去。

数学上长这样:

# STE 的核心实现
class FakeQuantizeSTE(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, scale, zero_point):
        # 前向:正常模拟量化
        x_int = torch.round(x / scale + zero_point)
        x_int = torch.clamp(x_int, 0, 255)
        x_fake = (x_int - zero_point) * scale
        return x_fake

    @staticmethod
    def backward(ctx, grad_output):
        # 反向:梯度直通,不做任何修改
        return grad_output, None, None

我曾经在项目里试过不用STE,结果模型训练到一半loss直接炸了。为什么?因为round()的梯度为0,参数根本更新不了。STE虽然「不严谨」,但实践证明它好用。

个人经验:STE不是唯一的选择。有些论文用「软量化」(soft quantization)来近似round的梯度,但我试下来,STE在大多数场景下最稳定。别纠结理论,先跑起来再说。

4.4 QAT训练流程

好了,理论讲完,咱们看看实际怎么操作。一个标准的QAT流程分三步:

  1. 预训练(FP32):先用全精度训练一个基线模型
  2. 插入模拟量化节点:在权重和激活值后面加上fake_quantize操作
  3. 微调(QAT):用较小的学习率继续训练,让模型适应量化

我习惯在第二步时特别注意一点:不要一开始就量化所有层。比如第一层和最后一层对精度影响最大,可以先保持FP32,等模型稳定了再逐步打开。

代码示例(PyTorch风格):

# 插入量化节点的典型做法
import torch.quantization as quant

# 1. 准备模型
model = MyModel()
model.qconfig = quant.get_default_qat_qconfig('fbgemm')

# 2. 插入模拟量化节点
quant.prepare_qat(model, inplace=True)

# 3. 微调训练
for epoch in range(num_epochs):
    for data, target in dataloader:
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

# 4. 转换为实际量化模型
quant.convert(model, inplace=True)

4.5 训练技巧与避坑指南

做QAT这几年,我踩过的坑比走过的路还多。下面这几个技巧,是我用真金白银换来的:

避坑1:学习率要调小

我曾经直接用预训练的学习率做QAT,结果loss震荡得像心电图。QAT本质是微调,学习率建议降到原来的1/10到1/100。

技巧2:逐层释放量化

我习惯先量化权重,再量化激活值。如果精度掉得厉害,可以只量化权重,激活值保持FP32。这叫「混合精度量化」,效果往往出奇的好。

技巧3:校准数据集别偷懒

很多人做QAT时随便拿几百张图做校准。我建议至少用1000张以上,而且要覆盖各种场景。你想想看,校准集如果全是猫,模型部署后看到狗不就懵了吗?

技巧4:监控量化误差

训练过程中,我习惯每几个epoch打印一下量化前后的数值差异。如果差异突然变大,说明模型可能「学歪了」,需要及时调整。

监控指标 正常范围 需要警惕
量化误差(MSE) < 0.01 > 0.05
精度下降 < 0.5% > 2%
梯度范数 稳定下降 剧烈波动

4.6 什么时候该用QAT?

说实话,不是所有场景都需要QAT。我个人总结了一个判断标准:

  • 必须用QAT:模型精度要求极高(比如人脸识别、医疗影像),或者模型本身很小(MobileNet、TinyBERT)
  • 可以试试PTQ:大模型(ResNet-50以上)、对精度要求不高的场景
  • 建议QAT+PTQ结合:先做PTQ看看精度损失,如果超过1%再上QAT

我记得有一次做语音识别模型,PTQ掉了3个点,QAT只掉了0.3个点。你说这10倍的差距,值不值得花两天时间做QAT?

4.7 小结

QAT说白了就是「让模型在训练时吃点苦,部署时才能享福」。核心就三件事:模拟量化、STE直通、微调训练。别把它想得太复杂,也别轻视它——我见过太多人因为偷懒不做QAT,最后部署时精度崩了才后悔。

下一章咱们聊聊量化部署的实战技巧,包括如何用TensorRT和ONNX Runtime做推理优化。到时候我会分享一些「压箱底」的调优经验,敬请期待。