mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
608 字
2 分钟
BERT 与双向预训练:NLP 预训练模型的崛起
2025-03-21

2018 年是 NLP 预训练模型元年。

这一年,OpenAI 发布了 GPT-1,Google 发布了 BERT。两条技术路线从此分道扬镳:GPT 选择单向自回归生成,BERT 选择双向编码理解。

BERT 证明了双向预训练的强大力量,深刻影响了整个 NLP 领域。

本文要点#

  • BERT 论文背景与动机
  • 双向 Transformer 架构
  • 掩码语言模型(MLM)
  • 下一句预测(NSP)
  • 预训练+微调范式
  • BERT vs GPT 对比

一、论文背景#

1.1 研究动机#

flowchart TB subgraph BERT解决的问题 A[2018 年前的困境] A --> A1[预训练模型主要是单向的(GPT、ELMo)] A --> A2[单向限制了对上下文的理解] A --> A3[任务特定架构需要从头设计] B[BERT 的创新] B --> B1[双向上下文理解] B --> B2[统一的预训练+微调范式] B --> B3[刷新 11 项 NLP 任务 SOTA] C[核心洞察] C --> C1["「语言理解需要同时看到左右两侧的上下文」"] end

1.2 论文信息#

论文:BERT: Pre-training of Deep Bidirectional Transformers
for Language Understanding
作者:Jacob Devlin, Ming-Wei Chang, Kenton Lee, Kristina Toutanova
机构:Google AI Language
发表:NAACL 2019(2018 年 10 月预印)
引用:10万+ 次

二、双向 Transformer 架构#

2.1 BERT vs GPT 架构对比#

flowchart TB subgraph GPT 单向 A1["Token 1"] --> A2["Token 2"] A2 --> A3["Token 3"] A3 --> A4["Token 4"] B1["只能看到左边"] --> B2["自回归生成"] end subgraph BERT 双向 C1["Token 1"] C2["Token 2"] C3["Token 3"] C4["Token 4"] C1 <--> C2 <--> C3 <--> C4 D1["可以看到左右两边"] --> D2["深度理解"] end
| 维度 | GPT | BERT |
|---------------|-----------------|---------------------------|
| Transformer | Decoder only | Encoder only |
| 注意力 | 单向(因果掩码)| 双向(无掩码) |
| 训练目标 | 语言模型 | MLM + NSP |
| 适用任务 | 生成 | 理解 |
| 输出 | 自回归生成 | 编码表示 |

2.2 BERT 模型规格#

flowchart TB subgraph BERT模型配置 A[BERT-Base] A --> A1[参数量:110M] A --> A2[层数:L = 12] A --> A3[隐藏维度:H = 768] A --> A4[注意力头:A = 12] B[BERT-Large] B --> B1[参数量:340M] B --> B2[层数:L = 24] B --> B3[隐藏维度:H = 1024] B --> B4[注意力头:A = 16] C[输入] C --> C1[最大长度:512 tokens] C --> C2[词表大小:30,000] C --> C3[训练数据:BookCorpus + Wikipedia(约 33 亿词)] end

三、掩码语言模型(MLM)#

3.1 核心思想#

flowchart LR A["I [MASK] to the store"] --> B[BERT Encoder] B --> C["I went to the store"] D["随机遮盖 15% 的 token"] --> E["预测被遮盖的词"] E --> F["双向上下文理解"]
flowchart TB subgraph MLM训练策略 A[输入处理(随机选择 15% 的 token)] A --> B[80% 替换为 [MASK]] A --> C[10% 替换为随机词] A --> D[10% 保持不变] B --> B1[""I went to the store" → "I [MASK] to the store""] C --> C1[""I went to the store" → "I apple to the store""] D --> D1[""I went to the store" → "I went to the store""] E[为什么这样设计?] E --> E1[避免 [MASK] 和微调时的分布差异] E --> E2[随机替换增加难度,学习鲁棒表示] E --> E3[保持部分原始信息] F[损失函数] F --> F1["L_MLM = -Σ log P(masked_token | context)"] end

3.2 代码实现#

