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

news2025/1/15 2:31:50

文章目录

  • 一、基本概念
  • 二、传统分词方法
    • 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,collections


def 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>'] += 1
    return vocab


def 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]] += freq
    return pairs


def 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_out


def get_tokens(vocab):
    tokens = collections.defaultdict(int)
    for word, freq in vocab.items():
        word_tokens = word.split()
        for token in word_tokens:
            tokens[token] += freq
    return 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 = 5
    for i in range(num_merges):
        pairs = get_stats(vocab)
        if not pairs:
            break
        best = 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")] += 1
    return stats


def compute_pair_freqs(splits):
    pair_freqs = defaultdict(int)
    for word, freq in stats.items():
        split = splits[word]
        if len(split) == 1:
            continue
        for i in range(len(split) - 1):
            pair = (split[i], split[i + 1])
            pair_freqs[pair] += freq
    return pair_freqs


def merge_pair(pair, splits):
    merged_byte = bytes(pair)
    for word in stats:
        split = splits[word]
        if len(split) == 1:
            continue
        i = 0
        while i < len(split) - 1:
            if split[i:i+2] == pair:  # 检查分割中是否有这对字节
                split = split[:i] + [merged_byte] + split[i + 2 :]
            else:
                i += 1
        splits[word] = split
    return splits


if __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 = 50

    while len(vocab) < vocab_size:
        pair_freqs = compute_pair_freqs(splits)
        best_pair = ()
        max_freq = None
        for pair, freq in pair_freqs.items():
            if max_freq is None or max_freq < freq:
                best_pair = pair
                max_freq = freq
        splits = 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.coloradmin.cn/o/2276754.html

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

相关文章

第三十六章 Spring之假如让你来写MVC——拦截器篇

Spring源码阅读目录 第一部分——IOC篇 第一章 Spring之最熟悉的陌生人——IOC 第二章 Spring之假如让你来写IOC容器——加载资源篇 第三章 Spring之假如让你来写IOC容器——解析配置文件篇 第四章 Spring之假如让你来写IOC容器——XML配置文件篇 第五章 Spring之假如让你来写…

PyTorch 深度学习框架快速入门 (小土堆)

PyTorch 深度学习框架快速入门 深度学习框架常用模块数据集存取图片数据处理库 —— PILOS 模块实例 Tensorboard 记录机器学习的过程Transform 进行图像变换数据集的下载DataLoaderModule 自定义网络前向传播卷积层卷积简单应用 最大池化非线性层线性层 简单的整合基于现有网络…

FPGA的 基本结构(Xilinx 公司Virtex-II 系列FPGA )

以Xilinx 公司Virtex-II 系列FPGA 为例&#xff0c;其基本结构由下图所示。它是主要由两大部分组成&#xff1a;可编程输入/输出&#xff08;Programmable I/Os&#xff09;部分和内部可配置&#xff08;Configurable Logic&#xff09;部分。 可编程输入/输出&#xff08;I/Os…

【Elasticsearch】批量操作:优化性能

🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,高并发设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探…

C++、Haskell 和 Rust 三种语言实现 Faster Suffix Sort 算法的比较

对 C、Haskell 和 Rust 三种语言实现 Faster Suffix Sort 算法的比较&#xff1a; 1. 编程效率 C&#xff1a; 优点&#xff1a;C 提供了丰富的标准库&#xff0c;如 std::sort&#xff0c;可以方便地结合自定义比较函数对后缀数组进行排序。使用 Lambda 表达式可以简洁地实现…

校园跑腿小程序---轮播图,导航栏开发

hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生…

uniapp实现H5页面内容居中与两边留白,打造类似微信公众号阅读体验

在 UniApp 中&#xff0c;由于需要兼容多端应用&#xff0c;我们通常使用 rpx 作为尺寸单位。然而&#xff0c;在某些情况下&#xff0c;如需要实现内容居中且两边留白时&#xff0c;直接使用 rpx 可能会带来一些限制。这时&#xff0c;我们可以考虑使用 px 或 rem 等单位&…

【Uniapp-Vue3】pages.json页面路由globalStyle的属性

项目的全局配置在pages.json中。 一、导航栏设置 二、下拉刷新设置 下拉就可以看到设置的样式 三、上拉触底 这个页面中&#xff0c;向下滑动页面到底部就会输出“到底了” 现在将触底距离设置为500 走到半路就会输出“到底了”

Type-C双屏显示器方案

在数字化时代&#xff0c;高效的信息处理和视觉体验已成为我们日常生活和工作的关键需求。随着科技的进步&#xff0c;一款结合了便携性和高效视觉输出的设备——双屏便携屏&#xff0c;逐渐崭露头角&#xff0c;成为追求高效工作和娱乐体验人群的新宠。本文将深入探讨双屏便携…

