常见的 LLM 分词器(Tokenizer)
Zhongjun Qiu 元婴开发者

本篇文章介绍大语言模型(LLM)中常用的几种分词(Tokenizer)方法,包括传统分词、BPE、WordPiece、SentencePiece 以及 Unigram Language Model。我们将通过原理解析、应用场景和代码示例,帮助读者理解这些分词技术在 LLM 中的作用与实现。

💡 引言:大语言模型(LLM)的基石——Tokenizer

“一切输入皆为序列”——这是所有现代深度学习模型处理数据的核心哲学,对于大语言模型(LLM)而言更是如此。

无论我们输入的是一个中文句子、一段英文代码、还是一串生动的 Emoji 表情,LLM 都无法直接理解这些人类语言的字符串(String)。它们只能接收并处理数字形式的输入。因此,我们迫切需要一座“翻译桥梁”,将人类可读的文本转化为机器可计算的数字序列,这座桥梁就是 Tokenizer(分词器)

为什么 Tokenizer 是 LLM 的基石?

  1. 机器可读性(Input Conversion):Tokenizer 将文本切分成最小的语义单元——Token,并用唯一的 ID(索引号) 来表示,使之成为模型可以处理的数字输入。
  2. 词汇表与计算效率(Vocabulary & Efficiency):一个理想的 Tokenizer 必须在词汇表(Vocabulary)大小和序列长度(Sequence Length)之间找到平衡点。
    • 如果词汇表过大(例如,包含所有可能的单词),模型训练将变得极其缓慢且难以泛化。
    • 如果 Token 粒度过细(例如,全部分割成单字符),虽然词汇表小了,但文本序列会变得太长,Self-Attention 机制的计算成本将以序列长度的平方 O(N2) 爆炸式增长。
  3. 泛化能力与 OOV 问题(Generalization & OOV):Tokenizer 的切分策略直接决定了模型处理未登录词(OOV, Out-Of-Vocabulary)的能力。像 BPEWordPiece 这样的子词(Subword)分词方法,通过将新词拆分成已知的词根或子词,显著增强了模型的泛化能力。

1️⃣ 传统分词(Rule-based / Word-level)

🔍 原理

传统分词基于空格或人工规则进行切分。

  • 英文:直接用空格拆词。
  • 中文:需要词典和规则(如结巴分词、THULAC 等)。
  • 无法处理未登录词(OOV),粒度较粗。

📦 应用场景

  • 传统 NLP 特征工程(TF-IDF、BM25)
  • 不依赖大模型的任务(情感分析、分类)

💻 示例(jieba 中文分词)

1
2
3
4
5
import jieba

text = "我爱自然语言处理 i like natural language process"
tokens = list(jieba.cut(text))
print(tokens)

输出:

1
['我', '爱', '自然语言', '处理', ' ', 'i', ' ', 'like', ' ', 'natural', ' ', 'language', ' ', 'process']

2️⃣ BPE(Byte Pair Encoding)

🔍 原理

BPE 是一种 基于频率的子词切分算法

  1. 从字符级开始(如 “l”, “o”, “w”, “e”, “r”)。
  2. 找出频率最高的相邻对(如 “l” + “o” → “lo”)。
  3. 合并成新的子词,不断重复,直到词汇表大小达到限制。
  4. 在推理时按最长匹配优先进行切分。

📦 应用场景

  • GPT-2、RoBERTa、LLaMA
  • 英文效果好,可应对拼写变化与新词

