Token 是大型语言模型 (Large Language Models, LLMs) 处理文本的基本单位。它不是传统意义上的“词”,而是模型将人类可读的文字序列(如句子、段落)切分、编码并最终用于学习和生成文本的离散符号表示。理解 Token 的概念对于深入了解 LLMs 的工作原理、能力边界以及成本核算至关重要。

核心思想:LLMs 不直接处理原始文本,而是将其分解为一系列经过特殊编码的 Token。这些 Token 构成了模型输入和输出的最小单元,并直接影响模型的性能、效率和成本。


一、什么是 Token?

在自然语言处理 (NLP) 领域,尤其是在 LLMs 中,Token 是指模型进行训练和推理时所使用的文本片段。它可能是:

  • 一个完整的词 (Word):例如 “cat”, “run”。
  • 一个词的一部分 (Subword):例如 “un”, “believe”, “able” 组合成 “unbelievable”。
  • 一个标点符号 (Punctuation):例如 “.”, “,”, “!”。
  • 一个特殊符号或控制字符 (Special Token):例如 [CLS], [SEP], <bos>, <eos>,用于标记文本的开始、结束或不同部分。

为什么是 Token 而不是简单的“词”?

  1. 处理未知词 (Out-of-Vocabulary, OOV) 问题:如果只用词作为基本单位,模型会遇到训练集中未出现过的新词。子词分词可以将新词分解成已知的子词单元,从而有效处理 OOV 问题。
  2. 控制词汇表大小 (Vocabulary Size):词汇表过大不仅占用大量内存,还会增加模型训练的复杂性。子词分词能够用相对较小的词汇表覆盖绝大多数词语,例如,通过组合有限的子词来表示无限的词汇。
  3. 保留形态学信息 (Morphological Information):通过分解成子词,模型能够更好地理解词语的构成和词根、前缀、后缀等带来的语义变化。例如,“running”分解成“run”和“ing”,模型就能学到“run”的含义以及“ing”表示进行时的语法功能。
  4. 跨语言一致性 (Cross-lingual Consistency):对于一些没有明确词语边界的语言(如汉语、日语),子词分词提供了更统一和有效的分词策略。

二、Tokenization (分词) 过程

Tokenization 是将原始文本分解成 Token 序列的过程。这个过程是 LLMs 工作流的第一步,也是至关重要的一步。

2.1 常见 Tokenization 算法

目前,主流的 LLMs 大多采用基于子词 (Subword) 的分词算法,其中最常见的有:

  1. Byte Pair Encoding (BPE)
    • 原理:BPE 算法通过迭代地合并文本中最常出现的字节对(或字符对)来构建词汇表。它从字符级别开始,逐渐合并成子词,直到达到预设的词汇表大小或合并次数。
    • 特点:倾向于保留常见的完整词,同时将不常见的词分解为有意义的子词。
  2. WordPiece
    • 原理:由 Google 开发,是 BERT 等模型使用的主要分词器。与 BPE 类似,但它不是合并出现频率最高的对,而是合并使语言模型概率增幅最大的对。
    • 特点:更侧重于优化语言模型的性能,生成更“有用”的子词。
  3. SentencePiece
    • 原理:一种语言无关的子词分词器,可以将所有文本视为字节序列。它支持 BPE 和 WordPiece 算法,并且能够处理预分词空白符(pre-tokenization whitespace),这意味着它不会预先将文本按空格切分。
    • 特点:特别适用于处理多种语言,包括那些没有明确词语边界的语言。

2.2 Tokenization 流程示意

2.3 特殊 Token

为了帮助模型理解文本结构和执行特定任务,LLMs 会使用一些特殊的 Token:

  • <bos> (Beginning Of Sentence/Stream):标记输入序列的开始。
  • <eos> (End Of Sentence/Stream):标记输入序列的结束。
  • <pad> (Padding):用于将不同长度的序列填充到相同的长度,以进行批量处理。
  • <unk> (Unknown):表示词汇表中不存在的 Token。
  • [CLS] (Classification):在 BERT 等模型中,作为输入序列的第一个 Token,其对应的输出通常用于分类任务。
  • [SEP] (Separator):在 BERT 等模型中,用于分隔不同的句子或文本段。

三、Token ID 与 Token Embeddings

经过 Tokenization 后,每个 Token 会被进一步转化为模型可以处理的数值形式:

3.1 Token ID (整数编码)

