| |
| """4 figuras adicionais pro probe ε × métricas (Análises A.2–A.5 do plano). |
| |
| Lê o CSV merged do probe (output de `merge_probe_csvs.py`) e gera: |
| A.2 — `{asr,ssim,lpips,psnr}_vs_eps_lines.png` (4 PNGs): |
| Curvas mean ± IC95 por ataque. Versão "limpa" dos boxplots. |
| A.3 — `heatmap_model_attack_eps8_{asr,ssim}.png` (2 PNGs): |
| Matriz 4×4 modelo × ataque, células coloridas por mean métrica a ε=8/255. |
| A.4 — `efficiency_per_attack_eps8.png` (1 PNG): |
| Bar chart: eficiência ASR / (1−SSIM) por ataque a ε=8/255. |
| A.5 — `asr_vs_eps_by_mask.png` (1 PNG): |
| Boxplot ASR ε × ataque, facetado por has_mask. |
| |
| Total: 8 figuras adicionais. |
| |
| Usage: |
| python scripts/plot_extra_analyses.py \\ |
| --csv results/raw/probe_merged_no_tgr.csv \\ |
| --out results/figures/probe_eps_curve_final/ \\ |
| --eps-ref 8 |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import sys |
| from pathlib import Path |
|
|
|
|
| def _project_root() -> Path: |
| cur = Path(__file__).resolve().parent |
| for p in [cur, *cur.parents]: |
| if (p / "requirements.txt").exists(): |
| return p |
| raise RuntimeError("project root not found") |
|
|
|
|
| PROJECT_ROOT = _project_root() |
|
|
| PALETTE = { |
| "FGSM": "#e41a1c", "PGD": "#377eb8", "MIM": "#4daf4a", |
| "TGR": "#984ea3", "SAGA": "#ff7f00", |
| } |
| ATTACK_ORDER = ["FGSM", "PGD", "MIM", "SAGA"] |
|
|
|
|
| def _model_short_name(name: str) -> str: |
| """'ViT-S/16 · ImageNet-1k' → 'ViT-S/16'.""" |
| return name.split(" ·")[0].strip() if " ·" in name else name.strip() |
|
|
|
|
| def _setup_matplotlib(): |
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| return plt |
|
|
|
|
| |
|
|
| def plot_lines_eps_metric(df, metric: str, ylabel: str, title: str, |
| out_path: Path, ylim=None) -> None: |
| plt = _setup_matplotlib() |
| import numpy as np |
|
|
| eps_values = sorted(df["eps_255"].unique()) |
| fig, ax = plt.subplots(figsize=(8, 5)) |
|
|
| for atk in ATTACK_ORDER: |
| sub = df[df["attack"] == atk] |
| if sub.empty: |
| continue |
| means, lo, hi = [], [], [] |
| for e in eps_values: |
| vals = sub.loc[sub["eps_255"] == e, metric].dropna().values |
| if len(vals) == 0: |
| means.append(np.nan); lo.append(np.nan); hi.append(np.nan) |
| continue |
| m = float(np.mean(vals)) |
| sem = float(np.std(vals, ddof=1)) / max(np.sqrt(len(vals)), 1) |
| means.append(m) |
| lo.append(m - 1.96 * sem) |
| hi.append(m + 1.96 * sem) |
| ax.plot(eps_values, means, marker="o", color=PALETTE[atk], |
| linewidth=2, label=atk) |
| ax.fill_between(eps_values, lo, hi, alpha=0.18, color=PALETTE[atk]) |
|
|
| ax.set_xlabel("ε∞ (×1/255)") |
| ax.set_ylabel(ylabel) |
| ax.set_title(title) |
| ax.set_xticks(eps_values) |
| if ylim: |
| ax.set_ylim(*ylim) |
| ax.grid(alpha=0.3) |
| ax.legend(loc="best", fontsize=10) |
| fig.tight_layout() |
| fig.savefig(out_path, dpi=150) |
| plt.close(fig) |
| print(f" ✓ {out_path.name}") |
|
|
|
|
| |
|
|
| def plot_heatmap_model_attack(df, metric: str, eps_ref: int, |
| cmap: str, fmt: str, |
| title: str, out_path: Path, |
| vmin=None, vmax=None) -> None: |
| plt = _setup_matplotlib() |
| import numpy as np |
|
|
| sub = df[df["eps_255"] == eps_ref].copy() |
| sub["model_short"] = sub["model"].apply(_model_short_name) |
| pivot = sub.pivot_table( |
| index="model_short", columns="attack", values=metric, aggfunc="mean" |
| ) |
| |
| cols = [a for a in ATTACK_ORDER if a in pivot.columns] |
| pivot = pivot[cols] |
| |
| desired_rows = ["ViT-S/16", "ViT-S/32", "ViT-B/32", "ViT-B/16"] |
| pivot = pivot.reindex([r for r in desired_rows if r in pivot.index]) |
|
|
| fig, ax = plt.subplots(figsize=(7, 5)) |
| im = ax.imshow(pivot.values, aspect="auto", cmap=cmap, vmin=vmin, vmax=vmax) |
|
|
| |
| for i in range(pivot.shape[0]): |
| for j in range(pivot.shape[1]): |
| v = pivot.values[i, j] |
| if np.isnan(v): |
| txt = "—" |
| else: |
| txt = format(v, fmt) |
| |
| cell_color = im.cmap(im.norm(v)) if not np.isnan(v) else (1, 1, 1, 1) |
| lum = 0.299 * cell_color[0] + 0.587 * cell_color[1] + 0.114 * cell_color[2] |
| text_color = "white" if lum < 0.5 else "black" |
| ax.text(j, i, txt, ha="center", va="center", color=text_color, fontsize=11) |
|
|
| ax.set_xticks(range(len(pivot.columns))) |
| ax.set_xticklabels(pivot.columns) |
| ax.set_yticks(range(len(pivot.index))) |
| ax.set_yticklabels(pivot.index) |
| ax.set_title(title) |
|
|
| cbar = plt.colorbar(im, ax=ax, fraction=0.04, pad=0.04) |
| cbar.set_label(metric.upper()) |
|
|
| fig.tight_layout() |
| fig.savefig(out_path, dpi=150) |
| plt.close(fig) |
| print(f" ✓ {out_path.name}") |
|
|
|
|
| |
|
|
| def plot_efficiency_bar(df, eps_ref: int, out_path: Path) -> None: |
| plt = _setup_matplotlib() |
|
|
| sub = df[df["eps_255"] == eps_ref] |
| rows = [] |
| for atk in ATTACK_ORDER: |
| atk_sub = sub[sub["attack"] == atk] |
| if atk_sub.empty: |
| continue |
| mean_asr = float(atk_sub["asr"].mean()) |
| mean_ssim = float(atk_sub["ssim"].mean()) |
| denom = max(1.0 - mean_ssim, 1e-4) |
| eff = mean_asr / denom |
| rows.append({"attack": atk, "asr": mean_asr, "ssim": mean_ssim, |
| "efficiency": eff}) |
|
|
| if not rows: |
| print(f" ⚠️ sem dados a ε={eps_ref}/255 — pulando efficiency bar") |
| return |
|
|
| rows.sort(key=lambda r: r["efficiency"], reverse=True) |
| attacks = [r["attack"] for r in rows] |
| effs = [r["efficiency"] for r in rows] |
| colors = [PALETTE[a] for a in attacks] |
|
|
| fig, ax = plt.subplots(figsize=(8, 5)) |
| bars = ax.bar(attacks, effs, color=colors, edgecolor="black", alpha=0.85) |
| for bar, r in zip(bars, rows): |
| ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() * 1.02, |
| f"{r['efficiency']:.1f}\n(ASR={r['asr']:.2f}, SSIM={r['ssim']:.3f})", |
| ha="center", va="bottom", fontsize=9) |
|
|
| ax.set_ylabel("Eficiência = mean ASR / (1 − mean SSIM)") |
| ax.set_xlabel("Ataque") |
| ax.set_title(f"Eficiência por ataque a ε={eps_ref}/255 — quanto ASR por unidade de degradação visual") |
| ax.grid(axis="y", alpha=0.3) |
| ax.set_ylim(0, max(effs) * 1.25) |
| fig.tight_layout() |
| fig.savefig(out_path, dpi=150) |
| plt.close(fig) |
| print(f" ✓ {out_path.name}") |
|
|
|
|
| |
|
|
| def plot_asr_by_mask(df, out_path: Path) -> None: |
| plt = _setup_matplotlib() |
|
|
| if "has_mask" not in df.columns: |
| print(f" ⚠️ has_mask ausente — pulando A.5") |
| return |
|
|
| eps_values = sorted(df["eps_255"].unique()) |
|
|
| fig, axes = plt.subplots(1, 2, figsize=(15, 5.5), sharey=True) |
| titles = ["has_mask=1 (Guillaumin GT)", "has_mask=0 (IN-1k val)"] |
| n_attacks = len(ATTACK_ORDER) |
| box_width = 0.8 / n_attacks |
|
|
| for ax, mask_val, ttl in zip(axes, [1, 0], titles): |
| sub_mask = df[df["has_mask"] == mask_val] |
| legend_handles = [] |
| for j, atk in enumerate(ATTACK_ORDER): |
| sub_atk = sub_mask[sub_mask["attack"] == atk] |
| if sub_atk.empty: |
| continue |
| data, positions = [], [] |
| for i, e in enumerate(eps_values): |
| vals = sub_atk.loc[sub_atk["eps_255"] == e, "asr"].dropna().values |
| if len(vals) == 0: |
| continue |
| data.append(vals) |
| offset = (j - (n_attacks - 1) / 2) * box_width |
| positions.append(i + offset) |
| if not data: |
| continue |
| ax.boxplot( |
| data, positions=positions, widths=box_width * 0.85, |
| patch_artist=True, showfliers=False, |
| medianprops={"color": "black", "linewidth": 1.2}, |
| boxprops={"facecolor": PALETTE[atk], "alpha": 0.7, |
| "edgecolor": PALETTE[atk]}, |
| whiskerprops={"color": PALETTE[atk]}, |
| capprops={"color": PALETTE[atk]}, |
| ) |
| legend_handles.append(plt.Rectangle( |
| (0, 0), 1, 1, fc=PALETTE[atk], alpha=0.7, label=atk |
| )) |
|
|
| n_imgs = sub_mask["image"].nunique() |
| ax.set_xticks(range(len(eps_values))) |
| ax.set_xticklabels([f"{e}" for e in eps_values]) |
| ax.set_xlabel("ε∞ (×1/255)") |
| ax.set_title(f"{ttl} — N={n_imgs} imgs") |
| ax.set_ylim(-0.05, 1.05) |
| ax.grid(axis="y", alpha=0.3) |
| if mask_val == 1: |
| ax.set_ylabel("ASR (per image)") |
| if legend_handles and mask_val == 0: |
| ax.legend(handles=legend_handles, loc="lower right", |
| fontsize=9, ncol=n_attacks) |
|
|
| fig.suptitle("ASR por ε × ataque, facetado por has_mask (Análise F)", |
| y=0.99, fontsize=12) |
| fig.tight_layout() |
| fig.savefig(out_path, dpi=150) |
| plt.close(fig) |
|
|
| |
| if 8 in eps_values: |
| sub8 = df[df["eps_255"] == 8] |
| for atk in ATTACK_ORDER: |
| sub_atk = sub8[sub8["attack"] == atk] |
| if sub_atk.empty: |
| continue |
| asr_with = sub_atk[sub_atk["has_mask"] == 1]["asr"].mean() |
| asr_without = sub_atk[sub_atk["has_mask"] == 0]["asr"].mean() |
| diff_pp = abs(asr_with - asr_without) * 100 |
| warn = " ⚠️ confound!" if diff_pp > 5 else "" |
| print(f" {atk} a ε=8: with_mask ASR={asr_with:.3f} | " |
| f"without_mask ASR={asr_without:.3f} | " |
| f"diff={diff_pp:.1f}pp{warn}") |
| print(f" ✓ {out_path.name}") |
|
|
|
|
| |
|
|
| def main() -> int: |
| parser = argparse.ArgumentParser(description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| parser.add_argument("--csv", type=Path, required=True, |
| help="CSV merged do probe (output de merge_probe_csvs.py)") |
| parser.add_argument("--out", type=Path, required=True, |
| help="Diretório de saída (será criado).") |
| parser.add_argument("--eps-ref", type=int, default=8, |
| help="ε de referência pra heatmap + efficiency (default: 8)") |
| args = parser.parse_args() |
|
|
| if not args.csv.exists(): |
| print(f"ERROR: CSV não encontrado: {args.csv}") |
| return 1 |
|
|
| try: |
| import pandas as pd |
| except ImportError: |
| print("ERROR: pandas necessário") |
| return 1 |
|
|
| args.out.mkdir(parents=True, exist_ok=True) |
|
|
| print(f"Lendo {args.csv} ...") |
| df = pd.read_csv(args.csv) |
| if "eps_255" not in df.columns: |
| df["eps_255"] = (df["epsilon"].astype(float) * 255).round().astype(int) |
| print(f" {len(df)} rows | " |
| f"modelos={df['model'].nunique()} | " |
| f"ataques={sorted(df['attack'].unique())} | " |
| f"ε={sorted(df['eps_255'].unique())} | " |
| f"imgs={df['image'].nunique()}") |
|
|
| print(f"\n=== A.2: Curvas mean ± IC95 ===") |
| plot_lines_eps_metric(df, "asr", "ASR (mean ± IC95)", |
| "Curva ε × ASR — linhas por ataque", |
| args.out / "asr_vs_eps_lines.png", ylim=(-0.05, 1.05)) |
| plot_lines_eps_metric(df, "ssim", "SSIM (mean ± IC95)", |
| "Curva ε × SSIM — linhas por ataque", |
| args.out / "ssim_vs_eps_lines.png", ylim=(0.4, 1.02)) |
| plot_lines_eps_metric(df, "lpips", "LPIPS (mean ± IC95)", |
| "Curva ε × LPIPS — linhas por ataque", |
| args.out / "lpips_vs_eps_lines.png") |
| plot_lines_eps_metric(df, "psnr", "PSNR dB (mean ± IC95)", |
| "Curva ε × PSNR — linhas por ataque", |
| args.out / "psnr_vs_eps_lines.png") |
|
|
| print(f"\n=== A.3: Heatmap modelo × ataque a ε={args.eps_ref}/255 ===") |
| plot_heatmap_model_attack( |
| df, metric="asr", eps_ref=args.eps_ref, cmap="Reds", fmt=".2f", |
| title=f"Mean ASR por modelo × ataque a ε={args.eps_ref}/255", |
| out_path=args.out / f"heatmap_model_attack_eps{args.eps_ref}_asr.png", |
| vmin=0, vmax=1, |
| ) |
| plot_heatmap_model_attack( |
| df, metric="ssim", eps_ref=args.eps_ref, cmap="Blues", fmt=".3f", |
| title=f"Mean SSIM por modelo × ataque a ε={args.eps_ref}/255", |
| out_path=args.out / f"heatmap_model_attack_eps{args.eps_ref}_ssim.png", |
| vmin=0.5, vmax=1.0, |
| ) |
|
|
| print(f"\n=== A.4: Bar chart de eficiência a ε={args.eps_ref}/255 ===") |
| plot_efficiency_bar(df, eps_ref=args.eps_ref, |
| out_path=args.out / f"efficiency_per_attack_eps{args.eps_ref}.png") |
|
|
| print(f"\n=== A.5: ASR ε × ataque facetado por has_mask ===") |
| plot_asr_by_mask(df, out_path=args.out / "asr_vs_eps_by_mask.png") |
|
|
| print(f"\n✓ Done. {len(list(args.out.glob('*.png')))} figuras em {args.out}") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|