NLP中常见的分词算法(BPE、WordPiece、Unigram、SentencePiece)

文章目录

  • 一、基本概念
  • 二、传统分词方法
    • 2.1 古典分词方法
    • 2.2 拆分为单个字符
  • 三、基于子词的分词方法(Subword Tokenization)
    • 3.1 主要思想
    • 3.2 主流的 Subword 算法
    • 3.3 Subword 与 传统分词方法的比较
  • 四、Byte Pair Encoding (BPE)
    • 4.1 主要思想
    • 4.2 算法过程
    • 4.3 完整例子
    • 4.4 BPE 的优缺点
    • 4.5 BPE 的适用范围
    • 4.6 BPE 的实现
    • 4.7 编码与解码
    • 4.8 调包使用 BPE
  • 五、BBPE (Byte-level BPE)
    • 5.1 基础知识
    • 5.2 基本思想
    • 5.3 代码实现
  • 六、WordPiece
    • 6.1 基本思想
    • 6.2 优势与劣势
  • 七、Unigram Language Model (ULM)
    • 7.1 基本思想
    • 7.2 优势与劣势
  • 八、SentencePiece
    • 8.1 基本思想
    • 8.2 使用说明
  • 九、LLM中的分词器
    • 9.1 BERT的分词器
    • 9.2 LLaMA的分词器
    • 9.3 GLM的分词器
  • 参考资料

一、基本概念

机器无法直接理解自然语言的文本,我们需要进行文本预处理 ,而最重要的一步就是分词(Tokenize) 。

其中,执行分词的算法模型称为 分词器(Tokenizer) ,划分好的一个个词称为 Token(词元) ,这个过程称为 Tokenization(分词)

分词的目的是将输入文本分成一个个token(词元),保证各个token(词元)拥有相对完整和独立的语义,以供后续任务(比如学习embedding或者作为高级模型的输入)使用。

由于一篇文本的词往往太多了,为了方便算法模型训练,我们会选取出频率 (也可能是其它的权重)最高的若干个词组成一个词表(Vocabulary)

二、传统分词方法

2.1 古典分词方法

分词,顾名思义,就是把一句话分词一个个词,这还不简单?直接把词与词直接加一个空格不就行了?那如果真这么简单我们也不用讨论了,还有什么办法呢,再想一想?或许还能按标点符号分词 ,或者按语法规则分词 。

在这里插入图片描述

上面提到的这些方法,统称为古典分词方法 ,区别不是很大。

古典分词方法的缺点
可见,一个句子,使用不同的规则,将有许多种不同的分词结果。古典分词方法的缺点非常明显:

  • 对于 未在词表中出现的词(Out Of Vocabulary, OOV ),模型将无法处理(未知符号标记为 [UNK])。
  • 词表中的低频词/稀疏词在模型训无法得到训练(因为词表大小有限,太大的话会影响效率)。
  • 很多语言难以用空格进行分词,例如英语单词的多形态,look 衍生出的 looks, looking, looked,其实都是一个意思,但是在词表中却被当作不同的词处理,模型也无法通过 old, older, oldest 之间的关系学到 smart, smarter, smartest 之间的关系。这一方面增加了训练冗余,另一方面也造成了大词汇量问题。

2.2 拆分为单个字符

这种方法称为 Character embedding,是一种更为极端的分词方法,直接把一个词分成一个一个的字母和特殊符号。虽然能解决 OOV 问题,也避免了大词汇量问题,但缺点也太明显了,粒度太细,训练花费的成本太高。而且单个字符无法承载丰富的语义。
在这里插入图片描述

三、基于子词的分词方法(Subword Tokenization)

如何结合word和char粒度各自的优势呢?基于子词的分词方法(Subword Tokenization) 应运而生,顾名思义,粒度介于word和char之间,基本思想为常用词应该保持原状,生僻词应该拆分成子词以共享token压缩空间,所以可以较好的平衡词表大小与语义表达能力,比如OOV问题可以通过subword的组合来解决。

总结一下,文本的分词粒度:

  • word:
    优点:词的边界和含义得到保留;
    缺点:1)词表大,稀有词学不好;2)OOV;3)无法处理单词形态关系和词缀关系;
  • char:
    优点:词表极小,比如26个英文字母几乎可以组合出所有词,5000多个中文常用字基本也能组合出足够的词汇;
    缺点:1)无法承载丰富的语义;2)序列长度大幅增长;
  • subword:可以较好的平衡词表大小与语义表达能力;

3.1 主要思想

基于子词的分词方法的目的是:通过一个有限的词表 来解决所有单词的分词问题,同时尽可能将结果中 token 的数目降到最低。

例如,可以用更小的词片段来组成更大的词,例如:

"unfortunately" = "un" + "for" + "tun" + "ate" + "ly"

可以看到,有点类似英语中的词根词缀拼词法,其中的这些小片段又可以用来构造其他词。可见这样做,既可以降低词表的大小,同时对相近词也能更好地处理。

3.2 主流的 Subword 算法

