File size: 12,577 Bytes
a06aad3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48ef031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a06aad3
0c8f923
a06aad3
0c8f923
 
 
a06aad3
0c8f923
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8f4d990
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48ef031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c8f923
 
 
 
 
 
 
a06aad3
0c8f923
 
 
 
 
 
a06aad3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48ef031
 
 
 
0c8f923
 
 
 
 
 
a06aad3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c8f923
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
---
license: mit
language: [en]
library_name: transformers
pipeline_tag: feature-extraction
tags: [sentence-similarity, feature-extraction, consensus-distillation, geometric-deep-learning, amoe]
datasets: [AbstractPhil/conceptual-captions-12m-webdataset-berts]
base_model: [google-bert/bert-base-uncased, answerdotai/ModernBERT-base, FacebookAI/roberta-base, albert/albert-base-v2, distilbert/distilbert-base-uncased]
---

# captionbert-8192-v2

A **58.3M** standalone sentence encoder distilled from the geometric **consensus**
of five BERT-family teachers. No expert models at inference: tokenizer + this
model, 768-d L2-normalized output.

12 layers, 512-d, 8 heads, FFN 2048, 8192 position capacity. **0.53x bert-base.**

```python
from transformers import AutoModel, AutoTokenizer
model = AutoModel.from_pretrained("AbstractPhil/captionbert-8192-v2", trust_remote_code=True)
tok   = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")

emb = model.encode(["a cat on a windowsill", "a feline by the window"])   # (2, 768)
(emb[0] @ emb[1]).item()
```

## Benchmark

| model | params | STS-B | SICK-R | STS12 | STS13 | STS14 | STS15 | STS16 | BIOSSES | mean |
|---|---|---|---|---|---|---|---|---|---|---|
| bert-base | 109.5M | 0.4729 | 0.5865 | 0.3087 | 0.5988 | 0.4773 | 0.6029 | 0.6373 | 0.5469 | 0.5289 |
| ModernBERT-base | 149.0M | 0.4215 | 0.5479 | 0.3527 | 0.4247 | 0.3795 | 0.5349 | 0.4174 | 0.5630 | 0.4552 |
| roberta-base | 124.6M | 0.5436 | 0.6296 | 0.3211 | 0.5631 | 0.4522 | 0.6134 | 0.6198 | 0.5777 | 0.5401 |
| albert-base-v2 | 11.7M | 0.4784 | 0.5364 | 0.3101 | 0.4831 | 0.3809 | 0.5542 | 0.5491 | 0.4863 | 0.4723 |
| distilbert | 66.4M | 0.5717 | 0.6424 | 0.4344 | 0.6490 | 0.5410 | 0.6663 | 0.6854 | 0.5162 | 0.5883 |
| **captionbert-8192-v2** | 58.3M | **0.5747** | **0.6526** | **0.5051** | **0.5995** | **0.5452** | **0.7136** | **0.6776** | **0.5933** | **0.6077** |
| **captionbert-8192-v2 + arms** | 63.2M | **0.7684** | **0.7391** | **0.6682** | **0.7557** | **0.6921** | **0.8055** | **0.7626** | **0.6382** | **0.7287** |
| captionbert-8192-b | 58.3M | 0.5752 | 0.6548 | 0.5012 | 0.6037 | 0.5470 | 0.7146 | 0.6782 | 0.5500 | 0.6031 |
| all-MiniLM-L6-v2 | 22.7M | 0.8203 | 0.7758 | 0.7237 | 0.8058 | 0.7559 | 0.8539 | 0.7899 | 0.8144 | 0.7925 |

All ten models measured in **one harness**, same eight tasks, **mean-pooled and
L2-normalized**, no task tuning. Spearman correlation; `mean` is the unweighted
average over the eight.

`all-MiniLM-L6-v2` was contrastively trained on 1B+ curated sentence pairs. It is
listed for scale, not as a peer -- nothing here saw a similarity label.

**The trunk beats every teacher it was distilled from**, and the best of them
(distilbert, .5883) by +.0194 -- at **13% of their combined 461M parameters**,
having never seen a similarity label. The margin comes mostly from STS12, where
every teacher collapses to .31-.43 and the trunk holds .50.

**With arms it clears the best teacher by +.14** and closes to within **.063** of
a model trained on a billion curated pairs.

Mean-pooled BERT-family encoders are known-weak sentence encoders -- that is the
reason Sentence-BERT exists -- so beating them is an efficiency result rather
than a state-of-the-art one. The MiniLM row is in the table to keep that honest.

### Geometry

