fourmodal-pathology-bind / experiments /13_eval_text_emergent.py
Carpediem123's picture
Migrate release files 151-180
e8fa066 verified
Raw History Blame Contribute Delete
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())