Skip to content

Commit 0c1ce12

Browse files
Refactor get_sequence_transform to utils and support multiple ignore indices
1 parent a4bc092 commit 0c1ce12

5 files changed

Lines changed: 134 additions & 76 deletions

File tree

ignite/metrics/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from ignite.metrics.accumulation import Average, GeometricAverage, VariableAccumulation
55
from ignite.metrics.accuracy import Accuracy
6+
from ignite.metrics.utils import get_sequence_transform
67
from ignite.metrics.average_precision import AveragePrecision
78
from ignite.metrics.classification_report import ClassificationReport
89
from ignite.metrics.cohen_kappa import CohenKappa
@@ -54,6 +55,7 @@
5455
"Loss",
5556
"MetricGroup",
5657
"MetricsLambda",
58+
"get_sequence_transform",
5759
"MeanAbsoluteError",
5860
"MeanPairwiseDistance",
5961
"MeanSquaredError",

ignite/metrics/accuracy.py

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -264,38 +264,4 @@ def compute(self) -> float:
264264
raise NotComputableError("Accuracy must have at least one example before it can be computed.")
265265
return self._num_correct.item() / self._num_examples
266266

267-
@staticmethod
268-
def get_sequence_transform(
269-
ignore_index: int | None = None,
270-
output_transform: Callable = lambda x: x,
271-
) -> Callable:
272-
"""
273-
Returns a callable to transform sequence model outputs for metric evaluation.
274-
It flattens the sequences and filters out the padding (``ignore_index``).
275-
"""
276-
def wrapper(output: Sequence[torch.Tensor]) -> tuple[torch.Tensor, torch.Tensor]:
277-
y_pred, y = output_transform(output)
278-
279-
if y_pred.ndimension() == 3 and y.ndimension() == 2:
280-
if y_pred.shape[:2] == y.shape:
281-
# y_pred is (N, L, C), y is (N, L)
282-
y_pred = y_pred.reshape(-1, y_pred.size(-1))
283-
y = y.reshape(-1)
284-
elif y_pred.shape[0] == y.shape[0] and y_pred.shape[2] == y.shape[1]:
285-
# y_pred is (N, C, L), y is (N, L)
286-
y_pred = y_pred.transpose(1, 2).reshape(-1, y_pred.size(1))
287-
y = y.reshape(-1)
288-
elif y_pred.ndimension() == 2 and y.ndimension() == 2:
289-
# y_pred is (N, L), y is (N, L)
290-
if y_pred.shape == y.shape:
291-
y_pred = y_pred.reshape(-1)
292-
y = y.reshape(-1)
293-
294-
if ignore_index is not None:
295-
mask = y != ignore_index
296-
y_pred = y_pred[mask]
297-
y = y[mask]
298-
299-
return y_pred, y
300-
301-
return wrapper
267+

ignite/metrics/utils.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import torch
2+
from typing import Callable, Iterable, Sequence, Union
3+
4+
5+
def get_sequence_transform(
6+
ignore_index: Union[int, Iterable[int], None] = None,
7+
output_transform: Callable = lambda x: x,
8+
) -> Callable:
9+
"""
10+
Returns a callable to transform sequence model outputs for metric evaluation.
11+
It flattens the sequences and filters out the padding (`ignore_index`).
12+
13+
Args:
14+
ignore_index: An integer or an iterable of integers representing padding or
15+
special tokens to be masked out from the sequence evaluation.
16+
output_transform: A callable to transform the output into `(y_pred, y)`.
17+
18+
Returns:
19+
Callable that flattens `y_pred` and `y` and removes `ignore_index` elements.
20+
"""
21+
def wrapper(output: Sequence[torch.Tensor]) -> tuple[torch.Tensor, torch.Tensor]:
22+
y_pred, y = output_transform(output)
23+
24+
if y_pred.ndimension() == 3 and y.ndimension() == 2:
25+
if y_pred.shape[:2] == y.shape:
26+
# y_pred is (N, L, C), y is (N, L)
27+
y_pred = y_pred.reshape(-1, y_pred.size(-1))
28+
y = y.reshape(-1)
29+
elif y_pred.shape[0] == y.shape[0] and y_pred.shape[2] == y.shape[1]:
30+
# y_pred is (N, C, L), y is (N, L)
31+
y_pred = y_pred.transpose(1, 2).reshape(-1, y_pred.size(1))
32+
y = y.reshape(-1)
33+
else:
34+
raise ValueError(
35+
f"y_pred and y have incompatible sequence shapes: "
36+
f"y_pred={y_pred.shape} vs y={y.shape}"
37+
)
38+
elif y_pred.ndimension() == 2 and y.ndimension() == 2:
39+
# y_pred is (N, L), y is (N, L)
40+
if y_pred.shape == y.shape:
41+
y_pred = y_pred.reshape(-1)
42+
y = y.reshape(-1)
43+
else:
44+
raise ValueError(
45+
f"y_pred and y have incompatible sequence shapes: "
46+
f"y_pred={y_pred.shape} vs y={y.shape}"
47+
)
48+
else:
49+
raise ValueError(
50+
f"y_pred and y must be 3D and 2D arrays, or both 2D arrays "
51+
f"for sequence transformation. Got {y_pred.ndimension()}D and {y.ndimension()}D."
52+
)
53+
54+
if ignore_index is not None:
55+
if isinstance(ignore_index, Iterable):
56+
mask = torch.ones_like(y, dtype=torch.bool)
57+
for idx in ignore_index:
58+
mask &= (y != idx)
59+
else:
60+
mask = y != ignore_index
61+
62+
y_pred = y_pred[mask]
63+
y = y[mask]
64+
65+
return y_pred, y
66+
67+
return wrapper

tests/ignite/metrics/test_accuracy.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -500,44 +500,3 @@ def update(self, output):
500500
acc.iteration_completed(engine)
501501

502502

503-
def test_get_sequence_transform():
504-
# test (N, L, C)
505-
y_pred = torch.tensor(
506-
[
507-
[[0.1, 0.9], [0.8, 0.2], [0.3, 0.7], [0.5, 0.5]],
508-
[[0.9, 0.1], [0.2, 0.8], [0.4, 0.6], [0.5, 0.5]],
509-
]
510-
) # shape: (2, 4, 2)
511-
y = torch.tensor([[1, 0, 1, -1], [0, 1, 0, -1]]) # shape: (2, 4)
512-
513-
transform = Accuracy.get_sequence_transform(ignore_index=-1)
514-
y_pred_t, y_t = transform((y_pred, y))
515-
516-
assert y_pred_t.shape == (6, 2)
517-
assert y_t.shape == (6,)
518-
assert y_t.tolist() == [1, 0, 1, 0, 1, 0]
519-
assert y_pred_t[:, 1].tolist() == pytest.approx([0.9, 0.2, 0.7, 0.1, 0.8, 0.6])
520-
521-
# test (N, C, L)
522-
y_pred_ncl = y_pred.transpose(1, 2).contiguous() # (2, 2, 4)
523-
y_pred_t2, y_t2 = transform((y_pred_ncl, y))
524-
assert y_pred_t2.shape == (6, 2)
525-
assert torch.all(y_pred_t2 == y_pred_t)
526-
assert torch.all(y_t2 == y_t)
527-
528-
# test binary (N, L)
529-
y_pred_bin = torch.tensor([[1, 0, 1, 1], [0, 1, 0, 0]])
530-
y_bin = torch.tensor([[1, 0, 1, 2], [0, 1, 0, 2]])
531-
transform_bin = Accuracy.get_sequence_transform(ignore_index=2)
532-
y_pred_bin_t, y_bin_t = transform_bin((y_pred_bin, y_bin))
533-
534-
assert y_pred_bin_t.shape == (6,)
535-
assert y_bin_t.shape == (6,)
536-
assert y_bin_t.tolist() == [1, 0, 1, 0, 1, 0]
537-
assert y_pred_bin_t.tolist() == [1, 0, 1, 0, 1, 0]
538-
539-
# test without padding
540-
transform_nopad = Accuracy.get_sequence_transform()
541-
y_pred_nopad, y_nopad = transform_nopad((y_pred_bin, y_bin))
542-
assert y_pred_nopad.shape == (8,)
543-
assert y_nopad.shape == (8,)