分词器会维护一个词汇表 (Vocabulary),其中包含了所有已知的 Token,并为每个 Token 分配一个唯一的整数 ID。
例如:

  • “hello” -> 100
  • “world” -> 200
  • “!” -> 10

3.2 Token Embeddings (向量表示)

Token ID 本身不包含语义信息。为了让模型理解 Token 的含义,每个 Token ID 会被映射到一个高维浮点数向量,这就是词嵌入 (Word Embedding)Token Embedding

  • 高维空间:这些向量通常是几百到几千维的(例如,GPT-3 的 Token Embedding 维度为 12288)。
  • 语义信息:语义相似的 Token 在这个高维向量空间中距离较近,而语义不相关的 Token 则相距较远。
  • 上下文感知:在现代 LLMs 中,Token 的最终嵌入向量会结合其上下文信息,使得同一个 Token 在不同语境下具有不同的向量表示。

数学表示
如果词汇表大小为 $V$ 且嵌入维度为 $d$,则存在一个嵌入矩阵 $E \in \mathbb{R}^{V \times d}$。对于一个 Token ID $i$,其嵌入向量为 $E_i \in \mathbb{R}^d$。

3.3 位置编码 (Positional Encoding)

Transformer 架构的注意力机制本身不包含序列的顺序信息。为了让模型理解 Token 的位置关系顺序信息,会在 Token Embedding 中加入位置编码 (Positional Encoding)

  • 原理:位置编码通常是一系列固定计算的(如正弦/余弦函数)或可学习的向量,它们被加到 Token Embedding 上。
  • 作用:让模型能够区分相同 Token 在不同位置时的含义,例如“我爱你”和“你爱我”虽然词相同但顺序不同,通过位置编码模型能理解其差异。

四、Token 在 LLMs 中的重要意义

Token 是连接原始文本与 LLM 内部计算的桥梁,其重要性体现在多个方面:

4.1 输入和输出的单位

  • 输入:LLMs 接受 Token 序列作为输入。
  • 输出:LLMs 逐个生成 Token 来构建响应。

4.2 上下文窗口 (Context Window)

  • 定义:LLMs 的“记忆”能力被限制在一个上下文窗口内,即模型在一次推理中能够同时处理的 Token 数量上限。
  • 影响:上下文窗口的大小直接决定了模型能够理解和生成的文本长度。如果输入的 Token 数量超过了上下文窗口,文本就会被截断,导致信息丢失。

4.3 成本核算 (Pricing)

  • 绝大多数 LLM API(如 OpenAI 的 GPT 系列)的收费是基于Token 数量。无论是输入还是输出,每个 Token 都会产生费用。这意味着更长的提示 (prompt) 和更长的响应会导致更高的成本。

4.4 模型性能与效率

  • Tokenization 策略:不同的分词策略会影响模型的最终性能。一个好的分词器可以生成更语义化、更紧凑的 Token 序列,从而提高模型的效率和理解能力。
  • 计算资源:处理的 Token 数量越多,所需的计算资源(内存、计算时间)就越大。

五、Go 语言中简化 Tokenization 的概念性示例

一个真实的 LLM 分词器(如基于 BPE 或 WordPiece)的实现非常复杂,涉及大规模语料库的统计分析和词汇表构建。然而,我们可以通过一个简化的 Go 语言示例来理解 Tokenization 的概念,特别是如何将文本分解成子词单元。

下面的示例模拟了一个极简的分词器,它尝试将某些常见的词根和后缀识别为独立的 Token。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package main

import (
"fmt"
"regexp"
"strings"
)

