《大模型原理通俗讲》系列 08|预训练是什么:它是怎么“读书”的
08|预训练是什么:它是怎么“读书”的
《大模型原理通俗讲》系列第 8 篇,共 16 篇。不需要数学基础,会用大模型聊天就能读。前七篇讲的是:训练好的模型,是怎么工作的。从这一篇开始,换一个问题:这些数字,是怎么练出来的?
你问大模型:“法国的首都是哪里?”它答:“巴黎。”
可是,没有人把“法国首都是巴黎”这句话,一条一条敲进它里面。前面几篇我们看到,模型里只有数字:词向量表、各种变换表、输出表。那么,“巴黎排在概率表第一”这件事,是怎么发生的?
答案是:练出来的。 这个练习的过程,叫预训练。读完这一篇,你能回答三个问题:
- 预训练到底在练什么?
- “学习”在技术上,到底发生了什么?
- 练完之后,模型里装了什么,又没装什么?
一、海量“完形填空”
预训练做的事情,用一个比喻最容易说明:
一个学生,做了海量的完形填空题。 每道题:给你一段话的前半部分,让你猜下一个字。猜完,立刻对答案;猜错了,就订正;然后做下一题。
这个学生,就是我们的模型。题目有多少?海量。有一个很妙的地方:不需要有人出题,也不需要有人批改。 因为任何一段文字,本身就带着答案。

“法国的首都是巴黎”这 8 个字,就可以变成 7 道题:看到“法”,猜“国”;看到“法国”,猜“的”……看到“法国的首都是”,猜“巴”。答对最后几道,恰好就需要“法国首都是巴黎”这个知识。 这就是模型在“读书”的过程中,逐渐把知识“练进”数字里的原因。(图里按字来画是为了好看,真实模型是按第 02 篇的 token 来的。)
这种“用文本自己当答案”的练法,有个名字,叫自监督。它的好处是:互联网上、书里、代码里,有多少文字,就能免费造出多少题目。
二、整个流程是怎么走的

