Repository: onnx/sklearn-onnx

Issue:
select_model_inputs_outputs clears metadata
Model metadata is lost after a call to `select_model_inputs_outputs`.
select_model_inputs_outputs clears metadata
Model metadata is lost after a call to `select_model_inputs_outputs`.


Discussion:
Can you share the code you are using?
Here is an example:

```python
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx.helpers.onnx_helper import select_model_inputs_outputs
from onnx.onnx_pb import StringStringEntryProto
from sklearn.ensemble import RandomForestClassifier
import numpy as np
from onnxruntime import InferenceSession


feature_names = 'abcd'
X = np.random.normal(size=(500, len(feature_names)))
y = np.random.randint(0, 2, 500)

model = RandomForestClassifier(n_estimators=10)
model.fit(X, y)

onnx = convert_sklearn(
    model,
    initial_types=[('input', FloatTensorType((None, len(feature_names))))],
)

metadata = dict(
    model_author='me',
    feature_names='a,b,c,d',
)
for key, value in metadata.items():
    onnx.metadata_props.append(StringStringEntryProto(key=key, value=value))


# see https://github.com/onnx/sklearn-onnx/issues/202
if hasattr(model, 'predict_proba'):
    onnx = select_model_inputs_outputs(onnx, ['probabilities'])


model_path = 'test.onnx'
with open(model_path, 'wb') as f:
    f.write(onnx.SerializeToString())


session = InferenceSession(model_path)
print(session.get_modelmeta().custom_metadata_map)
```

The last statement prints an empty dict. If I set the metadata after `select_model_inputs_outputs`, the meta data is avaialble:
```
{}
```
vs.

```
{'feature_names': 'a,b,c,d', 'model_author': 'me'}
```


@xadupre: Did you implement select_model_inputs_outputs()? Are you aware of missing metadata issue?
If I read the code correctly, the function creates a new model from the compute graph, thus ignoring any metadata:

https://github.com/onnx/sklearn-onnx/blob/1a0f314eb7f1cbade509e8303a88643e489c3b90/skl2onnx/helpers/onnx_helper.py#L151
Yeah, looks like the meta-data is not being copied.
Can you share the code you are using?
Here is an example:

```python
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx.helpers.onnx_helper import select_model_inputs_outputs
from onnx.onnx_pb import StringStringEntryProto
from sklearn.ensemble import RandomForestClassifier
import numpy as np
from onnxruntime import InferenceSession


feature_names = 'abcd'
X = np.random.normal(size=(500, len(feature_names)))
y = np.random.randint(0, 2, 500)

model = RandomForestClassifier(n_estimators=10)
model.fit(X, y)

onnx = convert_sklearn(
    model,
    initial_types=[('input', FloatTensorType((None, len(feature_names))))],
)

metadata = dict(
    model_author='me',
    feature_names='a,b,c,d',
)
for key, value in metadata.items():
    onnx.metadata_props.append(StringStringEntryProto(key=key, value=value))


# see https://github.com/onnx/sklearn-onnx/issues/202
if hasattr(model, 'predict_proba'):
    onnx = select_model_inputs_outputs(onnx, ['probabilities'])


model_path = 'test.onnx'
with open(model_path, 'wb') as f:
    f.write(onnx.SerializeToString())


session = InferenceSession(model_path)
print(session.get_modelmeta().custom_metadata_map)
```

The last statement prints an empty dict. If I set the metadata after `select_model_inputs_outputs`, the meta data is avaialble:
```
{}
```
vs.

```
{'feature_names': 'a,b,c,d', 'model_author': 'me'}
```


@xadupre: Did you implement select_model_inputs_outputs()? Are you aware of missing metadata issue?
If I read the code correctly, the function creates a new model from the compute graph, thus ignoring any metadata:

https://github.com/onnx/sklearn-onnx/blob/1a0f314eb7f1cbade509e8303a88643e489c3b90/skl2onnx/helpers/onnx_helper.py#L151
Yeah, looks like the meta-data is not being copied.

Resolving patch:
diff --git a/skl2onnx/common/_topology.py b/skl2onnx/common/_topology.py
--- a/skl2onnx/common/_topology.py
+++ b/skl2onnx/common/_topology.py
@@ -400,7 +400,7 @@ class Topology:
     def __init__(self, model, default_batch_size=1, initial_types=None,
                  reserved_variable_names=None, reserved_operator_names=None,
                  target_opset=None, custom_conversion_functions=None,
-                 custom_shape_calculators=None, metadata_props=None):
+                 custom_shape_calculators=None):
         """
         Initializes a *Topology* object, which is an intermediate
         representation of a computational graph.
@@ -435,7 +435,6 @@ def __init__(self, model, default_batch_size=1, initial_types=None,
                     reserved_operator_names
                     if reserved_operator_names is not None else set())
         self.initial_types = initial_types if initial_types else list()
-        self.metadata_props = metadata_props if metadata_props else dict()
         self.default_batch_size = default_batch_size
         self.target_opset = target_opset
         self.custom_conversion_functions = (
diff --git a/skl2onnx/helpers/onnx_helper.py b/skl2onnx/helpers/onnx_helper.py
--- a/skl2onnx/helpers/onnx_helper.py
+++ b/skl2onnx/helpers/onnx_helper.py
@@ -155,6 +155,9 @@ def select_model_inputs_outputs(model, outputs=None, inputs=None):
     onnx_model.domain = model.domain
     onnx_model.model_version = model.model_version
     onnx_model.doc_string = model.doc_string
+    if len(model.metadata_props) > 0:
+        values = {p.key: p.value for p in model.metadata_props}
+        onnx.helper.set_model_props(onnx_model, values)
 
     if len(onnx_model.graph.input) != len(model.graph.input):
         raise RuntimeError("Input mismatch {} != {}".format(


Test patch:
diff --git a/tests/test_onnx_helper.py b/tests/test_onnx_helper.py
--- a/tests/test_onnx_helper.py
+++ b/tests/test_onnx_helper.py
@@ -4,6 +4,7 @@
 import unittest
 from distutils.version import StrictVersion
 import numpy
+import onnx
 from sklearn import __version__ as sklearn_version
 from sklearn.pipeline import make_pipeline
 from sklearn.preprocessing import Binarizer, StandardScaler, OneHotEncoder
@@ -81,6 +82,23 @@ def test_onnx_helper_load_save_init(self):
         assert X1.shape == (4, 2)
         assert X2.shape == (4, 2)
 
+    @unittest.skipIf(
+        not one_hot_encoder_supports_string(),
+        reason="OneHotEncoder did not have categories_ before 0.20",
+    )
+    def test_onnx_helper_load_save_init_meta(self):
+        model = make_pipeline(Binarizer(), OneHotEncoder(sparse=False),
+                              StandardScaler())
+        X = numpy.array([[0.1, 1.1], [0.2, 2.2], [0.4, 2.2], [0.2, 2.4]])
+        model.fit(X)
+        model_onnx = convert_sklearn(model, "pipe3",
+                                     [("input", FloatTensorType([None, 2]))])
+        meta = {'pA': 'one', 'pB': 'two'}
+        onnx.helper.set_model_props(model_onnx, meta)
+        new_model = select_model_inputs_outputs(model_onnx, "variable")
+        vals = {p.key: p.value for p in new_model.metadata_props}
+        assert vals == meta
+
 
 if __name__ == "__main__":
     unittest.main()