跳转至

NLP 基础与预训练语言模型

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

学习目标:理解 NLP 的发展脉络、文本表示演进、以及预训练语言模型三大架构( Encoder-only 、 Decoder-only 、 Encoder-Decoder ),为深入学习大语言模型打下坚实基础。

📌 定位说明:本章覆盖从传统 NLP 到预训练语言模型( PLM )的完整知识体系。对标 happy-LLM Ch1 ( NLP 基础概念)和 Ch3 (预训练语言模型),我们的内容更系统、代码更完整、对比更深入。


目录


1. NLP 发展全景

1.1 NLP 发展的四个阶段

Text Only
时间线:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ~2013           2013-2018        2018-2022         2022~
  规则与统计时代    词向量时代        预训练模型时代      大模型时代

  专家系统          Word2Vec         BERT              GPT-4
  HMM/CRF          GloVe            GPT-2             LLaMA
  n-gram LM        ELMo             T5/BART           Claude
  SVM分类           Attention        XLNet             Gemini
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

第一阶段:规则与统计方法(~2013 )

  • 特点:人工设计规则和特征,统计模型学习参数
  • 代表方法
  • 正则表达式、语法规则
  • 隐马尔可夫模型( HMM )用于词性标注、命名实体识别
  • 条件随机场( CRF )用于序列标注
  • n-gram 语言模型
  • TF-IDF + SVM/朴素贝叶斯用于文本分类
  • 局限:依赖人工特征工程,泛化能力差

第二阶段:词向量与深度学习( 2013-2018 )

  • 特点:自动从数据中学习特征表示
  • 代表方法
  • Word2Vec ( 2013 ):分布式词向量
  • GloVe ( 2014 ):全局词向量
  • TextCNN ( 2014 ): CNN 用于文本分类
  • Seq2Seq + Attention ( 2015-2017 )
  • ELMo ( 2018 ):上下文相关词向量

第三阶段:预训练语言模型( 2018-2022 )

  • 特点:大规模预训练 + 微调( Pre-train & Fine-tune )
  • 代表模型: BERT 、 GPT-2 、 T5 、 BART 、 RoBERTa
  • 核心范式:先在大量无标注文本上预训练,再在下游任务上微调

第四阶段:大语言模型( 2022~)

  • 特点:规模法则( Scaling Laws )、涌现能力( Emergent Abilities )
  • 代表模型: GPT-4 、 Claude 、 LLaMA 、 Gemini
  • 核心范式:预训练 → 指令微调 → RLHF → 提示工程

1.2 NLP 任务分类体系

Text Only
NLP任务全景图
├── 文本分类
│   ├── 情感分析 (正面/负面/中性)
│   ├── 主题分类 (新闻类别)
│   ├── 意图识别 (对话系统)
│   └── 垃圾邮件检测
├── 序列标注
│   ├── 命名实体识别 NER (人名/地名/机构名)
│   ├── 词性标注 POS Tagging
│   └── 中文分词
├── 文本生成
│   ├── 机器翻译
│   ├── 文本摘要 (抽取式/生成式)
│   ├── 对话生成
│   └── 文本续写
├── 信息抽取
│   ├── 关系抽取
│   ├── 事件抽取
│   └── 三元组抽取 (主/谓/宾)
├── 文本匹配
│   ├── 语义相似度
│   ├── 自然语言推理 NLI
│   └── 问答匹配
└── 问答系统
    ├── 抽取式问答 (SQuAD)
    ├── 生成式问答
    └── 知识库问答 KBQA

2. 文本表示:从 One-hot 到 Contextual

文本表示是 NLP 的基石——如何把人类语言转化为计算机可处理的数值向量

2.1 One-hot 编码

最简单的文本表示:每个词用一个只有一个 1 其余全 0 的向量表示。

Python
import numpy as np

# 假设词表: ["猫", "狗", "鱼", "鸟"]
vocab = {"猫": 0, "狗": 1, "鱼": 2, "鸟": 3}
vocab_size = len(vocab)

def one_hot_encode(word, vocab, vocab_size):
    """One-hot编码"""
    vec = np.zeros(vocab_size)
    if word in vocab:
        vec[vocab[word]] = 1.0
    return vec

# 编码示例
cat_vec = one_hot_encode("猫", vocab, vocab_size)
dog_vec = one_hot_encode("狗", vocab, vocab_size)

print(f"猫: {cat_vec}")  # [1. 0. 0. 0.]
print(f"狗: {dog_vec}")  # [0. 1. 0. 0.]

# 问题:余弦相似度为0——"猫"和"狗"毫无关系?
cos_sim = np.dot(cat_vec, dog_vec) / (np.linalg.norm(cat_vec) * np.linalg.norm(dog_vec))
print(f"猫-狗 余弦相似度: {cos_sim}")  # 0.0

One-hot 的致命缺陷: 1. 维度灾难:词表大小=向量维度,中文词表可达 50 万+ 2. 语义缺失:任意两个不同词的相似度都是 0 3. 无法泛化:见过"猫很可爱"无法推广到"狗很可爱"

2.2 词袋模型与 TF-IDF

Python
from collections import Counter
import math

class TFIDF:
    """TF-IDF文本表示"""

    def __init__(self):
        self.vocab = {}
        self.idf = {}

    def fit(self, documents):
        """构建词表和计算IDF"""
        # 构建词表
        all_words = set()
        for doc in documents:
            all_words.update(doc.split())
        self.vocab = {word: i for i, word in enumerate(sorted(all_words))}

        # 计算IDF
        N = len(documents)
        for word in self.vocab:
            # 包含该词的文档数
            df = sum(1 for doc in documents if word in doc.split())
            self.idf[word] = math.log(N / (1 + df)) + 1  # 平滑IDF

    def transform(self, document):
        """将文档转换为TF-IDF向量"""
        words = document.split()
        tf = Counter(words)  # Counter统计元素出现次数
        total = len(words)

        vec = np.zeros(len(self.vocab))
        for word, count in tf.items():
            if word in self.vocab:
                tf_val = count / total
                vec[self.vocab[word]] = tf_val * self.idf.get(word, 1.0)
        return vec

# 示例
docs = [
    "猫 喜欢 吃 鱼",
    "狗 喜欢 吃 骨头",
    "猫 和 狗 都是 宠物"
]

tfidf = TFIDF()
tfidf.fit(docs)

for doc in docs:
    vec = tfidf.transform(doc)
    print(f"'{doc}' → 非零维度: {np.count_nonzero(vec)}, 向量模长: {np.linalg.norm(vec):.4f}")

TF-IDF 的优势与局限: - ✅ 考虑了词频( TF )和文档频率( IDF ) - ✅ 高频停用词("的"、"了")权重被压低 - ❌ 仍然是稀疏向量,无法捕捉语义 - ❌ 忽略词序:"猫追狗"和"狗追猫"表示相同

2.3 分词与子词切分:从字词到子词

为什么需要关注分词? 在任何文本表示方法之前,首先要将连续文本切分为离散单元( Token ),这一步直接影响模型的词表大小、未知词处理和语义表示质量。

中文分词的特殊挑战

Text Only
中文 vs 英文分词对比:
┌──────────────────────────────────────────────────┐
│ 英文:The cat sits on the mat.                    │
│ 天然空格分隔 → [The, cat, sits, on, the, mat]    │
├──────────────────────────────────────────────────┤
│ 中文:今天天气真好适合出去游玩                      │
│ 无天然分隔 → 需要分词算法                          │
│ 正确:[今天, 天气, 真, 好, 适合, 出去, 游玩]       │
│ 错误:[今, 天天, 气真, 好适, 合出, 去游, 玩]       │
└──────────────────────────────────────────────────┘

大模型时代的分词方法:现代 LLM 普遍采用子词切分( Subword Tokenization ),在词和字符之间取得平衡:

方法 核心思想 代表模型 特点
BPE 合并最高频字符对 GPT-⅔, LLaMA 自底向上,贪心合并
WordPiece 最大化语言模型似然 BERT, DistilBERT 类似 BPE 但选择标准不同
Unigram 从大词表逐步删减 T5, ALBERT 自顶向下,概率最优
SentencePiece 语言无关的统一框架 多语言模型 将文本视为字节流处理
Python
# BPE(Byte Pair Encoding)算法核心流程演示
def bpe_train(text, vocab_size=1000):
    """
    简化的 BPE 训练过程
    核心思想:反复合并最高频的相邻 token 对
    """
    # 步骤1:将文本拆分为字符序列
    words = text.split()
    # 每个词表示为字符元组,末尾加 </w> 标记词边界
    vocab = {}
    for word in words:
        symbols = tuple(list(word) + ['</w>'])
        vocab[symbols] = vocab.get(symbols, 0) + 1

    print(f"初始词表(字符级): {len(set(s for symbols in vocab for s in symbols))} 个符号")

    # 步骤2:迭代合并最高频的符号对
    num_merges = 10  # 简化演示
    for i in range(num_merges):
        # 统计所有相邻符号对的频率
        pairs = {}
        for symbols, freq in vocab.items():
            for j in range(len(symbols) - 1):
                pair = (symbols[j], symbols[j+1])
                pairs[pair] = pairs.get(pair, 0) + freq

        if not pairs:
            break

        # 找到频率最高的符号对
        best_pair = max(pairs, key=pairs.get)

        # 合并该符号对
        new_vocab = {}
        for symbols, freq in vocab.items():
            new_symbols = []
            j = 0
            while j < len(symbols):
                if j < len(symbols) - 1 and symbols[j] == best_pair[0] and symbols[j+1] == best_pair[1]:
                    new_symbols.append(best_pair[0] + best_pair[1])
                    j += 2
                else:
                    new_symbols.append(symbols[j])
                    j += 1
            new_vocab[tuple(new_symbols)] = freq

        vocab = new_vocab
        print(f"合并 {i+1}: {best_pair} → '{best_pair[0]+best_pair[1]}' (频率: {pairs[best_pair]})")

    return vocab

# 示例
sample_text = "low lower newest wide low low lower newest widest lowest"
print("=== BPE 训练演示 ===")
result = bpe_train(sample_text)
Text Only
子词切分的核心优势:
┌─────────────────────────────────────────────────────────────┐
│ 1. 解决未登录词( OOV )问题                                 │
│    "unhappiness" → ["un", "happi", "ness"]                  │
│    即使没见过完整词,也能通过子词理解含义                       │
│                                                              │
│ 2. 平衡词表大小和语义表示                                     │
│    纯字符:词表小(~数百),但语义信息少                        │
│    纯词:词表大(~数十万),但 OOV 严重                        │
│    子词:词表适中(~30K-50K),兼顾两者                        │
│                                                              │
│ 3. 多语言统一处理                                            │
│    SentencePiece 将文本视为字节流,无需语言特定预处理           │
│    支持中文(字+词混合)、英文(子词)、日文(多文字混合)等      │
└─────────────────────────────────────────────────────────────┘

参考来源:本节内容参考了 happy-llm 第一章( NLP 基础概念)和 hugging-llm 第一章(基础知识)对 Token 和 Embedding 的讲解,以及 diy-llm 第四章对分词算法的详细分析。

2.4 分布式词向量: Word2Vec

核心思想( Distributional Hypothesis ):一个词的含义由它的上下文决定

"You shall know a word by the company it keeps." — J.R. Firth, 1957

Python
import torch
import torch.nn as nn
import torch.optim as optim

class SkipGram(nn.Module):
    """
    Word2Vec Skip-Gram模型
    给定中心词,预测上下文词
    """
    def __init__(self, vocab_size, embedding_dim):
        super().__init__()  # super()调用父类方法
        # 中心词嵌入矩阵
        self.center_embeddings = nn.Embedding(vocab_size, embedding_dim)
        # 上下文词嵌入矩阵
        self.context_embeddings = nn.Embedding(vocab_size, embedding_dim)

        # 初始化
        nn.init.xavier_uniform_(self.center_embeddings.weight)
        nn.init.xavier_uniform_(self.context_embeddings.weight)

    def forward(self, center_ids, context_ids, negative_ids):
        """
        Negative Sampling训练
        Args:
            center_ids:   [batch_size] 中心词ID
            context_ids:  [batch_size] 正样本上下文词ID
            negative_ids: [batch_size, num_neg] 负样本ID
        """
        # 获取嵌入向量
        center = self.center_embeddings(center_ids)      # [B, D]
        context = self.context_embeddings(context_ids)    # [B, D]
        negatives = self.context_embeddings(negative_ids) # [B, num_neg, D]

        # 正样本得分: center · context
        pos_score = torch.sum(center * context, dim=-1)      # [B]
        pos_loss = -torch.log(torch.sigmoid(pos_score) + 1e-8)

        # 负样本得分: center · negative
        neg_score = torch.bmm(negatives, center.unsqueeze(-1)).squeeze(-1)  # [B, num_neg]  # unsqueeze增加一个维度
        neg_loss = -torch.log(torch.sigmoid(-neg_score) + 1e-8).sum(dim=-1)

        return (pos_loss + neg_loss).mean()

    def get_embedding(self, word_id):
        """获取词的最终向量(使用中心词嵌入)"""
        return self.center_embeddings(torch.tensor([word_id])).detach()

# --- 训练示例 ---
# 构建简单语料
corpus = "the cat sat on the mat the dog sat on the rug".split()
vocab = {w: i for i, w in enumerate(set(corpus))}
vocab_size = len(vocab)
embedding_dim = 32

model = SkipGram(vocab_size, embedding_dim)
optimizer = optim.Adam(model.parameters(), lr=0.01)

print(f"词表大小: {vocab_size}, 嵌入维度: {embedding_dim}")
print(f"词表: {vocab}")

# 生成训练样本(简化版,window_size=2)
window_size = 2
training_pairs = []
for i, word in enumerate(corpus):  # enumerate同时获取索引和元素
    center_id = vocab[word]
    for j in range(max(0, i - window_size), min(len(corpus), i + window_size + 1)):
        if i != j:
            context_id = vocab[corpus[j]]
            # 随机负采样
            neg_ids = []
            while len(neg_ids) < 5:
                neg = torch.randint(0, vocab_size, (1,)).item()
                if neg != context_id:
                    neg_ids.append(neg)
            training_pairs.append((center_id, context_id, neg_ids))

print(f"训练样本数: {len(training_pairs)}")

# 训练
for epoch in range(100):
    total_loss = 0
    for center, context, negs in training_pairs:
        loss = model(
            torch.tensor([center]),
            torch.tensor([context]),
            torch.tensor([negs])
        )
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    if (epoch + 1) % 20 == 0:
        print(f"Epoch {epoch+1}, Loss: {total_loss/len(training_pairs):.4f}")

# 词向量相似度
def cosine_similarity(v1, v2):
    return torch.cosine_similarity(v1, v2, dim=-1).item()

cat_emb = model.get_embedding(vocab["cat"])
dog_emb = model.get_embedding(vocab["dog"])
mat_emb = model.get_embedding(vocab["mat"])

print(f"\n词向量相似度:")
print(f"  cat-dog: {cosine_similarity(cat_emb, dog_emb):.4f}")
print(f"  cat-mat: {cosine_similarity(cat_emb, mat_emb):.4f}")

Word2Vec 的核心贡献: - 将词从稀疏的 one-hot 映射到低维稠密向量空间 - 语义相似的词在向量空间中距离接近 - 著名的类比推理:\(\vec{king} - \vec{man} + \vec{woman} \approx \vec{queen}\)

2.5 从静态到动态: ELMo

Word2Vec 的局限:一个词只有一个固定向量,无法处理一词多义。 - "Apple 推出了新 iPhone" → Apple 表示苹果公司 - "I ate an apple" → apple 表示水果

ELMo ( Embeddings from Language Models, 2018 ) 的解决方案:

Text Only
ELMo架构:
                    最终表示 = γ(s₀·h₀ + s₁·h₁ + s₂·h₂)
            ┌────────────┼────────────┐
       h₀(词嵌入)   h₁(前向LSTM层1  h₂(前向LSTM层2
                      +反向LSTM层1)   +反向LSTM层2)
            ↑            ↑                ↑
         字符CNN    双向LSTM层1      双向LSTM层2
            ↑            ↑                ↑
         输入词      隐藏状态传递     隐藏状态传递

ELMo 的关键创新: 1. 上下文相关:同一个词在不同句子中有不同向量 2. 多层表示:底层捕捉语法信息,高层捕捉语义信息 3. 双向建模:同时考虑左侧和右侧上下文

Python
# ELMo的核心思想示意(简化版)
class SimpleBiLM(nn.Module):
    """简化版双向语言模型(ELMo核心思想)"""

    def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)

        # 前向LSTM
        self.forward_lstm = nn.LSTM(
            embed_dim, hidden_dim, num_layers,
            batch_first=True, dropout=0.1
        )
        # 反向LSTM
        self.backward_lstm = nn.LSTM(
            embed_dim, hidden_dim, num_layers,
            batch_first=True, dropout=0.1
        )

        # 层权重(可学习的标量混合参数)
        self.layer_weights = nn.Parameter(torch.ones(num_layers + 1) / (num_layers + 1))
        self.gamma = nn.Parameter(torch.ones(1))

    def forward(self, input_ids):
        """
        Args:
            input_ids: [batch_size, seq_len]
        Returns:
            contextualized: [batch_size, seq_len, hidden_dim * 2]
        """
        embeds = self.embedding(input_ids)  # [B, L, E]

        # 前向
        fwd_out, _ = self.forward_lstm(embeds)        # [B, L, H]
        # 反向(翻转序列)
        bwd_out, _ = self.backward_lstm(embeds.flip(1))
        bwd_out = bwd_out.flip(1)                     # 翻回来 [B, L, H]

        # 拼接双向表示
        contextualized = torch.cat([fwd_out, bwd_out], dim=-1)  # [B, L, 2H]

        return contextualized

