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. 量化:把浮点输入缩放到整数范围,再取整。比如把[-1.0, 1.0]映射到[-128, 127]。
  2. 反量化:把整数再缩放回浮点范围。比如把127映射回1.0。
  3. 输出:输出的是浮点数,但这个浮点数已经带上了量化误差。

所以,fake量化节点的输出,和真实量化后再反量化的结果,是完全一致的。但因为它输出的是浮点数,所以后续的卷积、全连接层,依然可以用浮点运算来训练。

我的习惯:在插入fake量化节点时,我一般会在每个Conv2D和全连接层的输入和输出都各插一个。权重也要插。这样模型才能全方位感知量化误差。

下面这张图,展示了fake量化节点在训练流程中的位置:

QAT训练流程与Fake量化节点位置 QAT训练流程:Fake量化节点插入位置 输入图像 (Float) Fake量化 (输入) Conv2D (Float) Fake量化 (输出) 后续层 (Float) 说明: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几乎是必选项——尤其是当你面对那些对延迟和精度都极其苛刻的场景时。