Repository: onnx/sklearn-onnx

Issue:
Label binariser converter doesn't work on 2-D data
X1 = np.array([[0, 1, 1], [1, 0, 0]])
model = LabelBinarizer().fit(X1)
print(model.transform(X1))

onnx_fs = convert_sklearn(model, 'lb', [('float_input', Int64TensorType(X1.shape))])
onnxmltools.utils.save_model(onnx_fs, 'lb.onnx')
sess = InferenceSession('lb.onnx')
res = sess.run('', input_feed={'float_input': X1.astype(np.int64)})
res

[[0 1 1]
 [1 0 0]]
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-6-110ffb7fb0ed> in <module>
      5 onnx_fs = convert_sklearn(model, 'lb', [('float_input', Int64TensorType(X1.shape))])
      6 onnxmltools.utils.save_model(onnx_fs, 'lb.onnx')
----> 7 sess = InferenceSession('lb.onnx')
      8 res = sess.run('', input_feed={'float_input': X1.astype(np.int64)})
      9 res

~/Documents/MachineLearning/onnx_projects/onnx_env/lib/python3.6/site-packages/onnxruntime/capi/session.py in __init__(self, path_or_bytes, sess_options)
     27 
     28         if isinstance(path_or_bytes, str):
---> 29             self._sess.load_model(path_or_bytes)
     30         elif isinstance(path_or_bytes, bytes):
     31             self._sess.read_bytes(path_or_bytes)

RuntimeError: [ONNXRuntimeError] : 1 : GENERAL ERROR : Load model from lb.onnx failed:Node:Cast Output:variable [ShapeInferenceError] Can't merge shape info. Both source and target dimension have values but they differ. Source=6 Target=2 Dimension=0


Discussion:

Resolving patch:
diff --git a/skl2onnx/_parse.py b/skl2onnx/_parse.py
--- a/skl2onnx/_parse.py
+++ b/skl2onnx/_parse.py
@@ -118,6 +118,7 @@ def _parse_sklearn_simple_model(scope, model, inputs, custom_parsers=None):
                                     'probabilities', scope.tensor_type())
         this_operator.outputs.append(label_variable)
         this_operator.outputs.append(probability_tensor_variable)
+
     elif type(model) in cluster_list or isinstance(model, ClusterMixin):
         # For clustering, we may have two outputs, one for label and
         # the other one for scores of all classes. Notice that their
@@ -129,6 +130,7 @@ def _parse_sklearn_simple_model(scope, model, inputs, custom_parsers=None):
             'scores', scope.tensor_type())
         this_operator.outputs.append(label_variable)
         this_operator.outputs.append(score_tensor_variable)
+
     elif type(model) in outlier_list or isinstance(model, OutlierMixin):
         # For clustering, we may have two outputs, one for label and
         # the other one for scores.
@@ -138,6 +140,7 @@ def _parse_sklearn_simple_model(scope, model, inputs, custom_parsers=None):
             'scores', scope.tensor_type())
         this_operator.outputs.append(label_variable)
         this_operator.outputs.append(score_tensor_variable)
+
     elif type(model) == NearestNeighbors:
         # For Nearest Neighbours, we have two outputs, one for nearest
         # neighbours' indices and the other one for distances
@@ -147,6 +150,7 @@ def _parse_sklearn_simple_model(scope, model, inputs, custom_parsers=None):
                                                          scope.tensor_type())
         this_operator.outputs.append(index_variable)
         this_operator.outputs.append(distance_variable)
+
     elif type(model) in {GaussianMixture, BayesianGaussianMixture}:
         label_variable = scope.declare_local_variable('label',
                                                       Int64TensorType())
diff --git a/skl2onnx/operator_converters/label_binariser.py b/skl2onnx/operator_converters/label_binariser.py
--- a/skl2onnx/operator_converters/label_binariser.py
+++ b/skl2onnx/operator_converters/label_binariser.py
@@ -14,61 +14,81 @@ def convert_sklearn_label_binariser(scope, operator, container):
     """Converts Scikit Label Binariser model to onnx format."""
     binariser_op = operator.raw_operator
     classes = binariser_op.classes_
