本文将介绍以下内容:
- 1. Subword与传统tokenization技术的对比
- 2. WordPiece
- 3. Byte Pair Encoding (BPE)
- 4. Byte-level BPE(BBPE)
- 5. SentencePiece 以及各Subword算法代码实现
一、Subword与传统tokenization技术的对比
1. 传统tokenization技术
传统tokenization技术一般有两种方法:word(词)粒度、char(字符)粒度
1.1 word(词)粒度
传统构造词表的方法,是先对各个句子进行分词,然后再统计并选出频数最高的前N个词组成词表。通常训练集中包含了大量的词汇,以英语为例,总的单词数量在17万到100万左右。对于英文来说,word级别分词实现很简单,因为本身就有分隔符号。在中文里面word粒度,需要使用一些分词工具比如jieba、ltp等。该方案优劣势如下:
-
优点:
- 语义明确: 以词为粒度进行分词可以更好地保留每个词的语义,使得文本在后续处理中能够更准确地表达含义。
- 上下文理解: 以词为粒度进行分词有助于保留词语之间的关联性和上下文信息,从而在语义分析和理解时能够更好地捕捉句子的意图。
-
缺点:
- OOV(Out-of-Vocabulary): 分词模型只能够使用词表中的词进行处理,无法处理词表之外的词汇。
- 长尾效应和稀有词问题: 词表可能变的巨大,包含很多不常见的词汇,增加存储和训练成本,稀有词的训练数据有限,无法得到充分训练,进而模型不能充分理解这些词的语义。
- 形态关系和词缀关系: 无法捕捉同一词的不同形态,也无法学习词缀在不同词汇之间的共通性,限制了模型的语言理解能力。比如look和looks在词表中将会是两个词。一方面增加了训练冗余,另一方面也造成了大词汇量问题。
1.2 char(字符)粒度
以字符为单位进行分词,就是把文本拆分成一个个单独的字符作为最小的基本单元,这种对多种语言都比较适用,比如英文、中文、印尼语等。英文就是26个字母以及其他的一些符号,中文常见、次常用字大约共有 6000多个。该方案优劣势如下:
- 优点:
- 通用性: 字符粒度的分词方法使用与不同的语言,不需要设计不同的规则和工具。
- 避免OOV问题: 这种粒度的分词能够处理任何字符,无需维护词表,因此可以很好地处理一些新的词汇、专有名词等。
- 缺点:
- 语义信息不明确: 这种粒度的分词无法直接表达词的语义,可能在一些语义分析任务中效果比较差。
- 效率极低: 由于文本被拆分成字符,处理粒度较小,增加了后续处理的计算成本和时间。
2. Subword(子词)粒度
目前有三种主流的Subword算法,它们分别是:Byte Pair Encoding (BPE), WordPiece和Unigram Language Model。
针对上述问题,Subword(子词)模型方法横空出世。它的划分粒度介于词与字符之间,比如可以将”looking”划分为”look”和”ing”两个子词,而划分出来的"look",”ing”又能够用来构造其它词,如"look"和"ed"子词可组成单词"looked",因而Subword方法能够大大降低词典的大小,同时对相近词能更好地处理。
接下来将分别详细介绍:WordPiece、BPE、BBPE、ULM。
二、WordPiece
请见笔者之前文章:NLP Subword 之 WordPiece 算法原理
三、Byte Pair Encoding (BPE)
请见笔者之前文章:NLP Subword 之 BPE(Byte Pair Encoding) 算法原理
四、Byte-level BPE(BBPE)
请见笔者之前文章:NLP Subword 之 BBPE(Byte-level BPE) 算法原理
五、SentencePiece 以及各Subword算法代码实现
1. 什么是 SentencePiece?
在自然语言处理(NLP)中,分词(tokenization) 是一个极为关键的步骤。传统的分词方法往往依赖语言特定的规则(比如英文依靠空格,中文依靠分词工具),而在多语言任务或需要统一处理的场景下,这种方式就显得繁琐且不够通用。
SentencePiece 是 Google 开源的一个 语言无关(language-agnostic)子词建模工具。它的“工具属性”主要体现在:
- 端到端训练:直接基于原始语料,无需预分词或去掉空格,输出模型文件(
.model
)和词表(.vocab
)。 - 多算法统一接口:同一套命令/接口支持 Unigram(ULM)、BPE、Char、Word 等模型类型。
- 推理一致性:同一模型可用于编码(encode)与解码(decode),避免预处理不一致的问题。
- OOV 友好:支持
--byte_fallback
,可以在词表未登录时回退为字节级 token,实现 Byte-level BPE(BBPE) 的效果。
需要特别说明的是:
- SentencePiece 并没有直接提供 WordPiece 类型;
- 在实践中,通常使用 Unigram 模型 来 近似/替代 WordPiece。
2. 使用 Python SentencePiece 库分别实现 ULM、BPE、BBPE
下面通过 3 个独立的 Demo,完整展示 训练 + 推理 全流程。每个 Demo 都会:
- 生成一份 demo 语料;
- 调用
sentencepiece
训练模型; - 加载模型进行推理(encode/decode);
2.1 Unigram Language Model(ULM)
Unigram 是 SentencePiece 默认与推荐的算法之一,常作为 WordPiece 的替代实现。
# demo_ulm.py
import os
import sentencepiece as spm# 0) 准备语料
workdir = "./sp_ulm_work"
os.makedirs(workdir, exist_ok=True)
corpus = os.path.join(workdir, "corpus.txt")lines = ["我爱自然语言处理。","自然语言处理是人工智能的重要分支。","Hello world! This is a tiny corpus for subword training.","Emoji 测试:🙂🚀🌍,以及混合文本 NLP。",
]
with open(corpus, "w", encoding="utf-8") as f:f.write("\n".join(lines))# 1) 训练 Unigram 模型
model_prefix = os.path.join(workdir, "uni")
spm.SentencePieceTrainer.Train(input=corpus,model_prefix=model_prefix,vocab_size=800, # demo 用小词表model_type="unigram", # ★ 关键:Unigramcharacter_coverage=1.0
)# 2) 推理
sp = spm.SentencePieceProcessor(model_file=f"{model_prefix}.model")
test_text = "我爱NLP🚀!"
pieces = sp.encode(test_text, out_type=str)
ids = sp.encode(test_text, out_type=int)
back = sp.decode(ids)print("INPUT :", test_text)
print("PIECES:", pieces)
print("IDS :", ids)
print("DECODE:", back)
2.2 Byte Pair Encoding(BPE)
BPE 是另一种常见的子词算法,原理是迭代合并语料中频率最高的相邻 token 对。
# demo_bpe.py
import os
import sentencepiece as spm# 0) 准备语料
workdir = "./sp_bpe_work"
os.makedirs(workdir, exist_ok=True)
corpus = os.path.join(workdir, "corpus.txt")lines = ["BPE merges rules are learned from frequent pairs.","Hello world! Byte Pair Encoding is simple and effective.","中文测试:分词、子词建模。",
]
with open(corpus, "w", encoding="utf-8") as f:f.write("\n".join(lines))# 1) 训练 BPE 模型
model_prefix = os.path.join(workdir, "bpe")
spm.SentencePieceTrainer.Train(input=corpus,model_prefix=model_prefix,vocab_size=800,model_type="bpe", # ★ 关键:BPEcharacter_coverage=1.0
)# 2) 推理
sp = spm.SentencePieceProcessor(model_file=f"{model_prefix}.model")
test_text = "BPE 的分词效果如何?😊"
pieces = sp.encode(test_text, out_type=str)
ids = sp.encode(test_text, out_type=int)
back = sp.decode(ids)print("INPUT :", test_text)
print("PIECES:", pieces)
print("IDS :", ids)
print("DECODE:", back)
2.3 Byte-level BPE(BBPE)
BBPE 的核心思想是 在 BPE 上增加字节级回退(byte fallback),保证任何输入都能被编码。
# demo_bbpe.py
import os
import sentencepiece as spm# 0) 准备语料
workdir = "./sp_bbpe_work"
os.makedirs(workdir, exist_ok=True)
corpus = os.path.join(workdir, "corpus.txt")lines = ["Byte-level BPE ensures any input is representable.","Emoji 测试:🙂🚀🌍。","混合语言测试:NLP 和 数据。",
]
with open(corpus, "w", encoding="utf-8") as f:f.write("\n".join(lines))# 1) 训练 BBPE 模型(本质是 BPE + byte_fallback)
model_prefix = os.path.join(workdir, "bbpe")
spm.SentencePieceTrainer.Train(input=corpus,model_prefix=model_prefix,vocab_size=800,model_type="bpe", # 仍然是 BPEcharacter_coverage=1.0,byte_fallback=True # ★ 关键:开启字节回退
)# 2) 推理
sp = spm.SentencePieceProcessor(model_file=f"{model_prefix}.model")
test_text = "未登录字符𝔘𝔫𝔦𝔠𝔬𝔡𝔢😊"
pieces = sp.encode(test_text, out_type=str)
ids = sp.encode(test_text, out_type=int)
back = sp.decode(ids)print("INPUT :", test_text)
print("PIECES:", pieces)
print("IDS :", ids)
print("DECODE:", back)
3. 小结
- SentencePiece 是一个通用的子词建模工具,它屏蔽了语言差异,让我们直接在原始语料上训练子词模型。
- 它支持 Unigram(ULM)、BPE、Char、Word 等模型类型,但 没有直接的 WordPiece;通常用 Unigram 来近似/替代 WordPiece。
- 通过
byte_fallback
参数,SentencePiece 可以在 BPE 模型上实现 BBPE 的能力,从而覆盖任意输入字符(如 emoji、稀有符号)。