Repository: onnx/sklearn-onnx

Issue:
Converted TfidfVectorizer can't be run with dtype float64
I am trying to convert a tfidf Vectorizer followed by naive bayes to onnx and run it. I had discrepancies in the results between sklearn and onnx for some test sets, which were far worse than the usual difference in the 6th decimal point. Then i tried switching to dtype=numpy.float64 in the convert_sklearn method but running that in onnxruntime fails.

`import numpy as np`
`from sklearn.feature_extraction.text import TfidfVectorizer`
`import onnxruntime`
`from skl2onnx import convert_sklearn`
`from skl2onnx.common.data_types import StringTensorType`

`data = np.array(["ksd f i o osp  p posfj jp pas jj",`
`                "jfio psjfpisaj jfsaidfsfipjfp p gfdg asf",`
`                   "soh fo fsf dnp fdfjkdpfl f fpgpplf"],`
`                dtype=object)`

`initial_type = [('string_input', StringTensorType([None]))]`

`tfidf = TfidfVectorizer()`

`tfidf.fit(data)`

`onx = convert_sklearn(tfidf, initial_types=initial_type, dtype=np.float64)`

`sess = onnxruntime.InferenceSession(onx.SerializeToString())`

---------------------------------------------------------------------------
Fail                                      Traceback (most recent call last)
<ipython-input-1-d80a1d2a9a91> in <module>
     20 onx = convert_sklearn(tfidf, initial_types=initial_type, dtype=np.float64)
     21 
---> 22 sess = onnxruntime.InferenceSession(onx.SerializeToString())

c:\users\u733825\appdata\local\continuum\miniconda2\envs\classifly_from_yml\lib\site-packages\onnxruntime\capi\session.py in __init__(self, path_or_bytes, sess_options, providers)
     23         self._path_or_bytes = path_or_bytes
     24         self._sess_options = sess_options
---> 25         self._load_model(providers)
     26         self._enable_fallback = True
     27 

c:\users\u733825\appdata\local\continuum\miniconda2\envs\classifly_from_yml\lib\site-packages\onnxruntime\capi\session.py in _load_model(self, providers)
     41             raise TypeError("Unable to load from type '{0}'".format(type(self._path_or_bytes)))
     42 
---> 43         self._sess.load_model(providers)
     44 
     45         self._session_options = self._sess.session_options

