Tokenizer

在大模型中,Tokenizer(分词器) 是处理自然语言文本和模型之间的桥梁,它将原始文本转换成 模型可以理解的数字格式。由于大模型(如 GPT、BERT、T5 等)主要通过 token(标记)进行处理,分词器的作用尤为重要。理解大模型中的 Tokenizer 不仅有助于了解文本如何转换为可计算的输入,还可以更好地理解语言模型的处理流程。

在这里插入图片描述

1. 什么是 Tokenizer

Tokenizer 是一个将文本分割成单独的 token(标记)并将它们映射为对应的数字 ID 的过程。token 可以是:

  • :例如,“hello”、“world”。
  • 子词:例如,“unhappiness” 可以分解为 “un” 和 “happiness”。
  • 字符:例如,“hello” 被拆分为 “h”, “e”, “l”, “l”, “o”。
  • 标点符号:如 “!”, “.”, “,” 等。

这种将文本转为 token 的方式通常是由分词算法控制的,模型通过这些 token 来进行推理。

直观感受:https://huggingface.co/spaces/Xenova/the-tokenizer-playground

  • 不同的模型划分结果不一样

在这里插入图片描述

2. Tokenization 的流程

1. 文本预处理
  • 清理文本:通常文本在输入模型之前需要进行一些预处理,包括小写化、去除标点符号、去除停用词等。
  • 规范化:如 Unicode 标准化、拼写校正等,目的是确保分词器能够一致地处理不同类型的输入文本。
2. 分词(Tokenization)
  • 将预处理后的文本切割成 tokens(词、子词或字符)。这一过程依据所采用的分词算法来决定如何切割文本。
3. 映射为数字 ID
  • 每个 token 会被映射为一个 token ID,即在词汇表(Vocabulary)中的索引。这个过程是通过查找词汇表来完成的。

例如,假设词汇表中有这样的映射:

  • “hello” → 5
  • “world” → 10

如果文本为 “hello world”,那么 token IDs 可能为 [5, 10]

4. 模型输入
  • 经过分词器处理后的 token IDs 会作为输入传递给大模型。模型通过这些 ID 进行进一步的计算和推理。
5. 解码(Detokenization)
  • 解码是将模型生成的 token IDs 转换回可读的文本格式。通过查找词汇表,将 token ID 映射回相应的词或子词。

4. Tokenization 方法的种类

在大模型中,Tokenizer 的颗粒度(granularity)指的是将原始文本分割成最小单元(tokens)时的细致程度。颗粒度越小,分割的单元越细,模型能够处理的语义信息就越多,但计算复杂度也会增大;颗粒度越大,分割的单元越粗,计算效率更高,但可能会丢失一些细节。

大模型 中,Tokenizer 的颗粒度通常有以下几种常见的类型:

4.1 Word-based Tokenization(基于词的分词)

Word-based Tokenization 直接按空格和标点符号将文本划分成单独的词(tokens)。每个 token 对应一个完整的词

原理

  • 该方法将文本分割为由空格、标点符号等分隔的词。例如,句子 "I love programming!" 会被分割为:
    • ["I", "love", "programming", "!"]
  • 在英文中,这种方法通常使用正则表达式或空格和标点符号来划分单词。

优缺点

  • 优点

    • 直观且简单:非常适合处理标准化的、没有错别字的文本。
    • 计算效率较高:相比细粒度分词,词级分词在处理长文本时速度更快。
  • 缺点

    • OOV 问题:无法处理未登录词(Out-Of-Vocabulary),如新词、拼写错误、复合词等。如果模型未见过某个词,它无法有效处理。

    • 无法处理词形变化:如 runningran 等词形变化通常被认为是不同的词,无法捕捉它们之间的语法联系。

      在这里插入图片描述

适用场景

  • 适用于常规的 NLP 任务,如文本分类、情感分析、命名实体识别(NER)等,尤其是在词汇表较小且文本格式标准的任务中。

4.2 Character-based Tokenization(基于字符的分词)

Character-based Tokenization 是将文本按字符切割,每个 token 就是一个字符。该方法通常用于字符级别的任务。

原理

  • 文本中的每个字符都成为一个独立的 token。例如,句子 "hello" 会被分割为:
    • ["h", "e", "l", "l", "o"]

优缺点

  • 优点
    • 无需词汇表:这种方法可以处理任何文本,尤其适合处理拼写错误、未知词、特殊字符等。
    • 更细粒度:能够捕捉每个字符的细节,适合需要精确字符级别理解的任务。
  • 缺点
    • 计算量大:长文本将产生非常多的 tokens,导致计算效率降低。
    • 语义信息不足:无法捕捉词汇之间的高层次语义信息,只能通过字符级别的训练来学习。

适用场景

  • 适用于需要对字符级别进行建模的任务,如拼写纠错、命名实体识别、字符生成等。
