Duc-Anh Nguyen

Does in-domain SSL improve calibration?

Zero-shot calibration of frozen vision features on HyperKvasir, before vs. after self-supervised pretraining on GastroNet-5M — part of the Surgical Vision-Language Foundation Model collaboration.

LMU Munich × ZEISS · standalone benchmark, code public at github.com/DucAnhValentinoNguyen/VLF_Zeiss


The question

For four model settings — {LeJEPA, DINO v1} × {SigLIP-2 ViT-B/16 init, ImageNet ViT-B/16 init} — measure zero-shot calibration on HyperKvasir (weighted k-NN on frozen features, k = 20), then repeat after in-domain self-supervised pretraining on GastroNet-5M, and report the change in ECE and NLL across five downstream tasks (four classification, one zero-shot polyp segmentation).

3,244
lines, independent codebase
581,518
GastroNet-5M images curated for SSL
4 / 4
settings trained, 3 clean epochs each
~12.7 GB
total run storage, single-GPU throughout

Training specs

Every setting below ran to completion — full hyperparameters, loss/LR curves and per-step timing for all four are logged live to the W&B project. The table is the fixed part shared by every run; what varies is only the objective and the initialization.

BackboneViT-B/16, 768-d · feature = LayerNorm(mean patch token), non-affine
InitializationsSigLIP-2 (timm tag, open_clip fallback) · ImageNet-1k supervised (torchvision → timm remap)
SSL objectivesDINO v1 (centering + sharpening, teacher momentum 0.996 → 1) · LeJEPA (SIGReg isotropy + variance hinge + stop-grad prediction)
OptimizerAdamW, cosine schedule, base LR 1e-4 · layer-wise LR decay 0.75 · grad clip 3.0
EMALeJEPA backbone EMA decay 0.9995 · DINO teacher momentum cosine 0.996 → 1.0
Batch size · precision128 · bf16 mixed, gradient checkpointing
Schedule3 epochs on the curated tier · 14,061 steps per setting, verified by checkpoint step count
AugmentationEndoscopy-safe: rotation, colour jitter, blur — no flips or grayscale (laterality and colour are both diagnostic)
Zero-shot evalWeighted k-NN, k = 20, τ = 0.07, cosine similarity on L2-normalized features
CalibrationECE (15 equal-width + 15 adaptive bins), NLL, Brier · scalar temperature via LBFGS
HardwareSingle GPU per run, shared H100 94GB / A100‑SXM4‑80GB cluster, SLURM-scheduled

Zero-shot baseline — before any adaptation

Each backbone frozen at its public initialization, no in-domain SSL yet. Weighted k-NN over a 60% reference split, temperature fit on a 15% cal split, metrics on the held-out query split (~2,650 images per classification task; 900 query masks for segmentation). The point of this baseline is the contrast it draws with the post-SSL tables below.

Query accuracy (seg: Dice) · higher is better

0.25 .5.751 findingscategory tractpathology seg · Dice .846.940 .995.942.667 .763.897 .987.908.314
ImageNet initSigLIP-2 init

NLL (pre-temperature) · lower is better

0.25 .5.751.0 findingscategory tractpathology seg · px .557.177 .023.152.238 .926.319 .052.256.349
ImageNet initSigLIP-2 init
taskinitTaccbal-accAUROC ECE ewECE ew·TNLLNLL·T
tractimagenet ⚠1.38.9947.9955.9990.0023.0064.0233.0229
tractsiglip2 ⚠1.04.9872.9862.9977.0061.0038.0519.0515
pathologyimagenet1.33.9416.9013.9826.0170.0342.1522.1581
pathologysiglip20.91.9084.8512.9565.0338.0260.2555.2547
categoryimagenet1.19.9397.9434.9924.0258.0381.1772.1822
categorysiglip20.82.8968.9000.9802.0589.0330.3185.3099
findingsimagenet1.61.8455.6148.9474.0480.1114.5567.5547
findingssiglip21.34.7628.5027.9232.0843.1438.9259.9122
segmentationimagenetDice .667mIoU .541.0093.2384
segmentationsiglip2Dice .314mIoU .214.0140.3486
⚠ = accuracy > 0.98, flagged for leakage inspection (see below). Bold/green = better of the two inits on that metric.