目前几种主流的 Subword 算法,包括:

  • BPE:即字节对编码。其核心思想是从字母开始,不断找词频最高、且连续的两个token合并,直到达到目标词数。
  • BBPE:BBPE核心思想将BPE的从字符级别扩展到子节(Byte)级别。BPE的一个问题是如果遇到了unicode编码,基本字符集可能会很大。BBPE就是以一个字节为一种“字符”,不管实际字符集用了几个字节来表示一个字符。这样的话,基础字符集的大小就锁定在了256(2^8)。采用BBPE的好处是可以跨语言共用词表,显著压缩词表的大小。而坏处就是,对于类似中文这样的语言,一段文字的序列长度会显著增长。因此,BBPE based模型可能比BPE based模型表现的更好。然而,BBPE sequence比起BPE来说略长,这也导致了更长的训练/推理时间。BBPE其实与BPE在实现上并无大的不同,只不过基础词表使用256的字节集。
  • WordPiece:WordPiece算法可以看作是BPE的变种。不同的是,WordPiece基于概率生成新的subword而不是下一最高频字节对。WordPiece算法也是每次从词表中选出两个子词合并成新的子词。BPE选择频数最高的相邻子词合并,而WordPiece选择使得语言模型概率最大的相邻子词加入词表。
  • Unigram:它和 BPE 以及 WordPiece 从表面上看一个大的不同是,前两者都是初始化一个小词表,然后一个个增加到限定的词汇量,而 Unigram Language Model 却是先初始一个大词表,接着通过语言模型评估不断减少词表,直到限定词汇量。
  • SentencePiece:SentencePiece它是谷歌推出的子词开源工具包,它是把一个句子看作一个整体,再拆成片段,而没有保留天然的词语的概念。一般地,它把空格也当作一种特殊字符来处理,再用BPE或者Unigram算法来构造词汇表。SentencePiece除了集成了BPE、ULM子词算法之外,SentencePiece还能支持字符和词级别的分词。

下图是一些主流模型使用的分词算法:
在这里插入图片描述

从上表可以看出:

  • GPT-1 使用的 BPE 实现分词
  • LLaMA / BLOOM / GPT2 / ChatGLM 使用 BBPE 实现分词
  • BERT / DistilBERT / Electra 使用 WordPiece 进行分词
  • XLNet 则采用了 SentencePiece 进行分词

3.3 Subword 与 传统分词方法的比较

Subword 与 传统分词方法的区别主要在于:

  • 传统词表示方法无法很好的处理未知或罕见的词汇(OOV 问题)。
  • 传统词 tokenization 方法不利于模型学习词缀之间的关系,例如模型学到的“old”, “older”, and “oldest”之间的关系无法泛化到“smart”, “smarter”, and “smartest”。
  • Character embedding 作为 OOV 的解决方法粒度太细。
  • Subword 粒度在词与字符之间,能够较好的平衡 OOV 问题。

四、Byte Pair Encoding (BPE)

【参考资料】:BPE 算法原理及使用指南【深入浅出】

4.1 主要思想

字节对编码(BPE, Byte Pair Encoder),是一种数据压缩 算法,用来在固定大小的词表中实现可变⻓度的子词。该算法简单有效,因而目前它是最流行的方法。

BPE 首先将词分成单个字符,然后依次用另一个字符替换频率最高的一对字符 ,直到循环次数结束。

在这里插入图片描述

4.2 算法过程

接下来详细介绍 BPE 在分词中的算法过程:

  1. 准备语料库,确定期望的 subword 词表大小等参数。
  2. 通常在每个单词末尾添加后缀 </w>,统计每个单词出现的频率,例如,low 的频率为 5,那么我们将其改写为 "low</ w>": 5

注:停止符 </w> 的意义在于标明 subword 是词后缀。举例来说:st 不加 可以出现在词首,如 star;加了</w> 表明该子词位于词尾,如 west</w>,二者意义截然不同。

  1. 将语料库中所有单词拆分为单个字符,用所有单个字符建立最初的词典,并统计每个字符的频率,本阶段的 subword 的粒度是字符
  2. 挑出频次最高的符号对 ,比如说 th 组成的 th,将新字符加入词表,然后将语料中所有该字符对融合(merge),即所有 th 都变为 th

注:新字符依然可以参与后续的 merge,有点类似哈夫曼树,BPE 实际上就是一种贪心算法 。

  1. 重复遍历 2 和 3 操作,直到词表中单词数达到设定量 或下一个最高频数为 1 ,如果已经打到设定量,其余的词汇直接丢弃

注:看似我们要维护两张表,一个词表,一个字符表,实际上只有一张,词表只是为了我们方便理解。

4.3 完整例子

我们举一个完整的例子,来直观地看一下这个过程:

  1. 假设有语料集经过统计后表示为:
{'low':5, 'lower':2, 'newest':6, 'widest':3}

其中数字代表的是对应单词在语料中的频数。

  1. 拆分单词成最小单元,加后缀。初始化词表,这里词表的最小单元为字符。
    在这里插入图片描述

  2. 在语料上统计相邻单元的频数。这里,最高频连续子词对"e"和"s"出现了6+3=9次,将其合并成"es",有:

在这里插入图片描述

