Skip to content
Published on

BERT 论文完全解析:双向 Transformer 如何改变 NLP 格局

分享
Authors

1. 论文概述

"BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding" 是 2018 年 10 月由 Google AI Language 团队的 Jacob Devlin、Ming-Wei Chang、Kenton Lee、Kristina Toutanova 发表的论文,此后在 NAACL 2019 上被评为 Best Long Paper。

BERT 是 Bidirectional Encoder Representations from Transformers 的缩写,顾名思义,它是将 Transformer 的 Encoder 进行双向预训练所得到的语言表示模型。核心思路很简单:先在大规模无监督文本语料上学习双向上下文,然后针对具体任务只做少量微调即可。

这一简单的方法在 11 个 NLP 基准上同时取得了 State-of-the-Art(SOTA)。它把 GLUE 分数推高到 80.5%(绝对提升 7.7%),SQuAD v1.1 F1 达到 93.2(提升 1.5 个百分点),SQuAD v2.0 F1 达到 83.1(提升 5.1 个百分点)。BERT 是在 NLP 领域确立了 预训练 + 微调 范式的论文,也成为了此后几乎所有语言模型的出发点。


2. 背景:为什么需要双向

2.1 预训练语言表示的两条路径

在 BERT 之前,利用预训练语言表示的方法大致分为两条路径。

Feature-based 方法(ELMo)

Peters 等人(2018)提出的 ELMo(Embeddings from Language Models)分别独立训练 Forward LSTM 与 Backward LSTM,再将两个方向的 hidden state 连接(concatenation)起来,生成依赖上下文的词表示。ELMo 的表示被用作下游任务的输入特征,每个任务都需要单独设计架构。

ELMok=γj=0Lsj[hk,j;hk,j]\text{ELMo}_k = \gamma \sum_{j=0}^{L} s_j \cdot [\overrightarrow{h}_{k,j}; \overleftarrow{h}_{k,j}]

其中 hk,j\overrightarrow{h}_{k,j}hk,j\overleftarrow{h}_{k,j} 分别是 Forward、Backward LSTM 第 jj 层的 hidden state,sjs_j 是可学习的权重。其核心局限在于 Forward 与 Backward 是 独立训练 的,两个方向的信息无法在更深的层中相互作用。

微调方法(OpenAI GPT)

Radford 等人(2018)提出的 GPT(Generative Pre-Training)使用 Transformer Decoder 以 Left-to-Right Language Modeling 的方式进行预训练,再针对下游任务对整个模型做微调。这种方式的优点是每个任务的架构改动被降到最低,但存在只能 单向(Left-to-Right)编码上下文的根本局限。

2.2 单向的局限

要理解 "The bank of the river" 中「bank」一词的含义,不仅要看「bank」左边的「The」,还要看右边的「river」。Left-to-Right 模型在编码「bank」时无法参照「river」,这正是 GPT-1 的根本局限。

ELMo 虽然考虑了两个方向,但它是独立训练 Forward 与 Backward 后再简单连接,属于 "浅层(shallow)双向"。真正的双向表示,应当是左右两侧的上下文在每一层都同时相互作用、共同学习。

2.3 三种方法的比较

特性ELMoGPT-1BERT
架构Bi-LSTMTransformer DecoderTransformer Encoder
方向性浅层双向(独立训练)单向(Left-to-Right)深层双向
预训练目标Forward + Backward LMLeft-to-Right LMMLM + NSP
下游应用方式Feature-based微调微调
任务专属架构每个任务都需要单独设计改动最小改动最小
参数量94M117M110M(Base) / 340M(Large)

BERT 的核心贡献,正是把这两种方法的优点结合了起来。它像 GPT 一样基于微调,也像 ELMo 一样利用双向上下文,但不是各自独立训练,而是 在所有层中同时(deeply) 学习双向上下文。


3. BERT 架构

3.1 Transformer Encoder Stack

BERT 只使用 Vaswani 等人(2017)提出的 Transformer 架构中的 Encoder 部分,不使用 Decoder。每个 Encoder 层由以下两个 Sub-layer 构成。

  1. Multi-Head Self-Attention:输入序列中的每个 token 都双向参照其他所有 token
  2. Position-wise Feed-Forward Network:对每个 token 独立执行非线性变换

每个 Sub-layer 都应用了 Residual Connection 与 Layer Normalization。

output=LayerNorm(x+Sublayer(x))\text{output} = \text{LayerNorm}(x + \text{Sublayer}(x))

由于不使用 Decoder,也就没有 Auto-regressive Masking,所有位置的 token 都可以自由参照其他所有位置的 token。这正是 BERT 能够成为 真正的双向(deeply bidirectional) 模型的结构基础。

3.2 BERT-Base vs BERT-Large

论文提出了两种模型规模。

