BERT底层原理解析及基于pytorch的代码实现
1. BERT底层原理深度解析
1.1 核心架构
BERT(Bidirectional Encoder Representations from Transformers)是自然语言处理领域的里程碑模型,其核心是通过大规模无监督预训练获取深度双向语言表示。以下是其关键原理分析:
1)Transformer Encoder架构
- 仅使用Transformer的编码器堆叠(Base:12层,Large:24层)
- 每个Encoder Layer包含:
- 多头自注意力(Multi-Head Self-Attention)
- 前馈网络(Position-wise FFN)
- 残差连接+层归一化
2)双向上下文建模
- 突破传统语言模型的单向限制(如GPT)
- 通过掩码机制实现真正的双向理解
- 数学表达:
$$ h_i = \text{TransformerBlock}(h_{<i}, h_i, h_{>i}) $$
3)预训练任务
Masked Language Modeling (MLM),表达式(修正为条件概率形式):
![]()
-
符号说明
-
:原始输入序列,共 n 个 token。 -
:被随机选为“掩盖”(masked)的 token 位置集合,通常占总数的 15%。 -
:所有被掩盖位置上的真实 token。 -
:除掩盖位置外,其余位置的 token(即模型实际可见的输入)。
-
-
概率含义
模型需要对每个被掩盖位置 ii 给出条件概率分布 p(xi∣其余可见 tokens)p(x_i\mid \text{其余可见 tokens}),再将这些位置的联合概率相乘。
-
目标函数
通常取对数并求和,得到最大化对数似然:
训练时等价于对所有掩码位置的交叉熵损失求和,然后取负号最小化:

Next Sentence Prediction (NSP),表达式:

-
符号说明
-
A,BA, B:分别表示第一句和第二句的 token 序列。
-
is_next∈{True,False}\text{is\_next}\in\{\text{True},\text{False}\}:一个二分类标签,True 表示 BB 确实是原文中紧跟在 AA 之后的句子,False 则表示从语料库中随机抽取的“负例”句子。
-
-
概率含义
模型在给定句子对 (A,B)(A,B) 后,输出一个二分类的概率:
并以交叉熵形式计算对数似然。
-
目标函数
对整个训练集上的句对累加对数概率:
其中 yy 为该句对的真实标签(True/False)。训练时等价于最小化负对数似然(交叉熵):

