Tri-Netra-AI / scripts /eval_ood_v9b_jepa.py
anannyavyas1's picture
Upload folder using huggingface_hub
1cf3825 verified
Raw
History Blame Contribute Delete
8.96 kB
"""Thorough OOD evaluation of v9b normative JEPA + conformal calibration.
Pipeline per image:
1. Resize to 256x256, RGB, [0,1]
2. JEPA prediction_error_map(x) — for each of the 256 patches, mask it
out, predict its latent from the other 255, measure smooth-L1
residual. Returns (256, 256) anomaly map.
3. Per-image score = 95th percentile of all pixel residuals (matches
what conformal calibration used).
4. Conformal verdict: per_image_score > q -> anomaly (tumor).
q=0.308 loaded from v9b_conformal.json (alpha=0.10, empirical
coverage 0.90 on 17,487 healthy calibration samples).
Compares v9b verdict against:
- v8 segmentation alone @ threshold 0.20 (the current production baseline)
- the 4 historical classifier sets (OLD, v8-RAW, v8-BAL, v8-MVMM)
via per-classifier accuracy already in our earlier audit CSVs.
Outputs:
- per-image table (csv)
- per-source aggregate
- 4-policy comparison table for the executive summary
"""
from __future__ import annotations
import csv
import json
import sys
import time
from pathlib import Path
import numpy as np
import torch
from PIL import Image
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from src.research.jepa import IJEPAModel
from scripts.eval_ood_cascade import (
SEG_ONNX, MIN_TUMOR_AREA, _sess, _preprocess_seg, seg_tta, GT,
)
JEPA_CKPT = ROOT / 'v9b_artifacts' / 'v9b_jepa' / 'last.pt'
CONFORMAL_JSON = ROOT / 'v9b_artifacts' / 'v9b_conformal.json'
SAMPLES_DIR = ROOT / 'samples' / 'ood'
IMAGE_SIZE = 256
def load_jepa(device: str = 'cuda') -> IJEPAModel:
ckpt = torch.load(str(JEPA_CKPT), map_location=device, weights_only=False)
a = ckpt.get('args') or {}
model = IJEPAModel(
image_size=a.get('image_size', 256),
patch_size=a.get('patch_size', 16),
in_chans=a.get('in_chans', 3),
embed_dim=a.get('embed_dim', 384),
depth=a.get('depth', 12),
heads=a.get('heads', 6),
)
sd = ckpt.get('model_state_dict') or ckpt
miss, unexp = model.load_state_dict(sd, strict=False)
if miss or unexp:
print(f' [warn] checkpoint load: missing={len(miss)} unexpected={len(unexp)}')
model = model.to(device).eval()
return model
def jepa_anomaly_map(model: IJEPAModel, img_pil: Image.Image, device: str = 'cuda') -> np.ndarray:
img = img_pil.convert('RGB').resize((IMAGE_SIZE, IMAGE_SIZE), Image.BILINEAR)
arr = np.asarray(img, dtype=np.float32) / 255.0
x = torch.from_numpy(arr.transpose(2, 0, 1)).unsqueeze(0).to(device)
with torch.no_grad():
emap = model.prediction_error_map(x) # (1, 1, IMAGE_SIZE, IMAGE_SIZE)
return emap.squeeze().cpu().numpy()
def main():
if not JEPA_CKPT.exists():
sys.exit(f'missing {JEPA_CKPT}')
if not CONFORMAL_JSON.exists():
sys.exit(f'missing {CONFORMAL_JSON}')
conformal = json.loads(CONFORMAL_JSON.read_text())
q = float(conformal['q'])
alpha = float(conformal['alpha'])
print(f'[init] conformal q={q:.4f} alpha={alpha} (target coverage = {1-alpha:.0%})')
print(f'[init] calibrated on {conformal["report"]["n_calib"]} healthy samples, '
f'empirical_coverage={conformal["report"]["empirical_coverage"]:.4f}')
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'[init] device={device}'
+ (f' ({torch.cuda.get_device_name(0)})' if device == 'cuda' else ''))
model = load_jepa(device)
seg = _sess(SEG_ONNX)
samples = sorted(p for p in SAMPLES_DIR.rglob('*')
if p.suffix.lower() in ('.png', '.jpg', '.jpeg')
and p.parent.name in GT)
print(f'[init] {len(samples)} OOD samples\n')
rows = []
t0 = time.perf_counter()
last_print = t0
for i, p in enumerate(samples):
img = Image.open(p)
gt = GT[p.parent.name]
# v9b JEPA + conformal
emap = jepa_anomaly_map(model, img, device)
p95 = float(np.percentile(emap, 95))
max_err = float(emap.max())
mean_err = float(emap.mean())
# Anomaly mask (pixels with error > q)
ano_mask = (emap > q).astype(np.uint8)
ano_area = int(ano_mask.sum())
v9b_verdict = 'TUMOR' if p95 > q else 'no_tumor'
# v8 segmentation comparison
prob = seg_tta(seg, _preprocess_seg(img))
v8_area = int((prob >= 0.20).sum())
v8_verdict = 'TUMOR' if v8_area >= MIN_TUMOR_AREA else 'no_tumor'
rows.append({
'source': p.parent.name,
'file': p.name,
'gt': gt,
'v9b_p95': round(p95, 4),
'v9b_max': round(max_err, 4),
'v9b_mean': round(mean_err, 4),
'v9b_ano_area': ano_area,
'v9b_verdict': v9b_verdict,
'v8_area_020': v8_area,
'v8_verdict': v8_verdict,
})
if time.perf_counter() - last_print > 20:
last_print = time.perf_counter()
print(f' [{i+1}/{len(samples)}] elapsed={time.perf_counter()-t0:.0f}s')
elapsed = time.perf_counter() - t0
print(f'\n[done] {len(rows)} samples in {elapsed:.0f}s ({elapsed/len(rows):.2f}/sample)\n')
# ======================== aggregate ============================
def _stats(rs, col):
TP = sum(1 for r in rs if r['gt']=='tumor' and r[col]=='TUMOR')
FN = sum(1 for r in rs if r['gt']=='tumor' and r[col]=='no_tumor')
FP = sum(1 for r in rs if r['gt']=='no_tumor' and r[col]=='TUMOR')
TN = sum(1 for r in rs if r['gt']=='no_tumor' and r[col]=='no_tumor')
recall = TP/(TP+FN) if TP+FN else 0
fpr = FP/(FP+TN) if FP+TN else 0
acc = (TP+TN)/(TP+FN+FP+TN)
f1 = 2*TP/(2*TP+FP+FN) if 2*TP+FP+FN else 0
return TP, FN, FP, TN, recall, fpr, acc, f1
print('='*82)
print('v9b NORMATIVE JEPA + CONFORMAL — OOD SCORECARD')
print('='*82)
for col, label in (('v9b_verdict', 'v9b JEPA + conformal (q=0.308)'),
('v8_verdict', 'v8 segmentation @ 0.20 (baseline)')):
TP, FN, FP, TN, re, fp, acc, f1 = _stats(rows, col)
print(f'\n {label}')
print(f' TP={TP:2d} FN={FN:2d} FP={FP:2d} TN={TN:2d}')
print(f' recall={re:.0%} FPR={fp:.0%} accuracy={acc:.0%} F1={f1:.2f}')
# ======================== per-source ===========================
print('\n' + '='*82)
print('PER-SOURCE BREAKDOWN')
print('='*82)
by_src = {}
for r in rows:
by_src.setdefault(r['source'], []).append(r)
print(f'\n{"source":48s} GT n v9b_recall/FPR v8_recall/FPR')
for src in sorted(by_src):
rs = by_src[src]; gt = rs[0]['gt']; n = len(rs)
v9b_hits = sum(1 for r in rs if r['v9b_verdict']=='TUMOR')
v8_hits = sum(1 for r in rs if r['v8_verdict']=='TUMOR')
if gt == 'tumor':
print(f' {src:46s} pos {n:3d} recall={v9b_hits/n:.0%}'.ljust(76)
+ f' recall={v8_hits/n:.0%}')
else:
print(f' {src:46s} neg {n:3d} FPR={v9b_hits/n:.0%}'.ljust(76)
+ f' FPR={v8_hits/n:.0%}')
# ======================== 4-policy comparison =================
print('\n' + '='*82)
print('EXECUTIVE SUMMARY — every policy we have measured to date')
print('='*82)
# Historical numbers from the audit scripts we already ran
OLD = {'recall_range': '28-42%', 'fpr_range': '8-58%'}
BAL = {'recall_range': '31-44%', 'fpr_range': '0%'}
MVMM = {'recall_range': '25-47%', 'fpr_range': '0%'}
TP, FN, FP, TN, re_v9b, fp_v9b, acc_v9b, f1_v9b = _stats(rows, 'v9b_verdict')
_, _, _, _, re_v8, fp_v8, acc_v8, _ = _stats(rows, 'v8_verdict')
print(f'\n {"policy":40s} recall FPR accuracy')
print(f' {"OLD 3 classifiers (Kaggle-only)":40s} {OLD["recall_range"]:>10s} {OLD["fpr_range"]:>9s} 31-54%')
print(f' {"v8-BAL 3 classifiers (+OpenNeuro)":40s} {BAL["recall_range"]:>10s} {BAL["fpr_range"]:>9s} 46-58%')
print(f' {"v8-MVMM 3 classifiers (+multi-view)":40s} {MVMM["recall_range"]:>10s} {MVMM["fpr_range"]:>9s} 44-60%')
print(f' {"v8 segmentation only @ 0.20":40s} {re_v8:>10.0%} {fp_v8:>9.0%} {acc_v8:>8.0%}')
print(f' {"v9b JEPA + conformal":40s} {re_v9b:>10.0%} {fp_v9b:>9.0%} {acc_v9b:>8.0%} <-- NEW')
# ======================== persist ==============================
out_csv = SAMPLES_DIR / 'eval_v9b_jepa_results.csv'
fields = list(rows[0].keys())
with out_csv.open('w', newline='', encoding='utf-8') as f:
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for r in rows:
w.writerow(r)
print(f'\n[csv] {out_csv}')
if __name__ == '__main__':
main()