Fail: [ONNXRuntimeError] : 1 : FAIL : Type Error: Type parameter (T) bound to different types (tensor(float) and tensor(double) in node (Mul).


---------------------------------------------------------------------------

If the dtype of the convert method is chnaged to float32
`convert_sklearn(tfidf, initial_types=initial_type, dtype=np.float32)`
there is no error. The sklearn TfidfVectorizer has a default dtype argument of numpy.float64

Used versions:
numpy.__version__: 1.17.0
sklearn.__version__: 0.22.1
onnxruntime.__version__: 1.1.0
skl2onnx.__version__: 1.6.0
Python.version: 3.7.6





Discussion:
I think float64 is not handled properly, that needs to be fixed. Meanwhile, can you paste the example with float32 that gives you mismatches with scikit output?
Thanks for the quick reply. Unfortunately I can't share the data. The structure is very similar to the above example just with many more lines and a class label for each line. I would like to anonymize it while keeping the result but I'm not too confident about that.

Resolving patch:
diff --git a/skl2onnx/operator_converters/text_vectoriser.py b/skl2onnx/operator_converters/text_vectoriser.py
--- a/skl2onnx/operator_converters/text_vectoriser.py
+++ b/skl2onnx/operator_converters/text_vectoriser.py
@@ -324,14 +324,24 @@ def convert_sklearn_text_vectorizer(scope, operator, container):
     output = (scope.get_unique_variable_name('output')
               if op.binary else operator.output_full_names)
 
+    if container.proto_dtype == onnx_proto.TensorProto.DOUBLE:
+        output_tf = scope.get_unique_variable_name('cast_result')
+    else:
+        output_tf = output
+
     if container.target_opset < 9:
         op_type = 'Ngram'
-        container.add_node(op_type, tokenized, output,
+        container.add_node(op_type, tokenized, output_tf,
                            op_domain='com.microsoft', **attrs)
     else:
         op_type = 'TfIdfVectorizer'
-        container.add_node(op_type, tokenized, output, op_domain='',
+        container.add_node(op_type, tokenized, output_tf, op_domain='',
                            op_version=9, **attrs)
+
+    if container.proto_dtype == onnx_proto.TensorProto.DOUBLE:
+        apply_cast(scope, output_tf, output,
+                   container, to=container.proto_dtype)
+
     if op.binary:
         cast_result_name = scope.get_unique_variable_name('cast_result')
 
diff --git a/skl2onnx/operator_converters/tfidf_vectoriser.py b/skl2onnx/operator_converters/tfidf_vectoriser.py
--- a/skl2onnx/operator_converters/tfidf_vectoriser.py
+++ b/skl2onnx/operator_converters/tfidf_vectoriser.py
@@ -3,10 +3,10 @@
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
 # --------------------------------------------------------------------------
-
+from onnx import onnx_pb as onnx_proto
 from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
 from ..common._apply_operation import apply_identity
-from ..common.data_types import FloatTensorType
+from ..common.data_types import FloatTensorType, DoubleTensorType
 from ..common._registration import register_converter
 from .._supported_operators import sklearn_operator_name_map
 
@@ -23,7 +23,15 @@ def convert_sklearn_tfidf_vectoriser(scope, operator, container):
     cv_operator.inputs = operator.inputs
     cv_output_name = scope.declare_local_variable('count_vec_output')
     columns = max(operator.raw_operator.vocabulary_.values()) + 1
-    cv_output_name.type = FloatTensorType([None, columns])
+    if container.proto_dtype == onnx_proto.TensorProto.FLOAT:
+        clr = FloatTensorType
+    elif container.proto_dtype == onnx_proto.TensorProto.DOUBLE:
+        clr = DoubleTensorType
+    else:
+        raise RuntimeError(
+            "Unexpected dtype '{}'. Float or double expected.".format(
+                container.proto_dtype))
+    cv_output_name.type = clr([None, columns])
     cv_operator.outputs.append(cv_output_name)
 
     op_type = sklearn_operator_name_map[TfidfTransformer]


Test patch:
diff --git a/tests/test_sklearn_tfidf_vectorizer_converter.py b/tests/test_sklearn_tfidf_vectorizer_converter.py
--- a/tests/test_sklearn_tfidf_vectorizer_converter.py
+++ b/tests/test_sklearn_tfidf_vectorizer_converter.py
@@ -457,6 +457,39 @@ def test_model_tfidf_vectorizer_binary(self):
                           " <= StrictVersion('0.4.0')",
         )
 
+    @unittest.skipIf(
+        StrictVersion(onnx.__version__) <= StrictVersion("1.4.1"),
+        reason="Requires opset 9.")
+    @unittest.skipIf(
+        StrictVersion(onnxruntime.__version__) <= StrictVersion("0.3.0"),
+        reason="Requires opset 9.")
+    def test_model_tfidf_vectorizer11_64(self):
+        corpus = numpy.array([
+            "This is the first document.",
+            "This document is the second document.",
+            "And this is the third one.",
+            "Is this the first document?",
+        ]).reshape((4, 1))
+        vect = TfidfVectorizer(ngram_range=(1, 1), norm=None)
+        vect.fit(corpus.ravel())
+        model_onnx = convert_sklearn(vect, "TfidfVectorizer",
+                                     [("input", StringTensorType())],
+                                     options=self.get_options(),
+                                     dtype=numpy.float64)
+        self.assertTrue(model_onnx is not None)
+        dump_data_and_model(
+            corpus,
+            vect,
+            model_onnx,
+            basename="SklearnTfidfVectorizer1164-OneOff-SklCol",
+            allow_failure="StrictVersion(onnxruntime.__version__)"
+                          " <= StrictVersion('0.4.0')",
+        )
+
+        sess = InferenceSession(model_onnx.SerializeToString())
+        res = sess.run(None, {'input': corpus.ravel()})[0]
+        assert res.shape == (4, 9)
+
 
 if __name__ == "__main__":
     unittest.main()