BPE 算法原理:高频子词合并的艺术

聊到 Tokenization,BPE(Byte Pair Encoding)是个绕不开的核心算法。

最早它是用来做数据压缩的——把常见字节对替换成一个新符号。后来被 NLP 大佬们捡起来,改造成了子词切分的利器。说实话,我第一次接触时也觉得挺神奇:一个压缩算法,怎么就成 NLP 标配了?

你想想看,核心思想其实就一句话:从最细的字符开始,反复合并出现频率最高的相邻对。直到词表大小满足要求,或者没有可合并的对为止。

为什么需要 BPE?

传统词表有俩硬伤:

  • OOV(未登录词)问题:测试集里新词出现,词表直接崩了
  • 词表太大:英语就有几十万词,存储和计算都不划算

BPE 的好处是——不管什么新词,都能拆成已知的子词。

我在项目中用过字符级模型。效果嘛……长序列训练实在太慢。BPE 刚好站在中间:比词细,比字符粗。

核心优势:BPE 在词表大小和序列长度之间找到一个平衡点。词表通常在 3w-5w,既能覆盖绝大部分常见词,又不会把句子拉得太长。

BPE 的完整流程

下面这张图,是我给学员讲 BPE 时必画的。它把整个流程串起来了:

BPE 算法核心流程图 ① 文本预处理 ② 初始词表 ③ 统计所有相邻子词对频率 ④ 合并频率最高的相邻对 ⑤ 更新词表与文本 到达目标 词表大小? 循环直到条件满足 输出词表 说明:循环直至词表大小达标或无可合并对

手写一个 BPE 核心逻辑

别光看理论,我带你过一遍代码。这是我早期做分词器时写的精简版:

def bpe_corpus(corpus: list, vocab_size: int) -> dict:
    # 1. 拆成字符级初始词表
    vocab = {}
    for word in corpus:
        tokens = list(word) + ['</w>']
        _str = ' '.join(tokens)
        vocab[_str] = vocab.get(_str, 0) + 1

    # 2. 迭代合并
    current_vocab_size = len(set(' '.join(vocab.keys()).split()))
    
    while current_vocab_size < vocab_size:
        pairs = {}
        for _str, freq in vocab.items():
            tokens = _str.split()
            for i in range(len(tokens)-1):
                pair = (tokens[i], tokens[i+1])
                pairs[pair] = pairs.get(pair, 0) + freq
        
        if not pairs:
            break
        
        # 找到最高频对
        best_pair = max(pairs, key=pairs.get)
        
        # 合并
        new_vocab = {}
        for _str, freq in vocab.items():
            new_ = _str.replace(' '.join(best_pair), ''.join(best_pair))
            new_vocab[new_] = freq
        
        vocab = new_vocab
        current_vocab_size = len(set(' '.join(vocab.keys()).split()))
    
    # 返回最终词表
    final_vocab = set()
    for _str in vocab:
        final_vocab.update(_str.split())
    
    return {: idx for idx,  in enumerate(sorted(final_vocab))}

小技巧:我在实际项目中常用 tokenizers 库,比手写快得多。但理解这个流程很重要——很多坑都藏在细节里。

核心参数对比

做 BPE 时,有几个参数你得心里有数:

参数 作用 经验值
词表大小 最终子词数量 30k-50k(中英文通用)
最小频率阈值 低于阈值的合并忽略 2(避免罕见情况浪费词表)
特殊 [UNK], [CLS], [SEP] 等 至少留 10 个位置

避坑指南:我曾经在词表里忘了加 </w> 这个结尾标记。结果模型分不清 "play" 和 "playing" 是否该合并——训练损失死活降不下去。折腾了两天才发现原因。

BPE 的优缺点

没有完美算法,BPE 也一样:

  • 优点:词表可控、OOV 友好、实现简单
  • 缺点:贪心策略,不一定全局最优。我在处理某些非粘着语(如中文)时,效果不如直接从词切起

到底什么时候用 BPE?

我个人经验:

  1. 英文或其他西方语言——BPE 是首选。比如 、BERT 都在用
  2. 代码或混合文本——BPE 对数字和特殊字符很友好
  3. 中文——部分场景可以用,但我更推荐 WordPiece 或 SentencePiece

你想想看,BPE 就像一把瑞士军刀。不是所有螺丝都能拧,但大部分场景都够用。理解它的合并逻辑,你再去学后续的 WordPiece 或 Unigram,会发现很多相似之处。