import torch
import torch.nn as nn
class MaskedLanguageModel(nn.Module):
"""BERT 的掩码语言模型头"""
def __init__(self, hidden_size, vocab_size):
super().__init__()
self.transform = nn.Sequential(
nn.Linear(hidden_size, hidden_size),
nn.GELU(),
nn.LayerNorm(hidden_size)
)
self.decoder = nn.Linear(hidden_size, vocab_size, bias=False)
def forward(self, hidden_states, masked_positions):
"""
Args:
hidden_states: BERT 输出 [batch, seq_len, hidden_size]
masked_positions: 被遮盖的位置 [batch, num_masks]
"""
# 获取被遮盖位置的隐藏状态
batch_size, num_masks = masked_positions.size()
# 收集被遮盖位置的表示
masked_hidden = torch.gather(
hidden_states,
dim=1,
index=masked_positions.unsqueeze(-1).expand(-1, -1, hidden_states.size(-1))
)
# 预测
transformed = self.transform(masked_hidden)
logits = self.decoder(transformed)
return logits
def create_masked_input(input_ids, vocab_size, mask_token_id, mask_prob=0.15):
"""创建遮盖输入"""
labels = input_ids.clone()
# 随机选择要遮盖的位置
probability_matrix = torch.full(input_ids.shape, mask_prob)
masked_indices = torch.bernoulli(probability_matrix).bool()
# 只在被遮盖的位置计算损失
labels[~masked_indices] = -100 # PyTorch 忽略 -100
# 80% 替换为 [MASK]
indices_replaced = torch.bernoulli(torch.full(input_ids.shape, 0.8)).bool() & masked_indices
input_ids[indices_replaced] = mask_token_id
# 10% 替换为随机词
indices_random = torch.bernoulli(torch.full(input_ids.shape, 0.5)).bool() & masked_indices & ~indices_replaced
random_words = torch.randint(vocab_size, input_ids.shape, dtype=torch.long)
input_ids[indices_random] = random_words[indices_random]
# 10% 保持不变
return input_ids, labels

四、下一句预测(NSP)#

4.1 任务设计#

flowchart TB subgraph NSP 任务 A["[CLS] Sentence A [SEP] Sentence B [SEP]"] --> B[BERT] B --> C["[CLS] 表示"] C --> D["分类:是否连续?"] end E["IsNext"] --> F["正样本:连续句子"] G["NotNext"] --> H["负样本:随机句子"]
flowchart TB subgraph NSP训练策略 A[输入格式] A --> A1["[CLS] Sentence A [SEP] Sentence B [SEP]"] B[正样本构建] B --> B1[从语料中选取连续的两个句子] B --> B2[标签:IsNext] C[负样本构建] C --> C1[第一句保持不变] C --> C2[第二句从语料中随机选取] C --> C3[标签:NotNext] D[训练比例] D --> D1[50% IsNext + 50% NotNext] E[目的] E --> E1[学习句子间关系] E --> E2[增强篇章理解能力] E --> E3[对 QA、NLI 等任务有帮助] F[注] F --> F1[后续研究(如 RoBERTa)发现 NSP 可能不是必需的] end

4.2 代码实现#

class NextSentencePrediction(nn.Module):
"""下一句预测头"""
def __init__(self, hidden_size):
super().__init__()
self.classifier = nn.Sequential(
nn.Linear(hidden_size, hidden_size),
nn.Tanh(),
nn.Linear(hidden_size, 2) # IsNext / NotNext
)
def forward(self, cls_output):
"""
Args:
cls_output: [CLS] token 的表示 [batch, hidden_size]
"""
return self.classifier(cls_output)
class BERTPreTraining(nn.Module):
"""BERT 预训练模型"""
def __init__(self, bert, vocab_size):
super().__init__()
self.bert = bert
self.mlm = MaskedLanguageModel(bert.config.hidden_size, vocab_size)
self.nsp = NextSentencePrediction(bert.config.hidden_size)
def forward(self, input_ids, token_type_ids, attention_mask, masked_positions):
# BERT 编码
sequence_output, pooled_output = self.bert(
input_ids=input_ids,
token_type_ids=token_type_ids,
attention_mask=attention_mask
)
# MLM 预测
mlm_logits = self.mlm(sequence_output, masked_positions)
# NSP 预测
nsp_logits = self.nsp(pooled_output)
return mlm_logits, nsp_logits

五、输入表示#

