LinearProbeSegEvaluator¶
Registry key: "linear_probe_seg".
Trains a 1×1 nn.Conv2d head with bilinear upsampling on the frozen dense
patch features produced by a segmentation-mode backbone. The head is the
only trainable component, matching the "linear probe" protocol used for
classification — but applied to semantic segmentation.
Encoder + dataset contract¶
Two requirements that differ from the classification evaluators:
- Encoder must return
Tensor[B, D, H', W'](dense patch features), not the pooledTensor[B, D]. Every RadHarmonymake_*recipe supports this viaoutput_keys={"img", "mask"}:
from radharmony.evaluator.backbones import make_raddino
transform, encoder = make_raddino(device="cuda", output_keys={"img", "mask"})
- Dataset must emit a
"mask"key per sample (output_mask=True). The mask shape can be[B, 1, H, W],[B, H, W], or[B, C, H, W]one-hot — all are normalized to[B, H, W]int64 class indices internally.
If either contract is violated, evaluate() raises with a message pointing
at the right knob (see base.py:112-122).
Usage¶
from radharmony.evaluator import LinearProbeSegEvaluator
from radharmony.evaluator.backbones import make_raddino
from radharmony.dataset import SIIMACRPTXDataset
transform, encoder = make_raddino(device="cuda", output_keys={"img", "mask"})
ds = SIIMACRPTXDataset(
base_image_dir="/data/SIIM-ACR-PTX/",
transform=transform,
output_cls=True,
output_mask=True,
mask_output_dir="/scratch/ptx_masks/",
)
ev = LinearProbeSegEvaluator(
encoder,
dataset=ds, # k-fold mode
num_classes=2, # background + foreground
n_folds=5,
epochs=20,
lr=1e-3,
output_dir="outputs/linprobe_seg",
)
df = ev.evaluate()
ev.save_results(df)
Fixed-split mode:
ev = LinearProbeSegEvaluator(
encoder,
train_dataset=train_ds,
test_dataset=test_ds,
num_classes=2,
epochs=20,
n_seeds=3,
n_bootstrap=100,
)
df = ev.evaluate()
Constructor arguments¶
Specific to LinearProbeSegEvaluator:
| Argument | Type | Default | Description |
|---|---|---|---|
n_folds |
int |
5 |
Number of folds in k-fold mode; ignored in fixed-split mode |
n_train_samples |
list[int] | None |
None |
Training-set size sweep; None = full train pool as a single point |
epochs |
int |
20 |
Number of training epochs for the 1×1 Conv2d head |
lr |
float |
1e-3 |
AdamW learning rate |
weight_decay |
float |
0.05 |
AdamW weight decay |
class_weights |
list[float] | None |
None |
Per-class weights for nn.CrossEntropyLoss — useful for class-imbalanced foreground (e.g. small lesions) |
save_predictions |
bool |
False |
Write per-image GT / Pred / Prob PNGs under <output_dir>/{GT,Pred,Prob}/<run-id>/ |
early_stop_metric |
str |
"dice" |
Per-epoch val metric tracked for early stopping — any key in the seg metric panel (dice, iou, pixel_acc, tpr, tnr, ppv, npv); macro-averaged over the foreground classes |
early_stop_patience |
int |
5 |
Stop training after this many epochs without improvement on early_stop_metric; best-epoch head state is restored at the end |
val_fraction |
float |
0.1 |
Fraction of the train subsample carved off as an inner val set for early stopping. Set to 0.0 to disable. |
store_final_model |
bool |
False |
After evaluate(), also train a deployment head on all data → final_head_ (see Saving and reuse) |
Shared arguments (inherited from BaseSegEvaluator)¶
| Argument | Type | Default | Description |
|---|---|---|---|
image_encoder |
nn.Module / callable |
— | Frozen encoder; segmentation mode returns Tensor[B, D, H, W] |
dataset |
dataset | None |
k-fold mode (mutually exclusive with train_dataset/test_dataset) |
train_dataset |
dataset | None |
Fixed-split mode train pool |
test_dataset |
dataset | None |
Fixed-split mode test set |
num_classes |
int |
2 |
Number of segmentation classes, including background |
device |
str |
"cuda" |
"cuda", "cuda:N", or "cpu" |
batch_size |
int |
8 |
DataLoader batch size |
num_workers |
int |
4 |
DataLoader workers |
autocast_dtype |
torch.dtype |
torch.bfloat16 |
Encoder-forward autocast dtype; None disables |
output_dir |
str |
None |
Directory for CSV output |
n_seeds |
int |
1 |
Fixed-split only: train-subsample replicates |
base_seed |
int |
0 |
RNG seed base |
n_bootstrap |
int |
0 |
Fixed-split only: test-image bootstrap resamples |
bootstrap_seed |
int |
0 |
RNG seed base for test-image resampling |
threshold_strategy |
str |
"youden" |
Threshold selection strategy for binary metrics |
Output schema¶
evaluate() returns a pd.DataFrame with one row per
(class, fold|seed, [bootstrap]). Macro rows (label='macro_average')
aggregate the foreground classes per variance group. Metric panel:
| Column | Meaning |
|---|---|
dice |
Dice coefficient (2·TP / (2·TP + FP + FN)) |
iou |
Intersection-over-union (TP / (TP + FP + FN)) |
pixel_acc |
Pixel accuracy |
tpr |
Sensitivity / recall |
tnr |
Specificity |
ppv |
Precision |
npv |
Negative predictive value |
save_results(df, output_dir) writes results.csv (per-row) and
results_summary.csv (per-class mean / std / 95 % CI), matching the
classification evaluators' on-disk shape.
Saving and reuse¶
Train a single deployment head once, persist it, and reuse it for inference —
without re-running the cross-validated evaluate(). Only the trainable head is
saved; the frozen backbone is rebuilt from its factory at load time, so the
checkpoint is small.
ev = LinearProbeSegEvaluator(encoder, dataset=ds, num_classes=2, epochs=20)
ev.fit() # trains on all data (minus a patient-grouped
# inner-val slice for early stopping)
ev.save_head("runs/seg_head.pt")
# later — construct with the SAME backbone recipe and head args (num_classes, …):
transform, encoder = make_raddino(device="cuda", output_keys={"img", "mask"})
ev2 = LinearProbeSegEvaluator(encoder, dataset=ds, num_classes=2)
ev2.load_head("runs/seg_head.pt")
preds = ev2.predict(ds, indices=[0, 1, 2], return_images=True)
| Method | Behaviour |
|---|---|
fit(dataset=None) |
Train a deployment head on all data (defaults to dataset / train_dataset); sets final_head_ and returns it. Same patient-grouped inner-val early-stopping loop as evaluate(). |
save_head(path=None) |
Save the head state dict + shape / num_classes / head_config metadata. Defaults to <output_dir>/final_head.pt. Returns the path. |
load_head(path) |
Rebuild the head via _make_head from the checkpoint's shape metadata and reload weights; warns on a num_classes / head_config mismatch. |
predict(dataset, *, head=None, indices=None, return_images=False) |
Run the head over dataset, returning per-image {image_id, y_true[H,W], y_pred[H,W], prob[C,H,W]}. return_images=True adds a grayscale image[H,W] (min-max normalized, resized to the mask grid) for overlays; indices restricts to a subset; head defaults to final_head_. |
inner_val_indices(dataset=None) |
Dataset indices of the patient-grouped inner-val slice fit() holds out for early stopping. Deterministic (base_seed + patient grouping), so it reproduces the exact slice without retraining — works even after load_head(). Empty array when val_fraction disables the slice. Useful for visualizing held-out predictions (see fm_comparison_seg.ipynb). |
Setting store_final_model=True runs fit() automatically at the end of
evaluate(), so the benchmark and the saved head come from one call.
Notes¶
- The head is
nn.Conv2d(in_channels=D, out_channels=num_classes, kernel_size=1)followed byF.interpolate(..., mode="bilinear")to the mask resolution. - Encoder weights are frozen (
requires_grad_(False)); only the conv head is updated. Optimizer:AdamW+CosineAnnealingLR. - Early stopping is on by default. The val signal is the macro
foreground
early_stop_metric(defaultdice) recomputed each epoch; the best-epoch head state is restored before scoring. In both k-fold and fixed-split modes the val set is aval_fractionslice carved from the train subsample — the held-out fold (k-fold) and the test set (fixed-split) are used only for scoring, never for early-stopping selection. Setval_fraction=0.0to disable. Withearly_stop_patienceconsecutive no-improvement epochs, training exits early. - Single-channel masks with arbitrary value ranges (
{0,1},{0,255}, etc.) are rescaled to[0, num_classes-1]automatically — see linear_probe.py:95-118. bfloat16autocast is used for the encoder forward pass; the head runs in fp32 for numerical stability.- For SIIM-ACR-PTX / Montgomery / Shenzhen workflows see
notebooks/evaluator/fm_comparison_seg.ipynbandnotebooks/evaluator/evaluator_backbone_verify_seg.ipynb.