| model | self_cos | erank |
|---|---|---|
| bert-base | +0.6071 | 32.8 |
| ModernBERT-base | +0.9001 | 26.1 |
| roberta-base | +0.9594 | 19.8 |
| albert-base-v2 | +0.7473 | 20.9 |
| distilbert | +0.6920 | 31.1 |
| **captionbert-8192-v2** | +0.1396 | 36.6 |
| **captionbert-8192-v2 + arms** | +0.0964 | 57.6 |
| captionbert-8192-b | +0.1411 | 36.1 |
| all-MiniLM-L6-v2 | +0.0251 | 86.7 |

`self_cos` is the isotropy gauge: the mean cosine between unrelated sentences.
Mean-pooled BERT-family embeddings sit in a narrow cone (+.61 to +.96), where
cosine cannot discriminate. `erank` is the participation ratio -- how many of the
768 directions carry variance.

Both track capability almost perfectly across all ten models, and **isotropy is
the mechanism**: no isotropy objective appears anywhere in the training stack.
The arms then lift erank 36.6 -> 57.6, the first evidence in this line that
adaptation *adds* usable directions rather than only rotating them.

## With AMOE arms (`amoe/`)

The trunk is frozen; each anchor is 1.6M params across 12 sites. Anchors toggle
**bit-exact** -- all disabled reproduces the bare trunk exactly -- so one
artifact serves both the unsupervised baseline and the adapted model.

Best measured roster: **`equiv` + `simplify` + `paraphrase`** under a trained
dispatch, across the full 8-task STS suite:

| config | STS-B | SICK-R | STS12 | STS13 | STS14 | STS15 | STS16 | BIOSSES | mean |
|---|---|---|---|---|---|---|---|---|---|
| bare trunk | .5747 | .6526 | .5051 | .5995 | .5452 | .7136 | .6776 | .5933 | .6077 |
| best single member | .7208 | .7269 | .6275 | .6988 | .6423 | .7783 | .7107 | .5845 | .6862 |
| **3-arm collective** | **.7684** | **.7391** | **.6682** | **.7557** | **.6921** | **.8055** | **.7626** | **.6382** | **.7287** |

**Eight for eight over every single member**, +.1210 mean over the trunk, from
1.6M x 3 adapter params on a model that never moves. Note the BIOSSES column:
the strongest member alone reads .5845, *below* the bare trunk -- the mixture
turns a liability into a gain.

Solo anchor scores (trained alone, not masked inside a dispatch):

| anchor | trained on | relation | STS-B | SICK-R |
|---|---|---|---|---|
| `equiv` | all-nli | equivalence | .7254 | **.7550** |
| `simplify` | simple-wiki + altlex + sentence-compression | compression | **.7400** | .7075 |
| `paraphrase` | quora-duplicates | question paraphrase | see below | see below |

### How many arms, and which

Arms do **not** add capacity -- they split a roughly conserved amplitude budget.
`w_k/z = sinh(u_k) / SUM_j cosh(u_j)` sums over *all* anchors, so measured sums
run .850 at A=2 to .757 at A=6 while each arm's share collapses. And sharper
routing does not rescue it: tau .10/.05/.02 gave STS-B .7520 / .7514 / .7468
while key separation *rose*. **The value is graded blending, not selection.**

So every candidate arm is judged against an **untrained arm at the same arm
count**, not against the smaller base. An empty arm gains +.0045 here -- real
generic capacity. Greedy forward selection, bar = the measured seed spread .0054:

| candidate (A=3) | STS-B | vs empty arm | verdict |
|---|---|---|---|
| `paraphrase` (quora) | .7684 | **+.0146** | **kept** |
| *untrained control* | .7538 | -- | baseline |
| `lexical` (WordNet term/gloss) | .7518 | -.0020 | rejected |
| `topical` (specter citations) | .7508 | -.0030 | rejected |

Every A=4 candidate -- including the control -- came in below .7684. Selection
stopped at three arms. Zero starvation alarms; per-block routing spread .348, so
the 12 dispatches learned different decisions rather than 12 copies of one.

### Loading arms

Arms attach through the same `AutoModel` object. `amoe-lora` is imported lazily,
so the base model still loads on a machine that has never heard of it.

```python
from transformers import AutoModel
model = AutoModel.from_pretrained("AbstractPhil/captionbert-8192-v2",
                                  trust_remote_code=True)

model.attach_amoe()                       # the shipped 3-arm collective
emb = model.encode(["a cat on a windowsill"])

with model.amoe_off():                    # the unsupervised baseline
    base = model.encode(["a cat on a windowsill"])

model.set_amoe(["equiv"])                 # one arm, inside the dispatch
model.detach_amoe()                       # bit-exact restore, asserted
```

`attach_amoe()` with no arguments reads the roster **and its order** from the
dispatch checkpoint -- routing keys are per-arm and positional, so order is not
cosmetic. Both `attach_amoe` and `detach_amoe` assert the toggle law and raise
rather than return a silently-wrong model.

