Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BraTS 2026 — PATH: Patch-Level Brain-Tumor Sub-region Classification

A leakage-free, long-tailed ensemble pipeline using frozen pathology foundation models + chunked XGBoost.

Classify 512×512 H&E histopathology patches into ten brain-tumor histologic sub-region categories for the BraTS 2026 Pathology (PATH) challenge. Three frozen self-supervised pathology encoders (UNI2-h, Virchow2, Phikon-v2) produce complementary patch embeddings; an ensemble of XGBoost classifiers trained on fixed-width chunks of the embedding space turns those embeddings into class probabilities, which are then refined with class-balanced weighting, rare-class boosting, and per-class threshold optimisation.

Out-of-fold accuracy 0.762  •  MCC 0.713  •  macro-F1 0.541  •  AUC 0.788
Patient-grouped 5-fold StratifiedGroupKFold, pooled OOF, 1,042,640 training patches.


Table of contents

  1. Highlights
  2. The task & dataset
  3. Method overview
  4. Repository layout
  5. Installation
  6. Data preparation
  7. Training (end-to-end)
  8. Inference & submission
  9. Building the submission container (Docker / Apptainer)
  10. Results
  11. Running on an HPC / SLURM cluster
  12. Configuration reference
  13. Reproducibility & the leakage fix
  14. References

Highlights

  • Two-stage design — frozen foundation encoders (no fine-tuning) + gradient-boosted trees. Cheap to train, robust, and trivially parallelisable.
  • Three heterogeneous encoders — UNI2-h (1536-d), Virchow2 (2560-d), Phikon-v2 (1024-d), trained with different objectives/corpora for complementary features.
  • Chunked XGBoost — each embedding is split into 512-d chunks, one classifier per (fold, model, chunk); predictions are averaged. 50 predictors in total.
  • Leakage-free validation — whole patients (never individual patches) are assigned to folds with StratifiedGroupKFold. This corrects a severe defect in the naive split that leaked 124/126 patients across train/val and inflated metrics to ~0.999.
  • Long-tail handling — class-balanced sample weights, rare-class probability boosting, and grid-searched per-class decision thresholds.
  • Offline, reproducible submission container — Docker and Apptainer recipes with foundation-model weights baked in (no network needed at evaluation time).

The task & dataset

The BraTS-Path taxonomy has ten histologic sub-region classes. Nine are inherited from the 2024 edition; BraTS 2026 adds NOTA ("None of the Above") for tissue that matches no defined sub-region. The dataset is strongly long-tailed — the rarest class (DM) is ~14× rarer than the most common (CT).

Id Code — histologic sub-region # patches Share
0 CT — Cellular Tumor 275,909 26.46 %
1 DM — Dense Macrophages 19,596 1.88 %
2 IC — Infiltration into Cortex 111,194 10.66 %
3 LI — Leptomeningeal Infiltration 26,423 2.53 %
4 MP — Microvascular Proliferation 53,248 5.11 %
5 NC — Necrosis 214,884 20.61 %
6 PL — Presence of Lymphocytes 21,113 2.02 %
7 PN — Pseudopalisading Necrosis 92,109 8.83 %
8 WM — White Matter 68,360 6.56 %
9 NOTA — None of the Above 159,804 15.33 %
Total 1,042,640 100 %

Class ids follow Dataset/class_map.json exactly. The submission CSV uses these integer ids (see Inference & submission). Rare classes (< 4 % share) — DM, LI, PL — are bold above and are the targets of rare-class boosting.

Example patches (one per class):

CT DM IC LI MP
CT DM IC LI MP
NC PN WM PL NOTA
NC PN WM PL NOTA