4.3 Subword-based Tokenization(基于子词的分词)

Subword-based Tokenization 是将文本分割成子词(subwords)或词根。子词通常由较小的语言单元构成,如词缀、词干、常见的词根等。该方法广泛用于现代预训练语言模型中,如 BERTGPTT5

原理

  • 该方法通常通过 Byte Pair Encoding (BPE)WordPieceSentencePiece 等算法,将单词拆分为更小的单位。这些单位可以是词根、词缀或高频词段。
  • 例如,unhappiness 可以被拆分为 ["un", "happiness"]["un", "happi", "ness"]

在这里插入图片描述

常见的算法:

  • Byte Pair Encoding (BPE):逐步合并频率最高的字符对形成新的子词单元。
  • WordPiece:通过计算每个子词出现的频率,合并最常见的字符对。
  • SentencePiece:通过无监督的方式自动学习子词分词,不依赖空格进行分词。

优缺点

  • 优点
    • 解决了 OOV 问题:通过拆分单词,可以处理未登录词,并生成新词的子词单元。
    • 词汇表更小:相比词级分词,子词级分词生成的词汇表较小,且能有效涵盖更多的语言现象。
    • 更高效:子词单元能够高效处理词形变化(如 “running” 和 “run”)以及复合词。
  • 缺点
    • 较为复杂:分词过程比词级分词更复杂,需要通过算法生成子词单元,可能导致训练和推理的计算量增加。
    • 语义模糊:某些情况下,过于细粒度的子词可能会使得语义信息变得模糊,需要模型更强的上下文理解能力。

适用场景

  • 现代大模型(如 BERT、GPT、T5)广泛采用这种分词方式,尤其适用于机器翻译、文本生成、情感分析、问答系统等任务。
  • 适用于多语言支持、处理长尾词汇等复杂场景。

4.4 总结:三种分词方法的比较
特性 Word-based Tokenization Character-based Tokenization Subword-based Tokenization
颗粒度 词(词级别) 字符(字符级别) 子词(子词级别)
处理 OOV 问题 不能很好处理未登录词 处理 OOV 很好 通过子词分割有效处理未登录词
词汇表大小 较大,包含所有词 无需预定义词汇表 较小,可覆盖大量语言现象
计算效率 高,处理速度快 低,计算复杂度高 中,依赖算法,性能介于两者之间
语义捕捉能力 中,能够捕捉常见词汇的语义 低,难以捕捉词汇之间的语义关系 高,能够处理词形变化和复合词
适用场景 文本分类、情感分析、NER 等 拼写检查、命名实体识别、字符生成 机器翻译、文本生成、问答系统

5. 常见的 Subword Tokenization 算法

为了实现这种分词方法,有几种常见的算法,这些算法在处理文本时通过不同的方式将词分解成更小的子词单元。

5.1 Byte Pair Encoding (BPE)

定义

  • Byte Pair Encoding (BPE) 是一种基于频率的合并算法,最初用于数据压缩。其核心思想是通过反复合并文本中最常见的字符对来构建子词单元。
  • 该方法通过合并最频繁出现的字符对,不断构建新的子词单元,直到达到预定的词汇表大小。

实现流程

  1. 初始化:每个单词被视为字符的序列。例如,“lower” → ['l', 'o', 'w', 'e', 'r']
  2. 统计字符对频率:计算所有字符对(如 l o, o w, w e 等)在文本中的出现频率。
  3. 合并最频繁的字符对:选择出现频率最高的字符对(例如 l o),将其合并为一个新的子词(例如 lo),然后更新词汇表。
  4. 重复:反复执行以上步骤,直到达到预定的词汇表大小。

