AbstractPhil commited on
Commit
a06aad3
·
verified ·
1 Parent(s): 7d843f4

AutoModel: safetensors + config + modeling + card

Browse files
Files changed (4) hide show
  1. README.md +119 -0
  2. config.json +22 -0
  3. model.safetensors +3 -0
  4. modeling_captionbert.py +209 -209
README.md ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language: [en]
4
+ library_name: transformers
5
+ pipeline_tag: feature-extraction
6
+ tags: [sentence-similarity, feature-extraction, consensus-distillation, geometric-deep-learning, amoe]
7
+ datasets: [AbstractPhil/conceptual-captions-12m-webdataset-berts]
8
+ base_model: [google-bert/bert-base-uncased, answerdotai/ModernBERT-base, FacebookAI/roberta-base, albert/albert-base-v2, distilbert/distilbert-base-uncased]
9
+ ---
10
+
11
+ # captionbert-8192-v2
12
+
13
+ A **58.3M** standalone sentence encoder distilled from the geometric **consensus**
14
+ of five BERT-family teachers. No expert models at inference: tokenizer + this
15
+ model, 768-d L2-normalized output.
16
+
17
+ 12 layers, 512-d, 8 heads, FFN 2048, 8192 position capacity. **0.53x bert-base.**
18
+
19
+ ```python
20
+ from transformers import AutoModel, AutoTokenizer
21
+ model = AutoModel.from_pretrained("AbstractPhil/captionbert-8192-v2", trust_remote_code=True)
22
+ tok = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
23
+
24
+ emb = model.encode(["a cat on a windowsill", "a feline by the window"]) # (2, 768)
25
+ (emb[0] @ emb[1]).item()
26
+ ```
27
+
28
+ ## Results
29
+
30
+ Measured in one harness; every model mean-pooled and L2-normalized, no task
31
+ tuning. `erank` is the participation ratio of the embedding spectrum -- how many
32
+ of the 768 directions are actually used.
33
+
34
+ | model | params | STS-B rho | SICK-R rho | self_cos | erank |
35
+ |---|---|---|---|---|---|
36
+ | bert-base | 109.5M | .4729 | .5865 | +.580 | 32.0 |
37
+ | ModernBERT-base | 149.0M | .4215 | .5479 | +.948 | -- |
38
+ | roberta-base | 124.6M | .5436 | .6296 | +.976 | -- |
39
+ | albert-base-v2 | 11.7M | .4784 | .5364 | +.905 | -- |
40
+ | distilbert | 66.4M | .5717 | .6424 | +.840 | -- |
41
+ | **captionbert-8192-v2** | **58.3M** | **.5747** | **.6526** | **+.129** | 33.4 |
42
+ | all-MiniLM-L6-v2 (ref) | 22.7M | .8203 | .7758 | +.023 | 94.3 |
43
+
44
+ **It edges every teacher it was distilled from**, at 13% of their combined
45
+ parameters, having never seen a similarity label. It does **not** reach
46
+ `all-MiniLM-L6-v2`, which was contrastively trained on 1B+ curated pairs --
47
+ a different comparison class.
48
+
49
+ **Isotropy is the mechanism.** Mean-pooled BERT-family embeddings sit in a narrow
50
+ cone (self_cos .58-.98); this model reads **+.129**, and cosine discriminates far
51
+ better in a space that is not collapsed.
52
+
53
+ ## With an AMOE anchor (`amoe/`)
54
+
55
+ The trunk is frozen; adapters are 1.6M params each. Anchors ship in this repo
56
+ under `amoe/` and toggle **bit-exact** -- all anchors disabled reproduces the
57
+ bare trunk exactly, so one artifact serves both.
58
+
59
+ | config | STS-B rho | SICK-R rho |
60
+ |---|---|---|
61
+ | bare trunk | .5747 | .6526 |
62
+ | + `equiv` anchor (all-nli) | .7254 | **.7550** |
63
+ | + `simplify` anchor (wiki/altlex/compression) | .7400 | .7075 |
64
+ | + **2-anchor dispatch (MOE)** | **.7524** | .7380 |
65
+
66
+ The two anchors are complementary along the task axis, and the router separated
67
+ them: mean `|w/z|` moved from .310/.380 (blend) to .645/.223 (specialize) over
68
+ 800 keys-only steps. See [amoe-lora](https://github.com/AbstractEyes/amoe-lora).
69
+
70
+ ## How it was built
71
+
72
+ 1. Five teachers embedded 33M CC12M llava-next captions (mean-pooled, 768-d).
73
+ 2. One global **whitened Procrustes** map per teacher into `bert-base`'s frame,
74
+ fit on a stratified random sample and **reported out-of-sample**.
75
+ 3. Consensus = normalized centroid of the aligned teachers, per chunk.
76
+ 4. Student trained from scratch: InfoNCE(T=0.07) + per-sample MSE against the
77
+ consensus. Pure Adam, no weight decay. 26.9M rows, 52,548 steps at batch
78
+ 2048, ~5.4 h on one RTX 6000 Pro.
79
+
80
+ ## Known limits -- read before using
81
+
82
+ - **Consensus rank is ~28.7 of 768.** Five BERT-family teachers only agree on
83
+ about 29 directions. The student uses ~103 in domain but falls back to ~33 on
84
+ out-of-domain text: **the structure it builds on captions does not transfer.**
85
+ This is the model's ceiling and it is a property of the consensus, not the
86
+ student.
87
+ - **Alignment quality varies by teacher.** Out-of-sample cosine to the bert
88
+ frame: distil .625, roberta .372, albert .331, modern .327. The ordering
89
+ tracks architectural distance from bert-base.
90
+ - **10 of 66 source chunks lacked ModernBERT**, so 54 chunks (~27M rows) were
91
+ used. No 4-expert fallback: that would change the target definition mid-dataset.
92
+ - Trained on image captions. Expect caption-like text to be its strongest domain.
93
+ - Single seed. No variance estimate.
94
+
95
+ ## Output convention (differs from v1)
96
+
97
+ | field | shape | |
98
+ |---|---|---|
99
+ | `last_hidden_state` | (B, L, 512) | token states |
100
+ | `pooler_output` | (B, 768) | **the embedding**, L2-normalized |
101
+ | `embedding` | (B, 768) | alias |
102
+
103
+ `geolip-captionbert-8192` (v1) returned the pooled embedding as
104
+ `last_hidden_state`. If porting v1 code, use `pooler_output`. v1 also shipped an
105
+ `AlignmentBank`; v2 does not -- measured on v1, its expert-consistency features
106
+ varied 0.2% across samples because a rotation round-trip carries no data.
107
+
108
+ ## Citation
109
+
110
+ ```bibtex
111
+ @misc{abstractphil2026captionbertv2,
112
+ title = {captionbert-8192-v2: consensus distillation at CC12M scale},
113
+ author = {AbstractPhil},
114
+ year = {2026},
115
+ url = {https://huggingface.co/AbstractPhil/captionbert-8192-v2}
116
+ }
117
+ ```
118
+
119
+ MIT.
config.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "CaptionBertV2Model"
4
+ ],
5
+ "model_type": "captionbert_v2",
6
+ "auto_map": {
7
+ "AutoConfig": "modeling_captionbert.CaptionBertV2Config",
8
+ "AutoModel": "modeling_captionbert.CaptionBertV2Model"
9
+ },
10
+ "vocab_size": 30522,
11
+ "hidden_size": 512,
12
+ "num_hidden_layers": 12,
13
+ "num_attention_heads": 8,
14
+ "intermediate_size": 2048,
15
+ "output_dim": 768,
16
+ "max_position_embeddings": 8192,
17
+ "hidden_dropout_prob": 0.1,
18
+ "pad_token_id": 0,
19
+ "pooling": "mean",
20
+ "torch_dtype": "float32",
21
+ "tokenizer_class": "BertTokenizerFast"
22
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:303bfdbe4c2e0a920124849a5b72226afad6091172e05c74332d6a31c458f5f3
3
+ size 233251232
modeling_captionbert.py CHANGED
@@ -1,210 +1,210 @@
1
- # ============================================================================
2
- # modeling_captionbert.py -- AbstractPhil/captionbert-8192-v2
3
- #
4
- # from transformers import AutoModel, AutoTokenizer
5
- # model = AutoModel.from_pretrained("AbstractPhil/captionbert-8192-v2",
6
- # trust_remote_code=True)
7
- # tok = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
8
- # out = model(**tok(["a cat on a windowsill"], return_tensors="pt"))
9
- # emb = out.pooler_output # (B, 768) L2-normalized
10
- #
11
- # OR just: emb = model.encode(["a cat on a windowsill"])
12
- #
13
- # ---------------------------------------------------------------------------
14
- # BREAKING CHANGE FROM v1 -- READ THIS IF YOU USED geolip-captionbert-8192
15
- # v1 returned the POOLED 768-d embedding as `last_hidden_state`. That is not
16
- # the transformers convention and it silently breaks anything expecting token
17
- # states. v2 follows the convention:
18
- # last_hidden_state : (B, L, 512) token states
19
- # pooler_output : (B, 768) L2-normalized embedding <-- the product
20
- # embedding : (B, 768) alias for pooler_output
21
- # If you are porting v1 code, `last_hidden_state` -> `pooler_output`.
22
- #
23
- # v1 also shipped an AlignmentBank. v2 does NOT. Measured on v1: the bank's
24
- # expert-consistency block varied 0.2% across samples and took 0.23% of its
25
- # projection energy while anchor distances took 98.70% -- because
26
- # `back = x @ R.T @ R` is a rotation round-trip and carries no data. Content
27
- # extensions belong in an AMOE anchor, which this repo ships separately.
28
- # ============================================================================
29
-
30
- from dataclasses import dataclass
31
- from typing import List, Optional, Tuple, Union
32
-
33
- import torch
34
- import torch.nn as nn
35
- import torch.nn.functional as F
36
- from transformers import PretrainedConfig, PreTrainedModel
37
- from transformers.modeling_outputs import BaseModelOutputWithPooling
38
-
39
-
40
- class CaptionBertV2Config(PretrainedConfig):
41
- model_type = "captionbert_v2"
42
-
43
- def __init__(
44
- self,
45
- vocab_size: int = 30522,
46
- hidden_size: int = 512, # d_model
47
- num_hidden_layers: int = 12,
48
- num_attention_heads: int = 8,
49
- intermediate_size: int = 2048,
50
- output_dim: int = 768, # consensus space
51
- max_position_embeddings: int = 8192,
52
- hidden_dropout_prob: float = 0.1,
53
- pad_token_id: int = 0,
54
- pooling: str = "mean", # "mean" | "cls"
55
- **kwargs,
56
- ):
57
- super().__init__(pad_token_id=pad_token_id, **kwargs)
58
- self.vocab_size = vocab_size
59
- self.hidden_size = hidden_size
60
- self.num_hidden_layers = num_hidden_layers
61
- self.num_attention_heads = num_attention_heads
62
- self.intermediate_size = intermediate_size
63
- self.output_dim = output_dim
64
- self.max_position_embeddings = max_position_embeddings
65
- self.hidden_dropout_prob = hidden_dropout_prob
66
- self.pooling = pooling
67
-
68
-
69
- class CaptionBertV2Model(PreTrainedModel):
70
- """
71
- Standalone caption/sentence encoder distilled from the geometric consensus
72
- of five BERT-family teachers. No expert models at inference.
73
-
74
- Parameter names are deliberately NOT namespaced under a submodule so that
75
- the training checkpoint loads unchanged: token_emb, pos_emb, emb_norm,
76
- encoder.layers.*, output_proj.*.
77
- """
78
-
79
- config_class = CaptionBertV2Config
80
- base_model_prefix = "captionbert_v2"
81
- supports_gradient_checkpointing = True
82
-
83
- def __init__(self, config: CaptionBertV2Config):
84
- super().__init__(config)
85
- d = config.hidden_size
86
- self.token_emb = nn.Embedding(config.vocab_size, d,
87
- padding_idx=config.pad_token_id)
88
- self.pos_emb = nn.Embedding(config.max_position_embeddings, d)
89
- self.emb_norm = nn.LayerNorm(d)
90
- self.emb_drop = nn.Dropout(config.hidden_dropout_prob)
91
- layer = nn.TransformerEncoderLayer(
92
- d_model=d,
93
- nhead=config.num_attention_heads,
94
- dim_feedforward=config.intermediate_size,
95
- dropout=config.hidden_dropout_prob,
96
- activation="gelu",
97
- batch_first=True,
98
- norm_first=True,
99
- )
100
- self.encoder = nn.TransformerEncoder(
101
- layer, num_layers=config.num_hidden_layers, enable_nested_tensor=False)
102
- self.output_proj = nn.Sequential(
103
- nn.Linear(d, d), nn.GELU(), nn.LayerNorm(d), nn.Linear(d, config.output_dim))
104
- self.post_init()
105
-
106
- # -- HF plumbing --
107
- def get_input_embeddings(self):
108
- return self.token_emb
109
-
110
- def set_input_embeddings(self, value):
111
- self.token_emb = value
112
-
113
- def forward(
114
- self,
115
- input_ids: torch.LongTensor = None,
116
- attention_mask: Optional[torch.Tensor] = None,
117
- output_hidden_states: Optional[bool] = None,
118
- return_dict: Optional[bool] = None,
119
- **kwargs,
120
- ) -> Union[Tuple, BaseModelOutputWithPooling]:
121
- return_dict = return_dict if return_dict is not None else True
122
- L = input_ids.shape[1]
123
- pos = torch.arange(L, device=input_ids.device).unsqueeze(0)
124
- x = self.emb_drop(self.emb_norm(self.token_emb(input_ids) + self.pos_emb(pos)))
125
-
126
- kpm = (~attention_mask.bool()) if attention_mask is not None \
127
- else (input_ids == self.config.pad_token_id)
128
-
129
- hidden = [x] if output_hidden_states else None
130
- # Iterate the layers directly rather than calling self.encoder(...):
131
- # nn.TransformerEncoder's fast path inspects layer types, and an AMOE
132
- # anchor wraps each layer in a BlockWithAdapter that is not a
133
- # TransformerEncoderLayer. This keeps attach() a drop-in.
134
- for mod in self.encoder.layers:
135
- x = mod(x, src_key_padding_mask=kpm)
136
- if output_hidden_states:
137
- hidden.append(x)
138
- if self.encoder.norm is not None:
139
- x = self.encoder.norm(x)
140
-
141
- if self.config.pooling == "cls":
142
- pooled = x[:, 0]
143
- else:
144
- m = (attention_mask.unsqueeze(-1).to(x.dtype) if attention_mask is not None
145
- else (~kpm).unsqueeze(-1).to(x.dtype))
146
- pooled = (x * m).sum(1) / m.sum(1).clamp(min=1)
147
- embedding = F.normalize(self.output_proj(pooled), dim=-1)
148
-
149
- if not return_dict:
150
- return (x, embedding) + ((tuple(hidden),) if output_hidden_states else ())
151
- out = BaseModelOutputWithPooling(
152
- last_hidden_state=x, # (B, L, 512) token states
153
- pooler_output=embedding, # (B, 768) THE PRODUCT
154
- hidden_states=tuple(hidden) if output_hidden_states else None,
155
- )
156
- out.embedding = embedding # explicit alias
157
- return out
158
-
159
- @torch.no_grad()
160
- def encode(self, texts, tokenizer=None, batch_size: int = 128,
161
- max_length: int = 256, device=None) -> torch.Tensor:
162
- """Raw text -> (N, 768) L2-normalized embeddings."""
163
- if isinstance(texts, str):
164
- texts = [texts]
165
- if tokenizer is None:
166
- from transformers import AutoTokenizer
167
- tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
168
- device = device or next(self.parameters()).device
169
- was_training = self.training
170
- self.eval()
171
- out = []
172
- for i in range(0, len(texts), batch_size):
173
- t = tokenizer(list(texts[i:i + batch_size]), max_length=max_length,
174
- padding=True, truncation=True, return_tensors="pt").to(device)
175
- out.append(self(**t).pooler_output.float().cpu())
176
- if was_training:
177
- self.train()
178
- return torch.cat(out)
179
-
180
-
181
- # ---------------------------------------------------------------------------
182
- # AMOE binding -- lets amoe-lora attach anchors to this trunk unmodified.
183
- #
184
- # import amoe
185
- # from modeling_captionbert import CaptionBertV2Binding
186
- # h = amoe.attach(model, "amoe/moe/equiv.anchor.pt",
187
- # binding=CaptionBertV2Binding(d=model.config.hidden_size))
188
- #
189
- # amoe's PathBinding would find encoder.layers by dotted path but then read
190
- # model.config.hidden_size -- which works here because this IS a
191
- # PretrainedConfig. The explicit binding is kept for plain-nn.Module use.
192
- # ---------------------------------------------------------------------------
193
-
194
- @dataclass
195
- class CaptionBertV2Binding:
196
- d: int = 512
197
- name: str = "captionbert_v2"
198
-
199
- def layers(self, model):
200
- return model.encoder.layers
201
-
202
- def set_layers(self, model, new):
203
- model.encoder.layers = nn.ModuleList(new)
204
-
205
- def hidden_size(self, model) -> int:
206
- return int(self.d)
207
-
208
-
209
- CaptionBertV2Config.register_for_auto_class()
210
  CaptionBertV2Model.register_for_auto_class("AutoModel")
 
