BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

引言

BERT (Bidirectional Encoder Representations from Transformers) 是Google在2018年提出的革命性自然语言处理模型,它通过在无标注文本上进行预训练,学习深层的双向语言表示,在下游任务上取得了突破性的成果。

BERT的核心创新在于双向上下文编码,与之前的ELMo(浅层双向)和GPT(单向)不同,BERT使用Transformer编码器同时利用上下文信息,彻底改变了NLP领域的预训练范式。

背景知识

预训练语言模型的发展

在BERT之前,主流的预训练方法存在以下局限性:

  1. 单向语言模型(如GPT):只能从左到右或从右到左进行编码,无法同时利用双向上下文
  2. 浅层双向模型(如ELMo):虽然考虑了双向信息,但只是简单拼接左右向表示,而非深度双向,网络架构比较老,使用的RNN

为什么需要双向编码?

语言的理解往往需要同时考虑前后文信息。例如:

  • “银行” 这个词在 “我去银行存钱” 和 “河边的银行” 中含义完全不同
  • 只有同时看到前后文,才能准确理解词义

论文核心思想

主要贡献

  1. 双向预训练:通过Masked Language Model (MLM) 实现真正的双向编码
  2. 统一的预训练框架:一个模型可以适应多种下游任务
  3. 迁移学习的成功:证明了大规模预训练+微调的有效性

核心架构

BERT基于Transformer的编码器部分,主要包含:

1
BERT = Transformer Encoder × N层
  • BERT-Base: 12层Transformer,768维隐藏层,12个注意力头,110M参数
  • BERT-Large: 24层Transformer,1024维隐藏层,16个注意力头,340M参数

预训练方法

1. Masked Language Model (MLM)

目标:预测被掩码的词汇

方法

  1. 随机选择15%的token进行掩码
  2. 其中:
    • 80%替换为 [MASK]
    • 10%替换为随机token
    • 10%保持不变

为什么不完全用[MASK]?

  • 预训练时用[MASK],但微调时没有[MASK],会造成不匹配
  • 通过随机替换,模型学会在缺少明确信号时也能预测

数学表示

1
P(w_i | w_{context}) = softmax(W_h_i)

其中,h_i是第i个token的上下文表示

2. Next Sentence Prediction (NSP)

目标:判断两个句子是否是连续的

方法

  • 输入格式:[CLS] 句子A [SEP] 句子B [SEP]
  • 50%的概率是连续句子(IsNext)
  • 50%的概率是随机句子(NotNext)

为什么需要NSP?

  • 许多下游任务(如问答、自然语言推理)需要理解句子间关系
  • MLM主要学习词级表示,NSP帮助学习句子级表示

输入表示

BERT的输入由三个embedding相加:

1
Input = Token Embedding + Segment Embedding + Position Embedding

Token Embedding

  • 使用WordPiece分词
  • 特殊token:
    • [CLS]:分类任务的表示
    • [SEP]:句子分隔符
    • [MASK]:掩码token
    • [UNK]:未知词

Segment Embedding

  • 区分句子A和句子B
  • E_A = 0,E_B = 1

Position Embedding

  • 学习的位置编码(而非Transformer的固定位置编码)
  • 最大序列长度:512

模型架构详解

Transformer Encoder层

每个Transformer Encoder包含:

  1. 多头自注意力机制 (Multi-Head Self-Attention)

    1
    2
    Attention(Q, K, V) = softmax(QK^T / √d_k)V
    MultiHead = Concat(head_1, ..., head_h)W^O
  2. 前馈神经网络 (Feed-Forward Network)

    1
    FFN(x) = max(0, xW_1 + b_1)W_2 + b_2
  3. 残差连接和层归一化

    1
    output = LayerNorm(x + Sublayer(x))

BERT的输出

  • 最后一层的输出:每个token的上下文表示
  • [CLS] token的输出:用于分类任务的句子级表示

微调策略

下游任务适配

BERT可以通过简单的修改适应各种任务:

  1. 单句分类(如情感分析)

    1
    [CLS] 句子 [SEP] → 分类层
  2. 句子对分类(如自然语言推理)

    1
    [CLS] 句子A [SEP] 句子B [SEP] → 分类层
  3. 问答任务(如SQuAD)

    1
    [CLS] 问题 [SEP] 段落 [SEP] → 起始位置 + 结束位置
  4. 序列标注(如命名实体识别)

    1
    [CLS] token1 token2 ... [SEP] → 每个token的标签

微调技巧

  • 学习率:预训练的学习率通常较小(如2e-5)
  • Batch size:16或32通常效果较好
  • Epochs:2-4个epoch通常足够
  • 学习率调度:线性衰减或warmup

