Accept loi extractor config alias
Browse files- modeling_vjepa2_fmri_encoder.py +60 -22
modeling_vjepa2_fmri_encoder.py
CHANGED
|
@@ -38,8 +38,6 @@ class HookedFeatureExtractor:
|
|
| 38 |
self.layer_names = list(layer_names)
|
| 39 |
self.ret_type = ret_type
|
| 40 |
self.spatial_pool = int(spatial_pool)
|
| 41 |
-
self.outputs: dict[str, torch.Tensor] = {}
|
| 42 |
-
self.hooks = []
|
| 43 |
|
| 44 |
@staticmethod
|
| 45 |
def _get_layer(model: nn.Module, layer_name: str) -> nn.Module:
|
|
@@ -50,24 +48,33 @@ class HookedFeatureExtractor:
|
|
| 50 |
raise TypeError(f"{layer_name} did not resolve to a torch module")
|
| 51 |
return layer
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
def __call__(self, model: nn.Module, videos: torch.Tensor, **model_kwargs) -> list[torch.Tensor]:
|
| 54 |
-
|
| 55 |
-
|
| 56 |
self._get_layer(model, name).register_forward_hook(
|
| 57 |
-
lambda _module, _inputs, output, name=name:
|
| 58 |
)
|
| 59 |
for name in self.layer_names
|
| 60 |
]
|
| 61 |
try:
|
| 62 |
model(videos, **model_kwargs)
|
| 63 |
finally:
|
| 64 |
-
for hook in
|
| 65 |
hook.remove()
|
| 66 |
-
|
| 67 |
-
return [self._process_feature(self.outputs[name]) for name in self.layer_names]
|
| 68 |
|
| 69 |
def _process_feature(self, feature: torch.Tensor) -> torch.Tensor:
|
| 70 |
-
batch,
|
| 71 |
feature = feature.reshape(batch, -1, 14, 14, channels).permute(0, 1, 4, 2, 3)
|
| 72 |
if self.spatial_pool > 1:
|
| 73 |
batch, frames, channels, height, width = feature.shape
|
|
@@ -90,8 +97,8 @@ class HookedFeatureExtractor:
|
|
| 90 |
raise ValueError(f"Unsupported ret_type: {self.ret_type}")
|
| 91 |
|
| 92 |
|
| 93 |
-
class
|
| 94 |
-
def __init__(self, size: str, image_size: int, normalize_input: bool) -> None:
|
| 95 |
super().__init__()
|
| 96 |
self.image_size = int(image_size)
|
| 97 |
self.normalize_input = bool(normalize_input)
|
|
@@ -102,8 +109,13 @@ class VJEPA2Backbone(nn.Module):
|
|
| 102 |
"huge": "vjepa2_vit_huge",
|
| 103 |
"giant": "vjepa2_vit_giant",
|
| 104 |
}[size]
|
| 105 |
-
backbone = torch.hub.load("facebookresearch/vjepa2", hub_name, pretrained=
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
def forward(self, videos: torch.Tensor, normalize: bool | None = None) -> torch.Tensor:
|
| 109 |
if videos.ndim != 5:
|
|
@@ -132,6 +144,14 @@ class VJEPA2Backbone(nn.Module):
|
|
| 132 |
return self.backbone(videos.permute(0, 2, 1, 3, 4))
|
| 133 |
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
class VJEPA2FMRIEncoderModel(PreTrainedModel):
|
| 136 |
config_class = VJEPA2FMRIEncoderConfig
|
| 137 |
base_model_prefix = "vjepa2_fmri_encoder"
|
|
@@ -142,7 +162,7 @@ class VJEPA2FMRIEncoderModel(PreTrainedModel):
|
|
| 142 |
self.decoders = nn.ModuleList()
|
| 143 |
self.register_buffer("decoding_units", torch.empty(0, dtype=torch.long))
|
| 144 |
self.extractor: HookedFeatureExtractor | None = None
|
| 145 |
-
self.vjepa:
|
| 146 |
|
| 147 |
@classmethod
|
| 148 |
def from_pretrained(
|
|
@@ -176,7 +196,7 @@ class VJEPA2FMRIEncoderModel(PreTrainedModel):
|
|
| 176 |
local_files_only=local_files_only,
|
| 177 |
)
|
| 178 |
|
| 179 |
-
checkpoint_path = cls.
|
| 180 |
pretrained_model_name_or_path,
|
| 181 |
filename=config.checkpoint_filename,
|
| 182 |
revision=revision,
|
|
@@ -197,22 +217,31 @@ class VJEPA2FMRIEncoderModel(PreTrainedModel):
|
|
| 197 |
vjepa_size = config.vjepa_size if vjepa_size is None else vjepa_size
|
| 198 |
normalize_input = config.normalize_input if normalize_input is None else bool(normalize_input)
|
| 199 |
if load_vjepa:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
extractor_config = checkpoint["extractor_config"]
|
| 201 |
model.extractor = HookedFeatureExtractor(
|
| 202 |
-
layer_names=extractor_config
|
| 203 |
ret_type=extractor_config.get("ret_type", "chw"),
|
| 204 |
spatial_pool=extractor_config.get("spatial_pool", 14),
|
| 205 |
)
|
| 206 |
-
model.vjepa =
|
| 207 |
size=vjepa_size,
|
| 208 |
image_size=config.image_size,
|
| 209 |
normalize_input=normalize_input,
|
|
|
|
| 210 |
)
|
| 211 |
model.eval()
|
| 212 |
return model
|
| 213 |
|
| 214 |
@staticmethod
|
| 215 |
-
def
|
| 216 |
pretrained_model_name_or_path: str | os.PathLike[str],
|
| 217 |
*,
|
| 218 |
filename: str,
|
|
@@ -223,10 +252,10 @@ class VJEPA2FMRIEncoderModel(PreTrainedModel):
|
|
| 223 |
) -> str:
|
| 224 |
path = Path(pretrained_model_name_or_path)
|
| 225 |
if path.exists():
|
| 226 |
-
|
| 227 |
-
if not
|
| 228 |
-
raise FileNotFoundError(f"Missing
|
| 229 |
-
return str(
|
| 230 |
|
| 231 |
from huggingface_hub import hf_hub_download
|
| 232 |
|
|
@@ -240,6 +269,15 @@ class VJEPA2FMRIEncoderModel(PreTrainedModel):
|
|
| 240 |
local_files_only=local_files_only,
|
| 241 |
)
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
def forward_features(self, features: list[torch.Tensor]) -> torch.Tensor:
|
| 244 |
if len(features) != len(self.decoders):
|
| 245 |
raise ValueError(f"Expected {len(self.decoders)} feature tensors, got {len(features)}")
|
|
|
|
| 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:
|
|
|
|
| 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
|
|
|
|
| 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)
|
|
|
|
| 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:
|
|
|
|
| 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"
|
|
|
|
| 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(
|
|
|
|
| 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,
|
|
|
|
| 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,
|
|
|
|
| 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 |
|
|
|
|
| 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)}")
|