static_embeddings 0.1.5 → 1.5.7
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 +60 -0
- data/README.md +17 -2
- data/docs/ARCHITECTURE.md +14 -5
- data/docs/MODEL_AUDIT.md +110 -0
- data/ext/static_embeddings/se_alloc_stats.c +5 -6
- data/ext/static_embeddings/se_f16.c +1 -1
- data/ext/static_embeddings/se_tokenizer.c +6 -0
- data/ext/static_embeddings/se_topk.c +1 -2
- data/ext/static_embeddings/static_embeddings.c +60 -10
- 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
|
@@ -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
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
require "fileutils"
|
|
1
2
|
require "json"
|
|
2
3
|
require "optparse"
|
|
3
|
-
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module StaticEmbeddings
|
|
6
|
-
|
|
6
|
+
module CLI
|
|
7
7
|
COMMANDS = {
|
|
8
|
-
"convert" => :
|
|
9
|
-
"verify" => :
|
|
10
|
-
"inspect" => :
|
|
11
|
-
"tokenize" => :
|
|
12
|
-
"embed" => :
|
|
13
|
-
"cache-path" => :
|
|
8
|
+
"convert" => :convert_command,
|
|
9
|
+
"verify" => :verify_command,
|
|
10
|
+
"inspect" => :inspect_command,
|
|
11
|
+
"tokenize" => :tokenize_command,
|
|
12
|
+
"embed" => :embed_command,
|
|
13
|
+
"cache-path" => :cache_path_command,
|
|
14
14
|
"help" => :usage,
|
|
15
15
|
"-h" => :usage,
|
|
16
16
|
"--help" => :usage,
|
|
@@ -20,10 +20,14 @@ module StaticEmbeddings
|
|
|
20
20
|
HELP = <<~TEXT
|
|
21
21
|
static_embeddings <command> [options]
|
|
22
22
|
|
|
23
|
-
convert SOURCE_DIR Convert
|
|
23
|
+
convert SOURCE_DIR Convert Model2Vec or Sentence Transformers StaticEmbedding to .semb
|
|
24
24
|
--out PATH Output file (default: <cache>/models/<id>.semb)
|
|
25
25
|
--id ID Model id recorded in provenance
|
|
26
|
-
--max-tokens N
|
|
26
|
+
--max-tokens N Model2Vec default: 512; Sentence Transformers default: unlimited
|
|
27
|
+
--max-tokens unlimited
|
|
28
|
+
--dimensions N Keep the first N dims (Matryoshka prefix slice)
|
|
29
|
+
--revision SHA Source revision recorded in provenance
|
|
30
|
+
--trained-mrl-dims 1024,512,256,... training dims recorded in provenance
|
|
27
31
|
|
|
28
32
|
verify PATH Recompute the SHA-256 embedded in the header
|
|
29
33
|
inspect PATH Print header fields and provenance
|
|
@@ -32,67 +36,78 @@ module StaticEmbeddings
|
|
|
32
36
|
cache-path Print the model cache directory
|
|
33
37
|
TEXT
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
new.run(argv)
|
|
37
|
-
end
|
|
39
|
+
module_function
|
|
38
40
|
|
|
39
41
|
def run(argv)
|
|
40
|
-
|
|
41
|
-
return
|
|
42
|
+
command = COMMANDS[argv.shift]
|
|
43
|
+
return unknown_command unless command
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
rescue StaticEmbeddings::Error => e
|
|
45
|
+
public_send(command, argv)
|
|
46
|
+
rescue StaticEmbeddings::Error, ArgumentError, OptionParser::ParseError => e
|
|
45
47
|
warn "#{e.class.name.split('::').last}: #{e.message}"
|
|
46
48
|
1
|
|
47
49
|
end
|
|
48
50
|
|
|
49
|
-
private
|
|
50
|
-
|
|
51
51
|
def usage(*)
|
|
52
52
|
puts HELP
|
|
53
53
|
0
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def
|
|
56
|
+
def unknown_command
|
|
57
57
|
warn "unknown command"
|
|
58
58
|
usage
|
|
59
59
|
1
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def
|
|
62
|
+
def cache_path_command(*)
|
|
63
63
|
puts StaticEmbeddings.cache_dir
|
|
64
64
|
0
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
options = parse_convert_options(argv)
|
|
67
|
+
def convert_command(argv)
|
|
68
|
+
options = convert_options(argv)
|
|
70
69
|
source = required_arg(argv, "usage: static_embeddings convert SOURCE_DIR [--out PATH]")
|
|
71
70
|
model_id = options[:id] || File.basename(File.expand_path(source))
|
|
72
|
-
|
|
73
|
-
FileUtils.mkdir_p(File.dirname(
|
|
71
|
+
output = options[:out] || StaticEmbeddings.model_path(model_id)
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
74
73
|
|
|
75
|
-
report = StaticEmbeddings.convert(source, output_path:
|
|
76
|
-
|
|
77
|
-
puts conversion_report(out, report, options[:max_tokens])
|
|
74
|
+
report = StaticEmbeddings.convert(source, output_path: output, model_id: model_id, **conversion_options(options))
|
|
75
|
+
puts conversion_report(output, report)
|
|
78
76
|
0
|
|
79
77
|
end
|
|
80
78
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
def convert_options(argv)
|
|
80
|
+
{}.tap do |options|
|
|
81
|
+
OptionParser.new do |parser|
|
|
82
|
+
parser.on("--out PATH") { |value| options[:out] = value }
|
|
83
|
+
parser.on("--id ID") { |value| options[:id] = value }
|
|
84
|
+
parser.on("--max-tokens N") { |value| options[:max_tokens] = parse_max_tokens(value) }
|
|
85
|
+
parser.on("--dimensions N", Integer) { |value| options[:dimensions] = value }
|
|
86
|
+
parser.on("--revision SHA") { |value| options[:source_revision] = value }
|
|
87
|
+
parser.on("--trained-mrl-dims LIST") { |value| options[:trained_mrl_dims] = value }
|
|
88
|
+
end.parse!(argv)
|
|
89
|
+
end
|
|
89
90
|
end
|
|
90
91
|
|
|
91
|
-
def
|
|
92
|
+
def conversion_options(options)
|
|
93
|
+
options.select { |key, _| %i[max_tokens dimensions source_revision trained_mrl_dims].include?(key) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_max_tokens(value)
|
|
97
|
+
return :unlimited if value == "unlimited" || value == "0"
|
|
98
|
+
|
|
99
|
+
integer = Integer(value)
|
|
100
|
+
raise OptionParser::InvalidArgument, "--max-tokens must be positive or unlimited" unless integer.positive?
|
|
101
|
+
integer
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def conversion_report(path, report)
|
|
105
|
+
max_tokens = report[:max_tokens].to_i.zero? ? "unlimited" : report[:max_tokens]
|
|
106
|
+
dim = report[:native_dim] == report[:dim] ? report[:dim].to_s : "#{report[:dim]} (from native #{report[:native_dim]})"
|
|
92
107
|
[
|
|
93
108
|
"wrote #{path}",
|
|
94
109
|
" vocab #{report[:vocab_size]}",
|
|
95
|
-
" dim #{
|
|
110
|
+
" dim #{dim}",
|
|
96
111
|
" bytes #{report[:bytes]}",
|
|
97
112
|
" sha256 #{report[:sha256]}",
|
|
98
113
|
" max_tokens #{max_tokens}",
|
|
@@ -101,20 +116,25 @@ module StaticEmbeddings
|
|
|
101
116
|
].join("\n")
|
|
102
117
|
end
|
|
103
118
|
|
|
104
|
-
def
|
|
119
|
+
def verify_command(argv)
|
|
105
120
|
result = StaticEmbeddings.verify(required_arg(argv, "usage: static_embeddings verify PATH"))
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
if result[:ok]
|
|
122
|
+
puts "ok #{result[:expected]}"
|
|
123
|
+
0
|
|
124
|
+
else
|
|
125
|
+
warn "CHECKSUM MISMATCH"
|
|
126
|
+
warn " stored #{result[:stored]}"
|
|
127
|
+
warn " computed #{result[:expected]}"
|
|
128
|
+
1
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def inspect_command(argv)
|
|
115
133
|
model = StaticEmbeddings.load(required_arg(argv, "usage: static_embeddings inspect PATH"))
|
|
116
134
|
puts JSON.pretty_generate(model_summary(model))
|
|
117
135
|
0
|
|
136
|
+
ensure
|
|
137
|
+
model&.close
|
|
118
138
|
end
|
|
119
139
|
|
|
120
140
|
def model_summary(model)
|
|
@@ -131,18 +151,20 @@ module StaticEmbeddings
|
|
|
131
151
|
}
|
|
132
152
|
end
|
|
133
153
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
154
|
+
def tokenize_command(argv)
|
|
155
|
+
with_model_and_text(argv, "usage: static_embeddings tokenize PATH TEXT") do |model, text|
|
|
156
|
+
ids = model.tokenize(text)
|
|
157
|
+
puts JSON.generate("ids" => ids, "count" => ids.length, "unk" => ids.count(model.unk_id))
|
|
158
|
+
end
|
|
138
159
|
0
|
|
139
160
|
end
|
|
140
161
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
def embed_command(argv)
|
|
163
|
+
with_model_and_text(argv, "usage: static_embeddings embed PATH TEXT") do |model, text|
|
|
164
|
+
stats = model.embed_with_stats(text)
|
|
165
|
+
warn_high_unk(stats) if high_unk?(stats)
|
|
166
|
+
puts JSON.generate(stats_payload(model, stats))
|
|
167
|
+
end
|
|
146
168
|
0
|
|
147
169
|
end
|
|
148
170
|
|
|
@@ -151,7 +173,7 @@ module StaticEmbeddings
|
|
|
151
173
|
"token_count" => stats[:token_count],
|
|
152
174
|
"unk_count" => stats[:unk_count],
|
|
153
175
|
"truncated" => stats[:truncated],
|
|
154
|
-
"vector" => StaticEmbeddings.unpack(stats[:vector], model.dim).first.map { |
|
|
176
|
+
"vector" => StaticEmbeddings.unpack(stats[:vector], model.dim).first.map { |value| value.round(6) }
|
|
155
177
|
}
|
|
156
178
|
end
|
|
157
179
|
|
|
@@ -164,16 +186,19 @@ module StaticEmbeddings
|
|
|
164
186
|
warn "warning: #{(ratio * 100).round}% of tokens are [UNK] — wrong model for this language?"
|
|
165
187
|
end
|
|
166
188
|
|
|
167
|
-
def
|
|
189
|
+
def with_model_and_text(argv, usage)
|
|
168
190
|
path = argv.shift
|
|
169
191
|
text = argv.join(" ")
|
|
170
|
-
|
|
192
|
+
raise InvalidOptionError, usage if path.nil? || text.empty?
|
|
171
193
|
|
|
172
|
-
|
|
194
|
+
model = StaticEmbeddings.load(path)
|
|
195
|
+
yield model, text
|
|
196
|
+
ensure
|
|
197
|
+
model&.close
|
|
173
198
|
end
|
|
174
199
|
|
|
175
200
|
def required_arg(argv, usage)
|
|
176
|
-
argv.shift ||
|
|
201
|
+
argv.shift || raise(InvalidOptionError, usage)
|
|
177
202
|
end
|
|
178
203
|
end
|
|
179
204
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module StaticEmbeddings
|
|
2
|
+
module Codec
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def unpack(blob, dim, format: :f32)
|
|
6
|
+
dim = Integer(dim)
|
|
7
|
+
raise InvalidOptionError, "dim must be positive" unless dim.positive?
|
|
8
|
+
|
|
9
|
+
values = decode(blob, normalize_format(format))
|
|
10
|
+
raise InvalidOptionError, "blob is not a multiple of dim" unless (values.length % dim).zero?
|
|
11
|
+
|
|
12
|
+
values.each_slice(dim).to_a
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def pack(rows, format: :f32)
|
|
16
|
+
values = rows.first.is_a?(Array) ? rows.flatten(1) : rows
|
|
17
|
+
floats = values.map(&:to_f)
|
|
18
|
+
|
|
19
|
+
case normalize_format(format)
|
|
20
|
+
when :f32 then floats.pack("e*")
|
|
21
|
+
when :f16 then StaticEmbeddings.encode_f16(floats)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def decode(blob, format)
|
|
26
|
+
case format
|
|
27
|
+
when :f32
|
|
28
|
+
raise InvalidOptionError, "f32 blob byte size must be a multiple of 4" unless (blob.bytesize % 4).zero?
|
|
29
|
+
blob.unpack("e*")
|
|
30
|
+
when :f16
|
|
31
|
+
raise InvalidOptionError, "f16 blob byte size must be a multiple of 2" unless (blob.bytesize % 2).zero?
|
|
32
|
+
StaticEmbeddings.decode_f16(blob)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def normalize_format(format)
|
|
37
|
+
case format&.to_sym
|
|
38
|
+
when nil, :f32, :float32 then :f32
|
|
39
|
+
when :f16, :float16 then :f16
|
|
40
|
+
else
|
|
41
|
+
raise InvalidOptionError, "unsupported embedding format #{format.inspect} (expected :f32 or :f16)"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "static_embeddings/format/writer"
|
|
3
|
+
require "static_embeddings/importers"
|
|
4
|
+
require "static_embeddings/provenance"
|
|
5
|
+
require "static_embeddings/unicode_tables"
|
|
6
|
+
|
|
7
|
+
module StaticEmbeddings
|
|
8
|
+
module Conversion
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def call(source_dir, output_path:, **options)
|
|
12
|
+
model = Importers.import(source_dir, **options)
|
|
13
|
+
provenance = Provenance.json(model, unicode_source: UnicodeTables.source_stamp)
|
|
14
|
+
result = Format::Writer.call(
|
|
15
|
+
path: output_path,
|
|
16
|
+
meta: format_meta(model),
|
|
17
|
+
tokens: model.tokens,
|
|
18
|
+
matrix: model.matrix,
|
|
19
|
+
norm_tables: UnicodeTables.packed,
|
|
20
|
+
provenance: provenance
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
result.merge(
|
|
24
|
+
vocab_size: model.tokens.length,
|
|
25
|
+
dim: model.dimensions.output,
|
|
26
|
+
native_dim: model.dimensions.native,
|
|
27
|
+
max_tokens: model.runtime.max_tokens,
|
|
28
|
+
provenance: JSON.parse(provenance)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_meta(model)
|
|
33
|
+
tokenizer = model.tokenizer
|
|
34
|
+
runtime = model.runtime
|
|
35
|
+
{
|
|
36
|
+
dim: model.dimensions.output,
|
|
37
|
+
normalization_type: runtime.normalization,
|
|
38
|
+
max_tokens_default: runtime.max_tokens,
|
|
39
|
+
add_special_tokens: runtime.add_special_tokens,
|
|
40
|
+
unk_policy: runtime.unk_policy,
|
|
41
|
+
empty_policy: runtime.empty_policy,
|
|
42
|
+
do_lower_case: tokenizer.fetch(:do_lower_case),
|
|
43
|
+
strip_accents: tokenizer.fetch(:strip_accents),
|
|
44
|
+
handle_chinese_chars: tokenizer.fetch(:handle_chinese_chars),
|
|
45
|
+
clean_text: tokenizer.fetch(:clean_text),
|
|
46
|
+
added_token_mask: tokenizer.fetch(:added_token_mask),
|
|
47
|
+
max_input_chars_per_word: tokenizer.fetch(:max_input_chars_per_word),
|
|
48
|
+
max_token_chars: tokenizer.fetch(:max_token_chars),
|
|
49
|
+
subword_prefix: tokenizer.fetch(:subword_prefix),
|
|
50
|
+
pad_id: tokenizer.fetch(:pad_id),
|
|
51
|
+
unk_id: tokenizer.fetch(:unk_id),
|
|
52
|
+
cls_id: tokenizer.fetch(:cls_id),
|
|
53
|
+
sep_id: tokenizer.fetch(:sep_id),
|
|
54
|
+
mask_id: tokenizer.fetch(:mask_id)
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|