4. 量化实战:使用PyTorch进行模型后训练量化(PTQ)与量化感知训练(QAT)

聊到端侧推理,量化是绕不开的话题。我刚开始做端侧部署时,总觉得量化就是把float32转成int8,简单得很。结果第一次把量化后的模型跑在手机上,精度直接掉了5个点,被测试同学追着问了一周。嗯,从那以后我才真正开始认真研究量化这件事。

说白了,量化就是用更少的比特数来表示模型的权重和激活值。常见的做法是把FP32压缩到INT8,甚至INT4。这样做的好处很明显——模型体积变小,推理速度变快,功耗也降下来了。但代价呢?精度损失。怎么在压缩和精度之间找到平衡,这就是我们今天要聊的核心。

模型量化实战路径 PyTorch 量化 PTQ(后训练量化) 静态量化 动态量化 QAT(量化感知训练) 伪量化节点 微调恢复精度 PTQ快速部署 vs QAT高精度

4.1 后训练量化(PTQ)—— 最快上手的方式

PTQ,全称Post-Training Quantization。你想想看,模型已经训练好了,我们直接拿过来做量化,不需要重新训练。这是我最常用的方法,尤其是在项目初期快速验证的时候。

PyTorch提供了两种PTQ方式:动态量化和静态量化。我分别说说。

4.1.1 动态量化

动态量化只量化权重,激活值在推理时动态计算量化参数。好处是简单,不需要校准数据。坏处是推理速度提升有限。

import torch
import torch.quantization as quant

# 加载一个预训练模型
model = torch.load('my_model.pth')
model.eval()

# 应用动态量化
quantized_model = quant.quantize_dynamic(
    model, 
    {torch.nn.Linear, torch.nn.LSTM},  # 只量化这些层
    dtype=torch.qint8
)

# 保存量化后的模型
torch.save(quantized_model.state_dict(), 'model_dynamic_quant.pth')
我的经验:动态量化特别适合RNN、LSTM这类模型。我在一个语音识别项目里,用动态量化把模型体积压缩了4倍,推理速度提升了2倍,精度只掉了0.3%。效果相当不错。

4.1.2 静态量化

静态量化就复杂一些了。它需要你提供一小批校准数据,让PyTorch去统计激活值的分布,然后确定量化参数。这样做的好处是推理时不需要再计算量化参数,速度更快。

import torch
import torch.quantization as quant

# 准备模型
model = MyModel()
model.eval()

# 设置量化配置
model.qconfig = quant.get_default_qconfig('fbgemm')  # x86平台
# model.qconfig = quant.get_default_qconfig('qnnpack')  # ARM平台

# 准备量化(插入观察者)
quant.prepare(model, inplace=True)

# 用校准数据跑一遍
with torch.no_grad():
    for data in calibration_loader:
        model(data)

# 转换为量化模型
quant.convert(model, inplace=True)

# 现在model已经是量化后的了
torch.save(model.state_dict(), 'model_static_quant.pth')
注意:校准数据一定要有代表性。我曾经在一个图像分类项目里,用了100张猫的图片做校准,结果量化后的模型看到狗就认不出来。后来我改用500张涵盖所有类别的图片,问题就解决了。

4.2 量化感知训练(QAT)—— 精度优先的选择

PTQ虽然快,但有时候精度损失确实让人头疼。我记得有一次做语义分割模型,PTQ后mIoU直接掉了8个点,客户直接说不行。这时候就得请出QAT了。

QAT的核心思想是在训练过程中模拟量化效果。说白了,就是在前向传播时插入伪量化节点(FakeQuantize),让模型学会适应量化带来的误差。这样训练出来的模型,量化后精度损失会小很多。

import torch
import torch.quantization as quant

# 加载预训练模型
model = MyModel()
model.load_state_dict(torch.load('pretrained.pth'))

# 切换到训练模式
model.train()

# 设置QAT配置
model.qconfig = quant.get_default_qat_qconfig('fbgemm')

# 准备QAT
quant.prepare_qat(model, inplace=True)

# 继续训练几个epoch
optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)
for epoch in range(5):
    for data, target in train_loader:
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

# 训练完成后,转换为量化模型
model.eval()
quant.convert(model, inplace=True)

# 保存
torch.save(model.state_dict(), 'model_qat_quant.pth')
关键点:QAT训练时学习率要调小。我一般用原学习率的1/10到1/100。太大了模型会忘记之前学到的知识,太小了又学不到东西。这个度需要自己试。

4.3 PTQ vs QAT:怎么选?

这个问题我经常被问到。我的建议是:

场景 推荐方法 原因
快速验证、原型开发 PTQ动态量化 不需要数据,一行代码搞定
精度要求不高、部署时间紧 PTQ静态量化 速度快,精度损失可接受
精度敏感型任务(如医疗、自动驾驶) QAT 精度损失最小,但需要训练时间
模型已经很大,再量化怕崩 先PTQ试试,不行再QAT 我习惯的做法,省时间

4.4 避坑指南

做量化这几年,我踩过的坑不少。挑几个典型的说说:

  • 校准数据量不是越多越好。我曾经用10万张图片做校准,结果量化后的模型精度反而比用1000张的差。后来发现是因为数据分布太杂,量化参数被拉偏了。一般100-500张就够了。
  • 注意量化粒度。Per-tensor量化速度快,但精度差。Per-channel量化精度好,但速度慢。我在移动端项目里一般用per-channel,因为精度更重要。
  • 不要量化所有层。有些层对量化特别敏感,比如第一层和最后一层。我习惯保留这些层为FP32,其他层做INT8。PyTorch支持通过配置来指定哪些层不量化。
  • QAT训练时间别太长。我见过有人QAT训练了50个epoch,结果过拟合了。一般3-5个epoch就够了,最多10个。
一个小技巧:量化后一定要做精度验证。我每次量化完都会跑一遍验证集,对比量化前后的精度差异。如果差异超过1%,我就会考虑换QAT或者调整量化配置。

4.5 实战总结

量化这件事,说白了就是trade-off。你要速度还是要精度?要快速部署还是要精细调优?没有银弹,只有适合你场景的方法。

我个人习惯的流程是这样的:先用PTQ动态量化快速看看效果,如果精度损失在可接受范围内,就直接用了。如果不行,上PTQ静态量化。还不行,那就老老实实做QAT。这个流程帮我省了不少时间,也避免了一上来就做QAT那种大动干戈的情况。

最后说一句,量化不是终点,部署到端侧才是。下一节我们会聊怎么把量化后的模型部署到手机和嵌入式设备上,到时候再细说。