转换管道

skl2onnx 将任何机器学习管道转换为 ONNX 管道。每个转换器或预测器都转换为 ONNX 图中的一个或多个节点。然后,任何 ONNX 后端 都可以使用此图来计算相同输入的等效输出。

转换复杂管道

scikit-learn 引入了 ColumnTransformer,它有助于构建如下的复杂管道

from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.decomposition import TruncatedSVD
from sklearn.compose import ColumnTransformer

numeric_features = [0, 1, 2] # ["vA", "vB", "vC"]
categorical_features = [3, 4] # ["vcat", "vcat2"]

classifier = LogisticRegression(C=0.01, class_weight=dict(zip([False, True], [0.2, 0.8])),
                                n_jobs=1, max_iter=10, solver='lbfgs', tol=1e-3)

numeric_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

categorical_transformer = Pipeline(steps=[
    ('onehot', OneHotEncoder(sparse_output=True, handle_unknown='ignore')),
    ('tsvd', TruncatedSVD(n_components=1, algorithm='arpack', tol=1e-4))
])

preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)
    ])

model = Pipeline(steps=[
    ('precprocessor', preprocessor),
    ('classifier', classifier)
])

我们可以将其表示为

拟合后,模型将转换为 ONNX

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType, StringTensorType

initial_type = [('numfeat', FloatTensorType([None, 3])),
                ('strfeat', StringTensorType([None, 2]))]
model_onnx = convert_sklearn(model, initial_types=initial_type)

注意

错误 AttributeError: 'ColumnTransformer' object has no attribute 'transformers_' 表示模型未进行训练。转换器尝试访问由 fit 方法创建的属性。

它可以表示为 DOT

from onnx.tools.net_drawer import GetPydotGraph, GetOpNodeProducer
pydot_graph = GetPydotGraph(model_onnx.graph, name=model_onnx.graph.name, rankdir="TP",
                            node_producer=GetOpNodeProducer("docstring"))
pydot_graph.write_dot("graph.dot")

