SpiceNet / code /eval_confusion.py
Noushad999's picture
Upload folder using huggingface_hub
1ea7ba6 verified
Raw
History Blame Contribute Delete
4.98 kB
"""
Rich cross-source eval — paper-grade metrics beyond top-1.
Same 2x2 protocol as eval_shortcut_test.py but for each of the 4 cells also
computes macro-F1 and the full confusion matrix, and dumps per-sample
correctness for BOTH test sets (so bootstrap covers both directions). Reads
the overlap checkpoints; runs on standard OR dedup manifests.
python eval_confusion.py --ss_ckpt .../overlap_ss_s42/p1_best.pth \
--in_ckpt .../overlap_indian_s42/p1_best.pth --out_suffix s42
GPU eval-only (no training). Run after run_q1_anchor.py.
"""
import sys, os, argparse, json
from pathlib import Path
_base = "/mnt/d/SpiceNet" if os.path.exists("/mnt/d/SpiceNet") else "D:/SpiceNet"
sys.path.insert(0, _base)
import numpy as np
import torch
from torch.utils.data import DataLoader
from sklearn.metrics import f1_score, confusion_matrix
import config
from src.model import SpiceFusionNet
from src.dataset import SpiceDataset, get_val_transform, load_manifest_splits
ROOT = Path(_base)
SS_MANIFEST = str(ROOT / "outputs" / "manifest_overlap_ss.json")
IN_MANIFEST = str(ROOT / "outputs" / "manifest_overlap_indian.json")
def _loader(paths, labels):
ds = SpiceDataset(paths, labels, get_val_transform(), multimodal=False)
return DataLoader(ds, batch_size=64, num_workers=2, pin_memory=True)
@torch.no_grad()
def _eval(model, loader, device):
preds, gts = [], []
for imgs, tex, col, labels in loader:
logits = model.forward_image(imgs.to(device))
preds.extend(logits.argmax(1).cpu().tolist()); gts.extend(labels.tolist())
return np.asarray(gts), np.asarray(preds)
def _load(ckpt, n, device):
m = SpiceFusionNet(num_classes=n).to(device)
ck = torch.load(ckpt, map_location=device, weights_only=False)
m.load_state_dict(ck["model_state"]); m.eval()
return m
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--ss_ckpt", required=True)
ap.add_argument("--in_ckpt", required=True)
ap.add_argument("--ss_manifest", default=SS_MANIFEST)
ap.add_argument("--in_manifest", default=IN_MANIFEST)
ap.add_argument("--out_suffix", required=True)
args = ap.parse_args()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
ss_splits, classes = load_manifest_splits(args.ss_manifest)
in_splits, in_classes = load_manifest_splits(args.in_manifest)
assert classes == in_classes, "manifests must share classes"
n = len(classes)
labels_range = list(range(n))
ss_loader = _loader(*ss_splits["test"])
in_loader = _loader(*in_splits["test"])
ss_model = _load(args.ss_ckpt, n, device)
in_model = _load(args.in_ckpt, n, device)
results, preds_store = {}, {}
for mtag, model in (("ss", ss_model), ("in", in_model)):
row = {}
for ttag, loader in (("ss", ss_loader), ("in", in_loader)):
yt, yp = _eval(model, loader, device)
preds_store[(mtag, ttag)] = (yt, yp)
row[ttag] = {
"n": int(len(yt)), "acc": float((yt == yp).mean()),
"macro_f1": float(f1_score(yt, yp, average="macro", labels=labels_range, zero_division=0)),
"confusion": confusion_matrix(yt, yp, labels=labels_range).tolist(),
"per_class": {classes[c]: {"n": int((yt == c).sum()),
"acc": float((yp[yt == c] == c).mean()) if (yt == c).any() else 0.0}
for c in labels_range},
}
results[mtag] = row
sw, sc = results["ss"]["ss"]["acc"], results["ss"]["in"]["acc"]
iw, ic = results["in"]["in"]["acc"], results["in"]["ss"]["acc"]
results["analysis"] = {"ss_shortcut_tax_pp": round((sw - sc) * 100, 2),
"in_shortcut_tax_pp": round((iw - ic) * 100, 2),
"avg_shortcut_tax_pp": round(((sw - sc) + (iw - ic)) * 50, 2)}
results["classes"] = classes
# per-sample correctness for BOTH directions (bootstrap both ways)
yt_ss = preds_store[("ss", "ss")][0]; yt_in = preds_store[("ss", "in")][0]
results["per_sample"] = {
"ss_test": {"ss_trained_correct": (preds_store[("ss", "ss")][1] == yt_ss).astype(int).tolist(),
"in_trained_correct": (preds_store[("in", "ss")][1] == yt_ss).astype(int).tolist()},
"in_test": {"ss_trained_correct": (preds_store[("ss", "in")][1] == yt_in).astype(int).tolist(),
"in_trained_correct": (preds_store[("in", "in")][1] == yt_in).astype(int).tolist()},
}
out = ROOT / "outputs" / f"shortcut_evidence_{args.out_suffix}.json"
json.dump(results, open(out, "w"), indent=2)
print(f"acc: SS/SS={sw:.4f} SS/IN={sc:.4f} IN/SS={ic:.4f} IN/IN={iw:.4f} | "
f"macroF1 IN/SS={results['in']['ss']['macro_f1']:.4f}")
print(f"saved -> {out}")
if __name__ == "__main__":
main()