MobileViT量化训练与边缘推理性能调优实战 - 第4节
一、量化感知训练(QAT)到底是什么?
说实话,我第一次接触QAT的时候,也被一堆术语搞晕过。什么伪量化、直通估计器,听着就头大。但说白了,QAT就是——让模型在训练过程中就学会适应低精度。
你想想看,我们之前讲的PTQ(训练后量化),是模型训练完了再硬转。就像一个人习惯了开自动挡,突然让他开手动挡,肯定手忙脚乱。QAT的思路就聪明多了:从一开始就用手动挡练车,等练熟了再上路,自然就稳了。
我在项目中遇到过最典型的场景:一个检测模型,PTQ后mAP掉了3个点,客户不接受。换成QAT,只掉了0.5个点,完美交付。这就是QAT的价值所在。
二、QAT的核心原理
2.1 伪量化节点(FakeQuant)
QAT里最核心的概念,就是这个FakeQuant。名字起得挺有意思——「假装量化」。它到底在干什么?
简单说,FakeQuant做了两件事:
- 前向传播时:把浮点数先量化成整数,再反量化回浮点数。这个过程会引入量化误差。
- 反向传播时:把梯度直接传过去,假装量化操作不存在。
嗯,这里要注意:FakeQuant不是真的把模型转成int8,它只是在训练过程中模拟量化效果。模型权重和激活值依然是float32,只是数值上有了量化后的特征。
核心理解:FakeQuant = 量化 + 反量化,中间插了个「四舍五入」操作。
2.2 直通估计器(STE)
这里有个大问题:量化操作里的round函数,梯度几乎处处为0。你想想,梯度为0,网络还怎么学?
STE的解决方案简单粗暴:前向用round,反向直接把梯度传过去,就当round不存在。
用公式表达就是:
前向:q = round(x / scale)
反向:∂L/∂x = ∂L/∂q (忽略round的梯度)
我曾经在这个地方踩过坑。一开始觉得STE太粗糙了,会不会影响收敛?后来发现,实践证明它确实work。为什么?因为量化误差本身是有限的,STE相当于让网络「感知」到量化边界在哪里,然后主动调整参数去适应。
我的经验:STE不是完美的,但它是目前最实用的方案。别纠结理论完美性,先跑起来再说。
三、QAT实战流程
3.1 整体流程
我习惯把QAT分成三步走:
- 预训练浮点模型:先训一个高精度的float32模型,作为起点。
- 插入FakeQuant节点:在权重和激活值后面都加上量化模拟。
- 微调训练:用较小的学习率继续训练几个epoch,让模型适应量化。
下面这张图是我自己总结的QAT流程,你看一眼就明白了:
3.2 代码实战:PyTorch实现
我拿PyTorch举个例子。PyTorch官方提供了torch.quantization模块,用起来很方便:
import torch
import torch.nn as nn
import torch.quantization as quant
# 1. 定义模型
class MyModel(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 64, 3)
self.relu = nn.ReLU()
self.fc = nn.Linear(64, 10)
# 添加量化配置
self.quant = quant.QuantStub()
self.dequant = quant.DeQuantStub()
def forward(self, x):
x = self.quant(x) # 输入量化
x = self.conv1(x)
x = self.relu(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
x = self.dequant(x) # 输出反量化
return x
# 2. 准备QAT
model = MyModel()
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
# 插入FakeQuant节点
quant.prepare_qat(model, inplace=True)
# 3. 微调训练(用很小的学习率)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)
for epoch in range(5):
for data, target in train_loader:
optimizer.zero_grad()
output = model(data)
loss = nn.CrossEntropyLoss()(output, target)
loss.backward()
optimizer.step()
# 4. 转为量化模型
model.eval()
model = quant.convert(model, inplace=True)
# 此时model已经是int8了
注意:prepare_qat和convert必须成对使用。我曾经漏掉convert,结果模型还是float32,部署到边缘设备上直接报错。
四、QAT的关键参数与调优
| 参数 | 推荐值 | 我的经验 |
|---|---|---|
| 微调学习率 | 1e-4 ~ 1e-5 | 比预训练小10~100倍,太大了会破坏已学特征 |
| 微调epoch数 | 3 ~ 10 | 我一般用5个epoch,再多就过拟合了 |
| 量化位宽 | 8bit(默认) | 4bit也能做,但精度损失大,需要更多调参 |
| 校准数据集大小 | 500 ~ 2000张 | 太少的话scale估计不准,太多也没必要 |
五、避坑指南
做QAT这几年,我踩过的坑真不少。挑几个典型的说说:
- 学习率太大:我曾经直接用预训练的学习率做QAT,结果模型直接崩了,loss飞到天上。记住,QAT是微调,不是从头训练。
- 忘记冻结BN层:有些框架在QAT时需要冻结BN的running statistics,否则训练不稳定。我吃过这个亏,后来养成了习惯:QAT前先
model.eval()一下。 - 量化敏感层分析:不是所有层都需要量化。比如第一层和最后一层,对精度影响大,我通常保留float32。这个技巧在边缘设备上特别管用。
小技巧:如果你时间紧,可以先跑PTQ看看精度损失。如果损失在1%以内,就别折腾QAT了。如果超过2%,再上QAT。这样能省不少时间。
六、总结
QAT说白了就是让模型「提前适应」量化环境。核心就三个东西:FakeQuant模拟量化、STE解决梯度问题、微调让参数适应。流程也不复杂:预训练→插入节点→微调→导出。
我个人觉得,QAT是边缘计算里性价比最高的优化手段之一。虽然比PTQ多花点训练时间,但换来的是精度的大幅提升。在资源受限的边缘设备上,这1-2个点的精度提升,可能就是项目能不能交付的关键。
嗯,这一章就到这里。记住:先跑通,再调优。别一开始就追求完美,先把流程跑起来,后面慢慢调。