Repository: keras-team/autokeras

Issue:
Check before serialize or deserialize an attribute of a block
In the following code, we directly deserialized the value using hyperparameters.deserialize.
https://github.com/keras-team/autokeras/blob/490436db8b667b32bafdf5eeadca24aa003fb524/autokeras/blocks/basic.py#L193

However, it might be a raw type like int, float.
Should have a mechanism to only call hyperparameters.deserialize or serialize when it is a HyperParameter type.

Then we can rollback https://github.com/keras-team/keras-tuner/pull/594
which is a tech debt.



Discussion:

Resolving patch:
diff --git a/autokeras/blocks/basic.py b/autokeras/blocks/basic.py
--- a/autokeras/blocks/basic.py
+++ b/autokeras/blocks/basic.py
@@ -25,6 +25,7 @@
 from autokeras import keras_layers
 from autokeras.blocks import reduction
 from autokeras.engine import block as block_module
+from autokeras.utils import io_utils
 from autokeras.utils import layer_utils
 from autokeras.utils import utils
 
@@ -103,19 +104,19 @@ def get_config(self):
         config = super().get_config()
         config.update(
             {
-                "num_layers": hyperparameters.serialize(self.num_layers),
-                "num_units": hyperparameters.serialize(self.num_units),
+                "num_layers": io_utils.serialize_block_arg(self.num_layers),
+                "num_units": io_utils.serialize_block_arg(self.num_units),
                 "use_batchnorm": self.use_batchnorm,
-                "dropout": hyperparameters.serialize(self.dropout),
+                "dropout": io_utils.serialize_block_arg(self.dropout),
             }
         )
         return config
 
     @classmethod
     def from_config(cls, config):
-        config["num_layers"] = hyperparameters.deserialize(config["num_layers"])
-        config["num_units"] = hyperparameters.deserialize(config["num_units"])
-        config["dropout"] = hyperparameters.deserialize(config["dropout"])
+        config["num_layers"] = io_utils.deserialize_block_arg(config["num_layers"])
+        config["num_units"] = io_utils.deserialize_block_arg(config["num_units"])
+        config["dropout"] = io_utils.deserialize_block_arg(config["dropout"])
         return cls(**config)
 
     def build(self, hp, inputs=None):
@@ -190,20 +191,20 @@ def get_config(self):
         config.update(
             {
                 "return_sequences": self.return_sequences,
-                "bidirectional": hyperparameters.serialize(self.bidirectional),
-                "num_layers": hyperparameters.serialize(self.num_layers),
-                "layer_type": hyperparameters.serialize(self.layer_type),
+                "bidirectional": io_utils.serialize_block_arg(self.bidirectional),
+                "num_layers": io_utils.serialize_block_arg(self.num_layers),
+                "layer_type": io_utils.serialize_block_arg(self.layer_type),
             }
         )
         return config
 
     @classmethod
     def from_config(cls, config):