【示例】

  • 词频统计:

    对语料库进行词频统计:

    ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
    
  • 基本词表构建:

    基于语料库构建词表如下:

    ['b', 'g', 'h', 'n', 'p', 's', 'u']
    
  • 根据基本词表切词:

    ('h' 'u' 'g', 10), ('p' 'u' 'g', 5), ('p' 'u' 'n', 12), ('b' 'u' 'n', 4), ('h' 'u' 'g' 's', 5)
    
  • 统计词内相邻Token组成的pair出现频率:

    hu:15, ug:20, pu:17, un:16, bu:4, gs: 5
    
  • 词表合并:

    添加最高频pair到词表:应该可以看的出来, u g : 20 ug:20 ug:20 词频最高

    合并 u g ug ug 到词表,于是词表变成了:

    ['b', 'g', 'h', 'n', 'p', 's', 'u', 'ug']
    

    同步语料库切词,变成了:

    ('h' 'ug', 10), ('p' 'ug', 5), ('p' 'u' 'n', 12), ('b' 'u' 'n', 4), ('h' 'ug' 's', 5)
    
  • 继续统计词内相邻Token组成的pair出现频率:

    注意,这里就有点开始循环操作的意思了。

    hug:15, pug:5, pu:12, un:16, bu:4, ugs:5
    
  • 继续词表合并:

    添加最高频pair到词表:应该可以看的出来, u n : 16 un:16 un:16 词频最高

    合并 u n un un 到词表,于是词表变成了:

    ['b', 'g', 'h', 'n', 'p', 's', 'u', 'ug', 'un']
    

    同步语料库切词,变成了:

    ('h' 'ug', 10), ('p' 'ug', 5), ('p' 'un', 12), ('b' 'un', 4), ('h' 'ug' 's', 5)
    
  • 再继续统计词内相邻Token组成的pair出现频率:

    hug:15, pug:5, pun:12, bun:4, ugs:5
    
  • 再继续词表合并:

    添加最高频pair到词表:应该可以看的出来, h u g : 15 hug:15 hug:15 词频最高

    合并 h u g hug hug 到词表,于是词表变成了:

    ['b', 'g', 'h', 'n', 'p', 's', 'u', 'ug', 'un', 'hug']
    

    同步语料库切词,变成了:

    ('hug', 10), ('p' 'ug', 5), ('p' 'un', 12), ('b' 'un', 4), ('hug' 's', 5)
    
  • 完成词表更新

    当达到BPE合并次数之后,停止合并,这就是最终的Token词表。

    比如GPT的词汇表大小为40478,因为它有478个基本字符,并且在40000次合并后停止。

  • 词表使用

    bug => ['b', 'ug']
    mug => ['<unk>', 'ug']
    hugs=>[9, 5]
    

    没在词表里面的Token用表示。

优缺点

  • 优点
    • 处理 OOV 问题:能够通过合并字符对有效地处理未登录词。
    • 词汇表大小可控:与传统的词级分词相比,BPE 的词汇表更小。它通过合并最常见的字符对生成子词单元,因此能够有效减少词汇表的大小。
    • 灵活性:能够处理各种复杂的词形变化和复合词。
  • 缺点
    • 词汇表较大:虽然相较于词级分词小,但词汇表大小仍然较大。
    • 无法处理句法结构:BPE 是基于频率统计的,而非语法结构,因此有时可能生成不自然的子词单元。

应用

  • GPT 系列模型(如 GPT-2、GPT-3)使用 BPE 进行分词。

5.2 BBPE

BBPE双向字节对编码)是 BPE(Byte Pair Encoding)的一种扩展,旨在进一步优化子词分词过程,特别是在处理多语言文本时。BBPE 的设计灵感来自于传统的 BPE,但它通过改进的双向合并策略和模型,提升了处理能力和效率。

BBPE 的关键思想是使用 双向 合并策略,结合 BPE 的优点,但在模型训练过程中进行一定的改进。使用字节(byte)作为初始token

【示例】语料:深度学习需要一定的学习深度

text.encode('utf-8') 将字符串转换为 UTF-8 字节序列(bytes 类型)。
list() 将 bytes 对象转换为整数列表(每个字节对应一个 0~255 的数字)。
bytes(bytes_data).decode('utf-8')将UTF-8字节序列(bytes 类型)转换为字符串 。
a="深度学习需要一定的学习深度"
a=a.encode('utf-8')
a=list(a)
print(a)#[230, 183, 177, 229, 186, 166, 229, 173, 166, 228, 185, 160, 233, 156, 128, 232, 166, 129, 228, 184, 128, 229, 174, 154, 231, 154, 132, 229, 173, 166, 228, 185, 160, 230, 183, 177, 229, 186, 166]

result = bytes([230, 183, 177]).decode('utf-8')
print(result)#深

预处理

首先将句子转换为UTF-8编码的字节序列,十进制表示:

230 183 177 229 186 166 229 173 166 228 185 176 233 156 128 232 166 129 228 184 128 229 174 154 231 154 132 229 173 166 228 185 176 230 183 177 229 186 166

初始化词汇表

初始词汇表为所有唯一的字节 0 − 255 0-255 0255,但此处仅包含语料中出现的字节:

{128, 129, 132, 154, 156, 166, 171, 173, 174, 177, 183, 184, 185, 186, 228, 229, 230, 231, 232, 233}

初始分词结果

每个字节单独成词

230 183 177 | 229 186 166 | 229 173 166 | 228 185 176 | 233 156 128 | 232 166 129 | 228 184 128 | 229 174 154 | 231 154 132 | 229 173 166 | 228 185 176 | 230 183 177 | 229 186 166

统计字节对频率

遍历所有相邻的字节对,统计出现频率:

字节对 频率
(230, 183) 2
(183, 177) 2
(229, 186) 2
(186, 166) 2
(229, 173) 1

合并最高频字节对

