Files
hypertower/v4/figures/X1_v2m_punch.py
T
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

178 lines
6.6 KiB
Python
Raw 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.
"""F7 — Backbone upgrade: refugelike → refuge V2-M.
Four architecture configurations ordered from least to most complex, each
shown as a paired box plot (refugelike vs refuge_efficientnet_v2_m). Same
F2-style: black-bordered boxes, red median lines, baseline median dashed
reference. Within each group a Wilcoxon p-value compares V2-M to refugelike.
Configs (left → right, increasing architectural complexity):
1. Single-eye img+cd ensemble (no bilateral aggregation)
2. Bilateral img only (bilateral hb, single tower)
3. Bilateral img+cd ensemble (production architecture)
4. Bilateral tritower (img+cd+geom)
Re-run anytime:
python -m v4.figures.X1_v2m_punch
"""
from __future__ import annotations
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
from scipy.stats import wilcoxon
from v4.figures.util.loaders import RESULTS_ROOT
OUT = Path(__file__).parent / "output" / "X1_v2m_backbone.png"
# ── Style ───────────────────────────────────────────────────────────────────
C_REFUGELIKE = "#dd8452" # orange — the older fundus-pretrained baseline
C_REFUGE_V2M = "#4c72b0" # blue — the upgraded fundus-pretrained backbone
C_MEDIAN = "#c44e52" # red — median line
ALPHA = 0.82
# (config_label, refugelike_run_path, refuge_v2m_run_path)
CONFIGS = [
(
"Single\nimg only",
RESULTS_ROOT / "refuge_v2m_baseline" / "img_solo_single_refugelike",
RESULTS_ROOT / "refuge_v2m_baseline" / "img_solo_single_refuge_v2m",
),
(
"Single ensemble\n(img+cd)",
RESULTS_ROOT / "refuge_v2m_baseline" / "ensemble_single_refugelike",
RESULTS_ROOT / "refuge_v2m_baseline" / "ensemble_single_refuge_v2m",
),
(
"Bilateral ensemble\n(img+cd)",
RESULTS_ROOT / "tri_v1" / "baseline_ensemble",
RESULTS_ROOT / "efficientnet" / "refuge_efficientnetv2_m",
),
(
"3-way fusion\n(img+cd+geom)",
RESULTS_ROOT / "tri_v1" / "baseline_tri",
RESULTS_ROOT / "refuge_v2m_baseline" / "tritower",
),
]
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:
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)
def render() -> None:
data = []
for label, refg_path, v2m_path in CONFIGS:
refg = load_fold_aucs(refg_path)
v2m = load_fold_aucs(v2m_path)
data.append((label, refg, v2m))
print(f" {label.replace(chr(10), ' '):<32s} refg n={len(refg):>3d} {refg.mean():.3f}±{refg.std():.3f} "
f"v2m n={len(v2m):>3d} {v2m.mean():.3f}±{v2m.std():.3f}"
if (len(refg) and len(v2m)) else f" {label} pending")
# Layout: 4 groups of 2 boxes
box_w = 0.46
pair_gap = 0.10
group_gap = 0.85
group_width = 2 * box_w + pair_gap
positions: list[tuple[float, float]] = []
p = 0.0
for _ in CONFIGS:
positions.append((p, p + box_w + pair_gap))
p += group_width + group_gap
fig, ax = plt.subplots(figsize=(12.5, 5.8))
fig.suptitle("Backbone Upgrade — refugelike → refuge V2-M",
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 (label, refg, v2m), (xr, xv) in zip(data, positions):
if len(refg):
ax.boxplot(refg, positions=[xr], widths=box_w, patch_artist=True,
manage_ticks=False,
boxprops=dict(facecolor=C_REFUGELIKE, alpha=ALPHA, **boxprops_kw),
medianprops=medianprops, whiskerprops=whiskerprops,
capprops=capprops, flierprops=flierprops)
if len(v2m):
ax.boxplot(v2m, positions=[xv], widths=box_w, patch_artist=True,
manage_ticks=False,
boxprops=dict(facecolor=C_REFUGE_V2M, alpha=ALPHA, **boxprops_kw),
medianprops=medianprops, whiskerprops=whiskerprops,
capprops=capprops, flierprops=flierprops)
# Group tick labels (config name + Wilcoxon p between paired boxes)
tick_x = [(xr + xv) / 2 for xr, xv in positions]
tick_lb = []
for (label, refg, v2m), _ in zip(data, positions):
if len(refg) and len(v2m):
n = min(len(refg), len(v2m))
p_val = _wilcoxon_p(v2m[:n], refg[:n])
ps = f"p={p_val:.3f}" if not np.isnan(p_val) else "p=n/a"
tick_lb.append(f"{label}\n{ps}")
else:
tick_lb.append(label)
ax.set_xticks(tick_x)
ax.set_xticklabels(tick_lb, fontsize=9.5)
# Legend
legend_handles = [
mpatches.Patch(facecolor=C_REFUGELIKE, edgecolor="black",
alpha=ALPHA, label="refugelike (ResNet50 + REFUGE)"),
mpatches.Patch(facecolor=C_REFUGE_V2M, edgecolor="black",
alpha=ALPHA, label="refuge V2-M (EfficientNetV2-M + REFUGE)"),
]
ax.legend(handles=legend_handles, loc="lower right", fontsize=9, framealpha=0.92)
# Limits and grid
xmin = positions[0][0] - box_w
xmax = positions[-1][1] + box_w
ax.set_xlim(xmin - 0.3, xmax + 0.3)
ax.set_ylim(0.55, 1.0)
ax.set_ylabel("Test AUC (10 reps × 5 folds)", fontsize=11)
ax.grid(axis="y", alpha=0.3, linestyle="--")
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()