Transformer Encoder与BERT预训练实战教程

Transformer Encoder与BERT预训练实战教程

1 Transformer Encoder架构原理

1.1 整体架构概述

Transformer Encoder是一个由N个相同层堆叠而成的深度网络结构,每层包含两个核心子层:多头自注意力机制(Multi-Head Self-Attention)前馈神经网络(Feed-Forward Network)。每个子层都通过残差连接(Residual Connection)和层归一化(LayerNorm)进行包裹,确保训练稳定性。

编码器的设计哲学是将原始输入序列逐步提炼为富含上下文信息的向量表示,其最小功能单元可概括为:信息融合(通过Attention) + 特征增强(通过FFN) + 稳定训练(通过残差和LayerNorm)的打包结构

1.2 输入编码与位置编码

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

# 位置编码实现示例
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=512):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)

# 使用正弦和余弦函数生成位置编码
div_term = torch.exp(torch.arange(0, d_model, 2) *
(-torch.log(torch.tensor(10000.0)) / d_model))

pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度
pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度
pe = pe.unsqueeze(0) # [1, max_len, d_model]
self.register_buffer('pe', pe)

def forward(self, x):
# x: [batch_size, seq_len, d_model]
return x + self.pe[:, :x.size(1)]

位置编码的作用:弥补Transformer自注意力机制本身不具备的位置感知能力,通过正余弦函数为每个位置生成唯一编码,使模型能够理解词序信息。

1.3 多头自注意力机制

自注意力机制的核心思想是让序列中的每个词元都能关注序列中的所有其他词元,动态捕捉全局依赖关系。

计算公式

1
2
3
Attention(Q, K, V) = softmax(QKᵀ/√dₖ)V
MultiHead(Q, K, V) = Concat(head₁, ..., headₕ)Wᵒ
其中 headᵢ = Attention(QWᵢᵒ, KWᵢᴷ, VWᵢⱽ)

多头机制的优势

  • 多样化建模:不同注意力头可以关注不同类型的语义关系(语法结构、语义关联、指代关系等)
  • 并行计算效率:拆分后每个头的计算复杂度降低,整体仍可并行处理
  • 增强表达能力:多视角建模比单头注意力更灵活

1.4 前馈神经网络与归一化

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
29
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff=2048):
super().__init__()
# 两层全连接网络,中间维度扩展
self.linear1 = nn.Linear(d_model, d_ff)
self.relu = nn.ReLU()
self.linear2 = nn.Linear(d_ff, d_model)

def forward(self, x):
return self.linear2(self.relu(self.linear1(x)))

# 编码器层完整实现
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.norm1 = nn.LayerNorm(d_model)
self.ffn = FeedForward(d_model, d_ff)
self.norm2 = nn.LayerNorm(d_model)

def forward(self, x, mask=None):
# 自注意力 + 残差 + 层归一化
attn_out = self.self_attn(x, x, x, mask)
x = self.norm1(x + attn_out) # 第一次残差连接和归一化

# 前馈网络 + 残差 + 层归一化
ffn_out = self.ffn(x)
x = self.norm2(x + ffn_out) # 第二次残差连接和归一化
return x

层归一化与残差连接的作用

  • 残差连接:保留原始输入信息,防止深度网络中的梯度消失问题
  • 层归一化:对每个样本的所有特征维度进行归一化,稳定训练过程

2 BERT预训练任务详解

2.1 掩码语言模型(MLM)

BERT采用15%的掩码率,并对被选中的token应用80-10-10的替换策略。

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
29
30
31
32
33
34
35
36
37
38
39
def bert_mlm_strategy(input_ids, tokenizer, mask_rate=0.15):
"""
BERT的MLM掩码策略实现
"""
labels = input_ids.clone()
# 创建掩码矩阵,15%的位置被选中
mask_matrix = torch.rand(input_ids.shape) < mask_rate

# 避免对特殊标记进行掩码
special_tokens = [tokenizer.cls_token_id, tokenizer.sep_token_id, tokenizer.pad_token_id]
for token in special_tokens:
mask_matrix &= (input_ids != token)

# 应用80-10-10策略
random_matrix = torch.rand(input_ids.shape)

# 80%替换为[MASK]
mask_mask = mask_matrix & (random_matrix < 0.8)
input_ids[mask_mask] = tokenizer.mask_token_id

