// 训练产物(`tally-train v1`)的导出与回读。
//
// 与 `tally-vocab v1` 的分工:
//
// * 词表格式存的是**展开后的结果**——每个 id 对应哪串字节;
// * 训练产物存的是**配方**——参数、特殊 token、合并序列。
//
// 256 个单字节是隐含基座,不必写;合并按 rank 稠密升序,token 表可以完全重建。
// 文件里的 id 一路升序:先 `[merges]`(256 起),再 `[specials]`(接在合并产物之后)。
// 于是训练产物能回答"这份词表是拿什么语料、什么参数训出来的",词表格式答不了。
// 只存能重建词表的最小信息,不做冗余:冗余字段是漂移的机会。
///|
/// 训练产物的版本标识行。
pub let train_format_header : String = "# tally-train v1"
///|
/// 导出为 `tally-train v1`。
///
/// 与 `to_text` 同源的理由:绝不依赖 Map 的迭代顺序,
/// specials 按 id 升序、merges 按 rank 升序,行尾一律 `\n`。
pub fn TrainResult::to_text(self : TrainResult) -> String {
let out : Array[Char] = []
push_str(out, train_format_header)
push_str(out, "\n")
push_str(out, "# unicode ")
push_str(out, @bpe.unicode_version())
push_str(out, "\n")
push_str(out, "# corpus_bytes ")
push_int(out, self.corpus_bytes)
push_str(out, "\n")
push_str(out, "# corpus_words ")
push_int(out, self.corpus_words)
push_str(out, "\n")
push_str(out, "# vocab_size ")
push_int(out, self.vocab_size)
push_str(out, "\n")
push_str(out, "# min_frequency ")
push_int(out, self.min_frequency)
push_str(out, "\n")
push_str(out, "# tokens ")
push_int(out, self.vocab.token_count())
push_str(out, "\n")
push_str(out, "[merges]\n")
for m in self.merges {
push_int(out, m.rank)
out.push(' ')
push_str(out, b64_encode(m.merged))
out.push(' ')
push_str(out, b64_encode(m.left))
out.push(' ')
push_str(out, b64_encode(m.right))
out.push('\n')
}
push_str(out, "[specials]\n")
for e in self.vocab.specials {
let (lit, id) = e
push_int(out, id)
out.push(' ')
push_str(out, b64_encode(@utf8.encode(lit)))
out.push('\n')
}
String::from_array(out)
}
///|
/// 从 `# key value` 元数据行里取十进制值。
fn header_dec(line_no : Int, line : StringView, s : StringView) -> Int raise VocabError {
match parse_dec(s) {
Some(v) => v
None => raise MalformedLine(line_no, line.to_owned())
}
}
///|
/// 回读 `tally-train v1`,重建 `TrainResult`(词表一并重建)。
///
/// 校验的都是"重建会不会错"的要害:合并 rank 稠密升序、`merged == left + right`、
/// 特殊 token 的 id 接在合并产物之后、`# tokens` 与重建结果相符。
pub fn load_train(text : String) -> TrainResult raise VocabError {
let merges : Array[Merge] = []
let specials : Array[(String, Int)] = []
let mut corpus_bytes = 0
let mut corpus_words = 0
let mut vocab_size = 0
let mut min_frequency = 0
let mut declared_tokens = 0
let mut tokens_line = 0
let mut section = 0 // 0=头部 1=merges 2=specials
let mut line_no = 0
for raw in text.split("\n") {
line_no = line_no + 1
let line = raw.trim()
if line.length() == 0 {
continue
}
if line == "[merges]" {
section = 1
continue
}
if line == "[specials]" {
section = 2
continue
}
if line.has_prefix("#") {
let parts = split_fields(line)
if parts.length() == 3 {
match parts[1] {
"corpus_bytes" =>
corpus_bytes = header_dec(line_no, line, parts[2])
"corpus_words" =>
corpus_words = header_dec(line_no, line, parts[2])
"vocab_size" => vocab_size = header_dec(line_no, line, parts[2])
"min_frequency" =>
min_frequency = header_dec(line_no, line, parts[2])
"tokens" => {
declared_tokens = header_dec(line_no, line, parts[2])
tokens_line = line_no
}
_ => ()
}
}
continue
}
let parts = split_fields(line)
if section == 1 {
if parts.length() != 4 {
raise MalformedLine(line_no, line.to_owned())
}
let rank = header_dec(line_no, line, parts[0])
let merged = b64_decode(parts[1])
let left = b64_decode(parts[2])
let right = b64_decode(parts[3])
// rank 稠密升序才是训练的真实产物;merged 必须真是 left ++ right。
if rank != 256 + merges.length() {
raise MalformedLine(line_no, line.to_owned())
}
if !bytes_eq(merged, @bpe.concat_bytes(left, right)) {
raise MalformedLine(line_no, line.to_owned())
}
merges.push({ rank: rank, merged: merged, left: left, right: right })
} else if section == 2 {
if parts.length() != 2 {
raise MalformedLine(line_no, line.to_owned())
}
let id = header_dec(line_no, line, parts[0])
// 特殊 token 紧跟在 256 个基座与全部合并产物之后。
if id != 256 + merges.length() + specials.length() {
raise MalformedLine(line_no, line.to_owned())
}
specials.push((@utf8.decode_lossy(b64_decode(parts[1])), id))
} else {
raise UnknownSection(line.to_owned())
}
}
// 重建 token 表:基座 -> 合并产物 -> 特殊 token。
let tokens : Array[Bytes] = []
let ranks : Map[Bytes, Int] = Map([])
for b = 0; b < 256; b = b + 1 {
let one = Bytes::make(1, b.to_byte())
ranks.set(one, b)
tokens.push(one)
}
for m in merges {
ranks.set(m.merged, m.rank)
tokens.push(m.merged)
}
for e in specials {
tokens.push(@utf8.encode(e.0))
}
let vocab = build_vocab(tokens, ranks, specials)
if declared_tokens != vocab.token_count() {
raise MalformedLine(tokens_line, "# tokens 与重建结果不符")
}
{
vocab: vocab,
merges: merges,
corpus_bytes: corpus_bytes,
corpus_words: corpus_words,
vocab_size: vocab_size,
min_frequency: min_frequency,
}
}