PyTorch实战:从零开始构建BERT预训练模型的完整指南

1次阅读
没有评论

共计 5619 个字符,预计需要花费 15 分钟才能阅读完成。

image.webp

背景介绍

BERT(Bidirectional Encoder Representations from Transformers)是谷歌在 2018 年提出的预训练语言模型,它通过双向 Transformer 架构和掩码语言模型(MLM)任务,显著提升了 NLP 任务的性能。BERT 在文本分类、问答系统、命名实体识别等任务中表现出色,成为 NLP 领域的里程碑式模型。

PyTorch 实战:从零开始构建 BERT 预训练模型的完整指南

技术选型

在实现 BERT 模型时,TensorFlow 和 PyTorch 是两大主流框架。以下是它们的对比:

  • TensorFlow
  • 优点:官方支持 BERT 实现,社区资源丰富
  • 缺点:静态计算图,调试较复杂

  • PyTorch

  • 优点:动态计算图,易于调试和实验
  • 缺点:官方 BERT 实现较少,需要自行构建

对于初学者,PyTorch 的灵活性和易用性使其成为更好的选择。

核心实现

BERT 模型架构的 PyTorch 实现

BERT 的核心是 Transformer 编码器堆叠。以下是关键组件的实现:

import torch
import torch.nn as nn

class BertEmbeddings(nn.Module):
    def __init__(self, vocab_size, hidden_size, max_position_embeddings, dropout_prob):
        super().__init__()
        self.word_embeddings = nn.Embedding(vocab_size, hidden_size)
        self.position_embeddings = nn.Embedding(max_position_embeddings, hidden_size)
        self.token_type_embeddings = nn.Embedding(2, hidden_size)
        self.LayerNorm = nn.LayerNorm(hidden_size)
        self.dropout = nn.Dropout(dropout_prob)

    def forward(self, input_ids, token_type_ids=None, position_ids=None):
        seq_length = input_ids.size(1)
        if position_ids is None:
            position_ids = torch.arange(seq_length, dtype=torch.long, device=input_ids.device)
            position_ids = position_ids.unsqueeze(0).expand_as(input_ids)
        if token_type_ids is None:
            token_type_ids = torch.zeros_like(input_ids)

        words_embeddings = self.word_embeddings(input_ids)
        position_embeddings = self.position_embeddings(position_ids)
        token_type_embeddings = self.token_type_embeddings(token_type_ids)

        embeddings = words_embeddings + position_embeddings + token_type_embeddings
        embeddings = self.LayerNorm(embeddings)
        embeddings = self.dropout(embeddings)
        return embeddings

注意力机制的关键代码

自注意力机制是 Transformer 的核心,以下是多头注意力的实现:

class BertSelfAttention(nn.Module):
    def __init__(self, hidden_size, num_attention_heads, attention_probs_dropout_prob):
        super().__init__()
        self.num_attention_heads = num_attention_heads
        self.attention_head_size = int(hidden_size / num_attention_heads)
        self.all_head_size = self.num_attention_heads * self.attention_head_size

        self.query = nn.Linear(hidden_size, self.all_head_size)
        self.key = nn.Linear(hidden_size, self.all_head_size)
        self.value = nn.Linear(hidden_size, self.all_head_size)

        self.dropout = nn.Dropout(attention_probs_dropout_prob)

    def transpose_for_scores(self, x):
        new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size)
        x = x.view(*new_x_shape)
        return x.permute(0, 2, 1, 3)

    def forward(self, hidden_states, attention_mask=None):
        mixed_query_layer = self.query(hidden_states)
        mixed_key_layer = self.key(hidden_states)
        mixed_value_layer = self.value(hidden_states)

        query_layer = self.transpose_for_scores(mixed_query_layer)
        key_layer = self.transpose_for_scores(mixed_key_layer)
        value_layer = self.transpose_for_scores(mixed_value_layer)

        attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
        attention_scores = attention_scores / math.sqrt(self.attention_head_size)
        if attention_mask is not None:
            attention_scores = attention_scores + attention_mask

        attention_probs = nn.Softmax(dim=-1)(attention_scores)
        attention_probs = self.dropout(attention_probs)

        context_layer = torch.matmul(attention_probs, value_layer)
        context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
        new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,)
        context_layer = context_layer.view(*new_context_layer_shape)
        return context_layer