选择频率最高的字节对进行合并,如 ( 230 , 183 ) (230, 183) (230,183)
合并操作

  • 230    183 230 \:\: 183 230183 替换为新符号 230 _ 183 230\_183 230_183

  • 为该符号新分配一个ID,如 256 256 256

  • 更新词汇表:新增 256 = 230 _ 183 256 = 230\_183 256=230_183

  • 此时,我们可以更新分词结果了

    256 177 | 229 186 166 | 229 173 166 | 228 185 176 | 233 156 128 | 232 166 129 | 228 184 128 | 229 174 154 | 231 154 132 | 229 173 166 | 228 185 176 | 256 177 | 229 186 166
    

迭代合并

重复统计和合并,直到达到预设的合并次数或词汇表大小。

第二次合并

  • 统计当前字节对频率,如 ( 256 , 177 ) (256, 177) (256,177) 出现 2 2 2 次。
  • 合并 256    177 256 \:\: 177 256177 为新符号 257 = 256 _ 177 257 = 256\_177 257=256_177

更新后的分词结果:

257 | 229 186 166 | 229 173 166 | 228 185 176 | ... | 257 | 229 186 166

第三次合并

  • 合并 ( 229 , 186 ) (229, 186) (229,186)
  • 新符号 258 = 229 _ 186 258 = 229\_186 258=229_186

更新后的分词结果:

257 | 258 166 | 229 173 166 | 228 185 176 | ... | 257 | 258 166

最终词汇表

经过多次合并后,词汇表会包含初始字节和常见组合:

  • 初始字节: 230 , 183 , 177 , 229 , 186 , . . . 230, 183, 177, 229, 186, ... 230,183,177,229,186,...
  • 合并后的符号:
    • 256 = 230 _ 183 256 = 230\_183 256=230_183
    • 257 = 256 _ 177 257 = 256\_177 257=256_177
    • 258 = 229 _ 186 258 = 229\_186 258=229_186
    • 259 = 258 _ 166 259 = 258\_166 259=258_166

符号的层级关系

257 = 256 _ 177 257 = 256\_177 257=256_177,而 256 = 230 _ 183 256 = 230\_183 256=230_183,因此 257 257 257 实际表示 230 _ 183 _ 177 230\_183\_177 230_183_177,即完整的"深"的UTF-8字节序列。

原始句子编码为:

257 259 | 229 173 166 | 228 185 176 | ... | 257 259

其中 257 257 257 表示"深", 259 259 259 表示"度"

当然了, ( 257 , 259 ) (257, 259) (257,259) 出现 2 2 2 次,下一步合并 ( 257 , 259 ) (257, 259) (257,259) → 新符号 260 = 257 _ 259 260 = 257\_259 260=257_259,即"深度"的完整Token。

5.3 WordPiece

定义

  • WordPiece 是 Google 提出的子词分词方法,最早用于 BERT。它和 BPE 类似,但其核心思想是基于 最大似然估计(MLE) 来选择最合适的子词合并,而不是单纯依靠频率。
  • 通过计算每个子词的条件概率,WordPiece 可以构建出一个更加符合语料分布的词汇表。具体来说,它计算每个可能的子词合并的概率,并选择提高模型似然度的合并操作。

实现流程

  1. 初始化:从字符级别开始,每个单词分解为单独的字符(例如,unhappiness['u', 'n', 'h', 'a', 'p', 'p', 'i', 'n', 'e', 's', 's'])。

  2. 统计频率:计算文本中每对相邻字符出现的频率。

    • 与BPE类似,但是算法略有不同:将 p a i r pair pair 的频率除以其单个 t o k e n token token 的频率乘积
      pair 得分 = pair 出现的次数 token1 出现的次数 × token2 出现的次数 \text{pair 得分} = \frac{\text{pair 出现的次数}}{\text{token1 出现的次数} \times \text{token2 出现的次数}} pair 得分=token1 出现的次数×token2 出现的次数pair 出现的次数

      • 该算法优先考虑单个 t o k e n token token 在词表中不太频繁的 p a i r pair pair 进行合并
  3. 合并最频繁的字符对:根据最大似然估计,选择出现频率最高的字符对进行合并(例如,'h''a' 合并为 'ha')。

  4. 更新词汇表:将合并后的子词加入词汇表,并用新的子词替代原词中的字符对。

  5. 重复合并过程:重复进行字符对的合并操作,直到达到设定的词汇表大小或无法进一步合并为止。