💻 示例(使用 tokenizers 库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from tokenizers import Tokenizer, models, trainers, pre_tokenizers

# 初始化 BPE 模型
tokenizer = Tokenizer(models.BPE())

# 定义训练器
trainer = trainers.BpeTrainer(
vocab_size=50, # 小词表方便观察
min_frequency=1,# type: ignore
special_tokens=["<unk>", "<pad>", "<s>", "</s>"]
)

# 设置预分词规则(这里用空格分词)
tokenizer.pre_tokenizer = pre_tokenizers.Whitespace() # type: ignore

# 模拟一批文本数据
texts = [
"The lower and lowest temperatures are often lower than expected.",
"Newest technologies bring wider opportunities and new solutions",
]

# 训练 BPE 分词器
tokenizer.train_from_iterator(texts, trainer)

# tokenizer 是训练好的 Tokenizer 对象
vocab_dict = tokenizer.get_vocab()
print("词表大小:", len(vocab_dict))
vocab_sorted = sorted(vocab_dict.items(), key=lambda x: x[1])
print(vocab_sorted)

# 编码新的句子
test_sentence = "The lowest lower technologies are amazing."
encoding = tokenizer.encode(test_sentence)

# 输出结果
print("Input sentence:", test_sentence)
print("Tokens:", encoding.tokens)
print("Token IDs:", encoding.ids)
print("Decoded back:", tokenizer.decode(encoding.ids))

输出:

1
2
3
4
5
6
词表大小: 50
[('<unk>', 0), ('<pad>', 1), ('<s>', 2), ('</s>', 3), ('.', 4), ('N', 5), ('T', 6), ('a', 7), ('b', 8), ('c', 9), ('d', 10), ('e', 11), ('f', 12), ('g', 13), ('h', 14), ('i', 15), ('l', 16), ('m', 17), ('n', 18), ('o', 19), ('p', 20), ('r', 21), ('s', 22), ('t', 23), ('u', 24), ('w', 25), ('x', 26), ('es', 27), ('er', 28), ('lo', 29), ('te', 30), ('an', 31), ('low', 32), ('ew', 33), ('ies', 34), ('tu', 35), ('est', 36), ('and', 37), ('lower', 38), ('New', 39), ('Th', 40), ('ar', 41), ('atu', 42), ('br', 43), ('ch', 44), ('cte', 45), ('der', 46), ('ex', 47), ('ecte', 48), ('fte', 49)]
Input sentence: The lowest lower technologies are amazing.
Tokens: ['Th', 'e', 'low', 'est', 'lower', 'te', 'ch', 'n', 'o', 'lo', 'g', 'ies', 'ar', 'e', 'a', 'm', 'a', 'i', 'n', 'g', '.']
Token IDs: [40, 11, 32, 36, 38, 30, 44, 18, 19, 29, 13, 34, 41, 11, 7, 17, 7, 15, 18, 13, 4]
Decoded back: Th e low est lower te ch n o lo g ies ar e a m a i n g .
  • 词表太小,BPE 没法把所有长单词收录完整。
  • 算法会优先保留 高频子词/词根,低频词只能拆成更小单位。

因为vocab_size=50,词表比较小,所以BPE最后生成的大部分都是词根,并且是在训练的语句中经常出现的,例如:“lower”,“New”等等。

当我们设置vocab_size=100

1
2
3
4
5
6
词表大小: 96
[('<unk>', 0), ('<pad>', 1), ('<s>', 2), ('</s>', 3), ('.', 4), ('N', 5), ('T', 6), ('a', 7), ('b', 8), ('c', 9), ('d', 10), ('e', 11), ('f', 12), ('g', 13), ('h', 14), ('i', 15), ('l', 16), ('m', 17), ('n', 18), ('o', 19), ('p', 20), ('r', 21), ('s', 22), ('t', 23), ('u', 24), ('w', 25), ('x', 26), ('es', 27), ('er', 28), ('lo', 29), ('te', 30), ('an', 31), ('low', 32), ('ew', 33), ('ies', 34), ('tu', 35), ('est', 36), ('and', 37), ('lower', 38), ('New', 39), ('Th', 40), ('ar', 41), ('atu', 42), ('br', 43), ('ch', 44), ('cte', 45), ('der', 46), ('ex', 47), ('ecte', 48), ('fte', 49), ('gies', 50), ('han', 51), ('in', 52), ('io', 53), ('it', 54), ('ider', 55), ('lu', 56), ('mp', 57), ('no', 58), ('ns', 59), ('new', 60), ('nit', 61), ('op', 62), ('or', 63), ('ofte', 64), ('olu', 65), ('pecte', 66), ('por', 67), ('res', 68), ('solu', 69), ('than', 70), ('tio', 71), ('wider', 72), ('eratu', 73), ('logies', 74), ('tech', 75), ('temp', 76), ('lowest', 77), ('tunit', 78), ('Newest', 79), ('The', 80), ('are', 81), ('brin', 82), ('expecte', 83), ('nologies', 84), ('oppor', 85), ('often', 86), ('solutio', 87), ('eratures', 88), ('technologies', 89), ('temperatures', 90), ('tunities', 91), ('bring', 92), ('expected', 93), ('opportunities', 94), ('solutions', 95)]
Input sentence: The lowest lower technologies are amazing.
Tokens: ['The', 'lowest', 'lower', 'technologies', 'are', 'a', 'm', 'a', 'in', 'g', '.']
Token IDs: [80, 77, 38, 89, 81, 7, 17, 7, 52, 13, 4]
Decoded back: The lowest lower technologies are a m a in g .

高频单词和长单词被更多地完整保留:

  • lowest, lower, technologies 都出现在词表中

只有一些低频、长尾词被拆分,例如:

  • amazinga, m, a, in, g

分词粒度更粗,保留更多完整单词,而非拆成单字或极小子词。

在此基础上,设置min_frequency=2

1
2
3
4
5
6
词表大小: 39
[('<unk>', 0), ('<pad>', 1), ('<s>', 2), ('</s>', 3), ('.', 4), ('N', 5), ('T', 6), ('a', 7), ('b', 8), ('c', 9), ('d', 10), ('e', 11), ('f', 12), ('g', 13), ('h', 14), ('i', 15), ('l', 16), ('m', 17), ('n', 18), ('o', 19), ('p', 20), ('r', 21), ('s', 22), ('t', 23), ('u', 24), ('w', 25), ('x', 26), ('es', 27), ('er', 28), ('lo', 29), ('te', 30), ('an', 31), ('low', 32), ('ew', 33), ('ies', 34), ('tu', 35), ('est', 36), ('and', 37), ('lower', 38)]
Input sentence: The lowest lower technologies are amazing.
Tokens: ['T', 'h', 'e', 'low', 'est', 'lower', 'te', 'c', 'h', 'n', 'o', 'lo', 'g', 'ies', 'a', 'r', 'e', 'a', 'm', 'a', 'i', 'n', 'g', '.']
Token IDs: [6, 14, 11, 32, 36, 38, 30, 9, 14, 18, 19, 29, 13, 34, 7, 21, 11, 7, 17, 7, 15, 18, 13, 4]
Decoded back: T h e low est lower te c h n o lo g ies a r e a m a i n g .
  • 高频子词(如 low, est, lower)被保留
  • 低频子词或字符被拆得更细,例如:
    • The'T', 'h', 'e'
    • technologies'te', 'c', 'h', 'n', 'o', 'lo', 'g', 'ies'
    • amazing'a', 'm', 'a', 'i', 'n', 'g'

因为min_frequency=2排除低频组合,于是 BPE 没法把低频长单词直接加入词表(即使词表余量充足),只能拆成更小的字母或子词组合。高频子词仍然被保留,因此 lower, low, est 等能完整出现。


3️⃣ WordPiece(基于概率的BPE变体)

🔍 原理

  • BPE: 规则很简单,谁“数量多”就合并谁。它合并的是出现频率最高的相邻pair。
  • WordPiece: 规则更“聪明”:它合并的是能使训练数据总“似然(Likelihood)”提升最大的相邻 pair。

我们用一个简单的例子来说明:

  • 假设我们要决定是合并 AB 呢,还是合并 CD
  • count(A) 表示 A 出现的总次数。
  • count(A, B) 表示 AB 相邻出现的总次数。

1. BPE 的打分方式(频率)

BPE 只看相邻次数: Score(A, B) = count(A, B)

如果 count(A, B) = 100count(C, D) = 90,BPE会选择合并 A, B

2. WordPiece 的打分方式(似然)

WordPiece 认为,仅仅看 count(A, B) 是不够的。如果 AB 本身都是超级高频的字母(比如 ‘e’ 和 ‘s’),它们俩相邻出现100次可能纯属巧合

WordPiece 的打分公式(简化后)是: Score(A, B) = count(A, B) / ( count(A) * count(B) )

这个公式在计算 AB 相邻出现的概率 count(A, B) 的同时,还除以了它们各自独立出现的概率 count(A)count(B)

这实际上是在计算一个“点互信息 (Pointwise Mutual Information, PMI)”: AB 一起出现的概率,相比于它们各自独立、“碰巧”出现在一起的概率,要高出多少?

举个例子:为什么 WordPiece 更“语义相关”

假设我们的语料库非常大,我们有以下统计数据:

  • Pair 1: ('t', 'h') (比如 “the”, “this”, “that”)
    • count(t, h) = 1,000,000 (相邻出现100万次)
    • count(t) = 5,000,000
    • count(h) = 4,000,000
  • Pair 2: ('e', 's') (比如 “these”, “makes”, “goes”…)
    • count(e, s) = 1,200,000 (相邻出现120万次)
    • count(e) = 10,000,000 (最常见的字母)
    • count(s) = 8,000,000

BPE 的选择:

  • Score(t, h) = 1,000,000
  • Score(e, s) = 1,200,000
  • BPE 结论: 120万 > 100万,优先合并 ('e', 's')

WordPiece 的选择:

  • Score(t, h) = 1,000,000 / (5,000,000 * 4,000,000) = 1 / 20,000,000

  • Score(e, s) = 1,200,000 / (10,000,000 * 8,000,000) = 1.2 / 80,000,000 = 1 / 66,666,667

  • WordPiece 结论: 1/20M 远远大于 1/66M('t', 'h') 这个组合的“意义”远大于 ('e', 's')

  • 解读: th 结合在一起,比它们“随机”碰在一起的概率高得多,这说明 th 是一个强相关的“语义单元”。而 es 虽然也经常挨在一起,但因为它们各自都太常用了,所以它们挨在一起很可能只是“巧合”,es 并不是一个像 th 一样牢固的单元。

  • 因此,WordPiece 会优先合并 ('t', 'h')

BPE 是“频率驱动”的,简单粗暴。而 WordPiece 是“概率驱动”的,它通过除以单个 token 的频率,惩罚了那些由高频、无意义的 token 组成的“巧合”pair,从而优先保留了像 thingpro 这样真正具有强内部相关性的“语义单元”。

📦 应用场景

  • BERT、ALBERT、DistilBERT
  • 英文任务常用,能兼顾泛化性与语义一致性

💻 示例(Hugging Face自带的WordPiece)

1
2
3
4
5
6
7
8
9
10
11
12
13
from transformers import BertTokenizerFast

tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")

vocab_dict = tokenizer.get_vocab()
print("词表大小:", len(vocab_dict))

test_sentence = "The lowest lower technologies are amazing and counterproductive."

tokens = tokenizer.tokenize(test_sentence)
ids = tokenizer.convert_tokens_to_ids(tokens)
print("Tokens:", tokens)
print("IDs:", ids)

输出:

1
2
3
词表大小: 30522
Tokens: ['the', 'lowest', 'lower', 'technologies', 'are', 'amazing', 'and', 'counter', '##pro', '##ductive', '.']
IDs: [1996, 7290, 2896, 6786, 2024, 6429, 1998, 4675, 21572, 26638, 1012]

附注

## 标记是 WordPiece 算法的核心机制,用于确保分词过程的“可逆性”和“无歧义性”

简单来说,它的作用是告诉解码器:“这个 token 是一个词的中间或后缀,它应该紧贴着前一个 token,它们之间没有空格。

为什么这是必要的?

想象一下,如果我们没有 ## 标记,词汇表里只有 counter, pro, ductive。那么下面两个完全不同的输入会产生完全相同的结果:

  1. "counterproductive" (一个词) -> ['counter', 'pro', 'ductive']
  2. "counter pro ductive" (三个词) -> ['counter', 'pro', 'ductive']

这就导致了一个严重问题:当模型输出 ['counter', 'pro', 'ductive'] 时,我们无法知道应该将它重构为 “counterproductive” 还是 “counter pro ductive”。

## 如何解决这个问题:

WordPiece 的词汇表会同时包含 pro(词首)和 ##pro(非词首)。

  • 句子 1: "counterproductive"

    • 分词结果: ['counter', '##pro', '##ductive']
    • 解码:counter + (无空格)pro + (无空格)ductive -> “counterproductive”
  • 句子 2: "counter pro ductive"

    • 分词结果: ['counter', 'pro', 'ductive']

    • 解码:counter + (加空格)pro + (加空格)ductive -> “counter pro ductive”

BPE(及其变体 SentencePiece,被 GPT、RoBERTa 等模型使用)也必须解决同样的可逆性问题,但它采用了逻辑上相反的标记策略

  • WordPiece (BERT): 默认所有 token 间加空格,使用 ## 标记“不要加空格”。
  • BPE (GPT): 默认所有 token 间不加空格,使用一个特殊前缀(如 Ġ,代表空格)来标记“请在这里加一个空格”。

BPE/SentencePiece 示例:

在 BPE 的词汇表里,一个词根(如 “pro”)会有两种形式:

  1. pro (无前缀):表示这是一个词的中间或后缀(等同于 WordPiece 的 ##pro)。
  2. Ġpro (带前缀 Ġ):表示这是一个新词的开头(等同于 WordPiece 的 pro)。

我们用 BPE 来处理同样的两个句子:

  • 句子 1: "counterproductive"

    • 分词结果: ['counter', 'pro', 'ductive'] (这里的 counter 是词首,productive 是非词首)
    • 解码:counter + (无Ġ)pro + (无Ġ)ductive -> “counterproductive”
  • 句子 2: "counter pro ductive"

    • 分词结果: ['counter', 'Ġpro', 'Ġductive']
    • 解码:counter + (有Ġ)pro + (有Ġ)ductive -> “counter pro ductive”

两种算法都解决了歧义问题,只是标记策略相反。WordPiece 标记“非词首”,而 BPE/SentencePiece 标记“词首”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from transformers import AutoTokenizer

model_name = "gpt2"

tokenizer = AutoTokenizer.from_pretrained(model_name)

test_sentence = "The lowest lower technologies are amazing and counterproductive."

tokens = tokenizer.tokenize(test_sentence)
ids = tokenizer.convert_tokens_to_ids(tokens)
print("Tokens:", tokens)
print("IDs:", ids)

# 输出
# Tokens: ['The', 'Ġlowest', 'Ġlower', 'Ġtechnologies', 'Ġare', 'Ġamazing', 'Ġand', 'Ġcounterproductive', '.']
# IDs: [464, 9016, 2793, 8514, 389, 4998, 290, 46674, 13]

4️⃣ SentencePiece(Google开发的通用分词器)

🔍 原理

BPE / WordPiece 的经典实现(如 huggingface 的 tokenizer 或 GPT/BERT)中,训练输入通常是:the cat sat on the mat -> [“the”, “cat”, “sat”, “on”, “the”, “mat”] (默认用空格分割单词)。然后在这些 token 上做 BPE 统计相邻 pair 频率,逐步合并,空格已经被视为“不可合并的边界”。

SentencePiece 不依赖空格或人工分词规则,而是直接在原始字符流上学习子词单元

  1. 将空格也视作一种符号(用 _ 来替换所有空格)

  2. 使用两种常用模型进行分词:BPE 和 Unigram

📦 应用场景

  • T5、ALBERT、mBERT
  • 适用于中英混合、多语言模型

💻 示例(SentencePiece)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from transformers import T5TokenizerFast

# 1. 准备 T5 SentencePiece 分词器
# T5 使用 SentencePiece 且是 Unigram Language Model 算法的代表
tokenizer = T5TokenizerFast.from_pretrained("t5-base")

# 2. 准备中英文输入
english_sentence = "The lowest lower technologies are amazing and counterproductive."
chinese_sentence = "我爱自然语言处理,并且明天早上吃什么? 我爱吃枇杷!"

english_tokens = tokenizer.tokenize(english_sentence)
print("Tokens:", english_tokens)

chinese_tokens = tokenizer.tokenize(chinese_sentence)
print("Tokens:", chinese_tokens)

输出:

1
2
Tokens: ['▁The', '▁lowest', '▁lower', '▁technologies', '▁are', '▁amazing', '▁and', '▁counter', 'productive', '.']
Tokens: ['▁', '我爱自然语言处理', ',', '并且明天早上吃什么', '?', '▁', '我爱吃枇杷', '!']

“▁” 表示空格位置,进而也可以表示该token是不是一个单词或一句话的第一个token

当然,这里t5-base输出的中文分词粒度很粗,是因为其训练语料中中文数据很少

我们可以换几个用其他中文数据集训练的T5模型试试:

1
2
3
4
5
6
from transformers import AutoTokenizer

tok2 = AutoTokenizer.from_pretrained("google/mt5-small")
print(tok2.tokenize("我爱自然语言处理,并且明天早上吃什么?我爱吃枇杷!"))

# ['▁', '我爱', '自然', '语言', '处理', ',', '并且', '明天', '早上', '吃', '什么', '?', '▁', '我爱', '吃', '枇', '杷', '!']

5️⃣ Unigram Language Model(SentencePiece默认算法)

🔍 原理

Unigram Language Model 算法的核心目标是:找到一个最优的子词集合(词汇表),使得训练语料的整体似然(Likelihood)最大化。 它不依赖于简单的频率计数(如 BPE),而是引入了概率模型。

  • 模型假设: 假设任何一个句子 S 都可以被分解为一系列子词 x1, x2, …, xn。ULM 假设这些子词是独立地从词汇表 Σ 中抽样出来的(即 Unigram 假设)。
  • 似然函数: 句子 S 的概率(似然)可以表示为: 其中,Splits(S)S 所有可能的分词组合。算法的目标是找到词汇表 Σ 和每个子词的概率 P(x),使得 log (P(Corpus)) 最大化。

分词过程:自上而下的剪枝法

ULM 算法的训练分为三个关键阶段:初始化、迭代优化和最终分词。

阶段 I: 初始化

  1. 准备: 将语料库进行 SentencePiece 预处理(用 _ 替换空格)。
  2. 构建大词汇表 Σ 收集语料库中所有长度在 1 到某个最大值(如 5)之间的所有子字符串。这个初始词汇表 Σ 非常庞大。
  3. 计算初始概率:Σ 中的每个子词 x,计算其在语料库中的初始频率。

阶段 II: 迭代优化(剪枝)

这个阶段是 ULM 的核心,通过 EM (Expectation-Maximization) 算法的思想来优化词汇表 Σ 和子词概率 P(x)

  1. E-步(计算概率): 使用当前的词汇表 Σ 和子词概率 P(x),对语料库中的每个句子 S,找到最佳分词(即概率最大的分词路径)。这通常通过 Viterbi 算法实现,因为它需要找到 max ∏P(xi)
  2. M-步(计算损失 / 剪枝):
    • 计算损失: 对于词汇表中的每个子词 x,暂时将其删除,然后重新计算语料库的总似然。
    • 计算贡献度: 找出那些删除后对总似然影响最小(即贡献度最低)的子词。
    • 剪枝: 移除词汇表中贡献度最低的 p %(例如 20%)的子词。
  3. 重复: 重复 E 步和 M 步,直到词汇表 Σ 达到预设的目标大小(例如 32,000)。

阶段 III: 最终分词

训练完成后,词汇表 Σ 中的每个子词 x 都有一个固定的概率 P(x)

对于任何输入句子 S,分词器使用 Viterbi 算法,在所有可能的分词路径中,找到那个使 P(xi) 最大的分词序列作为最终结果。

假设我们已经训练好了一个 ULM 词汇表,其中包含以下子词及其概率 P(x)

子词 x 概率 P(x) (Log 形式)
_p -3.0
_play -1.0
ing -1.5
lay -2.0
_playing -3.5

现在,我们要对句子 S = "playing"(预处理后是 _playing)进行分词。

ULM 会考虑所有可能的拆分路径:

路径 (Split) 拆分 Tokens 概率计算 (Log) 结果
路径 A _play, ing P(_play) + P(ing) (−1.0) + (−1.5) = −2.5
路径 B _p, lay, ing P(_p) + P(lay) + P(ing) (−3.0) + (−2.0) + (−1.5) = −6.5
路径 C _playing P(_playing) (−3.5)

结论: 在 Log 概率中,数字越大(越接近 0)表示概率越高。路径 A (-2.5) 的概率最高,所以 ULM 最终选择:_play, ing

📦 应用场景

  • mT5、XLNet(与 SentencePiece 结合)
  • 适用于中英混合、多语言模型

💻 示例

上一节中的t5-base和google/mt5-small都是使用的SentencePiece+ULM


6️⃣ Byte-level BPE

🔍 原理

传统的 BPE 训练是从字符集开始的。例如,如果你的语料是英文,BPE 的起始词汇表可能是 26 个小写字母、26 个大写字母、10 个数字和一些标点符号。

然而,如果语料中出现了像 Emoji、生僻符号或非拉丁字符(如日文、俄文)时,BPE 必须将这些新的字符也添加到初始词汇表中。这导致一个问题:初始词汇表大小不固定,且可能非常巨大。 如果你需要支持全世界所有语言,初始词汇表可能包含成千上万个稀有字符。

而BBPE 抛弃了“字符集”作为起点,而是以 字节(Byte) 作为最小单位。

  • 起点: 所有的文本都被视为其 UTF-8 编码的字节序列。
  • 初始词汇表: 固定的 256 个字节(从 0x00 到 0xFF)。
  • 训练: 传统的 BPE 合并规则应用于这个 256 个字节的初始词汇表。

BBPE 如何实现全覆盖(Universal Coverage)

现代计算机中,所有文本都是以字节形式存储的。UTF-8 编码使用 1 到 4 个字节来表示一个字符。

字符 UTF-8 字节序列
英文 ‘A’ 0x41 (1 字节)
中文 ‘你’ 0xE4 0xBD 0xA0 (3 字节)

由于 BBPE 的初始词汇表包含了所有的 256 个字节,因此它理论上可以表示任何可能的文本输入,包括 Emoji、乱码、甚至是二进制数据。

这确保了 Tokenization 的零 OOV (Out-Of-Vocabulary) 问题:任何输入文本都能被拆分成一系列已知的字节或字节组合,无需使用 <UNK> 标记。

BBPE 的实际技巧:避免无意义的字节组合

虽然 BBPE 从 256 个原始字节开始,但如果直接合并这些原始字节,可能会导致一些奇怪的、无法解码的 Token。例如,如果 0xC30x28 合并了,这可能构成一个无效的 UTF-8 序列。

为了解决这个问题,BBPE 在训练时通常结合使用以下技巧:

A. 字节到字符的映射(Byte-to-Char Mapping)

为了在视觉上保持可读性(因为一个原始字节可能无法显示),BBPE 通常会将原始的 256 个字节映射到 256 个 Unicode 字符上。

  • 特殊处理: 通常会处理那些控制字符(如换行符、制表符)和 ASCII 不可见字符。例如,将 ASCII 0-31 和 127-160 的控制字节映射到 Unicode 中的其他可用区段,保证所有 256 个字节都能被清晰地表示和显示。
  • 好处: 这样训练的 BPE 算法是字节级别的,但输出的 Token 仍然是可读的字符串(仅限英文,中文仍是不可读)。
B. 空格编码

与 SentencePiece 类似,BBPE 也需要处理空格信息以确保可逆性。GPT-2/3 采用的方法是在空格前添加一个特殊字符,最常见的是 Ġ(Unicode 字符 U+0120,一个大写 G 上带点)。

  • 输入: “Hello World”
  • 内部处理: 会被编码为 ĠHelloĠWorld
  • 分词结果: ['ĠHello', 'ĠWorld']

📦 应用场景

  • 避免了为每种语言设计一个特定字符集的问题,是构建大规模多语言模型的理想选择。
  • BPE 算法直接在字节序列上运行,无需复杂的字符集预处理。

BBPE 是 BPE 算法面向现代大规模 Transformer 模型和多语言环境的进化版本。它通过将训练的最小单位固定为字节,实现了全覆盖零OOV,代价是在非拉丁语言上会产生更长的序列。

💻 示例(使用 GPT-2 tokenizer)

1
2
3
4
5
6
7
from transformers import GPT2TokenizerFast

tokenizer = GPT2TokenizerFast.from_pretrained("gpt2")

text = "😊Hello 你好11 The lowest lower technologies are amazing and counterproductive."
print(tokenizer.tokenize(text))
print(tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text)))

