static_embeddings 0.1.1
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 +7 -0
- data/CHANGELOG.md +136 -0
- data/LICENSE.txt +21 -0
- data/README.md +439 -0
- data/docs/ARCHITECTURE.md +278 -0
- data/docs/MODEL_AUDIT.md +61 -0
- data/docs/PERFORMANCE.md +60 -0
- data/exe/static_embeddings +6 -0
- data/ext/static_embeddings/extconf.rb +44 -0
- data/ext/static_embeddings/se_embed.c +192 -0
- data/ext/static_embeddings/se_format.c +762 -0
- data/ext/static_embeddings/se_internal.h +266 -0
- data/ext/static_embeddings/se_tokenizer.c +506 -0
- data/ext/static_embeddings/se_unicode.c +120 -0
- data/ext/static_embeddings/static_embeddings.c +2070 -0
- data/lib/static_embeddings/cli.rb +178 -0
- data/lib/static_embeddings/converter.rb +284 -0
- data/lib/static_embeddings/errors.rb +6 -0
- data/lib/static_embeddings/format.rb +289 -0
- data/lib/static_embeddings/model.rb +48 -0
- data/lib/static_embeddings/paths.rb +29 -0
- data/lib/static_embeddings/reference.rb +191 -0
- data/lib/static_embeddings/safetensors.rb +87 -0
- data/lib/static_embeddings/unicode_tables.rb +127 -0
- data/lib/static_embeddings/version.rb +3 -0
- data/lib/static_embeddings.rb +118 -0
- data/static_embeddings.gemspec +45 -0
- data/tools/benchmark.rb +38 -0
- data/tools/build_demo_model.rb +16 -0
- data/tools/check_model2vec_parity.rb +107 -0
- data/tools/make_fixture_model.rb +165 -0
- metadata +134 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "optparse"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module StaticEmbeddings
|
|
6
|
+
class CLI
|
|
7
|
+
COMMANDS = {
|
|
8
|
+
"convert" => :convert,
|
|
9
|
+
"verify" => :verify,
|
|
10
|
+
"inspect" => :inspect_model,
|
|
11
|
+
"tokenize" => :tokenize,
|
|
12
|
+
"embed" => :embed,
|
|
13
|
+
"cache-path" => :cache_path,
|
|
14
|
+
"help" => :usage,
|
|
15
|
+
"-h" => :usage,
|
|
16
|
+
"--help" => :usage,
|
|
17
|
+
nil => :usage
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
HELP = <<~TEXT
|
|
21
|
+
static_embeddings <command> [options]
|
|
22
|
+
|
|
23
|
+
convert SOURCE_DIR Convert a HuggingFace/Model2Vec directory to .semb
|
|
24
|
+
--out PATH Output file (default: <cache>/models/<id>.semb)
|
|
25
|
+
--id ID Model id recorded in provenance
|
|
26
|
+
--max-tokens N Truncation limit baked into the file (default 512)
|
|
27
|
+
|
|
28
|
+
verify PATH Recompute the SHA-256 embedded in the header
|
|
29
|
+
inspect PATH Print header fields and provenance
|
|
30
|
+
tokenize PATH TEXT Print token ids
|
|
31
|
+
embed PATH TEXT Print the vector, token count and UNK ratio
|
|
32
|
+
cache-path Print the model cache directory
|
|
33
|
+
TEXT
|
|
34
|
+
|
|
35
|
+
def self.run(argv)
|
|
36
|
+
new.run(argv)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run(argv)
|
|
40
|
+
method = COMMANDS[argv.shift]
|
|
41
|
+
return unknown unless method
|
|
42
|
+
|
|
43
|
+
send(method, argv)
|
|
44
|
+
rescue StaticEmbeddings::Error => e
|
|
45
|
+
warn "#{e.class.name.split('::').last}: #{e.message}"
|
|
46
|
+
1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def usage(*)
|
|
52
|
+
puts HELP
|
|
53
|
+
0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def unknown
|
|
57
|
+
warn "unknown command"
|
|
58
|
+
usage
|
|
59
|
+
1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cache_path(*)
|
|
63
|
+
puts StaticEmbeddings.cache_dir
|
|
64
|
+
0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def convert(argv)
|
|
68
|
+
options = parse_convert_options(argv)
|
|
69
|
+
source = required_arg(argv, "usage: static_embeddings convert SOURCE_DIR [--out PATH]")
|
|
70
|
+
model_id = options[:id] || File.basename(File.expand_path(source))
|
|
71
|
+
out = options[:out] || StaticEmbeddings.model_path(model_id)
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(out))
|
|
73
|
+
|
|
74
|
+
report = StaticEmbeddings.convert(source, output_path: out, model_id: model_id,
|
|
75
|
+
max_tokens: options[:max_tokens])
|
|
76
|
+
puts conversion_report(out, report, options[:max_tokens])
|
|
77
|
+
0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parse_convert_options(argv)
|
|
81
|
+
options = { max_tokens: Converter::REFERENCE_MAX_TOKENS }
|
|
82
|
+
OptionParser.new do |parser|
|
|
83
|
+
parser.on("--out PATH") { |value| options[:out] = value }
|
|
84
|
+
parser.on("--id ID") { |value| options[:id] = value }
|
|
85
|
+
parser.on("--max-tokens N", Integer) { |value| options[:max_tokens] = value }
|
|
86
|
+
end.parse!(argv)
|
|
87
|
+
options
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def conversion_report(path, report, max_tokens)
|
|
91
|
+
[
|
|
92
|
+
"wrote #{path}",
|
|
93
|
+
" vocab #{report[:vocab_size]}",
|
|
94
|
+
" dim #{report[:dim]}",
|
|
95
|
+
" bytes #{report[:bytes]}",
|
|
96
|
+
" sha256 #{report[:sha256]}",
|
|
97
|
+
" max_tokens #{max_tokens}",
|
|
98
|
+
"",
|
|
99
|
+
"Record this conversion in docs/MODEL_AUDIT.md before trusting the vectors."
|
|
100
|
+
].join("\n")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def verify(argv)
|
|
104
|
+
result = StaticEmbeddings.verify(required_arg(argv, "usage: static_embeddings verify PATH"))
|
|
105
|
+
return puts("ok #{result[:expected]}") || 0 if result[:ok]
|
|
106
|
+
|
|
107
|
+
warn "CHECKSUM MISMATCH"
|
|
108
|
+
warn " stored #{result[:stored]}"
|
|
109
|
+
warn " computed #{result[:expected]}"
|
|
110
|
+
1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def inspect_model(argv)
|
|
114
|
+
model = StaticEmbeddings.load(required_arg(argv, "usage: static_embeddings inspect PATH"))
|
|
115
|
+
puts JSON.pretty_generate(model_summary(model))
|
|
116
|
+
0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def model_summary(model)
|
|
120
|
+
{
|
|
121
|
+
"path" => model.path,
|
|
122
|
+
"dim" => model.dim,
|
|
123
|
+
"vocab_size" => model.vocab_size,
|
|
124
|
+
"max_tokens" => model.max_tokens,
|
|
125
|
+
"normalized" => model.normalized?,
|
|
126
|
+
"lowercase" => model.lowercase?,
|
|
127
|
+
"unk_id" => model.unk_id,
|
|
128
|
+
"mapped_bytes" => model.mapped_bytes,
|
|
129
|
+
"provenance" => model.provenance
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def tokenize(argv)
|
|
134
|
+
model, text = model_and_text(argv, "usage: static_embeddings tokenize PATH TEXT")
|
|
135
|
+
ids = model.tokenize(text)
|
|
136
|
+
puts JSON.generate("ids" => ids, "count" => ids.length, "unk" => ids.count(model.unk_id))
|
|
137
|
+
0
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def embed(argv)
|
|
141
|
+
model, text = model_and_text(argv, "usage: static_embeddings embed PATH TEXT")
|
|
142
|
+
stats = model.embed_with_stats(text)
|
|
143
|
+
warn_high_unk(stats) if high_unk?(stats)
|
|
144
|
+
puts JSON.generate(stats_payload(model, stats))
|
|
145
|
+
0
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def stats_payload(model, stats)
|
|
149
|
+
{
|
|
150
|
+
"token_count" => stats[:token_count],
|
|
151
|
+
"unk_count" => stats[:unk_count],
|
|
152
|
+
"truncated" => stats[:truncated],
|
|
153
|
+
"vector" => StaticEmbeddings.unpack(stats[:vector], model.dim).first.map { |v| v.round(6) }
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def high_unk?(stats)
|
|
158
|
+
stats[:token_count].positive? && stats[:unk_count].to_f / stats[:token_count] > 0.3
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def warn_high_unk(stats)
|
|
162
|
+
ratio = stats[:unk_count].to_f / stats[:token_count]
|
|
163
|
+
warn "warning: #{(ratio * 100).round}% of tokens are [UNK] — wrong model for this language?"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def model_and_text(argv, usage)
|
|
167
|
+
path = argv.shift
|
|
168
|
+
text = argv.join(" ")
|
|
169
|
+
abort usage if path.nil? || text.empty?
|
|
170
|
+
|
|
171
|
+
[StaticEmbeddings.load(path), text]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def required_arg(argv, usage)
|
|
175
|
+
argv.shift || abort(usage)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "digest"
|
|
3
|
+
|
|
4
|
+
module StaticEmbeddings
|
|
5
|
+
class Converter
|
|
6
|
+
REFERENCE_IMPL = "model2vec.StaticModel"
|
|
7
|
+
REFERENCE_MAX_TOKENS = 512
|
|
8
|
+
|
|
9
|
+
ALLOWED_NORMALIZER_KEYS = %w[type clean_text handle_chinese_chars strip_accents lowercase].freeze
|
|
10
|
+
STANDARD_SPECIAL_TOKENS = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"].freeze
|
|
11
|
+
SOURCE_FILES = %w[tokenizer.json config.json tokenizer_config.json model.safetensors].freeze
|
|
12
|
+
TOKENIZER_PROFILE = "BERT_WORDPIECE_V1"
|
|
13
|
+
|
|
14
|
+
attr_reader :source_dir, :report
|
|
15
|
+
|
|
16
|
+
def initialize(source_dir)
|
|
17
|
+
@source_dir = source_dir
|
|
18
|
+
@report = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def convert(output_path:, model_id: nil, max_tokens: REFERENCE_MAX_TOKENS)
|
|
22
|
+
source = load_source
|
|
23
|
+
profile = audit_tokenizer(source[:tokenizer], source[:tokenizer_config])
|
|
24
|
+
tokens = extract_vocab(source[:tokenizer])
|
|
25
|
+
matrix, dim = extract_matrix(tokens.length)
|
|
26
|
+
meta = runtime_meta(source[:config], profile, tokens, max_tokens)
|
|
27
|
+
|
|
28
|
+
result = write_model(output_path, meta, tokens, matrix,
|
|
29
|
+
provenance_for(model_id, meta, profile, source[:config], tokens.length, dim))
|
|
30
|
+
@report = result.merge(vocab_size: tokens.length, dim: dim, provenance: JSON.parse(result[:provenance]))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def load_source
|
|
36
|
+
{
|
|
37
|
+
tokenizer: load_json("tokenizer.json"),
|
|
38
|
+
config: load_json("config.json", optional: true) || {},
|
|
39
|
+
tokenizer_config: load_json("tokenizer_config.json", optional: true) || {}
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def load_json(name, optional: false)
|
|
44
|
+
path = File.join(source_dir, name)
|
|
45
|
+
return nil if optional && !File.file?(path)
|
|
46
|
+
raise ConversionError, "missing #{name} in #{source_dir}" unless File.file?(path)
|
|
47
|
+
|
|
48
|
+
JSON.parse(File.binread(path))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reject!(reason)
|
|
52
|
+
raise UnsupportedModelError,
|
|
53
|
+
"#{reason}. This converter only accepts the #{TOKENIZER_PROFILE} profile; " \
|
|
54
|
+
"supporting this model would require implementing that behaviour in the runtime first."
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def audit_tokenizer(tokenizer, tokenizer_config)
|
|
58
|
+
model = tokenizer["model"] or reject!("tokenizer.json has no model section")
|
|
59
|
+
normalizer = tokenizer["normalizer"] or reject!("tokenizer has no normalizer")
|
|
60
|
+
pre_tokenizer = tokenizer["pre_tokenizer"]
|
|
61
|
+
|
|
62
|
+
assert_wordpiece!(model)
|
|
63
|
+
assert_normalizer!(normalizer)
|
|
64
|
+
assert_pre_tokenizer!(pre_tokenizer)
|
|
65
|
+
audit_added_tokens(tokenizer)
|
|
66
|
+
|
|
67
|
+
profile = profile_from(model, normalizer, tokenizer_config)
|
|
68
|
+
reject!("clean_text=false is not supported by the runtime") unless profile[:clean_text]
|
|
69
|
+
profile
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def assert_wordpiece!(model)
|
|
73
|
+
reject!("tokenizer model.type is #{model['type'].inspect}, expected WordPiece") unless model["type"] == "WordPiece"
|
|
74
|
+
|
|
75
|
+
prefix = model.fetch("continuing_subword_prefix", "##")
|
|
76
|
+
reject!("continuing_subword_prefix #{prefix.inspect} is not supported") unless prefix == "##"
|
|
77
|
+
|
|
78
|
+
unk = model.fetch("unk_token", "[UNK]")
|
|
79
|
+
reject!("unk_token #{unk.inspect} is not supported") unless unk == "[UNK]"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def assert_normalizer!(normalizer)
|
|
83
|
+
reject!("normalizer type #{normalizer['type'].inspect} is not BertNormalizer") unless normalizer["type"] == "BertNormalizer"
|
|
84
|
+
|
|
85
|
+
unknown = normalizer.keys - ALLOWED_NORMALIZER_KEYS
|
|
86
|
+
reject!("normalizer has unsupported keys #{unknown.inspect}") unless unknown.empty?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def assert_pre_tokenizer!(pre_tokenizer)
|
|
90
|
+
return if pre_tokenizer && pre_tokenizer["type"] == "BertPreTokenizer"
|
|
91
|
+
|
|
92
|
+
reject!("pre_tokenizer type #{pre_tokenizer && pre_tokenizer['type'].inspect} is not BertPreTokenizer")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def profile_from(model, normalizer, tokenizer_config)
|
|
96
|
+
lowercase = flag_value(normalizer, "lowercase", tokenizer_config["do_lower_case"], true)
|
|
97
|
+
{
|
|
98
|
+
lowercase: lowercase,
|
|
99
|
+
strip_accents: strip_accents?(normalizer, lowercase),
|
|
100
|
+
clean_text: flag_value(normalizer, "clean_text", nil, true),
|
|
101
|
+
handle_chinese_chars: flag_value(normalizer, "handle_chinese_chars",
|
|
102
|
+
tokenizer_config["tokenize_chinese_chars"], true),
|
|
103
|
+
continuing_subword_prefix: model.fetch("continuing_subword_prefix", "##"),
|
|
104
|
+
unk_token: model.fetch("unk_token", "[UNK]"),
|
|
105
|
+
max_input_chars_per_word: model.fetch("max_input_chars_per_word", 100).to_i,
|
|
106
|
+
tokenizer_class: tokenizer_config["tokenizer_class"]
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def flag_value(hash, key, fallback, default)
|
|
111
|
+
return !!hash[key] if hash.key?(key) && !hash[key].nil?
|
|
112
|
+
return !!fallback unless fallback.nil?
|
|
113
|
+
|
|
114
|
+
default
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def strip_accents?(normalizer, lowercase)
|
|
118
|
+
return !!normalizer["strip_accents"] if normalizer.key?("strip_accents") && !normalizer["strip_accents"].nil?
|
|
119
|
+
|
|
120
|
+
lowercase
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def audit_added_tokens(tokenizer)
|
|
124
|
+
added = tokenizer["added_tokens"] || []
|
|
125
|
+
bad_content = added.reject { |token| standard_special?(token) }
|
|
126
|
+
reject!("tokenizer declares non-standard added_tokens #{bad_content.map { |t| t['content'] }.inspect}") unless bad_content.empty?
|
|
127
|
+
|
|
128
|
+
whitespace = added.find { |token| token["content"].to_s.match?(/\s/) }
|
|
129
|
+
reject!("added token #{whitespace['content'].inspect} contains whitespace") if whitespace
|
|
130
|
+
|
|
131
|
+
flagged = added.find { |token| token["lstrip"] || token["rstrip"] || token["single_word"] }
|
|
132
|
+
reject!("added token #{flagged['content'].inspect} uses lstrip/rstrip/single_word") if flagged
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def standard_special?(token)
|
|
136
|
+
token["special"] && STANDARD_SPECIAL_TOKENS.include?(token["content"])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def extract_vocab(tokenizer)
|
|
140
|
+
vocab = tokenizer.dig("model", "vocab") or reject!("tokenizer.json has no model.vocab")
|
|
141
|
+
vocab.each_with_object(Array.new(vocab.length)) do |(token, id), tokens|
|
|
142
|
+
validate_vocab_id!(token, id, vocab.length, tokens)
|
|
143
|
+
tokens[id] = token
|
|
144
|
+
end.tap do |tokens|
|
|
145
|
+
missing = tokens.index(nil)
|
|
146
|
+
raise ConversionError, "vocab has a hole at id #{missing}" if missing
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_vocab_id!(token, id, size, tokens)
|
|
151
|
+
reject!("vocab id #{id.inspect} is not an integer") unless id.is_a?(Integer)
|
|
152
|
+
unless id.between?(0, size - 1)
|
|
153
|
+
raise ConversionError, "vocab id #{id} for #{token.inspect} is outside 0...#{size}"
|
|
154
|
+
end
|
|
155
|
+
raise ConversionError, "duplicate vocab id #{id}" unless tokens[id].nil?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def extract_matrix(vocab_size)
|
|
159
|
+
path = File.join(source_dir, "model.safetensors")
|
|
160
|
+
raise ConversionError, "missing model.safetensors in #{source_dir}" unless File.file?(path)
|
|
161
|
+
|
|
162
|
+
name, tensor = sole_matrix_tensor(Safetensors.read(path)[:tensors])
|
|
163
|
+
rows, dim = tensor[:shape]
|
|
164
|
+
if rows != vocab_size
|
|
165
|
+
raise ConversionError,
|
|
166
|
+
"embedding matrix #{name} has #{rows} rows but the tokenizer has " \
|
|
167
|
+
"#{vocab_size} tokens — refusing to guess the mapping"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
[tensor[:bytes], dim]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def sole_matrix_tensor(tensors)
|
|
174
|
+
matrices = tensors.select { |_, tensor| tensor[:shape].length == 2 }
|
|
175
|
+
raise ConversionError, "model.safetensors contains no 2-D tensor" if matrices.empty?
|
|
176
|
+
return matrices.first if matrices.length == 1
|
|
177
|
+
|
|
178
|
+
raise ConversionError,
|
|
179
|
+
"model.safetensors contains several 2-D tensors (#{matrices.keys.inspect}); " \
|
|
180
|
+
"a static embedding model must have exactly one"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def runtime_meta(config, profile, tokens, max_tokens)
|
|
184
|
+
base_meta(config, max_tokens)
|
|
185
|
+
.merge(tokenizer_meta(profile, tokens))
|
|
186
|
+
.merge(token_ids(tokens, profile))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def base_meta(config, max_tokens)
|
|
190
|
+
{
|
|
191
|
+
dim: nil,
|
|
192
|
+
normalization_type: normalization_from(config),
|
|
193
|
+
max_tokens_default: max_tokens.to_i,
|
|
194
|
+
add_special_tokens: false,
|
|
195
|
+
unk_policy: Format::UNK_DROP,
|
|
196
|
+
empty_policy: Format::EMPTY_ZERO_VECTOR
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def tokenizer_meta(profile, tokens)
|
|
201
|
+
{
|
|
202
|
+
do_lower_case: profile[:lowercase],
|
|
203
|
+
strip_accents: profile[:strip_accents],
|
|
204
|
+
handle_chinese_chars: profile[:handle_chinese_chars],
|
|
205
|
+
clean_text: profile[:clean_text],
|
|
206
|
+
max_input_chars_per_word: profile[:max_input_chars_per_word],
|
|
207
|
+
max_token_chars: max_token_chars(tokens, profile[:continuing_subword_prefix]),
|
|
208
|
+
subword_prefix: profile[:continuing_subword_prefix]
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def max_token_chars(tokens, prefix)
|
|
213
|
+
tokens.map do |token|
|
|
214
|
+
body = token.start_with?(prefix) ? token[prefix.length..] : token
|
|
215
|
+
body.each_char.count
|
|
216
|
+
end.max || 1
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def token_ids(tokens, profile)
|
|
220
|
+
{
|
|
221
|
+
pad_id: token_id(tokens, "[PAD]", 0),
|
|
222
|
+
unk_id: token_id(tokens, profile[:unk_token]),
|
|
223
|
+
cls_id: token_id(tokens, "[CLS]", 0),
|
|
224
|
+
sep_id: token_id(tokens, "[SEP]", 0),
|
|
225
|
+
mask_id: token_id(tokens, "[MASK]", 0)
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def normalization_from(config)
|
|
230
|
+
config.key?("normalize") && !config["normalize"] ? Format::NORMALIZATION_NONE : Format::NORMALIZATION_L2
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def token_id(tokens, token, fallback = nil)
|
|
234
|
+
id = tokens.index(token)
|
|
235
|
+
return id unless id.nil?
|
|
236
|
+
return fallback unless fallback.nil?
|
|
237
|
+
|
|
238
|
+
raise ConversionError, "vocabulary has no #{token.inspect}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def source_digests
|
|
242
|
+
SOURCE_FILES.each_with_object({}) do |name, acc|
|
|
243
|
+
path = File.join(source_dir, name)
|
|
244
|
+
acc[name] = Digest::SHA256.hexdigest(File.binread(path)) if File.file?(path)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def provenance_for(model_id, meta, profile, config, vocab_size, dim)
|
|
249
|
+
ordered_json(
|
|
250
|
+
"format_version" => Format::VERSION,
|
|
251
|
+
"converter_version" => StaticEmbeddings::VERSION,
|
|
252
|
+
"source_model_id" => model_id || File.basename(File.expand_path(source_dir)),
|
|
253
|
+
"source_files_sha256" => source_digests,
|
|
254
|
+
"reference_impl" => REFERENCE_IMPL,
|
|
255
|
+
"reference_max_tokens" => meta[:max_tokens_default],
|
|
256
|
+
"unicode_source" => UnicodeTables.source_stamp,
|
|
257
|
+
"tokenizer_profile" => TOKENIZER_PROFILE,
|
|
258
|
+
"tokenizer_class" => profile[:tokenizer_class],
|
|
259
|
+
"vocab_size" => vocab_size,
|
|
260
|
+
"dim" => dim,
|
|
261
|
+
"normalize" => meta[:normalization_type] == Format::NORMALIZATION_L2,
|
|
262
|
+
"config_seq_length" => config["seq_length"],
|
|
263
|
+
"notes" => "Vectors are only reference-compatible if docs/MODEL_AUDIT.md records a passing oracle run for this source revision."
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def ordered_json(hash)
|
|
268
|
+
JSON.generate(hash.sort_by { |key, _| key }.to_h)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def write_model(output_path, meta, tokens, matrix, provenance)
|
|
272
|
+
dim = matrix.bytesize / (tokens.length * 4)
|
|
273
|
+
result = Format.write(
|
|
274
|
+
path: output_path,
|
|
275
|
+
meta: meta.merge(dim: dim),
|
|
276
|
+
tokens: tokens,
|
|
277
|
+
matrix: matrix,
|
|
278
|
+
norm_tables: UnicodeTables.packed,
|
|
279
|
+
provenance: provenance
|
|
280
|
+
)
|
|
281
|
+
result.merge(provenance: provenance)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|