Tutorial: calibrating a segmenter on Oxford-IIIT Pets
This tutorial runs the whole workflow on a public dataset, from the splits to the
metrics on a separate test set. It is one script,
examples/tutorial_oxford_pets.py,
which trains a small U-Net on CPU, so there are no pretrained weights to fetch.
The Oxford-IIIT Pet dataset gives each photo a trimap with three classes: pet, background and pet boundary.
pip install "fiducio[plots]" # Pillow and matplotlib come with the plots extra
python examples/tutorial_oxford_pets.py
The first run downloads about 810 MB (SHA-256 verified) into
~/.cache/fiducio/oxford-pets and takes a few minutes on CPU. Use --smoke for
a seconds-long run on synthetic data with no download; CI runs that mode.
1. Three disjoint splits
The script resizes 1200 random images to 64×64 and splits them 50/25/25:
| Split | Used for |
|---|---|
| train | fitting the U-Net |
| calibration | fitting the calibrators |
| test | reporting metrics, never fitted on |
Warning
Calibrate on images the network was not trained on, and report on images the calibrator did not see. Fitting on the training split calibrates an over-fitted model; fitting on the test split leaks the answer and flatters the metrics. The split here is per image. With patient or volume data, split per patient.
2. Collect logits
After training, stack the logits as (B, C, H, W) and labels as (B, H, W),
the layout Fiducio expects:
cal_logits = logits_of(model, x[splits["calibration"]])
test_logits = logits_of(model, x[splits["test"]])
3. Diagnose the model on the calibration split
Before choosing a calibrator, look at the model's reliability on the calibration
split with diagnose. The background is class 1 in these
labels, so it is excluded from the per-image mean Dice:
from fiducio import diagnose
cal_labels = y[splits["calibration"]]
print(
diagnose(
cal_logits, cal_labels, class_names=["pet", "background", "boundary"], background_class=1
)
)
Reliability report: 300 images, 1228800 valid voxels, 3 classes
ECE 0.0130 [0.0069, 0.0202]
NLL 0.4791 [0.4539, 0.5045]
Brier 0.2668 [0.2535, 0.2808]
Bias overconfident (confidence - accuracy = 0.0130 [0.0066, 0.0202])
Saturated voxels (confidence > 0.999): 4.7%
class label % pred % Dice class ECE routed vox routed ECE
pet 28.45 26.94 0.7701 0.0265 330991 0.0386
background 59.81 64.85 0.8908 0.0205 796859 0.0222
boundary 11.74 8.22 0.4517 0.0108 100950 0.0530
Failed images (mean Dice < 0.1): 0/300
This is an excerpt; reliability diagnostics describes every field. On this run the model is slightly over-confident (ECE 0.013) and no image fails, so there is nothing to remove from the calibration set. Image 187, the lowest, still has a mean Dice of 0.14 and is the first one to inspect. No class is rare, and every class is predicted on over 100 000 voxels, so a class-conditional calibrator has enough data per class. The two warnings about a class predicted where it is absent concern one image each, the kind of case this report is meant to surface.
4. Fit on the calibration split only
ts = TemperatureScaling(device="cpu").fit(cal_logits, cal_labels)
ms = MatrixScaling(lambda_reg=1e-2, device="cpu").fit(cal_logits, cal_labels)
cmsap = CMSap(device="cpu").fit(cal_logits, cal_labels)
cmsop = CMSop(device="cpu").fit(cal_logits, cal_labels)
5. Evaluate on the test split
raw = F.softmax(test_logits, dim=1)
calibrated = ts.transform(test_logits)
expected_calibration_error(calibrated, test_labels)
One run on CPU (1200 images, 40 epochs, seed 0) gave:
| NLL | ECE | Brier | Dice | |
|---|---|---|---|---|
| uncalibrated | 0.4952 | 0.0144 | 0.2734 | 0.6947 |
TemperatureScaling |
0.4929 | 0.0039 | 0.2731 | 0.6947 |
MatrixScaling |
0.4902 | 0.0043 | 0.2714 | 0.6926 |
CMSap |
0.4916 | 0.0046 | 0.2724 | 0.6947 |
CMSop |
0.4929 | 0.0051 | 0.2726 | 0.6947 |
Your numbers will differ slightly with the PyTorch version and thread count. Read them as follows:
- This small network is already close to calibrated, so the gains are modest. ECE falls by nearly a factor of four; NLL barely moves.
MatrixScalingis the only method here that can change the predicted class, so it is the only one whose Dice moves (0.6947 to 0.6926).TemperatureScaling,CMSapandCMSoppreserve the argmax (or the class ranking), so their Dice is unchanged.- No flexible calibrator beats temperature scaling on ECE here. With three classes
and a model this close to calibrated, a richer map has little to correct. Reach
for
MatrixScalingor the class-conditionalCMSap/CMSopwhen miscalibration depends on the class, and check Dice or volume metrics before adopting them. - ECE here is top-1, pixel-pooled ECE with 15 bins, not the per-image paper
protocol (see
expected_calibration_error).
The script also writes outputs/tutorial_reliability.png, a before/after
reliability diagram on the test split.