量化感知训练(QAT)原理:伪量化节点、直通估计器(STE)、QAT训练流程、与训练后量化(PTQ)的对比
各位同学,咱们今天聊聊量化感知训练,也就是 QAT。说实话,很多刚入行的朋友觉得 PTQ(训练后量化)够用了,跑个校准集,模型直接转 int8,省事。但我在实际项目中踩过不少坑——有些模型,尤其是小模型或者对精度极其敏感的任务,PTQ 一压下去,精度掉得你心疼。这时候,QAT 就是你的救命稻草。
为什么需要 QAT?PTQ 的局限性在哪?
PTQ 的原理很简单:拿一批校准数据,统计出每层激活值和权重的 min/max,然后直接映射到 int8。听起来很完美对吧?但问题在于,量化本身是有损的。你想想看,原本 float32 能表达的精细数值,硬塞进 256 个离散值里,信息必然丢失。
更关键的是,PTQ 没有让模型“适应”这种损失。模型在训练时看到的全是 float32 的前向传播,它不知道推理时会被量化。这就好比一个运动员一直在平原训练,突然让他去高原比赛,成绩肯定打折扣。
核心矛盾:训练时的浮点精度 vs 推理时的量化精度。PTQ 是“事后补救”,QAT 是“事前预防”。
QAT 的核心:伪量化节点(Fake Quantize)
QAT 的思路很直接:在训练过程中就模拟量化效果。具体怎么做?在网络的每一层(或者关键层)后面插入一个“伪量化节点”。这个节点干两件事:
- 前向传播时:把 float32 的输入先量化成 int8,再反量化回 float32。这样模型看到的虽然是 float32 的张量,但数值已经带上了量化误差。
- 反向传播时:正常计算梯度,更新权重。
伪量化节点的数学表达很简单:
# 伪量化操作
q = round(clamp(x / scale, -128, 127)) # 量化
x_hat = q * scale # 反量化
# 前向传播时,用 x_hat 替代 x 参与后续计算
# 反向传播时,梯度直接跳过 round 和 clamp,直通到 x
这里有个关键点:scale 和 zero_point 在训练过程中是动态更新的。我习惯在每个 batch 后根据当前激活值的统计量更新 scale,这样模型能逐步适应量化的数值范围。
直通估计器(STE):让梯度“绕道走”
刚才提到反向传播时梯度要“直通”,这就是 STE 的用武之地。你想想,round() 函数是不可导的,它的梯度几乎处处为 0。如果按正常链式法则,梯度传到伪量化节点就断了,模型根本学不下去。
STE 的做法很粗暴:前向传播该量化量化,反向传播假装量化不存在。梯度直接跳过 round() 和 clamp(),原封不动地传给前面的层。
我的经验:STE 虽然简单,但非常有效。不过要注意,如果量化误差太大(比如 scale 设置不合理),STE 传递的梯度方向可能是错的。我曾经在一个语音模型上遇到过训练不收敛,后来发现是初始 scale 太大,导致大部分激活值被截断。调整初始化策略后就好了。
STE 的数学表达可以理解为:
# 前向传播
y = round(x) # 量化
# 反向传播(STE)
∂L/∂x = ∂L/∂y # 梯度直接通过,忽略 round 的不可导性
QAT 训练流程:从浮点到量化的“渐进式适应”
完整的 QAT 流程,我一般分三步走:
| 阶段 | 操作 | 说明 |
|---|---|---|
| 1. 预训练 | 用 float32 正常训练模型,直到收敛或接近收敛 | 这一步不能省。从随机初始化直接做 QAT,效果很差 |
| 2. 插入伪量化节点 | 在权重和激活值后插入 FakeQuantize 节点,冻结 BN 层 | BN 层在 QAT 中需要冻结,否则 scale 会乱跳 |
| 3. 微调(Fine-tune) | 用小学习率(通常是预训练的 1/10)继续训练几个 epoch | 让模型适应量化噪声,恢复精度 |
这里有个细节:BN 层为什么要冻结? 因为伪量化节点改变了激活值的分布,如果 BN 还在统计 running mean/var,会导致 scale 和 zero_point 不稳定。我一般会在插入伪量化节点后,把 BN 切换到 eval 模式,用预训练时统计好的均值和方差。
避坑指南:我曾经在一个项目中,QAT 微调了 20 个 epoch 精度反而下降了。后来发现是学习率没调好。QAT 的学习率一定要小,建议用余弦退火或者阶梯下降,初始学习率不超过预训练的 1/10。
QAT vs PTQ:一张表说清楚
| 对比维度 | PTQ(训练后量化) | QAT(量化感知训练) |
|---|---|---|
| 是否需要训练 | 不需要,只需少量校准数据 | 需要,在预训练模型上微调 |
| 精度损失 | 较大,尤其对小模型或敏感任务 | 较小,通常能恢复 90% 以上的精度损失 |
| 部署复杂度 | 低,直接转换即可 | 中,需要修改训练代码,插入伪量化节点 |
| 适用场景 | 大模型、精度要求不高的场景 | 小模型、边缘设备、精度敏感任务 |
| 时间成本 | 几分钟到几小时 | 几小时到几天(取决于微调 epoch) |
说白了,PTQ 是“快但糙”,QAT 是“慢但精”。我个人习惯是:先跑 PTQ 看看精度,如果掉得不多(比如 1% 以内),就直接用 PTQ。如果掉得厉害,再上 QAT。毕竟 QAT 要改训练代码,还要多花时间微调,不是所有项目都值得。
QAT 在 CMSIS-NN 中的实际效果
最后聊点实战。我在 STM32F4 上部署过一个轻量级分类模型,PTQ 后精度从 92% 掉到了 85%,完全不可用。后来用 QAT 微调了 5 个 epoch,精度恢复到 91.2%。虽然比 float32 还是低了 0.8%,但已经可以接受了。
CMSIS-NN 本身不直接支持 QAT 训练,但你可以用 PyTorch 或 TensorFlow 的 QAT 工具链训练好模型,然后导出为 int8 的权重和 scale 参数,再手动移植到 CMSIS-NN 的推理代码中。这个过程我会在后面的章节详细讲。
一句话总结:QAT 通过伪量化节点让模型在训练时就“体验”量化误差,配合 STE 解决梯度传递问题,最终得到比 PTQ 更高质量的量化模型。代价是训练流程更复杂,时间成本更高。