# ELMo提供了上下文相关的词表示
# 同一个 "bank" 在 "river bank" 和 "bank account" 中有不同向量

2.6 文本表示演进总结

Text Only
文本表示方法演进对比
┌──────────────────┬──────────────┬──────────────┬──────────────┐
│ 方法             │ 表示类型     │ 语义能力     │ 上下文感知   │
├──────────────────┼──────────────┼──────────────┼──────────────┤
│ One-hot          │ 稀疏,高维    │ ✗ 无语义     │ ✗ 无         │
│ TF-IDF           │ 稀疏,高维    │ △ 词频统计   │ ✗ 无         │
│ Word2Vec/GloVe   │ 稠密,低维    │ ✓ 分布式语义 │ ✗ 静态       │
│ ELMo             │ 稠密,动态    │ ✓ 深层语义   │ ✓ 上下文相关 │
│ BERT/GPT         │ 稠密,动态    │ ✓✓ 深层语义  │ ✓✓ 全上下文  │
└──────────────────┴──────────────┴──────────────┴──────────────┘

3. 经典 NLP 任务与方法

3.1 文本分类

Python
import torch
import torch.nn as nn

class TextCNN(nn.Module):
    """
    TextCNN (Kim, 2014) — 经典文本分类模型
    用不同大小的卷积核捕捉n-gram特征
    """
    def __init__(self, vocab_size, embed_dim, num_classes,
                 filter_sizes=(3, 4, 5), num_filters=100):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)

        # 多尺度卷积
        self.convs = nn.ModuleList([
            nn.Conv1d(embed_dim, num_filters, fs)
            for fs in filter_sizes
        ])

        self.dropout = nn.Dropout(0.5)
        self.fc = nn.Linear(num_filters * len(filter_sizes), num_classes)

    def forward(self, x):
        """
        Args: x [batch_size, seq_len]
        Returns: logits [batch_size, num_classes]
        """
        # [B, L, E] → [B, E, L] (Conv1d需要 channels-first)
        embedded = self.embedding(x).transpose(1, 2)

        # 多尺度卷积 + ReLU + 最大池化
        pooled = []
        for conv in self.convs:
            h = torch.relu(conv(embedded))       # [B, num_filters, L-fs+1]
            h = torch.max(h, dim=-1).values      # [B, num_filters]
            pooled.append(h)

        # 拼接所有尺度的特征
        cat = torch.cat(pooled, dim=-1)           # [B, num_filters * len(filter_sizes)]
        return self.fc(self.dropout(cat))

# 使用示例
model = TextCNN(vocab_size=10000, embed_dim=128, num_classes=4)
dummy_input = torch.randint(0, 10000, (8, 50))  # batch=8, seq_len=50
output = model(dummy_input)
print(f"TextCNN输出形状: {output.shape}")  # [8, 4]

3.2 序列标注:命名实体识别

Python
class BiLSTM_CRF(nn.Module):
    """
    BiLSTM-CRF — 经典序列标注模型
    BiLSTM提取特征,CRF建模标签间的转移约束
    """
    def __init__(self, vocab_size, embed_dim, hidden_dim, num_tags):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.lstm = nn.LSTM(
            embed_dim, hidden_dim // 2,
            bidirectional=True, batch_first=True
        )
        self.hidden2tag = nn.Linear(hidden_dim, num_tags)

        # CRF转移矩阵: transitions[i][j] = 从标签j转移到标签i的分数
        self.transitions = nn.Parameter(torch.randn(num_tags, num_tags))
        self.num_tags = num_tags

    def _get_emissions(self, x):
        """获取发射分数"""
        embeds = self.embedding(x)
        lstm_out, _ = self.lstm(embeds)
        return self.hidden2tag(lstm_out)  # [B, L, num_tags]

    def forward(self, x):
        """推理时使用维特比解码"""
        emissions = self._get_emissions(x)  # [B, L, T]
        # 简化版:逐batch维特比解码
        batch_size, seq_len, num_tags = emissions.shape

        best_paths = []
        for b in range(batch_size):
            # 维特比算法
            viterbi = emissions[b, 0]  # [T]
            backpointers = []

            for t in range(1, seq_len):
                # viterbi_expand: [T_prev] + transitions: [T_cur, T_prev] → [T_cur, T_prev]
                scores = viterbi.unsqueeze(0) + self.transitions  # [T, T]
                scores = scores + emissions[b, t].unsqueeze(1)    # [T, T]

                best_scores, best_ids = scores.max(dim=1)        # [T]
                viterbi = best_scores
                backpointers.append(best_ids)

            # 回溯
            best_last = viterbi.argmax().item()
            best_path = [best_last]
            for bp in reversed(backpointers):
                best_path.append(bp[best_path[-1]].item())  # [-1]负索引取最后一个元素
            best_path.reverse()
            best_paths.append(best_path)

        return best_paths

# NER标签示例: BIO标注
tag2id = {"O": 0, "B-PER": 1, "I-PER": 2, "B-ORG": 3, "I-ORG": 4, "B-LOC": 5, "I-LOC": 6}
model = BiLSTM_CRF(vocab_size=5000, embed_dim=128, hidden_dim=256, num_tags=len(tag2id))

dummy_input = torch.randint(0, 5000, (4, 20))
predictions = model(dummy_input)
print(f"BiLSTM-CRF预测: 4个序列, 各{len(predictions[0])}个标签")

3.3 Seq2Seq 与注意力机制

Python
class Seq2SeqWithAttention(nn.Module):
    """
    带注意力的Seq2Seq(Bahdanau Attention)
    经典的编码器-解码器架构,为Transformer奠定基础
    """
    def __init__(self, src_vocab, tgt_vocab, embed_dim, hidden_dim):
        super().__init__()
        # 编码器
        self.src_embedding = nn.Embedding(src_vocab, embed_dim)
        self.encoder = nn.GRU(embed_dim, hidden_dim, bidirectional=True, batch_first=True)

        # 解码器
        self.tgt_embedding = nn.Embedding(tgt_vocab, embed_dim)
        self.decoder = nn.GRU(embed_dim + hidden_dim * 2, hidden_dim, batch_first=True)

        # 注意力
        self.attn = nn.Linear(hidden_dim * 3, hidden_dim)
        self.v = nn.Linear(hidden_dim, 1, bias=False)

        # 输出
        self.output = nn.Linear(hidden_dim, tgt_vocab)

    def attention(self, decoder_hidden, encoder_outputs):
        """
        Bahdanau (Additive) Attention
        Args:
            decoder_hidden: [B, 1, H]
            encoder_outputs: [B, src_len, 2H]
        Returns:
            context: [B, 1, 2H]
        """
        src_len = encoder_outputs.shape[1]
        hidden_expanded = decoder_hidden.repeat(1, src_len, 1)  # [B, src_len, H]

        # 拼接 + 打分
        energy = torch.tanh(self.attn(
            torch.cat([hidden_expanded, encoder_outputs], dim=-1)  # [B, src_len, 3H]
        ))
        scores = self.v(energy).squeeze(-1)      # [B, src_len]
        weights = torch.softmax(scores, dim=-1)   # [B, src_len]

        context = torch.bmm(weights.unsqueeze(1), encoder_outputs)  # [B, 1, 2H]
        return context, weights

print("Seq2Seq with Attention: 注意力机制是Transformer的前身")
print("关键洞察: 让解码器在每一步'看'编码器的不同位置")

4. 预训练语言模型的诞生

4.1 预训练-微调范式

Text Only
预训练-微调(Pre-train & Fine-tune)范式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

阶段1: 预训练(无监督/自监督)
┌──────────────────────────────────┐
│  大规模无标注文本(数十GB~TB)    │
│  ↓                              │
│  自监督目标(MLM / CLM / DAE)   │
│  ↓                              │
│  学习通用语言知识               │
│  → 语法、语义、世界知识、推理    │
└──────────────────────────────────┘

阶段2: 微调(有监督)
┌──────────────────────────────────┐
│  少量标注数据(数千~数万条)     │
│  ↓                              │
│  特定任务目标(分类/NER/QA...)  │
│  ↓                              │
│  适配下游任务                   │
└──────────────────────────────────┘