【示例】

  • 词频统计:

    对语料库进行词频统计:

    ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
    
  • 基本词表构建:

    基于语料库构建词表如下:

    ['b', 'h', 'p', '##g', '##n',  '##s', '##u']
    
  • 根据基本词表切词:

    ('h' '##u' '##g', 10), ('p' '##u' '##g', 5), ('p' '##u' '##n', 12), ('b' '##u' '##n', 4), ('h' '##u' '##g' '##s', 5)
    
  • 统计频率:

    • 词内相邻Token组成的pair出现频率

      hu:15, ug:20, pu:17, un:16, bu:4, gs:5
      
    • 每个token出现频率

      h:15, u:36, g:20, p:17, n:16, b:4, s:5
      
  • 计算pair得分

    符号对 计算公式 得分(近似值)
    hu 15 15 × 36 \frac{15}{15 \times 36} 15×3615 0.0278
    ug 20 36 × 20 \frac{20}{36 \times 20} 36×2020 0.0278
    pu 17 17 × 36 \frac{17}{17 \times 36} 17×3617 0.0278
    un 16 36 × 16 \frac{16}{36 \times 16} 36×1616 0.0278
    bu 4 4 × 36 \frac{4}{4 \times 36} 4×364 0.0278
    gs 5 20 × 5 \frac{5}{20 \times 5} 20×55 0.05
  • 词表合并:

    添加概率 p a i r pair pair 到词表:应该可以看的出来, g s : 0.05 gs:0.05 gs:0.05 词频最高

    合并 g s gs gs 到词表,于是词表变成了:

    ['b', 'h', 'p', '##g', '##n',  '##s', '##u', '##gs']
    

    同步语料库切词,变成了:

    ('h' '##u' '##g', 10), ('p' '##u' '##g', 5), ('p' '##u' '##n', 12), ('b' '##u' '##n', 4), ('h' '##u' '##gs', 5)
    

训练完成后,对新的单词进行分词的算法如下:

采用贪婪最长匹配优先算法

  • 将单词拆分为字符序列作为初始子词序列
  • 从词汇表中找出最长的可能子词:
    • 从单词开始位置开始
    • 找到词汇表中存在的最长子词
  • 对该子词进行分割
  • 对剩余部分重复上述过程
  • 如果无法匹配任何子词,则将该部分标记为未知(UNK)

设词汇表如下:

["un", "##able", "##ing", "##e", "##d", "re", "##run", "##runing", "run"]

分词 u n r u n n i n g unrunning unrunning 的过程:

  • 匹配最长前缀 u n un un,剩余 r u n n i n g running running
  • 匹配最长前缀 r u n run run,剩余 n i n g ning ning
  • 无法匹配 n i n g ning ning,回退到字符级:
    • 匹配 n n n + i n g ing ing

最终分词结果:

["un", "run", "##n", "##ing"]

优缺点

  • 优点
    • OOV 处理:与 BPE 一样,能够有效处理未登录词。
    • 优化语言模型:通过最大似然估计,可以让模型学习到更加合适的子词单元,提高生成文本的质量。
    • 词汇表较小:通过子词的合并,WordPiece 能够有效地控制词汇表的大小,使其比基于词级分词的模型小得多,但仍然能够覆盖大多数常见单词。
  • 缺点
    • 计算复杂:需要执行概率计算,相比于 BPE,计算量较大。
    • 语法信息弱:与 BPE 类似,WordPiece 也并非通过语言学知识进行分词,仍然依赖于频率和概率统计。

应用

  • BERT 和 ALBERT 使用 WordPiece 进行分词。

5.4 Unigram

它的核心思想与 BPE 不同,BPE 是基于频率的合并规则,而 Unigram 是基于概率模型的选择。

它假设一个给定的词或子词是 独立 生成的,即不考虑上下文信息,所有的词汇项都是基于它们的单独概率来进行建模的。它通过选择每个子词的概率来决定哪些子词将被用作词汇表的一部分,并通过训练来优化这些概率。

在这里插入图片描述

Unigram 是 子词分词算法(subword tokenization) 中的一种方法,和 BPE、WordPiece、SentencePiece 一样,常用于大模型的 Tokenizer 设计。它的核心思想与 BPE 不同,BPE 是基于频率的合并规则,而 Unigram 是基于概率模型的选择。

Unigram 假设整个训练语料中的文本都是由一个固定的 子词词表(subword vocabulary) 按照一定概率生成的。也就是说,每个子词单位都是从一个概率分布里独立抽样出来的,因此叫 Unigram(单元语法模型)

模型的目标就是找到一套最优的子词词表,使得在给定语料时,文本被分解成子词序列的概率最大。

训练 Unigram Tokenizer 的过程可以分为以下几个步骤:

  1. 初始化词表

    • 一开始使用 BPE 算法先训练一个很大的候选词表,比如包含所有可能的字符 n-gram(从单字符到更长的组合)。
    • 例如对于 “internationalization”,候选子词可能包括 “i”、“n”、“in”、“inter”、“nation”、“al”、“ization” 等。
  2. 计算概率

    • 给定词表 V V V,对每个句子 X X X,它可能有多种分词方式。

    • 句子的概率是所有可能分词方式的概率和:
      P ( X ) = ∑ S ∈ Segmentations ( X ) ∏ w ∈ S P ( w ) P(X) = \sum_{S \in \text{Segmentations}(X)} \prod_{w \in S} P(w) P(X)=SSegmentations(X)wSP(w)
      其中 S S S 是一种切分方式, w w w 是切分出来的子词。

  3. 优化过程

    • 使用 EM(期望最大化)算法或者变体,反复估计子词的概率分布。
    • 如果某些子词在分词中很少被使用,就会被逐渐淘汰。
    • 逐渐缩小词表,直到收敛或达到预设的词表大小。