实验结果

GLUE基准测试

BERT在11个NLP任务上取得了state-of-the-art的结果:

  • MNLI: 84.6% (4.6%提升)
  • QQP: 71.2% F1 (4.2%提升)
  • QNLI: 90.5% (5.1%提升)
  • SST-2: 93.5% (2.0%提升)
  • CoLA: 52.1% (5.6%提升)

SQuAD v1.1

  • F1: 93.2%
  • EM: 87.4%

消融实验

  1. MLM的影响:移除MLM导致显著性能下降
  2. NSP的影响:对某些任务有帮助,但不是必需的
  3. 模型大小的影响:更大的模型带来更好的性能
  4. 训练步数的影响:更多训练步数持续提升性能

代码实现

使用Hugging Face Transformers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from transformers import BertTokenizer, BertModel
import torch

# 加载预训练模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')

# 输入文本
text = "Hello, how are you?"
inputs = tokenizer(text, return_tensors='pt')

# 获取模型输出
with torch.no_grad():
outputs = model(**inputs)

# outputs包含:
# - last_hidden_state: 最后一层的隐藏状态
# - pooler_output: [CLS] token的池化输出
print(outputs.last_hidden_state.shape) # [batch_size, seq_len, hidden_size]
print(outputs.pooler_output.shape) # [batch_size, hidden_size]

文本分类示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from transformers import BertForSequenceClassification
from transformers import Trainer, TrainingArguments

# 加载分类模型
model = BertForSequenceClassification.from_pretrained(
'bert-base-uncased',
num_labels=2 # 二分类
)

# 准备数据
train_texts = ["I love this movie", "This movie is terrible"]
train_labels = [1, 0]

# 训练参数
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=3,
per_device_train_batch_size=16,
learning_rate=2e-5,
)

# 训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
)
trainer.train()

自定义BERT模型(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch
import torch.nn as nn
from torch.nn import TransformerEncoder, TransformerEncoderLayer

class SimpleBERT(nn.Module):
def __init__(self, vocab_size, d_model=768, nhead=12, num_layers=12):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.pos_encoding = nn.Parameter(torch.randn(512, d_model))

encoder_layers = TransformerEncoderLayer(
d_model=d_model,
nhead=nhead,
dim_feedforward=3072,
dropout=0.1
)
self.transformer = TransformerEncoder(encoder_layers, num_layers)

def forward(self, x, mask=None):
# x: [batch_size, seq_len]
x = self.embedding(x) + self.pos_encoding[:x.size(1)]
x = x.transpose(0, 1) # [seq_len, batch_size, d_model]
output = self.transformer(x, src_key_padding_mask=mask)
return output.transpose(0, 1) # [batch_size, seq_len, d_model]

技术细节与优化

注意力机制计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def scaled_dot_product_attention(Q, K, V, mask=None):
"""
Q: [batch_size, num_heads, seq_len, d_k]
K: [batch_size, num_heads, seq_len, d_k]
V: [batch_size, num_heads, seq_len, d_v]
"""
d_k = Q.size(-1)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)

if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)

attention_weights = torch.softmax(scores, dim=-1)
output = torch.matmul(attention_weights, V)
return output, attention_weights

预训练损失函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def compute_bert_loss(mlm_logits, nsp_logits, mlm_labels, nsp_labels, mlm_mask):
# MLM损失
mlm_loss = nn.CrossEntropyLoss(ignore_index=-100)(
mlm_logits.view(-1, mlm_logits.size(-1)),
mlm_labels.view(-1)
)

# NSP损失
nsp_loss = nn.CrossEntropyLoss()(
nsp_logits,
nsp_labels
)

# 总损失
total_loss = mlm_loss + nsp_loss
return total_loss, mlm_loss, nsp_loss

BERT的优缺点

优点

  1. 强大的表示能力:双向编码捕获丰富的上下文信息
  2. 通用性强:一个模型适应多种任务
  3. 迁移学习效果好:预训练+微调范式非常有效
  4. 开源可用:提供了多种规模的预训练模型

缺点

  1. 计算成本高:参数量大,推理速度慢
  2. 最大长度限制:只能处理512个token
  3. 预训练任务局限:MLM和NSP可能不是最优的预训练目标
  4. 单向生成困难:由于双向特性,不适合生成任务

BERT的后续发展

改进方向

  1. 模型压缩

    • DistilBERT:知识蒸馏减小模型
    • ALBERT:参数共享降低参数量
  2. 效率提升

    • ELECTRA:更高效的预训练任务
    • RoBERTa:去除NSP,优化训练策略
  3. 长文本处理

    • Longformer:处理更长序列
    • BigBird:稀疏注意力机制
  4. 多语言扩展

    • mBERT:多语言BERT
    • XLM:跨语言预训练

