Subword分词方法的BPE与BBPE

目录

一、BPE

[1.1 核心思想](#1.1 核心思想)

[1.1.1 训练](#1.1.1 训练)

[1.1.2 编码](#1.1.2 编码)

[1.1.3 总结](#1.1.3 总结)

[1.2 案例助解](#1.2 案例助解)

[1.2.1 训练阶段](#1.2.1 训练阶段)

[1.2.2 小结](#1.2.2 小结)

[1.2.3 编码阶段](#1.2.3 编码阶段)

[1.3 优点](#1.3 优点)

[1.4 存在的问题](#1.4 存在的问题)

[1.5 应用场景](#1.5 应用场景)

二、BBPE

[2.1 BBPE vs. BPE](#2.1 BBPE vs. BPE)

[2.2 工作原理](#2.2 工作原理)

[2.3 实例助解](#2.3 实例助解)

[2.3.1 预处理](#2.3.1 预处理)

[2.3.2 初始化词汇表](#2.3.2 初始化词汇表)

[2.3.3 初始分词结果](#2.3.3 初始分词结果)

[2.3.4 第1次统计字节对频率](#2.3.4 第1次统计字节对频率)

[2.3.5 合并最高频字节对](#2.3.5 合并最高频字节对)

[2.3.6 第2次统计字节对频率](#2.3.6 第2次统计字节对频率)

[2.3.7 第3次统计字节对频率](#2.3.7 第3次统计字节对频率)

[2.3.8 最终词汇表](#2.3.8 最终词汇表)

[2.3.9 编码](#2.3.9 编码)

[2.3.10 总结](#2.3.10 总结)

[2.4 优点](#2.4 优点)

[2.5 应用场景](#2.5 应用场景)


人类在阅读时能自然地通过空格(如英文)、语义逻辑(如中文)识别词边界,但计算机无法直接 "理解" 连续的字符流。例如:中文句子 "我爱自然语言处理" 若不切分,计算机看到的是连续的汉字 我爱自然语言处理,无法区分 "自然""语言""处理" 这些独立语义单元。

分词通过将文本拆分为机器可处理的 "最小语义单元",为后续模型(如神经网络、统计模型)提供了分析的 "基本素材",让机器能逐步从 Token 中学习语言的语法、语义规律。

在自然语言处理中,Tokenizer(分词器)的实现方式多种多样,常见的三种核心实现方式分别是基于规则的分词(Rule-Based)基于统计的分词(Statistical)基于子词(Subword)的分词

其中基于子词的分词 核心思想 :将高频词作为完整 Token,低频词拆分为更短的子词单元(Subword),平衡词表大小和 OOV 问题。其理论基础是:低频词可由高频子词组合而成(如 "unhappiness" 可拆分为 "un-""happiness" 或 "un-""happy""-ness")。

Subword 分词有很多实现的算法,本篇将基于笔者的理解详细介绍BPE和BBPE算法的原理。

Subword 分词方法 典型模型
BPE/BBPE GPT, DeepSeek, GPT-2, GPT-Neo, RoBERTa, LLaMA
WordPiece BERT, DistilBERT, MobileBERT
Unigram AIBERT, T5, mBART, XLNet

一、BPE

Byte-Pair Encoding,字节对编码。

1.1 核心思想

从最基本单元(字符)开始,通过迭代地合并最频繁共现的相邻符号对,逐步形成更大的、有意义的子词单元。

BPE算法包含两个主要阶段:1) 训练2) 编码

1.1.1 训练

这个阶段的目的是从训练语料库中学习一系列的合并规则。包含词频统计词表合并两部分:通过迭代合并高频的相邻字符对来生成子词。

输入 :一个大型的文本语料库。 输出:一个最终词汇表(Vocabulary)和一系列合并规则。

步骤

  1. 预处理 :将文本拆分成单词(可以加上特殊的结束符如 </w> 来标记单词边界,这有助于模型区分像"apple"和"app"这样的词)。

  2. 初始化基础词汇表 :将每个单词拆分为字符(包括</w>),并统计每个单词的频率。

  3. 迭代合并

  • 计算所有相邻符号对(Pairs)的频率。在初始阶段,符号就是字符。

  • 找到频率最高的符号对。

  • 将这个最高频的符号对合并成一个新的符号,并将这个新符号添加到词汇表中。

  • 在所有单词中,用这个新符号替换该符号对的所有出现。

  • 重复这个过程,直到完成了预定的合并次数(num_merges)或者词汇表达到了预定的大小。

1.1.2 编码

当我们有一份新文本需要处理时,就使用训练阶段学到的合并规则来对其进行分词。

输入 :一个新句子(或单词)。 输出:该句子分解后的子词标记(Tokens)序列。

步骤

  1. 预处理 :将单词拆分为字符,并加上结束符 </w>

  2. 应用合并规则 :按照训练时学到的顺序,遍历所有合并规则。对于每一条规则,检查当前符号序列中是否存在可以合并的符号对,如果存在就进行合并。

  3. 终止:当没有更多的合并规则可以应用,或者无法再合并时,停止过程。此时得到的符号序列就是最终的分词结果。

1.1.3 总结

具体过程简化为:

  1. 初始状态:将所有文本拆分为最小单元(如单个字符),并在每个词末尾添加特殊符号
    (如</w>)标识词边界。
  2. 统计频率:计算所有相邻单元对的出现次数。
  3. 合并高频对:将出现频率最高的一对单元合并成新的子词单元。
  4. 重复迭代:用新生成的子词单元重新统计频率并合并,直到达到预设的子词表大小或迭代次数。

通过这种方式,高频词会保持完整(如英文 "the"、中文 "的"),而低频词会被拆分为多个高频子词(如 "unhappiness" 可能拆分为 "un"、"happiness"),既控制了词表规模,又几乎消除了未登录词(OOV)问题。

1.2 案例助解

这里通过一个案例帮助大家理解BPE算法过程。

1.2.1 训练阶段
  • 原始语料库
复制代码
 hug hug hug hug hug hug hug hug hug hug pug pug pug pug pug pun pun pun pun pun pun pun pun pun pun pun pun bun bun bun bun hugs hugs hugs hugs hugs
  • 词频统计:

    对语料库进行词频统计:

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

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

    复制代码
     ['b', 'g', 'h', 'n', 'p', 's', 'u', '</w>']·
  • 根据基本词表切词:

    复制代码
     ('h' 'u' 'g', '</w>', 10), 
     ('p' 'u' 'g', '</w>', 5), 
     ('p' 'u' 'n', '</w>', 12), 
     ('b' 'u' 'n', '</w>', 4), 
     ('h' 'u' 'g' 's', '</w>', 5)
  • 第1次统计词内相邻Token组成的pair出现频率:

    复制代码
     hu:10, ug:10, g</w>:10
     pu:5, ug:5, g</w>:5
     pu:12, un:12, n</w>:12
     bu:4, un:4, n</w>:4
     hu:5, ug:5, gs:5,s</w>:5
  • 合并频率:

    复制代码
     hu:15, ug:20, g</w>:15, pu:17, un:16, n</w>:16, bu:4, gs:5,s</w>:5
  • 词表合并:

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

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

    复制代码
     ['b', 'g', 'h', 'n', 'p', 's', 'u', '</w>', 'ug']

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

    复制代码
     ('h' 'ug', '</w>', 10), 
     ('p' 'ug', '</w>', 5), 
     ('p' 'u' 'n', '</w>', 12), 
     ('b' 'u' 'n', '</w>', 4), 
     ('h' 'ug' 's', '</w>', 5)
  • 第2次统计词内相邻Token组成的pair出现频率:

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

    复制代码
     hug:15, ug</w>:15, pug:5, pu:12, un:16, n</w>:16,  bu:4, ugs:5, s</w>:5
  • 词表合并:

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

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

    复制代码
     ['b', 'g', 'h', 'n', 'p', 's', 'u', '</w>', 'ug', 'un']

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

    复制代码
     ('h', 'ug', '</w>', 10), 
     ('p', 'ug', '</w>', 5), 
     ('p', 'un', '</w>', 12), 
     ('b', 'un', '</w>', 4), 
     ('h', 'ug', 's', '</w>', 5)
  • 第3次统计词内相邻Token组成的pair出现频率:

    复制代码
     hug:15, ug</w>:15, pug:5, pun:12, un</w>:16, bun:4, ugs:5, s</w>:5 
  • 词表合并:

    添加最高频pair到词表:应该可以看的出来,un</w>:16 词频最高

    合并 un</w>:16 到词表,于是词表变成了:

    复制代码
     ['b', 'g', 'h', 'n', 'p', 's', 'u', 'ug', 'un', 'un</w>']

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

    复制代码
     ('h', 'ug', '</w>', 10), 
     ('p', 'ug', '</w>', 5), 
     ('p', 'un</w>', 12), 
     ('b', 'un</w>', 4), 
     ('h', 'ug', 's', '</w>', 5)
  • 第4次统计词内相邻Token组成的pair出现频率:

    复制代码
     hug:15, ug</w>:15, pug:5, pun</w>:12, bun</w>:4, ugs:5, s</w>:5

    词表合并:

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

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

    复制代码
     ['b', 'g', 'h', 'n', 'p', 's', 'u', 'ug', 'un', 'un</w>', hug]
  • 完成词表更新

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

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

    最终BPE合并规则(按顺序):

    1. "u" + "g" → "ug"

    2. "u" + "n" → "un"

    3. "un" + "</w>" → "un</w>"

    4. "h" + "ug" → "hug"

    最终的词汇表为:

    复制代码
     ['b', 'g', 'h', 'n', 'p', 's', 'u', 'ug', 'un', 'un</w>', hug]
1.2.2 小结

这里的词表训练更新的流程实际也很好理解,就是通过不断计算出现频率最高的一对单元合并成新的子词单元,需要注意的是合并次数的确定。

在 BPE(字节对编码)中,合并次数的确定主要取决于预期的子词表大小具体任务需求,核心是在 "子词表规模" 与 "分词粒度" 之间找到平衡。常见的确定方式有以下几种:

  1. 预设词表大小这是最常用的方法:根据预先设定目标子词表的大小(如 3 万、5 万),然后迭代合并直到子词数量达到该阈值。

    • 词表太小:子词粒度细(甚至接近字符),会增加序列长度,可能丢失语义完整性;
    • 词表太大:子词粒度粗(接近完整词),OOV(未登录词)问题可能复现,且模型参数规模会增加。实际应用中,预训练模型(如 BERT、GPT)通常选择 3-5 万的子词表,兼顾效率与覆盖度。
  2. 基于语料覆盖率当新增合并对带来的 "未覆盖 token 减少量" 低于某个阈值时停止。例如,当 99.9% 的文本都能被现有子词表覆盖时,即可停止合并,避免过度细分。

  3. 经验阈值根据任务特性或历史经验设定固定合并次数。例如,处理多语言时可能需要更多合并次数(如 10 万次)以覆盖不同语言的子词模式;而单一语言任务可适当减少(如 3-5 万次)。

  4. 验证集调优在下游任务(如机器翻译、文本分类)的验证集上测试不同合并次数的效果,选择性能最优的次数。这种方式更贴合具体任务,但计算成本较高。

总之,合并次数的核心作用是控制子词表规模,需根据 "任务类型、语言特性、模型效率需求" 综合确定,没有统一标准,但 3-5 万次合并是 NLP 预训练模型的常用选择。

1.2.3 编码阶段
  • 输入单词: "bug"

  • 预处理: 添加结束标记 → "bug</w>"

  • 初始拆分: ["b", "u", "g", "</w>"]

  • 逐步应用规则:

  1. 应用规则1: "u" + "g" → "ug"

    • 当前位置:["b", "u", "g", "</w>"]

    • 合并"u"和"g" → ["b", "ug", "</w>"]

  2. 检查规则2: 不适用(没有"u"+"n"组合)

  3. 检查规则3: 不适用(没有"un"符号)

  4. 检查规则4: 不适用(需要"h"+"ug",但这里是"b"+"ug")

  • 最终编码结果:

子词序列: 'b' 'ug' '</w>'

1.3 优点

  1. 有效平衡词汇表与OOV:在常见词(如 "the")和稀有词、新词(如 "lowest")之间取得了很好的平衡。

  2. 强大的泛化能力:通过组合子词,可以理解和生成大量未在训练中出现的词汇。

1.4 存在的问题

世界上有成千上万的字符(中文、日文、特殊符号等)。一个基于字符的 BPE 词汇表可能会非常大(例如 5 万或 10 万个 token),才能较好地覆盖这些语言。这会导致:

  • 词汇表爆炸:需要存储一个巨大的词汇表。

  • 未知词(UNK)问题 :即使词汇表很大,也难免遇到训练时没见过的罕见字符,模型不得不将其表示为 <UNK>,导致信息丢失。

1.5 应用场景

  • GPT系列的GPT-1:OpenAI的GPT-1模型直接使用BPE作为其分词方法。

二、BBPE

Byte-Level Byte-Pair Encoding,BPE的字节级扩展版本,主要用于处理多语言 NLP 任务。

BBPE不在字符级别操作,而是在 字节(Byte) 级别操作。

  • 一个字节有 256 种可能的值(0-255)。这是一个固定且极小的基础单元集合。

  • 任何文本都可以通过编码(如 UTF-8)转换为一个字节序列。

  • 核心思想:BBPE 在这个字节序列上运行 BPE 算法,学习将频繁共现的字节对合并成新的 token。

2.1 BBPE vs. BPE

特性 BPE BBPE
处理单位 字符或子词 字节、UTF-8编码
适用语言 适用于空格分隔语言 适用所有语言
OOV 处理 仍可能遇到OOV 几乎不会有 OOV 问题
存储开销 词表较小 词表较大,但更具泛化能力

2.2 工作原理

原理和BPE一致,只是使用字节(byte)作为初始token,适用于任何文本。

2.3 实例助解

语料:

复制代码
 hug hug hug hug hug hug hug hug hug hug pug pug pug pug pug pun pun pun pun pun pun pun pun pun pun pun pun bun bun bun bun hugs hugs hugs hugs hugs
2.3.1 预处理

语料库包含的单词都是英文,它们的 UTF-8 编码和 ASCII 编码是一致的。我们先列出所有单词及其字节表示(用十进制表示):

复制代码
 hug: 104 (h), 117 (u), 103 (g)
 pug: 112 (p), 117 (u), 103 (g)
 pun: 112 (p), 117 (u), 110 (n)
 bun: 98 (b), 117 (u), 110 (n)
 hugs: 104 (h), 117 (u), 103 (g), 115 (s)
2.3.2 初始化词汇表

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

复制代码
 {98, 103, 104, 110, 112, 115, 117}
2.3.3 初始分词结果

每个字节单独成词

复制代码
 hug (出现 10 次): [104, 117, 103]
 pug (出现 5 次): [112, 117, 103]
 pun (出现 12 次): [112, 117, 110]
 bun (出现 4 次): [98, 117, 110]
 hugs (出现 5 次): [104, 117, 103, 115]
2.3.4 第1次统计字节对频率

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

字节对 频率
(104, 117) 15
(117, 103) 20
(112, 117) 17
(117, 110) 16
(98, 117) 4
(103, 115) 5
2.3.5 合并最高频字节对

选择频率最高的字节对进行合并,如(117, 103),出现 20 次。 合并操作

将这个合并为一个新的符号(假设为 Z1,其ID为256,因为前255个ID是基础字节)。

新词汇表加入: 256 -> (117, 103)

更新语料库(将所有连续的 117, 103 替换为 256):

  • hug: [104, 117, 103] -> [104, 256] (发生10次)

  • pug: [112, 117, 103] -> [112, 256] (发生5次)

  • pun: [112, 117, 110] -> 不变 [112, 117, 110] (发生12次)

  • bun: [98, 117, 110] -> 不变 [98, 117, 110] (发生4次)

  • hugs: [104, 117, 103, 115] -> [104, 256, 115] (发生5次)

当前词汇表: {98, 103, 104, 110, 112, 115, 117, 256}

2.3.6 第2次统计字节对频率

基于更新后的语料库统计:

  • [104, 256] (10次)

  • [112, 256] (5次)

  • [112, 117, 110] (12次)

  • [98, 117, 110] (4次)

  • [104, 256, 115] (5次)

字节对 频率
(104, 256) 15
(112, 256) 5
(112, 117) 12
(117, 110) 16
(98, 117) 4
(104,256) 5
(256, 115) 5

最频繁的字节对是(117, 110),我们将合并 (117, 110) 为一个新的符号 Z2 (ID 257)。

新词汇表加入: 257 -> (117, 110)

更新语料库(将所有连续的 117, 110 替换为 257):

  • hug: [104, 256] -> 不变 (发生10次)

  • pug: [112, 256] -> 不变 (发生5次)

  • pun: [112, 117, 110] -> [112, 257] (发生12次)

  • bun: [98, 117, 110] -> [98, 257] (发生4次)

  • hugs: [104, 256, 115] -> 不变 (发生5次)

当前词汇表: {98, 103, 104, 110, 112, 115, 117, 256, 257}

2.3.7 第3次统计字节对频率

基于更新后的语料库统计:

  • [104, 256] (10次)

  • [112, 256] (5次)

  • [112, 257] (12次)

  • [98, 257] (4次)

  • [104, 256, 115] (5次)

统计相邻对:

字节对 频率
(104, 256) 15
(112, 256) 5
(112, 257) 12
(98, 257) 4
(256, 115) 5

最频繁的字节对是 (104, 256),出现 15 次。 我们将合并 (104, 256) 为一个新的符号 Z3 (ID 258)。

新词汇表加入: 258 -> (104, 256)

更新语料库(将所有连续的 104, 256 替换为 258):

  • hug: [104, 256] -> [258] (发生10次)

  • pug: [112, 256] -> 不变 (发生5次)

  • pun: [112, 257] -> 不变 (发生12次)

  • bun: [98, 257] -> 不变 (发生4次)

  • hugs: [104, 256, 115] -> [258, 115] (发生5次)

当前词汇表: {98, 103, 104, 110, 112, 115, 117, 256, 257, 258}

2.3.8 最终词汇表

我们可以继续合并,但通常我们会预先设定一个合并次数或目标词表大小。假设我们就进行这 3 轮合并。

最终的 BBPE 词表包括:

  1. 基础字节 (7个): 98 (b), 103 (g), 104 (h), 110 (n), 112 (p), 115 (s), 117 (u)

  2. 学到的合并规则 (3个):

    • 256: (117, 103) -> ug (由字节 ug 合并而来)

    • 257: (117, 110) -> un (由字节 un 合并而来)

    • 258: (104, 256) -> h + ug = hug

2.3.9 编码

现在使用训练好的 BBPE 词表来对新词 bug 进行编码。

训练得到的 BBPE 词表

  • 基础字节 (7个): 98 (b), 103 (g), 104 (h), 110 (n), 112 (p), 115 (s), 117 (u)

  • 学到的合并规则 (3个):

    • 256: (117, 103) -> ug

    • 257: (117, 110) -> un

    • 258: (104, 256) -> hug (即 h + ug)

bug 进行编码

  1. 将单词转换为初始字节序列: bug 的 UTF-8 编码是 [98, 117, 103] (对应字符 b, u, g)。

  2. 应用合并规则

    编码器的核心是从当前词汇表中找到最长的可能子词(或字节序列)进行匹配

    我们从左到右处理序列 [98, 117, 103]

    1. 查看第一个字节 98 (b):

      词表中有 98 本身,但没有对应的合并规则。所以,最长的匹配就是单个字节 98 (b)。我们将其作为一个 token。

    2. 查看剩余序列 [117, 103]

      检查词表。我们发现 (117, 103) 正好对应我们学到的合并规则 256 (ug),所以,我们将 [117, 103] 合并为 token 256 (ug)。

最终编码结果:

模型将 bug 理解为两个部分:b + ug

2.3.10 总结

BBPE 在 BPE 基础上,将初始分词单元从 "字符" 改为 "字节",彻底消除 OOV 问题,天然支持多语言与任意字符,同时简化分词器设计,是现代大语言模型(如 GPT 系列)的主流分词方案。

2.4 优点

  • 真正的统一词汇表 :词汇表大小始终最多为 256 + 合并次数。这创造了一个极其统一和简洁的表示基础,所有语言都基于同一套"原子"单元。

  • 从根本上消除 <UNK> :任何可以用UTF-8表示的字符都可以被模型处理,因为模型在底层认识每一个字节。即使是表情符号😊(其UTF-8字节为 240 159 152 138)也可以被分解为已知的字节或字节组合。这实现了 "万物皆可分词"

2.5 应用场景

  • GPT系列(GPT-2, GPT-3, GPT-4): 这是Byte-Level BPE最著名和应用最成功的例子。OpenAI在GPT-2中首次大规模使用此技术,使其能够处理来自互联网的海量、多语言、多领域的杂乱文本,而无需繁琐的文本清洗和标准化。

技术分享是一个相互学习的过程。关于本文的主题,如果你有不同的见解、发现了文中的错误,或者有任何不清楚的地方,都请毫不犹豫地在评论区留言。我很期待能和大家一起讨论,共同补充更多细节。

相关推荐
zy_destiny3 小时前
【工业场景】用YOLOv8实现反光衣识别
人工智能·python·yolo·机器学习·计算机视觉
zhangjipinggom3 小时前
QwenVL - 202310版-论文阅读
人工智能·深度学习
PKNLP3 小时前
深度学习之循环神经网络RNN
人工智能·pytorch·rnn·深度学习
蛋仔聊测试3 小时前
Playwright 文件上传与下载完成判断全指南
python·测试
大模型真好玩3 小时前
低代码Agent开发框架使用指南(三)—小白5分钟利用Coze轻松构建智能体
人工智能·agent·coze
计算衎3 小时前
PyTorch的AI框架小白入门的学习点
人工智能·pytorch·深度学习
傻啦嘿哟3 小时前
Python高效实现Excel转PDF:无Office依赖的轻量化方案
python·pdf·excel
Eiceblue3 小时前
Python OCR 技术实践:从图片中提取文本和坐标
开发语言·python·ocr·visual studio code
C嘎嘎嵌入式开发3 小时前
(13)100天python从入门到拿捏《目录操作》
windows·python·microsoft