3、模型量化基础(下):训练后量化(PTQ)实战、量化感知训练(QAT)入门、量化误差分析与调试
好,咱们接着聊。上一节我们把量化的理论框架搭起来了,这一节直接上硬菜——动手做。我会带你走一遍训练后量化(PTQ)的完整流程,再入门一下量化感知训练(QAT),最后聊聊怎么分析那些让人头疼的量化误差。
说实话,PTQ 是我在项目里用得最多的方法。为什么?因为它快。你模型都训好了,直接拿来量化,不需要重新训练。但快归快,坑也不少。我踩过的坑,今天一并告诉你。
3.1 训练后量化(PTQ)实战
PTQ 的核心思路很简单:用一小部分校准数据,统计出每层激活值的分布,然后算出最佳的缩放因子和零点。说白了,就是给模型做一次「静态分析」。
我个人习惯用 PyTorch 自带的量化工具,配合 Intel 的 IPEX 或者 NVIDIA 的 TensorRT。下面给一个最基础的 PTQ 流程,你一看就懂。
import torch
import torch.quantization as quant
# 1. 加载预训练模型(假设是浮点模型)
model = MyModel().eval()
# 2. 设置量化配置(这里用最常用的 per-tensor, symmetric)
model.qconfig = quant.get_default_qconfig('fbgemm')
# 3. 准备量化(插入观察器)
model_prepared = quant.prepare(model, inplace=False)
# 4. 用校准数据跑一遍(关键!)
with torch.no_grad():
for images in calibration_loader:
model_prepared(images)
# 5. 转换为量化模型
model_quantized = quant.convert(model_prepared, inplace=False)
# 6. 保存
torch.jit.save(torch.jit.script(model_quantized), 'model_quantized.pt')
嗯,这里要注意:校准数据千万别用训练集,最好用验证集里随机抽的几百张。我见过有人直接用训练集做校准,结果量化后精度反而比浮点模型还高——这其实是过拟合了,部署后一测真实数据就露馅。
3.2 量化感知训练(QAT)入门
PTQ 虽然快,但有时候精度掉得厉害。尤其是那些对数值敏感的小模型,比如 MobileNet、SqueezeNet,量化后可能直接掉 2~3 个点。这时候就得请出 QAT 了。
QAT 的原理其实不复杂:在训练过程中模拟量化误差,让模型自己去适应。你想想看,模型在训练时就「知道」自己将来要被量化,它就会学得「皮实」一点。
我在项目中遇到过最典型的场景:一个语音唤醒模型,PTQ 后唤醒率从 95% 掉到 82%,完全没法用。后来上了 QAT,只训了 5 个 epoch,唤醒率就回到了 93%。
# QAT 流程(基于 PyTorch)
model = MyModel().train()
# 设置 QAT 配置(注意和 PTQ 不同)
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
# 准备 QAT
model_prepared = quant.prepare_qat(model, inplace=False)
# 正常训练(但学习率要调小,我一般用原学习率的 1/10)
optimizer = torch.optim.SGD(model_prepared.parameters(), lr=1e-4)
for epoch in range(5):
for images, labels in train_loader:
output = model_prepared(images)
loss = criterion(output, labels)
loss.backward()
optimizer.step()
# 最后转换为量化模型
model_quantized = quant.convert(model_prepared, inplace=False)
3.3 量化误差分析与调试
量化完了,精度掉了,怎么办?别慌,先分析误差来源。我总结了一套「三步定位法」,屡试不爽。
3.3.1 第一步:逐层对比输出
把浮点模型和量化模型的每一层输出都拿出来对比。看哪一层的误差最大。误差可以用余弦相似度或者均方误差来衡量。
def compare_layer_outputs(float_model, quant_model, input_tensor):
# 注册 hook 来获取中间层输出
float_outputs = {}
quant_outputs = {}
def hook_fn(name, storage):
def hook(module, input, output):
storage[name] = output.detach()
return hook
# 对每一层注册 hook
for name, module in float_model.named_modules():
module.register_forward_hook(hook_fn(name, float_outputs))
for name, module in quant_model.named_modules():
module.register_forward_hook(hook_fn(name, quant_outputs))
# 前向传播
float_model(input_tensor)
quant_model(input_tensor)
# 计算每层的余弦相似度
for name in float_outputs:
cos_sim = torch.nn.functional.cosine_similarity(
float_outputs[name].flatten(),
quant_outputs[name].flatten(),
dim=0
)
print(f'Layer {name}: cosine similarity = {cos_sim.item():.4f}')
你会发现,误差通常集中在某几层。我遇到最多的是第一层卷积和最后一层全连接。第一层是因为输入是 RGB 图像,数值范围大;最后一层是因为输出 logits 的分布很稀疏。
3.3.2 第二步:检查量化参数
如果某层误差大,看看它的缩放因子和零点是不是合理。有时候校准数据没选好,导致缩放因子偏大或偏小。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 某层输出全为 0 | 缩放因子太小,所有值都被截断到 0 | 改用 per-channel 量化,或调整校准数据 |
| 某层输出饱和(全是 127 或 -128) | 缩放因子太大,值被截断到边界 | 改用更保守的校准方法(如 percentile) |
| 精度整体下降 | 激活值分布不均匀 | 尝试不同的量化粒度(per-tensor vs per-channel) |
3.3.3 第三步:针对性修复
定位到问题层后,有几种修复手段:
- 混合精度量化: 对敏感层保持 FP16 或 FP32,其他层用 INT8。我有个项目就是这么干的,精度只掉了 0.1%,速度还快了 3 倍。
- 调整校准策略: 默认用 min-max 校准,可以试试 percentile(比如取 99.9% 分位点),能有效抑制 outlier 的影响。
- 增加 QAT 训练轮数: 有时候 5 个 epoch 不够,试试 10 个。但别太多,否则模型会过拟合到量化噪声上。
好了,这一节的内容就到这儿。量化这东西,光看理论总觉得玄乎,动手做一遍就全明白了。下一节我们会讲更高级的量化技术——比如权值共享量化、二值化网络,到时候再聊。