fundus-9model-benchmark / code /make_kfold_summary.py
DoB24's picture
Add pooled-protocol provenance scripts (regenerate summary, 9-model McNemar, per-class, ensemble from fold predictions)
63fc39b verified
Raw
History Blame Contribute Delete
4.47 kB
"""Regenerate the three foundation rows of research_v2_latest/kfold/kfold_v2_summary.csv
under the uniform pooled-ensemble protocol (Table 4.1 source).
The six convolutional/CLIP rows were produced on the training VM as a pooled
five-fold ensemble (probabilities averaged across the five fold checkpoints,
scored once on the fixed 3,208-image test set). The three foundation models were
originally summarised by the mean of their per-fold scores instead, a different,
non-comparable aggregation that also denied them the same ensemble treatment.
This script brings the foundation models into the identical pooled-ensemble
protocol so all nine rows are directly comparable and every model carries a
single aligned per-image prediction (which also lets the McNemar test in
make_mcnemar9.py cover all nine).
The six CNN/CLIP rows are kept verbatim from the authoritative VM summary; only
the three foundation rows are recomputed from
kfold/foundation_fold<k>_<model>_preds.json. Confidence intervals use the
bootstrap documented in research_v2_latest/code/ensemble_and_stats.py: 2000
resamples, seed 42, 2.5/97.5 percentile. Brier uses the element-wise mean
convention of the original summary.
Run with the project .venv interpreter:
.venv/bin/python thesis_build/make_kfold_summary.py
"""
import csv
import json
import numpy as np
from pathlib import Path
from sklearn.metrics import (
accuracy_score, precision_recall_fscore_support, roc_auc_score,
average_precision_score, cohen_kappa_score,
)
ROOT = Path(__file__).resolve().parent.parent
KF = ROOT / "kfold"
CSV = KF / "kfold_v2_summary.csv"
FOUNDATION = ["dinov2_l", "swin_b", "retfound"]
NUM_CLASSES = 10
LABELS = list(range(NUM_CLASSES))
def pooled_foundation(model):
probs, labels = [], None
for k in range(5):
d = json.load(open(KF / f"foundation_fold{k}_{model}_preds.json"))
probs.append(np.array(d["probs"])); labels = np.array(d["labels"])
P = np.mean(probs, axis=0)
return labels, P.argmax(1), P
def ece(probs, labels, n_bins=15):
conf = probs.max(1); pred = probs.argmax(1); correct = (pred == labels).astype(float)
bins = np.linspace(0, 1, n_bins + 1); e = 0.0
for i in range(n_bins):
m = (conf > bins[i]) & (conf <= bins[i + 1])
if m.sum():
e += m.mean() * abs(correct[m].mean() - conf[m].mean())
return float(e)
def boot_ci(fn, labels, preds, n=2000, seed=42):
rng = np.random.default_rng(seed); N = len(labels); vals = []
for _ in range(n):
idx = rng.integers(0, N, N)
vals.append(fn(labels[idx], preds[idx]))
return float(np.percentile(vals, 2.5)), float(np.percentile(vals, 97.5))
def foundation_row(model):
y, p, pr = pooled_foundation(model)
acc = accuracy_score(y, p) * 100
prec, rec, f1, _ = precision_recall_fscore_support(y, p, average="macro", zero_division=0)
acc_lo, acc_hi = boot_ci(lambda a, b: accuracy_score(a, b) * 100, y, p)
f1_lo, f1_hi = boot_ci(
lambda a, b: precision_recall_fscore_support(a, b, average="macro", zero_division=0)[2] * 100, y, p)
roc = roc_auc_score(y, pr, multi_class="ovr", average="macro", labels=LABELS)
oh = np.eye(NUM_CLASSES)[y]
prauc = average_precision_score(oh, pr, average="macro")
brier = float(((pr - oh) ** 2).mean())
return [model, "Foundation", round(acc, 2), round(acc_lo, 2), round(acc_hi, 2),
round(f1 * 100, 2), round(f1_lo, 2), round(f1_hi, 2),
round(prec * 100, 2), round(rec * 100, 2), round(roc, 4), round(prauc, 4),
round(ece(pr, y), 4), round(cohen_kappa_score(y, p), 3), round(brier, 3),
5, "five-fold pooled preds"]
def main():
with open(CSV) as fh:
rdr = csv.reader(fh); header = next(rdr); rows = list(rdr)
kept = [r for r in rows if r[1] != "Foundation"]
for r in kept:
r[16] = "five-fold pooled preds"
new_found = [foundation_row(m) for m in FOUNDATION]
allrows = kept + new_found
allrows.sort(key=lambda r: -float(r[2]))
with open(CSV, "w", newline="") as fh:
w = csv.writer(fh); w.writerow(header); w.writerows(allrows)
with open("/tmp/summary_check.txt", "w") as fh:
for r in allrows:
fh.write(f"{r[0]:14s} acc {r[2]:6} ({r[3]}-{r[4]}) f1 {r[5]:6} "
f"roc {r[10]} ece {r[12]} kappa {r[13]} brier {r[14]} {r[16]}\n")
print("wrote", CSV.relative_to(ROOT))
if __name__ == "__main__":
main()