查看全课大纲(第 25 / 40 节)
  1. 1.从规则到学习:深度学习解决什么问题
  2. 2.表示学习与端到端训练
  3. 3.张量、计算图与自动微分
  4. 4.训练三要素:数据、模型、目标函数
  5. 5.从感知机到多层感知机
  6. 6.反向传播:手推一个两层网络
  7. 7.激活函数与初始化
  8. 8.优化器:SGD → Momentum → Adam
  9. 9.正则化:权重衰减、Dropout、BN/LN
  10. 10.卷积的直觉与参数共享
  11. 11.池化、感受野与下采样
  12. 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
  13. 13.残差连接与归一化为什么有效
  14. 14.迁移学习与特征可视化
  15. 15.RNN 与 BPTT
  16. 16.LSTM/GRU:门控如何缓解梯度问题
  17. 17.Seq2Seq 与编码器-解码器
  18. 18.注意力前史:Bahdanau 加性注意力
  19. 19.Self-Attention:QKV 与缩放点积
  20. 20.多头注意力:并行的多种关系
  21. 21.位置编码:正弦与可学习
  22. 22.残差、LayerNorm 与前馈层
  23. 23.从零实现一个最小 Transformer block
  24. 24.训练稳定性、复杂度与常见误区
  25. 25.自监督与语言建模目标
  26. 26.BERT:掩码语言模型与双向编码
  27. 27.GPT:自回归生成与缩放定律
  28. 28.指令微调与对齐:SFT → RLHF/DPO
  29. 29.参数高效微调:LoRA 与适配器
  30. 30.自编码器与 VAE
  31. 31.GAN:对抗训练与模式崩塌
  32. 32.扩散模型:前向加噪与反向去噪
  33. 33.扩散模型的应用与采样加速
  34. 34.对比学习与 CLIP
  35. 35.ViT:把图像当序列
  36. 36.多模态架构:对齐、融合与生成
  37. 37.能力边界、幻觉与评测
  38. 38.PyTorch 2.x 训练循环与混合精度
  39. 39.实验管理、评测与复现
  40. 40.推理部署与成本:量化/蒸馏概览

自监督与语言建模目标

自监督与语言建模目标:填空与续写

小象实战讲义 · 深度学习基础:从神经网络到 Transformer

上一章我们把 Transformer 的零件拼齐了。这一章换个问法:没有人工标注的数据,模型靠什么学?答案是自监督——从文本本身造出监督信号。本节先看两种最基本的语言建模目标:GPT 的“续写”(因果语言建模)和 BERT 的“填空”(掩码语言建模),理解它们如何决定模型能看见多少上下文、又适合什么任务。

先修:因果掩码(第 5 章 5.6 节)、编码器与解码器的结构差异(5.5 节)、交叉熵损失与 softmax。后续:下一节深入 BERT 的掩码语言模型与双向编码,再下一节讲 GPT 的自回归路线与缩放定律。

💡 核心导读

  1. 自监督学习:无需人工标注,从数据自身构造监督信号(如预测被掩盖的词),是预训练大模型的核心范式。
  2. 语言建模目标:核心任务是预测文本序列的概率。根据预测方式的不同,主要分为因果语言建模(CLM,如 GPT)和掩码语言建模(MLM,如 BERT)。
  3. CLM 与单向性:模型只能根据上文(左侧)信息预测下一个词,天然适用于文本生成任务,但无法同时利用上下文信息。
  4. MLM 与双向性:模型随机掩盖输入中的部分词,并利用所有未被掩盖的词(左右上下文)来预测被掩盖的词,能学习更丰富的上下文表征。
  5. 目标决定架构:CLM 目标要求使用 Transformer 解码器(带因果注意力掩码),而 MLM 目标允许使用 Transformer 编码器(全连接注意力)。

🧭 前置知识

  1. Transformer 编码器与解码器:了解编码器的全连接自注意力机制,以及解码器的因果注意力掩码(防止看到未来信息)。
  2. Softmax 与交叉熵损失:理解如何将模型输出的 logits 转换为概率分布,并计算预测与真实标签之间的损失。
  3. 基本的 PyTorch 张量操作:包括索引、切片、掩码(mask)操作。
  4. 条件概率:P(wt∣w1,w2,…,wt−1)P(w_t | w_1, w_2, …, w_{t-1}) 表示给定前 t−1t-1 个词,第 tt 个词出现的概率。不展开,见站内数学基础课。

