mmdiff / models /segmentation_losses.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
2267636 verified
Raw
History Blame Contribute Delete
8.06 kB
"""Segmentation losses: Lovasz-Softmax, Focal, Dice, and a combined loss."""
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Optional, List
import numpy as np
def lovasz_grad(gt_sorted):
"""
Compute gradient of the Lovasz extension w.r.t sorted errors.
See Algorithm 1 in paper.
"""
p = len(gt_sorted)
gts = gt_sorted.sum()
intersection = gts - gt_sorted.float().cumsum(0)
union = gts + (1 - gt_sorted).float().cumsum(0)
jaccard = 1.0 - intersection / union
if p > 1:
jaccard[1:p] = jaccard[1:p] - jaccard[0:-1]
return jaccard
def lovasz_softmax_flat(probas, labels, classes='present', ignore_index=255):
"""Multi-class Lovasz-Softmax loss over flat [P,C] probabilities and [P] labels."""
if probas.numel() == 0:
return probas * 0.0
C = probas.size(1)
losses = []
class_to_sum = list(range(C)) if classes == 'all' else []
for c in range(C):
fg = (labels == c).float() # foreground for class c
if classes == 'present' and fg.sum() == 0:
continue
if c == ignore_index:
continue
class_to_sum.append(c) if classes != 'all' else None
errors = (fg - probas[:, c]).abs()
errors_sorted, perm = torch.sort(errors, 0, descending=True)
perm = perm.data
fg_sorted = fg[perm]
losses.append(torch.dot(errors_sorted, lovasz_grad(fg_sorted)))
if len(losses) == 0:
return probas.sum() * 0.0
return torch.stack(losses).mean()
def flatten_probas(probas, labels, ignore_index=255):
"""Flatten [B,C,H,W] preds and [B,H,W] labels to [P,C]/[P], dropping ignored pixels."""
B, C, H, W = probas.size()
probas = probas.permute(0, 2, 3, 1).contiguous().view(-1, C) # [B*H*W, C]
labels = labels.view(-1) # [B*H*W]
if ignore_index is not None:
valid = (labels != ignore_index)
probas = probas[valid]
labels = labels[valid]
return probas, labels
class LovaszSoftmaxLoss(nn.Module):
"""
Lovasz-Softmax loss for multi-class semantic segmentation.
Directly optimizes the mean IoU (Jaccard index).
"""
def __init__(self, classes='present', ignore_index=255):
super().__init__()
self.classes = classes
self.ignore_index = ignore_index
def forward(self, logits, labels):
probas = F.softmax(logits, dim=1)
probas, labels = flatten_probas(probas, labels, self.ignore_index)
return lovasz_softmax_flat(probas, labels, self.classes, self.ignore_index)
class FocalLoss(nn.Module):
"""
Focal Loss for multi-class classification.
FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t)
"""
def __init__(
self,
gamma: float = 2.0,
alpha: Optional[torch.Tensor] = None,
ignore_index: int = 255,
reduction: str = 'mean'
):
super().__init__()
self.gamma = gamma
self.alpha = alpha
self.ignore_index = ignore_index
self.reduction = reduction
def forward(self, logits, labels):
B, C, H, W = logits.shape
ce_loss = F.cross_entropy(
logits, labels,
weight=self.alpha,
ignore_index=self.ignore_index,
reduction='none'
) # [B, H, W]
logits_flat = logits.permute(0, 2, 3, 1).contiguous().view(-1, C) # [B*H*W, C]
labels_flat = labels.view(-1) # [B*H*W]
valid_mask = (labels_flat != self.ignore_index)
probs = F.softmax(logits_flat, dim=1) # [B*H*W, C]
labels_clamped = labels_flat.clamp(0, C-1) # clamp for safe indexing of ignored pixels
p_t = probs.gather(1, labels_clamped.unsqueeze(1)).squeeze(1) # [B*H*W]
focal_weight = (1 - p_t) ** self.gamma # [B*H*W]
focal_weight = focal_weight.view(B, H, W) # [B, H, W]
focal_loss = focal_weight * ce_loss
valid_mask_2d = (labels != self.ignore_index)
if self.reduction == 'mean':
return focal_loss[valid_mask_2d].mean() if valid_mask_2d.sum() > 0 else focal_loss.sum() * 0.0
elif self.reduction == 'sum':
return focal_loss[valid_mask_2d].sum()
else:
return focal_loss
class DiceLoss(nn.Module):
"""
Multi-class Dice loss with optional class weighting.
"""
def __init__(
self,
num_classes: int = 21,
ignore_index: int = 255,
smooth: float = 1e-6,
class_weights: Optional[torch.Tensor] = None,
reduction: str = 'mean'
):
super().__init__()
self.num_classes = num_classes
self.ignore_index = ignore_index
self.smooth = smooth
self.class_weights = class_weights
self.reduction = reduction
def forward(self, logits, labels):
probs = F.softmax(logits, dim=1) # [B, C, H, W]
valid_mask = (labels != self.ignore_index).float() # [B, H, W]
dice_loss = 0.0
valid_classes = 0
class_losses = []
for c in range(self.num_classes):
target_c = (labels == c).float() # [B, H, W]
pred_c = probs[:, c, :, :] # [B, H, W]
target_c = target_c * valid_mask
pred_c = pred_c * valid_mask
intersection = (pred_c * target_c).sum()
union = pred_c.sum() + target_c.sum()
if union > 0:
dice = (2.0 * intersection + self.smooth) / (union + self.smooth)
class_loss = 1.0 - dice
if self.class_weights is not None:
class_loss = class_loss * self.class_weights[c]
class_losses.append(class_loss)
valid_classes += 1
if valid_classes > 0:
if self.reduction == 'mean':
return torch.stack(class_losses).mean()
else:
return torch.stack(class_losses).sum()
return logits.sum() * 0.0
class CombinedSegmentationLoss(nn.Module):
"""Weighted combination of CE/Focal, Dice, and Lovasz-Softmax losses."""
def __init__(
self,
num_classes: int = 21,
ignore_index: int = 255,
weight_ce: float = 1.0,
weight_dice: float = 1.0,
weight_lovasz: float = 1.0,
use_focal: bool = True,
focal_gamma: float = 2.0,
class_weights: Optional[torch.Tensor] = None,
):
super().__init__()
self.weight_ce = weight_ce
self.weight_dice = weight_dice
self.weight_lovasz = weight_lovasz
self.use_focal = use_focal
if use_focal:
self.ce_loss = FocalLoss(
gamma=focal_gamma,
alpha=class_weights,
ignore_index=ignore_index,
)
else:
self.ce_loss = nn.CrossEntropyLoss(
weight=class_weights,
ignore_index=ignore_index,
)
self.dice_loss = DiceLoss(
num_classes=num_classes,
ignore_index=ignore_index,
class_weights=class_weights,
)
self.lovasz_loss = LovaszSoftmaxLoss(
classes='present',
ignore_index=ignore_index,
)
def forward(self, logits, labels):
loss_ce = self.ce_loss(logits, labels)
loss_dice = self.dice_loss(logits, labels)
loss_lovasz = self.lovasz_loss(logits, labels)
loss_total = (
self.weight_ce * loss_ce +
self.weight_dice * loss_dice +
self.weight_lovasz * loss_lovasz
)
return {
'loss_ce': loss_ce,
'loss_dice': loss_dice,
'loss_lovasz': loss_lovasz,
'loss_total': loss_total,
}