-
-
Notifications
You must be signed in to change notification settings - Fork 719
Add MRR (Mean Reciprocal Rank) metric to rec_sys #3573
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
Open
steaphenai
wants to merge
19
commits into
pytorch:master
Choose a base branch
from
steaphenai:feat/mrr-metric-steaphen
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
a274b49
Add MRR (Mean Reciprocal Rank) metric to rec_sys
steaphenai 99c37eb
Fix missing newline at end of file
steaphenai 91346b6
Add ranx to requirements-dev.txt and add package link in description
steaphenai e154035
Add ranx to requirements-dev.txt and add package link in description
steaphenai 080636b
Merge branch 'pytorch:master' into feat/mrr-metric-steaphen
steaphenai 215a174
Add NDCG metric to rec_sys
steaphenai e910463
Added MRR to metrics.rst
steaphenai 8aeaa32
Delete tests/ignite/metrics/rec_sys/test_ndcg.py
steaphenai 516c250
Delete ignite/metrics/rec_sys/ndcg.py
steaphenai 5257309
Remove NDCG from metrics export list
steaphenai 6b965cb
Remove NDCG import from rec_sys module
steaphenai 5ce01b6
Remove NDCG import from metrics
steaphenai 9f59b3e
Merge branch 'master' into feat/mrr-metric-steaphen
steaphenai bbf121f
Merge branch 'master' into feat/mrr-metric-steaphen
steaphenai 2d5d9dd
Merge upstream/master into feat/mrr-metric-steaphen
steaphenai 8113af7
Merge branch 'master' into feat/mrr-metric-steaphen
steaphenai fa0570b
Update ignite/metrics/rec_sys/mrr.py
steaphenai de451d7
Merge branch 'master' into feat/mrr-metric-steaphen
steaphenai 5113d0b
Address PR review feedback for MRR metric
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| from ignite.metrics.rec_sys.hitrate import HitRate | ||
| from ignite.metrics.rec_sys.mrr import MRR |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| from typing import Callable | ||
|
|
||
| import torch | ||
|
|
||
| from ignite.exceptions import NotComputableError | ||
| from ignite.metrics.metric import Metric, reinit__is_reduced, sync_all_reduce | ||
|
|
||
| __all__ = ["MRR"] | ||
|
|
||
|
|
||
| class MRR(Metric): | ||
| r"""Calculates the Mean Reciprocal Rank (MRR) at `k` for Recommendation Systems. | ||
|
|
||
| MRR measures the average of the reciprocal of the rank of the first relevant item | ||
| in the predicted list. It is widely used in retrieval systems, recommendation systems, | ||
| and RAG pipelines. | ||
|
|
||
| .. math:: \text{MRR}@K = \frac{1}{N} \sum_{i=1}^{N} \frac{1}{\text{rank}_i} | ||
|
|
||
| where :math:`\text{rank}_i` is the rank (1-indexed) of the first relevant item | ||
| in the top-K predictions for user :math:`i`. If no relevant item is found in the | ||
| top-K, the reciprocal rank for that user is 0. | ||
|
|
||
| - ``update`` must receive output of the form ``(y_pred, y)``. | ||
| - ``y_pred`` is expected to be raw logits or probability score for each item in the catalog. | ||
| - ``y`` is expected to be binary (only 0s and 1s) values where `1` indicates relevant item. | ||
| Graded relevance labels are also supported via ``relevance_threshold``. | ||
| - ``y_pred`` and ``y`` are only allowed shape :math:`(batch, num\_items)`. | ||
| - returns a list of MRR ordered by the sorted values of ``top_k``. | ||
|
|
||
| Args: | ||
| top_k: a list of sorted positive integers that specifies `k` for calculating MRR@top-k. | ||
| ignore_zero_hits: if True, users with no relevant items (ground truth tensor being all zeros) | ||
| are ignored in computation of MRR. If set False, such users are counted as having | ||
| reciprocal rank of 0. By default, True. | ||
| relevance_threshold: minimum label value to be considered relevant. Defaults to ``1``, | ||
| which handles standard binary labels and graded relevance scales (e.g. TREC-style | ||
| 0-4) by treating any label >= 1 as relevant. | ||
| output_transform: a callable that is used to transform the | ||
| :class:`~ignite.engine.engine.Engine`'s ``process_function``'s output into the | ||
| form expected by the metric. | ||
| The output is expected to be a tuple `(prediction, target)` | ||
| where `prediction` and `target` are tensors | ||
| of shape ``(batch, num_items)``. | ||
| device: specifies which device updates are accumulated on. Setting the | ||
| metric's device to be the same as your ``update`` arguments ensures the ``update`` method is | ||
| non-blocking. By default, CPU. | ||
| skip_unrolling: specifies whether input should be unrolled or not before being | ||
| processed. Should be true for multi-output models.. | ||
|
|
||
| Examples: | ||
| To use with ``Engine`` and ``process_function``, simply attach the metric instance to the engine. | ||
| The output of the engine's ``process_function`` needs to be in the format of | ||
| ``(y_pred, y)``. If not, ``output_tranform`` can be added | ||
| to the metric to transform the output into the form expected by the metric. | ||
|
|
||
| For more information on how metric works with :class:`~ignite.engine.engine.Engine`, visit :ref:`attach-engine`. | ||
|
|
||
| .. include:: defaults.rst | ||
| :start-after: :orphan: | ||
|
|
||
| ignore_zero_hits=True case | ||
|
|
||
| .. testcode:: 1 | ||
|
|
||
| metric = MRR(top_k=[1, 2, 3, 4]) | ||
| metric.attach(default_evaluator,"mrr") | ||
| y_pred=torch.Tensor([ | ||
| [4.0, 2.0, 3.0, 1.0], | ||
| [1.0, 2.0, 3.0, 4.0] | ||
| ]) | ||
| y_true=torch.Tensor([ | ||
| [0.0, 0.0, 1.0, 1.0], | ||
| [0.0, 0.0, 0.0, 0.0] | ||
| ]) | ||
| state = default_evaluator.run([(y_pred, y_true)]) | ||
| print(state.metrics["mrr"]) | ||
|
|
||
| .. testoutput:: 1 | ||
|
|
||
| [0.0, 0.5, 0.5, 0.5] | ||
|
|
||
| ignore_zero_hits=False case | ||
|
|
||
| .. testcode:: 2 | ||
|
|
||
| metric = MRR(top_k=[1, 2, 3, 4], ignore_zero_hits=False) | ||
| metric.attach(default_evaluator,"mrr") | ||
| y_pred=torch.Tensor([ | ||
| [4.0, 2.0, 3.0, 1.0], | ||
| [1.0, 2.0, 3.0, 4.0] | ||
| ]) | ||
| y_true=torch.Tensor([ | ||
| [0.0, 0.0, 1.0, 1.0], | ||
| [0.0, 0.0, 0.0, 0.0] | ||
| ]) | ||
| state = default_evaluator.run([(y_pred, y_true)]) | ||
| print(state.metrics["mrr"]) | ||
|
|
||
| .. testoutput:: 2 | ||
|
|
||
| [0.0, 0.25, 0.25, 0.25] | ||
|
|
||
| .. versionadded:: 0.6.0 | ||
| """ | ||
|
|
||
| required_output_keys = ("y_pred", "y") | ||
| _state_dict_all_req_keys = ("_sum_reciprocal_ranks_per_k", "_num_examples") | ||
|
|
||
| def __init__( | ||
| self, | ||
| top_k: list[int], | ||
| ignore_zero_hits: bool = True, | ||
| relevance_threshold: float = 1.0, | ||
| output_transform: Callable = lambda x: x, | ||
| device: str | torch.device = torch.device("cpu"), | ||
| skip_unrolling: bool = False, | ||
| ): | ||
| if any(k <= 0 for k in top_k): | ||
| raise ValueError(" top_k must be list of positive integers only.") | ||
|
|
||
| self.top_k = sorted(top_k) | ||
| self.ignore_zero_hits = ignore_zero_hits | ||
| self.relevance_threshold = relevance_threshold | ||
| super(MRR, self).__init__(output_transform, device=device, skip_unrolling=skip_unrolling) | ||
|
|
||
| @reinit__is_reduced | ||
| def reset(self) -> None: | ||
| self._sum_reciprocal_ranks_per_k = torch.zeros(len(self.top_k), device=self._device) | ||
| self._num_examples = 0 | ||
|
|
||
| @reinit__is_reduced | ||
| def update(self, output: tuple[torch.Tensor, torch.Tensor]) -> None: | ||
| if len(output) != 2: | ||
| raise ValueError(f"output should be in format `(y_pred,y)` but got tuple of {len(output)} tensors.") | ||
|
|
||
| y_pred, y = output | ||
|
steaphenai marked this conversation as resolved.
Outdated
|
||
| if y_pred.shape != y.shape: | ||
| raise ValueError(f"y_pred and y must be in the same shape, got {y_pred.shape} != {y.shape}.") | ||
|
|
||
| if self.ignore_zero_hits: | ||
| valid_mask = torch.any(y >= self.relevance_threshold, dim=-1) | ||
| y_pred = y_pred[valid_mask] | ||
| y = y[valid_mask] | ||
|
|
||
| if y.shape[0] == 0: | ||
|
steaphenai marked this conversation as resolved.
Outdated
|
||
| return | ||
|
|
||
| max_k = self.top_k[-1] | ||
|
|
||
| # stable=True ensures deterministic tie-breaking, consistent with | ||
| # reference libraries such as ranx. | ||
| ranked_indices = torch.argsort(y_pred, dim=-1, descending=True, stable=True)[:, :max_k] | ||
|
steaphenai marked this conversation as resolved.
Outdated
|
||
| ranked_labels = torch.gather(y, dim=-1, index=ranked_indices) | ||
|
|
||
| for i, k in enumerate(self.top_k): | ||
| top_k_labels = ranked_labels[:, :k] | ||
| relevant_mask = top_k_labels >= self.relevance_threshold | ||
|
|
||
| has_hit = relevant_mask.any(dim=-1) | ||
|
|
||
| # argmax on int tensor returns 0-based position of first True | ||
| first_hit_pos = relevant_mask.int().argmax(dim=-1) | ||
|
|
||
| reciprocal_rank = torch.where( | ||
| has_hit, | ||
| 1.0 / (first_hit_pos.float() + 1.0), | ||
| torch.zeros_like(first_hit_pos, dtype=torch.float), | ||
| ) | ||
|
steaphenai marked this conversation as resolved.
|
||
|
|
||
| self._sum_reciprocal_ranks_per_k[i] += reciprocal_rank.sum().to(self._device) | ||
|
|
||
| self._num_examples += y.shape[0] | ||
|
|
||
| @sync_all_reduce("_sum_reciprocal_ranks_per_k", "_num_examples") | ||
| def compute(self) -> list[float]: | ||
| if self._num_examples == 0: | ||
| raise NotComputableError("MRR must have at least one example.") | ||
|
|
||
| rates = (self._sum_reciprocal_ranks_per_k / self._num_examples).tolist() | ||
| return rates | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.