Source code for ts3.models.inductive.nuclr.nuclr_extractor

"""NuCLR's unit-embedding extractor.

Inductive: one frozen checkpoint answers both regimes, because a NuCLR unit embedding is a
function of that unit's spikes and nothing about the unit's identity is stored in the
weights. Averaging a sequence model's output over the windows a unit appears in is what
turns it into a per-unit embedding, and the window length, the 50% overlap and the one-view
collate are the model's, read off the config it trained with.
"""

import multiprocessing as mp
from functools import partial

import hydra
import numpy as np
import torch
from torch_brain.samplers import SequentialFixedWindowSampler

from core.dataset import BenchmarkRegime
from core.utils.embedding_collector import EmbeddingCollector
from core.utils.logger import Logger
from core.utils.util import Precision, expand_path, move_to_device
from pretrain.models.nuclr.dataset import NuCLRDataset
from ts3.models.base import Extractor, load_pretrained


[docs] class NuCLRExtractor(Extractor): def __init__(self, ckpt: str, batch_size: int | None = None, num_workers: int | None = None): self.ckpt = expand_path(ckpt).resolve() self.batch_size = batch_size self.num_workers = num_workers @property def name(self) -> str: return f"{self.ckpt.parent.name}_{self.ckpt.stem}" @property def run_seed(self) -> int | None: return self.cfg.get("seed")
[docs] def setup(self, data_root, device: torch.device, logger: Logger) -> None: super().setup(data_root, device, logger) self.model, self.cfg = load_pretrained( self.ckpt, device, logger, batch_size=self.batch_size, num_workers=self.num_workers, )
def _build_loader(self, regime: BenchmarkRegime): """The sequential one-view loader, stepping half a context at a time. Unlike the training loader it keeps the final short batch, there being no reason for an inference pass to discard data. Some units still end with no embedding: the collector is sized to every unit in the dataset, while ``eval_transform`` and the qc filter remove units from the batches. The probe join drops those. """ ds = NuCLRDataset(root=self.data_root, regime=regime, input_fn=self.model.input_fn) if self.cfg.eval_transform: ds.transform = hydra.utils.instantiate(self.cfg.eval_transform) sampler = SequentialFixedWindowSampler( sampling_intervals=ds.get_sampling_intervals(), window_length=self.model.ctx_duration, step=0.5 * self.model.ctx_duration, drop_short=True, ) has_workers = self.cfg.num_workers > 0 loader = torch.utils.data.DataLoader( dataset=ds, sampler=sampler, collate_fn=partial(self.model.collate, two_view=False), batch_size=self.cfg.batch_size, num_workers=self.cfg.num_workers, drop_last=False, pin_memory=self.cfg.pin_memory, multiprocessing_context=mp.get_context("fork") if has_workers else None, ) return ds, loader
[docs] @torch.inference_mode() def encode(self, regime: BenchmarkRegime) -> tuple[torch.Tensor, np.ndarray]: """One mean embedding per unit, over every window that unit appears in.""" self.model.eval() ds, loader = self._build_loader(regime) self.logger.info( f"{regime}: {len(ds.get_session_ids())} sessions" f", {len(ds.get_unit_ids())} units" f", {len(loader)} batches" ) collector = EmbeddingCollector( unit_ids=ds.get_unit_ids(), dim=self.model.emb_dim, device=self.device, ) dtype = Precision(self.cfg.precision).dtype for X in self.logger.get_pbar(loader, prefix=f"emb-{regime}"): if X is None: # Some sessions have all bad neurons, and sequential sampling can land a # whole batch inside them. continue X = move_to_device(X, self.device) with torch.autocast(device_type=self.device.type, dtype=dtype): z = self.model(**X["model_inputs"]) assert torch.isfinite(z).all() collector.update(z, X["unit_ids"]) return collector.compute()