ImageNet init beats SigLIP-2 on every task — the opposite of the pre-registered guess that a web-scale contrastive model would transfer better. For endoscopy k-NN it does not: supervised ImageNet features are more linearly separable here, and their patch tokens carry far more local structure (segmentation Dice 0.67 vs 0.31).

The tract task is flagged — confirmed structural, not leakage. A dedicated ontology audit found 23 of 23 HyperKvasir findings are tract-exclusive (each occurs in upper-GI or lower-GI images only, never both), so upper-vs-lower-GI is recoverable from coarse image content almost by construction, independent of the accuracy number itself. Recorded as an interpretation caveat rather than treated as a leak.

Temperature scaling is not uniformly helpful: for ImageNet, T > 1 and post-T ECE sometimes rises (findings: .048 → .111) — the k-NN “logits” are not true logits, and a scalar fit on a 15% cal split can over-correct. The pre-vs-post-SSL delta below is the intended read, not the absolute baseline value.


The headline finding

DINO is a reliable in-domain adapter — it improved calibration and/or segmentation from both initializations, with no accuracy cost anywhere. LeJEPA is powerful but init-sensitive: from the weaker SigLIP-2 init it trades some accuracy for markedly better calibration; applied to the already-strong ImageNet init, its aggressive distributional regularizer is destructive rather than helpful. The practical takeaway: match the SSL objective's aggressiveness to how much the starting representation actually needs to move.

Result table — Δ = post-SSL − pre-SSL

Weighted k-NN, k = 20, on a perceptual-hash-deduplicated split (near-duplicate endoscopy frames between the k-NN reference set and the held-out query set were removed first — 17% of query images). Negative ΔECE / ΔNLL = SSL improved that metric. Green = SSL helped, red = SSL hurt, grey = within noise.

DINO v1 / ImageNet init — effective rank 51.7, no collapse

taskΔECE ewΔECE·TΔNLLΔNLL·TΔaccΔbal-accΔAUROCΔDice
findings (20-way)−.0155−.0162−.045−.076+.021+.035+.009
category (4-way)−.0095−.0032+.024+.004−.002−.004−.002
pathology (2-way)+.0003+.0080+.023+.012−.003−.008−.002
tract (2-way)+.0007−.0020−.008−.007+.001+.001+.001
segmentation+.0035−.026+.061

DINO v1 / SigLIP-2 init — effective rank 46.7, no collapse

taskΔECE ewΔECE·TΔNLLΔNLL·TΔaccΔbal-accΔAUROCΔDice
findings (20-way)−.0646−.0687−.413−.438+.092+.144+.015
category (4-way)−.0492−.0103−.153−.152+.047+.055+.014
pathology (2-way)−.0247−.0014−.111−.111+.037+.053+.032
tract (2-way)−.0028−.0003−.038−.039+.010+.010+.002
segmentation+.0035−.131+.402
Single largest gain in the study: segmentation Dice +0.40.

LeJEPA / SigLIP-2 init — effective rank 25.8, no collapse

taskΔECE ewΔECE·TΔNLLΔNLL·TΔaccΔbal-accΔAUROCΔDice
findings (20-way)−.0521−.0724+.126−.000−.058−.014−.036
category (4-way)−.0374−.0149+.068+.068−.037−.025−.009
pathology (2-way)−.0189−.0053+.054+.046−.030−.051−.023
tract (2-way)+.0087+.0034+.072+.072−.030−.039−.006
segmentation+.0222+.138+.128
Calibration improves, traded against accuracy/NLL — discussed below.

LeJEPA / ImageNet init — severe regression, effective rank 33.9 (not collapsed)

taskΔECE ewΔECE·TΔNLLΔNLL·TΔaccΔbal-accΔAUROCΔDice
findings (20-way)+.367−.072+6.91+2.07−.625−.447−.273
category (4-way)+.183+.116+1.94+.750−.278−.423−.234
pathology (2-way)+.147+.127+1.30+.450−.188−.343−.410
tract (2-way)+.075+.023+.592+.529−.292−.364−.238
segmentation+.008+.095−.136
Raw NLL Δ shown to 2 decimals — the magnitudes are large, not a formatting artefact.
Investigated, not a bug