总结

BERT通过双向Transformer编码器和创新的预训练任务(MLM + NSP),为NLP领域带来了革命性的变化。其核心思想是:

  1. 双向编码:同时利用前后文信息
  2. 预训练+微调:大规模无监督预训练 + 任务特定微调
  3. 统一架构:一个模型适应多种下游任务

BERT的成功证明了大规模预训练语言模型的有效性,为后续的GPT、T5等模型奠定了基础。

参考文献

  1. Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2018). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. arXiv preprint arXiv:1810.04805.

  2. Vaswani, A., et al. (2017). Attention is all you need. Advances in neural information processing systems, 30.

  3. Radford, A., et al. (2018). Improving language understanding by generative pre-training.

  4. Howard, J., & Ruder, S. (2018). Universal language model fine-tuning for text classification. arXiv preprint arXiv:1801.06146.

思考题

  1. 为什么BERT使用15%的mask比例?这个比例是否可以调整?
  2. BERT的MLM任务中,为什么要用80% [MASK]、10% 随机、10% 不变的方式?
  3. NSP任务对BERT的性能有多大影响?为什么RoBERTa去掉了NSP?
  4. BERT为什么不适合生成任务?如果要用于生成,应该怎么改进?
  5. 如何理解BERT的双向编码?它与ELMo的双向有什么本质区别?

思考题答案

1. 为什么BERT使用15%的mask比例?这个比例是否可以调整?

15% mask比例的原因:

  • 平衡学习效率与难度:如果mask比例太低(如5%),模型看到的有效训练信号太少,学习效率低;如果太高(如50%),输入信息损失过多,模型难以学习有效的语言表示。
  • 经验最优值:15%是经过实验验证的平衡点,既能提供足够的训练信号,又不会过度破坏句子的完整性。
  • 计算效率:只预测15%的token,相比预测所有token,计算成本更低。

是否可以调整?

可以调整,但需要权衡:

  • 更低的mask比例(5-10%):训练更稳定,但可能需要更多训练步数
  • 更高的mask比例(20-30%):可能学到更强的表示,但训练难度增加,可能影响性能

实验发现

  • RoBERTa的实验表明,15%仍然是一个较好的选择
  • 对于特定领域或任务,可能需要微调这个比例

2. BERT的MLM任务中,为什么要用80% [MASK]、10% 随机、10% 不变的方式?

这是BERT设计中的一个关键创新,主要解决预训练与微调的不匹配问题

问题背景

  • 预训练时:模型看到大量[MASK] token
  • 微调时:模型看不到[MASK] token,只有真实词汇
  • 如果只使用[MASK],模型会过度依赖这个特殊token,导致微调时性能下降

三种策略的作用

  1. 80% [MASK]

    • 主要学习目标:让模型学会从上下文预测被掩盖的词
    • 这是MLM的核心任务
  2. 10% 随机替换

    • 引入噪声:让模型学会在"错误"信息下也能正确预测
    • 提高鲁棒性:模拟真实场景中可能出现的错误或噪声
    • 防止过拟合:避免模型只记住[MASK]的模式
  3. 10% 保持不变

    • 关键设计:让模型在微调时能够利用真实词汇的信息
    • 缓解不匹配:预训练时也看到真实词汇,与微调场景更一致
    • 保持语言理解:确保模型不仅学习预测,还学习理解真实词汇

数学直觉

1
2
3
4
5
如果只使用[MASK]:
P(预测|看到[MASK]) ≠ P(预测|看到真实词) ❌ 不匹配

使用混合策略:
P(预测|看到[MASK]) ≈ P(预测|看到真实词) ✅ 更匹配

实验验证

  • 如果100%使用[MASK],在微调任务上性能会下降约2-3%
  • 混合策略使得预训练和微调更加一致

3. NSP任务对BERT的性能有多大影响?为什么RoBERTa去掉了NSP?

NSP的影响分析

正面影响

  • 对需要理解句子关系的任务有帮助:如自然语言推理(MNLI)、问答(QNLI)
  • 帮助模型学习句子级表示,而不仅仅是词级表示

负面影响

  • 任务过于简单:模型可能只关注句子边界信息,而非深层语义关系
  • 训练信号弱:相比MLM,NSP提供的学习信号较弱
  • 可能引入噪声:随机选择的负样本可能包含一些有用的信息,但被标记为"不相关"

