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 微调。具体步骤我列一下:
- 训练浮点基线模型:用常规方法训练一个高精度模型,作为起点。
- 插入伪量化节点:在权重和激活值后面插入 FakeQuantize 模块。PyTorch 里可以用
torch.quantization.FakeQuantize。 - 微调几个 epoch:用较小的学习率(通常是浮点训练的 1/10 到 1/100),继续训练模型。
- 冻结 BN 层统计量:QAT 后期,BatchNorm 的 running mean/var 需要固定,否则量化尺度会一直抖动。
- 导出量化模型:把 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 从训练到部署的完整流程,以及每个阶段的关键操作:
3.9 总结
QAT 是提升低精度模型精度的利器。它的核心就是通过伪量化节点和 STE,让模型在训练阶段就适应量化误差。虽然训练成本比 PTQ 高一些,但对于精度敏感的场景,这步投入是值得的。
嗯,这里要注意:QAT 不是万能药。如果浮点模型本身精度就不高,或者数据分布有问题,QAT 也救不回来。先保证基线模型足够好,再考虑量化优化。