Files
rpotter6298 280060db82 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.
2026-06-11 15:08:20 +02:00

199 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()