搜 索

GPT vs BERT:两条路的故事

  • 3阅读
  • 2025年02月22日
  • 0评论
首页 / AI/大数据 / 正文

前言:同一个 Transformer,两种命运

2017 年,Google 发布了 Transformer。

2018 年,基于 Transformer,两个重量级模型几乎同时诞生:

  • 6 月:OpenAI 发布 GPT-1
  • 10 月:Google 发布 BERT

它们都用 Transformer,但走了完全不同的路:

  • GPT 选择了 Decoder-Only,从左到右生成
  • BERT 选择了 Encoder-Only,双向理解
timeline title 两条路线的演进 2018 : GPT-1 (OpenAI) : BERT (Google) 2019 : GPT-2 "太危险不敢开源" : RoBERTa, ALBERT, DistilBERT 2020 : GPT-3 涌现能力 : BERT 继续刷榜 NLU 2022 : ChatGPT 出圈 : BERT... 还在做分类 2023-2025 : GPT-4, Claude, LLaMA : BERT 退居幕后

这篇文章,我们来搞懂这两条路的本质区别,以及为什么 GPT 路线最终"赢了"。


一、在开始之前:预训练范式的革命

1.1 传统 NLP 的痛苦

在 BERT 和 GPT 之前,NLP 是这样做的:

graph LR subgraph 传统方式 A[任务1: 情感分类] --> M1[模型1] B[任务2: 命名实体] --> M2[模型2] C[任务3: 问答] --> M3[模型3] D[任务4: 翻译] --> M4[模型4] end style M1 fill:#ff6b6b style M2 fill:#ff6b6b style M3 fill:#ff6b6b style M4 fill:#ff6b6b

每个任务都要从头训练一个模型。数据标注贵,训练成本高,而且知识无法迁移。

1.2 预训练 + 微调范式

GPT 和 BERT 带来的革命:

graph TB subgraph 预训练+微调 PT[大规模无监督预训练] --> Base[预训练模型] Base --> FT1[微调: 情感分类] Base --> FT2[微调: 命名实体] Base --> FT3[微调: 问答] Base --> FT4[微调: 翻译] end style PT fill:#4ecdc4 style Base fill:#4ecdc4

核心思想:

  1. 预训练:在海量无标注文本上学习语言的"通用知识"
  2. 微调:用少量标注数据适配具体任务

这就像:先上大学学通识教育,再根据工作需要专项培训。比每份工作都从小学开始学,效率高太多了。

1.3 关键问题:预训练学什么?

预训练需要一个"任务"来驱动学习,但我们没有标注数据。

解决方案:自监督学习——让模型从文本本身构造监督信号。

GPT 和 BERT 的分歧就在这里:它们选择了不同的自监督任务


二、GPT:从左到右的预言家

2.1 核心思想:预测下一个词

GPT 的预训练任务非常简单:给定前文,预测下一个词

这叫做 Causal Language Model (CLM)自回归语言模型

输入: "今天天气真"
目标: "好"

输入: "今天天气真好"
目标: ","

输入: "今天天气真好,"
目标: "适合"

数学形式:

$$ P(x_1, x_2, ..., x_n) = \prod_{i=1}^{n} P(x_i | x_1, x_2, ..., x_{i-1}) $$

用人话说:文本的概率 = 每个词在给定前文条件下的概率的乘积。

2.2 因果掩码:只能看前面

为了实现"预测下一个词",GPT 使用 因果掩码(Causal Mask):每个位置只能看到它之前的位置。

标准 Attention(可以看所有位置):
位置 0: [1, 1, 1, 1]
位置 1: [1, 1, 1, 1]
位置 2: [1, 1, 1, 1]
位置 3: [1, 1, 1, 1]

因果 Attention(只能看前面):
位置 0: [1, 0, 0, 0]  ← 只能看自己
位置 1: [1, 1, 0, 0]  ← 能看 0, 1
位置 2: [1, 1, 1, 0]  ← 能看 0, 1, 2
位置 3: [1, 1, 1, 1]  ← 能看所有
graph LR subgraph 因果注意力 T1[今天] T2[天气] T3[真] T4[好] T2 --> T1 T3 --> T1 T3 --> T2 T4 --> T1 T4 --> T2 T4 --> T3 end

