SentencePiece:语言无关的分词器训练工具,Google 系列模型的标配

我第一次用SentencePiece训练出一个分词器,拿它编码一句中文,结果输出是:['▁我爱', '▁北京']。盯着那个奇怪的▁看了半天——这到底是空格还是什么特殊字符?后来我才明白,正是这个小小的▁,让SentencePiece能和任意语言打交道,也让它成了Google系列模型(T5、Gemma、PaLM)的标配。

AI technology illustration

大模型的第一步,卡在“拆词”上

在把文本喂给Transformer之前,你必须先决定一个“词”的边界在哪里。英文天然有空格,中文却没有。老一辈分词器(比如BPE的原始实现)要求你先用空格预分词,然后再做词表合并。这就形成了一个死循环:对中文分词需要分词器,但分词器的训练又需要你已经分好词的语料

当然你可以用字级模型,但单个汉字的信息密度低,模型需要更长的序列才能学到同样的语义。理想情况是让模型自己发现哪些字符序列应该作为一个词——这正是SentencePiece的初衷。

SentencePiece是Google和日本东京大学合作开源的一个工具库,它实现了无监督的subword分词,核心思路是:不依赖任何语言上的先验知识,直接从原始文本中学习tokenization规则

一个反直觉的发明:把空格当普通字符

SentencePiece把空格替换成“▁”(Unicode字符U+2581),然后把替换后的整句话喂给子词算法。这样空格就不再是分隔符,而是一个可以出现在任何位置的普通字符。

英文:Hello world   ->   ▁Hello ▁world
中文:我爱北京       ->   ▁我爱 ▁北京

注意,中文的“我爱北京”没有空格,但SentencePiece在句首加了一个▁。训练完模型后,词表里既可能有“我爱”,也可能有“▁北京”。那为什么要把空格特殊标记?为了可逆性——当你把token序列转换回文本时,得知道哪里该有空格。

传统BPE在分词前已经丢了空格,所以无法重建原始文本。SentencePiece的▁机制保证了分词后的token能无损还原,这对翻译、生成这类需要输出原始文本的任务非常关键。

与BERT时代的WordPiece相比,这种设计更是降维打击:

对比项 WordPiece(BERT) SentencePiece
是否需要预分词 是,先按空格拆 否,直接吃原始文本
空格处理 预分词时丢失 变成▁字符保留
中文支持 需要额外分词器 开箱即用
可逆性

BPE和Unigram,两条路径走到同一个目标

SentencePiece最常用的两种算法是BPE和Unigram。它们的思路截然不同。

先看BPE。它的训练过程像是个贪心合并游戏:

  1. 把每一个Unicode字符当作初始token(此时空格已变成▁)。
  2. 统计语料中所有相邻token对的出现频率。
  3. 把频率最高的那一对合并成一个新token,加入词表。
  4. 重复第2~3步,直到词表大小达到你设定的目标值。

合并的次数越多,单个token越长,词表越大。这个过程简单、可解释,但它很容易被高频字符对带偏,比如英语中的“th”、“ing”会早早合并,导致一些低频语言的特征不够突出。

Unigram走的是完全相反的路线:它不是从原子字符开始合并,而是从一个很大的候选词表开始,然后根据一个语言模型的目标不断删掉那些“不重要”的token。具体过程依赖EM算法,最终得到一个词表,使得整个语料在这个词表下的分词结果概率最大。

从效果看,Unigram可以生成多个候选切分,并在解码时用维特比算法找到最优路径。这让它在处理未登录词和跨语言数据时更加灵活。

对比维度 BPE Unigram
训练方向 自底向上(合并) 自顶向下(删除)
贪心程度 完全贪心 基于全局概率优化
能否采样 可以
典型使用者 GPT-2、LLaMA T5、Gemma

你可能会问,GPT-2和LLaMA也是BPE,为什么不用SentencePiece?它们用的是byte-level BPE,把输入从Unicode字符换成字节。这样做的好处是任何字符都不会变成UNK,但代价是词表里可能混入不完整的UTF-8片段。而T5和Gemma选择的是SentencePiece的Unigram,它们更看中基于字符+▁的“语言无关”的干净。

