AbstractPhil commited on
Commit
e1b0541
·
verified ·
1 Parent(s): c7adf01

v2 modeling fix: match released checkpoint (encoder.layers.*, no untrained geo path); ModelOutput; opt-in diagnostics. Verified 0 missing/0 unexpected keys, bit-reproduces faithful load.

Browse files
Files changed (1) hide show
  1. modeling_geolip_vit.py +84 -141
modeling_geolip_vit.py CHANGED
@@ -3,46 +3,44 @@
3
  #
4
  # Usage:
5
  # from transformers import AutoModel
6
- # model = AutoModel.from_pretrained("AbstractPhil/geolip-vit-base-x3",
7
- # trust_remote_code=True)
8
  #
9
  # from torchvision import transforms
10
  # transform = transforms.Compose([
11
- # transforms.Resize((224, 224)),
12
  # transforms.ToTensor(),
13
  # transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
14
  # ])
15
  # pixel_values = transform(image).unsqueeze(0)
16
  # outputs = model(pixel_values)
17
  #
18
- # # 128-d embedding on hypersphere (L2-normalized)
19
- # embedding = outputs.embedding # (B, 128)
 
 
 
20
  #
21
- # # Multi-label classification logits (80 COCO classes)
22
- # logits = outputs.logits # (B, 80) — if soup_enabled
23
- #
24
- # # Triangulation distances to 256 constellation anchors
25
- # triangulation = outputs.triangulation # (B, 256)
26
- #
27
- # # Nearest anchor index per sample
28
- # nearest = outputs.nearest # (B,)
29
- #
30
- # # Geometric diagnostics
31
- # diagnostics = outputs.diagnostics # dict
32
  # ============================================================================
33
 
34
- import math
35
  import torch
36
  import torch.nn as nn
37
  import torch.nn.functional as F
38
- from transformers import PretrainedConfig, PreTrainedModel
39
- from dataclasses import dataclass, field
40
- from typing import Optional, Dict, Any
41
 
 
 
42
 
43
- # ══════════════════════════════════════════════════════════════════
44
- # CONFIG
45
- # ══════════════════════════════════════════════════════════════════
46
 
47
  class GeoLIPViTConfig(PretrainedConfig):
48
  model_type = "geolip_vit"
@@ -81,25 +79,21 @@ class GeoLIPViTConfig(PretrainedConfig):
81
  self.hidden_dropout_prob = hidden_dropout_prob
82
  self.soup_enabled = soup_enabled
83
  self.consensus_cv = consensus_cv
84
- self.experts = experts or ["clip_l14_openai", "dinov2_b14", "siglip_b16_384"]
85
-
86
 
87
- # ══════════════════════════════════════════════════════════════════
88
- # OUTPUT
89
- # ══════════════════════════════════════════════════════════════════
90
 
91
  @dataclass
92
- class GeoLIPViTOutput:
93
  """
94
- Output fields:
95
- embedding: (B, output_dim) L2-normalized on hypersphere
96
- logits: (B, n_classes) multi-label classification (if soup_enabled)
97
- triangulation: (B, n_anchors) distances to constellation anchors
98
- nearest: (B,) nearest anchor index
99
- patch_tokens: (B, n_patches, hidden_size) pre-pooling patch representations
100
- diagnostics: dict geometric metrics
101
  """
102
- embedding: torch.Tensor = None
103
  logits: Optional[torch.Tensor] = None
104
  triangulation: Optional[torch.Tensor] = None
105
  nearest: Optional[torch.Tensor] = None
@@ -107,15 +101,12 @@ class GeoLIPViTOutput:
107
  diagnostics: Optional[Dict[str, Any]] = None
108
 
109
 
110
- # ══════════════════════════════════════════════════════════════════
111
- # GEOMETRIC COMPONENTS
112
- # ══════════════════════════════════════════════════════════════════
113
-
114
  class Constellation(nn.Module):
115
  def __init__(self, n_anchors, d):
116
  super().__init__()
117
  self.n_anchors = n_anchors
118
- self.anchors = nn.Parameter(F.normalize(torch.randn(n_anchors, d), dim=-1))
 
119
 
120
  def triangulate(self, emb):
121
  a = F.normalize(self.anchors, dim=-1)
