本篇文章介绍大语言模型(LLM)中常用的几种分词(Tokenizer)方法,包括传统分词、BPE、WordPiece、SentencePiece
以及 Unigram Language
Model。我们将通过原理解析、应用场景和代码示例,帮助读者理解这些分词技术在
LLM 中的作用与实现。
💡
引言:大语言模型(LLM)的基石——Tokenizer
“一切输入皆为序列”——这是所有现代深度学习模型处理数据的核心哲学,对于大语言模型(LLM)而言更是如此。
无论我们输入的是一个中文句子、一段英文代码、还是一串生动的 Emoji
表情,LLM
都无法直接理解这些人类语言的字符串(String)。它们只能接收并处理数字形式的输入。因此,我们迫切需要一座“翻译桥梁”,将人类可读的文本转化为机器可计算的数字序列,这座桥梁就是
Tokenizer(分词器)。
为什么 Tokenizer 是 LLM
的基石?
机器可读性(Input Conversion):Tokenizer
将文本切分成最小的语义单元——Token,并用唯一的
ID(索引号)
来表示,使之成为模型可以处理的数字输入。
词汇表与计算效率(Vocabulary &
Efficiency):一个理想的 Tokenizer
必须在词汇表(Vocabulary)大小和序列长度(Sequence
Length)之间找到平衡点。
如果词汇表过大(例如,包含所有可能的单词),模型训练将变得极其缓慢且难以泛化。
如果 Token
粒度过细(例如,全部分割成单字符),虽然词汇表小了,但文本序列会变得太长,Self-Attention
机制的计算成本将以序列长度的平方 O(N2)
爆炸式增长。
泛化能力与 OOV 问题(Generalization &
OOV):Tokenizer
的切分策略直接决定了模型处理未登录词(OOV,
Out-Of-Vocabulary)的能力。像 BPE 和
WordPiece
这样的子词(Subword)分词方法,通过将新词拆分成已知的词根或子词,显著增强了模型的泛化能力。
1️⃣ 传统分词(Rule-based /
Word-level)
🔍 原理
传统分词基于空格或人工规则进行切分。
英文:直接用空格拆词。
中文:需要词典和规则(如结巴分词、THULAC 等)。
无法处理未登录词(OOV),粒度较粗。
📦 应用场景
传统 NLP 特征工程(TF-IDF、BM25)
不依赖大模型的任务(情感分析、分类)
💻 示例(jieba 中文分词)
12345import jiebatext = "我爱自然语言处理 i like natural language process"tokens = list(jieba.cut(text))print(tokens)
输出:
1['我', '爱', '自然语言', '处理', ' ', 'i', ' ', 'like', ' ', 'natural', ' ', 'language', ' ', 'process']
2️⃣ BPE(Byte Pair Encoding)
🔍 原理
BPE 是一种 基于频率的子词切分算法。
从字符级开始(如 “l”, “o”, “w”, “e”, “r”)。
找出频率最高的相邻对(如 “l” + “o” → “lo”)。
合并成新的子词,不断重复,直到词汇表大小达到限制。
在推理时按最长匹配优先进行切分。
📦 应用场景
GPT-2、RoBERTa、LLaMA
英文效果好,可应对拼写变化与新词
💻 示例(使用 tokenizers
库)
123456789101112131415161718192021222324252627282930313233343536373839from 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=["", ""])# 设置预分词规则(这里用空格分词)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))
输出:
123456词表大小: 50[('', 2), ('', 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:
123456词表大小: 96[('', 2), ('', 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
都出现在词表中
只有一些低频、长尾词被拆分,例如:
amazing → a, m,
a, in, g
分词粒度更粗,保留更多完整单词,而非拆成单字或极小子词。
在此基础上,设置min_frequency=2:
123456词表大小: 39[('', 2), ('', 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。
我们用一个简单的例子来说明:
假设我们要决定是合并 A 和 B 呢,还是合并
C 和 D。
count(A) 表示 A 出现的总次数。
count(A, B) 表示 A 和 B
相邻出现的总次数。
1. BPE 的打分方式(频率)
BPE 只看相邻次数: Score(A, B) = count(A, B)
如果 count(A, B) = 100 且
count(C, D) = 90,BPE会选择合并 A, B。
2. WordPiece 的打分方式(似然)
WordPiece 认为,仅仅看 count(A, B) 是不够的。如果
A 和 B 本身都是超级高频的字母(比如 ‘e’ 和
‘s’),它们俩相邻出现100次可能纯属巧合。
WordPiece 的打分公式(简化后)是:
Score(A, B) = count(A, B) / ( count(A) * count(B) )
这个公式在计算 A 和 B 相邻出现的概率
count(A, B)
的同时,还除以了它们各自独立出现的概率
count(A) 和 count(B)。
这实际上是在计算一个“点互信息 (Pointwise Mutual Information,
PMI)”: A 和 B
一起出现的概率,相比于它们各自独立、“碰巧”出现在一起的概率,要高出多少?
举个例子:为什么 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')。
解读: t 和 h
结合在一起,比它们“随机”碰在一起的概率高得多,这说明 th
是一个强相关的“语义单元”。而 e 和 s
虽然也经常挨在一起,但因为它们各自都太常用了,所以它们挨在一起很可能只是“巧合”,es
并不是一个像 th 一样牢固的单元。
因此,WordPiece 会优先合并 ('t', 'h')。
BPE 是“频率驱动”的,简单粗暴。而 WordPiece
是“概率驱动”的,它通过除以单个 token
的频率,惩罚了那些由高频、无意义的 token
组成的“巧合”pair,从而优先保留了像
th、ing、pro
这样真正具有强内部相关性的“语义单元”。
📦 应用场景
BERT、ALBERT、DistilBERT
英文任务常用,能兼顾泛化性与语义一致性
💻 示例(Hugging
Face自带的WordPiece)
12345678910111213from transformers import BertTokenizerFasttokenizer = 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)
输出:
123词表大小: 30522Tokens: ['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。那么下面两个完全不同的输入会产生完全相同的结果:
"counterproductive" (一个词) ->
['counter', 'pro', 'ductive']
"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”)会有两种形式:
pro
(无前缀):表示这是一个词的中间或后缀(等同于 WordPiece
的 ##pro)。
Ġpro (带前缀 Ġ 或
):表示这是一个新词的开头(等同于
WordPiece 的 pro)。
我们用 BPE 来处理同样的两个句子:
句子 1: "counterproductive"
分词结果: ['counter', 'pro', 'ductive'] (这里的
counter 是词首,pro 和 ductive
是非词首)
解码:counter + (无Ġ)pro +
(无Ġ)ductive -> “counterproductive”
句子 2: "counter pro ductive"
分词结果: ['counter', 'Ġpro', 'Ġductive']
解码:counter + (有Ġ)pro +
(有Ġ)ductive -> “counter pro ductive”
两种算法都解决了歧义问题,只是标记策略相反。WordPiece
标记“非词首”,而 BPE/SentencePiece 标记“词首”。
12345678910111213141516from transformers import AutoTokenizermodel_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
不依赖空格或人工分词规则,而是直接在原始字符流上学习子词单元。
将空格也视作一种符号(用 _ 来替换所有空格)
使用两种常用模型进行分词:BPE 和 Unigram
📦 应用场景
T5、ALBERT、mBERT
适用于中英混合、多语言模型
💻 示例(SentencePiece)
123456789101112131415from 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)
输出:
12Tokens: ['▁The', '▁lowest', '▁lower', '▁technologies', '▁are', '▁amazing', '▁and', '▁counter', 'productive', '.']Tokens: ['▁', '我爱自然语言处理', ',', '并且明天早上吃什么', '?', '▁', '我爱吃枇杷', '!']
“▁”
表示空格位置,进而也可以表示该token是不是一个单词或一句话的第一个token
当然,这里t5-base输出的中文分词粒度很粗,是因为其训练语料中中文数据很少
我们可以换几个用其他中文数据集训练的T5模型试试:
123456from transformers import AutoTokenizertok2 = 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: 初始化
准备: 将语料库进行 SentencePiece 预处理(用 _ 替换空格)。
构建大词汇表 Σ: 收集语料库中所有长度在
1 到某个最大值(如
5)之间的所有子字符串。这个初始词汇表 Σ 非常庞大。
计算初始概率: 对 Σ 中的每个子词 x,计算其在语料库中的初始频率。
阶段 II: 迭代优化(剪枝)
这个阶段是 ULM 的核心,通过 EM (Expectation-Maximization)
算法的思想来优化词汇表 Σ
和子词概率 P(x)。
E-步(计算概率): 使用当前的词汇表 Σ 和子词概率 P(x),对语料库中的每个句子
S,找到最佳分词(即概率最大的分词路径)。这通常通过
Viterbi 算法实现,因为它需要找到 max ∏P(xi)。
M-步(计算损失 / 剪枝):
计算损失: 对于词汇表中的每个子词 x,暂时将其删除,然后重新计算语料库的总似然。
计算贡献度:
找出那些删除后对总似然影响最小(即贡献度最低)的子词。
剪枝: 移除词汇表中贡献度最低的 p %(例如 20%)的子词。
重复: 重复 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)
问题:任何输入文本都能被拆分成一系列已知的字节或字节组合,无需使用
BBPE
的实际技巧:避免无意义的字节组合
虽然 BBPE 从 256
个原始字节开始,但如果直接合并这些原始字节,可能会导致一些奇怪的、无法解码的
Token。例如,如果 0xC3 和 0x28
合并了,这可能构成一个无效的 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)
1234567from transformers import GPT2TokenizerFasttokenizer = 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)))
输出:
12['ðŁĺ', 'Ĭ', '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