由于语料中不存在’s’子词了,因此将其从词表中删除。同时加入新的子词’es’。一增一减,词表大小保持不变。

  1. 继续统计相邻子词的频数。此时,最高频连续子词对"es"和"t"出现了6+3=9次, 将其合并成"est",有:
    在这里插入图片描述
  2. 接着,最高频连续子词对为"est"和"",有:

在这里插入图片描述
6. 继续上述迭代直到达到预设的Subword词表大小或下一个最高频的字节对出现频率为1。

这样我们就得到了更加合适的词表,这个词表可能会出现一些不是单词的组合,但是其本身有意义的一种形式。

4.4 BPE 的优缺点

(1)优点

上面例子中的语料库很小,知识为了方便我们理解 BPE 的过程,但实际中语料库往往非常非常大,无法给每个词(token)都放在词表中。

BPE 的优点就在于:可以很有效地平衡词典大小和编码步骤数(将语料编码所需要的 token 数量)。

随着合并的次数增加,词表大小通常先增加后减小。迭代次数太小,大部分还是字母,没什么意义;迭代次数多,又重新变回了原来那几个词。所以词表大小要取一个中间值。
在这里插入图片描述
(2)缺点

  • 对于同一个句子, 例如 Hello world,如图所示,可能会有不同的 Subword 序列。不同的 Subword 序列会产生完全不同的 id 序列表示,这种歧义可能在解码阶段无法解决。在翻译任务中,不同的 id 序列可能翻译出不同的句子,这显然是错误的。
    在这里插入图片描述

  • 在训练任务中,如果能对不同的 Subword 进行训练的话,将增加模型的健壮性,能够容忍更多的噪声,而 BPE 的贪心算法无法对随机分布进行学习。

4.5 BPE 的适用范围

BPE 一般适用在欧美语言拉丁语系中,因为欧美语言大多是字符形式,涉及前缀、后缀的单词比较多。而中文的汉字一般不用 BPE 进行编码,因为中文是字无法进行拆分。对中文的处理通常只有分词和分字两种。理论上分词效果更好,更好的区别语义。分字效率高、简洁,因为常用的字不过 3000 字,词表更加简短。

4.6 BPE 的实现

实现代码如下:

import re,collectionsdef get_vocab(filename):vocab = collections.defaultdict(int)with open(filename, 'r', encoding='utf-8') as fhand:for line in fhand:words = line.strip().split()for word in words:vocab[' '.join(list(word)) + ' </w>'] += 1return vocabdef get_stats(vocab):pairs = collections.defaultdict(int)for word, freq in vocab.items():symbols = word.split()for i in range(len(symbols)-1):pairs[symbols[i],symbols[i+1]] += freqreturn pairsdef merge_vocab(pair, v_in):v_out = {}bigram = re.escape(' '.join(pair))p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')for word in v_in:w_out = p.sub(''.join(pair), word)v_out[w_out] = v_in[word]return v_outdef get_tokens(vocab):tokens = collections.defaultdict(int)for word, freq in vocab.items():word_tokens = word.split()for token in word_tokens:tokens[token] += freqreturn tokens

跑一个例子试一下,这里已经对原句子进行了预处理:

if __name__ == "__main__":vocab = {'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}print('==========')print('Tokens Before BPE')tokens = get_tokens(vocab)print('Tokens: {}'.format(tokens))print('Number of tokens: {}'.format(len(tokens)))print('==========')num_merges = 5for i in range(num_merges):pairs = get_stats(vocab)if not pairs:breakbest = max(pairs, key=pairs.get)vocab = merge_vocab(best, vocab)print('Iter: {}'.format(i))print('Best pair: {}'.format(best))tokens = get_tokens(vocab)print('Tokens: {}'.format(tokens))print('Number of tokens: {}'.format(len(tokens)))

结果:

==========
Tokens Before BPE
Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 17, 'r': 2, 'n': 6, 's': 9, 't': 9, 'i': 3, 'd': 3})
Number of tokens: 11
==========
Iter: 0
Best pair: ('e', 's')
Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 8, 'r': 2, 'n': 6, 'es': 9, 't': 9, 'i': 3, 'd': 3})
Number of tokens: 11
==========
Iter: 1
Best pair: ('es', 't')
Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 8, 'r': 2, 'n': 6, 'est': 9, 'i': 3, 'd': 3})
Number of tokens: 10
==========
Iter: 2
Best pair: ('est', '</w>')
Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'est</w>': 9, 'i': 3, 'd': 3})
Number of tokens: 10
==========
Iter: 3
Best pair: ('l', 'o')
Tokens: defaultdict(<class 'int'>, {'lo': 7, 'w': 16, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'est</w>': 9, 'i': 3, 'd': 3})
Number of tokens: 9
==========
Iter: 4
Best pair: ('lo', 'w')
Tokens: defaultdict(<class 'int'>, {'low': 7, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'w': 9, 'est</w>': 9, 'i': 3, 'd': 3})
Number of tokens: 9
==========

4.7 编码与解码

上面的过程称为编码。解码过程比较简单,如果相邻子词间没有中止符,则将两子词直接拼接,否则两子词之间添加分隔符。 如果仍然有子字符串没被替换但所有 token 都已迭代完毕,则将剩余的子词替换为特殊 token,如 。例如:

# 编码序列
["the</w>", "high", "est</w>", "moun", "tain</w>"]
# 解码序列
"the</w> highest</w> mountain</w>"

