说明:本文主要基于李宏毅老师《李宏毅深度学习教程》第10章”自监督学习”整理而成,重点讲解BERT和GPT两大自监督学习方法。文中对比学习、视觉自监督学习等内容为作者根据领域知识补充,非原教程内容。
引言:数据标注的困境
在深度学习时代,我们面临一个矛盾:模型越来越强大,但对标注数据的需求也越来越大。
标注数据的代价
现实挑战:
- 成本高昂:ImageNet 数据集标注耗费数百万美元
- 时间漫长:医学影像标注需要专业医生,周期长达数月
- 规模受限:人工标注速度远远跟不上数据产生速度
一个启发性的事实:
- 人类婴儿在 2 岁前就能认识数千种物体
- 但他们并没有接受”这是猫,那是狗”的监督训练
- 他们通过观察世界本身来学习
核心问题:能否让 AI 像人类一样,从未标注的数据中自主学习?
一、什么是自监督学习?
定义与核心思想
自监督学习(Self-Supervised Learning, SSL):从数据本身构造监督信号,无需人工标注。
核心思想:
传统监督学习:(数据, 人工标签) → 模型学习自监督学习: (数据, 数据本身) → 模型学习关键洞察:数据本身就包含丰富的结构和模式,这些模式可以作为”免费的监督信号”。
本质理解:自监督学习是无监督学习的一种特殊实现方式,它通过设计”预文本任务”(Pretext Task)从数据中构造伪标签,将无监督问题转化为监督学习问题。
数学形式化
从数学角度看,自监督学习可以形式化为:
给定:无标注数据集
目标:学习一个表征函数 ,将输入空间 映射到表征空间
方法:设计预文本任务 ,从数据 自动构造输入-标签对 :
训练:最小化预文本任务的损失:
参数说明:
- :模型参数
- :从原始数据 变换得到的输入(如遮蔽部分词的句子)
- :自动生成的伪标签(如被遮蔽的词)
- :损失函数(如交叉熵)
预文本任务与下游任务
自监督学习遵循两阶段范式:
阶段一:预训练(Pre-training)
- 任务:预文本任务 (如掩码预测、对比学习)
- 数据:大规模无标注数据
- 目标:学习通用表征
- 特点:不需要人工标注,可以利用海量数据
阶段二:下游任务(Downstream Task)
- 任务:实际应用任务 (如情感分类、问答)
- 数据:少量标注数据
- 目标:利用预训练表征解决特定问题
- 方法:微调(Fine-tuning)或线性探针(Linear Probing)
关键洞察:
- 预文本任务的设计决定了学到的表征质量
- 好的预文本任务能让模型学到可迁移的通用知识
- 预文本任务不需要与下游任务完全一致,但应该能捕捉数据的本质结构
示例:
预文本任务:预测句子中被遮蔽的词→ 模型学会理解语言的语法和语义
下游任务:情感分类→ 利用预训练的语言理解能力,只需少量标注数据即可完成自监督学习与无监督学习的关系
相同点:
- 都不需要人工标注的数据
- 都从数据本身学习
不同点:
-
无监督学习:直接学习数据的分布或结构(如聚类、降维)
- 目标: 或数据的低维表示
- 方法:K-means、PCA、自编码器等
- 特点:没有明确的”任务”概念
-
自监督学习:通过构造监督任务来学习表征
- 目标:,其中 是自动生成的伪标签
- 方法:掩码预测、对比学习等
- 特点:有明确的”预文本任务”,训练过程类似监督学习
本质:自监督学习是无监督学习的一种实现策略,它巧妙地将无监督问题转化为监督学习问题,从而可以利用监督学习的强大优化方法。
自监督 vs 无监督 vs 监督学习
| 学习范式 | 数据需求 | 监督信号来源 | 典型任务 |
|---|---|---|---|
| 监督学习 | 大量标注数据 | 人工标注 | 分类、检测 |
| 无监督学习 | 无标注数据 | 无监督信号 | 聚类、降维 |
| 自监督学习 | 无标注数据 | 数据本身 | 表征学习 |
自监督学习的独特之处:
- 不需要人工标注(像无监督学习)
- 但有明确的学习目标(像监督学习)
- 目标是学习通用表征,可迁移到多个下游任务
二、自监督学习的核心方法
说明:李宏毅教程第10章主要聚焦于生成式自监督学习,以BERT(掩码语言模型)和GPT(自回归语言模型)为核心案例。本节对比学习的内容为作者补充,非教程原文。
自监督学习有多种实现方式,主要可以分为:
生成式/预测式方法(教程重点)
核心思想:通过预测数据的某些部分来学习表征。 典型任务:
- 掩码预测:遮住部分内容,让模型预测(BERT)
- 自回归:根据前文预测下一个词(GPT)
- 去噪:从噪声数据中恢复原始数据
直观理解:
输入:我爱[MASK]学习目标:预测 [MASK] = "深度"关键洞察:要预测被遮住的内容,模型必须理解上下文语义。
对比学习方法(补充内容)
核心思想:让相似的样本在表征空间中靠近,不相似的样本远离。
直观理解:
正样本对:同一张图片的不同增强版本 → 拉近负样本对:不同图片 → 推远关键组件:
- 数据增强:创造正样本对
- 编码器:提取特征表征
- 对比损失:拉近正样本,推远负样本
三、BERT:掩码语言模型(教程核心内容)
BERT(Bidirectional Encoder Representations from Transformers)是自监督学习在NLP领域的里程碑式工作。
3.1 BERT的预训练任务
BERT使用两个自监督任务进行预训练:
任务一:掩码语言建模(Masked Language Modeling, MLM)
核心思想:随机遮住部分词,让模型预测被遮住的内容。
具体做法:

-
随机遮蔽:随机选择 15% 的词进行处理
- 80% 替换为 [MASK] 标记
- 10% 替换为随机词
- 10% 保持不变
-
双向编码:利用左右两侧的上下文预测
输入:我 [MASK] 深度学习上下文:左边"我" + 右边"深度学习"预测:[MASK] = "爱" -
训练目标:
参数说明:
- :被遮蔽位置的原始词
- :双向上下文(未被遮蔽的所有词)
- :模型预测的词表概率分布
MLM的输入输出详解:
输入格式:
# 原始文本text = "我爱深度学习"
# 分词后的token IDsinput_ids = [101, 2769, 4263, 3995, 2428, 2110, 739, 102]# 对应:[CLS] 我 爱 深度 学习 [SEP]
# 随机遮蔽15%的词(假设遮蔽"爱")masked_input_ids = [101, 2769, 103, 3995, 2428, 2110, 739, 102]# 对应:[CLS] 我 [MASK] 深度 学习 [SEP]# 103是[MASK]的token ID
# 输入张量形状input_ids: [batch_size, seq_length] # 例如 [32, 128]attention_mask: [batch_size, seq_length] # 标记哪些位置是真实tokentoken_type_ids: [batch_size, seq_length] # 区分句子A和句子BBERT处理过程:
# BERT编码hidden_states = BERT(input_ids, attention_mask, token_type_ids)# 输出形状:[batch_size, seq_length, hidden_size]# 例如:[32, 128, 768] (BERT-base)
# 只取被遮蔽位置的输出masked_positions = [2] # 第2个位置被遮蔽("爱")masked_hidden = hidden_states[:, masked_positions, :]# 形状:[batch_size, num_masked, hidden_size]
# 预测词表分布logits = MLM_head(masked_hidden)# 形状:[batch_size, num_masked, vocab_size]# 例如:[32, 1, 21128] (中文BERT词表大小)
# 应用softmax得到概率分布probs = softmax(logits, dim=-1)predicted_token_id = argmax(probs, dim=-1) # 预测的token ID输出格式:
# 模型输出outputs = { 'loss': 标量, # MLM损失 'logits': [batch_size, seq_length, vocab_size], # 每个位置的词表预测 'hidden_states': [batch_size, seq_length, hidden_size], # 每层的隐藏状态 'attentions': [batch_size, num_heads, seq_length, seq_length] # 注意力权重}
# 预测结果示例predicted_token = "爱" # 模型成功预测出被遮蔽的词关键维度说明:
batch_size:批量大小,如32seq_length:序列长度,最大512hidden_size:隐藏层维度,BERT-base为768,BERT-large为1024vocab_size:词表大小,中文BERT约21128,英文BERT约30522num_heads:注意力头数,BERT-base为12,BERT-large为16
为什么不全部替换为 [MASK]?
- 如果全部替换为 [MASK],微调时会遇到问题(微调时没有 [MASK] 标记)
- 10% 替换为随机词:让模型学会纠错
- 10% 保持不变:让模型学会利用上下文
深层动机:缓解预训练-微调的分布偏移
这个 80%-10%-10% 的策略设计非常巧妙,背后有深刻的考虑:
问题:预训练和微调阶段的输入分布不一致
- 预训练:输入包含 [MASK] 标记
- 微调:输入是正常文本,没有 [MASK] 标记
- 风险:模型可能过度依赖 [MASK] 标记的存在,导致微调时性能下降
解决方案:混合策略
- 80% 用 [MASK]:主要训练目标,让模型学会预测
- 10% 用随机词:
- 模拟噪声环境,提升鲁棒性
- 强迫模型不能仅依赖当前位置的词,必须利用上下文
- 类似于去噪自编码器的思想
- 10% 保持原词:
- 缩小预训练和微调的分布差异
- 让模型习惯处理正常文本
- 模型需要判断:这个词是否需要”修正”
数学视角:
- 设 为原始词, 为替换后的词
- 预训练时模型看到:
- 微调时模型看到:
- 通过 10% 保持原词,让两个分布更接近
实验验证:
- 如果 100% 替换为 [MASK],下游任务性能下降约 1-2%
- 混合策略在预训练效率和微调性能之间取得最佳平衡
任务二:下一句预测(Next Sentence Prediction, NSP)
核心思想:判断两个句子是否在原文中连续出现。
具体做法:
输入:[CLS] 句子A [SEP] 句子B [SEP]输出:IsNext(连续) 或 NotNext(不连续)训练数据构造:
- 50% 的样本:句子B确实是句子A的下一句(正样本)
- 50% 的样本:句子B是随机选择的句子(负样本)
目的:让模型学习句子间的关系,有助于问答、自然语言推理等任务。
NSP的输入输出详解:
输入格式:
# 正样本示例(IsNext)sentence_A = "今天天气很好"sentence_B = "我们去公园散步吧" # 原文中的下一句
# 负样本示例(NotNext)sentence_A = "今天天气很好"sentence_B = "量子计算机的发展很快" # 随机选择的句子
# Token化后的输入input_ids = [101, ...sentence_A_ids..., 102, ...sentence_B_ids..., 102]# [CLS] + 句子A + [SEP] + 句子B + [SEP]
# 输入张量input_ids: [batch_size, seq_length] # 例如 [32, 256]attention_mask: [batch_size, seq_length] # 标记有效位置token_type_ids: [batch_size, seq_length] # 句子A为0,句子B为1# 例如:[0, 0, 0, ..., 0, 1, 1, 1, ..., 1]BERT处理过程:
# BERT编码hidden_states = BERT(input_ids, attention_mask, token_type_ids)# 输出形状:[batch_size, seq_length, hidden_size]
# 取[CLS]位置的输出(第0个位置)cls_output = hidden_states[:, 0, :]# 形状:[batch_size, hidden_size]# 例如:[32, 768]
# NSP分类头logits = NSP_head(cls_output)# 形状:[batch_size, 2] # 2分类:IsNext vs NotNext# 例如:[32, 2]
# 预测probs = softmax(logits, dim=-1)# probs[:, 0] = P(NotNext)# probs[:, 1] = P(IsNext)输出格式:
# 模型输出outputs = { 'loss': 标量, # NSP损失 'logits': [batch_size, 2], # IsNext/NotNext的logits 'prediction': [batch_size], # 0或1}
# 预测结果示例prediction = 1 # IsNext(句子B是句子A的下一句)confidence = 0.92 # 置信度联合训练:
# BERT同时训练MLM和NSPtotal_loss = MLM_loss + NSP_loss
# 输入input_ids: [batch_size, seq_length]masked_lm_labels: [batch_size, seq_length] # MLM标签next_sentence_labels: [batch_size] # NSP标签(0或1)
# 输出mlm_loss: 标量nsp_loss: 标量total_loss: 标量NSP任务的争议:真的有用吗?
NSP任务在BERT原论文中被认为是重要的预训练任务,但后续研究对其有效性提出了质疑:
支持NSP的观点:
- 句子间关系对某些任务(如问答、自然语言推理)很重要
- NSP能让模型学习长距离依赖关系
- [CLS] 标记的表征可以捕捉句子对的语义关系
质疑NSP的观点:
-
RoBERTa实验(2019):去除NSP后,性能不降反升
- 原因:NSP任务可能太简单了
- 随机选择的句子通常来自不同文档,主题差异明显
- 模型可能只是学会了”主题分类”,而非真正的句子关系
-
ALBERT改进(2019):用句子顺序预测(SOP)替代NSP
- SOP任务:判断两个连续句子的顺序是否正确
- 正样本:A → B(正确顺序)
- 负样本:B → A(颠倒顺序)
- 难度更大,因为两个句子来自同一文档,主题相同
- 实验表明SOP比NSP更有效
当前共识:
- NSP对某些任务有帮助,但不是必需的
- 更重要的是MLM任务和足够的训练数据
- 许多现代预训练模型(如RoBERTa)选择去除NSP
- 如果需要句子关系建模,SOP是更好的选择
关键洞察:预文本任务的设计需要平衡难度——太简单学不到有用信息,太难又无法收敛。
BERT的输入表示
BERT的输入不是简单的词嵌入,而是三种嵌入的求和:
三种嵌入:
-
Token Embeddings(词嵌入)
- 将每个词映射到向量空间
- 词表大小:通常30,000个词
- 维度:768(BERT-base)或1024(BERT-large)
-
Position Embeddings(位置嵌入)
- 编码词在句子中的位置信息
- 与Transformer原论文不同,BERT使用可学习的位置嵌入
- 最大序列长度:512
- 每个位置有独立的嵌入向量
-
Segment Embeddings(段落嵌入)
- 区分句子A和句子B
- 只有两个向量:(句子A)和 (句子B)
- 用于句子对任务(如NSP、自然语言推理)
输入表示公式:
示例:
输入:[CLS] 我 爱 深度 学习 [SEP] 它 很 有趣 [SEP]
Token Emb: E_[CLS] E_我 E_爱 E_深度 E_学习 E_[SEP] E_它 E_很 E_有趣 E_[SEP]Position Emb: E_0 E_1 E_2 E_3 E_4 E_5 E_6 E_7 E_8 E_9Segment Emb: E_A E_A E_A E_A E_A E_A E_B E_B E_B E_B
最终输入 = Token Emb + Position Emb + Segment Emb设计考虑:
-
为什么用可学习位置嵌入?
- 更灵活,可以适应不同的位置模式
- 实验表明效果优于固定的正弦位置编码
-
为什么需要Segment Embeddings?
- 让模型明确知道哪些词属于句子A,哪些属于句子B
- 对句子对任务至关重要
- 单句任务中,所有词都使用
特殊标记:
- [CLS](Classification):句子级表征,用于分类任务
- [SEP](Separator):分隔不同句子
- [MASK]:掩码标记,仅在预训练时使用
- [PAD]:填充标记,用于对齐不同长度的序列
3.2 BERT的使用方式:预训练+微调
BERT的强大之处在于”预训练-微调”范式:先在大规模无标注数据上预训练,再在特定任务上微调。
使用场景一:输入为单句子(情感分类)
任务:判断电影评论的情感(正面/负面)
输入格式:
[CLS] 这部电影太精彩了 [SEP]输出:使用 [CLS] 位置的向量进行分类
[CLS] 向量 → 线性层 → Softmax → 正面/负面详细的输入输出流程:
输入准备:
# 原始文本text = "这部电影太精彩了"
# Token化tokens = ["[CLS]", "这", "部", "电影", "太", "精彩", "了", "[SEP]"]input_ids = [101, 6821, 6956, 4510, 2512, 1922, 4689, 749, 102]
# 输入张量input_ids: [batch_size, seq_length] # 例如 [32, 64]attention_mask: [batch_size, seq_length] # 例如 [32, 64]token_type_ids: [batch_size, seq_length] # 单句任务全为0labels: [batch_size] # 例如 [32],值为0(负面)或1(正面)BERT编码:
# 通过BERT编码outputs = BERT(input_ids, attention_mask, token_type_ids)hidden_states = outputs.last_hidden_state# 形状:[batch_size, seq_length, hidden_size]# 例如:[32, 64, 768]
# 提取[CLS]位置的表征cls_output = hidden_states[:, 0, :]# 形状:[batch_size, hidden_size]# 例如:[32, 768]分类头:
# 添加分类层logits = classifier(cls_output)# classifier是一个线性层:nn.Linear(768, 2)# 形状:[batch_size, num_classes]# 例如:[32, 2]
# 计算概率probs = softmax(logits, dim=-1)# probs[:, 0] = P(负面)# probs[:, 1] = P(正面)
# 预测类别predictions = argmax(probs, dim=-1)# 形状:[batch_size]# 例如:[32],值为0或1输出结果:
# 完整输出outputs = { 'loss': 标量, # 交叉熵损失 'logits': [32, 2], # 原始分数 'probs': [32, 2], # 概率分布 'predictions': [32], # 预测类别}
# 示例结果prediction = 1 # 正面confidence = 0.95 # 置信度微调方式:
- 在 BERT 之上添加一个简单的分类层
- 使用少量标注数据进行端到端微调
- 整个 BERT 模型的参数都会更新
使用场景二:输入为句子对(自然语言推理)
任务:判断两个句子的关系(蕴含/矛盾/中立)
输入格式:
[CLS] 前提句 [SEP] 假设句 [SEP]示例:
前提:一个人在踢足球假设:一个人在运动关系:蕴含输出:同样使用 [CLS] 位置的向量进行三分类
详细的输入输出流程:
输入准备:
# 原始文本premise = "一个人在踢足球"hypothesis = "一个人在运动"
# Token化input_ids = [101, ...premise_ids..., 102, ...hypothesis_ids..., 102]# [CLS] + 前提句 + [SEP] + 假设句 + [SEP]
# 输入张量input_ids: [batch_size, seq_length] # 例如 [32, 128]attention_mask: [batch_size, seq_length]token_type_ids: [batch_size, seq_length]# token_type_ids: 前提句为0,假设句为1# 例如:[0,0,0,...,0, 1,1,1,...,1]
labels: [batch_size] # 0=蕴含, 1=矛盾, 2=中立BERT处理:
# BERT编码hidden_states = BERT(input_ids, attention_mask, token_type_ids)# 形状:[batch_size, seq_length, hidden_size]
# 提取[CLS]表征cls_output = hidden_states[:, 0, :]# 形状:[batch_size, hidden_size]# 例如:[32, 768]
# 三分类logits = classifier(cls_output)# classifier: nn.Linear(768, 3)# 形状:[batch_size, 3]
probs = softmax(logits, dim=-1)# probs[:, 0] = P(蕴含)# probs[:, 1] = P(矛盾)# probs[:, 2] = P(中立)输出结果:
outputs = { 'logits': [32, 3], 'predictions': [32], # 0, 1, 或 2}
# 示例prediction = 0 # 蕴含confidence = 0.89使用场景三:输入为单句子,输出为序列(词性标注)
任务:为句子中的每个词标注词性
输入格式:
[CLS] 我 爱 深度 学习 [SEP]输出:每个词位置的向量都用于分类
我 → 代词爱 → 动词深度 → 形容词学习 → 名词详细的输入输出流程:
输入准备:
# 原始文本text = "我爱深度学习"tokens = ["[CLS]", "我", "爱", "深度", "学习", "[SEP]"]
# 输入张量input_ids: [batch_size, seq_length] # 例如 [32, 64]attention_mask: [batch_size, seq_length]labels: [batch_size, seq_length] # 每个位置的词性标签# 例如:[-100, 3, 5, 2, 1, -100] # -100表示不计算损失BERT处理:
# BERT编码hidden_states = BERT(input_ids, attention_mask)# 形状:[batch_size, seq_length, hidden_size]# 例如:[32, 64, 768]
# 对每个位置进行分类logits = token_classifier(hidden_states)# token_classifier: nn.Linear(768, num_tags)# 形状:[batch_size, seq_length, num_tags]# 例如:[32, 64, 20] # 假设有20种词性
# 预测每个位置的标签predictions = argmax(logits, dim=-1)# 形状:[batch_size, seq_length]# 例如:[32, 64]输出结果:
outputs = { 'logits': [32, 64, 20], 'predictions': [32, 64],}
# 示例(忽略[CLS]和[SEP])tokens = ["我", "爱", "深度", "学习"]predictions = ["代词", "动词", "形容词", "名词"]微调方式:
- 在每个词的输出向量上添加分类层
- 对每个位置独立进行分类
使用场景四:抽取式问答(Question Answering)
任务:从文章中抽取答案片段
输入格式:
[CLS] 问题 [SEP] 文章 [SEP]输出:预测答案的起始位置和结束位置
文章:爱因斯坦于1879年出生于德国问题:爱因斯坦何时出生?答案:1879年(起始位置=5,结束位置=9)详细的输入输出流程:
输入准备:
# 原始文本question = "爱因斯坦何时出生?"context = "爱因斯坦于1879年出生于德国"
# Token化input_ids = [101, ...question_ids..., 102, ...context_ids..., 102]
# 输入张量input_ids: [batch_size, seq_length] # 例如 [32, 384]attention_mask: [batch_size, seq_length]token_type_ids: [batch_size, seq_length]# token_type_ids: 问题为0,文章为1
# 标签(答案的位置)start_positions: [batch_size] # 答案起始位置的索引end_positions: [batch_size] # 答案结束位置的索引BERT处理:
# BERT编码hidden_states = BERT(input_ids, attention_mask, token_type_ids)# 形状:[batch_size, seq_length, hidden_size]# 例如:[32, 384, 768]
# 预测起始位置start_logits = start_classifier(hidden_states)# start_classifier: nn.Linear(768, 1)# 形状:[batch_size, seq_length]# 例如:[32, 384]
# 预测结束位置end_logits = end_classifier(hidden_states)# 形状:[batch_size, seq_length]# 例如:[32, 384]
# 找到最可能的起始和结束位置start_probs = softmax(start_logits, dim=-1)end_probs = softmax(end_logits, dim=-1)
start_pos = argmax(start_probs, dim=-1) # [batch_size]end_pos = argmax(end_probs, dim=-1) # [batch_size]输出结果:
outputs = { 'start_logits': [32, 384], 'end_logits': [32, 384], 'start_position': [32], 'end_position': [32],}
# 提取答案answer_tokens = input_ids[start_pos:end_pos+1]answer_text = tokenizer.decode(answer_tokens)# 例如:"1879年"微调方式:
- 训练两个向量:起始位置向量和结束位置向量
- 计算每个位置是起始/结束的概率
- 选择概率最高的区间作为答案
3.3 BERT为什么有效?
教程深入探讨了BERT有效的原因:
原因一:上下文相关的词向量
传统词嵌入的问题:
- Word2Vec、GloVe 等方法为每个词生成固定的向量
- 无法处理一词多义现象
示例:
句子1:我在银行存钱句子2:我在河岸边散步在传统词嵌入中,“银行”和”河岸”(bank)得到相同的向量。
BERT的优势:
- 根据上下文动态生成词向量
- 同一个词在不同句子中得到不同的表征
- 自动解决一词多义问题
原因二:注意力的可解释性
分析方法:可视化注意力权重,理解模型关注什么。
发现:
- 某些注意力头关注语法关系(如主谓关系)
- 某些注意力头关注共指消解(代词指向的实体)
- 某些注意力头关注语义相关性
示例:
句子:The animal didn't cross the street because it was too tired.分析 “it” 的注意力权重,发现模型正确地将 “it” 关联到 “animal”,而非 “street”。
原因三:各层表征的差异
实验方法:线性探针(Linear Probing)
线性探针是分析自监督模型的重要工具:
- 固定BERT参数:不更新预训练模型的权重
- 训练线性分类器:在某一层的输出上训练一个简单的线性层
- 观察性能:通过分类器的性能判断该层学到了什么信息
这种方法就像用”显微镜”观察BERT的每一层,探测它们学到的知识。
发现:
- 底层(1-4层):更关注语法信息(词性、句法结构)
- 线性探针在词性标注任务上表现好
- 中层(5-8层):捕捉语义信息(词义、句子含义)
- 线性探针在语义相似度任务上表现好
- 高层(9-12层):学习任务相关的抽象表征
- 线性探针在情感分类等任务上表现好
启示:
- 不同任务可能需要不同层的表征
- 可以根据任务特点选择合适的层进行微调
- Linear Probing 是理解和分析预训练模型的重要方法
3.4 GLUE基准测试:评估语言理解能力
**GLUE(General Language Understanding Evaluation)**是评估自然语言理解系统的标准基准测试集合,在BERT等预训练模型的评估中扮演着核心角色。
GLUE是什么?
定义:
- GLUE是一个多任务基准测试集合
- 包含9个不同的自然语言理解任务
- 涵盖句子分类、句子对关系判断、语义相似度等多个方面
- 由纽约大学、华盛顿大学和DeepMind于2018年提出
目的:
- 提供标准化的评估框架
- 促进不同模型之间的公平比较
- 推动自然语言理解技术的发展
- 避免模型在单一任务上过拟合
GLUE包含的9个任务
1. CoLA(Corpus of Linguistic Acceptability)
- 任务类型:单句分类
- 目标:判断句子的语法是否正确
- 示例:
正确:"The book was written by John."错误:"Was written the book by John."
- 评估指标:Matthews相关系数(MCC)
2. SST-2(Stanford Sentiment Treebank)
- 任务类型:单句分类
- 目标:电影评论的情感分类(正面/负面)
- 示例:
正面:"This movie is absolutely fantastic!"负面:"A complete waste of time."
- 评估指标:准确率(Accuracy)
3. MRPC(Microsoft Research Paraphrase Corpus)
- 任务类型:句子对分类
- 目标:判断两个句子是否语义等价
- 示例:
句子1:"The company will lay off 5,000 workers."句子2:"5,000 employees will lose their jobs."标签:等价
- 评估指标:准确率和F1分数
4. STS-B(Semantic Textual Similarity Benchmark)
- 任务类型:句子对回归
- 目标:预测两个句子的语义相似度(0-5分)
- 示例:
句子1:"A man is playing a guitar."句子2:"A person is playing music."相似度:4.2
- 评估指标:Pearson和Spearman相关系数
5. QQP(Quora Question Pairs)
- 任务类型:句子对分类
- 目标:判断两个问题是否语义相同
- 示例:
问题1:"How do I learn Python?"问题2:"What's the best way to learn Python?"标签:相同
- 评估指标:准确率和F1分数
6. MNLI(Multi-Genre Natural Language Inference)
- 任务类型:句子对分类(3分类)
- 目标:自然语言推理(蕴含/矛盾/中立)
- 示例:
前提:"A man is playing guitar on stage."假设:"A musician is performing."关系:蕴含(Entailment)
- 评估指标:准确率(匹配和不匹配两个测试集)
7. QNLI(Question Natural Language Inference)
- 任务类型:句子对分类
- 目标:判断段落是否包含问题的答案
- 示例:
问题:"When was the university founded?"段落:"The university was established in 1850."标签:包含答案
- 评估指标:准确率
8. RTE(Recognizing Textual Entailment)
- 任务类型:句子对分类(2分类)
- 目标:判断是否存在文本蕴含关系
- 示例:
文本1:"Google acquired YouTube."文本2:"YouTube is owned by Google."标签:蕴含
- 评估指标:准确率
9. WNLI(Winograd Natural Language Inference)
- 任务类型:句子对分类
- 目标:代词消歧和常识推理
- 示例:
句子:"The trophy doesn't fit in the suitcase because it's too big."问题:"it"指的是trophy还是suitcase?
- 评估指标:准确率
- 注意:这是GLUE中最难的任务,很多模型表现不佳
GLUE评分机制
总分计算:
- GLUE总分是所有任务得分的平均值
- 每个任务使用其特定的评估指标
- 归一化到0-100的范围
基准对比:
人类表现(Human Baseline):87.1BERT-Large(2018):80.5RoBERTa-Large(2019):88.5ALBERT-xxlarge(2019):89.4BERT在GLUE上的突破性表现
历史意义:
- BERT首次让预训练模型在GLUE上接近人类水平
- 在8个任务上创造了新的SOTA(State-of-the-Art)
- 证明了预训练-微调范式的有效性
性能对比:
| 模型 | GLUE总分 | 相比之前提升 |
|---|---|---|
| 传统方法(ELMo等) | ~70 | - |
| BERT-Base | 78.3 | +8.3 |
| BERT-Large | 80.5 | +10.5 |
| 人类表现 | 87.1 | - |
关键发现:
- 模型规模的影响:BERT-Large比BERT-Base平均提升2.2分
- 预训练的重要性:预训练显著提升了所有任务的性能
- 迁移学习的有效性:同一个预训练模型可以适应多种不同任务
GLUE的意义与局限
重要意义:
- 标准化评估:提供统一的评估框架,避免cherry-picking
- 多任务覆盖:涵盖多种语言理解能力,全面评估模型
- 推动进步:激励研究者开发更强大的模型
- 公平比较:相同的数据集和评估指标,确保可比性
已知局限:
- 任务规模:部分任务的训练集较小(如RTE只有2.5k样本)
- 难度饱和:顶级模型已接近或超过人类表现,区分度下降
- 评估范围:未涵盖所有NLP能力(如生成、推理、常识等)
- 数据泄露风险:大规模预训练可能见过测试集相似数据
后续发展:
- SuperGLUE(2019):更难的任务集合,包含更具挑战性的任务
- XTREME:跨语言评估基准
- BigBench:包含200+任务的大规模基准
关键洞察:
- GLUE是评估预训练模型的重要工具,但不是唯一标准
- 高GLUE分数不等于模型在所有场景下都表现优秀
- 需要结合实际应用场景进行综合评估
3.5 BERT的变种
教程简要介绍了BERT的几个重要变种:
RoBERTa(Robustly Optimized BERT)
改进:
- 去除NSP任务:实验发现NSP任务帮助不大
- 更大的批量:从256增加到8192
- 更多的数据:训练数据量增加10倍
- 更长的训练:训练步数大幅增加
结果:在多个任务上超越BERT
ALBERT(A Lite BERT)
目标:减少参数量,提升训练效率
改进:
- 参数共享:所有层共享参数
- 嵌入矩阵分解:将大的嵌入矩阵分解为两个小矩阵
- 句子顺序预测:用SOP(Sentence Order Prediction)替代NSP
结果:参数量减少18倍,性能相当甚至更好
DistilBERT
目标:通过知识蒸馏压缩模型
方法:
- 使用BERT作为教师模型
- 训练一个更小的学生模型(6层)
- 学生模型学习教师模型的输出分布
结果:
- 参数量减少40%
- 速度提升60%
- 性能保留97%
Multilingual BERT(多语言BERT)
神奇现象:跨语言的通用语义空间
BERT展现出一个令人惊讶的能力:
实验发现:
- 用英文预训练的BERT,没有见过中文数据
- 但在中文任务上也能取得一定效果
- 说明BERT学到了跨语言的通用语义表征
多语言BERT(mBERT):
- 在104种语言的数据上预训练
- 不同语言的相似概念在表征空间中靠近
- 支持零样本跨语言迁移(Zero-shot Cross-lingual Transfer)
示例:
英文训练:The cat sits on the mat.中文测试:猫坐在垫子上。→ mBERT能识别出两者语义相似关键洞察:
- 语言的底层语义结构是相通的
- 自监督学习能够发现这种通用结构
- 为跨语言NLP任务提供了强大基础
四、GPT:自回归语言模型(教程核心内容)
GPT(Generative Pre-Training)采用与BERT不同的自监督学习策略:自回归语言建模。
4.1 GPT的预训练任务
核心思想:根据前文预测下一个词(单向语言模型)。
训练目标:
参数说明:
- :第 个词
- :前 个词(左侧上下文)
- :基于前文预测当前词的概率分布
- :句子长度
示例:
输入:我爱深度预测:学习
输入:我爱深度学习预测:,
输入:我爱深度学习,预测:它关键特点:
- 单向:只能看到左边的词,看不到右边的词
- 自回归:逐词生成,每次预测依赖之前的预测结果
- 生成式:天然适合文本生成任务
GPT的输入输出详解:
预训练阶段的输入输出:
输入格式:
# 原始文本text = "我爱深度学习"
# Token化(不需要[CLS]和[SEP])tokens = ["我", "爱", "深度", "学习"]input_ids = [2769, 4263, 3995, 2428, 2110, 739]
# 输入张量input_ids: [batch_size, seq_length] # 例如 [32, 128]attention_mask: [batch_size, seq_length] # 标记有效位置position_ids: [batch_size, seq_length] # 位置编码GPT处理过程(训练时):
# 输入序列:我 爱 深度 学习# 目标:预测下一个词
# 对于每个位置i,预测位置i+1的词# 位置0:输入"我",预测"爱"# 位置1:输入"我 爱",预测"深度"# 位置2:输入"我 爱 深度",预测"学习"# 位置3:输入"我 爱 深度 学习",预测<EOS>
# GPT编码hidden_states = GPT(input_ids, attention_mask)# 形状:[batch_size, seq_length, hidden_size]# 例如:[32, 128, 768] (GPT-2 small)
# 语言模型头(预测下一个词)logits = LM_head(hidden_states)# 形状:[batch_size, seq_length, vocab_size]# 例如:[32, 128, 50257] (GPT-2词表大小)
# 计算损失(每个位置预测下一个词)# labels是input_ids向左移动一位labels = input_ids[:, 1:] # [batch_size, seq_length-1]logits_for_loss = logits[:, :-1, :] # [batch_size, seq_length-1, vocab_size]
loss = cross_entropy(logits_for_loss, labels)推理阶段的输入输出(文本生成):
输入格式:
# 提示文本(prompt)prompt = "今天天气很好,"prompt_ids = [1234, 5678, 9012, 3456] # token IDs
# 输入张量input_ids: [batch_size, prompt_length] # 例如 [1, 4]自回归生成过程:
# 第1步:输入prompt,预测第1个新词hidden = GPT(prompt_ids) # [1, 4, 768]logits = LM_head(hidden[:, -1, :]) # 只取最后一个位置 [1, 50257]next_token = argmax(logits) # 例如:token_id=7890("我们")
# 第2步:拼接已生成的词,预测第2个新词new_input = concat([prompt_ids, next_token]) # [1, 5]hidden = GPT(new_input) # [1, 5, 768]logits = LM_head(hidden[:, -1, :]) # [1, 50257]next_token = argmax(logits) # 例如:token_id=2345("去")
# 第3步:继续生成...# 重复直到生成<EOS>或达到最大长度完整生成示例:
# 输入prompt = "今天天气很好,"
# 输出(自回归生成)generated_text = "今天天气很好,我们去公园散步吧。"
# 生成过程# Step 1: "今天天气很好," → 预测 "我们"# Step 2: "今天天气很好,我们" → 预测 "去"# Step 3: "今天天气很好,我们去" → 预测 "公园"# Step 4: "今天天气很好,我们去公园" → 预测 "散步"# Step 5: "今天天气很好,我们去公园散步" → 预测 "吧"# Step 6: "今天天气很好,我们去公园散步吧" → 预测 "。"关键维度说明:
batch_size:批量大小seq_length:序列长度,GPT-2最大1024,GPT-3最大2048hidden_size:隐藏层维度,GPT-2 small为768,GPT-3为12288vocab_size:词表大小,GPT-2为50257
采样策略:
# 贪婪解码(Greedy Decoding)next_token = argmax(logits) # 总是选择概率最高的词
# Top-k采样top_k_logits, top_k_indices = topk(logits, k=50)probs = softmax(top_k_logits)next_token = sample(top_k_indices, probs) # 从top-k中随机采样
# Top-p采样(Nucleus Sampling)sorted_probs, sorted_indices = sort(softmax(logits))cumsum_probs = cumsum(sorted_probs)nucleus = sorted_indices[cumsum_probs <= p] # p=0.9next_token = sample(nucleus) # 从累积概率<=p的词中采样
# 温度采样(Temperature Sampling)scaled_logits = logits / temperature # temperature=0.7probs = softmax(scaled_logits)next_token = sample(probs) # 温度越低,分布越尖锐4.2 GPT vs BERT:单向 vs 双向
| 特性 | BERT(双向) | GPT(单向) |
|---|---|---|
| 架构 | Transformer Encoder | Transformer Decoder |
| 上下文 | 左右两边 | 只有左边 |
| 预训练任务 | 掩码预测 + NSP | 自回归预测 |
| 适用任务 | 理解任务(分类、问答) | 生成任务(文本生成) |
| 微调方式 | 添加任务层 | 提示学习(Prompting) |
核心差异:
- BERT:看到完整句子,适合理解语义,但不适合生成
- GPT:只看前文,适合生成文本,但理解能力相对较弱
为什么GPT不能双向?
如果GPT能看到右边的词,预测任务就太简单了(直接复制右边的词),模型学不到有用的表征。
因果自注意力(Causal Self-Attention)的实现
GPT的单向特性是通过因果自注意力机制实现的,这是与BERT最核心的架构差异。
标准自注意力(BERT使用):
每个位置可以关注所有位置(包括左边和右边)。
因果自注意力(GPT使用):
其中 是因果掩码矩阵(Causal Mask):
参数说明:
- :查询位置(当前词)
- :键位置(被关注的词)
- :只能关注当前位置及之前的位置
- :经过softmax后变为0,完全屏蔽未来信息
可视化示例:
对于句子”我 爱 深度 学习”,注意力掩码矩阵为:
我 爱 深度 学习我 [0 -∞ -∞ -∞]爱 [0 0 -∞ -∞]深度 [0 0 0 -∞]学习 [0 0 0 0]解释:
- “我”只能看到”我”自己
- “爱”可以看到”我”和”爱”
- “深度”可以看到”我”、“爱”、“深度”
- “学习”可以看到所有前面的词
实现细节:
def causal_attention_mask(seq_len): """生成因果注意力掩码""" # 创建上三角矩阵(不包括对角线) mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) # 将1替换为-inf,0保持不变 mask = mask.masked_fill(mask == 1, float('-inf')) return mask
# 使用示例seq_len = 4mask = causal_attention_mask(seq_len)# 输出:# [[0., -inf, -inf, -inf],# [0., 0., -inf, -inf],# [0., 0., 0., -inf],# [0., 0., 0., 0.]]为什么用 -∞ 而不是 0?
- 掩码加在softmax之前的logits上
- ,完全屏蔽
- 如果直接用0,softmax后仍有非零概率
训练与推理的一致性:
- 训练时:并行计算,一次性预测所有位置,但每个位置只能看到之前的内容
- 推理时:逐词生成,自然只能看到已生成的内容
- 因果掩码保证了训练和推理的一致性
4.3 GPT的演进:规模化的力量
注意:教程仅概述了GPT的基本思想,具体参数规模等信息为作者补充。
GPT系列的演进:
- GPT-1(2018):基础版本,验证了预训练-微调范式
- GPT-2(2019):规模扩大,展现了零样本学习能力
- GPT-3(2020):大规模模型,涌现出上下文学习能力
- GPT-4(2023):多模态能力,性能大幅提升
关键洞察:
- 规模定律(Scaling Law):模型越大,数据越多,性能越好
- 涌现能力(Emergent Abilities):达到一定规模后,模型展现出训练时未明确优化的能力
- 上下文学习(In-Context Learning):无需微调,通过示例即可完成新任务
规模定律(Scaling Law)的直观理解
规模定律是GPT系列成功的核心发现之一,揭示了模型性能与规模之间的可预测关系。
核心发现(OpenAI,2020):
模型的损失(Loss)与三个因素呈现幂律关系:
参数说明:
- :测试集上的损失(越小越好)
- :模型参数量(Number of parameters)
- :训练数据量(Dataset size)
- :计算量(Compute budget,FLOPs)
- :幂律指数(实验测定)
关键结论:
-
平滑可预测:性能提升遵循平滑的幂律曲线,不是随机的
- 可以通过小规模实验预测大规模模型的性能
- 为模型设计和资源分配提供指导
-
三要素协同:模型大小、数据量、计算量需要协调增长
- 只增加模型大小而不增加数据,效果有限
- 只增加数据而不增加模型容量,也无法充分利用
- 最优策略:三者同步扩展
-
无饱和迹象:在实验范围内,性能持续提升
- GPT-3(175B参数)仍未达到性能上限
- 更大的模型(如GPT-4)继续验证这一规律
直观理解:
为什么模型越大越好?
- 容量假说:更大的模型有更多参数,可以存储更多知识
- 类比:就像更大的图书馆可以存放更多书籍
- 数学视角:参数空间更大,可以拟合更复杂的函数
为什么数据越多越好?
- 泛化假说:更多数据提供更丰富的模式,减少过拟合
- 类比:见过更多例子的学生,考试时更不容易遇到完全陌生的题
- 数学视角:更好地逼近真实数据分布
为什么会有涌现能力?
- 相变现象:当模型规模超过某个临界点,新能力突然出现
- 示例:
- GPT-2(1.5B):基本的文本生成
- GPT-3(175B):上下文学习、推理、代码生成
- 解释:某些复杂任务需要最小的模型容量才能完成
实际数据:
| 模型 | 参数量 | 训练数据 | 涌现能力 |
|---|---|---|---|
| GPT-1 | 117M | 5GB | 基础语言理解 |
| GPT-2 | 1.5B | 40GB | 零样本文本生成 |
| GPT-3 | 175B | 570GB | 上下文学习、推理 |
| GPT-4 | ~1.7T(估计) | 未公开 | 多模态、复杂推理 |
启示:
- 工程实践:投资更大的模型和更多数据是值得的
- 研究方向:理解规模定律的理论基础
- 未来展望:规模定律能持续多久?是否存在物理极限?
局限性:
- 规模定律主要适用于语言建模损失,不一定直接对应下游任务性能
- 计算成本随规模指数增长,存在经济和环境成本
- 更大不一定总是更好,需要考虑效率和可解释性
上下文学习(In-Context Learning)详解
这是GPT-3最令人惊讶的能力之一:
核心思想:通过Prompt(提示)激发模型已有的知识,而非微调模型参数。
零样本学习(Zero-shot):
Prompt: 将下面的英文翻译成中文:Hello, how are you?翻译:模型直接输出:你好,你好吗?
少样本学习(Few-shot):
Prompt:英文:Good morning中文:早上好
英文:Thank you中文:谢谢
英文:Hello, how are you?中文:模型通过几个示例就能理解任务,输出:你好,你好吗?
关键机制:
- 不更新参数:模型权重保持不变
- Prompt设计:通过精心设计的提示词引导模型
- 上下文理解:模型从示例中推断任务模式
- 知识激发:唤醒预训练时学到的知识
为什么有效?
- GPT在预训练时见过大量”模式识别”的例子
- 通过自回归预测,学会了从上下文推断规律
- 大规模模型有足够的容量存储和调用知识
与微调的对比:
| 特性 | 微调(Fine-tuning) | 上下文学习(In-Context Learning) |
|---|---|---|
| 参数更新 | 需要更新模型参数 | 参数保持不变 |
| 数据需求 | 需要标注数据集 | 只需几个示例 |
| 适应速度 | 需要训练时间 | 即时响应 |
| 灵活性 | 每个任务需单独微调 | 一个模型处理多任务 |
| 成本 | 计算成本高 | 推理成本相对低 |
这种能力展示了大规模语言模型的”智能涌现”——当模型足够大时,会自然出现训练时未明确优化的能力。
五、对比学习详解(补充内容)
说明:以下对比学习内容为作者根据领域知识补充,非李宏毅教程原文。教程第6章自注意力机制中仅介绍了Attention的QKV计算,未展开对比学习方法。
对比学习是近年来在计算机视觉领域取得巨大成功的自监督学习方法。
5.1 核心机制:正负样本构造
正样本对的构造:
对于同一张图片,通过不同的数据增强生成两个视图:
常见增强方法:
- 随机裁剪(Random Crop)
- 颜色抖动(Color Jitter)
- 高斯模糊(Gaussian Blur)
- 水平翻转(Horizontal Flip)
示例:
原图:一只猫的照片视图1:裁剪 + 颜色调整视图2:翻转 + 模糊→ 这两个视图是正样本对负样本对的构造:
Batch 中的其他所有样本都作为负样本。
关键洞察:
- 正样本对:语义相同,外观不同 → 学习不变性
- 负样本对:语义不同 → 学习区分性
5.2 SimCLR:简单而强大的对比学习框架
SimCLR(Simple Framework for Contrastive Learning)是对比学习的代表性方法。
四个关键步骤:
-
数据增强:生成正样本对
x_i = augment(x) # 第一个视图x_j = augment(x) # 第二个视图 -
编码器:提取特征
h_i = f(x_i) # 使用 ResNet 等h_j = f(x_j) -
投影头:映射到对比空间
z_i = g(h_i) # MLP 投影z_j = g(h_j) -
对比损失:NT-Xent Loss
参数说明(教程未提及,此处为补充):
- :同一图像两个增强视图的投影向量
- :余弦相似度,
- :温度参数,控制分布的平滑程度
- :指示函数,排除自身
- :批量大小(batch size)
- :总样本数(每个样本有两个视图)
NT-Xent损失的深层设计动机
NT-Xent(Normalized Temperature-scaled Cross Entropy)损失,也称为InfoNCE损失,其设计有深刻的理论基础。
概率视角的理解:
对比学习的本质是一个分类问题:给定查询样本 ,从 个候选样本中识别出它的正样本对 。
将相似度转换为概率:
这是一个softmax分类,将相似度归一化为概率分布。
损失函数:
这是标准的交叉熵损失,最大化正样本对的概率。
信息论视角:
InfoNCE损失来源于互信息最大化:
目标:最大化增强视图之间的互信息
互信息定义:
挑战:直接计算互信息需要知道真实分布 ,这在实践中不可行。
InfoNCE的巧妙之处:通过对比学习间接最大化互信息的下界:
关键洞察:
- 最小化InfoNCE损失 ⟺ 最大化互信息下界
- 更多负样本(更大的 )⟺ 更紧的下界
- 这解释了为什么大batch size对对比学习如此重要
温度参数 的作用:
数学效果:
- 小:分布更尖锐,模型更关注最相似的样本
- 大:分布更平滑,模型考虑更多样本
直观理解:
τ = 0.1(小):softmax([10, 8, 2]) → [0.88, 0.12, 0.00] # 集中τ = 1.0(大):softmax([1, 0.8, 0.2]) → [0.46, 0.38, 0.16] # 分散实践选择:
- 典型值: 或
- 太小:训练不稳定,梯度消失
- 太大:区分度不足,学习缓慢
为什么用余弦相似度?
归一化的好处:
- 消除向量模长的影响,只关注方向
- 数值稳定,避免指数爆炸
- 几何解释:向量夹角越小,相似度越高
公式:
其中 是两个向量的夹角。
对比学习的几何直观:
- 正样本对:拉近夹角()
- 负样本对:推远夹角()
- 在单位超球面上学习表征
为什么需要投影头?
这是一个重要的设计选择:
- 表征层(h):保留丰富的语义信息,用于下游任务
- 投影层(z):专门用于对比学习,可以丢弃一些信息
实验发现:
- 在投影空间(z)做对比学习效果更好
- 但下游任务使用表征层(h)效果更好
- 投影头在训练后可以丢弃
5.3 MoCo:动量对比学习
MoCo(Momentum Contrast)解决了对比学习的一个关键问题:如何高效利用大量负样本。
核心创新:队列机制
问题:
- SimCLR 需要大 Batch Size(如 8192)才能有足够的负样本
- 大 Batch Size 需要大量 GPU 内存
解决方案:
- 维护一个负样本队列(如 65536 个样本)
- 使用动量编码器生成负样本特征
- 队列先进先出,持续更新
动量编码器更新公式:
参数说明(教程未提及,此处为补充):
- :动量编码器(key encoder)的参数
- :在线编码器(query encoder)的参数
- :动量系数,通常取 0.999
- 注意:这里的动量与优化器中的动量不同,是参数的指数移动平均
动量编码器 vs 优化器动量:两个完全不同的概念
这是一个容易混淆的地方,需要明确区分:
1. 优化器动量(Optimizer Momentum)
作用层级:梯度层级 更新对象:梯度的移动平均
SGD with Momentum公式:
参数说明:
- :梯度的动量(velocity)
- :动量系数(通常0.9)
- :当前梯度
- :学习率
目的:
- 加速收敛,减少震荡
- 累积历史梯度信息
- 帮助跳出局部最优
2. MoCo动量编码器(Momentum Encoder)
作用层级:参数层级 更新对象:模型参数的移动平均
MoCo更新公式:
参数说明:
- :动量编码器的参数(慢变)
- :在线编码器的参数(快变)
- :动量系数(通常0.999,非常接近1)
目的:
- 保持特征一致性
- 避免队列中的特征过时
- 稳定训练过程
关键区别对比:
| 特性 | 优化器动量 | MoCo动量编码器 |
|---|---|---|
| 作用对象 | 梯度 | 参数 |
| 更新方式 | 梯度的指数移动平均 | 参数的指数移动平均 |
| 动量系数 | ||
| 更新频率 | 每次迭代 | 每次迭代 |
| 目的 | 加速优化 | 稳定特征 |
| 是否反向传播 | 是(计算梯度) | 否(只复制参数) |
为什么MoCo需要动量编码器?
问题背景:
- MoCo使用队列存储负样本特征
- 队列中的特征是由之前的编码器生成的
- 如果编码器参数变化太快,队列中的特征会”过时”
具体问题:
时刻 t=0: encoder_0 生成特征 f_0,加入队列时刻 t=100: encoder_100 生成查询 q_100问题:q_100 和 f_0 来自不同的编码器,不一致!解决方案:动量编码器
- 动量编码器参数变化缓慢()
- 队列中的特征虽然是不同时刻生成的,但编码器差异很小
- 保证特征空间的一致性
数学直观:
假设 ,经过100次迭代:
计算:
解释:
- 100次迭代后,动量编码器仍保留约90%的初始参数
- 变化非常缓慢,保证了特征一致性
- 相比之下,在线编码器可能已经变化很大
实验验证:
- 不使用动量编码器:性能显著下降
- 太小(如0.9):特征不一致,性能差
- 太大(如0.9999):更新太慢,学习效率低
- :最佳平衡点
关键洞察:
- 动量编码器是MoCo的核心创新之一
- 它解决了队列机制带来的特征不一致问题
- 这是一种参数层面的”时间平滑”技术
- 与优化器动量完全不同,不要混淆!
关键洞察:
- 队列提供大量负样本,无需大 Batch
- 动量更新保证特征一致性
- 训练更高效,效果更好
六、视觉自监督学习(补充内容)
说明:以下MAE、CLIP等视觉自监督学习内容为作者补充,非李宏毅教程原文。
6.1 掩码图像建模(Masked Image Modeling)
将掩码预测的思想扩展到视觉领域。
MAE(Masked Autoencoder)的做法:
-
随机遮蔽:遮住 75% 的图像块
原图:16×16 = 256 个 patch遮蔽:192 个 patch可见:64 个 patch -
编码器:只处理可见的 patch
- 大幅减少计算量
- 强制模型学习全局表征
-
解码器:重建被遮蔽的 patch
- 轻量级解码器
- 输出重建的像素值
为什么遮蔽比例这么高?
这是 MAE 的关键创新:
图像 vs 文本的区别:
- 文本:信息密度高,遮蔽 15% 就很难
- 图像:空间冗余大,相邻像素高度相关
高遮蔽比例的好处:
- 防止模型”作弊”(从邻近像素插值)
- 强制学习高层语义,而非低层纹理
- 大幅提升训练效率(只编码 25% 的 patch)
实验结果:
- 遮蔽 75% 效果最好
- 遮蔽太少(如 15%)效果差
- 遮蔽太多(如 90%)任务太难
MAE的非对称编码器-解码器设计
MAE的另一个关键创新是非对称架构,这与传统自编码器有本质区别。
传统自编码器(对称设计):
输入 → 编码器(大) → 潜在表征 → 解码器(大) → 输出- 编码器和解码器规模相当
- 处理所有输入数据
MAE(非对称设计):
可见patch → 编码器(大,ViT-Large) → 表征 ↓遮蔽patch + 表征 → 解码器(小,8层Transformer) → 重建关键设计决策:
1. 编码器只处理可见patch
动机:
- 如果编码器处理所有patch(包括mask token),计算量巨大
- 75%遮蔽意味着75%的计算是在处理无信息的mask token
- 这是巨大的浪费
实现:
- 输入:只有可见的25% patch(64个patch)
- 编码器:标准ViT-Large(24层,1024维)
- 输出:64个patch的表征
好处:
- 计算量减少约75%(只处理25%的patch)
- 训练速度提升3-4倍
- 使得大规模预训练变得可行
2. 解码器处理所有patch
动机:
- 解码器需要重建所有被遮蔽的patch
- 必须处理完整的patch序列(256个)
实现:
- 输入:编码器输出(64个) + mask tokens(192个)
- 解码器:轻量级Transformer(8层,512维)
- 输出:256个patch的像素重建
为什么解码器可以很小?
- 解码器只在预训练时使用,微调时丢弃
- 重建任务相对简单(像素级预测)
- 编码器已经学到了高层语义,解码器只需”填充细节”
3. 架构对比
| 组件 | 编码器 | 解码器 |
|---|---|---|
| 层数 | 24层 | 8层 |
| 维度 | 1024 | 512 |
| 参数量 | ~300M | ~50M |
| 输入 | 64个可见patch | 256个patch(64可见+192mask) |
| 作用 | 学习表征(保留) | 重建像素(丢弃) |
数学形式化:
编码阶段:
参数说明:
- :可见patch()
- :编码器输出()
解码阶段:
参数说明:
- :可学习的mask tokens()
- :拼接操作
- :重建的所有patch()
重建损失:
关键:只计算被遮蔽patch的重建损失,不计算可见patch。
为什么这个设计如此有效?
1. 计算效率:
- 编码器处理25%数据,解码器虽然处理100%但很小
- 总计算量远小于对称设计
- 使得在ImageNet上预训练只需几天(而非几周)
2. 表征质量:
- 编码器专注于从稀疏信息中提取语义
- 强制学习全局理解,而非局部纹理
- 学到的表征更适合下游任务
3. 训练稳定性:
- 解码器轻量,不会主导训练
- 避免了”解码器过拟合”问题
- 编码器学到的表征更通用
实验验证:
| 设计 | 预训练时间 | ImageNet准确率 |
|---|---|---|
| 对称(编码器=解码器) | 7天 | 82.5% |
| 非对称(MAE设计) | 2天 | 83.6% |
关键洞察:
- 非对称设计是MAE成功的关键
- 它平衡了计算效率和表征质量
- 这个设计思想可以推广到其他模态(如视频、音频)
6.2 多模态自监督学习:CLIP
CLIP(Contrastive Language-Image Pre-training):
方法:图像-文本对比学习
核心思想:
- 从 4 亿图像-文本对中学习
- 图像编码器和文本编码器
- 对比损失拉近匹配的图像-文本对
能力:
- Zero-shot 图像分类
- 跨模态检索
- 强大的迁移能力
应用:
- 文生图模型(DALL-E、Stable Diffusion)
- 视觉问答
- 图像描述生成
CLIP的双编码器架构详解
CLIP采用对称的双编码器设计,分别处理图像和文本,然后在共享的嵌入空间中进行对比学习。
架构组成:
1. 图像编码器(Image Encoder)
- 架构选择:Vision Transformer (ViT) 或 ResNet
- 输入:图像
- 输出:图像嵌入
2. 文本编码器(Text Encoder)
- 架构选择:Transformer(类似GPT)
- 输入:文本描述 (token序列)
- 输出:文本嵌入
3. 投影到共享空间
- 两个编码器的输出都投影到相同维度的嵌入空间
- 典型维度: 或
- 嵌入向量经过L2归一化
数学形式化:
编码过程:
参数说明:
- :图像编码器
- :文本编码器
- 归一化后的向量模长为1,位于单位超球面上
对比学习目标:
给定一个batch的 个图像-文本对 :
相似度矩阵:
其中 是可学习的温度参数。
对比损失(双向):
图像到文本方向:
文本到图像方向:
总损失:
直观理解:
训练时:
Batch中的N个样本:(图像1, 文本1) ✓ 匹配(图像2, 文本2) ✓ 匹配...(图像N, 文本N) ✓ 匹配
目标:- 拉近匹配对:(图像1, 文本1)- 推远不匹配对:(图像1, 文本2), (图像1, 文本3), ...推理时(Zero-shot分类):
给定图像和K个类别:1. 为每个类别生成文本提示:"a photo of a {类别}"2. 编码图像和所有文本提示3. 计算相似度,选择最高的类别
示例:图像:一只猫的照片文本提示: - "a photo of a cat" → 相似度 0.85 ✓ - "a photo of a dog" → 相似度 0.23 - "a photo of a car" → 相似度 0.12预测:cat关键设计决策:
1. 为什么用双编码器而非单编码器?
- 灵活性:图像和文本可以独立编码,支持高效检索
- 可扩展性:可以预先编码大量图像/文本,存储嵌入
- 对称性:图像→文本和文本→图像都可以查询
2. 为什么用对比学习而非分类?
- 开放词汇:不限于固定类别,可以理解任意文本描述
- 零样本能力:无需针对新任务微调
- 语义对齐:学习图像和文本的共享语义空间
3. 为什么需要大规模数据?
- CLIP在4亿图像-文本对上训练
- 大规模数据提供丰富的视觉-语言关联
- 覆盖广泛的概念和场景
训练技巧:
1. 温度参数
- 初始化为可学习参数(如 )
- 控制softmax分布的锐度
- 训练过程中自动调整
2. 大batch size
- 典型值:32,768
- 更多负样本提升对比学习效果
- 需要分布式训练
3. 数据增强
- 图像:随机裁剪、颜色抖动
- 文本:保持原始描述(不增强)
CLIP的影响力:
1. 零样本迁移
- 在ImageNet上达到76.2%准确率(零样本)
- 超越许多有监督方法
2. 鲁棒性
- 对分布偏移更鲁棒
- 在多个数据集上表现稳定
3. 下游应用
- 文生图(Stable Diffusion使用CLIP文本编码器)
- 图像检索
- 视觉问答
- 多模态理解
关键洞察:
- CLIP证明了大规模弱监督学习的威力
- 自然语言提供了丰富的监督信号
- 对比学习是连接视觉和语言的有效桥梁
七、PyTorch 实现示例(补充内容)
说明:以下代码为作者根据公开资料实现,非教程原文。
7.1 SimCLR 核心代码
import torchimport torch.nn as nnimport torch.nn.functional as F
class SimCLR(nn.Module): def __init__(self, base_encoder, projection_dim=128): super().__init__() self.encoder = base_encoder
# 投影头(Projection Head):SimCLR的关键设计 # 重要发现:在投影后的空间(z)计算对比损失, # 能保留编码器输出层(h)的更多语义信息用于下游任务。 # 这是SimCLR论文中最反直觉但也最重要的发现之一: # - 对比学习在投影空间(z)效果更好 # - 但下游任务使用表征层(h)效果更好 # - 投影头在预训练后可以丢弃 self.projection = nn.Sequential( nn.Linear(2048, 2048), nn.ReLU(), nn.Linear(2048, projection_dim) )
def forward(self, x_i, x_j): # 编码:提取特征表征(h) h_i = self.encoder(x_i) h_j = self.encoder(x_j)
# 投影:映射到对比学习空间(z) z_i = self.projection(h_i) z_j = self.projection(h_j)
return z_i, z_j
def nt_xent_loss(z_i, z_j, temperature=0.5): """ NT-Xent (Normalized Temperature-scaled Cross Entropy) 损失函数
这是SimCLR的核心损失函数,用于对比学习。
参数: z_i: 第一组增强视图的投影向量,形状 [N, D] z_j: 第二组增强视图的投影向量,形状 [N, D] temperature: 温度参数,控制分布的平滑程度
返回: loss: 标量损失值 """ # ============================================================ # 步骤1: 获取batch size # ============================================================ batch_size = z_i.shape[0] # N # 说明:batch中有N个样本,每个样本有两个增强视图(z_i, z_j) # 因此总共有2N个视图
# ============================================================ # 步骤2: L2归一化 # ============================================================ z_i = F.normalize(z_i, dim=1) # [N, D],每个向量的L2范数为1 z_j = F.normalize(z_j, dim=1) # [N, D],每个向量的L2范数为1 # 为什么归一化? # 1. 使得相似度计算变为余弦相似度(只关注方向,不关注模长) # 2. 数值稳定性:避免指数运算时的数值溢出 # 3. 几何解释:将所有向量投影到单位超球面上
# ============================================================ # 步骤3: 拼接两组视图 # ============================================================ z = torch.cat([z_i, z_j], dim=0) # [2N, D] # 说明:将两组视图拼接成一个大矩阵 # 前N行是z_i,后N行是z_j # 示例:如果batch_size=4,则z的形状为[8, D] # z[0:4] = z_i (第一组增强) # z[4:8] = z_j (第二组增强)
# ============================================================ # 步骤4: 计算相似度矩阵 # ============================================================ sim_matrix = torch.mm(z, z.T) / temperature # [2N, 2N] # torch.mm(z, z.T): 计算所有向量对之间的点积 # 由于向量已归一化,点积等于余弦相似度 # 除以temperature: 控制softmax分布的锐度 # - temperature小:分布更尖锐,模型更关注最相似的样本 # - temperature大:分布更平滑,模型考虑更多样本 # # sim_matrix[i, j] = cos(z[i], z[j]) / temperature # # 矩阵结构示例(N=2): # z_i[0] z_i[1] z_j[0] z_j[1] # z_i[0] [1.0 0.3 0.9 0.2 ] ← z_i[0]与所有向量的相似度 # z_i[1] [0.3 1.0 0.2 0.8 ] ← z_i[1]与所有向量的相似度 # z_j[0] [0.9 0.2 1.0 0.3 ] ← z_j[0]与所有向量的相似度 # z_j[1] [0.2 0.8 0.3 1.0 ] ← z_j[1]与所有向量的相似度 # # 注意:对角线是自己和自己的相似度(总是1.0)
# ============================================================ # 步骤5: 创建标签(正样本对的索引) # ============================================================ labels = torch.arange(batch_size).to(z.device) # [0, 1, 2, ..., N-1] labels = torch.cat([labels + batch_size, labels], dim=0) # [2N] # 第一部分:labels + batch_size = [N, N+1, N+2, ..., 2N-1] # 第二部分:labels = [0, 1, 2, ..., N-1] # 拼接后:[N, N+1, ..., 2N-1, 0, 1, ..., N-1] # # 含义:对于每个查询向量,指定其正样本对的位置 # - z_i[0]的正样本是z_j[0],位置是N # - z_i[1]的正样本是z_j[1],位置是N+1 # - z_j[0]的正样本是z_i[0],位置是0 # - z_j[1]的正样本是z_i[1],位置是1 # # 示例(N=2): # labels = [2, 3, 0, 1] # z[0]=z_i[0] → 正样本是z[2]=z_j[0] # z[1]=z_i[1] → 正样本是z[3]=z_j[1] # z[2]=z_j[0] → 正样本是z[0]=z_i[0] # z[3]=z_j[1] → 正样本是z[1]=z_i[1]
# ============================================================ # 步骤6: 移除对角线(自己和自己的相似度) # ============================================================ mask = torch.eye(2 * batch_size, dtype=torch.bool).to(z.device) # [2N, 2N] # torch.eye创建单位矩阵(对角线为True,其他为False) # # 示例(N=2): # mask = [[True, False, False, False], # [False, True, False, False], # [False, False, True, False], # [False, False, False, True ]]
sim_matrix = sim_matrix.masked_fill(mask, -9e15) # [2N, 2N] # masked_fill: 将mask为True的位置填充为-9e15(近似负无穷) # 为什么要移除对角线? # 1. 自己和自己的相似度总是最高(1.0),这是trivial的 # 2. 如果不移除,模型会学到"选择自己",而不是学习有意义的表征 # 3. -9e15经过softmax后约等于0,完全屏蔽这些位置 # # 为什么用-9e15而不是0? # - 因为mask是加在softmax之前的logits上 # - softmax(-9e15) ≈ 0,而softmax(0) ≈ 1/N(仍有概率)
# ============================================================ # 步骤7: 计算交叉熵损失 # ============================================================ loss = F.cross_entropy(sim_matrix, labels) # cross_entropy做了两件事: # 1. 对sim_matrix的每一行应用softmax,得到概率分布 # P(j|i) = exp(sim[i,j]) / Σ_k exp(sim[i,k]) # 2. 计算负对数似然:-log P(labels[i] | i) # # 直观理解: # - 对于每个查询向量i,我们希望它的正样本对labels[i]的概率最大 # - 损失函数鼓励正样本对的相似度高,负样本对的相似度低 # # 数学形式: # loss = -1/(2N) Σ_i log [exp(sim[i, labels[i]]) / Σ_j exp(sim[i, j])] # # 为什么这等价于对比学习? # - 分子:正样本对的相似度 # - 分母:所有样本对的相似度之和 # - 最大化这个比例 ⟺ 拉近正样本,推远负样本
return loss
# 训练循环示例model = SimCLR(base_encoder=resnet50())optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(num_epochs): for (x_i, x_j) in dataloader: # 前向传播 z_i, z_j = model(x_i, x_j)
# 计算损失 loss = nt_xent_loss(z_i, z_j)
# 反向传播 optimizer.zero_grad() loss.backward() optimizer.step()7.2 掩码语言模型示例
import torchimport torch.nn as nn
class MaskedLanguageModel(nn.Module): def __init__(self, vocab_size, d_model=768): super().__init__() self.transformer = nn.TransformerEncoder( nn.TransformerEncoderLayer(d_model, nhead=12), num_layers=12 ) self.lm_head = nn.Linear(d_model, vocab_size)
def forward(self, input_ids, masked_positions): # 编码 hidden_states = self.transformer(input_ids)
# 只预测被遮蔽的位置 masked_hidden = hidden_states[masked_positions]
# 预测词表分布 logits = self.lm_head(masked_hidden)
return logits
def create_masked_lm_data(tokens, mask_prob=0.15): """创建掩码语言模型的训练数据""" masked_tokens = tokens.clone() labels = tokens.clone()
# 随机选择要遮蔽的位置 mask = torch.rand(tokens.shape) < mask_prob
# 80% 替换为 [MASK] mask_token_indices = mask & (torch.rand(tokens.shape) < 0.8) masked_tokens[mask_token_indices] = MASK_TOKEN_ID
# 10% 替换为随机词 random_token_indices = mask & (torch.rand(tokens.shape) < 0.5) masked_tokens[random_token_indices] = torch.randint( 0, vocab_size, masked_tokens.shape )[random_token_indices]
# 10% 保持不变(已经是原始词)
# 只计算被遮蔽位置的损失 labels[~mask] = -100 # 忽略未遮蔽的位置
return masked_tokens, labels八、总结与展望
核心要点回顾
自监督学习的本质:
- 从数据本身构造监督信号,无需人工标注
- 是无监督学习的特殊实现方式
- 通过预文本任务学习通用表征
教程重点:生成式自监督学习:
-
BERT(双向):掩码语言模型,适合理解任务
- 预训练:MLM + NSP
- 微调:添加任务层,端到端训练
- 优势:上下文相关表征,可解释性强
-
GPT(单向):自回归语言模型,适合生成任务
- 预训练:根据前文预测下一个词
- 使用:提示学习,上下文学习
- 优势:规模化带来涌现能力
补充内容:对比学习:
- SimCLR、MoCo 等方法在视觉领域取得成功
- 核心思想:拉近相似样本,推远不相似样本
- 关键:数据增强、投影头、负样本采样
实践建议
选择合适的方法:
- 文本理解任务:使用 BERT 风格的预训练
- 文本生成任务:使用 GPT 风格的预训练
- 图像任务:考虑 MAE 或对比学习方法
- 多模态任务:考虑 CLIP 或类似方法
训练技巧:
- 使用足够大的预训练数据
- 合适的学习率调度策略
- 对比学习需要强数据增强
- 生成式方法需要足够的训练时间
评估方法:
- 线性评估(Linear Probing)
- 微调评估(Fine-tuning)
- 零样本评估(Zero-shot)
- 下游任务性能
参考资源
教程来源:
[1] 李宏毅,《李宏毅深度学习教程》,第10章”自监督学习”
经典论文:
[2] Devlin J, Chang M W, Lee K, et al. BERT: Pre-training of deep bidirectional transformers for language understanding[J]. arXiv preprint arXiv:1810.04805, 2018.
[3] Radford A, Narasimhan K, Salimans T, et al. Improving language understanding by generative pre-training[J]. 2018.
[4] Chen T, Kornblith S, Norouzi M, et al. A simple framework for contrastive learning of visual representations[C]//ICML, 2020.
[5] He K, Fan H, Wu Y, et al. Momentum contrast for unsupervised visual representation learning[C]//CVPR, 2020.
[6] He K, Chen X, Xie S, et al. Masked autoencoders are scalable vision learners[C]//CVPR, 2022.
[7] Radford A, Kim J W, Hallacy C, et al. Learning transferable visual models from natural language supervision[C]//ICML, 2021.
推荐阅读:
- Self-supervised Learning: The Dark Matter of Intelligence
- Lil’Log: Contrastive Representation Learning
开源实现:
- PyTorch SimCLR
- MoCo Official Implementation
- MAE Official Implementation
- Hugging Face Transformers(BERT、GPT等模型)
致谢
本笔记基于李宏毅老师的深度学习教程整理而成,重点学习了BERT和GPT两大自监督学习方法。感谢李宏毅老师的精彩讲解,以及所有为自监督学习领域做出贡献的研究者们。
自监督学习正在改变深度学习的范式,让 AI 能够从海量无标注数据中学习,这是通往通用人工智能的重要一步。
部分信息可能已经过时









