| """Per-class confusion analysis and hard-negative pair detection.
|
|
|
| Compares to config.HARD_NEG_PAIRS prior to validate the dataset's known
|
| confusable pairs.
|
| """
|
| import sys, os
|
| sys.path.insert(0, "/mnt/d/SpiceNet" if os.path.exists("/mnt/d/SpiceNet") else "D:/SpiceNet")
|
|
|
| import json
|
| import numpy as np
|
| import torch
|
| import matplotlib
|
| matplotlib.use("Agg")
|
| import matplotlib.pyplot as plt
|
| from sklearn.metrics import confusion_matrix, classification_report
|
|
|
| import config
|
| from src.dataset import get_dataloaders
|
| from src.model import load_checkpoint
|
|
|
|
|
| @torch.no_grad()
|
| def collect_preds(model, loader, device, mode="fusion"):
|
| model.eval()
|
| y_true, y_pred = [], []
|
| for imgs, tex, col, lbl in loader:
|
| imgs = imgs.to(device)
|
| if mode == "fusion":
|
| tex, col = tex.to(device), col.to(device)
|
| logits, _ = model.forward_fusion(imgs, tex, col)
|
| else:
|
| logits = model.forward_image(imgs)
|
| y_true.extend(lbl.tolist())
|
| y_pred.extend(logits.argmax(1).cpu().tolist())
|
| return np.array(y_true), np.array(y_pred)
|
|
|
|
|
| def main():
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| model, *_ = load_checkpoint(str(config.CHECKPOINT_DIR / "best.pth"), device)
|
|
|
| _, _, test_loader, _, _ = get_dataloaders(multimodal=True)
|
| y_true, y_pred = collect_preds(model, test_loader, device)
|
|
|
| cm = confusion_matrix(y_true, y_pred, labels=list(range(len(config.CLASSES))))
|
| print("\nConfusion matrix counts:")
|
| print(" " + " ".join(f"{c[:4]:>5}" for c in config.CLASSES))
|
| for i, row in enumerate(cm):
|
| print(f"{config.CLASSES[i][:4]:>4} " + " ".join(f"{v:>5d}" for v in row))
|
|
|
|
|
| off = []
|
| for i in range(cm.shape[0]):
|
| for j in range(cm.shape[1]):
|
| if i != j and cm[i, j] > 0:
|
| off.append((cm[i, j], i, j))
|
| off.sort(reverse=True)
|
|
|
| print(f"\nTotal misclassifications: {sum(c for c,_,_ in off)} / {cm.sum()}")
|
| print("\nTop confusion pairs (true -> pred):")
|
| for cnt, i, j in off[:10]:
|
| marker = " ⚠ PRIOR" if {i, j} in [set(p) for p in config.HARD_NEG_PAIRS] else ""
|
| print(f" {cnt:>3d} {config.CLASSES[i]:>14s} -> {config.CLASSES[j]:<14s}{marker}")
|
|
|
|
|
| n = cm.shape[0]
|
| sym = []
|
| for i in range(n):
|
| for j in range(i + 1, n):
|
| both = cm[i, j] + cm[j, i]
|
| if both > 0:
|
| sym.append((both, i, j))
|
| sym.sort(reverse=True)
|
|
|
|
|
| prior_set = set(frozenset(p) for p in config.HARD_NEG_PAIRS)
|
| print("\nPrior hard-negative pairs (from config.HARD_NEG_PAIRS):")
|
| for i, j in config.HARD_NEG_PAIRS:
|
| pair_cnt = int(cm[i, j] + cm[j, i])
|
| print(f" {config.CLASSES[i]} <-> {config.CLASSES[j]}: {pair_cnt} confusions")
|
|
|
| print("\nMost-confused pairs in practice (top 5):")
|
| for cnt, i, j in sym[:5]:
|
| in_prior = frozenset((i, j)) in prior_set
|
| print(f" {config.CLASSES[i]} <-> {config.CLASSES[j]}: {cnt} (in prior: {in_prior})")
|
|
|
|
|
| rep = classification_report(y_true, y_pred,
|
| target_names=config.CLASSES, digits=4,
|
| output_dict=True)
|
| out = {
|
| "confusion_matrix": cm.tolist(),
|
| "classes": config.CLASSES,
|
| "prior_hard_neg_pairs": config.HARD_NEG_PAIRS,
|
| "top_confusion_pairs": [(int(c), config.CLASSES[i], config.CLASSES[j]) for c, i, j in off[:10]],
|
| "symmetric_confusion_pairs": [(int(c), config.CLASSES[i], config.CLASSES[j]) for c, i, j in sym[:10]],
|
| "classification_report": rep,
|
| }
|
| out_path = config.OUTPUT_DIR / "confusion_analysis.json"
|
| with open(out_path, "w") as f:
|
| json.dump(out, f, indent=2)
|
| print(f"\nSaved -> {out_path}")
|
|
|
|
|
| fig, ax = plt.subplots(figsize=(11, 9))
|
| cm_pct = cm.astype(float) / cm.sum(axis=1, keepdims=True) * 100
|
| im = ax.imshow(cm_pct, cmap="Blues", vmin=0, vmax=100)
|
| ax.set_xticks(range(len(config.CLASSES)))
|
| ax.set_yticks(range(len(config.CLASSES)))
|
| ax.set_xticklabels(config.CLASSES, rotation=45, ha="right")
|
| ax.set_yticklabels(config.CLASSES)
|
| ax.set_xlabel("Predicted"); ax.set_ylabel("True")
|
| ax.set_title("Confusion matrix (strong-aug, % per row)")
|
| for i in range(cm.shape[0]):
|
| for j in range(cm.shape[1]):
|
| v = cm[i, j]
|
| if v > 0:
|
| color = "white" if cm_pct[i, j] > 50 else "black"
|
| ax.text(j, i, f"{v}", ha="center", va="center", color=color, fontsize=8)
|
| plt.colorbar(im, ax=ax, label="% of true class")
|
| plt.tight_layout()
|
| plt.savefig(config.OUTPUT_DIR / "confusion_matrix_annotated.png", dpi=150)
|
| plt.close()
|
| print(f"Saved -> {config.OUTPUT_DIR / 'confusion_matrix_annotated.png'}")
|
|
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|