Linux下部署Redis(本地部署超详细)

非docker 1、下载Redis 历史版本&#xff1a; http://download.redis.io/releases 我的&#xff1a; http://download.redis.io/releases/redis-7.0.5.tar.gz 2.安装教程 1.Redis是基于c语言编写的需要安装依赖&#xff0c;需要安装gcc yum install gcc-c 2.查看gcc版…

使用 Multer 上传图片到阿里云 OSS

文件上传到哪里更好&#xff1f; 上传到服务器本地 上传到服务器本地&#xff0c;这种方法在现今商业项目中&#xff0c;几乎已经见不到了。因为服务器带宽&#xff0c;磁盘 IO 都是非常有限的。将文件上传和读取放在自己服务器上&#xff0c;并不是明智的选择。 上传到云储存…

UE5 打包项目

UE5 打包项目 flyfish 通过 “文件”->“打开项目”&#xff0c;然后在弹出的对话框中选择项目文件&#xff08;通常是以.uproject为后缀的文件&#xff09; 选择目标平台&#xff1a; 在 UE5 主界面中&#xff0c;找到 “平台”&#xff08;Platforms&#xff09;。根据…

自然语言转 SQL:通过 One API 将 llama3 模型部署在 Bytebase SQL 编辑器

使用 Open AI 兼容的 API&#xff0c;可以在 Bytebase SQL 编辑器中使用自然语言查询数据库。 出于数据安全的考虑&#xff0c;私有部署大语言模型是一个较好的选择 – 本文选择功能强大的开源模型 llama3。 由于 OpenAI 默认阻止出站流量&#xff0c;为了简化网络配置&#…

杭州铭师堂的云原生升级实践

作者&#xff1a;升学e网通研发部基建团队 公司介绍 杭州铭师堂&#xff0c;是一个致力于为人的全面发展而服务的在线教育品牌。杭州铭师堂秉持“用互联网改变教育&#xff0c;让中国人都有好书读”的使命&#xff0c;致力于用“互联网教育”的科技手段让更多的孩子都能享有优…

STM32 FreeRTOS移植

目录 FreeRTOS源码结构介绍 获取源码 1、 官网下载 2、 Github下载 源码结构介绍 源码整体结构 FreeRTOS文件夹结构 Source文件夹结构如下 portable文件夹结构 RVDS文件夹 MemMang文件夹 FreeRTOS在基于寄存器项目中移植步骤 目录添加源码文件 工程添加源码文件 …

可以进行重复测量的方差分析的AI agent

可以进行重复测量的方差分析的AI agent 前几天做了机器学习的AI agent&#xff0c;把一个糖尿病机器学习模型采用API的形式接入到LLM模型中&#xff0c;结合LLM的智能性和机器学习模型的准确性&#xff0c;利用两者的有点&#xff0c;有可以避免两者的缺点&#xff0c;是一条合…

OpenCV实现Kuwahara滤波

Kuwahara滤波是一种非线性的平滑滤波技术&#xff0c;其基本原理在于通过计算图像模板中邻域内的均值和方差&#xff0c;选择图像灰度值较为均匀的区域的均值来替代模板中心像素的灰度值。以下是Kuwahara滤波的详细原理说明&#xff1a; 一、基本思想 Kuwahara滤波的基本思想…

[文献精汇]使用 LSTM Networks 的均值回归交易策略

Backtrader 策略实例 [Backtrader]实例:均线策略[Backtrader] 实例:MACD策略[Backtrader] 实例:KDJ 策略[Backtrader] 实例:RSI 与 EMA 结合[Backtrader] 实例:SMA自定义数据源[Backtrader] 实例:海龟策略[Backtrader] 实例:网格交易[Backtrader] 实例: 配对交[Backtrader] 机…

IDEA Maven构建时报错:无效的目标发行版17

报错分析 报错原因&#xff1a;Maven 构建时&#xff0c;Java 版本配置不匹配 我安装的JDK版本是1.8&#xff0c;但由于种种原因&#xff0c;Maven构建时指定了 Java 17 作为目标发行版&#xff0c;从而导致错误 解决方案 首先&#xff0c;java -version&#xff0c;查看环…

计算机网络 (39)TCP的运输连接管理

前言 TCP&#xff08;传输控制协议&#xff09;是一种面向连接的、可靠的传输协议&#xff0c;它在计算机网络中扮演着至关重要的角色。TCP的运输连接管理涉及连接建立、数据传送和连接释放三个阶段。 一、TCP的连接建立 TCP的连接建立采用三次握手机制&#xff0c;其过程如下&…