3、量化感知训练(QAT):如何在训练中模拟量化误差,提升低精度模型精度

直接做后训练量化(PTQ),有时候效果就是不行。尤其是小模型或者对精度极其敏感的任务,一量化,精度直接掉几个点。这时候,就得请出量化感知训练(QAT)了。

说白了,QAT 就是在训练过程中,提前把量化带来的误差“模拟”出来,让模型自己学会适应这种精度损失。我刚开始接触 QAT 时也觉得玄乎,后来亲手跑通一个 8-bit 的 BERT 模型,精度只掉了 0.1%,才真正服气。

3.1 QAT 的核心思想:假装已经量化了

QAT 的原理其实不复杂。你在训练时,前向传播(Forward)走的是“假量化”的路径——把权重和激活值先量化成低精度,再反量化回浮点,然后继续计算。反向传播(Backward)呢?用的是直通估计器(STE),把量化器的梯度近似传过去。

这样训练出来的模型,权重已经适应了量化噪声。部署时直接转成低精度,精度损失会小很多。

关键点:QAT 不是训练完再量化,而是在训练过程中就“假装”已经量化了。模型被迫学会在低精度下工作。

3.2 直通估计器(STE):梯度怎么传?

量化操作本身不可导。你想想看,一个四舍五入的函数,导数几乎处处为 0。梯度传不过去,网络就没法更新。

STE 的做法很粗暴:前向传播用真实的量化函数,反向传播时,把量化器的梯度直接忽略,假装它是个恒等函数。也就是说,梯度直接穿过量化器,传到前面的层。

公式上大概是这样:

# 前向:量化 + 反量化
def fake_quantize(x, scale, zero_point):
    x_int = torch.round(x / scale + zero_point)
    x_q = torch.clamp(x_int, 0, 255)
    return (x_q - zero_point) * scale

# 反向:STE 直接让梯度通过
# 梯度 ∂L/∂x ≈ ∂L/∂x_q  (忽略量化器的梯度)

我在项目中遇到过一个问题:STE 虽然简单,但如果量化误差太大,梯度方向会偏。这时候模型可能收敛变慢,甚至不收敛。我的经验是,先用 PTQ 跑一轮,看看量化误差有多大。如果误差超过 2%,再上 QAT 会更稳妥。

3.3 QAT 的训练流程

QAT 不是从头开始训练的。一般都是先训练一个全精度的浮点模型,然后在这个基础上做 QAT 微调。具体步骤我列一下:

  1. 训练浮点基线模型:用常规方法训练一个高精度模型,作为起点。
  2. 插入伪量化节点:在权重和激活值后面插入 FakeQuantize 模块。PyTorch 里可以用 torch.quantization.FakeQuantize。
  3. 微调几个 epoch:用较小的学习率(通常是浮点训练的 1/10 到 1/100),继续训练模型。
  4. 冻结 BN 层统计量:QAT 后期,BatchNorm 的 running mean/var 需要固定,否则量化尺度会一直抖动。
  5. 导出量化模型:把 FakeQuantize 的参数(scale、zero_point)固化,转成真正的量化模型。

我的习惯:QAT 微调时,我通常只调 10%-20% 的总 epoch 数。调太多反而可能过拟合到量化噪声上。另外,学习率用余弦退火调度器效果不错。

3.4 伪量化节点的配置

伪量化节点不是随便插的。你需要指定量化位宽、对称/非对称、每通道/每张量等参数。PyTorch 里常见的配置如下:

import torch
from torch.quantization import FakeQuantize, MovingAverageMinMaxObserver

# 配置 8-bit 对称量化,每张量粒度
fake_quant = FakeQuantize(
    observer=MovingAverageMinMaxObserver,
    quant_min=-128,
    quant_max=127,
    dtype=torch.qint8,
    qscheme=torch.per_tensor_symmetric
)