对语料库进行词频统计:

("hug", 10), ("pug", 12), ("lug", 5), ("bug", 4), ("dug", 5)

基本词表构建

基于语料库构建词表如下:

['h', 'u', 'g', 'l', 'p', 'b', 'd', 'u', 'hu', 'lu', 'bu', 'ug', 'pu', 'du']

词表token频率

['h':10/180, 'u':36/180, 'g':36/180, 'l':5/180, 'p':12/180, 'b':4/180, 'd':5/180, 'ug':36/180, 'pu':12/180, 'hu':10/180, 'lu':5/180, 'du':5/180, 'bu':4/180]
5.4.1 EM

使用EM(期望最大化)算法训练子词概率:

  • unigram模型假设每个词都是独立的。

    在这里插入图片描述

    根据分词计算hug所有可能的概率:
    h       u     g    : 10 180 × 36 180 × 36 180 = 2.22 × 1 0 − 3 hu     g    : 10 180 × 36 180 = 1.11 × 1 0 − 2 h     ug    : 10 180 × 36 180 = 1.11 × 1 0 − 2 hug    : 0 \textbf{h \: u \:g} \: \: : \quad \frac{10}{180} \times \frac{36}{180} \times \frac{36}{180} = 2.22 \times 10^{-3} \\ \textbf{hu\: g} \: \: : \quad \frac{10}{180} \times \frac{36}{180} = 1.11 \times 10^{-2} \\ \textbf{h\: ug} \: \: : \quad \frac{10}{180} \times \frac{36}{180} = 1.11 \times 10^{-2} \\ \textbf{hug} \: \: : \quad 0  u g18010×18036×18036=2.22×103hu g18010×18036=1.11×102h ug18010×18036=1.11×102hug0

  • 用 Viterbi 算法(动态规划)找出最大概率路径。

S ∗ = arg ⁡ max ⁡ S ∏ i = 1 n P ( w i ) S^* = \arg\max_S \prod_{i=1}^{n} P(w_i) S=argSmaxi=1nP(wi)

如果遇到概率相等的则随机选一个:

Corpus Splits Scores
“hug”, 10 hu g 1.11e-02
“pug”, 12 pu g 1.33e-02
“lug”, 5 lu g 5.56e-03
“bug”, 4 bu g 4.44e-03
“dug”, 5 du g 5.56e-03

评估Loss

使用负对数最大似然,对所有可能切分路径的概率总和计算 l o s s loss loss,即:
Unigram loss = − ∑ x ∈ Corpus log ⁡ P ( x ) \text{Unigram loss} = - \sum_{x \in \text{Corpus}} \log P(x) Unigram loss=xCorpuslogP(x)
其中: P ( x ) = ∑ S ∈ All Segmentations of  x ∏ w i ∈ S P ( w i ) P(x) = \sum_{S \in \text{All Segmentations of } x} \prod_{w_i \in S} P(w_i) P(x)=SAll Segmentations of xwiSP(wi)

于是就可以评估loss了:
10 × ( − log ⁡ ( 1.11 × 1 0 − 2 ) ) 10 \times (-\log(1.11 \times 10^{-2})) 10×(log(1.11×102))
+ 12 × ( − log ⁡ ( 1.33 × 1 0 − 2 ) ) + 12 \times (-\log(1.33 \times 10^{-2})) +12×(log(1.33×102))
+ 5 × ( − log ⁡ ( 5.56 × 1 0 − 3 ) ) + 5 \times (-\log(5.56 \times 10^{-3})) +5×(log(5.56×103))
+ 4 × ( − log ⁡ ( 4.44 × 1 0 − 3 ) ) + 4 \times (-\log(4.44 \times 10^{-3})) +4×(log(4.44×103))
+ 5 × ( − log ⁡ ( 5.56 × 1 0 − 3 ) ) + 5 \times (-\log(5.56 \times 10^{-3})) +5×(log(5.56×103))
= 170.4 = \mathbf{170.4} =170.4

模拟删除

对于词表中每个子词 w w w,假设将其删除,评估其对整体损失的影响:

  • 模拟删除 w w w,重新运行EM,并得到新的 l o s s loss loss

  • 记录该子词的 “损失增量”:
    Δ L ( w ) = L new − L old \Delta L(w) = L_{\text{new}} - L_{\text{old}} ΔL(w)=LnewLold