设置BERT-BaseBERT-Large
Layers (LL)1224
Hidden Size (HH)7681,024
Attention Heads (AA)1216
Feed-Forward Size3,072 (4H4H)4,096 (4H4H)
总参数量110M340M

BERT-Base 被有意设计为与 GPT-1 具有相同的模型规模(层数、Hidden Size、Attention Head 数),目的是为了公平比较——证明性能提升的原因是 预训练方法论的差异,而非架构规模。

每个 Attention Head 的维度为 dk=H/Ad_k = H / A。BERT-Base 中为 768/12=64768 / 12 = 64,BERT-Large 中为 1024/16=641024 / 16 = 64,两个模型每个 Head 的维度都同为 64。

3.3 激活函数:GELU

BERT 没有使用 Transformer 原始论文中的 ReLU,而是采用 GELU(Gaussian Error Linear Unit)作为激活函数。

GELU(x)=xΦ(x)=x12[1+erf(x2)]\text{GELU}(x) = x \cdot \Phi(x) = x \cdot \frac{1}{2}\left[1 + \text{erf}\left(\frac{x}{\sqrt{2}}\right)\right]

与 ReLU 不同,GELU 会根据输入值平滑地激活,在负值区间也保留一定的梯度。此后它成为了 GPT-2、RoBERTa 等大多数基于 Transformer 的模型的标准激活函数。


4. 输入表示(Input Representation)

BERT 的输入表示由三种 Embedding 相加构成。这一设计让单一模型能够同时处理单句和句子对任务。

4.1 WordPiece Tokenization

BERT 使用包含 30,000 个 WordPiece token 的词表。WordPiece 是 Byte-Pair Encoding(BPE)的变体,它不是按频率、而是按似然(likelihood)来合并子词。

例如,"playing" 会被拆分为 "play" + "##ing"。"##" 前缀表示该 token 不是单词的开头,而是前一个 token 的延续。这种方式的优点如下。

  • 解决 OOV(Out-of-Vocabulary)问题:任何单词都可以用子词组合来表示
  • 保留形态信息:"play" 与 "playing" 共享公共 token "play"
  • 控制词表规模:用 30,000 个有限词表即可覆盖所有文本

4.2 特殊 token:[CLS] 与 [SEP]

BERT 使用两种特殊 token。

[CLS] (Classification Token):添加在每个输入序列的最前面。在分类任务中,这个 token 的最终 hidden state 被用作整个序列的聚合(aggregate)表示,因为通过 Self-Attention,序列中所有 token 的信息都会汇聚到 [CLS] 上。

[SEP] (Separator Token):用于分隔两个句子。在单句任务中添加在句子末尾,在句子对任务中则添加在两个句子之间以及第二个句子的末尾。

输入格式示例如下。

单句:    [CLS] I love NLP [SEP]
句子对:  [CLS] How old are you ? [SEP] I am 25 years old . [SEP]

4.3 三种 Embedding 之和

每个 token 的输入表示,由以下三种 Embedding 按 element-wise 相加 生成。

Token Embedding:针对 WordPiece token 的可学习 Embedding,词表规模为 30,000 时,对应一个大小为 R30000×H\mathbb{R}^{30000 \times H} 的 Embedding 矩阵。

Segment Embedding:用于区分输入属于句子 A 还是句子 B 的可学习 Embedding。属于句子 A 的 token 用 EAE_A 表示,属于句子 B 的 token 用 EBE_B 表示。单句任务中所有 token 都使用 EAE_A

Position Embedding:编码序列内位置信息的可学习 Embedding。与 Transformer 原始论文中的 Sinusoidal Positional Encoding 不同,BERT 使用的是 可学习(learned) 的 Position Embedding,最大序列长度为 512。

Input(xi)=Etoken(xi)+Esegment(xi)+Eposition(i)\text{Input}(x_i) = E_{\text{token}}(x_i) + E_{\text{segment}}(x_i) + E_{\text{position}}(i)

这三种 Embedding 之和会被输入到 Transformer Encoder 的第一层。


5. 预训练方法论

BERT 的预训练由两个无监督任务构成,这两个任务的设计正是 BERT 论文的核心贡献。

5.1 Masked Language Model (MLM)

动机:双向训练的困境

标准 Language Model 预测的是 P(wtw1,...,wt1)P(w_t | w_1, ..., w_{t-1}),即给定之前的 token 时下一个 token 的概率,这本质上是 Left-to-Right 的。如果直接训练 Bidirectional Language Model,就会出现每个 token 间接"看到自己"的问题——在多层网络中,双向上下文会循环流动,导致预测目标词的信息发生泄漏。

BERT 用受 Cloze Task(完形填空)启发而来的 Masked Language Model 解决了这一困境。

15% 遮蔽策略

