需求真的是千奇百怪,最近项目需要修改多年前通过tensorflow转换得到的ONNX模型,关键转换前的tensorflow模型已经神秘地失踪了


本小姐真是无力吐槽,这个班真是一天都不想上了,冷静下来想想,这个“不想上班”的想法还是太年轻,毕竟挣钱要紧,然后记录一波打工人的艰难之旅,dddd(懂得都懂)


友情提示:阅读该内容大概需要20分钟,请理性安排,适当的时候可以知难而退

目录

一、ONNX简介

二、ONNX结构分析

make_tensor_value_info(name,elem_type,shape)

make_node(op_type, inputs, outputs, name=None)

make_graph(nodes,name,inputs,outputs,initializer=None)

make_model(graph, **kwargs)

make_tensor(name,data_type,dims,vals)

make_attribute(key,value)

三、ONNX模型修改

合并两个ONNX模型

查看ONNX节点信息

修改输入节点type和名称

插入Cast节点/算子

size算子

插入range算子

增加、删除输入输出节点

pad算子

插入Slice算子

Squeeze算子

shape算子

Sub算子

Concat算子

ConstantOfShape算子

【参考】


一、ONNX简介

官方介绍,开放神经网络交换(Open Neural Network Exchange)简称ONNX,是微软和Facebook提出用来表示深度学习模型的开放格式。所谓开放就是ONNX定义了一组和环境,平台均无关的标准格式,来增强各种AI模型的可交互性。

换句话说,无论你使用何种训练框架训练模型(比如TensorFlow/Pytorch/…),在训练完毕后都可以将这些框架的模型统一转换为ONNX这种统一的格式进行存储ONNX文件不仅仅存储了神经网络模型的权重,同时也存储了模型的结构信息以及网络中每一层的输入输出和一些其它的辅助信息。

二、ONNX结构分析

ONNX是把一个网络的每一层或者说一个算子当成节点 node使用这些 Node去构建一个 Graph即一个网络。最后将 Graph其它的生产者信息,版本信息等合并在一起生成一个 Model也即是最终的ONNX模型文件。在构建ONNX模型的时候,https://github.com/onnx/onnx/blob/master/onnx/helper.py

这个文件需要划重点,其中make_nodemake_graphmake_model是不可或缺的。make_tensor_value_infomake_tensor是构建graph中所需要用到的

make_tensor_value_info(name,elem_type,shape)

name: 节点名字 [类型:字符串]
elem_type: 数据类型 [类型:TensorProto.DataType]
shape: 数据维度(形状) [类型:int列表/元组]

model = onnx.load("./bert.onnx")
# 给模型插入一个冗余的输入节点
ori_segment_ids = onnx.helper.make_tensor_value_info("ori_segment_ids_ph:0", 6, shape=[None, None])
model.graph.input.append(ori_segment_ids)

make_node(op_type, inputs, outputs, name=None)

op_type: 节点的算子类型 [类型:字符串],详细可以参考onnx给出的算子列表
inputs: 存放节点输入的名字 [类型:字符串列表],每个节点输入的数量根据情况会有不同
outputs: 存放节点输出的名字 [类型:字符串列表],与inputs类似,同样需要根据官网给出的输出个数来设置,大多数情况是一个输出
name: 节点名,可有可无,不要和op_type搞混了

# 用于扩充维度   1*100   →  1*1*100
unsqueeze_node = onnx.helper.make_node(op_type="Unsqueeze",inputs=["insert_unsqueeze1"],outputs=["insert_unsqueeze"],axes=[0],
)

ONNX 对节点的输入要求:一个节点的输入,要么是整个模型的输入,要么是之前某个节点的输出

make_graph(nodes,name,inputs,outputs,initializer=None)

nodes: 用make_node生成的节点列表 [类型:NodeProto列表]
name: graph的名字 [类型:字符串]
inputs: 存放graph的输入数据信息 [类型:ValueInfoProto列表]
输入数据的信息以ValueInfoProto的形式存储,会用到make_tensor_value_info,来将输入数据的名字、数据类型、形状(维度)给记录下来。对于一个网络而言如何能体现其网络结构呢?即节点与节点之间的关联。在构建每一个node时就需要注意,当前node的输入来自于哪一个node的输出,名字要匹配上,才能将node间联系体现出来。
outputs: 存放graph的输出数据信息 [类型:ValueInfoProto列表],与inputs相同。
initializer: 存放超参数 [类型:TensorProto列表],对于一个多层网络而言,其中间层的输入有来自上一层的输出,也有来自外界的超参数和数据。 initializer作为存放超参数具体数值的TensorProto列表,其中每个TensorProto总会有与其对应的ValueInfoProto存在,对应关系通过name来联系。

