【AI】ONNX

长期更新,建议收藏关注!

友情链接
Netron

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

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

onnx的官方模型仓库拉一个yolov3-tiny的onnx模型(地址为:https://github.com/onnx/models/tree/master/vision/object_detection_segmentation/tiny-yolov3/model)用Netron可视化一下看看ONNX模型长什么样子。(本blog会有一篇专门的文章讲Netron)

在获得ONNX模型之后,模型部署人员自然就可以将这个模型部署到兼容ONNX的运行环境中去。这里一般还会设计到额外的模型转换工作,典型的比如在Android端利用NCNN部署ONNX格式模型,那么就需要将ONNX利用NCNN的转换工具转换到NCNN所支持的bin和param格式。
但在实际使用ONNX的过程中,大多数人对ONNX了解得并不多,仅仅认为它只是一个完成模型转换和部署工具人而已,我们可以利用它完成模型转换和部署。
**OP:output

Protobuf

ONNX作为一个文件格式,我们自然需要一定的规则去读取我们想要的信息或者是写入我们需要保存信息。ONNX使用的是Protobuf这个序列化数据结构去存储神经网络的权重信息。
熟悉Caffe或者Caffe2的同学应该知道,它们的模型存储数据结构协议也是Protobuf。

Protobuf是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

Protobuf协议是一个以*.proto后缀文件为基础的,这个文件描述了用户自定义的数据结构。ONNX是基于Protobuf来做数据存储和传输,那么自然onnx.proto就是ONNX格式文件了。

组织格式

ONNX的组织格式,上面提到ONNX中最核心的部分就是onnx.proto(https://github.com/onnx/onnx/blob/master/onnx/onnx.proto)这个文件,它定义了ONNX这个数据协议的规则和一些其它信息。

这个文件有很多行,我们没有必要把这个文件里面的每一行都贴出来,我们只要搞清楚里面的核心部分即可。在这个文件里面以message关键字开头的对象是我们需要关心的。我们列一下最核心的几个对象并解释一下它们之间的关系。
ModelProto
GraphProto
NodeProto
ValueInfoProto
TensorProto
AttributeProto

当我们加载了一个ONNX之后,我们获得的就是一个ModelProto,它包含了一些版本信息,生产者信息和一个GraphProto。在GraphProto里面又包含了四个repeated数组,它们分别是node(NodeProto类型),input(ValueInfoProto类型),output(ValueInfoProto类型)和initializer(TensorProto类型),其中node中存放了模型中所有的计算节点,input存放了模型的输入节点,output存放了模型中所有的输出节点,initializer存放了模型的所有权重参数。

我们知道要完整的表达一个神经网络,不仅仅要知道网络的各个节点信息,还要知道它们的拓扑关系。这个拓扑关系在ONNX中是如何表示的呢?ONNX的每个计算节点都会有input和output两个数组,这两个数组是string类型,通过input和output的指向关系,我们就可以利用上述信息快速构建出一个深度学习模型的拓扑图。

这里要注意一下,GraphProto中的input数组不仅包含我们一般理解中的图片输入的那个节点,还包含了模型中所有的权重。例如,Conv层里面的W权重实体是保存在initializer中的,那么相应的会有一个同名的输入在input中,其背后的逻辑应该是把权重也看成模型的输入,并通过initializer中的权重实体来对这个输入做初始化,即一个赋值的过程。

最后,每个计算节点中还包含了一个AttributeProto数组,用来描述该节点的属性,比如Conv节点或者说卷积层的属性包含group,pad,strides等等。

每一个计算节点的属性,输入输出信息都详细记录在https://github.com/onnx/onnx/blob/master/docs/Operators.md。

onnx.helper

ONNX是把一个网络的每一层或者说一个算子当成节点node,使用这些Node去构建一个Graph,即一个网络。最后将Graph和其它的生产者信息,版本信息等合并在一起生成一个Model,也即是最终的ONNX模型文件。
https://github.com/onnx/onnx/blob/master/onnx/helper.py这个文件非常重要,我们可以利用它提供的make_node,make_graph,make_tensor等等接口完成一个ONNX模型的构建,示例如下:

import onnx
from onnx import helper
from onnx import AttributeProto, TensorProto, GraphProto# The protobuf definition can be found here:
# https://github.com/onnx/onnx/blob/master/onnx/onnx.proto# Create one input (ValueInfoProto)
X = helper.make_tensor_value_info('X', TensorProto.FLOAT, [3, 2])
pads = helper.make_tensor_value_info('pads', TensorProto.FLOAT, [1, 4])
value = helper.make_tensor_value_info('value', AttributeProto.FLOAT, [1])# Create one output (ValueInfoProto)
Y = helper.make_tensor_value_info('Y', TensorProto.FLOAT, [3, 4])# Create a node (NodeProto) - This is based on Pad-11
node_def = helper.make_node('Pad', # node name['X', 'pads', 'value'], # inputs['Y'], # outputsmode='constant', # attributes
)# Create the graph (GraphProto)
graph_def = helper.make_graph([node_def],'test-model',[X, pads, value],[Y],
)# Create the model (ModelProto)
model_def = helper.make_model(graph_def, producer_name='onnx-example')print('The model is:\n{}'.format(model_def))
onnx.checker.check_model(model_def)
print('The model is checked!')

onnx-simplifier

以一个经典的Pytorch转ONNX的reshape问题为例子,来尝试讲解一下大老师的onnx-simplifier是怎么处理的,个人认为这个问题是基于ONNX进行模型部署最经典的问题。

#想把下面这段代码导出ONNX模型时:
import torchclass JustReshape(torch.nn.Module):def __init__(self):super(JustReshape, self).__init__()def forward(self, x):return x.view((x.shape[0], x.shape[1], x.shape[3], x.shape[2]))net = JustReshape()
model_name = 'just_reshape.onnx'
dummy_input = torch.randn(2, 3, 4, 5)
torch.onnx.export(net, dummy_input, model_name, input_names=['input'], output_names=['output'])#这个模型输入维度是固定的

在这里插入图片描述
实际上(用了ONNX的polished工具)得到的是

在这里插入图片描述
要解决这个问题,有两种方法,第一种是做一个强制类型转换,将x.shape[0]类似的变量强制转换为常量即int(x.shape[0]),或者使用大老师的onnx-simplifer来解决这一问题。

onnx-simplifer的核心思路就是利用onnxruntime推断一遍ONNX的计算图,然后使用常量输出替代冗余的运算OP。


def infer_shapes(model: onnx.ModelProto) -> onnx.ModelProto:
#相对于原始的ONNX模型,现在每一条线都新增了一个shape信息,代表它的前一个特征图的shape是怎样的。try:model = onnx.shape_inference.infer_shapes(model)except:passreturn modeldef check_and_update_input_shapes(model: onnx.ModelProto, input_shapes: TensorShapes) 
-> TensorShapes:input_names = get_input_names(model)if None in input_shapes:if len(input_names) == 1:input_shapes[input_names[0]] = input_shapes[None]del input_shapes[None]else:raise RuntimeError('The model has more than 1 inputs, please use the format "input_name:dim0,dim1,...,dimN" in --input-shape')for x in input_shapes:if x not in input_names:raise RuntimeError('The model doesn\'t have input named "{}"'.format(x))return input_shapesdef simplify(model: Union[str, onnx.ModelProto], check_n: int = 0, perform_optimization: bool = True,skip_fuse_bn: bool = False, input_shapes: Optional[TensorShapes] = None, skipped_optimizers: Optional[Sequence[str]] = None, skip_shape_inference=False ) \-> Tuple[onnx.ModelProto, bool]:if input_shapes is None:input_shapes = {}if type(model) == str:# 加载ONNX模型model = onnx.load(model)# 检查ONNX模型格式是否正确,图结构是否完整,节点是否正确等onnx.checker.check_model(model)# 深拷贝一份原始ONNX模型model_ori = copy.deepcopy(model)if not skip_shape_inference:# 获取ONNX模型中特征图的尺寸model = infer_shapes(model)input_shapes = check_and_update_input_shapes(model, input_shapes)#用来判断输入的格式是否正确以及输入模型是否存在所有的指定输入节点if perform_optimization:model = optimize(model, skip_fuse_bn, skipped_optimizers)const_nodes = get_constant_nodes(model)res = forward_for_node_outputs(model, const_nodes, input_shapes=input_shapes)const_nodes = clean_constant_nodes(const_nodes, res)model = eliminate_const_nodes(model, const_nodes, res)onnx.checker.check_model(model)if not skip_shape_inference:model = infer_shapes(model)if perform_optimization:model = optimize(model, skip_fuse_bn, skipped_optimizers)check_ok = check(model_ori, model, check_n, input_shapes=input_shapes)return model, check_ok

如果我们指定input_shapes为:{‘input’: [2, 3, 4, 5]},那么这个函数的输出也为{‘input’: [2, 3, 4, 5]}。如果不指定,输出就是{}
确定了输入没有问题之后,程序会根据用户指定是否优化ONNX模型进入优化函数,函数定义如下:

#功能是对原始的ONNX模型做一些图优化工作,比如merge_bn,fuse_add_bias_into_conv等等
def optimize(model: onnx.ModelProto, skip_fuse_bn: bool, skipped_optimizers: Optional[Sequence[str]]) -> onnx.ModelProto:""":model参数: 待优化的ONXX模型.:return: 优化之后的ONNX模型.简化之前, 使用这个方法产生会在'forward_all'用到的ValueInfo简化之后,使用这个方法去折叠前一步产生的常量到initializer中并且消除没被使用的常量"""onnx.checker.check_model(model)onnx.helper.strip_doc_string(model)optimizers_list = ['eliminate_deadend','eliminate_nop_dropout','eliminate_nop_cast','eliminate_nop_monotone_argmax', 'eliminate_nop_pad','extract_constant_to_initializer', 'eliminate_unused_initializer','eliminate_nop_transpose','eliminate_nop_flatten', 'eliminate_identity','fuse_add_bias_into_conv','fuse_consecutive_concats','fuse_consecutive_log_softmax','fuse_consecutive_reduce_unsqueeze', 'fuse_consecutive_squeezes','fuse_consecutive_transposes', 'fuse_matmul_add_bias_into_gemm','fuse_pad_into_conv', 'fuse_transpose_into_gemm', 'eliminate_duplicate_initializer']if not skip_fuse_bn:optimizers_list.append('fuse_bn_into_conv')if skipped_optimizers is not None:for opt in skipped_optimizers:try:optimizers_list.remove(opt)except ValueError:passmodel = onnxoptimizer.optimize(model, optimizers_list,fixed_point=True)onnx.checker.check_model(model)return model

使用onnx.save保存一下这个例子中图优化后的模型,相比optimize前的模型多出了一些initializer数组,这些数组存储的就是这个图中那些常量OP的具体值,通过这个处理我们就可以调用get_constant_nodes函数来获取ONNX模型的常量OP了

def get_constant_nodes(m: onnx.ModelProto) -> List[onnx.NodeProto]:const_nodes = []# 如果节点的name在ONNX的GraphProto的initizlizer数组里面,它就是静态的tensorconst_tensors = [x.name for x in m.graph.initializer]# 显示的常量OP也加进来const_tensors.extend([node.output[0]for node in m.graph.node if node.op_type == 'Constant'])# 一些节点的输出shape是由输入节点决定的,我们认为这个节点的输出shape并不是常量,#所以我们不需要简化这种节点dynamic_tensors = []# 判断是否为动态OPdef is_dynamic(node):if node.op_type in ['NonMaxSuppression', 'NonZero', 'Unique'] and node.input[0] not in const_tensors:return Trueif node.op_type in ['Reshape', 'Expand', 'Upsample', 'ConstantOfShape'] and len(node.input) > 1 and node.input[1] not in const_tensors:return Trueif node.op_type in ['Resize'] and ((len(node.input) > 2 and node.input[2] not in const_tensors) or (len(node.input) > 3 and node.input[3] not in const_tensors)):return Truereturn Falsefor node in m.graph.node:if any(x in dynamic_tensors for x in node.input):dynamic_tensors.extend(node.output)elif node.op_type == 'Shape':const_nodes.append(node)const_tensors.extend(node.output)elif is_dynamic(node):dynamic_tensors.extend(node.output)elif all([x in const_tensors for x in node.input]):const_nodes.append(node)const_tensors.extend(node.output)# 深拷贝return copy.deepcopy(const_nodes)
#打印一下执行这个获取常量OP函数之后,Graph中有哪些OP被看成了常量OP。

获取了模型中所有的常量OP之后,我们需要把所有的静态节点扩展到ONNX Graph的输出节点列表中,然后利用onnxruntme执行一次forward:

def add_features_to_output(m: onnx.ModelProto, nodes: List[onnx.NodeProto]) -> None:"""Add features to output in pb, so that ONNX Runtime will output them.:param m: the model that will be run in ONNX Runtime:param nodes: nodes whose outputs will be added into the graph outputs"""# ONNX模型的graph扩展输出节点,获取所有静态OP的输出和原始输出节点的输出for node in nodes:for output in node.output:m.graph.output.extend([onnx.ValueInfoProto(name=output)])def forward_for_node_outputs(model: onnx.ModelProto, nodes: List[onnx.NodeProto],input_shapes: Optional[TensorShapes] = None) -> Dict[str, np.ndarray]:if input_shapes is None:input_shapes = {}model = copy.deepcopy(model)# nodes 是Graph中所有的静态OPadd_features_to_output(model, nodes)res = forward(model, input_shapes=input_shapes)return resdef clean_constant_nodes(const_nodes: List[onnx.NodeProto], res: Dict[str, np.ndarray]):"""It seems not needed since commit 6f2a72, but maybe it still prevents some unknown bug:param const_nodes: const nodes detected by `get_constant_nodes`:param res: The dict containing all tensors, got by `forward_all`:return: The constant nodes which have an output in res"""return [node for node in const_nodes if node.output[0] in res]
/*
这个函数是用来清洗那些没有被onnxruntime推理的静态节点,但通过上面的optimize逻辑,我们的graph中其实已经不存在这个情况了(没有被onnxruntime推理的静态节点在图优化阶段会被优化掉),因此这个函数理论上是可以删除的。这个地方是为了避免删除掉有可能引发其它问题就保留了。
不过从一些实际经验来看还是应保留吧,毕竟不能保证ONNX的图优化就完全正确
*/

最后的forward函数就是利用onnxruntime推理获得我们指定的输出节点的值。这个函数这里不进行解释。推理完成之后,进入下一个函数clean_constant_nodes

onnx-simplifier最核心的步骤:将常量节点从原始的ONNX Graph中移除,函数接口为eliminate_const_nodes

def eliminate_const_nodes(model: onnx.ModelProto, const_nodes: List[onnx.NodeProto],res: Dict[str, np.ndarray]) -> onnx.ModelProto:""":model参数: 原始ONNX模型:const_nodes参数: 使用`get_constant_nodes`获得的静态OP:res参数: 包含所有输出Tensor的字典:return: 简化后的模型. 所有冗余操作都已删除."""for i, node in enumerate(model.graph.node):if node in const_nodes:for output in node.output:new_node = copy.deepcopy(node)new_node.name = "node_" + outputnew_node.op_type = 'Constant'new_attr = onnx.helper.make_attribute('value',onnx.numpy_helper.from_array(res[output], name=output))del new_node.input[:]del new_node.attribute[:]del new_node.output[:]new_node.output.extend([output])new_node.attribute.extend([new_attr])insert_elem(model.graph.node, i + 1, new_node)del model.graph.node[i]return model

注意,这里获得的ONNX模型中虽然常量节点已经从Graph中断开了,即相当于这个DAG里面多了一些单独的点,但是这些点还是存在的。因此,我们再执行一次optimize就可以获得最终简化后的ONNX模型了。

BUG汇总

在这节会总结一些使用ONNX进行模型部署经常碰到一些因为版本兼容性,或者各种框架OP没有对齐等原因导致的各种BUG(以及目前碰到的无法解决的部分)。

  1. TensorRT存在一个BUG,这个结构的relu会被TensorRT的优化器给移动到eltwise之后
    解决方案:保留clean_constant_nodes函数

ONNX API的详细使用

和其他框架之间的转换

OneFlow模型导出ONNX进行部署

参考资料

本文仅为个人学习笔记免费分享。
【1】https://zhuanlan.zhihu.com/p/86867138
【2】https://oldpan.me/archives/talk-about-onnx
【3】https://blog.csdn.net/chengzi_comm/article/details/53199278
【4】https://www.jianshu.com/p/a24c88c0526a
【5】https://bindog.github.io/blog/2020/03/13/deep-learning-model-convert-and-depoly/
【6】 https://github.com/daquexian/onnx-simplifier
【7】https://mp.weixin.qq.com/s/bfYmjJYD3vxnyHohpvl54g

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/6507.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ASP.NET IIS Express一定vs停止调试,就退出了,如何不退出

》》》 在项目右击属性,找到Web,把启用”编辑并继续“ 复选框 去掉

asp.net结课作业中遇到的问题解决2

目录 1、如何实现评论交流的界面 2、如果想要将文字添加到数据库中,而不是乱码,该怎么修改 3、如果想要添加的数据已经存在于数据库,就不允许添加了,该如何实现 4、想要实现某个模块下有好几个小的功能该如何实现 5、想要实现…

Altium Designer入门基础操作

软件下载环境搭建:pan.baidu.com/s/1HshgKTmkkBpbIRa-9Wq9cQ 密码:ckck 工程建立: 创建 库绘制 为什么管脚要100mil 元素10mil 原理图库得正确性报告 原理图页设置大小,标准自定义,格点为100mil 使用库画原理图&a…

08 IRF技术 华三交换机实现

IRF 详细介绍 我知道 AI IRF 技术是指集成路由功能(Integrated Routing and Bridging)技术,是惠普(Hewlett Packard)公司开发的一种基于硬件的虚拟化技术。IRF 技术可以将多台物理设备组合成一个逻辑设备,实现设备的高可用性和灵活性。 IRF 技术主要有以下特点: 1. …

MySQL-集群1

一、为什么要用mysql集群?: mysql单体架构在企业中很少用,原因:①会形成单点故障,没有高可用的效果;②mysql本身是一个I/O能力比较差,并发能力比较差的应用服务,在较高规模的网络I/…

【计算机网络】循环冗余校验:Cyclic Redundancy Check

1. 任务目标 利用循环冗余校验(CRC)检测错误。 循环冗余校验(英语:Cyclic redundancy check,通称 CRC)是一种根据网上数据包或计算机文件等数据产生简短固定位数校验码的一种散列函数,主要用来…

谈谈Tcpserver开启多线程并发处理遇到的问题!

最近在学习最基础的socket网络编程,在Tcpserver开启多线程并发处理时遇到了一些问题! 说明 在linux以及Windows的共享文件夹进行编写的,所以代码中有的部分使用 #ifdef WIN64 ... #else ... #endif 进入正题!!&…

OSPF优化

OSPF的优化主要目的是为了减少LSA的更新量 路由汇总-----可以减少骨干区域的LSA数量 特殊区域-----可以减少非骨干区域的LSA数量 OSPF路由汇总 域间路由汇总 域间路由汇总在ABR设备上进行操作 [GS-R2-ospf-1-area-0.0.0.1]abr-summary 192.168.0.0 255.255.224.0 [GS-R3-o…

NEO 学习之session7

文章目录 选项 A:它涉及学习标记数据。 选项 B:它需要预定义的输出标签进行训练。 选项 C:它涉及在未标记的数据中寻找模式和关系。 选项 D:它专注于根据输入-输出对进行预测。 答案:选项 C 描述了无监督学习的本质&am…

服务器被攻击,为什么后台任务管理器无法打开?

在服务器遭受DDoS攻击后,当后台任务管理器由于系统资源耗尽无法打开时,管理员需要依赖间接手段来进行攻击类型的判断和解决措施的实施。由于涉及真实代码可能涉及到敏感操作,这里将以概念性伪代码和示例指令的方式来说明。 判断攻击类型 步…

mac查看Linux服务器的性能

mac上安装 linux系统 如果有 linux服务器账号密码,那么上一部可忽略; 比如:直接连接阿里云或腾讯云账号 1. 安装termius 链接: https://pan.baidu.com/s/1iYsZPZThPizxqtkLPT89-Q?pwdbw6j 提取码: bw6j 官网 Termius - SSH platform for …

c3 笔记8 css排版技巧

相关内容:边界、边框、位置(absolute、relative、static)、overflow、z-index、超链接、鼠标光标特效、…… margin:上边界值 右边界值 下边界值 左边界值 笔记来源: ©《HTML5CSS3JavaScript网页设计》陈婉凌编&#xff…

腾讯正式推出视频号小店,24年做电商,这次机会一定要抓住

大家好,我是电商笨笨熊 作为一个电商六年多的老玩家,从闲鱼到天猫,从天猫到抖店; 抖音小店这个项目,我做了四年多的时间,从寂寂无名到现在多人团队,皆因在抖店风口期抓住了这个项目。 而这次…

【深耕 Python】Quantum Computing 量子计算机(2)绘制电子运动平面波

写在前面 往期量子计算机博客: 【深耕 Python】Quantum Computing 量子计算机(1)图像绘制基础 一、所需公式 1、自由空间中电子的波函数公式: 2、常量代换: 3、物理常量: 二、Python代码: …

SpringBoot实现Config下自动关联.xml、.properties配置信息的实例教程

本篇文章主要讲解在SpringBoot实现Config下自动关联.xml、.properties配置信息的实例教程。 日期:2024年5月4日 作者:任聪聪 .properties文件调用方法 步骤一、打开我们的 .properties 创建一个demo参数如下图: 步骤二、创建一个config的包&…

【docker】maven 打包docker的插件学习

docker-maven-plugin GitHub地址:https://github.com/spotify/docker-maven-plugin 您可以使用此插件创建一个 Docker 映像,其中包含从 Maven 项目构建的工件。例如,Java 服务的构建过程可以输出运行该服务的 Docker 映像。 该插件是 Spot…

吴恩达深度学习笔记:深度学习的 实践层面 (Practical aspects of Deep Learning)1.11-1.12

目录 第二门课: 改善深层神经网络:超参数调试、正 则 化 以 及 优 化 (Improving Deep Neural Networks:Hyperparameter tuning, Regularization and Optimization)第一周:深度学习的 实践层面 (Practical aspects of Deep Learning)1.11 神经网络的权重…

xftp破解版?No!xftp平替开源工具✔

文章目录 一、背景说明二、WindTerm介绍三、简单使用说明3.1 新建一个ssh连接窗口![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/bfbe5114916e4a7e94ca0f9ceb05ca37.png)3.2 输入主机ip和端口号3.3 点击Continue3.4 输入密码3.5 登入成功3.6 下载文件到本地3.7 上…

网络安全之弱口令与命令爆破(下篇)(技术进阶)

目录 一,什么是弱口令? 二,为什么会产生弱口令呢? 三,字典的生成 四,九头蛇(hydra)弱口令爆破工具 1,破解ssh登录密码 2,破解windows登录密码 3&#xf…

展会进行时|百华鞋业亮相第135届中国进出口商品交易会(广交会)三期,展会现场人气爆棚!

第135届中国进出口商品交易会(广交会)三期如约而至,本届展会汇集了来自世界各地的参展企业,带来各行业前沿技术与新产品展出。百华鞋业携足部安防职业鞋、户外作训靴等系列新产品强势亮相展会,位于2.2 G25-26 H23-24的…