3、ONNX基础:ONNX格式详解、ONNX算子标准、PyTorch模型导出为ONNX

这一章,我们来啃ONNX这块硬骨头。

说实话,我刚开始接触ONNX时也觉得它挺玄乎的。一个中间格式,凭什么能打通那么多框架?后来踩了不少坑,才慢慢摸清楚它的脾气。今天我把这些经验整理出来,希望能帮你少走弯路。

3.1 ONNX格式到底长什么样?

ONNX的全称是Open Neural Network Exchange。说白了,它就是一套标准化的模型描述语言。不管你是用PyTorch、TensorFlow还是其他框架训练出来的模型,都能转换成ONNX格式。

那ONNX文件里到底装了啥?我直接给你拆开看。

一个ONNX模型的核心结构,其实就三大部分:

  • 计算图(Graph):描述网络的结构,包括节点、边、张量
  • 算子集(Opset):定义每个节点用什么操作,比如Conv、Relu、MatMul
  • 元数据(Metadata):模型名称、版本、作者、文档等信息

你可以把ONNX想象成一张蓝图。蓝图本身不干活,但它精确描述了每一块砖应该怎么放。TensorRT拿到这张蓝图后,就知道该怎么优化和执行。

核心要点:ONNX是中间表示(IR),不是运行时。它只负责描述,不负责执行。

下面这张图,是我画的一个ONNX模型内部结构示意,帮你建立直观印象:

ONNX 模型内部结构 ONNX 模型 计算图 Graph 算子集 Opset 元数据 Metadata 计算图内部 节点列表 (Node) 边/连接 (Edge) 张量信息 (Tensor) 算子集内部 算子版本号 算子定义列表 类型约束 元数据内部 模型名称/版本 生产者信息 文档/注释 ONNX 是中间表示(IR),不是运行时 只负责描述网络结构和参数,不负责执行 TensorRT 拿到 ONNX 后,解析并生成优化引擎

3.2 ONNX算子标准——你不得不知道的事

ONNX定义了一套标准算子,比如Conv、Relu、BatchNormalization、Gemm(全连接)等等。每个算子都有固定的输入输出格式和属性参数。

这里有个关键点:不同框架的算子,映射到ONNX时可能不完全一致。比如PyTorch的padding模式,和ONNX的padding模式就有细微差别。我在项目中就遇到过,一个Conv2d导出后,因为padding参数没对齐,推理结果差了0.01。

避坑提醒:我曾经花了一整天排查一个模型精度问题,最后发现是ONNX的Pad模式默认是"constant",而PyTorch默认是"reflect"。这种细节,你不踩一次坑真的记不住。

下面这个表格,是我整理的常用ONNX算子与PyTorch算子的对应关系:

PyTorch 算子 ONNX 算子 注意事项
torch.nn.Conv2d Conv padding模式需对齐,dilation>1时注意
torch.nn.ReLU Relu 基本一致,无坑
torch.nn.BatchNorm2d BatchNormalization training=False时导出,否则多出训练节点
torch.nn.Linear Gemm 转置权重时注意transA/transB参数
torch.nn.MaxPool2d MaxPool ceil_mode需手动指定
torch.nn.AdaptiveAvgPool2d GlobalAveragePool 输出尺寸为1x1时自动映射
torch.cat Concat axis参数从0开始,与PyTorch一致
torch.reshape Reshape 允许-1维度,但ONNX要求shape是常量

个人建议:导出前,先查一下你用的PyTorch算子是否在ONNX官方算子列表里。不在的话,要么换实现,要么写自定义算子。我一般会先跑一遍onnxruntime验证,确认没问题再往下走。

3.3 PyTorch模型导出为ONNX——torch.onnx.export实战

好,理论说完了,咱们直接上手。PyTorch导出ONNX的核心函数就一个:torch.onnx.export()。但参数不少,我一个个给你拆。

先看一个最基础的导出示例:

import torch
import torch.onnx

# 假设你有一个训练好的模型
model = torch.load('my_model.pth')
model.eval()  # 一定要切到eval模式

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

