| """Compute the nine-model soft-vote ensemble on the pooled five-fold predictions. |
| |
| Every other result in the thesis reduces each model to a single pooled prediction |
| per image (the probabilities of its five fold checkpoints averaged on the fixed |
| 3,208-image test set). The original ensemble report shipped from the training VM |
| was instead computed on the single-checkpoint predictions, a different and |
| non-comparable aggregation. This script recomputes the ensemble in the same |
| pooled paradigm so Section 4.8 is consistent with Table 4.1 and the McNemar |
| analysis: each model is first pooled across its folds, the nine pooled |
| probability vectors are then combined by a soft vote weighted by each model's |
| macro-F1, and the result is compared against the best single pooled model with |
| the same exact-binomial McNemar test used in Section 4.5. |
| |
| Writes research_v2_latest/analysis/ensemble_pooled.json. |
| |
| Run with the project .venv interpreter: |
| .venv/bin/python thesis_build/make_ensemble_pooled.py |
| """ |
| import json |
| import numpy as np |
| from pathlib import Path |
| from scipy.stats import binom |
| from sklearn.metrics import ( |
| accuracy_score, precision_recall_fscore_support, roc_auc_score, cohen_kappa_score, |
| ) |
|
|
| ROOT = Path(__file__).resolve().parent.parent |
| KF = ROOT / "kfold" |
| OUT = ROOT / "analysis" / "ensemble_pooled.json" |
|
|
| CNN_CLIP = {"inception_v3", "clip_openai", "vgg19", "resnet101", "densenet121", "resnet50"} |
| ALL = ["inception_v3", "clip_openai", "vgg19", "resnet101", "dinov2_l", |
| "densenet121", "resnet50", "swin_b", "retfound"] |
| NUM_CLASSES = 10 |
| LABELS = list(range(NUM_CLASSES)) |
|
|
|
|
| def pooled(model): |
| if model in CNN_CLIP: |
| d = json.load(open(KF / "cnn_clip" / f"{model}_test_preds.json")) |
| return np.array(d["labels"]), np.array(d["probs"]) |
| 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"]) |
| return labels, np.mean(probs, axis=0) |
|
|
|
|
| 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 mcnemar(pred_a, pred_b, labels): |
| ca = pred_a == labels; cb = pred_b == labels |
| b = int(np.sum(ca & ~cb)); c = int(np.sum(~ca & cb)); n = b + c |
| p = 1.0 if n == 0 else float(min(1.0, 2 * binom.cdf(min(b, c), n, 0.5))) |
| return b, c, p |
|
|
|
|
| def main(): |
| P = {}; L = None |
| for m in ALL: |
| L, P[m] = pooled(m) |
| f1 = {m: precision_recall_fscore_support(L, P[m].argmax(1), average="macro", zero_division=0)[2] |
| for m in ALL} |
|
|
| |
| wsum = sum(f1.values()) |
| ens_probs = sum(f1[m] * P[m] for m in ALL) / wsum |
| ens_pred = ens_probs.argmax(1) |
|
|
| acc = accuracy_score(L, ens_pred) * 100 |
| prec, rec, fm, _ = precision_recall_fscore_support(L, ens_pred, average="macro", zero_division=0) |
| roc = roc_auc_score(L, ens_probs, multi_class="ovr", average="macro", labels=LABELS) |
|
|
| |
| best = max(ALL, key=lambda m: accuracy_score(L, P[m].argmax(1))) |
| best_acc = accuracy_score(L, P[best].argmax(1)) * 100 |
| b, c, p = mcnemar(ens_pred, P[best].argmax(1), L) |
|
|
| report = { |
| "protocol": "pooled five-fold soft vote, weight = pooled macro-F1, all nine models", |
| "members": ALL, |
| "ensemble": { |
| "acc": round(acc, 2), "f1": round(fm * 100, 2), "roc_auc": round(roc, 4), |
| "ece": round(ece(ens_probs, L), 4), "kappa": round(cohen_kappa_score(L, ens_pred), 3), |
| }, |
| "best_single": {"model": best, "acc": round(best_acc, 2), "f1": round(f1[best] * 100, 2)}, |
| "ensemble_vs_best_mcnemar": {"b": b, "c": c, "p": round(p, 3), |
| "significant_005": bool(p < 0.05)}, |
| "n_test": int(len(L)), |
| } |
| with open(OUT, "w") as fh: |
| json.dump(report, fh, indent=2) |
| with open("/tmp/ensemble_pooled_check.txt", "w") as fh: |
| fh.write(json.dumps(report, indent=2)) |
| print("wrote", OUT.relative_to(ROOT)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|