Chapter3: 大语言模型基础 Part 7:大模型如何“识字”?——分词器 (Tokenizer) 与 BPE 算法
摘要: 分词(Tokenization)是大模型处理文本的基础环节,将字符转换为数字序列。早期方法(按词或字符分词)存在词表爆炸或语义缺失问题,现代模型普遍采用子词分词(如BPE算法),通过合并高频字符对构建词表,平衡语义与效率。分词器影响显著:1)模型上下文窗口按Token计数,中文更占资源;2)API成本与Token数量挂钩;3)分词差异可能导致模型表现异常(如数学运算错误)。开发者可通过调整
基于Datawhale的hello-agent开源项目做的一些笔记,内容仅供参考,原PDF以及代码可以去github仓库获取https://datawhalechina.github.io/hello-agents
在上一篇中,我们学习了如何通过提示工程(Prompt Engineering)与大模型高效沟通。
但你是否想过,当我们输入“你好”或“Hello”时,计算机究竟是如何“读懂”这些字符的?事实上,大模型根本不认识字,它只认识数字。将文本转化为数字序列的过程,就是本篇的主角——分词 (Tokenization)。
这是大模型最底层、最基础,却也最容易被忽视的环节。很多时候模型表现出的“弱智”行为(比如算不对加法),根源都在这里。
3.2.2 文本分词 (Tokenization)
我们知道,计算机本质上只能理解数字。因此,在将自然语言文本喂给大语言模型之前,必须先将其转换成模型能够处理的数字格式。这个过程就叫做分词。
分词器 (Tokenizer) 的作用,就是定义一套规则,将原始文本切分成一个个最小的单元,我们称之为词元 (Token) 。
3.2.2.1 为何需要分词?
早期的自然语言处理任务可能会采用简单的分词策略,但它们都有明显的缺陷 :
-
按词分词 (Word-based):直接用空格分词。
-
缺陷:词表爆炸。英语单词变化太多(learn, learns, learned, learning…),如果每个变体都存,词表会大到无法管理。且无法处理未见过的词(OOV 问题)。
-
按字符分词 (Character-based):切分成单个字母。
-
缺陷:语义缺失。单个字母 ‘a’ 或 ‘b’ 通常没有独立语义,模型需要花费大量精力去学习如何拼写,导致学习效率低下。
现代主流:子词分词 (Subword Tokenization)
为了兼顾词表大小和语义表达,现代大模型(如 GPT、Llama)普遍采用子词分词。
核心思想:将常见的词(如 “agent”)保留为完整的词元,同时将不常见的词(如 “Tokenization”)拆分成多个有意义的子词片段(如 “Token” 和 “ization”)。
3.2.2.2 字节对编码 (BPE) 算法解析
字节对编码 (Byte-Pair Encoding, BPE) 是最主流的子词分词算法之一,GPT 系列就采用了它。
其核心思想是一个贪心的合并过程 :
- 初始化:将词表初始化为所有在语料库中出现过的基本字符。
- 迭代合并:统计所有相邻词元对的出现频率,找到频率最高的一对,合并成一个新的词元。
- 重复:重复第 2 步,直到词表大小达到预设阈值。
案例演示
假设我们的迷你语料库是 {"hug": 1, "pug": 1, "pun": 1, "bun": 1},目标是构建一个大小为 10 的词表。
| 步骤 | 最高频词元对 | 合并为新增词元 | 当前词表 (示例) |
|---|---|---|---|
| 初始化 | - | - | {h, u, g, p, n, b} (6个) |
| 合并 1 | u, g | ug | {ug, h, p, n, b} (7个) |
| 合并 2 | u, n | un | {ug, un, h, p, b} (8个) |
| 合并 3 | p, ug | pug | {pug, un, h, b} (9个) |
| 合并 4 | p, un | pun | {pug, pun, h, b} (10个) |
代码实现:BPE 算法模拟
为了深入理解 BPE,我们用一段 Python 代码来模拟这个“迭代合并”的过程 。
import re, collections
def get_stats(vocab):
"""统计词元对频率"""
pairs = collections.defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pairs[symbols[i], symbols[i+1]] += freq
return pairs
def merge_vocab(pair, v_in):
"""合并词元对"""
v_out = {}
bigram = re.escape(''.join(pair))
# 使用正则匹配,注意负向预查防止错误匹配
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
for word in v_in:
w_out = p.sub(''.join(pair), word)
v_out[w_out] = v_in[word]
return v_out
# 准备语料库,每个词末尾加上 </w> 表示结束,并切分好字符
vocab = {'h u g </w>': 1, 'p u g </w>': 1, 'p u n </w>': 1, 'b u n </w>': 1}
num_merges = 4 # 设置合并次数
for i in range(num_merges):
pairs = get_stats(vocab)
if not pairs:
break
best = max(pairs, key=pairs.get)
vocab = merge_vocab(best, vocab)
print(f"第{i+1}次合并: {best} -> {''.join(best)}")
print(f"新词表(部分): {list(vocab.keys())}")
print("-" * 20)
运行结果解析:
你会看到,算法首先发现了 u 和 g 经常一起出现,于是合并成 ug。接着发现 u 和 n 经常一起出现,合并成 un。最终,原本零散的字符被组合成了我们认识的单词片段。
其他主流算法 :
- WordPiece (BERT 使用):与 BPE 类似,但合并标准是看“能否最大化提升语言模型概率”,而不仅仅是频率。
- SentencePiece (Llama 使用):最大的特点是将空格也视作一个普通字符(通常用下划线
_表示),这使得分词过程完全可逆。
3.2.2.3 分词器对开发者的意义
理解这些底层细节,对我们开发智能体有什么实际用处呢?非常大!
- 上下文窗口限制 (Context Window):
模型的记忆限制(如 8K, 128K)是按 Token 数量计算的,而不是字符数。
- 注意:中文通常比英文占更多的 Token(一个汉字可能对应 1-2 个 Token,而一个英文单词可能只是 1 个 Token)。精确计算 Token 数是管理长时记忆的基础。
- API 成本:
大多数模型 API(如 OpenAI, DeepSeek)都是按 Token 数量计费的。了解分词机制有助于预估成本。 - 模型表现的“诡异”陷阱:
你是否遇到过模型算不对2+2?
- 原因:在某些分词器下,
2+2(无空格)可能被切分成['2', '+', '2'],甚至可能被作为一个未见过的整体乱码处理;而2 + 2(有空格)则会被清晰地切分。 - 大小写敏感:一个词的首字母大写与小写,可能会被切分成完全不同的 Token ID,导致模型对它们的理解产生偏差。
💡 注解:
在设计 Prompt 时,如果模型对某个词理解不到位,试着在它周围加空格,或者改变大小写,有时会有奇效。这本质上是在辅助分词器正确地切分语义单元。
更多推荐



所有评论(0)