@@ -114,6 +114,7 @@ def compute_multitask_loss(
114114 outputs : torch .Tensor ,
115115 labels : torch .Tensor ,
116116 stage : str = "train" ,
117+ mask : Optional [torch .Tensor ] = None ,
117118 ) -> Tuple [torch .Tensor , Dict [str , float ]]:
118119 """
119120 Compute losses for multi-task learning where different output channels
@@ -181,15 +182,26 @@ def compute_multitask_loss(
181182 loss_fn = self .loss_functions [loss_idx ]
182183 weight = self .loss_weights [loss_idx ]
183184
184- # [D3] Pass foreground-weighted mask to loss functions that support it
185- # (e.g., WeightedMSELoss, WeightedMAELoss, SmoothL1Loss)
185+ # [D3] Pass foreground-weighted mask (and validity mask) to loss functions
186186 if _loss_supports_weight (loss_fn ):
187187 fg_weight = 2.0
188188 loss_weight_mask = torch .ones_like (task_label )
189189 loss_weight_mask [task_label > 0 ] = fg_weight
190+ if mask is not None :
191+ # Zero weight for voxels outside valid mask — excluded from gradient
192+ loss_weight_mask = loss_weight_mask * (mask > 0 ).float ()
190193 loss = loss_fn (task_output , task_label , weight = loss_weight_mask )
191194 else :
192- loss = loss_fn (task_output , task_label )
195+ if mask is not None :
196+ # For losses without per-voxel weight support (e.g. DiceLoss):
197+ # suppress output to large-negative (sigmoid≈0) and zero label
198+ # in invalid regions so they contribute ≈0 to numerator/denominator
199+ mask_bool = (mask > 0 )
200+ task_output_m = task_output .masked_fill (~ mask_bool , - 20.0 )
201+ task_label_m = task_label * mask_bool .float ()
202+ loss = loss_fn (task_output_m , task_label_m )
203+ else :
204+ loss = loss_fn (task_output , task_label )
193205
194206 # Check for NaN/Inf
195207 if self .enable_nan_detection and (torch .isnan (loss ) or torch .isinf (loss )):
@@ -246,7 +258,12 @@ def compute_multitask_loss(
246258 return total_loss , loss_dict
247259
248260 def compute_loss_for_scale (
249- self , output : torch .Tensor , target : torch .Tensor , scale_idx : int , stage : str = "train"
261+ self ,
262+ output : torch .Tensor ,
263+ target : torch .Tensor ,
264+ scale_idx : int ,
265+ stage : str = "train" ,
266+ mask : Optional [torch .Tensor ] = None ,
250267 ) -> Tuple [torch .Tensor , Dict [str , float ]]:
251268 """
252269 Compute loss for a single scale with multi-task or standard loss.
@@ -288,7 +305,20 @@ def compute_loss_for_scale(
288305 loss_fn = self .loss_functions [loss_idx ]
289306 weight = self .loss_weights [loss_idx ]
290307
291- loss = loss_fn (task_output , task_target )
308+ if mask is not None and _loss_supports_weight (loss_fn ):
309+ fg_weight = 2.0
310+ loss_weight_mask = torch .ones_like (task_target )
311+ loss_weight_mask [task_target > 0 ] = fg_weight
312+ loss_weight_mask = loss_weight_mask * (mask > 0 ).float ()
313+ loss = loss_fn (task_output , task_target , weight = loss_weight_mask )
314+ elif mask is not None :
315+ mask_bool = (mask > 0 )
316+ loss = loss_fn (
317+ task_output .masked_fill (~ mask_bool , - 20.0 ),
318+ task_target * mask_bool .float (),
319+ )
320+ else :
321+ loss = loss_fn (task_output , task_target )
292322
293323 # Check for NaN/Inf (only in training mode)
294324 if (
@@ -376,7 +406,11 @@ def compute_loss_for_scale(
376406 return scale_loss , loss_dict
377407
378408 def compute_deep_supervision_loss (
379- self , outputs : Dict [str , torch .Tensor ], labels : torch .Tensor , stage : str = "train"
409+ self ,
410+ outputs : Dict [str , torch .Tensor ],
411+ labels : torch .Tensor ,
412+ stage : str = "train" ,
413+ mask : Optional [torch .Tensor ] = None ,
380414 ) -> Tuple [torch .Tensor , Dict [str , float ]]:
381415 """
382416 Compute multi-scale loss with deep supervision.
@@ -385,6 +419,7 @@ def compute_deep_supervision_loss(
385419 outputs: Dictionary with 'output' and 'ds_i' keys for deep supervision
386420 labels: Ground truth labels
387421 stage: 'train' or 'val' for logging prefix
422+ mask: Optional binary validity mask (B, 1, D, H, W); loss is zeroed outside mask
388423
389424 Returns:
390425 Tuple of (total_loss, loss_dict)
@@ -419,9 +454,19 @@ def compute_deep_supervision_loss(
419454 # Match target to output size
420455 target = match_target_to_output (labels , output )
421456
457+ # Downsample mask to match this scale using nearest interpolation
458+ scale_mask = None
459+ if mask is not None :
460+ if mask .shape [2 :] != output .shape [2 :]:
461+ scale_mask = F .interpolate (
462+ mask .float (), size = output .shape [2 :], mode = "nearest"
463+ )
464+ else :
465+ scale_mask = mask
466+
422467 # Compute loss for this scale
423468 scale_loss , scale_loss_dict = self .compute_loss_for_scale (
424- output , target , scale_idx , stage
469+ output , target , scale_idx , stage , mask = scale_mask
425470 )
426471
427472 # Accumulate with deep supervision weight
@@ -432,7 +477,11 @@ def compute_deep_supervision_loss(
432477 return total_loss , loss_dict
433478
434479 def compute_standard_loss (
435- self , outputs : torch .Tensor , labels : torch .Tensor , stage : str = "train"
480+ self ,
481+ outputs : torch .Tensor ,
482+ labels : torch .Tensor ,
483+ stage : str = "train" ,
484+ mask : Optional [torch .Tensor ] = None ,
436485 ) -> Tuple [torch .Tensor , Dict [str , float ]]:
437486 """
438487 Compute standard single-scale loss.
@@ -441,6 +490,7 @@ def compute_standard_loss(
441490 outputs: Model outputs (B, C, D, H, W)
442491 labels: Ground truth labels (B, C, D, H, W)
443492 stage: 'train' or 'val' for logging prefix
493+ mask: Optional binary validity mask (B, 1, D, H, W); loss is zeroed outside mask
444494
445495 Returns:
446496 Tuple of (total_loss, loss_dict)
@@ -451,18 +501,29 @@ def compute_standard_loss(
451501 # Check if multi-task learning is configured
452502 if self .is_multi_task :
453503 # Multi-task learning: apply specific losses to specific channels
454- total_loss , loss_dict = self .compute_multitask_loss (outputs , labels , stage = stage )
504+ total_loss , loss_dict = self .compute_multitask_loss (
505+ outputs , labels , stage = stage , mask = mask
506+ )
455507 else :
456508 # Standard single-scale loss: apply all losses to all outputs
457509 for i , (loss_fn , weight ) in enumerate (zip (self .loss_functions , self .loss_weights )):
458- # [D3] Pass foreground-weighted mask to loss functions that support it
510+ # [D3] Pass foreground-weighted mask (and validity mask) to loss functions
459511 if _loss_supports_weight (loss_fn ):
460512 fg_weight = 2.0
461513 loss_weight_mask = torch .ones_like (labels )
462514 loss_weight_mask [labels > 0 ] = fg_weight
515+ if mask is not None :
516+ loss_weight_mask = loss_weight_mask * (mask > 0 ).float ()
463517 loss = loss_fn (outputs , labels , weight = loss_weight_mask )
464518 else :
465- loss = loss_fn (outputs , labels )
519+ if mask is not None :
520+ mask_bool = (mask > 0 )
521+ loss = loss_fn (
522+ outputs .masked_fill (~ mask_bool , - 20.0 ),
523+ labels * mask_bool .float (),
524+ )
525+ else :
526+ loss = loss_fn (outputs , labels )
466527
467528 # Check for NaN/Inf (only in training mode)
468529 if (
0 commit comments