LLM 的分词(Tokenization)详解:文本如何变成 token

LLM 的分词(tokenization),就是把文本切分成称为 token 的小块、再把每个 token 转换成模型能够处理的数字的过程。

在这篇博客里,我们将学习 LLM 的分词------模型在理解任何内容之前,把我们的文本切分成小块的第一步。我们还会看到:为什么需要分词、token 是什么、把文本切成 token 有哪几种方式、字节对编码(Byte Pair Encoding)如何一步一步构建出词表、token 如何变成数字又如何变回文本,以及它在哪里好用、在哪里失灵。

我们将涵盖以下内容:

  • 分词是什么?
  • 为什么需要分词?
  • 什么是 token?
  • 方案一:字符级分词
  • 方案二:词级分词
  • 方案三:子词级分词
  • 字节对编码(BPE)是如何工作的?
  • 词表与 Token ID
  • 解码:从 token 回到文本
  • 用代码看分词实战
  • 特殊 token
  • 分词如何影响 LLM 的实际表现
  • 哪里好用,哪里失灵

分词是什么?

分词(tokenization)是把一段文本切分成称为 token 的小块、再把每个 token 转换成一个数字的过程。

假设我们在 LLM(大语言模型)里输入句子 "I love learning"。

我们看到的这句话是三个单词。但模型看到的和我们不一样。模型不理解字母,也不理解单词,它只懂数字。

所以,在其他任何事情发生之前,这句话会先被切成小块,像下面这样:

复制代码
"I" | " love" | " learning"

然后,每一块被转换成一个数字:

复制代码
40 | 3021 | 6975

模型真正接收到的就是这些数字。文本永远不会到达模型那里,到达的只有数字。

注意:这些数字只是为了便于理解。真实的数字取决于我们用的是哪个分词器(tokenizer)。

干这个活的程序叫分词器(tokenizer)。每个 LLM 都有自己专属的分词器。

为什么需要分词?

LLM 是一个机器学习模型。它的内核是在做大量数学运算------乘数字、加数字、比较数字。数学只对数字起作用,对字母不起作用。

于是问题来了:我们手里的是文本,而模型需要数字。

我们需要一个把文本转换成数字的东西。于是分词器登场了。

但分词器还有另一份工作。模型必须持有一份它认识的"文本块"的固定清单。模型生成回复时,就是从这份固定清单里一次挑一块。如果清单太大,模型会变得又慢又重;如果清单太小,每句话都会变成一长串碎块。

所以,真正的问题是:每一块切多大才合适?

我们会依次看三种方案,并理解为什么现代 LLM 用的是第三种。

什么是 token?

token 是模型处理文本的最小单元。

一个 token 可以是一个完整的单词、单词的一部分、单个字符、一个标点符号,或者"前面的空格加上单词"。

看几个例子:

  • "cat" 是一个 token。
  • "unbelievable" 会被切成三个 token:"un"、"believ"、"able"。
  • "!" 是一个 token。
  • " love"(连同前面的空格)是一个 token。

这里可以注意到,空格是附着在单词上的。大多数现代分词器就是这么做的:空格成为后面那个 token 的一部分。这样一来,模型就知道一个新单词从哪里开始。

英语有个简单的经验法则:一个 token 大约是 4 个字符,也就是大约四分之三个单词。所以 100 个 token 大约是 75 个单词。

接下来看看把文本切成 token 的几种不同方式。

方案一:字符级分词

最简单的想法:每个字符就是一个 token。

拿单词 "learning" 来说,token 会是这样:

复制代码
"l" | "e" | "a" | "r" | "n" | "i" | "n" | "g"

一个单词变成了 8 个 token。

优点:已知文本块的清单非常小。对英语来说,就只有字母、数字和少量标点。而且模型永远不会遇到"不认识的词",因为任何词都由字符组成。

缺点:每句话都会变得非常长。100 个单词的短段落会变成大约 500 个 token,模型得一个一个地处理。而且每个字符自身携带的语义非常少------单独一个字母 "l" 什么也说明不了。模型必须非常费劲地学习"l"、"e"、"a"、"r"、"n" 合在一起才是 learn。

这个方案的问题在于:序列太长,且每个 token 携带的语义太少。看看下一个方案怎么解决。

方案二:词级分词

下一个想法:每个单词就是一个 token。直接按空格切分。

拿句子 "I love learning" 来说,token 会是这样:

复制代码
"I" | "love" | "learning"

三个单词,3 个 token。

优点:每个 token 都携带完整语义,序列很短。

缺点:已知单词的清单会变得非常庞大。光英语就有几十万个单词,再加上人名、地名、其他语言、代码,以及每天都在涌现的新词------这份清单永远没有尽头。

那模型遇到清单里没有的词会怎么样?

