Correct the exploiter budget: it was a quarter of the target, not half

The exploiters ran 32.5M learner actions, not the 65.5M I recorded -- they train
one seat, so a 250x1024 run yields half of what the same shape gives the
both-seat self-play trainer. Against targets trained on 131M and 122.6M, that
makes the attacker roughly 4x underfunded.

Which means the absolute number does not support "ours is only 22.8%
exploitable". It supports exactly one claim: at a matched budget, league gives up
more. Whether the ordering survives a properly funded attacker is now the open
question, so the script takes --updates and --batch-games to run it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBQKgvBbxbheiTF1AVy1Sh
This commit is contained in:
2026-07-15 05:50:58 +09:00
co-authored by Claude Opus 4.8
parent e51d14d0fb
commit 0c83824243
2 changed files with 17 additions and 7 deletions
+5 -2
View File
@@ -551,7 +551,7 @@ Fable은 이것을 "가장 큰 누락 아이디어"로 꼽았다. 실측은 **
**측정 방법:** 정책을 얼려놓고, **오직 그놈만 이기도록 특화된 새 정책을 처음부터 학습**시킨다 **측정 방법:** 정책을 얼려놓고, **오직 그놈만 이기도록 특화된 새 정책을 처음부터 학습**시킨다
(`src/lost_cities_jax/exploit.py`). 착취자가 도달한 승률이 곧 그 정책이 못 막아낸 습관의 크기다. (`src/lost_cities_jax/exploit.py`). 착취자가 도달한 승률이 곧 그 정책이 못 막아낸 습관의 크기다.
**동일 착취자 예산 (250 업데이트 × batch 1024 = 65.5M learner 액션):** **동일 착취자 예산 (250 업데이트 × batch 1024 = 32.5M learner 액션):**
| 얼려놓은 정책 | 착취자 승률 | 95% CI | | 얼려놓은 정책 | 착취자 승률 | 95% CI |
|---|---|---| |---|---|---|
@@ -571,7 +571,10 @@ Fable은 이것을 "가장 큰 누락 아이디어"로 꼽았다. 실측은 **
1. **하한선이다.** 더 세거나 더 오래 학습한 착취자는 더 찾아낼 수 있다. **절대값이 아니라 1. **하한선이다.** 더 세거나 더 오래 학습한 착취자는 더 찾아낼 수 있다. **절대값이 아니라
동일 예산에서의 비교로만 의미가 있다.** 동일 예산에서의 비교로만 의미가 있다.**
2. 착취자 예산이 목표의 절반이다(65M vs 131M). 키우면 두 수치 다 오른다. 2. **착취자 예산이 목표의 약 1/4에 불과하다 (32.5M vs 131M).** 약한 공격자다.
따라서 **"우리는 22.8%밖에 안 털린다"고 말할 수 없다** — 그건 이 약한 공격자 기준일 뿐이다.
살아남는 주장은 **"동일 예산에서 league가 더 털린다"** 하나뿐이다.
착취자를 목표와 같은 예산(131M)으로 키워서 **순서가 유지되는지** 확인해야 한다.
3. league는 단판 정책이라 3라운드 게임에선 다소 제 물이 아니다 — 다만 carry가 무의미하다는 3. league는 단판 정책이라 3라운드 게임에선 다소 제 물이 아니다 — 다만 carry가 무의미하다는
것이 이미 증명됐으므로 큰 불리함은 아니다. 것이 이미 증명됐으므로 큰 불리함은 아니다.
+12 -5
View File
@@ -8,6 +8,7 @@ frozen policy could not defend.
from __future__ import annotations from __future__ import annotations
import argparse
import json import json
from pathlib import Path from pathlib import Path
@@ -77,6 +78,12 @@ def _final_score(cfg, exploiter_params, frozen, matches: int) -> dict:
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--updates", type=int, default=250, help="exploiter training updates")
parser.add_argument("--batch-games", type=int, default=1024)
parser.add_argument("--tag", default="", help="suffix for the run dir, to keep runs apart")
args = parser.parse_args()
match_cfg = load_config("configs/jax_ppo/match-selfplay.yaml") match_cfg = load_config("configs/jax_ppo/match-selfplay.yaml")
old_cfg = load_config("configs/jax_ppo/balanced.yaml") old_cfg = load_config("configs/jax_ppo/balanced.yaml")
@@ -94,10 +101,10 @@ def main() -> None:
for name, frozen in targets.items(): for name, frozen in targets.items():
print(f"\n===== training an exploiter against: {name} =====", flush=True) print(f"\n===== training an exploiter against: {name} =====", flush=True)
cfg = load_config("configs/jax_ppo/match-selfplay.yaml") cfg = load_config("configs/jax_ppo/match-selfplay.yaml")
cfg.ppo.batch_games = 1024 cfg.ppo.batch_games = args.batch_games
cfg.run.total_updates = 250 cfg.run.total_updates = args.updates
cfg.run.log_every = 25 cfg.run.log_every = max(1, args.updates // 10)
slug = name.split()[0] slug = name.split()[0] + args.tag
state = train_exploiter(cfg, frozen, Path(f"runs/jax-ppo-match/exploit-{slug}")) state = train_exploiter(cfg, frozen, Path(f"runs/jax-ppo-match/exploit-{slug}"))
result = _final_score(cfg, state.params, frozen, MATCHES) result = _final_score(cfg, state.params, frozen, MATCHES)
rows.append({"target": name, **result}) rows.append({"target": name, **result})
@@ -110,7 +117,7 @@ def main() -> None:
ci = f"[{row['wilson_low']:.3f}, {row['wilson_high']:.3f}]" ci = f"[{row['wilson_low']:.3f}, {row['wilson_high']:.3f}]"
print(f"{row['target']:<28}{row['exploiter_win_rate']:>20.4f}{ci:>22}") print(f"{row['target']:<28}{row['exploiter_win_rate']:>20.4f}{ci:>22}")
print("\nhigher = the frozen policy had more to farm. 0.5 = nothing found.") print("\nhigher = the frozen policy had more to farm. 0.5 = nothing found.")
Path("runs/jax-ppo-match/exploitability.json").write_text(json.dumps(rows, indent=2)) Path(f"runs/jax-ppo-match/exploitability{args.tag}.json").write_text(json.dumps(rows, indent=2))
if __name__ == "__main__": if __name__ == "__main__":