双向Transformer:BERT(Bidirectional Encoder Representations from Transformers)

发布于:2025-05-14 ⋅ 阅读:(11) ⋅ 点赞:(0)

基于Transformer架构,通过双向上下文建模训练,提高完成任务的性能。

一 BERT的核心理念

1.1双向上下文建模依赖

之前讲的双向递归是用两个RNN进行,而BERT是通过Transformer的自注意力机制同时捕捉上下文信息。

1.1.1掩码语言模型(MLM)

在预训练时,随机遮盖(mask) 输入句子中15%的Token,这样模型就需要预测被遮盖的Token,同时利用被遮盖位置左右两侧的上下文信息。

(Token:文本处理的基本单元,表示经过分词或子词划分后的最小语义单位,它是模型输入的最小组成部分。)

1.1.2Transformer编码器的双向支持:

由于是基于Transformer编码器,之前发过Transformer是基于自注意力机制,允许每个元素与其他所有元素都有交互,所以可以直接进行上下文的感知。

并行计算:不同于RNN/LSTM的逐词处理,Transformer可一次性处理全部输入序列。

全局上下文感知:每个Token的表示由其自身和所有其他Token的加权组合决定,天然支持双向信息融合。

二 BERT架构

BERT基于 Transformer Encoder 堆叠而成,核心组件如下:

2.1输入表示(Input Embeddings)

Token 作用 示例场景
[CLS] 分类任务的聚合表示(Classification) 句子分类、情感分析
[SEP] 分隔句子对(Separator) 问答、文本对任务(如句子A和B)
[PAD] 填充至统一序列长度(Padding) 批量训练时对齐输入长度
[MASK] 掩码标记(预训练任务专用) MLM任务中遮盖待预测的词
[UNK] 未登录词(Unknown) 处理词表中未包含的Token

Token Embeddings:将单词映射为向量。

Segment Embeddings:区分句子A和句子B(用于句间任务如问答)。

Position Embeddings:编码词的位置信息(取代RNN的时序处理)。

eg:

原句:[CLS] I love deep learning [SEP]

分词后:["[CLS]", "I", "love", "deep", "learning", "[SEP]"]         

序列长度:6(需填充至模型最大长度,如512,但此处简化示例)。

为什么要填充到最大长度,包括下面的列子也进行了填充,原因如下:

(1)批量计算的统一性

并行计算需求:GPU/TPU等硬件通过批量(Batch)处理数据加速训练,但同一批次内的样本必须具有相同的维度。若序列长度不一,无法直接堆叠成矩阵。

填充实现方法:将短序列末尾添加[PAD]标记,使同一批次内所有样本长度一致。

# 原始序列(长度不一)
["[CLS] I love NLP [SEP]", "[CLS] Hello [SEP]"]

# 填充后(统一长度为6)
[
  ["[CLS]", "I", "love", "NLP", "[SEP]", "[PAD]"],  
  ["[CLS]", "Hello", "[SEP]", "[PAD]", "[PAD]", "[PAD]"]
]

(2)模型架构的固定输入维度

(3)内存与计算资源优化

填充的副作用与解决方案

问题 解决方案
信息损失 优先截断尾部(因头部通常更重要),或分块处理保留全文。
计算浪费 使用注意力掩码(Mask)跳过填充部分,避免无效计算。
模型偏差 预训练时随机遮盖[PAD]附近的Token,防止模型依赖填充位置(如RoBERTa取消NSP任务)。

Token Embeddings:

Token 词表索引(假设值) Token Embedding(768维向量示例)
[CLS] 101 E_cls = [0.1, 0.3, ..., -0.2]
I 1045 E_I = [0.4, -0.5, ..., 0.7]
love 2293 E_love = [-0.2, 0.6, ..., 0.1]
deep 2772 E_deep = [0.5, 0.0, ..., -0.3]
learning 4879 E_learning = [0.3, 0.2, ..., 0.5]
[SEP] 102 E_sep = [0.0, -0.1, ..., 0.4]

