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

Embedding 的数字从哪来?

表里的数字是训出来的
Embedding 表一开始是随机数。训练时它和整个模型一起,被「预测下一个 token」的 loss 一点一点推着走,用法相近的 token 慢慢被推到相近的位置。没有人给任何一维定义含义。

本课一问:Embedding 表里的数字,是从哪来的?

这一课先不碰:反向传播的完整推导(地基章)、优化器的细节(C5)。只看清三件事:数字一开始是什么、被什么推着改、每一步改多少。

② 从已有出发

上一课留下的那张表

从已有出发 · 累计数据流 点已学环节看形状与参数
解码层

第 5 课:Embedding 是一张 V × d 的表,ID 是几就取第几行。贯穿例句「小明把苹果给了小王」查表得到 5 行、每行 5,120 个数,共 25,600 个;整张表是 248,320 × 5,120,一共 1,271,398,400 个数。它们必须「有意义」——用法相近的 token,向量也该相近——否则查表和 one-hot 没有区别。

③ 先自己设计

朴素的想法:人给每一维定义含义

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案人给每一维定义含义,再逐个填数

对话样本(L274–L312)讲维度时用过这个思路:给「猫」「狗」「汽车」各写 4 个人能看懂的分数——动物、有生命、交通工具、抽象概念。作为入口它很直观:相近的东西,分数也相近。

但它是教学构造,不是任何模型学到的样子。把它当成做法,就要为 1,271,398,400 个格子逐个定义含义、逐个填数。

④ 改进到现代做法

看它自己长出来:训练时间轴

本课在演进图里的位置:

文字 → 数字的演进:每一步都被上一步的某个真数字逼出来 · 高亮 = 本课
  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(官方自报)。

下面是我们自己的项目模型在训练过程中,20 个 token 的 embedding 快照(投影到 2D)。这里换了例子:项目模型是在三国演义上训练的,贯穿例句里的「苹」「给」它根本没学过(第 3 课压轴),所以选的是语料里的高频词——人名、数字、方位、动词、标点各 4 个。拖动时间轴,或者点「播放训练」:

真实日志回放 · 训练中的 embedding(项目模型)

为什么用法相近的 token 会被推到一起?模型要在每个位置预测下一个 token。如果两个 token 经常出现在相似的上下文里(前后跟着差不多的词),那么「把它们表示得相似」能让同一套后续计算对两者都预测得好,loss 更低——梯度就会把它们往相似的方向推。没有人告诉模型「猫和狗应该接近」,接近是降低 loss 的副产品。

我们项目模型的实测:20 个 token 在训练前按规则分成 5 组(人名、数字、方位、动词、标点),在完整的 256 维空间里算两两余弦——同组的平均值从训练前的 0.000 变成训练后的 0.297,不同组之间只从 0.003 变到 0.047。热力图在第 5 课的改进方案里。C0 第 1 课留下的「小明和小王像不像」,答案就在这里:常在相似上下文里出现的人名,向量会被推到一起。项目语料里没有小明、小王,换成书里的四个人名(玄德、孔明、曹操、關公):两两余弦的平均值从训练前的 -0.008 升到训练后的 0.384。

还有一个细节决定了「哪些行会被改」:输入端查表只用到了这批文字里出现过的那些行,没出现的 token,它那一行在这一步拿不到输入端的梯度。组件下方给出了我们项目里一个 batch 覆盖多少行的实测。

⚠

常见误解一:「每一维对应一个人能说清的特征」。 对话样本自己在 L322 就纠正过:真实模型的某一维通常不能解释成「动物程度」。语义分布在很多维的组合里。更根本的是,单独某一维的数值本来就不唯一:把向量空间整体换一组正交坐标轴(给 embedding 表乘一个旋转矩阵),再把所有读写这些向量的权重相应改写,模型的输出可以完全不变——同一个模型,「第 1 维」可以是任何方向。

常见误解二:「Qwen 里的 token embedding,就是 RAG、向量数据库用的那种 embedding」。 不是同一个东西(对话样本 L494–L536)。token embedding 是一张表,一个 token 一行,和上下文无关,作为语言模型的一部分用「预测下一个 token」端到端训练;BGE、E5、Qwen3-Embedding 这类 embedding 模型是一整个网络,把一整段话压成一根向量,通常用对比学习训练——相似的两段话拉近、不相似的推远。两者有关(embedding 模型常从语言模型初始化),但输出、训练目标都不同。

🔍 真实事故:认识却说不出名字

MiniMax 在 2026-05-25 的官方排查记录(原文)里描述:他们的模型说不出「马嘉祺」,「嘉祺」是单个 token(ID 190467),后训练数据里含它的样本不到 5 条。排查结果:SFT 前后输入 embedding 几乎没变,但输出端 lm_head 的对应行明显偏移——全词表 lm_head 与预训练版本的余弦相似度均值 0.9837、最小只有 0.3290,4.9% 的 token 低于 0.95。原因与本课那段「哪些行会被改」同源:输入端只有出现的 token 拿到梯度,输出端的 softmax 却在每一步都给所有「不是答案」的 token 送来「把打分压低」的梯度,很少当答案的 token 就一路被压走。修复办法是混入覆盖全词表的合成数据,最小余弦回到 0.9711。前提是输入输出不共享一张表(MiniMax、Qwen3.8 都不共享)。视频讲解:五道口纳什《为什么 LLM 无法输出马嘉祺》(B 站,24:47)。

⑤ 极小真数字,算一次

一次更新,拆开看

embedding 表中一行的一次更新(最简单的形式)
← − ·
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
这是最朴素的梯度下降(SGD)写法。我们的项目模型实际用 AdamW:每个参数的步长还要按它的历史梯度自适应缩放,并带权重衰减(C5 讲),所以组件里「一次真实更新」的变化量不等于 η × 梯度,但方向逻辑相同。那组更新前后的数值全部来自训练日志。

在上面的组件里看「一次真实更新」:整行的变化量只是这一行长度的一小部分——一步只挪一点点,几百步累积起来,点的聚拢才看得出来。对话样本里「猫 [0.20, 0.70] → [0.21, 0.68]」讲的是同一件事,但那组数是示意;这里是真的。

👀

试三件事: ① 把时间轴拖回第 0 步:同一组的 token 散在各处——随机初始化里没有语义;② 播放训练,看哪一组最先聚到一起,同时看右边 val loss 在那段下降了多少;③ 读「哪些行会被改」:一个 batch 只覆盖词表的一部分,想想只在语料里出现过几次的罕见 token,它那一行一共能被推几次。

⑦ 证据三问

「训出来的」有多可靠

⑧ ⑨ ⑩ 带走与下一步

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

📌

一句话: Embedding 表一开始是随机数;训练时它和整个模型一起被「预测下一个 token」的 loss 推着走,用法相近的 token 慢慢被推到相近的位置——没有人给任何一维定义含义。

下一个缺口

每个 token 一行、每行 d 个数。d 该取多少——越大越好吗?Qwen3.8 的 5,120 是算出来的吗?第 7 课:Embedding 维度越大越好吗?

你的提问

C1 还没有真实提问记录。你可能会问: