4、模型基础:ONNX模型结构解析、Netron可视化工具使用、模型输入输出张量分析

好,咱们进入第四章。说实话,很多做嵌入式部署的朋友,模型训练得挺溜,但一拿到ONNX文件就懵了——这玩意儿里面到底长啥样?输入输出维度对不对?哪个节点是卷积、哪个是激活?

我当年第一次部署BEV模型时,就吃过这个亏。模型跑起来结果全乱,查了半天才发现是输入张量的通道顺序搞反了。嗯,从那以后,我养成了一个习惯:拿到任何ONNX模型,第一件事就是打开看看结构。

这一章,咱们就手把手把ONNX模型扒个底朝天。

4.1 ONNX模型结构解析

ONNX,全称Open Neural Network Exchange。说白了,它就是AI模型的“通用语言”。不管你是用PyTorch、TensorFlow还是别的框架训练的模型,都能转成ONNX格式。

一个ONNX模型文件,本质上是一个protobuf序列化的二进制文件。它的内部结构可以用一个简单的树形图来理解:

ModelProto
├── ir_version: 4          // ONNX版本号
├── producer_name: "pytorch" // 哪个框架生成的
├── graph: GraphProto       // 核心!计算图
│   ├── node: [NodeProto]   // 所有算子节点
│   │   ├── input: ["input.1", "conv.weight"]
│   │   ├── output: ["conv_output"]
│   │   ├── op_type: "Conv" // 算子类型
│   │   └── attribute: {...} // 算子属性
│   ├── initializer: [TensorProto] // 权重参数
│   ├── input: [ValueInfoProto]    // 模型输入
│   └── output: [ValueInfoProto]   // 模型输出

你看,核心就是GraphProto。它里面包含了:

  • node列表:所有计算节点,比如Conv、Relu、Add、Reshape等
  • initializer列表:所有权重参数,比如卷积核、BN的gamma/beta
  • input/output:模型的输入输出描述

我个人习惯,拿到ONNX文件后,第一件事就是检查ir_version。为什么?因为不同版本的ONNX opset支持的算子不一样。BEV模型里经常用到的GridSample算子,在opset 16之后才稳定支持。如果你用的推理框架只支持opset 11,那就会报错。

注意:ONNX opset版本不匹配,是嵌入式部署中最常见的坑之一。我建议你统一使用opset 11或opset 16,这两个版本兼容性最好。

4.2 Netron可视化工具使用

光看代码结构不过瘾,咱们得用工具把模型“画”出来。Netron就是干这个的。

Netron是一个轻量级的模型可视化工具。它支持ONNX、TensorFlow、Caffe、PyTorch等多种格式。你想想看,一个几十层的BEV模型,靠人眼去读protobuf结构?那得看到猴年马月去。

Netron的安装方式:

# 方式一:直接使用网页版(推荐)
# 打开浏览器,访问:https://netron.app/

# 方式二:本地安装
pip install netron
python -m netron your_model.onnx

我个人更推荐网页版,省事。把ONNX文件拖进去,瞬间就能看到完整的计算图。

Netron的核心功能:

  • 节点点击查看详情:点一下某个Conv节点,右边会弹出它的属性——kernel size、stride、padding、dilation,一目了然
  • 输入输出张量形状:每个节点都标注了输入输出的维度,比如[1, 64, 128, 128]
  • 权重参数可视化:点击initializer,可以看到权重的shape和数据类型
  • 搜索功能:模型大了之后,按Ctrl+F搜索节点名,快速定位
小技巧:在Netron里,你可以按住鼠标右键拖动平移视图,滚轮缩放。如果模型太大,建议先导出子图再看,否则浏览器可能会卡死。

我记得有一次,同事说他的BEV模型部署后推理结果全是0。我让他用Netron打开模型,一看——最后一个节点的输出shape是[1, 0, 256]。嗯,有个Reshape节点把维度搞错了。这种问题,光看代码根本发现不了。

4.3 模型输入输出张量分析

好,这是本章的重头戏。模型输入输出张量分析,说白了就是搞清楚三件事:

  1. 输入有几个?每个输入的shape是什么?
  2. 输出有几个?每个输出的shape是什么?
  3. 数据排布方式是NCHW还是NHWC?

咱们以BEV模型为例。一个典型的BEV模型,输入通常是多视角图像,输出是BEV空间的特征图或检测结果。

用Python读取ONNX模型信息:

import onnx

# 加载模型
model = onnx.load("bev_model.onnx")

# 获取计算图
graph = model.graph

