Skip to content

Commit b29fc7d

Browse files
authored
Merge pull request #9 from torch-points3d/metrics
Metrics
2 parents 49052ce + 11e8a5e commit b29fc7d

File tree

15 files changed

+443
-42
lines changed

15 files changed

+443
-42
lines changed

conf/model/segmentation/default.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# @package model
22
defaults:
33
- /model/default
4+
- /tracker: segmentation/default
45

56
model:
67
_recursive_: false
@@ -11,4 +12,4 @@ model:
1112

1213
backbone:
1314
input_nc: ${dataset.cfg.feature_dimension}
14-
architecture: unet
15+
architecture: unet
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
_target_: torch_points3d.metrics.segmentation.segmentation_tracker.SegmentationTracker
2+
num_classes: ${dataset.cfg.num_classes}

test/__init__.py

Whitespace-only changes.

test/test_confusion_matrix.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import torch
2+
import os
3+
import sys
4+
import unittest
5+
import pytest
6+
import numpy as np
7+
8+
9+
DIR = os.path.dirname(os.path.realpath(__file__))
10+
ROOT = os.path.join(DIR, "..")
11+
sys.path.insert(0, ROOT)
12+
sys.path.append('.')
13+
14+
from torch_points3d.metrics.segmentation.metrics import compute_intersection_union_per_class
15+
from torch_points3d.metrics.segmentation.metrics import compute_average_intersection_union
16+
from torch_points3d.metrics.segmentation.metrics import compute_overall_accuracy
17+
from torch_points3d.metrics.segmentation.metrics import compute_mean_class_accuracy
18+
19+
20+
21+
def test_compute_intersection_union_per_class():
22+
matrix = torch.tensor([[4, 1], [2, 10]])
23+
iou, _ = compute_intersection_union_per_class(matrix)
24+
miou = compute_average_intersection_union(matrix)
25+
np.testing.assert_allclose(iou[0].item(), 4 / (4.0 + 1.0 + 2.0))
26+
np.testing.assert_allclose(iou[1].item(), 10 / (10.0 + 1.0 + 2.0))
27+
np.testing.assert_allclose(iou.mean().item(), miou.item())
28+
29+
def test_compute_overall_accuracy():
30+
list_matrix = [
31+
torch.tensor([[4, 1], [2, 10]]).float(),
32+
torch.tensor([[4, 1], [2, 10]]).int(),
33+
torch.tensor([[0, 0], [0, 0]]).float()
34+
]
35+
list_answer = [
36+
(4.0+10.0)/(4.0 + 10.0 + 1.0 +2.0),
37+
(4.0+10.0)/(4.0 + 10.0 + 1.0 +2.0),
38+
0.0
39+
]
40+
for i in range(len(list_matrix)):
41+
acc = compute_overall_accuracy(list_matrix[i])
42+
if(isinstance(acc, torch.Tensor)):
43+
np.testing.assert_allclose(acc.item(), list_answer[i])
44+
else:
45+
np.testing.assert_allclose(acc, list_answer[i])
46+
47+
48+
def test_compute_mean_class_accuracy():
49+
matrix = torch.tensor([[4, 1], [2, 10]]).float()
50+
macc = compute_mean_class_accuracy(matrix)
51+
np.testing.assert_allclose(macc.item(), (4/5 + 10/12)*0.5)
52+
53+
54+
55+
@pytest.mark.parametrize("missing_as_one, answer", [pytest.param(False, (0.5 + 0.5) / 2), pytest.param(True, (0.5 + 1 + 0.5) / 3)])
56+
def test_test_getMeanIoUMissing(missing_as_one, answer):
57+
matrix = torch.tensor([[1, 1, 0], [0, 1, 0], [0, 0, 0]])
58+
np.testing.assert_allclose(compute_average_intersection_union(matrix, missing_as_one=missing_as_one).item(), answer)
59+

test/test_model.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import unittest
1+
import pytest
22
import sys
33
import os
44
import torch
@@ -9,27 +9,25 @@
99
DIR = os.path.dirname(os.path.realpath(__file__))
1010
ROOT = os.path.join(DIR, "..")
1111
sys.path.insert(0, ROOT)
12+
sys.path.append(".")
1213

13-
from torch_points3d.models.segmentation.sparseconv3d import APIModel
14+
from torch_points3d.models.segmentation.base_model import SegmentationBaseModel
15+
from torch_points3d.core.instantiator import HydraInstantiator
1416

1517

