Repository: Lightning-AI/pytorch-lightning

Issue:
PSNR metric in metrics
## 🚀 Feature
Can we have [PSNR](https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio) metric in the metrics module?

### Motivation
This metric is used in Image generative models eg. super-resolution.


Discussion:
Actually I think I have implemented it somewhere
Couldn't find it in lightning.
> Couldn't find it in lightning.

No, sorry, I meant I've implemented it for my needs, and I'm about to open a PR where I add it
Oh! I have worked with it too. It's just `log inverse mse`. Thought it would be a good addition to lightning metrics. I think it needs to be discussed/approved by core maintainers first then only a PR can be submitted.

Resolving patch:
diff --git a/pytorch_lightning/metrics/__init__.py b/pytorch_lightning/metrics/__init__.py
--- a/pytorch_lightning/metrics/__init__.py
+++ b/pytorch_lightning/metrics/__init__.py
@@ -5,6 +5,7 @@
     RMSE,
     MAE,
     RMSLE,
+    PSNR
 )
 from pytorch_lightning.metrics.classification import (
     Accuracy,
@@ -50,6 +51,7 @@
     'MSE',
     'RMSE',
     'MAE',
-    'RMSLE'
+    'RMSLE',
+    'PSNR'
 ]
 __all__ = __regression_metrics + __classification_metrics + ['SklearnMetric']
diff --git a/pytorch_lightning/metrics/functional/regression.py b/pytorch_lightning/metrics/functional/regression.py
new file mode 100644
--- /dev/null
+++ b/pytorch_lightning/metrics/functional/regression.py
@@ -0,0 +1,40 @@
+import torch
+from torch.nn import functional as F
+
+from pytorch_lightning.metrics.functional.reduction import reduce
+
+
+def psnr(
+    pred: torch.Tensor,
+    target: torch.Tensor,
+    data_range: float = None,
+    base: float = 10.0,
+    reduction: str = 'elementwise_mean'
+) -> torch.Tensor:
+    """
+    Computes the peak signal-to-noise ratio metric
+
+    Args:
+        pred: estimated signal
+        target: groun truth signal
+        data_range: the range of the data. If None, it is determined from the data (max - min).
+        base: a base of a logarithm to use (default: 10)
+        reduction: method for reducing psnr (default: takes the mean)
+
+    Example:
+
+        >>> from pytorch_lightning.metrics.regression import PSNR
+        >>> pred = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
+        >>> target = torch.tensor([[3.0, 2.0], [1.0, 0.0]])
+        >>> metric = PSNR()
+        >>> metric(pred, target)
+        tensor(2.5527)
+    """
+
+    if data_range is None:
+        data_range = max(target.max() - target.min(), pred.max() - pred.min())
+    else:
+        data_range = torch.tensor(float(data_range))
+    mse = F.mse_loss(pred.view(-1), target.view(-1), reduction=reduction)
+    psnr_base_e = 2 * torch.log(data_range) - torch.log(mse)
+    return psnr_base_e * (10 / torch.log(torch.tensor(base)))
diff --git a/pytorch_lightning/metrics/regression.py b/pytorch_lightning/metrics/regression.py
--- a/pytorch_lightning/metrics/regression.py
+++ b/pytorch_lightning/metrics/regression.py
@@ -1,8 +1,11 @@
 import torch.nn.functional as F
 import torch
 from pytorch_lightning.metrics.metric import Metric
+from pytorch_lightning.metrics.functional.regression import (
+    psnr,
+)
 
-__all__ = ['MSE', 'RMSE', 'MAE', 'RMSLE']
+__all__ = ['MSE', 'RMSE', 'MAE', 'RMSLE', 'PSNR']
 
 
 class MSE(Metric):
