4、模型转换:将PyTorch/TensorFlow模型转换为ONNX格式

4.1 核心原理:为什么需要“导出”而非“重写”

在Java部署ONNX模型之前,首先要理解模型转换的本质。PyTorch和TensorFlow是训练框架,它们内部维护的是动态计算图(PyTorch的Autograd)或静态图(TensorFlow的GraphDef),这些结构服务于训练过程中的梯度回传与参数更新。而ONNX(Open Neural Network Exchange)是一种推理交换格式,它定义了一套与框架无关的计算算子(Opset)和静态图描述。转换过程并非“翻译代码”,而是将训练框架中的计算图追踪并映射为ONNX的算子序列,同时把权重参数序列化为ONNX的TensorProto格式。

这一过程的关键在于算子映射:PyTorch的nn.Conv2d对应ONNX的Conv算子,TensorFlow的tf.nn.batch_normalization对应ONNX的BatchNormalization算子。但并非所有算子都能一一对应,例如PyTorch中的torch.where在旧版ONNX中可能被拆解为多个基础算子。因此,转换工具的核心工作就是构建一张“框架算子→ONNX算子”的映射表,并在遇到无法直接映射的算子时,尝试用多个ONNX基础算子组合等价替换。

动态图与静态图的差异:PyTorch默认使用动态图(Define-by-Run),图结构在每次前向传播时动态构建。因此导出ONNX时,必须使用追踪(Tracing)或脚本化(Scripting)方式固定一次前向传播的路径。TensorFlow默认使用静态图(Define-and-Run),其SavedModel本身已是静态图,转换时只需做图结构翻译。
模型转换核心原理:追踪 → 映射 → 序列化 PyTorch 模型 nn.Module 动态图 权重: state_dict 算子: Conv, BatchNorm... torch.onnx.export() ONNX 计算图 GraphProto (节点列表) TensorProto (权重) ValueInfoProto (输入输出) Opset 版本控制 TensorFlow 模型 SavedModel / GraphDef 静态图 + Checkpoint 算子: Conv2D, FusedBatchNorm... tf2onnx.convert() 追踪 翻译 算子映射与等价替换示例 torch.nn.Conv2d → ONNX Conv torch.where(cond, a, b) → ONNX Where (Opset≥9) 或: Expand+Mul+Add 组合 映射失败怎么办? 1. 自定义算子注册 2. 分解为多个基础算子 3. 修改模型结构规避

4.2 方法流程:PyTorch导出与TensorFlow转换

4.2.1 PyTorch模型导出(torch.onnx.export)

PyTorch从1.2版本起提供了稳定的ONNX导出接口。核心函数是torch.onnx.export(),它通过追踪(Tracing)机制运行一次模型前向传播,记录实际执行过的Tensor操作,并将其转换为ONNX图。导出时需提供示例输入(dummy input),其形状和数据类型决定了ONNX图的输入签名。

import torch
import torchvision.models as models

# 1. 加载预训练模型并设为评估模式
model = models.resnet18(pretrained=True)
model.eval()

# 2. 构造示例输入(batch=1, 3通道, 224x224)
dummy_input = torch.randn(1, 3, 224, 224)

# 3. 导出ONNX
torch.onnx.export(
    model,                      # 要导出的模型
    dummy_input,                # 示例输入
    "resnet18.onnx",            # 输出文件路径
    export_params=True,         # 导出权重参数
    opset_version=12,           # ONNX算子集版本
    do_constant_folding=True,   # 常量折叠优化
    input_names=["input"],      # 输入节点名
    output_names=["output"],    # 输出节点名
    dynamic_axes={              # 动态轴定义
        "input": {0: "batch_size"},
        "output": {0: "batch_size"}
    }
)
print("导出完成: resnet18.onnx")

关键参数说明:opset_version决定了可用的ONNX算子集合,版本越高支持的新算子越多,但兼容性可能下降;dynamic_axes允许指定哪些维度是动态的(如batch大小可变),这在部署时非常有用,但会增加图优化的复杂度。

