当你打开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

这种分层结构保证了:

  1. 基础层(0-255):单字节token,覆盖所有可能的字节值。这保证了任何文本都能被tokenize——即使出现未知字符,也可以逐字节处理。

  2. 常用字符组合(256-几千):高频字符组合,如"th"、“er”、“ing"等。这些是跨语言通用的底层构建块。

  3. 常用词(几千-几万):高频完整词,如"the”、“is”、“hello”。英文中这些词往往能直接映射到单个Token ID。

  4. 专业术语和罕见组合(几万-词表上限):特定领域的术语、罕见词组合。

特殊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),背后有硬件效率考量:

  1. GPU内存对齐:GPU的内存访问以32或64字节为单位。维度是64的倍数时,内存访问效率最高。

  2. 注意力头的整除关系:Transformer使用多头注意力,Embedding维度必须能被注意力头数整除。BERT有12个头,768 ÷ 12 = 64,每个头的维度是64。

  3. 经验法则:维度太小,表达能力不足;维度太大,过拟合风险增加。经验上,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

好处有:

  1. 参数减少:节省$V \times d$个参数。对于50000词表、768维Embedding,这是3800万参数。

  2. 语义一致性:输入"cat"时查询的向量,应该与预测"cat"时需要产生的隐藏状态相似。权重共享强制这种一致性。

  3. 梯度双倍效应: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参数太多会导致:

  1. 稀有token训练不足,Embedding质量差
  2. 模型容量被Embedding"吃掉",Transformer层参数受限
  3. 内存占用增加

最优词表大小的数学推导

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的内存访问模式要求:

  1. 行优先存储:同一行的元素连续存储,利于访问同一token的完整Embedding向量。

  2. 对齐要求:Embedding维度为64或128的倍数时,内存访问效率最高。

  3. 混合精度训练:Embedding矩阵可以使用FP16或BF16存储,减少内存占用。但梯度更新通常需要FP32精度。


从Token ID看大模型的设计哲学

Token ID这个看似简单的概念,实际上承载了大模型设计的核心哲学:

离散符号与连续向量的桥接。人类语言是离散的符号系统(字符、单词),神经网络处理的是连续向量。Token ID是离散符号的数字化表示,Embedding是通往连续空间的桥梁。这个设计决定了语言模型的基本工作方式。

压缩与表达能力的权衡。词表大小、Token粒度、Embedding维度,每一个选择都是压缩效率与表达能力的权衡。没有最优解,只有特定场景下的最适解。

多语言公平性的挑战。当前主流模型的词表设计天然偏向英语,导致非英语用户承担更高的成本。这是技术选择的社会后果,值得更多关注。

字符级信息的丢失。Subword tokenize提高了效率,但丢失了字符级信息。这是大模型在算术、拼写等任务上表现不佳的技术根源之一。

理解Token ID,就是理解大模型如何"看"世界的第一步。当你下次输入一个词,不妨想一想:在模型眼中,这个词变成了什么数字?这个数字又如何影响模型的输出?这个简单的转换过程,蕴含着语言与数学、符号与向量之间的深刻联系。


参考文献

  1. Sennrich, R., Haddow, B., & Birch, A. (2016). Neural Machine Translation of Rare Words with Subword Units. ACL 2016.

  2. Gage, P. (1994). A New Algorithm for Data Compression. The C Users Journal, 12(2), 23-38.

  3. Press, O., & Wolf, L. (2017). Using the Output Embedding to Improve Language Models. EACL 2017.

  4. Inan, H., Khosravi, K., & Socher, R. (2017). Tying Word Vectors and Word Classifiers: A Loss Framework for Language Modeling. ICLR 2017.

  5. Tao, J., et al. (2024). Scaling Laws with Vocabulary: Larger Models Deserve Larger Vocabularies. arXiv:2407.13623.

  6. Nogueira, R., et al. (2022). Investigating the Limitations of Transformers with Simple Arithmetic Tasks. arXiv:2202.07385.

  7. Raschka, S. (2025). Implementing A Byte Pair Encoding (BPE) Tokenizer From Scratch.

  8. Hugging Face. (2024). Tokenizers Documentation.

  9. OpenAI. (2023). GPT-4 Technical Report.

  10. Radford, A., et al. (2019). Language Models are Unsupervised Multitask Learners. OpenAI Technical Report.