2.3 GPT 的架构

GPT 使用 Decoder-Only 架构(实际上是 Transformer Decoder 去掉 Cross-Attention):

graph TB subgraph GPT架构 Input[输入 Token] --> Embed[Token Embedding + Position Embedding] Embed --> Block1[Transformer Block 1] Block1 --> Block2[Transformer Block 2] Block2 --> BlockN[... Block N] BlockN --> LN[Layer Norm] LN --> Head[Language Model Head] Head --> Output[预测下一个 Token 的概率分布] end subgraph TransformerBlock[Transformer Block] TB_In[输入] --> Attn[Masked Self-Attention] Attn --> Add1[Add & Norm] TB_In -.-> Add1 Add1 --> FFN[Feed Forward] FFN --> Add2[Add & Norm] Add1 -.-> Add2 Add2 --> TB_Out[输出] end

2.4 GPT 的代码实现

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

class GPTBlock(nn.Module):
    """GPT 的一个 Transformer Block"""
    
    def __init__(self, embed_dim, num_heads, ff_dim, dropout=0.1):
        super().__init__()
        self.ln1 = nn.LayerNorm(embed_dim)
        self.attn = nn.MultiheadAttention(embed_dim, num_heads, dropout=dropout, batch_first=True)
        self.ln2 = nn.LayerNorm(embed_dim)
        self.ffn = nn.Sequential(
            nn.Linear(embed_dim, ff_dim),
            nn.GELU(),
            nn.Linear(ff_dim, embed_dim),
            nn.Dropout(dropout)
        )
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x, attn_mask=None):
        # Pre-Norm 架构(GPT-2 开始使用)
        # Self-Attention with Causal Mask
        attn_out, _ = self.attn(
            self.ln1(x), self.ln1(x), self.ln1(x),
            attn_mask=attn_mask,
            is_causal=True  # PyTorch 2.0+ 支持
        )
        x = x + self.dropout(attn_out)
        
        # Feed Forward
        x = x + self.ffn(self.ln2(x))
        
        return x


class GPT(nn.Module):
    """简化版 GPT 模型"""
    
    def __init__(self, vocab_size, embed_dim, num_heads, num_layers, max_len, ff_dim=None):
        super().__init__()
        ff_dim = ff_dim or embed_dim * 4
        
        self.token_embed = nn.Embedding(vocab_size, embed_dim)
        self.pos_embed = nn.Embedding(max_len, embed_dim)
        
        self.blocks = nn.ModuleList([
            GPTBlock(embed_dim, num_heads, ff_dim)
            for _ in range(num_layers)
        ])
        
        self.ln_f = nn.LayerNorm(embed_dim)
        self.head = nn.Linear(embed_dim, vocab_size, bias=False)
        
        # 权重共享:embedding 和 output head
        self.head.weight = self.token_embed.weight
    
    def forward(self, input_ids):
        batch, seq_len = input_ids.shape
        
        # Embedding
        positions = torch.arange(seq_len, device=input_ids.device)
        x = self.token_embed(input_ids) + self.pos_embed(positions)
        
        # Transformer Blocks
        for block in self.blocks:
            x = block(x)
        
        # Output
        x = self.ln_f(x)
        logits = self.head(x)
        
        return logits
    
    def generate(self, input_ids, max_new_tokens, temperature=1.0):
        """自回归生成"""
        for _ in range(max_new_tokens):
            # 只取最后 max_len 个 token(处理超长序列)
            logits = self(input_ids[:, -self.pos_embed.num_embeddings:])
            
            # 取最后一个位置的 logits
            logits = logits[:, -1, :] / temperature
            probs = F.softmax(logits, dim=-1)
            
            # 采样
            next_token = torch.multinomial(probs, num_samples=1)
            input_ids = torch.cat([input_ids, next_token], dim=-1)
        
        return input_ids


