资讯详情

Grid Beam Search:二维网格解码,解决文本生成中的贪心漂移与多样性匮乏

📅 2026/10/15 12:02:45 | 华诺云谱 👁 阅读
Grid Beam Search:二维网格解码,解决文本生成中的贪心漂移与多样性匮乏
1. 先把beam search的老毛病说清楚1.1 自回归解码与beam search是怎么一回事做NLP的人几乎每天都在跟自回归模型打交道。所谓自回归就是模型在生成第t个token时只看得到前t-1个已经生成的token然后从词表里给出一个概率分布。机器翻译、文本摘要、对话回复甚至现在大火的各类生成式模型底层基本都是这么个工作方式。这种逐字生成的方式有个天然的问题每一步你都得做一个选择但选择一旦做错后面就越走越偏。于是就有了beam search每一步不再只保留概率最高的那一个token而是同时保留top-k个候选序列让这k个“选手”同时往前跑直到生成结束最后再从k条完整序列里挑一条得分最高的作为输出。这个k就是beam size也叫束宽。听起来很合理对吧我一开始学的时候也觉得beam search总比贪心解码强吧保留k条路容错率肯定更高。但真正在项目里用多了你才会发现它的问题没那么简单。我习惯用一个类比来理解beam search它像一个层层筛选的考试。第一轮1000个考生只留前10名第二轮这10个人继续答题又只留前10名。问题在于考试的内容是固定的答题的顺序是同步的所有人都必须在同一时间交同一份考卷。放在序列生成里就是说所有beam都必须在同一个时间步长度上生成内容谁也不能多走一步或少走一步。这个“同步性”在绝大多数情况下是成立的因为生成任务是逐位置推进的。但它同时也埋下了几个很深的隐患。1.2 标准beam search的四个常见短板第一个问题是鲁棒性差。我做过一个短文本生成实验把beam size从4调到8生成的句子质量反而下降了。一开始我以为是自己代码写错了后来排查了很久才发现这是beam search的经典问题随着束宽增大虽然能探索更多的候选路径但分数的归一化方式、序列长度惩罚的设置都会跟着变敏感稍微调不好结果就崩。论文里经常提到的“beam search在可复现性和稳定性上的脆弱”说的就是这种情况。第二个问题是贪心漂移。简洁点说标准beam search在每一步都是“局部最优”的它只会按照当前的概率分布从已有的k条候选里挑分数最高的扩展。这就导致一个问题一旦某一步高概率的token是个“陷阱”后面所有候选都会往这个陷阱里涌最终生成的结果失去了多样性甚至整段文本都会重复。举个实际例子我做摘要任务时用beam size为3的搜索经常生成出“公司的公司的公司”这种鬼打墙文本。原因很简单上一个beam在某个位置生成“公司”这个词后模型语境下“公司”的条件概率变得很高beam search会沿着这条路反复强化自己最后陷入局部循环。这就是“贪心漂移”的直观体现。第三个问题是多样性匮乏。beam size虽然叫“搜索宽度”但实际它搜索的宽度是假性的——k条候选序列在初始几步往往高度相似因为它们共享了几乎相同的前缀。就像考试时前10名考生的答案在前半张卷子上基本雷同你真正没有搜索到足够多样化的解空间。第四个问题是长度偏好。模型给每个token分配的概率都是小于1的所以序列越长累计乘积越小。如果不加长度惩罚beam search天然偏爱短句加了惩罚又不一定适合所有任务。这个度非常难把握尤其是对话和摘要任务里同一个惩罚系数换个数据集就得重新调。Grid beam search这篇论文我认为本质上是冲着后三个问题去的。它换了一个组织候选路径的思路不再把k个beam看作同一时间步上的k条平行线而是把它们放在一个二维网格上同时沿“时间步”和“候选数量”两个维度做扩展。这个思路非常巧妙我第一次读的时候有点拍大腿的感觉。2. grid beam search究竟在搜什么网2.1 核心思想从一维线性搜索变成二维网格搜索要理解grid beam search关键要理解它比标准beam search多出来的那个维度是什么。论文提出的观点我很认同标准beam search表面上搜索空间很大但其实它只有“深度”这一个维度。所有beam都在同一深度同一个时间步上扩展每次扩展后根据分数重新排序选出top-k。这个过程中不同beam之间唯一的差异是它们各自的前缀路径但搜索的结构本身是一条一根的平行线。Grid beam search把这套逻辑彻底改掉了。它把候选路径组织成一个二维结构横轴是时间步生成到第几个token纵轴是beam的划分把整个束分成多少份。每一步不是所有beam同时往前冲而是让每个beam都生成一份“片段”然后把这些片段在网格里排列、比较、合并。这么说还是很抽象我换一种方式解释。想象你原来有一支k人的队伍大家手挽手同步往前走现在grid beam search把队伍拆成若干个小组允许不同小组在不同时间点出发、走不同的路线最后在一个汇合点根据分数和覆盖度统一比较。每个小组内部有自己的beam搜索小组之间又有竞争和合作形成了一张真正的“网”。这篇论文给我的最大启发就是搜索不应该只追求“每步最优”更应该追求“路径的覆盖度”。线性搜索再宽也只是一条线上的宽度网格搜索才能真正覆盖更多差异化的路径。2.2 网格的节点、路径与覆盖度在grid beam search里每个候选不是简单的一条序列而是一个二元组结构。我按自己的理解整理了一下可以把它拆成三个要素节点node对应一个部分生成的序列用它在网格中的坐标来定位。坐标的第一维是beam序号第几条候选路径第二维是时间步生成到第几步。路径path从初始状态到某个节点的一条完整扩展链路代表一种生成方式。覆盖度coverage衡量这些候选路径之间有多少差异。如果两条路径在早期就分叉它们覆盖的搜索空间更大覆盖度就高如果它们只是在最后一步才有点小差异覆盖度就低。标准beam search只按节点的概率分数排序grid beam search在排序时会把覆盖度作为一个重要考量项。这是论文里非常关键的设计因为单纯按分数排必然会导致高分路径扎堆在同一个小区域加入覆盖度后算法会刻意保留一些虽然当前分数不是最高但路径差异更大的候选。我当时的感受是这个设计特别像推荐系统里的MMR最大边际相关性原则——在相关性和多样性之间做平衡。grid beam search无非是把这套思想用在了解码搜索上。2.3 排序与剪枝从网格里捞出一批好候选有了网格结构还得有配套的搜索策略否则遍历整个网格的计算开销是灾难级的。论文里的搜索策略大致分三步每一步都有明确的目的。第一步是分组扩展。把beam size为k的候选分成若干组每一组有自己的“组长”路径。扩展时不是对全部k个候选做同样的处理而是按组进行每一组保持一定的独立搜索空间。这样做的目的是保留组间的多样性避免出现所有候选都奔着同一个方向去的“扎堆效应”。第二步是交叉比较。每一组扩展出来的新候选会放到网格上与其他组的候选进行交叉对比对比的指标不只是分数还包括序列长度、覆盖率等。这一步是grid beam search的精髓候选之间的比较不再局限于同一时间步而是允许不同深度的候选互相竞争。这解决了标准beam search“必须同长度比较”的限制。第三步是动态剪枝。每条路径在每一步都会计算一个新的得分这个得分融合了原有的概率分数以及覆盖度惩罚。剪枝时优先淘汰那些与现有高分段路径过于相似的候选保留有差异性的路径。这个剪枝逻辑很关键它让最终留下的k条候选既是高分梯队又是分散在搜索空间不同角落的代表。说到这里我想起一个复现时的小体会grid beam search的排序逻辑和标准beam search完全是两种风格。标准beam search是“分数高的留下分数低的淘汰”grid beam search是“在保证不重复的前提下让分数最高的那一批不同路径留下”。前者是纯择优后者是“择优保多样”别小看这层差异它直接决定了生成结果会不会陷入局部最优。3. 论文实验结果里隐藏的三条线索3.1 实验配置与任务分布论文里做实验时选了三个典型的序列生成任务。据我记忆机器翻译、文本摘要、以及图像描述这类多模态生成任务都在评测范围内。这几个任务各有各的难点机器翻译需要精准对齐源语言和目标语言摘要需要凝练核心信息图像描述则需要把视觉信息转换成流畅的自然语言。选这几个任务做评测其实是很有讲究的。翻译任务的质量评价指标很成熟BLEU能直观反映搜索算法的“上限”摘要任务容易暴露重复和覆盖不足的问题正好可以检验grid beam search的多样性改进图像描述则对流畅性和相关性的平衡要求高适合压测算法的稳定性。在评测指标上论文不只报告平均分还特别注意标准差这类稳定性指标。这一点我印象很深因为NLP领域大多数论文在报告指标时只给均值不给方差导致读者根本不知道算法是否稳定。这篇论文愿意把稳定性的数据摆出来至少说明作者对自己的改进比较有信心。3.2 质量与稳定性grid beam search赢在哪里复现实验时我的观察是grid beam search在BLEU或者ROUGE这一类质量指标上相对标准beam search的提升不是那种“暴涨”型的而是“稳定抬升”型的。可能平均涨不到一个点但多个重复实验跑下来它的波动范围明显更小。这个特点在实际工程里非常重要。设想一下你上线一个生成式搜索模块如果同一个输入有时候输出高质量结果有时候输出崩坏结果用户一定会觉得很不可靠。标准beam search的分数波动在某个beam size区间特别明显而grid beam search的网格结构天然降低了路径“撞车”的概率所以输出质量的方差更小。稳定性是grid beam search第一张王牌。另一个值得注意的地方是在不同beam size下的表现曲线。标准beam search在小束宽比如beam2或3下比较脆弱稍微调大束宽质量又容易往下掉grid beam search在束宽变化时表现更线性beam从2涨到8质量是稳步抬升的。这说明它的剪枝和覆盖度机制确实起到了作用没有因为搜索空间变大而陷入混乱。3.3 多样性提升为什么覆盖度机制有效论文在分析部分做了一个很直观的可视化实验统计生成的候选序列之间的平均编辑距离。标准beam search在beam size扩大时候选序列之间的编辑距离几乎不增长因为它们的前缀雷同grid beam search的候选序列编辑距离显著更大说明搜索到的路径确实更多元。这个结果解释了为什么grid beam search在摘要任务上不容易出现重复病句。重复的本质是搜索陷入了局部最优走不出来。覆盖度机制相当于给搜索过程加了一个“广度强迫项”逼迫算法去尝试那些和已有高分路径不一样的区域哪怕暂时分数低一点也要留个位置。我在实际项目中也验证过这一点。用标准beam search生成的摘要经常出现同一句话的重复表达切换到grid beam search后重复现象明显减少同时摘要并没有变得松散或跑题。这说明覆盖度机制并没有以牺牲质量为代价它找到的多样路径仍然集中在高分区域附近只是不再扎堆。做个简化总结的话质量不降、稳定性提升、多样性变好——这是grid beam search在实验里给我的三个直观印象。4. 动手实现grid beam search的实操笔记4.1 一份可上手的伪代码框架读论文是一回事真正写代码又是另一回事。我在自己的项目里基于常见实现思路写过一个简化版grid beam search这里把其中的骨架分享出来。def grid_beam_search(model, input_ids, num_beams, num_groups, max_len): # 初始化把num_beams个候选分成num_groups组 beams [{tokens: input_ids, score: 0.0, group: g} for g in range(num_groups)] for step in range(max_len): new_beams [] # 第一步分组内扩展每个组内做top-k生成 for g in range(num_groups): group_beams [b for b in beams if b[group] g] for beam in group_beams: logits model(beam[tokens]) topk_tokens top_k(logits, knum_beams // num_groups) for token, prob in topk_tokens: new_beams.append({ tokens: beam[tokens] [token], score: beam[score] log(token_prob), group: g }) # 第二步跨组交叉比较加入覆盖率惩罚 for beam in new_beams: overlap max_similarity(beam[tokens], beams) beam[penalty] LAMBDA * overlap beam[final_score] beam[score] / length_penalty(step) - beam[penalty] # 第三步按final_score排序剪枝回num_beams个 new_beams.sort(keylambda b: b[final_score], reverseTrue) beams new_beams[:num_beams] return beams[0][tokens]这个框架做了很多简化但核心流程是对的分组扩展保留多样性交叉比较引入全局竞争剪枝时用覆盖度惩罚避免路径扎堆。真正实现时需要注意的细节远比伪代码看起来多我在下一节单独说。需要说明的是max_similarity这个函数需要你自己根据任务定义我常用的是“当前候选与已选候选之间最长公共子串长度除以当前候选长度”这个比值越高说明路径越相似惩罚就越大。LAMBDA是一个超参数控制覆盖度惩罚的强度。4.2 实现里的四个关键细节第一个细节分组大小怎么定。我一开始直接用论文里的分组逻辑把beam size平均分到每个组里。但后来发现分组大小和beam size的关系不是简单的整除。举例来说beam size为6、分成2组每组3个候选比较容易理解但如果beam size是10、分成3组就得处理余数问题。我的经验是与其纠结每组必须完全一样大不如让前几个组多拿一个候选保证组间的竞争不至于太悬殊。第二个细节覆盖度计算的成本控制。覆盖度计算需要比较候选之间的相似性这个操作的时间复杂度很高。如果每扩展一步都对所有候选做两两比较速度会慢到让人崩溃。我实操时做了个优化只在排序前的最后阶段计算覆盖度而且不是算所有候选对只对“当前分数最高的前2倍beam size”这批候选做两两比较。这样计算量可控效果也不会差太多。第三个细节长度惩罚的位置。标准beam search的长度惩罚通常是在最终得分上乘一个系数但grid beam search中候选路径可能长度不同因为分组允许不同时间步竞争所以长度惩罚必须参与每一步的排序而不能放到最后统一算。这也是为什么我在伪代码里显式写出length_penalty(step)它本质上是对不同组之间的一种时间公平性补偿。第四个细节batch解码的实现。如果你的模型是PyTorch你可能会想直接用batch size等于beam size的方式并行解码。这个思路在标准beam search中是常规操作但grid beam search里不同组可能处于不同时间步batch内的序列长度不一致不能直接丢进模型。我当时花了挺长时间处理这个问题最后用了一个带padding mask的方案按最大时间步padding同时用mask把超出的部分屏蔽掉。这个操作确实增加了一些代码复杂度但推理速度能提升好几倍值得做。4.3 参数调优的直觉经验关于超参论文里给了一些参考值但真正用起来还是得靠经验。我最常用的三个参数beam size、分组数、覆盖度惩罚系数LAMBDA它们各有各的调法。beam size建议从5到10起步太小体现不出网格优势太大推理耗时陡增。分组数一般取2到4它的作用类似于“多样性温度计”分组越多组间多样性越强但每组内部可用的搜索空间变小生成质量容易下降。LAMBDA则是一个需要小心调的参数给太大算法会为了追求多样性而牺牲质量生成结果变得散乱给太小又回归到标准beam search的老路上去。我一般从0.1开始试每次调0.05的增量观察验证集上的ROUGE或者BLEU曲线找到一个平稳区间再固定下来。还有一个容易忽略的点覆盖度惩罚的相似度阈值。不同的任务对“相似”的定义不同。机器翻译任务里两条候选只要前缀不同就算有差异摘要任务里却要小心处理同义词替换的情况。我的做法是在相似度计算前先做一个简单的归一化比如把数字统一替换成占位符、把英文大小写归一化这个小改动可以让覆盖度的计算更稳定。话说回来这个实现只是我对论文思路的一个理解版本未必和作者团队的开源代码逐行一致。如果你要去复现我建议把它当成“参考put琳思路的基准实现”而不是“论文官方实现的替代品”动手前最好再对照原始论文和官方代码。5. 我在复现和落地中踩过的大坑5.1 排序逻辑写错导致多样性失效我第一次实现grid beam search时想偷个懒觉得“反正最后都是按最终分数排序剪枝只要我在打分函数里加了覆盖度惩罚就行了”。结果跑出来的效果跟标准beam search几乎一模一样多样性根本没啥提升。排查了很久才发现问题我在计算覆盖度惩罚时用的是“当前候选与上一轮所有候选的相似度”而不是“当前候选与已经入选最终集合的候选的相似度”。这两个看似差不多实际上差别巨大——上一轮的候选里有大量已经被剪枝掉的“死路径”拿它们当比较基准等于拿淘汰选手当裁判惩罚项就完全失去了意义。正确做法是把排序和剪枝分成两步先按原始分数取前2倍beam size的候选作为“候补池”再在候补池内两两计算覆盖度用最终分数排名选出top-k。这样算覆盖度的对象都是真正有潜力进入最终集合的候选惩罚项的作用才能真正发挥出来。5.2 推理速度比预期慢4倍标准beam search的推理耗时我可以控制在可接受范围内但第一版grid beam search跑起来直接慢了4倍以上一度想放弃这个方案。慢的主要原因是覆盖度计算。我最初的实现针对每个新候选都要对比所有已保留候选的最长公共子序列算法复杂度大概是O(n的平方乘以序列长度)。beam size为6时还能忍beam size拉大到10单步耗时直接翻了几倍。后来做两个改动速度才压下来。第一个改动是前面提过的“候补池”策略把覆盖度计算的候选对数量压缩到大约四分之一。第二个改动是把字符串相似度换成token ID序列的相似度避免反复调字符串库直接用数组循环做前缀哈希匹配。这两个优化合起来最终耗时压缩到只比标准beam search慢1.5倍左右属于可接受的工程代价。5.3 什么时候不该用grid beam search这个坑可能更多是决策上的而不是实现上的。有一段时间我特别迷信grid beam search什么任务都想往上套结果在开放域对话生成这个场景里碰了一鼻子灰。对话任务的特殊性在于它没有“唯一正确答案”用户更看重回复的多样性和趣味性。grid beam search的覆盖度机制虽然能生成多样化的候选但由于它保留了比较严格的分数排序逻辑最终输出的那些高分路径依然偏“安全”。相比直接加temperature或top-p采样grid beam search在对话生成上的“惊喜感”不足。我的结论是如果任务目标是一个确定性的指标翻译、摘要、关键词生成grid beam search是比标准beam search更稳的选择如果任务是开放式的创造型内容对话、故事续写采样类方法可能更有优势。工程选型不要为了用某个算法而用某个算法而是要看你最终要的是稳定性还是多样性。5.4 与长度惩罚参数打架的坑最后一个坑是参数耦合问题。前面说过grid beam search的长度惩罚要参与每一步排序但实际调参时我发现当beam size较大、分组数较多时长度惩罚和覆盖度惩罚会出现“打架”现象长度惩罚偏大算法会倾向短序列覆盖度惩罚偏大算法会倾向长序列。两个惩罚项在同一个最终得分里互相拉扯结果就是生成文本长度忽长忽短质量非常不稳定。解决思路是把这两个惩罚项解耦。我的做法是长度惩罚只参与候选内部的分数计算即组内排序时使用而覆盖度惩罚只参与组间排序和最终剪枝即跨组竞争时使用。这样两项各管一段互不干扰调参时也能更清晰地分析各自的影响。6. 最后说几点我的真实体会Grid beam search这篇论文给我最大的价值不是它提了一个多么精妙的新算法而是它重新提醒了我一件事搜索策略本质上是一个“探索-利用”的平衡问题没有免费的午餐。标准beam search选择了“利用”——每一步都往分数最高的方向走grid beam search在利用之外加了“探索”——保留那些路径差异更大、覆盖度更高的候选。我在自己的实际项目中用下来的整体感受是对于翻译、摘要这类质量指标明确、结果可评测的任务grid beam search确实能带来稳定且可感知的收益尤其是当你被标准beam search的重复输出、分数抖动、超参敏感折磨过之后换到grid beam search会有一种“终于稳了”的踏实感。当然它也有代价实现复杂度更高推理耗时更久调参需要更多耐心。如果你的项目对延迟极其敏感或者你的任务本身是开放生成型那我建议先评估清楚再用不必盲目跟风。最后再分享一个小技巧如果你想在现有代码里快速验证grid beam search是否适合你的任务可以先不改模型只把解码方式从标准beam search换成简化版分组覆盖度剪枝就是伪代码里的那套简化逻辑在验证集上跑一轮小规模评测对比一下输出文本的重复率和指标波动。如果连简化版都能看出明显改善再投入精力做完整的网格搜索实现也不迟。这篇论文值得所有做生成任务的NLP工程师读一读哪怕最后你没有用它替换beam search仅仅是理解了“覆盖度”这个维度在搜索算法中的意义就已经值回票价了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