4.2.2 TensorFlow模型转换(tf2onnx)

TensorFlow模型转换通常使用tf2onnx工具。它支持将SavedModel、GraphDef或Keras的.h5文件转换为ONNX。转换过程需要指定输入输出的张量名称,这些名称可以在TensorFlow的signature中查询。

# 命令行方式(推荐)
python -m tf2onnx.convert \
    --saved-model ./saved_model_dir \
    --output model.onnx \
    --opset 13 \
    --inputs serving_default_input:0 \
    --outputs StatefulPartitionedCall:0

# Python API方式
import tf2onnx
import tensorflow as tf

model = tf.keras.models.load_model("mobilenet.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("mobilenet.onnx", "wb") as f:
    f.write(onnx_model[0].SerializeToString())
注意:TensorFlow的FusedBatchNorm、Pad等算子在ONNX中可能没有直接对应,tf2onnx会自动进行图重写。转换后务必使用onnx.checker验证图结构完整性,并使用onnxruntime进行数值比对测试。

4.3 案例:ResNet18图像分类模型转换与验证

下面以一个完整的ResNet18转换案例,展示从PyTorch到ONNX再到验证的完整流程。该案例模拟了实际部署中的标准操作。

步骤 操作 关键代码/命令 预期结果
1 加载预训练模型 models.resnet18(pretrained=True) 模型权重下载完成
2 导出ONNX torch.onnx.export(...) 生成resnet18.onnx文件
3 结构验证 onnx.checker.check_model() 无异常抛出
4 形状与算子查看 onnx.helper.printable_graph() 打印图结构,确认输入输出
5 数值一致性比对 PyTorch输出 vs ONNX Runtime输出 最大误差 < 1e-4
# 步骤3:结构验证
import onnx
model_onnx = onnx.load("resnet18.onnx")
onnx.checker.check_model(model_onnx)
print("ONNX模型结构验证通过")

# 步骤5:数值比对
import onnxruntime as ort
import numpy as np

# 创建ONNX Runtime推理会话
sess = ort.InferenceSession("resnet18.onnx")
input_name = sess.get_inputs()[0].name

# 准备相同的输入数据
test_input = np.random.randn(1, 3, 224, 224).astype(np.float32)

# PyTorch推理
with torch.no_grad():
    torch_output = model(torch.from_numpy(test_input)).numpy()

# ONNX Runtime推理
onnx_output = sess.run(None, {input_name: test_input})[0]

# 计算最大绝对误差
max_error = np.max(np.abs(torch_output - onnx_output))
print(f"最大数值误差: {max_error:.6f}")
assert max_error < 1e-4, "数值误差超出阈值!"

4.4 常见误区与排障复盘

误区一:忽略model.eval()模式。PyTorch模型在训练模式下包含Dropout和BatchNorm的随机行为,导出时必须切换到评估模式,否则ONNX图中会固化训练时的随机逻辑,导致推理结果不稳定。
误区二:动态控制流无法被追踪。如果模型包含if条件分支或for循环且循环次数依赖输入,Tracing只能记录一次执行路径。此时应使用torch.jit.script先转换为TorchScript,再导出ONNX,或者将动态部分重构为静态逻辑。

排障复盘:算子不支持错误。当遇到Unsupported operator: aten::unique之类的错误时,首先检查ONNX算子集版本是否过旧(升级opset_version),其次查看该算子是否有ONNX等价实现。若确实无法映射,可考虑在模型定义中替换该算子,例如用torch.unique的替代实现或自定义ONNX算子。

排障复盘:动态轴设置不当。如果部署时输入batch大小会变化,但导出时未设置dynamic_axes,ONNX Runtime会固定输入形状,导致推理报错。正确做法是在导出时明确声明动态维度,并在Java端使用OrtSession.InputOptions配置动态输入。

最佳实践:转换完成后,建议使用onnxsim(ONNX Simplifier)进行图优化,它可以合并冗余节点、消除常量计算,平均减少10%~30%的模型大小并提升推理速度。执行命令:python -m onnxsim resnet18.onnx resnet18_sim.onnx。