# 训练 GPT
def train_gpt_step(model, input_ids, optimizer):
    """GPT 的训练步骤"""
    # input_ids: (batch, seq_len)
    
    # 输入是 [:-1],目标是 [1:]
    inputs = input_ids[:, :-1]
    targets = input_ids[:, 1:]
    
    # 前向传播
    logits = model(inputs)
    
    # 计算损失(交叉熵)
    loss = F.cross_entropy(
        logits.view(-1, logits.size(-1)),
        targets.view(-1)
    )
    
    # 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    return loss.item()


# 使用示例
if __name__ == "__main__":
    # 模型配置
    model = GPT(
        vocab_size=50257,   # GPT-2 的词表大小
        embed_dim=768,
        num_heads=12,
        num_layers=12,
        max_len=1024
    )
    
    print(f"参数量: {sum(p.numel() for p in model.parameters()):,}")
    # 约 124M,和 GPT-2 small 一样

2.5 GPT 的演进

模型参数量特点
GPT-1 (2018)117M首次证明预训练+微调有效
GPT-2 (2019)1.5BZero-shot 能力,"太危险不敢开源"
GPT-3 (2020)175BIn-Context Learning,涌现能力
GPT-4 (2023)~1.8T?多模态,推理能力大幅提升

三、BERT:双向理解的王者

3.1 核心思想:完形填空

BERT 的预训练任务是 Masked Language Model (MLM):随机遮住一些词,让模型预测被遮住的词。

原始文本: "今天天气真好"
输入:     "今天 [MASK] 真好"
目标:     预测 [MASK] = "天气"

关键区别:BERT 可以同时看到左边和右边的上下文

$$ P(x_{\text{masked}} | x_{\text{context}}) $$

这就像做完形填空:你可以看整个句子,然后填空。

3.2 双向注意力:全都能看

BERT 使用 Encoder-Only 架构,没有因果掩码:

BERT 的 Attention(所有位置都能看):
位置 0: [1, 1, 1, 1]  ← 能看所有
位置 1: [1, 1, 1, 1]  ← 能看所有
位置 2: [1, 1, 1, 1]  ← 能看所有
位置 3: [1, 1, 1, 1]  ← 能看所有
graph LR subgraph BERT双向注意力 T1[今天] <--> T2[天气] T2 <--> T3[真] T3 <--> T4[好] T1 <--> T3 T1 <--> T4 T2 <--> T4 end

3.3 BERT 的预训练任务

BERT 实际上有两个预训练任务:

任务 1:Masked Language Model (MLM)

  • 随机选择 15% 的 token
  • 其中 80% 替换为 [MASK]
  • 10% 替换为随机 token
  • 10% 保持不变
  • 模型预测原始 token

为什么不全部用 [MASK]?因为微调时没有 [MASK] token,需要让模型学会处理正常文本。

任务 2:Next Sentence Prediction (NSP)

  • 输入两个句子 A 和 B
  • 50% 的情况 B 是 A 的真实下一句
  • 50% 的情况 B 是随机句子
  • 模型预测 B 是否是 A 的下一句
输入: [CLS] 今天天气真好 [SEP] 我们去公园吧 [SEP]
标签: IsNext(是下一句)

输入: [CLS] 今天天气真好 [SEP] 股票大涨了 [SEP]
标签: NotNext(不是下一句)

后来发现 NSP 任务没什么用,RoBERTa 去掉了它,效果反而更好。

3.4 BERT 的架构

graph TB subgraph BERT架构 Input["[CLS] Token1 Token2 ... [SEP]"] Input --> Embed["Token Emb + Position Emb + Segment Emb"] Embed --> Block1[Transformer Encoder Block 1] Block1 --> Block2[Transformer Encoder Block 2] Block2 --> BlockN[... Block N] BlockN --> Output[输出表示] Output --> CLS["[CLS] 用于分类"] Output --> MLM["其他位置用于 MLM"] end

3.5 BERT 的代码实现

