BERT 与预训练范式

上一章的 Transformer 要为每个任务从零训练、还得喂大量配对数据;能不能先让模型读懂人话,再去做具体任务?

知识课 · 约 30 分钟 · 更新于 2026-08-14

一个昂贵的习惯:为每个任务从零训练

在「Transformer 实战」里,你已经从零搭出并训练过一个完整的 encoder–decoder Transformer,让它学会把一种日期格式翻译成另一种。它确实能用,但有两个绕不开的代价:

  • 任务专用 :它只会翻译日期。换一个任务——判断影评好坏、识别句子里的人名——就得改结构、从随机初始化重新训练一遍。
  • 依赖配对数据 :训练它需要大量「源句 — 目标句」的 配对 样本。日期翻译的样本能靠程序批量合成,是因为转换规则完全已知;可「影评是好评还是差评」这类真实任务的标签没有规则可循,只能靠人工一条条标,配对数据因此稀少又昂贵,每个新任务都要重新标注一批。

与此同时,另一种数据却近乎无限——网页、书籍、维基百科里的 纯文本 ,没有任何人工标注,却堆积如山。 一个自然的问题浮现出来:这笔巨大的免费资源,能不能用起来,让模型先「读懂人话」,再去做具体任务?

预训练 — 微调:一次学通用,处处去复用

这个问题的答案,重塑了整个领域。它把训练拆成了两个阶段:

  • 预训练(pre-training) :先在海量无标注文本上做一次大规模训练,让模型学到通用的语言知识——词义、语法、搭配、常识。这一步只做一次,成本高,但一劳永逸。
  • 微调(fine-tuning) :拿预训练好的模型作底座,再用 少量 标注数据,针对某个具体任务做一次轻量训练;这类具体任务叫 下游任务(downstream task) 。每个新任务都从这个「读过书」的底座出发,而不是从一张白纸开始。

这就是 预训练 — 微调范式 。它把「通用语言能力」与「具体任务」解耦:通用能力被预训练一次性吸收并反复复用,单个任务只需付出很小的额外代价。这种把已学到的能力复用到新任务的做法,在机器学习里统称 迁移学习(transfer learning) 。这一范式是现代大语言模型时代的核心起点。

下图把两个阶段放在一起。左边的大规模训练只发生一次,右边每个下游任务各自做一次轻量微调,用的都是同一个底座。

为什么预训练不需要人工标注

预训练要在「无标注」文本上训练。可训练总得有标签——模型预测什么、拿什么对答案?关键的转折在这里: 标签不必由人来标,它本就藏在文本自身里 。

做法是把文本的一部分遮住,让模型根据剩下的部分把它 预测 出来;预测对不对,直接拿原文一比就知道。 原文同时充当了「题目」和「标准答案」,不需要任何额外人工。这种 标签来自数据自身 的训练方式叫 自监督(self-supervised)学习 。

这套思路你在 word2vec 里已经见过,用上下文预测词,训练信号同样来自文本自身。区别在于产出,word2vec 预训练出的只是一张静态的词向量表,而这里要让 整个模型 在预训练中吸收语言知识。

正因为标签是自动生成的,互联网上几乎无限的文本就都成了可用的训练数据——这是预训练能「吃下」海量语料的根本原因。

分岔路口:编码器还是解码器

完整的 Transformer 由编码器和解码器两半组成。一个重要的观察是: 这两半各自都能独当一面 ,而多数任务只需要其中一种能力,理解类任务用不到逐词生成,生成类任务也不必再单配一个编码器,留着另一半是多余开销。于是现代模型大多只取其一:

本课先走编码器这条路线,看 BERT 如何把「双向理解 + 预训练微调」做到极致;解码器路线留到「GPT 与生成式预训练」一课展开,它催生的现代大模型再往后。

BERT 就是「只保留编码器的 Transformer」

BERT (Bidirectional Encoder Representations from Transformers)的骨架,正是你在「编码器」一课里亲手搭过的那个东西——多头自注意力 + 前馈网络(FFN)+ 残差与归一化(Add & Norm),层层堆叠。它 没有 解码器,也 没有 交叉注意力。

换句话说,BERT 的 架构 你已经会了。所以本课不再重讲注意力、FFN 这些零件,只聚焦它真正 新增 的东西:怎么给一个纯编码器设计预训练目标,以及输入要怎么组织。

