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:
@@ -551,7 +551,7 @@ Fable은 이것을 "가장 큰 누락 아이디어"로 꼽았다. 실측은 **
|
||||
**측정 방법:** 정책을 얼려놓고, **오직 그놈만 이기도록 특화된 새 정책을 처음부터 학습**시킨다
|
||||
(`src/lost_cities_jax/exploit.py`). 착취자가 도달한 승률이 곧 그 정책이 못 막아낸 습관의 크기다.
|
||||
|
||||
**동일 착취자 예산 (250 업데이트 × batch 1024 = 65.5M learner 액션):**
|
||||
**동일 착취자 예산 (250 업데이트 × batch 1024 = 32.5M learner 액션):**
|
||||
|
||||
| 얼려놓은 정책 | 착취자 승률 | 95% CI |
|
||||
|---|---|---|
|
||||
@@ -571,7 +571,10 @@ Fable은 이것을 "가장 큰 누락 아이디어"로 꼽았다. 실측은 **
|
||||
|
||||
1. **하한선이다.** 더 세거나 더 오래 학습한 착취자는 더 찾아낼 수 있다. **절대값이 아니라
|
||||
동일 예산에서의 비교로만 의미가 있다.**
|
||||
2. 착취자 예산이 목표의 절반이다(65M vs 131M). 키우면 두 수치 다 오른다.
|
||||
2. **착취자 예산이 목표의 약 1/4에 불과하다 (32.5M vs 131M).** 약한 공격자다.
|
||||
따라서 **"우리는 22.8%밖에 안 털린다"고 말할 수 없다** — 그건 이 약한 공격자 기준일 뿐이다.
|
||||
살아남는 주장은 **"동일 예산에서 league가 더 털린다"** 하나뿐이다.
|
||||
착취자를 목표와 같은 예산(131M)으로 키워서 **순서가 유지되는지** 확인해야 한다.
|
||||
3. league는 단판 정책이라 3라운드 게임에선 다소 제 물이 아니다 — 다만 carry가 무의미하다는
|
||||
것이 이미 증명됐으므로 큰 불리함은 아니다.
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ frozen policy could not defend.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
@@ -77,6 +78,12 @@ def _final_score(cfg, exploiter_params, frozen, matches: int) -> dict:
|
||||
|
||||
|
||||
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")
|
||||
old_cfg = load_config("configs/jax_ppo/balanced.yaml")
|
||||
|
||||
@@ -94,10 +101,10 @@ def main() -> None:
|
||||
for name, frozen in targets.items():
|
||||
print(f"\n===== training an exploiter against: {name} =====", flush=True)
|
||||
cfg = load_config("configs/jax_ppo/match-selfplay.yaml")
|
||||
cfg.ppo.batch_games = 1024
|
||||
cfg.run.total_updates = 250
|
||||
cfg.run.log_every = 25
|
||||
slug = name.split()[0]
|
||||
cfg.ppo.batch_games = args.batch_games
|
||||
cfg.run.total_updates = args.updates
|
||||
cfg.run.log_every = max(1, args.updates // 10)
|
||||
slug = name.split()[0] + args.tag
|
||||
state = train_exploiter(cfg, frozen, Path(f"runs/jax-ppo-match/exploit-{slug}"))
|
||||
result = _final_score(cfg, state.params, frozen, MATCHES)
|
||||
rows.append({"target": name, **result})
|
||||
@@ -110,7 +117,7 @@ def main() -> None:
|
||||
ci = f"[{row['wilson_low']:.3f}, {row['wilson_high']:.3f}]"
|
||||
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.")
|
||||
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__":
|
||||
|
||||
Reference in New Issue
Block a user