Add analysis scripts and experiment configurations for bridge attention and sensitivity studies
- Introduced `bridge_attention_ceiling_check.py` for variance decomposition analysis on bridge attention configurations. - Added `bridge_attention_readout.py` to perform per-tower gate and contribution readouts, including AUC sanity checks. - Created multiple JSON configuration files for backbone replication experiments, including anonymous CV variants and basic backbones. - Implemented sensitivity experiments to evaluate the impact of axial length inclusion and EfficientNetV2-M performance at higher resolutions. - Added a memory probe script to assess GPU memory usage during training with EfficientNetV2-M.
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
"""S2 - Variance decomposition of the four-bridge L1 fusion comparison.
|
||||
|
||||
Two-panel supplementary figure summarising the variance decomposition
|
||||
reported alongside section 3.2 of the manuscript.
|
||||
|
||||
Panel A (left): paired-line plot.
|
||||
x-axis : the 4 bridge variants (Concat, Pairwise, Gated, Hadamard)
|
||||
y-axis : eye-level test AUC
|
||||
each line : one fold-rep, connecting that fold-rep's 4 bridge AUCs
|
||||
overlay : per-bridge boxplot showing the marginal AUC distribution
|
||||
annotation : pooled across-architecture and across-fold-rep SDs,
|
||||
and the SD ratio
|
||||
|
||||
Panel B (right): centered-offset KDEs.
|
||||
x-axis : AUC offset from grouping mean (centered at 0)
|
||||
y-axis : density
|
||||
four coloured curves : per-bridge fold-rep distributions (50 fold-reps
|
||||
per bridge, centered by subtracting each bridge's
|
||||
own mean)
|
||||
dashed dark curve : architectural offset distribution (200 values,
|
||||
centered by subtracting each fold-rep's mean)
|
||||
annotation : variance ratio
|
||||
|
||||
Read directly from v4/results/experiments/{phase3_v4,refuge_v2m_baseline}.
|
||||
|
||||
Re-run:
|
||||
python -m v4.figures.S2_variance_decomposition
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import gaussian_kde
|
||||
|
||||
from v4.figures.util.loaders import RESULTS_ROOT
|
||||
|
||||
|
||||
OUT = Path(__file__).parent / "output" / "S2_variance_decomposition.png"
|
||||
|
||||
|
||||
# Bridge label, results dir relative to experiments/, and stage key
|
||||
BRIDGES = [
|
||||
("Concat", "phase3_v4/single_bcd_concat"),
|
||||
("Pairwise", "phase3_v4/single_bcd_pairwise"),
|
||||
("Gated", "phase3_v4/single_bcd_gated"),
|
||||
("Hadamard", "refuge_v2m_baseline/ensemble_single_refugelike"),
|
||||
]
|
||||
STAGE_KEY = "nt_test_auc"
|
||||
|
||||
|
||||
# Panel-A colour palette (paired lines + boxplots)
|
||||
C_LINE = "#1f6fb0" # single blue for all paired-cell lines
|
||||
C_LINE_ALPHA = 0.22
|
||||
C_MARKER = "#1f6fb0"
|
||||
C_MEDIAN = "#c44e52" # red box median line
|
||||
C_BOX_FILL = "#dbe6f0" # pale blue box fill
|
||||
|
||||
# Panel-B colour palette (per-bridge KDEs)
|
||||
C_ARCH = "#222" # dark grey for the architectural curve
|
||||
BRIDGE_COLORS = {
|
||||
"Concat": "#7f7f7f",
|
||||
"Pairwise": "#ff7f0e",
|
||||
"Gated": "#2ca02c",
|
||||
"Hadamard": "#1f77b4",
|
||||
}
|
||||
|
||||
|
||||
# ── Data loading + decomposition ────────────────────────────────────────────
|
||||
|
||||
def collect() -> pd.DataFrame:
|
||||
rows = []
|
||||
for label, rel in BRIDGES:
|
||||
root = RESULTS_ROOT / rel
|
||||
for s in sorted(root.glob("rep*/binary/summary.json")):
|
||||
rep = int(s.parents[1].name.replace("rep", ""))
|
||||
d = json.loads(s.read_text())
|
||||
for fr in d.get("fold_results", []):
|
||||
v = fr.get(STAGE_KEY)
|
||||
if v is None or not np.isfinite(v):
|
||||
continue
|
||||
rows.append({
|
||||
"bridge": label,
|
||||
"rep": rep,
|
||||
"fold": int(fr["fold"]),
|
||||
"auc": float(v),
|
||||
})
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def decompose(df: pd.DataFrame) -> tuple[float, float, float, dict[str, float]]:
|
||||
"""Return (arch_sd_pooled, fold_sd_pooled, ratio, per_bridge_sd)."""
|
||||
cell_var = df.groupby(["rep", "fold"])["auc"].var(ddof=1)
|
||||
bridge_var = df.groupby("bridge")["auc"].var(ddof=1)
|
||||
arch_sd = float(np.sqrt(cell_var.mean()))
|
||||
fold_sd = float(np.sqrt(bridge_var.mean()))
|
||||
ratio = fold_sd / arch_sd if arch_sd > 0 else float("inf")
|
||||
per_bridge_sd = {b: float(np.sqrt(v)) for b, v in bridge_var.items()}
|
||||
return arch_sd, fold_sd, ratio, per_bridge_sd
|
||||
|
||||
|
||||
# ── Panel A: paired-line plot ───────────────────────────────────────────────
|
||||
|
||||
def draw_panel_a(ax, df: pd.DataFrame,
|
||||
arch_sd: float, fold_sd: float, ratio: float,
|
||||
n_cells: int) -> None:
|
||||
bridge_order = [b for b, _ in BRIDGES]
|
||||
pivot = df.pivot_table(
|
||||
index=["rep", "fold"], columns="bridge", values="auc"
|
||||
)[bridge_order]
|
||||
x_positions = np.arange(len(bridge_order), dtype=float)
|
||||
|
||||
# Paired cell lines: one per fold-rep
|
||||
for _, row in pivot.iterrows():
|
||||
if row.isna().any():
|
||||
continue
|
||||
ax.plot(
|
||||
x_positions, row.values, color=C_LINE, alpha=C_LINE_ALPHA,
|
||||
linewidth=0.9, marker="o", markersize=2.0,
|
||||
markerfacecolor=C_MARKER, markeredgecolor="none", zorder=2,
|
||||
)
|
||||
|
||||
# Per-bridge boxplot
|
||||
box_data = [pivot[b].dropna().values for b in bridge_order]
|
||||
ax.boxplot(
|
||||
box_data,
|
||||
positions=x_positions,
|
||||
widths=0.32,
|
||||
patch_artist=True,
|
||||
manage_ticks=False,
|
||||
zorder=3,
|
||||
boxprops=dict(facecolor=C_BOX_FILL, edgecolor="black",
|
||||
linewidth=1.0, alpha=0.85),
|
||||
whiskerprops=dict(color="black", linewidth=0.9),
|
||||
capprops=dict(color="black", linewidth=0.9),
|
||||
medianprops=dict(color=C_MEDIAN, linewidth=1.8),
|
||||
flierprops=dict(marker="", markersize=0),
|
||||
)
|
||||
|
||||
ax.set_xticks(x_positions)
|
||||
ax.set_xticklabels(bridge_order, fontsize=11)
|
||||
ax.set_xlabel("L1 fusion bridge", fontsize=11)
|
||||
ax.set_ylabel("Eye-level test AUC", fontsize=11)
|
||||
ax.set_xlim(-0.5, len(bridge_order) - 0.5)
|
||||
ax.grid(axis="y", alpha=0.3, linestyle="--")
|
||||
|
||||
txt = (
|
||||
f"n = {n_cells} fold-rep AUC values | variance ratio {ratio:.2f}\n"
|
||||
f" across-architecture SD = {arch_sd:.3f}\n"
|
||||
f" across-fold-rep SD = {fold_sd:.3f}"
|
||||
)
|
||||
ax.text(
|
||||
0.985, 0.025, txt, transform=ax.transAxes,
|
||||
ha="right", va="bottom", fontsize=9.5, family="monospace",
|
||||
bbox=dict(boxstyle="round,pad=0.5", facecolor="white",
|
||||
edgecolor="#888", alpha=0.92),
|
||||
)
|
||||
|
||||
|
||||
# ── Panel B: centered-offset KDE curves ─────────────────────────────────────
|
||||
|
||||
def draw_panel_b(ax, df: pd.DataFrame,
|
||||
arch_sd: float, fold_sd: float, ratio: float,
|
||||
per_bridge_sd: dict[str, float]) -> None:
|
||||
df = df.copy()
|
||||
fold_rep_mean = df.groupby(["rep", "fold"])["auc"].transform("mean")
|
||||
bridge_mean = df.groupby("bridge")["auc"].transform("mean")
|
||||
df["arch_offset"] = df["auc"] - fold_rep_mean
|
||||
df["fold_offset"] = df["auc"] - bridge_mean
|
||||
|
||||
all_offsets = np.concatenate(
|
||||
[df["arch_offset"].values, df["fold_offset"].values]
|
||||
)
|
||||
x_max = float(np.abs(all_offsets).max()) * 1.10
|
||||
xs = np.linspace(-x_max, x_max, 600)
|
||||
|
||||
# Per-bridge fold-rep offset curves
|
||||
for b in [name for name, _ in BRIDGES]:
|
||||
offs = df.loc[df["bridge"] == b, "fold_offset"].values
|
||||
kde = gaussian_kde(offs)
|
||||
y = kde(xs)
|
||||
sd = per_bridge_sd[b]
|
||||
ax.plot(xs, y, color=BRIDGE_COLORS[b], linewidth=1.6, alpha=0.92,
|
||||
zorder=3,
|
||||
label=f"{b} SD = {sd:.3f}")
|
||||
|
||||
# Architectural offset curve (pooled)
|
||||
arch_offsets = df["arch_offset"].values
|
||||
kde_arch = gaussian_kde(arch_offsets)
|
||||
y_arch = kde_arch(xs)
|
||||
ax.fill_between(xs, y_arch, color=C_ARCH, alpha=0.18, zorder=2)
|
||||
ax.plot(xs, y_arch, color=C_ARCH, linewidth=2.2, linestyle="--", zorder=4,
|
||||
label=f"Architectural SD = {arch_sd:.3f}")
|
||||
|
||||
ax.axvline(0, color="#666", linewidth=0.8, linestyle=":",
|
||||
alpha=0.6, zorder=0)
|
||||
ax.set_xlabel("AUC offset from grouping mean", fontsize=11)
|
||||
ax.set_ylabel("Probability density", fontsize=11)
|
||||
ax.set_xlim(-x_max, x_max)
|
||||
ax.set_yticklabels([])
|
||||
ax.tick_params(axis="y", which="both", left=True, labelleft=False)
|
||||
ax.grid(axis="y", alpha=0.25, linestyle="--")
|
||||
|
||||
# Headroom on the y-axis so the variance-ratio box does not crowd the
|
||||
# architectural-curve peak.
|
||||
ymin, ymax = ax.get_ylim()
|
||||
ax.set_ylim(0, ymax * 1.10)
|
||||
|
||||
# Legend below the top so it clears the variance-ratio annotation.
|
||||
ax.legend(
|
||||
loc="upper left", bbox_to_anchor=(0.0, 0.82),
|
||||
fontsize=9, framealpha=0.92,
|
||||
)
|
||||
|
||||
txt = f"variance ratio fold-SD / arch-SD = {ratio:.2f}"
|
||||
ax.text(
|
||||
0.985, 0.975, txt, transform=ax.transAxes,
|
||||
ha="right", va="top", fontsize=10.5, family="monospace",
|
||||
bbox=dict(boxstyle="round,pad=0.45", facecolor="white",
|
||||
edgecolor="#888", alpha=0.92),
|
||||
)
|
||||
|
||||
|
||||
# ── Combined render ─────────────────────────────────────────────────────────
|
||||
|
||||
def render() -> None:
|
||||
df = collect()
|
||||
if df.empty:
|
||||
print("No data collected; check the source paths.")
|
||||
return
|
||||
|
||||
n_cells = df.groupby(["rep", "fold"]).ngroups
|
||||
n_bridges = df["bridge"].nunique()
|
||||
arch_sd, fold_sd, ratio, per_bridge_sd = decompose(df)
|
||||
|
||||
print(f"Collected {len(df)} observations ({n_bridges} bridges x {n_cells} cells)")
|
||||
print(f" across-architecture SD (within cell) : {arch_sd:.4f}")
|
||||
print(f" across-fold-rep SD (within bridge) : {fold_sd:.4f}")
|
||||
print(f" ratio fold-SD / arch-SD : {ratio:.2f}x")
|
||||
print("Per-bridge fold-rep SDs:")
|
||||
for b in [name for name, _ in BRIDGES]:
|
||||
print(f" {b:<10s} SD = {per_bridge_sd[b]:.4f}")
|
||||
|
||||
fig, (axA, axB) = plt.subplots(
|
||||
nrows=1, ncols=2, figsize=(16.0, 6.0),
|
||||
gridspec_kw=dict(wspace=0.22),
|
||||
)
|
||||
|
||||
draw_panel_a(axA, df, arch_sd, fold_sd, ratio, n_cells)
|
||||
draw_panel_b(axB, df, arch_sd, fold_sd, ratio, per_bridge_sd)
|
||||
|
||||
# Subfigure labels
|
||||
for ax, label in ((axA, "A"), (axB, "B")):
|
||||
ax.text(
|
||||
-0.07, 1.03, label, transform=ax.transAxes,
|
||||
ha="left", va="bottom", fontsize=15, fontweight="bold",
|
||||
)
|
||||
|
||||
fig.suptitle(
|
||||
"Variance decomposition: fold-assignment noise vs L1 bridge choice",
|
||||
fontsize=13.0, fontweight="bold", y=1.00,
|
||||
)
|
||||
fig.tight_layout(rect=(0, 0, 1, 0.97))
|
||||
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
fig.savefig(OUT, dpi=180, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
print(f"saved {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
render()
|
||||
Reference in New Issue
Block a user