Segment Embeddings:

Token Segment ID Segment Embedding(768维向量示例)
[CLS] 0 S_0 = [0.2, 0.1, ..., -0.3]
I 0 S_0 = [0.2, 0.1, ..., -0.3]
love 0 S_0 = [0.2, 0.1, ..., -0.3]
deep 0 S_0 = [0.2, 0.1, ..., -0.3]
learning 0 S_0 = [0.2, 0.1, ..., -0.3]
[SEP] 0 S_0 = [0.2, 0.1, ..., -0.3]

同属于一个句子的Segment ID相同,比如:

输入句子:[CLS] How old are you? [SEP] I am 20. [SEP]

Segment IDs:[0, 0, 0, 0, 0, 0, 1, 1, 1, 1]

Position Embeddings:

Token 位置编号 Position Embedding(768维向量示例)
[CLS] 0 P_0 = [0.0, 0.5, ..., -0.1]
I 1 P_1 = [0.3, -0.2, ..., 0.4]
love 2 P_2 = [-0.1, 0.7, ..., 0.2]
deep 3 P_3 = [0.4, 0.1, ..., -0.5]
learning 4 P_4 = [0.2, -0.3, ..., 0.6]
[SEP] 5 P_5 = [0.1, 0.0, ..., 0.3]

最终输入表示: 

每个 Token 的最终输入向量是三者之和:

Input_{i} = E_{token_{i}} + S_{segment_{i}} + P_{position_{i}}

Token 最终输入向量(简化示例)
[CLS] E_cls + S_0 + P_0 = [0.1+0.2+0.0, ..., -0.2-0.3-0.1] → [0.3, ..., -0.6]
I E_I + S_0 + P_1 = [0.4+0.2+0.3, ..., 0.7-0.3+0.4] → [0.9, ..., 0.8]
love E_love + S_0 + P_2 = [-0.2+0.2-0.1, ..., 0.1-0.3+0.2] → [-0.1, ..., 0.0]
deep E_deep + S_0 + P_3 = [0.5+0.2+0.4, ..., -0.3-0.3-0.5] → [1.1, ..., -1.1]
learning E_learning + S_0 + P_4 = [0.3+0.2+0.2, ..., 0.5-0.3+0.6] → [0.7, ..., 0.8]
[SEP] E_sep + S_0 + P_5 = [0.0+0.2+0.1, ..., 0.4-0.3+0.3] → [0.3, ..., 0.4]

2.2Multi-Head Self-Attention(多头自注意力)

之前Transformer文章讲过

2.3前馈神经网络(FFN)

之前Transformer文章讲过

三 预训练任务

BERT通过两个无监督任务预训练模型:

3.1Masked Language Model(MLM)

操作:随机遮盖输入中15%的词(如将“机器学习”变为“机器[MASK]”),模型预测被遮盖的词。

改进:部分遮盖词替换为随机词,防止模型过度依赖局部信息。

3.2Next Sentence Prediction(NSP)

目标:判断两个句子是否为上下句关系。

输入格式:50%正样本(连续句子),50%负样本(随机采样)。

四 BERT vs 传统模型

特性 BERT LSTM/RNN
上下文建模 双向全上下文 单向或有限窗口双向
长距离依赖 自注意力直接建模全局关系 依赖循环结构,长距易衰减
训练效率 预训练计算成本高,微调快 从零训练,任务专用
可解释性 注意力权重可解释(可视化聚焦区域) 隐藏状态难以直接解释

五 BERT的应用场景

(1)文本分类:直接取 [CLS] 向量输入分类器。

(2)命名实体识别(NER):对每个词的输出向量分类。

(3)问答系统:输入问题与文本,输出答案位置(如SQuAD数据集)。

(4)语义相似度:两文本拼接后通过 [CLS] 判断相似度。

六  BERT的限制

计算资源需求大:预训练需数千GPU小时。

文本生成长度受限:最大输入长度(通常512词)限制长文本处理。