class BERTEmbedding(nn.Module):
    """BERT 的 Embedding 层"""
    
    def __init__(self, vocab_size, embed_dim, max_len, num_segments=2):
        super().__init__()
        self.token_embed = nn.Embedding(vocab_size, embed_dim)
        self.pos_embed = nn.Embedding(max_len, embed_dim)
        self.segment_embed = nn.Embedding(num_segments, embed_dim)
        self.ln = nn.LayerNorm(embed_dim)
        self.dropout = nn.Dropout(0.1)
    
    def forward(self, input_ids, segment_ids=None):
        seq_len = input_ids.size(1)
        positions = torch.arange(seq_len, device=input_ids.device)
        
        x = self.token_embed(input_ids) + self.pos_embed(positions)
        
        if segment_ids is not None:
            x = x + self.segment_embed(segment_ids)
        
        return self.dropout(self.ln(x))


class BERTBlock(nn.Module):
    """BERT 的 Transformer Block"""
    
    def __init__(self, embed_dim, num_heads, ff_dim, dropout=0.1):
        super().__init__()
        self.attn = nn.MultiheadAttention(embed_dim, num_heads, dropout=dropout, batch_first=True)
        self.ln1 = nn.LayerNorm(embed_dim)
        self.ffn = nn.Sequential(
            nn.Linear(embed_dim, ff_dim),
            nn.GELU(),
            nn.Linear(ff_dim, embed_dim),
            nn.Dropout(dropout)
        )
        self.ln2 = nn.LayerNorm(embed_dim)
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x, attention_mask=None):
        # Post-Norm 架构(BERT 原版)
        # Self-Attention(双向,无因果掩码)
        attn_out, _ = self.attn(x, x, x, key_padding_mask=attention_mask)
        x = self.ln1(x + self.dropout(attn_out))
        
        # Feed Forward
        x = self.ln2(x + self.ffn(x))
        
        return x


class BERT(nn.Module):
    """简化版 BERT 模型"""
    
    def __init__(self, vocab_size, embed_dim, num_heads, num_layers, max_len, ff_dim=None):
        super().__init__()
        ff_dim = ff_dim or embed_dim * 4
        
        self.embedding = BERTEmbedding(vocab_size, embed_dim, max_len)
        
        self.blocks = nn.ModuleList([
            BERTBlock(embed_dim, num_heads, ff_dim)
            for _ in range(num_layers)
        ])
        
        # MLM Head
        self.mlm_head = nn.Sequential(
            nn.Linear(embed_dim, embed_dim),
            nn.GELU(),
            nn.LayerNorm(embed_dim),
            nn.Linear(embed_dim, vocab_size)
        )
        
        # NSP Head
        self.nsp_head = nn.Linear(embed_dim, 2)
    
    def forward(self, input_ids, segment_ids=None, attention_mask=None):
        x = self.embedding(input_ids, segment_ids)
        
        for block in self.blocks:
            x = block(x, attention_mask)
        
        return x
    
    def get_mlm_logits(self, hidden_states):
        """获取 MLM 预测的 logits"""
        return self.mlm_head(hidden_states)
    
    def get_nsp_logits(self, hidden_states):
        """获取 NSP 预测的 logits(使用 [CLS] token)"""
        cls_hidden = hidden_states[:, 0, :]
        return self.nsp_head(cls_hidden)


# 训练 BERT
def train_bert_step(model, input_ids, segment_ids, mlm_labels, nsp_labels, optimizer):
    """BERT 的训练步骤"""
    # input_ids: (batch, seq_len),包含 [MASK]
    # mlm_labels: (batch, seq_len),-100 表示不计算损失
    # nsp_labels: (batch,),0 或 1
    
    # 前向传播
    hidden = model(input_ids, segment_ids)
    
    # MLM 损失
    mlm_logits = model.get_mlm_logits(hidden)
    mlm_loss = F.cross_entropy(
        mlm_logits.view(-1, mlm_logits.size(-1)),
        mlm_labels.view(-1),
        ignore_index=-100  # 忽略非 mask 位置
    )
    
    # NSP 损失
    nsp_logits = model.get_nsp_logits(hidden)
    nsp_loss = F.cross_entropy(nsp_logits, nsp_labels)
    
    # 总损失
    loss = mlm_loss + nsp_loss
    
    # 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    return loss.item()


