AIP-AD / scripts /make_figures.py
Nabidnur's picture
AIP-AD full artifact: code, results, figures, video, Kaggle notebook, research report
acc1556 verified
Raw History Blame Contribute Delete
8.79 kB
#!/usr/bin/env python3
"""Publication-grade figures for the AIP-AD experiments."""
from __future__ import annotations
import json
import os
import sys
import matplotlib
matplotlib.use("Agg")
import matplotlib.font_manager as fm
for f in ("/usr/share/fonts/truetype/chinese/NotoSansSC-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"):
if os.path.exists(f):
fm.fontManager.addfont(f)
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams["font.sans-serif"] = ["DejaVu Sans", "Noto Sans SC"]
plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["figure.dpi"] = 150
plt.rcParams["savefig.bbox"] = "tight"
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
RES = os.path.join(ROOT, "results")
FIG = os.path.join(RES, "figures")
os.makedirs(FIG, exist_ok=True)
sys.path.insert(0, ROOT)
def style_ax(ax):
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_color("#cbd5e1")
ax.spines["bottom"].set_color("#cbd5e1")
ax.tick_params(colors="#475569", labelsize=8)
ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.2)
ax.set_axisbelow(True)
C_MAX = "#94a3b8"
C_RAW = "#0891b2"
C_ACL = "#7c3aed"
C_RL = "#dc2626"
C_AIP = "#f59e0b"
with open(os.path.join(RES, "results.json")) as f:
R = json.load(f)
cats = [c for c in R["fixed_strategies"]["max"] if c != "macro"]
table = R["fixed_strategies"]
acl_table = R["acl_strategies"]
arb = R["grpo_arbiter"]
def grouped_bars(ax, src, names, colors, title, arbiter_line=None):
x = np.arange(len(cats) + 1) # last slot = MACRO
w = 0.8 / len(names)
for i, n in enumerate(names):
v = [src[n][c] for c in cats] + [src[n]["macro"]]
ax.bar(x + i * w, v, w, label=n, color=colors[i], edgecolor="white", linewidth=0.4)
if arbiter_line is not None:
ax.axhline(arbiter_line, color=C_RL, ls="--", lw=1.6)
ax.text(0.15, arbiter_line + 0.004, f"GRPO arbiter {arbiter_line:.3f}",
color=C_RL, fontsize=8, weight="bold")
ax.set_xticks(x + w * (len(names) - 1) / 2)
ax.set_xticklabels(cats + ["MACRO"], rotation=20, ha="right", fontsize=8)
ax.set_ylim(0.5, 1.0)
ax.set_ylabel("Sample-level AUROC")
ax.set_title(title, fontsize=10, weight="bold")
style_ax(ax)
ax.legend(fontsize=8, ncol=len(names), loc="lower left", bbox_to_anchor=(0, 1.01), frameon=False)
fig, axes = plt.subplots(1, 2, figsize=(13, 4.4), constrained_layout=True)
grouped_bars(axes[0], table, ["max", "mean", "median"], [C_MAX, C_RAW, C_RAW],
"RAW camera agents — paper protocol (max) loses to robust/mean pooling")
grouped_bars(axes[1], acl_table, ["max", "median", "trimmed"], [C_MAX, C_ACL, C_ACL],
"ACL-calibrated agents — robust poolers recover; arbiter tops all",
arbiter_line=arb["per_category"]["macro"])
fig.savefig(os.path.join(FIG, "fig1_benchmark.png"))
plt.close(fig)
# --- fig2: radar of strategies (ACL) ---
names = ["max", "mean", "median", "top2", "trimmed", "aip_d05", "aip_c03", "aip_mt01", "aip_mt025", "aip_wt05"]
ang = np.linspace(0, 2 * np.pi, len(cats), endpoint=False).tolist()
ang += ang[:1]
fig, ax = plt.subplots(figsize=(5.8, 5.4), subplot_kw=dict(polar=True), constrained_layout=True)
pal = ["#64748b", "#0891b2", "#7c3aed", "#059669", "#65a30d", "#f59e0b", "#ea580c", "#db2777", "#be123c", "#dc2626"]
for n, c in zip(names, pal):
v = [acl_table[n][c] for c in cats]
v += v[:1]
ax.plot(ang, v, label=n, color=c, linewidth=1.2)
ax.fill(ang, v, color=c, alpha=0.04)
av = [arb["per_category"][c] for c in cats]
av += av[:1]
ax.plot(ang, av, label="GRPO arbiter", color="#dc2626", linewidth=2.4, linestyle="--")
ax.set_xticks(ang[:-1])
ax.set_xticklabels(cats, fontsize=8)
ax.set_ylim(0.5, 1.0)
ax.set_title("Fusion strategies on ACL-calibrated agents (S-AUROC)", fontsize=10, weight="bold", pad=18)
ax.legend(fontsize=6.5, loc="upper right", bbox_to_anchor=(1.34, 1.12))
fig.savefig(os.path.join(FIG, "fig2_radar.png"))
plt.close(fig)
# --- fig3: continual learning + online hedge ---
fig, axes = plt.subplots(1, 2, figsize=(11.5, 4.2), constrained_layout=True)
for ci, run in enumerate(R["continual"]):
xs = np.arange(1, len(run["curve"]) + 1)
c = ["#2563eb", "#dc2626"][ci]
axes[0].plot(xs, [q["macro_sauroc_all6"] for q in run["curve"]], "o-", color=c,
label=f"order {ci+1} (all 6 cats)")
axes[0].plot(xs, [q["macro_sauroc_seen"] for q in run["curve"]], "o--", color=c, alpha=0.45,
label=f"order {ci+1} (seen cats)")
for src, lab, col in ((acl_table["max"], "static max-pool (paper protocol)", C_MAX),
(acl_table["median"], "static median (ACL)", C_ACL)):
axes[0].axhline(np.mean([src[c2] for c2 in cats]), color=col, ls=":", lw=1.5)
axes[0].text(1.05, np.mean([src[c2] for c2 in cats]) + 0.003, lab, color=col, fontsize=7)
axes[0].set_xticks(range(1, len(cats) + 1))
axes[0].set_xlabel("stream step (category)")
axes[0].set_ylabel("macro S-AUROC")
axes[0].set_title("Continual learning of the GRPO arbiter (solid: all cats, dashed: seen)", fontsize=9, weight="bold")
axes[0].legend(fontsize=7, loc="lower left", bbox_to_anchor=(0, 1.02), ncol=2, frameon=False)
style_ax(axes[0])
hc = R["online_hedge"]
xs = np.arange(1, len(hc) + 1)
axes[1].plot(xs, [h["macro_sauroc_all"] for h in hc], "o-", color=C_AIP, label="online Hedge (operator feedback)")
axes[1].axhline(np.mean([table["max"][c] for c in cats]), color=C_MAX, ls=":", lw=1.5, label="static max-pool (raw)")
axes[1].set_xticks(xs)
axes[1].set_xticklabels([h["after"] for h in hc], rotation=20, ha="right", fontsize=8)
axes[1].set_ylabel("macro S-AUROC (cumulative)")
axes[1].set_title("Online learning: multiplicative-weights fusion", fontsize=9, weight="bold")
axes[1].legend(fontsize=8, loc="lower left", bbox_to_anchor=(0, 1.02), frameon=False)
style_ax(axes[1])
fig.savefig(os.path.join(FIG, "fig3_continual.png"))
plt.close(fig)
# --- fig4: policy behaviour ---
fig, axes = plt.subplots(1, 2, figsize=(11.5, 4.2), constrained_layout=True)
hist = arb["train_history"]
ep = [h["epoch"] for h in hist]
axes[0].plot(ep, [h["policy_entropy"] for h in hist], "-", color=C_RL)
axes[0].set_xlabel("GRPO epoch")
axes[0].set_ylabel("policy entropy (nats)")
axes[0].set_title("Policy specialization during GRPO training", fontsize=9, weight="bold")
style_ax(axes[0])
P = np.array(hist[-1]["mean_probs"])
snames = ["max", "mean", "median", "top2", "trimmed", "aip_d05", "aip_c03", "aip_soft2",
"aip_mt01", "aip_mt025", "aip_mt05", "aip_mt075", "aip_wt05", "aip_wt075"]
order = np.argsort(P)
axes[1].barh(np.array(snames)[order], P[order], color=C_RL, alpha=0.85)
axes[1].set_xlabel("mean policy weight")
axes[1].set_title("Learned policy over pooling strategies (after ACL)", fontsize=9, weight="bold")
style_ax(axes[1])
axes[1].grid(axis="x", linestyle="--", linewidth=0.5, alpha=0.2)
fig.savefig(os.path.join(FIG, "fig4_policy.png"))
plt.close(fig)
# --- fig5: why dissent is information (swarm statistics, ACL scale) ---
gaps_ok, gaps_ng, top1_ok, top1_ng = [], [], [], []
for cat in cats:
ev = np.load(os.path.join(RES, "scores", f"{cat}.npz.eval.npz"))
vs = ev["view_scores"].astype(np.float64)
y = ev["view_labels"].max(1)
srt = np.sort(vs, axis=1)
for i in np.where(y == 0)[0]:
gaps_ok.append(srt[i, -1] - srt[i, -2])
top1_ok.append(srt[i, -1])
for i in np.where(y == 1)[0]:
gaps_ng.append(srt[i, -1] - srt[i, -2])
top1_ng.append(srt[i, -1])
fig, axes = plt.subplots(1, 2, figsize=(11.5, 4.2), constrained_layout=True)
axes[0].hist(gaps_ok, bins=48, alpha=0.62, label=f"normal (n={len(gaps_ok)})", color=C_MAX, density=True)
axes[0].hist(gaps_ng, bins=48, alpha=0.62, label=f"anomalous (n={len(gaps_ng)})", color=C_RL, density=True)
axes[0].set_xlabel("dissent: top-1 minus top-2 per-view score")
axes[0].set_ylabel("density")
axes[0].set_title("The dissenting 'liar' view carries the anomaly signal", fontsize=9, weight="bold")
axes[0].legend(fontsize=8, loc="lower left", bbox_to_anchor=(0, 1.02), frameon=False)
style_ax(axes[0])
axes[1].hist(top1_ok, bins=48, alpha=0.62, label="normal", color=C_MAX, density=True)
axes[1].hist(top1_ng, bins=48, alpha=0.62, label="anomalous", color=C_RL, density=True)
axes[1].set_xlabel("top-1 per-view score (max report)")
axes[1].set_title("Max report alone is weakly separated", fontsize=9, weight="bold")
axes[1].legend(fontsize=8, loc="lower left", bbox_to_anchor=(0, 1.02), frameon=False)
style_ax(axes[1])
fig.savefig(os.path.join(FIG, "fig5_dissent.png"))
plt.close(fig)
print("figures written to", FIG)
for f in sorted(os.listdir(FIG)):
print(" ", f, os.path.getsize(os.path.join(FIG, f)))