BPE(Byte Pair Encoding) 算法

概述

BPE(Byte Pair Encoding)是一种基于统计频率的子词切分算法。

它从字符级别开始,不断将语料中出现频率最高的相邻符号对合并成一个新符号,逐步构建出一个包含字符、常见子词甚至完整单词的词表。

由于决策仅依赖于当前相邻对的频次,并且每次都贪心地选择频次最高的进行合并,因此BPE本质上是一种确定性的贪心算法。

数据准备与初始化

BPE 通常会在"预分词"后的单词序列上工作。比如我们已经有了按空格切分好的单词及它们的频次:

text 复制代码
"old": 7, "older": 3, "finest": 9, "lowest": 4

第一步:添加词边界标记

在每个单词末尾加上一个特殊符号 ,用来标明单词结束,防止跨单词的字符对被错误合并。

text 复制代码
"old</w>": 7
"older</w>": 3
"finest</w>": 9
"lowest</w>": 4

第二步:拆成字符序列

把每个单词拆成最小单位------字符,每个字符以及 都算作一个独立的 token。

初始词表就是所有出现过的字符加上 </w>

比如 "old</w>" 会被表示为序列:o l d </w>

第三步:统计每个 token 的初始频次

我们把每个 token 在所有单词中的出现总次数算出来。

这里单词的出现次数会乘以单词本身的频率。比如单词 "old" 出现 7 次,其中 o 出现 1 次,l 出现 1 次,d 出现 1 次, 出现 1 次,因此这 4 个 token 各增加 7 次计数。

汇总后得到初始频次:

text 复制代码
</w>: 7+3+9+4 = 23
o:   7+3+4     = 14
l:   7+3+4     = 14
d:   7+3       = 10
e:   3+9+4     = 16
r:   3         = 3
f:   9         = 9
i:   9         = 9
n:   9         = 9
s:   9+4       = 13
t:   9+4       = 13
w:   4         = 4

初始词表大小为 12。

词表解析

(o,l): 7+3 = 10 是怎么得出来的?

(o,l) 出现在两个单词开头,把它们的频次相加即可

相邻对频次 = Σ(单词频次 × 该有序对在这个单词的当前符号序列中出现的次数)

逐个单词检查 (o, l) 这个有序对:

单词 频次 当前符号序列 含(o,1)次数 贡献
old</w> 7 o, l, d, 1 7 × 1 = 7
older</w> 3 o, l, d, e, r, 1 3 × 1 = 3
finest</w> 9 f, i, n, e, s, t, 0 9 × 0 = 0
lowest</w> 4 l, o, w, e, s, t, 0 4 × 0 = 0

oldolder 都恰好以 o 紧跟 l 开头,各出现 1 次,所以是 7 + 3 = 10。

(l,o) 和 (o,l) 是两个不同的对。

lowest</w> 的开头是 l, o------l 在前 o 在后,所以它对 (l,o) 贡献 4,对 (o,l) 贡献 0。

反过来,old / older 里的 o, l 也不会被算进 (l,o)。

BPE 统计的是有序相邻对,方向不能颠倒。你的汇总表里这两行是并列存在的:

text 复制代码
(o,l): 7+3 = 10     ← old, older
(l,o): 4            ← lowest

统计相邻对的频次

在 BPE 的每一次迭代中,我们需要扫描所有单词的当前符号序列,统计相邻符号对的出现次数。

以初始态为例,每个单词被看成字符序列,我们列出所有相邻对并计频:

  • "old" (频次 7):相邻对 (o,l)、(l,d)、(d,) 各出现 7 次。
  • "older" (频次 3):相邻对 (o,l)、(l,d)、(d,e)、(e,r)、(r,) 各 3 次。
  • "finest" (频次 9):相邻对 (f,i)、(i,n)、(n,e)、(e,s)、(s,t)、(t,) 各 9 次。
  • "lowest" (频次 4):相邻对 (l,o)、(o,w)、(w,e)、(e,s)、(s,t)、(t,) 各 4 次。

汇总所有相邻对频次(仅列出非零):

text 复制代码
(o,l): 7+3 = 10
(l,d): 7+3 = 10
(d,</w>): 7
(d,e): 3
(e,r): 3
(r,</w>): 3
(f,i): 9
(i,n): 9
(n,e): 9
(e,s): 9+4 = 13
(s,t): 9+4 = 13
(t,</w>): 9+4 = 13
(l,o): 4
(o,w): 4
(w,e): 4

可以看到,频率最高的相邻对是 (e,s)、(s,t) 和 (t,),均为 13 次。

通常算法会选择其中之一合并(若有多个并列最高,可按某种规则如字母序选择,这里与原文一致先合并 (e,s))。

贪心合并与迭代过程

