4、模型转换基础:从PyTorch导出ONNX(torch.onnx.export)、从TensorFlow/Keras转换(tf2onnx)、从其他框架转换
模型转换是ONNX Runtime部署链路中的第一道关卡,也是决定后续推理性能与兼容性的关键环节。本章聚焦于三种主流转换路径:PyTorch的torch.onnx.export、TensorFlow/Keras的tf2onnx工具链,以及涵盖PaddlePaddle、MindSpore、scikit-learn等框架的通用转换策略。我们将从转换原理、操作流程、实战案例与常见误区四个维度展开,帮助读者建立一套可复用的转换方法论。
4.1 核心原理:ONNX作为中间表示的转换本质
ONNX(Open Neural Network Exchange)本质上是一种计算图中间表示(IR),它定义了标准化的算子集合、张量数据类型和图结构。模型转换并非简单的文件格式变更,而是将源框架的计算图逐算子映射到ONNX算子集(Opset)的过程。每个框架都有自己独特的图表示方式:PyTorch使用动态图(Eager模式)的追踪(Tracing)或脚本(Scripting)机制;TensorFlow使用静态图(GraphDef);PaddlePaddle使用ProgramDesc。转换工具的核心任务就是解析这些异构表示,提取出计算逻辑,再按照ONNX的规范重新构建图结构。
理解转换原理需要把握三个关键点:算子映射表决定了源算子能否找到对应的ONNX算子;Opset版本决定了可用算子的集合与行为语义;张量形状与数据类型的推断直接影响后续优化器的执行。转换过程中,如果遇到不支持的算子,工具通常会抛出错误或插入自定义算子(Custom Op),后者需要额外的实现才能被ONNX Runtime加载。
nn.Sequential或TensorFlow的Keras层)在转换时会被展开为底层算子序列。因此,转换后的ONNX模型可能比原始模型更“庞大”,但计算逻辑完全等价。
4.2 PyTorch导出ONNX:torch.onnx.export深度实践
torch.onnx.export是PyTorch官方提供的导出接口,其底层基于TorchScript的追踪(Tracing)机制。当调用该函数时,PyTorch会使用给定的示例输入实际执行一次模型前向传播,记录所有被调用的张量操作,并将这些操作序列化为ONNX图。这意味着控制流(如if-else、循环)只有在被追踪路径实际执行时才会被记录,这是Tracing模式最大的局限性。
4.2.1 操作流程与参数详解
import torch
import torch.onnx
# 定义模型
class MyModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv = torch.nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.relu = torch.nn.ReLU()
self.fc = torch.nn.Linear(16 * 8 * 8, 10)
def forward(self, x):
x = self.conv(x)
x = self.relu(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
model = MyModel()
model.eval() # 必须切换到eval模式
# 构造示例输入
dummy_input = torch.randn(1, 3, 8, 8)
# 导出ONNX
torch.onnx.export(
model, # 模型
dummy_input, # 示例输入
"my_model.onnx", # 输出路径
export_params=True, # 导出权重
opset_version=13, # Opset版本
do_constant_folding=True, # 常量折叠优化
input_names=["input"], # 输入名称
output_names=["output"], # 输出名称
dynamic_axes={ # 动态轴定义
"input": {0: "batch_size"},
"output": {0: "batch_size"}
}
)
上述代码展示了导出流程的核心要素。其中opset_version的选择至关重要:较新的Opset支持更多算子且语义更精确,但可能不被旧版ONNX Runtime支持。建议根据部署环境的ONNX Runtime版本选择兼容的Opset。dynamic_axes参数允许指定哪些维度是动态的,例如批次大小可变,这在处理变长输入时非常有用。
| 参数名 | 作用 | 推荐设置 |
|---|---|---|
| opset_version | 指定ONNX算子集版本 | 11-15(根据Runtime版本) |
| do_constant_folding | 在导出时执行常量折叠优化 | True(默认) |
| dynamic_axes | 定义动态维度 | 按需设置批次维度 |
| input_names/output_names | 指定输入输出张量名称 | 建议使用语义化名称 |
4.2.2 案例拆解:带条件分支的模型导出
假设我们有一个包含if条件分支的模型,该分支根据输入张量的均值决定是否应用额外的全连接层。使用Tracing方式导出时,只有if条件为True的分支会被记录,导致导出的模型在推理时永远走同一个分支。解决方案有两种:一是使用torch.jit.script将模型转换为ScriptModule后再导出;二是将条件逻辑改为张量运算(如使用torch.where)。
# 问题代码
class ConditionalModel(torch.nn.Module):
def forward(self, x):
if x.mean() > 0:
x = self.extra_fc(x)
return x
# 解决方案1:使用torch.jit.script
scripted_model = torch.jit.script(ConditionalModel())
torch.onnx.export(scripted_model, dummy_input, "conditional.onnx", opset_version=13)
# 解决方案2:改用张量运算
class SafeModel(torch.nn.Module):
def forward(self, x):
mask = (x.mean() > 0).float()
x = x + mask * self.extra_fc(x)
return x
这个案例揭示了Tracing机制的本质限制:它记录的是“数据流”而非“控制流”。在实际项目中,如果模型包含复杂的动态控制流,建议优先考虑Scripting方式或重构模型逻辑。
model.eval()会导致BatchNorm和Dropout层的行为不一致。BatchNorm在训练模式下使用批统计量,而推理模式使用运行均值/方差;Dropout在训练时随机失活,推理时保持恒等。务必在导出前切换到eval模式。
4.3 TensorFlow/Keras转换:tf2onnx工具链
TensorFlow生态的转换主要依赖tf2onnx工具,它支持将三种格式的模型转换为ONNX:SavedModel、Keras H5、以及Checkpoint。与PyTorch的Tracing不同,tf2onnx直接解析TensorFlow的GraphDef,因此能够完整保留控制流结构。转换过程分为两步:首先将TensorFlow图转换为ONNX图,然后执行图优化(如算子融合、常量折叠)。
4.3.1 转换流程与命令行操作
# 方式1:命令行转换
python -m tf2onnx.convert \
--saved-model ./saved_model_dir \
--output model.onnx \
--opset 13 \
--inputs input:0 \
--outputs output:0
# 方式2:Python API转换
import tf2onnx
import tensorflow as tf
# 加载Keras模型
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(keras_model, input_signature=spec, opset=13)
# 保存ONNX模型
with open("model.onnx", "wb") as f:
f.write(onnx_model.SerializeToString())
在转换Keras模型时,input_signature参数非常重要,它定义了ONNX模型的输入张量名称、形状和数据类型。如果不指定,tf2onnx会尝试从Keras模型的输入层推断,但可能无法正确识别动态维度。
4.3.2 实战场景:处理TensorFlow自定义层
当Keras模型包含自定义层(继承tf.keras.layers.Layer)时,tf2onnx可能无法自动映射这些层。此时需要手动注册算子映射或使用tf2onnx的select_opset功能。一个典型的处理流程是:先尝试直接转换,如果报错则检查日志中未映射的算子,然后通过tf2onnx.rewriter模块编写自定义重写规则。
# 自定义层示例
class MyLayer(tf.keras.layers.Layer):
def call(self, inputs):
return tf.math.asin(inputs) # 反正弦函数
# 转换时可能报错:Unsupported op: Asin
# 解决方案:检查ONNX Opset是否支持Asin(Opset 7+支持)
# 如果仍不支持,可以手动替换为等价运算
# 或者使用tf2onnx的custom_op机制
在实际项目中,遇到自定义层时建议优先检查ONNX算子集是否包含对应算子。大多数常用数学函数在Opset 11之后都有对应实现。如果确实缺失,可以考虑在转换前将自定义层替换为原生TensorFlow算子组合。
onnxruntime的onnxruntime.transformers.optimizer进行进一步优化。该优化器可以自动融合Attention、LayerNorm等Transformer结构,显著提升推理性能。
4.4 其他框架转换:PaddlePaddle、MindSpore与经典机器学习
除了PyTorch和TensorFlow,ONNX生态还覆盖了PaddlePaddle、MindSpore、MXNet等深度学习框架,以及scikit-learn等传统机器学习库。这些转换工具大多遵循“框架导出中间格式→ONNX转换器”的两阶段设计。
4.4.1 PaddlePaddle与MindSpore转换
# PaddlePaddle → ONNX
import paddle
import paddle2onnx
# 加载Paddle模型
paddle_model = paddle.jit.load("inference_model/model")
paddle_model.eval()
# 转换
onnx_model = paddle2onnx.run_convert(
model=paddle_model,
input_spec=[paddle.static.InputSpec([None, 3, 224, 224], "float32", "input")],
opset_version=13
)
# MindSpore → ONNX
import mindspore as ms
from mindspore import export, Tensor
# 定义模型
class Net(ms.nn.Cell):
def construct(self, x):
return ms.ops.relu(x)
net = Net()
input_tensor = Tensor(np.ones([1, 3, 224, 224]), ms.float32)
export(net, input_tensor, file_name="model", file_format="ONNX")
Paddle2ONNX和MindSpore的导出工具都支持动态形状和Opset选择。需要注意的是,PaddlePaddle的paddle.jit.save保存的推理模型包含输入输出信息,转换时可以直接复用;而MindSpore的export接口需要显式传入示例输入。
4.4.2 scikit-learn模型转换
对于传统机器学习模型,sklearn-onnx库提供了转换支持。它可以将训练好的Pipeline(如预处理+分类器)转换为ONNX格式,从而统一部署到ONNX Runtime中。
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from sklearn.ensemble import RandomForestClassifier
# 训练模型
clf = RandomForestClassifier(n_estimators=100)
clf.fit(X_train, y_train)
# 定义输入类型
initial_type = [("input", FloatTensorType([None, X_train.shape[1]]))]
# 转换
onx = convert_sklearn(clf, initial_types=initial_type, target_opset=13)
# 保存
with open("rf_model.onnx", "wb") as f:
f.write(onx.SerializeToString())
sklearn-onnx支持大多数主流模型(随机森林、XGBoost、逻辑回归等),但需要注意树模型在转换时会将树结构展开为ONNX的TreeEnsemble算子,这可能导致模型文件变大,但推理效率更高。
4.5 常见误区与总结复盘
在模型转换实践中,以下误区最为常见:
- 忽略Opset版本兼容性:使用过新的Opset导出,导致旧版ONNX Runtime无法加载。建议在导出前确认目标Runtime版本,并选择对应的Opset。
- 动态形状处理不当:将动态维度固定为静态值,导致推理时遇到不同形状输入报错。应使用
dynamic_axes或input_signature明确声明动态维度。 - 忽略预处理/后处理节点:ONNX模型只包含网络计算图,不包含图像解码、归一化等预处理逻辑。这些操作需要在部署代码中单独实现。
- 未验证转换后精度:转换后模型的数值输出可能与原始框架存在微小差异(通常<1e-5),但在某些敏感场景(如检测框回归)可能被放大。建议使用
onnxruntime与原始框架进行输出对比。
- 使用
onnx.checker.check_model验证模型结构合法性。 - 使用
onnxruntime加载模型并执行推理,与原始框架输出对比。 - 检查模型文件大小是否合理(过大可能包含冗余节点)。
- 使用
onnx.shape_inference推断张量形状,确认动态维度正确。 - 在目标硬件(CPU/GPU/NPU)上测试推理延迟和内存占用。