Tri-Netra-AI / scripts /eval_ood_threshold_sweep_new_clfs.py
anannyavyas1's picture
Upload folder using huggingface_hub
1cf3825 verified
Raw
History Blame Contribute Delete
3.96 kB
"""Threshold sweep on the v8-retrained classifiers.
Hypothesis: dataset_v8 is 78% positive class, so binary cross-entropy
without pos_weight pushed the new classifiers toward predicting tumor.
The default 0.5 threshold inherits that bias. A higher threshold should
recover specificity without sacrificing too much recall.
Reuses scripts/eval_ood_classifiers_v8_retrained.py's per-image probs;
just sweeps the decision threshold.
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from scripts.eval_ood_cascade import _sess, _preprocess_clf, GT
from scripts.eval_ood_classifiers_v8_retrained import NEW_CLF_ONNX, NORMALIZE_IMAGENET
SAMPLES_DIR = ROOT / 'samples' / 'ood'
def classify_all(clfs, img):
out = {}
for name, sess in clfs.items():
chw = _preprocess_clf(img, NORMALIZE_IMAGENET[name])
logit = float(sess.run(None, {sess.get_inputs()[0].name: chw[None]})[0].reshape(-1)[0])
out[name] = 1.0 / (1.0 + np.exp(-logit))
return out
def main():
clfs = {n: _sess(p) for n, p in NEW_CLF_ONNX.items()}
samples = sorted(p for p in SAMPLES_DIR.rglob('*')
if p.suffix.lower() in ('.png', '.jpg', '.jpeg')
and p.parent.name in GT)
rows = []
t0 = time.perf_counter()
for p in samples:
img = Image.open(p)
gt = GT[p.parent.name]
probs = classify_all(clfs, img)
rows.append({'gt': gt, **{f'p_{k}': v for k, v in probs.items()}})
print(f'[done] {len(rows)} samples in {time.perf_counter()-t0:.0f}s\n')
print('='*78)
print('THRESHOLD SWEEP per classifier (recall / FPR)')
print('='*78)
THR = [0.30, 0.40, 0.50, 0.60, 0.70, 0.75, 0.80, 0.85, 0.90]
for clf in ('cnn', 'transfer', 'vit'):
pkey = f'p_{clf}'
print(f'\n {clf.upper()}:')
print(f' {"thr":>5s} {"recall":>7s} {"FPR":>7s} {"acc":>6s} {"F1":>6s}')
for t in THR:
TP = sum(1 for r in rows if r['gt']=='tumor' and r[pkey]>=t)
FN = sum(1 for r in rows if r['gt']=='tumor' and r[pkey]<t)
FP = sum(1 for r in rows if r['gt']=='no_tumor' and r[pkey]>=t)
TN = sum(1 for r in rows if r['gt']=='no_tumor' and r[pkey]<t)
recall = TP/(TP+FN) if TP+FN else 0
fpr = FP/(FP+TN) if FP+TN else 0
acc = (TP+TN)/len(rows)
f1 = 2*TP/(2*TP+FP+FN) if 2*TP+FP+FN else 0
print(f' {t:>5.2f} {recall:>7.0%} {fpr:>7.0%} {acc:>6.0%} {f1:>6.2f}')
# Find optimal threshold per classifier (max F1)
print('\n' + '='*78)
print('SUGGESTED OPERATING POINTS (max F1 from sweep)')
print('='*78)
for clf in ('cnn', 'transfer', 'vit'):
pkey = f'p_{clf}'
best_t, best_f1 = None, -1
for t in np.arange(0.10, 0.95, 0.02):
TP = sum(1 for r in rows if r['gt']=='tumor' and r[pkey]>=t)
FN = sum(1 for r in rows if r['gt']=='tumor' and r[pkey]<t)
FP = sum(1 for r in rows if r['gt']=='no_tumor' and r[pkey]>=t)
f1 = 2*TP/(2*TP+FP+FN) if 2*TP+FP+FN else 0
if f1 > best_f1:
best_f1 = f1; best_t = t
# Report at the best
TP = sum(1 for r in rows if r['gt']=='tumor' and r[pkey]>=best_t)
FN = sum(1 for r in rows if r['gt']=='tumor' and r[pkey]<best_t)
FP = sum(1 for r in rows if r['gt']=='no_tumor' and r[pkey]>=best_t)
TN = sum(1 for r in rows if r['gt']=='no_tumor' and r[pkey]<best_t)
recall = TP/(TP+FN); fpr = FP/(FP+TN); acc = (TP+TN)/len(rows)
print(f' {clf:10s} t={best_t:.2f} recall={recall:.0%} FPR={fpr:.0%} '
f'acc={acc:.0%} F1={best_f1:.2f}')
if __name__ == '__main__':
main()