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

Fix naming of statistics in MeanAveragePrecision with custom max det thresholds #2367

Merged
merged 7 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed cached network in `FeatureShare` not being moved to the correct device ([#2348](https://github.com/Lightning-AI/torchmetrics/pull/2348))


- Fix naming of statistics in `MeanAveragePrecision` with custom max det thresholds ([#2367](https://github.com/Lightning-AI/torchmetrics/pull/2367))


- Fixed custom aggregation in retrieval metrics ([#2364](https://github.com/Lightning-AI/torchmetrics/pull/2364))


Expand Down
42 changes: 22 additions & 20 deletions src/torchmetrics/detection/mean_ap.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,12 @@ class MeanAveragePrecision(Metric):
- map_small: (:class:`~torch.Tensor`), mean average precision for small objects
- map_medium:(:class:`~torch.Tensor`), mean average precision for medium objects
- map_large: (:class:`~torch.Tensor`), mean average precision for large objects
- mar_1: (:class:`~torch.Tensor`), mean average recall for 1 detection per image
- mar_10: (:class:`~torch.Tensor`), mean average recall for 10 detections per image
- mar_100: (:class:`~torch.Tensor`), mean average recall for 100 detections per image
- mar_{mdt[0]}: (:class:`~torch.Tensor`), mean average recall for `max_detection_thresholds[0]` (default 1)
detection per image
- mar_{mdt[1]}: (:class:`~torch.Tensor`), mean average recall for `max_detection_thresholds[1]` (default 10)
detection per image
- mar_{mdt[1]}: (:class:`~torch.Tensor`), mean average recall for `max_detection_thresholds[2]` (default 100)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a typo here? Should it maybe be

mar_{mdt[2]}: (:class:`~torch.Tensor`), mean average recall for `max_detection_thresholds[2]` (default 100)
mar_{mdt[1]}: (:class:`~torch.Tensor`), mean average recall for `max_detection_thresholds[2]` (default 100)

I realize this is merged already, but maybe this can be fixed boyscout-style in some future PR.

detection per image
- mar_small: (:class:`~torch.Tensor`), mean average recall for small objects
- mar_medium: (:class:`~torch.Tensor`), mean average recall for medium objects
- mar_large: (:class:`~torch.Tensor`), mean average recall for large objects
Expand All @@ -140,8 +143,8 @@ class MeanAveragePrecision(Metric):
IoU=0.75
- map_per_class: (:class:`~torch.Tensor`) (-1 if class metrics are disabled), mean average precision per
observed class
- mar_100_per_class: (:class:`~torch.Tensor`) (-1 if class metrics are disabled), mean average recall for 100
detections per image per observed class
- mar_{mdt[2]}_per_class: (:class:`~torch.Tensor`) (-1 if class metrics are disabled), mean average recall for
`max_detection_thresholds[2]` (default 100) detections per image per observed class
- classes (:class:`~torch.Tensor`), list of all observed classes

For an example on how to use this metric check the `torchmetrics mAP example`_.
Expand Down Expand Up @@ -184,8 +187,7 @@ class MeanAveragePrecision(Metric):
with step ``0.01``. Else provide a list of floats.
max_detection_thresholds:
Thresholds on max detections per image. If set to `None` will use thresholds ``[1, 10, 100]``.
Else, please provide a list of ints. If the `pycocotools` backend is used then the list needs to have
length 3. If this is a problem, shift to `faster_coco_eval` which supports more detection thresholds.
Else, please provide a list of ints of length 3, which is the only supported length by both backends.
class_metrics:
Option to enable per-class metrics for mAP and mAR_100. Has a performance impact that scales linearly with
the number of classes in the dataset.
Expand Down Expand Up @@ -410,10 +412,10 @@ def __init__(
f"Expected argument `max_detection_thresholds` to either be `None` or a list of ints"
f" but got {max_detection_thresholds}"
)
if max_detection_thresholds is not None and backend == "pycocotools" and len(max_detection_thresholds) != 3:
if max_detection_thresholds is not None and len(max_detection_thresholds) != 3:
raise ValueError(
"When using `pycocotools` backend the number of max detection thresholds should be 3 else"
f" it will not work correctly with the backend. Got value {len(max_detection_thresholds)}."
"When providing a list of max detection thresholds it should have length 3."
" Got value {len(max_detection_thresholds)}"
)
max_det_thr, _ = torch.sort(torch.tensor(max_detection_thresholds or [1, 10, 100], dtype=torch.int))
self.max_detection_thresholds = max_det_thr.tolist()
Expand Down Expand Up @@ -556,7 +558,7 @@ def compute(self) -> dict:
coco_eval.params.maxDets = self.max_detection_thresholds

map_per_class_list = []
mar_100_per_class_list = []
mar_per_class_list = []
for class_id in self._get_classes():
coco_eval.params.catIds = [class_id]
with contextlib.redirect_stdout(io.StringIO()):
Expand All @@ -566,18 +568,18 @@ def compute(self) -> dict:
class_stats = coco_eval.stats

map_per_class_list.append(torch.tensor([class_stats[0]]))
mar_100_per_class_list.append(torch.tensor([class_stats[8]]))
mar_per_class_list.append(torch.tensor([class_stats[8]]))

map_per_class_values = torch.tensor(map_per_class_list, dtype=torch.float32)
mar_100_per_class_values = torch.tensor(mar_100_per_class_list, dtype=torch.float32)
mar_per_class_values = torch.tensor(mar_per_class_list, dtype=torch.float32)
else:
map_per_class_values = torch.tensor([-1], dtype=torch.float32)
mar_100_per_class_values = torch.tensor([-1], dtype=torch.float32)
mar_per_class_values = torch.tensor([-1], dtype=torch.float32)
prefix = "" if len(self.iou_type) == 1 else f"{i_type}_"
result_dict.update(
{
f"{prefix}map_per_class": map_per_class_values,
f"{prefix}mar_100_per_class": mar_100_per_class_values,
f"{prefix}mar_{self.max_detection_thresholds[-1]}_per_class": mar_per_class_values,
},
)
result_dict.update({"classes": torch.tensor(self._get_classes(), dtype=torch.int32)})
Expand Down Expand Up @@ -616,19 +618,19 @@ def _get_coco_datasets(self, average: Literal["macro", "micro"]) -> Tuple[object

return coco_preds, coco_target

@staticmethod
def _coco_stats_to_tensor_dict(stats: List[float], prefix: str) -> Dict[str, Tensor]:
def _coco_stats_to_tensor_dict(self, stats: List[float], prefix: str) -> Dict[str, Tensor]:
"""Converts the output of COCOeval.stats to a dict of tensors."""
mdt = self.max_detection_thresholds
return {
f"{prefix}map": torch.tensor([stats[0]], dtype=torch.float32),
f"{prefix}map_50": torch.tensor([stats[1]], dtype=torch.float32),
f"{prefix}map_75": torch.tensor([stats[2]], dtype=torch.float32),
f"{prefix}map_small": torch.tensor([stats[3]], dtype=torch.float32),
f"{prefix}map_medium": torch.tensor([stats[4]], dtype=torch.float32),
f"{prefix}map_large": torch.tensor([stats[5]], dtype=torch.float32),
f"{prefix}mar_1": torch.tensor([stats[6]], dtype=torch.float32),
f"{prefix}mar_10": torch.tensor([stats[7]], dtype=torch.float32),
f"{prefix}mar_100": torch.tensor([stats[8]], dtype=torch.float32),
f"{prefix}mar_{mdt[0]}": torch.tensor([stats[6]], dtype=torch.float32),
f"{prefix}mar_{mdt[1]}": torch.tensor([stats[7]], dtype=torch.float32),
f"{prefix}mar_{mdt[2]}": torch.tensor([stats[8]], dtype=torch.float32),
f"{prefix}mar_small": torch.tensor([stats[9]], dtype=torch.float32),
f"{prefix}mar_medium": torch.tensor([stats[10]], dtype=torch.float32),
f"{prefix}mar_large": torch.tensor([stats[11]], dtype=torch.float32),
Expand Down
30 changes: 8 additions & 22 deletions tests/unittests/detection/test_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,10 @@ def test_many_detection_thresholds(self, backend):
else:
assert round(res["map"].item(), 5) == 0.6

assert "mar_1" in res
assert "mar_10" in res
assert "mar_1000" in res

@pytest.mark.parametrize("max_detection_thresholds", [[1, 10], [1, 10, 50, 100]])
def test_with_more_and_less_detection_thresholds(self, max_detection_thresholds, backend):
"""Test how metric is working when list of max detection thresholds is not 3.
Expand All @@ -869,25 +873,7 @@ def test_with_more_and_less_detection_thresholds(self, max_detection_thresholds,
https://github.com/ppwwyyxx/cocoapi/blob/master/PythonAPI/pycocotools/cocoeval.py#L461

"""
preds = [
{
"boxes": torch.tensor([[258.0, 41.0, 606.0, 285.0]]),
"scores": torch.tensor([0.536]),
"labels": torch.tensor([0]),
}
]
target = [
{
"boxes": torch.tensor([[214.0, 41.0, 562.0, 285.0]]),
"labels": torch.tensor([0]),
}
]

if backend == "pycocotools":
with pytest.raises(
ValueError, match="When using `pycocotools` backend the number of max detection thresholds should.*"
):
metric = MeanAveragePrecision(max_detection_thresholds=max_detection_thresholds, backend=backend)
else:
metric = MeanAveragePrecision(max_detection_thresholds=max_detection_thresholds, backend=backend)
metric(preds, target)
with pytest.raises(
ValueError, match="When providing a list of max detection thresholds it should have length 3.*"
):
MeanAveragePrecision(max_detection_thresholds=max_detection_thresholds, backend=backend)
Loading