tests/ignite/metrics/test_utils.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import pytest
2+
import torch
3+
4+
from ignite.metrics.utils import get_sequence_transform
5+
6+
def test_get_sequence_transform():
7+
# test (N, L, C)
8+
y_pred = torch.tensor(
9+
[
10+
[[0.1, 0.9], [0.8, 0.2], [0.3, 0.7], [0.5, 0.5]],
11+
[[0.9, 0.1], [0.2, 0.8], [0.4, 0.6], [0.5, 0.5]],
12+
]
13+
) # shape: (2, 4, 2)
14+
y = torch.tensor([[1, 0, 1, -1], [0, 1, 0, -1]]) # shape: (2, 4)
15+
16+
transform = get_sequence_transform(ignore_index=-1)
17+
y_pred_t, y_t = transform((y_pred, y))
18+
19+
assert y_pred_t.shape == (6, 2)
20+
assert y_t.shape == (6,)
21+
assert y_t.tolist() == [1, 0, 1, 0, 1, 0]
22+
assert y_pred_t[:, 1].tolist() == pytest.approx([0.9, 0.2, 0.7, 0.1, 0.8, 0.6])
23+
24+
# test (N, C, L)
25+
y_pred_ncl = y_pred.transpose(1, 2).contiguous() # (2, 2, 4)
26+
y_pred_t2, y_t2 = transform((y_pred_ncl, y))
27+
assert y_pred_t2.shape == (6, 2)
28+
assert torch.all(y_pred_t2 == y_pred_t)
29+
assert torch.all(y_t2 == y_t)
30+
31+
# test binary (N, L)
32+
y_pred_bin = torch.tensor([[1, 0, 1, 1], [0, 1, 0, 0]])
33+
y_bin = torch.tensor([[1, 0, 1, 2], [0, 1, 0, 2]])
34+
transform_bin = get_sequence_transform(ignore_index=2)
35+
y_pred_bin_t, y_bin_t = transform_bin((y_pred_bin, y_bin))
36+
37+
assert y_pred_bin_t.shape == (6,)
38+
assert y_bin_t.shape == (6,)
39+
assert y_bin_t.tolist() == [1, 0, 1, 0, 1, 0]
40+
assert y_pred_bin_t.tolist() == [1, 0, 1, 0, 1, 0]
41+
42+
# test without padding
43+
transform_nopad = get_sequence_transform()
44+
y_pred_nopad, y_nopad = transform_nopad((y_pred_bin, y_bin))
45+
assert y_pred_nopad.shape == (8,)
46+
assert y_nopad.shape == (8,)
47+
48+
# test multiple ignore_index values
49+
y_bin = torch.tensor([[1, -1, 1, 2], [0, 1, -1, 2]])
50+
transform_multi = get_sequence_transform(ignore_index=[-1, 2])
51+
y_pred_multi_t, y_multi_t = transform_multi((y_pred_bin, y_bin))
52+
assert y_pred_multi_t.shape == (4,)
53+
assert y_multi_t.shape == (4,)
54+
assert y_multi_t.tolist() == [1, 1, 0, 1]
55+
56+
# test bad shapes
57+
y_bad = torch.tensor([1, 0, 1])
58+
with pytest.raises(ValueError, match="must be 3D and 2D arrays, or both 2D arrays"):
59+
transform((y_pred_bin, y_bad))
60+
61+
y_pred_bad = torch.tensor([[[1], [2]], [[3], [4]]])
62+
y_bad = torch.tensor([[1, 2, 3], [4, 5, 6]])
63+
with pytest.raises(ValueError, match="incompatible sequence shapes"):
64+
transform((y_pred_bad, y_bad))

0 commit comments

Comments
 (0)