Repository: onnx/sklearn-onnx

Issue:
convert_sklearn() with CalibratedClassifierCV(base_estimator=RandomForestClassifier) raises a RuntimeError
OS version: `macOS Monterey 12.0.1`
Python version: `3.7.11`

Libraries version:
```python
onnx==1.10.2
onnxconverter-common==1.8.1
onnxruntime==1.9.0
skl2onnx==1.10.0
```


Code to reproduce:
```python
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.calibration import CalibratedClassifierCV

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType


X, y = make_classification(n_samples=100, n_features=2, n_redundant=0, random_state=42)
base_clf = RandomForestClassifier()
calibrated_clf = CalibratedClassifierCV(base_estimator=base_clf, cv=3)
calibrated_clf.fit(X, y)

initial_type = [("float_input", FloatTensorType([None, 4]))]
onx = convert_sklearn(calibrated_clf, initial_types=initial_type)
```

Error traceback:
```python
~/.conda/envs/result-level-relevancy-poc/lib/python3.7/site-packages/skl2onnx/operator_converters/random_forest.py in convert_sklearn_random_forest_classifier(scope, operator, container, op_type, op_domain, op_version)
    192             raise RuntimeError(
    193                 "The converter cannot implement decision_function for "
--> 194                 "'{}'.".format(type(op)))
    195 
    196         input_name = operator.input_full_names

RuntimeError: The converter cannot implement decision_function for '<class 'sklearn.ensemble._forest.RandomForestClassifier'>'.
```


Discussion:

Resolving patch:
diff --git a/skl2onnx/operator_converters/calibrated_classifier_cv.py b/skl2onnx/operator_converters/calibrated_classifier_cv.py
--- a/skl2onnx/operator_converters/calibrated_classifier_cv.py
+++ b/skl2onnx/operator_converters/calibrated_classifier_cv.py
@@ -12,6 +12,7 @@
     guess_numpy_type, Int64TensorType, guess_proto_type)
 from ..common._registration import register_converter
 from .._supported_operators import sklearn_operator_name_map
