第8章:预训练语言模型(Pre-trained Language Models)
一、导读
1.1 本章学习目标
预训练语言模型是 NLP 的"GPT 时刻"。通过在大规模无标注语料上进行预训练,模型学习到丰富的语言知识,然后通过微调适配各种下游任务。这种"预训练-微调"范式彻底改变了 NLP 的研究和应用方式。
本章的核心学习目标包括:
- 理解预训练-微调范式:为什么先在大规模数据上预训练,再在小规模数据上微调?
- 掌握 BERT 的架构和预训练任务:MLM 和 NSP 的设计动机
- 掌握 GPT 系列的架构和预训练任务:自回归语言建模
- 理解微调策略:全参数微调、参数高效微调(LoRA、Adapter)
- 了解预训练模型的应用:文本分类、NER、问答、文本生成
- 理解规模定律:模型规模、数据规模与性能的关系
1.2 为什么本章如此重要
预训练语言模型是当代 NLP 的基石。在 BERT 之前,每个 NLP 任务都需要从头训练专门的模型。BERT 证明了"一个模型,多个任务"的可行性——在预训练阶段学习通用的语言表示,在微调阶段适配特定任务。
这种范式转变带来了深远影响:
- 降低了 NLP 的门槛:不需要大量标注数据和计算资源,直接使用预训练模型
- 提升了任务性能:在几乎所有 NLP 基准上都取得了突破性进展
- 统一了 NLP 任务:不同任务可以用相同的模型架构处理
- 催生了大语言模型:GPT-3、GPT-4 等模型展示了规模带来的涌现能力
1.3 核心问题引导
为什么"预训练-微调"比"从头训练"更有效?迁移学习的理论基础是什么?
BERT 的双向编码和 GPT 的单向编码有什么本质区别?各自适合什么任务?
MLM(掩码语言模型)的预训练目标为什么有效?它有什么局限性?
微调时应该冻结哪些层?全参数微调和参数高效微调各有什么优缺点?
规模定律告诉我们什么?模型越大一定越好吗?
预训练模型面临哪些挑战?幻觉、偏见、计算成本如何解决?
二、核心概念详解
8.1 迁移学习与预训练
8.1.1 迁移学习的动机
传统 NLP 的问题:每个任务需要大量标注数据从头训练。标注数据昂贵、稀缺,且不同任务之间无法共享知识。
迁移学习的思路:在大规模无标注数据上预训练,学习通用的语言表示;在下游任务的标注数据上微调,适配特定需求。
8.1.2 预训练-微调范式
预训练阶段:
大规模无标注语料 → 自监督学习 → 通用语言模型
微调阶段:
通用语言模型 + 小规模标注数据 → 任务特定模型8.2 BERT
8.2.1 架构
BERT (Bidirectional Encoder Representations from Transformers) 基于 Transformer 编码器:
- BERT-Base:12 层,768 维,12 头,1.1 亿参数
- BERT-Large:24 层,1024 维,16 头,3.4 亿参数
输入表示:Token Embedding + Segment Embedding + Position Embedding
8.2.2 预训练任务
掩码语言模型(MLM, Masked Language Model):随机遮盖输入中 15% 的 token,预测被遮盖的 token。
# MLM 示例
# 原始句子: "I love natural language processing"
# 遮盖后: "I love [MASK] language [MASK]"
# 目标: 预测 "natural" 和 "processing"下一句预测(NSP, Next Sentence Prediction):判断两个句子是否连续。
8.2.3 微调
微调时,在 BERT 顶部添加任务特定的输出层:
from transformers import BertForSequenceClassification, BertTokenizer
import torch
# 加载预训练模型(用于分类任务)
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
# 准备输入
text = "This movie is absolutely wonderful!"
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=128)
# 前向传播
outputs = model(**inputs)
logits = outputs.logits
prediction = torch.argmax(logits, dim=-1)
print(f"Prediction: {prediction.item()}") # 0 或 18.3 GPT 系列
8.3.1 架构
GPT (Generative Pre-trained Transformer) 基于 Transformer 解码器:
- 使用掩码自注意力(只能看到前面的 token)
- 自回归生成(逐个生成下一个 token)
8.3.2 预训练任务
自回归语言建模:
$$\mathcal{L} = -\sum_{i=1}^{N} \log P(x_i | x_1, \ldots, x_{i-1})$$
8.3.3 GPT-3 的涌现能力
GPT-3 (1750 亿参数) 展示了惊人的能力:
- 零样本学习:无需示例就能完成任务
- 少样本学习:通过提示中的几个示例学习
- 上下文学习:在推理时动态适应新任务
# GPT-3 风格的少样本提示
prompt = """
Classify the sentiment as positive or negative.
Text: I love this product!
Sentiment: positive
Text: This is terrible.
Sentiment: negative
Text: The quality is amazing.
Sentiment:"""
# GPT-3 会生成 "positive"8.4 BERT vs GPT
| 特性 | BERT | GPT |
|---|---|---|
| 架构 | Transformer 编码器 | Transformer 解码器 |
| 上下文 | 双向 | 单向(从左到右) |
| 预训练目标 | MLM + NSP | 自回归 LM |
| 适合任务 | 理解类(分类、NER、问答) | 生成类(文本生成、对话) |
| 微调方式 | 添加分类头 | 提示 + 生成 |
8.5 微调策略
8.5.1 全参数微调
更新模型的所有参数。效果好但计算成本高,且在小数据集上容易过拟合。
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=16,
learning_rate=2e-5,
weight_decay=0.01,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
)
trainer.train()8.5.2 参数高效微调
Adapter:在 Transformer 层中插入小型适配模块,只训练适配模块。
LoRA(Low-Rank Adaptation):在权重矩阵中添加低秩分解,只训练低秩矩阵。
$$W' = W + \Delta W = W + BA$$
其中 $B \in \mathbb{R}^{d \times r}$,$A \in \mathbb{R}^{r \times d}$,$r \ll d$。
from peft import LoraConfig, get_peft_model
# 配置 LoRA
lora_config = LoraConfig(
r=8, # 低秩维度
lora_alpha=32, # 缩放因子
target_modules=["query", "value"], # 应用 LoRA 的模块
lora_dropout=0.1,
task_type="SEQ_CLS",
)
# 获取 LoRA 模型
model = get_peft_model(base_model, lora_config)
model.print_trainable_parameters()
# 可训练参数: 0.1% of total8.6 规模定律(Scaling Laws)
Kaplan et al. (2020) 发现,模型性能(损失)与模型参数量 $N$、数据量 $D$、计算量 $C$ 之间存在幂律关系:
$$L(N) \propto N^{-\alpha_N}$$
这意味着可以通过增加规模来可预测地提升性能。但 Chinchilla (Hoffmann et al., 2022) 指出,模型参数和数据量应同步增长,否则浪费计算资源。
三、实战案例
3.1 使用 BERT 进行文本分类
from transformers import BertTokenizer, BertForSequenceClassification
from transformers import Trainer, TrainingArguments
from torch.utils.data import Dataset
import torch
class SentimentDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_len=128):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
encoding = self.tokenizer(
self.texts[idx],
max_length=self.max_len,
padding="max_length",
truncation=True,
return_tensors="pt",
)
return {
"input_ids": encoding["input_ids"].squeeze(),
"attention_mask": encoding["attention_mask"].squeeze(),
"labels": torch.tensor(self.labels[idx], dtype=torch.long),
}
# 数据
texts = ["这部电影太好看了", "非常失望,浪费钱", "剧情精彩,推荐", "无聊至极"]
labels = [1, 0, 1, 0]
# 模型
tokenizer = BertTokenizer.from_pretrained("bert-base-chinese")
model = BertForSequenceClassification.from_pretrained("bert-base-chinese", num_labels=2)
# 训练
dataset = SentimentDataset(texts, labels, tokenizer)
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=2,
learning_rate=2e-5,
)
trainer = Trainer(model=model, args=training_args, train_dataset=dataset)
trainer.train()
# 推理
test_text = "这个产品真的很棒"
inputs = tokenizer(test_text, return_tensors="pt", max_length=128, truncation=True, padding="max_length")
with torch.no_grad():
outputs = model(**inputs)
pred = torch.argmax(outputs.logits, dim=-1)
print(f"'{test_text}' → {'正面' if pred.item() == 1 else '负面'}")3.2 使用 GPT-2 进行文本生成
from transformers import GPT2LMHeadModel, GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
model = GPT2LMHeadModel.from_pretrained("gpt2")
prompt = "Natural language processing is"
inputs = tokenizer(prompt, return_tensors="pt")
# 生成文本
outputs = model.generate(
**inputs,
max_new_tokens=50,
temperature=0.7,
top_k=50,
top_p=0.95,
do_sample=True,
no_repeat_ngram_size=2,
)
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(generated_text)3.3 使用 LoRA 进行高效微调
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
# 基础模型
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
base_model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
# LoRA 配置
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS,
r=16,
lora_alpha=32,
target_modules=["query", "value"],
lora_dropout=0.1,
)
# 获取 LoRA 模型
model = get_peft_model(base_model, lora_config)
print(f"Trainable parameters: {model.num_parameters(only_trainable=True) / 1e6:.2f}M")
print(f"Total parameters: {model.num_parameters() / 1e6:.2f}M")四、本章小结
本章系统讲解了预训练语言模型:
预训练-微调范式:在大规模无标注数据上预训练,在小规模标注数据上微调。迁移学习降低了 NLP 的门槛。
BERT:基于 Transformer 编码器,双向上下文,MLM + NSP 预训练。适合理解类任务。
GPT 系列:基于 Transformer 解码器,单向自回归生成。适合生成类任务。GPT-3 展示了涌现能力和上下文学习。
微调策略:全参数微调效果好但成本高;LoRA 等参数高效方法大幅降低微调成本。
规模定律:性能随规模可预测地提升,但参数和数据需要同步增长。
预训练语言模型是当代 NLP 的核心技术。下一章我们将学习序列标注任务,这是 NLP 中最基础的任务之一。
五、思考与练习
思考题
BERT 的 MLM 预训练有什么局限性?为什么需要 15% 的遮盖率?
比较全参数微调和 LoRA 微调的优缺点。在什么场景下你会选择 LoRA?
GPT-3 的上下文学习是如何工作的?为什么模型不需要参数更新就能适应新任务?
预训练模型的幻觉问题如何解决?RAG 是一种有效的方案吗?
编程练习
使用 BERT 在 SST-2 数据集上进行情感分析微调。比较不同学习率和训练轮数的效果。
使用 LoRA 微调 BERT 进行 NER 任务。比较 LoRA 不同秩 r 对性能的影响。
使用 GPT-2 进行条件文本生成。实验不同的解码策略(贪心、束搜索、采样)对生成质量的影响。