NLP分词notes
BPE
贪心提取所有出现频率高的成为词。
BPE的训练流程
1.初始化:将所有单个字符作为初始词汇表的元素。
2.迭代合并:
- 统计语料中所有相邻符号对(包括字符和合并后的符号)的出现频率。
- 找到出现频率最高的符号对,将其合并为新的符号,并添加到词汇表中。
- 在语料中用新符号替换所有该符号对的出现。
3.重复迭代,直到达到预设的词汇表大小或到达合并次数。
输入:语料D,预设的合并次数N
初始化:词汇表V为语料中的所有字符
for i from 1 to N do:统计语料中所有相邻符号对的频率,得到列表P选择频率最高的符号对(s1, s2) ∈ P合并符号对s1、s2为新符号s_new将s_new添加到词汇表V用s_new替换语料D中所有的s1 s2
end for
输出:词汇表V
WordPiece
WordPiece目标是在固定大小的词表预算下,选择一组子词单元,使整个训练语料在当前词表下的分词似然最大化。它的核心思想是:初始词汇表包含所有单个字符,通过迭代合并带来最大似然增益的高频字符对,逐步扩展为更长的子词单元。
在 WordPiece 的建模假设中,一个单词的概率是其分解后子词序列概率的乘积:
P(word)=∏i=1nP(tokeni)P(\text{word}) = \prod_{i=1}^{n} P(\text{token}_i)P(word)=i=1∏nP(tokeni)
因此,WordPiece 的训练目标是最大化整个训练语料的对数似然:
max∑wordslogP(word)\max \sum_{\text{words}} \log P(\text{word})maxwords∑logP(word)
WordPiece 的训练流程
1.初始化词汇表:从所有单个字符开始,非开头的token会加上前缀/后缀(例如 ##ing
、##ed
)。
2.计算相邻字符对频率:统计整个训练语料中所有相邻字符对的出现频率。
3. 迭代合并:在每次迭代中,计算增益Gain=loglikelihood(new)−loglikelihood(old)Gain=loglikelihood(new)-loglikelihood(old)Gain=loglikelihood(new)−loglikelihood(old),选择能够使语言模型对数似然增益最大的字符对进行合并,并更新词表。重复此过程直到词汇表达到预设大小。
4. 得到最终词汇表
输入:语料D,预设的词汇表大小N
初始化:词汇表V为所有字符
while |V| < N do:初始化增益字典G = {}for 语料D中的每个可能的子词s(由现有词汇表中的符号组合而成):计算将s添加到词汇表V后,语料D的似然增益ΔL将(s, ΔL)加入G从G中选择ΔL最大的子词s_max将s_max添加到词汇表V更新模型参数(如概率)
end while
输出:词汇表V
eg.给定初始词表 ["a", "b", "c", "w"]
和词汇 v = ["abc", "abw"]
,各字符的概率分布如下:
字符概率分布:p(a)=p(b)=13p(a) = p(b) = \frac{1}{3}p(a)=p(b)=31,p(c)=p(w)=16p(c) = p(w) = \frac{1}{6}p(c)=p(w)=61
假如合并ab,合并后词表更新为 ["ab", "c", "w"]
,重新归一化概率 p(ab)=12,p(c)=p(w)=14p(ab) = \frac{1}{2},p(c) = p(w) = \frac{1}{4}p(ab)=21,p(c)=p(w)=41
词汇序列概率计算:p(v)=p(ab)×p(c)+p(ab)×p(w)=14p(v) =p(ab) \times p(c) + p(ab) \times p(w) = \frac{1}{4}p(v)=p(ab)×p(c)+p(ab)×p(w)=41
原始未合并概率p(old v)=p(a)×p(b)×p(c)+p(a)×p(b)×p(w)=127p(old\ v) =p(a) \times p(b) \times p(c) + p(a) \times p(b) \times p(w) = \frac{1}{27}p(old v)=p(a)×p(b)×p(c)+p(a)×p(b)×p(w)=271
Gain=p(v)−p(old v)=14−127=23108 Gain = p(v) - p(old\ v) = \frac{1}{4} - \frac{1}{27} = \frac{23}{108}Gain=p(v)−p(old v)=41−271=10823
GPT写的一个demo
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
# #BPE
# from tokenizers import models, trainers
# tokenizer = Tokenizer(models.BPE())
# trainer = trainers.BpeTrainer(vocab_size=30, special_tokens=["[UNK]"])# 1️⃣ 定义模型类型:WordPiece
tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))# 2️⃣ 定义预分词规则
tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()# 3️⃣ 定义训练器
trainer = trainers.WordPieceTrainer(vocab_size=100,special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]
)# 4️⃣ 训练文本
corpus = ["the quick brown fox jumps over the lazy dog","the quick brown dog jumps over the lazy fox","i love natural language processing","wordpiece helps with unknown words"
]# 5️⃣ 训练
tokenizer.train_from_iterator(corpus, trainer)# 6️⃣ 测试编码
output = tokenizer.encode("the quick brown fox")
print("Tokens:", output.tokens)
print("IDs:", output.ids)# 7️⃣ 保存分词器
tokenizer.save("wordpiece-tokenizer.json")
Q:为什么 vocab_size 越大,分词后句子里的 token 数会更少?
A:因为 vocab_size 越大,分词器词表能收录更多子词单元,包括更长的子词或整个单词。这让分词器在处理文本时可以用更大的块来覆盖文本,减少切分次数,所以最后得到的 token 数会更少。
Q:vocab_size 和合并次数是一个概念吗?
A:不是。vocab_size 是词表大小的上限,但并不保证最后一定会有 vocab_size 那么多的子词单元。分词器会尽量把词表填满到这个上限,但如果语料规模很小、可合并的模式有限,最终学到的词表可能会小于设定的 vocab_size。