Repository: keras-team/keras (Python) — Bug Fix

Issue:
Setting loss="crossentropy" in the compile method of a model raises an error: 'list' object has no attribute 'shape'
I love the workflow style of Keras so I decide to make some new metric in my own project. I want metrics more general like "accuracy". So when I run some tests like above, I came across that the loss seems not right. When I run the below code snippet:

```python
import os
os.environ["KERAS_BACKEND"] = "tensorflow"

import keras
from keras import ops, layers
from sklearn.datasets import make_classification

x_train, y_train = make_classification(n_samples=1000, n_classes=2)
x_train = x_train.astype("float32")
y_train = y_train.astype("int32")

x_train = ops.convert_to_tensor(x_train)
y_train = ops.convert_to_tensor(y_train)

inputs = layers.Input(shape=(20,))
x = layers.Dense(32, activation="relu")(inputs)
x = layers.Dense(32, activation="relu")(x)
outputs = layers.Dense(2, activation="softmax")(inputs)

model = keras.Model(inputs=inputs, outputs=outputs)

model.compile(loss="crossentropy", optimizer="adam", metrics=["accuracy"])
model.fit(x_train, y_train, epochs=10)
```

I find the more general choice "crossentropy" raises the error as following (I directly click the button "copy output" of vscode jupyter notebook so there may be more info):

```
Epoch 1/10
{
	"name": "AttributeError",
	"message": "'list' object has no attribute 'shape'",
	"stack": "---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[5], line 2
      1 model.compile(loss=\"crossentropy\", optimizer=\"adam\", metrics=[\"accuracy\"])
----> 2 model.fit(x_train, y_train, epochs=10)

File ~/miniconda3/lib/python3.10/site-packages/keras/src/utils/traceback_utils.py:123, in filter_traceback.<locals>.error_handler(*args, **kwargs)
    120     filtered_tb = _process_traceback_frames(e.__traceback__)
    121     # To get the full stack trace, call:
    122     # `keras.config.disable_traceback_filtering()`
--> 123     raise e.with_traceback(filtered_tb) from None
    124 finally:
    125     del filtered_tb

File ~/miniconda3/lib/python3.10/site-packages/keras/src/trainers/compile_utils.py:47, in is_binary_or_sparse_categorical(y_true, y_pred)
     45 def is_binary_or_sparse_categorical(y_true, y_pred):
     46     y_t_rank = len(y_true.shape)
---> 47     y_p_rank = len(y_pred.shape)
     48     y_t_last_dim = y_true.shape[-1]
     49     y_p_last_dim = y_pred.shape[-1]

AttributeError: 'list' object has no attribute 'shape'"
}
```

So I add a print statement directly in the `is_binary_or_sparse_categorical` function to figure out what `y_pred` is:

```
Epoch 1/10
[<tf.Tensor 'functional_1_1/dense_2_1/Softmax:0' shape=(None, 2) dtype=float32>]
```

Is it bug or I miss some key point here? 


Discussion:

Resolving patch:
diff --git a/keras/trainers/compile_utils.py b/keras/trainers/compile_utils.py
--- a/keras/trainers/compile_utils.py
+++ b/keras/trainers/compile_utils.py
@@ -468,6 +468,8 @@ def build(self, y_true, y_pred):
                     "must be a callable. "
                     f"Received instead:\nloss={loss} of type {type(loss)}"
                 )
+            if isinstance(y_pred, list) and len(y_pred) == 1:
+                y_pred = y_pred[0]
 
         if is_function_like(loss) and tree.is_nested(y_pred):
             # The model has multiple outputs but only one loss fn


Test patch:
diff --git a/keras/trainers/compile_utils_test.py b/keras/trainers/compile_utils_test.py
--- a/keras/trainers/compile_utils_test.py
+++ b/keras/trainers/compile_utils_test.py
@@ -251,6 +251,21 @@ def test_single_output_case(self):
         value = compile_loss(y_true, y_pred)
         self.assertAllClose(value, 0.068333, atol=1e-5)
 
+    def test_single_output_case_with_crossentropy_loss(self):
+        compile_loss = CompileLoss(loss="crossentropy")
+
+        # Test symbolic build
+        y_true, y_pred = backend.KerasTensor((3, 4)), backend.KerasTensor(
+            (3, 4)
+        )
+        compile_loss.build(y_true, y_pred)
+        # Test eager build
+        y_true = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
+        y_pred = np.array([[0.4, 0.1], [0.2, 0.6], [0.6, 0.1]])
+        compile_loss.build(y_true, y_pred)
+        value = compile_loss(y_true, y_pred)
+        self.assertAllClose(value, 0.706595, atol=1e-5)
+
     @parameterized.parameters(True, False)
     def test_list_output_case(self, broadcast):
         if broadcast: