自然语言处理(六)—— GPT 与生成式语言模型
Chen Kai BOSS

如果说 BERT 开启了理解式 NLP 的黄金时代,那么 GPT 系列则代表了生成式 NLP 的巅峰。从 2018 年的 GPT-1 到 2023 年的 GPT-4, OpenAI 通过不断放大模型规模和优化训练策略,证明了自回归语言模型可以成为通用人工智能的基础。 GPT 的成功不仅在于其强大的文本生成能力,更在于它展示了上下文学习( In-Context Learning)的神奇力量:模型可以在不更新参数的情况下,仅通过几个示例就学会新任务。

GPT 的核心是自回归语言建模:给定前面的 token,预测下一个 token 。这种看似简单的目标,配合 Transformer 解码器架构和大规模数据训练,产生了令人惊叹的涌现能力。理解 GPT 不仅是理解现代大语言模型的关键,更是探索 AI 通用智能的起点。

本文将深入探讨 GPT 系列的演进历程、自回归语言建模的原理、各种解码策略、上下文学习机制,以及如何评估生成质量。我们还将通过实战代码构建一个对话系统,展示 GPT 在实际应用中的强大能力。

GPT 系列的演进历程

GPT 系列的发展可以看作是一个不断"放大"的过程:更大的模型、更多的数据、更长的训练时间,最终产生了质的飞跃。

GPT-1: Transformer 解码器的首次预训练

2018 年 6 月, OpenAI 发布了 GPT-1( Generative Pre-trained Transformer),这是第一个将 Transformer 架构用于大规模预训练的语言模型。

架构特点: - 基于 Transformer 解码器( 12 层) - 单向注意力机制,只能看到左侧上下文 - 参数量: 117M - 预训练数据: BooksCorpus(约 7,000 本书, 4.5GB)

预训练目标

GPT-1 在多个任务上通过微调取得了不错的效果,但真正的突破在于它证明了预训练的语言模型可以作为通用特征提取器

GPT-2:零样本学习的探索

2019 年 2 月, OpenAI 发布了 GPT-2,这是一个重要的转折点。 GPT-2 的基本思路:语言模型应该能够执行任何语言任务,而不需要任务特定的架构修改

关键改进: - 更大的模型: GPT-2 Small (117M) 到 GPT-2 XL (1.5B) - 更大的数据: WebText( 800 万网页, 40GB) - 零样本学习:不进行微调,直接通过提示( prompt)执行任务

GPT-2 展示了语言模型的零样本学习能力:只需给出任务描述和示例,模型就能理解并执行任务。例如:

1
2
3
4
5
翻译成法语:
英语: The cat sat on the mat.
法语: Le chat s'est assis sur le tapis.
英语: The dog ran in the park.
法语:

GPT-2 会生成 "Le chien a couru dans le parc.",尽管它从未在翻译数据上训练过。

GPT-3:规模带来的涌现能力

2020 年 5 月, OpenAI 发布了 GPT-3,这是第一个真正意义上的"大语言模型"。 GPT-3 的参数量达到了 175B( 1750 亿),是 GPT-2 最大版本的 100 多倍。

关键特性: - 少样本学习( Few-shot Learning):通过几个示例就能学会新任务 - 上下文学习( In-Context Learning):不需要梯度更新,仅通过上下文就能适应任务 - 指令遵循:能够理解并遵循自然语言指令

GPT-3 展示了规模带来的涌现能力: - 参数量达到一定阈值后,模型突然获得了之前没有的能力 - 这些能力不是显式编程的,而是从数据中自然涌现的

GPT-3 的规模对比

模型 参数量 训练数据 主要能力
GPT-1 117M 4.5GB 微调适应任务
GPT-2 1.5B 40GB 零样本学习
GPT-3 175B 570GB 少样本学习、代码生成

GPT-4:多模态与指令优化

2023 年 3 月, OpenAI 发布了 GPT-4,虽然具体架构细节未公开,但已知的关键特性包括:

  • 多模态能力:可以处理文本和图像输入
  • 更强的指令遵循:通过强化学习从人类反馈( RLHF)优化
  • 更长的上下文:支持更长的输入序列
  • 更好的安全性:减少了有害输出

GPT-4 代表了当前大语言模型的最高水平,在多个基准测试上达到了人类水平或接近人类水平的性能。

自回归语言建模原理

GPT 的核心是自回归语言建模( Autoregressive Language Modeling)。理解这个原理是理解 GPT 的关键。

自回归的直觉理解

什么是自回归?

想象你在给朋友讲故事。你不会一次性说完整个故事,而是一句接一句地讲。每一句话都基于之前讲过的内容,这就是自回归的本质:当前的输出依赖于之前的所有输出

数学上的自回归

在时间序列分析中,自回归模型假设当前值可以由历史值的线性组合预测:

对于语言模型,自回归假设每个词的概率只依赖于前面的词:

其中 表示位置 之前的所有 token 。

为什么这个假设合理?

  1. 语言的顺序性:人类说话和写作都是顺序进行的,后面的词依赖于前面的词
  2. 概率链式法则:这是概率论的基本规则,没有做任何额外假设
  3. 生成的自然性:逐词生成符合人类的语言产生过程

实例

考虑句子 "The cat sat on the mat"。自回归模型会:

  1. 首先预测$P(x_1 = )P(x_2 = | x_1 = )P(x_3 = | x_1 = , x_2 = )$4. 依此类推...

总概率为:

自回归 vs 非自回归

自回归模型( GPT): - 生成: - 每一步依赖于前面的所有步骤 - 必须顺序生成,无法并行

非自回归模型( BERT): - 编码:同时看到所有输入 - 可以并行处理 - 但无法自然地用于生成任务

权衡: - ✅ 自回归的优势:自然适合生成任务,生成质量高 - ⚠️ 自回归的劣势:生成速度慢(必须顺序生成) - ✅ 非自回归的优势:编码速度快(可并行) - ⚠️ 非自回归的劣势:不适合生成任务(需要特殊设计)

自回归的定义

自回归模型假设序列中的每个元素都依赖于前面的元素:

其中 表示位置 之前的所有 token 。

Transformer 解码器架构

GPT 使用 Transformer 解码器架构,与编码器的关键区别在于掩码自注意力( Masked Self-Attention)。

掩码自注意力的作用: - 防止模型在预测位置 的 token 时看到位置 及之后的信息 - 确保训练和推理的一致性:训练时只能看到前面的 token,推理时也是如此

掩码自注意力的数学表达:

其中 是掩码矩阵:

这样,位置 只能关注到位置 的信息。

位置编码

GPT 使用可学习的位置编码,与 BERT 相同。位置编码与 token 嵌入相加:

前向传播过程

给定输入序列, GPT 的前向传播过程:

  1. 嵌入层:将 token 转换为向量

  2. Transformer 块(重复 次):

    • 掩码自注意力
    • 残差连接和层归一化
    • 前馈网络
    • 残差连接和层归一化
  3. 输出层:预测下一个 token 的概率分布

其中 是位置 的隐状态, 是输出权重矩阵。

训练目标

GPT 的训练目标是最大化似然函数:

在实现中,通常使用交叉熵损失:𝟙

其中 是词汇表大小,𝟙 是指示函数。

解码策略

GPT 生成文本的过程是自回归解码:每次生成一个 token,然后将其作为输入继续生成下一个 token 。不同的解码策略会产生不同的生成效果。

解码策略的决策树:如何选择?

在开始详细介绍各种策略之前,先给出一个实用的决策指南:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
你的任务是什么?

├─ 需要确定性结果(如代码生成、摘要提取)
│ ├─ 只要最佳答案 → 贪婪解码
│ └─ 要多个高质量候选 → Beam Search

├─ 需要创意性内容(如故事生成、对话)
│ ├─ 控制质量下限 → Top-k 采样( k=30-50)
│ └─ 平衡质量和多样性 → Top-p 采样( p=0.9)

└─ 混合需求
└─ Top-p + Temperature 调整
- 更保守( T=0.7)
- 更创意( T=1.2)

快速选择指南

任务类型 推荐策略 参数建议
代码补全 Greedy -
摘要生成 Beam Search beam_size=3-5
机器翻译 Beam Search beam_size=5
对话生成 Top-p p=0.9, T=0.8
故事创作 Top-p p=0.9, T=1.0-1.2
问答 Top-k k=10-20, T=0.7

现在让我们深入理解每种策略的原理。

贪婪解码( Greedy Decoding)

问题背景:生成文本时,需要在每一步选择下一个 token 。最简单的方法是每次都选择概率最高的 token,这就是贪婪解码。

解决思路:在每个时间步,计算所有可能 token 的概率分布,选择概率最高的 token 。这种方法简单直接,计算效率高。

设计考虑: - 贪婪解码是确定性的,相同输入总是产生相同输出 - 适合需要确定性结果的场景(如代码生成) - 但可能陷入局部最优,产生重复或单调的文本

最简单的策略是每次都选择概率最高的 token:

优点:简单快速,确定性输出

缺点:容易产生重复和单调的文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def greedy_decode(model, tokenizer, prompt, max_length=100):
"""
贪婪解码:每一步选择概率最高的 token

问题:如何生成文本?
解决:自回归生成,每一步选择概率最高的 token

Args:
model: GPT 模型
tokenizer: 分词器
prompt: 输入提示文本
max_length: 最大生成长度

Returns:
生成的文本
"""
# 将提示文本转换为 token IDs
# inputs['input_ids']: [batch_size, seq_len]
inputs = tokenizer(prompt, return_tensors='pt')
generated = inputs['input_ids'].clone() # 复制输入,避免修改原始 tensor