规模上,最常用的 BERT-base 约有 1.1 亿个参数。这个数字在当时已属庞大,但相比之下,后面会讲到的 LLaMA、Qwen 这类现代大模型动辄数十亿乃至上千亿参数——记住 BERT 这个量级,方便日后建立对「大」的尺度感。

原始论文: BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 。

一个矛盾:双向编码器没法「预测下一个词」

要给编码器设计一个自监督目标,最容易想到的是照搬解码器的做法—— 预测下一个词 :给出前文,猜下一个;这种按前文预测下一个词的模型,是经典的 语言模型(language model) 。可这个目标在双向编码器上行不通。

沿用「掩码注意力」的办法,给编码器也加上因果掩码,确实能挡住答案,但每个词就只剩左侧上下文可看,双向理解的优势随之丢掉,等于退回解码器那条路。所以双向编码器需要一个 不会泄露答案 、又不牺牲双向上下文的训练目标。BERT 的解法,是换一种「考法」。

掩码语言模型:像完形填空一样训练

BERT 把目标换成了 完形填空 :随机 选中 句子里约 15% 的词作为预测目标,把它们盖住(大多换成一个特殊记号 [MASK] ,具体替换规则见下一节),再让模型 同时利用左右两侧 的上下文,把被盖住的词猜回来。这里的「掩码」动作发生在 输入 上,是把词换成 [MASK] 记号;注意力本身不加因果遮挡,仍是全双向的,和「掩码注意力」里改注意力矩阵是两回事。

这个目标叫 掩码语言模型(Masked Language Model,MLM) 。它的巧妙之处在于:被预测的词已经被盖住,答案不再泄露;而要猜准它,模型必须充分调动双向上下文——既不浪费编码器「能看全句」的优势,又构造出了一个无需人工标注的自监督任务。

猜词并不需要解码器。编码器每个位置的最终向量都接同一个映射到词表的输出层,得到该处词的概率分布,这和「训练与推理通路」里见过的输出层是同一回事,只是不必逐词生成。训练时只把被选中位置的预测与原词算交叉熵、计入损失,其余位置的预测直接忽略;哪些位置计入损失,由训练代码按选中名单筛选,模型不需要自己判断。

原句: 巴黎 是 法国 的 首都 遮罩后: 巴黎 是 [MASK] 的 首都 ← 喂给模型的输入 目标: [MASK] 处应当预测出「法国」 ← 标签,由原句自动得到

双向编码器抄答案的矛盾,和完形填空这个解法,可以放到同一个例句上对照。点击词块更换要预测的词,再切换三种训练目标,看看模型此时能看见哪些词,答案在不在可见范围里。

示意里按「词」为单位遮盖;实际操作以分词器切出的 token 为单位,粒度可能更细,后面动手环节的中文模型就是按字来遮的。

80 / 10 / 10:被选中的词并不都换成 [MASK]

这里有一个容易忽略、却很关键的细节。被选中的那 15% 的词, 并非 全部替换成 [MASK] ,而是按下面的比例分三种处理:

无论哪种处理,模型都要在这些位置预测出 原本的词 。为什么要这么折腾?根源在于一个 训练与使用不一致 的问题:

掺入「随机词」和「保持原词」正是为了消除这种依赖。换成 [MASK] 的位置模型自然认得出,关键在另外两成:一个看起来正常的词,可能是原词,也可能是随机换上来的,模型分辨不了。于是对 每一个 普通词,它都得保留从上下文推断「这个词可不可信、该是什么」的能力,而不是只在 [MASK] 处工作。那 10% 保持原词的位置看似把答案摆在了输入里,但模型不知道这里可以照抄,仍得从上下文推断一遍,推断的结论是眼前这个词本身就是对的。这样学到的表示,迁移到真实任务时才稳。

输入怎么组装:特殊记号与两处不同

BERT 的输入在词序列基础上做了一些约定,输入向量的构成也和你之前见过的编码器有两处不同。

