当你打开ChatGPT输入"你好"两个字,模型看到的是什么?
它看到的不是汉字,不是笔画,甚至不是图像像素。它看到的是两个整数:[22024, 22845]。
这两个数字就是Token ID——大模型理解人类语言的第一把钥匙。每一个词、每一个字、甚至每一个标点符号,在进入模型之前,都会被转换成一个或多个整数。这些整数是模型世界的"通用货币",是连接人类符号系统与神经网络向量空间的桥梁。
但Token ID远不止是一个简单的编号。它的分配方式、数值范围、与Embedding的关系,以及不同语言的Token效率差异,都深刻影响着模型的性能、成本,甚至是推理能力。
从文字到数字:Token ID的诞生
词表:Token ID的家
想象你有一本字典,这本字典有50000页,每一页对应一个词或词片段。第一页是"the"(ID: 0),第42页是"hello"(ID: 41),第49999页是某个罕见的化学术语(ID: 49999)。
这就是词表(Vocabulary)——所有可能的Token及其对应ID的集合。不同的模型有不同的词表:
| 模型 | 词表大小 | 说明 |
|---|---|---|
| GPT-2 | 50,257 | 经典BPE词表 |
| GPT-4 | 100,256 | 扩展的多语言支持 |
| GPT-4o | 199,997 | 更高效的多语言编码 |
| LLaMA 2 | 32,000 | 相对紧凑的词表 |
| LLaMA 3 | 128,000 | 大幅扩展的词表 |
词表大小是一个重要的超参数。太小,常见词会被切分成碎片,增加序列长度;太大,Embedding矩阵占用过多参数,稀有词得不到充分训练。
BPE:构建词表的核心算法
现代大模型几乎都使用**Byte Pair Encoding(BPE)**算法来构建词表。这个算法最初是1994年Philip Gage提出的数据压缩方法,2016年被Sennrich等人引入神经机器翻译领域。
BPE的核心思想非常朴素:把最常见的字符对合并成新token。
graph TD
A["初始文本: the cat in the hat"] --> B["迭代1: 合并 'th' → ID 256"]
B --> C["文本: [256]e cat in [256]e hat"]
C --> D["迭代2: 合并 '[256]e' → ID 257"]
D --> E["文本: [257] cat in [257] hat"]
E --> F["迭代3: 合并 '[257] ' → ID 258"]
F --> G["文本: [258]cat in [258]hat"]
G --> H["... 继续迭代直到达到目标词表大小"]
这个迭代过程会持续进行,直到词表达到预设大小。最终,我们得到一个词汇表,其中:
- ID 0-255:单字节token(覆盖所有ASCII字符)
- ID 256+:合并产生的多字节token
Token ID的分配规律
通过BPE算法构建的词表,Token ID的分配遵循一定规律:
graph LR
subgraph "词表结构"
A["ID 0-255<br/>单字节Token<br/>ASCII基础层"] --> B["ID 256-5000<br/>常用字符组合<br/>th, er, ing..."]
B --> C["ID 5000-30000<br/>常用完整词<br/>the, is, hello..."]
C --> D["ID 30000+<br/>专业术语<br/>罕见组合"]
end
这种分层结构保证了:
-
基础层(0-255):单字节token,覆盖所有可能的字节值。这保证了任何文本都能被tokenize——即使出现未知字符,也可以逐字节处理。
-
常用字符组合(256-几千):高频字符组合,如"th"、“er”、“ing"等。这些是跨语言通用的底层构建块。
-
常用词(几千-几万):高频完整词,如"the”、“is”、“hello”。英文中这些词往往能直接映射到单个Token ID。
-
专业术语和罕见组合(几万-词表上限):特定领域的术语、罕见词组合。
特殊Token:词表中的"公务员"
在任何词表中,前几个Token ID通常被保留给特殊用途:
graph TD
subgraph "特殊Token ID"
A["ID 0: PAD<br/>填充,对齐序列长度"]
B["ID 1: UNK<br/>未知词,词表外词汇"]
C["ID 2: BOS<br/>序列开始标记"]
D["ID 3: EOS<br/>序列结束标记"]
end
这些特殊token承担着关键功能:
-
PAD(Padding):当批处理中不同样本长度不一致时,用PAD填充短序列,使它们长度相同。注意力机制会忽略这些PAD token。
-
UNK(Unknown):当词表中不存在某个词时(虽然在现代subword tokenizer中这种情况很少),用UNK代替。
-
BOS(Beginning of Sequence):标记序列的开始。在生成任务中,模型从这个token开始生成。
-
EOS(End of Sequence):标记序列的结束。模型生成这个token时停止。
Token ID到Embedding:查表的数学本质
从整数到向量
Token ID只是一个整数,神经网络无法直接处理整数。它需要的是连续的向量。这个转换过程通过Embedding层完成。
Embedding层本质上是一个查找表(Lookup Table)。假设词表大小为$V$,Embedding维度为$d$,Embedding层存储一个矩阵$E \in \mathbb{R}^{V \times d}$。
当输入Token ID为$i$时,Embedding层直接返回矩阵的第$i$行:
$$\text{embedding}_i = E[i, :]$$graph LR
subgraph "Embedding查表过程"
A["Token ID: 15496"] --> B["Embedding矩阵<br/>50000 × 768"]
B --> C["向量: [768维]"]
end
subgraph "矩阵结构"
D["Row 0: token 'the'"]
E["Row 15496: token 'Hello'"]
F["Row 49999: token '...'"]
end
为什么不直接用One-Hot编码?
一个自然的疑问是:为什么不把Token ID转换成one-hot向量,然后用一个线性层处理?
答案揭晓了一个有趣的数学等价性:Embedding查表和one-hot编码后乘以权重矩阵,在数学上是完全等价的。
设Token ID为$i$,其one-hot表示为$e_i \in \mathbb{R}^V$(只有第$i$个位置为1,其余为0)。那么:
$$e_i^\top E = E[i, :]$$graph LR
subgraph "数学等价性"
A["Token ID i"] --> B["One-hot向量<br/>[0,0,...,1,...,0]"]
B --> C["矩阵乘法<br/>e_i^T × E"]
C --> D["结果向量<br/>E的第i行"]
E["Token ID i"] --> F["直接查表<br/>Embedding[i, :]"]
F --> D
end
既然数学等价,为什么还要用Embedding层?
效率。对于词表大小50000、Embedding维度768的模型:
- One-hot向量大小:50000维
- 每次前向传播需要:50000 × 768 = 38,400,000次乘法(绝大多数是乘以0)
而Embedding查表只需要:
- 一次内存读取,获取第$i$行的768个数值
Embedding维度的选择
Embedding维度$d$是一个重要的模型超参数。常见的维度有:
| 模型规模 | Embedding维度 | 参数量(仅Embedding) |
|---|---|---|
| 小型 | 512 | 25M (50K vocab) |
| 中型 | 768 | 38M (50K vocab) |
| 大型 | 1024 | 51M (50K vocab) |
| 超大型 | 4096 | 205M (50K vocab) |
这些数字看起来很整齐(512、768、1024),背后有硬件效率考量:
-
GPU内存对齐:GPU的内存访问以32或64字节为单位。维度是64的倍数时,内存访问效率最高。
-
注意力头的整除关系:Transformer使用多头注意力,Embedding维度必须能被注意力头数整除。BERT有12个头,768 ÷ 12 = 64,每个头的维度是64。
-
经验法则:维度太小,表达能力不足;维度太大,过拟合风险增加。经验上,Embedding维度约为模型隐藏层维度的1/4到1/2。
Token ID在模型中的生命周期
输入阶段:文本到数字
当用户输入"Hello, world!“时,分词器开始工作:
graph LR
A["Hello, world!"] --> B["分词器"]
B --> C["['Hello', ',', ' world', '!']"]
C --> D["[15496, 11, 995, 0]"]
D --> E["Embedding层"]
E --> F["[batch, 4, 768]"]
分词器首先进行预_tokenize处理(如空格替换为特殊字符),然后应用BPE合并规则,最终输出Token ID序列。
处理阶段:数字到向量到…更多向量
Token ID序列进入Embedding层后,变成向量序列。这些向量随后经过数十层Transformer层的处理:
graph TD
A["Token IDs: [15496, 11, 995, 0]"] --> B["Embedding Lookup"]
B --> C["Token Embeddings [4, 768]"]
D["Position IDs: [0, 1, 2, 3]"] --> E["Position Embeddings [4, 768]"]
C --> F["Add"]
E --> F
F --> G["Transformer Layer 1"]
G --> H["Transformer Layer 2"]
H --> I["..."]
I --> J["Transformer Layer N"]
J --> K["Final Hidden States [4, 768]"]
每一层Transformer都会对向量进行变换,但Token ID本身已经完成了它的使命——它只是进入模型世界的"入场券”。
输出阶段:向量回到数字
模型最终输出的是隐藏状态向量,需要转换回Token ID概率分布。这个过程通过**语言模型头(LM Head)**完成:
$$\text{logits} = h \cdot W_{\text{out}}^T$$其中$h \in \mathbb{R}^d$是最终的隐藏状态,$W_{\text{out}} \in \mathbb{R}^{V \times d}$是输出投影矩阵。
graph TD
A["最终隐藏状态 h<br/>[batch, seq, 768]"] --> B["LM Head"]
C["输出投影矩阵 W_out<br/>[50000, 768]"] --> B
B --> D["Logits [batch, seq, 50000]"]
D --> E["Softmax"]
E --> F["概率分布<br/>每个Token ID的概率"]
F --> G["采样策略<br/>Greedy/Top-k/Top-p"]
G --> H["预测Token ID"]
权重共享:一个矩阵,两种用途
细心的读者可能注意到:输入Embedding矩阵$E$和输出投影矩阵$W_{\text{out}}$都是$V \times d$的矩阵。它们可以共享吗?
答案是:可以,而且效果通常更好。
这种技术称为权重共享(Weight Tying),让$W_{\text{out}} = E$。
graph TD
subgraph "权重共享架构"
A["输入Token ID"] --> B["共享Embedding矩阵<br/>E ∈ R^(V×d)"]
B --> C["Token Embeddings"]
C --> D["Transformer层"]
D --> E["隐藏状态 h"]
E --> F["h × E^T"]
F --> G["Logits"]
B -.->|"共享"| F
end
好处有:
-
参数减少:节省$V \times d$个参数。对于50000词表、768维Embedding,这是3800万参数。
-
语义一致性:输入"cat"时查询的向量,应该与预测"cat"时需要产生的隐藏状态相似。权重共享强制这种一致性。
-
梯度双倍效应:Embedding矩阵同时接收来自输入和输出的梯度更新,加速学习。
主流模型如GPT-2、BERT、LLaMA都默认使用权重共享。
Token ID的效率困境
英语的"特权"
如果你用中英文分别测试同样的内容,会发现一个令人不安的事实:中文消耗的Token数量远多于英文。
以"人工智能正在改变世界"为例:
- 中文tokenize结果(GPT-4):约12-15个token
- 英文"Artificial intelligence is changing the world":约6-8个token
这不是中文的错,而是词表构建过程的必然结果。BPE算法基于训练语料的字符对频率进行合并。大多数主流模型的训练语料以英文为主,词表自然会优化英文的token效率。
多语言的Token成本差异
研究者量化了这种差异。一篇分析指出:
| 语言 | 相对于英文的Token倍率 |
|---|---|
| 英文 | 1.0x (基准) |
| 中文 | 1.5-2.0x |
| 日文 | 2.0-2.5x |
| 阿拉伯语 | 1.3-1.8x |
| 西班牙语 | 1.0-1.2x |
graph LR
subgraph "同样内容的Token成本对比"
A["英文<br/>100 tokens"] --> B["基准成本"]
C["中文<br/>150-200 tokens"] --> D["1.5-2x 成本"]
E["日文<br/>200-250 tokens"] --> F["2-2.5x 成本"]
end
这意味着同样的内容,中文用户可能需要支付1.5-2倍的API调用费用。
更令人担忧的是,有研究发现一个国家的人类发展指数与其语言的Token效率呈负相关——越不发达的地区,其语言在LLM中的Token成本越高。这不仅是技术问题,更是数字公平问题。
Token效率与模型性能的矛盾
解决多语言Token效率问题的一个直观方法是增大词表。更大的词表可以容纳更多语言的专用token,提高压缩效率。
但这带来另一个问题:Embedding矩阵随词表线性增长。
以LLaMA 2和LLaMA 3为例:
- LLaMA 2:32K词表,7B参数模型中Embedding约占7%参数
- LLaMA 3:128K词表,同样的7B模型中Embedding约占25%参数
Embedding参数太多会导致:
- 稀有token训练不足,Embedding质量差
- 模型容量被Embedding"吃掉",Transformer层参数受限
- 内存占用增加
最优词表大小的数学推导
2024年的一篇论文提出了计算最优词表大小的公式。研究发现:
$$V^* \propto N^{0.5} \cdot D^{0.25}$$其中$V^*$是最优词表大小,$N$是模型参数量,$D$是训练数据量。
这个公式揭示了一个反直觉的结论:大模型应该使用更大的词表。
论文发现,LLaMA 2-70B的32K词表实际上偏小。根据公式,最优词表应该在200K以上。将词表从32K增加到43K,ARC-Challenge基准测试的准确率从29.1提升到32.0,而总计算量不变。
Token ID如何影响模型能力
算术能力的"Token诅咒"
大模型在算术任务上的表现往往令人困惑:能通过律师资格考试,却算不对两位数加法。
根本原因之一是Token ID对数字的非一致编码。
graph TD
subgraph "数字tokenize的不一致性"
A["100"] --> B["单个Token<br/>[ID: 100]"]
C["137"] --> D["两个Token<br/>[13, 7]"]
E["56291"] --> F["多个Token<br/>[56, 29, 1]"]
end
这种不一致性破坏了数字的位值结构。模型看到token [100]时,无法知道它代表"1×100",因为token本身只是一个任意的ID编号。
“strawberry"问题:字母计数的困境
类似的问题出现在字母计数任务上。问GPT-4"strawberry这个单词有几个字母’r’",它可能会回答错误。
原因同样是tokenize:
- “strawberry” 被tokenize为
['straw', 'berry']或['str', 'aw', 'berry'] - 模型看不到独立的字母’r’,它只看到两个整体的token
模型需要在"脑海中"重建字母序列才能计数,这个过程容易出错。
解决方案与权衡
针对算术和字符级任务的问题,研究者提出了几种方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 字符级tokenize | 完美保留字符信息 | 序列长度爆炸 |
| 数字专用tokenize | 保留位值结构 | 需要特殊处理 |
| 思维链提示 | 利用生成能力 | 消耗更多token |
| 工具调用 | 计算准确可靠 | 增加系统复杂度 |
这些方案各有权衡,没有银弹。在实际应用中,往往需要根据具体场景选择合适的策略。
Token ID的技术实现细节
PyTorch中的Embedding层
在PyTorch中,Embedding层的实现非常简洁:
import torch.nn as nn
# 词表大小50000,Embedding维度768
embedding = nn.Embedding(num_embeddings=50000, embedding_dim=768)
# 输入:batch_size=2, seq_len=10
input_ids = torch.tensor([[1, 234, 567, ...], [89, 100, 234, ...]])
# 输出:batch_size=2, seq_len=10, embedding_dim=768
embeddings = embedding(input_ids)
内部实现上,nn.Embedding存储一个$50000 \times 768$的float32矩阵。调用时,它执行一个高效的"index select"操作,从矩阵中提取对应行的向量。
梯度反向传播
Embedding层的梯度反向传播有一个有趣的特性:只有被查询过的token会收到梯度更新。
graph TD
subgraph "梯度更新机制"
A["输入Token IDs: [5, 10, 10, 5]"] --> B["前向传播"]
B --> C["损失计算"]
C --> D["反向传播"]
D --> E["Token 5 Embedding<br/>更新2次"]
D --> F["Token 10 Embedding<br/>更新2次"]
D --> G["其他49998个Token<br/>不更新"]
end
这意味着:
- 高频token的Embedding会被频繁更新,质量较高
- 低频token的Embedding更新次数少,可能质量较差
- 这是"稀有词问题"的根源之一
内存布局与效率
Embedding矩阵的内存布局对性能有显著影响。现代GPU的内存访问模式要求:
-
行优先存储:同一行的元素连续存储,利于访问同一token的完整Embedding向量。
-
对齐要求:Embedding维度为64或128的倍数时,内存访问效率最高。
-
混合精度训练:Embedding矩阵可以使用FP16或BF16存储,减少内存占用。但梯度更新通常需要FP32精度。
从Token ID看大模型的设计哲学
Token ID这个看似简单的概念,实际上承载了大模型设计的核心哲学:
离散符号与连续向量的桥接。人类语言是离散的符号系统(字符、单词),神经网络处理的是连续向量。Token ID是离散符号的数字化表示,Embedding是通往连续空间的桥梁。这个设计决定了语言模型的基本工作方式。
压缩与表达能力的权衡。词表大小、Token粒度、Embedding维度,每一个选择都是压缩效率与表达能力的权衡。没有最优解,只有特定场景下的最适解。
多语言公平性的挑战。当前主流模型的词表设计天然偏向英语,导致非英语用户承担更高的成本。这是技术选择的社会后果,值得更多关注。
字符级信息的丢失。Subword tokenize提高了效率,但丢失了字符级信息。这是大模型在算术、拼写等任务上表现不佳的技术根源之一。
理解Token ID,就是理解大模型如何"看"世界的第一步。当你下次输入一个词,不妨想一想:在模型眼中,这个词变成了什么数字?这个数字又如何影响模型的输出?这个简单的转换过程,蕴含着语言与数学、符号与向量之间的深刻联系。
参考文献
-
Sennrich, R., Haddow, B., & Birch, A. (2016). Neural Machine Translation of Rare Words with Subword Units. ACL 2016.
-
Gage, P. (1994). A New Algorithm for Data Compression. The C Users Journal, 12(2), 23-38.
-
Press, O., & Wolf, L. (2017). Using the Output Embedding to Improve Language Models. EACL 2017.
-
Inan, H., Khosravi, K., & Socher, R. (2017). Tying Word Vectors and Word Classifiers: A Loss Framework for Language Modeling. ICLR 2017.
-
Tao, J., et al. (2024). Scaling Laws with Vocabulary: Larger Models Deserve Larger Vocabularies. arXiv:2407.13623.
-
Nogueira, R., et al. (2022). Investigating the Limitations of Transformers with Simple Arithmetic Tasks. arXiv:2202.07385.
-
Raschka, S. (2025). Implementing A Byte Pair Encoding (BPE) Tokenizer From Scratch.
-
Hugging Face. (2024). Tokenizers Documentation.
-
OpenAI. (2023). GPT-4 Technical Report.
-
Radford, A., et al. (2019). Language Models are Unsupervised Multitask Learners. OpenAI Technical Report.