5.1 Tokenization#

flowchart LR A["输入文本"] --> B[WordPiece 分词] B --> C["添加特殊 token"] C --> D["Segment 嵌入"] D --> E["位置嵌入"] E --> F["最终输入"]
flowchart TB subgraph BERT输入表示 A["输入 = Token Embedding + Segment Embedding + Position Embedding"] B[Token Embedding] B --> B1[WordPiece 分词(30,000 词表)] B --> B2["[CLS]:句首,用于分类任务"] B --> B3["[SEP]:句子分隔符"] B --> B4["[MASK]:预训练时的遮盖标记"] C[Segment Embedding] C --> C1[区分句子 A 和句子 B] C --> C2[句子 A:EA,句子 B:EB] C --> C3[单句任务:全部 EA] D[Position Embedding] D --> D1[学习的位置嵌入(非正弦余弦)] D --> D2[最大长度 512] E[示例] E --> E1["输入:[CLS] I love dogs [SEP] They are cute [SEP]"] E --> E2["Token: [101, 1045, 2293, 8799, 102, 2027, 2024, 10125, 102]"] E --> E3["Segment: [0, 0, 0, 0, 0, 1, 1, 1, 1]"] E --> E4["Position: [0, 1, 2, 3, 4, 5, 6, 7, 8]"] end

六、预训练+微调范式#

6.1 微调方式#

flowchart TB subgraph 预训练 A[大规模无标注文本] --> B[BERT 预训练] B --> C[预训练模型] end subgraph 微调 C --> D[下游任务] D --> E[分类:取 [CLS] 表示] D --> F[NER:每个 token 分类] D --> G[QA:预测答案起止] D --> H[相似度:句对分类] end
flowchart TB subgraph 不同任务的微调方式 A[单句分类(情感分析、语义相似度)] A --> A1["输入:[CLS] 句子 [SEP]"] A --> A2["输出:[CLS] 的表示 → 分类层"] B[句对分类(NLI、QA)] B --> B1["输入:[CLS] 句子A [SEP] 句子B [SEP]"] B --> B2["输出:[CLS] 的表示 → 分类层"] C[序列标注(NER、POS)] C --> C1["输入:[CLS] 句子 [SEP]"] C --> C2["输出:每个 token 的表示 → 分类层"] D[问答(SQuAD)] D --> D1["输入:[CLS] 问题 [SEP] 文章 [SEP]"] D --> D2["输出:预测答案的起止位置"] E[微调技巧] E --> E1[学习率:2e-5 到 5e-5] E --> E2[Epochs:2-4] E --> E3[批量大小:16 或 32] E --> E4[预训练学习率是微调学习率的 1/10 到 1/100] end

6.2 微调代码示例#

from transformers import BertForSequenceClassification, BertTokenizer
# 加载预训练模型
model = BertForSequenceClassification.from_pretrained(
'bert-base-uncased',
num_labels=2 # 二分类
)
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# 准备输入
text = "This movie is fantastic!"
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
# 微调训练循环
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5)
for epoch in range(3):
for batch in train_dataloader:
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
optimizer.zero_grad()
# 推理
with torch.no_grad():
outputs = model(**inputs)
predictions = torch.argmax(outputs.logits, dim=-1)

七、实验结果#

7.1 GLUE 基准#

xychart-beta title "GLUE 基准测试分数" x-axis ["Pre-BERT SOTA", "BERT-Base", "BERT-Large"] y-axis "平均分数" 75 --> 85 bar [76.2, 79.6, 82.1]
| 任务 | Pre-BERT | BERT-Base | BERT-Large |
|-----------|-------------|-------------|------------------|
| MNLI | 86.6 | 87.6 | 88.6 |
| QQP | 66.1 | 89.2 | 90.3 |
| QNLI | 87.4 | 90.1 | 91.1 |
| SST-2 | 93.2 | 93.5 | 94.9 |
| CoLA | 35.0 | 52.1 | 60.5 |
| STS-B | 81.0 | 85.8 | 86.5 |
| MRPC | 86.0 | 88.9 | 89.3 |
| RTE | 61.7 | 66.4 | 70.1 |
| 平均 | 76.2 | 79.6 | 82.1 |

7.2 SQuAD 问答#

