3、量化感知训练(QAT)实战—— 如何在训练中模拟量化误差,让模型适应低精度
各位同学,咱们今天聊点硬核的。
上一节讲了PTQ(训练后量化),说白了就是模型训练完了再硬转低精度。但有个问题——PTQ在小模型上掉点挺严重的。我去年做个BERT-base的部署,直接INT8量化,准确率掉了快2个点,客户当场脸就黑了。
那怎么办?让模型自己学会适应低精度。这就是量化感知训练(QAT)。
3.1 QAT的核心思想:模拟推理时的量化过程
QAT的原理其实不复杂。你在训练过程中,假装推理时已经做了量化。前向传播时插入伪量化节点(FakeQuant),把权重和激活值先量化再反量化。这样梯度回传时,模型就知道低精度下哪些参数容易出错,自己调整。
我习惯把QAT比作「戴着镣铐跳舞」。模型在训练时就被限制了精度,它必须学会在这种约束下还能表现好。
关键点:QAT不是训练完再量化,而是在训练过程中就模拟量化误差。这样模型参数会朝着「对量化不敏感」的方向收敛。
3.2 伪量化节点(FakeQuant)的工作原理
伪量化节点做了两件事:
- 量化:把浮点数映射到离散的整数值(比如INT8的-128到127)
- 反量化:把整数值再映射回浮点数,但精度已经损失了
公式很简单:
# 伪量化操作
scale = (max - min) / 255
zero_point = round(-min / scale)
# 量化
q = round(x / scale) + zero_point
# 反量化
x_hat = (q - zero_point) * scale
注意,这里的round操作是不可导的。那梯度怎么回传?
嗯,这里有个经典技巧——直通估计器(STE)。简单说就是:前向传播时用round,反向传播时假装round不存在,直接把梯度传过去。
我的经验:STE虽然粗暴,但实际效果出奇的好。我做过实验,用STE训练的QAT模型,收敛速度和全精度训练几乎一样。不过要注意学习率——我建议比全精度训练小一半,否则容易震荡。
3.3 QAT的训练流程
整个流程分三步走:
- 预训练全精度模型:先用FP32训一个基线模型,收敛到差不多
- 插入伪量化节点:在权重和激活值后面加上FakeQuant操作
- 微调(Fine-tune):用很小的学习率继续训练几个epoch,让模型适应量化误差
我画了个流程图,你看一眼就明白了:
3.4 代码实战:用PyTorch实现QAT
咱们直接上代码。我用PyTorch的torch.quantization模块,手把手带你走一遍。
import torch
import torch.nn as nn
import torch.quantization as quant
# 1. 定义模型(带BN的简单CNN)
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, 3)
self.bn1 = nn.BatchNorm2d(16)
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2)
self.fc = nn.Linear(16 * 15 * 15, 10)
def forward(self, x):
x = self.pool(self.relu(self.bn1(self.conv1(x))))
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
# 2. 准备QAT配置
model = SimpleCNN()
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
# 我个人习惯用fbgemm后端,在x86上性能最好
# 3. 插入FakeQuant节点
model = quant.prepare_qat(model, inplace=True)
# 这一步会在每个Conv/Linear前后插入伪量化
# 4. 继续训练(微调)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4) # 学习率要小!
criterion = nn.CrossEntropyLoss()
for epoch in range(3): # 一般2-5个epoch就够了
for inputs, labels in dataloader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 5. 转换为INT8推理模型
model.eval()
model = quant.convert(model, inplace=True)
# 现在model就是纯INT8模型了,可以直接部署
我曾经踩过的坑:
- BN层在QAT微调时一定要保持训练模式(model.train()),否则统计量不对
- 学习率千万别用太大,我试过1e-3,直接训崩了
- 微调epoch数不是越多越好,3个epoch左右就够了,多了反而过拟合量化噪声
3.5 QAT vs PTQ:到底选哪个?
我直接给你个决策表,省得你纠结:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 大模型(>1B参数) | PTQ | QAT微调成本太高,PTQ配合校准集就够了 |
| 小模型(<100M参数) | QAT | 小模型对量化更敏感,QAT能挽回1-2个点 |
| 精度要求极高 | QAT | 比如医疗、金融场景,掉点0.5%都不能忍 |
| 快速迭代部署 | PTQ | QAT需要额外训练时间,PTQ几分钟搞定 |
说白了,QAT是用训练时间换精度。如果你模型不大、精度要求又高,那就别偷懒,上QAT。
3.6 实战中的避坑指南
最后分享几个我这些年总结的经验:
- 校准集别乱选:QAT微调用的数据要和实际部署场景一致。我见过有人用ImageNet训的模型,部署到医疗影像上,结果量化后直接废了。
- 先跑PTQ看看基线:我习惯先做一轮PTQ,看看掉点多少。如果掉点<1%,那QAT的必要性就不大。
- 注意量化粒度:Per-tensor量化快但精度差,Per-channel量化慢但精度好。我一般对权重用per-channel,激活用per-tensor。
- 混合精度QAT:有些层对量化特别敏感(比如第一层和最后一层),可以保留FP32,只量化中间层。这个技巧我屡试不爽。
一个小技巧:QAT微调时,可以先用较大的学习率(比如1e-4)训1个epoch,再降到1e-5训2个epoch。这样模型先快速适应量化环境,再精细调整。我试过,比固定学习率好0.3-0.5个点。
好了,QAT的核心内容就这些。说白了就是让模型在训练时就「体验」低精度的痛苦,然后自己学会怎么在这种限制下还能好好干活。下一节咱们聊聊更进阶的——怎么在QAT中做蒸馏,让大模型教小模型怎么量化。
公众号:蓝海资料掘金营,微信deep3321