3、PyTorch模型导出ONNX:torch.onnx.export详解、动态轴与静态轴设置、输入输出命名规范、opset版本选择

好,咱们进入第三章。前面两章我们把环境搭好了,也跑通了YOLO的推理。但说实话,PyTorch模型在生产环境里直接跑,效率上不去,部署也麻烦。所以这一章,咱们得把PyTorch模型转成ONNX——一个中间格式,方便后续做TensorRT优化。

torch.onnx.export这个函数,我刚开始用的时候也踩过不少坑。说白了,它就是把PyTorch的计算图“拍”成ONNX的静态图。但怎么拍得对、拍得好,这里头门道不少。

3.1 torch.onnx.export核心参数解析

先看一个最基础的导出代码。我习惯把参数都写清楚,方便后面调试。

import torch
import torch.onnx

# 假设我们有一个训练好的YOLOv8模型
model = torch.load('yolov8n.pt', map_location='cpu')
model.eval()

# 创建一个虚拟输入,batch_size=1,3通道,640x640
dummy_input = torch.randn(1, 3, 640, 640)

# 导出ONNX
torch.onnx.export(
    model,                     # 要导出的模型
    dummy_input,               # 虚拟输入
    'yolov8n.onnx',            # 输出文件名
    export_params=True,        # 导出训练好的参数
    opset_version=11,          # ONNX算子集版本
    do_constant_folding=True,  # 常量折叠优化
    input_names=['input'],     # 输入张量名称
    output_names=['output'],   # 输出张量名称
    dynamic_axes={             # 动态轴设置
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)

这段代码看起来简单,但每个参数都有讲究。我一个个说。

参数 作用 我的建议
model 要导出的PyTorch模型实例 务必先调用model.eval(),关掉dropout和batch norm的training模式
dummy_input 一个形状正确的虚拟输入张量 用torch.randn生成,数值不重要,形状要对
export_params 是否导出模型权重 默认True,一般不动
opset_version ONNX算子集版本 YOLO推荐11或12,后面细说
do_constant_folding 是否做常量折叠优化 建议True,能减小模型体积
input_names 输入张量的名字 命名要规范,后面TensorRT要用
output_names 输出张量的名字 同上,建议用有意义的名称
dynamic_axes 哪些维度是动态的 YOLO推理时batch_size和宽高经常变
小技巧: 导出前先用torch.jit.trace或torch.jit.script试试,能提前发现一些图结构问题。我遇到过好几次模型里有动态控制流,直接导出ONNX会报错。

3.2 动态轴与静态轴设置

这是最容易出问题的地方。静态轴,就是导出时固定死的维度。动态轴,就是推理时可以变化的维度。

举个例子。YOLO模型输入是[batch, 3, height, width]。如果你导出时固定了batch=1,那推理时只能一次处理一张图。但实际场景里,你可能想一次处理4张、8张甚至16张。这时候就需要把batch维度设成动态的。

动态轴的配置方式是这样的:

dynamic_axes = {
    'input': {
        0: 'batch_size',    # 第0维是batch,名字叫batch_size
        2: 'height',        # 第2维是高度
        3: 'width'          # 第3维是宽度
    },
    'output': {
        0: 'batch_size'     # 输出也要对应动态
    }
}

这里有个坑。YOLO的输出通常包含多个head,比如检测头、分割头。每个输出的形状可能不一样。我建议只把batch维度设成动态,宽高尽量固定。为什么呢?

  • 宽高动态:会导致输出特征图的尺寸变化,后续的NMS处理逻辑要跟着变,容易出bug。
  • batch动态:对输出形状影响小,只是多几张图的数据堆在一起。
注意: 动态轴不是越多越好。每个动态轴都会让ONNX模型变得更复杂,推理引擎需要做额外的形状推导。我见过有人把宽高也设成动态,结果TensorRT优化时性能反而下降了。

那什么时候必须用动态宽高?比如你的应用需要处理不同分辨率的输入,像手机拍照的尺寸不固定。这时候可以设,但要做好性能下降的心理准备。

3.3 输入输出命名规范

命名这事,看着不起眼,但真能坑人。我刚开始做部署时,随便起了个名字叫input1、output1,结果后面TensorRT绑定数据时,半天找不到对应的张量。

我的命名习惯是这样的:

  • 输入:用input或images,简单明了。
  • 输出:用output或detections,如果是多输出,用output_0、output_1。
  • 中间张量:一般不需要命名,除非你要做中间层调试。

命名时注意几点:

  1. 不要用特殊字符:比如空格、斜杠、点号。有些推理引擎解析时会报错。
  2. 不要用中文:ONNX标准里不支持中文张量名。
  3. 保持一致性:输入输出名字要和后续代码里用的一致。

经验之谈: 我建议在项目里统一一个命名规范文档。比如输入都叫input,输出都叫output,中间层用layer_xxx。这样团队协作时,每个人都知道该绑哪个张量。

3.4 opset版本选择

opset,就是ONNX算子集的版本号。每个版本会新增或废弃一些算子。选错了,模型可能导出失败,或者推理时某些算子不支持。

YOLO模型常用的opset版本:

opset版本 特点 YOLO兼容性
10 基础算子,支持较少 YOLOv5早期版本可用,但容易报错
11 增加了ScatterND、GatherElements等 YOLOv5/v8推荐,兼容性好
12 增加了ReduceL1、ReduceL2等 YOLOv8可用,但部分算子可能不被TensorRT支持
13 增加了SoftmaxCrossEntropyLoss等 YOLOv8可用,但建议先测试TensorRT兼容性
14+ 新算子多,但兼容性风险大 不推荐,除非你确定推理引擎支持

我个人建议:YOLO模型导出ONNX时,opset选11或12。这两个版本在TensorRT上支持得最好。选太高了,TensorRT可能还没实现那些新算子,推理时会fallback到CPU,性能直接崩掉。

踩坑记录: 我曾经用opset=14导出一个YOLOv8模型,里面用到了ScatterND算子。结果TensorRT 8.4不支持,只能跑在CPU上,推理速度从30ms掉到300ms。后来改成opset=11,一切正常。

3.5 知识结构图:ONNX导出核心流程

下面这张图,把整个导出流程串起来了。你可以对照着看,每一步该做什么。

ONNX导出核心流程 ONNX导出核心流程 PyTorch模型 设置参数 opset_version / input_names / output_names 配置动态轴 dynamic_axes = { 'input': {0: 'batch'} } torch.onnx.export() ONNX模型 关键检查点 1. model.eval() 2. dummy_input形状正确 3. opset版本匹配 常见问题 • 动态轴过多 • 算子不支持 • 命名冲突

3.6 验证导出结果

导出完ONNX,别急着往下走。先验证一下模型对不对。我一般用ONNX Runtime跑一遍,看看输出和PyTorch原模型是否一致。

import onnx
import onnxruntime as ort
import numpy as np

# 加载ONNX模型
onnx_model = onnx.load('yolov8n.onnx')
onnx.checker.check_model(onnx_model)  # 检查模型结构

# 创建推理会话
ort_session = ort.InferenceSession('yolov8n.onnx')

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

# 运行推理
outputs = ort_session.run(None, {input_name: input_data})

# 对比PyTorch输出
with torch.no_grad():
    torch_outputs = model(torch.from_numpy(input_data))

# 检查数值差异
for i, (ort_out, torch_out) in enumerate(zip(outputs, torch_outputs)):
    diff = np.abs(ort_out - torch_out.numpy()).max()
    print(f'输出 {i} 最大差异: {diff:.6f}')
    if diff < 1e-3:
        print('✅ 验证通过')
    else:
        print('❌ 差异过大,请检查模型')

这里有个细节。ONNX Runtime和PyTorch的数值精度不完全一样,因为算子实现有差异。我一般容忍1e-3的误差。如果差异超过这个值,就要排查是不是导出时某些算子被替换了。

调试技巧: 如果验证不通过,可以用onnxruntime.transformers里的工具,把ONNX模型可视化,看看计算图有没有异常。我遇到过几次,模型里多了些奇怪的Identity节点,删掉就好了。

嗯,这一章的内容就这些。torch.onnx.export看着简单,但每个参数都值得仔细推敲。动态轴设好了,后面TensorRT优化事半功倍。命名规范了,团队协作不吵架。opset选对了,少踩一半的坑。