-    zeros_tensor = np.full((1, len(classes)),
-                           binariser_op.neg_label, dtype=np.float)
-    unit_tensor = np.full((1, len(classes)),
-                          binariser_op.pos_label, dtype=np.float)
-
-    reshaped_input_name = scope.get_unique_variable_name('reshaped_input')
-    classes_tensor_name = scope.get_unique_variable_name('classes_tensor')
-    equal_condition_tensor_name = scope.get_unique_variable_name(
-        'equal_condition_tensor')
-    zeros_tensor_name = scope.get_unique_variable_name('zero_tensor')
-    unit_tensor_name = scope.get_unique_variable_name('unit_tensor')
-    where_result_name = scope.get_unique_variable_name('where_result')
-
-    class_dtype = onnx_proto.TensorProto.STRING
-    if np.issubdtype(binariser_op.classes_.dtype, np.signedinteger):
-        class_dtype = onnx_proto.TensorProto.INT64
+    if (hasattr(binariser_op, 'sparse_input_') and
+            binariser_op.sparse_input_):
+        raise RuntimeError("sparse is not supported for LabelBinarizer.")
+    if (hasattr(binariser_op, 'y_type_') and
+            binariser_op.y_type_ == "multilabel-indicator"):
+        if binariser_op.pos_label != 1:
+            raise RuntimeError("pos_label != 1 is not supported "
+                               "for LabelBinarizer.")
+        if list(classes) != list(range(len(classes))):
+            raise RuntimeError("classes != [0, 1, ..., n_classes] is not "
+                               "supported for LabelBinarizer.")
+        container.add_node('Identity', operator.inputs[0].full_name,
+                           operator.output_full_names,
+                           name=scope.get_unique_operator_name('identity'))
     else:
-        classes = np.array([s.encode('utf-8') for s in classes])
+        zeros_tensor = np.full((1, len(classes)),
+                               binariser_op.neg_label, dtype=np.float)
+        unit_tensor = np.full((1, len(classes)),
+                              binariser_op.pos_label, dtype=np.float)
 
-    container.add_initializer(classes_tensor_name, class_dtype,
-                              [len(classes)], classes)
-    container.add_initializer(zeros_tensor_name, onnx_proto.TensorProto.FLOAT,
-                              zeros_tensor.shape, zeros_tensor.ravel())
-    container.add_initializer(unit_tensor_name, onnx_proto.TensorProto.FLOAT,
-                              unit_tensor.shape, unit_tensor.ravel())
+        classes_tensor_name = scope.get_unique_variable_name('classes_tensor')
+        equal_condition_tensor_name = scope.get_unique_variable_name(
+            'equal_condition_tensor')
+        zeros_tensor_name = scope.get_unique_variable_name('zero_tensor')
+        unit_tensor_name = scope.get_unique_variable_name('unit_tensor')
+        where_result_name = scope.get_unique_variable_name('where_result')
 
-    apply_reshape(scope, operator.inputs[0].full_name, reshaped_input_name,
-                  container, desired_shape=[-1, 1])
-    # Models with classes_/inputs of string type would fail in the
-    # following step as Equal op does not support string comparison.
-    container.add_node('Equal', [classes_tensor_name, reshaped_input_name],
-                       equal_condition_tensor_name,
-                       name=scope.get_unique_operator_name('equal'))
-    container.add_node(
-        'Where',
-        [equal_condition_tensor_name, unit_tensor_name, zeros_tensor_name],
-        where_result_name,
-        name=scope.get_unique_operator_name('where'))
-    where_res = where_result_name
-    if len(binariser_op.classes_) == 2:
-        array_feature_extractor_result_name = scope.get_unique_variable_name(
-            'array_feature_extractor_result')
-        pos_class_index_name = scope.get_unique_variable_name(
-            'pos_class_index')
+        class_dtype = onnx_proto.TensorProto.STRING
+        if np.issubdtype(binariser_op.classes_.dtype, np.signedinteger):
+            class_dtype = onnx_proto.TensorProto.INT64
+        else:
+            classes = np.array([s.encode('utf-8') for s in classes])
 
