4、TensorFlow转ONNX:tf2onnx工具使用与Keras模型转换陷阱

说实话,TensorFlow转ONNX是我日常工作中打交道最多的场景之一。毕竟现在用Keras搭模型的人太多了,但部署时又离不开ONNX的生态。今天我就把tf2onnx这个工具的使用心得,以及那些容易踩的坑,一次性说清楚。

4.1 tf2onnx的基本用法

tf2onnx这个工具,说白了就是一个桥接器。它能把TensorFlow的图结构翻译成ONNX的图结构。我个人习惯用命令行方式,简单直接。

# 最简单的用法
python -m tf2onnx.convert \
    --saved-model ./saved_model \
    --output model.onnx \
    --opset 13

这里有个细节——opset版本。我建议至少用13以上。为什么?因为低版本opset对很多新算子支持不好,转换完跑起来报错,你都不知道问题出在哪。

如果你用的是Keras的.h5模型,命令稍微变一下:

python -m tf2onnx.convert \
    --keras_model ./model.h5 \
    --output model.onnx \
    --opset 13

嗯,这里要注意:tf2onnx对Keras的Sequential模型支持得最好。Functional API的模型也还行,但Subclassing模型...我建议你绕道走。

4.2 那些年我踩过的Keras转换坑

我在项目中遇到过好几次,明明Keras模型跑得好好的,转成ONNX就各种报错。总结下来,主要有这么几个坑:

坑一:自定义层

这是最头疼的问题。你写了个自定义层,里面用了tf.custom_gradient或者自定义的梯度计算。tf2onnx根本认不出来。

警告: 自定义层是转换失败的头号杀手。如果你非要用,建议先把它改写成标准Keras层的组合。

我曾经接手过一个项目,里面有个自定义的Attention层,用了很多tf.einsum操作。转ONNX时直接报"Unsupported op"。最后我花了整整两天,把那个层拆成了Dense、Reshape和MatMul的组合,才搞定。

坑二:动态输入形状

Keras模型默认支持动态batch size,但ONNX对动态形状的支持有限。你想想看,如果你的模型输入是(None, 224, 224, 3),转成ONNX后,有些推理引擎会直接报错。

技巧: 转换时指定输入形状,固定batch size为1。或者用--inputs-as-nchw参数来明确维度顺序。
python -m tf2onnx.convert \
    --keras_model ./model.h5 \
    --output model.onnx \
    --inputs input:0[1,224,224,3] \
    --opset 13

坑三:训练与推理模式不一致

这个坑我印象特别深。有一次我转一个带Dropout和BatchNormalization的模型,转完发现推理结果完全不对。查了半天才发现,Keras模型默认是training=True模式,而ONNX推理需要training=False。

核心要点: 转换前一定要确保模型处于推理模式。用model.trainable = False或者直接加载训练好的权重文件。

4.3 常见错误及解决方案

我把这些年遇到的典型错误整理成了表格,方便你对照排查:

错误信息 原因 解决方案
Unsupported op: FusedBatchNormV3 TensorFlow版本与ONNX算子不匹配 升级tf2onnx到最新版,或指定--opset 13+
Shape inference failed 动态形状导致形状推导失败 固定输入形状,或使用--inputs-as-nchw
Keras model not found 模型路径或格式不对 确认是.h5文件,且Keras版本匹配
ONNX runtime error: Invalid argument 输入张量的数据类型不匹配 检查输入dtype,统一为float32

4.4 我的转换工作流

经过无数次踩坑后,我总结了一套相对稳妥的转换流程:

  1. 先验证Keras模型——确保在原始框架下推理结果正确
  2. 固定输入形状——用model.build()明确输入维度
  3. 设置推理模式——model.trainable = False
  4. 转换并验证——用onnxruntime跑一遍,对比输出差异
  5. 精度对比——差异超过1e-5就要警惕了

我的小习惯: 每次转换完,我都会写个简单的对比脚本,随机生成10组输入数据,分别用Keras和ONNX跑,看输出的最大绝对误差。如果超过1e-4,我就要回头检查了。

4.5 避坑指南

最后,分享几个我血泪换来的经验:

  • 版本匹配很重要——tf2onnx 1.9+ 配合 TensorFlow 2.x 比较稳
  • 别用tf.function装饰器——它会让图结构变得复杂,转换容易出问题
  • Lambda层要小心——里面如果用了Python原生操作,ONNX根本认不了
  • 多模态模型先拆分——文本+图像的模型,建议分别转换再合并

我曾经有个项目,模型里用了tf.keras.layers.Lambda(lambda x: tf.clip_by_value(x, 0, 1))。转ONNX时直接报错。后来改成用ReLU+Clip的组合才解决。你想想看,这种小细节不注意,排查起来真的很费时间。

好了,关于TensorFlow转ONNX的内容就这些。记住一句话:转换前多花10分钟检查模型结构,能省下转换后10小时的排查时间。这是我在无数个加班夜里悟出来的道理。