# 自回归生成循环
for _ in range(max_length):
# 前向传播:计算下一个 token 的概率分布
outputs = model(generated)
# outputs.logits: [batch_size, seq_len, vocab_size]
# 取最后一个位置的 logits: [batch_size, vocab_size]
logits = outputs.logits[:, -1, :]

# 选择概率最高的 token(贪婪选择)
# next_token: [batch_size]
next_token = torch.argmax(logits, dim=-1)

# 将新 token 添加到生成序列
# next_token.unsqueeze(0): [1, batch_size]
# 需要转置以匹配 generated 的形状
generated = torch.cat([generated, next_token.unsqueeze(0).T], dim=1)

# 如果生成结束 token,停止生成
if next_token.item() == tokenizer.eos_token_id:
break

# 将 token IDs 转换回文本
return tokenizer.decode(generated[0], skip_special_tokens=True)

关键点解读: - 自回归生成:每次生成一个 token,然后将其作为输入继续生成下一个 token - 贪婪选择argmax选择概率最高的 token,不考虑其他可能性 - 序列扩展:每次将新 token 拼接到已生成序列,形成新的输入

设计权衡: - ✅ 优点:简单快速,确定性输出,适合需要可重复结果的场景 - ⚠️ 缺点:容易产生重复(如"the the the..."),缺乏多样性

常见问题: - Q: 为什么会产生重复? A: 贪婪解码可能陷入局部最优,重复选择相同的 token - Q: 如何避免重复? A: 可以使用no_repeat_ngram_size参数,或使用采样策略

使用示例

1
2
3
4
5
6
# 使用示例
model = GPT2LMHeadModel.from_pretrained('gpt2')
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
prompt = "The future of AI is"
generated = greedy_decode(model, tokenizer, prompt, max_length=50)
print(generated)

Beam Search 维护 个最有可能的序列( beam),每一步扩展这些序列:

算法流程: 1. 初始化: beam 包含起始 token 2. 扩展:对每个 beam,生成所有可能的下一 token,计算分数 3. 选择:保留分数最高的 个序列 4. 重复步骤 2-3,直到生成结束 token 或达到最大长度

分数计算

为了避免偏向短序列,通常使用长度归一化:

其中 是长度惩罚系数(通常为 0.6-0.7)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def beam_search(model, tokenizer, prompt, beam_size=5, max_length=100, length_penalty=0.6):
inputs = tokenizer(prompt, return_tensors='pt')
beams = [(inputs['input_ids'], 0.0)] # (sequence, score)

for _ in range(max_length):
candidates = []
for seq, score in beams:
if seq[0, -1].item() == tokenizer.eos_token_id:
candidates.append((seq, score))
continue

outputs = model(seq)
logits = outputs.logits[:, -1, :]
probs = torch.log_softmax(logits, dim=-1)
top_k_probs, top_k_indices = torch.topk(probs, beam_size, dim=-1)

for prob, idx in zip(top_k_probs[0], top_k_indices[0]):
new_seq = torch.cat([seq, idx.unsqueeze(0).unsqueeze(0)], dim=1)
new_score = score + prob.item()
candidates.append((new_seq, new_score))

# 选择 top-k
candidates.sort(key=lambda x: x[1] / (len(x[0][0]) ** length_penalty), reverse=True)
beams = candidates[:beam_size]

# 检查是否全部结束
if all(seq[0, -1].item() == tokenizer.eos_token_id for seq, _ in beams):
break

best_seq = beams[0][0]
return tokenizer.decode(best_seq[0], skip_special_tokens=True)

优点:通常比贪婪解码生成更好的文本

缺点:计算成本高,可能产生过于保守的文本

Top-k 采样

问题背景:贪婪解码缺乏多样性,而完全随机采样可能产生低质量的文本。 Top-k 采样在质量和多样性之间取得平衡。

解决思路:只从概率最高的 k 个 token 中采样,既保证了质量(排除低概率 token),又增加了多样性(在 top-k 中随机选择)。

设计考虑: - k 值的选择很重要:太小(如 k=1)退化为贪婪解码,太大(如 k=vocab_size)退化为完全随机 - 需要重新归一化 top-k 的概率,确保概率和为 1 - Temperature 参数可以进一步控制分布的尖锐程度

Top-k 采样限制候选 token 只从概率最高的 个中选择:

算法: 1. 计算所有 token 的概率分布 2. 选择概率最高的 个 token 3. 重新归一化这 个 token 的概率 4. 根据归一化后的概率采样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def top_k_decode(model, tokenizer, prompt, k=50, max_length=100, temperature=1.0):
"""
Top-k 采样:从概率最高的 k 个 token 中采样

问题:如何在保证质量的同时增加多样性?
解决:只考虑概率最高的 k 个 token,在它们之间采样

Args:
model: GPT 模型
tokenizer: 分词器
prompt: 输入提示文本
k: Top-k 的 k 值,控制候选 token 数量
max_length: 最大生成长度
temperature: 温度参数,控制分布的尖锐程度
- T < 1: 分布更尖锐,更保守
- T = 1: 标准 softmax
- T > 1: 分布更平滑,更多样

Returns:
生成的文本
"""
inputs = tokenizer(prompt, return_tensors='pt')
generated = inputs['input_ids'].clone()

for _ in range(max_length):
outputs = model(generated)
# 应用 temperature 缩放: T 越大,分布越平滑
# logits / temperature 相当于调整 softmax 的"温度"
logits = outputs.logits[:, -1, :] / temperature
# 计算概率分布: [batch_size, vocab_size]
probs = torch.softmax(logits, dim=-1)

# Top-k 选择:选择概率最高的 k 个 token
# top_k_probs: [batch_size, k], top_k_indices: [batch_size, k]
top_k_probs, top_k_indices = torch.topk(probs, k, dim=-1)

# 重新归一化:确保 top-k 的概率和为 1
# top_k_probs: [batch_size, k]
top_k_probs = top_k_probs / top_k_probs.sum(dim=-1, keepdim=True)

# 从 top-k 中采样
# multinomial 根据概率分布采样,返回索引(在 top-k 中的位置)
next_token_idx = torch.multinomial(top_k_probs, 1) # [batch_size, 1]
# 根据索引找到对应的 token ID
next_token_id = top_k_indices[0, next_token_idx[0, 0]].item()

# 添加到生成序列
generated = torch.cat([generated, torch.tensor([[next_token_id]])], dim=1)

if next_token_id == tokenizer.eos_token_id:
break

return tokenizer.decode(generated[0], skip_special_tokens=True)

关键点解读: - Top-k 选择torch.topk选择概率最高的 k 个 token,排除低概率 token - 重新归一化:确保采样概率和为 1,符合概率分布的定义 - Temperature 缩放:通过调整 temperature 控制分布的尖锐程度,影响多样性

设计权衡: - ✅ 优点:平衡质量和多样性,排除低质量 token,增加生成多样性 - ⚠️ 注意: k 值需要根据任务调整,固定 k 可能不适合所有上下文

常见问题: - Q: k 值如何选择? A: 通常选择 10-100,根据 vocab 大小和任务调整。可以尝试 k=50 作为起点 - Q: Temperature 的作用? A: Temperature 控制分布的平滑程度, T 越大越多样,但可能降低质量

使用示例

1
2
3
4
# 不同 k 值的对比
text1 = top_k_decode(model, tokenizer, prompt, k=10) # 更保守
text2 = top_k_decode(model, tokenizer, prompt, k=50) # 平衡
text3 = top_k_decode(model, tokenizer, prompt, k=100) # 更多样

优点:平衡了多样性和质量

缺点 的选择需要调优,固定 可能不适合所有情况

Top-p( Nucleus)采样

Top-p 采样(也称为 Nucleus Sampling)是 Top-k 的改进版本,动态选择概率累积达到 的最小 token 集合。

算法: 1. 按概率从高到低排序所有 token 2. 选择概率累积和达到 的最小 token 集合 3. 重新归一化这些 token 的概率 4. 根据归一化后的概率采样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def top_p_decode(model, tokenizer, prompt, p=0.9, max_length=100, temperature=1.0):
inputs = tokenizer(prompt, return_tensors='pt')
generated = inputs['input_ids'].clone()

for _ in range(max_length):
outputs = model(generated)
logits = outputs.logits[:, -1, :] / temperature
probs = torch.softmax(logits, dim=-1)

# 排序
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
cumsum_probs = torch.cumsum(sorted_probs, dim=-1)

# 找到累积概率达到 p 的位置
mask = cumsum_probs <= p
mask[0] = True # 至少保留一个

# 过滤和归一化
filtered_probs = sorted_probs[mask]
filtered_indices = sorted_indices[mask]
filtered_probs = filtered_probs / filtered_probs.sum()

# 采样
next_token_idx = torch.multinomial(filtered_probs.unsqueeze(0), 1)
next_token_id = filtered_indices[next_token_idx].item()

generated = torch.cat([generated, torch.tensor([[next_token_id]])], dim=1)

if next_token_id == tokenizer.eos_token_id:
break

return tokenizer.decode(generated[0], skip_special_tokens=True)

优点: - 自适应:根据概率分布动态调整候选数量 - 更灵活:在不同上下文中自动适应

缺点:计算稍复杂

Temperature 参数

Temperature 参数控制采样的随机性:

其中 是 logits, 是 temperature 。

  • :分布更尖锐,输出更确定(更保守)
  • :标准 softmax
  • :分布更平滑,输出更多样(更有创造性)