4.2 三种预训练目标

预训练目标 全称 代表模型 示意
MLM Masked Language Model BERT [CLS] 我 [MASK] 北京 [SEP] → 预测 "在"
CLM Causal Language Model GPT 我 在 北京 → 预测 "的"
DAE Denoising AutoEncoder T5 输入加噪 → 恢复原文
Python
# 三种预训练目标的直观对比
print("""
┌─────────────────────────────────────────────────────────┐
│ MLM (BERT): 完形填空                                     │
│   输入: "小明在[MASK]读大学, 他的专业是[MASK]科学"        │
│   目标: 预测被遮住的词 → "北京", "计算机"                 │
│   特点: 双向上下文, 15%的词被随机遮盖                     │
├─────────────────────────────────────────────────────────┤
│ CLM (GPT): 从左到右续写                                  │
│   输入: "小明在北京读大学"                                │
│   目标: P(在|小明) × P(北京|小明,在) × P(读|小明,在,北京)│
│   特点: 单向(从左到右), 自回归生成                        │
├─────────────────────────────────────────────────────────┤
│ DAE (T5): 去噪重建                                      │
│   输入: "小明在<X>读大学, 他的专业是<Y>"                  │
│   目标: "<X> 北京 <Y> 计算机科学"                        │
│   特点: Encoder看全文, Decoder生成被替换的片段            │
└─────────────────────────────────────────────────────────┘
""")

5. Encoder-only 架构: BERT 系列

5.1 BERT 核心设计

Text Only
BERT (Bidirectional Encoder Representations from Transformers)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

输入:   [CLS] 我 爱 [MASK] 京 [SEP] 天气 真 好 [SEP]
         ↓    ↓  ↓    ↓     ↓    ↓     ↓   ↓  ↓    ↓
Token:  E0   E1  E2   E3   E4   E5    E6  E7  E8   E9
   +
Segment: SA   SA  SA   SA   SA   SA    SB  SB  SB   SB
   +
Position: P0  P1  P2   P3   P4   P5    P6  P7  P8   P9
         ↓    ↓   ↓    ↓    ↓    ↓     ↓   ↓   ↓    ↓
    ┌──────────────────────────────────────────────────┐
    │           Transformer Encoder × 12/24            │
    │         (双向Self-Attention, 每个位置             │
    │          都能看到所有其他位置)                    │
    └──────────────────────────────────────────────────┘
         ↓    ↓   ↓    ↓    ↓    ↓     ↓   ↓   ↓    ↓
输出:   T0   T1  T2   T3   T4   T5    T6  T7  T8   T9
        ↓                   ↓
      [CLS]用于          [MASK]位置
      句子级任务          用于MLM预测

5.2 BERT 微调实战

Python
import torch
import torch.nn as nn

class BERTForClassification(nn.Module):
    """
    BERT用于文本分类(微调范式的典型用法)
    """
    def __init__(self, hidden_size=768, num_classes=2, dropout=0.1):
        super().__init__()
        # 实际使用时这里加载预训练的BERT
        # self.bert = BertModel.from_pretrained('bert-base-chinese')

        # 简化版:模拟BERT encoder
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=hidden_size, nhead=12,
            dim_feedforward=3072, dropout=dropout,
            batch_first=True
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=12)
        self.embedding = nn.Embedding(21128, hidden_size)  # BERT中文词表大小
        self.pos_embedding = nn.Embedding(512, hidden_size)

        # 分类头
        self.classifier = nn.Sequential(
            nn.Dropout(dropout),
            nn.Linear(hidden_size, hidden_size),
            nn.Tanh(),
            nn.Dropout(dropout),
            nn.Linear(hidden_size, num_classes)
        )

    def forward(self, input_ids, attention_mask=None):
        """
        Args:
            input_ids: [B, L] token IDs
            attention_mask: [B, L] 1=有效token, 0=padding
        """
        B, L = input_ids.shape
        positions = torch.arange(L, device=input_ids.device).unsqueeze(0).expand(B, L)

        # Token + Position Embedding
        x = self.embedding(input_ids) + self.pos_embedding(positions)

        # Transformer Encoder
        if attention_mask is not None:
            # 将 0/1 mask 转换为 True/False(True表示被遮蔽)
            src_key_padding_mask = (attention_mask == 0)
        else:
            src_key_padding_mask = None

        encoded = self.encoder(x, src_key_padding_mask=src_key_padding_mask)

        # 取 [CLS] 位置的输出做分类
        cls_output = encoded[:, 0, :]  # [B, H]
        return self.classifier(cls_output)

# 使用示例
model = BERTForClassification(hidden_size=768, num_classes=3)
dummy_ids = torch.randint(0, 21128, (4, 128))
dummy_mask = torch.ones(4, 128)

logits = model(dummy_ids, dummy_mask)
print(f"BERT分类输出: {logits.shape}")  # [4, 3]

5.3 BERT 系列演进

模型 改进点 关键思想
BERT (2018) 基础版 MLM + NSP, 双向编码
RoBERTa (2019) 训练优化 去掉 NSP ,动态 Masking ,更多数据/更长训练
ALBERT (2019) 参数效率 因式分解嵌入,跨层参数共享
DistilBERT (2019) 知识蒸馏 6 层蒸馏 12 层,速度 2x ,性能保留 97%
ELECTRA (2020) 训练效率 替换 Token 检测( RTD ),比 MLM 效率更高
DeBERTa (2021) 注意力增强 解耦注意力(内容+位置分开计算)

5.3.1 RoBERTa 详解

RoBERTa( Robustly Optimized BERT Pretraining Approach )由 Meta 于 2019 年发布,核心思想是通过更充分的训练来释放 BERT 架构的潜力,而非修改模型结构。

三大优化

Text Only
优化一:去掉 NSP 任务
├── 实验证明 NSP 过于简单,对下游任务帮助有限甚至有害
├── 改为"单文档 MLM ":一个输入只从一个文档采样
└── 实验组对比:去掉 NSP 后在所有下游任务上均有提升

优化二:更大规模预训练
├── 数据量:BERT 13GB → RoBERTa 160GB( 10 倍)
├── 数据来源:BookCorpus + Wikipedia + CC-NEWS + OpenWebText + Stories
├── Batch Size:BERT 256 → RoBERTa 8K
├── 训练步长:BERT 1M 步 → RoBERTa 500K 步(更大 batch )
└── 训练资源:1024 块 V100( 32GB )训练 1 天

优化三:动态遮蔽 + 更大 BPE 词表
├── BERT :静态 Mask (数据预处理阶段完成,每 10 epoch 相同)
├── RoBERTa :动态 Mask (训练时实时生成,每个 epoch 不同)
└── 词表:BERT 30K WordPiece → RoBERTa 50K BPE
Python
# RoBERTa vs BERT 预训练配置对比
config_comparison = {
    "BERT-base": {
        "参数量": "110M",
        "预训练数据": "13GB (3.3B tokens)",
        "Batch Size": 256,
        "训练步数": "1M",
        "词表大小": "30K (WordPiece)",
        "预训练任务": "MLM + NSP",
        "遮蔽策略": "静态(4次随机Mask)",
    },
    "RoBERTa-base": {
        "参数量": "125M",
        "预训练数据": "160GB (约10x BERT)",
        "Batch Size": "8K",
        "训练步数": "500K",
        "词表大小": "50K (BPE)",
        "预训练任务": "仅MLM(去掉NSP)",
        "遮蔽策略": "动态(每epoch实时生成)",
    }
}

# 核心洞察:同样的架构,更充分的训练 → 显著性能提升
# 证明了"训练方法"与"模型架构"同等重要

5.3.2 ALBERT 详解

ALBERT( A Lite BERT )由 Google 于 2019 年发布,核心思想是通过参数共享和因式分解来降低模型参数量,同时保持甚至提升模型性能。

三大优化

Text Only
优化一:嵌入参数因式分解
├── 问题:BERT 的 Embedding 矩阵为 V×H(词表×隐藏层维度)
│   如 V=30K, H=1024 → Embedding 参数 30M
│   且 H 增大时 Embedding 参数同步膨胀
├── 洞察:Word2Vec 仅用 100 维就获得了好的词向量
│   Embedding 不需要和隐藏层同维度
└── 方案:V×E + E×H(E=128),参数从 V×H 降低到 V×E + E×H
    当 E << H 时优化明显

优化二:跨层参数共享
├── 发现:BERT 各层 Encoder 参数存在高度一致性
├── 方案:所有层共享同一组参数(只初始化一层)
├── 效果:24 层 ALBERT-xlarge 仅 59M 参数( BERT-large 340M )
└── 注意:参数减少但计算量不减(仍需 24 次前向传播)

