大模型背后的“黑魔法”:深入剖析 Tokenization(BPE、WordPiece 与 SentencePiece)
当我们惊叹于 ChatGPT 的对答如流,或者沉醉于 Llama 3 生成的优美代码时,我们通常会将功劳归结于模型庞大的参数量、精妙的 Transformer 架构或是海量的训练数据。然而,在这些宏大的叙事之下,隐藏着一个经常被忽视、却至关重要的基础组件:Tokenization(分词)。
大模型并不直接阅读由 26 个字母组成的单词,也看不懂成千上万个汉字。它们的世界里只有数字。如何将人类的自然语言完美地转换为模型能够理解的数字序列?这就是 Tokenization 的使命。
如果把大模型比作一台高性能的发动机,那么 Tokenizer 就是那台精密的燃油喷射系统。如果燃油(文本)没有被打散、雾化成恰到好处的颗粒(Token),发动机就无法正常运转,甚至会直接熄火(报错超出最大长度)。
在这篇文章中,我们将深入探讨大模型中 Tokenization 的核心原理,并全面拆解目前业界最主流的三种分词算法:BPE、WordPiece 与 SentencePiece。同时,我们将结合实际的 Python 代码,带你揭开这层“黑魔法”的面纱。
一、 为什么我们需要复杂的 Tokenization?
在深入算法之前,我们先来聊聊背景。为什么不让模型直接按“字符”或按“单词”来学习呢?
1. 按单词分词的困境
最直观的想法是按照空格和标点符号,将文本切分成一个个完整的单词。
- 缺点 1:词表过大。 英语中虽然有十几万个单词,但加上各种词形变化(如
play,playing,played)和专业术语,词汇量几乎是无限的。如果词表无限大,模型的 Embedding 层和输出层(Softmax)就会变得无比庞大,导致显存爆炸。 - 缺点 2:OOV(Out-of-Vocabulary,未登录词)问题。 无论词表多大,总会遇到没见过的词。如果模型在推理时遇到了一个不在词表里的词,它就会彻底“懵逼”,只能输出一个
<UNK>(Unknown)标记。大量出现<UNK>会严重破坏模型的理解能力。 - 缺点 3:对多语言不友好。 中文、日文等亚洲语言本身就没有明显的空格分隔,按单词切分根本行不通。
2. 按字符分词的困境
既然按单词不行,那我们按字母(a, b, c)或者单个汉字(你, 好)来切分呢?
- 优点: 词表极小(英文只需 26 个字母加一些符号),完全没有 OOV 问题。
- 缺点: 序列太长!一句 “I love programming” 按单词只有 3 个 Token,按字符则有 18 个 Token。由于 Transformer 模型的计算复杂度是序列长度的平方(),过长的序列会导致计算量呈指数级爆炸。此外,单个字母或汉字往往缺乏独立的语义(比如字母 ‘a’ 本身没有具体含义)。
3. 完美的平衡点:子词分词
为了解决上述问题,业界引入了子词分词算法。它的核心思想是:高频词保留,低频词拆解。
- 对于像
apple,love这样常见的完整单词,模型将它们作为一个整体的 Token。 - 对于像
unfriend这样的低频词或未见过的词,模型将其拆解为有意义的子词:un和friend。 - 这样一来,既控制了词表的大小(通常在 3万 到 15万 之间),又彻底解决了 OOV 问题,同时还大幅缩短了序列长度。
目前主流的大模型(GPT 系列、BERT、Llama 等)无一例外地采用了子词分词技术。
二、 核心算法深度解析
接下来,我们将深入剖析三大主流子词分词算法。
1. 字节对编码—— GPT 系列的基石
BPE 最早是一种数据压缩算法,后来被引入到 NLP 领域。它的核心逻辑非常简单:寻找高频出现的字符对,并将它们合并为一个新的 Token。
训练过程(如何构建词表):
- 准备语料: 将训练文本拆分为字符级别的初始序列,并在单词结尾添加特殊的结束符
</w>(用来区分词内和词尾)。 - 统计频率: 统计语料中相邻字符对的出现频率。
- 合并: 将出现频率最高的字符对合并为一个新的字符(子词)。
- 迭代: 重复步骤 2 和 3,直到达到预设的词表大小。
举个直观的例子:
假设我们的语料库中只有两个单词:low (出现了 5 次),lower (出现了 2 次)。
-
初始状态(字符级别):
l o w </w>(5次)
l o w e r </w>(2次) -
统计字符对频率:
l和o相邻出现了 7 次 (5+2)
o和w相邻出现了 7 次 (5+2)
w和</w>相邻出现了 5 次
… 等等。 -
第一次合并: 假设
l和o频率最高,合并为lo。
语料变成:
lo w </w>(5次)
lo w e r </w>(2次) -
第二次合并:
lo和w频率最高,合并为low。
语料变成:
low </w>(5次)
low e r </w>(2次)
以此类推,最终 low 就成了一个不可分割的 Token 存入词表中。
编码过程(如何切分新文本):
当遇到一个新句子时,我们从左到右,按照训练时生成的合并规则的优先级依次进行合并。如果遇到词表中不存在的生僻字,就会保留为单个字符。
Python 代码实现简易 BPE 训练
为了让你更直观地理解,这里提供一段极简的 BPE 训练代码:
1 | import re |
2. WordPiece —— BERT 的选择
WordPiece 算法与 BPE 非常相似,它最早由谷歌为语音搜索系统提出,后来被广泛应用于 BERT 及其衍生模型(如 RoBERTa, DistilBERT)中。
核心区别:合并的判断标准
- BPE: 统计字符对的绝对出现频率,合并频率最高的字符对。
- WordPiece: 使用似然概率来评估字符对。它不仅考虑了字符对出现的频率,还考虑了组成该字符对的各个部分单独出现的频率。
其打分公式为:
为什么这么做?
假设 th 出现了 1000 次,er 出现了 1000 次,但单独的 t 和 h 出现的次数极少,而 e 和 r 到处都是。
BPE 可能会同等对待 th 和 er。但 WordPiece 会认为:既然 e 和 r 单独出现的概率已经很高了,把它们强行绑在一起意义不大;反而 th 结合得如此紧密(因为单独的 t 和 h 少),更值得作为一个整体 Token。
特殊的表示方式
如果你仔细看过 BERT 的词表,会发现 WordPiece 在切分子词时,有一个非常明显的特征:
- 如果是一个词的开头,则正常表示(如
play)。 - 如果是一个词的中间或结尾部分,则在前面加上
##(如##ing)。
例如单词 unwanted,可能会被切分为:['un', '##want', '##ed']。这种表示方法让模型明确知道哪些部分是属于同一个原始单词的。
3. SentencePiece —— 跨语言的终极解法
无论是 BPE 还是 WordPiece,它们在底层都存在一个巨大的前提假设:文本是由空格分隔的单词组成的。这个假设在英文等印欧语系中成立,但在中文、日文、韩文(CJK)等非空格分隔的语言中,这个假设直接崩塌。
为了解决多语言问题,Google 开源了 SentencePiece 工具。Llama、T5、BLOOM 等现代大模型都采用了 SentencePiece。
核心创新:将文本视为纯字节流
SentencePiece 完全抛弃了“空格即单词边界”的传统观念。它将所有的输入文本(包括空格、标点符号、中英文字符)统统转化为 UTF-8 字节流。
在 SentencePiece 的眼中,文本就是一个长长的字符串序列,空格只是一个普通的字符(通常被特殊编码为 _ 或者 ▁,即下划线)。
SentencePiece 包含的两种主流算法
SentencePiece 是一个工具包,它内部支持多种子词算法,最常用的有两种:
unigram(一元语言模型): 这是 SentencePiece 的默认算法。与 BPE 自底向上的合并不同,Unigram 是自顶向下的。它一开始初始化一个巨大的词表,然后通过评估移除某个 Token 对整体语言模型似然度(Loss)的影响,逐步修剪、删除不重要的 Token,直到达到目标词表大小。bpe: SentencePiece 也实现了经典的 BPE 算法,但处理粒度从字符变成了字节。
为什么现代大模型偏爱 SentencePiece?
- 真正的语言无关性: 无论输入的是中英混杂、代码块还是特殊符号,它都能统一处理,不需要针对不同语言写复杂的预处理脚本。
- 纯端到端: 从原始文本到 Token IDs 的映射完全自动化,无需手动分词。
- 极度的灵活性: 能够有效压缩如中文这样以字符为主的语言的序列长度。
三、 大模型实战:Tokenizer 的使用与对比
理论讲完了,让我们来看看在实际的大模型开发中,我们是如何使用 Tokenizer 的。目前最主流的工具库是 HuggingFace 的 transformers 和 tokenizers。
我们将对比 GPT-2 (BPE) 和 BERT (WordPiece) 在处理中英文混合文本时的表现。
环境准备
1 | pip install transformers |
实战代码
1 | from transformers import AutoTokenizer |
结果分析与对比
对于句子:"大语言模型(LLM)正在改变世界!Let's explore the future.",各大模型的切分差异非常大:
GPT-2 的表现(经典 BPE)
GPT-2 的词表主要是英文,对中文支持较差。它的切分结果会像这样:
大->'大','§'(中文字符被强行拆解为 UTF-8 字节,乱码状)LLM->'LL','M'!->'!'
评价: GPT-2 处理中文的效率极低,一个汉字往往需要消耗 3 个甚至更多的 Token,这极大地浪费了模型的上下文长度(Context Window)。
BERT 的表现
BERT 的词表中包含中文字符,它的切分结果:
大语言模型->['大', '语', '言', '模', '型'](逐字切分,因为中文成语在词表中可能不是高频词组)LLM->['ll', '##m'](转小写,并使用了##前缀表示子词)Let's->['let', "'", '##s']
评价: BERT 能够正确处理中文,但为了控制词表大小,主要以单字为主。其标志性的##前缀清晰地标明了词的边界。
Llama / 现代大模型的表现
现代大模型(如 Llama 3, ChatGLM)为了提高多语言效率,在词表中加入了大量的中文词组和字节级 Token。切分结果可能如下:
大语言模型->['大语言', '模型'](将高频词组作为完整 Token)正在->['正在']!->'!'或可能合并为世界!
评价: 极大地压缩了非英语文本的 Token 数量。这意味着同样的一段中文,在新模型中可以比在 GPT-2 中容纳更多的信息,这也是为什么现代大模型上下文窗口越来越长的原因之一(不仅是模型结构的优化,也是 Token 压缩率的提升)。
四、 总结与大模型的未来
Tokenization 是自然语言处理中一座连接人类语言与机器数字的桥梁。从传统的基于规则的分词,到如今主导大模型架构的三大核心算法,我们见证了文本处理方式的进化:
- BPE (GPT系列): 通过最高频的字符对自底向上合并,直观高效。
- WordPiece (BERT系列): 同样是合并,但基于语言模型的似然概率,并使用
##标记子词,更注重词边界的语义。 - SentencePiece (Llama/T5系列): 将文本视为纯字节流,剥离了对空格等语言的先验假设,真正实现了语言无关的跨语言大一统分词。
未来的发展:我们还需要 Tokenization 吗?
尽管现在的 Tokenization 技术已经非常成熟,但它依然存在痛点。比如:
- 反直觉的切分: 模型可能把
"(引号) 和前面的字母切在一起,导致你很难通过字符串匹配来精准删除文本中的某一段敏感信息。 - 推理时的性能瓶颈: Tokenization 过程通常在 CPU 上运行,涉及大量的字符串匹配和正则表达式,这在实时流式输出时往往成为速度的瓶颈。
在业界,一种新的趋势正在悄然兴起:Byte-Level Models(字节级模型)。
例如,2023 年底 Meta 提出的 MegaByte 架构,或者最新的研究直接尝试让模型读取原始的 UTF-8 字节(总共只有 256 个 Token)。这种方案彻底抛弃了庞大的词表和复杂的 Tokenization 训练过程。
虽然目前字节级模型在性能和长序列处理上还不如基于 Transformer 的传统大模型,但这也许才是真正通向 AGI(通用人工智能)的终极道路——让模型像人类一样,直接看清构成文本的每一个最基础的字节。
Tokenization 也许在未来某一天会退出历史舞台,但理解它的原理,对于我们理解今天的大模型为何如此强大,以及如何优化大模型的 Prompt、控制 API 调用成本(毕竟按 Token 计费),都有着不可替代的价值。