Chucks90 commited on
Commit
6f64c8c
·
verified ·
1 Parent(s): 3df3d6a

G1c FINO-style adapter: metadata guidance offsets SSL-globalization directionally (+0.014) but no CI-significant recovery; F1b not de-risked at adapter scale (honest)

Browse files
RESEARCH_OVERVIEW.md CHANGED
@@ -59,13 +59,15 @@ geometry — no training, no metadata. It addresses none of covtoken's contribut
59
  localization, the rank-vs-concentration law, the conformal certificate, rare-structure focus).
60
  Three implications:
61
  - **Differentiate** in related work: frozen geometric probe + law + certificate, not adaptation.
62
- - **De-risks F1b**: G1a showed the eroded subspace cannot be recovered *training-free* (globalization
63
- is entangling). FINO is an existence proof that *with* a training signal one can "preserve
64
- informative, suppress spurious" factors so the practical concentration-preserving lever is
65
- **metadata-guided adaptation**, not steering.
66
- - **Complementary experiment** (new future direction): run the covtoken probe on a FINO-adapted
67
- backbone does metadata-guided adaptation preserve the mid-layer concentration subspace at depth
68
- and raise rare-signal separability? Bridges the two lines and rides their release.
 
 
69
 
70
  ## Reproduce
71
  All jobs in `jobs/` (PEP-723 uv scripts) run via `hf jobs uv run --flavor <t4-medium|t4-small|cpu>
 
59
  localization, the rank-vs-concentration law, the conformal certificate, rare-structure focus).
60
  Three implications:
61
  - **Differentiate** in related work: frozen geometric probe + law + certificate, not adaptation.
62
+ - **F1b lever TESTED at adapter scale (G1c), not de-risked.** No FINO checkpoint is released, so we
63
+ trained a label-free metadata-guided adapter (the trained counterpart to G1a). Result: SSL-only
64
+ adaptation *erodes* localizability (globalization, reproduced by a trained objective), and metadata
65
+ guidance *offsets* it directionally (+0.014 mean vs SSL) — but the gap is not CI-significant and it
66
+ does **not recover** the depth-eroded concentration subspace toward the mid-layer (0.866). So at
67
+ adapter scale with anatomical metadata, FINO's lever moves the needle in the right direction but
68
+ does not solve the erosion. F1b would need full-backbone capacity AND lesion-relevant metadata.
69
+ - **Complementary experiment, if a FINO checkpoint ships**: run the probe on a *full* FINO-adapted
70
+ backbone (more capacity than our adapter) — the open question G1c could only lower-bound.
71
 
72
  ## Reproduce
73
  All jobs in `jobs/` (PEP-723 uv scripts) run via `hf jobs uv run --flavor <t4-medium|t4-small|cpu>
