Repository: keras-team/autokeras

Issue:
Fit function for portable model.
<!---
Please label your issue with `feature_request`.
-->

### Feature Description
<!---
A clear and concise description of idea/feature. If it is related to some problem you are facing, then explain the problem.
-->

### Reason
<!---
A clear and concise description of why this feature would be useful for the project.
-->
Allow futher training and retraining of the exported portable model.

### Solution
<!---
A clear and concise description of what you want to happen.
-->

### Alternative Solutions
<!---
A clear and concise description of any alternative solutions or features you've considered.
-->

### Additional Context
<!---
Add any other context or screenshots about the feature request here.
-->



Discussion:

Resolving patch:
diff --git a/autokeras/image/image_supervised.py b/autokeras/image/image_supervised.py
--- a/autokeras/image/image_supervised.py
+++ b/autokeras/image/image_supervised.py
@@ -1,15 +1,12 @@
 import os
 from abc import ABC
-from functools import reduce
-
 import numpy as np
-import torch
 
 from autokeras.constant import Constant
 from autokeras.nn.loss_function import classification_loss, regression_loss
 from autokeras.nn.metric import Accuracy, MSE
 from autokeras.preprocessor import OneHotEncoder, ImageDataTransformer
-from autokeras.supervised import PortableClass, DeepSupervised
+from autokeras.supervised import PortableDeepSupervised, DeepSupervised
 from autokeras.utils import pickle_to_file, \
     read_csv_file, read_image, compute_image_resize_params, resize_image_data
 
@@ -117,16 +114,6 @@ def init_transformer(self, x):
         if self.data_transformer is None:
             self.data_transformer = ImageDataTransformer(x, augment=self.augment)
 
-    def export_autokeras_model(self, model_file_name):
-        """ Creates and Exports the AutoKeras model to the given filename. """
-        portable_model = PortableImageSupervised(graph=self.cnn.best_model,
-                                                 y_encoder=self.y_encoder,
-                                                 data_transformer=self.data_transformer,
-                                                 metric=self.metric,
-                                                 inverse_transform_y_method=self.inverse_transform_y,
-                                                 resize_params=self.resize_shape)
-        pickle_to_file(portable_model, model_file_name)
-
     def preprocess(self, x):
         return resize_image_data(x, self.resize_shape)
 
@@ -142,6 +129,10 @@ class ImageClassifier(ImageSupervised):
     def loss(self):
         return classification_loss
 
+    @property
+    def metric(self):
+        return Accuracy
+
     def transform_y(self, y_train):
         # Transform y_train.
         if self.y_encoder is None:
@@ -156,9 +147,14 @@ def inverse_transform_y(self, output):
     def get_n_output_node(self):
         return self.y_encoder.n_classes
 
-    @property
-    def metric(self):
-        return Accuracy
+    def export_autokeras_model(self, model_file_name):
+        """ Creates and Exports the AutoKeras model to the given filename. """
+        portable_model = PortableImageClassifier(graph=self.cnn.best_model,
+                                                 y_encoder=self.y_encoder,
+                                                 data_transformer=self.data_transformer,
+                                                 resize_params=self.resize_shape,
+                                                 path=self.path)
+        pickle_to_file(portable_model, model_file_name)
 
 
 class ImageClassifier1D(ImageClassifier):
@@ -209,6 +205,15 @@ def transform_y(self, y_train):
     def inverse_transform_y(self, output):
         return output.flatten()
 
