FinetuneEvaluator¶
Registry key: "finetune".
Minimal single-GPU end-to-end fine-tune: BCE loss, AdamW optimizer, cosine learning-rate schedule, and optional early stopping. The same split-mode and variance-estimation machinery as the probe evaluators applies here — results are directly comparable across evaluator types.
Heavy training workflows (DDP, WandB, mixed-precision, MONAI Ignite engines) are intentionally out of scope.
Usage¶
from radharmony.evaluator import FinetuneEvaluator
ev = FinetuneEvaluator(
image_encoder,
train_dataset=train_ds,
test_dataset=test_ds,
n_seeds=3,
n_train_samples=[1500],
n_bootstrap=100,
epochs=20,
lr=1e-4,
freeze_backbone=False,
loss="bce",
output_dir="outputs/finetune",
)
df = ev.evaluate()
ev.save_results(df)
Head-only fine-tune (freeze the backbone):
ev = FinetuneEvaluator(
image_encoder,
train_dataset=train_ds,
test_dataset=test_ds,
epochs=30,
lr=1e-3,
freeze_backbone=True,
)
k-fold mode:
Constructor arguments¶
Specific to FinetuneEvaluator:
| Argument | Type | Default | Description |
|---|---|---|---|
n_folds |
int |
5 |
Number of folds in k-fold mode |
n_train_samples |
list[int] | None |
None |
Training-set size sweep |
epochs |
int |
20 |
Training epochs per seed/fold |
lr |
float |
1e-4 |
Learning rate for the classification head (and backbone when freeze_backbone=False) |
backbone_lr_scale |
float |
0.1 |
Multiplier applied to lr for backbone parameters when freeze_backbone=False |
weight_decay |
float |
0.05 |
AdamW weight decay |
freeze_backbone |
bool |
False |
If True, only the classification head is trained |
head |
nn.Module | None |
None |
Custom classification head; default = nn.Linear(D, n_labels) |
loss |
str |
"bce" |
"bce" (binary cross-entropy per label) or "ce" (cross-entropy) |
early_stop_metric |
str |
"auroc" |
Metric monitored for early stopping |
early_stop_patience |
int |
3 |
Epochs without improvement before stopping |
val_fraction |
float |
0.1 |
Fraction of the train subsample carved off (patient-grouped) as an inner val set for early stopping; 0.0 disables |
store_final_model |
bool |
False |
After evaluate(), also train a deployment model on all data → final_encoder_ / final_head_ (see Saving and reuse) |
Shared arguments (inherited from BaseClsEvaluator)¶
| Argument | Type | Default | Description |
|---|---|---|---|
image_encoder |
nn.Module / callable |
— | forward(imgs) → Tensor[B, D] |
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 |
labels |
list[str] |
None |
Subset of LABEL_COLS to evaluate; default = all |
device |
str |
"cuda" |
"cuda", "cuda:N", or "cpu" |
batch_size |
int |
64 |
Inference DataLoader batch size |
num_workers |
int |
4 |
Inference DataLoader workers |
autocast_dtype |
torch.dtype |
torch.bfloat16 |
Inference autocast dtype; None disables |
embedding_cache |
str |
None |
Path prefix to pickle cached embeddings (caches store raw features; normalization is reapplied after load) |
l2_normalize |
bool |
False |
L2-normalize features at the cache boundary |
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-row bootstrap resamples |
bootstrap_seed |
int |
0 |
RNG seed base for test-row resampling |
threshold_strategy |
str |
"youden" |
"youden", "f1", or "fixed:<float>" |
Saving and reuse¶
Train a single deployment model (encoder + head) once, persist it, and reuse it
for inference — without re-running the cross-validated evaluate():
ev = FinetuneEvaluator(image_encoder, dataset=ds, epochs=20, freeze_backbone=True)
ev.fit() # trains on all data (minus a patient-grouped
# inner-val slice for early stopping)
ev.save_model("runs/ft.pt")
# later — construct with the SAME encoder factory, head=, and freeze_backbone=:
ev2 = FinetuneEvaluator(image_encoder, dataset=ds, freeze_backbone=True)
ev2.load_model("runs/ft.pt")
preds = ev2.predict(test_ds) # [{image_id, y_true[L], prob[L]}, ...]
| Method | Behaviour |
|---|---|
fit(dataset=None) |
Train a deployment model on all data (defaults to dataset / train_dataset); sets final_encoder_ / final_head_ and returns the head. Uses the same patient-grouped inner-val early-stopping loop as evaluate(). |
save_model(path=None) |
Save the head weights + shape/label metadata; the encoder weights are saved too only when freeze_backbone=False (a frozen backbone is rebuilt unchanged from its factory). Defaults to <output_dir>/final_model.pt. Returns the path. |
load_model(path) |
Rebuild the head from the checkpoint's shape metadata and reload weights; warns on a freeze_backbone mismatch. Construct the evaluator with the same image_encoder factory, head=, and freeze_backbone=. |
predict(dataset, *, encoder=None, head=None, indices=None) |
Run the trained model over dataset, returning per-image {image_id, y_true, prob} (sigmoid probabilities over final_labels_). indices restricts to a subset; encoder / head default to final_encoder_ / final_head_. |
Setting store_final_model=True runs fit() automatically at the end of
evaluate(), so the benchmark and the saved model come from one call.
Notes¶
- When
freeze_backbone=False,backbone_lr_scale=0.1applies a 10× lower learning rate to backbone parameters. This prevents catastrophic forgetting while still allowing the backbone to adapt. - Bootstrap resamples reuse the fitted model's test predictions — no retraining per bootstrap draw.
loss="bce"treats each label as an independent binary classification task, which is appropriate for multi-label CXR datasets. Useloss="ce"for single-label datasets.- For very small
n_train_samples,freeze_backbone=Trueis usually better because there are too few examples to fine-tune the backbone without overfitting.