# BERT 微调示例:文本分类
class BERTForClassification(nn.Module):
    """BERT 用于文本分类"""
    
    def __init__(self, bert_model, num_classes):
        super().__init__()
        self.bert = bert_model
        self.classifier = nn.Linear(bert_model.embedding.token_embed.embedding_dim, num_classes)
    
    def forward(self, input_ids, segment_ids=None, attention_mask=None):
        hidden = self.bert(input_ids, segment_ids, attention_mask)
        
        # 使用 [CLS] token 的表示进行分类
        cls_hidden = hidden[:, 0, :]
        logits = self.classifier(cls_hidden)
        
        return logits

3.6 BERT 的变体

模型改进点
RoBERTa去掉 NSP,更多数据,动态 mask
ALBERT参数共享,因式分解 embedding
DistilBERT知识蒸馏,模型压缩
ELECTRA替换检测任务,更高效
DeBERTa解耦注意力,相对位置编码

四、GPT vs BERT:本质区别

4.1 注意力模式对比

graph TB subgraph GPT因果注意力 G1[Token 1] --> G2[Token 2] G1 --> G3[Token 3] G1 --> G4[Token 4] G2 --> G3 G2 --> G4 G3 --> G4 end subgraph BERT双向注意力 B1[Token 1] <--> B2[Token 2] B1 <--> B3[Token 3] B1 <--> B4[Token 4] B2 <--> B3 B2 <--> B4 B3 <--> B4 end

4.2 训练目标对比

维度GPT (CLM)BERT (MLM)
预测内容下一个词被遮住的词
上下文只有左边左边 + 右边
每个 token 的利用每个 token 都预测下一个只有 15% 被 mask 的 token
训练效率相对低

4.3 能力对比

graph TB subgraph 任务类型 NLU[自然语言理解 NLU] NLG[自然语言生成 NLG] end subgraph BERT擅长 NLU --> B_Task1[文本分类] NLU --> B_Task2[命名实体识别] NLU --> B_Task3[问答抽取] NLU --> B_Task4[句子相似度] end subgraph GPT擅长 NLG --> G_Task1[文本生成] NLG --> G_Task2[对话] NLG --> G_Task3[代码生成] NLG --> G_Task4[翻译] NLU -.->|通过生成方式| G_Task5[分类/问答] end style BERT擅长 fill:#ff6b6b style GPT擅长 fill:#4ecdc4

BERT 的优势

  • 双向上下文,理解更准确
  • 在 NLU 任务上效果好
  • 提取的表示质量高(用于下游任务)

GPT 的优势

  • 天然支持生成
  • 可以用同一个模型处理几乎所有任务
  • Scaling 效果更好

4.4 关键差异代码演示

def compare_attention_patterns():
    """可视化 GPT 和 BERT 的注意力模式"""
    import matplotlib.pyplot as plt
    import numpy as np
    
    seq_len = 5
    
    # GPT: 因果注意力(下三角)
    gpt_mask = np.tril(np.ones((seq_len, seq_len)))
    
    # BERT: 全注意力
    bert_mask = np.ones((seq_len, seq_len))
    
    fig, axes = plt.subplots(1, 2, figsize=(10, 4))
    
    axes[0].imshow(gpt_mask, cmap='Blues')
    axes[0].set_title('GPT: Causal Attention')
    axes[0].set_xlabel('Key Position')
    axes[0].set_ylabel('Query Position')
    
    axes[1].imshow(bert_mask, cmap='Blues')
    axes[1].set_title('BERT: Bidirectional Attention')
    axes[1].set_xlabel('Key Position')
    axes[1].set_ylabel('Query Position')
    
    plt.tight_layout()
    plt.savefig('attention_patterns.png')
    print("GPT 只能看到当前位置及之前的 token")
    print("BERT 可以看到所有位置的 token")


