Download models/denoiser.py from Sentinal4D/PhenoSeq: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/Sentinal4D/PhenoSeq/resolve/main/models/denoiser.py
- Command line
-
hf download hf://Sentinal4D/PhenoSeq/models/denoiser.py
-
curl -L -o denoiser.py https://huggingface.co/Sentinal4D/PhenoSeq/resolve/main/models/denoiser.py
10.1 kB
| """ | |
| Cross-Attention Denoiser for Image-conditioned RNA-seq Diffusion. | |
| The denoiser takes: | |
| - noisy RNA-seq embeddings (scGPT, 512-dim) | |
| - imaging features (ViT-L, 5120-dim) as conditioning context | |
| - diffusion timestep | |
| And predicts the noise Ξ΅ added to the RNA-seq embeddings. | |
| Architecture: | |
| 1. Project imaging features β model_dim, apply self-attention to create a | |
| rich context representation. | |
| 2. Embed noisy RNA + sinusoidal time embedding β model_dim. | |
| 3. Multiple cross-attention transformer blocks where RNA queries attend to | |
| imaging context. | |
| 4. Project back to RNA embedding space and predict noise. | |
| """ | |
| from __future__ import annotations | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from models.model_utils import ( | |
| SinusoidalTimeEmbedding, | |
| Mish, | |
| FeedForward, | |
| AdaLayerNorm, | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Cross-Attention Block | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class CrossAttentionBlock(nn.Module): | |
| """ | |
| Single cross-attention layer: RNA query attends to imaging context. | |
| Uses adaptive layer norm for time-conditioning and pre-norm residual style. | |
| """ | |
| def __init__(self, dim: int, num_heads: int, time_dim: int, ff_mult: int = 4, dropout: float = 0.1): | |
| super().__init__() | |
| # Time-conditioned norms | |
| self.norm_rna = AdaLayerNorm(dim, time_dim) | |
| self.norm_ctx = nn.LayerNorm(dim) | |
| self.norm_ff = AdaLayerNorm(dim, time_dim) | |
| # Cross-attention: Q from RNA, K/V from imaging | |
| self.cross_attn = nn.MultiheadAttention( | |
| embed_dim=dim, | |
| num_heads=num_heads, | |
| dropout=dropout, | |
| batch_first=True, | |
| ) | |
| # Self-attention on RNA after cross-attention | |
| self.self_attn_norm = AdaLayerNorm(dim, time_dim) | |
| self.self_attn = nn.MultiheadAttention( | |
| embed_dim=dim, | |
| num_heads=num_heads, | |
| dropout=dropout, | |
| batch_first=True, | |
| ) | |
| # Feedforward | |
| self.ff = FeedForward(dim, mult=ff_mult, dropout=dropout) | |
| def forward( | |
| self, | |
| rna: torch.Tensor, # (B, 1, D) | |
| context: torch.Tensor, # (B, S, D) imaging context | |
| time_emb: torch.Tensor, # (B, time_dim) | |
| ) -> torch.Tensor: | |
| # Cross-attention | |
| rna_normed = self.norm_rna(rna, time_emb) | |
| ctx_normed = self.norm_ctx(context) | |
| rna = rna + self.cross_attn(rna_normed, ctx_normed, ctx_normed, need_weights=False)[0] | |
| # Self-attention (useful when we have multiple RNA tokens, but also adds | |
| # a residual self-refinement step) | |
| rna_normed = self.self_attn_norm(rna, time_emb) | |
| rna = rna + self.self_attn(rna_normed, rna_normed, rna_normed, need_weights=False)[0] | |
| # Feedforward | |
| rna = rna + self.ff(self.norm_ff(rna, time_emb)) | |
| return rna | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Imaging Context Encoder | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ImagingEncoder(nn.Module): | |
| """ | |
| Encodes a set of imaging cell features into context representations. | |
| Uses a small self-attention stack to let imaging cells attend to each other | |
| before being used as cross-attention context. | |
| """ | |
| def __init__(self, img_dim: int, model_dim: int, num_heads: int = 4, num_layers: int = 2, dropout: float = 0.1): | |
| super().__init__() | |
| self.proj = nn.Sequential( | |
| nn.Linear(img_dim, model_dim), | |
| Mish(), | |
| nn.LayerNorm(model_dim), | |
| nn.Dropout(dropout), | |
| ) | |
| encoder_layer = nn.TransformerEncoderLayer( | |
| d_model=model_dim, | |
| nhead=num_heads, | |
| dim_feedforward=model_dim * 4, | |
| dropout=dropout, | |
| activation="gelu", | |
| batch_first=True, | |
| norm_first=True, | |
| ) | |
| self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) | |
| def forward(self, img_features: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Args: | |
| img_features: (B, N, img_dim) | |
| Returns: | |
| (B, N, model_dim) context representations | |
| """ | |
| x = self.proj(img_features) | |
| return self.encoder(x) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Full Denoiser | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class Img2RNADenoiser(nn.Module): | |
| """ | |
| Noise prediction network for image-conditioned RNA-seq diffusion. | |
| Args: | |
| img_dim: input imaging feature dimension (5120) | |
| rna_dim: RNA-seq embedding dimension (512) | |
| model_dim: internal model dimension (1024) | |
| num_heads: number of attention heads (8) | |
| num_layers: number of cross-attention blocks (6) | |
| time_dim: time embedding dimension (256) | |
| ff_mult: feedforward multiplier (4) | |
| dropout: dropout rate (0.1) | |
| """ | |
| def __init__( | |
| self, | |
| img_dim: int = 5120, | |
| rna_dim: int = 512, | |
| model_dim: int = 1024, | |
| num_heads: int = 8, | |
| num_layers: int = 6, | |
| time_dim: int = 256, | |
| ff_mult: int = 4, | |
| dropout: float = 0.1, | |
| ): | |
| super().__init__() | |
| self.model_dim = model_dim | |
| self.rna_dim = rna_dim | |
| # ββ Time embedding ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| self.time_embed = nn.Sequential( | |
| SinusoidalTimeEmbedding(time_dim), | |
| nn.Linear(time_dim, time_dim * 4), | |
| Mish(), | |
| nn.Linear(time_dim * 4, time_dim), | |
| ) | |
| # ββ Imaging encoder βββββββββββββββββββββββββββββββββββββββββββββββ | |
| self.img_encoder = ImagingEncoder( | |
| img_dim=img_dim, | |
| model_dim=model_dim, | |
| num_heads=min(num_heads, 4), | |
| num_layers=2, | |
| dropout=dropout, | |
| ) | |
| # ββ RNA input projection ββββββββββββββββββββββββββββββββββββββββββ | |
| self.rna_proj = nn.Sequential( | |
| nn.Linear(rna_dim, model_dim), | |
| Mish(), | |
| nn.LayerNorm(model_dim), | |
| ) | |
| # ββ Cross-attention transformer stack βββββββββββββββββββββββββββββ | |
| self.layers = nn.ModuleList([ | |
| CrossAttentionBlock( | |
| dim=model_dim, | |
| num_heads=num_heads, | |
| time_dim=time_dim, | |
| ff_mult=ff_mult, | |
| dropout=dropout, | |
| ) | |
| for _ in range(num_layers) | |
| ]) | |
| # ββ Output projection: predict noise in RNA embedding space ββββββ | |
| self.out_norm = nn.LayerNorm(model_dim) | |
| self.out_proj = nn.Sequential( | |
| nn.Linear(model_dim, model_dim), | |
| Mish(), | |
| nn.Linear(model_dim, rna_dim), | |
| ) | |
| self._init_weights() | |
| def _init_weights(self): | |
| """ | |
| Initialize weights for stable diffusion training. | |
| Uses PyTorch defaults (kaiming) for all layers, with a small-scale | |
| init on the very last linear so the model starts by predicting | |
| near-zero noise while still allowing gradient flow. | |
| """ | |
| # Small (not zero) init for the final output linear | |
| nn.init.normal_(self.out_proj[2].weight, std=1e-4) | |
| nn.init.zeros_(self.out_proj[2].bias) | |
| def forward( | |
| self, | |
| noisy_rna: torch.Tensor, # (B, rna_dim) β noisy scGPT embedding | |
| img_features: torch.Tensor, # (B, N, img_dim) β imaging features | |
| timestep: torch.Tensor, # (B,) β diffusion timestep | |
| ) -> torch.Tensor: | |
| """ | |
| Predict the noise component in the noisy RNA embedding. | |
| Returns: | |
| predicted_noise: (B, rna_dim) | |
| """ | |
| # Time embedding | |
| t_emb = self.time_embed(timestep) # (B, time_dim) | |
| # Encode imaging context | |
| context = self.img_encoder(img_features) # (B, N, model_dim) | |
| # Project noisy RNA and add as a single token | |
| rna = self.rna_proj(noisy_rna).unsqueeze(1) # (B, 1, model_dim) | |
| # Cross-attention layers | |
| for layer in self.layers: | |
| rna = layer(rna, context, t_emb) | |
| # Output projection | |
| rna = self.out_norm(rna.squeeze(1)) # (B, model_dim) | |
| noise_pred = self.out_proj(rna) # (B, rna_dim) | |
| return noise_pred | |
| def count_parameters(self) -> dict: | |
| """Count trainable and total parameters.""" | |
| total = sum(p.numel() for p in self.parameters()) | |
| trainable = sum(p.numel() for p in self.parameters() if p.requires_grad) | |
| return {"total": total, "trainable": trainable} | |