jobs/g1c_fino_adapter_job.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """G1c — FINO-style metadata-guided adaptation: the TRAINED counterpart to G1a. HF Job (GPU).
9
+
10
+ FINO (Gardes et al. 2026, arXiv:2606.05107) adapts FMs label-free via metadata-guided SSL TRAINING;
11
+ no adapted checkpoint is released, so we test the MECHANISM at adapter scale. G1a showed the eroded
12
+ late-layer concentration subspace is NOT recoverable training-free. Here, with MedDINOv3 FROZEN, we
13
+ train a small residual-MLP adapter g (NO lesion labels) at a late layer under three conditions:
14
+ RAW : no adapter (baseline late-layer localizability)
15
+ SSL_only : augmentation-invariance objective only (the self-distillation pressure that globalizes)
16
+ FINO_style: SSL + METADATA guidance -- keep slice-position (continuous) and scan-id (discrete)
17
+ linearly decodable from pooled g(z) (preserve informative factors)
18
+ Probe: lesion density-AUROC on g(z). FINO-direction supported if FINO_style > SSL_only and recovers
19
+ toward the mid-layer target -> metadata-guided ADAPTATION succeeds where training-free steering
20
+ failed, de-risking F1b. Honest proxy: adapter-scale, anatomical metadata; not a FINO reproduction.
21
+ Labels eval-only. Emits G1C_RESULT.
22
+ """
23
+ from __future__ import annotations
24
+ import json, os, sys, time
25
+ from pathlib import Path
26
+ import numpy as np, torch, torch.nn as nn, torch.nn.functional as F
27
+ from PIL import Image
28
+ from scipy import stats
29
+ from sklearn.neighbors import NearestNeighbors
30
+ from huggingface_hub import hf_hub_download
31
+ sys.path.insert(0,"/mnt/processed/covtoken_code")
32
+ from dinov3.models.vision_transformer import vit_base # noqa: E402
33
+
34
+ BACKBONE_REPO="ricklisz123/MedDINOv3-ViTB-16-CT-3M"; MNT=Path("/mnt")
35
+ RAW_LIDC=MNT/"raw"/"lidc"; MASK_ROOT=MNT/"processed"/"lidc_v2"; OUT=MNT/"processed"/"covtoken"
36
+ N_PATCH,CLS_OFF,GRID=196,5,14
37
+ LAYERS=[int(x) for x in os.environ.get("LAYERS","8,11").split(",")] # blocks 9, 12 (eroded)
38
+ MID_TARGET=0.866 # block-3 multi-seed peak
39
+ BANK_SLICES=int(os.environ.get("BANK_SLICES","700")); EVAL_SLICES=int(os.environ.get("EVAL_SLICES","600"))
40
+ STEPS=int(os.environ.get("STEPS","400")); LMETA=float(os.environ.get("LMETA","1.0"))
41
+ CT_MEAN=np.array([0.485,0.456,0.406],np.float32); CT_STD=np.array([0.229,0.224,0.225],np.float32)
42
+ def log(m): print(f"[g1c] {m}", flush=True)
43
+
44
+
45
+ def load_backbone(device):
46
+ ck=hf_hub_download(BACKBONE_REPO,"model.pth",token=os.environ.get("HF_TOKEN"))
47
+ m=vit_base(drop_path_rate=0.0,layerscale_init=1e-5,n_storage_tokens=4,qkv_bias=False,mask_k_bias=True)
48
+ raw=torch.load(ck,map_location="cpu"); sd=raw.get("teacher",raw)
49
+ sd={(k[9:] if k.startswith("backbone.") else k):v for k,v in sd.items()}
50
+ m.load_state_dict(sd,strict=False); m.eval().to(device)
51
+ for p in m.parameters(): p.requires_grad_(False)
52
+ feats={}
53
+ for i,blk in enumerate(m.blocks):
54
+ def mk(i):
55
+ def h(_m,_i,out):
56
+ while isinstance(out,(list,tuple)): out=out[0]
57
+ feats[i]=out.detach()
58
+ return h
59
+ blk.register_forward_hook(mk(i))
60
+ return m,feats
61
+
62
+
63
+ def to_t(pil):
64
+ a=(np.asarray(pil.resize((224,224),Image.BILINEAR),np.float32)/255.0-CT_MEAN)/CT_STD
65
+ return torch.from_numpy(a).permute(2,0,1)
66
+
67
+
68
+ @torch.inference_mode()
69
+ def feats_at(model,feats,imgs,device):
70
+ model.forward_features(imgs.to(device,torch.float32))
71
+ return {L:feats[L][:,CLS_OFF:CLS_OFF+N_PATCH,:].float() for L in LAYERS} # GPU tensors
72
+
73
+
74
+ class Adapter(nn.Module):
75
+ def __init__(self,d=768,h=256):
76
+ super().__init__(); self.n=nn.LayerNorm(d); self.a=nn.Linear(d,h); self.b=nn.Linear(h,d)
77
+ nn.init.zeros_(self.b.weight); nn.init.zeros_(self.b.bias)
78
+ def forward(self,z): return z + self.b(F.gelu(self.a(self.n(z))))
79
+
80
+
81
+ def auroc(s,y):
82
+ s=np.asarray(s,float); y=np.asarray(y,int); pos,neg=y.sum(),len(y)-y.sum()
83
+ if pos==0 or neg==0: return float("nan")
84
+ r=stats.rankdata(s); return float((r[y==1].sum()-pos*(pos+1)/2)/(pos*neg))
85
+
86
+
87
+ def boot_ci(s,y,n=1000,seed=0):
88
+ rng=np.random.default_rng(seed); s=np.asarray(s); y=np.asarray(y); idx=np.arange(len(y)); out=[]
89
+ for _ in range(n):
90
+ b=rng.choice(idx,len(idx),replace=True); out.append(auroc(s[b],y[b]))
91
+ return float(np.nanpercentile(out,2.5)),float(np.nanpercentile(out,97.5))
92
+
93
+
94
+ def main():
95
+ t0=time.time(); device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
96
+ model,feats=load_backbone(device); rng=np.random.default_rng(0)
97
+ 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"]
98
+ # train images with metadata: zpos (continuous), scanid (discrete) -- NO lesion labels
99
+ scans=[];
100
+ for b in sorted(RAW_LIDC.glob("batch_*")):
101
+ for sd in b.iterdir():
102
+ if sd.is_dir() and scan_split.get(sd.name)=="train": scans.append(sd)
103
+ train=[] # (path, zpos, scanid)
104
+ for sid,sd in enumerate(scans):
105
+ sl=sorted(sd.glob("slice_*.png")); n=len(sl)
106
+ for j,p in enumerate(sl): train.append((p, j/max(1,n-1), sid))
107
+ sel=rng.choice(len(train),min(BANK_SLICES,len(train)),replace=False); train=[train[i] for i in sel]
108
+ n_scans=len(scans)
109
+ ev=[]
110
+ for cd in sorted((MASK_ROOT/"test").iterdir()):
111
+ npz=cd/"patch_masks.npz"
112
+ if cd.is_dir() and npz.exists():
113
+ pm=np.load(npz)["patch_masks"]
114
+ for idx in range(len(pm)): ev.append((cd/f"slice_{idx:04d}.png", pm[idx]))
115
+ ev=[ev[i] for i in rng.choice(len(ev),min(EVAL_SLICES,len(ev)),replace=False)]
116
+ log(f"device={device.type}; train={len(train)} ({n_scans} scans) eval={len(ev)} layers={[L+1 for L in LAYERS]} steps={STEPS}")
117
+
118
+ # precompute frozen features once: orig + intensity-jitter + hflip, per layer; + metadata
119
+ flip_idx=torch.tensor((np.arange(N_PATCH).reshape(GRID,GRID)[:,::-1]).reshape(-1).copy(),device=device)
120
+ Zo={L:[] for L in LAYERS}; Zj={L:[] for L in LAYERS}; Zf={L:[] for L in LAYERS}; ZP=[]; ID=[]
121
+ for i in range(0,len(train),48):
122
+ ch=train[i:i+48]; pil=[Image.open(p).convert("RGB") for p,_,_ in ch]
123
+ o=feats_at(model,feats,torch.stack([to_t(im) for im in pil]),device)
124
+ jit=[Image.fromarray(np.clip(np.asarray(im,np.float32)*rng.uniform(0.85,1.15)+rng.uniform(-12,12),0,255).astype(np.uint8)) for im in pil]
125
+ j=feats_at(model,feats,torch.stack([to_t(im) for im in jit]),device)
126
+ fl=feats_at(model,feats,torch.stack([to_t(im.transpose(Image.FLIP_LEFT_RIGHT)) for im in pil]),device)
127
+ for L in LAYERS: Zo[L].append(o[L].cpu()); Zj[L].append(j[L].cpu()); Zf[L].append(fl[L].cpu())
128
+ ZP+=[z for _,z,_ in ch]; ID+=[s for _,_,s in ch]
129
+ Zo={L:torch.cat(v).to(device) for L,v in Zo.items()}; Zj={L:torch.cat(v).to(device) for L,v in Zj.items()}; Zf={L:torch.cat(v).to(device) for L,v in Zf.items()}
130
+ zpos=torch.tensor(ZP,dtype=torch.float32,device=device); sid=torch.tensor(ID,dtype=torch.long,device=device)
131
+ log(f"precomputed features in {time.time()-t0:.0f}s")
132
+ # eval features (orig only) per layer + labels
133
+ Ze={L:[] for L in LAYERS}; lab=[]
134
+ for i in range(0,len(ev),48):
135
+ ch=ev[i:i+48]; o=feats_at(model,feats,torch.stack([to_t(Image.open(p).convert("RGB")) for p,_ in ch]),device)
136
+ for L in LAYERS: Ze[L].append(o[L].cpu())
137
+ lab.append(np.stack([pm for _,pm in ch]).reshape(-1))
138
+ Ze={L:torch.cat(v).to(device) for L,v in Ze.items()}; lab=np.concatenate(lab)
139
+
140
+ def train_adapter(L, use_meta):
141
+ g=Adapter().to(device); pos_h=nn.Linear(768,1).to(device); id_h=nn.Linear(768,n_scans).to(device)
142
+ params=list(g.parameters())+(list(pos_h.parameters())+list(id_h.parameters()) if use_meta else [])
143
+ opt=torch.optim.Adam(params,lr=2e-3,weight_decay=1e-4); N=Zo[L].shape[0]
144
+ for step in range(STEPS):
145
+ bi=torch.randint(0,N,(64,),device=device)
146
+ zo=g(Zo[L][bi]); zj=g(Zj[L][bi]); zf=g(Zf[L][bi])
147
+ # SSL: invariance to jitter (same positions) + equivariance to flip (mirrored positions)
148
+ l_ssl=(1-F.cosine_similarity(zo,zj,dim=2)).mean() + (1-F.cosine_similarity(zo,zf[:,flip_idx,:],dim=2)).mean()
149
+ loss=l_ssl
150
+ if use_meta:
151
+ pool=zo.mean(1)
152
+ l_meta=F.mse_loss(pos_h(pool).squeeze(-1),zpos[bi]) + F.cross_entropy(id_h(pool),sid[bi])
153
+ loss=l_ssl+LMETA*l_meta
154
+ opt.zero_grad(); loss.backward(); opt.step()
155
+ g.eval(); return g
156
+
157
+ def probe(L, g):
158
+ with torch.no_grad():
159
+ ref=(g(Zo[L]) if g else Zo[L]).reshape(-1,768).cpu().numpy()
160
+ ref=ref[rng.choice(len(ref),min(50000,len(ref)),replace=False)]
161
+ nn_=NearestNeighbors(n_neighbors=11).fit(ref)
162
+ ze=(g(Ze[L]) if g else Ze[L]).reshape(-1,768).cpu().numpy()
163
+ d,_=nn_.kneighbors(ze); sc=d[:,1:].mean(1)
164
+ return auroc(sc,lab), boot_ci(sc,lab)
165
+
166
+ res={"backbone":"MedDINOv3","mid_layer_target":MID_TARGET,"steps":STEPS,"lambda_meta":LMETA,"by_layer":{}}
167
+ for L in LAYERS:
168
+ raw_a,raw_ci=probe(L,None)
169
+ g_ssl=train_adapter(L,use_meta=False); ssl_a,ssl_ci=probe(L,g_ssl)
170
+ g_fino=train_adapter(L,use_meta=True); fin_a,fin_ci=probe(L,g_fino)
171
+ row={"raw":{"auroc":round(raw_a,4),"ci":[round(x,4) for x in raw_ci]},
172
+ "ssl_only":{"auroc":round(ssl_a,4),"ci":[round(x,4) for x in ssl_ci]},
173
+ "fino_style":{"auroc":round(fin_a,4),"ci":[round(x,4) for x in fin_ci]},
174
+ "fino_minus_ssl":round(fin_a-ssl_a,4),"fino_minus_raw":round(fin_a-raw_a,4)}
175
+ res["by_layer"][str(L+1)]=row
176
+ log(f" block {L+1}: raw {raw_a:.4f} | ssl_only {ssl_a:.4f} | FINO {fin_a:.4f} | FINO-ssl {fin_a-ssl_a:+.4f} | FINO-raw {fin_a-raw_a:+.4f}")
177
+
178
+ # verdict
179
+ deltas=[res["by_layer"][str(L+1)]["fino_minus_ssl"] for L in LAYERS]
180
+ fino_beats_ssl=all(res["by_layer"][str(L+1)]["fino_style"]["ci"][0] > res["by_layer"][str(L+1)]["ssl_only"]["auroc"] for L in LAYERS)
181
+ res["verdict"]={
182
+ "fino_beats_ssl_all_layers_ci": bool(fino_beats_ssl),
183
+ "mean_fino_minus_ssl": round(float(np.mean(deltas)),4),
184
+ "recovers_toward_mid": bool(any(res["by_layer"][str(L+1)]["fino_style"]["auroc"]>=res["by_layer"][str(L+1)]["raw"]["auroc"]+0.03 for L in LAYERS)),
185
+ "interpretation": ("FINO-style metadata-guided adaptation beats pure-SSL adaptation"
186
+ if np.mean(deltas)>0 else "metadata guidance does NOT beat SSL-only at adapter scale")
187
+ + f" (mean FINO-ssl {np.mean(deltas):+.4f}); contrast with G1a (training-free steering, no recovery)."}
188
+ res["elapsed_s"]=round(time.time()-t0,1)
189
+ OUT.mkdir(parents=True,exist_ok=True); (OUT/"g1c_fino_adapter.json").write_text(json.dumps(res,indent=2))
190
+ print("G1C_RESULT "+json.dumps(res),flush=True)
191
+
192
+
193
+ if __name__=="__main__": main()
research_specs/RESEARCH_SPEC_v4.md CHANGED
@@ -26,6 +26,23 @@ like mid. A deployable steering module for heads forced to use late features.
26
 
