update 3-19

This commit is contained in:
rpotter6298
2026-03-19 11:18:58 +01:00
parent 7ea85d5426
commit 786457b30d
35 changed files with 4019 additions and 258 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ from pathlib import Path
import sys
import argparse
REPO_ROOT = Path(__file__).resolve().parents[2]
REPO_ROOT = Path(__file__).resolve().parents[3]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
10× repeated 5-fold CV runner for the best hypertower configuration
(nocrop, ensemble mode, both binary and multiclass).
Each repetition uses a different fold-seed so the 5 folds are split
differently, giving 50 folds per eval-mode total. Holdout composition
is kept identical across repetitions (same --holdout-seed).
Results land under:
{output-root}/rep{N:02d}/{eval_mode}/ensemble/fold{K}/
Usage
-----
python scripts/main/v2/run_10x5cv.py \
--n-reps 10 \
--eval-modes binary multiclass \
--output-root analysis_data/pipeline_10x5 \
--epochs 40 --fused-head \
--backbone refugelike
Any extra flags are forwarded directly to V2HyperTower.
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from classes.v2.v2_hypertower import V2HyperTower
# Base fold seed for rep 0; rep N uses BASE_SEED + N * SEED_STRIDE
_BASE_SEED = 100
_SEED_STRIDE = 100
def _parse_own(argv=None):
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
)
ap.add_argument("--n-reps", type=int, default=10,
help="Number of repetitions (default: 10).")
ap.add_argument("--eval-modes", nargs="+",
choices=["binary", "multiclass"],
default=["binary", "multiclass"])
ap.add_argument("--output-root", default="analysis_data/pipeline_10x5",
help="Parent directory for all rep sub-runs.")
ap.add_argument("-h", "--help", action="store_true")
return ap.parse_known_args(argv)
def main(argv=None):
own, remaining = _parse_own(argv)
if own.help:
print(__doc__)
base_parser = V2HyperTower.build_parser()
base_parser.print_help()
return
base_parser = V2HyperTower.build_parser()
output_root = Path(own.output_root)
first_run = True
for rep in range(own.n_reps):
fold_seed = _BASE_SEED + rep * _SEED_STRIDE
rep_label = f"rep{rep:02d}"
for eval_mode in own.eval_modes:
tower_mode = "ensemble"
# Skip if already fully complete
tm_dir = output_root / rep_label / eval_mode / tower_mode
if (tm_dir / "summary.json").exists():
print(f"[10x5cv] {rep_label} {eval_mode}:{tower_mode} — already done, skipping.")
first_run = False
continue
cli = list(remaining) + [
"--eval-mode", eval_mode,
"--tower-mode", tower_mode,
"--fold-seed", str(fold_seed),
"--run-name", rep_label,
"--output-root", str(output_root),
]
# Reuse crop cache across runs after the first
if not first_run:
cli.append("--persist-img-crop-cache")
print(f"\n[10x5cv] Starting {rep_label} {eval_mode}:{tower_mode} "
f"(fold_seed={fold_seed})")
args = base_parser.parse_args(cli)
V2HyperTower(args).run()
first_run = False
print(f"\n[10x5cv] All done. Results in: {output_root}")
if __name__ == "__main__":
main()
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""
Run single-mode binary + multiclass for each IOP correction method and
collect all results under one output root for easy comparison.
Output layout:
analysis_data/iop_corr_comparison/
ratio/binary/single/ ratio/multiclass/single/
ols/binary/single/ ols/multiclass/single/
lad/binary/single/ lad/multiclass/single/
multi/binary/single/ multi/multiclass/single/
Usage
-----
python scripts/main/v2/run_iop_corr_comparison.py [V2HyperTower args...]
Any extra args (backbone, epochs, img-crop-*, etc.) are forwarded to every run.
"""
from __future__ import annotations
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from classes.v2.v2_hypertower import V2HyperTower
IOP_METHODS = ["ratio", "ols", "lad", "multi"]
EVAL_MODES = ["binary", "multiclass"]
OUTPUT_ROOT = "analysis_data/iop_corr_comparison"
def main() -> None:
base_parser = V2HyperTower.build_parser()
# Consume only the remaining (forwarded) args — iop-corr-method and
# run-name are set by this script; eval-mode and tower-mode likewise.
_, remaining = base_parser.parse_known_args()
first_run = True
for method in IOP_METHODS:
for eval_mode in EVAL_MODES:
run_name = method # one sub-folder per method
tm_dir = (Path(OUTPUT_ROOT) / run_name / eval_mode / "single")
if (tm_dir / "summary.json").exists():
print(f"[iop_corr] {method}/{eval_mode}/single — already done, skipping.")
first_run = False
continue
cli = list(remaining) + [
"--eval-mode", eval_mode,
"--tower-mode", "single",
"--iop-corr-method", method,
"--output-root", OUTPUT_ROOT,
"--run-name", run_name,
]
if not first_run:
cli.append("--persist-img-crop-cache")
print(f"\n[iop_corr] Starting {method}/{eval_mode}/single ...")
args = base_parser.parse_args(cli)
V2HyperTower(args).run()
first_run = False
# ── summary table ──────────────────────────────────────────────────────
import json
print("\n" + "=" * 60)
print("IOP correction method comparison — single mode")
print("=" * 60)
header = f"{'Method':<8} {'Mode':<12} {'Val AUC':>10} {'Hld AUC':>10}"
print(header)
print("-" * len(header))
for method in IOP_METHODS:
for eval_mode in EVAL_MODES:
p = Path(OUTPUT_ROOT) / method / eval_mode / "single" / "summary.json"
if not p.exists():
print(f"{method:<8} {eval_mode:<12} {'missing':>10} {'missing':>10}")
continue
ms = json.loads(p.read_text()).get("mode_summary", {})
val = ms.get("classic_best_val", {})
hld = ms.get("classic_holdout", {})
val_s = f"{val['auc_mean']:.3f}±{val['auc_std']:.3f}" if val.get("auc_mean") else ""
hld_s = f"{hld['auc_mean']:.3f}±{hld['auc_std']:.3f}" if hld.get("auc_mean") else ""
print(f"{method:<8} {eval_mode:<12} {val_s:>10} {hld_s:>10}")
print("=" * 60)
if __name__ == "__main__":
main()
+23
View File
@@ -323,6 +323,7 @@ def main() -> None:
best_epoch = 0
best_phase = ""
best_state = None
epoch_log_rows = []
print(
f"\n[fold {fold_idx+1}/{args.n_splits}] "
@@ -350,6 +351,14 @@ def main() -> None:
aggregate_patient=aggregate_patient,
)
hld_auc_ep = float("nan")
hld_acc_ep = float("nan")
if holdout_loader is not None:
_, _, hld_auc_ep, hld_acc_ep = _evaluate_single(
model, holdout_loader, device, num_classes,
aggregate_patient=aggregate_patient,
)
is_main = phase == "main"
if is_main and (not np.isnan(val_auc)) and val_auc > best_auc:
best_auc = float(val_auc)
@@ -357,6 +366,17 @@ def main() -> None:
best_epoch = ep + 1
best_phase = phase
epoch_log_rows.append({
"epoch": ep + 1,
"phase": phase,
"train_loss": float(tr_loss),
"train_acc": float(tr_acc),
"val_auc": float(val_auc),
"val_acc": float(val_acc),
"hld_auc": float(hld_auc_ep),
"hld_acc": float(hld_acc_ep),
})
if ep == 0 or (ep + 1) % 10 == 0 or (ep + 1) == total_epochs:
print(
f" ep {ep+1:>3}/{total_epochs} [{phase}:{main_ep}/{args.epochs}] "
@@ -366,6 +386,9 @@ def main() -> None:
flush=True,
)
import pandas as _pd
_pd.DataFrame(epoch_log_rows).to_csv(fold_dir / "epoch_log.csv", index=False)
if best_state is not None:
model.load_state_dict(best_state)
+1 -1
View File
@@ -6,7 +6,7 @@ from __future__ import annotations
from pathlib import Path
import sys
REPO_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = Path(__file__).resolve().parents[3]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))