Tokenization(分词)在NLP任务中是最基本的一步,把文本内容处理为最小基本单元即token用于后续的处理,如何把文本处理成token呢?有一系列的方法,其基本思想是构建一个词表,通过词表一一映射进行分词,但如何构建合适的词表呢?以下以分词粒度为角度进行介绍。
1. word(词)粒度
在英文语系中,word级别分词实现很简单,因为有天然的分隔符。在中文中word级别分词实现需要依靠分词工具比如jieba,以下是中英文的例子:
中文句子:我喜欢看电影和读书。
分词结果:我 | 喜欢 | 看 | 电影 | 和 | 读书。
英文句子:I enjoy watching movies and reading books.
分词结果:I | enjoy | watching | movies | and | reading | books.
优点:
- 语义明确:以词为单位进行分词可以更好地保留每个词的语义,使得文本在后续处理中能够更准确地表达含义。
- 上下文理解:以词为粒度进行分词有助于保留词语之间的关联性和上下文信息,从而在语义分析和理解时能够更好地捕捉句子的意图。
缺点:
- 长尾效应和稀有词问题:词表可能变得巨大,包含很多不常见的词汇,增加存储和训练成本,稀有词的训练数据有限,难以获得准确的表示。
- OOV(Out-of-Vocabulary):词粒度分词模型只能使用词表中的词来进行处理,无法处理词表之外的词汇,这就是所谓的OOV问题。
- 形态关系和词缀关系:无法捕捉同一词的不同形态,也无法有效学习词缀在不同词汇之间的共通性,限制了模型的语言理解能力,比如love和loves在word粒度的词表中将会是两个词。
2. char(字符)粒度
以字符为单位进行分词,即将文本拆分成一个个单独的字符作为最小基本单元,这种字符粒度的分词方法适用于多种语言,无论是英文、中文还是其他不同语言,都能够一致地使用字符粒度进行处理,因为英文就26个字母以及其他的一些符号,中文常见字就6000个左右。
中文句子:我喜欢看电影和读书。
分词结果:我 | 喜 | 欢 | 看 | 电 | 影 | 和 | 读 | 书 | 。
英文句子:I enjoy watching movies and reading books.
分词结果:I | | e | n | j | o | y | | w | a | t | c | h | i | n | g | |...
优点:
- 统一处理方式:字符粒度分词方法适用于不同语言,无需针对每种语言设计不同的分词规则或工具,具有通用性。
- 解决OOV问题:由于字符粒度分词可以处理任何字符,无需维护词表,因此可以很好地处理一些新创词汇、专有名词等问题。
缺点:
- 语义信息不明确:字符粒度分词无法直接表达词的语义,可能导致在一些语义分析任务中效果较差。
- 处理效率低:由于文本被拆分为字符,处理的粒度较小,增加后续处理的计算成本和时间。
3.subword(子词)粒度
在很多情况下,既不希望将文本切分为单独的词(太大),也不想将其切分为单个字符(太小),而是希望得到介于词和字符之间的子词单元。这就引入了subword(子词)粒度的分词方法。
在BERT时代,WordPiece分词方法被广泛应用,比如BERT、DistilBERT等。WordPiece分词方法是subword(子词)粒度的一种方法。
3.1 WordPiece
WordPiece核心思想是将单词拆分为多个前缀符号(比如BERT中的##)最小单元,再通过子词合并规则将最小单元进行合并为子词级别。例如对于单词“word”,拆分为 :
w ##o ##r ##d
然后通过合并规则进行合并,从而循环迭代构建出一个词表,以下是核心步骤:
(1)计算初始词表:通过训练语料获得,或者最初的英文中26个字母加上各种符号以及常见中文字符这些作为初始词表。
(2)计算合并分数:对训练语料拆分的多个子词单元通过合并规则计算合并分数。
(3)合并分数最高的子词对:选择分数最高的子词对,将它们合并成一个新的子词单元,并更新词表。
(4)重复合并步骤:不断重复步骤(2)和步骤(3),直到达到预定的词表大小、合并次数,或者直到不再产生有意义的合并(即进一步合并不会显著提高词表的效益)。
(5)分词:使用最终得到的词汇表对文本进行分词。
例如我们有以下的训练语料中的样例,括号中第2位为在训练语料库中出现的频率:
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
我们对其进行拆分为带前缀的形式:
("h" "##u" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12),
("b" "##u" "##n", 4), ("h" "##u" "##g" "##s", 5)
所以这些样例的初始词表将会是:
["b", "h", "p", "##g", "##n", "##s", "##u"]
接下来重要的一步进行计算合并分数,也称作互信息(信息论中衡量两个变量之间的关联程度)。
分数 = 合并pair候选的频率 / (第一个元素的频率 * 第二个元素的频率)
对于上述样例中这个pair("##u", "##g")出现的频率是最高的20次,但是"##u"出现的频率是36次,"##g"出现的频率是20次,所以这个pair的分数是 20/(36*20)=1/36,同理计算这个pair("##g", "##s")的分数为 5/(20*5)=1/20,所以最先合并的pair是("##g", "##s")->("##gs")。此时词表和拆分后的频率将会变成以下:
Vocabulary:["b", "h", "p", "##g", "##n", "##s", "##u", "##gs"]
Corpus:("h" "##u" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12),
("b" "##u" "##n", 4), ("h" "##u" "##gs", 5)
重复上述的操作,直到达到你想要的词表的大小。
Vocabulary:["b", "h", "p", "##g", "##n", "##s", "##u", "##gs", "hu", "hug"]
Corpus:("hug", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4),
("hu" "##gs", 5)
一般来说最后会在词表中加上一些特殊词汇、英文26个字母,各种符号以及常见中文字符。不过如果训练语料比较大以及词表比较大那这些应该也是包括了,只需要添加特殊词汇:
all_vocab = vocab + ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"] + other_alphabet
在大语言模型时代,最常用的分词方法是Byte-Pair Encoding(BPE)和Byte-level BPE(BBPE)。Byte-Pair Encoding(BPE)最初是一种文本压缩算法在15年被引入到NLP用于分词,后续好多模型(例如GPT,RoBERTa等)都采用了这种分词方法。Byte-level BPE(BBPE)是于19年在BPE的基础上提出以Byte-level(字节)为粒度的分词方法。目前GPT2、BLOOM、Llama、Falcon等采用的是该分词方法。
3.2 Byte-Pair Encoding(BPE)
Byte-Pair Encoding(BPE)核心思想是逐步合并出现频率最高的子词对而不是像Wordpiece计算合并分数,从而构建出一个词汇表,以下是核心步骤:
(1)计算初始词表:通过训练语料获得或者最初的英文中26个英文字母加上各种符号以及常见中文字符,这些作为初始词表。
(2)构建频率统计:统计所有子词单元对(两个连续的子词)在文本中出现的频率。
(3)合并频率最高的子词对:选择出现频率最高的子词对,将它们合并成一个新的子词单元,并更新词汇表。
(4)重复合并步骤:不断重复步骤(2)和步骤(3),直到达到预定的词汇表大小、合并次数,或者直到不再有有意义的合并(即进一步合并不会显著提高词汇表的效益)。
(5)分词:使用最终得到的词汇表对文本进行分词。
BPE理论上还是会出现OOV的,当词汇表的大小受限时,一些较少频繁出现的子词和没有在训练过程中见过的子词,就会无法进入词汇表出现OOV,而Byte-level BPE(BBPE)理论上是不会出现这个情况的。
3.3 Byte-level BPE(BBPE)
Unicode:Unicode是一种字符集,旨在涵盖地球上几乎所有的书写系统和字符。它为每个字符分配了一个唯一的代码点用于标识字符。Unicode不关注字符在计算机内部的具体表示方式,而只是提供了一种字符到代码点的映射。Unicode的出现解决了字符集的碎片化问题,使得不同的语言和字符能够在一个共同的标准下共存。然而,Unicode并没有规定如何在计算机内存中存储和传输这些字符。
UTF-8:UTF-8是一种变长的字符编码方案,它将Unicode中的代码点转换为字节序列。UTF-8的一个重要特点是它是向后兼容ASCII的,这意味着标准的ASCII字符在UTF-8中使用相同的字节表示,从而确保现有的ASCII文本可以无缝地与UTF-8共存。在UTF-8编码中,字符的表示长度可以是1到4个字节,不同范围的Unicode代码点使用不同长度的字节序列表示,这样可以高效地表示整个Unicode字符集。UTF-8的编码规则是:
· 单字节字符(ASCII范围内的字符)使用一个字节表示,保持与ASCII编码的兼容性。
· 带有更高代码点的字符使用多个字节表示。UTF-8使用特定的字节序列来指示一个字符所需的字节数,以及字符的实际数据。
例如,英文字母”A“的Unicode代码点是U+0041,在UTF-8中表示为0x41(与ASCII编码相同);而中文汉字”你“的Unicode代码点是U+4F60,在UTF-8中表示为0xE4 0xBD 0xA0三个字节的序列。所以简单来说:
Unicode是字符集,为每个字符分配唯一的代码点。
UTF-8是一种基于Unicode的字符编码方式,用于在计算机中存储和传输字符。
计算机存储和处理数据时,字节是最小的单位。一个字节包含8个(Bit)二进制位,每个位可以是0或1,每位的不同排列组合可以表示不同的数据,所以一个字节能表示的范围是256个。
综上,Byte-level BPE(BBPE)和Byte-Pair Encoding(BPE)区别为:BPE的的最小字符是字符级别,而BBPE是字节级别的。通过UTF-8的编码方式这一个字节的256的范围,理论上可以表示这个世界上所有的字符。所以实现的步骤和BPE就是实现的粒度不一样,其他都是一样的。
(1)计算初始词表:通过训练语料获得或者最初的英文中26个英文字母加上各种符号以及常见中文字符,这些作为初始词表。
(2)构建频率统计:统计所有子词单元对(两个连续的子词)在文本中出现的概率。
(3)合并频率最高的子词对:选择出现频率最高的子词对,将它们合并成一个新的子词单元,并更新词汇表。
(4)重复合并步骤:不断重复步骤(2)和步骤(3),直到达到预定的词汇表大小、合并次数,或者直到不再有有意义的合并(即进一步合并不会显著提高词汇表的效益)。
(5)分词:使用最终得到的词汇表对文本进行分词。