这是原始词表:

['h':10/180, 'u':36/180, 'g':36/180, 'l':5/180, 'p':12/180, 'b':4/180, 'd':5/180, 'ug':36/180, 'pu':12/180, 'hu':10/180, 'lu':5/180, 'du':5/180, 'bu':4/180]

第一次迭代:

模拟删除 ‘ug’:36/180 ,那么 ug 的概率就是 0 0 0,于是就有了:
h       u     g    : 10 180 × 36 180 × 36 180 = 2.22 × 1 0 − 3 hu     g    : 10 180 × 36 180 = 1.11 × 1 0 − 2 h     ug    : 10 180 × 0 = 0 hug    : 0 \textbf{h \: u \:g} \: \: : \quad \frac{10}{180} \times \frac{36}{180} \times \frac{36}{180} = 2.22 \times 10^{-3} \\ \textbf{hu\: g} \: \: : \quad \frac{10}{180} \times \frac{36}{180} = 1.11 \times 10^{-2} \\ \textbf{h\: ug} \: \: : \quad \frac{10}{180} \times 0 = 0 \\ \textbf{hug} \: \: : \quad 0  u g18010×18036×18036=2.22×103hu g18010×18036=1.11×102h ug18010×0=0hug0
最大概率路径得分:

Corpus Splits Scores
“hug”, 10 hu g 1.11e-02
“pug”, 12 pu g 1.33e-02
“lug”, 5 lu g 5.56e-03
“bug”, 4 bu g 4.44e-03
“dug”, 5 du g 5.56e-03

计算loss:
10 × ( − log ⁡ ( 1.11 × 1 0 − 2 ) ) 10 \times (-\log(1.11 \times 10^{-2})) 10×(log(1.11×102))
+ 12 × ( − log ⁡ ( 1.33 × 1 0 − 2 ) ) + 12 \times (-\log(1.33 \times 10^{-2})) +12×(log(1.33×102))
+ 5 × ( − log ⁡ ( 5.56 × 1 0 − 3 ) ) + 5 \times (-\log(5.56 \times 10^{-3})) +5×(log(5.56×103))
+ 4 × ( − log ⁡ ( 4.44 × 1 0 − 3 ) ) + 4 \times (-\log(4.44 \times 10^{-3})) +4×(log(4.44×103))
+ 5 × ( − log ⁡ ( 5.56 × 1 0 − 3 ) ) + 5 \times (-\log(5.56 \times 10^{-3})) +5×(log(5.56×103))
= 170.4 = \mathbf{170.4} =170.4

继续n次迭代后得出:

Vocabulary Loss 损失增量
With all vocabulary 170.4 -
Without 消融实验 - -
ug 170.4 0
pu 170.4 0
hu 170.4 0
lu 170.4 0
du 170.4 0
bu 170.4 0

loss均无变化,那就随机删除一个,此时就不考虑百分比问题了。

词表更新

这是更新后的词表:

['h':10/144, 'u':36/144, 'g':36/144, 'l':5/144, 'p':12/144, 'b':4/144, 'd':5/144, 'pu':12/144, 'hu':10/144, 'lu':5/144, 'du':5/144, 'bu':4/144]

然后基于新词表继续模拟删除: hu
h       u     g    : 10 144 × 36 144 × 36 144 = 4.34 × 1 0 − 3 hu     g    : 0 × 36 144 = 0 h     ug    : 10 144 × 0 = 0 hug    : 0 \textbf{h \: u \:g} \: \: : \quad \frac{10}{144} \times \frac{36}{144} \times \frac{36}{144} = 4.34 \times 10^{-3} \\ \textbf{hu\: g} \: \: : \quad 0 \times \frac{36}{144} = 0 \\ \textbf{h\: ug} \: \: : \quad \frac{10}{144} \times 0 = 0 \\ \textbf{hug} \: \: : \quad 0  u g14410×14436×14436=4.34×103hu g0×14436=0h ug14410×0=0hug0
最大概率路径得分:

Corpus Splits Scores
“hug”, 10 h u g 4.34e-03
“pug”, 12 pu g 2.08e-02
“lug”, 5 lu g 8.68e-03
“bug”, 4 bu g 6.94e-03
“dug”, 5 du g 8.68e-03

计算loss:

10 × ( − log ⁡ ( 4.34 × 1 0 − 3 ) ) 10 \times (-\log(4.34\times 10^{-3})) 10×(log(4.34×103))
+ 12 × ( − log ⁡ ( 2.08 × 1 0 − 2 ) ) +12 \times (-\log(2.08 \times 10^{-2})) +12×(log(2.08×102))
+ 5 × ( − log ⁡ ( 8.68 × 1 0 − 3 ) ) +5 \times (-\log(8.68 \times 10^{-3})) +5×(log(8.68×103))
+ 4 × ( − log ⁡ ( 6.94 × 1 0 − 3 ) ) +4 \times (-\log(6.94 \times 10^{-3})) +4×(log(6.94×103))
+ 5 × ( − log ⁡ ( 8.68 × 1 0 − 3 ) ) +5 \times (-\log(8.68 \times 10^{-3})) +5×(log(8.68×103))
= 168.2 \mathbf{168.2} 168.2

