共计 2934 个字符,预计需要花费 8 分钟才能阅读完成。
传统词向量方法的局限性
在自然语言处理(NLP)领域,词向量(Word Embedding)是将词语转化为计算机可以理解的数值形式的一种方式。早期的词向量方法如 Word2Vec 虽然简单有效,但存在一些明显的局限性:
- 静态词向量 :Word2Vec 生成的词向量是静态的,同一个词在不同上下文中的表示是相同的。例如 ”bank” 在 ”river bank” 和 ”bank account” 中的含义不同,但 Word2Vec 无法区分。
- 无法处理 OOV 问题 :对于未出现在训练语料中的词(Out-of-Vocabulary),Word2Vec 无法生成有效的词向量。
- 缺乏上下文信息 :Word2Vec 无法捕捉句子级别的语义信息,只关注词语本身的共现关系。
这些局限性促使了更先进的预训练模型如 BERT 的出现。
BERT 与前期模型的对比
| 特性 | Word2Vec | ELMo | GPT | BERT |
|---|---|---|---|---|
| 词向量类型 | 静态 | 动态 | 动态 | 动态 |
| 上下文方向性 | 无 | 双向 | 单向 | 双向 |
| 预训练任务 | CBOW/Skip-gram | 语言模型 | 语言模型 | MLM+NSP |
| Transformer 层数 | 无 | 无 | 12 | 12/24 |
| 参数量级 | 小 | 中 | 大 | 很大 |
BERT 核心原理
Transformer Encoder 结构
BERT 基于 Transformer 的 Encoder 部分构建,其核心是多层自注意力机制(Self-Attention)。一个典型的 BERT-base 模型包含 12 层 Transformer Encoder。

Self-Attention 计算过程
自注意力机制的计算可以用以下公式表示:
Attention(Q, K, V) = softmax(QK^T/√d_k)V
其中:
– Q(Query) 是查询向量
– K(Key) 是键向量
– V(Value) 是值向量
– d_k 是向量的维度
预训练任务
BERT 通过两个主要的预训练任务来学习语言表示:
-
Masked Language Model (MLM):随机遮盖输入序列中的部分 token,让模型预测这些被遮盖的 token。这迫使模型理解双向上下文。
-
Next Sentence Prediction (NSP):给定两个句子 A 和 B,预测 B 是否是 A 的下一句。这帮助模型理解句子间关系。
代码实现:简化版 BERT 的 MLM 任务
下面是用 PyTorch 实现简化版 BERT 的 MLM 任务的完整代码:
import torch
import torch.nn as nn
from transformers import BertTokenizer, BertConfig
# 1. 数据预处理
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "The quick brown fox jumps over the lazy dog."
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
# 随机遮盖 15% 的 token
mask_indices = torch.randperm(inputs['input_ids'].size(1))[:int(0.15*inputs['input_ids'].size(1))]
inputs['labels'] = inputs['input_ids'].clone()
inputs['input_ids'][0, mask_indices] = tokenizer.mask_token_id
# 2. 模型构建
class SimpleBERT(nn.Module):
def __init__(self, config):
super().__init__()
self.embeddings = nn.Embedding(config.vocab_size, config.hidden_size)
self.encoder = nn.TransformerEncoder(
nn.TransformerEncoderLayer(
d_model=config.hidden_size,
nhead=config.num_attention_heads
),
num_layers=config.num_hidden_layers
)
self.classifier = nn.Linear(config.hidden_size, config.vocab_size)
def forward(self, input_ids):
x = self.embeddings(input_ids)
x = self.encoder(x)
return self.classifier(x)
config = BertConfig.from_pretrained('bert-base-uncased')
model = SimpleBERT(config)
# 3. 训练循环
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
criterion = nn.CrossEntropyLoss()
for epoch in range(3): # 简单演示,实际需要更多 epoch
outputs = model(inputs['input_ids'])
loss = criterion(outputs.view(-1, config.vocab_size),
inputs['labels'].view(-1))
loss.backward()
optimizer.step()
optimizer.zero_grad()
print(f"Epoch {epoch}, Loss: {loss.item()}")
实践建议
小规模数据微调技巧
- 分层学习率 :对不同层使用不同的学习率,底层使用较小的学习率,顶层使用较大的学习率。
- 早停法 :监控验证集性能,当性能不再提升时停止训练。
- 数据增强 :通过回译、同义词替换等方法扩充小规模数据集。
显存优化方案
- 梯度累积 :通过多次前向传播累积梯度,然后一次性更新参数,相当于增大 batch size 但减少显存占用。
- 混合精度训练 :使用 FP16 和 FP32 混合精度训练,可以显著减少显存使用。
- 模型并行 :将大模型拆分到多个 GPU 上。
常见训练失败原因排查
- 学习率不合适 :过大导致震荡,过小导致收敛慢。
- 数据质量问题 :检查数据是否有标签错误或噪声。
- 模型架构问题 :检查是否有梯度消失或爆炸。
延伸思考
BERT 的可改进设计
- 更高效的注意力机制 :原始的 Self-Attention 计算复杂度是 O(n^2),可以考虑稀疏注意力或线性注意力。
- 预训练任务优化 :MLM 任务可以改进为全词遮盖或短语遮盖。
- 多语言统一表示 :如何更好地处理多语言场景。
评估预训练模型质量
- 下游任务性能 :在多个标准 NLP 任务上的表现。
- 推理速度 :模型在实际应用中的响应时间。
- 资源消耗 :训练和推理时的计算资源需求。
自测问题
- BERT 的 MLM 任务为什么要随机遮盖 15% 的 token,而不是更多或更少?
- 为什么 BERT 使用 Layer Normalization 而不是 Batch Normalization?
- 在实际应用中,如何决定使用 BERT-base 还是 BERT-large?
希望这篇指南能帮助你理解 BERT 预训练机制的核心原理和实现方法。在实际应用中,建议从简单的任务开始,逐步深入理解模型的各个组件。
正文完
