Repository: onnx/sklearn-onnx

Issue:
OnnxConstantOfShape Cannot Properly Infer Data Type for Argument `value`
I am testing my custom converter for relevance vector machien that I just created with input type np.float64 type:
```
onx = to_onnx(rvr, X_train.astype(np.float64), target_opset=13)
```
**Scenario 1:**
With the following code:
```
shape: np.ndarray = np.array([n_features], dtype=int)
OnnxConstantOfShape(shape, value=np.float64(1.0), op_version=op_version)
```
The model could be converted without error.
However, at inference time, the following error is raised:
```
InvalidGraph: [ONNXRuntimeError] : 10 : INVALID_GRAPH : This is an invalid model. Error in Node:Co_ConstantOfShape : Mismatched attribute type in 'Co_ConstantOfShape : value'
---------------------------------------------------------------------------
InvalidGraph                              Traceback (most recent call last)
~/Documents/research/sklearn_plugins/test/sklearn_plugins/rvm/test_rvr_export.py in 
----> 63 sess = rt.InferenceSession(onx.SerializeToString())
      64 results = sess.run(None, {'X': X_train.astype(np.float64)})

~/.local/lib/python3.8/site-packages/onnxruntime/capi/onnxruntime_inference_collection.py in __init__(self, path_or_bytes, sess_options, providers, provider_options)
    278 
    279         try:
--> 280             self._create_inference_session(providers, provider_options)
    281         except RuntimeError:
    282             if self._enable_fallback:

~/.local/lib/python3.8/site-packages/onnxruntime/capi/onnxruntime_inference_collection.py in _create_inference_session(self, providers, provider_options)
    307             sess = C.InferenceSession(session_options, self._model_path, True, self._read_config_from_model)
    308         else:
--> 309             sess = C.InferenceSession(session_options, self._model_bytes, False, self._read_config_from_model)
    310 
    311         # initialize the C++ InferenceSession

InvalidGraph: [ONNXRuntimeError] : 10 : INVALID_GRAPH : This is an invalid model. Error in Node:Co_ConstantOfShape : Mismatched attribute type in 'Co_ConstantOfShape : value'
```
I visualized the exported model with Netron and found out value is of type float32.