def compare_training_signals():
    """对比训练信号的利用效率"""
    
    sentence = "今天天气真的很好"
    tokens = list(sentence)
    
    print("=" * 50)
    print("GPT 的训练信号(每个位置都有监督):")
    print("=" * 50)
    for i in range(len(tokens)):
        context = "".join(tokens[:i]) if i > 0 else "[START]"
        target = tokens[i]
        print(f"输入: {context:20s} → 预测: {target}")
    
    print("\n" + "=" * 50)
    print("BERT 的训练信号(只有 MASK 位置有监督):")
    print("=" * 50)
    # 假设 mask 15% ≈ 1-2 个 token
    masked_positions = [2, 5]  # "天" 和 "的"
    masked_sentence = tokens.copy()
    for pos in masked_positions:
        masked_sentence[pos] = "[MASK]"
    
    print(f"输入: {''.join(masked_sentence)}")
    for pos in masked_positions:
        print(f"预测位置 {pos}: [MASK] → {tokens[pos]}")
    
    print(f"\nGPT 利用率: {len(tokens)}/{len(tokens)} = 100%")
    print(f"BERT 利用率: {len(masked_positions)}/{len(tokens)} = {len(masked_positions)/len(tokens)*100:.1f}%")


compare_training_signals()

输出:

==================================================
GPT 的训练信号(每个位置都有监督):
==================================================
输入: [START]              → 预测: 今
输入: 今                   → 预测: 天
输入: 今天                 → 预测: 天
输入: 今天天               → 预测: 气
输入: 今天天气             → 预测: 真
输入: 今天天气真           → 预测: 的
输入: 今天天气真的         → 预测: 很
输入: 今天天气真的很       → 预测: 好

==================================================
BERT 的训练信号(只有 MASK 位置有监督):
==================================================
输入: 今天[MASK]气真[MASK]很好
预测位置 2: [MASK] → 天
预测位置 5: [MASK] → 的

GPT 利用率: 8/8 = 100%
BERT 利用率: 2/8 = 25.0%

五、为什么 GPT 路线最终胜出?

5.1 生成能力的碾压

BERT 的致命问题:它不会生成

# BERT: 只能填空
input = "今天天气 [MASK] 好"
output = "今天天气 很 好"  # ✓ 可以

# BERT: 不能续写
input = "今天天气"
output = ???  # ✗ 做不到!
# GPT: 天然支持生成
input = "今天天气"
output = "今天天气真好,我们去公园玩吧!"  # ✓ 轻松

在 ChatGPT 出现之前,BERT 在各种 NLU benchmark 上刷榜。

但用户要的不是 benchmark 分数,是 能对话、能写文章、能写代码 的 AI。

BERT:做不到。
GPT:我可以。

5.2 Scaling Law 的差异

研究发现,Decoder-Only 架构的 scaling 更好:

graph LR subgraph Scaling效果 A[参数量 ↑] --> B{架构} B -->|Decoder-Only| C[效果平滑提升] B -->|Encoder-Only| D[效果提升有限] B -->|Encoder-Decoder| E[介于两者之间] end

为什么?可能的原因:

  1. 训练效率:GPT 每个 token 都有监督信号
  2. 统一性:所有任务都可以转化为生成
  3. 参数利用:没有分成 encoder/decoder 两部分

5.3 任务统一:Prompt 的魔力

GPT 的杀手锏:用 prompt 把所有任务都转成生成任务

# 情感分类
输入: "这部电影太棒了!\n情感: "
输出: "正面"

# 命名实体识别
输入: "马云创立了阿里巴巴\n实体: "
输出: "马云(人名), 阿里巴巴(公司)"

# 翻译
输入: "Translate to Chinese: Hello world\n"
输出: "你好世界"

# 问答
输入: "问题: 地球到月球的距离是多少?\n答案: "
输出: "约38万公里"

一个模型,处理所有任务。不需要为每个任务微调一个 BERT。

5.4 涌现能力

当 GPT 足够大时,出现了一些神奇的能力:

graph TB subgraph 涌现能力 ICL[In-Context Learning
给几个例子就能学会] CoT[Chain-of-Thought
一步步推理] IF[Instruction Following
理解并遵循指令] end Small[小模型] -.->|完全不会| ICL Large[大模型] -->|突然会了| ICL style Large fill:#4ecdc4

