Repository: Lightning-AI/torchmetrics

Issue:
MetricTracker best_metric() returns the step instead of the best value
## 🐛 Bug

`MetricTracker.best_metric()` function returns the **step** with the highest metric value instead of the best **value** itself.
<!-- A clear and concise description of what the bug is. -->

### To Reproduce
Please see [colab] for a full example and proposed solution.
<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

#### Code sample
```py
from torchmetrics import Accuracy, MetricTracker
import torch
_ = torch.manual_seed(42)
tracker = MetricTracker(Accuracy(num_classes=10))
for epoch in range(5):
    tracker.increment()
    for batch_idx in range(5):
        preds, target = torch.randint(10, (100,)), torch.randint(10, (100,))
        tracker.update(preds, target)
best_acc, which_epoch = tracker.best_metric(return_step=True)

best_acc2 = tracker.best_metric(return_step=False)

assert best_acc == best_acc2 # does not hold but should
assert which_epoch == best_acc2 # instead this condition holds but shouldn't
```
<!-- Ideally attach a minimal code sample to reproduce the decried issue.
Minimal means having the shortest code but still preserving the bug. -->

### Expected behavior
Function `best_metric` without any parameters should return the best value:
> Returns the highest metric out of all tracked.  
> [best_metric function docstring](https://github.com/Lightning-AI/metrics/blob/604ed8061449a88e07ccbf764237fd47200c2038/src/torchmetrics/wrappers/tracker.py#L163)
<!-- A clear and concise description of what you expected to happen. -->

### Environment
Does not depend on the version.

## Additional context

### Inconsistent behavior even with `return_step=True` 
In fact, even with `return_step=True` the order is swapped, the function should return:
```py
return idx.item(), best.item()
```
index first and best value second ([source code](https://github.com/Lightning-AI/metrics/blob/604ed8061449a88e07ccbf764237fd47200c2038/src/torchmetrics/wrappers/tracker.py#L176)), but in current version this function return best value first and index second (`best_acc, which_epoch = ...` from example).

### Proposed solution
Use a namedtuple (values, indices) ([pytorch max function doc](https://pytorch.org/docs/stable/generated/torch.max.html)) instead of implicit unpacking.  

Replace the line with the error
```py
idx[k], best[k] = out[0].item(), out[1].item()
```
from [torchmetrics source code](https://github.com/Lightning-AI/metrics/blob/604ed8061449a88e07ccbf764237fd47200c2038/src/torchmetrics/wrappers/tracker.py#L197)
with:
```py
idx[k], best[k] = out.indices.item(), out.values.item()
```

It would be more readable and easier to understand and more prone to error.

If you agree with the proposal I can submit a follow-up pull request.

### Proposed test case
Add the following test case:
```py
# Assert that best_metric returns both index and value
val, idx = tracker.best_metric(return_step=True)
# Assert that best_metric returns just value without any parameters
val2 = tracker.best_metric()
assert val == val2
```
to the current [test](https://github.com/Lightning-AI/metrics/blob/604ed8061449a88e07ccbf764237fd47200c2038/tests/unittests/wrappers/test_tracker.py#L120).


### Backward compatibility
I think in order not to break backward compatibility this function should return the best value first and index second (as it shown in current examples). And probably the order should be placed in the documentation.
<!-- Add any other context about the problem here. -->


[colab]: https://colab.research.google.com/drive/1LauxXyQZTkchhPuc5vD2zdOu9sP7UZX-?usp=sharing


Discussion:
Hi! thanks for your contribution!, great first issue!

Resolving patch:
diff --git a/src/torchmetrics/wrappers/tracker.py b/src/torchmetrics/wrappers/tracker.py
--- a/src/torchmetrics/wrappers/tracker.py
+++ b/src/torchmetrics/wrappers/tracker.py
@@ -11,7 +11,6 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-import warnings
 from copy import deepcopy
 from typing import Any, Dict, List, Tuple, Union
 
@@ -21,6 +20,7 @@
 
 from torchmetrics.collections import MetricCollection
 from torchmetrics.metric import Metric
+from torchmetrics.utilities.prints import rank_zero_warn
 
 
 class MetricTracker(ModuleList):
@@ -171,12 +171,12 @@ def best_metric(
         if isinstance(self._base_metric, Metric):
             fn = torch.max if self.maximize else torch.min
             try:
-                idx, best = fn(self.compute_all(), 0)
+                value, idx = fn(self.compute_all(), 0)
                 if return_step:
-                    return idx.item(), best.item()
-                return best.item()
+                    return value.item(), idx.item()
+                return value.item()
             except ValueError as error:
-                warnings.warn(
+                rank_zero_warn(
                     f"Encountered the following error when trying to get the best metric: {error}"
                     "this is probably due to the 'best' not being defined for this metric."
                     "Returning `None` instead.",
@@ -189,24 +189,24 @@ def best_metric(
         else:  # this is a metric collection
             res = self.compute_all()
             maximize = self.maximize if isinstance(self.maximize, list) else len(res) * [self.maximize]
-            idx, best = {}, {}
+            value, idx = {}, {}
             for i, (k, v) in enumerate(res.items()):
                 try:
                     fn = torch.max if maximize[i] else torch.min
                     out = fn(v, 0)
-                    idx[k], best[k] = out[0].item(), out[1].item()
+                    value[k], idx[k] = out[0].item(), out[1].item()
                 except ValueError as error:
-                    warnings.warn(
+                    rank_zero_warn(
                         f"Encountered the following error when trying to get the best metric for metric {k}:"
                         f"{error} this is probably due to the 'best' not being defined for this metric."
                         "Returning `None` instead.",
                         UserWarning,
                     )
-                    idx[k], best[k] = None, None
+                    value[k], idx[k] = None, None
 
             if return_step:
-                return idx, best
-            return best
+                return value, idx
+            return value
 
     def _check_for_increment(self, method: str) -> None:
         if not self._increment_called:


Test patch:
diff --git a/tests/unittests/wrappers/test_tracker.py b/tests/unittests/wrappers/test_tracker.py
--- a/tests/unittests/wrappers/test_tracker.py
+++ b/tests/unittests/wrappers/test_tracker.py
@@ -126,6 +126,9 @@ def test_tracker(base_metric, metric_input, maximize):
         assert val != 0.0
         assert idx in list(range(5))
 
+    val2 = tracker.best_metric(return_step=False)
+    assert val == val2
+
 
 @pytest.mark.parametrize(
     "base_metric",