onnx / sklearn-onnx

Convert scikit-learn models and pipelines to ONNX
Apache License 2.0
545 stars 99 forks source link

"Unable to guess ONNX type from type object" KNN #611

Closed gespriella closed 3 years ago

gespriella commented 3 years ago

Hi, I'm getting this error when trying to save my KNN model as onnx and y has string values (that's how it's provided from the fetch_openml call). It works fine when using logisticregression, but fails with KNN on conversion. It does work also if I cast the y values to ints, but my guess is it should probably work without needing to do that.

NotImplementedError: Unable to guess ONNX type from type object. You may raise an issue at https://github.com/onnx/sklearn-onnx/issues.

The code to reproduce is: from sklearn.datasets import fetch_openml from sklearn.neighbors import KNeighborsClassifier from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType

X, y = fetch_openml('mnist_784', version=1, return_X_y=True, as_frame=False) MnistModel = KNeighborsClassifier(n_neighbors=6, weights="distance") MnistModel.fit(X, y) initial_types = [('input', FloatTensorType([1, X.shape[1]]))] onx = convert_sklearn(MnistModel, initial_types=initial_types) with open("MnistLR.onnx", "wb") as file: file.write(onx.SerializeToString())

It fails on convert_sklearn.

Here's the debug info:


NotImplementedError Traceback (most recent call last)

in 1 initial_types = [('input', FloatTensorType([1, X.shape[1]]))] ----> 2 onx = convert_sklearn(MnistModel, initial_types=initial_types) 3 with open("MnistLR.onnx", "wb") as file: 4 file.write(onx.SerializeToString()) ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\convert.py in convert_sklearn(model, name, initial_types, doc_string, target_opset, custom_conversion_functions, custom_shape_calculators, custom_parsers, options, dtype, intermediate, white_op, black_op, final_types) 151 152 # Convert our Topology object into ONNX. The outcome is an ONNX model. --> 153 onnx_model = convert_topology(topology, name, doc_string, target_opset, 154 dtype=dtype, options=options) 155 ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\common\_topology.py in convert_topology(topology, model_name, doc_string, target_opset, channel_first_inputs, dtype, options) 1052 type(getattr(operator, 'raw_model', None)))) 1053 container.validate_options(operator) -> 1054 conv(scope, operator, container) 1055 1056 # Create a graph from its main components ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\common\_registration.py in __call__(self, *args) 27 if args[1].raw_operator is not None: 28 args[2]._get_allowed_options(args[1].raw_operator) ---> 29 return self._fct(*args) 30 31 def get_allowed_options(self): ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\operator_converters\nearest_neighbours.py in convert_nearest_neighbors_classifier(scope, operator, container) 369 out_labels = OnnxReshape(res_name, np.array([-1], dtype=np.int64), 370 output_names=out[:1], op_version=opv) --> 371 out_labels.add_to(scope, container) 372 probas.add_to(scope, container) 373 ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\algebra\onnx_operator.py in add_to(self, scope, container, operator) 416 op_domain=domain, onnx_prefix_name=self.onnx_prefix, 417 **kwargs) --> 418 self.state.run(operator=operator) 419 420 @property ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\algebra\graph_state.py in run(self, operator) 191 inputs = [] 192 for i in self.inputs: --> 193 v = self._get_var_name(i, False, operator=operator) 194 if v is not None: 195 inputs.append(v) ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\algebra\graph_state.py in _get_var_name(self, var, unused, operator) 64 return self._add_constant(var.ConstantValue, var.ImplicitCast) 65 elif hasattr(var, 'add_to'): ---> 66 var.add_to(self.scope, self.container, operator=operator) 67 outputs = var.outputs 68 if isinstance(outputs, list): ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\algebra\onnx_operator.py in add_to(self, scope, container, operator) 416 op_domain=domain, onnx_prefix_name=self.onnx_prefix, 417 **kwargs) --> 418 self.state.run(operator=operator) 419 420 @property ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\algebra\graph_state.py in run(self, operator) 191 inputs = [] 192 for i in self.inputs: --> 193 v = self._get_var_name(i, False, operator=operator) 194 if v is not None: 195 inputs.append(v) ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\algebra\graph_state.py in _get_var_name(self, var, unused, operator) 62 return self._add_constant(var) 63 elif hasattr(var, 'ConstantValue'): ---> 64 return self._add_constant(var.ConstantValue, var.ImplicitCast) 65 elif hasattr(var, 'add_to'): 66 var.add_to(self.scope, self.container, operator=operator) ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\algebra\graph_state.py in _add_constant(self, cst, can_cast) 119 name = self.scope.get_unique_variable_name( 120 self.onnx_prefix + 'cst') --> 121 cst, ty, astype = _ty_astype(cst) 122 if astype is not None: 123 cst = cst.astype(astype) ~\AppData\Roaming\Python\Python38\site-packages\skl2onnx\algebra\graph_state.py in _ty_astype(cst) 108 cst = np.array([s.encode('utf-8') for s in cst]) 109 else: --> 110 raise NotImplementedError( 111 "Unable to guess ONNX type from type {}. " 112 "You may raise an issue at https://github.com/onnx/" NotImplementedError: Unable to guess ONNX type from type object. You may raise an issue at https://github.com/onnx/sklearn-onnx/issues.
xadupre commented 3 years ago

