diff --git a/README.rst b/README.rst index da1de2035..d3fd35197 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,7 @@ mlprodict :target: https://github.com/sdpython/mlprodict/ :alt: size -The packages explores ways to productionize machine learning predictions. +*mlprodict* explores ways to productionize machine learning predictions. One approach uses *ONNX* and tries to implement a runtime in python / numpy or wraps `onnxruntime `_ diff --git a/_doc/sphinxdoc/source/api/npy.rst b/_doc/sphinxdoc/source/api/npy.rst index c4ac54420..34d080983 100644 --- a/_doc/sphinxdoc/source/api/npy.rst +++ b/_doc/sphinxdoc/source/api/npy.rst @@ -82,6 +82,8 @@ Decorators .. autosignature:: mlprodict.npy.onnx_sklearn_wrapper.onnxsklearn_class +.. autosignature:: mlprodict.npy.onnx_sklearn_wrapper.onnxsklearn_classifier + .. autosignature:: mlprodict.npy.onnx_sklearn_wrapper.onnxsklearn_regressor .. autosignature:: mlprodict.npy.onnx_sklearn_wrapper.onnxsklearn_transformer @@ -98,6 +100,12 @@ OnnxVar .. autosignature:: mlprodict.npy.onnx_variable.OnnxVar :members: +.. autosignature:: mlprodict.npy.onnx_variable.MultiOnnxVar + :members: + +.. autosignature:: mlprodict.npy.onnx_variable.TupleOnnxAny + :members: + Registration ++++++++++++ diff --git a/_unittests/ut_npy/test_custom_classifier.py b/_unittests/ut_npy/test_custom_classifier.py new file mode 100644 index 000000000..b84d386e4 --- /dev/null +++ b/_unittests/ut_npy/test_custom_classifier.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +""" +@brief test log(time=3s) +""" +import unittest +import warnings +import io +import pickle +from logging import getLogger +import numpy +from scipy.special import expit # pylint: disable=E0611 +from sklearn.base import ClassifierMixin, BaseEstimator +from sklearn.linear_model import LogisticRegression +from pyquickhelper.pycode import ExtTestCase, ignore_warnings +from skl2onnx import update_registered_converter +from skl2onnx.algebra.onnx_ops import ( # pylint: disable=E0611 + OnnxIdentity, OnnxMatMul, OnnxAdd, OnnxSigmoid, OnnxArgMax) +from skl2onnx.common.data_types import guess_numpy_type, Int64TensorType +from mlprodict.onnx_conv import to_onnx +from mlprodict.onnxrt import OnnxInference +from mlprodict.npy import onnxsklearn_classifier, onnxsklearn_class +import mlprodict.npy.numpy_onnx_impl as nxnp + + +class CustomLinearClassifier(ClassifierMixin, BaseEstimator): + def __init__(self): + BaseEstimator.__init__(self) + ClassifierMixin.__init__(self) + + def fit(self, X, y=None, sample_weights=None): + lr = LogisticRegression().fit(X, y, sample_weights) + self.coef_ = lr.coef_ # pylint: disable=W0201 + self.intercept_ = lr.intercept_ # pylint: disable=W0201 + if len(y.shape) == 1 or y.shape[1] == 1: + # binary class + self.coef_ = numpy.vstack( # pylint: disable=W0201 + [-self.coef_, self.coef_]) # pylint: disable=E1130 + self.intercept_ = numpy.vstack( # pylint: disable=W0201 + [-self.intercept_, self.intercept_]).T # pylint: disable=E1130 + return self + + def predict_proba(self, X): + return expit(X @ self.coef_ + self.intercept_) + + def predict(self, X): + prob = self.predict_proba(X) + return numpy.argmax(prob, axis=1) + + +def custom_linear_classifier_shape_calculator(operator): + op = operator.raw_operator + input_type = operator.inputs[0].type.__class__ + input_dim = operator.inputs[0].type.shape[0] + lab_type = Int64TensorType([input_dim]) + prob_type = input_type([input_dim, op.coef_.shape[-1]]) + operator.outputs[0].type = lab_type + operator.outputs[1].type = prob_type + + +def custom_linear_classifier_converter(scope, operator, container): + op = operator.raw_operator + opv = container.target_opset + out = operator.outputs + X = operator.inputs[0] + dtype = guess_numpy_type(X.type) + raw = OnnxAdd( + OnnxMatMul(X, op.coef_.astype(dtype), op_version=opv), + op.intercept_.astype(dtype), op_version=opv) + prob = OnnxSigmoid(raw, op_version=opv) + label = OnnxArgMax(prob, axis=1, op_version=opv) + Yl = OnnxIdentity(label, op_version=opv, output_names=out[:1]) + Yp = OnnxIdentity(prob, op_version=opv, output_names=out[1:]) + Yl.add_to(scope, container) + Yp.add_to(scope, container) + + +class CustomLinearClassifier3(CustomLinearClassifier): + pass + + +@onnxsklearn_classifier(register_class=CustomLinearClassifier3) +def custom_linear_classifier_converter3(X, op_=None): + if X.dtype is None: + raise AssertionError("X.dtype cannot be None.") + if isinstance(X, numpy.ndarray): + raise TypeError("Unexpected type %r." % X) + if op_ is None: + raise AssertionError("op_ cannot be None.") + coef = op_.coef_.astype(X.dtype) + intercept = op_.intercept_.astype(X.dtype) + prob = nxnp.expit((X @ coef) + intercept) + label = nxnp.argmax(prob, axis=1) + return nxnp.xtuple(label, prob) + + +@onnxsklearn_class("onnx_predict") +class CustomLinearClassifierOnnx(ClassifierMixin, BaseEstimator): + def __init__(self): + BaseEstimator.__init__(self) + ClassifierMixin.__init__(self) + + def fit(self, X, y=None, sample_weights=None): + lr = LogisticRegression().fit(X, y, sample_weights) + self.coef_ = lr.coef_ # pylint: disable=W0201 + self.intercept_ = lr.intercept_ # pylint: disable=W0201 + if len(y.shape) == 1 or y.shape[1] == 1: + # binary class + self.coef_ = numpy.vstack( # pylint: disable=W0201 + [-self.coef_, self.coef_]) # pylint: disable=E1130 + self.intercept_ = numpy.vstack( # pylint: disable=W0201 + [-self.intercept_, self.intercept_]).T # pylint: disable=E1130 + return self + + def onnx_predict(self, X): + if X.dtype is None: + raise AssertionError("X.dtype cannot be None.") + if isinstance(X, numpy.ndarray): + raise TypeError("Unexpected type %r." % X) + coef = self.coef_.astype(X.dtype) + intercept = self.intercept_.astype(X.dtype) + prob = nxnp.expit((X @ coef) + intercept) + label = nxnp.argmax(prob, axis=1) + return nxnp.xtuple(label, prob) + + +class TestCustomClassifier(ExtTestCase): + + def setUp(self): + logger = getLogger('skl2onnx') + logger.disabled = True + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ResourceWarning) + update_registered_converter( + CustomLinearClassifier, "SklearnCustomLinearClassifier", + custom_linear_classifier_shape_calculator, + custom_linear_classifier_converter) + + @ignore_warnings((DeprecationWarning, RuntimeWarning)) + def test_function_classifier(self): + X = numpy.random.randn(20, 2).astype(numpy.float32) + y = ((X.sum(axis=1) + numpy.random.randn( + X.shape[0]).astype(numpy.float32)) >= 0).astype(numpy.int64) + dec = CustomLinearClassifier() + dec.fit(X, y) + onx = to_onnx(dec, X.astype(numpy.float32)) + oinf = OnnxInference(onx) + exp = dec.predict(X) + prob = dec.predict_proba(X) + got = oinf.run({'X': X}) + self.assertEqualArray(exp, got['label'].ravel()) + self.assertEqualArray(prob, got['probabilities']) + + @ignore_warnings((DeprecationWarning, RuntimeWarning)) + def test_function_classifier3_float32(self): + X = numpy.random.randn(20, 2).astype(numpy.float32) + y = ((X.sum(axis=1) + numpy.random.randn( + X.shape[0]).astype(numpy.float32)) >= 0).astype(numpy.int64) + dec = CustomLinearClassifier3() + dec.fit(X, y) + onx = to_onnx(dec, X.astype(numpy.float32)) + oinf = OnnxInference(onx) + exp = dec.predict(X) + prob = dec.predict_proba(X) # pylint: disable=W0612 + got = oinf.run({'X': X}) + self.assertEqualArray(exp, got['label']) + self.assertEqualArray(prob, got['probabilities']) + X2, P2 = custom_linear_classifier_converter3( # pylint: disable=E0633 + X, op_=dec) + self.assertEqualArray(X2, got['label']) + self.assertEqualArray(P2, got['probabilities']) + + @ignore_warnings((DeprecationWarning, RuntimeWarning)) + def test_function_classifier3_float64(self): + X = numpy.random.randn(20, 2).astype(numpy.float64) + y = ((X.sum(axis=1) + numpy.random.randn( + X.shape[0]).astype(numpy.float32)) >= 0).astype(numpy.int64) + dec = CustomLinearClassifier3() + dec.fit(X, y) + onx = to_onnx(dec, X.astype(numpy.float64)) + oinf = OnnxInference(onx) + exp = dec.predict(X) + prob = dec.predict_proba(X) + got = oinf.run({'X': X}) + self.assertEqualArray(exp, got['label']) + self.assertEqualArray(prob, got['probabilities']) + X2, P2 = custom_linear_classifier_converter3( # pylint: disable=E0633 + X, op_=dec) + self.assertEqualArray(X2, got['label']) + self.assertEqualArray(P2, got['probabilities']) + + @ignore_warnings((DeprecationWarning, RuntimeWarning)) + def test_function_classifier_onnx_float32(self): + X = numpy.random.randn(20, 2).astype(numpy.float32) + y = ((X.sum(axis=1) + numpy.random.randn( + X.shape[0]).astype(numpy.float32)) >= 0).astype(numpy.int64) + dec = CustomLinearClassifierOnnx() + dec.fit(X, y) + res = dec.onnx_predict_(X) # pylint: disable=E1101 + self.assertNotEmpty(res) + exp1 = dec.predict(X) # pylint: disable=E1101 + prob1 = dec.predict_proba(X) # pylint: disable=E1101 + onx = to_onnx(dec, X.astype(numpy.float32)) + oinf = OnnxInference(onx) + exp2 = dec.predict(X) # pylint: disable=E1101 + prob2 = dec.predict_proba(X) # pylint: disable=E1101 + got = oinf.run({'X': X}) + self.assertEqualArray(prob1, res[1]) + self.assertEqualArray(prob1, got['probabilities']) + self.assertEqualArray(prob2, got['probabilities']) + self.assertEqualArray(exp1, res[0]) + self.assertEqualArray(exp1, got['label']) + self.assertEqualArray(exp2, got['label']) + + @ignore_warnings((DeprecationWarning, RuntimeWarning)) + def test_function_classifier_onnx_float64(self): + X = numpy.random.randn(20, 2).astype(numpy.float64) + y = ((X.sum(axis=1) + numpy.random.randn( + X.shape[0]).astype(numpy.float64)) >= 0).astype(numpy.int64) + dec = CustomLinearClassifierOnnx() + dec.fit(X, y) + res = dec.onnx_predict_(X) # pylint: disable=E1101 + self.assertIsInstance(res, tuple) + self.assertEqual(len(res), 2) + exp1 = dec.predict(X) # pylint: disable=E1101 + prob1 = dec.predict_proba(X) # pylint: disable=E1101 + onx = to_onnx(dec, X.astype(numpy.float64)) + oinf = OnnxInference(onx) + exp2 = dec.predict(X) # pylint: disable=E1101 + prob2 = dec.predict_proba(X) # pylint: disable=E1101 + got = oinf.run({'X': X}) + self.assertEqualArray(exp1, got['label']) + self.assertEqualArray(exp2, got['label']) + self.assertEqualArray(prob1, got['probabilities']) + self.assertEqualArray(prob2, got['probabilities']) + + @ignore_warnings((DeprecationWarning, RuntimeWarning)) + def test_function_classifier_onnx_pickle(self): + X = numpy.random.randn(20, 2).astype(numpy.float64) + y = ((X.sum(axis=1) + numpy.random.randn( + X.shape[0]).astype(numpy.float32)) >= 0).astype(numpy.int64) + dec = CustomLinearClassifierOnnx() + dec.fit(X, y) + exp1 = dec.predict(X) # pylint: disable=E1101 + prob1 = dec.predict_proba(X) # pylint: disable=E1101 + st = io.BytesIO() + pickle.dump(dec, st) + dec2 = pickle.load(io.BytesIO(st.getvalue())) + exp2 = dec2.predict(X) + prob2 = dec2.predict_proba(X) + self.assertEqualArray(exp1, exp2) + self.assertEqualArray(prob1, prob2) + + +if __name__ == "__main__": + unittest.main() diff --git a/_unittests/ut_npy/test_custom_predictor.py b/_unittests/ut_npy/test_custom_regressor.py similarity index 87% rename from _unittests/ut_npy/test_custom_predictor.py rename to _unittests/ut_npy/test_custom_regressor.py index e56c1aafa..0164f9659 100644 --- a/_unittests/ut_npy/test_custom_predictor.py +++ b/_unittests/ut_npy/test_custom_regressor.py @@ -18,6 +18,7 @@ from mlprodict.onnx_conv import to_onnx from mlprodict.onnxrt import OnnxInference from mlprodict.npy import onnxsklearn_regressor, onnxsklearn_class +import mlprodict.npy.numpy_onnx_impl as nxnp class CustomLinearRegressor(RegressorMixin, BaseEstimator): @@ -61,12 +62,14 @@ class CustomLinearRegressor3(CustomLinearRegressor): @onnxsklearn_regressor(register_class=CustomLinearRegressor3) -def custom_linear_regressor_converter3(X, op=None): +def custom_linear_regressor_converter3(X, op_=None): + if op_ is None: + raise AssertionError("op_ cannot be None.") if X.dtype is None: raise AssertionError("X.dtype cannot be None.") - coef = op.coef_.astype(X.dtype) - intercept = op.intercept_.astype(X.dtype) - return (X @ coef) + intercept + coef = op_.coef_.astype(X.dtype) + intercept = op_.intercept_.astype(X.dtype) + return nxnp.identity((X @ coef) + intercept) @onnxsklearn_class("onnx_predict") @@ -82,10 +85,10 @@ def fit(self, X, y=None, sample_weights=None): return self def onnx_predict(self, X): - return X @ self.coef_ + self.intercept_ + return nxnp.identity(X @ self.coef_.astype(X.dtype) + self.intercept_.astype(X.dtype)) -class TestCustomTransformer(ExtTestCase): +class TestCustomRegressor(ExtTestCase): def setUp(self): logger = getLogger('skl2onnx') @@ -117,12 +120,16 @@ def test_function_regressor3_float32(self): X.shape[0]).astype(numpy.float32)) dec = CustomLinearRegressor3() dec.fit(X, y) + exp = dec.predict(X) + print("**g", id(dec.predict), dec.predict) + self.assertIsInstance(exp, numpy.ndarray) + onx = to_onnx(dec, X.astype(numpy.float32)) oinf = OnnxInference(onx) - exp = dec.predict(X) got = oinf.run({'X': X}) + self.assertIsInstance(got['variable'], numpy.ndarray) self.assertEqualArray(exp, got['variable']) - X2 = custom_linear_regressor_converter3(X, op=dec) + X2 = custom_linear_regressor_converter3(X, op_=dec) self.assertEqualArray(X2, got['variable']) @ignore_warnings((DeprecationWarning, RuntimeWarning)) @@ -132,12 +139,14 @@ def test_function_regressor3_float64(self): X.shape[0]).astype(numpy.float64)) dec = CustomLinearRegressor3() dec.fit(X, y) + exp = dec.predict(X) + self.assertIsInstance(exp, numpy.ndarray) + onx = to_onnx(dec, X.astype(numpy.float64)) oinf = OnnxInference(onx) - exp = dec.predict(X) got = oinf.run({'X': X}) self.assertEqualArray(exp, got['variable']) - X2 = custom_linear_regressor_converter3(X, op=dec) + X2 = custom_linear_regressor_converter3(X, op_=dec) self.assertEqualArray(X2, got['variable']) @ignore_warnings((DeprecationWarning, RuntimeWarning)) @@ -148,9 +157,11 @@ def test_function_regressor_onnx(self): dec = CustomLinearRegressorOnnx() dec.fit(X, y) exp1 = dec.predict(X) # pylint: disable=E1101 + self.assertIsInstance(exp1, numpy.ndarray) onx = to_onnx(dec, X.astype(numpy.float64)) oinf = OnnxInference(onx) exp2 = dec.predict(X) # pylint: disable=E1101 + self.assertIsInstance(exp2, numpy.ndarray) got = oinf.run({'X': X}) self.assertEqualArray(exp1, got['variable']) self.assertEqualArray(exp2, got['variable']) diff --git a/_unittests/ut_npy/test_custom_transformer.py b/_unittests/ut_npy/test_custom_transformer.py index 154faf963..7fe715ef9 100644 --- a/_unittests/ut_npy/test_custom_transformer.py +++ b/_unittests/ut_npy/test_custom_transformer.py @@ -19,6 +19,7 @@ from mlprodict.onnx_conv import to_onnx from mlprodict.onnxrt import OnnxInference from mlprodict.npy import onnxsklearn_transformer, onnxsklearn_class +import mlprodict.npy.numpy_onnx_impl as nxnp class DecorrelateTransformer(TransformerMixin, BaseEstimator): @@ -76,12 +77,12 @@ class DecorrelateTransformer3(DecorrelateTransformer): @onnxsklearn_transformer(register_class=DecorrelateTransformer3) -def decorrelate_transformer_converter3(X, op=None): +def decorrelate_transformer_converter3(X, op_=None): if X.dtype is None: raise AssertionError("X.dtype cannot be None.") - mean = op.pca_.mean_.astype(X.dtype) - cmp = op.pca_.components_.T.astype(X.dtype) - return (X - mean) @ cmp + mean = op_.pca_.mean_.astype(X.dtype) + cmp = op_.pca_.components_.T.astype(X.dtype) + return nxnp.identity((X - mean) @ cmp) @onnxsklearn_class("onnx_transform") @@ -101,7 +102,7 @@ def onnx_transform(self, X): raise AssertionError("X.dtype cannot be None.") mean = self.pca_.mean_.astype(X.dtype) cmp = self.pca_.components_.T.astype(X.dtype) - return (X - mean) @ cmp + return nxnp.identity((X - mean) @ cmp) class TestCustomTransformer(ExtTestCase): @@ -152,7 +153,7 @@ def test_function_transformer3_float32(self): exp = dec.transform(X) got = oinf.run({'X': X}) self.assertEqualArray(exp, got['variable']) - X2 = decorrelate_transformer_converter3(X, op=dec) + X2 = decorrelate_transformer_converter3(X, op_=dec) self.assertEqualArray(X2, got['variable']) @ignore_warnings((DeprecationWarning, RuntimeWarning)) @@ -165,7 +166,7 @@ def test_function_transformer3_float64(self): exp = dec.transform(X) got = oinf.run({'X': X}) self.assertEqualArray(exp, got['variable']) - X2 = decorrelate_transformer_converter3(X, op=dec) + X2 = decorrelate_transformer_converter3(X, op_=dec) self.assertEqualArray(X2, got['variable']) @ignore_warnings((DeprecationWarning, RuntimeWarning)) diff --git a/_unittests/ut_npy/test_numpy_onnx_pyrt.py b/_unittests/ut_npy/test_numpy_onnx_pyrt.py index a8484c2f2..3e86a80ea 100644 --- a/_unittests/ut_npy/test_numpy_onnx_pyrt.py +++ b/_unittests/ut_npy/test_numpy_onnx_pyrt.py @@ -8,8 +8,8 @@ from pyquickhelper.pycode import ExtTestCase from pyquickhelper.texthelper import compare_module_version from mlprodict.onnxrt import OnnxInference -import mlprodict.npy.numpy_onnx_pyrt as nxnpy from mlprodict.onnxrt.ops_cpu.op_pad import onnx_pad +import mlprodict.npy.numpy_onnx_pyrt as nxnpy from onnxruntime import __version__ as ort_version @@ -232,6 +232,11 @@ def test_exp_float32(self): x = numpy.array([[0.5, 0.1], [-0.5, -0.1]], dtype=numpy.float32) self.common_test1(x, numpy.exp, nxnpy.exp, numpy.float32) + def test_expit_float32(self): + x = numpy.array([[0.5, 0.1], [-0.5, -0.1]], dtype=numpy.float32) + self.common_test1(x, sp.expit, nxnpy.expit, # pylint: disable=E1101 + numpy.float32) + def test_expand_dims_float32(self): x = numpy.array([[0.5, 0.1], [-0.5, -0.1]], dtype=numpy.float32) self.common_test1( @@ -313,6 +318,11 @@ def test_round_float64(self): x = numpy.array([[6.1, 5], [3.5, -7.8]], dtype=numpy.float64) self.common_test1(x, numpy.round, nxnpy.round, numpy.float64) + def test_sigmoid_float32(self): + x = numpy.array([[0.5, 0.1], [-0.5, -0.1]], dtype=numpy.float32) + self.common_test1(x, sp.expit, nxnpy.sigmoid, # pylint: disable=E1101 + numpy.float32) + def test_sign_float64(self): x = numpy.array([[-6.1, 5], [3.5, 7.8]], dtype=numpy.float64) self.common_test1(x, numpy.sign, nxnpy.sign, numpy.float64) diff --git a/_unittests/ut_npy/test_onnx_variable_tuple.py b/_unittests/ut_npy/test_onnx_variable_tuple.py new file mode 100644 index 000000000..48addd5a1 --- /dev/null +++ b/_unittests/ut_npy/test_onnx_variable_tuple.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +""" +@brief test log(time=3s) +""" +import unittest +from typing import Any +import numpy +from pyquickhelper.pycode import ExtTestCase, ignore_warnings +from mlprodict.npy import onnxnumpy, onnxnumpy_default +import mlprodict.npy.numpy_onnx_impl as nxnp +from mlprodict.npy import NDArray + + +@ignore_warnings(DeprecationWarning) +def get_bool(unused): + try: + return numpy.bool + except AttributeError: + return bool + + +numpy_bool = get_bool(None) + + +def common_test_abs_topk(x): + "common onnx topk" + temp = nxnp.abs(x) + return nxnp.topk(temp, numpy.array([1], dtype=numpy.int64)) + + +@onnxnumpy_default +def test_abs_topk(x: NDArray[Any, numpy.float32], + ) -> (NDArray[Any, numpy.float32], + NDArray[Any, numpy.int64]): + "onnx topk" + return common_test_abs_topk(x) + + +@onnxnumpy(runtime='onnxruntime1') +def test_abs_topk_ort(x: NDArray[Any, numpy.float32], + ) -> (NDArray[Any, numpy.float32], + NDArray[Any, numpy.int64]): + "onnx topk" + return common_test_abs_topk(x) + + +class TestOnnxVariableTuple(ExtTestCase): + + @ignore_warnings(DeprecationWarning) + def test_py_abs_topk(self): + x = numpy.array([6.1, -5, 3.5, -7.8, 6.7, -5.0], + dtype=numpy.float32).reshape((-1, 2)) + y, yi = test_abs_topk(x) # pylint: disable=E0633 + self.assertIn('output: "y"', str(test_abs_topk.compiled.onnx_)) + exp_y = numpy.array([[6.1, 7.8, 6.7]], dtype=numpy.float32).T + exp_yi = numpy.array([[0, 1, 0]], dtype=numpy.float32).T + self.assertEqualArray(exp_y, y) + self.assertEqualArray(exp_yi, yi) + + @ignore_warnings(DeprecationWarning) + def test_py_abs_topk_ort(self): + x = numpy.array([6.1, -5, 3.5, -7.8, 6.7, -5.0], + dtype=numpy.float32).reshape((-1, 2)) + y, yi = test_abs_topk_ort(x) # pylint: disable=E0633 + exp_y = numpy.array([[6.1, 7.8, 6.7]], dtype=numpy.float32).T + exp_yi = numpy.array([[0, 1, 0]], dtype=numpy.float32).T + self.assertEqualArray(exp_y, y) + self.assertEqualArray(exp_yi, yi) + + +if __name__ == "__main__": + unittest.main() diff --git a/mlprodict/npy/__init__.py b/mlprodict/npy/__init__.py index 809d04dd0..fc6e47194 100644 --- a/mlprodict/npy/__init__.py +++ b/mlprodict/npy/__init__.py @@ -12,4 +12,5 @@ from .onnx_numpy_wrapper import onnxnumpy, onnxnumpy_default, onnxnumpy_np from .onnx_sklearn_wrapper import ( update_registered_converter_npy, onnxsklearn_class, - onnxsklearn_transformer, onnxsklearn_regressor) + onnxsklearn_transformer, onnxsklearn_regressor, + onnxsklearn_classifier) diff --git a/mlprodict/npy/numpy_onnx_impl.py b/mlprodict/npy/numpy_onnx_impl.py index 715065a1c..2c71f12ab 100644 --- a/mlprodict/npy/numpy_onnx_impl.py +++ b/mlprodict/npy/numpy_onnx_impl.py @@ -26,7 +26,7 @@ OnnxErf, OnnxExp, OnnxFloor, - OnnxIsNaN, + OnnxIdentity, OnnxIsNaN, OnnxLog, OnnxMatMul, OnnxPad, @@ -38,14 +38,15 @@ OnnxReduceSum, OnnxRelu, OnnxRound, + OnnxSigmoid, OnnxSign, OnnxSin, OnnxSinh, OnnxSqrt, OnnxSqueeze, - OnnxTan, OnnxTanh, + OnnxTan, OnnxTanh, OnnxTopK, OnnxUnsqueeze, ) -from .onnx_variable import OnnxVar +from .onnx_variable import OnnxVar, MultiOnnxVar as xtuple def abs(x): @@ -209,6 +210,11 @@ def expand_dims(x, axis): op=OnnxUnsqueeze) +def expit(x): + "See :epkg:`scipy:special:expit`." + return OnnxVar(x, op=OnnxSigmoid) + + def floor(x): "See :epkg:`numpy:floor`." return OnnxVar(x, op=OnnxFloor) @@ -224,6 +230,11 @@ def isnan(x): return OnnxVar(x, op=OnnxIsNaN) +def identity(x): + "Identity." + return OnnxVar(x, op=OnnxIdentity) + + def log(x): "See :epkg:`numpy:log`." return OnnxVar(x, op=OnnxLog) @@ -272,6 +283,11 @@ def round(x): return OnnxVar(x, op=OnnxRound) +def sigmoid(x): + "See :epkg:`scipy:special:expit`." + return OnnxVar(x, op=OnnxSigmoid) + + def sign(x): "See :epkg:`numpy:sign`." return OnnxVar(x, op=OnnxSign) @@ -322,6 +338,12 @@ def tanh(x): return OnnxVar(x, op=OnnxTanh) +def topk(x, k, axis=-1, largest=1, sorted=1): + "See :epkg:`numpy:argsort`." + return xtuple(x, k, op=OnnxTopK, axis=axis, largest=largest, + sorted=sorted) + + def unsqueeze(x, axes): "See :epkg:`numpy:expand_dims`." return OnnxVar(x, axes, op=OnnxUnsqueeze) diff --git a/mlprodict/npy/numpy_onnx_pyrt.py b/mlprodict/npy/numpy_onnx_pyrt.py index 8e50e0d49..2f069698c 100644 --- a/mlprodict/npy/numpy_onnx_pyrt.py +++ b/mlprodict/npy/numpy_onnx_pyrt.py @@ -36,6 +36,7 @@ einsum as nx_einsum, erf as nx_erf, exp as nx_exp, + expit as nx_expit, expand_dims as nx_expand_dims, floor as nx_floor, hstack as nx_hstack, @@ -47,6 +48,7 @@ reciprocal as nx_reciprocal, relu as nx_relu, round as nx_round, + sigmoid as nx_sigmoid, sign as nx_sign, sin as nx_sin, sinh as nx_sinh, @@ -55,6 +57,7 @@ sum as nx_sum, tan as nx_tan, tanh as nx_tanh, + topk as nx_topk, unsqueeze as nx_unsqueeze, vstack as nx_vstack, ) @@ -206,6 +209,12 @@ def exp(x): return nx_exp(x) +@onnxnumpy_np(signature=NDArraySameTypeSameShape("floats")) +def expit(x): + "expit" + return nx_expit(x) + + @onnxnumpy_np(signature=NDArrayType("floats")) def expand_dims(x, axis=0): "expand_dims" @@ -272,6 +281,12 @@ def round(x): return nx_round(x) +@onnxnumpy_np(signature=NDArraySameTypeSameShape("floats")) +def sigmoid(x): + "expit" + return nx_sigmoid(x) + + @onnxnumpy_np(signature=NDArraySameTypeSameShape("floats")) def sign(x): "sign" @@ -320,6 +335,12 @@ def tanh(x): return nx_tanh(x) +@onnxnumpy_np(signature=NDArrayType(("T:all", "ints"), ("T", (numpy.int64,)))) +def topk(x, k, axis=-1, largest=1, sorted=1): + "topk" + return nx_topk(x, k, axis=axis, largest=largest, sorted=sorted) + + @onnxnumpy_np(signature=NDArrayType(("all", numpy.int64))) def unsqueeze(x, axes): "unsqueeze" diff --git a/mlprodict/npy/onnx_numpy_annotation.py b/mlprodict/npy/onnx_numpy_annotation.py index 9dabffcba..9b915b6ea 100644 --- a/mlprodict/npy/onnx_numpy_annotation.py +++ b/mlprodict/npy/onnx_numpy_annotation.py @@ -37,6 +37,8 @@ def get_args_kwargs(fct, n_optional): optional arguments and not parameters, this parameter skips the first *n_optional* paramerters :return: arguments, OrderedDict + + Any optional argument ending with '_' is ignored. """ params = inspect.signature(fct).parameters if n_optional == 0: @@ -59,6 +61,9 @@ def get_args_kwargs(fct, n_optional): kwargs = OrderedDict((name, p.default) for name, p in items if (p.default != inspect.Parameter.empty and name != 'op_version')) + if args[0] == 'self': + args = args[1:] + kwargs['op_'] = None return args, kwargs @@ -91,6 +96,10 @@ class _NDArrayAlias: :param nvars: True if the function allows an infinite number of inputs, this is incompatible with parameter *n_optional*. + *dtypes*, *dtypes_out* by default are a tuple of tuple: + * first dimension: type of every input + * second dimension: list of types for one input + .. versionadded:: 0.6 """ @@ -141,7 +150,8 @@ def __init__(self, dtypes=None, dtypes_out=None, n_optional=None, if (len(self.dtypes_out) == 0 or not isinstance(self.dtypes_out[0], tuple)): raise TypeError( # pragma: no cover - "Type mismatch in self.dtypes_out: {}.".format(self.dtypes_out)) + "Type mismatch in self.dtypes_out={}, " + "self.dtypes={}.".format(self.dtypes_out, self.dtypes)) if (len(self.dtypes_out[0]) == 0 or isinstance(self.dtypes_out[0][0], tuple)): raise TypeError( # pragma: no cover @@ -249,6 +259,9 @@ def get_inputs_outputs(self, args, kwargs, version): :return: *tuple(inputs, outputs, n_input_range)*, each of them is a list of tuple with the name and the dtype """ + if args == ['args', 'kwargs']: + raise RuntimeError( + "Issue with signature %r." % args) for k, v in kwargs.items(): if isinstance(v, type): raise RuntimeError( # pragma: no cover @@ -300,7 +313,7 @@ def _possible_names(): names_in = set(inp[0] for inp in inputs) for _ in key_out: for name in _possible_names(): - if name not in names_in: + if name not in names_in and name not in names_out: name_out = name break names_out.append(name_out) @@ -311,9 +324,10 @@ def _possible_names(): if optional < 0: raise RuntimeError( "optional cannot be negative %r (self.n_optional=%r, " - "len(self.dtypes)=%r, len(inputs)=%r)." % ( + "len(self.dtypes)=%r, len(inputs)=%r) " + "names_in=%r, names_out=%r." % ( optional, self.n_optional, len(self.dtypes), - len(inputs))) + len(inputs), names_in, names_out)) return inputs, kwargs, outputs, optional def shape_calculator(self, dims): diff --git a/mlprodict/npy/onnx_numpy_compiler.py b/mlprodict/npy/onnx_numpy_compiler.py index 97e36f1a1..8d14d5509 100644 --- a/mlprodict/npy/onnx_numpy_compiler.py +++ b/mlprodict/npy/onnx_numpy_compiler.py @@ -8,6 +8,7 @@ from typing import Any import numpy from skl2onnx.common.data_types import guess_numpy_type +from skl2onnx import __max_supported_opset__ from ..onnxrt import OnnxInference from .onnx_numpy_annotation import get_args_kwargs from .onnx_variable import OnnxVar @@ -101,14 +102,16 @@ class OnnxNumpyCompiler: :param version: the same function can be instantiated with different type, this parameter is None or a numpy type if the signature allows multiple types + :param fctsig: function used to overwrite the fct signature + in case this one is using `*args, **kwargs` .. versionadded:: 0.6 """ def __init__(self, fct, op_version=None, runtime=None, signature=None, - version=None): + version=None, fctsig=None): + self.fctsig = fctsig if op_version is None: - from skl2onnx import __max_supported_opset__ op_version = __max_supported_opset__ if hasattr(fct, 'SerializeToString'): self.fct_ = None @@ -129,7 +132,7 @@ def __init__(self, fct, op_version=None, runtime=None, signature=None, inputs, outputs, kwargs, n_input_range = self._parse_annotation( signature=signature, version=version) n_opt = 0 if signature is None else signature.n_optional - args, kwargs2 = get_args_kwargs(self.fct_, n_opt) + args, kwargs2 = get_args_kwargs(self.fctsig or self.fct_, n_opt) self.meta_ = dict(op_version=op_version, runtime=runtime, signature=signature, version=version, inputs=inputs, outputs=outputs, @@ -197,7 +200,7 @@ def _parse_annotation(self, signature, version): if hasattr(self, 'meta_'): args, kwargs = self.meta_['args'], self.meta_['kwargs2'] else: - args, kwargs = get_args_kwargs(self.fct_, n_opt) + args, kwargs = get_args_kwargs(self.fctsig or self.fct_, n_opt) if isinstance(version, tuple): nv = len(version) - len(args) - n_opt if (signature is not None and not @@ -252,11 +255,30 @@ def _possible_names(): shape = self._to_onnx_shape(shape) dtype = self._to_onnx_dtype(dtype, shape) inputs.append((a, dtype)) + ret = annotations['return'] + names_in = set(inp[0] for inp in inputs) + + if isinstance(ret, tuple): + # multiple outputs + names_none = set() + for shape_dtype in ret: + shape, dtype = shape_dtype.__args__ + shape = self._to_onnx_shape(shape) + dtype = self._to_onnx_dtype(dtype, shape) + name_out = None + for name in _possible_names(): + if name not in names_in and name not in names_none: + name_out = name + break + outputs.append((name_out, dtype)) + names_none.add(name_out) + return inputs, outputs, kwargs, 0 + + # single outputs shape, dtype = ret.__args__ shape = self._to_onnx_shape(shape) dtype = self._to_onnx_dtype(dtype, shape) - names_in = set(inp[0] for inp in inputs) name_out = None for name in _possible_names(): if name not in names_in: @@ -284,6 +306,7 @@ def _to_onnx(self, op_version=None, signature=None, version=None): names_out = [oi[0] for oi in outputs] names_var = [OnnxVar(n, dtype=guess_numpy_type(dt[1])) for n, dt in zip(names_in, inputs)] + if 'op_version' in self.fct_.__code__.co_varnames: onx_algebra = self.fct_( *names_in, op_version=op_version, **kwargs) @@ -294,6 +317,7 @@ def _to_onnx(self, op_version=None, signature=None, version=None): "The function %r to convert must return an instance of " "OnnxVar but returns type %r." % (self.fct_, type(onx_var))) onx_algebra = onx_var.to_algebra(op_version=op_version) + if isinstance(onx_algebra, str): raise RuntimeError( # pragma: no cover "Unexpected str type %r." % onx_algebra) @@ -310,7 +334,8 @@ def _to_onnx(self, op_version=None, signature=None, version=None): if self.onnx_ is None: raise RuntimeError( # pragma: no cover - "Unable to get the ONNX graph.") + "Unable to get the ONNX graph (class %r, fct_=%r)" % ( + type(self), self.fct_)) return self.onnx_ def _build_runtime(self, op_version=None, runtime=None, diff --git a/mlprodict/npy/onnx_numpy_wrapper.py b/mlprodict/npy/onnx_numpy_wrapper.py index 87e70161b..09b86eb64 100644 --- a/mlprodict/npy/onnx_numpy_wrapper.py +++ b/mlprodict/npy/onnx_numpy_wrapper.py @@ -119,6 +119,7 @@ class wrapper_onnxnumpy_np: def __init__(self, **kwargs): self.fct = kwargs['fct'] self.signature = kwargs['signature'] + self.fctsig = kwargs.get('fctsig', None) self.args, self.kwargs = get_args_kwargs( self.fct, 0 if self.signature is None else self.signature.n_optional) @@ -188,7 +189,7 @@ def _populate(self, version): compiled = OnnxNumpyCompiler( fct=self.data["fct"], op_version=self.data["op_version"], runtime=self.data["runtime"], signature=self.data["signature"], - version=version) + version=version, fctsig=self.data.get('fctsig', None)) name = "onnxnumpy_np_%s_%s_%s_%s" % ( self.data["fct"].__name__, str(self.data["op_version"]), self.data["runtime"], str(version).split('.')[-1]) diff --git a/mlprodict/npy/onnx_sklearn_wrapper.py b/mlprodict/npy/onnx_sklearn_wrapper.py index 740039768..f7212da47 100644 --- a/mlprodict/npy/onnx_sklearn_wrapper.py +++ b/mlprodict/npy/onnx_sklearn_wrapper.py @@ -5,12 +5,14 @@ .. versionadded:: 0.6 """ -from sklearn.base import TransformerMixin, RegressorMixin +import numpy +from sklearn.base import TransformerMixin, RegressorMixin, ClassifierMixin from skl2onnx import update_registered_converter +from skl2onnx.common.data_types import Int64TensorType from skl2onnx.algebra.onnx_ops import OnnxIdentity # pylint: disable=E0611 -from .onnx_variable import OnnxVar +from .onnx_variable import OnnxVar, TupleOnnxAny from .onnx_numpy_wrapper import _created_classes_inst, wrapper_onnxnumpy_np -from .onnx_numpy_annotation import NDArraySameType +from .onnx_numpy_annotation import NDArraySameType, NDArrayType def _shape_calculator_transformer(operator): @@ -29,7 +31,8 @@ def _shape_calculator_transformer(operator): "This function only supports one input not %r." % len(X)) if len(operator.outputs) != 1: raise RuntimeError( - "This function only supports one output not %r." % len(operator.outputs)) + "This function only supports one output not %r." % len( + operator.outputs)) cl = X[0].type.__class__ dim = [X[0].type.shape[0], None] operator.outputs[0].type = cl(dim) @@ -51,13 +54,39 @@ def _shape_calculator_regressor(operator): "This function only supports one input not %r." % len(X)) if len(operator.outputs) != 1: raise RuntimeError( - "This function only supports one output not %r." % len(operator.outputs)) + "This function only supports one output not %r." % len( + operator.outputs)) op = operator.raw_operator cl = X[0].type.__class__ dim = [X[0].type.shape[0], getattr(op, 'n_outputs_', None)] operator.outputs[0].type = cl(dim) +def _shape_calculator_classifier(operator): + """ + Default shape calculator for a classifier with one input + and two outputs, label (int64) and probabilites of the same type. + + .. versionadded:: 0.6 + """ + if not hasattr(operator, 'onnx_numpy_fct_'): + raise AttributeError( + "operator must have attribute 'onnx_numpy_fct_'.") + X = operator.inputs + if len(X) != 1: + raise RuntimeError( + "This function only supports one input not %r." % len(X)) + if len(operator.outputs) != 2: + raise RuntimeError( + "This function only supports two outputs not %r." % len( + operator.outputs)) + op = operator.raw_operator + cl = X[0].type.__class__ + dim = [X[0].type.shape[0], getattr(op, 'n_outputs_', None)] + operator.outputs[0].type = Int64TensorType(dim[:1]) + operator.outputs[1].type = cl(dim) + + def _converter_transformer(scope, operator, container): """ Default converter for a transformer with one input @@ -77,14 +106,15 @@ def _converter_transformer(scope, operator, container): "This function only supports one input not %r." % len(X)) if len(operator.outputs) != 1: raise RuntimeError( - "This function only supports one output not %r." % len(operator.outputs)) + "This function only supports one output not %r." % len( + operator.outputs)) xvar = OnnxVar(X[0]) fct_cl = operator.onnx_numpy_fct_ opv = container.target_opset try: - inst = fct_cl.fct(xvar, op=operator.raw_operator) + inst = fct_cl.fct(xvar, op_=operator.raw_operator) except TypeError as e: raise TypeError( "Unable to call function %r from %r for operator %r." @@ -114,19 +144,68 @@ def _converter_regressor(scope, operator, container): "This function only supports one input not %r." % len(X)) if len(operator.outputs) != 1: raise RuntimeError( - "This function only supports one output not %r." % len(operator.outputs)) + "This function only supports one output not %r." % len( + operator.outputs)) xvar = OnnxVar(X[0]) fct_cl = operator.onnx_numpy_fct_ opv = container.target_opset - inst = fct_cl.fct(xvar, op=operator.raw_operator) + inst = fct_cl.fct(xvar, op_=operator.raw_operator) onx = inst.to_algebra(op_version=opv) final = OnnxIdentity(onx, op_version=opv, output_names=[operator.outputs[0].full_name]) final.add_to(scope, container) +def _converter_classifier(scope, operator, container): + """ + Default converter for a classifier with one input + and two outputs, label and probabilities of the same input type. + It assumes instance *operator* + has an attribute *onnx_numpy_fct_* from a function + wrapped with decorator :func:`onnxsklearn_classifier + `. + + .. versionadded:: 0.6 + """ + if not hasattr(operator, 'onnx_numpy_fct_'): + raise AttributeError( + "operator must have attribute 'onnx_numpy_fct_'.") + X = operator.inputs + if len(X) != 1: + raise RuntimeError( + "This function only supports one input not %r." % len(X)) + if len(operator.outputs) != 2: + raise RuntimeError( + "This function only supports two outputs not %r." % len( + operator.outputs)) + + xvar = OnnxVar(X[0]) + fct_cl = operator.onnx_numpy_fct_ + + opv = container.target_opset + inst = fct_cl.fct(xvar, op_=operator.raw_operator) + onx = inst.to_algebra(op_version=opv) + if isinstance(onx, TupleOnnxAny): + if len(operator.outputs) != len(onx): + raise RuntimeError( + "Mismatched number of outputs expected %d, got %d." % ( + len(operator.outputs), len(onx))) + for out, ox in zip(operator.outputs, onx): + if not hasattr(ox, 'add_to'): + raise TypeError( + "Unexpected type for onnx graph %r, inst=%r." % ( + type(ox), type(inst))) + final = OnnxIdentity(ox, op_version=opv, + output_names=[out.full_name]) + final.add_to(scope, container) + else: + final = OnnxIdentity(onx, op_version=opv, + output_names=[operator.outputs[0].full_name]) + final.add_to(scope, container) + + def update_registered_converter_npy( model, alias, convert_fct, shape_fct=None, overwrite=True, parser=None, options=None): @@ -168,13 +247,16 @@ def addattr(operator, obj): default_cvt = { TransformerMixin: (_shape_calculator_transformer, _converter_transformer), - RegressorMixin: (_shape_calculator_regressor, _converter_regressor) + RegressorMixin: (_shape_calculator_regressor, _converter_regressor), + ClassifierMixin: (_shape_calculator_classifier, _converter_classifier), } if issubclass(model, TransformerMixin): defcl = TransformerMixin elif issubclass(model, RegressorMixin): defcl = RegressorMixin + elif issubclass(model, ClassifierMixin): + defcl = ClassifierMixin else: defcl = None @@ -199,9 +281,6 @@ def addattr(operator, obj): def _internal_decorator(fct, op_version=None, runtime=None, signature=None, register_class=None): - if signature is None: - signature = NDArraySameType("all") - name = "onnxsklearn_parser_%s_%s_%s" % ( fct.__name__, str(op_version), runtime) newclass = type( @@ -238,6 +317,9 @@ def onnxsklearn_transformer(op_version=None, runtime=None, signature=None, .. versionadded:: 0.6 """ + if signature is None: + signature = NDArraySameType("all") + def decorator_fct(fct): return _internal_decorator(fct, signature=signature, op_version=op_version, @@ -262,6 +344,36 @@ def onnxsklearn_regressor(op_version=None, runtime=None, signature=None, .. versionadded:: 0.6 """ + if signature is None: + signature = NDArraySameType("all") + + def decorator_fct(fct): + return _internal_decorator(fct, signature=signature, + op_version=op_version, + runtime=runtime, + register_class=register_class) + return decorator_fct + + +def onnxsklearn_classifier(op_version=None, runtime=None, signature=None, + register_class=None): + """ + Decorator to declare a converter for a classifier implemented using + :epkg:`numpy` syntax but executed with :epkg:`ONNX` + operators. + + :param op_version: :epkg:`ONNX` opset version + :param runtime: `'onnxruntime'` or one implemented by @see cl OnnxInference + :param signature: if None, the signature is replaced by a standard signature + for transformer ``NDArraySameType("all")`` + :param register_class: automatically register this converter + for this class to :epkg:`sklearn-onnx` + + .. versionadded:: 0.6 + """ + if signature is None: + signature = NDArrayType(("T:all", ), dtypes_out=((numpy.int64, ), 'T')) + def decorator_fct(fct): return _internal_decorator(fct, signature=signature, op_version=op_version, @@ -286,6 +398,12 @@ def _internal_method_decorator(register_class, method, op_version=None, signature = NDArraySameType("all") if method_names is None: method_names = ("predict", ) + elif issubclass(register_class, ClassifierMixin): + if signature is None: + signature = NDArrayType( + ("T:all", ), dtypes_out=((numpy.int64, ), 'T')) + if method_names is None: + method_names = ("predict", "predict_proba") if method_names is None: raise RuntimeError( @@ -305,9 +423,18 @@ def _internal_method_decorator(register_class, method, op_version=None, '__getstate__': wrapper_onnxnumpy_np.__getstate__, '__setstate__': wrapper_onnxnumpy_np.__setstate__}) _created_classes_inst.append(name, newclass) + + def _check_(op): + if isinstance(op, str): + raise TypeError( + "Unexpected type: %r: %r." % (type(op), op)) + return op + res = newclass( - fct=lambda *args, op=None, **kwargs: method(op, *args, **kwargs), - op_version=op_version, runtime=runtime, signature=signature) + fct=lambda *args, op_=None, **kwargs: method( + _check_(op_), *args, **kwargs), + op_version=op_version, runtime=runtime, signature=signature, + fctsig=method) if len(method_names) == 1: name = method_names[0] @@ -315,11 +442,21 @@ def _internal_method_decorator(register_class, method, op_version=None, raise RuntimeError( "Cannot overwrite method %r because it already exists in " "class %r." % (name, register_class)) - m = lambda self, X: method(self, X) + m = lambda self, X: res(X, op_=self) setattr(register_class, name, m) + elif len(method_names) == 0: + raise RuntimeError("No available method.") else: - raise NotImplementedError( - "Several methods are updated for classifier and clusterers.") + m = lambda self, X: res(X, op_=self) + setattr(register_class, method.__name__ + "_", m) + for iname, name in enumerate(method_names): + if hasattr(register_class, name): + raise RuntimeError( + "Cannot overwrite method %r because it already exists in " + "class %r." % (name, register_class)) + m = lambda self, X, index_output=iname: res(X, op_=self)[ + index_output] + setattr(register_class, name, m) update_registered_converter_npy( register_class, "Sklearn%s" % getattr( diff --git a/mlprodict/npy/onnx_variable.py b/mlprodict/npy/onnx_variable.py index c2e1ea26b..b3ac46539 100644 --- a/mlprodict/npy/onnx_variable.py +++ b/mlprodict/npy/onnx_variable.py @@ -438,3 +438,136 @@ def flatten(self, axis=0): return OnnxVar(fl, numpy.array([0], dtype=numpy.int64), op=OnnxSqueeze) return fl + + +class TupleOnnxAny: + """ + Class used to return multiple @see cl OnnxVar + at the same time. + """ + + def __init__(self, first, *args): + if isinstance(first, (list, tuple)): + raise TypeError( + "Unexpected type for first %r." % type(first)) + if len(args) > 0: + self.values = (first,) + args + self.unique = None + else: + self.values = None + self.unique = first + + def __len__(self): + "usual" + if self.values is None: + raise NotImplementedError( + "Not yet implemented in this case unique=%r, " + "values=%r." % (self.unique, self.values)) + return len(self.values) + + def __iter__(self): + "Iterates on the outputs." + if self.values is None: + raise NotImplementedError( + "Not yet implemented in this case.") + for v in self.values: + yield v + + def __getitem__(self, i): + "usual" + if self.values is None: + return self.unique[i] + return self.values[i] + + @property + def output_names(self): + "Returns 'output_names' of attribute 'unique'." + if self.values is None: + if hasattr(self.unique, 'to_onnx'): + return self.unique.output_names + raise NotImplementedError( + "Not implemented yet unique=%r values=%r." % ( + self.unique, self.values)) + + @output_names.setter + def output_names(self, value): + "Updates 'output_names' of attribute 'unique'." + if self.values is None: + if hasattr(self.unique, 'to_onnx'): + self.unique.output_names = value + return + if self.values is not None and len(self.values) == len(value): + for name, v in zip(value, self.values): + v.output_names = [name] + return + raise NotImplementedError( + "Not implemented yet, value=%r, unique=%r values=%r." % ( + value, self.unique, self.values)) + + def to_onnx(self, *args, **kwargs): + "Converts the underlying class into an ONNX graph." + if self.values is None: + if hasattr(self.unique, 'to_onnx'): + return self.unique.to_onnx(*args, **kwargs) + if self.values is not None: + if len(self.values) == len(kwargs.get('outputs', [])): + return self.values[0].to_onnx( + *args, other_outputs=self.values[1:], **kwargs) + raise NotImplementedError( + "Not implemented yet unique=%r values=%r args=%r " + "kwargs=%r." % (self.unique, self.values, args, kwargs)) + + +class MultiOnnxVar: + """ + Class used to return multiple @see cl OnnxVar + at the same time. + """ + + def __init__(self, *inputs, op=None, dtype=None, **kwargs): + "constructor" + self.onxvar = OnnxVar(*inputs, op=op, dtype=None, **kwargs) + self.alg_ = None + + @property + def inputs(self): + "Returns `self.onxvar.inputs`." + return self.onxvar.inputs + + @property + def onnx_op(self): + "Returns `self.onxvar.onnx_op`." + return self.onxvar.onnx_op + + @property + def onnx_op_kwargs(self): + "Returns `self.onxvar.onnx_op_kwargs`." + return self.onxvar.onnx_op_kwargs + + def to_algebra(self, op_version=None): + """ + Converts the variable into an operator. + """ + if self.alg_ is None: + new_inputs = [] + for inp in self.inputs: + if isinstance(inp, ( + int, float, str, numpy.ndarray, numpy.int32, + numpy.int64, numpy.float32, numpy.float64, + numpy_bool, numpy_str, numpy.int8, numpy.uint8, + numpy.int16, numpy.uint16, numpy.uint32, numpy.uint64)): + new_inputs.append(inp) + else: + new_inputs.append( + inp.to_algebra(op_version=op_version)) + + if self.onnx_op is None: + if len(new_inputs) == 1: + self.alg_ = TupleOnnxAny(new_inputs[0]) + else: + self.alg_ = TupleOnnxAny(new_inputs[0], *(new_inputs[1:])) + else: + res = self.onnx_op( # pylint: disable=E1102 + *new_inputs, op_version=op_version, **self.onnx_op_kwargs) + self.alg_ = TupleOnnxAny(res) + return self.alg_ diff --git a/mlprodict/onnxrt/ops_cpu/op_pad.py b/mlprodict/onnxrt/ops_cpu/op_pad.py index 8cfbf9267..97a7a29b7 100644 --- a/mlprodict/onnxrt/ops_cpu/op_pad.py +++ b/mlprodict/onnxrt/ops_cpu/op_pad.py @@ -41,7 +41,7 @@ def onnx_pad(data, pads, constant_value=None, mode='constant'): :param constant_value: A scalar value to be used if the mode chosen is `constant` (by default it is 0, empty string or False). :param mode: Supported modes: `constant`(default), `reflect`, `edge` - :return tensor after padding + :return: tensor after padding """ if constant_value is None: constant_value = data.dtype(constant_value) diff --git a/mlprodict/onnxrt/validate/validate_helper.py b/mlprodict/onnxrt/validate/validate_helper.py index ea1bd786d..f233f0b7d 100644 --- a/mlprodict/onnxrt/validate/validate_helper.py +++ b/mlprodict/onnxrt/validate/validate_helper.py @@ -377,7 +377,7 @@ def _multiply_time_kwargs(time_kwargs, time_kwargs_fact, inst): :param time_kwargs: see below :param time_kwargs_fact: see below :param inst: :epkg:`scikit-learn` model - :return : new *time_kwargs* + :return: new *time_kwargs* Possible values for *time_kwargs_fact*: diff --git a/setup.py b/setup.py index 8cf935409..27824fc67 100644 --- a/setup.py +++ b/setup.py @@ -469,16 +469,17 @@ def get_extensions(): install_requires=["pybind11", "numpy>=1.17", "onnx>=1.7", 'scipy>=1.0.0', 'jinja2', 'cython'], extras_require={ - 'onnx_conv': ['scikit-learn>=0.23', 'skl2onnx>=1.7', + 'npy': ['scikit-learn>=0.23', 'skl2onnx>=1.8'], + 'onnx_conv': ['scikit-learn>=0.23', 'skl2onnx>=1.8', 'joblib', 'threadpoolctl', 'mlinsights>=0.3', 'lightgbm', 'xgboost'], - 'sklapi': ['scikit-learn>=0.23', 'joblib', 'threadpoolctl'], - 'onnx_val': ['scikit-learn>=0.23', 'skl2onnx>=1.7', + 'onnx_val': ['scikit-learn>=0.23', 'skl2onnx>=1.8', 'onnxconverter-common>=1.8', 'onnxruntime>=1.6.0', 'joblib', 'threadpoolctl'], - 'all': ['scikit-learn>=0.23', 'skl2onnx>=1.7', + 'sklapi': ['scikit-learn>=0.23', 'joblib', 'threadpoolctl'], + 'all': ['scikit-learn>=0.23', 'skl2onnx>=1.8', 'onnxconverter-common>=1.7', - 'onnxruntime>=1.6.0', 'scipy' 'joblib', 'pandas', + 'onnxruntime>=1.7.0', 'scipy' 'joblib', 'pandas', 'threadpoolctl', 'mlinsights>=0.3', 'lightgbm', 'xgboost'], },