在每个训练序列中,随机选取 15% 的 WordPiece token 进行遮蔽(masking),训练目标就是预测被选中 token 的原始单词。

但如果只是简单地替换成 [MASK] token,就会产生问题:微调时 [MASK] token 并不会出现在输入中,因此预训练与微调之间会产生 不一致(mismatch)

80/10/10 规则

为缓解这种不一致,被选中的 15% token 会按以下比例进行处理。

比例处理方式示例(从 "my dog is hairy" 中选中 "hairy" 时)
80%替换为 [MASK] tokenmy dog is [MASK]
10%替换为随机 tokenmy dog is apple
10%保留原始 tokenmy dog is hairy

具体来看,这一策略的效果如下。

  • 80% [MASK]:让模型学习从上下文还原原始单词
  • 10% Random:让模型对每个输入 token 是否为真实单词保持不确定,从而迫使模型在任何位置都维持正确的表示
  • 10% Unchanged:缩小与微调时实际观察到的输入之间的差距

论文的 Ablation Study 表明 80/10/10 的比例是最优的;若只使用 100% [MASK],在 Feature-based 方法下性能会大幅下降。

MLM Loss 函数

MLM 的损失函数只在被遮蔽的位置计算 Cross-Entropy Loss。

LMLM=iMlogP(xix~)\mathcal{L}_{\text{MLM}} = -\sum_{i \in \mathcal{M}} \log P(x_i | \tilde{x})

其中 M\mathcal{M} 是被遮蔽 token 的索引集合,x~\tilde{x} 是经过遮蔽处理的输入序列,P(xix~)P(x_i | \tilde{x}) 是模型在被遮蔽位置 ii 预测出原始 token xix_i 的概率。

具体来说,会先将每个被遮蔽位置的最终 hidden state hih_i 投影到词表大小,再应用 Softmax。

P(xix~)=softmax(Whi+b)xiP(x_i | \tilde{x}) = \text{softmax}(W \cdot h_i + b)_{x_i}

其中 WRV×HW \in \mathbb{R}^{|V| \times H} 是输出权重矩阵,V|V| 是词表大小(30,000)。

MLM 的缺点是每个 batch 中只预测 15% 的 token,因此收敛速度比一般的 Left-to-Right LM 更慢。不过论文通过实验证明,性能提升足以抵消这一代价,且绰绰有余。

5.2 Next Sentence Prediction (NSP)

动机

Question Answering(QA)或 Natural Language Inference(NLI)这类任务,需要理解两个句子之间的 关系,而这种关系很难仅靠单纯的 Language Modeling 学到。

训练方法

NSP 是一个二分类任务。给定两个句子 A 和 B,模型要预测 B 是否真的是紧跟在 A 之后的句子(IsNext),还是从语料库中随机选取的句子(NotNext)。

  • 50%:B 是 A 的真实下一句(IsNext)
  • 50%:B 是从语料库中随机选取的句子(NotNext)
[CLS] The man went to [MASK] store [SEP] He bought a gallon [MASK] milk [SEP]
Label: IsNext

[CLS] The man [MASK] to the store [SEP] Penguin [MASK] are flight ##less birds [SEP]
Label: NotNext

把 [CLS] token 的最终 hidden state CC 输入二分类器进行预测。

P(IsNextC)=softmax(WNSPC)P(\text{IsNext} | C) = \text{softmax}(W_{\text{NSP}} \cdot C)

NSP 的效果与争议

论文的 Ablation Study 显示,去掉 NSP 后,QNLI(-3.5%)、MNLI(-0.5%)、SQuAD(-0.6%)出现了显著的性能下降,尤其是在句子对关系推理很重要的任务上,下降幅度更为明显。

然而,此后的 RoBERTa(Liu et al., 2019)研究对 NSP 的效果提出了质疑。RoBERTa 在去掉 NSP 的情况下依然取得了比 BERT 更高的性能,并主张 NSP 的效果更多取决于训练数据的构成方式。

5.3 整体预训练损失

最终的预训练损失是 MLM 与 NSP 损失之和。

L=LMLM+LNSP\mathcal{L} = \mathcal{L}_{\text{MLM}} + \mathcal{L}_{\text{NSP}}

5.4 预训练数据与设置

设置
训练数据BooksCorpus(800M 词) + English Wikipedia(2,500M 词)
总数据规模约 16GB 文本
词表规模30,000 个 WordPiece token
最大序列长度512 token
Batch Size256 个序列(128,000 token/批)
训练步数1,000,000 步(约 40 个 epoch)
OptimizerAdam (β1=0.9\beta_1=0.9, β2=0.999\beta_2=0.999, ϵ=106\epsilon=10^{-6})
Learning Rate1e-4(10,000 步 warmup 后 linear decay)
Dropout0.1(所有层)
ActivationGELU
Hardware4 个 Cloud TPU Pod(16 个 TPU 芯片,BERT-Base) / 16 个 Cloud TPU Pod(64 个 TPU 芯片,BERT-Large)
训练时间BERT-Base:4 天,BERT-Large:4 天

