在 Fairseq 中使用带动态 beam 分配的词法约束解码(Lexically Constrained Decoding)
【免费下载链接】unilmLarge-scale Self-supervised Pre-training Across Tasks, Languages, and Modalities项目地址: https://gitcode.com/GitHub_Trending/un/unilm
导读
本文以 Fairseq 仓库(位于kosmos-2/fairseq)中examples/constrained_decoding示例为线索,系统讲解词法约束解码(Lexically Constrained Decoding)的原理、命令行用法与源码实现。词法约束解码允许你在解码时强制目标输出中出现指定的词或短语(如人名、术语、多词短语),常用于机器翻译中的术语翻译控制与单语改写场景。读完本文,你将掌握--constraints参数的完整用法、输入输出格式、有序/无序两种约束模式的差异,并能从源码层面理解LexicallyConstrainedBeamSearch的动态 beam 分配机制。
一、词法约束解码是什么
词法约束解码的核心思想是:解码器在生成每个目标词时,除了依据语言模型打分,还必须保证最终输出包含用户预先指定的词或短语序列。在 Fairseq 中,这一功能由fairseq/search.py中的LexicallyConstrainedBeamSearch实现,它基于以下两篇论文:
- Fast Lexically Constrained Decoding With Dynamic Beam Allocation(Post & Vilar, NAACL 2018):提出了带动态 beam 分配的快速词法约束解码算法;
- Improved Lexically Constrained Decoding for Translation and Monolingual Rewriting(Hu et al., NAACL 2019):在向量化层面改进约束处理,使其同时适用于翻译与单语改写。
约束可以是单个 token 或多个 token 组成的短语(如 "hard" 或 "to influence"),同一个约束短语还可以在输出中多次出现(例如 "Berlin" 出现两次,则输出中必须包含两处 "Berlin")。这种能力让机器翻译系统在遇到专有名词、产品名、法律条款等场景时,能够按用户给定的术语表强制生成指定译法,从而显著提升术语一致性。
二、快速开始:用--constraints跑通第一个例子
2.1 启用方式与输入格式
在 Fairseq 中,约束解码无需修改任何模型代码,只需在fairseq-interactive命令后追加--constraints参数即可开启。输入行中,源句与每个约束之间用Tab 分隔,每个约束是一个独立字段(可含多个 token):
源句\t约束1\t约束2\t...下面的命令使用 Fairseq 的 WMT19 德英模型,将德语句子Die maschinelle Übersetzung ist schwer zu kontrollieren.翻译成英文,并强制输出中必须包含约束hard与to influence:
echo -e "Die maschinelle Übersetzung ist schwer zu kontrollieren.\thard\ttoinfluence" \ | normalize.py | tok.py \ | fairseq-interactive /path/to/model \ --path /path/to/model/model1.pt \ --bpe fastbpe \ --bpe-codes /path/to/model/bpecodes \ --constraints \ -s de -t en \ --beam 10命令中的normalize.py与tok.py位于本 README 同目录下(即kosmos-2/fairseq/examples/constrained_decoding/),它们只是 Fairseq WMT19 预处理流程的快捷封装。注意这里约束to influence写成了toinfluence——因为 BPE 切分后influence本身就是一个词片,无需空格;同理约束中的多词短语也应以模型 BPE 切分后的形式给出,这是约束能否命中的关键细节。
2.2 输出格式解读
上述命令会产生如下输出:
[snip] S-0 Die masch@@ in@@ elle Über@@ setzung ist schwer zu kontrollieren . W-0 1.844 seconds C-0 hard C-0 influence H-0 -1.5333266258239746 Mach@@ ine trans@@ lation is hard to influence . D-0 -1.5333266258239746 Machine translation is hard to influence . P-0 -0.5434 -0.1423 -0.1930 -0.1415 -0.2346 -1.8031 -0.1701 -11.7727 -0.1815 -0.1511各字段含义:
S-0:源句及其 BPE 切分结果;W-0:解码耗时;C-0:约束回显行,每一行对应一个约束(此处为hard与influence,注意输出中是 BPE 后的形式),用于确认约束已正确送入解码器;H-0:假设(hypothesis)及其累计得分,BPE 形式;D-0:去 BPE 后的最终译文,可见hard与influence均被强制出现在译文中;P-0:每个生成 token 的位置得分(positional scores)。
对比可知,未加约束时模型很可能输出 "difficult to control" 之类的译法,而加了约束后译文的措辞被精确锁定为 "hard to influence"。
三、约束的输入与预处理链路
从 interactive.py 的make_batches可以看到约束从文本到张量的完整转换流程(对应 interactive.py):
- 切分字段:当
cfg.generation.constraints开启时,对每一行按\t切分,第一个字段是源句,其余字段是约束; - 编码约束:每个约束字符串经
task.target_dictionary.encode_line(...)编码为 token id 序列(append_eos=False, add_if_not_exist=False,即不允许约束中出现 OOV 词); - 打包成张量:调用
pack_constraints将「句子 × 约束」的嵌套列表打成 batch 级张量,随 batch 一起进入推理流程。
pack_constraints的打包格式非常巧妙(见 token_generation_constraints.py):每一行是一个句子所有约束的拼接序列,行首第一个元素是「该句的约束数量」,每个约束之后补一个0作为分隔符。例如 batch 中有 3 个句子、约束数分别为 3/0/1 时,打包结果形如:
[ [ 3 3 1 2 0 3 0 4 5 6 7 0], [ 0 0 0 0 0 0 0 0 0 0 0 0], [ 1 1 8 9 10 1 4 11 12 0 0 0] ]行首的3、0、1即各句约束个数。整行宽度(maxlen)取所有句子中「约束数 + 各约束长度之和 + 1」的最大值。对应的unpack_constraints则负责在解码后把单行张量还原成约束列表。在fairseq-cli generate(批量生成)场景下,约束则通过sample["constraints"]从数据集读取并传给 generator(见 generate.py)。
四、有序约束与无序约束:--constraintsvs--constraints unordered
约束解码支持两种模式,对应LexicallyConstrainedBeamSearch初始化的representation参数(ordered / unordered,见 search.py):
| 模式 | 命令行写法 | 行为 | 数据结构 |
|---|---|---|---|
| 有序 | --constraints | 约束按输入顺序依次出现,约束之间允许出现任意数量(0 个或多个)的其他 token | OrderedConstraintState |
| 无序 | --constraints unordered | 解码器自行决定约束的出现顺序,尝试全部C!种顺序 | UnorderedConstraintState |
默认的有序模式对大多数场景(如术语表按原文语序排列)已经足够;无序模式会探索更多顺序,输出更灵活,但搜索空间更大。两种模式的共同点在于:约束之间永远允许插入任意内容,约束内部则必须连续生成(多 token 短语不可拆散)。
4.1 OrderedConstraintState:线性状态机
有序模式把 C 个约束拼接成一条线性序列,用单个整数状态指针(state)记录进度,见 token_generation_constraints.py。advance(token)的转移规则直观:
- 若下一个待生成 token 恰好匹配
sequence[state + 1],则指针前进一步; - 若当前正处于某个约束的终点(endpoint),则可以接受任意 token(约束之间的「间隔」);
- 若 token 等于整个序列的第一个 token,则回到状态 0(重新开始);
- 否则回到根状态 -1(从头再来)。
finished属性即state + 1 == len(sequence),表示所有约束都已生成完毕;next_tokens()只返回「下一个约束 token + 序列首 token」两个候选,搜索空间极小。
4.2 UnorderedConstraintState:Trie 驱动的自动机
无序模式将全部约束(含重复约束)建成一棵Trie(前缀树),节点类ConstraintNode记录terminal(此节点结束了几条约束)与num_constraints(子树内约束总数),见 token_generation_constraints.py。每个 beam 假设持有一个UnorderedConstraintState,通过advance(token)游走在 Trie 节点之间:
- 若当前节点存在匹配子节点且该路径未被「饱和」(
generated[child] < child.num_constraints),则深入子节点; - 若无法深入,则「回退(rewind)」到根节点,回退沿途检查是否有已满足的前缀约束并标记为
completed; - 状态用
generated(已生成节点计数)、completed(已完成约束计数)、needed_tokens(剩余必需 token 集合)共同刻画,finished当且仅当所有约束均已完成。
五、实现细节:动态 beam 分配的核心算法
约束解码的心脏是 search.py 中的LexicallyConstrainedBeamSearch。它继承自Search基类并设置self.supports_constraints = True;若用户传入约束而当前搜索器不支持,sequence_generator.py会直接报错(见 sequence_generator.py),确保约束不会被静默忽略。
与传统 Beam Search 不同,它需要同时维护 beam 中每个假设的约束进度,并在每个时间步做 8 步处理(step与step_sentence方法):
- STEP 0(防提前结束):对尚未完成约束的假设,把
<eos>的 log 概率置为-inf,防止模型在约束未满足时就收尾; - STEP 1(候选构建):候选池由三部分组成——全 beam 上
top 2×beam_size、每个假设的top-1(each_k=1)、以及每个假设的「下一个约束 token」集合; - STEP 2(注入约束):遍历每个 beam 假设,把
state.next_tokens()中所有合法约束 token 加入候选,并以对应位置的 log 概率作为得分(见 search.py); - STEP 3(计算 bank):对每个候选执行
advance(token)得到新状态,bank即该候选「已生成的约束数量」,这是后续动态分配的排序依据; - STEP 4(按 (bank, score) 排序):通过巧妙的排序键
(num_constraint_tokens - bank) * MAX_SCORE + score实现先按 bank、再按得分排序; - STEP 5(去重):把
(beam, token_id)映射到一维,用 roll 移位比较法去除重复候选; - STEP 6(round-robin 分配):这是「动态 beam 分配」的精髓——对排序后的候选按 bank 分桶,跨 bank 做轮转分配(stripe):依次取 bank C 的最佳、bank C-1 的最佳、…… 再取 bank C 的次佳、bank C-1 的次佳……,保证进展快(bank 高)的假设优先占位,又不至于垄断整个 beam;
- STEP 7/8(排序与截断):按 stripe 值排序后截取前
num_cands = 2 × beam_size个候选作为下一时间步的 beam。
因为有了「按 bank 轮转」的精细分配,约束搜索不再需要像传统做法那样剪枝(prune)beam,beam 宽度只需 10 甚至 5 就往往足够。这正是论文标题中 "Dynamic Beam Allocation" 的含义。
六、与 Sockeye 实现的差异
Fairseq 的实现在设计上与 Sockeye 的约束解码存在若干差异(详见原文档),这些差异也是选择 Fairseq 实现时的理由:
- 有序生成:默认的「按输入顺序生成约束」模式在 Sockeye 中不可用;
- 无需剪枝:得益于改进的 beam 分配方法,Fairseq 无需对 beam 进行剪枝;
- beam 更小:同样因为分配更优,beam 宽度 10 甚至 5 通常就已足够;
- 向量化扩展:Hu et al.(NAACL 2019)描述的向量化扩展(trie 约束版本)从未被合并进 Sockeye 主分支,而 Fairseq 的
LexicallyConstrainedBeamSearch原生支持。
七、引用文献
词法约束解码的第一篇论文(Grid Beam Search)为:
@inproceedings{hokamp-liu-2017-lexically, title = "Lexically Constrained Decoding for Sequence Generation Using Grid Beam Search", author = "Hokamp, Chris and Liu, Qun", booktitle = "Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)", month = jul, year = "2017", address = "Vancouver, Canada", publisher = "Association for Computational Linguistics", url = "https://www.aclweb.org/anthology/P17-1141", doi = "10.18653/v1/P17-1141", pages = "1535--1546", }Fairseq 实现所依赖的两篇扩展工作分别为:
@inproceedings{post-vilar-2018-fast, title = "Fast Lexically Constrained Decoding with Dynamic Beam Allocation for Neural Machine Translation", author = "Post, Matt and Vilar, David", booktitle = "Proceedings of the 2018 Conference of the North {A}merican Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long Papers)", month = jun, year = "2018", address = "New Orleans, Louisiana", publisher = "Association for Computational Linguistics", url = "https://www.aclweb.org/anthology/N18-1119", doi = "10.18653/v1/N18-1119", pages = "1314--1324", }@inproceedings{hu-etal-2019-improved, title = "Improved Lexically Constrained Decoding for Translation and Monolingual Rewriting", author = "Hu, J. Edward and Khayrallah, Huda and Culkin, Ryan and Xia, Patrick and Chen, Tongfei and Post, Matt and Van Durme, Benjamin", booktitle = "Proceedings of the 2019 Conference of the North {A}merican Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers)", month = jun, year = "2019", address = "Minneapolis, Minnesota", publisher = "Association for Computational Linguistics", url = "https://www.aclweb.org/anthology/N19-1090", doi = "10.18653/v1/N19-1090", pages = "839--850", }八、深入阅读指引
如需进一步钻研,可在本仓库内查阅以下关键文件:
- 示例与预处理脚本:kosmos-2/fairseq/examples/constrained_decoding/(本 README、normalize.py、tok.py)
- 核心搜索算法:search.py(
LexicallyConstrainedBeamSearch及其 8 步动态分配流程) - 约束状态机与 Trie:token_generation_constraints.py(
pack_constraints、OrderedConstraintState、UnorderedConstraintState) - 命令行入口:interactive.py(约束解析与打包)、generate.py(批量推理)
- 解码主循环:sequence_generator.py(约束初始化与每步更新)
值得留意的是,fairseq/search.py中还存在LengthConstrainedBeamSearch等变体,它们与词法约束解码同属「解码期控制」家族,但机制完全不同,读者在阅读源码时应加以区分。
【免费下载链接】unilmLarge-scale Self-supervised Pre-training Across Tasks, Languages, and Modalities项目地址: https://gitcode.com/GitHub_Trending/un/unilm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考