import os
os.system('dot -O -Tpng graph.dot'
_images/pipeline.png

解析器、形状计算器、转换器

三种类型的函数参与 scikit-pipeline 的转换。它们按以下顺序调用

  • parser(scope, model, inputs, custom_parser):解析器构建模型的预期输出,因为生成的图必须包含唯一名称,scope 包含已提供的名称,model 是要转换的模型,inputs 是模型在 ONNX 图中接收的 inputs。它是一个 Variable 列表。custom_parsers 包含一个映射 {model type: parser},它扩展了默认的解析器列表。解析器为标准机器学习问题定义默认输出。形状计算器根据模型更改每个输出的形状和类型,并在定义所有输出(拓扑)后调用。此步骤定义每个节点的输出数量及其类型,并将它们设置为默认形状 [None, None],其中输出节点只有一行,并且目前没有已知的列。

  • shape_calculator(model): 形状计算器更改解析器创建的输出的形状。一旦此函数返回其结果,图结构将完全定义,无法更改。形状计算器不应更改类型。许多运行时是用 C++ 实现的,不支持隐式转换。类型更改可能会使运行时由于两个连续节点(由两个不同的转换器生成)之间存在类型不匹配而失败。

  • converter(scope, operator, container): 转换器将转换器或预测器转换为 ONNX 节点。每个节点可以是 ONNX 操作符ML 操作符 或自定义 ONNX 操作符。

由于 sklearn-onnx 可能会转换包含来自其他库的模型的管道,因此该库必须处理来自其他软件包的解析器、形状计算器或转换器。这可以通过两种方式完成。第一种方法是通过将模型类型映射到特定解析器、特定形状计算器或特定转换器来调用函数 convert_sklearn。可以通过使用两个函数之一 update_registered_converterupdate_registered_parser 来注册新的解析器或形状计算器或转换器以避免这些规范。以下是一个示例。

管道中的新转换器

许多库实现了 scikit-learn API,它们的模型可以包含在 scikit-learn 管道中。但是,如果 sklearn-onnx 不知道相应的转换器,它将无法转换包含 XGBoostLightGbm 等模型的管道:它需要注册。这就是函数 skl2onnx.update_registered_converter() 的作用。以下示例展示了如何注册新的转换器或更新现有转换器。注册了四个元素

  • 模型类

  • 别名,通常是库名称加上类名称的前缀

  • 计算预期输出的类型和形状的形状计算器

  • 模型转换器

以下代码展示了随机森林的这四个元素是什么

from skl2onnx.common.shape_calculator import calculate_linear_classifier_output_shapes
from skl2onnx.operator_converters.RandomForest import convert_sklearn_random_forest_classifier
from skl2onnx import update_registered_converter
update_registered_converter(SGDClassifier, 'SklearnLinearClassifier',
                            calculate_linear_classifier_output_shapes,
                            convert_sklearn_random_forest_classifier)

请参阅示例 转换包含 LightGBM 分类器的管道,了解包含 LightGbm 模型的完整示例。

泰坦尼克号示例

第一个示例是一个来自 scikit-learn 文档的简化管道:包含混合类型的 Column Transformer。完整的故事可以在可运行的示例中找到:转换包含 ColumnTransformer 的管道,该示例还展示了用户在尝试转换管道时可能会遇到的一些错误。

参数化转换

大多数转换器不需要特定的选项来转换 scikit-learn 模型。它始终产生相同的结果。但是,在某些情况下,转换无法生成返回完全相同结果的模型。用户可能希望通过向转换器提供更多信息来优化转换,即使要转换的模型包含在管道中。这就是选项机制实现的原因:具有选项的转换器

调查差异

错误的转换器可能会在转换器管道中引入差异,但并不总是很容易隔离差异的来源。然后可以使用函数 collect_intermediate_steps 来独立调查每个组件。以下代码片段摘自单元测试 test_investigate.py,它将独立转换管道及其每个组件。

import numpy
from numpy.testing import assert_almost_equal
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import onnxruntime
from skl2onnx.helpers import collect_intermediate_steps, compare_objects
from skl2onnx.common.data_types import FloatTensorType

# Let's fit a model.
data = numpy.array([[0, 0], [0, 0], [2, 1], [2, 1]],
                   dtype=numpy.float32)
model = Pipeline([("scaler1", StandardScaler()),
                  ("scaler2", StandardScaler())])
model.fit(data)

# Convert and collect every operator in a pipeline
# and modifies the current pipeline to keep
# intermediate inputs and outputs when method
# predict or transform is called.
operators = collect_intermediate_steps(model, "pipeline",
                                       [("input",
                                         FloatTensorType([None, 2]))])

# Method and transform is called.
model.transform(data)

# Loop on every operator.
for op in operators:

    # The ONNX for this operator.
    onnx_step = op['onnx_step']

    # Use onnxruntime to compute ONNX outputs
    sess = onnxruntime.InferenceSession(onnx_step.SerializeToString(),
                                        providers=["CPUExecutionProvider"])

    # Let's use the initial data as the ONNX model
    # contains all nodes from the first inputs to this node.
    onnx_outputs = sess.run(None, {'input': data})
    onnx_output = onnx_outputs[0]
    skl_outputs = op['model']._debug.outputs['transform']

    # Compares the outputs between scikit-learn and onnxruntime.
    assert_almost_equal(onnx_output, skl_outputs)

    # A function which is able to deal with different types.
    compare_objects(onnx_output, skl_outputs)

调查缺少的转换器

在转换管道之前,许多转换器可能缺失。当找到第一个缺失的转换器时,将引发异常 MissingShapeCalculator。前面的代码片段可以修改以查找所有缺失的转换器。

import numpy
from numpy.testing import assert_almost_equal
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import onnxruntime
from skl2onnx.common.data_types import guess_data_type
from skl2onnx.common.exceptions import MissingShapeCalculator
from skl2onnx.helpers import collect_intermediate_steps, compare_objects, enumerate_pipeline_models
from skl2onnx.helpers.investigate import _alter_model_for_debugging
from skl2onnx import convert_sklearn

class MyScaler(StandardScaler):
    pass

# Let's fit a model.
data = numpy.array([[0, 0], [0, 0], [2, 1], [2, 1]],
                   dtype=numpy.float32)
model = Pipeline([("scaler1", StandardScaler()),
                  ("scaler2", StandardScaler()),
                  ("scaler3", MyScaler()),
                ])
model.fit(data)

# This function alters the pipeline, every time
# methods transform or predict are used, inputs and outputs
# are stored in every operator.
_alter_model_for_debugging(model, recursive=True)

# Let's use the pipeline and keep intermediate
# inputs and outputs.
model.transform(data)

# Let's get the list of all operators to convert
# and independently process them.
all_models = list(enumerate_pipeline_models(model))

# Loop on every operator.
for ind, op, last in all_models:
    if ind == (0,):
        # whole pipeline
        continue

    # The dump input data for this operator.
    data_in = op._debug.inputs['transform']

    # Let's infer some initial shape.
    t = guess_data_type(data_in)

    # Let's convert.
    try:
        onnx_step = convert_sklearn(op, initial_types=t)
    except MissingShapeCalculator as e:
        if "MyScaler" in str(e):
            print(e)
            continue
        raise

    # If it does not fail, let's compare the ONNX outputs with
    # the original operator.
    sess = onnxruntime.InferenceSession(onnx_step.SerializeToString(),
                                        providers=["CPUExecutionProvider"])
    onnx_outputs = sess.run(None, {'input': data_in})
    onnx_output = onnx_outputs[0]
    skl_outputs = op._debug.outputs['transform']
    assert_almost_equal(onnx_output, skl_outputs)
    compare_objects(onnx_output, skl_outputs)