Repository: pytorch/ignite

Issue:
[Contrib metrics] Add R2 score metric
Maybe adding [R2 metric](https://en.wikipedia.org/wiki/Coefficient_of_determination) to `ignite.contrib.metrics.regression` could make sense. Thoughts ?


Discussion:
@vfdev-5 I think that's a good idea! R2 is probably the first regression metric people start with.
@vfdev-5 do you think this should be an epoch metric? or does it make sense to calculate this batch by batch?
@anmolsjoshi I would say batch per batch is feasible.
@vfdev-5 sounds good, if you haven't started working on it, I can send in a PR tonight
yes, go ahead, please :)

On Wed, Mar 20, 2019, 20:00 Anmol Joshi <notifications@github.com wrote:

> @vfdev-5 <https://github.com/vfdev-5> sounds good, if you haven't started
> working on it, I can send in a PR tonight
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/pytorch/ignite/issues/453#issuecomment-474984300>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/ACWHH-kSM6uWKWMo68lhlj6-hsh-mA3bks5vYoUwgaJpZM4btr9D>
> .
>


Resolving patch:
diff --git a/ignite/contrib/metrics/regression/__init__.py b/ignite/contrib/metrics/regression/__init__.py
--- a/ignite/contrib/metrics/regression/__init__.py
+++ b/ignite/contrib/metrics/regression/__init__.py
@@ -12,3 +12,4 @@
 from ignite.contrib.metrics.regression.median_relative_absolute_error import MedianRelativeAbsoluteError
 from ignite.contrib.metrics.regression.median_absolute_percentage_error import MedianAbsolutePercentageError
 from ignite.contrib.metrics.regression.geometric_mean_relative_absolute_error import GeometricMeanRelativeAbsoluteError
+from ignite.contrib.metrics.regression.r2_score import R2Score
diff --git a/ignite/contrib/metrics/regression/r2_score.py b/ignite/contrib/metrics/regression/r2_score.py
new file mode 100644
--- /dev/null
+++ b/ignite/contrib/metrics/regression/r2_score.py
@@ -0,0 +1,39 @@
+from __future__ import division
+
+import torch
+
+from ignite.exceptions import NotComputableError
+from ignite.contrib.metrics.regression._base import _BaseRegression
+
+
+class R2Score(_BaseRegression):
+    r"""
+        Calculates the R-Squared, the
+        `coefficient of determination <https://en.wikipedia.org/wiki/Coefficient_of_determination>`_:
+
+        :math:`R^2 = 1 - \frac{\sum_{j=1}^n(A_j - P_j)^2}{\sum_{j=1}^n(A_j - \bar{A})^2}`,
+
+        where :math:`A_j` is the ground truth, :math:`P_j` is the predicted value and
+        :math:`\bar{A}` is the mean of the ground truth.
+
+        - `update` must receive output of the form `(y_pred, y)`.
+        - `y` and `y_pred` must be of same shape `(N, )` or `(N, 1)` and of type `float32`.
+    """
+    def reset(self):
+        self._num_examples = 0
+        self._sum_of_errors = 0
+        self._y_sq_sum = 0
+        self._y_sum = 0
+
+    def _update(self, output):
+        y_pred, y = output
+        self._num_examples += y.shape[0]
+        self._sum_of_errors += torch.sum(torch.pow(y_pred - y, 2)).item()
+
+        self._y_sum += torch.sum(y).item()
+        self._y_sq_sum += torch.sum(torch.pow(y, 2)).item()
+
+    def compute(self):
+        if self._num_examples == 0:
+            raise NotComputableError('R2Score must have at least one example before it can be computed.')
+        return 1 - self._sum_of_errors / (self._y_sq_sum - (self._y_sum ** 2) / self._num_examples)


Test patch:
diff --git a/tests/ignite/contrib/metrics/regression/test_r2_score.py b/tests/ignite/contrib/metrics/regression/test_r2_score.py
new file mode 100644
--- /dev/null
+++ b/tests/ignite/contrib/metrics/regression/test_r2_score.py
@@ -0,0 +1,91 @@
+import torch
+import numpy as np
+import pytest
+from ignite.engine import Engine
+from ignite.contrib.metrics.regression import R2Score
+from sklearn.metrics import r2_score
+
+
+def test_wrong_input_shapes():
+    m = R2Score()
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 1, 2)))
+
+
+def test_r2_score():
+
+    size = 51
+    np_y_pred = np.random.rand(size,)
+    np_y = np.random.rand(size,)
+
+    m = R2Score()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    m.update((y_pred, y))
+
+    assert r2_score(np_y, np_y_pred) == pytest.approx(m.compute())
+
+
+def test_r2_score_2():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+
+    m = R2Score()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    batch_size = 16
+    n_iters = size // batch_size + 1
+    for i in range(n_iters):
+        idx = i * batch_size
+        m.update((y_pred[idx: idx + batch_size], y[idx: idx + batch_size]))
+
+    assert r2_score(np_y, np_y_pred) == pytest.approx(m.compute())
+
+
+def test_integration_r2_score_with_output_transform():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+
+    batch_size = 15
+
+    def update_fn(engine, batch):
+        idx = (engine.state.iteration - 1) * batch_size
+        y_true_batch = np_y[idx:idx + batch_size]
+        y_pred_batch = np_y_pred[idx:idx + batch_size]
+        return idx, torch.from_numpy(y_pred_batch), torch.from_numpy(y_true_batch)
+
+    engine = Engine(update_fn)
+
+    m = R2Score(output_transform=lambda x: (x[1], x[2]))
+    m.attach(engine, 'r2_score')
+
+    data = list(range(size // batch_size))
+    r_squared = engine.run(data, max_epochs=1).metrics['r2_score']
+
+    assert r2_score(np_y, np_y_pred) == pytest.approx(r_squared)