# 10%替换为随机词
random_mask = mask_matrix & (random_matrix >= 0.8) & (random_matrix < 0.9)
random_words = torch.randint(len(tokenizer), input_ids.shape)
input_ids[random_mask] = random_words[random_mask]

# 10%保持不变
return input_ids, labels, mask_matrix

# MLM损失计算
class MLMLoss(nn.Module):
def __init__(self):
super().__init__()
self.loss_fn = nn.CrossEntropyLoss()

def forward(self, predictions, labels, mask_positions):
# 只计算被掩码位置的损失
masked_predictions = predictions[mask_positions]
masked_labels = labels[mask_positions]
return self.loss_fn(masked_predictions, masked_labels)

MLM策略设计原理

  • 80% [MASK]替换:让模型学习基于上下文预测被掩盖的词
  • 10% 随机替换:增强模型对噪声的鲁棒性
  • 10% 保持不变:防止模型过度依赖[MASK]标记,保持表示一致性

2.2 下一句预测(NSP)

NSP任务旨在让模型理解句子间的逻辑关系,是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
25
26
def prepare_nsp_data(sentence_a, sentence_b, tokenizer, max_length=512):
"""
准备NSP任务的输入数据
"""
# 添加特殊标记:[CLS]句子A[SEP]句子B[SEP]
inputs = tokenizer(
sentence_a,
sentence_b,
max_length=max_length,
padding='max_length',
truncation=True,
return_tensors='pt'
)

# token_type_ids用于区分两个句子
# 句子A对应0,句子B对应1
return {
'input_ids': inputs['input_ids'],
'attention_mask': inputs['attention_mask'],
'token_type_ids': inputs['token_type_ids']
}

# NSP任务示例
sentence_a = "今天天气很好"
sentence_b = "我决定去公园散步"
nsp_input = prepare_nsp_data(sentence_a, sentence_b, tokenizer)

NSP任务的输入格式

1
[CLS] 句子A [SEP] 句子B [SEP]

标签类型

  • IsNext(50%):句子B是句子A的实际后续句子
  • NotNext(50%):句子B是随机选择的无关句子

3 实践环节:BERT模型实战

3.1 环境准备与模型加载

1
2
3
4
5
6
7
8
9
10
11
12
13
# 安装必要的库
# pip install transformers torch

from transformers import BertModel, BertTokenizer, BertForPreTraining
import torch

# 加载中文BERT模型和分词器
model_name = 'bert-base-chinese'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name)

print(f"词汇表大小: {tokenizer.vocab_size}")
print(f"模型参数数量: {sum(p.numel() for p in model.parameters()):,}")

3.2 文本预处理与输入分析

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
def analyze_bert_inputs(text, tokenizer):
"""
分析BERT的输入张量格式
"""
# 分词和编码
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)

print("=== BERT输入张量分析 ===")
print(f"原始文本: {text}")
print(f"Tokenized: {tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])}")
print(f"input_ids shape: {inputs['input_ids'].shape}")
print(f"attention_mask shape: {inputs['attention_mask'].shape}")
print(f"token_type_ids shape: {inputs['token_type_ids'].shape}")

# 详细解析每个输入张量的含义
print("\n--- 张量含义说明 ---")
print("1. input_ids: 词元ID序列,包含[CLS]、文本token和[SEP]")
print("2. attention_mask: 注意力掩码,1表示实际token,0表示padding")
print("3. token_type_ids: 句子标识,0表示第一句话,1表示第二句话")

return inputs

# 示例文本处理
text = "自然语言处理是人工智能的重要分支。"
inputs = analyze_bert_inputs(text, tokenizer)

3.3 模型推理与隐藏状态分析

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def extract_bert_hidden_states(model, inputs, layer_to_analyze=-1):
"""
提取BERT的隐藏状态并进行分析
"""
model.eval()

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

# 分析输出结构
print("\n=== BERT输出分析 ===")
print(f"最后一层隐藏状态 shape: {outputs.last_hidden_state.shape}")
print(f"池化输出 shape: {outputs.pooler_output.shape if outputs.pooler_output is not None else 'N/A'}")
print(f"总隐藏层数: {len(outputs.hidden_states)}")

