Transformer Encoder与BERT预训练实战教程
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) self.register_buffer('pe', pe) def forward(self, x): 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() 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) random_matrix = torch.rand(input_ids.shape) mask_mask = mask_matrix & (random_matrix < 0.8) input_ids[mask_mask] = tokenizer.mask_token_id 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] return input_ids, labels, mask_matrix
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任务的输入数据 """ inputs = tokenizer( sentence_a, sentence_b, max_length=max_length, padding='max_length', truncation=True, return_tensors='pt' ) return { 'input_ids': inputs['input_ids'], 'attention_mask': inputs['attention_mask'], 'token_type_ids': inputs['token_type_ids'] }
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
|
from transformers import BertModel, BertTokenizer, BertForPreTraining import torch
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}") print(f"数值范围: [{hidden_state.min():.4f}, {hidden_state.max():.4f}]") print(f"平均值: {hidden_state.mean():.4f}") cls_embedding = hidden_state[0, 0, :] 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] 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) embedding1 = outputs1.last_hidden_state[0, 0, :] 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 关键知识点总结
- 自注意力机制:实现序列内全局依赖捕捉,避免RNN的顺序处理限制
- 位置编码:通过正余弦函数注入位置信息,弥补自注意力缺乏位置感知的缺陷
- 残差连接与层归一化:稳定深度网络训练,防止梯度消失
- 前馈神经网络:提供非线性变换能力,增强模型表达能力
4.2 BERT预训练关键创新
- 掩码语言模型(MLM):15%掩码率与80-10-10策略的平衡设计
- 下一句预测(NSP):句子级语义关系理解能力
- 双向上下文利用:同时考虑左右上下文,突破传统语言模型的单向性限制
4.3 实践注意事项
- 输入格式:正确处理[CLS]、[SEP]等特殊标记和token_type_ids
- 注意力掩码:区分实际token与padding,避免无效计算
- 隐藏状态利用:根据不同任务选择合适的隐藏层输出(最后层、所有层平均或[CLS] token)
通过本教程的理论学习和实践操作,你应该已经掌握了Transformer Encoder的核心原理、BERT的预训练机制以及实际应用方法。这些知识为后续的NLP任务微调和模型优化奠定了坚实基础。