解码策略对比

策略 多样性 质量 速度 适用场景
Greedy 中等 确定性任务
Beam Search 质量优先
Top-k 中高 平衡场景
Top-p 中高 中高 创意生成

Zero-shot 、 Few-shot 和 In-Context Learning

GPT 系列最令人惊叹的能力之一是上下文学习( In-Context Learning):模型可以在不更新参数的情况下,仅通过几个示例就学会新任务。

上下文学习的机制:它是如何工作的?

核心直觉:模式识别与泛化

想象你在学习一门外语。老师没有教你语法规则,而是给你看了几个例句:

1
2
3
示例 1: apple → 苹果
示例 2: banana → 香蕉
示例 3: orange →

即使没人告诉你"这是翻译任务",你也能推断出应该填"橙子"。这种能力来自于: 1. 模式识别:识别出输入输出的对应关系 2. 归纳推理:从少量示例推断出通用规则 3. 应用:将规则应用到新样本

GPT 的上下文学习正是这个过程的自动化。

数学视角:条件概率的精妙之处

从数学上看,上下文学习是利用了条件概率的强大表达能力。

完整的概率分解GPT 通过自回归建模学习到了这种条件概率分布。关键洞察:模型在预训练时见过大量"示例-查询"的模式,学会了如何从示例中提取规则并应用

为什么大模型能做到上下文学习?

规模带来的涌现能力

研究表明,上下文学习能力在模型规模达到一定阈值后突然涌现。具体来说:

模型规模 上下文学习能力
< 1B 参数 几乎没有
1B - 10B 弱上下文学习
10B - 100B 中等上下文学习
> 100B(如 GPT-3) 强上下文学习

可能的机制解释

  1. 隐式元学习假说

    • 预训练时,模型见过无数次"在上下文中快速适应"的场景
    • 学习到了一种通用的"学习算法"
    • 前向传播时,注意力机制隐式执行了类似梯度下降的优化

    数学形式:注意力机制可以被看作是一种软查找表,通过关注相似示例来"更新"内部表示

  2. 模式匹配假说

    • 模型记住了预训练数据中的大量模式
    • 上下文学习是在这些记忆模式中进行匹配和组合
  3. 表示空间假说

    • 大模型学习到了一个结构化的表示空间
    • 少量示例足以在这个空间中定位任务的"子空间"
    • 模型沿着这个子空间生成输出

实验证据:上下文学习的惊人效果

数学推理示例

1
2
3
4
示例 1: 2 + 3 = 5
示例 2: 7 + 11 = 18
示例 3: 15 + 22 = 37
现在: 9 + 6 = ?

GPT-3 在没有任何算术训练的情况下,能够正确回答"15"。这说明模型学会了: - 识别加法模式 - 理解等号的含义 - 执行(或模拟执行)加法运算

逻辑推理示例

1
2
3
示例 1:如果今天下雨,地面会湿。今天下雨了。结论:地面湿了。
示例 2:如果小明学习,他会进步。小明学习了。结论:小明进步了。
现在:如果太阳升起,天会亮。太阳升起了。结论:?

GPT-3 能够推断出"天亮了",展示了三段论推理能力。

上下文学习的局限性

尽管强大,上下文学习仍有局限:

  1. 示例敏感性
    • 示例的顺序会影响结果
    • 示例的质量直接影响性能
    • 最优示例选择是一个难题
  2. 任务难度限制
    • 对于非常复杂的任务,少量示例不足以传达任务语义
    • 需要大量上下文的任务受限于模型的上下文窗口长度
  3. 无法真正"学习"新知识
    • 参数不更新,无法吸收示例中的知识到模型中
    • 每次推理都需要重新提供示例
  4. 计算成本
    • 每次推理都需要处理长上下文(包含所有示例)
    • 相比微调,推理成本更高

Zero-shot Learning

零样本学习是指模型在没有任务特定训练数据的情况下执行任务,仅依靠任务描述或提示。

定义与示例

定义:只给出任务描述,不提供示例,让模型直接执行任务。

示例

1
2
将以下英文翻译成中文:
The cat sat on the mat.

模型需要: 1. 理解"翻译"这个任务的含义 2. 识别源语言(英文)和目标语言(中文) 3. 执行翻译

Zero-shot 能力的来源

GPT 的零样本能力来自于:

  1. 预训练数据的多样性
    • 预训练数据中包含了大量"任务描述 + 执行"的模式
    • 例如,网页中可能有"如何翻译..."后跟翻译示例
  2. 指令理解
    • 模型学会了理解自然语言指令
    • "翻译"、"总结"、"分类"等动词被映射到对应的操作
  3. 知识的隐式存储
    • 模型参数中隐式存储了翻译、推理等能力
    • 指令只是"激活"这些能力

实践技巧

清晰的指令

