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)奠定基础

更多推荐