Source code for pimm.models.losses.instance_unified_fast

"""Instance mask, class, and per-query-head losses for detector-v5."""

from __future__ import annotations

from typing import Dict, List

import torch
import torch.nn as nn
import torch.nn.functional as F
from scipy.optimize import linear_sum_assignment

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, split_by_counts

_HEAD_CRITERIA = {
    "continuous": {"type": "SmoothL1RegressionLoss", "beta": 1.0, "reduction": "mean"},
    "categorical": {"type": "CrossEntropyHeadLoss", "reduction": "mean"},
}


[docs] @LOSSES.register_module() class FastUnifiedInstanceLoss(nn.Module): """Combine instance mask/class losses with configured query-head losses. Non-overlap mode builds masks from per-point instance ids and aggregates each matched instance's point targets for its query heads. Overlap mode uses the configured multi-hot membership matrix, validity mask, and per-instance head targets. ``aux_outputs`` provide deep supervision and are scaled by ``aux_loss_weight``. ``query_heads`` is a list of named mappings. Continuous heads use a regression criterion; categorical heads use cross-entropy. Each mapping selects its prediction key, target key, target aggregation, and loss weight. """ 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 = "instance", segment_key: str = "segment", aux_loss_weight: float = 1.0, *, query_heads, overlap: bool = False, membership_key: str = "inst_pe", valid_key: str = "ring_valid", mask_pe_thresh: float = 0.0, noobj_mask_loss_weight: float = 0.0, ): super().__init__() self.aux_loss_weight = aux_loss_weight self.overlap = bool(overlap) self.criterion = FastUnifiedSingleLayerLoss( 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, segment_key=segment_key, query_heads=query_heads, overlap=overlap, membership_key=membership_key, valid_key=valid_key, mask_pe_thresh=mask_pe_thresh, noobj_mask_loss_weight=noobj_mask_loss_weight, )
[docs] def forward(self, pred: Dict, input_dict: Dict): target_cache = self.criterion.build_targets(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 FastUnifiedSingleLayerLoss(FastSingleLayerInstanceLoss): def __init__( self, *args, query_heads, overlap=False, membership_key="inst_pe", valid_key="ring_valid", mask_pe_thresh=0.0, noobj_mask_loss_weight=0.0, **kwargs, ): super().__init__(*args, momentum_loss_weight=0.0, iou_loss_weight=0.0, **kwargs) self.overlap = bool(overlap) self.membership_key = membership_key self.valid_key = valid_key self.mask_pe_thresh = float(mask_pe_thresh) self.noobj_mask_loss_weight = float(noobj_mask_loss_weight) self.query_heads = query_heads self.head_losses = nn.ModuleDict() for head in self.query_heads: kind = head.get("kind", "continuous") self.head_losses[head["name"]] = LOSSES.build( head.get("criterion", _HEAD_CRITERIA[kind]) ) self.aux_component_keys = { "focal", "dice", "cls_matched", "cls_noobj", *[head["name"] for head in self.query_heads], } # ------------------------------------------------------------------ # # target building # # ------------------------------------------------------------------ # def build_targets(self, pred, input_dict): pred_masks_list = pred["pred_masks"] if self.overlap: return self._build_overlap_target_cache(pred_masks_list, input_dict) return self._build_target_cache(pred_masks_list, input_dict) def _build_overlap_target_cache(self, pred_masks_list, input_dict): """Per-event multi-hot masks + per-instance head targets from a membership matrix. Mirrors RingPanopticDetector._event_targets.""" assert ( self.membership_key in input_dict ), f"overlap loss needs membership matrix input_dict[{self.membership_key!r}]" device = pred_masks_list[0].device counts = [pm.shape[1] for pm in pred_masks_list] membership = input_dict[self.membership_key] # (N_total, R_max) R_max = membership.shape[1] valid_all = input_dict.get(self.valid_key) point_offsets = torch.tensor( [0] + list(torch.tensor(counts).cumsum(0).tolist()) ) cache = [] for b, P_b in enumerate(counts): p0, p1 = point_offsets[b].item(), point_offsets[b + 1].item() r0, r1 = b * R_max, (b + 1) * R_max mem_b = membership[p0:p1].to(device) # (P_b, R_max) if valid_all is not None: valid_b = valid_all[r0:r1].to(device).bool() else: valid_b = (mem_b > self.mask_pe_thresh).any(0) masks = (mem_b[:, valid_b] > self.mask_pe_thresh).T.float() # (R, P_b) meta = { "num_instances": int(masks.shape[0]), "valid_mask": torch.ones(P_b, dtype=torch.bool, device=device), "target_masks": masks, "target_sizes": masks.sum(1), "ring_slice": (r0, r1), "valid_rings": valid_b, } cache.append(meta) return cache # ------------------------------------------------------------------ # # per-instance target aggregation # # ------------------------------------------------------------------ # @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 _mode_per_instance(self, values, inverse, num_instances): values = values.long().view(-1) out = values.new_zeros(num_instances) for inst in range(num_instances): v = values[inverse == inst] if v.numel() == 0: continue out[inst] = torch.bincount(v).argmax() return out def _aggregate_target(self, values, inverse, num_instances, aggregation): if aggregation == "mean": return self._mean_per_instance(values.float(), inverse, num_instances) if aggregation == "first": out = values.new_zeros((num_instances,) + values.shape[1:]) for inst in range(num_instances): sel = values[inverse == inst] if sel.numel() > 0: out[inst] = sel[0] return out if aggregation == "mode": return self._mode_per_instance(values, inverse, num_instances) raise ValueError(f"Unknown head aggregation: {aggregation!r}") def _compute_head_losses( self, pred, input_dict, batch_idx, counts, meta, idx_q, idx_gt, device ): losses = {} for head in self.query_heads: name = head["name"] kind = head.get("kind", "continuous") pred_key = head.get("pred_key", f"pred_{name}") target_key = head.get("target_key", name) weight = float(head.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 head.get("required", True): raise KeyError( f"Missing query head {name!r}: expected prediction " f"{pred_key!r} and input {target_key!r}" ) continue if self.overlap: # per-instance targets: input is a per-instance (ring) tensor. r0, r1 = meta["ring_slice"] target_full = input_dict[target_key][r0:r1].to(device) target_per_inst = target_full[meta["valid_rings"]] else: 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"], head.get("aggregation", "mean" if kind == "continuous" else "mode"), ) pred_b = pred[pred_key][batch_idx].to(device) pred_matched = pred_b[idx_q.long()] target_matched = target_per_inst[idx_gt.long()] if kind == "continuous": target_matched = target_matched.to(pred_matched.dtype) pred_matched, target_matched = self._align_regression_shapes( pred_matched, target_matched ) losses[name] = weight * self.head_losses[name](pred_matched, target_matched) return losses # ------------------------------------------------------------------ # # overlap matcher (per-event scipy Hungarian over float masks) # # ------------------------------------------------------------------ # def _overlap_match(self, pred_masks_b, pred_logits_b, gt_masks): with torch.autocast(device_type=pred_masks_b.device.type, enabled=False): ml = pred_masks_b.float() # (Q, P) R = gt_masks.shape[0] if R == 0 or ml.shape[0] == 0: empty = torch.zeros(0, dtype=torch.long, device=ml.device) return empty, empty N = ml.shape[1] prob = ml.sigmoid() a, g = self.focal_alpha, self.focal_gamma pos = a * (1 - prob) ** g * (-F.logsigmoid(ml)) neg = (1 - a) * prob**g * (-F.logsigmoid(-ml)) cost_mask = (pos @ gt_masks.T + neg @ (1 - gt_masks).T) / max(N, 1) num = 2 * (prob @ gt_masks.T) den = prob.sum(1, keepdim=True) + gt_masks.sum(1)[None, :] cost_dice = 1 - (num + 1) / (den + 1) C = self.cost_mask * cost_mask + self.cost_dice * cost_dice if pred_logits_b is not None and self.cost_class > 0: # no per-ring class cost without a class target here; skip pass qi, ri = linear_sum_assignment(C.detach().cpu().numpy()) dev = ml.device return ( torch.as_tensor(qi, dtype=torch.long, device=dev), torch.as_tensor(ri, dtype=torch.long, device=dev), ) # ------------------------------------------------------------------ # # forward # # ------------------------------------------------------------------ # def forward(self, pred, input_dict, target_cache=None): assert isinstance(pred, dict) and "pred_masks" in pred 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_targets(pred, input_dict) if self.overlap: indices = [] for b, (pm_b, meta) in enumerate(zip(pred_masks_list, target_cache)): logits_b = pred["pred_logits"][b] if "pred_logits" in pred else None indices.append( self._overlap_match(pm_b, logits_b, meta["target_masks"]) ) else: assert self.truth_label in input_dict indices = self.matcher( { "pred_masks": pred_masks_list, "pred_logits": pred.get("pred_logits", None), }, target_cache, ) z = pred_masks_list[0].new_tensor(0.0) total_loss_focal = z.clone() total_loss_dice = z.clone() total_loss_cls = z.clone() total_loss_noobj_mask = z.clone() total_head = {h["name"]: z.clone() for h in self.query_heads} num_batches_with_head = {h["name"]: 0 for h in self.query_heads} num_batches_with_loss = 0 total_focal = z.clone() total_dice = z.clone() total_pairs = 0 total_ce_matched = z.clone() count_ce_matched = z.clone() total_ce_noobj = z.clone() count_ce_noobj = z.clone() queries_total = z.clone() gt_instances_total = z.clone() 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"] if self.overlap: target_masks = meta["target_masks"].to(pm_b.dtype) else: 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 ) total_loss_focal = total_loss_focal + focal_per_pair.sum() / num_pairs_b total_loss_dice = total_loss_dice + dice.sum() / num_pairs_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 # optional: push unmatched query masks to empty if self.noobj_mask_loss_weight > 0: unmatched = torch.ones( pm_b.shape[0], dtype=torch.bool, device=pm_b.device ) unmatched[idx_q] = False if unmatched.any(): nm = pm_b[unmatched] total_loss_noobj_mask = ( total_loss_noobj_mask + F.binary_cross_entropy_with_logits(nm, torch.zeros_like(nm)) ) # primary classification (matched -> pid, unmatched -> no-obj) if ( not self.overlap and "pred_logits" in pred and self.segment_key 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_m, ce_n = 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_m is not None: total_ce_matched = total_ce_matched + ce_m count_ce_matched = count_ce_matched + 1.0 if ce_n is not None: total_ce_noobj = total_ce_noobj + ce_n count_ce_noobj = count_ce_noobj + 1.0 head_losses = self._compute_head_losses( pred, input_dict, batch_idx, counts, meta, idx_q, idx_gt, pm_b.device ) for name, value in head_losses.items(): total_head[name] = total_head[name] + value num_batches_with_head[name] += 1 denom = max(num_batches_with_loss, 1) loss = ( self.loss_weight_focal * (total_loss_focal / denom) + self.loss_weight_dice * (total_loss_dice / denom) + total_loss_cls / denom ) if self.noobj_mask_loss_weight > 0: loss = loss + self.noobj_mask_loss_weight * (total_loss_noobj_mask / denom) for name, value in total_head.items(): loss = loss + value / max(num_batches_with_head[name], 1) 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": queries_total - total_pairs, "unmatched_gt": gt_instances_total - total_pairs, } for name, value in total_head.items(): components[name] = value / max(num_batches_with_head[name], 1) return loss, components