一句话总结:计算机只认识数字,不认识文字。Tokenization 就是把文字转换成数字序列的过程——这是 Transformer 处理语言的第一步。
4.1 为什么需要 Tokenization?
在第 3 章的全景图中,我们看到 Transformer 的第一步是"文字转数字"。这一章,我们来详细聊聊这个过程。
4.1.1 计算机的语言是数字
这是一个非常基础但重要的事实:计算机不认识汉字,也不认识英文字母。它只认识 0 和 1。
所以,当我们输入"你好世界"时,计算机看到的不是这四个汉字,而是一串数字。
把文字切成模型单元并转换成编号的过程,就叫做 Tokenization(分词/编码)。文本单元叫 token,它对应的数字编号叫 token ID。
4.1.2 在架构中的位置
看这张图,"文字转数字"(黄色框)是整个流程的起点:
输入文字 → Tokenization → Embedding → 位置信息 → Transformer Blocks → ...
没有这一步,后面的一切都无从谈起。
4.2 Tokenization 的两种方法
把文字转成数字,最直观的想法是:给每个字一个编号。但实际情况比这复杂一些。
4.2.1 方法一:简单字符匹配
最简单的方案是:给每个字符一个固定的编号。
比如输入"小沈阳江西演唱会邀请":
小 → 1
沈 → 2
阳 → 3
江 → 4
西 → 5
演 → 6
唱 → 7
会 → 8
邀 → 9
请 → 10
每个字对应一个数字,简单直接。
如果用这种方法,我们需要一个"字典"来存储所有字符和它们的编号。这个字典的大小叫做 Vocab Size(词表大小)。
对于中文来说,常用汉字大约 3000-5000 个,加上标点符号、数字、英文字母,vocab_size 可能是 10000 左右。
4.2.2 方法一的问题
简单字符匹配有几个问题:
- 效率不高:每个字都是一个 token,一篇文章会产生大量 token
- 无法处理词表外字符:新词可以用已知字符组合,但没有收录的字符仍需要回退机制
- 序列更长:“小沈阳”拆成三个字后,模型需要在后续层里再组合出人名含义;不是完全“丢失”了语义
4.2.3 方法二:字节级 BPE / tiktoken
OpenAI 的 tiktoken 使用 BPE(Byte Pair Encoding,字节对编码)。它建立在字节之上,遇到罕见文字时可以回退到字节片段,不需要把每个 Unicode 字符都收进词表。
OpenAI 的实现叫做 tiktoken,它的特点是:
- 常见片段合并:高频字节序列会变成一个 token,不保证恰好对齐字或词
- 有限词表 + 字节回退:
cl100k_base含 100,256 个可合并字节序列,加上特殊/保留 ID 后enc.n_vocab为 100,277;这是编码配置,不是“GPT-4 参数规格” - 更高效:常见词组用一个 token 表示
看同样的输入"小沈阳江西演唱会邀请",tiktoken 的处理结果:
小 → 31809
沈 → 31106, 230("沈"被拆成两部分)
阳 → 83175
江 → 70277
西 → 61786
演 → 78256, 242
唱 → 84150, 109
会 → 38093
邀 → 45932, 222
请 → 15225
你会发现:
- 有些字(如"小"、"江")用一个 token 表示
- 有些字(如"沈"、"演")被拆成了多个 token
这是因为 tiktoken 是基于字节级别的编码,对于不常见的汉字,会拆分成多个字节来表示。
4.2.4 Context Length
还有一个重要概念:Context Length(上下文长度)。
如果在上面的输入后再加上“了,”,完整句子“小沈阳江西演唱会邀请了,”会被 cl100k_base 转成 16 个 token。
Context Length 决定了模型配置允许的序列预算。不要背一张会很快过期的产品表,记住两个论文数字就够了:
- GPT-3 研究论文(2020):2,048 tokens
- LLaMA 2(2023):4,096 tokens
现代产品模型可以支持长得多的窗口,但精确的输入/输出限制要查当前模型文档。
token 效率取决于文本和 tokenizer,不存在固定的“一个汉字等于几个 token”。在 cl100k_base 里,“中华人民共和国”是 7 个字、7 个 token;“小沈阳江西演唱会邀请了,”是 12 个字、16 个 token。换一句话或换一个 tokenizer,比例都会变。
API 按 token 计量,因为 token 是模型真正处理的单位。结果是:人看起来差不多长的内容,在不同语言和编码下可能占用不同的上下文和费用。
4.3 从 Token 到 Embedding
有了 Token ID 之后,下一步是把它转换成向量。这个过程叫做 Embedding(嵌入)。
4.3.1 Embedding Lookup Table
模型里有一个巨大的"查找表",叫做 Embedding Lookup Table(嵌入查找表)。
这个表的大小是 [vocab_size, d_model]:
- vocab_size:词表大小(有多少个不同的 token)
- d_model:每个 token 用多少维的向量表示
图中是一个为了讲解方便的玩具配置,不是 GPT-4 的已公开规格:
- vocab_size = 100256(图中假设查找表有 100256 行)
- d_model = 64(每个 token 用 64 维向量表示)
表中每一行对应一个 token,每一列是向量的一个维度。比如:
- Token 0 的向量是 [0.62, 0.51, 0.09, ..., 0.31, 0.92, 0.85]
- Token 1 的向量是 [1.49, 0.59, 1.53, ..., 0.98, 0.12, 0.62]
- ...
整个表有 100256 × 64 ≈ 640 万个数字。这些数字是可学习参数。真实模型的行数必须以模型配置的 vocab_size 为准。
4.3.2 查找过程
有了查找表,把 Token ID 转换成向量就很简单了:直接按行号查找。
以输入"小沈阳江西演唱会邀请了,"为例:
-
首先 Tokenization:得到 Token ID 序列 [31809, 31106, 230, 83175, 70277, 61786, 78256, 242, 84150, 109, 38093, 45932, 222, 15225, 35287, 3922]
-
然后查表:每个 Token ID 对应一行向量
- Token 31809("小")→ 查第 31809 行 → 一个 64 维向量
- Token 31106("沈"的部分字节)→ 查第 31106 行 → 另一个 64 维向量
- ...
-
最终得到一个矩阵:
[context_length, d_model]=[16, 64]
这个 16×64 的矩阵,就是输入文字的"数字表示",可以送入后续的神经网络处理了。
4.3.3 为什么用向量表示?
你可能会问:为什么要把 Token ID 转换成向量?直接用 ID 不行吗?
向量表示有几个关键优势,但要分清“初始 Embedding”和“经过 Transformer 后的上下文表示”:
-
学习起点:查表得到的是每个 token ID 的静态起点向量,它可以学到有用的结构,但还没有这句话的上下文。
-
上下文化:Transformer Block 会把静态起点变成上下文相关的隐藏状态。“小沈阳”这样的名称,其整体含义通常是多个 token 经过多层后组合出来的。
-
连续空间:ID 是离散的(1, 2, 3...),向量是连续的,可以参与矩阵运算和梯度学习。
Embedding 向量不是人工设计的,而是模型在训练中学习的。相近 token 可能呈现有用结构,但不要把“所有语义相近的词都必然在初始表里互为近邻”当成保证。
4.4 实际操作:用 tiktoken 试试
如果你想亲自体验 Tokenization,可以用 OpenAI 的 tiktoken 库:
# 代码示例
import tiktoken
# 使用 cl100k_base 编码配置
enc = tiktoken.get_encoding("cl100k_base")
# 编码
text = "小沈阳江西演唱会邀请了"
tokens = enc.encode(text)
print(f"Token IDs: {tokens}")
print(f"Token 数量: {len(tokens)}")
# 解码(把 Token ID 转回文字)
decoded = enc.decode(tokens)
print(f"解码结果: {decoded}")
# 查看每个 Token 对应的原始字节
for token_id in tokens:
token_bytes = enc.decode_single_token_bytes(token_id)
token_text = token_bytes.decode("utf-8", errors="replace")
print(f" {token_id} → bytes={token_bytes!r}, text={token_text!r}")
运行结果大概是:
Token IDs: [31809, 31106, 230, 83175, ...]
Token 数量: 15
解码结果: 小沈阳江西演唱会邀请了
31809 → bytes=b'\xe5\xb0\x8f', text='小'
31106 → bytes=b'\xe6\xb2', text='�'
...
这里故意用 decode_single_token_bytes()。单个 token 可能只包含一个 UTF-8 字符的部分字节,强行把它单独解码成文字会丢失信息;整串 tokens 放在一起解码才能还原原文。
4.5 关键数字:参数量计算
Embedding 层是模型参数的重要组成部分。让我们算一下它有多少参数。
4.5.1 计算公式
Embedding 参数量 = vocab_size × d_model
4.5.2 实际例子
| 模型 | vocab_size | d_model | Embedding 参数量 |
|---|---|---|---|
| GPT-2 Small | 50257 | 768 | 3860 万 |
| GPT-2 Large | 50257 | 1280 | 6433 万 |
| GPT-3 | 50257 | 12288 | 6.18 亿 |
| LLaMA-2-7B | 32000 | 4096 | 1.31 亿 |
可以看到,Embedding 层的参数量相当可观。对于 GPT-3 来说,光是这个查找表就有 6 亿多个参数!
有些架构会让输入 Embedding 和输出词表投影共享同一组权重。这种情况下,计算整个模型参数量时不能把它们重复统计两次。
4.6 本章总结
这一章我们学习了 Transformer 的第一步:把文字转换成数字。
4.6.1 核心概念
| 概念 | 英文 | 解释 |
|---|---|---|
| Tokenization | 分词/编码 | 把文字切分成 token 并转换成数字 |
| Token | - | 分词后的基本单元 |
| Vocab Size | 词表大小 | 字典中有多少个不同的 token |
| Embedding | 嵌入 | 把 Token ID 转换成向量 |
| d_model | 模型维度 | 每个 token 的向量维度 |
| Context Length | 上下文长度 | 配置允许的序列预算,通常由输入和输出共享 |
4.6.2 数据变换过程
"小沈阳江西演唱会邀请了" # 原始文字
↓ Tokenization
[31809, 31106, 230, 83175, ...] # Token ID 序列 [context_length]
↓ Embedding Lookup
[[0.62, -0.51, ...], # Embedding 矩阵 [context_length, d_model]
[1.30, 0.47, ...],
...]
4.6.3 核心认知
Tokenization + Embedding 是文字进入 Transformer 的第一步:先把文字切成 token,再把每个 token ID 查成一个高维起点向量。真正的上下文语义,还要靠后面的 Transformer Block 一层层组合出来。
本章交付物
学完这一章,你应该能够:
- 解释为什么需要 Tokenization
- 说出 BPE/tiktoken 相比简单字符分割的优势
- 理解 Embedding Lookup Table 的结构和作用
- 计算 Embedding 层的参数量
下一章预告
现在我们有了文字的向量表示。但还缺少一个关键信息:位置。
"我爱你"和"你爱我"包含完全相同的字,但意思完全不同。如何让模型知道每个字在句子中的位置?
下一章,我们来聊 Positional Encoding(位置编码)——给每个位置加上独特的"门牌号"。