@@ -130,53 +121,38 @@ class Patchwork(nn.Module):
130
  self.n_anchors = n_anchors
131
  asgn = torch.arange(n_anchors) % n_comp
132
  self.register_buffer("asgn", asgn)
133
- # Compute input sizes from ints, not tensors (meta-tensor safe)
134
  anchors_per_comp = n_anchors // n_comp
135
  remainder = n_anchors % n_comp
136
  self.comps = nn.ModuleList([nn.Sequential(
137
- nn.Linear(anchors_per_comp + (1 if k < remainder else 0), d_comp * 2),
 
138
  nn.GELU(),
139
  nn.Linear(d_comp * 2, d_comp), nn.LayerNorm(d_comp))
140
  for k in range(n_comp)])
141
 
142
  def forward(self, tri):
143
  return torch.cat([self.comps[k](tri[:, self.asgn == k])
144
- for k in range(self.n_comp)], -1)
145
-
146
 
147
- # ══════════════════════════════════════════════════════════════════
148
- # MODEL
149
- # ══════════════════════════════════════════════════════════════════
150
 
151
  class GeoLIPViTModel(PreTrainedModel):
152
  """
153
- From-scratch Vision Transformer producing L2-normalized embeddings
154
- on a 128-d hypersphere, geometrically anchored by a constellation
155
- of 256 reference points trained via 3-expert consensus distillation.
156
-
157
- The encoder is trained from Xavier initialization against consensus
158
- targets from CLIP ViT-L/14, DINOv2 ViT-B/14, and SigLIP ViT-B/16.
159
-
160
- Optional soup pipeline (constellation + patchwork + classifier)
161
- provides multi-label COCO classification from the embedding.
162
-
163
- Output fields:
164
- embedding: (B, 128) L2-normalized, consensus-aligned
165
- logits: (B, 80) multi-label COCO logits (if soup_enabled)
166
- triangulation: (B, 256) distances to constellation anchors
167
- nearest: (B,) nearest anchor index
168
- patch_tokens: (B, 196, 384) pre-pooling patch representations
169
- diagnostics: dict geometric metrics
170
  """
171
  config_class = GeoLIPViTConfig
172
 
173
  def __init__(self, config):
174
  super().__init__(config)
175
  self.config = config
