ONNX 概念

ONNX 可以与专门用于数学函数的编程语言相媲美。它定义了机器学习模型实现其推理功能所需的所有必要操作。线性回归可以以下列方式表示

def onnx_linear_regressor(X):
    "ONNX code for a linear regression"
    return onnx.Add(onnx.MatMul(X, coefficients), bias)

这个例子与开发者在 Python 中编写的表达式非常相似。它也可以表示为图,逐步展示如何转换特征以获得预测。这就是为什么使用 ONNX 实现的机器学习模型通常被称为 **ONNX 图** 的原因。

../_images/linreg1.png

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*,其中第二个输入被定义为一个初始化器。这张图是使用 初始化器,默认值 代码获得的。

Snapshot of Netron

一个 **属性** 是一个运算符的固定参数。运算符 Gemm 有四个属性:*alpha*、*beta*、*transA*、*transB*。除非运行时通过其 API 允许,否则一旦加载了 ONNX 图,这些值就无法更改,对于所有预测保持不变。

使用 protobuf 进行序列化

将机器学习模型部署到生产环境通常需要复制用于训练模型的整个生态系统,大多数情况下是使用 *docker*。一旦模型转换为 ONNX,生产环境只需要一个运行时来执行使用 ONNX 运算符定义的图。该运行时可以使用适用于生产应用的任何语言开发,例如 C、Java、Python、Javascript、C#、WebAssembly、ARM 等。但要实现这一点,ONNX 图需要保存。ONNX 使用 *protobuf* 将图序列化为一个单一块(参见 解析和序列化)。其目的是尽可能优化模型大小。

为了做到这一点,ONNX 图需要保存下来。ONNX 使用 protobuf 将图序列化为一个单独的块(参见 解析和序列化)。其目的是尽可能优化模型大小。

元数据

机器学习模型持续更新。跟踪模型的版本、作者以及如何训练它非常重要。ONNX 提供了在模型本身存储额外数据的可能性。

  • doc_string: 此模型的人类可读文档。

    允许使用 Markdown。

  • domain: 一个反向 DNS 名称,用于指示模型命名空间或域,

    例如 'org.onnx'

  • metadata_props: 命名元数据,采用字典 map<string,string> 形式,

    (values, keys) 应该唯一。

  • model_author: 以逗号分隔的名称列表,

    模型的作者个人姓名和/或其组织。

  • model_license: 模型可用许可证的知名名称或 URL。

    模型在此条件下可用。

  • model_version: 模型本身的版本,以整数编码。

  • producer_name: 用于生成模型的工具名称。

  • producer_version: 生成工具的版本。

  • training_info: 一个可选扩展,包含

    训练信息(参见 TrainingInfoProto

可用运算符和域列表

主列表描述于此: ONNX 运算符。它合并了标准矩阵运算符(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 规范针对使用张量进行数值计算进行了优化。张量是多维数组。它由以下部分定义:

  • 一个类型:元素类型,张量中所有元素的类型相同

  • 一个形状:包含所有维度的数组,此数组可以为空,维度可以为 null

  • 一个连续数组:它表示所有值

此定义不包括 *步幅 (strides)* 或基于现有张量定义张量视图的可能性。ONNX 张量是一个没有步幅的密集完整数组。

元素类型

ONNX 最初是为帮助部署深度学习模型而开发的。这就是为什么规范最初设计用于浮点数(32 位)。当前版本支持所有常用类型。字典 l-onnx-types-mapping 给出了 *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 是强类型化的,其定义不支持隐式转换。即使其他语言允许,也无法添加两个不同类型的张量或矩阵。因此,必须在图中插入显式转换。

稀疏张量

稀疏张量对于表示包含许多 null 系数的数组很有用。ONNX 支持 2D 稀疏张量。类 SparseTensorProto 定义了属性 dimsindices (int64) 和 values

其他类型

除了张量和稀疏张量,ONNX 通过类型 SequenceProtoMapProto 支持张量序列、张量映射、张量映射序列。它们很少使用。

什么是 opset 版本?

opset 映射到 *onnx* 包的版本。每当次要版本增加时,它就会递增。每个版本都会带来更新或新的运算符。

import onnx
print(onnx.__version__, " opset=", onnx.defs.onnx_opset_version())
1.19.0  opset= 23

opset 也附加到每个 ONNX 图。这是一个全局信息。它定义了图中所有运算符的版本。运算符 *Add* 在版本 6、7、13 和 14 中更新。如果图 opset 是 15,则表示运算符 *Add* 遵循规范版本 14。如果图 opset 是 12,则运算符 *Add* 遵循规范版本 7。图中的运算符遵循低于(或等于)全局图 opset 的其最新定义。

一个图可能包含来自多个域的运算符,例如 ai.onnxai.onnx.ml。在这种情况下,图必须为每个域定义一个全局 opset。该规则适用于同一域内的所有运算符。

子图、测试和循环

ONNX 实现了测试和循环。它们都将另一个 ONNX 图作为属性。这些结构通常速度慢且复杂。如果可能,最好避免使用它们。

If

运算符 If 根据条件评估的结果执行两个图中的一个。

If(condition) then
    execute this ONNX graph (`then_branch`)
else
    execute this ONNX graph (`else_branch`)

这两个图可以使用图中已计算的任何结果,并且必须生成完全相同数量的输出。这些输出将是运算符 If 的输出。

../_images/dot_if.png

Scan

运算符 Scan 实现具有固定迭代次数的循环。它遍历输入的行(或任何其他维度),并沿同一轴连接输出。让我们看一个实现成对距离的例子:\(M(i,j) = \lVert X_i - X_j \rVert^2\)

../_images/dot_scan.png

这个循环效率很高,尽管仍然比自定义实现的成对距离慢。它假设输入和输出是张量,并自动将每次迭代的输出连接成单个张量。前面的示例只有一个,但可以有多个。

Loop

运算符 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 图非常有用。它是唯一不需要编程的工具。第一张截图就是用这个工具制作的。

../_images/linreg1.png

onnx2py.py 从 ONNX 图创建 Python 文件。这个脚本可以创建相同的图。用户可以修改它来更改图。

zetane 可以加载 onnx 模型并在模型执行时显示中间结果。