想象你是一位综艺导演,要在一场选秀比赛中从成千上万名选手里挑出“最动听的一句歌词”。比赛规则很特别:选手不能一口气唱完,只能一个词一个词地接龙演唱——你听到第一个字后决定谁唱第二个字,听到前两个字后再定第三个字,直到有人唱出终止符“谢幕”。台下的评委团(模型)会给每一个候选词打分,但真正棘手的是:你永远不知道现在选的词,会不会让后面的整句垮掉。 一个只顾眼前的导演,会每轮只留下评分最高的那位选手继续唱(贪心搜索);而真正老练的导演会采用“多队晋级赛”策略——每轮保留 K 位最有潜力的选手,让他们各自接龙展示,直到整首歌结束,再从这批完整歌单里选出总冠军。这个策略,就是自然语言处理中大名鼎鼎的 Beam Search(束搜索)。它不保证一定找到完美歌词,但总能在“看得人多”与“舞台装得下”之间取得巧妙平衡。
理解 Beam Search 是走向 NLP 工程实战的必经之路。无论做机器翻译、语音识别还是文本摘要,解码环节选对搜索策略,直接决定生成质量与推理速度的平衡,是每位 AI 从业者的基本功。
可视化演示
动态演示Beam Search的核心机制,通过开始/执行/监控/完成四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Beam Search:Beam Beam Search基本原理 Beam Width对生成质量 核心机制可视化 Beam Search整体架构与关键组件
序列生成困境
选秀导演遇到的第一个难题,是“候选歌单”多到离谱。假设参赛选手只能用 10 万个词库接龙,一句 20 个字的歌词,理论上就有 10 万的 20 次方种组合——这个数字比宇宙中的原子还多。导演不可能让所有组合都唱一遍再打分,只能让选手一个词一个词地唱,每唱完一个词就评估一次,这就是“逐步解码”。
在 Seq2Seq 模型中,解码器正是这样工作的:它拿到编码器压缩好的源句语义向量后,从起始符 <SOS> 出发,每步根据“已经生成的词”预测“下一个词的概率分布”,然后挑词、拼接、继续,直到吐出终止符 <EOS>。这个过程本质上是在巨大的组合空间里做一次搜索,而搜索策略的优劣,直接决定了最终句子的质量。

搜索问题的数学本质可以这样看:假设词表大小为 $V$,目标句子的长度为 $L$,那么候选句子总数是:
回到导演的视角:组合爆炸意味着“全部听完再打分”永远不可能。于是导演们退而求其次,设计各种“边走边看”的搜索策略——有的只看眼前,有的多留几条后路。Beam Search 就是其中最有名的一种“多留后路”策略。
- 词表大小 $|\mathcal{V}|$词表就是选手们能用的所有词语集合,它的规模决定了每一步有多少种选择。
- 句子长度 $L$生成目标包含 $L$ 个词,每多一个词,组合数就多乘一次词表大小。
- 指数关系整个式子是 $|\mathcal{V}|$ 自乘 $L$ 次,长度每增加 1,候选数就扩大 $V$ 倍,这就是“组合爆炸”。
贪心搜索的局限
如果导演是个急性子,他会采用最省事的办法:每轮只让当前得分最高的那位选手继续唱,其他选手全部淘汰。这就是“贪心搜索”——每一步都选择当下概率最大的词,不考虑这个词会把整句带向何方。它快、省内存,但常常酿成“一步错、步步错”的悲剧。
比如翻译 “I love you“ 时,第一个词 ”I“ 的候选分布里 ”我“(0.5)和 ”爱“(0.3)分列前二,贪心会毫不犹豫选 ”我“;但如果某一步模型给 ”的“ 打了较高分,贪心就会脱口而出 ”我爱你的“ 这种别扭的句子,而真正合理的 ”我爱的家” 在早期可能因为某步落后被早早淘汰,再也回不了头——因为被淘汰的路径不会重新进入候选池。

