4、模型导出与转换:PyTorch导出ONNX、TensorFlow转ONNX、PaddlePaddle转ONNX
好,咱们进入正题。模型训练完了,要部署到端侧设备上,第一步就是把它导出成ONNX格式。这一步看似简单,但坑不少。我见过太多人卡在这一步,导出出来的模型要么精度不对,要么算子不支持,跑都跑不起来。
说白了,ONNX就是一个中间格式。它不依赖任何训练框架,是各大硬件厂商都支持的通用语言。你想想看,你的模型要在手机、嵌入式设备、甚至MCU上跑,总不能指望人家都装个PyTorch吧?
4.1 PyTorch导出ONNX
PyTorch导出ONNX,我个人习惯用 torch.onnx.export。这个接口很成熟,但有几个关键参数必须搞明白。
核心参数:
model:你的模型,记得要切换到eval()模式args:一个示例输入,形状要和实际推理时一致input_names/output_names:给输入输出起个名字,后面推理要用dynamic_axes:动态轴,比如batch size不固定时用opset_version:算子集版本,端侧一般用13或14比较稳
来看一个实际例子。我最近在做一个图像分类项目,模型是ResNet50:
import torch
import torchvision.models as models
model = models.resnet50(pretrained=True)
model.eval()
# 创建一个示例输入,batch size为1
dummy_input = torch.randn(1, 3, 224, 224)
# 导出ONNX
torch.onnx.export(
model,
dummy_input,
"resnet50.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size"},
"output": {0: "batch_size"}
},
opset_version=14,
do_constant_folding=True
)
print("导出成功!")
小技巧:导出后一定要用 onnxruntime 验证一下精度。我习惯写个脚本,对比PyTorch和ONNX的输出,误差在1e-5以内才算通过。
嗯,这里要注意。如果你的模型里有自定义算子,比如自己写的某个层,PyTorch默认是导不出去的。这时候需要注册一个符号函数,告诉ONNX怎么表示这个操作。我曾经在一个检测模型里遇到过,折腾了大半天才搞定。
4.2 TensorFlow转ONNX
TensorFlow转ONNX,主流工具是 tf2onnx。这个工具支持SavedModel、Keras H5、Checkpoint等多种格式。
我个人建议,先把TensorFlow模型保存成SavedModel格式,再用tf2onnx转换。为什么?因为SavedModel包含了完整的计算图和权重,转换最稳定。
# 安装工具
pip install tf2onnx
# 命令行转换
python -m tf2onnx.convert \
--saved-model ./saved_model \
--output model.onnx \
--opset 14 \
--inputs "input:0" \
--outputs "output:0"
如果你用的是Keras,也可以直接在Python里转:
import tf2onnx
import tensorflow as tf
model = tf.keras.models.load_model("my_model.h5")
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)
output_path = "model.onnx"
model_proto, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=14)
with open(output_path, "wb") as f:
f.write(model_proto.SerializeToString())
避坑指南:我曾经遇到过一个坑——TensorFlow的NHWC格式和ONNX的NCHW格式不兼容。很多端侧推理引擎默认用NCHW,如果你的模型是NHWC,推理速度会慢很多。建议在转换前就调整好数据格式,或者在转换时指定通道顺序。
还有一个常见问题:TensorFlow的某些操作,比如 tf.image.resize,在ONNX里没有直接对应的算子。tf2onnx会尝试用组合算子替代,但有时候会失败。我的经验是,尽量在模型里用标准卷积、池化、全连接这些基础操作,少用图像预处理相关的自定义操作。
4.3 PaddlePaddle转ONNX
PaddlePaddle转ONNX,官方提供了 paddle2onnx 工具。这个工具发展得很快,现在基本覆盖了主流模型。
先安装:
pip install paddle2onnx onnxruntime
然后转换:
paddle2onnx \
--model_dir ./inference_model \
--model_filename model.pdmodel \
--params_filename model.pdiparams \
--save_file model.onnx \
--opset_version 14 \
--enable_onnx_checker True
如果你用的是动态图模型,也可以这样:
import paddle
import paddle2onnx
# 加载动态图模型
model = paddle.jit.load("my_model")
model.eval()
# 导出ONNX
paddle2onnx.export(
model,
input_spec=[paddle.static.InputSpec(shape=[None, 3, 224, 224], dtype="float32")],
path="model.onnx",
opset_version=14
)
关键点:PaddlePaddle的模型导出,一定要确保输入输出的shape和dtype正确。我见过有人把int64当float32传,结果推理结果全错。建议导出后用 onnxruntime 跑一遍,对比原始PaddlePaddle的输出。
说实话,PaddlePaddle转ONNX的生态不如PyTorch成熟。有些模型转换后,算子数量会膨胀很多。比如一个简单的 paddle.nn.Conv2d,可能会被拆成多个ONNX算子。这时候就需要用 onnx-simplifier 做一次简化。
pip install onnx-simplifier
python -m onnxsim model.onnx model_simplified.onnx
4.4 转换后的验证与调试
不管从哪个框架转过来,最后一步都是验证。我总结了一个三步验证法:
- 结构验证:用
onnx.checker.check_model检查模型结构是否合法 - 精度验证:用同一份输入数据,对比原始框架和ONNX的输出
- 性能验证:在目标设备上用ONNX Runtime跑一遍,看推理时间和内存占用
import onnx
import onnxruntime as ort
import numpy as np
# 1. 结构验证
onnx_model = onnx.load("model.onnx")
onnx.checker.check_model(onnx_model)
# 2. 精度验证
ort_session = ort.InferenceSession("model.onnx")
input_name = ort_session.get_inputs()[0].name
output_name = ort_session.get_outputs()[0].name
test_input = np.random.randn(1, 3, 224, 224).astype(np.float32)
ort_output = ort_session.run([output_name], {input_name: test_input})
# 对比原始框架输出(这里以PyTorch为例)
# torch_output = model(torch.from_numpy(test_input))
# np.testing.assert_allclose(ort_output[0], torch_output.numpy(), rtol=1e-4, atol=1e-5)
个人经验:精度验证时,别只看最大值误差。我建议计算每个输出通道的余弦相似度,如果低于0.99,说明模型转换有问题。另外,记得用多组输入测试,别只测一组。
好了,模型导出这块就讲这么多。下一章咱们聊聊ONNX模型的优化,包括算子融合、量化、剪枝这些实战技巧。到时候我会分享一些在手机端和嵌入式设备上的调优经验,保证干货满满。