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 + cxac 是 **输入**,y 是 **输出**。r 是一个中间结果。MatMulAdd 是 **节点**。它们也有输入和输出。节点也有一个类型,是 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 有四个属性:alphabetatransAtransB。除非运行时通过其 API 允许,否则在它加载 ONNX 图后,这些值就无法更改,并且在所有预测中保持冻结。

使用 protobuf 序列化

将机器学习模型部署到生产中通常需要复制用于训练模型的整个生态系统,大多数情况下使用 docker。一旦模型转换为 ONNX,生产环境只需要一个运行时来执行用 ONNX 运算符定义的图。此运行时可以用任何适合生产应用程序的语言开发,如 C、java、python、javascript、C#、Webassembly、ARM 等。

但要做到这一点,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

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

此定义不包括步幅 或者基于现有张量定义张量视图的可能性。ONNX 张量是密集的完整数组,没有步幅。

元素类型

ONNX 最初是为了帮助部署深度学习模型而开发的。因此,规范最初是针对浮点数(32 位)设计的。当前版本支持所有常见类型。字典 TENSOR_TYPE_MAP 给出了ONNXnumpy 之间的对应关系。

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 定义了属性 dimsindices(int64)和 values

其他类型

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

什么是 opset 版本?

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

import onnx
print(onnx.__version__, " opset=", onnx.defs.onnx_opset_version())
1.18.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(condition) then
    execute this ONNX graph (`then_branch`)
else
    execute this ONNX graph (`else_branch`)

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

../_images/dot_if.png

扫描

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

../_images/dot_scan.png

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

循环

算子 Loop 实现 for 循环和 while 循环。它可以进行固定数量的迭代,并且/或者在不再满足条件时结束。输出以两种不同的方式处理。第一种类似于循环 Scan,输出连接到张量(沿着第一个维度)。这也意味着这些输出必须具有兼容的形状。第二种机制将张量连接到张量序列中。

可扩展性

ONNX 定义了一个算子列表作为标准:ONNX 算子。但是,完全可以在此域或新域下定义自己的算子。onnxruntime 定义了自定义算子来改进推理。每个节点都有一个类型、一个名称、命名输入和输出以及属性。只要在这些约束下描述了一个节点,就可以将该节点添加到任何 ONNX 图中。

成对距离可以使用算子 Scan 实现。但是,一个名为 CDist 的专用算子被证明快得多,足以付出为其实现专用运行时的努力。

函数

函数是扩展 ONNX 规范的一种方法。一些模型需要相同的算子组合。可以通过创建使用现有 ONNX 算子定义的函数本身来避免这种情况。一旦定义,函数的行为与任何其他算子一样。它具有输入、输出和属性。

使用函数有两个优点。第一个是代码更短,更容易阅读。第二个是任何 onnxruntime 都可以利用这些信息来更快地运行预测。运行时可能具有特定函数的特定实现,而不是依赖于现有算子的实现。

形状(和类型)推断

了解结果的形状对于执行 ONNX 图不是必需的,但这些信息可以用于使其更快。如果你有以下图

Add(x, y) -> z
Abs(z) -> w

如果 xy 具有相同的形状,那么 zw 也具有相同的形状。了解这一点后,可以使用为 z 分配的缓冲区来就地计算绝对值 w。形状推断帮助运行时管理内存,从而提高效率。

ONNX 包可以在大多数情况下计算出每个标准算子的输出形状,同时了解输入形状。它显然无法对官方列表之外的任何自定义算子执行此操作。

工具

netron 非常有助于可视化 ONNX 图。这是唯一不需要编程的工具。第一个屏幕截图是用此工具生成的。

../_images/linreg1.png

onnx2py.py 从 ONNX 图创建 python 文件。此脚本可以创建相同的图。用户可以对其进行修改以更改图。

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