整个流程有五步:
- 取一段文字: 从海量文本里,截一段。
- 遮住下一个字: 让模型看前面的字,猜后面的。(文本自己就是答案。)
- 给出概率表: 模型照着前七篇的流程,算出“下一个字是谁”的概率表。
- 对答案: 看正确的那个字,在概率表里排多高。概率越低,扣分越多。
- 订正参数: 根据扣了多少分,把模型里的每个数字,微调一点点,让下次遇到同样的题时,正确的字概率更高一些。
然后回到第 1 步,换一段文字,再来一圈。这样的循环,要重复海量次。
这里有三个关键词,下面分别讲:对答案(损失)、订正(调参数)、一点点。
三、“对答案”:怎么给一道题打分
怎么衡量“猜得准不准”?很简单:看正确答案的那个字,被模型分到了多少概率。
- 正确字概率 0.9:模型很有把握,而且对了,扣分很少;
- 正确字概率 0.01:模型几乎完全没想到,扣分很多。
把这个扣分,用一个叫“损失”的数来表示(专业名字叫交叉熵,它其实就是 “−ln 正确字的概率”,你不用记公式)。算出来大概是这样:
| 正确字被分到的概率 | 扣分(损失) |
|---|---|
| 0.90 | 0.11 |
| 0.50 | 0.69 |
| 0.10 | 2.30 |
| 0.01 | 4.61 |
可以看到:概率越低,扣分涨得越快。 把所有题的扣分,求个平均,就是整体的损失。
训练的全部目标,就是让这个“平均扣分”变小。 其他的,都是为了这件事服务。
四、“订正”:怎么知道该调哪个数字
模型里有成千上万(真实的大模型是上百亿、上千亿)个数字,这些数字叫参数。每次做完题,要从这么多数字里,找出“该调哪个、往哪边调、调多少”。
用一个比喻:蒙着眼睛下山。
你想走到山谷(扣分最低的地方),但你蒙着眼。怎么办?每走一步前,先用脚探一探:哪个方向是下坡?然后朝下坡的方向,迈一小步。再探,再迈。这样重复很多步,就能慢慢走到谷底。
模型里每个数字,都是一个“方向”。有一套办法,能一次算出“每个数字往哪边调,扣分会变少”(叫反向传播,你只需要知道它存在,这是现代深度学习的标准做法)。然后,每个数字都朝着“扣分变少”的方向,微调一小步。这一小步有多大,也是人定的(叫学习率),太大容易“踩过头”,太小则走得太慢。
所以“一点点”很重要:每一圈只调一点点,但重复海量圈,就能从一个什么都不会的随机状态,走到一个“很会猜下一个字”的状态。
要说明白的是:这个“蒙眼下山”,只是帮助你理解“一点点往好的方向调”。真实的山不是一座山,而是有几百亿个维度的“山”,没人能直观想象。
五、动手:41 行代码,亲眼看一次“训练”
我们做一个迷你预训练(需要 numpy)。语料只有一小段话(48 道题),模型也很小:看到一个字,猜下一个字。它用的是前几篇出现过的零件:向量表、输出表、概率表。
它的起点,是随机数,什么都不会。 我们看看它怎么一步步变好。
# mini_train.py —— 迷你"预训练":从随机数出发,反复做"猜下一个字",一点点调整参数
import numpy as np
text = "法国的首都是巴黎。日本的首都是东京。法国的首都是巴黎。我们去公园玩。我们去海边玩。我们去公园散步。"
chars = sorted(set(text)); idx = {c: i for i, c in enumerate(chars)}; V = len(chars)
x = np.array([idx[a] for a in text[:-1]]) # 题目:看到这个字……
y = np.array([idx[b] for b in text[1:]]) # 答案:……下一个字是它(文本自己就是答案,不用人出题)
N = len(x)
rng = np.random.default_rng(0)
E = rng.normal(size=(V, 8)) * 0.1 # 向量表(第 03 篇):随机起点,什么都不知道
W = rng.normal(size=(8, V)) * 0.1 # 输出表(第 06 篇):同样是随机数
def predict(): # 每道题:向量 → 每个字的分数 → 概率表
z = E[x] @ W
p = np.exp(z - z.max(axis=1, keepdims=True))
return p / p.sum(axis=1, keepdims=True)
def table(c): # 看到字 c 时,模型认为下一个字是谁:只列出文中真的出现过的
p = predict()[x == idx[c]][0]
return {chars[j]: float(p[j]) for j in set(y[x == idx[c]])}
counts = np.zeros((V, V)); np.add.at(counts, (x, y), 1) # 对照:直接"数"出来的概率(第 01 篇的做法)
floor = -np.log(counts[x, y] / counts[x].sum(axis=1)).mean()
print(f"一共 {N} 道题,{V} 个不同的字。完全瞎猜的损失 = ln({V}) = {np.log(V):.2f};数出来的理论下限 = {floor:.2f}")
before = {c: table(c) for c in "是去"}
hist = [] # 记下每一步的损失
for step in range(1001):
p = predict()
loss = -np.log(p[np.arange(N), y]).mean() # 对答案:正确字的概率越低,扣分越多
hist.append(loss)
if step in (0, 10, 50, 100, 300, 1000): print(f"第 {step:>4} 步 损失 {loss:.2f}")
g = p.copy(); g[np.arange(N), y] -= 1; g /= N # 订正:每个旋钮该往哪拧("预测 - 答案",再一路传回去)
dE = np.zeros_like(E); np.add.at(dE, x, g @ W.T)
W -= 3.0 * (E[x].T @ g); E -= 3.0 * dE # 每次只拧一点点
after = {c: table(c) for c in "是去"}
for c in "是去":
print(f"\n看到『{c}』,下一个字是谁?")
for n in before[c]: print(f" {n} 训练前 {before[c][n]:.2f} 训练后 {after[c][n]:.2f} 数出来 {counts[idx[c], idx[n]] / counts[idx[c]].sum():.2f}")
运行结果(实测,随机种子固定):
一共 48 道题,23 个不同的字。完全瞎猜的损失 = ln(23) = 3.14;数出来的理论下限 = 0.21
第 0 步 损失 3.13
第 10 步 损失 2.53
第 50 步 损失 0.34
第 100 步 损失 0.22
第 300 步 损失 0.21
第 1000 步 损失 0.21
看到『是』,下一个字是谁?
巴 训练前 0.04 训练后 0.67 数出来 0.67
东 训练前 0.04 训练后 0.33 数出来 0.33
看到『去』,下一个字是谁?
海 训练前 0.05 训练后 0.33 数出来 0.33
公 训练前 0.05 训练后 0.67 数出来 0.67
画成图:

逐点看:
- 起点:损失 3.13。 一共有 23 个不同的字,完全瞎猜,每个字的概率都是 1/23,损失就是 ln(23) = 3.14。第 0 步的 3.13,说明它真的是“瞎猜”。
- 快速下降。 第 10 步 2.53,第 50 步 0.34,第 100 步 0.22。模型很快就从“瞎猜”变成了“猜得很准”。
- 走平,降不到 0。 后面几乎不动了,停在 0.21。为什么?因为有的题本来就有不止一个答案:比如“是”后面,既可能是“巴”,也可能是“东”。没有任何模型能把这样的题全答对,它能做到的最好,就是“按真实比例分配概率”。我用第 01 篇那样直接数出来的比例,算出这个理论下限,正好是 0.21,模型训练到最后,和这个下限几乎重合。
再看一眼训练前后,同一道题的答案:

