文字变成数字 — 分词、BPE、滑窗与嵌入
这一章讲一件事: 一段文字要经过哪几道工序,才能变成神经网络能吃的数字。 它在全书链条里是「阶段 1 造机 器」的第一步:备料。 读完后,第 03 章的注意力机制拿到的「输入」对你不再是黑箱。
1. 这一章讲什么
上一章说了,大语言模型是个「猜下一个词」的机器。但这句话里藏着一个被跳过的环节: 机器不会读字,只会算数。那「猫」这个词,该是哪个数?
这一章回答这个问题,答案是四道工序:切分、编号、查表、加位置。 原书第 2 章用一篇真实的短篇小说(Edith Wharton 的《The Verdict》,公有领域,20,479 个字符)1 把每一道工序都跑了一遍,所有数字都是真实输出。
2. 顶层全景
《The Verdict》原文:"I HAD always thought Jack Gisburn rather a cheap genius--…"(20,479 字符)
│
① 分词:切成词元 → ["I", "HAD", "always", …] 正则版 4,690 个;BPE 版 5,145 个
② 编号:查词表 → [40, 367, 2885, …] 每个词元一个整数 ID
③ 嵌入:查另一张表 → 每个 ID 换成 256 个数 (真实模型用 768 或 12,288 个)
④ 加位置:+ 位置向量 → 同上,256 个数 否则模型分不清谁在前谁在后
│
▼
喂给模型的训练样本:x = [290, 4920, 2241, 287],y = [4920, 2241, 287, 257]
(y 就是 x 往左挪一位——「下一个词」就是答案,第 5 章拿它算损失)
图说:四道工序全是「查表与变换」,没有任何学习发生;学习发生在后面几章。
3. 核心原理
3.1 为什么非要变成数:嵌入这个概念
神经网络内部全是加减乘除,文字是离散的符号,两者根本不兼容2。 所以第一步是建立一个从「词」到「一串数」的映射——映射就是「对应关系」:这边的每个东西,在那边都有确定的一个对应物。
这个映射叫嵌入(embedding)—— 把离散的东西(词、图、文档)映到连续向量空间(「一串数住着的地方」,住得近的两个词意思往往也近)里的一串数3。 「向量」就是「固定长度的一串数」,比如用 3 个数表示一个词:[1.78, 0.18, -2.10]。
这串数不是随便给的。早年的经典做法 Word2Vec 靠一条朴素观察把词嵌进空间: 出现在相似上下文(前后左右的词)里的词,意思也相近——于是「鸭」「鹅」「鹰」在这串数上彼此靠近, 离「柏林」「伦敦」很远4。
但这本书不用 Word2Vec。LLM 的做法更彻底:嵌入表本身就是模型参数的一部分, 初始是随机数,跟着整个模型一起训练5。这样学出来的对照表是专门为手头任务调出来的。 后面第 06 章训练时,这些数会和 其他参数一起被调整。
3.2 第一道工序:分词,以及它踩的坑
分词(tokenization)就是把文字切成一个个词元(token)—— 词元是模型读写文字的最小单位,可能是一个词、一个标点,或一个词片段6。
书里先用最简单的办法演示:拿正则表达式按空白和标点切。
《The Verdict》的 20,479 个字符被切成 4,690 个词元,开头长这样:
['I', 'HAD', 'always', 'thought', 'Jack', 'Gisburn', 'rather', 'a', 'cheap', 'genius', '--', …]7。
这个演示里藏着两个刻意的设计决定,都是坑的预演:
然后书里让这台玩具分词器当场翻车:用它处理一句不在原文里的话 "Hello, do you like tea?",
直接报 KeyError: 'Hello'——「Hello」不在词表里,整个流程当场崩掉10。
这个词表只有 1,130 个词(从一篇短篇小说里收出来的),而真实世界的词是无穷的。
这个坑的解法就是下一节的 BPE。
3.3 BPE:没有「不认识的词」这回事
真实模型的分词器叫 BPE——英文 byte pair encoding,中文「字节(计算机存文字的最小单位,一个英文字母正好占一个)对编码」—— GPT-2、GPT-3 和初版 ChatGPT 用的都是它11。
它的原理一句话:从单个字符开始,反复把「最常相邻出现的一对」合并成一个新词元, 直到词表达标12。比如「d」和「e」经常挨着,就合成「de」—— 于是常见词整个进词表,生僻词自动拆成片段,任何词都拆得开,也就不存在「词表外」。
书里给了两个真实演示:
- 生造词 "someunknownPlace" 被拆成几段正常编码,解码回来一字不差——不需要「未知词」标记(词表里顶替生词的占位项)13;
- 习题里的 "Akwirw ier" 被拆成
["Ak", "w", "ir", "w", " ", "ier"](第 5 个是空格词元), 对应 ID[33901, 86, 343, 86, 220, 959]14。
这本书直接用 OpenAI 开源的 tiktoken 库(底层是 Rust,快),调 tiktoken.get_encoding("gpt2")
就拿到 GPT-2 的分词器:词表 50,257 个词元,其中 <|endoftext|> 拿最大编号 5025615。
顺带把特殊词元(词表里不代表文字、只代表标记的项)交代清楚:
<|unk|> 顶替不认识的词(BPE 用不上它),<|endoftext|> 标记一篇文章的边界
(把上万篇文档拼成训练集时,它告诉模型「前后两段毫不相干」)。
GPT 只用 <|endoftext|> 一个,它还兼职当补位符——补位(PAD)是把一批不等长的句子
补到一样长好排成矩阵;反正注意力会掩掉补位部分,用哪个词元补无所谓16。
补充(不在书里,依据我们的 frontier 书架):BPE 的完整实现(训练合并表 + 编码重演) 在 Karpathy 的 minbpe 里有不到两百行的可读版本,我们拆过。 依 据: shelf=ai-frontier-reference/minbpe#01-bpe-core.md 事实=BPE 的全部机制就两个原子操作(数相邻对、合并替换对)加两个方向相反的循环。 原书明确说「BPE 的详细实现超出本书范围」17,想亲手写一遍的读者去那里。
3.4 第二道工序:把一本书变成「上文→下一个词」的样本对
现在有了分词器,下一步是造训练样本。这一节是本章的主走查,全程真实数字。
第一步,整篇编码。 《The Verdict》经 BPE 编码后是 5,145 个词元18。
第二步,右移一位造答案。 取一小段,x 是输入,y 是「x 往左挪一位」—— 也就是每个位置的「正确答案」就是它右边那个词元:
x: [290, 4920, 2241, 287]
y: [4920, 2241, 287, 257]
翻译成文字(这四步就是四个训练样本):
[290] → 4920 "and" → "established"
[290, 4920] → 2241 "and established" → "himself"
[290, 4920, 2241] → 287 "and established himself" → "in"
[290, 4920, 2241, 287] → 257 "and established himself in" → "a"
第 01 章那句「自监督」在这里落地了:不需要任何人标答案,把输入右移一位就是标准答案19。
第三步,滑窗切段、再按批(batch,一次喂给模型的一小摞样本)组织。 一篇长文被切成等长的小段,窗口每次往前挪 stride 个位置:
stride=1 时相邻两行只差一个词元(重叠多,浪费但样本多);
stride 等于窗口长时互不重叠。书里提示:重叠太多会增加过拟合风险——
「过拟合」指模型把训练数据背下来而没学会通用规律,这个话题第 06 章会亲眼看到20。
这些样本再按批组织,每批装几条,这个数叫批大小(batch size)。
批大小是个超参数——不是模型自己学出来的、要人提前定的设定。
小批省内存,但每步更新的噪声(每次更新方向里的随机抖动,批越小抖得越厉害)大,要试了才知道21。
3.5 第三、四道工序:嵌入查表,再加位置
词元 ID 是整数,整数之间没有意义关系——编号 3542 和 3543 毫不相干。 所以最后一道工序是嵌入层:一张「词表行数 × 嵌入维度(每个词元用多少个数来表示)」的大表, 拿词元 ID 当行号,把那一行取出来——就这一下,它本质是个查表操作,不是计算22。
书里用小尺寸演示:6 词小词表 × 3 维,固定随机种子后,嵌入表是 6 行真实随机数;
查 ID 3 就是原样取出第 4 行 [-0.4015, 0.9666, -1.1481](行号从 0 数)23。
这张表的数字一开始是随机的,训练时会被一起优化——它就是模型参数的一 部分(3.1 节说的那件事)。
真实模型的表大得多:书里的演示模型用 256 维,最小的 GPT-2 用 768 维, 最大的 GPT-3 用 12,288 维24。
但还差一样东西:这张表对顺序一无所知。 同一个词元 ID,不管出现在句首还是句尾, 查出来的都是同一行——而下一章的注意力机制本身也不带顺序感25。
修法是给每个位置再加一份「位置向量」。GPT 用的是绝对位置嵌入: 第 1 个位置加一份,第 2 个位置加另一份……而且这份位置表同样是学出来的 (2017 年原始 Transformer 用的是固定的位置编码,GPT 把它也变成了参数)26。 书里演示的成品:8 个样本 × 4 个词元 × 256 维的一批输入, token 嵌入加上位置嵌入,形状不变,内容里有了顺序27。
4. 作者的判断与证据
有证据的(都是可复现的代码输出): 4,690(正则切词数)、5,145(BPE 词元数)、50,257(词表大小:词表里词元的总数)、
50256(<|endoftext|> 的 ID)、768/12,288(两代 GPT 的嵌入维度)——全部是书里印出的真实程序输出7181524。
作者的判断:
- 演示特意选小数据(一篇短篇小说),理由是「教育目的足够、笔记本几分钟跑得完」28。 这是教学法决定,不是工程推荐。
- 嵌入维度「更高能捕捉更细微的关系,但计算更贵」,768 与 12,288 都是性能与效率的折中24。
判断(我们的,不是书里的): 这一章最大的认知价值是拆掉了两个神秘词: 「分词器」拆到最后是正则表达式加一张合并表;「嵌入层」拆到最后是一张查表的表。 这两样东西在多数科普里被当成黑箱,而这本书让你看到它们的全部代码。 如果错,会错在: 如果读者因此低估它们的工程深度——tiktoken 用 Rust 重写不是闲的, 万亿词元规模下分词速度是硬约束——我们同意,但那是性能工程, 不改变机制层面的结论。
5. 边界与局限
- BPE 的训练过程(合并表怎么造)这本书没写,只给了原理一句话;想亲手实现,看上面 minbpe 的书架锚17。
- 正则玩具分词器只是教具,别在任何真实场景用它。
- 这一章的位置嵌入是 GPT 式的「绝对位置、可学习」。2024 年后主流模型多用旋转位置编码(RoPE) 这类相对位置方案——这本书成书时没讲,同族的 Grigorov 那本书(我们拆过)有专章。
- 等长切块是为了教学简单;书末附注提到,真实训练用变长输入有助于模型泛化(把学到的规律用到没见过的新情况上)到不同长度的文本29。
6. 可带走的
- 神经网络只认数;「嵌入」就是把离散符号映成一串数的映射,LLM 的嵌入表跟着模型一起训练。
- 词元 ≠ 词:它可以是词、标点或词片段;GPT-2 的词表有 50,257 个词元。
- BPE 的用处:任何生词都能拆成子词(比词更小的词片段),从此没有「词表外」——
<|unk|>在 GPT 里不存在 。 <|endoftext|>身兼三职:文档边界、结束符、补位符。- 训练样本 = 滑窗切段 + 右移一位;「下一个词」就是答案,这就是自监督的全部含义。
- 嵌入层是查表不是计算;它的权重是模型参数,训练时一起被调。
- 位置信息要单独加:GPT 用可学习的绝对位置嵌入,与词元嵌入逐位相加。
- 批大小、窗口长、步长都是超参数;重叠太多会推高中第 06 章要讲的过拟合风险。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 为什么必须变成向量 | 2 Working with text data | text/10-ch02-2-working-with-text-data.txt:60(搜「cannot process raw text directly」) |
| Word2Vec 与「相似上下文」 | 同上 | text/10-ch02-2-working-with-text-data.txt:113(搜「Word2Vec」) |
| LLM 自己学嵌入 | 同上 | text/10-ch02-2-working-with-text-data.txt:145(搜「produce their own embeddings」) |
| 正则玩具分词与 4,690 | 同上 | text/10-ch02-2-working-with-text-data.txt:310(搜「4690」) |
| KeyError 翻车 | 同上 | text/10-ch02-2-working-with-text-data.txt:503(搜「KeyError」) |
| 特殊词元与 GPT 只用 endoftext | 同上 | text/10-ch02-2-working-with-text-data.txt:679(搜「does not need any of these tokens」) |
| BPE 原理与词表 50,257 | 同上 | text/10-ch02-2-working-with-text-data.txt:741(搜「50,257」) · text/10-ch02-2-working-with-text-data.txt:779(搜「iteratively merging」) |
| 滑窗与右移一位 | 同上 | text/10-ch02-2-working-with-text-data.txt:828(搜「5145」) · text/10-ch02-2-working-with-text-data.txt:847(搜「2241」) |
| 嵌入是查表 | 同上 | text/10-ch02-2-working-with-text-data.txt:1435(搜「function as a lookup operation」) |
| 位置嵌入(绝对、可学习) | 同上 | text/10-ch02-2-working-with-text-data.txt:1301(搜「absolute positional embeddings」) |