Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the ndcg metric [WIP] #2632

Draft
wants to merge 38 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cd6ec7f
Added the ndcg metric [WIP]
kamalojasv181 Jul 23, 2022
6421879
Merge branch 'master' into ndcg
kamalojasv181 Jul 23, 2022
4535af1
added GPU support, corrected mypy errors, and minor fixes
kamalojasv181 Jul 23, 2022
eb73c99
Merge branch 'ndcg' of https://github.com/kamalojasv181/ignite into ndcg
kamalojasv181 Jul 23, 2022
70d06e5
Incorporated the suggested changes
kamalojasv181 Jul 24, 2022
6a86f5f
Fixed mypy error
kamalojasv181 Jul 24, 2022
7b7ed6f
Fixed bugs in NDCG and added tests for output and reset
kamalojasv181 Jul 24, 2022
2c87ee1
Fixed mypy error
kamalojasv181 Jul 24, 2022
f4c628a
Added the exponential form on https://en.wikipedia.org/wiki/Discounte…
kamalojasv181 Jul 25, 2022
e72b59e
Corrected true, pred order and corresponding tests
kamalojasv181 Jul 25, 2022
ef63d85
Added ties case, exponential tests, log_base tests, corresponding tes…
kamalojasv181 Jul 26, 2022
115501b
Added GPU check on top
kamalojasv181 Jul 26, 2022
189b579
Put tensors on GPU inside the function to pervent error
kamalojasv181 Jul 26, 2022
c509456
Improved tests and minor bugfixes
kamalojasv181 Jul 27, 2022
84900f0
Removed device hyperparam from _ndcg_smaple_scores
kamalojasv181 Jul 27, 2022
9bfc06e
Skipped GPU tests for CPU only systems
kamalojasv181 Jul 27, 2022
477e096
Changed Error message
kamalojasv181 Jul 27, 2022
44329d7
Merge branch 'master' of https://github.com/pytorch/ignite into ndcg
kamalojasv181 Aug 27, 2022
5ba7fb7
Merge branch 'pytorch:master' into ndcg
kamalojasv181 Aug 27, 2022
ac800ff
Made tests randomised from deterministic and introduced ignore_ties_f…
kamalojasv181 Aug 27, 2022
691e89a
Merge branch 'ndcg' of https://github.com/kamalojasv181/ignite into ndcg
kamalojasv181 Aug 27, 2022
962bcef
Merge branch 'master' of https://github.com/pytorch/ignite into ndcg
kamalojasv181 Aug 29, 2022
79979cc
Merge branch 'pytorch:master' into ndcg
kamalojasv181 Aug 29, 2022
0c1d6fd
Changed test name to test_output_cuda from test_output_gpu
kamalojasv181 Aug 29, 2022
85cdcaf
Merge branch 'ndcg' of https://github.com/kamalojasv181/ignite into ndcg
kamalojasv181 Aug 29, 2022
fdf7877
Merge branch 'master' into ndcg
kamalojasv181 Aug 29, 2022
2931d20
Changed variable names to replacement and ignore_ties and removed red…
kamalojasv181 Aug 30, 2022
c308e41
Changed variable names to replacement and ignore_ties and removed red…
kamalojasv181 Aug 30, 2022
95ede6c
Merge branch 'master' of https://github.com/pytorch/ignite into ndcg
kamalojasv181 Aug 30, 2022
3a4d2af
Merge branch 'ndcg' of https://github.com/kamalojasv181/ignite into ndcg
kamalojasv181 Aug 30, 2022
6e66273
Removed redundant test cases and removed the redundant if statement
kamalojasv181 Aug 30, 2022
eb75afa
Added distributed tests, added multiple test cases corresponding to o…
kamalojasv181 Aug 30, 2022
dcf276d
Made the tests wsork on in ddp configuration
kamalojasv181 Aug 31, 2022
cb273e7
Merge branch 'master' of https://github.com/pytorch/ignite into ndcg
kamalojasv181 Aug 31, 2022
b0f449b
Merge branch 'pytorch:master' into ndcg
kamalojasv181 Aug 31, 2022
388db23
Merge branch 'ndcg' of https://github.com/kamalojasv181/ignite into ndcg
kamalojasv181 Aug 31, 2022
b3b6b28
Merge branch 'pytorch:master' into ndcg
kamalojasv181 Sep 1, 2022
6dcf3b2
Returning tuple of two tensors instead of tuple of list of tensors
kamalojasv181 Sep 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ignite/metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ignite.metrics.precision import Precision
from ignite.metrics.psnr import PSNR
from ignite.metrics.recall import Recall
from ignite.metrics.recsys.ndcg import NDCG
from ignite.metrics.root_mean_squared_error import RootMeanSquaredError
from ignite.metrics.running_average import RunningAverage
from ignite.metrics.ssim import SSIM
Expand Down
5 changes: 5 additions & 0 deletions ignite/metrics/recsys/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ignite.metrics.recsys.ndcg import NDCG

__all__ = [
"NDCG",
]
73 changes: 73 additions & 0 deletions ignite/metrics/recsys/ndcg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from typing import Callable, Optional, Sequence, Union

import torch

from ignite.exceptions import NotComputableError
from ignite.metrics.metric import Metric

__all__ = ["NDCG"]


