2. TensorFlow Lite 模型量化:训练后量化、量化感知训练与混合量化策略
说到模型量化,很多刚入门的同学第一反应就是「把 float32 转成 int8 嘛,精度肯定掉得厉害」。说实话,我早年也这么想。直到我在一个智能门锁项目里,硬是把一个 4MB 的人脸识别模型压到了 800KB,跑在 STM32F4 上还能保持 97% 的准确率——从那以后,我对量化的看法彻底变了。
量化不是简单的精度换速度。它是一门平衡的艺术。今天我就把三种主流量化策略掰开揉碎讲清楚。
2.1 训练后量化(Post-training Quantization)
这是最省事的方法。模型训练完了,直接拿来做量化。不需要重新训练,不需要改网络结构。说白了,就是「事后补救」。
TensorFlow Lite 提供了三种训练后量化方式:
| 量化方式 | 权重类型 | 激活值类型 | 典型精度损失 | 适用场景 |
|---|---|---|---|---|
| 动态范围量化 | int8 | float32 | < 1% | 快速验证、资源充足 |
| 全整数量化 | int8 | int8 | 1-3% | STM32 等嵌入式部署 |
| float16 量化 | float16 | float32 | < 0.5% | GPU 加速场景 |
我个人习惯先用动态范围量化试水。为什么?因为它快。你只需要几行代码就能看到量化后的效果。
import tensorflow as tf
# 加载训练好的模型
model = tf.keras.models.load_model('my_model.h5')
# 动态范围量化
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_quant_model = converter.convert()
# 保存量化模型
with open('model_dynamic.tflite', 'wb') as f:
f.write(tflite_quant_model)
但注意了,动态范围量化只在推理时把权重转成 int8,激活值还是 float32。这意味着你的 MCU 还得保留浮点运算单元。对于没有 FPU 的 STM32 型号,比如 STM32F103,这招就不太灵了。
2.2 全整数量化(Full Integer Quantization)
全整数量化才是嵌入式部署的「真香」方案。权重和激活值全部用 int8 表示。你想想看,一个 32 位的浮点数变成 8 位整数,模型体积直接缩到四分之一。
实现全整数量化需要一个小技巧——提供代表性数据集(Representative Dataset)。TensorFlow Lite 需要用它来校准激活值的动态范围。
def representative_dataset():
# 从验证集中取 100 个样本
for i in range(100):
data = val_images[i:i+1]
yield [data.astype(np.float32)]
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_quant_model = converter.convert()
这里有个关键点:supported_ops 设置成 TFLITE_BUILTINS_INT8,强制所有算子都用 int8 实现。如果模型里有不支持的算子,转换会失败。我遇到过好几次,最后发现是某个自定义层没注册量化实现。
tf.lite.TFLiteConverter 的 allow_custom_ops 参数检查一下。或者干脆把模型里的自定义层换成标准层,省得后面折腾。
2.3 量化感知训练(Quantization-Aware Training, QAT)
训练后量化虽然方便,但精度损失有时候扛不住。尤其是小模型,比如 MobileNetV2 这种轻量网络,量化后准确率可能掉 3-5 个点。这时候就得请出量化感知训练了。
QAT 的原理其实不复杂:在训练过程中模拟量化效果,让模型自己去适应低精度表达。说白了,就是让模型「提前知道」自己将来要被量化,提前做好心理准备。
import tensorflow_model_optimization as tfmot
# 在模型上应用 QAT
quantize_model = tfmot.quantization.keras.quantize_model
# 注意:这里要用原始模型,不是已经训练好的量化模型
qat_model = quantize_model(model)
# 编译并训练
qat_model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 用较小的学习率微调几个 epoch
qat_model.fit(train_images, train_labels,
epochs=5,
validation_data=(val_images, val_labels))
# 转换为 TFLite
converter = tf.lite.TFLiteConverter.from_keras_model(qat_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_qat_model = converter.convert()
我在一个工业缺陷检测项目里用过 QAT。原始模型 float32 精度 98.2%,训练后量化掉到 95.1%,差了 3 个点。用了 QAT 微调 5 个 epoch 后,精度回到 97.6%。虽然没完全恢复,但已经够用了。
2.4 混合量化策略
实际项目中,我很少只用一种量化策略。更多时候是混合着来。什么叫混合量化?就是模型里一部分层用 int8,一部分层用 float32。
为什么要混合?因为有些层对量化特别敏感。比如注意力机制里的 softmax,量化后误差会累积。再比如某些 BatchNorm 层,量化后行为会变。
TensorFlow Lite 支持按层指定量化策略:
# 先做全整数量化
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
# 指定某些层保持 float32
converter.target_spec.supported_ops = [
tf.lite.OpsSet.TFLITE_BUILTINS_INT8,
tf.lite.OpsSet.TFLITE_BUILTINS
]
# 或者用选择性量化
converter._experimental_disable_per_channel = False
converter._experimental_default_to_single_batch_inference = False
嗯,这里要注意:混合量化虽然灵活,但会引入额外的数据类型转换开销。每次从 int8 切到 float32,再切回来,都会消耗时钟周期。所以我的经验是:尽量让敏感层集中在一起,减少切换次数。
2.5 三种策略对比与选择
说了这么多,到底该选哪种?我整理了一个对比表,方便你决策:
| 维度 | 训练后量化 | 全整数量化 | 量化感知训练 |
|---|---|---|---|
| 实现难度 | 低(几行代码) | 中(需校准数据) | 高(需修改训练流程) |
| 精度损失 | 1-3% | 1-3% | < 1% |
| 推理速度提升 | 2-3x | 3-4x | 3-4x |
| 模型体积缩减 | 2x | 4x | 4x |
| 适用阶段 | 快速原型验证 | 嵌入式部署 | 精度敏感场景 |
我的个人建议是:
- 项目初期:先用动态范围量化快速验证可行性
- 确定部署平台后:切换到全整数量化,用代表性数据集校准
- 精度不达标时:引入 QAT,微调 3-5 个 epoch
- 特殊敏感层:用混合量化策略单独处理
最后说一句:量化不是银弹。有些模型结构天生不适合量化,比如含有大量 LayerNorm 的 Transformer。遇到这种情况,别硬来,考虑换模型结构或者用知识蒸馏。