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分成三步走:

  1. 预训练浮点模型:先训一个高精度的float32模型,作为起点。
  2. 插入FakeQuant节点:在权重和激活值后面都加上量化模拟。
  3. 微调训练:用较小的学习率继续训练几个epoch,让模型适应量化。

下面这张图是我自己总结的QAT流程,你看一眼就明白了:

浮点模型预训练 float32精度训练 插入FakeQuant节点 权重+激活值量化模拟 微调训练 小学习率适应量化 迭代优化 导出量化模型 移除FakeQuant,转为int8

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个点的精度提升,可能就是项目能不能交付的关键。

嗯,这一章就到这里。记住:先跑通,再调优。别一开始就追求完美,先把流程跑起来,后面慢慢调。

可可精品资料库