From 2b65f890de00288acc58e94683f6a7a8fb21ae2b Mon Sep 17 00:00:00 2001 From: Utsab Dahal Date: Sat, 4 Oct 2025 18:00:36 +0545 Subject: [PATCH 1/6] =?UTF-8?q?Simplify=20save=5Fimg:=20remove=20=5Fformat?= =?UTF-8?q?,=20normalize=20jpg=E2=86=92jpeg,=20add=20RGBA=E2=86=92RGB=20ha?= =?UTF-8?q?ndling=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- integration_tests/test_save_img.py | 27 +++++++++++++++++++++++++++ keras/src/utils/image_utils.py | 7 +++++-- 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 integration_tests/test_save_img.py diff --git a/integration_tests/test_save_img.py b/integration_tests/test_save_img.py new file mode 100644 index 000000000000..baec2712bfc2 --- /dev/null +++ b/integration_tests/test_save_img.py @@ -0,0 +1,27 @@ +import os + +import numpy as np +import pytest + +from keras.utils import img_to_array +from keras.utils import load_img +from keras.utils import save_img + + +@pytest.mark.parametrize( + "shape, name", + [ + ((50, 50, 3), "rgb.jpg"), + ((50, 50, 4), "rgba.jpg"), + ], +) +def test_save_jpg(tmp_path, shape, name): + img = np.random.randint(0, 256, size=shape, dtype=np.uint8) + path = tmp_path / name + save_img(path, img, file_format="jpg") + assert os.path.exists(path) + + # Check that the image was saved correctly and converted to RGB if needed. + loaded_img = load_img(path) + loaded_array = img_to_array(loaded_img) + assert loaded_array.shape == (50, 50, 3) \ No newline at end of file diff --git a/keras/src/utils/image_utils.py b/keras/src/utils/image_utils.py index ca8289c9f9b7..a8781a0f46ae 100644 --- a/keras/src/utils/image_utils.py +++ b/keras/src/utils/image_utils.py @@ -175,10 +175,13 @@ def save_img(path, x, data_format=None, file_format=None, scale=True, **kwargs): **kwargs: Additional keyword arguments passed to `PIL.Image.save()`. """ data_format = backend.standardize_data_format(data_format) + # Normalize jpg → jpeg + if file_format is not None and file_format.lower() == "jpg": + file_format = "jpeg" img = array_to_img(x, data_format=data_format, scale=scale) - if img.mode == "RGBA" and (file_format == "jpg" or file_format == "jpeg"): + if img.mode == "RGBA" and file_format == "jpeg": warnings.warn( - "The JPG format does not support RGBA images, converting to RGB." + "The JPEG format does not support RGBA images, converting to RGB." ) img = img.convert("RGB") img.save(path, format=file_format, **kwargs) From deebbc6a38770514caa8553af2bf0f1a7e33bda8 Mon Sep 17 00:00:00 2001 From: Utsab Dahal Date: Sun, 5 Oct 2025 07:23:11 +0545 Subject: [PATCH 2/6] Fix negative index handling in MultiHeadAttention attention_axes --- .gitignore | 1 + ...test_multi_head_attention_negative_axis.py | 28 +++++++++++++++++++ integration_tests/test_save_img.py | 2 +- .../layers/attention/multi_head_attention.py | 18 +++++++++++- keras/src/utils/image_utils.py | 2 +- 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 integration_tests/test_multi_head_attention_negative_axis.py diff --git a/.gitignore b/.gitignore index afd700b49952..416f213f2c82 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__ **/.vscode test/** **/.vscode-smoke/** **/.venv*/ +venv bin/** build/** obj/** diff --git a/integration_tests/test_multi_head_attention_negative_axis.py b/integration_tests/test_multi_head_attention_negative_axis.py new file mode 100644 index 000000000000..64cc1a6c0f92 --- /dev/null +++ b/integration_tests/test_multi_head_attention_negative_axis.py @@ -0,0 +1,28 @@ +import numpy as np + +import keras + + +def test_attention_axes_negative_indexing_matches_positive(): + x = np.random.normal(size=(2, 3, 8, 4)) + + mha_pos = keras.layers.MultiHeadAttention( + num_heads=2, key_dim=4, attention_axes=2 + ) + mha_neg = keras.layers.MultiHeadAttention( + num_heads=2, key_dim=4, attention_axes=-2 + ) + + _ = mha_pos(x, x) + _ = mha_neg(x, x) + + mha_neg.set_weights(mha_pos.get_weights()) + + z_pos, a_pos = mha_pos(x, x, return_attention_scores=True) + z_neg, a_neg = mha_neg(x, x, return_attention_scores=True) + + assert z_pos.shape == z_neg.shape + assert a_pos.shape == a_neg.shape + + np.testing.assert_allclose(z_pos, z_neg, rtol=1e-5, atol=1e-5) + np.testing.assert_allclose(a_pos, a_neg, rtol=1e-5, atol=1e-5) diff --git a/integration_tests/test_save_img.py b/integration_tests/test_save_img.py index baec2712bfc2..6ec7951564cb 100644 --- a/integration_tests/test_save_img.py +++ b/integration_tests/test_save_img.py @@ -24,4 +24,4 @@ def test_save_jpg(tmp_path, shape, name): # Check that the image was saved correctly and converted to RGB if needed. loaded_img = load_img(path) loaded_array = img_to_array(loaded_img) - assert loaded_array.shape == (50, 50, 3) \ No newline at end of file + assert loaded_array.shape == (50, 50, 3) diff --git a/keras/src/layers/attention/multi_head_attention.py b/keras/src/layers/attention/multi_head_attention.py index a8aa86838d5a..e621c9f4b4d0 100644 --- a/keras/src/layers/attention/multi_head_attention.py +++ b/keras/src/layers/attention/multi_head_attention.py @@ -378,7 +378,17 @@ def _build_attention(self, rank): if self._attention_axes is None: self._attention_axes = tuple(range(1, rank - 2)) else: - self._attention_axes = tuple(self._attention_axes) + # Normalize negative indices relative to INPUT rank (rank - 1) + input_rank = rank - 1 + normalized_axes = [] + for ax in self._attention_axes: + if ax < 0: + # Normalize relative to input rank + normalized_ax = input_rank + ax + else: + normalized_ax = ax + normalized_axes.append(normalized_ax) + self._attention_axes = tuple(normalized_axes) ( self._dot_product_equation, self._combine_equation, @@ -760,6 +770,12 @@ def _build_attention_equation(rank, attn_axes): Returns: Einsum equations. """ + # Normalize negative indices to positive indices + if isinstance(attn_axes, (list, tuple)): + attn_axes = tuple(ax % rank if ax < 0 else ax for ax in attn_axes) + else: + attn_axes = (attn_axes % rank if attn_axes < 0 else attn_axes,) + target_notation = "" for i in range(rank): target_notation += _index_to_einsum_variable(i) diff --git a/keras/src/utils/image_utils.py b/keras/src/utils/image_utils.py index a8781a0f46ae..abf5c413fde0 100644 --- a/keras/src/utils/image_utils.py +++ b/keras/src/utils/image_utils.py @@ -179,7 +179,7 @@ def save_img(path, x, data_format=None, file_format=None, scale=True, **kwargs): if file_format is not None and file_format.lower() == "jpg": file_format = "jpeg" img = array_to_img(x, data_format=data_format, scale=scale) - if img.mode == "RGBA" and file_format == "jpeg": + if img.mode == "RGBA" and file_format == "jpeg": warnings.warn( "The JPEG format does not support RGBA images, converting to RGB." ) From 17731f16518466fc8a3dc904f82bf059a802616e Mon Sep 17 00:00:00 2001 From: Utsab Dahal Date: Tue, 7 Oct 2025 09:00:17 +0545 Subject: [PATCH 3/6] Fix negative index handling in MultiHeadAttention attention_axes --- ...test_multi_head_attention_negative_axis.py | 28 ---------------- integration_tests/test_save_img.py | 2 -- .../layers/attention/multi_head_attention.py | 21 +++--------- .../attention/multi_head_attention_test.py | 32 +++++++++++++++++++ keras/src/utils/image_utils.py | 8 ++--- 5 files changed, 38 insertions(+), 53 deletions(-) delete mode 100644 integration_tests/test_multi_head_attention_negative_axis.py diff --git a/integration_tests/test_multi_head_attention_negative_axis.py b/integration_tests/test_multi_head_attention_negative_axis.py deleted file mode 100644 index 64cc1a6c0f92..000000000000 --- a/integration_tests/test_multi_head_attention_negative_axis.py +++ /dev/null @@ -1,28 +0,0 @@ -import numpy as np - -import keras - - -def test_attention_axes_negative_indexing_matches_positive(): - x = np.random.normal(size=(2, 3, 8, 4)) - - mha_pos = keras.layers.MultiHeadAttention( - num_heads=2, key_dim=4, attention_axes=2 - ) - mha_neg = keras.layers.MultiHeadAttention( - num_heads=2, key_dim=4, attention_axes=-2 - ) - - _ = mha_pos(x, x) - _ = mha_neg(x, x) - - mha_neg.set_weights(mha_pos.get_weights()) - - z_pos, a_pos = mha_pos(x, x, return_attention_scores=True) - z_neg, a_neg = mha_neg(x, x, return_attention_scores=True) - - assert z_pos.shape == z_neg.shape - assert a_pos.shape == a_neg.shape - - np.testing.assert_allclose(z_pos, z_neg, rtol=1e-5, atol=1e-5) - np.testing.assert_allclose(a_pos, a_neg, rtol=1e-5, atol=1e-5) diff --git a/integration_tests/test_save_img.py b/integration_tests/test_save_img.py index 6ec7951564cb..e76257f1d64c 100644 --- a/integration_tests/test_save_img.py +++ b/integration_tests/test_save_img.py @@ -1,5 +1,3 @@ -import os - import numpy as np import pytest diff --git a/keras/src/layers/attention/multi_head_attention.py b/keras/src/layers/attention/multi_head_attention.py index e621c9f4b4d0..7bd784f7d8c3 100644 --- a/keras/src/layers/attention/multi_head_attention.py +++ b/keras/src/layers/attention/multi_head_attention.py @@ -378,17 +378,10 @@ def _build_attention(self, rank): if self._attention_axes is None: self._attention_axes = tuple(range(1, rank - 2)) else: - # Normalize negative indices relative to INPUT rank (rank - 1) - input_rank = rank - 1 - normalized_axes = [] - for ax in self._attention_axes: - if ax < 0: - # Normalize relative to input rank - normalized_ax = input_rank + ax - else: - normalized_ax = ax - normalized_axes.append(normalized_ax) - self._attention_axes = tuple(normalized_axes) + self._attention_axes = tuple( + axis if axis >= 0 else (rank - 1) + axis + for axis in self._attention_axes + ) ( self._dot_product_equation, self._combine_equation, @@ -770,12 +763,6 @@ def _build_attention_equation(rank, attn_axes): Returns: Einsum equations. """ - # Normalize negative indices to positive indices - if isinstance(attn_axes, (list, tuple)): - attn_axes = tuple(ax % rank if ax < 0 else ax for ax in attn_axes) - else: - attn_axes = (attn_axes % rank if attn_axes < 0 else attn_axes,) - target_notation = "" for i in range(rank): target_notation += _index_to_einsum_variable(i) diff --git a/keras/src/layers/attention/multi_head_attention_test.py b/keras/src/layers/attention/multi_head_attention_test.py index d74abbd8841c..dda4cd763e40 100644 --- a/keras/src/layers/attention/multi_head_attention_test.py +++ b/keras/src/layers/attention/multi_head_attention_test.py @@ -203,6 +203,38 @@ def test_high_dim_attention( run_training_check=False, ) + + def test_attention_axes_negative_indexing(self): + """Test that negative attention_axes indexing matches positive indexing.""" + x = np.random.normal(size=(2, 3, 8, 4)) + + # Create two layers with equivalent positive and negative indices + mha_pos = layers.MultiHeadAttention( + num_heads=2, key_dim=4, attention_axes=2 + ) + mha_neg = layers.MultiHeadAttention( + num_heads=2, key_dim=4, attention_axes=-2 + ) + + # Initialize both layers + _ = mha_pos(x, x) + _ = mha_neg(x, x) + + # Set same weights for fair comparison + mha_neg.set_weights(mha_pos.get_weights()) + + # Get outputs and attention scores + z_pos, a_pos = mha_pos(x, x, return_attention_scores=True) + z_neg, a_neg = mha_neg(x, x, return_attention_scores=True) + + # Verify shapes match + self.assertEqual(z_pos.shape, z_neg.shape) + self.assertEqual(a_pos.shape, a_neg.shape) + + # Verify outputs are identical + self.assertAllClose(z_pos, z_neg, rtol=1e-5, atol=1e-5) + self.assertAllClose(a_pos, a_neg, rtol=1e-5, atol=1e-5) + @parameterized.named_parameters( ("without_key_same_proj", (4, 8), (2, 8), None, None), ("with_key_same_proj", (4, 8), (2, 8), (2, 3), None), diff --git a/keras/src/utils/image_utils.py b/keras/src/utils/image_utils.py index abf5c413fde0..fd983dbcee2b 100644 --- a/keras/src/utils/image_utils.py +++ b/keras/src/utils/image_utils.py @@ -175,18 +175,14 @@ def save_img(path, x, data_format=None, file_format=None, scale=True, **kwargs): **kwargs: Additional keyword arguments passed to `PIL.Image.save()`. """ data_format = backend.standardize_data_format(data_format) - # Normalize jpg → jpeg - if file_format is not None and file_format.lower() == "jpg": - file_format = "jpeg" img = array_to_img(x, data_format=data_format, scale=scale) - if img.mode == "RGBA" and file_format == "jpeg": + if img.mode == "RGBA" and (file_format == "jpg" or file_format == "jpeg"): warnings.warn( - "The JPEG format does not support RGBA images, converting to RGB." + "The JPG format does not support RGBA images, converting to RGB." ) img = img.convert("RGB") img.save(path, format=file_format, **kwargs) - @keras_export(["keras.utils.load_img", "keras.preprocessing.image.load_img"]) def load_img( path, From 2588cd14031964ec0e7b881af39b9e07629bd549 Mon Sep 17 00:00:00 2001 From: Utsab Dahal Date: Tue, 7 Oct 2025 09:00:17 +0545 Subject: [PATCH 4/6] Fix negative index handling in MultiHeadAttention attention_axes --- keras/src/layers/attention/multi_head_attention.py | 2 +- .../layers/attention/multi_head_attention_test.py | 13 ++++++------- keras/src/utils/image_utils.py | 1 + 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/keras/src/layers/attention/multi_head_attention.py b/keras/src/layers/attention/multi_head_attention.py index 7bd784f7d8c3..4cf70ee2c112 100644 --- a/keras/src/layers/attention/multi_head_attention.py +++ b/keras/src/layers/attention/multi_head_attention.py @@ -379,7 +379,7 @@ def _build_attention(self, rank): self._attention_axes = tuple(range(1, rank - 2)) else: self._attention_axes = tuple( - axis if axis >= 0 else (rank - 1) + axis + axis if axis >= 0 else (rank - 1) + axis for axis in self._attention_axes ) ( diff --git a/keras/src/layers/attention/multi_head_attention_test.py b/keras/src/layers/attention/multi_head_attention_test.py index dda4cd763e40..b70e72d19de3 100644 --- a/keras/src/layers/attention/multi_head_attention_test.py +++ b/keras/src/layers/attention/multi_head_attention_test.py @@ -203,11 +203,10 @@ def test_high_dim_attention( run_training_check=False, ) - def test_attention_axes_negative_indexing(self): """Test that negative attention_axes indexing matches positive indexing.""" x = np.random.normal(size=(2, 3, 8, 4)) - + # Create two layers with equivalent positive and negative indices mha_pos = layers.MultiHeadAttention( num_heads=2, key_dim=4, attention_axes=2 @@ -215,22 +214,22 @@ def test_attention_axes_negative_indexing(self): mha_neg = layers.MultiHeadAttention( num_heads=2, key_dim=4, attention_axes=-2 ) - + # Initialize both layers _ = mha_pos(x, x) _ = mha_neg(x, x) - + # Set same weights for fair comparison mha_neg.set_weights(mha_pos.get_weights()) - + # Get outputs and attention scores z_pos, a_pos = mha_pos(x, x, return_attention_scores=True) z_neg, a_neg = mha_neg(x, x, return_attention_scores=True) - + # Verify shapes match self.assertEqual(z_pos.shape, z_neg.shape) self.assertEqual(a_pos.shape, a_neg.shape) - + # Verify outputs are identical self.assertAllClose(z_pos, z_neg, rtol=1e-5, atol=1e-5) self.assertAllClose(a_pos, a_neg, rtol=1e-5, atol=1e-5) diff --git a/keras/src/utils/image_utils.py b/keras/src/utils/image_utils.py index fd983dbcee2b..ca8289c9f9b7 100644 --- a/keras/src/utils/image_utils.py +++ b/keras/src/utils/image_utils.py @@ -183,6 +183,7 @@ def save_img(path, x, data_format=None, file_format=None, scale=True, **kwargs): img = img.convert("RGB") img.save(path, format=file_format, **kwargs) + @keras_export(["keras.utils.load_img", "keras.preprocessing.image.load_img"]) def load_img( path, From a05c61c75c2b090bf58d36e28aff0bf4bcd3e6ba Mon Sep 17 00:00:00 2001 From: Utsab Dahal Date: Tue, 7 Oct 2025 09:00:17 +0545 Subject: [PATCH 5/6] Fix negative index handling in MultiHeadAttention attention_axes --- integration_tests/test_save_img.py | 25 ------------------- .../attention/multi_head_attention_test.py | 3 ++- 2 files changed, 2 insertions(+), 26 deletions(-) delete mode 100644 integration_tests/test_save_img.py diff --git a/integration_tests/test_save_img.py b/integration_tests/test_save_img.py deleted file mode 100644 index e76257f1d64c..000000000000 --- a/integration_tests/test_save_img.py +++ /dev/null @@ -1,25 +0,0 @@ -import numpy as np -import pytest - -from keras.utils import img_to_array -from keras.utils import load_img -from keras.utils import save_img - - -@pytest.mark.parametrize( - "shape, name", - [ - ((50, 50, 3), "rgb.jpg"), - ((50, 50, 4), "rgba.jpg"), - ], -) -def test_save_jpg(tmp_path, shape, name): - img = np.random.randint(0, 256, size=shape, dtype=np.uint8) - path = tmp_path / name - save_img(path, img, file_format="jpg") - assert os.path.exists(path) - - # Check that the image was saved correctly and converted to RGB if needed. - loaded_img = load_img(path) - loaded_array = img_to_array(loaded_img) - assert loaded_array.shape == (50, 50, 3) diff --git a/keras/src/layers/attention/multi_head_attention_test.py b/keras/src/layers/attention/multi_head_attention_test.py index b70e72d19de3..4d5fb65c0645 100644 --- a/keras/src/layers/attention/multi_head_attention_test.py +++ b/keras/src/layers/attention/multi_head_attention_test.py @@ -204,7 +204,8 @@ def test_high_dim_attention( ) def test_attention_axes_negative_indexing(self): - """Test that negative attention_axes indexing matches positive indexing.""" + """Test that negative attention_axes indexing matches + positive indexing.""" x = np.random.normal(size=(2, 3, 8, 4)) # Create two layers with equivalent positive and negative indices From 23f2a18495aaf05a6804e0a73128b7627f43ca44 Mon Sep 17 00:00:00 2001 From: Utsab Dahal Date: Tue, 7 Oct 2025 09:00:17 +0545 Subject: [PATCH 6/6] Fix negative index handling in MultiHeadAttention attention_axes --- keras/src/layers/attention/multi_head_attention_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/keras/src/layers/attention/multi_head_attention_test.py b/keras/src/layers/attention/multi_head_attention_test.py index 4d5fb65c0645..e284635053cf 100644 --- a/keras/src/layers/attention/multi_head_attention_test.py +++ b/keras/src/layers/attention/multi_head_attention_test.py @@ -204,8 +204,6 @@ def test_high_dim_attention( ) def test_attention_axes_negative_indexing(self): - """Test that negative attention_axes indexing matches - positive indexing.""" x = np.random.normal(size=(2, 3, 8, 4)) # Create two layers with equivalent positive and negative indices