Repository: Lightning-AI/torchmetrics

Issue:
BinnedPrecisionRecallCurve throws error when `thresholds` is not passed
## 🐛 Bug

BinnedPrecisionRecallCurve argument `thresholds` is default to `None`, when `None` is passed, `self.num_thresholds` is not initialized, causing error like  `AttributeError: 'BinnedAveragePrecision' object has no attribute 'num_thresholds'`. 

Before thresholds argument is removed in #379, not passing in `thresholds` and using the default argument value works fine.  

### To Reproduce

```py
from torchmetrics import BinnedPrecisionRecallCurve
pred = torch.tensor([0, 0.1, 0.8, 0.4])
target = torch.tensor([0, 1, 1, 0])
metric = BinnedAveragePrecision(num_classes=1)
metric.update(preds, target)
```

#### Code sample
In PR #322 , num_thresholds which defaults to 100 was deprecated and further removed in PR #379 . Let's reuse the default value instead of using `None`. 

```py
def __init__(
    self,
    num_classes: int,
    thresholds: Union[int, Tensor, List[float]] = 100,
    **kwargs: Dict[str, Any],
) -> None:
```


### Expected behavior

No errors when default argument is used




Discussion:

Resolving patch:
diff --git a/torchmetrics/classification/binned_precision_recall.py b/torchmetrics/classification/binned_precision_recall.py
--- a/torchmetrics/classification/binned_precision_recall.py
+++ b/torchmetrics/classification/binned_precision_recall.py
@@ -116,7 +116,7 @@ class BinnedPrecisionRecallCurve(Metric):
     def __init__(
         self,
         num_classes: int,
-        thresholds: Union[int, Tensor, List[float], None] = None,
+        thresholds: Union[int, Tensor, List[float]] = 100,
         **kwargs: Dict[str, Any],
     ) -> None:
         super().__init__(**kwargs)
@@ -278,7 +278,7 @@ def __init__(
         self,
         num_classes: int,
         min_precision: float,
-        thresholds: Union[int, Tensor, List[float], None] = None,
+        thresholds: Union[int, Tensor, List[float]] = 100,
         **kwargs: Dict[str, Any],
     ) -> None:
         super().__init__(num_classes=num_classes, thresholds=thresholds, **kwargs)


Test patch:
diff --git a/tests/classification/test_binned_precision_recall.py b/tests/classification/test_binned_precision_recall.py
--- a/tests/classification/test_binned_precision_recall.py
+++ b/tests/classification/test_binned_precision_recall.py
@@ -100,6 +100,28 @@ def test_binned_recall_at_precision(
             },
         )
 
+    @pytest.mark.parametrize("ddp", [True, False])
+    @pytest.mark.parametrize("dist_sync_on_step", [True, False])
+    @pytest.mark.parametrize("min_precision", [0.05, 0.1, 0.3, 0.5, 0.8, 0.95])
+    def test_binned_recall_at_precision_default_thresholds(
+        self, preds, target, sk_metric, num_classes, ddp, dist_sync_on_step, min_precision
+    ):
+        # rounding will simulate binning for both implementations
+        preds = Tensor(np.round(preds.numpy(), 2)) + 1e-6
+
+        self.run_class_metric_test(
+            ddp=ddp,
+            preds=preds,
+            target=target,
+            metric_class=BinnedRecallAtFixedPrecision,
+            sk_metric=partial(sk_metric, num_classes=num_classes, min_precision=min_precision),
+            dist_sync_on_step=dist_sync_on_step,
+            metric_args={
+                "num_classes": num_classes,
+                "min_precision": min_precision,
+            },
+        )
+
 
 @pytest.mark.parametrize(
     "preds, target, sk_metric, num_classes",
@@ -111,6 +133,8 @@ def test_binned_recall_at_precision(
     ],
 )
 class TestBinnedAveragePrecision(MetricTester):
+    atol = 0.002
+
     @pytest.mark.parametrize("ddp", [True, False])
     @pytest.mark.parametrize("dist_sync_on_step", [True, False])
     @pytest.mark.parametrize("thresholds", (301, torch.linspace(0.0, 1.0, 101)))
@@ -127,3 +151,21 @@ def test_binned_average_precision(self, preds, target, sk_metric, num_classes, d
             dist_sync_on_step=dist_sync_on_step,
             metric_args={"num_classes": num_classes, "thresholds": thresholds},
         )
+
+    @pytest.mark.parametrize("ddp", [True, False])
+    @pytest.mark.parametrize("dist_sync_on_step", [True, False])
+    def test_binned_average_precision_default_thresholds(
+        self, preds, target, sk_metric, num_classes, ddp, dist_sync_on_step
+    ):
+        # rounding will simulate binning for both implementations
+        preds = Tensor(np.round(preds.numpy(), 2)) + 1e-6
+
+        self.run_class_metric_test(
+            ddp=ddp,
+            preds=preds,
+            target=target,
+            metric_class=BinnedAveragePrecision,
+            sk_metric=partial(sk_metric, num_classes=num_classes),
+            dist_sync_on_step=dist_sync_on_step,
+            metric_args={"num_classes": num_classes},
+        )