第 2 课的数字都是我们手工打的。可大模型要读几百万条句子,谁替它打? 这一课我们自己动手:造一套中文算术语料,用三种刀法把它切开,给每一块编号, 再让模型把每个字的向量自己跑出来。 全程只有加减乘除和一个「猜下一个字」的游戏。
大模型不读课本,它读语料(corpus)——一大坨句子。你喂它什么,它就成为什么。
第 2 课的「国王 − 男人 + 女人 = 女王」,数字是我们手工打的。 手工打分能撑住 2 个维度、9 个词;可真实语料是几百万条句子、几十万个词——没人打得起这个分。
所以这一课我们换个计划:不手工打分,让模型自己学。为了让「它到底学没学会」一眼可验证,我们给它出最干净的一类题——只有加减乘除的中文算式,答案唯一,对错分明:
十五加一百一十五等于? 谜底是 一百三
四种运算各出一批:加法、减法在 0~999 里随便挑(结果不超过 999), 乘法和除法只取 2~9 的乘法口诀表范围内(除法一律除得尽)。 这套语料一共 4,000 条,每一条都长这样:
于是造语料之前,先把「中文怎么念数字」的规则钉死。下面这台机器你随便拨一个数, 它会告诉你正确的读法,以及为什么这么读:
115?就是「轮到模型接话了」的信号):? + 答案语料一共 4,000 条,加起来 51,288 个字, 平均一条 12.82 个字。 整本书翻来覆去只用到 20 个不同的字:
? 一 七 三 乘 九 二 于 五 以 八 六 减 加 十 四 百 等 除 零
注意这里面有 ? 和「等于」——它们是给模型的路标:? 的意思就是「轮到你接话了」。
有了语料,马上撞上第一个问题:模型不认识汉字,它只认识数字。所以第一步必须把句子切成小块,再给每块发一个编号。这个「切」的动作, 专业叫法就是分词;切开后的一块,叫一个标记(token)。
最省事的刀法:一个字切一块。不需要任何词典,规则一句话讲完。 拿第 1 站那道题试一刀:
切完把 4,000 条语料全剁碎、倒进一个大筐里数一数: 一共只有 20 种不同的块。 这就是我们手上最小的一个词表——小到可以整个背下来。
很自然的下一个念头:「一百一十五」明明是一个整体,那就把它当成一块。
于是我们需要一本词典,里面写满「哪些字组合在一起算一个词」。 对这套算术语料,词典其实好写得很:把 0~999 的读法一条条列进去,再加上加、减、乘、除以、等于、?——一共1006 条。
十五 | 加 | 一百一十五 | 等于 | ? | 一百三效果好得惊人:14 块变成了 6 块, 全语料平均一条只要 6.00 块(按字要 12.82 块)。 而且每一块都是有意义的完整词。
但代价立刻显形。试试词典外的世界——比如结果超过 999(语料里每一道题的结果都 ≤ 999, 所以这种数它压根没见过):
一千零二十四减五等于?一千零一十九
词典里只有「一」和「千」两个字,没有「一千」这个词。 最大匹配切到这里就卡住,只能给它打上一个标记:[UNK](unknown,不认识)—— 这句话会被切成 13 块,其中 2 块是 [UNK],两块都是那个「千」字。词典有多全,模型的世界就有多大—— 词典外的一切,对模型来说都是同一个「我不知道」。
两份方案都不完美。有没有第三条路——不写词典,让机器自己决定怎么切?
有,而且规则简单到小学生也能自己推出来:谁和谁最常挨在一起,就把它俩粘成一块。粘完重新数,再粘下一对。重复 40 次,词表就自己长出来了。 这套办法叫 BPE(Byte Pair Encoding,字节对编码)。
| 候选相邻对 | 一起出现了几次 |
|---|---|
| 等+于→等于 | 4000 |
| 于+?→于? | 4000 |
| 除+以→除以 | 1000 |
| 二+十→二十 | 963 |
| 十+二→十二 | 891 |
跑完之后,它确实长出了「一百」「等于」「二十」这些像样的块。但它也长出两块怪东西:一块 加一百——把运算符「加」和数字「一百」粘在了一起; 还有一块 五等于?——把数字、等号和问号一起装了进去。 这些块跨过了语义的边界,因为「加」后面常常紧跟「一百」、而「?」总跟在「等于」后头。BPE 不懂语法,它只懂「谁挨着谁」。
三把刀摆在一起,各自的脾气就清楚了:
| 刀法 | 词表大小 | 平均一条切几块 | 这道题被切成什么 | 脾气 |
|---|---|---|---|---|
| 按字切 | 20 | 12.82 | 十 | 五 | 加 | 一 | 百 | 一 | 十 | 五 | 等 | 于 | ? | 一 | 百 | 三 | 词表极小,永远没有生词;但每块没有意义、句子长 |
| 按词切(最大匹配) | 1006 | 6.00 | 十五 | 加 | 一百一十五 | 等于 | ? | 一百三 | 块少、每块有意义;但词典要人写死,生词只能 UNK |
| BPE(合并 40 次) | 60 | 6.51 | 十 | 五 | 加一百 | 一十 | 五等于? | 一百 | 三 | 词表不大、生词也能拆成字兜底;但会切出跨边界的怪块 |
真实的大模型全都选了第三条路。Qwen2.5-7B 的词表里有 151,936 个 token: 既有「的」「一」这样的单字,也有「今天」「人工智能」这样的整词,还有些谁也读不出来的半截词。它们全是被这样一次次「粘」出来的。
用第一把刀(按字切),理由很实在:这套语料只有 20 个不同的字、 不可能出现生词,词表小到可以直接印在纸上给你看。等第 14 课真的训练算术小模型时, 这个选择还会再出现一次——那时候你会看到它够不够用。
还有一个连带的选择,现在就交代清楚,免得后面返工:数字只留汉字这一套写法。语料里永远是 十五加一百一十五等于?一百三 这样的汉字数字,词表里不会出现0~9 这十个字符。这样模型只需要学一套读法、一套进位规则, 不用把同一个数认两遍。
可真实用的时候,人打的是 123加456。解法是在模型外面加一层翻译: 喂进去之前先把阿拉伯数字转成汉字,模型吐出来的汉字答案再转回阿拉伯数字给人看。 转换规则就是第 1 站那台读数机已经讲过的那些(「零」要占位、「一十」要读成「十」)。对模型来说外面这层不存在,它永远只看见汉字。
刀法定了,接下来给每一块发座位号。 翻开这本册子:第 0 格是 ?,第 1 格是 一,…, 第 19 格是 零。点一下就能看到每个块的编号:
现在,把这几个名词一次钉死——它们的区别,很多人学了很久还在混:
最后一句要紧的话:编号只是座位号。
看编号:? 是 0 号,一 是 1 号,零 是 19 号。 按编号看,「零」比「一」大 18——可这两个编号之间,没有任何数学关系。 编号不表示大小、不表示顺序、不表示相似。它只是「第几格」。这本册子从头到尾,没有告诉模型「十五有多大」——一个字都没说。
有编号,最顺手的做法就来了:准备 20 个格子,第几号就在第几格写 1,其余写 0。比如 一 是 1 号,它的向量就是 [0, 1, 0, 0, …]——20 个数里只有一个 1。这种做法叫 one-hot(独热)。
点几行看看,你会立刻发现两个硬伤:
第 2 课我们辛辛苦苦把「像不像」变成可以算的距离。 可 one-hot 这张表里,所有人跟所有人的距离都是同一个数—— 距离算出来了,却什么都没告诉你。
换个思路:别用一个格子表示一块 token,用 8 个格子,而且每一格都填上有意义的数字。这就叫稠密表示,这样的一串数,就是我们第 2 课说的向量—— 在语言模型里,它有个专门的名字:词向量,也叫词嵌入(embedding)。
下面这张表,是刚才那 20 个字真正的向量——不是我们手工打的, 是模型自己学出来的(怎么学的,下一站讲)。颜色越琥珀越正,越蓝越负:
[151936, 3584] 的嵌入矩阵的迷你版: 这里是 【20 × 8】——行是 token,列是维度,查一个 token 的向量,就是查它那一行。点开「一」,看它最像的三个字;再点「二」。 你会发现:这些字自己聚成了一堆一堆—— 数字和数字近、运算符和运算符近。 没有人告诉过模型「二」和「三」应该像,这个「像」是它自己数出来的。
再用一个 3D 玩具,把「稀疏」和「稠密」的差别看得更狠一点。 假设词表里只有三个词,每个词只用 3 个格子:
于是整套流程终于接通了:文字 → 切成 token → 查编号 → 查嵌入矩阵的那一行 → 一串数字。从此模型面前不再有汉字,只有数字。
关键问题还没回答:「二」的向量凭什么是那几个小数?谁写的?
答案是:没人写,是它自己跑出来的。而且起步时它什么都不会——第 0 步的向量就是一堆随机数, 和瞎猜的水平一模一样(loss = 3.00)。 要让它长出本事,只需要给它一个游戏:猜下一个字。
① 猜:给模型看一个字,让它猜下一个字是谁。② 算分:猜错了就记一笔,叫 loss(损失)——数字越大表示越「不会」。③ 挪一点:把每个向量往「猜得更准」的方向微调一下。重复。
就这么简单。它没有背规则,只是在同一个游戏里玩 300,000 次。 下面的播放器,就是这台机器 300,000 步的全程录像(真跑出来的,不是动画):
现在回头看第 2 课那句「所有数字都是模型从海量文本里自己学出来的」—— 你刚刚亲眼看完了它的迷你版:从随机数开始,一路把 loss 压小,向量自己长成了有结构的样子。
至于 loss 曲线最后那一大段几乎是平的——2 万步时是 1.65, 又跑了 28 万步,也只压到 1.62, 中间还起起落落。多花 15 倍的时间,几乎没换来进步——先记着这个疑问,第 13 课「泛化与过拟合」会专门算这笔账:训练不是越久越好。
模型只认识数字,所以文字要经过一条流水线:语料 → 分词 → 词表编号 → 查嵌入矩阵 → 一串数字。 切分有三种刀法——按字(词表最小,但每块没意义)、按词(块最少最有意义,但要人写词典、生词就 UNK)、BPE(让机器数「谁最常挨着谁」,自己长出词表,真实大模型都用它)。 编号之后的第一个长相是 one-hot:一格亮 1,其余全 0——省事,但所有 token 互相「六亲不认」; 第二个长相是稠密词向量:每一格都有数,于是「像不像」终于变成了距离。 而这些数字不是谁写的,是模型在「猜下一个字」的游戏里一步步练出来的。