Repository: Lightning-AI/torchmetrics

Issue:
`r2_score` larger than 1 with (near) constant `target`
## 🐛 Bug
With (near) constant `targets`, the `r2_score` can be larger than 1 which shouldn't be possible.  

### To Reproduce

See the code example below. 

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

<details>
  <summary>Code sample</summary>

```python
import torch
from torchmetrics.functional import r2_score
from sklearn.metrics import r2_score as sklearn_r2_score

y_true = torch.tensor([-5.1608, -5.1609, -5.1608, -5.1608, -5.1608, -5.1608])
y_pred = torch.tensor([-3.9865, -5.4648, -5.0238, -4.3899, -5.6672, -4.7336])
score = r2_score(preds=y_pred, target=y_true)
print(score)  # prints 'tensor(82685.5312)'

score = sklearn_r2_score(y_true=y_true.numpy(), y_pred=y_pred.numpy())
print(score)  # prints '-301979050.37719727'
```
</details>

### Expected behavior
The `r2_score` should never be above 1. The expected  behavior would be to return a negative value.

### Environment

My environment was installed through `mamba`.
- TorchMetrics version: `0.11.0`
- Python & PyTorch Version (e.g., 1.0): `3.9.16` (Python) and `1.12.1` (PyTorch)
- Any other relevant information such as OS (e.g., Linux): Ubuntu

```
python                    3.9.16          h2782a2a_0_cpython         conda-forge
pytorch                   1.12.1          cuda112py39hb0b7ed5_201    conda-forge
pytorch-lightning         1.8.1           pyhd8ed1ab_0               conda-forge
pytorch_geometric         2.2.0           pyhd8ed1ab_0               conda-forge
pytorch_scatter           2.1.0           cuda112py39h83a068c_0      conda-forge
pytorch_sparse            0.6.15          py39h83a068c_0             conda-forge
torchmetrics              0.11.0          pyhd8ed1ab_0               conda-forge
torchvision               0.13.0          cuda112py39hd2c45b6_0      conda-forge
```




Discussion:

Resolving patch:
diff --git a/src/torchmetrics/functional/regression/r2.py b/src/torchmetrics/functional/regression/r2.py
--- a/src/torchmetrics/functional/regression/r2.py
+++ b/src/torchmetrics/functional/regression/r2.py
@@ -41,7 +41,6 @@ def _r2_score_update(preds: Tensor, target: Tensor) -> Tuple[Tensor, Tensor, Ten
     residual = target - preds
     rss = torch.sum(residual * residual, dim=0)
     n_obs = target.size(0)
-
     return sum_squared_obs, sum_obs, rss, n_obs
 
 
@@ -79,7 +78,15 @@ def _r2_score_compute(
 
     mean_obs = sum_obs / n_obs
     tss = sum_squared_obs - sum_obs * mean_obs
-    raw_scores = 1 - (rss / tss)
+
+    # Account for near constant targets
+    cond_rss = ~torch.isclose(rss, torch.zeros_like(rss), atol=1e-4)
+    cond_tss = ~torch.isclose(tss, torch.zeros_like(tss), atol=1e-4)
+    cond = cond_rss & cond_tss
+
+    raw_scores = torch.ones_like(rss)
+    raw_scores[cond] = 1 - (rss[cond] / tss[cond])
+    raw_scores[cond_rss & ~cond_tss] = 0.0
 
     if multioutput == "raw_values":
         r2 = raw_scores
diff --git a/src/torchmetrics/regression/r2.py b/src/torchmetrics/regression/r2.py
--- a/src/torchmetrics/regression/r2.py
+++ b/src/torchmetrics/regression/r2.py
@@ -32,6 +32,9 @@ class R2Score(Metric):
     .. math:: R^2_{adj} = 1 - \frac{(1-R^2)(n-1)}{n-k-1}
 
     where the parameter :math:`k` (the number of independent regressors) should be provided as the `adjusted` argument.
+    The score is only proper defined when :math:`SS_{tot}\neq 0`, which can happen for near constant targets. In this
+    case a score of 0 is returned. By definition the score is bounded between 0 and 1, where 1 corresponds to the
+    predictions exactly matching the targets.
 
     As input to ``forward`` and ``update`` the metric accepts the following input:
 


Test patch:
diff --git a/tests/unittests/regression/test_r2.py b/tests/unittests/regression/test_r2.py
--- a/tests/unittests/regression/test_r2.py
+++ b/tests/unittests/regression/test_r2.py
@@ -159,3 +159,11 @@ def test_warning_on_too_large_adjusted(metric_class=R2Score):
 
     with pytest.warns(UserWarning, match="Division by zero in adjusted r2 score. Falls back to" " standard r2 score."):
         metric(torch.randn(11), torch.randn(11))
+
+
+def test_constant_target():
+    """Check for a near constant target that a value of 0 is returned."""
+    y_true = torch.tensor([-5.1608, -5.1609, -5.1608, -5.1608, -5.1608, -5.1608])
+    y_pred = torch.tensor([-3.9865, -5.4648, -5.0238, -4.3899, -5.6672, -4.7336])
+    score = r2_score(preds=y_pred, target=y_true)
+    assert score == 0