转换器¶
在生产中使用 ONNX 意味着模型的预测函数可以用 ONNX 算子实现。必须选择一个运行时,该运行时在模型部署的平台上可用。检查差异,最后测量延迟。如果存在支持模型所有部分的此框架的转换库,则模型转换的第一步可能很容易。如果不是这种情况,则必须在 ONNX 中实现缺失的部分。这可能非常耗时。
什么是转换库?¶
sklearn-onnx 将 scikit-learn 模型转换为 ONNX。它使用上面介绍的 API 用 ONNX 算子重写模型的预测函数,无论它是什么。它确保预测与使用原始模型计算的预期预测相等或至少非常接近。
机器学习库通常有自己的设计。这就是为什么每个库都存在一个专门的转换库。其中许多列在这里:转换为 ONNX 格式。这是一个简短的列表
sklearn-onnx:从 scikit-learn 转换模型,
tensorflow-onnx:从 tensorflow 转换模型,
onnxmltools:从 lightgbm、xgboost、pyspark、libsvm 转换模型
torch.onnx:从 pytorch 转换模型。
所有这些库面临的主要挑战是保持节奏。它们必须在 ONNX 或它们支持的库每次发布新版本时进行更新。这意味着每年要发布 3 到 5 个新版本。
转换库之间不兼容。 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 自动添加初始化器,隐藏每个中间结果名称的创建,处理不同操作集的不同版本。
带有 add_node 方法的 Graph 类¶
tensorflow-onnx
实现了一个 graph 类。当 ONNX 没有类似函数时,它用 ONNX 算子重写 tensorflow 函数(参见 Erf)。
sklearn-onnx 定义了两个不同的 API。在本例中介绍的第一个 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。当预测函数是连续的时,它不会产生重大影响。如果不是,则必须使用正确的类型。示例切换到 float 时的问题提供了有关该主题的更多见解。
并行化会改变计算顺序。通常它并不重要,但这可能解释了一些奇怪的差异。1 + 1e17 - 1e17 = 0
但 1e17 - 1e17 + 1 = 1
。高数量级很少见,但在模型使用矩阵的逆时并不少见。
IsolationForest 技巧¶
ONNX 只实现了一个TreeEnsembleRegressor,但它不提供检索决策路径或统计信息到图表的可能性。技巧是使用一个森林来预测叶子索引,并将此叶子索引一次或多次映射到所需的信息。
离散化¶
查看特征属于哪个区间。使用 numpy 很容易做到,但使用 ONNX 高效地做到这一点并不容易。最快的方法是使用 TreeEnsembleRegressor(二分搜索),它输出区间索引。这就是这个例子实现的:WOE 转换器。
贡献¶
必须分叉和克隆onnx 存储库。
构建¶
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包含最新的 opset 版本。如果升级了 opset,也必须更新它。
文件onnx/version_converter/convert.h包含在将节点从一个 opset 转换为下一个 opset 时要应用的规则。该文件也可能需要更新。
必须编译包并重新生成文档以自动更新 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
的子集。