Repository: keras-team/keras (Python) — Bug Fix

Issue:
"ValueError: Undefined shapes are not supported." when calling model.call()
hello everybody.

I'm having trouble creating a Siamese network class, which extends keras.Model , from a function that returns the same model. My knowledge about [keras.Model](https://keras.io/api/models/model/) isn't good, so I don't know if it is a bug or my mistake.

This is the function:

```
def siamese_loss_network():
    inputs = keras.layers.Input((128, 128, 3))
    x = keras.applications.efficientnet.preprocess_input(inputs)
    base = keras.applications.EfficientNetB0(include_top=False, input_tensor=inputs, pooling = 'max')
    head = base.output
    x = keras.layers.Dense(256, activation="relu")(head)
    x = keras.layers.Dense(32)(x)
    embedding_network = keras.Model(inputs, x)
    input_1 = keras.layers.Input((128, 128, 3),name="input_layer_base_r")
    input_2 = keras.layers.Input((128, 128, 3),name="input_layer_base_l")
    tower_1 = embedding_network(input_1)
    tower_2 = embedding_network(input_2)
    merge_layer = keras.layers.Lambda(euclidean_distance, output_shape=(1,))(
        [tower_1, tower_2]
    )
    output_layer = keras.layers.Dense(1, activation="sigmoid")(merge_layer)
    siamese = keras.Model(inputs=[input_1, input_2], outputs=output_layer)
    return siamese

def euclidean_distance(vects):
    x, y = vects
    sum_square = ops.sum(ops.square(x - y), axis=1, keepdims=True)
    return ops.sqrt(ops.maximum(sum_square, keras.backend.epsilon()))
```

When running:

``` 
model = siamese_loss_network()
model.compile(optimizer=Adam(), loss=loss())
model.summary()
```


I get the following output:

```
Model: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                  ┃ Output Shape              ┃         Param # ┃ Connected to               ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ input_layer_base_r            │ (None, 128, 128, 3)       │               0 │ -                          │
│ (InputLayer)                  │                           │                 │                            │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ input_layer_base_l            │ (None, 128, 128, 3)       │               0 │ -                          │
│ (InputLayer)                  │                           │                 │                            │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ functional (Functional)       │ (None, 32)                │       4,385,731 │ input_layer_base_r[0][0],  │
│                               │                           │                 │ input_layer_base_l[0][0]   │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ lambda (Lambda)               │ (None, 1)                 │               0 │ functional[0][0],          │
│                               │                           │                 │ functional[1][0]           │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ dense_2 (Dense)               │ (None, 1)                 │               2 │ lambda[0][0]               │
└───────────────────────────────┴───────────────────────────┴─────────────────┴────────────────────────────┘
 Total params: 4,385,733 (16.73 MB)
 Trainable params: 4,343,710 (16.57 MB)
 Non-trainable params: 42,023 (164.16 KB)
```


So here is my adaptation for a class that inherits keras.Model:

```
class SiameseModel(keras.Model):
    def __init__(self): 
        super().__init__()
        self.inputs = keras.layers.Input((128, 128, 3))
        self.input_1 = keras.layers.Input((128, 128, 3),name="input_layer_base_r")
        self.input_2 = keras.layers.Input((128, 128, 3),name="input_layer_base_l")
        self.base = keras.applications.EfficientNetB0(include_top=False, input_tensor=self.inputs, pooling = 'max')
        self.dense_1 = keras.layers.Dense(256, activation="relu")
        self.dense_2 = keras.layers.Dense(32)
        self.merge_layer = keras.layers.Lambda(euclidean_distance, output_shape=(1,))
        self.output_layer = keras.layers.Dense(1, activation="sigmoid")

    def call(self, inputs):
        head = self.base.output
        x = self.dense_1(head)
        x = self.dense_2(x)
        embedding_network = keras.Model(inputs, x)
        tower_1 = embedding_network(self.input_1)
        tower_2 = embedding_network(self.input_2)
        merge = self.merge_layer([tower_1, tower_2])
        output = self.output_layer(merge)
        return keras.Model(inputs=[self.input_1, self.input_2], outputs=output)
``` 

When running:

``` 
model = SiameseModel()
model.compile(optimizer=Adam(), loss=loss())
model.summary()
```

i got the error: 