6. 微调策略

BERT 的微调出奇地简单:只需在预训练好的 BERT 上加一个任务专属的输出层,再对整个模型做端到端(End-to-End)微调。相比预训练,微调非常快,大多数任务 用单块 Cloud TPU 一小时以内、用 GPU 数小时以内即可完成。

6.1 微调超参数

以下超参数范围在大多数任务上都表现良好。

超参数推荐范围
Batch Size16、32
Learning Rate (Adam)5e-5、4e-5、3e-5、2e-5
Epochs2、3、4
Dropout0.1(与预训练相同)

6.2 句子/句子对分类(Classification)

在情感分析(SST-2)、自然语言推理(MNLI、RTE)、句子相似度(STS-B、MRPC、QQP)等任务中,会把 [CLS] token 的最终 hidden state CRHC \in \mathbb{R}^H 输入分类器。

P(yx)=softmax(WC+b)P(y | x) = \text{softmax}(W \cdot C + b)

其中 WRK×HW \in \mathbb{R}^{K \times H}KK 是标签数量。

对于句子对任务,输入格式如下。

[CLS] Sentence A [SEP] Sentence B [SEP]

用 Segment Embedding 区分句子 A 与 B,[CLS] 的表示则编码了两个句子之间的关系。

6.3 问答(Question Answering, QA)

在 SQuAD(Stanford Question Answering Dataset)中,给定问题和文章后,模型要预测答案在文章中对应区间(span)的起始与结束位置。

[CLS] Question [SEP] Passage [SEP]

训练起始位置向量 SRHS \in \mathbb{R}^H 与结束位置向量 ERHE \in \mathbb{R}^H,并对文章中每个 token ii 计算起始/结束概率。

Pstart(i)=eSTijeSTj,Pend(i)=eETijeETjP_{\text{start}}(i) = \frac{e^{S \cdot T_i}}{\sum_j e^{S \cdot T_j}}, \quad P_{\text{end}}(i) = \frac{e^{E \cdot T_i}}{\sum_j e^{E \cdot T_j}}

其中 TiT_i 是 token ii 的最终 hidden state。候选答案 span (i,j)(i, j) 的得分由 STi+ETjS \cdot T_i + E \cdot T_j (jij \geq i)计算得出。

SQuAD v2.0 还需要处理无答案的情况:当没有答案时,起始与结束位置都设为 [CLS],以此计算"无答案"的概率。

6.4 命名实体识别(Named Entity Recognition, NER)

在 CoNLL-2003 NER 任务中,每个 token 会被分类为 Person、Organization、Location、Miscellaneous、Other 之一。此时输入分类器的是 每个 token 的最终 hidden state,而非 [CLS] 的表示

P(yix)=softmax(WNERTi+bNER)P(y_i | x) = \text{softmax}(W_{\text{NER}} \cdot T_i + b_{\text{NER}})

对于被 WordPiece 拆分出的子词 token,通常用第一个子词的预测结果作为该单词的标签。

6.5 Sequence Labeling 的推广

包括 NER 在内,POS Tagging、Chunking 等所有需要为序列中每个 token 打标签的任务,都可以套用同样的方式。因为 BERT 的每个 token 表示已经充分编码了双向上下文,所以即便没有 CRF(Conditional Random Field)这类额外的序列建模层,也能取得很高的性能。

6.6 Feature-based 方法

除了微调之外,BERT 也可以用作 Feature-based 方法:将 Transformer Encoder 冻结(freeze),抽取特定层的 hidden state 作为特征,输入到另外的模型中。

论文将 Feature-based 方式应用于 CoNLL-2003 NER 任务的结果显示,将最后 4 层的 hidden state 连接(concatenation)起来的表示达到了 96.1% F1(dev),非常接近微调方式的 96.4% F1(dev)。这说明 BERT 预训练得到的表示本身就蕴含着非常丰富的语言信息。


7. 核心公式汇总

7.1 Self-Attention (Scaled Dot-Product Attention)

这是 BERT 每个 Encoder 层中执行的 Self-Attention 公式。

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

其中 Q=XWQQ = XW^QK=XWKK = XW^KV=XWVV = XW^Vdk=H/Ad_k = H / A 是每个 Attention Head 的维度。

7.2 Multi-Head Attention

MultiHead(Q,K,V)=Concat(head1,...,headA)WO\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_A) \cdot W^O headi=Attention(QWiQ,KWiK,VWiV)\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)

BERT-Base 使用 A=12A=12 个 head,BERT-Large 使用 A=16A=16 个 head。

7.3 Feed-Forward Network

