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
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参数,或者分步转换 |
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的进阶技巧,包括动态轴、自定义算子这些硬核内容。到时候见。