量化感知训练(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 的思路很直接:在训练过程中就模拟量化效果。具体怎么做?在网络的每一层(或者关键层)后面插入一个“伪量化节点”。这个节点干两件事:

  1. 前向传播时:把 float32 的输入先量化成 int8,再反量化回 float32。这样模型看到的虽然是 float32 的张量,但数值已经带上了量化误差。
  2. 反向传播时:正常计算梯度,更新权重。

伪量化节点的数学表达很简单:

# 伪量化操作
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 更高质量的量化模型。代价是训练流程更复杂,时间成本更高。

QAT 训练流程与伪量化节点结构 训练流程 1. Float32 预训练 2. 插入伪量化节点 3. 冻结 BN 层 4. 小学习率微调 5. 导出 int8 模型 伪量化节点内部 输入 float32 量化:round(clamp(x/s)) 反量化:q * s 输出 float32(带误差) STE:反向传播时梯度直通 ∂L/∂x = ∂L/∂y(跳过 round/clamp) 插入伪量化节点