4、TensorFlow模型导出ONNX:使用tf2onnx工具进行模型转换

好,咱们接着聊。前面几章我们把PyTorch模型的导出讲透了,这一章轮到TensorFlow了。说实话,TF模型转ONNX,我个人的经验是——坑比PyTorch多那么一点点。但别担心,掌握了套路之后,其实也就那么回事。

4.1 为什么需要tf2onnx?

TensorFlow的生态很庞大,从1.x到2.x,从Keras到SavedModel,格式五花八门。你想想看,一个模型训练好了,想部署到移动端、Web端,或者换个框架做推理,总不能每次都重写一遍吧?

tf2onnx这个工具,说白了就是一座桥。它能把TF的各种模型格式,统一转换成ONNX标准格式。我在项目中遇到过最典型的情况:客户用TF2训练了一个目标检测模型,但他们的推理服务器只支持ONNX Runtime。没有tf2onnx,这活根本干不了。

4.2 环境准备与安装

嗯,这里要注意。tf2onnx对版本有要求,不是随便装个最新版就能跑的。我个人习惯用Python 3.8 + TensorFlow 2.6 ~ 2.10这个区间,兼容性最好。

# 安装tf2onnx
pip install tf2onnx

# 建议同时安装onnx和onnxruntime做验证
pip install onnx onnxruntime
版本警告:如果你用的是TensorFlow 2.15及以上版本,tf2onnx可能需要从源码安装。我曾经在这上面浪费了半天时间,最后发现是版本不匹配。

4.3 从Keras模型导出

这是最常见的场景。你训练好了一个Keras模型,保存成了.h5文件或者SavedModel格式。怎么转?看代码:

import tf2onnx
import tensorflow as tf

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

# 指定输入输出的名字(这个很重要)
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)

# 转换
onnx_model, _ = tf2onnx.convert.from_keras(model, 
                                           input_signature=spec,
                                           opset=13)

# 保存
with open("model.onnx", "wb") as f:
    f.write(onnx_model.SerializeToString())

这里有个细节:input_signature里的name参数,我建议你起个有意义的名字,比如"input"、"image"之类的。为什么?因为后续做推理时,这个name就是输入张量的标识符。我之前有个项目,随便起了个"Placeholder_1",结果同事接手时一脸懵。

4.4 从SavedModel导出

SavedModel是TF2推荐的标准格式。如果你有完整的SavedModel目录,转换起来更直接:

import tf2onnx

# 命令行方式(我最常用的)
!python -m tf2onnx.convert \
    --saved-model ./saved_model_dir \
    --output model.onnx \
    --opset 13

# 或者用Python API
import tf2onnx
import tensorflow as tf

model = tf.saved_model.load('./saved_model_dir')
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)
onnx_model, _ = tf2onnx.convert.from_saved_model(model, 
                                                  input_signature=spec,
                                                  opset=13)
小技巧:命令行方式有个好处,它会打印出模型的输入输出节点信息。我每次转换前都会先跑一遍,看看节点名字对不对,避免后面踩坑。

4.5 处理动态输入形状

实际部署时,batch size往往是动态的。你想想看,线上请求的图片数量每次都不一样,总不能固定死吧?

tf2onnx默认支持动态batch。你只需要在TensorSpec的第一个维度写None就行:

spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)

但要注意,有些算子对动态形状支持不好。比如TF里的tf.shape操作,转成ONNX后可能会变成静态的。我遇到过一个问题:模型里用了tf.image.resize,结果转出来的ONNX在batch size变化时直接报错。

解决方案?要么在模型里避免使用这类算子,要么在转换时指定--inputs-as-nchw参数强制转换格式。

4.6 常见问题与避坑指南

我整理了一个表格,都是我在实际项目中踩过的坑:

问题 原因 解决方案
转换后精度下降 某些TF算子没有对应的ONNX实现 使用--opset 13或更高版本,或者替换模型中的算子
ONNX Runtime推理报错 输入输出名字不匹配 onnx.load()查看节点名,确保和代码一致
动态batch不支持 模型中有静态形状操作 检查模型,替换tf.reshape等操作
转换速度极慢 模型太大或算子太复杂 尝试--large_model参数,或者分步转换
核心经验:我曾经有一个TF2的EfficientDet模型,转换后死活跑不通。折腾了两天,最后发现是模型里用了自定义的NMS算子。解决方案是先把NMS部分剥离,转成ONNX后再手动加上去。嗯,这种骚操作虽然不优雅,但管用。

4.7 验证转换结果

转换完了,别急着部署。先验证一下:

import onnx
import onnxruntime as ort
import numpy as np

# 加载ONNX模型
onnx_model = onnx.load("model.onnx")
onnx.checker.check_model(onnx_model)

# 用ONNX Runtime跑一下
session = ort.InferenceSession("model.onnx")
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

# 构造随机输入
dummy_input = np.random.randn(1, 224, 224, 3).astype(np.float32)
result = session.run([output_name], {input_name: dummy_input})

print("转换验证通过!输出形状:", result[0].shape)

这一步我建议你养成习惯。每次转换完,至少跑一次推理,确认输出形状和数值范围是合理的。别问我为什么强调这个——有一次我转换完直接上线,结果模型输出全是NaN,差点酿成生产事故。

4.8 小结

TensorFlow转ONNX,说白了就是三步:装工具、写spec、跑转换。但真正做好,需要对TF的算子集和ONNX的opset有了解。我的建议是:先从简单的Keras模型开始,跑通了再处理复杂的SavedModel。遇到问题别慌,查opset版本、查算子兼容性,大部分坑都能填上。

下一章,我们会聊PyTorch模型导出ONNX的进阶技巧,包括动态轴、自定义算子这些硬核内容。到时候见。