1
+ # ============================================================================
2
+ # modeling_captionbert.py -- AbstractPhil/captionbert-8192-v2
3
+ #
4
+ # from transformers import AutoModel, AutoTokenizer
5
+ # model = AutoModel.from_pretrained("AbstractPhil/captionbert-8192-v2",
6
+ # trust_remote_code=True)
7
+ # tok = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
8
+ # out = model(**tok(["a cat on a windowsill"], return_tensors="pt"))
9
+ # emb = out.pooler_output # (B, 768) L2-normalized
10
+ #
11
+ # OR just: emb = model.encode(["a cat on a windowsill"])
12
+ #
13
+ # ---------------------------------------------------------------------------
14
+ # BREAKING CHANGE FROM v1 -- READ THIS IF YOU USED geolip-captionbert-8192
15
+ # v1 returned the POOLED 768-d embedding as `last_hidden_state`. That is not
16
+ # the transformers convention and it silently breaks anything expecting token
17
+ # states. v2 follows the convention:
18
+ # last_hidden_state : (B, L, 512) token states
19
+ # pooler_output : (B, 768) L2-normalized embedding <-- the product
20
+ # embedding : (B, 768) alias for pooler_output
21
+ # If you are porting v1 code, `last_hidden_state` -> `pooler_output`.
22
+ #
23
+ # v1 also shipped an AlignmentBank. v2 does NOT. Measured on v1: the bank's
24
+ # expert-consistency block varied 0.2% across samples and took 0.23% of its
25
+ # projection energy while anchor distances took 98.70% -- because
26
+ # `back = x @ R.T @ R` is a rotation round-trip and carries no data. Content
27
+ # extensions belong in an AMOE anchor, which this repo ships separately.
28
+ # ============================================================================
29
+
30
+ from dataclasses import dataclass
31
+ from typing import List, Optional, Tuple, Union
32
+
33
+ import torch
34
+ import torch.nn as nn
35
+ import torch.nn.functional as F
36
+ from transformers import PretrainedConfig, PreTrainedModel
37
+ from transformers.modeling_outputs import BaseModelOutputWithPooling
38
+
39
+
40
+ class CaptionBertV2Config(PretrainedConfig):
41
+ model_type = "captionbert_v2"
42
+
43
+ def __init__(
44
+ self,
45
+ vocab_size: int = 30522,
46
+ hidden_size: int = 512, # d_model
47
+ num_hidden_layers: int = 12,
48
+ num_attention_heads: int = 8,
49
+ intermediate_size: int = 2048,
50
+ output_dim: int = 768, # consensus space
51
+ max_position_embeddings: int = 8192,
52
+ hidden_dropout_prob: float = 0.1,
53
+ pad_token_id: int = 0,
54
+ pooling: str = "mean", # "mean" | "cls"
55
+ **kwargs,
56
+ ):
57
+ super().__init__(pad_token_id=pad_token_id, **kwargs)
58
+ self.vocab_size = vocab_size
59
+ self.hidden_size = hidden_size
60
+ self.num_hidden_layers = num_hidden_layers
61
+ self.num_attention_heads = num_attention_heads
62
+ self.intermediate_size = intermediate_size
63
+ self.output_dim = output_dim
64
+ self.max_position_embeddings = max_position_embeddings
65
+ self.hidden_dropout_prob = hidden_dropout_prob
66
+ self.pooling = pooling
67
+
68
+
69
+ class CaptionBertV2Model(PreTrainedModel):
70
+ """
71
+ Standalone caption/sentence encoder distilled from the geometric consensus
72
+ of five BERT-family teachers. No expert models at inference.
73
+
74
+ Parameter names are deliberately NOT namespaced under a submodule so that
75
+ the training checkpoint loads unchanged: token_emb, pos_emb, emb_norm,
76
+ encoder.layers.*, output_proj.*.
77
+ """
78
+
79
+ config_class = CaptionBertV2Config
80
+ base_model_prefix = "captionbert_v2"
81
+ supports_gradient_checkpointing = True
82
+
83
+ def __init__(self, config: CaptionBertV2Config):
84
+ super().__init__(config)
85
+ d = config.hidden_size
86
+ self.token_emb = nn.Embedding(config.vocab_size, d,
87
+ padding_idx=config.pad_token_id)
88
+ self.pos_emb = nn.Embedding(config.max_position_embeddings, d)
89
+ self.emb_norm = nn.LayerNorm(d)
90
+ self.emb_drop = nn.Dropout(config.hidden_dropout_prob)
91
+ layer = nn.TransformerEncoderLayer(
92
+ d_model=d,
93
+ nhead=config.num_attention_heads,
94
+ dim_feedforward=config.intermediate_size,
95
+ dropout=config.hidden_dropout_prob,
96
+ activation="gelu",
97
+ batch_first=True,
98
+ norm_first=True,
99
+ )
100
+ self.encoder = nn.TransformerEncoder(
101
+ layer, num_layers=config.num_hidden_layers, enable_nested_tensor=False)
102
+ self.output_proj = nn.Sequential(
103
+ nn.Linear(d, d), nn.GELU(), nn.LayerNorm(d), nn.Linear(d, config.output_dim))
104
+ self.post_init()
105
+
106
+ # -- HF plumbing --
107
+ def get_input_embeddings(self):
108
+ return self.token_emb
109
+
110
+ def set_input_embeddings(self, value):
111
+ self.token_emb = value
112
+
113
+ def forward(
114
+ self,
115
+ input_ids: torch.LongTensor = None,
116
+ attention_mask: Optional[torch.Tensor] = None,
117
+ output_hidden_states: Optional[bool] = None,
118
+ return_dict: Optional[bool] = None,
119
+ **kwargs,
120
+ ) -> Union[Tuple, BaseModelOutputWithPooling]:
121
+ return_dict = return_dict if return_dict is not None else True
122
+ L = input_ids.shape[1]
123
+ pos = torch.arange(L, device=input_ids.device).unsqueeze(0)
124
+ x = self.emb_drop(self.emb_norm(self.token_emb(input_ids) + self.pos_emb(pos)))
125
+
126
+ kpm = (~attention_mask.bool()) if attention_mask is not None \
127
+ else (input_ids == self.config.pad_token_id)
128
+
129
+ hidden = [x] if output_hidden_states else None
130
+ # Iterate the layers directly rather than calling self.encoder(...):
131
+ # nn.TransformerEncoder's fast path inspects layer types, and an AMOE
132
+ # anchor wraps each layer in a BlockWithAdapter that is not a
133
+ # TransformerEncoderLayer. This keeps attach() a drop-in.
134
+ for mod in self.encoder.layers:
135
+ x = mod(x, src_key_padding_mask=kpm)
136
+ if output_hidden_states:
137
+ hidden.append(x)
138
+ if self.encoder.norm is not None:
139
+ x = self.encoder.norm(x)
140
+
141
+ if self.config.pooling == "cls":
142
+ pooled = x[:, 0]
143
+ else:
144
+ m = (attention_mask.unsqueeze(-1).to(x.dtype) if attention_mask is not None
145
+ else (~kpm).unsqueeze(-1).to(x.dtype))
146
+ pooled = (x * m).sum(1) / m.sum(1).clamp(min=1)
147
+ embedding = F.normalize(self.output_proj(pooled), dim=-1)
148
+
149
+ if not return_dict:
150
+ return (x, embedding) + ((tuple(hidden),) if output_hidden_states else ())
151
+ out = BaseModelOutputWithPooling(
152
+ last_hidden_state=x, # (B, L, 512) token states
153
+ pooler_output=embedding, # (B, 768) THE PRODUCT
154
+ hidden_states=tuple(hidden) if output_hidden_states else None,
155
+ )
156
+ out.embedding = embedding # explicit alias
157
+ return out
158
+
159
+ @torch.no_grad()
160
+ def encode(self, texts, tokenizer=None, batch_size: int = 128,
161
+ max_length: int = 256, device=None) -> torch.Tensor:
162
+ """Raw text -> (N, 768) L2-normalized embeddings."""
163
+ if isinstance(texts, str):
164
+ texts = [texts]
165
+ if tokenizer is None:
166
+ from transformers import AutoTokenizer
167
+ tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
168
+ device = device or next(self.parameters()).device
169
+ was_training = self.training
170
+ self.eval()
171
+ out = []
172
+ for i in range(0, len(texts), batch_size):
173
+ t = tokenizer(list(texts[i:i + batch_size]), max_length=max_length,
174
+ padding=True, truncation=True, return_tensors="pt").to(device)
175
+ out.append(self(**t).pooler_output.float().cpu())
176
+ if was_training:
177
+ self.train()
178
+ return torch.cat(out)
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # AMOE binding -- lets amoe-lora attach anchors to this trunk unmodified.
183
+ #
184
+ # import amoe
185
+ # from modeling_captionbert import CaptionBertV2Binding
186
+ # h = amoe.attach(model, "amoe/moe/equiv.anchor.pt",
187
+ # binding=CaptionBertV2Binding(d=model.config.hidden_size))
188
+ #
189
+ # amoe's PathBinding would find encoder.layers by dotted path but then read
190
+ # model.config.hidden_size -- which works here because this IS a
191
+ # PretrainedConfig. The explicit binding is kept for plain-nn.Module use.
192
+ # ---------------------------------------------------------------------------
193
+
194
+ @dataclass
195
+ class CaptionBertV2Binding:
196
+ d: int = 512
197
+ name: str = "captionbert_v2"
198
+
199
+ def layers(self, model):
200
+ return model.encoder.layers
201
+
202
+ def set_layers(self, model, new):
203
+ model.encoder.layers = nn.ModuleList(new)
204
+
205
+ def hidden_size(self, model) -> int:
206
+ return int(self.d)
207
+
208
+
209
+ CaptionBertV2Config.register_for_auto_class()
210
  CaptionBertV2Model.register_for_auto_class("AutoModel")