🏠 总目录📚 本教程 27 · 实战与挑战项目
📑 本页目录(点开跳转)

27 · 实战与挑战项目

项目 3 小时 – 2 天 | 挑战 2 – 4 天 | ⭐⭐ 这个板块最大的便宜:所有算法都能用几十行纯 Python 写出来


🎯 一句话

别的板块要动手得先有 GPU、数据集和一堆库;这一套从头到尾只需要 Python 自带的 itertoolsfractions —— 而且每个算法都有一份「书上的标准答案」可以对账。

前面 25 章每一章都留了完整的手算例子。这一章就是把那些例子变成 assert


🧰 开工前:三条这个板块特有的规矩

规矩 为什么
① 零依赖、零数据 pip install 一次都不用。玩家 3 个、物品 5 件、选民 14 人 —— 规模全在纸笔量级
⭐⭐ ② 先复现书上的例子 每个项目的第一步都是跑出正文那个手算结果。跑不出书上的数字就是写错了 —— 这是别的板块给不了的调试条件
③ 用 Fraction 不要用 float 混合 NE 是 2/3、Shapley 是 4/6、PS 概率是 3/4,全是分数

⚠️ 第三条之外还有一条:算法都是确定性的,没有随机种子 —— 唯一的不确定性来自 ⭐ 「并列怎么打破」,而它偏偏能改结果(项目一、项目四都会撞上)。


🥉 项目一:投票规则对比器(3 – 5 小时)

目标:亲手跑出第 24 章那句话 —— 同一份选票,六条规则,五个不同的赢家; 再补上第 25 章Copeland 凑成第七条。 依赖24 · 25 · 26

from collections import Counter

# 第 24 章那份 14 人选票:每个元素是一位选民「从最爱到最恨」的排序
PROFILE = ([list("acbde")] * 5 + [list("ebcda")] * 4
           + [list("dcbea")] * 3 + [list("bdeca")] * 2)
CANDS = sorted(set(PROFILE[0]))

def positional(profile, cands, weights):        # ⭐ 一个函数覆盖一整族计分规则
    s = {c: 0 for c in cands}
    for ballot in profile:
        for j, c in enumerate(ballot):
            s[c] += weights[j]
    return s

def pairwise(profile, x, y):                    # 把 x 排在 y 前面的人数
    return sum(1 for b in profile if b.index(x) < b.index(y))

def condorcet(profile, cands):                  # ⭐ 可能返回 None —— 那才是重点
    for x in cands:
        if all(pairwise(profile, x, y) > pairwise(profile, y, x)
               for y in cands if y != x):
            return x
    return None

m = len(CANDS)
print("Plurality ", positional(PROFILE, CANDS, [1] + [0] * (m - 1)))
print("Borda     ", positional(PROFILE, CANDS, list(range(m - 1, -1, -1))))
print("Condorcet ", condorcet(PROFILE, CANDS))

🎯 该跑出来的数字

