Repository: pytorch/ignite

Issue:
Add pairwise distance to Metrics
I think in evaluation of regression task, pairwise distance, especially norm-2 distance, as in `torch.nn.functional.pairwise_distance` is at least as frequently used as MSE, which is actually mostly used as loss rather than evaluation metrics. Therefore, I was wondering if it is worthy of being added to Metrics package as a commonly used metrics. 

Discussion:
jasonkriss: @alykhantejani I just added MeanPairwiseDistance to this PR.

Patch:
diff --git a/ignite/metrics/__init__.py b/ignite/metrics/__init__.py
index 3902c5de..2cc4e1ff 100644
--- a/ignite/metrics/__init__.py
+++ b/ignite/metrics/__init__.py
@@ -1,3 +1,8 @@
+from .binary_accuracy import BinaryAccuracy
 from .categorical_accuracy import CategoricalAccuracy
+from .mean_absolute_error import MeanAbsoluteError
+from .mean_pairwise_distance import MeanPairwiseDistance
 from .mean_squared_error import MeanSquaredError
 from .metric import Metric
+from .root_mean_squared_error import RootMeanSquaredError
+from .top_k_categorical_accuracy import TopKCategoricalAccuracy
diff --git a/ignite/metrics/binary_accuracy.py b/ignite/metrics/binary_accuracy.py
new file mode 100644
index 00000000..3b33a4db
--- /dev/null
+++ b/ignite/metrics/binary_accuracy.py
@@ -0,0 +1,28 @@
+from __future__ import division
+
+import torch
+
+from .metric import Metric
+from ignite.exceptions import NotComputableError
+
+
+class BinaryAccuracy(Metric):
+    """
+    Calculates the binary accuracy.
+
+    `update` must receive output of the form (y_pred, y).
+    """
+    def reset(self):
+        self._num_correct = 0
+        self._num_examples = 0
+
+    def update(self, output):
+        y_pred, y = output
+        correct = torch.eq(torch.round(y_pred).type(torch.LongTensor), y)
+        self._num_correct += torch.sum(correct)
+        self._num_examples += correct.shape[0]
+
+    def compute(self):
+        if self._num_examples == 0:
+            raise NotComputableError('BinaryAccuracy must have at least one example before it can be computed')
+        return self._num_correct / self._num_examples
diff --git a/ignite/metrics/mean_absolute_error.py b/ignite/metrics/mean_absolute_error.py
new file mode 100644
index 00000000..88c620de
--- /dev/null
+++ b/ignite/metrics/mean_absolute_error.py
@@ -0,0 +1,28 @@
+from __future__ import division
+
+import torch
+
+from .metric import Metric
+from ignite.exceptions import NotComputableError
+
+
+class MeanAbsoluteError(Metric):
+    """
+    Calculates the mean absolute error.
+
+    `update` must receive output of the form (y_pred, y).
+    """
+    def reset(self):
+        self._sum_of_absolute_errors = 0.0
+        self._num_examples = 0
+
+    def update(self, output):
+        y_pred, y = output
+        absolute_errors = torch.abs(y_pred - y.view_as(y_pred))
+        self._sum_of_absolute_errors += torch.sum(absolute_errors)
+        self._num_examples += y.shape[0]
+
+    def compute(self):
+        if self._num_examples == 0:
+            raise NotComputableError('MeanAbsoluteError must have at least one example before it can be computed')
+        return self._sum_of_absolute_errors / self._num_examples
diff --git a/ignite/metrics/mean_pairwise_distance.py b/ignite/metrics/mean_pairwise_distance.py
new file mode 100644
index 00000000..b18be661
--- /dev/null
+++ b/ignite/metrics/mean_pairwise_distance.py
@@ -0,0 +1,34 @@
+from __future__ import division
+
+import torch
+from torch.nn.functional import pairwise_distance
+
+from .metric import Metric
+from ignite.exceptions import NotComputableError
+
+
+class MeanPairwiseDistance(Metric):
+    """
+    Calculates the mean pairwise distance.
+
+    `update` must receive output of the form (y_pred, y).
+    """
+    def __init__(self, p=2, eps=1e-6):
+        super(MeanPairwiseDistance, self).__init__()
+        self._p = p
+        self._eps = eps
+
+    def reset(self):
+        self._sum_of_distances = 0.0
+        self._num_examples = 0
+
+    def update(self, output):
+        y_pred, y = output
+        distances = pairwise_distance(y_pred, y, p=self._p, eps=self._eps)
+        self._sum_of_distances += torch.sum(distances)
+        self._num_examples += y.shape[0]
+
+    def compute(self):
+        if self._num_examples == 0:
+            raise NotComputableError('MeanAbsoluteError must have at least one example before it can be computed')
+        return self._sum_of_distances / self._num_examples
diff --git a/ignite/metrics/root_mean_squared_error.py b/ignite/metrics/root_mean_squared_error.py
new file mode 100644
index 00000000..47e9f396
--- /dev/null
+++ b/ignite/metrics/root_mean_squared_error.py
@@ -0,0 +1,18 @@
+from __future__ import division
+import math
+
+import torch
+
+from .mean_squared_error import MeanSquaredError
+from ignite.exceptions import NotComputableError
+
+
+class RootMeanSquaredError(MeanSquaredError):
+    """
+    Calculates the root mean squared error.
+
+    `update` must receive output of the form (y_pred, y).
+    """
+    def compute(self):
+        mse = super(RootMeanSquaredError, self).compute()
+        return math.sqrt(mse)
diff --git a/ignite/metrics/top_k_categorical_accuracy.py b/ignite/metrics/top_k_categorical_accuracy.py
new file mode 100644
index 00000000..1ee90353
--- /dev/null
+++ b/ignite/metrics/top_k_categorical_accuracy.py
@@ -0,0 +1,34 @@
+from __future__ import division
+
+import torch
+
+from .metric import Metric
+from ignite.exceptions import NotComputableError
+
+
+class TopKCategoricalAccuracy(Metric):
+    """
+    Calculates the top-k categorical accuracy.
+
+    `update` must receive output of the form (y_pred, y).
+    """
+    def __init__(self, k=5):
+        super(TopKCategoricalAccuracy, self).__init__()
+        self._k = k
+
+    def reset(self):
+        self._num_correct = 0
+        self._num_examples = 0
+
+    def update(self, output):
+        y_pred, y = output
+        sorted_indices = torch.topk(y_pred, self._k, dim=1)[1]
+        expanded_y = y.view(-1, 1).expand(-1, self._k)
+        correct = torch.sum(torch.eq(sorted_indices, expanded_y), dim=1)
+        self._num_correct += torch.sum(correct)
+        self._num_examples += correct.shape[0]
+
+    def compute(self):
+        if self._num_examples == 0:
+            raise NotComputableError('TopKCategoricalAccuracy must have at least one example before it can be computed')
+        return self._num_correct / self._num_examples