迭代 1

合并:(e,s) → 新 token es

在所有单词序列中,将连续出现的 e 和 s 合并为一个符号 es,并更新序列:

  • "finest": f i n es t
  • "lowest": l o w es t
  • 其他单词不变。

重新统计频次:根据新序列,重新计算每个 token 和相邻对的频次。注意合并后原来的 e 和 s 在 es 中不再独立存在,它们的频次会减少。

新序列下的 token 频次变化:

  • es 在 finest 和 lowest 中各出现 1 次 → 9+4 = 13 次。
  • e 仅剩 older 中的一个(频次 3);finest 和 lowest 中原本的 e 已被合并掉。
  • s 原本只出现在这两个词中,现在完全被 es 替代,频次降为 0。
  • 其他 token 频次不变。

更新后的 token 频次表(省略 0 频次):

text 复制代码
</w>: 23
o:   14
l:   14
d:   10
e:   3   (来自 older 的 e)
r:   3
f:   9
i:   9
n:   9
t:   13
w:   4
es:  13  (新增)

此时词表大小仍为 12(去掉了 s,新增 es)。

新的相邻对频次(在更新后的序列上统计):

  • "old": (o,l):7, (l,d):7, (d,):7
  • "older": (o,l):3, (l,d):3, (d,e):3, (e,r):3, (r,):3
  • "finest": (f,i):9, (i,n):9, (n,es):9, (es,t):9, (t,):9
  • "lowest": (l,o):4, (o,w):4, (w,es):4, (es,t):4, (t,):4

高频相邻对:(es,t) 出现 9+4=13 次,(t,) 也是 13 次。选择合并 (es,t)。

合并的要点

合并 (e,s) 不是可选项,而是BPE 每一轮唯一要做的动作------而选它,是因为它在上一轮的相邻对频次表里得分最高(13 分)。

为什么要"合并":这是 BPE 唯一的学习动作

BPE 的训练目标很朴素:从一个很小的初始词表(这里是 12 个字符级 token)出发,每轮往词表里加 1 个新符号,一直加到预设的词表大小为止。

怎么加?不是凭直觉挑,而是按一条铁律:

每轮统计所有相邻符号对的频次,把频次最高的那一对粘成一个新 token。

选"最高频"的理由是贪心压缩:把出现 f 次的对合并成一个符号,整个语料的总 token 数就恰好减少 f。

所以每轮挑最高频对 = 每轮拿最大压缩收益。

我实测了这个语料的初始总 token 数:

text 复制代码
old 4×7 + older 6×3 + finest 7×9 + lowest 7×4 = 28+18+63+28 = 137

合并频次 13 的 (e,s) 之后:

text 复制代码
28 + 18 + 6×9 + 6×4 = 28+18+54+24 = 124 = 137 − 13 ✓

finest 从 7 个符号缩到 6 个,lowest 同样,一步省下 13 个 token。这就是"需要合并"的全部动机。

迭代 2

合并:(es,t) → 新 token est

更新序列:

  • "finest": f i n est
  • "lowest": l o w est

重新统计频次:

  • est 频次 = 9+4 = 13
  • es 和 t 的单独频次相应减少:es 完全被吸收,频次变为 0;t 也只剩 0(原本只在 finest 和 lowest 中有 t)。
  • 其他 token 不变。

新 token 频次(省略 0):

text 复制代码
</w>: 23
o:   14
l:   14
d:   10
e:   3
r:   3
f:   9
i:   9
n:   9
w:   4
est: 13

词表大小:11。

相邻对:

  • 两个含 est 的词序列变为 (n,est):9,(est,):9 和 (w,est):4,(est,):4
  • 所以 (est,) 总频次 = 9+4 = 13

最高频次对就是 (est,</w>)(13次)。

迭代 3

合并:(est,) → est

这个合并非常关键,因为它会把词尾的 est 和边界标记粘在一起,形成一个代表"以 est 结尾"的完整词单元。

更新序列:

  • "finest": f i n est (注意这里 est 是一个 token)
  • "lowest": l o w est

重新统计频次:

  • est 频次 = 9+4 = 13
  • 原来的 est 和 频次降低:est 降为 0, 从 23 减去 13 剩 10(来自 old 的 7 和 older 的 3)。
  • 其他不变。

频次表:

text 复制代码
</w>: 10
o:   14
l:   14
d:   10
e:   3
r:   3
f:   9
i:   9
n:   9
w:   4
est</w>: 13

词表大小仍是 11。

相邻对:

  • "old": (o,l):7, (l,d):7, (d,):7
  • "older": (o,l):3, (l,d):3, (d,e):3, (e,r):3, (r,):3
  • "finest": (f,i):9, (i,n):9, (n,est):9
  • "lowest": (l,o):4, (o,w):4, (w,est):4

