目 录CONTENT

文章目录

从零开始预训练BERT模型

Administrator
2025-11-26 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

📢 转载信息

原文链接:https://machinelearningmastery.com/pretrain-a-bert-model-from-scratch/

原文作者:Jason Brownlee


BERT代表Bidirectional Encoder Representations from Transformers(来自Transformer的双向编码器表示),是2018年Google在论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》中提出的一种革命性的语言模型。

这项工作被视为自然语言处理(NLP)领域的一个重要里程碑,因为它表明了深度双向Transformer模型在各种下游任务(如问答、文本蕴含和命名实体识别)上取得了最先进(state-of-the-art)的成果。这些成果是通过在大量文本数据上进行预训练(pre-training)并随后在特定任务上进行微调(fine-tuning)实现的。

本文的目的是向您展示如何使用Python和PyTorch从零开始预训练一个BERT模型。我们将重点关注模型架构预训练任务,而不是数据准备和训练过程本身,因为这些是特定于任务和数据集的。

我们将实现BERT的核心组件,包括:

  • Transformer编码器:用于理解输入文本。
  • 掩码语言模型(MLM):一个预训练目标,用于学习单词的上下文表示。
  • 下一句预测(NSP):另一个预训练目标,用于学习句子关系。

注意:本教程的重点是理解BERT的核心组件,而不是创建一个可以直接在大型语料库上训练的完整、可部署的框架。训练一个真正的BERT模型需要大量的计算资源和时间。

BERT架构概述

BERT模型基于Transformer架构,特别是其编码器部分。它是一个由多个堆叠的编码器层组成的深度网络。

Token嵌入层

在BERT中,输入Token的嵌入(embedding)是三种不同嵌入的组合:

  1. Token 嵌入:标准词嵌入。
  2. 位置嵌入:指示序列中Token的位置。
  3. 分段嵌入:指示Token属于输入序列中的哪个部分(通常用于处理两个句子)。

这些嵌入随后相加,形成最终的输入表示。

BERT嵌入示意图

在我们的实现中,我们将关注核心的Token嵌入位置嵌入。分段嵌入对于我们的单序列预训练任务(MLM)来说是不必要的。

Transformer编码器层

BERT的核心是Transformer编码器。它由一个多头自注意力机制(multi-head self-attention)和一个前馈网络(feed-forward network)组成,每个子层后面都有一个残差连接(residual connection)和一个层归一化(layer normalization)。

我们首先需要实现注意力机制。

自注意力机制(Self-Attention)

注意力机制允许模型在处理序列中的每个Token时,权衡序列中所有其他Token的重要性。

在PyTorch中,我们可以使用nn.MultiheadAttention模块来实现这一点,这比手动构建它更简单,但也允许我们了解其内部工作原理。

前馈网络

注意力子层输出通过一个两层全连接网络,使用GELU(Gaussian Error Linear Unit)激活函数。

BERT模型(编码器堆栈)

BERT模型本质上是许多编码器层的堆栈。输入嵌入通过所有这些层,并且在每层之后应用层归一化和残差连接。

我们不需要在预训练过程中重新发明Transformer编码器,而是使用PyTorch的内置模块nn.TransformerEncoder

预训练任务

BERT通过两个无监督的辅助任务进行预训练:掩码语言模型 (MLM)下一句预测 (NSP)

掩码语言模型 (MLM)

MLM是BERT预训练的关键。它类似于传统的因果语言建模,但它是双向的。在MLM中,输入序列中的一些Token(通常是15%)被随机掩盖(masked),然后模型的目标是预测原始Token。

这个过程需要我们创建一个掩码函数,用于:

  1. 随机选择输入序列中的Token进行修改。
  2. 替换选定的Token为特殊[MASK]Token。
  3. 记录原始Token,以便计算损失。

BERT的原始实现采用了一种更复杂的方法:在被选中的Token中,80%被替换为[MASK],10%被替换为随机Token,剩下的10%保持不变。我们将使用更简单的15%的[MASK]方法进行演示。

下一句预测 (NSP)

NSP是一个二元分类任务,旨在让模型学习句子之间的关系。对于训练样本,一半是来自同一文档的连续句子对(标记为IsNext),另一半是随机选择的句子对(标记为NotNext)。

在输入序列的开头,BERT使用一个特殊的[CLS]Token。对于NSP任务,模型使用[CLS]Token在所有层处理后的输出表示来预测下一个句子是否是原始序列中的下一个句子。

代码实现

我们将使用PyTorch来实现一个最小化的BERT预训练管道。为了保持代码简洁,我们将使用nn.TransformerEncodernn.Embedding

1. 实用程序函数:创建掩码

首先,我们需要一个函数来随机掩盖Token。

import torch
import torch.nn as nn
import torch.nn.functional as F

# ... 其他导入

def create_mlm_mask(input_tensor, mask_ratio=0.15):
    """随机掩盖输入张量中的Token"""
    batch_size, seq_len = input_tensor.shape
    mask = torch.zeros_like(input_tensor, dtype=torch.bool)
    
    # 计算需要掩盖的Token数量
    num_to_mask = int(seq_len * mask_ratio)
    
    # 为每个样本生成掩码
    for i in range(batch_size):
        # 随机选择需要掩盖的Token索引(不包括特殊Token,如[CLS]和[SEP])
        # 假设[CLS]在索引0,[SEP]在索引seq_len-1
        token_indices = torch.randperm(seq_len - 2)[:num_to_mask] + 1
        mask[i, token_indices] = True
        
    # 假设我们有一个特殊的MASK Token ID,比如103
    MASK_TOKEN_ID = 103
    masked_input = input_tensor.clone()
    masked_input[mask] = MASK_TOKEN_ID
    
    return masked_input, mask