-        config["bidirectional"] = hyperparameters.deserialize(
+        config["bidirectional"] = io_utils.deserialize_block_arg(
             config["bidirectional"]
         )
-        config["num_layers"] = hyperparameters.deserialize(config["num_layers"])
-        config["layer_type"] = hyperparameters.deserialize(config["layer_type"])
+        config["num_layers"] = io_utils.deserialize_block_arg(config["num_layers"])
+        config["layer_type"] = io_utils.deserialize_block_arg(config["layer_type"])
         return cls(**config)
 
     def build(self, hp, inputs=None):
@@ -314,24 +315,24 @@ def get_config(self):
         config = super().get_config()
         config.update(
             {
-                "kernel_size": hyperparameters.serialize(self.kernel_size),
-                "num_blocks": hyperparameters.serialize(self.num_blocks),
-                "num_layers": hyperparameters.serialize(self.num_layers),
-                "filters": hyperparameters.serialize(self.filters),
+                "kernel_size": io_utils.serialize_block_arg(self.kernel_size),
+                "num_blocks": io_utils.serialize_block_arg(self.num_blocks),
+                "num_layers": io_utils.serialize_block_arg(self.num_layers),
+                "filters": io_utils.serialize_block_arg(self.filters),
                 "max_pooling": self.max_pooling,
                 "separable": self.separable,
-                "dropout": hyperparameters.serialize(self.dropout),
+                "dropout": io_utils.serialize_block_arg(self.dropout),
             }
         )
         return config
 
     @classmethod
     def from_config(cls, config):
-        config["kernel_size"] = hyperparameters.deserialize(config["kernel_size"])
-        config["num_blocks"] = hyperparameters.deserialize(config["num_blocks"])
-        config["num_layers"] = hyperparameters.deserialize(config["num_layers"])
-        config["filters"] = hyperparameters.deserialize(config["filters"])
-        config["dropout"] = hyperparameters.deserialize(config["dropout"])
+        config["kernel_size"] = io_utils.deserialize_block_arg(config["kernel_size"])
+        config["num_blocks"] = io_utils.deserialize_block_arg(config["num_blocks"])
+        config["num_layers"] = io_utils.deserialize_block_arg(config["num_layers"])
+        config["filters"] = io_utils.deserialize_block_arg(config["filters"])
+        config["dropout"] = io_utils.deserialize_block_arg(config["dropout"])
         return cls(**config)
 
     def build(self, hp, inputs=None):
@@ -560,24 +561,24 @@ def get_config(self):
         config.update(
             {
                 "max_features": self.max_features,
-                "pretraining": hyperparameters.serialize(self.pretraining),
-                "embedding_dim": hyperparameters.serialize(self.embedding_dim),
-                "num_heads": hyperparameters.serialize(self.num_heads),
-                "dense_dim": hyperparameters.serialize(self.dense_dim),
-                "dropout": hyperparameters.serialize(self.dropout),
+                "pretraining": io_utils.serialize_block_arg(self.pretraining),
+                "embedding_dim": io_utils.serialize_block_arg(self.embedding_dim),
+                "num_heads": io_utils.serialize_block_arg(self.num_heads),
+                "dense_dim": io_utils.serialize_block_arg(self.dense_dim),
+                "dropout": io_utils.serialize_block_arg(self.dropout),
             }
         )
         return config
 
     @classmethod
     def from_config(cls, config):
-        config["pretraining"] = hyperparameters.deserialize(config["pretraining"])
-        config["embedding_dim"] = hyperparameters.deserialize(
+        config["pretraining"] = io_utils.deserialize_block_arg(config["pretraining"])
+        config["embedding_dim"] = io_utils.deserialize_block_arg(
             config["embedding_dim"]
         )
-        config["num_heads"] = hyperparameters.deserialize(config["num_heads"])
-        config["dense_dim"] = hyperparameters.deserialize(config["dense_dim"])
-        config["dropout"] = hyperparameters.deserialize(config["dropout"])
+        config["num_heads"] = io_utils.deserialize_block_arg(config["num_heads"])
+        config["dense_dim"] = io_utils.deserialize_block_arg(config["dense_dim"])
+        config["dropout"] = io_utils.deserialize_block_arg(config["dropout"])
         return cls(**config)
 
     def build(self, hp, inputs=None):
@@ -872,18 +873,18 @@ def get_config(self):
         config.update(
             {
                 "max_features": self.max_features,
-                "pretraining": hyperparameters.serialize(self.pretraining),
-                "embedding_dim": hyperparameters.serialize(self.embedding_dim),
-                "dropout": hyperparameters.serialize(self.dropout),
+                "pretraining": io_utils.serialize_block_arg(self.pretraining),
+                "embedding_dim": io_utils.serialize_block_arg(self.embedding_dim),
+                "dropout": io_utils.serialize_block_arg(self.dropout),
             }
         )
         return config
 
     @classmethod
     def from_config(cls, config):
-        config["pretraining"] = hyperparameters.deserialize(config["pretraining"])
-        config["dropout"] = hyperparameters.deserialize(config["dropout"])
-        config["embedding_dim"] = hyperparameters.deserialize(
+        config["pretraining"] = io_utils.deserialize_block_arg(config["pretraining"])
+        config["dropout"] = io_utils.deserialize_block_arg(config["dropout"])
+        config["embedding_dim"] = io_utils.deserialize_block_arg(
             config["embedding_dim"]
         )
         return cls(**config)
@@ -956,7 +957,7 @@ def get_config(self):
         config = super().get_config()
         config.update(
             {
-                "max_sequence_length": hyperparameters.serialize(
+                "max_sequence_length": io_utils.serialize_block_arg(
                     self.max_sequence_length
                 )
             }
@@ -965,7 +966,7 @@ def get_config(self):
 
     @classmethod
     def from_config(cls, config):
-        config["max_sequence_length"] = hyperparameters.deserialize(
+        config["max_sequence_length"] = io_utils.deserialize_block_arg(
             config["max_sequence_length"]
         )
         return cls(**config)
diff --git a/autokeras/blocks/preprocessing.py b/autokeras/blocks/preprocessing.py
--- a/autokeras/blocks/preprocessing.py
+++ b/autokeras/blocks/preprocessing.py
@@ -23,6 +23,7 @@
 from autokeras import analysers
 from autokeras import keras_layers
 from autokeras.engine import block as block_module
+from autokeras.utils import io_utils
 from autokeras.utils import utils
 
 
@@ -272,23 +273,34 @@ def get_config(self):
         config = super().get_config()
         config.update(
             {
-                "translation_factor": hyperparameters.serialize(
+                "translation_factor": io_utils.serialize_block_arg(
                     self.translation_factor
                 ),
                 "horizontal_flip": self.horizontal_flip,
                 "vertical_flip": self.vertical_flip,
-                "rotation_factor": hyperparameters.serialize(self.rotation_factor),
-                "zoom_factor": hyperparameters.serialize(self.zoom_factor),
-                "contrast_factor": hyperparameters.serialize(self.contrast_factor),
+                "rotation_factor": io_utils.serialize_block_arg(
+                    self.rotation_factor
+                ),
+                "zoom_factor": io_utils.serialize_block_arg(self.zoom_factor),
+                "contrast_factor": io_utils.serialize_block_arg(
+                    self.contrast_factor
+                ),
             }
         )
         return config
 
     @classmethod
     def from_config(cls, config):
-        config["rotation_factor"] = hyperparameters.deserialize(
+        config["translation_factor"] = io_utils.deserialize_block_arg(
+            config["translation_factor"]
+        )
+        config["rotation_factor"] = io_utils.deserialize_block_arg(
             config["rotation_factor"]
         )
+        config["zoom_factor"] = io_utils.deserialize_block_arg(config["zoom_factor"])
+        config["contrast_factor"] = io_utils.deserialize_block_arg(
+            config["contrast_factor"]
+        )
         return cls(**config)
 
 
diff --git a/autokeras/utils/io_utils.py b/autokeras/utils/io_utils.py
--- a/autokeras/utils/io_utils.py
+++ b/autokeras/utils/io_utils.py
@@ -20,6 +20,7 @@
 
 import numpy as np
 import tensorflow as tf
+from keras_tuner.engine import hyperparameters
 
 WHITELIST_FORMATS = (".bmp", ".gif", ".jpeg", ".jpg", ".png")
 
@@ -380,3 +381,15 @@ def path_to_image(image, num_channels, image_size, interpolation):
     image = tf.image.resize(image, image_size, method=interpolation)
     image.set_shape((image_size[0], image_size[1], num_channels))
     return image
+
+
+def deserialize_block_arg(arg):
+    if isinstance(arg, dict):
+        return hyperparameters.deserialize(arg)
+    return arg
+
+
+def serialize_block_arg(arg):
+    if isinstance(arg, hyperparameters.HyperParameter):
+        return hyperparameters.serialize(arg)
+    return arg


Test patch:
diff --git a/autokeras/blocks/preprocessing_test.py b/autokeras/blocks/preprocessing_test.py
--- a/autokeras/blocks/preprocessing_test.py
+++ b/autokeras/blocks/preprocessing_test.py
@@ -14,6 +14,7 @@
 
 import keras_tuner
 import tensorflow as tf
+from keras_tuner.engine import hyperparameters
 from tensorflow import keras
 from tensorflow import nest
 
@@ -88,11 +89,18 @@ def test_augment_build_with_contrast_factor_return_tensor():
 
 
 def test_augment_deserialize_to_augment():
-    serialized_block = blocks.serialize(blocks.ImageAugmentation())
+    serialized_block = blocks.serialize(
+        blocks.ImageAugmentation(
+            zoom_factor=0.1,
+            contrast_factor=hyperparameters.Float("contrast_factor", 0.1, 0.5),
+        )
+    )
 
     block = blocks.deserialize(serialized_block)
 
     assert isinstance(block, blocks.ImageAugmentation)
+    assert block.zoom_factor == 0.1
+    assert isinstance(block.contrast_factor, hyperparameters.Float)
 
 
 def test_augment_get_config_has_all_attributes():