这是每个 Attention Sub-layer 之后的 Position-wise FFN。

FFN(x)=GELU(xW1+b1)W2+b2\text{FFN}(x) = \text{GELU}(xW_1 + b_1)W_2 + b_2

其中 W1RH×4HW_1 \in \mathbb{R}^{H \times 4H}W2R4H×HW_2 \in \mathbb{R}^{4H \times H},是内部维度为 Hidden Size 4 倍的 Bottleneck 结构。

7.4 MLM Loss(重新整理)

这是针对被遮蔽 token 集合 M\mathcal{M} 的 Cross-Entropy Loss。

LMLM=1MiMlogexp(Wxihi)v=1Vexp(Wvhi)\mathcal{L}_{\text{MLM}} = -\frac{1}{|\mathcal{M}|}\sum_{i \in \mathcal{M}} \log \frac{\exp(W_{x_i} \cdot h_i)}{\sum_{v=1}^{|V|} \exp(W_v \cdot h_i)}

其中 hih_i 是被遮蔽位置 ii 的最终 hidden state,WvW_v 是输出 Embedding 矩阵的第 vv 行,V=30,000|V| = 30,000

7.5 NSP Loss

LNSP=[ylogP(IsNext)+(1y)logP(NotNext)]\mathcal{L}_{\text{NSP}} = -\left[y \log P(\text{IsNext}) + (1-y) \log P(\text{NotNext})\right]

其中 y{0,1}y \in \{0, 1\} 是真实标签。

7.6 整体预训练目标

Lpretrain=LMLM+LNSP\mathcal{L}_{\text{pretrain}} = \mathcal{L}_{\text{MLM}} + \mathcal{L}_{\text{NSP}}

8. 实验结果

8.1 GLUE Benchmark

GLUE(General Language Understanding Evaluation)是由 9 个 NLU 任务构成的基准。BERT-Large 取得了 80.5% 的 GLUE 总分,相比既有 SOTA 实现了 7.7% 的绝对提升。

任务指标BERT-BaseBERT-Large既有 SOTA
MNLI-m / MNLI-mmAccuracy84.6 / 83.486.7 / 85.980.6 / 80.1
QQPF171.272.166.1
QNLIAccuracy90.592.787.4
SST-2Accuracy93.594.993.2
CoLAMatthews Corr52.160.535.0
STS-BSpearman Corr85.886.581.0
MRPCF188.989.386.0
RTEAccuracy66.470.161.7
WNLIAccuracy-65.165.1

尤其令人印象深刻的是,CoLA 相比既有 SOTA 取得了 25.5% 的绝对提升。CoLA(Corpus of Linguistic Acceptability)是判断句子语法可接受性的任务,需要深层的语言理解能力。

8.2 SQuAD v1.1

SQuAD v1.1(Stanford Question Answering Dataset)是从文章中抽取答案区间的 Extractive QA 任务。

模型EM (Exact Match)F1
既有 SOTA(单模型)84.491.0
BERT-Large(单模型)84.190.9
既有 SOTA(集成)86.791.7
BERT-Large + TriviaQA(集成)87.493.2

利用 TriviaQA 数据的集成模型取得了 93.2 的 F1,首次超过了人类表现(91.2 F1)。

8.3 SQuAD v2.0

SQuAD v2.0 是在 v1.1 基础上增加了 无法回答的问题(unanswerable questions) 的版本。

模型EMF1
既有 SOTA73.777.0
BERT-Large(单模型)80.083.1

相比既有 SOTA,F1 提升了 6.1 个百分点。在判断问题是否可回答这项能力上,BERT 的双向上下文理解表现出了特别的优势。

8.4 SWAG

SWAG(Situations With Adversarial Generations)是一个常识推理任务,需要从 4 个候选项中选出最适合接在给定句子后面的后续句子。

模型Dev AccuracyTest Accuracy
Human-88.0
ESIM + ELMo51.952.7
OpenAI GPT-78.0
BERT-Base81.6-
BERT-Large86.686.3

BERT-Large 领先 GPT 8.3%,逼近人类表现(88.0%)。

8.5 Ablation Study:预训练任务的影响

论文以 BERT-Base 为基准,分析了各预训练任务的重要性。

设置MNLI-mQNLIMRPCSST-2SQuAD F1
BERT-Base (MLM + NSP)84.488.486.792.788.5
No NSP83.984.986.592.687.9
LTR (Left-to-Right) & No NSP82.184.377.592.177.8
LTR + BiLSTM & No NSP82.184.175.791.684.9

核心发现如下。

  1. 去掉 NSP:QNLI 下降 3.5%,在句子对关系很重要的任务上影响较大
  2. Left-to-Right:相比双向,SQuAD 上 F1 下降 10.7,双向性对 token 级别任务至关重要
  3. LTR + BiLSTM:即便加上 BiLSTM,也无法替代双向预训练

