43ntropy dawidtang commited on
Commit
16d3ebf
·
0 Parent(s):

Duplicate from epfl-neuroai/vjepa2-encoder-basic

Browse files

Co-authored-by: david <dawidtang@users.noreply.huggingface.co>

.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.keep ADDED
File without changes
README.md ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - neuroscience
4
+ - fmri
5
+ - video
6
+ - v-jepa
7
+ - pytorch
8
+ library_name: pytorch
9
+ ---
10
+
11
+ # V-JEPA2 Offline Encoder for Video-Evoked BOLD Responses
12
+
13
+ This repository contains a PyTorch checkpoint for a basic V-JEPA2-based offline encoder trained to predict video-evoked BOLD responses. The encoder is intended for research workflows involving neural response prediction and neural response-guided visual synthesis.
14
+
15
+ The checkpoint stores decoder weights and metadata for an offline encoder. This repository includes a custom `transformers.AutoModel` wrapper and does not require the original training codebase.
16
+
17
+ ## Input/Output Contract
18
+
19
+ The intended input is a short video clip corresponding to the training stimulus duration:
20
+
21
+ - **Input**: one 3-second RGB video clip, represented as a float tensor shaped `[B, T, C, H, W]` with values in `[0, 1]`.
22
+ - **Output**: one vector of predicted z-scored fMRI beta responses per video, shaped `[B, 20484]`.
23
+ - **Temporal dimension**: the output has no time dimension. Each 3-second video maps to a single predicted response vector.
24
+
25
+ This makes the encoder suitable for scoring or optimizing short generated videos against static target neural-response patterns.
26
+
27
+ The video-input path resizes frames to `224 x 224` and applies the ImageNet normalization used by the V-JEPA2 training pipeline. If you pass already-normalized V-JEPA2 inputs, call `model.predict_fmri(video, normalize=False)`.
28
+
29
+ ## Loading
30
+
31
+ This checkpoint can be loaded with `transformers.AutoModel` and `trust_remote_code=True`.
32
+
33
+ Example:
34
+
35
+ ```python
36
+ import torch
37
+ from transformers import AutoModel
38
+
39
+ model = AutoModel.from_pretrained(
40
+ "epfl-neuroai/vjepa2-encoder-basic",
41
+ trust_remote_code=True,
42
+ )
43
+ model.eval()
44
+
45
+ # Replace this with a preprocessed 3-second video tensor.
46
+ # Shape: [batch, frames, channels, height, width].
47
+ video = torch.zeros(1, 16, 3, 224, 224)
48
+
49
+ with torch.no_grad():
50
+ prediction = model.predict_fmri(video)
51
+
52
+ print(prediction.shape) # [1, 20484]
53
+ ```
54
+
55
+ For decoder-only debugging, the model can also run from precomputed V-JEPA2 layer features:
56
+
57
+ ```python
58
+ model = AutoModel.from_pretrained(
59
+ "epfl-neuroai/vjepa2-encoder-basic",
60
+ trust_remote_code=True,
61
+ load_vjepa=False,
62
+ )
63
+
64
+ features = [
65
+ torch.zeros(1, decoder.mean.shape[1])
66
+ for decoder in model.decoders
67
+ ]
68
+
69
+ with torch.no_grad():
70
+ prediction = model.forward_features(features)
71
+ ```
72
+
73
+ ## Data
74
+
75
+ This checkpoint was trained using data from:
76
+
77
+ - **BOLD Moments Dataset (BMD)**: whole-brain fMRI responses to short naturalistic videos.
78
+ - **Social interaction video fMRI dataset from Emalie McMahon and collaborators**: fMRI responses to naturalistic two-person social action videos.
79
+
80
+ This repository does not include the underlying fMRI datasets or stimulus videos.
81
+
82
+ ## Files
83
+
84
+ - `vjepa2_offline_encoder.pth`: PyTorch checkpoint containing decoder weights, decoding-unit selection metadata, feature-extractor configuration, and registered attributes.
85
+ - `config.json`, `configuration_vjepa2_fmri_encoder.py`, `modeling_vjepa2_fmri_encoder.py`: custom Transformers files for `AutoModel` loading.
86
+ - `requirements.txt`: minimal Python dependencies.
87
+
88
+ ## Backbone Source
89
+
90
+ The V-JEPA2 backbone weights are shipped in this repository as:
91
+
92
+ ```text
93
+ vitl.pt
94
+ ```
95
+
96
+ The loader uses the V-JEPA2 Torch Hub architecture with `pretrained=False`, then loads the local `vitl.pt` weights directly. This avoids relying on the moving `facebookresearch/vjepa2` Torch Hub checkpoint URL while preserving compatibility with the original decoder features. The decoder checkpoint uses canonical `extractor_config["layer_names"]` metadata.
97
+
98
+ ## Citations
99
+
100
+ If you use this checkpoint, please cite the V-JEPA/V-JEPA 2 backbone papers and source datasets:
101
+
102
+ ```bibtex
103
+ @article{bardes2024revisiting,
104
+ title={Revisiting Feature Prediction for Learning Visual Representations from Video},
105
+ author={Bardes, Adrien and Garrido, Quentin and Ponce, Jean and Chen, Xinlei and Rabbat, Michael and LeCun, Yann and Assran, Mahmoud and Ballas, Nicolas},
106
+ journal={arXiv preprint arXiv:2404.08471},
107
+ year={2024}
108
+ }
109
+
110
+ @article{assran2025vjepa2,
111
+ title={V-JEPA 2: Self-Supervised Video Models Enable Understanding, Prediction and Planning},
112
+ author={Assran, Mido and Bardes, Adrien and Fan, David and Garrido, Quentin and Howes, Russell and Komeili, Mojtaba and Muckley, Matthew and Rizvi, Ammar and Roberts, Claire and Sinha, Koustuv and others},
113
+ journal={arXiv preprint arXiv:2506.09985},
114
+ year={2025}
115
+ }
116
+
117
+ @article{tang2025diverse,
118
+ title={Diverse perceptual representations across visual pathways emerge from a single objective},
119
+ author={Tang, Yingtian and Gokce, Abdulkadir and Al-Karkari, Khaled Jedoui and Yamins, Daniel and Schrimpf, Martin},
120
+ journal={bioRxiv},
121
+ pages={2025--07},
122
+ year={2025},
123
+ publisher={Cold Spring Harbor Laboratory}
124
+ }
125
+
126
+ @article{lahner2024modeling,
127
+ title={Modeling short visual events through the BOLD moments video fMRI dataset and metadata},
128
+ author={Lahner, Benjamin and Dwivedi, Kshitij and Iamshchinina, Polina and Graumann, Monika and Lascelles, Alex and Roig, Gemma and Gifford, Alessandro Thomas and Pan, Bowen and Jin, SouYoung and Ratan Murty, N Apurva and others},
129
+ journal={Nature communications},
130
+ volume={15},
131
+ number={1},
132
+ pages={6241},
133
+ year={2024},
134
+ publisher={Nature Publishing Group UK London}
135
+ }
136
+
137
+ @article{mcmahon2023hierarchical,
138
+ title={Hierarchical organization of social action features along the lateral visual pathway},
139
+ author={McMahon, Emalie and Bonner, Michael F and Isik, Leyla},
140
+ journal={Current Biology},
141
+ volume={33},
142
+ number={23},
143
+ pages={5035--5047},
144
+ year={2023},
145
+ publisher={Elsevier}
146
+ }
147
+ ```
config.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "vjepa2_fmri_encoder",
3
+ "architectures": [
4
+ "VJEPA2FMRIEncoderModel"
5
+ ],
6
+ "auto_map": {
7
+ "AutoConfig": "configuration_vjepa2_fmri_encoder.VJEPA2FMRIEncoderConfig",
8
+ "AutoModel": "modeling_vjepa2_fmri_encoder.VJEPA2FMRIEncoderModel"
9
+ },
10
+ "checkpoint_filename": "vjepa2_offline_encoder.pth",
11
+ "output_dim": 20484,
12
+ "input_duration_seconds": 3.0,
13
+ "input_format": "video_tensor_b_t_c_h_w",
14
+ "output_description": "z_scored_fmri_betas_no_time_dimension",
15
+ "vjepa_size": "large",
16
+ "load_vjepa": true,
17
+ "image_size": 224,
18
+ "normalize_input": true
19
+ }
configuration_vjepa2_fmri_encoder.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transformers config for the V-JEPA2 fMRI encoder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from transformers import PretrainedConfig
6
+
7
+
8
+ class VJEPA2FMRIEncoderConfig(PretrainedConfig):
9
+ model_type = "vjepa2_fmri_encoder"
10
+
11
+ def __init__(
12
+ self,
13
+ checkpoint_filename: str = "vjepa2_offline_encoder.pth",
14
+ output_dim: int = 20484,
15
+ input_duration_seconds: float = 3.0,
16
+ input_format: str = "video_tensor_b_t_c_h_w",
17
+ output_description: str = "z_scored_fmri_betas_no_time_dimension",
18
+ backbone_filename: str = "vitl.pt",
19
+ vjepa_size: str = "large",
20
+ load_vjepa: bool = True,
21
+ image_size: int = 224,
22
+ normalize_input: bool = True,
23
+ **kwargs,
24
+ ) -> None:
25
+ super().__init__(**kwargs)
26
+ self.checkpoint_filename = checkpoint_filename
27
+ self.output_dim = int(output_dim)
28
+ self.input_duration_seconds = float(input_duration_seconds)
29
+ self.input_format = input_format
30
+ self.output_description = output_description
31
+ self.backbone_filename = backbone_filename
32
+ self.vjepa_size = vjepa_size
33
+ self.load_vjepa = bool(load_vjepa)
34
+ self.image_size = int(image_size)
35
+ self.normalize_input = bool(normalize_input)
modeling_vjepa2_fmri_encoder.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Custom AutoModel implementation for a basic V-JEPA2 fMRI encoder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Iterable
8
+
9
+ import torch
10
+ import torch.nn.functional as F
11
+ from torch import nn
12
+ from transformers import PreTrainedModel
13
+
14
+ try:
15
+ from .configuration_vjepa2_fmri_encoder import VJEPA2FMRIEncoderConfig
16
+ except ImportError:
17
+ from configuration_vjepa2_fmri_encoder import VJEPA2FMRIEncoderConfig
18
+
19
+
20
+ class RidgeDecoder(nn.Module):
21
+ def __init__(self, state_dict: dict[str, torch.Tensor]) -> None:
22
+ super().__init__()
23
+ self.register_buffer("mean", state_dict["steps.1.mean"])
24
+ self.register_buffer("std", state_dict["steps.1.std"])
25
+ self.register_buffer("coef", state_dict["steps.2.regressor._coef"])
26
+ self.register_buffer("intercept", state_dict["steps.2.regressor._intercept"])
27
+
28
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
29
+ x = x.reshape(x.shape[0], -1)
30
+ x = (x - self.mean.to(device=x.device)) / self.std.to(device=x.device)
31
+ coef = self.coef.to(device=x.device)
32
+ x = x.to(dtype=coef.dtype)
33
+ return x @ coef.T + self.intercept.to(device=x.device)
34
+
35
+
36
+ class HookedFeatureExtractor:
37
+ def __init__(self, layer_names: Iterable[str], ret_type: str = "chw", spatial_pool: int = 14) -> None:
38
+ self.layer_names = list(layer_names)
39
+ self.ret_type = ret_type
40
+ self.spatial_pool = int(spatial_pool)
41
+
42
+ @staticmethod
43
+ def _get_layer(model: nn.Module, layer_name: str) -> nn.Module:
44
+ layer: object = model
45
+ for part in layer_name.split("."):
46
+ layer = layer[int(part)] if part.isdigit() else getattr(layer, part)
47
+ if not isinstance(layer, nn.Module):
48
+ raise TypeError(f"{layer_name} did not resolve to a torch module")
49
+ return layer
50
+
51
+ @staticmethod
52
+ def _unwrap_output(output: Any) -> torch.Tensor:
53
+ if isinstance(output, (list, tuple)):
54
+ if len(output) == 0:
55
+ raise ValueError("Received an empty feature tuple.")
56
+ output = output[0]
57
+ if not torch.is_tensor(output):
58
+ raise TypeError(f"Expected tensor feature output, got {type(output)!r}")
59
+ return output
60
+
61
+ def __call__(self, model: nn.Module, videos: torch.Tensor, **model_kwargs) -> list[torch.Tensor]:
62
+ outputs: dict[str, torch.Tensor] = {}
63
+ hooks = [
64
+ self._get_layer(model, name).register_forward_hook(
65
+ lambda _module, _inputs, output, name=name: outputs.__setitem__(name, self._unwrap_output(output))
66
+ )
67
+ for name in self.layer_names
68
+ ]
69
+ try:
70
+ model(videos, **model_kwargs)
71
+ finally:
72
+ for hook in hooks:
73
+ hook.remove()
74
+ return [self._process_feature(outputs[name]) for name in self.layer_names]
75
+
76
+ def _process_feature(self, feature: torch.Tensor) -> torch.Tensor:
77
+ batch, tokens, channels = feature.shape
78
+ feature = feature.reshape(batch, -1, 14, 14, channels).permute(0, 1, 4, 2, 3)
79
+ if self.spatial_pool > 1:
80
+ batch, frames, channels, height, width = feature.shape
81
+ new_height = height // self.spatial_pool
82
+ new_width = width // self.spatial_pool
83
+ feature = feature.reshape(
84
+ batch,
85
+ frames,
86
+ channels,
87
+ new_height,
88
+ self.spatial_pool,
89
+ new_width,
90
+ self.spatial_pool,
91
+ )
92
+ feature = feature.permute(0, 1, 2, 3, 5, 4, 6).mean(dim=(-2, -1))
93
+ if self.ret_type == "chw":
94
+ return feature.mean(dim=1)
95
+ if self.ret_type == "tchw":
96
+ return feature
97
+ raise ValueError(f"Unsupported ret_type: {self.ret_type}")
98
+
99
+
100
+ class LocalVJEPA2Backbone(nn.Module):
101
+ def __init__(self, size: str, image_size: int, normalize_input: bool, checkpoint_path: str) -> None:
102
+ super().__init__()
103
+ self.image_size = int(image_size)
104
+ self.normalize_input = bool(normalize_input)
105
+ self.register_buffer("image_mean", torch.tensor([0.485, 0.456, 0.406]).view(1, 1, 3, 1, 1))
106
+ self.register_buffer("image_std", torch.tensor([0.229, 0.224, 0.225]).view(1, 1, 3, 1, 1))
107
+ hub_name = {
108
+ "large": "vjepa2_vit_large",
109
+ "huge": "vjepa2_vit_huge",
110
+ "giant": "vjepa2_vit_giant",
111
+ }[size]
112
+ backbone = torch.hub.load("facebookresearch/vjepa2", hub_name, pretrained=False)
113
+ backbone, predictor = backbone if isinstance(backbone, (list, tuple)) else (backbone, None)
114
+ state_dict = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
115
+ backbone.load_state_dict(_clean_backbone_key(state_dict["target_encoder"]), strict=False)
116
+ if predictor is not None and "predictor" in state_dict:
117
+ predictor.load_state_dict(_clean_backbone_key(state_dict["predictor"]), strict=False)
118
+ self.backbone = backbone
119
+
120
+ def forward(self, videos: torch.Tensor, normalize: bool | None = None) -> torch.Tensor:
121
+ if videos.ndim != 5:
122
+ raise ValueError(f"Expected video tensor shaped [B, T, C, H, W], got {tuple(videos.shape)}")
123
+ if videos.shape[2] != 3:
124
+ raise ValueError(f"Expected RGB video with 3 channels at dim 2, got {videos.shape[2]}")
125
+
126
+ videos = videos.float()
127
+ batch, frames, channels, height, width = videos.shape
128
+ if height != self.image_size or width != self.image_size:
129
+ videos = videos.reshape(batch * frames, channels, height, width)
130
+ videos = F.interpolate(
131
+ videos,
132
+ size=(self.image_size, self.image_size),
133
+ mode="bilinear",
134
+ align_corners=False,
135
+ )
136
+ videos = videos.reshape(batch, frames, channels, self.image_size, self.image_size)
137
+
138
+ normalize = self.normalize_input if normalize is None else bool(normalize)
139
+ if normalize:
140
+ videos = (videos - self.image_mean.to(device=videos.device, dtype=videos.dtype)) / self.image_std.to(
141
+ device=videos.device,
142
+ dtype=videos.dtype,
143
+ )
144
+ return self.backbone(videos.permute(0, 2, 1, 3, 4))
145
+
146
+
147
+ def _clean_backbone_key(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
148
+ cleaned = {}
149
+ for key, value in state_dict.items():
150
+ key = key.replace("module.", "").replace("backbone.", "")
151
+ cleaned[key] = value
152
+ return cleaned
153
+
154
+
155
+ class VJEPA2FMRIEncoderModel(PreTrainedModel):
156
+ config_class = VJEPA2FMRIEncoderConfig
157
+ base_model_prefix = "vjepa2_fmri_encoder"
158
+ main_input_name = "videos"
159
+
160
+ def __init__(self, config: VJEPA2FMRIEncoderConfig) -> None:
161
+ super().__init__(config)
162
+ self.decoders = nn.ModuleList()
163
+ self.register_buffer("decoding_units", torch.empty(0, dtype=torch.long))
164
+ self.extractor: HookedFeatureExtractor | None = None
165
+ self.vjepa: LocalVJEPA2Backbone | None = None
166
+
167
+ @classmethod
168
+ def from_pretrained(
169
+ cls,
170
+ pretrained_model_name_or_path: str | os.PathLike[str],
171
+ *model_args: Any,
172
+ config: VJEPA2FMRIEncoderConfig | None = None,
173
+ load_vjepa: bool | None = None,
174
+ vjepa_size: str | None = None,
175
+ normalize_input: bool | None = None,
176
+ **kwargs: Any,
177
+ ) -> "VJEPA2FMRIEncoderModel":
178
+ if model_args:
179
+ raise TypeError("Unexpected positional arguments for VJEPA2FMRIEncoderModel.from_pretrained")
180
+
181
+ revision = kwargs.pop("revision", None)
182
+ token = kwargs.pop("token", None)
183
+ cache_dir = kwargs.pop("cache_dir", None)
184
+ local_files_only = kwargs.pop("local_files_only", False)
185
+ for ignored in ("trust_remote_code", "state_dict", "ignore_mismatched_sizes", "adapter_kwargs", "weights_only"):
186
+ kwargs.pop(ignored, None)
187
+ if kwargs:
188
+ raise TypeError(f"Unsupported keyword argument(s): {', '.join(sorted(kwargs))}")
189
+
190
+ if config is None:
191
+ config = VJEPA2FMRIEncoderConfig.from_pretrained(
192
+ pretrained_model_name_or_path,
193
+ revision=revision,
194
+ token=token,
195
+ cache_dir=cache_dir,
196
+ local_files_only=local_files_only,
197
+ )
198
+
199
+ checkpoint_path = cls._resolve_file_path(
200
+ pretrained_model_name_or_path,
201
+ filename=config.checkpoint_filename,
202
+ revision=revision,
203
+ token=token,
204
+ cache_dir=cache_dir,
205
+ local_files_only=local_files_only,
206
+ )
207
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
208
+
209
+ model = cls(config)
210
+ model.decoders = nn.ModuleList([RidgeDecoder(state_dict) for state_dict in checkpoint["decoders_state_dict"]])
211
+ model.register_buffer("decoding_units", checkpoint["decoding_units"].long())
212
+ for name, value in checkpoint.get("registered_attrs", {}).items():
213
+ if torch.is_tensor(value):
214
+ model.register_buffer(name, value)
215
+
216
+ load_vjepa = config.load_vjepa if load_vjepa is None else bool(load_vjepa)
217
+ vjepa_size = config.vjepa_size if vjepa_size is None else vjepa_size
218
+ normalize_input = config.normalize_input if normalize_input is None else bool(normalize_input)
219
+ if load_vjepa:
220
+ backbone_path = cls._resolve_file_path(
221
+ pretrained_model_name_or_path,
222
+ filename=config.backbone_filename,
223
+ revision=revision,
224
+ token=token,
225
+ cache_dir=cache_dir,
226
+ local_files_only=local_files_only,
227
+ )
228
+ extractor_config = checkpoint["extractor_config"]
229
+ model.extractor = HookedFeatureExtractor(
230
+ layer_names=cls._resolve_layer_names(extractor_config),
231
+ ret_type=extractor_config.get("ret_type", "chw"),
232
+ spatial_pool=extractor_config.get("spatial_pool", 14),
233
+ )
234
+ model.vjepa = LocalVJEPA2Backbone(
235
+ size=vjepa_size,
236
+ image_size=config.image_size,
237
+ normalize_input=normalize_input,
238
+ checkpoint_path=backbone_path,
239
+ )
240
+ model.eval()
241
+ return model
242
+
243
+ @staticmethod
244
+ def _resolve_file_path(
245
+ pretrained_model_name_or_path: str | os.PathLike[str],
246
+ *,
247
+ filename: str,
248
+ revision: str | None,
249
+ token: str | bool | None,
250
+ cache_dir: str | os.PathLike[str] | None,
251
+ local_files_only: bool,
252
+ ) -> str:
253
+ path = Path(pretrained_model_name_or_path)
254
+ if path.exists():
255
+ file_path = path / filename if path.is_dir() else path
256
+ if not file_path.exists():
257
+ raise FileNotFoundError(f"Missing file: {file_path}")
258
+ return str(file_path)
259
+
260
+ from huggingface_hub import hf_hub_download
261
+
262
+ return hf_hub_download(
263
+ repo_id=str(pretrained_model_name_or_path),
264
+ filename=filename,
265
+ repo_type="model",
266
+ revision=revision,
267
+ token=token,
268
+ cache_dir=cache_dir,
269
+ local_files_only=local_files_only,
270
+ )
271
+
272
+ @staticmethod
273
+ def _resolve_layer_names(extractor_config: dict[str, Any]) -> list[str]:
274
+ layer_names = extractor_config.get("layer_names")
275
+ if layer_names is None:
276
+ layer_names = extractor_config.get("loi")
277
+ if layer_names is None:
278
+ raise KeyError("extractor_config must contain `layer_names` or `loi`.")
279
+ return list(layer_names)
280
+
281
+ def forward_features(self, features: list[torch.Tensor]) -> torch.Tensor:
282
+ if len(features) != len(self.decoders):
283
+ raise ValueError(f"Expected {len(self.decoders)} feature tensors, got {len(features)}")
284
+ outputs = [decoder(feature) for decoder, feature in zip(self.decoders, features)]
285
+ output = torch.stack(outputs, dim=-1)
286
+ index = self.decoding_units.to(output.device).unsqueeze(0).unsqueeze(-1)
287
+ index = index.expand(output.shape[0], -1, -1)
288
+ return output.gather(dim=2, index=index).squeeze(-1)
289
+
290
+ def forward(self, videos: torch.Tensor, normalize: bool | None = None) -> torch.Tensor:
291
+ if self.vjepa is None or self.extractor is None:
292
+ raise RuntimeError("This model was loaded with load_vjepa=False.")
293
+ features = self.extractor(self.vjepa, videos, normalize=normalize)
294
+ return self.forward_features(features)
295
+
296
+ def predict_fmri(self, videos: torch.Tensor, normalize: bool | None = None) -> torch.Tensor:
297
+ """Predict z-scored fMRI beta responses for videos shaped [B, T, C, H, W]."""
298
+
299
+ return self(videos, normalize=normalize)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ torch
2
+ huggingface_hub
3
+ transformers
vitl.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5346856ec9df69487fe72a25bf2632aaa8112df33fb67708e3f7374edc1f7012
3
+ size 5127726842
vjepa2_offline_encoder.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3b2ec1499735098a1c97e67c84837241c349809f278d334f8c0e0c7b5ef1fe3b
3
+ size 2125320301