+    def export_autokeras_model(self, model_file_name):
+        """ Creates and Exports the AutoKeras model to the given filename. """
+        portable_model = PortableImageRegressor(graph=self.cnn.best_model,
+                                                y_encoder=self.y_encoder,
+                                                data_transformer=self.data_transformer,
+                                                resize_params=self.resize_shape,
+                                                path=self.path)
+        pickle_to_file(portable_model, model_file_name)
+
 
 class ImageRegressor1D(ImageRegressor):
     """ ImageRegressor1D class.
@@ -234,47 +239,46 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
 
 
-class PortableImageSupervised(PortableClass):
-    def __init__(self, graph, data_transformer, y_encoder, metric, inverse_transform_y_method, resize_params):
+class PortableImageSupervised(PortableDeepSupervised):
+    def __init__(self, graph, y_encoder, data_transformer, resize_params, verbose=False, path=None):
         """Initialize the instance.
         Args:
             graph: The graph form of the learned model
         """
-        super().__init__(graph)
-        self.data_transformer = data_transformer
-        self.y_encoder = y_encoder
-        self.metric = metric
-        self.inverse_transform_y_method = inverse_transform_y_method
+        super().__init__(graph, y_encoder, data_transformer, verbose, path)
         self.resize_shape = resize_params
 
-    def predict(self, x_test):
-        """Return predict results for the testing data.
+    def preprocess(self, x):
+        return resize_image_data(x, self.resize_shape)
 
-        Args:
-            x_test: An instance of numpy.ndarray containing the testing data.
 
-        Returns:
-            A numpy.ndarray containing the results.
-        """
-        if Constant.LIMIT_MEMORY:
-            pass
+class PortableImageClassifier(PortableImageSupervised):
+    @property
+    def loss(self):
+        return classification_loss
 
-        x_test = resize_image_data(x_test, self.resize_shape)
-        test_loader = self.data_transformer.transform_test(x_test)
-        model = self.graph.produce_model()
-        model.eval()
+    @property
+    def metric(self):
+        return Accuracy
 
-        outputs = []
-        with torch.no_grad():
-            for index, inputs in enumerate(test_loader):
-                outputs.append(model(inputs).numpy())
-        output = reduce(lambda x, y: np.concatenate((x, y)), outputs)
-        return self.inverse_transform_y(output)
+    def transform_y(self, y_train):
+        return self.y_encoder.transform(y_train)
 
     def inverse_transform_y(self, output):
-        return self.inverse_transform_y_method(output)
+        return self.y_encoder.inverse_transform(output)
+
+
+class PortableImageRegressor(PortableImageSupervised):
+    @property
+    def loss(self):
+        return regression_loss
+
+    @property
+    def metric(self):
+        return MSE
+
+    def transform_y(self, y_train):
+        return y_train.flatten().reshape(len(y_train), 1)
 
-    def evaluate(self, x_test, y_test):
-        """Return the accuracy score between predict value and `y_test`."""
-        y_predict = self.predict(x_test)
-        return self.metric().evaluate(y_test, y_predict)
+    def inverse_transform_y(self, output):
+        return output.flatten()
diff --git a/autokeras/supervised.py b/autokeras/supervised.py
--- a/autokeras/supervised.py
+++ b/autokeras/supervised.py
@@ -1,10 +1,13 @@
 import os
 from abc import ABC, abstractmethod
 from sklearn.model_selection import train_test_split
+import torch
+import numpy as np
+from functools import reduce
 
 from autokeras.constant import Constant
 from autokeras.net_module import CnnModule
-from autokeras.search import BayesianSearcher
+from autokeras.search import BayesianSearcher, train
 from autokeras.utils import rand_temp_folder_generator, pickle_from_file, validate_xy, pickle_to_file, ensure_dir
 
 
@@ -207,16 +210,98 @@ def evaluate(self, x_test, y_test):
 
 
 class PortableClass(ABC):
-    def __init__(self, graph):
+    def __init__(self, graph, verbose=False):
+        self.graph = graph
+        self.verbose = verbose
+
+    @abstractmethod
+    def fit(self, **kwargs):
+        """further training of the model (graph).
+        """
+        pass
+
+    @abstractmethod
+    def predict(self, x_test):
+        """Return predict results for the testing data.
+
+        Args:
+            x_test: An instance of numpy.ndarray containing the testing data.
+
+        Returns:
+            A numpy.ndarray containing the results.
+        """
+        pass
+
+    @abstractmethod
+    def evaluate(self, x_test, y_test):
+        """Return the accuracy score between predict value and `y_test`."""
+        pass
+
+
+class PortableDeepSupervised(PortableClass):
+    def __init__(self, graph, y_encoder, data_transformer, verbose=False, path=None):
         """Initialize the instance.
 
         Args:
             graph: The graph form of the learned model
 
         """
-        self.graph = graph
+        super(PortableDeepSupervised, self).__init__(graph, verbose)
+        self.y_encoder = y_encoder
+        self.data_transformer = data_transformer
+        if path is None:
+            path = rand_temp_folder_generator()
+        self.path = path
+
+    def fit(self, x_train, y_train, x_test, y_test, trainer_args=None, retrain=False):
+        """further training of the model (graph).
+
+        Args:
+            x_train: A numpy.ndarray of training data.
+            y_train: A numpy.ndarray of training targets.
+            x_test: A numpy.ndarray of testing data.
+            y_test: A numpy.ndarray of testing targets.
+            trainer_args: A dictionary containing the parameters of the ModelTrainer constructor.
+            retrain: A boolean of whether reinitialize the weights of the model.
+        """
+        x_train = self.preprocess(x_train)
+        x_test = self.preprocess(x_test)
+        if trainer_args is None:
+            trainer_args = {'max_no_improvement_num': 30}
+
+        y_train = self.transform_y(y_train)
+        y_test = self.transform_y(y_test)
+
+        train_data = self.data_transformer.transform_train(x_train, y_train)
+        test_data = self.data_transformer.transform_test(x_test, y_test)
+
+        if retrain:
+            self.graph.weighted = False
+        _, _1, self.graph = train(None, self.graph, train_data, test_data, trainer_args,
+                                  self.metric, self.loss, self.verbose, self.path)
+
+    @property
+    @abstractmethod
+    def metric(self):
+        pass
+
+    @property
+    @abstractmethod
+    def loss(self):
+        pass
 
     @abstractmethod
+    def preprocess(self, x):
+        pass
+
+    @abstractmethod
+    def transform_y(self, y):
+        pass
+
+    @abstractmethod
+    def inverse_transform_y(self, output):
+        pass
+
     def predict(self, x_test):
         """Return predict results for the testing data.
 
