4. 量化感知训练(QAT)原理:伪量化节点、直通估计器与训练微调策略

讲完 PTQ,咱们得聊聊 QAT 了。说实话,PTQ 虽然省事,但精度掉得厉害的时候,我第一个想到的就是上 QAT。QAT 说白了就是让模型在训练过程中「提前适应」量化带来的误差。你想想看,模型在训练时就知道自己将来要被量化,它自然会学得「皮实」一点。

我最早接触 QAT 是在一个安防项目上,YOLOv5s 做 PTQ 后 mAP 掉了 3 个点,客户不答应。后来上了 QAT,只微调了 5 个 epoch,精度就基本追平了浮点模型。嗯,从那以后我就把 QAT 当成了精度救急的标配手段。

4.1 伪量化节点(FakeQuantize)—— 模拟量化的「替身演员」

伪量化节点,英文叫 FakeQuantize,名字起得很直白——它并不是真的把权重和激活值转成 int8 存起来,而是在浮点计算过程中「模拟」量化-反量化的全过程。

它的工作流程是这样的:

  1. 输入浮点张量
  2. 用量化公式 q = round(clamp(x / scale + zero_point, qmin, qmax)) 模拟量化
  3. 再用反量化公式 x_hat = (q - zero_point) * scale 恢复成浮点
  4. 输出一个「看起来像浮点,但已经带上了量化误差」的张量

这个 x_hat 和原始 x 之间的差异,就是量化误差。模型在训练时看到的就是这个带误差的版本,它得学会在这种「失真」下依然做出正确预测。

核心要点: 伪量化节点只出现在训练/微调阶段。推理部署时,这些节点会被移除,替换成真正的量化算子(如 Conv2d 的 int8 实现)。

我在项目中遇到过一个问题:伪量化节点插入的位置不对,导致某些层的梯度爆炸。后来我总结了一个经验——BN 层后面的激活值,量化误差往往更大,需要重点观察。

4.2 直通估计器(STE)—— 让梯度「绕道走」

量化操作里有个 round() 函数,这玩意儿导数几乎处处为 0。你想想看,梯度为 0 意味着什么?网络没法更新权重了,训练直接卡死。

直通估计器(Straight-Through Estimator, STE)就是来解决这个问题的。它的思路很粗暴:前向传播时老老实实做量化,反向传播时假装量化不存在,直接把梯度「绕」过去。

数学上可以这么理解:

# 前向传播
x_quant = fake_quantize(x)  # 包含 round() 操作

# 反向传播
∂L/∂x ≈ ∂L/∂x_quant          # STE:梯度直接通过,忽略 round() 的导数

说白了,STE 就是一个「骗」梯度的技巧。虽然理论上不严谨,但实践中效果非常好。我个人的习惯是:在 QAT 的前几个 epoch 使用 STE,后面如果精度震荡,可以尝试改用带噪声的 STE 变体。

我的经验: 有些框架(如 PyTorch 的 torch.quantization)默认就用了 STE,你不需要手动实现。但如果你自己写量化算子,一定要记得把 round() 的梯度用 STE 替换掉,否则训练根本跑不动。

4.3 QAT 训练与微调策略 —— 怎么调才不翻车?

QAT 不是从头训练,而是在一个已经训练好的浮点模型基础上做微调。微调策略直接决定了最终精度。我踩过不少坑,下面是我总结的一套比较稳的流程:

4.3.1 准备工作:校准与初始化

在开始 QAT 微调之前,需要先做一步校准:

  • 用一小部分训练数据(通常 100-500 张图)跑一遍前向
  • 统计每层激活值的 min/max 或百分位值
  • 用这些统计量初始化伪量化节点的 scale 和 zero_point

这一步很关键。如果初始化不准,微调时 loss 可能直接飞掉。我曾经因为偷懒跳过了校准,结果 loss 直接 NaN,查了半天才发现是 scale 设得太小了。

4.3.2 微调超参数设置

超参数 推荐值 说明
学习率 浮点训练的 1/10 ~ 1/100 QAT 是微调,学习率太大容易破坏已学到的特征
训练轮数 5 ~ 20 epoch YOLO 模型通常 10 epoch 左右就够,多了反而过拟合
batch size 与浮点训练一致或略小 BN 层统计量需要足够样本
优化器 SGD 或 AdamW 我习惯用 SGD + cosine 学习率衰减
数据增强 与浮点训练一致 不要额外加增强,避免分布偏移

避坑指南: 我曾经在 QAT 微调时用了很大的 weight decay,结果量化后的权重分布被过度压缩,精度反而下降了。建议 weight decay 设为浮点训练的 1/2 或直接关掉。

4.3.3 逐层冻结策略

对于 YOLO 这种大模型,我建议采用逐层冻结的策略:

  1. 先冻结 backbone 的前几层,只微调 head 部分
  2. 逐步解冻更多层,让整个网络慢慢适应量化
  3. 最后几个 epoch 全部解冻,做全局微调

这样做的好处是:底层特征提取器相对稳定,不需要大改;高层检测头对量化误差更敏感,需要更多调整空间。

4.4 QAT 整体流程(SVG 图)

下面这张图展示了 QAT 从准备到部署的完整流程:

QAT 量化感知训练完整流程 浮点预训练模型 插入 FakeQuantize 节点 (权重 + 激活值) 校准数据统计 scale/zero_point QAT 微调(STE 反向传播) 学习率 1e-4 ~ 1e-5,5~20 epoch 导出 int8 量化模型 移除 FakeQuantize,替换为 int8 算子 部署到嵌入式设备 关键点 • 前向:模拟量化误差 • 反向:STE 绕过 round() • 微调:低学习率 + 逐层解冻 • 部署:移除伪量化节点

4.5 常见问题与避坑

QAT 虽然效果好,但坑也不少。我把自己踩过的和帮别人排查过的典型问题列出来:

问题 现象 原因 解决方案
Loss 震荡不收敛 训练 loss 上下跳动,精度上不去 学习率太大,或伪量化节点初始化不准 降低学习率,重新做校准统计
量化后精度反而低于 PTQ QAT 模型精度不如直接 PTQ 微调过度,破坏了原始特征 减少 epoch,或冻结 backbone 前几层
推理时输出 NaN 部署后模型输出异常值 scale/zero_point 溢出,或算子不兼容 检查量化参数范围,确认推理框架支持
BN 层统计量漂移 验证集精度正常,测试集崩了 微调时 BN 层 running_mean/var 未更新 设置 torch.nn.BatchNorm2d 的 track_running_stats=True

我的习惯: 每次 QAT 微调结束后,我都会用验证集做一次「模拟部署」——把伪量化节点换成真正的量化算子,跑一遍推理,对比精度。这一步能提前发现很多部署阶段才会暴露的问题。

4.6 小结

QAT 的核心就三件事:伪量化节点模拟误差、直通估计器让梯度流通、低学习率微调让模型适应量化。说白了,就是让模型在训练时就「吃」下量化这颗药,提前产生抗体。

我个人觉得,QAT 是嵌入式部署中最值得投入时间的技术之一。虽然比 PTQ 多花几个小时训练,但换来的是 0.5%~2% 的精度提升,在资源受限的设备上,这点精度可能就是「能用」和「不能用」的区别。