三行命令,训练一个多语言分词器

SentencePiece的使用简单得不像一个学术工具。你只要准备一个文本文件,然后运行:

spm_train 
  --input=all_languages.txt 
  --model_prefix=mymodel 
  --vocab_size=32000 
  --model_type=unigram

训练完成后,你会得到mymodel.modelmymodel.vocab。加载并编码文本:

import sentencepiece as spm
sp = spm.SentencePieceProcessor(model_file='mymodel.model')
sp.encode('Hello world', out_type='str')
# ['▁Hello', '▁world']

有意思的是,即使输入的是带空格的英文,空格也被转成了▁。很多初学的人会以为这是bug,其实是特性。

我最早以为SentencePiece需要像旧工具那样传入已经预处理好的文本,结果发现它直接把我的乱糟糟的数据扔进去也能训练。后来才意识到,它对输入格式的容忍度正是它作为通用工具的底气。

从T5到Gemma,Google到底看上了它什么?

Google几乎所有主流大模型都押在SentencePiece上。T5论文明确提到,他们在C4语料上训练了一个32k词表的Unigram分词器。Gemma模型卡也写着“使用SentencePiece”,而且词表大小达到了256k——这么大规模的词表,还是SentencePiece在处理。

统一选择SentencePiece有两个核心原因:

  • 语言无关:不需要为中文、日文、阿拉伯文各自准备一套分词脚本,一个工具就能覆盖全世界上百种语言。
  • 可逆性:在需要将模型输出映射回原始文本时(比如翻译或生成),无歧义的重建是基本要求。

反观Mistral和Llama 2,它们用的是byte-level BPE,在多语言上靠字节级曲线救国。SentencePiece则是直接从文本层面学习,对中文这种“无空格语言”更自然。

它最大的坑,是“Character”不等于“Byte”

SentencePiece并不是没有弱点。它的一个根本限制是:处理的是Unicode字符序列,而不是字节序列

假如你的语料里有Unicode没有分配的私有区字符,SentencePiece会直接把它标记为UNK。而byte-level BPE永远不会UNK——因为字节有且只有256种。这也是为什么很多追求零损失的模型会放弃SentencePiece。

另一个坑是归一化。SentencePiece支持NFKC、NFC等选项。如果你训练时用了NFKC,但推理时忘了,同一个词会被拆成完全不同的token。我自己就踩过这个坑:线上模型突然大量出现UNK,排查了半天,最后发现是训练时开了NFC,推理时用默认值。

还有一个实际选择问题:vocab_size设多少?太小会丢失长词信息,太大会让模型学到很多无意义碎片。T5选了32k,Gemma选了256k,跨度很大。实际使用中,8k~32k是一个常用区间,具体要看语料的多样性和模型容量。

你可能会问的3个问题

Q1:SentencePiece能不能做纯字符分词?

能。训练时指定--model_type=char,它就会每个字符一个token,相当于字符级模型。

Q2:为什么我不能直接用encode()处理大批量数据?

encode()默认会做更多处理,速度较慢。如果要快速批量处理,用encode_as_pieces(),或调用内置的C++接口。

Q3:SentencePiece和HuggingFace的tokenizer能结合使用吗?

能。HuggingFace的PreTrainedTokenizerFast可以直接载入SentencePiece模型文件,让你继续使用transformers生态。

最后的判断

回到标题的问句:SentencePiece为什么能成为Google系模型的标配?因为它把“分词”这件脏活累活变得标准化、可复现。你不需要为每个语言写正则规则,也不需要担心训练和推理时在tokenization上的不一致。

但它也有清晰的边界:如果你极度关注字节级的多语言覆盖,或者语料包含大量不可预测的Unicode,SentencePiece可能不是最稳的方案。选择分词器,本质上是在“语言可移植性”和“字节完整性”之间做取舍。理解这个取舍,比背诵任何模型的tokenizer名字都重要。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/740.html

(0)
上一篇 45分钟前
下一篇 32分钟前

相关推荐