贪心搜索的决策规则可以写成:每一步都直接取概率最大的那个词。
所以贪心导演省了心,却输了质量。聪明的导演开始思考:与其孤注一掷,不如每轮同时保留几位有潜力的选手,让他们各自唱下去,最后再整体比较——这正是下一节 Beam Search 的核心思想。
- $\arg\max$ 的含义在所有词里找出“让概率最大”的那个词,相当于评委举牌选出本轮最高分。
- 条件概率 $P(y \mid x, y_{1}, \dots, y_{t-1})$这是“在已知源句和前文的情况下,下一个词是 $y$ 的概率”,它是解码器给出的核心信号。
- 下标 $t$ 与 $t-1$$t$ 表示当前生成到第几个词,$y_{<t}$ 表示已经生成的前缀,贪心每一步都只基于这个前缀决策。
Beam Search 核心机制
导演终于想通了:把选秀改成“多队晋级赛”。每轮比赛结束后,只保留 K 位表现最好的选手(而不是 1 位),让他们各自带着自己的“歌词前缀”进入下一轮,继续接龙。这个 K 就是束宽(beam width),而整场比赛就是 Beam Search。
具体过程是这样的:假设词表有 $V$ 个词,当前有 K 条存活路径。每走一步,每条路径都要尝试接上词表里的每一个词,于是生成 $K \times V$ 个候选;评委团给所有候选打分排序,只留下分数最高的 K 条进入下一轮。如此反复,直到候选句子都以 <EOS> 结尾,或者达到预设的最大长度。

每一条候选路径的分数,是所有已生成词的对数概率之和。
回到选秀现场:K=3 意味着每轮有 3 位选手同时备战,每人试唱 V 个候选词,但最终只有 3 个名额能留下。这份“广撒网”的从容,让最终获胜的歌词往往比贪心导演选出的更连贯、更自然——因为每条路径都完整唱完后,才由总冠军统一加冕。
- 累计分数 $S(Y)$一个候选句子 $Y$ 的总分,是所有词对数概率的总和,分数越高代表模型越认可。
- 对数概率 $\log P(y_t \mid x, y_{<t})$把每一步的条件概率取自然对数,得到的是一个非正数;连乘变成连加,避免了下溢。
- 求和范围 $t=1$ 到 $T$从第一个词累加到最后一个词(含
<EOS>),这就是完整路径的累计得分。
分数计算与长度惩罚
导演很快发现一个新问题:选手唱得越长,评委打的累计分就越吃亏。这很好理解——每个词的对数概率都是负数,唱 10 个词的歌手累计 -20 分,唱 6 个词的歌手可能只有 -12 分,可这并不代表短歌词更优秀。如果不做处理,Beam Search 会系统性偏爱短句,机器翻译里就会出现“漏译”。
解决思路也很朴素:像田径比赛按体重分级一样,给分数除以一个与句子长度相关的量,让不同长度的句子站在同一起跑线上。这就是“长度惩罚”(length penalty)。

加入长度惩罚后的评分公式为:
有了长度惩罚,评委的记分牌终于公平了:短小精悍的歌词不会因为词少就占便宜,洋洋洒洒的长句也不会因为词多而被误杀。工程实践里,这一项往往是翻译质量提升的“免费午餐”。
- 句子长度 $|Y|$候选句子的总词数(含终止符),它代表“歌手总共唱了多少个词”。
- 惩罚指数 $\alpha$控制惩罚力度的大小。$\alpha=0$ 时不做惩罚,$\alpha$ 越大,长句获得的补偿越多,相当于给长跑选手加权重。
- 归一化分数 $S_{\text{norm}}(Y)$把原始累计对数分数除以 $|Y|^{\alpha}$,得到的可比较分数;选择最终结果时,比较的就是这个归一化分数。
束宽调参的艺术
既然 K 越大,同时考察的选手越多,是不是无脑调大 K 就行?导演会告诉你:舞台是有成本的。每轮要试唱的候选数量是 $K \times V$,K 翻倍,计算量和内存几乎也翻倍。更微妙的是,当 K 增加到一定程度后,生成质量的提升会快速饱和,甚至会引入更多“高分但重复”的套话。
经验法则通常是这样:K=1 时退化为贪心搜索;K=5~10 是大多数翻译、摘要系统默认甜区;K 超过 50 以后,质量曲线几乎走平,而推理延迟让用户无法接受。实际工程中还要结合“最大生成长度”和“长度惩罚”一起调,像调音师一样反复试听。

