Repository: onnx/sklearn-onnx

Issue:
Output Shape
Hello,

I've followed the tutorial to Convert your scikit-learn model into ONNX,

If I understand well, the shape `[None, 4]` means that the first dimension is dynamic. In order to batch calls to the model.

The thing I don't understand is why the output tensor has a shape of `[1]` and not `[None]`

```python
input name='float_input' and shape=[None, 4]
NodeArg(name='output_label', type='tensor(int64)', shape=[1])
```





Discussion:
Can you give me link to the tutorial? I don't use output shape when converting models, so I'll need to look at the exact code.
I've used this one, 

I've just replace: 
```
initial_type = [('float_input', FloatTensorType([1, 4]))]
``` 
to
```
initial_type = [('float_input', FloatTensorType([None, 4]))]
```

Seems like the first one don't work.
```
onnxruntime.capi.onnxruntime_pybind11_state.InvalidArgument: [ONNXRuntimeError] : 2 : INVALID_ARGUMENT : Got invalid dimensions for input: float_input for the following indices
 index: 0 Got: 38 Expected: 1
 Please fix either the inputs or the model.
```
I don't see a link in your message, are you using this: http://onnx.ai/sklearn-onnx/?
Yes, we need to use _None_ for dynamic number of inputs because of changes in latest onnxruntime.
Yes it’s this one. (Forgot to paste, sorry)
Yes I understood the None as input that create the dynamic shape. 

But the 1 as output should then be None ? As it’s a scalar output with a dynamic shape also ?
This is the correct example:

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y)
clr = RandomForestClassifier()
clr.fit(X_train, y_train)

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
initial_type = [('float_input', FloatTensorType([None, 4]))]
onx = convert_sklearn(clr, initial_types=initial_type)
with open("rf_iris.onnx", "wb") as f:
    f.write(onx.SerializeToString())

import onnxruntime as rt
import numpy
sess = rt.InferenceSession("rf_iris.onnx")
input_name = sess.get_inputs()[0].name
label_name = sess.get_outputs()[0].name
pred_onx = sess.run([label_name], {input_name: X_test.astype(numpy.float32)})[0]

Replace 1 -> None in input shape. I don't see output shape in this example, may be you used a different one. Can you paste your complete code here? Sorry, I have never used output shape before.
Yes you are right, this is the code.

Then you can print the onnx input / outputs and you will see the i/o shape.
```python3
print("input name='{}' and shape={}".format(
    sess.get_inputs()[0].name, sess.get_inputs()[0].shape))
print("output name='{}' and shape={}".format(
    sess.get_outputs()[0].name, sess.get_outputs()[0].shape))
```
will print:
```
input name='float_input' and shape=[None, 4]
output name='output_label' and shape=[1]
```

I'm not setting up the output shape, just displaying it


I see what you mean, I'll mark this as a bug and debug this further.
Great. 