# 导出
torch.onnx.export(
    model,                    # 模型
    dummy_input,              # 示例输入
    'my_model.onnx',          # 输出路径
    export_params=True,       # 导出训练好的参数
    opset_version=11,         # 算子集版本
    do_constant_folding=True, # 常量折叠优化
    input_names=['input'],    # 输入节点名称
    output_names=['output'],  # 输出节点名称
    dynamic_axes={
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)
print("导出完成!")

这段代码看着简单,但每个参数都有讲究。我挑几个重点说说:

  • opset_version:我建议用11或更高。版本太低,有些新算子不支持;版本太高,老版本TensorRT可能不兼容。我个人习惯用opset=11,兼容性最好。
  • dynamic_axes:这个参数很关键。它告诉ONNX哪些维度是可变的。比如batch_size,你训练时是1,推理时可能是8或16。不设置的话,导出的模型就固定batch_size=1了。
  • do_constant_folding:建议开启。它会提前计算一些常量表达式,减小模型体积,提升推理速度。

重要提醒:导出前一定要调用 model.eval()。我见过太多人忘了这步,结果导出的模型里还带着Dropout和BatchNorm的训练状态,推理结果完全不对。

3.4 导出后的验证——别急着往下走

导出完ONNX文件,第一件事不是扔给TensorRT,而是先验证一下。

我一般做三步验证:

  1. 结构验证:用 onnx.checker.check_model() 检查模型结构是否合法
  2. 精度验证:用onnxruntime跑一遍,对比PyTorch的输出,误差应该在1e-5以内
  3. 可视化验证:用Netron打开ONNX文件,看看网络结构对不对

下面这段代码,是我常用的验证流程:

import onnx
import onnxruntime as ort
import numpy as np

# 1. 结构验证
onnx_model = onnx.load('my_model.onnx')
onnx.checker.check_model(onnx_model)
print("✅ 模型结构验证通过")

# 2. 精度验证
ort_session = ort.InferenceSession('my_model.onnx')
ort_inputs = {ort_session.get_inputs()[0].name: dummy_input.numpy()}
ort_outputs = ort_session.run(None, ort_inputs)

# 与PyTorch输出对比
with torch.no_grad():
    torch_output = model(dummy_input).numpy()

diff = np.abs(ort_outputs[0] - torch_output).max()
print(f"✅ 最大误差: {diff:.2e}")
if diff < 1e-5:
    print("✅ 精度验证通过")
else:
    print("⚠️ 精度偏差较大,请检查")

我曾经踩过的坑:有一次导出后精度验证一直不过,误差到了0.1。查了半天,发现是模型里有个自定义的F.interpolate,ONNX不支持它的align_corners参数。最后我换成了torch.nn.Upsample才解决。所以,遇到精度问题,先检查有没有不支持的算子或参数。

3.5 常见导出问题与解决方案

导出ONNX时,你大概率会遇到下面这些问题。我直接给你列出来,省得你再去搜:

问题现象 根本原因 解决方案
导出时报错"Unsupported operator" PyTorch算子不在ONNX算子集中 用torch.onnx.register_custom_op_symbolic注册,或换用ONNX支持的实现
导出成功但推理结果不对 模型未切到eval模式,或算子参数不匹配 检查model.eval(),对比每个算子的参数
动态batch导出失败 dynamic_axes设置错误,或模型中有固定shape的操作 检查所有reshape/view操作,确保batch维度是动态的
ONNX文件体积过大 包含了不必要的常量或调试信息 开启do_constant_folding,导出后可以用onnx-simplifier简化
TensorRT加载ONNX报错 算子版本不兼容,或使用了TensorRT不支持的算子 降低opset版本,或替换算子为TensorRT支持的版本

我的经验:如果你不确定某个算子是否支持,可以先导出一个小模型测试。我一般会先导出一个只有Conv+Relu的极简网络,确认流程通了,再逐步增加复杂度。这样定位问题快很多。