📖 第10章:预训练语言模型¶
学习时间:10小时 难度星级:⭐⭐⭐⭐⭐ 前置知识:Transformer、文本分类、序列标注 学习目标:深入理解从ELMo到BERT的预训练范式,掌握Hugging Face Transformers实战
📋 目录¶
- 1. 预训练语言模型概述
- 2. ELMo
- 3. GPT系列
- 4. BERT详解
- 5. BERT变体
- 6. 中文预训练模型
- 7. Hugging Face Transformers实战
- 8. 微调最佳实践
- 9. 面试要点
- 10. 练习题
1. 预训练语言模型概述¶
1.1 预训练的演进历程¶
预训练语言模型发展时间线:
2013 ─ Word2Vec (静态词向量)
│
2014 ─ GloVe (全局词向量,EMNLP 2014)
│
2018 ─ ELMo (动态词向量,BiLSTM)
│
2018 ─ GPT-1 (Transformer Decoder,单向)
│
2018 ─ BERT (Transformer Encoder,双向) ← 里程碑
│
2019 ─ GPT-2 / XLNet / RoBERTa / ALBERT
│
2019 ─ T5 (Text-to-Text统一框架)
│
2020 ─ GPT-3 (175B参数,In-Context Learning)
│
2022 ─ ChatGPT (RLHF对齐)
│
2023 ─ GPT-4 / LLaMA / Claude (大模型时代)
│
2024 ─ GPT-4o / Claude 3.5 / DeepSeek (多模态+开源)
1.2 预训练范式¶
pretraining_paradigms = {
"自编码器(AE)": {
"代表": "BERT",
"方式": "掩码语言模型(MLM) - 双向",
"训练": "随机遮盖15%的token,预测被遮盖的token",
"优点": "可以看到上下文,适合理解任务",
"缺点": "不能直接生成文本",
},
"自回归(AR)": {
"代表": "GPT",
"方式": "因果语言模型(CLM) - 单向(左到右)",
"训练": "根据前文预测下一个token",
"优点": "天然适合生成文本",
"缺点": "只能看到左边,可能损失双向信息",
},
"编码器-解码器(Enc-Dec)": {
"代表": "T5, BART",
"方式": "文本到文本的统一框架",
"训练": "将所有任务转化为文本生成",
"优点": "统一框架,灵活性高",
},
}
for paradigm, info in pretraining_paradigms.items():
print(f"\n📌 {paradigm}: {info['代表']}")
print(f" 训练方式: {info['方式']}")
print(f" 优点: {info['优点']}")
2. ELMo¶
2.1 ELMo原理¶
ELMo (Embeddings from Language Models):
输入: "苹果 手机 很 好用"
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────┐
│ 字符级CNN嵌入层 │
└─────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────┐
│ → BiLSTM Layer 1 → │ 前向+后向
│ ← BiLSTM Layer 1 ← │
└─────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────┐
│ → BiLSTM Layer 2 → │
│ ← BiLSTM Layer 2 ← │
└─────────────────────────┘
ELMo表示 = γ(s₀·h₀ + s₁·h₁ + s₂·h₂)
h₀: 字符嵌入层输出
h₁: BiLSTM第1层输出
h₂: BiLSTM第2层输出
s₀,s₁,s₂: 可学习的权重
γ: 缩放因子
关键创新:上下文相关的词向量
"苹果手机" 中的"苹果" ≠ "苹果好吃" 中的"苹果"
import torch
import torch.nn as nn
class SimpleELMo(nn.Module): # 继承nn.Module定义网络层
"""简化版ELMo"""
def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_layers=2):
super().__init__() # super()调用父类方法
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.embed_proj = nn.Linear(embed_dim, hidden_dim) # 将embedding投影到hidden_dim
self.forward_lstms = nn.ModuleList([
nn.LSTM(embed_dim if i == 0 else hidden_dim, hidden_dim, batch_first=True)
for i in range(num_layers)
])
self.backward_lstms = nn.ModuleList([
nn.LSTM(embed_dim if i == 0 else hidden_dim, hidden_dim, batch_first=True)
for i in range(num_layers)
])
# 各层权重(可学习)
self.layer_weights = nn.Parameter(torch.ones(num_layers + 1) / (num_layers + 1))
self.gamma = nn.Parameter(torch.tensor(1.0))
def forward(self, x):
embedded = self.embedding(x)
all_layers = [self.embed_proj(embedded)] # 投影到hidden_dim以匹配后续层维度
fwd_input = embedded
bwd_input = torch.flip(embedded, [1])
for fwd_lstm, bwd_lstm in zip(self.forward_lstms, self.backward_lstms): # zip按位置配对
fwd_out, _ = fwd_lstm(fwd_input)
bwd_out, _ = bwd_lstm(bwd_input)
bwd_out = torch.flip(bwd_out, [1])
combined = fwd_out + bwd_out
all_layers.append(combined)
fwd_input = fwd_out
bwd_input = torch.flip(bwd_out, [1])
# 加权求和
weights = torch.softmax(self.layer_weights, dim=0)
elmo_repr = self.gamma * sum(w * layer for w, layer in zip(weights, all_layers))
return elmo_repr
model = SimpleELMo(vocab_size=5000)
x = torch.randint(0, 5000, (2, 10))
output = model(x)
print(f"ELMo输出: {output.shape}") # (2, 10, 256)
3. GPT系列¶
3.1 GPT-1 → GPT-4 演进¶
gpt_evolution = {
"GPT-1 (2018)": {
"参数量": "1.17亿",
"训练数据": "BooksCorpus (8亿词)",
"特点": "首次证明Transformer预训练+微调的有效性",
},
"GPT-2 (2019)": {
"参数量": "15亿",
"训练数据": "WebText (40GB)",
"特点": "Zero-shot能力,文本生成质量飞跃",
},
"GPT-3 (2020)": {
"参数量": "1750亿",
"训练数据": "CommonCrawl等 (570GB)",
"特点": "In-Context Learning,Few-shot能力",
},
"GPT-4 (2023)": {
"参数量": "未公开",
"训练数据": "未公开",
"特点": "多模态,推理能力大幅提升",
},
}
for name, info in gpt_evolution.items():
print(f"\n🔹 {name}")
for k, v in info.items():
print(f" {k}: {v}")
3.2 GPT的训练目标¶
"""
GPT训练目标:因果语言模型 (CLM)
给定前面的token,预测下一个token:
L = -∑ log P(xᵢ | x₁, x₂, ..., xᵢ₋₁)
示例:
输入: [BOS] 我 爱 自然 语言
标签: 我 爱 自然 语言 处理
每个位置只能看到自己和左边的token(因果掩码)
"""
import torch
import torch.nn as nn
import math
class CausalLanguageModel(nn.Module):
"""因果语言模型"""
def __init__(self, vocab_size, d_model=256, num_heads=4, num_layers=4):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.pos_embedding = nn.Embedding(512, d_model)
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model, nhead=num_heads, dim_feedforward=d_model*4,
batch_first=True
)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
self.lm_head = nn.Linear(d_model, vocab_size)
def forward(self, x):
seq_len = x.size(1)
positions = torch.arange(seq_len, device=x.device).unsqueeze(0) # unsqueeze增加一个维度
# 因果掩码
causal_mask = nn.Transformer.generate_square_subsequent_mask(seq_len).to(x.device)
h = self.embedding(x) + self.pos_embedding(positions)
h = self.transformer(h, mask=causal_mask)
logits = self.lm_head(h)
return logits
clm = CausalLanguageModel(vocab_size=5000)
x = torch.randint(0, 5000, (2, 20))
logits = clm(x)
print(f"CLM输出: {logits.shape}") # (2, 20, 5000)
4. BERT详解¶
4.1 BERT架构¶
BERT (Bidirectional Encoder Representations from Transformers):
输入: [CLS] 我 爱 [MASK] 语言 处理 [SEP]
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌──────────────────────────────────────────────┐
│ Token Embedding + Segment Embedding + PE │
└──────────────────────────────────────────────┘
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌──────────────────────────────────────────────┐
│ Transformer Encoder × 12 │
│ ┌──────────────────────────────────────┐ │
│ │ Multi-Head Self-Attention │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ Feed-Forward Network │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
h[CLS] h₁ h₂ h₃ h₄ h₅ h[SEP]
h[CLS] → 文本分类 (NSP、文本对任务)
h₃ → MLM预测 "自然"
4.2 BERT预训练任务¶
"""
BERT两个预训练任务:
1. MLM (Masked Language Model):
- 随机遮盖15%的token
- 其中80%替换为[MASK],10%替换为随机词,10%保持不变
- 预测被遮盖的token
**80%-10%-10%策略的设计原因:**
- 80%替换为[MASK]:主要训练方式,让模型学习上下文理解
- 10%替换为随机词:迫使模型学习鲁棒性,不能完全依赖[MASK]标记
- 10%保持不变:**关键设计**!避免预训练与微调之间的mismatch
* 预训练时模型会看到[MASK]标记,但微调/实际使用时没有
* 如果100%都替换为[MASK],模型可能过度依赖这个特殊标记
* 保留10%原始token让模型学会对任意输入都能产生有意义的表示
* 这是一种"去噪自编码器"的思想,提升模型的泛化能力
2. NSP (Next Sentence Prediction):
- 输入两个句子A和B
- 50%的概率B是A的真实下一句(IsNext)
- 50%的概率B是随机句子(NotNext)
- 预测B是否是A的下一句
"""
class BERTPretraining:
"""BERT预训练数据准备"""
def __init__(self, tokenizer_vocab_size=21128, mask_prob=0.15):
self.vocab_size = tokenizer_vocab_size
self.mask_prob = mask_prob
self.mask_token_id = 103 # [MASK]
self.cls_token_id = 101 # [CLS]
self.sep_token_id = 102 # [SEP]
def create_mlm_data(self, token_ids):
"""创建MLM训练数据"""
import random
masked_ids = list(token_ids)
labels = [-100] * len(token_ids) # -100表示不计算loss
for i in range(len(token_ids)):
if token_ids[i] in [self.cls_token_id, self.sep_token_id, 0]:
continue
if random.random() < self.mask_prob:
labels[i] = token_ids[i]
rand = random.random()
if rand < 0.8:
masked_ids[i] = self.mask_token_id # 80% → [MASK]
elif rand < 0.9:
masked_ids[i] = random.randint(1, self.vocab_size - 1) # 10% → random
# else: 10% → keep original
return masked_ids, labels
def create_nsp_data(self, sent_a_ids, sent_b_ids, is_next):
"""创建NSP训练数据"""
input_ids = [self.cls_token_id] + sent_a_ids + [self.sep_token_id] + sent_b_ids + [self.sep_token_id]
segment_ids = [0] * (len(sent_a_ids) + 2) + [1] * (len(sent_b_ids) + 1)
return input_ids, segment_ids, 1 if is_next else 0
# 演示
bert_pre = BERTPretraining()
# MLM示例
original = [101, 2769, 4263, 5765, 4197, 6427, 6404, 1164, 4415, 102]
masked, labels = bert_pre.create_mlm_data(original)
print("MLM示例:")
print(f" 原始: {original}")
print(f" 掩码: {masked}")
print(f" 标签: {labels}")
4.3 BERT参数详情¶
bert_configs = {
"BERT-Base": {
"层数(L)": 12,
"隐藏维度(H)": 768,
"注意力头(A)": 12,
"参数量": "110M",
"说明": "最常用的标准版本",
},
"BERT-Large": {
"层数(L)": 24,
"隐藏维度(H)": 1024,
"注意力头(A)": 16,
"参数量": "340M",
"说明": "更大更强,但需要更多算力",
},
}
for name, config in bert_configs.items():
print(f"\n📌 {name}:")
for k, v in config.items():
print(f" {k}: {v}")
# 参数量计算
def count_bert_params(L=12, H=768, A=12, V=30522):
"""计算BERT参数量"""
# Embedding层
token_emb = V * H
pos_emb = 512 * H
seg_emb = 2 * H
emb_layer_norm = 2 * H
emb_total = token_emb + pos_emb + seg_emb + emb_layer_norm
# 每层Transformer
# Self-Attention: Q, K, V各有W(H×H)+b(H),输出投影W(H×H)+b(H)
attn = 4 * (H * H + H)
# LayerNorm: 2 * H
attn_ln = 2 * H
# FFN: W1(H×4H)+b1(4H) + W2(4H×H)+b2(H)
ffn = H * 4 * H + 4 * H + 4 * H * H + H
# LayerNorm: 2 * H
ffn_ln = 2 * H
per_layer = attn + attn_ln + ffn + ffn_ln
all_layers = L * per_layer
# 输出层
pooler = H * H + H
total = emb_total + all_layers + pooler
return total
params = count_bert_params()
print(f"\nBERT-Base参数量计算: {params:,} ≈ {params/1e6:.0f}M")
5. BERT变体¶
5.1 主要变体对比¶
bert_variants = {
"RoBERTa (2019)": {
"改进": "去掉NSP、动态Masking、更大batch和数据、更长训练",
"效果": "比BERT平均提升2-3个点",
},
"ALBERT (2019)": {
"改进": "嵌入矩阵分解、跨层参数共享",
"效果": "参数量减少80%+,但训练速度不一定快",
},
"XLNet (2019)": {
"改进": "排列语言模型,融合AR和AE的优点",
"效果": "在多个任务上超过BERT",
},
"ELECTRA (2020)": {
"改进": "替换词检测(RTD)代替MLM,训练效率更高",
"效果": "小模型效果显著提升",
},
"DeBERTa (2020)": {
"改进": "解耦注意力(内容+位置分别计算)",
"效果": "SuperGLUE首次超过人类",
},
"T5 (2019)": {
"改进": "所有任务统一为文本到文本格式",
"效果": "极其灵活的统一框架",
},
}
print("BERT变体对比:")
for name, info in bert_variants.items():
print(f"\n 🔹 {name}")
print(f" 改进: {info['改进']}")
print(f" 效果: {info['效果']}")
5.2 RoBERTa的关键改进¶
"""
RoBERTa vs BERT 的关键差异:
1. 去掉NSP任务
- BERT: MLM + NSP
- RoBERTa: 只用MLM(实验证明NSP伤效果)
2. 动态Masking
- BERT: 数据预处理时做静态masking(每个epoch相同)
- RoBERTa: 每次取数据时动态masking(增加数据多样性)
3. 更大的Batch Size
- BERT: 256
- RoBERTa: 8192
4. 更多训练数据
- BERT: 16GB
- RoBERTa: 160GB
5. 更长的训练时间
- BERT: 1M steps
- RoBERTa: 500K steps(但batch更大,等效更多)
"""
print("RoBERTa要点已展示在注释中")
6. 中文预训练模型¶
chinese_models = {
"BERT-Base-Chinese": {
"来源": "Google",
"词汇量": 21128,
"特点": "字级别分词,中文NLP基础模型",
"HF名称": "bert-base-chinese",
},
"ERNIE (百度)": {
"来源": "百度",
"特点": "知识增强、实体级别Masking",
"HF名称": "nghuyong/ernie-3.0-base-zh",
},
"MacBERT (哈工大)": {
"来源": "哈工大讯飞",
"特点": "用近义词替换代替[MASK],缓解预训练-微调差距",
"HF名称": "hfl/chinese-macbert-base",
},
"RoBERTa-wwm-ext (哈工大)": {
"来源": "哈工大讯飞",
"特点": "全词Masking + 更多数据",
"HF名称": "hfl/chinese-roberta-wwm-ext",
},
"ChatGLM": {
"来源": "清华智谱",
"特点": "中文对话大模型",
"HF名称": "THUDM/chatglm3-6b",
},
}
print("常用中文预训练模型:")
for name, info in chinese_models.items():
print(f"\n 📌 {name} ({info['来源']})")
print(f" 特点: {info['特点']}")
print(f" Hugging Face: {info['HF名称']}")
7. Hugging Face Transformers实战¶
7.1 基本使用¶
from transformers import AutoTokenizer, AutoModel, AutoModelForSequenceClassification
import torch
# ==================
# 1. 加载模型和分词器
# ==================
model_name = "bert-base-chinese"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# 分词
text = "自然语言处理是人工智能的重要分支"
tokens = tokenizer(text, return_tensors="pt")
print(f"Token IDs: {tokens['input_ids']}")
print(f"Tokens: {tokenizer.convert_ids_to_tokens(tokens['input_ids'][0])}")
print(f"Attention Mask: {tokens['attention_mask']}")
# 获取encoder输出
with torch.no_grad(): # 禁用梯度计算,节省内存
outputs = model(**tokens)
print(f"\nLast hidden state: {outputs.last_hidden_state.shape}")
print(f"Pooler output: {outputs.pooler_output.shape}")
# ==================
# 2. 文本分类
# ==================
clf_model = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=3
)
with torch.no_grad():
outputs = clf_model(**tokens)
logits = outputs.logits
probs = torch.softmax(logits, dim=-1)
print(f"\n分类logits: {logits}")
print(f"分类概率: {probs}")
7.2 Pipeline API¶
from transformers import pipeline
# 情感分析
# classifier = pipeline("sentiment-analysis", model="uer/roberta-base-finetuned-chinanews-chinese")
# 命名实体识别
# ner = pipeline("ner", model="ckiplab/bert-base-chinese-ner")
# 问答
# qa = pipeline("question-answering", model="uer/roberta-base-chinese-extractive-qa")
# 文本生成
# generator = pipeline("text-generation", model="uer/gpt2-chinese-cluecorpussmall")
# 零样本分类
# zero_shot = pipeline("zero-shot-classification", model="MoritzLaurer/mDeBERTa-v3-base-mnli-xnli")
print("Hugging Face Pipeline示例:")
print(" classifier = pipeline('sentiment-analysis')")
print(" result = classifier('这个电影很好看')")
print(" # 输出: [{'label': 'POSITIVE', 'score': 0.99}]")
7.3 自定义微调¶
from transformers import Trainer, TrainingArguments
import torch
class FineTuneExample:
"""微调示例框架"""
@staticmethod # @staticmethod不需要实例即可调用
def prepare_dataset(texts, labels, tokenizer, max_len=128):
"""准备数据集"""
class TextDataset(torch.utils.data.Dataset):
def __init__(self, texts, labels, tokenizer, max_len):
self.encodings = tokenizer(texts, truncation=True,
padding=True, max_length=max_len,
return_tensors='pt')
self.labels = torch.tensor(labels)
def __getitem__(self, idx): # __getitem__定义索引访问行为
item = {k: v[idx] for k, v in self.encodings.items()}
item['labels'] = self.labels[idx]
return item
def __len__(self): # __len__定义len()行为
return len(self.labels)
return TextDataset(texts, labels, tokenizer, max_len)
@staticmethod
def get_training_args(output_dir="./results", epochs=3, batch_size=16, lr=2e-5):
"""获取训练参数"""
return TrainingArguments(
output_dir=output_dir,
num_train_epochs=epochs,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
learning_rate=lr,
weight_decay=0.01,
warmup_ratio=0.1,
logging_steps=10,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)
print("微调框架代码已定义")
print("使用: Trainer(model=model, args=args, train_dataset=train_ds, eval_dataset=eval_ds).train()")
8. 微调最佳实践¶
fine_tuning_tips = {
"学习率": {
"推荐值": "1e-5 ~ 5e-5",
"说明": "太大会破坏预训练参数,太小收敛慢",
"技巧": "底层学习率更小(Layer-wise LR Decay)",
},
"Batch Size": {
"推荐值": "16 或 32",
"说明": "显存允许下尽量大",
"技巧": "梯度累积模拟大batch",
},
"Epochs": {
"推荐值": "2-5",
"说明": "过多会过拟合(数据少时尤其注意)",
"技巧": "使用Early Stopping",
},
"Warmup": {
"推荐值": "总步数的6%-10%",
"说明": "防止训练初期梯度过大",
},
"对抗训练": {
"推荐值": "FGM或PGD",
"说明": "对输入嵌入加扰动,提升鲁棒性",
"效果": "通常提升0.5-1.0 F1",
},
"R-Drop": {
"说明": "对同一输入做两次前向传播,最小化输出分布的KL散度",
"效果": "有效的正则化方法",
},
}
print("BERT微调最佳实践:")
for key, info in fine_tuning_tips.items():
print(f"\n 📌 {key}:")
for k, v in info.items():
print(f" {k}: {v}")
对抗训练FGM实现¶
class FGM:
"""Fast Gradient Method 对抗训练"""
def __init__(self, model, epsilon=1.0, emb_name='word_embeddings'):
self.model = model
self.epsilon = epsilon
self.emb_name = emb_name
self.backup = {}
def attack(self):
"""添加扰动"""
for name, param in self.model.named_parameters():
if param.requires_grad and self.emb_name in name:
self.backup[name] = param.data.clone()
norm = torch.norm(param.grad)
if norm != 0 and not torch.isnan(norm):
r_at = self.epsilon * param.grad / norm
param.data.add_(r_at)
def restore(self):
"""恢复原始参数"""
for name, param in self.model.named_parameters():
if param.requires_grad and self.emb_name in name:
assert name in self.backup # assert断言
param.data = self.backup[name]
self.backup = {}
"""
对抗训练流程:
1. 正常前向+反向
2. fgm.attack() # 加扰动
3. 扰动后再次前向+反向
4. fgm.restore() # 恢复参数
5. optimizer.step() # 更新参数
"""
print("FGM对抗训练类已定义")
9. 面试要点¶
🔑 面试高频考点
考点1:BERT和GPT的核心区别?¶
✅ 标准答案要点:
BERT GPT
架构: Transformer Encoder Transformer Decoder
方向: 双向(看所有位置) 单向(只看左边)
预训练: MLM + NSP CLM(自回归)
适合任务: 理解任务 生成任务
输入格式: [CLS] A [SEP] B A → 续写B
核心差异在注意力掩码:
- BERT: 全连接(可以看到所有position)
- GPT: 因果掩码(只能看到左边的position)
考点2:为什么BERT用15%的Masking比例?¶
✅ 标准答案要点:
- 太少(如5%):训练效率低,每个batch学到的信息少
- 太多(如50%):上下文信息损失太大,预测难度太高
- 15%是在训练效率和上下文信息之间的经验平衡点
- 如果更多token被mask,剩余的上下文不足以准确预测
额外知识:
- RoBERTa证明了动态Masking优于静态Masking
- SpanBERT证明连续span masking效果更好
- ERNIE用实体/短语级别的masking
考点3:BERT的位置编码和Transformer的区别?¶
✅ 标准答案要点:
Transformer原文:
- 使用固定的正弦/余弦位置编码
- pos_i = sin(pos/10000^(2i/d))
BERT:
- 使用可学习的位置编码(nn.Embedding(512, 768))
- 最大序列长度512
二者区别:
- 固定编码可以处理超过训练长度的序列(外推性好)
- 可学习编码在训练范围内效果更好
- RoPE(旋转位置编码)结合了两者优点,被大模型广泛使用
考点4:如何选择中文预训练模型?¶
✅ 标准答案要点:
通用场景:
- 首选 chinese-roberta-wwm-ext(哈工大)
- 其次 bert-base-chinese(Google)
分类/理解任务:
- MacBERT > RoBERTa-wwm > BERT-base
长文本:
- Longformer-Chinese
- LED-base-Chinese
生成任务:
- GPT2-Chinese
- ChatGLM / Qwen
NER任务:
- 全词Masking模型效果更好(wwm系列)
10. 练习题¶
📝 基础题¶
- 详细说明BERT的MLM和NSP预训练任务的细节。
答案:MLM(Masked Language Model):随机选择输入token的15%进行遮蔽,其中80%替换为[MASK]、10%替换为随机token、10%保持不变,模型预测被遮蔽的原始token。这种设计缓解了[MASK]在预训练与微调间的不一致。MLM使BERT学习双向上下文表示。NSP(Next Sentence Prediction):输入[CLS]A[SEP]B,50%B是A的真实下一句(正例),50%随机替换(负例),对[CLS]做二分类。NSP旨在理解句间关系,但后续研究(RoBERTa)发现去除NSP后效果反而更好。
- 对比BERT、RoBERTa、ALBERT的异同。
答案:共同点:都基于Transformer Encoder,使用MLM预训练。BERT:MLM+NSP,静态Masking,16GB训练数据。RoBERTa:①去除NSP;②动态Masking(每epoch重新生成掩码);③更大数据(160GB)、更大batch、更长训练;效果显著优于BERT,证明BERT"训练不充分"。ALBERT:①嵌入矩阵分解(V×H→V×E+E×H)减少参数;②跨层参数共享,参数量仅BERT的1/10;③用SOP(Sentence Order Prediction)替代NSP。ALBERT参数少但推理速度不变(层数相同),适合参数敏感场景。
💻 编程题¶
- 使用Hugging Face完成一个中文文本分类任务的微调。
- 实现一个FGM对抗训练的完整训练循环。
- 使用BERT对句子进行编码,计算语义相似度。
🔬 思考题¶
- 在资源有限的情况下,如何选择和使用预训练模型?何时用BERT微调,何时用大模型零样本/少样本?
答案:选BERT微调:有几百到几千条标注数据;需低延迟高并发推理;计算资源有限(单卡可训练);任务明确类别固定;需本地部署数据不出域。选大模型零样本/少样本:无标注数据或极少(<50条);任务定义灵活或频繁变化;快速验证想法;需要复杂推理或世界知识。建议流程:先用大模型零样本验证可行性 → 用大模型标注一批数据 → 用小模型微调上线 → 持续迭代。蒸馏(用大模型输出训练小模型)也是高性价比策略。
✅ 自我检查清单¶
□ 我能说清BERT和GPT的区别
□ 我理解MLM和NSP的训练目标
□ 我知道BERT的参数量是怎么算的
□ 我了解主要的BERT变体(RoBERTa/ALBERT/XLNet)
□ 我能用Hugging Face进行模型微调
□ 我知道微调的关键超参数
□ 我完成了至少3道练习题
📚 延伸阅读¶
- BERT论文: Pre-training of Deep Bidirectional Transformers
- RoBERTa论文
- Hugging Face Transformers文档
- 中文BERT全家桶
下一篇:11-大模型时代的NLP — 从GPT-3到ChatGPT的范式变革