转换器¶
在生产中使用 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 或它们支持的库发布新版本时,它们都必须进行更新。这意味着每年三到五个新版本。
转换库之间不兼容。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 将认为该图对最新操作集有效。
新的操作集通常会引入新的算子。同一个推理函数可以以不同的方式实现,通常更高效。但是,模型运行的运行时可能不支持最新的操作集,或者至少不支持已安装的版本。这就是为什么每个转换库都提供了为特定操作集(通常称为 target_opset)创建 ONNX 图的可能性。ONNX 语言描述了简单和复杂的算子。更改操作集类似于升级库。onnx 和 onnx 运行时必须支持向后兼容性。
其他 API¶
前面章节中的示例表明 onnx API 非常冗长。除非图形很小,否则通过阅读代码很难全面了解图形。几乎每个转换库都实现了一个不同的 API 来创建图形,通常比 onnx 包的 API 更简单、更简洁。所有 API 都自动化了初始化器的添加,隐藏了每个中间结果名称的创建,并处理了不同操作集的版本差异。
带 add_node 方法的 Graph 类¶
tensorflow-onnx 实现了一个 Graph 类。当 ONNX 没有类似函数时(参见 Erf),它会用 ONNX 算子重写 tensorflow 函数。
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。当预测函数连续时,它没有显著影响。当它不连续时,必须使用正确的类型。示例 切换到浮点数时的问题 提供了更多关于该主题的见解。
并行化改变了计算顺序。这通常不显著,但它可能解释一些奇怪的差异。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:包含先前操作集中已废弃的算子版本
更新算子意味着将 defs.cc 中的定义复制到 old.cc,并更新 defs.cc 中现有的定义。
必须修改一个遵循 onnx/defs/operator_sets*.h 模式的文件。这些头文件注册了现有算子的列表。
文件 onnx/defs/schema.h 包含最新的 opset 版本。如果某个 opset 已升级,则也必须更新它。
文件 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 的子集。