static_embeddings 0.1.4 → 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 +118 -0
- data/README.md +67 -25
- data/Rakefile +1 -1
- data/docs/ARCHITECTURE.md +54 -34
- data/docs/LIMITATIONS.md +12 -6
- data/docs/MODEL_AUDIT.md +195 -52
- data/ext/static_embeddings/se_embed.c +2 -1
- data/ext/static_embeddings/se_format.c +41 -4
- data/ext/static_embeddings/se_internal.h +17 -5
- data/ext/static_embeddings/se_tokenizer.c +155 -18
- data/ext/static_embeddings/se_unicode.c +1 -1
- data/ext/static_embeddings/static_embeddings.c +32 -2
- 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 -62
- 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 -300
- 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 +17 -4
- data/lib/static_embeddings/provenance.rb +58 -0
- data/lib/static_embeddings/reference.rb +90 -33
- data/lib/static_embeddings/row_prefix_payload.rb +59 -0
- data/lib/static_embeddings/safetensors.rb +178 -34
- data/lib/static_embeddings/version.rb +1 -1
- data/lib/static_embeddings.rb +29 -57
- data/static_embeddings.gemspec +2 -2
- data/tools/check_model2vec_parity.rb +89 -54
- 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 -284
|
@@ -87,16 +87,17 @@ static int push_id(se_scratch_t *sc, size_t *n_ids, uint32_t id) {
|
|
|
87
87
|
return 1;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
#define SE_SCRATCH_CPS_KEEP
|
|
91
|
-
#define
|
|
92
|
-
#define
|
|
90
|
+
#define SE_SCRATCH_CPS_KEEP 256
|
|
91
|
+
#define SE_SCRATCH_IDS_RESERVE 512
|
|
92
|
+
#define SE_SCRATCH_IDS_RETAIN_MAX 8192
|
|
93
|
+
#define SE_SCRATCH_BYTES_KEEP 1024
|
|
93
94
|
|
|
94
95
|
int se_scratch_reserve(se_scratch_t *s, uint32_t dim) {
|
|
95
96
|
if (!grow_u32(&s->cps, &s->cps_cap, SE_SCRATCH_CPS_KEEP))
|
|
96
97
|
return 0;
|
|
97
98
|
if (!grow_u32(&s->cps2, &s->cps2_cap, SE_SCRATCH_CPS_KEEP))
|
|
98
99
|
return 0;
|
|
99
|
-
if (!grow_u32(&s->ids, &s->ids_cap,
|
|
100
|
+
if (!grow_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_RESERVE))
|
|
100
101
|
return 0;
|
|
101
102
|
if (!grow_bytes(&s->bytes, &s->bytes_cap, SE_SCRATCH_BYTES_KEEP))
|
|
102
103
|
return 0;
|
|
@@ -149,7 +150,8 @@ static int shrink_bytes(uint8_t **buf, size_t *cap, size_t keep) {
|
|
|
149
150
|
static void se_scratch_trim(se_scratch_t *s) {
|
|
150
151
|
(void)shrink_u32(&s->cps, &s->cps_cap, SE_SCRATCH_CPS_KEEP);
|
|
151
152
|
(void)shrink_u32(&s->cps2, &s->cps2_cap, SE_SCRATCH_CPS_KEEP);
|
|
152
|
-
(
|
|
153
|
+
if (s->ids_cap > SE_SCRATCH_IDS_RETAIN_MAX)
|
|
154
|
+
(void)shrink_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_RETAIN_MAX);
|
|
153
155
|
(void)shrink_bytes(&s->bytes, &s->bytes_cap, SE_SCRATCH_BYTES_KEEP);
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -393,6 +395,37 @@ static int cp_is_cjk_segment(const se_model_t *m, uint32_t cp) {
|
|
|
393
395
|
return m->meta.handle_chinese_chars && cp >= 0x3400 && se_is_cjk(cp);
|
|
394
396
|
}
|
|
395
397
|
|
|
398
|
+
typedef struct {
|
|
399
|
+
const char *text;
|
|
400
|
+
uint32_t len;
|
|
401
|
+
uint32_t bit;
|
|
402
|
+
} se_added_token_spec_t;
|
|
403
|
+
|
|
404
|
+
static const se_added_token_spec_t SE_ADDED_TOKENS[] = {
|
|
405
|
+
{"[MASK]", 6u, SE_ADDED_MASK}, {"[PAD]", 5u, SE_ADDED_PAD}, {"[UNK]", 5u, SE_ADDED_UNK},
|
|
406
|
+
{"[CLS]", 5u, SE_ADDED_CLS}, {"[SEP]", 5u, SE_ADDED_SEP},
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
static int boundary_splits_added_token(const se_model_t *model, const uint8_t *input,
|
|
410
|
+
size_t input_len, size_t boundary) {
|
|
411
|
+
if (model->meta.added_token_mask == 0 || boundary == 0 || boundary >= input_len)
|
|
412
|
+
return 0;
|
|
413
|
+
|
|
414
|
+
for (size_t k = 0; k < SE_ARRAY_LEN(SE_ADDED_TOKENS); k++) {
|
|
415
|
+
const se_added_token_spec_t *token = &SE_ADDED_TOKENS[k];
|
|
416
|
+
if ((model->meta.added_token_mask & token->bit) == 0)
|
|
417
|
+
continue;
|
|
418
|
+
for (size_t back = 1; back < token->len && back <= boundary; back++) {
|
|
419
|
+
size_t start = boundary - back;
|
|
420
|
+
if (start + token->len > input_len || input[start] != '[')
|
|
421
|
+
continue;
|
|
422
|
+
if (memcmp(input + start, token->text, token->len) == 0)
|
|
423
|
+
return 1;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
396
429
|
size_t se_prefix_boundary_len(const se_model_t *model, const uint8_t *input, size_t input_len,
|
|
397
430
|
size_t target, size_t backscan) {
|
|
398
431
|
if (target >= input_len)
|
|
@@ -420,14 +453,15 @@ size_t se_prefix_boundary_len(const se_model_t *model, const uint8_t *input, siz
|
|
|
420
453
|
continue;
|
|
421
454
|
|
|
422
455
|
if (cp_is_cjk_segment(model, cp)) {
|
|
423
|
-
if (after <= target)
|
|
456
|
+
if (after <= target && !boundary_splits_added_token(model, input, input_len, after))
|
|
424
457
|
return after;
|
|
425
|
-
if (pos > 0)
|
|
458
|
+
if (pos > 0 && !boundary_splits_added_token(model, input, input_len, pos))
|
|
426
459
|
return pos;
|
|
427
|
-
|
|
460
|
+
continue;
|
|
428
461
|
}
|
|
429
462
|
|
|
430
|
-
if ((is_whitespace(model, cp) || is_punct(model, cp)) && after <= target
|
|
463
|
+
if ((is_whitespace(model, cp) || is_punct(model, cp)) && after <= target &&
|
|
464
|
+
!boundary_splits_added_token(model, input, input_len, after))
|
|
431
465
|
return after;
|
|
432
466
|
}
|
|
433
467
|
|
|
@@ -454,6 +488,7 @@ typedef struct {
|
|
|
454
488
|
const se_model_t *model;
|
|
455
489
|
se_scratch_t *scratch;
|
|
456
490
|
uint32_t max_tokens;
|
|
491
|
+
se_token_limit_t limit_mode;
|
|
457
492
|
size_t n_ids;
|
|
458
493
|
size_t n_unk;
|
|
459
494
|
size_t segment_len;
|
|
@@ -481,17 +516,44 @@ static int append_segment_cp(token_state_t *st, uint32_t cp) {
|
|
|
481
516
|
return 1;
|
|
482
517
|
}
|
|
483
518
|
|
|
519
|
+
static size_t limited_token_count(const token_state_t *st) {
|
|
520
|
+
if (st->limit_mode == SE_TOKEN_LIMIT_USABLE && st->model->meta.unk_policy == SE_UNK_DROP)
|
|
521
|
+
return st->n_ids - st->n_unk;
|
|
522
|
+
return st->n_ids;
|
|
523
|
+
}
|
|
524
|
+
|
|
484
525
|
static int cap_after_append(token_state_t *st) {
|
|
485
|
-
if (st->max_tokens == 0 || st
|
|
526
|
+
if (st->max_tokens == 0 || limited_token_count(st) <= (size_t)st->max_tokens)
|
|
486
527
|
return 0;
|
|
487
528
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
529
|
+
if (st->limit_mode == SE_TOKEN_LIMIT_RAW || st->model->meta.unk_policy != SE_UNK_DROP) {
|
|
530
|
+
size_t dropped_unk = 0;
|
|
531
|
+
for (size_t k = st->max_tokens; k < st->n_ids; k++) {
|
|
532
|
+
if (st->scratch->ids[k] == st->model->meta.unk_id)
|
|
533
|
+
dropped_unk++;
|
|
534
|
+
}
|
|
535
|
+
st->n_unk -= dropped_unk;
|
|
536
|
+
st->n_ids = st->max_tokens;
|
|
537
|
+
} else {
|
|
538
|
+
size_t usable = 0;
|
|
539
|
+
size_t kept_unk = 0;
|
|
540
|
+
size_t keep = 0;
|
|
541
|
+
for (size_t k = 0; k < st->n_ids; k++) {
|
|
542
|
+
uint32_t id = st->scratch->ids[k];
|
|
543
|
+
if (id == st->model->meta.unk_id) {
|
|
544
|
+
kept_unk++;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
usable++;
|
|
548
|
+
if (usable == (size_t)st->max_tokens) {
|
|
549
|
+
keep = k + 1;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
st->n_ids = keep;
|
|
554
|
+
st->n_unk = kept_unk;
|
|
492
555
|
}
|
|
493
|
-
|
|
494
|
-
st->n_ids = st->max_tokens;
|
|
556
|
+
|
|
495
557
|
st->stats->truncated = 1;
|
|
496
558
|
return 1;
|
|
497
559
|
}
|
|
@@ -602,6 +664,54 @@ static wordpiece_status_t wordpiece(const se_model_t *m, se_scratch_t *sc, const
|
|
|
602
664
|
return 1;
|
|
603
665
|
}
|
|
604
666
|
|
|
667
|
+
static uint32_t added_token_id(const se_model_t *model, uint32_t bit) {
|
|
668
|
+
switch (bit) {
|
|
669
|
+
case SE_ADDED_PAD:
|
|
670
|
+
return model->meta.pad_id;
|
|
671
|
+
case SE_ADDED_UNK:
|
|
672
|
+
return model->meta.unk_id;
|
|
673
|
+
case SE_ADDED_CLS:
|
|
674
|
+
return model->meta.cls_id;
|
|
675
|
+
case SE_ADDED_SEP:
|
|
676
|
+
return model->meta.sep_id;
|
|
677
|
+
case SE_ADDED_MASK:
|
|
678
|
+
return model->meta.mask_id;
|
|
679
|
+
default:
|
|
680
|
+
return SE_SLOT_EMPTY;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
static int match_added_token(const se_model_t *model, const uint8_t *input, size_t input_len,
|
|
685
|
+
size_t pos, uint32_t *id_out, size_t *len_out) {
|
|
686
|
+
if (model->meta.added_token_mask == 0 || pos >= input_len || input[pos] != '[')
|
|
687
|
+
return 0;
|
|
688
|
+
|
|
689
|
+
for (size_t k = 0; k < SE_ARRAY_LEN(SE_ADDED_TOKENS); k++) {
|
|
690
|
+
const se_added_token_spec_t *token = &SE_ADDED_TOKENS[k];
|
|
691
|
+
if ((model->meta.added_token_mask & token->bit) == 0)
|
|
692
|
+
continue;
|
|
693
|
+
if (token->len > input_len - pos)
|
|
694
|
+
continue;
|
|
695
|
+
if (memcmp(input + pos, token->text, token->len) != 0)
|
|
696
|
+
continue;
|
|
697
|
+
|
|
698
|
+
*id_out = added_token_id(model, token->bit);
|
|
699
|
+
*len_out = token->len;
|
|
700
|
+
return 1;
|
|
701
|
+
}
|
|
702
|
+
return 0;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
static se_status_t append_direct_id(token_state_t *st, uint32_t id, int *stop) {
|
|
706
|
+
if (!push_id(st->scratch, &st->n_ids, id))
|
|
707
|
+
return oom(st, "adding a special token");
|
|
708
|
+
if (id == st->model->meta.unk_id)
|
|
709
|
+
st->n_unk++;
|
|
710
|
+
if (cap_after_append(st))
|
|
711
|
+
*stop = 1;
|
|
712
|
+
return SE_OK;
|
|
713
|
+
}
|
|
714
|
+
|
|
605
715
|
static se_status_t append_wordpiece(token_state_t *st, const uint32_t *word, size_t word_len,
|
|
606
716
|
int *stop) {
|
|
607
717
|
wordpiece_status_t wp =
|
|
@@ -752,6 +862,31 @@ static se_status_t tokenize_ascii_run(token_state_t *st, const uint8_t *input, s
|
|
|
752
862
|
if (b >= 0x80)
|
|
753
863
|
break;
|
|
754
864
|
|
|
865
|
+
if (b == '[' && st->model->meta.added_token_mask != 0) {
|
|
866
|
+
uint32_t added_id = 0;
|
|
867
|
+
size_t added_len = 0;
|
|
868
|
+
if (match_added_token(st->model, input, input_len, i, &added_id, &added_len)) {
|
|
869
|
+
se_status_t rc = flush_segment(st, stop);
|
|
870
|
+
if (rc != SE_OK) {
|
|
871
|
+
*ip = i;
|
|
872
|
+
return rc;
|
|
873
|
+
}
|
|
874
|
+
if (*stop) {
|
|
875
|
+
*ip = i;
|
|
876
|
+
return SE_OK;
|
|
877
|
+
}
|
|
878
|
+
rc = append_direct_id(st, added_id, stop);
|
|
879
|
+
if (rc != SE_OK) {
|
|
880
|
+
*ip = i;
|
|
881
|
+
return rc;
|
|
882
|
+
}
|
|
883
|
+
i += added_len;
|
|
884
|
+
if (*stop)
|
|
885
|
+
break;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
755
890
|
if (i >= next_cancel_check) {
|
|
756
891
|
if (token_cancelled(st)) {
|
|
757
892
|
*ip = i;
|
|
@@ -811,8 +946,9 @@ static se_status_t tokenize_ascii_run(token_state_t *st, const uint8_t *input, s
|
|
|
811
946
|
}
|
|
812
947
|
|
|
813
948
|
se_status_t se_tokenize(const se_model_t *model, se_scratch_t *sc, const uint8_t *input,
|
|
814
|
-
size_t input_len, uint32_t max_tokens,
|
|
815
|
-
|
|
949
|
+
size_t input_len, uint32_t max_tokens, se_token_limit_t limit_mode,
|
|
950
|
+
se_token_stats_t *stats, se_error_t *err,
|
|
951
|
+
volatile sig_atomic_t *cancelled) {
|
|
816
952
|
memset(stats, 0, sizeof(*stats));
|
|
817
953
|
|
|
818
954
|
token_state_t st;
|
|
@@ -820,6 +956,7 @@ se_status_t se_tokenize(const se_model_t *model, se_scratch_t *sc, const uint8_t
|
|
|
820
956
|
st.model = model;
|
|
821
957
|
st.scratch = sc;
|
|
822
958
|
st.max_tokens = max_tokens;
|
|
959
|
+
st.limit_mode = limit_mode;
|
|
823
960
|
st.stats = stats;
|
|
824
961
|
st.err = err;
|
|
825
962
|
st.cancelled = cancelled;
|
|
@@ -115,6 +115,6 @@ const se_map_entry_t *se_map_lookup(const se_map_entry_t *entries, uint32_t coun
|
|
|
115
115
|
int se_is_cjk(uint32_t cp) {
|
|
116
116
|
return (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3400 && cp <= 0x4DBF) ||
|
|
117
117
|
(cp >= 0x20000 && cp <= 0x2A6DF) || (cp >= 0x2A700 && cp <= 0x2B73F) ||
|
|
118
|
-
(cp >= 0x2B740 && cp <= 0x2B81F) || (cp >=
|
|
118
|
+
(cp >= 0x2B740 && cp <= 0x2B81F) || (cp >= 0x2B920 && cp <= 0x2CEAF) ||
|
|
119
119
|
(cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0x2F800 && cp <= 0x2FA1F);
|
|
120
120
|
}
|
|
@@ -80,6 +80,7 @@ static ID id_blocking_p;
|
|
|
80
80
|
static ID id_vector;
|
|
81
81
|
static ID id_token_count;
|
|
82
82
|
static ID id_unk_count;
|
|
83
|
+
static ID id_pooled_count;
|
|
83
84
|
static ID id_truncated;
|
|
84
85
|
static ID id_dim;
|
|
85
86
|
static ID id_allow_unfrozen;
|
|
@@ -1030,6 +1031,10 @@ static VALUE model_embed_with_stats(int argc, VALUE *argv, VALUE self) {
|
|
|
1030
1031
|
rb_hash_aset(hash, ID2SYM(id_vector), vector);
|
|
1031
1032
|
rb_hash_aset(hash, ID2SYM(id_token_count), UINT2NUM(stats.token_count));
|
|
1032
1033
|
rb_hash_aset(hash, ID2SYM(id_unk_count), UINT2NUM(stats.unk_count));
|
|
1034
|
+
uint32_t pooled_count = get_model(self)->model.meta.unk_policy == SE_UNK_DROP
|
|
1035
|
+
? stats.token_count - stats.unk_count
|
|
1036
|
+
: stats.token_count;
|
|
1037
|
+
rb_hash_aset(hash, ID2SYM(id_pooled_count), UINT2NUM(pooled_count));
|
|
1033
1038
|
rb_hash_aset(hash, ID2SYM(id_truncated), stats.truncated ? Qtrue : Qfalse);
|
|
1034
1039
|
return hash;
|
|
1035
1040
|
}
|
|
@@ -1049,7 +1054,7 @@ typedef struct {
|
|
|
1049
1054
|
static VALUE tokenize_scratch_body(VALUE arg) {
|
|
1050
1055
|
tokenize_scratch_job_t *job = (tokenize_scratch_job_t *)(uintptr_t)arg;
|
|
1051
1056
|
job->rc = se_tokenize(job->model, job->scratch, job->input, job->input_len, job->max_tokens,
|
|
1052
|
-
&job->stats, &job->err, NULL);
|
|
1057
|
+
SE_TOKEN_LIMIT_RAW, &job->stats, &job->err, NULL);
|
|
1053
1058
|
if (job->rc != SE_OK)
|
|
1054
1059
|
return Qnil;
|
|
1055
1060
|
|
|
@@ -1237,7 +1242,27 @@ static VALUE embed_token_ids_value(VALUE self, VALUE ids_value, VALUE max_tokens
|
|
|
1237
1242
|
size_t n = (size_t)n_long;
|
|
1238
1243
|
int truncated = 0;
|
|
1239
1244
|
|
|
1240
|
-
if (max_tokens != 0 &&
|
|
1245
|
+
if (max_tokens != 0 && w->model.meta.unk_policy == SE_UNK_DROP) {
|
|
1246
|
+
size_t usable = 0;
|
|
1247
|
+
size_t cutoff = n;
|
|
1248
|
+
for (size_t i = 0; i < n; i++) {
|
|
1249
|
+
unsigned long long value = NUM2ULL(rb_ary_entry(ids_value, (long)i));
|
|
1250
|
+
if (value >= w->model.meta.vocab_size)
|
|
1251
|
+
rb_raise(rb_eArgError, "token id at index %zu is out of range", i);
|
|
1252
|
+
|
|
1253
|
+
if ((uint32_t)value == w->model.meta.unk_id)
|
|
1254
|
+
continue;
|
|
1255
|
+
|
|
1256
|
+
usable++;
|
|
1257
|
+
if (usable == (size_t)max_tokens) {
|
|
1258
|
+
cutoff = i + 1;
|
|
1259
|
+
} else if (usable > (size_t)max_tokens) {
|
|
1260
|
+
n = cutoff;
|
|
1261
|
+
truncated = 1;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
} else if (max_tokens != 0 && n > (size_t)max_tokens) {
|
|
1241
1266
|
n = (size_t)max_tokens;
|
|
1242
1267
|
truncated = 1;
|
|
1243
1268
|
}
|
|
@@ -1297,6 +1322,10 @@ static VALUE model_embed_token_ids_with_stats(int argc, VALUE *argv, VALUE self)
|
|
|
1297
1322
|
rb_hash_aset(hash, ID2SYM(id_vector), vector);
|
|
1298
1323
|
rb_hash_aset(hash, ID2SYM(id_token_count), UINT2NUM(stats.token_count));
|
|
1299
1324
|
rb_hash_aset(hash, ID2SYM(id_unk_count), UINT2NUM(stats.unk_count));
|
|
1325
|
+
uint32_t pooled_count = get_model(self)->model.meta.unk_policy == SE_UNK_DROP
|
|
1326
|
+
? stats.token_count - stats.unk_count
|
|
1327
|
+
: stats.token_count;
|
|
1328
|
+
rb_hash_aset(hash, ID2SYM(id_pooled_count), UINT2NUM(pooled_count));
|
|
1300
1329
|
rb_hash_aset(hash, ID2SYM(id_truncated), stats.truncated ? Qtrue : Qfalse);
|
|
1301
1330
|
return hash;
|
|
1302
1331
|
}
|
|
@@ -1607,6 +1636,7 @@ RUBY_FUNC_EXPORTED void Init_static_embeddings(void) {
|
|
|
1607
1636
|
id_vector = rb_intern("vector");
|
|
1608
1637
|
id_token_count = rb_intern("token_count");
|
|
1609
1638
|
id_unk_count = rb_intern("unk_count");
|
|
1639
|
+
id_pooled_count = rb_intern("pooled_count");
|
|
1610
1640
|
id_truncated = rb_intern("truncated");
|
|
1611
1641
|
id_dim = rb_intern("dim");
|
|
1612
1642
|
id_allow_unfrozen = rb_intern("allow_unfrozen");
|
|
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
|