Dice and other segmentation losses
Pixel-by-pixel scoring rewards a model for ignoring small objects, so segmentation uses overlap-based losses instead, each with its own trap.
- 16 min read
- 3 reading levels
- Updated
Read these first
On this page 8
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
The short answer
Scoring a mask pixel by pixel rewards a model for finding nothing, so segmentation scores the overlap instead.
The analogy
Pick stones out of a sack of rice. Somebody grades you grain by grain, and stones are one grain in every four hundred.
Say "not a stone" to every single grain. You are right more than ninety-nine times in a hundred, and the rice still breaks a tooth.
That grading scheme is broken, and no amount of effort on your part fixes it. The scheme has to change.
The fix is to grade the stones you found against the stones that were there. Miss them all and you score zero, however many grains of rice you correctly ignored.
Why it exists
Segmentation is almost always lopsided. A tumour is a sliver of a scan. A crack is a thread across a wall. A wire is one pixel wide in a photo of a room.
Score every pixel equally and the background drowns everything. A model that shades nothing at all looks nearly perfect, and training happily drifts towards it.
Dice is the standard fix, named after a botanist who wanted to compare species lists. It asks one question: of the two shapes, how much do they share?
How Dice thinks
Trace the true outline on tracing paper. Lay your predicted outline on top.
true shape your guess laid on top
######## ....#### ....oooo o where both agree
######## ....#### ....oooo
######## ....#### ....oooo
....
missed by your guess
Dice asks: how much of the two shapes is shared,
compared to how big the two shapes are together?The background never enters the question. Shade nothing and there is no overlap, so the score is the worst possible.
That is the whole difference, and it is why Dice is everywhere in medical imaging.
The family
IoU, meaning intersection over union, asks nearly the same question. It divides the shared part by the total area covered by either shape. It gives lower numbers than Dice for the same prediction, and the two always rank predictions the same way.
Tversky lets you tilt the balance. Punish missed pixels harder than false alarms, and the model becomes cautious about missing anything. Radiologists usually want that tilt; a factory reject system usually wants the opposite.
Focal loss takes a different route. Keep scoring pixel by pixel, but quietly ignore the easy pixels the model already gets right. The hard ones near the boundary then dominate.
Combinations of two of these are what most real systems use, and there is a reason for that below.
What is honestly hard here
There is no best loss. Anyone who tells you otherwise has tested one dataset.
Published comparisons keep finding the same thing. A pixel-wise loss combined with an overlap loss is the most reliable start. The gaps between reasonable choices are often small, and they show up most in the early part of training.
Choose your loss to match how you will be judged, then measure. That sentence is worth more than any ranking table.
Remember this
- Pixel-by-pixel scoring rewards predicting nothing when objects are small.
- Dice and IoU score the overlap, so predicting nothing scores worst.
- Tversky tilts the balance between missing things and inventing them.
What to learn next
- Loss functions — the general picture these are special cases of.
- Imbalanced data — the same problem outside segmentation.
- Cleaning up predicted masks — what to do once the loss has done all it can.
Developer — Code and libraries.
Setup
pip install torchBoth scripts run on CPU. The second takes about thirty seconds.
What each loss actually values
Five predictions against one ground-truth mask, scored five ways. Read the columns, not the rows.
import torch, torch.nn.functional as F
H = W = 40
y = torch.zeros(H, W)
for i in range(H):
y[i, i] = 1.0 # a one-pixel-wide diagonal wire
print(f"foreground: {y.sum():.0f} of {H*W} pixels = {y.mean():.1%}")
def bce(p, y):
return F.binary_cross_entropy(p.clamp(1e-7, 1 - 1e-7), y).item()
def dice(p, y, smooth=1.0):
return (1 - (2 * (p * y).sum() + smooth) / (p.sum() + y.sum() + smooth)).item()
def tversky(p, y, a, b, smooth=1.0):
tp, fp, fn = (p * y).sum(), (p * (1 - y)).sum(), ((1 - p) * y).sum()
return (1 - (tp + smooth) / (tp + a * fp + b * fn + smooth)).item()
def focal(p, y, gamma=2.0):
p = p.clamp(1e-7, 1 - 1e-7)
pt = torch.where(y > 0.5, p, 1 - p) # probability given to the correct answer
return (-((1 - pt) ** gamma) * pt.log()).mean().item()
preds = {
"everything background": torch.zeros(H, W) + 0.01,
"everything foreground": torch.zeros(H, W) + 0.99,
"wire found, 2px thick": torch.zeros(H, W),
"wire missed by 1 px ": torch.zeros(H, W),
"perfect ": y * 0.99 + 0.005,
}
for i in range(H):
preds["wire found, 2px thick"][i, i] = 0.99
if i + 1 < W:
preds["wire found, 2px thick"][i, i + 1] = 0.99
preds["wire missed by 1 px "][i, i + 1] = 0.99
for k in ("wire found, 2px thick", "wire missed by 1 px "):
preds[k] = preds[k].clamp(0.005, 0.995)
print(f"\n{'prediction':24s}{'BCE':>8}{'Dice':>8}{'focal':>8}{'Tversky .3/.7':>15}{'Tversky .7/.3':>15}")
for name, p in preds.items():
print(f"{name:24s}{bce(p, y):>8.4f}{dice(p, y):>8.3f}{focal(p, y):>8.4f}"
f"{tversky(p, y, 0.3, 0.7):>15.3f}{tversky(p, y, 0.7, 0.3):>15.3f}")
print("\nan empty ground truth, and a prediction with 2 stray foreground pixels:")
empty = torch.zeros(H, W)
stray = torch.zeros(H, W); stray[0, 0] = stray[5, 9] = 1.0
for s in (1e-6, 1.0, 10.0, 100.0):
print(f" smooth = {s:<8} Dice loss {dice(stray, empty, s):.4f}")
print(" same mistake, four different losses. The smoothing constant is not cosmetic.")foreground: 40 of 1600 pixels = 2.5% prediction BCE Dice focal Tversky .3/.7 Tversky .7/.3 everything background 0.1249 0.968 0.1128 0.959 0.942 everything foreground 4.4903 0.951 4.4007 0.919 0.964 wire found, 2px thick 0.1173 0.368 0.1100 0.258 0.444 wire missed by 1 px 0.2495 0.984 0.2412 0.972 0.974 perfect 0.0050 0.090 0.0000 0.057 0.119 an empty ground truth, and a prediction with 2 stray foreground pixels: smooth = 1e-06 Dice loss 1.0000 smooth = 1.0 Dice loss 0.6667 smooth = 10.0 Dice loss 0.1667 smooth = 100.0 Dice loss 0.0196 same mistake, four different losses. The smoothing constant is not cosmetic.
Reading the table
Compare the BCE column for rows one and three. Predicting all background costs 0.1249. Actually finding the wire costs 0.1173. The gap is under six per cent of the loss value, and the useless answer is nearly as good as the useful one.
Now the Dice column for the same two rows: 0.968 against 0.368. That is the entire argument for overlap losses in one comparison.
Row four is the sharpest lesson. A wire predicted one pixel to the right of the true wire has zero overlap. Dice scores 0.984, barely better than predicting nothing. Every reasonable person would call that prediction nearly right.
This is a real weakness of overlap losses on thin structures, not a quirk of the example. It is exactly the case where boundary-aware losses were invented, and it is the reason a small mis-registration in medical data can wreck Dice scores.
The two Tversky columns tilt in opposite directions. With a low weight on false positives, Tversky .3/.7 scores 0.258 on the over-thick prediction and 0.919 on all-foreground. Reverse the weights and the same prediction scores 0.444. You are choosing which kind of mistake hurts more, and that choice belongs to the application.
Focal loss changes very little here. 0.1128 against 0.1100 for rows one and three, a smaller gap than plain BCE. Focal reweights hard versus easy pixels; it does not measure overlap. On extreme imbalance it needs pairing with something that does.
The smoothing block is the trap nobody warns about. One identical mistake — two stray pixels on an empty mask — scores anywhere from 1.0 to 0.02 depending on a constant many people copy without reading. With per-image Dice on a dataset containing empty masks, that constant silently decides how much empty images matter.
There are two usual fixes. Compute Dice over the whole batch instead of per image. Or exclude empty ground truths from the Dice term, and let a pixel-wise loss handle them.
Does the choice change the outcome?
The table shows what the losses value. Whether that changes the trained model is a separate question, and it deserves a measurement rather than an opinion.
import torch, torch.nn as nn, torch.nn.functional as F
S = 40
def batch(n, gen):
"""One small bright block per image, on noisy background: 0.6% foreground."""
x = torch.rand(n, 1, S, S, generator=gen) * 0.6
y = torch.zeros(n, 1, S, S)
for i in range(n):
r, c = torch.randint(2, S - 3, (2,), generator=gen).tolist()
y[i, 0, r:r+3, c:c+3] = 1.0
return (x + 0.30 * y).clamp(0, 1), y # low contrast, on purpose
def net():
return nn.Sequential(nn.Conv2d(1, 16, 3, padding=1), nn.ReLU(),
nn.Conv2d(16, 16, 3, padding=1), nn.ReLU(),
nn.Conv2d(16, 16, 3, padding=1), nn.ReLU(),
nn.Conv2d(16, 1, 1))
def soft_dice(logits, y, smooth=1.0):
p = torch.sigmoid(logits)
num = 2 * (p * y).sum((1, 2, 3)) + smooth
den = p.sum((1, 2, 3)) + y.sum((1, 2, 3)) + smooth
return (1 - num / den).mean()
LOSSES = {
"BCE only ": lambda l, y: F.binary_cross_entropy_with_logits(l, y),
"Dice only": lambda l, y: soft_dice(l, y),
"BCE+Dice ": lambda l, y: F.binary_cross_entropy_with_logits(l, y) + soft_dice(l, y),
}
vg = torch.Generator().manual_seed(7); xv, yv = batch(64, vg)
print(f"foreground fraction: {yv.mean():.2%}")
print(f"{'loss':10s}{'IoU@200':>9}{'IoU@600':>9}{'IoU@1200':>10}{'recall':>9}{'precision':>11}{'pred fg':>9}")
for name, fn in LOSSES.items():
torch.manual_seed(0); m = net(); opt = torch.optim.Adam(m.parameters(), lr=3e-3)
g = torch.Generator().manual_seed(1); row = {}
for step in range(1, 1201):
x, y = batch(8, g)
loss = fn(m(x), y)
opt.zero_grad(); loss.backward(); opt.step()
if step in (200, 600, 1200):
with torch.no_grad(): p = (m(xv) > 0).float()
row[step] = ((p * yv).sum() / ((p + yv) > 0).float().sum().clamp(min=1)).item()
rec = ((p * yv).sum() / yv.sum()).item()
prec = ((p * yv).sum() / p.sum().clamp(min=1)).item()
print(f"{name:10s}{row[200]:>9.3f}{row[600]:>9.3f}{row[1200]:>10.3f}"
f"{rec:>9.3f}{prec:>11.3f}{p.mean():>9.2%}")foreground fraction: 0.56% loss IoU@200 IoU@600 IoU@1200 recall precision pred fg BCE only 0.456 0.585 0.713 0.842 0.823 0.58% Dice only 0.503 0.640 0.723 0.845 0.832 0.57% BCE+Dice 0.618 0.695 0.723 0.847 0.831 0.57%
The honest reading
All three arrive at the same place. By 1200 steps: 0.713, 0.723, 0.723. If you had run only the final number, you would conclude the loss choice does not matter on this task.
The differences are in how fast they get there. At 200 steps the compound loss leads by a wide margin: 0.618 against 0.456 for BCE alone. Dice sits between them. Under a fixed compute budget, that gap is the whole result.
Recall and precision end up balanced for all three. Nothing here collapsed to predicting background. A three-layer network on this low-contrast task is still learnable by BCE, given more steps.
This matches what the literature reports. Compound losses are the most reliable default; the margins over a well-trained single loss are frequently modest. Anybody claiming a large, universal win from one loss function has not run this comparison.
Common mistakes
Applying Dice to logits. Dice needs probabilities. Pass logits and the sums are meaningless. Always torch.sigmoid first, or softmax for multi-class.
Per-image Dice on a dataset with empty masks. Covered above. Use batch-level Dice, or route empty images through the pixel-wise term only.
Thresholding inside the loss. (p > 0.5).float() has zero gradient everywhere. The soft version, using probabilities directly, is what makes Dice differentiable.
Averaging multi-class Dice over classes that are absent. A class with no pixels in this image contributes a smoothing-dominated value that swamps the real classes. Mask it out.
Assuming a good Dice score means calibrated probabilities. It does not. Networks trained on Dice alone are typically overconfident, pushing probabilities towards 0 and 1. If you need trustworthy confidence, keep a cross-entropy term, then check calibration.
Try it yourself
In the second script, change soft_dice to a Tversky loss with alpha=0.3, beta=0.7, which punishes misses harder. Watch recall rise, precision fall, and IoU move by very little. That is the trade made visible, and it is often more useful than a higher score.
What to learn next
- Loss functions — the general picture these are special cases of.
- Imbalanced data — the same problem outside segmentation.
- Cleaning up predicted masks — what to do once the loss has done all it can.
Researcher — Mathematics and papers.
Definitions
For a predicted probability map $p \in [0,1]^{N}$ and binary target $g \in {0,1}^{N}$:
Soft Dice, from Milletari et al. (2016), V-Net:
$$ \mathcal{L}_{\text{Dice}} = 1 - \frac{2\sum_i p_i g_i + \epsilon}{\sum_i p_i + \sum_i g_i + \epsilon} $$
Soft Jaccard (IoU):
$$ \mathcal{L}_{\text{IoU}} = 1 - \frac{\sum_i p_i g_i + \epsilon}{\sum_i p_i + \sum_i g_i - \sum_i p_i g_i + \epsilon} $$
The two are related by $J = \frac{D}{2 - D}$, a strictly increasing map, so they induce the same ranking over predictions. They do not induce the same gradients, and they are not interchangeable during optimisation.
Tversky, from Salehi et al. (2017):
$$ \mathcal{L}_{\text{T}} = 1 - \frac{TP + \epsilon}{TP + \alpha FP + \beta FN + \epsilon}, \quad TP = \sum_i p_i g_i,\; FP = \sum_i p_i (1-g_i),\; FN = \sum_i (1-p_i) g_i $$
$\alpha = \beta = 0.5$ recovers Dice; $\alpha = \beta = 1$ recovers Jaccard. Setting $\beta > \alpha$ penalises misses more heavily and raises recall.
Focal, from Lin et al. (2017), with $p_t = p$ if $g = 1$ else $1 - p$:
$$ \mathcal{L}_{\text{focal}} = -\alpha_t (1 - p_t)^{\gamma} \log p_t $$
The modulating factor $(1-p_t)^\gamma$ shrinks the contribution of well-classified pixels. It rebalances by difficulty, not by region overlap, which is why it complements Dice rather than replacing it.
Gradient behaviour, and why it matters
The Dice gradient with respect to one prediction $p_k$ is
$$ \frac{\partial \mathcal{L}_{\text{Dice}}}{\partial p_k} = -\frac{2 g_k (\sum_i p_i + \sum_i g_i) - 2\sum_i p_i g_i}{(\sum_i p_i + \sum_i g_i + \epsilon)^2} $$
Three consequences follow directly.
- The gradient is non-local. Every pixel's gradient depends on the sums over all pixels, so one pixel's update depends on the rest of the image. Cross-entropy has no such coupling.
- The denominator scales with object size. Small objects produce large per-pixel gradients. That is the desired imbalance correction, and the source of instability when an object is a handful of pixels.
- Near-empty predictions produce vanishing signal. With $\sum p_i \approx 0$ and $\sum g_i \approx 0$, the loss is dominated by $\epsilon$, and gradients are tiny. This is the empty-mask pathology in the developer section, stated analytically.
Direct surrogates for IoU
Dice and soft-Jaccard are relaxations of the discrete metric, not tight surrogates. Two lines of work address that.
Lovász-Softmax (Berman, Rannen Triki, Blaschko, CVPR 2018) uses the Lovász extension of the submodular Jaccard set function. The result is a convex, piecewise-linear surrogate that is tight on the discrete metric. It requires sorting the per-pixel errors, so it costs $O(N \log N)$ per class. It reliably improves mIoU when used as a fine-tuning loss after cross-entropy pre-training, and is less reliable trained from scratch.
Eelbode et al. (2020) analyse the relationship between the soft relaxations and the metrics. They give conditions under which optimising one optimises the other. The paper is Optimization for Medical Image Segmentation: Theory and Practice when Evaluating with Dice Score or Jaccard Index.
Boundary-aware terms
Region losses are blind to how far a wrong pixel is from the truth. The one-pixel-offset wire in the developer section scores like a total miss.
Boundary loss (Kervadec et al., MIDL 2019) integrates the predicted region against a pre-computed distance map of the true boundary. Its gradient is then proportional to distance from that boundary. It is used with a scheduled weight, starting near zero and rising, because alone it is unstable.
Hausdorff-distance surrogates and surface Dice (also called normalised surface distance) target the same failure. Surface Dice with a tolerance in millimetres is now standard in medical challenges, because clinicians care about boundary error in physical units, not pixel overlap.
What the comparisons find
Ma et al. (2021), Loss Odyssey in Medical Image Segmentation, benchmark around twenty losses across four segmentation tasks. Their headline conclusions are stable and worth internalising:
- No single loss wins across tasks.
- Compound losses, particularly Dice combined with cross-entropy, are the most robust default.
- Combining a region term with a boundary term is the most effective choice for small structures.
That is close to what the training run above shows on a toy problem: the compound loss is fastest, and the final gaps are small.
Practical recipe
- Start with cross-entropy plus Dice, equally weighted. Deviating needs a reason and a measurement.
- If misses matter more than false alarms, move to Tversky with $\beta > \alpha$ and tune on validation data, not intuition.
- If your evaluation is boundary-based, add a boundary or surface term with a warm-up schedule.
- Report the metric you optimised and one you did not. A model tuned on Dice alone can have poor boundary accuracy and poor calibration at once.
- For multi-class, decide explicitly how absent classes are handled. That single decision moves reported mean Dice by several points.
Papers
- Milletari, Navab, Ahmadi, V-Net, 3DV 2016 — arxiv.org/abs/1606.04797
- Salehi, Erdogmus, Gholipour, Tversky loss function for image segmentation using 3D fully convolutional deep networks, MLMI 2017 — arxiv.org/abs/1706.05721
- Lin, Goyal, Girshick, He, Dollár, Focal Loss for Dense Object Detection, ICCV 2017 — arxiv.org/abs/1708.02002
- Berman, Rannen Triki, Blaschko, The Lovász-Softmax loss, CVPR 2018 — arxiv.org/abs/1705.08790
- Kervadec et al., Boundary loss for highly unbalanced segmentation, MIDL 2019 — arxiv.org/abs/1812.07032
- Ma et al., Loss Odyssey in Medical Image Segmentation, Medical Image Analysis 2021 — arxiv.org/abs/2005.13449
What to learn next
- Loss functions — the general picture these are special cases of.
- Imbalanced data — the same problem outside segmentation.
- Cleaning up predicted masks — what to do once the loss has done all it can.