4.8 调包使用 BPE

BPE 可以直接用最经典的 subword-nmt 包,不需要自己实现。

五、BBPE (Byte-level BPE)

5.1 基础知识

(1)Unicode
Unicode 是一种字符集,旨在涵盖地球上几乎所有的书写系统和字符。它为每个字符分配了一个唯一的代码点(code point)用于标识字符。Unicode 不关注字符在计算机内部的具体表示方式,而只是提供了一种字符到代码点的映射。Unicode 的出现解决了字符集的碎片化问题,使得不同的语言和字符能够在一个共同的标准下共存。然而,Unicode 并没有规定如何在计算机内存中存储和传输这些字符。

(2)UTF-8
UTF-8(Unicode Transformation Format-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 的字符编码方式,用于在计算机中存储和传输字符。

(3)Byte(字节)
计算机存储和数据处理时,字节是最小的单位。一个字节包含8个(Bit)二进制位,每个位可以是0或1,每位的不同排列和组合可以表示不同的数据,所以一个字节能表示的范围是256个。

5.2 基本思想

Byte-level BPE(BBPE) 和 Byte-Pair Encoding (BPE) 区别就是:

  • BPE是最小词汇是字符级别
  • 而BBPE是字节级别的,通过UTF-8的编码方式这一个字节的256的范围,理论上可以表示这个世界上的所有字符。

所以实现的步骤和BPE就是实现的粒度不一样,其他的都是一样的。具体步骤为:

  • 初始词表:构建初始词表,包含一个字节的所有表示(256)。
  • 构建频率统计:统计所有子词单元对(两个连续的子词)在文本中的出现频率。
  • 合并频率最高的子词对:选择出现频率最高的子词对,将它们合并成一个新的子词单元,并更新词汇表。
  • 重复合并步骤:不断重复步骤 2 和步骤 3,直到达到预定的词汇表大小、合并次数,或者直到不再有有意义的合并(即,进一步合并不会显著提高词汇表的效益)。
  • 分词:使用最终得到的词汇表对文本进行分词。

5.3 代码实现

from collections import defaultdict# 构建频率统计
def build_stats(sentences):stats = defaultdict(int)for sentence in sentences:symbols = sentence.split()for symbol in symbols:stats[symbol.encode("utf-8")] += 1return statsdef compute_pair_freqs(splits):pair_freqs = defaultdict(int)for word, freq in stats.items():split = splits[word]if len(split) == 1:continuefor i in range(len(split) - 1):pair = (split[i], split[i + 1])pair_freqs[pair] += freqreturn pair_freqsdef merge_pair(pair, splits):merged_byte = bytes(pair)for word in stats:split = splits[word]if len(split) == 1:continuei = 0while i < len(split) - 1:if split[i:i+2] == pair:  # 检查分割中是否有这对字节split = split[:i] + [merged_byte] + split[i + 2 :]else:i += 1splits[word] = splitreturn splitsif __name__ == "__main__":sentences = ["我","喜欢","吃","苹果","他","不","喜欢","吃","苹果派","I like to eat apples","She has a cute cat","you are very cute","give you a hug",]# 构建初始词汇表,包含一个字节的256个表示initial_vocab = [bytes([byte]) for byte in range(256)]vocab = initial_vocab.copy()print("initial_vocab:", initial_vocab)stats = build_stats(sentences)splits = {word: [byte for byte in word] for word in stats.keys()}pair_freqs = compute_pair_freqs(splits)vocab_size = 50while len(vocab) < vocab_size:pair_freqs = compute_pair_freqs(splits)best_pair = ()max_freq = Nonefor pair, freq in pair_freqs.items():if max_freq is None or max_freq < freq:best_pair = pairmax_freq = freqsplits = merge_pair(best_pair, splits)merged_byte = bytes(best_pair)print("vocab:", vocab)

解释一下为什么Byte-level BPE(BBPE)不会出现OOV问题,初始的词表里有256个表示如下:

initial_vocab: [b'\x00', b'\x01', b'\x02', b'\x03', b'\x04', b'\x05', b'\x06', b'\x07', b'\x08', b'\t', b'\n', b'\x0b', b'\x0c', b'\r', b'\x0e', b'\x0f', b'\x10', b'\x11', b'\x12', b'\x13', b'\x14', b'\x15', b'\x16', b'\x17', b'\x18', b'\x19', b'\x1a', b'\x1b', b'\x1c', b'\x1d', b'\x1e', b'\x1f', b' ', b'!', b'"', b'#', b'$', b'%', b'&', b"'", b'(', b')', b'*', b'+', b',', b'-', b'.', b'/', b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b':', b';', b'<', b'=', b'>', b'?', b'@', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'[', b'\\', b']', b'^', b'_', b'`', b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', b'{', b'|', b'}', b'~', b'\x7f', b'\x80', b'\x81', b'\x82', b'\x83', b'\x84', b'\x85', b'\x86', b'\x87', b'\x88', b'\x89', b'\x8a', b'\x8b', b'\x8c', b'\x8d', b'\x8e', b'\x8f', b'\x90', b'\x91', b'\x92', b'\x93', b'\x94', b'\x95', b'\x96', b'\x97', b'\x98', b'\x99', b'\x9a', b'\x9b', b'\x9c', b'\x9d', b'\x9e', b'\x9f', b'\xa0', b'\xa1', b'\xa2', b'\xa3', b'\xa4', b'\xa5', b'\xa6', b'\xa7', b'\xa8', b'\xa9', b'\xaa', b'\xab', b'\xac', b'\xad', b'\xae', b'\xaf', b'\xb0', b'\xb1', b'\xb2', b'\xb3', b'\xb4', b'\xb5', b'\xb6', b'\xb7', b'\xb8', b'\xb9', b'\xba', b'\xbb', b'\xbc', b'\xbd', b'\xbe', b'\xbf', b'\xc0', b'\xc1', b'\xc2', b'\xc3', b'\xc4', b'\xc5', b'\xc6', b'\xc7', b'\xc8', b'\xc9', b'\xca', b'\xcb', b'\xcc', b'\xcd', b'\xce', b'\xcf', b'\xd0', b'\xd1', b'\xd2', b'\xd3', b'\xd4', b'\xd5', b'\xd6', b'\xd7', b'\xd8', b'\xd9', b'\xda', b'\xdb', b'\xdc', b'\xdd', b'\xde', b'\xdf', b'\xe0', b'\xe1', b'\xe2', b'\xe3', b'\xe4', b'\xe5', b'\xe6', b'\xe7', b'\xe8', b'\xe9', b'\xea', b'\xeb', b'\xec', b'\xed', b'\xee', b'\xef', b'\xf0', b'\xf1', b'\xf2', b'\xf3', b'\xf4', b'\xf5', b'\xf6', b'\xf7', b'\xf8', b'\xf9', b'\xfa', b'\xfb', b'\xfc', b'\xfd', b'\xfe', b'\xff']

通过上述的方式其实是在一直根据训练语料循环迭代合成子词或者词,最后形成词表,比如 “苹果” 通过UTF-8进行编码后为 \xe8\x8b\xb9\xe6\x9e\x9c,如果词表里面有,那“苹果” 就通过词表映射成了1个表示,准确来说是1个token;如果词表里没有,那就用256中的 \xe8+\x8b+\xb9+\xe6+\x9e+\x9c 来表示“苹果”这个词,那就是6个token。

在先前的各种分词方法中,如果词典里没有”苹果“这个词,也没有”苹“,”果“这样的子词的话,那就变成了[UNK]。所以在现在的大模型中,以 Byte-level BPE(BBPE) 这种方式进行分词是不会出现OOV,但词表中如果没有word级别的词的话,一些中英文就会分词分的很细碎,比如Llama在中文上就会把一些词分成多个token其实就是UTF-8后的中文编码,对编码效率以及语义会有影响,于是出现了一些扩充Llama中文词表的工作。

六、WordPiece

6.1 基本思想

Google的Bert模型在分词的时候使用的是WordPiece算法。与BPE算法类似,WordPiece算法也是每次从词表中选出两个子词合并成新的子词。

与BPE的最大区别在于,如何选择两个子词进行合并:

  • BPE选择频数最高的相邻子词合并;
  • WordPiece选择能够提升语言模型概率最大的相邻子词加入词表。

看到这里,你可能不清楚WordPiece是如何选取子词的。这里,通过形式化方法,能够清楚地理解WordPiece在合并这一步是如何作出选择的。假设句子 S = ( t 1 , t 2 , . . . , t n ) S = (t_1, t_2,...,t_n) S=(t1,t2,...,tn) 由n个子词组成, t i t_i ti表示子词,且假设各个子词之间是独立存在的,则句子 S S S 的语言模型似然值等价于所有子词概率的乘积:
在这里插入图片描述

假设把相邻位置的 x x x y y y 两个子词进行合并,合并后产生的子词记为 z z z ,此时句子 S S S 似然值的变化可表示为:

在这里插入图片描述
从上面的公式,很容易发现,似然值的变化就是两个子词之间的互信息。简而言之,WordPiece每次选择合并的两个子词,他们具有最大的互信息值,也就是两子词在语言模型上具有较强的关联性,它们经常在语料中以相邻方式同时出现。

6.2 优势与劣势

  • 优势:可以较好的平衡词表大小和OOV问题;

  • 劣势:可能会产生一些不太合理的子词或者说错误的切分;对拼写错误非常敏感;对前缀的支持不够好;

七、Unigram Language Model (ULM)

7.1 基本思想

与WordPiece一样,Unigram Language Model(ULM)同样使用语言模型来挑选子词。不同之处在于,BPE和WordPiece算法的词表大小都是从小到大变化,属于增量法。 而Unigram Language Model则是减量法,即先初始化一个大词表,根据评估准则不断丢弃词表,直到满足限定条件。 ULM算法考虑了句子的不同分词可能,因而能够输出带概率的多个子词分段。

我们接下来看看ULM是如何操作的。对于句子S, x ⃗ = ( x 1 , x 2 , . . . , x m ) \vec x = (x_1, x_2,...,x_m) x =(x1,x2,...,xm) 为句子的一个分词结果,由m个子词组成。所以,当前分词下句子S的似然值可以表示为:
在这里插入图片描述
对于句子S,挑选似然值最大的作为分词结果,则可以表示为:
在这里插入图片描述

这里 U ( x ) U(x) U(x) 包含了句子的所有分词结果。在实际应用中,词表大小有上万个,直接罗列所有可能的分词组合不具有操作性。针对这个问题,可通过维特比算法得到 x ∗ {x^*} x 来解决。

那怎么求解每个子词的概率 P ( x i ) P(x_i) P(xi) 呢?ULM通过EM算法来估计。假设当前词表 V V V, 则 M M M 步最大化的对象是如下似然函数:
在这里插入图片描述
其中,|D|是语料库中语料数量。上述公式的一个直观理解是,将语料库中所有句子的所有分词组合形成的概率相加。

但是,初始时,词表V并不存在。因而,ULM算法采用不断迭代的方法来构造词表以及求解分词概率:

  1. 初始时,建立一个足够大的词表。一般,可用语料中的所有字符加上常见的子字符串初始化词表,也可以通过BPE算法初始化。
  2. 针对当前词表,用EM算法求解每个子词在语料上的概率。
  3. 对于每个子词,计算当该子词被从词表中移除时,总的loss降低了多少,记为该子词的loss。
  4. 将子词按照loss大小进行排序,丢弃一定比例loss最小的子词(比如20%),保留下来的子词生成新的词表。这里需要注意的是,单字符不能被丢弃,这是为了避免OOV情况。
  5. 重复步骤2到4,直到词表大小减少到设定范围。

可以看出,ULM会保留那些以较高频率出现在很多句子的分词结果中的子词,因为这些子词如果被丢弃,其损失会很大。

7.2 优势与劣势

  • 优势:

    • 使用的训练算法可以利用所有可能的分词结果,这是通过data sampling算法实现的;
    • 提出一种基于语言模型的分词算法,这种语言模型可以给多种分词结果赋予概率,从而可以学到其中的噪声;
    • 使用时也可以给出带概率的多个分词结果。
  • 劣势:

    • 效果与初始词表息息相关,初始的大词表要足够好,比如可以通过BPE来初始化;
    • 略显复杂。

八、SentencePiece

8.1 基本思想

SentencePiece 是谷歌推出的子词开源工具包,其中集成了BPE、ULM子词算法。除此之外,SentencePiece还能支持字符和词级别的分词。更进一步,为了能够处理多语言问题,sentencePiece将句子视为Unicode编码序列,从而子词算法不用依赖于语言的表示。

SentencePiece 的主要特性包括:

  • 多分词粒度:支持BPE、ULM子词算法,也支持char, word分词;
  • 多语言:以unicode方式编码字符,将所有的输入(英文、中文等不同语言)都转化为unicode字符,解决了多语言编码方式不同的问题;
  • 编解码的可逆性:之前几种分词算法对空格的处理略显粗暴,有时是无法还原的。Sentencepiece显式地将空白作为基本标记来处理,用一个元符号 “▁”( U+2581 )转义空白,这样就可以实现简单且可逆的编解码;
  • 无须Pre-tokenization:Sentencepiece可以直接从raw text/setences进行训练,无须Pre-tokenization;
  • 快速、轻量化

8.2 使用说明

(1) 安装依赖包

pip install sentencepiece

(2) 训练模型和使用模型
SentencePiece分为两部分:训练模型和使用模型。训练结束后生成一个model文件和一个词典vocab文件。

spm.SentencePieceTrainer.train('--input=train.txt --model_prefix=m --vocab_size=1000 --character_coverage=0.9995 --model_type=bpe')

参数说明:

  • input: 训练语料文件,可以传递以逗号分隔的文件列表。文件格式为每行一个句子。 无需运行tokenizer、normalizer或preprocessor。 默认情况下,SentencePiece 使用 Unicode NFKC 规范化输入。
  • model_prefix:输出模型名称前缀。 训练完成后将生成 <model_name>.model 和 <model_name>.vocab 文件。
  • vocab_size:训练后的词表大小,例如:8000、16000 或 32000
  • character_coverage:模型覆盖的字符数量,对于字符集丰富的语言(如日语或中文)推荐默认值为 0.9995,对于其他字符集较小的语言推荐默认值为 1.0。
  • model_type:模型类型。 可选值:unigram(默认)、bpe、char 或 word 。 使用word类型时,必须对输入句子进行pretokenized。

(3) 代码示例

以下是一个使用 SentencePiece 进行文本符号化的代码示例:

import sentencepiece as spm# 训练 SentencePiece 模型
spm.SentencePieceTrainer.train('--input=train.txt --model_prefix=m --vocab_size=1000 --character_coverage=0.9995 --model_type=bpe')# 加载训练好的模型
sp = spm.SentencePieceProcessor()
sp.load('m.model')# 文本符号化
text = "Hello, world!"
tokens = sp.encode_as_pieces(text)# 输出结果
print(tokens)

(3) 代码解读

  • 训练模型:使用 SentencePieceTrainer.train 方法训练 SentencePiece 模型,指定输入文件、模型前缀和词汇表大小。
  • 加载模型:使用 SentencePieceProcessor 加载训练好的模型。
  • 文本符号化:使用 encode_as_pieces 方法对文本进行符号化处理。
  • 输出结果:打印符号化后的结果。

九、LLM中的分词器

9.1 BERT的分词器

  • 代码:https://github.com/huggingface/transformers/blob/main/src/transformers/models/bert/tokenization_bert.py
  • 文档:https://huggingface.co/docs/transformers/v4.28.1/en/model_doc/bert#transformers.BertTokenizer

BERT的分词器由两个部分组成:

  1. BasicTokenizer:
    转成 unicode:Python3,输入为str时,可以省略这一步
    _clean_text:去除各种奇怪字符
    _tokenize_chinese_chars:中文按字拆开
    whitespace_tokenize:空格分词
    _run_strip_accents:去掉变音符号
    _run_split_on_punc:标点分词
    再次空格分词:whitespace_tokenize(" ".join(split_tokens)),先用空格join再按空白分词,可以去掉连续空格

  2. WordpieceTokenizer:
    贪心最大匹配:用双指针实现;

9.2 LLaMA的分词器

  • 代码:https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama/tokenization_llama.py

  • 文档:https://huggingface.co/docs/transformers/v4.28.1/en/model_doc/llama#transformers.LlamaTokenizer

LLaMA的分词器基于BBPE实现;这也是LLaMA支持多语言的原因。

9.3 GLM的分词器

  • 代码:
    • https://github.com/THUDM/GLM
    • https://github.com/THUDM/GLM/blob/main/data_utils/tokenization.py

参考资料

  • BPE 算法原理及使用指南【深入浅出】
  • NLP三大Subword模型详解:BPE、WordPiece、ULM
  • 【OpenLLM 008】大模型基础组件之分词器-万字长文全面解读LLM中的分词算法与分词器(tokenization & tokenizers):BPE/WordPiece/ULM & beyond
  • python库 - sentencepiece

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/67005.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OceanBase数据库设计与管理:构建高效分布式数据架构基石

《OceanBase数据库设计与管理&#xff1a;构建高效分布式数据架构基石》 在当今数字化浪潮汹涌澎湃的时代&#xff0c;数据量呈爆炸式增长&#xff0c;业务复杂度不断攀升&#xff0c;传统数据库在应对海量数据存储、高并发读写以及高可用性需求时愈发捉襟见肘。OceanBase作为…

搭建Hadoop分布式集群

软件和操作系统版本 Hadoop框架是采用Java语言编写&#xff0c;需要java环境&#xff08;jvm&#xff09; JDK版本&#xff1a;JDK8版本 &#xff0c;本次使用的是 Java: jdk-8u431-linux-x64.tar.gz Hadoop: hadoop-3.3.6.tar.gz 三台Linux虚拟节点: CentOS-7-x86_64-DVD-2…

分享一下使用高版本(3.10.x)sonar-maven-plugin 进行构建时需要注意的两个问题

SonarScanner用来执行源代码分析。这个独立的程序在CI/CD主机上运行&#xff0c;并将分析结果发送到SonarQube服务器&#xff0c;由其计算分析结果&#xff0c;计算质量门并生成报告。我们可以通过命令行和maven构建两种方式来执行SonarScanner的源码分析。关于SonarScanner更多…

C语言---函数和数组实践:扫雷游戏

函数和数组实践&#xff1a;扫雷游戏 在这次的实践项目中&#xff0c;需要编写一个可以在在控制台运行的经典的扫雷游戏。 一、游戏要求 游戏有菜单&#xff0c;可以通过菜单实现继续玩或者退出游戏游戏要求棋盘9*9&#xff0c;雷&#xff08;10个&#xff09;要求随机布置可…

数据结构与算法之二叉树: LeetCode 701. 二叉搜索树中的插入操作 (Ts版)

二叉搜索树中的插入操作 https://leetcode.cn/problems/insert-into-a-binary-search-tree/description/ 描述 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和要插入树中的值 value &#xff0c;将值插入二叉搜索树返回插入后二叉搜索树的根节点。 输入数据 保…

数据集-目标检测系列- 石榴 检测数据集 pomegranate >> DataBall

数据集-目标检测系列- 石榴 检测数据集 pomegranate >> DataBall DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。 需要更多数据资源和技术解决方案&#xff0c;知识星球&#xff1a; “DataBall - X 数据球(fre…

项目实战——使用python脚本完成指定OTA或者其他功能的自动化断电上电测试

前言 在嵌入式设备的OTA场景测试和其他断电上电测试过程中&#xff0c;有的场景发生在夜晚或者随时可能发生&#xff0c;这个时候不可能24h人工盯着&#xff0c;需要自动化抓取串口日志处罚断电上电操作。 下面的python脚本可以实现自动抓取串口指定关键词&#xff0c;然后触发…

深入详解DICOM医学影像定位线相关知识:理解定位线的概念、定位线的作用以及定位线显示和计算原理

DICOM医学影像中的定位线(Localization Line) 在医学影像学中,DICOM是用于存储和交换医学影像的标准格式。定位线(Localization Line)在医学影像的显示和分析中起着重要作用,它帮助医生和医学专业人员在影像中精确地标定重要的解剖结构、区域或特征,辅助进行定位、治疗计…

《自动驾驶与机器人中的SLAM技术》ch8:基于 IESKF 的紧耦合 LIO 系统

目录 基于 IESKF 的紧耦合 LIO 系统 1 IESKF 的状态变量和运动过程 1.1 对名义状态变量的预测 1.2 对误差状态变量的预测及对协方差矩阵的递推 2 观测方程中的迭代过程 3 高维观测中的等效处理 4 NDT 和 卡尔曼滤波的联系 5 紧耦合 LIO 系统的主要流程 5.1 IMU 静止初始化 …

Java语言的并发编程

Java语言的并发编程 引言 在现代软件开发中&#xff0c;随着计算机硬件性能的不断提升&#xff0c;应用程序的并发性变得愈加重要。并发编程是提升程序性能和响应速度的有效手段&#xff0c;Java作为一门广泛使用的编程语言&#xff0c;提供了丰富的并发编程机制。本文将深入…

Go语言开发高效的RPC服务的方法

在Go语言中开发高效的RPC&#xff08;远程过程调用&#xff09;服务&#xff0c;可以通过以下几个关键步骤和方法来实现&#xff1a; 一、定义服务接口 首先需要定义需要提供的RPC方法及其参数和返回值。可以使用Go语言的interface类型来定义RPC接口&#xff0c;同时也可以为…

HTML实战课堂之简单的拜年程序

一、目录&#xff1a; &#xfffc;&#xfffc; 一、目录&#xff1a; 二、祝福 三&#xff1a;代码讲解 &#xff08;1&#xff09;详细解释&#xff1a; 1.HTML部分 2. CSS部分 三、运行效果&#xff08;随机截图&#xff09;&#xff1a; 四、完整代码&#xff1a; 二、祝福…

vue 与 vue-json-viewer 实现 JSON 数据可视化

前言 接口的调试和测试是确保系统稳定性的重要步骤。为了让开发人员和测试人员能够直观地查看接口返回的 JSON 数据&#xff0c;使用合适的工具至关重要。vue-json-viewer 插件为 vue 开发者提供了一个简单而强大的解决方案。本文将详细介绍如何在 vue 项目中使用该插件&#x…

用Pygame Zero 画矩形(空心、实心、多个矩形、多层同心矩形、彩虹条矩形、条纹相间、随机颜色矩形、特殊效果、渐变效果)

用Pygame Zero 画矩形 &#xff08;空心、实心、多个矩形、多层同心矩形、彩虹条矩形、条纹相间、随机颜色矩形、特殊效果、渐变效果&#xff09; 本文目录&#xff1a; 零、时光宝盒 一、绘制空心矩形 二、绘制实心矩形 三、画多个静止矩形 四、绘制多层同心矩形 4.1、…

【Rust自学】11.9. 单元测试

喜欢的话别忘了点赞、收藏加关注哦&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 11.9.1. 测试的分类 Rust把测试分为两类&#xff0c;一个是单元测试&#xff0c;一个是集成测试。 单元测试比较小也比较专注&#xff…

[java基础-集合篇]优先队列PriorityQueue结构与源码解析

优先队列PriorityQueue 优先级队列表示为平衡二进制堆&#xff1a; queue[n] 的两个子级是 queue[2*n1] 和 queue[2*&#xff08;n1&#xff09;]。 注&#xff1a;左子节点index2*parentIndex1,右子节点index2*parentIndex2,源码中计算parent位置时就是这样反过来计算的 优…

(经过验证)在 Ubuntu 系统中为 VSCode、PyCharm 终端及 Jupyter Notebook 配置代理的完整方案

文章目录 1. 通过系统环境变量配置代理步骤一&#xff1a;打开终端步骤二&#xff1a;编辑 ~/.bashrc 文件步骤三&#xff1a;添加代理环境变量步骤四&#xff1a;保存并关闭文件步骤五&#xff1a;使配置生效步骤六&#xff1a;重启相关应用步骤七&#xff1a;使用代理函数 2.…

深入探讨 Vue.js 的动态组件渲染与性能优化

Vue.js 作为一款前端领域中备受欢迎的渐进式框架&#xff0c;以其简单优雅的 API 和灵活性受到开发者的喜爱。在开发复杂应用时&#xff0c;动态组件渲染是一项极其重要的技术&#xff0c;它能够在页面中动态地加载或切换组件&#xff0c;从而显著提升应用的灵活性与用户体验。…

回归预测 | MATLAB实RVM-Adaboost相关向量机集成学习多输入单输出回归预测

回归预测 | MATLAB实RVM-Adaboost相关向量机集成学习多输入单输出回归预测 目录 回归预测 | MATLAB实RVM-Adaboost相关向量机集成学习多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 RVM-Adaboost相关向量机集成学习多输入单输出回归预测是一种先进…

Linux(Centos7)安装Mysql/Redis/MinIO

安装Mysql 安装Redis 搜索Redis最先版本所在的在线安装yum库 查看以上两个组件是否是开机自启 安装MinIO 开源的对象存储服务&#xff0c;存储非结构化数据&#xff0c;兼容亚马逊S3协议。 minio --help #查询命令帮助minio --server --help #查询--server帮助minio serve…