Method overview

                        ┌──────────────────────────────────────────────┐
  512×512 H&E patch ──► │  Stage 1 — frozen pathology foundation models │
                        │   • UNI2-h    ViT-h/14   → 1536-d  (CLS)       │
                        │   • Virchow2  ViT-h      → 2560-d  (CLS ⊕ mean)│
                        │   • Phikon-v2 ViT        → 1024-d  (CLS)       │
                        └───────────────┬──────────────────────────────┘
                                        │  embeddings cached to HDF5
                                        ▼
                        ┌──────────────────────────────────────────────┐
                        │  Stage 2 — chunked XGBoost ensemble           │
                        │   • split each embedding into 512-d chunks     │
                        │   • 1 XGBoost per (fold, model, chunk)         │
                        │   • average predict_proba over all 50 models   │
                        └───────────────┬──────────────────────────────┘
                                        ▼
                        ┌──────────────────────────────────────────────┐
                        │  Long-tail post-processing                    │
                        │   • class-balanced sample weights (train)      │
                        │   • rare-class boost ×2.5  (DM, LI, PL)        │
                        │   • per-class threshold optimisation (macro-F1)│
                        └───────────────┬──────────────────────────────┘
                                        ▼
                                argmax → class id (0–9)

Why these choices

  • Frozen encoders — pathology foundation models already encode rich tissue morphology; freezing them avoids over-fitting on a long-tailed set and is far cheaper than end-to-end fine-tuning.
  • Token pooling — UNI2-h and Phikon-v2 use the CLS token directly; Virchow2 concatenates its CLS token with the mean of its patch tokens (→ 2560-d).
  • 512-d chunking — instead of one classifier on the full 5,120-d concatenation, each 512-d slice gets its own XGBoost (UNI2-h→3, Virchow2→5, Phikon-v2→2 chunks; zero-padded if the last chunk is short). Averaging the chunk predictors acts as an implicit regulariser.
  • Rare-class boost — minority-class probabilities are scaled by 2.5 then re-normalised, a cheap training-free recall lever for DM/LI/PL.
  • Threshold optimisation — per-class decision thresholds are grid-searched on pooled OOF predictions to maximise macro-F1.

A full write-up (architecture diagrams, ablations, per-fold tables) is in report/brats_path_report.pdf.


Repository layout