+from sklearn.ensemble import RandomForestClassifier
 
 
 def _handle_zeros(scope, container, concatenated_prob_name,
@@ -256,6 +257,8 @@ def convert_calibrated_classifier_base_estimator(scope, operator, container,
     #                           |                        |
     #                           V                        |
     #                        class_prob_tensor [M, C] <--'
+    model_proba = {RandomForestClassifier}
+
     if scope.get_options(operator.raw_operator, dict(nocl=False))['nocl']:
         raise RuntimeError(
             "Option 'nocl' is not implemented for operator '{}'.".format(
@@ -275,7 +278,8 @@ def convert_calibrated_classifier_base_estimator(scope, operator, container,
     prob_name = [None] * n_classes
 
     this_operator = scope.declare_local_operator(op_type, base_model)
-    if container.has_options(base_model, 'raw_scores'):
+    if (container.has_options(base_model, 'raw_scores') and
+            not type(base_model) in model_proba):
         container.add_options(id(base_model), {'raw_scores': True})
         scope.add_options(id(base_model), {'raw_scores': True})
     this_operator.inputs = operator.inputs
diff --git a/skl2onnx/operator_converters/random_forest.py b/skl2onnx/operator_converters/random_forest.py
--- a/skl2onnx/operator_converters/random_forest.py
+++ b/skl2onnx/operator_converters/random_forest.py
@@ -191,7 +191,7 @@ def convert_sklearn_random_forest_classifier(
         elif use_raw_scores:
             raise RuntimeError(
                 "The converter cannot implement decision_function for "
-                "'{}'.".format(type(op)))
+                "'{}' and loss '{}'.".format(type(op), loss))
 
         input_name = operator.input_full_names
         if type(operator.inputs[0].type) == BooleanTensorType:


Test patch:
diff --git a/tests/test_sklearn_calibrated_classifier_cv_converter.py b/tests/test_sklearn_calibrated_classifier_cv_converter.py
--- a/tests/test_sklearn_calibrated_classifier_cv_converter.py
+++ b/tests/test_sklearn_calibrated_classifier_cv_converter.py
@@ -11,11 +11,17 @@
 from onnxruntime import InferenceSession, __version__ as ort_version
 from sklearn.calibration import CalibratedClassifierCV
 from sklearn.datasets import load_digits, load_iris
-from sklearn.ensemble import RandomForestClassifier
+from sklearn.ensemble import (
+    RandomForestClassifier, GradientBoostingClassifier)
+try:
+    from sklearn.ensemble import HistGradientBoostingClassifier
+except ImportError:
+    HistGradientBoostingClassifier = None
 from sklearn.linear_model import LogisticRegression
 from sklearn.naive_bayes import MultinomialNB
 from sklearn.neighbors import KNeighborsClassifier
 from sklearn.svm import SVC, LinearSVC
+from sklearn.tree import DecisionTreeClassifier
 from sklearn.exceptions import ConvergenceWarning
 try:
     # scikit-learn >= 0.22
@@ -192,15 +198,81 @@ def test_model_calibrated_classifier_cv_rf(self):
         model = CalibratedClassifierCV(
             base_estimator=RandomForestClassifier(n_estimators=2),
             method='sigmoid').fit(X, y)
-        try:
-            convert_sklearn(
-                model, "unused",
-                [("input", FloatTensorType([None, X.shape[1]]))],
-                target_opset=TARGET_OPSET)
-            raise AssertionError(
-                "RandomForestClassifier has no decision_function")
-        except RuntimeError as e:
-            assert "cannot implement decision_function" in str(e)
+        model_onnx = convert_sklearn(
+            model, "clarf",
+            [("input", FloatTensorType([None, X.shape[1]]))],
+            target_opset=TARGET_OPSET)
+        dump_data_and_model(
+            X.astype(np.float32), model, model_onnx,
+            basename="SklearnCalibratedClassifierRF")
+
+    @unittest.skipIf(not onnx_built_with_ml(),
+                     reason="Requires ONNX-ML extension.")
+    @unittest.skipIf(
+        StrictVersion(ort_version) < StrictVersion("0.5.0"),
+        reason="not available")
+    @ignore_warnings(
+        category=(FutureWarning, ConvergenceWarning, DeprecationWarning))
+    def test_model_calibrated_classifier_cv_gbt(self):
+        data = load_iris()
+        X, y = data.data, data.target
+        y[y > 1] = 1
+        model = CalibratedClassifierCV(
+            base_estimator=GradientBoostingClassifier(n_estimators=2),
+            method='sigmoid').fit(X, y)
+        model_onnx = convert_sklearn(
+            model, "clarf",
+            [("input", FloatTensorType([None, X.shape[1]]))],
+            target_opset=TARGET_OPSET)
+        dump_data_and_model(
+            X.astype(np.float32), model, model_onnx,
+            basename="SklearnCalibratedClassifierGBT")
+
+    @unittest.skipIf(not onnx_built_with_ml(),
+                     reason="Requires ONNX-ML extension.")
+    @unittest.skipIf(
+        HistGradientBoostingClassifier is None, reason="not available")
+    @unittest.skipIf(
+        StrictVersion(ort_version) < StrictVersion("0.5.0"),
+        reason="not available")
+    @ignore_warnings(
+        category=(FutureWarning, ConvergenceWarning, DeprecationWarning))
+    def test_model_calibrated_classifier_cv_hgbt(self):
+        data = load_iris()
+        X, y = data.data, data.target
+        y[y > 1] = 1
+        model = CalibratedClassifierCV(
+            base_estimator=HistGradientBoostingClassifier(max_iter=4),
+            method='sigmoid').fit(X, y)
+        model_onnx = convert_sklearn(
+            model, "clarf",
+            [("input", FloatTensorType([None, X.shape[1]]))],
+            target_opset=TARGET_OPSET)
+        dump_data_and_model(
+            X.astype(np.float32), model, model_onnx,
+            basename="SklearnCalibratedClassifierHGBT")
+
+    @unittest.skipIf(not onnx_built_with_ml(),
+                     reason="Requires ONNX-ML extension.")
+    @unittest.skipIf(
+        StrictVersion(ort_version) < StrictVersion("0.5.0"),
+        reason="not available")
+    @ignore_warnings(
+        category=(FutureWarning, ConvergenceWarning, DeprecationWarning))
+    def test_model_calibrated_classifier_cv_tree(self):
+        data = load_iris()
+        X, y = data.data, data.target
+        y[y > 1] = 1
+        model = CalibratedClassifierCV(
+            base_estimator=DecisionTreeClassifier(),
+            method='sigmoid').fit(X, y)
+        model_onnx = convert_sklearn(
+            model, "clarf",
+            [("input", FloatTensorType([None, X.shape[1]]))],
+            target_opset=TARGET_OPSET)
+        dump_data_and_model(
+            X.astype(np.float32), model, model_onnx,
+            basename="SklearnCalibratedClassifierDT")
 
     @unittest.skipIf(not onnx_built_with_ml(),
                      reason="Requires ONNX-ML extension.")