3、ONNX模型结构解析:ONNX计算图、节点(Node)、张量(Tensor)、初始器(Initializer)、图(Graph)与元数据
好,咱们今天来拆一个ONNX模型。
说实话,我第一次拿到ONNX文件时,第一反应是:这玩意儿到底长什么样?是像JSON那样能直接看,还是像二进制那样黑盒?后来我打开一看,嗯,它其实是个Protobuf序列化文件。说白了,就是Google那套序列化协议打包出来的。
我个人习惯,拿到一个新模型,第一件事就是把它反序列化,看看里面到底装了啥。今天我就带你一层层剥开ONNX的“洋葱”。
3.1 ONNX计算图:一切的核心
ONNX的核心,就是一张计算图(Graph)。
你想想看,深度学习模型本质上是什么?是一堆数学运算的组合。卷积、池化、激活、全连接……这些运算按照一定的顺序连接起来,就形成了一张图。这张图就是计算图。
我在项目中遇到过,有些同学把ONNX当成一个“黑盒”,直接拿来推理,出了问题也不知道怎么查。其实你只要理解了计算图,很多问题一眼就能看穿。
计算图由几个关键部分组成:
- 节点(Node):图中的每个运算操作,比如Conv、Relu、Add。
- 张量(Tensor):节点之间的数据流,也就是输入和输出。
- 初始器(Initializer):模型的权重和偏置,这些是固定的参数。
- 图(Graph):把节点、张量、初始器组织起来的容器。
- 元数据(Metadata):模型的版本、作者、描述等信息。
核心理解:ONNX模型 = 一张计算图 + 一堆权重 + 一些元数据。就这么简单。
3.2 节点(Node):运算的“原子”
节点是计算图的基本单元。每个节点代表一个运算操作。
比如一个卷积层,在ONNX里就是一个Conv节点。它有几个关键属性:
- op_type:运算类型,比如"Conv"、"Relu"、"Add"。
- input:输入张量的名称列表。
- output:输出张量的名称列表。
- attribute:运算的参数,比如卷积的kernel_shape、strides、pads。
我举个例子,一个简单的卷积节点长这样:
node {
input: "input_tensor"
input: "conv_weight"
input: "conv_bias"
output: "conv_output"
op_type: "Conv"
attribute {
name: "kernel_shape"
ints: 3
ints: 3
}
attribute {
name: "strides"
ints: 1
ints: 1
}
attribute {
name: "pads"
ints: 1
ints: 1
ints: 1
ints: 1
}
}
你看,这个节点接收三个输入:输入张量、权重、偏置。输出一个张量。属性里定义了卷积核大小、步长、填充。
避坑指南:我曾经在转换模型时,发现ONNX里的卷积padding计算方式和PyTorch不一样。PyTorch默认是"same"或"valid",但ONNX里你得手动算pads。这个坑我踩过,后来我写了个脚本自动对齐padding。
3.3 张量(Tensor):数据的“载体”
张量就是数据。在ONNX里,张量有名字、数据类型、形状和具体的数值。
张量分为两种:
- 输入/输出张量:节点之间的数据流,比如卷积的输入和输出。
- 初始器张量:模型的权重和偏置,这些是固定的。
一个张量的定义大概是这样:
tensor {
dims: 1
dims: 3
dims: 224
dims: 224
data_type: FLOAT
name: "input_tensor"
}
这里定义了一个形状为[1, 3, 224, 224]的浮点张量,名字叫"input_tensor"。
你想想看,整个模型推理过程,就是张量在节点之间流动的过程。输入张量经过Conv节点,变成特征图张量;再经过Relu节点,变成激活后的张量……直到最后的输出张量。
3.4 初始器(Initializer):模型的“记忆”
初始器就是模型的权重和偏置。它们是训练好的参数,在推理时不会改变。
在ONNX里,初始器被单独存放在一个列表里。每个初始器就是一个张量,但它是“可读不可写”的。
我举个例子,一个卷积层的权重初始器:
initializer {
dims: 64
dims: 3
dims: 3
dims: 3
data_type: FLOAT
name: "conv1.weight"
raw_data: "..."
}
这里的raw_data就是权重数值的二进制表示。ONNX支持两种存储方式:
- raw_data:二进制格式,体积小,解析快。
- float_data / int32_data:明文格式,可读性好,但体积大。
重要提示:量化压缩时,我们主要动的是初始器。把float32的权重变成int8,体积直接缩小4倍。我做过一个项目,把ResNet50的权重从FP32量化到INT8,模型大小从98MB降到25MB,精度只掉了0.3%。
3.5 图(Graph):把一切组织起来
图是ONNX模型的骨架。它把节点、张量、初始器组织成一个完整的计算流程。
一个图包含:
- node:所有运算节点。
- initializer:所有权重和偏置。
- input:模型的输入张量。
- output:模型的输出张量。
- value_info:中间张量的形状和类型信息。
我画个简单的图帮你理解:
输入张量 (input_tensor)
↓
Conv节点 (conv1) ← 权重初始器 (conv1.weight) + 偏置初始器 (conv1.bias)
↓
Relu节点 (relu1)
↓
Conv节点 (conv2) ← 权重初始器 (conv2.weight) + 偏置初始器 (conv2.bias)
↓
输出张量 (output_tensor)
你看,这就是一个最简单的两层卷积网络的计算图。每个节点从初始器或前一个节点获取输入,然后产生输出。
个人经验:我调试模型时,经常用Netron可视化工具看计算图。它能直观地展示节点之间的连接关系。有一次我发现模型里多了一个没用的Identity节点,就是通过可视化发现的。
3.6 元数据(Metadata):模型的“身份证”
元数据记录了模型的基本信息。虽然不参与计算,但非常重要。
常见的元数据包括:
- ir_version:ONNX中间表示的版本号。
- opset_import:使用的算子集版本。
- producer_name:生成模型的框架,比如"pytorch"、"tensorflow"。
- producer_version:框架的版本号。
- domain:模型所属的域,通常是"ai.onnx"。
- model_version:模型版本号,可以自己定义。
- doc_string:模型的描述信息。
我举个例子:
ir_version: 8
opset_import {
version: 18
}
producer_name: "pytorch"
producer_version: "2.1.0"
domain: "ai.onnx"
model_version: 1
doc_string: "ResNet50 model for image classification"
注意:元数据里的opset_import版本很重要。不同版本的ONNX支持的算子不一样。我遇到过一个问题:用opset 18导出的模型,在只支持opset 15的推理引擎上跑不了。后来我统一用opset 11,兼容性最好。
3.7 实战:用Python解析ONNX模型
光说不练假把式。咱们写几行代码,看看ONNX模型到底长什么样。
import onnx
# 加载模型
model = onnx.load("resnet50.onnx")
# 获取图
graph = model.graph
# 打印节点信息
print("=== 节点列表 ===")
for i, node in enumerate(graph.node):
print(f"节点{i}: op_type={node.op_type}, input={node.input}, output={node.output}")
# 打印初始器信息
print("\n=== 初始器列表 ===")
for initializer in graph.initializer:
print(f"名称: {initializer.name}, 形状: {initializer.dims}, 数据类型: {initializer.data_type}")
# 打印输入输出
print("\n=== 输入 ===")
for input_tensor in graph.input:
print(f"名称: {input_tensor.name}, 形状: {input_tensor.type.tensor_type.shape}")
print("\n=== 输出 ===")
for output_tensor in graph.output:
print(f"名称: {output_tensor.name}, 形状: {output_tensor.type.tensor_type.shape}")
# 打印元数据
print(f"\n=== 元数据 ===")
print(f"IR版本: {model.ir_version}")
print(f"算子集版本: {model.opset_import[0].version}")
print(f"生产者: {model.producer_name} {model.producer_version}")
运行这段代码,你就能看到模型的全貌。我个人习惯,拿到一个新模型,先跑一遍这个脚本,心里就有底了。
总结一下:ONNX模型 = 计算图 + 权重 + 元数据。计算图由节点、张量、初始器组成。理解了这些,你就掌握了ONNX的“骨架”。下一章,咱们聊聊怎么用Python操作ONNX模型,包括增删改查节点。
嗯,今天就到这儿。记住,动手实践是最好的学习方式。打开你的Python,加载一个ONNX模型,看看它里面到底有啥。