# 分析指定层的隐藏状态
if layer_to_analyze == -1:
layer_to_analyze = len(outputs.hidden_states) - 1

hidden_state = outputs.hidden_states[layer_to_analyze]
print(f"\n第{layer_to_analyze}层隐藏状态分析:")
print(f"Shape: {hidden_state.shape}") # [batch_size, seq_len, hidden_size]
print(f"数值范围: [{hidden_state.min():.4f}, {hidden_state.max():.4f}]")
print(f"平均值: {hidden_state.mean():.4f}")

# [CLS] token的表示(常用于分类任务)
cls_embedding = hidden_state[0, 0, :] # 取第一个样本的第一个token
print(f"[CLS] token维度: {cls_embedding.shape}")

return outputs

# 执行推理
outputs = extract_bert_hidden_states(model, inputs)

# 可视化注意力权重(可选)
def visualize_attention(inputs, tokenizer, model, layer=0, head=0):
"""
可视化特定层和头的注意力权重
"""
model.eval()
with torch.no_grad():
outputs = model(**inputs, output_attentions=True)

attentions = outputs.attentions # 所有层的注意力权重
attention = attentions[layer][0, head] # 取第一个样本,指定头的注意力

# 可视化代码(需要matplotlib)
import matplotlib.pyplot as plt

tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
plt.figure(figsize=(10, 8))
plt.imshow(attention.numpy(), cmap='hot', interpolation='nearest')
plt.xticks(range(len(tokens)), tokens, rotation=45)
plt.yticks(range(len(tokens)), tokens)
plt.title(f"Attention Weights - Layer {layer}, Head {head}")
plt.colorbar()
plt.show()

return attention

3.4 完整实践示例:文本相似度计算

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
29
30
31
32
33
34
35
36
def bert_text_similarity(text1, text2, model, tokenizer):
"""
使用BERT计算两个文本的语义相似度
"""
# 编码文本
inputs1 = tokenizer(text1, return_tensors='pt', padding=True, truncation=True)
inputs2 = tokenizer(text2, return_tensors='pt', padding=True, truncation=True)

model.eval()
with torch.no_grad():
# 获取文本表示
outputs1 = model(**inputs1)
outputs2 = model(**inputs2)

# 使用[CLS] token的表示
embedding1 = outputs1.last_hidden_state[0, 0, :] # 第一个样本的[CLS] token
embedding2 = outputs2.last_hidden_state[0, 0, :]

# 计算余弦相似度
cosine_sim = torch.nn.functional.cosine_similarity(
embedding1.unsqueeze(0),
embedding2.unsqueeze(0)
)

return cosine_sim.item()

# 测试文本相似度
text1 = "今天天气很好,我想去公园散步"
text2 = "阳光明媚,我打算去公园走走"
text3 = "机器学习是人工智能的重要分支"

sim12 = bert_text_similarity(text1, text2, model, tokenizer)
sim13 = bert_text_similarity(text1, text3, model, tokenizer)

print(f"相似文本相似度: {sim12:.4f}")
print(f"不相似文本相似度: {sim13:.4f}")

4 关键知识点总结

4.1 Transformer Encoder核心要点

  1. 自注意力机制:实现序列内全局依赖捕捉,避免RNN的顺序处理限制
  2. 位置编码:通过正余弦函数注入位置信息,弥补自注意力缺乏位置感知的缺陷
  3. 残差连接与层归一化:稳定深度网络训练,防止梯度消失
  4. 前馈神经网络:提供非线性变换能力,增强模型表达能力

4.2 BERT预训练关键创新

  1. 掩码语言模型(MLM):15%掩码率与80-10-10策略的平衡设计
  2. 下一句预测(NSP):句子级语义关系理解能力
  3. 双向上下文利用:同时考虑左右上下文,突破传统语言模型的单向性限制

4.3 实践注意事项

  1. 输入格式:正确处理[CLS]、[SEP]等特殊标记和token_type_ids
  2. 注意力掩码:区分实际token与padding,避免无效计算
  3. 隐藏状态利用:根据不同任务选择合适的隐藏层输出(最后层、所有层平均或[CLS] token)

通过本教程的理论学习和实践操作,你应该已经掌握了Transformer Encoder的核心原理、BERT的预训练机制以及实际应用方法。这些知识为后续的NLP任务微调和模型优化奠定了坚实基础。