4. 模型文件管理:.tflite、.onnx、.h5 等模型文件的存储与差异化管理

模型文件管理这件事,说实话,我早期做 AI 嵌入式项目时踩过不少坑。那时候项目一多,桌面上全是 model_final.h5、model_final_v2.h5、model_final_真的不改了.h5…… 你想想看,三个月后连自己都分不清哪个版本是真正部署到产线上的。

这一章,我就把模型文件管理的经验拆开揉碎了讲。咱们重点解决三个问题:不同格式到底有什么区别?怎么存才不乱?怎么在 STM32 项目里高效切换模型?

4.1 三大模型格式的本质差异

先看一张对比表,把 .h5、.tflite、.onnx 的核心差异说清楚。

格式 来源框架 主要用途 STM32 直接支持? 文件大小(典型) 量化支持
.h5 Keras / TensorFlow 1.x 训练保存、断点续训 ❌ 需转换 大(浮点权重) 无原生量化
.tflite TensorFlow Lite 移动端 / 嵌入式推理 ✅ 直接导入 小(可量化至 1/4) INT8 / FP16
.onnx PyTorch / TF / ONNX 框架间转换、中间交换 ⚠️ 需转 .tflite 中等 需额外工具

核心结论:.h5 是「训练态」,.onnx 是「交换态」,.tflite 是「部署态」。STM32Cube AI 只认 .tflite,其他格式必须走转换流程。

我个人习惯在项目里同时保留三种格式,但用不同的目录隔离。为什么?因为 .h5 用来回滚训练参数,.onnx 用来跨团队协作,.tflite 才是最终烧进芯片的东西。缺一个,后面都可能抓瞎。

4.2 模型文件的存储结构设计

我在上一个工业视觉项目中,吃过「模型文件乱放」的亏。当时产线固件升级,发现烧进去的模型是旧版本,排查了两天才找到原因——同事把新模型覆盖了旧文件,但文件名没改。

后来我定了一套目录规范,分享给你参考:

models/
├── training/                  # 训练原始文件
│   ├── v1.0/
│   │   ├── model.h5
│   │   └── training_log.txt
│   ├── v1.1/
│   │   ├── model.h5
│   │   └── training_log.txt
│   └── latest/                # 软链接指向当前训练版本
├── onnx/                      # 中间交换格式
│   ├── v1.0_model.onnx
│   ├── v1.1_model.onnx
│   └── latest.onnx
├── tflite/                    # 部署文件
│   ├── v1.0/
│   │   ├── model_float32.tflite
│   │   ├── model_int8.tflite
│   │   └── model_float16.tflite
│   ├── v1.1/
│   │   ├── model_float32.tflite
│   │   ├── model_int8.tflite
│   │   └── model_float16.tflite
│   └── current/               # 当前产线使用的版本
│       └── model.tflite
└── manifest.json              # 版本清单文件

我的小技巧:在 manifest.json 里记录每个版本的 SHA256 校验值、量化方式、输入输出张量形状。这样固件升级时可以先校验文件完整性,避免烧进去一个损坏的模型。

4.3 格式转换的实战流程

从 .h5 到 .tflite,看起来就一行代码,但里面门道不少。我直接给你一个我项目里用熟的转换脚本框架:

import tensorflow as tf

# 加载训练好的 .h5 模型
model = tf.keras.models.load_model('models/training/v1.1/model.h5')

# 转换为 float32 的 .tflite(基准版本)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_fp32 = converter.convert()
with open('models/tflite/v1.1/model_float32.tflite', 'wb') as f:
    f.write(tflite_fp32)

# 转换为 int8 量化版本(STM32 最常用)
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_int8 = converter.convert()
with open('models/tflite/v1.1/model_int8.tflite', 'wb') as f:
    f.write(tflite_int8)

注意:int8 量化必须提供 representative_dataset,否则转换出来的模型精度可能崩掉。我曾经有一次偷懒没给校准集,结果模型在 STM32 上推理结果全是 0,排查了整整一个下午。

至于 .onnx 转 .tflite,我一般走两条路:

  • 路径 A:.onnx → onnx2tf → .tflite(适合 PyTorch 导出的模型)
  • 路径 B:.onnx → onnx2keras → .h5 → .tflite(适合需要中间调试的场景)

我个人更倾向路径 A,因为少一步转换就少一次精度损失。但如果你需要对比量化前后的输出差异,路径 B 更灵活。

4.4 模型文件的差异化管理策略

项目迭代过程中,模型文件会越来越多。怎么管理这些差异?我总结了三板斧:

  1. 命名即文档:文件名里必须包含版本号、量化类型、输入尺寸。比如 v1.1_int8_96x96.tflite,一看就知道是什么。
  2. 差异对比自动化:每次生成新模型,自动跑一个脚本,对比新旧模型的输出差异。我写过一个小工具,输入两张 .tflite,输出每个输出张量的最大绝对误差。
  3. 版本回滚机制:在 STM32 的固件里预留两个模型分区。一个跑当前版本,一个跑上一个稳定版本。如果新模型推理异常,自动回退。

避坑指南:我曾经在固件里只存了一个模型分区,结果 OTA 升级到一半断电了,模型文件损坏,设备直接变砖。从那以后,我坚持双分区 + 校验机制。

4.5 模型文件在 STM32 项目中的集成

STM32Cube AI 导入 .tflite 后,会生成一个 network.c / network.h 的 C 代码文件。但这里有个容易被忽略的点:模型文件本身要不要烧进 Flash?

我的做法是:

  • 小模型(< 256 KB):直接编译进固件,作为常量数组放在 Flash 里。访问速度快,不需要外部存储。
  • 大模型(256 KB ~ 2 MB):放在外部 SPI Flash 或 QSPI Flash 中,固件里只存加载地址和长度。
  • 超大模型(> 2 MB):考虑模型剪枝或改用更小的网络结构。STM32 的 Flash 资源有限,硬塞进去会影响其他功能。

下面这张图展示了模型文件从训练到部署的完整流转路径:

模型文件流转与差异化管理流程 训练阶段 .h5 格式 交换阶段 .onnx 格式 部署阶段 .tflite 格式 转换 量化 版本目录管理 + manifest.json 清单 按版本号 / 量化类型 / 输入尺寸分层存储 差异化管理:命名规范 + 自动对比 + 双分区回滚 每次迭代自动生成差异报告,固件预留回滚分区 STM32 集成 小模型→Flash数组 / 大模型→外部SPI Flash

4.6 模型文件管理的自动化脚本

手动管理模型文件太容易出错了。我写了一个 Python 脚本,每次训练完自动执行以下操作:

  1. 读取 manifest.json,确定下一个版本号
  2. 转换 .h5 → .tflite(float32 + int8 两个版本)
  3. 计算每个文件的 SHA256 并写入清单
  4. 运行新旧模型对比,输出精度差异报告
  5. 如果精度下降超过 1%,自动告警并阻止部署

效率提升:这个脚本帮我节省了至少 70% 的模型管理时间。以前每次迭代要手动操作 10 分钟,现在一键搞定。你可以在 GitHub 上找到类似的开源工具,但建议根据自己项目的目录结构改一改。

嗯,模型文件管理这块,说白了就是「存得清楚、转得规范、回得安全」。做到这三点,你的 STM32 AI 项目就不会在模型文件上翻车。