"""
Misc Losses
Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com)
Please cite our work if the code is helpful to you.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Optional, Dict, Union
from torch import Tensor
from .builder import LOSSES
from torch.nn.modules.loss import _WeightedLoss
[docs]
@LOSSES.register_module()
class CrossEntropyLoss(nn.Module):
"""Standard multi-class cross-entropy over per-point class logits.
Thin wrapper around :class:`torch.nn.CrossEntropyLoss`. ``forward(pred,
target)`` expects ``pred`` of shape ``(N, C)`` (logits) and ``target`` of
shape ``(N,)`` or ``(N, 1)`` (the trailing dim is squeezed); returns the
reduced loss scaled by ``loss_weight``. Registered as ``CrossEntropyLoss`` --
use in a ``criteria=[...]`` list (assembled by ``build_criteria``).
Args:
weight (list | None): Per-class rescaling weights; moved to CUDA when
given. Defaults to ``None``.
size_average (bool | None): Deprecated torch reduction flag. Defaults to
``None``.
reduce (bool | None): Deprecated torch reduction flag. Defaults to
``None``.
reduction (str): Reduction mode (``"mean"``, ``"sum"``, ``"none"``).
Defaults to ``"mean"``.
label_smoothing (float): Label-smoothing factor in ``[0, 1)``. Defaults
to ``0.0``.
loss_weight (float): Global scale on the returned loss. Defaults to
``1.0``.
ignore_index (int): Target value excluded from the loss. Defaults to
``-1``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="CrossEntropyLoss", ignore_index=-1)])
>>> pred = torch.randn(4, 3) # (N=4 points, C=3 classes) logits
>>> target = torch.tensor([0, 2, 1, -1]) # -1 is ignored
>>> crit(pred, target) # scalar loss
tensor(1.0995)
"""
def __init__(
self,
weight=None,
size_average=None,
reduce=None,
reduction="mean",
label_smoothing=0.0,
loss_weight=1.0,
ignore_index=-1,
):
super(CrossEntropyLoss, self).__init__()
weight = torch.tensor(weight).cuda() if weight is not None else None
self.loss_weight = loss_weight
self.loss = nn.CrossEntropyLoss(
weight=weight,
size_average=size_average,
ignore_index=ignore_index,
reduce=reduce,
reduction=reduction,
label_smoothing=label_smoothing,
)
[docs]
def forward(self, pred, target):
return self.loss(pred, target.squeeze(-1)) * self.loss_weight
[docs]
@LOSSES.register_module()
class SmoothCELoss(nn.Module):
"""Label-smoothed cross-entropy with NaN-robust averaging.
``forward(pred, target)`` expects ``pred`` of shape ``(N, C)`` (logits) and
``target`` of shape ``(N,)`` (class indices); builds a smoothed one-hot
target, computes the soft cross-entropy, and averages over finite entries.
Registered as ``SmoothCELoss`` -- use in a ``criteria=[...]`` list.
Args:
smoothing_ratio (float): Smoothing mass spread over the non-target
classes. Defaults to ``0.1``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="SmoothCELoss", smoothing_ratio=0.1)])
>>> pred = torch.randn(4, 3) # (N=4, C=3) logits
>>> target = torch.tensor([0, 2, 1, 0]) # class indices (no ignore_index)
>>> crit(pred, target) # scalar: smoothed soft-CE, averaged over finite entries
"""
def __init__(self, smoothing_ratio=0.1):
super(SmoothCELoss, self).__init__()
self.smoothing_ratio = smoothing_ratio
[docs]
def forward(self, pred, target):
eps = self.smoothing_ratio
n_class = pred.size(1)
one_hot = torch.zeros_like(pred).scatter(1, target.view(-1, 1), 1)
one_hot = one_hot * (1 - eps) + (1 - one_hot) * eps / (n_class - 1)
log_prb = F.log_softmax(pred, dim=1)
loss = -(one_hot * log_prb).total(dim=1)
loss = loss[torch.isfinite(loss)].mean()
return loss
[docs]
@LOSSES.register_module()
class SmoothL1RegressionLoss(nn.Module):
"""Smooth-L1 (Huber) regression loss for continuous targets.
``forward(pred, target)`` returns ``loss_weight * smooth_l1_loss(pred,
target)`` with the configured transition point and reduction; ``pred`` and
``target`` must broadcast. Registered as ``SmoothL1RegressionLoss`` -- use in
a ``criteria=[...]`` list or as a per-head ``criterion`` in the instance
regression losses.
Args:
beta (float): Transition point between the L2 and L1 regimes. Defaults to
``1.0``.
reduction (str): Reduction mode (``"mean"``, ``"sum"``, ``"none"``).
Defaults to ``"mean"``.
loss_weight (float): Global scale on the returned loss. Defaults to
``1.0``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="SmoothL1RegressionLoss", beta=1.0)])
>>> pred = torch.zeros(4)
>>> target = torch.ones(4) # |err|=1 == beta -> 0.5 * err^2
>>> crit(pred, target) # scalar loss
tensor(0.5000)
"""
def __init__(self, beta=1.0, reduction="mean", loss_weight=1.0):
super().__init__()
self.beta = beta
self.reduction = reduction
self.loss_weight = loss_weight
[docs]
def forward(self, pred, target):
return self.loss_weight * F.smooth_l1_loss(
pred, target, beta=self.beta, reduction=self.reduction
)
[docs]
@LOSSES.register_module()
class L1RegressionLoss(nn.Module):
"""Mean-absolute-error (L1) regression loss for continuous targets.
``forward(pred, target)`` returns ``loss_weight * l1_loss(pred, target)``;
``pred`` and ``target`` must broadcast. Registered as ``L1RegressionLoss`` --
use in a ``criteria=[...]`` list or as a per-head ``criterion``.
Args:
reduction (str): Reduction mode (``"mean"``, ``"sum"``, ``"none"``).
Defaults to ``"mean"``.
loss_weight (float): Global scale on the returned loss. Defaults to
``1.0``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="L1RegressionLoss", loss_weight=1.0)])
>>> crit(torch.zeros(4), torch.ones(4)) # mean |0 - 1| = 1.0
tensor(1.)
"""
def __init__(self, reduction="mean", loss_weight=1.0):
super().__init__()
self.reduction = reduction
self.loss_weight = loss_weight
[docs]
def forward(self, pred, target):
return self.loss_weight * F.l1_loss(pred, target, reduction=self.reduction)
[docs]
@LOSSES.register_module()
class MSERegressionLoss(nn.Module):
"""Mean-squared-error (L2) regression loss for continuous targets.
``forward(pred, target)`` returns ``loss_weight * mse_loss(pred, target)``;
``pred`` and ``target`` must broadcast. Registered as ``MSERegressionLoss`` --
use in a ``criteria=[...]`` list or as a per-head ``criterion``.
Args:
reduction (str): Reduction mode (``"mean"``, ``"sum"``, ``"none"``).
Defaults to ``"mean"``.
loss_weight (float): Global scale on the returned loss. Defaults to
``1.0``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="MSERegressionLoss", loss_weight=1.0)])
>>> crit(torch.zeros(4), torch.full((4,), 2.0)) # mean (0 - 2)^2 = 4.0
tensor(4.)
"""
def __init__(self, reduction="mean", loss_weight=1.0):
super().__init__()
self.reduction = reduction
self.loss_weight = loss_weight
[docs]
def forward(self, pred, target):
return self.loss_weight * F.mse_loss(pred, target, reduction=self.reduction)
[docs]
@LOSSES.register_module()
class CrossEntropyHeadLoss(nn.Module):
"""Cross-entropy for an extra categorical per-query head.
``forward(pred, target)`` expects ``pred`` of shape ``(M, num_classes)``
(matched-query logits) and ``target`` of shape ``(M,)`` (per-instance class
indices, cast to long); returns ``loss_weight * cross_entropy(...)``. Intended
as the per-head ``criterion`` for auxiliary categorical heads (beyond the
primary PID head) in the unified instance loss. Registered as
``CrossEntropyHeadLoss``.
Args:
reduction (str): Reduction mode (``"mean"``, ``"sum"``, ``"none"``).
Defaults to ``"mean"``.
loss_weight (float): Global scale on the returned loss. Defaults to
``1.0``.
label_smoothing (float): Label-smoothing factor in ``[0, 1)``. Defaults
to ``0.0``.
ignore_index (int): Target value excluded from the loss. Defaults to
``-100``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="CrossEntropyHeadLoss")])
>>> pred = torch.randn(3, 5) # (M=3 matched queries, num_classes=5) logits
>>> target = torch.tensor([0, 1, 2]) # per-instance class indices
>>> crit(pred, target) # scalar loss
tensor(2.1342)
>>> # used as a per-query head criterion in the unified instance loss:
>>> # query_heads=[dict(name="pid", kind="categorical",
>>> # criterion=dict(type="CrossEntropyHeadLoss"))]
"""
def __init__(self, reduction="mean", loss_weight=1.0, label_smoothing=0.0,
ignore_index=-100):
super().__init__()
self.reduction = reduction
self.loss_weight = loss_weight
self.label_smoothing = label_smoothing
self.ignore_index = ignore_index
[docs]
def forward(self, pred, target):
return self.loss_weight * F.cross_entropy(
pred,
target.long(),
reduction=self.reduction,
label_smoothing=self.label_smoothing,
ignore_index=self.ignore_index,
)
[docs]
@LOSSES.register_module()
class BinaryFocalLoss(nn.Module):
"""Binary focal loss for class-imbalanced binary targets.
Focal loss (`Lin et al. 2017 <https://arxiv.org/abs/1708.02002>`_) for a
single binary output. ``forward(pred, target)`` expects ``pred`` and
``target`` of matching shape ``(N,)`` (or broadcastable); ``pred`` is treated
as logits when ``logits`` is ``True``, else as probabilities. Returns the
(optionally mean-reduced) loss scaled by ``loss_weight``. Registered as
``BinaryFocalLoss`` -- use in a ``criteria=[...]`` list.
Args:
gamma (float): Focusing exponent that down-weights easy examples.
Defaults to ``2.0``.
alpha (float): Positive-class weight in ``(0, 1)``. Defaults to ``0.5``.
logits (bool): Whether ``pred`` is raw logits (vs probabilities).
Defaults to ``True``.
reduce (bool): Mean-reduce the per-element loss when ``True``. Defaults to
``True``.
loss_weight (float): Global scale on the returned loss. Defaults to
``1.0``.
weight (torch.Tensor | None): Optional per-element BCE weight. Defaults to
``None``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="BinaryFocalLoss", gamma=2.0, alpha=0.5)])
>>> pred = torch.zeros(4) # logits (p=0.5 for all)
>>> target = torch.tensor([0., 1., 0., 1.])
>>> crit(pred, target) # scalar loss
tensor(0.0866)
"""
def __init__(self, gamma=2.0, alpha=0.5, logits=True, reduce=True, loss_weight=1.0, weight=None):
"""Binary Focal Loss
<https://arxiv.org/abs/1708.02002>`
"""
super(BinaryFocalLoss, self).__init__()
assert 0 < alpha < 1
self.gamma = gamma
self.alpha = alpha
self.logits = logits
self.reduce = reduce
self.loss_weight = loss_weight
self.weight = weight
[docs]
def forward(self, pred, target, **kwargs):
"""Forward function.
Args:
pred (torch.Tensor): The prediction with shape (N).
target (torch.Tensor): The ground truth. If containing class
indices, shape (N) where each value is ``0 <= target[i] <= 1``;
if containing class probabilities, same shape as the input.
Returns:
torch.Tensor: The calculated loss.
"""
if self.logits:
bce = F.binary_cross_entropy_with_logits(pred, target, reduction="none", weight=self.weight)
else:
bce = F.binary_cross_entropy(pred, target, reduction="none", weight=self.weight)
pt = torch.exp(-bce)
alpha = self.alpha * target + (1 - self.alpha) * (1 - target)
focal_loss = alpha * (1 - pt) ** self.gamma * bce
if self.reduce:
focal_loss = torch.mean(focal_loss)
return focal_loss * self.loss_weight
[docs]
@LOSSES.register_module()
class FocalLoss(_WeightedLoss):
"""Multi-class focal loss over per-point class logits.
Focal loss (`Lin et al. 2017 <https://arxiv.org/abs/1708.02002>`_) for
multi-class classification. ``forward(pred, target)`` flattens ``pred`` to
``(N, C)`` (logits) and ``target`` to ``(N,)`` (class indices), applies the
``(1 - p_t) ** gamma`` focal modulation on top of cross-entropy, and reduces
over non-ignored points. The optional class ``weight`` is indexed by the
ground-truth label when ``penalize_pred`` is ``True``, otherwise by the
predicted (argmax) label. Returns the loss scaled by ``loss_weight``.
Registered as ``FocalLoss`` -- use in a ``criteria=[...]`` list.
Args:
weight (torch.Tensor | None): Per-class rescaling weights. Defaults to
``None``.
size_average (bool | None): Deprecated torch reduction flag. Defaults to
``None``.
reduce (bool | None): Deprecated torch reduction flag. Defaults to
``None``.
reduction (str): Reduction mode (``"mean"``, ``"sum"``, ``"none"``); the
``"mean"`` path divides by the count of non-ignored targets. Defaults
to ``"mean"``.
gamma (float): Focusing exponent that down-weights easy examples.
Defaults to ``2``.
ignore_index (int): Target value excluded from the loss. Defaults to
``-1``.
loss_weight (float): Global scale on the returned loss. Defaults to
``1.0``.
penalize_pred (bool): Index the class ``weight`` by the ground-truth label
instead of the predicted label. Defaults to ``False``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="FocalLoss", gamma=2.0, loss_weight=1.0)])
>>> pred = torch.randn(4, 3) # (N=4 points, C=3 classes) logits
>>> target = torch.tensor([0, 2, 1, -1]) # -1 is ignored
>>> crit(pred, target) # scalar loss
tensor(0.9830)
"""
def __init__(
self,
weight: Optional[Tensor] = None,
size_average: Optional[bool] = None,
reduce: Optional[bool] = None,
reduction: str = "mean",
gamma: float = 2,
ignore_index: int = -1,
loss_weight: float = 1.0,
penalize_pred: bool = False,
):
super().__init__(
torch.tensor(weight) if weight is not None else None,
size_average,
reduce,
reduction,
)
self.gamma = gamma
self.ignore_index = ignore_index
self.loss_weight = loss_weight
self.penalize_pred = penalize_pred
[docs]
def forward(self, pred, target):
if self.weight is not None and self.weight.device != pred.device:
self.weight = self.weight.to(pred.device)
flattened_logits = pred.reshape(-1, pred.shape[-1])
flattened_labels = target.view(-1).long()
p_t = flattened_logits.softmax(dim=-1)
ce_loss = F.cross_entropy(
flattened_logits,
flattened_labels,
reduction="none",
ignore_index=self.ignore_index,
) # -log(p_t)
if self.weight is not None:
alpha_t = self.weight[
flattened_labels
if self.penalize_pred
else flattened_logits.argmax(dim=-1)
]
else:
alpha_t = 1.0
loss = (
alpha_t
* ((1 - p_t[torch.arange(p_t.shape[0]), flattened_labels]) ** self.gamma)
* ce_loss
)
if self.reduction == "mean":
loss = loss.sum() / target.ne(self.ignore_index).sum()
elif self.reduction == "sum":
loss = loss.sum()
elif self.reduction == "none":
pass
else:
raise ValueError(f"Invalid reduction: {self.reduction}")
return self.loss_weight * loss
[docs]
@LOSSES.register_module()
class DiceLoss(nn.Module):
"""Soft multi-class Dice loss over per-point class probabilities.
Dice loss (`V-Net, Milletari et al. 2016
<https://arxiv.org/abs/1606.04797>`_). ``forward(pred, target)`` reshapes
``pred`` to ``(N, C)`` (logits, soft-maxed internally) and ``target`` to
``(N,)`` (class indices), drops ignored points, one-hot encodes the targets,
and averages ``1 - Dice`` across classes. Returns the loss scaled by
``loss_weight``. Registered as ``DiceLoss`` -- use in a ``criteria=[...]``
list, commonly alongside a cross-entropy or Lovasz term.
Args:
smooth (float): Smoothing constant added to the Dice numerator and
denominator. Defaults to ``1``.
exponent (float): Exponent applied to the per-class terms in the
denominator. Defaults to ``2``.
loss_weight (float): Global scale on the returned loss. Defaults to
``1.0``.
ignore_index (int): Target value excluded from the loss. Defaults to
``-1``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.builder import build_criteria
>>> crit = build_criteria([dict(type="DiceLoss", loss_weight=1.0)])
>>> pred = torch.randn(4, 3) # (N=4 points, C=3 classes) logits
>>> target = torch.tensor([0, 2, 1, 0]) # class indices
>>> crit(pred, target) # scalar 1 - Dice averaged over classes
tensor(0.5031)
"""
def __init__(self, smooth=1, exponent=2, loss_weight=1.0, ignore_index=-1):
"""DiceLoss.
This loss is proposed in `V-Net: Fully Convolutional Neural Networks for
Volumetric Medical Image Segmentation <https://arxiv.org/abs/1606.04797>`_.
"""
super(DiceLoss, self).__init__()
self.smooth = smooth
self.exponent = exponent
self.loss_weight = loss_weight
self.ignore_index = ignore_index
[docs]
def forward(self, pred, target, **kwargs):
# [B, C, d_1, d_2, ..., d_k] -> [C, B, d_1, d_2, ..., d_k]
pred = pred.transpose(0, 1)
# [C, B, d_1, d_2, ..., d_k] -> [C, N]
pred = pred.reshape(pred.size(0), -1)
# [C, N] -> [N, C]
pred = pred.transpose(0, 1).contiguous()
# (B, d_1, d_2, ..., d_k) --> (B * d_1 * d_2 * ... * d_k,)
target = target.view(-1).contiguous()
assert pred.size(0) == target.size(
0
), "The shape of pred doesn't match the shape of target"
valid_mask = target != self.ignore_index
target = target[valid_mask]
pred = pred[valid_mask]
pred = F.softmax(pred, dim=1)
num_classes = pred.shape[1]
target = F.one_hot(
torch.clamp(target.long(), 0, num_classes - 1), num_classes=num_classes
)
total_loss = 0
for i in range(num_classes):
if i != self.ignore_index:
num = torch.sum(torch.mul(pred[:, i], target[:, i])) * 2 + self.smooth
den = (
torch.sum(
pred[:, i].pow(self.exponent) + target[:, i].pow(self.exponent)
)
+ self.smooth
)
dice_loss = 1 - num / den
total_loss += dice_loss
loss = total_loss / num_classes
return self.loss_weight * loss