优化三:SOP 替代 NSP
├── NSP 问题:太简单(随机句子对 vs 连续句子)
├── SOP( Sentence Order Prediction ):判断两句话顺序是否正确
│   正样本:连续两句(A→B)
│   负样本:交换顺序(B→A)
└── SOP 比 NSP 更难,迫使模型学习更细粒度的句间关系
Python
# ALBERT 参数效率演示
def albert_param_savings(vocab_size, hidden_size, embed_size, num_layers):
    """
    计算 ALBERT 通过因式分解和参数共享节省的参数量
    """
    # 标准 BERT 的 Embedding 参数
    bert_embed_params = vocab_size * hidden_size

    # ALBERT 因式分解后的 Embedding 参数
    albert_embed_params = vocab_size * embed_size + embed_size * hidden_size

    # 单层 Encoder 参数(简化估计)
    single_layer_params = 4 * hidden_size * hidden_size  # Attention + FFN

    # BERT: 每层独立参数
    bert_encoder_params = num_layers * single_layer_params

    # ALBERT: 所有层共享参数
    albert_encoder_params = single_layer_params  # 仅一层

    bert_total = bert_embed_params + bert_encoder_params
    albert_total = albert_embed_params + albert_encoder_params

    print(f"BERT 参数量: {bert_total / 1e6:.1f}M")
    print(f"ALBERT 参数量: {albert_total / 1e6:.1f}M")
    print(f"参数压缩比: {bert_total / albert_total:.1f}x")

# 示例:24层模型
albert_param_savings(vocab_size=30000, hidden_size=1024,
                     embed_size=128, num_layers=24)
# BERT 参数量: ~340M → ALBERT 参数量: ~59M(压缩约 5.8x )

参考来源:本节内容参考了 happy-llm 第三章(预训练语言模型)对 RoBERTa 和 ALBERT 的详细分析,以及原始论文 Liu et al. (2019) 和 Lan et al. (2019) 。


6. Decoder-only 架构: GPT 系列

6.1 GPT 核心设计

Text Only
GPT (Generative Pre-trained Transformer)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

输入:   今天 天气 →  真 →  好
         ↓    ↓      ↓     ↓
    ┌──────────────────────────────┐
    │   Transformer Decoder × N    │
    │   (因果自注意力:              │
    │    每个位置只能看到           │
    │    自己及之前的位置)          │
    │                              │
    │   今天 → 能看到: [今天]      │
    │   天气 → 能看到: [今天,天气]  │
    │   真   → 能看到: [今天,..,真]│
    └──────────────────────────────┘
         ↓    ↓      ↓     ↓
预测:   天气  真     好    [EOS]
   P(天气|今天) × P(真|今天,天气) × P(好|今天,天气,真)×...

GPT 与 BERT 的根本区别: - BERT 是理解模型:双向编码,擅长分类/抽取 - GPT 是生成模型:自回归,擅长文本生成

6.2 GPT 系列演进

Python
# GPT系列参数规模演进
gpt_evolution = {
    "GPT-1 (2018)":   {"参数": "117M",  "数据": "5GB",    "关键创新": "预训练+微调范式"},
    "GPT-2 (2019)":   {"参数": "1.5B",  "数据": "40GB",   "关键创新": "Zero-shot多任务"},
    "GPT-3 (2020)":   {"参数": "175B",  "数据": "570GB",  "关键创新": "In-context Learning"},
    "GPT-3.5 (2022)": {"参数": "~175B", "数据": "更大",   "关键创新": "RLHF/InstructGPT"},
    "GPT-4 (2023)":   {"参数": "未公开", "数据": "未公开", "关键创新": "多模态/MoE传闻"},
}

print("GPT系列关键里程碑:")
print("=" * 60)
for name, info in gpt_evolution.items():
    print(f"{name}:")
    print(f"  参数量: {info['参数']}")
    print(f"  训练数据: {info['数据']}")
    print(f"  关键创新: {info['关键创新']}")
    print()

print("核心洞察:")
print("GPT-1→2:规模增大 → 零样本能力出现")
print("GPT-2→3:10x规模 → In-context Learning涌现")
print("GPT-3→3.5:RLHF对齐 → 真正可用的AI助手")
print("GPT-3.5→4:多模态 + 推理能力飞跃")

6.3 In-context Learning : GPT-3 的涌现能力

Python
# In-context Learning示例:不需要微调,直接在prompt中提供示例

# Zero-shot(零样本)
zero_shot_prompt = """
请将以下英文翻译成中文:
The weather is beautiful today.
"""

# One-shot(单样本)
one_shot_prompt = """
英文翻译成中文示例:
English: Hello, how are you?
中文: 你好,你怎么样?

请翻译:
English: The weather is beautiful today.
中文:
"""

# Few-shot(少样本)
few_shot_prompt = """
将英文情感分类为正面或负面:

"This movie is amazing!" → 正面
"I hated every minute of it." → 负面
"The food was delicious and service was great." → 正面

"The product broke after one day." →
"""

print("In-context Learning的三种模式:")
print("  Zero-shot: 不给示例,直接问")
print("  One-shot:  给1个示例")
print("  Few-shot:  给2-5个示例")
print()
print("关键洞察:GPT-3不需要更新参数就能适应新任务!")
print("这是'涌现能力'的典型体现。")

7. Encoder-Decoder 架构: T5/BART

7.1 T5 :统一文本到文本框架

Text Only
T5 (Text-to-Text Transfer Transformer)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

核心思想:将所有NLP任务统一为 文本→文本 格式

分类:     "classify: I love this movie"            → "positive"
翻译:     "translate English to Chinese: Hello"     → "你好"
摘要:     "summarize: <长文本>"                     → "<短摘要>"
问答:     "question: What is AI? context: <上下文>" → "AI is..."
NER:      "tag: 小明在北京上学"                     → "小明:PER 北京:LOC"
Python
# T5的统一框架思想
class T5Conceptual(nn.Module):
    """
    T5概念性实现:展示Encoder-Decoder统一框架
    """
    def __init__(self, vocab_size, d_model, nhead, num_layers):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.pos_embedding = nn.Embedding(512, d_model)

        # 编码器
        encoder_layer = nn.TransformerEncoderLayer(
            d_model, nhead, d_model * 4, batch_first=True
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)

        # 解码器
        decoder_layer = nn.TransformerDecoderLayer(
            d_model, nhead, d_model * 4, batch_first=True
        )
        self.decoder = nn.TransformerDecoder(decoder_layer, num_layers)

        # 输出投影
        self.output_proj = nn.Linear(d_model, vocab_size)

    def forward(self, src_ids, tgt_ids):
        """
        Encoder-Decoder前向传播
        src_ids: 编码器输入 [B, src_len]
        tgt_ids: 解码器输入 [B, tgt_len]
        """
        B, src_len = src_ids.shape
        _, tgt_len = tgt_ids.shape

        # 嵌入
        src_pos = torch.arange(src_len, device=src_ids.device).unsqueeze(0)
        tgt_pos = torch.arange(tgt_len, device=tgt_ids.device).unsqueeze(0)

        src = self.embedding(src_ids) + self.pos_embedding(src_pos)
        tgt = self.embedding(tgt_ids) + self.pos_embedding(tgt_pos)

        # 编码
        memory = self.encoder(src)  # [B, src_len, D]

        # 因果mask(解码器只能看到之前的token)
        causal_mask = nn.Transformer.generate_square_subsequent_mask(tgt_len)
        causal_mask = causal_mask.to(src_ids.device)

        # 解码
        decoded = self.decoder(tgt, memory, tgt_mask=causal_mask)  # [B, tgt_len, D]

        return self.output_proj(decoded)  # [B, tgt_len, vocab_size]

model = T5Conceptual(vocab_size=32000, d_model=512, nhead=8, num_layers=6)
src = torch.randint(0, 32000, (2, 30))
tgt = torch.randint(0, 32000, (2, 20))
output = model(src, tgt)
print(f"T5输出形状: {output.shape}")  # [2, 20, 32000]

7.2 BART :去噪自编码器

Text Only
BART预训练:对输入加噪,让模型恢复原文
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

5种噪声策略:
┌─────────────────────────────────────┐
│ 1. Token Masking  : A B _ D E       │
│ 2. Token Deletion : A B D E         │
│ 3. Text Infilling : A _ D E (用1个_ │
│                     替换连续多个)    │
│ 4. Sentence Permutation: 打乱句子序 │
│ 5. Document Rotation: 旋转文档起点  │
└─────────────────────────────────────┘
   原文: A B C D E

BART = BERT的双向编码 + GPT的自回归解码
     → 兼具理解和生成能力

8. 三种架构深度对比

8.1 架构对比表