1
2
✅ 好:将以下英文句子翻译成中文:
❌ 差:翻译:

格式提示

1
2
✅ 好:问题:... 答案:
❌ 差:(直接给问题,不说明期望格式)

Few-shot Learning

少样本学习是指模型通过几个示例(通常 1-10 个)学习任务模式,然后应用到新样本。

定义与示例

定义:提供少量示例,让模型从中学习任务模式,然后应用到新样本。

示例

1
2
3
4
将以下英文翻译成中文:
示例 1: The cat sat on the mat. → 猫坐在垫子上。
示例 2: The dog ran in the park. → 狗在公园里跑。
现在翻译: The bird flew in the sky.

模型通过示例学习: - 翻译的目标语言(中文) - 翻译风格(直译 vs 意译) - 格式约定(句号的使用等)

Few-shot vs Zero-shot

性能对比(典型情况):

任务复杂度 Zero-shot 准确率 Few-shot 准确率( 5 个示例) 提升幅度
简单分类 70% 85% +15%
翻译 50% 75% +25%
逻辑推理 40% 60% +20%
复杂问答 30% 55% +25%

为什么 Few-shot 更好?

  1. 明确任务语义:示例比纯文字描述更精确地定义了任务
  2. 消除歧义:同一个指令可能有多种解释,示例消除了歧义
  3. 提供格式模板:示例展示了期望的输出格式
  4. 校准模型:示例帮助模型"定位"到正确的知识和能力

示例设计的最佳实践

1. 多样性

1
2
3
4
5
6
✅ 好:示例覆盖不同情况
示例 1:短句翻译
示例 2:长句翻译
示例 3:带有专有名词的句子

❌ 差:所有示例都很相似

2. 相关性

1
2
3
4
✅ 好:示例与查询任务相似
如果查询是翻译技术文档,示例也应该是技术领域的

❌ 差:示例与查询领域差异大

3. 顺序

1
2
✅ 好:从简单到复杂排列示例
❌ 差:随机顺序(可能影响性能)

4. 数量

1
2
3
4
一般建议:
- 简单任务: 1-3 个示例
- 中等任务: 3-5 个示例
- 复杂任务: 5-10 个示例

过多示例会: - 超出上下文窗口限制 - 增加推理成本 - 可能引入噪声

提示工程( Prompt Engineering)

提示工程是优化模型输入以获得更好输出的技术:

1. 任务描述 - 清晰描述任务目标 - 使用自然语言说明期望的输出格式

2. 示例选择 - 选择有代表性的示例 - 示例应该覆盖任务的多样性

3. 格式一致性 - 保持输入格式的一致性 - 使用清晰的分隔符

4. 思维链( Chain-of-Thought) - 对于复杂任务,引导模型逐步推理 - 示例中包含推理过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def few_shot_prompt(task_description, examples, query):
prompt = f"{task_description}\n\n"
for i, (input_text, output_text) in enumerate(examples, 1):
prompt += f"示例{i}{input_text}{output_text}\n"
prompt += f"\n 现在:{query} →"
return prompt

# 使用示例
task = "将以下英文翻译成中文:"
examples = [
("The cat sat on the mat.", "猫坐在垫子上。"),
("The dog ran in the park.", "狗在公园里跑。")
]
query = "The bird flew in the sky."

prompt = few_shot_prompt(task, examples, query)
response = model.generate(prompt)

生成质量评估

评估生成文本的质量是一个复杂的问题,通常需要结合多个指标。

BLEU 分数

BLEU( Bilingual Evaluation Understudy)最初用于机器翻译评估,通过比较生成文本和参考文本的 n-gram 重叠度计算。

计算过程: 1. 计算不同 n-gram( 1-gram 到 4-gram)的精确度 2. 应用长度惩罚( Brevity Penalty) 3. 计算几何平均

其中 是 n-gram 精确度, BP 是长度惩罚:

其中 是生成文本长度, 是参考文本长度。

1
2
3
4
5
6
7
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction

def calculate_bleu(generated, reference):
reference_tokens = [reference.split()]
generated_tokens = generated.split()
smoothing = SmoothingFunction().method1
return sentence_bleu(reference_tokens, generated_tokens, smoothing_function=smoothing)

优点:快速、客观

缺点:只考虑精确匹配,不考虑语义相似性

ROUGE 分数

ROUGE( Recall-Oriented Understudy for Gisting Evaluation)主要用于摘要评估,关注召回率。

ROUGE-N:计算 n-gram 的召回率

ROUGE-L:基于最长公共子序列( LCS)

1
2
3
4
5
6
7
8
9
10
from rouge_score import rouge_scorer

def calculate_rouge(generated, reference):
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
scores = scorer.score(reference, generated)
return {
'rouge1': scores['rouge1'].fmeasure,
'rouge2': scores['rouge2'].fmeasure,
'rougeL': scores['rougeL'].fmeasure
}

