3、模型导出实战:从PyTorch导出ONNX模型(torch.onnx.export),常见参数详解

好,咱们直接进入正题。今天聊的是PyTorch模型转ONNX最核心的一步——torch.onnx.export。这个函数,说白了就是PyTorch和ONNX之间的桥梁。我刚开始接触ONNX时,觉得这玩意儿不就是一行代码的事吗?后来发现,参数调不好,导出的模型要么跑不起来,要么精度对不上。嗯,这里面的门道不少。

3.1 最基础的导出流程

先看一个最简单的例子。假设你有一个训练好的PyTorch模型,想导出成ONNX:

import torch
import torch.onnx

# 假设 model 是你的 PyTorch 模型
model = MyModel()
model.eval()  # 记得切到 eval 模式

# 构造一个 dummy input
dummy_input = torch.randn(1, 3, 224, 224)

# 导出
torch.onnx.export(
    model,               # 要导出的模型
    dummy_input,         # 模型的输入张量
    "model.onnx",        # 输出文件名
    input_names=['input'],
    output_names=['output']
)

这段代码跑完,你就能得到一个 model.onnx 文件。但说实话,这只是最基础的用法。实际项目中,你肯定需要调更多参数。

我的习惯:导出前一定先跑一次 model.eval()。为什么?因为 dropout 和 batch normalization 在训练和推理时的行为不一样。我见过有人忘了这步,结果导出的 ONNX 推理结果和 PyTorch 对不上,排查了半天才发现是这个问题。

3.2 核心参数详解

咱们一个一个来看。这些参数,我按重要程度排了个序。

3.2.1 opset_version

这个参数指定了 ONNX 算子集的版本。说白了,就是告诉导出器:你想用哪个版本的 ONNX 标准。

torch.onnx.export(
    model, dummy_input, "model.onnx",
    opset_version=11  # 常用版本
)

我个人建议用 opset_version=1113。为什么?

  • opset 11 支持了大多数常用算子,兼容性很好
  • opset 13 增加了对 Softmax 等算子的优化
  • opset 17 虽然新,但有些推理引擎还没完全支持
避坑指南:我曾经在项目中用了 opset 17,结果部署到某款边缘设备上时,发现它只支持到 opset 12。那叫一个尴尬。所以,导出前先确认目标平台的 ONNX 支持版本。

3.2.2 input_names 和 output_names

这两个参数给模型的输入输出张量起名字。别小看这个,后面做推理时,你要靠这些名字来获取数据。

torch.onnx.export(
    model, dummy_input, "model.onnx",
    input_names=['input'],      # 输入张量名字
    output_names=['output']     # 输出张量名字
)

你想想看,如果一个模型有多个输入(比如图片和文本),你给它们起名叫 image_inputtext_input,后面调试时一眼就能看出来哪个是哪个。

3.2.3 dynamic_axes

这个参数太重要了。它用来指定哪些维度是动态的。比如,你的模型输入 batch size 是可变的:

torch.onnx.export(
    model, dummy_input, "model.onnx",
    dynamic_axes={
        'input': {0: 'batch_size'},   # 第0维是动态的
        'output': {0: 'batch_size'}
    }
)

如果不设置 dynamic_axes,导出的 ONNX 模型会固定输入输出的形状。比如你传了 (1, 3, 224, 224),那模型就只接受这个形状。想换个 batch size?不行。

重点:动态轴虽然灵活,但会带来一些性能损失。因为推理引擎需要为动态形状做更多处理。如果业务场景中 batch size 是固定的,我建议就别开动态轴了。

3.2.4 export_params

这个参数控制是否导出模型权重。默认是 True。如果你只想导出模型结构,可以设为 False

torch.onnx.export(
    model, dummy_input, "model.onnx",
    export_params=False  # 只导出结构,不导出权重
)

嗯,这个场景比较少见。我一般只在做模型结构分析时才会用到。

3.2.5 do_constant_folding

常量折叠优化。说白了,就是把一些在推理时不会变化的计算提前算好,存成常量。

torch.onnx.export(
    model, dummy_input, "model.onnx",
    do_constant_folding=True  # 开启常量折叠
)

我建议默认开启。它能减小模型体积,还能提升推理速度。但要注意,如果模型中有一些看似常量但实际上会变化的操作(比如某些自定义层),开启后可能导致结果错误。

3.2.6 verbose

调试利器。设为 True 后,导出过程中会打印详细的算子转换信息:

torch.onnx.export(
    model, dummy_input, "model.onnx",
    verbose=True  # 打印详细信息
)

当导出失败时,这个参数能帮你快速定位是哪个算子出了问题。

3.3 一个完整的导出示例

好了,咱们把上面这些参数串起来,写一个实际项目中常用的导出代码:

import torch
import torch.onnx

def export_to_onnx(model, dummy_input, save_path):
    model.eval()
    
    torch.onnx.export(
        model,
        dummy_input,
        save_path,
        opset_version=11,
        input_names=['input'],
        output_names=['output'],
        dynamic_axes={
            'input': {0: 'batch_size'},
            'output': {0: 'batch_size'}
        },
        do_constant_folding=True,
        export_params=True,
        verbose=False
    )
    
    print(f"模型已导出至: {save_path}")

# 使用示例
model = MyModel()
dummy = torch.randn(1, 3, 224, 224)
export_to_onnx(model, dummy, "my_model.onnx")

3.4 导出后的验证

导出完别急着走。我建议做两件事:

  1. 用 ONNX Runtime 跑一遍推理,看结果和 PyTorch 是否一致
  2. 用 Netron 打开 ONNX 文件,可视化检查模型结构

验证代码大概长这样:

import onnxruntime as ort
import numpy as np

# 加载 ONNX 模型
session = ort.InferenceSession("my_model.onnx")

# 准备输入
input_name = session.get_inputs()[0].name
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)

# 推理
outputs = session.run(None, {input_name: input_data})
print("ONNX 推理结果:", outputs[0])
我的经验:验证时,最好用和训练时一样的数据预处理方式。比如归一化、resize 等。否则结果对不上,你都不知道是模型问题还是预处理问题。

3.5 常见问题与解决方案

问题 原因 解决方案
导出时报 "Unsupported operator" PyTorch 的某个算子 ONNX 不支持 升级 opset 版本,或自定义算子映射
ONNX 推理结果和 PyTorch 不一致 模型未切到 eval 模式,或动态轴设置错误 检查 model.eval(),确认 dynamic_axes 配置
导出的 ONNX 文件太大 模型权重未压缩,或常量折叠未开启 开启 do_constant_folding,或使用 ONNX 量化
推理时输入形状不匹配 未设置 dynamic_axes,或设置错误 检查 dynamic_axes 配置,确保维度索引正确

嗯,以上就是 torch.onnx.export 的核心内容。说白了,导出模型就像打包行李——参数调好了,一路顺畅;参数没调好,后面全是坑。我建议你每次导出后都做一遍验证,养成习惯。

下一章,咱们聊聊如何用 ONNX Runtime 做推理,以及怎么排查推理中的精度问题。