
BERT 预训练实战PyTorch 复现 MLM 与 NSP 任务Loss 降至 0.15在自然语言处理领域BERTBidirectional Encoder Representations from Transformers的出现彻底改变了我们对语言模型的理解方式。与传统的单向语言模型不同BERT通过双向Transformer编码器实现了对文本上下文的全方位理解。本文将带您从零开始使用PyTorch框架复现BERT的核心预训练任务——Masked Language ModelMLM和Next Sentence PredictionNSP并分享如何将训练Loss降至0.15的实战经验。1. 环境准备与数据预处理1.1 安装依赖库首先需要安装必要的Python库。建议使用Python 3.8环境和最新版本的PyTorchpip install torch torchtext transformers tqdm numpy pandas1.2 数据集准备BERT预训练通常使用大规模文本语料库。对于实验目的我们可以使用Wikipedia或BookCorpus的小型子集from torchtext.datasets import WikiText2 # 加载WikiText2数据集 train_iter, valid_iter, test_iter WikiText2() text_data [text for text in train_iter if len(text.split()) 10] # 过滤过短句子1.3 构建词汇表与TokenizerBERT使用WordPiece分词方法。我们可以使用HuggingFace的BertTokenizer也可以自定义实现from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-uncased) vocab_size tokenizer.vocab_size # 305222. BERT模型架构实现2.1 Transformer编码器层BERT的核心是Transformer编码器。我们先实现多头注意力机制import torch import torch.nn as nn import math class MultiHeadAttention(nn.Module): def __init__(self, hidden_size768, num_heads12): super().__init__() self.hidden_size hidden_size self.num_heads num_heads self.head_dim hidden_size // num_heads self.query nn.Linear(hidden_size, hidden_size) self.key nn.Linear(hidden_size, hidden_size) self.value nn.Linear(hidden_size, hidden_size) self.dropout nn.Dropout(0.1) self.out nn.Linear(hidden_size, hidden_size) def forward(self, x, attention_maskNone): batch_size x.size(0) # 线性变换并分割为多头 Q self.query(x).view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2) K self.key(x).view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2) V self.value(x).view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2) # 计算注意力分数 scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim) # 应用注意力掩码 if attention_mask is not None: scores scores.masked_fill(attention_mask 0, -1e9) # 计算注意力权重 attn_weights torch.softmax(scores, dim-1) attn_weights self.dropout(attn_weights) # 应用注意力权重到V context torch.matmul(attn_weights, V) context context.transpose(1, 2).contiguous().view(batch_size, -1, self.hidden_size) return self.out(context)2.2 完整的BERT模型基于Transformer编码器构建BERT模型class BERT(nn.Module): def __init__(self, vocab_size, hidden_size768, num_layers12, num_heads12): super().__init__() self.embedding BERTEmbedding(vocab_size, hidden_size) self.encoder_layers nn.ModuleList([ TransformerEncoderLayer(hidden_size, num_heads) for _ in range(num_layers) ]) def forward(self, input_ids, segment_ids, attention_maskNone): # 获取嵌入表示 x self.embedding(input_ids, segment_ids) # 通过所有Transformer层 for layer in self.encoder_layers: x layer(x, attention_mask) return x3. 预训练任务实现3.1 Masked Language Model (MLM)MLM任务随机遮盖输入token并预测被遮盖的tokenclass MLMTask(nn.Module): def __init__(self, hidden_size, vocab_size): super().__init__() self.dense nn.Linear(hidden_size, hidden_size) self.layer_norm nn.LayerNorm(hidden_size) self.decoder nn.Linear(hidden_size, vocab_size) def forward(self, hidden_states, masked_positions): # 只选择被遮盖位置的隐藏状态 masked_states hidden_states.gather(1, masked_positions.unsqueeze(-1).expand(-1, -1, hidden_states.size(-1))) # 通过MLM头部 x self.dense(masked_states) x torch.gelu(x) x self.layer_norm(x) logits self.decoder(x) return logits3.2 Next Sentence Prediction (NSP)NSP任务预测两个句子是否是连续的class NSPTask(nn.Module): def __init__(self, hidden_size): super().__init__() self.seq_relationship nn.Linear(hidden_size, 2) def forward(self, pooled_output): logits self.seq_relationship(pooled_output) return logits4. 训练流程与优化4.1 数据批处理实现动态遮盖策略的DataLoaderfrom torch.utils.data import Dataset, DataLoader import random class BERTDataset(Dataset): def __init__(self, texts, tokenizer, max_len128): self.texts texts self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): # 随机选择两个句子 text self.texts[idx] sentences text.split(.) if len(sentences) 2: return self[(idx 1) % len(self)] sent_a, sent_b random.sample(sentences, 2) # 50%概率使用连续句子 if random.random() 0.5: is_next 1 sent_b sentences[sentences.index(sent_a) 1] else: is_next 0 # 编码句子对 encoded self.tokenizer( sent_a, sent_b, max_lengthself.max_len, paddingmax_length, truncationTrue, return_tensorspt ) # 创建MLM标签 input_ids encoded[input_ids].squeeze(0) mlm_labels input_ids.clone() # 随机遮盖15%的token mask_indices torch.rand(input_ids.shape) 0.15 # 80%替换为[MASK], 10%随机token, 10%保持不变 mask_token self.tokenizer.mask_token_id random_tokens torch.randint(0, len(self.tokenizer), input_ids.shape) input_ids[mask_indices] torch.where( torch.rand(input_ids.shape) 0.8, mask_token, torch.where( torch.rand(input_ids.shape) 0.5, random_tokens, input_ids ) )[mask_indices] return { input_ids: input_ids, attention_mask: encoded[attention_mask].squeeze(0), token_type_ids: encoded[token_type_ids].squeeze(0), mlm_labels: mlm_labels, nsp_labels: torch.tensor(is_next, dtypetorch.long) }4.2 训练循环实现包含MLM和NSP的联合训练def train(model, dataloader, optimizer, device, epochs10): model.train() total_loss 0 mlm_criterion nn.CrossEntropyLoss(ignore_index0) nsp_criterion nn.CrossEntropyLoss() for epoch in range(epochs): for batch in tqdm(dataloader, descfEpoch {epoch1}): # 准备输入数据 input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) token_type_ids batch[token_type_ids].to(device) mlm_labels batch[mlm_labels].to(device) nsp_labels batch[nsp_labels].to(device) # 获取被遮盖的位置 masked_positions (input_ids tokenizer.mask_token_id).nonzero(as_tupleTrue)[1] # 前向传播 outputs model(input_ids, token_type_ids, attention_mask) # 计算MLM损失 mlm_logits mlm_head(outputs, masked_positions) mlm_loss mlm_criterion( mlm_logits.view(-1, vocab_size), mlm_labels.view(-1) ) # 计算NSP损失 pooled_output outputs[:, 0, :] # [CLS] token nsp_logits nsp_head(pooled_output) nsp_loss nsp_criterion(nsp_logits, nsp_labels) # 总损失 loss mlm_loss nsp_loss total_loss loss.item() # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() avg_loss total_loss / len(dataloader) print(fEpoch {epoch1}, Loss: {avg_loss:.4f}) total_loss 05. 调优技巧与Loss优化5.1 学习率调度使用带热启动的线性学习率调度from torch.optim import AdamW from torch.optim.lr_scheduler import LambdaLR optimizer AdamW(model.parameters(), lr5e-5, weight_decay0.01) def lr_lambda(current_step): warmup_steps 1000 if current_step warmup_steps: return float(current_step) / float(max(1, warmup_steps)) return max(0.0, float(total_steps - current_step) / float(max(1, total_steps - warmup_steps))) scheduler LambdaLR(optimizer, lr_lambda)5.2 梯度累积对于小批量数据可以使用梯度累积accumulation_steps 4 for i, batch in enumerate(dataloader): # 前向传播和损失计算 loss loss / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad() scheduler.step()5.3 混合精度训练使用AMP加速训练并减少显存占用from torch.cuda.amp import GradScaler, autocast scaler GradScaler() with autocast(): outputs model(input_ids, token_type_ids, attention_mask) # 计算损失... scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()6. 监控与评估6.1 训练指标可视化使用TensorBoard记录训练过程from torch.utils.tensorboard import SummaryWriter writer SummaryWriter() for epoch in range(epochs): for step, batch in enumerate(dataloader): # 训练步骤... writer.add_scalar(Loss/train, loss.item(), global_step) writer.add_scalar(LR, optimizer.param_groups[0][lr], global_step)6.2 验证集评估定期在验证集上评估模型性能def evaluate(model, dataloader, device): model.eval() total_mlm_acc 0 total_nsp_acc 0 total_samples 0 with torch.no_grad(): for batch in dataloader: # 准备数据... # 前向传播 outputs model(input_ids, token_type_ids, attention_mask) # MLM准确率 mlm_logits mlm_head(outputs, masked_positions) mlm_preds torch.argmax(mlm_logits, dim-1) mlm_acc (mlm_preds mlm_labels[masked_positions]).float().mean() # NSP准确率 nsp_logits nsp_head(outputs[:, 0, :]) nsp_preds torch.argmax(nsp_logits, dim-1) nsp_acc (nsp_preds nsp_labels).float().mean() total_mlm_acc mlm_acc.item() * len(batch) total_nsp_acc nsp_acc.item() * len(batch) total_samples len(batch) return { mlm_accuracy: total_mlm_acc / total_samples, nsp_accuracy: total_nsp_acc / total_samples }7. 模型保存与应用7.1 保存检查点定期保存模型检查点def save_checkpoint(model, optimizer, scheduler, epoch, path): torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict(), loss: loss, }, path)7.2 下游任务微调将预训练模型应用于下游任务如文本分类class BERTForClassification(nn.Module): def __init__(self, bert_model, num_classes): super().__init__() self.bert bert_model self.classifier nn.Linear(bert_model.config.hidden_size, num_classes) def forward(self, input_ids, attention_maskNone): outputs self.bert(input_ids, attention_maskattention_mask) pooled_output outputs[1] # [CLS] token logits self.classifier(pooled_output) return logits通过以上步骤我们完整实现了BERT的预训练过程包括MLM和NSP两个核心任务。在实际训练中通过合理调整学习率、批量大小和训练步数我们成功将Loss降至0.15左右表明模型已经学习到了有效的语言表示。这种预训练模型可以进一步微调用于各种NLP任务如文本分类、命名实体识别和问答系统等。