3、量化感知训练(QAT)入门:QAT原理、伪量化节点(FakeQuant)、QAT训练流程
好,咱们继续往下聊。前面讲了PTQ(训练后量化),说白了就是模型训完了,我们再拿它去“瘦身”。但有个问题——PTQ在小模型上,精度掉得挺厉害的。我遇到过好几次,一个轻量级网络,PTQ一压,准确率直接掉了两三个点,客户那边直接炸毛了。
那怎么办?这时候就得请出我们今天的主角——量化感知训练(QAT)。
3.1 QAT的核心思想:让模型“学会”被量化
QAT的原理,其实一句话就能说清楚:在训练过程中,模拟量化带来的误差,让模型自己去适应它。
你想想看,PTQ是“事后诸葛亮”,模型已经定型了,你再怎么调量化参数,它该丢的精度还是丢。而QAT是“事前预防针”,在训练的时候就告诉模型:“嘿,兄弟,你以后要变成INT8的,你提前适应一下。”
我个人习惯把QAT比作“戴着镣铐跳舞”。模型在训练时,权重和激活值都被“伪量化”了,它只能在有限的精度范围内去优化。这样训练出来的模型,对量化误差的鲁棒性就强得多。
核心公式(简化版):
QAT = 正常训练 + 伪量化节点 + 梯度直通估计(STE)
这里有个关键点:梯度直通估计(STE)。量化操作本身是不可导的,但反向传播需要梯度。STE的做法很简单——前向传播时做量化,反向传播时假装量化不存在,直接把梯度传过去。嗯,这里要注意,STE是个近似,但实践证明它工作得很好。
3.2 伪量化节点(FakeQuant):QAT的灵魂
伪量化节点,英文叫FakeQuant,是QAT里最重要的组件。它长什么样?说白了就是一个“模拟器”。
前向传播时,FakeQuant做三件事:
- 计算缩放因子(scale)和零点(zero_point)——跟PTQ一样,但这里的scale是可训练的,或者通过滑动平均更新的。
- 量化:把浮点数映射到INT8的整数范围(比如-128到127)。
- 反量化:再把INT8映射回浮点数。
你看,输入是浮点,输出也是浮点,但中间走了一圈INT8。这就是“伪量化”名字的由来——它并没有真正把数据存成INT8,只是模拟了量化的效果。
我曾经在一个项目里踩过坑:FakeQuant的初始化很重要。如果初始scale设得不好,模型训练初期就会剧烈震荡,甚至不收敛。后来我学乖了,先用一小批数据跑一下PTQ,拿到一个合理的scale初值,再开始QAT训练。
避坑指南:
我曾经把FakeQuant放在所有层后面,结果发现某些层的输出范围特别大,量化后信息全丢了。后来我养成了习惯——先观察每层的激活值分布,再决定哪些层需要FakeQuant,哪些层可以跳过。
下面是一个简化的FakeQuant实现逻辑(PyTorch风格):
class FakeQuant(torch.nn.Module):
def __init__(self, num_bits=8):
super().__init__()
self.num_bits = num_bits
self.scale = torch.nn.Parameter(torch.tensor(1.0)) # 可训练
self.zero_point = torch.nn.Parameter(torch.tensor(0.0))
def forward(self, x):
# 量化
x_int = torch.round(x / self.scale) + self.zero_point
x_int = torch.clamp(x_int, -128, 127) # INT8范围
# 反量化
x_fp = (x_int - self.zero_point) * self.scale
return x_fp
当然,实际框架(比如PyTorch的torch.quantization)里实现更复杂,但核心逻辑就是上面这几行。
3.3 QAT训练流程:一步一步来
QAT的训练流程,说白了就是在正常训练流程里插入FakeQuant节点。我把它拆成几个步骤:
步骤1:准备预训练模型
先用FP32训练一个基线模型。这个模型要尽可能收敛好,因为QAT是在它的基础上“微调”的。我一般会保存一个FP32的checkpoint,万一QAT效果不好,还能回退。
步骤2:插入FakeQuant节点
在模型的权重和激活值后面,都加上FakeQuant。具体来说:
- 每个卷积层/全连接层的权重后面加一个FakeQuant
- 每个层的输出(激活值)后面加一个FakeQuant
- 如果有BN层,注意BN层和FakeQuant的顺序——我习惯把BN层融合到卷积里再量化
警告:
千万别在输入层和输出层加FakeQuant!输入层一般是图像(0-255),输出层是softmax/logits,量化它们会严重损失精度。我见过有人把输入也量化了,结果模型直接废了。
步骤3:设置量化参数
这里有几个超参数需要调:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 量化位数 | 8 bit | 大部分场景够用,4 bit需要特殊处理 |
| 对称/非对称 | 权重用对称,激活用非对称 | 权重分布通常对称,激活分布可能有偏移 |
| 学习率 | FP32的1/10 ~ 1/100 | QAT是微调,学习率不能太大 |
| 训练轮数 | FP32的10% ~ 20% | 比如FP32训了100轮,QAT训10-20轮就够了 |
步骤4:微调训练
用较小的学习率,在原有数据集上继续训练。注意:
- 损失函数跟FP32一样,不需要改
- 优化器建议用SGD或Adam,学习率要衰减
- 每个epoch结束后,观察量化后的精度——如果精度下降超过1%,说明量化参数没调好
我个人的习惯是:QAT训练的前几个epoch,先冻结FakeQuant的scale参数,只更新权重。等权重稳定了,再放开scale一起训练。这样能避免训练初期的不稳定。
步骤5:导出INT8模型
训练完成后,把FakeQuant节点去掉,直接导出INT8的权重和scale。这一步各个框架的API不同,但核心就是:
- 提取每个FakeQuant的scale和zero_point
- 把FP32权重转换成INT8整数
- 保存成INT8模型格式(比如ONNX INT8、TensorRT engine等)
3.4 QAT vs PTQ:什么时候用哪个?
很多同学问我:“到底该用QAT还是PTQ?”我的回答是:看场景。
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 大模型(ResNet50以上) | PTQ | 模型冗余度高,PTQ损失小 |
| 小模型(MobileNet、SqueezeNet) | QAT | 模型容量小,PTQ掉点严重 |
| 有大量训练数据 | QAT | 数据多,QAT能充分适应 |
| 部署时间紧迫 | PTQ | QAT需要额外训练时间 |
| 精度要求极高(>99%) | QAT | PTQ很难做到无损 |
说白了,QAT是“用时间换精度”,PTQ是“用精度换时间”。没有绝对的好坏,只有合不合适。
3.5 本章知识体系图
下面我用一张流程图,把QAT的核心逻辑串起来:
这张图把QAT的流程串起来了:从FP32预训练开始,插入FakeQuant节点(权重和激活各一个),然后微调训练,最后导出INT8模型。每一步都有坑,但踩过了就是经验。
3.6 小结
QAT说白了就是“让模型提前适应量化”。它比PTQ多花点训练时间,但精度通常能保住。我个人的经验是:如果PTQ掉点超过1%,果断上QAT。别犹豫,犹豫就会败北。
嗯,这一章的内容就到这儿。记住:FakeQuant是QAT的灵魂,STE是它的引擎,而你的耐心是它成功的关键。