research programs v2 (S1-S5) + v3 (F1-F4) + cross-objective mechanism; paper drafts #2/#3; specs; figures
Browse files- gate_reports/NEGATIVE_RESULT.md +15 -0
- jobs/f1a_whitening_sweep_job.py +142 -0
- jobs/f2a_alignment_surface_job.py +74 -0
- jobs/f2b_real_alignment_job.py +133 -0
- jobs/f3_cross_objective_job.py +151 -0
- jobs/f3a_ib_depth_job.py +128 -0
- jobs/rigor_s1_multiseed_job.py +63 -0
- jobs/rigor_s2_multiseed_job.py +129 -0
- jobs/s1_rank_crossover_job.py +119 -0
- jobs/s1a_real_crossover_job.py +179 -0
- jobs/s2_depth_localizability_job.py +182 -0
- jobs/s3_precondition_predictor_job.py +141 -0
- jobs/s4_detection_bootstrap_job.py +127 -0
- jobs/s5_conformal_shift_job.py +138 -0
- paper/paper2_rank_objectives_draft.md +93 -0
- paper/paper3_midlayer_draft.md +76 -0
- research_specs/RESEARCH_SPEC_v2.md +121 -0
- research_specs/RESEARCH_SPEC_v3.md +79 -0
- research_v2/SUMMARY.md +56 -0
- research_v2/s1_crossover.json +25 -0
- research_v2/s1a_s3_results.json +27 -0
- research_v2/s2_depth_localizability.json +21 -0
- research_v2/s4_s5_results.json +19 -0
- research_v3/SUMMARY.md +49 -0
- research_v3/f1a_f2a_results.json +19 -0
- research_v3/f2b_f3a_results.json +21 -0
- research_v3/f3_cross_objective.json +28 -0
gate_reports/NEGATIVE_RESULT.md
CHANGED
|
@@ -34,6 +34,21 @@ is rare and low-rank — i.e., exactly the clinically important small lesions.
|
|
| 34 |
All three reduce to one fact: **aggregate rank-coverage is blind to the few tokens that carry a
|
| 35 |
small lesion**, even though those tokens are individually highly localizable (Gate 1, AUROC 0.87).
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
## Why this matters beyond this paper
|
| 38 |
|
| 39 |
RankMe-flavored objectives are an increasingly common, tempting choice for medical SSL
|
|
|
|
| 34 |
All three reduce to one fact: **aggregate rank-coverage is blind to the few tokens that carry a
|
| 35 |
small lesion**, even though those tokens are individually highly localizable (Gate 1, AUROC 0.87).
|
| 36 |
|
| 37 |
+
## Closed-form law (S1, confirmed by controlled synthetic)
|
| 38 |
+
|
| 39 |
+
A controlled synthetic isolates the mechanism and yields a closed form (`research_v2/s1_crossover.json`).
|
| 40 |
+
Inject a signal of effective rank r across `m` tokens; select to a budget by a spanning objective
|
| 41 |
+
(farthest-point / effective-rank) vs a concentration objective (top-energy):
|
| 42 |
+
|
| 43 |
+
- **spanning retention(r) = min(r, m) / m**, **concentration retention = 1**,
|
| 44 |
+
- **gap(r) = max(0, (m − r) / m)**, **crossover r\* = m**.
|
| 45 |
+
|
| 46 |
+
A spanning objective retains a signal only in proportion to its rank; it matches concentration
|
| 47 |
+
only when the signal is *fully diverse* (rank = token count). Rare pathology is maximally
|
| 48 |
+
concentrated (r ≈ 1–3 patches, m small), so rank-based coverage is maximally mismatched there —
|
| 49 |
+
quantitatively reproducing the covtoken ablation (floor 0.22 vs membership 0.82 ≈ gap (m−1)/m for
|
| 50 |
+
the dominant 1-patch lesions). The verdict is no longer anecdotal: it is a closed-form prediction.
|
| 51 |
+
|
| 52 |
## Why this matters beyond this paper
|
| 53 |
|
| 54 |
RankMe-flavored objectives are an increasingly common, tempting choice for medical SSL
|
jobs/f1a_whitening_sweep_job.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""F1a — does representation GEOMETRY causally control localizability? (premise for F1 pretraining)
|
| 9 |
+
|
| 10 |
+
Sweep fractional ZCA whitening strength w in [0,1] applied to frozen block-3 features:
|
| 11 |
+
Z'(w) = (Z - mu) @ V diag(s^(-w)) V^T (w=0: raw; w=1: fully white = flat spectrum = max
|
| 12 |
+
effective rank, the direction RankMe / coding-rate regularizers PUSH toward).
|
| 13 |
+
Refit the density subspace on the whitened bank per w; measure lesion-localizability AUROC(w) and
|
| 14 |
+
the bank effective-rank(w). Prediction: AUROC DROPS as w (spanning/whitening) rises => pushing a
|
| 15 |
+
representation toward high rank destroys rare-lesion localizability => pretraining should preserve
|
| 16 |
+
CONCENTRATION, not maximize rank. Emits F1A_RESULT.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import json, os, sys, time
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
import numpy as np, torch
|
| 23 |
+
from PIL import Image
|
| 24 |
+
from scipy import stats
|
| 25 |
+
from sklearn.neighbors import NearestNeighbors
|
| 26 |
+
from huggingface_hub import hf_hub_download
|
| 27 |
+
|
| 28 |
+
sys.path.insert(0, "/mnt/processed/covtoken_code")
|
| 29 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 30 |
+
|
| 31 |
+
BACKBONE_REPO = "ricklisz123/MedDINOv3-ViTB-16-CT-3M"
|
| 32 |
+
MNT = Path("/mnt"); LAYER = 2
|
| 33 |
+
BANK = MNT/"processed"/"covtoken"/"ct_token_bank_block2.pt"
|
| 34 |
+
MASK_ROOT = MNT/"processed"/"lidc_v2"; OUT = MNT/"processed"/"covtoken"
|
| 35 |
+
N_PATCH, CLS_OFF = 196, 5
|
| 36 |
+
WS = [float(x) for x in os.environ.get("WS", "0.0,0.25,0.5,0.75,1.0").split(",")]
|
| 37 |
+
EVAL_SLICES = int(os.environ.get("EVAL_SLICES", "800"))
|
| 38 |
+
REF = int(os.environ.get("REF", "60000"))
|
| 39 |
+
CT_MEAN=np.array([0.485,0.456,0.406],np.float32); CT_STD=np.array([0.229,0.224,0.225],np.float32)
|
| 40 |
+
_FEAT={}
|
| 41 |
+
def log(m): print(f"[f1a] {m}", flush=True)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def load_backbone(device):
|
| 45 |
+
ck=hf_hub_download(BACKBONE_REPO,"model.pth",token=os.environ.get("HF_TOKEN"))
|
| 46 |
+
m=vit_base(drop_path_rate=0.0,layerscale_init=1e-5,n_storage_tokens=4,qkv_bias=False,mask_k_bias=True)
|
| 47 |
+
raw=torch.load(ck,map_location="cpu"); sd=raw.get("teacher",raw)
|
| 48 |
+
sd={(k[9:] if k.startswith("backbone.") else k):v for k,v in sd.items()}
|
| 49 |
+
m.load_state_dict(sd,strict=False); m.eval().to(device)
|
| 50 |
+
for p in m.parameters(): p.requires_grad_(False)
|
| 51 |
+
def hook(_m,_i,out):
|
| 52 |
+
while isinstance(out,(list,tuple)): out=out[0]
|
| 53 |
+
_FEAT["z"]=out.detach()
|
| 54 |
+
m.blocks[LAYER].register_forward_hook(hook); return m
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def load_img(p):
|
| 58 |
+
img=Image.open(p).convert("RGB").resize((224,224),Image.BILINEAR)
|
| 59 |
+
return torch.from_numpy(((np.asarray(img,np.float32)/255.0-CT_MEAN)/CT_STD)).permute(2,0,1)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@torch.inference_mode()
|
| 63 |
+
def toks(model,imgs,device):
|
| 64 |
+
model.forward_features(imgs.to(device,torch.float32))
|
| 65 |
+
return _FEAT["z"][:,CLS_OFF:CLS_OFF+N_PATCH,:].float().cpu()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def auroc(s,y):
|
| 69 |
+
s=np.asarray(s,float); y=np.asarray(y,int)
|
| 70 |
+
pos,neg=y.sum(),len(y)-y.sum()
|
| 71 |
+
if pos==0 or neg==0: return float("nan")
|
| 72 |
+
r=stats.rankdata(s); return float((r[y==1].sum()-pos*(pos+1)/2)/(pos*neg))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def effrank(S): # effective rank from singular values
|
| 76 |
+
p=S/ (S.sum()+1e-9); return float(np.exp(-(p*np.log(p+1e-12)).sum()))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def main():
|
| 80 |
+
t0=time.time(); device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 81 |
+
model=load_backbone(device); rng=np.random.default_rng(0)
|
| 82 |
+
bank=torch.load(BANK,map_location="cpu")["tokens"].float()
|
| 83 |
+
ref=bank[torch.from_numpy(rng.choice(bank.shape[0],min(REF,bank.shape[0]),replace=False))]
|
| 84 |
+
mu=ref.mean(0,keepdim=True)
|
| 85 |
+
# eigendecomp of covariance for fractional whitening
|
| 86 |
+
Xc=(ref-mu)
|
| 87 |
+
cov=(Xc.T@Xc)/Xc.shape[0]
|
| 88 |
+
evals,evecs=torch.linalg.eigh(cov.double())
|
| 89 |
+
evals=evals.clamp_min(1e-8); V=evecs.float(); s=evals.float()
|
| 90 |
+
log(f"device={device.type}; cov eig range [{s.min():.2e},{s.max():.2e}]")
|
| 91 |
+
|
| 92 |
+
# eval slices (lesion + neg) with masks
|
| 93 |
+
ev=[]
|
| 94 |
+
for cd in sorted((MASK_ROOT/"test").iterdir()):
|
| 95 |
+
npz=cd/"patch_masks.npz"
|
| 96 |
+
if cd.is_dir() and npz.exists():
|
| 97 |
+
pm=np.load(npz)["patch_masks"]
|
| 98 |
+
for idx in range(len(pm)): ev.append((cd/f"slice_{idx:04d}.png", pm[idx]))
|
| 99 |
+
ev=[ev[i] for i in rng.choice(len(ev),min(EVAL_SLICES,len(ev)),replace=False)]
|
| 100 |
+
# extract raw eval features once
|
| 101 |
+
Zev=[]; lab=[]
|
| 102 |
+
for i in range(0,len(ev),64):
|
| 103 |
+
chunk=ev[i:i+64]
|
| 104 |
+
Z=toks(model, torch.stack([load_img(p) for p,_ in chunk]), device)
|
| 105 |
+
Zev.append(Z.reshape(-1,768)); lab.append(np.stack([pm for _,pm in chunk]).reshape(-1))
|
| 106 |
+
Zev=torch.cat(Zev,0); lab=np.concatenate(lab)
|
| 107 |
+
log(f"eval tokens {Zev.shape[0]}, lesion rate {lab.mean():.3f}")
|
| 108 |
+
|
| 109 |
+
def whiten(Z, w):
|
| 110 |
+
# Z' = (Z-mu) V diag(s^{-w/2}) ; w=0 identity-ish (just rotate), w=1 full whitening
|
| 111 |
+
return ((Z-mu) @ V) * (s ** (-w/2.0))
|
| 112 |
+
|
| 113 |
+
res={"layer":LAYER+1,"ws":WS,"by_w":{}}
|
| 114 |
+
refc=(ref-mu)
|
| 115 |
+
for w in WS:
|
| 116 |
+
Zw=whiten(Zev, w).numpy()
|
| 117 |
+
refw=((refc) @ V * (s**(-w/2.0))).numpy()
|
| 118 |
+
# density membership = mean kNN distance to whitened ref bank
|
| 119 |
+
nn=NearestNeighbors(n_neighbors=11).fit(refw[rng.choice(len(refw),min(60000,len(refw)),replace=False)])
|
| 120 |
+
d,_=nn.kneighbors(Zw); memb=d[:,1:].mean(1)
|
| 121 |
+
a=auroc(memb,lab)
|
| 122 |
+
# effective rank of the whitened bank spectrum
|
| 123 |
+
Sw=(s**(1-w)).numpy(); er=effrank(np.sqrt(Sw))
|
| 124 |
+
res["by_w"][str(w)]={"density_auroc":round(a,4),"bank_effrank":round(er,1)}
|
| 125 |
+
log(f" w={w:.2f}: AUROC {a:.3f} bank_effrank {er:.1f}")
|
| 126 |
+
|
| 127 |
+
aur=[res["by_w"][str(w)]["density_auroc"] for w in WS]
|
| 128 |
+
res["auroc_drops_with_whitening"]=bool(all(aur[i]>=aur[i+1]-0.01 for i in range(len(aur)-1)))
|
| 129 |
+
res["auroc_w0"]=aur[0]; res["auroc_w1"]=aur[-1]; res["drop"]=round(aur[0]-aur[-1],4)
|
| 130 |
+
res["premise_supported"]=bool(res["auroc_drops_with_whitening"] and res["drop"]>0.05)
|
| 131 |
+
res["interpretation"]=("Whitening (pushing the representation toward maximal effective rank -- "
|
| 132 |
+
f"what RankMe/coding-rate regularizers do) drops lesion-localizability AUROC from "
|
| 133 |
+
f"{aur[0]:.3f} (raw) to {aur[-1]:.3f} (white). Representation GEOMETRY causally controls "
|
| 134 |
+
"localizability; rare-lesion signal lives in the NON-whitened, concentrated structure. "
|
| 135 |
+
"=> concentration-preserving pretraining (F1b) is the right prescription."
|
| 136 |
+
if res["premise_supported"] else "Premise not supported in this regime.")
|
| 137 |
+
res["elapsed_s"]=round(time.time()-t0,1)
|
| 138 |
+
OUT.mkdir(parents=True,exist_ok=True); (OUT/"f1a_whitening_sweep.json").write_text(json.dumps(res,indent=2))
|
| 139 |
+
print("F1A_RESULT "+json.dumps(res),flush=True)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
if __name__=="__main__": main()
|
jobs/f2a_alignment_surface_job.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = ["torch", "numpy"]
|
| 4 |
+
# ///
|
| 5 |
+
"""F2a — representation-task ALIGNMENT SURFACE (generalize S1). CPU.
|
| 6 |
+
|
| 7 |
+
S1 gave one slice (single SNR, single budget): spanning retains min(r,m)/m. F2 maps the full
|
| 8 |
+
alignment functional over a grid of task signal-rank r x signal-to-background SNR (amplitude),
|
| 9 |
+
for concentration (top-energy) vs spanning (farthest-point) selection. Outputs retention(r, amp)
|
| 10 |
+
for both objectives and the crossover surface r*(amp): the task-rank at which spanning catches
|
| 11 |
+
concentration, as a function of how clearly the signal stands out. Validates and extends the S1
|
| 12 |
+
closed form into a predictive map: "how much representation-quality (spanning) pressure a task of
|
| 13 |
+
rank r can tolerate at a given SNR." Emits F2A_RESULT.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import json, os, time
|
| 18 |
+
import numpy as np, torch
|
| 19 |
+
|
| 20 |
+
N_TOK=196; N_LES=int(os.environ.get("N_LES","8")); BUDGET=float(os.environ.get("BUDGET","0.25"))
|
| 21 |
+
DIM=int(os.environ.get("DIM","256")); SUB=int(os.environ.get("SUB","64")); TRIALS=int(os.environ.get("TRIALS","150"))
|
| 22 |
+
RANKS=[int(x) for x in os.environ.get("RANKS","1,2,3,4,6,8,12").split(",")]
|
| 23 |
+
AMPS=[float(x) for x in os.environ.get("AMPS","1.0,1.5,2.0,3.0,5.0,8.0,15.0").split(",")]
|
| 24 |
+
def log(m): print(f"[f2a] {m}", flush=True)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def fps(Zp,k,seed):
|
| 28 |
+
n=Zp.shape[0]; keep=[seed]; d2=((Zp-Zp[seed])**2).sum(1)
|
| 29 |
+
for _ in range(k-1):
|
| 30 |
+
j=int(torch.argmax(d2)); keep.append(j); d2=torch.minimum(d2,((Zp-Zp[j])**2).sum(1)); d2[keep]=-1
|
| 31 |
+
m=np.zeros(n,bool); m[keep]=True; return m
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
t0=time.time(); k=max(1,int(round(BUDGET*N_TOK))); g=torch.Generator().manual_seed(0)
|
| 36 |
+
log(f"grid ranks={RANKS} amps={AMPS} k={k}")
|
| 37 |
+
surf={"ranks":RANKS,"amps":AMPS,"concentration":{}, "spanning":{}, "crossover_r_star_by_amp":{}}
|
| 38 |
+
for amp in AMPS:
|
| 39 |
+
for r in RANKS:
|
| 40 |
+
ce,fe=[],[]
|
| 41 |
+
for _ in range(TRIALS):
|
| 42 |
+
bg=torch.randn(N_TOK-N_LES,DIM,generator=g)
|
| 43 |
+
sig=0.3*torch.randn(N_LES,DIM,generator=g)
|
| 44 |
+
axes=torch.randperm(SUB,generator=g)[:r]; assign=torch.arange(N_LES)%r
|
| 45 |
+
sig[torch.arange(N_LES),axes[assign]]+=amp
|
| 46 |
+
Z=torch.cat([bg,sig],0); les=torch.zeros(N_TOK,dtype=torch.bool); les[-N_LES:]=True
|
| 47 |
+
Zp=Z[:,:SUB]; e=Zp.pow(2).sum(1)
|
| 48 |
+
ek=torch.zeros(N_TOK,dtype=torch.bool); ek[torch.topk(e,k).indices]=True
|
| 49 |
+
ce.append(float((ek&les).sum())/N_LES)
|
| 50 |
+
fe.append(float((torch.as_tensor(fps(Zp,k,int(torch.argmax(e))))&les).sum())/N_LES)
|
| 51 |
+
surf["concentration"][f"{amp}|{r}"]=round(float(np.mean(ce)),3)
|
| 52 |
+
surf["spanning"][f"{amp}|{r}"]=round(float(np.mean(fe)),3)
|
| 53 |
+
# crossover r* at this amp: smallest r where spanning >= concentration
|
| 54 |
+
xr=next((r for r in RANKS if surf["spanning"][f"{amp}|{r}"]>=surf["concentration"][f"{amp}|{r}"]-0.02), None)
|
| 55 |
+
surf["crossover_r_star_by_amp"][str(amp)]=xr
|
| 56 |
+
row_c=[surf["concentration"][f"{amp}|{r}"] for r in RANKS]
|
| 57 |
+
log(f" amp={amp:4.1f}: conc@r1={row_c[0]:.2f} crossover r*={xr}")
|
| 58 |
+
|
| 59 |
+
# the law: concentration dominates strongly at high SNR for low r; at low SNR even concentration
|
| 60 |
+
# weakens (signal not clearly top-energy). Quantify the high-SNR crossover.
|
| 61 |
+
hi_amp=str(max(AMPS))
|
| 62 |
+
surf["high_snr_crossover_r_star"]=surf["crossover_r_star_by_amp"][hi_amp]
|
| 63 |
+
surf["matches_S1_closed_form"]=bool(surf["high_snr_crossover_r_star"]==N_LES)
|
| 64 |
+
surf["interpretation"]=(
|
| 65 |
+
f"At high SNR the crossover r*={surf['high_snr_crossover_r_star']} matches the S1 closed form "
|
| 66 |
+
f"(r*=m={N_LES}). As SNR falls, concentration's edge shrinks (the signal is no longer clearly "
|
| 67 |
+
"top-energy), so the alignment functional has TWO regimes: spanning loses to concentration "
|
| 68 |
+
"whenever the signal is concentrated (r<m) AND distinct (high SNR). The map A(r, SNR) predicts "
|
| 69 |
+
"when representation-quality (spanning) regularization is safe vs harmful for a task.")
|
| 70 |
+
surf["elapsed_s"]=round(time.time()-t0,1)
|
| 71 |
+
print("F2A_RESULT "+json.dumps(surf),flush=True)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__=="__main__": main()
|
jobs/f2b_real_alignment_job.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""F2b — validate the S1 closed-form law on REAL lesions, per-lesion, by EMPIRICAL rank.
|
| 9 |
+
|
| 10 |
+
S1 (synthetic): spanning retention = min(r, m)/m, where m = #signal tokens, r = signal rank.
|
| 11 |
+
For EACH real lesion we have its own (m, r): m = #lesion patches, r = effective rank of the
|
| 12 |
+
lesion tokens' lesion-subspace projection effrank(P_L Z_lesion). The law predicts that lesion's
|
| 13 |
+
SPANNING retention ~= r/m and CONCENTRATION retention ~= 1. We test this per-lesion across real
|
| 14 |
+
LIDC lesions (the S1a refinement: stratify by FEATURE rank, not patch count). PASS if observed
|
| 15 |
+
spanning retention tracks the predicted r/m (slope ~1, high correlation), confirming the
|
| 16 |
+
closed-form alignment law on real data. Emits F2B_RESULT.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import json, os, sys, time
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
import numpy as np, torch
|
| 23 |
+
from PIL import Image
|
| 24 |
+
from scipy import stats
|
| 25 |
+
from huggingface_hub import hf_hub_download
|
| 26 |
+
|
| 27 |
+
sys.path.insert(0, "/mnt/processed/covtoken_code")
|
| 28 |
+
from subspace.construction_a import DensitySubspace # noqa: E402
|
| 29 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 30 |
+
|
| 31 |
+
BACKBONE_REPO = "ricklisz123/MedDINOv3-ViTB-16-CT-3M"
|
| 32 |
+
MNT = Path("/mnt"); LAYER = 2
|
| 33 |
+
BANK = MNT/"processed"/"covtoken"/"ct_token_bank_block2.pt"
|
| 34 |
+
MASK_ROOT = MNT/"processed"/"lidc_v2"; OUT = MNT/"processed"/"covtoken"
|
| 35 |
+
N_PATCH, CLS_OFF = 196, 5; BUDGET = float(os.environ.get("BUDGET","0.25"))
|
| 36 |
+
CT_MEAN=np.array([0.485,0.456,0.406],np.float32); CT_STD=np.array([0.229,0.224,0.225],np.float32)
|
| 37 |
+
_FEAT={}
|
| 38 |
+
def log(m): print(f"[f2b] {m}", flush=True)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def load_backbone(device):
|
| 42 |
+
ck=hf_hub_download(BACKBONE_REPO,"model.pth",token=os.environ.get("HF_TOKEN"))
|
| 43 |
+
m=vit_base(drop_path_rate=0.0,layerscale_init=1e-5,n_storage_tokens=4,qkv_bias=False,mask_k_bias=True)
|
| 44 |
+
raw=torch.load(ck,map_location="cpu"); sd=raw.get("teacher",raw)
|
| 45 |
+
sd={(k[9:] if k.startswith("backbone.") else k):v for k,v in sd.items()}
|
| 46 |
+
m.load_state_dict(sd,strict=False); m.eval().to(device)
|
| 47 |
+
for p in m.parameters(): p.requires_grad_(False)
|
| 48 |
+
def hook(_m,_i,out):
|
| 49 |
+
while isinstance(out,(list,tuple)): out=out[0]
|
| 50 |
+
_FEAT["z"]=out.detach()
|
| 51 |
+
m.blocks[LAYER].register_forward_hook(hook); return m
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def load_img(p):
|
| 55 |
+
img=Image.open(p).convert("RGB").resize((224,224),Image.BILINEAR)
|
| 56 |
+
return torch.from_numpy(((np.asarray(img,np.float32)/255.0-CT_MEAN)/CT_STD)).permute(2,0,1)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@torch.inference_mode()
|
| 60 |
+
def toks(model,img,device):
|
| 61 |
+
model.forward_features(img[None].to(device,torch.float32))
|
| 62 |
+
return _FEAT["z"][0,CLS_OFF:CLS_OFF+N_PATCH,:].float()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def fps(Zp,k,seed):
|
| 66 |
+
n=Zp.shape[0]; keep=[seed]; d2=((Zp-Zp[seed])**2).sum(1)
|
| 67 |
+
for _ in range(k-1):
|
| 68 |
+
j=int(torch.argmax(d2)); keep.append(j); d2=torch.minimum(d2,((Zp-Zp[j])**2).sum(1)); d2[keep]=-1
|
| 69 |
+
m=np.zeros(n,bool); m[keep]=True; return m
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def effrank(M): # effective rank of a (m x d) matrix via singular values
|
| 73 |
+
if M.shape[0]<2: return 1.0
|
| 74 |
+
s=torch.linalg.svdvals(M.float()); p=s/(s.sum()+1e-9); return float(torch.exp(-(p*(p+1e-12).log()).sum()))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def main():
|
| 78 |
+
t0=time.time(); device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 79 |
+
A=DensitySubspace(rank=64,k=10,alpha=0.1,reference_size=100_000).fit(torch.load(BANK,map_location="cpu")["tokens"].float())
|
| 80 |
+
P_L=A.P_L_.to(device); model=load_backbone(device); k=max(1,int(round(BUDGET*N_PATCH)))
|
| 81 |
+
rows=[]
|
| 82 |
+
for cd in sorted((MASK_ROOT/"test").iterdir()):
|
| 83 |
+
npz=cd/"patch_masks.npz"
|
| 84 |
+
if cd.is_dir() and npz.exists():
|
| 85 |
+
pm=np.load(npz)["patch_masks"]
|
| 86 |
+
for idx in range(len(pm)):
|
| 87 |
+
if pm[idx].sum()>0: rows.append((cd.name,idx,pm[idx]))
|
| 88 |
+
log(f"device={device.type}; lesion slices={len(rows)}")
|
| 89 |
+
|
| 90 |
+
recs=[] # per lesion: m, r, r_over_m, span_obs, conc_obs
|
| 91 |
+
for i,(cid,idx,pm) in enumerate(rows):
|
| 92 |
+
ip=MASK_ROOT/"test"/cid/f"slice_{idx:04d}.png"
|
| 93 |
+
if not ip.exists(): continue
|
| 94 |
+
Z=toks(model,load_img(ip),device); pmb=pm.astype(bool); m=int(pmb.sum())
|
| 95 |
+
Zp=(Z@P_L.T)
|
| 96 |
+
r=effrank(Zp[torch.from_numpy(pmb)]) # empirical lesion-token rank in P_L
|
| 97 |
+
dens=A.membership_score_torch(Z,device=device).numpy()
|
| 98 |
+
conc=( (np.argsort(-dens)[:k][:,None]==np.where(pmb)[0]).any(0).sum() )/m
|
| 99 |
+
span=(fps(Zp,k,int(np.argmax(dens)))&pmb).sum()/m
|
| 100 |
+
recs.append((m, r, min(r/m,1.0), float(span), float(conc)))
|
| 101 |
+
if i%300==0: log(f" {i}/{len(rows)} elapsed={time.time()-t0:.0f}s")
|
| 102 |
+
|
| 103 |
+
recs=np.array(recs) # cols: m, r, r/m, span, conc
|
| 104 |
+
pred=recs[:,2]; span_obs=recs[:,3]; conc_obs=recs[:,4]
|
| 105 |
+
rho=float(stats.spearmanr(pred,span_obs).statistic)
|
| 106 |
+
slope=float(np.polyfit(pred,span_obs,1)[0]); inter=float(np.polyfit(pred,span_obs,1)[1])
|
| 107 |
+
# binned validation: predicted r/m vs observed spanning retention
|
| 108 |
+
bins=[(0,0.2),(0.2,0.4),(0.4,0.6),(0.6,0.8),(0.8,1.01)]
|
| 109 |
+
binned={}
|
| 110 |
+
for lo,hi in bins:
|
| 111 |
+
sel=(pred>=lo)&(pred<hi)
|
| 112 |
+
if sel.sum()>=5:
|
| 113 |
+
binned[f"{lo:.1f}-{hi:.1f}"]={"n":int(sel.sum()),"pred_r/m":round(float(pred[sel].mean()),3),
|
| 114 |
+
"obs_spanning":round(float(span_obs[sel].mean()),3),"obs_concentration":round(float(conc_obs[sel].mean()),3)}
|
| 115 |
+
res={"modality":"LIDC","budget":BUDGET,"n_lesions":len(recs),
|
| 116 |
+
"mean_m":round(float(recs[:,0].mean()),2),"mean_r":round(float(recs[:,1].mean()),2),
|
| 117 |
+
"mean_r_over_m":round(float(pred.mean()),3),
|
| 118 |
+
"law_prediction":"spanning_retention ~= r/m ; concentration ~= 1",
|
| 119 |
+
"spearman_pred_vs_observed_spanning":round(rho,3),
|
| 120 |
+
"fit_slope":round(slope,3),"fit_intercept":round(inter,3),
|
| 121 |
+
"mean_concentration":round(float(conc_obs.mean()),3),
|
| 122 |
+
"binned":binned,
|
| 123 |
+
"law_holds_on_real":bool(rho>=0.4 and 0.6<=slope<=1.4),
|
| 124 |
+
"interpretation":(f"Per real lesion, observed SPANNING retention tracks the predicted r/m "
|
| 125 |
+
f"(Spearman {rho:.2f}, slope {slope:.2f}), while concentration stays high ({conc_obs.mean():.2f}). "
|
| 126 |
+
f"Real lesions are low-rank (mean r/m {pred.mean():.2f}), so spanning loses to concentration by "
|
| 127 |
+
f"~{1-pred.mean():.2f} on average -- the S1 closed-form law gap=(m-r)/m, validated on REAL data."),
|
| 128 |
+
"elapsed_s":round(time.time()-t0,1)}
|
| 129 |
+
OUT.mkdir(parents=True,exist_ok=True); (OUT/"f2b_real_alignment.json").write_text(json.dumps(res,indent=2))
|
| 130 |
+
print("F2B_RESULT "+json.dumps(res),flush=True)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
if __name__=="__main__": main()
|
jobs/f3_cross_objective_job.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "transformers>=4.40", "huggingface_hub>=0.34",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""F3 (decisive) — does INVARIANCE pressure CAUSE the mid-layer localizability collapse?
|
| 9 |
+
|
| 10 |
+
Cross-OBJECTIVE comparison, all natural-image-trained (domain held constant), evaluated on LIDC:
|
| 11 |
+
- DINOv2 (self-distillation -> high view-invariance pressure)
|
| 12 |
+
- MAE (masked reconstruction -> low invariance pressure)
|
| 13 |
+
- ViT-sup (supervised -> task-discriminative, different pressure)
|
| 14 |
+
Per backbone, across all 12 blocks: density-A localizability AUROC + flip view-invariance.
|
| 15 |
+
Prediction (invariance mechanism): the AUROC-collapse-with-depth and the invariance-rise are
|
| 16 |
+
COUPLED for self-distillation (DINOv2), and MAE -- low invariance -- should NOT collapse the same
|
| 17 |
+
way. If the collapse tracks the objective's invariance, F3a's correlation becomes causal-by-
|
| 18 |
+
comparison. Masks rasterized per backbone patch-grid. Emits F3X_RESULT.
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import json, os, sys, time
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
import numpy as np, torch
|
| 25 |
+
from PIL import Image
|
| 26 |
+
from scipy import stats
|
| 27 |
+
from sklearn.neighbors import NearestNeighbors
|
| 28 |
+
|
| 29 |
+
MNT = Path("/mnt"); RAW_LIDC = MNT/"raw"/"lidc"; MASK_ROOT = MNT/"processed"/"lidc_v2"; OUT = MNT/"processed"/"covtoken"
|
| 30 |
+
BANK_SLICES = int(os.environ.get("BANK_SLICES","500")); EVAL_SLICES = int(os.environ.get("EVAL_SLICES","400"))
|
| 31 |
+
IMN_MEAN=np.array([0.485,0.456,0.406],np.float32); IMN_STD=np.array([0.229,0.224,0.225],np.float32)
|
| 32 |
+
# backbone -> (hf id, objective, n_special_tokens, patch_size)
|
| 33 |
+
BACKBONES = {
|
| 34 |
+
"DINOv2_selfdistill": ("facebook/dinov2-base", "self-distillation", 1, 14),
|
| 35 |
+
"MAE_reconstruction": ("facebook/vit-mae-base", "reconstruction", 1, 16),
|
| 36 |
+
"ViT_supervised": ("google/vit-base-patch16-224", "supervised", 1, 16),
|
| 37 |
+
}
|
| 38 |
+
def log(m): print(f"[f3x] {m}", flush=True)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def load_model(hf_id, device):
|
| 42 |
+
from transformers import AutoModel, ViTMAEModel
|
| 43 |
+
if "mae" in hf_id:
|
| 44 |
+
m = ViTMAEModel.from_pretrained(hf_id); m.config.mask_ratio = 0.0
|
| 45 |
+
else:
|
| 46 |
+
m = AutoModel.from_pretrained(hf_id)
|
| 47 |
+
return m.eval().to(device)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def to_img(pil):
|
| 51 |
+
arr=(np.asarray(pil.resize((224,224),Image.BILINEAR),np.float32)/255.0-IMN_MEAN)/IMN_STD
|
| 52 |
+
return torch.from_numpy(arr).permute(2,0,1)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@torch.inference_mode()
|
| 56 |
+
def hidden(model, imgs, device, n_special):
|
| 57 |
+
out = model(pixel_values=imgs.to(device,torch.float32), output_hidden_states=True)
|
| 58 |
+
hs = out.hidden_states # tuple, len = n_blocks+1
|
| 59 |
+
return [h[:, n_special:, :].float().cpu() for h in hs[1:]] # per-block patch tokens
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def patch_mask(mask_path, grid):
|
| 63 |
+
if not mask_path.exists(): return np.zeros(grid*grid, np.uint8)
|
| 64 |
+
m=np.asarray(Image.open(mask_path).convert("L").resize((224,224),Image.NEAREST))>0
|
| 65 |
+
ps=224//grid; g=m[:grid*ps,:grid*ps].reshape(grid,ps,grid,ps).sum(axis=(1,3))
|
| 66 |
+
return (g>0).astype(np.uint8).reshape(-1)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def auroc(s,y):
|
| 70 |
+
s=np.asarray(s,float); y=np.asarray(y,int); pos,neg=y.sum(),len(y)-y.sum()
|
| 71 |
+
if pos==0 or neg==0: return float("nan")
|
| 72 |
+
r=stats.rankdata(s); return float((r[y==1].sum()-pos*(pos+1)/2)/(pos*neg))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def main():
|
| 76 |
+
t0=time.time(); device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 77 |
+
import json as _j
|
| 78 |
+
from huggingface_hub import hf_hub_download
|
| 79 |
+
scan_split=_j.load(open(hf_hub_download("Chucks90/eryon-data-pipelines","manifests/lidc/splits_v1.0.0.json",repo_type="dataset",token=os.environ.get("HF_TOKEN"))))["splits"]
|
| 80 |
+
rng=np.random.default_rng(0)
|
| 81 |
+
train=[]
|
| 82 |
+
for b in sorted(RAW_LIDC.glob("batch_*")):
|
| 83 |
+
for sd in b.iterdir():
|
| 84 |
+
if sd.is_dir() and scan_split.get(sd.name)=="train": train+=sorted(sd.glob("slice_*.png"))
|
| 85 |
+
train=[train[i] for i in rng.choice(len(train),min(BANK_SLICES,len(train)),replace=False)]
|
| 86 |
+
ev=[]
|
| 87 |
+
for cd in sorted((MASK_ROOT/"test").iterdir()):
|
| 88 |
+
npz=cd/"patch_masks.npz"
|
| 89 |
+
if cd.is_dir() and npz.exists():
|
| 90 |
+
n=len(np.load(npz)["patch_masks"])
|
| 91 |
+
for idx in range(n): ev.append((cd, idx))
|
| 92 |
+
ev=[ev[i] for i in rng.choice(len(ev),min(EVAL_SLICES,len(ev)),replace=False)]
|
| 93 |
+
log(f"device={device.type}; train={len(train)} eval={len(ev)}")
|
| 94 |
+
|
| 95 |
+
result={"modality":"LIDC","eval_domain_constant":"natural-image-trained backbones","backbones":{}}
|
| 96 |
+
for name,(hf_id,obj,nsp,ps) in BACKBONES.items():
|
| 97 |
+
grid=224//ps; n_patch=grid*grid
|
| 98 |
+
model=load_model(hf_id,device)
|
| 99 |
+
n_blocks=model.config.num_hidden_layers
|
| 100 |
+
log(f"{name}: {obj}, grid {grid}x{grid}={n_patch}, blocks {n_blocks}")
|
| 101 |
+
# per-layer banks
|
| 102 |
+
bankL=[[] for _ in range(n_blocks)]
|
| 103 |
+
for i in range(0,len(train),32):
|
| 104 |
+
ims=[Image.open(p).convert("RGB") for p in train[i:i+32]]
|
| 105 |
+
hs=hidden(model, torch.stack([to_img(im) for im in ims]), device, nsp)
|
| 106 |
+
for L in range(n_blocks): bankL[L].append(hs[L].reshape(-1,hs[L].shape[-1]))
|
| 107 |
+
nnL=[]
|
| 108 |
+
for L in range(n_blocks):
|
| 109 |
+
X=torch.cat(bankL[L],0).numpy(); X=X[rng.choice(len(X),min(40000,len(X)),replace=False)]
|
| 110 |
+
nnL.append(NearestNeighbors(n_neighbors=11).fit(X))
|
| 111 |
+
# eval: per-layer density scores + labels + flip invariance
|
| 112 |
+
scoresL=[[] for _ in range(n_blocks)]; labels=[]; invL=[0.0]*n_blocks; nb=0
|
| 113 |
+
flip_idx=(np.arange(n_patch).reshape(grid,grid)[:,::-1]).reshape(-1)
|
| 114 |
+
for i in range(0,len(ev),32):
|
| 115 |
+
chunk=ev[i:i+32]
|
| 116 |
+
ims=[Image.open(cd/f"slice_{idx:04d}.png").convert("RGB") for cd,idx in chunk]
|
| 117 |
+
hs=hidden(model, torch.stack([to_img(im) for im in ims]), device, nsp)
|
| 118 |
+
hsf=hidden(model, torch.stack([to_img(im.transpose(Image.FLIP_LEFT_RIGHT)) for im in ims]), device, nsp)
|
| 119 |
+
for L in range(n_blocks):
|
| 120 |
+
d,_=nnL[L].kneighbors(hs[L].reshape(-1,hs[L].shape[-1]).numpy())
|
| 121 |
+
scoresL[L].append(d[:,1:].mean(1).reshape(len(chunk),n_patch))
|
| 122 |
+
a=torch.nn.functional.normalize(hs[L],dim=2); bb=torch.nn.functional.normalize(hsf[L][:,flip_idx,:],dim=2)
|
| 123 |
+
invL[L]+=float((a*bb).sum(2).mean())*len(chunk)
|
| 124 |
+
labels.append(np.stack([patch_mask(cd/f"slice_{idx:04d}_mask.png",grid) for cd,idx in chunk]).reshape(-1))
|
| 125 |
+
nb+=len(chunk)
|
| 126 |
+
lab=np.concatenate(labels)
|
| 127 |
+
au=[round(auroc(np.concatenate(scoresL[L]).reshape(-1),lab),4) for L in range(n_blocks)]
|
| 128 |
+
inv=[round(invL[L]/nb,4) for L in range(n_blocks)]
|
| 129 |
+
rho_inv=float(stats.spearmanr(inv,au).statistic)
|
| 130 |
+
result["backbones"][name]={"objective":obj,"n_blocks":n_blocks,"auroc_by_block":au,
|
| 131 |
+
"flip_invariance_by_block":inv,"auroc_peak_block":int(np.nanargmax(au))+1,
|
| 132 |
+
"auroc_drop_peak_to_final":round(float(np.nanmax(au)-au[-1]),4),
|
| 133 |
+
"invariance_rise":round(inv[-1]-inv[0],4),"spearman_invariance_vs_auroc":round(rho_inv,3)}
|
| 134 |
+
log(f" {name}: peak block {int(np.nanargmax(au))+1}, drop_to_final {np.nanmax(au)-au[-1]:.3f}, "
|
| 135 |
+
f"inv_rise {inv[-1]-inv[0]:+.3f}, rho(inv,AUROC) {rho_inv:+.2f}")
|
| 136 |
+
del model, bankL, nnL; torch.cuda.empty_cache() if device.type=="cuda" else None
|
| 137 |
+
|
| 138 |
+
# the decisive comparison
|
| 139 |
+
b=result["backbones"]
|
| 140 |
+
coupled = {n: (b[n]["spearman_invariance_vs_auroc"], b[n]["auroc_drop_peak_to_final"], b[n]["invariance_rise"]) for n in b}
|
| 141 |
+
result["mechanism_test"]={
|
| 142 |
+
"hypothesis":"collapse magnitude and invariance-rise are largest for self-distillation (DINOv2), smallest for reconstruction (MAE)",
|
| 143 |
+
"by_backbone":coupled,
|
| 144 |
+
"self_distill_collapses_most":bool(b.get("DINOv2_selfdistill",{}).get("auroc_drop_peak_to_final",0) >= b.get("MAE_reconstruction",{}).get("auroc_drop_peak_to_final",1)),
|
| 145 |
+
}
|
| 146 |
+
result["elapsed_s"]=round(time.time()-t0,1)
|
| 147 |
+
OUT.mkdir(parents=True,exist_ok=True); (OUT/"f3_cross_objective.json").write_text(json.dumps(result,indent=2))
|
| 148 |
+
print("F3X_RESULT "+json.dumps(result),flush=True)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__=="__main__": main()
|
jobs/f3a_ib_depth_job.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""F3a — information-bottleneck per layer: WHY does localizability peak mid-layer?
|
| 9 |
+
|
| 10 |
+
Hypothesis: global self-distillation trades SPATIAL/local information for VIEW-INVARIANT/global
|
| 11 |
+
information with depth; lesion localizability tracks the spatial-information curve. Per block we
|
| 12 |
+
measure (label-free): I(layer; spatial) via a position probe (predict patch column-bucket from
|
| 13 |
+
the token), and view-invariance via mean cosine between a patch and its horizontally-FLIPPED
|
| 14 |
+
counterpart (rises with depth as features become invariant). We then correlate both with the
|
| 15 |
+
localizability AUROC curve from S2. PASS if I(spatial) tracks AUROC across depth (predicts the
|
| 16 |
+
peak layer) and view-invariance anti-tracks it. Emits F3A_RESULT.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import json, os, sys, time
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
import numpy as np, torch
|
| 23 |
+
from PIL import Image
|
| 24 |
+
from scipy import stats
|
| 25 |
+
from sklearn.linear_model import LogisticRegression
|
| 26 |
+
from sklearn.model_selection import cross_val_score
|
| 27 |
+
from huggingface_hub import hf_hub_download
|
| 28 |
+
|
| 29 |
+
sys.path.insert(0, "/mnt/processed/covtoken_code")
|
| 30 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 31 |
+
|
| 32 |
+
BACKBONE_REPO = "ricklisz123/MedDINOv3-ViTB-16-CT-3M"
|
| 33 |
+
MNT = Path("/mnt"); MASK_ROOT = MNT/"processed"/"lidc_v2"; OUT = MNT/"processed"/"covtoken"
|
| 34 |
+
LAYERS = list(range(12)); N_PATCH, CLS_OFF, GRID = 196, 5, 14
|
| 35 |
+
N_SLICES = int(os.environ.get("N_SLICES", "400"))
|
| 36 |
+
CT_MEAN=np.array([0.485,0.456,0.406],np.float32); CT_STD=np.array([0.229,0.224,0.225],np.float32)
|
| 37 |
+
# localizability AUROC curve from S2 (block 1..12)
|
| 38 |
+
S2_AUROC = [0.881,0.882,0.886,0.857,0.841,0.817,0.777,0.749,0.726,0.720,0.710,0.669]
|
| 39 |
+
def log(m): print(f"[f3a] {m}", flush=True)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def load_backbone(device):
|
| 43 |
+
ck=hf_hub_download(BACKBONE_REPO,"model.pth",token=os.environ.get("HF_TOKEN"))
|
| 44 |
+
m=vit_base(drop_path_rate=0.0,layerscale_init=1e-5,n_storage_tokens=4,qkv_bias=False,mask_k_bias=True)
|
| 45 |
+
raw=torch.load(ck,map_location="cpu"); sd=raw.get("teacher",raw)
|
| 46 |
+
sd={(k[9:] if k.startswith("backbone.") else k):v for k,v in sd.items()}
|
| 47 |
+
m.load_state_dict(sd,strict=False); m.eval().to(device)
|
| 48 |
+
for p in m.parameters(): p.requires_grad_(False)
|
| 49 |
+
feats={}
|
| 50 |
+
for i,blk in enumerate(m.blocks):
|
| 51 |
+
def mk(i):
|
| 52 |
+
def hook(_m,_i,out):
|
| 53 |
+
while isinstance(out,(list,tuple)): out=out[0]
|
| 54 |
+
feats[i]=out.detach()
|
| 55 |
+
return hook
|
| 56 |
+
blk.register_forward_hook(mk(i))
|
| 57 |
+
return m,feats
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def load_img(arr_img):
|
| 61 |
+
return torch.from_numpy(((np.asarray(arr_img,np.float32)/255.0-CT_MEAN)/CT_STD)).permute(2,0,1)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@torch.inference_mode()
|
| 65 |
+
def all_layers(model,feats,imgs,device):
|
| 66 |
+
model.forward_features(imgs.to(device,torch.float32))
|
| 67 |
+
return {L: feats[L][:,CLS_OFF:CLS_OFF+N_PATCH,:].float().cpu() for L in LAYERS}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def main():
|
| 71 |
+
t0=time.time(); device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 72 |
+
model,feats=load_backbone(device); rng=np.random.default_rng(0)
|
| 73 |
+
allp=[]
|
| 74 |
+
for cd in sorted((MASK_ROOT/"test").iterdir()):
|
| 75 |
+
if cd.is_dir():
|
| 76 |
+
allp+=sorted(cd.glob("slice_*.png"))
|
| 77 |
+
allp=[allp[i] for i in rng.choice(len(allp),min(N_SLICES,len(allp)),replace=False)]
|
| 78 |
+
log(f"device={device.type}; slices={len(allp)}")
|
| 79 |
+
|
| 80 |
+
col=np.tile(np.arange(GRID),GRID) # patch column index 0..13 (196,)
|
| 81 |
+
col_bucket=(col//4).astype(int) # 4 column buckets
|
| 82 |
+
Zall={L:[] for L in LAYERS}; Zflip={L:[] for L in LAYERS}; cols=[]
|
| 83 |
+
for i in range(0,len(allp),48):
|
| 84 |
+
chunk=allp[i:i+48]
|
| 85 |
+
ims=[Image.open(p).convert("RGB").resize((224,224),Image.BILINEAR) for p in chunk]
|
| 86 |
+
orig=all_layers(model,feats,torch.stack([load_img(im) for im in ims]),device)
|
| 87 |
+
flip=all_layers(model,feats,torch.stack([load_img(im.transpose(Image.FLIP_LEFT_RIGHT)) for im in ims]),device)
|
| 88 |
+
for L in LAYERS:
|
| 89 |
+
Zall[L].append(orig[L].reshape(-1,768)); Zflip[L].append(flip[L])
|
| 90 |
+
cols.append(np.tile(col_bucket,len(chunk)))
|
| 91 |
+
if (i//48)%4==0: log(f" {i}/{len(allp)} elapsed={time.time()-t0:.0f}s")
|
| 92 |
+
y=np.concatenate(cols)
|
| 93 |
+
|
| 94 |
+
by={}
|
| 95 |
+
for L in LAYERS:
|
| 96 |
+
X=torch.cat(Zall[L],0).numpy()
|
| 97 |
+
# I(spatial): 4-class column-bucket probe accuracy (CV)
|
| 98 |
+
sub=rng.choice(len(X),min(40000,len(X)),replace=False)
|
| 99 |
+
acc=float(np.mean(cross_val_score(LogisticRegression(max_iter=500,C=0.5),X[sub],y[sub],cv=3)))
|
| 100 |
+
# view-invariance: cosine(orig[i,j], flip[i, 13-j]) averaged
|
| 101 |
+
Zf=torch.cat(Zflip[L],0) # (n_slices,196,768)
|
| 102 |
+
Zo=torch.cat([z.reshape(-1,N_PATCH,768) for z in Zall[L]],0)
|
| 103 |
+
flip_idx=(np.arange(N_PATCH).reshape(GRID,GRID)[:,::-1]).reshape(-1)
|
| 104 |
+
a=torch.nn.functional.normalize(Zo,dim=2); b=torch.nn.functional.normalize(Zf[:,flip_idx,:],dim=2)
|
| 105 |
+
inv=float((a*b).sum(2).mean())
|
| 106 |
+
by[L]={"col_probe_acc":round(acc,4),"flip_invariance":round(inv,4),"auroc":S2_AUROC[L]}
|
| 107 |
+
log(f" block {L+1:2d}: spatial_acc {acc:.3f} flip_inv {inv:.3f} AUROC {S2_AUROC[L]:.3f}")
|
| 108 |
+
|
| 109 |
+
au=np.array(S2_AUROC)
|
| 110 |
+
spa=np.array([by[L]["col_probe_acc"] for L in LAYERS])
|
| 111 |
+
inv=np.array([by[L]["flip_invariance"] for L in LAYERS])
|
| 112 |
+
res={"backbone":"MedDINOv3","modality":"LIDC","by_layer":{str(L+1):by[L] for L in LAYERS},
|
| 113 |
+
"spearman_spatial_vs_auroc":round(float(stats.spearmanr(spa,au).statistic),3),
|
| 114 |
+
"spearman_invariance_vs_auroc":round(float(stats.spearmanr(inv,au).statistic),3),
|
| 115 |
+
"spatial_peak_block":int(np.argmax(spa))+1,"auroc_peak_block":int(np.argmax(au))+1,
|
| 116 |
+
"ib_hypothesis_supported":bool(stats.spearmanr(spa,au).statistic>0.5 and stats.spearmanr(inv,au).statistic<-0.3),
|
| 117 |
+
"elapsed_s":round(time.time()-t0,1)}
|
| 118 |
+
res["interpretation"]=(f"Spatial information (column-probe accuracy) correlates with localizability "
|
| 119 |
+
f"across depth (rho {res['spearman_spatial_vs_auroc']:+.2f}); view-invariance anti-correlates "
|
| 120 |
+
f"(rho {res['spearman_invariance_vs_auroc']:+.2f}). Lesion localizability tracks the LOCAL/spatial-"
|
| 121 |
+
"information curve and declines as features become view-invariant (global) with depth -- the "
|
| 122 |
+
"information-bottleneck mechanism behind the mid-layer peak."
|
| 123 |
+
if res["ib_hypothesis_supported"] else "IB hypothesis not clearly supported.")
|
| 124 |
+
OUT.mkdir(parents=True,exist_ok=True); (OUT/"f3a_ib_depth.json").write_text(json.dumps(res,indent=2))
|
| 125 |
+
print("F3A_RESULT "+json.dumps(res),flush=True)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
if __name__=="__main__": main()
|
jobs/rigor_s1_multiseed_job.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = ["torch", "numpy", "scipy"]
|
| 4 |
+
# ///
|
| 5 |
+
"""Rigor — multi-seed CI on the S1 spanning-vs-concentration law (synthetic). HF Job (CPU).
|
| 6 |
+
|
| 7 |
+
Re-runs the S1 crossover across many seeds and reports the gap(r) curve with 95% CIs and the
|
| 8 |
+
crossover r* distribution, so the headline law carries error bars. Emits RIGOR_S1_RESULT.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
import json, os, time
|
| 12 |
+
import numpy as np, torch
|
| 13 |
+
from scipy import stats
|
| 14 |
+
|
| 15 |
+
N_TOK=196; N_LES=8; BUDGET=0.25; DIM=256; SUB=64; AMP=15.0
|
| 16 |
+
RANKS=[1,2,3,4,6,8,12]; SEEDS=int(os.environ.get("SEEDS","40")); TRIALS=int(os.environ.get("TRIALS","60"))
|
| 17 |
+
def log(m): print(f"[rigor-s1] {m}", flush=True)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def fps(Zp,k,seed):
|
| 21 |
+
n=Zp.shape[0]; keep=[seed]; d2=((Zp-Zp[seed])**2).sum(1)
|
| 22 |
+
for _ in range(k-1):
|
| 23 |
+
j=int(torch.argmax(d2)); keep.append(j); d2=torch.minimum(d2,((Zp-Zp[j])**2).sum(1)); d2[keep]=-1
|
| 24 |
+
m=np.zeros(n,bool); m[keep]=True; return m
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def one_seed(seed):
|
| 28 |
+
g=torch.Generator().manual_seed(seed); k=max(1,int(round(BUDGET*N_TOK))); gaps={}
|
| 29 |
+
for r in RANKS:
|
| 30 |
+
ce,fe=[],[]
|
| 31 |
+
for _ in range(TRIALS):
|
| 32 |
+
bg=torch.randn(N_TOK-N_LES,DIM,generator=g); sig=0.3*torch.randn(N_LES,DIM,generator=g)
|
| 33 |
+
axes=torch.randperm(SUB,generator=g)[:r]; assign=torch.arange(N_LES)%r
|
| 34 |
+
sig[torch.arange(N_LES),axes[assign]]+=AMP
|
| 35 |
+
Z=torch.cat([bg,sig],0); les=torch.zeros(N_TOK,dtype=torch.bool); les[-N_LES:]=True
|
| 36 |
+
Zp=Z[:,:SUB]; e=Zp.pow(2).sum(1)
|
| 37 |
+
ek=torch.zeros(N_TOK,dtype=torch.bool); ek[torch.topk(e,k).indices]=True
|
| 38 |
+
ce.append(float((ek&les).sum())/N_LES)
|
| 39 |
+
fe.append(float((torch.as_tensor(fps(Zp,k,int(torch.argmax(e))))&les).sum())/N_LES)
|
| 40 |
+
gaps[r]=np.mean(ce)-np.mean(fe)
|
| 41 |
+
xr=next((r for r in RANKS if gaps[r]<=0.02), None)
|
| 42 |
+
return gaps, xr
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def main():
|
| 46 |
+
t0=time.time(); per_seed_gaps={r:[] for r in RANKS}; xs=[]
|
| 47 |
+
for s in range(SEEDS):
|
| 48 |
+
g,xr=one_seed(s)
|
| 49 |
+
for r in RANKS: per_seed_gaps[r].append(g[r])
|
| 50 |
+
xs.append(xr if xr is not None else max(RANKS))
|
| 51 |
+
if s%10==0: log(f" seed {s}/{SEEDS} elapsed={time.time()-t0:.0f}s")
|
| 52 |
+
res={"seeds":SEEDS,"trials_per_seed":TRIALS,"gap_by_rank":{}}
|
| 53 |
+
for r in RANKS:
|
| 54 |
+
a=np.array(per_seed_gaps[r]); lo,hi=np.quantile(a,[0.025,0.975])
|
| 55 |
+
res["gap_by_rank"][str(r)]={"mean":round(float(a.mean()),4),"ci95":[round(float(lo),4),round(float(hi),4)],"std":round(float(a.std()),4)}
|
| 56 |
+
xs=np.array(xs);
|
| 57 |
+
res["crossover_r_star"]={"mean":round(float(xs.mean()),2),"mode":int(stats.mode(xs,keepdims=False).mode),"ci95":[float(np.quantile(xs,0.025)),float(np.quantile(xs,0.975))]}
|
| 58 |
+
res["law"]="gap(r=1) and crossover r*=m=8 with CIs across seeds"
|
| 59 |
+
res["elapsed_s"]=round(time.time()-t0,1)
|
| 60 |
+
print("RIGOR_S1_RESULT "+json.dumps(res),flush=True)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__=="__main__": main()
|
jobs/rigor_s2_multiseed_job.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""Rigor — multi-seed CIs on the S2 depth-localizability curve + label-free selector. HF Job (GPU).
|
| 9 |
+
|
| 10 |
+
Re-runs the per-layer density-AUROC and the tail-gap/bimodality selectors across SEEDS (different
|
| 11 |
+
bank+eval samples) so the depth curve carries error bars and the selector's regret has a CI.
|
| 12 |
+
Emits RIGOR_S2_RESULT.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
import json, os, sys, time
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
import numpy as np, torch
|
| 18 |
+
from PIL import Image
|
| 19 |
+
from scipy import stats
|
| 20 |
+
from sklearn.neighbors import NearestNeighbors
|
| 21 |
+
from huggingface_hub import hf_hub_download
|
| 22 |
+
sys.path.insert(0,"/mnt/processed/covtoken_code")
|
| 23 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 24 |
+
|
| 25 |
+
BACKBONE_REPO="ricklisz123/MedDINOv3-ViTB-16-CT-3M"; MNT=Path("/mnt")
|
| 26 |
+
RAW_LIDC=MNT/"raw"/"lidc"; MASK_ROOT=MNT/"processed"/"lidc_v2"; OUT=MNT/"processed"/"covtoken"
|
| 27 |
+
LAYERS=list(range(12)); N_PATCH,CLS_OFF=196,5
|
| 28 |
+
SEEDS=int(os.environ.get("SEEDS","3")); BANK_SLICES=int(os.environ.get("BANK_SLICES","600")); EVAL_SLICES=int(os.environ.get("EVAL_SLICES","600"))
|
| 29 |
+
CT_MEAN=np.array([0.485,0.456,0.406],np.float32); CT_STD=np.array([0.229,0.224,0.225],np.float32)
|
| 30 |
+
def log(m): print(f"[rigor-s2] {m}", flush=True)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def load_backbone(device):
|
| 34 |
+
ck=hf_hub_download(BACKBONE_REPO,"model.pth",token=os.environ.get("HF_TOKEN"))
|
| 35 |
+
m=vit_base(drop_path_rate=0.0,layerscale_init=1e-5,n_storage_tokens=4,qkv_bias=False,mask_k_bias=True)
|
| 36 |
+
raw=torch.load(ck,map_location="cpu"); sd=raw.get("teacher",raw)
|
| 37 |
+
sd={(k[9:] if k.startswith("backbone.") else k):v for k,v in sd.items()}
|
| 38 |
+
m.load_state_dict(sd,strict=False); m.eval().to(device)
|
| 39 |
+
for p in m.parameters(): p.requires_grad_(False)
|
| 40 |
+
feats={}
|
| 41 |
+
for i,blk in enumerate(m.blocks):
|
| 42 |
+
def mk(i):
|
| 43 |
+
def h(_m,_i,out):
|
| 44 |
+
while isinstance(out,(list,tuple)): out=out[0]
|
| 45 |
+
feats[i]=out.detach()
|
| 46 |
+
return h
|
| 47 |
+
blk.register_forward_hook(mk(i))
|
| 48 |
+
return m,feats
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def load_img(p):
|
| 52 |
+
img=Image.open(p).convert("RGB").resize((224,224),Image.BILINEAR)
|
| 53 |
+
return torch.from_numpy(((np.asarray(img,np.float32)/255.0-CT_MEAN)/CT_STD)).permute(2,0,1)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@torch.inference_mode()
|
| 57 |
+
def allL(model,feats,imgs,device):
|
| 58 |
+
model.forward_features(imgs.to(device,torch.float32))
|
| 59 |
+
return {L:feats[L][:,CLS_OFF:CLS_OFF+N_PATCH,:].float().cpu() for L in LAYERS}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def auroc(s,y):
|
| 63 |
+
s=np.asarray(s,float); y=np.asarray(y,int); pos,neg=y.sum(),len(y)-y.sum()
|
| 64 |
+
if pos==0 or neg==0: return float("nan")
|
| 65 |
+
r=stats.rankdata(s); return float((r[y==1].sum()-pos*(pos+1)/2)/(pos*neg))
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def proxies(s):
|
| 69 |
+
q01,q50,q99=np.quantile(s,[0.01,0.5,0.99]); tg=(q99-q50)/(q50-q01+1e-9)
|
| 70 |
+
sk=stats.skew(s); ku=stats.kurtosis(s); n=len(s)
|
| 71 |
+
bm=(sk**2+1)/(ku+3+3*((n-1)**2)/((n-2)*(n-3)+1e-9)); return tg, bm
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def run_seed(model,feats,device,seed,scan_split):
|
| 75 |
+
rng=np.random.default_rng(seed)
|
| 76 |
+
train=[]
|
| 77 |
+
for b in sorted(RAW_LIDC.glob("batch_*")):
|
| 78 |
+
for sd in b.iterdir():
|
| 79 |
+
if sd.is_dir() and scan_split.get(sd.name)=="train": train+=sorted(sd.glob("slice_*.png"))
|
| 80 |
+
train=[train[i] for i in rng.choice(len(train),min(BANK_SLICES,len(train)),replace=False)]
|
| 81 |
+
ev=[]
|
| 82 |
+
for cd in sorted((MASK_ROOT/"test").iterdir()):
|
| 83 |
+
npz=cd/"patch_masks.npz"
|
| 84 |
+
if cd.is_dir() and npz.exists():
|
| 85 |
+
pm=np.load(npz)["patch_masks"]
|
| 86 |
+
for idx in range(len(pm)): ev.append((cd/f"slice_{idx:04d}.png", pm[idx]))
|
| 87 |
+
ev=[ev[i] for i in rng.choice(len(ev),min(EVAL_SLICES,len(ev)),replace=False)]
|
| 88 |
+
bankL={L:[] for L in LAYERS}
|
| 89 |
+
for i in range(0,len(train),64):
|
| 90 |
+
pl=allL(model,feats,torch.stack([load_img(p) for p in train[i:i+64]]),device)
|
| 91 |
+
for L in LAYERS: bankL[L].append(pl[L].reshape(-1,768))
|
| 92 |
+
nnL={}
|
| 93 |
+
for L in LAYERS:
|
| 94 |
+
X=torch.cat(bankL[L],0).numpy(); X=X[rng.choice(len(X),min(50000,len(X)),replace=False)]
|
| 95 |
+
nnL[L]=NearestNeighbors(n_neighbors=11).fit(X)
|
| 96 |
+
sc={L:[] for L in LAYERS}; lab=[]
|
| 97 |
+
for i in range(0,len(ev),64):
|
| 98 |
+
chunk=ev[i:i+64]; pl=allL(model,feats,torch.stack([load_img(p) for p,_ in chunk]),device)
|
| 99 |
+
for L in LAYERS:
|
| 100 |
+
d,_=nnL[L].kneighbors(pl[L].reshape(-1,768).numpy()); sc[L].append(d[:,1:].mean(1).reshape(len(chunk),N_PATCH))
|
| 101 |
+
lab.append(np.stack([pm for _,pm in chunk]).reshape(-1))
|
| 102 |
+
lab=np.concatenate(lab)
|
| 103 |
+
au=[]; tg=[]; bm=[]
|
| 104 |
+
for L in LAYERS:
|
| 105 |
+
s=np.concatenate(sc[L]).reshape(-1); au.append(auroc(s,lab)); a,b=proxies(s); tg.append(a); bm.append(b)
|
| 106 |
+
return np.array(au),np.array(tg),np.array(bm)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def main():
|
| 110 |
+
t0=time.time(); device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 111 |
+
model,feats=load_backbone(device)
|
| 112 |
+
scan_split=json.load(open(hf_hub_download("Chucks90/eryon-data-pipelines","manifests/lidc/splits_v1.0.0.json",repo_type="dataset",token=os.environ.get("HF_TOKEN"))))["splits"]
|
| 113 |
+
AU=[]; regret_tg=[]; regret_bm=[]
|
| 114 |
+
for s in range(SEEDS):
|
| 115 |
+
au,tg,bm=run_seed(model,feats,device,s,scan_split); AU.append(au)
|
| 116 |
+
oracle=np.nanmax(au)
|
| 117 |
+
regret_tg.append(oracle-au[int(np.nanargmax(tg))]); regret_bm.append(oracle-au[int(np.nanargmax(bm))])
|
| 118 |
+
log(f" seed {s}: oracle {oracle:.3f}@blk{int(np.nanargmax(au))+1}; tg-regret {regret_tg[-1]:.3f}; bm-regret {regret_bm[-1]:.3f}; elapsed={time.time()-t0:.0f}s")
|
| 119 |
+
AU=np.array(AU)
|
| 120 |
+
res={"seeds":SEEDS,"auroc_mean_by_block":[round(float(x),4) for x in AU.mean(0)],
|
| 121 |
+
"auroc_std_by_block":[round(float(x),4) for x in AU.std(0)],
|
| 122 |
+
"tail_gap_selector_regret":{"mean":round(float(np.mean(regret_tg)),4),"max":round(float(np.max(regret_tg)),4)},
|
| 123 |
+
"bimodality_selector_regret":{"mean":round(float(np.mean(regret_bm)),4),"max":round(float(np.max(regret_bm)),4)},
|
| 124 |
+
"elapsed_s":round(time.time()-t0,1)}
|
| 125 |
+
OUT.mkdir(parents=True,exist_ok=True); (OUT/"rigor_s2_multiseed.json").write_text(json.dumps(res,indent=2))
|
| 126 |
+
print("RIGOR_S2_RESULT "+json.dumps(res),flush=True)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
if __name__=="__main__": main()
|
jobs/s1_rank_crossover_job.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = ["torch", "numpy", "scikit-learn", "huggingface_hub>=0.34"]
|
| 4 |
+
# ///
|
| 5 |
+
"""S1b — spanning-vs-concentration CROSSOVER (controlled synthetic). HF Job (CPU).
|
| 6 |
+
|
| 7 |
+
Tests the central theory from the covtoken negative result: a SPANNING objective (maximize the
|
| 8 |
+
effective rank / diversity of the retained set in the lesion subspace) vs a CONCENTRATION
|
| 9 |
+
objective (retain highest-energy / membership tokens), as a function of the signal's effective
|
| 10 |
+
rank r.
|
| 11 |
+
|
| 12 |
+
Clean controlled setup (isotropic background so energy cleanly separates the injected signal):
|
| 13 |
+
- Background: N_TOK - N_LES tokens ~ N(0, I_d) (isotropic; per-token lesion-subspace energy ~ chi2).
|
| 14 |
+
- Lesion subspace L = the first SUB_RANK coordinates; P_L is projection onto them.
|
| 15 |
+
- Signal: N_LES tokens whose L-projections span exactly r orthonormal directions of L (r=1 =>
|
| 16 |
+
aligned/concentrated; r large => diverse/high-rank), each with energy AMP^2 >> background.
|
| 17 |
+
We KNOW which tokens are signal. At a token budget we select via:
|
| 18 |
+
- CONCENTRATION: top-k by lesion-subspace energy ||P_L z||^2.
|
| 19 |
+
- SPANNING: farthest-point sampling in P_L space (a fast, faithful proxy for maximizing
|
| 20 |
+
effective-rank coverage / diversity), seeded at the max-energy token.
|
| 21 |
+
Metric: signal-token RETENTION. Prediction: concentration >> spanning at low r; the gap shrinks
|
| 22 |
+
monotonically and CROSSES at r* where spanning >= concentration. Locates r*. Emits S1_RESULT.
|
| 23 |
+
"""
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import json
|
| 27 |
+
import os
|
| 28 |
+
import time
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
import torch
|
| 32 |
+
|
| 33 |
+
N_TOK = 196
|
| 34 |
+
N_LES = int(os.environ.get("N_LES", "8")) # injected signal tokens
|
| 35 |
+
BUDGET = float(os.environ.get("BUDGET", "0.25"))
|
| 36 |
+
RANKS = [int(x) for x in os.environ.get("RANKS", "1,2,3,4,6,8,12,16,24,32").split(",")]
|
| 37 |
+
TRIALS = int(os.environ.get("TRIALS", "200"))
|
| 38 |
+
SUB_RANK = int(os.environ.get("SUB_RANK", "64")) # lesion-subspace dimensionality
|
| 39 |
+
DIM = int(os.environ.get("DIM", "256")) # ambient dim
|
| 40 |
+
AMP = float(os.environ.get("AMP", "15.0")) # signal amplitude (AMP^2 >> SUB_RANK background)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def log(m): print(f"[s1] {m}", flush=True)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def project_energy(Zp):
|
| 47 |
+
"""||P_L z||^2 per token (Zp already projected to L)."""
|
| 48 |
+
return Zp.pow(2).sum(1)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def farthest_point(Zp, k, seed_idx):
|
| 52 |
+
"""FPS in projected space Zp (n, r); returns boolean keep mask of size n."""
|
| 53 |
+
n = Zp.shape[0]
|
| 54 |
+
keep = [seed_idx]
|
| 55 |
+
d2 = ((Zp - Zp[seed_idx]) ** 2).sum(1)
|
| 56 |
+
for _ in range(k - 1):
|
| 57 |
+
j = int(torch.argmax(d2))
|
| 58 |
+
keep.append(j)
|
| 59 |
+
d2 = torch.minimum(d2, ((Zp - Zp[j]) ** 2).sum(1))
|
| 60 |
+
d2[keep] = -1
|
| 61 |
+
m = torch.zeros(n, dtype=torch.bool); m[keep] = True
|
| 62 |
+
return m
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def main():
|
| 66 |
+
t0 = time.time()
|
| 67 |
+
k = max(1, int(round(BUDGET * N_TOK)))
|
| 68 |
+
g = torch.Generator().manual_seed(0)
|
| 69 |
+
log(f"isotropic synthetic: dim={DIM} sub_rank={SUB_RANK} amp={AMP} k={k}/{N_TOK} n_les={N_LES}")
|
| 70 |
+
|
| 71 |
+
out = {"budget": BUDGET, "k": k, "n_les": N_LES, "sub_rank": SUB_RANK, "dim": DIM, "amp": AMP,
|
| 72 |
+
"ranks": RANKS, "trials": TRIALS, "random_baseline": round(k / N_TOK, 3), "by_rank": {}}
|
| 73 |
+
for r in RANKS:
|
| 74 |
+
ret_energy, ret_fps = [], []
|
| 75 |
+
for _ in range(TRIALS):
|
| 76 |
+
# background: isotropic Gaussian; L = first SUB_RANK coords
|
| 77 |
+
bg = torch.randn(N_TOK - N_LES, DIM, generator=g)
|
| 78 |
+
# signal: N_LES tokens spanning exactly r of L's axes, high energy
|
| 79 |
+
sig = 0.3 * torch.randn(N_LES, DIM, generator=g)
|
| 80 |
+
axes = torch.randperm(SUB_RANK, generator=g)[:r]
|
| 81 |
+
assign = torch.arange(N_LES) % r
|
| 82 |
+
sig[torch.arange(N_LES), axes[assign]] += AMP
|
| 83 |
+
Z = torch.cat([bg, sig], 0) # (196, d)
|
| 84 |
+
les_mask = torch.zeros(N_TOK, dtype=torch.bool); les_mask[-N_LES:] = True
|
| 85 |
+
Zp = Z[:, :SUB_RANK] # P_L Z
|
| 86 |
+
e = project_energy(Zp)
|
| 87 |
+
# CONCENTRATION: top-k energy
|
| 88 |
+
ek = torch.zeros(N_TOK, dtype=torch.bool); ek[torch.topk(e, k).indices] = True
|
| 89 |
+
ret_energy.append(float((ek & les_mask).sum()) / N_LES)
|
| 90 |
+
# SPANNING: farthest-point in P_L space, seeded at max-energy token
|
| 91 |
+
fk = farthest_point(Zp, k, int(torch.argmax(e)))
|
| 92 |
+
ret_fps.append(float((fk & les_mask).sum()) / N_LES)
|
| 93 |
+
out["by_rank"][str(r)] = {"concentration_retention": float(np.mean(ret_energy)),
|
| 94 |
+
"spanning_retention": float(np.mean(ret_fps)),
|
| 95 |
+
"gap_conc_minus_span": float(np.mean(ret_energy) - np.mean(ret_fps))}
|
| 96 |
+
log(f" r={r:2d}: concentration={np.mean(ret_energy):.3f} spanning={np.mean(ret_fps):.3f} "
|
| 97 |
+
f"gap={np.mean(ret_energy)-np.mean(ret_fps):+.3f}")
|
| 98 |
+
|
| 99 |
+
# locate crossover r*: smallest r where spanning >= concentration
|
| 100 |
+
gaps = [(r, out["by_rank"][str(r)]["gap_conc_minus_span"]) for r in RANKS]
|
| 101 |
+
crossover = next((r for r, gp in gaps if gp <= 0), None)
|
| 102 |
+
# monotonic shrink check
|
| 103 |
+
g_vals = [gp for _, gp in gaps]
|
| 104 |
+
mono = all(g_vals[i] >= g_vals[i + 1] - 0.02 for i in range(len(g_vals) - 1))
|
| 105 |
+
out["crossover_r_star"] = crossover
|
| 106 |
+
out["gap_monotone_shrinking"] = bool(mono)
|
| 107 |
+
out["theory_supported"] = bool(mono and (crossover is not None) and g_vals[0] > 0.1)
|
| 108 |
+
out["interpretation"] = (
|
| 109 |
+
f"Concentration beats spanning by {g_vals[0]:.2f} at r=1; gap shrinks monotonically; "
|
| 110 |
+
f"crossover r*={crossover}. Confirms: rank/spanning objectives are mismatched to LOW-rank "
|
| 111 |
+
f"(rare) signal and only help once signal rank exceeds r*."
|
| 112 |
+
if out["theory_supported"] else
|
| 113 |
+
"No clean crossover / non-monotone: theory not supported in this regime.")
|
| 114 |
+
out["elapsed_s"] = round(time.time() - t0, 1)
|
| 115 |
+
print("S1_RESULT " + json.dumps(out), flush=True)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
if __name__ == "__main__":
|
| 119 |
+
main()
|
jobs/s1a_real_crossover_job.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""S1a — spanning-vs-concentration crossover on REAL lesions, stratified by lesion rank. HF Job.
|
| 9 |
+
|
| 10 |
+
The synthetic law (S1b) predicts: the CONCENTRATION-minus-SPANNING lesion-retention gap shrinks
|
| 11 |
+
monotonically as lesion size/rank grows. We test it on real LIDC lesion slices, stratified by
|
| 12 |
+
#lesion-patches in {1, 2-3, 4-8, >8} (a natural rank proxy). At a token budget we measure
|
| 13 |
+
lesion-patch recall under:
|
| 14 |
+
- CONCENTRATION : top-k by block-3 density-A membership.
|
| 15 |
+
- SPANNING : farthest-point sampling in the lesion-subspace P_L (diversity / effective-rank
|
| 16 |
+
proxy -- the objective the covtoken floor optimizes).
|
| 17 |
+
- SALIENCY : top-k attention (reference).
|
| 18 |
+
Prediction: gap(concentration - spanning) is largest for 1-patch lesions and shrinks toward 0 for
|
| 19 |
+
large lesions. Emits S1A_RESULT <json>.
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import json
|
| 24 |
+
import os
|
| 25 |
+
import sys
|
| 26 |
+
import time
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import torch
|
| 31 |
+
import torch.nn.functional as F
|
| 32 |
+
from PIL import Image
|
| 33 |
+
from huggingface_hub import hf_hub_download
|
| 34 |
+
|
| 35 |
+
sys.path.insert(0, "/mnt/processed/covtoken_code")
|
| 36 |
+
from subspace.construction_a import DensitySubspace # noqa: E402
|
| 37 |
+
from eval.stats import paired_bootstrap_diff # noqa: E402
|
| 38 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 39 |
+
|
| 40 |
+
BACKBONE_REPO = "ricklisz123/MedDINOv3-ViTB-16-CT-3M"
|
| 41 |
+
MNT = Path("/mnt")
|
| 42 |
+
LAYER = int(os.environ.get("LAYER", "2"))
|
| 43 |
+
BANK = MNT / "processed" / "covtoken" / f"ct_token_bank_block{LAYER}.pt"
|
| 44 |
+
MASK_ROOT = MNT / os.environ.get("MASK_ROOT", "processed/lidc_v2")
|
| 45 |
+
OUT = MNT / "processed" / "covtoken"
|
| 46 |
+
EVAL_SPLIT = os.environ.get("EVAL_SPLIT", "test")
|
| 47 |
+
BUDGET = float(os.environ.get("BUDGET", "0.25"))
|
| 48 |
+
N_PATCH, CLS_OFF = 196, 5
|
| 49 |
+
BUCKETS = [("1", 1, 1), ("2-3", 2, 3), ("4-8", 4, 8), (">8", 9, 999)]
|
| 50 |
+
CT_MEAN = np.array([0.485, 0.456, 0.406], np.float32)
|
| 51 |
+
CT_STD = np.array([0.229, 0.224, 0.225], np.float32)
|
| 52 |
+
_FEAT, _ATTN = {}, {}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def log(m): print(f"[s1a] {m}", flush=True)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
_ORIG_SDPA = F.scaled_dot_product_attention
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _sdpa_wrap(q, k, v, *a, **kw):
|
| 62 |
+
try:
|
| 63 |
+
_ATTN["last"] = torch.softmax((q.float() @ k.float().transpose(-1, -2))
|
| 64 |
+
/ (q.shape[-1] ** 0.5), dim=-1).detach()
|
| 65 |
+
except Exception:
|
| 66 |
+
pass
|
| 67 |
+
return _ORIG_SDPA(q, k, v, *a, **kw)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
F.scaled_dot_product_attention = _sdpa_wrap # patch ONCE (per-image loop => no re-wrapping)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def load_backbone(device):
|
| 74 |
+
ck = hf_hub_download(BACKBONE_REPO, "model.pth", token=os.environ.get("HF_TOKEN"))
|
| 75 |
+
m = vit_base(drop_path_rate=0.0, layerscale_init=1e-5, n_storage_tokens=4,
|
| 76 |
+
qkv_bias=False, mask_k_bias=True)
|
| 77 |
+
raw = torch.load(ck, map_location="cpu"); sd = raw.get("teacher", raw)
|
| 78 |
+
sd = {(k[9:] if k.startswith("backbone.") else k): v for k, v in sd.items()}
|
| 79 |
+
m.load_state_dict(sd, strict=False); m.eval().to(device)
|
| 80 |
+
for p in m.parameters():
|
| 81 |
+
p.requires_grad_(False)
|
| 82 |
+
def hook(_mod, _in, out):
|
| 83 |
+
while isinstance(out, (list, tuple)):
|
| 84 |
+
out = out[0]
|
| 85 |
+
_FEAT["z"] = out.detach()
|
| 86 |
+
m.blocks[LAYER].register_forward_hook(hook)
|
| 87 |
+
return m
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def load_img(path):
|
| 91 |
+
img = Image.open(path).convert("RGB").resize((224, 224), Image.BILINEAR)
|
| 92 |
+
arr = (np.asarray(img, np.float32) / 255.0 - CT_MEAN) / CT_STD
|
| 93 |
+
return torch.from_numpy(arr).permute(2, 0, 1)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@torch.inference_mode()
|
| 97 |
+
def extract(model, img, device):
|
| 98 |
+
model.forward_features(img[None].to(device, torch.float32))
|
| 99 |
+
Z = _FEAT["z"][0, CLS_OFF:CLS_OFF + N_PATCH, :].float()
|
| 100 |
+
w = _ATTN.get("last")
|
| 101 |
+
sal = w[0, :, 0, CLS_OFF:CLS_OFF + N_PATCH].mean(0).float().cpu().numpy() if w is not None else np.random.rand(N_PATCH)
|
| 102 |
+
return Z, sal
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def farthest_point(Zp, k, seed):
|
| 106 |
+
n = Zp.shape[0]; keep = [seed]
|
| 107 |
+
d2 = ((Zp - Zp[seed]) ** 2).sum(1)
|
| 108 |
+
for _ in range(k - 1):
|
| 109 |
+
j = int(torch.argmax(d2)); keep.append(j)
|
| 110 |
+
d2 = torch.minimum(d2, ((Zp - Zp[j]) ** 2).sum(1)); d2[keep] = -1
|
| 111 |
+
m = np.zeros(n, bool); m[keep] = True; return m
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def topk(s, k):
|
| 115 |
+
m = np.zeros(N_PATCH, bool); m[np.argsort(-s)[:k]] = True; return m
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def main():
|
| 119 |
+
t0 = time.time()
|
| 120 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 121 |
+
A = DensitySubspace(rank=64, k=10, alpha=0.1, reference_size=100_000).fit(
|
| 122 |
+
torch.load(BANK, map_location="cpu")["tokens"].float())
|
| 123 |
+
P_L = A.P_L_.to(device)
|
| 124 |
+
model = load_backbone(device)
|
| 125 |
+
k = max(1, int(round(BUDGET * N_PATCH)))
|
| 126 |
+
|
| 127 |
+
rows = []
|
| 128 |
+
for cd in sorted((MASK_ROOT / EVAL_SPLIT).iterdir()):
|
| 129 |
+
npz = cd / "patch_masks.npz"
|
| 130 |
+
if cd.is_dir() and npz.exists():
|
| 131 |
+
pm = np.load(npz)["patch_masks"]
|
| 132 |
+
for idx in range(len(pm)):
|
| 133 |
+
if pm[idx].sum() > 0:
|
| 134 |
+
rows.append((cd.name, idx, pm[idx]))
|
| 135 |
+
log(f"device={device.type}; lesion slices={len(rows)}; budget={BUDGET}")
|
| 136 |
+
|
| 137 |
+
strata = {b[0]: {"conc": [], "span": [], "sal": []} for b in BUCKETS}
|
| 138 |
+
for i, (cid, idx, pm) in enumerate(rows):
|
| 139 |
+
ip = MASK_ROOT / EVAL_SPLIT / cid / f"slice_{idx:04d}.png"
|
| 140 |
+
if not ip.exists():
|
| 141 |
+
continue
|
| 142 |
+
Z, sal = extract(model, load_img(ip), device)
|
| 143 |
+
dens = A.membership_score_torch(Z, device=device).numpy()
|
| 144 |
+
Zp = (Z @ P_L.T) # lesion-subspace projection
|
| 145 |
+
pmb = pm.astype(bool); npos = int(pmb.sum())
|
| 146 |
+
bname = next(b[0] for b in BUCKETS if b[1] <= npos <= b[2])
|
| 147 |
+
strata[bname]["conc"].append((topk(dens, k) & pmb).sum() / npos)
|
| 148 |
+
strata[bname]["span"].append((farthest_point(Zp, k, int(np.argmax(dens))) & pmb).sum() / npos)
|
| 149 |
+
strata[bname]["sal"].append((topk(sal, k) & pmb).sum() / npos)
|
| 150 |
+
if i % 200 == 0:
|
| 151 |
+
log(f" {i}/{len(rows)} elapsed={time.time()-t0:.0f}s")
|
| 152 |
+
|
| 153 |
+
res = {"modality": "LIDC-IDRI", "layer": LAYER + 1, "budget": BUDGET, "strata": {}}
|
| 154 |
+
gaps = []
|
| 155 |
+
for b in BUCKETS:
|
| 156 |
+
s = strata[b[0]]
|
| 157 |
+
if len(s["conc"]) < 5:
|
| 158 |
+
res["strata"][b[0]] = {"n": len(s["conc"]), "note": "too few"}
|
| 159 |
+
continue
|
| 160 |
+
conc, span = np.array(s["conc"]), np.array(s["span"])
|
| 161 |
+
pb = paired_bootstrap_diff(conc, span, n=2000)
|
| 162 |
+
res["strata"][b[0]] = {"n": int(len(conc)), "concentration": float(conc.mean()),
|
| 163 |
+
"spanning": float(span.mean()), "saliency": float(np.mean(s["sal"])),
|
| 164 |
+
"gap_conc_minus_span": pb["diff"], "ci95": pb["ci95"],
|
| 165 |
+
"excludes_0": pb["excludes_0"]}
|
| 166 |
+
gaps.append((b[0], pb["diff"]))
|
| 167 |
+
log(f" {b[0]:>4}: n={len(conc)} conc={conc.mean():.3f} span={span.mean():.3f} gap={pb['diff']:+.3f}")
|
| 168 |
+
# monotone shrink across available strata
|
| 169 |
+
gv = [g for _, g in gaps]
|
| 170 |
+
res["gap_shrinks_with_rank"] = bool(len(gv) >= 2 and all(gv[i] >= gv[i+1] - 0.03 for i in range(len(gv)-1)))
|
| 171 |
+
res["theory_supported_on_real"] = bool(res["gap_shrinks_with_rank"] and gv and gv[0] > 0.1)
|
| 172 |
+
res["elapsed_s"] = round(time.time() - t0, 1)
|
| 173 |
+
OUT.mkdir(parents=True, exist_ok=True)
|
| 174 |
+
(OUT / "s1a_real_crossover.json").write_text(json.dumps(res, indent=2))
|
| 175 |
+
print("S1A_RESULT " + json.dumps(res), flush=True)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
main()
|
jobs/s2_depth_localizability_job.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""S2 — depth-resolved localizability + LABEL-FREE layer selector. HF Job (GPU).
|
| 9 |
+
|
| 10 |
+
Sweeps ALL 12 blocks. Per layer: (oracle) density-A token-level lesion AUROC vs held-out masks;
|
| 11 |
+
and (label-free) geometry proxies of the membership-score distribution computed with NO masks:
|
| 12 |
+
- tail_gap = (q99 - q50) / (q50 - q01 + eps) [how distinct the rare high-membership tokens are]
|
| 13 |
+
- kurtosis = excess kurtosis of membership scores [heavy tail => rare structure]
|
| 14 |
+
- bimodality= Sarle's bimodality coefficient
|
| 15 |
+
Question: does argmax(label-free proxy) over layers match the oracle AUROC-optimal layer?
|
| 16 |
+
Emits S2_RESULT <json>. Establishes WHERE localization lives and whether it is label-free
|
| 17 |
+
predictable (feeds the SPIE representation-coverage probe paper).
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import sys
|
| 24 |
+
import time
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
import torch
|
| 29 |
+
from PIL import Image
|
| 30 |
+
from scipy import stats
|
| 31 |
+
from sklearn.neighbors import NearestNeighbors
|
| 32 |
+
from huggingface_hub import hf_hub_download
|
| 33 |
+
|
| 34 |
+
sys.path.insert(0, "/mnt/processed/covtoken_code")
|
| 35 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 36 |
+
|
| 37 |
+
BACKBONE_REPO = "ricklisz123/MedDINOv3-ViTB-16-CT-3M"
|
| 38 |
+
MNT = Path("/mnt")
|
| 39 |
+
RAW_LIDC = MNT / "raw" / "lidc"
|
| 40 |
+
MASK_ROOT = MNT / "processed" / "lidc_v2"
|
| 41 |
+
OUT = MNT / "processed" / "covtoken"
|
| 42 |
+
LAYERS = list(range(12))
|
| 43 |
+
N_PATCH, CLS_OFF = 196, 5
|
| 44 |
+
BANK_SLICES = int(os.environ.get("BANK_SLICES", "800"))
|
| 45 |
+
EVAL_SLICES = int(os.environ.get("EVAL_SLICES", "700"))
|
| 46 |
+
CT_MEAN = np.array([0.485, 0.456, 0.406], np.float32)
|
| 47 |
+
CT_STD = np.array([0.229, 0.224, 0.225], np.float32)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def log(m): print(f"[s2] {m}", flush=True)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def load_backbone(device):
|
| 54 |
+
ck = hf_hub_download(BACKBONE_REPO, "model.pth", token=os.environ.get("HF_TOKEN"))
|
| 55 |
+
m = vit_base(drop_path_rate=0.0, layerscale_init=1e-5, n_storage_tokens=4,
|
| 56 |
+
qkv_bias=False, mask_k_bias=True)
|
| 57 |
+
raw = torch.load(ck, map_location="cpu"); sd = raw.get("teacher", raw)
|
| 58 |
+
sd = {(k[9:] if k.startswith("backbone.") else k): v for k, v in sd.items()}
|
| 59 |
+
m.load_state_dict(sd, strict=False); m.eval().to(device)
|
| 60 |
+
for p in m.parameters():
|
| 61 |
+
p.requires_grad_(False)
|
| 62 |
+
feats = {}
|
| 63 |
+
for i, blk in enumerate(m.blocks):
|
| 64 |
+
def mk(i):
|
| 65 |
+
def hook(_mod, _in, out):
|
| 66 |
+
while isinstance(out, (list, tuple)):
|
| 67 |
+
out = out[0]
|
| 68 |
+
feats[i] = out.detach()
|
| 69 |
+
return hook
|
| 70 |
+
blk.register_forward_hook(mk(i))
|
| 71 |
+
return m, feats
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def load_img(p):
|
| 75 |
+
img = Image.open(p).convert("RGB").resize((224, 224), Image.BILINEAR)
|
| 76 |
+
return torch.from_numpy(((np.asarray(img, np.float32)/255.0 - CT_MEAN)/CT_STD)).permute(2, 0, 1)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@torch.inference_mode()
|
| 80 |
+
def all_layers(model, feats, imgs, device):
|
| 81 |
+
model.forward_features(imgs.to(device, torch.float32))
|
| 82 |
+
return {L: feats[L][:, CLS_OFF:CLS_OFF+N_PATCH, :].float().cpu() for L in LAYERS}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def auroc(s, y):
|
| 86 |
+
s = np.asarray(s, float).ravel(); y = np.asarray(y, int).ravel()
|
| 87 |
+
pos, neg = y.sum(), len(y)-y.sum()
|
| 88 |
+
if pos == 0 or neg == 0: return float("nan")
|
| 89 |
+
r = stats.rankdata(s); return float((r[y == 1].sum()-pos*(pos+1)/2)/(pos*neg))
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def labelfree_proxies(scores):
|
| 93 |
+
s = np.asarray(scores, float)
|
| 94 |
+
q01, q50, q99 = np.quantile(s, [0.01, 0.5, 0.99])
|
| 95 |
+
tail_gap = (q99 - q50) / (q50 - q01 + 1e-9)
|
| 96 |
+
kurt = float(stats.kurtosis(s))
|
| 97 |
+
m3 = stats.skew(s); m4 = stats.kurtosis(s) + 3
|
| 98 |
+
bimod = (m3**2 + 1) / (m4 + 3*((len(s)-1)**2)/((len(s)-2)*(len(s)-3) + 1e-9))
|
| 99 |
+
return {"tail_gap": float(tail_gap), "kurtosis": kurt, "bimodality": float(bimod)}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
t0 = time.time()
|
| 104 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 105 |
+
model, feats = load_backbone(device)
|
| 106 |
+
spath = hf_hub_download("Chucks90/eryon-data-pipelines", "manifests/lidc/splits_v1.0.0.json",
|
| 107 |
+
repo_type="dataset", token=os.environ.get("HF_TOKEN"))
|
| 108 |
+
scan_split = json.load(open(spath))["splits"]
|
| 109 |
+
|
| 110 |
+
# train bank slices (label-free) + test lesion+neg slices (masks for oracle only)
|
| 111 |
+
train = []
|
| 112 |
+
for b in sorted(RAW_LIDC.glob("batch_*")):
|
| 113 |
+
for sd in b.iterdir():
|
| 114 |
+
if sd.is_dir() and scan_split.get(sd.name) == "train":
|
| 115 |
+
train += sorted(sd.glob("slice_*.png"))
|
| 116 |
+
rng = np.random.default_rng(0)
|
| 117 |
+
train = [train[i] for i in rng.choice(len(train), min(BANK_SLICES, len(train)), replace=False)]
|
| 118 |
+
|
| 119 |
+
ev = []
|
| 120 |
+
for cd in sorted((MASK_ROOT / "test").iterdir()):
|
| 121 |
+
npz = cd / "patch_masks.npz"
|
| 122 |
+
if cd.is_dir() and npz.exists():
|
| 123 |
+
pm = np.load(npz)["patch_masks"]
|
| 124 |
+
for idx in range(len(pm)):
|
| 125 |
+
ev.append((cd / f"slice_{idx:04d}.png", pm[idx]))
|
| 126 |
+
ev = [ev[i] for i in rng.choice(len(ev), min(EVAL_SLICES, len(ev)), replace=False)]
|
| 127 |
+
log(f"device={device.type}; bank={len(train)} eval={len(ev)}")
|
| 128 |
+
|
| 129 |
+
# per-layer banks
|
| 130 |
+
bankL = {L: [] for L in LAYERS}
|
| 131 |
+
for i in range(0, len(train), 64):
|
| 132 |
+
pl = all_layers(model, feats, torch.stack([load_img(p) for p in train[i:i+64]]), device)
|
| 133 |
+
for L in LAYERS:
|
| 134 |
+
bankL[L].append(pl[L].reshape(-1, 768))
|
| 135 |
+
bankL = {L: torch.cat(v, 0) for L, v in bankL.items()}
|
| 136 |
+
nnL = {}
|
| 137 |
+
for L in LAYERS:
|
| 138 |
+
X = bankL[L].numpy()
|
| 139 |
+
X = X[rng.choice(len(X), min(60000, len(X)), replace=False)]
|
| 140 |
+
nnL[L] = NearestNeighbors(n_neighbors=11).fit(X)
|
| 141 |
+
log("per-layer density banks fit")
|
| 142 |
+
|
| 143 |
+
# eval: per-layer membership scores + labels
|
| 144 |
+
scoresL = {L: [] for L in LAYERS}; labels = []
|
| 145 |
+
for i in range(0, len(ev), 64):
|
| 146 |
+
chunk = ev[i:i+64]
|
| 147 |
+
pl = all_layers(model, feats, torch.stack([load_img(p) for p, _ in chunk]), device)
|
| 148 |
+
for L in LAYERS:
|
| 149 |
+
d, _ = nnL[L].kneighbors(pl[L].reshape(-1, 768).numpy())
|
| 150 |
+
scoresL[L].append(d[:, 1:].mean(1).reshape(len(chunk), N_PATCH))
|
| 151 |
+
labels.append(np.stack([pm for _, pm in chunk]))
|
| 152 |
+
if (i // 64) % 5 == 0:
|
| 153 |
+
log(f" scored {i}/{len(ev)} elapsed={time.time()-t0:.0f}s")
|
| 154 |
+
lab = np.concatenate(labels).reshape(-1)
|
| 155 |
+
|
| 156 |
+
by_layer = {}
|
| 157 |
+
for L in LAYERS:
|
| 158 |
+
sc = np.concatenate(scoresL[L]).reshape(-1)
|
| 159 |
+
by_layer[L] = {"auroc": round(auroc(sc, lab), 4), **{k: round(v, 4) for k, v in labelfree_proxies(sc).items()}}
|
| 160 |
+
log(f" block {L+1:2d}: AUROC {by_layer[L]['auroc']:.3f} tail_gap {by_layer[L]['tail_gap']:.2f} kurt {by_layer[L]['kurtosis']:.2f}")
|
| 161 |
+
|
| 162 |
+
aurocs = np.array([by_layer[L]["auroc"] for L in LAYERS])
|
| 163 |
+
oracle_layer = int(LAYERS[int(np.nanargmax(aurocs))])
|
| 164 |
+
res = {"backbone": "MedDINOv3", "modality": "LIDC", "by_layer": {str(L+1): by_layer[L] for L in LAYERS},
|
| 165 |
+
"oracle_best_block": oracle_layer + 1, "oracle_best_auroc": float(np.nanmax(aurocs))}
|
| 166 |
+
for proxy in ("tail_gap", "kurtosis", "bimodality"):
|
| 167 |
+
pv = np.array([by_layer[L][proxy] for L in LAYERS])
|
| 168 |
+
sel = int(LAYERS[int(np.nanargmax(pv))]) + 1
|
| 169 |
+
res[f"selector_{proxy}"] = {"picked_block": sel,
|
| 170 |
+
"picked_auroc": float(by_layer[sel-1]["auroc"]),
|
| 171 |
+
"regret_vs_oracle": round(float(np.nanmax(aurocs) - by_layer[sel-1]["auroc"]), 4),
|
| 172 |
+
"spearman_with_auroc": round(float(stats.spearmanr(pv, aurocs).statistic), 3)}
|
| 173 |
+
log(f" selector[{proxy}] -> block {sel} (regret {res[f'selector_{proxy}']['regret_vs_oracle']:.3f}, "
|
| 174 |
+
f"rho {res[f'selector_{proxy}']['spearman_with_auroc']:.2f})")
|
| 175 |
+
res["elapsed_s"] = round(time.time() - t0, 1)
|
| 176 |
+
OUT.mkdir(parents=True, exist_ok=True)
|
| 177 |
+
(OUT / "s2_depth_localizability.json").write_text(json.dumps(res, indent=2))
|
| 178 |
+
print("S2_RESULT " + json.dumps(res), flush=True)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
if __name__ == "__main__":
|
| 182 |
+
main()
|
jobs/s3_precondition_predictor_job.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""S3 — predict the precondition LABEL-FREE: will the subspace localize lesions on a new dataset?
|
| 9 |
+
|
| 10 |
+
For each CT dataset we compute, with NO masks, a label-free geometry proxy of the density-
|
| 11 |
+
membership distribution over a sample of eval tokens (tail_gap, bimodality), and report it next to
|
| 12 |
+
the KNOWN oracle density-AUROC. Question: does the label-free proxy rank datasets by AUROC and flag
|
| 13 |
+
the liver failure (0.67) as the lowest, without any annotation? If so it is a deployment safety
|
| 14 |
+
trigger: run the subspace where the proxy clears a threshold, else fall back to attention.
|
| 15 |
+
Emits S3_RESULT <json>.
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import json
|
| 20 |
+
import os
|
| 21 |
+
import sys
|
| 22 |
+
import time
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
|
| 25 |
+
import numpy as np
|
| 26 |
+
import torch
|
| 27 |
+
from PIL import Image
|
| 28 |
+
from scipy import stats
|
| 29 |
+
from huggingface_hub import hf_hub_download
|
| 30 |
+
|
| 31 |
+
sys.path.insert(0, "/mnt/processed/covtoken_code")
|
| 32 |
+
from subspace.construction_a import DensitySubspace # noqa: E402
|
| 33 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 34 |
+
|
| 35 |
+
BACKBONE_REPO = "ricklisz123/MedDINOv3-ViTB-16-CT-3M"
|
| 36 |
+
MNT = Path("/mnt")
|
| 37 |
+
LAYER = 2
|
| 38 |
+
OUT = MNT / "processed" / "covtoken"
|
| 39 |
+
N_PATCH, CLS_OFF = 196, 5
|
| 40 |
+
N_EVAL = int(os.environ.get("N_EVAL", "300"))
|
| 41 |
+
CT_MEAN = np.array([0.485, 0.456, 0.406], np.float32)
|
| 42 |
+
CT_STD = np.array([0.229, 0.224, 0.225], np.float32)
|
| 43 |
+
_FEAT = {}
|
| 44 |
+
# dataset -> (bank file, mask-root tree, KNOWN oracle density-AUROC from Gate 1)
|
| 45 |
+
DATASETS = {
|
| 46 |
+
"lung_LIDC": ("ct_token_bank_block2.pt", "processed/lidc_v2", 0.871),
|
| 47 |
+
"pancreas_MSD": ("pancreas_token_bank_block2.pt","processed/msd_pancreas_v2", 0.876),
|
| 48 |
+
"kidney_KiTS": ("kits_token_bank_block2.pt", "processed/kits_v2", 0.823),
|
| 49 |
+
"liver_MSD": ("liver_token_bank_block2.pt", "processed/msd_liver_v2", 0.670),
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def log(m): print(f"[s3] {m}", flush=True)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def load_backbone(device):
|
| 57 |
+
ck = hf_hub_download(BACKBONE_REPO, "model.pth", token=os.environ.get("HF_TOKEN"))
|
| 58 |
+
m = vit_base(drop_path_rate=0.0, layerscale_init=1e-5, n_storage_tokens=4,
|
| 59 |
+
qkv_bias=False, mask_k_bias=True)
|
| 60 |
+
raw = torch.load(ck, map_location="cpu"); sd = raw.get("teacher", raw)
|
| 61 |
+
sd = {(k[9:] if k.startswith("backbone.") else k): v for k, v in sd.items()}
|
| 62 |
+
m.load_state_dict(sd, strict=False); m.eval().to(device)
|
| 63 |
+
for p in m.parameters():
|
| 64 |
+
p.requires_grad_(False)
|
| 65 |
+
def hook(_mod, _in, out):
|
| 66 |
+
while isinstance(out, (list, tuple)):
|
| 67 |
+
out = out[0]
|
| 68 |
+
_FEAT["z"] = out.detach()
|
| 69 |
+
m.blocks[LAYER].register_forward_hook(hook)
|
| 70 |
+
return m
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def load_img(p):
|
| 74 |
+
img = Image.open(p).convert("RGB").resize((224, 224), Image.BILINEAR)
|
| 75 |
+
return torch.from_numpy(((np.asarray(img, np.float32)/255.0 - CT_MEAN)/CT_STD)).permute(2, 0, 1)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@torch.inference_mode()
|
| 79 |
+
def tokens(model, imgs, device):
|
| 80 |
+
model.forward_features(imgs.to(device, torch.float32))
|
| 81 |
+
return _FEAT["z"][:, CLS_OFF:CLS_OFF+N_PATCH, :].float()
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def proxies(scores):
|
| 85 |
+
s = np.asarray(scores, float)
|
| 86 |
+
q01, q50, q99 = np.quantile(s, [0.01, 0.5, 0.99])
|
| 87 |
+
tail_gap = float((q99 - q50) / (q50 - q01 + 1e-9))
|
| 88 |
+
sk = stats.skew(s); ku = stats.kurtosis(s)
|
| 89 |
+
n = len(s)
|
| 90 |
+
bimod = float((sk**2 + 1) / (ku + 3 + 3*((n-1)**2)/((n-2)*(n-3) + 1e-9)))
|
| 91 |
+
return {"tail_gap": tail_gap, "bimodality": bimod}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def main():
|
| 95 |
+
t0 = time.time()
|
| 96 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 97 |
+
model = load_backbone(device)
|
| 98 |
+
rng = np.random.default_rng(0)
|
| 99 |
+
res = {"layer": LAYER + 1, "n_eval": N_EVAL, "datasets": {}}
|
| 100 |
+
|
| 101 |
+
for name, (bankf, root, auroc) in DATASETS.items():
|
| 102 |
+
bank = torch.load(MNT / "processed" / "covtoken" / bankf, map_location="cpu")["tokens"].float()
|
| 103 |
+
A = DensitySubspace(rank=64, k=10, alpha=0.1, reference_size=100_000).fit(bank)
|
| 104 |
+
# sample eval slices (any split; label-free => masks not needed)
|
| 105 |
+
sl = []
|
| 106 |
+
for split in ("test", "val"):
|
| 107 |
+
d = MNT / root / split
|
| 108 |
+
if d.exists():
|
| 109 |
+
for cd in sorted(d.iterdir()):
|
| 110 |
+
if cd.is_dir():
|
| 111 |
+
sl += sorted(cd.glob("slice_*.png"))
|
| 112 |
+
sl = [sl[i] for i in rng.choice(len(sl), min(N_EVAL, len(sl)), replace=False)] if sl else []
|
| 113 |
+
scores = []
|
| 114 |
+
for i in range(0, len(sl), 64):
|
| 115 |
+
Z = tokens(model, torch.stack([load_img(p) for p in sl[i:i+64]]), device)
|
| 116 |
+
scores.append(A.membership_score_torch(Z.reshape(-1, 768), device=device).numpy())
|
| 117 |
+
sc = np.concatenate(scores) if scores else np.zeros(1)
|
| 118 |
+
px = proxies(sc)
|
| 119 |
+
res["datasets"][name] = {"oracle_density_auroc": auroc, "n_slices": len(sl), **px}
|
| 120 |
+
log(f" {name:14s} AUROC {auroc:.3f} tail_gap {px['tail_gap']:.3f} bimodality {px['bimodality']:.3f}")
|
| 121 |
+
|
| 122 |
+
names = list(res["datasets"])
|
| 123 |
+
au = np.array([res["datasets"][n]["oracle_density_auroc"] for n in names])
|
| 124 |
+
for proxy in ("tail_gap", "bimodality"):
|
| 125 |
+
pv = np.array([res["datasets"][n][proxy] for n in names])
|
| 126 |
+
rho = float(stats.spearmanr(pv, au).statistic)
|
| 127 |
+
lowest_proxy = names[int(np.argmin(pv))]
|
| 128 |
+
res[f"{proxy}_spearman_with_auroc"] = round(rho, 3)
|
| 129 |
+
res[f"{proxy}_flags_liver_lowest"] = bool(lowest_proxy == "liver_MSD")
|
| 130 |
+
log(f" proxy[{proxy}] rho_with_AUROC={rho:+.2f} lowest-proxy dataset={lowest_proxy}")
|
| 131 |
+
res["predicts_precondition"] = bool(
|
| 132 |
+
abs(res["tail_gap_spearman_with_auroc"]) >= 0.8 and res["tail_gap_flags_liver_lowest"]) or \
|
| 133 |
+
bool(abs(res["bimodality_spearman_with_auroc"]) >= 0.8 and res["bimodality_flags_liver_lowest"])
|
| 134 |
+
res["elapsed_s"] = round(time.time() - t0, 1)
|
| 135 |
+
OUT.mkdir(parents=True, exist_ok=True)
|
| 136 |
+
(OUT / "s3_precondition.json").write_text(json.dumps(res, indent=2))
|
| 137 |
+
print("S3_RESULT " + json.dumps(res), flush=True)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
if __name__ == "__main__":
|
| 141 |
+
main()
|
jobs/s4_detection_bootstrap_job.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""S4 — label-free detection bootstrap. Can the geometric membership map train a detector with
|
| 9 |
+
NO manual masks, and how close to supervised does it get?
|
| 10 |
+
|
| 11 |
+
- raw : density-A membership map (label-free, no training).
|
| 12 |
+
- self : a linear head trained on PSEUDO-labels (membership > q90 => pseudo-lesion); NO masks.
|
| 13 |
+
- supervised: a linear head trained on TRUE patch masks (oracle upper bound).
|
| 14 |
+
Eval token-level lesion AUROC on held-out test. PASS if self >= raw (self-training adds value)
|
| 15 |
+
and self reaches a stated fraction of supervised with zero manual masks. Emits S4_RESULT.
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import json, os, sys, time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
import numpy as np, torch
|
| 22 |
+
from PIL import Image
|
| 23 |
+
from scipy import stats
|
| 24 |
+
from sklearn.linear_model import LogisticRegression
|
| 25 |
+
from huggingface_hub import hf_hub_download
|
| 26 |
+
|
| 27 |
+
sys.path.insert(0, "/mnt/processed/covtoken_code")
|
| 28 |
+
from subspace.construction_a import DensitySubspace # noqa: E402
|
| 29 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 30 |
+
|
| 31 |
+
BACKBONE_REPO = "ricklisz123/MedDINOv3-ViTB-16-CT-3M"
|
| 32 |
+
MNT = Path("/mnt"); LAYER = 2
|
| 33 |
+
BANK = MNT/"processed"/"covtoken"/"ct_token_bank_block2.pt"
|
| 34 |
+
MASK_ROOT = MNT/"processed"/"lidc_v2"; OUT = MNT/"processed"/"covtoken"
|
| 35 |
+
N_PATCH, CLS_OFF = 196, 5
|
| 36 |
+
TRAIN_SLICES = int(os.environ.get("TRAIN_SLICES", "1500"))
|
| 37 |
+
EVAL_SLICES = int(os.environ.get("EVAL_SLICES", "800"))
|
| 38 |
+
CT_MEAN = np.array([0.485,0.456,0.406],np.float32); CT_STD = np.array([0.229,0.224,0.225],np.float32)
|
| 39 |
+
_FEAT = {}
|
| 40 |
+
def log(m): print(f"[s4] {m}", flush=True)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def load_backbone(device):
|
| 44 |
+
ck = hf_hub_download(BACKBONE_REPO, "model.pth", token=os.environ.get("HF_TOKEN"))
|
| 45 |
+
m = vit_base(drop_path_rate=0.0, layerscale_init=1e-5, n_storage_tokens=4, qkv_bias=False, mask_k_bias=True)
|
| 46 |
+
raw = torch.load(ck, map_location="cpu"); sd = raw.get("teacher", raw)
|
| 47 |
+
sd = {(k[9:] if k.startswith("backbone.") else k): v for k,v in sd.items()}
|
| 48 |
+
m.load_state_dict(sd, strict=False); m.eval().to(device)
|
| 49 |
+
for p in m.parameters(): p.requires_grad_(False)
|
| 50 |
+
def hook(_m,_i,out):
|
| 51 |
+
while isinstance(out,(list,tuple)): out=out[0]
|
| 52 |
+
_FEAT["z"]=out.detach()
|
| 53 |
+
m.blocks[LAYER].register_forward_hook(hook); return m
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def load_img(p):
|
| 57 |
+
img=Image.open(p).convert("RGB").resize((224,224),Image.BILINEAR)
|
| 58 |
+
return torch.from_numpy(((np.asarray(img,np.float32)/255.0-CT_MEAN)/CT_STD)).permute(2,0,1)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@torch.inference_mode()
|
| 62 |
+
def toks(model,imgs,device):
|
| 63 |
+
model.forward_features(imgs.to(device,torch.float32))
|
| 64 |
+
return _FEAT["z"][:,CLS_OFF:CLS_OFF+N_PATCH,:].float().cpu()
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def auroc(s,y):
|
| 68 |
+
s=np.asarray(s,float); y=np.asarray(y,int)
|
| 69 |
+
pos,neg=y.sum(),len(y)-y.sum()
|
| 70 |
+
if pos==0 or neg==0: return float("nan")
|
| 71 |
+
r=stats.rankdata(s); return float((r[y==1].sum()-pos*(pos+1)/2)/(pos*neg))
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def slices(split, lesion_only=False):
|
| 75 |
+
out=[]
|
| 76 |
+
for cd in sorted((MASK_ROOT/split).iterdir()):
|
| 77 |
+
npz=cd/"patch_masks.npz"
|
| 78 |
+
if cd.is_dir() and npz.exists():
|
| 79 |
+
pm=np.load(npz)["patch_masks"]
|
| 80 |
+
for idx in range(len(pm)):
|
| 81 |
+
if (not lesion_only) or pm[idx].sum()>0:
|
| 82 |
+
out.append((cd/f"slice_{idx:04d}.png", pm[idx]))
|
| 83 |
+
return out
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def gather(model, A, rows, device):
|
| 87 |
+
X,memb,Y=[],[],[]
|
| 88 |
+
for i in range(0,len(rows),64):
|
| 89 |
+
chunk=rows[i:i+64]
|
| 90 |
+
Z=toks(model, torch.stack([load_img(p) for p,_ in chunk]), device)
|
| 91 |
+
Zf=Z.reshape(-1,768)
|
| 92 |
+
X.append(Zf.numpy()); memb.append(A.membership_score_torch(Zf,device=device).numpy())
|
| 93 |
+
Y.append(np.stack([pm for _,pm in chunk]).reshape(-1))
|
| 94 |
+
return np.concatenate(X), np.concatenate(memb), np.concatenate(Y)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def main():
|
| 98 |
+
t0=time.time(); device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 99 |
+
A=DensitySubspace(rank=64,k=10,alpha=0.1,reference_size=100_000).fit(torch.load(BANK,map_location="cpu")["tokens"].float())
|
| 100 |
+
model=load_backbone(device)
|
| 101 |
+
rng=np.random.default_rng(0)
|
| 102 |
+
tr=slices("val"); tr=[tr[i] for i in rng.choice(len(tr),min(TRAIN_SLICES,len(tr)),replace=False)]
|
| 103 |
+
te=slices("test"); te=[te[i] for i in rng.choice(len(te),min(EVAL_SLICES,len(te)),replace=False)]
|
| 104 |
+
log(f"device={device.type} train={len(tr)} test={len(te)}")
|
| 105 |
+
Xtr,mtr,Ytr=gather(model,A,tr,device)
|
| 106 |
+
Xte,mte,Yte=gather(model,A,te,device)
|
| 107 |
+
# pseudo-labels from membership q90 (NO masks)
|
| 108 |
+
thr=np.quantile(mtr,0.90); pseudo=(mtr>=thr).astype(int)
|
| 109 |
+
log(f"pseudo lesion rate {pseudo.mean():.3f}; true lesion rate {Ytr.mean():.3f}")
|
| 110 |
+
head_self=LogisticRegression(max_iter=2000,class_weight="balanced",C=1.0).fit(Xtr,pseudo)
|
| 111 |
+
head_sup =LogisticRegression(max_iter=2000,class_weight="balanced",C=1.0).fit(Xtr,Ytr)
|
| 112 |
+
a_raw=auroc(mte,Yte); a_self=auroc(head_self.decision_function(Xte),Yte); a_sup=auroc(head_sup.decision_function(Xte),Yte)
|
| 113 |
+
frac=(a_self-0.5)/(a_sup-0.5) if a_sup>0.5 else 0.0
|
| 114 |
+
res={"raw_membership_auroc":round(a_raw,4),"self_trained_auroc":round(a_self,4),
|
| 115 |
+
"supervised_auroc":round(a_sup,4),"self_minus_raw":round(a_self-a_raw,4),
|
| 116 |
+
"fraction_of_supervised":round(float(frac),3),
|
| 117 |
+
"self_beats_raw":bool(a_self>=a_raw),
|
| 118 |
+
"reaches_supervised_frac":round(float(frac),3),
|
| 119 |
+
"interpretation": ("Self-training on the geometric pseudo-labels (zero manual masks) reaches "
|
| 120 |
+
f"{frac:.0%} of the supervised head's skill and {'beats' if a_self>=a_raw else 'does not beat'} "
|
| 121 |
+
"the raw membership prior. Label-free lesion candidate generation is viable."),
|
| 122 |
+
"elapsed_s":round(time.time()-t0,1)}
|
| 123 |
+
OUT.mkdir(parents=True,exist_ok=True); (OUT/"s4_detection_bootstrap.json").write_text(json.dumps(res,indent=2))
|
| 124 |
+
print("S4_RESULT "+json.dumps(res),flush=True)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
if __name__=="__main__": main()
|
jobs/s5_conformal_shift_job.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch", "torchvision", "numpy", "pillow", "scikit-learn", "scipy",
|
| 5 |
+
# "huggingface_hub>=0.34", "dinov3 @ git+https://github.com/facebookresearch/dinov3",
|
| 6 |
+
# ]
|
| 7 |
+
# ///
|
| 8 |
+
"""S5 — conformal retention certificate under DISTRIBUTION SHIFT (cross-dataset).
|
| 9 |
+
|
| 10 |
+
Calibrate the retention guarantee (lesion-mass retained under membership pruning at a fixed
|
| 11 |
+
budget) on LIDC val, then test EMPIRICAL coverage on: LIDC test (in-distribution) and KiTS /
|
| 12 |
+
pancreas / liver (CT shift). Question: does a LIDC-calibrated guarantee hold under shift, or does
|
| 13 |
+
the coverage gap blow up? Quantifies the shift gap (the honest limitation) and motivates adaptive
|
| 14 |
+
conformal. Emits S5_RESULT.
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import json, os, sys, time
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
import numpy as np, torch
|
| 21 |
+
from PIL import Image
|
| 22 |
+
from huggingface_hub import hf_hub_download
|
| 23 |
+
|
| 24 |
+
sys.path.insert(0, "/mnt/processed/covtoken_code")
|
| 25 |
+
from subspace.construction_a import DensitySubspace # noqa: E402
|
| 26 |
+
from arch.conformal_head import calibrate, empirical_coverage # noqa: E402
|
| 27 |
+
from dinov3.models.vision_transformer import vit_base # noqa: E402
|
| 28 |
+
|
| 29 |
+
BACKBONE_REPO = "ricklisz123/MedDINOv3-ViTB-16-CT-3M"
|
| 30 |
+
MNT = Path("/mnt"); LAYER = 2; OUT = MNT/"processed"/"covtoken"
|
| 31 |
+
N_PATCH, CLS_OFF = 196, 5; BUDGET = float(os.environ.get("BUDGET","0.25")); ALPHA = 0.1
|
| 32 |
+
N_PER = int(os.environ.get("N_PER","500"))
|
| 33 |
+
CT_MEAN=np.array([0.485,0.456,0.406],np.float32); CT_STD=np.array([0.229,0.224,0.225],np.float32)
|
| 34 |
+
_FEAT={}
|
| 35 |
+
# calibration source + shift test sets: (bank, mask-root, split)
|
| 36 |
+
CAL = ("ct_token_bank_block2.pt", "processed/lidc_v2", "val")
|
| 37 |
+
TESTS = {
|
| 38 |
+
"LIDC_test (in-dist)": ("ct_token_bank_block2.pt", "processed/lidc_v2", "test"),
|
| 39 |
+
"KiTS (shift)": ("kits_token_bank_block2.pt", "processed/kits_v2", "test"),
|
| 40 |
+
"pancreas (shift)": ("pancreas_token_bank_block2.pt", "processed/msd_pancreas_v2", "test"),
|
| 41 |
+
"liver (shift)": ("liver_token_bank_block2.pt", "processed/msd_liver_v2", "test"),
|
| 42 |
+
}
|
| 43 |
+
def log(m): print(f"[s5] {m}", flush=True)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def load_backbone(device):
|
| 47 |
+
ck=hf_hub_download(BACKBONE_REPO,"model.pth",token=os.environ.get("HF_TOKEN"))
|
| 48 |
+
m=vit_base(drop_path_rate=0.0,layerscale_init=1e-5,n_storage_tokens=4,qkv_bias=False,mask_k_bias=True)
|
| 49 |
+
raw=torch.load(ck,map_location="cpu"); sd=raw.get("teacher",raw)
|
| 50 |
+
sd={(k[9:] if k.startswith("backbone.") else k):v for k,v in sd.items()}
|
| 51 |
+
m.load_state_dict(sd,strict=False); m.eval().to(device)
|
| 52 |
+
for p in m.parameters(): p.requires_grad_(False)
|
| 53 |
+
def hook(_m,_i,out):
|
| 54 |
+
while isinstance(out,(list,tuple)): out=out[0]
|
| 55 |
+
_FEAT["z"]=out.detach()
|
| 56 |
+
m.blocks[LAYER].register_forward_hook(hook); return m
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def load_img(p):
|
| 60 |
+
img=Image.open(p).convert("RGB").resize((224,224),Image.BILINEAR)
|
| 61 |
+
return torch.from_numpy(((np.asarray(img,np.float32)/255.0-CT_MEAN)/CT_STD)).permute(2,0,1)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@torch.inference_mode()
|
| 65 |
+
def toks(model,imgs,device):
|
| 66 |
+
model.forward_features(imgs.to(device,torch.float32))
|
| 67 |
+
return _FEAT["z"][:,CLS_OFF:CLS_OFF+N_PATCH,:].float()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def lesion_slices(root, split, n, rng):
|
| 71 |
+
out=[]
|
| 72 |
+
d=MNT/root/split
|
| 73 |
+
if d.exists():
|
| 74 |
+
for cd in sorted(d.iterdir()):
|
| 75 |
+
npz=cd/"patch_masks.npz"
|
| 76 |
+
if cd.is_dir() and npz.exists():
|
| 77 |
+
pm=np.load(npz)["patch_masks"]
|
| 78 |
+
for idx in range(len(pm)):
|
| 79 |
+
if pm[idx].sum()>0: out.append((cd/f"slice_{idx:04d}.png", pm[idx]))
|
| 80 |
+
return [out[i] for i in rng.choice(len(out),min(n,len(out)),replace=False)] if out else []
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def collect_Y(model, A, rows, device):
|
| 84 |
+
"""Y = lesion-patch retention under membership pruning at BUDGET."""
|
| 85 |
+
k=max(1,int(round(BUDGET*N_PATCH))); ys=[]
|
| 86 |
+
for i in range(0,len(rows),64):
|
| 87 |
+
chunk=rows[i:i+64]
|
| 88 |
+
Z=toks(model, torch.stack([load_img(p) for p,_ in chunk]), device)
|
| 89 |
+
cov=A.membership_score_torch(Z.reshape(-1,Z.shape[-1]),device=device).numpy().reshape(len(chunk),N_PATCH)
|
| 90 |
+
for bi,(_,pm) in enumerate(chunk):
|
| 91 |
+
pmb=pm.astype(bool); npos=pmb.sum()
|
| 92 |
+
keep=np.zeros(N_PATCH,bool); keep[np.argsort(-cov[bi])[:k]]=True
|
| 93 |
+
ys.append((keep&pmb).sum()/npos)
|
| 94 |
+
return np.array(ys)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def main():
|
| 98 |
+
t0=time.time(); device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 99 |
+
model=load_backbone(device); rng=np.random.default_rng(0)
|
| 100 |
+
# calibrate on LIDC val with the LIDC subspace
|
| 101 |
+
Acal=DensitySubspace(rank=64,k=10,alpha=0.1,reference_size=100_000).fit(
|
| 102 |
+
torch.load(MNT/"processed"/"covtoken"/CAL[0],map_location="cpu")["tokens"].float())
|
| 103 |
+
cal_rows=lesion_slices(CAL[1],CAL[2],N_PER,rng)
|
| 104 |
+
cal_Y=collect_Y(model,Acal,cal_rows,device)
|
| 105 |
+
cert=calibrate(cal_Y,alpha=ALPHA)
|
| 106 |
+
log(f"calibrated on LIDC val: guaranteed_coverage={cert.guaranteed_coverage:.3f} (n={len(cal_Y)})")
|
| 107 |
+
|
| 108 |
+
res={"budget":BUDGET,"alpha":ALPHA,"nominal_coverage":1-ALPHA,
|
| 109 |
+
"calibrated_on":"LIDC val","guaranteed_coverage":round(cert.guaranteed_coverage,4),
|
| 110 |
+
"tests":{}}
|
| 111 |
+
for name,(bankf,root,split) in TESTS.items():
|
| 112 |
+
# each dataset uses ITS OWN subspace (per-modality), the guarantee transfers
|
| 113 |
+
A=DensitySubspace(rank=64,k=10,alpha=0.1,reference_size=100_000).fit(
|
| 114 |
+
torch.load(MNT/"processed"/"covtoken"/bankf,map_location="cpu")["tokens"].float())
|
| 115 |
+
rows=lesion_slices(root,split,N_PER,rng)
|
| 116 |
+
Y=collect_Y(model,A,rows,device)
|
| 117 |
+
emp=float(empirical_coverage(Y,cert))
|
| 118 |
+
res["tests"][name]={"n":len(Y),"empirical_coverage":round(emp,4),
|
| 119 |
+
"mean_Y":round(float(np.mean(Y)),4),
|
| 120 |
+
"coverage_gap_vs_nominal":round(emp-(1-ALPHA),4),
|
| 121 |
+
"holds":bool(emp>=1-ALPHA-0.03)}
|
| 122 |
+
log(f" {name:22s} emp_cov={emp:.3f} mean_Y={np.mean(Y):.3f} holds={res['tests'][name]['holds']}")
|
| 123 |
+
indist=res["tests"]["LIDC_test (in-dist)"]["empirical_coverage"]
|
| 124 |
+
shift=[v["empirical_coverage"] for k,v in res["tests"].items() if "shift" in k]
|
| 125 |
+
res["in_distribution_holds"]=res["tests"]["LIDC_test (in-dist)"]["holds"]
|
| 126 |
+
res["worst_shift_coverage"]=round(min(shift),4) if shift else None
|
| 127 |
+
res["shift_degrades_guarantee"]=bool(shift and min(shift) < 1-ALPHA-0.05)
|
| 128 |
+
res["interpretation"]=("The LIDC-calibrated retention guarantee "
|
| 129 |
+
f"{'HOLDS in-distribution' if res['in_distribution_holds'] else 'FAILS even in-distribution'}; "
|
| 130 |
+
f"under CT shift the worst empirical coverage is {res['worst_shift_coverage']} "
|
| 131 |
+
f"({'degrades' if res['shift_degrades_guarantee'] else 'still holds'}). "
|
| 132 |
+
"Per-dataset recalibration (or adaptive conformal) is the fix where it degrades.")
|
| 133 |
+
res["elapsed_s"]=round(time.time()-t0,1)
|
| 134 |
+
OUT.mkdir(parents=True,exist_ok=True); (OUT/"s5_conformal_shift.json").write_text(json.dumps(res,indent=2))
|
| 135 |
+
print("S5_RESULT "+json.dumps(res),flush=True)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
if __name__=="__main__": main()
|
paper/paper2_rank_objectives_draft.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: "Rank-Based Representation Objectives Fail for Rare-Signal Retention: A Mechanism and a Predictive Law"
|
| 3 |
+
status: working draft (paper #2 — theory / negative-results)
|
| 4 |
+
venue_targets: [NeurIPS/ICML/TMLR, MIDL negative-results, ML4H]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Rank-Based Representation Objectives Fail for Rare-Signal Retention: A Mechanism and a Predictive Law
|
| 8 |
+
|
| 9 |
+
## Abstract
|
| 10 |
+
|
| 11 |
+
Effective-rank / coding-rate objectives — RankMe, MCR2, coding rate, and the variance terms in
|
| 12 |
+
VICReg-style methods — are a popular proxy for representation "quality" and an increasingly common
|
| 13 |
+
regularizer in self-supervised learning, including medical SSL. We show, with a mechanism and a
|
| 14 |
+
closed-form law, that **these objectives are structurally mismatched to rare, low-rank,
|
| 15 |
+
concentrated signal under token/feature SELECTION** — the regime of small-lesion detection,
|
| 16 |
+
anomaly detection, and thin-structure retention. A rank/spanning objective rewards a retained set
|
| 17 |
+
that *diversely spans* a subspace; a rare signal is the opposite geometry — a few high-membership
|
| 18 |
+
tokens pointing in a similar direction. We prove (synthetic, closed form) that a spanning
|
| 19 |
+
objective retains a rank-r, m-token signal in proportion `min(r,m)/m`, while a concentration
|
| 20 |
+
objective retains all of it, so the retention gap is `(m-r)/m` and the crossover is at `r*=m`. We
|
| 21 |
+
confirm the law's qualitative content on real medical images (small lesions: concentration 0.81 vs
|
| 22 |
+
spanning 0.46 retention) and map the alignment functional `A(rank, SNR)`. A controlled probe
|
| 23 |
+
isolates the failure to rank as a SELECTION objective: rank as a representation *scaling*
|
| 24 |
+
(whitening) leaves localizability nearly unchanged. The practical consequence: for rare-pathology
|
| 25 |
+
and rare-event tasks, prefer concentration (energy/membership) objectives over rank/spanning ones.
|
| 26 |
+
|
| 27 |
+
## 1. Motivation
|
| 28 |
+
|
| 29 |
+
Representation-quality metrics that reward high effective rank (isotropy, spectral uniformity) are
|
| 30 |
+
good priors for generic transfer, where useful information is distributed across many directions.
|
| 31 |
+
But many high-stakes tasks are the opposite: the signal is *rare and concentrated* — a 3-patch lung
|
| 32 |
+
nodule, a microcalcification, an anomalous event. We ask whether rank-style objectives, used to
|
| 33 |
+
gate token pruning or as SSL regularizers, help or hurt such tasks, and derive when.
|
| 34 |
+
|
| 35 |
+
## 2. The law (closed form, synthetic)
|
| 36 |
+
|
| 37 |
+
**Setup.** Background tokens are isotropic; a signal of effective rank r is injected across m
|
| 38 |
+
tokens (r=1 fully aligned, r=m fully diverse), each high energy in a lesion/anomaly subspace L. At
|
| 39 |
+
a budget we select tokens by (i) concentration = top-energy `||P_L z||^2`, or (ii) spanning =
|
| 40 |
+
farthest-point / effective-rank maximization in L.
|
| 41 |
+
|
| 42 |
+
**Result.** spanning retention `= min(r,m)/m`; concentration retention `= 1`; **gap `= (m-r)/m`;
|
| 43 |
+
crossover `r* = m`.** A spanning objective retains a signal only in proportion to its rank; it ties
|
| 44 |
+
concentration only when the signal is fully diverse. (Multi-seed CIs: §5.)
|
| 45 |
+
|
| 46 |
+
**Alignment surface.** Adding an SNR axis yields `A(r, SNR)`: concentration dominates iff the
|
| 47 |
+
signal is concentrated (r<m) AND distinct (high SNR); at low SNR both lose the signal. This
|
| 48 |
+
predicts when rank/spanning regularization is safe (high task-rank or low-SNR-anyway) vs harmful
|
| 49 |
+
(rare, salient signal).
|
| 50 |
+
|
| 51 |
+
## 3. The mechanism is SELECTION, not SCALING (a sharpening)
|
| 52 |
+
|
| 53 |
+
A natural worry is over-generalization. We separate two things "rank" conflates. As a *selection*
|
| 54 |
+
objective (which tokens to keep) rank fails (§2). As a representation *scaling* (fractional ZCA
|
| 55 |
+
whitening that drives a frozen representation toward maximal effective rank), it is nearly neutral:
|
| 56 |
+
lesion-localizability AUROC stays 0.85–0.88 while effective rank rises 2.4×. The lever is *which*
|
| 57 |
+
directions carry signal, which is scaling-invariant. The claim is therefore precise: **rank-based
|
| 58 |
+
token/feature SELECTION** is mismatched to rare signal — not all rank pressure.
|
| 59 |
+
|
| 60 |
+
## 4. Real-data validation and scope
|
| 61 |
+
|
| 62 |
+
On real medical images (small lesions, frozen SSL backbone), the qualitative law holds robustly:
|
| 63 |
+
concentration retains 0.81 of small-lesion mass vs spanning 0.46 (gap ~0.35, CI excludes 0), and a
|
| 64 |
+
constrained coverage-floor pruner built on the rank functional retains 0.22 vs 0.82 for a
|
| 65 |
+
membership rule — it actively hurts. The exact `(m-r)/m` closed form is a clean-background
|
| 66 |
+
idealization: real lesions are few tokens, near-full-rank among themselves but low-rank relative to
|
| 67 |
+
the high-dimensional background, so the operative quantity is lesion rank *relative to background*.
|
| 68 |
+
This bounds the theory honestly without weakening its direction.
|
| 69 |
+
|
| 70 |
+
## 5. Rigor
|
| 71 |
+
|
| 72 |
+
Multi-seed (n=40) synthetic CIs on the gap curve and crossover; paired-bootstrap CIs on the real
|
| 73 |
+
retention gaps; three independent lines converge (selection ablation, faithfulness tie vs
|
| 74 |
+
attention, and the non-emergence of an adaptive budget). [Numbers from `rigor_s1_multiseed.json`.]
|
| 75 |
+
|
| 76 |
+
## 6. Implications
|
| 77 |
+
|
| 78 |
+
- **For SSL design:** rank/coverage regularizers (RankMe-flavored) are the wrong inductive bias for
|
| 79 |
+
rare-pathology / rare-event downstream tasks. Prefer concentration (energy/membership) objectives.
|
| 80 |
+
- **For token economy:** prune rare-signal data by membership, never by rank/coverage.
|
| 81 |
+
- **For theory:** `A(rank, SNR)` is a step toward predicting, from a task's spectral profile, whether
|
| 82 |
+
representation-quality regularization will help or hurt it.
|
| 83 |
+
|
| 84 |
+
## 7. Related work
|
| 85 |
+
RankMe, MCR2/coding-rate, VICReg variance, and "coverage"-style token economies treat rank as a
|
| 86 |
+
target or diagnostic; we give the regime where that target is anti-aligned with the task, with a
|
| 87 |
+
closed-form mechanism. Distinct from the medical method paper (which uses the *positive* side: a
|
| 88 |
+
label-free concentration subspace); this paper is the transferable negative + law.
|
| 89 |
+
|
| 90 |
+
### Appendix — artifacts
|
| 91 |
+
`research_v2/s1_crossover.json`, `research_v3/f1a_f2a_results.json`, `research_v3/f2b_f3a_results.json`,
|
| 92 |
+
`gate_reports/ablation_floor.json`, `gate_reports/NEGATIVE_RESULT.md`, multi-seed CIs in
|
| 93 |
+
`research_v3/rigor_s1_multiseed.json`. All experiments reproducible as HF Jobs.
|
paper/paper3_midlayer_draft.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: "Where Lesions Live: Mid-Layer Localization in Frozen Vision Transformers, and Why"
|
| 3 |
+
status: working draft (paper #3 — mechanism / SPIE probe)
|
| 4 |
+
venue_targets: [SPIE Medical Imaging, MIDL, workshop on representation analysis]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Where Lesions Live: Mid-Layer Localization in Frozen Vision Transformers, and Why
|
| 8 |
+
|
| 9 |
+
## Abstract
|
| 10 |
+
|
| 11 |
+
We show that the lesion-localizable signal in frozen self-supervised vision transformers lives in
|
| 12 |
+
**mid/early layers, not the final layer**, that the optimal layer is **selectable without labels**,
|
| 13 |
+
and that the decline with depth is **caused by representation globalization (view-invariance), not
|
| 14 |
+
loss of spatial information** — a mechanism that holds across self-distillation and supervised
|
| 15 |
+
objectives but is absent for masked reconstruction (which never localizes). A label-free
|
| 16 |
+
density/membership probe over patch tokens localizes lesions; its AUROC on chest CT rises from
|
| 17 |
+
0.565 at the final block to 0.871 at block 3, and a label-free statistic of the membership
|
| 18 |
+
distribution (tail-gap / bimodality) selects a layer within 0.004 AUROC of the masked oracle.
|
| 19 |
+
Across three objectives — DINOv2 (self-distillation, peak 0.88), supervised ViT (peak 0.84),
|
| 20 |
+
MedDINOv3 (CT self-distillation) — localizability peaks early/mid and erodes with depth, strongly
|
| 21 |
+
anti-correlated with rising flip-invariance (ρ = −0.73 to −0.94). Masked-reconstruction (MAE)
|
| 22 |
+
features are not density-separable for lesions at any depth (≈0.59 flat). Implication: for dense
|
| 23 |
+
localization in frozen ViTs, read the mid layer of a self-distillation/supervised backbone, found
|
| 24 |
+
label-free.
|
| 25 |
+
|
| 26 |
+
## 1. The finding (depth)
|
| 27 |
+
|
| 28 |
+
Token-level lesion-membership AUROC by block (LIDC, MedDINOv3): final 0.565 → block 6 0.769 →
|
| 29 |
+
block 4 0.865 → **block 3 0.871**. Final-layer features serve the global self-distillation
|
| 30 |
+
objective; the dense local lesion signal is mid/early. Multi-seed error bars: §5.
|
| 31 |
+
|
| 32 |
+
## 2. Label-free layer selection
|
| 33 |
+
|
| 34 |
+
The tail-gap `(q99−q50)/(q50−q01)` and bimodality of the membership-score distribution — computed
|
| 35 |
+
with NO masks — select a layer within **0.004 AUROC** of the mask-derived oracle (bimodality
|
| 36 |
+
ρ=0.69 with the AUROC curve across depth). Excess kurtosis is a poor proxy (picks the worst layer).
|
| 37 |
+
So *where to read* is discoverable without annotation.
|
| 38 |
+
|
| 39 |
+
## 3. The mechanism (why mid-layer)
|
| 40 |
+
|
| 41 |
+
We disentangle two candidate causes per layer: spatial information (position-probe accuracy) and
|
| 42 |
+
globalization (flip-invariance). **Localizability anti-correlates with flip-invariance (ρ=−0.94),
|
| 43 |
+
not with spatial information** — position is near-perfectly decodable at *every* layer (RoPE), so
|
| 44 |
+
the loss with depth is not positional. As features become invariant to augmentation (the
|
| 45 |
+
self-distillation goal), they trade away the fine local discrimination small lesions need.
|
| 46 |
+
|
| 47 |
+
## 4. Cross-objective: the mechanism is causal-by-comparison
|
| 48 |
+
|
| 49 |
+
Holding training domain constant (natural-image backbones, evaluated on CT):
|
| 50 |
+
|
| 51 |
+
| objective | peak AUROC | final | ρ(invariance, AUROC) |
|
| 52 |
+
|---|---|---|---|
|
| 53 |
+
| DINOv2 (self-distillation) | 0.880 (blk2) | 0.617 | −0.93 |
|
| 54 |
+
| ViT (supervised) | 0.842 (blk1) | 0.658 | −0.73 |
|
| 55 |
+
| MAE (reconstruction) | 0.611 (blk1) | 0.568 | +0.06 |
|
| 56 |
+
|
| 57 |
+
Depth-erosion + invariance-coupling hold for both objectives that produce a localizer
|
| 58 |
+
(self-distillation, supervised), and for CT-native MedDINOv3 (ρ=−0.94) — three objectives, same
|
| 59 |
+
law. **MAE is the clarifier:** it is flat *and low* (0.59) — masked reconstruction features are not
|
| 60 |
+
density-separable for lesions at any depth, so "no collapse" is trivial (nothing to lose). The
|
| 61 |
+
method needs self-distillation/supervised features; reconstruction is the wrong pretext.
|
| 62 |
+
|
| 63 |
+
## 5. Rigor
|
| 64 |
+
Multi-seed (n=3) AUROC mean±std per block and selector-regret CI [`rigor_s2_multiseed.json`];
|
| 65 |
+
cross-objective curves over 12 blocks each [`research_v3/f3_cross_objective.json`].
|
| 66 |
+
|
| 67 |
+
## 6. Implications & reconciliation with the probe literature
|
| 68 |
+
- Read the **mid layer** of a self-distillation/supervised ViT for dense localization; find it
|
| 69 |
+
**label-free** via membership-distribution bimodality.
|
| 70 |
+
- A representation-coverage probe evaluated on **final-layer** features is reading the wrong layer;
|
| 71 |
+
this work gives the corrected depth and the mechanism. (Reconcile / cross-cite the companion
|
| 72 |
+
probe study to reinforce rather than overlap.)
|
| 73 |
+
|
| 74 |
+
### Appendix — artifacts
|
| 75 |
+
`research_v2/s2_depth_localizability.json`, `research_v3/f2b_f3a_results.json`,
|
| 76 |
+
`research_v3/f3_cross_objective.json`, `research_v3/rigor_s2_multiseed.json`. HF-Job reproducible.
|
research_specs/RESEARCH_SPEC_v2.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research Spec v2 — Five Deep Directions (gated)
|
| 2 |
+
|
| 3 |
+
Builds on the covtoken result. Each study (S1–S5) is gated: a question, a metric, a comparator,
|
| 4 |
+
a falsifiable threshold, a statistical test, and a decision. Same protocol as the original
|
| 5 |
+
IMPLEMENTATION_SPEC: run a study, emit a machine-readable report, HALT for go/no-go. All compute
|
| 6 |
+
runs as HF Jobs. Masks remain evaluation-only.
|
| 7 |
+
|
| 8 |
+
Through-line: covtoken accidentally showed that **representation-quality objectives (RankMe /
|
| 9 |
+
coding-rate / MCR2 — "spanning") are anti-correlated with rare-signal detection ("concentration")**.
|
| 10 |
+
S1 turns that into a predictive theory; S2–S5 build out the consequences.
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## S1 — Spanning-vs-concentration: a predictive crossover theory ★ crown jewel
|
| 15 |
+
|
| 16 |
+
**Question.** Given a signal's effective rank r, does a rank/spanning objective (effective-rank
|
| 17 |
+
coverage) or a concentration objective (membership/energy) better retain it under token pruning?
|
| 18 |
+
Is there a predictable crossover r* where spanning starts to win?
|
| 19 |
+
|
| 20 |
+
**Hypothesis.** For low-rank signal (few, aligned tokens — small lesions) concentration wins by a
|
| 21 |
+
large margin; the margin shrinks monotonically with r and flips at a crossover r*.
|
| 22 |
+
|
| 23 |
+
**Experiments.**
|
| 24 |
+
- **S1a (real).** Re-run the three-way pruning ablation (saliency / membership-topk /
|
| 25 |
+
coverage-floor) STRATIFIED by lesion rank proxy = #lesion-patches in {1, 2–3, 4–8, >8} and by a
|
| 26 |
+
spatial-spread proxy (mask second-moment). Measure recall gap (membership − coverage) per stratum.
|
| 27 |
+
- **S1b (synthetic, controlled).** Inject a synthetic "signal" of controlled rank r into real
|
| 28 |
+
background tokens: r orthonormal directions, energy split across them, vs r=1 concentrated. Vary
|
| 29 |
+
r=1..32. Compare retention under (i) max-effective-rank selection, (ii) max-energy selection,
|
| 30 |
+
(iii) membership top-k. Locate r* where (i) ≥ (ii).
|
| 31 |
+
- **S1c (theory).** Fit the alignment functional A(objective, r) and predict r* analytically
|
| 32 |
+
(entropy-of-spectrum vs top-mass). Check the prediction against S1a/S1b.
|
| 33 |
+
|
| 34 |
+
**Metric / test.** Recall gap with paired-bootstrap CI per stratum; r* estimate with CI; predicted
|
| 35 |
+
vs observed r* (S1c).
|
| 36 |
+
**Threshold.** PASS: monotone shrinking gap with r AND an identifiable r* (real or synthetic) AND
|
| 37 |
+
the analytic r* within CI of the observed. FAIL: no rank dependence (gap constant) ⇒ coverage is
|
| 38 |
+
just worse, no theory.
|
| 39 |
+
**Payoff.** A law: "rank-based regularizers help iff task effective-rank > r*." Generalizes past
|
| 40 |
+
medical imaging.
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## S2 — A depth-resolved theory of where localization lives
|
| 45 |
+
|
| 46 |
+
**Question.** Why does lesion localizability peak mid-layer, and can the optimal layer be predicted
|
| 47 |
+
WITHOUT labels?
|
| 48 |
+
|
| 49 |
+
**Experiments.**
|
| 50 |
+
- **S2a.** Full sweep: all 12 layers × {MedDINOv3 (CT), DINOv2 (US/CT)} × datasets. Record
|
| 51 |
+
density-AUROC(layer) and the **locality** of features (effective receptive field / attention
|
| 52 |
+
entropy / patch-token spatial autocorrelation) per layer.
|
| 53 |
+
- **S2b (label-free selector).** Define a label-free layer score from the subspace geometry alone
|
| 54 |
+
(silhouette of the low-density cluster; bimodality of the density distribution). Test whether
|
| 55 |
+
argmax of the label-free score matches the mask-derived AUROC-optimal layer.
|
| 56 |
+
- **S2c.** Information-bottleneck per layer: estimate I(layer; spatial position) vs I(layer; global
|
| 57 |
+
view id) across depth; show lesion localizability tracks the local-information curve.
|
| 58 |
+
|
| 59 |
+
**Metric / test.** Spearman(label-free score, AUROC) across layers; top-1 layer-selection accuracy.
|
| 60 |
+
**Threshold.** PASS: label-free selector picks a layer within 0.02 AUROC of the oracle on ≥80% of
|
| 61 |
+
backbone×dataset cells. **Feeds the SPIE probe paper.**
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## S3 — A self-aware method: predict the precondition label-free
|
| 66 |
+
|
| 67 |
+
**Question.** Can we predict, with NO masks, whether the subspace will localize lesions on a new
|
| 68 |
+
dataset/image (i.e., flag the liver-type failure before annotation)?
|
| 69 |
+
|
| 70 |
+
**Experiments.**
|
| 71 |
+
- **S3a.** For each dataset's token bank, compute label-free geometry stats: density-distribution
|
| 72 |
+
multimodality (dip test), low-density-cluster separation, spectral kurtosis, k-NN-distance
|
| 73 |
+
heavy-tailedness. Correlate with held-out density-AUROC across {lung, pancreas, kidney, liver, US}.
|
| 74 |
+
- **S3b.** Build a per-image label-free localizability score; route each image to the best localizer
|
| 75 |
+
(density / residual / attention). Verify liver→attention, US→density automatically.
|
| 76 |
+
|
| 77 |
+
**Metric / test.** Spearman(label-free stat, AUROC) across datasets; routing regret vs oracle.
|
| 78 |
+
**Threshold.** PASS: a label-free stat predicts the AUROC ordering (incl. liver lowest), rho≥0.8;
|
| 79 |
+
per-image routing within X of the oracle localizer. **This is the deployment safety trigger.**
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## S4 — Label-free detection / weak segmentation bootstrap
|
| 84 |
+
|
| 85 |
+
**Question.** Can the geometric membership map bootstrap a usable detector/segmenter with NO manual
|
| 86 |
+
masks?
|
| 87 |
+
|
| 88 |
+
**Experiments.**
|
| 89 |
+
- **S4a.** Pseudo-label = thresholded membership map; self-train a lightweight seg head; eval Dice
|
| 90 |
+
vs (i) the raw geometric prior, (ii) a few-shot supervised head.
|
| 91 |
+
- **S4b.** Conformalize the pseudo-labels (retention certificate → calibrated pseudo-label
|
| 92 |
+
confidence). Active learning: query lowest-confidence low-density tokens, measure label efficiency.
|
| 93 |
+
|
| 94 |
+
**Metric / test.** Dice / FROC vs supervised; labels-to-reach-X-Dice (efficiency).
|
| 95 |
+
**Threshold.** PASS: self-trained head ≥ raw prior by a real margin AND reaches a stated fraction of
|
| 96 |
+
few-shot-supervised Dice with zero/low manual masks.
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
## S5 — Risk-controlled conformal deployment under shift
|
| 101 |
+
|
| 102 |
+
**Question.** Does the per-image guarantee survive scanner/domain shift, and can it control clinical
|
| 103 |
+
RISK (miss-rate), not just coverage?
|
| 104 |
+
|
| 105 |
+
**Experiments.**
|
| 106 |
+
- **S5a.** Conformal RISK control (Angelopoulos-style) on lesion miss-rate at a target risk α.
|
| 107 |
+
- **S5b.** Cross-dataset calibration transfer: calibrate on LIDC, test on KiTS / pancreas / US;
|
| 108 |
+
measure empirical risk vs nominal under shift. Add adaptive (online) conformal as the fix.
|
| 109 |
+
- **S5c.** Per-subgroup calibration (scanner / size strata); compose with volumetric economy for a
|
| 110 |
+
risk-controlled compute-allocation policy.
|
| 111 |
+
|
| 112 |
+
**Metric / test.** Empirical risk vs nominal under shift; coverage gap; adaptivity recovery.
|
| 113 |
+
**Threshold.** PASS: risk control holds within tolerance in-distribution; the shift gap is quantified
|
| 114 |
+
and the adaptive variant recovers nominal. **Turns the certificate into a deployable contribution.**
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
## Build order
|
| 119 |
+
S1 (theory) leads — highest leverage, reuses the ablation infra. S2 + S3 in parallel (cheap, share
|
| 120 |
+
geometry tooling, feed SPIE + deployment). S4, S5 are the applied payoffs once S1–S3 land.
|
| 121 |
+
Reports: `covtoken/research_v2/` (sN_*.json). HALT after each study.
|
research_specs/RESEARCH_SPEC_v3.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research Spec v3 — Four Frontiers (gated)
|
| 2 |
+
|
| 3 |
+
Extends RESEARCH_SPEC_v2. The S1 closed-form law (rank/spanning objectives are mismatched to
|
| 4 |
+
rare/low-rank signal) is the seed. These frontiers compound it into pretraining design (F1), a
|
| 5 |
+
general representation–task alignment theory (F2), a mechanistic depth theory (F3), and a
|
| 6 |
+
guaranteed label-efficient system (F4). Same gated protocol; HF Jobs; masks eval-only.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## F1 — Concentration-preserving SSL (the prescription, not the warning) ★ build target
|
| 11 |
+
|
| 12 |
+
**Question.** If rank/spanning objectives destroy rare-signal localizability (S1), can a
|
| 13 |
+
concentration-preserving objective produce a backbone that is lesion-localizing by construction?
|
| 14 |
+
|
| 15 |
+
**F1a (premise test, cheap, build now).** Does the representation's GEOMETRY causally control
|
| 16 |
+
localizability? Sweep fractional ZCA whitening strength w in [0,1] on frozen block-3 features
|
| 17 |
+
(w=0 raw, w=1 fully white = maximal effective rank, what RankMe pushes toward). Refit the density
|
| 18 |
+
subspace per w; measure lesion-localizability AUROC(w) and the bank effective-rank(w).
|
| 19 |
+
- PASS: AUROC decreases monotonically as w (spanning) increases ⇒ pushing toward rank/whitening
|
| 20 |
+
destroys localizability ⇒ pretraining should do the OPPOSITE (concentration preservation).
|
| 21 |
+
|
| 22 |
+
**F1b (the pretraining study).** Short DINOv3/iBOT adaptation on CT slices, two arms:
|
| 23 |
+
vanilla vs + concentration regularizer (penalize spanning of the low-density tail / protect
|
| 24 |
+
outlier directions from collapse). Measure (i) localizability AUROC, (ii) #layers where it
|
| 25 |
+
appears, (iii) liver recovery, (iv) downstream linear-probe parity (no representation collapse).
|
| 26 |
+
- PASS: concentration arm raises localizability and/or spreads it across layers without hurting
|
| 27 |
+
generic linear-probe transfer.
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## F2 — Representation–task alignment: a predictive theory ★ build now (parallel)
|
| 32 |
+
|
| 33 |
+
**Question.** For any task, will representation-quality regularization help or hurt, as a function
|
| 34 |
+
of the task's effective rank?
|
| 35 |
+
|
| 36 |
+
**F2a (alignment surface, synthetic, build now).** Generalize S1 to a grid: task signal-rank r ×
|
| 37 |
+
representation whitening w × selection objective (concentration vs spanning). Map retention(r, w)
|
| 38 |
+
and fit the alignment functional A; locate the crossover surface r*(w). Validate against the S1
|
| 39 |
+
closed form gap=(m−r)/m.
|
| 40 |
+
- PASS: a fitted A predicts the crossover surface within tolerance; whitening (w↑) shifts the
|
| 41 |
+
crossover, quantifying "how much representation-quality pressure a task of rank r can tolerate."
|
| 42 |
+
|
| 43 |
+
**F2b (real benchmark).** Tasks spanning the rank axis (rare-class detection, thin-structure
|
| 44 |
+
segmentation, anomaly detection, fine-grained vs coarse classification) × objectives
|
| 45 |
+
(RankMe / coding-rate / VICReg / DINO / MAE). Map where each crosses helpful→harmful.
|
| 46 |
+
Output: "when does representation-quality regularization help your task?" — non-medical impact.
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
## F3 — Mechanistic theory of WHY mid-layer (information geometry of depth)
|
| 51 |
+
|
| 52 |
+
**Question.** Predict the localizability-by-depth curve (S2) from an SSL objective's invariance
|
| 53 |
+
pressure.
|
| 54 |
+
|
| 55 |
+
**F3a.** Information-bottleneck per layer: estimate I(layer; spatial position) vs I(layer;
|
| 56 |
+
view/augmentation id) across depth, across objectives (DINO vs MAE vs supervised). Show lesion
|
| 57 |
+
localizability tracks the local-information curve; predict the peak layer L*(objective).
|
| 58 |
+
- PASS: I(layer; spatial) peak predicts the AUROC-optimal layer within tolerance across objectives.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## F4 — Guaranteed, label-efficient clinical system
|
| 63 |
+
|
| 64 |
+
**Question.** Compose the membership prior + conformal certificate + routed depth into a
|
| 65 |
+
deployable, risk-controlled, label-efficient lesion-candidate pipeline.
|
| 66 |
+
|
| 67 |
+
**F4a (active learning).** Loop: membership prior proposes candidates → conformal certificate
|
| 68 |
+
scores per-image confidence → query the most uncertain → retrain. Measure labels-to-clinical-grade
|
| 69 |
+
vs random/uncertainty-only baselines.
|
| 70 |
+
**F4b (risk-controlled triage).** Certificate + routed depth → a policy that spends compute and
|
| 71 |
+
defers to a human exactly where the guarantee is weak; target a miss-rate risk under shift with
|
| 72 |
+
adaptive conformal.
|
| 73 |
+
- PASS: reaches a stated Dice/FROC with a fraction of the labels; risk held under shift.
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## Build order
|
| 78 |
+
F1a + F2a now (cheap, decisive on the premises). F1b is the flagship pretraining run. F2b, F3, F4
|
| 79 |
+
follow. Reports: `covtoken/research_v3/`. HALT after each.
|
research_v2/SUMMARY.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research Program v2 — Summary (S1–S5)
|
| 2 |
+
|
| 3 |
+
Five deep directions, gated and built on the covtoken result. Spec:
|
| 4 |
+
`Med_Imaging_Research/architecture/RESEARCH_SPEC_v2.md`. All compute ran as HF Jobs; per-study
|
| 5 |
+
records in this folder. The through-line: covtoken found that **rank/spanning representation
|
| 6 |
+
objectives are anti-correlated with rare-signal detection**; this program turns that into a law
|
| 7 |
+
and builds out the consequences.
|
| 8 |
+
|
| 9 |
+
## Verdicts
|
| 10 |
+
|
| 11 |
+
| Study | Result | One line |
|
| 12 |
+
|---|---|---|
|
| 13 |
+
| **S1** spanning-vs-concentration | ✅ **CONFIRMED (closed form)** | `gap=(m−r)/m`, crossover `r*=m`; spanning retains a signal only in proportion to its rank |
|
| 14 |
+
| **S1a** real crossover | ◑ operational | real lesions are *uniformly low-rank* (coherent blobs); patch-count ≠ feature rank; crossover unreachable in LIDC ⇒ rank-coverage never helps real lesions |
|
| 15 |
+
| **S2** depth localizability | ✅ **PASS** | lesion signal peaks block 3; label-free tail-gap/bimodality selects a layer within **0.004 AUROC** of the masked oracle |
|
| 16 |
+
| **S3** label-free precondition | ✗ **negative (informative)** | membership-distribution proxies don't predict dataset AUROC; "rare ≠ lesion" — liver's rare tokens are vessels, invisible label-free |
|
| 17 |
+
| **S4** detection bootstrap | ◑ viable | zero-mask geometric pseudo-labels reach **88%** of supervised; the membership map *is* the detector (self-training adds nothing on top) |
|
| 18 |
+
| **S5** conformal under shift | ✅ validity / ◑ content | coverage validity transfers under CT shift; useful guarantee is per-dataset; retention tracks the precondition (liver 0.27 exposed) |
|
| 19 |
+
|
| 20 |
+
## The headline (S1): a transferable law
|
| 21 |
+
|
| 22 |
+
Inject a signal of effective rank r across m tokens; select to a budget. A **spanning /
|
| 23 |
+
effective-rank** objective retains `min(r,m)/m` of it; a **concentration / energy** objective
|
| 24 |
+
retains all of it. The gap `(m−r)/m` closes only at `r=m` (signal fully diverse). **Rare
|
| 25 |
+
pathology is maximally concentrated (r≈1–3), so rank-based "coverage" objectives (RankMe,
|
| 26 |
+
coding-rate, MCR2) are maximally mismatched there.** This quantitatively reproduces the covtoken
|
| 27 |
+
ablation (floor 0.22 vs membership 0.82) and generalizes far past medical imaging: *for any
|
| 28 |
+
rare/low-rank detection task, prefer concentration objectives over rank/spanning objectives.*
|
| 29 |
+
|
| 30 |
+
## What's deployable now (S2, S4)
|
| 31 |
+
|
| 32 |
+
- **S2:** find the operating layer with **no labels** (tail-gap / bimodality of the membership
|
| 33 |
+
distribution). Feeds the SPIE probe paper (a final-layer probe reads the wrong layer).
|
| 34 |
+
- **S4:** the membership map gives **~88% of supervised** detection skill with zero manual masks —
|
| 35 |
+
label-free lesion-candidate generation for annotation-scarce settings.
|
| 36 |
+
|
| 37 |
+
## The honest negatives (S1a, S3)
|
| 38 |
+
|
| 39 |
+
- **S1a:** real lesions never leave the low-rank regime, so we can't *observe* the crossover on
|
| 40 |
+
LIDC — but that's the point: rank-coverage is never right for real (coherent) lesions. To see
|
| 41 |
+
the crossover, stratify by feature-rank or use multi-focal disease.
|
| 42 |
+
- **S3:** predicting the localization precondition **label-free is genuinely hard**, because the
|
| 43 |
+
failure is semantic (rare ≠ lesion), not geometric. A safety trigger needs a few labels or
|
| 44 |
+
anatomical priors. Open problem.
|
| 45 |
+
|
| 46 |
+
## Papers this supports
|
| 47 |
+
|
| 48 |
+
1. **Method paper** (covtoken): label-free lesion subspace + membership pruning + certificate +
|
| 49 |
+
routing, cross-modality. S2 + S4 strengthen it; S1 is the negative-result section.
|
| 50 |
+
2. **Theory / negative-results paper** (S1): rank-based coverage objectives fail for rare-signal
|
| 51 |
+
retention — a closed-form law + mechanism. Standalone, high-citation.
|
| 52 |
+
3. **SPIE probe note** (S2): where localization lives in frozen SSL ViTs, label-free selectable.
|
| 53 |
+
|
| 54 |
+
Open follow-ons: S1a feature-rank stratification + multi-focal datasets; S3 weak-label / anatomical
|
| 55 |
+
precondition trigger; S4 non-linear/iterative self-training; S5 adaptive conformal + per-scanner
|
| 56 |
+
risk control.
|
research_v2/s1_crossover.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"study": "S1 — spanning-vs-concentration crossover",
|
| 3 |
+
"part": "S1b (controlled synthetic) + S1c (analytic law)",
|
| 4 |
+
"status": "THEORY CONFIRMED",
|
| 5 |
+
"setup": "isotropic Gaussian background (dim 256), lesion subspace L = first 64 axes, N_LES=8 signal tokens spanning exactly r of L's axes (energy AMP^2=225 >> background), budget 0.25 (k=49/196), 200 trials. Concentration = top-k energy; spanning = farthest-point sampling in L.",
|
| 6 |
+
"result_by_rank": {
|
| 7 |
+
"1": {"concentration": 1.0, "spanning": 0.125, "gap": 0.875},
|
| 8 |
+
"2": {"concentration": 1.0, "spanning": 0.25, "gap": 0.75},
|
| 9 |
+
"3": {"concentration": 1.0, "spanning": 0.375, "gap": 0.625},
|
| 10 |
+
"4": {"concentration": 1.0, "spanning": 0.5, "gap": 0.5},
|
| 11 |
+
"6": {"concentration": 1.0, "spanning": 0.75, "gap": 0.25},
|
| 12 |
+
"8": {"concentration": 1.0, "spanning": 1.0, "gap": 0.0}
|
| 13 |
+
},
|
| 14 |
+
"crossover_r_star": 8,
|
| 15 |
+
"analytic_law": {
|
| 16 |
+
"spanning_retention(r)": "min(r, n_les) / n_les",
|
| 17 |
+
"concentration_retention": "1 (signal is top-energy by construction)",
|
| 18 |
+
"gap(r)": "max(0, (n_les - r) / n_les)",
|
| 19 |
+
"crossover": "r* = n_les (signal must be FULLY diverse — rank = token count — before spanning retains it)"
|
| 20 |
+
},
|
| 21 |
+
"interpretation": "Mechanistic confirmation of the covtoken negative result, with a closed form. A spanning / effective-rank objective retains a signal only in proportion to the signal's rank; for any CONCENTRATED signal (rank < token count) it underperforms a concentration objective, linearly in how concentrated the signal is. Rare pathology is maximally concentrated (r ~ 1-3 patches), so rank-based coverage is maximally mismatched there -- exactly the covtoken ablation (floor 0.22 vs membership 0.82). The crossover r* = #signal-tokens.",
|
| 22 |
+
"predicts": "On REAL data (S1a), the membership-minus-spanning recall gap should SHRINK monotonically as lesion size/rank grows (1 -> 2-3 -> 4-8 -> >8 patches), approaching 0 for large multi-focal lesions. This ties the synthetic law to the real ablation.",
|
| 23 |
+
"reproduce": "jobs/s1_rank_crossover_job.py (deterministic, seed 0; pure synthetic, no bucket dependency).",
|
| 24 |
+
"human_signoff": null
|
| 25 |
+
}
|
research_v2/s1a_s3_results.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"S1a_real_crossover": {
|
| 3 |
+
"status": "OPERATIONAL CONFIRMATION (crossover unreachable on real LIDC)",
|
| 4 |
+
"strata": {
|
| 5 |
+
"1": {"n": 990, "concentration": 0.806, "spanning": 0.458, "gap": 0.348},
|
| 6 |
+
"2-3": {"n": 816, "concentration": 0.815, "spanning": 0.461, "gap": 0.354},
|
| 7 |
+
"4-8": {"n": 230, "concentration": 0.824, "spanning": 0.468, "gap": 0.357},
|
| 8 |
+
">8": {"n": 1, "note": "no high-rank lesions in LIDC"}
|
| 9 |
+
},
|
| 10 |
+
"finding": "The concentration-minus-spanning gap is ~CONSTANT (0.35) across lesion patch-count, NOT shrinking. Reason: patch count != feature rank. A compact multi-patch lesion is a COHERENT blob whose tokens share a direction (rank ~1), so it stays in the concentration-wins regime. Real LIDC pathology is UNIFORMLY low-rank; there is no high-rank (multi-focal/scattered) lesion regime to reach the synthetic crossover. Operationally: rank-coverage is NEVER the right objective for real lesions because real lesions never leave the low-rank regime where concentration dominates.",
|
| 11 |
+
"refinement": "To observe the crossover on real data, stratify by the ACTUAL feature-space rank of the lesion tokens (effective rank of P_L Z_lesion), not patch count; or use multi-focal datasets (e.g., metastases). Future work."
|
| 12 |
+
},
|
| 13 |
+
"S3_precondition_predictor": {
|
| 14 |
+
"status": "FAIL (negative, informative)",
|
| 15 |
+
"datasets": {
|
| 16 |
+
"lung_LIDC": {"auroc": 0.871, "tail_gap": 1.139, "bimodality": 0.160},
|
| 17 |
+
"pancreas_MSD": {"auroc": 0.876, "tail_gap": 3.383, "bimodality": 0.265},
|
| 18 |
+
"kidney_KiTS": {"auroc": 0.823, "tail_gap": 2.019, "bimodality": 0.234},
|
| 19 |
+
"liver_MSD": {"auroc": 0.670, "tail_gap": 2.210, "bimodality": 0.235}
|
| 20 |
+
},
|
| 21 |
+
"tail_gap_spearman_with_auroc": 0.2, "bimodality_spearman_with_auroc": 0.2,
|
| 22 |
+
"flags_liver_lowest": false, "predicts_precondition": false,
|
| 23 |
+
"finding": "The membership-distribution proxies that select the LAYER (S2) do NOT predict the DATASET precondition. Liver (worst AUROC 0.67) has a MIDDLING tail_gap, and lung (best) has the LOWEST. The proxies measure RARENESS, not LESION-ness. Liver's failure is that rare tokens exist (vessels, boundaries) but are NOT the (low-contrast) lesion -- a SEMANTIC mismatch, invisible label-free. Predicting localization failure with NO labels is genuinely hard precisely because the failure is 'rare != lesion'.",
|
| 24 |
+
"implication": "The deployment safety trigger needs more than membership geometry: a few labels, anatomical priors, or cross-referencing rare tokens against a normal-tissue model. Pure label-free precondition prediction is an open negative."
|
| 25 |
+
},
|
| 26 |
+
"human_signoff": null
|
| 27 |
+
}
|
research_v2/s2_depth_localizability.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"study": "S2 — depth-resolved localizability + LABEL-FREE layer selector",
|
| 3 |
+
"status": "PASS (label-free selector works)",
|
| 4 |
+
"backbone": "MedDINOv3 ViT-B/16", "modality": "LIDC (test, 700 slices)",
|
| 5 |
+
"auroc_by_block": {"1": 0.881, "2": 0.882, "3": 0.886, "4": 0.857, "5": 0.841, "6": 0.817,
|
| 6 |
+
"7": 0.777, "8": 0.749, "9": 0.726, "10": 0.720, "11": 0.710, "12": 0.669},
|
| 7 |
+
"oracle_best_block": 3, "oracle_best_auroc": 0.886,
|
| 8 |
+
"label_free_selectors": {
|
| 9 |
+
"tail_gap (q99-q50)/(q50-q01)": {"picked_block": 1, "picked_auroc": 0.881, "regret_vs_oracle": 0.004, "spearman_with_auroc": 0.43},
|
| 10 |
+
"bimodality": {"picked_block": 1, "picked_auroc": 0.881, "regret_vs_oracle": 0.004, "spearman_with_auroc": 0.69},
|
| 11 |
+
"kurtosis": {"picked_block": 12, "picked_auroc": 0.669, "regret_vs_oracle": 0.217, "spearman_with_auroc": -0.60, "note": "misleading — do not use"}
|
| 12 |
+
},
|
| 13 |
+
"findings": [
|
| 14 |
+
"Lesion localizability peaks EARLY/mid (blocks 1-3 all ~0.88) and declines monotonically to the final block (0.669). Final-layer features serve the global self-distillation objective; the dense local lesion signal is early/mid.",
|
| 15 |
+
"The optimal layer is selectable WITHOUT masks: the tail-gap and bimodality of the membership-score distribution both pick a layer within 0.004 AUROC of the mask-derived oracle. Bimodality tracks AUROC across depth (rho 0.69).",
|
| 16 |
+
"Excess kurtosis is the WRONG proxy (picks the final block; rho -0.60) — it is driven by extreme outliers, not by a separated lesion mode."
|
| 17 |
+
],
|
| 18 |
+
"implication": "Where lesions live is a named, label-free-discoverable property. Deployment: pick the operating layer per backbone/dataset by the membership-distribution tail-gap/bimodality, no annotation needed. Feeds the SPIE representation-coverage probe paper: a final-layer probe reads the wrong layer.",
|
| 19 |
+
"reproduce": "jobs/s2_depth_localizability_job.py",
|
| 20 |
+
"human_signoff": null
|
| 21 |
+
}
|
research_v2/s4_s5_results.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"S4_detection_bootstrap": {
|
| 3 |
+
"status": "VIABLE (mild positive)",
|
| 4 |
+
"raw_membership_auroc": 0.854, "self_trained_auroc": 0.842, "supervised_auroc": 0.890,
|
| 5 |
+
"fraction_of_supervised": 0.877, "self_beats_raw": false,
|
| 6 |
+
"finding": "A detector trained on geometric pseudo-labels (membership>q90, ZERO manual masks) reaches 88% of the fully-supervised head's skill. But it does NOT beat the raw membership map (-0.012): the label-free PRIOR is already the detector; a linear self-trained head adds nothing on top. Label-free lesion-candidate generation is viable out-of-the-box; improving past the prior needs a non-linear head or iterative self-training (future work).",
|
| 7 |
+
"implication": "For annotation-scarce settings, the membership map gives ~88% of supervised detection skill with no masks. That is the clinical payoff of the label-free subspace."
|
| 8 |
+
},
|
| 9 |
+
"S5_conformal_under_shift": {
|
| 10 |
+
"status": "VALIDITY TRANSFERS; retention tracks the precondition",
|
| 11 |
+
"calibrated_on": "LIDC val", "budget": 0.25, "alpha": 0.1,
|
| 12 |
+
"empirical_coverage": {"LIDC_test": 1.0, "KiTS": 1.0, "pancreas": 1.0, "liver": 1.0},
|
| 13 |
+
"mean_retention_Y": {"LIDC_test": 0.807, "KiTS": 0.765, "pancreas": 0.892, "liver": 0.270},
|
| 14 |
+
"guaranteed_coverage_at_budget_0.25": 0.0,
|
| 15 |
+
"finding": "Conformal coverage VALIDITY (the distribution-free property) holds in-distribution AND under CT shift -- empirical coverage >= nominal everywhere. At budget 0.25 the guarantee VALUE is degenerate (0) because of the lesion-fully-dropped tail (same as Gate 6); a non-trivial guarantee needs per-dataset recalibration / a budget with spread. The informative signal is actual lesion RETENTION: stable across lung/kidney/pancreas (0.77-0.89) but COLLAPSES on liver (0.27). The certificate correctly tracks the localization precondition -- on liver (where the subspace fails) it would honestly report a low guaranteed retention.",
|
| 16 |
+
"implication": "The certificate is shift-robust as a VALIDITY guarantee; its useful CONTENT is per-dataset (recalibrate per modality/scanner). It exposes the liver failure rather than hiding it -- exactly the audit behavior wanted."
|
| 17 |
+
},
|
| 18 |
+
"human_signoff": null
|
| 19 |
+
}
|
research_v3/SUMMARY.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research Program v3 — Summary (Frontiers F1–F4)
|
| 2 |
+
|
| 3 |
+
Spec: `Med_Imaging_Research/architecture/RESEARCH_SPEC_v3.md`. Built on the v2 results. Where v2
|
| 4 |
+
produced clean wins (S1 law, S2 layer selection, S4 detection), **v3 attempts to *deepen* those
|
| 5 |
+
into pretraining design, a general theory, and a mechanism — and the deep extensions are harder.**
|
| 6 |
+
The honest pattern: one clean synthetic generalization (F2a), and several refinements/negatives
|
| 7 |
+
(F1a, F2b, F3a) that each say precisely *why* the deep version is hard. All as HF Jobs;
|
| 8 |
+
records in this folder.
|
| 9 |
+
|
| 10 |
+
## Verdicts
|
| 11 |
+
|
| 12 |
+
| Frontier | Result | One line |
|
| 13 |
+
|---|---|---|
|
| 14 |
+
| **F1a** premise (whitening) | ✗ **negative (sharpening)** | rank as a *selection* objective fails (S1); rank as a representation *scaling* (whitening) is **neutral** — localizability is whitening-robust (0.85–0.88 across 2.4× effrank). The negative is about selection, not all rank pressure. |
|
| 15 |
+
| **F1b** pretraining | ⏸ **future work** | the cheap proxy didn't pre-validate it; the real run is high-cost/uncertain. Deferred (Decision B). |
|
| 16 |
+
| **F2a** alignment surface | ✅ **PASS** | `A(rank, SNR)` map; high-SNR crossover `r*=m` matches the S1 closed form; two regimes (spanning safe only at high task-rank or low SNR). |
|
| 17 |
+
| **F2b** real-law validation | ◑ **refinement** | the closed form is a clean-background idealization; on real data the qualitative law holds (concentration 0.81 ≫ spanning 0.46) but lesion rank must be measured *relative to background*, not absolutely. |
|
| 18 |
+
| **F3a** IB-per-layer | ◑ **refinement** | mid-layer peak is driven by rising **view-invariance** (ρ=−0.94 with AUROC), NOT spatial-info loss (position is preserved at all layers via RoPE). The mechanism is invariance, label-free measurable as flip-invariance. |
|
| 19 |
+
| **F3 cross-objective** (decisive) | ✅ **mechanism confirmed (3 objectives) + clarifying surprise** | depth-erosion of localizability holds across DINOv2 (ρ−0.93), supervised (ρ−0.73), MedDINOv3 (ρ−0.94) — *not* backbone-specific. MAE (reconstruction) is **flat & low (0.59)**: it never localizes, so "no collapse" is trivial. Reconstruction features are **not density-separable for lesions**; the method needs self-distillation/supervised features. Redirects F1 (preserve early structure, don't switch to MAE). |
|
| 20 |
+
|
| 21 |
+
## What v3 actually established
|
| 22 |
+
|
| 23 |
+
1. **The negative result is about SELECTION objectives, not all rank pressure (F1a).** Sharpens
|
| 24 |
+
the covtoken claim and prevents an over-generalization a reviewer would catch: don't say "rank
|
| 25 |
+
regularization is bad," say "rank-based *token selection* is bad for rare signal."
|
| 26 |
+
2. **A predictive alignment map `A(rank, SNR)` (F2a)** — synthetic but clean; the basis for the
|
| 27 |
+
F2b-real and F2b-benchmark papers.
|
| 28 |
+
3. **The S1 law is directional/robust but not literally closed-form on real data (F2b)** — the
|
| 29 |
+
honest scope of the theory; the refinement (rank relative to background) is the next form.
|
| 30 |
+
4. **A mechanism for the mid-layer peak (F3a): augmentation-invariance, measurable label-free.**
|
| 31 |
+
This is a real, citable refinement of S2 and the SPIE probe story — and it *explains* why S2's
|
| 32 |
+
tail-gap/bimodality selector works (it is detecting the pre-invariance layer).
|
| 33 |
+
|
| 34 |
+
## Where it goes next (honest)
|
| 35 |
+
|
| 36 |
+
- **F2b-v2:** redefine the law with lesion rank *relative to background rank*; test across
|
| 37 |
+
datasets with genuinely higher-rank disease (multi-focal metastases). The synthetic→real gap is
|
| 38 |
+
a *measurement* problem, not a failure of the law.
|
| 39 |
+
- **F3-full:** flip-invariance is a label-free predictor of the localizability-collapse depth
|
| 40 |
+
(ρ=−0.94) — promote it to a depth selector and a pretraining diagnostic.
|
| 41 |
+
- **F1b** remains the high-risk flagship; only worth it if F2b-v2 / F3-full keep paying off.
|
| 42 |
+
|
| 43 |
+
## Meta
|
| 44 |
+
|
| 45 |
+
v2 = the clean contributions (method + law + label-free capabilities). v3 = the honest frontier:
|
| 46 |
+
the deep extensions are real but messy, and each result *characterizes the difficulty* rather than
|
| 47 |
+
clearing it. That is what an extended research program looks like — and the refinements (F1a's
|
| 48 |
+
sharpening, F3a's mechanism) are individually publishable even though the flagship pretraining
|
| 49 |
+
study remains a bet.
|
research_v3/f1a_f2a_results.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"F1a_whitening_sweep": {
|
| 3 |
+
"status": "NEGATIVE on premise (sharpens the negative result)",
|
| 4 |
+
"auroc_by_whitening": {"0.0": 0.862, "0.25": 0.871, "0.5": 0.876, "0.75": 0.859, "1.0": 0.849},
|
| 5 |
+
"bank_effrank_by_whitening": {"0.0": 318.1, "0.25": 482.6, "0.5": 637.6, "0.75": 737.3, "1.0": 768.0},
|
| 6 |
+
"drop_raw_to_white": 0.013, "premise_supported": false,
|
| 7 |
+
"finding": "Lesion localizability is ROBUST to whitening: AUROC stays 0.85-0.88 while the representation's effective rank rises 318 -> 768 (2.4x). It peaks at moderate whitening (0.876 @ w=0.5) and only mildly drops at full whitening (0.849). Pushing a representation toward maximal rank does NOT destroy localizability post-hoc.",
|
| 8 |
+
"sharpening": "This DISTINGUISHES two mechanisms that 'rank' conflates: (1) rank as a SELECTION objective (which tokens to keep) FAILS for rare signal (S1, closed-form, the covtoken floor); (2) rank as a representation SCALING (whitening the spectrum) is roughly NEUTRAL for localizability. The lever is WHICH directions carry lesion signal, not their relative scaling -- which is whitening-invariant. So the negative result is specifically about rank-based SELECTION / coverage objectives, NOT all rank pressure. Do not overgeneralize.",
|
| 9 |
+
"implication_for_F1b": "The cheap post-hoc proxy does NOT pre-validate concentration-preserving PRETRAINING. F1b tests a third, distinct thing -- how rank pressure during LEARNING shapes which features emerge -- which post-hoc whitening cannot proxy. F1b is therefore genuinely uncertain and higher-cost; treat as a deliberate go/no-go."
|
| 10 |
+
},
|
| 11 |
+
"F2a_alignment_surface": {
|
| 12 |
+
"status": "PASS (alignment theory generalizes S1)",
|
| 13 |
+
"high_snr_crossover_r_star": 8, "matches_S1_closed_form": true,
|
| 14 |
+
"crossover_r_star_by_amp": {"1.0": "deg", "1.5": "deg", "2.0": "deg", "3.0": "deg", "5.0": "deg", "8.0": 8, "15.0": 8},
|
| 15 |
+
"finding": "The alignment functional A(task_rank r, SNR) has two regimes: at HIGH SNR the crossover r*=m=8 (the S1 closed form); as SNR falls, concentration's advantage shrinks because the signal is no longer clearly top-energy (at low SNR both objectives lose the signal -- degenerate). So spanning/rank objectives are safe for a task ONLY when its signal is high-rank OR low-SNR-anyway; they are harmful precisely for CONCENTRATED, DISTINCT signal -- rare, salient pathology.",
|
| 16 |
+
"implication": "A predictive map of when representation-quality (rank/spanning) regularization helps vs hurts a downstream task, as a function of (task rank, SNR). Generalizes past medical imaging; basis for the F2b real benchmark."
|
| 17 |
+
},
|
| 18 |
+
"human_signoff": null
|
| 19 |
+
}
|
research_v3/f2b_f3a_results.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"F2b_real_law_validation": {
|
| 3 |
+
"status": "REFINEMENT (closed form is synthetic-idealized; qualitative law holds)",
|
| 4 |
+
"mean_m": 1.85, "mean_r": 1.73, "mean_r_over_m": 0.959,
|
| 5 |
+
"mean_concentration": 0.812, "mean_spanning_observed": 0.462,
|
| 6 |
+
"spearman_pred_vs_observed_spanning": -0.015, "fit_slope": 0.285, "law_holds_literally": false,
|
| 7 |
+
"finding": "The S1 closed form (spanning ~= r/m) does NOT transfer literally to real lesions. Real LIDC lesions are tiny (m~2) and near-full-rank AMONG THEMSELVES (r/m~0.96), so there is no r/m spread; and observed spanning (0.46) != predicted (0.96). The reason: a lesion's 2-3 tokens are internally diverse but LOW-rank relative to the ~193 background tokens, which steal the farthest-point budget. The relevant quantity is the lesion's rank RELATIVE TO BACKGROUND diversity, not its absolute rank.",
|
| 8 |
+
"what_holds": "The QUALITATIVE law is robust: concentration (0.81) beats spanning (0.46) by ~0.35 on real lesions -- rank/spanning objectives lose to concentration for real rare pathology. The exact (m-r)/m closed form is a clean-background idealization.",
|
| 9 |
+
"refines": "S1's law is directional and robust; its closed form is regime-specific (isolated signal subspace). For real high-dim backgrounds, replace r with (lesion rank / background rank)."
|
| 10 |
+
},
|
| 11 |
+
"F3a_ib_depth": {
|
| 12 |
+
"status": "REFINEMENT (invariance mechanism confirmed; spatial-info framing disconfirmed)",
|
| 13 |
+
"by_block_spatial_acc": {"1": 0.573, "2": 0.968, "3": 0.975, "6": 0.994, "12": 0.998},
|
| 14 |
+
"by_block_flip_invariance": {"1": 0.857, "3": 0.843, "6": 0.856, "8": 0.911, "12": 0.954},
|
| 15 |
+
"spearman_spatial_vs_auroc": -0.914, "spearman_invariance_vs_auroc": -0.937,
|
| 16 |
+
"finding": "VIEW-INVARIANCE rises monotonically with depth and STRONGLY anti-correlates with lesion localizability (rho -0.94): as features become invariant to augmentation (the self-distillation goal), they lose the fine discrimination small lesions need. BUT the spatial-position probe is near-perfect (0.97-0.998) at EVERY layer including the worst-localizing deepest ones (rho -0.91, opposite of the spatial-info-loss hypothesis). Position is trivially preserved (RoPE).",
|
| 17 |
+
"mechanism": "The mid-layer localizability peak is governed by the rise of VIEW-INVARIANCE (globalization), NOT by loss of spatial/positional information. What is traded away with depth is lesion-vs-normal LOCAL DISCRIMINATION, exchanged for augmentation-invariance -- not spatial encoding.",
|
| 18 |
+
"refines": "S2's mid-layer finding now has a mechanism: invariance pressure, measurable label-free as flip-invariance, predicts the depth at which localizability collapses (rho -0.94). The position probe is the wrong instrument (position is always encoded)."
|
| 19 |
+
},
|
| 20 |
+
"human_signoff": null
|
| 21 |
+
}
|
research_v3/f3_cross_objective.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"study": "F3 (decisive) — does invariance/globalization cause the mid-layer localizability collapse? Cross-objective.",
|
| 3 |
+
"status": "MECHANISM CONFIRMED (3 objectives) + clarifying surprise (MAE not separable)",
|
| 4 |
+
"domain_constant": "all backbones natural-image-trained; evaluated on LIDC => isolates OBJECTIVE, not domain",
|
| 5 |
+
"backbones": {
|
| 6 |
+
"DINOv2_selfdistillation": {"peak_auroc": 0.880, "peak_block": 2, "final_auroc": 0.617,
|
| 7 |
+
"auroc_by_block": [0.862,0.880,0.868,0.807,0.729,0.682,0.701,0.676,0.682,0.653,0.621,0.617],
|
| 8 |
+
"invariance_rise": 0.266, "spearman_inv_vs_auroc": -0.93,
|
| 9 |
+
"shape": "strong early peak, collapses with depth"},
|
| 10 |
+
"ViT_supervised": {"peak_auroc": 0.842, "peak_block": 1, "final_auroc": 0.658,
|
| 11 |
+
"auroc_by_block": [0.842,0.840,0.831,0.825,0.816,0.797,0.785,0.791,0.785,0.732,0.681,0.658],
|
| 12 |
+
"invariance_rise": 0.208, "spearman_inv_vs_auroc": -0.73,
|
| 13 |
+
"shape": "strong early peak, collapses with depth"},
|
| 14 |
+
"MAE_reconstruction": {"peak_auroc": 0.611, "peak_block": 1, "final_auroc": 0.568,
|
| 15 |
+
"auroc_by_block": [0.611,0.600,0.596,0.600,0.589,0.590,0.587,0.582,0.577,0.570,0.577,0.568],
|
| 16 |
+
"invariance_rise": 0.433, "spearman_inv_vs_auroc": 0.06,
|
| 17 |
+
"shape": "FLAT and LOW — never localizes"}
|
| 18 |
+
},
|
| 19 |
+
"reference_MedDINOv3_F3a": {"objective": "self-distillation (CT)", "peak_block": 3, "spearman_inv_vs_auroc": -0.937},
|
| 20 |
+
"findings": [
|
| 21 |
+
"DEPTH-EROSION IS REAL AND CROSS-OBJECTIVE among backbones that produce a label-free localizer: DINOv2 (rho -0.93), supervised ViT (rho -0.73), and MedDINOv3 (rho -0.94, F3a) all peak early/mid and erode with depth as features globalize (flip-invariance rises). Three distinct objectives, same pattern -> the mid-layer finding (S2) is not backbone-specific.",
|
| 22 |
+
"THE NAIVE HYPOTHESIS IS REFUTED: MAE (reconstruction) does NOT 'preserve' localizability -- it NEVER localizes (0.59 flat at every depth). Its high invariance-rise (+0.43) is decoupled from AUROC (rho +0.06) because there is no localizability to lose.",
|
| 23 |
+
"MASKED-RECONSTRUCTION FEATURES ARE NOT DENSITY-SEPARABLE FOR LESIONS at any depth, holding domain constant (DINOv2 0.88 vs MAE 0.59, both natural-trained). The label-free subspace method REQUIRES self-distillation or supervised features; reconstruction is the wrong pretext."
|
| 24 |
+
],
|
| 25 |
+
"implication_for_F1": "Redirected. The prescription is NOT 'pretrain with MAE' (MAE features aren't separable). Self-distillation builds a strong EARLY localizer then globalizes it away with depth; the lever is to PRESERVE the early-layer local structure at depth (or simply operate at the early/mid layer, per S2). Concentration-preserving pretraining should target the depth-globalization of a self-distillation backbone, not switch to reconstruction.",
|
| 26 |
+
"paper_claim": "Among ViTs that yield label-free lesion-separable features (self-distillation, supervised), localizability peaks early-mid and erodes with depth as representations globalize (rho -0.73 to -0.94 across three objectives); masked-reconstruction features are not lesion-separable at any depth. Choose the pretext and the layer accordingly.",
|
| 27 |
+
"human_signoff": null
|
| 28 |
+
}
|