NLP 中的 Tokenization 方法总结
参考博客:
关于 Tokenization,网上有翻译成"分词"的,但是我觉得不是很准确,容易引起误导。一直找不到合适的中文来恰当表达,所以下文采用原汁原味的英文表达。
在正式进入主题之前,先来看看 NLP 任务中最基础也最先需要进行的一步:tokenization。简单说,该操作的目地是将输入文本分割成一个个token,和词典配合以让机器认识文本。Tokenization 的难点在于如何获得理想的切分,使文本中所有的 token 都具有正确的表义,并且不会存在遗漏(OOV 问题,out of vocabulary)。
接下来,我们简单梳理下目前主流的 tokenization 方法,及其优缺点。
# 1. 词粒度
词粒度的切分就跟人类平时理解文本原理一样,常常用一些工具来完成,例如英文的 NLTK、SpaCy,中文的 jieba、LTP 等。举个栗子:
英文:
- live in New York ------> live / in / New York /
中文:
- 在纽约生活 -----> 在 / 纽约 / 生活
在这个切分过程中,注意考虑标点符号的 tokenization,因为每个不同标点都会导致同一单词的不同表示。
词粒度的切分能够非常好地保留完整语义信息,但是如果出现拼写错误、英文中的缩写等情况,鲁棒性一般。另一方面,词切分会产生非常巨大的词表,而且这都不能确保不会出现 out of vocabulary 问题。
空格、标点符号和基于规则的分词都是词粒度的 tokenization 的方法。然后使用 ID 表示每个单词,每个 ID 包含句子中每个单词的上下文和语义信息。
# 2. 字粒度
字粒度最早应该是2015年 Karpathy[1] 提出,简单说英文就是以字母为单位(对于大小写不敏感的任务,甚至可以先转小写再切分),中文就是以字为单位,举个栗子:
英文:
- live in New York -----> l / i / v /e / i / n / N / e / w / Y / o / r /k
中文:
- 在纽约生活 -----> 在 / 纽 / 约 / 生 / 活
可以看出,字粒度的切分很好地解决了词粒度的缺陷,鲁棒性增强、词表大大减小。但另一方面,也会带来一些麻烦:
- 毫无意义:一个字母或一个单字本质上并没有任何语义意义;
- 增加输入计算压力:减小词表的代价就是输入长度大大增加,从而输入计算变得更耗时耗力;
如果词粒度不理想,而且字粒度似乎也有自己的问题,那么还有什么替代方法呢?
Here comes subword tokenization!
# 3. Subword 粒度
我们理想中的 tokenization 需要满足:
- 它能够在不需要无限词汇表的情况下处理缺失的标记,即通过有限的已知单词列表来处理无限的潜在词汇;
- 此外,我们不希望将所有内容分解为单个字符的额外复杂性,因为字符级别可能会丢失单词级别的一些含义和语义细节。
为此,我们需要考虑如何重新利用『小』单词来创建『大』单词。Subword Tokenization 不转换最常见的单词,而是将稀有单词分解成有意义的子词单元。如果unfriendly
被标记为一个稀有词,它将被分解为un-friendly-ly
,这些单位都是有意义的单位,un
的意思是相反的,friend
是一个名词,ly
则变成副词。这里的挑战是如何进行细分,我们如何获得un-friend-ly
而不是unfr-ien-dly
。
基于 subword 的 tokenization 算法两大原则,也是核心优点:
- 原则 1:不要将常用词拆分成更小的子词。例如,“boy”不应拆分。
- 原则 2:将稀有词拆分成更小的有意义的子词。但“boys”应拆分为“boy”和“s”,词义略有不同,但词根相同。
subword 的两个原则能够继续有效提升 tokenization 效果,既满足 vocabulary 不太大,又满足语义相似性要求。它甚至可以处理它以前从未见过的 word,因为分解会产生已知的 subword,从而方便后续模型的处理。
NLP最火的网红 Transformer 和 BERT 就是 Subword 的带盐人,来看个它们做 tokenization 的栗子:
I have a new GPU ----> [’i’, ’have’, ’a’, ’new’, ’gp’, ’##u’, ’.’]
subword 粒度切分算法又有以下几种:
- BPE
- WordPiece
- ULM
- SentencePiece
- BERT 和 DistilBert 使用了 WordPiece
- XLNet 和 ALBERT 使用了 Unigram
- GPT-2 和 RoBERTa 使用了 BPE
- XLM 使用了 SentencePiece 并为中文、日文和泰文添加特定的预分词器。
# 3.1 BPE
BPE 全称 Byte Pair Encoding,字节对编码,首先在 Neural Machine Translation of Rare Words with Subword Units[2] 中提出。BPE 迭代地合并最频繁出现的字符或字符序列,具体步骤:
- 准备足够大的 corpus
- 定义好所需要的 vocabulary 大小
- 在每个 word 末尾添加后缀
</w>
- 将单词拆分为字符序列,并统计单词频率。本阶段的 subword 的粒度是字符。例如,“low”的频率为 5,那么我们将其改写为
l o w </w>:5
- 统计每一个连续字节对的出现频率,选择最高频者合并成新的 subword
- 重复第 5 步直到达到第 2 步设定的 subword 词表大小或下一个最高频的字节对出现频率为 1
所以 BPE 算法就是迭代的每一轮都寻找最频繁的配对(byte pair),然后将他们合并,直到达到 token 限制或迭代限制。
举个例子,我们输入:
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
第一轮迭代,统计连续的每两个字节出现的次数,发现 e
和 s
共现次数最大,合并成 es
,有:
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w es t </w>': 6, 'w i d es t </w>': 3}
第二轮迭代,统计连续的每两个字节出现的次数,发现 es 和 t 共现次数最大,合并成 est
,有:
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}
依次继续迭代直到达到预设的 subword 词表大小或下一个最高频的字节对出现频率为 1。
以上是 BPE 的整体流程,关于 BPE 更多细节可以参考:Byte Pair Encoding[3]。
# 3.2 Unigram LM
Unigram 语言建模首先在 Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates[4] 中提出,基于所有子词出现是独立的假设,因此子词序列由子词出现概率的乘积生成。
算法步骤如下:
- 准备足够大的 corpus
- 定义好所需要的 vocabulary 大小
- 根据训练语料设置一个合理的 seed 词表
- 给定词序列优化下一个词出现的概率
- 计算每个 subword
的损失 ,这里的 表示当 subword 被移出当前词表的时候,似然函数减小的可能性。 - 基于损失
对subword排序并保留前 X% 的 subword。为了避免 OOV,保留字符级的单元 - 重复第 3 至第 5 步直到达到第 2 步设定的 subword 词表大小或第 5 步的结果不再变化
这里有一些选择 seed 词表的方法,常用方法是结合语料中所有字符和最高频的子字符串。而且最终的词表也要包含所有独立的字符。因此这种分割可以看作是字符、subword 和 word 的概率混合。
可以看出,Unigram LM 是从一大堆基本符号开始,并逐轮减少词表的大小。
# 3.3 WordPiece
WordPiece 首先在 JAPANESE AND KOREAN VOICE SEARCH[5] 中提出,最初用于解决日语和韩语语音问题。它在许多方面类似于BPE,只是它基于可能性而不是下一个最高频率对来形成一个新的子词。算法步骤如下:
- 准备足够大的 corpus
- 定义好所需要的 vocabulary 大小
- 将单词拆分成字符序列
- 基于第 3 步数据训练语言模型
- 从所有可能的 subword 单元中选择加入语言模型后能最大程度地增加训练数据概率的单元作为新的单元
- 重复第 5 步直到达到第 2 步设定的 subword 词表大小或概率增量低于某一阈值
WordPiece 更像是 BPE 和 Unigram LM 的结合。
Summary
简单几句话总结下 Subword 的三种算法:
- BPE:只需在每次迭代中使用「出现频率」来确定最佳匹配,直到达到预定义的词汇表大小;
- Unigram:使用概率模型训练LM,移除提高整体可能性最小的token;然后迭代进行,直到达到预定义的词汇表大小;
- WordPiece:结合 BPE 与 Unigram,使用「出现频率」来确定潜在匹配,但根据合并 token 的概率做出最终决定.
# 3.4 SentencePiece
到目前为止,可以发现 subword 结合了词粒度和字粒度方法的优点,并避免了其不足。但是,仔细想会发现上述三种 subword 算法都存在一些问题:
- 都需要提前切分(pretokenization):这对于某些语言来说,可能是不合理的,因为不可以用空格来分隔单词;
- 无法逆转:原始输入和切分后序列是不可逆的。举个栗子,下面两者的结果是相等的,即空格的信息经过该操作被丢失:
Tokenize(“World.”) == Tokenize(“World .”)
- 不是 end-to-end:使用起来没有那么方便
ok,here comes SentencePiece!来看看是怎么解决上述问题的:
- SentencePiece 首先将所有输入转换为 unicode 字符。这意味着它不必担心不同的语言、字符或符号,可以以相同的方式处理所有输入;
- 空白也被当作普通符号来处理。Sentencepiece 显式地将空白作为基本 token 来处理,用一个元符号 “▁”( U+2581 )转义空白,这样就可以实现简单地 decoding
- SentencePiece 可以直接从 raw text 进行训练,并且官方称非常快!
SentencePiece 集成了两种 subword 算法,BPE 和 UniLM, WordPiece 则是谷歌内部的子词包,没对外公开。感兴趣的可以去官方开源代码库玩玩:google/sentencepiece[6]。
放个栗子:
>>> import sentencepiece as spm
>>> s = spm.SentencePieceProcessor(model_file='spm.model')
>>> for n in range(5):
... s.encode('New York', out_type=str, enable_sampling=True, alpha=0.1, nbest=-1)
...
['▁', 'N', 'e', 'w', '▁York']
['▁', 'New', '▁York']
['▁', 'New', '▁Y', 'o', 'r', 'k']
['▁', 'New', '▁York']
['▁', 'New', '▁York']
2
3
4
5
6
7
8
9
10
最后,如果想尝试 WordPiece
,大家也可以试试 HuggingFace 的 Tokenization 库[7]:
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.pre_tokenizers import Whitespace
from tokenizers.trainers import BpeTrainer
tokenizer = Tokenizer(BPE())
tokenizer.pre_tokenizer = Whitespace()
trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
tokenizer.train(trainer, ["wiki.train.raw", "wiki.valid.raw", "wiki.test.raw"])
output = tokenizer.encode("Hello, y'all! How are you 😁 ?")
print(output.tokens)
# ["Hello", ",", "y", "'", "all", "!", "How", "are", "you", "[UNK]", "?"]
2
3
4
5
6
7
8
9
10
11
12
13
14
参考资料:
[1] 2015年Karpathy: https://github.com/karpathy/char-rnn
[2] Neural Machine Translation of Rare Words with Subword Units: https://arxiv.org/abs/1508.07909
[3] Byte Pair Encoding: https://leimao.github.io/blog/Byte-Pair-Encoding/
[4] Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates: https://arxiv.org/abs/1804.10959
[5] JAPANESE AND KOREAN VOICE SEARCH: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37842.pdf
[6] google/sentencepiece: https://github.com/google/sentencepiece
[7] HuggingFace的 Tokenization 库: https://github.com/huggingface/tokenizers