Python
comparison = """
┌──────────────────┬──────────────────┬──────────────────┬──────────────────┐
│                  │ Encoder-only     │ Decoder-only     │ Encoder-Decoder  │
│                  │ (BERT系列)       │ (GPT系列)        │ (T5/BART)        │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 注意力方向       │ 双向(全可见)     │ 单向(因果mask)   │ 编码器双向       │
│                  │                  │                  │ 解码器单向+交叉  │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 预训练目标       │ MLM(完形填空)    │ CLM(下一词预测)  │ DAE(去噪重建)    │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 输入             │ 完整文本         │ 前缀/提示        │ 源文本           │
│ 输出             │ 每位置的表示     │ 自回归生成       │ 目标文本         │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 擅长任务         │ 分类/NER/匹配    │ 文本生成/对话    │ 翻译/摘要/QA     │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 微调方式         │ 加分类头微调     │ 自回归微调       │ Seq2Seq微调      │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 代表模型         │ BERT, RoBERTa    │ GPT-4, LLaMA     │ T5, BART, mT5    │
│                  │ DeBERTa, ELECTRA │ Claude, Gemini   │ FLAN-T5          │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 当前地位         │ NLU任务首选      │ LLM主流架构      │ 翻译/摘要经典    │
│                  │ (但被LLM逐渐     │ (统治性地位)     │ (但被LLM逐渐     │
│                  │  取代)           │                  │  取代)           │
└──────────────────┴──────────────────┴──────────────────┴──────────────────┘
"""
print(comparison)

8.2 为什么 Decoder-only 成为 LLM 主流

Python
reasons = """
Decoder-only架构(GPT/LLaMA/Claude)为何成为主流?

1. 统一输入输出格式
   - Encoder-Decoder需要区分"源"和"目标"
   - Decoder-only只需要一个连续序列,更简洁
   - 所有任务统一为: prompt → completion

2. 规模扩展效率更高
   - 只有一个Transformer栈,参数利用更高效
   - 相同参数量下,Decoder-only通常效果更好
   - KV Cache加速推理,Encoder-Decoder需要额外存编码器cache

3. In-context Learning天然适配
   - 自回归生成 = 自然的多轮对话
   - Few-shot提示 = 把示例放在前面然后续写
   - BERT的[CLS] + Fine-tune范式在LLM时代不够灵活

4. 训练数据利用率
   - CLM利用每一个token位置做预测(N-1个预测目标)
   - MLM只预测15%被mask的token
   - 在海量数据上,CLM的数据利用率更高

5. 生态与工程优势
   - 推理只需要一个统一的Decoder,工程简单
   - KV Cache可以高效缓存,支持流式输出
   - 但BERT在检索、Embedding等任务上仍有独特优势!
"""
print(reasons)

8.3 代码对比实验

Python
import torch
import torch.nn as nn

class MiniEncoder(nn.Module):
    """最小Encoder-only(BERT风格)"""
    def __init__(self, vocab_size=1000, d_model=128, nhead=4, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4, batch_first=True)
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
        self.mlm_head = nn.Linear(d_model, vocab_size)

    def forward(self, x, mask_positions=None):
        h = self.encoder(self.embedding(x))
        if mask_positions is not None:
            # 只在mask位置预测
            return self.mlm_head(h[:, mask_positions, :])
        return h  # 返回所有位置的表示

class MiniDecoder(nn.Module):
    """最小Decoder-only(GPT风格)"""
    def __init__(self, vocab_size=1000, d_model=128, nhead=4, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        decoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4, batch_first=True)
        self.decoder = nn.TransformerEncoder(decoder_layer, num_layers)
        self.lm_head = nn.Linear(d_model, vocab_size)

    def forward(self, x):
        L = x.shape[1]
        # 因果mask: 每个位置只能看到自己和之前
        causal_mask = torch.triu(torch.ones(L, L), diagonal=1).bool().to(x.device)
        h = self.decoder(self.embedding(x), src_key_padding_mask=None,
                         mask=causal_mask)
        return self.lm_head(h)  # 每个位置预测下一个token

class MiniEncDec(nn.Module):
    """最小Encoder-Decoder(T5风格)"""
    def __init__(self, vocab_size=1000, d_model=128, nhead=4, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.transformer = nn.Transformer(
            d_model, nhead, num_layers, num_layers, d_model*4, batch_first=True
        )
        self.output = nn.Linear(d_model, vocab_size)

    def forward(self, src, tgt):
        L = tgt.shape[1]
        causal_mask = torch.triu(torch.ones(L, L), diagonal=1).bool().to(src.device)

        src_emb = self.embedding(src)
        tgt_emb = self.embedding(tgt)
        h = self.transformer(src_emb, tgt_emb, tgt_mask=causal_mask)
        return self.output(h)

# 对比三种架构
print("三种架构参数量对比(相同配置):")
print("=" * 50)

for name, model in [
    ("Encoder-only (BERT)", MiniEncoder()),
    ("Decoder-only (GPT)", MiniDecoder()),
    ("Encoder-Decoder (T5)", MiniEncDec())
]:
    params = sum(p.numel() for p in model.parameters())
    print(f"  {name}: {params:,} 参数")

# 推理对比
print("\n推理特性对比:")
print("  Encoder-only: 一次性编码整个序列 → 产出固定表示")
print("  Decoder-only: 自回归逐token生成 → 可配合KV Cache")
print("  Encoder-Decoder: 先编码源序列, 再自回归解码目标序列")

9. 从 PLM 到 LLM :范式转变

9.1 Scaling Laws(规模法则)

Scaling Laws 是大模型时代最重要的经验规律之一,由 OpenAI 的 Kaplan et al. (2020) 首次系统阐述,后经 DeepMind 的 Chinchilla 论文 (Hoffmann et al., 2022) 修正。

核心发现:幂律关系

模型在自回归语言建模上的交叉熵损失 \(L\) 与三个因素呈幂律关系

\[ L(N) \propto N^{-\alpha_N}, \quad \alpha_N \approx 0.076 \quad \text{(参数量 } N \text{)} \]
\[ L(D) \propto D^{-\alpha_D}, \quad \alpha_D \approx 0.095 \quad \text{(数据量 } D \text{,token 数)} \]
\[ L(C) \propto C^{-\alpha_C}, \quad \alpha_C \approx 0.050 \quad \text{(计算量 } C \text{,FLOPs)} \]

这意味着:将参数量增加 10 倍,损失大约降低 15%(因为 \(10^{-0.076} \approx 0.83\),即损失减少到 83%)。

三个关键推论

Text Only
推论1: 模型越大越高效
├── 在固定计算预算下,训练一个较大的模型 fewer steps
│   比训练一个较小的模型 more steps 效果更好
├── 这解释了为什么 GPT-3 (175B) 只训练了 300B tokens
│   而不是把 6B 模型训练更久
└── 实践含义:优先增大模型,而非延长训练

推论2: 数据和模型需要同步增长
├── Chinchilla 论文修正了 Kaplan 的结论
├── 最优训练数据量 ≈ 20 × 模型参数量
│   即 7B 模型需要约 140B tokens
├── GPT-3 (175B) 只用了 300B tokens → 训练不足!
│   Chinchilla (70B) 用 1.4T tokens → 超越 GPT-3
└── 实践含义:LLaMA 系列用更多数据训练较小模型

推论3: 计算预算的最优分配
├── 给定计算预算 C(FLOPs),最优参数量:
│   N_opt ≈ (C / 5.4 × 10^13)^(0.5)
├── 最优训练 token 数:
│   D_opt ≈ 20 × N_opt
└── 实践含义:不要盲目增大模型,要平衡模型大小和数据量

实际数据验证

Text Only
模型规模与性能的演进(验证 Scaling Laws):
┌───────────────┬──────────┬────────────┬──────────────┐
│ 模型           │ 参数量    │ 训练数据    │ MMLU (5-shot)│
├───────────────┼──────────┼────────────┼──────────────┤
│ GPT-2         │ 1.5B     │ ~40GB      │ ~30%         │
│ GPT-3         │ 175B     │ 570GB      │ 43.9%        │
│ Chinchilla    │ 70B      │ 1.4T tokens│ 67.5%        │
│ LLaMA-2 70B   │ 70B      │ 2T tokens  │ 68.9%        │
│ LLaMA-3 70B   │ 70B      │ 15T tokens │ 79.0%        │
│ GPT-4         │ ~1.8T?   │ ~13T?      │ 86.4%        │
└───────────────┴──────────┴────────────┴──────────────┘

关键洞察:LLaMA-3 70B 仅用 70B 参数就接近 GPT-4 水平
→ 验证了"更多数据 + 适度模型大小"优于"更大模型 + 较少数据"

💡 Scaling Laws 的实际意义:它为大模型的训练提供了科学指导——在投入巨额计算资源之前,可以用小模型的实验结果预测大模型的性能,从而做出最优的资源分配决策。

9.2 涌现能力( Emergent Abilities )

Python
emergent_abilities = """
涌现能力: 在小模型中不存在, 在大模型中突然出现的能力

┌────────────────────────────────────────────────┐
│           准确率                                │
│    100% │                    ╭──── GPT-4       │
│         │                  ╭─╯                  │
│     50% │              ╭──╯   ← 涌现点         │
│         │          ╭──╯       (能力突然跳跃)    │
│      0% │──────────╯                            │
│         └────────────────────────── 模型规模     │
│         1B    10B    100B   1T                   │
└────────────────────────────────────────────────┘

已观测到的涌现能力:
├── 算术推理 (多位数加法)
├── 思维链 (Chain-of-Thought)
├── 指令遵循 (Instruction Following)
├── 代码生成
├── 多步推理
└── 世界知识问答

注: 涌现能力是否真的存在引发争论(Wei et al. vs Schaeffer et al.)
    有研究认为用不同的评测指标可能不存在"突变"
"""
print(emergent_abilities)

9.3 范式对比

维度 PLM 时代 (2018-2022) LLM 时代 (2022~)
典型模型 BERT-base (110M) GPT-4 (~1T?), LLaMA-70B
使用方式 Pre-train → Fine-tune Pre-train → Instruct-tune → RLHF → Prompt
适配方法 全参数微调 LoRA/QLoRA, In-context Learning
评估方式 固定 benchmark (GLUE/SQuAD) 多维度 (MMLU/HumanEval/GSM8K)
核心能力 特定任务表现 通用能力 + 涌现能力
计算需求 几张 GPU 可微调 预训练需要数千 GPU

10. 代码实战: PLM 三架构对比实验

10.1 实验设计

Python
"""
实验: 在文本分类任务上对比三种架构
数据集: 简单的情感分类 (正面/负面)
目标: 理解不同架构在NLU任务上的差异
"""

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import random
import numpy as np

# 设置随机种子
def set_seed(seed=42):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
set_seed()

# 模拟数据集
class SentimentDataset(Dataset):
    """简单情感分类数据集"""
    # 正面关键词和负面关键词
    POS_WORDS = ["good", "great", "excellent", "wonderful", "amazing", "love", "best", "happy"]
    NEG_WORDS = ["bad", "terrible", "awful", "horrible", "hate", "worst", "sad", "poor"]
    NEUTRAL_WORDS = ["the", "is", "a", "this", "that", "it", "very", "really", "so", "and"]

    def __init__(self, num_samples=500, seq_len=20, vocab_size=100):
        self.data = []
        all_words = self.POS_WORDS + self.NEG_WORDS + self.NEUTRAL_WORDS
        self.word2id = {w: i+1 for i, w in enumerate(all_words)}  # 0 for PAD
        self.vocab_size = max(self.word2id.values()) + 1

        for _ in range(num_samples):
            label = random.choice([0, 1])  # 0=负面, 1=正面
            words = []
            for _ in range(seq_len):
                if random.random() < 0.3:  # 30%概率选情感词
                    if label == 1:
                        words.append(random.choice(self.POS_WORDS))
                    else:
                        words.append(random.choice(self.NEG_WORDS))
                else:
                    words.append(random.choice(self.NEUTRAL_WORDS))
            ids = [self.word2id.get(w, 0) for w in words]
            self.data.append((torch.tensor(ids), label))

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]

