ronniebasak's picture
Upload folder using huggingface_hub
7fec7f7 verified
Raw
History Blame Contribute Delete
9.29 kB
"""
Encoder-only Transformer for predicting circuit summary statistics.
Architecture:
1. Each scalar input feature β†’ learned linear projection to d_model
2. Add learned positional + type embeddings (ACh token gets special type)
3. Prepend a [CLS] aggregation token
4. Transformer encoder (N layers, multi-head self-attention)
5. [CLS] output β†’ MLP head β†’ 11 predicted statistics
Two configurations:
- Model A: 10 input tokens (no ACh), predicts 11 stats
- Model B: 11 input tokens (with ACh), predicts 11 stats
"""
from __future__ import annotations
import math
import torch
import torch.nn as nn
class FeatureTokenizer(nn.Module):
"""Project each scalar feature to d_model via per-feature linear layers.
Input: (B, n_features) β€” raw normalized scalars
Output: (B, n_features, d_model) β€” token embeddings
"""
def __init__(self, n_features: int, d_model: int):
super().__init__()
self.projections = nn.ModuleList([
nn.Linear(1, d_model) for _ in range(n_features)
])
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: (B, n_features)
tokens = []
for i, proj in enumerate(self.projections):
tokens.append(proj(x[:, i : i + 1])) # (B, 1) β†’ (B, d_model)
return torch.stack(tokens, dim=1) # (B, n_features, d_model)
class CircuitTransformer(nn.Module):
"""Encoder-only transformer for circuit statistics prediction.
Args:
n_features: Number of input features (10 for Model A, 11 for Model B)
n_outputs: Number of output statistics (11)
d_model: Embedding dimension
n_heads: Number of attention heads
n_layers: Number of transformer encoder layers
d_ff: Feed-forward hidden dimension
dropout: Dropout rate
has_ach: Whether ACh is included (for type embedding)
"""
def __init__(
self,
n_features: int,
n_outputs: int = 11,
d_model: int = 64,
n_heads: int = 4,
n_layers: int = 4,
d_ff: int = 256,
dropout: float = 0.1,
has_ach: bool = True,
):
super().__init__()
self.n_features = n_features
self.n_outputs = n_outputs
self.d_model = d_model
self.has_ach = has_ach
# ── Input embedding ──────────────────────────────────────────────
self.tokenizer = FeatureTokenizer(n_features, d_model)
# Learned [CLS] token
self.cls_token = nn.Parameter(torch.randn(1, 1, d_model) * 0.02)
# Positional embedding: n_features + 1 (for [CLS])
self.pos_embed = nn.Parameter(
torch.randn(1, n_features + 1, d_model) * 0.02
)
# Type embedding: 0 = structural param, 1 = ACh token, 2 = CLS
self.type_embed = nn.Embedding(3, d_model)
self.embed_dropout = nn.Dropout(dropout)
self.embed_norm = nn.LayerNorm(d_model)
# ── Transformer encoder ──────────────────────────────────────────
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model,
nhead=n_heads,
dim_feedforward=d_ff,
dropout=dropout,
activation="gelu",
batch_first=True,
norm_first=True, # Pre-norm for training stability
)
self.encoder = nn.TransformerEncoder(
encoder_layer, num_layers=n_layers
)
# ── Output head ──────────────────────────────────────────────────
self.output_norm = nn.LayerNorm(d_model)
self.output_head = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_ff, n_outputs),
)
# Initialize weights
self.apply(self._init_weights)
def _init_weights(self, module: nn.Module):
if isinstance(module, nn.Linear):
nn.init.trunc_normal_(module.weight, std=0.02)
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
nn.init.trunc_normal_(module.weight, std=0.02)
elif isinstance(module, nn.LayerNorm):
nn.init.ones_(module.weight)
nn.init.zeros_(module.bias)
def _build_type_ids(self, batch_size: int, device: torch.device) -> torch.Tensor:
"""Build type IDs: [CLS]=2, structural=0, ACh=1."""
# Sequence: [CLS, feat_0, feat_1, ..., feat_{n-1}]
type_ids = torch.zeros(
batch_size, self.n_features + 1, dtype=torch.long, device=device
)
type_ids[:, 0] = 2 # CLS token
if self.has_ach:
# Last feature position is ACh
type_ids[:, -1] = 1 # ACh token type
return type_ids
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: (B, n_features) β€” normalized input features
Returns:
(B, n_outputs) β€” predicted statistics (in normalized space)
"""
B = x.shape[0]
device = x.device
# 1. Tokenize features β†’ (B, n_features, d_model)
tokens = self.tokenizer(x)
# 2. Prepend [CLS] β†’ (B, n_features+1, d_model)
cls_expanded = self.cls_token.expand(B, -1, -1)
tokens = torch.cat([cls_expanded, tokens], dim=1)
# 3. Add positional + type embeddings
type_ids = self._build_type_ids(B, device)
tokens = tokens + self.pos_embed + self.type_embed(type_ids)
# 4. Norm + dropout
tokens = self.embed_norm(tokens)
tokens = self.embed_dropout(tokens)
# 5. Transformer encoder
tokens = self.encoder(tokens)
# 6. Extract [CLS] representation β†’ predict
cls_out = tokens[:, 0] # (B, d_model)
cls_out = self.output_norm(cls_out)
return self.output_head(cls_out) # (B, n_outputs)
def count_params(self) -> int:
return sum(p.numel() for p in self.parameters() if p.requires_grad)
# ── MLP Baseline ─────────────────────────────────────────────────────────────
class CircuitMLP(nn.Module):
"""Simple MLP baseline for tabular regression.
Properly sized for small datasets (~5K-55K samples).
Default: 2 layers Γ— 64 units = ~5K-10K params.
"""
def __init__(
self,
n_features: int,
n_outputs: int = 11,
hidden_dims: list[int] | None = None,
dropout: float = 0.1,
):
super().__init__()
self.n_features = n_features
self.n_outputs = n_outputs
hidden_dims = hidden_dims or [64, 64]
layers = []
in_dim = n_features
for h_dim in hidden_dims:
layers.extend([
nn.Linear(in_dim, h_dim),
nn.GELU(),
nn.Dropout(dropout),
])
in_dim = h_dim
layers.append(nn.Linear(in_dim, n_outputs))
self.net = nn.Sequential(*layers)
self.apply(self._init_weights)
def _init_weights(self, module: nn.Module):
if isinstance(module, nn.Linear):
nn.init.kaiming_normal_(module.weight, nonlinearity="linear")
if module.bias is not None:
nn.init.zeros_(module.bias)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
def count_params(self) -> int:
return sum(p.numel() for p in self.parameters() if p.requires_grad)
# ── Factory functions ────────────────────────────────────────────────────────
def build_model_a(cfg, arch: str = "transformer") -> nn.Module:
"""Model A: plain HH (no ACh token)."""
if arch == "mlp":
return CircuitMLP(
n_features=cfg.n_input_features_a,
n_outputs=cfg.n_output_stats,
hidden_dims=cfg.mlp_hidden,
dropout=cfg.mlp_dropout,
)
return CircuitTransformer(
n_features=cfg.n_input_features_a,
n_outputs=cfg.n_output_stats,
d_model=cfg.d_model,
n_heads=cfg.n_heads,
n_layers=cfg.n_layers,
d_ff=cfg.d_ff,
dropout=cfg.dropout,
has_ach=False,
)
def build_model_b(cfg, arch: str = "transformer") -> nn.Module:
"""Model B: HH + ACh modulation."""
if arch == "mlp":
return CircuitMLP(
n_features=cfg.n_input_features_b,
n_outputs=cfg.n_output_stats,
hidden_dims=cfg.mlp_hidden,
dropout=cfg.mlp_dropout,
)
return CircuitTransformer(
n_features=cfg.n_input_features_b,
n_outputs=cfg.n_output_stats,
d_model=cfg.d_model,
n_heads=cfg.n_heads,
n_layers=cfg.n_layers,
d_ff=cfg.d_ff,
dropout=cfg.dropout,
has_ach=True,
)