Repository: speechbrain/speechbrain

Issue:
BCE loss breaching (batch, seq, feat) coding style
Hello,

I would like some help understanding a transpose function in the BCE loss at speechbrain/nnet/losses.py#L464. It seems to me that it breaks the (batch, seq, feat) dimension order code style.

```python
if len(inputs.shape) == 3:
    inputs, targets = truncate(inputs, targets, allowed_len_diff)
    inputs = inputs.transpose(1, -1)
```

If the BCE is applied to a sequence of predictions and labels, the two last dimensions are transposed. Meanwhile the
targets are unchanged. 

Using the BCE_loss function in a recipe of mine:
- BCE((batch, seq, predictions), (batch, seq, one_hot_encoded label)) doesn't work.
- Removing the  transpose operation in the BCE Loss makes it work.

Is there something I missed ? Could you help me understand the purpose of the transpose operation ?

Thanks for your help

Dan


Discussion:
Hi ! I am not quite sure as I didn't participate in this part. However, from the argument docstring we can see that:

```
 inputs : torch.Tensor
        The output before applying the final softmax
        Format is [batch, 1] or [batch, frames, 1].
 targets : torch.Tensor
        The targets, of shape [batch] or [batch, frames].
```

Then if input.shape() == 3 we need (I guess?) to transpose the last dimension to have the frames (or predictions in your case) at the same position. But it's just because target is always expected to be 1 or 2 dimensional. 
Thanks for your quick reply,

Indeed I only tried BCE_loss on 3-dimension inputs and targets (which works despite the docstring).

I will unit test the BCE_loss on simpler tensors with 3D input and 2D targets to get a better understanding, then keep the issue updated.
The docstring seems erroneous for the 3d input 2d label case:

For a very simple 1 class classification problem with input.shape (1, 1, 1) and target.shape (1, 1)

```python
# Simple 1-class classification problem
from speechbrain.nnet.losses import BCE_loss
import torch
preds = torch.Tensor([[[1]]]) # max probability on first class
labels = torch.Tensor([[0]]) # classified as first class
BCE_loss(a, b)
# ValueError: Target size (torch.Size([1, 1])) must be the same as input size (torch.Size([1, 1, 1]))
```

Then, trying a 3-classes classification with 3dim preds and 3dim one hot encoded target:

```python
# Simple 1-class classification problem
from speechbrain.nnet.losses import BCE_loss
import torch
preds = torch.Tensor([[[1, 0, 0], [0, 1, 0]]]) # perfect prediction
labels = torch.Tensor([[[1, 0, 0], [0, 1, 0]]])
BCE_loss(a, b)
# ValueError: Target size (torch.Size([1, 2, 3])) must be the same as input size (torch.Size([1, 3, 2]))
```
It really seems like the dims have been unduly swapped

Edit:
Actually the docstring is strange even for the 2D input 1D target case:
- The provided code example is 1D input 1D target
- 2D input 1D target yields the same error that 3D input 2D target
- The nb of features in the docstring seems to be 1
- In the provided example there is no batch nor frame dimension, just features.
- The provided code example won't work in 2D/1D or 3D/2D, and works in 3D input 3D target with batch_size=1 and seq_len=1 if and only if I remove the transpose line, and yields then the exact same result than the doc usage example.

Resolving patch:
diff --git a/speechbrain/nnet/losses.py b/speechbrain/nnet/losses.py
--- a/speechbrain/nnet/losses.py
+++ b/speechbrain/nnet/losses.py
@@ -419,7 +419,7 @@ def nll_loss(
     )
 
 
-def BCE_loss(
+def bce_loss(
     inputs,
     targets,
     length=None,
@@ -436,7 +436,8 @@ def BCE_loss(
     ---------
     inputs : torch.Tensor
         The output before applying the final softmax
-        Format is [batch, 1] or [batch, frames, 1].
+        Format is [batch[, 1]?] or [batch, frames[, 1]?].
+        (Works with or without a singleton dimension at the end).
     targets : torch.Tensor
         The targets, of shape [batch] or [batch, frames].
     length : torch.Tensor
@@ -458,12 +459,18 @@ def BCE_loss(
     -------
     >>> inputs = torch.tensor([10.0, -6.0])
     >>> targets = torch.tensor([1, 0])
-    >>> BCE_loss(inputs, targets)
+    >>> bce_loss(inputs, targets)
     tensor(0.0013)
     """
-    if len(inputs.shape) == 3:
+    # Squeeze singleton dimension so inputs + targets match
+    if len(inputs.shape) == len(targets.shape) + 1:
+        inputs = inputs.squeeze(-1)
+
+    # Make sure tensor lengths match
+    if len(inputs.shape) >= 2:
         inputs, targets = truncate(inputs, targets, allowed_len_diff)
-        inputs = inputs.transpose(1, -1)
+    elif length is not None:
+        raise ValueError("length can be passed only for >= 2D inputs.")
 
     # Pass the loss function but apply reduction="none" first
     loss = functools.partial(


Test patch:
diff --git a/tests/unittests/test_losses.py b/tests/unittests/test_losses.py
--- a/tests/unittests/test_losses.py
+++ b/tests/unittests/test_losses.py
@@ -36,6 +36,30 @@ def test_l1():
     assert torch.all(torch.eq(out_cost, 0))
 
 
+def test_bce_loss():
+    from speechbrain.nnet.losses import bce_loss
+
+    # Ensure this works both with and without singleton dimension
+    predictions_singleton = torch.zeros(4, 10, 1)
+    predictions_match = torch.zeros(4, 10)
+    targets = torch.ones(4, 10)
+    lengths = torch.ones(4)
+    out_cost_singleton = bce_loss(predictions_singleton, targets, lengths)
+    out_cost_match = bce_loss(predictions_match, targets, lengths)
+    assert torch.allclose(torch.exp(out_cost_singleton), torch.tensor(2.0))
+    assert torch.allclose(torch.exp(out_cost_match), torch.tensor(2.0))
+
+    # How about one dimensional inputs
+    predictions = torch.zeros(5, 1)
+    targets = torch.ones(5)
+    out_cost = bce_loss(predictions, targets)
+    assert torch.allclose(torch.exp(out_cost), torch.tensor(2.0))
+
+    # Can't pass lengths in 1D case
+    with pytest.raises(ValueError):
+        bce_loss(predictions, targets, length=torch.ones(5))
+
+
 def test_classification_error():
     from speechbrain.nnet.losses import classification_error