| # ------------------------------------------------------------ | |
| # CancerTranscriptome-Mini-48M | |
| # Model: Lightweight adaptation of BulkFormer | |
| # Author: Walter Alvarado (NASA Ames Research Center) | |
| # License: MIT | |
| # | |
| # References: | |
| # (1) Boming Kang, Rui Fan, Meizheng Yi, Chunmei Cui, Qinghua Cui. | |
| # “A large-scale foundation model for bulk transcriptomes.” | |
| # bioRxiv (2025). doi:10.1101/2025.06.11.659222 | |
| # | |
| # (2) Alvarado W. “CancerTranscriptome-Mini-48M: A compact cancer- | |
| # focused BulkFormer derivative.” https://github.com/alwalt/BioFM | |
| # | |
| # Data Source: | |
| # ARCHS4 Human RNA-seq v2.5 (Lachmann et al., Nat Commun 2018) | |
| # ------------------------------------------------------------ | |
| import torch | |
| import torch.nn as nn | |
| from torch_geometric.nn.conv import GCNConv | |
| from performer_pytorch import Performer | |
| # Default model hyperparameters | |
| model_params = { | |
| "dim": 320, | |
| "bins": 10, | |
| "gb_repeat": 1, | |
| "p_repeat": 2, | |
| "bin_head": 8, | |
| "full_head": 4, | |
| "gene_length": 19357 | |
| } | |
| # ------------------------------------------------------------ | |
| # Rotary Expression Embedding (REE) | |
| # ------------------------------------------------------------ | |
| class PositionalExprEmbedding(nn.Module): | |
| """ | |
| Rotary Expression Embedding (REE): | |
| Converts continuous gene expression values into a sinusoidal | |
| embedding usable by Performer/Transformer blocks. Deterministic, | |
| not learned. Masked positions (-10) → zero vector. | |
| """ | |
| def __init__(self, dim, mask_token=-10): | |
| super().__init__() | |
| self.mask_token = mask_token | |
| self.inv_freq = nn.Parameter( | |
| 1.0 / (100 ** (torch.arange(0, dim, 2).float() / dim)), | |
| requires_grad=False | |
| ) | |
| def forward(self, x): | |
| mask = (x == self.mask_token).nonzero(as_tuple=False) | |
| x = torch.einsum("bi,j->bij", x, self.inv_freq) | |
| x = torch.cat([x.sin(), x.cos()], dim=-1) | |
| x[mask[:, 0], mask[:, 1]] = 0 | |
| return x | |
| # ------------------------------------------------------------ | |
| # GBFormer Block (Graph + Local Performer + Global Performer) | |
| # ------------------------------------------------------------ | |
| class GBFormer(nn.Module): | |
| """ | |
| A single GBFormer block: | |
| - LayerNorm | |
| - GCNConv (gene-gene propagation) | |
| - Binning by learned importance score | |
| - Local Performer per-bin | |
| - Global Performer | |
| """ | |
| def __init__(self, dim, gene_length, bin_head, full_head, bins, p_repeat): | |
| super().__init__() | |
| self.dim = dim | |
| self.bins = bins | |
| self.bin_head = bin_head | |
| self.full_head = full_head | |
| self.p_repeat = p_repeat | |
| self.layernorm = nn.LayerNorm(dim) | |
| self.gcn = GCNConv(dim, dim, cached=True, add_self_loops=False) | |
| # Learn scoring → assign gene to bin | |
| self.which_bin = nn.Linear(dim, 1) | |
| # Local Performer per bin | |
| self.bin_layers = nn.ModuleList([ | |
| Performer( | |
| dim=dim, | |
| heads=bin_head, | |
| depth=1, | |
| dim_head=dim // bin_head, | |
| attn_dropout=0.2, | |
| ff_dropout=0.2 | |
| ) | |
| for _ in range(bins) | |
| ]) | |
| # Global Performer stack | |
| self.global_layers = nn.Sequential(*[ | |
| Performer( | |
| dim=dim, | |
| heads=full_head, | |
| depth=1, | |
| dim_head=dim // full_head | |
| ) | |
| for _ in range(p_repeat) | |
| ]) | |
| def forward(self, x, graph): | |
| B, G, D = x.shape | |
| x = self.layernorm(x) | |
| x = x + self.gcn(x, graph) # residual GCN update | |
| if self.bins > 0: | |
| scores = self.which_bin(x).squeeze(-1) # [B, G] | |
| order = torch.argsort(scores, dim=1, descending=True) | |
| order_full = order.unsqueeze(-1).expand(-1, -1, D) | |
| x_sorted = x.gather(1, order_full) | |
| bin_size = (G - 1) // self.bins + 1 | |
| chunks = torch.split(x_sorted, bin_size, dim=1) | |
| processed = [ | |
| layer(chunk) | |
| for chunk, layer in zip(chunks, self.bin_layers) | |
| ] | |
| x_cat = torch.cat(processed, dim=1) | |
| x = torch.empty_like(x_cat).scatter_(1, order_full, x_cat) | |
| x = self.global_layers(x) | |
| return x | |
| # ------------------------------------------------------------ | |
| # Full BulkFormer Model | |
| # ------------------------------------------------------------ | |
| class BulkFormer(nn.Module): | |
| """ | |
| CancerTranscriptome-Mini-48M: | |
| A compact BulkFormer-style masked-expression model. | |
| Combines: | |
| - ESM2 gene identity embeddings | |
| - Rotary Expression Embeddings (REE) | |
| - Graph Convolution (GCNConv) | |
| - Local/global Performer attention | |
| - Optional intermediate repr_layers for feature extraction | |
| """ | |
| def __init__( | |
| self, | |
| dim, | |
| graph, | |
| gene_emb, | |
| gene_length, | |
| bin_head=4, | |
| full_head=4, | |
| bins=10, | |
| gb_repeat=1, | |
| p_repeat=1 | |
| ): | |
| super().__init__() | |
| self.dim = dim | |
| self.graph = graph | |
| self.gene_length = gene_length | |
| # Identity embeddings from ESM2 (trainable projection) | |
| self.gene_emb = nn.Parameter(gene_emb) | |
| self.gene_proj = nn.Sequential( | |
| nn.Linear(gene_emb.shape[1], 4 * dim), | |
| nn.ReLU(), | |
| nn.Linear(4 * dim, dim) | |
| ) | |
| # REE for expression | |
| self.expr_emb = PositionalExprEmbedding(dim) | |
| # Pre-attention mixing layer | |
| self.mix = nn.Sequential( | |
| nn.Linear(dim, 4 * dim), | |
| nn.ReLU(), | |
| nn.Linear(4 * dim, dim) | |
| ) | |
| # Stacked GBFormer blocks | |
| self.gb_blocks = nn.ModuleList([ | |
| GBFormer(dim, gene_length, bin_head, full_head, bins, p_repeat) | |
| for _ in range(gb_repeat) | |
| ]) | |
| self.final_norm = nn.LayerNorm(dim) | |
| # Output head → scalar prediction per gene | |
| self.head = nn.Sequential( | |
| nn.Linear(dim, 4 * dim), | |
| nn.ReLU(), | |
| nn.Linear(4 * dim, 1), | |
| nn.ReLU() | |
| ) | |
| def forward(self, x, repr_layers=None): | |
| B, G = x.shape | |
| hidden = {} | |
| x = ( | |
| self.expr_emb(x) + | |
| self.gene_proj(self.gene_emb) + | |
| torch.zeros(B, 1, self.dim, device=x.device) # no AE latent in this version | |
| ) | |
| x = self.mix(x) | |
| for i, block in enumerate(self.gb_blocks): | |
| x = block(x, self.graph) | |
| if repr_layers and i in repr_layers: | |
| hidden[i] = x | |
| x = self.final_norm(x) | |
| out = self.head(x).squeeze(-1) | |
| if repr_layers: | |
| return out, hidden | |
| return out | |
Xet Storage Details
- Size:
- 6.78 kB
- Xet hash:
- 5b0ae549cd1ff253fac76b8ec178b3572663fd8431ccb68392f9cb60ec746ed3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.