Repository: keras-team/keras

Issue:
Possibly unintended regression (though maybe intended?)
Hello - have been doing some limits testing on keras3 and exploring possible edge cases and I believe I've found two in the saving/loading flow.

Here's one:

---

### minimal repro

```
import keras

input_one = keras.layers.Input((10, 10))
input_two = keras.layers.Input((10,))

y = keras.layers.Add()([input_one, input_two[..., None]])

result = keras.Model({'input_one': input_one, 'input_two': input_two}, y)

result.save('test.keras')
```
results in:
```
TypeError: Cannot serialize object Ellipsis of type <class 'ellipsis'>. To be serializable, a class must implement the `get_config()` method.
```

---

### is this a bug?

While the model trains regardless of backend, it fails to save and load.  I believe this is unintended from browsing the "Calling TF ops on a Keras Tensor": https://keras.io/guides/migrating_to_keras_3/ - though perhaps it is intended?

A trivial workaround is to just wrap in a lambda:

```
expanded_input_two = keras.layers.Lambda(x: x[..., None])(input_two)
``` 

Perhaps the "workaround" is the intended new flow?

---

if its a bug happy to take a deeper look at a solution


Discussion:
When you do `input_two[..., None]` (where `input_two` is a KerasTensor) you are hitting `keras/src/ops/numpy.py:GetItem.compute_output_spec()`, which supports Ellipsis.

This is why your model can be built and trained. You'd see an error at this line otherwise.

However the framework doesn't know how to serialize Ellipsis. I think we can support it in the general case, by adding a case for it in `serialize_keras_object`/`deserialize_keras_object` (in `keras/src/saving/serialization_lib.py`). Similar to what we do for `__slice__`. If you're able, please open a PR.
Sounds good - I'll give this a shot and see if I can add some tests + add support later today.
Are you satisfied with the resolution of your issue?
<a href="https://docs.google.com/forms/d/e/1FAIpQLSdHag0RVFS7UXzZkKcsFCKOcX8raCupKK9RHSlYxp5U8lSJbQ/viewform?entry.492125872=Yes&entry.243948740=https://github.com/keras-team/keras/issues/19561">Yes</a>
<a href="https://docs.google.com/forms/d/e/1FAIpQLSdHag0RVFS7UXzZkKcsFCKOcX8raCupKK9RHSlYxp5U8lSJbQ/viewform?entry.492125872=No&entry.243948740=https://github.com/keras-team/keras/issues/19561">No</a>




Patch:
diff --git a/keras/src/saving/serialization_lib.py b/keras/src/saving/serialization_lib.py
index 40125572809b..3adc832884ee 100644
--- a/keras/src/saving/serialization_lib.py
+++ b/keras/src/saving/serialization_lib.py
@@ -162,6 +162,11 @@ def serialize_keras_object(obj):
                 "step": serialize_keras_object(obj.step),
             },
         }
+    # Ellipsis is an instance, and ellipsis class is not in global scope.
+    # checking equality also fails elsewhere in the library, so we have
+    # to dynamically get the type.
+    if isinstance(obj, type(Ellipsis)):
+        return {"class_name": "__ellipsis__", "config": {}}
     if isinstance(obj, backend.KerasTensor):
         history = getattr(obj, "_keras_history", None)
         if history:
@@ -613,6 +618,8 @@ class ModifiedMeanSquaredError(keras.losses.MeanSquaredError):
         return np.array(inner_config["value"], dtype=inner_config["dtype"])
     if config["class_name"] == "__bytes__":
         return inner_config["value"].encode("utf-8")
+    if config["class_name"] == "__ellipsis__":
+        return Ellipsis
     if config["class_name"] == "__slice__":
         return slice(
             deserialize_keras_object(


Test patch:
diff --git a/keras/src/saving/serialization_lib_test.py b/keras/src/saving/serialization_lib_test.py
index 06ed6ac7198f..80df36f3eeb9 100644
--- a/keras/src/saving/serialization_lib_test.py
+++ b/keras/src/saving/serialization_lib_test.py
@@ -107,6 +107,10 @@ def tuples_to_lists_str(x):
         reserialized_str = tuples_to_lists_str(reserialized)
         self.assertEqual(serialized_str, reserialized_str)
 
+    def test_serialize_ellipsis(self):
+        _, deserialized, _ = self.roundtrip(Ellipsis)
+        self.assertEqual(..., deserialized)
+
     def test_tensors_and_shapes(self):
         x = ops.random.normal((2, 2), dtype="float64")
         obj = {"x": x}