Download 43_rnafm_se600m.py from Pathstudy/fourmodal-pathology-bind: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/Pathstudy/fourmodal-pathology-bind/resolve/main/43_rnafm_se600m.py
- Command line
-
hf download hf://Pathstudy/fourmodal-pathology-bind/43_rnafm_se600m.py
-
curl -L -o 43_rnafm_se600m.py https://huggingface.co/Pathstudy/fourmodal-pathology-bind/resolve/main/43_rnafm_se600m.py
14.8 kB
| """Frozen Arc Institute STATE embedding (SE-600M) spot embeddings of the 5 RNA sets (each spot = one pseudo-cell). | |
| python 43_rnafm_se600m.py [set ...] (default: all of rna_sets.SETS) | |
| Model : arcinstitute/SE-600M (HF), revision 5a9a80f44f7ce32ce57059933ef0d735d7c10ce5, files model.safetensors | |
| (metadata: converted from se600m_epoch16.ckpt, i.e. the final 16-epoch checkpoint), config.yaml and | |
| protein_embeddings.pt (19,790 gene symbols -> ESM2-15B mean protein embedding, 5120-d; identical, in | |
| the same order, to the checkpoint's pe_embedding.weight -- asserted). Licence: Arc Research Institute | |
| State Model Non-Commercial License + Acceptable Use Policy (weights; non-commercial research use only), | |
| code CC BY-NC-SA 4.0. Official code: github.com/ArcInstitute/state (commit 9bbfe78a, cloned to | |
| ext/state/state, read but NOT imported / installed): the forward pass of src/state/emb/nn/model.py | |
| (StateEmbeddingModel) + nn/flash_transformer.py is re-implemented below and loaded with a strict | |
| state-dict load of all 238 tensors (the encoder / gene_embedding_layer tie is asserted); the unused | |
| heads (binary_decoder, dataset_encoder, gene_embedding_layer) are dropped after loading. | |
| 16-layer post-LN transformer, d=2048, 16 heads, FFN 2048, GELU, no positional encoding, no attention mask. | |
| Inputs : rna_sets.load(set) raw(-like) counts + HGNC symbols. Genes = the set's genes whose symbol is a key of | |
| protein_embeddings.pt (official FilteredGenesCounts valid mask, set gene order kept); other genes dropped. | |
| Preprocess: official `state emb transform` path = Inference.encode_adata -> create_dataloader -> | |
| VCIDatasetSentenceCollator.sample_cell_sentences (is_train=False): | |
| * counts are raw -> log1p (the official per-cell raw-vs-log1p heuristic, max>35 or sum(expm1)>5e6, is | |
| bypassed because all inputs are known raw counts; how many spots it would misclassify is logged); | |
| * expression weight w = log1p / sum(log1p) over the valid genes (uniform if the spot is all-zero); | |
| * cell sentence (pad_length 2048) = [CLS] + 2047 genes ranked by descending expression with random | |
| tie-breaking (every set has > 2047 valid genes, so: all detected genes in rank order, top 2047 if more, | |
| then randomly drawn undetected genes without replacement); seeded numpy RNG (seed 0) instead of | |
| torch.randperm; | |
| * token count value = 100 * w[gene]; position 0 carries 100 * w[local gene index 3] exactly as the | |
| official collator (cls_token_idx = 3 is written as a local gene index before the count lookup); | |
| * token embedding = L2-normalised ESM2 embedding, position 0 replaced by the learned cls_token, the | |
| learned dataset_token appended (count embedding 0) -> 2049 tokens. | |
| Forward : encoder(Linear 5120->2048, LayerNorm, SiLU) * sqrt(2048) + soft-binned count embedding | |
| (softmax(count_encoder(c)) @ bin_encoder) -> 16 layers -> decoder (SkipBlock + Linear). | |
| Whole model in bf16 (official get_precision_config('cuda') casts the model to bf16 + bf16 autocast). | |
| Readout : official embedding = L2-normalised decoder output at the CLS position (StateEmbeddingModel.forward | |
| `embedding`), D = 2048. `state emb transform` additionally concatenates a 10-d dataset-token vector | |
| (dataset_embedder(decoder output at the dataset token)) -> 2058-d X_state; that batch-identity part is NOT | |
| saved here (only the 2048-d cell CLS embedding). | |
| Output : rna_sets.save("se600m", set, E) -> cache/rnafm/se600m_<set>.npy (n_spot, 2048) float16. | |
| Sanity : finite, mean pairwise cosine of 200 random spots, (hest) organ agreement of cosine 10-NN vs base rate, | |
| and (hest, 300 spots) cosine between two runs with different tie-breaking seeds (stochasticity of the | |
| undetected-gene filler tokens). | |
| """ | |
| import math | |
| import os | |
| import sys | |
| import time | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import yaml | |
| from huggingface_hub import hf_hub_download | |
| from safetensors.torch import load_file | |
| from common import CACHE | |
| from rna_sets import SETS, load, save | |
| MODEL = "se600m" | |
| REPO, REV = "arcinstitute/SE-600M", "5a9a80f44f7ce32ce57059933ef0d735d7c10ce5" | |
| dev = "cuda" | |
| DT = torch.bfloat16 | |
| # ----------------------------------------------------------------------------- model (re-implemented from ext/state) | |
| class SkipBlock(nn.Module): | |
| def __init__(self, d): | |
| super().__init__() | |
| self.intermediate_dense = nn.Linear(d, 2 * d) | |
| self.dense = nn.Linear(2 * d, d) | |
| self.layer_norm = nn.LayerNorm(d) | |
| def forward(self, x): | |
| return self.layer_norm(x + self.dense(F.relu(self.intermediate_dense(x)))) | |
| class Layer(nn.Module): # FlashTransformerEncoderLayer (post-LN, dropout 0 at inference) | |
| def __init__(self, d, nhead, d_hid): | |
| super().__init__() | |
| self.nhead = nhead | |
| self.qkv_proj = nn.Linear(d, 3 * d) | |
| self.out_proj = nn.Linear(d, d) | |
| self.norm1, self.norm2 = nn.LayerNorm(d), nn.LayerNorm(d) | |
| self.linear1, self.linear2 = nn.Linear(d, d_hid), nn.Linear(d_hid, d) | |
| def forward(self, x): | |
| B, T, D = x.shape | |
| q, k, v = self.qkv_proj(x).chunk(3, dim=-1) | |
| q, k, v = (t.view(B, T, self.nhead, D // self.nhead).transpose(1, 2) for t in (q, k, v)) | |
| a = F.scaled_dot_product_attention(q, k, v, is_causal=False).transpose(1, 2).reshape(B, T, D) | |
| x = self.norm1(x + self.out_proj(a)) | |
| return self.norm2(x + self.linear2(F.gelu(self.linear1(x)))) | |
| class TransformerEncoder(nn.Module): | |
| def __init__(self, layers): | |
| super().__init__() | |
| self.layers = nn.ModuleList(layers) | |
| class SE(nn.Module): | |
| def __init__(self, token_dim, d, nhead, d_hid, nlayers, out_dim, n_pe, z_dim, n_ds): | |
| super().__init__() | |
| self.d = d | |
| self.cls_token = nn.Parameter(torch.zeros(1, token_dim)) | |
| self.dataset_token = nn.Parameter(torch.zeros(1, token_dim)) | |
| self.encoder = nn.Sequential(nn.Linear(token_dim, d), nn.LayerNorm(d), nn.SiLU()) | |
| self.gene_embedding_layer = self.encoder # tied, as in the official model | |
| self.transformer_encoder = TransformerEncoder([Layer(d, nhead, d_hid) for _ in range(nlayers)]) | |
| self.decoder = nn.Sequential(SkipBlock(d), nn.Linear(d, out_dim)) | |
| self.binary_decoder = nn.Sequential(SkipBlock(out_dim + d + z_dim), SkipBlock(out_dim + d + z_dim), | |
| nn.Linear(out_dim + d + z_dim, 1)) | |
| self.bin_encoder = nn.Embedding(10, d) | |
| self.count_encoder = nn.Sequential(nn.Linear(1, 512), nn.LeakyReLU(), nn.Linear(512, 10)) | |
| self.dataset_embedder = nn.Linear(out_dim, 10) | |
| self.dataset_encoder = nn.Sequential(nn.Linear(out_dim, d), nn.SiLU(), nn.LayerNorm(d), nn.Dropout(0.1), | |
| nn.Linear(d, n_ds)) | |
| self.pe_embedding = nn.Embedding(n_pe, token_dim) | |
| def forward(self, tok, cnt): | |
| """tok: (B, L) global gene ids (position 0 = placeholder for CLS); cnt: (B, L) count values.""" | |
| x = F.normalize(self.pe_embedding(tok), dim=2) | |
| x[:, 0, :] = self.cls_token.expand(x.size(0), -1) | |
| x = torch.cat([x, self.dataset_token.expand(x.size(0), -1).unsqueeze(1)], 1) | |
| x = self.encoder(x) * math.sqrt(self.d) | |
| w = F.softmax(self.count_encoder(cnt.unsqueeze(-1)), dim=-1) | |
| ce = w @ self.bin_encoder.weight | |
| x = x + torch.cat([ce, torch.zeros_like(ce[:, :1])], 1) | |
| for lyr in self.transformer_encoder.layers: | |
| x = lyr(x) | |
| return F.normalize(self.decoder(x)[:, 0, :], dim=1) | |
| def build(): | |
| cfg = yaml.safe_load(open(hf_hub_download(REPO, "config.yaml", revision=REV))) | |
| m, ds = cfg["model"], cfg["dataset"] | |
| assert m["counts"] and m["rda"] and m["dataset_correction"] and ds["pad_length"] == 2048 | |
| assert ds["cls_token_idx"] == 3 and cfg["loss"]["name"] == "tabular" | |
| pe = torch.load(hf_hub_download(REPO, "protein_embeddings.pt", revision=REV), map_location="cpu", | |
| weights_only=True) | |
| sd = load_file(hf_hub_download(REPO, "model.safetensors", revision=REV)) | |
| n_ds = cfg["dataset"][cfg["dataset"]["current"]]["num_datasets"] | |
| model = SE(cfg["tokenizer"]["token_dim"], m["emsize"], m["nhead"], m["d_hid"], m["nlayers"], m["output_dim"], | |
| len(pe), 1 + 10, n_ds) | |
| for k in ["0.weight", "0.bias", "1.weight", "1.bias"]: | |
| assert torch.equal(sd[f"encoder.{k}"], sd[f"gene_embedding_layer.{k}"]) | |
| model.load_state_dict(sd, strict=True) | |
| assert torch.equal(model.pe_embedding.weight, torch.vstack(list(pe.values()))) # CLI's protein_embeddings.pt | |
| del model.binary_decoder, model.dataset_encoder, model.gene_embedding_layer | |
| model = model.eval().to(dev, DT) | |
| print(f"[{MODEL}] loaded {len(sd)} tensors strictly; config pad_length={ds['pad_length']} " | |
| f"{m['nlayers']}x{m['emsize']} heads={m['nhead']}", flush=True) | |
| return model, cfg, {g: i for i, g in enumerate(pe)} | |
| # ----------------------------------------------------------------------------- tokenisation (official collator) | |
| def sentences(X, genes, g2i, pad_len, cls_idx, seed): | |
| valid = np.array([g in g2i for g in genes]) | |
| Xv = X[:, valid].tocsr() | |
| gid = np.array([g2i[g] for g in genes[valid]], dtype=np.int64) | |
| n = Xv.shape[1] | |
| assert n >= pad_len - 1 # -> official "top pad_len-1 genes" branch | |
| rng = np.random.default_rng(seed) | |
| T = np.zeros((Xv.shape[0], pad_len), np.int64) | |
| C = np.zeros((Xv.shape[0], pad_len), np.float32) | |
| with np.errstate(over="ignore"): # official is_raw_integer_counts heuristic | |
| n_heur = int(sum((r.data.max(initial=0) <= 35) and (np.expm1(r.data.astype(np.float64)).sum() <= 5e6) | |
| for r in (X.getrow(i) for i in range(X.shape[0])))) # (on the full row, as official) | |
| n_trunc = 0 | |
| for r in range(Xv.shape[0]): | |
| a, b = Xv.indptr[r], Xv.indptr[r + 1] | |
| nz, raw = Xv.indices[a:b], Xv.data[a:b].astype(np.float64) | |
| lv = np.log1p(raw) | |
| s = lv.sum() | |
| w = np.zeros(n) | |
| if s > 0: | |
| w[nz] = lv / s | |
| else: | |
| w[:] = 1.0 / n # softmax of an all-zero row | |
| p = rng.permutation(len(nz)) # random tie-break among expressed genes | |
| order = nz[p][np.argsort(-lv[p], kind="stable")][:pad_len - 1] | |
| n_trunc += len(nz) > pad_len - 1 | |
| k = pad_len - 1 - len(order) | |
| if k > 0: # random undetected genes, no replacement | |
| zero = np.setdiff1d(np.arange(n), nz, assume_unique=True) | |
| order = np.r_[order, rng.choice(zero, k, replace=False)] | |
| loc = np.r_[cls_idx, order] | |
| T[r] = gid[loc] | |
| C[r] = 100.0 * w[loc] | |
| stats = dict(valid=int(valid.sum()), n_gene=len(genes), heur_log=n_heur, trunc=n_trunc, | |
| det_med=float(np.median(np.diff(Xv.indptr)))) | |
| return T, C, stats | |
| def embed(model, T, C, bs=16): | |
| E = np.zeros((len(T), model.decoder[1].out_features), np.float32) | |
| i = 0 | |
| with torch.no_grad(), torch.autocast("cuda", dtype=DT): | |
| while i < len(T): | |
| try: | |
| t = torch.from_numpy(T[i:i + bs]).to(dev) | |
| c = torch.from_numpy(C[i:i + bs]).to(dev, DT) | |
| E[i:i + bs] = model(t, c).float().cpu().numpy() | |
| i += bs | |
| except torch.OutOfMemoryError: | |
| torch.cuda.empty_cache() | |
| assert bs > 1, "OOM at batch size 1" | |
| bs //= 2 | |
| print(f" OOM -> batch size {bs}", flush=True) | |
| return E | |
| def sanity(E, name): | |
| rng = np.random.default_rng(0) | |
| S = E[rng.choice(len(E), min(200, len(E)), replace=False)] | |
| S = S / np.linalg.norm(S, axis=1, keepdims=True) | |
| mc = float((S @ S.T)[np.triu_indices(len(S), 1)].mean()) | |
| msg = f"[{MODEL}] {name}: finite={np.isfinite(E).all()} std={E.std(0).mean():.4f} mean-pair-cos(200)={mc:.4f}" | |
| if name == "hest": | |
| org = pd.read_csv(os.path.join(CACHE, "hest", "meta.csv")).organ.values | |
| assert len(org) == len(E) | |
| Z = torch.from_numpy(E / np.linalg.norm(E, axis=1, keepdims=True)).to(dev) | |
| nn_idx = [] | |
| for k in range(0, len(Z), 2048): | |
| s = Z[k:k + 2048] @ Z.T | |
| s[torch.arange(s.shape[0]), torch.arange(k, k + s.shape[0])] = -2 | |
| nn_idx.append(s.topk(10, dim=1).indices.cpu().numpy()) | |
| nn_idx = np.concatenate(nn_idx) | |
| knn = float((org[nn_idx] == org[:, None]).mean()) | |
| p = pd.Series(org).value_counts(normalize=True).values | |
| msg += f" 10-NN same-organ={knn:.3f} (base rate {float((p ** 2).sum()):.3f})" | |
| print(msg, flush=True) | |
| return mc | |
| if __name__ == "__main__": | |
| torch.cuda.set_per_process_memory_fraction(min(1.0, 8 * 2 ** 30 / torch.cuda.get_device_properties(0).total_memory)) | |
| t0 = time.time() | |
| model, cfg, g2i = build() | |
| PAD, CLS = cfg["dataset"]["pad_length"], cfg["dataset"]["cls_token_idx"] | |
| for name in (sys.argv[1:] or SETS): | |
| t1 = time.time() | |
| X, genes = load(name) | |
| T, C, st = sentences(X, genes, g2i, PAD, CLS, seed=0) | |
| print(f"[{MODEL}] {name}: {X.shape[0]} spots; gene coverage {st['valid']}/{st['n_gene']} " | |
| f"({st['valid'] / st['n_gene']:.1%}) in protein_embeddings; detected valid genes/spot median " | |
| f"{st['det_med']:.0f}; spots with >2047 detected (truncated) {st['trunc']}; spots the official " | |
| f"raw-vs-log1p heuristic would treat as log1p {st['heur_log']}", flush=True) | |
| E = embed(model, T, C) | |
| if name == "hest": # tie-breaking stochasticity check | |
| idx = np.random.default_rng(1).choice(len(T), 300, replace=False) | |
| T2, C2, _ = sentences(X[idx], genes, g2i, PAD, CLS, seed=1) | |
| c = (embed(model, T2, C2) * E[idx]).sum(1) | |
| print(f" seed-0 vs seed-1 tie-breaking: per-spot cosine median {np.median(c):.4f} min {c.min():.4f}", | |
| flush=True) | |
| sanity(E, name) | |
| save(MODEL, name, E) | |
| print(f" {name}: {time.time() - t1:.0f}s peak GPU {torch.cuda.max_memory_allocated() / 2 ** 30:.2f} GB", | |
| flush=True) | |
| print(f"[{MODEL}] done in {time.time() - t0:.0f}s", flush=True) | |