base.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. # Copyright The Lightning team.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from abc import ABC, abstractmethod
  15. from typing import Any, Callable, List, Optional, Union
  16. import torch
  17. from torch import Tensor, tensor
  18. from typing_extensions import Literal
  19. from torchmetrics import Metric
  20. from torchmetrics.utilities.checks import _check_retrieval_inputs
  21. from torchmetrics.utilities.data import _flexible_bincount, dim_zero_cat
  22. def _retrieval_aggregate(
  23. values: Tensor,
  24. aggregation: Union[Literal["mean", "median", "min", "max"], Callable] = "mean",
  25. dim: Optional[int] = None,
  26. ) -> Tensor:
  27. """Aggregate the final retrieval values into a single value."""
  28. if aggregation == "mean":
  29. return values.mean() if dim is None else values.mean(dim=dim)
  30. if aggregation == "median":
  31. return values.median() if dim is None else values.median(dim=dim).values
  32. if aggregation == "min":
  33. return values.min() if dim is None else values.min(dim=dim).values
  34. if aggregation == "max":
  35. return values.max() if dim is None else values.max(dim=dim).values
  36. return aggregation(values, dim=dim)
  37. class RetrievalMetric(Metric, ABC):
  38. """Works with binary target data. Accepts float predictions from a model output.
  39. As input to ``forward`` and ``update`` the metric accepts the following input:
  40. - ``preds`` (:class:`~torch.Tensor`): A float tensor of shape ``(N, ...)``
  41. - ``target`` (:class:`~torch.Tensor`): A long or bool tensor of shape ``(N, ...)``
  42. - ``indexes`` (:class:`~torch.Tensor`): A long tensor of shape ``(N, ...)`` which indicate to which query a
  43. prediction belongs
  44. .. hint::
  45. The ``indexes``, ``preds`` and ``target`` must have the same dimension and will be flattened
  46. to single dimension once provided.
  47. .. attention::
  48. Predictions will be first grouped by ``indexes`` and then the real metric, defined by overriding
  49. the `_metric` method, will be computed as the mean of the scores over each query.
  50. As output to ``forward`` and ``compute`` the metric returns the following output:
  51. - ``metric`` (:class:`~torch.Tensor`): A tensor as computed by ``_metric`` if the number of positive targets is
  52. at least 1, otherwise behave as specified by ``self.empty_target_action``.
  53. Args:
  54. empty_target_action:
  55. Specify what to do with queries that do not have at least a positive
  56. or negative (depend on metric) target. Choose from:
  57. - ``'neg'``: those queries count as ``0.0`` (default)
  58. - ``'pos'``: those queries count as ``1.0``
  59. - ``'skip'``: skip those queries; if all queries are skipped, ``0.0`` is returned
  60. - ``'error'``: raise a ``ValueError``
  61. ignore_index:
  62. Ignore predictions where the target is equal to this number.
  63. aggregation:
  64. Specify how to aggregate over indexes. Can either a custom callable function that takes in a single tensor
  65. and returns a scalar value or one of the following strings:
  66. - ``'mean'``: average value is returned
  67. - ``'median'``: median value is returned
  68. - ``'max'``: max value is returned
  69. - ``'min'``: min value is returned
  70. kwargs: Additional keyword arguments, see :ref:`Metric kwargs` for more info.
  71. Raises:
  72. ValueError:
  73. If ``empty_target_action`` is not one of ``error``, ``skip``, ``neg`` or ``pos``.
  74. ValueError:
  75. If ``ignore_index`` is not `None` or an integer.
  76. """
  77. is_differentiable: bool = False
  78. higher_is_better: bool = True
  79. full_state_update: bool = False
  80. indexes: List[Tensor]
  81. preds: List[Tensor]
  82. target: List[Tensor]
  83. def __init__(
  84. self,
  85. empty_target_action: str = "neg",
  86. ignore_index: Optional[int] = None,
  87. aggregation: Union[Literal["mean", "median", "min", "max"], Callable] = "mean",
  88. **kwargs: Any,
  89. ) -> None:
  90. super().__init__(**kwargs)
  91. self.allow_non_binary_target = False
  92. empty_target_action_options = ("error", "skip", "neg", "pos")
  93. if empty_target_action not in empty_target_action_options:
  94. raise ValueError(f"Argument `empty_target_action` received a wrong value `{empty_target_action}`.")
  95. self.empty_target_action = empty_target_action
  96. if ignore_index is not None and not isinstance(ignore_index, int):
  97. raise ValueError("Argument `ignore_index` must be an integer or None.")
  98. self.ignore_index = ignore_index
  99. if not (aggregation in ("mean", "median", "min", "max") or callable(aggregation)):
  100. raise ValueError(
  101. "Argument `aggregation` must be one of `mean`, `median`, `min`, `max` or a custom callable function"
  102. f"which takes tensor of values, but got {aggregation}."
  103. )
  104. self.aggregation = aggregation
  105. self.add_state("indexes", default=[], dist_reduce_fx=None)
  106. self.add_state("preds", default=[], dist_reduce_fx=None)
  107. self.add_state("target", default=[], dist_reduce_fx=None)
  108. def update(self, preds: Tensor, target: Tensor, indexes: Tensor) -> None:
  109. """Check shape, check and convert dtypes, flatten and add to accumulators."""
  110. if indexes is None:
  111. raise ValueError("Argument `indexes` cannot be None")
  112. indexes, preds, target = _check_retrieval_inputs(
  113. indexes, preds, target, allow_non_binary_target=self.allow_non_binary_target, ignore_index=self.ignore_index
  114. )
  115. self.indexes.append(indexes)
  116. self.preds.append(preds)
  117. self.target.append(target)
  118. def compute(self) -> Tensor:
  119. """First concat state ``indexes``, ``preds`` and ``target`` since they were stored as lists.
  120. After that, compute list of groups that will help in keeping together predictions about the same query. Finally,
  121. for each group compute the ``_metric`` if the number of positive targets is at least 1, otherwise behave as
  122. specified by ``self.empty_target_action``.
  123. """
  124. indexes = dim_zero_cat(self.indexes)
  125. preds = dim_zero_cat(self.preds)
  126. target = dim_zero_cat(self.target)
  127. indexes, indices = torch.sort(indexes)
  128. preds = preds[indices]
  129. target = target[indices]
  130. split_sizes = _flexible_bincount(indexes).detach().cpu().tolist()
  131. res = []
  132. for mini_preds, mini_target in zip(
  133. torch.split(preds, split_sizes, dim=0), torch.split(target, split_sizes, dim=0)
  134. ):
  135. if not mini_target.sum():
  136. if self.empty_target_action == "error":
  137. raise ValueError("`compute` method was provided with a query with no positive target.")
  138. if self.empty_target_action == "pos":
  139. res.append(tensor(1.0))
  140. elif self.empty_target_action == "neg":
  141. res.append(tensor(0.0))
  142. else:
  143. # ensure list contains only float tensors
  144. res.append(self._metric(mini_preds, mini_target))
  145. if res:
  146. return _retrieval_aggregate(torch.stack([x.to(preds) for x in res]), self.aggregation)
  147. return tensor(0.0).to(preds)
  148. @abstractmethod
  149. def _metric(self, preds: Tensor, target: Tensor) -> Tensor:
  150. """Compute a metric over a predictions and target of a single group.
  151. This method should be overridden by subclasses.
  152. """