转换器

在生产环境中使用ONNX意味着模型的预测功能可以通过ONNX操作符实现。必须选择一个运行时,该运行时在模型部署的平台上可用。检查差异,最后测量延迟。如果存在一个转换库支持该框架的所有部分,模型转换的第一步可能会很容易。如果不是这种情况,则必须在ONNX中实现缺失的部分。这可能会非常耗时。

什么是转换库?

sklearn-onnxscikit-learn 模型 转换为 ONNX。它使用上面介绍的 API 重写模型的预测函数, 无论它是什么,使用 ONNX 运算符。它确保预测结果与使用 原始模型计算的预期预测结果相等或至少非常接近。

机器学习库通常有它们自己的设计。 这就是为什么每个库都有一个特定的转换库。 许多这样的库都列在这里: 转换为ONNX格式。 以下是一个简短的列表:

所有这些库面临的主要挑战是保持节奏。 每当ONNX或它们支持的库有新版本发布时,它们都必须进行更新。这意味着每年有三到五次新版本发布。

转换库之间不兼容。 tensorflow-onnx 专门用于tensorflow,且仅适用于tensorflow。 同样,sklearn-onnx专门用于scikit-learn。

一个挑战是定制化。在机器学习模型中支持定制部分是很困难的。他们必须为这部分编写特定的转换器。某种程度上,这就像实现了两次预测函数。有一个简单的例子:深度学习框架有自己的原语,以确保相同的代码可以在不同的环境中执行。只要自定义层或子部分使用的是pytorch或tensorflow的部分,就不需要做太多工作。对于scikit-learn来说,情况就不同了。这个包没有自己的加法或乘法,它依赖于numpy或scipy。用户必须使用ONNX原语实现其转换器或预测器,无论它是否是用numpy实现的。

替代方案

实现ONNX导出功能的一种替代方案是利用标准协议,例如Array API标准,该标准标准化了一组常见的数组操作。它使得像NumPy、JAX、PyTorch、CuPy等库之间的代码重用成为可能。ndonnx允许使用ONNX后端执行,并为符合Array API标准的代码提供即时ONNX导出。这减少了对专用转换器库代码的需求,因为用于实现库的大部分代码可以在ONNX转换中重复使用。对于希望在构建ONNX图时获得类似NumPy体验的转换器作者来说,它还提供了一个方便的原始工具。

操作集

ONNX 发布的包带有版本号,如 major.minor.fix。每次次要更新意味着操作符列表 不同或签名已更改。它还与一个操作集相关联,版本 1.10 是操作集 15,1.11 将是操作集 16。 每个 ONNX 图都应定义其遵循的操作集。更改此 版本而不更新操作符可能会使图无效。 如果未指定操作集,ONNX 将认为该图 适用于最新的操作集。

新的操作集通常引入新的操作符。相同的推理函数可以以不同的方式实现,通常以更高效的方式。然而,模型运行的运行时可能不支持最新的操作集,或者至少在安装的版本中不支持。这就是为什么每个转换库都提供了为特定操作集创建ONNX图的可能性,通常称为target_opset。ONNX语言描述了简单和复杂的操作符。更改操作集类似于升级库。onnx和onnx运行时必须支持向后兼容性。

其他API

前面章节中的示例显示,onnx API 非常冗长。除非是一个小的图,否则通过阅读代码很难获得整个图的概览。几乎每个转换库都实现了不同的 API 来创建图,通常比 onnx 包的 API 更简单、更不冗长。所有 API 都自动添加初始化器,隐藏每个中间结果的名称创建,处理不同 opset 的不同版本。

一个带有方法 add_node 的类 Graph

