Lecture L2

Focal Loss, Dice Loss & Class Weights

Advanced loss functions for severely imbalanced segmentation — the foundation of DAHiTrA-style damage assessment on xBD.

Class imbalanceWeighted CEFocal (γ=2)Dice overlapFocal + DiceMulti-scale supervisionPyTorch
In plain English — read this first

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.

1

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
Background96.0%
No damage2.7%
Minor damage0.1%
Major damage0.1%
Destroyed0.1%
The "lazy model" — predict only background
Predicted: BG
True: BG (96k pixels)✓ 96,000
True: Damage (4k pixels)✗ 4,000 missed!

96% accuracy · 0% usefulness — every damaged building is missed.

What we need instead
  • 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).
The accuracy trap
A model that always predicts background scores 96% accuracy — and is 0% useful for damage detection. We need losses that actively penalise missing rare, critical classes.
2

Solution 1 — Class Weights

Make rare mistakes more expensive.

Standard cross-entropy
LCE  =  i=1Cyilog(pi)\mathcal{L}_{\text{CE}} \;=\; -\sum_{i=1}^{C} y_i \,\log(p_i)
where
  • CCnumber of classes (5 here: background + 4 damage tiers)
  • yiy_i1 if the true class is i, otherwise 0
  • pip_imodel'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 (pi1p_i \approx 1), logpi0\log p_i \approx 0 and the loss is tiny; when it is wrong (pi0p_i \approx 0), the loss explodes.

Weighted cross-entropy
LwCE  =  i=1Cwiyilog(pi)\mathcal{L}_{\text{wCE}} \;=\; -\sum_{i=1}^{C} w_i \, y_i \,\log(p_i)
where
  • wiw_iper-class weight — large for rare classes, ≈1 for background

The weight wiw_i multiplies the loss of the rare class. A missed "destroyed" pixel with w=20w = 20 now hurts 20× more than the same mistake on background, so the optimiser stops ignoring it.

ClassCount (per 100k)Weight wEffect
Background96,0001Baseline — very common
No damage2,7005Moderately important
Minor damage10020High penalty — rare
Major damage10020High penalty — rare
Destroyed10020Highest — critical

Worked example. Model says p=0.2p = 0.2 for a destroyed pixel: log(0.2)=1.61-\log(0.2) = 1.61. Weighted: 20×1.61=32.220 \times 1.61 = 32.2. Same model on a background pixel (p=0.95p = 0.95): log(0.95)=0.051-\log(0.95) = 0.051. The destroyed mistake now costs ~600× more.

Why this isn't enough
Class weights still let the model coast on millions of easy background pixels. We need to suppress easy examples too — that is Focal Loss.
3

Solution 2 — Focal Loss (Lin et al., 2017)

Focus on hard examples, ignore easy ones.

Focal loss
FL(pt)  =  αt(1pt)γlog(pt)\mathrm{FL}(p_t) \;=\; -\,\alpha_t \,\bigl(1 - p_t\bigr)^{\gamma}\,\log(p_t)
where
  • ptp_tmodel's probability for the correct class on this pixel
  • αt\alpha_tclass weight (same idea as wCE, large for rare classes)
  • (1pt)γ(1 - p_t)^{\gamma}modulating factor — shrinks loss on easy examples
  • γ\gammafocusing parameter (we use 2)

The new piece is (1pt)γ(1-p_t)^{\gamma}. If pt=0.95p_t = 0.95 (model already correct and confident), then (10.95)2=0.0025(1-0.95)^2 = 0.0025 — the loss is multiplied by almost nothing. If pt=0.1p_t = 0.1 (model is wrong on a destroyed pixel), then (10.1)2=0.81(1-0.1)^2 = 0.81 — the loss barely shrinks. Net effect: training time is spent on hard pixels, not on the easy background.

Examplep_tCE = −log(p_t)(1−p_t)²Focalvs CE
Easy (bg)0.950.0510.00250.00013~400× smaller
Medium0.500.6930.250.173~4× smaller
Hard (destroyed)0.102.3030.811.865~1.2× smaller
Very hard0.052.9960.90252.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.

Figure — Loss vs. p_t for γ ∈ {0, 0.5, 1, 2, 5}
0123450.00.20.40.60.81.0p_t — predicted probability of the true classLossγ = 0 (cross-entropy)γ = 0.5γ = 1γ = 2 (used here)γ = 5easy (p_t=0.9)hard (p_t=0.1)

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.

4

Solution 3 — Dice Loss

Reward region overlap, not per-pixel accuracy.

