-
-
Notifications
You must be signed in to change notification settings - Fork 712
feat: Add average parameter to Accuracy for per-label multilabel output #3810
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
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||
| from collections.abc import Callable, Sequence, Iterable | ||||||||||
| from typing import cast | ||||||||||
|
|
||||||||||
| import torch | ||||||||||
|
|
||||||||||
|
|
@@ -167,6 +168,11 @@ class Accuracy(_BaseClassification): | |||||||||
| :class:`~ignite.engine.engine.Engine`'s ``process_function``'s output into the | ||||||||||
| form expected by the metric. This can be useful if, for example, you have a multi-output model and | ||||||||||
| you want to compute the metric with respect to one of the outputs. | ||||||||||
| average: if None (default), computes subset accuracy for multilabel data | ||||||||||
| (a sample is correct only if all labels match exactly), returning a single float. | ||||||||||
| If False, computes per-label accuracy for multilabel data, returning a tensor | ||||||||||
| of shape ``(num_labels,)`` where each element is the fraction of samples correctly | ||||||||||
| predicted for that label. Only applicable when ``is_multilabel=True``. | ||||||||||
| is_multilabel: flag to use in multilabel case. By default, False. | ||||||||||
| 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 | ||||||||||
|
|
@@ -246,6 +252,33 @@ class Accuracy(_BaseClassification): | |||||||||
|
|
||||||||||
| 0.2 | ||||||||||
|
|
||||||||||
| Multilabel case, per-label accuracy | ||||||||||
|
|
||||||||||
| .. testcode:: 5 | ||||||||||
|
|
||||||||||
| metric = Accuracy(is_multilabel=True, average=False) | ||||||||||
| metric.attach(default_evaluator, "label_accuracy") | ||||||||||
| y_true = torch.tensor([ | ||||||||||
| [0, 0, 1, 0, 1], | ||||||||||
| [1, 0, 1, 0, 0], | ||||||||||
| [0, 0, 0, 0, 1], | ||||||||||
| [1, 0, 0, 0, 1], | ||||||||||
| [0, 1, 1, 0, 1], | ||||||||||
| ]) | ||||||||||
| y_pred = torch.tensor([ | ||||||||||
| [1, 1, 0, 0, 0], | ||||||||||
| [1, 0, 1, 0, 0], | ||||||||||
| [1, 0, 0, 0, 0], | ||||||||||
| [1, 0, 1, 1, 1], | ||||||||||
| [1, 1, 0, 0, 1], | ||||||||||
| ]) | ||||||||||
| state = default_evaluator.run([[y_pred, y_true]]) | ||||||||||
| print(state.metrics["label_accuracy"]) | ||||||||||
|
|
||||||||||
| .. testoutput:: 5 | ||||||||||
|
|
||||||||||
| tensor([0.4000, 0.8000, 0.4000, 0.8000, 0.6000]) | ||||||||||
|
|
||||||||||
| In binary and multilabel cases, the elements of `y` and `y_pred` should have 0 or 1 values. Thresholding of | ||||||||||
| predictions can be done as below: | ||||||||||
|
|
||||||||||
|
|
@@ -269,24 +302,33 @@ def thresholded_output_transform(output): | |||||||||
|
|
||||||||||
| .. versionchanged:: 0.5.1 | ||||||||||
| ``skip_unrolling`` argument is added. | ||||||||||
|
|
||||||||||
| .. versionchanged:: 0.6.0 | ||||||||||
| ``average`` argument is added. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| _state_dict_all_req_keys = ("_num_correct", "_num_examples") | ||||||||||
|
|
||||||||||
| def __init__( | ||||||||||
| self, | ||||||||||
| output_transform: Callable = lambda x: x, | ||||||||||
| average: bool | None = None, | ||||||||||
| is_multilabel: bool = False, | ||||||||||
| device: str | torch.device = torch.device("cpu"), | ||||||||||
| skip_unrolling: bool = False, | ||||||||||
| ): | ||||||||||
| if average is not None and average is not False: | ||||||||||
| raise ValueError("Argument average should be None or False.") | ||||||||||
|
Comment on lines
+320
to
+321
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will update if we go with the bool-only approach from the comment above. |
||||||||||
| if average is False and not is_multilabel: | ||||||||||
| raise ValueError("Argument average=False is only applicable with is_multilabel=True.") | ||||||||||
| self._average = average | ||||||||||
| super().__init__( | ||||||||||
| output_transform=output_transform, is_multilabel=is_multilabel, device=device, skip_unrolling=skip_unrolling | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| @reinit__is_reduced | ||||||||||
| def reset(self) -> None: | ||||||||||
| self._num_correct = torch.tensor(0, device=self._device) | ||||||||||
| self._num_correct: int | torch.Tensor = 0 | ||||||||||
|
aaishwarymishra marked this conversation as resolved.
|
||||||||||
| self._num_examples = 0 | ||||||||||
| super().reset() | ||||||||||
|
|
||||||||||
|
|
@@ -307,6 +349,12 @@ def update(self, output: Sequence[torch.Tensor]) -> None: | |||||||||
| last_dim = y_pred.ndimension() | ||||||||||
| y_pred = torch.transpose(y_pred, 1, last_dim - 1).reshape(-1, num_classes) | ||||||||||
| y = torch.transpose(y, 1, last_dim - 1).reshape(-1, num_classes) | ||||||||||
| if self._average is False: | ||||||||||
| correct = (y == y_pred.type_as(y)).float() | ||||||||||
| self._num_correct += correct.sum(dim=0).to(self._device) | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dont think we need to move it into a new device
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept it for consistency with the existing default path a few lines below (
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its redundant in my opinion |
||||||||||
| self._num_examples += correct.shape[0] | ||||||||||
| return | ||||||||||
| # Default: subset accuracy — entire label vector must match exactly | ||||||||||
| correct = torch.all(y == y_pred.type_as(y), dim=-1) | ||||||||||
| else: | ||||||||||
| raise ValueError(f"Unexpected type: {self._type}") | ||||||||||
|
|
@@ -315,7 +363,9 @@ def update(self, output: Sequence[torch.Tensor]) -> None: | |||||||||
| self._num_examples += correct.shape[0] | ||||||||||
|
|
||||||||||
| @sync_all_reduce("_num_examples", "_num_correct") | ||||||||||
| def compute(self) -> float: | ||||||||||
| def compute(self) -> float | torch.Tensor: | ||||||||||
| if self._num_examples == 0: | ||||||||||
| raise NotComputableError("Accuracy must have at least one example before it can be computed.") | ||||||||||
| return self._num_correct.item() / self._num_examples | ||||||||||
| if self._average is False: | ||||||||||
| return self._num_correct / self._num_examples | ||||||||||
| return cast(torch.Tensor, self._num_correct).item() / self._num_examples | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can have it true by default instead of the
NoneThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Switching to
average: bool = Truewith True as the default keeps it simple. But I want to flag a naming conflict before changing the default. InPrecision/Recall,average=Truemeans "macro average," not "current default behavior." Using True to mean "subset accuracy" here would be semantically different from the sibling classes. Happy to go either way — do you think the simplicity of a plain bool outweighs the consistency concern, or should we keep None to leave room for future'micro'/'macro'modes?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making it None doesn't make is consistent either if consistency is the problem rename it possibly