![image](https://user-images.githubusercontent.com/32878707/130131876-19f94530-600c-46ad-8b7c-f65374b92267.png)

**Scenario 2:**
However, if I try the following code:
```
shape: np.ndarray = np.array([n_features], dtype=int)
OnnxConstantOfShape(shape, value=np.array(1.0, dtype=np.float64), op_version=op_version)
```
The `to_onnx` call couldn't even finish:
```
Traceback (most recent call last):
  File \"<ipython-input-6-ef63ae625366>\", line 6, in <module>
    onx = to_onnx(rvr, X_train.astype(np.float64), target_opset=13)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/convert.py\", line 216, in to_onnx
    return convert_sklearn(model, initial_types=initial_types,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/convert.py\", line 162, in convert_sklearn
    onnx_model = convert_topology(topology, name, doc_string, target_opset,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/common/_topology.py\", line 1202, in convert_topology
    conv(scope, operator, container)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/common/_registration.py\", line 26, in __call__
    return self._fct(*args)
  File \"<ipython-input-2-c107a6998a24>\", line 3, in converter
    rvr_converter(scope=scope,
  File \"/home/fred/Documents/research/sklearn_plugins/src/sklearn_plugins/rvm/onnx_transfrom.py\", line 146, in rvr_converter
    y.add_to(scope=scope, container=container)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/onnx_operator.py\", line 549, in add_to
    self.state.run()
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 414, in run
    v = self._get_var_name(i, False, index=None)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 124, in _get_var_name
    var.add_to(self.scope, self.container, operator=operator,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/onnx_operator.py\", line 549, in add_to
    self.state.run()
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 414, in run
    v = self._get_var_name(i, False, index=None)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 124, in _get_var_name
    var.add_to(self.scope, self.container, operator=operator,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/onnx_operator.py\", line 549, in add_to
    self.state.run()
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 414, in run
    v = self._get_var_name(i, False, index=None)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 124, in _get_var_name
    var.add_to(self.scope, self.container, operator=operator,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/onnx_operator.py\", line 549, in add_to
    self.state.run()
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 414, in run
    v = self._get_var_name(i, False, index=None)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 124, in _get_var_name
    var.add_to(self.scope, self.container, operator=operator,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/onnx_operator.py\", line 549, in add_to
    self.state.run()
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 414, in run
    v = self._get_var_name(i, False, index=None)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 124, in _get_var_name
    var.add_to(self.scope, self.container, operator=operator,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/onnx_operator.py\", line 549, in add_to
    self.state.run()
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 414, in run
    v = self._get_var_name(i, False, index=None)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 124, in _get_var_name
    var.add_to(self.scope, self.container, operator=operator,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/onnx_operator.py\", line 549, in add_to
    self.state.run()
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 414, in run
    v = self._get_var_name(i, False, index=None)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 124, in _get_var_name
    var.add_to(self.scope, self.container, operator=operator,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/onnx_operator.py\", line 549, in add_to
    self.state.run()
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/algebra/graph_state.py\", line 503, in run
    self.container.add_node(
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/common/_container.py\", line 555, in add_node
    node = make_node(op_type, inputs, outputs, name=name,
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/proto/onnx_helper_modified.py\", line 68, in make_node
    node.attribute.extend(
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/proto/onnx_helper_modified.py\", line 69, in <genexpr>
    make_attribute(key, value, dtype=_dtype, domain=domain)
  File \"/home/fred/anaconda3/envs/sklearn_plugins/lib/python3.8/site-packages/skl2onnx/proto/onnx_helper_modified.py\", line 130, in make_attribute
    byte_array = [_to_bytes_or_false(v) for v in value]
TypeError: iteration over a 0-d **array**
```

I set the data type to np.float64 for testing purpose.


Discussion:

**_Update_**
The same error shows up even when all the tensor is running in `np.float32`, that is the same error shows up even with the following two code snippets.
```
onx = to_onnx(rvr, X_train.astype(np.float64), target_opset=13)
```
```
shape: np.ndarray = np.array([n_features], dtype=int)
OnnxConstantOfShape(shape, value=np.float32(1.0), op_version=op_version)
```
This makes me believe it is expecting the value argument is expecting things like `np.array(1.0, dtype=np.float32)` or `np.array(1.0, dtype=np.float64)`. But if I try the `np.array()` approach, the error described in **Scenario 2** shows up when I try to do this.
For this operator (following the example in ONNX specification for [ConstantOfShape](https://github.com/onnx/onnx/blob/master/docs/Operators.md#ConstantOfShape)), you can use this code:
```
tensor_value = onnx.helper.make_tensor("value", onnx.TensorProto.FLOAT, [1], [5])
cst = OnnxConstantOfShape(shape, value=tensor_value, op_version=opset)
```

I'll create a PR to make it easier with numpy.


Resolving patch:
diff --git a/skl2onnx/algebra/onnx_operator.py b/skl2onnx/algebra/onnx_operator.py
--- a/skl2onnx/algebra/onnx_operator.py
+++ b/skl2onnx/algebra/onnx_operator.py
@@ -14,7 +14,7 @@
 from ..common._registration import _converter_pool, _shape_calculator_pool
 from .._supported_operators import sklearn_operator_name_map
 from ..proto import get_latest_tested_opset_version, onnx_proto
-from ..proto.onnx_helper_modified import make_graph, make_model
+from ..proto.onnx_helper_modified import make_graph, make_model, from_array
 from ..helpers.onnx_helper import infer_outputs
 from .graph_state import GraphState, GraphStateVar
 from .type_helper import _guess_type
@@ -218,7 +218,7 @@ def __init__(self, *inputs, op_version=None, output_names=None,
                  domain=None, **kwargs):
 
         if (output_names is None and
-                self.__class__.__name__ in {"OnnxScan"}):
+                self.__class__.__name__.startswith("OnnxScan")):
             raise NotImplementedError(
                 "The class cannot infer the number of variables "
                 "for node '{}' yet. output_names must be specified"
@@ -381,6 +381,33 @@ def __init__(self, *inputs, op_version=None, output_names=None,
                 self.expected_inputs.append(inp)
 
         self.output_names_ = None
+        self._post_process_attributes()
+
+    def _post_process_attributes(self):
+        """
+        Walks through attributes and replaces them by ONNX
+        values.
+        """
+        if (self.__class__.__name__.startswith("OnnxConstantOfShape") and
+                "value" in self.kwargs):
+            value = self.kwargs['value']
+            if isinstance(value, TensorProto):
+                return
+            if isinstance(value, np.ndarray):
+                if value.shape == (1, ):
+                    val = value[0]
+                elif len(value.shape) == 0:
+                    val = value
+                else:
+                    raise RuntimeError(
+                        "Unexpected shape %r for value, it must be an array "
+                        "of one element." % value.shape)
+                self.kwargs['value'] = from_array(
+                    np.array([val], dtype=value.dtype))
+                return
+            raise TypeError(
+                "Unexpected type %r for value. It should be an array "
+                "of one element." % type(value))
 
     def __str__(self):
         """


Test patch:
diff --git a/tests/test_algebra_onnx_operators.py b/tests/test_algebra_onnx_operators.py
--- a/tests/test_algebra_onnx_operators.py
+++ b/tests/test_algebra_onnx_operators.py
@@ -25,7 +25,8 @@
     OnnxReduceSumSquare, OnnxGemm,
     OnnxAdd, OnnxArgMin, OnnxSqrt,
     OnnxArrayFeatureExtractor, OnnxMul,
-    OnnxPad, OnnxBatchNormalization)
+    OnnxPad, OnnxBatchNormalization,
+    OnnxConstantOfShape)
 from test_utils import dump_data_and_model, TARGET_OPSET
 
 
@@ -203,6 +204,49 @@ def test_constant(self):
         cst = OnnxOperator.ConstantVariable("a")
         self.assertEqual(cst.value, "a")
 
+    def test_constant_of_shape(self):
+        for opset in [TARGET_OPSET, 14, 13, 12, 11, 10, 9]:
+            for value in [np.array([5], dtype=np.float32),
+                          np.array(5, dtype=np.float32)]:
+                if opset > TARGET_OPSET:
+                    continue
+                with self.subTest(opset=opset, value=value):
+                    tensor_value = onnx.helper.make_tensor(
+                        "value", onnx.TensorProto.FLOAT,
+                        [1], [5])
+
+                    cst = OnnxConstantOfShape(
+                        'X', value=tensor_value, op_version=opset,
+                        output_names=['Y'])
+                    shape = np.array([3, 4], dtype=np.int64)
+                    onx = cst.to_onnx(
+                        {'X': shape}, target_opset=opset,
+                        outputs=[('Y', FloatTensorType())])
+                    sess = InferenceSession(onx.SerializeToString())
+                    res = sess.run(None, {'X': shape})
+                    assert_almost_equal(
+                        res[0], np.full(tuple(shape), 5, dtype=np.float32))
+
+                    cst = OnnxConstantOfShape(
+                        'X', value=value, op_version=opset,
+                        output_names=['Y'])
+                    shape = np.array([3, 4], dtype=np.int64)
+                    onx = cst.to_onnx(
+                        {'X': shape}, target_opset=opset,
+                        outputs=[('Y', FloatTensorType())])
+                    sess = InferenceSession(onx.SerializeToString())
+                    res = sess.run(None, {'X': shape})
+                    assert_almost_equal(
+                        res[0], np.full(tuple(shape), 5, dtype=np.float32))
+
+        for opset in [TARGET_OPSET]:
+            for value in [5, np.float32(5)]:
+                with self.subTest(opset=opset, value=value):
+                    with self.assertRaises(TypeError):
+                        OnnxConstantOfShape(
+                            'X', value=value, op_version=opset,
+                            output_names=['Y'])
+
     @unittest.skipIf(StrictVersion(onnx__version__) < StrictVersion("1.4.0"),
                      reason="only available for opset >= 10")
     def test_onnx_reversed_order(self):