规则 得分 赢家
Plurality a 5 | b 2 | c 0 | d 3 | e 4 a
2-approval a 5 | b 6 | c 8 | d 5 | e 4 c
3-approval a 5 | b 14 | c 12 | d 5 | e 6 b
Borda a 20 | b 36 | c 34 | d 27 | e 23(总分 140 b
两轮决选 a 对 e = 5 : 9 e
IRV 四轮依次淘汰 b → e → c,最后 d 9 : a 5 d
Copeland a 0 | b 3 | c 4 | d 2 | e 1 c

⭐⭐ 五个候选人,每一个都当选过。 而最后一行更刺眼:赢家 a 的出度是 0 —— 它输给了所有人。

✅ 通关:七行数字逐格对上(Borda 总分 140 是最好用的自查);悖论上 condorcet() 返回 NoneT6 的 IIA 翻转跑出来了((a 6, b 7, c 2) → (a 6, b 4, c 5),没有任何人改过对 a、b 的看法)。


🥉 项目二:纳什均衡求解器(1 天)

目标:把第 10 – 13 章的四套手算法变成代码,并让它们互相验证依赖10 · 11 · 12 · 13

def pure_ne(U1, U2):
    """U1[i][j]、U2[i][j] 是行/列玩家在第 i 行第 j 列的收益。返回全部纯策略 NE。"""
    nR, nC = len(U1), len(U1[0])
    out = []
    for j in range(nC):
        colmax = max(U1[i][j] for i in range(nR))       # 逐列圈行玩家
        for i in range(nR):
            if U1[i][j] != colmax:                      # ⚠️ 并列最大值必须全圈
                continue
            rowmax = max(U2[i][k] for k in range(nC))   # 逐行圈列玩家
            if U2[i][j] == rowmax:                      # ⭐ 双圈 = 纯策略 NE
                out.append((i, j))
    return out

print("囚徒困境", pure_ne([[2, 0], [3, 1]], [[2, 3], [0, 1]]))   # [(1, 1)]
print("性别之争", pure_ne([[2, 0], [0, 1]], [[1, 0], [0, 2]]))   # [(0,0), (1,1)]
print("罚球    ", pure_ne([[-1, 1], [1, -1]], [[1, -1], [-1, 1]]))  # [] 没有纯 NE

🎯 该跑出来的数字

博弈 结果
第 11 章那个 3×3 唯一双圈 (c, y) = (3, 5);(a,x)、(b,z) 都只有单圈
性别之争 两个纯 NE + 混合 p = 2/3, q = 1/3,两人期望都是 2/3
罚球 / 石头剪刀布 纯 NE 一个都没有;混合分别是 (1/2, 1/2)(1/3, 1/3, 1/3)
第 10 章那个 3×3 IESDS 四步(C 支配 R → M 支配 D → C 支配 L → M 支配 U),剩 (M, C) = (3, 3)
第 13 章那个零和 3×3 行最小 (1, 2, 0)、列最大 (3, 2, 6),相等 → 鞍点 (b, y),v = 2

✅ 通关:上表全对;T2 的 1000 次随机测试零反例;⭐ T6 的对比做出来了 —— 零和里 maximin 组合就是 NE,而性别之争里行玩家 maximin 是 1/3、混合 NE 却是 2/3


🥈 项目三:稳定匹配求解器(1 天)

目标:把「谁提案,谁受益」和「没有稳定机制能对双方同时防伪」从结论变成你自己搜出来的证据。 依赖17 · 18(TTC 是加分项)

def da(proposers, receivers):
    """一对一延迟接受。两个参数都是 dict:名字 -> 偏好列表(从最想要开始)。"""
    held, free, nxt = {}, list(proposers), {s: 0 for s in proposers}
    while free:
        s = free.pop(0)
        c = proposers[s][nxt[s]]; nxt[s] += 1          # 向还没拒过自己的最高一位提案
        cur = held.get(c)
        if cur is None:
            held[c] = s                                # 空着,暂收
        elif receivers[c].index(s) < receivers[c].index(cur):
            held[c] = s; free.append(cur)              # ⭐ 来了更好的,把手里的踢掉
        else:
            free.append(s)                             # 被拒,回去继续提案
    return {v: k for k, v in held.items()}

S = {"s1": ["c1", "c2", "c3"], "s2": ["c2", "c3", "c1"], "s3": ["c3", "c1", "c2"]}
C = {"c1": ["s2", "s3", "s1"], "c2": ["s3", "s1", "s2"], "c3": ["s1", "s2", "s3"]}
print("SPDA", da(S, C))                                # 学生全拿第 1 志愿
cpda = da(C, S)                                        # ⭐ 换一边提案,同一个函数
print("CPDA", {v: k for k, v in cpda.items()})         # 学校全拿第 1 志愿

🎯 该跑出来的数字

任务 结果
第 17 章例一 SPDA $\{(s_1,c_2),(s_2,c_1),(s_3,c_3)\}$,$s_3$ 被拒 2 次、全程 5 次提案
循环例 SPDA / CPDA $\mu_S$(学生全第 1、学校全第 3)/ $\mu_C$(反过来)—— 同一个函数换个方向
T3 穷举 那个实例恰好有 3 个稳定匹配(学生名次 1,1,1 / 2,2,2 / 3,3,3)
T4 谎报 $c_1$ 谎报后拿到 $s_1$ —— 从第 2 名升到第 1 名
⭐⭐ T5 暴力搜索 46656 个 $n=3$ 实例中:学生端成功谎报 0 个;学校端 864 个(1.9%)
T6 TTC 第 18 章那个 5 人例子三轮成交,结果 $(o_2, o_1, o_3, o_5, o_4)$

⭐⭐ T5 那两个数字是这个项目的灵魂:书上的两句话在这里变成 0 和 864 —— 你亲手把 Roth 1982 的不可能性搜了出来。

✅ 通关:SPDA / CPDA 分别给出 $\mu_S$ 和 $\mu_C$;T3 的穷举结果和它们对得上;T5 跑出 0 / 864


🥈 项目四:公平分配实验室(1 – 2 天)

目标:三个算法跑同一组估值,看 EF1 和 PO 是怎么分家的依赖19 · 20 · 21(加分)

🎯 该跑出来的数字

任务 结果
Round-Robin(第 20 章的表,顺序 1→2) $A_1$ 值 135、$A_2$ 值 45,EF1 ✓
⚠️ 并列的坑 第 2 轮 2 号在 $g_3$、$g_4$ 之间并列(都值 5):取 $g_4$ 得书上的 135 / 45,取 $g_3$ 得 140 / 45两个都是 EF1,报告里必须写清你的打破平局规则
RR 不保 PO 的最小反例 $v_1=(1,1)$、$v_2=(1,0)$ → RR 给 $(1,0)$;换一下就是 $(1,1)$,1 号一点没亏、2 号白赚
嫉妒图破环那一步 两人的值从 5、5 变成 10、40两条边同时消失
MNW(2 人 4 物) 最优 $X_1=\{o_1,o_2\}$、$X_2=\{o_3,o_4\}$,乘积 $8\times5=$ 40,而且它完全 EF
⭐⭐ T5 统计 Round-Robin:EF1 100%,PO 只有约 81%MNW:EF1 100%,PO 100%

T5 是最值钱的一步EF1 那一列两个都是 100%,PO 那一列裂开了。 ⚠️ 百分比取决于估值分布和平局规则 —— 要看的是那道裂缝,不是那个数

✅ 通关:四个判据在书上的正反例上都判对;嫉妒图破环后边数严格下降;T5 的表做出来、能指出 PO 那一列的裂缝


🥈 项目五:Shapley 值计算器(1 天)

目标:把第 23 章算出来,然后 ⭐⭐ 和你工作里在用的 shap 库对一次账依赖22 · 23

🎯 该跑出来的数字

任务 结果
煎饼博弈 $\phi=(6,4,6)$,总和 16 $=v(N)$ ✓
手套博弈 Shapley $(\tfrac46,\tfrac16,\tfrac16)$;⭐ Core 只有 $(1,0,0)$ 一个点(1 号是否决者,非否决者一律拿 0)
⭐⭐ 两者的关系 Shapley 不在 Core 里 —— 1 号和 3 号合计只拿 $\tfrac56<1$,会甩开 2 号重新配对
第 22 章的 $X$ 扫描 单点全 0、$v(12)=50,v(13)=70,v(23)=X,v(N)=100$:$X\le80$ 时 Core 非空,$X=81$ 就空了
Banzhaf(手套) $\eta=(3,1,1)\Rightarrow\beta=(\tfrac35,\tfrac15,\tfrac15)$,⚠️ 和 Shapley 不同,而且不满足效率

⭐⭐ T7 做完你会明白shapbase_value + sum(shap_values) == prediction 就是效率公理TreeSHAP 能取代你那段 $n!$ 循环靠的正是「表示法决定复杂度」。工程上会踩什么坑, 见《模型上线之后》10 · SHAP 能做什么不能做什么

✅ 通关:煎饼博弈 $(6,4,6)$、行和 assert 全程不炸;T3 两条路给出同一个数; ⭐ T6 把「公平」和「稳定」的分歧打印了出来


🥇 挑战 A:拥塞博弈实验室(2 – 3 天)

难度 ★★★☆☆目标:让第 15 章那个三行论证在你屏幕上一行一行地发生。

def phi(cost, counts):
    """Rosenthal 势函数。⚠️ 它不是总成本,是「每加一个人记一次当时的价」。"""
    return sum(sum(cost(x, k) for k in range(1, counts[x] + 1)) for x in counts)

total = lambda cost, cnt: sum(cnt[x] * cost(x, cnt[x]) for x in cnt)
cost = lambda x, k: {"a": 50 + k / 1000, "b": 40 + k / 500, "c": k / 100}[x]
for name, s in [("开局", {"a": 10000, "b": 0, "c": 0}),      # 一万人全走高速路
                ("均衡", {"a": 0, "b": 5000, "c": 5000})]:   # 第 15 章算出的均衡
    print(name, "Φ =", round(phi(cost, s), 2), " 总成本 =", round(total(cost, s), 2))

🎯 该看到的现象

观察点 数字
⚠️ Φ 和总成本不是一回事 开局 Φ 550005 / 总成本 600000;均衡 Φ 350030 / 总成本 500000
⭐⭐ T3 的核心验证 每一步 $\Delta_\Phi$ 都严格为正,而且和那个人省下的成本一分不差 —— 这就是第 15 章那三行论证
T4 收敛 以 100 人一组的粒度从「全走高速路」出发,149 步收敛到 $(0,5000,5000)$,每人 50 分钟
⭐ 高速路上一个人都没有 $t_a(1)=50.001>50$ —— 空着都跑不赢挤满的另外两条。看到 0 别以为算错
T5 Braess 改造前 65 分钟 → 加一条耗时 0 的路之后 80 分钟,PoA = 80/65 ≈ 1.231
⭐⭐ T6 塌给你看 加权拥塞博弈里最佳响应动态绕圈、$\Phi$ 不再单调 —— 纯策略均衡的存在性保证没了

✅ 通关T3 的三列数字打印出来、$\Delta_\Phi=\Delta_{\text{cost}}$ 逐步成立;T4、T5 的数字和书上一致; ⭐ T6 找到一个不收敛的加权实例,并能说清被打破的是哪条前提(成本对使用者不再匿名)。


🥇 挑战 B:操纵率实验室(2 – 4 天)

难度 ★★★★☆目标:⭐⭐ 把 Gibbard–Satterthwaite 定理从一句话变成一张表。

第 26 章说「任何非独裁的满射 SCF 都存在可被谎报操纵的情形」。这个挑战让你把那些情形全部枚举出来、数一遍。

🎯 该跑出来的数字(3 候选 · 3 选民 · 全部 216 个轮廓 · 字母序打破平局)

规则 可被单人操纵的轮廓 占比
Borda 51 / 216 23.6% ⭐ 最高 —— 对上第 24 章那句「极易被策略性投票操纵」
Plurality 36 / 216 16.7%
IRV 24 / 216 11.1%
Copeland 24 / 216 11.1%
⭐⭐ 独裁(永远听 1 号的) 0 / 216 0.0% —— G–S 定理的那个唯一出口

T5(4 候选 4 选民,抽 4000 个轮廓):Plurality ≈33%、Borda ≈51%、IRV ≈18%、Copeland ≈55%,独裁仍是 0%。 ⭐ 候选项一多,所有非独裁规则的可操纵率都明显上升。

T6 单峰域(4 种单峰偏好 → $4^3=64$ 个轮廓):⭐⭐ 中位选民规则 0 / 64 = 0%;IRV、Copeland 也是 0; Borda 6 / 64;⚠️ Plurality 反而是 12 / 64 = 18.8%,比全域的 16.7% 还高。

⭐⭐ 这是第 26 章第六节「两条能绕开的路」的实验版:你没有推翻 G–S,你换了一个更小的输入域。 ⚠️ 但 Plurality 那一行提醒你:域限制救的是「某一条特定规则」,不是「所有规则」。

✅ 通关:216 行的表做出来,四条常见规则非零、独裁恰好为 0;写清了打破平局规则并试过第二种; ⭐⭐ T6 的单峰实验做出来了,能解释「为什么这不叫推翻定理」


📝 报告模板

# 实验:____
## 设定(玩家/候选项/物品的规模,偏好或估值从哪来)
## ⚠️ 打破平局的规则            ← 这个板块特有的一节,必须写 ⭐
## 和书上例子的对账表            ← 每个数字都要有出处(第几章第几节)
## 我搜出来的反例 / 边界案例
## 我原本以为 ___,实际上 ___

⚠️ 第二节是这个板块独有的:Round-Robin 的 135 还是 140、IRV 先淘汰谁,全看这一行。 不写清楚,别人复现不出你的数字。


🔗 这一章连到哪里

去哪 为什么
附录 A · 速查 写代码时开着它。每个算法的手算步骤在那里被压成了可照做的清单,代码就是把那份清单翻译一遍
26 · 不可能定理 挑战 B 全建立在那一章上。⭐ 跑完操纵率表再回去读第五节那张「取舍地图」,感觉完全不同
17 · 稳定匹配 项目三 T5 那个 0 / 864 是 Roth 1982 的实验版;回去确认它为什么是不可能而不是没设计好
../强化学习基础/15-实战与挑战项目.html 对照着看:那边第一铁律是「跑多个种子」,这边根本没有种子,唯一的不确定性来自打破平局
../模型上线之后/10-SHAP能做什么不能做什么.html ⭐⭐ 项目五 T7 的落点:你写的 $n!$ 排列枚举和线上那个 shap 调用算的是同一个东西
../AI基础设施/20-推理服务化.html 挑战 A 的工业版:每个请求都想去最空的机器,结果大家一起涌过去

✅ 检查点

  1. 这个板块动手的门槛和别的板块比差在哪?三条规矩分别是什么?
  2. 为什么说「这边没有种子,但有一件事同样会改结果」?举一个带数字的例子。
  3. 同一份 14 人选票在七条规则下的赢家分别是谁?哪个数字最适合做自查?
  4. 圈法里最容易漏掉均衡的是哪一步?2×2 混合 NE 里唯一的错法是什么?
  5. 项目三 T5 的两个数字是多少?它们分别对应哪条结论?
  6. 项目四 T5 的统计实验,EF1 那一列和 PO 那一列分别长什么样?说明了什么?
  7. 怎么用一行 assert 抓出算错的 Shapley 值?挑战 A 的 T3 要同时打印哪三个数?
  8. 挑战 B 里独裁规则的可操纵率是多少?单峰域上得到 0% 为什么不叫推翻 G–S 定理?
👀 答案
  1. 不需要 GPU、数据集或任何库 —— 规模全在纸笔量级。三条规矩:① 零依赖零数据② 先复现书上的手算例子(跑不出书上的数字就是写错了,这是别的板块给不了的调试条件);③ 用 Fraction 不要用 float(混合 NE 是 2/3、Shapley 是 4/6、PS 概率是 3/4)。
  2. 算法是确定性的,⚠️ 但并列怎么打破会改结果Round-Robin 第 2 轮 2 号在 $g_3$、$g_4$ 之间并列(都值 5),取 $g_4$ 得书上的 135 / 45、取 $g_3$ 得 140 / 45 —— 两个都是 EF1。所以报告模板里专门有「打破平局的规则」一节。
  3. Plurality a、2-approval c、3-approval b、Borda b、两轮决选 e、IRV d、Copeland/Condorcet c —— ⭐⭐ 五个候选人每一个都当选过。自查用 Borda 总分 = 140($14\times(4+3+2+1+0)$)。
  4. ⚠️ 并列最大值必须全圈 —— 漏圈并列值等于漏掉整个均衡。混合的唯一错法是拿自己的收益解自己的概率:⭐ 解 p 用列玩家的收益(每格第二个数),解 q 用行玩家的收益
  5. 学生端 0 个、学校端 864 个(占 46656 个 $n=3$ 实例的 1.9%)。0 对应「SPDA 对学生策略防伪」;864 对应 Roth 1982没有任何稳定匹配机制能对双方同时策略防伪」。
  6. EF1 那一列两个算法都是 100%PO 那一列裂开了 —— Round-Robin 只有约 81%、MNW 是 100%。⭐ 说明「MNW ⟹ EF1 且 PO」买到的是第二个性质,Round-Robin 用 $O(mn)$ 只买到第一个。
  7. 每个排列的边际贡献加起来必须等于 $v(N)$(望远镜求和),写成 assert abs(row - full) < 1e-9。挑战 A 的 T3 打印那个人省下的成本 $\Delta_{\text{cost}}$、$\Phi$ 的降幅 $\Delta_\Phi$、当前 $\Phi$ —— ⭐⭐ 验证前两者逐步一分不差地相等,这正是第 15 章那三行论证。
  8. 0 / 216 = 0.0%(其余是 Borda 23.6%、Plurality 16.7%、IRV 与 Copeland 各 11.1%)—— ⭐⭐ G–S 说「满射 + 策略防伪 ⟹ 独裁」,独裁是唯一出口,你在自己的表里看到它是唯一一个 0。单峰域上是换了一个更小的输入域(6 种排序里只有 4 种单峰),而 G–S 要求规则在所有轮廓上有定义;⚠️ 而且救的是特定规则 —— 同一个域上 Plurality 反而涨到 18.8%

🛑 完成了?

附录 A · 速查把定义、手算套路卡和陷阱清单过一遍,然后回总目录


走神救援

⭐⭐ 这个板块动手的门槛低到不像话:不需要 GPU、数据集或 pip install,因为玩家 3 个、物品 5 件、选民 14 人 —— 规模全在纸笔量级。三条规矩:① 零依赖零数据 ② 先复现书上的手算例子(跑不出书上的数字就是写错了,这是别的板块给不了的调试条件③ 用 Fraction 不要用 float。⚠️ 和 RL 那边对照:那边第一铁律是「跑多个种子」,这边根本没有种子,唯一的不确定性来自 ⭐ 并列怎么打破 —— Round-Robin 的并列取 $g_4$ 得 135/45、取 $g_3$ 得 140/45(两个都是 EF1),所以报告模板里专门有「打破平局的规则」一节。七个项目项目一 投票规则对比器跑出「同一份 14 人选票、七条规则、五个赢家」(Plurality a、2-approval c、3-approval b、Borda b、两轮决选 e、IRV d、Copeland c,自查 Borda 总分 140),⭐⭐ 而赢家 a 的 Copeland 出度是 0 —— 它输给了所有人项目二 纳什均衡求解器:圈法(⚠️ 并列最大值必须全圈)、IESDS(第 10 章那个 3×3 四步剩 (M,C)=(3,3))、2×2 混合(⭐ 解 p 用列玩家的收益、解 q 用行玩家的收益)、鞍点(maximin 2 = minimax 2);T6 对比出零和里 maximin 组合就是 NE,性别之争里 maximin 是 1/3 而混合 NE 是 2/3项目三 稳定匹配:同一个 DA 函数换个方向就得到 $\mu_S$ 和 $\mu_C$;⭐⭐ T5 是灵魂 —— 46656 个 $n=3$ 实例里学生端成功谎报 0 个、学校端 864 个(1.9%),你亲手把 Roth 1982 搜了出来项目四 公平分配实验室:Round-Robin、嫉妒图(破环后两人从 5、5 变 10、40,两条边同时消失)、MNW(乘积 40 且完全 EF);⭐⭐ T5 统计 500 个随机实例:EF1 那一列两个都 100%,PO 那一列裂开 —— RR 约 81%、MNW 100%项目五 Shapley:煎饼博弈 (6,4,6)、手套博弈 (4/6,1/6,1/6),⭐ 行和 assert 是最好的检错手段;手套博弈的 Core 只有 (1,0,0)Shapley 落在 Core 外面 —— 公平和稳定在这里分家。⭐ 挑战 A 拥塞博弈:核心是 T3 —— 每步打印那个人省下的成本、$\Phi$ 的降幅和当前 $\Phi$,验证前两者一分不差地相等;⚠️ 开局 Φ=550005 而总成本=600000Φ 不是总成本),149 步收敛到 (0,5000,5000);Braess 65 → 80PoA ≈ 1.231;⭐⭐ T6 改成加权拥塞博弈,最佳响应动态当场绕圈,存在性保证塌掉。⭐⭐ 挑战 B 操纵率实验室把 G–S 变成一张表:216 个轮廓上 Borda 23.6%、Plurality 16.7%、IRV 与 Copeland 各 11.1%,独裁恰好 0% —— 那个唯一出口,你在自己的表里看到它是唯一一个 0;换成单峰域后中位选民规则 0/64,⚠️ 但这叫换了更小的输入域,救的是特定规则不是所有规则(同一个域上 Plurality 反而涨到 18.8%)。

下一节 👉 附录A-速查.md

打卡记录保存在你的浏览器里,首页能看到总进度