从监督学习到自监督学习

在传统的监督学习中,我们需要为每个训练样本提供明确的标签(如图像分类中的“猫”“狗”)。对于海量的文本数据,这种人工标注成本极高且不现实。

自监督学习巧妙地解决了这个问题。其核心思想是:从数据本身自动生成监督信号。对于文本数据,最自然的方式就是利用词与词之间的内在关联。例如,给定一句话“今天天气很[]”,我们很容易猜到被掩盖的词可能是“好”。通过让模型学习完成这类任务,我们就能让它在无标注数据上获得对语言的理解。

语言建模(Language Modeling, LM)是自监督学习在自然语言处理中最成功的体现。它的目标是学习一个模型,使其能够为任何给定的文本序列分配一个概率 P(w1,w2,…,wT)P(w_1, w_2, …, w_T),或者预测序列中某个位置缺失的词。

因果语言建模:像人类一样“续写”

因果语言建模(Causal Language Modeling, CLM),也称为自回归语言建模或标准语言建模。其目标是预测序列中下一个词的概率,公式为: P(wt∣w1,w2,…,wt−1)P(w_t | w_1, w_2, …, w_{t-1}) 模型在预测位置 tt 的词时,只能看到它之前的所有词 w1w_1 到 wt−1w_{t-1},而不能看到 tt 及之后的词。这模仿了人类阅读或书写时“从左到右”的过程。

实现关键:因果注意力掩码 在 Transformer 架构中,CLM 目标通过解码器来实现。解码器的自注意力层用一张掩码把“未来位置”标出来(被屏蔽的区域是上三角,因为行是查询位置 ii、列是键位置 jj,j>ij > i 就是未来)。对于序列长度为 nn,该掩码矩阵 MM 满足:

  • Mij=0M_{ij} = 0 (False) 如果 i≥ji \ge j (允许看当前位置及过去位置)
  • Mij=−∞M_{ij} = -\infty (True) 如果 i<ji < j (禁止看未来位置)

这样,每个位置在计算注意力时,都无法“看到”其右侧(未来)的信息。

让我们通过代码直观感受 CLM 的过程和因果掩码的作用。

import torch
import torch.nn.functional as F

torch.manual_seed(0)  # 固定随机种子,确保结果可复现

# 1. 模拟输入:batch_size=2, seq_len=8, vocab_size=10000
batch_size, seq_len = 2, 8
vocab_size = 10000
# 随机生成 token ids
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len))
print("输入 token ids (形状: {}):".format(input_ids.shape))
for row in input_ids.tolist():
    print("  " + " ".join(f"{v:5d}" for v in row))
print()

# 2. 模拟一个简单的语言模型输出 logits
# 假设模型 hidden_dim = 64
hidden_dim = 64
# 随机初始化一个“投影层”,将隐藏状态映射到词表空间
# 除以 sqrt(hidden_dim) 让 logits 的量级落在 1 附近,接近“完全没学会”的状态
projection = torch.randn(hidden_dim, vocab_size) / hidden_dim ** 0.5
# 随机生成最后一个隐藏层的输出 (模拟模型前向传播结果)
hidden_states = torch.randn(batch_size, seq_len, hidden_dim)

# 计算所有位置的 logits
logits = torch.matmul(hidden_states, projection)  # 形状: (2, 8, 10000)

# 3. CLM 目标:用前 t-1 个词预测第 t 个词
# 我们将输入向右移动一位作为标签(即预测下一个词)
shifted_labels = input_ids[:, 1:]  # 取第 1 到末尾的词作为目标
# 对应的 logits 是前 seq_len-1 个位置输出的
shifted_logits = logits[:, :-1, :]  # 形状: (2, 7, 10000)

# 4. 计算交叉熵损失
# 将 logits 和 labels  reshape 成适合交叉熵损失的形式
loss = F.cross_entropy(
    shifted_logits.reshape(-1, vocab_size),  # (batch * (seq_len-1), vocab)
    shifted_labels.reshape(-1)               # (batch * (seq_len-1),)
)
print("CLM 损失值 (近似): {:.4f}".format(loss.item()))
print()