这些能力在 BERT 上几乎看不到,即使把 BERT 做得很大。

5.5 现状:GPT 路线的全面胜利

pie showData title 2024年主流大模型架构分布 "Decoder-Only (GPT类)" : 90 "Encoder-Decoder (T5类)" : 8 "Encoder-Only (BERT类)" : 2

几乎所有你听过的大模型都是 Decoder-Only:

  • GPT-4, GPT-4o (OpenAI)
  • Claude 3.5 (Anthropic)
  • LLaMA 3 (Meta)
  • Gemini (Google)
  • Qwen 2.5 (阿里)
  • DeepSeek (深度求索)

5.6 BERT 还有用吗?

有!但角色变了。

BERT 现在主要用于:

  1. 文本 Embedding

    # 用 BERT 提取句子表示,做相似度搜索
    from sentence_transformers import SentenceTransformer
    model = SentenceTransformer('all-MiniLM-L6-v2')  # 基于 BERT
    embeddings = model.encode(["今天天气真好", "天气不错"])
  2. 分类任务(当你只需要分类,不需要生成时)

    # 情感分类、垃圾邮件检测等
    from transformers import pipeline
    classifier = pipeline("sentiment-analysis")
    result = classifier("I love this product!")
  3. 信息抽取

    # NER、关系抽取
    from transformers import pipeline
    ner = pipeline("ner")
    result = ner("马云创立了阿里巴巴")
  4. Reranker(RAG 中的重排序)

    # 用 BERT 判断 query 和 document 的相关性
    from sentence_transformers import CrossEncoder
    model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
    scores = model.predict([("query", "doc1"), ("query", "doc2")])

六、第三条路:Encoder-Decoder

6.1 T5:Text-to-Text 框架

Google 的 T5 走了第三条路:Encoder-Decoder,但把所有任务都转成文本到文本。

# 翻译
输入: "translate English to German: That is good."
输出: "Das ist gut."

# 分类
输入: "sst2 sentence: This movie is great!"
输出: "positive"

# 问答
输入: "question: What is the capital of France? context: France is a country in Europe. Its capital is Paris."
输出: "Paris"

6.2 为什么没成为主流?

  1. 架构复杂:需要 Encoder 和 Decoder 两部分
  2. Scaling 效率:同等参数量,效果不如 Decoder-Only
  3. 推理慢:需要先跑完 Encoder,再自回归生成

不过 T5 在一些场景仍有应用:

  • 机器翻译
  • 摘要生成
  • FLAN-T5(指令微调版)

七、实战:使用 GPT 和 BERT

7.1 使用 HuggingFace Transformers

from transformers import (
    GPT2LMHeadModel, GPT2Tokenizer,
    BertForMaskedLM, BertTokenizer,
    BertForSequenceClassification
)

# ============ GPT-2 文本生成 ============
def gpt2_generation():
    tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
    model = GPT2LMHeadModel.from_pretrained('gpt2')
    
    prompt = "The future of AI is"
    inputs = tokenizer(prompt, return_tensors='pt')
    
    outputs = model.generate(
        inputs['input_ids'],
        max_length=50,
        num_return_sequences=1,
        temperature=0.7,
        do_sample=True
    )
    
    print("GPT-2 生成:")
    print(tokenizer.decode(outputs[0], skip_special_tokens=True))


# ============ BERT 完形填空 ============
def bert_fill_mask():
    tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
    model = BertForMaskedLM.from_pretrained('bert-base-uncased')
    
    text = "The capital of France is [MASK]."
    inputs = tokenizer(text, return_tensors='pt')
    
    with torch.no_grad():
        outputs = model(**inputs)
    
    mask_idx = (inputs['input_ids'] == tokenizer.mask_token_id).nonzero(as_tuple=True)[1]
    logits = outputs.logits[0, mask_idx, :]
    top_tokens = logits.topk(5).indices[0]
    
    print("BERT 填空预测:")
    for token_id in top_tokens:
        print(f"  {tokenizer.decode([token_id])}")


