Token ID 为什么不能直接拿来算?
本课一问:Token ID 为什么不能直接拿来算?
这一课先不碰:表里的数字是怎么来的(第 6 课)、每行为什么是 5,120 个数(第 7 课)。只回答:从整数编号到「能参与计算的数」,中间该怎么接。
流水线走到了 Embedding 这一格
前四课把一句话变成了一串 ID:贯穿例句「小明把苹果给了小王」在 Qwen3.8 里是 5 个整数:115942、96373、99203、100105、126835。接下来的网络要做乘法、加法、求相似度——这串整数能直接喂进去吗?
两个朴素方案:ID 当数值,或者 one-hot
最直接:ID 本来就是数,直接当输入。稍微讲究一点:ID 的大小没意义,那就把每个 ID 变成一根 one-hot 向量——长度等于词表大小,只有自己那一位是 1。选一个 token,看这两种表示下它「离谁最近」:
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
给每个 ID 配一行 d 个可学习的数,把这些行摞成一张 V × d 的表,叫 embedding 表。ID 是几,就取第几行。向量之间的距离不再由编号决定,而由训练决定——训练会把用法相近的 token 推到相近的位置(第 6 课)。下面是我们项目模型训练前后,20 个 token 的向量相似度:
Embedding:one-hot 乘矩阵,就是查表
本课在演进图里的位置:从编号走到向量的第一步。
- 解决了不用训练,任何文字都能编号。卡在码点从 0 排到 1,114,111,embedding 表得开 1,114,112 行,是 Qwen3.8 实际 248,320 行的 4.5 倍;而且一个字一个 ID,序列一点不缩短。
- 解决了字符集封闭,词表小。卡在序列最长:209 字的段落按字切要做 43,681 次注意力打分,是 Qwen3.8 子词切分(106 个 token)的 3.9 倍。
- 解决了一个词一块,序列短。卡在词是开放集合:我们的语料按空白和标点切出 70,310 种块,94.8% 只出现过一次;验证集里连训练集没见过的字都有 83 种——含它们的词只能记成 UNK。
- 解决了按频次反复合并相邻对:常见的整块、罕见的拆碎,词表大小由合并次数控制。卡在初始单元是字符:要覆盖全部 Unicode,基础词表就超过 13 万(GPT-2 论文 §2.2);只收见过的字,我们的验证集照样缺 83 种字。
- 解决了初始词表就是 256 个字节,任何文字都能编码,永远不出 UNK。卡在词表没学过的语言会被切碎:同义的中文段落,GPT-2 切 461 个 token,Qwen3.8 切 106 个,差 4.3 倍;同义英文段落两者一样多。
- 解决了中文常用字串整块进词表:「我喜欢机器学习」只要 2 个 token(GPT-2 要 17 个)。各家词表:DeepSeek-V3 129,280 · Qwen3 151,936 · GLM-5.3 154,880 · Kimi K3 163,840 · Qwen3.8 248,320。卡在两头各一张 V × d 的表:2 × 248,320 × 5,120 = 2,542,796,800 个参数,占 Qwen3.8 的 9.2%;同样的词表搬到我们 d = 256 的小模型上,embedding 占 95.3%。
- 解决了ID 只是行号,取出一行 5,120 维向量;one-hot 乘矩阵与查表等价,但查表只拷现成的数。卡在表里的 1,271,398,400 个数(输入端一张表)从哪来?人填不完,也不知道每一维该是什么意思。
- 解决了用「预测下一个 token」的 loss 端到端训练,用法相近的 token 被推到相近的位置。卡在只在出现时才被推:我们的项目里一个 batch 8,192 个位置,平均只出现 1,453 个不同 token;罕见 token 学得少——MiniMax 的「嘉祺」在后训练里被推偏,输出层那一行与原来的余弦只剩 0.329。
- 解决了维度越高,每个 token 能装的信息越多。卡在参数按 d² 涨:我们的模型 d 从 128 到 256,总参数 1,066,112 → 3,688,704,val loss 4.776 → 4.641(d = 256 这一档是参照 run ref_baseline,与项目基线同配置同种子;基线记录的 4.626 与它之差是重跑波动,见 C14);没有公式能给出「最优的 d」。
one-hot 本身不是没用,它只是太稀疏、太长。把 one-hot 行向量乘上一张 V × d 的矩阵 E,因为只有一位是 1,结果恰好是 E 的某一行——乘法退化成了查表。实现里从不真的构造 one-hot,而是直接按 ID 取行;两者数学上完全等价。下面是锚点模型 Qwen3.8 的真实形状(切「小规模」看结构,切「真实规模」看实际大小):
| 维度 | 含义 | 真实规模 | 小规模 |
|---|---|---|---|
| T | 序列长度:这句话切出的 token 数 | 5 | 5 |
| V | 词表行数(vocab_size,含补齐的 243 行) | 248,320 | 8 |
| d | 模型宽度(hidden_size),每个 token 的向量长度 | 5120 | 4 |
两件事从这里开始成立,后面几章都要用:
- 每个 token 的向量只由它自己的 ID 决定,和上下文无关。「苹果」在「吃苹果」和「苹果手机」里查到的是同一行;「它」查到的向量不知道它指的是谁。让向量吸收上下文,是注意力(C3)的工作。
- 只看这些向量,分不出顺序。「小明打了小王」和「小王打了小明」切出的 3 个 token 完全相同,只是顺序不同(真分词器实测),查表得到的是同一组向量。模型怎么知道谁在前,是 C2 的问题。
常见误解:「ID 大的 token 更重要」「ID 相近的 token 意思相近」。 词表按 BPE 合并的先后编号:GPT-2 的 5 万次合并里,第 i 次合并造出的 token 的 ID 恰好是 256 + i;Qwen3.8 近似如此(差最多 201)。所以 ID 的大小只记录「它第几个被合并出来」。对话样本里的「猫 3812、汽车 5517」是示意数;真 Qwen3.8 里猫是 98500、狗是 98713、汽车是 97377——挨得近,只因为它们在同一阶段被合并出来。
🔍 为什么分词器训练完就要冻结
embedding 表是按 ID 排的:第 99203 行就是「苹果」的向量。如果训练到一半换了分词器,同一个词换了 ID,查到的就是别的词训练出来的那一行,之前学到的全部作废。所以分词器必须在训练模型之前定死、存档,和模型一起发布——Qwen3.8 的仓库里,tokenizer.json 就和权重放在一起。输出端也一样:Qwen3.8 的 lm_head 是另一张 248,320 × 5,120 的表(不与 embedding 共享),第 i 行负责给 ID 为 i 的 token 打分,同样按 ID 绑定。
一次查表,拆开看
试三件事: ① 在朴素方案里依次选「猫」「狗」「汽车」,看按 ID 排出的「最近」有没有道理;② 张量图切到「真实规模」,把鼠标放在 V 那条边上,体会 one-hot 有多长;③ 改进方案里选一个人名,对比训练前后离它最近的 5 个 token。
查表这一步凭什么成立
- 它解决什么问题:把没有数学意义的编号,换成可以学习、可以比较远近的向量。
- 数学上为什么可能有效:one-hot 乘矩阵等于取出对应行 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条只有一项非零的求和 出处待补 ;one-hot 之间两两距离恒为 √2 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条两个位置各差 1 出处待补 ;ID 大小 ≈ 合并先后是对两个真词表的实测 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条GPT-2:50,000 次合并全部满足 ID = 256 + i;Qwen3.8:247,587 次合并中 42.0% 严格满足,最大偏差 201 出处c1-tokenize.json · vocab_order ↗需联网 。
- 实验上为什么最后选了它:把词表示成可学习的向量、和语言模型一起训练,最早系统提出于 Bengio 等 2003 的神经语言模型 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条论文在 Brown 与 AP News 语料上与 n-gram 模型对比困惑度 出处Bengio 等 2003 · JMLR ↗需联网 ;训练后「用法相近的 token 更近」要看我们模型的实测(改进方案里的热力图)。
一句话、下一个缺口、你的提问
一句话: ID 只是行号;Embedding 就是拿行号去一张可学习的表里取出一行向量——one-hot 乘矩阵和直接查表是同一件事。
下一个缺口
表有了,形状也清楚了。可这 1,271,398,400 个数是谁写进去的?总不能是人一个一个填的。第 6 课:Embedding 的数字从哪来?
你的提问
你可能会问:「RAG、向量数据库里说的 embedding,就是这张表吗?」 不是同一个东西,第 6 课的「常见误解」专门讲。