# 5. 展示因果注意力掩码
def generate_causal_mask(seq_len):
    """生成掩码:上三角(不含对角线)为 True,表示这些未来位置要被屏蔽。"""
    # 先造全 1 矩阵,再用 triu 只保留上三角(diagonal=1 表示不含对角线)
    mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
    return mask

causal_mask = generate_causal_mask(seq_len)
print("因果注意力掩码 (1 表示被遮盖,即不能看未来):")
for row in causal_mask.tolist():
    print("  " + " ".join("1" if v else "0" for v in row))
输入 token ids (形状: torch.Size([2, 8])):
   6044  8239  4933  3760  8963  8379  5427  8503
   3497  5683  4101  6866  2756  1399  5878   376

CLM 损失值 (近似): 9.6401

因果注意力掩码 (1 表示被遮盖,即不能看未来):
  0 1 1 1 1 1 1 1
  0 0 1 1 1 1 1 1
  0 0 0 1 1 1 1 1
  0 0 0 0 1 1 1 1
  0 0 0 0 0 1 1 1
  0 0 0 0 0 0 1 1
  0 0 0 0 0 0 0 1
  0 0 0 0 0 0 0 0

CLM 的特点与局限

  • 优点:训练目标与文本生成任务完全一致,模型学到的“续写”能力可直接用于对话、创作等生成式任务。
  • 局限:单向上下文。在预测时,每个词只能基于其左侧的历史信息,无法同时利用右侧的上下文。这对于需要理解完整句意的任务(如句子分类、实体识别)是不利的。

顺便看一眼那个损失值:模型完全没训练时,14 个位置上的平均交叉熵应该接近均匀猜测的 ln⁡(10000)≈9.21\ln(10000) \approx 9.21,实跑 9.64 说明 logits 尺度基本正常。如果这个数远大于 9.21(比如二三十),多半是投影层忘了按 1/d1/\sqrt{d} 缩放,softmax 已经饱和。

以 GPT 系列模型为代表的模型均采用 CLM 目标进行预训练(Language Models are Few-Shot Learners, Brown et al., 2020, arXiv:2005.14165)。

掩码语言建模:像完形填空一样“理解”

为了克服 CLM 的单向性限制,BERT 采用了掩码语言建模(Masked Language Modeling, MLM)目标(BERT: Pre-training of Deep Bidirectional Transformers, Devlin et al., 2018, arXiv:1810.04805)。

MLM 的核心操作是:

  1. 随机选择输入序列中一定比例(如 15%)的 Token。
  2. 将这些被选中的 Token:
    • 80% 的概率替换为特殊的 [MASK] 符号。
    • 10% 的概率替换为词表中的随机一个词。
    • 10% 的概率保持不变。
  3. 让模型根据所有未被掩盖(或未被随机替换)的上下文,来预测被选中位置的原始词。

为什么要有 10% 随机词和 10% 不变? 这是为了缓解预训练与微调之间的不匹配。在微调阶段,输入中不会出现 [MASK] 符号。如果模型只在 [MASK] 位置上学习预测,那么面对真实的、没有掩码的句子时,其表征能力可能会下降。加入随机词和不变词,迫使模型不仅学习“填空”,还要学会“纠错”和“确认”,从而获得更鲁棒的表征。

实现关键:双向注意力 MLM 目标允许模型在预测被掩盖的词时,同时利用其左侧和右侧的所有上下文信息。因此,它可以使用 Transformer 编码器,其自注意力机制是全连接的,没有因果限制。

下面我们模拟 BERT 论文中描述的 MLM 数据准备过程。

import torch
import torch.nn.functional as F

torch.manual_seed(0)  # 固定随机种子

# 1. 模拟输入
batch_size, seq_len = 4, 8
vocab_size = 10000
special_token_id = {
    '[PAD]': 0,
    '[UNK]': 1,
    '[CLS]': 2,
    '[SEP]': 3,
    '[MASK]': 4,  # 假设 [MASK] 的 id 是 4
}
mask_id = special_token_id['[MASK]']