176
-
177
  n_patches = (config.image_size // config.patch_size) ** 2
178
 
179
- # ── Encoder ──
180
  self.patch_embed = nn.Conv2d(
181
  3, config.hidden_size,
182
  kernel_size=config.patch_size, stride=config.patch_size)
@@ -186,23 +162,20 @@ class GeoLIPViTModel(PreTrainedModel):
186
  self.embed_norm = nn.LayerNorm(config.hidden_size)
187
  self.embed_drop = nn.Dropout(config.hidden_dropout_prob)
188
 
189
- # Individual layers for geometric injection between each
190
- self.layers = nn.ModuleList([
191
- nn.TransformerEncoderLayer(
192
- d_model=config.hidden_size,
193
- nhead=config.num_attention_heads,
194
- dim_feedforward=config.intermediate_size,
195
- dropout=config.hidden_dropout_prob,
196
- activation="gelu",
197
- batch_first=True,
198
- norm_first=True)
199
- for _ in range(config.num_hidden_layers)])
200
-
201
- # Geometric injection: pool → anchor_dim → triangulate → hidden_size
202
- self.geo_pool_proj = nn.Linear(config.hidden_size, config.output_dim)
203
- self.geo_tri_proj = nn.Sequential(
204
- nn.Linear(config.n_anchors, config.hidden_size), nn.GELU(),
205
- nn.LayerNorm(config.hidden_size))
206
 
207
  self.output_proj = nn.Sequential(
208
  nn.Linear(config.hidden_size, config.hidden_size),
@@ -211,9 +184,9 @@ class GeoLIPViTModel(PreTrainedModel):
211
  nn.Linear(config.hidden_size, config.output_dim),
212
  )
213
 
214
- # ── Soup Pipeline (optional) ──
215
  if getattr(config, "soup_enabled", False):
216
- self.constellation = Constellation(config.n_anchors, config.output_dim)
 
217
  self.patchwork = Patchwork(
218
  config.n_anchors, config.n_comp, config.d_comp)
219
  pw_dim = config.n_comp * config.d_comp
@@ -241,68 +214,38 @@ class GeoLIPViTModel(PreTrainedModel):
241
  nn.init.ones_(module.weight)
242
  nn.init.zeros_(module.bias)
243
 
244
- def forward(self, pixel_values, output_patch_tokens=False, **kwargs):
 
245
  B = pixel_values.shape[0]
246
-
247
- # ── Encode ──
248
- x = self.patch_embed(pixel_values)
249
- x = x.flatten(2).transpose(1, 2)
250
-
251
- cls = self.cls_token.expand(B, -1, -1)
252
- x = torch.cat([cls, x], dim=1)
253
  x = x + self.pos_embed
254
  x = self.embed_drop(self.embed_norm(x))
 
255
 
256
- # ── Transformer with geometric injection ──
257
- # Get anchors for triangulation (from constellation if available)
258
- if self.constellation is not None:
259
- anchors_n = F.normalize(self.constellation.anchors.detach(), dim=-1)
260
- else:
261
- anchors_n = None
262
-
263
- for layer in self.layers:
264
- if anchors_n is not None:
265
- # Pool → project → triangulate → geo token
266
- pooled = x[:, 1:, :].mean(dim=1)
267
- geo_128 = F.normalize(self.geo_pool_proj(pooled), dim=-1)
268
- tri_dists = 1.0 - geo_128 @ anchors_n.T
269
- geo_token = self.geo_tri_proj(tri_dists).unsqueeze(1)
270
- x_with_geo = torch.cat([geo_token, x], dim=1)
271
- x_with_geo = layer(x_with_geo)
272
- x = x_with_geo[:, 1:, :]
273
- else:
274
- x = layer(x)
275
-
276
- # ── Pool + Project ──
277
  patch_tokens = x[:, 1:, :]
278
- pooled = patch_tokens.mean(dim=1)
279
- embedding = F.normalize(self.output_proj(pooled), dim=-1)
280
-
281
- # ── Soup Pipeline ──
282
- logits = None
283
- triangulation = None
284
- nearest = None
285
- diagnostics = {}
286
 
 
 
287
  if self.constellation is not None:
288
- tri, near = self.constellation.triangulate(embedding)
289
- triangulation = tri
290
- nearest = near
291
-
292
  if self.patchwork is not None and self.classifier is not None:
293
- pw = self.patchwork(tri)
294
  logits = self.classifier(torch.cat([pw, embedding], -1))
295
-
296
- # Geometric diagnostics
297
- with torch.no_grad():
298
- anchors_n = F.normalize(self.constellation.anchors, dim=-1)
299
- cos_to_anchors = embedding @ anchors_n.T
300
- diagnostics = {
301
- "nearest_cos": cos_to_anchors.max(dim=-1).values.mean().item(),
302
- "mean_anchor_cos": cos_to_anchors.mean().item(),
303
- "n_active_anchors": near.unique().numel(),
304
- "embedding_norm": embedding.norm(dim=-1).mean().item(),
305
- }
 
306
 
307
  return GeoLIPViTOutput(
308
  embedding=embedding,
@@ -311,4 +254,4 @@ class GeoLIPViTModel(PreTrainedModel):
311
  nearest=nearest,
312
  patch_tokens=patch_tokens if output_patch_tokens else None,
313
  diagnostics=diagnostics,
314
- )
 
3
  #
4
  # Usage:
5
  # from transformers import AutoModel
6
+ # model = AutoModel.from_pretrained("AbstractPhil/geolip-vit-large-x3",
7
+ # trust_remote_code=True).eval()
8
  #
9
  # from torchvision import transforms
10
  # transform = transforms.Compose([
11
+ # transforms.Resize((224, 224)), # squash resize (no crop)
12
  # transforms.ToTensor(),
13
  # transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
14
  # ])
15
  # pixel_values = transform(image).unsqueeze(0)
16
  # outputs = model(pixel_values)
17
  #
18
+ # outputs.embedding # (B, 128) L2-normalized consensus-space vector
19
+ # outputs.logits # (B, 80) multi-label COCO logits (soup head)
20
+ # outputs.triangulation # (B, 256) 1 - cos to constellation anchors
21
+ # outputs.nearest # (B,) nearest anchor index
22
+ # outputs = model(px, compute_diagnostics=True) # adds .diagnostics dict
23
  #
24
+ # REVISION 2026-07-27 (v2): the original remote-code file did not match the
25
+ # released checkpoint — the transformer was saved under `encoder.layers.*`
26
+ # (nn.TransformerEncoder) but the class declared per-layer `layers.*` plus a
27
+ # geometric-injection path (`geo_pool_proj`/`geo_tri_proj`) that was never
28
+ # trained, so AutoModel silently returned a mostly-random model. This
29
+ # revision matches the checkpoint exactly (verified: zero missing / zero
30
+ # unexpected keys; COCO val anchor coverage and mAP reproduce the training
31
+ # run's TensorBoard values). The output object is now a transformers
32
+ # ModelOutput, and diagnostics are opt-in (they force a GPU sync).
 
 
33
  # ============================================================================
34
 
 
35
  import torch
36
  import torch.nn as nn
37
  import torch.nn.functional as F
38
+ from dataclasses import dataclass
39
+ from typing import Any, Dict, Optional
 
40
 
41
+ from transformers import PretrainedConfig, PreTrainedModel
42
+ from transformers.utils import ModelOutput
43
 
 
 
 
44
 
45
  class GeoLIPViTConfig(PretrainedConfig):
46
  model_type = "geolip_vit"
 
79
  self.hidden_dropout_prob = hidden_dropout_prob
80
  self.soup_enabled = soup_enabled
81
  self.consensus_cv = consensus_cv
82
+ self.experts = experts or ["clip_l14_openai", "dinov2_b14",
83
+ "siglip_b16_384"]
84
 
 
 
 
85
 
86
  @dataclass
87
+ class GeoLIPViTOutput(ModelOutput):
88
  """
89
+ embedding: (B, output_dim) L2-normalized on the consensus sphere
90
+ logits: (B, n_classes) multi-label logits (if soup_enabled)
91
+ triangulation: (B, n_anchors) 1 - cos to constellation anchors
92
+ nearest: (B,) nearest anchor index
93
+ patch_tokens: (B, n_patches, hidden_size) if output_patch_tokens
94
+ diagnostics: dict only if compute_diagnostics=True
 
95
  """
96
+ embedding: Optional[torch.Tensor] = None
97
  logits: Optional[torch.Tensor] = None
98
  triangulation: Optional[torch.Tensor] = None
99
  nearest: Optional[torch.Tensor] = None
 
101
  diagnostics: Optional[Dict[str, Any]] = None
102
 
103
 
 
 
 
 
104
  class Constellation(nn.Module):
105
  def __init__(self, n_anchors, d):
106
  super().__init__()
107
  self.n_anchors = n_anchors
108
+ self.anchors = nn.Parameter(
109
+ F.normalize(torch.randn(n_anchors, d), dim=-1))
110
 
111
  def triangulate(self, emb):
112
  a = F.normalize(self.anchors, dim=-1)
 
121
  self.n_anchors = n_anchors
122
  asgn = torch.arange(n_anchors) % n_comp
123
  self.register_buffer("asgn", asgn)
 
124
  anchors_per_comp = n_anchors // n_comp
125
  remainder = n_anchors % n_comp
126
  self.comps = nn.ModuleList([nn.Sequential(
127
+ nn.Linear(anchors_per_comp + (1 if k < remainder else 0),
128
+ d_comp * 2),
129
  nn.GELU(),
130
  nn.Linear(d_comp * 2, d_comp), nn.LayerNorm(d_comp))
131
  for k in range(n_comp)])
132
 
133
  def forward(self, tri):
134
  return torch.cat([self.comps[k](tri[:, self.asgn == k])
135
+ for k in range(self.n_comp)], -1)
 
136
 
 
 
 
137
 
138
  class GeoLIPViTModel(PreTrainedModel):
139
  """
140
+ From-scratch Vision Transformer producing L2-normalized embeddings on
141
+ a 128-d hypersphere — the 3-expert consensus space distilled from
142
+ CLIP ViT-L/14 + DINOv2 ViT-B/14 + SigLIP ViT-B/16-384 (GPA-aligned,
143
+ PCA-128, whitened-Procrustes calibrated) on COCO-2017.
144
+
145
+ The soup pipeline (constellation of 256 anchors + 8-compartment
146
+ patchwork + classifier) is the frozen calibrated teacher head and
147
+ yields 80-class multi-label COCO logits from the embedding.
 
 
 
 
 
 
 
 
 
148
  """
149
  config_class = GeoLIPViTConfig
150
 
151
  def __init__(self, config):
152
  super().__init__(config)
153
  self.config = config
 
154
  n_patches = (config.image_size // config.patch_size) ** 2
155
 
 
156
  self.patch_embed = nn.Conv2d(
157
  3, config.hidden_size,
158
  kernel_size=config.patch_size, stride=config.patch_size)
 
162
  self.embed_norm = nn.LayerNorm(config.hidden_size)
163
  self.embed_drop = nn.Dropout(config.hidden_dropout_prob)
164
 
165
+ # Matches the released checkpoint: nn.TransformerEncoder saved as
166
+ # `encoder.layers.*`. (The v1 remote-code per-layer ModuleList +
167
+ # geometric-injection path never existed in the weights.)
168
+ encoder_layer = nn.TransformerEncoderLayer(
169
+ d_model=config.hidden_size,
170
+ nhead=config.num_attention_heads,
171
+ dim_feedforward=config.intermediate_size,
172
+ dropout=config.hidden_dropout_prob,
173
+ activation="gelu",
174
+ batch_first=True,
175
+ norm_first=True)
176
+ self.encoder = nn.TransformerEncoder(
177
+ encoder_layer, num_layers=config.num_hidden_layers,
178
+ enable_nested_tensor=False)
 
 
 
179
 
180
  self.output_proj = nn.Sequential(
181
  nn.Linear(config.hidden_size, config.hidden_size),
 
184
  nn.Linear(config.hidden_size, config.output_dim),
185
  )
186
 
 
187
  if getattr(config, "soup_enabled", False):
188
+ self.constellation = Constellation(config.n_anchors,
189
+ config.output_dim)
190
  self.patchwork = Patchwork(
191
  config.n_anchors, config.n_comp, config.d_comp)
192
  pw_dim = config.n_comp * config.d_comp
 
214
  nn.init.ones_(module.weight)
215
  nn.init.zeros_(module.bias)
216
 
217
+ def forward(self, pixel_values, output_patch_tokens=False,
218
+ compute_diagnostics=False, **kwargs):
219
  B = pixel_values.shape[0]
220
+ x = self.patch_embed(pixel_values).flatten(2).transpose(1, 2)
221
+ x = torch.cat([self.cls_token.expand(B, -1, -1), x], dim=1)
 
 
 
 
 
222
  x = x + self.pos_embed
223
  x = self.embed_drop(self.embed_norm(x))
224
+ x = self.encoder(x)
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  patch_tokens = x[:, 1:, :]
227
+ embedding = F.normalize(self.output_proj(patch_tokens.mean(dim=1)),
228
+ dim=-1)
 
 
 
 
 
 
229
 
230
+ logits = triangulation = nearest = None
231
+ diagnostics = None
232
  if self.constellation is not None:
233
+ triangulation, nearest = self.constellation.triangulate(embedding)
 
 
 
234
  if self.patchwork is not None and self.classifier is not None:
235
+ pw = self.patchwork(triangulation)
236
  logits = self.classifier(torch.cat([pw, embedding], -1))
237
+ if compute_diagnostics:
238
+ with torch.no_grad():
239
+ a_n = F.normalize(self.constellation.anchors, dim=-1)
240
+ cos = embedding @ a_n.T
241
+ diagnostics = {
242
+ "nearest_cos":
243
+ cos.max(dim=-1).values.mean().item(),
244
+ "mean_anchor_cos": cos.mean().item(),
245
+ "n_active_anchors": nearest.unique().numel(),
246
+ "embedding_norm":
247
+ embedding.norm(dim=-1).mean().item(),
248
+ }
249
 
250
  return GeoLIPViTOutput(
251
  embedding=embedding,
 
254
  nearest=nearest,
255
  patch_tokens=patch_tokens if output_patch_tokens else None,
256
  diagnostics=diagnostics,
257
+ )