文本分词算法:Byte-Pair Encoding (BPE)、WordPiece 和 SentencePiece

文本分词算法:Byte-Pair Encoding (BPE)、WordPiece 和 SentencePiece 在将自然语言文本输入大语言模型之前必须先将其转换为模型能够计算的数字序列。这一过程被称为分词Tokenization而执行该转换的模块即为分词器Tokenizer。分词器定义了一套规则将原始文本切分为一个个最小的语义单元这些单元叫作词元Token。为了在词汇表大小与语义表达能力之间取得平衡现代大语言模型普遍采用子词分词Subword Tokenization算法。其核心思想是将高频词如 “agent”保留为一个完整的词元而将低频或未登录词如 “Tokenization”拆分成多个有意义的子词片段如 “Token” 和 “ization”。这样做既能控制词汇表规模又赋予模型通过子词组合来理解与生成新词的能力。最终所有词元会被映射为固定的整数 ID形成模型可直接计算的数字序列。本文围绕子词分词技术详细讲解三种主流算法Byte-Pair Encoding (BPE)、WordPiece和SentencePiece (Unigram 语言模型)。1. Byte-Pair Encoding (BPE)原理BPE 最早是一种数据压缩算法后被用于机器翻译等 NLP 任务。它从字符级开始统计所有相邻符号对的出现频率反复将频率最高的那一对合并成一个新符号直到词汇表达到预定大小或没有可合并的对。训练 BPE 前通常需要一个预分词步骤如按空格分词并为每个词加上结尾标记如/w保证合并出的子词能还原词边界。最终词汇表包含单字符、合并得到的子词以及词尾标记。举例假设有词频数据low/w出现 5 次lower/w出现 2 次newest/w出现 6 次widest/w出现 3 次。初始分割为字符序列例如l o w /w。统计所有相邻对(l, o)出现 527 次(o, w)出现 7 次(w, /w)出现 5 次(e, w)出现 2 次…最高频对为(e, s)出现 639 次于是合并成es。继续迭代直到词汇表达到预设大小。编码/分词对于一个新词按照训练时学到的合并顺序merges依次应用合并规则将词拆分为子词序列。Python 实现importrefromcollectionsimportCounter,defaultdictdefget_initial_vocab(word_freqs):将词拆为字符序列末尾加特殊标记/wvocab{}forword,freqinword_freqs.items():chars .join(list(word)) /wvocab[chars]freqreturnvocabdefget_pair_stats(vocab):统计所有相邻符号对的频率pairsCounter()forword,freqinvocab.items():symbolsword.split()foriinrange(len(symbols)-1):pairs[(symbols[i],symbols[i1])]freqreturnpairsdefmerge_vocab(pair,vocab):将指定符号对在词汇表中合并bigram .join(pair)replacement.join(pair)new_vocab{}forword,freqinvocab.items():new_wordword.replace(bigram,replacement)new_vocab[new_word]freqreturnnew_vocabdefbpe_train(word_freqs,num_merges):训练 BPE返回合并列表和最终词汇表vocabget_initial_vocab(word_freqs)merges[]foriinrange(num_merges):pairsget_pair_stats(vocab)ifnotpairs:breakbest_pairmax(pairs,keypairs.get)merges.append(best_pair)vocabmerge_vocab(best_pair,vocab)returnmerges,vocab# ---- 使用演示 ----word_freqs{low:5,lower:2,newest:6,widest:3}merges,final_vocabbpe_train(word_freqs,num_merges10)print(合并顺序:,merges)print(最终词汇表示例:,list(final_vocab.items())[:5])2. WordPiece原理WordPiece 与 BPE 非常相似最大的区别在于合并准则WordPiece 不选频率最高的对而是选择最大化语言模型似然的符号对。实际实现中计算分数score freq ( x , y ) freq ( x ) × freq ( y ) \text{score} \frac{\text{freq}(x,y)}{\text{freq}(x) \times \text{freq}(y)}scorefreq(x)×freq(y)freq(x,y)​其中freq ( x , y ) \text{freq}(x,y)freq(x,y)是相邻对共现次数freq ( x ) \text{freq}(x)freq(x)和freq ( y ) \text{freq}(y)freq(y)分别是两个符号单独出现的次数。这样倾向于合并那些经常一起出现、且单独出现相对较少的组合得到的子词更具语言学意义。与 BPE 的另一区别是WordPiece 在词内部用##前缀标记非起始子词如word与##piece以防止跨词边界的歧义合并。举例词频同 BPE 例子但合并时选择 score 最高的对统计出(e, s)频率高但e和s本身也频繁出现因此 score 可能不如(w, ##i)在wide中等高。最终合并会优先使单个符号之间依赖更强的合并。编码/分词采用贪心最长匹配策略从左到右逐步匹配词汇表中最长的子词。Python 实现defwordpiece_train(word_freqs,num_merges):简化的 WordPiece 训练# 词内符号以 ## 标记非首字符vocab{}forword,freqinword_freqs.items():charsword[0]# 首字符不加 ##forcinword[1:]:chars ##c chars /wvocab[chars]freq merges[]for_inrange(num_merges):pairsCounter()# 统计每个符号的独立频率symbol_freqCounter()forword,freqinvocab.items():symbolsword.split()forsinsymbols:symbol_freq[s]freqforiinrange(len(symbols)-1):pairs[(symbols[i],symbols[i1])]freqifnotpairs:break# 计算 score选最大best_pairmax(pairs,keylambdap:pairs[p]/(symbol_freq[p[0]]*symbol_freq[p[1]]))merges.append(best_pair)bigram .join(best_pair)replacement.join(best_pair)new_vocab{}forword,freqinvocab.items():new_wordword.replace(bigram,replacement)new_vocab[new_word]freq vocabnew_vocabreturnmerges,vocab3. SentencePiece (Unigram 语言模型)原理SentencePiece 是 Google 推出的分词框架它不依赖预分词即不需要事先按空格切分而是把整个输入视为原始字节流直接学习子词。虽然它支持 BPE但其最具代表性的算法是Unigram 语言模型配合子词正则化。核心思想从一个很大的候选词汇表如所有可能的子串出发假设每个子词的出现相互独立句子的概率为各子词概率的乘积P ( s e n t e n c e ) ∏ x ∈ s e g m e n t a t i o n p ( x ) P(sentence) \prod_{x \in segmentation} p(x)P(sentence)∏x∈segmentation​p(x)通过期望最大化EM算法优化训练数据的边缘似然同时逐步剪掉概率较低的候选子词直到词汇表达到指定大小。训练完成后编码可以用维特比算法寻找概率最大的分割也可以根据概率采样多种分割以实现正则化。SentencePiece 把空格编码为_或其它元符号并将所有字符统一处理非常适合多语言或无需分词的场景如中文、日文。举例训练数据low lower newest widest初始候选词汇所有单个字符l, o, w, e, r, n, s, t, d, i, _空格以及高频率的 bigram如lo,ow,er,es, …。给每个候选子词均匀概率然后开始 EM 迭代E 步计算每个句子所有可能分割下各子词的期望出现次数。M 步用期望计数重新估计子词概率。每轮剪掉概率最低的 20% 子词重复直到词汇表大小符合要求。Python 实现importmath,refromcollectionsimportCounter,defaultdictdefinit_vocab(texts,max_len3):从文本中收集所有长度 max_len 的子串作为初始候选词汇vocabCounter()fortextintexts:# 将空格替换为特殊标记使其可分割texttext.replace( ,_)foriinrange(len(text)):forjinrange(1,max_len1):ifijlen(text):vocab[text[i:ij]]1# 只保留出现次数足够多的候选例如至少2次return{w:cforw,cinvocab.items()ifc2}defviterbi_segment(text,vocab_probs):用动态规划找概率最大的分割路径nlen(text)dp[0.0]*(n1)dp[0]1.0back[0]*(n1)foriinrange(n):ifdp[i]0:continueforjinrange(i1,n1):subtext[i:j]ifsubinvocab_probs:probdp[i]*vocab_probs[sub]ifprobdp[j]:dp[j]prob back[j]i# 回溯segments[]idxnwhileidx0:prevback[idx]segments.append(text[prev:idx])idxprevreturnsegments[::-1]deftrain_unigram(texts,vocab_size100,num_iters10):简化的 Unigram 训练Viterbi 近似 EM# 初始词汇表raw_vocabinit_vocab(texts,max_len4)vocablist(raw_vocab.keys())print(初始候选词汇数:,len(vocab))# 均匀概率probs{w:1/len(vocab)forwinvocab}foritinrange(num_iters):# 用维特比硬分割并统计频率countsCounter()total0fortextintexts:texttext.replace( ,_)segviterbi_segment(text,probs)forsubinseg:counts[sub]1total1# 重新估计概率probs{w:counts[w]/totalforwinvocabifcounts[w]0}# 剪枝如果词汇量超过目标去掉概率最低的whilelen(probs)vocab_size:worstmin(probs,keyprobs.get)delprobs[worst]vocablist(probs.keys())print(fIter{it1}, vocab size:{len(vocab)})returnprobs# ---- 使用演示 ----texts[low lower newest widest]probstrain_unigram(texts,vocab_size20,num_iters10)print(最终词汇:,sorted(probs.keys()))三者优缺点对比方法优点缺点BPE简单、高效完全基于频率可解释性强训练速度快编码规则确定。依赖预分词可能合并不合理的子词无法解决多分割歧义对低频对容易过拟合。WordPiece合并准则基于概率最大化子词更具语言学意义结合##前缀边界清晰在 BERT 等模型中效果好。训练开销稍大于 BPE需维护单符号频率依赖预分词贪心分词可能非最优。SentencePiece不依赖预分词将输入视为字节流天然支持所有语言Unigram 提供概率模型和子词正则化增强鲁棒性空格可见标记。训练复杂EM速度慢实现难度高Viterbi 解码开销较大超参数调节需经验。三者都在现代 NLP 中被广泛使用。BPE 适合快速实现与清晰规则WordPiece 在理解型模型如 BERT中平衡了效果与速度而 SentencePieceUnigram提供了最灵活、最一致的多语言处理方案尤其在需要避免预分词偏见的生产系统中优势明显。