ONNX 模型修改:从节点编辑到算子替换,新手也能快速上手
flyfish
文末有完整代码
一、导入需要的工具库
import onnx
from onnx import helper, numpy_helper
import numpy as np
import onnx:导入ONNX官方库,用来加载、保存、校验ONNX模型。
from onnx import helper, numpy_helper:导入两个辅助工具
helper:用来快速创建新的计算节点、张量等结构。
numpy_helper:用来把Python/NumPy的数值,转换成ONNX能识别的张量格式。
import numpy as np:导入数值计算库,用来生成常量数值(比如后面的1.0)。
二、加载模型,查看基础信息
model = onnx.load("./custom.onnx")
print(f"模型IR版本: {model.ir_version}")
print(f"模型生成工具: {model.producer_name}")
print(f"模型opset版本: {model.opset_import[0].version}")
graph = model.graph
print(f"计算图名称: {graph.name}")
onnx.load("./custom.onnx"):从文件里加载ONNX模型,存到model变量中。
model.ir_version:模型遵循的ONNX中间表示版本号。
model.producer_name:导出这个模型的工具名称,比如PyTorch、TensorFlow。
model.opset_import[0].version:模型使用的算子集版本,决定了支持哪些算子。
graph = model.graph:取出模型的核心部分——计算图,后面所有修改都在计算图上进行。
graph.name:计算图的名字,由导出模型时设定。
模型 IR 版本: 6
模型生成工具: pytorch
模型 opset 版本: 11
计算图名称: main_graph
三、查看计算图的心组成
计算图主要由四部分构成:输入、输出、权重常量、计算节点,下面分别打印查看。
3.1 查看模型输入张量
print("模型输入张量:")
for i, inp in enumerate(graph.input):
shape = [d.dim_value if d.dim_value > 0 else "动态" for d in inp.type.tensor_type.shape.dim]
print(f" 输入{i}:名字={inp.name},形状={shape}")
graph.input:列表,存所有需要从外部传入的张量,比如图片输入。
循环遍历每个输入,inp.name是张量的名字。
形状处理:每个维度如果是固定数值就显示数字,如果是动态维度(比如batch size)就显示“动态”。
模型输入张量 (graph.input) —
含义:推理时需要从外部传入的数据,比如图片像素
输入[0] 名称: images
类型: tensor_type {
elem_type: 1
shape {
dim {
dim_param: "batch"
}
dim {
dim_value: 3
}
dim {
dim_value: 640
}
dim {
dim_value: 640
}
}
}
3.2 查看模型输出张量
print("模型输出张量:")
for i, out in enumerate(graph.output):
shape = [d.dim_value if d.dim_value > 0 else "动态" for d in out.type.tensor_type.shape.dim]
print(f" 输出{i}:名字={out.name},形状={shape}")
`graph.output`:列表,存模型计算完成后输出的结果张量。
逻辑和输入一致,打印名字和形状。
模型输出张量 (graph.output) —
含义:模型计算完成后输出的结果
输出[0] 名称: output
形状: ['动态', 2]
3.3 查看模型权重常量
print(f"权重常量总数量:{len(graph.initializer)}")
print("前10个权重常量:")
for i, init in enumerate(graph.initializer[:10]):
print(f" 权重{i}:名字={init.name},形状={list(init.dims)}")
graph.initializer:列表,存模型里所有自带的固定数值,比如卷积核权重、偏置、归一化参数等,不需要外部传入。
模型通常有非常多权重,所以只打印前10个避免刷屏。
init.name是权重的名字,init.dims是权重的形状。
模型权重常量 (graph.initializer) —
含义:模型自带的固定数值,比如卷积核权重、偏置、归一化参数等
权重总数量: 182
(只打印前10个)
权重[0] 名称: downsample_layers.0.0.weight
数据类型: 1
形状: [96, 3, 4, 4]
权重[1] 名称: downsample_layers.0.0.bias
数据类型: 1
形状: [96]
权重[2] 名称: downsample_layers.1.1.weight
数据类型: 1
形状: [192, 96, 2, 2]
权重[3] 名称: downsample_layers.1.1.bias
数据类型: 1
形状: [192]
权重[4] 名称: downsample_layers.2.1.weight
数据类型: 1
形状: [384, 192, 2, 2]
权重[5] 名称: downsample_layers.2.1.bias
数据类型: 1
形状: [384]
权重[6] 名称: downsample_layers.3.1.weight
数据类型: 1
形状: [768, 384, 2, 2]
权重[7] 名称: downsample_layers.3.1.bias
数据类型: 1
形状: [768]
权重[8] 名称: stages.0.0.gamma
数据类型: 1
形状: [96]
权重[9] 名称: stages.0.0.dwconv.weight
数据类型: 1
形状: [96, 1, 7, 7]
3.4 查看计算节点
print(f"计算节点总数量:{len(graph.node)}")
print("前15个计算节点:")
for i, node in enumerate(graph.node[:15]):
print(f"节点{i+1}:")
print(f" 节点名:{node.name}")
print(f" 算子类型:{node.op_type}")
print(f" 输入张量:{list(node.input)}")
print(f" 输出张量:{list(node.output)}")
graph.node:列表,存所有运算节点,每个节点代表一次计算。
node.name:节点的唯一名字,用来定位和区分不同节点。
node.op_type:算子类型,说明这个节点做什么运算,常见的有Conv(卷积)、Relu(激活)、Add(加法)、Div(除法)等。
node.input:列表,存这个节点所有输入张量的名字,都是字符串。
node.output:列表,存这个节点计算完成后,输出张量的名字,也是字符串。
理解:ONNX计算图靠张量名字串联。上一个节点的输出名字,会作为下一个节点的输入名字,以此形成数据流动的路径,比如节点A的输出叫 ‘x’,节点B的输入写 ‘x’,就代表B用A的结果。
计算节点 (graph.node) —
含义:每一个 node 就是一次运算,比如卷积、ReLU、加法、除法
节点总数量: 565
(只打印前15个,重点看 op_type / name / input / output)
【节点 1】
节点名称(name) : /downsample_layers.0/downsample_layers.0.0/Conv ← 节点的唯一名字,方便定位
算子类型(op_type): Conv ← 这个节点做什么运算
输入张量(input) : ['images', 'downsample_layers.0.0.weight', 'downsample_layers.0.0.bias'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.0/Conv_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 2】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/ReduceMean ← 节点的唯一名字,方便定位
算子类型(op_type): ReduceMean ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.0/Conv_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/ReduceMean_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 3】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Sub ← 节点的唯一名字,方便定位
算子类型(op_type): Sub ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.0/Conv_output_0', '/downsample_layers.0/downsample_layers.0.1/ReduceMean_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Sub_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 4】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Constant ← 节点的唯一名字,方便定位
算子类型(op_type): Constant ← 这个节点做什么运算
输入张量(input) : [] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Constant_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 5】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Pow ← 节点的唯一名字,方便定位
算子类型(op_type): Pow ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.1/Sub_output_0', '/downsample_layers.0/downsample_layers.0.1/Constant_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Pow_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 6】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/ReduceMean_1 ← 节点的唯一名字,方便定位
算子类型(op_type): ReduceMean ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.1/Pow_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/ReduceMean_1_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 7】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Constant_1 ← 节点的唯一名字,方便定位
算子类型(op_type): Constant ← 这个节点做什么运算
输入张量(input) : [] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Constant_1_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 8】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Add ← 节点的唯一名字,方便定位
算子类型(op_type): Add ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.1/ReduceMean_1_output_0', '/downsample_layers.0/downsample_layers.0.1/Constant_1_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Add_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 9】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Sqrt ← 节点的唯一名字,方便定位
算子类型(op_type): Sqrt ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.1/Add_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Sqrt_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 10】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Div ← 节点的唯一名字,方便定位
算子类型(op_type): Div ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.1/Sub_output_0', '/downsample_layers.0/downsample_layers.0.1/Sqrt_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Div_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 11】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Mul ← 节点的唯一名字,方便定位
算子类型(op_type): Mul ← 这个节点做什么运算
输入张量(input) : ['onnx::Mul_803', '/downsample_layers.0/downsample_layers.0.1/Div_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Mul_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 12】
节点名称(name) : /downsample_layers.0/downsample_layers.0.1/Add_1 ← 节点的唯一名字,方便定位
算子类型(op_type): Add ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.1/Mul_output_0', 'onnx::Add_805'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/downsample_layers.0/downsample_layers.0.1/Add_1_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 13】
节点名称(name) : /stages.0/stages.0.0/dwconv/Conv ← 节点的唯一名字,方便定位
算子类型(op_type): Conv ← 这个节点做什么运算
输入张量(input) : ['/downsample_layers.0/downsample_layers.0.1/Add_1_output_0', 'stages.0.0.dwconv.weight', 'stages.0.0.dwconv.bias'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/stages.0/stages.0.0/dwconv/Conv_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 14】
节点名称(name) : /stages.0/stages.0.0/Transpose ← 节点的唯一名字,方便定位
算子类型(op_type): Transpose ← 这个节点做什么运算
输入张量(input) : ['/stages.0/stages.0.0/dwconv/Conv_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/stages.0/stages.0.0/Transpose_output_0'] ← 计算完输出什么张量,也是字符串名字
【节点 15】
节点名称(name) : /stages.0/stages.0.0/norm/ReduceMean ← 节点的唯一名字,方便定位
算子类型(op_type): ReduceMean ← 这个节点做什么运算
输入张量(input) : ['/stages.0/stages.0.0/Transpose_output_0'] ← 从哪里拿数据,都是张量名字(字符串)
输出张量(output) : ['/stages.0/stages.0.0/norm/ReduceMean_output_0'] ← 计算完输出什么张量,也是字符串名字
四、定位目标除法节点
target_div_nodes = []
for n in graph.node:
if n.op_type == "Div" and "downsample_layers" in n.name:
target_div_nodes.append(n)
print(f"共找到{len(target_div_nodes)}个目标除法节点:")
for idx, div_node in enumerate(target_div_nodes):
print(f"第{idx+1}个:")
print(f" 节点名:{div_node.name}")
print(f" 分子输入:{div_node.input[0]}")
print(f" 分母输入:{div_node.input[1]}")
print(f" 输出结果:{div_node.output[0]}")
遍历所有计算节点,筛选符合两个条件的节点:
把符合条件的节点存入target_div_nodes列表。
逐个打印目标节点的详情:
div_node.input[0]:除法的分子输入张量名。
div_node.input[1]:除法的分母输入张量名。
div_node.output[0]:除法计算结果的张量名。
共找到 4 个目标除法节点,详细信息如下:
目标Div节点 1 —
节点名: /downsample_layers.0/downsample_layers.0.1/Div
算子类型: Div
输入分子(input[0]): /downsample_layers.0/downsample_layers.0.1/Sub_output_0
输入分母(input[1]): /downsample_layers.0/downsample_layers.0.1/Sqrt_output_0
输出结果: /downsample_layers.0/downsample_layers.0.1/Div_output_0
目标Div节点 2 —
节点名: /downsample_layers.1/downsample_layers.1.0/Div
算子类型: Div
输入分子(input[0]): /downsample_layers.1/downsample_layers.1.0/Sub_output_0
输入分母(input[1]): /downsample_layers.1/downsample_layers.1.0/Sqrt_output_0
输出结果: /downsample_layers.1/downsample_layers.1.0/Div_output_0
目标Div节点 3 —
节点名: /downsample_layers.2/downsample_layers.2.0/Div
算子类型: Div
输入分子(input[0]): /downsample_layers.2/downsample_layers.2.0/Sub_output_0
输入分母(input[1]): /downsample_layers.2/downsample_layers.2.0/Sqrt_output_0
输出结果: /downsample_layers.2/downsample_layers.2.0/Div_output_0
目标Div节点 4 —
节点名: /downsample_layers.3/downsample_layers.3.0/Div
算子类型: Div
输入分子(input[0]): /downsample_layers.3/downsample_layers.3.0/Sub_output_0
输入分母(input[1]): /downsample_layers.3/downsample_layers.3.0/Sqrt_output_0
输出结果: /downsample_layers.3/downsample_layers.3.0/Div_output_0
五、修改计算图,插入乘1节点
这是核心部分,在每个目标除法节点的分母支路上,插入一个“乘1.0”的运算,数值不变但破坏算子融合。
for idx, div_node in enumerate(target_div_nodes):
denom_input = div_node.input[1]
prefix = f"pixelnorm_break_{idx}"
循环处理每一个目标除法节点。
denom_input = div_node.input[1]:取出原始分母的张量名字。
prefix = f"pixelnorm_break_{idx}":生成一个专属前缀,idx是序号,保证每个新增节点、张量的名字都不重复。
5.1 创建值为1.0的常量节点
one_const_node = helper.make_node(
op_type="Constant",
inputs=[],
outputs=[f"{prefix}_one_const"],
name=f"{prefix}_one_const_node",
value=numpy_helper.from_array(np.array(1.0, dtype=np.float32), name=f"{prefix}_one_val")
)
helper.make_node:创建一个新的ONNX节点。
op_type="Constant":节点类型是常量,作用是输出一个固定数值。
inputs=[]:常量节点没有输入,数值直接内嵌在节点里。
outputs=[f"{prefix}_one_const"]:这个节点的输出张量名字,由前缀加_one_const拼接而成。
name=…:这个节点自身的名字。
value=…:常量的具体数值,用numpy_helper.from_array把NumPy的1.0转成ONNX能识别的张量格式。
5.2 创建乘法节点
mul_node = helper.make_node(
op_type="Mul",
inputs=[denom_input, f"{prefix}_one_const"],
outputs=[f"{prefix}_denom_out"],
name=f"{prefix}_mul_one"
)
创建一个乘法(Mul)节点,计算“原始分母 × 1.0”。
inputs:两个输入,分别是原始分母张量、前面创建的1.0常量张量。
outputs=[f"{prefix}_denom_out"]:乘法节点的输出张量名,由前缀加_denom_out拼接而成。_denom_out就是这里来的,完全是代码手动拼接的字符串,不是ONNX自带的。
name=…:这个乘法节点自身的名字。
5.3 修改原除法节点的输入
div_node.input[1] = f"{prefix}_denom_out"
把原除法节点的分母输入,替换成乘法节点的输出张量名。
替换后,除法节点的分母就不再是原始张量,而是“原始张量乘1”的结果,数值上完全等价。
5.4 把新节点加入计算图
graph.node.extend([one_const_node, mul_node])
把新建的常量节点和乘法节点,追加到计算图的节点列表末尾。
此时节点顺序是乱的,因为除法节点在前面,却引用了后面新增节点的输出,所以后面需要重新排序。
处理第 1 个Div节点 —
原始分母张量名: /downsample_layers.0/downsample_layers.0.1/Sqrt_output_0
生成的命名前缀: pixelnorm_break_0
→ 常量张量名: pixelnorm_break_0_one_const
→ Mul输出张量名: pixelnorm_break_0_denom_out ← 就是 _denom_out,字符串拼接出来的!
→ Mul节点名: pixelnorm_break_0_mul_one
修改前Div节点分母输入: /downsample_layers.0/downsample_layers.0.1/Sqrt_output_0
修改后Div节点分母输入: pixelnorm_break_0_denom_out
已新增2个节点到计算图:Constant节点 + Mul节点
处理第 2 个Div节点 —
原始分母张量名: /downsample_layers.1/downsample_layers.1.0/Sqrt_output_0
生成的命名前缀: pixelnorm_break_1
→ 常量张量名: pixelnorm_break_1_one_const
→ Mul输出张量名: pixelnorm_break_1_denom_out ← 就是 _denom_out,字符串拼接出来的!
→ Mul节点名: pixelnorm_break_1_mul_one
修改前Div节点分母输入: /downsample_layers.1/downsample_layers.1.0/Sqrt_output_0
修改后Div节点分母输入: pixelnorm_break_1_denom_out
已新增2个节点到计算图:Constant节点 + Mul节点
处理第 3 个Div节点 —
原始分母张量名: /downsample_layers.2/downsample_layers.2.0/Sqrt_output_0
生成的命名前缀: pixelnorm_break_2
→ 常量张量名: pixelnorm_break_2_one_const
→ Mul输出张量名: pixelnorm_break_2_denom_out ← 就是 _denom_out,字符串拼接出来的!
→ Mul节点名: pixelnorm_break_2_mul_one
修改前Div节点分母输入: /downsample_layers.2/downsample_layers.2.0/Sqrt_output_0
修改后Div节点分母输入: pixelnorm_break_2_denom_out
已新增2个节点到计算图:Constant节点 + Mul节点
处理第 4 个Div节点 —
原始分母张量名: /downsample_layers.3/downsample_layers.3.0/Sqrt_output_0
生成的命名前缀: pixelnorm_break_3
→ 常量张量名: pixelnorm_break_3_one_const
→ Mul输出张量名: pixelnorm_break_3_denom_out ← 就是 _denom_out,字符串拼接出来的!
→ Mul节点名: pixelnorm_break_3_mul_one
修改前Div节点分母输入: /downsample_layers.3/downsample_layers.3.0/Sqrt_output_0
修改后Div节点分母输入: pixelnorm_break_3_denom_out
已新增2个节点到计算图:Constant节点 + Mul节点
六、拓扑排序函数
def topological_sort_nodes(graph):
nodes = list(graph.node)
n = len(nodes)
ready = set(inp.name for inp in graph.input)
for init in graph.initializer:
ready.add(init.name)
visited = [False] * n
sorted_nodes = []
while len(sorted_nodes) < n:
progress = False
for i in range(n):
if visited[i]:
continue
node = nodes[i]
if all(in_name in ready for in_name in node.input):
sorted_nodes.append(node)
visited[i] = True
for out_name in node.output:
ready.add(out_name)
progress = True
if not progress:
unfinished = [nodes[i].name for i in range(n) if not visited[i]]
raise RuntimeError(f"无法完成排序,剩余节点:{unfinished[:10]}")
return sorted_nodes
作用
ONNX要求节点按执行顺序排列:一个节点的所有输入,必须已经被排在它前面的节点计算完成。插入新节点后顺序被打乱,需要重新排序。
逐段逻辑
- 把这个节点加入排序结果列表。
- 标记为已处理。
- 把这个节点的所有输出张量加入ready,供后面的节点使用。
七、执行排序并验证
sorted_nodes = topological_sort_nodes(graph)
del graph.node[:]
graph.node.extend(sorted_nodes)
print("排序后的前10个节点:")
for i, node in enumerate(graph.node[:10]):
print(f" {i+1}. {node.op_type} – {node.name}")
调用排序函数,得到正确顺序的节点列表。
del graph.node[:]:清空计算图里原来的节点列表。
graph.node.extend(sorted_nodes):把排好序的节点写回计算图。
打印排序后的前10个节点,验证顺序是否合理。
排序前节点总数: 573
排序后节点总数: 573
排序完成!现在计算图的节点执行顺序是正确的
排序后的前10个节点:
1. Conv | 节点名: /downsample_layers.0/downsample_layers.0.0/Conv
2. ReduceMean | 节点名: /downsample_layers.0/downsample_layers.0.1/ReduceMean
3. Sub | 节点名: /downsample_layers.0/downsample_layers.0.1/Sub
4. Constant | 节点名: /downsample_layers.0/downsample_layers.0.1/Constant
5. Pow | 节点名: /downsample_layers.0/downsample_layers.0.1/Pow
6. ReduceMean | 节点名: /downsample_layers.0/downsample_layers.0.1/ReduceMean_1
7. Constant | 节点名: /downsample_layers.0/downsample_layers.0.1/Constant_1
8. Add | 节点名: /downsample_layers.0/downsample_layers.0.1/Add
9. Sqrt | 节点名: /downsample_layers.0/downsample_layers.0.1/Sqrt
10. Constant | 节点名: /stages.0/stages.0.0/norm/Constant
八、校验并保存模型
onnx.checker.check_model(model)
onnx.save(model, "./custom_fixed.onnx")
print("修改完成,模型已保存")
onnx.checker.check_model:ONNX官方校验工具,检查模型是否符合规范,比如拓扑顺序是否正确、算子参数是否合法、张量维度是否匹配等。
onnx.save:把修改后的模型保存到新文件里。
完整代码
import onnx
from onnx import helper, numpy_helper
import numpy as np
def topological_sort_nodes(graph):
nodes = list(graph.node)
n = len(nodes)
ready = set(inp.name for inp in graph.input)
for init in graph.initializer:
ready.add(init.name)
visited = [False] * n
sorted_nodes = []
while len(sorted_nodes) < n:
progress = False
for i in range(n):
if visited[i]:
continue
node = nodes[i]
if all(in_name in ready for in_name in node.input):
sorted_nodes.append(node)
visited[i] = True
for out_name in node.output:
ready.add(out_name)
progress = True
if not progress:
unfinished = [nodes[i].name for i in range(n) if not visited[i]]
raise RuntimeError(f"拓扑排序失败,剩余节点: {unfinished[:10]}")
return sorted_nodes
if __name__ == "__main__":
# 加载模型
model = onnx.load("./custom.onnx")
graph = model.graph
# 定位目标 Div 节点
target_div_nodes = [
n for n in graph.node
if n.op_type == "Div" and "downsample_layers" in n.name
]
print(f"匹配到 {len(target_div_nodes)} 个目标节点")
# 插入乘 1 节点
for idx, div_node in enumerate(target_div_nodes):
denom_input = div_node.input[1]
prefix = f"pixelnorm_break_{idx}"
# 1.0 常量节点
one_const = helper.make_node(
op_type="Constant",
inputs=[],
outputs=[f"{prefix}_one_const"],
name=f"{prefix}_one_const_node",
value=numpy_helper.from_array(
np.array(1.0, dtype=np.float32),
name=f"{prefix}_one_val"
)
)
# 乘法节点
mul_node = helper.make_node(
op_type="Mul",
inputs=[denom_input, f"{prefix}_one_const"],
outputs=[f"{prefix}_denom_out"],
name=f"{prefix}_mul_one"
)
# 替换原除法节点的分母输入
div_node.input[1] = f"{prefix}_denom_out"
# 新节点加入计算图
graph.node.extend([one_const, mul_node])
# 重新拓扑排序
sorted_nodes = topological_sort_nodes(graph)
del graph.node[:]
graph.node.extend(sorted_nodes)
# 校验并保存
onnx.checker.check_model(model)
onnx.save(model, "./custom_fixed.onnx")
print("处理完成,已保存为 custom_fixed.onnx")
算子 与 计算节点 的关系
如果以前是做面向对象开发的,看下面的例子立马就懂了
# 算子 = 类:只定义规则
class Conv: # 这就是 Conv 算子
def __init__(self, stride, padding, kernel_shape):
self.stride = stride
self.padding = padding
self.kernel_shape = kernel_shape
# 计算节点 = 对象:实例化后的具体个体
conv1_node = Conv(stride=[1,1], padding=[1,1], kernel_shape=[3,3])
conv2_node = Conv(stride=[2,2], padding=[0,0], kernel_shape=[5,5])
Conv 这个“类”全程只有 1 个,但可以用它实例出 conv1_node、conv2_node 等无数个节点对象,各自参数、位置、作用都可以不同。
对应关系
| 类(Class) | 算子(Operator) | 抽象的模板/规范,定义了“是什么运算、有哪些参数、输入输出规则”,本身不占用计算图里的具体位置 |
| 对象/实例(Object/Instance) | 计算节点(Node) | 按照算子模板创建出的具体实体,有专属名称、具体参数值、专属的输入输出张量,是计算图的实际组成单元 |
ONNX 里的算子更偏向接口规范定义,只约定语义和格式,具体执行逻辑由推理引擎(ONNX Runtime、TensorRT 等)去实现。
算子(Operator / Op)
算子是抽象的功能定义,代表一类计算操作的规则与语义。
比如 Conv(卷积)、ReLU(激活)、Add(张量相加)、Reshape(维度变换)都属于算子;
它只定义“做什么运算、输入输出格式、可配置属性”,不绑定具体的输入张量、输出张量和在网络中的位置;
ONNX 官方通过 Opset(算子集)统一维护所有标准算子的规范。
计算节点(Node)
计算节点是算子在计算图里的具体实例,是模型网络的实际组成单元。
每一个节点都会绑定一个明确的算子类型(op_type),同时拥有自己专属的输入张量名、输出张量名、属性参数(比如 Conv1 节点的 stride、padding);
它是计算图(Graph)的基本单元,ONNX 模型的网络结构就是由一个个节点串联/并联而成的。
一个直观例子
假设一个简单的神经网络:输入 → Conv1 → ReLU1 → Conv2 → ReLU2 → 输出。
算子层面:一共只有 2 种算子 —— Conv 算子和 ReLU 算子;
计算节点层面:一共有 4 个节点 —— Conv1 节点、ReLU1 节点、Conv2 节点、ReLU2 节点。
同一个 ReLU 算子,在网络里被实例化了两次,就对应两个独立的计算节点。
