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/20;最后编辑时间:2024/2/21
ONNX 概念
ONNX 可以类比为一种专门用于数学函数的编程语言。它定义了机器学习模型执行推理功能所需的所有必要操作。线性回归可以用以下方式表示:
def onnx_linear_regressor(X): # 线性回归的表达式:y = ax + b
"ONNX code for a linear regression"
return onnx.Add(onnx.MatMul(X, coefficients), bias)
这个例子非常类似于一个开发人员可以用 Python 写的表达式。它也可以用一个图表来表示,该图表一步一步地展示了如何转换特征以获得预测。这就是为什么使用 ONNX 实现的机器学习模型经常被称为:ONNX图(ONNX Graph)。
ONNX 的目标是提供一种通用语言,任何机器学习框架都可以用来描述其模型。第一个场景是简化机器学习模型在生产环境中的部署。可以在部署环境中专门实现和优化 ONNX 解释器(或运行时)来完成这项任务。通过 ONNX,可以构建一个独特的流程,将模型部署到生产环境中,并且独立于构建模型所使用的学习框架。ONNX 实现了一个 Python 运行时,可以用来评估 ONNX 模型和评估 ONNX 操作。这旨在阐明 ONNX 的语义,并帮助理解和调试 ONNX 工具和转换器。此 Python 运行时并非用于生产环境,其性能也并非设计目标(请参见 onnx.reference)。
Input, Output, Node, Initializer, Attributes
本小节介绍五个概念的定义:
构建一个 ONNX 图意味着使用 ONNX 语言,或者更确切地说是使用ONNX Operators来实现一个函数。 线性回归可以用这种方式来编写。 下面的代码行不遵循 Python 语法,它们只是用于说明模型的一种伪代码。
Input: float[M,K] x, float[K,N] a, float[N] c
Output: float[M, N] y
r = onnx.MatMul(x, a)
y = onnx.Add(r, c)
这段代码实现了一个函数 f(x, a, c) -> y = x @ a + c,其中 x, a, c 是inputs,y 是outputs。r 是一个中间结果。MatMul 和 Add 是nodes,它们也有输入和输出。节点还具有类型,它是 ONNX Operators 中的一个算子(Operator)。
该图也可能有一个initializer。当输入永远不会改变时,例如线性回归的系数,将其转换为图中存储的常量是最有效的。如下所示:
Input: float[M,K] x
Initializer: float[K,N] a, float[N] c
Output: float[M, N] xac
xa = onnx.MatMul(x, a)
xac = onnx.Add(xa, c)
该图将类似于以下图像。右侧描述了操作符 Add,其中第二个输入被定义为initializer。
属性(attribute)是算子的固定参数。算子 Gemm 有四个属性:alpha、beta、transA 和 transB。除非运行时通过其 API 允许,否则在加载 ONNX 图后,这些值将无法更改,并且在所有预测中保持不变。
使用protobuf进行序列化(Serialization)
机器学习模型部署到生产环境通常需要复制整个用于训练模型的生态系统,大多数情况下会使用 Docker 进行容器化部署。然而,一旦模型被转换为 ONNX 格式,生产环境只需一个运行时(runtime)就能执行使用 ONNX算子定义的计算图。这个运行时可以用任何适合生产应用的语言开发,例如 C、Java、Python、JavaScript、C#、WebAssembly、ARM 等。
但是,为了实现上述目的,需要保存 ONNX 计算图。ONNX 使用 protobuf 将计算图序列化成一个单独的块(block)。其目标是尽可能优化模型的尺寸。
Metadata
机器学习模型会不断更新。因此,跟踪模型版本、模型作者以及训练方式非常重要。ONNX 允许将额外数据存储在模型本身中。
doc_string: 对该模型的人类可读文档。 支持 Markdown 格式。
domain: 指示模型命名空间或领域的反向 DNS 名称,例如,“org.onnx”。
metadata_props: 命名元数据,以字典形式表示 map<string,string>
,值和键应该不重复。
model_author: 以逗号分隔的姓名列表,模型作者和/或其组织的个人姓名。
model_license: 模型可用的许可证的知名名称或 URL。
model_version: 模型本身的版本,以整数编码。
producer_name: 生成模型的工具名称。
producer_version: 生成工具的版本。
training_info: 可选扩展,包含训练信息
可用算子和域(domains)的列表
这是现有列表:ONNX Operators。ONNX 算子列表涵盖了标准矩阵算子、归约、图像变换、深度神经网络层、激活函数。它涵盖了实现标准和深度机器学习推理函数所需的大部分操作。ONNX 并不会实现所有现有的机器学习算子,因为这样的列表会无限长。
主要算子列表使用域 ai.onnx
标识。域可以定义为一组算子。该列表中少数几个算子专用于文本处理,但几乎无法满足所有需求。该列表还缺少标准机器学习中非常流行的基于树的模型。这些模型是另一个域 ai.onnx.ml
的一部分,其中包括基于树的模型(TreeEnsemble Regressor 等)、预处理(OneHotEncoder、LabelEncoder 等)、SVM 模型(SVMRegressor 等)、插值器(Imputer)。
ONNX 只定义了这两个域。但是 ONNX 库支持任何自定义域和算子。
支持类型
ONNX 规范针对使用张量进行数值计算进行了优化。张量是一个多维数组,由以下几点定义:
- a type: 张量中所有元素的元素类型,它是统一的。
- a shape: 一个包含所有维度的数组,这个数组可以是空数组,维度也可以为空。
- a contiguous array: 它表示所有值。
此定义不包括步长(strides)或基于现有张量定义张量视图的可能性。ONNX 张量是一个密集的完整数组,没有步长。
支持类型
ONNX 规范针对使用张量进行数值计算进行了优化。张量是一个多维数组,由以下几点定义:
- a type: 张量中所有元素的元素类型,它是统一的。
- a shape: 一个包含所有维度的数组,这个数组可以是空数组,维度也可以为空。
- a contiguous array: 它表示所有值。
此定义不包括步长(strides)或基于现有张量定义张量视图的可能性。ONNX 张量是一个密集的完整数组,没有步长。
ONNX支持的类型包括元素类型,稀疏张量和一些其他的类型。
元素类型Element Type
ONNX最初是为了帮助部署深度学习模型而开发的。这就是为什么规范最初是为浮点数(32位)设计的。当前版本支持所有常用类型。字典TENSOR_TYPE_MAP给出了ONNX和numpy之间的对应关系。
import re
from onnx import TensorProto
reg = re.compile('^[0-9A-Z_]+$')
values = {}
for att in sorted(dir(TensorProto)):
if att in {'DESCRIPTOR'}:
continue
if reg.match(att):
values[getattr(TensorProto, att)] = att
for i, att in sorted(values.items()):
si = str(i)
if len(si) == 1:
si = " " + si
print("%s: onnx.TensorProto.%s" % (si, att))
运行上述代码,得到下述输出:
0: onnx.TensorProto.UNDEFINED
1: onnx.TensorProto.FLOAT
2: onnx.TensorProto.UINT8
3: onnx.TensorProto.INT8
4: onnx.TensorProto.UINT16
5: onnx.TensorProto.INT16
6: onnx.TensorProto.INT32
7: onnx.TensorProto.INT64
8: onnx.TensorProto.STRING
9: onnx.TensorProto.BOOL
10: onnx.TensorProto.FLOAT16
11: onnx.TensorProto.DOUBLE
12: onnx.TensorProto.UINT32
13: onnx.TensorProto.UINT64
14: onnx.TensorProto.COMPLEX64
15: onnx.TensorProto.COMPLEX128
16: onnx.TensorProto.BFLOAT16
17: onnx.TensorProto.FLOAT8E4M3FN
18: onnx.TensorProto.FLOAT8E4M3FNUZ
19: onnx.TensorProto.FLOAT8E5M2
20: onnx.TensorProto.FLOAT8E5M2FNUZ
21: onnx.TensorProto.UINT4
22: onnx.TensorProto.INT4
ONNX是强类型的,它的定义不支持隐式强制转换。即使其他语言可以添加两个不同类型的张量或矩阵,也不可能。这就是为什么必须在图中插入显式强制转换的原因。
稀疏张量Sparse Tensor
稀疏张量常用于表示许多元素为空的数组。ONNX 支持二维稀疏张量。SparseTensorProto 类定义了维度 (dims)、索引 (indices,使用 int64) 和值 (values) 等属性。
其他类型
除了张量和稀疏张量,ONNX 还支持通过 SequenceProto 和 MapProto 类型来表示序列张量、张量映射以及序列张量映射。不过,这些类型的使用比较少见。
什么是opset version
ONNX 算子集 (opset) 与 ONNX 软件包的版本映射。每次 ONNX 的次版本号增加时,opset 版本也会增加。新的 opset 版本会包含更新或新增的算子。
import onnx
print(onnx.__version__, " opset=", onnx.defs.onnx_opset_version())
使用上述代码,输出是:
1.16.0 opset= 21
每个 ONNX 图都会附加一个 opset,它是一个全局信息,定义了图中所有算子的版本。Add算子在版本 6、7、13 和 14 中进行了更新。如果图的 opset 是 15,则意味着 Add算子遵循 14 版本的规范。如果图的 opset 是 12,则 Add算子遵循 7 版本的规范。图中的一个算子遵循其在全局图 opset 以下(或等于)的最新定义。
一个图可能包含来自多个域的算子,例如 ai.onnx 和 ai.onnx.ml。在这种情况下,图必须为每个域定义一个全局 opset。该规则适用于同一域内的所有算子。
子图,测试与循环 subgroups, tests and loops
ONNX 支持测试和循环,但这些结构通常又慢又复杂,因为它们都需要包含另一个 ONNX 图作为属性。如果可以,最好避免使用它们。
If
算子If根据条件(condition)评估执行两个图中的一个。
If(condition) then
execute this ONNX graph (`then_branch`)
else
execute this ONNX graph (`else_branch`)
这两个图可以利用图中已经计算的结果,并且必须产生完全相同的数量的输出。这些输出将是If算子的输出。
Scan
算子Scan 用于实现一个具有固定迭代次数的循环。它遍历输入的每一行(或任何其他维度),并将输出沿着相同轴进行连接。看一个成对距离pairwise distances的例子:
M
(
i
,
j
)
=
∣
∣
X
i
−
X
j
∣
∣
2
M(i,j)=||X_i-X_j||^2
M(i,j)=∣∣Xi−Xj∣∣2
该循环即使比自定义实现成对距离慢,但效率仍然很高。它假设输入和输出都是张量,并自动将每次迭代的输出连接成单个张量。之前的例子只有一个,但它可能有多个。
Loop
算子Loop是一个能够实现 for循环 和 while循环 的组件。它既可以让循环执行固定次数,也可以在某个条件不满足时自动结束。
循环的输出以两种方式处理:
- 第一种方式类似于 Loop Scan: 输出结果以张量形式沿第一个维度进行拼接。这意味着所有输出张量必须具有兼容的形状。
- 第二种方式将张量拼接成一个序列: 每个张量作为一个单独的元素存储在序列中。
扩展性Extensibility
ONNX 定义了标准算子列表,称为:ONNX Operators。但是,您完全可以在此域或新域下定义自己的算子。onnxruntime 定义了自定义算子以提高推理性能。每个节点都有一个类型、一个名称、命名输入和输出以及属性。只要节点符合这些约束描述,就可以将其添加到任何 ONNX 图形中。
可以使用 Scan算子实现成对距离计算。但是,一个名为 CDist 的专用算子被证明速度明显更快,快到足以让人愿意为此实现一个专门的运行时。
函数
函数是扩展 ONNX 规范的一种方式。某些模型需要使用相同组合的算子。通过创建由现有 ONNX Operators定义的函数本身,可以避免这种情况。一旦定义,函数就像任何其他运算符一样工作,它具有输入、输出和属性。
使用函数有两个优点。第一个是使代码更短,更容易阅读。第二个是任何 onnxruntime 都可以利用该信息来更快速地运行预测。对于不依赖现有运算符实现的函数,运行时可以有特定的实现。
形状和种类推理 Shape (and Type) Inference
了解结果的形状并不是执行 ONNX 图形所必需的,但是可以利用这些信息来提高其速度。例如,假设您有一个以下图形:
Add(x, y) -> z
Abs(z) -> w
如果 x 和 y 具有相同的形状,那么 z 和 w 也具有相同的形状。知道这一点,就可以重用为 z 分配的缓冲区,从而原地计算绝对值 w。形状推理有助于运行时管理内存,从而提高效率。
在大多数情况下,ONNX 软件包可以根据每个标准算子的输入形状来计算输出形状。对于官方列表之外的任何自定义算子,则无法进行推理。
工具
在可视化 ONNX 图表方面,netron是一个非常实用的工具,而且不需要编程。第一个屏幕截图就是用这个工具制作的。
onnx2py.py 脚本可以将一个 ONNX 图转换为 Python 文件。生成的脚本能够创建与原图相同的计算图,并且用户可以进行修改。
zetane工具可以加载 ONNX 模型并在模型执行过程中显示中间结果。