`pip install git+https://github.com/AbstractEyes/amoe-lora` for the arm methods.

### Arms do not transfer between trunks

These anchors were trained against **this** trunk's residual stream. Measured
against the sibling [`captionbert-8192-b`](https://huggingface.co/AbstractPhil/captionbert-8192-v2-B)
(same architecture, complete 66-chunk corpus):

| configuration | 8-task mean |
|---|---|
| v2 arms on v2 | .7287 |
| v2 arms on **-b** | .6863 |
| + re-aligned routing keys | .6987 |
| -b's own anchors on -b | .7295 |

Transferring costs **31% of the gain**; re-training only the 1,536 routing keys
recovers 29% of that, so **71% of the loss is in the anchors themselves**.

The two trunks are indistinguishable on all eight tasks (+.0009 mean excluding
the 100-row BIOSSES) and on geometry, yet 1.6M adapter parameters tell them
apart -- adapters read the residual stream, the task gauges read the pooled
output, and the stream carries trunk identity the output does not. Budget one
anchor set per trunk (~18 min).

### Arm library

`amoe/arms/` holds one canonical copy of every anchor with `ARMS.json`
(provenance, training spec, solo scores, capacity verdict). **Resolve from
there; do not retrain what is already listed.** The campaign folders
(`amoe/{sts,sts-combo,moe,moe-v2,collective}`) remain as the records behind the
published numbers.

Scope worth stating: these anchors are solo-trained and always-on, which the
[aleph line](https://huggingface.co/blog/AbstractPhil/aleph-differentiation-ft3)
identifies as *blend* regime -- the dispatch blends them rather than containing
them. That is survivable here only because every arm serves one task. On a
multi-task trunk, expect the cross-task interference that record documents.
See [amoe-lora](https://github.com/AbstractEyes/amoe-lora).

## How it was built

1. Five teachers embedded 33M CC12M llava-next captions (mean-pooled, 768-d).
2. One global **whitened Procrustes** map per teacher into `bert-base`'s frame,
   fit on a stratified random sample and **reported out-of-sample**.
3. Consensus = normalized centroid of the aligned teachers, per chunk.
4. Student trained from scratch: InfoNCE(T=0.07) + per-sample MSE against the
   consensus. Pure Adam, no weight decay. 26.9M rows, 52,548 steps at batch
   2048, ~5.4 h on one RTX 6000 Pro.

## Known limits -- read before using

- **Consensus rank is ~28.7 of 768.** Five BERT-family teachers only agree on
  about 29 directions. The student uses ~103 in domain but falls back to ~33 on
  out-of-domain text: **the structure it builds on captions does not transfer.**
  This is the model's ceiling and it is a property of the consensus, not the
  student.
- **Alignment quality varies by teacher.** Out-of-sample cosine to the bert
  frame: distil .625, roberta .372, albert .331, modern .327. The ordering
  tracks architectural distance from bert-base.
- **10 of 66 source chunks lacked ModernBERT**, so 54 chunks (~27M rows) were
  used. No 4-expert fallback: that would change the target definition mid-dataset.
- Trained on image captions. Expect caption-like text to be its strongest domain.
- **A sibling trained on the complete 66-chunk corpus** ([`-b`](https://huggingface.co/AbstractPhil/captionbert-8192-v2-B))
  scores within .0046 of this one bare and .0008 with arms. 19% more data bought
  nothing: the ceiling is teacher agreement (28.7 of 768 shared directions), not
  corpus size.
- **The trunk is single-seed.** The AMOE results are 2-seed with a measured
  spread of .003-.005, against margins of +.012 to +.019.
- `amoe/sts-combo` stopped at step 500 of a planned 4000; its card and config
  describe the full run. `amoe/moe-v2` retrained its anchors instead of reusing
  them, so the moe-vs-moe-v2 comparison moved two variables, not one. Both are
  recorded rather than quietly corrected.

## Output convention (differs from v1)

| field | shape | |
|---|---|---|
| `last_hidden_state` | (B, L, 512) | token states |
| `pooler_output` | (B, 768) | **the embedding**, L2-normalized |
| `embedding` | (B, 768) | alias |

`geolip-captionbert-8192` (v1) returned the pooled embedding as
`last_hidden_state`. If porting v1 code, use `pooler_output`. v1 also shipped an
`AlignmentBank`; v2 does not -- measured on v1, its expert-consistency features
varied 0.2% across samples because a rotation round-trip carries no data.

## Citation

```bibtex
@misc{abstractphil2026captionbertv2,
  title  = {captionbert-8192-v2: consensus distillation at CC12M scale},
  author = {AbstractPhil},
  year   = {2026},
  url    = {https://huggingface.co/AbstractPhil/captionbert-8192-v2}
}
```

MIT.