8.6 Ablation Study:模型规模的影响

设置LHAParamsMNLI-mMRPCSST-2SQuAD F1
3-layer37681245M77.979.888.475.6
6-layer67681267M80.684.391.183.7
BERT-Base1276812110M84.486.792.788.5
BERT-Large24102416340M86.687.893.791.3

模型规模增大后,所有任务的性能都得到了一致的提升。特别值得注意的是,即使在小数据集(MRPC:3,600 个训练样本)上,更大的模型依然表现更好。这说明预训练提供了充分的知识,即使微调数据很少,大模型也能被有效训练出来。


9. 代码示例:HuggingFace Transformers

9.1 用 BERT 做 Masked Language Model 预测

from transformers import BertTokenizer, BertForMaskedLM
import torch

# 加载模型与分词器
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
model = BertForMaskedLM.from_pretrained("bert-base-uncased")
model.eval()

# 准备被遮蔽的句子
text = "The capital of France is [MASK]."
inputs = tokenizer(text, return_tensors="pt")

# 找到 [MASK] 的位置
mask_token_index = torch.where(inputs["input_ids"] == tokenizer.mask_token_id)[1]

# 预测
with torch.no_grad():
    outputs = model(**inputs)
    logits = outputs.logits

# 提取 Top-5 预测 token
mask_logits = logits[0, mask_token_index, :]
top5_tokens = torch.topk(mask_logits, 5, dim=1)

print(f"输入: {text}")
print("Top-5 预测:")
for i, (token_id, score) in enumerate(
    zip(top5_tokens.indices[0], top5_tokens.values[0])
):
    token = tokenizer.decode([token_id])
    print(f"  {i+1}. {token} (score: {score:.4f})")

输出示例:

输入: The capital of France is [MASK].
Top-5 预测:
  1. paris (score: 18.2341)
  2. lyon (score: 12.1456)
  3. lille (score: 10.8923)
  4. toulouse (score: 10.5678)
  5. marseille (score: 10.3210)

9.2 句子分类(Sentiment Analysis)微调

from transformers import BertTokenizer, BertForSequenceClassification
from transformers import Trainer, TrainingArguments
from datasets import load_dataset

# 加载数据集(SST-2)
dataset = load_dataset("glue", "sst2")
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")

# 分词
def tokenize_function(examples):
    return tokenizer(
        examples["sentence"],
        padding="max_length",
        truncation=True,
        max_length=128,
    )

tokenized_datasets = dataset.map(tokenize_function, batched=True)

# 初始化模型(二分类)
model = BertForSequenceClassification.from_pretrained(
    "bert-base-uncased",
    num_labels=2,
)

# 训练设置(论文推荐的超参数)
training_args = TrainingArguments(
    output_dir="./results",
    num_train_epochs=3,              # 论文推荐: 2-4
    per_device_train_batch_size=32,  # 论文推荐: 16 或 32
    learning_rate=2e-5,              # 论文推荐: 2e-5 ~ 5e-5
    weight_decay=0.01,
    warmup_ratio=0.1,
    evaluation_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    metric_for_best_model="accuracy",
)

# 用 Trainer 训练
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
)

trainer.train()

9.3 问答(Question Answering)

from transformers import BertTokenizer, BertForQuestionAnswering
import torch

tokenizer = BertTokenizer.from_pretrained("bert-large-uncased-whole-word-masking-finetuned-squad")
model = BertForQuestionAnswering.from_pretrained("bert-large-uncased-whole-word-masking-finetuned-squad")
model.eval()

# 问题与文章
question = "What is BERT?"
context = """BERT is a language representation model developed by Google.
It stands for Bidirectional Encoder Representations from Transformers.
BERT is designed to pre-train deep bidirectional representations from
unlabeled text by jointly conditioning on both left and right context
in all layers."""

# 分词
inputs = tokenizer(question, context, return_tensors="pt")

# 预测
with torch.no_grad():
    outputs = model(**inputs)
    start_logits = outputs.start_logits
    end_logits = outputs.end_logits

# 提取答案
start_idx = torch.argmax(start_logits)
end_idx = torch.argmax(end_logits)

answer_tokens = inputs["input_ids"][0][start_idx : end_idx + 1]
answer = tokenizer.decode(answer_tokens)
print(f"问题: {question}")
print(f"答案: {answer}")
# 输出: 答案: a language representation model developed by google

9.4 命名实体识别(NER)

from transformers import pipeline

# 创建 NER pipeline
ner_pipeline = pipeline(
    "ner",
    model="bert-base-cased",  # NER 任务中大小写区分很重要
    aggregation_strategy="simple",
)

text = "Google released BERT in 2018 at their Mountain View headquarters."
results = ner_pipeline(text)

