Source code for ts3.models.transductive.base
"""What every transductive extractor shares: which checkpoint answers which regime.
The train side comes from the shared pretrain checkpoint and the eval side from one
finetuning checkpoint per eval recording, so the eval part of the embeddings file is a
concatenation across encoders that drifted apart on their own sessions. That is the regime,
not a defect, but it is why the two halves are less comparable here than under
``ts3.models.inductive``, and why the probe is the only thing that ever sees them together.
Subclasses supply :meth:`read`, which turns one checkpoint into embeddings for the units it
was trained on. They exist as a family because for these models a unit embedding is a free
parameter indexed by unit identity: no row exists for a unit the checkpoint never saw, so
there is nothing to read for held-out units without finetuning on them first.
"""
from abc import abstractmethod
import numpy as np
import torch
from tqdm import tqdm
from core.dataset import BenchmarkRegime, WholeSessionSpikeDataset
from core.utils.logger import Logger
from core.utils.util import expand_path
from ts3.models.base import Extractor
from ts3.models.transductive.ckpt_index import resolve_finetune_ckpts
[docs]
class TransductiveExtractor(Extractor):
def __init__(
self,
pretrain_ckpt: str,
seed: int,
ckpt_dir: str,
filters: dict | None = None,
csv_path: str | None = None,
):
self.pretrain_ckpt = expand_path(pretrain_ckpt).resolve()
self.seed = seed
self.ckpt_dir = ckpt_dir
self.filters = filters
self.csv_path = csv_path
@property
def name(self) -> str:
return f"{self.pretrain_ckpt.parent.name}_{self.pretrain_ckpt.stem}_s{self.seed}"
@property
def run_seed(self) -> int | None:
# the finetuning seed: the pretrain checkpoint is shared, so this is what varies
return self.seed
[docs]
def setup(self, data_root, device: torch.device, logger: Logger) -> None:
super().setup(data_root, device, logger)
# failing units are kept here and filtered later, at eval, so nothing is cut twice.
# That is a population TS3 does not score, so it does not come through its dataset.
self.datasets = {
regime: WholeSessionSpikeDataset(
root=data_root,
regime=regime,
contract="TS3 transductive",
)
for regime in ("pretrain", "eval")
}
self.uids = {r: set(ds.get_unit_ids()) for r, ds in self.datasets.items()}
assert not (self.uids["pretrain"] & self.uids["eval"]), (
"Pretrain and eval datasets must not share any units"
)
self.finetune_ckpts = resolve_finetune_ckpts(
seed=self.seed,
ckpt_dir=self.ckpt_dir,
pretrain_ckpt=self.pretrain_ckpt,
filters=self.filters,
csv_path=self.csv_path,
)
logger.info(f"seed {self.seed}: {len(self.finetune_ckpts)} finetuning checkpoints")
[docs]
@abstractmethod
def read(
self, state_dict: dict, dataset: WholeSessionSpikeDataset, uids: set[str]
) -> tuple[torch.Tensor, np.ndarray]:
"""Embeddings for ``uids`` out of one checkpoint's weights, and the uids they match."""
def _read_ckpt(
self, path, dataset, uids, expect_seed: int | None = None
) -> tuple[torch.Tensor, np.ndarray]:
ckpt = torch.load(path, map_location="cpu", weights_only=False)
if expect_seed is not None:
# two seeds of one recording match in shape, so nothing else catches a swap
trained_with = ckpt["cfg"].get("seed")
assert trained_with in (expect_seed, None), (
f"{path} was trained with seed {trained_with}, not {expect_seed}: whatever "
"listed it disagrees with the checkpoint"
)
return self.read(ckpt["model_state_dict"], dataset, uids)
[docs]
def encode(self, regime: BenchmarkRegime) -> tuple[torch.Tensor, np.ndarray]:
if regime == "pretrain":
return self._read_ckpt(self.pretrain_ckpt, self.datasets[regime], self.uids[regime])
dataset = self.datasets[regime]
all_embs, all_uids = [], []
pbar = tqdm(self.finetune_ckpts, desc=f"[emb-{regime}]")
for entry in pbar:
rec_uids = set(dataset.get_recording(entry.recording_id).units.id.astype(str))
embs, uids = self._read_ckpt(entry.path, dataset, rec_uids, expect_seed=self.seed)
all_embs.append(embs)
all_uids.append(uids)
pbar.set_postfix(units=sum(len(u) for u in all_uids))
return torch.cat(all_embs), np.concatenate(all_uids)