@@ -226,9 +311,22 @@ def predict(self, x_test):
         Returns:
             A numpy.ndarray containing the results.
         """
-        pass
+        if Constant.LIMIT_MEMORY:
+            pass
+
+        x_test = self.preprocess(x_test)
+        test_loader = self.data_transformer.transform_test(x_test)
+        model = self.graph.produce_model()
+        model.eval()
+
+        outputs = []
+        with torch.no_grad():
+            for index, inputs in enumerate(test_loader):
+                outputs.append(model(inputs).numpy())
+        output = reduce(lambda x, y: np.concatenate((x, y)), outputs)
+        return self.inverse_transform_y(output)
 
-    @abstractmethod
     def evaluate(self, x_test, y_test):
         """Return the accuracy score between predict value and `y_test`."""
-        pass
+        y_predict = self.predict(x_test)
+        return self.metric().evaluate(y_test, y_predict)


Test patch:
diff --git a/tests/image/test_image_supervised.py b/tests/image/test_image_supervised.py
--- a/tests/image/test_image_supervised.py
+++ b/tests/image/test_image_supervised.py
@@ -215,6 +215,9 @@ def test_export_keras_model(_, _1):
     assert len(results) == len(test_x)
     score = model.evaluate(train_x, train_y)
     assert score <= 1.0
+    before = model.graph
+    model.fit(train_x, train_y, train_x, train_y)
+    assert model.graph == before
     clean_dir(TEST_TEMP_DIR)
 
     clf = ImageRegressor(path=TEST_TEMP_DIR, verbose=False, resume=False)