class NDCG(Metric):
def __init__(
self,
output_transform: Callable = lambda x: x,
device: Union[str, torch.device] = torch.device("cpu"),
k: Optional[int] = None,
log_base: Union[int, float] = 2,
exponential: bool = False,
):

self.log_base = log_base
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
self.k = k
self.exponential = exponential
super(NDCG, self).__init__(output_transform=output_transform, device=device)

def _dcg_sample_scores(
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
self, y_true: torch.Tensor, y_score: torch.Tensor, k: Optional[int] = None, log_base: Union[int, float] = 2
) -> torch.Tensor:

discount = (
torch.div(torch.log(torch.arange(y_true.shape[1]) + 2), torch.log(torch.tensor(log_base)))
.pow(-1)
.to(self._device)
)
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
if k is not None:
discount[k:] = 0.0

ranking = torch.argsort(y_score, descending=True)
ranked = y_true[torch.arange(ranking.shape[0]).reshape(-1, 1), ranking].to(self._device)

if self.exponential:
ranked = ranked.pow(2) - 1

discounted_gains = torch.mm(ranked, discount.reshape(-1, 1))
return discounted_gains

def _ndcg_sample_scores(
self, y_true: torch.Tensor, y_score: torch.Tensor, k: Optional[int] = None, log_base: Union[int, float] = 2
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
) -> torch.Tensor:

gain = self._dcg_sample_scores(y_true, y_score, k, log_base=log_base)
normalizing_gain = self._dcg_sample_scores(y_true, y_true, k, log_base=log_base)
all_relevant = normalizing_gain != 0
normalized_gain = torch.div(gain[all_relevant], normalizing_gain[all_relevant])
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
return normalized_gain

def reset(self) -> None:

self.num_examples = 0
self.ndcg = torch.tensor(0.0, device=self._device)

def update(self, output: Sequence[torch.Tensor]) -> None:

y, y_pred = output[0].detach(), output[1].detach()
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
gain = self._ndcg_sample_scores(y, y_pred, k=self.k, log_base=self.log_base)
self.ndcg = torch.add(self.ndcg, torch.sum(gain))
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
self.num_examples += y_pred.shape[0]

def compute(self) -> float:
if self.num_examples == 0:
raise NotComputableError("NGCD must have at least one example before it can be computed.")

return (self.ndcg / self.num_examples).item()
67 changes: 67 additions & 0 deletions tests/ignite/metrics/test_ndcg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import numpy as np
import pytest
import torch
from sklearn.metrics import ndcg_score

from ignite.exceptions import NotComputableError
from ignite.metrics.recsys.ndcg import NDCG


@pytest.mark.parametrize(
"true_score, pred_score",
[
(torch.tensor([[1.0, 2.0, 3.0, 4.0, 5.0]]), torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5]])),
(torch.tensor([[2.9, 5.6, 3.8, 7.9, 6.2]]), torch.tensor([[3.7, 4.8, 3.9, 4.3, 4.9]])),
(
torch.tensor([[1.0, 2.0, 3.0, 4.0, 5.0], [1.2, 4.5, 8.9, 5.6, 7.2], [2.9, 5.6, 3.8, 7.9, 6.2]]),
torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5], [3.7, 4.8, 3.9, 4.3, 4.9], [3.7, 4.8, 3.9, 4.3, 4.9]]),
),
],
)
@pytest.mark.parametrize("k", [None, 2, 3])
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
def test_output_cpu(true_score, pred_score, k):

device = "cpu"

ndcg = NDCG(k=k, device=device)
ndcg.update([true_score, pred_score])
result_ignite = ndcg.compute()
result_sklearn = ndcg_score(true_score.numpy(), pred_score.numpy(), k=k)

np.testing.assert_allclose(np.array(result_ignite), result_sklearn, rtol=2e-7)


@pytest.mark.parametrize(
"true_score, pred_score",
[
(torch.tensor([[1.0, 2.0, 3.0, 4.0, 5.0]]), torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5]])),
(torch.tensor([[2.9, 5.6, 3.8, 7.9, 6.2]]), torch.tensor([[3.7, 4.8, 3.9, 4.3, 4.9]])),
(
torch.tensor([[1.0, 2.0, 3.0, 4.0, 5.0], [1.2, 4.5, 8.9, 5.6, 7.2], [2.9, 5.6, 3.8, 7.9, 6.2]]),
torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5], [3.7, 4.8, 3.9, 4.3, 4.9], [3.7, 4.8, 3.9, 4.3, 4.9]]),
),
],
)
@pytest.mark.parametrize("k", [None, 2, 3])
@pytest.mark.skipif(torch.cuda.device_count() < 1, reason="Skip if no GPU")
def test_output_gpu(true_score, pred_score, k):

device = "cuda"
ndcg = NDCG(k=k, device=device)
ndcg.update([true_score, pred_score])
vfdev-5 marked this conversation as resolved.
Show resolved Hide resolved
result_ignite = ndcg.compute()
result_sklearn = ndcg_score(true_score.numpy(), pred_score.numpy(), k=k)

np.testing.assert_allclose(np.array(result_ignite), result_sklearn, rtol=2e-7)


def test_reset():

true_score = torch.tensor([[1.0, 2.0, 3.0, 4.0, 5.0]])
pred_score = torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5]])
ndcg = NDCG()
ndcg.update([true_score, pred_score])
ndcg.reset()

with pytest.raises(NotComputableError, match=r"NGCD must have at least one example before it can be computed."):
ndcg.compute()