2、ONNX基础回顾:ONNX格式、计算图、节点与张量、模型转换的基本流程与工具链
在深入多输入模型转换之前,必须先夯实ONNX(Open Neural Network Exchange)的核心知识体系。ONNX不仅是一种文件格式,更是一种描述机器学习模型计算逻辑的标准化中间表示(Intermediate Representation, IR)。它像一座桥梁,连接着PyTorch、TensorFlow等训练框架与ONNX Runtime、TensorRT、OpenVINO等推理引擎。对于传感器融合或多模态模型而言,理解ONNX的计算图结构、张量流动方式以及转换工具链的底层逻辑,是确保多路输入数据在图中正确路由、拼接和同步的前提。本节将系统梳理ONNX格式的构成要素、计算图的核心概念、标准转换流程,并结合一个多模态输入场景的案例,剖析常见误区。
2.1 ONNX格式的本质:一种可交换的中间表示
ONNX(Open Neural Network Exchange)最初由微软和Facebook于2017年联合推出,其核心目标是为深度学习模型提供一种框架无关的标准化表示。它既不是训练框架,也不是推理引擎,而是一种“通用语言”——训练好的模型被“翻译”成这种语言后,任何支持该语言的推理引擎都能“读懂”并执行它。这种设计解决了深度学习生态中长期存在的“模型孤岛”问题:PyTorch训练的模型无法直接在TensorRT中部署,TensorFlow的模型难以被CoreML调用。
从文件结构来看,一个标准的.onnx文件本质上是使用Protobuf(Protocol Buffers)序列化后的二进制文件。Protobuf是一种高效的结构化数据序列化机制,它定义了清晰的数据模式(Schema),使得不同语言(C++、Python、Java等)都能解析同一份ONNX文件。ONNX格式的核心Schema包含以下关键部分:
| Schema组件 | 作用 | 多输入模型中的意义 |
|---|---|---|
ModelProto |
ONNX模型的顶层容器,包含图结构、元数据、算子集版本 | 定义整个多模态模型的入口和出口 |
GraphProto |
描述计算图:节点列表、初始化器、输入/输出张量 | 多路输入在此声明,决定推理时的数据入口 |
NodeProto |
单个算子实例,包含算子类型、输入/输出张量名、属性 | 多模态融合算子(如Concat、Add)在此定义 |
TensorProto |
存储权重、偏置等初始化数据 | 各模态分支的独立权重存储 |
ValueInfoProto |
描述张量的数据类型、形状、名称 | 明确每个输入张量的维度,如[batch, channel, height, width] |
OperatorSetIdProto |
声明使用的算子集版本(Opset Version) | 不同版本对多输入算子的支持程度不同 |
理解ONNX的Protobuf结构对于排查转换问题至关重要。例如,当转换后的模型在推理时提示“输入名称不匹配”,往往是因为GraphProto中的input字段与推理代码中提供的张量名称不一致。在多输入场景下,这种名称匹配问题尤为常见,因为每个输入张量都需要在ValueInfoProto中显式声明。
GraphProto中的input列表定义了模型接受的所有外部输入,每个输入必须包含唯一的name、明确的elem_type(数据类型)和shape(形状)。推理引擎正是依据这些信息来分配内存和调度计算。
2.2 计算图:节点、张量与数据流
ONNX的计算图(Graph)是一种有向无环图(DAG),它描述了张量(Tensor)如何在算子(Operator/Node)之间流动。图中的每个节点代表一个数学操作(如卷积、矩阵乘法、拼接),每条边代表一个张量(即数据)。这种表示方式与具体的硬件无关,因此可以灵活映射到CPU、GPU、NPU等不同计算设备上。
在ONNX中,计算图的核心元素包括:
- 节点(Node):每个节点包含
op_type(算子类型,如Conv、Relu、Concat)、input(输入张量名列表)、output(输出张量名列表)以及attribute(算子属性,如卷积的kernel_shape、strides)。 - 张量(Tensor):数据的基本载体,具有名称、数据类型(float32、int64等)和形状。张量分为三类:图输入(由外部提供)、图输出(最终结果)、中间张量(节点间传递的数据)。
- 初始化器(Initializer):存储模型权重和偏置的张量,它们不依赖外部输入,在模型加载时直接写入内存。
- 数据流:张量从输入节点出发,沿着有向边依次经过各节点,最终到达输出节点。这种流动是“静态”的——图结构在转换时已完全确定,推理时只是按照既定路径执行计算。
对于多输入模型(如摄像头+激光雷达融合),计算图通常呈现“多分支-融合-输出”的拓扑结构。以经典的点云与图像融合网络为例:图像分支经过若干卷积层提取2D特征,点云分支经过PointNet或VoxelNet提取3D特征,两个分支的特征图在某个中间层通过Concat或Add算子融合,最后经过全连接层输出检测结果。在ONNX图中,这意味着存在两个独立的输入节点(image_input和lidar_input),它们各自连接一条子图路径,最终汇聚到融合节点。
shape是否与训练时一致。特别是动态维度(如batch维度设为None或symbolic),需要在转换时通过dynamic_axes参数显式声明,否则导出的模型会固定输入形状,导致推理时无法适应不同batch size。
2.3 模型转换的基本流程与工具链
模型转换的本质是将训练框架(PyTorch、TensorFlow等)中的动态图或静态图“翻译”为ONNX的静态计算图。这个过程涉及算子映射、张量形状推断、常量折叠等多个步骤。以PyTorch为例,标准转换流程如下:
- 准备模型:将PyTorch模型设置为
eval()模式,并确保所有参数已加载完毕。 - 构造示例输入:创建一个与训练时输入形状一致的张量(可以是随机数),用于“跟踪”模型的计算路径。
- 调用导出API:使用
torch.onnx.export(),传入模型、示例输入、输出文件路径、输入/输出名称、动态轴定义等参数。 - 验证ONNX模型:使用
onnx.checker.check_model()检查模型结构合法性,再用onnxruntime进行推理验证,对比输出与PyTorch原始输出的误差。 - 优化与部署:根据目标推理引擎(如TensorRT)进行图优化或精度校准。
对于TensorFlow模型,常用工具是tf2onnx,它支持将SavedModel、GraphDef或Keras模型转换为ONNX。转换命令通常为:
python -m tf2onnx.convert \
--saved-model ./saved_model_dir \
--output model.onnx \
--opset 13 \
--inputs "input_1:0[1,224,224,3],input_2:0[1,128,128,3]" \
--outputs "output_0:0"
在转换多输入模型时,--inputs参数需要列出所有输入张量的名称和形状,且顺序必须与模型定义一致。此外,还需要注意--opset版本的选择——较新的算子集(如Opset 13+)对多输入算子的支持更完善,但某些旧版推理引擎可能不支持高版本Opset。
| 工具链组件 | 适用框架 | 关键参数/功能 | 多输入注意事项 |
|---|---|---|---|
torch.onnx.export |
PyTorch | input_names, output_names, dynamic_axes, opset_version |
必须为每个输入指定唯一的input_names;dynamic_axes需覆盖所有输入 |
tf2onnx.convert |
TensorFlow | --inputs, --outputs, --opset, --target |
--inputs中需包含所有输入张量的完整签名 |
onnx-simplifier |
通用 | 简化图结构、消除冗余算子 | 简化后需重新验证多输入分支是否被正确保留 |
onnxruntime |
通用 | 跨平台推理、性能优化 | 支持多输入会话,需按input_names传入数据 |
2.4 案例拆解:双输入传感器融合模型的转换实战
假设我们有一个自动驾驶场景下的摄像头-激光雷达融合检测模型。模型结构为:图像分支(ResNet-18骨干网络)提取2D特征,点云分支(PointPillars)提取3D特征,两分支特征在torch.cat操作后送入检测头。训练框架为PyTorch,目标部署环境为ONNX Runtime。
初始现象与目标:直接使用torch.onnx.export导出模型时,虽然导出成功,但在ONNX Runtime中推理时,输入数据无法正确匹配——模型期望两个输入张量,但推理代码只传入了一个。进一步检查发现,导出的ONNX图中只有一个输入节点,另一个输入被错误地当作了常量(Constant)折叠进了图里。
分析判断过程:问题根源在于示例输入构造不当。在导出时,我们使用了torch.randn(1,3,224,224)作为唯一的示例输入,而点云分支的输入lidar_input没有被传入export函数。PyTorch的JIT tracer在跟踪模型时,只记录了实际参与计算的张量路径,未传入的输入被当作“未定义”处理,导致其被优化掉。
实际操作步骤:
import torch
import torch.onnx
# 定义双输入模型
class FusionModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.image_branch = ... # 图像分支
self.lidar_branch = ... # 点云分支
self.fusion = ... # 融合层
def forward(self, image_input, lidar_input):
img_feat = self.image_branch(image_input)
lidar_feat = self.lidar_branch(lidar_input)
fused = torch.cat([img_feat, lidar_feat], dim=1)
return self.fusion(fused)
model = FusionModel().eval()
# 构造两个示例输入
image_example = torch.randn(1, 3, 224, 224)
lidar_example = torch.randn(1, 64, 128, 128)
# 导出时同时传入两个输入
torch.onnx.export(
model,
(image_example, lidar_example), # 注意:必须是元组
"fusion_model.onnx",
input_names=["image_input", "lidar_input"],
output_names=["detection_output"],
dynamic_axes={
"image_input": {0: "batch"},
"lidar_input": {0: "batch"},
"detection_output": {0: "batch"}
},
opset_version=13
)
结果对比与验证:导出后,使用onnxruntime进行推理验证:
import onnxruntime as ort
import numpy as np
sess = ort.InferenceSession("fusion_model.onnx")
# 准备两个输入
image_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
lidar_data = np.random.randn(1, 64, 128, 128).astype(np.float32)
# 按名称传入
outputs = sess.run(
["detection_output"],
{"image_input": image_data, "lidar_input": lidar_data}
)
print(outputs[0].shape)
验证通过后,模型推理结果与PyTorch原始输出误差小于1e-5,转换成功。
风险与代价:在转换过程中,我们遇到了两个额外问题:一是dynamic_axes设置不当导致batch维度被固定;二是点云分支中的自定义算子(如Scatter)在ONNX中没有直接对应,需要手动实现或使用onnx-simplifier处理。这些问题的排查耗时约2小时,但通过逐步打印ONNX图结构(onnx.helper.printable_graph)最终定位。
复盘经验:多输入模型转换的核心要点是“显式声明”——所有输入必须显式传入导出函数,所有输入名称必须显式定义,所有动态维度必须显式标注。任何“隐式”的假设都可能导致转换后的模型行为异常。
2.5 常见误区与总结复盘
在ONNX转换实践中,尤其是多输入模型场景,以下误区值得警惕:
- 误区一:忽略
eval()模式。PyTorch模型在train()模式下包含Dropout、BatchNorm等训练专用层,这些层在推理时行为不同。未切换到eval()会导致导出的ONNX模型包含错误的计算逻辑。 - 误区二:示例输入形状与真实数据不一致。ONNX图会记录示例输入的形状信息,如果推理时输入形状不同(且未设置
dynamic_axes),会直接报错。 - 误区三:多输入顺序混乱。ONNX Runtime的
run()方法接受字典格式输入,键名必须与input_names完全一致,顺序无关但名称必须匹配。 - 误区四:忽略Opset版本兼容性。某些算子(如
GridSample、NonMaxSuppression)在不同Opset版本中行为有差异,部署时需确认推理引擎支持的Opset范围。 - 误区五:不进行输出验证。转换后直接部署,未对比ONNX Runtime与原始框架的输出差异,可能导致精度下降问题被掩盖。
netron工具(在线或本地)查看图结构,确认所有输入节点、融合节点、输出节点的连接关系是否符合预期。如果发现某个输入分支被“剪枝”或“折叠”,应立即检查示例输入的构造和dynamic_axes设置。
总结:ONNX作为模型交换的“通用语言”,其核心价值在于计算图的标准化描述。对于多输入模型,转换工作的重点不在于“导出”本身,而在于输入定义的完整性和图结构的正确性。掌握ONNX的Protobuf结构、计算图的数据流逻辑、转换工具链的参数语义,是后续处理传感器融合、多模态模型的基础。在实际项目中,建议建立一套标准化的转换验证流程:导出前检查模型模式、导出时显式声明所有输入、导出后使用onnx.checker和onnxruntime双重验证、最后用netron人工审查图结构。这套流程能覆盖绝大多数多输入模型的转换需求,并为后续的算子定制和性能优化打下坚实基础。