预训练

7,580 字 · 预计阅读 22 分钟

分词器准备好了,现在该造模型本身了。

这一节要从一堆随机数出发,训练出一个约 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%。降下来也是同样的道理:水温快合适时,就该轻轻地微调,大幅拧动反而会把调好的温度拧乱。

本文是会员内容

全文 7,580 字,已试读约 30%。开通专业版可阅读全文,并解锁全部官方课程与后续更新。

开通专业版

支持 7 天无理由退款 · 早鸟价 99 元/年(售完恢复365/年)

课程中涉及的第三方项目,其著作权与商标归各自权利人所有;引用处的署名与许可见《开源与来源说明》。