New model card: GrapHist v2 only, shorter, matches the new layout
Browse files
README.md
CHANGED
|
@@ -9,206 +9,154 @@ tags:
|
|
| 9 |
- pytorch-geometric
|
| 10 |
- graph-representation-learning
|
| 11 |
- edge-features
|
| 12 |
-
- vicreg
|
| 13 |
---
|
| 14 |
|
| 15 |
-
# GrapHist v2
|
| 16 |
|
| 17 |
-
GrapHist v2
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
edge
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
|
| 28 |
-
|
| 29 |
-
error) plus the VICReg term on the pooled graph embedding.
|
| 30 |
|
| 31 |
-
##
|
| 32 |
|
| 33 |
-
|
| 34 |
-
graphist_V2.pt # final v2+VICReg checkpoint - 126,597,010 B, md5 81a2e6b91cefff0bbbc13c6fd318ee78
|
| 35 |
-
models/
|
| 36 |
-
├── __init__.py # build_model(args) factory
|
| 37 |
-
├── edcoder.py # PreModel encoder-decoder wrapper
|
| 38 |
-
├── acm_gineconv.py # ACM-GINEConv backbone (v2, edge-informed)
|
| 39 |
-
├── acm_gin.py # ACM-GIN backbone (v1) - REQUIRED IMPORT, see note
|
| 40 |
-
└── utils.py # activation helpers
|
| 41 |
-
graphist_utils.py # NormalizeData + AddVirtualNode transforms, GraphDataset,
|
| 42 |
-
# filter_valid_graph_paths, checkpoint helpers - REQUIRED
|
| 43 |
-
rebase_graph_paths.py # REQUIRED before using data/v2_graphs/ - see data/README.md
|
| 44 |
-
checkpoints/
|
| 45 |
-
└── graphist_v1.pt # vanilla GrapHist v1 (ACM-GIN, 1-dim edges) - AdapterGNN starting point
|
| 46 |
-
data/ # all datasets and embeddings - see data/README.md for the full
|
| 47 |
-
# artifact -> purpose -> branch -> paper-table map
|
| 48 |
-
├── v2_graphs/ # 75-dim edge cell graphs (BACH, BRACS, BreakHis)
|
| 49 |
-
├── v2_embeddings/ # slide (4 cohorts) + cell (22 tars) embeddings from graphist_V2.pt
|
| 50 |
-
├── v1_graphs/ # NuCLS + PanNuke 1-dim edge graphs (AdapterGNN, homophily)
|
| 51 |
-
├── v1_graphs_upstream/ # byte-identical mirrors of the upstream v1 releases:
|
| 52 |
-
│ # BRACS + BreakHis tars, and tcga_brca/ (254 files, 271 GB)
|
| 53 |
-
├── homophily_tiles/ # 109,904 CellViT++ pseudo-labelled 224px tile graphs
|
| 54 |
-
├── baseline_embeddings/# DINOv2, MAE, GrapHist-v1 TCGA-BRCA features (Table 3 baselines)
|
| 55 |
-
├── homophily_bach_cellvit/ # 400 per-image CellViT pseudo-label CSVs (BACH fat-tail)
|
| 56 |
-
├── preproc_final/ # Table C.8 preprocessing-runtime run (19,200 BACH tiles)
|
| 57 |
-
└── survival/ # GDC TCGA-BRCA clinical.tsv - required for Table 3
|
| 58 |
-
README.md
|
| 59 |
-
```
|
| 60 |
-
|
| 61 |
-
**Which files do I need?** For inference only: `graphist_V2.pt`, `models/`,
|
| 62 |
-
`graphist_utils.py`. Everything under `data/` is there to reproduce a specific published
|
| 63 |
-
number — [`data/README.md`](data/README.md) says which one, for which branch.
|
| 64 |
-
|
| 65 |
-
⚠️ **If you use `data/v2_graphs/`, you must run `rebase_graph_paths.py` first**, or the
|
| 66 |
-
loader drops every graph — non-fatally, with a single `WARNING:` line, then an unhelpful
|
| 67 |
-
`IndexError`. `data/README.md` has the exact commands and the expected counts to check
|
| 68 |
-
against.
|
| 69 |
-
|
| 70 |
-
> **`acm_gin.py` is required even though v2 never uses it.** `models/edcoder.py` imports
|
| 71 |
-
> `ACM_GIN_model` and `ACM_GINEConv_model` at module top level; omitting it is an `ImportError`.
|
| 72 |
-
|
| 73 |
-
> **`graphist_utils.py` is required.** Reproducing the published embeddings needs
|
| 74 |
-
> `Compose([ToUndirected(), NormalizeData(...), AddVirtualNode(...)])`, and neither
|
| 75 |
-
> transform lives in `models/`.
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
differ. So the old "which of the four?" question is moot: any of them yields the same encoder.
|
| 83 |
|
| 84 |
-
|
| 85 |
-
that produced every reported v2+VICReg number:
|
| 86 |
-
|
| 87 |
-
| Field | Value |
|
| 88 |
-
|---|---|
|
| 89 |
-
| `epoch` | 99 |
|
| 90 |
-
| `best_loss` | 0.019939 |
|
| 91 |
-
| `run_id` | `1udpmgxw` |
|
| 92 |
-
| md5 | `81a2e6b91cefff0bbbc13c6fd318ee78` |
|
| 93 |
-
|
| 94 |
-
Parameters: **encoder 7,980,649 (7.98 M)**; **9.29 M on the inference path**, because
|
| 95 |
-
`embed()` ends with `rep = self.encoder_to_decoder(enc_rep)` (`edcoder.py:324`), adding
|
| 96 |
-
1,310,720; **10,528,542 (10.53 M)** for the full pretraining model (encoder +
|
| 97 |
-
`encoder_to_decoder` + decoder 1,237,077 + mask token 96, counting each shared module once).
|
| 98 |
-
A naive `state_dict` sum reports 19.35 M because `acm_gineconv.py` registers each channel
|
| 99 |
-
MLP twice — 156 of the 332 keys are aliases of the same tensors (332 keys, 176 unique
|
| 100 |
-
storages).
|
| 101 |
-
|
| 102 |
-
## Requirements
|
| 103 |
|
| 104 |
```
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
```
|
| 109 |
-
Verified on torch 2.10.0+cu128 / PyG 2.7.0. CPU-only inference works.
|
| 110 |
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
##
|
| 114 |
|
| 115 |
```python
|
| 116 |
from huggingface_hub import snapshot_download
|
| 117 |
path = snapshot_download(repo_id="Ace3Z/graphist-v2")
|
| 118 |
```
|
| 119 |
|
| 120 |
-
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
| 127 |
-
|---|---|---|---|
|
| 128 |
-
| `edge_distance_in_proj` | `False` | `getattr` default `True` (`models/__init__.py:29`) | `edge_input_proj` built as (512, **75**); checkpoint has (512, **74**) |
|
| 129 |
-
| `encoder_norm` | `"layer"` | `getattr` default `"none"` (`models/__init__.py:22`) | `encoder.layer_norms.*` missing from the model |
|
| 130 |
-
| `concat_hidden` | `True` | **`AttributeError`** — `models/__init__.py:40` is `args.concat_hidden`, a plain attribute read with no default | if instead set to `False`: `encoder_to_decoder` built as (512, 512); checkpoint has (512, **2560**) |
|
| 131 |
-
|
| 132 |
-
(The `generate_embs.py` CLI defaults `--concat_hidden` to `True`, and `False` appears only as
|
| 133 |
-
a `PreModel.__init__` signature default that the `Args` flow below never reaches — so
|
| 134 |
-
`concat_hidden` has no effective "silent wrong default", it simply must be present.)
|
| 135 |
|
| 136 |
```python
|
| 137 |
import sys, torch
|
| 138 |
-
sys.path.insert(0, path)
|
| 139 |
from models import build_model
|
| 140 |
|
| 141 |
class Args:
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
num_features = 96 # node features
|
| 146 |
-
num_edge_features = 75 # edge features (projection sees 74; distance excluded)
|
| 147 |
num_hidden = 512
|
| 148 |
num_layers = 5
|
| 149 |
-
concat_hidden = True
|
| 150 |
-
encoder_norm = "layer"
|
|
|
|
| 151 |
input_norm = "none"
|
| 152 |
-
edge_distance_in_proj = False # load-critical
|
| 153 |
batchnorm = False
|
| 154 |
activation = "prelu"
|
| 155 |
norm = None
|
| 156 |
residual = False
|
| 157 |
-
# --- SSL objective ---
|
| 158 |
loss_fn = "sce"
|
| 159 |
alpha_l = 3
|
| 160 |
mask_rate = 0.5
|
| 161 |
replace_rate = 0.1
|
| 162 |
drop_edge_rate = 0.0
|
| 163 |
-
|
| 164 |
-
vicreg_var_weight = 0.05
|
| 165 |
vicreg_cov_weight = 0.002
|
| 166 |
vicreg_gamma = 1.0
|
| 167 |
-
|
| 168 |
-
num_heads = 4
|
| 169 |
num_out_heads = 1
|
| 170 |
in_drop = 0.2
|
| 171 |
attn_drop = 0.1
|
| 172 |
negative_slope = 0.2
|
| 173 |
|
| 174 |
model = build_model(Args())
|
| 175 |
-
ckpt = torch.load(f"{path}/
|
| 176 |
model.load_state_dict(ckpt["model_state_dict"], strict=True) # note the key name
|
| 177 |
model.eval()
|
| 178 |
-
```
|
| 179 |
|
| 180 |
-
|
| 181 |
-
best_loss, run_id}`. The weights are under **`model_state_dict`**, not `model`.
|
| 182 |
-
|
| 183 |
-
### 3. Inference
|
| 184 |
-
|
| 185 |
-
```python
|
| 186 |
-
# x: [num_nodes, 96] edge_index: [2, num_edges] edge_attr: [num_edges, 75]
|
| 187 |
# batch: [num_nodes] graph assignment (zeros for a single graph)
|
| 188 |
with torch.no_grad():
|
| 189 |
-
node_emb = model.embed(x, edge_index, edge_attr, batch)
|
| 190 |
```
|
| 191 |
|
| 192 |
-
Region
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
`AddVirtualNode`) — see `graphist_utils.py`.
|
| 196 |
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
| 202 |
|
| 203 |
-
|
|
|
|
| 204 |
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
-
##
|
| 210 |
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
```bibtex
|
| 214 |
@article{ogut2026graphist,
|
|
@@ -222,3 +170,6 @@ The v2 preprint is not yet available. Please cite the original GrapHist paper:
|
|
| 222 |
primaryClass = {cs.CV}
|
| 223 |
}
|
| 224 |
```
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
- pytorch-geometric
|
| 10 |
- graph-representation-learning
|
| 11 |
- edge-features
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# GrapHist v2 — Edge-Informed Graph Self-Supervised Learning for Histopathology
|
| 15 |
|
| 16 |
+
GrapHist v2 encodes a histopathology tile as a **cell graph** — one node per nucleus, edges
|
| 17 |
+
from a Delaunay triangulation pruned at 100 µm — and embeds it with a self-supervised
|
| 18 |
+
**ACM-GINEConv** encoder. It extends [GrapHist](https://huggingface.co/ogutsevda/graphist)
|
| 19 |
+
([arXiv:2603.00143](https://arxiv.org/abs/2603.00143)) by making the **edges carry
|
| 20 |
+
information**: each edge holds a 75-dimensional descriptor of the tissue between two cells,
|
| 21 |
+
and the encoder injects those features into message passing instead of treating edges as mere
|
| 22 |
+
connectivity.
|
| 23 |
|
| 24 |
+
**Trained on 11,149,499 cell graphs from TCGA-BRCA**, 100 epochs, batch 2048, no labels. The
|
| 25 |
+
objective is GraphMAE-style masked node-feature reconstruction (mask 50 %, scaled cosine
|
| 26 |
+
error, α = 3) plus a **VICReg** variance/covariance term on the pooled graph embedding, which
|
| 27 |
+
keeps all embedding dimensions in use (final `pca_1` 0.17, effective dimension 11.9).
|
| 28 |
|
| 29 |
+
7.98 M-parameter encoder · 512-dim embeddings · 0.093 ms/patch inference.
|
|
|
|
| 30 |
|
| 31 |
+
## Results
|
| 32 |
|
| 33 |
+
Slide-level MIL, transfer to unseen cohorts (test macro-F1 %, best of three MIL heads):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
+
| | BRACS | BreakHis | BACH |
|
| 36 |
+
|---|:--:|:--:|:--:|
|
| 37 |
+
| GrapHist (v1) | 60.30 | 89.37 | **69.16** |
|
| 38 |
+
| **GrapHist v2** | **69.00** | **95.53** | 68.98 |
|
| 39 |
|
| 40 |
+
Survival on TCGA-BRCA (Cox PH, C-index): **0.793**, vs 0.763 for v1, 0.724 MAE, 0.632 DINOv2.
|
| 41 |
+
Cell-type identification and patch-level subtyping are in [`DATA.md`](DATA.md), which maps
|
| 42 |
+
every artifact here to the result it reproduces.
|
|
|
|
| 43 |
|
| 44 |
+
## What's in this repo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
```
|
| 47 |
+
graphist_v2.pt the model — 126,597,010 B, md5 81a2e6b91cefff0bbbc13c6fd318ee78
|
| 48 |
+
modeling/ code to load and run it, plus helper scripts
|
| 49 |
+
graphs/ cell graphs: TCGA-BRCA (11.1 M, pre-training) · BACH · BRACS ·
|
| 50 |
+
BreakHis · SPIDER-breast
|
| 51 |
+
embeddings/ embeddings from this model: slide-level (4 cohorts), cell-level (22 sets)
|
| 52 |
+
baselines/ DINOv2, MAE and GrapHist v1 features for the comparison rows
|
| 53 |
+
labels/ TCGA-BRCA clinical table and slide labels
|
| 54 |
+
studies/ side analyses: homophily, AdapterGNN, batch effect, preprocessing runtime
|
| 55 |
+
upstream_v1/ mirror of the original GrapHist v1 releases
|
| 56 |
```
|
|
|
|
| 57 |
|
| 58 |
+
For inference you need only `graphist_v2.pt` + `modeling/`. Everything else exists to reproduce
|
| 59 |
+
a specific published number — see [`DATA.md`](DATA.md).
|
| 60 |
+
|
| 61 |
+
> ⚠️ **If you use `graphs/`, run `modeling/rebase_graph_paths.py` first.** The label CSVs store
|
| 62 |
+
> bare filenames and the loader resolves them against the working directory, so without this
|
| 63 |
+
> step every graph is silently dropped. `DATA.md` has the commands and the expected counts.
|
| 64 |
|
| 65 |
+
## Usage
|
| 66 |
|
| 67 |
```python
|
| 68 |
from huggingface_hub import snapshot_download
|
| 69 |
path = snapshot_download(repo_id="Ace3Z/graphist-v2")
|
| 70 |
```
|
| 71 |
|
| 72 |
+
Three arguments are **load-critical** — `build_model` reads two with defaults that do not match
|
| 73 |
+
this checkpoint, and the third has no default at all:
|
| 74 |
|
| 75 |
+
| Argument | Must be | If wrong |
|
| 76 |
+
|---|---|---|
|
| 77 |
+
| `edge_distance_in_proj` | `False` | `edge_input_proj` built as (512, **75**); checkpoint has (512, **74**) |
|
| 78 |
+
| `encoder_norm` | `"layer"` | `encoder.layer_norms.*` missing from the model |
|
| 79 |
+
| `concat_hidden` | `True` | `AttributeError` if omitted; (512, 512) instead of (512, **2560**) if `False` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
```python
|
| 82 |
import sys, torch
|
| 83 |
+
sys.path.insert(0, f"{path}/modeling")
|
| 84 |
from models import build_model
|
| 85 |
|
| 86 |
class Args:
|
| 87 |
+
encoder = decoder = "acm_gineconv"
|
| 88 |
+
num_features = 96 # per-cell features
|
| 89 |
+
num_edge_features = 75 # per-edge features (projection sees 74; distance excluded)
|
|
|
|
|
|
|
| 90 |
num_hidden = 512
|
| 91 |
num_layers = 5
|
| 92 |
+
concat_hidden = True # load-critical
|
| 93 |
+
encoder_norm = "layer" # load-critical
|
| 94 |
+
edge_distance_in_proj = False # load-critical
|
| 95 |
input_norm = "none"
|
|
|
|
| 96 |
batchnorm = False
|
| 97 |
activation = "prelu"
|
| 98 |
norm = None
|
| 99 |
residual = False
|
|
|
|
| 100 |
loss_fn = "sce"
|
| 101 |
alpha_l = 3
|
| 102 |
mask_rate = 0.5
|
| 103 |
replace_rate = 0.1
|
| 104 |
drop_edge_rate = 0.0
|
| 105 |
+
vicreg_var_weight = 0.05 # training only
|
|
|
|
| 106 |
vicreg_cov_weight = 0.002
|
| 107 |
vicreg_gamma = 1.0
|
| 108 |
+
num_heads = 4 # vestigial, but read by build_model
|
|
|
|
| 109 |
num_out_heads = 1
|
| 110 |
in_drop = 0.2
|
| 111 |
attn_drop = 0.1
|
| 112 |
negative_slope = 0.2
|
| 113 |
|
| 114 |
model = build_model(Args())
|
| 115 |
+
ckpt = torch.load(f"{path}/graphist_v2.pt", map_location="cpu", weights_only=False)
|
| 116 |
model.load_state_dict(ckpt["model_state_dict"], strict=True) # note the key name
|
| 117 |
model.eval()
|
|
|
|
| 118 |
|
| 119 |
+
# x: [num_nodes, 96] edge_index: [2, num_edges] edge_attr: [num_edges, 75]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
# batch: [num_nodes] graph assignment (zeros for a single graph)
|
| 121 |
with torch.no_grad():
|
| 122 |
+
node_emb = model.embed(x, edge_index, edge_attr, batch) # -> [num_nodes, 512]
|
| 123 |
```
|
| 124 |
|
| 125 |
+
Region- and slide-level embeddings are means over node embeddings. To match the published
|
| 126 |
+
numbers, apply the transforms used at training time — `NormalizeData` with the dataset's
|
| 127 |
+
`normalization.json`, then `AddVirtualNode` (both in `modeling/graphist_utils.py`).
|
|
|
|
| 128 |
|
| 129 |
+
Requires `torch >= 2.2`, `torch_geometric >= 2.5`, `numpy`, `pandas`. Verified on torch
|
| 130 |
+
2.10.0+cu128 / PyG 2.7.0; CPU-only inference works.
|
| 131 |
+
|
| 132 |
+
> `modeling/models/acm_gin.py` is required even though this model never uses it —
|
| 133 |
+
> `edcoder.py` imports it at module level.
|
| 134 |
+
|
| 135 |
+
## Graph format
|
| 136 |
|
| 137 |
+
| Tensor | Shape | Meaning |
|
| 138 |
+
|---|---|---|
|
| 139 |
+
| `x` | `[num_nodes, 96]` | per-cell morphology, texture and colour features |
|
| 140 |
+
| `edge_index` | `[2, num_edges]` | Delaunay edges, pruned at 100 µm |
|
| 141 |
+
| `edge_attr` | `[num_edges, 75]` | column 0 = centroid distance (µm); 1–74 describe the inter-cellular region |
|
| 142 |
|
| 143 |
+
The v1 releases use a 1-dimensional `edge_attr` (distance only) and are **not** interchangeable
|
| 144 |
+
with this model in either direction.
|
| 145 |
|
| 146 |
+
## Verification
|
| 147 |
+
|
| 148 |
+
`build_model` + `load_state_dict(strict=True)` returns 0 missing / 0 unexpected keys using only
|
| 149 |
+
the files shipped here. All 397 BACH slide embeddings regenerate from this checkpoint at
|
| 150 |
+
minimum cosine similarity **0.9999999997** against the published set (a different checkpoint
|
| 151 |
+
gives ≈ 0), confirming these weights produced the released embeddings.
|
| 152 |
|
| 153 |
+
## Licence and citation
|
| 154 |
|
| 155 |
+
Released `cc-by-nc-sa-4.0`. Derived from public cohorts — BACH/ICIAR 2018, BreakHis, BRACS,
|
| 156 |
+
TCGA-BRCA, PanNuke, NuCLS and [SPIDER-breast](https://huggingface.co/datasets/histai/SPIDER-breast)
|
| 157 |
+
(`cc-by-nc-4.0`, research use only). These are derived cell-level features, not images; the
|
| 158 |
+
source cohorts keep their own terms, and `labels/tcga_brca_clinical.tsv` is the open-access GDC
|
| 159 |
+
clinical export. Cite the original cohort papers alongside GrapHist.
|
| 160 |
|
| 161 |
```bibtex
|
| 162 |
@article{ogut2026graphist,
|
|
|
|
| 170 |
primaryClass = {cs.CV}
|
| 171 |
}
|
| 172 |
```
|
| 173 |
+
|
| 174 |
+
Built on GraphMAE (Hou et al., 2022), ACM (Luan et al., 2022), GINEConv (Hu et al., 2020) and
|
| 175 |
+
VICReg (Bardes et al., 2022). Developed at [LTS4, EPFL](https://www.epfl.ch/labs/lts4/).
|