02 · 分词 · 8 min

从文本到标记

文本如何变成数字。BPE、子词,以及为什么大语言模型难以计算字母数量。

为什么需要 Token

语言模型无法直接处理文本。它处理的是数字。每次你与大语言模型对话,第一步都是将你的文本转换成一个整数序列——即 Token ID

词元化(Tokenization)就是实现这种转换的分割过程。

为什么不一个词一个 Token

乍一看,你可能会设想:一个词 = 一个 Token。简单明了。

但这行不通:

  • 语言中有数百万个可能的词(变形、新词、专有名词、错别字……)。每词一 Token 需要一个巨大的词表。
  • 模型对它从未见过的词无能为力
  • 有些语言(中文、日文)词与词之间根本没有空格。

几乎所有现代大语言模型采用的解决方案是:子词(Subword)

子词词元化

使用子词词元器:

  • 高频词成为单个 Token("的"、"是"、"在")
  • 罕见词被拆分成更小的片段("tokenization" → "token" + "ization")
  • 未知字符总是可以分解到单个字母

结果:一个大小合理的词表(通常在 30,000 到 200,000 个 Token 之间),可以表示任意文本

使用最广泛的算法叫做 BPE字节对编码)。它从单个字符开始,在训练语料库中迭代地合并最频繁出现的字符对。

三十秒理解 BPE

设想一个只有三个词的迷你语料库:lowlowerlowest。我们先在字符级别进行词元化:

l o w
l o w e r
l o w e s t

在每次迭代中,我们寻找最频繁出现的相邻 Token。这里,l o 出现了三次——我们将它合并为 lo

lo w
lo w e r
lo w e s t

现在 lo w 是最频繁的对。我们再合并:

low
low e r
low e s t

如此继续,直到达到目标词表大小。频繁出现的片段(low)变成单个 Token。罕见的(est)则保持分解状态。这正是 BPE 所做的——只不过在数十亿词上进行,而不是三个,并且在现代模型中是基于字节而非字符(byte-level BPE),这保证了任何输入都不会"超出词表"。

几个值得了解的近亲算法:WordPiece(BERT)、SentencePiece(T5、Llama)、Unigram LM(mT5)。它们共享同一个思想——子词词表——只是合并启发式不同。

特殊 Token

除了文本子词之外,词元器还保留了一些特殊 Token,它们在自然文本中永远不会出现:

  • <|im_start|><|im_end|>(OpenAI),[INST]…[/INST](Llama),<|user|> / <|assistant|>——用于分隔对话轮次。
  • <|endoftext|>——文档结尾。
  • <|fim_prefix|><|fim_middle|>——用于中间填充(fill-in-the-middle),常用于代码补全。

正是这些标记把*"用户说了 X,助手回复 Y"*这样的对话转换成模型可处理的单一线性 Token 序列。当你向 ChatGPT 发送消息时,这些标记会在词元化之前被自动加上。

亲自试试

右侧显示出子词:每个词元都是一个可复用的片段,不一定是一个完整的单词。常见词只占一个词元,而生僻词会被拆成多块。

有几点值得注意:

  • 短而高频的词很少被拆分。
  • 长词或罕见词往往会被分成多个片段。
  • 词前面的空格Token 的一部分(这就是为什么 · hellohello 是不同的 Token)。
  • 对于中文,词元化的方式与英文有所不同——模型在训练中接触过的语言越多,词元化效率就越高。

实际影响

Token 这件事有很多出乎意料的影响:

  • 大语言模型数字母很差。"'草莓'里有几个'莓'字?"——它们经常答错,因为单词是以几个 Token 而非单独字母的形式进入模型的。
  • API 价格Token 计算,而不是按词数计算。词元化效率较低的语言处理起来会贵一些。
  • 上下文窗口128k tokens200k tokens……)也以 Token 为单位衡量。一部 10 万字的书大约相当于 15 万个 Token

对模型来说,"tokenization"和"token·iza·tion"是同一回事。它只看到这些片段。

接下来

你的文本现在已经变成了一个整数序列,这些整数将进入模型。模型内部的第一步:它们被转换成向量,在一个数百维的空间中,意义成为几何上的位置。

这就是下一章的主题。

更新于

从文本到词元:LLM 分词机制详解 · Step by Token