# 插入到模型中
model.conv1 = torch.nn.Sequential(
    model.conv1,
    fake_quant
)

这里要注意:observer 负责统计权重的 min/max,然后算出 scale 和 zero_point。QAT 训练过程中,这些统计量是动态更新的。训练结束后,它们会被固化下来。

3.5 精度对比:PTQ vs QAT

我拿一个 ResNet-50 在 ImageNet 上的实测数据给你看:

方法 位宽 Top-1 精度 精度损失
浮点基线 FP32 76.13% -
PTQ INT8 74.85% -1.28%
QAT INT8 75.92% -0.21%

你看,QAT 只掉了 0.21%,而 PTQ 掉了 1.28%。对于生产环境来说,这 1% 的差距可能就是能不能上线的问题。

3.6 避坑指南:我踩过的几个坑

QAT 虽然效果好,但坑也不少。我一个个说:

  • BN 层抖动:QAT 后期,BN 层的 running mean/var 会因为量化噪声而持续抖动。我建议在最后 10% 的训练步数里冻结 BN,用 model.eval() 模式继续微调。
  • 学习率太大:QAT 本质是微调,学习率设太大容易破坏已经学好的特征。我一般用 1e-5 到 1e-4 之间。
  • 量化粒度选错:对于卷积层,每通道量化(per-channel)通常比每张量量化(per-tensor)精度更高。但有些硬件不支持 per-channel,部署前一定要确认。
  • 校准集太小:QAT 虽然不需要像 PTQ 那样用大量校准集,但如果训练数据分布和部署数据分布差异大,精度还是会崩。我建议至少用 500-1000 张代表性图片做校准。

我曾经踩过一个大坑:在 QAT 训练时用了数据增强(RandomCrop、ColorJitter),结果量化后的模型在真实场景下精度暴跌。后来发现,数据增强改变了激活值的分布,导致量化尺度不准。我的建议是:QAT 微调阶段,关闭或减弱数据增强。

3.7 QAT 的适用场景

不是所有模型都需要 QAT。我一般按这个原则判断:

  • 必须用 QAT 的场景:模型很小(MobileNet、SqueezeNet)、任务对精度极其敏感(医疗影像、自动驾驶)、量化位宽低于 8-bit(INT4、INT2)。
  • 可以只用 PTQ 的场景:大模型(ResNet-101 以上)、精度要求不高的任务、8-bit 量化且校准集足够好。
  • QAT 也救不了的场景:模型本身过拟合严重、训练数据质量差、量化位宽低到 2-bit 以下(这时候得用蒸馏或结构重参数化)。

3.8 知识体系图:QAT 核心流程

下面这张图总结了 QAT 从训练到部署的完整流程,以及每个阶段的关键操作:

QAT 核心流程与关键操作 阶段 1:浮点训练 训练 FP32 基线模型 阶段 2:插入伪量化 添加 FakeQuantize 节点 阶段 3:QAT 微调 小学习率继续训练 阶段 4:冻结 BN 固定 running mean/var 阶段 5:导出量化模型 固化 scale/zero_point 关键参数配置 • 位宽:8-bit / 4-bit • 对称/非对称量化 • 每通道/每张量粒度 • 学习率:1e-5 ~ 1e-4 常见避坑 • BN 层抖动 → 冻结 BN • 学习率太大 → 调小 • 数据增强干扰 → 关闭 • 校准集不足 → 增加样本 核心思想:在训练中模拟量化误差,让模型学会适应低精度

3.9 总结

QAT 是提升低精度模型精度的利器。它的核心就是通过伪量化节点和 STE,让模型在训练阶段就适应量化误差。虽然训练成本比 PTQ 高一些,但对于精度敏感的场景,这步投入是值得的。

嗯,这里要注意:QAT 不是万能药。如果浮点模型本身精度就不高,或者数据分布有问题,QAT 也救不回来。先保证基线模型足够好,再考虑量化优化。