词表剪枝

如此搞整下来,他们的增量损失就会有个排名:

  • 对所有子词计算 Δ L ( w ) \Delta L(w) ΔL(w),从中选择使损失增量最小的前 40 % 40\% 40% 的子词进行删除,当然比例可设定。
  • 剪枝后,留下质量较高、对概率影响较小的子词。
  • 计算模拟删除后的loss:

在这里插入图片描述
把排名后 p % p\% p% t o k e n token token 从词表删除,如 40 % 40\% 40%
在这里插入图片描述

注:不删除基础token,如26个字母和标点

继续EM

  • 在剪枝后的词表上重新运行 EM 迭代几轮,使概率重新收敛。
  • 如此反复,直到达到目标词表大小。

特点

  • 基于概率,而不是贪心合并(不同于 BPE 的逐步合并)。
  • 可以保留多种可能的切分方式,解码时可用 Viterbi 算法找到最优路径。
  • 训练得到的词表更符合语言统计规律,尤其在多语言和低资源场景下更鲁棒。
  • SentencePiece 默认实现的就是 Unigram 或 BPE。
5.5 SentencePiece

定义

  • SentencePiece 是由 Google 提出的基于子词的分词方法,与 BPE 和 WordPiece 不同的是,SentencePiece 不依赖于空格分隔符,能够处理没有显式词边界的文本,它采用 无监督学习 的方式来生成子词单元,自动学习出最佳的子词单位
  • 它是通过构建一个语言模型(如 unigram language model)来最大化训练数据的概率,从而得到一组最佳的子词单元。

实现流程

  1. 训练数据:输入的文本数据并不要求按词分割,SentencePiece 自动从文本中学习子词单元。
  2. 无监督建模:利用 unigram language model 来选择最合适的子词。
  3. 词汇表构建:生成的子词单元构成最终的词汇表,这些子词单元可以是字符、词根或词缀等。
  4. 合并子词:通过优化语言模型的概率来构建词汇表,逐步合并子词单位。

【示例】

我喜欢学习人工智能

步骤 1

添加空格标记:在句子开头或句中适当位置加 ,表示词或短语的开头。

"我喜欢学习人工智能" → "▁我喜欢学习人工智能"

步骤 2

训练后得到的子词词表,随便假设的:

['▁我', '喜欢', '学习', '人工', '智能', '人工智能', '我喜欢']

步骤 3

分词结果:根据词表拆分成概率最大的一种方式

句子:“ 我喜欢学习人工智能”

  • 如果词表中有“人工智能”:

    ['▁我', '喜欢', '学习', '人工智能']
    
  • 如果词表中没有“人工智能”,但有“人工”和“智能”:

    ['▁我', '喜欢', '学习', '人工', '智能']
    
  • 如果都没有,就进一步拆分:

    ['▁我', '喜', '欢', '学', '习', '人', '工', '智', '能']
    

优缺点

  • 优点
    • 不依赖空格:SentencePiece 可以用于各种语言,特别是对像中文这样的不以空格分割单词的语言处理非常有效。
    • 无监督:不依赖于现成的分词工具,可以直接从原始文本中学习。
  • 缺点
    • 复杂度较高:与 BPE 和 WordPiece 相比,SentencePiece 的实现相对复杂。
    • 不一定符合语言习惯:由于完全依赖模型的训练,它可能生成一些不符合语言习惯的子词。

应用

  • T5、BART 和 MASS 等模型使用 SentencePiece 作为其子词分词算法。
  • 也广泛应用于多语言模型,如 mBART

5.6 总结:常见的 Subword Tokenization 算法
算法 工作原理 优点 缺点 典型应用场景
BPE 贪心频率合并字符对 简单高效,适用于形态简单语言 忽略了长词和复合词的结构 英语等形态简单语言的机器翻译
BBPE BPE 改进,考虑信息平衡性 捕捉长词和复合词,减少信息丢失 计算复杂,训练时间长 复杂语言(如中文、日文)
WordPiece 最大化语言模型概率 适应性强,生成高效的词表 算法复杂,计算资源需求高 BERT、RoBERTa、DistilBERT 等
Unigram 基于概率模型的子词分词 更好适应复杂语言,低资源语言效果好 训练时间长,可能生成冗余词表 低资源语言、多语言任务
SentencePiece 无监督训练,支持 BPE/Unigram 支持无监督训练,跨语言适应性强 生成词表可能不精确 T5、BART、ALBERT 等
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