4、模型文件准备与格式转换:支持的模型格式(Keras/TFLite/ONNX)、模型量化(FP32/INT8)、输入输出张量形状对齐

好,咱们进入第四章。模型文件准备与格式转换。

说实话,很多人在STM32上跑AI模型,第一步就卡住了。不是模型太大,而是格式不对。你想想看,你在电脑上用Keras训练好的.h5文件,直接扔到CubeAI里,它根本不认。为什么?因为嵌入式环境需要的是轻量级、可优化的中间表示。

我个人习惯,在开始部署前,先把模型格式这件事理清楚。这能省掉后面80%的麻烦。

4.1 支持的模型格式:Keras / TFLite / ONNX

STM32CubeAI目前支持三种主流格式。我按推荐程度排个序:

格式 扩展名 推荐场景 我的评价
Keras .h5 快速原型验证 最直接,但体积大
TFLite .tflite 量化部署首选 强烈推荐,支持INT8
ONNX .onnx 跨框架迁移 兼容性好,但需额外转换

Keras (.h5):这是TensorFlow的原生格式。如果你用Keras训练,直接保存成.h5就行。我在项目中遇到过,直接用.h5导入CubeAI,它会自动帮你做一次转换。但注意,.h5文件里包含了完整的训练配置,很多信息对推理来说是冗余的。所以,我个人建议,除非你只是做快速验证,否则别用这个格式做最终部署。

TFLite (.tflite):这是我最推荐的格式。说白了,它就是为移动端和嵌入式端设计的。体积小、速度快,而且支持量化。你想想看,一个50MB的Keras模型,转成TFLite INT8后可能只有5MB。这在STM32上意味着什么?意味着你能跑更大的模型,或者省下Flash空间放别的代码。

ONNX (.onnx):这个格式的好处是跨框架。比如你用PyTorch训练,转成ONNX,再给CubeAI用。但要注意,ONNX到CubeAI的转换链路相对长一些,中间容易出问题。我曾经有一次,ONNX模型里有个自定义算子,CubeAI死活不认,最后只能手动改网络结构。所以,能用TFLite就别折腾ONNX。

核心原则:能选TFLite就选TFLite。这是STM32CubeAI的“母语”。

4.2 模型量化:FP32 vs INT8

量化,说白了就是把模型里的浮点数变成整数。为什么要这么做?因为STM32没有GPU,浮点运算全靠CPU硬算,慢得让人抓狂。而整数运算,特别是8位整数,有硬件加速支持。

我刚开始做量化时,总觉得精度会掉很多。后来发现,对于大多数分类任务,INT8量化后精度损失不到1%。但如果是回归任务或者目标检测,就要小心了。

量化类型 权重精度 激活精度 典型体积缩减 推理速度提升
FP32 32位浮点 32位浮点 1x 1x(基准)
INT8(权重量化) 8位整数 32位浮点 4x 2-3x
INT8(全量化) 8位整数 8位整数 4x 4-6x

FP32:精度最高,但体积大、速度慢。适合在开发板上做精度验证。我一般用FP32模型作为“黄金标准”,用来对比量化后的精度损失。

INT8权重量化:只把权重转成INT8,激活值还是FP32。这种方式精度损失很小,但速度提升有限。适合那些对精度敏感、但对速度要求不高的场景。

INT8全量化:权重和激活值都转成INT8。这是我最常用的方式。速度提升明显,而且如果校准数据集选得好,精度几乎不掉。

我的经验:做INT8全量化时,校准数据集一定要有代表性。我曾经用100张猫的图片做校准,结果模型对狗的分类精度掉了5%。后来换成混合数据集,问题就解决了。

4.3 输入输出张量形状对齐

嗯,这里要注意。很多人在这一步翻车。

模型在电脑上训练时,输入张量的形状可能是 (None, 224, 224, 3)。这个None代表batch size,在训练时可以任意。但部署到STM32上时,batch size必须是1。为什么?因为STM32一次只处理一帧数据。

所以,你在导出模型前,必须把输入形状固定下来。我建议这样做:

# 固定batch size为1
import tensorflow as tf

# 加载你的Keras模型
model = tf.keras.models.load_model('my_model.h5')

# 重新定义输入形状,batch size固定为1
inputs = tf.keras.Input(shape=(224, 224, 3), batch_size=1)
outputs = model(inputs)

# 创建新模型
fixed_model = tf.keras.Model(inputs=inputs, outputs=outputs)

# 保存为TFLite
converter = tf.lite.TFLiteConverter.from_keras_model(fixed_model)
tflite_model = converter.convert()

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

除了batch size,还要注意数据顺序。在TensorFlow中,图像数据的默认顺序是 (height, width, channels),也就是HWC。但有些框架用的是CHW。STM32CubeAI默认使用HWC,所以如果你的模型是CHW顺序,需要做转置。

输出张量也要对齐。比如你的模型输出是10个类别的概率,形状就是 (1, 10)。这个1就是batch size。在STM32上,你读取输出时,直接取第0维的数据就行。

避坑指南:我曾经遇到过一个模型,输入形状是 (1, 3, 224, 224),这是CHW格式。直接导入CubeAI后,推理结果全是错的。后来发现是数据排列顺序不对。解决办法是在模型转换时加一个转置层,或者在上位机预处理时就把数据排好。

4.4 实战:从Keras到TFLite INT8的完整流程

好,咱们走一遍完整流程。假设你有一个训练好的Keras模型。

import tensorflow as tf
import numpy as np

# 1. 加载模型
model = tf.keras.models.load_model('my_model.h5')

# 2. 固定batch size
inputs = tf.keras.Input(shape=(224, 224, 3), batch_size=1)
outputs = model(inputs)
fixed_model = tf.keras.Model(inputs=inputs, outputs=outputs)

# 3. 准备校准数据集(用于INT8量化)
# 从你的训练集中取100-500张图片
calibration_images = np.random.randn(100, 224, 224, 3).astype(np.float32)

def representative_dataset():
    for i in range(100):
        yield [calibration_images[i:i+1]]

# 4. 转换并量化
converter = tf.lite.TFLiteConverter.from_keras_model(fixed_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()

# 5. 保存
with open('model_quant_int8.tflite', 'wb') as f:
    f.write(tflite_quant_model)

print("量化完成!模型大小:", len(tflite_quant_model) / 1024, "KB")

这段代码我用了很多次。注意第4步里的 inference_input_typeinference_output_type,我设置成了int8。这意味着输入输出都是8位整数。在STM32上,你需要把输入数据也转成int8,然后推理结果也是int8。这样做的好处是,整个推理链路都是整数运算,速度最快。

但如果你不想改上位机代码,也可以把输入输出设成float32。这样CubeAI会在内部做类型转换,但会损失一些性能。

最终建议:对于STM32部署,我推荐使用TFLite格式 + INT8全量化。输入输出形状固定为batch_size=1,数据顺序为HWC。这样配置,基本能发挥出STM32的最佳性能。

好了,模型文件准备这块就讲到这里。下一章咱们聊聊如何在CubeMX里配置AI模型,以及那些容易忽略的细节。