+        container.add_initializer(classes_tensor_name, class_dtype,
+                                  [len(classes)], classes)
         container.add_initializer(
-            pos_class_index_name, onnx_proto.TensorProto.INT64, [], [1])
+            zeros_tensor_name, onnx_proto.TensorProto.FLOAT,
+            zeros_tensor.shape, zeros_tensor.ravel())
+        container.add_initializer(
+            unit_tensor_name, onnx_proto.TensorProto.FLOAT,
+            unit_tensor.shape, unit_tensor.ravel())
+
+        reshaped_input_name = scope.get_unique_variable_name('reshaped_input')
+        apply_reshape(scope, operator.inputs[0].full_name, reshaped_input_name,
+                      container, desired_shape=[-1, 1])
 
+        # Models with classes_/inputs of string type would fail in the
+        # following step as Equal op does not support string comparison.
+        container.add_node('Equal', [classes_tensor_name, reshaped_input_name],
+                           equal_condition_tensor_name,
+                           name=scope.get_unique_operator_name('equal'))
         container.add_node(
-            'ArrayFeatureExtractor', [where_result_name, pos_class_index_name],
-            array_feature_extractor_result_name, op_domain='ai.onnx.ml',
-            name=scope.get_unique_operator_name('ArrayFeatureExtractor'))
-        where_res = array_feature_extractor_result_name
-    apply_cast(scope, where_res, operator.output_full_names, container,
-               to=onnx_proto.TensorProto.INT64)
+            'Where',
+            [equal_condition_tensor_name, unit_tensor_name, zeros_tensor_name],
+            where_result_name,
+            name=scope.get_unique_operator_name('where'))
+        where_res = where_result_name
+
+        if len(binariser_op.classes_) == 2:
+            array_f_name = scope.get_unique_variable_name(
+                'array_feature_extractor_result')
+            pos_class_index_name = scope.get_unique_variable_name(
+                'pos_class_index')
+
+            container.add_initializer(
+                pos_class_index_name, onnx_proto.TensorProto.INT64, [], [1])
+
+            container.add_node(
+                'ArrayFeatureExtractor',
+                [where_result_name, pos_class_index_name],
+                array_f_name, op_domain='ai.onnx.ml',
+                name=scope.get_unique_operator_name('ArrayFeatureExtractor'))
+            where_res = array_f_name
+        apply_cast(scope, where_res, operator.output_full_names, container,
+                   to=onnx_proto.TensorProto.INT64)
 
 
 register_converter('SklearnLabelBinarizer', convert_sklearn_label_binariser)


Test patch:
diff --git a/tests/test_sklearn_label_binariser_converter.py b/tests/test_sklearn_label_binariser_converter.py
--- a/tests/test_sklearn_label_binariser_converter.py
+++ b/tests/test_sklearn_label_binariser_converter.py
@@ -4,7 +4,9 @@
 
 import unittest
 import numpy as np
+from numpy.testing import assert_almost_equal
 from sklearn.preprocessing import LabelBinarizer
+from onnxruntime import InferenceSession
 from skl2onnx import convert_sklearn
 from skl2onnx.common.data_types import Int64TensorType
 from test_utils import dump_data_and_model
@@ -106,6 +108,19 @@ def test_model_label_binariser_binary_labels(self):
             "<= StrictVersion('0.2.1')",
         )
 
+    def test_model_label_binariser_2d(self):
+        X1 = np.array([[0, 1, 1], [1, 0, 0]], dtype=np.int64)
+        model = LabelBinarizer().fit(X1)
+        onnx_fs = convert_sklearn(
+            model, 'lb',
+            [('float_input', Int64TensorType([None, X1.shape[1]]))])
+        sess = InferenceSession(onnx_fs.SerializeToString())
+
+        res = sess.run(None, input_feed={'float_input': X1})
+        exp = model.transform(X1)
+        got = res[0]
+        assert_almost_equal(exp, got)
+
 
 if __name__ == "__main__":
     unittest.main()