for entity in results:
    print(f"Entity: {entity['word']:20s} | "
          f"Label: {entity['entity_group']:5s} | "
          f"Score: {entity['score']:.4f}")

9.5 提取 BERT Embedding(Feature-based 方法)

from transformers import BertModel, BertTokenizer
import torch

tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
model = BertModel.from_pretrained("bert-base-uncased", output_hidden_states=True)
model.eval()

text = "BERT produces contextualized word embeddings."
inputs = tokenizer(text, return_tensors="pt")

with torch.no_grad():
    outputs = model(**inputs)

# 所有层的 hidden states(共 13 个:embedding + 12 layers)
all_hidden_states = outputs.hidden_states  # tuple of (batch, seq_len, 768)

# 用 [CLS] token 表示句子(最后一层)
sentence_embedding = outputs.last_hidden_state[:, 0, :]
print(f"句子 embedding 维度: {sentence_embedding.shape}")  # (1, 768)

# 论文方式: 连接最后 4 层(Feature-based)
last_4_layers = torch.cat(
    [all_hidden_states[i] for i in [-4, -3, -2, -1]], dim=-1
)
print(f"最后 4 层连接后的维度: {last_4_layers.shape}")  # (1, seq_len, 3072)

10. BERT 的影响与后续研究

BERT 给 NLP 领域带来了范式转变。"预训练 + 微调"这一方法论如今已经成为 NLP 的标准,为克服 BERT 的局限,大量后续研究相继涌现。

10.1 RoBERTa (Liu et al., 2019, Meta AI)

Robustly Optimized BERT Pretraining Approach。在不改变 BERT 架构的前提下,仅通过 优化训练方法论 就大幅提升了性能。

主要变更:

  • 去掉 NSP:通过实验证明 NSP 反而会损害性能
  • Dynamic Masking:BERT 在数据预处理阶段就固定(static)了遮蔽方式,而 RoBERTa 每个 epoch 都会改变遮蔽模式
  • 更多数据:从 16GB 增加到 160GB,扩大 10 倍(加入 CC-News、OpenWebText、Stories)
  • 更大的 batch:从 256 增加到 8,000
  • 更长的训练:50,000 个 BPE 词表、更多训练步数

结果:在 GLUE、SQuAD、RACE 上的所有任务中都超过了 BERT-Large,说明 BERT 的预训练此前 严重欠训练(undertrained)

10.2 ALBERT (Lan et al., 2019, Google)

A Lite BERT。这是一个聚焦参数效率的模型。

主要技巧:

  • Factorized Embedding Parameterization:将词表 Embedding 维度(EE)与 Hidden 维度(HH)分离,用 V×E+E×HV \times E + E \times H 代替 V×HV \times H 来削减参数。例如当 E=128E=128 时,30000×768=23M30000 \times 768 = 23M 个参数会减少到 30000×128+128×768=3.9M30000 \times 128 + 128 \times 768 = 3.9M
  • Cross-layer Parameter Sharing:所有 Transformer 层共享同一组参数,在大幅削减参数量的同时把性能下降控制到最小
  • Sentence Order Prediction (SOP):用判断两个连续句子顺序是否正确的任务取代 NSP。这是一个比 NSP 更难的任务,对学习句子间关系更有效

结果:以比 BERT-Large 少 18 倍的参数,取得了相近甚至更好的性能。

10.3 DistilBERT (Sanh et al., 2019, Hugging Face)

利用 Knowledge Distillation 压缩 BERT 而得到的模型。

  • BERT-Base 40% 的体积(66M 参数,6 层)
  • 比 BERT-Base 快 60%
  • 保留了 BERT-Base 97% 的性能
  • 训练时使用了模仿 Teacher(BERT-Base)Soft Label 的 Distillation Loss
  • 在 SQuAD v1.1 上取得 86.9 F1(BERT-Base:88.5 F1)

成为了适合部署在移动端、边缘设备上的轻量模型的起点。

10.4 ELECTRA (Clark et al., 2020, Google/Stanford)

Efficiently Learning an Encoder that Classifies Token Replacements Accurately。从根本上改进了 BERT MLM 训练低效问题的模型。

核心思路:

  • Generator-Discriminator 结构:由小型 Generator(MLM)填补被遮蔽的 token,再由 Discriminator 判断每个 token 是原始的还是 Generator 生成的
  • 在所有 token 上学习:BERT 只在 15% 被遮蔽的 token 上学习,而 ELECTRA 在 所有 token 上学习"原始 vs 替换"的判别,训练效率大幅提升
  • Replaced Token Detection (RTD):一个比 MLM 更高效的预训练目标

结果:在相同计算预算下超过了 BERT、RoBERTa、ALBERT,尤其在小型模型(ELECTRA-Small)上,效率差距被拉到最大。