输出:

1
2
['ðŁĺ', 'Ĭ', 'Hello', 'Ġ', 'ä½', 'ł', 'å¥', '½', '11', 'ĠThe', 'Ġlowest', 'Ġlower', 'Ġtechnologies', 'Ġare', 'Ġamazing', 'Ġand', 'Ġcounterproductive', '.']
[47249, 232, 15496, 220, 19526, 254, 25001, 121, 1157, 383, 9016, 2793, 8514, 389, 4998, 290, 46674, 13]

可见即使包含 emoji 和中文,也能稳定切分。

并且从输出可以看出,一个中文字符在utf8中常常占3个字节。

Byte-level BPE (BBPE) 是 BPE 算法的一个重要变体,由 OpenAI 率先应用于 GPT-2 模型,并被后续的 GPT-3、GPT-4 和所有现代 BPE 模型(如 RoBERTa、BART)广泛采用。


现代分词算法对比总结

方法 基本原理 是否数据驱动 是否依赖原始空格 优点 缺点 代表模型
传统分词 规则 / 词典 快速可解释;精确匹配已知词 无法处理新词(OOV);无法支持多种语言 Jieba, TF-IDF
BPE (基础版) 最频繁相邻子词合并 简单高效;OOV问题减轻 依赖空格预分割;对罕见字符支持差 GPT-2 (使用BBPE)
WordPiece 最大似然相邻子词合并 子词语义更强;高频词汇拆分更合理 依赖空格预分割;需要特殊标记 ## BERT, ALBERT
SentencePiece (框架) 通用字符流分词器框架 语言无关;支持中/日/韩等无空格语言;完美可逆 需额外的训练和推理库;Token化可能较慢 T5, XLNet, LLaMA
Unigram LM 概率剪枝;Viterbi最优分词 理论严谨;分词路径可采样(正则化) 实现复杂;训练所需资源较大 T5, mT5, Flan-T5
Byte-level BPE (BBPE) UTF-8 字节流 进行 BPE 合并 零 OOV;最鲁棒;真正语言无关 序列变长(尤其中文);Token可读性稍差 GPT-3/4, RoBERTa, BART
 REWARD AUTHOR
 Comments
Comment plugin failed to load
Loading comment plugin