input1 = np.random.rand(1, 3, 4, 5).astype("float32")
input2 = np.random.rand(1, 5).astype("float32")
inputs = [helper.make_tensor_value_info("input1", TensorProto.FLOAT, shape=(1, 3, 4, 5)),helper.make_tensor_value_info("input2", TensorProto.FLOAT, shape=(1, 5))]outputs = [helper.make_tensor_value_info("output", TensorProto.FLOAT, shape=(1, 3, 4, 5))]nodes = [helper.make_node("Add", ["input1", "input2"], ["output"])]graph = helper.make_graph(nodes, "bcast_test", inputs, outputs)bcast_model = helper.make_model(graph)

其中,make_graph 的节点参数要求:计算图的节点必须以拓扑序给出

make_model(graph, **kwargs)

graph: 用make_graph生成的GraphProto

make_tensor(name,data_type,dims,vals)

name: 数据名字,要与该数据的信息tensor value info中名字对应 [类型:字符串]
data_type: 数据类型 [类型:TensorProto.DataType] 如TensorProto.FLOAT、TensorProto.UINT8、TensorProto.FLOAT16等
dims: 数据维度 [类型:int列表/元组]
vals: 数据值,好像要可迭代的 [类型:任意]raw:选择是否用二进制编码 [类型:bool]

tensor = make_tensor('test_tensor', onnx.TensorProto.FLOAT, [1], [1])

make_attribute(key,value)

key: 键值 [类型:字符串]
value: 数值 [类型:任意]

attr = helper.make_attribute("int", 5)

三、ONNX模型修改

大家都知道,模型结构优化一直以来都是比较fancy的工作,太fancy我么也做不了,所以我们今天只简单地修改一下模型结构

合并两个ONNX模型

import onnx
model_1 = onnx.load(file1)
onnx.compose.expand_out_dim(model_1, dim_idx=0, inplace=True)#用于扩展维度  [None,None] [1,None,None]
model_2 = onnx.load(file2)
combined_model=onnx.compose.merge_models(model_1, model_2, io_map=[('output:0', 'input:0')])    # 分别为两个模型中待合并的节点

查看ONNX节点信息

import onnx
model = onnx.load("./bert.onnx")
outputs = model.graph.output   # 输出节点
# input_node = model.graph.input   # 输入节点
# nodes = model.graph.node  # 所有节点
print(outputs )_________输出信息如下_________
[name: "label_score:0"
type {tensor_type {elem_type: 1shape {dim {dim_param: "unk__403"}dim {dim_value: 2}}}
}]
__________输出信息__________

其中,elem_type为输出节点的类型,数据类型(elem_type)共有16种,

elem_type: 1 --> float32

elem_type: 2 --> uint8

elem_type: 3 --> int8

elem_type: 4 --> uint16

elem_type: 5 --> int16

elem_type: 6 --> int32

elem_type: 7 --> int64

elem_type: 8 --> string

elem_type: 9 --> boolean

elem_type: 10 --> float16

elem_type: 11 --> float64

elem_type: 12 --> uint32

elem_type: 14 --> uint64

elem_type: 15 --> complex128

elem_type: 16 --> bfloat16      其余的数字全为undefined

修改输入节点type和名称

model = onnx.load("./bert.onnx")
input_node = model.graph.input
for i in range(len(input_node)):if input_node[i].name == "ori_input_quests:0":model.graph.input[i].type.tensor_type.elem_type = 6
model.graph.input[0].name = "proj/cond_1/Merge:0"   #修改输入节点名字nodes = model.graph.node
length = len(nodes)
for i in range(length):if nodes[i].name == "cond_If__38":model.graph.node[i].output[0] = "proj/cond_1/Merge:0"  # 修改节点名字

插入Cast节点/算子

通过算子原型构建Graph时,要求前后算子的dtype必须一致,上一个算子的输出dtype如果和下一层算子的输入dtype不匹配时,需要插入Cast算子