```
Traceback (most recent call last):
  File "D:[project path]\main.py", line 20, in <module>
    model.summary()
  File "C:[user path]\.conda\envs\[env path]\lib\site-packages\keras\src\utils\traceback_utils.py", line 122, in error_handler
    raise e.with_traceback(filtered_tb) from None
  File "C:\[user path]\.conda\envs\[env path]\lib\site-packages\optree\ops.py", line 594, in tree_map
    return treespec.unflatten(map(func, *flat_args))
ValueError: Undefined shapes are not supported.
``` 


I read [this other issue](https://github.com/keras-team/keras/issues/19482), but i honestly didn't understand the reason for the error, nor how to resolve it. Could anyone enlighten me about this?

Python version: Python 3.10.13
pip version: 24.0
Tensorflow version: 2.16.1
Keras version: Version: 3.4.1

Grateful for the attention and hard work!




Discussion:
Got the same Error,
![image](https://github.com/user-attachments/assets/a4a406d1-4426-4427-9423-c235e8afb9d8)
![image](https://github.com/user-attachments/assets/7b71b264-ed57-4be9-a55c-d5267c33e639)

Hey @jpeg-souza 
you can try the following:

```python
import keras
from keras import ops


def euclidean_distance(vects):
    x, y = vects
    sum_square = ops.sum(ops.square(x - y), axis=1, keepdims=True)
    return ops.sqrt(ops.maximum(sum_square, keras.backend.epsilon()))


class SiameseModel(keras.Model):
    def __init__(self):
        self.base = keras.applications.EfficientNetB0(
            include_top=False, input_shape=[128, 128, 3], pooling="max"
        )
        self.dense_1 = keras.layers.Dense(256, activation="relu")
        self.dense_2 = keras.layers.Dense(32)
        self.merge_layer = keras.layers.Lambda(
            euclidean_distance, output_shape=(1,)
        )
        self.output_layer = keras.layers.Dense(1, activation="sigmoid")

        # Build functional model
        input_1 = keras.layers.Input((128, 128, 3), name="input_layer_base_r")
        input_2 = keras.layers.Input((128, 128, 3), name="input_layer_base_l")
        embedding_1 = self.base(input_1)
        embedding_1 = self.dense_1(embedding_1)
        tower_1 = self.dense_2(embedding_1)
        embedding_2 = self.base(input_2)
        embedding_2 = self.dense_1(embedding_2)
        tower_2 = self.dense_2(embedding_2)
        merge = self.merge_layer([tower_1, tower_2])
        output = self.output_layer(merge)
        super().__init__(inputs=[input_1, input_2], outputs=output)


model = SiameseModel()
# model.compile(optimizer=keras.optimizers.Adam())
model.summary()
keras.utils.plot_model(model)

# Sample run
output = model([ops.ones([1, 128, 128, 3]), ops.ones([1, 128, 128, 3])])
print(output.shape)

```

Should give you

```bash
Model: "siamese_model_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                  ┃ Output Shape              ┃         Param # ┃ Connected to               ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ input_layer_base_r            │ (None, 128, 128, 3)       │               0 │ -                          │
│ (InputLayer)                  │                           │                 │                            │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ input_layer_base_l            │ (None, 128, 128, 3)       │               0 │ -                          │
│ (InputLayer)                  │                           │                 │                            │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ efficientnetb0 (Functional)   │ (None, 1280)              │       4,049,571 │ input_layer_base_r[0][0],  │
│                               │                           │                 │ input_layer_base_l[0][0]   │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ dense (Dense)                 │ (None, 256)               │         327,936 │ efficientnetb0[0][0],      │
│                               │                           │                 │ efficientnetb0[1][0]       │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ dense_1 (Dense)               │ (None, 32)                │           8,224 │ dense[0][0], dense[1][0]   │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ lambda (Lambda)               │ (None, 1)                 │               0 │ dense_1[0][0],             │
│                               │                           │                 │ dense_1[1][0]              │
├───────────────────────────────┼───────────────────────────┼─────────────────┼────────────────────────────┤
│ dense_2 (Dense)               │ (None, 1)                 │               2 │ lambda[0][0]               │
└───────────────────────────────┴───────────────────────────┴─────────────────┴────────────────────────────┘
 Total params: 4,385,733 (16.73 MB)
 Trainable params: 4,343,710 (16.57 MB)
 Non-trainable params: 42,023 (164.16 KB)
(1, 1)
```

<img src="https://github.com/user-attachments/assets/88543e15-fb7c-43ae-b108-bd7ff7cb9a61" width="200">


The key concept is to build a functional model in `__init__`, similar to how KerasNLP constructs the LLMs.
https://github.com/keras-team/keras-nlp/blob/master/keras_nlp/src/models/gemma/gemma_backbone.py#L163-L183

Resolving patch:
diff --git a/keras/src/models/sequential.py b/keras/src/models/sequential.py
--- a/keras/src/models/sequential.py
+++ b/keras/src/models/sequential.py
@@ -137,6 +137,12 @@ def _maybe_rebuild(self):
         if isinstance(self._layers[0], InputLayer) and len(self._layers) > 1:
             input_shape = self._layers[0].batch_shape
             self.build(input_shape)
+        elif hasattr(self._layers[0], "input_shape") and len(self._layers) > 1:
+            # We can build the Sequential model if the first layer has the
+            # `input_shape` property. This is most commonly found in Functional
+            # model.
+            input_shape = self._layers[0].input_shape
+            self.build(input_shape)
 
     def _lock_state(self):
         # Unlike other layers, Sequential is mutable after build.
diff --git a/keras/src/utils/summary_utils.py b/keras/src/utils/summary_utils.py
--- a/keras/src/utils/summary_utils.py
+++ b/keras/src/utils/summary_utils.py
@@ -96,12 +96,15 @@ def format_shape(shape):
             )
     else:
         try:
-            outputs = layer.compute_output_shape(**layer._build_shapes_dict)
+            if hasattr(layer, "output_shape"):
+                output_shapes = layer.output_shape
+            else:
+                outputs = layer.compute_output_shape(**layer._build_shapes_dict)
+                output_shapes = tree.map_shape_structure(
+                    lambda x: format_shape(x), outputs
+                )
         except NotImplementedError:
             return "?"
-        output_shapes = tree.map_shape_structure(
-            lambda x: format_shape(x), outputs
-        )
     if len(output_shapes) == 1:
         return output_shapes[0]
     out = str(output_shapes)


Test patch:
diff --git a/keras/src/models/sequential_test.py b/keras/src/models/sequential_test.py
--- a/keras/src/models/sequential_test.py
+++ b/keras/src/models/sequential_test.py
@@ -150,6 +150,58 @@ def test_basic_flow_as_a_submodel(self):
         y = model(x)
         self.assertEqual(y.shape, (2, 3, 4))
 
+    def test_basic_flow_with_functional_model_as_first_layer(self):
+        # Build functional model
+        inputs = Input((16, 16, 3))
+        outputs = layers.Conv2D(4, 3, padding="same")(inputs)
+        functional_model = Model(inputs=inputs, outputs=outputs)
+
+        model = Sequential(
+            [functional_model, layers.Flatten(), layers.Dense(1)]
+        )
+        model.summary()
+        self.assertEqual(len(model.layers), 3)
+        self.assertTrue(model.built)
+        for layer in model.layers:
+            self.assertTrue(layer.built)
+
+        # Test eager call
+        x = np.random.random((1, 16, 16, 3))
+        y = model(x)
+        self.assertEqual(type(model._functional), Functional)
+        self.assertEqual(tuple(y.shape), (1, 1))
+
+        # Test symbolic call
+        x = backend.KerasTensor((1, 16, 16, 3))
+        y = model(x)
+        self.assertEqual(y.shape, (1, 1))
+
+    def test_basic_flow_with_sequential_model_as_first_layer(self):
+        # Build sequential model
+        sequential_model = Sequential(
+            [Input((16, 16, 3)), layers.Conv2D(4, 3, padding="same")]
+        )
+
+        model = Sequential(
+            [sequential_model, layers.Flatten(), layers.Dense(1)]
+        )
+        model.summary()
+        self.assertEqual(len(model.layers), 3)
+        self.assertTrue(model.built)
+        for layer in model.layers:
+            self.assertTrue(layer.built)
+
+        # Test eager call
+        x = np.random.random((1, 16, 16, 3))
+        y = model(x)
+        self.assertEqual(type(model._functional), Functional)
+        self.assertEqual(tuple(y.shape), (1, 1))
+
+        # Test symbolic call
+        x = backend.KerasTensor((1, 16, 16, 3))
+        y = model(x)
+        self.assertEqual(y.shape, (1, 1))
+
     def test_dict_inputs(self):
         class DictLayer(layers.Layer):
             def call(self, inputs):