def show_int(mat, width=5):
    for row in mat:
        print("  " + " ".join(f"{v:{width}d}" for v in row))

def show_bool(mat):
    for row in mat:
        print("  " + " ".join("1" if v else "0" for v in row))

# 随机生成原始输入 token ids (假设已经去除了特殊符号)
original_ids = torch.randint(5, vocab_size, (batch_size, seq_len))  # 从5开始,避免特殊token
print("原始输入 token ids:")
show_int(original_ids.tolist())

# 2. MLM 预处理:选择掩盖位置
mask_prob = 0.15
# 生成一个与输入形状相同的随机矩阵
random_matrix = torch.rand(original_ids.shape)
# 候选掩盖位置:随机数小于 mask_prob 的位置
candidate_mask_positions = random_matrix < mask_prob
print("\n候选掩盖位置 (1 表示被选中):")
show_bool(candidate_mask_positions.tolist())

# 3. 对每个候选位置,决定是替换为 [MASK]、随机词还是保持不变
# 生成三个独立的随机矩阵来决定命运
fate_random = torch.rand(original_ids.shape)
input_ids = original_ids.clone()  # 创建输入副本
labels = torch.full_like(original_ids, -100)  # 用 -100 初始化标签,-100在交叉熵损失中会被忽略

# 统计
mask_replace_count = 0
random_replace_count = 0
keep_count = 0

for i in range(batch_size):
    for j in range(seq_len):
        if candidate_mask_positions[i, j]:
            fate = fate_random[i, j]
            if fate < 0.8:
                # 80% -> [MASK]
                input_ids[i, j] = mask_id
                mask_replace_count += 1
            elif fate < 0.9:
                # 10% -> 随机词
                input_ids[i, j] = torch.randint(5, vocab_size, (1,)).item()
                random_replace_count += 1
            else:
                # 10% -> 保持不变
                keep_count += 1
            # 无论哪种命运,标签都是原始词
            labels[i, j] = original_ids[i, j]

print("\n最终输入 token ids:")
show_int(input_ids.tolist())
print("\nMLM 标签(-100 表示该位置不参与损失):")
show_int(labels.tolist())

# 4. 统计信息
total_selected = candidate_mask_positions.sum().item()
print(f"\n统计: 共选中 {total_selected} 个候选token。")
if total_selected > 0:
    print(f"  [MASK] 替换: {mask_replace_count} ({100*mask_replace_count/total_selected:.1f}%)")
    print(f"  随机词替换: {random_replace_count} ({100*random_replace_count/total_selected:.1f}%)")
    print(f"  保持不变: {keep_count} ({100*keep_count/total_selected:.1f}%)")
原始输入 token ids:
   5204  2004  1563  7715  4038  1729  6142  8873
   3747  5448  1846  2736  2941   674  1908  2231
    546  5688  2644  2328  4196  3114  2756  7514
   3304  1766  5969  7334   179   400  4686  9647

候选掩盖位置 (1 表示被选中):
  0 0 0 1 0 0 0 0
  0 0 0 1 1 0 0 0
  0 0 0 0 0 0 0 0
  0 1 0 0 0 0 0 0

最终输入 token ids:
   5204  2004  1563     4  4038  1729  6142  8873
   3747  5448  1846  2736     4   674  1908  2231
    546  5688  2644  2328  4196  3114  2756  7514
   3304     4  5969  7334   179   400  4686  9647

MLM 标签(-100 表示该位置不参与损失):
   -100  -100  -100  7715  -100  -100  -100  -100
   -100  -100  -100  2736  2941  -100  -100  -100
   -100  -100  -100  -100  -100  -100  -100  -100
   -100  1766  -100  -100  -100  -100  -100  -100

统计: 共选中 4 个候选token。
  [MASK] 替换: 3 (75.0%)
  随机词替换: 0 (0.0%)
  保持不变: 1 (25.0%)

只有 4 个被选位置,比例自然偏离 8:1:1(这里的 75% / 0% / 25% 是随机抽样的正常波动);样本量大了才会收敛到 80/10/10。真正要盯的是下一行那个 labels:非 -100 的位置恰好就是被选中的那 4 个,且值都是原始词——即使它被换成了 [MASK]。