此时最高频对是 (o,l):7+3=10 次。

迭代 4

合并:(o,l) → ol

序列更新:

  • "old": ol d
  • "older": ol d e r
  • "lowest" 中的 l o 不会被合并,因为它是 (l,o) 而非 (o,l)。

频次更新:

  • ol 获得 7+3 = 10 次。
  • o 从 14 减到 4(只剩 "lowest" 中的 o),l 从 14 减到 4(同样只剩 "lowest" 中的 l)。

新 token 频次:

text 复制代码
</w>: 10
ol: 10
d: 10
e: 3
r: 3
f: 9
i: 9
n: 9
w: 4
est</w>: 13
o: 4
l: 4

词表大小 12。

迭代 5

相邻对统计:

  • "old": (ol,d):7, (d,):7
  • "older": (ol,d):3, (d,e):3, (e,r):3, (r,):3
  • "finest": (f,i):9, (i,n):9, (n,est):9
  • "lowest": (l,o):4, (o,w):4, (w,est):4

最高频对:(ol,d) 出现 7+3=10 次。

合并:(ol,d) → old

序列:

  • "old": old
  • "older": old e r

频次更新:

  • old 获得 10 次。
  • ol 和 d 频次各减 10:ol 变为 0,d 变为 0(原本只在 old/older 中有 d)。

最终频次:

text 复制代码
</w>: 10
e: 3
r: 3
f: 9
i: 9
n: 9
w: 4
est</w>: 13
o: 4
l: 4
old: 10

词表大小 11。

至此,常用词 old、词尾 est 等都已形成独立的 subword 单元。

我们可以选择停在这里,或者继续合并 (old,)、(l,o) 等。

解码与新词编码

解码极其简单:将 token 序列直接拼接即可。例如 "old", "", "est" → "old" + "" + "est" ?(这里需要小心)

实际上 token 本身已经包含 ,所以解码时遇到 就知道那是词边界。

例如序列 "old", "", "high", "est" 解码为 "old", "highest"。这保证了 est 只会出现在词尾。

对新词编码:将新词拆成字符,然后用学到的 BPE 合并规则(从最大 token 开始尝试匹配)逐步合并成已知的 subword 序列。如果某部分无法匹配,就用 代替。

为什么说 BPE 是"仅基于频率的贪心算法"?

仅基于频率:每一次选择要合并的相邻对,依据的唯一标准就是它在当前语料中的出现次数,完全不关心这个字符组合是否具有语言学意义。

贪心:每次都选择当前统计下频率最高的对进行合并,不会回溯,不会考虑这一合并对未来步骤的全局最优性。尽管是贪心,实际效果却很好,能够自然地捕捉到常见词根、前缀、后缀等有意义的 subword 单元。

迭代构建词表:从最细粒度的字符开始,每合并一次就向词表中加入一个新 token,同时更新整个语料的符号序列和频率分布,直到词表达到预定大小(如 32k 个 token)或相邻对最高频率为 1。

这种机制让 BPE 能很好地平衡词表大小与编码效率:高频词保持完整,低频词被分解为可共享的 subword,极大缓解了未登录词问题,也因此成为 GPT-2、RoBERTa 等主流预训练模型的核心分词算法。

相关推荐
愛芳芳1 小时前
Swagger2 多环境动态配置实战指南
java·后端·maven·springboot·swagger
huaweichenai1 小时前
spring boot实现任务异步处理(队列)
java·spring boot
Easy88AI1 小时前
DeepSeek-V4.1-Flash 接入实战:从官方 API 到聚合网关(以 Easy88AI 为例)
人工智能·深度学习
MZA6661 小时前
实战:基于 XXL-JOB + 策略模式 + Nacos 配置化的 CSV 导出任务系统设计
java·spring boot·mybatis
邪修king1 小时前
Re:Linux 系统篇(二十九):动静态库Chapter2:动态库深度辨析 —— 核心本质、制作流程、双阶段查找模型与排错指南
android·java·linux·开发语言
zx_741484811 小时前
【深度学习入门】循环神经网络(RNN)与长短期记忆网络(LSTM)详解
rnn·深度学习·lstm
泡海椒2 小时前
告别 iText 繁杂配置:jquick-pdf 极简 PDF 生成实战(零基础上手)
java·开发语言·pdf
AI Blog3 小时前
YOLO数据集 | 第05期:无人机检测、电力巡检、阀门识别
深度学习·目标检测·计算机视觉·yolo数据集
比奥利奥还傲.3 小时前
哪吒监控面板实战:部署 Dashboard、接入 Agent、钉钉告警,再配置固定公网访问
网络·人工智能·分布式·1024程序员节