假设清单里没有 "tokenization" 这个词。分词器别无选择,只能把它替换成一个叫"未知"(unknown)的特殊 token,通常写作 <UNK>。模型看到 <UNK>,完全不知道我们说了什么。

还有一点值得注意:在这个方案里,"learn"、"learning"、"learned"、"learner" 是四个互不相干的 token。模型并不知道它们是相关的,只能一个一个从头学。

这个方案的问题在于:单词清单太庞大,以及未知词问题。看看下一个方案怎么解决。

方案三:子词级分词

现在,我们走中间路线。

想法很简单:常见词保持为一个 token,罕见词拆成常见的小块。

看几个例子:

  • "the" 非常常见,保持一个 token:"the"
  • "learning" 常见,保持一个 token:"learning"
  • "tokenization" 罕见,拆成两个:"token"、"ization"
  • "unbelievable" 罕见,拆成三个:"un"、"believ"、"able"

可以看到,切出来的块是有意义的。"token" 是一个真实的块,"ization" 也是一个真实的块。这意味着模型可以学到"ization"通常表示"做某件事的行为",并把学到的规律复用到 "organization"、"civilization" 等很多词上。因此模型学得更快。

优点:已知文本块的清单规模可控,通常在 50,000 到 200,000 之间。没有未知词问题,因为任何罕见词都可以一直拆下去,最坏拆到单个字符也总能命中已知的块。而且相关单词共享小块,模型不用每个都从头学。

缺点:有时一个词的切分位置在我们看来不太自然。

几乎所有现代 LLM 用的都是这一种。

我把三种方案的差异整理成表格,方便你理解:

对比项 字符级 词级 子词级
已知清单大小 非常小 非常大 中等
每句话的 token 数 非常多 少 少
未知词问题 从不出现 经常出现 从不出现
单个 token 的语义 非常少 完整 良好
是否用于现代 LLM 否 否 是

现在的问题是:分词器怎么决定哪些块常见、哪些块罕见?这份清单是谁定的?

于是,字节对编码(Byte Pair Encoding)来救场了。

字节对编码(BPE)是如何工作的?

字节对编码(Byte Pair Encoding,BPE)是一种构建 token 清单的算法:从单个字符出发,反复把"最常一起出现的块对"合并成新块。

简单说,BPE 会审视海量文本,找出最常相邻出现的两个块,把它们粘成一个新块,然后不断重复。

学这个算法最好的办法就是来一个例子。

假设训练文本里只有三个单词,出现次数如下:

  • "hug" 出现 10 次
  • "pug" 出现 5 次
  • "hugs" 出现 5 次

第 0 步:把每个单词拆成字符。

复制代码
h u g   (10 次)
p u g   (5 次)
h u g s (5 次)

初始 token 清单:h、u、g、p、s。共 5 个 token。

第 1 步:统计所有"块对"。块对指一个单词内部相邻的两个块。

  • "h" + "u" 出现 10 + 5 = 15 次
  • "u" + "g" 出现 10 + 5 + 5 = 20 次
  • "p" + "u" 出现 5 次
  • "g" + "s" 出现 5 次

最频繁的块对是 "u" + "g",共 20 次。把它们合并成新 token ug。

现在 token 清单:h、u、g、p、s、ug。共 6 个 token。

现在单词长这样:

复制代码
h ug   (10 次)
p ug   (5 次)
h ug s (5 次)

第 2 步:再次统计块对。

  • "h" + "ug" 出现 10 + 5 = 15 次
  • "p" + "ug" 出现 5 次
  • "ug" + "s" 出现 5 次

最频繁的块对是 "h" + "ug",共 15 次。合并成新 token hug。

现在 token 清单:h、u、g、p、s、ug、hug。共 7 个 token。

现在单词长这样:

复制代码
hug   (10 次)
p ug  (5 次)
hug s (5 次)

第 3 步:再次统计块对。

  • "p" + "ug" 出现 5 次
  • "hug" + "s" 出现 5 次

两者打平。这种情况下,分词器按固定规则选一个。我们把 "p" + "ug" 合并成 pug。

现在 token 清单:h、u、g、p、s、ug、hug、pug。共 8 个 token。

不断重复,直到清单达到我们想要的规模。真实分词器的目标规模是事先定好的,比如 100,000 个 token。

这里我们做了三次合并。每次合并都按顺序保存为一条规则:

复制代码
规则 1:u + g   -> ug
规则 2:h + ug  -> hug
规则 3:p + ug  -> pug

之后新文本进来时,分词器不再做任何统计。它只是把文本拆成字符,然后按同样的顺序套用这些规则。

假设新文本是 "pugs"。分词器的工作过程如下:

  • 起点:p、u、g、s
  • 应用规则 1:p、ug、s
  • 应用规则 2:没有可合并的,因为这里没有 "h" + "ug"
  • 应用规则 3:pug、s
  • 最终 token:pug、s

