Repository: onnx/sklearn-onnx

Issue:
Logistic Regression probability scores wrong in case of string labels and multi_class set to multinomial.
data = load_digits()
X = data.data
y = data.target

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=42)
model = LogisticRegression(random_state=42, multi_class='multinomial', solver='sag').fit(X_train, y_train)

onnx_model = convert_sklearn(model, 'lr', [('input', FloatTensorType(X_test.shape))])
save_model(onnx_model, 'lr.onnx')
sess = InferenceSession('lr.onnx')
res = sess.run(None, input_feed={'input': X_test.astype(np.float32)})

np.mean(np.isclose(model.predict_proba(X_test), list(map(lambda x:
                                                         list(map(lambda y: x[y], x)), res[1]))))

>> 0.010901001112347052
Logistic Regression probability scores wrong in case of string labels and multi_class set to multinomial.
data = load_digits()
X = data.data
y = data.target

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=42)
model = LogisticRegression(random_state=42, multi_class='multinomial', solver='sag').fit(X_train, y_train)

onnx_model = convert_sklearn(model, 'lr', [('input', FloatTensorType(X_test.shape))])
save_model(onnx_model, 'lr.onnx')
sess = InferenceSession('lr.onnx')
res = sess.run(None, input_feed={'input': X_test.astype(np.float32)})

np.mean(np.isclose(model.predict_proba(X_test), list(map(lambda x:
                                                         list(map(lambda y: x[y], x)), res[1]))))

>> 0.010901001112347052


Discussion:

Resolving patch:
diff --git a/skl2onnx/operator_converters/linear_classifier.py b/skl2onnx/operator_converters/linear_classifier.py
--- a/skl2onnx/operator_converters/linear_classifier.py
+++ b/skl2onnx/operator_converters/linear_classifier.py
@@ -16,12 +16,14 @@ def convert_sklearn_linear_classifier(scope, operator, container):
     classes = op.classes_
     number_of_classes = len(classes)
     coefficients = op.coef_.flatten().astype(float).tolist()
+
     if isinstance(op.intercept_, (float, np.float32)) and op.intercept_ == 0:
         # fit_intercept = False
         intercepts = ([0.0] * number_of_classes if number_of_classes != 2 else
                       [0.0])
     else:
         intercepts = op.intercept_.tolist()
+
     if number_of_classes == 2:
         coefficients = list(map(lambda x: -1 * x, coefficients)) + coefficients
         intercepts = list(map(lambda x: -1 * x, intercepts)) + intercepts
@@ -45,26 +47,14 @@ def convert_sklearn_linear_classifier(scope, operator, container):
     if op.__class__.__name__ == 'LinearSVC':
         classifier_attrs['post_transform'] = 'NONE'
     elif op.__class__.__name__ == 'LogisticRegression':
-        if multi_class == 2:
-            if number_of_classes == 2:
-                """
-                See method _predict_proba_lr.
-                When number if classes is two, the function
-                is not SOFTMAX.
-                https://github.com/scikit-learn/scikit-learn/blob/bac89c253b35a8f1a3827389fbee0f5bebcbc985/sklearn/linear_model/base.py#L300
-                """ # noqa
-                classifier_attrs['post_transform'] = 'LOGISTIC'
-            else:
-                classifier_attrs['post_transform'] = 'LOGISTIC'
-        else:
-            classifier_attrs['post_transform'] = 'LOGISTIC'
-    elif op.__class__.__name__ in ('LinearSVC'):
-        classifier_attrs['post_transform'] = 'NONE'
+        ovr = (op.multi_class in ["ovr", "warn"] or
+               (op.multi_class == 'auto' and (op.classes_.size <= 2 or
+                                              op.solver == 'liblinear')))
+        classifier_attrs['post_transform'] = (
+            'LOGISTIC' if ovr else 'SOFTMAX')
     else:
-        if multi_class == 2:
-            classifier_attrs['post_transform'] = 'SOFTMAX'
-        else:
-            classifier_attrs['post_transform'] = 'LOGISTIC'
+        classifier_attrs['post_transform'] = (
+            'LOGISTIC' if multi_class > 2 else 'SOFTMAX')
 
     if all(isinstance(i, (six.string_types, six.text_type)) for i in classes):
         class_labels = [str(i) for i in classes]


Test patch:
diff --git a/tests/test_sklearn_glm_classifier_converter.py b/tests/test_sklearn_glm_classifier_converter.py
--- a/tests/test_sklearn_glm_classifier_converter.py
+++ b/tests/test_sklearn_glm_classifier_converter.py
@@ -95,6 +95,51 @@ def test_model_logistic_regression_multi_class(self):
                           " <= StrictVersion('0.2.1')",
         )
 
+    @unittest.skipIf(not onnx_built_with_ml(),
+                     reason="Requires ONNX-ML extension.")
+    def test_model_logistic_regression_multi_class_ovr(self):
+        model, X = self._fit_model_multiclass_classification(
+            linear_model.LogisticRegression(multi_class='ovr'))
+        model_onnx = convert_sklearn(
+            model,
+            "multi-class logistic regression",
+            [("input", FloatTensorType([1, 3]))],
+        )
+        self.assertIsNotNone(model_onnx)
+        dump_data_and_model(
+            X.astype(numpy.float32),
+            model,
+            model_onnx,
+            basename="SklearnLogitisticRegressionMulti",
+            allow_failure="StrictVersion(onnx.__version__)"
+                          " < StrictVersion('1.2') or "
+                          "StrictVersion(onnxruntime.__version__)"
+                          " <= StrictVersion('0.2.1')",
+        )
+
+    @unittest.skipIf(not onnx_built_with_ml(),
+                     reason="Requires ONNX-ML extension.")
+    def test_model_logistic_regression_multi_class_multinomial(self):
+        model, X = self._fit_model_multiclass_classification(
+            linear_model.LogisticRegression(
+                multi_class="multinomial", solver="lbfgs"))
+        model_onnx = convert_sklearn(
+            model,
+            "multi-class logistic regression",
+            [("input", FloatTensorType([1, 3]))],
+        )
+        self.assertIsNotNone(model_onnx)
+        dump_data_and_model(
+            X.astype(numpy.float32),
+            model,
+            model_onnx,
+            basename="SklearnLogitisticRegressionMulti",
+            allow_failure="StrictVersion(onnx.__version__)"
+                          " < StrictVersion('1.2') or "
+                          "StrictVersion(onnxruntime.__version__)"
+                          " <= StrictVersion('0.2.1')",
+        )
+
     @unittest.skipIf(not onnx_built_with_ml(),
                      reason="Requires ONNX-ML extension.")
     def test_model_logistic_regression_multi_class_no_intercept(self):