4. TensorFlow 模型导出 ONNX:tf2onnx 工具实战
好,咱们进入第四章。前面几章我们把 ONNX 的基础概念和工具链搭好了,现在该动真格的了——把 TensorFlow 训练出来的模型转成 ONNX。
说实话,TF 转 ONNX 在几个框架里算是最折腾的。PyTorch 有 torch.onnx.export,一步到位。TF 这边呢?版本多、API 乱、还有 SavedModel、Keras H5、Checkpoint 好几种格式。我刚开始做这个的时候,光踩版本坑就花了两天。
不过别怕。今天我就把 tf2onnx 这个工具给你讲透。你跟着走一遍,以后遇到 TF 模型导出,心里就有底了。
4.1 tf2onnx 是什么?为什么选它?
tf2onnx 是微软开源的一个转换工具。它的核心作用就一个:把 TensorFlow 的计算图翻译成 ONNX 的计算图。
你可能会问:为什么不直接用 ONNX 官方工具?原因很简单——TF 的算子太“野”了。很多自定义算子、控制流、动态 shape,ONNX 原生不认。tf2onnx 相当于一个翻译官,把这些 TF 特有的东西映射到 ONNX 标准算子上去。
核心原理一句话: tf2onnx 读取 TF 的 GraphDef 或 SavedModel,逐节点匹配算子映射表,输出 ONNX 模型。
我个人习惯用 tf2onnx 的 Python API 来做转换,而不是命令行。为什么?因为可以在代码里做预处理、后处理,调试起来方便。当然,命令行也有它的好处——快速验证。
4.2 环境准备:版本匹配是头等大事
嗯,这里我要重点强调一下。TF 和 ONNX 的版本匹配,是转换成功的第一道门槛。我在项目中遇到过好几次,明明代码没问题,就是转不过去,最后发现是版本不兼容。
下面这个表格是我实测过的稳定组合,你直接照着来就行:
| TensorFlow 版本 | tf2onnx 版本 | ONNX 版本 | 备注 |
|---|---|---|---|
| 2.4.x | 1.9.x | 1.9.x | 稳定组合 |
| 2.5.x - 2.8.x | 1.10.x - 1.12.x | 1.10.x - 1.12.x | 推荐 |
| 2.9.x - 2.12.x | 1.13.x - 1.15.x | 1.13.x - 1.15.x | 最新稳定 |
避坑指南: 我曾经因为 TF 2.6 配了 tf2onnx 1.8,结果转换时卡在 FusedBatchNorm 算子上,报错信息完全看不懂。后来发现是 tf2onnx 版本太老,不支持 TF 2.6 新增的算子变体。升级到 1.10 就好了。
安装命令很简单:
pip install tf2onnx onnx onnxruntime
如果你用的是 TF 2.x,tf2onnx 会自动检测并适配。但建议你装完后跑一下版本检查:
python -c "import tf2onnx; print(tf2onnx.__version__)"
4.3 方法一:Keras H5 模型转换
这是最常见的情况。你用 Keras 的 Sequential 或 Functional API 训练好模型,保存成 .h5 文件。现在要转成 ONNX。
流程其实就三步:
- 加载 Keras 模型:用 tf.keras.models.load_model()
- 指定输入签名:告诉 tf2onnx 输入张量的名字、形状和数据类型
- 调用转换函数:tf2onnx.convert.from_keras()
看代码:
import tensorflow as tf
import tf2onnx
import onnx
# 1. 加载 Keras 模型
model = tf.keras.models.load_model('my_model.h5')
# 2. 定义输入签名
# 注意:batch_size 可以设为 None,表示动态 batch
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)
# 3. 转换
output_path = "my_model.onnx"
model_proto, _ = tf2onnx.convert.from_keras(
model,
input_signature=spec,
output_path=output_path
)
print(f"转换完成,模型保存至 {output_path}")
小技巧: input_signature 里的 name 参数很重要。这个 name 会成为 ONNX 模型的输入节点名。你在嵌入式端部署时,推理代码里要用这个名字来绑定输入数据。我一般习惯用 "input" 或 "data",简单明了。
你想想看,为什么这里要显式指定 TensorSpec?因为 Keras 模型本身不保存输入张量的名字和 dtype 信息。如果不指定,tf2onnx 会自己猜,但猜错的情况我遇到过好几次。所以,手动指定是最稳妥的。
4.4 方法二:SavedModel 转换
SavedModel 是 TF 2.x 官方推荐的保存格式。它比 .h5 更完整——包含了模型的计算图、权重、以及 serving 相关的签名信息。
转换 SavedModel 有两种方式:命令行和 Python API。
4.4.1 命令行方式(快速验证)
python -m tf2onnx.convert \
--saved-model ./saved_model_dir \
--output model.onnx \
--inputs input:0 \
--outputs output:0
这里 --inputs 和 --outputs 指定的是张量的名字。怎么知道名字?用 saved_model_cli 工具查看:
saved_model_cli show --dir ./saved_model_dir --all
输出里会显示 SignatureDef,里面就有输入输出的 tensor_name。
4.4.2 Python API 方式(推荐)
import tf2onnx
# 直接指定 SavedModel 目录
output_path = "model_from_saved_model.onnx"
model_proto, _ = tf2onnx.convert.from_saved_model(
saved_model_path="./saved_model_dir",
input_names=["input:0"],
output_names=["output:0"],
output_path=output_path
)
关键区别: SavedModel 转换不需要加载模型到内存,tf2onnx 直接读取磁盘上的 protobuf 文件。对于超大模型(比如 1GB 以上),这种方式更省内存。
我记得有一次,客户给了一个 2.3GB 的 SavedModel,用 Keras 加载直接 OOM 了。换成 from_saved_model 方式,内存占用不到 500MB,顺利转完。
4.5 方法三:Checkpoint + GraphDef 转换
这个方法现在用得少了,但如果你维护的是 TF 1.x 的老项目,可能会遇到。流程是:从 checkpoint 恢复图结构,冻结成 frozen graph,再转 ONNX。
import tensorflow as tf
from tensorflow.python.framework import graph_io
# 1. 恢复图
with tf.Session() as sess:
saver = tf.train.import_meta_graph('model.ckpt.meta')
saver.restore(sess, 'model.ckpt')
# 2. 冻结图
frozen_graph = tf.graph_util.convert_variables_to_constants(
sess,
sess.graph_def,
output_node_names=['output']
)
# 3. 保存 frozen graph
graph_io.write_graph(frozen_graph, '.', 'frozen_graph.pb', as_text=False)
# 4. 用 tf2onnx 转换 frozen graph
import tf2onnx
tf2onnx.convert.from_graph_def(
frozen_graph,
input_names=["input:0"],
output_names=["output:0"],
output_path="model_from_frozen.onnx"
)
注意: TF 1.x 的图里经常有控制流操作(如 tf.cond、tf.while_loop),这些在 ONNX 里没有直接对应。tf2onnx 会尝试展开,但有时候会失败。我曾经遇到一个循环展开后节点数暴增 10 倍,推理速度反而变慢了。这种情况建议在 TF 侧先重写模型,避免复杂控制流。
4.6 三种方法对比:什么时候用哪个?
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| from_keras | Keras .h5 模型 | API 简洁,代码量少 | 需要加载模型到内存 |
| from_saved_model | TF 2.x 标准导出格式 | 省内存,支持签名 | 需要知道 tensor name |
| from_graph_def | TF 1.x 老项目、frozen graph | 兼容旧版 | 流程繁琐,控制流难处理 |
我个人建议:新项目优先用 SavedModel。它是 TF 官方推荐格式,tf2onnx 对它的支持也最完善。Keras 转换适合快速原型验证。至于 frozen graph,除非你维护的是遗留系统,否则尽量别碰。
4.7 转换后的验证:别急着部署
模型转完了,别急着往嵌入式板子上烧。先做两件事:
- ONNX 模型结构检查:用 onnx.checker 验证模型是否合法
- 数值一致性对比:用同一份输入数据,对比 TF 和 ONNX 的输出差异
验证代码:
import onnx
import onnxruntime as ort
import numpy as np
import tensorflow as tf
# 1. 检查 ONNX 模型
onnx_model = onnx.load("my_model.onnx")
onnx.checker.check_model(onnx_model)
print("ONNX 模型结构检查通过")
# 2. 准备测试数据
test_input = np.random.randn(1, 224, 224, 3).astype(np.float32)
# 3. TF 推理
tf_model = tf.keras.models.load_model('my_model.h5')
tf_output = tf_model.predict(test_input)
# 4. ONNX 推理
ort_session = ort.InferenceSession("my_model.onnx")
ort_inputs = {ort_session.get_inputs()[0].name: test_input}
ort_output = ort_session.run(None, ort_inputs)[0]
# 5. 对比
diff = np.max(np.abs(tf_output - ort_output))
print(f"最大数值差异:{diff:.6f}")
if diff < 1e-4:
print("✅ 转换成功,数值一致")
else:
print("⚠️ 差异较大,请检查模型")
经验之谈: 我一般要求差异小于 1e-4。如果差异在 1e-3 量级,可能是算子精度问题,可以接受。但如果差异超过 0.1,那肯定有问题——要么是输入输出名字搞错了,要么是某个算子映射不对。
4.8 常见问题与避坑
- 算子不支持:tf2onnx 会报 "Unsupported Op"。解决办法是升级 tf2onnx 版本,或者用
--opset参数指定更高的 opset 版本。 - 动态 shape 问题:ONNX 默认支持动态 batch,但有些算子要求静态 shape。我建议在转换时把 batch 维度设为 None,其他维度固定。
- 控制流炸了:TF 的 tf.while_loop 转 ONNX 后可能变成一堆 If 节点,推理效率极低。能避免就避免。
- 输入输出名字:名字搞错是最常见的低级错误。转换前用
print(model.inputs)确认一下。
4.9 本章知识体系
下面这张图总结了 TF 模型导出 ONNX 的完整流程和三种路径:
从图上可以看得很清楚:不管你的 TF 模型是什么格式,最终都汇聚到 ONNX。关键就是选对转换方法,然后做好验证。
好了,第四章就到这里。你只要把 tf2onnx 的三种方法都跑一遍,以后遇到 TF 模型导出就不会慌了。记住:版本匹配、输入签名、数值验证,这三步做到位,转换成功率能到 95% 以上。