dataset = SentimentDataset(num_samples=1000, seq_len=20)
train_set, val_set = torch.utils.data.random_split(dataset, [800, 200])
train_loader = DataLoader(train_set, batch_size=32, shuffle=True)
val_loader = DataLoader(val_set, batch_size=32)

print(f"词表大小: {dataset.vocab_size}")
print(f"训练集: {len(train_set)}, 验证集: {len(val_set)}")

10.2 三种架构定义

Python
class EncoderClassifier(nn.Module):
    """Encoder-only分类器(BERT风格)"""
    def __init__(self, vocab_size, d_model=64, nhead=4, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
        encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4,
                                                    batch_first=True, dropout=0.1)
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
        self.classifier = nn.Linear(d_model, 2)

    def forward(self, x):
        h = self.encoder(self.embedding(x))
        # 平均池化作为句子表示
        h_mean = h.mean(dim=1)
        return self.classifier(h_mean)

class DecoderClassifier(nn.Module):
    """Decoder-only分类器(GPT风格)"""
    def __init__(self, vocab_size, d_model=64, nhead=4, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
        encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4,
                                                    batch_first=True, dropout=0.1)
        self.decoder = nn.TransformerEncoder(encoder_layer, num_layers)
        self.classifier = nn.Linear(d_model, 2)

    def forward(self, x):
        L = x.shape[1]
        causal_mask = torch.triu(torch.ones(L, L, device=x.device), diagonal=1).bool()
        h = self.decoder(self.embedding(x), mask=causal_mask)
        # 取最后一个位置(因为因果注意力,最后位置看到全部信息)
        h_last = h[:, -1, :]
        return self.classifier(h_last)

class EncDecClassifier(nn.Module):
    """Encoder-Decoder分类器(T5风格)"""
    def __init__(self, vocab_size, d_model=64, nhead=4, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
        self.transformer = nn.Transformer(d_model, nhead, num_layers, num_layers,
                                           d_model*4, batch_first=True, dropout=0.1)
        self.classifier = nn.Linear(d_model, 2)
        # 用一个可学习的query token
        self.query = nn.Parameter(torch.randn(1, 1, d_model))

    def forward(self, x):
        B = x.shape[0]
        src = self.embedding(x)
        # 用query token做解码
        tgt = self.query.expand(B, -1, -1)
        h = self.transformer(src, tgt)
        return self.classifier(h.squeeze(1))

10.3 训练与对比

Python
def train_and_evaluate(model, name, train_loader, val_loader, epochs=20, lr=1e-3):
    """训练并评估模型"""
    optimizer = optim.Adam(model.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss()

    history = {"train_loss": [], "val_acc": []}

    for epoch in range(epochs):
        # 训练
        model.train()
        total_loss = 0
        for x, y in train_loader:
            logits = model(x)
            loss = criterion(logits, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_loss += loss.item()

        # 验证
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():  # 禁用梯度计算,节省内存(推理时使用)
            for x, y in val_loader:
                logits = model(x)
                preds = logits.argmax(dim=-1)
                correct += (preds == y).sum().item()
                total += len(y)

        acc = correct / total
        avg_loss = total_loss / len(train_loader)
        history["train_loss"].append(avg_loss)
        history["val_acc"].append(acc)

        if (epoch + 1) % 5 == 0:
            print(f"  [{name}] Epoch {epoch+1}: loss={avg_loss:.4f}, val_acc={acc:.4f}")

    return history

# 运行对比实验
print("=" * 60)
print("三种PLM架构在文本分类任务上的对比实验")
print("=" * 60)

vocab_size = dataset.vocab_size
results = {}

for name, ModelClass in [
    ("Encoder-only (BERT)", EncoderClassifier),
    ("Decoder-only (GPT)", DecoderClassifier),
    ("Encoder-Decoder (T5)", EncDecClassifier)
]:
    print(f"\n--- {name} ---")
    model = ModelClass(vocab_size)
    params = sum(p.numel() for p in model.parameters())
    print(f"  参数量: {params:,}")

    history = train_and_evaluate(model, name, train_loader, val_loader)
    results[name] = history
    print(f"  最终验证准确率: {max(history['val_acc']):.4f}")

print("\n" + "=" * 60)
print("实验结论:")
print("  1. Encoder-only在分类任务上通常效果最好(双向注意力优势)")
print("  2. Decoder-only仅用最后位置的表示,信息利用不够充分")
print("  3. Encoder-Decoder参数更多,但在简单任务上可能过度复杂")
print("  4. 在实际大规模模型中,Decoder-only通过规模弥补了单向局限")

练习题

练习 1 :词向量实验

实现 CBOW ( Continuous Bag of Words )模型,比较和 Skip-Gram 的区别。

💡 参考答案 CBOW 与 Skip-Gram 的核心区别:**CBOW 从上下文预测中心词,Skip-Gram 从中心词预测上下文**。
Python
class CBOW(nn.Module):
    """
    CBOW (Continuous Bag of Words) 模型
    给定上下文词,预测中心词
    """
    def __init__(self, vocab_size, embedding_dim):
        super().__init__()
        self.in_embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.out_embeddings = nn.Embedding(vocab_size, embedding_dim)
        nn.init.xavier_uniform_(self.in_embeddings.weight)
        nn.init.xavier_uniform_(self.out_embeddings.weight)

    def forward(self, context_ids, center_id, negative_ids):
        """
        Args:
            context_ids: [batch_size, window_size*2] 上下文词ID
            center_id: [batch_size] 中心词ID
            negative_ids: [batch_size, num_neg] 负样本ID
        """
        # 上下文嵌入取平均
        context_emb = self.in_embeddings(context_ids).mean(dim=1)  # [B, D]
        center_emb = self.out_embeddings(center_id)                 # [B, D]
        negatives = self.out_embeddings(negative_ids)               # [B, num_neg, D]

        # 正样本得分
        pos_score = torch.sum(context_emb * center_emb, dim=-1)
        pos_loss = -torch.log(torch.sigmoid(pos_score) + 1e-8)

        # 负样本得分
        neg_score = torch.bmm(negatives, context_emb.unsqueeze(-1)).squeeze(-1)
        neg_loss = -torch.log(torch.sigmoid(-neg_score) + 1e-8).sum(dim=-1)

        return (pos_loss + neg_loss).mean()

# CBOW vs Skip-Gram 对比:
# 1. Skip-Gram: 中心词 → 预测上下文,适合低频词(每个中心词生成多个训练样本)
# 2. CBOW: 上下文 → 预测中心词,训练更快(上下文平均后更稳定)
# 3. 实践中 Skip-Gram 在语义任务上通常表现更好

练习 2 :预训练目标实验

分别实现 MLM 和 CLM 预训练目标,在小语料上训练,比较学到的表示质量。

💡 参考答案
Python
class MLMObjective(nn.Module):
    """MLM (Masked Language Model) 预训练目标"""
    def __init__(self, vocab_size, d_model=128, nhead=4, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4, batch_first=True)
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
        self.mlm_head = nn.Linear(d_model, vocab_size)
        self.mask_token_id = vocab_size - 1  # [MASK] token

    def forward(self, input_ids, mask_positions, labels):
        """
        Args:
            input_ids: [B, L] 已替换为[MASK]的输入
            mask_positions: [B, M] 被mask的位置索引
            labels: [B, M] 被mask位置的真实token ID
        """
        h = self.encoder(self.embedding(input_ids))
        # 只在mask位置预测
        masked_h = h.gather(1, mask_positions.unsqueeze(-1).expand(-1, -1, h.size(-1)))
        logits = self.mlm_head(masked_h)  # [B, M, vocab]
        loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))
        return loss

class CLMObjective(nn.Module):
    """CLM (Causal Language Model) 预训练目标"""
    def __init__(self, vocab_size, d_model=128, nhead=4, num_layers=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, d_model*4, batch_first=True)
        self.decoder = nn.TransformerEncoder(encoder_layer, num_layers)
        self.lm_head = nn.Linear(d_model, vocab_size)

    def forward(self, input_ids, labels):
        """
        Args:
            input_ids: [B, L] 输入序列 (x_0, x_1, ..., x_{L-1})
            labels: [B, L] 目标序列 (x_1, x_2, ..., x_L)
        """
        L = input_ids.size(1)
        causal_mask = torch.triu(torch.ones(L, L, device=input_ids.device), diagonal=1).bool()
        h = self.decoder(self.embedding(input_ids), mask=causal_mask)
        logits = self.lm_head(h)  # [B, L, vocab]
        loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))
        return loss