位置编码的实现细节

BERT 使用可学习的位置编码而非固定的正弦 / 余弦函数:

class BertPositionalEmbedding(nn.Module):
    def __init__(self, max_position_embeddings, hidden_size):
        super().__init__()
        self.position_embeddings = nn.Embedding(max_position_embeddings, hidden_size)

    def forward(self, input_ids):
        seq_length = input_ids.size(1)
        position_ids = torch.arange(seq_length, dtype=torch.long, device=input_ids.device)
        position_ids = position_ids.unsqueeze(0).expand_as(input_ids)
        position_embeddings = self.position_embeddings(position_ids)
        return position_embeddings

完整代码示例

以下是 BERT 模型的完整 PyTorch 实现框架:

import math
import torch
import torch.nn as nn

class BertModel(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.embeddings = BertEmbeddings(config.vocab_size, config.hidden_size,
                                        config.max_position_embeddings, config.hidden_dropout_prob)
        self.encoder = BertEncoder(config)
        self.pooler = BertPooler(config)

    def forward(self, input_ids, attention_mask=None, token_type_ids=None, position_ids=None):
        if attention_mask is None:
            attention_mask = torch.ones_like(input_ids)
        if token_type_ids is None:
            token_type_ids = torch.zeros_like(input_ids)

        extended_attention_mask = attention_mask.unsqueeze(1).unsqueeze(2)
        extended_attention_mask = extended_attention_mask.to(dtype=next(self.parameters()).dtype)
        extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0

        embedding_output = self.embeddings(input_ids, token_type_ids, position_ids)
        encoder_outputs = self.encoder(embedding_output, extended_attention_mask)
        sequence_output = encoder_outputs[0]
        pooled_output = self.pooler(sequence_output)
        return sequence_output, pooled_output

训练优化

学习率调度策略

BERT 训练通常使用带 warmup 的线性衰减学习率:

from transformers import get_linear_schedule_with_warmup

total_steps = len(train_dataloader) * epochs
optimizer = AdamW(model.parameters(), lr=5e-5, correct_bias=False)
scheduler = get_linear_schedule_with_warmup(
    optimizer,
    num_warmup_steps=0.1 * total_steps,
    num_training_steps=total_steps
)

批量大小选择

  • 根据 GPU 内存选择最大可能的批量大小
  • 典型值:16-32(单卡),64-128(多卡)

梯度累积技巧

当显存不足时,可以通过梯度累积模拟更大的批量:

accumulation_steps = 4
for step, batch in enumerate(train_dataloader):
    outputs = model(**batch)
    loss = outputs[0]
    loss = loss / accumulation_steps
    loss.backward()

    if (step + 1) % accumulation_steps == 0:
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad()

避坑指南

  1. 忘记设置模型为训练模式
  2. 解决方案:训练前调用 model.train(),评估前调用 model.eval()

  3. 忽略注意力掩码

  4. 解决方案:正确处理 padding tokens 的注意力掩码

  5. 学习率设置不当

  6. 解决方案:使用 warmup 策略,初始学习率 5e-5

  7. 批量归一化问题

  8. 解决方案:BERT 使用 LayerNorm 而非 BatchNorm

  9. GPU 内存不足

  10. 解决方案:减小批量大小或使用梯度累积

性能考量

  • 模型大小 :base 版(110M 参数)vs large 版(340M 参数)
  • 训练效率
  • 使用混合精度训练(torch.cuda.amp
  • 考虑模型并行或数据并行

实践建议

  1. 从预训练模型微调开始,而非从头训练
  2. 使用 HuggingFace 的 transformers 库作为基础
  3. 监控训练过程中的损失和指标

思考题

  1. 如何修改 BERT 架构以适应特定领域的 NLP 任务?
  2. 在大规模数据集上训练 BERT 时,有哪些优化策略可以加速训练?
  3. 除了 MLM 任务,还可以设计哪些预训练任务来提升 BERT 的性能?
正文完
 0
评论(没有评论)