Repository: keras-team/keras (python)

Issue:
Keras fails to train on custom generator (Torch backend)
Keras model would throw an error on training when using Torch backend and a custom (Python) data generator.

Steps to reproduce:
1) Define a model
2) Define a simple data generator that yields one batch of random data
3) Start training with `epoch=1` and `steps_per_epoch=1`
4) Get an `UnboundLocalError: local variable 'logs' referenced before assignment` suggesting that the training loop itself was skipped, and `epoch_iterator.enumerate_epoch()` has issues wrapping provided generator.

The same steps would work just fine with TF.

[Colab](https://colab.research.google.com/drive/1kOO9W-I2A3wrD3NOuwgH0_MPB-YFs7vr)

Edit: torch==2.3 seems to be incompatible with Keras, but it would have the same fail on 2.2 which should be supported

Pull request description:
Fix `Trainer._symbolic_build`
https://github.com/keras-team/keras/blob/24ffc7cadb0e54a56d77527a0b9e47053dfebc97/keras/src/trainers/trainer.py#L1004-L1010

In `Trainer.fit()`, there's a call to `self._symbolic_build()` before training.
It consumes one extra piece of data which may be unexpected behavior if the amount of data is calculated.
Only the JAX and Torch backends are affected.

Closes #19929

Patch:
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index a7dfc864b..067274a6e 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -56,6 +56,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
+        uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11
         with:
           sarif_file: results.sarif
diff --git a/keras/src/backend/jax/image.py b/keras/src/backend/jax/image.py
index 790f19154..131336292 100644
--- a/keras/src/backend/jax/image.py
+++ b/keras/src/backend/jax/image.py
@@ -188,9 +188,9 @@ def resize(
         else:
             height, width = shape[-2], shape[-1]
         crop_height = int(float(width * target_height) / target_width)
-        crop_height = min(height, crop_height)
+        crop_height = max(min(height, crop_height), 1)
         crop_width = int(float(height * target_width) / target_height)
-        crop_width = min(width, crop_width)
+        crop_width = max(min(width, crop_width), 1)
         crop_box_hstart = int(float(height - crop_height) / 2)
         crop_box_wstart = int(float(width - crop_width) / 2)
         if data_format == "channels_last":
diff --git a/keras/src/backend/numpy/image.py b/keras/src/backend/numpy/image.py
index b4f4dd0c2..69dffda58 100644
--- a/keras/src/backend/numpy/image.py
+++ b/keras/src/backend/numpy/image.py
@@ -184,9 +184,9 @@ def resize(
         else:
             height, width = shape[-2], shape[-1]
         crop_height = int(float(width * target_height) / target_width)
-        crop_height = min(height, crop_height)
+        crop_height = max(min(height, crop_height), 1)
         crop_width = int(float(height * target_width) / target_height)
-        crop_width = min(width, crop_width)
+        crop_width = max(min(width, crop_width), 1)
         crop_box_hstart = int(float(height - crop_height) / 2)
         crop_box_wstart = int(float(width - crop_width) / 2)
         if data_format == "channels_last":
diff --git a/keras/src/backend/tensorflow/image.py b/keras/src/backend/tensorflow/image.py
index 3ebf9e255..7b5f297f9 100644
--- a/keras/src/backend/tensorflow/image.py
+++ b/keras/src/backend/tensorflow/image.py
@@ -151,13 +151,13 @@ def resize(
             tf.cast(width * target_height, "float32") / target_width,
             "int32",
         )
-        crop_height = tf.minimum(height, crop_height)
+        crop_height = tf.maximum(tf.minimum(height, crop_height), 1)
         crop_height = tf.cast(crop_height, "int32")
         crop_width = tf.cast(
             tf.cast(height * target_width, "float32") / target_height,
             "int32",
         )
-        crop_width = tf.minimum(width, crop_width)
+        crop_width = tf.maximum(tf.minimum(width, crop_width), 1)
         crop_width = tf.cast(crop_width, "int32")
 
         crop_box_hstart = tf.cast(
diff --git a/keras/src/backend/torch/image.py b/keras/src/backend/torch/image.py
index 8390eb098..3f5e571aa 100644
--- a/keras/src/backend/torch/image.py
+++ b/keras/src/backend/torch/image.py
@@ -193,9 +193,9 @@ def resize(
         height, width = shape[-2], shape[-1]
         target_height, target_width = size
         crop_height = int(float(width * target_height) / target_width)
-        crop_height = min(height, crop_height)
+        crop_height = max(min(height, crop_height), 1)
         crop_width = int(float(height * target_width) / target_height)
-        crop_width = min(width, crop_width)
+        crop_width = max(min(width, crop_width), 1)
         crop_box_hstart = int(float(height - crop_height) / 2)
         crop_box_wstart = int(float(width - crop_width) / 2)
         if len(images.shape) == 4:
diff --git a/keras/src/trainers/trainer.py b/keras/src/trainers/trainer.py
index 0c7880db7..f47b116a3 100644
--- a/keras/src/trainers/trainer.py
+++ b/keras/src/trainers/trainer.py
@@ -1001,12 +1001,6 @@ class Trainer:
         optimizer_unbuilt = (
             self.optimizer is not None and not self.optimizer.built
         )
-        if model_unbuilt or compile_metrics_unbuilt or optimizer_unbuilt:
-            if data_batch is None:
-                for _, data in iterator.enumerate_epoch():
-                    data_batch = data[0]
-                    break
-
         if model_unbuilt or compile_metrics_unbuilt:
             # Create symbolic tensors matching an input batch.
 
@@ -1017,6 +1011,10 @@ class Trainer:
                     v.shape, backend.standardize_dtype(v.dtype)
                 )
 
+            if data_batch is None:
+                for _, data in iterator.enumerate_epoch():
+                    data_batch = data[0]
+                    break
             data_batch = tree.map_structure(to_symbolic_input, data_batch)
             (
                 x,
diff --git a/keras/src/utils/image_utils.py b/keras/src/utils/image_utils.py
index 70f3e870c..ca8289c9f 100644
--- a/keras/src/utils/image_utils.py
+++ b/keras/src/utils/image_utils.py
@@ -388,9 +388,9 @@ def smart_resize(
     if isinstance(height, int) and isinstance(width, int):
         # For JAX, we need to keep the slice indices as static integers
         crop_height = int(float(width * target_height) / target_width)
-        crop_height = min(height, crop_height)
+        crop_height = max(min(height, crop_height), 1)
         crop_width = int(float(height * target_width) / target_height)
-        crop_width = min(width, crop_width)
+        crop_width = max(min(width, crop_width), 1)
         crop_box_hstart = int(float(height - crop_height) / 2)
         crop_box_wstart = int(float(width - crop_width) / 2)
     else:
@@ -400,13 +400,16 @@ def smart_resize(
             "int32",
         )
         crop_height = backend_module.numpy.minimum(height, crop_height)
+        crop_height = backend_module.numpy.maximum(crop_height, 1)
         crop_height = backend_module.cast(crop_height, "int32")
+
         crop_width = backend_module.cast(
             backend_module.cast(height * target_width, "float32")
             / target_height,
             "int32",
         )
         crop_width = backend_module.numpy.minimum(width, crop_width)
+        crop_width = backend_module.numpy.maximum(crop_width, 1)
         crop_width = backend_module.cast(crop_width, "int32")
 
         crop_box_hstart = backend_module.cast(


Test patch:
diff --git a/keras/src/layers/preprocessing/center_crop_test.py b/keras/src/layers/preprocessing/center_crop_test.py
index 4652f9abe..deaefb2ef 100644
--- a/keras/src/layers/preprocessing/center_crop_test.py
+++ b/keras/src/layers/preprocessing/center_crop_test.py
@@ -194,3 +194,18 @@ class CenterCropTest(testing.TestCase, parameterized.TestCase):
         )
         self.assertEqual(tuple(output.shape), output_shape)
         self.assertAllClose(ref_output, output)
+
+    @parameterized.parameters(
+        [((5, 17), "channels_last"), ((5, 100), "channels_last")]
+    )
+    def test_image_stretch(self, size, data_format):
+        img = np.random.rand(2, 11, 3, 9)
+        out = layers.CenterCrop(
+            size[0],
+            size[1],
+            data_format=data_format,
+        )(img)
+        ref_out = layers.Resizing(
+            size[0], size[1], data_format=data_format, crop_to_aspect_ratio=True
+        )(img)
+        self.assertAllClose(ref_out, out)
diff --git a/keras/src/layers/preprocessing/resizing_test.py b/keras/src/layers/preprocessing/resizing_test.py
index d5b9a718d..e940b43ea 100644
--- a/keras/src/layers/preprocessing/resizing_test.py
+++ b/keras/src/layers/preprocessing/resizing_test.py
@@ -213,3 +213,13 @@ class ResizingTest(testing.TestCase, parameterized.TestCase):
         for output in ds.take(1):
             output = output.numpy()
         self.assertEqual(tuple(output.shape), output_shape)
+
+    @parameterized.parameters(
+        [((15, 10), "channels_last"), ((15, 100), "channels_last")]
+    )
+    def test_data_stretch(self, size, data_format):
+        img = np.random.rand(1, 1, 4, 4)
+        output = layers.Resizing(
+            size[0], size[1], data_format=data_format, crop_to_aspect_ratio=True
+        )(img)
+        self.assertEqual(output.shape, (1, *size, 4))