update 3-19
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Plot per-epoch holdout metrics across all folds in a 10x5 (or any multi-rep) run.
|
||||
|
||||
Each fold gets its own line. Lines are coloured by rep.
|
||||
|
||||
Modes
|
||||
-----
|
||||
holdout — raw holdout AUC per epoch (original plot)
|
||||
delta — val_auc - holdout_auc per epoch (generalization gap;
|
||||
closer to 0 = val most faithfully reflects holdout)
|
||||
|
||||
Usage
|
||||
-----
|
||||
python scripts/output_analysis/visualizations/plot_holdout_epoch_curves.py \
|
||||
--run-dir analysis_data/pipeline_10x5 \
|
||||
--eval-mode binary \
|
||||
--tower-mode ensemble \
|
||||
--head fused \
|
||||
--mode delta
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
HEAD_COL = {
|
||||
"fused": ("ensemble_val_auc", "ensemble_holdout_auc"),
|
||||
"img": ("ensemble_val_auc_img", "ensemble_holdout_auc_img"),
|
||||
"md": ("ensemble_val_auc_md", "ensemble_holdout_auc_md"),
|
||||
"classic": ("classic_val_auc", "classic_holdout_auc"),
|
||||
}
|
||||
|
||||
|
||||
def load_curves(run_dir: Path, eval_mode: str, tower_mode: str,
|
||||
val_col: str, hld_col: str, mode: str):
|
||||
"""
|
||||
Returns list of (rep, fold, epochs_array, values_array).
|
||||
mode='holdout' → values = holdout_auc
|
||||
mode='delta' → values = val_auc - holdout_auc
|
||||
"""
|
||||
curves = []
|
||||
for rep_dir in sorted(run_dir.glob("rep*")):
|
||||
mode_dir = rep_dir / eval_mode / tower_mode
|
||||
if not mode_dir.exists():
|
||||
continue
|
||||
fold_dirs = sorted(
|
||||
[d for d in mode_dir.iterdir() if d.is_dir() and d.name.startswith("fold")],
|
||||
key=lambda p: int(p.name.replace("fold", "")),
|
||||
)
|
||||
for fd in fold_dirs:
|
||||
log = fd / "epoch_log.csv"
|
||||
if not log.exists():
|
||||
continue
|
||||
df = pd.read_csv(log)
|
||||
needed = [hld_col] if mode == "holdout" else [val_col, hld_col]
|
||||
if any(c not in df.columns for c in needed):
|
||||
continue
|
||||
df = df.dropna(subset=needed)
|
||||
if mode == "holdout":
|
||||
values = df[hld_col].to_numpy()
|
||||
elif mode == "delta":
|
||||
values = (df[val_col] - df[hld_col]).to_numpy()
|
||||
else: # delta_adj
|
||||
val_arr = df[val_col].to_numpy()
|
||||
hld_arr = df[hld_col].to_numpy()
|
||||
val_best = np.nanmax(val_arr)
|
||||
penalty = val_best - val_arr # 0 when val is at its peak
|
||||
values = (val_arr - hld_arr) + penalty
|
||||
curves.append((rep_dir.name, fd.name, df["epoch"].to_numpy(), values))
|
||||
return curves
|
||||
|
||||
|
||||
def _build_mean_matrix(curves):
|
||||
all_ep = max(len(e) for _, _, e, _ in curves)
|
||||
mat = np.full((len(curves), all_ep), np.nan)
|
||||
for i, (_, _, e, a) in enumerate(curves):
|
||||
mat[i, :len(a)] = a
|
||||
return mat, np.arange(1, all_ep + 1)
|
||||
|
||||
|
||||
def plot(curves, mode: str, out_path: Path, title: str):
|
||||
reps = sorted(set(r for r, _, _, _ in curves))
|
||||
cmap = matplotlib.colormaps.get_cmap("tab10")
|
||||
rep_color = {r: cmap(i / max(len(reps) - 1, 1)) for i, r in enumerate(reps)}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
|
||||
for rep, fold, epochs, vals in curves:
|
||||
ax.plot(epochs, vals, color=rep_color[rep], alpha=0.3, linewidth=0.9)
|
||||
|
||||
# per-rep mean
|
||||
for rep in reps:
|
||||
rep_curves = [(e, a) for r, _, e, a in curves if r == rep]
|
||||
max_ep = max(len(e) for e, _ in rep_curves)
|
||||
mat = np.full((len(rep_curves), max_ep), np.nan)
|
||||
for i, (e, a) in enumerate(rep_curves):
|
||||
mat[i, :len(a)] = a
|
||||
mean_curve = np.nanmean(mat, axis=0)
|
||||
ax.plot(np.arange(1, max_ep + 1), mean_curve,
|
||||
color=rep_color[rep], linewidth=1.8, alpha=0.85, label=rep)
|
||||
|
||||
# global mean ± std
|
||||
all_mat, ep_axis = _build_mean_matrix(curves)
|
||||
global_mean = np.nanmean(all_mat, axis=0)
|
||||
global_std = np.nanstd(all_mat, axis=0)
|
||||
ax.plot(ep_axis, global_mean, color="black", linewidth=2.5, zorder=5, label="global mean")
|
||||
ax.fill_between(ep_axis, global_mean - global_std, global_mean + global_std,
|
||||
color="black", alpha=0.12, zorder=4)
|
||||
|
||||
if mode in ("delta", "delta_adj"):
|
||||
ax.axhline(0, color="black", linewidth=1.0, linestyle="--", alpha=0.5)
|
||||
if mode == "delta":
|
||||
ax.set_ylabel("Val AUC − Holdout AUC (gap)")
|
||||
else:
|
||||
ax.set_ylabel("(Val − Holdout) + (ValBest − Val) (adjusted gap)")
|
||||
min_ep = int(ep_axis[np.nanargmin(global_mean)])
|
||||
min_val = global_mean[np.nanargmin(global_mean)]
|
||||
ax.axvline(min_ep, color="red", linewidth=1.2, linestyle=":", alpha=0.7,
|
||||
label=f"min adjusted gap @ epoch {min_ep} ({min_val:+.3f})")
|
||||
else:
|
||||
ax.set_ylabel("Holdout AUC")
|
||||
ax.set_ylim(0, 1.05)
|
||||
|
||||
ax.set_xlabel("Epoch")
|
||||
ax.set_title(title)
|
||||
ax.legend(fontsize=7, ncol=2, loc="upper right" if mode == "delta" else "lower right")
|
||||
ax.grid(axis="y", linestyle="--", alpha=0.4)
|
||||
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fig.tight_layout()
|
||||
fig.savefig(out_path, dpi=150)
|
||||
plt.close(fig)
|
||||
print(f"Saved → {out_path}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--run-dir", default="analysis_data/pipeline_10x5")
|
||||
ap.add_argument("--eval-mode", default="binary")
|
||||
ap.add_argument("--tower-mode", default="ensemble")
|
||||
ap.add_argument("--head", default="fused", choices=list(HEAD_COL))
|
||||
ap.add_argument("--mode", default="holdout", choices=["holdout", "delta", "delta_adj"])
|
||||
ap.add_argument("--out", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
run_dir = Path(args.run_dir)
|
||||
val_col, hld_col = HEAD_COL[args.head]
|
||||
curves = load_curves(run_dir, args.eval_mode, args.tower_mode,
|
||||
val_col, hld_col, args.mode)
|
||||
|
||||
if not curves:
|
||||
print("No epoch_log.csv files found — check --run-dir / --eval-mode / --tower-mode")
|
||||
return
|
||||
|
||||
print(f"Loaded {len(curves)} fold curves, up to {max(len(e) for _,_,e,_ in curves)} epochs each")
|
||||
|
||||
out = Path(args.out) if args.out else (
|
||||
run_dir / args.eval_mode / args.tower_mode / "plots" /
|
||||
f"holdout_epoch_curves_{args.head}_{args.mode}.png"
|
||||
)
|
||||
label = {"holdout": "holdout AUC", "delta": "val−holdout gap", "delta_adj": "val−holdout gap (val-adjusted)"}[args.mode]
|
||||
title = (f"Per-fold {label} by epoch\n"
|
||||
f"{run_dir.name} · {args.eval_mode}/{args.tower_mode} · head={args.head}")
|
||||
plot(curves, args.mode, out, title)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user