# 打印输入信息
print("=== 模型输入 ===")
for input_tensor in graph.input:
    print(f"名称: {input_tensor.name}")
    shape = [dim.dim_value for dim in input_tensor.type.tensor_type.shape.dim]
    print(f"形状: {shape}")
    print(f"数据类型: {input_tensor.type.tensor_type.elem_type}")
    print()

# 打印输出信息
print("=== 模型输出 ===")
for output_tensor in graph.output:
    print(f"名称: {output_tensor.name}")
    shape = [dim.dim_value for dim in output_tensor.type.tensor_type.shape.dim]
    print(f"形状: {shape}")
    print(f"数据类型: {output_tensor.type.tensor_type.elem_type}")
    print()

运行这段代码,你会看到类似这样的输出:

=== 模型输入 ===
名称: images
形状: [1, 6, 3, 256, 704]
数据类型: 1  (1代表float32)

=== 模型输出 ===
名称: bev_feature
形状: [1, 256, 128, 128]
数据类型: 1

你看,输入是[1, 6, 3, 256, 704]。这代表什么?

  • 1:batch size,一次推理一张图
  • 6:6个视角的相机图像
  • 3:RGB三个通道
  • 256:图像高度
  • 704:图像宽度

输出是[1, 256, 128, 128],代表BEV空间的特征图,256个通道,128x128的空间分辨率。

重点:输入输出的数据类型也很关键。大部分模型用float32,但嵌入式设备上为了加速,可能会用float16或int8。如果模型输出是float32,你的推理框架却按int8去解析,结果肯定不对。

数据排布方式:

ONNX默认是NCHW排布。但很多嵌入式推理框架(比如TensorRT、ONNX Runtime)内部会用NHWC。为什么?因为NHWC在内存访问上更连续,对CPU/GPU更友好。

我建议你在导出ONNX模型时,就明确指定排布方式。以PyTorch为例:

# 导出时指定输入输出名称和动态轴
torch.onnx.export(
    model,
    dummy_input,
    "bev_model.onnx",
    input_names=["images"],
    output_names=["bev_feature"],
    dynamic_axes={
        "images": {0: "batch_size"},
        "bev_feature": {0: "batch_size"}
    },
    opset_version=16
)

这里有个细节:dynamic_axes参数。它允许你指定哪些维度是动态的。比如batch size,你训练时是1,但部署时可能想一次推理多张图。如果不设置动态轴,ONNX模型就会把batch size固定死。

避坑指南:我曾经遇到过一个模型,输入shape写的是[1, 3, 256, 704],但实际推理时需要[1, 6, 3, 256, 704]。为什么?因为模型内部有个reshape操作,把6个视角合并成了batch维度。这种“隐式”的维度变换,Netron里一眼就能看出来。所以,拿到模型后,一定要用Netron走一遍,确认输入输出和你的预期一致。

4.4 实战:用Netron分析一个BEV模型

咱们来走一遍完整流程。假设你有一个bev_model.onnx文件。

第一步:打开Netron

拖入文件,等待加载。你会看到一张巨大的计算图。别慌,先看最左边和最右边——那就是输入和输出节点。

第二步:查看输入节点

点击输入节点images,右侧面板显示:

  • 名称:images
  • 形状:[1, 6, 3, 256, 704]
  • 数据类型:float32

第三步:查看输出节点

点击输出节点bev_feature,右侧面板显示:

  • 名称:bev_feature
  • 形状:[1, 256, 128, 128]
  • 数据类型:float32

第四步:检查关键算子

BEV模型里,最关键的算子就是GridSample。在Netron里搜索GridSample,看看它前后的张量形状。GridSample的输入通常是:

  • 输入特征图:[1, 64, 32, 32]
  • 采样网格:[1, 128, 128, 2]

输出是:[1, 64, 128, 128]

这个操作把图像特征投影到了BEV空间。如果这里的维度不对,整个BEV结果就全歪了。

个人经验:我每次部署BEV模型,都会在Netron里截图保存。特别是GridSample节点附近的子图,方便后续调试时对照。你想想看,嵌入式设备上没法跑Netron,但你可以拿着截图去对比推理中间结果,定位问题快得多。

好,这一章的内容就到这里。总结一下:

  • ONNX模型的核心是GraphProto,包含节点、权重、输入输出
  • Netron是可视化神器,能帮你快速定位模型结构问题
  • 输入输出张量分析,要关注shape、数据类型、排布方式
  • BEV模型里,GridSample是关键算子,务必重点检查

下一章,咱们要开始动手写推理框架了。到时候,这些ONNX结构知识全都会用上。准备好了吗?