Diagnosing reliability before calibration
fiducio.diagnose summarises how reliable a segmenter is on a calibration set,
before you pick a calibrator: class balance, global and per-class calibration,
and the images or classes where the model fails (Dice close to zero).
import torch
import fiducio
torch.manual_seed(0)
probs = torch.tensor([0.75, 0.245, 0.005])
labels = torch.multinomial(probs, 24 * 32 * 32, replacement=True).view(24, 32, 32)
onehot = torch.nn.functional.one_hot(labels, 3).permute(0, 3, 1, 2).float()
logits = (onehot * 1.5 + torch.randn(24, 3, 32, 32)) * 3 # an over-confident model
logits[3] = 0.0
logits[3, 0] = 10.0 # on image 3 the model predicts the background everywhere
report = fiducio.diagnose(
logits, labels, class_names=["background", "pet", "edge"], background_class=0
)
print(report)
Reliability report: 24 images, 24576 valid voxels, 3 classes
ECE 0.1164 [0.1077, 0.1346]
NLL 0.7628 [0.6751, 1.0075]
Brier 0.3569 [0.3462, 0.3788]
Bias overconfident (confidence - accuracy = 0.1164 [0.1077, 0.1346])
Saturated voxels (confidence > 0.999): 17.6%
First/last bin mass 0.0%/58.2%, gap n/a/0.0972, ECE contribution 0.0566
class label % pred % Dice class ECE routed vox routed ECE
background 74.74 62.28 0.8496 0.1675 15305 0.0647
pet 24.74 26.32 0.7039 0.0820 6469 0.1814
edge 0.52 11.40 0.0641 0.1226 2802 0.7371
Per-class 95% intervals (bootstrap over images):
class class ECE routed ECE Dice
background 0.1675 [0.1609, 0.1788] 0.0647 [0.0483, 0.1036] 0.8496 [0.8462, 0.8529]
pet 0.0820 [0.0730, 0.1058] 0.1814 [0.1724, 0.1907] 0.7039 [0.6554, 0.7225]
edge 0.1226 [0.1072, 0.1298] 0.7371 [0.7309, 0.7441] 0.0641 [0.0542, 0.0734]
Failed images (mean Dice < 0.1): 1/24
ECE with all images 0.1164, without the failed ones 0.1100; mean confidence 0.9999 on failed vs 0.8768 on the rest
Per class, a failure is Dice < 0.1 on an image where it is present:
class present failed spurious Dice median p10 min
background 24 0 0 0.8495 0.8386 0.8344
pet 24 1 0 0.7152 0.6910 0.0000
edge 24 21 0 0.0611 0.0223 0.0000
Lowest-Dice images:
3: mean Dice 0.0000, worst class pet (0.0000)
23: mean Dice 0.3609, worst class edge (0.0317)
20: mean Dice 0.3648, worst class edge (0.0167)
15: mean Dice 0.3663, worst class edge (0.0342)
1: mean Dice 0.3721, worst class edge (0.0345)
Warnings
- class 'edge' is rare (129 voxels, 0.52% of labels): its estimates are noisy
- class 'edge' fails (Dice < 0.1) on 21/24 of the images where it is present
- 1 image(s) have a mean Dice below 0.1 and enter the fit; ECE is 0.1164 with them and 0.1100 without
- the model is overconfident on this set (confidence - accuracy = 0.1164)
The input is (B, C, *spatial) logits (or probabilities with input_type="probs")
and (B, *spatial) integer labels; B is the number of images or cases. In a
notebook, report displays the same text. report.plot() returns a 2×2
matplotlib figure (reliability diagram, confidence histogram, per-class ECE and
per-image Dice; needs fiducio[plots]), and report.to_dict() returns a
JSON-serializable dictionary with NaN as null.
Streaming over a model and DataLoader
Pass a model and a DataLoader instead of materialised tensors:
from torch.utils.data import DataLoader
loader = DataLoader(calibration_dataset, batch_size=8)
report = fiducio.diagnose(model, loader, device="cuda", class_names=["background", "pet"])
Each batch is (inputs, labels) or (inputs, labels, mask). inputs are
whatever the model accepts (tensors, or lists/tuples/dicts of tensors), moved to
device; the model must return (B, C, *spatial) logits (or probabilities with
input_type="probs"). It runs under torch.no_grad() with its training flag
unchanged, so call model.eval() yourself for inference-mode dropout or
batchnorm. device defaults to the first parameter's device. Only per-image
statistics are accumulated, so the report streams over data that does not fit in
memory. Masks come from the batch; the mask argument applies to tensor input.
Soft targets (MHR)
labels may be floating class distributions (B, C, *spatial), as produced by
mean_human_response. Top-1 and routed ECE compare the top-1 confidence with
q[argmax(p)]; per-class ECE compares p_c with q_c; NLL and Brier keep the
soft-target definitions from fiducio.metrics. mask selects soft targets;
ignore_index applies to hard labels only.
Hard-label Dice is undefined for class distributions, so image_dice, every
Dice field and the failure statistics are NaN (counts are 0) and the report
warns. Class balance still reports the expected label mass sum(q_c), which may
be fractional. No soft Dice substitute is invented.
Reading the report
- Intervals are 95% bootstrap intervals over images, not voxels, so their
width comes from the number of images. They need at least two images and two
resamples (
n_boot=0disables them,n_boot=1yields none). Class ECE intervals need at least two images; routed ECE and Dice intervals additionally need the class routed or present on at least two images, else they are NaN. - The ECE interval is approximate. ECE has a finite-sample noise floor that
resampling raises further, so for a well-calibrated model it can sit slightly
above the estimate (in a simulation of 100 calibrated models of 24 images it
missed the estimate 15 times;
reproducibility/diagnose_interval_check.pyreruns it). Read it as an order of magnitude. - The bias line always prints, and calls the model over- or under-confident only when the interval of the voxel-weighted mean confidence − accuracy excludes 0. That gap has no noise floor, but with few images its intervals are slightly narrow: the same simulation flagged 9 of 100 calibrated models, against 5 expected. Treat a weak flag with caution.
- Dice in the main table is pooled over all voxels. The failure table's Dice
median, p10 and min are per image, over the images where the class is present.
Saturated voxels have confidence above 0.999. ECE uses
n_bins(default 15) equal-width bins on the top-1 confidence. - First/last bin describes the
[0, 1/n_bins]and[(n_bins-1)/n_bins, 1]confidence bins: the share of voxels in each, their mean confidence minus accuracy (positive means over-confident), and their exact contribution to the reportedece. Theecedefinition is unchanged. - label % / pred % show class balance. class ECE compares the probability of the class with its observed frequency. routed columns restrict to the voxels whose argmax is the class, the split that class-conditional calibrators (CDC, CMSap, CMSop) fit one map on, so few routed voxels mean a map fitted on little data.
- Failures. A class fails on an image when it is in the labels and its Dice
is below
dice_failure_threshold(default 0.1). An image fails when its mean Dice over the classes present is below it. Passbackground_classto leave the background out of that mean, since Dice near 1 would hide a failure elsewhere. Without it, an image where the model predicts a single class can keep a mean Dice above the threshold; the per-class failure table still shows it. A class absent from both labels and prediction has undefined Dice and is not a failure; a class predicted where it is absent is counted as spurious. Ifbackground_classleaves no class present in the labels, the per-image mean Dice is undefined: the report warns and counts no image as failed. The report gives the ECE with and without the failed images, since they enter any calibrator's fit.
Note
The report describes the calibration set, using its labels. It does not predict
which new images will fail, and it does not choose a calibrator. The warning
thresholds (min_images, min_class_fraction, min_class_voxels) are
heuristics; adjust them to your data. min_images and min_class_voxels
must be non-negative integers, min_class_fraction and
dice_failure_threshold must lie in [0, 1].
logits must have a spatial axis: a (N, C) voxel table is rejected, since
diagnose works per image.
The accumulator keeps only O(n_images) summaries, not the voxels, so the
bootstrap resamples summaries; with a model and DataLoader the forward pass
dominates. See the diagnose reference for cost details.