tensorflow-onnx 实现了一个类图。 当ONNX没有类似的函数时,它会用ONNX操作符重写tensorflow函数(参见 Erf

sklearn-onnx 定义了两个不同的 API。第一个在该示例中引入的 实现一个转换器 遵循了与 tensorflow-onnx 类似的设计。以下行是从线性分类器的转换器中提取的。

# initializer

coef = scope.get_unique_variable_name('coef')
model_coef = np.array(
    classifier_attrs['coefficients'], dtype=np.float64)
model_coef = model_coef.reshape((number_of_classes, -1)).T
container.add_initializer(
    coef, proto_dtype, model_coef.shape, model_coef.ravel().tolist())

intercept = scope.get_unique_variable_name('intercept')
model_intercept = np.array(
    classifier_attrs['intercepts'], dtype=np.float64)
model_intercept = model_intercept.reshape((number_of_classes, -1)).T
container.add_initializer(
    intercept, proto_dtype, model_intercept.shape,
    model_intercept.ravel().tolist())

# add nodes

multiplied = scope.get_unique_variable_name('multiplied')
container.add_node(
    'MatMul', [operator.inputs[0].full_name, coef], multiplied,
    name=scope.get_unique_operator_name('MatMul'))

# [...]

argmax_output_name = scope.get_unique_variable_name('label')
container.add_node('ArgMax', raw_score_name, argmax_output_name,
                   name=scope.get_unique_operator_name('ArgMax'),
                   axis=1)

操作符作为函数

实现一个新的转换器中展示的第二个API更加紧凑,并将每个ONNX操作符定义为可组合的函数。对于KMeans,语法看起来像这样,更简洁且更易于阅读。

rs = OnnxReduceSumSquare(
    input_name, axes=[1], keepdims=1, op_version=opv)

gemm_out = OnnxMatMul(
    input_name, (C.T * (-2)).astype(dtype), op_version=opv)

z = OnnxAdd(rs, gemm_out, op_version=opv)
y2 = OnnxAdd(C2, z, op_version=opv)
ll = OnnxArgMin(y2, axis=1, keepdims=0, output_names=out[:1],
                op_version=opv)
y2s = OnnxSqrt(y2, output_names=out[1:], op_version=opv)

从经验中学到的技巧

差异

ONNX 是强类型的,并且针对深度学习中常见的 float32 类型进行了优化。标准机器学习库中同时使用 float32 和 float64。numpy 通常会转换为最通用的类型 float64。当预测函数是连续的时候,这没有显著影响。当不是连续的时候,必须使用正确的类型。示例 切换到浮点数时的问题 提供了更多关于该主题的见解。

并行化改变了计算的顺序。这通常并不重要,但它可能解释一些奇怪的差异。1 + 1e17 - 1e17 = 01e17 - 1e17 + 1 = 1。高数量级的情况很少见,但当模型使用矩阵的逆时,这种情况并不罕见。

IsolationForest 技巧

ONNX 仅实现了 TreeEnsembleRegressor,但它没有提供检索决策路径或图形统计信息的可能性。技巧是使用一个森林来预测叶子索引,并将这个叶子索引与所需信息映射一次或多次。

../_images/iff.png

离散化

查找特征落在哪个区间。使用numpy很容易做到,但在ONNX中高效实现则不那么容易。最快的方法是使用TreeEnsembleRegressor,一种二分搜索,它输出区间索引。这就是这个示例所实现的:WOE转换器

贡献

onnx repository 必须被fork并克隆。

构建

Windows 构建需要 conda。以下步骤可能不是最新的。 文件夹 onnx/.github/workflows 包含最新的说明。

Windows

使用Anaconda构建更容易。首先:创建一个环境。 这只需要做一次。

conda create --yes --quiet --name py3.9 python=3.9
conda install -n py3.9 -y -c conda-forge numpy libprotobuf=3.16.0

然后构建包:

git submodule update --init --recursive
set ONNX_BUILD_TESTS=1
set ONNX_ML=$(onnx_ml)
set CMAKE_ARGS=-DONNX_USE_PROTOBUF_SHARED_LIBS=ON -DONNX_USE_LITE_PROTO=ON -DONNX_WERROR=ON

python -m build --wheel

现在可以安装该包了。

Linux

克隆仓库后,可以运行以下指令:

python -m build --wheel

构建Markdown文档

必须先构建包(参见上一节)。

set ONNX_BUILD_TESTS=1
set ONNX_ML=$(onnx_ml)
set CMAKE_ARGS=-DONNX_USE_PROTOBUF_SHARED_LIBS=ON -DONNX_USE_LITE_PROTO=ON -DONNX_WERROR=ON

python onnx\gen_proto.py -l
python onnx\gen_proto.py -l --ml
pip install -e .
python onnx\backend\test\cmd_tools.py generate-data
python onnx\backend\test\stat_coverage.py
python onnx\defs\gen_doc.py
set ONNX_ML=0
python onnx\defs\gen_doc.py
set ONNX_ML=1

更新现有操作符

所有操作符都定义在文件夹 onnx/onnx/defs中。 每个子文件夹中有两个文件,一个叫做defs.cc,另一个叫做old.cc

  • defs.cc: 包含每个操作符的最新定义

  • old.cc: 包含之前opset中已弃用的操作符版本

更新操作符意味着将定义从defs.cc复制到old.cc,并更新defs.cc中的现有定义。

必须修改一个遵循模式onnx/defs/operator_sets*.h的文件。这些头文件注册了现有操作符的列表。

文件 onnx/defs/schema.h 包含最新的操作集版本。如果一个操作集被升级,它也必须被更新。

文件 onnx/version_converter/convert.h 包含将节点从一个操作集转换到下一个操作集时要应用的规则。 此文件也可能会更新。

必须编译包并再次生成文档,以自动更新markdown文档,并且必须将其包含在PR中。

然后必须更新单元测试。

总结

  • 修改文件 defs.cc, old.cc, onnx/defs/operator_sets*.h, onnx/defs/schema.h

  • 可选:修改文件 onnx/version_converter/convert.h

  • 构建onnx。

  • 构建文档。

  • 更新单元测试。

PR 应包括修改后的文件和修改后的 markdown 文档, 通常是以下文件的一部分 docs/docs/Changelog-ml.md, docs/Changelog.md, docs/Operators-ml.md, docs/Operators.md, docs/TestCoverage-ml.md, docs/TestCoverage.md.