3、量化感知训练(QAT)原理:QAT与训练后量化(PTQ)的区别、伪量化节点(FakeQuantize)的作用、QAT的训练流程

好,咱们今天聊聊量化感知训练,也就是 QAT。

说实话,很多刚接触量化的同学,第一反应都是:直接拿训练好的模型跑一下 PTQ 不就行了?为什么还要费劲去做 QAT?

嗯,这个问题我当年也问过。后来在部署一个 MobileNetV3 的时候,PTQ 直接掉点 3 个点,业务方不干了。没办法,只能上 QAT。结果呢?掉点控制在 0.3% 以内。所以,QAT 到底干了什么?咱们今天把它拆开揉碎了讲清楚。

3.1 QAT 与 PTQ 的本质区别

先说说 PTQ,训练后量化。它的流程很简单:模型训练好 → 拿一批校准数据跑一遍 → 统计出每层激活值的 min/max → 算出 scale 和 zero_point → 量化。

这个过程很快,几分钟就搞定了。但问题在哪?

我举个例子。你训练好的模型,权重分布是 FP32 的,激活值也是 FP32 的。你突然把它压缩到 INT8,相当于把一个高精度的地图强行压成低分辨率。有些细节就丢了。尤其是那些离群点(outlier),一旦被截断,精度哗哗往下掉。

QAT 的思路不一样。它是在训练过程中,就让模型「学会」适应量化带来的误差。说白了,就是让模型提前知道:嘿,你以后要被量化了,现在先习惯一下。

核心区别一句话总结:

  • PTQ:先训练,后量化。模型对量化毫不知情。
  • QAT:边训练,边模拟量化。模型主动适应量化误差。

我在项目中遇到过一种情况:PTQ 在分类模型上表现还行,但一到检测模型(比如 YOLOv5),掉点就很明显。为什么?因为检测模型对边界框的回归精度要求更高,量化误差被放大了。这时候,QAT 几乎是唯一的选择。

3.2 伪量化节点(FakeQuantize)的作用

好,那 QAT 是怎么让模型「提前适应」量化的呢?关键就在这个伪量化节点,FakeQuantize。

名字很直白:fake(假的)+ quantize(量化)。它并不是真的把模型转成 INT8,而是在 FP32 的计算图中,插入一些操作,模拟量化的效果。

FakeQuantize 到底干了什么?

它做了两件事:

  1. 量化:把 FP32 的值映射到 INT8 的离散空间。
  2. 反量化:再把 INT8 的值映射回 FP32。

你想想看,这一来一回,精度损失就产生了。但关键是,这个损失是在前向传播中发生的,反向传播的时候,梯度会绕过这个量化操作,直接更新底层的 FP32 权重。

用公式表示就是:

# 伪量化操作
q = round(clamp(x / scale + zero_point, q_min, q_max))
x_hat = (q - zero_point) * scale

其中 x 是原始 FP32 值,x_hat 是模拟量化后的 FP32 值。两者之间的差异,就是量化误差。

我的个人习惯:

在插入 FakeQuantize 节点时,我一般会在权重和激活值上都加。权重上的 FakeQuantize 负责模拟权重量化,激活值上的负责模拟激活量化。两者缺一不可。

我曾经踩过一个坑:只对权重做了 FakeQuantize,没管激活值。结果训练出来的模型,在部署时激活值量化后精度直接崩了。嗯,从那以后我再也不敢偷懒了。

3.3 QAT 的训练流程

好,咱们把整个流程串起来。QAT 的训练流程,其实就是在普通训练流程上,插入了 FakeQuantize 节点。

标准流程如下:

  1. 准备预训练模型:先用 FP32 训练一个 baseline 模型。这一步不能省,QAT 不是从零开始训练的。
  2. 插入 FakeQuantize 节点:在模型的权重层和激活层之后,插入伪量化节点。注意,BN 层(Batch Normalization)一般放在 FakeQuantize 之前。
  3. 微调训练:用较小的学习率(通常是原学习率的 1/10 到 1/100)继续训练几个 epoch。数据量不需要太大,一个 epoch 甚至半个 epoch 就够了。
  4. 冻结 BN 统计量:在训练的最后阶段,冻结 BN 层的均值和方差,让模型稳定下来。
  5. 导出量化模型:移除 FakeQuantize 节点,导出真正的 INT8 模型。

避坑指南:

我曾经在微调时用了太大的学习率,结果模型直接发散。后来我学乖了,QAT 的学习率一般设置在 1e-5 到 1e-6 之间,而且要用余弦退火策略,慢慢降下去。

这里有个细节要注意:FakeQuantize 节点在训练时是「活跃」的,但在推理时会被移除。所以,你训练时看到的精度,就是部署后 INT8 模型的精度。这一点非常实用——你可以在训练过程中直接评估量化后的效果。

3.4 QAT 与 PTQ 的对比表格

为了让你看得更清楚,我整理了一个对比表:

对比维度 PTQ(训练后量化) QAT(量化感知训练)
是否需要训练 不需要 需要微调
耗时 几分钟 几小时到几天
精度损失 较大(尤其小模型) 较小(通常 < 1%)
适用场景 大模型、精度要求不高 小模型、精度敏感场景
实现复杂度 中高
对校准数据依赖 需要少量校准数据 需要训练数据

注意:

QAT 并不是万能的。如果你的模型本身精度就很差(比如训练不充分),QAT 也救不回来。我建议先确保 FP32 模型精度达标,再考虑量化。

3.5 一个简单的 QAT 训练伪代码

最后,给你一个 PyTorch 风格的伪代码,方便理解:

# 1. 加载预训练 FP32 模型
model = load_pretrained_model()

# 2. 插入 FakeQuantize 节点
model = prepare_qat(model, inplace=True)
# 这一步会在权重和激活后自动插入 FakeQuantize

# 3. 设置优化器,学习率要小
optimizer = torch.optim.SGD(model.parameters(), lr=1e-5)

# 4. 微调训练
for epoch in range(5):
    for images, labels in dataloader:
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

# 5. 冻结 BN,导出 INT8 模型
model.eval()
model = convert_to_int8(model)
torch.save(model.state_dict(), 'model_int8.pth')

你看,代码量其实不多。但每一步都有坑。比如 prepare_qat 这个函数,不同框架的实现细节不一样。PyTorch 的 torch.quantization 和 TensorRT 的 QAT 工具包,接口差异挺大的。我建议你先在 PyTorch 上跑通,再迁移到 TensorRT。

好了,QAT 的原理就讲到这里。下一章咱们聊聊具体的代码实现,包括怎么在 PyTorch 里配置 QAT,以及怎么导出给 TensorRT 用。