onnx作为一个通用格式,很少有中文教程,因此开一篇文章对onnx 1.16文档进行翻译与进一步解释,
onnx 1.16官方文档:https://onnx.ai/onnx/intro/index.html](https://onnx.ai/onnx/intro/index.html),
如果觉得有收获,麻烦点赞收藏关注,目前仅在CSDN发布,本博客会分为多个章节,目前尚在连载中,详见专栏链接:
https://blog.csdn.net/qq_33345365/category_12581965.html
开始编辑时间:2024/2/21;最后编辑时间:2024/2/21
这是本教程的第四篇,其余内容见上述专栏链接。
ONNX with Python
本教程的第一篇:介绍了ONNX的基本概念。
在本教程的第二篇,介绍了ONNX关于Python的API,具体涉及一个简单的线性回归例子和序列化。
本教程的第三篇,包括python API的三个部分:初始化器Initializer;属性Attributes;算子集和元数据Opset和Metadata
本教程的第四篇,包括子图的两个内容,使用If和Scan算子实现子图的选择和循环。
在本篇中,会介绍以下内容:
- 函数
- 不带属性的函数
- 带有属性的函数
- 模型解析:onnx提供了一种简单定义图的方式,可以实现快速构图,并不常用。
- 检查器与形状推理 checker and shape inference
- 检查器:检查模型是否有效
- 形状推理:估计中间结果的形状和种类
函数
正如前一章所述,函数可以用来缩短构建模型的代码,并为运行时预测提供更多可能性,如果存在特定函数实现,可以使其运行更快。如果没有特定实现,运行时仍然可以基于现有算子使用默认实现。
make_function
函数用于定义一个函数。它就像一个简化类型的图,更像是一个模板。这个 API 可能会有所演变,它也不包含初始化器。
没有属性的函数
这是更简单的情况,函数的每个输入都是在执行时已知的动态对象。
import numpy
from onnx import numpy_helper, TensorProto
from onnx.helper import (make_model, make_node, set_model_props, make_tensor,make_graph, make_tensor_value_info, make_opsetid,make_function)
from onnx.checker import check_modelnew_domain = 'custom'
opset_imports = [make_opsetid("", 14), make_opsetid(new_domain, 1)]# 定义一个线程回归的函数
node1 = make_node('MatMul', ['X', 'A'], ['XA'])
node2 = make_node('Add', ['XA', 'B'], ['Y'])linear_regression = make_function(new_domain, # domain name'LinearRegression', # function name['X', 'A', 'B'], # input names['Y'], # output names[node1, node2], # nodesopset_imports, # opsets[]) # attribute names# 在没有变量前,函数是可以构建的
X = make_tensor_value_info('X', TensorProto.FLOAT, [None, None])
A = make_tensor_value_info('A', TensorProto.FLOAT, [None, None])
B = make_tensor_value_info('B', TensorProto.FLOAT, [None, None])
Y = make_tensor_value_info('Y', TensorProto.FLOAT, [None])# 函数会作为算子来构建节点
graph = make_graph([make_node('LinearRegression', ['X', 'A', 'B'], ['Y1'], domain=new_domain),make_node('Abs', ['Y1'], ['Y'])],'example',[X, A, B], [Y])onnx_model = make_model(graph, opset_imports=opset_imports,functions=[linear_regression]) # 新增函数需要添加这个参数
check_model(onnx_model)print(onnx_model)
输出为:
ir_version: 10
graph {node {input: "X"input: "A"input: "B"output: "Y1"op_type: "LinearRegression"domain: "custom"}node {input: "Y1"output: "Y"op_type: "Abs"}name: "example"input {name: "X"type {tensor_type {elem_type: 1shape {dim {}dim {}}}}}input {name: "A"type {tensor_type {elem_type: 1shape {dim {}dim {}}}}}input {name: "B"type {tensor_type {elem_type: 1shape {dim {}dim {}}}}}output {name: "Y"type {tensor_type {elem_type: 1shape {dim {}}}}}
}
opset_import {domain: ""version: 14
}
opset_import {domain: "custom"version: 1
}
functions {name: "LinearRegression"input: "X"input: "A"input: "B"output: "Y"node {input: "X"input: "A"output: "XA"op_type: "MatMul"}node {input: "XA"input: "B"output: "Y"op_type: "Add"}opset_import {domain: ""version: 14}opset_import {domain: "custom"version: 1}domain: "custom"
}
带有属性的函数
下面的函数与之前的函数几乎完全相同,只是将一个名为 B 的输入转换为名为 bias 的参数。函数内部,创建了一个 Constant 节点,将参数作为结果插入。它通过 ref_attr_name 属性与参数相连。
输入和属性之间存在着明确的区别。输入是动态的,可能在每次执行时都发生变化。而属性是静态的,永远不会改变。优化器可以假设属性不会改变来改善执行图。因此,将输入转换为属性是不可能的。唯一可以将属性转换为输入的操作符是常量操作符。
import numpy
from onnx import numpy_helper, TensorProto, AttributeProto
from onnx.helper import (make_model, make_node, set_model_props, make_tensor,make_graph, make_tensor_value_info, make_opsetid,make_function)
from onnx.checker import check_modelnew_domain = 'custom'
opset_imports = [make_opsetid("", 14), make_opsetid(new_domain, 1)]# Let's define a function for a linear regression
# The first step consists in creating a constant
# equal to the input parameter of the function.
cst = make_node('Constant', [], ['B'])att = AttributeProto()
att.name = "value"# This line indicates the value comes from the argument
# named 'bias' the function is given.
att.ref_attr_name = "bias"
att.type = AttributeProto.TENSOR
cst.attribute.append(att)node1 = make_node('MatMul', ['X', 'A'], ['XA'])
node2 = make_node('Add', ['XA', 'B'], ['Y'])linear_regression = make_function(new_domain, # domain name'LinearRegression', # function name['X', 'A'], # input names['Y'], # output names[cst, node1, node2], # nodesopset_imports, # opsets["bias"]) # attribute namesX = make_tensor_value_info('X', TensorProto.FLOAT, [None, None])
A = make_tensor_value_info('A', TensorProto.FLOAT, [None, None])
B = make_tensor_value_info('B', TensorProto.FLOAT, [None, None])
Y = make_tensor_value_info('Y', TensorProto.FLOAT, [None])graph = make_graph([make_node('LinearRegression', ['X', 'A'], ['Y1'], domain=new_domain,# bias是一个函数参数,1是维度,0.67是值bias=make_tensor('former_B', TensorProto.FLOAT, [1], [0.67])), make_node('Abs', ['Y1'], ['Y'])],'example',[X, A], [Y])onnx_model = make_model(graph, opset_imports=opset_imports,functions=[linear_regression]) # functions to add)
check_model(onnx_model)print(onnx_model)
输出是:
ir_version: 10
graph {node {input: "X"input: "A"output: "Y1"op_type: "LinearRegression"attribute {name: "bias"t {dims: 1data_type: 1float_data: 0.6700000166893005name: "former_B"}type: TENSOR}domain: "custom"}node {input: "Y1"output: "Y"op_type: "Abs"}name: "example"input {name: "X"type {tensor_type {elem_type: 1shape {dim {}dim {}}}}}input {name: "A"type {tensor_type {elem_type: 1shape {dim {}dim {}}}}}output {name: "Y"type {tensor_type {elem_type: 1shape {dim {}}}}}
}
opset_import {domain: ""version: 14
}
opset_import {domain: "custom"version: 1
}
functions {name: "LinearRegression"input: "X"input: "A"output: "Y"attribute: "bias"node {output: "B"op_type: "Constant"attribute {name: "value"type: TENSORref_attr_name: "bias"}}node {input: "X"input: "A"output: "XA"op_type: "MatMul"}node {input: "XA"input: "B"output: "Y"op_type: "Add"}opset_import {domain: ""version: 14}opset_import {domain: "custom"version: 1}domain: "custom"
}
解析 Parsing
onnx 模块提供了一种更快速定义计算图并使其更易读的方法。当计算图在单个函数中构建时,这种方式非常方便易用,但当计算图由许多不同的函数构建,每个函数转换机器学习管道的一部分时,就没那么容易了。
import onnx.parser
from onnx.checker import check_modelinput = '''<ir_version: 8,opset_import: [ "" : 15]>agraph (float[I,J] X, float[I] A, float[I] B) => (float[I] Y) {XA = MatMul(X, A)Y = Add(XA, B)}'''
onnx_model = onnx.parser.parse_model(input)
check_model(onnx_model)print(onnx_model)
输出是:
ir_version: 8
graph {
node {input: "X"input: "A"output: "XA"op_type: "MatMul"domain: ""
}
node {input: "XA"input: "B"output: "Y"op_type: "Add"domain: ""
}
name: "agraph"
input {name: "X"type {tensor_type {elem_type: 1shape {dim {dim_param: "I"}dim {dim_param: "J"}}}}
}
input {name: "A"type {tensor_type {elem_type: 1shape {dim {dim_param: "I"}}}}
}
input {name: "B"type {tensor_type {elem_type: 1shape {dim {dim_param: "I"}}}}
}
output {name: "Y"type {tensor_type {elem_type: 1shape {dim {dim_param: "I"}}}}
}
}
opset_import {
domain: ""
version: 15
}
这是一种创建小型模型的方法,但很少用于转换库。
检查器与形状推理Checker and Shape Inference
onnx 提供了一个用于检查模型是否有效的函数。它会在能检测到不一致性时检查输入类型或形状。以下示例演示了添加两个不同类型的矩阵,这是不允许的。
import onnx.parser
import onnx.checkerinput = '''<ir_version: 8,opset_import: [ "" : 15]>agraph (float[I,4] X, float[4,2] A, int[4] B) => (float[I] Y) {XA = MatMul(X, A)Y = Add(XA, B)}'''
try:onnx_model = onnx.parser.parse_model(input)onnx.checker.check_model(onnx_model)
except Exception as e:print(e)
输出是:
b'[ParseError at position (line: 6 column: 44)]\nError context: agraph (float[I,4] X, float[4,2] A, int[4] B) => (float[I] Y) {\nExpected character ) not found.'
check_model
由于遇到不一致性而引发错误。它适用于所有在主域或 ML 域中定义的操作符。对于任何未在任何规范中定义的自定义操作符,它不会发出任何警告。
形状推断只有一个目的:估计中间结果的形状和类型。如果已知,运行时可以预先估计内存消耗并优化计算。它可以融合一些操作符,也可以进行原地计算…
import onnx.parser
from onnx import helper, shape_inferenceinput = '''<ir_version: 8,opset_import: [ "" : 15]>agraph (float[I,4] X, float[4,2] A, float[4] B) => (float[I] Y) {XA = MatMul(X, A)Y = Add(XA, B)}'''
onnx_model = onnx.parser.parse_model(input)
inferred_model = shape_inference.infer_shapes(onnx_model)print(inferred_model)
输出为:
ir_version: 8
graph {node {input: "X"input: "A"output: "XA"op_type: "MatMul"domain: ""}node {input: "XA"input: "B"output: "Y"op_type: "Add"domain: ""}name: "agraph"input {name: "X"type {tensor_type {elem_type: 1shape {dim {dim_param: "I"}dim {dim_value: 4}}}}}input {name: "A"type {tensor_type {elem_type: 1shape {dim {dim_value: 4}dim {dim_value: 2}}}}}input {name: "B"type {tensor_type {elem_type: 1shape {dim {dim_value: 4}}}}}output {name: "Y"type {tensor_type {elem_type: 1shape {dim {dim_param: "I"}}}}}value_info { # 新增加的name: "XA"type {tensor_type {elem_type: 1shape {dim {dim_param: "I"}dim {dim_value: 2}}}}}
}
opset_import {domain: ""version: 15
}
添加了一个新的属性值_info来存储推断的形状。dim_param中的字母I可以看作是一个变量。它取决于输入,但函数能够判断哪个中间结果会共享相同的维度。形状推理并不总是有效的。例如,Reshape算子。形状推理只有在形状是常量的情况下才有效。如果不是常量,则形状很难推断,除非后面的节点期望特定的形状。