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

词表该多大?

词表大小是一笔交易
词表每多一项,模型两头就各多 d 个参数;词表太小,同一段话就要更多 token。词表大小是在参数和序列长度之间做交易,要用实测数据来定。

本课一问:词表该多大?

这一课先不碰:embedding 表具体怎么查、里面的数字从哪来(第 5、6 课)。这里只需要一个事实:词表里的每一项,在模型的输入端和输出端各对应一行 d 个参数(d 是模型宽度,Qwen3.8 是 5,120)。

② 从已有出发

词表大小是 BPE 唯一要你定的数

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

第 3 课:BPE 合并到多大为止,是你来定的超参数。贯穿例句「小明把苹果给了小王」在 Qwen3.8 里只要 5 个 token,靠的是一张 248,320 行的大词表;GPT-2 的 50,257 行词表要 21 个。它牵动流水线的两头:输入端灰色的「Embedding 查表」是一张 V 行的表,输出端的「lm_head」要对 V 个候选各打一个分——两张都是 V × d 的矩阵。

③ 先自己设计

朴素的想法:词表越大越好

词表越大,常见字串越能整块进词表,序列越短——看起来只有好处。拖动词表大小,看它在两个模型里各吃掉多少参数:

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案词表越大越好,直接用大模型的词表
④ 改进到现代做法

交易的两边各是什么

本课在演进图里的位置:第三代,大词表。

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

词表变大,得到的:常见字串整块进词表,同样的文字 token 更少——训练吃得下更多文字,推理更快,上下文窗口装得更多。付出的:

  1. 参数:输入 embedding 与输出 lm_head 各是 V × d;不共享时合计 2 × V × d。
  2. 每个 token 的输出计算:lm_head 要对 V 个候选各算一个打分,V 越大越慢。
  3. 训练信号被摊薄:词表越大,每个 token 在语料里出现得越少,它那一行参数得到的训练越少——罕见 token 的向量学得差(第 6 课回扣)。

现实里的词表一直在变大。各模型官方 config.json 里的词表(embedding 行数):

模型词表 V宽度 d输入输出共享?
GPT-2(2019)50,257768共享
DeepSeek-V2102,4005,120不共享
DeepSeek-V3129,2807,168不共享
Qwen3-8B / Qwen3-32B151,9364,096 / 5,120不共享
GLM-5.3154,8806,144不共享
Kimi K3163,8407,168不共享
Qwen3.8-27B(锚点)248,3205,120不共享

大词表配大模型时,两张表占的比例并不大:Qwen3.8-27B 是 9.2%,Qwen3-32B 是 4.7%,Qwen3-8B 是 15.2%(按各自 config 与实测总参数现算)。同一代 Qwen3 里 0.6B 到 4B 的小模型则让输入输出共享一张表(tie_word_embeddings = true),正是因为小模型里两张表太占比例。

⚠

常见误解一:「词表大小拍脑袋定个数就行」。 旧版课程说「小模型配 15 万词表,embedding 占掉 70% 参数」,这个数没有实算过。用我们的模型结构现算:d = 256、共享一张表时,151,936 项的词表让 embedding 占 92.5%;248,320 项占 95.3%。词表要按模型规模和语料实测来定。

常见误解二:「vocab_size 就是分词器里的 token 数」。 Qwen3.8 的分词器有 248,077 项(248,044 个 BPE token + 33 个特殊 token),config 里的 vocab_size 却是 248,320 = 970 × 256——embedding 表按 256 的倍数多留了 243 行,多出来的行永远不会被查到。这是为了让矩阵形状对齐硬件,方便高效计算;nanoGPT 把 GPT-2 的 50,257 补到 50,304(64 的倍数)也是同一个做法。

🔍 大模型该配多大词表

Tao 等 2024《Scaling Laws with Vocabulary: Larger Models Deserve Larger Vocabularies》(arXiv:2407.13623,NeurIPS 2024)在 33M 到 3B 参数的模型上扫描词表大小,结论是最优词表随算力预算增大;按他们的拟合,Llama2-70B 的词表应至少是 21.6 万,而它实际只用了 3.2 万。这是实验拟合的结论,只在他们的模型族和数据上验证过。

⑤ 极小真数字,算一次

两个要会算的量

第一个:词表占多少参数。 点公式里每一项:

输入 embedding + 输出 lm_head 的参数
× × =
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
Qwen3.8 的数取自锚点模型官方 config(anchor-model.json),与 safetensors 实测的 embed_tokens、lm_head 各 1,271,398,400 一致;项目模型的数取自 configs/baseline.json 与基线 run 的 summary.json。

第二个:词表切得好不好。 压缩率(第 2 课)看每个 token 装几个字符;fertility 反过来看每个「词」平均被切成几个 token:

fertility(每个词被切成几个 token)
= ÷
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
旧版课程写「fertility 大于 1.5 说明词表不友好」,这个阈值没有出处,本课程不再使用。比较不同词表时,同一份文本上并排比 fertility 即可。

我们的里程碑分词器(词表 2,048)在验证集上:压缩率 1.022 字符/token,fertility 1.195 token/汉字;验证集里出现的 2,319 个不同汉字中,只有 1,015 个整字就是一个 token;1,791 次合并里有 1,059 次刚好拼成一个字、299 次只拼出半个字——小词表的合并名额,大多花在把单字拼完整上。

五档各训了一个模型(其余同基线)。val bpb 在 4,096 档最低(2.164),8,192 档反而回升到 2.184。读这个「最低点」要小心,实验脚本自己写明了两个局限:每步看的 token 数相同,而大词表每个 token 装的字节更多,400 步里 8,192 档把语料过了 8.52 遍、512 档只过了 3.99 遍;而且这是 370 万参数、50 万 token 的小规模,结论不能外推。这正是第 4 课的主题:没有通用的最优词表,只有在自己的规模上测出来的折中。

👀

试三件事: ① 在上面的滑杆上依次点 GPT-2、Qwen3、Qwen3.8,看我们小模型里 embedding 的占比怎么变;② 展开改进方案,在五档之间切换,找压缩率涨得最慢的那一段;③ 看「val bpb」一栏:词表不同的模型不能直接比 val loss(每个 token 装的文字多少不同),要比每字节比特数。

⑦ 证据三问

词表大小怎么定才算有依据

⑧ ⑨ ⑩ 带走与下一步

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

📌

一句话: 词表每多一项,模型两头就各多 d 个参数;词表太小,同一段话就要更多 token——词表大小是在参数和序列长度之间做交易,要用实测来定。

下一个缺口

到这里,一句话已经变成了一串 ID,词表也定了。可 ID 只是编号——能直接拿来做数学运算吗?第 5 课:Token ID 为什么不能直接拿来算?

你的提问

你可能会问:「Qwen3.8 的 248,320 是怎么定出来的?」 官方没有公开选择过程。能核对的只有:分词器实际 248,077 项,补到 256 的倍数得到 248,320;这张词表从 Qwen3.5 开始使用(R2 调研按发布说明整理)。本课程不替官方编理由。