MLM 的特点与局限

  • 优点:双向上下文。模型能同时利用左右两侧的信息来学习每个词的深层表征,这对于理解类任务(如文本分类、问答)非常有利。
  • 局限:训练与生成的不一致。MLM 是“填空”任务,而文本生成是“续写”任务。因此,纯 MLM 预训练的模型(如 BERT)在直接进行开放域文本生成时比较困难,通常需要额外的解码器或进行特定调整。

CLM vs MLM:目标如何决定模型能力

让我们通过一个简单的对比实验,直观感受两种目标下模型“注意力范围”的差异。我们将模拟一个极简的“模型”,它不进行实际训练,只是展示在两种不同掩码下,每个位置能“看到”的上下文范围。

import torch

torch.manual_seed(0)

seq_len = 6
print(f"序列长度: {seq_len}")
print("-" * 40)

# 1. 因果掩码 (CLM)
causal_mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
print("因果掩码 (CLM) - 用于生成模型:")
print("(行: 当前查询位置, 列: 可访问的键位置, False=可访问)")
print(causal_mask)
print()
print("CLM 下,位置 i 能看到的上下文: [0, i]")
for i in range(seq_len):
    visible = [j for j in range(seq_len) if not causal_mask[i, j]]
    print(f"  位置 {i}: {visible}")
print()

# 2. 全连接掩码 (MLM) - 实际上就是没有掩码(除了padding)
full_mask = torch.zeros(seq_len, seq_len).bool()
print("全连接掩码 (MLM) - 用于理解模型:")
print("(理论上所有位置都可互相访问)")
print(full_mask)
print()
print("MLM 下,位置 i 能看到的上下文: [0, 1, ..., seq_len-1] (全部)")
for i in range(seq_len):
    visible = [j for j in range(seq_len) if not full_mask[i, j]]
    print(f"  位置 {i}: {visible}")
序列长度: 6
----------------------------------------
因果掩码 (CLM) - 用于生成模型:
(行: 当前查询位置, 列: 可访问的键位置, False=可访问)
tensor([[False,  True,  True,  True,  True,  True],
        [False, False,  True,  True,  True,  True],
        [False, False, False,  True,  True,  True],
        [False, False, False, False,  True,  True],
        [False, False, False, False, False,  True],
        [False, False, False, False, False, False]])

CLM 下,位置 i 能看到的上下文: [0, i]
  位置 0: [0]
  位置 1: [0, 1]
  位置 2: [0, 1, 2]
  位置 3: [0, 1, 2, 3]
  位置 4: [0, 1, 2, 3, 4]
  位置 5: [0, 1, 2, 3, 4, 5]

全连接掩码 (MLM) - 用于理解模型:
(理论上所有位置都可互相访问)
tensor([[False, False, False, False, False, False],
        [False, False, False, False, False, False],
        [False, False, False, False, False, False],
        [False, False, False, False, False, False],
        [False, False, False, False, False, False],
        [False, False, False, False, False, False]])

MLM 下,位置 i 能看到的上下文: [0, 1, ..., seq_len-1] (全部)
  位置 0: [0, 1, 2, 3, 4, 5]
  位置 1: [0, 1, 2, 3, 4, 5]
  位置 2: [0, 1, 2, 3, 4, 5]
  位置 3: [0, 1, 2, 3, 4, 5]
  位置 4: [0, 1, 2, 3, 4, 5]
  位置 5: [0, 1, 2, 3, 4, 5]

这个对比清晰地展示了根本差异:

  • CLM(GPT 路线):信息流是严格单向的,像一个不断展开的卷轴。这完美匹配生成任务,但理解是局部的。
  • MLM(BERT 路线):信息流是全向的,每个词都能瞬间“感知”整个句子。这极大地增强了理解能力,但破坏了自回归生成的结构。

📝 动手练一练

假设我们有一个长度为 5 的序列 [A, B, C, D, E],掩码概率为 20%。按照 BERT 的 MLM 策略(80% MASK, 10% 随机, 10% 不变),如果随机选中了位置 1 (B) 和位置 4 (E) 进行掩盖,且随机决定 B 被替换为 [MASK],E 被替换为随机词 X。

  1. 模型的输入序列会变成什么?
  2. 计算损失时,哪些位置需要被计算?
👉 点击查看参考答案
  1. 输入序列:[A, [MASK], C, D, X]

    • 位置 1 (原 B): 80% 概率 → 替换为 [MASK]。
    • 位置 4 (原 E): 10% 概率 → 替换为随机词 X。
    • 其他位置保持不变。
  2. 损失计算位置:只有被选中的位置(即位置 1 和位置 4)需要计算损失,无论它们最终被替换成什么。

    • 位置 1 的标签是原词 B。
    • 位置 4 的标签是原词 E。
    • 位置 0, 2, 3 的标签在损失计算中应被忽略(例如设为 -100)。

自查:拿本节第二个代码块的 labels 对照一下——非 -100 的位置,正好就是被选中的那几个。

常见错误

错误理解或写法现象或报错原因正确做法
在 MLM 计算损失时,对所有位置(包括未掩盖位置)计算交叉熵。损失值异常高或训练不稳定,模型难以收敛。未掩盖位置的标签是原始词,如果也计算损失,相当于让模型去“预测”一个它已经看到且无需改变的词,这与 MLM 任务定义矛盾,引入了大量噪声。使用 torch.nn.CrossEntropyLoss 的 ignore_index 参数(通常设为 -100),或在计算损失前,只选取被掩盖位置对应的 logits 和 labels。
在实现 CLM 时,忘记将输入序列进行偏移(shift)来构造标签。模型学习的是“根据当前词预测当前词”,导致损失极低但模型没有任何预测能力。混淆了输入和预测目标。CLM 是用历史预测未来,所以第 t 个位置的输出,对应的是第 t+1 个位置的标签。确保 labels = input_ids[:, 1:],而用于计算损失的 logits = model_output[:, :-1, :]。
在生成因果掩码时,错误地使用了 torch.tril(下三角)而不是 torch.triu(上三角)。模型可以看到未来信息,破坏了自回归性质,在生成任务中会导致信息泄漏。torch.triu(上三角)返回主对角线及以上元素为 1,我们通常需要将其 bool() 后作为掩码,这样上三角(i < j)为 True(被掩盖)。torch.tril 则相反。明确定义:causal_mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()。diagonal=1 表示从主对角线之上一条对角线开始,这样主对角线(i=j)是 False(可见)。

本章小结

  1. 自监督学习是预训练的核心,通过从数据自身构造目标(如预测被掩盖的词)来利用海量无标注数据。
  2. 因果语言建模(CLM)训练模型根据上文预测下一个词,对应 Transformer 解码器和因果注意力掩码,擅长生成任务,但上下文是单向的。
  3. 掩码语言建模(MLM)训练模型利用双向上下文预测被随机掩盖的词,对应 Transformer 编码器和全连接注意力,擅长理解任务,但直接生成能力弱。
  4. MLM 的掩盖策略(80% MASK, 10% 随机, 10% 不变)是为了缓解预训练-微调差异,让模型表征更鲁棒。
  5. 训练目标决定了模型的基础能力:CLM 塑造了生成器,MLM 塑造了理解器。这是后续 BERT 与 GPT 系列模型分道扬镳的起点。

📋 行动清单

  • 运行本节 CLM 代码块,将序列长度从 8 改为 4,观察因果掩码矩阵的变化,理解每个位置能访问的历史范围。
  • 修改 MLM 代码块中的 mask_prob 为 0.3,重新运行,观察被选中掩盖的 token 数量变化,并思考过高或过低的掩盖率可能带来什么问题。
  • 尝试写几行代码,模拟一个包含 [CLS] 和 [SEP] 的输入序列,然后对其内容部分(而非特殊符号)应用 MLM 掩盖策略。

延伸阅读

  1. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding (Devlin et al., 2018, arXiv:1810.04805) —— 提出 MLM 和 NSP 预训练目标。
  2. Language Models are Few-Shot Learners (Brown et al., 2020, arXiv:2005.14165) —— GPT-3 论文,大规模 CLM 预训练的典范。

—— 小象教研组