diff --git a/_doc/sphinxdoc/source/_exts/generate_onnx_ops.py b/_doc/sphinxdoc/source/_exts/generate_onnx_ops.py index db68cbe8e..3c645a645 100644 --- a/_doc/sphinxdoc/source/_exts/generate_onnx_ops.py +++ b/_doc/sphinxdoc/source/_exts/generate_onnx_ops.py @@ -22,7 +22,7 @@ class SupportedOnnxOpsDirective(Directive): has_content = False def run(self): - cls = _dynamic_class_creation() + cls = _dynamic_class_creation(include_past=True) cls_name = [(c.__name__, c) for c in cls] rows = [] sorted_cls_name = list(sorted(cls_name)) @@ -32,8 +32,8 @@ def make_ref(cl): return ":ref:`l-xop-onnx-{}`".format(cl.__name__) table = [] - cut = len(sorted_cls_name) // 3 + \ - (1 if len(sorted_cls_name) % 3 else 0) + cut = (len(sorted_cls_name) // 3 + + (1 if len(sorted_cls_name) % 3 else 0)) for i in range(cut): row = [] row.append(make_ref(sorted_cls_name[i][1])) @@ -56,9 +56,9 @@ def make_ref(cl): nested_parse_with_titles(self.state, st, node) main += node - rows.append('') for name, cl in sorted_cls_name: rows = [] + rows.append('') rows.append('.. _l-xop-onnx-{}:'.format(cl.__name__)) rows.append('') rows.append(cl.__name__) diff --git a/_doc/sphinxdoc/source/api/ast.rst b/_doc/sphinxdoc/source/api/ast.rst new file mode 100644 index 000000000..3e75f4eef --- /dev/null +++ b/_doc/sphinxdoc/source/api/ast.rst @@ -0,0 +1,38 @@ + +=== +AST +=== + +.. contents:: + :local: + +Main functions +============== + +.. autosignature:: mlprodict.onnx_tools.onnx_translation.translate_fct2onnx + +Additional functions +==================== + +.. autosignature:: mlprodict.onnx_tools.onnx_translation.get_default_context + +.. autosignature:: mlprodict.onnx_tools.onnx_translation.get_default_context_cpl + +.. autosignature:: mlprodict.onnx_tools.onnx_translation.py_make_float_array + +.. autosignature:: mlprodict.onnx_tools.onnx_translation.py_opp + +.. autosignature:: mlprodict.onnx_tools.onnx_translation.py_mul + +.. autosignature:: mlprodict.onnx_tools.onnx_translation.py_pow + +.. autosignature:: mlprodict.onnx_tools.onnx_translation.squareform_pdist + +Grammar Objects +=============== + +.. autosignature:: mlprodict.onnx_tools.onnx_grammar.node_visitor_translator.CodeNodeVisitor + +.. autosignature:: mlprodict.onnx_tools.onnx_grammar.onnx_translator.CodeTranslator + +.. autosignature:: mlprodict.onnx_tools.onnx_grammar.onnx_translator.OnnxTranslator diff --git a/_doc/sphinxdoc/source/api/index.rst b/_doc/sphinxdoc/source/api/index.rst index 28d161bc6..05db1a967 100644 --- a/_doc/sphinxdoc/source/api/index.rst +++ b/_doc/sphinxdoc/source/api/index.rst @@ -11,8 +11,15 @@ This is a summary of functions this modules provides. onnx_conv sklapi + +**Write ONNX graphs** + +.. toctree:: + :maxdepth: 1 + npy xop + ast **ONNX runtime** diff --git a/_doc/sphinxdoc/source/api/xop.rst b/_doc/sphinxdoc/source/api/xop.rst index d650f4bf3..8ffa37954 100644 --- a/_doc/sphinxdoc/source/api/xop.rst +++ b/_doc/sphinxdoc/source/api/xop.rst @@ -1,32 +1,40 @@ .. _l-xop-onnxpy: -Create ONNX graphs -================== +======= +Xop API +======= .. contents:: :local: -Example -+++++++ - -Converters -++++++++++ - API -+++ +=== + +Automated gathering of operators +++++++++++++++++++++++++++++++++ .. autosignature:: mlprodict.npy.xop.ClassFactory .. autosignature:: mlprodict.npy.xop.dynamic_class_creation -.. autosignature:: mlprodict.npy.xops_variable.Variable +.. autosignature:: mlprodict.npy.xop._GraphBuilder + +Main classes +++++++++++++ + +.. autosignature:: mlprodict.npy.xop_variable.Variable + +.. autosignature:: mlprodict.npy.xop.OnnxOperator + +.. autosignature:: mlprodict.npy.xop.OnnxOperatorItem -.. autosignature:: mlprodict.npy.xop_ops._GraphBuilder +.. autosignature:: mlprodict.npy.xop_convert.OnnxSubOnnx -.. autosignature:: mlprodict.npy.xop_ops.OnnxOperator +.. autosignature:: mlprodict.npy.xop_convert.OnnxSubEstimator -.. autosignature:: mlprodict.npy.xop_ops.OnnxOperatorItem +Helpers to handle API changing with opsets +++++++++++++++++++++++++++++++++++++++++++ .. autosignature:: mlprodict.npy.xop_opset.OnnxReduceSumApi11 @@ -41,7 +49,7 @@ API .. autosignature:: mlprodict.npy.xop_opset.OnnxReshapeApi13 Available ONNX operators -++++++++++++++++++++++++ +======================== .. toctree:: diff --git a/_doc/sphinxdoc/source/tutorial/index.rst b/_doc/sphinxdoc/source/tutorial/index.rst index b0336d008..afe8bec14 100644 --- a/_doc/sphinxdoc/source/tutorial/index.rst +++ b/_doc/sphinxdoc/source/tutorial/index.rst @@ -5,11 +5,26 @@ Tutorial The only tutorial is about :epkg:`ONNX` and only one piece this module can do. More should follow. +.. contents:: + :local: + +Run inference ++++++++++++++ + .. toctree:: :maxdepth: 1 - onnx - onnx_numpy - numpy_api_onnx + skl + onnx_runtime optim benchmark + +Write custom ONNX graph ++++++++++++++++++++++++ + +.. toctree:: + :maxdepth: 1 + + onnx_numpy + numpy_api_onnx + xop_api diff --git a/_doc/sphinxdoc/source/tutorial/numpy_api_onnx.rst b/_doc/sphinxdoc/source/tutorial/numpy_api_onnx.rst index 9d428e8d8..b2a73bb2d 100644 --- a/_doc/sphinxdoc/source/tutorial/numpy_api_onnx.rst +++ b/_doc/sphinxdoc/source/tutorial/numpy_api_onnx.rst @@ -48,7 +48,7 @@ Following example shows how to replace *numpy* by *ONNX*. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -187,7 +187,7 @@ One instance is added in a pipeline trained on the Iris dataset. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -342,7 +342,7 @@ is used. Let's see how to do it. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning import numpy from pandas import DataFrame @@ -455,7 +455,7 @@ the class is a transformer and automatically adds method .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning import numpy from pandas import DataFrame @@ -517,7 +517,7 @@ with arguments :class:`onnxnumpy_np .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -565,7 +565,7 @@ as an argument of `to_onnx`. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -621,7 +621,7 @@ another operator. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: import numpy as np @@ -713,7 +713,7 @@ the conversion to ONNX :meth:`to_algebra .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -763,7 +763,7 @@ types. If types are different, one must be cast into the other one. .. runpython:: :showcode: :exception: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -790,7 +790,7 @@ except one. .. runpython:: :showcode: :exception: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -841,7 +841,7 @@ a new one supporting custom functions implemented this API. .. runpython:: :showcode: :exception: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -893,7 +893,7 @@ does. However it produces the following error. .. runpython:: :showcode: :exception: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: import numpy @@ -947,7 +947,7 @@ in class @see cl OnnxVar. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any @@ -995,7 +995,7 @@ is called. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: from typing import Any diff --git a/_doc/sphinxdoc/source/tutorial/onnx_numpy.rst b/_doc/sphinxdoc/source/tutorial/onnx_numpy.rst index b540bdc7d..56fe17a68 100644 --- a/_doc/sphinxdoc/source/tutorial/onnx_numpy.rst +++ b/_doc/sphinxdoc/source/tutorial/onnx_numpy.rst @@ -1,8 +1,8 @@ .. _l-numpy2onnx-tutorial: -From numpy to ONNX -================== +Create custom ONNX graphs +========================= Converting a :epkg:`scikit-learn` pipeline is easy when the pipeline contains only pieces implemented in :epkg:`scikit-learn` @@ -25,7 +25,7 @@ the first examples of `sklearn-onnx tutorial`. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning import numpy from sklearn.pipeline import make_pipeline @@ -55,8 +55,8 @@ into *ONNX*. Even if function :epkg:`numpy:log` does exist in ONNX specification this problem is equivalent to a translation from a language, Python, to another one, ONNX. -Translating numpy to ONNX -+++++++++++++++++++++++++ +Translating numpy to ONNX with AST +++++++++++++++++++++++++++++++++++ .. index:: algebric function @@ -81,7 +81,7 @@ produces the :epkg:`ONNX` graph. .. runpython:: :showcode: - :warningout: DeprecationWarning + :warningout: DeprecationWarning, FutureWarning :process: :store_in_file: fct2onnx_expsine.py @@ -95,7 +95,7 @@ produces the :epkg:`ONNX` graph. # The function to convert into ONNX. def kernel_call_ynone(X, length_scale=1.2, periodicity=1.1, - pi=3.141592653589793): + pi=3.141592653589793, op_version=15): # squareform(pdist(X, ...)) in one function. dists = squareform_pdist(X, metric='euclidean') @@ -140,7 +140,7 @@ produces the :epkg:`ONNX` graph. # Calls the ONNX algebric function to produce the ONNX graph. inputs = {'X': x.astype(numpy.float32)} - onnx_g = onnx_model.to_onnx(inputs, target_opset=12) + onnx_g = onnx_model.to_onnx(inputs, target_opset=15) # Creates a python runtime associated to the ONNX function. oinf = OnnxInference(onnx_g) diff --git a/_doc/sphinxdoc/source/tutorial/onnx.rst b/_doc/sphinxdoc/source/tutorial/onnx_runtime.rst similarity index 82% rename from _doc/sphinxdoc/source/tutorial/onnx.rst rename to _doc/sphinxdoc/source/tutorial/onnx_runtime.rst index f16d4a6dc..b77bfeed7 100644 --- a/_doc/sphinxdoc/source/tutorial/onnx.rst +++ b/_doc/sphinxdoc/source/tutorial/onnx_runtime.rst @@ -1,8 +1,8 @@ .. _l-onnx-tutorial: -ONNX and Python Runtime -======================= +Execute ONNX graphs +=================== This package implements a python runtime for ONNX in class :class:`OnnxInference `. @@ -184,37 +184,3 @@ As a consequence, interdiate results cannot be seen anymore. oinf = OnnxInference(model_def, runtime='python_compiled') print(oinf.run({'X': X_test[:5]})) - -From scikit-learn to ONNX -+++++++++++++++++++++++++ - -Function `skl2onnx.to_onnx `_ is the -main entrypoint to convert a *scikit-learn* pipeline into ONNX. -The same function was extended in this package into -:func:`to_onnx ` to handle -dataframes, an extended list of supported converters, scorers. -It works exactly the same: - -.. runpython:: - :showcode: - :warningout: DeprecationWarning - - import numpy - from sklearn.datasets import load_iris - from sklearn.model_selection import train_test_split - from sklearn.cluster import KMeans - from mlprodict.onnx_conv import to_onnx - from mlprodict.onnxrt import OnnxInference - - iris = load_iris() - X = iris.data.astype(numpy.float32) - X_train, X_test = train_test_split(X) - clr = KMeans(n_clusters=3) - clr.fit(X_train) - - model_def = to_onnx(clr, X_train.astype(numpy.float32), - target_opset=12) - - oinf = OnnxInference(model_def, runtime='python') - print(oinf.run({'X': X_test[:5]})) diff --git a/_doc/sphinxdoc/source/tutorial/skl.rst b/_doc/sphinxdoc/source/tutorial/skl.rst new file mode 100644 index 000000000..912a4ae8f --- /dev/null +++ b/_doc/sphinxdoc/source/tutorial/skl.rst @@ -0,0 +1,33 @@ +From scikit-learn to ONNX +========================= + +Function `skl2onnx.to_onnx `_ is the +main entrypoint to convert a *scikit-learn* pipeline into ONNX. +The same function was extended in this package into +:func:`to_onnx ` to handle +dataframes, an extended list of supported converters, scorers. +It works exactly the same: + +.. runpython:: + :showcode: + :warningout: DeprecationWarning + + import numpy + from sklearn.datasets import load_iris + from sklearn.model_selection import train_test_split + from sklearn.cluster import KMeans + from mlprodict.onnx_conv import to_onnx + from mlprodict.onnxrt import OnnxInference + + iris = load_iris() + X = iris.data.astype(numpy.float32) + X_train, X_test = train_test_split(X) + clr = KMeans(n_clusters=3) + clr.fit(X_train) + + model_def = to_onnx(clr, X_train.astype(numpy.float32), + target_opset=12) + + oinf = OnnxInference(model_def, runtime='python') + print(oinf.run({'X': X_test[:5]})) diff --git a/_doc/sphinxdoc/source/tutorial/xop_api.rst b/_doc/sphinxdoc/source/tutorial/xop_api.rst new file mode 100644 index 000000000..a299f3edf --- /dev/null +++ b/_doc/sphinxdoc/source/tutorial/xop_api.rst @@ -0,0 +1,4 @@ +Xop API +======= + +*to be completed* diff --git a/_unittests/ut_npy/test_xop_convert.py b/_unittests/ut_npy/test_xop_convert.py new file mode 100644 index 000000000..447479d7f --- /dev/null +++ b/_unittests/ut_npy/test_xop_convert.py @@ -0,0 +1,101 @@ +# pylint: disable=E0611 +""" +@brief test log(time=15s) +""" +import unittest +import numpy +from pyquickhelper.pycode import ExtTestCase +from sklearn.datasets import make_regression +from sklearn.linear_model import LinearRegression +from mlprodict.onnxrt import OnnxInference +from mlprodict.npy.xop import loadop +from mlprodict.npy.xop_convert import OnnxSubOnnx, OnnxSubEstimator +from mlprodict.npy.xop_variable import max_supported_opset + + +class TestXOpsConvert(ExtTestCase): + + def test_onnx_abs(self): + OnnxAbs = loadop("Abs") + ov = OnnxAbs('X', output_names=['Y']) + onx = ov.to_onnx(numpy.float32, numpy.float32, verbose=0) + + sub = OnnxSubOnnx(onx, 'X', output_names=['Y']) + onx = sub.to_onnx(numpy.float32, numpy.float32, verbose=0) + + oinf = OnnxInference(onx) + x = numpy.array([-2, 2], dtype=numpy.float32) + got = oinf.run({'X': x}) + self.assertEqualArray(numpy.abs(x), got['Y']) + + def test_onnx_add(self): + OnnxAdd = loadop("Add") + ov = OnnxAdd('X', numpy.array([2], dtype=numpy.float32), + output_names=['Y']) + onx = ov.to_onnx(numpy.float32, numpy.float32, verbose=0) + + sub = OnnxSubOnnx(onx, 'X', output_names=['Y']) + onx = sub.to_onnx(numpy.float32, numpy.float32, verbose=0) + + oinf = OnnxInference(onx) + x = numpy.array([-2, 2], dtype=numpy.float32) + got = oinf.run({'X': x}) + self.assertEqualArray(x + 2, got['Y']) + + def test_onnx_cast(self): + OnnxCast = loadop("Cast") + ov = OnnxCast('X', to=numpy.int64, output_names=['Y']) + onx = ov.to_onnx(numpy.float32, numpy.float32, verbose=0) + + sub = OnnxSubOnnx(onx, 'X', output_names=['Y']) + onx = sub.to_onnx(numpy.float32, numpy.int64, verbose=0) + r = repr(sub) + self.assertStartsWith('OnnxSubOnnx(..., output_name', r) + + oinf = OnnxInference(onx) + x = numpy.array([-2.4, 2.4], dtype=numpy.float32) + got = oinf.run({'X': x}) + self.assertEqualArray(x.astype(numpy.int64), got['Y']) + + def test_onnx_lr(self): + X, y = make_regression(n_features=2) # pylint: disable=W0632 + lr = LinearRegression() + lr.fit(X, y) + X32 = X.astype(numpy.float32) + + OnnxIdentity, OnnxReshape = loadop("Identity", "Reshape") + ov = OnnxIdentity('X') + self.assertRaise(lambda: OnnxSubEstimator(lr, ov), NotImplementedError) + sub = OnnxSubEstimator( + lr, ov, op_version=max_supported_opset(), + initial_types=X32[:1]) + r = repr(sub) + self.assertStartsWith('OnnxSubEstimator(LinearRegression()', r) + last = OnnxReshape(sub, numpy.array([-1], dtype=numpy.int64), + output_names=['Y']) + onx = last.to_onnx(numpy.float32, numpy.float32, verbose=0) + + oinf = OnnxInference(onx) + got = oinf.run({'X': X32}) + expected = lr.predict(X32) + self.assertEqualArray(expected, got['Y'], decimal=4) + + def test_onnx_lr_only(self): + X, y = make_regression(n_features=2) # pylint: disable=W0632 + lr = LinearRegression() + lr.fit(X, y) + X32 = X.astype(numpy.float32) + + last = OnnxSubEstimator( + lr, 'X', op_version=max_supported_opset(), + initial_types=X32[:1], output_names=['Y']) + onx = last.to_onnx(numpy.float32, numpy.float32, verbose=0) + + oinf = OnnxInference(onx) + got = oinf.run({'X': X32}) + expected = lr.predict(X32) + self.assertEqualArray(expected, got['Y'].ravel(), decimal=4) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/mlprodict/npy/xop.py b/mlprodict/npy/xop.py index d7282d255..5e4e8b19c 100644 --- a/mlprodict/npy/xop.py +++ b/mlprodict/npy/xop.py @@ -1,7 +1,7 @@ # pylint: disable=E1101,C0302 """ @file -@brief Easier API to build onnx graphs. Inspired from :epkg:`skl2onnx`. +@brief Xop API to build onnx graphs. Inspired from :epkg:`skl2onnx`. .. versionadded:: 0.9 """ @@ -14,7 +14,7 @@ from onnx.helper import ( make_node, make_graph, make_model, make_tensor_value_info) -from onnx.numpy_helper import from_array +from onnx.numpy_helper import from_array, to_array from onnx.shape_inference import infer_shapes from ._cache import cache_folder from .xop_variable import ( @@ -40,6 +40,18 @@ def _default_OPSET_TO_IR_VERSION(): def _domain_to_class_name(domain): + """ + Converts domain into a name. + + :param domain: domain name such as `ai.onnx.ml` + :return: string + + .. runpython:: + :showcode: + + from mlprodict.npy.xop import _domain_to_class_name + print(_domain_to_class_name('ai.onnx.ml')) + """ if domain == 'ai.onnx': return '' dom = domain.split('.') @@ -229,7 +241,8 @@ def __init__(self, *args, **kwargs): return newclass -def _dynamic_class_creation(operator_names=None, cache=False, verbose=0, fLOG=print): +def _dynamic_class_creation(operator_names=None, cache=False, include_past=False, + verbose=0, fLOG=print): """ Automatically generates classes for each of the operators module *onnx* defines and described at @@ -242,6 +255,7 @@ def _dynamic_class_creation(operator_names=None, cache=False, verbose=0, fLOG=pr :param operator_names: list of operators to request or None for all :param cache: extract the documentation from onnx package and saves it on disk it True + :param include_past: includes past versions if operator_names is None :param verbose: display some progress :param fLOG: logging function :return: list of requested operators as a tuple @@ -254,6 +268,14 @@ def _c(obj, label, i): cache_dir = cache_folder() if operator_names is None: operator_names = list(_all_schemas_versions) + if include_past: + add = [] + for domain, op in operator_names: + add.extend( + [(domain, k) + for k in _all_schemas_versions[domain, op]]) + operator_names.extend(add) + operator_names.sort() # type verification ops = [] @@ -284,7 +306,7 @@ def _c(obj, label, i): if op_domain == 'ai.onnx': op_domain = '' set_names[op_domain, op_name] = pos - if '_' in op_name: + if '_' in op_name and not include_past: n = op_name.split('_')[0] set_skip.add((op_domain, n)) if n not in set_names: @@ -1424,6 +1446,19 @@ def get_input_names(self, node, inputs): "Unexpected type for an input %r." % type(i)) return names + def add_initializer(self, name, init): + """ + Adds an initializer to the graph. + + :param name: initializer name + :param init: initializer to copy + :return: created intializer + """ + value = to_array(init) + val = from_array(value, name) + self.initializer.append(val) + return val + def add_node(self, op_type, name, inputs, outputs, domain='', opset=None, **attributes): """ @@ -1435,6 +1470,7 @@ def add_node(self, op_type, name, inputs, outputs, domain='', :param outputs: outputs name list :param domain: node domain :param opset: node opset + :return: created node """ if not isinstance(inputs, list): raise TypeError( # pragma: no cover @@ -1456,6 +1492,7 @@ def add_node(self, op_type, name, inputs, outputs, domain='', node = make_node(op_type, inputs, outputs, name=name, domain=domain, **attributes) self.node.append(node) + return node def _process_io(self, inputs, input_names): if inputs is None: diff --git a/mlprodict/npy/xop_auto.py b/mlprodict/npy/xop_auto.py index 157a66d99..427bf4f76 100644 --- a/mlprodict/npy/xop_auto.py +++ b/mlprodict/npy/xop_auto.py @@ -1,6 +1,7 @@ """ @file -@brief Automates the generation of the documentation. +@brief Automates the generation of operators for the +documentation for the Xop API. .. versionadded:: 0.9 """ diff --git a/mlprodict/npy/xop_auto_import_.py b/mlprodict/npy/xop_auto_import_.py index 935388042..a6d82d076 100644 --- a/mlprodict/npy/xop_auto_import_.py +++ b/mlprodict/npy/xop_auto_import_.py @@ -1,6 +1,6 @@ """ @file -@brief Importing this file takes time. It should be avoided. +@brief Xop API. Importing this file takes time. It should be avoided. .. versionadded:: 0.9 """ diff --git a/mlprodict/npy/xop_convert.py b/mlprodict/npy/xop_convert.py new file mode 100644 index 000000000..3f0dc7b7e --- /dev/null +++ b/mlprodict/npy/xop_convert.py @@ -0,0 +1,253 @@ +""" +@file +@brief Easier API to build onnx graphs. Inspired from :epkg:`skl2onnx`. + +.. versionadded:: 0.9 +""" +import numpy +from .xop import OnnxOperator + + +class OnnxSubOnnx(OnnxOperator): + """ + This operator is used to insert existing ONNX into + the ONNX graph being built. + """ + + domain = 'mlprodict' + since_version = 1 + expected_inputs = None + expected_outputs = None + input_range = [1, 1e9] + output_range = [1, 1e9] + + def __init__(self, model, *inputs, output_names=None): + if model is None: + raise ValueError("Model cannot be None.") + if len(inputs) > len(model.graph.input): + raise RuntimeError( + "Unexpected number of inputs %r > expected %r." % ( + len(inputs), len(model.graph.input))) + if (output_names is not None and + len(output_names) != len(model.graph.output)): + raise RuntimeError( + "Unexpected number of outputs %r != expected %r." % ( + len(output_names), len(model.graph.output))) + OnnxOperator.__init__(self, *inputs, output_names=output_names) + self.model = model + + def __repr__(self): + "usual" + atts = {} + for att in ['output_names']: + value = getattr(self, att, None) + if value is not None: + atts[att] = value + atts.update(self.kwargs) + msg = ", ".join("%s=%r" % (k, v) for k, v in atts.items()) + if len(atts) > 0: + msg = ", " + msg + return "%s(...%s)" % ( + self.__class__.__name__, msg) + + def add_to(self, builder): + """ + Adds to graph builder. + + :param builder: instance of @see cl _GraphBuilder, + it must have a method `add_node` + """ + inputs = builder.get_input_names(self, self.inputs) + n_outputs = len(self.model.graph.output) + outputs = [builder.get_output_name(self, i) for i in range(n_outputs)] + + mapped_names = {} + + # adding initializers + for init in self.model.graph.initializer: + new_name = builder.get_unique_name(init.name) + mapped_names[init.name] = new_name + builder.add_initializer(new_name, init) + + # linking inputs + for inp, name in zip(self.model.graph.input, inputs): + new_name = builder.get_unique_name(inp.name) + mapped_names[inp.name] = new_name + builder.add_node( + 'Identity', builder.get_unique_name('_sub_' + name), + [name], [new_name]) + + # adding nodes + for node in self.model.graph.node: + new_inputs = [] + for i in node.input: + if i not in mapped_names: + raise RuntimeError( + "Unable to find input %r in %r." % (i, mapped_names)) + new_inputs.append(mapped_names[i]) + new_outputs = [] + for o in node.output: + new_name = builder.get_unique_name(o) + mapped_names[o] = new_name + new_outputs.append(new_name) + + atts = {} + for att in node.attribute: + if att.type == 2: # .i + value = att.i + atts[att.name] = value + continue + if att.type == 6: # .floats + value = list(att.floats) + atts[att.name] = value + continue + raise NotImplementedError( + "Unable to copy attribute type %r (%r)." % ( + att.type, att)) + + builder.add_node( + node.op_type, + builder.get_unique_name('_sub_' + node.name), + new_inputs, new_outputs, domain=node.domain, **atts) + + # linking outputs + for out, name in zip(self.model.graph.output, outputs): + builder.add_node( + 'Identity', builder.get_unique_name('_sub_' + out.name), + [mapped_names[out.name]], [name]) + + +class OnnxSubEstimator(OnnxSubOnnx): + """ + This operator is used to call the converter of a model + to insert the node coming from the conversion into a + bigger ONNX graph. It supports model from :epkg:`scikit-learn` + using :epkg:`sklearn-onnx`. + + :param model: model to convert + :param inputs: inputs + :param op_version: targetted opset + :param options: to rewrite the options used to convert the model + :param initial_types: the implementation may be wrong in guessing + the input types of the model, this parameter can be used + to overwrite them, usually a dictionary + `{ input_name: numpy array as an example }` + :param kwargs: any other parameters such as black listed or + white listed operators + """ + + since_version = 1 + expected_inputs = None + expected_outputs = None + input_range = [1, 1e9] + output_range = [1, 1e9] + + def __init__(self, model, *inputs, op_version=None, + output_names=None, options=None, + initial_types=None, **kwargs): + if model is None: + raise ValueError("Model cannot be None.") + onx = OnnxSubEstimator._to_onnx( + model, inputs, op_version=op_version, options=options, + initial_types=initial_types, **kwargs) + OnnxSubOnnx.__init__( + self, onx, *inputs, output_names=output_names) + self.ml_model = model + self.options = options + self.initial_types = initial_types + self.op_version = op_version + + def __repr__(self): + "usual" + atts = {} + for att in ['op_version', 'output_names', 'options', + 'initial_types']: + value = getattr(self, att, None) + if value is not None: + atts[att] = value + atts.update(self.kwargs) + msg = ", ".join("%s=%r" % (k, v) for k, v in atts.items()) + if len(atts) > 0: + msg = ", " + msg + return "%s(%r%s)" % ( + self.__class__.__name__, self.ml_model, msg) + + @staticmethod + def _to_onnx(model, inputs, op_version=None, options=None, + initial_types=None, **kwargs): + """ + Converts a model into ONNX and inserts it into an ONNX graph. + + :param model: a trained machine learned model + :param inputs: inputs + :param op_version: opset versions or None to use the latest one + :param options: options to change the behaviour of the converter + :param kwargs: additional parameters such as black listed or while listed + operators + :return: ONNX model + + The method currently supports models trained with + :epkg:`scikit-learn`, :epkg:`xgboost`, :epkg`:lightgbm`. + """ + from sklearn.base import BaseEstimator + + if isinstance(model, BaseEstimator): + return OnnxSubEstimator._to_onnx_sklearn( + model, inputs, op_version=op_version, options=options, + initial_types=initial_types, **kwargs) + raise RuntimeError( + "Unable to convert into ONNX model type %r." % type(model)) + + @staticmethod + def _to_onnx_sklearn(model, inputs, op_version=None, options=None, + initial_types=None, **kwargs): + """ + Converts a :epkg:`scikit-learn` model into ONNX + and inserts it into an ONNX graph. The library relies on + function @see fn to_onnx and library :epkg:`skearn-onnx`. + + :param model: a trained machine learned model + :param inputs: inputs + :param op_version: opset versions or None to use the latest one + :param initial_types: if None, the input types are guessed from the + inputs. The function converts into ONNX the previous + node of the graph and tries to infer the initial_types + with the little informations it has. It may not work. + It is recommended to specify this parameter. + :param options: options to change the behaviour of the converter + :param kwargs: additional parameters such as black listed or while listed + operators + :return: ONNX model + + Default options is `{'zipmap': False}` for a classifier. + """ + from ..onnx_conv.convert import to_onnx + if options is None: + from sklearn.base import ClassifierMixin + if isinstance(model, ClassifierMixin): + options = {'zipmap': False} + if initial_types is None: + # Let's to infer them from previous nodes. + raise NotImplementedError( + "initial_types is None and the method cannot guess the " + "initial_types of the model.") + + if isinstance(initial_types, numpy.ndarray): + if len(inputs) != 1: + raise RuntimeError( + "The model has %s inputs but only %d input are " + "described in 'initial_types'." % ( + len(inputs), 1)) + X = initial_types + initial_types = None + elif len(inputs) != len(initial_types): + raise RuntimeError( + "The model has %s inputs but only %d input are " + "described in 'initial_types'." % ( + len(inputs), len(initial_types))) + else: + X = None + + onx = to_onnx(model, X, initial_types=initial_types, options=options, + rewrite_ops=True, target_opset=op_version, **kwargs) + return onx diff --git a/mlprodict/npy/xop_opset.py b/mlprodict/npy/xop_opset.py index d8a25d734..d5f790c39 100644 --- a/mlprodict/npy/xop_opset.py +++ b/mlprodict/npy/xop_opset.py @@ -1,7 +1,7 @@ # pylint: disable=E0602 """ @file -@brief Easier API to build onnx graphs. Inspired from :epkg:`skl2onnx`. +@brief Xop API to build onnx graphs. Inspired from :epkg:`skl2onnx`. .. versionadded:: 0.9 """ diff --git a/mlprodict/npy/xop_variable.py b/mlprodict/npy/xop_variable.py index 8a0b528ff..cc0cbace5 100644 --- a/mlprodict/npy/xop_variable.py +++ b/mlprodict/npy/xop_variable.py @@ -1,6 +1,6 @@ """ @file -@brief Easier API to build onnx graphs. Inspired from :epkg:`skl2onnx`. +@brief Xop API to build onnx graphs. Inspired from :epkg:`skl2onnx`. .. versionadded:: 0.9 """ diff --git a/mlprodict/onnx_tools/onnx_grammar/onnx_translation.py b/mlprodict/onnx_tools/onnx_grammar/onnx_translation.py index 8d7e2df1b..c33dc8dff 100644 --- a/mlprodict/onnx_tools/onnx_grammar/onnx_translation.py +++ b/mlprodict/onnx_tools/onnx_grammar/onnx_translation.py @@ -197,10 +197,10 @@ def trs(x, y): import numpy from mlprodict.onnx_tools.onnx_grammar import translate_fct2onnx + from mlprodict.plotting.text_plot import onnx_simple_text_plot from mlprodict.onnxrt import OnnxInference from skl2onnx.algebra.onnx_ops import ( - OnnxAdd, OnnxTranspose, OnnxMul, OnnxIdentity - ) + OnnxAdd, OnnxTranspose, OnnxMul, OnnxIdentity) ctx = {'OnnxAdd': OnnxAdd, 'OnnxTranspose': OnnxTranspose, @@ -222,16 +222,17 @@ def trs(x, y): trs, context={'numpy.transpose': numpy.transpose}, cpl=True, context_cpl=ctx, output_names=['Z']) - onnx_code = onnx_fct('x', 'y', opset_version=12) - print('ONNX code:', onnx_code) + onnx_code = onnx_fct('x', 'y', op_version=12) onnx_g = onnx_code.to_onnx(inputs, target_opset=12) + print("ONNX model") + print(onnx_simple_text_plot(onnx_g)) oinf = OnnxInference(onnx_g) res = oinf.run(inputs) + print('-----------') print("ONNX inference:", res['Z']) - print("ONNX graph:", onnx_g) The function to be converted may include python functions which must not be converted. In that case, their name diff --git a/mlprodict/onnxrt/doc/doc_helper.py b/mlprodict/onnxrt/doc/doc_helper.py index ffae0816a..4a5a30753 100644 --- a/mlprodict/onnxrt/doc/doc_helper.py +++ b/mlprodict/onnxrt/doc/doc_helper.py @@ -352,7 +352,7 @@ def visual_rst_template(): Fitted on a problem type *{{ kind }}* (see :func:`find_suitable_problem `), - method {{ method }} matches output {{ output_index }}. + method `{{ method }}` matches output {{ output_index }}. {{ optim_param }} ::