added better memory caching, multithreaded processing, cleanup scripts dir
This commit is contained in:
@@ -1,326 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run classical ML models and plot *combined* ROC curves (multimodel overlays).
|
||||
|
||||
Keeps your original workflow for folds/tests exactly the same.
|
||||
Only changes: collects predictions per test and makes:
|
||||
• One ROC plot per class (OvR), overlaying all models
|
||||
• One binary ROC plot (Healthy vs Glaucoma), overlaying all models
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Tuple, Dict, Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from sklearn.preprocessing import StandardScaler, label_binarize
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.svm import SVC
|
||||
from sklearn.metrics import roc_curve, auc
|
||||
|
||||
from classes import build_papila_clinical
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
# ---------------------------------------------------------------------------
|
||||
SPLIT_ROOT = Path("HelpCode/kfold")
|
||||
TRUST_INDEX_COL = False
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Feature matrix
|
||||
# ---------------------------------------------------------------------------
|
||||
def build_feature_matrix(clinical):
|
||||
df = clinical.df.copy()
|
||||
scalars = ["Age", "dioptre_1", "dioptre_2", "astigmatism", "Pachymetry", "Axial_Length", "IOP_corr"]
|
||||
cats = ["Gender", "Phakic/Pseudophakic"]
|
||||
X = pd.concat([df[scalars], pd.get_dummies(df[cats].astype("category"), drop_first=False, prefix=cats)], axis=1)
|
||||
y = df[clinical.label_col].astype(int).values
|
||||
return X, y, scalars, df # X keeps NaNs; we impute per-fold
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Models with tuned hyper-parameters (unchanged)
|
||||
# ---------------------------------------------------------------------------
|
||||
def make_models() -> Dict[str, Pipeline]:
|
||||
return {
|
||||
"LogReg": Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", LogisticRegression(
|
||||
C=1,
|
||||
class_weight="balanced",
|
||||
max_iter=200,
|
||||
solver="lbfgs",
|
||||
multi_class="auto")),
|
||||
]),
|
||||
"kNN": Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", KNeighborsClassifier(
|
||||
n_neighbors=11, weights="distance")),
|
||||
]),
|
||||
"RF": Pipeline([
|
||||
("clf", RandomForestClassifier(n_estimators=200, max_depth=8,
|
||||
min_samples_split=4, random_state=42)),
|
||||
]),
|
||||
"SVM": Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", SVC(C=10, kernel="rbf", gamma=0.1, probability=True)),
|
||||
]),
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Split helpers copied from paper_clinical_baselines_official.py (unchanged)
|
||||
# ---------------------------------------------------------------------------
|
||||
_FNAME_RE = re.compile(r"RET\s*(\d+)\s*([Oo][DSs])\.jpg$", re.IGNORECASE)
|
||||
|
||||
def _read_sheet_any(p: Path) -> pd.DataFrame:
|
||||
if p.suffix.lower() == ".xlsx":
|
||||
return pd.read_excel(p)
|
||||
if p.suffix.lower() == ".csv":
|
||||
return pd.read_csv(p)
|
||||
if p.suffix.lower() == ".txt":
|
||||
lines = [ln.strip() for ln in p.read_text(encoding="utf-8", errors="ignore").splitlines() if ln.strip()]
|
||||
return pd.DataFrame({"filename": lines})
|
||||
raise ValueError(f"Unsupported split file type: {p.suffix}")
|
||||
|
||||
def _normcols(cols: List[str]) -> Dict[str, str]:
|
||||
def norm(s: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]", "", s.lower())
|
||||
return {norm(c): c for c in cols}
|
||||
|
||||
def _parse_fname_to_pid_eye(fname: str) -> Optional[Tuple[int, str]]:
|
||||
base = os.path.basename(str(fname))
|
||||
m = _FNAME_RE.search(base.replace(" ", ""))
|
||||
if not m:
|
||||
return None
|
||||
return int(m.group(1)), m.group(2).upper()
|
||||
|
||||
def _rows_from_sheet(sheet: pd.DataFrame, df_master: pd.DataFrame) -> List[int]:
|
||||
cols = _normcols(list(sheet.columns))
|
||||
if "filename" in cols:
|
||||
fn_col = cols["filename"]
|
||||
lookup: Dict[str, List[int]] = {}
|
||||
for i, (pid, eye) in enumerate(zip(df_master["Patient ID"].astype(int), df_master["eyeID"].astype(str))):
|
||||
lookup.setdefault(f"{pid}|{eye.upper()}", []).append(i)
|
||||
rows: List[int] = []
|
||||
for fn in sheet[fn_col].astype(str).tolist():
|
||||
pe = _parse_fname_to_pid_eye(fn)
|
||||
if pe is None:
|
||||
continue
|
||||
pid, eye = pe
|
||||
rows.extend(lookup.get(f"{pid}|{eye}", []))
|
||||
return rows
|
||||
if "patientid" in cols and "eyeid" in cols:
|
||||
pid_col, eye_col = cols["patientid"], cols["eyeid"]
|
||||
lookup = {}
|
||||
for i, (pid, eye) in enumerate(zip(df_master["Patient ID"].astype(int), df_master["eyeID"].astype(str))):
|
||||
lookup.setdefault(f"{pid}|{eye.upper()}", []).append(i)
|
||||
rows = []
|
||||
for pid, eye in zip(sheet[pid_col], sheet[eye_col]):
|
||||
rows.extend(lookup.get(f"{int(pid)}|{str(eye).upper()}", []))
|
||||
return rows
|
||||
if TRUST_INDEX_COL and "index" in cols:
|
||||
idx = sheet[cols["index"]].astype(int).tolist()
|
||||
n = len(df_master)
|
||||
return [i for i in idx if 0 <= i < n]
|
||||
raise RuntimeError("Split sheet missing usable columns")
|
||||
|
||||
def _pair_train_test_files(dir_train: Path, dir_test: Path) -> List[Tuple[Path, Path]]:
|
||||
def fold_key(p: Path) -> str:
|
||||
m = re.search(r"(\d+)", p.stem)
|
||||
return m.group(1) if m else p.stem.lower()
|
||||
trains = sorted([p for p in dir_train.iterdir() if p.is_file() and p.suffix.lower() in (".xlsx", ".csv", ".txt")], key=fold_key)
|
||||
tests = sorted([p for p in dir_test.iterdir() if p.is_file() and p.suffix.lower() in (".xlsx", ".csv", ".txt")], key=fold_key)
|
||||
return [(trains[i], tests[i]) for i in range(min(len(trains), len(tests)))]
|
||||
|
||||
def iter_official_folds_xlsx(clinical, split_root: Path, test_name: str) -> Iterable[Tuple[pd.DataFrame, pd.DataFrame]]:
|
||||
df_master = clinical.df.copy()
|
||||
test_dir = split_root / test_name
|
||||
dir_train = test_dir / "Train"
|
||||
dir_test = test_dir / "Test"
|
||||
if not dir_train.exists() or not dir_test.exists():
|
||||
raise FileNotFoundError(f"Expected: {dir_train} and {dir_test}")
|
||||
for train_file, test_file in _pair_train_test_files(dir_train, dir_test):
|
||||
sh_tr, sh_te = _read_sheet_any(train_file), _read_sheet_any(test_file)
|
||||
tr_rows, te_rows = _rows_from_sheet(sh_tr, df_master), _rows_from_sheet(sh_te, df_master)
|
||||
tr_df, te_df = df_master.iloc[tr_rows].copy(), df_master.iloc[te_rows].copy()
|
||||
yield tr_df, te_df
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Utilities (unchanged)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _prepare_fold_X(X: pd.DataFrame, scalars: List[str], tr_idx: np.ndarray, te_idx: np.ndarray):
|
||||
Xtr, Xte = X.iloc[tr_idx].copy(), X.iloc[te_idx].copy()
|
||||
med = Xtr[scalars].median(numeric_only=True)
|
||||
Xtr[scalars] = Xtr[scalars].fillna(med)
|
||||
Xte[scalars] = Xte[scalars].fillna(med)
|
||||
return Xtr.values.astype(np.float32), Xte.values.astype(np.float32)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NEW: combined plotting helpers (multimodel overlays)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _plot_multiclass_overlay(y_true: np.ndarray, prob_dict: Dict[str, np.ndarray], out_dir: Path, test_tag: str):
|
||||
"""One figure per class (OvR), overlaying all models."""
|
||||
n_classes = next(iter(prob_dict.values())).shape[1]
|
||||
class_names = [f"Class{k}" for k in range(n_classes)]
|
||||
y_bin = label_binarize(y_true, classes=list(range(n_classes)))
|
||||
|
||||
for k in range(n_classes):
|
||||
fig, ax = plt.subplots(figsize=(6, 5))
|
||||
for model_name, proba in prob_dict.items():
|
||||
fpr, tpr, _ = roc_curve(y_bin[:, k], proba[:, k])
|
||||
auc_val = auc(fpr, tpr)
|
||||
ax.plot(fpr, tpr, lw=1.8, label=f"{model_name} (AUC={auc_val:.3f})")
|
||||
ax.plot([0, 1], [0, 1], "k--", lw=1)
|
||||
ax.set_xlabel("False Positive Rate")
|
||||
ax.set_ylabel("True Positive Rate")
|
||||
ax.set_title(f"{class_names[k]} vs Rest — {test_tag}")
|
||||
ax.legend(loc="lower right")
|
||||
ax.grid(True, alpha=0.3, linestyle="--")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out_dir / f"{test_tag}_{class_names[k]}.png", dpi=170)
|
||||
plt.close(fig)
|
||||
|
||||
def _plot_binary_overlay(y_true: np.ndarray, prob1d_dict: Dict[str, np.ndarray], out_dir: Path, test_tag: str):
|
||||
"""One figure (Healthy vs Glaucoma), overlaying all models. Assumes y_true ∈ {0,1}."""
|
||||
fig, ax = plt.subplots(figsize=(6, 5))
|
||||
any_curve = False
|
||||
for model_name, scores in prob1d_dict.items():
|
||||
if scores.size == 0:
|
||||
continue
|
||||
fpr, tpr, _ = roc_curve(y_true, scores, pos_label=1)
|
||||
auc_val = auc(fpr, tpr)
|
||||
ax.plot(fpr, tpr, lw=1.8, label=f"{model_name} (AUC={auc_val:.3f})")
|
||||
any_curve = True
|
||||
ax.plot([0, 1], [0, 1], "k--", lw=1)
|
||||
ax.set_xlabel("False Positive Rate")
|
||||
ax.set_ylabel("True Positive Rate")
|
||||
ax.set_title(f"Binary Healthy vs Glaucoma — {test_tag}")
|
||||
if any_curve:
|
||||
ax.legend(loc="lower right")
|
||||
ax.grid(True, alpha=0.3, linestyle="--")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out_dir / f"{test_tag}_binary.png", dpi=170)
|
||||
plt.close(fig)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main (same folds/tests flow; only result collation & plotting changed)
|
||||
# ---------------------------------------------------------------------------
|
||||
def main():
|
||||
clinical = build_papila_clinical(
|
||||
image_dir="Papila/FundusImages",
|
||||
clinical_dir="Papila/ClinicalData",
|
||||
label_col="Diagnosis",
|
||||
cat_cols=["Gender", "Phakic/Pseudophakic"],
|
||||
)
|
||||
X, y, scalars, _ = build_feature_matrix(clinical)
|
||||
models = make_models()
|
||||
out_dir = Path("analysis_data/roc_baselines")
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tests = [
|
||||
("Test 3", False), ("Test 4", True)
|
||||
] if (SPLIT_ROOT / "Test 3").exists() else [
|
||||
("Test 1", False), ("Test 2", True)
|
||||
]
|
||||
|
||||
for test_name, is_binary in tests:
|
||||
# Collect per-model probabilities following your original per-model loop.
|
||||
# For multiclass: dict[model] -> (N, C)
|
||||
# For binary: dict[model] -> (N,) (probability of class 1)
|
||||
prob_dict_multi: Dict[str, np.ndarray] = {}
|
||||
prob_dict_bin: Dict[str, np.ndarray] = {}
|
||||
y_ref_multi: Optional[np.ndarray] = None
|
||||
y_ref_bin: Optional[np.ndarray] = None
|
||||
|
||||
for model_name, model in models.items():
|
||||
y_all: List[np.ndarray] = []
|
||||
p_all: List[np.ndarray] = []
|
||||
|
||||
for fold_idx, (train_df, test_df) in enumerate(iter_official_folds_xlsx(clinical, SPLIT_ROOT, test_name), 1):
|
||||
# Keep your exact masking/handling
|
||||
dup_rows = set(train_df.index).intersection(set(test_df.index))
|
||||
shared_pids = set(train_df["Patient ID"]).intersection(set(test_df["Patient ID"]))
|
||||
if test_name in ("Test 1", "Test 2") and shared_pids:
|
||||
train_df = train_df[~train_df["Patient ID"].isin(shared_pids)].copy()
|
||||
dup_rows = set(train_df.index).intersection(set(test_df.index))
|
||||
shared_pids = set(train_df["Patient ID"]).intersection(set(test_df["Patient ID"]))
|
||||
|
||||
tr_idx, te_idx = train_df.index.values, test_df.index.values
|
||||
|
||||
if is_binary:
|
||||
# original binary handling: drop Suspects on both sets
|
||||
mask_tr = np.isin(y[tr_idx], [0, 1])
|
||||
mask_te = np.isin(y[te_idx], [0, 1])
|
||||
if not mask_tr.any() or not mask_te.any():
|
||||
# skip empty fold (keeps behavior safe without changing fold logic)
|
||||
continue
|
||||
Xtr, Xte = _prepare_fold_X(X, scalars, tr_idx[mask_tr], te_idx[mask_te])
|
||||
ytr, yte = y[tr_idx][mask_tr], y[te_idx][mask_te]
|
||||
else:
|
||||
Xtr, Xte = _prepare_fold_X(X, scalars, tr_idx, te_idx)
|
||||
ytr, yte = y[tr_idx], y[te_idx]
|
||||
|
||||
# Fit and score (unchanged approach)
|
||||
model.fit(Xtr, ytr)
|
||||
if is_binary:
|
||||
if hasattr(model[-1], "predict_proba"):
|
||||
prob = model.predict_proba(Xte)[:, 1]
|
||||
else:
|
||||
dec = model.decision_function(Xte)
|
||||
prob = 1.0 / (1.0 + np.exp(-dec)) if np.ptp(dec) > 0 else np.full_like(dec, 0.5)
|
||||
y_all.append(yte)
|
||||
p_all.append(prob)
|
||||
else:
|
||||
if hasattr(model[-1], "predict_proba"):
|
||||
prob = model.predict_proba(Xte)
|
||||
else:
|
||||
dec = model.decision_function(Xte)
|
||||
if dec.ndim == 1:
|
||||
dec = np.stack([-dec, dec], axis=1)
|
||||
e = np.exp(dec - dec.max(axis=1, keepdims=True))
|
||||
prob = e / e.sum(axis=1, keepdims=True)
|
||||
y_all.append(yte)
|
||||
p_all.append(prob)
|
||||
|
||||
if not y_all:
|
||||
# No valid folds for this model under this test (e.g., all-bad after mask); skip
|
||||
continue
|
||||
|
||||
y_cat = np.concatenate(y_all)
|
||||
p_cat = np.concatenate(p_all)
|
||||
|
||||
if is_binary:
|
||||
# Store 1D scores per model
|
||||
prob_dict_bin[model_name] = p_cat
|
||||
if y_ref_bin is None:
|
||||
y_ref_bin = y_cat
|
||||
else:
|
||||
# Align lengths defensively (should match in normal use)
|
||||
n = min(len(y_ref_bin), len(y_cat))
|
||||
y_ref_bin = y_ref_bin[:n]
|
||||
prob_dict_bin[model_name] = prob_dict_bin[model_name][:n]
|
||||
else:
|
||||
# Store (N, C) per model
|
||||
prob_dict_multi[model_name] = p_cat
|
||||
if y_ref_multi is None:
|
||||
y_ref_multi = y_cat
|
||||
else:
|
||||
# Align lengths defensively (should match in normal use)
|
||||
n = min(len(y_ref_multi), len(y_cat))
|
||||
y_ref_multi = y_ref_multi[:n]
|
||||
prob_dict_multi[model_name] = prob_dict_multi[model_name][:n, :]
|
||||
|
||||
tag = test_name.replace(" ", "")
|
||||
|
||||
# Produce overlays
|
||||
if prob_dict_multi and y_ref_multi is not None:
|
||||
_plot_multiclass_overlay(y_ref_multi, prob_dict_multi, out_dir, tag)
|
||||
if prob_dict_bin and y_ref_bin is not None:
|
||||
_plot_binary_overlay(y_ref_bin, prob_dict_bin, out_dir, tag)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,384 +0,0 @@
|
||||
"""Evaluate REFUGE-trained classifier on Papila images using UNet crops."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Sequence, Set
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
from tqdm import tqdm
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
import sys
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from classes.refuge_preprocessing import RefugePreprocessing, RefugeSample
|
||||
from classes.refuge_segmentation import RefugeSegmentation
|
||||
from classes.refuge_classification import (
|
||||
RefugeClassification,
|
||||
RefugeClassificationDataset,
|
||||
RefugeClassificationRecord,
|
||||
UNetGeometryProvider,
|
||||
_default_image_transform,
|
||||
_geometry_from_mask,
|
||||
)
|
||||
from classes.backbones import BACKBONES, load_backbone_weights
|
||||
from classes.unet_segmenter import UNetSegmenter
|
||||
from classes.papila_builders import build_papila_clinical
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Evaluate classifier on Papila with UNet crops")
|
||||
parser.add_argument("--filtered-metrics", type=Path, required=True, help="CSV of Papila samples with acceptable Dice")
|
||||
parser.add_argument("--segmenter-manifest", type=Path, required=True, help="Manifest used to train the UNet segmenter")
|
||||
parser.add_argument("--segmenter-weights", type=Path, required=True, help="Path to trained UNet weights (best.pt)")
|
||||
parser.add_argument("--classifier-weights", type=Path, required=False, help="Path to classifier checkpoint (refuge_classifier_best.pt)")
|
||||
parser.add_argument("--refuge-root", type=Path, default=Path("REFUGE"))
|
||||
parser.add_argument("--image-dir", type=Path, default=Path("Papila/FundusImages"))
|
||||
parser.add_argument("--clinical-dir", type=Path, default=Path("Papila/ClinicalData"))
|
||||
parser.add_argument("--label-col", type=str, default="Diagnosis", help="Column name holding Papila labels")
|
||||
parser.add_argument(
|
||||
"--positive-labels",
|
||||
nargs="*",
|
||||
default=["glaucoma", "glaucoma suspect", "suspect"],
|
||||
help="Values treated as glaucoma-positive when labels are non-numeric",
|
||||
)
|
||||
parser.add_argument("--dice-threshold", type=float, default=0.01, help="Minimum Dice (disc or cup) to keep a sample")
|
||||
parser.add_argument("--segmenter-threshold", type=float, default=0.5, help="Probability threshold for UNet geometry")
|
||||
parser.add_argument("--segmenter-normalize", choices=["none", "imagenet", "per_image"], default="per_image")
|
||||
parser.add_argument("--segmenter-tta", action="store_true", help="Enable TTA (H/V flips) when deriving geometry")
|
||||
parser.add_argument("--crop-scale", type=float, default=2.5)
|
||||
parser.add_argument("--crop-size", type=int, default=224)
|
||||
parser.add_argument("--batch-size", type=int, default=32)
|
||||
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
||||
parser.add_argument("--output", type=Path, default=None, help="Optional CSV to store per-sample probabilities")
|
||||
parser.add_argument(
|
||||
"--cache-dir",
|
||||
type=Path,
|
||||
default=Path("analysis_data/classifier_cache"),
|
||||
help="Directory to reuse classifier preprocessing cache",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-gt-masks",
|
||||
action="store_true",
|
||||
help="Use ground truth Papila contours instead of UNet predictions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gt-contours-dir",
|
||||
type=Path,
|
||||
default=Path("Papila/ExpertsSegmentations/Contours"),
|
||||
help="Directory containing Papila contour text files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backbone",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Optional backbone name (e.g. inception_v3, densenet121). Requires matching classifier weights.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_allowed_ids(path: Path, dice_threshold: float) -> Set[str]:
|
||||
allowed: Set[str] = set()
|
||||
with path.open(newline="") as fp:
|
||||
reader = csv.DictReader(fp)
|
||||
for row in reader:
|
||||
sample_id = row.get("sample_id")
|
||||
if not sample_id or sample_id == "__mean__":
|
||||
continue
|
||||
try:
|
||||
disc = float(row.get("dice_disc", "nan"))
|
||||
cup = float(row.get("dice_cup", "nan"))
|
||||
except ValueError:
|
||||
continue
|
||||
if disc < dice_threshold and cup < dice_threshold:
|
||||
continue
|
||||
allowed.add(sample_id)
|
||||
return allowed
|
||||
|
||||
|
||||
def build_papila_samples(
|
||||
image_dir: Path,
|
||||
clinical_dir: Path,
|
||||
label_col: str,
|
||||
positive_labels: Sequence[str],
|
||||
allowed_ids: Set[str],
|
||||
) -> List[RefugeSample]:
|
||||
clinical = build_papila_clinical(
|
||||
image_dir=str(image_dir),
|
||||
clinical_dir=str(clinical_dir),
|
||||
label_col=label_col,
|
||||
cat_cols=[],
|
||||
)
|
||||
positives = {lbl.lower() for lbl in positive_labels}
|
||||
samples: Dict[str, RefugeSample] = {}
|
||||
for _, row in clinical.df.iterrows():
|
||||
image_path = clinical.get_image_path(row)
|
||||
sample_id = f"papila_{image_path.stem}"
|
||||
if sample_id not in allowed_ids or sample_id in samples:
|
||||
continue
|
||||
value = row.get(label_col)
|
||||
if value is None or (isinstance(value, float) and np.isnan(value)):
|
||||
continue
|
||||
label: Optional[int]
|
||||
try:
|
||||
label_int = int(value)
|
||||
if label_int == 2:
|
||||
continue
|
||||
label = 1 if label_int > 0 else 0
|
||||
except (TypeError, ValueError):
|
||||
label = 1 if str(value).strip().lower() in positives else 0
|
||||
samples[sample_id] = RefugeSample(
|
||||
sample_id=sample_id,
|
||||
dataset="papila",
|
||||
split="eval",
|
||||
image_path=Path(image_path),
|
||||
label=label,
|
||||
device=None,
|
||||
mask_path=None,
|
||||
fovea_coord=None,
|
||||
)
|
||||
return list(samples.values())
|
||||
|
||||
|
||||
def load_contour(path: Path) -> np.ndarray:
|
||||
coords = np.loadtxt(path)
|
||||
if coords.ndim == 1:
|
||||
coords = coords.reshape(-1, 2)
|
||||
return coords
|
||||
|
||||
|
||||
def contour_to_mask(coords: np.ndarray, size: Sequence[int]) -> np.ndarray:
|
||||
if coords is None or coords.size == 0:
|
||||
return np.zeros((size[1], size[0]), dtype=np.uint8)
|
||||
img = Image.new("L", size, 0)
|
||||
draw = ImageDraw.Draw(img)
|
||||
points = [tuple(map(float, pt)) for pt in coords]
|
||||
draw.polygon(points, outline=1, fill=1)
|
||||
return np.array(img, dtype=np.uint8)
|
||||
|
||||
|
||||
class PapilaGTGeometryProvider:
|
||||
def __init__(self, contours_dir: Path) -> None:
|
||||
self.contours_dir = contours_dir
|
||||
|
||||
def _pick(self, base: str, kind: str) -> Optional[Path]:
|
||||
for exp in ("exp2", "exp1"):
|
||||
cand = self.contours_dir / f"{base}_{kind}_{exp}.txt"
|
||||
if cand.exists():
|
||||
return cand
|
||||
return None
|
||||
|
||||
def __call__(self, sample: RefugeSample, scale: float):
|
||||
base = Path(sample.image_path).stem
|
||||
disc_path = self._pick(base, "disc")
|
||||
cup_path = self._pick(base, "cup")
|
||||
if disc_path is None or cup_path is None:
|
||||
raise RuntimeError(f"Missing ground-truth contours for {sample.sample_id}")
|
||||
|
||||
image = Image.open(sample.image_path).convert("RGB")
|
||||
disc_coords = load_contour(disc_path)
|
||||
cup_coords = load_contour(cup_path)
|
||||
disc_mask = contour_to_mask(disc_coords, image.size)
|
||||
cup_mask = contour_to_mask(cup_coords, image.size)
|
||||
cup_mask = ((cup_mask > 0) & (disc_mask > 0)).astype(np.uint8)
|
||||
geom = _geometry_from_mask(disc_mask, scale)
|
||||
return geom, disc_mask.astype(np.uint8), cup_mask.astype(np.uint8)
|
||||
|
||||
|
||||
def build_backbone(name: Optional[str]) -> Optional[torch.nn.Module]:
|
||||
if not name:
|
||||
return None
|
||||
key = name.lower()
|
||||
if key not in BACKBONES:
|
||||
raise ValueError(f"Unknown backbone '{name}'. Available: {', '.join(sorted(BACKBONES.keys()))}")
|
||||
spec = BACKBONES[key]
|
||||
model = spec.ctor(weights=spec.weights_default)
|
||||
out_dim, model = spec.strip(model)
|
||||
setattr(model, "_feature_dim", out_dim)
|
||||
if key == "refugelike":
|
||||
load_backbone_weights(key, model)
|
||||
return model
|
||||
|
||||
|
||||
def evaluate_records(
|
||||
clf: RefugeClassification,
|
||||
records: Sequence[RefugeClassificationRecord],
|
||||
device: str,
|
||||
batch_size: int,
|
||||
) -> Dict[str, float]:
|
||||
dataset = RefugeClassificationDataset(
|
||||
records,
|
||||
transform=clf.eval_transform,
|
||||
polar_transform=clf.polar_transform,
|
||||
size=clf.crop_size,
|
||||
)
|
||||
loader = DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=0)
|
||||
clf.backbone.to(device).eval()
|
||||
clf.classifier_head.to(device).eval()
|
||||
preds: List[float] = []
|
||||
targets: List[int] = []
|
||||
with torch.no_grad():
|
||||
for batch in tqdm(loader, desc="Papila Eval", leave=False, unit="batch"):
|
||||
images = batch["image"].to(device)
|
||||
polars = batch["polar"].to(device)
|
||||
extra_feats = batch["features"].to(device)
|
||||
labels = batch["label"].cpu().numpy().tolist()
|
||||
feats_img = clf.backbone(images)
|
||||
feats = feats_img
|
||||
if clf.use_polar:
|
||||
feats_polar = clf.backbone(polars)
|
||||
feats = torch.cat([feats, feats_polar], dim=1)
|
||||
if clf.extra_feature_dim > 0:
|
||||
feats = torch.cat([feats, extra_feats], dim=1)
|
||||
logits = clf.classifier_head(feats)
|
||||
probs = torch.softmax(logits, dim=1)[:, 1].cpu().numpy().tolist()
|
||||
preds.extend(probs)
|
||||
targets.extend(labels)
|
||||
metrics: Dict[str, float] = {"count": float(len(targets))}
|
||||
unique_labels = set(targets)
|
||||
if len(unique_labels) >= 2:
|
||||
metrics["auc"] = float(torchmetrics_auc(targets, preds))
|
||||
else:
|
||||
metrics["auc"] = float("nan")
|
||||
preds_bin = [1 if p >= 0.5 else 0 for p in preds]
|
||||
accuracy = sum(int(p == t) for p, t in zip(preds_bin, targets)) / max(1, len(targets))
|
||||
metrics["accuracy"] = float(accuracy)
|
||||
metrics["mean_prob"] = float(np.mean(preds)) if preds else float("nan")
|
||||
metrics["labels_pos"] = float(sum(targets))
|
||||
if preds:
|
||||
metrics["probs_std"] = float(np.std(preds))
|
||||
return metrics
|
||||
|
||||
|
||||
def torchmetrics_auc(targets: Sequence[int], preds: Sequence[float]) -> float:
|
||||
try:
|
||||
from sklearn.metrics import roc_auc_score
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("scikit-learn is required to compute AUC") from exc
|
||||
|
||||
return float(roc_auc_score(targets, preds))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
device = args.device
|
||||
|
||||
allowed_ids = load_allowed_ids(args.filtered_metrics, args.dice_threshold)
|
||||
if not allowed_ids:
|
||||
raise SystemExit("No Papila samples passed the Dice threshold.")
|
||||
|
||||
papila_samples = build_papila_samples(
|
||||
args.image_dir,
|
||||
args.clinical_dir,
|
||||
args.label_col,
|
||||
args.positive_labels,
|
||||
allowed_ids,
|
||||
)
|
||||
if not papila_samples:
|
||||
raise SystemExit("No Papila samples with labels matched the filtered metrics.")
|
||||
|
||||
cache_dir = args.cache_dir
|
||||
if args.use_gt_masks and cache_dir is not None:
|
||||
cache_dir = cache_dir / "gt"
|
||||
|
||||
if args.use_gt_masks:
|
||||
geometry_provider = PapilaGTGeometryProvider(args.gt_contours_dir)
|
||||
segmenter = None
|
||||
else:
|
||||
segmenter = UNetSegmenter(
|
||||
manifest_path=args.segmenter_manifest,
|
||||
device=device,
|
||||
normalize=args.segmenter_normalize,
|
||||
)
|
||||
seg_state = torch.load(args.segmenter_weights, map_location=device)
|
||||
seg_state_dict = seg_state.get("model", seg_state)
|
||||
segmenter.model.load_state_dict(seg_state_dict)
|
||||
segmenter.model.to(device)
|
||||
geometry_provider = UNetGeometryProvider(
|
||||
segmenter=segmenter,
|
||||
threshold=args.segmenter_threshold,
|
||||
tta=args.segmenter_tta,
|
||||
)
|
||||
|
||||
pre = RefugePreprocessing(args.refuge_root)
|
||||
dummy_seg = RefugeSegmentation(pre)
|
||||
backbone = build_backbone(args.backbone)
|
||||
clf = RefugeClassification(
|
||||
pre,
|
||||
dummy_seg,
|
||||
geometry_fn=geometry_provider,
|
||||
cache_dir=cache_dir,
|
||||
backbone=backbone,
|
||||
)
|
||||
clf.crop_scale = args.crop_scale
|
||||
clf.crop_size = args.crop_size
|
||||
clf.eval_transform = _default_image_transform(args.crop_size)
|
||||
clf.ttt_transform = clf.eval_transform
|
||||
|
||||
if args.classifier_weights is not None:
|
||||
clf_state = torch.load(args.classifier_weights, map_location=device)
|
||||
clf.backbone.load_state_dict(clf_state["backbone"])
|
||||
clf.classifier_head.load_state_dict(clf_state["classifier"])
|
||||
clf.rotation_head.load_state_dict(clf_state["rotation"])
|
||||
if "feature_reg" in clf_state and getattr(clf, "feature_reg_head", None) is not None:
|
||||
clf.feature_reg_head.load_state_dict(clf_state["feature_reg"])
|
||||
|
||||
records = clf.build_records_for_samples(
|
||||
papila_samples,
|
||||
crop_scale=args.crop_scale,
|
||||
progress_prefix="papila_eval",
|
||||
)
|
||||
if not records:
|
||||
raise SystemExit("Unable to build any records; check geometry predictions or labels.")
|
||||
|
||||
metrics = evaluate_records(clf, records, device=device, batch_size=args.batch_size)
|
||||
print(f"Samples evaluated: {int(metrics['count'])}")
|
||||
print(f"AUC: {metrics['auc']:.4f}" if not np.isnan(metrics['auc']) else "AUC: NaN")
|
||||
print(f"Accuracy @0.5: {metrics['accuracy']:.4f}")
|
||||
print(f"Mean glaucoma prob: {metrics['mean_prob']:.4f}")
|
||||
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with args.output.open("w", newline="") as fp:
|
||||
writer = csv.writer(fp)
|
||||
writer.writerow(["sample_id", "prob_glaucoma", "label"])
|
||||
clf.backbone.eval()
|
||||
clf.classifier_head.eval()
|
||||
dataset = RefugeClassificationDataset(
|
||||
records,
|
||||
transform=clf.eval_transform,
|
||||
polar_transform=clf.polar_transform,
|
||||
size=clf.crop_size,
|
||||
)
|
||||
loader = DataLoader(dataset, batch_size=args.batch_size, shuffle=False, num_workers=0)
|
||||
with torch.no_grad():
|
||||
for batch in tqdm(loader, desc="Papila Output", leave=False, unit="batch"):
|
||||
images = batch["image"].to(device)
|
||||
polars = batch["polar"].to(device)
|
||||
extra_feats = batch["features"].to(device)
|
||||
ids = batch["sample_id"]
|
||||
labels = batch["label"].tolist()
|
||||
feats_img = clf.backbone(images)
|
||||
feats = feats_img
|
||||
if clf.use_polar:
|
||||
feats_polar = clf.backbone(polars)
|
||||
feats = torch.cat([feats, feats_polar], dim=1)
|
||||
if clf.extra_feature_dim > 0:
|
||||
feats = torch.cat([feats, extra_feats], dim=1)
|
||||
logits = clf.classifier_head(feats)
|
||||
probs = torch.softmax(logits, dim=1)[:, 1].cpu().numpy().tolist()
|
||||
for sid, prob, label in zip(ids, probs, labels):
|
||||
writer.writerow([sid, prob, label])
|
||||
print(f"Per-sample probabilities written to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick utility to recover the best epoch metrics from HyperTower run folders.
|
||||
|
||||
Example:
|
||||
python scripts/extract_best_auc.py analysis_data/img_only_densenet_gt_bin/img_only_densenet_gt_bin_20251028_112733
|
||||
|
||||
By default it looks for columns named like `auc_fused` (set via --metric) inside each
|
||||
`fold{n}_epoch_log.csv`, returning the epoch with the highest value plus the holdout
|
||||
metrics, if present.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
|
||||
def to_float(value: Optional[str]) -> Optional[float]:
|
||||
if value is None:
|
||||
return None
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
out = float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
if math.isnan(out):
|
||||
return None
|
||||
return out
|
||||
|
||||
|
||||
def best_row(path: Path, metric: str) -> Optional[Dict[str, str]]:
|
||||
if not path.exists():
|
||||
return None
|
||||
best: Optional[Tuple[float, int, Dict[str, str]]] = None
|
||||
with path.open("r", newline="") as fp:
|
||||
reader = csv.DictReader(fp)
|
||||
for row in reader:
|
||||
val = to_float(row.get(metric))
|
||||
if val is None:
|
||||
continue
|
||||
epoch = int(row.get("epoch", reader.line_num))
|
||||
if best is None or val > best[0]:
|
||||
best = (val, epoch, row)
|
||||
return best[2] if best else None
|
||||
|
||||
|
||||
def summarize_fold(row: Dict[str, str], metric: str) -> Dict[str, float]:
|
||||
data: Dict[str, float] = {}
|
||||
for key in (metric, f"holdout_{metric.split('_', 1)[-1]}", "holdout_auc_img", "holdout_auc_fused"):
|
||||
val = to_float(row.get(key))
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
epoch_val = to_float(row.get("epoch"))
|
||||
if epoch_val is not None:
|
||||
data["epoch"] = int(epoch_val)
|
||||
return data
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Extract best-per-fold metric from HyperTower runs.")
|
||||
ap.add_argument("run_dir", type=Path, help="Run directory (contains fold*_epoch_log.csv)")
|
||||
ap.add_argument("--metric", default="auc_fused", help="Metric column to maximise (default: auc_fused)")
|
||||
ap.add_argument("--json", type=Path, default=None, help="Optional path to dump JSON summary")
|
||||
args = ap.parse_args()
|
||||
|
||||
run_dir: Path = args.run_dir
|
||||
metric: str = args.metric
|
||||
|
||||
if not run_dir.exists():
|
||||
raise SystemExit(f"Run directory not found: {run_dir}")
|
||||
|
||||
fold_summaries: Dict[str, Dict[str, float]] = {}
|
||||
metric_values = []
|
||||
|
||||
for csv_path in sorted(run_dir.glob("fold*_epoch_log.csv")):
|
||||
best = best_row(csv_path, metric)
|
||||
fold_name = csv_path.stem.replace("_epoch_log", "")
|
||||
if best is None:
|
||||
print(f"{fold_name}: no valid '{metric}' values found")
|
||||
continue
|
||||
summary = summarize_fold(best, metric)
|
||||
fold_summaries[fold_name] = summary
|
||||
val = summary.get(metric)
|
||||
if val is not None:
|
||||
metric_values.append(val)
|
||||
holdout_val = summary.get(f"holdout_{metric.split('_', 1)[-1]}")
|
||||
print(f"{fold_name}: epoch={summary.get('epoch')} {metric}={val:.4f}" if val is not None else f"{fold_name}: epoch={summary.get('epoch')}")
|
||||
if holdout_val is not None:
|
||||
print(f" holdout_{metric.split('_', 1)[-1]}={holdout_val:.4f}")
|
||||
|
||||
if metric_values:
|
||||
mean_val = sum(metric_values) / len(metric_values)
|
||||
print(f"\nMean best {metric}: {mean_val:.4f}")
|
||||
|
||||
if args.json:
|
||||
payload = {
|
||||
"run_dir": str(run_dir),
|
||||
"metric": metric,
|
||||
"folds": fold_summaries,
|
||||
"mean_metric": (sum(metric_values) / len(metric_values)) if metric_values else None,
|
||||
}
|
||||
args.json.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json.write_text(json.dumps(payload, indent=2))
|
||||
print(f"Summary written to {args.json}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,493 +0,0 @@
|
||||
|
||||
import pandas as pd
|
||||
from classes import HyperTower, ClinicalData, list_names, build_papila_clinical
|
||||
from pathlib import Path
|
||||
import shutil, json, textwrap
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
from typing import Dict, List, Tuple
|
||||
from sklearn.model_selection import GroupKFold
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.metrics import roc_auc_score
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.svm import SVC
|
||||
|
||||
from sklearn.preprocessing import label_binarize
|
||||
|
||||
def _proba_from_model(model, X):
|
||||
if hasattr(model[-1], "predict_proba"):
|
||||
return model.predict_proba(X)
|
||||
dec = model.decision_function(X)
|
||||
if dec.ndim == 1: # binary margins -> make 2-col
|
||||
dec = np.stack([-dec, dec], axis=1)
|
||||
e = np.exp(dec - dec.max(axis=1, keepdims=True))
|
||||
return e / e.sum(axis=1, keepdims=True)
|
||||
|
||||
def _cv_auc_multiclass_per_class(X, y, groups, model, n_splits=5) -> np.ndarray:
|
||||
"""
|
||||
Returns a length-3 array of mean OvR AUCs for Class0/1/2 across GroupKFold.
|
||||
Uses nan-safe means if a class is absent in a fold's test split.
|
||||
"""
|
||||
gkf = GroupKFold(n_splits=n_splits)
|
||||
per_class_lists = [[], [], []]
|
||||
for tr, te in gkf.split(X, y, groups):
|
||||
model.fit(X[tr], y[tr])
|
||||
proba = _proba_from_model(model, X[te])
|
||||
y_te = y[te]
|
||||
y_bin = label_binarize(y_te, classes=[0, 1, 2]) # (n,3)
|
||||
for k in range(3):
|
||||
yk = y_bin[:, k]
|
||||
if yk.min() != yk.max(): # both classes present
|
||||
per_class_lists[k].append(roc_auc_score(yk, proba[:, k]))
|
||||
else:
|
||||
per_class_lists[k].append(np.nan)
|
||||
return np.array([np.nanmean(per_class_lists[k]) for k in range(3)], dtype=float)
|
||||
|
||||
def _cv_auc_binary(X, y, groups, model, n_splits=5) -> float:
|
||||
mask = np.isin(y, [0, 1])
|
||||
Xb, yb, gb = X[mask], y[mask], groups[mask]
|
||||
gkf = GroupKFold(n_splits=n_splits)
|
||||
aucs = []
|
||||
for tr, te in gkf.split(Xb, yb, gb):
|
||||
model.fit(Xb[tr], yb[tr])
|
||||
if hasattr(model[-1], "predict_proba"):
|
||||
p = model.predict_proba(Xb[te])[:, 1]
|
||||
else:
|
||||
p = model.decision_function(Xb[te])
|
||||
# logistic squash for safety
|
||||
if np.ptp(p) > 0:
|
||||
p = 1.0 / (1.0 + np.exp(-p))
|
||||
else:
|
||||
p = np.full_like(p, 0.5, dtype=float)
|
||||
# only compute if both classes present
|
||||
if len(np.unique(yb[te])) == 2:
|
||||
aucs.append(roc_auc_score(yb[te], p))
|
||||
else:
|
||||
aucs.append(np.nan)
|
||||
return float(np.nanmean(aucs))
|
||||
|
||||
|
||||
|
||||
# -----------------------------------
|
||||
# 1) Build Clinical Data (paper-faithful)
|
||||
# -----------------------------------
|
||||
IMAGE_DIR = "Papila/FundusImages"
|
||||
CLINICAL_DIR = "Papila/ClinicalData"
|
||||
LABEL_COL = "Diagnosis"
|
||||
CAT_COLS = ["Gender", "Phakic/Pseudophakic"]
|
||||
|
||||
paper_auc = {
|
||||
"TEST3_multiclass": { # Class0=Healthy, Class1=Glaucoma, Class2=Suspect
|
||||
"LogReg": {"Class0": 0.67, "Class1": 0.66, "Class2": 0.67}, # from Fig. 7 (rounded)
|
||||
"kNN": {"Class0": 0.72, "Class1": 0.70, "Class2": 0.76}, # your read of Fig. 7
|
||||
"RF": {"Class0": 0.66, "Class1": 0.66, "Class2": 0.67}, # from Fig. 7 (rounded)
|
||||
"SVM": {"Class0": 0.66, "Class1": 0.65, "Class2": 0.66}, # from Fig. 7 (rounded)
|
||||
},
|
||||
"TEST4_binary": { # Healthy vs Glaucoma (Suspects removed)
|
||||
"LogReg": 0.71, # from text/Fig. 7 range midpoint
|
||||
"kNN": 0.75, # your read of Fig. 7
|
||||
"RF": 0.70, # from Fig. 7 (rounded)
|
||||
"SVM": 0.69, # from Fig. 7 (rounded)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
clinical = build_papila_clinical(
|
||||
image_dir=IMAGE_DIR,
|
||||
clinical_dir=CLINICAL_DIR,
|
||||
label_col=LABEL_COL,
|
||||
cat_cols=CAT_COLS,
|
||||
)
|
||||
|
||||
# -----------------------------------
|
||||
# 2) Feature matrix (no MD; IOP_corr already present)
|
||||
# -----------------------------------
|
||||
def build_feature_matrix(clinical) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[str]]:
|
||||
"""
|
||||
Returns:
|
||||
X: features (N x D)
|
||||
y: labels (Diagnosis: 0 healthy, 1 glaucoma, 2 suspect)
|
||||
groups: patient IDs for GroupKFold
|
||||
feat_names: list of feature names in X order
|
||||
"""
|
||||
df = clinical.df.copy()
|
||||
|
||||
# Scalars used in paper-style baselines (no VF_MD)
|
||||
scalars = ["Age", "dioptre_1", "dioptre_2", "astigmatism",
|
||||
"Pachymetry", "Axial_Length", "IOP_corr"]
|
||||
|
||||
# Categorical one-hot
|
||||
cats = ["Gender", "Phakic/Pseudophakic"]
|
||||
df_cats = pd.get_dummies(df[cats].astype("category"), drop_first=False, prefix=cats)
|
||||
|
||||
# Combine
|
||||
X = pd.concat([df[scalars], df_cats], axis=1)
|
||||
|
||||
# Median impute numerics (simple, consistent)
|
||||
for c in scalars:
|
||||
med = pd.to_numeric(X[c], errors="coerce").median()
|
||||
X[c] = pd.to_numeric(X[c], errors="coerce").fillna(med)
|
||||
|
||||
y = df[LABEL_COL].astype(int).values
|
||||
groups = df["Patient ID"].astype(int).values
|
||||
feat_names = list(X.columns)
|
||||
return X.values.astype(np.float32), y, groups, feat_names
|
||||
|
||||
# ----------------------------
|
||||
# 3) Model zoo (the four methods used in the paper)
|
||||
# ----------------------------
|
||||
def make_models(best_params: dict | None = None, random_state: int = 42) -> dict:
|
||||
"""
|
||||
Build paper-like baseline models. If best_params is provided (a dict mapping
|
||||
model-name -> param dict with pipeline-style keys like 'clf__C'), those
|
||||
params are applied to the corresponding pipelines.
|
||||
"""
|
||||
models = {
|
||||
"LogReg": Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", LogisticRegression(
|
||||
max_iter=100,
|
||||
solver="lbfgs",
|
||||
multi_class="auto"
|
||||
))
|
||||
]),
|
||||
"kNN": Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", KNeighborsClassifier(
|
||||
n_neighbors=5,
|
||||
weights="uniform",
|
||||
metric="minkowski",
|
||||
p=2
|
||||
))
|
||||
]),
|
||||
"RF": Pipeline([
|
||||
("clf", RandomForestClassifier(
|
||||
n_estimators=100,
|
||||
criterion="gini",
|
||||
max_depth=None,
|
||||
min_samples_split=2,
|
||||
min_samples_leaf=1,
|
||||
max_features="sqrt",
|
||||
bootstrap=True,
|
||||
# random_state left as default; set via best_params if desired
|
||||
))
|
||||
]),
|
||||
"SVM": Pipeline([
|
||||
("scaler", StandardScaler()),
|
||||
("clf", SVC(
|
||||
C=1.0,
|
||||
kernel="rbf",
|
||||
gamma="scale",
|
||||
probability=False
|
||||
))
|
||||
]),
|
||||
}
|
||||
|
||||
# Apply overrides if provided
|
||||
if best_params:
|
||||
for name, params in best_params.items():
|
||||
if name in models and params:
|
||||
models[name].set_params(**params)
|
||||
|
||||
return models
|
||||
|
||||
|
||||
# -----------------------------------
|
||||
# 4) CV AUCs (mean over 5 folds; GroupKFold by patient)
|
||||
# -----------------------------------
|
||||
def _cv_auc_multiclass(X, y, groups, model, n_splits=5) -> float:
|
||||
gkf = GroupKFold(n_splits=n_splits)
|
||||
aucs = []
|
||||
for tr, te in gkf.split(X, y, groups):
|
||||
model.fit(X[tr], y[tr])
|
||||
if hasattr(model[-1], "predict_proba"):
|
||||
proba = model.predict_proba(X[te])
|
||||
else:
|
||||
dec = model.decision_function(X[te])
|
||||
if dec.ndim == 1:
|
||||
dec = np.stack([-dec, dec], axis=1)
|
||||
e = np.exp(dec - dec.max(axis=1, keepdims=True))
|
||||
proba = e / e.sum(axis=1, keepdims=True)
|
||||
aucs.append(roc_auc_score(y[te], proba, multi_class="ovr", average="macro"))
|
||||
return float(np.mean(aucs))
|
||||
|
||||
|
||||
def _cv_auc_binary(X, y, groups, model, n_splits=5) -> float:
|
||||
# Keep classes 0 (healthy) and 1 (glaucoma); drop suspects (2)
|
||||
mask = np.isin(y, [0, 1])
|
||||
Xb, yb, gb = X[mask], y[mask], groups[mask]
|
||||
|
||||
gkf = GroupKFold(n_splits=n_splits)
|
||||
aucs = []
|
||||
for tr, te in gkf.split(Xb, yb, gb):
|
||||
model.fit(Xb[tr], yb[tr])
|
||||
if hasattr(model[-1], "predict_proba"):
|
||||
p = model.predict_proba(Xb[te])[:, 1]
|
||||
else:
|
||||
p = model.decision_function(Xb[te])
|
||||
# simple logistic squashing if needed
|
||||
if np.ptp(p) > 0:
|
||||
p = 1.0 / (1.0 + np.exp(-p))
|
||||
else:
|
||||
p = np.full_like(p, 0.5, dtype=float)
|
||||
aucs.append(roc_auc_score(yb[te], p))
|
||||
return float(np.mean(aucs))
|
||||
|
||||
# -----------------------------------
|
||||
# 5) Run both tests (multiclass + binary) and print table
|
||||
# -----------------------------------
|
||||
def run_papila_clinical_baselines(clinical, n_splits: int = 5,
|
||||
random_state: int = 42,
|
||||
best_params: dict | None = None) -> pd.DataFrame:
|
||||
X, y, groups, feat_names = build_feature_matrix(clinical)
|
||||
models = make_models(best_params=best_params, random_state=random_state)
|
||||
|
||||
rows = []
|
||||
for name, model in models.items():
|
||||
c0, c1, c2 = _cv_auc_multiclass_per_class(X, y, groups, model, n_splits=n_splits)
|
||||
auc_bin = _cv_auc_binary(X, y, groups, model, n_splits=n_splits)
|
||||
rows.append({"model": name, "Class0": c0, "Class1": c1, "Class2": c2, "Binary": auc_bin})
|
||||
|
||||
df = pd.DataFrame(rows).set_index("model").sort_index()
|
||||
return df
|
||||
|
||||
|
||||
results = run_papila_clinical_baselines(clinical, n_splits=5)
|
||||
# print(results.to_string(float_format=lambda x: f"{x:.3f}"))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
##############################
|
||||
from sklearn.model_selection import ParameterGrid
|
||||
from sklearn.base import clone
|
||||
from sklearn.preprocessing import label_binarize
|
||||
from sklearn.utils import check_random_state
|
||||
|
||||
# ==============================
|
||||
# Helper: per-class & binary AUC with GroupKFold
|
||||
# ==============================
|
||||
def _proba_from_model(model, X):
|
||||
if hasattr(model[-1], "predict_proba"):
|
||||
return model.predict_proba(X)
|
||||
# decision_function fallback
|
||||
dec = model.decision_function(X)
|
||||
if dec.ndim == 1: # binary margin -> 2-col probs
|
||||
dec = np.stack([-dec, dec], axis=1)
|
||||
e = np.exp(dec - dec.max(axis=1, keepdims=True))
|
||||
return e / e.sum(axis=1, keepdims=True)
|
||||
|
||||
def _cv_auc_perclass_and_binary(X, y, groups, model, n_splits=5):
|
||||
"""
|
||||
Returns:
|
||||
per_class_auc: length-3 array (Class0, Class1, Class2) averaged over folds
|
||||
binary_auc: scalar (0 vs 1) averaged over folds
|
||||
"""
|
||||
gkf = GroupKFold(n_splits=n_splits)
|
||||
|
||||
# Hold fold-wise per-class AUCs (list of arrays of length 3)
|
||||
perclass_fold_scores = []
|
||||
binary_fold_scores = []
|
||||
|
||||
for tr, te in gkf.split(X, y, groups):
|
||||
y_te = y[te]
|
||||
# Multiclass per-class (OvR)
|
||||
model.fit(X[tr], y[tr])
|
||||
proba = _proba_from_model(model, X[te])
|
||||
|
||||
# One-vs-rest per-class AUCs (skip a class if absent in test fold)
|
||||
y_bin = label_binarize(y_te, classes=[0, 1, 2]) # shape (n, 3)
|
||||
perclass_scores = []
|
||||
for k in range(3):
|
||||
yk = y_bin[:, k]
|
||||
# Only compute if both 0 and 1 are present
|
||||
if yk.min() != yk.max():
|
||||
perclass_scores.append(roc_auc_score(yk, proba[:, k]))
|
||||
else:
|
||||
perclass_scores.append(np.nan)
|
||||
perclass_fold_scores.append(perclass_scores)
|
||||
|
||||
# Binary AUC (0 vs 1; drop class 2)
|
||||
mask = np.isin(y_te, [0, 1])
|
||||
if mask.sum() > 0 and len(np.unique(y_te[mask])) == 2:
|
||||
# we need probabilities/margins for class 1 among (0,1)
|
||||
# Map proba[:, 1] if the model was trained 3-way; we restrict te samples to 0/1
|
||||
binary_p = proba[mask, 1]
|
||||
binary_y = y_te[mask]
|
||||
binary_fold_scores.append(roc_auc_score(binary_y, binary_p))
|
||||
else:
|
||||
binary_fold_scores.append(np.nan)
|
||||
|
||||
# Average over folds (ignore NaNs if a class was missing in a fold)
|
||||
perclass_arr = np.array(perclass_fold_scores, dtype=float) # (n_folds, 3)
|
||||
per_class_auc = np.nanmean(perclass_arr, axis=0)
|
||||
binary_auc = float(np.nanmean(np.array(binary_fold_scores, dtype=float)))
|
||||
return per_class_auc, binary_auc
|
||||
|
||||
# ==============================
|
||||
# Distance-to-paper objective
|
||||
# ==============================
|
||||
def _distance_to_paper(model_name: str,
|
||||
per_class_auc: np.ndarray,
|
||||
binary_auc: float,
|
||||
paper_auc: Dict,
|
||||
w_mc: float = 1.0,
|
||||
w_bin: float = 1.0) -> float:
|
||||
mc_targets = paper_auc["TEST3_multiclass"][model_name]
|
||||
tvec = np.array([mc_targets["Class0"], mc_targets["Class1"], mc_targets["Class2"]], dtype=float)
|
||||
mc_diff = np.nanmean(np.abs(per_class_auc - tvec)) # mean absolute difference over 3 classes
|
||||
|
||||
bin_target = paper_auc["TEST4_binary"][model_name]
|
||||
bin_diff = abs(binary_auc - bin_target)
|
||||
|
||||
return float(w_mc * mc_diff + w_bin * bin_diff)
|
||||
|
||||
# ==============================
|
||||
# Parameter grids (paper-ish, not crazy-large)
|
||||
# ==============================
|
||||
def get_param_grids() -> Dict[str, List[dict]]:
|
||||
return {
|
||||
"LogReg": [
|
||||
{
|
||||
"clf__C": [0.01, 0.1, 1.0, 3.0, 10.0],
|
||||
"clf__class_weight": [None, "balanced"],
|
||||
"clf__max_iter": [200, 500],
|
||||
# lbfgs + l2 is implied
|
||||
}
|
||||
],
|
||||
"kNN": [
|
||||
{
|
||||
"clf__n_neighbors": [3, 5, 7, 9, 11],
|
||||
"clf__weights": ["uniform", "distance"],
|
||||
"clf__p": [1, 2], # Manhattan vs Euclidean
|
||||
}
|
||||
],
|
||||
"RF": [
|
||||
{
|
||||
"clf__n_estimators": [200, 500, 1000],
|
||||
"clf__max_depth": [None, 5, 10, 20],
|
||||
"clf__max_features": ["sqrt", "log2", 0.5],
|
||||
"clf__min_samples_leaf": [1, 2, 5],
|
||||
"clf__class_weight": [None, "balanced"],
|
||||
# If you want determinism add: "clf__random_state": [42]
|
||||
}
|
||||
],
|
||||
"SVM": [
|
||||
{
|
||||
"clf__C": [0.1, 1.0, 3.0, 10.0],
|
||||
"clf__gamma": ["scale", "auto", 0.1, 0.01, 0.001],
|
||||
"clf__kernel": ["rbf"], # fixed to rbf as in paper-like default
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# ==============================
|
||||
# Grid search loop minimizing distance-to-paper
|
||||
# ==============================
|
||||
def search_params_to_match_paper(
|
||||
clinical,
|
||||
models: Dict[str, Pipeline],
|
||||
paper_auc: Dict,
|
||||
n_splits: int = 5,
|
||||
w_mc: float = 1.0,
|
||||
w_bin: float = 1.0,
|
||||
verbose: bool = True,
|
||||
) -> Tuple[pd.DataFrame, Dict[str, dict]]:
|
||||
X, y, groups, feat_names = build_feature_matrix(clinical)
|
||||
grids = get_param_grids()
|
||||
|
||||
summary_rows = []
|
||||
best_params_by_model = {}
|
||||
|
||||
for name, base_model in models.items():
|
||||
if name not in grids:
|
||||
if verbose:
|
||||
print(f"[warn] No grid for {name}, skipping.")
|
||||
continue
|
||||
|
||||
best_loss = np.inf
|
||||
best_params = None
|
||||
best_mc = None
|
||||
best_bin = None
|
||||
|
||||
for param_set in ParameterGrid(grids[name]):
|
||||
model = clone(base_model).set_params(**param_set)
|
||||
per_class_auc, binary_auc = _cv_auc_perclass_and_binary(
|
||||
X, y, groups, model, n_splits=n_splits
|
||||
)
|
||||
loss = _distance_to_paper(
|
||||
name, per_class_auc, binary_auc, paper_auc, w_mc=w_mc, w_bin=w_bin
|
||||
)
|
||||
|
||||
if verbose:
|
||||
mc_str = " / ".join(f"{a:.3f}" if np.isfinite(a) else "nan" for a in per_class_auc)
|
||||
print(f"[{name}] params={param_set} | mc per-class={mc_str} | bin={binary_auc:.3f} | loss={loss:.4f}")
|
||||
|
||||
if loss < best_loss:
|
||||
best_loss = loss
|
||||
best_params = param_set
|
||||
best_mc = per_class_auc
|
||||
best_bin = binary_auc
|
||||
|
||||
# store
|
||||
best_params_by_model[name] = best_params
|
||||
summary_rows.append({
|
||||
"model": name,
|
||||
"best_loss": best_loss,
|
||||
"best_params": json.dumps(best_params),
|
||||
"mc_Class0": float(best_mc[0]),
|
||||
"mc_Class1": float(best_mc[1]),
|
||||
"mc_Class2": float(best_mc[2]),
|
||||
"binary_auc": float(best_bin),
|
||||
"paper_mc_Class0": paper_auc["TEST3_multiclass"][name]["Class0"],
|
||||
"paper_mc_Class1": paper_auc["TEST3_multiclass"][name]["Class1"],
|
||||
"paper_mc_Class2": paper_auc["TEST3_multiclass"][name]["Class2"],
|
||||
"paper_binary": paper_auc["TEST4_binary"][name],
|
||||
})
|
||||
|
||||
df = pd.DataFrame(summary_rows).set_index("model").sort_values("best_loss")
|
||||
return df, best_params_by_model
|
||||
|
||||
# ==============================
|
||||
# Run the search
|
||||
# ==============================
|
||||
models = make_models(random_state=42)
|
||||
df_match, best_params = search_params_to_match_paper(
|
||||
clinical=clinical,
|
||||
models=models,
|
||||
paper_auc=paper_auc,
|
||||
n_splits=5,
|
||||
w_mc=1.0, # weight multiclass distance
|
||||
w_bin=1.0, # weight binary distance
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# print("\n=== Best params found (by minimal distance-to-paper) ===")
|
||||
# print(df_match[["best_loss","best_params","mc_Class0","mc_Class1","mc_Class2","binary_auc",
|
||||
# "paper_mc_Class0","paper_mc_Class1","paper_mc_Class2","paper_binary"]])
|
||||
|
||||
# print("\nBest param dicts:")
|
||||
for k, v in best_params.items():
|
||||
print(k, "->", v)
|
||||
|
||||
results2 = run_papila_clinical_baselines(clinical, n_splits=5, random_state=42, best_params=best_params)
|
||||
print(f"Default Settings: {results.round(2)}")
|
||||
print(f"Best Params Settings: {results2.round(2)}")
|
||||
print(f" Paper Results: {pd.DataFrame({
|
||||
model: {**vals, "Binary": paper_auc["TEST4_binary"][model]}
|
||||
for model, vals in paper_auc["TEST3_multiclass"].items()
|
||||
}).T[["Class0","Class1","Class2","Binary"]]}")
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Usage:
|
||||
# bash scripts/run_all_sweep.sh --epochs 25 --n-splits 5 --batch-size 8 [extra args]
|
||||
#
|
||||
# Merged sweep: runs the SE attention grid (bridge/tower/both × R=8/16/32),
|
||||
# skipping tower-only non-normalized variants (tower normalization is a no-op),
|
||||
# and includes binary eval counterparts for each baseline run. It also submits
|
||||
# the full gradual-thaw grid (multiclass + binary variants).
|
||||
|
||||
ARGS=("$@")
|
||||
|
||||
run() {
|
||||
local SHORT="$1"; shift
|
||||
echo "=== Running: $SHORT ==="
|
||||
# Skip if a summary for this shortname already exists
|
||||
if ls "analysis_data/${SHORT}_"*.md >/dev/null 2>&1; then
|
||||
echo "… skipping ${SHORT} (summary already present)"
|
||||
return 0
|
||||
fi
|
||||
python3 scripts/run_multifold.py \
|
||||
--shortname "$SHORT" \
|
||||
"$@" \
|
||||
"${ARGS[@]}" || true
|
||||
}
|
||||
|
||||
echo "--- SE Grid (bridge/tower/both × R=8/16/32; tower nonorm skipped) ---"
|
||||
for R in 8 16 32; do
|
||||
# Bridge-only
|
||||
run "se_bridge_R${R}_norm" --se-where bridge --se-reduction ${R} --se-pre-norm --checkpoint-best
|
||||
run "se_bridge_R${R}_norm_bin" --se-where bridge --se-reduction ${R} --se-pre-norm --checkpoint-best --eval_mode binary
|
||||
run "se_bridge_R${R}_nonorm" --se-where bridge --se-reduction ${R} --no-se-pre-norm --checkpoint-best
|
||||
run "se_bridge_R${R}_nonorm_bin" --se-where bridge --se-reduction ${R} --no-se-pre-norm --checkpoint-best --eval_mode binary
|
||||
|
||||
# Tower-only
|
||||
run "se_tower_R${R}_norm" --se-where tower --se-reduction-tower ${R} --se-pre-norm-tower --checkpoint-best
|
||||
run "se_tower_R${R}_norm_bin" --se-where tower --se-reduction-tower ${R} --se-pre-norm-tower --checkpoint-best --eval_mode binary
|
||||
|
||||
# Tower+Bridge
|
||||
run "se_tower_bridge_R${R}_norm" \
|
||||
--se-where both --se-reduction ${R} --se-reduction-tower ${R} \
|
||||
--se-pre-norm --se-pre-norm-tower --checkpoint-best
|
||||
run "se_tower_bridge_R${R}_norm_bin" \
|
||||
--se-where both --se-reduction ${R} --se-reduction-tower ${R} \
|
||||
--se-pre-norm --se-pre-norm-tower --checkpoint-best --eval_mode binary
|
||||
run "se_tower_bridge_R${R}_nonorm" \
|
||||
--se-where both --se-reduction ${R} --se-reduction-tower ${R} \
|
||||
--no-se-pre-norm --no-se-pre-norm-tower --checkpoint-best
|
||||
run "se_tower_bridge_R${R}_nonorm_bin" \
|
||||
--se-where both --se-reduction ${R} --se-reduction-tower ${R} \
|
||||
--no-se-pre-norm --no-se-pre-norm-tower --checkpoint-best --eval_mode binary
|
||||
done
|
||||
|
||||
THAW_COMMON_ARGS=(
|
||||
--gradual-thaw
|
||||
--thaw-phase-duration 5
|
||||
--thaw-ratio 0.33
|
||||
--thaw-start-epoch 5
|
||||
--early-stop
|
||||
--early-patience 5
|
||||
)
|
||||
|
||||
echo "--- Gradual Thaw Grid (multiclass + binary) ---"
|
||||
|
||||
# Bridge-only thaw runs (norm and nonorm)
|
||||
for R in 8 16 32; do
|
||||
for MODE in norm nonorm; do
|
||||
if [[ "$MODE" == "norm" ]]; then
|
||||
FLAGS=(--se-where bridge --se-reduction "$R" --se-pre-norm --checkpoint-best)
|
||||
else
|
||||
FLAGS=(--se-where bridge --se-reduction "$R" --no-se-pre-norm --checkpoint-best)
|
||||
fi
|
||||
run "thaw_se_bridge_R${R}_${MODE}" "${FLAGS[@]}" "${THAW_COMMON_ARGS[@]}"
|
||||
run "thawbin_se_bridge_R${R}_${MODE}" "${FLAGS[@]}" "${THAW_COMMON_ARGS[@]}" --eval_mode binary
|
||||
done
|
||||
done
|
||||
|
||||
# Tower-only thaw runs (norm and nonorm)
|
||||
for R in 8 16 32; do
|
||||
for MODE in norm nonorm; do
|
||||
if [[ "$MODE" == "norm" ]]; then
|
||||
FLAGS=(--se-where tower --se-reduction-tower "$R" --se-pre-norm-tower --checkpoint-best)
|
||||
else
|
||||
FLAGS=(--se-where tower --se-reduction-tower "$R" --no-se-pre-norm-tower --checkpoint-best)
|
||||
fi
|
||||
run "thaw_se_tower_R${R}_${MODE}" "${FLAGS[@]}" "${THAW_COMMON_ARGS[@]}"
|
||||
run "thawbin_se_tower_R${R}_${MODE}" "${FLAGS[@]}" "${THAW_COMMON_ARGS[@]}" --eval_mode binary
|
||||
done
|
||||
done
|
||||
|
||||
# Tower+bridge thaw runs (norm and nonorm)
|
||||
for R in 8 16 32; do
|
||||
for MODE in norm nonorm; do
|
||||
if [[ "$MODE" == "norm" ]]; then
|
||||
FLAGS=(
|
||||
--se-where both
|
||||
--se-reduction "$R"
|
||||
--se-reduction-tower "$R"
|
||||
--se-pre-norm
|
||||
--se-pre-norm-tower
|
||||
--checkpoint-best
|
||||
)
|
||||
else
|
||||
FLAGS=(
|
||||
--se-where both
|
||||
--se-reduction "$R"
|
||||
--se-reduction-tower "$R"
|
||||
--no-se-pre-norm
|
||||
--no-se-pre-norm-tower
|
||||
--checkpoint-best
|
||||
)
|
||||
fi
|
||||
run "thaw_se_tower_bridge_R${R}_${MODE}" "${FLAGS[@]}" "${THAW_COMMON_ARGS[@]}"
|
||||
run "thawbin_se_tower_bridge_R${R}_${MODE}" "${FLAGS[@]}" "${THAW_COMMON_ARGS[@]}" --eval_mode binary
|
||||
done
|
||||
done
|
||||
|
||||
echo "Merged sweep submitted. Check analysis_data/* and models/* for outputs."
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Usage:
|
||||
# bash scripts/run_gradual_thaw_top5.sh --epochs 20 --n-splits 5 --batch-size 8 [extra args]
|
||||
#
|
||||
# Runs the full gradual-thaw grid aligned with the SE sweep (bridge/tower/both × R=8/16/32 × norm vs nonorm).
|
||||
|
||||
ARGS=("$@")
|
||||
|
||||
run() {
|
||||
local SHORT="$1"; shift
|
||||
echo "=== Running: $SHORT ==="
|
||||
if ls "analysis_data/${SHORT}_"*.md >/dev/null 2>&1; then
|
||||
echo "… skipping ${SHORT} (summary already present)"
|
||||
return 0
|
||||
fi
|
||||
python3 scripts/run_multifold.py \
|
||||
--shortname "$SHORT" \
|
||||
--gradual-thaw --thaw-phase-duration 5 --thaw-ratio 0.33 --thaw-start-epoch 5 \
|
||||
--early-stop --early-patience 5 \
|
||||
"$@" \
|
||||
"${ARGS[@]}" || true
|
||||
}
|
||||
|
||||
# Bridge-only thaw runs
|
||||
for R in 8 16 32; do
|
||||
for MODE in norm nonorm; do
|
||||
if [[ "$MODE" == "norm" ]]; then
|
||||
FLAGS=(--se-where bridge --se-reduction "$R" --se-pre-norm --checkpoint-best)
|
||||
else
|
||||
FLAGS=(--se-where bridge --se-reduction "$R" --no-se-pre-norm --checkpoint-best)
|
||||
fi
|
||||
run "thaw_se_bridge_R${R}_${MODE}" "${FLAGS[@]}"
|
||||
run "thawbin_se_bridge_R${R}_${MODE}" "${FLAGS[@]}" --eval_mode binary
|
||||
done
|
||||
done
|
||||
|
||||
# Tower-only thaw runs
|
||||
for R in 8 16 32; do
|
||||
for MODE in norm nonorm; do
|
||||
if [[ "$MODE" == "norm" ]]; then
|
||||
FLAGS=(--se-where tower --se-reduction-tower "$R" --se-pre-norm-tower --checkpoint-best)
|
||||
else
|
||||
FLAGS=(--se-where tower --se-reduction-tower "$R" --no-se-pre-norm-tower --checkpoint-best)
|
||||
fi
|
||||
run "thaw_se_tower_R${R}_${MODE}" "${FLAGS[@]}"
|
||||
run "thawbin_se_tower_R${R}_${MODE}" "${FLAGS[@]}" --eval_mode binary
|
||||
done
|
||||
done
|
||||
|
||||
# Tower+bridge thaw runs
|
||||
for R in 8 16 32; do
|
||||
for MODE in norm nonorm; do
|
||||
if [[ "$MODE" == "norm" ]]; then
|
||||
FLAGS=(
|
||||
--se-where both
|
||||
--se-reduction "$R"
|
||||
--se-reduction-tower "$R"
|
||||
--se-pre-norm
|
||||
--se-pre-norm-tower
|
||||
--checkpoint-best
|
||||
)
|
||||
else
|
||||
FLAGS=(
|
||||
--se-where both
|
||||
--se-reduction "$R"
|
||||
--se-reduction-tower "$R"
|
||||
--no-se-pre-norm
|
||||
--no-se-pre-norm-tower
|
||||
--checkpoint-best
|
||||
)
|
||||
fi
|
||||
run "thaw_se_tower_bridge_R${R}_${MODE}" "${FLAGS[@]}"
|
||||
run "thawbin_se_tower_bridge_R${R}_${MODE}" "${FLAGS[@]}" --eval_mode binary
|
||||
done
|
||||
done
|
||||
|
||||
echo "Gradual thaw grid submitted. Check analysis_data/* and models/* for outputs."
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Launch the Tkinter front-end for run_multifold."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from classes.frontend import launch_frontend
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
launch_frontend()
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse, subprocess, sys, time, json
|
||||
from pathlib import Path
|
||||
|
||||
# Backbones in the paper that torchvision supports
|
||||
BACKBONES = [
|
||||
"efficientnet_b0",
|
||||
"resnet50",
|
||||
"densenet121",
|
||||
"vgg16",
|
||||
"mobilenet_v2",
|
||||
"inception_v3",
|
||||
# (Xception omitted; not in torchvision — add via timm later if needed)
|
||||
]
|
||||
|
||||
MODES = [
|
||||
("multiclass", ["Healthy", "Glaucoma", "Suspect"]),
|
||||
("binary", ["Healthy", "Glaucoma"]),
|
||||
]
|
||||
|
||||
def run(cmd):
|
||||
print("\n$ " + " ".join(map(str, cmd)))
|
||||
res = subprocess.run(cmd, check=True)
|
||||
return res.returncode
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Run all paper CNNs across folds in multiclass + binary, then compile plots.")
|
||||
ap.add_argument("--epochs", type=int, default=5, help="Epochs per fold (fast sanity first).")
|
||||
ap.add_argument("--shortname", type=str, default="papergrid", help="Prefix for run IDs.")
|
||||
ap.add_argument("--n-splits", type=int, default=5, help="Number of folds.")
|
||||
ap.add_argument("--fusion-mode", type=str, default="fused", choices=["image_only","fused","metadata_only","vote"],
|
||||
help="Paper CNNs are image-only; leave as image_only unless you’re testing others.")
|
||||
ap.add_argument("--freeze-ratio", type=float, default=0.0, help="0.0 = full fine-tune (as in the paper).")
|
||||
# You can override data roots if needed
|
||||
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("--cat-cols", nargs="*", default=["Gender", "Phakic/Pseudophakic"])
|
||||
args = ap.parse_args()
|
||||
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
master_tag = f"{args.shortname}_{ts}"
|
||||
master_dir = Path("analysis_data") / master_tag
|
||||
master_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Keep a log of all subruns for the master report
|
||||
index = []
|
||||
|
||||
for backbone in BACKBONES:
|
||||
for eval_mode, class_names in MODES:
|
||||
# build a child shortname per (backbone, mode)
|
||||
sub_prefix = f"{args.shortname}_{backbone}_{eval_mode}"
|
||||
cmd = [
|
||||
sys.executable, "scripts/run_multifold.py",
|
||||
"--backbone", backbone,
|
||||
"--freeze-ratio", str(args.freeze_ratio),
|
||||
"--fusion-mode", args.fusion_mode,
|
||||
"--epochs", str(args.epochs),
|
||||
"--n-splits", str(args.n_splits),
|
||||
"--shortname", sub_prefix,
|
||||
"--eval_mode", eval_mode,
|
||||
"--image-dir", args.image_dir,
|
||||
"--clinical-dir", args.clinical_dir,
|
||||
"--label-col", args.label_col,
|
||||
]
|
||||
|
||||
# class names by mode (ensures plot legends are correct)
|
||||
cmd += ["--class-names", *class_names]
|
||||
|
||||
plot_head_map = {
|
||||
"image_only": "image",
|
||||
"fused" : "fused",
|
||||
"metadata_only": "metadata",
|
||||
"vote": "fused",
|
||||
}
|
||||
# We always aggregate/plot the image head for paper CNNs
|
||||
cmd += ["--plot-head", plot_head_map.get(args.fusion_mode)]
|
||||
|
||||
# Delegate the whole run to run_multifold.py
|
||||
run(cmd)
|
||||
|
||||
# Discover the child run folder (the newest folder matching the shortname prefix)
|
||||
# We do this because run_multifold appends its own timestamp.
|
||||
adir = Path("analysis_data")
|
||||
children = sorted([p for p in adir.glob(f"{sub_prefix}_*") if p.is_dir()])
|
||||
if not children:
|
||||
print(f"[WARN] No analysis_data folder found for {sub_prefix}; skipping index entry.")
|
||||
continue
|
||||
run_dir = children[-1]
|
||||
summary_json = run_dir / "summary.json"
|
||||
plots_dir = run_dir / "plots"
|
||||
|
||||
# Record entry
|
||||
entry = {
|
||||
"backbone": backbone,
|
||||
"eval_mode": eval_mode,
|
||||
"run_dir": str(run_dir),
|
||||
"summary_json": str(summary_json) if summary_json.exists() else None,
|
||||
"plots": {
|
||||
"mean": str(plots_dir / "roc_image_mean_ovr.png"),
|
||||
"overlay": str(plots_dir / "roc_image_perfold_overlay.png"),
|
||||
}
|
||||
}
|
||||
# Try to read AUCs
|
||||
try:
|
||||
if summary_json.exists():
|
||||
entry.update(json.loads(summary_json.read_text()))
|
||||
except Exception:
|
||||
pass
|
||||
index.append(entry)
|
||||
|
||||
# Write a master JSON + markdown report
|
||||
(master_dir / "index.json").write_text(json.dumps(index, indent=2), encoding="utf-8")
|
||||
|
||||
# Simple markdown table of results with links
|
||||
lines = [
|
||||
f"# Multimodel grid — {master_tag}",
|
||||
"",
|
||||
f"- Epochs per fold: **{args.epochs}**",
|
||||
f"- Folds: **{args.n_splits}**",
|
||||
f"- Fusion mode: **{args.fusion_mode}** (paper CNNs = image-only)",
|
||||
f"- Freeze ratio: **{args.freeze_ratio}**",
|
||||
"",
|
||||
"| Backbone | Mode | Mean AUC (macro/mc or ROC-AUC/bin) | Plots | Run folder |",
|
||||
"|---|---|---:|---|---|",
|
||||
]
|
||||
for e in index:
|
||||
auc_mean = e.get("macro_ovr_auc_mean", None)
|
||||
if auc_mean is not None:
|
||||
auc_str = f"{auc_mean:.3f}"
|
||||
else:
|
||||
auc_str = "—"
|
||||
mean_png = e["plots"]["mean"]
|
||||
overlay_png = e["plots"]["overlay"]
|
||||
plots_md = f"[mean]({mean_png}) / [overlay]({overlay_png})"
|
||||
lines.append(
|
||||
f"| `{e['backbone']}` | `{e['eval_mode']}` | {auc_str} | {plots_md} | `{e['run_dir']}` |"
|
||||
)
|
||||
(master_dir / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
print(f"\nAll done.\n- Master index: {master_dir/'index.json'}\n- Report: {master_dir/'README.md'}")
|
||||
print(f"- Individual runs live under analysis_data/<shortname_backbone_mode_*> with plots and summaries.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Usage:
|
||||
# bash scripts/run_se_sweep.sh --epochs 50 --n-splits 5 --batch-size 8 --eval_mode multiclass [extra args]
|
||||
#
|
||||
# This will launch a series of runs covering the grid from the slide:
|
||||
# - Bridge-only R8/R16/R32 (normalized and non-normalized)
|
||||
# - Tower-only R8/R16/R32 (normalized and non-normalized)
|
||||
# - Tower+Bridge R8/R16/R32 (normalized and non-normalized)
|
||||
|
||||
ARGS=("$@")
|
||||
|
||||
run() {
|
||||
local SHORT="$1"; shift
|
||||
echo "=== Running: $SHORT ==="
|
||||
# Skip if a summary for this shortname already exists
|
||||
if ls "analysis_data/${SHORT}_"*.md >/dev/null 2>&1; then
|
||||
echo "… skipping ${SHORT} (summary already present)"
|
||||
return 0
|
||||
fi
|
||||
python3 scripts/run_multifold.py \
|
||||
--shortname "$SHORT" \
|
||||
"$@" \
|
||||
"${ARGS[@]}" || true
|
||||
}
|
||||
|
||||
# Bridge-only (normalized + non-normalized)
|
||||
for R in 8 16 32; do
|
||||
run "se_bridge_R${R}_norm" --se-where bridge --se-reduction ${R} --se-pre-norm --checkpoint-best
|
||||
run "se_bridge_R${R}_nonorm" --se-where bridge --se-reduction ${R} --no-se-pre-norm --checkpoint-best
|
||||
done
|
||||
|
||||
# Tower-only (normalized + non-normalized)
|
||||
for R in 8 16 32; do
|
||||
run "se_tower_R${R}_norm" \
|
||||
--se-where tower --se-reduction-tower ${R} --se-pre-norm-tower \
|
||||
--checkpoint-best
|
||||
run "se_tower_R${R}_nonorm" \
|
||||
--se-where tower --se-reduction-tower ${R} --no-se-pre-norm-tower \
|
||||
--checkpoint-best
|
||||
done
|
||||
|
||||
# Tower+Bridge (normalized + non-normalized)
|
||||
for R in 8 16 32; do
|
||||
# normalized (both pre-norm on)
|
||||
run "se_tower_bridge_R${R}_norm" \
|
||||
--se-where both --se-reduction ${R} --se-reduction-tower ${R} \
|
||||
--se-pre-norm --se-pre-norm-tower --checkpoint-best
|
||||
# non-normalized (both pre-norm off)
|
||||
run "se_tower_bridge_R${R}_nonorm" \
|
||||
--se-where both --se-reduction ${R} --se-reduction-tower ${R} \
|
||||
--no-se-pre-norm --no-se-pre-norm-tower --checkpoint-best
|
||||
done
|
||||
|
||||
echo "Sweep submitted. Check analysis_data/* and models/* for outputs."
|
||||
@@ -1287,7 +1287,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 3a) No crop — original full-size images\n",
|
||||
"!python scripts/basic_analysis/compare_hypertower_modes.py \\\n",
|
||||
"!python scripts/main/v2/multirun_hypertower.py \\\n",
|
||||
" --tower-modes single ensemble \\\n",
|
||||
" --eval-modes binary multiclass \\\n",
|
||||
" --epochs 40 --n-splits 5 \\\n",
|
||||
@@ -1305,7 +1305,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 3b) GT crop — expert segmentation masks crop the optic disc region\n",
|
||||
"!python scripts/basic_analysis/compare_hypertower_modes.py \\\n",
|
||||
"!python scripts/main/v2/multirun_hypertower.py \\\n",
|
||||
" --tower-modes single ensemble \\\n",
|
||||
" --eval-modes binary multiclass \\\n",
|
||||
" --epochs 40 --n-splits 5 \\\n",
|
||||
@@ -1323,7 +1323,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 3c) UNet crop — trained segmenter crops the optic disc region\n",
|
||||
"!python scripts/basic_analysis/compare_hypertower_modes.py \\\n",
|
||||
"!python scripts/main/v2/multirun_hypertower.py \\\n",
|
||||
" --tower-modes single ensemble \\\n",
|
||||
" --eval-modes binary multiclass \\\n",
|
||||
" --epochs 40 --n-splits 5 \\\n",
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Binary runs v2.2 (4 total):
|
||||
# UNet crop: single | fused head
|
||||
# GT crop: single | 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
|
||||
--single-warmup-tower-epochs 4
|
||||
--single-warmup-fused-epochs 4
|
||||
--img-crop-manifest "$MANIFEST"
|
||||
)
|
||||
|
||||
UNET_CROP=(
|
||||
--img-crop-weights "$UNET_WEIGHTS"
|
||||
)
|
||||
|
||||
GT_CROP=(
|
||||
--img-crop-gt
|
||||
)
|
||||
|
||||
# ── UNet crop ────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[1/4] 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/4] UNet crop — binary, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${UNET_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--fused-head --fusion-epochs 20 \
|
||||
--run-name v2.2_fused_binary_unet_40ep_5fold
|
||||
|
||||
# ── GT crop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[3/4] 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 "[4/4] GT crop — binary, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${GT_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--fused-head --fusion-epochs 20 \
|
||||
--run-name v2.2_fused_binary_gt_40ep_5fold
|
||||
|
||||
echo "Binary v2.2 runs complete."
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Image-only runs v2.21 (6 total):
|
||||
# No crop: binary | multiclass
|
||||
# GT crop: binary | multiclass
|
||||
# UNet crop: binary | multiclass
|
||||
#
|
||||
# Purpose: isolate the effect of ROI cropping at the single-CNN level,
|
||||
# without any MD tower contribution (bridge-mode=image_only).
|
||||
|
||||
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
|
||||
--tower-mode single
|
||||
--bridge-mode image_only
|
||||
--single-warmup-tower-epochs 4
|
||||
--single-warmup-fused-epochs 0
|
||||
--img-crop-manifest "$MANIFEST"
|
||||
)
|
||||
|
||||
# ── No crop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[1/6] No crop — binary, image-only..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" --eval-mode binary \
|
||||
--run-name v2.21_imgonly_binary_nocrop_40ep_5fold
|
||||
|
||||
echo "[2/6] No crop — multiclass, image-only..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" --eval-mode multiclass \
|
||||
--run-name v2.21_imgonly_multiclass_nocrop_40ep_5fold
|
||||
|
||||
# ── GT crop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[3/6] GT crop — binary, image-only..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" --eval-mode binary --img-crop-gt \
|
||||
--run-name v2.21_imgonly_binary_gt_40ep_5fold
|
||||
|
||||
echo "[4/6] GT crop — multiclass, image-only..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" --eval-mode multiclass --img-crop-gt \
|
||||
--run-name v2.21_imgonly_multiclass_gt_40ep_5fold
|
||||
|
||||
# ── UNet crop ────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[5/6] UNet crop — binary, image-only..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" --eval-mode binary \
|
||||
--img-crop-weights "$UNET_WEIGHTS" \
|
||||
--run-name v2.21_imgonly_binary_unet_40ep_5fold
|
||||
|
||||
echo "[6/6] UNet crop — multiclass, image-only..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" --eval-mode multiclass \
|
||||
--img-crop-weights "$UNET_WEIGHTS" \
|
||||
--run-name v2.21_imgonly_multiclass_unet_40ep_5fold
|
||||
|
||||
echo "Image-only v2.21 runs complete."
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Multiclass runs v2.2 (4 total):
|
||||
# UNet crop: single | fused head
|
||||
# GT crop: single | 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
|
||||
--single-warmup-tower-epochs 4
|
||||
--single-warmup-fused-epochs 4
|
||||
--img-crop-manifest "$MANIFEST"
|
||||
)
|
||||
|
||||
UNET_CROP=(
|
||||
--img-crop-weights "$UNET_WEIGHTS"
|
||||
)
|
||||
|
||||
GT_CROP=(
|
||||
--img-crop-gt
|
||||
)
|
||||
|
||||
# ── UNet crop ────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[1/4] 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/4] UNet crop — multiclass, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${UNET_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--fused-head --fusion-epochs 20 \
|
||||
--run-name v2.2_fused_multiclass_unet_40ep_5fold
|
||||
|
||||
# ── GT crop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "[3/4] 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 "[4/4] GT crop — multiclass, ensemble + fused head..."
|
||||
python3 scripts/main/v2/run_multifold_v2.py \
|
||||
"${COMMON[@]}" "${GT_CROP[@]}" \
|
||||
--tower-mode ensemble \
|
||||
--fused-head --fusion-epochs 20 \
|
||||
--run-name v2.2_fused_multiclass_gt_40ep_5fold
|
||||
|
||||
echo "Multiclass v2.2 runs complete."
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "=== v2.3 ensemble binary nocrop ==="
|
||||
python scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
--tower-modes ensemble --eval-modes binary \
|
||||
--epochs 40 --n-splits 5 \
|
||||
--backbone refugelike \
|
||||
--img-crop-manifest analysis_data/unet_manifest.csv \
|
||||
--run-name v2.3_ensemble_binary_nocrop
|
||||
|
||||
echo "=== v2.3 ensemble multiclass nocrop ==="
|
||||
python scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
--tower-modes ensemble --eval-modes multiclass \
|
||||
--epochs 40 --n-splits 5 \
|
||||
--backbone refugelike \
|
||||
--img-crop-manifest analysis_data/unet_manifest.csv \
|
||||
--run-name v2.3_ensemble_multiclass_nocrop
|
||||
|
||||
echo "=== done ==="
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "=== v2.3 fused binary nocrop ==="
|
||||
python scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
--tower-modes ensemble --eval-modes binary \
|
||||
--epochs 40 --n-splits 5 \
|
||||
--backbone refugelike \
|
||||
--img-crop-manifest analysis_data/unet_manifest.csv \
|
||||
--warmup-md-epochs 50 \
|
||||
--fused-head \
|
||||
--run-name v2.3_fused_binary_nocrop
|
||||
|
||||
echo "=== v2.3 fused multiclass nocrop ==="
|
||||
python scripts/basic_analysis/compare_hypertower_modes.py \
|
||||
--tower-modes ensemble --eval-modes multiclass \
|
||||
--epochs 40 --n-splits 5 \
|
||||
--backbone refugelike \
|
||||
--img-crop-manifest analysis_data/unet_manifest.csv \
|
||||
--warmup-md-epochs 50 \
|
||||
--fused-head \
|
||||
--run-name v2.3_fused_multiclass_nocrop
|
||||
|
||||
echo "=== done ==="
|
||||
Reference in New Issue
Block a user