实时性不足:推理速度较慢,需针对性优化。

# -*- coding: utf-8 -*-
# @FileName: bert_text_classification.py
import torch
from torch.utils.data import DataLoader
from transformers import BertTokenizer, BertForSequenceClassification, AdamW
from datasets import load_dataset
from tqdm import tqdm

# 参数设置
MODEL_NAME = "bert-base-uncased"
NUM_LABELS = 2
MAX_LENGTH = 256
BATCH_SIZE = 16
EPOCHS = 3
LEARNING_RATE = 2e-5

# 1. 加载模型和分词器
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)
model = BertForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=NUM_LABELS)

# 2. 加载并预处理数据集
def preprocess_data():
    dataset = load_dataset('imdb')
    
    # 编码函数
    def tokenize_func(batch):
        return tokenizer(
            batch["text"], 
            padding="max_length", 
            truncation=True, 
            max_length=MAX_LENGTH,
            return_tensors="pt"
        )

    # 应用分词
    dataset = dataset.map(tokenize_func, batched=True)
    dataset = dataset.rename_column("label", "labels")
    
    # 设置Tensor格式
    for split in ['train', 'test']:
        dataset[split].set_format(
            type='torch', 
            columns=['input_ids', 'attention_mask', 'labels']
        )
    
    return dataset

dataset = preprocess_data()
train_loader = DataLoader(dataset['train'], batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(dataset['test'], batch_size=BATCH_SIZE)

# 3. 训练配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
optimizer = AdamW(model.parameters(), lr=LEARNING_RATE)

# 4. 训练循环
def train_model():
    model.train()
    for epoch in range(EPOCHS):
        total_loss = 0
        progress_bar = tqdm(train_loader, desc=f'Epoch {epoch+1}/{EPOCHS}')
        
        for batch in progress_bar:
            optimizer.zero_grad()
            
            inputs = {
                "input_ids": batch["input_ids"].to(device),
                "attention_mask": batch["attention_mask"].to(device),
                "labels": batch["labels"].to(device)
            }
            
            outputs = model(**inputs)
            loss = outputs.loss
            loss.backward()
            optimizer.step()
            
            total_loss += loss.item()
            progress_bar.set_postfix({'loss': loss.item()})
        
        print(f"Epoch {epoch+1} Average Loss: {total_loss/len(train_loader):.4f}")

# 运行训练
train_model()

# 5. 保存模型
model.save_pretrained("./bert_imdb_sentiment")

# 6. 评估函数
def evaluate_model():
    model.eval()
    total_correct = 0
    
    with torch.no_grad():
        for batch in tqdm(test_loader, desc="Evaluating"):
            inputs = {
                "input_ids": batch["input_ids"].to(device),
                "attention_mask": batch["attention_mask"].to(device)
            }
            labels = batch["labels"].to(device)
            
            outputs = model(**inputs)
            predictions = torch.argmax(outputs.logits, dim=1)
            total_correct += (predictions == labels).sum().item()
    
    accuracy = total_correct / len(dataset['test'])
    print(f"\nTest Accuracy: {accuracy*100:.2f}%\n")

evaluate_model()

# 7. 预测函数
def predict_sentiment(text):
    model.eval()
    encoding = tokenizer(
        text, 
        max_length=MAX_LENGTH, 
        padding='max_length', 
        truncation=True, 
        return_tensors='pt'
    ).to(device)
    
    with torch.no_grad():
        outputs = model(**encoding)
        prob = torch.nn.functional.softmax(outputs.logits, dim=1)
    
    label = torch.argmax(prob).item()
    return "Positive" if label == 1 else "Negative", prob[0][label].item()

# 测试样例
sample_texts = [
    "This movie is fantastic! The acting is brilliant.",
    "A terrible waste of time. The plot makes no sense."
]

for text in sample_texts:
    label, confidence = predict_sentiment(text)
    print(f"Text: {text[:60]}...")
    print(f"=> Predicted: {label} (Confidence: {confidence:.4f})\n")