LLM 中的分词是将文本拆分成称为 token 的小片段,然后将每个 token 转换为模型可以处理的数字的过程。

在这篇博客中,我们将学习 LLM 中的分词——这是模型在理解任何内容之前将我们的文本拆分成小片段的第一步。我们还将了解为什么需要分词、什么是 token、将文本拆分成 token 的不同方法、Byte Pair Encoding 如何逐步构建词表、token 如何变成数字再变回文本,以及它在哪些情况下表现良好、在哪些情况下会失败。

我们将涵盖以下内容:

  • 什么是分词?
  • 为什么我们需要分词?
  • 什么是 Token?
  • 方法一:字符级分词
  • 方法二:词级分词
  • 方法三:子词级分词
  • Byte Pair Encoding(BPE)是如何工作的?
  • 词表与 Token ID
  • 解码:从 Token 回到文本
  • 用代码实战演示分词
  • 特殊 Token
  • 分词在实践中如何影响 LLM
  • 它在哪些情况下表现良好,在哪些情况下会失败

我是 Amit Shekhar,Outcome School 的创始人。我曾指导和培养过许多开发者,他们的努力帮助他们获得了高薪的技术工作,帮助许多科技公司解决了他们的独特问题,并创建了许多被顶级公司使用的开源库。我热衷于通过开源、博客和视频分享知识。

我在 Outcome School 教授 AI 和机器学习。

让我们开始吧。

什么是分词?

分词是将一段文本拆分成称为 token 的小片段,然后将每个 token 转换为数字的过程。

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

我们看到的是三个单词。但模型看待它的方式与我们不同。模型不理解字母或单词。它只理解数字。

所以,在其他任何事情发生之前,这个句子会先被拆分成如下的小片段:

"I" | " love" | " learning"

然后,每个片段会被转换为如下所示的数字:

40 | 3021 | 6975

这些数字才是模型实际接收到的内容。文本永远不会到达模型,只有数字会到达模型。

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

执行这项工作的程序称为分词器。每个 LLM 都有自己的分词器。

为什么我们需要分词?

LLM 是一个机器学习模型。它的核心是进行大量的数学运算。它对数字进行乘法、加法和比较。数学运算的对象是数字,而不是字母。

所以,我们遇到了一个问题:我们拥有的是文本,而模型需要的是数字。

我们需要一种能把文本转换成数字的方法。于是,分词器(tokenizer)就登场了。

不过,分词器还有另一项任务。模型必须拥有一份固定的、它所认识的片段列表。当模型生成回复时,它会从这个固定列表中一次挑选一个片段。如果列表太大,模型就会变得缓慢而笨重。如果列表太小,每个句子都会变成一长串微小的片段。

所以,真正的问题是:每个片段的合适大小是多少?

