ONNX 概念¶
ONNX 可以被比作一种专门用于数学函数的编程语言。它定义了机器学习模型实现其推理函数所需的所有必要操作。线性回归可以用以下方式表示:
def onnx_linear_regressor(X):
"ONNX code for a linear regression"
return onnx.Add(onnx.MatMul(X, coefficients), bias)
这个例子与开发人员在Python中编写的表达式非常相似。它也可以表示为一个图表,展示如何逐步转换特征以获得预测。这就是为什么用ONNX实现的机器学习模型通常被称为ONNX图表。
ONNX 旨在提供一种任何机器学习框架都可以使用的通用语言来描述其模型。第一个场景是使在生产环境中部署机器学习模型变得更加容易。可以在部署环境中专门实现和优化一个 ONNX 解释器(或运行时)以完成此任务。通过 ONNX,可以构建一个独特的过程来在生产环境中部署模型,并且独立于用于构建模型的学习框架。onnx 实现了一个 Python 运行时,可用于评估 ONNX 模型和评估 ONNX 操作。这旨在澄清 ONNX 的语义,并帮助理解和调试 ONNX 工具和转换器。它不打算用于生产环境,性能也不是目标(参见 onnx.reference)。
输入、输出、节点、初始化器、属性¶
构建一个ONNX图意味着使用ONNX语言或更确切地说是ONNX运算符来实现一个函数。 线性回归可以这样编写。 以下行不遵循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 是输入,y 是输出。
r 是一个中间结果。
MatMul 和 Add 是节点。它们也有输入和输出。
一个节点还有一个类型,是
ONNX 运算符 中的一个操作符。这个图是用
一个简单的例子:线性回归 中的示例构建的。
图表也可以有一个初始化器。当输入从不改变时,例如线性回归的系数,将其转换为存储在图表中的常量是最有效的。
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, default value。
一个属性是操作符的一个固定参数。操作符Gemm 有四个属性,alpha、beta、transA、transB。除非运行时通过其API允许,一旦加载了ONNX图,这些值就不能更改,并且在所有预测中保持冻结状态。
使用protobuf进行序列化¶
将机器学习模型部署到生产环境通常需要复制用于训练模型的整个生态系统,大多数情况下使用docker。一旦模型转换为ONNX格式,生产环境只需要一个运行时来执行由ONNX操作符定义的图。这个运行时可以用任何适合生产应用的语言开发,如C、java、python、javascript、C#、Webassembly、ARM等。
但为了实现这一点,ONNX图需要被保存。 ONNX使用protobuf将图序列化为 一个单一的块 (参见解析和序列化)。它的目标是尽可能优化模型的大小。
元数据¶
机器学习模型会不断更新。跟踪模型的版本、模型的作者以及模型的训练方式非常重要。ONNX 提供了在模型本身中存储额外数据的可能性。
- doc_string: Human-readable documentation for this model.
允许使用Markdown。
- domain: A reverse-DNS name to indicate the model namespace or domain,
例如,‘org.onnx’
- metadata_props: Named metadata as dictionary
map<string,string>, (values, keys)应该是唯一的。
- metadata_props: Named metadata as dictionary
- model_author: A comma-separated list of names,
模型的作者的个人姓名,以及/或者他们的组织。
- model_license: The well-known name or URL of the license
模型在何种条件下可用。
model_version: 模型本身的版本,以整数编码。
producer_name: 用于生成模型的工具名称。
producer_version: 生成工具的版本。
- training_info: An optional extension that contains
训练信息(参见 TrainingInfoProto)
可用运算符和域的列表¶
主要列表在这里描述:ONNX Operators。 它合并了标准矩阵运算符(Add, Sub, MatMul, Transpose, Greater, IsNaN, Shape, Reshape…), 归约操作(ReduceSum, ReduceMin, …), 图像变换(Conv, MaxPool, …), 深度神经网络层(RNN, DropOut, …), 激活函数(Relu, Softmax, …)。 它涵盖了实现标准和深度机器学习推理函数所需的大多数操作。 ONNX 并没有实现所有现有的机器学习运算符, 因为运算符的列表将是无限的。
主要操作符列表以域ai.onnx标识。 域可以定义为一组操作符。 此列表中的一些操作符专门用于文本,但它们几乎无法满足需求。 主要列表还缺少在标准机器学习中非常流行的基于树的模型。 这些是另一个域ai.onnx.ml的一部分, 它包括基于树的模型(TreeEnsemble Regressor, …), 预处理(OneHotEncoder, LabelEncoder, …),SVM模型 (SVMRegressor, …),填补器(Imputer)。
ONNX 仅定义了这两个域。但 onnx 库支持任何自定义域和操作符 (参见 扩展性)。
支持的类型¶
ONNX 规范针对张量的数值计算进行了优化。一个张量是一个多维数组。它由以下定义:
类型:元素类型,张量中所有元素的类型相同
形状:一个包含所有维度的数组,这个数组可以为空,一个维度可以为空
一个连续的数组:它代表所有的值
此定义不包括步幅或基于现有张量定义张量视图的可能性。ONNX 张量是一个没有步幅的密集完整数组。
元素类型¶
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))
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
23: onnx.TensorProto.FLOAT4E2M1
ONNX 是强类型的,其定义不支持隐式转换。即使其他语言支持,也无法将两个类型不同的张量或矩阵相加。这就是为什么在图中必须插入显式转换的原因。
稀疏张量¶
稀疏张量对于表示具有许多空系数的数组非常有用。
ONNX 支持 2D 稀疏张量。类 SparseTensorProto
定义了属性 dims, indices (int64) 和 values。
其他类型¶
除了张量和稀疏张量,ONNX 还支持通过类型 SequenceProto, MapProto 支持的张量序列、张量映射、张量映射序列。它们很少被使用。
什么是opset版本?¶
opset 映射到 onnx 包的版本。 每当次要版本增加时,它都会递增。 每个版本都会带来更新或新的操作符。
import onnx
print(onnx.__version__, " opset=", onnx.defs.onnx_opset_version())
1.18.0 opset= 23
每个ONNX图也附加了一个操作集。这是一个全局信息。它定义了图中所有操作符的版本。操作符Add在版本6、7、13和14中进行了更新。如果图的操作集是15,这意味着操作符Add遵循版本14的规范。如果图的操作集是12,那么操作符Add遵循版本7的规范。图中的操作符遵循其最接近的(或等于)全局图操作集的定义。
一个图可能包含来自多个领域的操作符,例如ai.onnx和
ai.onnx.ml。在这种情况下,图必须为每个领域定义一个
全局操作集。该规则适用于同一领域内的所有操作符。
子图、测试和循环¶
ONNX 实现了测试和循环。它们都接受另一个 ONNX 图作为属性。这些结构通常较慢且复杂。如果可能的话,最好避免使用它们。
如果¶
操作符 If 根据条件评估执行两个图中的其中一个。
If(condition) then
execute this ONNX graph (`then_branch`)
else
execute this ONNX graph (`else_branch`)
这两个图可以使用图中已经计算的任何结果,并且必须产生完全相同数量的输出。这些输出将是操作符 If 的输出。
扫描¶
操作符 Scan 实现了一个具有固定迭代次数的循环。 它循环遍历输入的行(或任何其他维度),并沿同一轴连接输出。让我们看一个实现成对距离的示例:\(M(i,j) = \lVert X_i - X_j \rVert^2\)。
这个循环即使仍然比自定义实现的成对距离慢,也是高效的。它假设输入和输出是张量,并自动将每次迭代的输出连接成单个张量。前面的例子只有一个,但它可能有多个。
循环¶
操作符 Loop 实现了for循环和while循环。它可以执行固定次数的迭代,并且/或者在条件不再满足时结束。输出以两种不同的方式处理。第一种方式类似于循环 Scan,输出被连接成张量(沿第一个维度)。这也意味着这些输出必须具有兼容的形状。第二种机制将张量连接成张量序列。
可扩展性¶
ONNX 定义了一系列运算符作为标准:ONNX 运算符。然而,完全有可能在这个领域或新的领域中定义你自己的运算符。onnxruntime 定义了自定义运算符以提高推理效率。每个节点都有一个类型、一个名称、命名的输入和输出以及属性。只要一个节点在这些约束下被描述,就可以将其添加到任何 ONNX 图中。
成对距离可以通过操作符Scan实现。 然而,一个名为CDist的专用操作符被证明显著更快,快到足以为其实现一个专用的运行时。
函数¶
函数是扩展ONNX规范的一种方式。某些模型需要相同的操作符组合。这可以通过创建一个由现有ONNX操作符定义的函数来避免。一旦定义,函数的行为就像任何其他操作符一样。它有输入、输出和属性。
使用函数有两个优点。第一个是代码更短且更易于阅读。第二个是任何onnxruntime都可以利用这些信息来更快地运行预测。运行时可以有一个特定的函数实现,而不依赖于现有操作符的实现。
形状(和类型)推断¶
执行ONNX图不需要知道结果的形状,但这些信息可以用来使其更快。如果你有以下图:
Add(x, y) -> z
Abs(z) -> w
如果x和y具有相同的形状,那么z和w也具有相同的形状。知道这一点后,可以重用为z分配的缓冲区,以在原地计算绝对值w。形状推断有助于运行时管理内存,从而提高效率。
ONNX 包在大多数情况下可以计算输出形状,只要知道每个标准运算符的输入形状。显然,对于官方列表之外的任何自定义运算符,它无法做到这一点。
工具¶
netron 对于帮助可视化ONNX图非常有用。 这是唯一一个不需要编程的工具。第一张截图就是用这个工具制作的。
onnx2py.py 从ONNX图创建一个python文件。此脚本可以创建相同的图。用户可以修改它以更改图。
zetane 可以加载onnx模型并在模型执行时显示中间结果。