RoBERTa去掉NSP的原因

  1. 实验发现NSP效果有限

    • 在某些任务上,去掉NSP反而性能更好
    • NSP带来的提升主要来自更长的训练,而非任务本身
  2. NSP任务设计有问题

    • 负样本(随机句子对)可能包含有用的信息
    • 模型可能学到"只要不是连续句子就是负样本"的简单模式
  3. MLM已经足够强大

    • MLM本身就能学习到句子间的关系
    • 通过跨句子的上下文,模型自然能理解句子关系
  4. 简化训练流程

    • 去掉NSP后,训练更简单,只需要MLM任务
    • 可以专注于优化MLM的训练策略

实验结果

  • RoBERTa去掉NSP后,在大多数任务上性能持平或略有提升
  • 证明了NSP并非必需,MLM已经足够学习句子级表示

4. BERT为什么不适合生成任务?如果要用于生成,应该怎么改进?

BERT不适合生成任务的原因

  1. 双向编码的本质

    • BERT在编码时同时看到整个序列(包括"未来"的信息)
    • 生成任务需要自回归(从左到右),不能看到未来信息
    • 这违反了生成任务的基本要求
  2. 架构限制

    • BERT只有编码器,没有解码器
    • 无法进行自回归生成
  3. 预训练目标不匹配

    • MLM是"填空"任务,不是"续写"任务
    • 模型没有学习到生成下一个token的能力

改进方案

  1. 使用BERT作为编码器 + 独立解码器

    1
    BERT Encoder + Transformer Decoder
    • 编码器:用BERT理解输入
    • 解码器:用Transformer解码器进行自回归生成
    • 应用:机器翻译、摘要生成
  2. 单向BERT变体

    • 训练时使用因果掩码(只能看到左侧信息)
    • 类似GPT,但可以保留BERT的其他优势
  3. 序列到序列BERT(BART)

    • 使用BERT的编码器 + GPT的解码器
    • 预训练任务:文本去噪(删除、打乱、填充等)
    • 既保留BERT的理解能力,又具备生成能力
  4. T5(Text-to-Text Transfer Transformer)

    • 统一的编码器-解码器架构
    • 所有任务都转换为文本生成任务
    • 更强的生成能力

实际应用

  • BART:专门为生成任务设计的BERT变体
  • T5:统一的文本到文本模型
  • GPT系列:更适合纯生成任务

5. 如何理解BERT的双向编码?它与ELMo的双向有什么本质区别?

BERT的双向编码

  1. 同时编码

    • 在单次前向传播中,每个token都能同时看到左侧和右侧的所有信息
    • 通过自注意力机制,所有位置的信息同时参与计算
  2. 深度双向

    • 多层Transformer编码器,每一层都是双向的
    • 信息在多层间双向流动和融合
  3. 数学表示

    1
    h_i^l = Transformer_Encoder([h_1^{l-1}, ..., h_n^{l-1}])

    其中,h_i^l 同时依赖于所有位置的 h_j^{l-1}

ELMo的双向

  1. 分别编码

    • 使用两个独立的LSTM:前向LSTM和后向LSTM
    • 前向LSTM:从左到右编码,h_i^forward 只看到左侧信息
    • 后向LSTM:从右到左编码,h_i^backward 只看到右侧信息
  2. 浅层拼接

    • 最终表示 = [h_i^forward; h_i^backward]
    • 只是简单拼接,没有深度融合
  3. 数学表示

    1
    2
    3
    h_i^forward = LSTM_forward(x_1, ..., x_i)
    h_i^backward = LSTM_backward(x_i, ..., x_n)
    h_i = [h_i^forward; h_i^backward]

本质区别

特性ELMoBERT
编码方式两个独立的单向编码器一个双向编码器
信息融合浅层拼接深度融合(多层)
同时性分别处理,后拼接同时处理所有位置
架构RNN(LSTM)Transformer
表示质量词级表示为主上下文相关表示
计算效率顺序计算,较慢并行计算,较快

关键差异示例

假设要理解句子 “The bank is closed” 中的 “bank”:

  • ELMo

    • 前向LSTM看到 “The bank”,可能理解为"银行"
    • 后向LSTM看到 “bank is closed”,也可能理解为"银行"
    • 拼接后得到表示,但两个方向的信息是独立计算的
  • BERT

    • 自注意力机制同时考虑 “The”、“bank”、“is”、“closed”
    • 所有位置的信息在每一层都相互影响
    • 最终表示融合了完整的上下文信息

为什么BERT的双向更好?

  1. 信息融合更充分:不是简单拼接,而是深度交互
  2. 并行计算:Transformer可以并行处理,效率更高
  3. 长距离依赖:自注意力机制能更好地捕捉长距离依赖
  4. 表示能力更强:多层双向编码产生更丰富的表示

总结

  • ELMo是"伪双向":两个单向模型的拼接
  • BERT是"真双向":真正的双向同时编码和融合