Skip to content

Commit e690fbd

Browse files
committed
Feat: add ignore_index support to DiceLoss
Signed-off-by: qwepablo12 <grazava6@gmail.com>
1 parent d6713fd commit e690fbd

1 file changed

Lines changed: 24 additions & 63 deletions

File tree

monai/losses/dice.py

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -67,45 +67,11 @@ def __init__(
6767
batch: bool = False,
6868
weight: Sequence[float] | float | int | torch.Tensor | None = None,
6969
soft_label: bool = False,
70+
ignore_index: int | None = None,
7071
) -> None:
7172
"""
72-
Args:
73-
include_background: if False, channel index 0 (background category) is excluded from the calculation.
74-
if the non-background segmentations are small compared to the total image size they can get overwhelmed
75-
by the signal from the background so excluding it in such cases helps convergence.
76-
to_onehot_y: whether to convert the ``target`` into the one-hot format,
77-
using the number of classes inferred from `input` (``input.shape[1]``). Defaults to False.
78-
sigmoid: if True, apply a sigmoid function to the prediction.
79-
softmax: if True, apply a softmax function to the prediction.
80-
other_act: callable function to execute other activation layers, Defaults to ``None``. for example:
81-
``other_act = torch.tanh``.
82-
squared_pred: use squared versions of targets and predictions in the denominator or not.
83-
jaccard: compute Jaccard Index (soft IoU) instead of dice or not.
84-
reduction: {``"none"``, ``"mean"``, ``"sum"``}
85-
Specifies the reduction to apply to the output. Defaults to ``"mean"``.
86-
87-
- ``"none"``: no reduction will be applied.
88-
- ``"mean"``: the sum of the output will be divided by the number of elements in the output.
89-
- ``"sum"``: the output will be summed.
90-
91-
smooth_nr: a small constant added to the numerator to avoid zero.
92-
smooth_dr: a small constant added to the denominator to avoid nan.
93-
batch: whether to sum the intersection and union areas over the batch dimension before the dividing.
94-
Defaults to False, a Dice loss value is computed independently from each item in the batch
95-
before any `reduction`.
96-
weight: weights to apply to the voxels of each class. If None no weights are applied.
97-
The input can be a single value (same weight for all classes), a sequence of values (the length
98-
of the sequence should be the same as the number of classes. If not ``include_background``,
99-
the number of classes should not include the background category class 0).
100-
The value/values should be no less than 0. Defaults to None.
101-
soft_label: whether the target contains non-binary values (soft labels) or not.
102-
If True a soft label formulation of the loss will be used.
103-
104-
Raises:
105-
TypeError: When ``other_act`` is not an ``Optional[Callable]``.
106-
ValueError: When more than 1 of [``sigmoid=True``, ``softmax=True``, ``other_act is not None``].
107-
Incompatible values.
108-
73+
Args follow standard MONAI DiceLoss with the addition of:
74+
ignore_index: Specifies a target value that is ignored and does not contribute to the input gradient.
10975
"""
11076
super().__init__(reduction=LossReduction(reduction).value)
11177
if other_act is not None and not callable(other_act):
@@ -126,29 +92,13 @@ def __init__(
12692
self.register_buffer("class_weight", weight)
12793
self.class_weight: None | torch.Tensor
12894
self.soft_label = soft_label
95+
self.ignore_index = ignore_index
12996

13097
def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
13198
"""
13299
Args:
133100
input: the shape should be BNH[WD], where N is the number of classes.
134101
target: the shape should be BNH[WD] or B1H[WD], where N is the number of classes.
135-
136-
Raises:
137-
AssertionError: When input and target (after one hot transform if set)
138-
have different shapes.
139-
ValueError: When ``self.reduction`` is not one of ["mean", "sum", "none"].
140-
141-
Example:
142-
>>> from monai.losses.dice import * # NOQA
143-
>>> import torch
144-
>>> from monai.losses.dice import DiceLoss
145-
>>> B, C, H, W = 7, 5, 3, 2
146-
>>> input = torch.rand(B, C, H, W)
147-
>>> target_idx = torch.randint(low=0, high=C - 1, size=(B, H, W)).long()
148-
>>> target = one_hot(target_idx[:, None, ...], num_classes=C)
149-
>>> self = DiceLoss(reduction='none')
150-
>>> loss = self(input, target)
151-
>>> assert np.broadcast_shapes(loss.shape, input.shape) == input.shape
152102
"""
153103
if self.sigmoid:
154104
input = torch.sigmoid(input)
@@ -163,27 +113,42 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
163113
if self.other_act is not None:
164114
input = self.other_act(input)
165115

116+
# Create valid mask if ignore_index is specified and target is in index format
117+
valid_mask = None
118+
if self.ignore_index is not None and target.shape[1] == 1:
119+
valid_mask = (target != self.ignore_index).to(input.dtype)
120+
166121
if self.to_onehot_y:
167122
if n_pred_ch == 1:
168123
warnings.warn("single channel prediction, `to_onehot_y=True` ignored.", stacklevel=2)
169124
else:
170125
target = one_hot(target, num_classes=n_pred_ch)
171126

127+
# Create valid mask if target was already one-hot but ignore_index channel is specified
128+
if self.ignore_index is not None and valid_mask is None:
129+
if 0 <= self.ignore_index < target.shape[1]:
130+
valid_mask = torch.ones_like(target)
131+
valid_mask[:, self.ignore_index] = 0.0
132+
172133
if not self.include_background:
173134
if n_pred_ch == 1:
174135
warnings.warn("single channel prediction, `include_background=False` ignored.", stacklevel=2)
175136
else:
176-
# if skipping background, removing first channel
177137
target = target[:, 1:]
178138
input = input[:, 1:]
139+
if valid_mask is not None and valid_mask.shape[1] == n_pred_ch:
140+
valid_mask = valid_mask[:, 1:]
179141

180142
if target.shape != input.shape:
181143
raise AssertionError(f"ground truth has different shape ({target.shape}) from input ({input.shape})")
182144

183-
# reducing only spatial dimensions (not batch nor channels)
145+
# Apply mask to both predictions and targets to exclude ignored regions
146+
if valid_mask is not None:
147+
input = input * valid_mask
148+
target = target * valid_mask
149+
184150
reduce_axis: list[int] = torch.arange(2, len(input.shape)).tolist()
185151
if self.batch:
186-
# reducing spatial dimensions and batch
187152
reduce_axis = [0] + reduce_axis
188153

189154
ord = 2 if self.squared_pred else 1
@@ -198,7 +163,6 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
198163

199164
num_of_classes = target.shape[1]
200165
if self.class_weight is not None and num_of_classes != 1:
201-
# make sure the lengths of weights are equal to the number of classes
202166
if self.class_weight.ndim == 0:
203167
self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes)
204168
else:
@@ -209,16 +173,13 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
209173
)
210174
if self.class_weight.min() < 0:
211175
raise ValueError("the value/values of the `weight` should be no less than 0.")
212-
# apply class_weight to loss
213176
f = f * self.class_weight.to(f)
214177

215178
if self.reduction == LossReduction.MEAN.value:
216-
f = torch.mean(f) # the batch and channel average
179+
f = torch.mean(f)
217180
elif self.reduction == LossReduction.SUM.value:
218-
f = torch.sum(f) # sum over the batch and channel dims
181+
f = torch.sum(f)
219182
elif self.reduction == LossReduction.NONE.value:
220-
# If we are not computing voxelwise loss components at least
221-
# make sure a none reduction maintains a broadcastable shape
222183
broadcast_shape = list(f.shape[0:2]) + [1] * (len(input.shape) - 2)
223184
f = f.view(broadcast_shape)
224185
else:

0 commit comments

Comments
 (0)