// SimpleConceptualTokenizer 模拟一个简化的子词分词器
// 目标:将输入文本分解成一个Token序列
// 注意:这只是一个概念性示例,与真实LLM分词器(如BPE)的复杂性不可比
func SimpleConceptualTokenizer(text string) []string {
// 1. 预处理:标准化文本,例如转换为小写,处理常见标点符号
text = strings.ToLower(text)
text = strings.ReplaceAll(text, ".", " . ") // 将句号作为独立Token
text = strings.ReplaceAll(text, ",", " , ") // 将逗号作为独立Token
text = strings.ReplaceAll(text, "?", " ? ") // 将问号作为独立Token
text = strings.ReplaceAll(text, "!", " ! ") // 将感叹号作为独立Token

// 使用正则表达式将文本按空格和特殊符号分割,并过滤空字符串
re := regexp.MustCompile(`\s+`)
words := re.Split(text, -1)
var initialTokens []string
for _, w := range words {
if w != "" {
initialTokens = append(initialTokens, w)
}
}

// 2. 模拟子词分解(非常简化和硬编码的规则)
var finalTokens []string
subwordRules := map[string]string{
"un": "un-", // 前缀
"re": "re-", // 前缀
"ing": "-ing", // 后缀
"ed": "-ed", // 后缀
"ly": "-ly", // 后缀
"able": "-able", // 后缀
}

for _, token := range initialTokens {
// 尝试匹配后缀
foundSuffix := false
for suffix, subword := range subwordRules {
if strings.HasSuffix(token, suffix) && strings.HasPrefix(subword, "-") {
// 确保不是完整的词就是后缀本身
base := strings.TrimSuffix(token, suffix)
if base != "" { // 避免 "ing" 变成 "", "ing"
finalTokens = append(finalTokens, base)
}
finalTokens = append(finalTokens, suffix)
foundSuffix = true
break
}
}
if foundSuffix {
continue // 已处理,跳过当前token的后续处理
}

// 尝试匹配前缀
foundPrefix := false
for prefix, subword := range subwordRules {
if strings.HasPrefix(token, prefix) && strings.HasSuffix(subword, "-") {
base := strings.TrimPrefix(token, prefix)
if base != "" {
finalTokens = append(finalTokens, prefix)
}
finalTokens = append(finalTokens, base)
foundPrefix = true
break
}
}
if foundPrefix {
continue
}

// 如果没有匹配到子词规则,则保持原样
finalTokens = append(finalTokens, token)
}

// 3. 示例添加特殊Token(实际中由模型决定)
// 例如,我们可以在开头和结尾添加 <bos> 和 <eos>
// return append([]string{"<bos>"}, append(finalTokens, "<eos>")...) // 简化为不添加,保持输出纯净
return finalTokens
}

func main() {
sentence1 := "The unbelievable cat is running quickly."
tokens1 := SimpleConceptualTokenizer(sentence1)
fmt.Printf("Original: \"%s\"\nTokens: %v\n\n", sentence1, tokens1)
// 预期输出接近:[the un believe able cat is run ing quick ly .]

sentence2 := "I replied immediately."
tokens2 := SimpleConceptualTokenizer(sentence2)
fmt.Printf("Original: \"%s\"\nTokens: %v\n\n", sentence2, tokens2)
// 预期输出接近:[i re pli ed immediate ly .]

sentence3 := "He played a new game."
tokens3 := SimpleConceptualTokenizer(sentence3)
fmt.Printf("Original: \"%s\"\nTokens: %v\n\n", sentence3, tokens3)
// 预期输出接近:[he play ed a new game .]
}

解释:上述 Go 代码展示了一个高度简化的概念性分词器。它首先对文本进行基本清洗和分割,然后通过硬编码的规则尝试将词语分解成前缀、词根和后缀。真实的 LLM 分词器会通过统计学习和迭代合并(如 BPE)来动态构建子词词汇表,远比这个示例复杂和智能。这个示例的目的在于说明 Token 不仅仅是词,它们可以是更小的、有语义意义的单元。

六、挑战与考量

  1. 分词策略的影响:不同的分词器和词汇表可能导致相同的文本被分解成不同的 Token 序列,这会影响模型的学习效率和泛化能力。
  2. 多语言处理:对于像中文、日文这样没有明显词语边界的语言,Tokenization 更加复杂,通常需要依赖专门的分词器。
  3. 计算效率:Tokenization 过程必须高效,尤其是在处理大规模文本和实时应用时。
  4. Token 的粒度:Token 过大可能导致 OOV 问题和词汇表膨胀;Token 过小(如字符级)会丢失语义信息,且序列长度过长增加计算负担。子词分词是这两者之间的一种平衡。
  5. 隐私和偏见:Tokenization 过程中,如果处理不当,可能会在 Token 层面引入偏见或泄露敏感信息。

七、总结

Token 是 LLMs 与人类语言交互的基石。它们作为模型输入和输出的基本单位,不仅承载了文本的语义信息,也直接决定了模型的上下文理解能力、计算效率以及运行成本。通过精巧的分词算法(如 BPE、WordPiece),LLMs 能够以一种高效且灵活的方式处理复杂多变的自然语言,从而展现出惊人的生成和理解能力。深入理解 Token 的概念,是掌握大型语言模型核心工作原理的关键一步。