Query accuracy on the 20-way task collapsed from 82.4% pre-SSL to 19.9% post-SSL. Checked against a pipeline error and ruled out: the pre-SSL baselines in the same sweep are normal, the other three settings in the same sweep are normal, and SSL training itself shows no instability — loss fell smoothly, no non-finite-loss skips, effective rank stayed healthy (33.9; collapse reads ~1). AUROC staying above 0.5 confirms the features still carry some signal. Reading: LeJEPA's SIGReg term reshapes the batch toward an isotropic Gaussian; applied full-backbone to an already highly class-structured supervised embedding, it appears to overwrite that structure faster than the layer-wise LR decay can protect it. A sweep over lower LR / stronger layer decay is in progress to see whether gentler optimization avoids this.

The story across all four settings: DINO is the reliable adapter — it improved calibration and/or segmentation from both initializations, with no accuracy cost anywhere. LeJEPA is powerful but init-sensitive — from SigLIP-2 (room to improve) it trades accuracy for markedly better calibration and a strong segmentation gain; from ImageNet (already excellent) it is destructive, since the aggressive regularizer has nothing to gain and a great deal to lose. Findings and segmentation are where SSL matters most for the three well-behaved settings; the near-saturated tract/pathology tasks barely move either way. Practical takeaway: if the deployment init is a strong supervised backbone, prefer a gentle self-distillation objective (DINO) for in-domain adaptation, and reserve an aggressive distributional regularizer (LeJEPA) for weaker starting points.

Compute

settingGPUwall time
LeJEPA / SigLIP-2A100‑SXM4‑80GB1h 30m
DINO / ImageNetH100 94GB3h 43m
DINO / SigLIP-2H100 94GB2h 54m
LeJEPA / ImageNetH100 94GB1h 04m
All single-GPU, batch 128, bf16, gradient checkpointing, on LRZ's shared HPC cluster.

Case study — the LR = 0 bug

The first full-scale training job started on an A100 the morning of 2026-09-08. Watching its scalar dashboard live caught something the smoke tests could not: it was not training at all.

Symptom

Learning rate flat at 0.000e+00 for over 10,000 steps. Loss flat at 0.737 over the same span. The variance-hinge loss term pinned at its floor. Zero checkpoints written to the run directory.

Two independent bugs compounded:

  1. The framework's own step counter never advanced. The training step called the raw optimizer's .step() instead of the wrapped one — a workaround for an intermittent CPU hang on a login node, left in place on GPU where it was never needed. On GPU that means the step counter stays at 0: the cosine LR schedule, keyed on that counter, sits at its warmup-step-zero value — LR ≡ 0, so every optimizer step is a no-op — and the checkpoint trigger, keyed on the same counter, never fires either. A crash-and-resume would have restarted from scratch, forever.
  2. The data pipeline's notion of “one epoch” was 12× too long. An epoch-length setting meant for the whole pipeline had been applied per data-loading worker instead, so with 12 parallel workers one epoch became roughly 5 hours instead of the intended length. The other checkpoint trigger, keyed on epoch-end, would not have fired inside the job's time limit either.

Together: the run could produce nothing usable, and would have looped indefinitely, silently, across every restart.

Fix

1. Use the wrapped optimizer step on GPU (the raw bypass kept only for CPU smoke tests) — the step counter advances, so the schedule and the checkpoint trigger both work.

2. Move the epoch-length setting to apply after the parallel workers are merged back into one stream, not before — verified directly: request N batches, get exactly N, stable across epochs and worker counts.

The GPU smoke test had passed because a handful of steps at a tiny batch size sits entirely inside LR warmup — the rate is near-zero there whether or not the counter moves, and the smoke test only checks that a checkpoint file appears and that resuming from it works, which the epoch-end trigger did handle at that scale. The bug only showed up under a full-scale run, which is exactly why the run was being watched live rather than fired and checked the next day.