The converter does not support string labels in that case. Adding y = y.astype(np.int64) before training fiwes your issue.

gespriella commented 3 years ago

Thank you, it works fine when casting to integers. I also noticed there's a slight difference between the probabilities returned by the same KNN model produced as ONNX and Pickle. Just wanted to ask if you know why this might be happening. I'm giving a short presentation on using models trained in PY on C# with ONNX for my university and want to make sure I can explain this difference.

Tested RandomTrees, ExtraTrees and Logistic, and they all produce the same results, but KNN doesn't: KNN difference

I'm using KNeighborsClassifier(n_neighbors=6, weights="distance").fit(X,y) for training in python. onx = convert_sklearn(MnistModelKNN, initial_types=[('input', FloatTensorType([1, X.shape[1]]))]) for saving to ONNX. And the following to predict on C#:

var tensor = new DenseTensor<float>(floatArray, inferenceSession.InputMetadata["input"].Dimensions);
var results = inferenceSession.Run(new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", tensor) }).ToArray();
xadupre commented 3 years ago

You could use double instead of float. The onnx graph for KNNClassifier (http://www.xavierdupre.fr/app/mlprodict/helpsphinx/skl_converters/visual-neighbors-001.html) includes a node Reciprocal and everything related matrix inversion usually increases the probability of having discrepencies. We could probably switch to double in the middle of the pipeline by adding an explicit option. You'll find other details here: http://onnx.ai/sklearn-onnx/auto_tutorial/plot_ebegin_float_double.html.

gespriella commented 3 years ago

Thanks very much for your response. I'll test out the MLProdict options at the second link as soon as I can. In the meantime, it seems that using the KNeighborsClassifier() without the hyperparameters that I had explicitly added (n_neighbors=6 and weight='distance'), makes it show no discrepancies. I wonder if the weight='distance' is what's triggering the discrepancy in this case, since it "weight points by the inverse of their distance".

update: Actually it seems very likely that the weight='distance' was to blame, since the RECIPROCAL node disappeared now that I left it as the default (uniform): Screenshot 2021-02-09 112329

xadupre commented 3 years ago

Definitively yes! Anytime inverse is used, discrepencies usually appear, mostly because there are null values in almost most cases. The difference in orders of magnitude between small and big values in the inverse matrix are usually higher than the original matrix. I probably should add a section in the above link to study (1 / (float)x) - (float)(1 / x).

xadupre commented 3 years ago

May I close the issue?

gespriella commented 3 years ago

Yes, thank you!