TT Lab
开始
学习 学习路径 课程

MiniMind — 亲手从头到尾训练一个小型语言模型

借来的分词器把韩文切成字节

在 TT Lab 中继续学习

一句话总结

分词器是把文本转换成模型所“吃”的编号(令牌,token)的词典。MiniMind 使用词表 6,400 个 的字节级 BPE,这个大小是为了让嵌入在小模型里不占用过多参数而选的值。可是那部词典是用中文和英文训练的,几乎是按字节来切分韩文的。在这个模块里,我们用自己的语料重新训练词典,并用数字比较同一段文本会变成多少个令牌。

为什么需要它

语言模型不认识字符。它只接收一串整数编号,并按编号各取出一行嵌入向量来用。所以有两件事会同时被确定。

MiniMind 还建议 不要重新训练分词器。因为词典一变,权重、数据格式和推理接口都要跟着变,就没法与别人共用模型;以令牌为单位计算的困惑度(PPL)在词典不同的情况下,彼此也无法比较。这门课程明知这条建议,仍然特意重新训练——为的是亲自量出,在韩文语料上借来的词典有多么昂贵。

工作原理

MiniMind 的 trainer/train_tokenizer.py 用 HuggingFace tokenizers 拼装出三个部件。

tok = Tokenizer(models.BPE())
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
trainer = trainers.BpeTrainer(vocab_size=6400,
    initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),   # 바이트 256개를 처음부터
    special_tokens=["<|endoftext|>", "<|im_start|>", "<|im_end|>", ...])
tok.decoder = decoders.ByteLevel()

字节级(ByteLevel)先把文本转成 UTF-8 字节,以 256 个字节作为基本字符。任何字符都可以用字节表示,所以不会出现“不认识的字符”。一个韩文字符在 UTF-8 中占 3 个字节,如果词典里没有把这个字符合并起来的令牌,一个字符最多会变成三个令牌。

BPE 会反复把最常相邻出现的两个片段合并成一个,直到词表被填满。如果可合并的配对先用完,它就会在词表尚未填满时停下——这门课程的语料里词的种类很少,即使指定 1,024,也会在 700 多个时停下。在实验中你会亲眼看到。

特殊令牌 在训练之前就占据最前面的位置。MiniMind 使用 0 号 <|endoftext|>(填充)、1 号 <|im_start|>(bos)、2 号 <|im_end|>(eos),并且为工具调用、思考模式用的令牌一共预留了 36 个位置。这些编号是聊天格式和损失掩码所依赖的标记,不能被拆成字节。

在现场相遇的样子

想用公司内部文档微调开源模型,却发现上下文窗口很快就满、长文档被截断,这时应当先量一下该模型的分词器把韩文切成多少个令牌。同样的成本,能装下的文本量会因分词器不同而相差两三倍。不过,已经训练好的模型的词典是不能改的——词典一换,嵌入表就作废了。选词典这件事,只有从头训练时才能做决定,所以 MiniMind 的词典一旦定下来就一直沿用。

比较模型之间的质量时,词典不同,也不能直接比较每个令牌的损失。令牌长,猜中一个令牌就难,令牌短就容易。这就是 MiniMind 的 README 建议在分词器不同时使用每字节比特数(BPB)的原因,这门课程的最后一个模块会亲自测量 BPB。

本课程与 MiniMind 原版的不同之处

MiniMind 的 train_tokenizer.py 用 SFT 数据(sft_t2t_mini.jsonl)中的对话内容训练词表 6,400,并预留了 36 个特殊令牌位置——包括工具调用(<tool_call>)、思考模式(<think>)以及今后使用的备用格(<|buffer1|> …)。这门课程改了三处。训练数据只使用预训练语料的 text(这样评分器才能在相同条件下重新训练并进行比较),词表缩小到 1,024,特殊令牌只保留聊天所必需的三个。因为我们不处理工具调用和思考模式。但它沿用与 MiniMind 相同的名称和相同的编号(0、1、2),让后续模块的聊天格式和损失掩码与 MiniMind 的代码完全一样地工作。

下一项实验要做什么

用 MiniMind 的词典切分我们的韩文语料,测出每个字符的令牌数,再用同样的拼装训练出词表 1,024 的词典来比较。把词表改成 384、512、1,024,看压缩率如何变化,词表大小会使嵌入所占的份额改变多少,这些也都以数字留下来。