 —— 分词器)
TL;DRBPE自底向上合并每一步只看相邻 token 对的频率谁出现得多就合并谁简单高效GPT 系常用。WordPiece同样自底向上合并但决策依据换成似然增益P(A,B) / (P(A)·P(B))更看重合并后片段是否“语义凝聚”BERT 系常用。Unigram方向相反自顶向下删减先建一个超大词表再逐步删掉“删了之后似然损失最小”的 tokenT5/Gemma 系常用。SentencePiece不是算法而是框架内部可跑 BPE 或 Unigram把空格转义为▁直接在原始文本上训练做到语言无关、无损可逆。一句话BPE / WordPiece / Unigram 是三种词表构建策略频率贪心 / 似然贪心 / 概率删减SentencePiece 是把它们工程化的容器。目录BPE自底向上按“频率”贪心合并WordPiece自底向上按“似然增益”择优合并Unigram自顶向下按“概率贡献”逐步删减SentencePiece不是算法而是承载算法的“端到端框架”四者关系一句话总结训练产物补充字符、ASCII、Unicode、UTF-8、字节、Token ID 的关系4种常用分词器对比表分词器的任务不是简单地“切词”而是在字符与完整词之间找到一种可复用、可组合的中间单元。DIY-LLM 教程用“快递分拣中心”来比喻这个过程理想的切分像incredible → [in, credible]这样既不过于碎片化也不过于笼统。BPE、WordPiece、Unigram 本质上都是在用不同的“迭代策略”寻找这种平衡而 SentencePiece 则是承载这些策略的一个工程框架。下面结合具体例子逐一拆解它们的迭代逻辑。tokenizer 与 LLM四种分词算法BPE自底向上按“频率”贪心合并核心迭代逻辑BPE 从字符或字节级别开始把每个字符视为一个独立 token。然后反复扫描语料统计相邻 token 对的出现频次每次合并频次最高的那一对生成一个新 token直到词表达到预设大小或不再有可合并的对。例子合并low/lower/newest/widest假设语料中newest出现 6 次widest出现 3 次。初始时所有单词都被拆成字符词边界用/w标记。BPE 统计相邻字符对发现e和s的组合出现了 9 次来自newest的es和widest的es是所有相邻对中频率最高的。于是第一次迭代将e s → es合并。下一次迭代再统计时es和t的组合频率最高同样是 9 次继续合并为est。如此迭代高频片段会逐渐“长”成更大的子词单元。Python 实现手写一遍 BPE 合并过程下面用一段可运行的代码把上面的迭代逻辑完整走一遍初始化词表 → 统计相邻对频次 → 合并最高频对 → 循环迭代。fromcollectionsimportCounter# 1. 初始语料词 - 出现次数词边界用 /w 标记corpus{low:5,lower:2,newest:6,widest:3,}# 2. 初始词表把每个词拆成字符序列末尾追加 /wdefinit_vocab(corpus):vocab{}forword,freqincorpus.items():charslist(word)[/w]vocab[tuple(chars)]freqreturnvocab# 3. 统计所有相邻 token 对的频次defget_pair_stats(vocab):pairsCounter()fortokens,freqinvocab.items():foriinrange(len(tokens)-1):pairs[(tokens[i],tokens[i1])]freqreturnpairs# 4. 把词中所有出现的 best_pair 合并成一个新 tokendefmerge_vocab(best_pair,vocab):new_vocab{}bigram.join(best_pair)# 例如 (e, s) - esfortokens,freqinvocab.items():new_tokens,i[],0whileilen(tokens):ifilen(tokens)-1and(tokens[i],tokens[i1])best_pair:new_tokens.append(bigram)i2else:new_tokens.append(tokens[i])i1new_vocab[tuple(new_tokens)]freqreturnnew_vocab# 5. 主循环反复合并最高频对vocabinit_vocab(corpus)print(初始词表)fortokens,freqinvocab.items():print(f{ .join(tokens):30}freq{freq})num_merges5forstepinrange(1,num_merges1):pairsget_pair_stats(vocab)ifnotpairs:breakbest_pairmax(pairs,keypairs.get)print(f\n第{step}次迭代最高频对{best_pair}频次{pairs[best_pair]})vocabmerge_vocab(best_pair,vocab)fortokens,freqinvocab.items():print(f{ .join(tokens):30}freq{freq})运行后的输出词表变化示例初始词表 l o w /w freq5 l o w e r /w freq2 n e w e s t /w freq6 w i d e s t /w freq3 第 1 次迭代最高频对 (e, s)频次 9 l o w /w freq5 l o w e r /w freq2 n e w es t /w freq6 w i d es t /w freq3 第 2 次迭代最高频对 (es, t)频次 9 l o w /w freq5 l o w e r /w freq2 n e w est /w freq6 w i d est /w freq3 第 3 次迭代最高频对 (est, /w)频次 9 l o w /w freq5 l o w e r /w freq2 n e w est/w freq6 w i d est/w freq3 第 4 次迭代最高频对 (l, o)频次 7 lo w /w freq5 lo w e r /w freq2 n e w est/w freq6 w i d est/w freq3 第 5 次迭代最高频对 (lo, w)频次 7 low /w freq5 low e r /w freq2 n e w est/w freq6 w i d est/w freq3可以看到前三次迭代把e s → es → est → est/w一路“长”成完整后缀这正是频率驱动的直观体现随后low也被合并成一个整体。每一步的合并规则(e, s)、(es, t)、(est, /w)会被依次写入merges文件与vocab一起构成训练产物。特点BPE 的决策完全由频率驱动不涉及概率模型因此实现简单、训练高效。代价是它可能合并出纯粹因为“常挨在一起”但语义关联并不强的片段。GPT 系列GPT-2/3/4的 byte-level BPE 是这一思路的典型工程化实现。WordPiece自底向上按“似然增益”择优合并核心迭代逻辑WordPiece 的迭代方向与 BPE 相同自底向上合并但选择合并哪一对的标准不同。BPE 只看频次WordPiece 则评估合并这一对之后语言模型对语料的整体似然能提升多少。一个常用的近似评分是score ( A , B ) P ( A , B ) P ( A ) × P ( B ) \text{score}(A,B) \frac{P(A,B)}{P(A) \times P(B)}score(A,B)P(A)×P(B)P(A,B)这个比值衡量的是(A) 和 (B) 一起出现的概率比它们“各自独立随机出现”的期望高出多少。比值越大说明 (A) 和 (B) 的共现越“有意义”越值得合并。例子同一个语料选择可能不同在语料low:5, lower:2, newest:6, widest:3中BPE 会优先合并频次最高的e s9 次。但 WordPiece 可能会计算后发现合并low和est能带来更大的似然增益因为lowest作为整体在语言中的搭配概率远高于low和est随机拼在一起的概率从而优先选择不同的合并路径。也就是说频次最高的对不一定是 WordPiece 会选的对WordPiece 更在意合并后的片段是否构成一个“语义上更凝聚”的单元。特点WordPiece 用“似然增益”替代了单纯的频率能更好地控制词表质量减少 OOV未登录词也因此成为BERT系列模型的标准分词方案。代价是计算量比 BPE 更大每一步都需要评估概率。Unigram自顶向下按“概率贡献”逐步删减核心迭代逻辑Unigram 的迭代方向与前两者相反它不是从小词表往大词表“长”而是先构造一个过大的初始词表包含语料中所有可能出现的子串或高频片段初始化每个 token 的概率然后反复删除那些“删掉之后对语料整体似然损失增加最小”的 token直到词表收缩到目标大小。例子hug / pug / pun / bun / hugs假设语料中各词频为hug:10, pug:5, pun:12, bun:4, hugs:5。Unigram 的初始词表可能包含所有子串h, u, g, hu, ug, p, pu, n, un, b, bu, s, hug, gs, ugs等。然后它为每个子串计算在语料中的概率例如ug出现在hug、pug、hugs中总频次 20总频次之和 210所以 (\text{P}(\text{ug}) 20/210)。接下来算法评估如果从词表中删掉ugs这个片段语料库的似然会下降多少如果下降很小说明ugs“不太必要”就可以删除。每一步删除一批贡献最小的 token迭代收缩直到词表达到目标大小。特点Unigram 基于概率语言模型对低频词和多语言场景更友好因为它不像 BPE/WordPiece 那样依赖贪心合并路径而是从全局概率视角筛选词表。T5、mT5、Gemma等 Google 系模型常使用 Unigram 模式。代价是训练计算开销通常更高。SentencePiece不是算法而是承载算法的“端到端框架”关键定位严格来说SentencePiece 与前三种不在同一层级BPE、WordPiece、Unigram 是“分词算法”SentencePiece 是一个实现框架/工具它内部可以选择运行 BPE 或 Unigram但额外解决了前几种算法在工程上遗留的一个问题——预分词依赖。核心差异把空格也当作 token 来学习传统 BPE/WordPiece 通常需要先按空格把句子切成“词”再在每个词内部做子词切分。这对中文、日文等无空格语言不友好也会让空格本身游离在建模之外。SentencePiece直接在原始文本上训练把空格转义为特殊字符▁U2581让空格也成为一个可学习的 token从而做到语言无关且可逆解码。例子英文Hello World→ SentencePieceBPE 模式可能输出[▁Hello, ▁World]▁标记了词首的空格。解码时把▁还原为空格即可无损恢复原文。中文你好世界不需要先做“词”级别的预分词SentencePiece 直接把这个字符串当作 Unicode 序列在其上运行 BPE 或 Unigram 进行子词构建。特点SentencePiece 的价值在于端到端、语言无关、无损可逆。LLaMA使用的是 SentencePiece 的 BPE 模式T5使用的是 SentencePiece 的 Unigram 模式DeepSeek系列也基于这一框架。它让同一个工程流程可以服务于中、英、日、韩等多种文字体系而不需要为每种语言单独设计预分词规则。四者关系一句话总结算法 / 框架迭代方向每一步的决策依据工程定位BPE自底向上合并相邻对的频率最高算法简单高效GPT 系常用WordPiece自底向上合并似然增益最大算法BERT 系常用Unigram自顶向下删减删除后似然损失增加最小算法T5/Gemma 系常用SentencePiece取决于内部算法取决于内部选 BPE 或 Unigram框架解决预分词与空格建模语言无关所以理解这四者的关键不在于把它们并列成“四种同类的算法”而在于看清BPE、WordPiece、Unigram 是三种不同的词表构建策略频率贪心 / 似然贪心 / 概率删减SentencePiece 则是把其中某些策略工程化、语言无关化的容器。训练产物Step1导出核心产物不管是使用哪一种分词算法训练完成后都需要导出至少两个关键文件vocab文件记录所有token及其对应的id是编码器和解码器的核心索引。merges文件按顺序记录所有子词合并规则或概率模型。二者共同决定tokenizer的编码与解码逻辑并确保编码的可逆。Step2下游使用前的验证与评估将tokenizer应用于一部分验证集后建议统计以下关键指标平均token数与最大长度分布 直接影响显存占用、训练速度和推理效率。碎片化情况 检查关键实体、专业术语是否被拆得过碎避免影响模型理解。跨语言token平衡度 多语言任务中需确保不同语言的常见模式都有足够的token支持。如果后续需要扩表如加入新领域术语、专业词或品牌名等建议优先采用这些方式而非完全重训tokenizer增量训练、加入新的merges项、清理极低频token。并且扩表后应进行一次回归测试确保——与旧模型保持兼容且根据数字化编码可以还原回到最开始的输入文本、不发生token分配冲突或token耗尽问题。补充字符、ASCII、Unicode、UTF-8、字节、Token ID 的关系ASCII128 个字符编号 0127只覆盖英文、数字、常见标点中文、emoji 没有位置。Unicode给全世界字符统一编号即码点只规定“字符 → 编号”不规定编号如何存成字节。UTF-8规定“码点 → 字节序列”“A” → 411 字节“注” → E6 B3 A83 字节“” → F0 9F 9A 804 字节。字节8 bit范围 0255计算机存储的基本单位。提问为什么 tokenizer 又在操作“字符”DeepSeek 使用 byte-level BPE语义字符 → Unicode 码点 → UTF-8 字节 → 字节代理符号 → BPE token → Token ID例如 “注”“注” → Unicode 码点 U6CE8 → UTF-8 字节 E6 B3 A8 → 字节映射为 ‘æ’ ‘³’ ‘¨’ → BPE 可能合并为 ‘注’ → 查表得到 ID 7421因为 BPE 需要一个可遍历、可比较、可拼接的符号序列但这里的“字符”不是原始语义字符而是字节的代理符号‘æ’ 在这里不是字母 æ而是字节 0xE6 的显示代号。BPE 合并出的 ‘注’ 也不是语义字符“注”而是字节组合的 token。按照原来教程中的解释为什么DeepSeek要用latin1编解码?在 DeepSeek 的分词流程中最终处理得到的是数字化的 token但在 BPE 分词器训练阶段需要按“字符”操作。如果直接用 UTF-8 编解码汉字或 emoji 等多字节字符在拆分为单字节时会出现不完整序列Python 会报错或替换导致信息丢失。而 latin-1 是单字节编码它把每个字节0–255机械映射为一个 Unicode 字符保证任意字节序列都能完整、可逆地保存从而让 BPE 或其他子词算法能把字节当作字符合并而不丢数据。简单来说使用 latin-1 是为了在分词器中安全地把原始字节当作字符处理确保编码器阶段信息完整。4种常用分词器对比表分词器类型粒度词表大小词表外(OOV)序列长度代表模型字符级细小(100–5k)无非常长Char-RNN字节级更细 (字节)很小 (~256–1k)无很长GPT-2词级粗极大 (100k)严重短Word2Vec, GloVeBPE中 (自适应)适中 (30k–100k)极少适中GPT-4, Llama 3