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,那就会报错。
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搜索节点名,快速定位
我记得有一次,同事说他的BEV模型部署后推理结果全是0。我让他用Netron打开模型,一看——最后一个节点的输出shape是[1, 0, 256]。嗯,有个Reshape节点把维度搞错了。这种问题,光看代码根本发现不了。
4.3 模型输入输出张量分析
好,这是本章的重头戏。模型输入输出张量分析,说白了就是搞清楚三件事:
- 输入有几个?每个输入的shape是什么?
- 输出有几个?每个输出的shape是什么?
- 数据排布方式是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的空间分辨率。
数据排布方式:
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固定死。
[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结果就全歪了。
好,这一章的内容就到这里。总结一下:
- ONNX模型的核心是GraphProto,包含节点、权重、输入输出
- Netron是可视化神器,能帮你快速定位模型结构问题
- 输入输出张量分析,要关注shape、数据类型、排布方式
- BEV模型里,GridSample是关键算子,务必重点检查
下一章,咱们要开始动手写推理框架了。到时候,这些ONNX结构知识全都会用上。准备好了吗?