3、模型量化入门:为什么需要量化?量化原理(FP32到INT8)、训练后量化(Post-training Quantization)、量化感知训练(QAT)简介
好,咱们进入第三讲。说实话,量化这个话题,是很多嵌入式AI新手的第一道坎。我记得刚入行那会儿,看到“量化”两个字,第一反应是——这玩意儿是不是把模型给“压缩”了?其实没那么玄乎。
今天我就带你把这层窗户纸捅破。咱们从三个角度聊:为什么非得量化、FP32到INT8到底发生了什么、以及两种主流量化方法——训练后量化(PTQ)和量化感知训练(QAT)。
3.1 为什么需要量化?——MCU的“内存焦虑”
你想想看,一个典型的TinyML模型,比如MobileNet V1,FP32版本大概4.3MB。听起来不大?但很多MCU的Flash只有512KB甚至256KB。4.3MB塞进去?门都没有。
更关键的是RAM。FP32模型推理时,中间激活值动不动就几百KB。而Cortex-M4内核的MCU,SRAM通常只有128KB~256KB。跑一次推理,内存直接爆掉。
我遇到过最惨的一次,是在STM32F407上部署一个语音关键词识别模型。模型本身1.2MB,Flash勉强够。但一跑推理,SRAM直接溢出,系统反复重启。后来量化到INT8,模型体积降到300KB,RAM占用也砍了一半。嗯,这才跑起来。
所以量化解决的核心问题就三个:
- 模型体积缩小:FP32到INT8,直接缩到1/4
- 内存占用降低:中间激活值也变小了
- 推理速度提升:INT8运算比FP32快2~4倍(很多MCU有硬件加速)
核心结论:量化不是可选项,而是MCU部署的必选项。不量化,你连模型都塞不进去。
3.2 量化原理:FP32到INT8,到底发生了什么?
说白了,量化就是把一个高精度的数值范围,映射到一个低精度的数值范围。FP32是32位浮点数,范围大约是±3.4×10³⁸。INT8是8位有符号整数,范围只有-128到127。
怎么映射?核心公式就一个:
q = round(r / S) + Z
其中:
- r:原始FP32值
- q:量化后的INT8值
- S:缩放因子(Scale),FP32的小数
- Z:零点偏移(Zero Point),INT8的整数
反过来,反量化公式:
r = (q - Z) × S
举个例子。假设我们有一组FP32权重,范围是[-1.0, 1.0]。要量化到INT8的[-128, 127]:
S = (1.0 - (-1.0)) / (127 - (-128)) = 2.0 / 255 ≈ 0.00784
Z = round(0 - (-1.0) / 0.00784) = round(127.55) = 128
那么FP32的0.5,量化后就是:
q = round(0.5 / 0.00784) + 128 = round(63.78) + 128 = 64 + 128 = 192
你看,就这么简单。但这里有个坑——精度损失。因为round操作会引入误差。我刚开始做量化时,总觉得误差无所谓。直到有一次,一个分类模型量化后准确率从95%掉到了82%。排查了半天,发现是某个层的权重范围特别大,导致量化步长S太大,精度损失严重。
避坑指南:量化误差主要来自两个地方——截断误差(超出INT8范围的值被截断)和舍入误差(round操作)。对于权重分布特别不均匀的层,建议单独统计范围,不要用全局统计。
3.3 训练后量化(Post-training Quantization)
PTQ,顾名思义,就是模型训练完之后再做量化。这是最省事的方法,也是我推荐新手入门的方式。
具体流程:
- 训练一个FP32模型(正常训练流程)
- 准备一小部分校准数据(通常几百张图片或几十段音频)
- 用校准数据跑一遍推理,统计每层的激活值范围(min, max)
- 根据统计结果计算每层的S和Z
- 将权重和激活值都量化到INT8
在TFLite Micro中,PTQ的实现非常简单:
import tensorflow as tf
# 加载FP32模型
converter = tf.lite.TFLiteConverter.from_saved_model('model_fp32')
# 设置量化优化
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# 提供校准数据集(代表性数据)
def representative_dataset():
for _ in range(100):
data = np.random.randn(1, 224, 224, 3).astype(np.float32)
yield [data]
converter.representative_dataset = representative_dataset
# 转换到INT8
tflite_model = converter.convert()
这段代码里,representative_dataset是关键。它提供的数据要能代表真实推理时的输入分布。我见过有人随便传了一堆噪声数据,结果量化后模型直接废了。
我的经验:校准数据最好从训练集或验证集中随机抽取,数量不用太多,200~500个样本就够。多了反而可能过拟合到校准集上。
PTQ的优点很明显:快、简单、不需要重新训练。但缺点也很突出:精度损失可能较大,尤其是对激活值范围敏感的网络(比如MobileNet系列)。
3.4 量化感知训练(Quantization-aware Training)
QAT,说白了就是在训练过程中模拟量化误差,让模型学会“忍受”量化带来的精度损失。
具体做法:
- 在FP32训练的前向传播中,插入伪量化节点(Fake Quantization)
- 这些节点模拟量化-反量化过程(即:FP32→INT8→FP32)
- 反向传播时,梯度仍然用FP32计算(因为量化操作不可导)
- 训练完成后,移除伪量化节点,导出真正的INT8模型
在TFLite中,QAT的实现需要先训练一个带伪量化节点的模型:
import tensorflow_model_optimization as tfmot
# 在原始模型上应用QAT
quantize_model = tfmot.quantization.keras.quantize_model
# 假设model是你的FP32模型
qat_model = quantize_model(model)
# 正常训练(学习率可以调小一点)
qat_model.compile(optimizer='adam', loss='categorical_crossentropy')
qat_model.fit(x_train, y_train, epochs=5)
# 导出TFLite模型
converter = tf.lite.TFLiteConverter.from_keras_model(qat_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
QAT的精度通常比PTQ高1%~3%。我做过一个对比实验:在CIFAR-10上,ResNet-20的PTQ精度掉了2.1%,而QAT只掉了0.3%。
什么时候用QAT? 当PTQ精度损失超过可接受范围(比如>1%),或者模型对量化特别敏感(比如有大量BatchNorm层)时,建议上QAT。
但QAT也有代价:训练时间变长(因为前向传播多了量化模拟),而且需要重新训练。如果你的项目时间紧,PTQ够用就别折腾QAT。
3.5 PTQ vs QAT:怎么选?
| 对比维度 | PTQ(训练后量化) | QAT(量化感知训练) |
|---|---|---|
| 实现难度 | 低(几行代码搞定) | 中(需要修改训练流程) |
| 精度损失 | 较大(通常0.5%~3%) | 较小(通常0.1%~1%) |
| 训练时间 | 不需要额外训练 | 需要额外训练(约1.5倍时间) |
| 适用场景 | 快速原型、精度要求不高 | 精度敏感、生产部署 |
| 校准数据需求 | 需要少量校准数据 | 不需要(训练时已学习) |
我个人习惯是:先跑PTQ看看精度。如果掉点超过1%,再考虑QAT。毕竟QAT要重新训练,时间成本摆在那。
嗯,这一讲就到这。量化入门其实不难,关键是理解“映射”的本质。下一讲咱们会深入TFLite Micro的量化工具链,手把手教你如何把量化后的模型部署到MCU上。
课后小作业:找一个你手头的FP32模型,用PTQ量化到INT8,对比一下模型体积和推理精度的变化。你会发现,量化后的模型体积只有原来的1/4,但精度可能只掉了不到1%。这就是量化的魅力。