model = onnx.load("./bert.onnx")
nodes = model.graph.node
for i in range(len(nodes)):if nodes[i].output[0] == "_not_equal_scalar0_eq":index = i   # 记录节点idbreak
else:raise ValueError("find insert_node error!!!")
cast_node = onnx.helper.make_node(op_type="Cast",inputs=["ori_input_quests:0"],outputs=["insert_cast_0"],to=getattr(onnx.TensorProto, "FLOAT")
)
model.graph.node.insert(index, cast_node)  # 插入节点
model.graph.node[index + 1].input[0] = "insert_cast_0"  # 指向插入的节点

size算子

将张量作为输入并输出一个 int64 标量,该标量等于输入张量的元素总数

size_node = onnx.helper.make_node(op_type="Size",inputs=["ori_input_quests:0"],outputs=["insert_Size"],
)

插入range算子

Range算子类似python的range()函数

model = onnx.load("./bert.onnx")
node = model.graph.node
for i in range(len(node)):if node[i].output[0] == "bert/embeddings/position_embeddings_indices_casted":index = i  # 搜索节点所在的网络idbreak
else:raise ValueError("查找节点失败")initializer = model.graph.initializer  # ONNX初始化值
start_const = onnx.helper.make_tensor(name='start_const',data_type=6,dims=[],vals=[0])
# 初始化 常量 0和1
step_const = onnx.helper.make_tensor(name='step_const',data_type=6,dims=[],vals=[1])
# 将生成的tensor插入途中
model.graph.initializer.append(start_const)
model.graph.initializer.append(step_const)
range_node = onnx.helper.make_node(op_type="Range",inputs=["start_const", "insert_Size", "step_const"],outputs=["src_position"],
)
# 若"insert_Size"=10,则range之后生成[0,1,2,3,4,5,6,7,8,9]
model.graph.node.insert(index, range_node)
model.graph.node[index + 1].input[0] = "src_position"   # 插入节点指向下一个节点

增加、删除输入输出节点

model = onnx.load("./bert.onnx")
# 插入一个输入节点
ori_segment_ids = onnx.helper.make_tensor_value_info("ori_segment_ids_ph:0", 6, shape=[None, None])
model.graph.input.append(ori_segment_ids)# 删除
input_node = model.graph.input
for i in range(len(input_node)):if input_node[i].name == "src_positions":model.graph.input.remove(input_node[i])breakonnx.checker.check_model(model)  # 检测模型格式
onnx.save(model, "./new.onnx") # 保存模型

pad算子

有时候,已有的模型输入维度固定的,比如[1,100],但我们 想让输入支持可变长度的[1,None],需要用到padding  OP

实现方法:支持输入动态,而且模型里面的操作都是固定维度,需要先修改输入维度为动态,然后进步模型后在修改维度到固定值;

model = onnx.load("./bert.onnx")
inputs = model.graph.input
for i in range(len(inputs)):if inputs[i].name == "ori_input_quests:0":model.graph.input[i].type.tensor_type.elem_type = 6  # 修改输入类型model.graph.input[i].type.tensor_type.shape.dim[1].dim_param = "len"  # 修改输入维度为动态长度
input_len = 100  # 假设原始的输入长度 [1,100]
pads_value = onnx.helper.make_tensor(name='pads_value',data_type=6,dims=[],vals=[0])
pads_const = onnx.helper.make_tensor(name='pads_const',data_type=7,dims=[4],vals=[0, 0, 0, input_len])
# 用于padding, 结束位置补100个0  [1,200]  vals:第一个维度左边补0个数,第二个维度左边补0个数,第一个维度右边补0个数,第二个维度右边补0个数
# data =[
#     [1.0, 1.2],
#     [2.3, 3.4],
#     [4.5, 5.7],]
# pads = [0, 2, 0, 0]
# mode = 'constant'
# constant_value = 0.0
# output =[
#     [0.0, 0.0, 1.0, 1.2],
#     [0.0, 0.0, 2.3, 3.4],
#     [0.0, 0.0, 4.5, 5.7],]
start_const = onnx.helper.make_tensor(name='start_const',data_type=7,dims=[2],vals=[0, 0])
end_const = onnx.helper.make_tensor(name='end_const',data_type=7,dims=[2],vals=[1, input_len])
# 用于切片操作, 第一个维度[0:1]   第二个维度[0:100]
model.graph.initializer.append(pads_value)
model.graph.initializer.append(pads_const)
model.graph.initializer.append(start_const)
model.graph.initializer.append(end_const)pad_node = onnx.helper.make_node(op_type="Pad",inputs=["ori_input_quests:0", "pads_const", "pads_value"],outputs=["insert_pad"],mode='constant',
)
slice_node = onnx.helper.make_node("Slice",inputs=["insert_pad", "start_const", "end_const"],outputs=["insert_slice"],
)
nodes = model.graph.node
for i in range(len(nodes)):if nodes[i].output[0] == "bert/embeddings/word_embeddings_indices_casted":index = ibreak
else:raise ValueError("查找节点失败")model.graph.node.insert(index, pad_node)
model.graph.node.insert(index + 1, slice_node)
model.graph.node[index + 2].input[0] = "insert_slice"onnx.checker.check_model(model)  # 检测模型格式
from onnx import shape_inference
model = shape_inference.infer_shapes(model)   # 维度推断
onnx.save(model, "./new.onnx")   # 保存模型

