4、模型导出与ONNX中间格式:打通从训练到部署的桥梁

模型训练好了,权重文件躺在硬盘里。接下来怎么办?直接扔给STM32?不行。训练框架(比如Keras)有自己的模型格式,而STM32Cube.AI只认它自己的那一套。中间需要一个“通用语言”——ONNX。

我个人习惯把ONNX比作“模型界的PDF”。不管你是用Word、LaTeX还是Pages写的文档,转成PDF就能到处打开。ONNX也是这个道理。它让不同框架的模型能互相流通,也让我们能提前做很多检查工作。

核心流程: Keras模型 → ONNX中间格式 → 验证与检查 → 简化优化 → 最终用于量化部署

4.1 Keras模型转ONNX:第一步,也是关键一步

把Keras的.h5模型转成.onnx文件,我用的是tf2onnx这个工具。它是个Python库,专门干这个活。

安装很简单:

pip install tf2onnx onnx onnxruntime

转换命令也直接:

python -m tf2onnx.convert \
    --saved-model ./saved_model \
    --output ./model.onnx \
    --opset 13

这里有个参数我要特别提一下——--opset。它指定ONNX算子集的版本。我建议用13或以上。为什么?因为STM32Cube.AI对较新版本的算子支持更好。我曾经用opset 11转过一个模型,结果部署时报算子不支持,折腾了两天才发现是版本太老。

参数 说明 我的建议
--saved-model Keras SavedModel格式的路径 推荐用SavedModel,别用.h5直接转
--output 输出的ONNX文件路径 文件名别带中文和空格
--opset ONNX算子集版本 至少13,推荐15或17
--inputs-as-nchw 指定输入格式为NCHW STM32通常用NCHW,加上这个参数

小技巧: 转换前先用onnx.shape_inference.infer_shapes检查一下模型输入输出的shape。很多转换失败都是因为shape信息丢失。

4.2 ONNX模型验证:别让错误模型进入下一环节

转完后的ONNX文件,必须验证。我见过太多人跳过这步,结果在STM32Cube.AI里报一堆莫名其妙的错。

验证用onnx.checker模块:

import onnx

model = onnx.load("model.onnx")
onnx.checker.check_model(model)
print("模型验证通过!")

如果验证通过,会打印“模型验证通过!”。如果失败,会抛出异常,告诉你哪里有问题。常见的错误包括:

  • 节点输入输出不匹配:某个算子的输入张量数量和定义不符
  • 类型错误:比如要求float32,你传了int8
  • 图结构错误:存在孤立节点或循环依赖

嗯,这里要注意:验证通过不代表模型在目标设备上一定能跑。它只说明ONNX文件本身是合法的。真正的兼容性检查,我们下一步做。

4.3 ONNX算子兼容性检查:提前发现“地雷”

STM32Cube.AI支持的算子有限。不是所有ONNX算子都能在MCU上跑。所以,我们必须提前检查。

我一般用两种方法:

  1. 手动查表:去ST官网下载算子支持列表,对照模型里的算子一个个查。适合小模型。
  2. 自动检查:写脚本遍历模型节点,和已知支持列表比对。适合大模型。

这里给个简单的自动检查脚本思路:

import onnx

model = onnx.load("model.onnx")
supported_ops = ["Conv", "Relu", "MaxPool", "AveragePool", 
                 "Gemm", "Flatten", "Softmax", "Add", "Mul"]

unsupported = []
for node in model.graph.node:
    if node.op_type not in supported_ops:
        unsupported.append(node.op_type)

if unsupported:
    print("不支持的算子:", set(unsupported))
else:
    print("所有算子都支持!")

我在项目中遇到过最坑的是Reshape算子。Keras里很常用,但某些版本的ONNX Reshape在STM32上不支持动态shape。解决办法是转成Flatten或固定shape的Reshape。

避坑指南: 我曾经有一个模型,验证通过了,算子也都在支持列表里,但部署后推理结果全错。查了两天,发现是BatchNormalization算子在训练和推理模式下行为不同。ONNX导出时默认是推理模式,但某些框架会搞混。解决办法是导出前显式设置training=False。

4.4 ONNX简化与优化:给模型“瘦身”

原始ONNX模型通常有很多冗余。比如:

  • 恒等节点(Identity):啥也不干,就传个值
  • 不必要的Cast:类型转换多余
  • 连续的Reshape:可以合并

我用onnx-simplifier这个工具来清理。它基于ONNX Runtime做符号推理,能去掉很多无用节点。

安装:

pip install onnx-simplifier

使用:

python -m onnxsim model.onnx model_simplified.onnx

简化前后的对比,我见过最夸张的:节点数从120降到80,模型大小从15MB降到11MB。而且推理速度还快了20%。

除了简化,我还会做几项优化:

  • 常量折叠:把能提前算的常量计算掉,减少运行时计算量
  • 算子融合:把Conv+BN+Relu这种组合融合成一个算子
  • 精度裁剪:如果模型对精度不敏感,可以把一些中间结果从float32降到float16

这些优化可以用onnxoptimizer库来做:

import onnxoptimizer

model = onnx.load("model_simplified.onnx")
passes = ["fuse_bn_into_conv", "eliminate_identity", "eliminate_nop_transpose"]
optimized_model = onnxoptimizer.optimize(model, passes)
onnx.save(optimized_model, "model_optimized.onnx")

你想想看,一个经过简化+优化的模型,在STM32上跑起来,不仅省内存,还省电。这对嵌入式设备来说,太重要了。

4.5 本章知识体系总览

下面这张图,把整个流程串起来了。我建议你保存下来,每次做模型导出时对照着看。

模型导出与ONNX中间格式 — 知识体系总览 Keras模型 .h5 / SavedModel tf2onnx ONNX模型 .onnx 文件 模型验证 onnx.checker 算子兼容性检查 对照STM32支持列表 简化与优化 onnxsim + onnxoptimizer 优化后的ONNX模型 可直接用于量化部署 关键检查点 1. 模型结构完整性(节点、边、shape) 2. 算子类型是否在STM32支持列表中 3. 输入输出格式(NCHW vs NHWC) 4. 动态shape是否已固定 核心原则:验证先行,优化跟进,兼容性贯穿始终 每一步都做检查,避免问题积累到部署阶段才暴露

说白了,模型导出这一步,花的时间可能只占整个项目的10%,但它决定了后面90%的工作能不能顺利进行。我见过太多项目,前面训练花了好几个月,结果导出时没注意算子兼容性,最后不得不重新训练。那感觉,真的不好受。

所以,我的建议是:每一步都做验证,每一步都做检查。别嫌麻烦。你想想看,在PC上发现问题的成本,和在STM32上发现问题的成本,差了多少倍?

我的工作流: 每次导出模型后,我会在本地用ONNX Runtime跑一次推理,和Keras模型的输出对比。误差在1e-5以内才算通过。这个习惯帮我避免了很多次“模型部署后结果不对”的尴尬。