单步扩展的计算量可以写成:
选秀导演的经验是:先调 K,再调 $\alpha$,最后看具体任务。K 不是越大越好,而是“够用就好”——找到那个质量曲线开始走平、开销还能接受的甜蜜点,就是束宽调参的全部艺术。
- 束宽 $K$每轮保留的路径条数,是算法唯一的“旋钮”,直接决定并行考察的规模。
- 词表大小 $|\mathcal{V}|$每条路径都要在完整词表上扩展一遍,所以词表也线性影响计算量。
- 大 $O$ 记号 $O(\cdot)$表示数量级。$K \times V$ 意味着计算开销随 K 线性增长,K 变大没有指数灾难,但也绝非免费。
应用与实战
这套“多队晋级赛”如今是各大 AI 舞台的标配。机器翻译里,Google 神经翻译系统用 Beam Search 解码,配合长度惩罚显著提升译文流畅度;语音识别中,LAS、Whisper 等模型在声学特征上跑束搜索,把“听到的声音”变成“最可能的文本”;文本摘要和对话生成里,BART、T5 也用束搜索从候选摘要中挑出信息最完整的那一条。

在所有这些任务中,最终结果的选择可以统一写成:在所有走完的候选路径(束)中,挑选归一化分数最高的一条。
从选秀舞台到生产环境,Beam Search 的核心逻辑始终如一:不把鸡蛋放在一个篮子里,也不无限扩大篮子。理解了 K、累计分数、长度惩罚这三板斧,你就掌握了 NLP 解码的核心武器,接下来在 Python 演示中亲手实现一个束搜索解码器,一切都会水到渠成。
- 候选集合 $\mathcal{B}$束搜索过程中所有以
<EOS>结尾的完整句子,相当于总决赛的选手名单。 - 归一化分数 $S_{\text{norm}}(Y)$每条候选经过长度惩罚后的最终得分,是评委手中的决定性票数。
- $\arg\max$ 选择从候选集合里挑出分数最高者,这就是模型最终输出的句子。
AI 应用场景
机器翻译解码
阶段:推理/解码阶段(Encoder-Decoder 模型的生成端)
解决的问题:传统贪心解码在翻译长句时容易“漏译”和“倒挂”,比如把 “I love you“ 译成 ”我爱你的”;纯随机采样则输出不稳定。Beam Search 通过同时维护多条候选译文,再按长度惩罚后的总分选出最佳译文,显著提升 BLEU 分数与人工观感。
具体案例:Google 神经机器翻译(GNMT)在在线翻译系统中使用束宽 4~8 的 Beam Search 解码,配合长度惩罚($\alpha \approx 0.6$),在千万级句对上保持毫秒级响应,同时避免短句偏好。
语音识别文本化
阶段:声学模型后处理/解码阶段
解决的问题:语音识别需要把声学特征序列转成文本,候选路径在音素层就存在大量歧义。束搜索可以在解码时同时考虑声学分数与语言模型分数,保留多条候补句子,避免“听错一个字、整句跑偏”。
具体案例:Whisper 等语音识别模型在解码阶段采用带时间戳的 Beam Search,逐帧对齐音频与文本;耳语模型针对长音频还引入了分帧束搜索,保证每个片段都能输出稳定文本。
文本摘要与对话生成
阶段:生成式模型的解码阶段
解决的问题:摘要生成要求“信息完整且长度适中”,贪心解码常漏掉关键实体;对话生成则要求回复自然不重复。Beam Search 配合长度惩罚(以及工程中常见的重复惩罚)可以筛选出信息密度与流畅度俱佳的候选。
具体案例:BART 在 CNN/DailyMail 摘要任务上使用束宽 4~6 的解码器,配合三gram重复惩罚,生成的摘要 ROUGE 分数远超贪心基线;企业客服机器人也用它控制回复质量与延迟的平衡。
Python 演示
# -*- coding: utf-8 -*-"""Beam Search 解码演示:在模拟概率表上搜索最优序列"""import mathfrom heapq import nlargest VOCAB = ["我", "爱", "你", "的", "家", "<EOS>"] def next_probs(prefix): """模拟解码器:根据已有前缀返回下一个词的 log 概率列表""" table = { (): [("我", -0.7), ("爱", -1.2), ("你", -1.8)], ("我",): [("爱", -0.6), ("的", -1.6), ("<EOS>", -2.4)], ("爱",): [("你", -0.8), ("的", -1.9), ("<EOS>", -1.5)], ("你",): [("的", -0.5), ("家", -1.4), ("<EOS>", -2.2)], ("我", "爱"): [("你", -0.4), ("<EOS>", -2.6)], ("我", "的"): [("家", -0.9), ("<EOS>", -1.3)], ("爱", "你"): [("的", -0.7), ("<EOS>", -1.1)], ("爱", "的"): [("家", -1.0), ("<EOS>", -0.8)], ("你", "的"): [("家", -0.6), ("<EOS>", -1.5)], ("你", "家"): [("<EOS>", -0.3)], ("我", "爱", "你"): [("的", -0.5), ("<EOS>", -1.0)], ("我", "爱", "你", "的"): [("<EOS>", -0.2)], ("我", "的", "家"): [("<EOS>", -0.3)], ("爱", "你", "的"): [("家", -0.4), ("<EOS>", -0.9)], ("你", "的", "家"): [("<EOS>", -0.4)], } return table.get(prefix, [("<EOS>", 0.0)]) def greedy_decode(max_len=6): """贪心解码:每步只选概率最大的词""" seq = () score = 0.0 for _ in range(max_len): best = max(next_probs(seq), key=lambda x: x[1]) seq = seq + (best[0],) score += best[1] if best[0] == "<EOS>": break return score, seq def beam_search(beam_width=3, max_len=6, alpha=0.0): """Beam Search 解码:每轮保留 beam_width 条最高分路径""" beam = [(0.0, ())] # (累计log概率, 已生成序列) finished = [] for _ in range(max_len): candidates = [] for log_prob, seq in beam: for word, lp in next_probs(seq): new_seq = seq + (word,) new_score = log_prob + lp if word == "<EOS>": if alpha > 0: new_score = new_score / ((len(new_seq)) ** alpha) finished.append((new_score, new_seq)) else: candidates.append((new_score, new_seq)) if not candidates: break beam = nlargest(beam_width, candidates, key=lambda x: x[0]) finished.sort(reverse=True) return finished[0] if finished else beam[0] if __name__ == "__main__": print("贪心解码: ", greedy_decode()) for k in [1, 3, 5]: print("Beam K=%d 无惩罚: " % k, beam_search(k, alpha=0.0)) print("Beam K=3 alpha=0.6:", beam_search(3, alpha=0.6))
table = {...}用字典模拟解码器输出的条件概率分布,键是已生成前缀,值是候选词及其对数概率;真实项目中这里是神经网络预测。beam = [(0.0, ())]束的初始状态:起始符前缀为空,累计分数为 0。for word, lp in next_probs(seq):每条存活路径在词表上扩展出所有候选词,对应文中“每步扩展 K×V 个候选”。new_score = log_prob + lp累计对数概率:连乘转为连加,越长的句子分数越负,这就是长句吃亏的原因。if word == "<EOS>":遇到终止符的路径不再参与下一轮扩展,而是进入finished决赛名单。new_score = new_score / ((len(new_seq)) ** alpha)长度惩罚:用长度幂次做除法,公平化长短句之后才计入最终比较。beam = nlargest(beam_width, candidates, key=lambda x: x[0])从所有扩展候选中挑出分数最高的 K 条,这就是“每轮只留 K 位选手”。finished.sort(reverse=True)对完整句子按最终分数排序,取第一名作为整场选秀的总冠军。
常见误区
误区K 就是搜索精度,直接设成 100,模型一定生成得更好,反正服务器性能强。
正解束宽 K 代表每轮保留的路径数,每步要评估的候选是 K×V 个。K 越大,计算和内存线性增长;更关键的是,质量提升会迅速饱和,K 从 5 涨到 50 往往只带来微小收益,却可能让推理慢一个数量级。这就像选秀海选留下 1000 人,评委疲惫不堪,反而选出大量相似套话。工程上默认 5~10 才是性价比高峰。
为何常见:人们直觉认为“看得越多越稳”,忽略了搜索质量的边际递减效应和计算成本的线性增长,两相叠加后 K 存在明显甜点。
误区Beam Search 每一步都保留 K 条路径,肯定能覆盖全局最优解,是精确搜索。
正解Beam Search 是典型的启发式剪枝搜索。每轮被淘汰出前 K 的路径,即使未来可能“低开高走”成为最优,也永远没有翻身机会。全局最优只有在穷举全部 $V^L$ 条路径或使用动态规划(如隐马尔可夫的维特比算法)时才能保证。Beam Search 只是用 K 条路径去“大概率逼近”最优解。
为何常见:看到“Beam“一词容易联想到”广覆盖”,且实验结果通常明显优于贪心,于是误认为它等价于穷举,忽略了它本质上仍是剪枝近似。
误区模型输出的概率就是可信度,把整句概率连乘得到的就是最终分数,凭什么要除一个长度?
正解每个词的概率都小于 1,连乘出的整句概率随长度指数级衰减。两句同样流畅的译文,长句的累计概率天然远低于短句,不惩罚就会让解码器系统性偏爱短句,导致翻译漏词。长度惩罚用 $|Y|^\alpha$ 做除法,本质是把“分数量级”和“语义质量”解耦,让长短句在公平规则下竞技。
为何常见:初学时只盯着“概率高=好”的直觉,没有意识到连乘对长度的天然不公,直到发现模型总输出过短句子才反应过来。
误区K=5 就代表每一步生成 5 个词,然后从里面挑一个。
正解K 是每轮结束后保留的完整路径条数,不是生成的词数。每一步是让 K 条路径各在词表 V 个词上扩展,产生 K×V 个候选,再筛回 K 条。K=5 意味着同时有 5 个“半成品句子”在并行生长,而不是一步吐出 5 个词。
为何常见:把“每步候选数”和“每步保留路径数”混为一谈,实际上前者是 K×V,后者才是 K,这两个数量级完全不同。
面试题
Q1. 当 beam width K=1 时,Beam Search 等效于哪种解码方式?
- A.贪心搜索← 正确答案
- B.随机采样
- C.纯束搜索
- D.维特比解码
答案:A。K=1 表示每轮只保留 1 条最高分路径,行为与每步取 argmax 的贪心搜索完全一致。随机采样会引入随机性,维特比解码是动态规划,都不等价。所以正确答案是 A。
Q2. Beam Search 每一轮结束后保留的路径数量是多少?
- A.词表大小 V
- B.束宽 K← 正确答案
- C.句子最大长度
- D.仅 1 条
答案:B。束宽 K 定义的就是“每轮从 K×V 个扩展候选中保留的最高分路径条数”。词表大小 V 是每步可选的词数,与保留路径数无关;贪心才只保留 1 条。所以选 B。
Q3. 解码打分时使用 log 概率而非原始概率连乘,最主要的原因是?
- A.数值更稳定,避免下溢← 正确答案
- B.把负分数变成正分数
- C.词表可以无限扩大
- D.不再需要长度惩罚
答案:A。整句概率是大量小于 1 的数连乘,在浮点运算中会迅速下溢为 0;取对数后连乘变连加,数值稳定且单调性不变。log 概率是负数,不会变正;词表与长度惩罚都与是否取 log 无关。所以选 A。
Q4. 如果不做长度惩罚,Beam Search 可能出现哪些现象?(多选)
- A.偏好较短句子← 正确答案
- B.长句累计分数偏低← 正确答案
- C.计算变得更慢
- D.必现重复词语
答案:AB。每个词的对数概率为负,句子越长累加越负,因此长句系统性被低估,解码器偏向短句,A、B 正确。长度惩罚不直接影响计算速度(C 错);重复词是另一类问题,通常由重复惩罚处理,不是长度惩罚缺失的必然结果(D 错)。
Q5. 束宽 K 从 1 增大到 8,最直接的变化是什么?
- A.搜索质量提升且开销更低
- B.候选路径更多,质量通常提升,开销变大← 正确答案
- C.每步只生成一个词
- D.彻底消除局部最优
答案:B。K 增大意味着每轮保留更多候选路径,单步扩展量从 1×V 变为 8×V,质量通常改善、计算和内存线性增长。贪婪的“开销更低”是 K=1 的特征;Beam Search 是近似搜索,不能“彻底消除”局部最优。所以选 B。
Q6. 关于 Beam Search 能找到全局最优解的说法,正确的是?
- A.一定能找到全局最优
- B.不一定,因为低分路径被提前剪枝← 正确答案
- C.束宽越小越容易找到
- D.只有 K=1 时能找到
答案:B。Beam Search 每轮只保留 K 条路径,被剪掉的低分路径即使未来成为最优也无缘复活,因此它是启发式近似而非精确搜索。束宽越大越接近最优而非越小;K=1 等价贪心,恰恰最不可能找到全局最优。所以选 B。
Q7. 关于长度惩罚的作用,正确的理解有哪些?(多选)
- A.让长句分数更公平← 正确答案
- B.修正长句被低估← 正确答案
- C.让分数变为正数
- D.取代累计对数概率
答案:AB。长度惩罚用 $|Y|^\alpha$ 做除法,抵消长句因项数多而累积的负分,让长短句可以公平比较,A、B 正确。log 概率非正,除以正数后仍非正,不会变正(C 错);它只是对累计分数的后处理,并不取代累计对数概率(D 错)。
Q8. Seq2Seq 模型中,Beam Search 何时停止生成?
- A.达到束宽 K
- B.所有候选都以 EOS 结尾或达到最大长度← 正确答案
- C.贪心搜索完成一步
- D.词表中的词全部用完
答案:B。解码终止条件是候选路径输出 <EOS> 或达到预设最大生成长度。束宽 K 不是长度概念(A 错);贪心与否与终止无关(C 错);词表是候选来源而非终止依据(D 错)。所以选 B。
Q9. 关于贪心搜索与 Beam Search 的对比,正确的有哪些?(多选)
- A.贪心每步只保留一条路径← 正确答案
- B.Beam 每步保留 K 条路径← 正确答案
- C.贪心可能陷入局部最优← 正确答案
- D.束宽不影响计算开销
答案:ABC。贪心是 K=1 的特例,每步唯一,容易因为局部最优而错失全局(A、C 对);Beam 每步保留 K 条,覆盖面更广(B 对)。束宽直接决定单步扩展量 K×V,明显影响计算开销(D 错)。
Q10. 词表 V=10000、束宽 K=5 时,单步扩展产生的候选数量约为多少?
- A.约 5 万个← 正确答案
- B.约 50 个
- C.约 1 万个
- D.约 25 个
答案:A。每一步需要让 K=5 条存活路径各自在 V=10000 的词表上扩展,候选总数 = K×V = 50000,即约 5 万个。50 是“每轮保留数”被误当成扩展数;10000 忽略束宽;25 是 K² 的误解。所以选 A。