分词器准备好了,现在该造模型本身了。
这一节要从一堆随机数出发,训练出一个约 2675 万参数的语言模型。训练的目标只有一个:给它前面的文字,让它猜下一个 token。
猜了上亿次之后,它会慢慢学会中文的字词搭配、句子结构,能顺着你给的开头往下写。这个阶段叫预训练。
学完这一节,你能做到:写出一个完整的预训练程序,看懂训练日志里的每一个数字,并且知道在 GPU、Mac 和纯 CPU 上分别该怎么跑。
一、预训练在做什么
模型的输出是"下一个 token 的概率"。预训练就是拿大量真实文本,一遍遍地让模型做这道题:
输入: 人 工 智 能 是 一 门
目标: 工 智 能 是 一 门 学同一段文本里,每个位置都是一道题:看到"人",猜"工";看到"人工",猜"智"……一段 512 个 token 的文本,一次就能出 511 道题。
为什么是 511 道,不是 512 道?每道题的答案是"下一个 token",而最后一个 token 后面已经没有字了,出不了题,所以是 512 − 1 = 511 道。
这 511 道题是同时做的,不用一道一道来。第一节讲过,注意力只能看前面:模型站在"能"这个位置猜下一个字时,只看得到"人工智能",看不到后面的"是",所以不会偷看到答案。
拿上面这句话画出来,就是一级级的台阶:

所以训练时,输入和标签其实是同一段文字,只是错开了一位。Transformers 的模型内部会自动做这个错位,代码里直接把同一段 token 既当输入、又当标签就行。
预训练不需要任何人工标注,有文本就行,这也是它能用上海量数据的原因。
人工标注,就是请人给每条数据写上标准答案,比如给一万张照片逐张写上"猫"或"狗"。又贵又慢,数据量很难做大。
预训练的题目却自带答案:答案就是原文里的下一个 token。上面说的"标签",指的就是这个标准答案。
就像拿课本出填空题,把后半句遮住,翻开课本就能对答案。课本有多少,题就有多少。
二、准备数据
我们用上一节合并好的语料 data/pretrain.jsonl,约 1GB。用刚训练好的分词器切分后,大约是 1.9 亿个 token。
处理方法是:把所有文本分词后首尾相接,文档之间插一个 <|endoftext|>,存成一个大的二进制文件。训练时从里面随机截取一段段 512 个 token 的片段。
文档1的 token … <|endoftext|> 文档2的 token … <|endoftext|> 文档3 …
└──── 随机截一段 512 个 token ────┘为什么要首尾相接,不一篇文档当一条?因为文档有长有短:有的只有几十个 token,有的有好几千个。
一篇一条的话,短的要补齐到 512,补上去的部分白算;长的超过 512,又要把后面截掉。接成一长条再截,每一段都是满满 512 个真实的 token,一点不浪费:

