第4章:ONNX模型可视化——用Netron看懂模型结构
说实话,我刚入行那会儿,拿到一个ONNX模型就像拿到一个黑盒子。只知道输入输出,中间发生了什么完全靠猜。直到我遇到了Netron这个工具——嗯,它就像给模型做了个CT扫描,每一层都看得清清楚楚。
这一章,我就带你亲手用Netron打开一个ONNX模型,看懂它的结构、输入输出,还有中间层的特征。我个人习惯,拿到任何模型的第一步,就是先可视化看看长什么样。
4.1 为什么需要可视化?
你想想看,一个ResNet-50的ONNX模型,光文本描述就有几千行。你一行一行去读?不现实。可视化能帮你快速回答三个核心问题:
- 输入长什么样? 是1x3x224x224还是别的形状?
- 输出有几个? 分类、检测框还是特征图?
- 中间层有没有坑? 比如突然来个Reshape把维度搞乱了
我在项目中遇到过,一个同事部署模型时怎么都跑不对,最后可视化一看——输入顺序是NHWC,而NPU驱动默认是NCHW。这种问题,光看代码根本发现不了。
4.2 安装Netron
Netron的安装非常简单。我个人推荐用pip安装,这样随时可以升级:
pip install netron
装完之后,启动方式有两种:
- 命令行启动:
netron model.onnx,会自动打开浏览器 - Python脚本启动: 适合集成到你的工具链中
import netron
netron.start('model.onnx')
启动后,浏览器会打开一个本地地址,比如 http://localhost:8080。模型结构就显示在页面上了。
4.3 看懂Netron界面
打开模型后,你会看到一个树状结构。左侧是模型的计算图,右侧是选中节点的详细信息。我来带你认认几个关键部分:
4.3.1 输入节点
输入节点通常在最顶部,用绿色或蓝色标识。点击它,右侧会显示:
- 名称: 比如
input或data - 数据类型: float32、int8 等
- 形状: 比如 [1, 3, 224, 224]
这里要注意,形状中的 1 通常代表batch size。有些模型会写成 dynamic,表示batch size可变。我在NPU适配时遇到过,有些NPU不支持动态batch,必须固定成1。
4.3.2 中间层节点
每个节点代表一个算子,比如Conv、Relu、MaxPool等。点击节点,右侧会显示:
- 算子类型: 比如
Conv - 属性: 卷积核大小、步长、padding等
- 输入输出形状: 这个最重要!
举个例子,一个Conv节点的属性可能是:
kernel_shape: [3, 3]
strides: [1, 1]
pads: [1, 1, 1, 1]
dilations: [1, 1]
group: 1
这些参数直接决定了NPU上怎么配置硬件算子。我曾经因为没注意 dilations 参数,导致空洞卷积在NPU上实现错了,结果精度掉了2个点。
4.3.3 输出节点
输出节点通常在底部。点击它,可以看到最终输出的形状和数据类型。如果是分类模型,输出形状通常是 [1, 1000];如果是检测模型,可能有多组输出。
output,形状是 [1, 84, 8400],那你就得按这个维度去解析。
4.4 实战:可视化一个ONNX模型
光说不练假把式。我们来实际操作一下。假设你有一个 resnet50.onnx 文件,执行:
netron resnet50.onnx
浏览器打开后,你会看到:
- 最顶部是输入节点
input,形状 [1, 3, 224, 224] - 往下是第一个卷积层
Conv_0,输出形状 [1, 64, 112, 112] - 接着是BatchNormalization、Relu、MaxPool...
- 中间有几个残差块,每个块包含多个卷积
- 最后是全局平均池化和全连接层,输出 [1, 1000]
你可以用鼠标滚轮缩放,拖动查看细节。我个人习惯先看整体结构,再双击某个残差块展开看内部。
netron --port 8081 model.onnx 换个端口。
4.5 理解中间层特征
可视化不只是看结构,更重要的是理解中间层特征。我教你一个方法:
- 选中某个中间层节点,记下它的输出形状
- 在代码中插入一个
onnxruntime的中间层输出 - 对比实际输出和预期形状是否一致
举个例子,你想看第一个卷积层的输出:
import onnxruntime as ort
import numpy as np
sess = ort.InferenceSession('resnet50.onnx')
# 获取中间层名称
intermediate_layer = 'Conv_0'
# 运行推理
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
outputs = sess.run([intermediate_layer], {'input': input_data})
print(outputs[0].shape) # 应该输出 [1, 64, 112, 112]
如果形状对不上,说明模型可能被优化过了,或者你找错了节点名称。Netron里显示的节点名称,就是你在代码里要用的那个。
4.6 避坑指南
做模型可视化这么多年,我踩过不少坑。分享几个给你:
- 节点名称重复: 有些模型导出时,节点名称会自动生成,比如
Conv_0、Conv_1。但如果你自己写代码时用了相同的名称,Netron会显示混乱。我建议导出模型时用opset_version参数控制一下。 - 输入输出名称不对: 有时候模型输入叫
input.1,输出叫output.1。你写推理代码时一定要用Netron里看到的名称,别想当然。 - 动态形状: 有些模型的输入形状是
dynamic,比如 [?, 3, ?, ?]。这种模型在NPU上适配起来比较麻烦,因为NPU通常需要固定形状。我建议先用onnx.shape_inference工具把形状固定下来。
4.7 小结
这一章我们学了:
- Netron的安装和启动
- 看懂输入、中间层、输出节点
- 实战可视化一个ONNX模型
- 理解中间层特征的方法
- 几个常见的避坑点
下一章,我们会深入ONNX的算子集,看看每个算子到底在做什么。到时候你会发现,可视化只是第一步,真正理解算子才是关键。
好了,快去打开你的模型看看吧。有什么问题,咱们下章见。