Perplexity(困惑度)

困惑度衡量模型对测试数据的预测不确定性:

较低的困惑度表示模型对数据更有信心。

1
2
3
4
5
6
def calculate_perplexity(model, tokenizer, text):
inputs = tokenizer(text, return_tensors='pt')
with torch.no_grad():
outputs = model(**inputs, labels=inputs['input_ids'])
loss = outputs.loss
return torch.exp(loss).item()

人工评估

人工评估仍然是最可靠的方法,通常评估: - 流畅性:文本是否自然流畅 - 相关性:是否与输入相关 - 准确性:信息是否正确 - 创造性:是否有新意

评估指标的选择

不同任务适合不同的指标:

任务 推荐指标
机器翻译 BLEU 、 METEOR
文本摘要 ROUGE 、 BLEU
对话系统 BLEU 、人工评估
创意写作 人工评估、多样性指标

实战:构建对话系统

下面展示如何使用 GPT 模型构建一个简单的对话系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from transformers import GPT2LMHeadModel, GPT2Tokenizer
import torch

class ChatBot:
def __init__(self, model_name='gpt2'):
self.tokenizer = GPT2Tokenizer.from_pretrained(model_name)
self.model = GPT2LMHeadModel.from_pretrained(model_name)
self.model.eval()

# 设置 pad_token
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token

def generate_response(self, user_input, max_length=100, temperature=0.7, top_p=0.9):
# 构建对话上下文
prompt = f"用户:{user_input}\n 助手:"

# 编码
inputs = self.tokenizer.encode(prompt, return_tensors='pt')

# 生成
with torch.no_grad():
outputs = self.model.generate(
inputs,
max_length=inputs.shape[1] + max_length,
temperature=temperature,
top_p=top_p,
do_sample=True,
pad_token_id=self.tokenizer.eos_token_id,
eos_token_id=self.tokenizer.eos_token_id,
no_repeat_ngram_size=2
)

# 解码
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
# 提取助手回复部分
response = response.split("助手:")[-1].strip()

return response

# 使用示例
chatbot = ChatBot()

while True:
user_input = input("你:")
if user_input.lower() in ['quit', 'exit', '退出']:
break
response = chatbot.generate_response(user_input)
print(f"助手:{response}")

改进:多轮对话上下文

为了支持多轮对话,需要维护对话历史:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MultiTurnChatBot:
def __init__(self, model_name='gpt2'):
self.tokenizer = GPT2Tokenizer.from_pretrained(model_name)
self.model = GPT2LMHeadModel.from_pretrained(model_name)
self.model.eval()
self.conversation_history = []

if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token

def add_to_history(self, user_input, assistant_response):
self.conversation_history.append(f"用户:{user_input}")
self.conversation_history.append(f"助手:{assistant_response}")

def build_context(self, user_input, max_history=5):
# 只保留最近的对话历史
recent_history = self.conversation_history[-max_history*2:]
context = "\n".join(recent_history)
context += f"\n 用户:{user_input}\n 助手:"
return context

def generate_response(self, user_input, max_length=100, temperature=0.7, top_p=0.9):
context = self.build_context(user_input)
inputs = self.tokenizer.encode(context, return_tensors='pt')

with torch.no_grad():
outputs = self.model.generate(
inputs,
max_length=inputs.shape[1] + max_length,
temperature=temperature,
top_p=top_p,
do_sample=True,
pad_token_id=self.tokenizer.eos_token_id,
eos_token_id=self.tokenizer.eos_token_id,
no_repeat_ngram_size=2
)

response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
response = response.split("助手:")[-1].strip()

self.add_to_history(user_input, response)
return response

使用 HuggingFace Pipeline

HuggingFace 提供了更简单的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from transformers import pipeline

generator = pipeline('text-generation', model='gpt2')

def chat_with_pipeline(user_input):
prompt = f"用户:{user_input}\n 助手:"
response = generator(
prompt,
max_length=len(prompt.split()) + 50,
temperature=0.7,
top_p=0.9,
do_sample=True,
num_return_sequences=1
)[0]['generated_text']

return response.split("助手:")[-1].strip()

GPT 的局限性

尽管 GPT 取得了巨大成功,但它也存在一些局限性:

1. 幻觉问题 - 模型可能生成看似合理但实际错误的信息 - 缺乏事实核查机制

2. 上下文长度限制 - 早期 GPT 模型的上下文窗口有限(如 GPT-3 的 2048 tokens) - 无法处理超长文档

3. 计算成本高 - 大模型需要大量计算资源 - 推理速度可能较慢

4. 训练数据偏见 - 模型可能学习到训练数据中的偏见 - 需要仔细的数据筛选和模型对齐

5. 可控性有限 - 难以精确控制生成内容 - 可能生成有害或不合适的内容