Test patch:
diff --git a/tests/ignite/metrics/test_binary_accuracy.py b/tests/ignite/metrics/test_binary_accuracy.py
new file mode 100644
index 00000000..4e083ec3
--- /dev/null
+++ b/tests/ignite/metrics/test_binary_accuracy.py
@@ -0,0 +1,25 @@
+from ignite.exceptions import NotComputableError
+from ignite.metrics import BinaryAccuracy
+import pytest
+import torch
+
+
+def test_zero_div():
+    acc = BinaryAccuracy()
+    with pytest.raises(NotComputableError):
+        acc.compute()
+
+
+def test_compute():
+    acc = BinaryAccuracy()
+
+    y_pred = torch.FloatTensor([0.2, 0.4, 0.6, 0.8])
+    y = torch.ones(4).type(torch.LongTensor)
+    acc.update((y_pred, y))
+    assert acc.compute() == 0.5
+
+    acc.reset()
+    y_pred = torch.FloatTensor([0.2, 0.7, 0.8, 0.9])
+    y = torch.ones(4).type(torch.LongTensor)
+    acc.update((y_pred, y))
+    assert acc.compute() == 0.75
diff --git a/tests/ignite/metrics/test_mean_absolute_error.py b/tests/ignite/metrics/test_mean_absolute_error.py
new file mode 100644
index 00000000..f2bd60bf
--- /dev/null
+++ b/tests/ignite/metrics/test_mean_absolute_error.py
@@ -0,0 +1,25 @@
+from ignite.exceptions import NotComputableError
+from ignite.metrics import MeanAbsoluteError
+import pytest
+import torch
+
+
+def test_zero_div():
+    mae = MeanAbsoluteError()
+    with pytest.raises(NotComputableError):
+        mae.compute()
+
+
+def test_compute():
+    mae = MeanAbsoluteError()
+
+    y_pred = torch.Tensor([[2.0], [-2.0]])
+    y = torch.zeros(2)
+    mae.update((y_pred, y))
+    assert mae.compute() == 2.0
+
+    mae.reset()
+    y_pred = torch.Tensor([[3.0], [-3.0]])
+    y = torch.zeros(2)
+    mae.update((y_pred, y))
+    assert mae.compute() == 3.0
diff --git a/tests/ignite/metrics/test_mean_pairwise_distance.py b/tests/ignite/metrics/test_mean_pairwise_distance.py
new file mode 100644
index 00000000..45c69fdc
--- /dev/null
+++ b/tests/ignite/metrics/test_mean_pairwise_distance.py
@@ -0,0 +1,26 @@
+from ignite.exceptions import NotComputableError
+from ignite.metrics import MeanPairwiseDistance
+import pytest
+from pytest import approx
+import torch
+
+
+def test_zero_div():
+    mpd = MeanPairwiseDistance()
+    with pytest.raises(NotComputableError):
+        mpd.compute()
+
+
+def test_compute():
+    mpd = MeanPairwiseDistance()
+
+    y_pred = torch.Tensor([[3.0, 4.0], [-3.0, -4.0]])
+    y = torch.zeros(2, 2)
+    mpd.update((y_pred, y))
+    assert mpd.compute() == approx(5.0)
+
+    mpd.reset()
+    y_pred = torch.Tensor([[4.0, 4.0, 4.0, 4.0], [-4.0, -4.0, -4.0, -4.0]])
+    y = torch.zeros(2, 4)
+    mpd.update((y_pred, y))
+    assert mpd.compute() == approx(8.0)
diff --git a/tests/ignite/metrics/test_root_mean_squared_error.py b/tests/ignite/metrics/test_root_mean_squared_error.py
new file mode 100644
index 00000000..69408ced
--- /dev/null
+++ b/tests/ignite/metrics/test_root_mean_squared_error.py
@@ -0,0 +1,25 @@
+from ignite.exceptions import NotComputableError
+from ignite.metrics import RootMeanSquaredError
+import pytest
+import torch
+
+
+def test_zero_div():
+    rmse = RootMeanSquaredError()
+    with pytest.raises(NotComputableError):
+        rmse.compute()
+
+
+def test_compute():
+    rmse = RootMeanSquaredError()
+
+    y_pred = torch.Tensor([[2.0], [-2.0]])
+    y = torch.zeros(2)
+    rmse.update((y_pred, y))
+    assert rmse.compute() == 2.0
+
+    rmse.reset()
+    y_pred = torch.Tensor([[3.0], [-3.0]])
+    y = torch.zeros(2)
+    rmse.update((y_pred, y))
+    assert rmse.compute() == 3.0
diff --git a/tests/ignite/metrics/test_top_k_categorical_accuracy.py b/tests/ignite/metrics/test_top_k_categorical_accuracy.py
new file mode 100644
index 00000000..d9164f9f
--- /dev/null
+++ b/tests/ignite/metrics/test_top_k_categorical_accuracy.py
@@ -0,0 +1,25 @@
+from ignite.exceptions import NotComputableError
+from ignite.metrics import TopKCategoricalAccuracy
+import pytest
+import torch
+
+
+def test_zero_div():
+    acc = TopKCategoricalAccuracy(2)
+    with pytest.raises(NotComputableError):
+        acc.compute()
+
+
+def test_compute():
+    acc = TopKCategoricalAccuracy(2)
+
+    y_pred = torch.FloatTensor([[0.2, 0.4, 0.6, 0.8], [0.8, 0.6, 0.4, 0.2]])
+    y = torch.ones(2).type(torch.LongTensor)
+    acc.update((y_pred, y))
+    assert acc.compute() == 0.5
+
+    acc.reset()
+    y_pred = torch.FloatTensor([[0.4, 0.8, 0.2, 0.6], [0.8, 0.6, 0.4, 0.2]])
+    y = torch.ones(2).type(torch.LongTensor)
+    acc.update((y_pred, y))
+    assert acc.compute() == 1.0