而 "pugs" 这个词从没在训练文本里出现过,我们照样得到了有意义的 token。

注意:在许多现代分词器(比如 GPT 系列模型用的那些)里,起点清单并不是字母,而是全部 256 种可能的字节值。任何语言的任何字符、任何 emoji、任何符号都以字节形式存储,所以分词器永远不会遇到未知的东西。这种变体叫字节级 BPE(byte-level BPE)。

还有一点值得注意:分词器是单独先行训练的,在 LLM 训练之前。token 清单一旦定好就被冻结,LLM 接着用这份冻结清单来训练。LLM 永远不会更改分词器。

词表与 Token ID

词表(vocabulary)是分词器认识的所有 token 的完整清单。

词表里的每个 token 都有一个唯一的编号,叫 Token ID。

可以把它想象成一部每个词条都带序号的词典:分词器查到某个文本块,找到序号,把这个编号交给模型。

对我们这个小例子来说,词表长这样:

token Token ID
h 0
u 1
g 2
p 3
s 4
ug 5
hug 6
pug 7

于是文本 "pugs" 先变成 token pug、s,再变成 Token ID 7、4。

完整旅程是:

复制代码
文本 -> token -> Token ID
"pugs" -> ["pug", "s"] -> [7, 4]

这里,[7, 4] 就是喂给 LLM 的东西。

那么,词表大小为什么重要?LLM 生成回复时,每一步都要从词表里挑一个 token。所以 100,000 个词表意味着模型每一步都要在 100,000 个选项里做选择。词表更大,每句话的 token 更少,但模型更重;词表更小,模型更轻,但序列更长。真实模型会根据自己的使用场景,在两者之间取一个平衡。

注意:在模型内部,每个 Token ID 会用来查一长串数字,叫嵌入(embedding)。真正的"理解"从那里才开始,那是另一篇博客的主题。现在我们只需知道:分词是嵌入之前的那一步。

解码:从 token 回到文本

现在看文本是怎么从模型里出来的。

LLM 并不产出文本,它产出的是 Token ID,一次一个。

假设模型产出了 6、4。分词器反向查词表:

  • 6 是 hug
  • 4 是 s

然后拼接起来:"hugs"。

这个反向步骤叫解码(decoding),和我们之前做的正好相反。

复制代码
Token ID -> token -> 文本
[6, 4] -> ["hug", "s"] -> "hugs"

这就是为什么,当我们看 LLM 在屏幕上打字时,有时会先出现半个单词、然后才出现剩下的部分------那就是 token 边界。模型产出了一个 token,分词器把它解码,然后展示给了我们。

用代码看分词实战

来看整套流程的实际运行。我们用 tiktoken,这是 OpenAI 的库,里面包含 OpenAI 模型所用的分词器。

python 复制代码
import tiktoken

encoding = tiktoken.get_encoding("cl100k_base")

token_ids = encoding.encode("I love learning")
print(token_ids)

text = encoding.decode(token_ids)
print(text)

它会打印出类似这样的结果:

复制代码
[40, 3021, 6975]
I love learning

这里我们用的是 cl100k_base,这是一个分词器的名字,词表约有 100,000 个 token。encode 函数把文本转换成 Token ID,decode 函数把 Token ID 转换回文本。我们拿回了原句,一字不差。

再来看看每个 Token ID 对应哪块文本:

python 复制代码
for token_id in token_ids:
    print(token_id, "->", "[" + encoding.decode([token_id]) + "]")

它会打印:

复制代码
40 -> [I]
3021 -> [ love]
6975 -> [ learning]

可以看到,空格确实附着在单词上,和我们前面讨论的一致:" love" 是一个 token," learning" 是一个 token。

注意:具体的 Token ID 取决于分词器。换一个模型、换一个分词器,同一句话得到的数字就不一样。

特殊 token

除了从文本切出来的 token,词表里还有几个特殊 token。它们不是单词,是信号。

几个重要的:

  • 文本结束 (End of text):告诉模型文本到此为止,通常写作 <|endoftext|> 或 <eos>。模型产出这个 token 时,就停止生成。
  • 文本开始 (Beginning of text):标记文本从哪里开始,通常写作 <bos>。
  • 填充(Padding):占位用的 token,用来把多个输入补齐到同一长度,方便模型一起处理。
  • 对话标记(Chat markers):标记用户消息的起止位置、助手回复的起始位置。对话模型靠这个知道谁说了什么。

这里可以注意到:特殊 token 永远不会由切分普通文本产生,它们是分词器或模型外层系统有意添加的。

分词如何影响 LLM 的实际表现

分词发生在模型之前,而模型永远无法看到 token 内部。这一个事实就能解释 LLM 做出的许多怪事。