总结

GPT 系列代表了生成式语言模型的巅峰,通过自回归语言建模和 Transformer 架构,实现了强大的文本生成能力。从 GPT-1 到 GPT-4,模型规模的不断放大带来了涌现能力,特别是上下文学习能力,使得模型可以在不更新参数的情况下适应新任务。

GPT 的核心贡献: 1. 自回归语言建模:简单而强大的预训练目标 2. 上下文学习:零样本和少样本学习能力 3. 通用性:同一个模型可以处理多种任务

理解 GPT 不仅是理解现代大语言模型的关键,更是探索 AI 通用智能的起点。随着模型规模的继续增大和训练策略的不断优化,可以期待更强大的生成式 AI 系统。

❓ Q&A: GPT 常见问题

Q1: GPT 和 BERT 的主要区别是什么?

A: 主要区别: - 架构: GPT 是解码器(单向), BERT 是编码器(双向) - 预训练任务: GPT 使用语言建模, BERT 使用 MLM + NSP - 适用任务: GPT 擅长生成任务, BERT 擅长理解任务 - 上下文利用: GPT 只能看到前文, BERT 可以同时看到前后文

Q2: 为什么 GPT 使用掩码自注意力?

A: 掩码自注意力确保训练和推理的一致性: - 训练时,模型只能看到前面的 token 来预测当前 token - 推理时,模型也只能看到已生成的 token - 如果不使用掩码,模型在训练时就能"看到未来",导致训练和推理不一致

Q3: GPT 如何实现零样本学习?

A: GPT 的零样本学习能力来自: 1. 大规模预训练:在大量多样化数据上训练,见过各种任务格式 2. 模式匹配:通过识别任务描述和格式,匹配到相应的生成模式 3. 上下文理解: Transformer 架构能够理解长距离依赖,捕捉任务模式

Q4: Top-k 和 Top-p 采样有什么区别?

A: 主要区别: - Top-k:固定选择概率最高的 k 个 token - Top-p:动态选择概率累积达到 p 的 token 集合 - Top-p 更灵活:在不同上下文中自动调整候选数量 - Top-k 更简单:实现和理解都更直观

Q5: 如何选择合适的解码策略?

A: 选择建议: - 确定性任务(如代码补全): Greedy 或 Beam Search - 创意任务(如故事生成): Top-p 采样, temperature > 1 - 平衡场景: Top-k 或 Top-p, temperature ≈ 0.7-0.9 - 质量优先: Beam Search, beam_size = 3-5 - 速度优先: Greedy 或 Top-k( k 较小)

Q6: GPT 的上下文学习能力是如何产生的?

A: 上下文学习能力可能来自: 1. 预训练数据多样性:见过大量任务示例和格式 2. Transformer 注意力机制:能够关注相关示例并提取模式 3. 隐式元学习:在预训练过程中学习了如何快速适应 4. 规模效应:模型规模达到一定阈值后涌现出这种能力

Q7: 如何评估 GPT 生成文本的质量?

A: 评估方法: 1. 自动指标: BLEU 、 ROUGE 、 Perplexity 2. 人工评估:流畅性、相关性、准确性 3. 任务特定指标:根据具体任务选择合适指标 4. 组合评估:结合多个指标综合判断

Q8: GPT 模型为什么会出现幻觉?

A: 幻觉的原因: 1. 训练数据噪声:预训练数据包含错误信息 2. 概率性生成:采样过程可能选择低概率但错误的 token 3. 缺乏事实核查:模型没有显式的事实验证机制 4. 过度拟合模式:可能生成符合语言模式但不符合事实的内容

Q9: 如何减少 GPT 生成的有害内容?

A: 减少有害内容的方法: 1. 数据筛选:仔细筛选和清理训练数据 2. RLHF:使用强化学习从人类反馈优化模型 3. 安全提示:在提示中添加安全约束 4. 后处理过滤:对生成内容进行过滤和检查 5. 模型对齐:通过微调使模型遵循安全准则

Q10: GPT 的未来发展方向是什么?

A: 可能的发展方向: 1. 更大规模:继续增大模型和训练数据 2. 多模态:整合文本、图像、音频等多种模态 3. 更长上下文:支持处理更长的输入序列 4. 更好的可控性:精确控制生成内容和风格 5. 更高效:减少计算成本,提高推理速度 6. 更安全:减少偏见和有害内容 7. 专业化:针对特定领域优化模型

  • 本文标题:自然语言处理(六)—— GPT 与生成式语言模型
  • 本文作者:Chen Kai
  • 创建时间:2024-03-03 14:00:00
  • 本文链接:https://www.chenk.top/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86%EF%BC%88%E5%85%AD%EF%BC%89%E2%80%94%E2%80%94-GPT%E4%B8%8E%E7%94%9F%E6%88%90%E5%BC%8F%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论