插入Slice算子

类似python的切片操作

model = onnx.load("./bert.onnx")
start_const = onnx.helper.make_tensor(name='start_const',data_type=7,dims=[2],vals=[0, 0])
end_const_1 = onnx.helper.make_tensor(name='end_const_1',data_type=7,dims=[2],vals=[1, -1])
# 用于切片操作,第一个维度[0:1]   第二个维度[0:-1]  eg:维度 1*15 → 1*14
model.graph.initializer.append(start_const)
model.graph.initializer.append(end_const_1)slice_node = onnx.helper.make_node("Slice",inputs=["ori_input_quests:0", "start_const", "end_const_1"],outputs=["insert_slice_2"],
)
nodes = model.graph.node
for i in range(len(nodes)):if nodes[i].output[0] == "insert_pad":model.graph.node.insert(i, slice_node)model.graph.node[i + 1].input[0] = "insert_slice_2"break

Squeeze算子

维度1*1*100  需要 变为 1*100,可以用到Squeeze,相反的操作就是Unsqueeze,用于扩充维度

squeeze_node_1 = onnx.helper.make_node(op_type="Squeeze",inputs=["ori_input_quests:0"],outputs=["insert_Squeeze_1"],axes=[0],
)

shape算子

获取维度

size_node_1 = onnx.helper.make_node(op_type="Shape",inputs=["insert_Squeeze_1"],outputs=["insert_size_1"],
)# torch.tensor([[2, 3, 4], [3,4,5]])
# shape操作输出 (2, 3)

Sub算子

model = onnx.load("./bert.onnx")
sub_const = onnx.helper.make_tensor(name='sub_const',data_type=7,dims=[1],vals=[2])
model.graph.initializer.append(sub_const)
sub_contant = onnx.helper.make_node("Sub",inputs=["insert_size_1", "sub_const"],outputs=["insert_sub"],
)

Concat算子

将list of tensor合并成一个tensor

contact_node = onnx.helper.make_node(op_type="Concat",inputs=["insert_size_2", "insert_sub"],outputs=["insert_contact"],axis=0,
)

ConstantOfShape算子

生成具有给定值和形状的张量

constant_node = onnx.helper.make_node("ConstantOfShape",inputs=["insert_contact"],outputs=["insert_contant"],value=onnx.helper.make_tensor("value", onnx.TensorProto.FLOAT, [1], [1]),
)
# 根据inputs的维度生成全1的向量,eg[[1,1],[1,1]

注意:

如果新插入了一个节点,没有删除相应的节点,想要在保存模型checkmodel的时候通过就必须要保证,插入节点的insert位置就是在原先节点的位置,然后在输出节点之后的名字再进行修改,输入节点也要进行修改,就是onnx模型中的输入节点一定要在该节点之前存在,否则就会导致拓扑不可分,就会报错

【参考】

ONNX 算子文档:https://github.com/onnx/onnx/blob/main/docs/Operators.md#Unsqueeze

onnx源码:https://github.com/onnx/onnx/tree/main/onnx

ONNX常用函数:https://github.com/onnx/onnx/blob/main/docs/PythonAPIOverview.md

   

