3. TensorFlow Lite 模型准备:从选型到导出
好,咱们进入第三章。这一章要解决一个很实际的问题——怎么把 TensorFlow 里的模型,变成能在 STM32 上跑的东西。说白了,就是模型选型、量化、导出这三步。我做了好几个项目,发现这一步要是没走对,后面调内存、调性能全白搭。
3.1 模型选择原则:为什么我偏爱 MobileNetV1
选模型就像选工具。你不能拿个大锤去修手表,对吧?在 STM32 这种资源受限的 MCU 上,模型选择直接决定了你能不能跑起来。
我个人习惯,先看三个硬指标:
- 参数量:STM32 的 Flash 通常就 512KB~2MB,模型权重不能太大
- 乘加运算量(MACC):决定了推理速度,MCU 主频一般 80~240MHz
- 内存峰值:激活值、中间特征图占 RAM,这个经常被忽略
拿 MobileNetV1 来说,它用深度可分离卷积代替标准卷积。什么意思呢?标准卷积一次干两件事——空间滤波和通道融合。MobileNetV1 把它拆成两步:先逐通道做空间滤波,再用 1x1 卷积做通道融合。这样参数量直接降到原来的 1/8 到 1/9。
关键数据对比(输入 224x224x3,分类数 1000):
| 模型 | 参数量 | MACC | STM32 可行性 |
|---|---|---|---|
| MobileNetV1 (α=1.0) | 4.2M | 569M | 需量化 + 裁剪 |
| MobileNetV1 (α=0.5) | 1.3M | 149M | 较可行 |
| MobileNetV1 (α=0.25) | 0.5M | 41M | 推荐 |
| ResNet-50 | 25.6M | 3.8G | 基本不可行 |
我在一个工业缺陷检测项目里,一开始用了 ResNet-18,结果 STM32H743 的 1MB Flash 根本塞不下。后来换成 MobileNetV1 α=0.5,量化后模型才 400KB 左右,跑一次推理 200ms,完全够用。嗯,选对模型,事半功倍。
3.2 模型量化:FP32 转 INT8 的实战要点
量化,就是把 FP32 的权重和激活值,映射到 INT8 的整数范围。为什么要做?STM32 没有 FPU 的型号很多,就算有,INT8 的乘加运算也比 FP32 快 2~4 倍,内存占用直接砍到 1/4。
量化的核心公式很简单:
q = round(r / scale) + zero_point
其中:
r = 原始浮点值
scale = 缩放因子(浮点)
zero_point = 零点偏移(整数)
q = 量化后的 INT8 值
但实际做的时候,有几个坑要注意。我曾经在一个语音唤醒项目里,直接用了 post-training 量化,结果精度掉了 5%。后来发现是激活值分布不均匀,有些通道的 scale 设得太粗了。
我的量化建议:
- 先用 post-training 量化试水:不需要重新训练,速度快。如果精度损失 < 1%,直接用它
- 精度损失大?上量化感知训练(QAT):在训练时模拟量化误差,精度恢复效果明显
- 注意激活值范围:ReLU 后的激活值都是非负的,zero_point 可以设为 0,省一点计算
- 逐通道量化 vs 逐层量化:逐通道精度更高,但 STM32Cube.AI 目前支持有限,我建议先用逐层
量化流程,我一般这么走:
- 准备校准数据集(100~500 张典型图片)
- 用 TensorFlow Lite Converter 做量化
- 对比量化前后的精度
- 如果精度下降 > 2%,改用 QAT
3.3 模型导出为 .tflite 格式
这一步其实最机械,但最容易出错。我见过有人导出时忘了设置输入输出张量的名字,结果在 STM32Cube.AI 里解析失败。
下面是我常用的导出脚本,加了详细的注释:
import tensorflow as tf
# 加载你的 Keras 模型
model = tf.keras.models.load_model('mobilenet_v1_0.5.h5')
# 转换器配置
converter = tf.lite.TFLiteConverter.from_keras_model(model)
# 设置量化 —— 这里用默认的 INT8 量化
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# 提供校准数据,用于计算激活值的动态范围
def representative_dataset():
# 从你的验证集里取 100 张图
for i in range(100):
img = load_and_preprocess(f'calib_data/img_{i}.jpg')
# 注意:必须保持和模型输入一样的 shape
yield [img.astype(np.float32)]
converter.representative_dataset = representative_dataset
# 强制输出 INT8(有些算子可能 fallback 到 FP32)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
# 设置输入输出格式
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8
# 导出
tflite_model = converter.convert()
with open('mobilenet_v1_0.5_quant.tflite', 'wb') as f:
f.write(tflite_model)
print('导出完成!模型大小:', len(tflite_model) / 1024, 'KB')
常见导出错误:
- representative_dataset 没写对:必须返回 list 或 generator,每个元素是 shape 匹配的 numpy array
- 模型输入是 uint8,但校准数据是 float32:记得做归一化或类型转换
- 某些算子不支持 INT8:比如一些自定义层,需要先替换成标准算子
3.4 知识体系总览
下面这张图,把本章的核心逻辑串起来了。从模型选型开始,到量化决策,再到导出验证,每一步都有明确的判断条件。
3.5 导出后的检查清单
模型导出来了,别急着往 STM32 里烧。先做几个快速检查:
| 检查项 | 方法 | 合格标准 |
|---|---|---|
| 模型大小 | 查看 .tflite 文件大小 | < 512KB(视 Flash 而定) |
| 输入输出张量 | 用 tf.lite.Interpreter 查看 |
名字、shape、dtype 正确 |
| 推理结果 | 用 Python 跑一次推理 | 与原始模型输出一致(误差 < 5%) |
| 算子支持 | 用 tflite_convert 的 --dump 选项 |
所有算子都在 STM32Cube.AI 支持列表里 |
一个小技巧:导出后,用 Netron 打开 .tflite 文件,可视化检查网络结构。我每次都会看一遍,确认没有多余的输入输出节点,或者奇怪的 reshape 层。有一次我发现模型里多了一个 Identity 层,就是靠 Netron 抓出来的。