Repository: Lightning-AI/torchmetrics

Issue:
`ClasswiseWrapper` double prefixes with `self.prefix` when enclosed in a `MetricCollection`
## 🐛 Bug

#1866 double prefixes with `self.prefix` when enclosed in a `MetricCollection`. This is because `MetricCollection` already handles prefixing, here:
https://github.com/Lightning-AI/torchmetrics/blob/a448ad3ff4329682a83fe1036ef21f35a2a8418a/src/torchmetrics/collections.py#L335-L339
but #1866 doesn't account for it.

### To Reproduce

Enclose a `ClasswiseWrapper` with a `prefix` within a `MetricCollection`.

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

<details>
  <summary>Finds a multiclass accuracy and a classwise f1 score.</summary>


```py
from torchmetrics import *
import torch

category_names = ['Tree', 'Bush']
num_classes = len(category_names)

input_ = torch.rand((5, num_classes, 3, 3))
target = torch.ones((5, num_classes, 3, 3)).long()


val_metrics = MetricCollection(
    {
        "accuracy": Accuracy(task="multiclass", num_classes=num_classes),
        "f1": ClasswiseWrapper(
            F1Score(
                task="multiclass",
                num_classes=num_classes,
                average="none",
                dist_sync_on_step=True,
            ),
            category_names,
            prefix="f_score_",
        ),
    },
    prefix="val/",
)

val_metrics["precision"](input_, target)
val_metrics(input_, target)
```

</details>

### Expected behavior

I should get `{'val/acc': tensor(0.), 'val/f1_Tree': tensor(0.), 'val/f1_Bush': tensor(0.)}`. I instead get `{'val/acc': tensor(0.), 'val/f1_f1_Tree': tensor(0.), 'val/f1_f1_Bush': tensor(0.)}`.

### Environment

- `torchmetrics` 1.0.0 via `pip`
- Python `3.10.6` & PyTorch `1.12`:
- OS: Linux




Discussion:
Hi! thanks for your contribution!, great first issue!
Perhaps adding a flag for `Metric` subclasses to specify if they want `MetricCollection`s to prefix them, like `_key_names_fix = False` can fix the issue.

Resolving patch:
diff --git a/src/torchmetrics/wrappers/classwise.py b/src/torchmetrics/wrappers/classwise.py
--- a/src/torchmetrics/wrappers/classwise.py
+++ b/src/torchmetrics/wrappers/classwise.py
@@ -130,22 +130,22 @@ def __init__(
 
         if prefix is not None and not isinstance(prefix, str):
             raise ValueError(f"Expected argument `prefix` to either be `None` or a string but got {prefix}")
-        self.prefix = prefix
+        self._prefix = prefix
 
         if postfix is not None and not isinstance(postfix, str):
             raise ValueError(f"Expected argument `postfix` to either be `None` or a string but got {postfix}")
-        self.postfix = postfix
+        self._postfix = postfix
 
         self._update_count = 1
 
     def _convert(self, x: Tensor) -> Dict[str, Any]:
         # Will set the class name as prefix if neither prefix nor postfix is given
-        if not self.prefix and not self.postfix:
+        if not self._prefix and not self._postfix:
             prefix = f"{self.metric.__class__.__name__.lower()}_"
             postfix = ""
         else:
-            prefix = self.prefix or ""
-            postfix = self.postfix or ""
+            prefix = self._prefix or ""
+            postfix = self._postfix or ""
         if self.labels is None:
             return {f"{prefix}{i}{postfix}": val for i, val in enumerate(x)}
         return {f"{prefix}{lab}{postfix}": val for lab, val in zip(self.labels, x)}


Test patch:
diff --git a/tests/unittests/wrappers/test_classwise.py b/tests/unittests/wrappers/test_classwise.py
--- a/tests/unittests/wrappers/test_classwise.py
+++ b/tests/unittests/wrappers/test_classwise.py
@@ -1,7 +1,7 @@
 import pytest
 import torch
 from torchmetrics import MetricCollection
-from torchmetrics.classification import MulticlassAccuracy, MulticlassRecall
+from torchmetrics.classification import MulticlassAccuracy, MulticlassF1Score, MulticlassRecall
 from torchmetrics.wrappers import ClasswiseWrapper
 
 
@@ -120,3 +120,33 @@ def _get_correct_name(base):
         assert name in val
         name = _get_correct_name(f"multiclassrecall_{lab}")
         assert name in val
+
+
+def test_double_use_of_prefix_with_metriccollection():
+    """Test that the expected output is produced when using prefix/postfix with metric collection.
+
+    See issue: https://github.com/Lightning-AI/torchmetrics/issues/1915
+
+    """
+    category_names = ["Tree", "Bush"]
+    num_classes = len(category_names)
+
+    input_ = torch.rand((5, 2, 3, 3))
+    target = torch.ones((5, 2, 3, 3)).long()
+
+    val_metrics = MetricCollection(
+        {
+            "accuracy": MulticlassAccuracy(num_classes=num_classes),
+            "f1": ClasswiseWrapper(
+                MulticlassF1Score(num_classes=num_classes, average="none"),
+                category_names,
+                prefix="f_score_",
+            ),
+        },
+        prefix="val/",
+    )
+
+    res = val_metrics(input_, target)
+    assert "val/accuracy" in res
+    assert "val/f_score_Tree" in res
+    assert "val/f_score_Bush" in res