ONNX 模型图优化相关推荐

  1. 使用OpenVINO部署ONNX模型

    做深度学习神经网络的设计.训练与部署,往往会困扰于不同的操作系统,不同的深度学习框架,不同的部署硬件,以及不同的版本.由于相互之间的不兼容,给开发使用者造成了很大的不便. 联合使用OpenVINO和O ...

  2. 3dmax导出fbx没有贴图_SU教程 | 如何通过Skimp插件导入高质量模型、优化模型、匹配贴图?...

    作者:mind.sight.studios Skimp可以导放并简化SketchUp模型的任何对象, 并完美匹配贴图坐标. 支持FBX,OBJ,STL,DAE, 3DS,PLY和VRML格式文件的导入 ...

  3. onnx模型如何增加或者去除里面node,即修改图方法

    有时候我们通过pytorch导出onnx模型,需要修改一下onnx的图结构,怎么修改呢? 下面两个Python实例提供了修改思路. Changing the graph is easier than ...

  4. SketchUp教程 | 如何通过Skimp插件导入高质量模型、优化模型、匹配贴图?

    作者:mind.sight.studios Skimp可以导入并简化SketchUp模型的任何对象,并完美匹配贴图坐标. 支持FBX,OBJ,STL,DAE,3DS,PLY和VRML格式文件的导入. ...

  5. ONNX 实时graph优化方法

    ONNX 实时graph优化方法 ONNX实时提供了各种图形优化来提高模型性能.图优化本质上是图级别的转换,从小型图简化和节点消除,到更复杂的节点融合和布局优化. 图形优化根据其复杂性和功能分为几个类 ...

  6. NVIDIA GPUs上深度学习推荐模型的优化

    NVIDIA GPUs上深度学习推荐模型的优化 Optimizing the Deep Learning Recommendation Model on NVIDIA GPUs 推荐系统帮助人在成倍增 ...

  7. onnx模型部署(一) ONNXRuntime

    通常我们在训练模型时可以使用很多不同的框架,比如有的同学喜欢用 Pytorch,有的同学喜欢使用 TensorFLow,也有的喜欢 MXNet,以及深度学习最开始流行的 Caffe等等,这样不同的训练 ...

  8. java调用onnx模型_开源一年多的模型交换格式ONNX,已经一统框架江湖了?

    原标题:开源一年多的模型交换格式ONNX,已经一统框架江湖了? 机器之心原创 作者:思源 近日,微软亚洲研究院和华为举办了 ONNX 合作伙伴研讨会,这是 ONNX 开源社区成立以来首次在中国举办的活 ...

  9. 【地平线开发板 模型转换】将pytorch生成的onnx模型转换成.bin模型

    文章目录 1 获取onnx模型 2 启动docker容器 3 onnx模型检查 3.1 为什么要检查? 3.2 如何操作 4 图像数据预处理 4.1 一些问题的思考 4.2 图片挑选与放置 4.2 使 ...

最新文章

  1. 未来数据中心的选择:宽带多模光纤
  2. 事件ID 6038审核NTLM使用情况
  3. 继续- 管理百人研发团队的烦恼(下)
  4. 从Tensorflow代码中理解LSTM网络
  5. php+如何按文字分割文件夹,PHP实现的大文件切割与合并功能示例
  6. Map接口的实现类HashMap的操作
  7. java连接mysql数据库C3P0入门
  8. Jquery操作对控件的取值、赋值
  9. 使用 IntelliJ IDEA打包Spark应用程序
  10. day22 正则表达式 re
  11. mac+ffmpeg+php,mac折腾安装ffmpeg小记
  12. stata 空间杜宾模型_一文读懂空间计量及stata应用(二)(附lr检验、动态空间面板杜宾/滞后模型dofile等)...
  13. RuntimeError: mat1 and mat2 shapes cannot be multiplied
  14. 企业提高客户保持率基本方法
  15. 计算机显示usb无法识别,计算机不断弹出无法识别的USB设备的解决方案
  16. [darknet源码系列-3] 在darknet中,如何根据解析出来的配置进行网络层构建
  17. 腾讯背水一战,视频号底牌尽出
  18. vue实现星级评价及上传多张图片等功能(类似淘宝商品评价页面)
  19. CSS3 径向(圆形)渐变 (radial-gradient)
  20. Python——内置库函数

热门文章

  1. AI在医疗影像设备全流程应用
  2. 【CentOS】IBM X3650M4 IMM远程管理【转载】
  3. (附源码)计算机毕业设计SSM基于web的企业人事管理系统
  4. 女生计算机考研方向有哪些 就业前景怎么样,女生计算机考研方向有哪些 就业前景怎么样...
  5. AI聊天机器人接口制作php,使用图灵api创建微信聊天机器人_php技巧
  6. Python小游戏——Pygame制作2048小游戏
  7. QT:SQLite数据库 '增、删、改、查'
  8. Excel的内容进行比较工具:Beyond Compare 3.X
  9. DataBase Tablespace
  10. 网站接入第三方微博登录—PHP