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,173 @@
|
||||
"""Post-hoc regression calibration on predictions.h5 files.
|
||||
|
||||
For each fold of a regression run, fit a linear calibration
|
||||
actual_md ≈ a · predicted_md + b
|
||||
on the *val* split, then apply (a, b) to that fold's *test* predictions
|
||||
and report metrics before vs after calibration. No retraining required.
|
||||
|
||||
Usage:
|
||||
python -m v4.scripts.analysis.calibrate_regression \\
|
||||
v4/results/experiments/reg_head/baseline_reg_nt50
|
||||
|
||||
Pass a single predictions.h5 file or a folder; the script finds every
|
||||
predictions.h5 under it and produces a per-rep + aggregate report.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
|
||||
BIN_LABELS = ["<=-10", "-9..-5", "-4..-1", ">=1"]
|
||||
|
||||
|
||||
def _severity_bin(values: np.ndarray) -> np.ndarray:
|
||||
bins = np.full(values.shape, -1, dtype=int)
|
||||
bins[values <= -9.5] = 0
|
||||
bins[(values > -9.5) & (values <= -4.5)] = 1
|
||||
bins[(values > -4.5) & (values <= 0.0)] = 2
|
||||
bins[values > 0.0] = 3
|
||||
return bins
|
||||
|
||||
|
||||
def _decode(arr) -> np.ndarray:
|
||||
return np.array([s.decode("utf-8") if isinstance(s, bytes) else str(s) for s in arr])
|
||||
|
||||
|
||||
def _metrics(y_true: np.ndarray, y_pred: np.ndarray) -> dict:
|
||||
res = y_pred - y_true
|
||||
mse = float(np.mean(res ** 2))
|
||||
abs_res = np.abs(res)
|
||||
out = {
|
||||
"n": int(y_true.size),
|
||||
"mse": mse,
|
||||
"rmse": math.sqrt(mse),
|
||||
"mae": float(np.mean(abs_res)),
|
||||
"bias": float(np.mean(res)),
|
||||
"within_1": float(np.mean(abs_res <= 1.0)),
|
||||
"within_3": float(np.mean(abs_res <= 3.0)),
|
||||
"within_5": float(np.mean(abs_res <= 5.0)),
|
||||
"r": float(np.corrcoef(y_true, y_pred)[0, 1])
|
||||
if y_true.size > 1 and np.std(y_pred) > 0 else float("nan"),
|
||||
}
|
||||
bins_true = _severity_bin(y_true)
|
||||
bins_pred = _severity_bin(y_pred)
|
||||
valid = (bins_true >= 0) & (bins_pred >= 0)
|
||||
out["bin_exact"] = float(np.mean(bins_true[valid] == bins_pred[valid])) if valid.any() else float("nan")
|
||||
out["bin_adjacent"] = float(np.mean(np.abs(bins_true[valid] - bins_pred[valid]) <= 1)) if valid.any() else float("nan")
|
||||
return out
|
||||
|
||||
|
||||
def _fit_linear(y_true: np.ndarray, y_pred: np.ndarray) -> tuple[float, float]:
|
||||
"""Fit y_true = a * y_pred + b via OLS. Returns (a, b)."""
|
||||
if y_pred.size < 2 or np.std(y_pred) == 0:
|
||||
return 1.0, 0.0
|
||||
a, b = np.polyfit(y_pred, y_true, 1)
|
||||
return float(a), float(b)
|
||||
|
||||
|
||||
def calibrate_one(path: Path) -> dict:
|
||||
"""Run val→test linear calibration on one predictions.h5. Returns aggregate metrics."""
|
||||
with h5py.File(path, "r") as f:
|
||||
# Pick the last phase that has a head (heuristic: highest stage)
|
||||
phases = sorted(k for k in f.keys() if isinstance(f[k], h5py.Group) and "logits" in f[k])
|
||||
if not phases:
|
||||
return {"path": str(path), "skipped": "no logits"}
|
||||
# Prefer 'hb' if present
|
||||
phase = "hb" if "hb" in phases else phases[-1]
|
||||
grp = f[phase]
|
||||
logits = grp["logits"][:] # (folds, epochs, samples, heads, outputs)
|
||||
y_true = grp["y_true"][:].astype(float)
|
||||
split = grp["split"][:]
|
||||
n_folds, n_epochs, n_samples, n_heads, _ = logits.shape
|
||||
# Use the last epoch and head 0, output 0
|
||||
ep, head, out = n_epochs - 1, n_heads - 1, 0
|
||||
|
||||
raw_test: list[np.ndarray] = []
|
||||
cal_test: list[np.ndarray] = []
|
||||
truth_test: list[np.ndarray] = []
|
||||
slopes: list[float] = []
|
||||
intercepts: list[float] = []
|
||||
|
||||
for fold in range(n_folds):
|
||||
labels = _decode(split[fold])
|
||||
v_mask = (labels == "val") & np.isfinite(y_true) & np.isfinite(logits[fold, ep, :, head, out])
|
||||
t_mask = (labels == "test") & np.isfinite(y_true) & np.isfinite(logits[fold, ep, :, head, out])
|
||||
if not v_mask.any() or not t_mask.any():
|
||||
continue
|
||||
v_pred = logits[fold, ep, v_mask, head, out].astype(float)
|
||||
v_true = y_true[v_mask]
|
||||
a, b = _fit_linear(v_true, v_pred)
|
||||
slopes.append(a); intercepts.append(b)
|
||||
t_pred = logits[fold, ep, t_mask, head, out].astype(float)
|
||||
t_true = y_true[t_mask]
|
||||
raw_test.append(t_pred)
|
||||
cal_test.append(a * t_pred + b)
|
||||
truth_test.append(t_true)
|
||||
|
||||
if not raw_test:
|
||||
return {"path": str(path), "skipped": "no val/test rows"}
|
||||
|
||||
truth = np.concatenate(truth_test)
|
||||
raw = _metrics(truth, np.concatenate(raw_test))
|
||||
cal = _metrics(truth, np.concatenate(cal_test))
|
||||
return {
|
||||
"path": str(path),
|
||||
"slopes": slopes,
|
||||
"intercepts": intercepts,
|
||||
"raw": raw,
|
||||
"cal": cal,
|
||||
}
|
||||
|
||||
|
||||
def _fmt(row: dict, label: str) -> str:
|
||||
return (f" {label}: mse={row['mse']:.3f} mae={row['mae']:.3f} bias={row['bias']:+.3f} "
|
||||
f"r={row['r']:.3f} bin_exact={row['bin_exact']:.3f} bin_adj={row['bin_adjacent']:.3f} "
|
||||
f"within_1={row['within_1']:.2f} within_3={row['within_3']:.2f}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("path", type=Path, help="predictions.h5 file or directory")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.path.is_file():
|
||||
files = [args.path]
|
||||
else:
|
||||
files = sorted(args.path.rglob("predictions.h5"))
|
||||
if not files:
|
||||
raise SystemExit(f"No predictions.h5 found under {args.path}")
|
||||
|
||||
# Aggregate raw vs cal across all reps
|
||||
agg_raw_truth, agg_raw_pred = [], []
|
||||
agg_cal_truth, agg_cal_pred = [], []
|
||||
all_slopes, all_intercepts = [], []
|
||||
|
||||
for fp in files:
|
||||
res = calibrate_one(fp)
|
||||
if res.get("skipped"):
|
||||
print(f"\n{fp.parent.parent.name}: SKIPPED ({res['skipped']})")
|
||||
continue
|
||||
rel = fp.relative_to(args.path) if args.path.is_dir() else fp.name
|
||||
print(f"\n{rel}")
|
||||
print(f" fold-wise slopes: {', '.join(f'{a:.2f}' for a in res['slopes'])}")
|
||||
print(f" fold-wise intercepts: {', '.join(f'{b:+.2f}' for b in res['intercepts'])}")
|
||||
print(_fmt(res["raw"], "raw "))
|
||||
print(_fmt(res["cal"], "cal "))
|
||||
all_slopes.extend(res["slopes"])
|
||||
all_intercepts.extend(res["intercepts"])
|
||||
|
||||
if all_slopes:
|
||||
print(
|
||||
f"\nCalibration parameters across all reps × folds (n={len(all_slopes)}):\n"
|
||||
f" slope: mean={np.mean(all_slopes):.3f} ± {np.std(all_slopes):.3f} "
|
||||
f"min={np.min(all_slopes):.2f} max={np.max(all_slopes):.2f}\n"
|
||||
f" intercept: mean={np.mean(all_intercepts):+.3f} ± {np.std(all_intercepts):.3f}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,349 @@
|
||||
"""Plot regression predictions against actual values from v4 predictions.h5.
|
||||
|
||||
Usage:
|
||||
python -m v4.scripts.analysis.plot_regression_predictions \
|
||||
v4/results/experiments/reg_head/baseline_reg
|
||||
|
||||
python -m v4.scripts.analysis.plot_regression_predictions \
|
||||
v4/results/experiments/reg_head/baseline_reg/rep00/binary/predictions.h5 \
|
||||
--split val --phase hb --head hb_head --glaucoma-only
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import h5py
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _decode_array(arr: np.ndarray) -> np.ndarray:
|
||||
return np.array([v.decode() if isinstance(v, bytes) else str(v) for v in arr])
|
||||
|
||||
|
||||
def _head_names(grp: h5py.Group) -> list[str]:
|
||||
return [v.decode() if isinstance(v, bytes) else str(v) for v in grp["head_names"][:]]
|
||||
|
||||
|
||||
def _find_prediction_files(path: Path) -> list[Path]:
|
||||
if path.is_file():
|
||||
if path.name != "predictions.h5":
|
||||
raise SystemExit(f"Expected predictions.h5 file, got: {path}")
|
||||
return [path]
|
||||
if not path.is_dir():
|
||||
raise SystemExit(f"Not a file or directory: {path}")
|
||||
files = sorted(path.rglob("predictions.h5"))
|
||||
if not files:
|
||||
raise SystemExit(f"No predictions.h5 files found under: {path}")
|
||||
return files
|
||||
|
||||
|
||||
def _choose_phase(f: h5py.File, phase: str | None) -> str:
|
||||
phases = sorted(k for k in f.keys() if isinstance(f[k], h5py.Group) and "logits" in f[k])
|
||||
if not phases:
|
||||
raise ValueError("No phase groups containing a logits dataset were found")
|
||||
if phase is None:
|
||||
return phases[0]
|
||||
if phase not in phases:
|
||||
raise ValueError(f"Phase {phase!r} not found. Available phases: {', '.join(phases)}")
|
||||
return phase
|
||||
|
||||
|
||||
def _choose_head(grp: h5py.Group, head: str | None) -> tuple[str, int]:
|
||||
names = _head_names(grp)
|
||||
if not names:
|
||||
raise ValueError("No head names found")
|
||||
if head is None:
|
||||
return names[0], 0
|
||||
if head not in names:
|
||||
raise ValueError(f"Head {head!r} not found. Available heads: {', '.join(names)}")
|
||||
return head, names.index(head)
|
||||
|
||||
|
||||
def _collect_points(
|
||||
path: Path,
|
||||
*,
|
||||
phase: str | None,
|
||||
head: str | None,
|
||||
split: str,
|
||||
epoch: int,
|
||||
output_index: int,
|
||||
glaucoma_only: bool,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, str, str]:
|
||||
with h5py.File(path, "r") as f:
|
||||
phase_name = _choose_phase(f, phase)
|
||||
grp = f[phase_name]
|
||||
head_name, head_idx = _choose_head(grp, head)
|
||||
|
||||
logits = grp["logits"]
|
||||
n_folds, n_epochs, n_samples, _, n_outputs = logits.shape
|
||||
epoch_idx = epoch if epoch >= 0 else n_epochs + epoch
|
||||
if not 0 <= epoch_idx < n_epochs:
|
||||
raise ValueError(f"Epoch {epoch} is out of range for {n_epochs} epochs")
|
||||
if not 0 <= output_index < n_outputs:
|
||||
raise ValueError(f"Output index {output_index} is out of range for {n_outputs} outputs")
|
||||
|
||||
y_true_all = grp["y_true"][:].astype(float)
|
||||
split_all = grp["split"][:]
|
||||
|
||||
actuals: list[np.ndarray] = []
|
||||
preds: list[np.ndarray] = []
|
||||
folds: list[np.ndarray] = []
|
||||
for fold in range(n_folds):
|
||||
labels = _decode_array(split_all[fold])
|
||||
mask = np.ones(n_samples, dtype=bool) if split == "all" else labels == split
|
||||
if glaucoma_only:
|
||||
mask &= y_true_all != 0
|
||||
y_pred = logits[fold, epoch_idx, :, head_idx, output_index].astype(float)
|
||||
finite = mask & np.isfinite(y_true_all) & np.isfinite(y_pred)
|
||||
if not finite.any():
|
||||
continue
|
||||
actuals.append(y_true_all[finite])
|
||||
preds.append(y_pred[finite])
|
||||
folds.append(np.full(int(finite.sum()), fold, dtype=int))
|
||||
|
||||
if not actuals:
|
||||
suffix = " and y_true != 0" if glaucoma_only else ""
|
||||
raise ValueError(f"No finite rows found for split={split!r}{suffix}")
|
||||
return (
|
||||
np.concatenate(actuals),
|
||||
np.concatenate(preds),
|
||||
np.concatenate(folds),
|
||||
phase_name,
|
||||
head_name,
|
||||
)
|
||||
|
||||
|
||||
def _metrics(y_true: np.ndarray, y_pred: np.ndarray) -> dict[str, float]:
|
||||
residual = y_pred - y_true
|
||||
mse = float(np.mean(residual ** 2))
|
||||
abs_residual = np.abs(residual)
|
||||
out = {
|
||||
"n": float(y_true.size),
|
||||
"mse": mse,
|
||||
"rmse": math.sqrt(mse),
|
||||
"mae": float(np.mean(abs_residual)),
|
||||
"bias": float(np.mean(residual)),
|
||||
"within_1": float(np.mean(abs_residual <= 1.0)),
|
||||
"within_3": float(np.mean(abs_residual <= 3.0)),
|
||||
"within_5": float(np.mean(abs_residual <= 5.0)),
|
||||
}
|
||||
if y_true.size > 1 and np.std(y_true) > 0 and np.std(y_pred) > 0:
|
||||
out["r"] = float(np.corrcoef(y_true, y_pred)[0, 1])
|
||||
else:
|
||||
out["r"] = float("nan")
|
||||
return out
|
||||
|
||||
|
||||
BIN_LABELS = ["<=-10", "-9..-5", "-4..-1", ">=1"]
|
||||
|
||||
|
||||
def _severity_bin(values: np.ndarray) -> np.ndarray:
|
||||
"""Map continuous values onto glaucoma severity bins.
|
||||
|
||||
Actual labels use integer bins: <=-10, -9..-5, -4..-1, >=1.
|
||||
Predictions are continuous, so boundaries are placed halfway between
|
||||
adjacent integer bins: -9.5, -4.5, and 0.0.
|
||||
"""
|
||||
bins = np.full(values.shape, -1, dtype=int)
|
||||
bins[values <= -9.5] = 0
|
||||
bins[(values > -9.5) & (values <= -4.5)] = 1
|
||||
bins[(values > -4.5) & (values <= 0.0)] = 2
|
||||
bins[values > 0.0] = 3
|
||||
return bins
|
||||
|
||||
|
||||
def _bin_summary_lines(y_true: np.ndarray, y_pred: np.ndarray) -> list[str]:
|
||||
actual_bins = _severity_bin(y_true)
|
||||
pred_bins = _severity_bin(y_pred)
|
||||
valid = (actual_bins >= 0) & (pred_bins >= 0)
|
||||
if not valid.any():
|
||||
return ["Bin summary: no rows matched the configured severity bins"]
|
||||
|
||||
actual_bins = actual_bins[valid]
|
||||
pred_bins = pred_bins[valid]
|
||||
confusion = np.zeros((len(BIN_LABELS), len(BIN_LABELS)), dtype=int)
|
||||
for actual, pred in zip(actual_bins, pred_bins):
|
||||
confusion[actual, pred] += 1
|
||||
|
||||
exact = float(np.mean(actual_bins == pred_bins))
|
||||
adjacent = float(np.mean(np.abs(actual_bins - pred_bins) <= 1))
|
||||
lines = [
|
||||
f"Bin summary: exact={exact:.3f} within_adjacent={adjacent:.3f}",
|
||||
" actual/pred " + " ".join(f"{label:>8s}" for label in BIN_LABELS),
|
||||
]
|
||||
for idx, label in enumerate(BIN_LABELS):
|
||||
row = confusion[idx]
|
||||
n = int(row.sum())
|
||||
row_text = " ".join(f"{v:8d}" for v in row)
|
||||
lines.append(f" {label:>11s} n={n:3d} {row_text}")
|
||||
|
||||
lines.append(" per-actual-bin:")
|
||||
for idx, label in enumerate(BIN_LABELS):
|
||||
mask = actual_bins == idx
|
||||
if not mask.any():
|
||||
continue
|
||||
stats = _metrics(y_true[valid][mask], y_pred[valid][mask])
|
||||
lines.append(
|
||||
f" {label:>7s} n={stats['n']:.0f} "
|
||||
f"mean_pred={float(np.mean(y_pred[valid][mask])):.2f} "
|
||||
f"bias={stats['bias']:.2f} mae={stats['mae']:.2f} rmse={stats['rmse']:.2f} "
|
||||
f"within_3={stats['within_3']:.3f} within_5={stats['within_5']:.3f}"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def _plot_one(
|
||||
y_true: np.ndarray,
|
||||
y_pred: np.ndarray,
|
||||
folds: np.ndarray,
|
||||
*,
|
||||
title: str,
|
||||
output: Path,
|
||||
dpi: int,
|
||||
) -> None:
|
||||
stats = _metrics(y_true, y_pred)
|
||||
residual = y_pred - y_true
|
||||
|
||||
lo = float(np.nanmin([y_true.min(), y_pred.min()]))
|
||||
hi = float(np.nanmax([y_true.max(), y_pred.max()]))
|
||||
pad = max((hi - lo) * 0.05, 1.0)
|
||||
lo -= pad
|
||||
hi += pad
|
||||
|
||||
fig, (ax_scatter, ax_resid) = plt.subplots(
|
||||
1,
|
||||
2,
|
||||
figsize=(11.5, 5.0),
|
||||
gridspec_kw={"width_ratios": [1.4, 1.0]},
|
||||
constrained_layout=True,
|
||||
)
|
||||
|
||||
scatter = ax_scatter.scatter(
|
||||
y_true,
|
||||
y_pred,
|
||||
c=folds,
|
||||
cmap="tab10",
|
||||
s=34,
|
||||
alpha=0.72,
|
||||
linewidths=0,
|
||||
)
|
||||
ax_scatter.plot([lo, hi], [lo, hi], color="black", linewidth=1.2, linestyle="--", label="ideal")
|
||||
if y_true.size > 1:
|
||||
slope, intercept = np.polyfit(y_true, y_pred, deg=1)
|
||||
ax_scatter.plot(
|
||||
[lo, hi],
|
||||
[slope * lo + intercept, slope * hi + intercept],
|
||||
color="#b03a2e",
|
||||
linewidth=1.4,
|
||||
label=f"fit: y={slope:.2f}x{intercept:+.2f}",
|
||||
)
|
||||
ax_scatter.set_xlim(lo, hi)
|
||||
ax_scatter.set_ylim(lo, hi)
|
||||
ax_scatter.set_aspect("equal", adjustable="box")
|
||||
ax_scatter.set_xlabel("Actual")
|
||||
ax_scatter.set_ylabel("Predicted")
|
||||
ax_scatter.set_title(title)
|
||||
ax_scatter.grid(True, color="#e6e6e6", linewidth=0.8)
|
||||
ax_scatter.legend(loc="upper left", frameon=False)
|
||||
|
||||
cbar = fig.colorbar(scatter, ax=ax_scatter, fraction=0.046, pad=0.04)
|
||||
cbar.set_label("Fold")
|
||||
|
||||
text = (
|
||||
f"n={stats['n']:.0f}\n"
|
||||
f"MSE={stats['mse']:.3f}\n"
|
||||
f"RMSE={stats['rmse']:.3f}\n"
|
||||
f"MAE={stats['mae']:.3f}\n"
|
||||
f"bias={stats['bias']:.3f}\n"
|
||||
f"r={stats['r']:.3f}\n"
|
||||
f"±1={stats['within_1']:.3f}\n"
|
||||
f"±3={stats['within_3']:.3f}\n"
|
||||
f"±5={stats['within_5']:.3f}"
|
||||
)
|
||||
ax_scatter.text(
|
||||
0.98,
|
||||
0.02,
|
||||
text,
|
||||
transform=ax_scatter.transAxes,
|
||||
ha="right",
|
||||
va="bottom",
|
||||
fontsize=9,
|
||||
bbox={"boxstyle": "round,pad=0.35", "facecolor": "white", "edgecolor": "#cccccc", "alpha": 0.92},
|
||||
)
|
||||
|
||||
bins = min(30, max(8, int(np.sqrt(residual.size))))
|
||||
ax_resid.hist(residual, bins=bins, color="#4c78a8", alpha=0.85, edgecolor="white")
|
||||
ax_resid.axvline(0, color="black", linewidth=1.1, linestyle="--")
|
||||
ax_resid.axvline(stats["bias"], color="#b03a2e", linewidth=1.4, label="mean residual")
|
||||
ax_resid.set_xlabel("Predicted - actual")
|
||||
ax_resid.set_ylabel("Count")
|
||||
ax_resid.set_title("Residuals")
|
||||
ax_resid.grid(True, axis="y", color="#e6e6e6", linewidth=0.8)
|
||||
ax_resid.legend(loc="upper right", frameon=False)
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
fig.savefig(output, dpi=dpi)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def default_output_path(path: Path, phase: str, head: str, split: str, label: str) -> Path:
|
||||
stem = f"regression_predictions_{phase}_{head}_{split}"
|
||||
if label:
|
||||
stem += f"_{label}"
|
||||
stem += ".png"
|
||||
return path.with_name(stem)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("path", type=Path, help="A predictions.h5 file or a directory containing predictions.h5 files")
|
||||
ap.add_argument("--phase", default=None, help="Phase group to plot; defaults to the first logits phase")
|
||||
ap.add_argument("--head", default=None, help="Head name to plot; defaults to the first head")
|
||||
ap.add_argument("--split", default="test", help="Split label to plot: test, val, train, or all")
|
||||
ap.add_argument("--epoch", type=int, default=-1, help="Epoch index to plot; negative values count from the end")
|
||||
ap.add_argument("--output-index", type=int, default=0, help="Regression output index within logits' final axis")
|
||||
ap.add_argument("--glaucoma-only", action="store_true", help="Only plot rows where actual y_true is non-zero")
|
||||
ap.add_argument("--out-dir", type=Path, default=None, help="Optional directory for all output PNGs")
|
||||
ap.add_argument("--bin-summary", action="store_true", help="Print severity-bin accuracy and confusion matrix")
|
||||
ap.add_argument("--dpi", type=int, default=160, help="Output PNG resolution")
|
||||
args = ap.parse_args()
|
||||
|
||||
filter_label = "glaucoma_only" if args.glaucoma_only else ""
|
||||
for pred_path in _find_prediction_files(args.path):
|
||||
y_true, y_pred, folds, phase, head = _collect_points(
|
||||
pred_path,
|
||||
phase=args.phase,
|
||||
head=args.head,
|
||||
split=args.split,
|
||||
epoch=args.epoch,
|
||||
output_index=args.output_index,
|
||||
glaucoma_only=args.glaucoma_only,
|
||||
)
|
||||
rel_title = pred_path.parent.as_posix()
|
||||
filter_text = ", glaucoma only" if args.glaucoma_only else ""
|
||||
title = f"{rel_title}\nphase={phase}, head={head}, split={args.split}, epoch={args.epoch}{filter_text}"
|
||||
if args.out_dir is None:
|
||||
out_path = default_output_path(pred_path, phase, head, args.split, filter_label)
|
||||
else:
|
||||
rep_name = "_".join(pred_path.parent.parts[-3:])
|
||||
suffix = f"_{filter_label}" if filter_label else ""
|
||||
out_path = args.out_dir / f"{rep_name}_{phase}_{head}_{args.split}{suffix}.png"
|
||||
_plot_one(y_true, y_pred, folds, title=title, output=out_path, dpi=args.dpi)
|
||||
stats = _metrics(y_true, y_pred)
|
||||
print(
|
||||
f"{out_path} n={stats['n']:.0f} "
|
||||
f"mse={stats['mse']:.4f} rmse={stats['rmse']:.4f} "
|
||||
f"mae={stats['mae']:.4f} r={stats['r']:.4f} "
|
||||
f"within_1={stats['within_1']:.3f} "
|
||||
f"within_3={stats['within_3']:.3f} "
|
||||
f"within_5={stats['within_5']:.3f}"
|
||||
)
|
||||
if args.bin_summary:
|
||||
print("\n".join(_bin_summary_lines(y_true, y_pred)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,188 @@
|
||||
"""3-bin severity confusion matrix for vf_md regression heads.
|
||||
|
||||
Reads predictions.h5 files from a regression run, bins both actual and predicted
|
||||
MD into 3 severity tiers (severe / moderate / no-problem), and reports:
|
||||
|
||||
* confusion matrix (counts and per-row %)
|
||||
* exact-bin & adjacent-bin accuracy
|
||||
* per-bin recall
|
||||
* binary "no-problem vs disease" sensitivity/specificity at the −4.5 dB boundary
|
||||
* optional saved heatmap PNG
|
||||
|
||||
Bin boundaries (placed halfway between integer bins, matching plot_regression_predictions):
|
||||
severe : vf_md <= -9.5
|
||||
moderate : -9.5 < vf_md <= -4.5
|
||||
no-problem : vf_md > -4.5
|
||||
|
||||
Usage:
|
||||
python -m v4.scripts.analysis.severity_confusion \\
|
||||
v4/results/experiments/reg_head/baseline_reg_nt50 \\
|
||||
--save-fig analysis/figures/regression_severity_confusion.png
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
|
||||
LABELS = ["severe (≤−10)", "moderate (−9..−5)", "no-problem (≥−4)"]
|
||||
|
||||
|
||||
def severity_3bin(values: np.ndarray) -> np.ndarray:
|
||||
bins = np.full(values.shape, -1, dtype=int)
|
||||
bins[values <= -9.5] = 0
|
||||
bins[(values > -9.5) & (values <= -4.5)] = 1
|
||||
bins[values > -4.5] = 2
|
||||
return bins
|
||||
|
||||
|
||||
def _decode(arr) -> np.ndarray:
|
||||
return np.array([s.decode("utf-8") if isinstance(s, bytes) else str(s) for s in arr])
|
||||
|
||||
|
||||
def collect_test_predictions(path: Path) -> tuple[np.ndarray, np.ndarray] | None:
|
||||
with h5py.File(path, "r") as f:
|
||||
if "hb" not in f:
|
||||
return None
|
||||
grp = f["hb"]
|
||||
logits = grp["logits"][:]
|
||||
y_true = grp["y_true"][:].astype(float)
|
||||
split = grp["split"][:]
|
||||
n_folds, n_epochs, _, n_heads, _ = logits.shape
|
||||
ep, head, out = n_epochs - 1, n_heads - 1, 0
|
||||
actuals: list[np.ndarray] = []
|
||||
preds: list[np.ndarray] = []
|
||||
for fold in range(n_folds):
|
||||
labels = _decode(split[fold])
|
||||
mask = (labels == "test") & np.isfinite(y_true) & np.isfinite(logits[fold, ep, :, head, out])
|
||||
if not mask.any():
|
||||
continue
|
||||
actuals.append(y_true[mask])
|
||||
preds.append(logits[fold, ep, mask, head, out].astype(float))
|
||||
if not actuals:
|
||||
return None
|
||||
return np.concatenate(actuals), np.concatenate(preds)
|
||||
|
||||
|
||||
def report(actuals: np.ndarray, preds: np.ndarray) -> dict:
|
||||
ab = severity_3bin(actuals)
|
||||
pb = severity_3bin(preds)
|
||||
valid = (ab >= 0) & (pb >= 0)
|
||||
ab, pb = ab[valid], pb[valid]
|
||||
cm = np.zeros((3, 3), dtype=int)
|
||||
for x, y in zip(ab, pb):
|
||||
cm[x, y] += 1
|
||||
|
||||
# Binary disease vs no-problem (bins 0+1 vs bin 2)
|
||||
actual_disease = ab <= 1
|
||||
pred_disease = pb <= 1
|
||||
tp = int(np.sum(actual_disease & pred_disease))
|
||||
tn = int(np.sum(~actual_disease & ~pred_disease))
|
||||
fp = int(np.sum(~actual_disease & pred_disease))
|
||||
fn = int(np.sum(actual_disease & ~pred_disease))
|
||||
sens = tp / max(tp + fn, 1)
|
||||
spec = tn / max(tn + fp, 1)
|
||||
|
||||
return {
|
||||
"confusion": cm,
|
||||
"n_test": int(ab.size),
|
||||
"exact_acc": float(np.mean(ab == pb)),
|
||||
"adjacent_acc": float(np.mean(np.abs(ab - pb) <= 1)),
|
||||
"recall_per_bin": [float(np.mean(pb[ab == i] == i)) if (ab == i).any() else float("nan")
|
||||
for i in range(3)],
|
||||
"n_per_bin": [int((ab == i).sum()) for i in range(3)],
|
||||
"binary_sens": sens,
|
||||
"binary_spec": spec,
|
||||
"binary_tp": tp,
|
||||
"binary_fp": fp,
|
||||
"binary_fn": fn,
|
||||
"binary_tn": tn,
|
||||
}
|
||||
|
||||
|
||||
def print_report(r: dict) -> None:
|
||||
cm = r["confusion"]
|
||||
print(f"\nn_test (pooled across reps × folds): {r['n_test']}")
|
||||
print(f"\nConfusion matrix (rows = actual, cols = predicted):")
|
||||
print(f"{'actual \\ pred':<22s} {LABELS[0]:>16s} {LABELS[1]:>20s} {LABELS[2]:>18s} n")
|
||||
for i in range(3):
|
||||
row = cm[i]
|
||||
print(f"{LABELS[i]:<22s} {row[0]:>16d} {row[1]:>20d} {row[2]:>18d} {row.sum()}")
|
||||
print(f"\nExact-bin accuracy: {r['exact_acc']:.3f}")
|
||||
print(f"Adjacent-bin accuracy: {r['adjacent_acc']:.3f}")
|
||||
print("\nPer-bin recall:")
|
||||
for i in range(3):
|
||||
n = r["n_per_bin"][i]
|
||||
rec = r["recall_per_bin"][i]
|
||||
print(f" {LABELS[i]:<22s} n={n:>4d} recall={rec:.3f}")
|
||||
print(f"\nBinary disease (severe+moderate) vs no-problem, threshold = −4.5 dB:")
|
||||
print(f" sensitivity (correctly flag disease): {r['binary_sens']:.3f} ({r['binary_tp']}/{r['binary_tp']+r['binary_fn']})")
|
||||
print(f" specificity (correctly clear healthy): {r['binary_spec']:.3f} ({r['binary_tn']}/{r['binary_tn']+r['binary_fp']})")
|
||||
|
||||
|
||||
def save_heatmap(r: dict, path: Path) -> None:
|
||||
import matplotlib.pyplot as plt
|
||||
cm = r["confusion"]
|
||||
cm_pct = cm / np.maximum(cm.sum(axis=1, keepdims=True), 1)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6.5, 5.5))
|
||||
im = ax.imshow(cm_pct, cmap="Blues", vmin=0, vmax=1, aspect="equal")
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
ax.text(j, i, f"{cm[i,j]}\n({cm_pct[i,j]*100:.1f}%)",
|
||||
ha="center", va="center",
|
||||
color="white" if cm_pct[i,j] > 0.5 else "black",
|
||||
fontsize=10)
|
||||
ax.set_xticks(range(3)); ax.set_xticklabels(LABELS, rotation=20, ha="right")
|
||||
ax.set_yticks(range(3)); ax.set_yticklabels(LABELS)
|
||||
ax.set_xlabel("Predicted")
|
||||
ax.set_ylabel("Actual")
|
||||
ax.set_title(f"VF-MD severity confusion (n={r['n_test']})\n"
|
||||
f"exact={r['exact_acc']:.3f} adjacent={r['adjacent_acc']:.3f} "
|
||||
f"sens={r['binary_sens']:.3f} spec={r['binary_spec']:.3f}")
|
||||
fig.colorbar(im, ax=ax, label="Row-normalised fraction")
|
||||
fig.tight_layout()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fig.savefig(path, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
print(f"\nSaved heatmap: {path}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("path", type=Path, help="A predictions.h5 file or a directory containing them")
|
||||
ap.add_argument("--save-fig", type=Path, default=None, help="Optional path to save the confusion-matrix heatmap PNG")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.path.is_file():
|
||||
files = [args.path]
|
||||
else:
|
||||
files = sorted(args.path.rglob("predictions.h5"))
|
||||
if not files:
|
||||
raise SystemExit(f"No predictions.h5 under {args.path}")
|
||||
|
||||
all_actual: list[np.ndarray] = []
|
||||
all_pred: list[np.ndarray] = []
|
||||
for fp in files:
|
||||
res = collect_test_predictions(fp)
|
||||
if res is None:
|
||||
print(f" skipped (no hb predictions): {fp}")
|
||||
continue
|
||||
a, p = res
|
||||
all_actual.append(a); all_pred.append(p)
|
||||
if not all_actual:
|
||||
raise SystemExit("No usable predictions found")
|
||||
|
||||
actuals = np.concatenate(all_actual)
|
||||
preds = np.concatenate(all_pred)
|
||||
print(f"Pooled across {len(all_actual)} predictions.h5 files")
|
||||
r = report(actuals, preds)
|
||||
print_report(r)
|
||||
if args.save_fig:
|
||||
save_heatmap(r, args.save_fig)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -39,14 +39,20 @@ def find_rep_summaries(run_dir: Path) -> list[tuple[int, Path]]:
|
||||
|
||||
|
||||
def load_rep(summary_path: Path) -> dict:
|
||||
"""Extract the fields we summarise from one rep's summary.json."""
|
||||
"""Extract the fields we summarise from one rep's summary.json.
|
||||
|
||||
Uses `primary_metric` if present (for regression runs e.g. neg_mse),
|
||||
falling back to AUC for legacy classification runs.
|
||||
"""
|
||||
d = json.loads(summary_path.read_text())
|
||||
pm = d.get("primary_metric", "auc")
|
||||
return {
|
||||
"val_mean": float(d.get("mean_val_auc", float("nan"))),
|
||||
"val_std": float(d.get("std_val_auc", float("nan"))),
|
||||
"test_mean": float(d.get("mean_test_auc", float("nan"))),
|
||||
"test_std": float(d.get("std_test_auc", float("nan"))),
|
||||
"elapsed_s": float(d.get("elapsed_s", float("nan"))),
|
||||
"primary": pm,
|
||||
"val_mean": float(d.get(f"mean_val_{pm}", d.get("mean_val_auc", float("nan")))),
|
||||
"val_std": float(d.get(f"std_val_{pm}", d.get("std_val_auc", float("nan")))),
|
||||
"test_mean": float(d.get(f"mean_test_{pm}", d.get("mean_test_auc", float("nan")))),
|
||||
"test_std": float(d.get(f"std_test_{pm}", d.get("std_test_auc", float("nan")))),
|
||||
"elapsed_s": float(d.get("elapsed_s", float("nan"))),
|
||||
"eval_stage": d.get("eval_stage", "?"),
|
||||
}
|
||||
|
||||
@@ -63,6 +69,7 @@ def summarise(run_dir: Path) -> dict:
|
||||
"run": str(run_dir),
|
||||
"n_reps": len(rows),
|
||||
"eval_stage": rows[0][1]["eval_stage"],
|
||||
"primary": rows[0][1]["primary"],
|
||||
"val_mean": float(np.mean(val)),
|
||||
"val_std": float(np.std(val)),
|
||||
"val_min": float(np.min(val)),
|
||||
@@ -83,12 +90,13 @@ def render(s: dict, per_rep: bool = False) -> str:
|
||||
if s["n_reps"] == 0:
|
||||
return f"Run: {s['run']}\n no reps with summary.json found."
|
||||
|
||||
metric = s.get("primary", "auc")
|
||||
lines = [
|
||||
f"Run: {s['run']}",
|
||||
f"Reps: {s['n_reps']} (eval_stage={s['eval_stage']})",
|
||||
f"Val AUC: {s['val_mean']:.4f} ± {s['val_std']:.4f} "
|
||||
f"Reps: {s['n_reps']} (eval_stage={s['eval_stage']}, metric={metric})",
|
||||
f"Val {metric}: {s['val_mean']:.4f} ± {s['val_std']:.4f} "
|
||||
f"[min={s['val_min']:.4f} max={s['val_max']:.4f}]",
|
||||
f"Test AUC: {s['test_mean']:.4f} ± {s['test_std']:.4f} "
|
||||
f"Test {metric}: {s['test_mean']:.4f} ± {s['test_std']:.4f} "
|
||||
f"[min={s['test_min']:.4f} max={s['test_max']:.4f}]",
|
||||
]
|
||||
if s.get("elapsed_total_s") is not None:
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
"""fetch_tcga_brca — programmatic download of TCGA-BRCA multimodal data.
|
||||
|
||||
Two-phase fetch:
|
||||
|
||||
Phase 1 — TABULAR (cBioPortal bulk distribution):
|
||||
- Clinical fields (~90 columns: stage, grade, treatment, vital status, etc.)
|
||||
- RPPA protein expression (~200 proteins)
|
||||
- mRNA expression (~20,000 genes; optional)
|
||||
- Mutations (MAF)
|
||||
- All pre-joined by sample ID and cleaned by Broad/MSK curation
|
||||
- One tarball, ~200 MB compressed, fast download
|
||||
- Source: https://cbioportal-datahub.s3.amazonaws.com/
|
||||
- Curated study: brca_tcga_pan_can_atlas_2018
|
||||
|
||||
Phase 2 — PATHOLOGY IMAGES (GDC API):
|
||||
- Diagnostic image thumbnails (small, JPG-like, ~MBs each — manageable)
|
||||
- Or full SVS slide images (gigapixel, ~100s of MB each — heavy)
|
||||
- Uses GDC's REST API to build a manifest, then downloads files
|
||||
- Source: https://api.gdc.cancer.gov/
|
||||
|
||||
Usage:
|
||||
python -m v4.scripts.data.fetch_tcga_brca --out data/tcga_brca
|
||||
python -m v4.scripts.data.fetch_tcga_brca --out data/tcga_brca --skip-images
|
||||
python -m v4.scripts.data.fetch_tcga_brca --out data/tcga_brca --images diagnostic --limit 50
|
||||
|
||||
All TCGA-BRCA data downloaded here is in GDC's *open-access* tier — no DUA,
|
||||
no controlled-access approval needed. Standard NIH attribution required for
|
||||
publications.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CBIOPORTAL_STUDY = "brca_tcga_pan_can_atlas_2018"
|
||||
# cBioPortal datahub stores files in a GitHub repo with LFS. The S3 bucket
|
||||
# is no longer publicly accessible, so we pull individual files from GitHub.
|
||||
# Large data files are stored via LFS (different endpoint); small meta/text
|
||||
# files are regular git blobs. We try LFS first, fall back to raw.
|
||||
CBIOPORTAL_LFS_BASE = (
|
||||
f"https://media.githubusercontent.com/media/cBioPortal/datahub/master/public/{CBIOPORTAL_STUDY}"
|
||||
)
|
||||
CBIOPORTAL_RAW_BASE = (
|
||||
f"https://raw.githubusercontent.com/cBioPortal/datahub/master/public/{CBIOPORTAL_STUDY}"
|
||||
)
|
||||
|
||||
# Curated file list for the BRCA Pan-Cancer Atlas 2018 study.
|
||||
CBIOPORTAL_FILES_ESSENTIAL = [
|
||||
"data_clinical_patient.txt", # ~90 clinical fields per patient
|
||||
"data_clinical_sample.txt", # sample-level annotations
|
||||
"data_rppa.txt", # RPPA protein expression (~200 proteins)
|
||||
"data_rppa_zscores.txt", # RPPA z-scored against normal samples
|
||||
"meta_clinical_patient.txt",
|
||||
"meta_clinical_sample.txt",
|
||||
"meta_rppa.txt",
|
||||
"meta_study.txt",
|
||||
]
|
||||
CBIOPORTAL_FILES_OPTIONAL = [
|
||||
"data_protein_quantification.txt", # mass-spec proteomics (CPTAC) — richer than RPPA
|
||||
"data_phosphoprotein_quantification.txt", # phosphoproteomics
|
||||
"data_protein_quantification_zscores.txt",
|
||||
"data_mutations.txt", # MAF — somatic mutations
|
||||
"data_cna.txt", # copy-number alterations (gistic)
|
||||
"data_mrna_seq_v2_rsem.txt", # RNA-seq counts (LARGE, ~150 MB)
|
||||
"data_mrna_seq_v2_rsem_zscores_ref_normal_samples.txt",
|
||||
]
|
||||
|
||||
GDC_API_FILES = "https://api.gdc.cancer.gov/files"
|
||||
GDC_API_DATA = "https://api.gdc.cancer.gov/data"
|
||||
|
||||
USER_AGENT = "hypertower-data-fetch/1.0 (research; python urllib)"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 1: cBioPortal tabular bundle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def fetch_cbioportal(out_dir: Path, include_optional: bool = False) -> Path:
|
||||
"""Download cBioPortal TCGA-BRCA Pan-Cancer Atlas files via GitHub LFS."""
|
||||
study_dir = out_dir / "cbioportal" / CBIOPORTAL_STUDY
|
||||
study_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
files = list(CBIOPORTAL_FILES_ESSENTIAL)
|
||||
if include_optional:
|
||||
files += CBIOPORTAL_FILES_OPTIONAL
|
||||
|
||||
print(f"[cBioPortal] downloading {len(files)} files from datahub")
|
||||
print(f" → {study_dir}")
|
||||
failed = []
|
||||
for fname in files:
|
||||
dest = study_dir / fname
|
||||
if dest.exists() and dest.stat().st_size > 0:
|
||||
print(f" · {fname} (already present, {dest.stat().st_size/1e6:.2f} MB)")
|
||||
continue
|
||||
# Try LFS first (for large data files), then raw (for small meta files).
|
||||
last_err = None
|
||||
for url in (f"{CBIOPORTAL_LFS_BASE}/{fname}",
|
||||
f"{CBIOPORTAL_RAW_BASE}/{fname}"):
|
||||
try:
|
||||
print(f" ↓ {fname}")
|
||||
_stream_download(url, dest)
|
||||
print(f" {dest.stat().st_size/1e6:.2f} MB")
|
||||
last_err = None
|
||||
break
|
||||
except (urllib.error.HTTPError, urllib.error.URLError) as e:
|
||||
last_err = e
|
||||
if last_err is not None:
|
||||
print(f" failed: {last_err}")
|
||||
failed.append(fname)
|
||||
|
||||
print(f"\n[cBioPortal] {len(files) - len(failed)}/{len(files)} files retrieved.")
|
||||
if failed:
|
||||
print(f"[cBioPortal] failed files: {failed}")
|
||||
|
||||
print(f"\nFiles under {study_dir}:")
|
||||
for f in sorted(study_dir.iterdir()):
|
||||
if f.is_file():
|
||||
size_mb = f.stat().st_size / 1e6
|
||||
print(f" {f.name:60s} {size_mb:>8.2f} MB")
|
||||
return study_dir
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2: GDC pathology images
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Image-type aliases for convenience. "Diagnostic Slide" is the larger SVS;
|
||||
# "Tissue Slide" is similar. Diagnostic image thumbnails are not always
|
||||
# listed as a separate type — they're embedded inside the slide files.
|
||||
_IMAGE_TYPE_FILTERS = {
|
||||
"diagnostic": "Diagnostic Slide",
|
||||
"tissue": "Tissue Slide",
|
||||
}
|
||||
|
||||
|
||||
def build_image_manifest(image_type: str = "diagnostic",
|
||||
limit: int | None = None,
|
||||
max_size_mb: float | None = None,
|
||||
out_path: Path | None = None) -> list[dict]:
|
||||
"""Query GDC API for TCGA-BRCA pathology images, return file metadata.
|
||||
|
||||
Returns a list of dicts: file_id, file_name, file_size, patient_id, sample_id.
|
||||
"""
|
||||
filt_type = _IMAGE_TYPE_FILTERS.get(image_type, image_type)
|
||||
filters = {
|
||||
"op": "and",
|
||||
"content": [
|
||||
{"op": "in", "content": {"field": "cases.project.project_id",
|
||||
"value": ["TCGA-BRCA"]}},
|
||||
{"op": "in", "content": {"field": "data_format", "value": ["SVS"]}},
|
||||
{"op": "in", "content": {"field": "experimental_strategy",
|
||||
"value": [filt_type]}},
|
||||
{"op": "in", "content": {"field": "access", "value": ["open"]}},
|
||||
],
|
||||
}
|
||||
# Request more than `limit` so we can filter by size client-side first.
|
||||
page_size = max(limit or 1000, 1000)
|
||||
params = {
|
||||
"filters": json.dumps(filters),
|
||||
"fields": ("file_id,file_name,file_size,experimental_strategy,"
|
||||
"cases.submitter_id,cases.samples.submitter_id"),
|
||||
"format": "JSON",
|
||||
"size": str(page_size),
|
||||
}
|
||||
url = f"{GDC_API_FILES}?{urllib.parse.urlencode(params)}"
|
||||
print(f"[GDC] querying for image manifest "
|
||||
f"(type={image_type}, max_size={max_size_mb}MB, limit={limit})...")
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
data = json.loads(resp.read())
|
||||
|
||||
raw_hits = data.get("data", {}).get("hits", [])
|
||||
total = data.get("data", {}).get("pagination", {}).get("total", len(raw_hits))
|
||||
print(f"[GDC] GDC reports {total} total matching files; fetched {len(raw_hits)}")
|
||||
|
||||
# Flatten + filter
|
||||
hits = []
|
||||
for h in raw_hits:
|
||||
case = (h.get("cases") or [{}])[0]
|
||||
sample = ((case.get("samples") or [{}])[0])
|
||||
size_mb = h.get("file_size", 0) / 1e6
|
||||
if max_size_mb is not None and size_mb > max_size_mb:
|
||||
continue
|
||||
hits.append({
|
||||
"file_id": h["file_id"],
|
||||
"file_name": h["file_name"],
|
||||
"file_size": h.get("file_size", 0),
|
||||
"experimental_strategy": h.get("experimental_strategy"),
|
||||
"patient_id": case.get("submitter_id"),
|
||||
"sample_id": sample.get("submitter_id"),
|
||||
})
|
||||
if limit is not None:
|
||||
hits = hits[:limit]
|
||||
|
||||
print(f"[GDC] {len(hits)} files in manifest after filter "
|
||||
f"({sum(h['file_size'] for h in hits)/1e9:.2f} GB total)")
|
||||
|
||||
if out_path is not None:
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(hits, indent=2))
|
||||
print(f"[GDC] manifest saved → {out_path}")
|
||||
|
||||
return hits
|
||||
|
||||
|
||||
def download_images(manifest: list[dict], out_dir: Path) -> None:
|
||||
"""Download images from a GDC manifest. Files are SVS (gigapixel)."""
|
||||
import time
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
n = len(manifest)
|
||||
if n == 0:
|
||||
return
|
||||
total_bytes = sum(item.get("file_size", 0) for item in manifest)
|
||||
print(f"[GDC] downloading {n} files ({total_bytes/1e9:.2f} GB total) → {out_dir}")
|
||||
done_bytes = 0
|
||||
t0 = time.time()
|
||||
for i, item in enumerate(manifest, 1):
|
||||
fid = item["file_id"]
|
||||
name = item["file_name"]
|
||||
sz = item.get("file_size", 0)
|
||||
dest = out_dir / name
|
||||
if dest.exists() and dest.stat().st_size == sz:
|
||||
print(f" [{i:>3d}/{n}] {name} (already complete, skip)")
|
||||
done_bytes += sz
|
||||
continue
|
||||
elif dest.exists():
|
||||
dest.unlink() # partial / wrong size, redo
|
||||
url = f"{GDC_API_DATA}/{fid}"
|
||||
print(f" [{i:>3d}/{n}] {name} ({sz/1e6:.1f} MB) "
|
||||
f"[total so far {done_bytes/1e9:.2f}/{total_bytes/1e9:.2f} GB, "
|
||||
f"elapsed {(time.time()-t0)/60:.1f} min]")
|
||||
try:
|
||||
_stream_download(url, dest)
|
||||
done_bytes += sz
|
||||
except (urllib.error.URLError, urllib.error.HTTPError) as e:
|
||||
print(f" failed: {e}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _stream_download(url: str, dest: Path, chunk_size: int = 1 << 16) -> None:
|
||||
"""Stream-download a URL to a destination path, with a progress indicator."""
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = dest.with_suffix(dest.suffix + ".part")
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
total = int(resp.headers.get("Content-Length", 0))
|
||||
got = 0
|
||||
last_pct = -1
|
||||
with open(tmp, "wb") as f:
|
||||
while True:
|
||||
chunk = resp.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
got += len(chunk)
|
||||
if total > 0:
|
||||
pct = int(got * 100 / total)
|
||||
if pct >= last_pct + 5:
|
||||
print(f" ... {pct}% ({got/1e6:.1f}/{total/1e6:.1f} MB)", flush=True)
|
||||
last_pct = pct
|
||||
tmp.rename(dest)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--out", type=Path, default=Path("data/tcga_brca"),
|
||||
help="Output directory (default: data/tcga_brca)")
|
||||
ap.add_argument("--skip-tabular", action="store_true",
|
||||
help="Skip the cBioPortal tabular file downloads")
|
||||
ap.add_argument("--include-optional", action="store_true",
|
||||
help="Also fetch optional larger files (mutations, CNA, RNA-seq)")
|
||||
ap.add_argument("--skip-images", action="store_true",
|
||||
help="Skip the GDC image download (manifest only is still built)")
|
||||
ap.add_argument("--images", choices=["diagnostic", "tissue"], default="diagnostic",
|
||||
help="Image type to fetch — diagnostic (H&E, ~1.5 GB each) or "
|
||||
"tissue (~200 MB each). Default: diagnostic")
|
||||
ap.add_argument("--limit", type=int, default=None,
|
||||
help="Cap number of images downloaded (after size filter)")
|
||||
ap.add_argument("--max-size-mb", type=float, default=None,
|
||||
help="Skip files larger than this many MB (useful for sampling smaller slides)")
|
||||
ap.add_argument("--manifest-only", action="store_true",
|
||||
help="Build the GDC image manifest JSON but don't download images")
|
||||
args = ap.parse_args()
|
||||
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not args.skip_tabular:
|
||||
fetch_cbioportal(args.out, include_optional=args.include_optional)
|
||||
|
||||
if args.skip_images:
|
||||
return
|
||||
|
||||
manifest = build_image_manifest(
|
||||
image_type=args.images,
|
||||
limit=args.limit,
|
||||
max_size_mb=args.max_size_mb,
|
||||
out_path=args.out / "images" / args.images / f"manifest.json",
|
||||
)
|
||||
|
||||
if args.manifest_only:
|
||||
print("[GDC] manifest-only mode, skipping downloads.")
|
||||
return
|
||||
|
||||
download_images(manifest, args.out / "images" / args.images)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,65 +0,0 @@
|
||||
"""Compare fold assignments between v3 PatientFirstSplitManager and v4 SplitManager."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from v3.classes.split_manager import PatientFirstSplitManager
|
||||
from v4.classes.split_manager import SplitManager
|
||||
from v4.classes.profiles.v4papila import build_data
|
||||
|
||||
args = {
|
||||
"image_dir": "Papila/FundusImages",
|
||||
"clinical_dir": "Papila/ClinicalData",
|
||||
"label_col": "Diagnosis",
|
||||
"iop_corr_method": "ratio",
|
||||
"iop_drop_raw": True,
|
||||
"exclude_cols": ["Axial_Length"],
|
||||
}
|
||||
# Resolve relative paths
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
args["image_dir"] = str(root / args["image_dir"])
|
||||
args["clinical_dir"] = str(root / args["clinical_dir"])
|
||||
|
||||
data = build_data(args)
|
||||
|
||||
label_col = data.label_col
|
||||
patient_col = data.patient_col
|
||||
df_mode = data.df[data.df[label_col].isin([0, 1])].reset_index(drop=True)
|
||||
|
||||
# ── v3 splits ────────────────────────────────────────────────────────────────
|
||||
split_mgr_v3 = PatientFirstSplitManager(patient_col=patient_col, label_col=label_col)
|
||||
split_args_v3 = SimpleNamespace(eval_mode="binary", n_splits=5, fold_seed=100)
|
||||
clinical_ns = SimpleNamespace(df=df_mode, label_col=label_col)
|
||||
splits_v3 = split_mgr_v3.build_plans(clinical=clinical_ns, args=split_args_v3, profile=None)
|
||||
|
||||
# ── v4 splits ────────────────────────────────────────────────────────────────
|
||||
splits_v4 = SplitManager(group_col=patient_col).build_plans(
|
||||
df_mode, label_col=label_col, n_splits=5, seed=100,
|
||||
)
|
||||
|
||||
# ── Compare ──────────────────────────────────────────────────────────────────
|
||||
print(f"{'Fold':<6} {'Set':<6} {'v3 patients':<8} {'v4 patients':<8} {'Match'}")
|
||||
print("-" * 50)
|
||||
|
||||
all_match = True
|
||||
for fold in range(5):
|
||||
s3, s4 = splits_v3[fold], splits_v4[fold]
|
||||
for label, df3, df4 in [
|
||||
("train", s3.train, s4.train),
|
||||
("val", s3.val, s4.val),
|
||||
("test", s3.test, s4.test),
|
||||
]:
|
||||
ids3 = set(df3[patient_col].unique()) if df3 is not None else set()
|
||||
ids4 = set(df4[patient_col].unique()) if df4 is not None else set()
|
||||
match = ids3 == ids4
|
||||
if not match:
|
||||
all_match = False
|
||||
print(f"{fold+1:<6} {label:<6} {len(ids3):<8} {len(ids4):<8} {'✓' if match else '✗ DIFF'}")
|
||||
if not match:
|
||||
print(f" only in v3: {sorted(ids3 - ids4)[:10]}")
|
||||
print(f" only in v4: {sorted(ids4 - ids3)[:10]}")
|
||||
|
||||
print()
|
||||
print("All folds match!" if all_match else "SPLITS DIFFER — fold assignments changed.")
|
||||
@@ -0,0 +1,230 @@
|
||||
[
|
||||
{
|
||||
"_note": "Bridge attention sweep — gated × mobilenet_v2 (ImageNet, weakest off-the-shelf). Per-sample sigmoid gates expose how much each tower contributes. Tests the hypothesis 'as image tower strengthens, bridge downweights clinical'.",
|
||||
"run_name": "experiments/bridge_attention/gated_mobilenet_v2",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "mobilenet_v2", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — gated × resnet50 (ImageNet weights, NOT refuge-pretrained).",
|
||||
"run_name": "experiments/bridge_attention/gated_resnet50",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "resnet50", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — gated × efficientnet_b0 (ImageNet).",
|
||||
"run_name": "experiments/bridge_attention/gated_efficientnet_b0",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "efficientnet_b0", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — gated × efficientnet_v2_m (ImageNet, strongest off-the-shelf).",
|
||||
"run_name": "experiments/bridge_attention/gated_efficientnet_v2_m",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — gated × refugelike (resnet50 + REFUGE pretrain). Pairs with refugelike fundus-domain prior; tests whether REFUGE-pretrained image tower shifts the bridge's attention compared to its ImageNet-only counterpart.",
|
||||
"run_name": "experiments/bridge_attention/gated_refugelike",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refugelike", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — gated × refuge_efficientnet_v2_m (V2-M + REFUGE pretrain, headline production backbone).",
|
||||
"run_name": "experiments/bridge_attention/gated_refuge_efficientnet_v2_m",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — ortho_w0.1 × mobilenet_v2 (ImageNet). Orthogonality penalty pushes streams to encode different info; per-stream variance-explained is the attention readout.",
|
||||
"run_name": "experiments/bridge_attention/ortho_mobilenet_v2",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "mobilenet_v2", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — ortho_w0.1 × resnet50 (ImageNet).",
|
||||
"run_name": "experiments/bridge_attention/ortho_resnet50",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "resnet50", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — ortho_w0.1 × efficientnet_b0 (ImageNet).",
|
||||
"run_name": "experiments/bridge_attention/ortho_efficientnet_b0",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "efficientnet_b0", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — ortho_w0.1 × efficientnet_v2_m (ImageNet).",
|
||||
"run_name": "experiments/bridge_attention/ortho_efficientnet_v2_m",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — ortho_w0.1 × refugelike (resnet50 + REFUGE pretrain).",
|
||||
"run_name": "experiments/bridge_attention/ortho_refugelike",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refugelike", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_note": "Bridge attention sweep — ortho_w0.1 × refuge_efficientnet_v2_m (V2-M + REFUGE pretrain, headline production backbone).",
|
||||
"run_name": "experiments/bridge_attention/ortho_refuge_efficientnet_v2_m",
|
||||
"reps": 10,
|
||||
"overrides": { "save_checkpoints": true, "save_predictions": true },
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"_note": "ConvNeXt-V2-Tiny vs the resnet50/refugelike img backbone. Otherwise identical to experiments/tri_v1/baseline_ensemble — same ensemble_fused base, binary classification, hb eval.",
|
||||
"run_name": "experiments/convnext/baseline_convnextv2_tiny",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "convnextv2_tiny",
|
||||
"freeze_ratio": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"_note": "Hypothesis 1 — ImageNet pretraining is OOD for fundus; freezing low-level filters helps. Keep tiny but freeze 60% of stages (stem + first 2 of 4 stages).",
|
||||
"run_name": "experiments/convnext/baseline_convnextv2_tiny_freeze60",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "convnextv2_tiny",
|
||||
"freeze_ratio": 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Hypothesis 2 — model too big for 330 patients; try the smallest variant.",
|
||||
"run_name": "experiments/convnext/baseline_convnextv2_atto",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "convnextv2_atto",
|
||||
"freeze_ratio": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Hypothesis 3 — LR too high for the bigger model from ImageNet init; halve LR with the tiny backbone unfrozen.",
|
||||
"run_name": "experiments/convnext/baseline_convnextv2_tiny_lr5e5",
|
||||
"reps": 3,
|
||||
"overrides": {
|
||||
"training": { "lr": 5e-5 }
|
||||
},
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "convnextv2_tiny",
|
||||
"freeze_ratio": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
[
|
||||
{
|
||||
"_note": "EfficientNet-B7 (ImageNet-pretrained, torchvision) vs the refugelike resnet50 img backbone. Otherwise identical to experiments/tri_v1/baseline_ensemble.",
|
||||
"run_name": "experiments/efficientnet/baseline_efficientnet_b7",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "efficientnet_b7",
|
||||
"freeze_ratio": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Same architecture, freeze the first 40% of B7 blocks (keep low-level ImageNet filters fixed since fundus is out-of-distribution).",
|
||||
"run_name": "experiments/efficientnet/baseline_efficientnet_b7_freeze40",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "efficientnet_b7",
|
||||
"freeze_ratio": 0.4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
[
|
||||
{
|
||||
"_note": "EfficientNetV2-S (ImageNet, torchvision). 20M params, 1280-dim output. Direct comparison to B7 unfrozen (0.9015 test AUC).",
|
||||
"run_name": "experiments/efficientnet/baseline_efficientnetv2_s",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "efficientnet_v2_s",
|
||||
"freeze_ratio": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "EfficientNetV2-S + freeze 0.4. Applies the lesson from refugelike_freeze_sweep — anchoring low-level filters from ImageNet pretraining.",
|
||||
"run_name": "experiments/efficientnet/baseline_efficientnetv2_s_freeze40",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "efficientnet_v2_s",
|
||||
"freeze_ratio": 0.4
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "EfficientNetV2-M (53M params). Mid-size variant; tests whether extra capacity helps or overfits on PAPILA.",
|
||||
"run_name": "experiments/efficientnet/baseline_efficientnetv2_m",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "efficientnet_v2_m",
|
||||
"freeze_ratio": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"_note": "REFUGE-pretrained EfficientNetV2-M (whole-image, no UNet/disc crop) vs ImageNet-pretrained V2-M (0.9029) and refugelike resnet50 (0.8958). Tests whether fundus-domain pretraining beats ImageNet for V2-M.",
|
||||
"run_name": "experiments/efficientnet/refuge_efficientnetv2_m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": {
|
||||
"args": {
|
||||
"backbone": "refuge_efficientnet_v2_m",
|
||||
"freeze_ratio": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"_note": "10-rep checkpointed run of the production bilateral img+cd ensemble at refuge_efficientnet_v2_m. Per-fold tower and stage_models state_dicts saved under each rep's checkpoints/foldN/ directory. Used for F8 explainability — reconstructing per-tower predictions at the nt (eye) and hb (patient) levels so we can compare img-tower-only, cd-tower-only, eye-fusion, and patient-fusion outputs.",
|
||||
"run_name": "experiments/explainability/ensemble_v2m_ckpt",
|
||||
"reps": 10
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"_note": "refugelike (resnet50 + REFUGE fundus pretraining), freeze stem only (1/5 blocks). Tests whether even minimal anchoring helps stability.",
|
||||
"run_name": "experiments/freeze_sweep/refugelike_freeze20",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refugelike", "freeze_ratio": 0.2 } }
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Freeze stem + layer1 (2/5 blocks). Keeps low-level conv filters fixed, lets layers 2-4 + fc adapt. Bumped to 10 reps to confirm the 0.9105 result vs the 10-rep baseline_ensemble at 0.8958.",
|
||||
"run_name": "experiments/freeze_sweep/refugelike_freeze40",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refugelike", "freeze_ratio": 0.4 } }
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Freeze stem + layer1 + layer2 (3/5 blocks). Only the deep semantic layers adapt — most aggressive practical setting before model loses capacity.",
|
||||
"run_name": "experiments/freeze_sweep/refugelike_freeze60",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refugelike", "freeze_ratio": 0.6 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"_note": "Single-eye cd-only at refugelike (no img tower). 10 reps. For Figure 2 cd column. Pairs with img_solo_single_refugelike and ensemble_single_refugelike to complete the single-mode tower-ablation cascade.",
|
||||
"run_name": "experiments/phase2_v4/cd_solo_single",
|
||||
"reps": 10
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"_note": "PAPILA paper backbone replication — VGG16, single-eye img-only, ImageNet-pretrained (no fundus pretraining). For Figure 2 anchor row.",
|
||||
"run_name": "experiments/phase2_v4/papila_backbones/vgg16",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "vgg16", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "DenseNet121 ImageNet-pretrained, no fundus.",
|
||||
"run_name": "experiments/phase2_v4/papila_backbones/densenet121",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "densenet121", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "MobileNetV2 ImageNet-pretrained, no fundus.",
|
||||
"run_name": "experiments/phase2_v4/papila_backbones/mobilenet_v2",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "mobilenet_v2", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "InceptionV3 ImageNet-pretrained, no fundus. (Note: 299x299 native; pipeline uses 224 — note in figure caption.)",
|
||||
"run_name": "experiments/phase2_v4/papila_backbones/inception_v3",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "inception_v3", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"_note": "Single-eye img+cd ensemble at refugelike with PairwiseAdditiveBridge at nt (eye-level fusion). For F3 confidence-strip panel expansion.",
|
||||
"run_name": "experiments/phase3_v4/single_bcd_pairwise",
|
||||
"reps": 10,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.pairwise_bridge",
|
||||
"class": "PairwiseAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Single-eye img+cd ensemble at refugelike with GatedAdditiveBridge at nt.",
|
||||
"run_name": "experiments/phase3_v4/single_bcd_gated",
|
||||
"reps": 10,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
[
|
||||
{
|
||||
"_note": "Single-eye + all_losses + Hadamard FusionBridge. Pairs with the in-flight ensemble_single_refugelike (single+BCD+Hadamard) to isolate the BCD-vs-all-losses effect. v3 phase 3 originally showed BCD generalizes better; this re-establishes it in v4.",
|
||||
"run_name": "experiments/phase3_v4/single_all_losses_hadamard",
|
||||
"reps": 10,
|
||||
"overrides": {
|
||||
"training": { "tower_loss_mode": "all_losses" }
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Single-eye + BCD + ConcatBridge. Pairs with in-flight baseline (single+BCD+Hadamard) to isolate the bridge effect. Justifies why we use FusionBridge (Hadamard) as default.",
|
||||
"run_name": "experiments/phase3_v4/single_bcd_concat",
|
||||
"reps": 10,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.concat_bridge",
|
||||
"class": "ConcatBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Single-eye + all_losses + ConcatBridge. Fourth cell of the BCD-vs-all-losses × Hadamard-vs-Concat 2x2.",
|
||||
"run_name": "experiments/phase3_v4/single_all_losses_concat",
|
||||
"reps": 10,
|
||||
"overrides": {
|
||||
"training": { "tower_loss_mode": "all_losses" }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.concat_bridge",
|
||||
"class": "ConcatBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"_note": "Bilateral cd-only at refugelike. 10 reps. For Figure 4 bilateral cd column. Pairs with phase2_v4/cd_solo_single (single-eye) to show whether the bilateral hb fusion improves a pure-clinical model.",
|
||||
"run_name": "experiments/phase4_v4/cd_solo_bilateral",
|
||||
"reps": 10
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"_note": "Bilateral img+cd ensemble at refugelike with HyperBridge mode = classic_bridge (Hadamard per-side projection + product) instead of the default embedding_mlp (concat + linear). For Figure 4 inset showing the bilateral-fusion bridge choice doesn't materially change the patient-level result.",
|
||||
"run_name": "experiments/phase4_v4/ensemble_hb_classic_bridge",
|
||||
"reps": 10,
|
||||
"stage_overrides": {
|
||||
"hb": {
|
||||
"args": { "hidden_dim": 256, "mode": "classic_bridge" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"_note": "Tritower (img+cd+geom) with the geom tower fed GT contour-rasterized masks instead of UNet predictions. Completes the geometry panel by disentangling 'GT signal is what matters' from 'vector form is what matters'. 10 reps at refugelike.",
|
||||
"run_name": "experiments/phase6_v4/tritower_geom_gt",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"geom": { "args": { "seg_source": "gt" } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"_note": "Geometry-vector injection from UNet-derived segmentation, bumped to 10 reps to round out the refugelike geometry panel alongside ensemble_fused/no_geom (0.8979, n=10), ensemble_fused/geom_gt (0.9121, n=10), tri_v1/baseline_tri (0.8932, n=10), and tri_v1/baseline_solo (0.6833, n=10). Use the existing ensemble_fused_geom_unet.json base config which already wires up the EPC geometry_vectors channel.",
|
||||
"run_name": "experiments/phase6_v4/ensemble_geom_vec_unet",
|
||||
"reps": 10
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"_note": "Clinical-only floor reference under the new baseline framework. No img backbone change since there is no img tower.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/cd_solo",
|
||||
"reps": 3
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,149 @@
|
||||
[
|
||||
{
|
||||
"_note": "Ortho-wrapped Hadamard at w=0.1 — best ortho weight from prior sweep. Bumped to 10 reps after 3-rep showed +0.009 vs anchor.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_ortho_w0.1",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "ConcatBridge alternative to Hadamard.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_concat",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.concat_bridge",
|
||||
"class": "ConcatBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "PairwiseAdditiveBridge — for N=2 streams this reduces to ≈ FusionBridge additive=True. Bumped to 10 reps after 3-rep showed +0.017 vs anchor (but with a suspicious val/test gap).",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_pairwise",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.pairwise_bridge",
|
||||
"class": "PairwiseAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "GatedAdditiveBridge — per-sample sigmoid gates over each stream.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_gated",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Ortho w=0.1 wrapping ConcatBridge inner.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_ortho_concat_w0.1",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.concat_bridge",
|
||||
"inner_class": "ConcatBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Ortho w=0.1 wrapping PairwiseAdditiveBridge inner.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_ortho_pairwise_w0.1",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.pairwise_bridge",
|
||||
"inner_class": "PairwiseAdditiveBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Ortho w=0.1 wrapping GatedAdditiveBridge inner.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_ortho_gated_w0.1",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.gated_bridge",
|
||||
"inner_class": "GatedAdditiveBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Bottleneck (fusion_dim=8) — tests whether the new backbone still survives aggressive compression.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_bottleneck",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": { "args": { "fusion_dim": 8 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"_note": "Geometry-vector injection (GT contours, EPC) added to img+cd ensemble at refuge V2-M backbone. Replaces refugelike's ensemble_fused/geom_gt (0.9121) at the new backbone. For the geometry panel.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_geom_vec_gt",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Geometry-vector injection via UNet-derived geometry (not GT). Same architecture; uses the per-fold-finetuned UNet segmenter EPC channel. Tests whether GT vs predicted segmentation matters under the new backbone.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_geom_vec_unet",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"_note": "Image-only (no cd) at the new refuge_efficientnet_v2_m baseline. 10 reps — definite test, establishes how much cd contributes to the ensemble.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/img_solo",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"_note": "Bilateral img-only with refugelike — missing corner of the img_solo single-vs-bilateral × refugelike-vs-V2M grid. (Bilateral V2-M already exists at 0.8923.)",
|
||||
"run_name": "experiments/refuge_v2m_baseline/img_solo_bilateral_refugelike",
|
||||
"reps": 10
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"_note": "Single-eye ensemble (img+cd, eval at nt) under the original refugelike backbone. Pairs with tri_v1/baseline_ensemble (bilateral, 0.8958 ± 0.017 at 10 reps) to standardize the v3 single-vs-bilateral comparison.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_single_refugelike",
|
||||
"reps": 10
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Single-eye ensemble under the new refuge V2-M backbone. Pairs with efficientnet/refuge_efficientnetv2_m (bilateral, 0.9132 ± 0.019 at 10 reps).",
|
||||
"run_name": "experiments/refuge_v2m_baseline/ensemble_single_refuge_v2m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"_note": "Single-eye img-only (eval at img_fuse) under the original refugelike backbone. Goes with img_solo_bilateral_refugelike below to fill the 2x2 grid.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/img_solo_single_refugelike",
|
||||
"reps": 10
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Single-eye img-only under the new refuge V2-M backbone. Pairs with refuge_v2m_baseline/img_solo (bilateral, 0.8923 ± 0.018 at 10 reps).",
|
||||
"run_name": "experiments/refuge_v2m_baseline/img_solo_single_refuge_v2m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"_note": "Tritower (img + cd + geom) at the new img backbone. 10 reps — definite test, establishes whether the geom tower adds value over img+cd.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/tritower",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Tritower + OrthoBridge w=0.1 (best ortho weight from earlier sweep) wrapping Hadamard inner. Bumped to 10 reps after 3-rep showed 0.9059 with tight std — wanted to confirm against plain tritower's 0.9040.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/tritower_ortho_w0.1",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "Tritower + bottleneck (fusion_dim=8) — tests whether the strong img backbone can survive aggressive bottlenecking.",
|
||||
"run_name": "experiments/refuge_v2m_baseline/tritower_bottleneck",
|
||||
"reps": 3,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": { "args": { "fusion_dim": 8 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"_note": "CD tower only, regression. Ablation vs baseline_reg_nt50 to isolate the contribution of clinical data.",
|
||||
"run_name": "experiments/reg_head/cd_solo_reg",
|
||||
"reps": 3
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"_note": "Image tower only, regression. Ablation vs baseline_reg_nt50 to isolate the contribution of fundus images.",
|
||||
"run_name": "experiments/reg_head/img_solo_reg",
|
||||
"reps": 3
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,97 @@
|
||||
[
|
||||
{
|
||||
"_note": "baseline_reg at nt=50 (vs default 36). All heads regression on vf_md, label_filter expanded to [0,1,2]. Bumped to 10 reps for final regression-head reporting + 3-bin severity confusion matrix.",
|
||||
"run_name": "experiments/reg_head/baseline_reg_nt50",
|
||||
"reps": 10,
|
||||
"overrides": {
|
||||
"label_filter": [0, 1, 2]
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": { "epochs": 50 },
|
||||
"img_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"cd_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"nt_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"hb_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "baseline_reg at nt=75",
|
||||
"run_name": "experiments/reg_head/baseline_reg_nt75",
|
||||
"reps": 3,
|
||||
"overrides": {
|
||||
"label_filter": [0, 1, 2]
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": { "epochs": 75 },
|
||||
"img_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"cd_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"nt_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"hb_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "baseline_reg at nt=100",
|
||||
"run_name": "experiments/reg_head/baseline_reg_nt100",
|
||||
"reps": 3,
|
||||
"overrides": {
|
||||
"label_filter": [0, 1, 2]
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": { "epochs": 100 },
|
||||
"img_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"cd_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"nt_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"hb_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,116 @@
|
||||
[
|
||||
{
|
||||
"_note": "1) Baseline classification — sanity check that the runner refactor didn't break anything. Should match baseline_ensemble at 0.896.",
|
||||
"run_name": "experiments/reg_head/baseline_class",
|
||||
"reps": 3
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "2) Same architecture as baseline, but all heads (img_aux, cd_aux, nt_head, hb_head) are regression heads targeting vf_md. label_filter expanded to include suspect patients (label=2) since regression handles continuous targets naturally.",
|
||||
"run_name": "experiments/reg_head/baseline_reg",
|
||||
"reps": 3,
|
||||
"overrides": {
|
||||
"label_filter": [0, 1, 2]
|
||||
},
|
||||
"stage_overrides": {
|
||||
"img_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"cd_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"nt_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"hb_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "3) OrthoBridge (w=0.1) wrapping Hadamard inner + regression heads",
|
||||
"run_name": "experiments/reg_head/ortho_reg",
|
||||
"reps": 3,
|
||||
"overrides": {
|
||||
"label_filter": [0, 1, 2]
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
},
|
||||
"img_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"cd_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"nt_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"hb_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "4) PairwiseAdditiveBridge + regression heads",
|
||||
"run_name": "experiments/reg_head/pairwise_reg",
|
||||
"reps": 3,
|
||||
"overrides": {
|
||||
"label_filter": [0, 1, 2]
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.pairwise_bridge",
|
||||
"class": "PairwiseAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
},
|
||||
"img_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"cd_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"nt_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"hb_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"_note": "Single-eye regression at nt=50, refugelike backbone. Eye-level fusion (no hb), regression heads predicting per-eye vf_md. Pairs with the bilateral baseline_reg_nt50 (mean-of-eyes target, hb fusion) for the worst-eye aggregation analysis: predict per-eye MD at nt, then aggregate to patient-level via min(OD_pred, OS_pred).",
|
||||
"run_name": "experiments/reg_head/single_eye_reg_nt50",
|
||||
"reps": 10,
|
||||
"overrides": {
|
||||
"label_filter": [0, 1, 2]
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": { "epochs": 50 },
|
||||
"img_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"cd_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"nt_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
[
|
||||
{ "_note": "OrthoBridge (w=0.1) wrapping ConcatBridge — img+cd ensemble. Generality test for ortho across alternative inner bridges. 3 reps each.",
|
||||
"run_name": "experiments/tri_v1/ortho_alts/ensemble_concat_w0.1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.concat_bridge",
|
||||
"inner_class": "ConcatBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho_alts/ensemble_pairwise_w0.1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.pairwise_bridge",
|
||||
"inner_class": "PairwiseAdditiveBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho_alts/ensemble_gated_w0.1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.gated_bridge",
|
||||
"inner_class": "GatedAdditiveBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
[
|
||||
{ "_note": "OrthoBridge (w=0.1) wrapping ConcatBridge — img+cd+geom tritower. Generality test for ortho across alternative inner bridges. 3 reps each.",
|
||||
"run_name": "experiments/tri_v1/ortho_alts/tritower_concat_w0.1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.concat_bridge",
|
||||
"inner_class": "ConcatBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho_alts/tritower_pairwise_w0.1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.pairwise_bridge",
|
||||
"inner_class": "PairwiseAdditiveBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho_alts/tritower_gated_w0.1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.gated_bridge",
|
||||
"inner_class": "GatedAdditiveBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
[
|
||||
{ "_note": "OrthoBridge wrapping Hadamard FusionBridge, img+cd ensemble. Sweep ortho_weight ∈ {0.01, 0.1, 1.0, 10.0}. 3 reps each.",
|
||||
|
||||
"run_name": "experiments/tri_v1/ortho/ensemble_w0.01", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.01,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho/ensemble_w0.1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho/ensemble_w1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 1.0,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho/ensemble_w10", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 10.0,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
[
|
||||
{ "_note": "OrthoBridge wrapping Hadamard FusionBridge, img+cd+geom tritower. Sweep ortho_weight ∈ {0.01, 0.1, 1.0, 10.0}. 3 reps each.",
|
||||
|
||||
"run_name": "experiments/tri_v1/ortho/tritower_w0.01", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.01,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho/tritower_w0.1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho/tritower_w1", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 1.0,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "run_name": "experiments/tri_v1/ortho/tritower_w10", "reps": 3,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 10.0,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"_note": "Promote ortho_w0.1 ensemble to 10 reps. First 3 will be skipped (results exist).",
|
||||
"run_name": "experiments/tri_v1/ortho/ensemble_w0.1",
|
||||
"reps": 10,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"_note": "Promote ortho_w0.1 tritower to 10 reps. First 3 will be skipped (results exist).",
|
||||
"run_name": "experiments/tri_v1/ortho/tritower_w0.1",
|
||||
"reps": 10,
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.ortho_bridge",
|
||||
"class": "OrthoBridge",
|
||||
"args": {
|
||||
"fusion_dim": 256,
|
||||
"ortho_weight": 0.1,
|
||||
"inner_module": "v4.classes.bridges.fusion_bridge",
|
||||
"inner_class": "FusionBridge",
|
||||
"inner_args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
[
|
||||
{
|
||||
"_note": "F6 V2-M: regression baseline_reg_nt50 at refuge V2-M. Same architecture as reg_head/baseline_reg_nt50 but with the V2-M backbone in place of refugelike.",
|
||||
"run_name": "experiments/v2m_variants/baseline_reg_nt50_v2m",
|
||||
"reps": 10,
|
||||
"overrides": {
|
||||
"label_filter": [0, 1, 2]
|
||||
},
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": { "epochs": 50 },
|
||||
"img_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"cd_aux": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"nt_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
},
|
||||
"hb_head": {
|
||||
"module": "v4.classes.heads.regression",
|
||||
"class": "RegressionHead",
|
||||
"args": { "dropout": 0.3, "target_key": "vf_md", "loss": "mse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"_note": "S1 V2-M: ensemble + GT geometry vector injection at refuge V2-M. Pairs with ensemble_fused/geom_gt (refugelike) for cross-backbone geometry view.",
|
||||
"run_name": "experiments/v2m_variants/ensemble_geom_vec_gt_v2m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"_note": "S1 V2-M: ensemble + UNet-derived geometry vector injection at refuge V2-M.",
|
||||
"run_name": "experiments/v2m_variants/ensemble_geom_vec_unet_v2m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
[
|
||||
{
|
||||
"_note": "F3 V2-M variant: single-eye img+cd ensemble at refuge V2-M with ConcatBridge at nt. Pairs with phase3_v4/single_bcd_concat (refugelike) for the cross-backbone view.",
|
||||
"run_name": "experiments/v2m_variants/single_bcd_concat_v2m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.concat_bridge",
|
||||
"class": "ConcatBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "F3 V2-M: single-eye PairwiseAdditiveBridge at refuge V2-M.",
|
||||
"run_name": "experiments/v2m_variants/single_bcd_pairwise_v2m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.pairwise_bridge",
|
||||
"class": "PairwiseAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"_note": "F3 V2-M: single-eye GatedAdditiveBridge at refuge V2-M.",
|
||||
"run_name": "experiments/v2m_variants/single_bcd_gated_v2m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } }
|
||||
},
|
||||
"stage_overrides": {
|
||||
"nt": {
|
||||
"module": "v4.classes.bridges.gated_bridge",
|
||||
"class": "GatedAdditiveBridge",
|
||||
"args": { "fusion_dim": 256 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"_note": "S1 V2-M: tritower (img+cd+geom) with the geom tower fed GT contour-rasterized masks, at refuge V2-M. Pairs with phase6_v4/tritower_geom_gt (refugelike) and refuge_v2m_baseline/tritower (V2-M, UNet seg).",
|
||||
"run_name": "experiments/v2m_variants/tritower_geom_gt_v2m",
|
||||
"reps": 10,
|
||||
"tower_overrides": {
|
||||
"img": { "args": { "backbone": "refuge_efficientnet_v2_m", "freeze_ratio": 0.0 } },
|
||||
"geom": { "args": { "seg_source": "gt" } }
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user