从0训练LLM · 现代大模型体系 阅读 + 实验 ≈ 30 分钟 页内实验 ×2
C1 文字怎么变成数字 第 5 课

Token ID 为什么不能直接拿来算?

从编号到向量
ID 只是行号,大小和距离都没有意思;one-hot 让任意两个词一样远。Embedding 就是拿行号去一张可学习的表里取出一行向量——one-hot 乘矩阵和直接查表是同一件事。

本课一问: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,看这两种表示下它「离谁最近」:

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案直接把 ID 当数值,或者换成 one-hot 向量
④ 改进到现代做法

Embedding:one-hot 乘矩阵,就是查表

本课在演进图里的位置:从编号走到向量的第一步。

文字 → 数字的演进:每一步都被上一步的某个真数字逼出来 · 高亮 = 本课
  1. 起点 每个字符直接用 Unicode 码点当编号 最容易想到的做法
    解决了不用训练,任何文字都能编号。
    卡在码点从 0 排到 1,114,111,embedding 表得开 1,114,112 行,是 Qwen3.8 实际 248,320 行的 4.5 倍;而且一个字一个 ID,序列一点不缩短。
  2. 朴素 ① 按字切(字符级词表) 只收语料里出现过的字
    解决了字符集封闭,词表小。
    卡在序列最长:209 字的段落按字切要做 43,681 次注意力打分,是 Qwen3.8 子词切分(106 个 token)的 3.9 倍。
  3. 朴素 ② 按词切 + 固定词表 早期神经机器翻译的做法(Sennrich 2015 的对照组)
    解决了一个词一块,序列短。
    卡在词是开放集合:我们的语料按空白和标点切出 70,310 种块,94.8% 只出现过一次;验证集里连训练集没见过的字都有 83 种——含它们的词只能记成 UNK。
  4. 第一代 子词 BPE(以字符为初始单元) Gage 1994 压缩算法 → Sennrich 2015 用于切词
    解决了按频次反复合并相邻对:常见的整块、罕见的拆碎,词表大小由合并次数控制。
    卡在初始单元是字符:要覆盖全部 Unicode,基础词表就超过 13 万(GPT-2 论文 §2.2);只收见过的字,我们的验证集照样缺 83 种字。
  5. 第二代 字节级 BPE(256 个字节兜底) GPT-2 2019
    解决了初始词表就是 256 个字节,任何文字都能编码,永远不出 UNK。
    卡在词表没学过的语言会被切碎:同义的中文段落,GPT-2 切 461 个 token,Qwen3.8 切 106 个,差 4.3 倍;同义英文段落两者一样多。
  6. 第三代 为多语言训练的大词表 2024–2026
    解决了中文常用字串整块进词表:「我喜欢机器学习」只要 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%。
  7. 表示 ① ID → Embedding 查表 神经语言模型的标准做法 本课
    解决了ID 只是行号,取出一行 5,120 维向量;one-hot 乘矩阵与查表等价,但查表只拷现成的数。
    卡在表里的 1,271,398,400 个数(输入端一张表)从哪来?人填不完,也不知道每一维该是什么意思。
  8. 表示 ② 随机初始化、和整个模型一起训练的 Embedding Bengio 等 2003 → 今天的 LLM
    解决了用「预测下一个 token」的 loss 端到端训练,用法相近的 token 被推到相近的位置。
    卡在只在出现时才被推:我们的项目里一个 batch 8,192 个位置,平均只出现 1,453 个不同 token;罕见 token 学得少——MiniMax 的「嘉祺」在后训练里被推偏,输出层那一行与原来的余弦只剩 0.329。
  9. 表示 ③ 向量该多宽(hidden size d) 各家自选:GPT-2 768 · Qwen3-8B 4,096 · Qwen3.8-2.4T 8,192 · Qwen3.8-27B 5,120
    解决了维度越高,每个 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」。
数字口径:码点总数按 Unicode 标准(U+0000–U+10FFFF);词表与宽度取各模型官方 config.json(anchor-model.json、frontier-models.json);token 数取真 Qwen3.8 / GPT-2 分词器的离线切分(c1-tokenize.json);语料统计、batch 覆盖、宽度扫描取项目实测(c1-tokens.json);「13 万以上」出自 GPT-2 论文 §2.2;「嘉祺」的余弦 0.329 出自 MiniMax 官方博客 2026-05-25(官方自报)。

one-hot 本身不是没用,它只是太稀疏、太长。把 one-hot 行向量乘上一张 V × d 的矩阵 E,因为只有一位是 1,结果恰好是 E 的某一行——乘法退化成了查表。实现里从不真的构造 one-hot,而是直接按 ID 取行;两者数学上完全等价。下面是锚点模型 Qwen3.8 的真实形状(切「小规模」看结构,切「真实规模」看实际大小):

张量图 · Embedding 查表(Qwen3.8 真实形状)
「小明把苹果给了小王」进入 Embedding
X = onehot(ids) · E = E[ids]
输入参数(可学习)输出中间结果 编号槽 = 索引斜线 = 掩码(留白 = 不读)
边长按对数比例缩放,块边上的数字是真实值;同一维度符号的边在全图等长。
维度含义 真实规模 小规模
T 序列长度:这句话切出的 token 数 5 5
V 词表行数(vocab_size,含补齐的 243 行) 248,320 8
d 模型宽度(hidden_size),每个 token 的向量长度 5120 4
索引(编号槽)→ 掩码(one-hot,只读 1 的那一列)→ 选中的值(X 的每一行),选择链是闭合的。实现里跳过中间的 one-hot,直接按 ID 取行(gather)。形状取自 anchor-model.json。

两件事从这里开始成立,后面几章都要用:

⚠

常见误解:「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 绑定。

⑤ 极小真数字,算一次

一次查表,拆开看

Embedding:第 t 个 token 的向量
t = · =
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
为什么实现里直接查表:真按矩阵乘法算,这 5 个 token 要做 5 × 248,320 × 5,120 = 6,356,992,000 次乘加,算出来的只是 25,600 个现成的数;查表只需要把这 25,600 个数拷出来。
👀

试三件事: ① 在朴素方案里依次选「猫」「狗」「汽车」,看按 ID 排出的「最近」有没有道理;② 张量图切到「真实规模」,把鼠标放在 V 那条边上,体会 one-hot 有多长;③ 改进方案里选一个人名,对比训练前后离它最近的 5 个 token。

⑦ 证据三问

查表这一步凭什么成立

⑧ ⑨ ⑩ 带走与下一步

一句话、下一个缺口、你的提问

📌

一句话: ID 只是行号;Embedding 就是拿行号去一张可学习的表里取出一行向量——one-hot 乘矩阵和直接查表是同一件事。

下一个缺口

表有了,形状也清楚了。可这 1,271,398,400 个数是谁写进去的?总不能是人一个一个填的。第 6 课:Embedding 的数字从哪来?

你的提问

你可能会问:「RAG、向量数据库里说的 embedding,就是这张表吗?」 不是同一个东西,第 6 课的「常见误解」专门讲。