Repository: keras-team/keras (python)

Issue:
Cloning of Sequential models w. input_tensors argument fails
Cloning a Sequential models layers fails if using the input_tensors argument.

Minimum reproducible example:

```
import keras

model = keras.models.Sequential()
model.add(keras.layers.Input(shape=(7, 3)))
model.add(keras.layers.Conv1D(2, 2, padding="same"))

input_tensor = model.inputs[0]
new_model = keras.models.clone_model(model, input_tensors=input_tensor)
```

Traceback:
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/workspaces/keras/keras/src/models/cloning.py", line 159, in clone_model
    return _clone_sequential_model(
  File "/workspaces/keras/keras/src/models/cloning.py", line 301, in _clone_sequential_model
    if input_tensors:
  File "/workspaces/keras/keras/src/backend/common/keras_tensor.py", line 172, in __bool__
    raise TypeError("A symbolic KerasTensor cannot be used as a boolean.")
TypeError: A symbolic KerasTensor cannot be used as a boolean.
```

Still fails even after manually changing `if input_tensors:` to `if input_tensors is not None:` in keras/src/models/cloning.py:
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/workspaces/keras/keras/src/models/cloning.py", line 159, in clone_model
    return _clone_sequential_model(
  File "/workspaces/keras/keras/src/models/cloning.py", line 313, in _clone_sequential_model
    inputs = Input(tensor=input_tensors, name=input_name)
  File "/workspaces/keras/keras/src/layers/core/input_layer.py", line 154, in Input
    layer = InputLayer(
  File "/workspaces/keras/keras/src/layers/core/input_layer.py", line 44, in __init__
    raise ValueError("You must pass a `shape` argument.")
ValueError: You must pass a `shape` argument.
```

Pull request description:
Fix cloning of Sequential models w. input_tensors argument
Changed `_clone_sequential_model` to work properly when using the `input_tensors` argument. Fixes #20549.

`keras.models.clone_model()` now checks for `input_tensors` in the right way and uses `input_tensors` shape and dtype in the clone as expected.

Also added tests for cloning Functional and Sequential (w./wo. explicit Input layer) models with defined `input_tensors`:
```
keras/src/models/cloning_test.py::CloneModelTest::test_input_tensors_cnn_functional
keras/src/models/cloning_test.py::CloneModelTest::test_input_tensors_cnn_sequential
keras/src/models/cloning_test.py::CloneModelTest::test_input_tensors_cnn_sequential_noinputlayer
```

Patch:
diff --git a/keras/src/models/cloning.py b/keras/src/models/cloning.py
index 018cddd67..23a138746 100644
--- a/keras/src/models/cloning.py
+++ b/keras/src/models/cloning.py
@@ -298,7 +298,7 @@ def _clone_sequential_model(model, clone_function, input_tensors=None):
         input_dtype = None
         input_batch_shape = None
 
-    if input_tensors:
+    if input_tensors is not None:
         if isinstance(input_tensors, (list, tuple)):
             if len(input_tensors) != 1:
                 raise ValueError(
@@ -310,7 +310,12 @@ def _clone_sequential_model(model, clone_function, input_tensors=None):
                 "Argument `input_tensors` must be a KerasTensor. "
                 f"Received invalid value: input_tensors={input_tensors}"
             )
-        inputs = Input(tensor=input_tensors, name=input_name)
+        inputs = Input(
+            tensor=input_tensors,
+            batch_shape=input_tensors.shape,
+            dtype=input_tensors.dtype,
+            name=input_name,
+        )
         new_layers = [inputs] + new_layers
     else:
         if input_batch_shape is not None:


Test patch:
diff --git a/keras/src/models/cloning_test.py b/keras/src/models/cloning_test.py
index 254b53fb3..7a1fd14ad 100644
--- a/keras/src/models/cloning_test.py
+++ b/keras/src/models/cloning_test.py
@@ -61,6 +61,15 @@ def get_sequential_model(explicit_input=True):
     return model
 
 
+def get_cnn_sequential_model(explicit_input=True):
+    model = models.Sequential()
+    if explicit_input:
+        model.add(layers.Input(shape=(7, 3)))
+    model.add(layers.Conv1D(2, 2, padding="same"))
+    model.add(layers.Conv1D(2, 2, padding="same"))
+    return model
+
+
 def get_subclassed_model():
     class ExampleModel(models.Model):
         def __init__(self, **kwargs):
@@ -124,6 +133,23 @@ class CloneModelTest(testing.TestCase):
             if not isinstance(l1, layers.InputLayer):
                 self.assertEqual(l2.name, l1.name + "_custom")
 
+    @parameterized.named_parameters(
+        ("cnn_functional", get_cnn_functional_model),
+        ("cnn_sequential", get_cnn_sequential_model),
+        (
+            "cnn_sequential_noinputlayer",
+            lambda: get_cnn_sequential_model(explicit_input=False),
+        ),
+    )
+    def test_input_tensors(self, model_fn):
+        ref_input = np.random.random((2, 7, 3))
+        model = model_fn()
+        model(ref_input)  # Maybe needed to get model inputs if no Input layer
+        input_tensor = model.inputs[0]
+        new_model = clone_model(model, input_tensors=input_tensor)
+        tree.assert_same_structure(model.inputs, new_model.inputs)
+        tree.assert_same_structure(model.outputs, new_model.outputs)
+
     def test_shared_layers_cloning(self):
         model = get_mlp_functional_model(shared_layers=True)
         new_model = clone_model(model)