27
  ---
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  ## G2 — Universality: the principle is not about lesions
30
 
31
  **Question.** Does the label-free concentration-subspace readout localize RARE structure in a
 
26
 
27
  ---
28
 
29
+ ## G1c — FINO-style metadata-guided adaptation (the trained counterpart to G1a) ★ built
30
+
31
+ **Context.** FINO (Gardès et al. 2026, arXiv:2606.05107) adapts FMs label-free via metadata-guided
32
+ SSL *training*. No adapted checkpoint is released (FINO branch = code/recipes only), so full FINO is
33
+ a pretraining run, not a job. We test the *mechanism* at adapter scale: G1a showed the eroded late-
34
+ layer subspace is NOT recoverable training-free. Does a TRAINED, label-free, metadata-guided adapter
35
+ recover it — and does FINO's metadata lever beat pure-SSL adaptation?
36
+
37
+ **G1c.** Freeze MedDINOv3. At a late layer, train a small residual-MLP adapter g_theta (no lesion
38
+ labels) under three conditions: (RAW) none; (SSL-only) augmentation-invariance objective — the pure
39
+ self-distillation pressure that globalizes; (FINO-style) SSL + metadata guidance (keep slice-
40
+ position [continuous] and scan-id [discrete] linearly decodable from pooled g(z) — preserve
41
+ informative factors). Probe lesion density-AUROC on g(z). PASS-for-FINO-direction if FINO-style >
42
+ SSL-only (CI excludes 0) and recovers toward the mid-layer target -> metadata-guided adaptation
43
+ succeeds where training-free steering failed, de-risking F1b. Honest proxy: adapter-scale, anatomical
44
+ metadata; not a FINO reproduction.
45
+
46
  ## G2 — Universality: the principle is not about lesions