@@ -187,3 +190,33 @@ def forward(self, pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
             A Tensor with the rmsle loss.
         """
         return F.mse_loss(torch.log(pred + 1), torch.log(target + 1), self.reduction)
+
+
+class PSNR(Metric):
+    """
+    Computes the peak signal-to-noise ratio metric
+    """
+
+    def __init__(self, data_range: float = None, base: int = 10, reduction: str = 'elementwise_mean'):
+        """
+        Args:
+            data_range: the range of the data. If None, it is determined from the data (max - min).
+            base: a base of a logarithm to use (default: 10)
+            reduction: method for reducing psnr (default: takes the mean)
+
+
+        Example:
+
+            >>> pred = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
+            >>> target = torch.tensor([[3.0, 2.0], [1.0, 0.0]])
+            >>> metric = PSNR()
+            >>> metric(pred, target)
+            tensor(2.5527)
+        """
+        super().__init__(name='psnr')
+        self.data_range = data_range
+        self.base = float(base)
+        self.reduction = reduction
+
+    def forward(self, pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
+        return psnr(pred, target, self.data_range, self.base, self.reduction)


Test patch:
diff --git a/requirements/test.txt b/requirements/test.txt
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -7,7 +7,7 @@ flake8
 flake8-black
 check-manifest
 twine==1.13.0
-
+scikit-image
 black==19.10b0
 pre-commit>=1.0
 
diff --git a/tests/metrics/functional/test_regression.py b/tests/metrics/functional/test_regression.py
new file mode 100644
--- /dev/null
+++ b/tests/metrics/functional/test_regression.py
@@ -0,0 +1,37 @@
+import pytest
+import torch
+
+from skimage.metrics import peak_signal_noise_ratio as ski_psnr
+from pytorch_lightning.metrics.functional.regression import psnr
+
+
+@pytest.mark.parametrize(['sklearn_metric', 'torch_metric'], [
+    pytest.param(ski_psnr, psnr, id='peak_signal_noise_ratio')
+])
+def test_psnr_against_sklearn(sklearn_metric, torch_metric):
+    """Compare PL metrics to sklearn version."""
+    device = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+    pred = torch.randint(10, (500,), device=device, dtype=torch.double)
+    target = torch.randint(10, (500,), device=device, dtype=torch.double)
+    assert torch.allclose(
+        torch.tensor(sklearn_metric(target.cpu().detach().numpy(),
+                                    pred.cpu().detach().numpy(),
+                                    data_range=10), dtype=torch.double, device=device),
+        torch_metric(pred, target, data_range=10))
+
+    pred = torch.randint(5, (500,), device=device, dtype=torch.double)
+    target = torch.randint(10, (500,), device=device, dtype=torch.double)
+    assert torch.allclose(
+        torch.tensor(sklearn_metric(target.cpu().detach().numpy(),
+                                    pred.cpu().detach().numpy(),
+                                    data_range=10), dtype=torch.double, device=device),
+        torch_metric(pred, target, data_range=10))
+
+    pred = torch.randint(10, (500,), device=device, dtype=torch.double)
+    target = torch.randint(5, (500,), device=device, dtype=torch.double)
+    assert torch.allclose(
+        torch.tensor(sklearn_metric(target.cpu().detach().numpy(),
+                                    pred.cpu().detach().numpy(),
+                                    data_range=5), dtype=torch.double, device=device),
+        torch_metric(pred, target, data_range=5))
diff --git a/tests/metrics/test_regression.py b/tests/metrics/test_regression.py
--- a/tests/metrics/test_regression.py
+++ b/tests/metrics/test_regression.py
@@ -1,8 +1,10 @@
 import pytest
 import torch
+from skimage.metrics import peak_signal_noise_ratio as ski_psnr
+import numpy as np
 
 from pytorch_lightning.metrics.regression import (
-    MAE, MSE, RMSE, RMSLE
+    MAE, MSE, RMSE, RMSLE, PSNR
 )
 
 
@@ -64,3 +66,45 @@ def test_rmsle(pred, target, exp):
 
     assert isinstance(score, torch.Tensor)
     assert pytest.approx(score.item(), rel=1e-3) == exp
+
+
+@pytest.mark.parametrize(['pred', 'target', 'exp'], [
+    pytest.param(
+        [0., 1., 2., 3.],
+        [0., 1., 2., 2.],
+        ski_psnr(np.array([0., 1., 2., 3.]), np.array([0., 1., 2., 2.]), data_range=3)
+    ),
+    pytest.param(
+        [4., 3., 2., 1.],
+        [1., 4., 3., 2.],
+        ski_psnr(np.array([4., 3., 2., 1.]), np.array([1., 4., 3., 2.]), data_range=3)
+    )
+])
+def test_psnr(pred, target, exp):
+    psnr = PSNR()
+    assert psnr.name == 'psnr'
+    score = psnr(pred=torch.tensor(pred),
+                 target=torch.tensor(target))
+    assert isinstance(score, torch.Tensor)
+    assert pytest.approx(score.item(), rel=1e-3) == exp
+
+
+@pytest.mark.parametrize(['pred', 'target', 'exp'], [
+    pytest.param(
+        [0., 1., 2., 3.],
+        [0., 1., 2., 2.],
+        ski_psnr(np.array([0., 1., 2., 3.]), np.array([0., 1., 2., 2.]), data_range=4) * np.log(10)
+    ),
+    pytest.param(
+        [4., 3., 2., 1.],
+        [1., 4., 3., 2.],
+        ski_psnr(np.array([4., 3., 2., 1.]), np.array([1., 4., 3., 2.]), data_range=4) * np.log(10)
+    )
+])
+def test_psnr_base_e_wider_range(pred, target, exp):
+    psnr = PSNR(data_range=4, base=2.718281828459045)
+    assert psnr.name == 'psnr'
+    score = psnr(pred=torch.tensor(pred),
+                 target=torch.tensor(target))
+    assert isinstance(score, torch.Tensor)
+    assert pytest.approx(score.item(), rel=1e-3) == exp