- 训练前: 看到“是”,“巴”和“东”都只有 0.04,和瞎猜没区别;
- 训练后: “巴”0.67,“东”0.33;
- 数出来: 语料里“是”后面,“巴”出现 2 次,“东”1 次,也是 0.67 和 0.33。
训练的结果,和“数”出来的完全一致。 延申多想一下:
在这个迷你例子里,“训练”和第 01 篇的“数一数”,最后得到的是同一样东西。那为什么大模型不直接数?因为大模型看的上文很长,可能的前文组合多到根本没法一一数。只能让一组数字(参数)学会这个规律,再对没见过的上文做推断。这也是“训练出来”和“数出来”最根本的区别。
说明下:这段代码里,手搓了“订正”的计算,真实大模型的训练框架会自动完成,但原理一样。
六、真实的预训练,是什么样的
迷你版和真实版,思路完全一样,但规模差得很远:
- 数据: 真实的预训练使用海量文本,来源通常包括网页、书籍、论文、代码等。具体用了哪些,各家公开程度不一,可以网上查。数据通常还要经过清洗、去重等处理。数量通常以“万亿 token”这样的量级来说,具体数字多数不公开。
- 模型: 我们的迷你模型只有几百个数字;真实的大模型有几十亿到上千亿个参数(第 10 篇)。
- 上文: 迷你模型只看前一个字;真实模型看很长的上文,靠的是前七篇讲的整套结构。
- 时间和成本: 通常需要成规模的 GPU 集群,训练较长时间;具体因模型而异(第 10 篇)。
- 监控: 工程师盯的,就是我们上面画的“损失曲线”,只是它下降得慢得多,也“更抖、更不平稳”。
七、练完之后:学到了什么,没学到什么
学到了:
- 语法、词的用法、写作风格;
- 大量事实性的知识,比如“法国的首都是巴黎”(前提是训练数据里出现过,并且出现得足够多);
- 代码的写法、常见的推理套路,等等。
几点说明:
- 装进去的是“规律”,不是“原文”。 模型里没有一份互联网的存档,只有压缩过的统计规律。(不过,被反复见到的文字,也有可能被近乎背下来,这是研究者观察到的现象,这里不展开。)
- 好的坏的都学。 数据里有什么,它就学什么,包括偏见、错误和过时的信息。
- 有截止日期。 训练结束之后发生的事,它不知道。这也是第 13 篇“RAG”要解决的问题之一。
- 没有人逐条教过它。 人做的是:准备数据、设计训练目标、监控训练过程。具体学到了什么,不是人写进去的。
八、预训练完得到什么
预训练完成的模型,叫基础模型(也叫基座模型)。它的能力是续写:给它一段话,它接着往下写。
但这不等于“会听话”。你给它一个问题,它可能会把问题当成“一段话的开头”,接着续写成更多的问题,或者一篇文章,而不是给你一个回答。(这是常见现象,不同模型的表现不一样。)
从“会续写”变成“会聊天、会听话”,还要再练一次:微调与对齐。这是下一篇的内容。
九、读书这个比喻,到哪里为止
- 学生知道自己在学;模型只是被调数字。 它不“知道”自己在读书,也没有“理解了”的主观体验,它只是数字被调得越来越准。
- 学生有老师讲解;模型只有“对答案”。 没人给它讲“为什么”,只有“对了”和“错了多少”。
- 学生的书是有限的;模型的题是海量的。 而且每一圈只看其中一小部分。
- 学完后,大脑还会变;模型的数字是固定的。 训练结束,数字就定下来了;你平时和它聊天,数字不会因此改变。
十、三个常见误解
误解 1:“大模型把整个互联网存下来了,回答时是在里面查。”
不是。装进模型的是压缩后的规律(第 01 篇也说过,它不是在数据库里搜答案)。所以它既能回答没人问过的问题,也会出错。
误解 2:“有人把知识一条条教给它。”
没有。人不会逐条教“法国首都是巴黎”。人做的是准备数据和设计目标,剩下的是模型自己在“做题—订正”中练出来的。
误解 3:“训练的数据越多越好,训练得越久越好。”
不一定。数据的质量、种类的搭配、有没有重复,都会影响效果;训练也不是越久越好。这些是各家的工程细节,我不展开,只提醒你:“越多越好”这个说法太简单。
十一、本篇要点
- 预训练 = 海量“看前文、猜下一个字”。文本自己就是答案,不需要人出题、批改。
- 一圈 = 猜 → 对答案(算损失)→ 把每个数字微调一点点;重复海量次,损失下降,模型越猜越准。
- 练完得到的是固定的一组数字(基础模型):里面是规律,不是原文;它会续写,但还不一定会听话。
下一篇预告: 预训练出来的模型,只会“续写”。你问它问题,它可能给你续写出更多问题。怎么把它变成会听话、会聊天的助手?下一篇讲微调与对齐。
系列目录

文中的图、代码,欢迎自己动手改一改,比如把语料里再加几句话,或者把代码里两处步长 3.0 改成 30。(我试过:损失会变成 inf,训练直接崩掉,这就是“步子太大,踩过头”。)有哪里没讲明白,欢迎在评论区告诉我。
更多推荐

所有评论(0)