cv-exam-sequence-padded-mask-loss
SkillDev toolsPad variable-length per-slice sequences to a fixed batch length, carry a 0/1 mask alongside, and multiply per-slice BCE by the mask before reducing — gives correct per-exam loss with batched training and zero contamination from padding tokens
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the cv-exam-sequence-padded-mask-loss skill
What this skill tells your AI
The instructions your AI receives, as published by wenmin-wu/ds-skills in skills/cv/exam-sequence-padded-mask-loss/SKILL.md and read by ahel’s review.
Overview
Variable-length CT series (one study has 200 slices, another has 800) cannot be batched directly — torch needs rectangular tensors. The standard fix is to pad to max_slices, but the trap is that any per-slice loss now includes the padded zeros and produces a meaningless gradient. Carrying a (B, max_slices) 0/1 mask and multiplying it into the per-element BCE before sum-and-divide is the fix. Crucially, normalize by the real slice count mask.sum(), not by max_slices — otherwise short studies produce artificially small losses and the optimizer ignores them. The same pattern applies to any padded-sequence regression (NLP token loss, point-cloud labeling, time-series anomaly per-step heads).
Quick Start
import torch
import torch.nn.functional as F
def padded_per_slice_bce(y_pred, y_true, mask, label_weight=1.0):
"""
y_pred, y_true: (B, T) per-slice logits and targets
mask: (B, T) 1.0 for real slices, 0.0 for padding
"""
raw = F.binary_cross_entropy_with_logits(y_pred, y_true, reduction='none')
raw = raw * mask # zero out padding
# per-exam normalization by real slice count
n_real = mask.sum(dim=1).clamp(min=1.0) # (B,)
per_exam_loss = raw.sum(dim=1) / n_real # (B,)
# downweight short exams less aggressively with sqrt or log if needed
return (label_weight * per_exam_loss).mean()
Workflow
- In
__getitem__, pad slice features tomax_sliceswith zeros and produce a mask:mask[:n_real] = 1.0 - Stack into batch tensors
(B, max_slices, n_feats)and(B, max_slices)for the mask - In the loss, compute per-element BCE with
reduction='none'then multiply by the mask - Sum over the time axis and divide by
mask.sum(dim=1).clamp(min=1)to get per-exam loss - Mean over the batch (or sum if combining with other heads at exam level)
- Apply the same masking inside any metric (AUROC, accuracy) — sklearn doesn't know about padding
Key Decisions
reduction='none'then mask: neverreduction='mean'followed by mask; the mean already divided by the wrong denominator.- Normalize by
mask.sum(), notmax_slices: the latter underweights short exams by a factor ofn_real / max_slices. clamp(min=1)on the divisor: defends against an empty exam in the batch (rare but possible with bad data).- Mask is float, not bool: BCE multiplication needs float; bool works for simple slicing but not for the multiplication path.
- Apply the mask to the prediction before sigmoid in any per-slice metric: don't compute AUC over padded zeros, you'll get garbage.
References
Signals
- GitHub stars
- 60
- Forks
- 4
- Last commit
- Apr 2026
Advanced
- Catalog kind
- skill
- Gateway key
cv-exam-sequence-padded-mask-loss- Source
- github.com/wenmin-wu/ds-skills