目录
0 前言
本文为李宏毅学习笔记——2024春《GENERATIVE AI》篇——作业笔记HW5的补充内容1。
如果你还没获取到LLM API,请查看我的另一篇笔记:
HW1~2:LLM API获取步骤及LLM API使用演示:环境配置与多轮对话演示-CSDN博客
完整内容参见:
李宏毅学习笔记——2024春《GENERATIVE AI》篇
从示例到代码演示,讲解 Beam Search 的数学原理,这应该能解决一些之前阅读的困惑,最终提供一个简单的使用 Hugging Face Transformers 库的示例(如果跳过了之前的文章的话可以尝试它)。
Beam Search 是生成任务中常用的一种方法。在之前的文章中,我们都曾经用到过与它相关的参数,而对于早就有着实操经验的同学们,想必见到的更多。这篇文章将从示例到数学原理和代码带你进行理解。
Beam Search 对应的中文翻译为“集束搜索”或“束搜索”。你可以将其当作是贪心算法的拓展,其实是很简单的概念:贪心算法每次只选择最好的,而 Beam Search 会在多个候选中进行选择。通过这篇文章,你将了解到:
- Beam Width(束宽) 的实际作用,常对应于参数名
num_beams。- 所有候选序列生成结束标记的含义,常对应于参数名
early_stopping。- Beam Search 的基本原理和工作机制。
强烈建议访问:Beam Search Visualizer,这是一个非常 Amazing 的交互式项目。
我之前也吴恩达老师的深度学习课程“第五课:序列模型”章节也简单了解过集束搜索Beam Search,链接如下:
![]()
1 Beam Search的工作原理
Beam Search 是一种宽度优先搜索算法,通过保留多个候选序列来探索可能的输出空间,这与贪心算法每次只选择一个当前最优序列不同,可以将贪心算法当成 1 个候选序列下的 Beam Search。
具体来讲,每一步生成时,Beam Search 会保留束宽 k 个最有可能的候选序列(k=1 即贪心),并为每个候选序列计算它们的累积概率或对数概率。在每一步搜索时,Beam Search 会生成所有可能的下一个词汇,并从中选择得分最高的 k 个序列继续下一步。所以,束宽越大,搜索空间越广,计算成本越高。
以下是 Beam Search 的基本步骤:
- 初始化:从一个初始序列(通常为空或特殊起始标记)开始,设定束宽 k,初始化候选序列集 B0=start。
- 迭代生成:对于当前所有候选序列 Bt−1,扩展一个新的词汇或符号,生成所有可能的下一个词汇组合,并计算每个序列的概率。
- 选择顶束:从所有扩展的候选序列中,选择得分最高的 k 个序列,作为下一步的候选序列 Bt。
- 终止条件:当所有候选序列都生成了结束标记(如
)或达到设定的最大长度 T 时,停止生成。 - 选择最终序列:从最终的候选序列集中,选择得分最高的序列作为输出。
注:以 GPT 为例,扩展实际对应于去获取 tokens 的概率。
2 生成示例
为了清晰,这里使用累积概率进行得分的计算。
2.1 初始化
-
束宽 (k): 2
-
当前候选集 (B0): (空)
-
词汇表 A,B,C,
-
扩展(生成所有可能的下一个词汇):
| 扩展结果 | 概率 |
|---|---|
| A | 0.4 |
| B | 0.3 |
| C | 0.2 |
| 0.1 |
-
选择顶束 (k=2):
- A (0.4)
- B (0.3)
-
新的候选集 (B1): A(0.4),B(0.3)
2.2 扩展 A 和 B
-
扩展 A:
- 生成概率: A:0.3,B:0.1,C:0.4,
:0.2
- 生成概率: A:0.3,B:0.1,C:0.4,
| 扩展结果 | 概率计算 | 概率 |
|---|---|---|
| AA | 0.4×0.3 | 0.12 |
| AB | 0.4×0.1 | 0.04 |
| AC | 0.4×0.4 | 0.16 |
| A | 0.4×0.2 | 0.08 |
扩展 B:
- 生成概率: A:0.1,B:0.1,C:0.3,
:0.5
| 扩展结果 | 概率计算 | 概率 |
|---|---|---|
| BA | 0.3×0.1 | 0.03 |
| BB | 0.3×0.1 | 0.03 |
| BC | 0.3×0.3 | 0.09 |
| B | 0.3×0.5 | 0.15 |
所有扩展序列及其概率:
| 序列 | 概率 |
|---|---|
| AC | 0.16 |
| AA | 0.12 |
| B | 0.15 |
| BC | 0.09 |
| A | 0.08 |
| AB | 0.04 |
| BA | 0.03 |
| BB | 0.03 |
-
选择顶束 (k=2):
- AC (0.16)
- B
(0.15)
-
新的候选集 (B2): AC(0.16)
-
完成集合: B
(0.15)
2.3 仅扩展 AC
- 生成概率: A:0.1,B:0.2,C:0.5,
:0.2
| 扩展结果 | 概率计算 | 概率 |
|---|---|---|
| ACA | 0.16×0.1 | 0.016 |
| ACB | 0.16×0.2 | 0.032 |
| ACC | 0.16×0.5 | 0.080 |
| AC | 0.16×0.2 | 0.032 |
- 由于 B
已完成 ,我们选择扩展结果中的顶束:- ACC (0.080)
- 以某种规则选择 ACB 或 AC
(0.032)
- 新的候选集 (B3): ACC(0.080),ACB(0.032)
- 完成集合: B
(0.15)
![]()
现在是你访问它的最好时机:Beam Search Visualizer
2.4 后续步骤
- 继续扩展:重复上述过程,直到所有候选序列都生成了
或达到设定的最大长度。
3 怎么处理?
在每一步生成过程中,如果某个序列生成了 ,则将其标记为完成,不再进行扩展。以下是处理 的举例:
- 假设在某一步,序列 ACB 扩展出 ACB
(0.032×1=0.032),则: - ACB
保留在最终候选集,并不再扩展。 - Beam Search 继续扩展其他未完成的序列,直到所有序列完成或达到最大长度。
- ACB
问题:如果有一个序列被标记为完成(生成了 ),在下一个扩展步骤中,Beam Search 应该扩展多少个候选序列?
答:束宽 (k) 个,如果被标记完成的序列还保留在候选集中不移出,就是 k−1 个。
4 处理示例图(k=3)
你可以在下图中看到,即便有一个序列生成了 ,下一个扩展步骤中还是会扩展 k=3 个候选序列。
![]()
5 进一步深入Beam Search
5.1 使用对数概率
在实际应用中,尤其是在处理长序列时,直接相乘概率会导致数值下溢问题。为了避免这种情况,通常会使用对数概率来累加评分。
示例说明:
假设使用对数概率,序列的评分计算如下:
- 序列 A 的概率为 0.4,其对数概率为 log(0.4)≈−0.916。
- 序列 AC 的概率为 0.16,其对数概率为 log(0.16)≈−1.833。
在 Beam Search 中,我们会选择对数概率较高(即绝对值较小)的序列作为顶束。
5.2 参数解释
除了 num_beams 和 early_stopping,Beam Search 通常还涉及其他参数,以下是常见参数的简要解释:
max_length(最大生成长度):限制生成序列的最大长度。length_penalty(长度惩罚):用于调整生成序列的长度偏好,通常用于平衡生成序列的长度与概率评分。值大于 1 时,会惩罚过长的序列,值小于 1 时,会鼓励生成较长的序列。no_repeat_ngram_size:防止生成序列中出现重复的 n-gram,提高生成内容的多样性。num_return_sequences:指定生成的序列数量,允许一次生成多个不同的候选序列,<= num_beams。
6 代码演示
下面是一个 beam search 演示代码,结果完全对应于之前讨论的示例。为了简单起见,我们进一步假设在序列 ACB 和 ACC 之后一定是
- import math
-
- def beam_search(initial_sequence, beam_width, max_length, vocab, get_next_probs):
- beam = [(initial_sequence, 0.0)] # (sequence, log_prob)
- completed = []
-
- for step in range(max_length):
- print(f"\n第 {step + 1} 步:")
- all_candidates = []
- for seq, score in beam:
- if seq.endswith('
' ): - completed.append((seq, score))
- print(f"已完成序列: {seq},得分为 {score}")
- continue
- next_probs = get_next_probs(seq)
- print(f"扩展序列: {seq},当前得分为 {score}")
- for token, prob in next_probs.items():
- new_seq = seq + token
- new_score = score + math.log(prob)
- all_candidates.append((new_seq, new_score))
- print(f" 候选序列: {new_seq},得分为 {new_score}")
-
- # 对所有候选序列按得分降序排列,选择得分最高的 beam_width 个序列
- all_candidates.sort(key=lambda x: x[1], reverse=True)
- beam = all_candidates[:beam_width]
-
- # 打印选出的顶束序列
- print(f"\n选择的 {beam_width} 个顶束序列:")
- for seq, score in beam:
- print(f" {seq},得分为 {score}")
-
- # 如果没有更多序列可以扩展,则退出循环
- if not beam:
- break
-
- # 将当前 beam 中剩下的序列加入完成序列中
- completed += beam
-
- # 对完成的序列按得分降序排列,选择得分最高的序列
- completed.sort(key=lambda x: x[1], reverse=True)
-
- print("\n已完成的所有序列:")
- for seq, score in completed:
- print(f" {seq},得分为 {score}")
-
- return completed[0][0]
-
- # 我们之前示例中设置的概率
- def get_next_probs(seq):
- probs = {
- "": {"A": 0.4, "B": 0.3, "C": 0.2, "
" : 0.1}, - "A": {"A": 0.3, "B": 0.1, "C": 0.4, "
" : 0.2}, - "B": {"A": 0.1, "B": 0.1, "C": 0.3, "
" : 0.5}, - "AC": {"A": 0.1, "B": 0.2, "C": 0.5, "
" : 0.2}, - }
- return probs.get(seq, {"
" : 1.0}) -
- initial_sequence = ""
- beam_width = 3 # 你可以修改这个参数来感受区别
- max_length = 5
- vocab = {"A", "B", "C", "
" } -
- best_sequence = beam_search(initial_sequence, beam_width, max_length, vocab, get_next_probs)
- print("\n最佳序列:", best_sequence)
输出:
- 第 1 步:
- 扩展序列: ,当前得分为 0.0
- 候选序列: A,得分为 -0.916290731874155
- 候选序列: B,得分为 -1.2039728043259361
- 候选序列: C,得分为 -1.6094379124341003
- 候选序列:
,得分为 -2.3025850929940455 -
- 选择的 2 个顶束序列:
- A,得分为 -0.916290731874155
- B,得分为 -1.2039728043259361
-
- 第 2 步:
- 扩展序列: A,当前得分为 -0.916290731874155
- 候选序列: AA,得分为 -2.120263536200091
- 候选序列: AB,得分为 -3.2188758248682006
- 候选序列: AC,得分为 -1.83258146374831
- 候选序列: A
,得分为 -2.525728644308255 - 扩展序列: B,当前得分为 -1.2039728043259361
- 候选序列: BA,得分为 -3.506557897319982
- 候选序列: BB,得分为 -3.506557897319982
- 候选序列: BC,得分为 -2.4079456086518722
- 候选序列: B
,得分为 -1.8971199848858813 -
- 选择的 2 个顶束序列:
- AC,得分为 -1.83258146374831
- B
,得分为 -1.8971199848858813 -
- 第 3 步:
- 扩展序列: AC,当前得分为 -1.83258146374831
- 候选序列: ACA,得分为 -4.135166556742355
- 候选序列: ACB,得分为 -3.4420193761824103
- 候选序列: ACC,得分为 -2.525728644308255
- 候选序列: AC
,得分为 -3.4420193761824103 - 已完成序列: B
,得分为 -1.8971199848858813 -
- 选择的 2 个顶束序列:
- ACC,得分为 -2.525728644308255
- ACB,得分为 -3.4420193761824103
-
- 第 4 步:
- 扩展序列: ACC,当前得分为 -2.525728644308255
- 候选序列: ACC
,得分为 -2.525728644308255 - 扩展序列: ACB,当前得分为 -3.4420193761824103
- 候选序列: ACB
,得分为 -3.4420193761824103 -
- 选择的 2 个顶束序列:
- ACC
,得分为 -2.525728644308255 - ACB
,得分为 -3.4420193761824103 -
- 第 5 步:
- 已完成序列: ACC
,得分为 -2.525728644308255 - 已完成序列: ACB
,得分为 -3.4420193761824103 -
- 选择的 2 个顶束序列:
-
- 已完成的所有序列:
- B
,得分为 -1.8971199848858813 - ACC
,得分为 -2.525728644308255 - ACB
,得分为 -3.4420193761824103 -
- 最佳序列: B
7 数学描述
7.1 序列概率
假设我们要生成一个长度为 T 的序列 Y=(y1,y2,…,yT),该序列的生成是逐步进行的,即每个词汇 yt 的生成依赖于前面已经生成的词汇 y1,y2,…,yt−1。因此,序列 Y 的联合概率为:
![]()
7.2 评分函数
由于直接计算概率乘积在处理长序列时容易导致数值下溢问题,所以通过取对数来稳定数值并简化计算。取对数后的评分函数(log likelihood)为:
![]()
模型的目标是最大化序列的概率:
7.3 Beam Search的更新步骤
![]()
7.4 最终选择
当生成过程结束时,从最终的候选集
中,选择得分最高的序列作为最终输出:
![]()
8 实际应用
先安装一些演示用到的库:
- pip install transformers
- #pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
8.1 代码示例
使用 Hugging Face Transformers 库的简单示例:
- from transformers import AutoTokenizer, AutoModelForCausalLM
- import torch
-
- # 指定模型名称
- model_name = "distilgpt2"
-
- # 加载分词器和模型
- tokenizer = AutoTokenizer.from_pretrained(model_name)
- model = AutoModelForCausalLM.from_pretrained(model_name)
-
- # 设置 pad_token 为 eos_token
- tokenizer.pad_token = tokenizer.eos_token
-
- # 移动模型到设备
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
- model.to(device)
-
- # 设置模型为评估模式
- model.eval()
-
- # 输入文本
- input_text = "Hello GPT"
-
- # 编码输入文本,同时返回 attention_mask
- inputs = tokenizer.encode_plus(input_text, return_tensors="pt", padding=True).to(device)
-
- # 生成文本,使用 Beam Search
- beam_width = 5
- with torch.no_grad():
- outputs = model.generate(
- inputs["input_ids"],
- attention_mask=inputs["attention_mask"],
- max_length=50,
- num_beams=beam_width, # 你可以看到 beam_width 对应的参数名为 num_beams
- no_repeat_ngram_size=2,
- early_stopping=True, # 当所有候选序列生成
停止 - pad_token_id=tokenizer.eos_token_id
- )
-
- # 解码生成的文本
- generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
- print("生成的文本:")
- print(generated_text)
输出:
- 生成的文本:
- Hello GPT.
-
- This article was originally published on The Conversation. Read the original article.
8.2 对比不同束宽的输出
- # 输入文本
- input_text = "Hello GPT"
-
- # 编码输入文本,同时返回 attention_mask
- inputs = tokenizer.encode_plus(input_text, return_tensors="pt", padding=True).to(device)
-
- # 设置束宽不同的生成策略
- beam_widths = [1, 3, 5] # 使用不同的束宽
-
- # 生成并打印结果
- for beam_width in beam_widths:
- with torch.no_grad():
- outputs = model.generate(
- inputs["input_ids"],
- attention_mask=inputs["attention_mask"],
- max_length=50,
- num_beams=beam_width,
- no_repeat_ngram_size=2,
- early_stopping=True,
- pad_token_id=tokenizer.eos_token_id
- )
- generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
- print(f"束宽 {beam_width} 的生成结果:")
- print(generated_text)
- print('-' * 50)
- 束宽 1 的生成结果:
- Hello GPT is a free and open source software project that aims to provide a platform for developers to build and use GPGP-based GPSP based GPCs. GPP is an open-source software development platform that is designed to
- --------------------------------------------------
- 束宽 3 的生成结果:
- Hello GPT.
-
- This article is part of a series of articles on the topic, and will be updated as more information becomes available.
- --------------------------------------------------
- 束宽 5 的生成结果:
- Hello GPT.
-
- This article was originally published on The Conversation. Read the original article.
- --------------------------------------------------
9 Beam Search Visualizer示例
Beam Search Visualizer是一个用GPT2进行集束搜索然后可视化的工具。
现在我们设置一下下面的参数:
- 解码所需的输入(inputs):传递给解码器的输入序列。
- 生成步骤数(max_new_tokens):要生成的最大 token 数量。
- 搜索宽度(num_beams):用于生成的 beam search 的宽度,即 beam 的数量。
- 长度惩罚系数(length_penalty):对输出序列应用的长度惩罚。length_penalty > 0.0 会倾向于生成较长的序列,而 length_penalty < 0.0 会鼓励生成较短的序列。此参数不会影响 beam search 的路径搜索,但会在最终选择序列时根据长度偏好进行调整。
- 返回序列数量(num_return_sequences):最终生成的序列数量。此值应小于或等于 num_beams。
![]()
得到的结果如下图所示:
![]()
结论性序列是以<| endftext |>标记结尾或以生成结束结尾的序列。
它们根据分数进行排名,如公式score = cumulative_score / (output_length ** length_penalty)所示。
只返回最上面的num_beams评分序列:在树中它们以蓝色突出显示。未选择的序列也显示在树中,用黄色突出显示。
输出的序列为:
- Score
-1.14:Conclusion: thanks a lot. That's all for today.\n\nAdvertisements<|endoftext|>- Score
-1.30:Conclusion: thanks a lot. That's all for today. I hope you enjoyed- Score
-1.33:Conclusion: thanks a lot. That's all for today. I'll be back
评论记录:
回复评论: