Repository: onnx/sklearn-onnx

Issue:
Possible bug in convert_sklearn_array_feature_extractor(), operator.inputs not reshaped
Hi, 
before the issue report: skl2onnx is just great, so much better than pickling sklearn models.

/Users/XXXX/.conda/envs/cfserver/lib/python3.7/site-packages/sklearn/mixture/gaussian_mixture.py:366: RuntimeWarning: divide by zero encountered in log n_components, -1)[:, ::n_features + 1]), 1)) 
came up when I converted a pipeline. The resulting onnx model does not work properly and returns '0' as prediction results. I think it is caused when converting a ColumnTransformer in a 
pipeline because convert_sklearn_array_feature_extractor() does not reshape the inputs variable size as in other converters. 

I'm by no means an onnx expert but tried to dig through the code and attached an explanation of what I think happens and a code snippet that in essence replicates the issue.

Cheers

[column_transformer_bug.txt](https://github.com/onnx/sklearn-onnx/files/4626917/column_transformer_bug.txt)
[column_transformer_snippet_py.txt](https://github.com/onnx/sklearn-onnx/files/4626918/column_transformer_snippet_py.txt)



Discussion:

Resolving patch:
diff --git a/skl2onnx/_parse.py b/skl2onnx/_parse.py
--- a/skl2onnx/_parse.py
+++ b/skl2onnx/_parse.py
@@ -312,12 +312,10 @@ def _parse_sklearn_column_transformer(scope, model, inputs,
 
         # Declare output name of scikit-learn ColumnTransformer
         transformed_column_name = scope.declare_local_variable(
-                                            'transformed_column', ty)
+            'transformed_column', ty)
         concat_operator.outputs.append(transformed_column_name)
-
         return concat_operator.outputs
-    else:
-        return transformed_result_names
+    return transformed_result_names
 
 
 def _parse_sklearn_grid_search_cv(scope, model, inputs, custom_parsers=None):
diff --git a/skl2onnx/operator_converters/one_hot_encoder.py b/skl2onnx/operator_converters/one_hot_encoder.py
--- a/skl2onnx/operator_converters/one_hot_encoder.py
+++ b/skl2onnx/operator_converters/one_hot_encoder.py
@@ -90,9 +90,28 @@ def convert_sklearn_one_hot_encoder(scope, operator, container):
 
         if isinstance(inp_type, (Int64TensorType, Int32TensorType)):
             attrs['cats_int64s'] = categories.astype(np.int64)
-        else:
+        elif isinstance(inp_type, StringTensorType):
             attrs['cats_strings'] = np.array(
                 [str(s).encode('utf-8') for s in categories])
+        elif isinstance(inp_type, (FloatTensorType, DoubleTensorType)):
+            # The converter checks that categories can be casted into
+            # integers. String is not allowed here.
+            # Input type is casted into int64.
+            for c in categories:
+                try:
+                    ci = int(c)
+                except TypeError:
+                    raise RuntimeError(
+                        "Category '{}' cannot be casted into int.".format(c))
+                if ci != c:
+                    raise RuntimeError(
+                        "Category '{}' is not an int64.".format(c))
+            attrs['cats_int64s'] = categories.astype(np.int64)
+        else:
+            raise RuntimeError(
+                "Input type {} is not supported for OneHotEncoder. "
+                "Ideally, it should either be integer or strings.".format(
+                    inp_type))
 
         ohe_output = scope.get_unique_variable_name(name + 'out')
 
diff --git a/skl2onnx/shape_calculators/array_feature_extractor.py b/skl2onnx/shape_calculators/array_feature_extractor.py
--- a/skl2onnx/shape_calculators/array_feature_extractor.py
+++ b/skl2onnx/shape_calculators/array_feature_extractor.py
@@ -10,6 +10,10 @@
 
 def calculate_sklearn_array_feature_extractor(operator):
     check_input_and_output_numbers(operator, output_count_range=1)
+    i = operator.inputs[0]
+    N = i.type.shape[0]
+    C = len(operator.column_indices)
+    operator.outputs[0].type = i.type.__class__([N, C])
 
 
 register_shape_calculator('SklearnArrayFeatureExtractor',


Test patch:
diff --git a/tests/test_sklearn_array_feature_extractor.py b/tests/test_sklearn_array_feature_extractor.py
new file mode 100644
--- /dev/null
+++ b/tests/test_sklearn_array_feature_extractor.py
@@ -0,0 +1,77 @@
+# -------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+import unittest
+from distutils.version import StrictVersion
+import pandas as pd
+import numpy as np
+from onnxruntime import __version__ as ort_version
+from sklearn.mixture import GaussianMixture
+from sklearn.preprocessing import OneHotEncoder
+try:
+    from sklearn.compose import ColumnTransformer
+except ImportError:
+    # old version of scikit-learn
+    ColumnTransformer = None
+from skl2onnx import to_onnx
+from skl2onnx.common.data_types import FloatTensorType
+from sklearn.pipeline import Pipeline
+from test_utils import (
+    dump_data_and_model,
+    TARGET_OPSET
+)
+
+
+class TestSklearnArrayFeatureExtractor(unittest.TestCase):
+
+    @unittest.skipIf(
+        ColumnTransformer is None or
+        StrictVersion(ort_version) <= StrictVersion("0.4.0"),
+        reason="onnxruntime too old")
+    def test_array_feature_extractor(self):
+        data_to_cluster = pd.DataFrame(
+            [[1, 2, 3.5, 4.5], [1, 2, 1.7, 4.0],
+             [2, 4, 2.4, 4.3], [2, 4, 2.5, 4.0]],
+            columns=[1, 2, 3, 4])
+        cat_attributes_clustering = [1, 2]
+        num_attributes_clustering = [3, 4]  # this is of length 12 in reality
+        gmm = GaussianMixture(n_components=2, random_state=1)
+        ohe_cat = [OneHotEncoder(categories='auto', sparse=False, drop=None)
+                   for i in cat_attributes_clustering]
+        ct_cat = ColumnTransformer([
+            ("oneHotEncoder" + str(i), ohe_cat[i], [i])
+            for i, item in enumerate(cat_attributes_clustering)
+        ], remainder='passthrough')
+        onehotencoding_pipeline = Pipeline([("columnTransformer", ct_cat), ])
+        clustering_pipeline = Pipeline([
+            ('onehotencoder_and_scaler', onehotencoding_pipeline),
+            ('clustering', gmm)])
+        clustering_pipeline.fit(X=data_to_cluster)
+        initial_type = [
+            ('float_input', FloatTensorType(
+                [None, len([*cat_attributes_clustering,
+                            *num_attributes_clustering])]))]
+        data = data_to_cluster.values.astype(np.float32)
+
+        # checks the first step
+        model_onnx = to_onnx(
+            clustering_pipeline.steps[0][1], initial_types=initial_type,
+            target_opset=TARGET_OPSET, dtype=np.float32)
+        dump_data_and_model(
+            data, clustering_pipeline.steps[0][1], model_onnx,
+            basename="SklearnArrayFeatureExtractorStep0")
+
+        # checks the whole pipeline
+        model_onnx = to_onnx(
+            clustering_pipeline, initial_types=initial_type,
+            target_opset=TARGET_OPSET, dtype=np.float32)
+        dump_data_and_model(
+            data, clustering_pipeline, model_onnx,
+            basename="SklearnArrayFeatureExtractor")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_sklearn_one_vs_rest_classifier_converter.py b/tests/test_sklearn_one_vs_rest_classifier_converter.py
--- a/tests/test_sklearn_one_vs_rest_classifier_converter.py
+++ b/tests/test_sklearn_one_vs_rest_classifier_converter.py
@@ -238,7 +238,7 @@ def test_ovr_classification_int_binary(self):
                      reason="Requires ONNX-ML extension.")
     def test_ovr_classification_float_mlp(self):
         model, X = fit_classification_model(
-            OneVsRestClassifier(MLPClassifier()), 5)
+            OneVsRestClassifier(MLPClassifier()), 4)
         model_onnx = convert_sklearn(
             model,
             "ovr classification",
@@ -370,7 +370,7 @@ def test_ovr_regression_int(self):
                      reason="Requires ONNX-ML extension.")
     def test_ovr_regression_float_mlp(self):
         model, X = fit_classification_model(
-            OneVsRestClassifier(MLPRegressor()), 7)
+            OneVsRestClassifier(MLPRegressor()), 5)
         model_onnx = convert_sklearn(
             model,
             "ovr regression",