BraTS2026_PATH/
├── README.md                      ← you are here
├── setup.py                       ← pip-installable package (entry points)
├── requirements.txt
├── codes/
│   ├── config.py                  ← central configuration (paths, models, hyperparams)
│   ├── foundation_models.py       ← load encoders + extract embeddings
│   ├── data_loader.py             ← manifest, dataset, dataloaders
│   ├── patient_split.py           ← leakage-free patient-grouped CV folds
│   ├── build_manifest.py          ← one-time dataset index
│   ├── extract_features.py        ← extract train/val embeddings → HDF5
│   ├── extract_val_features.py    ← extract OFFICIAL val embeddings → HDF5
│   ├── train_xgboost.py           ← train one XGBoost per (model, chunk)
│   ├── run_cv.py                  ← full patient-grouped 5-fold CV (OOF)
│   ├── ensemble_predict.py        ← average chunk predictors + rare-class boost
│   ├── optimize_thresholds.py     ← per-class threshold grid search
│   ├── evaluate.py                ← the 6 challenge metrics + reports
│   ├── predict_val.py             ← OFFICIAL val → submission_val.csv
│   ├── inference.py               ← predict on arbitrary images/dirs
│   ├── plot_training.py           ← training/eval plots
│   ├── saved_models/              ← trained XGBoost fold models + thresholds.npy (committed)
│   │   ├── cv/fold_{0..4}/*.json
│   │   └── thresholds.npy
│   ├── results/                   ← cv_metrics.json, plots, submission_val.csv (committed)
│   ├── features/                  ← embedding HDF5s (git-ignored — regenerate)
│   └── *.sh                       ← SLURM submission scripts
├── docker/
│   ├── Dockerfile                 ← submission image (offline)
│   ├── brats.def                  ← Apptainer/Singularity equivalent
│   ├── infer.py                   ← container entrypoint (/input → /output/predictions.csv)
│   ├── build.sh / run.sh          ← Docker build/run helpers
│   ├── build_apptainer.sh         ← Apptainer build helper
│   ├── requirements-docker.txt
│   └── README.md                  ← container-specific docs
└── report/
    ├── brats_path_report.pdf      ← full technical report
    ├── brats_path_report.tex
    └── figures/                   ← plots + sample patches used in this README

Not in git (regenerate locally): the raw Dataset/, the 21 GB of embedding HDF5s in codes/features/, large .npy result arrays, and the 8.9 GB built SIF. See .gitignore.


Installation

Requires Python ≥ 3.10 and a CUDA GPU for feature extraction (XGBoost training and inference can fall back to CPU).

git clone https://github.com/ujjwalbaid0408/BraTS2026_PATH.git
cd BraTS2026_PATH

# (recommended) a fresh environment
python -m venv .venv && source .venv/bin/activate
#   or: conda create -n brats-path python=3.10 && conda activate brats-path

# install dependencies + the package (exposes brats-* console scripts)
pip install -e .
#   or, just the deps:  pip install -r requirements.txt

Foundation-model access. UNI2-h and Virchow2 are gated on the Hugging Face Hub — request access on their model pages, then authenticate once:

huggingface-cli login            # paste an HF token with the accepted licenses
# optional pre-download (otherwise downloaded lazily on first extract):
python - <<'PY'
from huggingface_hub import snapshot_download
for r in ["MahmoodLab/UNI2-h", "paige-ai/Virchow2", "owkin/phikon-v2"]:
    snapshot_download(r)
PY

Core dependencies: torch, torchvision, timm, transformers, huggingface_hub, safetensors, xgboost, h5py, numpy, pandas, scikit-learn, scipy, Pillow, matplotlib, seaborn, tqdm (full versions in requirements.txt).


Data preparation

Download the BraTS-Path data from the challenge portal and lay it out as config.py expects (override BASE_DIR in config.py if your paths differ):

Dataset/
├── extracted/                              ← {sample_id}.jpg + {sample_id}.cls  (train patches + labels)
├── validation/images/                      ← official val patches  val_<hash>.jpg
├── class_map.json                          ← class name → id
└── BraTS-Path-2026-Train-Patch-Patient-Slide-Mapping.csv   ← Name,Patient,Slide (for leakage-free splits)

Build the one-time manifest (scans extracted/ for .cls label files):

python codes/build_manifest.py          # → codes/manifest.csv

Inspect/verify the patient-grouped folds (asserts no patient spans two folds):

python codes/patient_split.py           # prints per-fold patient/patch/class counts

Training (end-to-end)

The pipeline is staged so every step is cached and resumable.

1. Extract embeddings (GPU)

# all 3 active models × {train, val}; HDF5 written to codes/features/
python codes/extract_features.py --model all --split all
# or a single combination
python codes/extract_features.py --model uni2h --split train

Each output is codes/features/{model}_{split}.h5 with datasets embeddings (N,D) float32, labels (N,) int32, sample_ids (N,) str. Existing files are skipped, so re-runs are cheap.

2. Cross-validation (the headline numbers)

run_cv.py is the recommended path: it runs the full patient-grouped 5-fold CV by reusing the cached embeddings, writes every fold model, and reports pooled out-of-fold metrics. Early stopping uses an inner fold carved from the training folds, so the held-out fold is never seen during fitting.

python codes/run_cv.py                         # all models with embeddings
python codes/run_cv.py --models uni2h virchow2 phikon_v2 --device auto

Outputs:

  • codes/saved_models/cv/fold_{0..4}/{model}_chunk_{NNN}.json — 50 fold models
  • codes/saved_models/thresholds.npy — optimised per-class thresholds
  • codes/results/cv/cv_metrics.json — all metrics (argmax + thresholded + per-fold)
  • codes/results/cv/plots/{oof_confusion_matrix,oof_per_class}.png

3. (Alternative) single-split training

A simpler one-shot train/val path also exists:

python codes/train_xgboost.py --model all      # trains on the patient-disjoint train split
python codes/ensemble_predict.py --split val   # average chunk predictors (+ rare-class boost)
python codes/optimize_thresholds.py            # grid-search per-class thresholds
python codes/evaluate.py --split val --thresh  # report the 6 metrics

Inference & submission

Official validation set → submission CSV

First extract embeddings for the official (unlabeled) val patches, then predict by averaging all 5 CV-fold models per (model, chunk):

python codes/extract_val_features.py           # → codes/features/{model}_valofficial.h5
python codes/predict_val.py                    # → codes/results/submission_val.csv

The submission CSV is exactly:

SubjectID,Prediction
val_00002994ff324576,5
val_00007e874ce61d7c,9
...

where Prediction is the integer class id (0=CT … 8=WM, 9=NOTA) and SubjectID is the patch filename without extension.

Arbitrary images / a directory

python codes/inference.py --input /path/to/patch.jpg
python codes/inference.py --input /path/to/dir_of_jpgs/
# → CSV with sample_id, predicted_class, confidence, and per-class probabilities

Building the submission container (Docker / Apptainer)

The container reads every image in /input and writes a single /output/predictions.csv — the exact challenge contract. Foundation-model weights are baked in, so it runs fully offline (no HF token / network at eval time). See docker/README.md for full details.

Docker

# weights must be in the local HF cache first (gated repos — see Installation)
cd docker
./build.sh                                   # → brats2026-path:latest  (~9 GB)
IMAGE=team/brats-path:v1 ./build.sh          # custom tag

./run.sh /abs/path/to/input_images /abs/path/to/output_dir
# → /abs/path/to/output_dir/predictions.csv

Apptainer / Singularity (HPC, no Docker daemon)

cd docker
./build_apptainer.sh                         # → docker/brats.sif
apptainer run --nv -B /abs/in:/input:ro -B /abs/out:/output brats.sif

Both recipes install the same deps, bake the same weights + trained fold models, and run the same infer.py. Tunables (via -e / env): BRATS_BATCH_SIZE (default 256), BRATS_NUM_WORKERS (default 8). GPU is strongly recommended (≈114 k val patches through three ViTs); CPU-only also works (drop --gpus all / --nv).


Results

Patient-grouped 5-fold StratifiedGroupKFold, pooled out-of-fold (OOF) — 1,042,640 patches.

Metric OOF (argmax) OOF (+ thresholds) per-fold mean ± std
Accuracy 0.7428 0.7618 0.750 ± 0.102
AUC (macro) 0.7885 0.7885 0.884 ± 0.065
F1 (macro) 0.5184 0.5411 0.477 ± 0.058
F1 (weighted) 0.7274 0.7455 0.716 ± 0.130
MCC 0.6900 0.7126 0.684 ± 0.112
Sensitivity 0.5124 0.5378 0.490 ± 0.042
Specificity 0.9694 0.9716 0.968 ± 0.012

Per-class OOF (argmax):

Class Sensitivity Specificity F1
CT 0.840 0.895 0.789
DM 0.278 0.989 0.299
IC 0.863 0.968 0.811
LI 0.023 0.969 0.021
MP 0.177 0.991 0.263
NC 0.907 0.949 0.862
PL 0.000 0.992 0.000
PN 0.541 0.968 0.578
WM 0.573 0.983 0.632
NOTA 0.923 0.989 0.929

The rare classes (DM, LI, PL, and MP) remain the hardest — they dominate the gap between weighted and macro F1. Threshold optimisation recovers ~+0.02 macro-F1 / MCC over plain argmax. Common, morphologically distinct classes (NC, NOTA, IC, CT) are recognised well.

OOF confusion matrix OOF per-class F1 / sensitivity
Confusion matrix Per-class

Predicted class distribution vs. training prior (official val submission):

Distribution comparison

Numbers above are reproduced from codes/results/cv/cv_metrics.json.


Running on an HPC / SLURM cluster

Ready-to-edit SLURM batch scripts live in codes/ and docker/:

Script Purpose
codes/submit_uni2h_extract_*.sh extract embeddings on GPU nodes (B200 / RTX PRO 6000)
codes/submit_val_extract.sh extract official-val embeddings
codes/submit_cv_cpu.sh run the 5-fold CV on CPU partitions (XGBoost is CPU-friendly)
codes/submit_cv_pipeline.sh full CV pipeline
codes/run_after_extract.sh chain CV + threshold opt after extraction finishes
docker/build_apptainer.sbatch build the SIF on a compute node with egress
docker/smoke_test.sbatch container smoke test on a few sample patches

Useful environment overrides (see config.py): BRATS_NUM_WORKERS, BRATS_BATCH_SIZE. Feature extraction is GPU-bound; XGBoost CV runs comfortably on CPU partitions, which is handy when GPU queues are congested.


Configuration reference

All knobs live in codes/config.py:

Setting Default Meaning
ACTIVE_MODELS FOUNDATION_MODELS_V2 UNI2-h + Virchow2 + Phikon-v2 (switch sets here)
CV_FOLDS / CV_SEED 5 / 42 StratifiedGroupKFold config
HOLDOUT_VAL_FOLD 0 which fold is "val" for the single-split path
CHUNK_SIZE 512 embedding dims per XGBoost model
XGBOOST_PARAMS depth 6, lr 0.1, 500 trees, hist gradient-boosting hyperparameters
RARE_THRESHOLD / RARE_BOOST_FACTOR 0.04 / 2.5 rare-class boost trigger & multiplier
THRESHOLD_METRIC / THRESHOLD_STEPS macro_f1 / 50 threshold grid search
BATCH_SIZE / NUM_WORKERS 512 / 8 extraction (env-overridable)
IMAGE_SIZE 224 encoder input resolution

To add GigaPath (a 4th encoder), accept its HF license and uncomment its block in FOUNDATION_MODELS_V2.


Reproducibility & the leakage fix

The original split hashed each patch to a shard via md5(patch_name) % NUM_SHARDS, scattering patches from one slide/patient across both train and val. 124 of 126 patients and 248 of 255 slides appeared on both sides, so the model was validated on near-identical neighbours of its own training patches — inflating metrics to ~0.999 (memorisation, not generalisation).

codes/patient_split.py is the single source of truth for a correct split: StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42) grouped by patient, so no subject ever appears in two folds. The assignment is cached to Dataset/cv_folds.json and a hard assertion (_verify_no_overlap) fails the run if any patient spans multiple folds. No embedding re-extraction is needed — the per-patch embeddings are split-independent; only the partition was wrong.

Determinism: fixed seeds (CV_SEED=42, XGBOOST_PARAMS.random_state=42) and a cached fold file mean run_cv.py reproduces the reported numbers.


References

  1. R. Chen et al. Towards a general-purpose foundation model for computational pathology (UNI). Nature Medicine, 2024. — MahmoodLab/UNI2-h
  2. E. Vorontsov et al. Virchow: A million-slide digital pathology foundation model. 2024. — paige-ai/Virchow2
  3. A. Filiot et al. Phikon / Phikon-v2: Scaling self-supervised learning for histopathology. Owkin, 2023–2024. — owkin/phikon-v2
  4. T. Chen and C. Guestrin. XGBoost: A scalable tree boosting system. KDD, 2016.
  5. M. Oquab et al. DINOv2: Learning robust visual features without supervision. 2023.
  6. F. Pedregosa et al. Scikit-learn: Machine learning in Python (StratifiedGroupKFold). JMLR, 2011.
  7. BraTS-Path Challenge. Patch-Level Classification of Histopathological Subregions in Glioblastoma. BraTS-Path 2026 Proceedings. — source of the official class-name key.
  8. Patch-Level Brain-Tumor Sub-region Classification Using Foundation Models Under Long-Tailed Data Distributions. BraTS-Path winning solution (method this pipeline builds on).

Acknowledgements & license

Built for the BraTS 2026 PATH challenge. The raw data is governed by the challenge's data-use agreement and is not redistributed here. Foundation-model weights belong to their respective authors under their own licenses. Please cite the references above if you build on this work.

About

Leakage-free foundation-model (UNI2-h + Virchow2 + Phikon-v2) + chunked-XGBoost ensemble for 10-class brain-tumor histopathology patch classification — BraTS 2026 PATH. OOF acc 0.762 / MCC 0.713.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages