Repository: keras-team/keras

Issue:
Fails to deserialise model with TextVectorization layer
Keras fails to deserialise a model containing a `TextVectorization` layer in tf-idf mode (works in int mode). 

## Minimum example
```
def test_text_vectorization_tfidf_mode_serialization():
    """
    Minimal test showing TextVectorization serialization does not work in tf-idf mode
    """
    import tempfile
    import os
    import keras
    import tensorflow

    # Arrange
    texts = ["page 1", "page 2", "page 1 again", "page 2 again", "different text"]
    vectoriser = keras.layers.TextVectorization(
        max_tokens=100,
        output_mode='tf_idf',
    )
    vectoriser.adapt(texts)

    # Act
    test_input = ["page 1"]
    original_output = vectoriser(test_input).numpy()

    # Wrap in Sequential model to save
    model = keras.Sequential([
        keras.layers.Input(shape=(), dtype=tensorflow.string),
        vectoriser
    ])

    # Try to save and then load
    with tempfile.TemporaryDirectory() as temp_dir:
        model_path = os.path.join(temp_dir, "vectoriser_model.keras")
        model.save(model_path)

        loaded_model = keras.models.load_model(model_path)

    # Assert
    loaded_vectoriser = loaded_model.layers[0]
    loaded_output = loaded_vectoriser(test_input).numpy()
    tensorflow.debugging.assert_near(
        original_output,
        loaded_output,
        message="Outputs don't match after serialization"
    )
```

## Error
```
_______________________________ test_text_vectorization_tfidf_mode_serialization ________________________________

    def test_text_vectorization_tfidf_mode_serialization():
        """
        Minimal test showing TextVectorization serialization does not work in tf-idf mode
        """
        import tempfile
        import os
        import keras
        import tensorflow
    
        # Arrange
        texts = ["page 1", "page 2", "page 1 again", "page 2 again", "different text"]
        vectoriser = keras.layers.TextVectorization(
            max_tokens=100,
            output_mode='tf_idf',
        )
        vectoriser.adapt(texts)
    
        # Act
        test_input = ["page 1"]
        original_output = vectoriser(test_input).numpy()
    
        # Wrap in Sequential model to save
        model = keras.Sequential([
            keras.layers.Input(shape=(), dtype=tensorflow.string),
            vectoriser
        ])
    
        # Try to save and then load
        with tempfile.TemporaryDirectory() as temp_dir:
            model_path = os.path.join(temp_dir, "vectoriser_model.keras")
            model.save(model_path)
    
>           loaded_model = keras.models.load_model(model_path)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/unit/.../tensorflow/test_train.py:285: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.11/site-packages/keras/src/saving/saving_api.py:189: in load_model
    return saving_lib.load_model(
.venv/lib/python3.11/site-packages/keras/src/saving/saving_lib.py:365: in load_model
    return _load_model_from_fileobj(
.venv/lib/python3.11/site-packages/keras/src/saving/saving_lib.py:507: in _load_model_from_fileobj
    _raise_loading_failure(error_msgs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

error_msgs = {281470481567120: (<StringLookup name=string_lookup_1, built=False>, TypeError("object of type 'bool' has no len()")),...0682325008: (<TextVectorization name=text_vectorization, built=True>, TypeError("object of type 'bool' has no len()"))}
warn_only = False

    def _raise_loading_failure(error_msgs, warn_only=False):
        first_key = list(error_msgs.keys())[0]
        ex_saveable, ex_error = error_msgs[first_key]
        msg = (
            f"A total of {len(error_msgs)} objects could not "
            "be loaded. Example error message for "
            f"object {ex_saveable}:\n\n"
            f"{ex_error}\n\n"
            "List of objects that could not be loaded:\n"
            f"{[x[0] for x in error_msgs.values()]}"
        )
        if warn_only:
            warnings.warn(msg)
        else:
>           raise ValueError(msg)
E           ValueError: A total of 2 objects could not be loaded. Example error message for object <TextVectorization name=text_vectorization, built=True>:
E           
E           object of type 'bool' has no len()
E           
E           List of objects that could not be loaded:
E           [<TextVectorization name=text_vectorization, built=True>, <StringLookup name=string_lookup_1, built=False>]

.venv/lib/python3.11/site-packages/keras/src/saving/saving_lib.py:645: ValueError
```

## System details
python 3.11.14
keras 3.13.0
tensorflow 2.20.0

Discussion:

Fix (patch):
diff --git a/keras/src/layers/preprocessing/index_lookup.py b/keras/src/layers/preprocessing/index_lookup.py
index 09c9f04e862c..8098634fbc27 100644
--- a/keras/src/layers/preprocessing/index_lookup.py
+++ b/keras/src/layers/preprocessing/index_lookup.py
@@ -824,7 +824,11 @@ def save_own_variables(self, store):
 
     def load_own_variables(self, store):
         if self.output_mode == "tf_idf":
-            self.idf_weights.assign(store["idf_weights"])
+            idf_weights = store["idf_weights"]
+            if hasattr(self, "idf_weights"):
+                self.idf_weights.assign(idf_weights)
+            else:
+                self.idf_weights = tf.Variable(idf_weights, trainable=False)
             self.idf_weights_const = self.idf_weights.value()
 
     def save_assets(self, dir_path):
@@ -852,7 +856,8 @@ def load_assets(self, dir_path):
             else:
                 values = [int(line) for line in lines]
             if self.output_mode == "tf_idf":
-                self.set_vocabulary(values, idf_weights=False)
+                idf_weights = self.idf_weights_const.numpy()
+                self.set_vocabulary(values, idf_weights=idf_weights)
             else:
                 self.set_vocabulary(values)
 


Test changes:
diff --git a/keras/src/layers/preprocessing/text_vectorization_test.py b/keras/src/layers/preprocessing/text_vectorization_test.py
index d523f1a88925..27a763cddc8a 100644
--- a/keras/src/layers/preprocessing/text_vectorization_test.py
+++ b/keras/src/layers/preprocessing/text_vectorization_test.py
@@ -85,6 +85,24 @@ def test_save_load_with_ngrams_flow(self):
         model = saving.load_model(temp_filepath)
         self.assertAllClose(output, model(input_data))
 
+    @pytest.mark.skipif(
+        backend.backend() != "tensorflow", reason="Requires string input dtype"
+    )
+    def test_save_load_tf_idf_mode(self):
+        input_data = np.array(["foo bar", "bar baz", "baz bada boom"])
+        model = Sequential(
+            [
+                layers.Input(dtype="string", shape=()),
+                layers.TextVectorization(max_tokens=100, output_mode="tf_idf"),
+            ]
+        )
+        model.layers[0].adapt(input_data)
+        output = model(input_data)
+        temp_filepath = os.path.join(self.get_temp_dir(), "model.keras")
+        model.save(temp_filepath)
+        loaded_model = saving.load_model(temp_filepath)
+        self.assertAllClose(output, loaded_model(input_data))
+
     def test_tf_data_compatibility(self):
         max_tokens = 5000
         max_len = 4