随机截出来的一段,可能跨过两篇文档。中间的 <|endoftext|> 就是分界线:模型看到它,就知道后面是新的一篇,不必和前文接上。
每个 token 编号用 2 字节存储就够了,因为我们的词表只有 8192,最大编号不超过 65535。1.9 亿个 token,文件约 380MB。
1 个字节能表示 0 到 255,一共 256 个数,也就是上一节说的"256 个字节"。2 个字节拼起来,就有 256 × 256 = 65536 种组合,能表示 0 到 65535。
我们的编号最大是 8191,放得下。千问的词表有 24.8 万个 token,编号会超过 65535,2 个字节就不够用了。
文件大小也是这么算的:1.9 亿个 token × 每个 2 字节 = 3.8 亿字节,约 380MB。
所谓二进制文件,就是直接把这些数字按字节存进去,不像 .jsonl 那样存成人能读的文字。用记事本打开,只会看到一堆乱码。
训练时用内存映射的方式读,不用一次全部载入内存。
内存映射可以这样理解:看一本很厚的电子书,阅读器不会把整本书一次装进内存,你翻到哪页,它才去硬盘上取哪页。
训练时每一步只取其中一小批片段,用不着把 380MB 全部读进来。语料再大几十倍,这个办法照样能用。
三、定义模型
我们不从零手写 Transformer,而是借用 Transformers 库里 Qwen3 的模型结构,按自己的尺寸填配置、随机初始化。
这样做有两个好处:结构和真实的千问模型一样,后面遇到的概念都能对上;训练好的模型能直接用 Transformers 加载、生成,不用自己写推理代码。
先分清两个名字:Transformer 是模型的结构;Transformers(多一个 s)是 Hugging Face 做的 Python 库,里面收录了 Qwen3 在内的许多模型结构。
还要注意,借的只是结构,不是千问学到的本事。结构就像房子的图纸:几层楼、每层几个房间、房间之间怎么连。千问训练好的权重,是房子里摆好的家具。
我们照着同一张图纸,盖一栋小一号的房子,千问的家具一件也不搬,屋里先随手乱摆一通:所有权重都填上随机数,这就是"随机初始化"。所以这个模型一开始什么都不会,全靠下面的训练一点点学。
| 配置 | 默认版 | 缩小版 --tiny |
|---|---|---|
| 每个 token 的维度 | 512 | 256 |
| 层数 | 8 | 4 |
| Query 头 / KV 头 | 8 / 2 | 4 / 2 |
| 前馈网络中间层 | 1408 | 704 |
| 参数量 | 约 26.7M | 约 5.0M |
默认版的参数量可以把词嵌入和各层矩阵逐项相加验证。缩小版是给只有 CPU 的电脑准备的,用来快速走通流程。
表里的 M 是英文 million(百万)的缩写,26.7M 就是 2670 多万。后面日志里的 20.3M、5.0M 也这么读。
默认版的 2675 万,第一节的"权重"部分逐项算过:词嵌入 8192 × 512 约 419 万,8 层合计约 2255 万。
缩小版照着算:每个 token 的维度减半,大部分矩阵的长和宽都减半,只剩四分之一;层数也减半,各层合计从约 2255 万降到约 295 万,差不多是八分之一。
词表还是 8192,词嵌入 8192 × 256 只减了一半,约 210 万。加起来约 505 万,就是表里的 5.0M。
四、训练前先认识几个设置
训练程序里有几个设置,每一个都对应训练中的一个概念。先把它们认一遍,后面看代码时,就只剩"这一行在做什么"了。
1. 梯度和反向传播:往回找原因
第一节讲过,训练就是一点点调整权重。告诉每个权重该往哪个方向调、调多少的信号,叫梯度。
反向传播是算梯度的方法。损失算出来以后,程序从输出往回,一层一层地推:每个权重稍微变大一点,损失会变大还是变小、变多少。因为是从结果往回推,所以叫"反向",就像考完试从错题往回找,是哪一步没学会。
2. 一批:一次做好几段
"一批"就是一次拿好几段文本一起做,默认是 32 段。做完这一批,才统一调一次权重。
为什么不一段一段地调?只看一段,调整的方向很容易被这一段带偏;把 32 段的梯度平均一下,方向更靠谱。显卡也正好擅长同时算很多段。
3. 学习率:每一步迈多大
梯度说了往哪调、调多少,学习率决定的是:照着这个信号,这一步迈多大。
可以想象调热水器的水温。拧得太猛,一会儿烫一会儿冰,永远调不准;拧得太轻,半天也调不到合适的温度。
我们的程序里,最大学习率是 1e-3。这是科学计数法,表示 1 × 10 的 −3 次方,也就是 0.001。
学习率不是一直不变的。先用总步数的 5% 把它从零升到最大值,这叫预热,能避免随机初始化的模型一开始就被过大的更新"冲坏"。
预热就是刚打开热水器的那一下:还不知道该往哪边拧,先轻轻试,摸清方向再放开手脚。随机初始化的模型也一样,一开始算出的梯度很乱,迈大步容易一脚迈歪。
预热之后,再按余弦曲线慢慢降到最大值的 10%。降下来也是同样的道理:水温快合适时,就该轻轻地微调,大幅拧动反而会把调好的温度拧乱。