fundus-9model-benchmark / code /make_per_class_pooled_csv.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
3.72 kB
"""Generate per_class_metrics_pooled.csv from the latest five-fold predictions.
This is the provenance script for research_v2_latest/analysis/per_class_metrics_pooled.csv,
the per-class precision/recall/F1/support table that drives Appendix B and Figure 4.2.
Aggregation matches Table 4.1 exactly under the uniform pooled-ensemble protocol:
* the five convolutional networks and CLIP use their pooled five-fold test
predictions (kfold/cnn_clip/<model>_test_preds.json), i.e. the probabilities
averaged across the five fold checkpoints, scored once on the fixed test set;
* the three foundation models are pooled the same way, averaging the probability
vectors of their five fold checkpoints (kfold/foundation_fold<k>_<model>_preds.json)
into one aligned per-image prediction before the per-class metrics are computed.
Run with the project .venv interpreter (needs numpy + scikit-learn):
.venv/bin/python thesis_build/make_per_class_pooled_csv.py
"""
import csv
import json
import numpy as np
from pathlib import Path
from sklearn.metrics import precision_recall_fscore_support
ROOT = Path(__file__).resolve().parent.parent
KF = ROOT / "kfold"
OUT = ROOT / "analysis" / "per_class_metrics_pooled.csv"
# Class index -> display name (the dataset's alphabetical label order).
CLASSES = [
"Central Serous Chorioretinopathy", # 0
"Diabetic Retinopathy", # 1
"Disc Edema", # 2
"Glaucoma", # 3
"Healthy", # 4
"Macular Scar", # 5
"Myopia", # 6
"Pterygium", # 7
"Retinal Detachment", # 8
"Retinitis Pigmentosa", # 9
]
LABELS = list(range(len(CLASSES)))
# Model write order = accuracy-descending order of Table 4.1 (DINOv2-L is 5th).
CNN_CLIP = ["inception_v3", "clip_openai", "vgg19", "resnet101", "densenet121", "resnet50"]
FOUNDATION = ["dinov2_l", "swin_b", "retfound"]
MODEL_ORDER = ["inception_v3", "clip_openai", "vgg19", "resnet101", "dinov2_l",
"densenet121", "resnet50", "swin_b", "retfound"]
def pooled_cnn_clip(model):
"""Per-class metrics from the single pooled five-fold prediction file."""
d = json.load(open(KF / "cnn_clip" / f"{model}_test_preds.json"))
y, p = np.array(d["labels"]), np.array(d["preds"])
P, R, F, S = precision_recall_fscore_support(y, p, labels=LABELS, zero_division=0)
return P, R, F, S
def pooled_foundation(model):
"""Per-class metrics from the pooled five-fold ensemble (probabilities averaged
across the five fold checkpoints, then argmax), matching Table 4.1."""
files = sorted(KF.glob(f"foundation_fold*_{model}_preds.json"))
assert len(files) == 5, f"expected 5 folds for {model}, found {len(files)}"
probs, y = [], None
for f in files:
d = json.load(open(f))
probs.append(np.array(d["probs"])); y = np.array(d["labels"])
p = np.mean(probs, axis=0).argmax(1)
P, R, F, S = precision_recall_fscore_support(y, p, labels=LABELS, zero_division=0)
return P, R, F, S
def main():
rows = []
for model in MODEL_ORDER:
if model in CNN_CLIP:
P, R, F, S = pooled_cnn_clip(model)
else:
P, R, F, S = pooled_foundation(model)
for i in LABELS:
rows.append([model, CLASSES[i], P[i], R[i], F[i], int(S[i])])
with open(OUT, "w", newline="") as fh:
w = csv.writer(fh)
w.writerow(["model", "class", "precision", "recall", "f1", "support"])
w.writerows(rows)
print(f"wrote {OUT.relative_to(ROOT)} ({len(rows)} rows)")
if __name__ == "__main__":
main()