3. TensorFlow Lite Micro 模型准备:轻量、量化、转换

好,到了第三章。前面两章我们把 STM32 和 Cube.AI 工具链搭起来了,也跑通了第一个示例。但说实话,那个示例模型离真正的低功耗部署还差得远。这一章,我们来解决核心问题:模型怎么才能又小又快,还能在单片机上跑起来。

我个人习惯把模型准备分成三步:选型、量化、转换。每一步都有坑,我踩过不少,今天一并告诉你。

3.1 选型:为什么是 MobileNetV1?

你可能会问,那么多模型,为什么偏偏选 MobileNetV1?

原因很简单:它专为移动和嵌入式设备设计。2017 年 Google 提出时,目标就是在手机和嵌入式设备上跑图像分类。它的核心是深度可分离卷积(Depthwise Separable Convolution),把标准卷积拆成两步,参数量和计算量直接降了一个数量级。

我在项目中做过对比:同样输入 96x96 的图像,MobileNetV1 的参数量大约是标准卷积的 1/8 到 1/10。对于 STM32 这种只有几百 KB RAM 的芯片,这个差距就是「能跑」和「跑不了」的区别。

选型原则: 对于 STM32 部署,模型参数量建议控制在 100K 以内,RAM 占用不超过芯片 SRAM 的 60%。MobileNetV1 的 0.25 倍宽度版本(alpha=0.25)正好在这个范围内。

当然,不是只有 MobileNetV1 可选。TinyML 社区还有不少好模型,我整理了一个对比表:

模型 参数量 RAM 占用(INT8) 适用场景 我的评价
MobileNetV1 (alpha=0.25) ~47K ~120KB 图像分类、关键词识别 最稳妥,资料多
MobileNetV2 (alpha=0.35) ~70K ~160KB 图像分类、检测 精度略高,但稍大
Tiny ConvNet ~20K ~60KB 简单分类、唤醒词 极致轻量,精度有限
DS-CNN ~30K ~80KB 音频关键词识别 音频场景首选

嗯,这里要注意:不要盲目追求小模型。我见过有人为了省 RAM 选了 Tiny ConvNet,结果精度掉了 15%,项目直接返工。选型要在精度和资源之间找到平衡点。

3.2 模型量化:INT8 的原理与实操

模型选好了,下一步就是量化。说白了,就是把模型里的浮点数(float32)变成整数(int8)。为什么这么做?因为 STM32 没有硬件浮点单元(或者有但功耗高),整数运算快得多,而且模型体积直接缩小 4 倍。

3.2.1 量化原理:一句话讲清楚

量化的核心公式其实很简单:

q = round(r / scale) + zero_point

其中 r 是原始浮点数,q 是量化后的整数,scale 和 zero_point 是量化参数。反量化就是反过来:

r = (q - zero_point) * scale

你想想看,本质上就是用整数去近似浮点数。scale 控制精度,zero_point 控制偏移。量化后的模型,每个权重和激活值都变成了 8 位整数,计算时用整数乘法累加,最后再反量化回浮点。

我的经验: 量化后的精度损失通常在 1-3% 以内。如果损失超过 5%,说明你的模型对量化敏感,可以尝试量化感知训练(QAT),而不是简单的训练后量化(PTQ)。

3.2.2 实操:用 TensorFlow 做 INT8 量化

实操部分,我用 TensorFlow 2.x 的 API 来演示。假设你已经训练好了一个 MobileNetV1 模型(浮点版本)。

第一步:准备代表性数据集

量化需要一小部分数据来校准 scale 和 zero_point。我一般用训练集的 100-200 张图片就够了。

import tensorflow as tf

# 加载训练好的浮点模型
model = tf.keras.models.load_model('mobilenet_v1_float.h5')

# 准备校准数据(从训练集中取 100 张)
def representative_dataset():
    for i in range(100):
        # 假设 data 是预处理好的图像,shape=(1, 96, 96, 3)
        yield [data[i:i+1].astype(np.float32)]

第二步:配置量化转换器

这里有个关键参数:optimizations 和 target_spec。我习惯这样配:

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_model = converter.convert()

注意 inference_input_type 和 inference_output_type 都设为 int8。这样整个模型从输入到输出都是整数,STM32 上不需要额外的浮点转换。

我曾经踩过的坑: 如果只设 optimizations=[tf.lite.Optimize.DEFAULT] 而不指定 supported_ops,转换器可能会保留部分浮点运算。结果模型里混着 int8 和 float32,在 STM32 上跑起来奇慢无比。一定要显式指定 TFLITE_BUILTINS_INT8。

第三步:验证量化模型

转换完一定要验证精度。我通常写个简单的测试脚本:

import numpy as np

# 加载量化模型
interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

# 获取输入输出张量
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# 测试 50 张图片
correct = 0
for i in range(50):
    # 输入已经是 int8 类型
    interpreter.set_tensor(input_details[0]['index'], test_data[i:i+1])
    interpreter.invoke()
    output = interpreter.get_tensor(output_details[0]['index'])
    if np.argmax(output) == test_labels[i]:
        correct += 1

print(f'量化模型精度: {correct/50:.2%}')

如果精度下降在可接受范围内,就可以进行下一步了。

3.3 模型转换:生成 .tflite 文件

量化完成后,tflite_model 就是我们要的 .tflite 文件。保存它:

with open('mobilenet_v1_int8.tflite', 'wb') as f:
    f.write(tflite_model)

这个文件就是最终要部署到 STM32 上的模型。你可以用 xxd 或 Python 把它转成 C 数组,后面章节会详细讲。

为了让你更直观地理解整个流程,我画了一张图:

模型准备全流程 1. 模型选型 MobileNetV1 / Tiny ConvNet 2. INT8 量化 训练后量化 / 量化感知训练 3. 格式转换 生成 .tflite 文件 选型要点 • 参数量 < 100K • RAM 占用 < 60% SRAM • 匹配任务场景 • 社区支持度 量化关键参数 • scale: 步长精度 • zero_point: 零点偏移 • 校准数据集 100-200 张 • 精度损失 < 3% 转换注意事项 • 指定 INT8 算子 • 输入输出类型一致 • 验证推理结果 • 保存为 .tflite 输出:mobilenet_v1_int8.tflite 体积缩小 4 倍,推理速度提升 3-5 倍

这张图把整个流程串起来了。从选型到量化再到转换,每一步都有对应的输出。你跟着走一遍,就能拿到可以在 STM32 上部署的模型文件。

3.4 避坑指南:我踩过的三个坑

最后,分享几个我实际项目中遇到的坑,你遇到了可以少走弯路。

  1. 量化后模型精度暴跌:有一次我量化一个语音模型,精度从 92% 掉到 60%。排查了半天,发现是校准数据太少(只用了 10 张)。增加到 200 张后,精度恢复到 89%。校准数据要覆盖真实场景的分布。
  2. 模型转换后无法在 STM32 上运行:原因是模型里包含了不支持的算子(比如 tf.reshape 的某些用法)。解决办法是转换前用 converter.target_spec.supported_ops 限制算子集,或者手动替换不支持的层。
  3. 输入输出类型不匹配:我试过输入是 int8,输出是 float32,结果在 STM32 上推理结果全是错的。后来统一设为 int8 才解决。记住:嵌入式设备上,类型一致性比精度更重要。
小技巧: 转换完成后,用 tflite_convert 命令行的 --output_format 参数可以快速检查模型是否完全量化。如果输出显示「Fully quantized」,那就稳了。