2. BERT模型定义

我们将定义一个包含Token嵌入、位置嵌入、Transformer编码器和MLM头(head)的模型。

class TinyBERT(nn.Module):
    def __init__(self, vocab_size, d_model=128, nhead=4, num_layers=2, max_len=512, dropout=0.1):
        super().__init__()
        self.d_model = d_model
        
        # 嵌入层
        self.token_embedding = nn.Embedding(vocab_size, d_model)
        self.position_embedding = nn.Embedding(max_len, d_model)
        self.dropout = nn.Dropout(dropout)

        # Transformer编码器
        encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, dim_feedforward=d_model*4, dropout=dropout, batch_first=True)
        self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        
        # MLM头:将模型输出映射回词汇表大小
        self.mlm_head = nn.Linear(d_model, vocab_size)

    def forward(self, input_ids):
        # 1. 创建嵌入
        token_ids = input_ids
        batch_size, seq_len = token_ids.shape
        
        # 创建位置索引 [0, 1, ..., seq_len-1]
        positions = torch.arange(0, seq_len, device=input_ids.device).unsqueeze(0).repeat(batch_size, 1)
        
        # 计算嵌入
        token_emb = self.token_embedding(token_ids)
        pos_emb = self.position_embedding(positions)
        
        # 组合嵌入
        embeddings = token_emb + pos_emb
        embeddings = self.dropout(embeddings)
        
        # 2. Transformer编码器
        # 注意:这里我们假设batch_first=True,如果使用旧版PyTorch可能需要转置
        transformer_output = self.transformer_encoder(embeddings)
        
        # 3. MLM预测
        mlm_logits = self.mlm_head(transformer_output)
        
        return mlm_logits

3. 模拟训练循环(MLM)

现在我们定义一个简单的循环来模拟使用MLM任务对模型进行训练的过程。

# --- 假设的参数和模型初始化 ---
VOCAB_SIZE = 30000
MAX_LEN = 128

model = TinyBERT(vocab_size=VOCAB_SIZE, d_model=128, num_layers=2, max_len=MAX_LEN)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss(ignore_index=0) # 假设0是填充(padding)Token ID

# --- 模拟数据加载器 (DataLoader) ---
# 假设我们有一个包含Token ID序列的张量
# 真实场景中,数据加载器会处理数据批次和填充

def get_dummy_batch(batch_size=4, seq_len=MAX_LEN):
    # 模拟一些随机的输入Token ID(避免使用特殊Token ID 0, 103)
    return torch.randint(low=2, high=VOCAB_SIZE, size=(batch_size, seq_len))

# --- 训练步骤 ---
model.train()

input_ids = get_dummy_batch()

# 1. 创建MLM掩码
masked_input, mlm_mask = create_mlm_mask(input_ids, mask_ratio=0.15)

# 2. 前向传播
mlm_logits = model(masked_input)

# 3. 计算损失
# 仅计算被掩盖Token的损失
# 维度: [B*L, V] vs [B*L]

# 将logits和掩码展平,只保留被掩盖的Token位置
logits_for_loss = mlm_logits[mlm_mask]
labels_for_loss = input_ids[mlm_mask]

# 检查是否有任何Token被掩盖
if labels_for_loss.numel() > 0:
    loss = criterion(logits_for_loss, labels_for_loss)
    
    # 4. 反向传播和优化
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    print(f"MLM Loss: {loss.item():.4f}")
else:
    print("Warning: No tokens were masked in this batch.")

# ------------------------------------------------------------------
# 模拟NSP任务(此处仅为概念性展示,实际实现需要更复杂的输入和输出处理)

class BertForNSP(nn.Module):
    def __init__(self, bert_model):
        super().__init__()
        self.bert = bert_model
        # NSP头使用[CLS]Token (索引0)的输出进行预测
        self.nsp_head = nn.Linear(bert_model.d_model, 2) # 2 classes: IsNext or NotNext
        
    def forward(self, input_ids, segment_ids=None):
        # 假设我们已经处理了输入,并获取了所有层的输出
        transformer_output = self.bert.transformer_encoder(self.bert.embeddings(input_ids)) # 简化处理
        
        # 使用[CLS] Token的输出 (通常是第一个Token的输出)
        cls_output = transformer_output[:, 0, :]
        nsp_logits = self.nsp_head(cls_output)
        return nsp_logits

# ------------------------------------------------------------------

总结

从零开始预训练BERT涉及构建一个Transformer编码器模型,并设计两个辅助任务:

  • MLM:通过预测被掩盖的Token来学习深度的上下文表示。
  • NSP:通过预测句子对关系来学习句子级别的理解。

虽然我们没有涵盖数据预处理(如WordPiece分词、创建训练数据对)的细节,但上述代码展示了BERT核心计算图的实现,即嵌入层 + Transformer编码器 + 预测头

掌握这些基础知识是进行任何高级NLP研究或定制模型训练的第一步。




🚀 想要体验更好更全面的AI调用?

欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。

0

评论区