16-
class TestAPIModel(unittest.TestCase):
17-
def test_forward(self):
18-
option_dataset = OmegaConf.create({"feature_dimension": 1, "num_classes": 10})
18+
@pytest.mark.skip("For now we skip the tests...")
19+
def test_forward(self):
20+
option_dataset = OmegaConf.create({"feature_dimension": 1, "num_classes": 10})
21+
option_criterion = OmegaConf.create({"_target_": "torch.nn.NLLLoss"})
22+
instantiator = HydraInstantiator()
1923

20-
option = OmegaConf.load(os.path.join(ROOT, "conf", "models", "segmentation", "sparseconv3d.yaml"))
21-
name_model = list(option.keys())[0]
22-
model = APIModel(option[name_model], option_dataset)
24+
model = SegmentationBaseModel(instantiator, 10, option_backbone, option_criterion)
2325

24-
pos = torch.randn(1000, 3)
25-
coords = torch.round(pos * 10000)
26-
x = torch.ones(1000, 1)
27-
batch = torch.zeros(1000).long()
28-
y = torch.randint(0, 10, (1000,))
29-
data = Batch(pos=pos, x=x, batch=batch, y=y, coords=coords)
30-
model.set_input(data)
31-
model.forward()
32-
33-
34-
if __name__ == "__main__":
35-
unittest.main()
26+
pos = torch.randn(1000, 3)
27+
coords = torch.round(pos * 10000)
28+
x = torch.ones(1000, 6)
29+
batch = torch.zeros(1000).long()
30+
y = torch.randint(0, 10, (1000,))
31+
data = Batch(pos=pos, x=x, batch=batch, y=y, coords=coords)
32+
model.set_input(data)
33+
model.forward()

test/test_segmentation_tracker.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import numpy as np
2+
import torch
3+
import sys
4+
import os
5+
6+
import pytest
7+
8+
9+
from torch_geometric.data import Data
10+
11+
DIR = os.path.dirname(os.path.realpath(__file__))
12+
ROOT = os.path.join(DIR, "..")
13+
sys.path.insert(0, ROOT)
14+
sys.path.append(".")
15+
16+
from torch_points3d.metrics.segmentation.segmentation_tracker import SegmentationTracker
17+
18+
19+
class MockDataset:
20+
INV_OBJECT_LABEL = {0: "first", 1: "wall", 2: "not", 3: "here", 4: "hoy"}
21+
pos = torch.tensor([[1, 0, 0], [2, 0, 0], [3, 0, 0], [-1, 0, 0]]).float()
22+
test_label = torch.tensor([1, 1, 0, 0])
23+
24+
def __init__(self):
25+
self.num_classes = 2
26+
27+
@property
28+
def test_data(self):
29+
return Data(pos=self.pos, y=self.test_label)
30+
31+
def has_labels(self, stage):
32+
return True
33+
34+
35+
class MockModel:
36+
def __init__(self):
37+
self.iter = 0
38+
self.losses = [
39+
{"loss_1": 1, "loss_2": 2},
40+
{"loss_1": 2, "loss_2": 2},
41+
{"loss_1": 1, "loss_2": 2},
42+
{"loss_1": 1, "loss_2": 2},
43+
]
44+
self.outputs = [
45+
torch.tensor([[0, 1], [0, 1]]),
46+
torch.tensor([[1, 0], [1, 0]]),
47+
torch.tensor([[1, 0], [1, 0]]),
48+
torch.tensor([[1, 0], [1, 0], [1, 0]]),
49+
]
50+
self.labels = [torch.tensor([1, 1]), torch.tensor([1, 1]), torch.tensor([1, 1]), torch.tensor([0, 0, -100])]
51+
self.batch_idx = [torch.tensor([0, 1]), torch.tensor([0, 1]), torch.tensor([0, 1]), torch.tensor([0, 0, 1])]
52+
53+
def get_input(self):
54+
return Data(pos=MockDataset.pos[:2, :], origin_id=torch.tensor([0, 1]))
55+
56+
def get_output(self):
57+
return self.outputs[self.iter].float()
58+
59+
def get_labels(self):
60+
return self.labels[self.iter]
61+
62+
def get_current_losses(self):
63+
return self.losses[self.iter]
64+
65+
def get_batch(self):
66+
return self.batch_idx[self.iter]
67+
68+
@property
69+
def device(self):
70+
return "cpu"
71+
72+
73+
def test_forward():
74+
tracker = SegmentationTracker(num_classes=2, stage="train")
75+
model = MockModel()
76+
output = {"preds": model.get_output(), "labels": model.get_labels()}
77+
losses = model.get_current_losses()
78+
metrics = tracker(output, losses)
79+
# metrics = tracker.get_metrics()
80+
81+
for k in ["train_acc", "train_miou", "train_macc"]:
82+
np.testing.assert_allclose(metrics[k], 100, rtol=1e-5)
83+
model.iter += 1
84+
output = {"preds": model.get_output(), "labels": model.get_labels()}
85+
losses = model.get_current_losses()
86+
metrics = tracker(output, losses)
87+
# metrics = tracker.get_metrics()
88+
metrics = tracker.finalise()
89+
for k in ["train_acc", "train_macc"]:
90+
assert metrics[k] == 50
91+
np.testing.assert_allclose(metrics["train_miou"], 25, atol=1e-5)
92+
assert metrics["train_loss_1"] == 1.5
93+
94+
tracker.reset("test")
95+
model.iter += 1
96+
output = {"preds": model.get_output(), "labels": model.get_labels()}
97+
losses = model.get_current_losses()
98+
metrics = tracker(output, losses)
99+
# metrics = tracker.get_metrics()
100+
for name in ["test_acc", "test_miou", "test_macc"]:
101+
np.testing.assert_allclose(metrics[name].item(), 0, atol=1e-5)
102+
103+
104+
@pytest.mark.parametrize("finalise", [pytest.param(True), pytest.param(False)])
105+
def test_ignore_label(finalise):
106+
tracker = SegmentationTracker(num_classes=2, ignore_label=-100)
107+
tracker.reset("test")
108+
model = MockModel()
109+
model.iter = 3
110+
output = {"preds": model.get_output(), "labels": model.get_labels()}
111+
losses = model.get_current_losses()
112+
metrics = tracker(output, losses)
113+
if not finalise:
114+
# metrics = tracker.get_metrics()
115+
for k in ["test_acc", "test_miou", "test_macc"]:
116+
np.testing.assert_allclose(metrics[k], 100)
117+
else:
118+
tracker.finalise()
119+
with pytest.raises(RuntimeError):
120+
tracker(output)