Dice coefficient & Dice loss
Dice(A,B)  =  2ABA+B    [0,1]LDice  =  1Dice\mathrm{Dice}(A,B) \;=\; \frac{2\,|A \cap B|}{|A| + |B|} \;\in\; [0,1] \qquad \mathcal{L}_{\text{Dice}} \;=\; 1 - \mathrm{Dice}
where
  • AAset of pixels the model predicts as damaged
  • BBset of pixels that are actually damaged (ground truth)
  • AB|A \cap B|number of pixels in both — true positives
  • A+B|A| + |B|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 truthPredicted overlapDiceQuality
100 px100 (perfect)2·100/(100+100) = 1.00Perfect
100 px802·80/200 = 0.80Good
100 px502·50/200 = 0.50Moderate
100 px102·10/200 = 0.10Poor
100 px00.00None

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.

5

Combining Focal + Dice (DAHiTrA)

Each solves a different problem — together they are complete.

Combined loss
L  =  λ1LFocal  +  λ2LDice\mathcal{L} \;=\; \lambda_1\,\mathcal{L}_{\text{Focal}} \;+\; \lambda_2\,\mathcal{L}_{\text{Dice}}
where
  • λ1,λ2\lambda_1, \lambda_2mixing weights — we use 0.6 and 0.4 respectively
  • LFocal\mathcal{L}_{\text{Focal}}handles class imbalance and easy-vs-hard pixels
  • LDice\mathcal{L}_{\text{Dice}}handles boundary shape and small regions

Setting λ1=λ2=1\lambda_1 = \lambda_2 = 1 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.

CE only
Pixel-wise; can miss building boundaries.
Dice only
Good shapes, still struggles with imbalance.
Focal + Dice ✓
Finds and outlines rare damaged buildings.
6

Multi-Scale Loss Supervision

Apply loss at multiple decoder stages — gradients reach early layers faster.

StageResolutionLossλ
Deep supervision1/8Focal0.4
Intermediate1/4Focal + Dice0.6
Final outputFullFocal + Dice1.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.

7

PyTorch — Focal Loss

python
1import torch, torch.nn as nn
2import torch.nn.functional as F
3
4class 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 parameter
9
10 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)^gamma
19 focal_loss = ((1 - pt) ** self.gamma) * ce_loss
20 return focal_loss.mean()
8

PyTorch — Dice Loss

python
1class DiceLoss(nn.Module):
2 def __init__(self, smooth=1e-6):
3 super().__init__()
4 self.smooth = smooth # prevents div by zero
5
6 def forward(self, preds, targets):
7 preds = torch.softmax(preds, dim=1) # logits -> probs
8 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
9

PyTorch — Combined Loss & Siamese Training Loop

python
1# Class weights — derived from inverse frequency
2class_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)
5
6for pre_img, post_img, mask in dataloader:
7 pre_img, post_img, mask = pre_img.cuda(), post_img.cuda(), mask.cuda()
8
9 # Siamese forward — both images through shared encoder
10 logits = siamese_model(pre_img, post_img) # [B, C, H, W]
11
12 # One-hot encode mask for Dice
13 mask_onehot = F.one_hot(mask.long(), num_classes=5)
14 mask_onehot = mask_onehot.permute(0, 3, 1, 2).float()
15
16 loss_focal = focal_fn(logits, mask.long())
17 loss_dice = dice_fn(logits, mask_onehot)
18 total_loss = loss_focal + loss_dice # or λ₁·focal + λ₂·dice
19
20 optimizer.zero_grad()
21 total_loss.backward()
22 optimizer.step()
10

Summary — Why all three components are needed

Each fixes a different failure mode; removing any one breaks training.

ComponentWhat it solvesMechanismWithout it...
Class weightsClass-frequency imbalanceHigher penalty for misclassifying rare classes (e.g. Destroyed × 20)Model ignores rare classes entirely
Focal lossEasy-example dominanceModulating factor (1−p_t)^γ shrinks loss for confident pixelsBackground floods gradients; rare classes never get learned
Dice lossPixel-wise blindnessScores the whole region (2|A∩B| / (|A|+|B|))Building boundaries stay fuzzy / fragmented
Practical recipe
Start with γ=2, inverse-frequency class weights, and an equal-weighted Focal+Dice. Tune λ₁, λ₂ only after a clean baseline trains. Monitor per-class Dice and recall — not overall accuracy.
Acronyms & jargon — quick reference
CE
— cross-entropy, the default classification loss −log p of 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.