Focal Loss, Dice Loss & Class Weights
Advanced loss functions for severely imbalanced segmentation — the foundation of DAHiTrA-style damage assessment on xBD.
What is a loss function? While the network trains, it makes guesses about every pixel. The loss is a single number that says "how wrong were the guesses?". Training works by repeatedly nudging the network's weights to make that number smaller. Choose the wrong loss and the network will happily minimise it by doing something useless.
The problem in one sentence. In xBD satellite tiles, 96% of pixels are just background (grass, roads, untouched roofs). If we use plain cross-entropy, the laziest possible model — "predict background everywhere" — already scores 96% pixel accuracy. It is useless to a disaster responder because it never spots a destroyed building, but the loss is happy. We need a loss that punishesthat laziness.
The three ideas you need before reading on:
- Class weights — count how rare each class is, then multiply its loss by a big number. Rare classes scream louder so the network can't ignore them.
- Focal loss — automatically turns the volume down on pixels the network already gets right, and up on the hard ones. The factor
(1 − p)γwith γ = 2 does this. Like a teacher who stops drilling you on the easy questions and only quizzes you on the ones you keep missing. - Dice loss — instead of counting individual pixels, it measures shape overlapbetween the predicted region and the true region. Great for thin or small objects (a single destroyed house) that pixel-counting losses tend to miss.
The recipe used in this project. L_total = 0.6 · L_focal + 0.4 · L_dice, with class weights baked into both. Focal handles the easy/hard split, Dice handles the small/rare regions, and class weights keep the rare damage tiers in the conversation. Each piece fixes a different failure mode of plain cross-entropy.
The Problem — Class Imbalance in Disaster Datasets
Why standard accuracy is a misleading metric for xBD.
In xBD, pixel distribution is roughly:
| Class | % of pixels |
|---|---|
| Background | 96.0% |
| No damage | 2.7% |
| Minor damage | 0.1% |
| Major damage | 0.1% |
| Destroyed | 0.1% |
| Predicted: BG | |
|---|---|
| True: BG (96k pixels) | ✓ 96,000 |
| True: Damage (4k pixels) | ✗ 4,000 missed! |
96% accuracy · 0% usefulness — every damaged building is missed.
- A loss that punishes missing rare classes (class weights).
- A loss that stops rewarding easy background pixels (Focal).
- A loss that scores the whole building shape, not single pixels (Dice).
Solution 1 — Class Weights
Make rare mistakes more expensive.
- — number of classes (5 here: background + 4 damage tiers)
- — 1 if the true class is i, otherwise 0
- — model's predicted probability for class i (after softmax)
Reads as: look only at the slot for the correct class, take the log of its probability, and flip the sign.When the model is confident in the right answer (), and the loss is tiny; when it is wrong (), the loss explodes.
- — per-class weight — large for rare classes, ≈1 for background
The weight multiplies the loss of the rare class. A missed "destroyed" pixel with now hurts 20× more than the same mistake on background, so the optimiser stops ignoring it.
| Class | Count (per 100k) | Weight w | Effect |
|---|---|---|---|
| Background | 96,000 | 1 | Baseline — very common |
| No damage | 2,700 | 5 | Moderately important |
| Minor damage | 100 | 20 | High penalty — rare |
| Major damage | 100 | 20 | High penalty — rare |
| Destroyed | 100 | 20 | Highest — critical |
Worked example. Model says for a destroyed pixel: . Weighted: . Same model on a background pixel (): . The destroyed mistake now costs ~600× more.
Solution 2 — Focal Loss (Lin et al., 2017)
Focus on hard examples, ignore easy ones.
- — model's probability for the correct class on this pixel
- — class weight (same idea as wCE, large for rare classes)
- — modulating factor — shrinks loss on easy examples
- — focusing parameter (we use 2)
The new piece is . If (model already correct and confident), then — the loss is multiplied by almost nothing. If (model is wrong on a destroyed pixel), then — the loss barely shrinks. Net effect: training time is spent on hard pixels, not on the easy background.
| Example | p_t | CE = −log(p_t) | (1−p_t)² | Focal | vs CE |
|---|---|---|---|---|---|
| Easy (bg) | 0.95 | 0.051 | 0.0025 | 0.00013 | ~400× smaller |
| Medium | 0.50 | 0.693 | 0.25 | 0.173 | ~4× smaller |
| Hard (destroyed) | 0.10 | 2.303 | 0.81 | 1.865 | ~1.2× smaller |
| Very hard | 0.05 | 2.996 | 0.9025 | 2.704 | ~1.1× smaller |
The teacher's dilemma. Student A scores 95% — they already know it. Student B scores 20% — they need focused attention. Focal Loss is the teacher who only spends time on Student B.
Reading the chart. γ = 0 reproduces standard cross-entropy (top curve). As γ grows, the loss for easy examples (p_t → 1, right side) collapses toward zero while the loss for hard examples (p_t → 0, left side) is barely affected. At γ = 2 — the value used throughout this thesis — a confident background pixel (p_t = 0.9) contributes ~100× less than a missed destroyed-building pixel (p_t = 0.1). Visually, that is the gap between the green and dark-red curves at the right edge of the plot.
Solution 3 — Dice Loss
Reward region overlap, not per-pixel accuracy.
- — set of pixels the model predicts as damaged
- — set of pixels that are actually damaged (ground truth)
- — number of pixels in both — true positives
- — predicted-area plus true-area
Dice is a shape-overlap score: 1.0 means perfect overlap, 0.0 means no overlap at all. We subtract from 1 so that gradient descent (which minimises) actually pushes overlap up. Unlike cross-entropy, Dice treats the whole building as one object — a near-miss on a small destroyed house is heavily penalised even if 99.9% of pixels (mostly background) are correct.
| Ground truth | Predicted overlap | Dice | Quality |
|---|---|---|---|
| 100 px | 100 (perfect) | 2·100/(100+100) = 1.00 | Perfect |
| 100 px | 80 | 2·80/200 = 0.80 | Good |
| 100 px | 50 | 2·50/200 = 0.50 | Moderate |
| 100 px | 10 | 2·10/200 = 0.10 | Poor |
| 100 px | 0 | 0.00 | None |
Cross-entropy looks at pixels independently and can miss a building's boundary while still scoring well. Dice rewards correctly drawing the whole shape — essential for disaster mapping where boundary quality matters.
Combining Focal + Dice (DAHiTrA)
Each solves a different problem — together they are complete.
- — mixing weights — we use 0.6 and 0.4 respectively
- — handles class imbalance and easy-vs-hard pixels
- — handles boundary shape and small regions
Setting gives the "simple" sum. We tilt towards Focal (0.6) because xBD is heavily imbalanced, while still giving Dice (0.4) enough weight to keep building outlines crisp.
Multi-Scale Loss Supervision
Apply loss at multiple decoder stages — gradients reach early layers faster.
| Stage | Resolution | Loss | λ |
|---|---|---|---|
| Deep supervision | 1/8 | Focal | 0.4 |
| Intermediate | 1/4 | Focal + Dice | 0.6 |
| Final output | Full | Focal + Dice | 1.0 |
| Total | — | Σ λᵢ · Lᵢ | — |
Deep supervision injects loss signal at multiple resolutions so that even the deepest encoder layers see strong gradients — directly mitigating vanishing gradient on tall encoder/decoder stacks.
PyTorch — Focal Loss
python1import torch, torch.nn as nn2import torch.nn.functional as F34class FocalLoss(nn.Module):5 def __init__(self, alpha=None, gamma=2):6 super().__init__()7 self.alpha = alpha # class-weight tensor, shape [C]8 self.gamma = gamma # focusing parameter910 def forward(self, logits, targets):11 # 1. Per-pixel CE (no reduction yet)12 ce_loss = F.cross_entropy(13 logits, targets,14 weight=self.alpha, reduction="none"15 )16 # 2. Recover p_t from CE: CE = -log(p_t) -> p_t = exp(-CE)17 pt = torch.exp(-ce_loss)18 # 3. Apply modulating factor (1 - p_t)^gamma19 focal_loss = ((1 - pt) ** self.gamma) * ce_loss20 return focal_loss.mean()
PyTorch — Dice Loss
python1class DiceLoss(nn.Module):2 def __init__(self, smooth=1e-6):3 super().__init__()4 self.smooth = smooth # prevents div by zero56 def forward(self, preds, targets):7 preds = torch.softmax(preds, dim=1) # logits -> probs8 intersection = (preds * targets).sum() # |A ∩ B|9 union = preds.sum() + targets.sum() # |A| + |B|10 dice = (2 * intersection + self.smooth) / (union + self.smooth)11 return 1 - dice
PyTorch — Combined Loss & Siamese Training Loop
python1# Class weights — derived from inverse frequency2class_weights = torch.tensor([1.0, 5.0, 20.0, 20.0, 20.0]).cuda()3focal_fn = FocalLoss(alpha=class_weights, gamma=2)4dice_fn = DiceLoss(smooth=1e-6)56for pre_img, post_img, mask in dataloader:7 pre_img, post_img, mask = pre_img.cuda(), post_img.cuda(), mask.cuda()89 # Siamese forward — both images through shared encoder10 logits = siamese_model(pre_img, post_img) # [B, C, H, W]1112 # One-hot encode mask for Dice13 mask_onehot = F.one_hot(mask.long(), num_classes=5)14 mask_onehot = mask_onehot.permute(0, 3, 1, 2).float()1516 loss_focal = focal_fn(logits, mask.long())17 loss_dice = dice_fn(logits, mask_onehot)18 total_loss = loss_focal + loss_dice # or λ₁·focal + λ₂·dice1920 optimizer.zero_grad()21 total_loss.backward()22 optimizer.step()
Summary — Why all three components are needed
Each fixes a different failure mode; removing any one breaks training.
| Component | What it solves | Mechanism | Without it... |
|---|---|---|---|
| Class weights | Class-frequency imbalance | Higher penalty for misclassifying rare classes (e.g. Destroyed × 20) | Model ignores rare classes entirely |
| Focal loss | Easy-example dominance | Modulating factor (1−p_t)^γ shrinks loss for confident pixels | Background floods gradients; rare classes never get learned |
| Dice loss | Pixel-wise blindness | Scores the whole region (2|A∩B| / (|A|+|B|)) | Building boundaries stay fuzzy / fragmented |
- CE
- — cross-entropy, the default classification loss
−log pof the right class. - wCE
- — weighted cross-entropy: CE scaled per class so rare classes hurt more.
- p_t
- — model's predicted probability of the true class on this pixel.
- γ (gamma)
- — focal "focusing" exponent; we use 2. Larger γ = ignore easy pixels harder.
- α (alpha)
- — per-class weight inside focal loss. Same idea as the wCE weight.
- FL / Focal
- — focal loss: CE multiplied by
(1−p_t)^γ. - Dice
- — overlap score
2|A∩B|/(|A|+|B|)in [0,1]; 1 = perfect overlap. - L_total
- — final loss the optimiser sees. Here
0.6·Focal + 0.4·Dice. - xBD
- — satellite dataset of pre/post-disaster building damage (5 classes).
- BG
- — background pixels (no building, no damage). ~96% of every tile.
- logits
- — raw network outputs before softmax. Can be negative.
- softmax
- — turns logits into probabilities that sum to 1.
- argmax
- — pick the class with the highest probability.
- recall
- — of all true damaged pixels, what fraction did we catch?
- deep supervision
- — extra loss attached partway through the decoder so early layers also get strong gradients.