Repository: keras-team/keras (python)

Issue:
sparse_categorical_crossentropy with ignore_class fails for 4D inputs
Using `ignore_class` with `keras.losses.sparse_categorical_crossentropy` and 4D inputs (Batch x Height x Width x Class) fails with a ValueError indicating wrong shapes.

Minimal example to reproduce:
```
import numpy as np
import tensorflow as tf

y_true = np.zeros((1, 224, 224, 1))
y_true[0, 0, 0, 0] = 255
y_pred = np.ones((1, 224, 224, 21))
tf.keras.losses.sparse_categorical_crossentropy(y_true, y_pred, ignore_class=255)
```
--> "ValueError: Arguments `target` and `output` must have the same shape up until the last dimension: target.shape=(1, 224, 224, 1), output.shape=(1, 224, 224, 224, 21)"

This expand_dims seems to be the culprit:
https://github.com/keras-team/keras/blob/2305fada8889e86463493bb4893b13ee8a8f0573/keras/src/losses/losses.py#L1719

Pull request:
Fix inconsistent behavior of `losses.sparse_categorical_crossentropy`…
… with and without `ignore_class`

All backends remove trailing dimension of size 1 from `target`. 
In `losses`, if `ignore_class` is not None, the preprocessing before calling the backend's `sparse_categorical_crossentropy` ignores this situation.

https://github.com/keras-team/keras/blob/26abe697a8802de40cb2761fc98b843fe1b2d5f6/keras/src/backend/jax/nn.py#L487-L491

Fixes #19825

Patch:
diff --git a/keras/src/backend/jax/trainer.py b/keras/src/backend/jax/trainer.py
index dd8052519..cab833368 100644
--- a/keras/src/backend/jax/trainer.py
+++ b/keras/src/backend/jax/trainer.py
@@ -63,6 +63,7 @@ class JAXTrainer(base_trainer.Trainer):
             y=y,
             y_pred=y_pred,
             sample_weight=sample_weight,
+            training=training,
         )
         if losses:
             self._losses_override.clear()
diff --git a/keras/src/backend/numpy/trainer.py b/keras/src/backend/numpy/trainer.py
index 68eeba340..6d40982be 100644
--- a/keras/src/backend/numpy/trainer.py
+++ b/keras/src/backend/numpy/trainer.py
@@ -28,8 +28,8 @@ class NumpyTrainer(base_trainer.Trainer):
             y_pred = self(x, training=False)
         else:
             y_pred = self(x)
-        loss = self.compute_loss(
-            x=x, y=y, y_pred=y_pred, sample_weight=sample_weight
+        loss = self._compute_loss(
+            x=x, y=y, y_pred=y_pred, sample_weight=sample_weight, training=False
         )
         self._loss_tracker.update_state(
             loss, sample_weight=tree.flatten(x)[0].shape[0]
diff --git a/keras/src/backend/tensorflow/trainer.py b/keras/src/backend/tensorflow/trainer.py
index 0f7498f92..44582760f 100644
--- a/keras/src/backend/tensorflow/trainer.py
+++ b/keras/src/backend/tensorflow/trainer.py
@@ -51,8 +51,12 @@ class TensorFlowTrainer(base_trainer.Trainer):
                 y_pred = self(x, training=True)
             else:
                 y_pred = self(x)
-            loss = self.compute_loss(
-                x=x, y=y, y_pred=y_pred, sample_weight=sample_weight
+            loss = self._compute_loss(
+                x=x,
+                y=y,
+                y_pred=y_pred,
+                sample_weight=sample_weight,
+                training=True,
             )
             self._loss_tracker.update_state(
                 loss, sample_weight=tf.shape(tree.flatten(x)[0])[0]
@@ -78,8 +82,8 @@ class TensorFlowTrainer(base_trainer.Trainer):
             y_pred = self(x, training=False)
         else:
             y_pred = self(x)
-        loss = self.compute_loss(
-            x=x, y=y, y_pred=y_pred, sample_weight=sample_weight
+        loss = self._compute_loss(
+            x=x, y=y, y_pred=y_pred, sample_weight=sample_weight, training=False
         )
         self._loss_tracker.update_state(
             loss, sample_weight=tf.shape(tree.flatten(x)[0])[0]
@@ -601,8 +605,8 @@ class TensorFlowTrainer(base_trainer.Trainer):
         self, y, y_pred, sample_weight=None, regularization_losses=None
     ):
         warnings.warn(
-            "`model.compiled_loss()` is deprecated. "
-            "Instead, use `model.compute_loss(x, y, y_pred, sample_weight)`.",
+            "`model.compiled_loss()` is deprecated. Instead, use "
+            "`model.compute_loss(x, y, y_pred, sample_weight, training)`.",
         )
         return self.compute_loss(
             x=None, y=y, y_pred=y_pred, sample_weight=sample_weight
@@ -610,8 +614,8 @@ class TensorFlowTrainer(base_trainer.Trainer):
 
     def loss(self, y, y_pred, sample_weight=None):
         warnings.warn(
-            "`model.loss` is deprecated. "
-            "Instead, use `model.compute_loss(x, y, y_pred, sample_weight)`.",
+            "`model.loss()` is deprecated. Instead, use "
+            "`model.compute_loss(x, y, y_pred, sample_weight, training)`.",
         )
         return self.compute_loss(
             x=None, y=y, y_pred=y_pred, sample_weight=sample_weight
diff --git a/keras/src/backend/torch/trainer.py b/keras/src/backend/torch/trainer.py
index 1572d3c90..bf0e133d6 100644
--- a/keras/src/backend/torch/trainer.py
+++ b/keras/src/backend/torch/trainer.py
@@ -49,8 +49,8 @@ class TorchTrainer(base_trainer.Trainer):
         # for the weights from the previous train step.
         self.zero_grad()
 
-        loss = self.compute_loss(
-            x=x, y=y, y_pred=y_pred, sample_weight=sample_weight
+        loss = self._compute_loss(
+            x=x, y=y, y_pred=y_pred, sample_weight=sample_weight, training=True
         )
         self._loss_tracker.update_state(
             loss, sample_weight=tree.flatten(x)[0].shape[0]
@@ -85,8 +85,8 @@ class TorchTrainer(base_trainer.Trainer):
             y_pred = self(x, training=False)
         else:
             y_pred = self(x)
-        loss = self.compute_loss(
-            x=x, y=y, y_pred=y_pred, sample_weight=sample_weight
+        loss = self._compute_loss(
+            x=x, y=y, y_pred=y_pred, sample_weight=sample_weight, training=False
         )
         self._loss_tracker.update_state(
             loss, sample_weight=tree.flatten(x)[0].shape[0]
diff --git a/keras/src/initializers/__init__.py b/keras/src/initializers/__init__.py
index af46b7ff9..e7cf6f76e 100644
--- a/keras/src/initializers/__init__.py
+++ b/keras/src/initializers/__init__.py
@@ -49,6 +49,7 @@ ALL_OBJECTS_DICT.update(
         "uniform": RandomUniform,
         "normal": RandomNormal,
         "orthogonal": OrthogonalInitializer,
+        "Orthogonal": OrthogonalInitializer,  # Legacy
         "one": Ones,
         "zero": Zeros,
     }
diff --git a/keras/src/layers/normalization/batch_normalization.py b/keras/src/layers/normalization/batch_normalization.py
index ecbd1a453..efc92cd16 100644
--- a/keras/src/layers/normalization/batch_normalization.py
+++ b/keras/src/layers/normalization/batch_normalization.py
@@ -214,6 +214,16 @@ class BatchNormalization(Layer):
         return input_shape
 
     def call(self, inputs, training=None, mask=None):
+        # Check if the mask has one less dimension than the inputs.
+        if mask is not None:
+            if len(mask.shape) != len(inputs.shape) - 1:
+                # Raise a value error
+                raise ValueError(
+                    "The mask provided should be one dimension less "
+                    "than the inputs. Received: "
+                    f"mask.shape={mask.shape}, inputs.shape={inputs.shape}"
+                )
+
         input_dtype = standardize_dtype(inputs.dtype)
         if input_dtype in ("float16", "bfloat16"):
             # BN is prone to overflowing for float16/bfloat16 inputs, so we opt
diff --git a/keras/src/layers/normalization/unit_normalization.py b/keras/src/layers/normalization/unit_normalization.py
index 1dbf97c74..76eb923b6 100644
--- a/keras/src/layers/normalization/unit_normalization.py
+++ b/keras/src/layers/normalization/unit_normalization.py
@@ -14,7 +14,7 @@ class UnitNormalization(Layer):
 
     >>> data = np.arange(6).reshape(2, 3)
     >>> normalized_data = keras.layers.UnitNormalization()(data)
-    >>> print(np.sum(normalized_data[0, :] ** 2)
+    >>> np.sum(normalized_data[0, :] ** 2)
     1.0
 
     Args:
diff --git a/keras/src/layers/rnn/gru.py b/keras/src/layers/rnn/gru.py
index 8a516d0b4..7372d769b 100644
--- a/keras/src/layers/rnn/gru.py
+++ b/keras/src/layers/rnn/gru.py
@@ -500,6 +500,7 @@ class GRU(RNN):
             trainable=kwargs.get("trainable", True),
             name="gru_cell",
             seed=seed,
+            implementation=kwargs.pop("implementation", 2),
         )
         super().__init__(
             cell,
diff --git a/keras/src/losses/losses.py b/keras/src/losses/losses.py
index 8bb32c21b..db19dc64d 100644
--- a/keras/src/losses/losses.py
+++ b/keras/src/losses/losses.py
@@ -1711,6 +1711,9 @@ def sparse_categorical_crossentropy(
     array([0.0513, 2.303], dtype=float32)
     """
 
+    if len(y_true.shape) == len(y_pred.shape) and y_true.shape[-1] == 1:
+        y_true = ops.squeeze(y_true, axis=-1)
+
     if ignore_class is not None:
         res_shape = ops.shape(y_pred)[:-1]
         valid_mask = ops.not_equal(y_true, ops.cast(ignore_class, y_pred.dtype))
diff --git a/keras/src/ops/core.py b/keras/src/ops/core.py
index 5d7fdf2ff..db91f1e78 100644
--- a/keras/src/ops/core.py
+++ b/keras/src/ops/core.py
@@ -254,7 +254,7 @@ def slice(inputs, start_indices, shape):
     inputs = np.zeros((5, 5))
     start_indices = np.array([3, 3])
     shape = np.array([2, 2])
-    inputs = keras.ops.slice(inputs, start_indices, updates)
+    inputs = keras.ops.slice(inputs, start_indices, shape)
     ```
 
     Args:
diff --git a/keras/src/trainers/trainer.py b/keras/src/trainers/trainer.py
index af5e3a3f7..0c7880db7 100644
--- a/keras/src/trainers/trainer.py
+++ b/keras/src/trainers/trainer.py
@@ -1,3 +1,4 @@
+import inspect
 import platform
 import warnings
 
@@ -25,6 +26,9 @@ class Trainer:
         self.steps_per_execution = 1
         # Can be set by callbacks in on_train_begin
         self._initial_epoch = None
+        self._compute_loss_has_training_arg = (
+            "training" in inspect.signature(self.compute_loss).parameters
+        )
 
     @traceback_utils.filter_traceback
     @tracking.no_automatic_dependency_tracking
@@ -262,6 +266,7 @@ class Trainer:
         y=None,
         y_pred=None,
         sample_weight=None,
+        training=True,
     ):
         """Compute the total loss, validate it, and return it.
 
@@ -276,7 +281,7 @@ class Trainer:
                 super().__init__(*args, **kwargs)
                 self.loss_tracker = metrics.Mean(name='loss')
 
-            def compute_loss(self, x, y, y_pred, sample_weight):
+            def compute_loss(self, x, y, y_pred, sample_weight, training=True):
                 loss = ops.means((y_pred - y) ** 2)
                 loss += ops.sum(self.losses)
                 self.loss_tracker.update_state(loss)
@@ -306,12 +311,15 @@ class Trainer:
             y: Target data.
             y_pred: Predictions returned by the model (output of `model(x)`)
             sample_weight: Sample weights for weighting the loss function.
+            training: Whether we are training or evaluating the model.
 
         Returns:
             The total loss as a scalar tensor, or `None` if no loss results
             (which is the case when called by `Model.test_step`).
         """
-        del x  # The default implementation does not use `x`.
+        # The default implementation does not use `x` or `training`.
+        del x
+        del training
         losses = []
         if self._compile_loss is not None:
             loss = self._compile_loss(y, y_pred, sample_weight)
@@ -331,6 +339,27 @@ class Trainer:
             total_loss = ops.sum(losses)
         return total_loss
 
+    def _compute_loss(
+        self,
+        x=None,
+        y=None,
+        y_pred=None,
+        sample_weight=None,
+        training=True,
+    ):
+        """Backwards compatibility wrapper for `compute_loss`.
+
+        This should be used instead `compute_loss` within `train_step` and
+        `test_step` to support overrides of `compute_loss` that may not have
+        the `training` argument, as this argument was added in Keras 3.3.
+        """
+        if self._compute_loss_has_training_arg:
+            return self.compute_loss(
+                x, y, y_pred, sample_weight, training=training
+            )
+        else:
+            return self.compute_loss(x, y, y_pred, sample_weight)
+
     def stateless_compute_loss(
         self,
         trainable_variables,
@@ -340,6 +369,7 @@ class Trainer:
         y=None,
         y_pred=None,
         sample_weight=None,
+        training=True,
     ):
         var_mapping = list(zip(self.trainable_variables, trainable_variables))
         var_mapping.extend(
@@ -349,11 +379,12 @@ class Trainer:
         with backend.StatelessScope(state_mapping=var_mapping) as scope:
             # Note that this is needed for the regularization loss, which need
             # the latest value of train/non-trainable variables.
-            loss = self.compute_loss(
+            loss = self._compute_loss(
                 x,
                 y,
                 y_pred,
                 sample_weight=sample_weight,
+                training=training,
             )
 
         # Update non trainable vars (may have been updated in compute_loss)
diff --git a/requirements-common.txt b/requirements-common.txt
index 29090e9c9..54e6d45f7 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -19,5 +19,4 @@ optree
 pytest-cov
 packaging
 # for tree_test.py
-parameterized
-dm_tree
\ No newline at end of file
+dm_tree


Test patch:
diff --git a/keras/src/initializers/random_initializers_test.py b/keras/src/initializers/random_initializers_test.py
index b58cf64ba..c0fec3812 100644
--- a/keras/src/initializers/random_initializers_test.py
+++ b/keras/src/initializers/random_initializers_test.py
@@ -147,6 +147,10 @@ class InitializersTest(testing.TestCase):
 
         self.run_class_serialization_test(initializer)
 
+        # Test legacy class_name
+        initializer = initializers.get("Orthogonal")
+        self.assertIsInstance(initializer, initializers.OrthogonalInitializer)
+
     def test_get_method(self):
         obj = initializers.get("glorot_normal")
         self.assertTrue(obj, initializers.GlorotNormal)
diff --git a/keras/src/layers/rnn/gru_test.py b/keras/src/layers/rnn/gru_test.py
index 529220803..d2c4f9656 100644
--- a/keras/src/layers/rnn/gru_test.py
+++ b/keras/src/layers/rnn/gru_test.py
@@ -286,3 +286,26 @@ class GRUTest(testing.TestCase, parameterized.TestCase):
             np.array([[0.11669192, 0.11669192], [0.28380975, 0.28380975]]),
             output,
         )
+
+    def test_legacy_implementation_argument(self):
+        sequence = np.arange(72).reshape((3, 6, 4)).astype("float32")
+        layer = layers.GRU(
+            3,
+            kernel_initializer=initializers.Constant(0.01),
+            recurrent_initializer=initializers.Constant(0.02),
+            bias_initializer=initializers.Constant(0.03),
+        )
+        config = layer.get_config()
+        config["implementation"] = 0  # Add legacy argument
+        layer = layers.GRU.from_config(config)
+        output = layer(sequence)
+        self.assertAllClose(
+            np.array(
+                [
+                    [0.5217289, 0.5217289, 0.5217289],
+                    [0.6371659, 0.6371659, 0.6371659],
+                    [0.39384964, 0.39384964, 0.3938496],
+                ]
+            ),
+            output,
+        )
diff --git a/keras/src/losses/losses_test.py b/keras/src/losses/losses_test.py
index f5475fb9c..b09f4573e 100644
--- a/keras/src/losses/losses_test.py
+++ b/keras/src/losses/losses_test.py
@@ -1055,7 +1055,7 @@ class CategoricalCrossentropyTest(testing.TestCase):
             from_logits=True, reduction=None
         )
         loss = cce_obj(y_true, logits)
-        self.assertAllClose((0.001822, 0.000459, 0.169846), loss, 3)
+        self.assertAllClose((0.001822, 0.000459, 0.169846), loss)
 
     def test_label_smoothing(self):
         logits = np.array([[100.0, -100.0, -100.0]])
@@ -1170,7 +1170,7 @@ class SparseCategoricalCrossentropyTest(testing.TestCase):
             from_logits=True, reduction=None
         )
         loss = cce_obj(y_true, logits)
-        self.assertAllClose((0.001822, 0.000459, 0.169846), loss, 3)
+        self.assertAllClose((0.001822, 0.000459, 0.169846), loss)
 
     def test_ignore_class(self):
         y_true = np.array([[-1, 2]])
@@ -1179,7 +1179,15 @@ class SparseCategoricalCrossentropyTest(testing.TestCase):
             from_logits=True, ignore_class=-1, reduction=None
         )
         loss = cce_obj(y_true, logits)
-        self.assertAllClose([[0.0, 1.48012]], loss, 3)
+        self.assertAllClose([[0.0, 1.480129]], loss)
+
+        y_true = np.array([[[-1], [2]]])
+        logits = np.array([[[0.854, 0.698, 0.598], [0.088, 0.86, 0.018]]])
+        cce_obj = losses.SparseCategoricalCrossentropy(
+            from_logits=True, ignore_class=-1, reduction=None
+        )
+        loss = cce_obj(y_true, logits)
+        self.assertAllClose([[0.0, 1.480129]], loss)
 
 
 class BinaryFocalCrossentropyTest(testing.TestCase):
@@ -1272,7 +1280,7 @@ class BinaryFocalCrossentropyTest(testing.TestCase):
             reduction=None,
         )
         loss = obj(y_true, y_pred)
-        self.assertAllClose(loss, (0.5155, 0.0205), 3)
+        self.assertAllClose(loss, (0.515547, 0.020513))
 
 
 class CategoricalFocalCrossentropyTest(testing.TestCase):
@@ -1358,7 +1366,6 @@ class CategoricalFocalCrossentropyTest(testing.TestCase):
         self.assertAllClose(
             (1.5096224e-09, 2.4136547e-11, 1.0360638e-03),
             loss,
-            3,
         )
 
     def test_label_smoothing(self):
diff --git a/keras/src/trainers/trainer_test.py b/keras/src/trainers/trainer_test.py
index a7ec7b9b5..a4b21e5f5 100644
--- a/keras/src/trainers/trainer_test.py
+++ b/keras/src/trainers/trainer_test.py
@@ -1387,6 +1387,7 @@ class TestTrainer(testing.TestCase, parameterized.TestCase):
 
     @pytest.mark.requires_trainable_backend
     def test_metric_update_in_compute_loss(self):
+        test_self = self
 
         class MyModel(keras.Model):
             def __init__(self):
@@ -1398,9 +1399,17 @@ class TestTrainer(testing.TestCase, parameterized.TestCase):
                 return self.dense(x)
 
             def compute_loss(
-                self, x=None, y=None, y_pred=None, sample_weight=None
+                self,
+                x=None,
+                y=None,
+                y_pred=None,
+                sample_weight=None,
+                training=True,
             ):
-                loss = super().compute_loss(x, y, y_pred, sample_weight)
+                test_self.assertTrue(training)
+                loss = super().compute_loss(
+                    x, y, y_pred, sample_weight, training
+                )
                 self.custom_metric.update_state(loss * 4)
                 return loss
 
@@ -1415,6 +1424,7 @@ class TestTrainer(testing.TestCase, parameterized.TestCase):
 
     @pytest.mark.requires_trainable_backend
     def test_fwd_pass_loss_presence_in_compute_loss(self):
+        test_self = self
 
         class MyModel(keras.Model):
             def __init__(self):
@@ -1426,9 +1436,17 @@ class TestTrainer(testing.TestCase, parameterized.TestCase):
                 return self.dense(x)
 
             def compute_loss(
-                self, x=None, y=None, y_pred=None, sample_weight=None
+                self,
+                x=None,
+                y=None,
+                y_pred=None,
+                sample_weight=None,
+                training=True,
             ):
-                loss = super().compute_loss(x, y, y_pred, sample_weight)
+                test_self.assertTrue(training)
+                loss = super().compute_loss(
+                    x, y, y_pred, sample_weight, training
+                )
                 self.custom_metric.update_state(sum(self.losses))
                 return loss
 
@@ -1439,6 +1457,75 @@ class TestTrainer(testing.TestCase, parameterized.TestCase):
         history = model.fit(x, y)
         self.assertGreater(history.history["custom"][0], 0.0)
 
+    @pytest.mark.requires_trainable_backend
+    def test_evaluate_with_custom_compute_loss(self):
+        test_self = self
+
+        class MyModel(keras.Model):
+            def __init__(self):
+                super().__init__()
+                self.custom_metric = keras.metrics.Mean(name="custom")
+                self.dense = keras.layers.Dense(2, activity_regularizer="l2")
+
+            def call(self, x):
+                return self.dense(x)
+
+            def compute_loss(
+                self,
+                x=None,
+                y=None,
+                y_pred=None,
+                sample_weight=None,
+                training=True,
+            ):
+                test_self.assertFalse(training)
+                loss = super().compute_loss(
+                    x, y, y_pred, sample_weight, training
+                )
+                self.custom_metric.update_state(loss * 4)
+                return loss
+
+        model = MyModel()
+        model.compile(optimizer="sgd", loss="mse")
+        x = np.ones((32, 4))
+        y = np.ones((32, 2)) * 2
+        logs = model.evaluate(x, y, return_dict=True)
+        self.assertAlmostEqual(logs["custom"], logs["loss"] * 4)
+
+    @pytest.mark.requires_trainable_backend
+    def test_compute_loss_no_training_backwards_compatibility(self):
+
+        class MyModel(keras.Model):
+            def __init__(self):
+                super().__init__()
+                self.custom_metric = keras.metrics.Mean(name="custom")
+                self.dense = keras.layers.Dense(2, activity_regularizer="l2")
+
+            def call(self, x):
+                return self.dense(x)
+
+            def compute_loss(
+                self,
+                x=None,
+                y=None,
+                y_pred=None,
+                sample_weight=None,
+            ):
+                loss = super().compute_loss(x, y, y_pred, sample_weight)
+                self.custom_metric.update_state(loss * 4)
+                return loss
+
+        model = MyModel()
+        model.compile(optimizer="sgd", loss="mse")
+        x = np.ones((32, 4))
+        y = np.ones((32, 2)) * 2
+        logs = model.evaluate(x, y, return_dict=True)
+        self.assertAlmostEqual(logs["custom"], logs["loss"] * 4)
+        history = model.fit(x, y)
+        self.assertAlmostEqual(
+            history.history["custom"][0], history.history["loss"][0] * 4
+        )
+
     @pytest.mark.requires_trainable_backend
     def test_loss_weights(self):
         epochs = 3
diff --git a/keras/src/tree/tree_test.py b/keras/src/tree/tree_test.py
index 6859ac4a6..a560d5009 100644
--- a/keras/src/tree/tree_test.py
+++ b/keras/src/tree/tree_test.py
@@ -1,7 +1,7 @@
 import collections
 
 import numpy as np
-import parameterized
+from absl.testing import parameterized
 
 from keras.src import ops
 from keras.src import testing
@@ -13,97 +13,89 @@ STRUCTURE2 = ((("foo1", "foo2"), "foo3"), "foo4", ("foo5", "foo6"))
 STRUCTURE_DIFFERENT_NUM_ELEMENTS = ("spam", "eggs")
 STRUCTURE_DIFFERENT_NESTING = (((1, 2), 3), 4, 5, (6,))
 
+TEST_CASES = []
+if dmtree.available:
+    from keras.src.tree import dmtree_impl
 
-@parameterized.parameterized_class(
-    ("tree_module",),
-    [
-        (dmtree,),
-        (optree,),
-    ],
-)
-class TreeTest(testing.TestCase):
-    tree_impl = None
-    tree_api_name = ""
+    TEST_CASES += [
+        {
+            "testcase_name": "dmtree",
+            "tree_impl": dmtree_impl,
+            "is_optree": False,
+        }
+    ]
+if optree.available:
+    from keras.src.tree import optree_impl
 
-    def setUp(self):
-        super().setUp()
-        if self.tree_module.available:
-            if self.tree_module == dmtree:
-                from keras.src.tree import dmtree_impl as tree_impl
+    TEST_CASES += [
+        {
+            "testcase_name": "optree",
+            "tree_impl": optree_impl,
+            "is_optree": True,
+        },
+    ]
 
-                self.tree_impl = tree_impl
-                self.tree_api_name = "dmtree"
-            elif self.tree_module == optree:
-                from keras.src.tree import optree_impl as tree_impl
 
-                self.tree_impl = tree_impl
-                self.tree_api_name = "optree"
-            else:
-                raise ValueError(f"Unrecognized module {self.tree_module}.")
-        else:
-            self.skipTest(f"Skip since {self.tree_module} is not available.")
+@parameterized.named_parameters(TEST_CASES)
+class TreeTest(testing.TestCase):
 
-    def test_is_nested(self):
-        self.assertFalse(self.tree_impl.is_nested("1234"))
-        self.assertFalse(self.tree_impl.is_nested(b"1234"))
-        self.assertFalse(self.tree_impl.is_nested(bytearray("1234", "ascii")))
-        self.assertTrue(self.tree_impl.is_nested([1, 3, [4, 5]]))
-        self.assertTrue(self.tree_impl.is_nested(((7, 8), (5, 6))))
-        self.assertTrue(self.tree_impl.is_nested([]))
-        self.assertTrue(self.tree_impl.is_nested({"a": 1, "b": 2}))
-        self.assertFalse(self.tree_impl.is_nested(set([1, 2])))
+    def test_is_nested(self, tree_impl, is_optree):
+        self.assertFalse(tree_impl.is_nested("1234"))
+        self.assertFalse(tree_impl.is_nested(b"1234"))
+        self.assertFalse(tree_impl.is_nested(bytearray("1234", "ascii")))
+        self.assertTrue(tree_impl.is_nested([1, 3, [4, 5]]))
+        self.assertTrue(tree_impl.is_nested(((7, 8), (5, 6))))
+        self.assertTrue(tree_impl.is_nested([]))
+        self.assertTrue(tree_impl.is_nested({"a": 1, "b": 2}))
+        self.assertFalse(tree_impl.is_nested(set([1, 2])))
         ones = np.ones([2, 3])
-        self.assertFalse(self.tree_impl.is_nested(ones))
-        self.assertFalse(self.tree_impl.is_nested(np.tanh(ones)))
-        self.assertFalse(self.tree_impl.is_nested(np.ones((4, 5))))
+        self.assertFalse(tree_impl.is_nested(ones))
+        self.assertFalse(tree_impl.is_nested(np.tanh(ones)))
+        self.assertFalse(tree_impl.is_nested(np.ones((4, 5))))
 
-    def test_flatten(self):
+    def test_flatten(self, tree_impl, is_optree):
         structure = ((3, 4), 5, (6, 7, (9, 10), 8))
         flat = ["a", "b", "c", "d", "e", "f", "g", "h"]
 
         self.assertEqual(
-            self.tree_impl.flatten(structure), [3, 4, 5, 6, 7, 9, 10, 8]
+            tree_impl.flatten(structure), [3, 4, 5, 6, 7, 9, 10, 8]
         )
         point = collections.namedtuple("Point", ["x", "y"])
         structure = (point(x=4, y=2), ((point(x=1, y=0),),))
         flat = [4, 2, 1, 0]
-        self.assertEqual(self.tree_impl.flatten(structure), flat)
+        self.assertEqual(tree_impl.flatten(structure), flat)
 
-        self.assertEqual([5], self.tree_impl.flatten(5))
-        self.assertEqual([np.array([5])], self.tree_impl.flatten(np.array([5])))
+        self.assertEqual([5], tree_impl.flatten(5))
+        self.assertEqual([np.array([5])], tree_impl.flatten(np.array([5])))
 
-    def test_flatten_dict_order(self):
+    def test_flatten_dict_order(self, tree_impl, is_optree):
         ordered = collections.OrderedDict(
             [("d", 3), ("b", 1), ("a", 0), ("c", 2)]
         )
         plain = {"d": 3, "b": 1, "a": 0, "c": 2}
-        ordered_flat = self.tree_impl.flatten(ordered)
-        plain_flat = self.tree_impl.flatten(plain)
+        ordered_flat = tree_impl.flatten(ordered)
+        plain_flat = tree_impl.flatten(plain)
         # dmtree does not respect the ordered dict.
-        if self.tree_api_name == "optree":
+        if is_optree:
             self.assertEqual([3, 1, 0, 2], ordered_flat)
         else:
             self.assertEqual([0, 1, 2, 3], ordered_flat)
         self.assertEqual([0, 1, 2, 3], plain_flat)
 
-    def test_map_structure(self):
+    def test_map_structure(self, tree_impl, is_optree):
         assertion_message = (
             "have the same structure"
-            if self.tree_api_name == "optree"
+            if is_optree
             else "have the same nested structure"
         )
-        assertion_type_error = (
-            ValueError if self.tree_api_name == "optree" else TypeError
-        )
+        assertion_type_error = ValueError if is_optree else TypeError
         structure2 = (((7, 8), 9), 10, (11, 12))
-        structure1_plus1 = self.tree_impl.map_structure(
-            lambda x: x + 1, STRUCTURE1
-        )
-        self.tree_impl.assert_same_structure(STRUCTURE1, structure1_plus1)
+        structure1_plus1 = tree_impl.map_structure(lambda x: x + 1, STRUCTURE1)
+        tree_impl.assert_same_structure(STRUCTURE1, structure1_plus1)
         self.assertAllEqual(
-            [2, 3, 4, 5, 6, 7], self.tree_impl.flatten(structure1_plus1)
+            [2, 3, 4, 5, 6, 7], tree_impl.flatten(structure1_plus1)
         )
-        structure1_plus_structure2 = self.tree_impl.map_structure(
+        structure1_plus_structure2 = tree_impl.map_structure(
             lambda x, y: x + y, STRUCTURE1, structure2
         )
         self.assertEqual(
@@ -111,57 +103,49 @@ class TreeTest(testing.TestCase):
             structure1_plus_structure2,
         )
 
-        self.assertEqual(3, self.tree_impl.map_structure(lambda x: x - 1, 4))
+        self.assertEqual(3, tree_impl.map_structure(lambda x: x - 1, 4))
 
-        self.assertEqual(
-            7, self.tree_impl.map_structure(lambda x, y: x + y, 3, 4)
-        )
+        self.assertEqual(7, tree_impl.map_structure(lambda x, y: x + y, 3, 4))
 
         # Empty structures
-        self.assertEqual((), self.tree_impl.map_structure(lambda x: x + 1, ()))
-        self.assertEqual([], self.tree_impl.map_structure(lambda x: x + 1, []))
-        self.assertEqual({}, self.tree_impl.map_structure(lambda x: x + 1, {}))
+        self.assertEqual((), tree_impl.map_structure(lambda x: x + 1, ()))
+        self.assertEqual([], tree_impl.map_structure(lambda x: x + 1, []))
+        self.assertEqual({}, tree_impl.map_structure(lambda x: x + 1, {}))
         empty_nt = collections.namedtuple("empty_nt", "")
         self.assertEqual(
             empty_nt(),
-            self.tree_impl.map_structure(lambda x: x + 1, empty_nt()),
+            tree_impl.map_structure(lambda x: x + 1, empty_nt()),
         )
 
         # This is checking actual equality of types, empty list != empty tuple
-        self.assertNotEqual(
-            (), self.tree_impl.map_structure(lambda x: x + 1, [])
-        )
+        self.assertNotEqual((), tree_impl.map_structure(lambda x: x + 1, []))
 
         with self.assertRaisesRegex(TypeError, "callable"):
-            self.tree_impl.map_structure("bad", structure1_plus1)
+            tree_impl.map_structure("bad", structure1_plus1)
         with self.assertRaisesRegex(ValueError, "at least one structure"):
-            self.tree_impl.map_structure(lambda x: x)
+            tree_impl.map_structure(lambda x: x)
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.map_structure(lambda x, y: None, (3, 4), (3, 4, 5))
+            tree_impl.map_structure(lambda x, y: None, (3, 4), (3, 4, 5))
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.map_structure(lambda x, y: None, 3, (3,))
+            tree_impl.map_structure(lambda x, y: None, 3, (3,))
         with self.assertRaisesRegex(assertion_type_error, assertion_message):
-            self.tree_impl.map_structure(
-                lambda x, y: None, ((3, 4), 5), [(3, 4), 5]
-            )
+            tree_impl.map_structure(lambda x, y: None, ((3, 4), 5), [(3, 4), 5])
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.map_structure(
-                lambda x, y: None, ((3, 4), 5), (3, (4, 5))
-            )
+            tree_impl.map_structure(lambda x, y: None, ((3, 4), 5), (3, (4, 5)))
 
         structure1_list = [[[1, 2], 3], 4, [5, 6]]
         with self.assertRaisesRegex(assertion_type_error, assertion_message):
-            self.tree_impl.map_structure(
+            tree_impl.map_structure(
                 lambda x, y: None, STRUCTURE1, structure1_list
             )
 
-    def test_map_structure_up_to(self):
+    def test_map_structure_up_to(self, tree_impl, is_optree):
         # Named tuples.
         ab_tuple = collections.namedtuple("ab_tuple", "a, b")
         op_tuple = collections.namedtuple("op_tuple", "add, mul")
         inp_val = ab_tuple(a=2, b=3)
         inp_ops = ab_tuple(a=op_tuple(add=1, mul=2), b=op_tuple(add=2, mul=3))
-        out = self.tree_impl.map_structure_up_to(
+        out = tree_impl.map_structure_up_to(
             inp_val,
             lambda val, ops: (val + ops.add) * ops.mul,
             inp_val,
@@ -173,7 +157,7 @@ class TreeTest(testing.TestCase):
         # Lists.
         data_list = [[2, 4, 6, 8], [[1, 3, 5, 7, 9], [3, 5, 7]]]
         name_list = ["evens", ["odds", "primes"]]
-        out = self.tree_impl.map_structure_up_to(
+        out = tree_impl.map_structure_up_to(
             name_list,
             lambda name, sec: "first_{}_{}".format(len(sec), name),
             name_list,
@@ -183,77 +167,75 @@ class TreeTest(testing.TestCase):
             out, ["first_4_evens", ["first_5_odds", "first_3_primes"]]
         )
 
-    def test_assert_same_structure(self):
+    def test_assert_same_structure(self, tree_impl, is_optree):
         assertion_message = (
             "have the same structure"
-            if self.tree_api_name == "optree"
+            if is_optree
             else "have the same nested structure"
         )
-        assertion_type_error = (
-            ValueError if self.tree_api_name == "optree" else TypeError
-        )
-        self.tree_impl.assert_same_structure(
+        assertion_type_error = ValueError if is_optree else TypeError
+        tree_impl.assert_same_structure(
             STRUCTURE1, STRUCTURE2, check_types=False
         )
-        self.tree_impl.assert_same_structure("abc", 1.0, check_types=False)
-        self.tree_impl.assert_same_structure(b"abc", 1.0, check_types=False)
-        self.tree_impl.assert_same_structure("abc", 1.0, check_types=False)
-        self.tree_impl.assert_same_structure(
+        tree_impl.assert_same_structure("abc", 1.0, check_types=False)
+        tree_impl.assert_same_structure(b"abc", 1.0, check_types=False)
+        tree_impl.assert_same_structure("abc", 1.0, check_types=False)
+        tree_impl.assert_same_structure(
             bytearray("abc", "ascii"), 1.0, check_types=False
         )
-        self.tree_impl.assert_same_structure(
+        tree_impl.assert_same_structure(
             "abc", np.array([0, 1]), check_types=False
         )
 
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.assert_same_structure(
+            tree_impl.assert_same_structure(
                 STRUCTURE1, STRUCTURE_DIFFERENT_NUM_ELEMENTS
             )
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.assert_same_structure([0, 1], np.array([0, 1]))
+            tree_impl.assert_same_structure([0, 1], np.array([0, 1]))
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.assert_same_structure(0, [0, 1])
+            tree_impl.assert_same_structure(0, [0, 1])
         with self.assertRaisesRegex(assertion_type_error, assertion_message):
-            self.tree_impl.assert_same_structure((0, 1), [0, 1])
+            tree_impl.assert_same_structure((0, 1), [0, 1])
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.assert_same_structure(
+            tree_impl.assert_same_structure(
                 STRUCTURE1, STRUCTURE_DIFFERENT_NESTING
             )
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.assert_same_structure([[3], 4], [3, [4]])
+            tree_impl.assert_same_structure([[3], 4], [3, [4]])
         with self.assertRaisesRegex(ValueError, assertion_message):
-            self.tree_impl.assert_same_structure({"a": 1}, {"b": 1})
+            tree_impl.assert_same_structure({"a": 1}, {"b": 1})
         structure1_list = [[[1, 2], 3], 4, [5, 6]]
         with self.assertRaisesRegex(assertion_type_error, assertion_message):
-            self.tree_impl.assert_same_structure(STRUCTURE1, structure1_list)
-        self.tree_impl.assert_same_structure(
+            tree_impl.assert_same_structure(STRUCTURE1, structure1_list)
+        tree_impl.assert_same_structure(
             STRUCTURE1, STRUCTURE2, check_types=False
         )
         # dm-tree treat list and tuple only on type mismatch, but optree treat
         # them as structure mismatch.
-        if self.tree_api_name == "optree":
+        if is_optree:
             with self.assertRaisesRegex(
                 assertion_type_error, assertion_message
             ):
-                self.tree_impl.assert_same_structure(
+                tree_impl.assert_same_structure(
                     STRUCTURE1, structure1_list, check_types=False
                 )
         else:
-            self.tree_impl.assert_same_structure(
+            tree_impl.assert_same_structure(
                 STRUCTURE1, structure1_list, check_types=False
             )
 
-    def test_pack_sequence_as(self):
+    def test_pack_sequence_as(self, tree_impl, is_optree):
         structure = {"key3": "", "key1": "", "key2": ""}
         flat_sequence = ["value1", "value2", "value3"]
         self.assertEqual(
-            self.tree_impl.pack_sequence_as(structure, flat_sequence),
+            tree_impl.pack_sequence_as(structure, flat_sequence),
             {"key3": "value3", "key1": "value1", "key2": "value2"},
         )
         structure = (("a", "b"), ("c", "d", "e"), "f")
         flat_sequence = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
         self.assertEqual(
-            self.tree_impl.pack_sequence_as(structure, flat_sequence),
+            tree_impl.pack_sequence_as(structure, flat_sequence),
             ((1.0, 2.0), (3.0, 4.0, 5.0), 6.0),
         )
         structure = {
@@ -262,7 +244,7 @@ class TreeTest(testing.TestCase):
         }
         flat_sequence = ["val2", "val1", 3.0, 1.0, 2.0]
         self.assertEqual(
-            self.tree_impl.pack_sequence_as(structure, flat_sequence),
+            tree_impl.pack_sequence_as(structure, flat_sequence),
             {
                 "key3": {"c": (1.0, 2.0), "a": 3.0},
                 "key1": {"e": "val1", "d": "val2"},
@@ -271,43 +253,41 @@ class TreeTest(testing.TestCase):
         structure = ["a"]
         flat_sequence = [np.array([[1, 2], [3, 4]])]
         self.assertAllClose(
-            self.tree_impl.pack_sequence_as(structure, flat_sequence),
+            tree_impl.pack_sequence_as(structure, flat_sequence),
             [np.array([[1, 2], [3, 4]])],
         )
         structure = ["a"]
         flat_sequence = [ops.ones([2, 2])]
         self.assertAllClose(
-            self.tree_impl.pack_sequence_as(structure, flat_sequence),
+            tree_impl.pack_sequence_as(structure, flat_sequence),
             [ops.ones([2, 2])],
         )
 
         with self.assertRaisesRegex(TypeError, "Attempted to pack value:"):
             structure = ["a"]
             flat_sequence = 1
-            self.tree_impl.pack_sequence_as(structure, flat_sequence)
+            tree_impl.pack_sequence_as(structure, flat_sequence)
         with self.assertRaisesRegex(ValueError, "The target structure is of"):
             structure = "a"
             flat_sequence = [1, 2]
-            self.tree_impl.pack_sequence_as(structure, flat_sequence)
+            tree_impl.pack_sequence_as(structure, flat_sequence)
 
-    def test_lists_to_tuples(self):
+    def test_lists_to_tuples(self, tree_impl, is_optree):
         structure = [1, 2, 3]
-        self.assertEqual(self.tree_impl.lists_to_tuples(structure), (1, 2, 3))
+        self.assertEqual(tree_impl.lists_to_tuples(structure), (1, 2, 3))
         structure = [[1], [2, 3]]
-        self.assertEqual(
-            self.tree_impl.lists_to_tuples(structure), ((1,), (2, 3))
-        )
+        self.assertEqual(tree_impl.lists_to_tuples(structure), ((1,), (2, 3)))
         structure = [[1], [2, [3]]]
         self.assertEqual(
-            self.tree_impl.lists_to_tuples(structure), ((1,), (2, (3,)))
+            tree_impl.lists_to_tuples(structure), ((1,), (2, (3,)))
         )
 
-    def test_traverse(self):
+    def test_traverse(self, tree_impl, is_optree):
         # Lists to tuples
         structure = [(1, 2), [3], {"a": [4]}]
         self.assertEqual(
             ((1, 2), (3,), {"a": (4,)}),
-            self.tree_impl.traverse(
+            tree_impl.traverse(
                 lambda x: tuple(x) if isinstance(x, list) else x,
                 structure,
                 top_down=False,
@@ -321,7 +301,7 @@ class TreeTest(testing.TestCase):
             visited.append(x)
             return "X" if isinstance(x, tuple) and len(x) > 2 else None
 
-        output = self.tree_impl.traverse(visit, structure)
+        output = tree_impl.traverse(visit, structure)
         self.assertEqual([(1, [2]), [3, "X"]], output)
         self.assertEqual(
             [