词语结构与子词模型
固定词表的假设
- 模型训练时会建立一个固定的词表(通常是几万词),每个词都会分配一个索引
- 在测试时,所有未在词表中出现的新词(novel words) 都会被映射到一个特殊的符号 UNK(unknown)
Embedding 的问题
- 词表内的词(如 hat, learn)有各自不同的 embedding
- 词表外的所有词(taaaaasty, laern, Transformerify)都只能共享 同一个 UNK embedding
- 这样会导致模型无法分辨 不同的新词、拼写错误或变体 —— 都被混为一个“未知词”
BPE 算法
核心思想
BPE 的目标是:
- 不再把整词作为最小单位,而是学到 子词单元(subword tokens)
- 把词表从「固定单词」变成「可以组合的子词」:这样就能减少 UNK(未知词) 问题
流程
- 初始化
- 一开始,词表只包含 单个字符(a, b, c, …)和一个 “end-of-word” 符号(比如 “_”)。
- 统计最常见的相邻字符对
- 在语料库里,找出出现频率最高的相邻字符对(比如 “a” 和 “b”)。
- 合并成新的子词
- 把 “ab” 加入到词表中,作为一个新的 subword。
- 替换文本中的字符对
- 在语料中把 “a b” 替换成新的 subword “ab”。
- 重复
- 不断重复步骤 2–4,直到达到 预设的词表大小(比如 30k、50k)
我的问题: BPE 后如何保证序列长度一致
- 如果太长的话, 进行截断/滑动窗口
- 如果太短的话, 用 pad 补齐
Embedding 发展历程
2017 前的预训练词嵌入
- 当时的做法(word2vec, GloVe 等):
- 先训练好一个词向量表(每个词对应一个向量,和上下文无关)
- 下游任务(QA、分类、翻译等)时,把句子里的每个词替换成对应的向量,再输入到 LSTM/Transformer 里
- 局限性:
- embedding 没有上下文
- 例如 “movie” 在任何句子里都是同一个向量
- 所以模型必须在 下游训练 时自己学会利用上下文
- 数据需求很大
- 下游任务的数据必须足够大,才能学出“语境依赖”
- 但很多任务的数据其实很小(比如医学、法律 NLP)
- 参数大多随机初始化
- 除了词向量是预训练的,其余网络层(LSTM/Transformer)都是随机的,训练难度大
- embedding 没有上下文
现代 nlp 的做法
现代 NLP 的做法
- 整个模型都预训练 (pretraining whole models):
- 不仅 embedding,连 Transformer 的多层参数(self-attention, FFN)也都在海量无监督语料上训练好
- 预训练目标通常是 遮盖部分输入 (masking / language modeling),让模型学会重建
预训练的优势
- 更强的语言表示 (representations of language)
- 模型已经学会了语义、语法、上下文依赖。
- 更好的参数初始化
- 下游任务只需要微调 (fine-tuning),不再从随机参数开始
- 这大大减少了下游数据需求
- 概率分布 (probability distribution over language)
- 模型本质上学到了一个语言模型,可以生成、预测、补全文本
预训练
语言模型预训练
-
语言建模任务 (Language Modeling Task)
目标是建模:$$ pθ(wt∣w_{1:t−1}) $$即在已知前文的情况下,预测下一个词的概率分布 -
优点
- 有大量数据可以用(无监督,所有文本都能用)
- 模型可以是 LSTM, Transformer 等任意序列模型
-
做法
- 用大规模语料训练一个语言模型
- 保存训练好的参数
- 在下游任务中加载这些参数作为初始化
预训练+微调范式
现代 NLP 的核心范式:
Step 1: Pretrain (on language modeling)
- 用海量无监督语料,训练模型预测下一个词或填空(masked LM)
- 学到 通用的语言知识(语法、语义、常识)
Step 2: Finetune (on your task)
- 在下游任务(情感分析、QA、NER …)上,用少量标注数据进行微调
- 模型已经具备语言理解能力,所以只需要学一点点任务特定知识
为什么这种范式更有效?
-
预训练阶段
- 我们先在大规模语料上做预训练,优化预训练损失函数:
$$ \hat{\theta} \approx \arg \min_{\theta} \mathcal{L}_{\text{pretrain}}(\theta)$$ $$
- 得到参数 $\hat{\theta}$(这是一个“好”的起点)
- 我们先在大规模语料上做预训练,优化预训练损失函数:
-
微调阶段
- 在下游任务上,我们要优化:
$$ \theta^* = \arg \min_{\theta} \mathcal{L}_{\text{finetune}}(\theta)$$ $$
- 但是我们 不是从随机初始化开始,而是从 $\hat{\theta}$ 开始。
- 在下游任务上,我们要优化:
-
为什么有用?
- SGD 粘滞性 (stickiness):随机梯度下降往往会在初始点附近收敛 → 从 $\hat{\theta}$ 出发,就更可能收敛到一个“好的局部极小值”
- 局部极小值的质量:靠近 $\hat{\theta}$ 的解往往比随机初始化更优
- 梯度更合理:在 $\hat{\theta}$附近,finetune loss 的梯度通常更有意义,优化更快
总结
- 随机初始化 → finetune:难,容易收敛到糟糕的局部最小值,需要大量数据
- 预训练初始化 $\hat{\theta}$ → finetune:快,好,能在少量下游数据上表现优异
三种架构的预训练
Encoder only
这里讲的是Encoder 模型(比如 BERT)在预训练时用的目标函数,也就是 Masked Language Modeling (MLM)
背景
-
之前我们说过 语言模型预训练(GPT 的方式)是 单向的:
$p(w_t \mid w_{1:t-1})$
只能利用前文预测下一个词 -
但 Encoder (BERT) 是 双向的,即它能同时看到前后文
- 如果直接用 “预测下一个词” 任务,就破坏了双向性
解决方法:Masked LM (BERT 的核心)
- 做法
- 随机把输入序列中的一部分 token 替换为
[MASK]
- 模型的目标是预测这些被遮盖的 token 例子:
- 输入句子:
I went to the [MASK] to buy [MASK].
- 模型需要预测:
store
,tea
- 随机把输入序列中的一部分 token 替换为
- 数学表达
- 输入:$\tilde{x}$(被 mask 掉的序列)
- 学习目标:
$p_\theta(x \mid \tilde{x})$ - 只对被 mask 的位置计算 loss
- 好处
- 利用 双向上下文,每个词都能看见左右两边的信息
- 模型学到的 embedding 更适合理解语义,而不仅仅是预测下一个词
图解
- 左边:输入序列(
I
,[MASK]
,to
,the
,[MASK]
…) - 中间:Encoder(Transformer 编码器,双向 self-attention)
- 右边:输出隐藏状态 h1,…,hTh_1, …, h_T,用来预测 mask 的词
BERT
掩码语言建模
- BERT 的核心预训练目标是 Masked Language Modeling:
- 随机挑选 15% 的 token 来预测
- 具体规则:
- 80% 的时候 → 把词替换成
[MASK]
- 例子:
I like pizza
→I like [MASK]
- 例子:
- 10% 的时候 → 把词替换成一个随机词
- 例子:
I like pizza
→I like car
- 例子:
- 10% 的时候 → 保持原样,但仍然要求模型预测它
- 例子:
I like pizza
→I like pizza
- 例子:
- 80% 的时候 → 把词替换成
- 为什么要这样做?
- 如果总是
[MASK]
,模型会“偷懒”,只学会预测 mask - 引入随机词和原词,迫使模型对 非 mask 的词也要构建强语义表示
- 注意:在微调阶段不会出现
[MASK]
,所以这种训练方式避免了 pretrain-finetune gap
- 如果总是
下一句预测 NSP
- 除了 MLM,BERT 还设计了 下一个句子预测 (Next Sentence Prediction, NSP):
- 输入:两个文本片段 A 和 B
- 任务:预测 B 是否是 A 的真实后续句子
- 50% 的情况 B 是 A 的后续
- 50% 的情况 B 是随机抽的
- BERT 用了 segment embeddings 来区分 A 和 B
- 目的
- 让模型学会理解句子之间的关系(不仅是单句的词级语义)
- 对 QA、自然语言推理 (NLI) 等任务有帮助
- 后来研究发现
- NSP 的效果有限甚至可能负作用(RoBERTa 就去掉了 NSP)
- 但 MLM 仍然是 BERT 的核心训练目标
为什么 NSP 没用?
NSP 的设计初衷
BERT 作者 (Devlin et al., 2018) 认为:
- 语言理解不仅需要词级别的上下文,还需要句子间的关系
- 所以加了 NSP 任务:判断句子 B 是否是句子 A 的真实后续
NSP 的问题
- 任务过于简单
- NSP 只是判断 “B 是否是 A 的后续”,很多时候模型可以依赖 表面线索 来解题,比如:
- 标点符号
- 主题切换(topic shift)
- 并不需要真正理解语义
- NSP 只是判断 “B 是否是 A 的后续”,很多时候模型可以依赖 表面线索 来解题,比如:
- 和下游任务差距大
- 很多下游任务(QA、NLI、NER)并不是在做“是不是后一句”的判断
- NSP 学到的表示对这些任务 迁移价值有限
- 可能引入噪声
- NSP 里“负样本”是随机拼接的句子,这些随机负例和真实任务的负例分布不一样
- 这种 训练–推理分布差异 反而会影响效果
- 消耗了训练资源
- NSP 占用了模型一部分学习能力,但收益不明显
- RoBERTa 的结论是:如果把 NSP 去掉,把计算力都用在 MLM 上 → 表示效果更强
全参数微调和部分参数微调
Full Finetuning
-
做法:下游任务时,把 预训练模型的全部参数(比如 Transformer 的每一层权重)都拿出来更新
-
优点:
- 灵活,可以完全适配目标任务
- 通常能达到最高的下游任务性能
-
缺点:
- 参数量巨大,显存和算力消耗很高
- 对小数据集容易 过拟合
- 需要为每个下游任务存一整份模型参数(非常浪费)
Lightweight / Parameter-Efficient Finetuning (PEFT)
- 做法:冻结大部分预训练参数,只训练 很少的新参数或已有参数的一部分
- 比如:
-
Adapter:在每层插入一个小网络,只训练这个小网络的参数
-
LoRA (Low-Rank Adaptation):对权重矩阵做低秩分解,只训练低秩矩阵
-
Prefix-tuning / Prompt-tuning:训练一些额外的 token embedding,不改动主体模型
-
- 比如:
- 优点:
- 训练 高效、低成本
- 不容易过拟合(因为主体模型冻结)
- 每个任务只需要存储一小份新参数
- 缺点:
- 灵活性比 full finetuning 差
- 在一些复杂任务上可能效果稍逊
Prefix-Tuning / Prompt-Tuning
- 核心:不改动预训练模型的主体参数(全部冻结),而是 在输入前拼接一些可学习的向量(prefix 或 prompt)
- 这些 learnable prefix 会被模型当作额外的“虚拟 token”处理,就像真的输入文本一样进入 Transformer。
过程
- 冻结大模型:预训练好的 Transformer / LSTM / ++ 不动
- 加入 prefix/prompt:
- 在输入序列前面加一段 learnable 向量
- 这段 prefix 可以理解为“任务提示 + 可学习参数”
- 训练时:只更新 prefix 参数,不更新模型主体
- 推理时:prefix 作为上下文 + 任务适配器,帮助模型表现出对应的任务行为
优点
- 极高参数效率:比如 GPT-3 175B 参数,prefix 只需几十万或几百万参数即可适配新任务
- 不破坏原模型:主体保持 frozen,避免遗忘原有知识
- 多任务共存:不同任务可以有不同的 prefix,在推理时选择性加载
区分 Prompt-tuning 和 Prefix-tuning
- Prompt-tuning:在输入 embedding 层前加可学习 token(更像“虚拟单词”)
- Prefix-tuning:不仅在 embedding 层,还在每一层的 attention key/value 前加 prefix
- Prefix-tuning 相当于 更深层控制模型注意力,效果通常比 prompt-tuning 稍好
LoRA 的核心思想
- 在 参数矩阵 W 上,不是直接更新 W(太大),而是只学习一个 低秩“差分”矩阵
- 数学形式:
- $W^′=W+AB$
- $W∈R^{d×d}$:预训练好的大矩阵(冻结,不更新)。
- $A∈R^{d×k},B∈R^{k×d}$:新增的低秩分解矩阵,秩 $k≪d$
- 只训练 A 和 B,参数量大幅减少
为什么好?
- 参数效率高:
- 如果$d=4096$,全矩阵有 ~16M 参数
- 设$k=8$,LoRA 只需 $2×4096×8≈65k$ 参数,缩小数百倍
- 兼容性强:
- 训练时只更新 $AB$,推理时合并 $W+AB$,无额外推理开销
- 容易训练:
- 比 Prefix-tuning 还稳定,因为它直接作用在权重矩阵空间里
- 多任务方便:
- 不同任务只需保存不同的 $AB$,不必重复保存整个模型
Encoder - Decoder
问题背景
-
Encoder-only 模型(BERT) → 用 Masked LM(随机 mask 单个 token)
-
Decoder-only 模型(GPT) → 用 Causal LM(预测下一个 token)
-
Encoder–Decoder 模型(T5) → 需要一个 更适合 seq2seq 的预训练目标
Span Corruption(片段掩码)
T5 发现:
- 单词级别的随机 mask(BERT 的 MLM)效果一般
- 更好的做法是 mask 掉连续的片段(span),再让模型去恢复
例子
原句:
Thank you for inviting me to your party last week.
处理后(输入给 encoder):
Thank you <X> me to your party <Y> week.
目标输出(decoder 需要生成):
<X> for inviting <Y> last <Z>
特点:
- mask 的单位是 片段 (span),而不是单词
- 每个 span 对应一个 唯一的占位符 token(< X >, < Y >, < Z >)
- Decoder 需要顺序生成这些被掩盖的内容
为什么有效?
- 比单词级别更贴近自然语言
- 人类语言中被删除或缺失的部分往往是 词组/片段,而不是单个词
- 比如 “for inviting” 整块缺失,更接近翻译/摘要等任务
- 更适合 Encoder–Decoder 结构
- Encoder 输入是被挖空的句子
- Decoder 生成的是 缺失内容,自然形成了一个 seq2seq 任务
- 保持语言建模特性
- Decoder 还是在做语言建模,只不过目标是“补全缺失片段”
Decoder only
Decoder-only 预训练思想
- 核心目标:自回归语言建模
$p(wt∣w_{1:t−1})$
也就是预测下一个词 - 预训练产物:得到能生成自然语言的 decoder
在下游任务(比如分类)时,可以在最后一个 hidden state 上接一个线性分类器(A、b 随机初始化),再一起 finetune
GPT-1 (2018)
- 提出 Generative Pretrained Transformer
- 流程:
- 大规模无监督预训练(语言建模)
- 下游任务小规模有监督微调
- 输入格式:把任务改写成文本串输入 decoder,比如 NLI 就把 premise 和 hypothesis 拼接
GPT-2 (2019)
- 参数规模提升到 1.5B
- 发现:只靠预训练(不微调)也能在很多任务上直接生成“像样”的答案
- 现象:生成的文本越来越连贯
- 关键转变:少/零样本任务能力出现萌芽
GPT-3 (2020)
- 参数量直接上到 175B
- 关键发现:
- In-context learning:模型只需在输入里看到几个例子(few-shot),就能学会对应任务
- 不需要梯度更新,只靠 prompt 里的示例就能完成新任务
- 意义:模型参数本身学到的是通用分布,prompt 决定如何利用它
Scaling Efficiency
- 背景:GPT-3 有 175B 参数,用了 300B tokens 训练
- 问题:成本大致和
参数量 × tokens
成正比。GPT-3 的训练点并不是最优(太“胖”参数多,token 不够) - 对比表:
-
LaMDA:137B 参数 / 168B tokens
-
GPT-3:175B / 300B
-
Jurassic:178B / 300B
-
Gopher:280B / 300B
-
MT-NLG:530B / 270B
-
Chinchilla:70B / 1.4T tokens ✅(小模型 + 大量数据 = 更高效,效果更优)
-
- 结论:
GPT-3 并不是最佳的参数-数据比例。后续 Chinchilla scaling law 证明:更多 tokens、相对少一些参数,能得到更好的效果和泛化
Chain-of-Thought Prompting
- 普通 Prompting:直接问 → 模型只给出答案(往往错,比如 27)
- CoT Prompting:要求“逐步推理” → 模型先写推理过程,再得出答案
- 原理:
- 相当于在 prefix 里提供了思维链条范例,模型学会模仿“逐步推理”的分布
- 让大模型用“scratch pad”(草稿纸)去展开逻辑,而不是直接跳到结果
- 意义:
- 极大提升模型在算术推理、符号推理、逻辑任务上的表现
- 是 In-context learning 的一个典型 case