Repository: sktime/sktime

Issue:
[BUG] `LSTMFCNClassifier` `load_model` error: object has no attribute 'model_'
**Describe the bug**
error : 'LSTMFCNClassifier' object has no attribute 'model_'


**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
-->

```python
from sktime.classification.deep_learning.lstmfcn import LSTMFCNClassifier
from sktime.utils import mlflow_sktime  
from sktime.datasets import load_italy_power_demand
X_train, y_train = load_italy_power_demand(split="train", return_type="numpy3D")
X_new, _ = load_italy_power_demand(split="test", return_type="numpy3D")
classifier=LSTMFCNClassifier(verbose=2,n_epochs=5)
classifier.fit(X_train,y_train)
mlflow_sktime.save_model(sktime_model=classifier,path='classifier')
classifier=mlflow_sktime.load_model(model_uri='classifier') 
classifier.predict(X_new)
```



**Versions**
sktime 0.22.0

<!-- Thanks for contributing! -->



Discussion:
@liudexiang3218 Thanks for bringing it to our notice. I'll get this fixed soon. However, if you're not restricted to only use `mlflow`, I recommend you to use the sktime-native saving method:

``` python
from sktime.classification.deep_learning.lstmfcn import LSTMFCNClassifier
from sktime.datasets import load_italy_power_demand
from sktime.base import load

X_train, y_train = load_italy_power_demand(split="train", return_type="numpy3D")
X_new, _ = load_italy_power_demand(split="test", return_type="numpy3D")

classifier=LSTMFCNClassifier(verbose=2,n_epochs=5)
classifier.fit(X_train,y_train)

classifier.save("estimator")
new_classifier = load("estimator")

new_classifier.predict(X_new)
```