我们将逐一介绍三种方法,并理解为什么现代大语言模型(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 的特殊标记来替换这个词,通常写作

<UNK>

。模型看到

<UNK>

时,完全不知道我们说了什么。

还有一点需要注意。在这种方法中,“learn”、“learning”、“learned”和“learner”都是各自独立的标记。模型不知道它们是相关的,必须从零开始学习每一个。

这种方法的问题在于庞大的词表和未知词问题。接下来我们看看下一种方法如何解决这个问题。

方法三:子词级分词

现在,我们走中间路线。

这个想法很简单。常见词保持为一个标记。生僻词则被拆分成更小的常见片段。

让我们通过一个例子来看:

  • “the”非常常见,所以保持为一个标记:“the”
  • “learning”很常见,所以保持为一个标记:“learning”
  • “tokenization”很生僻,所以拆分成两个:“token”、“ization”
  • “unbelievable”很生僻,所以拆分成三个:“un”、“believ”、“able”

在这里,我们可以看到这些片段是有意义的。“token”是一个真实的片段。“ization”也是一个真实的片段。这意味着模型可以学到“ization”通常表示做某事的行为,并将这一知识复用到“organization”、“civilization”等许多词上。因此,模型学得更快。

优点:已知片段的列表是可控的,通常在 5 万到 20 万之间。不存在未知词问题,因为任何生僻词都可以不断拆分,直到得到我们认识的片段,最坏情况下是单个字符。而且,相关的词共享片段,所以模型不必从零开始学习每一个词。

缺点:有时一个词会在我们看来不自然的地方被拆开。

这就是几乎所有现代 LLM 所使用的方法。

为了帮助你更好地理解,我把三种方法的差异整理成表格。

Character-level Word-level Subword-level
已知列表的大小非常小非常大中等
每个句子的标记数非常多少少
未知词问题从不经常从不
每个标记的含义非常少完整良好
用于现代 LLM否否是

计划进军 AI 领域?

无论你今天在哪个技术领域工作,只要付出努力,你都可以转型到以下这些角色:

  • AI 工程师
  • 前沿部署工程师
  • AI Agent 工程师
  • Harness 工程师
  • 语音 AI 工程师
  • AI 架构师
  • LLM 工程师
  • LLM 推理工程师
  • 生成式 AI 工程师
  • MLOps 工程师
  • 机器学习工程师
  • 深度学习工程师

Outcome School 的 AI 与机器学习课程带你到达那里。

现在,问题是:分词器如何决定哪些片段是常见的、哪些是罕见的?这份列表是谁制定的?

于是,字节对编码就来救场了。

Byte Pair Encoding(BPE)是如何工作的?

字节对编码,即 BPE,是一种构建词元列表的算法:它从单个字符开始,反复合并出现频率最高的相邻片段对。

简单来说,BPE 会查看大量文本,找出最常相邻出现的两个片段,把它们合并成一个新片段,然后不断重复这一过程。

学习它的最好方法是通过一个例子。

假设我们的训练文本只有三个词,出现次数如下:

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

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

h u g      (10 times)
p u g      (5 times)
h u g s    (5 times)

我们的初始词元列表:

h

,

u

,

g

,

p

,

s

。共 5 个词元。

第 1 步: 统计每一对。一对指的是一个词内相邻的两个片段。

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

频率最高的一对是 "u" + "g",共 20 次。于是,我们把它们合并成一个新词元

ug

.

我们的词元列表:

h

,

u

,

g

,

p

,

s

,

ug

。共 6 个词元。

我们的词现在如下所示:

h ug       (10 times)
p ug       (5 times)
h ug s     (5 times)

第 2 步: 再次统计各对。

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

频率最高的一对是 "h" + "ug",共 15 次。于是,我们把它们合并成一个新词元

hug

.

我们的词元列表:

h

,

u

,

g

,

p

,

s

,

ug

,

hug

。共 7 个词元。

我们的词现在如下所示:

hug        (10 times)
p ug       (5 times)
hug s      (5 times)

第 3 步: 再次统计各对。

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

两者相等。在这种情况下,分词器会按照固定规则选择其一。假设我们把 "p" + "ug" 合并为

pug

.

我们的词元列表:

h

,

u

,

g

,

p

,

s

,

ug

,

hug

,

pug

。共 8 个词元。

我们不断重复这一过程,直到列表达到我们想要的大小。在真实的分词器中,这个大小是预先确定的,例如 100,000 个词元。

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

Rule 1: u + g  -> ug
Rule 2: h + ug -> hug
Rule 3: p + ug -> pug

现在,当一段新文本进来时,分词器不再进行任何统计。它只是把文本拆分成字符,然后按相同的顺序应用这些规则。

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

  • 开始:p, u, g, s
  • 应用规则 1:p, ug, s
  • 应用规则 2:没有可合并的,因为 "h" + "ug" 不在这里
  • 应用规则 3:pug, s
  • 最终词元:pug, s

而且,“pugs”这个词从未出现在我们的训练文本中。尽管如此,我们还是得到了有意义的词元。

注意: 在许多现代分词器中,比如 GPT 模型所使用的分词器,起始列表并不是字母,而是全部 256 种可能的字节值。每种语言的每个字符、每个表情符号和每个符号都以字节形式存储。因此,分词器永远不会遇到未知的内容。这种变体被称为字节级 BPE。

还有一点需要注意。分词器是在 LLM 训练之前单独训练的。一旦词元列表准备就绪,它就会被冻结。然后 LLM 使用这个冻结的列表进行训练。LLM 从不改变分词器。

给你的快速提示

无论你在哪个技术领域工作,都要熟悉以下这些主题:

  • LLM
  • RAG
  • MCP
  • Agent
  • 微调
  • 量化

我们把它们全部整合在一个视频中:

AI 工程详解:LLM、RAG、MCP、Agent、微调和量化

不必停止阅读——先收藏起来,等有空的时候再看。未来的你会感谢现在的你。

现在,让我们回到主题。

词表与 Token ID

词表是分词器所知道的全部词元的完整列表。

词表中的每个词元都有一个唯一的编号。这个编号被称为词元 ID。

可以把它想象成一本字典,其中每个条目都有一个序列号。分词器查找该片段,找到序列号,然后把编号交给模型。

对于我们的小例子,词表如下所示:

词元词元 ID
h0
u1
g2
p3
s4
ug5
hug6
pug7

因此,文本 "pugs" 变成了词元

pug

,

s

,然后是词元 ID

7

,

4

.

这是完整的流程:

Text   -> Tokens        -> Token IDs
"pugs" -> ["pug", "s"]  -> [7, 4]

这里,

[7, 4]

就是输入到 LLM 中的内容。

那么,为什么词表大小很重要?当 LLM 生成回复时,每一步都会从这个词表中挑选一个词元。因此,一个包含 100,000 个词元的词表意味着模型在每一步都要从 100,000 个选项中进行选择。更大的词表意味着每个句子所需的词元更少,但模型更重。更小的词表意味着模型更轻,但序列更长。真实的模型会根据其使用场景选择一个介于两者之间的大小。

注意: 在模型内部,每个词元 ID 都用于查找一个长长的数字列表,称为嵌入。真正的理解从那里开始,这是另一篇博客的主题。目前,我们只需要知道分词是它之前的一个步骤。

如果你想深入了解分词、LLM 基础知识和 LLM 内部机制,并从零开始构建一个大语言模型(LLM),请查看 Outcome School 的 AI 与机器学习课程。

解码:从 Token 回到文本

现在,让我们看看文本是如何从模型中输出的。

LLM 并不直接产生文本。它产生的是词元 ID,一次一个。

假设模型产生了

6

,

4

。分词器反向查找词表:

  • 6 是 hug
  • 4 是 s

然后,它将它们拼接起来:"hugs"。

这个反向步骤称为解码。它与我们之前所做的完全相反。

Token IDs -> Tokens        -> Text
[6, 4]    -> ["hug", "s"]  -> "hugs"

这就是为什么,当我们观看 LLM 在屏幕上输入答案时,有时会先看到半个单词出现,然后才是其余部分。这就是 token 边界。模型产生了一个 token,分词器将其解码,然后展示给我们。

保持更新: 订阅我们的新闻通讯,将我们最新的 AI 和机器学习博客直接发送到您的收件箱。

用代码实战演示分词

让我们看看整个过程的实际运作。我们将使用

tiktoken

,这是 OpenAI 的库,包含 OpenAI 模型所使用的分词器。

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 代表哪段文本。

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。它们不是单词,而是信号。

几个重要的特殊 token:

  • End of text: tells the model that the text has ended. Usually written as
    <|endoftext|>
    or
    <eos>
    . When the model produces this token, it stops generating.
  • Beginning of text: marks where the text starts. Usually written as
    <bos>
    .
  • 填充: 一种填充 token,用于在模型一起处理多个输入时使它们的长度相同。
  • 聊天标记: 标记用户消息开始和结束以及助手回复开始的 token。聊天模型就是这样知道谁说了什么的。

在这里,我们可以注意到,特殊 token 绝不是通过拆分正常文本产生的。它是由分词器或模型周围的系统有意添加的。

分词在实践中如何影响 LLM

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

数字母: 假设我们问,字母 "r" 在 "strawberry" 中出现了多少次?分词器会把这个词拆成类似 "str"、"aw"、"berry" 的片段。模型看到的是三个 Token ID,它从未见过单个字母。所以,它只能凭记忆去猜这些片段里有多少个 "r"。这就是为什么大语言模型有时会答错这类问题。

数字运算: 数字 "12345" 会被拆成 "123" 和 "45" 这样的片段。模型看到的不是五个数字,而是两个 Token。对这样的片段做加法,比逐位运算要难得多。

成本与限制: 每个大语言模型都有一次能读取的 Token 数量上限,这被称为上下文窗口。而且,大语言模型的定价也是按 Token 计费的。所以,Token 的数量既决定了能装下什么,也决定了成本是多少。

其他语言: 大多数分词器是在以英语为主的文本上训练的。所以,英语单词会得到紧凑的 Token。印地语、泰米尔语或日语的句子会被拆成多得多的小片段。同样的句子在这些语言中可能需要多几倍的 Token。这意味着成本更高,上下文窗口也更快被占满。

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

它在哪些情况下表现良好,在哪些情况下会失败

让我总结一下子词分词在哪些方面表现良好:

  • 它能处理任何输入,不会出现未知词问题。
  • 对于常见文本,它能保持序列较短。
  • 它在相关单词之间共享有意义的片段,因此模型学习得更快。
  • 它用一套词表就能同时处理文本、代码和多种语言。

而它在以下方面会失败:

  • 需要看到单个字母的任务,比如数字母或反转单词。
  • 对长数字做数学运算,因为数字会以不自然的方式被分组。
  • 在训练文本中出现较少的语言,因为它们会消耗更多 Token,这意味着更高的成本和更少的上下文窗口空间。
  • 任何在奇怪位置被拆开的单词都可能让模型困惑,因为这些片段并不承载我们预期的含义。

现在,我们应该已经理解了大语言模型中的 Tokenization 是什么、BPE 如何构建词表、文本如何变成 Token ID 再变回来,以及它在哪里表现良好、在哪里会失败。

常见问题

所有大语言模型都使用相同的分词器吗?

不是。每个大语言模型都有自己的分词器。使用不同分词器的不同模型,对同一个句子会给出不同的 Token ID。所以,我们得到的具体数字始终取决于使用的是哪个分词器。

大语言模型会在训练过程中更改它的分词器吗?

不会。分词器是在大语言模型训练之前单独训练的。一旦 Token 列表准备好,它就被冻结,然后大语言模型使用这个冻结的列表进行训练。大语言模型从不更改分词器。

100 个 Token 相当于多少个单词?

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

为什么大语言模型难以数出一个单词中的字母数量?

因为模型从未见过单个字母。像 "strawberry" 这样的单词会被拆分成 "str"、"aw"、"berry" 等片段,模型只能看到它们的 Token ID。因此,它只能凭记忆去猜测某个字母在这些片段中出现了多少次。

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

大多数分词器是在以英语为主的文本上训练的,所以英语单词会得到紧凑的 token。而印地语、泰米尔语或日语的句子会被拆分成更多的小片段。这意味着成本更高,也会更快填满上下文窗口。

今天就到这里。

谢谢

Amit Shekhar Outcome School 创始人

你可以通过以下方式与我联系:

关注 Outcome School: