From 86ed453736832ab88b092557f38cc648599c4e63 Mon Sep 17 00:00:00 2001 From: rpotter6298 Date: Thu, 14 May 2026 13:51:41 +0200 Subject: [PATCH] Add new analysis scripts and configuration files for model comparison and feature extraction --- v4/classes/v4_hypertower.py | 63 ++++---- v4/configs/cd_geom_duo.json | 143 ++++++++++++++++++ v4/distributed/jobs.db | Bin 69632 -> 69632 bytes v4/scripts/analysis/inspect_embeddings.py | 22 ++- v4/scripts/analysis/logreg_cdr_compare.py | 129 ++++++++++++++++ .../tri_v1/baseline_tri_features_all.json | 10 ++ .../experiments/tri_v1/cd_geom_duo.json | 7 + 7 files changed, 342 insertions(+), 32 deletions(-) create mode 100644 v4/configs/cd_geom_duo.json create mode 100644 v4/scripts/analysis/logreg_cdr_compare.py create mode 100644 v4/scripts/experiments/tri_v1/baseline_tri_features_all.json create mode 100644 v4/scripts/experiments/tri_v1/cd_geom_duo.json diff --git a/v4/classes/v4_hypertower.py b/v4/classes/v4_hypertower.py index 8eba28a..6a773d0 100644 --- a/v4/classes/v4_hypertower.py +++ b/v4/classes/v4_hypertower.py @@ -266,6 +266,7 @@ def main(): save_features = cfg.get("save_features", False) fold_results = [] eval_stage_preds = [] # list[dict] — one per fold, only for eval_stage + all_phase_preds: dict[str, list[dict]] = {} # phase → list[dict] across folds t0 = time.time() for fold in range(cfg.get("folds", 5)): @@ -277,6 +278,11 @@ def main(): fold_results.append(result) if save_predictions and eval_stage in fold_preds: eval_stage_preds.append(fold_preds[eval_stage]) + if save_features: + for ph, pdata in fold_preds.items(): + if pdata.get("val_z") is None: + continue + all_phase_preds.setdefault(ph, []).append(pdata) print( f" fold{fold+1} DONE" f" val_auc={result.get(f'{eval_stage}_val_auc', float('nan')):.4f}" @@ -346,42 +352,43 @@ def main(): store.save(pred_path) print(f"Predictions saved: {pred_path}", flush=True) - if save_features and eval_stage_preds: - emb_dim = eval_stage_preds[0]["val_z"].shape[-1] - fstore = FeatureStore(n_folds=len(eval_stage_preds)) - - # Build entity_id / y_true universe (same as predictions). - seen, all_ids, id_to_y = set(), [], {} - for fp in eval_stage_preds: - for eid, y in zip(fp["val_ids"], fp["val_y"]): - k = str(eid) - if k not in seen: - seen.add(k); all_ids.append(eid) - id_to_y[k] = int(y) - if fp.get("test_ids"): - for eid, y in zip(fp["test_ids"], fp["test_y"]): + if save_features and all_phase_preds: + # One FeatureStore covers all phases; each phase gets its own group. + n_folds_any = max(len(v) for v in all_phase_preds.values()) + fstore = FeatureStore(n_folds=n_folds_any) + for phase, phase_preds in all_phase_preds.items(): + emb_dim = phase_preds[0]["val_z"].shape[-1] + seen, all_ids, id_to_y = set(), [], {} + for fp in phase_preds: + for eid, y in zip(fp["val_ids"], fp["val_y"]): k = str(eid) if k not in seen: seen.add(k); all_ids.append(eid) id_to_y[k] = int(y) + if fp.get("test_ids"): + for eid, y in zip(fp["test_ids"], fp["test_y"]): + k = str(eid) + if k not in seen: + seen.add(k); all_ids.append(eid) + id_to_y[k] = int(y) + y_true = np.array([id_to_y.get(str(e), -1) for e in all_ids], dtype=np.int64) + fstore.register_phase(phase=phase, entity_ids=all_ids, y_true=y_true) + fstore.register_head(phase=phase, head=f"{phase}_embedding", + n_epochs=1, embedding_dim=emb_dim) - y_true = np.array([id_to_y.get(str(e), -1) for e in all_ids], dtype=np.int64) - fstore.register_phase(phase=eval_stage, entity_ids=all_ids, y_true=y_true) - fstore.register_head(phase=eval_stage, head=f"{eval_stage}_embedding", - n_epochs=1, embedding_dim=emb_dim) - - for fold_idx, fp in enumerate(eval_stage_preds): - fstore.record(eval_stage, fold_idx, 0, fp["val_ids"], - f"{eval_stage}_embedding", fp["val_z"]) - fstore.set_split(eval_stage, fold_idx, fp["val_ids"], "val") - if fp.get("test_ids") and fp.get("test_z") is not None: - fstore.record(eval_stage, fold_idx, 0, fp["test_ids"], - f"{eval_stage}_embedding", fp["test_z"]) - fstore.set_split(eval_stage, fold_idx, fp["test_ids"], "test") + for fold_idx, fp in enumerate(phase_preds): + fstore.record(phase, fold_idx, 0, fp["val_ids"], + f"{phase}_embedding", fp["val_z"]) + fstore.set_split(phase, fold_idx, fp["val_ids"], "val") + if fp.get("test_ids") and fp.get("test_z") is not None: + fstore.record(phase, fold_idx, 0, fp["test_ids"], + f"{phase}_embedding", fp["test_z"]) + fstore.set_split(phase, fold_idx, fp["test_ids"], "test") feat_path = out_dir / "features.h5" fstore.save(feat_path) - print(f"Features saved: {feat_path}", flush=True) + print(f"Features saved (phases: {sorted(all_phase_preds.keys())}): {feat_path}", + flush=True) if __name__ == "__main__": diff --git a/v4/configs/cd_geom_duo.json b/v4/configs/cd_geom_duo.json new file mode 100644 index 0000000..41f3ef9 --- /dev/null +++ b/v4/configs/cd_geom_duo.json @@ -0,0 +1,143 @@ +{ + "_notes": [ + "Two-tower ensemble: cd + geom seg-CNN (UNet seg source).", + "Mirrors ensemble_fused.json but swaps the img tower for the geom tower.", + "Tells us how close the seg-CNN-over-masks gets to the image tower's contribution", + "when paired with clinical features." + ], + "run_name": "v4/cd_geom_duo", + "num_classes": 2, + "label_filter": [0, 1], + "split_identity_level": 1, + "eval_stage": "hb", + "save_predictions": true, + "save_features": true, + "seed": 1234, + "folds": 5, + "fold_seed": 100, + "output_root": "v4/results", + "out_dir_tags": ["binary"], + + "data": { + "module": "v4.classes.profiles.v4papila", + "args": { + "image_dir": "Papila/FundusImages", + "clinical_dir": "Papila/ClinicalData", + "label_col": "Diagnosis", + "iop_corr_method": "ratio", + "iop_drop_raw": true, + "exclude_cols": ["Axial_Length"], + "in_memory_cache": false + } + }, + + "towers": [ + { + "name": "cd", + "module": "v4.classes.towers.clinical_tower", + "class": "ClinicalEncoder", + "data_source": "matrix", + "args": { + "hidden_dim": 128 + } + }, + { + "name": "geom", + "module": "v4.classes.towers.geometry_tower", + "class": "GeometrySegEncoder", + "data_source": "image", + "args": { + "backbone": "resnet18", + "channels": 3, + "target_size": 224, + "augment": true, + "freeze_ratio": 0.0, + "seg_source": "unet", + "weights_path": "models/v2/refuge/segmentation/per_image/best.pt", + "contour_dir": "Papila/ExpertsSegmentations/Contours", + "unet_size": 512, + "normalize": "per_image", + "threshold": 0.5, + "crop_to_disc": true, + "finetune_epochs": 10, + "finetune_lr": 1e-5, + "finetune_batch_size": 4 + } + } + ], + + "stages": [ + { + "name": "cd_warm", + "type": "warm", + "tower": "cd", + "head_name": "cd_aux", + "level": "eye", + "epochs": 40 + }, + { + "name": "cd_aux", + "type": "head", + "input": "cd", + "train_with": "nt", + "bcd": true + }, + { + "name": "geom_aux", + "type": "head", + "input": "geom", + "train_with": "nt", + "bcd": true + }, + { + "name": "nt", + "type": "fusion", + "module": "v4.classes.bridges.fusion_bridge", + "class": "FusionBridge", + "inputs": ["cd", "geom"], + "level": "eye", + "epochs": 36, + "train_towers": true, + "warmup": { + "tower_epochs": 3, + "fused_epochs": 3 + }, + "args": { + "fusion_dim": 256 + } + }, + { + "name": "nt_head", + "type": "head", + "input": "nt", + "train_with": "nt" + }, + { + "name": "hb", + "type": "fusion", + "module": "v4.classes.bridges.hyperbridge", + "class": "HyperBridge", + "inputs": { "a": "nt", "b": "nt" }, + "level": "patient", + "epochs": 10, + "args": { + "hidden_dim": 256, + "mode": "embedding_mlp" + } + }, + { + "name": "hb_head", + "type": "head", + "input": "hb", + "train_with": "hb", + "args": { "dropout": 0.3 } + } + ], + + "training": { + "lr": 1e-4, + "batch_size": 8, + "bcd_prob": 0.5, + "tune_binary_threshold": true + } +} diff --git a/v4/distributed/jobs.db b/v4/distributed/jobs.db index 5f5bc10f669e370be2ef7310fdecd654268c49f2..f53c30528d80ee22831f8d918c16ea484cd6fda9 100644 GIT binary patch delta 627 zcmZozz|ydQWr8$g|3n#Q#{P{73;Fr^{xLA}{$b$#!~2IPgePRPfWRBx`o?+|e$`Oh zMr}oD4q0Dc1_q8a)6}$-6f*-|0}I2{ih|Ul%-qzxl4AXmqRjX*L;d8G`1I8L-1wBz zeEp)-0t2Hm6TRe|#Ny)AV!bkx_>4-Rj*|TH)S_r5UESpTytK@8B^?E&G826Wr&vEF zv$!C!Bsn8BML)Hk2n+PGiu3c7Vu2KdAa7#dm`m{}Q_>6uv?SQuGq8yHv_7@*1X}^%?NcLNFf*hC!mpU8Tj8q;AR1byZp*5%p8m$Sw0|!0xtdvphABJet!tuED&JN zudBfv%1BHcfDG%{d@-O!K!A(y3j-tHdOpr0=?gr!lNk*F9UK0<(&GV%Z| MaTbvHWB~`W$qEh} zo826i1I2}b;%p#suIR}E0YI4mw#{w