NLP 基础与预训练语言模型¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
学习目标:理解 NLP 的发展脉络、文本表示演进、以及预训练语言模型三大架构( Encoder-only 、 Decoder-only 、 Encoder-Decoder ),为深入学习大语言模型打下坚实基础。
📌 定位说明:本章覆盖从传统 NLP 到预训练语言模型( PLM )的完整知识体系。对标 happy-LLM Ch1 ( NLP 基础概念)和 Ch3 (预训练语言模型),我们的内容更系统、代码更完整、对比更深入。
目录¶
- 1. NLP 发展全景
- 2. 文本表示:从 One-hot 到 Contextual
- 3. 经典 NLP 任务与方法
- 4. 预训练语言模型的诞生
- 5. Encoder-only 架构: BERT 系列
- 6. Decoder-only 架构: GPT 系列
- 7. Encoder-Decoder 架构: T5/BART
- 8. 三种架构深度对比
- 9. 从 PLM 到 LLM :范式转变
- 10. 代码实战: PLM 三架构对比实验
1. NLP 发展全景¶
1.1 NLP 发展的四个阶段¶
时间线:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
~2013 2013-2018 2018-2022 2022~
规则与统计时代 词向量时代 预训练模型时代 大模型时代
专家系统 Word2Vec BERT GPT-4
HMM/CRF GloVe GPT-2 LLaMA
n-gram LM ELMo T5/BART Claude
SVM分类 Attention XLNet Gemini
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一阶段:规则与统计方法(~2013 )
- 特点:人工设计规则和特征,统计模型学习参数
- 代表方法:
- 正则表达式、语法规则
- 隐马尔可夫模型( HMM )用于词性标注、命名实体识别
- 条件随机场( CRF )用于序列标注
- n-gram 语言模型
- TF-IDF + SVM/朴素贝叶斯用于文本分类
- 局限:依赖人工特征工程,泛化能力差
第二阶段:词向量与深度学习( 2013-2018 )
- 特点:自动从数据中学习特征表示
- 代表方法:
- Word2Vec ( 2013 ):分布式词向量
- GloVe ( 2014 ):全局词向量
- TextCNN ( 2014 ): CNN 用于文本分类
- Seq2Seq + Attention ( 2015-2017 )
- ELMo ( 2018 ):上下文相关词向量
第三阶段:预训练语言模型( 2018-2022 )
- 特点:大规模预训练 + 微调( Pre-train & Fine-tune )
- 代表模型: BERT 、 GPT-2 、 T5 、 BART 、 RoBERTa
- 核心范式:先在大量无标注文本上预训练,再在下游任务上微调
第四阶段:大语言模型( 2022~)
- 特点:规模法则( Scaling Laws )、涌现能力( Emergent Abilities )
- 代表模型: GPT-4 、 Claude 、 LLaMA 、 Gemini
- 核心范式:预训练 → 指令微调 → RLHF → 提示工程
1.2 NLP 任务分类体系¶
NLP任务全景图
├── 文本分类
│ ├── 情感分析 (正面/负面/中性)
│ ├── 主题分类 (新闻类别)
│ ├── 意图识别 (对话系统)
│ └── 垃圾邮件检测
├── 序列标注
│ ├── 命名实体识别 NER (人名/地名/机构名)
│ ├── 词性标注 POS Tagging
│ └── 中文分词
├── 文本生成
│ ├── 机器翻译
│ ├── 文本摘要 (抽取式/生成式)
│ ├── 对话生成
│ └── 文本续写
├── 信息抽取
│ ├── 关系抽取
│ ├── 事件抽取
│ └── 三元组抽取 (主/谓/宾)
├── 文本匹配
│ ├── 语义相似度
│ ├── 自然语言推理 NLI
│ └── 问答匹配
└── 问答系统
├── 抽取式问答 (SQuAD)
├── 生成式问答
└── 知识库问答 KBQA
2. 文本表示:从 One-hot 到 Contextual¶
文本表示是 NLP 的基石——如何把人类语言转化为计算机可处理的数值向量。
2.1 One-hot 编码¶
最简单的文本表示:每个词用一个只有一个 1 其余全 0 的向量表示。
import numpy as np
# 假设词表: ["猫", "狗", "鱼", "鸟"]
vocab = {"猫": 0, "狗": 1, "鱼": 2, "鸟": 3}
vocab_size = len(vocab)
def one_hot_encode(word, vocab, vocab_size):
"""One-hot编码"""
vec = np.zeros(vocab_size)
if word in vocab:
vec[vocab[word]] = 1.0
return vec
# 编码示例
cat_vec = one_hot_encode("猫", vocab, vocab_size)
dog_vec = one_hot_encode("狗", vocab, vocab_size)
print(f"猫: {cat_vec}") # [1. 0. 0. 0.]
print(f"狗: {dog_vec}") # [0. 1. 0. 0.]
# 问题:余弦相似度为0——"猫"和"狗"毫无关系?
cos_sim = np.dot(cat_vec, dog_vec) / (np.linalg.norm(cat_vec) * np.linalg.norm(dog_vec))
print(f"猫-狗 余弦相似度: {cos_sim}") # 0.0
One-hot 的致命缺陷: 1. 维度灾难:词表大小=向量维度,中文词表可达 50 万+ 2. 语义缺失:任意两个不同词的相似度都是 0 3. 无法泛化:见过"猫很可爱"无法推广到"狗很可爱"
2.2 词袋模型与 TF-IDF¶
from collections import Counter
import math
class TFIDF:
"""TF-IDF文本表示"""
def __init__(self):
self.vocab = {}
self.idf = {}
def fit(self, documents):
"""构建词表和计算IDF"""
# 构建词表
all_words = set()
for doc in documents:
all_words.update(doc.split())
self.vocab = {word: i for i, word in enumerate(sorted(all_words))}
# 计算IDF
N = len(documents)
for word in self.vocab:
# 包含该词的文档数
df = sum(1 for doc in documents if word in doc.split())
self.idf[word] = math.log(N / (1 + df)) + 1 # 平滑IDF
def transform(self, document):
"""将文档转换为TF-IDF向量"""
words = document.split()
tf = Counter(words) # Counter统计元素出现次数
total = len(words)
vec = np.zeros(len(self.vocab))
for word, count in tf.items():
if word in self.vocab:
tf_val = count / total
vec[self.vocab[word]] = tf_val * self.idf.get(word, 1.0)
return vec
# 示例
docs = [
"猫 喜欢 吃 鱼",
"狗 喜欢 吃 骨头",
"猫 和 狗 都是 宠物"
]
tfidf = TFIDF()
tfidf.fit(docs)
for doc in docs:
vec = tfidf.transform(doc)
print(f"'{doc}' → 非零维度: {np.count_nonzero(vec)}, 向量模长: {np.linalg.norm(vec):.4f}")
TF-IDF 的优势与局限: - ✅ 考虑了词频( TF )和文档频率( IDF ) - ✅ 高频停用词("的"、"了")权重被压低 - ❌ 仍然是稀疏向量,无法捕捉语义 - ❌ 忽略词序:"猫追狗"和"狗追猫"表示相同
2.3 分词与子词切分:从字词到子词¶
为什么需要关注分词? 在任何文本表示方法之前,首先要将连续文本切分为离散单元( Token ),这一步直接影响模型的词表大小、未知词处理和语义表示质量。
中文分词的特殊挑战:
中文 vs 英文分词对比:
┌──────────────────────────────────────────────────┐
│ 英文:The cat sits on the mat. │
│ 天然空格分隔 → [The, cat, sits, on, the, mat] │
├──────────────────────────────────────────────────┤
│ 中文:今天天气真好适合出去游玩 │
│ 无天然分隔 → 需要分词算法 │
│ 正确:[今天, 天气, 真, 好, 适合, 出去, 游玩] │
│ 错误:[今, 天天, 气真, 好适, 合出, 去游, 玩] │
└──────────────────────────────────────────────────┘
大模型时代的分词方法:现代 LLM 普遍采用子词切分( Subword Tokenization ),在词和字符之间取得平衡:
| 方法 | 核心思想 | 代表模型 | 特点 |
|---|---|---|---|
| BPE | 合并最高频字符对 | GPT-⅔, LLaMA | 自底向上,贪心合并 |
| WordPiece | 最大化语言模型似然 | BERT, DistilBERT | 类似 BPE 但选择标准不同 |
| Unigram | 从大词表逐步删减 | T5, ALBERT | 自顶向下,概率最优 |
| SentencePiece | 语言无关的统一框架 | 多语言模型 | 将文本视为字节流处理 |
# BPE(Byte Pair Encoding)算法核心流程演示
def bpe_train(text, vocab_size=1000):
"""
简化的 BPE 训练过程
核心思想:反复合并最高频的相邻 token 对
"""
# 步骤1:将文本拆分为字符序列
words = text.split()
# 每个词表示为字符元组,末尾加 </w> 标记词边界
vocab = {}
for word in words:
symbols = tuple(list(word) + ['</w>'])
vocab[symbols] = vocab.get(symbols, 0) + 1
print(f"初始词表(字符级): {len(set(s for symbols in vocab for s in symbols))} 个符号")
# 步骤2:迭代合并最高频的符号对
num_merges = 10 # 简化演示
for i in range(num_merges):
# 统计所有相邻符号对的频率
pairs = {}
for symbols, freq in vocab.items():
for j in range(len(symbols) - 1):
pair = (symbols[j], symbols[j+1])
pairs[pair] = pairs.get(pair, 0) + freq
if not pairs:
break
# 找到频率最高的符号对
best_pair = max(pairs, key=pairs.get)
# 合并该符号对
new_vocab = {}
for symbols, freq in vocab.items():
new_symbols = []
j = 0
while j < len(symbols):
if j < len(symbols) - 1 and symbols[j] == best_pair[0] and symbols[j+1] == best_pair[1]:
new_symbols.append(best_pair[0] + best_pair[1])
j += 2
else:
new_symbols.append(symbols[j])
j += 1
new_vocab[tuple(new_symbols)] = freq
vocab = new_vocab
print(f"合并 {i+1}: {best_pair} → '{best_pair[0]+best_pair[1]}' (频率: {pairs[best_pair]})")
return vocab
# 示例
sample_text = "low lower newest wide low low lower newest widest lowest"
print("=== BPE 训练演示 ===")
result = bpe_train(sample_text)
子词切分的核心优势:
┌─────────────────────────────────────────────────────────────┐
│ 1. 解决未登录词( OOV )问题 │
│ "unhappiness" → ["un", "happi", "ness"] │
│ 即使没见过完整词,也能通过子词理解含义 │
│ │
│ 2. 平衡词表大小和语义表示 │
│ 纯字符:词表小(~数百),但语义信息少 │
│ 纯词:词表大(~数十万),但 OOV 严重 │
│ 子词:词表适中(~30K-50K),兼顾两者 │
│ │
│ 3. 多语言统一处理 │
│ SentencePiece 将文本视为字节流,无需语言特定预处理 │
│ 支持中文(字+词混合)、英文(子词)、日文(多文字混合)等 │
└─────────────────────────────────────────────────────────────┘
参考来源:本节内容参考了 happy-llm 第一章( NLP 基础概念)和 hugging-llm 第一章(基础知识)对 Token 和 Embedding 的讲解,以及 diy-llm 第四章对分词算法的详细分析。
2.4 分布式词向量: Word2Vec¶
核心思想( Distributional Hypothesis ):一个词的含义由它的上下文决定。
"You shall know a word by the company it keeps." — J.R. Firth, 1957
import torch
import torch.nn as nn
import torch.optim as optim
class SkipGram(nn.Module):
"""
Word2Vec Skip-Gram模型
给定中心词,预测上下文词
"""
def __init__(self, vocab_size, embedding_dim):
super().__init__() # super()调用父类方法
# 中心词嵌入矩阵
self.center_embeddings = nn.Embedding(vocab_size, embedding_dim)
# 上下文词嵌入矩阵
self.context_embeddings = nn.Embedding(vocab_size, embedding_dim)
# 初始化
nn.init.xavier_uniform_(self.center_embeddings.weight)
nn.init.xavier_uniform_(self.context_embeddings.weight)
def forward(self, center_ids, context_ids, negative_ids):
"""
Negative Sampling训练
Args:
center_ids: [batch_size] 中心词ID
context_ids: [batch_size] 正样本上下文词ID
negative_ids: [batch_size, num_neg] 负样本ID
"""
# 获取嵌入向量
center = self.center_embeddings(center_ids) # [B, D]
context = self.context_embeddings(context_ids) # [B, D]
negatives = self.context_embeddings(negative_ids) # [B, num_neg, D]
# 正样本得分: center · context
pos_score = torch.sum(center * context, dim=-1) # [B]
pos_loss = -torch.log(torch.sigmoid(pos_score) + 1e-8)
# 负样本得分: center · negative
neg_score = torch.bmm(negatives, center.unsqueeze(-1)).squeeze(-1) # [B, num_neg] # unsqueeze增加一个维度
neg_loss = -torch.log(torch.sigmoid(-neg_score) + 1e-8).sum(dim=-1)
return (pos_loss + neg_loss).mean()
def get_embedding(self, word_id):
"""获取词的最终向量(使用中心词嵌入)"""
return self.center_embeddings(torch.tensor([word_id])).detach()
# --- 训练示例 ---
# 构建简单语料
corpus = "the cat sat on the mat the dog sat on the rug".split()
vocab = {w: i for i, w in enumerate(set(corpus))}
vocab_size = len(vocab)
embedding_dim = 32
model = SkipGram(vocab_size, embedding_dim)
optimizer = optim.Adam(model.parameters(), lr=0.01)
print(f"词表大小: {vocab_size}, 嵌入维度: {embedding_dim}")
print(f"词表: {vocab}")
# 生成训练样本(简化版,window_size=2)
window_size = 2
training_pairs = []
for i, word in enumerate(corpus): # enumerate同时获取索引和元素
center_id = vocab[word]
for j in range(max(0, i - window_size), min(len(corpus), i + window_size + 1)):
if i != j:
context_id = vocab[corpus[j]]
# 随机负采样
neg_ids = []
while len(neg_ids) < 5:
neg = torch.randint(0, vocab_size, (1,)).item()
if neg != context_id:
neg_ids.append(neg)
training_pairs.append((center_id, context_id, neg_ids))
print(f"训练样本数: {len(training_pairs)}")
# 训练
for epoch in range(100):
total_loss = 0
for center, context, negs in training_pairs:
loss = model(
torch.tensor([center]),
torch.tensor([context]),
torch.tensor([negs])
)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1}, Loss: {total_loss/len(training_pairs):.4f}")
# 词向量相似度
def cosine_similarity(v1, v2):
return torch.cosine_similarity(v1, v2, dim=-1).item()
cat_emb = model.get_embedding(vocab["cat"])
dog_emb = model.get_embedding(vocab["dog"])
mat_emb = model.get_embedding(vocab["mat"])
print(f"\n词向量相似度:")
print(f" cat-dog: {cosine_similarity(cat_emb, dog_emb):.4f}")
print(f" cat-mat: {cosine_similarity(cat_emb, mat_emb):.4f}")
Word2Vec 的核心贡献: - 将词从稀疏的 one-hot 映射到低维稠密向量空间 - 语义相似的词在向量空间中距离接近 - 著名的类比推理:\(\vec{king} - \vec{man} + \vec{woman} \approx \vec{queen}\)
2.5 从静态到动态: ELMo¶
Word2Vec 的局限:一个词只有一个固定向量,无法处理一词多义。 - "Apple 推出了新 iPhone" → Apple 表示苹果公司 - "I ate an apple" → apple 表示水果
ELMo ( Embeddings from Language Models, 2018 ) 的解决方案:
ELMo架构:
最终表示 = γ(s₀·h₀ + s₁·h₁ + s₂·h₂)
↑
┌────────────┼────────────┐
h₀(词嵌入) h₁(前向LSTM层1 h₂(前向LSTM层2
+反向LSTM层1) +反向LSTM层2)
↑ ↑ ↑
字符CNN 双向LSTM层1 双向LSTM层2
↑ ↑ ↑
输入词 隐藏状态传递 隐藏状态传递
ELMo 的关键创新: 1. 上下文相关:同一个词在不同句子中有不同向量 2. 多层表示:底层捕捉语法信息,高层捕捉语义信息 3. 双向建模:同时考虑左侧和右侧上下文
# ELMo的核心思想示意(简化版)
class SimpleBiLM(nn.Module):
"""简化版双向语言模型(ELMo核心思想)"""
def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
# 前向LSTM
self.forward_lstm = nn.LSTM(
embed_dim, hidden_dim, num_layers,
batch_first=True, dropout=0.1
)
# 反向LSTM
self.backward_lstm = nn.LSTM(
embed_dim, hidden_dim, num_layers,
batch_first=True, dropout=0.1
)
# 层权重(可学习的标量混合参数)
self.layer_weights = nn.Parameter(torch.ones(num_layers + 1) / (num_layers + 1))
self.gamma = nn.Parameter(torch.ones(1))
def forward(self, input_ids):
"""
Args:
input_ids: [batch_size, seq_len]
Returns:
contextualized: [batch_size, seq_len, hidden_dim * 2]
"""
embeds = self.embedding(input_ids) # [B, L, E]
# 前向
fwd_out, _ = self.forward_lstm(embeds) # [B, L, H]
# 反向(翻转序列)
bwd_out, _ = self.backward_lstm(embeds.flip(1))
bwd_out = bwd_out.flip(1) # 翻回来 [B, L, H]
# 拼接双向表示
contextualized = torch.cat([fwd_out, bwd_out], dim=-1) # [B, L, 2H]
return contextualized
# ELMo提供了上下文相关的词表示
# 同一个 "bank" 在 "river bank" 和 "bank account" 中有不同向量
2.6 文本表示演进总结¶
文本表示方法演进对比
┌──────────────────┬──────────────┬──────────────┬──────────────┐
│ 方法 │ 表示类型 │ 语义能力 │ 上下文感知 │
├──────────────────┼──────────────┼──────────────┼──────────────┤
│ One-hot │ 稀疏,高维 │ ✗ 无语义 │ ✗ 无 │
│ TF-IDF │ 稀疏,高维 │ △ 词频统计 │ ✗ 无 │
│ Word2Vec/GloVe │ 稠密,低维 │ ✓ 分布式语义 │ ✗ 静态 │
│ ELMo │ 稠密,动态 │ ✓ 深层语义 │ ✓ 上下文相关 │
│ BERT/GPT │ 稠密,动态 │ ✓✓ 深层语义 │ ✓✓ 全上下文 │
└──────────────────┴──────────────┴──────────────┴──────────────┘
3. 经典 NLP 任务与方法¶
3.1 文本分类¶
import torch
import torch.nn as nn
class TextCNN(nn.Module):
"""
TextCNN (Kim, 2014) — 经典文本分类模型
用不同大小的卷积核捕捉n-gram特征
"""
def __init__(self, vocab_size, embed_dim, num_classes,
filter_sizes=(3, 4, 5), num_filters=100):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
# 多尺度卷积
self.convs = nn.ModuleList([
nn.Conv1d(embed_dim, num_filters, fs)
for fs in filter_sizes
])
self.dropout = nn.Dropout(0.5)
self.fc = nn.Linear(num_filters * len(filter_sizes), num_classes)
def forward(self, x):
"""
Args: x [batch_size, seq_len]
Returns: logits [batch_size, num_classes]
"""
# [B, L, E] → [B, E, L] (Conv1d需要 channels-first)
embedded = self.embedding(x).transpose(1, 2)
# 多尺度卷积 + ReLU + 最大池化
pooled = []
for conv in self.convs:
h = torch.relu(conv(embedded)) # [B, num_filters, L-fs+1]
h = torch.max(h, dim=-1).values # [B, num_filters]
pooled.append(h)
# 拼接所有尺度的特征
cat = torch.cat(pooled, dim=-1) # [B, num_filters * len(filter_sizes)]
return self.fc(self.dropout(cat))
# 使用示例
model = TextCNN(vocab_size=10000, embed_dim=128, num_classes=4)
dummy_input = torch.randint(0, 10000, (8, 50)) # batch=8, seq_len=50
output = model(dummy_input)
print(f"TextCNN输出形状: {output.shape}") # [8, 4]
3.2 序列标注:命名实体识别¶
class BiLSTM_CRF(nn.Module):
"""
BiLSTM-CRF — 经典序列标注模型
BiLSTM提取特征,CRF建模标签间的转移约束
"""
def __init__(self, vocab_size, embed_dim, hidden_dim, num_tags):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(
embed_dim, hidden_dim // 2,
bidirectional=True, batch_first=True
)
self.hidden2tag = nn.Linear(hidden_dim, num_tags)
# CRF转移矩阵: transitions[i][j] = 从标签j转移到标签i的分数
self.transitions = nn.Parameter(torch.randn(num_tags, num_tags))
self.num_tags = num_tags
def _get_emissions(self, x):
"""获取发射分数"""
embeds = self.embedding(x)
lstm_out, _ = self.lstm(embeds)
return self.hidden2tag(lstm_out) # [B, L, num_tags]
def forward(self, x):
"""推理时使用维特比解码"""
emissions = self._get_emissions(x) # [B, L, T]
# 简化版:逐batch维特比解码
batch_size, seq_len, num_tags = emissions.shape
best_paths = []
for b in range(batch_size):
# 维特比算法
viterbi = emissions[b, 0] # [T]
backpointers = []
for t in range(1, seq_len):
# viterbi_expand: [T_prev] + transitions: [T_cur, T_prev] → [T_cur, T_prev]
scores = viterbi.unsqueeze(0) + self.transitions # [T, T]
scores = scores + emissions[b, t].unsqueeze(1) # [T, T]
best_scores, best_ids = scores.max(dim=1) # [T]
viterbi = best_scores
backpointers.append(best_ids)
# 回溯
best_last = viterbi.argmax().item()
best_path = [best_last]
for bp in reversed(backpointers):
best_path.append(bp[best_path[-1]].item()) # [-1]负索引取最后一个元素
best_path.reverse()
best_paths.append(best_path)
return best_paths
# NER标签示例: BIO标注
tag2id = {"O": 0, "B-PER": 1, "I-PER": 2, "B-ORG": 3, "I-ORG": 4, "B-LOC": 5, "I-LOC": 6}
model = BiLSTM_CRF(vocab_size=5000, embed_dim=128, hidden_dim=256, num_tags=len(tag2id))
dummy_input = torch.randint(0, 5000, (4, 20))
predictions = model(dummy_input)
print(f"BiLSTM-CRF预测: 4个序列, 各{len(predictions[0])}个标签")
3.3 Seq2Seq 与注意力机制¶
class Seq2SeqWithAttention(nn.Module):
"""
带注意力的Seq2Seq(Bahdanau Attention)
经典的编码器-解码器架构,为Transformer奠定基础
"""
def __init__(self, src_vocab, tgt_vocab, embed_dim, hidden_dim):
super().__init__()
# 编码器
self.src_embedding = nn.Embedding(src_vocab, embed_dim)
self.encoder = nn.GRU(embed_dim, hidden_dim, bidirectional=True, batch_first=True)
# 解码器
self.tgt_embedding = nn.Embedding(tgt_vocab, embed_dim)
self.decoder = nn.GRU(embed_dim + hidden_dim * 2, hidden_dim, batch_first=True)
# 注意力
self.attn = nn.Linear(hidden_dim * 3, hidden_dim)
self.v = nn.Linear(hidden_dim, 1, bias=False)
# 输出
self.output = nn.Linear(hidden_dim, tgt_vocab)
def attention(self, decoder_hidden, encoder_outputs):
"""
Bahdanau (Additive) Attention
Args:
decoder_hidden: [B, 1, H]
encoder_outputs: [B, src_len, 2H]
Returns:
context: [B, 1, 2H]
"""
src_len = encoder_outputs.shape[1]
hidden_expanded = decoder_hidden.repeat(1, src_len, 1) # [B, src_len, H]
# 拼接 + 打分
energy = torch.tanh(self.attn(
torch.cat([hidden_expanded, encoder_outputs], dim=-1) # [B, src_len, 3H]
))
scores = self.v(energy).squeeze(-1) # [B, src_len]
weights = torch.softmax(scores, dim=-1) # [B, src_len]
context = torch.bmm(weights.unsqueeze(1), encoder_outputs) # [B, 1, 2H]
return context, weights
print("Seq2Seq with Attention: 注意力机制是Transformer的前身")
print("关键洞察: 让解码器在每一步'看'编码器的不同位置")
4. 预训练语言模型的诞生¶
4.1 预训练-微调范式¶
预训练-微调(Pre-train & Fine-tune)范式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
阶段1: 预训练(无监督/自监督)
┌──────────────────────────────────┐
│ 大规模无标注文本(数十GB~TB) │
│ ↓ │
│ 自监督目标(MLM / CLM / DAE) │
│ ↓ │
│ 学习通用语言知识 │
│ → 语法、语义、世界知识、推理 │
└──────────────────────────────────┘
阶段2: 微调(有监督)
┌──────────────────────────────────┐
│ 少量标注数据(数千~数万条) │
│ ↓ │
│ 特定任务目标(分类/NER/QA...) │
│ ↓ │
│ 适配下游任务 │
└──────────────────────────────────┘
4.2 三种预训练目标¶
| 预训练目标 | 全称 | 代表模型 | 示意 |
|---|---|---|---|
| MLM | Masked Language Model | BERT | [CLS] 我 [MASK] 北京 [SEP] → 预测 "在" |
| CLM | Causal Language Model | GPT | 我 在 北京 → 预测 "的" |
| DAE | Denoising AutoEncoder | T5 | 输入加噪 → 恢复原文 |
# 三种预训练目标的直观对比
print("""
┌─────────────────────────────────────────────────────────┐
│ MLM (BERT): 完形填空 │
│ 输入: "小明在[MASK]读大学, 他的专业是[MASK]科学" │
│ 目标: 预测被遮住的词 → "北京", "计算机" │
│ 特点: 双向上下文, 15%的词被随机遮盖 │
├─────────────────────────────────────────────────────────┤
│ CLM (GPT): 从左到右续写 │
│ 输入: "小明在北京读大学" │
│ 目标: P(在|小明) × P(北京|小明,在) × P(读|小明,在,北京)│
│ 特点: 单向(从左到右), 自回归生成 │
├─────────────────────────────────────────────────────────┤
│ DAE (T5): 去噪重建 │
│ 输入: "小明在<X>读大学, 他的专业是<Y>" │
│ 目标: "<X> 北京 <Y> 计算机科学" │
│ 特点: Encoder看全文, Decoder生成被替换的片段 │
└─────────────────────────────────────────────────────────┘
""")
5. Encoder-only 架构: BERT 系列¶
5.1 BERT 核心设计¶
BERT (Bidirectional Encoder Representations from Transformers)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
输入: [CLS] 我 爱 [MASK] 京 [SEP] 天气 真 好 [SEP]
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
Token: E0 E1 E2 E3 E4 E5 E6 E7 E8 E9
+
Segment: SA SA SA SA SA SA SB SB SB SB
+
Position: P0 P1 P2 P3 P4 P5 P6 P7 P8 P9
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
┌──────────────────────────────────────────────────┐
│ Transformer Encoder × 12/24 │
│ (双向Self-Attention, 每个位置 │
│ 都能看到所有其他位置) │
└──────────────────────────────────────────────────┘
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
输出: T0 T1 T2 T3 T4 T5 T6 T7 T8 T9
↓ ↓
[CLS]用于 [MASK]位置
句子级任务 用于MLM预测
5.2 BERT 微调实战¶
import torch
import torch.nn as nn
class BERTForClassification(nn.Module):
"""
BERT用于文本分类(微调范式的典型用法)
"""
def __init__(self, hidden_size=768, num_classes=2, dropout=0.1):
super().__init__()
# 实际使用时这里加载预训练的BERT
# self.bert = BertModel.from_pretrained('bert-base-chinese')
# 简化版:模拟BERT encoder
encoder_layer = nn.TransformerEncoderLayer(
d_model=hidden_size, nhead=12,
dim_feedforward=3072, dropout=dropout,
batch_first=True
)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=12)
self.embedding = nn.Embedding(21128, hidden_size) # BERT中文词表大小
self.pos_embedding = nn.Embedding(512, hidden_size)
# 分类头
self.classifier = nn.Sequential(
nn.Dropout(dropout),
nn.Linear(hidden_size, hidden_size),
nn.Tanh(),
nn.Dropout(dropout),
nn.Linear(hidden_size, num_classes)
)
def forward(self, input_ids, attention_mask=None):
"""
Args:
input_ids: [B, L] token IDs
attention_mask: [B, L] 1=有效token, 0=padding
"""
B, L = input_ids.shape
positions = torch.arange(L, device=input_ids.device).unsqueeze(0).expand(B, L)
# Token + Position Embedding
x = self.embedding(input_ids) + self.pos_embedding(positions)
# Transformer Encoder
if attention_mask is not None:
# 将 0/1 mask 转换为 True/False(True表示被遮蔽)
src_key_padding_mask = (attention_mask == 0)
else:
src_key_padding_mask = None
encoded = self.encoder(x, src_key_padding_mask=src_key_padding_mask)
# 取 [CLS] 位置的输出做分类
cls_output = encoded[:, 0, :] # [B, H]
return self.classifier(cls_output)
# 使用示例
model = BERTForClassification(hidden_size=768, num_classes=3)
dummy_ids = torch.randint(0, 21128, (4, 128))
dummy_mask = torch.ones(4, 128)
logits = model(dummy_ids, dummy_mask)
print(f"BERT分类输出: {logits.shape}") # [4, 3]
5.3 BERT 系列演进¶
| 模型 | 改进点 | 关键思想 |
|---|---|---|
| BERT (2018) | 基础版 | MLM + NSP, 双向编码 |
| RoBERTa (2019) | 训练优化 | 去掉 NSP ,动态 Masking ,更多数据/更长训练 |
| ALBERT (2019) | 参数效率 | 因式分解嵌入,跨层参数共享 |
| DistilBERT (2019) | 知识蒸馏 | 6 层蒸馏 12 层,速度 2x ,性能保留 97% |
| ELECTRA (2020) | 训练效率 | 替换 Token 检测( RTD ),比 MLM 效率更高 |
| DeBERTa (2021) | 注意力增强 | 解耦注意力(内容+位置分开计算) |
5.3.1 RoBERTa 详解¶
RoBERTa( Robustly Optimized BERT Pretraining Approach )由 Meta 于 2019 年发布,核心思想是通过更充分的训练来释放 BERT 架构的潜力,而非修改模型结构。
三大优化:
优化一:去掉 NSP 任务
├── 实验证明 NSP 过于简单,对下游任务帮助有限甚至有害
├── 改为"单文档 MLM ":一个输入只从一个文档采样
└── 实验组对比:去掉 NSP 后在所有下游任务上均有提升
优化二:更大规模预训练
├── 数据量:BERT 13GB → RoBERTa 160GB( 10 倍)
├── 数据来源:BookCorpus + Wikipedia + CC-NEWS + OpenWebText + Stories
├── Batch Size:BERT 256 → RoBERTa 8K
├── 训练步长:BERT 1M 步 → RoBERTa 500K 步(更大 batch )
└── 训练资源:1024 块 V100( 32GB )训练 1 天
优化三:动态遮蔽 + 更大 BPE 词表
├── BERT :静态 Mask (数据预处理阶段完成,每 10 epoch 相同)
├── RoBERTa :动态 Mask (训练时实时生成,每个 epoch 不同)
└── 词表:BERT 30K WordPiece → RoBERTa 50K BPE
# RoBERTa vs BERT 预训练配置对比
config_comparison = {
"BERT-base": {
"参数量": "110M",
"预训练数据": "13GB (3.3B tokens)",
"Batch Size": 256,
"训练步数": "1M",
"词表大小": "30K (WordPiece)",
"预训练任务": "MLM + NSP",
"遮蔽策略": "静态(4次随机Mask)",
},
"RoBERTa-base": {
"参数量": "125M",
"预训练数据": "160GB (约10x BERT)",
"Batch Size": "8K",
"训练步数": "500K",
"词表大小": "50K (BPE)",
"预训练任务": "仅MLM(去掉NSP)",
"遮蔽策略": "动态(每epoch实时生成)",
}
}
# 核心洞察:同样的架构,更充分的训练 → 显著性能提升
# 证明了"训练方法"与"模型架构"同等重要
5.3.2 ALBERT 详解¶
ALBERT( A Lite BERT )由 Google 于 2019 年发布,核心思想是通过参数共享和因式分解来降低模型参数量,同时保持甚至提升模型性能。
三大优化:
优化一:嵌入参数因式分解
├── 问题:BERT 的 Embedding 矩阵为 V×H(词表×隐藏层维度)
│ 如 V=30K, H=1024 → Embedding 参数 30M
│ 且 H 增大时 Embedding 参数同步膨胀
├── 洞察:Word2Vec 仅用 100 维就获得了好的词向量
│ Embedding 不需要和隐藏层同维度
└── 方案:V×E + E×H(E=128),参数从 V×H 降低到 V×E + E×H
当 E << H 时优化明显
优化二:跨层参数共享
├── 发现:BERT 各层 Encoder 参数存在高度一致性
├── 方案:所有层共享同一组参数(只初始化一层)
├── 效果:24 层 ALBERT-xlarge 仅 59M 参数( BERT-large 340M )
└── 注意:参数减少但计算量不减(仍需 24 次前向传播)
优化三:SOP 替代 NSP
├── NSP 问题:太简单(随机句子对 vs 连续句子)
├── SOP( Sentence Order Prediction ):判断两句话顺序是否正确
│ 正样本:连续两句(A→B)
│ 负样本:交换顺序(B→A)
└── SOP 比 NSP 更难,迫使模型学习更细粒度的句间关系
# ALBERT 参数效率演示
def albert_param_savings(vocab_size, hidden_size, embed_size, num_layers):
"""
计算 ALBERT 通过因式分解和参数共享节省的参数量
"""
# 标准 BERT 的 Embedding 参数
bert_embed_params = vocab_size * hidden_size
# ALBERT 因式分解后的 Embedding 参数
albert_embed_params = vocab_size * embed_size + embed_size * hidden_size
# 单层 Encoder 参数(简化估计)
single_layer_params = 4 * hidden_size * hidden_size # Attention + FFN
# BERT: 每层独立参数
bert_encoder_params = num_layers * single_layer_params
# ALBERT: 所有层共享参数
albert_encoder_params = single_layer_params # 仅一层
bert_total = bert_embed_params + bert_encoder_params
albert_total = albert_embed_params + albert_encoder_params
print(f"BERT 参数量: {bert_total / 1e6:.1f}M")
print(f"ALBERT 参数量: {albert_total / 1e6:.1f}M")
print(f"参数压缩比: {bert_total / albert_total:.1f}x")
# 示例:24层模型
albert_param_savings(vocab_size=30000, hidden_size=1024,
embed_size=128, num_layers=24)
# BERT 参数量: ~340M → ALBERT 参数量: ~59M(压缩约 5.8x )
参考来源:本节内容参考了 happy-llm 第三章(预训练语言模型)对 RoBERTa 和 ALBERT 的详细分析,以及原始论文 Liu et al. (2019) 和 Lan et al. (2019) 。
6. Decoder-only 架构: GPT 系列¶
6.1 GPT 核心设计¶
GPT (Generative Pre-trained Transformer)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
输入: 今天 天气 → 真 → 好
↓ ↓ ↓ ↓
┌──────────────────────────────┐
│ Transformer Decoder × N │
│ (因果自注意力: │
│ 每个位置只能看到 │
│ 自己及之前的位置) │
│ │
│ 今天 → 能看到: [今天] │
│ 天气 → 能看到: [今天,天气] │
│ 真 → 能看到: [今天,..,真]│
└──────────────────────────────┘
↓ ↓ ↓ ↓
预测: 天气 真 好 [EOS]
P(天气|今天) × P(真|今天,天气) × P(好|今天,天气,真)×...
GPT 与 BERT 的根本区别: - BERT 是理解模型:双向编码,擅长分类/抽取 - GPT 是生成模型:自回归,擅长文本生成
6.2 GPT 系列演进¶
# GPT系列参数规模演进
gpt_evolution = {
"GPT-1 (2018)": {"参数": "117M", "数据": "5GB", "关键创新": "预训练+微调范式"},
"GPT-2 (2019)": {"参数": "1.5B", "数据": "40GB", "关键创新": "Zero-shot多任务"},
"GPT-3 (2020)": {"参数": "175B", "数据": "570GB", "关键创新": "In-context Learning"},
"GPT-3.5 (2022)": {"参数": "~175B", "数据": "更大", "关键创新": "RLHF/InstructGPT"},
"GPT-4 (2023)": {"参数": "未公开", "数据": "未公开", "关键创新": "多模态/MoE传闻"},
}
print("GPT系列关键里程碑:")
print("=" * 60)
for name, info in gpt_evolution.items():
print(f"{name}:")
print(f" 参数量: {info['参数']}")
print(f" 训练数据: {info['数据']}")
print(f" 关键创新: {info['关键创新']}")
print()
print("核心洞察:")
print("GPT-1→2:规模增大 → 零样本能力出现")
print("GPT-2→3:10x规模 → In-context Learning涌现")
print("GPT-3→3.5:RLHF对齐 → 真正可用的AI助手")
print("GPT-3.5→4:多模态 + 推理能力飞跃")
6.3 In-context Learning : GPT-3 的涌现能力¶
# In-context Learning示例:不需要微调,直接在prompt中提供示例
# Zero-shot(零样本)
zero_shot_prompt = """
请将以下英文翻译成中文:
The weather is beautiful today.
"""
# One-shot(单样本)
one_shot_prompt = """
英文翻译成中文示例:
English: Hello, how are you?
中文: 你好,你怎么样?
请翻译:
English: The weather is beautiful today.
中文:
"""
# Few-shot(少样本)
few_shot_prompt = """
将英文情感分类为正面或负面:
"This movie is amazing!" → 正面
"I hated every minute of it." → 负面
"The food was delicious and service was great." → 正面
"The product broke after one day." →
"""
print("In-context Learning的三种模式:")
print(" Zero-shot: 不给示例,直接问")
print(" One-shot: 给1个示例")
print(" Few-shot: 给2-5个示例")
print()
print("关键洞察:GPT-3不需要更新参数就能适应新任务!")
print("这是'涌现能力'的典型体现。")
7. Encoder-Decoder 架构: T5/BART¶
7.1 T5 :统一文本到文本框架¶
T5 (Text-to-Text Transfer Transformer)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
核心思想:将所有NLP任务统一为 文本→文本 格式
分类: "classify: I love this movie" → "positive"
翻译: "translate English to Chinese: Hello" → "你好"
摘要: "summarize: <长文本>" → "<短摘要>"
问答: "question: What is AI? context: <上下文>" → "AI is..."
NER: "tag: 小明在北京上学" → "小明:PER 北京:LOC"
# T5的统一框架思想
class T5Conceptual(nn.Module):
"""
T5概念性实现:展示Encoder-Decoder统一框架
"""
def __init__(self, vocab_size, d_model, nhead, num_layers):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.pos_embedding = nn.Embedding(512, d_model)
# 编码器
encoder_layer = nn.TransformerEncoderLayer(
d_model, nhead, d_model * 4, batch_first=True
)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
# 解码器
decoder_layer = nn.TransformerDecoderLayer(
d_model, nhead, d_model * 4, batch_first=True
)
self.decoder = nn.TransformerDecoder(decoder_layer, num_layers)
# 输出投影
self.output_proj = nn.Linear(d_model, vocab_size)
def forward(self, src_ids, tgt_ids):
"""
Encoder-Decoder前向传播
src_ids: 编码器输入 [B, src_len]
tgt_ids: 解码器输入 [B, tgt_len]
"""
B, src_len = src_ids.shape
_, tgt_len = tgt_ids.shape
# 嵌入
src_pos = torch.arange(src_len, device=src_ids.device).unsqueeze(0)
tgt_pos = torch.arange(tgt_len, device=tgt_ids.device).unsqueeze(0)
src = self.embedding(src_ids) + self.pos_embedding(src_pos)
tgt = self.embedding(tgt_ids) + self.pos_embedding(tgt_pos)
# 编码
memory = self.encoder(src) # [B, src_len, D]
# 因果mask(解码器只能看到之前的token)
causal_mask = nn.Transformer.generate_square_subsequent_mask(tgt_len)
causal_mask = causal_mask.to(src_ids.device)
# 解码
decoded = self.decoder(tgt, memory, tgt_mask=causal_mask) # [B, tgt_len, D]
return self.output_proj(decoded) # [B, tgt_len, vocab_size]
model = T5Conceptual(vocab_size=32000, d_model=512, nhead=8, num_layers=6)
src = torch.randint(0, 32000, (2, 30))
tgt = torch.randint(0, 32000, (2, 20))
output = model(src, tgt)
print(f"T5输出形状: {output.shape}") # [2, 20, 32000]
7.2 BART :去噪自编码器¶
BART预训练:对输入加噪,让模型恢复原文
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5种噪声策略:
┌─────────────────────────────────────┐
│ 1. Token Masking : A B _ D E │
│ 2. Token Deletion : A B D E │
│ 3. Text Infilling : A _ D E (用1个_ │
│ 替换连续多个) │
│ 4. Sentence Permutation: 打乱句子序 │
│ 5. Document Rotation: 旋转文档起点 │
└─────────────────────────────────────┘
原文: A B C D E
BART = BERT的双向编码 + GPT的自回归解码
→ 兼具理解和生成能力
8. 三种架构深度对比¶
8.1 架构对比表¶
comparison = """
┌──────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ │ Encoder-only │ Decoder-only │ Encoder-Decoder │
│ │ (BERT系列) │ (GPT系列) │ (T5/BART) │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 注意力方向 │ 双向(全可见) │ 单向(因果mask) │ 编码器双向 │
│ │ │ │ 解码器单向+交叉 │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 预训练目标 │ MLM(完形填空) │ CLM(下一词预测) │ DAE(去噪重建) │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 输入 │ 完整文本 │ 前缀/提示 │ 源文本 │
│ 输出 │ 每位置的表示 │ 自回归生成 │ 目标文本 │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 擅长任务 │ 分类/NER/匹配 │ 文本生成/对话 │ 翻译/摘要/QA │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 微调方式 │ 加分类头微调 │ 自回归微调 │ Seq2Seq微调 │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 代表模型 │ BERT, RoBERTa │ GPT-4, LLaMA │ T5, BART, mT5 │
│ │ DeBERTa, ELECTRA │ Claude, Gemini │ FLAN-T5 │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 当前地位 │ NLU任务首选 │ LLM主流架构 │ 翻译/摘要经典 │
│ │ (但被LLM逐渐 │ (统治性地位) │ (但被LLM逐渐 │
│ │ 取代) │ │ 取代) │
└──────────────────┴──────────────────┴──────────────────┴──────────────────┘
"""
print(comparison)
8.2 为什么 Decoder-only 成为 LLM 主流¶
reasons = """
Decoder-only架构(GPT/LLaMA/Claude)为何成为主流?
1. 统一输入输出格式
- Encoder-Decoder需要区分"源"和"目标"
- Decoder-only只需要一个连续序列,更简洁
- 所有任务统一为: prompt → completion
2. 规模扩展效率更高
- 只有一个Transformer栈,参数利用更高效
- 相同参数量下,Decoder-only通常效果更好
- KV Cache加速推理,Encoder-Decoder需要额外存编码器cache
3. In-context Learning天然适配
- 自回归生成 = 自然的多轮对话
- Few-shot提示 = 把示例放在前面然后续写
- BERT的[CLS] + Fine-tune范式在LLM时代不够灵活
4. 训练数据利用率
- CLM利用每一个token位置做预测(N-1个预测目标)
- MLM只预测15%被mask的token
- 在海量数据上,CLM的数据利用率更高
5. 生态与工程优势
- 推理只需要一个统一的Decoder,工程简单
- KV Cache可以高效缓存,支持流式输出
- 但BERT在检索、Embedding等任务上仍有独特优势!
"""
print(reasons)
8.3 代码对比实验¶
import torch
import torch.nn as nn
class MiniEncoder(nn.Module):
"""最小Encoder-only(BERT风格)"""
def __init__(self, vocab_size=1000, d_model=128, nhead=4, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4, batch_first=True)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
self.mlm_head = nn.Linear(d_model, vocab_size)
def forward(self, x, mask_positions=None):
h = self.encoder(self.embedding(x))
if mask_positions is not None:
# 只在mask位置预测
return self.mlm_head(h[:, mask_positions, :])
return h # 返回所有位置的表示
class MiniDecoder(nn.Module):
"""最小Decoder-only(GPT风格)"""
def __init__(self, vocab_size=1000, d_model=128, nhead=4, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
decoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4, batch_first=True)
self.decoder = nn.TransformerEncoder(decoder_layer, num_layers)
self.lm_head = nn.Linear(d_model, vocab_size)
def forward(self, x):
L = x.shape[1]
# 因果mask: 每个位置只能看到自己和之前
causal_mask = torch.triu(torch.ones(L, L), diagonal=1).bool().to(x.device)
h = self.decoder(self.embedding(x), src_key_padding_mask=None,
mask=causal_mask)
return self.lm_head(h) # 每个位置预测下一个token
class MiniEncDec(nn.Module):
"""最小Encoder-Decoder(T5风格)"""
def __init__(self, vocab_size=1000, d_model=128, nhead=4, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.transformer = nn.Transformer(
d_model, nhead, num_layers, num_layers, d_model*4, batch_first=True
)
self.output = nn.Linear(d_model, vocab_size)
def forward(self, src, tgt):
L = tgt.shape[1]
causal_mask = torch.triu(torch.ones(L, L), diagonal=1).bool().to(src.device)
src_emb = self.embedding(src)
tgt_emb = self.embedding(tgt)
h = self.transformer(src_emb, tgt_emb, tgt_mask=causal_mask)
return self.output(h)
# 对比三种架构
print("三种架构参数量对比(相同配置):")
print("=" * 50)
for name, model in [
("Encoder-only (BERT)", MiniEncoder()),
("Decoder-only (GPT)", MiniDecoder()),
("Encoder-Decoder (T5)", MiniEncDec())
]:
params = sum(p.numel() for p in model.parameters())
print(f" {name}: {params:,} 参数")
# 推理对比
print("\n推理特性对比:")
print(" Encoder-only: 一次性编码整个序列 → 产出固定表示")
print(" Decoder-only: 自回归逐token生成 → 可配合KV Cache")
print(" Encoder-Decoder: 先编码源序列, 再自回归解码目标序列")
9. 从 PLM 到 LLM :范式转变¶
9.1 Scaling Laws(规模法则)¶
Scaling Laws 是大模型时代最重要的经验规律之一,由 OpenAI 的 Kaplan et al. (2020) 首次系统阐述,后经 DeepMind 的 Chinchilla 论文 (Hoffmann et al., 2022) 修正。
核心发现:幂律关系¶
模型在自回归语言建模上的交叉熵损失 \(L\) 与三个因素呈幂律关系:
这意味着:将参数量增加 10 倍,损失大约降低 15%(因为 \(10^{-0.076} \approx 0.83\),即损失减少到 83%)。
三个关键推论¶
推论1: 模型越大越高效
├── 在固定计算预算下,训练一个较大的模型 fewer steps
│ 比训练一个较小的模型 more steps 效果更好
├── 这解释了为什么 GPT-3 (175B) 只训练了 300B tokens
│ 而不是把 6B 模型训练更久
└── 实践含义:优先增大模型,而非延长训练
推论2: 数据和模型需要同步增长
├── Chinchilla 论文修正了 Kaplan 的结论
├── 最优训练数据量 ≈ 20 × 模型参数量
│ 即 7B 模型需要约 140B tokens
├── GPT-3 (175B) 只用了 300B tokens → 训练不足!
│ Chinchilla (70B) 用 1.4T tokens → 超越 GPT-3
└── 实践含义:LLaMA 系列用更多数据训练较小模型
推论3: 计算预算的最优分配
├── 给定计算预算 C(FLOPs),最优参数量:
│ N_opt ≈ (C / 5.4 × 10^13)^(0.5)
├── 最优训练 token 数:
│ D_opt ≈ 20 × N_opt
└── 实践含义:不要盲目增大模型,要平衡模型大小和数据量
实际数据验证¶
模型规模与性能的演进(验证 Scaling Laws):
┌───────────────┬──────────┬────────────┬──────────────┐
│ 模型 │ 参数量 │ 训练数据 │ MMLU (5-shot)│
├───────────────┼──────────┼────────────┼──────────────┤
│ GPT-2 │ 1.5B │ ~40GB │ ~30% │
│ GPT-3 │ 175B │ 570GB │ 43.9% │
│ Chinchilla │ 70B │ 1.4T tokens│ 67.5% │
│ LLaMA-2 70B │ 70B │ 2T tokens │ 68.9% │
│ LLaMA-3 70B │ 70B │ 15T tokens │ 79.0% │
│ GPT-4 │ ~1.8T? │ ~13T? │ 86.4% │
└───────────────┴──────────┴────────────┴──────────────┘
关键洞察:LLaMA-3 70B 仅用 70B 参数就接近 GPT-4 水平
→ 验证了"更多数据 + 适度模型大小"优于"更大模型 + 较少数据"
💡 Scaling Laws 的实际意义:它为大模型的训练提供了科学指导——在投入巨额计算资源之前,可以用小模型的实验结果预测大模型的性能,从而做出最优的资源分配决策。
9.2 涌现能力( Emergent Abilities )¶
emergent_abilities = """
涌现能力: 在小模型中不存在, 在大模型中突然出现的能力
┌────────────────────────────────────────────────┐
│ 准确率 │
│ 100% │ ╭──── GPT-4 │
│ │ ╭─╯ │
│ 50% │ ╭──╯ ← 涌现点 │
│ │ ╭──╯ (能力突然跳跃) │
│ 0% │──────────╯ │
│ └────────────────────────── 模型规模 │
│ 1B 10B 100B 1T │
└────────────────────────────────────────────────┘
已观测到的涌现能力:
├── 算术推理 (多位数加法)
├── 思维链 (Chain-of-Thought)
├── 指令遵循 (Instruction Following)
├── 代码生成
├── 多步推理
└── 世界知识问答
注: 涌现能力是否真的存在引发争论(Wei et al. vs Schaeffer et al.)
有研究认为用不同的评测指标可能不存在"突变"
"""
print(emergent_abilities)
9.3 范式对比¶
| 维度 | PLM 时代 (2018-2022) | LLM 时代 (2022~) |
|---|---|---|
| 典型模型 | BERT-base (110M) | GPT-4 (~1T?), LLaMA-70B |
| 使用方式 | Pre-train → Fine-tune | Pre-train → Instruct-tune → RLHF → Prompt |
| 适配方法 | 全参数微调 | LoRA/QLoRA, In-context Learning |
| 评估方式 | 固定 benchmark (GLUE/SQuAD) | 多维度 (MMLU/HumanEval/GSM8K) |
| 核心能力 | 特定任务表现 | 通用能力 + 涌现能力 |
| 计算需求 | 几张 GPU 可微调 | 预训练需要数千 GPU |
10. 代码实战: PLM 三架构对比实验¶
10.1 实验设计¶
"""
实验: 在文本分类任务上对比三种架构
数据集: 简单的情感分类 (正面/负面)
目标: 理解不同架构在NLU任务上的差异
"""
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import random
import numpy as np
# 设置随机种子
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
set_seed()
# 模拟数据集
class SentimentDataset(Dataset):
"""简单情感分类数据集"""
# 正面关键词和负面关键词
POS_WORDS = ["good", "great", "excellent", "wonderful", "amazing", "love", "best", "happy"]
NEG_WORDS = ["bad", "terrible", "awful", "horrible", "hate", "worst", "sad", "poor"]
NEUTRAL_WORDS = ["the", "is", "a", "this", "that", "it", "very", "really", "so", "and"]
def __init__(self, num_samples=500, seq_len=20, vocab_size=100):
self.data = []
all_words = self.POS_WORDS + self.NEG_WORDS + self.NEUTRAL_WORDS
self.word2id = {w: i+1 for i, w in enumerate(all_words)} # 0 for PAD
self.vocab_size = max(self.word2id.values()) + 1
for _ in range(num_samples):
label = random.choice([0, 1]) # 0=负面, 1=正面
words = []
for _ in range(seq_len):
if random.random() < 0.3: # 30%概率选情感词
if label == 1:
words.append(random.choice(self.POS_WORDS))
else:
words.append(random.choice(self.NEG_WORDS))
else:
words.append(random.choice(self.NEUTRAL_WORDS))
ids = [self.word2id.get(w, 0) for w in words]
self.data.append((torch.tensor(ids), label))
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx]
dataset = SentimentDataset(num_samples=1000, seq_len=20)
train_set, val_set = torch.utils.data.random_split(dataset, [800, 200])
train_loader = DataLoader(train_set, batch_size=32, shuffle=True)
val_loader = DataLoader(val_set, batch_size=32)
print(f"词表大小: {dataset.vocab_size}")
print(f"训练集: {len(train_set)}, 验证集: {len(val_set)}")
10.2 三种架构定义¶
class EncoderClassifier(nn.Module):
"""Encoder-only分类器(BERT风格)"""
def __init__(self, vocab_size, d_model=64, nhead=4, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4,
batch_first=True, dropout=0.1)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
self.classifier = nn.Linear(d_model, 2)
def forward(self, x):
h = self.encoder(self.embedding(x))
# 平均池化作为句子表示
h_mean = h.mean(dim=1)
return self.classifier(h_mean)
class DecoderClassifier(nn.Module):
"""Decoder-only分类器(GPT风格)"""
def __init__(self, vocab_size, d_model=64, nhead=4, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4,
batch_first=True, dropout=0.1)
self.decoder = nn.TransformerEncoder(encoder_layer, num_layers)
self.classifier = nn.Linear(d_model, 2)
def forward(self, x):
L = x.shape[1]
causal_mask = torch.triu(torch.ones(L, L, device=x.device), diagonal=1).bool()
h = self.decoder(self.embedding(x), mask=causal_mask)
# 取最后一个位置(因为因果注意力,最后位置看到全部信息)
h_last = h[:, -1, :]
return self.classifier(h_last)
class EncDecClassifier(nn.Module):
"""Encoder-Decoder分类器(T5风格)"""
def __init__(self, vocab_size, d_model=64, nhead=4, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
self.transformer = nn.Transformer(d_model, nhead, num_layers, num_layers,
d_model*4, batch_first=True, dropout=0.1)
self.classifier = nn.Linear(d_model, 2)
# 用一个可学习的query token
self.query = nn.Parameter(torch.randn(1, 1, d_model))
def forward(self, x):
B = x.shape[0]
src = self.embedding(x)
# 用query token做解码
tgt = self.query.expand(B, -1, -1)
h = self.transformer(src, tgt)
return self.classifier(h.squeeze(1))
10.3 训练与对比¶
def train_and_evaluate(model, name, train_loader, val_loader, epochs=20, lr=1e-3):
"""训练并评估模型"""
optimizer = optim.Adam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
history = {"train_loss": [], "val_acc": []}
for epoch in range(epochs):
# 训练
model.train()
total_loss = 0
for x, y in train_loader:
logits = model(x)
loss = criterion(logits, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
# 验证
model.eval()
correct = 0
total = 0
with torch.no_grad(): # 禁用梯度计算,节省内存(推理时使用)
for x, y in val_loader:
logits = model(x)
preds = logits.argmax(dim=-1)
correct += (preds == y).sum().item()
total += len(y)
acc = correct / total
avg_loss = total_loss / len(train_loader)
history["train_loss"].append(avg_loss)
history["val_acc"].append(acc)
if (epoch + 1) % 5 == 0:
print(f" [{name}] Epoch {epoch+1}: loss={avg_loss:.4f}, val_acc={acc:.4f}")
return history
# 运行对比实验
print("=" * 60)
print("三种PLM架构在文本分类任务上的对比实验")
print("=" * 60)
vocab_size = dataset.vocab_size
results = {}
for name, ModelClass in [
("Encoder-only (BERT)", EncoderClassifier),
("Decoder-only (GPT)", DecoderClassifier),
("Encoder-Decoder (T5)", EncDecClassifier)
]:
print(f"\n--- {name} ---")
model = ModelClass(vocab_size)
params = sum(p.numel() for p in model.parameters())
print(f" 参数量: {params:,}")
history = train_and_evaluate(model, name, train_loader, val_loader)
results[name] = history
print(f" 最终验证准确率: {max(history['val_acc']):.4f}")
print("\n" + "=" * 60)
print("实验结论:")
print(" 1. Encoder-only在分类任务上通常效果最好(双向注意力优势)")
print(" 2. Decoder-only仅用最后位置的表示,信息利用不够充分")
print(" 3. Encoder-Decoder参数更多,但在简单任务上可能过度复杂")
print(" 4. 在实际大规模模型中,Decoder-only通过规模弥补了单向局限")
练习题¶
练习 1 :词向量实验¶
实现 CBOW ( Continuous Bag of Words )模型,比较和 Skip-Gram 的区别。
💡 参考答案
CBOW 与 Skip-Gram 的核心区别:**CBOW 从上下文预测中心词,Skip-Gram 从中心词预测上下文**。class CBOW(nn.Module):
"""
CBOW (Continuous Bag of Words) 模型
给定上下文词,预测中心词
"""
def __init__(self, vocab_size, embedding_dim):
super().__init__()
self.in_embeddings = nn.Embedding(vocab_size, embedding_dim)
self.out_embeddings = nn.Embedding(vocab_size, embedding_dim)
nn.init.xavier_uniform_(self.in_embeddings.weight)
nn.init.xavier_uniform_(self.out_embeddings.weight)
def forward(self, context_ids, center_id, negative_ids):
"""
Args:
context_ids: [batch_size, window_size*2] 上下文词ID
center_id: [batch_size] 中心词ID
negative_ids: [batch_size, num_neg] 负样本ID
"""
# 上下文嵌入取平均
context_emb = self.in_embeddings(context_ids).mean(dim=1) # [B, D]
center_emb = self.out_embeddings(center_id) # [B, D]
negatives = self.out_embeddings(negative_ids) # [B, num_neg, D]
# 正样本得分
pos_score = torch.sum(context_emb * center_emb, dim=-1)
pos_loss = -torch.log(torch.sigmoid(pos_score) + 1e-8)
# 负样本得分
neg_score = torch.bmm(negatives, context_emb.unsqueeze(-1)).squeeze(-1)
neg_loss = -torch.log(torch.sigmoid(-neg_score) + 1e-8).sum(dim=-1)
return (pos_loss + neg_loss).mean()
# CBOW vs Skip-Gram 对比:
# 1. Skip-Gram: 中心词 → 预测上下文,适合低频词(每个中心词生成多个训练样本)
# 2. CBOW: 上下文 → 预测中心词,训练更快(上下文平均后更稳定)
# 3. 实践中 Skip-Gram 在语义任务上通常表现更好
练习 2 :预训练目标实验¶
分别实现 MLM 和 CLM 预训练目标,在小语料上训练,比较学到的表示质量。
💡 参考答案
class MLMObjective(nn.Module):
"""MLM (Masked Language Model) 预训练目标"""
def __init__(self, vocab_size, d_model=128, nhead=4, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4, batch_first=True)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
self.mlm_head = nn.Linear(d_model, vocab_size)
self.mask_token_id = vocab_size - 1 # [MASK] token
def forward(self, input_ids, mask_positions, labels):
"""
Args:
input_ids: [B, L] 已替换为[MASK]的输入
mask_positions: [B, M] 被mask的位置索引
labels: [B, M] 被mask位置的真实token ID
"""
h = self.encoder(self.embedding(input_ids))
# 只在mask位置预测
masked_h = h.gather(1, mask_positions.unsqueeze(-1).expand(-1, -1, h.size(-1)))
logits = self.mlm_head(masked_h) # [B, M, vocab]
loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))
return loss
class CLMObjective(nn.Module):
"""CLM (Causal Language Model) 预训练目标"""
def __init__(self, vocab_size, d_model=128, nhead=4, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4, batch_first=True)
self.decoder = nn.TransformerEncoder(encoder_layer, num_layers)
self.lm_head = nn.Linear(d_model, vocab_size)
def forward(self, input_ids, labels):
"""
Args:
input_ids: [B, L] 输入序列 (x_0, x_1, ..., x_{L-1})
labels: [B, L] 目标序列 (x_1, x_2, ..., x_L)
"""
L = input_ids.size(1)
causal_mask = torch.triu(torch.ones(L, L, device=input_ids.device), diagonal=1).bool()
h = self.decoder(self.embedding(input_ids), mask=causal_mask)
logits = self.lm_head(h) # [B, L, vocab]
loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))
return loss
# MLM vs CLM 对比:
# 1. MLM: 利用双向上下文,适合理解任务(分类、NER)
# 2. CLM: 只看左侧上下文,适合生成任务(文本续写、对话)
# 3. MLM 只在15%的token上计算损失,CLM在每个token上都计算损失
# 4. 在下游分类任务上,MLM通常优于CLM;在生成任务上,CLM更自然
练习 3 :架构分析¶
- 为什么 BERT 不适合文本生成任务?从注意力 mask 的角度解释。
- 如果要让 Decoder-only 做好分类任务,有哪些策略?(提示: pool 策略、特殊 token )
- Encoder-Decoder 架构在什么场景下仍然优于 Decoder-only ?
💡 参考答案
**问题 1:BERT 不适合文本生成** BERT 使用双向注意力(无因果 mask),每个位置可以看到所有其他位置(包括未来位置)。文本生成要求自回归:位置 $i$ 的生成只能依赖位置 $0, 1, \ldots, i-1$。如果用 BERT 做生成,模型在生成第 $i$ 个 token 时会"偷看"到第 $i+1, i+2, \ldots$ 的信息,这在推理时是不可用的(因为这些 token 还没生成)。虽然可以通过 iterative masking 等技巧绕过,但效率极低且效果不如 Decoder-only。 **问题 2:Decoder-only 做分类的策略** - **最后位置池化**:取序列最后一个 token 的隐状态做分类(因因果注意力,最后位置看到了全部信息) - **特殊 [CLS] token**:在序列开头添加可学习的分类 token,取其输出做分类 - **平均池化**:对所有位置的输出取平均作为句子表示 - **P-tuning / Prefix-tuning**:在输入前添加可学习的连续 prompt,用 prompt 的输出做分类 **问题 3:Encoder-Decoder 仍然优势的场景** - **机器翻译**:源语言和目标语言是两种不同的语言,Encoder-Decoder 天然分离编码和解码过程 - **长文本摘要**:编码器处理完整长文(双向注意力理解更全面),解码器生成摘要 - **代码生成/修改**:需要先理解完整输入代码,再生成修改后的代码 - **语音识别(ASR)**:编码器处理音频特征,解码器生成文本练习 4 :论文阅读¶
阅读以下论文,用自己的话总结关键贡献: 1. Word2Vec 原始论文:"Efficient Estimation of Word Representations in Vector Space" (Mikolov et al., 2013) 2. BERT 论文:"BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding" (Devlin et al., 2019) 3. GPT-3 论文:"Language Models are Few-Shot Learners" (Brown et al., 2020)
💡 参考答案(关键贡献摘要)
**Word2Vec (Mikolov et al., 2013)** - 提出 Skip-Gram 和 CBOW 两种高效词向量训练模型 - 引入 Negative Sampling 和 Hierarchical Softmax 加速训练 - 证明了分布式词向量能捕捉语义关系(如 `king - man + woman ≈ queen`) - 将词向量训练从数周缩短到数小时,使大规模词向量成为可能 **BERT (Devlin et al., 2019)** - 提出 MLM(Masked Language Model)预训练目标,实现真正的双向编码 - 引入 NSP(Next Sentence Prediction)任务,增强句子级理解 - 证明预训练-微调范式在 11 项 NLP 任务上全面超越之前的方法 - 统一了 NLP 下游任务的接口:只需在 BERT 上加一个简单分类头即可适配 **GPT-3 (Brown et al., 2020)** - 1750 亿参数,验证了规模法则(Scaling Laws)的威力 - 展示了 In-context Learning(上下文学习):无需更新参数即可适配新任务 - 在 Few-shot 设置下,GPT-3 在翻译、问答、摘要等任务上接近微调模型 - 开创了大模型"预训练 → 提示工程"的新范式,取代了"预训练 → 微调"📝 本章小结¶
| 知识点 | 掌握程度检查 |
|---|---|
| NLP 四个发展阶段 | 能否画出时间线并说出每阶段代表方法? |
| 文本表示演进 | 能否解释 One-hot→Word2Vec→ELMo→BERT 各解决什么问题? |
| 预训练-微调范式 | 能否解释为什么 Pre-train & Fine-tune 优于从头训练? |
| 三种 PLM 架构 | 能否画出 BERT/GPT/T5 的架构图并说明差异? |
| MLM vs CLM vs DAE | 能否解释三种预训练目标的工作方式和适用场景? |
| Decoder-only 主流原因 | 能否说出至少 3 个 Decoder-only 成为 LLM 主流的原因? |
| 涌现能力 | 能否解释什么是涌现能力并举例? |
🔗 后续学习路径¶
- 深入 Transformer 实现 → 03-手写 Transformer 完整实现
- 大模型预训练技术 → 03-大模型预训练
- 高效微调 → 01-高效微调技术
- 从零搭建 LLM → 07-从零搭建小型 LLM
📚 参考资料¶
- Mikolov et al. "Efficient Estimation of Word Representations in Vector Space" (2013) — Word2Vec
- Pennington et al. "GloVe: Global Vectors for Word Representation" (2014) — GloVe
- Peters et al. "Deep contextualized word representations" (2018) — ELMo
- Devlin et al. "BERT: Pre-training of Deep Bidirectional Transformers" (2019) — BERT
- Radford et al. "Language Models are Unsupervised Multitask Learners" (2019) — GPT-2
- Brown et al. "Language Models are Few-Shot Learners" (2020) — GPT-3
- Raffel et al. "Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer" (2020) — T5
- Lewis et al. "BART: Denoising Sequence-to-Sequence Pre-training" (2020) — BART
- Kaplan et al. "Scaling Laws for Neural Language Models" (2020) — Scaling Laws
- Wei et al. "Emergent Abilities of Large Language Models" (2022) — Emergent Abilities
最后更新日期: 2026-03-26 适用版本: LLM 学习教程 v2026