CH 07 / SOURCE EXAMPLE
beam_search_generation.py
ch07 重排模块/code/ch03_list_generation/beam_search_generation.py
这个示例讲什么
│ └── beam_search_generation.py 逐位置 Beam Search + 业务约束剪枝,与穷举最优对齐
对应正文第 7 章 §7.3「基于束搜索(beam search)的序列生成方法」。 本实现做了什么 ---------------- 1. 复刻正文 code_beam_search_core 的思路:在 beam search 的每一步对每条路径的权重向量 再做一次高斯扰动采样,用新权重贪心排序产出新序列,按序列分保留 top-beam_size 条; 2. 另外实现正文正文更强调的「逐位置扩展 + 业务规则剪枝」版 beam search: 状态 = 已选前缀,每步枚举所有未选候选,嵌入正文给出的硬约束 「相同垂类内容不得连续出现」,对非法路径提前剪枝,保留 top-K 前缀; 3. 对比三者:单次贪心、权重扰动式 beam search、逐位置 beam search 的 序列分与「相邻同垂类对数」,并统计逐位置 beam search 剪掉了多少条非法路
上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。
源码结构与阅读入口
build_items· 第 42 行point_score· 第 54 行seq_score· 第 58 行位置衰减加权的序列分,w_t = 1/t。greedy· 第 63 行beam_search_weight_perturb· 第 68 行正文 code_beam_search_core 的口径:beam 中保存(序列, 分数, 权重),beam_search_positionwise· 第 90 行逐位置扩展的 beam search,带「相邻同垂类禁止」硬约束剪枝。adj_same_cat· 第 114 行fmt· 第 118 行main· 第 123 行
1"""对应正文第 7 章 §7.3「基于束搜索(beam search)的序列生成方法」。23本实现做了什么4----------------51. 复刻正文 code_beam_search_core 的思路:在 beam search 的每一步对每条路径的权重向量6 再做一次高斯扰动采样,用新权重贪心排序产出新序列,按序列分保留 top-beam_size 条;72. 另外实现正文正文更强调的「逐位置扩展 + 业务规则剪枝」版 beam search:8 状态 = 已选前缀,每步枚举所有未选候选,嵌入正文给出的硬约束9 「相同垂类内容不得连续出现」,对非法路径提前剪枝,保留 top-K 前缀;103. 对比三者:单次贪心、权重扰动式 beam search、逐位置 beam search 的11 序列分与「相邻同垂类对数」,并统计逐位置 beam search 剪掉了多少条非法路径;124. 打印穷举规模 P(N,T) 与 beam search 实际扩展的节点数,说明剪枝带来的复杂度收益。1314关键公式15--------16- 路径分:score(pi) = sum_{t=1..T} w_t_decay * <w, pxtr_{i_t}>,这里位置衰减 w_t_decay = 1/t17 (正文 §7.4 的位置偏置权重 w_i = 1/i);18- beam search:每步把 B 条路径各扩展 |剩余候选| 个子节点,按 score 截断回 B 条,19 复杂度 O(T * B * N) 远小于穷举 P(N, T) = N!/(N-T)!;20- 权重扰动:w' ~ N(w, Sigma),Sigma = diag(0.01)。2122注:正文代码用 numpy + pandas,本脚本只用 numpy。23"""2425import math26import random2728import numpy as np2930TARGETS = ["p_evr", "p_lvr", "p_ltr", "p_cmr", "p_ftr"]31CATEGORIES = ["影视", "美食", "萌宠", "知识", "游戏"]3233BOOK_TABLE = [34 [0.374540, 0.020584, 0.611853, 0.607545, 0.122038],35 [0.950714, 0.969910, 0.139494, 0.170524, 0.495177],36 [0.731994, 0.832443, 0.292145, 0.065052, 0.034389],37 [0.598658, 0.212339, 0.366362, 0.948886, 0.909320],38 [0.156019, 0.181825, 0.456070, 0.965632, 0.258780],39]404142def build_items(num_items=10):43 rows = [list(r) for r in BOOK_TABLE]44 while len(rows) < num_items:45 rows.append([round(random.random(), 6) for _ in TARGETS])46 items = []47 for i in range(num_items):48 # 垂类人为做成头部效应:影视占一半,便于观察「同垂类不得连续」的剪枝效果49 cat = "影视" if i % 2 == 0 else CATEGORIES[1 + (i // 2) % 4]50 items.append({"idx": i, "item_id": "item_%d" % i, "pxtr": rows[i], "cat": cat})51 return items525354def point_score(item, w):55 return float(np.dot(w, item["pxtr"]))565758def seq_score(seq, w):59 """位置衰减加权的序列分,w_t = 1/t。"""60 return sum(point_score(it, w) / (t + 1) for t, it in enumerate(seq))616263def greedy(items, w, top_t):64 ranked = sorted(items, key=lambda it: -point_score(it, w))65 return ranked[:top_t]666768def beam_search_weight_perturb(items, mean, cov_scale, top_t, beam_size=3, num_steps=5,69 rng=None):70 """正文 code_beam_search_core 的口径:beam 中保存(序列, 分数, 权重),71 每步对每条路径的权重再采样 beam_size 组,用贪心重新生成整条序列。"""72 cov = np.diag([cov_scale] * len(mean))73 beam = []74 for w in rng.multivariate_normal(mean, cov, beam_size):75 seq = greedy(items, w, top_t)76 beam.append((seq, seq_score(seq, w), w))77 expanded = beam_size78 for _ in range(num_steps):79 cands = []80 for seq, score, w in beam:81 for new_w in rng.multivariate_normal(w, cov, beam_size):82 new_seq = greedy(items, new_w, top_t)83 cands.append((new_seq, seq_score(new_seq, new_w), new_w))84 expanded += 185 cands.sort(key=lambda x: -x[1])86 beam = cands[:beam_size]87 return beam, expanded888990def beam_search_positionwise(items, w, top_t, beam_size=4, no_same_cat_adjacent=True):91 """逐位置扩展的 beam search,带「相邻同垂类禁止」硬约束剪枝。9293 返回 (beam, expanded, pruned):beam 为 [(序列, 分数)],按分数降序。94 """95 beam = [([], 0.0)]96 expanded = pruned = 097 for t in range(top_t):98 cands = []99 for seq, score in beam:100 used = {it["idx"] for it in seq}101 for it in items:102 if it["idx"] in used:103 continue104 expanded += 1105 if no_same_cat_adjacent and seq and seq[-1]["cat"] == it["cat"]:106 pruned += 1 # 中间步骤提前剔除非法路径107 continue108 cands.append((seq + [it], score + point_score(it, w) / (t + 1)))109 cands.sort(key=lambda x: -x[1])110 beam = cands[:beam_size]111 return beam, expanded, pruned112113114def adj_same_cat(seq):115 return sum(1 for a, b in zip(seq, seq[1:]) if a["cat"] == b["cat"])116117118def fmt(seq):119 return " > ".join("%s(%s)" % (it["item_id"].replace("item_", "i"), it["cat"])120 for it in seq)121122123def main():124 random.seed(42)125 rng = np.random.default_rng(42)126127 items = build_items(10)128 top_t, n = 6, 10129 mean = np.array([0.2, 0.3, 0.2, 0.2, 0.1])130131 print("=" * 84)132 print("§7.3 基于 Beam Search 的序列生成(含业务规则剪枝)")133 print("=" * 84)134 print("\n[0] 候选集:%d 个物品,目标序列长度 T=%d" % (n, top_t))135 print(" 垂类分布:%s" % ", ".join(136 "%s=%d" % (c, sum(1 for it in items if it["cat"] == c)) for c in CATEGORIES))137 print(" 穷举排列数 P(%d,%d) = %d" % (n, top_t, math.perm(n, top_t)))138139 # ---------- A. 单次贪心 ----------140 g_seq = greedy(items, mean, top_t)141 print("\n[1] 基线:单次贪心(基准权重)")142 print(" %s" % fmt(g_seq))143 print(" 序列分(1/i 衰减)=%.4f,相邻同垂类对数=%d" % (seq_score(g_seq, mean),144 adj_same_cat(g_seq)))145146 # ---------- B. 权重扰动式 beam search ----------147 beam, expanded = beam_search_weight_perturb(items, mean, 0.01, top_t,148 beam_size=3, num_steps=5, rng=rng)149 print("\n[2] 权重扰动式 Beam Search(beam_size=3, steps=5,正文代码口径)")150 print(" 共扩展 %d 个候选序列,最终保留 3 条:" % expanded)151 for i, (seq, score, w) in enumerate(beam):152 print(" 候选%d 分数=%.4f 权重=%s" % (i + 1, score, np.round(w, 3).tolist()))153 print(" %s 相邻同垂类=%d" % (fmt(seq), adj_same_cat(seq)))154 print(" 注:该口径每条路径仍由「贪心全量重排」产出,权重是唯一的搜索变量,")155 print(" 因此无法保证满足「相邻不同垂类」这类序列级硬约束。")156157 # ---------- C. 逐位置 beam search + 剪枝 ----------158 for bs in (1, 4, 16):159 pb, exp2, pruned = beam_search_positionwise(items, mean, top_t, beam_size=bs)160 best, bscore = pb[0]161 print("\n[3] 逐位置 Beam Search(beam_size=%d,相邻同垂类剪枝)" % bs)162 print(" 扩展节点 %d 个,其中 %d 个被约束剪掉(%.1f%%)"163 % (exp2, pruned, 100.0 * pruned / exp2))164 print(" 最优:%s" % fmt(best))165 print(" 序列分=%.4f,相邻同垂类对数=%d" % (bscore, adj_same_cat(best)))166167 # ---------- D. 与穷举最优对比(T=6, N=10 可枚举) ----------168 print("\n[4] 与穷举最优对比(合法序列中,位置衰减序列分最大者)")169 import itertools170 best_exh, best_v = None, -1e18171 legal = 0172 for perm in itertools.permutations(items, top_t):173 if adj_same_cat(perm):174 continue175 legal += 1176 v = seq_score(perm, mean)177 if v > best_v:178 best_exh, best_v = perm, v179 print(" 合法排列 %d / %d 条" % (legal, math.perm(n, top_t)))180 print(" 穷举最优:%s" % fmt(list(best_exh)))181 print(" 穷举最优序列分=%.4f" % best_v)182 pb16, _, _ = beam_search_positionwise(items, mean, top_t, beam_size=16)183 print(" beam_size=16 的最优分=%.4f,与穷举最优差距=%.6f"184 % (pb16[0][1], best_v - pb16[0][1]))185186 print("\n[结论] beam search 把搜索从 P(N,T)=%d 条降到 O(T*B*N)=%d 次扩展;"187 % (math.perm(n, top_t), top_t * 16 * n))188 print(" 同时「相邻同垂类禁止」等业务约束能在中间步骤剪枝,是贪心生成做不到的。")189190191if __name__ == "__main__":192 main()如何用于学习或编写新代码
- 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
- 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
- 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
- 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。