用 MiniMind 分词器测量韩语,并亲手训练小词表
目标
用 MiniMind 发布的词表 6,400 的分词器切分韩文语料,测出每个字符的令牌数,并用同样的拼装(字节级 BPE)训练一部词表 1,024 的词典来比较。把词表大小如何改变压缩率和嵌入所占份额,用数字留下来。
为什么重要
令牌数就是计算量,也是上下文窗口的大小。同一段文本如果变成两倍的令牌,训练和推理的开销都会翻倍,一次能看到的文本则减半。而词典与模型是一体的,开始训练之后就不能再改——所以只有从头训练的人才需要做这个决定。 MiniMind 的词典是用中文和英文数据训练的。对那部词典来说,韩文几乎是第一次见到的字符,会被按字节切开。这个实验量出这笔成本,并确认针对我们语料定制的词典能切得多短。同时也看一下在小模型里词表占参数的多少——这就是 MiniMind 选择 6,400 这个较小词表的原因。
步骤
- 把 MiniMind 分词器(
/opt/minimind/model/tokenizer.json、tokenizer_config.json)的词表大小,以及 bos、eos、pad 令牌及其编号,写入 /root/mm/tok/minimind_vocab.json。 - 用该分词器切分验证语料(
/opt/mm/data/pretrain_val.jsonl的 text),把每个字符和每个字节的令牌数写入 /root/mm/tok/borrowed.json。 - 只用
/opt/mm/data/pretrain.jsonl的 text 训练字节级 BPE(词表 1024,特殊令牌<|endoftext|>、<|im_start|>、<|im_end|>按此顺序),保存到 /root/mm/tok/tokenizer.json。 - 用新的分词器切分
<|im_start|>user\n안녕<|im_end|>\n(其中的韩文意为“你好”),写入 /root/mm/tok/chat_ids.json。 - 用新的分词器做与第 2 步相同的测量,写入 /root/mm/tok/own.json。
- 把词表 384、512、1024 的字符/令牌写入 /root/mm/tok/sweep.json。
- 把 MiniMind-3 的默认设置、我们的小设置、在我们的设置中放入词表 6400 这三个模型的嵌入份额,写入 /root/mm/tok/embed_share.json。
- 在 /root/mm/tok/report.md 中写
## 빌려 온 토크나이저、## 우리 토크나이저、## 어휘 크기와 임베딩三节(三个标题为韩文,依次意为“借来的分词器”“我们的分词器”“词表大小与嵌入”),并放入第 2 步和第 5 步的字符/令牌。
参考
- 输入
mm-info,就能一眼看到镜像里的 MiniMind 副本、数据和基准产物的位置。 - 分词器用
from tokenizers import Tokenizer; Tokenizer.from_file(경로)(占位符为文件路径)读取,切分时使用encode(글, add_special_tokens=False).ids(占位符为文本)。 - 常见错误:训练时漏掉特殊令牌,导致
<|im_start|>被拆成十个字节;用训练语料而不是验证语料来测压缩率;把共享的嵌入和输出层算了两遍。 - 原文:MiniMind train_tokenizer.py · HuggingFace tokenizers — BPE · ByteLevel 预分词
打开 MiniMind 的词典
读取 /opt/minimind/model/tokenizer.json 和 tokenizer_config.json,把词表大小、bos、eos、pad 令牌字符串及其编号,以 vocab_size、bos、eos、pad、bos_id、eos_id、pad_id 写入 /root/mm/tok/minimind_vocab.json。
词表大小用 Tokenizer.get_vocab_size(),编号用 token_to_id()。哪个令牌是 bos、eos、pad,不在 tokenizer.json 里,而在 tokenizer_config.json 的 bos_token、eos_token、pad_token 中。
用借来的词典测量韩文
用 MiniMind 分词器切分 /opt/mm/data/pretrain_val.jsonl 中的全部 text,把总字符数÷令牌数和总 UTF-8 字节数÷令牌数,以 tokens、chars_per_token、bytes_per_token 写入 /root/mm/tok/borrowed.json。
一个韩文字符在 UTF-8 中占 3 个字节。字符/令牌小于 1,就意味着把一个字符切成了多个片段。如果把测量脚本写成接收分词器路径和结果路径作为参数,第 5 步就能直接再用。
训练一部词表 1024 的词典
编写脚本 /root/mm/tok/train_tok.py(参数:词表大小、保存路径),只用 /opt/mm/data/pretrain.jsonl 的 text,按与 MiniMind 相同的拼装(models.BPE + pre_tokenizers.ByteLevel(add_prefix_space=False) + decoders.ByteLevel),以 vocab_size=1024、特殊令牌 <|endoftext|>、<|im_start|>、<|im_end|> 的顺序训练,并保存到 /root/mm/tok/tokenizer.json。
是 BpeTrainer(vocab_size=…, initial_alphabet=pre_tokenizers.ByteLevel.alphabet(), special_tokens=[…])。特殊令牌会按写下的顺序成为 0、1、2 号。保存之后,get_vocab_size() 得到的值小于 1024 是正常的——可合并的配对先用完了。
特殊令牌必须是一个整体
用新的分词器切分字符串 <|im_start|>user\n안녕<|im_end|>\n(其中的韩文意为“你好”),以 text(该字符串)和 ids(令牌编号列表)写入 /root/mm/tok/chat_ids.json。
注册为特殊令牌的字符串不会被拆成字节,而是成为一个编号。第一个编号应该是 1(<|im_start|>),2(<|im_end|>)应该出现一次。SFT 的损失掩码就以这些编号作为标记。
用我们的词典重新测量
用新的分词器(/root/mm/tok/tokenizer.json)做与第 2 步相同的测量,以 tokens、chars_per_token、bytes_per_token 写入 /root/mm/tok/own.json。
必须是同一份验证语料、同样的计算,这两个数字才能比较。我们的语料里词的种类很少,词表只有 700 多个,一个词也能成为一个令牌——如果是真实的韩文语料,会留下比这长得多的长尾。
词表增加,会缩短多少
用相同条件再训练词表 384、512 的词典,把三部词典(384、512、1024)在验证语料上的字符/令牌,以 {"384": 값, "512": 값, "1024": 값}(韩文占位符,意为“值”)写入 /root/mm/tok/sweep.json。
词表越大,字符/令牌越大,但到某一点就会停住。想一想指定 1024 时实际词表是多少个(第 3 步),就能明白为什么会停住。1024 的值必须与第 5 步的值相同。
词表吃掉多少参数
用 mmkit.new_model(설정)(占位符为配置)创建三个模型——MiniMind-3 默认设置(MiniMindConfig() 的 hidden_size、num_hidden_layers、vocab_size)、我们的小设置(mmkit.SMALL)、在我们的设置中把 vocab_size 设为 6400——并把嵌入参数数、总参数数和份额,以 minimind3、ours、ours_vocab6400 写入 /root/mm/tok/embed_share.json。
MiniMind 共享嵌入和输出层(lm_head)(tie_word_embeddings)。model.parameters() 只会把共享的张量返回一次,所以它们的总和就是全部。嵌入是 model.model.embed_tokens.weight。
留下选择词典的依据
在 /root/mm/tok/report.md 中写 ## 빌려 온 토크나이저、## 우리 토크나이저、## 어휘 크기와 임베딩 三节(三个标题为韩文,依次意为“借来的分词器”“我们的分词器”“词表大小与嵌入”),并以数字放入第 2 步和第 5 步的字符/令牌(chars_per_token)。
借来的词典把韩文切得长了几倍,它在训练和推理中的代价是什么,各用一行写下来。最后一节里,如果加上在我们这个体量上使用词表 6400 时,嵌入占全部的百分之几,就更好了。