Repository: emlearn/emlearn (language: python)

Issue:
Support for MLPRegressor missing
Hi, thanks for the great work!
Doco says you support [MLPRegressor](https://emlearn.readthedocs.io/en/latest/regression.html#classification-models), but the implementation is missing:
```
/tmp/ipykernel_1127/216139384.py in <module>
     40     # Convert model using emlearn
     41     path = f'{regr}_model.h'
---> 42     cmodel = emlearn.convert(regr, method='inline')
     43     cmodel.save(file=path, name=f'{regr}_model')
     44     print('Wrote model to', path)

~/.local/lib/python3.8/site-packages/emlearn/convert.py in convert(estimator, kind, method, dtype)
     26         return bayes.Wrapper(estimator, method)
     27     else:
---> 28         raise ValueError("Unknown model type: '{}'".format(kind))

ValueError: Unknown model type: 'MLPRegressor'
```

Would it be hard to add it?


Discussion / hints:
Hi @adam-ah - thank you for the report. This needs basically the same as #41 - then just a small bit of wiring to support the scikit-learn class.

Fix patch:
diff --git a/emlearn/convert.py b/emlearn/convert.py
index 66172d0..39e7f66 100644
--- a/emlearn/convert.py
+++ b/emlearn/convert.py
@@ -69,8 +69,8 @@ def convert(estimator,
         return distance.Wrapper(estimator, method, dtype=dtype)
     elif kind in ['GaussianMixture', 'BayesianGaussianMixture']:
         return mixture.Wrapper(estimator, method, dtype=dtype)
-    elif kind == 'MLPClassifier':
-        return net.convert_sklearn_mlp(estimator, method)
+    elif kind in ('MLPClassifier', 'MLPRegressor'):
+        return net.convert_sklearn_mlp(estimator, method, return_type=return_type)
     elif kind == 'Sequential':
         return net.convert_keras(estimator, method, return_type)
     elif kind == 'GaussianNB':
diff --git a/emlearn/net.py b/emlearn/net.py
index eca9572..b7c6da4 100644
--- a/emlearn/net.py
+++ b/emlearn/net.py
@@ -43,11 +43,13 @@ def __init__(self, activations, weights, biases, classifier, return_type='classi
             func = 'eml_net_regress1(&{}, values, length)'.format(name)
             code = self.save(name=name)
             self.classifier = common.CompiledClassifier(code, name=name, call=func, out_dtype='float')
-        elif self.inference_type == 'inline':
+        elif self.inference_type == 'inline' and return_type == 'classifier':
             name = 'mynet'
             func = f'{name}_predict(values, length)'
             code = self.save(name=name)
             self.classifier = common.CompiledClassifier(code, name=name, call=func)
+        elif self.inference_type == 'inline' and return_type == 'regressor':
+            raise NotImplementedError("Inline inference not supported for regressors, use loadable instead")
         else:
             raise ValueError(f"Unsupported classifier method '{classifier}' with return_type of '{return_type}'")
 
@@ -255,7 +257,7 @@ def init_layer(name, n_outputs, n_inputs, weights_name, biases_name, activation_
 
     return out
 
-def convert_sklearn_mlp(model, method):
+def convert_sklearn_mlp(model, method, return_type='classifier'):
     """Convert sklearn.neural_network.MLPClassifier models"""
 
     if (model.n_layers_ < 3):
@@ -265,7 +267,7 @@ def convert_sklearn_mlp(model, method):
     biases = model.intercepts_
     activations = [model.activation]*(len(weights)-1) + [ model.out_activation_ ]
 
-    return Wrapper(activations, weights, biases, classifier=method)
+    return Wrapper(activations, weights, biases, classifier=method, return_type=return_type)
 
 def from_keras_activation(act):
     name = act.__name__


Test patch:
diff --git a/test/test_net.py b/test/test_net.py
index cd43484..bcd5271 100644
--- a/test/test_net.py
+++ b/test/test_net.py
@@ -11,7 +11,7 @@
 import warnings
 warnings.filterwarnings(action='ignore', category=sklearn.exceptions.ConvergenceWarning)
 
-from sklearn.neural_network import MLPClassifier
+from sklearn.neural_network import MLPClassifier, MLPRegressor
 from sklearn.datasets import make_classification
 from sklearn.datasets import make_regression
 from sklearn.model_selection import train_test_split, cross_val_score
@@ -79,6 +79,34 @@ def test_sklearn_predict(modelparams,params):
 
 
 
+@pytest.mark.parametrize('modelparams,params', SKLEARN_PARAMS)
+def test_sklearn_regress(modelparams,params):
+
+    model = MLPRegressor(**modelparams, max_iter=10)
+
+    for random in range(0, 3):
+        # create dataset
+        rng = numpy.random.RandomState(0)
+        X, y = make_regression(n_features=params['features'], 
+                               n_informative=params['features'],
+                               random_state=rng, n_samples=50)
+        X += 2 * rng.uniform(size=X.shape)
+        X = StandardScaler().fit_transform(X)
+        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.2)
+
+        # train model
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            model.fit(X_train, y_train)
+
+        # check equivalence
+        cmodel = emlearn.convert(model)
+        X_test = X_test[:3]
+        assert_equivalent_float(model, X_test, method='loadable')
+        #assert_equivalent_float(model, X_test, method='inline') # not supported at the moment
+
+
+
 def keras_mlp_multiclass_activation_layers(features, classes, activation='relu'):
     model = Sequential([
         Dense(8, input_dim=features),