static_embeddings 0.1.5 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +17 -2
- data/docs/ARCHITECTURE.md +14 -5
- data/docs/MODEL_AUDIT.md +110 -0
- data/lib/models/demo.semb +0 -0
- data/lib/static_embeddings/bert_wordpiece.rb +191 -0
- data/lib/static_embeddings/canonical.rb +50 -0
- data/lib/static_embeddings/cli.rb +88 -63
- data/lib/static_embeddings/codec.rb +45 -0
- data/lib/static_embeddings/conversion.rb +58 -0
- data/lib/static_embeddings/errors.rb +2 -2
- data/lib/static_embeddings/format/constants.rb +109 -0
- data/lib/static_embeddings/format/hash_table.rb +69 -0
- data/lib/static_embeddings/format/trie.rb +78 -0
- data/lib/static_embeddings/format/verifier.rb +41 -0
- data/lib/static_embeddings/format/writer.rb +131 -0
- data/lib/static_embeddings/format.rb +3 -338
- data/lib/static_embeddings/importers/model2vec.rb +52 -0
- data/lib/static_embeddings/importers/sentence_transformers_static.rb +103 -0
- data/lib/static_embeddings/importers/support.rb +111 -0
- data/lib/static_embeddings/importers.rb +50 -0
- data/lib/static_embeddings/model.rb +35 -20
- data/lib/static_embeddings/paths.rb +8 -12
- data/lib/static_embeddings/provenance.rb +58 -0
- data/lib/static_embeddings/reference.rb +50 -40
- data/lib/static_embeddings/row_prefix_payload.rb +59 -0
- data/lib/static_embeddings/version.rb +1 -1
- data/lib/static_embeddings.rb +29 -55
- data/static_embeddings.gemspec +2 -2
- data/tools/check_model2vec_parity.rb +5 -1
- data/tools/check_st_parity.rb +125 -0
- data/tools/eval_retrieval.rb +58 -0
- metadata +24 -6
- data/lib/static_embeddings/converter.rb +0 -328
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 550531f7573a8d05e8d5a3a1ae0fa301c01d77b238573ae11834797e8ac44a0f
|
|
4
|
+
data.tar.gz: 57f487c729296e3a858604dc64aa146aa6dd6e372a45961d90a61564e3c1b117
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52162c96d458dffaaec8498cd712678b4727b29ddaa66eadf7834893113a6bd05f636cc47a2f84d969f76eb4651fe57664212fdb4ac2925449827b9d3a69a9bf
|
|
7
|
+
data.tar.gz: 115a0b0f3ff9d82cea099fc0ee24385034d8bcc66ca4ff9337f2213608ee4d611215b6d3400fe39aecf562cf201f5f5044eab7f9863fbdc813362c0af0eb7739
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.6
|
|
4
|
+
|
|
5
|
+
Multi-source WordPiece release. The native `.semb` v3 runtime is unchanged; new
|
|
6
|
+
source families are normalized offline into the same audited
|
|
7
|
+
`BERT_WORDPIECE_V1 -> lookup -> mean -> NONE/L2` contract. Model identity is
|
|
8
|
+
chosen at `load_model`, not per `embed` call.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Model2Vec and Sentence Transformers importers.** A Model2Vec directory and a
|
|
13
|
+
Sentence Transformers `StaticEmbedding` directory (`modules.json` + exactly
|
|
14
|
+
one module path) compile to the same canonical data. The ST importer is
|
|
15
|
+
fail-closed and confines the module path to the source root with `realpath`.
|
|
16
|
+
- **Source-faithful policies.** Model2Vec uses `UNK_DROP`, normalization from its
|
|
17
|
+
config and a 512-token default. Sentence Transformers `StaticEmbedding` uses
|
|
18
|
+
`UNK_INCLUDE`, `NORMALIZATION_NONE`, unlimited tokens and
|
|
19
|
+
`add_special_tokens=false`.
|
|
20
|
+
- **`--dimensions N`.** Matryoshka prefix slicing happens while streaming the
|
|
21
|
+
embedding matrix; the runtime still loads an ordinary fixed-dimension `.semb`.
|
|
22
|
+
- **`--max-tokens unlimited`.** Convert-time `0` remains an alias; runtime
|
|
23
|
+
`max_tokens: 0` is still rejected and `false` means unlimited.
|
|
24
|
+
- **Russian retrieval fixture.** `tools/eval_retrieval.rb` and the checked-in
|
|
25
|
+
Russian FAQ corpus provide a small domain sanity check for the multilingual
|
|
26
|
+
WordPiece model; it is not presented as a general benchmark.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Offline Ruby conversion was simplified.** Stateful `Converter` and the
|
|
31
|
+
behavior-heavy `CanonicalModel` were replaced by import functions, immutable
|
|
32
|
+
canonical data, pure provenance/meta transforms and a streaming format writer.
|
|
33
|
+
Writer/hash/trie/verifier code is split from runtime format constants, so plain
|
|
34
|
+
`require "static_embeddings"` does not load conversion code.
|
|
35
|
+
- The Ruby `Reference` twin follows canonical UNK/normalization/max-token
|
|
36
|
+
policies instead of re-deriving Model2Vec defaults.
|
|
37
|
+
- Model2Vec `normalize` now defaults to `false` when the key is absent, matching
|
|
38
|
+
the pinned upstream behavior. Unknown source families are rejected rather than
|
|
39
|
+
guessed from `tokenizer.json + model.safetensors`.
|
|
40
|
+
- Header integer writes validate their unsigned range instead of silently
|
|
41
|
+
wrapping oversized Ruby integers.
|
|
42
|
+
- `Model#provenance` is parsed once and cached.
|
|
43
|
+
|
|
44
|
+
Local WordPiece conversions requiring **no C change**: `potion-base-8M`,
|
|
45
|
+
`potion-science-32M`, `static-retrieval-mrl-en-v1` at 1024/512 and
|
|
46
|
+
`static-similarity-mrl-multilingual-v1` at 512/256. The retrieval model has a
|
|
47
|
+
recorded `SentenceTransformer.encode` oracle (436/436). Records are in
|
|
48
|
+
`docs/MODEL_AUDIT.md`.
|
|
49
|
+
|
|
50
|
+
SentencePiece/Unigram is intentionally **not** part of 1.5.6; it remains a
|
|
51
|
+
future runtime capability rather than shipping an unaudited v4 path.
|
|
52
|
+
|
|
3
53
|
## 0.1.5
|
|
4
54
|
|
|
5
55
|
Correctness/parity release. The native runtime architecture is unchanged, but
|
data/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# static_embeddings
|
|
2
2
|
|
|
3
|
-
A Ruby runtime for converted Model2Vec
|
|
3
|
+
A Ruby runtime for converted Model2Vec and Sentence Transformers static embedding models. A model
|
|
4
4
|
is converted once into a local `.semb` file and loaded through a small C
|
|
5
5
|
extension. No ONNX Runtime, no Rust or Python at runtime, no network access, one
|
|
6
6
|
mmap-able file, binary float32 output.
|
|
7
7
|
|
|
8
|
-
The
|
|
8
|
+
The reference production target remains `minishlab/potion-retrieval-32M`; 1.5.6 also supports audited WordPiece `StaticEmbedding` sources.
|
|
9
9
|
|
|
10
10
|
```ruby
|
|
11
11
|
require "static_embeddings"
|
|
@@ -49,6 +49,21 @@ bundle exec ruby -Ilib exe/static_embeddings convert ./potion-retrieval-32M \
|
|
|
49
49
|
--id potion-retrieval-32m
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
Sentence Transformers static models (`modules.json` + one `StaticEmbedding`
|
|
53
|
+
module) use the same command. Matryoshka prefix-slice is a convert-time flag,
|
|
54
|
+
not a runtime `dim:`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bundle exec ruby -Ilib exe/static_embeddings convert ./static-retrieval-mrl-en-v1 \
|
|
58
|
+
--id static-retrieval-mrl-en-v1-512 \
|
|
59
|
+
--dimensions 512 \
|
|
60
|
+
--trained-mrl-dims 1024,512,256,128,64,32
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`--max-tokens unlimited` bakes no token cap (Sentence Transformers default).
|
|
64
|
+
`--max-tokens 0` is a convert-time alias; `embed(max_tokens: 0)` is still
|
|
65
|
+
rejected. Query and documents must use the same loaded `.semb`.
|
|
66
|
+
|
|
52
67
|
The result lands in `~/.cache/static_embeddings/models/potion-retrieval-32m.semb`.
|
|
53
68
|
Inspect or verify it with the `inspect` and `verify` subcommands, then load it:
|
|
54
69
|
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -8,10 +8,10 @@ HuggingFace / Model2Vec files offline, once, on your machine
|
|
|
8
8
|
model.safetensors
|
|
9
9
|
|
|
|
10
10
|
v
|
|
11
|
-
|
|
12
|
-
|
|
|
11
|
+
import -> canonical data -> writer <--- pure Ruby, offline, strict
|
|
12
|
+
| all source parsing/validation here
|
|
13
13
|
v
|
|
14
|
-
model.semb
|
|
14
|
+
model.semb <--- flat, versioned, mmap-able
|
|
15
15
|
|
|
|
16
16
|
v
|
|
17
17
|
C runtime <--- mmap + bounds checks + tokenize +
|
|
@@ -24,6 +24,15 @@ Everything expensive, fragile or security-sensitive about reading third-party
|
|
|
24
24
|
model files happens once, offline, in a language where it is easy to get
|
|
25
25
|
right. What remains in C is a bounds-checked mmap and three loops.
|
|
26
26
|
|
|
27
|
+
Only the offline import layer knows HuggingFace layouts. Model2Vec and Sentence
|
|
28
|
+
Transformers `StaticEmbedding` sources are reduced to immutable canonical data:
|
|
29
|
+
tokens, streamed matrix, tokenizer metadata, runtime policies and provenance.
|
|
30
|
+
Pure transforms then build the v3 header/provenance and the existing WordPiece
|
|
31
|
+
writer emits the artifact. A new WordPiece + mean-pooling source therefore does
|
|
32
|
+
not require a C change. New tokenizer families remain explicit future
|
|
33
|
+
capabilities and require a separately audited format/runtime change rather than
|
|
34
|
+
source-name special cases.
|
|
35
|
+
|
|
27
36
|
The C side is `se_format.c` (mmap and validation), `se_tokenizer.c`,
|
|
28
37
|
`se_embed.c`, `se_unicode.c`, `se_f16.c` (half-precision codec and kernels),
|
|
29
38
|
`se_topk.c`, `se_alloc_stats.c` (optional allocation counters), and
|
|
@@ -336,8 +345,8 @@ chunks -> .semb vectors --/
|
|
|
336
345
|
1. **Which potion models actually match `BERT_WORDPIECE_V1`.** The runtime now
|
|
337
346
|
implements the exact `normalized: false` extraction semantics needed by the
|
|
338
347
|
five standard BERT added tokens, but still rejects arbitrary AddedVocabulary,
|
|
339
|
-
BPE and Unigram profiles.
|
|
340
|
-
treated as compatible.
|
|
348
|
+
BPE and Unigram profiles. Type A (WordPiece + mean) models convert without a
|
|
349
|
+
C change; each source still needs an audit before it is treated as compatible.
|
|
341
350
|
2. **Russian.** Distilling a multilingual teacher into a WordPiece vocabulary
|
|
342
351
|
we control keeps the pure-C path, but skips the Tokenlearn pre-training
|
|
343
352
|
that gives the published potion models their quality. That gap has to be
|
data/docs/MODEL_AUDIT.md
CHANGED
|
@@ -119,3 +119,113 @@ synthetic `tiny-wordpiece` fixture: `434/434`, all four failure lists empty,
|
|
|
119
119
|
`min_cosine=0.9999999999999988`, `max_abs_all=5.960464477539063e-08`. That job
|
|
120
120
|
proves the fixture loads in `StaticModel.from_pretrained` and that the checker
|
|
121
121
|
contracts hold; it is not a potion audit.
|
|
122
|
+
|
|
123
|
+
## 1.5.6 Patch 1 proof
|
|
124
|
+
|
|
125
|
+
Architecture: Model2Vec / Sentence Transformers import functions → immutable canonical data →
|
|
126
|
+
existing WordPiece `.semb` v3 writer. C runtime unchanged.
|
|
127
|
+
|
|
128
|
+
### potion-retrieval-32M reconvert
|
|
129
|
+
|
|
130
|
+
Official snapshot `6fc8051fab2a1e0ee76689cf08c853792ac285e7` ships a Sentence
|
|
131
|
+
Transformers `modules.json` (`StaticEmbedding` + `Normalize`). Detection uses
|
|
132
|
+
`config.json` `model_type=model2vec` first, so this stays UNK_DROP / L2 / 512.
|
|
133
|
+
|
|
134
|
+
New file `potion-retrieval-32m-reconvert.semb` vs existing
|
|
135
|
+
`potion-retrieval-32m-v3.semb`: vocab/hash/embeddings/norm_tables/tries
|
|
136
|
+
byte-identical. 10-text corpus `max_abs=0`. Provenance JSON differs (new keys).
|
|
137
|
+
|
|
138
|
+
### static-retrieval-mrl-en-v1 vs SentenceTransformer.encode
|
|
139
|
+
|
|
140
|
+
Oracle: `tools/st_oracle.py` + `tools/check_st_parity.rb`,
|
|
141
|
+
`sentence-transformers 6.0.1`, `add_special_tokens=false`,
|
|
142
|
+
`normalize_embeddings=false`, 436 rows including `all-unk:private-use` and
|
|
143
|
+
`long:known-words-600`.
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
1024: 436/436 min_cosine=0.999999999999999 max_abs_all=7.62939453125e-06
|
|
147
|
+
512: 436/436 min_cosine=0.9999999999999989 max_abs_all=7.62939453125e-06
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
512 compared as prefix of the 1024-d encode vectors. Ruby Reference matched C
|
|
151
|
+
separately. Diagnostic `StaticEmbedding.forward` matched encode on this model.
|
|
152
|
+
|
|
153
|
+
### UNK and max_tokens on the real models
|
|
154
|
+
|
|
155
|
+
- potion-retrieval-32M `🧬 🧬 🧬` → UNK ids dropped → zero vector; 600×`hello`
|
|
156
|
+
→ `truncated=true pooled=512`.
|
|
157
|
+
- static-retrieval-mrl-en-v1-1024 same emoji string → three UNK ids pooled
|
|
158
|
+
(non-zero, mean of UNK rows); 600×`hello` → `truncated=false pooled=600`.
|
|
159
|
+
- Private-use codepoints are BertNormalizer `Co` and become empty, not UNK.
|
|
160
|
+
|
|
161
|
+
## 1.5.6 Type A conversions
|
|
162
|
+
|
|
163
|
+
These records prove the offline import layer maps extra WordPiece sources onto
|
|
164
|
+
the existing runtime. They are **not** a replacement for the
|
|
165
|
+
potion-retrieval-32m Python oracle above. `static-retrieval-mrl-en-v1` has the
|
|
166
|
+
separate `SentenceTransformer.encode` oracle recorded above; the multilingual
|
|
167
|
+
similarity artifact was checked against the Ruby `.semb` Reference twin and the
|
|
168
|
+
small Russian retrieval fixture, not claimed as a general upstream retrieval
|
|
169
|
+
benchmark.
|
|
170
|
+
|
|
171
|
+
All conversions used `static_embeddings 1.5.6` on arm64-darwin24.
|
|
172
|
+
|
|
173
|
+
### minishlab/potion-base-8M
|
|
174
|
+
|
|
175
|
+
- Snapshot: `bf8b056651a2c21b8d2565580b8569da283cab23`
|
|
176
|
+
- Layout: Model2Vec (`UNK_DROP`, L2, `max_tokens` 512)
|
|
177
|
+
- `.semb`: `potion-base-8m.semb` `dim=256` `vocab=29528` `33346200` bytes
|
|
178
|
+
SHA256 `8abd8d1f26511959e14ca26ec4dcacad1d3db9b4b66b2e67683fb2d06074f490`
|
|
179
|
+
- Native C matched Ruby Reference on short English/OOV/empty texts (`max_abs < 1e-5`)
|
|
180
|
+
- `model2vec 0.9.0` / `tokenizers 0.23.1` oracle: 434/434, vectors_checked=432,
|
|
181
|
+
intentional character-pretruncate deviations=2, min_cosine=0.9999999999999989,
|
|
182
|
+
max_abs_all=2.98e-08
|
|
183
|
+
|
|
184
|
+
### minishlab/potion-science-32M
|
|
185
|
+
|
|
186
|
+
- Snapshot: `7366079845507de14a4330007cdfa01bb92bca52`
|
|
187
|
+
- Layout: Model2Vec (`UNK_DROP`, L2, `max_tokens` 512)
|
|
188
|
+
- `.semb`: `potion-science-32m.semb` `dim=256` `vocab=124428` `140353240` bytes
|
|
189
|
+
SHA256 `e968f93b89d59d9c63c6a8152495283d1138144e61ed5c08d306c21c00b7cdb1`
|
|
190
|
+
- Native C matched Ruby Reference on English and Russian snippets (`max_abs < 1e-5`)
|
|
191
|
+
- `model2vec 0.9.0` / `tokenizers 0.23.1` oracle: 434/434, vectors_checked=432,
|
|
192
|
+
intentional character-pretruncate deviations=2, min_cosine=0.9999999999999986,
|
|
193
|
+
max_abs_all=2.98e-08
|
|
194
|
+
|
|
195
|
+
### sentence-transformers/static-retrieval-mrl-en-v1
|
|
196
|
+
|
|
197
|
+
- Snapshot: `f60985c706f192d45d218078e49e5a8b6f15283a`
|
|
198
|
+
- Layout: Sentence Transformers StaticEmbedding (`UNK_INCLUDE`, no L2, unlimited)
|
|
199
|
+
- `static-retrieval-mrl-en-v1-1024.semb` `dim=1024` `vocab=30522` `128186264` bytes
|
|
200
|
+
SHA256 `f316bb07348503418ed0f6d897ddbf7b44422d2077ca4860518f31f328610946`
|
|
201
|
+
- `static-retrieval-mrl-en-v1-512.semb` `--dimensions 512` `65677208` bytes
|
|
202
|
+
SHA256 `efbb43c14def793d0bbb42ef591a0951caed23a49cdd94dcf72312569d1b7cff`
|
|
203
|
+
- Prefix of a 1024-d vector matched the 512-d artifact on in-vocabulary English
|
|
204
|
+
(valid because this source does not L2-normalize)
|
|
205
|
+
- Native C matched Ruby Reference on short texts
|
|
206
|
+
|
|
207
|
+
### sentence-transformers/static-similarity-mrl-multilingual-v1
|
|
208
|
+
|
|
209
|
+
- Snapshot: `b68f4122911bcffcd6e1f695f2d99cd6788972d8`
|
|
210
|
+
- Layout: Sentence Transformers StaticEmbedding (`UNK_INCLUDE`, no L2, unlimited)
|
|
211
|
+
- `static-similarity-mrl-multilingual-v1-512.semb` `--dimensions 512` `vocab=105879`
|
|
212
|
+
`227983824` bytes SHA256 `8d9a63d20ee23468ae2fd2584ef3f0ec75e720e68410e416157aaf2b873e110e`
|
|
213
|
+
- `static-similarity-mrl-multilingual-v1-256.semb` `--dimensions 256` `119563728` bytes
|
|
214
|
+
SHA256 `2e9e689bd3afd18ed50a77f0f8842b4b5a5be39bfad75e3fa24ad99b478de068`
|
|
215
|
+
- Native C matched Ruby Reference on Russian text (`max_abs 9.1e-7`)
|
|
216
|
+
|
|
217
|
+
This model is published as similarity, not retrieval. `tools/eval_retrieval.rb`
|
|
218
|
+
on `test/fixtures/russian_faq_eval.json` (10 queries / 10 docs, cosine@10,
|
|
219
|
+
arm64-darwin24, 1.5.6):
|
|
220
|
+
|
|
221
|
+
| model | dim | MRR | nDCG@10 |
|
|
222
|
+
|---|---:|---:|---:|
|
|
223
|
+
| static-similarity-mrl-multilingual-v1-256 | 256 | 0.875 | 0.906 |
|
|
224
|
+
| static-similarity-mrl-multilingual-v1-512 | 512 | 0.850 | 0.889 |
|
|
225
|
+
| static-retrieval-mrl-en-v1-512 | 512 | 0.792 | 0.842 |
|
|
226
|
+
| potion-retrieval-32m | 512 | 0.733 | 0.799 |
|
|
227
|
+
| potion-base-8m | 256 | 0.712 | 0.780 |
|
|
228
|
+
|
|
229
|
+
Hit@10 was 1.0 for every model on this tiny labeled set. That is a domain sanity
|
|
230
|
+
check, not a public retrieval benchmark, and it does not make the similarity
|
|
231
|
+
model a hard-coded Russian default.
|
|
Binary file
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
require "static_embeddings/errors"
|
|
2
|
+
require "static_embeddings/format/constants"
|
|
3
|
+
|
|
4
|
+
module StaticEmbeddings
|
|
5
|
+
module BertWordPiece
|
|
6
|
+
TOKENIZER_PROFILE = "BERT_WORDPIECE_V1"
|
|
7
|
+
ALLOWED_NORMALIZER_KEYS = %w[type clean_text handle_chinese_chars strip_accents lowercase].freeze
|
|
8
|
+
STANDARD_SPECIAL_TOKENS = {
|
|
9
|
+
"[PAD]" => Format::ADDED_PAD,
|
|
10
|
+
"[UNK]" => Format::ADDED_UNK,
|
|
11
|
+
"[CLS]" => Format::ADDED_CLS,
|
|
12
|
+
"[SEP]" => Format::ADDED_SEP,
|
|
13
|
+
"[MASK]" => Format::ADDED_MASK
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def compile(tokenizer, tokenizer_config = {})
|
|
19
|
+
profile = audit(tokenizer, tokenizer_config)
|
|
20
|
+
tokens = vocabulary(tokenizer)
|
|
21
|
+
validate_added_token_ids(profile.fetch(:added_tokens), tokens)
|
|
22
|
+
[profile.freeze, tokens]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def runtime_meta(profile, tokens)
|
|
26
|
+
prefix = profile.fetch(:continuing_subword_prefix)
|
|
27
|
+
{
|
|
28
|
+
tokenizer_type: Format::TOKENIZER_BERT_WORDPIECE_V1,
|
|
29
|
+
tokenizer_profile: TOKENIZER_PROFILE,
|
|
30
|
+
do_lower_case: profile.fetch(:lowercase),
|
|
31
|
+
strip_accents: profile.fetch(:strip_accents),
|
|
32
|
+
handle_chinese_chars: profile.fetch(:handle_chinese_chars),
|
|
33
|
+
clean_text: profile.fetch(:clean_text),
|
|
34
|
+
added_token_mask: profile.fetch(:added_token_mask),
|
|
35
|
+
max_input_chars_per_word: profile.fetch(:max_input_chars_per_word),
|
|
36
|
+
max_token_chars: max_token_chars(tokens, prefix),
|
|
37
|
+
subword_prefix: prefix,
|
|
38
|
+
pad_id: token_id(tokens, "[PAD]", 0),
|
|
39
|
+
unk_id: token_id(tokens, profile.fetch(:unk_token)),
|
|
40
|
+
cls_id: token_id(tokens, "[CLS]", 0),
|
|
41
|
+
sep_id: token_id(tokens, "[SEP]", 0),
|
|
42
|
+
mask_id: token_id(tokens, "[MASK]", 0)
|
|
43
|
+
}.freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def audit(tokenizer, tokenizer_config)
|
|
47
|
+
unsupported!("tokenizer.json is not an object") unless tokenizer.is_a?(Hash)
|
|
48
|
+
unsupported!("tokenizer_config.json is not an object") unless tokenizer_config.is_a?(Hash)
|
|
49
|
+
model = tokenizer["model"] || unsupported!("tokenizer.json has no model section")
|
|
50
|
+
normalizer = tokenizer["normalizer"] || unsupported!("tokenizer has no normalizer")
|
|
51
|
+
pre_tokenizer = tokenizer["pre_tokenizer"]
|
|
52
|
+
unsupported!("tokenizer model section is not an object") unless model.is_a?(Hash)
|
|
53
|
+
unsupported!("tokenizer normalizer is not an object") unless normalizer.is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
audit_model(model)
|
|
56
|
+
audit_normalizer(normalizer)
|
|
57
|
+
audit_pre_tokenizer(pre_tokenizer)
|
|
58
|
+
added_tokens = audit_added_tokens(tokenizer["added_tokens"] || [])
|
|
59
|
+
lowercase = flag(normalizer, "lowercase", tokenizer_config["do_lower_case"], true)
|
|
60
|
+
clean_text = flag(normalizer, "clean_text", nil, true)
|
|
61
|
+
max_input_chars = Integer(model.fetch("max_input_chars_per_word", 100))
|
|
62
|
+
unsupported!("max_input_chars_per_word must be positive") unless max_input_chars.positive?
|
|
63
|
+
unsupported!("clean_text=false is not supported by the runtime") unless clean_text
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
lowercase: lowercase,
|
|
67
|
+
strip_accents: strip_accents(normalizer, lowercase),
|
|
68
|
+
clean_text: clean_text,
|
|
69
|
+
handle_chinese_chars: flag(normalizer, "handle_chinese_chars",
|
|
70
|
+
tokenizer_config["tokenize_chinese_chars"], true),
|
|
71
|
+
continuing_subword_prefix: model.fetch("continuing_subword_prefix", "##"),
|
|
72
|
+
unk_token: model.fetch("unk_token", "[UNK]"),
|
|
73
|
+
max_input_chars_per_word: max_input_chars,
|
|
74
|
+
tokenizer_class: tokenizer_config["tokenizer_class"],
|
|
75
|
+
added_tokens: added_tokens,
|
|
76
|
+
added_token_mask: added_tokens.reduce(0) do |mask, token|
|
|
77
|
+
mask | STANDARD_SPECIAL_TOKENS.fetch(token.fetch("content"))
|
|
78
|
+
end
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def audit_model(model)
|
|
83
|
+
unsupported!("tokenizer model.type is #{model['type'].inspect}, expected WordPiece") unless model["type"] == "WordPiece"
|
|
84
|
+
|
|
85
|
+
prefix = model.fetch("continuing_subword_prefix", "##")
|
|
86
|
+
unsupported!("continuing_subword_prefix #{prefix.inspect} is not supported") unless prefix == "##"
|
|
87
|
+
|
|
88
|
+
unk_token = model.fetch("unk_token", "[UNK]")
|
|
89
|
+
unsupported!("unk_token #{unk_token.inspect} is not supported") unless unk_token == "[UNK]"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def audit_normalizer(normalizer)
|
|
93
|
+
unsupported!("normalizer type #{normalizer['type'].inspect} is not BertNormalizer") unless normalizer["type"] == "BertNormalizer"
|
|
94
|
+
|
|
95
|
+
unknown = normalizer.keys - ALLOWED_NORMALIZER_KEYS
|
|
96
|
+
unsupported!("normalizer has unsupported keys #{unknown.inspect}") unless unknown.empty?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def audit_pre_tokenizer(pre_tokenizer)
|
|
100
|
+
return if pre_tokenizer.is_a?(Hash) && pre_tokenizer["type"] == "BertPreTokenizer"
|
|
101
|
+
|
|
102
|
+
unsupported!("pre_tokenizer type #{pre_tokenizer && pre_tokenizer['type'].inspect} is not BertPreTokenizer")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def audit_added_tokens(tokens)
|
|
106
|
+
unsupported!("added_tokens is not an array") unless tokens.is_a?(Array)
|
|
107
|
+
unsupported!("added_tokens contains a non-object entry") unless tokens.all? { |token| token.is_a?(Hash) }
|
|
108
|
+
bad = tokens.reject { |token| standard_special?(token) }
|
|
109
|
+
unsupported!("tokenizer declares non-standard added_tokens #{bad.map { |token| token['content'] }.inspect}") unless bad.empty?
|
|
110
|
+
|
|
111
|
+
tokens.each do |token|
|
|
112
|
+
content = token["content"].to_s
|
|
113
|
+
unsupported!("added token #{content.inspect} contains whitespace") if content.match?(/\s/)
|
|
114
|
+
unsupported!("added token #{content.inspect} uses lstrip/rstrip/single_word") if token["lstrip"] || token["rstrip"] || token["single_word"]
|
|
115
|
+
unsupported!("added token #{content.inspect} must use normalized=false") unless token["normalized"] == false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
duplicate = tokens.group_by { |token| token["content"] }.find { |_, rows| rows.length > 1 }
|
|
119
|
+
unsupported!("duplicate added token #{duplicate.first.inspect}") if duplicate
|
|
120
|
+
tokens.freeze
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def vocabulary(tokenizer)
|
|
124
|
+
vocab = tokenizer.dig("model", "vocab") || unsupported!("tokenizer.json has no model.vocab")
|
|
125
|
+
unsupported!("tokenizer model.vocab is not an object") unless vocab.is_a?(Hash)
|
|
126
|
+
tokens = Array.new(vocab.length)
|
|
127
|
+
vocab.each { |token, id| assign_vocab(tokens, token, id) }
|
|
128
|
+
missing = tokens.index(nil)
|
|
129
|
+
invalid!("vocab has a hole at id #{missing}") if missing
|
|
130
|
+
tokens
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def assign_vocab(tokens, token, id)
|
|
134
|
+
unsupported!("vocab id #{id.inspect} is not an integer") unless id.is_a?(Integer)
|
|
135
|
+
invalid!("vocab id #{id} for #{token.inspect} is outside 0...#{tokens.length}") unless id.between?(0, tokens.length - 1)
|
|
136
|
+
invalid!("duplicate vocab id #{id}") unless tokens[id].nil?
|
|
137
|
+
tokens[id] = token
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def validate_added_token_ids(added_tokens, tokens)
|
|
141
|
+
added_tokens.each do |token|
|
|
142
|
+
content = token.fetch("content")
|
|
143
|
+
id = token["id"]
|
|
144
|
+
unsupported!("added token #{content.inspect} has non-integer id #{id.inspect}") unless id.is_a?(Integer)
|
|
145
|
+
valid = id.between?(0, tokens.length - 1) && tokens[id] == content
|
|
146
|
+
unsupported!("added token #{content.inspect} id #{id} does not match model.vocab") unless valid
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def flag(hash, key, fallback, default)
|
|
151
|
+
return !!hash[key] if hash.key?(key) && !hash[key].nil?
|
|
152
|
+
return !!fallback unless fallback.nil?
|
|
153
|
+
|
|
154
|
+
default
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def strip_accents(normalizer, lowercase)
|
|
158
|
+
value = normalizer["strip_accents"]
|
|
159
|
+
normalizer.key?("strip_accents") && !value.nil? ? !!value : lowercase
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def standard_special?(token)
|
|
163
|
+
token["special"] && STANDARD_SPECIAL_TOKENS.key?(token["content"])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def max_token_chars(tokens, prefix)
|
|
167
|
+
tokens.reduce(1) do |maximum, token|
|
|
168
|
+
body = token.start_with?(prefix) ? token[prefix.length..] : token
|
|
169
|
+
[maximum, body.each_char.count].max
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def token_id(tokens, token, fallback = nil)
|
|
174
|
+
id = tokens.index(token)
|
|
175
|
+
return id unless id.nil?
|
|
176
|
+
return fallback unless fallback.nil?
|
|
177
|
+
|
|
178
|
+
invalid!("vocabulary has no #{token.inspect}")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def unsupported!(message)
|
|
182
|
+
raise UnsupportedModelError,
|
|
183
|
+
"#{message}. Supported tokenizer profile: #{TOKENIZER_PROFILE}; " \
|
|
184
|
+
"other tokenizer behaviour requires a separate runtime capability."
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def invalid!(message)
|
|
188
|
+
raise ConversionError, message
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module StaticEmbeddings
|
|
2
|
+
module Canonical
|
|
3
|
+
Dimensions = Struct.new(:native, :output, :trained, keyword_init: true)
|
|
4
|
+
Runtime = Struct.new(:normalization, :unk_policy, :empty_policy, :max_tokens, :add_special_tokens,
|
|
5
|
+
keyword_init: true)
|
|
6
|
+
Source = Struct.new(:family, :model, :revision, :oracle, :files_sha256, :tokenizer_class,
|
|
7
|
+
:config_seq_length, keyword_init: true)
|
|
8
|
+
Model = Struct.new(:tokens, :matrix, :dimensions, :runtime, :tokenizer, :source, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def dimensions(native:, output:, trained: nil)
|
|
13
|
+
Dimensions.new(native: native, output: output, trained: trained&.freeze).freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def runtime(normalization:, unk_policy:, empty_policy:, max_tokens:, add_special_tokens: false)
|
|
17
|
+
Runtime.new(
|
|
18
|
+
normalization: normalization,
|
|
19
|
+
unk_policy: unk_policy,
|
|
20
|
+
empty_policy: empty_policy,
|
|
21
|
+
max_tokens: max_tokens,
|
|
22
|
+
add_special_tokens: add_special_tokens
|
|
23
|
+
).freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def source(family:, model:, oracle:, files_sha256:, revision: nil, tokenizer_class: nil,
|
|
27
|
+
config_seq_length: nil)
|
|
28
|
+
Source.new(
|
|
29
|
+
family: family,
|
|
30
|
+
model: model,
|
|
31
|
+
revision: revision,
|
|
32
|
+
oracle: oracle,
|
|
33
|
+
files_sha256: files_sha256.freeze,
|
|
34
|
+
tokenizer_class: tokenizer_class,
|
|
35
|
+
config_seq_length: config_seq_length
|
|
36
|
+
).freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def model(tokens:, matrix:, dimensions:, runtime:, tokenizer:, source:)
|
|
40
|
+
Model.new(
|
|
41
|
+
tokens: tokens.freeze,
|
|
42
|
+
matrix: matrix,
|
|
43
|
+
dimensions: dimensions,
|
|
44
|
+
runtime: runtime,
|
|
45
|
+
tokenizer: tokenizer.freeze,
|
|
46
|
+
source: source
|
|
47
|
+
).freeze
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|