If you give me some pointer I be glad to help you
It seems a bug in onnxruntime and onnx (see #323), it works for regressors but fails for classifiers.

Resolving patch:
diff --git a/skl2onnx/_parse.py b/skl2onnx/_parse.py
--- a/skl2onnx/_parse.py
+++ b/skl2onnx/_parse.py
@@ -41,7 +41,7 @@ class OutlierMixin:
 from .common._topology import Topology
 from .common.data_types import DictionaryType
 from .common.data_types import Int64TensorType, SequenceType
-from .common.data_types import Int64Type, StringType, TensorType
+from .common.data_types import StringTensorType, TensorType
 from .common.utils import get_column_indices
 from .common.utils_checking import check_signature
 from .common.utils_classifier import get_label_classes
@@ -334,7 +334,7 @@ def _parse_sklearn_classifier(scope, model, inputs, custom_parsers=None):
         return probability_tensor
     this_operator = scope.declare_local_operator('SklearnZipMap')
     this_operator.inputs = probability_tensor
-    label_type = Int64Type()
+    label_type = Int64TensorType([None])
     classes = get_label_classes(scope, model)
 
     if (isinstance(model.classes_, list) and
@@ -354,7 +354,7 @@ def _parse_sklearn_classifier(scope, model, inputs, custom_parsers=None):
     else:
         classes = np.array([s.encode('utf-8') for s in classes])
         this_operator.classlabels_strings = classes
-        label_type = StringType()
+        label_type = StringTensorType([None])
 
     output_label = scope.declare_local_variable('output_label', label_type)
     output_probability = scope.declare_local_variable(
diff --git a/skl2onnx/common/_topology.py b/skl2onnx/common/_topology.py
--- a/skl2onnx/common/_topology.py
+++ b/skl2onnx/common/_topology.py
@@ -220,21 +220,18 @@ def infer_types(self):
     def tensor_type(self):
         if self.dtype == np.float32:
             return FloatTensorType
-        elif self.dtype == np.float64:
+        if self.dtype == np.float64:
             return FloatTensorType
-        else:
-            raise NotImplementedError(
-                "Unable to guess the tensor type from {}.".format(self.dtype))
+        raise NotImplementedError(
+            "Unable to guess the tensor type from [{}].".format(self.dtype))
 
     @property
     def proto_type(self):
         if self.dtype == np.float32:
             return onnx_proto.TensorProto.FLOAT
-        elif self.dtype == np.float64:
+        if self.dtype == np.float64:
             return onnx_proto.TensorProto.DOUBLE
-        else:
-            raise ValueError("dtype should be either np.float32 or "
-                             "np.float64.")
+        raise ValueError("dtype should be either np.float32 or np.float64.")
 
 
 class Scope:


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
@@ -5,6 +5,7 @@
 from sklearn import linear_model
 from sklearn.svm import LinearSVC
 from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
+from onnxruntime import InferenceSession, __version__ as ort_version
 from skl2onnx import convert_sklearn
 from skl2onnx.common.data_types import (
     BooleanTensorType,
@@ -46,6 +47,42 @@ def test_model_logistic_regression_binary_class(self):
                           "StrictVersion(onnxruntime.__version__)"
                           " <= StrictVersion('0.2.1')",
         )
+        if StrictVersion(ort_version) >= StrictVersion("1.0.0"):
+            sess = InferenceSession(model_onnx.SerializeToString())
+            out = sess.get_outputs()
+            lb = out[0].type
+            sh = out[0].shape
+            self.assertEqual(str(lb), "tensor(int64)")
+            self.assertEqual(sh, [None])
+
+    @unittest.skipIf(not onnx_built_with_ml(),
+                     reason="Requires ONNX-ML extension.")
+    def test_model_logistic_regression_binary_class_string(self):
+        model, X = fit_classification_model(
+            linear_model.LogisticRegression(max_iter=100), 2,
+            label_string=True)
+        model_onnx = convert_sklearn(
+            model, "logistic regression",
+            [("input", FloatTensorType([None, X.shape[1]]))])
+        self.assertIsNotNone(model_onnx)
+        dump_data_and_model(
+            X,
+            model,
+            model_onnx,
+            basename="SklearnLogitisticRegressionBinary",
+            # Operator cast-1 is not implemented in onnxruntime
+            allow_failure="StrictVersion(onnx.__version__)"
+                          " < StrictVersion('1.3') or "
+                          "StrictVersion(onnxruntime.__version__)"
+                          " <= StrictVersion('0.2.1')",
+        )
+        if StrictVersion(ort_version) >= StrictVersion("1.0.0"):
+            sess = InferenceSession(model_onnx.SerializeToString())
+            out = sess.get_outputs()
+            lb = out[0].type
+            sh = out[0].shape
+            self.assertEqual(str(lb), "tensor(string)")
+            self.assertEqual(sh, [None])
 
     def test_model_logistic_regression_int(self):
         model, X = fit_classification_model(