Download experiments/13_eval_text_emergent.py from Pathstudy/fourmodal-pathology-bind: direct link, hf CLI and curl.
- Browser
- Download file 5.22 kB
-
https://huggingface.co/Pathstudy/fourmodal-pathology-bind/resolve/main/experiments/13_eval_text_emergent.py
- Command line
-
hf download hf://Pathstudy/fourmodal-pathology-bind/experiments/13_eval_text_emergent.py
-
curl -L -o 13_eval_text_emergent.py https://huggingface.co/Pathstudy/fourmodal-pathology-bind/resolve/main/experiments/13_eval_text_emergent.py
5.22 kB
| """Emergent text alignment on ORION. The text head was trained ONLY on PathGen H&E-caption pairs (12_), the IF / | |
| marker encoders ONLY on ORION H&E pairs (09_, --if_enc convnext_tiny --mk_enc mlp). Text and IF/markers never | |
| co-occur in training; any text<->IF / text<->markers agreement is emergent through the shared UNI2 H&E space. | |
| (A) concept probes: prompts.json (PathGen-style sentences) scored against every ORION test tile embedding | |
| (he / if / markers); ground truth = marker-positive fraction (or nuclei count). Metrics: Spearman rho and | |
| AUROC (top quartile vs rest), on tissue tiles (>=20 nuclei; all tiles for 'cellularity'). | |
| Baselines: zero-shot PLIP and ImageBind (their own image/text towers, H&E only, no training). | |
| (B) retrieval with templated ORION descriptions (07_) as text queries through the PathGen-trained head | |
| (domain-shifted: template style != PathGen captions), tie-aware R@1 on 100-tile galleries. | |
| Outputs results/text_emergent_probes.csv, results/text_emergent_retrieval.csv | |
| """ | |
| import glob | |
| import json | |
| import os | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| import torch.nn.functional as F | |
| from scipy.stats import spearmanr | |
| from sklearn.metrics import roc_auc_score | |
| from common import CACHE, ROOT, RUNS, load_all_tiles, metrics | |
| TXT = ["plip", "biomedclip", "imagebind"] | |
| OUT = os.path.join(ROOT, "results") | |
| prompts = json.load(open(os.path.join(ROOT, "prompts.json"))) | |
| df = load_all_tiles() | |
| stats = pd.read_csv(os.path.join(OUT, "tile_stats.csv")).set_index("tile_id") | |
| df["n_nuclei"] = stats.loc[df.tile_id, "n_nuclei"].values | |
| def load_head(enc, din, dout): | |
| h = torch.nn.Sequential(torch.nn.Linear(din, 512), torch.nn.GELU(), torch.nn.Dropout(0.1), torch.nn.Linear(512, 512), | |
| torch.nn.GELU(), torch.nn.LayerNorm(512), torch.nn.Linear(512, dout)) | |
| h.load_state_dict(torch.load(os.path.join(RUNS, "text_pathgen", f"uni2_{enc}", "head.pt"), map_location="cpu")) | |
| return h.eval() | |
| def probe(scores, tiles, key, label_col): | |
| sub = tiles if key == "cellularity" else tiles[df.loc[tiles, "n_nuclei"].values >= 20] | |
| pos = np.isin(tiles, sub) | |
| s = scores[pos] | |
| y = df.loc[sub, label_col].values.astype(float) | |
| if label_col != "n_nuclei": | |
| y = y / np.maximum(df.loc[sub, "n_nuclei"].values, 1) | |
| lab = y >= np.quantile(y, 0.75) | |
| return {"spearman": float(spearmanr(s, y).correlation), | |
| "auroc": float(roc_auc_score(lab, s)) if 0 < lab.sum() < len(lab) else np.nan, "n": int(len(sub))} | |
| probe_rows, ret_rows = [], [] | |
| for f in sorted(glob.glob(os.path.join(RUNS, "bench", "*_uni2_convnext_tiny_mlp_*", "test_embeddings.pt"))): | |
| run = os.path.basename(os.path.dirname(f)) | |
| split, fold = run.split("_fold")[0], int(run.split("_fold")[1].split("_")[0]) | |
| E = torch.load(f, weights_only=False) | |
| tiles, Z = np.asarray(E["tile_id"]), E["z"] | |
| for enc in TXT: | |
| prm = torch.from_numpy(np.load(os.path.join(CACHE, f"prompts_text_{enc}.npy"))[:, 0].astype(np.float32)) | |
| orion_txt = torch.from_numpy(np.load(os.path.join(CACHE, f"text_{enc}.npy"))[tiles, 0].astype(np.float32)) | |
| head = load_head(enc, prm.shape[1], Z["he"].shape[1]) | |
| with torch.no_grad(): | |
| zp = F.normalize(head(prm), dim=-1) | |
| zt = F.normalize(head(orion_txt), dim=-1) | |
| for i, (key, pr) in enumerate(prompts.items()): | |
| for m in ["he", "if", "markers"]: | |
| sc = (Z[m] @ zp[i]).numpy() | |
| probe_rows.append({"split": split, "fold": fold, "text_enc": enc, "concept": key, "modality": m, | |
| "kind": "bound (PathGen text head)", **probe(sc, tiles, key, pr["label"])}) | |
| for m in ["he", "if", "markers"]: | |
| for q, g, name in [(zt, Z[m], f"text->{m}"), (Z[m], zt, f"{m}->text")]: | |
| ret_rows.append({"split": split, "fold": fold, "text_enc": enc, "pair": name, **metrics(q, g)}) | |
| # zero-shot baselines on H&E with the model's own towers (computed once per fold) | |
| for fm in ["plip", "imagebind"]: | |
| hz = F.normalize(torch.from_numpy(np.load(os.path.join(CACHE, f"he_{fm}.npy"))[tiles, 0].astype(np.float32)), dim=-1) | |
| pz = F.normalize(torch.from_numpy(np.load(os.path.join(CACHE, f"prompts_text_{fm}.npy"))[:, 0].astype(np.float32)), dim=-1) | |
| for i, (key, pr) in enumerate(prompts.items()): | |
| probe_rows.append({"split": split, "fold": fold, "text_enc": fm, "concept": key, "modality": "he", | |
| "kind": "zero-shot (own image tower)", **probe((hz @ pz[i]).numpy(), tiles, key, pr["label"])}) | |
| P = pd.DataFrame(probe_rows); R = pd.DataFrame(ret_rows) | |
| P.to_csv(os.path.join(OUT, "text_emergent_probes.csv"), index=False) | |
| R.to_csv(os.path.join(OUT, "text_emergent_retrieval.csv"), index=False) | |
| pd.set_option("display.width", 250); pd.set_option("display.max_rows", 300) | |
| print(P.groupby(["kind", "text_enc", "modality", "split"])[["auroc", "spearman"]].mean().round(3).unstack("split").to_string()) | |
| print(P.groupby(["kind", "text_enc", "modality", "concept"])["auroc"].mean().unstack("concept").round(2).to_string()) | |
| print(R.groupby(["text_enc", "pair", "split"])["R@1_g100"].mean().unstack("split").round(3).to_string()) | |