logging rework temp save
This commit is contained in:
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Binary runs v2.2 (6 total):
|
||||
# UNet crop: single | ensemble | fused head
|
||||
# GT crop: single | ensemble | fused head
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
MANIFEST="manifest.csv"
|
||||
UNET_WEIGHTS="models/v2/refuge/segmentation/per_image/best.pt"
|
||||
|
||||
COMMON=(
|
||||
--epochs 40
|
||||
--n-splits 5
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
--eval-mode binary
|
||||
--img-crop-manifest "$MANIFEST"
|
||||
)
|
||||
|
||||
UNET_CROP=(
|
||||
--img-crop-weights "$UNET_WEIGHTS"
|
||||
)
|
||||
|
||||
GT_CROP=(
|
||||
--img-crop-gt
|
||||
)
|
||||
|
||||
# ── UNet crop ────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[1/6] UNet crop — binary, single..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${UNET_CROP[@]}" \
|
||||
--tower-mode single \
|
||||
--run-name v2.2_single_binary_unet_40ep_5fold
|
||||
|
||||
echo "[2/6] UNet crop — binary, ensemble..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${UNET_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--run-name v2.2_ensemble_binary_unet_40ep_5fold
|
||||
|
||||
echo "[3/6] UNet crop — binary, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${UNET_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--fused-head --fusion-epochs 10 \
|
||||
--run-name v2.2_fused_binary_unet_40ep_5fold
|
||||
|
||||
# ── GT crop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[4/6] GT crop — binary, single..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${GT_CROP[@]}" \
|
||||
--tower-mode single \
|
||||
--run-name v2.2_single_binary_gt_40ep_5fold
|
||||
|
||||
echo "[5/6] GT crop — binary, ensemble..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${GT_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--run-name v2.2_ensemble_binary_gt_40ep_5fold
|
||||
|
||||
echo "[6/6] GT crop — binary, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${GT_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--img-crop-gt \
|
||||
--fused-head --fusion-epochs 10 \
|
||||
--run-name v2.2_fused_binary_gt_40ep_5fold
|
||||
|
||||
echo "Binary v2.2 runs complete."
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Fused-head ensemble runs (2 total) — UNet ROI crop:
|
||||
# binary × ensemble + fused head
|
||||
# multiclass × ensemble + fused head
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
CROP_ARGS=(
|
||||
--img-crop-manifest manifest.csv
|
||||
--img-crop-weights models/v2/refuge/segmentation/per_image_refuge_build/best.pt
|
||||
--img-crop-normalize per_image
|
||||
)
|
||||
|
||||
COMMON_ARGS=(
|
||||
--epochs 40
|
||||
--n-splits 5
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
--tower-mode ensemble
|
||||
--fused-head
|
||||
--fusion-epochs 10
|
||||
)
|
||||
|
||||
echo "[1/2] UNet ROI — binary, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-mode binary \
|
||||
--run-name v2_ensemble_fused_binary_unet_40ep_5fold_v1
|
||||
|
||||
echo "[2/2] UNet ROI — multiclass, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-mode multiclass \
|
||||
--run-name v2_ensemble_fused_multiclass_unet_40ep_5fold_v1
|
||||
|
||||
echo "Fused-head runs complete."
|
||||
@@ -0,0 +1,461 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
V2-parity metadata-only runner.
|
||||
|
||||
Goal:
|
||||
- Match V2HyperTower single-model metadata-only behavior as closely as possible.
|
||||
- Avoid image tower/image IO overhead in forward/training.
|
||||
|
||||
How parity is achieved:
|
||||
- Uses PatientFirstSplitManager (same split policy).
|
||||
- Uses PAPILA profile builders + V2 filters:
|
||||
- eye_train = filter_eye_samples(...)
|
||||
- bilat_val/test = filter_bilateral_samples(...)
|
||||
- Uses V2 training/eval helpers directly:
|
||||
- train_single_epoch(...)
|
||||
- collect_probs_single_components(...)
|
||||
- Uses bridge_mode="metadata_only".
|
||||
|
||||
Implementation detail:
|
||||
- Batch dictionaries still include image slots to satisfy shared V2 helpers,
|
||||
but these are tiny dummy tensors and are never consumed in metadata-only mode.
|
||||
|
||||
Outputs:
|
||||
analysis_data/{run_name}/{eval_mode}/{tower_mode}/fold{N}/
|
||||
y_true.npy
|
||||
probs_classic.npy or probs_ensemble.npy
|
||||
y_true_holdout.npy
|
||||
probs_classic_holdout.npy or probs_ensemble_holdout.npy
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
# ensure repo root is on sys.path when run directly
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
|
||||
from classes.v2.bridges import Bridge
|
||||
from classes.v2.loader_factory import (
|
||||
build_balanced_sampler,
|
||||
filter_bilateral_samples,
|
||||
filter_eye_samples,
|
||||
)
|
||||
from classes.v2.metrics import _score_arrays
|
||||
from classes.v2.models import collect_probs_single_components, train_single_epoch
|
||||
from classes.v2.papila_builders import build_papila_data
|
||||
from classes.v2.profiles import build_papila_profile
|
||||
from classes.v2.split_manager import PatientFirstSplitManager
|
||||
from classes.v2.towers import MDTower
|
||||
from classes.v2.utils import choose_device, seed_everything
|
||||
|
||||
|
||||
class MetadataOnlySingleHT(nn.Module):
|
||||
"""SingleEyeHT-compatible shell without real image tower usage."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
clinical_data,
|
||||
num_classes: int,
|
||||
md_hidden_dim: int,
|
||||
fusion_dim: int,
|
||||
dropout: float,
|
||||
use_se: bool,
|
||||
se_reduction: int,
|
||||
se_pre_norm: bool,
|
||||
):
|
||||
super().__init__()
|
||||
# Placeholder module to satisfy phase toggling logic.
|
||||
self.img_tower = nn.Identity()
|
||||
self.md_tower = MDTower(
|
||||
clinical_data=clinical_data,
|
||||
hidden_dim=md_hidden_dim,
|
||||
dropout=dropout,
|
||||
use_se=use_se,
|
||||
se_reduction=se_reduction,
|
||||
se_pre_norm=se_pre_norm,
|
||||
)
|
||||
# img_dim is irrelevant in metadata_only mode, but Bridge defines img head params.
|
||||
self.bridge = Bridge(
|
||||
img_dim=1,
|
||||
meta_dim=self.md_tower.out_dim,
|
||||
num_classes=num_classes,
|
||||
fusion_dim=fusion_dim,
|
||||
mode="metadata_only",
|
||||
use_se=False,
|
||||
se_reduction=16,
|
||||
se_pre_norm=True,
|
||||
)
|
||||
|
||||
|
||||
class EyeMetaDataset(Dataset):
|
||||
"""Eye-level dataset for V2 train_single_epoch input contract."""
|
||||
|
||||
def __init__(self, samples: list[dict]):
|
||||
self.samples = samples
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx: int) -> dict[str, torch.Tensor]:
|
||||
s = self.samples[idx]
|
||||
return {
|
||||
"image_1": torch.zeros(1, dtype=torch.float32),
|
||||
"matrix_1": torch.as_tensor(s["matrix_1"], dtype=torch.float32),
|
||||
"label_1": torch.tensor(int(s["label_1"]), dtype=torch.long),
|
||||
}
|
||||
|
||||
|
||||
class BilatMetaDataset(Dataset):
|
||||
"""Patient-level bilateral dataset for collect_probs_single_components."""
|
||||
|
||||
def __init__(self, samples: list[dict]):
|
||||
self.samples = samples
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx: int) -> dict[str, torch.Tensor]:
|
||||
s = self.samples[idx]
|
||||
return {
|
||||
"image_1": torch.zeros(1, dtype=torch.float32),
|
||||
"image_2": torch.zeros(1, dtype=torch.float32),
|
||||
"matrix_1": torch.as_tensor(s["matrix_1"], dtype=torch.float32),
|
||||
"matrix_2": torch.as_tensor(s["matrix_2"], dtype=torch.float32),
|
||||
"label_1": torch.tensor(int(s["label_1"]), dtype=torch.long),
|
||||
}
|
||||
|
||||
|
||||
def _phase_for_epoch(epoch_idx: int, warm_tower: int, warm_fused: int, main_epochs: int) -> tuple[str, int]:
|
||||
if epoch_idx < warm_tower:
|
||||
return "tower_warmup", 0
|
||||
if epoch_idx < (warm_tower + warm_fused):
|
||||
return "fused_warmup", 0
|
||||
if epoch_idx < (warm_tower + warm_fused + main_epochs):
|
||||
main_ep = epoch_idx - warm_tower - warm_fused + 1
|
||||
return "main", main_ep
|
||||
return "done", main_epochs
|
||||
|
||||
|
||||
def _evaluate_single(
|
||||
model: nn.Module,
|
||||
loader: DataLoader,
|
||||
device: torch.device,
|
||||
num_classes: int,
|
||||
aggregate_patient: bool,
|
||||
) -> tuple[np.ndarray, np.ndarray, float, float]:
|
||||
y, p_fused, _, p_md = collect_probs_single_components(
|
||||
model, loader, device, aggregate_patient=aggregate_patient
|
||||
)
|
||||
# In metadata_only mode p_fused == p_md; keep md explicitly for clarity.
|
||||
probs = p_md if p_md.size else p_fused
|
||||
acc, auc, _ = _score_arrays(y, probs, num_classes)
|
||||
return y, probs, float(auc), float(acc)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="V2-parity metadata-only runner.")
|
||||
ap.add_argument("--eval-mode", required=True, choices=["binary", "multiclass"])
|
||||
ap.add_argument("--tower-mode", default="single", choices=["single", "ensemble"])
|
||||
ap.add_argument("--run-name", required=True)
|
||||
|
||||
ap.add_argument("--epochs", type=int, default=40, help="Main-phase epochs.")
|
||||
ap.add_argument("--warmup-tower-epochs", type=int, default=None)
|
||||
ap.add_argument("--warmup-fused-epochs", type=int, default=None)
|
||||
ap.add_argument("--batch-size", type=int, default=8)
|
||||
ap.add_argument("--lr", type=float, default=1e-4)
|
||||
ap.add_argument("--weight-decay", type=float, default=0.0)
|
||||
ap.add_argument("--bcd-prob", type=float, default=0.5)
|
||||
|
||||
ap.add_argument("--md-hidden-dim", type=int, default=128)
|
||||
ap.add_argument("--fusion-dim", type=int, default=256)
|
||||
ap.add_argument("--dropout", type=float, default=0.1)
|
||||
ap.add_argument("--use-se", action="store_true")
|
||||
ap.add_argument("--se-reduction", type=int, default=16)
|
||||
ap.add_argument("--se-pre-norm", action="store_true")
|
||||
|
||||
ap.add_argument("--n-splits", type=int, default=5)
|
||||
ap.add_argument("--holdout-per-class", type=int, default=5)
|
||||
ap.add_argument("--holdout-seed", type=int, default=123)
|
||||
ap.add_argument("--fold-seed", type=int, default=42)
|
||||
ap.add_argument("--seed", type=int, default=1234)
|
||||
ap.add_argument("--balanced-sampling", action=argparse.BooleanOptionalAction, default=False)
|
||||
|
||||
ap.add_argument("--analysis-dir", default="analysis_data")
|
||||
ap.add_argument("--image-dir", default="Papila/FundusImages")
|
||||
ap.add_argument("--clinical-dir", default="Papila/ClinicalData")
|
||||
ap.add_argument("--label-col", default="Diagnosis")
|
||||
ap.add_argument("--patient-col", default="Patient ID")
|
||||
ap.add_argument("--cat-cols", nargs="*", default=["Gender", "Phakic/Pseudophakic"])
|
||||
|
||||
args = ap.parse_args()
|
||||
|
||||
seed_everything(args.seed)
|
||||
device = choose_device(None)
|
||||
print(f"Device: {device}", flush=True)
|
||||
|
||||
print("Loading PAPILA data...", flush=True)
|
||||
data = build_papila_data(
|
||||
image_dir=args.image_dir,
|
||||
clinical_dir=args.clinical_dir,
|
||||
label_col=args.label_col,
|
||||
cat_cols=list(args.cat_cols),
|
||||
n_splits=args.n_splits,
|
||||
random_seed=args.fold_seed,
|
||||
)
|
||||
print(f"Loaded: {len(data.df)} rows feature_dim={data.feature_dim}", flush=True)
|
||||
|
||||
num_classes = 2 if args.eval_mode == "binary" else 3
|
||||
df_mode = data.df.copy()
|
||||
if args.eval_mode == "binary":
|
||||
df_mode = df_mode[df_mode[args.label_col].isin([0, 1])].reset_index(drop=True)
|
||||
print(f"[{args.eval_mode}] rows={len(df_mode)}", flush=True)
|
||||
|
||||
class _ClinicalShim:
|
||||
label_col = args.label_col
|
||||
|
||||
def __init__(self, df):
|
||||
self.df = df
|
||||
|
||||
split_args = SimpleNamespace(
|
||||
eval_mode=args.eval_mode,
|
||||
holdout_per_class=args.holdout_per_class,
|
||||
holdout_seed=args.holdout_seed,
|
||||
n_splits=args.n_splits,
|
||||
fold_seed=args.fold_seed,
|
||||
)
|
||||
splitter = PatientFirstSplitManager(patient_col=args.patient_col, label_col=args.label_col)
|
||||
plans = splitter.build_plans(clinical=_ClinicalShim(df_mode), args=split_args)
|
||||
|
||||
profile_eye = build_papila_profile(
|
||||
patient_col=args.patient_col,
|
||||
label_col=args.label_col,
|
||||
sample_mode="eye",
|
||||
)
|
||||
profile_patient = build_papila_profile(
|
||||
patient_col=args.patient_col,
|
||||
label_col=args.label_col,
|
||||
sample_mode="patient",
|
||||
)
|
||||
|
||||
warm_tower = int(args.warmup_tower_epochs) if args.warmup_tower_epochs is not None else 2
|
||||
warm_fused = int(args.warmup_fused_epochs) if args.warmup_fused_epochs is not None else 2
|
||||
total_epochs = warm_tower + warm_fused + int(args.epochs)
|
||||
|
||||
out_root = Path(args.analysis_dir) / args.run_name / args.eval_mode / args.tower_mode
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
fold_metrics = []
|
||||
aggregate_patient = args.tower_mode == "ensemble"
|
||||
|
||||
for fold_idx, split in enumerate(plans[: args.n_splits]):
|
||||
fold_dir = out_root / f"fold{fold_idx}"
|
||||
fold_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
eye_train = filter_eye_samples(profile_eye.build_samples(df=split.train, clinical=data))
|
||||
bilat_val = filter_bilateral_samples(profile_patient.build_samples(df=split.val, clinical=data))
|
||||
|
||||
holdout_bilat = []
|
||||
if split.holdout is not None and not split.holdout.empty:
|
||||
holdout_bilat = filter_bilateral_samples(
|
||||
profile_patient.build_samples(df=split.holdout, clinical=data)
|
||||
)
|
||||
|
||||
if not eye_train or not bilat_val:
|
||||
print(f"[fold {fold_idx+1}] skipped (eye_train={len(eye_train)} bilat_val={len(bilat_val)})", flush=True)
|
||||
fold_metrics.append(
|
||||
{
|
||||
"fold": fold_idx,
|
||||
"best_epoch": None,
|
||||
"best_phase": None,
|
||||
"val_auc": float("nan"),
|
||||
"val_acc": float("nan"),
|
||||
"hld_auc": float("nan"),
|
||||
"hld_acc": float("nan"),
|
||||
"eye_train_n": len(eye_train),
|
||||
"bilat_val_n": len(bilat_val),
|
||||
"bilat_holdout_n": len(holdout_bilat),
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
sampler = build_balanced_sampler(eye_train) if args.balanced_sampling else None
|
||||
train_loader = DataLoader(
|
||||
EyeMetaDataset(eye_train),
|
||||
batch_size=args.batch_size,
|
||||
shuffle=(sampler is None),
|
||||
sampler=sampler,
|
||||
)
|
||||
val_loader = DataLoader(BilatMetaDataset(bilat_val), batch_size=args.batch_size, shuffle=False)
|
||||
holdout_loader = (
|
||||
DataLoader(BilatMetaDataset(holdout_bilat), batch_size=args.batch_size, shuffle=False)
|
||||
if holdout_bilat
|
||||
else None
|
||||
)
|
||||
|
||||
model = MetadataOnlySingleHT(
|
||||
clinical_data=data,
|
||||
num_classes=num_classes,
|
||||
md_hidden_dim=args.md_hidden_dim,
|
||||
fusion_dim=args.fusion_dim,
|
||||
dropout=args.dropout,
|
||||
use_se=bool(args.use_se),
|
||||
se_reduction=int(args.se_reduction),
|
||||
se_pre_norm=bool(args.se_pre_norm),
|
||||
).to(device)
|
||||
opt = torch.optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
|
||||
|
||||
best_auc = -1.0
|
||||
best_epoch = 0
|
||||
best_phase = ""
|
||||
best_state = None
|
||||
|
||||
print(
|
||||
f"\n[fold {fold_idx+1}/{args.n_splits}] "
|
||||
f"eye_train_n={len(eye_train)} bilat_val_n={len(bilat_val)} "
|
||||
f"holdout_n={len(holdout_bilat)} warmup={warm_tower}+{warm_fused} total={total_epochs}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
for ep in range(total_epochs):
|
||||
phase, main_ep = _phase_for_epoch(ep, warm_tower, warm_fused, int(args.epochs))
|
||||
tr_loss, tr_acc = train_single_epoch(
|
||||
model,
|
||||
train_loader,
|
||||
opt,
|
||||
device,
|
||||
phase=phase,
|
||||
bcd_prob=float(args.bcd_prob),
|
||||
)
|
||||
|
||||
_, p_val, val_auc, val_acc = _evaluate_single(
|
||||
model,
|
||||
val_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)
|
||||
best_state = copy.deepcopy(model.state_dict())
|
||||
best_epoch = ep + 1
|
||||
best_phase = phase
|
||||
|
||||
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}] "
|
||||
f"loss={tr_loss:.4f} acc={tr_acc:.4f} "
|
||||
f"val_auc={val_auc:.4f} val_acc={val_acc:.4f} "
|
||||
f"best_auc={best_auc:.4f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if best_state is not None:
|
||||
model.load_state_dict(best_state)
|
||||
|
||||
y_val, p_val, val_auc, val_acc = _evaluate_single(
|
||||
model,
|
||||
val_loader,
|
||||
device,
|
||||
num_classes,
|
||||
aggregate_patient=aggregate_patient,
|
||||
)
|
||||
|
||||
if args.tower_mode == "single":
|
||||
probs_name = "probs_classic.npy"
|
||||
probs_h_name = "probs_classic_holdout.npy"
|
||||
else:
|
||||
probs_name = "probs_ensemble.npy"
|
||||
probs_h_name = "probs_ensemble_holdout.npy"
|
||||
|
||||
np.save(fold_dir / "y_true.npy", y_val)
|
||||
np.save(fold_dir / probs_name, p_val)
|
||||
|
||||
hld_auc = float("nan")
|
||||
hld_acc = float("nan")
|
||||
if holdout_loader is not None:
|
||||
y_h, p_h, hld_auc, hld_acc = _evaluate_single(
|
||||
model,
|
||||
holdout_loader,
|
||||
device,
|
||||
num_classes,
|
||||
aggregate_patient=aggregate_patient,
|
||||
)
|
||||
np.save(fold_dir / "y_true_holdout.npy", y_h)
|
||||
np.save(fold_dir / probs_h_name, p_h)
|
||||
|
||||
print(
|
||||
f" [fold {fold_idx+1}] best_epoch={best_epoch} best_auc={best_auc:.4f} "
|
||||
f"val_auc={val_auc:.4f} val_acc={val_acc:.4f} "
|
||||
f"hld_auc={hld_auc:.4f} hld_acc={hld_acc:.4f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
fold_metrics.append(
|
||||
{
|
||||
"fold": fold_idx,
|
||||
"best_epoch": best_epoch,
|
||||
"best_phase": best_phase,
|
||||
"val_auc": float(val_auc),
|
||||
"val_acc": float(val_acc),
|
||||
"hld_auc": float(hld_auc),
|
||||
"hld_acc": float(hld_acc),
|
||||
"eye_train_n": len(eye_train),
|
||||
"bilat_val_n": len(bilat_val),
|
||||
"bilat_holdout_n": len(holdout_bilat),
|
||||
}
|
||||
)
|
||||
|
||||
val_aucs = [m["val_auc"] for m in fold_metrics if not np.isnan(m["val_auc"])]
|
||||
hld_aucs = [m["hld_auc"] for m in fold_metrics if not np.isnan(m["hld_auc"])]
|
||||
if val_aucs:
|
||||
print(f"\nMean val AUC: {np.mean(val_aucs):.4f} ± {np.std(val_aucs):.4f}", flush=True)
|
||||
if hld_aucs:
|
||||
print(f"Mean hld AUC: {np.mean(hld_aucs):.4f} ± {np.std(hld_aucs):.4f}", flush=True)
|
||||
|
||||
summary = {
|
||||
"run_name": args.run_name,
|
||||
"eval_mode": args.eval_mode,
|
||||
"tower_mode": args.tower_mode,
|
||||
"bridge_mode": "metadata_only",
|
||||
"model": "MetadataOnlySingleHT",
|
||||
"epochs": int(args.epochs),
|
||||
"warmup_tower_epochs": warm_tower,
|
||||
"warmup_fused_epochs": warm_fused,
|
||||
"md_hidden_dim": int(args.md_hidden_dim),
|
||||
"fusion_dim": int(args.fusion_dim),
|
||||
"dropout": float(args.dropout),
|
||||
"lr": float(args.lr),
|
||||
"weight_decay": float(args.weight_decay),
|
||||
"bcd_prob": float(args.bcd_prob),
|
||||
"balanced_sampling": bool(args.balanced_sampling),
|
||||
"feature_dim": int(data.feature_dim),
|
||||
"timestamp": time.strftime("%Y%m%d_%H%M%S"),
|
||||
"fold_metrics": fold_metrics,
|
||||
"val_auc_mean": float(np.mean(val_aucs)) if val_aucs else None,
|
||||
"val_auc_std": float(np.std(val_aucs)) if val_aucs else None,
|
||||
"hld_auc_mean": float(np.mean(hld_aucs)) if hld_aucs else None,
|
||||
"hld_auc_std": float(np.std(hld_aucs)) if hld_aucs else None,
|
||||
}
|
||||
(out_root / "summary.json").write_text(json.dumps(summary, indent=2))
|
||||
print(f"\nOutputs written to: {out_root}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Six V2HyperTower runs using GT ROI crop:
|
||||
# binary × {ensemble, bilateral} (runs 1-2)
|
||||
# multiclass × {ensemble, bilateral} (runs 3-4)
|
||||
# multiclass × {ensemble, bilateral} + balanced (runs 5-6)
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
CROP_ARGS=(
|
||||
--img-crop-manifest manifest.csv
|
||||
--img-crop-gt
|
||||
)
|
||||
|
||||
COMMON_ARGS=(
|
||||
--epochs 40
|
||||
--n-splits 5
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
)
|
||||
|
||||
# Runs 1-4: binary + multiclass, ensemble + bilateral, no balanced sampling
|
||||
echo "[1/2] GT ROI — binary + multiclass, ensemble + bilateral..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes binary multiclass \
|
||||
--tower-modes ensemble bilateral \
|
||||
--run-name v2_modes_gt_40ep_5fold_no_single_v2
|
||||
|
||||
# Runs 5-6: multiclass only, ensemble + bilateral, balanced sampling
|
||||
# (reuse the crop cache built during runs 1-4)
|
||||
echo "[2/2] GT ROI — multiclass, ensemble + bilateral, balanced sampling..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes multiclass \
|
||||
--tower-modes ensemble bilateral \
|
||||
--balanced-sampling \
|
||||
--persist-img-crop-cache \
|
||||
--run-name v2_modes_gt_40ep_5fold_multiclass_balanced_v2
|
||||
|
||||
echo "All runs complete."
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Bilateral tower runs (3 total):
|
||||
# binary × bilateral
|
||||
# multiclass × bilateral
|
||||
# multiclass × bilateral + balanced sampling
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
CROP_ARGS=(
|
||||
--img-crop-manifest manifest.csv
|
||||
--img-crop-gt
|
||||
)
|
||||
|
||||
COMMON_ARGS=(
|
||||
--epochs 40
|
||||
--n-splits 5
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
--tower-modes bilateral
|
||||
)
|
||||
|
||||
# Runs 1-2: binary + multiclass bilateral (no balanced sampling)
|
||||
echo "[1/2] GT ROI — binary + multiclass, bilateral..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes binary multiclass \
|
||||
--run-name v2_modes_gt_40ep_5fold_bilateral_v2
|
||||
|
||||
# Run 3: multiclass bilateral + balanced sampling
|
||||
# (reuse the crop cache built above)
|
||||
echo "[2/2] GT ROI — multiclass, bilateral, balanced sampling..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes multiclass \
|
||||
--balanced-sampling \
|
||||
--persist-img-crop-cache \
|
||||
--run-name v2_modes_gt_40ep_5fold_bilateral_balanced_v2
|
||||
|
||||
echo "Bilateral runs complete."
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Bilateral tower runs (3 total) — UNet ROI crop:
|
||||
# binary × bilateral
|
||||
# multiclass × bilateral
|
||||
# multiclass × bilateral + balanced sampling
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
CROP_ARGS=(
|
||||
--img-crop-manifest manifest.csv
|
||||
--img-crop-weights models/v2/refuge/segmentation/per_image_refuge_build/best.pt
|
||||
--img-crop-normalize per_image
|
||||
)
|
||||
|
||||
COMMON_ARGS=(
|
||||
--epochs 40
|
||||
--n-splits 5
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
--tower-modes bilateral
|
||||
)
|
||||
|
||||
# Runs 1-2: binary + multiclass bilateral (no balanced sampling)
|
||||
echo "[1/2] UNet ROI — binary + multiclass, bilateral..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes binary multiclass \
|
||||
--run-name v2_modes_unet_40ep_5fold_bilateral_v2
|
||||
|
||||
# Run 3: multiclass bilateral + balanced sampling
|
||||
# (reuse the crop cache built above)
|
||||
echo "[2/2] UNet ROI — multiclass, bilateral, balanced sampling..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes multiclass \
|
||||
--balanced-sampling \
|
||||
--persist-img-crop-cache \
|
||||
--run-name v2_modes_unet_40ep_5fold_bilateral_balanced_v2
|
||||
|
||||
echo "Bilateral UNet runs complete."
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Ensemble tower runs (3 total):
|
||||
# binary × ensemble
|
||||
# multiclass × ensemble
|
||||
# multiclass × ensemble + balanced sampling
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
CROP_ARGS=(
|
||||
--img-crop-manifest manifest.csv
|
||||
--img-crop-gt
|
||||
)
|
||||
|
||||
COMMON_ARGS=(
|
||||
--epochs 40
|
||||
--n-splits 5
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
--tower-modes ensemble
|
||||
)
|
||||
|
||||
# Runs 1-2: binary + multiclass ensemble (no balanced sampling)
|
||||
echo "[1/2] GT ROI — binary + multiclass, ensemble..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes binary multiclass \
|
||||
--run-name v2_modes_gt_40ep_5fold_ensemble_v2
|
||||
|
||||
# Run 3: multiclass ensemble + balanced sampling
|
||||
# (reuse the crop cache built above)
|
||||
echo "[2/2] GT ROI — multiclass, ensemble, balanced sampling..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes multiclass \
|
||||
--balanced-sampling \
|
||||
--persist-img-crop-cache \
|
||||
--run-name v2_modes_gt_40ep_5fold_ensemble_balanced_v2
|
||||
|
||||
echo "Ensemble runs complete."
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Ensemble tower runs (3 total) — UNet ROI crop:
|
||||
# binary × ensemble
|
||||
# multiclass × ensemble
|
||||
# multiclass × ensemble + balanced sampling
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
CROP_ARGS=(
|
||||
--img-crop-manifest manifest.csv
|
||||
--img-crop-weights models/v2/refuge/segmentation/per_image_refuge_build/best.pt
|
||||
--img-crop-normalize per_image
|
||||
)
|
||||
|
||||
COMMON_ARGS=(
|
||||
--epochs 40
|
||||
--n-splits 5
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
--tower-modes ensemble
|
||||
)
|
||||
|
||||
# Runs 1-2: binary + multiclass ensemble (no balanced sampling)
|
||||
echo "[1/2] UNet ROI — binary + multiclass, ensemble..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes binary multiclass \
|
||||
--run-name v2_modes_unet_40ep_5fold_ensemble_v2
|
||||
|
||||
# Run 3: multiclass ensemble + balanced sampling
|
||||
# (reuse the crop cache built above)
|
||||
echo "[2/2] UNet ROI — multiclass, ensemble, balanced sampling..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
"${CROP_ARGS[@]}" \
|
||||
--eval-modes multiclass \
|
||||
--balanced-sampling \
|
||||
--persist-img-crop-cache \
|
||||
--run-name v2_modes_unet_40ep_5fold_ensemble_balanced_v2
|
||||
|
||||
echo "Ensemble UNet runs complete."
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Quick smoke test for ROI mode runs:
|
||||
# 1) GT masks
|
||||
# 2) UNet masks
|
||||
# Uses 1 epoch and 1 fold for fast validation.
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
COMMON_ARGS=(
|
||||
--eval-modes binary multiclass
|
||||
--tower-modes single ensemble bilateral
|
||||
--epochs 1
|
||||
--n-splits 2
|
||||
--folds 1
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
--img-crop-manifest manifest.csv
|
||||
--warmup-tower-epochs 0
|
||||
--warmup-fused-epochs 0
|
||||
--single-warmup-tower-epochs 0
|
||||
--single-warmup-fused-epochs 0
|
||||
--bilat-warmup-tower-epochs 0
|
||||
--bilat-warmup-fused-epochs 0
|
||||
)
|
||||
|
||||
echo "[smoke 1/2] Starting GT ROI run..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
--img-crop-gt \
|
||||
--run-name smoke_v2_modes_roi_gt
|
||||
|
||||
echo "[smoke 2/2] Starting UNet ROI run..."
|
||||
python3 scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
"${COMMON_ARGS[@]}" \
|
||||
--img-crop-weights models/v2/refuge/segmentation/per_image_refuge_build/best.pt \
|
||||
--img-crop-normalize per_image \
|
||||
--run-name smoke_v2_modes_roi_unet_perimage
|
||||
|
||||
echo "Smoke runs complete."
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Multiclass runs v2.2 (6 total):
|
||||
# UNet crop: single | ensemble | fused head
|
||||
# GT crop: single | ensemble | fused head
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
MANIFEST="manifest.csv"
|
||||
UNET_WEIGHTS="models/v2/refuge/segmentation/per_image/best.pt"
|
||||
|
||||
COMMON=(
|
||||
--epochs 40
|
||||
--n-splits 5
|
||||
--batch-size 8
|
||||
--backbone refugelike
|
||||
--eval-mode multiclass
|
||||
--img-crop-manifest "$MANIFEST"
|
||||
)
|
||||
|
||||
UNET_CROP=(
|
||||
--img-crop-weights "$UNET_WEIGHTS"
|
||||
)
|
||||
|
||||
GT_CROP=(
|
||||
--img-crop-gt
|
||||
)
|
||||
|
||||
# ── UNet crop ────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[1/6] UNet crop — multiclass, single..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${UNET_CROP[@]}" \
|
||||
--tower-mode single \
|
||||
--run-name v2.2_single_multiclass_unet_40ep_5fold
|
||||
|
||||
echo "[2/6] UNet crop — multiclass, ensemble..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${UNET_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--run-name v2.2_ensemble_multiclass_unet_40ep_5fold
|
||||
|
||||
echo "[3/6] UNet crop — multiclass, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${UNET_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--fused-head --fusion-epochs 10 \
|
||||
--run-name v2.2_fused_multiclass_unet_40ep_5fold
|
||||
|
||||
# ── GT crop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[4/6] GT crop — multiclass, single..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${GT_CROP[@]}" \
|
||||
--tower-mode single \
|
||||
--run-name v2.2_single_multiclass_gt_40ep_5fold
|
||||
|
||||
echo "[5/6] GT crop — multiclass, ensemble..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${GT_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--run-name v2.2_ensemble_multiclass_gt_40ep_5fold
|
||||
|
||||
echo "[6/6] GT crop — multiclass, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${GT_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--fused-head --fusion-epochs 10 \
|
||||
--run-name v2.2_fused_multiclass_gt_40ep_5fold
|
||||
|
||||
echo "Multiclass v2.2 runs complete."
|
||||
Reference in New Issue
Block a user