Source code for pretrain.models.nuclr.nuclr

import numpy as np
import torch
import torch.nn as nn
from torch import Tensor
from torch_brain.batching import chain
from torch_brain.nn import RotaryTimeEmbedding

from core.utils.util import is_divisible

from .attention import SelfAttention


[docs] class NuCLR(nn.Module): """Contrastive unit encoder over population context :cite:`nuclr`. Reference implementation: `nerdslab/nuclr <https://github.com/nerdslab/nuclr>`_, with the deviations listed in ``src/pretrain/models/nuclr/README.md``. """ def __init__( self, ctx_duration: float, latent_step: float, bin_size: float, dim: int, self_heads: int, dim_head: int, lin_dropout: float, t_layers: int, st_layers: int, rot_ratio: float = 0.5, rotate_value: bool = True, attn_biases: bool = True, ): super().__init__() assert is_divisible(latent_step, bin_size) assert is_divisible(ctx_duration, latent_step) self.ctx_duration = ctx_duration self.latent_step = latent_step self.bin_size = bin_size self.emb_dim = self.dim = dim # derived params self.bins_per_latent = int(latent_step / bin_size) # num of bins per latent self.num_latents = int(np.ceil(ctx_duration / latent_step)) t_min, t_max = 1.0, 8.0 * self.num_latents self.rotary_emb = RotaryTimeEmbedding( head_dim=dim_head, rotate_dim=int(dim_head * rot_ratio), t_min=t_min, t_max=t_max, ) self.read_in = nn.Linear(self.bins_per_latent, dim) self.t_blocks: list[list[nn.Module]] = nn.ModuleList( [ nn.ModuleList( [ SelfAttention( dim=dim, heads=self_heads, dim_head=dim_head, rotate_value=rotate_value, to_qkv_bias=attn_biases, ), FFN(dim=dim, mult=4, dropout=lin_dropout, pre_norm=True), ] ) for _ in range(t_layers) ] ) # ty:ignore[invalid-assignment] self.st_blocks: list[list[nn.Module]] = nn.ModuleList( [ nn.ModuleList( [ SelfAttention( dim=dim, heads=self_heads, dim_head=dim_head, to_qkv_bias=attn_biases, ), FFN(dim=dim, mult=4, dropout=lin_dropout, pre_norm=True), SelfAttention( dim=dim, heads=self_heads, dim_head=dim_head, rotate_value=rotate_value, to_qkv_bias=attn_biases, ), FFN(dim=dim, mult=4, dropout=lin_dropout, pre_norm=True), ] ) for _ in range(st_layers) ] ) # ty:ignore[invalid-assignment] self.dp = nn.Dropout(lin_dropout)
[docs] def forward(self, bins: Tensor, spax_seqlen: Tensor) -> Tensor: r""" Args: bins: Chained tensor of binned spikes ``(*num_neurons, num_bins)`` spax_seqlen: seqlen to constrain spatial attention """ device = bins.device num_units = int(spax_seqlen.sum()) spax_seqlen = spax_seqlen.repeat(self.num_latents) # (U, L*B) -> (U, L, B) x = bins.reshape(bins.shape[0], self.num_latents, self.bins_per_latent) x = self.read_in(x.float()) t = torch.arange(self.num_latents, device=device).repeat(num_units, 1).float() x_rot = self.rotary_emb(t) # Temporal attention layers for t_attn, t_ffn in self.t_blocks: x = x + self.dp(t_attn(x, rotary=x_rot)) x = x + self.dp(t_ffn(x)) # Spatio-temporal attention layers for s_attn, s_ffn, t_attn, t_ffn in self.st_blocks: # Spatial x = x.permute(1, 0, 2).reshape(-1, x.shape[2]) # (U, L, D) -> (L*U, D) x = x + self.dp(s_attn(x, seqlen=spax_seqlen)) x = x + self.dp(s_ffn(x)) x = x.reshape(self.num_latents, num_units, -1).permute(1, 0, 2) # (L*U, D) -> (U, L, D) # Temporal x = x + self.dp(t_attn(x, rotary=x_rot)) x = x + self.dp(t_ffn(x)) y = x.mean(1) return y
def input_fn(self, data) -> dict | None: if len(data.units) < 2: return None # cannot work with a single unit spike_times = torch.tensor(data.spikes.timestamps, dtype=torch.float32) spike_units = torch.tensor(data.spikes.unit_index, dtype=torch.long) num_units = len(data.units.id) # bin spikes num_bins = int(self.ctx_duration / self.bin_size) rate = 1 / self.bin_size bins = torch.zeros((num_units, num_bins + 1), dtype=torch.int16) bins.index_put_( indices=(spike_units, torch.floor(spike_times * rate).long()), values=torch.ones(len(spike_times), dtype=torch.int16), accumulate=True, ) bins = bins[:, : self.num_latents * self.bins_per_latent] pids = data.units.probe_id.astype(str) _, probe_counts = np.unique(pids, return_counts=True) probe_boundary_idx = np.where(pids[1:] != pids[:-1])[0] assert len(probe_boundary_idx) <= 1 assert len(probe_boundary_idx) == len(probe_counts) - 1 return { "model_inputs": { "bins": chain(bins), "spax_seqlen": chain(probe_counts), }, "unit_seqlen": num_units, "unit_ids": data.units.id.astype(str).tolist(), "probe_ids": data.units.probe_id.astype(str).tolist(), } @staticmethod def collate(data: list[dict | None], two_view: bool = False): import itertools from torch_brain.batching import collate as tb_collate X = [x for x in data if x is not None] if two_view: # flatten the list of tuples, i.e. the two views are _interleaved_ X = list(itertools.chain(*X)) if len(X) == 0: return None out = { "model_inputs": tb_collate([x["model_inputs"] for x in X]), "unit_seqlen": tb_collate([x["unit_seqlen"] for x in X]), "unit_ids": np.concatenate([x["unit_ids"] for x in X]).astype(str), "probe_ids": np.concatenate([x["probe_ids"] for x in X]).astype(str), } return out
class FFN(nn.Module): def __init__(self, dim: int, mult: int, dropout: float, pre_norm: bool): super().__init__() self.norm = nn.LayerNorm(dim) if pre_norm else nn.Identity() self.in_proj = nn.Linear(dim, 2 * dim * mult) self.out_proj = nn.Linear(dim * mult, dim) self.dp = nn.Dropout(p=dropout) self.act = nn.GELU() def forward(self, x): y = self.norm(x) y, gate = self.in_proj(y).chunk(2, dim=-1) y = self.act(gate) * y y = self.dp(y) return self.out_proj(y)