欢迎光临
我们一直在努力

ONNX 模型修改:从节点编辑到算子替换,新手也能快速上手

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]}")

遍历所有计算节点,筛选符合两个条件的节点:

  • 算子类型是Div,也就是除法运算。
  • 节点名字里包含downsample_layers,说明是下采样层里的除法。
    把符合条件的节点存入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要求节点按执行顺序排列:一个节点的所有输入,必须已经被排在它前面的节点计算完成。插入新节点后顺序被打乱,需要重新排序。

    逐段逻辑

  • nodes = list(graph.node):把所有节点转成列表,n记录节点总数。
  • ready集合:存当前已经可以直接使用的张量。初始时包含所有模型输入和权重常量,这些不需要计算就能用。
  • visited数组:标记每个节点是否已经排好序,避免重复处理。
  • 外层循环:直到所有节点都排好序才停止。
  • 内层遍历:逐个检查未处理的节点,如果它的所有输入张量都在ready里,说明依赖已经满足,可以执行:
    • 把这个节点加入排序结果列表。
    • 标记为已处理。
    • 把这个节点的所有输出张量加入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 等无数个节点对象,各自参数、位置、作用都可以不同。

    对应关系

    面向对象概念ONNX 概念说明
    类(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 算子,在网络里被实例化了两次,就对应两个独立的计算节点。

    赞(0)
    未经允许不得转载:171主机测评 » ONNX 模型修改:从节点编辑到算子替换,新手也能快速上手
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址