Upload folder using huggingface_hub
Browse files- README.md +94 -18
- eval/eva_configs/bach.yaml +114 -0
- eval/eva_configs/crc.yaml +113 -0
- eval/eva_configs/patch_camelyon.yaml +129 -0
- eval/openpath_eva_backbone.py +129 -0
- eval/run_patch_eval.sh +44 -0
- eval/st_bench.py +176 -0
README.md
CHANGED
|
@@ -12,12 +12,18 @@ library_name: pytorch
|
|
| 12 |
pipeline_tag: image-feature-extraction
|
| 13 |
---
|
| 14 |
|
| 15 |
-
# OpenPath โ Pathology Foundation Model (training code)
|
| 16 |
|
| 17 |
**OpenPath** is a vision foundation model for computational pathology: a **ViT-g/14** encoder
|
| 18 |
pre-trained with self-supervision (**DINOv2** + **gram anchoring**) on **public-only** whole-slide
|
| 19 |
-
histopathology tiles. This repository contains the **training
|
| 20 |
-
checkpoints are hosted separately (see below).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
- **Encoder:** ViT-g/14 (reg4), 1536-dim CLS embedding
|
| 23 |
- **Objective:** DINO + iBOT + KDE (DINOv2) with **gram anchoring** ported from DINOv3
|
|
@@ -39,6 +45,11 @@ scripts/
|
|
| 39 |
autoresume.sh # crash-tolerant auto-resume
|
| 40 |
watch_eval.sh # online HEST probing per checkpoint
|
| 41 |
run_hest_3way.py # HEST evaluation (Meta DINOv2 / Phikon-v2 / OpenPath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
requirements.txt
|
| 43 |
```
|
| 44 |
|
|
@@ -47,7 +58,7 @@ requirements.txt
|
|
| 47 |
| Artifact | Hugging Face repo | Notes |
|
| 48 |
|---|---|---|
|
| 49 |
| **Corpus** | `taejoon89/openpath-corpus` | Native 40ร pathology tiles, 33,991 WebDataset shards / ~17 TB (public, gated) |
|
| 50 |
-
| **Checkpoints** | `taejoon89/openpath-checkpoints` | OpenPath teacher checkpoints across training (private) |
|
| 51 |
| **Code** | `taejoon89/openpath` | This repository |
|
| 52 |
|
| 53 |
The training config points to the corpus via
|
|
@@ -100,20 +111,85 @@ cls = m.forward_features(x)["x_norm_clstoken"] # (B, 1536)
|
|
| 100 |
## Evaluation
|
| 101 |
|
| 102 |
Frozen-encoder linear/ridge probing. The headline benchmark is a **contamination-free** in-house
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
| NCT-CRC-HE (9-
|
| 111 |
-
|
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
## License
|
| 119 |
|
|
|
|
| 12 |
pipeline_tag: image-feature-extraction
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# OpenPath โ Pathology Foundation Model (training & evaluation code)
|
| 16 |
|
| 17 |
**OpenPath** is a vision foundation model for computational pathology: a **ViT-g/14** encoder
|
| 18 |
pre-trained with self-supervision (**DINOv2** + **gram anchoring**) on **public-only** whole-slide
|
| 19 |
+
histopathology tiles. This repository contains the **training, reproduction, and evaluation code**.
|
| 20 |
+
The corpus and checkpoints are hosted separately (see below).
|
| 21 |
+
|
| 22 |
+
> **Headline result.** On a **contamination-free** in-house spatial-transcriptomics (ST) cohort โ the
|
| 23 |
+
> least leakage-prone benchmark, since no public foundation model was trained on it โ OpenPath **ranks
|
| 24 |
+
> #1 among seven foundation models** (mean Pearson: OpenPath **0.323** > UNI2-h 0.301 > OpenMidnight
|
| 25 |
+
> 0.300 > Virchow2 0.292 > prov-gigapath 0.286 > Phikon-v2 0.274 > UNI 0.257). Released checkpoint:
|
| 26 |
+
> **`training_316250`** (in `openpath-checkpoints`). See [Evaluation](#evaluation).
|
| 27 |
|
| 28 |
- **Encoder:** ViT-g/14 (reg4), 1536-dim CLS embedding
|
| 29 |
- **Objective:** DINO + iBOT + KDE (DINOv2) with **gram anchoring** ported from DINOv3
|
|
|
|
| 45 |
autoresume.sh # crash-tolerant auto-resume
|
| 46 |
watch_eval.sh # online HEST probing per checkpoint
|
| 47 |
run_hest_3way.py # HEST evaluation (Meta DINOv2 / Phikon-v2 / OpenPath)
|
| 48 |
+
eval/ # downstream benchmark / reference-FM comparison
|
| 49 |
+
openpath_eva_backbone.py # backbone factories: OpenPath + Phikon / OpenMidnight / UNI / UNI2-h / gigapath / Virchow2
|
| 50 |
+
st_bench.py # in-house spatial-transcriptomics benchmark (LOPO ridge, headline)
|
| 51 |
+
run_patch_eval.sh # PCam / CRC / BACH patch probing via kaiko-eva
|
| 52 |
+
eva_configs/ # eva YAML configs (crc / bach / patch_camelyon)
|
| 53 |
requirements.txt
|
| 54 |
```
|
| 55 |
|
|
|
|
| 58 |
| Artifact | Hugging Face repo | Notes |
|
| 59 |
|---|---|---|
|
| 60 |
| **Corpus** | `taejoon89/openpath-corpus` | Native 40ร pathology tiles, 33,991 WebDataset shards / ~17 TB (public, gated) |
|
| 61 |
+
| **Checkpoints** | `taejoon89/openpath-checkpoints` | OpenPath teacher checkpoints across training (`training_0` โฆ `training_345000`); **released model = `training_316250`** (private) |
|
| 62 |
| **Code** | `taejoon89/openpath` | This repository |
|
| 63 |
|
| 64 |
The training config points to the corpus via
|
|
|
|
| 111 |
## Evaluation
|
| 112 |
|
| 113 |
Frozen-encoder linear/ridge probing. The headline benchmark is a **contamination-free** in-house
|
| 114 |
+
Visium spatial-transcriptomics (ST) cohort (leave-one-patient-out, mean Pearson over top-50
|
| 115 |
+
highly-variable genes) โ **no public FM was trained on it**, so it is the least leakage-prone
|
| 116 |
+
comparison. The reported OpenPath checkpoint is `training_316250`.
|
| 117 |
+
|
| 118 |
+
**Comparison** โ all 7 models loaded through one backbone factory and probed under an identical
|
| 119 |
+
protocol; sorted by the clean ST benchmark:
|
| 120 |
+
|
| 121 |
+
| Model | ST (in-house, clean) โ | HEST-1K (public) | NCT-CRC-HE (9-cls acc) | BACH (4-cls acc) |
|
| 122 |
+
|---|---|---|---|---|
|
| 123 |
+
| **OpenPath** | **0.323** | 0.372 | 0.954 | 0.761 |
|
| 124 |
+
| UNI2-h | 0.301 | 0.414 | 0.966 | 0.908 |
|
| 125 |
+
| OpenMidnight | 0.300 | 0.390 | 0.967 | 0.906 |
|
| 126 |
+
| Virchow2 | 0.292 | 0.398 | 0.964 | 0.875 |
|
| 127 |
+
| prov-gigapath | 0.286 | 0.393 | 0.953 | 0.752 |
|
| 128 |
+
| Phikon-v2 | 0.274 | 0.375 | 0.937 | 0.708 |
|
| 129 |
+
| UNI | 0.257 | 0.386 | 0.946 | 0.777 |
|
| 130 |
+
|
| 131 |
+
**On the contamination-free ST cohort OpenPath ranks #1** among all seven foundation models. The
|
| 132 |
+
picture inverts on the **public** benchmarks (HEST-1K, CRC, BACH): there OpenPath is mid-pack to
|
| 133 |
+
low, and the large FMs lead. Those benchmarks derive from public repositories (TCGA/GTEx/etc.) that
|
| 134 |
+
these FMs were pre-trained on, so their apparent edge is confounded by **train/test leakage** โ which
|
| 135 |
+
is exactly why the leakage-free in-house ST cohort is our headline. (The reported checkpoint
|
| 136 |
+
`training_316250` is selected by ST; OpenPath's HEST-1K peaks earlier in training at ~0.38.)
|
| 137 |
+
PCam / CAMELYON is excluded because it overlaps our own training corpus.
|
| 138 |
+
|
| 139 |
+
### Reproducing the comparison
|
| 140 |
+
|
| 141 |
+
All models are loaded through a single backbone-factory module (`eval/openpath_eva_backbone.py`) and
|
| 142 |
+
probed under an identical protocol, so OpenPath and the reference FMs (Phikon-v2, OpenMidnight, UNI,
|
| 143 |
+
UNI2-h, gigapath, Virchow2) are directly comparable.
|
| 144 |
+
|
| 145 |
+
```bash
|
| 146 |
+
export PYTHONPATH="$PWD/OpenPath:$PWD/eval"
|
| 147 |
+
# Headline: in-house spatial-transcriptomics (LOPO ridge; cohort is private, code is provided)
|
| 148 |
+
python eval/st_bench.py --backbone openpath --weights <teacher_checkpoint.pth>
|
| 149 |
+
python eval/st_bench.py --backbone uni # reference FM (also: uni2 / gigapath / virchow2 / phikon / openmidnight)
|
| 150 |
+
|
| 151 |
+
# Patch probing (PCam / CRC / BACH) via kaiko-eva
|
| 152 |
+
bash eval/run_patch_eval.sh openpath crc <teacher_checkpoint.pth>
|
| 153 |
+
bash eval/run_patch_eval.sh uni crc # reference FM
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
Reference FM weights are pulled from their Hugging Face hubs on first use (UNI / UNI2-h / Virchow2
|
| 157 |
+
are gated โ request access on HF beforehand).
|
| 158 |
+
|
| 159 |
+
## Intended use & limitations
|
| 160 |
+
|
| 161 |
+
**Intended use.** OpenPath is a **frozen feature extractor** for H&E histopathology. It produces a
|
| 162 |
+
1536-dim CLS embedding per 224ร224 tile (native ~40ร / 0.5 ยตm-per-pixel regime, ImageNet
|
| 163 |
+
normalization) for downstream **linear/ridge probing, k-NN, MIL aggregation, and retrieval**. It is a
|
| 164 |
+
research artifact, **not a medical device**, and must not be used for diagnosis or clinical
|
| 165 |
+
decision-making.
|
| 166 |
+
|
| 167 |
+
**Limitations.**
|
| 168 |
+
- **Public-benchmark leakage.** Public benchmarks (HEST-1K, NCT-CRC-HE, BACH) derive from repositories
|
| 169 |
+
(TCGA/GTEx/โฆ) that many foundation models โ and partly OpenPath โ were pre-trained on. Absolute
|
| 170 |
+
numbers and cross-model rankings on them are confounded; prefer leakage-controlled evaluation.
|
| 171 |
+
- **Checkpoint trade-off.** The released `training_316250` is selected by the clean ST benchmark;
|
| 172 |
+
earlier checkpoints score higher on HEST-1K (~0.38). Pick a checkpoint to match your downstream task.
|
| 173 |
+
- **Domain.** Trained on H&E WSIs at native magnification. Behavior on IHC, cytology, frozen sections,
|
| 174 |
+
non-0.5 ยตm-per-pixel inputs, or non-pathology images is untested.
|
| 175 |
+
- **Patch-level encoder.** OpenPath encodes tiles independently; slide-level context requires a
|
| 176 |
+
separate aggregator (future work).
|
| 177 |
+
|
| 178 |
+
## Citation
|
| 179 |
+
|
| 180 |
+
A paper is in preparation. Until then, please cite the repository and the upstream work it builds on:
|
| 181 |
+
|
| 182 |
+
```bibtex
|
| 183 |
+
@misc{openpath2026,
|
| 184 |
+
title = {OpenPath: a public-data pathology foundation model},
|
| 185 |
+
author = {OpenPath authors},
|
| 186 |
+
year = {2026},
|
| 187 |
+
note = {https://huggingface.co/taejoon89/openpath}
|
| 188 |
+
}
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
OpenPath builds on **DINOv2**, **OpenMidnight / Midnight**, and **gram anchoring (DINOv3)** โ see
|
| 192 |
+
`OpenPath/README.md` for the full upstream citations, which should also be cited.
|
| 193 |
|
| 194 |
## License
|
| 195 |
|
eval/eva_configs/bach.yaml
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
trainer:
|
| 3 |
+
class_path: eva.Trainer
|
| 4 |
+
init_args:
|
| 5 |
+
n_runs: &N_RUNS ${oc.env:N_RUNS, 5}
|
| 6 |
+
default_root_dir: &OUTPUT_ROOT ${oc.env:OUTPUT_ROOT, logs/${oc.env:MODEL_NAME, dino_vits16}/offline/bach}
|
| 7 |
+
max_steps: &MAX_STEPS ${oc.env:MAX_STEPS, 12500}
|
| 8 |
+
checkpoint_type: ${oc.env:CHECKPOINT_TYPE, best}
|
| 9 |
+
accelerator: ${oc.env:ACCELERATOR, auto}
|
| 10 |
+
devices: ${oc.env:NUM_DEVICES, 1}
|
| 11 |
+
callbacks:
|
| 12 |
+
- class_path: eva.callbacks.ConfigurationLogger
|
| 13 |
+
- class_path: lightning.pytorch.callbacks.TQDMProgressBar
|
| 14 |
+
init_args:
|
| 15 |
+
refresh_rate: ${oc.env:TQDM_REFRESH_RATE, 1}
|
| 16 |
+
- class_path: lightning.pytorch.callbacks.LearningRateMonitor
|
| 17 |
+
init_args:
|
| 18 |
+
logging_interval: epoch
|
| 19 |
+
- class_path: lightning.pytorch.callbacks.ModelCheckpoint
|
| 20 |
+
init_args:
|
| 21 |
+
filename: best
|
| 22 |
+
save_last: ${oc.env:SAVE_LAST, false}
|
| 23 |
+
save_top_k: 1
|
| 24 |
+
monitor: &MONITOR_METRIC ${oc.env:MONITOR_METRIC, val/MulticlassAccuracy}
|
| 25 |
+
mode: &MONITOR_METRIC_MODE ${oc.env:MONITOR_METRIC_MODE, max}
|
| 26 |
+
- class_path: lightning.pytorch.callbacks.EarlyStopping
|
| 27 |
+
init_args:
|
| 28 |
+
min_delta: 0
|
| 29 |
+
patience: ${oc.env:PATIENCE, 1250}
|
| 30 |
+
monitor: *MONITOR_METRIC
|
| 31 |
+
mode: *MONITOR_METRIC_MODE
|
| 32 |
+
- class_path: eva.callbacks.ClassificationEmbeddingsWriter
|
| 33 |
+
init_args:
|
| 34 |
+
output_dir: &DATASET_EMBEDDINGS_ROOT ${oc.env:EMBEDDINGS_ROOT, ./data/embeddings}/${oc.env:MODEL_NAME, dino_vits16}/bach
|
| 35 |
+
dataloader_idx_map:
|
| 36 |
+
0: train
|
| 37 |
+
1: val
|
| 38 |
+
backbone:
|
| 39 |
+
class_path: eva.core.models.wrappers.ModelFromFunction
|
| 40 |
+
init_args:
|
| 41 |
+
path: ${oc.env:BACKBONE_FN, openpath_eva_backbone.build_openpath}
|
| 42 |
+
arguments:
|
| 43 |
+
weights: ${oc.env:OPENPATH_WEIGHTS, "none"}
|
| 44 |
+
overwrite: false
|
| 45 |
+
logger:
|
| 46 |
+
- class_path: lightning.pytorch.loggers.TensorBoardLogger
|
| 47 |
+
init_args:
|
| 48 |
+
save_dir: *OUTPUT_ROOT
|
| 49 |
+
name: ""
|
| 50 |
+
model:
|
| 51 |
+
class_path: eva.HeadModule
|
| 52 |
+
init_args:
|
| 53 |
+
head:
|
| 54 |
+
class_path: torch.nn.Linear
|
| 55 |
+
init_args:
|
| 56 |
+
in_features: ${oc.env:IN_FEATURES, 384}
|
| 57 |
+
out_features: &NUM_CLASSES 4
|
| 58 |
+
criterion: torch.nn.CrossEntropyLoss
|
| 59 |
+
optimizer:
|
| 60 |
+
class_path: torch.optim.AdamW
|
| 61 |
+
init_args:
|
| 62 |
+
lr: ${oc.env:LR_VALUE, 0.0003}
|
| 63 |
+
metrics:
|
| 64 |
+
common:
|
| 65 |
+
- class_path: eva.metrics.AverageLoss
|
| 66 |
+
- class_path: eva.metrics.MulticlassClassificationMetrics
|
| 67 |
+
init_args:
|
| 68 |
+
num_classes: *NUM_CLASSES
|
| 69 |
+
data:
|
| 70 |
+
class_path: eva.DataModule
|
| 71 |
+
init_args:
|
| 72 |
+
datasets:
|
| 73 |
+
train:
|
| 74 |
+
class_path: eva.datasets.EmbeddingsClassificationDataset
|
| 75 |
+
init_args: &DATASET_ARGS
|
| 76 |
+
root: *DATASET_EMBEDDINGS_ROOT
|
| 77 |
+
manifest_file: manifest.csv
|
| 78 |
+
split: train
|
| 79 |
+
val:
|
| 80 |
+
class_path: eva.datasets.EmbeddingsClassificationDataset
|
| 81 |
+
init_args:
|
| 82 |
+
<<: *DATASET_ARGS
|
| 83 |
+
split: val
|
| 84 |
+
predict:
|
| 85 |
+
- class_path: eva.vision.datasets.BACH
|
| 86 |
+
init_args: &PREDICT_DATASET_ARGS
|
| 87 |
+
root: ${oc.env:DATA_ROOT, ./data/bach}
|
| 88 |
+
split: train
|
| 89 |
+
download: ${oc.env:DOWNLOAD_DATA, false}
|
| 90 |
+
# Set `download: true` to download the dataset from https://zenodo.org/records/3632035
|
| 91 |
+
# The BACH dataset is distributed under the following license
|
| 92 |
+
# Attribution-NonCommercial-NoDerivs 4.0 International license
|
| 93 |
+
# (see: https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode)
|
| 94 |
+
transforms:
|
| 95 |
+
class_path: eva.vision.data.transforms.common.ResizeAndCrop
|
| 96 |
+
init_args:
|
| 97 |
+
size: ${oc.env:RESIZE_DIM, 224}
|
| 98 |
+
mean: ${oc.env:NORMALIZE_MEAN, [0.485, 0.456, 0.406]}
|
| 99 |
+
std: ${oc.env:NORMALIZE_STD, [0.229, 0.224, 0.225]}
|
| 100 |
+
- class_path: eva.vision.datasets.BACH
|
| 101 |
+
init_args:
|
| 102 |
+
<<: *PREDICT_DATASET_ARGS
|
| 103 |
+
split: val
|
| 104 |
+
dataloaders:
|
| 105 |
+
train:
|
| 106 |
+
batch_size: &BATCH_SIZE ${oc.env:BATCH_SIZE, 256}
|
| 107 |
+
num_workers: &N_DATA_WORKERS ${oc.env:N_DATA_WORKERS, 4}
|
| 108 |
+
shuffle: true
|
| 109 |
+
val:
|
| 110 |
+
batch_size: *BATCH_SIZE
|
| 111 |
+
num_workers: *N_DATA_WORKERS
|
| 112 |
+
predict:
|
| 113 |
+
batch_size: &PREDICT_BATCH_SIZE ${oc.env:PREDICT_BATCH_SIZE, 64}
|
| 114 |
+
num_workers: *N_DATA_WORKERS
|
eval/eva_configs/crc.yaml
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
trainer:
|
| 3 |
+
class_path: eva.Trainer
|
| 4 |
+
init_args:
|
| 5 |
+
n_runs: &N_RUNS ${oc.env:N_RUNS, 5}
|
| 6 |
+
default_root_dir: &OUTPUT_ROOT ${oc.env:OUTPUT_ROOT, logs/${oc.env:MODEL_NAME, dino_vits16}/offline/crc}
|
| 7 |
+
max_steps: &MAX_STEPS ${oc.env:MAX_STEPS, 12500}
|
| 8 |
+
checkpoint_type: ${oc.env:CHECKPOINT_TYPE, best}
|
| 9 |
+
accelerator: ${oc.env:ACCELERATOR, auto}
|
| 10 |
+
devices: ${oc.env:NUM_DEVICES, 1}
|
| 11 |
+
callbacks:
|
| 12 |
+
- class_path: eva.callbacks.ConfigurationLogger
|
| 13 |
+
- class_path: lightning.pytorch.callbacks.TQDMProgressBar
|
| 14 |
+
init_args:
|
| 15 |
+
refresh_rate: ${oc.env:TQDM_REFRESH_RATE, 1}
|
| 16 |
+
- class_path: lightning.pytorch.callbacks.LearningRateMonitor
|
| 17 |
+
init_args:
|
| 18 |
+
logging_interval: epoch
|
| 19 |
+
- class_path: lightning.pytorch.callbacks.ModelCheckpoint
|
| 20 |
+
init_args:
|
| 21 |
+
filename: best
|
| 22 |
+
save_last: ${oc.env:SAVE_LAST, false}
|
| 23 |
+
save_top_k: 1
|
| 24 |
+
monitor: &MONITOR_METRIC ${oc.env:MONITOR_METRIC, val/MulticlassAccuracy}
|
| 25 |
+
mode: &MONITOR_METRIC_MODE ${oc.env:MONITOR_METRIC_MODE, max}
|
| 26 |
+
- class_path: lightning.pytorch.callbacks.EarlyStopping
|
| 27 |
+
init_args:
|
| 28 |
+
min_delta: 0
|
| 29 |
+
patience: ${oc.env:PATIENCE, 7}
|
| 30 |
+
monitor: *MONITOR_METRIC
|
| 31 |
+
mode: *MONITOR_METRIC_MODE
|
| 32 |
+
- class_path: eva.callbacks.ClassificationEmbeddingsWriter
|
| 33 |
+
init_args:
|
| 34 |
+
output_dir: &DATASET_EMBEDDINGS_ROOT ${oc.env:EMBEDDINGS_ROOT, ./data/embeddings}/${oc.env:MODEL_NAME, dino_vits16}/crc
|
| 35 |
+
dataloader_idx_map:
|
| 36 |
+
0: train
|
| 37 |
+
1: val
|
| 38 |
+
backbone:
|
| 39 |
+
class_path: eva.core.models.wrappers.ModelFromFunction
|
| 40 |
+
init_args:
|
| 41 |
+
path: ${oc.env:BACKBONE_FN, openpath_eva_backbone.build_openpath}
|
| 42 |
+
arguments:
|
| 43 |
+
weights: ${oc.env:OPENPATH_WEIGHTS, "none"}
|
| 44 |
+
overwrite: false
|
| 45 |
+
logger:
|
| 46 |
+
- class_path: lightning.pytorch.loggers.TensorBoardLogger
|
| 47 |
+
init_args:
|
| 48 |
+
save_dir: *OUTPUT_ROOT
|
| 49 |
+
name: ""
|
| 50 |
+
model:
|
| 51 |
+
class_path: eva.HeadModule
|
| 52 |
+
init_args:
|
| 53 |
+
head:
|
| 54 |
+
class_path: openpath_eva_backbone.BNHead
|
| 55 |
+
init_args:
|
| 56 |
+
in_features: ${oc.env:IN_FEATURES, 384}
|
| 57 |
+
out_features: &NUM_CLASSES 9
|
| 58 |
+
criterion: torch.nn.CrossEntropyLoss
|
| 59 |
+
optimizer:
|
| 60 |
+
class_path: torch.optim.AdamW
|
| 61 |
+
init_args:
|
| 62 |
+
lr: ${oc.env:LR_VALUE, 0.0003}
|
| 63 |
+
metrics:
|
| 64 |
+
common:
|
| 65 |
+
- class_path: eva.metrics.AverageLoss
|
| 66 |
+
- class_path: eva.metrics.MulticlassClassificationMetrics
|
| 67 |
+
init_args:
|
| 68 |
+
num_classes: *NUM_CLASSES
|
| 69 |
+
data:
|
| 70 |
+
class_path: eva.DataModule
|
| 71 |
+
init_args:
|
| 72 |
+
datasets:
|
| 73 |
+
train:
|
| 74 |
+
class_path: eva.datasets.EmbeddingsClassificationDataset
|
| 75 |
+
init_args: &DATASET_ARGS
|
| 76 |
+
root: *DATASET_EMBEDDINGS_ROOT
|
| 77 |
+
manifest_file: manifest.csv
|
| 78 |
+
split: train
|
| 79 |
+
val:
|
| 80 |
+
class_path: eva.datasets.EmbeddingsClassificationDataset
|
| 81 |
+
init_args:
|
| 82 |
+
<<: *DATASET_ARGS
|
| 83 |
+
split: val
|
| 84 |
+
predict:
|
| 85 |
+
- class_path: eva.vision.datasets.CRC
|
| 86 |
+
init_args: &PREDICT_DATASET_ARGS
|
| 87 |
+
root: ${oc.env:DATA_ROOT, ./data/crc}
|
| 88 |
+
split: train
|
| 89 |
+
download: ${oc.env:DOWNLOAD_DATA, false}
|
| 90 |
+
# Set `download: true` to download the dataset from https://zenodo.org/records/1214456
|
| 91 |
+
# The CRC dataset is distributed under the following license: "CC BY 4.0 LEGAL CODE"
|
| 92 |
+
# (see: https://creativecommons.org/licenses/by/4.0/legalcode)
|
| 93 |
+
transforms:
|
| 94 |
+
class_path: eva.vision.data.transforms.common.ResizeAndCrop
|
| 95 |
+
init_args:
|
| 96 |
+
size: ${oc.env:RESIZE_DIM, 224}
|
| 97 |
+
mean: ${oc.env:NORMALIZE_MEAN, [0.485, 0.456, 0.406]}
|
| 98 |
+
std: ${oc.env:NORMALIZE_STD, [0.229, 0.224, 0.225]}
|
| 99 |
+
- class_path: eva.vision.datasets.CRC
|
| 100 |
+
init_args:
|
| 101 |
+
<<: *PREDICT_DATASET_ARGS
|
| 102 |
+
split: val
|
| 103 |
+
dataloaders:
|
| 104 |
+
train:
|
| 105 |
+
batch_size: &BATCH_SIZE ${oc.env:BATCH_SIZE, 256}
|
| 106 |
+
num_workers: &N_DATA_WORKERS ${oc.env:N_DATA_WORKERS, 4}
|
| 107 |
+
shuffle: true
|
| 108 |
+
val:
|
| 109 |
+
batch_size: *BATCH_SIZE
|
| 110 |
+
num_workers: *N_DATA_WORKERS
|
| 111 |
+
predict:
|
| 112 |
+
batch_size: &PREDICT_BATCH_SIZE ${oc.env:PREDICT_BATCH_SIZE, 64}
|
| 113 |
+
num_workers: *N_DATA_WORKERS
|
eval/eva_configs/patch_camelyon.yaml
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
trainer:
|
| 3 |
+
class_path: eva.Trainer
|
| 4 |
+
init_args:
|
| 5 |
+
n_runs: &N_RUNS ${oc.env:N_RUNS, 5}
|
| 6 |
+
default_root_dir: &OUTPUT_ROOT ${oc.env:OUTPUT_ROOT, logs/${oc.env:MODEL_NAME, dino_vits16}/offline/patch_camelyon}
|
| 7 |
+
max_steps: &MAX_STEPS ${oc.env:MAX_STEPS, 12500}
|
| 8 |
+
checkpoint_type: ${oc.env:CHECKPOINT_TYPE, best}
|
| 9 |
+
accelerator: ${oc.env:ACCELERATOR, auto}
|
| 10 |
+
devices: ${oc.env:NUM_DEVICES, 1}
|
| 11 |
+
callbacks:
|
| 12 |
+
- class_path: eva.callbacks.ConfigurationLogger
|
| 13 |
+
- class_path: lightning.pytorch.callbacks.TQDMProgressBar
|
| 14 |
+
init_args:
|
| 15 |
+
refresh_rate: ${oc.env:TQDM_REFRESH_RATE, 1}
|
| 16 |
+
- class_path: lightning.pytorch.callbacks.LearningRateMonitor
|
| 17 |
+
init_args:
|
| 18 |
+
logging_interval: epoch
|
| 19 |
+
- class_path: lightning.pytorch.callbacks.ModelCheckpoint
|
| 20 |
+
init_args:
|
| 21 |
+
filename: best
|
| 22 |
+
save_last: ${oc.env:SAVE_LAST, false}
|
| 23 |
+
save_top_k: 1
|
| 24 |
+
monitor: &MONITOR_METRIC ${oc.env:MONITOR_METRIC, val/BinaryBalancedAccuracy}
|
| 25 |
+
mode: &MONITOR_METRIC_MODE ${oc.env:MONITOR_METRIC_MODE, max}
|
| 26 |
+
- class_path: lightning.pytorch.callbacks.EarlyStopping
|
| 27 |
+
init_args:
|
| 28 |
+
min_delta: 0
|
| 29 |
+
patience: ${oc.env:PATIENCE, 3}
|
| 30 |
+
monitor: *MONITOR_METRIC
|
| 31 |
+
mode: *MONITOR_METRIC_MODE
|
| 32 |
+
- class_path: eva.callbacks.ClassificationEmbeddingsWriter
|
| 33 |
+
init_args:
|
| 34 |
+
output_dir: &DATASET_EMBEDDINGS_ROOT ${oc.env:EMBEDDINGS_ROOT, ./data/embeddings}/${oc.env:MODEL_NAME, dino_vits16}/patch_camelyon
|
| 35 |
+
dataloader_idx_map:
|
| 36 |
+
0: train
|
| 37 |
+
1: val
|
| 38 |
+
2: test
|
| 39 |
+
backbone:
|
| 40 |
+
class_path: eva.core.models.wrappers.ModelFromFunction
|
| 41 |
+
init_args:
|
| 42 |
+
path: ${oc.env:BACKBONE_FN, openpath_eva_backbone.build_openpath}
|
| 43 |
+
arguments:
|
| 44 |
+
weights: ${oc.env:OPENPATH_WEIGHTS, "none"}
|
| 45 |
+
overwrite: false
|
| 46 |
+
logger:
|
| 47 |
+
- class_path: lightning.pytorch.loggers.TensorBoardLogger
|
| 48 |
+
init_args:
|
| 49 |
+
save_dir: *OUTPUT_ROOT
|
| 50 |
+
name: ""
|
| 51 |
+
model:
|
| 52 |
+
class_path: eva.HeadModule
|
| 53 |
+
init_args:
|
| 54 |
+
head:
|
| 55 |
+
class_path: torch.nn.Linear
|
| 56 |
+
init_args:
|
| 57 |
+
in_features: ${oc.env:IN_FEATURES, 384}
|
| 58 |
+
out_features: 1
|
| 59 |
+
criterion: torch.nn.BCEWithLogitsLoss
|
| 60 |
+
optimizer:
|
| 61 |
+
class_path: torch.optim.AdamW
|
| 62 |
+
init_args:
|
| 63 |
+
lr: ${oc.env:LR_VALUE, 0.0003}
|
| 64 |
+
metrics:
|
| 65 |
+
common:
|
| 66 |
+
- class_path: eva.metrics.AverageLoss
|
| 67 |
+
- class_path: eva.metrics.BinaryClassificationMetrics
|
| 68 |
+
data:
|
| 69 |
+
class_path: eva.DataModule
|
| 70 |
+
init_args:
|
| 71 |
+
datasets:
|
| 72 |
+
train:
|
| 73 |
+
class_path: eva.datasets.EmbeddingsClassificationDataset
|
| 74 |
+
init_args: &DATASET_ARGS
|
| 75 |
+
root: *DATASET_EMBEDDINGS_ROOT
|
| 76 |
+
manifest_file: manifest.csv
|
| 77 |
+
split: train
|
| 78 |
+
target_transforms:
|
| 79 |
+
class_path: torchvision.transforms.v2.ToDtype
|
| 80 |
+
init_args:
|
| 81 |
+
dtype: torch.float32
|
| 82 |
+
val:
|
| 83 |
+
class_path: eva.datasets.EmbeddingsClassificationDataset
|
| 84 |
+
init_args:
|
| 85 |
+
<<: *DATASET_ARGS
|
| 86 |
+
split: val
|
| 87 |
+
test:
|
| 88 |
+
class_path: eva.datasets.EmbeddingsClassificationDataset
|
| 89 |
+
init_args:
|
| 90 |
+
<<: *DATASET_ARGS
|
| 91 |
+
split: test
|
| 92 |
+
predict:
|
| 93 |
+
- class_path: eva.vision.datasets.PatchCamelyon
|
| 94 |
+
init_args: &PREDICT_DATASET_ARGS
|
| 95 |
+
root: ${oc.env:DATA_ROOT, ./data/patch_camelyon}
|
| 96 |
+
split: train
|
| 97 |
+
download: ${oc.env:DOWNLOAD_DATA, false}
|
| 98 |
+
# Set `download: true` to download the dataset from https://zenodo.org/records/1494286
|
| 99 |
+
# The PatchCamelyon dataset is distributed under the following license:
|
| 100 |
+
# "Creative Commons Zero v1.0 Universal"
|
| 101 |
+
# (see: https://choosealicense.com/licenses/cc0-1.0/)
|
| 102 |
+
transforms:
|
| 103 |
+
class_path: eva.vision.data.transforms.common.ResizeAndCrop
|
| 104 |
+
init_args:
|
| 105 |
+
size: ${oc.env:RESIZE_DIM, 224}
|
| 106 |
+
mean: ${oc.env:NORMALIZE_MEAN, [0.485, 0.456, 0.406]}
|
| 107 |
+
std: ${oc.env:NORMALIZE_STD, [0.229, 0.224, 0.225]}
|
| 108 |
+
- class_path: eva.vision.datasets.PatchCamelyon
|
| 109 |
+
init_args:
|
| 110 |
+
<<: *PREDICT_DATASET_ARGS
|
| 111 |
+
split: val
|
| 112 |
+
- class_path: eva.vision.datasets.PatchCamelyon
|
| 113 |
+
init_args:
|
| 114 |
+
<<: *PREDICT_DATASET_ARGS
|
| 115 |
+
split: test
|
| 116 |
+
dataloaders:
|
| 117 |
+
train:
|
| 118 |
+
batch_size: &BATCH_SIZE ${oc.env:BATCH_SIZE, 256}
|
| 119 |
+
num_workers: &N_DATA_WORKERS ${oc.env:N_DATA_WORKERS, 4}
|
| 120 |
+
shuffle: true
|
| 121 |
+
val:
|
| 122 |
+
batch_size: *BATCH_SIZE
|
| 123 |
+
num_workers: *N_DATA_WORKERS
|
| 124 |
+
test:
|
| 125 |
+
batch_size: *BATCH_SIZE
|
| 126 |
+
num_workers: *N_DATA_WORKERS
|
| 127 |
+
predict:
|
| 128 |
+
batch_size: &PREDICT_BATCH_SIZE ${oc.env:PREDICT_BATCH_SIZE, 64}
|
| 129 |
+
num_workers: *N_DATA_WORKERS
|
eval/openpath_eva_backbone.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""eva ์ฐ๋์ฉ OpenPath ViT-g/14 ๋ฐฑ๋ณธ ํฉํ ๋ฆฌ.
|
| 2 |
+
|
| 3 |
+
eva์ ModelFromFunction์ด ํธ์ถ. dinov2 vit_giant2๋ฅผ ๋น๋ํ๊ณ ์ถ์ถ๋ teacher
|
| 4 |
+
state_dict(.pth)๋ฅผ ๋ก๋ํด, forward(x)->(B,1536) CLS ์๋ฒ ๋ฉ์ ๋ฐํํ๋ nn.Module์ ๋๋ ค์ค๋ค.
|
| 5 |
+
์ฐ๋ฆฌ ํ์ต/์ถ์ถ๊ณผ ๋์ผ ๊ท์ฝ(ImageNet norm์ eva ์ชฝ transform์ด ๋ด๋น; ์ฌ๊ธฐ์ ๋ชจ๋ธ๋ง).
|
| 6 |
+
"""
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def build_openpath_vitg14(weights: str) -> nn.Module:
|
| 12 |
+
import dinov2.models.vision_transformer as vits
|
| 13 |
+
|
| 14 |
+
ck = torch.load(weights, map_location="cpu", weights_only=False)
|
| 15 |
+
arch = ck.get("arch", "vit_giant2")
|
| 16 |
+
kw = dict(patch_size=ck.get("patch_size", 14), img_size=224,
|
| 17 |
+
block_chunks=0, init_values=1.0)
|
| 18 |
+
if arch == "vit_giant2":
|
| 19 |
+
kw["ffn_layer"] = "swiglufused"
|
| 20 |
+
model = getattr(vits, arch)(**kw)
|
| 21 |
+
miss, unexp = model.load_state_dict(ck["teacher_backbone"], strict=True)
|
| 22 |
+
assert not miss and not unexp, f"load mismatch miss={miss} unexp={unexp}"
|
| 23 |
+
|
| 24 |
+
class CLSWrapper(nn.Module):
|
| 25 |
+
def __init__(self, m):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.m = m
|
| 28 |
+
|
| 29 |
+
@torch.no_grad()
|
| 30 |
+
def forward(self, x):
|
| 31 |
+
out = self.m(x) # vit_giant2(x) -> (B, embed_dim) CLS
|
| 32 |
+
if out.ndim == 3: # ํน์ ํ ํฐ ์ํ์ค๋ฉด CLS ์ถ์ถ
|
| 33 |
+
out = out[:, 0, :]
|
| 34 |
+
return out
|
| 35 |
+
|
| 36 |
+
return CLSWrapper(model).eval()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def build_phikon(weights: str = None) -> nn.Module:
|
| 40 |
+
"""Phikon-v2 (owkin/phikon-v2, ViT-L, CLS=1024). weights ๋ฌด์(HF ๋ก๋)."""
|
| 41 |
+
from transformers import AutoModel
|
| 42 |
+
base = AutoModel.from_pretrained("owkin/phikon-v2")
|
| 43 |
+
|
| 44 |
+
class W(nn.Module):
|
| 45 |
+
def __init__(self, m):
|
| 46 |
+
super().__init__(); self.m = m
|
| 47 |
+
@torch.no_grad()
|
| 48 |
+
def forward(self, x):
|
| 49 |
+
return self.m(pixel_values=x).last_hidden_state[:, 0, :] # CLS
|
| 50 |
+
print("[eva] backbone=Phikon-v2 (CLS 1024)", flush=True)
|
| 51 |
+
return W(base).eval()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def build_openmidnight(weights: str = None) -> nn.Module:
|
| 55 |
+
"""OpenMidnight (SophontAI/OpenMidnight) teacher_checkpoint โ dinov2 CLS.
|
| 56 |
+
weights ๋ฏธ์ง์ ์ HF ์บ์์ ์๋ ๋ก๋. (run_hest_3way build_om๊ณผ ๋์ผ ๊ท์ฝ, block_chunks=4)"""
|
| 57 |
+
import dinov2.models.vision_transformer as vits
|
| 58 |
+
if not weights or weights in ("none", "None", ""):
|
| 59 |
+
from huggingface_hub import hf_hub_download
|
| 60 |
+
weights = hf_hub_download("SophontAI/OpenMidnight", "teacher_checkpoint.pth")
|
| 61 |
+
ck = torch.load(weights, map_location="cpu", weights_only=False)
|
| 62 |
+
t = ck["teacher"] if "teacher" in ck else ck
|
| 63 |
+
sd = {k[len("backbone."):]: v for k, v in t.items() if k.startswith("backbone.")}
|
| 64 |
+
embed_dim = sd["cls_token"].shape[-1]
|
| 65 |
+
arch, ffn = ("vit_giant2", "swiglufused") if embed_dim == 1536 else ("vit_large", "mlp")
|
| 66 |
+
m = getattr(vits, arch)(patch_size=14, img_size=224, block_chunks=4,
|
| 67 |
+
num_register_tokens=4, ffn_layer=ffn, init_values=1.0e-05,
|
| 68 |
+
interpolate_antialias=True, interpolate_offset=0.0)
|
| 69 |
+
miss, unexp = m.load_state_dict(sd, strict=True)
|
| 70 |
+
assert not miss and not unexp, f"OM miss={miss} unexp={unexp}"
|
| 71 |
+
print(f"[eva] backbone=OpenMidnight arch={arch} embed_dim={embed_dim}", flush=True)
|
| 72 |
+
|
| 73 |
+
class W(nn.Module):
|
| 74 |
+
def __init__(self, mm):
|
| 75 |
+
super().__init__(); self.m = mm
|
| 76 |
+
@torch.no_grad()
|
| 77 |
+
def forward(self, x):
|
| 78 |
+
return self.m.forward_features(x)["x_norm_clstoken"]
|
| 79 |
+
return W(m).eval()
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _cls_wrap(base):
|
| 83 |
+
class W(nn.Module):
|
| 84 |
+
def __init__(s, m): super().__init__(); s.m = m
|
| 85 |
+
@torch.no_grad()
|
| 86 |
+
def forward(s, x):
|
| 87 |
+
out = s.m(x)
|
| 88 |
+
if isinstance(out, (tuple, list)): out = out[0]
|
| 89 |
+
if out.ndim == 3: out = out[:, 0, :] # tokens โ CLS
|
| 90 |
+
return out
|
| 91 |
+
return W(base).eval()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def build_timm_hub(repo, **kw):
|
| 95 |
+
"""timm hf-hub ๋ณ๋ฆฌ FM (UNI/UNI2/Virchow2/gigapath). config.json ์๋."""
|
| 96 |
+
import timm
|
| 97 |
+
base = timm.create_model(f"hf-hub:{repo}", pretrained=True, **kw)
|
| 98 |
+
print(f"[eva] backbone=timm:{repo}", flush=True)
|
| 99 |
+
return _cls_wrap(base)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def build_uni(weights=None): return build_timm_hub("MahmoodLab/UNI", init_values=1e-5, dynamic_img_size=True)
|
| 103 |
+
def build_uni2(weights=None):
|
| 104 |
+
return build_timm_hub("MahmoodLab/UNI2-h", img_size=224, patch_size=14, depth=24, num_heads=24,
|
| 105 |
+
init_values=1e-5, embed_dim=1536, mlp_ratio=2.66667*2, num_classes=0,
|
| 106 |
+
no_embed_class=True, mlp_layer=__import__("timm").layers.SwiGLUPacked,
|
| 107 |
+
act_layer=__import__("torch").nn.SiLU, reg_tokens=8, dynamic_img_size=True)
|
| 108 |
+
def build_virchow2(weights=None):
|
| 109 |
+
import timm
|
| 110 |
+
return build_timm_hub("paige-ai/Virchow2", mlp_layer=timm.layers.SwiGLUPacked, act_layer=__import__("torch").nn.SiLU)
|
| 111 |
+
def build_gigapath(weights=None): return build_timm_hub("prov-gigapath/prov-gigapath", dynamic_img_size=True)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def build_openpath(weights: str) -> nn.Module:
|
| 115 |
+
"""OpenPath teacher_checkpoint(dinov2 ViT-g/14 reg4) โ CLS. our run ์ฒดํฌํฌ์ธํธ ๋ก๋."""
|
| 116 |
+
return build_openmidnight(weights)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class BNHead(nn.Module):
|
| 120 |
+
"""eva linear-probe head: BatchNorm1d(์ฐจ์๋ณ ํ์คํ) โ Linear.
|
| 121 |
+
์ฐ๋ฆฌ ์๋ฒ ๋ฉ์ ์ผ๋ถ ์ฐจ์์ massive activation(norm~500)์ด ์์ด raw Linear๊ฐ
|
| 122 |
+
ํ์ต ์คํจ. BN์ผ๋ก per-dim ํ์คํํ๋ฉด sklearn StandardScaler์ ๋๋ฑํด์ ธ ํ๋ณต."""
|
| 123 |
+
def __init__(self, in_features: int, out_features: int):
|
| 124 |
+
super().__init__()
|
| 125 |
+
self.bn = nn.BatchNorm1d(in_features, affine=True)
|
| 126 |
+
self.fc = nn.Linear(in_features, out_features)
|
| 127 |
+
|
| 128 |
+
def forward(self, x):
|
| 129 |
+
return self.fc(self.bn(x))
|
eval/run_patch_eval.sh
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Patch ๋ถ๋ฅ ๋ฒค์น(pcam/crc/bach)๋ฅผ kaiko-eva๋ก frozen-encoder linear probe.
|
| 3 |
+
# ์ฐธ์กฐ FM๊ณผ OpenPath๋ฅผ ๋์ผ ํ๋กํ ์ฝ๋ก ๋น๊ต.
|
| 4 |
+
# model: openpath|openmidnight|phikon|uni|uni2|gigapath|virchow2
|
| 5 |
+
# bench: pcam|crc|bach
|
| 6 |
+
# ์ฌ์ฉ: [GPU=0] [N_RUNS=5] bash eval/run_patch_eval.sh <model> <bench> [weights]
|
| 7 |
+
# openpath๋ weights(teacher_checkpoint.pth) ํ์. evaยทtimmยท๋ฐ์ดํฐ์
์ ์ฌ์ ์ค์น/๋ค์ด๋ก๋ ํ์.
|
| 8 |
+
set -u
|
| 9 |
+
R="${OPENPATH_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}" # repo ๋ฃจํธ(eval/์ ์์)
|
| 10 |
+
cd "$R"
|
| 11 |
+
export PYTHONPATH="$R/OpenPath:$R/eval" # dinov2(OpenPath/) + backbone(eval/)
|
| 12 |
+
export CUDA_VISIBLE_DEVICES="${GPU:-0}" # eva๋ ๋จ์ผ GPU ๊ถ์ฅ
|
| 13 |
+
export N_RUNS="${N_RUNS:-5}"
|
| 14 |
+
export ACCELERATOR=gpu NUM_DEVICES=1 OPENPATH_WEIGHTS="${3:-none}"
|
| 15 |
+
export PREDICT_BATCH_SIZE="${PREDICT_BATCH_SIZE:-512}" BATCH_SIZE="${BATCH_SIZE:-4096}" N_DATA_WORKERS="${N_DATA_WORKERS:-8}"
|
| 16 |
+
model="$1"; bench="$2"
|
| 17 |
+
|
| 18 |
+
case "$model" in
|
| 19 |
+
openpath) export BACKBONE_FN=openpath_eva_backbone.build_openpath IN_FEATURES=1536 ;;
|
| 20 |
+
openmidnight) export BACKBONE_FN=openpath_eva_backbone.build_openmidnight IN_FEATURES=1536 ;;
|
| 21 |
+
phikon) export BACKBONE_FN=openpath_eva_backbone.build_phikon IN_FEATURES=1024 ;;
|
| 22 |
+
uni) export BACKBONE_FN=openpath_eva_backbone.build_uni IN_FEATURES=1024 ;;
|
| 23 |
+
uni2) export BACKBONE_FN=openpath_eva_backbone.build_uni2 IN_FEATURES=1536 ;;
|
| 24 |
+
gigapath) export BACKBONE_FN=openpath_eva_backbone.build_gigapath IN_FEATURES=1536 ;;
|
| 25 |
+
virchow2) export BACKBONE_FN=openpath_eva_backbone.build_virchow2 IN_FEATURES=1280 ;;
|
| 26 |
+
*) echo "unknown model $model (openpath|openmidnight|phikon|uni|uni2|gigapath|virchow2)"; exit 1 ;;
|
| 27 |
+
esac
|
| 28 |
+
export MODEL_NAME="${MODEL_NAME:-$model}"
|
| 29 |
+
|
| 30 |
+
case "$bench" in
|
| 31 |
+
pcam) cfg=eval/eva_configs/patch_camelyon.yaml; export DATA_ROOT="$R/data/eva/pcam_h5" DOWNLOAD_DATA="${DL:-true}" ;;
|
| 32 |
+
crc) cfg=eval/eva_configs/crc.yaml; export DATA_ROOT="$R/data/eva/crc" DOWNLOAD_DATA=false ;;
|
| 33 |
+
bach) cfg=eval/eva_configs/bach.yaml; export DATA_ROOT="$R/data/eva/bach" DOWNLOAD_DATA="${DL:-false}" ;;
|
| 34 |
+
*) echo "unknown bench $bench (pcam|crc|bach)"; exit 1 ;;
|
| 35 |
+
esac
|
| 36 |
+
|
| 37 |
+
# eva predict_fit๋ ์๋ฒ ๋ฉ ์ถ๋ ฅํด๋๊ฐ ์์ผ๋ฉด ๊ฑฐ๋ถ โ ๋งค ์คํ ์ ์ ๋ฆฌ(fresh ์ถ์ถ).
|
| 38 |
+
# eva ์๋ฒ ๋ฉ ํด๋๋ช
์ ๋ฐ์ดํฐ์
๋ช
: pcamโpatch_camelyon.
|
| 39 |
+
evadir="$bench"; [ "$bench" = pcam ] && evadir=patch_camelyon
|
| 40 |
+
rm -rf "$R/data/embeddings/$MODEL_NAME/$evadir" 2>/dev/null
|
| 41 |
+
|
| 42 |
+
echo "=== EVAL $model ร $bench | IN=$IN_FEATURES N_RUNS=$N_RUNS GPU=$CUDA_VISIBLE_DEVICES DATA_ROOT=$DATA_ROOT DL=$DOWNLOAD_DATA | $(date) ==="
|
| 43 |
+
eva predict_fit --config "$cfg"
|
| 44 |
+
echo "=== DONE $model ร $bench | $(date) ==="
|
eval/st_bench.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""HEST-style spatial-transcriptomics(ST) ๋ฒค์น๋งํฌ โ in-house Visium ์ฝํธํธ.
|
| 3 |
+
๊ฐ spot์์ WSI ํจ์น ์ถ์ถ โ FM ์๋ฒ ๋ฉ(CLS) โ PCA โ Ridge ํ๊ท๋ก ์์ HVG ๋ฐํ ์์ธก
|
| 4 |
+
โ Pearson ์๊ด(์ ์ ์ํ๊ท ). Leave-one-patient-out CV.
|
| 5 |
+
|
| 6 |
+
์ฌ์ฉ:
|
| 7 |
+
PYTHONPATH=OpenPath:eval venv_eva/bin/python eval/st_bench.py \
|
| 8 |
+
--backbone openmidnight # ์ฐธ์กฐ: OpenMidnight
|
| 9 |
+
... --backbone phikon
|
| 10 |
+
... --backbone openpath --weights data/runs/openpath_run/eval/training_316250/teacher_checkpoint.pth
|
| 11 |
+
"""
|
| 12 |
+
import os, sys, json, glob, argparse, re
|
| 13 |
+
import numpy as np, pandas as pd
|
| 14 |
+
import torch, openslide
|
| 15 |
+
from PIL import Image
|
| 16 |
+
from sklearn.linear_model import Ridge
|
| 17 |
+
from sklearn.decomposition import PCA
|
| 18 |
+
from sklearn.preprocessing import StandardScaler
|
| 19 |
+
from scipy.stats import pearsonr
|
| 20 |
+
import torchvision.transforms as T
|
| 21 |
+
|
| 22 |
+
ROOT = os.environ.get("OPENPATH_ROOT", os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # repo ๋ฃจํธ(eval/์ ์์)
|
| 23 |
+
DATA = os.environ.get("ST_ROOT", f"{ROOT}/data/st_bench") # in-house Visium ST ์ฝํธํธ(๋น๊ณต๊ฐ; ์ฝ๋๋ง ๊ณต๊ฐ)
|
| 24 |
+
IMAGENET_MEAN = (0.485, 0.456, 0.406); IMAGENET_STD = (0.229, 0.224, 0.225)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def build_backbone(name, weights):
|
| 28 |
+
sys.path.insert(0, f"{ROOT}/OpenPath"); sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 29 |
+
import openpath_eva_backbone as B
|
| 30 |
+
ref = {"uni": (B.build_uni, 1024), "uni2": (B.build_uni2, 1536),
|
| 31 |
+
"gigapath": (B.build_gigapath, 1536),
|
| 32 |
+
"virchow2": (B.build_virchow2, 1280),
|
| 33 |
+
"phikon": (B.build_phikon, 1024),
|
| 34 |
+
"openmidnight": (B.build_openmidnight, 1536)}
|
| 35 |
+
if name in ref:
|
| 36 |
+
fn, dim = ref[name]; return fn(), dim
|
| 37 |
+
# openpath: ์ฐ๋ฆฌ teacher_checkpoint(dinov2 teacher ํฌ๋งท) ๋ก๋
|
| 38 |
+
return B.build_openpath(weights), 1536
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def slide_dirs():
|
| 42 |
+
return sorted([d for d in glob.glob(f"{DATA}/*") if os.path.isdir(d)])
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def patient_of(slide_dir):
|
| 46 |
+
b = os.path.basename(slide_dir) # ์: pt<N>-<M> ๋๋ pt<N>
|
| 47 |
+
m = re.match(r"(pt\d+)", b)
|
| 48 |
+
return m.group(1) if m else b # pt<N>-<M>, pt<N>-<K> โ ๋์ผ ํ์ pt<N>
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def load_one(slide_dir):
|
| 52 |
+
exp_f = glob.glob(f"{slide_dir}/*.spatial.data.exp.csv")
|
| 53 |
+
pos_f = glob.glob(f"{slide_dir}/*.tissue_positions_fullres.csv")
|
| 54 |
+
sf_f = glob.glob(f"{slide_dir}/*.scalefactors_json.json")
|
| 55 |
+
wsi_f = glob.glob(f"{slide_dir}/*_p0.tif")
|
| 56 |
+
if not (exp_f and pos_f and sf_f and wsi_f):
|
| 57 |
+
return None
|
| 58 |
+
exp = pd.read_csv(exp_f[0], index_col=0) # spots ร genes (log-norm)
|
| 59 |
+
pos = pd.read_csv(pos_f[0], index_col=0) # barcode โ pxl coords
|
| 60 |
+
sf = json.load(open(sf_f[0]))
|
| 61 |
+
diam = int(round(sf["spot_diameter_fullres"])) # ~147px
|
| 62 |
+
common = exp.index.intersection(pos.index)
|
| 63 |
+
exp = exp.loc[common]; pos = pos.loc[common]
|
| 64 |
+
return dict(dir=slide_dir, exp=exp, pos=pos, diam=diam, wsi=wsi_f[0])
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _load_patches(sl):
|
| 68 |
+
"""224 uint8 ํจ์น (N,224,224,3). ์ฌ๋ผ์ด๋๋ณ ์บ์ ์ฌ์ฌ์ฉ(์ฒดํฌํฌ์ธํธ๋ง๋ค ๋์ผ ํจ์น)."""
|
| 69 |
+
import numpy as np
|
| 70 |
+
cache = f"{DATA}/_pcache/{os.path.basename(sl['dir'])}.pt"
|
| 71 |
+
if os.path.exists(cache):
|
| 72 |
+
try: return torch.load(cache)
|
| 73 |
+
except Exception: pass
|
| 74 |
+
sldx = openslide.OpenSlide(sl["wsi"]); d = sl["diam"]
|
| 75 |
+
rs = T.Resize((224, 224))
|
| 76 |
+
xs = sl["pos"]["pxl_x_in_fullres"].values.astype(int)
|
| 77 |
+
ys = sl["pos"]["pxl_y_in_fullres"].values.astype(int)
|
| 78 |
+
plist = []
|
| 79 |
+
for x, y in zip(xs, ys):
|
| 80 |
+
patch = sldx.read_region((int(x - d // 2), int(y - d // 2)), 0, (d, d)).convert("RGB")
|
| 81 |
+
plist.append(torch.from_numpy(np.asarray(rs(patch)))) # (224,224,3) uint8
|
| 82 |
+
sldx.close()
|
| 83 |
+
patches = torch.stack(plist)
|
| 84 |
+
os.makedirs(os.path.dirname(cache), exist_ok=True)
|
| 85 |
+
tmp = cache + f".tmp{os.getpid()}"
|
| 86 |
+
torch.save(patches, tmp); os.replace(tmp, cache) # atomic
|
| 87 |
+
return patches
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@torch.no_grad()
|
| 91 |
+
def embed_slide(sl, model, device, bs=256):
|
| 92 |
+
patches = _load_patches(sl) # (N,224,224,3) uint8
|
| 93 |
+
mean = torch.tensor(IMAGENET_MEAN).view(1, 3, 1, 1)
|
| 94 |
+
std = torch.tensor(IMAGENET_STD).view(1, 3, 1, 1)
|
| 95 |
+
embs = []
|
| 96 |
+
for i in range(0, len(patches), bs):
|
| 97 |
+
b = patches[i:i + bs].permute(0, 3, 1, 2).float().div_(255.0) # (B,3,224,224)
|
| 98 |
+
b = ((b - mean) / std).to(device)
|
| 99 |
+
embs.append(model(b).float().cpu().numpy())
|
| 100 |
+
return np.concatenate(embs, 0) # (n_spots, dim)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def top_hvg(exp_all, k=50):
|
| 104 |
+
# ํ์ต์
์ ์ฒด log-norm ๋ฐํ์ ๋ถ์ฐ ์์ k ์ ์ ์
|
| 105 |
+
v = exp_all.var(axis=0)
|
| 106 |
+
return v.sort_values(ascending=False).index[:k].tolist()
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def main():
|
| 110 |
+
ap = argparse.ArgumentParser()
|
| 111 |
+
ap.add_argument("--backbone", required=True, choices=["openpath","openmidnight","phikon","uni","uni2","gigapath","virchow2"])
|
| 112 |
+
ap.add_argument("--weights", default=None)
|
| 113 |
+
ap.add_argument("--k-genes", type=int, default=50)
|
| 114 |
+
ap.add_argument("--pca", type=int, default=256)
|
| 115 |
+
ap.add_argument("--alpha", type=float, default=100.0)
|
| 116 |
+
ap.add_argument("--tag", default=None)
|
| 117 |
+
args = ap.parse_args()
|
| 118 |
+
device = "cuda"
|
| 119 |
+
|
| 120 |
+
model, dim = build_backbone(args.backbone, args.weights)
|
| 121 |
+
model = model.to(device).eval()
|
| 122 |
+
|
| 123 |
+
dirs = slide_dirs()
|
| 124 |
+
print(f"[st]slides={len(dirs)} backbone={args.backbone} dim={dim}", flush=True)
|
| 125 |
+
slides = []
|
| 126 |
+
for d in dirs:
|
| 127 |
+
sl = load_one(d)
|
| 128 |
+
if sl is None: print(f" skip {os.path.basename(d)} (ํ์ผ ๋ถ์กฑ)"); continue
|
| 129 |
+
sl["emb"] = embed_slide(sl, model, device)
|
| 130 |
+
sl["pat"] = patient_of(d)
|
| 131 |
+
slides.append(sl)
|
| 132 |
+
print(f" {os.path.basename(d)}: spots={len(sl['pos'])} emb={sl['emb'].shape} pat={sl['pat']}", flush=True)
|
| 133 |
+
|
| 134 |
+
# ๊ณตํต ์ ์ ์
|
| 135 |
+
genes = slides[0]["exp"].columns
|
| 136 |
+
for s in slides[1:]: genes = genes.intersection(s["exp"].columns)
|
| 137 |
+
genes = list(genes)
|
| 138 |
+
print(f"[st]๊ณตํต ์ ์ ์ {len(genes)}", flush=True)
|
| 139 |
+
|
| 140 |
+
pats = sorted(set(s["pat"] for s in slides))
|
| 141 |
+
# leave-one-patient-out
|
| 142 |
+
per_gene_corr = []
|
| 143 |
+
for held in pats:
|
| 144 |
+
tr = [s for s in slides if s["pat"] != held]
|
| 145 |
+
te = [s for s in slides if s["pat"] == held]
|
| 146 |
+
Xtr = np.concatenate([s["emb"] for s in tr], 0)
|
| 147 |
+
Ytr = np.concatenate([s["exp"][genes].values for s in tr], 0)
|
| 148 |
+
Xte = np.concatenate([s["emb"] for s in te], 0)
|
| 149 |
+
Yte = np.concatenate([s["exp"][genes].values for s in te], 0)
|
| 150 |
+
# HVG๋ ํ์ต์
์ ์ ํ
|
| 151 |
+
hvg_idx = np.argsort(-Ytr.var(0))[:args.k_genes]
|
| 152 |
+
Ytr_h, Yte_h = Ytr[:, hvg_idx], Yte[:, hvg_idx]
|
| 153 |
+
# ํ์คํ + PCA + Ridge
|
| 154 |
+
sc = StandardScaler().fit(Xtr)
|
| 155 |
+
Xtr2, Xte2 = sc.transform(Xtr), sc.transform(Xte)
|
| 156 |
+
p = PCA(n_components=min(args.pca, Xtr2.shape[1])).fit(Xtr2)
|
| 157 |
+
Xtr3, Xte3 = p.transform(Xtr2), p.transform(Xte2)
|
| 158 |
+
reg = Ridge(alpha=args.alpha).fit(Xtr3, Ytr_h)
|
| 159 |
+
pred = reg.predict(Xte3)
|
| 160 |
+
cors = []
|
| 161 |
+
for g in range(Yte_h.shape[1]):
|
| 162 |
+
if Yte_h[:, g].std() < 1e-8 or pred[:, g].std() < 1e-8:
|
| 163 |
+
cors.append(0.0)
|
| 164 |
+
else:
|
| 165 |
+
cors.append(pearsonr(Yte_h[:, g], pred[:, g])[0])
|
| 166 |
+
m = float(np.nanmean(cors))
|
| 167 |
+
per_gene_corr.append(m)
|
| 168 |
+
print(f" [fold {held}] test_spots={Yte.shape[0]} meanPearson={m:.4f}", flush=True)
|
| 169 |
+
|
| 170 |
+
overall = float(np.mean(per_gene_corr))
|
| 171 |
+
tag = args.tag or args.backbone
|
| 172 |
+
print(f"[ST RESULT] backbone={tag} | LOPO mean Pearson = {overall:.4f} (folds={len(pats)}, HVG={args.k_genes})", flush=True)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
if __name__ == "__main__":
|
| 176 |
+
main()
|