Repository: keras-team/keras (python)

Issue:
support for dictionary-type loss functions without explicitly declaring `None`
In previous versions of Keras, when providing a loss function as a dictionary for models with multiple outputs, the compileLoss function would automatically assign a default value for outputs without a defined loss. The relevant code was:
```            for name, yt, yp in zip(output_names, y_true, y_pred):
                if name in loss:
                    if loss[name]:
                        flat_losses.append(get_loss(loss[name], yt, yp))
                    else:
                        flat_losses.append(None)
                else:
                    flat_losses.append(None)
``` 
 However, in the latest version, flat_losses is now simply:
  ``` flat_losses = tree.flatten(loss) ``` 
Is there a way to reintroduce support for dictionary-type loss functions without explicitly declaring None for undefined losses? This change reduces flexibility in using dictionary-based loss definitions.

Pull request description:
Fix `CompileLoss` with undefined `loss` argument
Fix #20243 

Actually, #20243 works as expected in torch and jax backends without this PR. The underlying issue in TF backend has been resolved.

The root cause was that we used `pop` in `_filter_unused_inputs` within `CompileLoss`, which is not allowed in a jitted tf graph. The solution is to avoid modifying the incoming data.

A test has been added to verify the implementation.

Patch:
diff --git a/keras/src/trainers/compile_utils.py b/keras/src/trainers/compile_utils.py
index 3a0ba8ce7..410a782db 100644
--- a/keras/src/trainers/compile_utils.py
+++ b/keras/src/trainers/compile_utils.py
@@ -591,14 +591,21 @@ class CompileLoss(losses_module.Loss):
         filtered_y_pred_keys,
         output_names,
     ):
-        if len(filtered_y_true_keys) > 0:
-            if isinstance(y_true, dict):
-                for k in filtered_y_true_keys:
-                    y_true.pop(k)
+        if len(filtered_y_true_keys) > 0 and isinstance(y_true, dict):
+            # Modifying data in-place can cause errors in TF's graph.
+            filtered_y_true = {}
+            for k, v in y_true.items():
+                if k not in filtered_y_true_keys:
+                    filtered_y_true[k] = v
+            y_true = filtered_y_true
         if len(filtered_y_pred_keys) > 0:
             if isinstance(y_pred, dict):
-                for k in filtered_y_pred_keys:
-                    y_pred.pop(k)
+                # Modifying data in-place can cause errors in TF's graph.
+                filtered_y_pred = {}
+                for k, v in y_pred.items():
+                    if k not in filtered_y_pred_keys:
+                        filtered_y_pred[k] = v
+                y_pred = filtered_y_pred
             elif output_names is not None:
                 y_pred = []
                 for x, output_name in zip(tree.flatten(y_pred), output_names):


Test patch:
diff --git a/keras/src/models/model_test.py b/keras/src/models/model_test.py
index 5a6a57302..0bbcf011d 100644
--- a/keras/src/models/model_test.py
+++ b/keras/src/models/model_test.py
@@ -389,6 +389,49 @@ class ModelTest(testing.TestCase):
         )
         self.assertListEqual(hist_keys, ref_keys)
 
+    def test_functional_dict_outputs_dict_losses_with_undefined_loss(self):
+        model = _get_model_multi_outputs_dict()
+        self.assertIsInstance(model, Functional)
+        x = np.random.rand(8, 3)
+        y1 = np.random.rand(8, 1)
+        y2 = np.random.randint(0, 2, (8, 1))
+        model.compile(
+            optimizer="sgd",
+            loss={
+                "output_b": ["binary_crossentropy"],
+            },
+            metrics={
+                "output_b": ["mean_squared_error", "accuracy"],
+            },
+            weighted_metrics={
+                "output_b": ["mean_squared_error", "accuracy"],
+            },
+        )
+        # Check dict outputs.
+        outputs = model.predict(x)
+        self.assertIsInstance(outputs, dict)
+        self.assertEqual(outputs["output_a"].shape, (8, 1))
+        self.assertEqual(outputs["output_b"].shape, (8, 1))
+        # Fit the model to make sure compile_metrics are built
+        hist = model.fit(
+            x,
+            {"output_a": y1, "output_b": y2},
+            batch_size=2,
+            epochs=1,
+            verbose=0,
+        )
+        hist_keys = sorted(hist.history.keys())
+        ref_keys = sorted(
+            [
+                "loss",
+                "output_b_accuracy",
+                "output_b_mean_squared_error",
+                "output_b_weighted_accuracy",
+                "output_b_weighted_mean_squared_error",
+            ]
+        )
+        self.assertListEqual(hist_keys, ref_keys)
+
     def test_functional_list_outputs_dict_losses_metrics(self):
         model = _get_model_multi_outputs_list()
         self.assertIsInstance(model, Functional)