Resolving patch:
diff --git a/sktime/utils/mlflow_sktime.py b/sktime/utils/mlflow_sktime.py
--- a/sktime/utils/mlflow_sktime.py
+++ b/sktime/utils/mlflow_sktime.py
@@ -28,12 +28,12 @@
     `{"predict_method": {"predict": {}, "predict_interval": {"coverage": [0.1, 0.9]}}`.
     `Dict[str, list]`, with default parameters in predict method, for example
     `{"predict_method": ["predict", "predict_interval"}` (Note: when including
-    `predict_proba` method the former approach must be followed as `quantiles`
+    `predict_proba` method the former appraoch must be followed as `quantiles`
     parameter has to be provided by the user). If no prediction config is defined
     `pyfunc.predict()` will return output from sktime `predict()` method.
 """
 
-__author__ = ["benjaminbluhm"]
+__author__ = ["benjaminbluhm", "achieveordie"]
 __all__ = [
     "get_default_pip_requirements",
     "get_default_conda_env",
@@ -44,18 +44,15 @@
 
 import logging
 import os
-import pickle
 
 import pandas as pd
 import yaml
 
 import sktime
 from sktime import utils
+from sktime.base._serialize import load
 from sktime.utils.multiindex import flatten_multiindex
-from sktime.utils.validation._dependencies import (
-    _check_mlflow_dependencies,
-    _check_soft_dependencies,
-)
+from sktime.utils.validation._dependencies import _check_mlflow_dependencies
 
 if _check_mlflow_dependencies(severity="warning"):
     from mlflow import pyfunc
@@ -261,7 +258,7 @@ def save_model(
     if input_example is not None:
         _save_example(mlflow_model, input_example, path)
 
-    model_data_subpath = "model.pkl"
+    model_data_subpath = "model"
     model_data_path = os.path.join(path, model_data_subpath)
     _save_model(
         sktime_model, model_data_path, serialization_format=serialization_format
@@ -526,22 +523,12 @@ def _save_model(model, path, serialization_format):
     from mlflow.exceptions import MlflowException
     from mlflow.protos.databricks_pb2 import INTERNAL_ERROR
 
-    with open(path, "wb") as out:
-        if serialization_format == SERIALIZATION_FORMAT_PICKLE:
-            pickle.dump(model, out)
-        elif serialization_format == SERIALIZATION_FORMAT_CLOUDPICKLE:
-            _check_soft_dependencies("cloudpickle", severity="error")
-            import cloudpickle
-
-            cloudpickle.dump(model, out)
-        else:
-            raise MlflowException(
-                message="Unrecognized serialization format: "
-                "{serialization_format}".format(
-                    serialization_format=serialization_format
-                ),
-                error_code=INTERNAL_ERROR,
-            )
+    if serialization_format not in SUPPORTED_SERIALIZATION_FORMATS:
+        raise MlflowException(
+            message="Unrecognized serialization format: " f"{serialization_format}.",
+            error_code=INTERNAL_ERROR,
+        )
+    model.save(path=path, serialization_format=serialization_format)
 
 
 def _load_model(path, serialization_format):
@@ -562,14 +549,7 @@ def _load_model(path, serialization_format):
             error_code=INVALID_PARAMETER_VALUE,
         )
 
-    with open(path, "rb") as pickled_model:
-        if serialization_format == SERIALIZATION_FORMAT_PICKLE:
-            return pickle.load(pickled_model)
-        elif serialization_format == SERIALIZATION_FORMAT_CLOUDPICKLE:
-            _check_soft_dependencies("cloudpickle", severity="error")
-            import cloudpickle
-
-            return cloudpickle.load(pickled_model)
+    return load(path)
 
 
 def _load_pyfunc(path):


Test patch:
diff --git a/sktime/utils/tests/test_mlflow_sktime_model_export.py b/sktime/utils/tests/test_mlflow_sktime_model_export.py
--- a/sktime/utils/tests/test_mlflow_sktime_model_export.py
+++ b/sktime/utils/tests/test_mlflow_sktime_model_export.py
@@ -1,6 +1,6 @@
 """Tests for mlflow-sktime custom model flavor."""
 
-__author__ = ["benjaminbluhm"]
+__author__ = ["benjaminbluhm", "achieveordie"]
 
 import os
 import sys
@@ -11,7 +11,7 @@
 import pandas as pd
 import pytest
 
-from sktime.datasets import load_airline, load_longley
+from sktime.datasets import load_airline, load_arrow_head, load_longley
 from sktime.forecasting.arima import AutoARIMA
 from sktime.forecasting.naive import NaiveForecaster
 from sktime.split import temporal_train_test_split
@@ -77,6 +77,14 @@ def test_data_longley():
     return y_train, y_test, X_train, X_test
 
 
+@pytest.fixture(scope="module")
+def test_data_arrow_head():
+    """Create sample data for univariate classification."""
+    X_train, y_train = load_arrow_head(split="TRAIN")
+    X_test, y_test = load_arrow_head(split="TEST")
+    return y_train.astype(int), y_test.astype(int), X_train, X_test
+
+
 @pytest.fixture(scope="module")
 def auto_arima_model(test_data_airline):
     """Create instance of fitted auto arima model."""
@@ -85,6 +93,22 @@ def auto_arima_model(test_data_airline):
     )
 
 
+@pytest.mark.skipif(
+    not _check_soft_dependencies("tensorflow", severity="none"),
+    reaons="skip test if required soft dependency is not available.",
+)
+@pytest.fixture(scope="module")
+def cnn_model(test_data_arrow_head):
+    """Create an instance of fitted ResNet Classifier model."""
+    from sktime.classification.deep_learning.cnn import CNNClassifier
+
+    y_train, _, X_train, _ = test_data_arrow_head
+
+    return CNNClassifier(n_epochs=1, n_conv_layers=1, kernel_size=3).fit(
+        X_train, y_train
+    )
+
+
 @pytest.fixture(scope="module")
 def naive_forecaster_model_with_regressor(test_data_longley):
     """Create instance of fitted naive forecaster model."""
@@ -168,6 +192,31 @@ def test_auto_arima_model_pyfunc_output(
     np.testing.assert_array_equal(model_predictions.values, pyfunc_predict.values)
 
 
+@pytest.mark.skipif(
+    not _check_soft_dependencies("mlflow", severity="none"),
+    reason="skip test if required soft dependency not available",
+)
+@pytest.mark.parametrize("serialization_format", ["pickle", "cloudpickle"])
+def test_cnn_model_save_and_load(
+    cnn_model, test_data_arrow_head, model_path, serialization_format
+):
+    """Test saving and loading of DL sktime estimator."""
+    from sktime.utils import mlflow_sktime
+
+    mlflow_sktime.save_model(
+        sktime_model=cnn_model,
+        path=model_path,
+        serialization_format=serialization_format,
+    )
+    loaded_model = mlflow_sktime.load_model(model_uri=model_path)
+
+    _, _, _, X_test = test_data_arrow_head
+
+    np.testing.assert_array_almost_equal(
+        cnn_model.predict(X_test), loaded_model.predict(X_test)
+    )
+
+
 @pytest.mark.skipif(
     not _check_soft_dependencies("mlflow", severity="none"),
     reason="skip test if required soft dependency not available",