torch_points3d/core/instantiator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def litmodel(self, cfg: DictConfig) -> "PointCloudBaseModule":
4444
def model(self, cfg: DictConfig) -> "PointCloudBaseModel":
4545
return self.instantiate(cfg, self)
4646

47+
def tracker(self, cfg: DictConfig, stage: str = ""):
48+
return self.instantiate(cfg, stage=stage)
49+
4750
def backbone(self, cfg: DictConfig):
4851
return self.instantiate(cfg)
4952

torch_points3d/metrics/__init__.py

Whitespace-only changes.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from typing import Any, Dict, Optional
2+
import torch
3+
from torch import nn
4+
from torchmetrics import AverageMeter
5+
6+
7+
class BaseTracker(nn.Module):
8+
"""
9+
pytorch Module to manage the losses and the metrics
10+
"""
11+
12+
def __init__(self, stage: str = "train"):
13+
super().__init__()
14+
self.stage: str = stage
15+
self._finalised: bool = False
16+
self.loss_metrics: nn.ModuleDict = nn.ModuleDict()
17+
18+
def track(self, output_model, *args, **kwargs) -> Dict[str, Any]:
19+
raise NotImplementedError
20+
21+
def track_loss(self, losses: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
22+
out_loss = dict()
23+
for key, loss in losses.items():
24+
loss_key = f"{self.stage}_{key}"
25+
if loss_key not in self.loss_metrics.keys():
26+
self.loss_metrics[loss_key] = AverageMeter().to(loss)
27+
val = self.loss_metrics[loss_key](loss)
28+
out_loss[loss_key] = val
29+
return out_loss
30+
31+
def forward(
32+
self, output_model: Dict[str, Any], losses: Optional[Dict[str, torch.Tensor]] = None, *args, **kwargs
33+
) -> Dict[str, Any]:
34+
if self._finalised:
35+
raise RuntimeError("Cannot track new values with a finalised tracker, you need to reset it first")
36+
tracked_metric = self.track(output_model, *args, **kwargs)
37+
if losses is not None:
38+
tracked_loss = self.track_loss(losses)
39+
tracked_results = dict(**tracked_loss, **tracked_metric)
40+
else:
41+
tracked_results = tracked_metric
42+
return tracked_results
43+
44+
def _finalise(self) -> Dict[str, Any]:
45+
raise NotImplementedError("method that aggregae metrics")
46+
47+
def finalise(self) -> Dict[str, Any]:
48+
metrics = self._finalise()
49+
self._finalised = True
50+
loss_metrics = self.get_final_loss_metrics()
51+
final_metrics = {**loss_metrics, **metrics}
52+
return final_metrics
53+
54+
def get_final_loss_metrics(self):
55+
metrics = dict()
56+
for key, m in self.loss_metrics.items():
57+
metrics[key] = m.compute()
58+
self.loss_metrics = nn.ModuleDict()
59+
return metrics

torch_points3d/metrics/segmentation/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)