group_fairness.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. # Copyright The PyTorch 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 typing import Optional
  15. import torch
  16. from typing_extensions import Literal
  17. from torchmetrics.functional.classification.stat_scores import (
  18. _binary_stat_scores_arg_validation,
  19. _binary_stat_scores_format,
  20. _binary_stat_scores_tensor_validation,
  21. _binary_stat_scores_update,
  22. )
  23. from torchmetrics.utilities import rank_zero_warn
  24. from torchmetrics.utilities.compute import _safe_divide
  25. from torchmetrics.utilities.data import _flexible_bincount
  26. def _groups_validation(groups: torch.Tensor, num_groups: int) -> None:
  27. """Validate groups tensor.
  28. - The largest number in the tensor should not be larger than the number of groups. The group identifiers should
  29. be ``0, 1, ..., (num_groups - 1)``.
  30. - The group tensor should be dtype long.
  31. """
  32. if torch.max(groups) > num_groups:
  33. raise ValueError(
  34. f"The largest number in the groups tensor is {torch.max(groups)}, which is larger than the specified",
  35. f"number of groups {num_groups}. The group identifiers should be ``0, 1, ..., (num_groups - 1)``.",
  36. )
  37. if groups.dtype != torch.long:
  38. raise ValueError(f"Expected dtype of argument groups to be long, not {groups.dtype}.")
  39. def _groups_format(groups: torch.Tensor) -> torch.Tensor:
  40. """Reshape groups to correspond to preds and target."""
  41. return groups.reshape(groups.shape[0], -1)
  42. def _binary_groups_stat_scores(
  43. preds: torch.Tensor,
  44. target: torch.Tensor,
  45. groups: torch.Tensor,
  46. num_groups: int,
  47. threshold: float = 0.5,
  48. ignore_index: Optional[int] = None,
  49. validate_args: bool = True,
  50. ) -> list[tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]]:
  51. """Compute the true/false positives and true/false negatives rates for binary classification by group.
  52. Related to `Type I and Type II errors`_.
  53. """
  54. if validate_args:
  55. _binary_stat_scores_arg_validation(threshold, "global", ignore_index)
  56. _binary_stat_scores_tensor_validation(preds, target, "global", ignore_index)
  57. _groups_validation(groups, num_groups)
  58. preds, target = _binary_stat_scores_format(preds, target, threshold, ignore_index)
  59. groups = _groups_format(groups)
  60. indexes, indices = torch.sort(groups.squeeze(1))
  61. preds = preds[indices]
  62. target = target[indices]
  63. split_sizes = _flexible_bincount(indexes).detach().cpu().tolist()
  64. group_preds = list(torch.split(preds, split_sizes, dim=0))
  65. group_target = list(torch.split(target, split_sizes, dim=0))
  66. return [_binary_stat_scores_update(group_p, group_t) for group_p, group_t in zip(group_preds, group_target)]
  67. def _groups_reduce(
  68. group_stats: list[tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]],
  69. ) -> dict[str, torch.Tensor]:
  70. """Compute rates for all the group statistics."""
  71. return {f"group_{group}": torch.stack(stats) / torch.stack(stats).sum() for group, stats in enumerate(group_stats)}
  72. def _groups_stat_transform(
  73. group_stats: list[tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]],
  74. ) -> dict[str, torch.Tensor]:
  75. """Transform group statistics by creating a tensor for each statistic."""
  76. return {
  77. "tp": torch.stack([stat[0] for stat in group_stats]),
  78. "fp": torch.stack([stat[1] for stat in group_stats]),
  79. "tn": torch.stack([stat[2] for stat in group_stats]),
  80. "fn": torch.stack([stat[3] for stat in group_stats]),
  81. }
  82. def binary_groups_stat_rates(
  83. preds: torch.Tensor,
  84. target: torch.Tensor,
  85. groups: torch.Tensor,
  86. num_groups: int,
  87. threshold: float = 0.5,
  88. ignore_index: Optional[int] = None,
  89. validate_args: bool = True,
  90. ) -> dict[str, torch.Tensor]:
  91. r"""Compute the true/false positives and true/false negatives rates for binary classification by group.
  92. Related to `Type I and Type II errors`_.
  93. Accepts the following input tensors:
  94. - ``preds`` (int or float tensor): ``(N, ...)``. If preds is a floating point tensor with values outside
  95. [0,1] range we consider the input to be logits and will auto apply sigmoid per element. Additionally,
  96. we convert to int tensor with thresholding using the value in ``threshold``.
  97. - ``target`` (int tensor): ``(N, ...)``.
  98. - ``groups`` (int tensor): ``(N, ...)``. The group identifiers should be ``0, 1, ..., (num_groups - 1)``.
  99. The additional dimensions are flatted along the batch dimension.
  100. Args:
  101. preds: Tensor with predictions.
  102. target: Tensor with true labels.
  103. groups: Tensor with group identifiers. The group identifiers should be ``0, 1, ..., (num_groups - 1)``.
  104. num_groups: The number of groups.
  105. threshold: Threshold for transforming probability to binary {0,1} predictions.
  106. ignore_index:
  107. Specifies a target value that is ignored and does not contribute to the metric calculation
  108. validate_args: bool indicating if input arguments and tensors should be validated for correctness.
  109. Set to ``False`` for faster computations.
  110. Returns:
  111. The metric returns a dict with a group identifier as key and a tensor with the tp, fp, tn and fn rates as value.
  112. Example (preds is int tensor):
  113. >>> from torchmetrics.functional.classification import binary_groups_stat_rates
  114. >>> target = torch.tensor([0, 1, 0, 1, 0, 1])
  115. >>> preds = torch.tensor([0, 1, 0, 1, 0, 1])
  116. >>> groups = torch.tensor([0, 1, 0, 1, 0, 1])
  117. >>> binary_groups_stat_rates(preds, target, groups, 2)
  118. {'group_0': tensor([0., 0., 1., 0.]), 'group_1': tensor([1., 0., 0., 0.])}
  119. Example (preds is float tensor):
  120. >>> from torchmetrics.functional.classification import binary_groups_stat_rates
  121. >>> target = torch.tensor([0, 1, 0, 1, 0, 1])
  122. >>> preds = torch.tensor([0.11, 0.84, 0.22, 0.73, 0.33, 0.92])
  123. >>> groups = torch.tensor([0, 1, 0, 1, 0, 1])
  124. >>> binary_groups_stat_rates(preds, target, groups, 2)
  125. {'group_0': tensor([0., 0., 1., 0.]), 'group_1': tensor([1., 0., 0., 0.])}
  126. """
  127. group_stats = _binary_groups_stat_scores(preds, target, groups, num_groups, threshold, ignore_index, validate_args)
  128. return _groups_reduce(group_stats)
  129. def _compute_binary_demographic_parity(
  130. tp: torch.Tensor, fp: torch.Tensor, tn: torch.Tensor, fn: torch.Tensor
  131. ) -> dict[str, torch.Tensor]:
  132. """Compute demographic parity based on the binary stats."""
  133. pos_rates = _safe_divide(tp + fp, tp + fp + tn + fn)
  134. min_pos_rate_id = torch.argmin(pos_rates)
  135. max_pos_rate_id = torch.argmax(pos_rates)
  136. return {
  137. f"DP_{min_pos_rate_id}_{max_pos_rate_id}": _safe_divide(pos_rates[min_pos_rate_id], pos_rates[max_pos_rate_id])
  138. }
  139. def demographic_parity(
  140. preds: torch.Tensor,
  141. groups: torch.Tensor,
  142. threshold: float = 0.5,
  143. ignore_index: Optional[int] = None,
  144. validate_args: bool = True,
  145. ) -> dict[str, torch.Tensor]:
  146. r"""`Demographic parity`_ compares the positivity rates between all groups.
  147. If more than two groups are present, the disparity between the lowest and highest group is reported. The lowest
  148. positivity rate is divided by the highest, so a lower value means more discrimination against the numerator.
  149. In the results this is also indicated as the key of dict is DP_{identifier_low_group}_{identifier_high_group}.
  150. .. math::
  151. \text{DP} = \dfrac{\min_a PR_a}{\max_a PR_a}.
  152. where :math:`\text{PR}` represents the positivity rate for group :math:`\text{a}`.
  153. Accepts the following input tensors:
  154. - ``preds`` (int or float tensor): ``(N, ...)``. If preds is a floating point tensor with values outside
  155. [0,1] range we consider the input to be logits and will auto apply sigmoid per element. Additionally,
  156. we convert to int tensor with thresholding using the value in ``threshold``.
  157. - ``groups`` (int tensor): ``(N, ...)``. The group identifiers should be ``0, 1, ..., (num_groups - 1)``.
  158. - ``target`` (int tensor): ``(N, ...)``.
  159. The additional dimensions are flatted along the batch dimension.
  160. Args:
  161. preds: Tensor with predictions.
  162. groups: Tensor with group identifiers. The group identifiers should be ``0, 1, ..., (num_groups - 1)``.
  163. threshold: Threshold for transforming probability to binary {0,1} predictions.
  164. ignore_index:
  165. Specifies a target value that is ignored and does not contribute to the metric calculation
  166. validate_args: bool indicating if input arguments and tensors should be validated for correctness.
  167. Set to ``False`` for faster computations.
  168. Returns:
  169. The metric returns a dict where the key identifies the group with the lowest and highest positivity rates
  170. as follows: DP_{identifier_low_group}_{identifier_high_group}. The value is a tensor with the DP rate.
  171. Example (preds is int tensor):
  172. >>> from torchmetrics.functional.classification import demographic_parity
  173. >>> preds = torch.tensor([0, 1, 0, 1, 0, 1])
  174. >>> groups = torch.tensor([0, 1, 0, 1, 0, 1])
  175. >>> demographic_parity(preds, groups)
  176. {'DP_0_1': tensor(0.)}
  177. Example (preds is float tensor):
  178. >>> from torchmetrics.functional.classification import demographic_parity
  179. >>> preds = torch.tensor([0.11, 0.84, 0.22, 0.73, 0.33, 0.92])
  180. >>> groups = torch.tensor([0, 1, 0, 1, 0, 1])
  181. >>> demographic_parity(preds, groups)
  182. {'DP_0_1': tensor(0.)}
  183. """
  184. num_groups = torch.unique(groups).shape[0]
  185. target = torch.zeros(preds.shape)
  186. group_stats = _binary_groups_stat_scores(preds, target, groups, num_groups, threshold, ignore_index, validate_args)
  187. transformed_group_stats = _groups_stat_transform(group_stats)
  188. return _compute_binary_demographic_parity(**transformed_group_stats)
  189. def _compute_binary_equal_opportunity(
  190. tp: torch.Tensor, fp: torch.Tensor, tn: torch.Tensor, fn: torch.Tensor
  191. ) -> dict[str, torch.Tensor]:
  192. """Compute equal opportunity based on the binary stats."""
  193. true_pos_rates = _safe_divide(tp, tp + fn)
  194. min_pos_rate_id = torch.argmin(true_pos_rates)
  195. max_pos_rate_id = torch.argmax(true_pos_rates)
  196. return {
  197. f"EO_{min_pos_rate_id}_{max_pos_rate_id}": _safe_divide(
  198. true_pos_rates[min_pos_rate_id], true_pos_rates[max_pos_rate_id]
  199. )
  200. }
  201. def equal_opportunity(
  202. preds: torch.Tensor,
  203. target: torch.Tensor,
  204. groups: torch.Tensor,
  205. threshold: float = 0.5,
  206. ignore_index: Optional[int] = None,
  207. validate_args: bool = True,
  208. ) -> dict[str, torch.Tensor]:
  209. r"""`Equal opportunity`_ compares the true positive rates between all groups.
  210. If more than two groups are present, the disparity between the lowest and highest group is reported. The lowest
  211. true positive rate is divided by the highest, so a lower value means more discrimination against the numerator.
  212. In the results this is also indicated as the key of dict is EO_{identifier_low_group}_{identifier_high_group}.
  213. .. math::
  214. \text{DP} = \dfrac{\min_a TPR_a}{\max_a TPR_a}.
  215. where :math:`\text{TPR}` represents the true positives rate for group :math:`\text{a}`.
  216. Accepts the following input tensors:
  217. - ``preds`` (int or float tensor): ``(N, ...)``. If preds is a floating point tensor with values outside
  218. [0,1] range we consider the input to be logits and will auto apply sigmoid per element. Additionally,
  219. we convert to int tensor with thresholding using the value in ``threshold``.
  220. - ``target`` (int tensor): ``(N, ...)``.
  221. - ``groups`` (int tensor): ``(N, ...)``. The group identifiers should be ``0, 1, ..., (num_groups - 1)``.
  222. The additional dimensions are flatted along the batch dimension.
  223. Args:
  224. preds: Tensor with predictions.
  225. target: Tensor with true labels.
  226. groups: Tensor with group identifiers. The group identifiers should be ``0, 1, ..., (num_groups - 1)``.
  227. threshold: Threshold for transforming probability to binary {0,1} predictions.
  228. ignore_index:
  229. Specifies a target value that is ignored and does not contribute to the metric calculation
  230. validate_args: bool indicating if input arguments and tensors should be validated for correctness.
  231. Set to ``False`` for faster computations.
  232. Returns:
  233. The metric returns a dict where the key identifies the group with the lowest and highest true positives rates
  234. as follows: EO_{identifier_low_group}_{identifier_high_group}. The value is a tensor with the EO rate.
  235. Example (preds is int tensor):
  236. >>> from torchmetrics.functional.classification import equal_opportunity
  237. >>> target = torch.tensor([0, 1, 0, 1, 0, 1])
  238. >>> preds = torch.tensor([0, 1, 0, 1, 0, 1])
  239. >>> groups = torch.tensor([0, 1, 0, 1, 0, 1])
  240. >>> equal_opportunity(preds, target, groups)
  241. {'EO_0_1': tensor(0.)}
  242. Example (preds is float tensor):
  243. >>> from torchmetrics.functional.classification import equal_opportunity
  244. >>> target = torch.tensor([0, 1, 0, 1, 0, 1])
  245. >>> preds = torch.tensor([0.11, 0.84, 0.22, 0.73, 0.33, 0.92])
  246. >>> groups = torch.tensor([0, 1, 0, 1, 0, 1])
  247. >>> equal_opportunity(preds, target, groups)
  248. {'EO_0_1': tensor(0.)}
  249. """
  250. num_groups = torch.unique(groups).shape[0]
  251. group_stats = _binary_groups_stat_scores(preds, target, groups, num_groups, threshold, ignore_index, validate_args)
  252. transformed_group_stats = _groups_stat_transform(group_stats)
  253. return _compute_binary_equal_opportunity(**transformed_group_stats)
  254. def binary_fairness(
  255. preds: torch.Tensor,
  256. target: torch.Tensor,
  257. groups: torch.Tensor,
  258. task: Literal["demographic_parity", "equal_opportunity", "all"] = "all",
  259. threshold: float = 0.5,
  260. ignore_index: Optional[int] = None,
  261. validate_args: bool = True,
  262. ) -> dict[str, torch.Tensor]:
  263. r"""Compute either `Demographic parity`_ and `Equal opportunity`_ ratio for binary classification problems.
  264. This is done by setting the ``task`` argument to either ``'demographic_parity'``, ``'equal_opportunity'``
  265. or ``all``. See the documentation of
  266. :func:`~torchmetrics.functional.classification.demographic_parity`
  267. and :func:`~torchmetrics.functional.classification.equal_opportunity` for the specific details of
  268. each argument influence and examples.
  269. Args:
  270. preds: Tensor with predictions.
  271. target: Tensor with true labels (not required for demographic_parity).
  272. groups: Tensor with group identifiers. The group identifiers should be ``0, 1, ..., (num_groups - 1)``.
  273. task: The task to compute. Can be either ``demographic_parity`` or ``equal_opportunity`` or ``all``.
  274. threshold: Threshold for transforming probability to binary {0,1} predictions.
  275. ignore_index:
  276. Specifies a target value that is ignored and does not contribute to the metric calculation
  277. validate_args: bool indicating if input arguments and tensors should be validated for correctness.
  278. Set to ``False`` for faster computations.
  279. """
  280. if task not in ["demographic_parity", "equal_opportunity", "all"]:
  281. raise ValueError(
  282. f"Expected argument `task` to either be ``demographic_parity``,"
  283. f"``equal_opportunity`` or ``all`` but got {task}."
  284. )
  285. if task == "demographic_parity":
  286. if target is not None:
  287. rank_zero_warn("The task demographic_parity does not require a target.", UserWarning)
  288. target = torch.zeros(preds.shape)
  289. num_groups = torch.unique(groups).shape[0]
  290. group_stats = _binary_groups_stat_scores(preds, target, groups, num_groups, threshold, ignore_index, validate_args)
  291. transformed_group_stats = _groups_stat_transform(group_stats)
  292. if task == "demographic_parity":
  293. return _compute_binary_demographic_parity(**transformed_group_stats)
  294. if task == "equal_opportunity":
  295. return _compute_binary_equal_opportunity(**transformed_group_stats)
  296. if task == "all":
  297. return {
  298. **_compute_binary_demographic_parity(**transformed_group_stats),
  299. **_compute_binary_equal_opportunity(**transformed_group_stats),
  300. }
  301. return None