Add new regression and ensemble experiment configurations for V2-M and OrthoBridge
- Introduced multiple regression experiment configurations targeting vf_md, including: - cd_solo_reg_set.json: CD tower only regression setup. - img_solo_reg_set.json: Image tower only regression setup. - reg_head_epoch_sweep.json: Baseline regression sweeps at different epochs (50, 75, 100). - reg_head_set.json: Various regression setups including baseline and OrthoBridge configurations. - single_eye_reg.json: Single-eye regression setup for worst-eye aggregation analysis. - Added ensemble configurations for OrthoBridge with different inner bridges: - ortho_alts_ensemble.json: Ensemble tests with ConcatBridge, PairwiseAdditiveBridge, and GatedAdditiveBridge. - ortho_alts_tritower.json: Tritower tests with the same inner bridges. - Created V2-M specific configurations: - baseline_reg_nt50.json: Regression baseline with V2-M backbone. - geom_vec_gt.json and geom_vec_unet.json: Geometry vector injection experiments with V2-M. - single_l1_bridges.json: Single-eye ensemble experiments with various bridge types. - tritower_geom_gt.json: Tritower setup with GT contour-rasterized masks. - Promoted existing experiments to higher repetitions for robustness.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
"""V2M_S1 — Adding geometry as a third information source (refuge V2-M backbone).
|
||||
|
||||
V2-M counterpart to S1. Section structure unchanged; runs swapped for V2-M
|
||||
variants where the image stream is present. "solo" still uses the existing
|
||||
geometry-only run since that path doesn't use the image backbone.
|
||||
|
||||
Re-run after data lands:
|
||||
python -m v4.figures.V2M_S1_geometry
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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 wilcoxon
|
||||
|
||||
from v4.figures.util.loaders import RESULTS_ROOT
|
||||
|
||||
OUT = Path(__file__).parent / "output" / "V2M_S1_geometry.png"
|
||||
|
||||
# ── Style (mirrors F2) ───────────────────────────────────────────────────────
|
||||
C_VAR = "#4c72b0" # blue — variant boxes
|
||||
C_BASE = "#dd8452" # orange — baseline reference box
|
||||
C_MEDIAN = "#c44e52" # red — median line
|
||||
ALPHA = 0.82
|
||||
|
||||
|
||||
def _wilcoxon_p(a: np.ndarray, b: np.ndarray) -> float:
|
||||
diffs = a - b
|
||||
if len(diffs) < 5 or np.all(diffs == 0):
|
||||
return float("nan")
|
||||
try:
|
||||
return float(wilcoxon(diffs, alternative="two-sided").pvalue)
|
||||
except Exception:
|
||||
return float("nan")
|
||||
|
||||
|
||||
def load_fold_aucs(run_dir: Path) -> np.ndarray:
|
||||
"""Aggregate test AUC across all rep × fold, picking the run's eval_stage."""
|
||||
import json
|
||||
if not run_dir.exists():
|
||||
return np.array([])
|
||||
out: list[float] = []
|
||||
for rep in sorted(run_dir.glob("rep*")):
|
||||
s = next(iter(rep.rglob("summary.json")), None)
|
||||
if s is None: continue
|
||||
d = json.loads(s.read_text())
|
||||
eval_stage = d.get("eval_stage", "hb")
|
||||
key = f"{eval_stage}_test_auc"
|
||||
for fr in d.get("fold_results", []):
|
||||
v = fr.get(key)
|
||||
if v is not None and np.isfinite(v):
|
||||
out.append(float(v))
|
||||
return np.array(out)
|
||||
|
||||
|
||||
# ── Per-section data definitions ─────────────────────────────────────────────
|
||||
|
||||
# Baseline (used in both sections as reference) — refuge V2-M ensemble, no geometry
|
||||
BASELINE_LABEL = "baseline\n(no geometry)"
|
||||
BASELINE_RUN = RESULTS_ROOT / "efficientnet" / "refuge_efficientnetv2_m"
|
||||
|
||||
# Section A — Vector injection variants at refuge V2-M
|
||||
VECTOR_VARIANTS = [
|
||||
("U-Net vector", RESULTS_ROOT / "v2m_variants" / "ensemble_geom_vec_unet_v2m"),
|
||||
("GT vector", RESULTS_ROOT / "v2m_variants" / "ensemble_geom_vec_gt_v2m"),
|
||||
]
|
||||
|
||||
# Section B — network variants (CNN over segmentation maps) at refuge V2-M
|
||||
NETWORK_VARIANTS = [
|
||||
# Geometry-only network does not use the image backbone, so refugelike data
|
||||
# is the same as V2-M would be.
|
||||
("solo (geom network alone)", RESULTS_ROOT / "tri_v1" / "baseline_solo"),
|
||||
("U-Net fusion", RESULTS_ROOT / "refuge_v2m_baseline" / "tritower"),
|
||||
("GT fusion", RESULTS_ROOT / "v2m_variants" / "tritower_geom_gt_v2m"),
|
||||
]
|
||||
|
||||
|
||||
def render() -> None:
|
||||
base_aucs = load_fold_aucs(BASELINE_RUN)
|
||||
vec_data = [(lbl, load_fold_aucs(p)) for lbl, p in VECTOR_VARIANTS]
|
||||
network_data = [(lbl, load_fold_aucs(p)) for lbl, p in NETWORK_VARIANTS]
|
||||
|
||||
print(f"Baseline (no geometry): n={len(base_aucs):>3d} "
|
||||
f"mean={base_aucs.mean():.3f}±{base_aucs.std():.3f}"
|
||||
if len(base_aucs) else "Baseline: no data")
|
||||
print("Vector injection variants:")
|
||||
for lbl, a in vec_data:
|
||||
print(f" {lbl:<28s} n={len(a):>3d} mean={a.mean():.3f}±{a.std():.3f}"
|
||||
if len(a) else f" {lbl:<28s} pending")
|
||||
print("Network variants:")
|
||||
for lbl, a in network_data:
|
||||
print(f" {lbl:<28s} n={len(a):>3d} mean={a.mean():.3f}±{a.std():.3f}"
|
||||
if len(a) else f" {lbl:<28s} pending")
|
||||
|
||||
# Layout positions
|
||||
box_w = 0.55
|
||||
inner_gap = 0.50
|
||||
section_gap = 0.95
|
||||
|
||||
# Section A: baseline | unet vector | gt vector
|
||||
section_a_labels = [BASELINE_LABEL] + [l for l, _ in vec_data]
|
||||
section_a_data = [base_aucs] + [a for _, a in vec_data]
|
||||
section_a_colors = [C_BASE] + [C_VAR] * len(vec_data)
|
||||
|
||||
# Section B: solo | unet fusion | gt fusion
|
||||
section_b_labels = [l for l, _ in network_data]
|
||||
section_b_data = [a for _, a in network_data]
|
||||
section_b_colors = [C_VAR] * len(network_data)
|
||||
|
||||
positions: list[float] = []
|
||||
p = 0.0
|
||||
for _ in section_a_labels:
|
||||
positions.append(p); p += box_w + inner_gap
|
||||
section_a_right = positions[-1] + box_w / 2
|
||||
p = positions[-1] + box_w + section_gap
|
||||
section_b_left = p
|
||||
for _ in section_b_labels:
|
||||
positions.append(p); p += box_w + inner_gap
|
||||
|
||||
all_labels = section_a_labels + section_b_labels
|
||||
all_data = section_a_data + section_b_data
|
||||
all_colors = section_a_colors + section_b_colors
|
||||
|
||||
fig, ax = plt.subplots(figsize=(12.5, 5.8))
|
||||
fig.suptitle("Geometry Integration — refuge V2-M backbone",
|
||||
fontsize=13, fontweight="bold")
|
||||
|
||||
boxprops_kw = dict(linewidth=1.2, edgecolor="black")
|
||||
medianprops = dict(color=C_MEDIAN, linewidth=2)
|
||||
whiskerprops = dict(color="black", linewidth=1.0)
|
||||
capprops = dict(color="black", linewidth=1.0)
|
||||
flierprops = dict(marker="o", markersize=3, alpha=0.55,
|
||||
markerfacecolor="#888", markeredgecolor="#444")
|
||||
|
||||
for x, aucs, color in zip(positions, all_data, all_colors):
|
||||
if not len(aucs):
|
||||
continue
|
||||
ax.boxplot(
|
||||
aucs, positions=[x], widths=box_w, patch_artist=True, manage_ticks=False,
|
||||
boxprops=dict(facecolor=color, alpha=ALPHA, **boxprops_kw),
|
||||
medianprops=medianprops,
|
||||
whiskerprops=whiskerprops,
|
||||
capprops=capprops,
|
||||
flierprops=flierprops,
|
||||
)
|
||||
|
||||
# Baseline median reference line across the whole plot
|
||||
if len(base_aucs):
|
||||
ax.axhline(np.median(base_aucs), color=C_BASE,
|
||||
linewidth=1.2, linestyle="--", alpha=0.55,
|
||||
label="Baseline median (no geometry)")
|
||||
|
||||
# Section dividers
|
||||
div_x = (section_a_right + section_b_left - box_w / 2) / 2
|
||||
ax.axvline(div_x, color="#aaa", linewidth=0.7, alpha=0.6, linestyle="-")
|
||||
|
||||
# Section headers
|
||||
sec_a_cx = (positions[0] + positions[len(section_a_labels) - 1]) / 2
|
||||
sec_b_cx = (positions[len(section_a_labels)] + positions[-1]) / 2
|
||||
ax.text(sec_a_cx, 1.02, "Vector injection (5-dim structured features)",
|
||||
ha="center", va="bottom", fontsize=11, fontweight="bold", color="#333",
|
||||
transform=ax.get_xaxis_transform())
|
||||
ax.text(sec_b_cx, 1.02, "Geometry network (CNN over segmentation map)",
|
||||
ha="center", va="bottom", fontsize=11, fontweight="bold", color="#333",
|
||||
transform=ax.get_xaxis_transform())
|
||||
|
||||
# X-tick labels with Wilcoxon p-values vs baseline for non-baseline boxes
|
||||
tick_lbls = []
|
||||
for lbl, aucs in zip(all_labels, all_data):
|
||||
if lbl == BASELINE_LABEL or not len(aucs) or not len(base_aucs):
|
||||
tick_lbls.append(lbl); continue
|
||||
n = min(len(aucs), len(base_aucs))
|
||||
p_val = _wilcoxon_p(aucs[:n], base_aucs[:n])
|
||||
ps = f"p={p_val:.3f}" if not np.isnan(p_val) else "p=n/a"
|
||||
tick_lbls.append(f"{lbl}\n{ps}")
|
||||
ax.set_xticks(positions)
|
||||
ax.set_xticklabels(tick_lbls, fontsize=9.5)
|
||||
|
||||
ax.set_xlim(positions[0] - box_w, positions[-1] + box_w + 0.3)
|
||||
ax.set_ylim(0.55, 1.0)
|
||||
ax.set_ylabel("Test AUC", fontsize=11)
|
||||
ax.grid(axis="y", alpha=0.3, linestyle="--")
|
||||
ax.legend(loc="lower left", fontsize=9, framealpha=0.92)
|
||||
|
||||
fig.tight_layout()
|
||||
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