-
MLM 关注单个 token 在上下文(剩余 tokens)下的预测准确度,通过掩盖一部分输入并最大化它们的条件概率。
-
NSP 则是对句子级别的连贯性进行建模,判断两句是否相邻,通过二分类概率最大化句对标签的对数似然。
两者联合构成了像 BERT 这类模型的预训练目标,使模型既能学习深层的上下文表征,又能把握句与句之间的连贯关系。
4)输入表示
- Token Embeddings:WordPiece分词
- Segment Embeddings:区分句子A/B
- Position Embeddings:绝对位置编码
- 特殊标记:`[CLS]`用于分类,`[SEP]`分隔句子
1.2 关键技术细节
1)动态掩码策略**
- 80%替换为`[MASK]`
- 10%随机替换为其他词
- 10%保持原词(缓解预训练-微调差异)
2)注意力头模式分析
- 不同头捕获不同语法/语义关系
- 高层头更关注语义角色,底层头关注局部语法
3)微调范式
- 通过添加任务特定输出层适配下游任务
- 通用架构适配多种任务:
- 单句分类([CLS]向量)
- 句子对分类([CLS]+Segment Emb)
- 序列标注(每个token输出)
- Q&A(起始/结束位置预测)
2. 基于PyTorch实现
import torch
import torch.nn as nn
from torch.nn import functional as F
import math
class BertEmbeddings(nn.Module):
def __init__(self, vocab_size, hidden_size=768, max_position=512):
super().__init__()
self.token_emb = nn.Embedding(vocab_size, hidden_size)
self.pos_emb = nn.Embedding(max_position, hidden_size)
self.seg_emb = nn.Embedding(2, hidden_size) # 句子类型A/B
self.norm = nn.LayerNorm(hidden_size)
self.dropout = nn.Dropout(0.1)
def forward(self, input_ids, segment_ids):
seq_len = input_ids.size(1)
pos_ids = torch.arange(seq_len, dtype=torch.long, device=input_ids.device).unsqueeze(0)
# 三种嵌入求和
token_emb = self.token_emb(input_ids)
pos_emb = self.pos_emb(pos_ids)
seg_emb = self.seg_emb(segment_ids)
embeddings = token_emb + pos_emb + seg_emb
return self.dropout(self.norm(embeddings))
class BertSelfAttention(nn.Module):
def __init__(self, hidden_size=768, num_heads=12):
super().__init__()
assert hidden_size % num_heads == 0
self.num_heads = num_heads
self.head_size = hidden_size // num_heads
self.q = nn.Linear(hidden_size, hidden_size)
self.k = nn.Linear(hidden_size, hidden_size)
self.v = nn.Linear(hidden_size, hidden_size)
self.dropout = nn.Dropout(0.1)
def split_heads(self, x):
return x.view(x.size(0), -1, self.num_heads, self.head_size).transpose(1, 2)
def forward(self, x, attention_mask=None):
# 线性变换 + 分头
q = self.split_heads(self.q(x)) # (B, h, S, d_k)
k = self.split_heads(self.k(x))
v = self.split_heads(self.v(x))
# 计算注意力分数
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_size)
if attention_mask is not None:
scores = scores.masked_fill(attention_mask == 0, -1e9)
attn_weights = F.softmax(scores, dim=-1)
attn_weights = self.dropout(attn_weights)
# 加权求和
context = torch.matmul(attn_weights, v)
context = context.transpose(1, 2).contiguous().view(x.size(0), -1, x.size(2))
return context
class BertLayer(nn.Module):
def __init__(self, hidden_size=768, num_heads=12, intermediate_size=3072):
super().__init__()
self.attention = BertSelfAttention(hidden_size, num_heads)
self.ffn = nn.Sequential(
nn.Linear(hidden_size, intermediate_size),
nn.GELU(),
nn.Linear(intermediate_size, hidden_size),
nn.Dropout(0.1)
)
self.norm1 = nn.LayerNorm(hidden_size)
self.norm2 = nn.LayerNorm(hidden_size)
def forward(self, x, attention_mask=None):
# 自注意力子层
attn_output = self.attention(x, attention_mask)
x = self.norm1(x + attn_output)
# 前馈子层
ffn_output = self.ffn(x)
x = self.norm2(x + ffn_output)
return x
class BertModel(nn.Module):
def __init__(self, vocab_size, hidden_size=768, num_layers=12, num_heads=12):
super().__init__()
self.embeddings = BertEmbeddings(vocab_size, hidden_size)
self.layers = nn.ModuleList(
[BertLayer(hidden_size, num_heads) for _ in range(num_layers)]
)
def forward(self, input_ids, segment_ids, attention_mask=None):
x = self.embeddings(input_ids, segment_ids)
for layer in self.layers:
x = layer(x, attention_mask)
return x
class BertForPreTraining(nn.Module):
def __init__(self, vocab_size):
super().__init__()
self.bert = BertModel(vocab_size)
self.mlm_head = nn.Sequential(
nn.Linear(768, 768),
nn.GELU(),
nn.LayerNorm(768),
nn.Linear(768, vocab_size)
)
self.nsp_head = nn.Linear(768, 2)
def forward(self, input_ids, segment_ids, attention_mask=None):
sequence_output = self.bert(input_ids, segment_ids, attention_mask)
# [CLS]向量用于NSP
cls_output = sequence_output[:, 0, :]
nsp_logits = self.nsp_head(cls_output)
# MLM预测
mlm_logits = self.mlm_head(sequence_output)
return mlm_logits, nsp_logits
# 示例使用
if __name__ == "__main__":
vocab_size = 30522 # BERT-base词表大小
# 模拟输入
input_ids = torch.randint(0, vocab_size, (2, 512)) # (batch, seq_len)
segment_ids = torch.cat([
torch.zeros(2, 256),
torch.ones(2, 256)
], dim=1).long()
attention_mask = torch.ones(2, 512) # 实际使用需处理padding
# 初始化模型
model = BertForPreTraining(vocab_size)
# 前向传播
mlm_logits, nsp_logits = model(input_ids, segment_ids, attention_mask)
print("MLM logits shape:", mlm_logits.shape) # (2, 512, 30522)
print("NSP logits shape:", nsp_logits.shape) # (2, 2)
3. 代码解释
1)嵌入层(BertEmbeddings)
pos_ids = torch.arange(seq_len).unsqueeze(0)
- 生成位置ID:`[0, 1, 2,..., seq_len-1]`
- `unsqueeze(0)` 添加批次维度以匹配输入形状
embeddings = token_emb + pos_emb + seg_emb
- 三种嵌入的求和操作(非拼接)
- 与原始Transformer的位置编码实现方式不同
2)自注意力(BertSelfAttention)
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_size)
- 缩放点积计算注意力分数
- 防止点积结果过大导致softmax梯度消失
scores.masked_fill(attention_mask == 0, -1e9)
- 应用注意力掩码(padding mask)
- 将无效位置的分数设为极小值,softmax后接近0
3)预训练头(BertForPreTraining)
cls_output = sequence_output[:, 0, :]
- 提取[CLS]标记的隐藏状态
- 用于NSP任务(传统做法,最新研究显示可能不是最优)
self.mlm_head = nn.Sequential(...)
- MLM预测头:两层线性变换+GELU激活
- 与原始BERT实现完全一致
4. 扩展建议
1)高效训练策略
- 动态掩码:每个epoch重新生成掩码模式
- 梯度累积:突破GPU显存限制
- 混合精度训练:`torch.cuda.amp`
2)模型压缩技术
- 知识蒸馏:使用大模型训练小模型(如DistilBERT)
- 量化感知训练:8bit整数精度
- 参数共享:ALBERT的跨层参数共享
3)预训练改进
- Whole Word Masking:掩盖完整词语
- SOP(Sentence Order Prediction):替代NSP
- ELECTRA的替换token检测任务
4)多语言扩展
- 共享词表与语言特定嵌入
- 语言对抗训练(消除语言标识偏差)
5)领域适配
- 持续预训练(Continue Pretraining)
- 领域词表扩展(添加专业术语)
- 实体类型增强(ERNIE风格)
通过深入理解BERT的架构设计与实现细节,开发者可以:
- 构建领域专用预训练模型(如BioBERT、LegalBERT)
- 优化微调策略(如Layer-wise Learning Rate Decay)
- 开发基于BERT的工业级NLP系统(如智能客服、文档分析)
- 为理解更复杂的大模型(如T5、PaLM)奠定基础
更多推荐

所有评论(0)