from __future__ import annotations
from copy import deepcopy
from typing import Dict, List
import torch
import torch.nn as nn
from pimm.models.losses.builder import LOSSES
from pimm.models.losses.instance_fast import FastSingleLayerInstanceLoss
from pimm.models.panda_detector.matcher_fast import get_target_masks
def _as_list(value):
if value is None:
return []
if isinstance(value, dict):
if "name" in value:
return [value]
return [dict(cfg, name=name) for name, cfg in value.items()]
return list(value)
def _normalize_regression_targets(regression_targets):
targets = []
seen = set()
for cfg in _as_list(regression_targets):
cfg = deepcopy(cfg)
name = cfg.get("name")
if not name:
raise ValueError("Each regression target needs a non-empty 'name'")
if name in seen:
raise ValueError(f"Duplicate regression target name: {name!r}")
seen.add(name)
cfg.setdefault("target_key", name)
cfg.setdefault("pred_key", f"pred_{name}")
cfg.setdefault("aggregation", "mean")
cfg.setdefault("loss_weight", 1.0)
cfg.setdefault(
"criterion",
dict(type="SmoothL1RegressionLoss", beta=1.0, reduction="mean"),
)
cfg.setdefault("required", True)
targets.append(cfg)
return targets
[docs]
@LOSSES.register_module()
class FastInstanceSegmentationRegressionLoss(nn.Module):
"""Instance segmentation loss plus configurable per-instance regressions.
Extends :class:`FastInstanceSegmentationLoss` (same Hungarian matching, focal
mask, Dice, and classification terms) with an arbitrary set of continuous
regression heads supervised on matched query/instance pairs. Each entry in
``regression_targets`` names a prediction key (default ``pred_<name>``), an
``input_dict`` target key (default ``<name>``), a per-instance aggregation
(``"mean"`` or ``"first"``), a per-target ``criterion`` (default
``SmoothL1RegressionLoss``), and a ``loss_weight``. Deep supervision over
``aux_outputs`` is summed and scaled by ``aux_loss_weight``.
``forward(pred, input_dict)`` returns ``(loss, components)`` with the mask/
class diagnostics plus one scalar per regression target. Registered as
``FastInstanceSegmentationRegressionLoss``.
Args:
cost_mask (float): Matcher weight on the focal mask cost. Defaults to
``1.0``.
cost_dice (float): Matcher weight on the Dice cost. Defaults to ``1.0``.
cost_class (float): Matcher weight on the classification cost. Defaults to
``0.0``.
num_points (int): Sampled points for the mask cost (``0`` uses all).
Defaults to ``0``.
ignore_index (int): Instance/segment id treated as void. Defaults to
``-1``.
loss_weight_focal (float): Weight on the focal mask loss. Defaults to
``1.0``.
loss_weight_dice (float): Weight on the Dice loss. Defaults to ``1.0``.
cls_weight_matched (float): Weight on the matched-query CE term. Defaults
to ``2.0``.
cls_weight_noobj (float): Weight on the no-object CE term. Defaults to
``0.1``.
focal_alpha (float): Focal-loss positive-class weight. Defaults to
``0.25``.
focal_gamma (float): Focal-loss focusing exponent. Defaults to ``2.0``.
truth_label (str): ``input_dict`` key holding per-point instance ids.
Defaults to ``"segment"``.
aux_loss_weight (float): Scale on the summed auxiliary loss. Defaults to
``1.0``.
regression_targets: List (or name-keyed dict) of regression-target
configs as described above. Defaults to ``None``.
Example:
.. code-block:: python
>>> import torch
>>> from pimm.models.losses.instance_regression_fast import (
... FastInstanceSegmentationRegressionLoss)
>>> crit = FastInstanceSegmentationRegressionLoss(
... truth_label="instance",
... regression_targets=[dict(name="momentum", loss_weight=1.0)])
>>> # pred needs pred_masks plus pred_<name> for each regression target
>>> pred = {"pred_masks": [torch.randn(8, 50)],
... "pred_momentum": [torch.randn(8, 1)]}
>>> inst = torch.zeros(50, dtype=torch.long)
>>> inst[10:30] = 1; inst[30:50] = 2
>>> input_dict = {"instance": inst.view(-1, 1), "momentum": torch.randn(50, 1)}
>>> loss, comp = crit(pred, input_dict) # scalar loss + diagnostics dict
>>> "momentum" in comp # one scalar per regression target
True
"""
def __init__(
self,
cost_mask: float = 1.0,
cost_dice: float = 1.0,
cost_class: float = 0.0,
num_points: int = 0,
ignore_index: int = -1,
loss_weight_focal: float = 1.0,
loss_weight_dice: float = 1.0,
cls_weight_matched: float = 2.0,
cls_weight_noobj: float = 0.1,
focal_alpha: float = 0.25,
focal_gamma: float = 2.0,
truth_label: str = "segment",
aux_loss_weight: float = 1.0,
regression_targets=None,
):
super().__init__()
self.aux_loss_weight = aux_loss_weight
self.criterion = FastSingleLayerInstanceRegressionLoss(
cost_mask=cost_mask,
cost_dice=cost_dice,
cost_class=cost_class,
num_points=num_points,
ignore_index=ignore_index,
loss_weight_focal=loss_weight_focal,
loss_weight_dice=loss_weight_dice,
cls_weight_matched=cls_weight_matched,
cls_weight_noobj=cls_weight_noobj,
focal_alpha=focal_alpha,
focal_gamma=focal_gamma,
truth_label=truth_label,
regression_targets=regression_targets,
)
def _target_cache(self, pred: Dict, input_dict: Dict) -> List[Dict]:
return self.criterion._build_target_cache(pred["pred_masks"], input_dict)
[docs]
def forward(self, pred: Dict, input_dict: Dict) -> torch.Tensor:
if hasattr(pred, "outputs"):
pred = pred.outputs
target_cache = self._target_cache(pred, input_dict)
final_loss, components = self.criterion(pred, input_dict, target_cache)
if "aux_outputs" in pred and pred["aux_outputs"]:
aux_loss = pred["pred_masks"][0].new_tensor(0.0)
for layer_idx, aux_pred in enumerate(pred["aux_outputs"]):
aux_loss_val, aux_comp = self.criterion(
aux_pred, input_dict, target_cache
)
aux_loss = aux_loss + aux_loss_val
for key, value in aux_comp.items():
if key in self.criterion.aux_component_keys:
components[f"aux_{key}_L{layer_idx}"] = value
final_loss = final_loss + self.aux_loss_weight * aux_loss
return final_loss, components
class FastSingleLayerInstanceRegressionLoss(FastSingleLayerInstanceLoss):
def __init__(self, *args, regression_targets=None, **kwargs):
kwargs.pop("momentum_loss_weight", None)
kwargs.pop("iou_loss_weight", None)
super().__init__(*args, momentum_loss_weight=0.0, iou_loss_weight=0.0, **kwargs)
self.regression_targets = _normalize_regression_targets(regression_targets)
self.regression_losses = nn.ModuleDict()
for cfg in self.regression_targets:
self.regression_losses[cfg["name"]] = LOSSES.build(cfg["criterion"])
self.aux_component_keys = {
"focal",
"dice",
"cls_matched",
"cls_noobj",
*[cfg["name"] for cfg in self.regression_targets],
}
@staticmethod
def _align_regression_shapes(pred, target):
if pred.dim() == 1 and target.dim() == 2 and target.shape[-1] == 1:
target = target.squeeze(-1)
if pred.dim() == 2 and pred.shape[-1] == 1 and target.dim() == 1:
pred = pred.squeeze(-1)
return pred, target
def _aggregate_target(self, values, inverse, num_instances, aggregation):
values = values.float()
if aggregation == "mean":
return self._mean_per_instance(values, inverse, num_instances)
if aggregation == "first":
out = values.new_zeros((num_instances,) + values.shape[1:])
for inst_idx in range(num_instances):
out[inst_idx] = values[inverse == inst_idx][0]
return out
raise ValueError(f"Unknown regression target aggregation: {aggregation!r}")
def _compute_regression_losses(
self,
pred,
input_dict,
batch_idx,
counts,
meta,
idx_q,
idx_gt,
device,
):
losses = {}
for cfg in self.regression_targets:
name = cfg["name"]
pred_key = cfg["pred_key"]
target_key = cfg["target_key"]
weight = float(cfg.get("loss_weight", 1.0))
if weight == 0.0:
continue
missing = pred_key not in pred or target_key not in input_dict
if missing:
if cfg.get("required", True):
raise KeyError(
f"Missing regression target {name!r}: expected "
f"prediction {pred_key!r} and input {target_key!r}"
)
continue
target = self._batch_tensor(
input_dict[target_key],
batch_idx,
counts,
device,
meta["valid_mask"],
)
target_per_inst = self._aggregate_target(
target,
meta["inverse"],
meta["num_instances"],
cfg["aggregation"],
)
pred_b = pred[pred_key][batch_idx].to(device)
pred_matched = pred_b[idx_q.long()]
target_matched = target_per_inst[idx_gt.long()].to(pred_matched.dtype)
pred_matched, target_matched = self._align_regression_shapes(
pred_matched, target_matched
)
losses[name] = weight * self.regression_losses[name](
pred_matched, target_matched
)
return losses
def forward(self, pred: Dict[str, List[torch.Tensor]], input_dict: Dict, target_cache=None):
assert isinstance(pred, dict) and "pred_masks" in pred, (
"pred must be a dict with key 'pred_masks'"
)
assert self.truth_label in input_dict, (
f"input_dict must contain key '{self.truth_label}'"
)
pred_masks_list = pred["pred_masks"]
counts = [pm.shape[1] for pm in pred_masks_list]
if target_cache is None:
target_cache = self._build_target_cache(pred_masks_list, input_dict)
indices = self.matcher(
{
"pred_masks": pred_masks_list,
"pred_logits": pred.get("pred_logits", None),
},
target_cache,
)
total_loss_focal = pred_masks_list[0].new_tensor(0.0)
total_loss_dice = pred_masks_list[0].new_tensor(0.0)
total_loss_cls = pred_masks_list[0].new_tensor(0.0)
total_loss_regression = {
cfg["name"]: pred_masks_list[0].new_tensor(0.0)
for cfg in self.regression_targets
}
num_batches_with_regression = {cfg["name"]: 0 for cfg in self.regression_targets}
num_batches_with_loss = 0
total_focal = pred_masks_list[0].new_tensor(0.0)
total_dice = pred_masks_list[0].new_tensor(0.0)
total_pairs = 0
total_ce_matched = pred_masks_list[0].new_tensor(0.0)
count_ce_matched = pred_masks_list[0].new_tensor(0.0)
total_ce_noobj = pred_masks_list[0].new_tensor(0.0)
count_ce_noobj = pred_masks_list[0].new_tensor(0.0)
total_regression = {
cfg["name"]: pred_masks_list[0].new_tensor(0.0)
for cfg in self.regression_targets
}
queries_total = pred_masks_list[0].new_tensor(0.0)
gt_instances_total = pred_masks_list[0].new_tensor(0.0)
for batch_idx, (pm_b_full, meta, (idx_q, idx_gt)) in enumerate(
zip(pred_masks_list, target_cache, indices)
):
queries_total = queries_total + pm_b_full.shape[0]
if idx_q.numel() == 0 or meta["num_instances"] == 0:
continue
pm_b = pm_b_full[:, meta["valid_mask"]]
gt_instances_total = gt_instances_total + meta["num_instances"]
target_masks = get_target_masks(meta, pm_b.dtype)
idx_q = idx_q.to(pm_b.device).long()
idx_gt = idx_gt.to(pm_b.device).long()
pred_sel = pm_b[idx_q]
num_pairs_b = pred_sel.shape[0]
if num_pairs_b == 0:
continue
focal_per_pair, dice = self._matched_mask_losses(
pred_sel,
target_masks,
meta["target_sizes"].to(pm_b.device),
idx_gt,
)
focal_loss_b = focal_per_pair.sum() / num_pairs_b
dice_loss_b = dice.sum() / num_pairs_b
total_loss_focal = total_loss_focal + focal_loss_b
total_loss_dice = total_loss_dice + dice_loss_b
total_focal = total_focal + focal_per_pair.sum()
total_dice = total_dice + dice.sum()
total_pairs += num_pairs_b
num_batches_with_loss += 1
if "pred_logits" in pred and "segment" in input_dict:
logits_b = pred["pred_logits"][batch_idx]
inst_class = meta["inst_class"].to(pm_b.device)
cls_loss_b, cls_count_b, ce_matched, ce_noobj = self._classification_terms(
logits_b, inst_class, idx_q, idx_gt
)
if cls_count_b > 0:
total_loss_cls = total_loss_cls + (cls_loss_b / cls_count_b)
if ce_matched is not None:
total_ce_matched = total_ce_matched + ce_matched
count_ce_matched = count_ce_matched + 1.0
if ce_noobj is not None:
total_ce_noobj = total_ce_noobj + ce_noobj
count_ce_noobj = count_ce_noobj + 1.0
regression_losses = self._compute_regression_losses(
pred,
input_dict,
batch_idx,
counts,
meta,
idx_q,
idx_gt,
pm_b.device,
)
for name, value in regression_losses.items():
total_loss_regression[name] = total_loss_regression[name] + value
total_regression[name] = total_regression[name] + value
num_batches_with_regression[name] += 1
denom = max(num_batches_with_loss, 1)
loss_masks = self.loss_weight_focal * (
total_loss_focal / denom
) + self.loss_weight_dice * (total_loss_dice / denom)
loss_cls = total_loss_cls / denom
loss = loss_masks + loss_cls
for name, value in total_loss_regression.items():
count = max(num_batches_with_regression[name], 1)
loss = loss + value / count
unmatched_queries = queries_total - total_pairs
unmatched_gt = gt_instances_total - total_pairs
components = {
"focal": total_focal / max(total_pairs, 1),
"dice": total_dice / max(total_pairs, 1),
"cls_matched": total_ce_matched / count_ce_matched.clamp_min(1),
"cls_noobj": total_ce_noobj / count_ce_noobj.clamp_min(1),
"num_pairs": total_pairs,
"queries_total": queries_total,
"gt_instances_total": gt_instances_total,
"unmatched_queries": unmatched_queries,
"unmatched_gt": unmatched_gt,
"num_cls_matched": count_ce_matched,
"num_cls_noobj": count_ce_noobj,
}
for name, value in total_regression.items():
components[name] = value / max(num_batches_with_regression[name], 1)
return loss, components