# MLM vs CLM 对比:
# 1. MLM: 利用双向上下文,适合理解任务(分类、NER)
# 2. CLM: 只看左侧上下文,适合生成任务(文本续写、对话)
# 3. MLM 只在15%的token上计算损失,CLM在每个token上都计算损失
# 4. 在下游分类任务上,MLM通常优于CLM;在生成任务上,CLM更自然

练习 3 :架构分析

  1. 为什么 BERT 不适合文本生成任务?从注意力 mask 的角度解释。
  2. 如果要让 Decoder-only 做好分类任务,有哪些策略?(提示: pool 策略、特殊 token )
  3. Encoder-Decoder 架构在什么场景下仍然优于 Decoder-only ?
💡 参考答案 **问题 1:BERT 不适合文本生成** BERT 使用双向注意力(无因果 mask),每个位置可以看到所有其他位置(包括未来位置)。文本生成要求自回归:位置 $i$ 的生成只能依赖位置 $0, 1, \ldots, i-1$。如果用 BERT 做生成,模型在生成第 $i$ 个 token 时会"偷看"到第 $i+1, i+2, \ldots$ 的信息,这在推理时是不可用的(因为这些 token 还没生成)。虽然可以通过 iterative masking 等技巧绕过,但效率极低且效果不如 Decoder-only。 **问题 2:Decoder-only 做分类的策略** - **最后位置池化**:取序列最后一个 token 的隐状态做分类(因因果注意力,最后位置看到了全部信息) - **特殊 [CLS] token**:在序列开头添加可学习的分类 token,取其输出做分类 - **平均池化**:对所有位置的输出取平均作为句子表示 - **P-tuning / Prefix-tuning**:在输入前添加可学习的连续 prompt,用 prompt 的输出做分类 **问题 3:Encoder-Decoder 仍然优势的场景** - **机器翻译**:源语言和目标语言是两种不同的语言,Encoder-Decoder 天然分离编码和解码过程 - **长文本摘要**:编码器处理完整长文(双向注意力理解更全面),解码器生成摘要 - **代码生成/修改**:需要先理解完整输入代码,再生成修改后的代码 - **语音识别(ASR)**:编码器处理音频特征,解码器生成文本

练习 4 :论文阅读

阅读以下论文,用自己的话总结关键贡献: 1. Word2Vec 原始论文:"Efficient Estimation of Word Representations in Vector Space" (Mikolov et al., 2013) 2. BERT 论文:"BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding" (Devlin et al., 2019) 3. GPT-3 论文:"Language Models are Few-Shot Learners" (Brown et al., 2020)

💡 参考答案(关键贡献摘要) **Word2Vec (Mikolov et al., 2013)** - 提出 Skip-Gram 和 CBOW 两种高效词向量训练模型 - 引入 Negative Sampling 和 Hierarchical Softmax 加速训练 - 证明了分布式词向量能捕捉语义关系(如 `king - man + woman ≈ queen`) - 将词向量训练从数周缩短到数小时,使大规模词向量成为可能 **BERT (Devlin et al., 2019)** - 提出 MLM(Masked Language Model)预训练目标,实现真正的双向编码 - 引入 NSP(Next Sentence Prediction)任务,增强句子级理解 - 证明预训练-微调范式在 11 项 NLP 任务上全面超越之前的方法 - 统一了 NLP 下游任务的接口:只需在 BERT 上加一个简单分类头即可适配 **GPT-3 (Brown et al., 2020)** - 1750 亿参数,验证了规模法则(Scaling Laws)的威力 - 展示了 In-context Learning(上下文学习):无需更新参数即可适配新任务 - 在 Few-shot 设置下,GPT-3 在翻译、问答、摘要等任务上接近微调模型 - 开创了大模型"预训练 → 提示工程"的新范式,取代了"预训练 → 微调"

📝 本章小结

知识点 掌握程度检查
NLP 四个发展阶段 能否画出时间线并说出每阶段代表方法?
文本表示演进 能否解释 One-hot→Word2Vec→ELMo→BERT 各解决什么问题?
预训练-微调范式 能否解释为什么 Pre-train & Fine-tune 优于从头训练?
三种 PLM 架构 能否画出 BERT/GPT/T5 的架构图并说明差异?
MLM vs CLM vs DAE 能否解释三种预训练目标的工作方式和适用场景?
Decoder-only 主流原因 能否说出至少 3 个 Decoder-only 成为 LLM 主流的原因?
涌现能力 能否解释什么是涌现能力并举例?

🔗 后续学习路径

📚 参考资料

  1. Mikolov et al. "Efficient Estimation of Word Representations in Vector Space" (2013) — Word2Vec
  2. Pennington et al. "GloVe: Global Vectors for Word Representation" (2014) — GloVe
  3. Peters et al. "Deep contextualized word representations" (2018) — ELMo
  4. Devlin et al. "BERT: Pre-training of Deep Bidirectional Transformers" (2019) — BERT
  5. Radford et al. "Language Models are Unsupervised Multitask Learners" (2019) — GPT-2
  6. Brown et al. "Language Models are Few-Shot Learners" (2020) — GPT-3
  7. Raffel et al. "Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer" (2020) — T5
  8. Lewis et al. "BART: Denoising Sequence-to-Sequence Pre-training" (2020) — BART
  9. Kaplan et al. "Scaling Laws for Neural Language Models" (2020) — Scaling Laws
  10. Wei et al. "Emergent Abilities of Large Language Models" (2022) — Emergent Abilities

最后更新日期: 2026-03-26 适用版本: LLM 学习教程 v2026