SQuAD v1.1:
• BERT-Base:F1 88.5,EM 80.4
• BERT-Large:F1 91.1,EM 84.1
• Pre-BERT SOTA:F1 85.8,EM 78.0
SQuAD v2.0(包含无答案问题):
• BERT-Large:F1 83.1,EM 80.0
• Pre-BERT SOTA:F1 66.3,EM 63.4
提升显著,证明了预训练表示的力量

八、BERT vs GPT 深度对比#

flowchart TB subgraph BERT vs GPT全面对比 A[架构] A --> A1[BERT: Encoder only] A --> A2[GPT: Decoder only] B[注意力] B --> B1[BERT: 双向] B --> B2[GPT: 单向] C[预训练任务] C --> C1[BERT: MLM + NSP] C --> C2[GPT: 语言模型] D[核心能力] D --> D1[BERT: 理解] D --> D2[GPT: 生成] E[典型应用] E --> E1[BERT: 分类、NER、QA] E --> E2[GPT: 文本生成、对话] F[微调方式] F --> F1[BERT: 添加任务头] F --> F2[GPT: Few-Shot / 微调] G[上下文长度] G --> G1[BERT: 512] G --> G2[GPT: 1024+] H[训练效率] H --> H1[BERT: 较慢(MLM)] H --> H2[GPT: 较快(自回归)] I[表示质量] I --> I1[BERT: 深度双向] I --> I2[GPT: 单向累积] J[优势] J --> J1[BERT: 理解任务强] J --> J2[GPT: 生成任务强] K[劣势] K --> K1[BERT: 不擅长生成] K --> K2[GPT: 不擅长理解] end

8.1 选择建议#

选择 BERT 当:
• 任务是理解型(分类、NER、QA)
• 需要双向上下文
• 有标注数据进行微调
• 对准确率要求高
选择 GPT 当:
• 任务是生成型(写作、对话、翻译)
• 需要开放域生成
• 标注数据少
• 需要 Few-Shot 能力

九、BERT 的后续发展#

BERT 衍生模型:
RoBERTa (Facebook, 2019)
• 移除 NSP 任务
• 更大的批量和数据
• 动态掩码
• 性能进一步提升
ALBERT (Google, 2019)
• 参数共享,减少参数量
• 跨层参数共享
• 句子顺序预测替代 NSP
DistilBERT (HuggingFace, 2019)
• 蒸馏压缩
• 保留 97% 性能,减少 40% 参数
ELECTRA (Google, 2020)
• 替换词检测任务
• 更高效的预训练
DeBERTa (Microsoft, 2020)
• 解耦注意力
• 更强的性能

常见问题 FAQ#

Q1:BERT 为什么不能用于生成?

A:BERT 是 Encoder,没有因果掩码,可以看到未来的 token。生成需要自回归地逐词预测,BERT 的双向特性反而会导致「作弊」。

Q2:MLM 为什么比语言模型更难?

A:MLM 需要预测被遮盖的词,只能依赖上下文。语言模型可以「复制」前面的内容。但 MLM 学到的表示更丰富。

Q3:BERT 的 [CLS] 是什么?

A:[CLS] 是句首的特殊 token,其表示被设计用于聚合整个句子的信息,常用于分类任务。

Q4:为什么 BERT 有最大长度限制?

A:位置嵌入是学习的,最大长度 512。超过需要截断或使用 Longformer、BigBird 等变体。

Q5:BERT 还值得学吗?

A:BERT 的思想(双向编码、预训练+微调)仍然重要。理解 BERT 有助于理解现代 NLP。


小结#

BERT 证明了双向预训练在语言理解任务上的强大能力。

核心贡献:

flowchart TB subgraph BERT核心总结 A[双向 Transformer] --> A1[打破单向限制] B[掩码语言模型] --> B1[学习深度上下文表示] C[预训练+微调] --> C1[统一的迁移学习范式] D[11 项 SOTA] --> D1[证明预训练的力量] E[影响深远] --> E1[开启 NLP 预训练时代] end

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

BERT 与双向预训练:NLP 预训练模型的崛起
https://blog.souloss.com/posts/machine-learning/llm-paper-history/bert-and-bidirectional-pretraining/
作者
Souloss
发布于
2025-03-21
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时