47
 
48
  **Question.** Does the label-free concentration-subspace readout localize RARE structure in a
research_v4/g1c_fino_adapter.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "study": "G1c — FINO-style metadata-guided adaptation (trained counterpart to G1a)",
3
+ "status": "MEASURED / WEAK-NEGATIVE (directional mechanism signal; strong claim not supported)",
4
+ "context": "FINO (arXiv:2606.05107) adapts FMs label-free via metadata-guided SSL training; no checkpoint released, so tested at adapter scale. Question: does a TRAINED, label-free, metadata-guided adapter recover the eroded late-layer concentration subspace that training-free steering (G1a) could not?",
5
+ "mid_layer_target": 0.866,
6
+ "by_layer": {
7
+ "block9": {"raw": 0.7145, "ssl_only": 0.6890, "fino_style": 0.7080, "fino_minus_ssl": 0.0190, "fino_minus_raw": -0.0065},
8
+ "block12": {"raw": 0.6513, "ssl_only": 0.6465, "fino_style": 0.6545, "fino_minus_ssl": 0.0079, "fino_minus_raw": 0.0032}
9
+ },
10
+ "verdict": {
11
+ "fino_beats_ssl_ci_significant": false,
12
+ "mean_fino_minus_ssl": 0.0135,
13
+ "recovers_toward_mid": false
14
+ },
15
+ "findings": [
16
+ "MECHANISM CONFIRMED DIRECTIONALLY: pure-SSL adaptation (augmentation-invariance only) HURT localizability at block 9 (0.715 -> 0.689) -- the globalization/invariance erosion, now reproduced by a TRAINED objective. FINO-style metadata guidance OFFSET this (+0.019 vs ssl), recovering back toward raw.",
17
+ "STRONG CLAIM NOT SUPPORTED: the FINO-vs-SSL gap is not CI-significant, and neither layer recovers toward the mid-layer concentration subspace (0.866). Metadata guidance at adapter scale mostly AVOIDS the SSL-induced loss rather than recovering the depth-eroded subspace.",
18
+ "CONSISTENT WITH G1a: the depth-erosion is robust even to a trained signal at adapter scale -- reinforcing that globalization is an entangling transformation, and that the deployment answer remains 'read the mid layer' (S2)."
19
+ ],
20
+ "honest_caveats": [
21
+ "Adapter-scale (residual MLP at one layer) is NOT full-backbone FINO pretraining; FINO adapts the whole backbone with far more capacity to reshape representations -- this is a lower bound on FINO's potential.",
22
+ "Anatomical metadata (slice-position, scan-id) captures anatomy, not pathology; FINO's premise needs metadata that captures the TASK-relevant factor. Lesion-relevant weak signal could do better."
23
+ ],
24
+ "implication_for_F1b": "NOT de-risked at this scale. F1b (concentration-preserving adaptation) would need (a) full-backbone capacity (not an adapter) AND (b) lesion-relevant metadata/weak supervision, not anatomical metadata. The bar for F1b is now better understood and higher than the FINO abstract suggested for rare-pathology localization specifically.",
25
+ "human_signoff": null
26
+ }