# ============ BERT 文本分类 ============
def bert_classification():
    from transformers import pipeline
    
    classifier = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
    
    results = classifier([
        "I absolutely loved this movie!",
        "This was a waste of my time.",
        "It was okay, nothing special."
    ])
    
    print("BERT 情感分类:")
    for r in results:
        print(f"  {r['label']}: {r['score']:.3f}")


# ============ BERT Embedding ============
def bert_embedding():
    from sentence_transformers import SentenceTransformer
    from sklearn.metrics.pairwise import cosine_similarity
    
    model = SentenceTransformer('all-MiniLM-L6-v2')
    
    sentences = [
        "今天天气真好",
        "天气不错,阳光明媚",
        "我喜欢吃火锅",
        "今天适合出去玩"
    ]
    
    embeddings = model.encode(sentences)
    
    print("BERT Embedding 相似度:")
    sim_matrix = cosine_similarity(embeddings)
    for i, s1 in enumerate(sentences):
        for j, s2 in enumerate(sentences):
            if i < j:
                print(f"  '{s1}' vs '{s2}': {sim_matrix[i][j]:.3f}")


if __name__ == "__main__":
    print("=" * 50)
    gpt2_generation()
    print("\n" + "=" * 50)
    bert_fill_mask()
    print("\n" + "=" * 50)
    bert_classification()
    print("\n" + "=" * 50)
    bert_embedding()

7.2 使用现代大模型 API

# 现代 GPT 类模型的使用
from openai import OpenAI
from anthropic import Anthropic

# OpenAI GPT-4
def use_gpt4():
    client = OpenAI()
    
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "Explain the difference between GPT and BERT."}
        ]
    )
    
    print(response.choices[0].message.content)


# Anthropic Claude
def use_claude():
    client = Anthropic()
    
    response = client.messages.create(
        model="claude-3-opus-20240229",
        max_tokens=1024,
        messages=[
            {"role": "user", "content": "Explain the difference between GPT and BERT."}
        ]
    )
    
    print(response.content[0].text)

八、总结

核心对比表

维度GPT (Decoder-Only)BERT (Encoder-Only)
注意力因果(只看前面)双向(全部可见)
预训练任务预测下一个词预测被遮住的词
训练效率每个 token 都用只有 15% token
天然能力生成理解
Scaling效果好效果有限
现状主流大模型Embedding、分类

架构选择决策树

flowchart TB Start[选择模型架构] --> Q1{需要生成能力?} Q1 -->|是| Q2{需要理解输入?} Q1 -->|否| Q3{需要高质量表示?} Q2 -->|简单理解即可| A1[Decoder-Only
GPT/LLaMA/Claude] Q2 -->|需要深度理解| A2[Encoder-Decoder
T5/BART] Q3 -->|是| A3[Encoder-Only
BERT/RoBERTa] Q3 -->|否| A4[取决于具体任务] style A1 fill:#4ecdc4 style A3 fill:#ff6b6b

关键 Takeaway

  1. GPT 和 BERT 都基于 Transformer,但选择了不同的预训练任务和架构
  2. GPT 用因果注意力,BERT 用双向注意力,这决定了它们的能力边界
  3. GPT 天然支持生成,BERT 只能理解,这是 GPT 胜出的关键
  4. Scaling Law 偏爱 Decoder-Only,这让 GPT 路线越走越远
  5. BERT 还有用,主要用于 Embedding 和分类任务
  6. 现代大模型清一色 Decoder-Only,这是经过验证的最优选择

下一步学习

  • [ ] Tokenization:大模型的"碎碎念"
  • [ ] 预训练:如何"喂"出一个大模型
  • [ ] SFT 与 RLHF:让模型学会"听话"

参考资料

  1. GPT-1 Paper - Improving Language Understanding by Generative Pre-Training
  2. BERT Paper - BERT: Pre-training of Deep Bidirectional Transformers
  3. GPT-2 Paper - Language Models are Unsupervised Multitask Learners
  4. GPT-3 Paper - Language Models are Few-Shot Learners
  5. T5 Paper - Exploring the Limits of Transfer Learning

评论区
暂无评论
avatar