数字母:假设我们问,"strawberry" 里字母 "r" 出现了几次?分词器把这个词切成了类似 "str"、"aw"、"berry" 的块。模型看到的是三个 Token ID,从没见过单个字母。所以它只能靠记忆去猜这些块里有几个 "r"。这就是 LLM 有时会答错这类问题的原因。

数字算术:数字 "12345" 会被切成 "123"、"45" 这样的块。模型看到的不是五个数位,而是两个 token。在这些块上做加法,比逐位做加法难得多。

成本与限制:每个 LLM 一次能读的 token 数有上限,这就是上下文窗口(context window)。而 LLM 的计价也按 token 算。所以 token 数量同时决定了"装不装得下"和"花多少钱"。

其他语言:大多数分词器训练用的文本以英语为主,所以英语单词的 token 很紧凑。而印地语、泰米尔语、日语的句子会被切成多得多的碎块。同一句话在这些语言里消耗的 token 可能是英语的好几倍------意味着成本更高、上下文窗口被填得更快。

代码:空格和缩进也是 token。四格缩进可能是一个 token,也可能是四个 token,取决于分词器。这会影响模型读写代码的质量。

哪里好用,哪里失灵

总结一下子词分词好用的地方:

  • 它能处理任何输入,没有未知词问题。
  • 它让常见文本的序列保持得很短。
  • 它让相关单词共享有意义的块,模型学得更快。
  • 它凭一份词表就能同时服务文本、代码和多种语言。

再看它失灵的地方:

  • 需要看到单个字母的任务,比如数字母、反转单词。
  • 长数字的算术,因为数位会被按不自然的方式分组。
  • 训练文本中占比低的语言------token 更多,意味着成本更高、上下文窗口余量更小。
  • 任何被从奇怪位置切开的词都可能让模型困惑,因为切出来的块承载的不是我们以为的语义。

到这里,我们必须已经理解了:LLM 的分词是什么、BPE 怎么构建词表、文本怎么变成 Token ID 又怎么变回来,以及它在哪里好用、在哪里失灵。

常见问题

所有 LLM 都用同一个分词器吗?

不。每个 LLM 都有自己专属的分词器。不同的模型配上不同的分词器,同一句话会得到不同的 Token ID。所以我们拿到的具体数字,永远取决于用的是哪个分词器。

LLM 会在训练过程中更换分词器吗?

不会。分词器是单独先行训练的,在 LLM 训练之前。token 清单一旦定好就被冻结,LLM 接着用这份冻结清单来训练。LLM 永远不会更改分词器。

100 个 token 是多少个单词?

对英语来说,100 个 token 大约是 75 个单词。简单的经验法则:一个 token 大约 4 个字符,也就是大约四分之三个单词。

为什么 LLM 数不清一个单词里的字母?

因为模型从没见过单个字母。像 "strawberry" 这样的词会被切成 "str"、"aw"、"berry" 这样的块,模型只看到它们的 Token ID。所以它只能靠记忆去猜某个字母在这些块里出现了几次。

为什么印地语或日语的文本比英语费更多 token?

大多数分词器训练用的文本以英语为主,所以英语单词的 token 很紧凑。印地语、泰米尔语、日语的句子会被切成多得多的碎块。这意味着成本更高、上下文窗口被填得更快。

相关推荐
Pocker_Spades_A1 小时前
论文精读(十四):基于先验地图的视觉重定位方法综述:启元机器人“知道自己在哪“的底层能力
大数据·人工智能·机器人
昨日之日20061 小时前
FLUX 3 Image Klein:一键搞定文生图和智能编辑,支持精准布局,黑森林新开源
人工智能·计算机视觉·ai作画
Chengbei111 小时前
JsOwl|AI 赋能 JS 敏感信息扫描器,AST + 正则、source‑map 源码还原、登录态爬虫输出交互式 HTML 报告
开发语言·javascript·网络·人工智能·爬虫·网络安全·自动化
leoZ2311 小时前
第 31 篇 金融与医疗:高合规行业的 AI 产品方法论
大数据·人工智能·金融·大模型·agent
细节处有神明1 小时前
零样本时序预测:Mistral Timestral 本地实测
人工智能·机器学习·时间序列
阿明副业观察1 小时前
AI短剧分镜秘籍:从零到电影级视觉冲击
大数据·人工智能·aigc·音视频·ai写作
艾克斯观察1 小时前
做产品正在失去稀缺性,功能即护城河的时代结束了?
人工智能·产品运营·逆向工程·rea·渠道分发
邓工说电1 小时前
智慧用电常见问题全解答:概念辨析、技术构成、落地路径与选型判断
人工智能
迪飞特科技1 小时前
业务是否适合 AI 改造,3 个可落地的判断评估标准
人工智能