SpiceNet / code /analyze_confusion.py
Noushad999's picture
Upload folder using huggingface_hub
1ea7ba6 verified
Raw
History Blame Contribute Delete
5.11 kB
"""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))
# Find all off-diagonal errors (i, j with i != j)
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}")
# Symmetric confusion: cm[i,j] + cm[j,i]
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)
# Compare with the prior HARD_NEG_PAIRS
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})")
# Save report
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}")
# Pretty CM heatmap (counts + percentages)
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()