10.5 DeBERTa (He et al., 2020, Microsoft)

Decoding-enhanced BERT with Disentangled Attention。改进了 Attention 机制本身的模型。

主要创新:

  • Disentangled Attention:以往的 BERT 会先把 Token Embedding 与 Position Embedding 相加(sum)再做 Attention,而 DeBERTa 把 Content 与 Position 分离开,分别做 Attention,再结合 Content-to-Content、Content-to-Position、Position-to-Content 三种 Attention
  • Enhanced Mask Decoder:在解码层注入绝对位置信息,同时利用相对位置与绝对位置信息

结果:在同等模型规模下取得了比 BERT、RoBERTa 更高的性能,尤其仅用一半的训练数据就超越了既有模型,并在 SuperGLUE 上超过了人类表现。

10.6 后续研究谱系小结

BERT (2018)
├── RoBERTa (2019) ─── 训练方法论优化
├── ALBERT (2019) ─── 参数效率
├── DistilBERT (2019) ─── Knowledge Distillation
├── ELECTRA (2020) ─── 训练效率 (RTD)
├── DeBERTa (2020) ─── Attention 机制改进
├── SpanBERT (2020) ─── Span 级遮蔽
├── ERNIE (2019) ─── Knowledge-enhanced 预训练
└── ModernBERT (2024) ─── 应用现代架构

11. 局限与启示

11.1 BERT 的局限

1. 预训练与微调的不一致 (Pre-train/Fine-tune Discrepancy)

[MASK] token 只出现在预训练中,不会出现在微调中。80/10/10 策略缓解了这一问题,但没有从根本上解决。ELECTRA 的 RTD 用更优雅的方式解决了这个问题。

2. 假设被遮蔽 token 之间相互独立

MLM 是独立地预测各个被遮蔽的 token。例如当 "New York" 整体被遮蔽时,"New" 与 "York" 的预测是各自独立进行的,但实际上这两个 token 之间存在很强的依赖关系。XLNet(Yang et al., 2019)试图用 Permutation Language Modeling 解决这一问题。

3. 训练效率低

由于每个 batch 只预测 15% 的 token,收敛需要更多训练步数。ELECTRA 通过在所有 token 上学习,大幅改善了这一低效问题。

4. 最大序列长度限制

512 token 的最大长度限制不适合处理长文档。由于 Self-Attention 的 O(n2)O(n^2) 复杂度,很难大幅增加序列长度。Longformer、BigBird 等模型用 Sparse Attention 缓解了这一问题。

5. 不适合生成任务

由于 BERT 是 Encoder-only 模型,无法直接用于文本生成(摘要、翻译、对话等)。生成任务更适合用 GPT 系列(Decoder-only)或 T5、BART 系列(Encoder-Decoder)模型。

6. NSP 的效果存疑

RoBERTa 的实验揭示,NSP 反而可能损害性能。问题在于 NSP 本身是一个过于简单的任务——随机选取的句子仅凭主题(topic)差异就很容易被区分开。

11.2 从论文中获得的启示

1. 简单想法的力量

BERT 的核心思路(学习双向上下文)出奇地简单。它并非依靠复杂的架构创新,仅凭改变预训练目标(MLM)就取得了戏剧性的性能提升。

2. Scaling 的效果

Ablation Study 表明,增大模型规模与数据规模会带来持续一致的性能提升。这为此后的 Scaling Law 研究以及 GPT-3 等大规模模型的开发奠定了基础。

3. 确立 Transfer Learning 范式

正如 ImageNet 预训练曾经变革了 Computer Vision,BERT 证明了在 NLP 中,利用预训练语言模型应当成为 一切任务的出发点。从此不再需要为每个任务从零开始设计架构。

4. 公平比较的重要性

把 BERT-Base 设计成与 GPT-1 相同的规模,清楚地表明性能提升的原因是方法论的差异,而不是架构规模。这是研究论文实验设计中的一条重要启示。


12. 结语

BERT 自 2018 年发表以来,彻底改变了 NLP 领域的格局。它用 Masked Language Model 这一优雅的方法,实现了双向上下文学习这个简单却强大的想法,并通过同时横扫 11 个基准证明了其效果。

BERT 最大的遗产并不是某个具体基准上的数字,而是在 NLP 领域确立了 "先在大规模无监督语料上预训练,再用少量标注数据微调" 这一范式。这一范式此后成为了从 GPT-3、T5、PaLM 直到 LLaMA 等现代语言模型的基石。

当然,BERT 也有它的局限。遮蔽不一致、训练低效、序列长度限制等问题,后来都被 RoBERTa、ELECTRA、Longformer 等后续研究逐一解决。但所有这些后续研究都建立在 BERT 这个起点之上,这一事实本身,就说明了 BERT 论文的历史重要性。


参考资料