先是两个特殊记号:

  • [CLS] :加在序列 最前面 ,是整句的「代表位」。双向自注意力让它在每一层都能看到全句,而它本身不对应任何实际的词、不用承担自己的词义,正好专职汇聚整句信息。微调时通常拿它的最终向量当整句的汇总表示,在上面接一个映射到类别的小输出层,也就是分类头(「BERT 微调实战」里会这么用)。
  • [SEP] :加在句子 之间 和 结尾 ,用来分隔 / 标记句子边界。

再看输入向量的两处不同,一处新增、一处替换:

  • 段嵌入(segment embedding) :不少理解类任务的输入天然是两句话,比如判断两句是否同义、根据问题在一段话里找答案,BERT 把这样的两句拼成一条序列输入。段嵌入用一个额外的嵌入标明每个词属于句子 A 还是句子 B,帮模型分清「这是哪一句」。
  • 可学习位置编码 :在「位置编码」一课里用的是固定的正弦函数,它的两个优点在 BERT 这里都派不上用场:一是序列多长都能算,而 BERT 设计时就把最大输入长度定为 512 个 token(更长的输入会被截断),用不到这份延展性;二是无需训练,但 BERT 本来就要做大规模预训练,512 个位置向量顺带就学了,每个都能训练充分。于是 BERT 改用 从数据中学出来 的位置向量——每个位置对应一个可训练的嵌入,和词嵌入一样在训练中更新。

于是 BERT 每个位置的输入向量 = 词嵌入 + 段嵌入 + 位置嵌入,三者逐元素相加。至于分词,BERT 用的是 WordPiece(与你在「文本表示与词向量」里学过的 BPE 同属子词分词),这里只引用、不再展开。

下图用一条双句输入把这套组装完整摆出来。最上面一行是加好特殊记号的 token 序列,往下每一列就是一个位置的三种嵌入,逐元素相加得到该位置的输入向量。

关于 NSP,简单带过

原始 BERT 还配了第二个预训练任务—— 下一句预测(Next Sentence Prediction,NSP) :判断句子 B 是不是句子 A 在原文里的下一句。后来的研究(如 RoBERTa)发现它收益有限,之后的同类模型大多不再采用。知道有这回事即可,本课不展开。

这也解释了上一节输入组装的两个设定。原始 BERT 的一条预训练输入,就是拼接起来的两句话, [CLS] 在预训练阶段给 NSP 当分类位,段嵌入标出两句的边界。NSP 弃用后,这套输入约定保留了下来,段嵌入继续服务问答这类两句输入的下游任务, [CLS] 的整句汇总能力则主要在微调阶段练出来。

动手:亲手造出 MLM 训练样本

把上面的规则写成一个函数,看看自监督的标签是怎么从纯文本里「无中生有」的。下面对一条 token 序列,按 15% 选中、再按 80/10/10 施加替换:

只有被选中的位置 labels 才是原词、参与损失,其余位置标记成 -100 被忽略。「原句 → 遮罩后输入 → 需要预测的标签与位置」三者的对应关系,下面的演示会直接展示,你能看到标签完全来自文本自身,无需任何人工标注。

动手:让预训练好的 BERT 做完形填空

不必自己预训练(那需要海量算力)。社区已经把训练好的 BERT 权重公开了,直接下载来用。下面借助 Hugging Face 的 transformers 库,让一个中文 BERT 来填空:

两句话里的「苹果」一个指水果、一个指公司,模型在 [MASK] 处给出的预测也完全不同,这正是「双向上下文决定词义」的直接体现。这种 上下文相关(contextual) 的表示,你在「Transformer 实战」结尾的一词多义检验里已经见过,只是那发生在玩具任务的小模型上;BERT 把它带到了海量真实语料的尺度,任何词放进任何语境,都能得到贴合语境的表示。「文本表示与词向量」里静态词向量表达不了一词多义的局限,就此有了通用的解法。

下面亲手验证这一点。例句的候选与概率全部来自 bert-base-chinese 的真实预测,点一个例句,看模型在 [MASK] 处给出的前 5 个候选和各自的概率。

读懂了语言,然后呢?

预训练好的 BERT 已经「读懂」了语言——但它此刻会的,只是 完形填空 。要让它去做判断影评好坏(情感分类)、识别句子里的人名(命名实体识别)这些 真实任务 ,还差一步:怎么把这身通用的理解能力,「接」到一个具体任务上?

这一步,叫微调,留给「BERT 微调实战」。