The earlier exploiters ran a quarter of their targets' budget. Rerun at a matched ~131M learner actions (measured: 130.4M and 131.1M): ours exploiter reaches 0.4657 [0.455, 0.477], mean lead -5.5 league exploiter reaches 0.6295 [0.619, 0.640], mean lead +28.8 The CIs are nowhere near each other and the gap widened under funding, 0.094 -> 0.164. A qualitative line gets crossed too: a properly funded attacker trained from scratch *beats* league outright, and still loses to ours. Two things a reader has to carry with that, both of which cut against us: - Neither exploiter plateaued -- both climb through the final update, so these remain lower bounds. And ours' attacker has the steeper tail (+0.031 vs +0.016 per 100 updates) while league's curve is already bending, so a much larger attacker could narrow the gap. Cannot rule out that it closes. - League is a single-round policy blind to carry and match score, and both frozen policies are played greedily, which is maximally exploitable by construction. So this is not a clean "training method A vs B". The claim that survives is the matched-budget one, and only that one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBQKgvBbxbheiTF1AVy1Sh
618 lines
39 KiB
Markdown
618 lines
39 KiB
Markdown
# Plan: 클래식 3라운드 로스트시티 에이전트
|
||
|
||
**Status:** Ready to execute (Fable 2차 검토 반영 완료)
|
||
**Priority:** High — 현재 스택은 **단판(1라운드)** 게임을 학습하는데, 원작 로스트시티는
|
||
**3라운드 합산**이 승패를 가른다. 즉 지금 에이전트는 애초에 다른 게임을 배우고 있다.
|
||
**Background:** Fable 설계 검토 2회 (2026-07-15), 인간 대 AI 기보
|
||
(`data/human-play/game-records.jsonl`, 계속 증가 중)
|
||
|
||
## 목표
|
||
|
||
**원작 규칙 그대로의 로스트시티에서 가장 강한 에이전트.** 레거시 코드 호환은 고려하지 않는다.
|
||
env/obs/보상/학습 루프 재작성 모두 허용.
|
||
|
||
## 확정된 규칙 (룰북 원문 검증 완료)
|
||
|
||
코스모스 공식 영문 룰북(691820-02) 및 Board Game Arena 구현체로 교차 확인:
|
||
|
||
- 3라운드를 두고 **누적 총점**이 높은 쪽이 매치 승리.
|
||
- 1라운드 선공: "가장 나이 많은 사람" → 게임 내적으로는 **임의**.
|
||
- **2·3라운드 선공: 누적 점수가 더 많은 쪽** ("The player who has more points begins.")
|
||
— 번갈아 가는 것이 아니다.
|
||
- **정확한 동점일 때의 선공은 룰북에 없다.** 공식 룰북·BGA 모두 침묵.
|
||
→ **무작위(동전 던지기)로 정한다.** 결정론적 규칙은 대칭 제로섬 셀프플레이에
|
||
자리(seat) 비대칭을 주입해 착취 가능한 구멍이 된다.
|
||
|
||
점수 계산(`(랭크합 − 20) × (1 + 악수) + (8장 이상 +20)`)과 "방금 버린 카드는 즉시
|
||
회수 불가"(`just_discarded`)는 **현재 엔진이 이미 정확하다**
|
||
(`engine.py:244`, `engine.py:124`).
|
||
|
||
### 선공이 중요한 이유: 덱 시계
|
||
|
||
라운드는 **덱의 마지막 카드를 뽑는 순간** 끝난다. 카드는 놓은 뒤에 뽑으므로 마지막
|
||
카드를 뽑은 쪽은 그 카드를 쓰지 못한다. 총 턴 수가 홀수면 **선공이 한 장 더 놓는다**.
|
||
총 턴 수 = 44 + (버림패 드로우 횟수)이므로 **플레이어가 홀짝을 조작할 수 있다**
|
||
(실측 게임당 버림패 드로우 ≈6.6회). 선공권이 누적 점수에 달려 있으므로, 2라운드
|
||
마진에는 **점수를 넘어 3라운드 선공권이라는 추가 가치**가 붙는다.
|
||
|
||
### 측정된 사실: stalling은 정적 상대 착취이지 균형이 아니다 (Phase 1 사이징의 근거)
|
||
|
||
버림패 드로우는 덱을 줄이지 않으므로, **양쪽이 계속 버림패만 뒤지면 라운드가 끝나지
|
||
않는다.** 기존 학습 로그를 보면 이 착취가 실제로 학습된다:
|
||
|
||
| 학습 체제 | 라운드 길이 추이 | 400수 상한 도달 |
|
||
|---|---|---|
|
||
| 정적 휴리스틱 상대 (`balanced.yaml`) | 82 → **116수** (eval greedy **168수**) | 0.4~0.7% |
|
||
| **셀프플레이 (league)** | 71 → **53.6수로 수렴** | **0%** |
|
||
|
||
정적 약체 상대에게는 턴을 늘릴수록 원정 깊이·8장 보너스에서 강자가 이득이라 질질 끄는
|
||
것이 합리적이다. 그러나 **셀프플레이에서는 상대도 똑같이 끌 수 있어 상쇄되고, 라운드가
|
||
자연 길이(≈54수 = 44 덱 드로우 + ~10 버림패 드로우)로 수렴한다.**
|
||
|
||
**결론:** 우리의 목표 체제는 셀프플레이/league이므로 **3라운드 매치 ≈ 160수**로 잡으면
|
||
되고, 기존 `rollout_steps=400` 안에 여유롭게 들어간다. 단 두 가지를 유의한다:
|
||
|
||
1. **정적 상대로 3라운드를 학습시키면 매치가 350~500수까지 늘어날 수 있다** — 정적 상대
|
||
워밍업 단계를 쓴다면 스캔 길이를 따로 잡아야 한다.
|
||
2. **라운드 상한(~120수)은 그 자체가 착취 가능한 구멍이다.** 앞선 쪽이 상한까지 끌면
|
||
라운드가 유리한 상태로 얼어붙는다. `max_steps_rate`를 게이트 지표로 계속 감시할 것
|
||
(셀프플레이에서는 0이어야 정상).
|
||
|
||
## 현재 코드의 결함 (Fable 2회 교차 검증 — 8건 전부 사실 확인)
|
||
|
||
1. **env가 1라운드짜리** — `State`에 라운드/누적 점수 없음 (`types.py:46`),
|
||
`to_move=0` 하드코딩 (`engine.py:63`).
|
||
2. **롤아웃의 87%가 죽은 스텝** — 한 라운드는 실측 평균 50.6수(최대 56)인데
|
||
`rollout_steps = MAX_STEPS = 400` (`ppo.py:66`)이고, 스캔 **안에는 리셋이 없다**
|
||
(`reset_done_envs`는 업데이트 사이에서만, `ppo.py:275`).
|
||
3. **GAE 부트스트랩 초기값이 0** (`ppo.py:691`). 지금은 모든 에피소드가 스캔 안에서 끝나
|
||
무해하나, **in-scan 리셋을 켜는 순간 편향**이 된다.
|
||
4. **shaping 어닐링 회계 버그** — `update * batch_games * rollout_steps` (`ppo.py:222`)로
|
||
**죽은 스텝까지 세어서**, 5M 어닐링이 250 업데이트 중 2번째에 끝난다. league는 shaping을
|
||
아예 끈다 (`league.py:592`).
|
||
→ **"shaping은 불필요하다"는 과거 결론이 있다면 무효다. 켜진 적이 없다.**
|
||
5. **obs에 `to_move`가 없다.** 그런데 value loss는 `active` 마스크로 학습되어 (`ppo.py:624`)
|
||
**상대 차례 상태에서도** 가치를 맞추라고 요구한다. critic이 차례를 구분할 단서가
|
||
`step_count / 400`뿐 — MLP에게 부동소수점에서 홀짝을 뽑으라는 요구다.
|
||
6. **점수차 정규화가 ÷780** (`MAX_ABS_SCORE`, `obs.py:68`). ±50점 차가 ±0.06 → 사실상
|
||
안 보인다.
|
||
7. **셀프플레이에서 상대 자리 결정을 전부 버린다** (`stop_gradient`, `ppo.py:425`) —
|
||
의사결정의 절반을 낭비.
|
||
8. **평가가 단판 승률** (`gates.py`) — 3라운드 개편의 성패를 측정할 자가 없다.
|
||
|
||
## 핵심 설계 결정
|
||
|
||
### PRNG: 모든 무작위성을 `reset()`에서 미리 뽑는다 (필수)
|
||
|
||
라운드 전환 리셔플과 동점 코인플립을 `step()` 안에서 샘플링하면 `step`이 확률적이 되어
|
||
서명이 바뀌고, 모든 rollout/eval/gates 바디에 키를 꿰어야 하며 duplicate 미러링이 꼬인다.
|
||
|
||
**`reset()`에서 3라운드 덱 순서 전부(3×60), 동점 코인플립 비트 2개, 1라운드 선공 비트
|
||
1개를 미리 샘플링해 `State`에 저장한다.** `step()`은 결정론을 유지하고 라운드 전환은
|
||
다음 덱 슬라이스로 스위치만 한다. 덤: **미러 매치(같은 3딜 + 자리 교대 + 같은 코인플립
|
||
비트)가 공짜로 따라온다** — Phase 5의 antithetic 페어링이 그대로 성립.
|
||
|
||
### 라운드 전환 vs 매치 리셋의 분리
|
||
|
||
- **라운드 전환은 엔진 `step()` 내부**에서 (`done=False` 유지, carry 갱신, 덱 슬라이스 전환)
|
||
- **매치 done 리셋은 rollout 바디**에서 (in-scan auto-reset)
|
||
|
||
이러면 두 메커니즘이 깨끗이 분리된다. 단 위의 pre-sampled 덱 설계가 전제다.
|
||
|
||
- `State`에 **라운드 스텝 카운터와 매치 스텝 카운터를 둘 다** 둔다 (forced-done은 라운드
|
||
단위, obs 정규화는 라운드 상대 진행도 + round 원-핫).
|
||
- 중간 라운드 forced-done(라운드 상한 초과) 시 **자연 종료와 동일하게 라운드 종료 → 전환**.
|
||
|
||
### 보상 스케일에 대한 해소 (혼동 방지)
|
||
|
||
`sign()`에 가까운 작은 scale은 **틀린 선택**이다 — 150수에 걸쳐 모든 수가 동일한 ±1을
|
||
받아 크레딧 할당이 전부 critic에 떠넘겨지고, 5점 차 패배와 80점 차 패배의 그래디언트가
|
||
같아진다. **scale 30~50의 tanh가 절충점**이다: |마진| ≳ 60에서는 `sign`을 근사하면서
|
||
크레딧 그래디언트를 보존한다. 대신 5점 승과 80점 승을 여전히 구분하므로 순수 승률
|
||
최적화 대비 **마진 쪽으로 약간 왜곡**된다 — 이 잔여 왜곡은 Phase 5의 후기 fine-tune으로
|
||
scale을 낮춰서(예: 50 → 25) 제거한다.
|
||
|
||
명시적 risk 항은 **넣지 않는다.** carry + round를 obs에 넣은 종료 보상이 올바른 리스크
|
||
태도(뒤지면 도박, 앞서면 잠금)를 자동으로 유도한다.
|
||
|
||
## 체크리스트
|
||
|
||
### Phase 0a — 학습 루프 수정 (최우선, 단판 체제에서 검증 가능)
|
||
|
||
- [x] **GAE 부트스트랩 수정**: 잘린 에피소드를 `V(s_final)`로 부트스트랩 (`ppo.py:691`).
|
||
- [x] **in-scan auto-reset**: 롤아웃 스캔 안에서 done env 리셋 → 죽은 스텝 제거.
|
||
**실측(rollout_steps=400, batch_games=64, 3 업데이트):** main은 활성 17.3%
|
||
(learner 액션 2,221/업데이트), 수정 후 **활성 100% (12,842/업데이트) — 동일 연산량에
|
||
샘플 5.8배.** 활성 비율은 게임 길이에 비례하므로, 학습 초기 정책(~69수)에서 5.8배,
|
||
학습된 정책(~50수)이면 8배에 가까워진다. 3라운드 매치(~150수)가 되면 약 2.7배로
|
||
줄지만 그때는 스캔 전체가 실제 매치로 채워진다.
|
||
- [x] **메트릭 파이프라인 재작업 (필수, 놓치면 조용히 오염됨)**: `rollout_metrics`는
|
||
`final_env`에서 통계를 읽고 `episode_return = jnp.sum(transitions.reward, axis=0)`은
|
||
**env당 에피소드 1개를 가정**한다 (`ppo.py:1069`). in-scan 리셋 후에는 final_env가
|
||
에피소드 중간이고 여러 에피소드의 보상이 합산된다 → **metrics.jsonl 전체가 쓰레기가
|
||
된다.** done 경계에서 누적하는 에피소드 단위 집계로 바꿀 것.
|
||
- [x] **league assignments를 scan carry로**: `learner_seat`/`opponent_index`/`use_mirror`가
|
||
업데이트 사이에서만 재샘플링된다 (`ppo.py:568`). in-scan 리셋을 켜면 리셋 시점에
|
||
스캔 내부에서 재샘플링해야 한다 — 안 하면 자리/상대가 에피소드 간 고정되어 편향.
|
||
- [x] **shaping 어닐링 회계 수정**: 패딩된 스캔 스텝이 아니라 **learner 액션 수** 기준
|
||
(`ppo.py:222`). `shaping_coef`는 호스트에서 계산해 jit 함수에 넘기므로
|
||
(`ppo.py:221`), 직전 업데이트의 액션 수를 호스트에서 누적하는 **1-업데이트 지연**
|
||
구조가 된다 (무해).
|
||
- [x] **회귀 게이트 — 통과 (단, 게이트 설계를 고쳐야 했다).**
|
||
|
||
**(1) "동일 learner-액션 예산"은 잘못된 게이트였다.** PPO 진척도는 샘플 수와
|
||
**옵티마이저 스텝 수** 둘 다에 달려 있다. 샘플을 맞추면(64 vs 250 업데이트) 새 코드는
|
||
그래디언트 스텝이 1/4이 되어 덜 수렴한다. 업데이트당 연산량은 동일하므로
|
||
**올바른 게이트는 250 대 250**(같은 컴퓨트·같은 그래디언트 스텝, 샘플만 5.8배)이다.
|
||
|
||
**(2) 동일 컴퓨트 게이트 결과 (2만판 duplicate eval):**
|
||
|
||
| 앵커 | 지표 | 베이스라인 | Phase 0a |
|
||
|---|---|---|---|
|
||
| **expert** (강함) | 승률 | 0.4525 | **0.5071** |
|
||
| **expert** | 평균 점수차 | −4.28 | **−0.56** |
|
||
| balanced (약함) | 승률 | 0.9866 | 0.9292 |
|
||
| balanced | 평균 점수차 | 116.8 | **128.1** |
|
||
|
||
학습 곡선은 **모든 업데이트 지점에서** 새 코드가 위(최종 return 0.963 vs 0.935).
|
||
|
||
**(3) balanced 승률 하락은 전부 `MAX_STEPS=400` 아티팩트다.** 새 정책 2,048판 분해:
|
||
|
||
| | 판수 | 패배율 |
|
||
|---|---|---|
|
||
| 자연 종료 | 1,214 | **0.08%** |
|
||
| 400수 상한 도달 | 834 (40.7%) | **20.62%** |
|
||
|
||
**전체 패배의 99.4%가 상한에 부딪힌 판에서 발생.** 새 정책은 자연 종료 게임에서
|
||
**99.92% 승률**이다. 샘플이 5.8배라 stalling 착취를 더 깊이 배웠고(greedy 225수),
|
||
그 결과 40%가 인위적 벽에 박아 원정 미완성인 채 얼어붙는다.
|
||
|
||
**결론:** Phase 0a는 성공. expert(=stalling을 허용하지 않는 앵커)에서 45%→51%로
|
||
실제 실력이 올랐다.
|
||
|
||
### Phase 1 — 3라운드 env
|
||
|
||
**구현: `src/lost_cities_jax/match.py` (커밋 `7cd299d`).** 단판 엔진을 고치지 않고 **감쌌다**
|
||
— `engine.py`는 TS 클라이언트와의 차분 테스트가 지키는 규칙 오라클이고, 라운드 자체는
|
||
매치가 생겨도 변하지 않는다. `MatchState`가 내부에 단판 `State`를 든다.
|
||
|
||
- [x] `MatchState`: `round`(단판 State), `deck_orders`(3×60), `coin_flips`(3,),
|
||
`round_idx`, `carry`(2, 플레이어별 누적 점수), `done`.
|
||
별도 매치 스텝 카운터는 두지 않았다 — 매치는 3라운드가 끝나면 종료하므로 종료 판정에
|
||
불필요하고, 라운드 진행도는 내부 `State.step_count`(라운드마다 리셋)가 이미 준다.
|
||
- [x] `match_step()`: 덱 소진 시 라운드 < 3이면 carry 갱신 → 다음 덱 슬라이스 → `round_idx += 1`,
|
||
`done` 유지. 라운드 == 3이면 `done = True`. **보상은 3라운드 끝에서만.**
|
||
- [x] **선공 규칙**: `carry[0] > carry[1]` → p0, `<` → p1, `==` → pre-sampled 코인플립.
|
||
1라운드는 특수 케이스가 **필요 없다** — carry가 (0,0)이라 동점 분기가 자동으로 코인을
|
||
뽑고, 그게 룰북의 "가장 나이 많은 사람"(= 임의)과 정확히 같다.
|
||
- [x] **PRNG**: 딜 3벌 + 코인 3개를 `match_reset`에서 전부 미리 뽑아 상태에 저장.
|
||
`match_step`은 결정론적이라 키를 rollout/eval/gates에 꿸 필요가 없고, **미러 매치가
|
||
자리 라벨 교체만으로 성립**한다 (Phase 5 antithetic 페어링이 여기 의존).
|
||
- [x] ~~라운드당 스텝 상한을 400 → ~120으로 분리~~ — **철회한다. 라운드 상한은 400을 유지.**
|
||
|
||
**이 항목의 원래 명분이 사라졌다.** 근거는 "3라운드(~360수)가 400스텝 스캔에 들어가게"
|
||
였는데, **Phase 0a의 GAE 절단 부트스트랩이 스캔 길이 제약을 없앴다.** 잘린 매치는
|
||
올바르게 부트스트랩되므로 스캔 길이는 이제 자유 파라미터다.
|
||
|
||
**그리고 상한을 낮추면 아티팩트가 악화된다.** Phase 0a 실측: 400수 벽에 부딪힌 판의
|
||
20.6%가 패배로 뒤집혔고 **전체 패배의 99.4%가 거기서 발생**했다. 상한을 120으로 내리면
|
||
**더 많은 판이 벽에 박고**, "앞선 쪽이 상한까지 끌어 라운드를 얼린다"는 착취가 훨씬
|
||
쉬워진다. 셀프플레이에서는 라운드가 54수로 수렴해 120이든 400이든 안 걸리므로,
|
||
**덜 걸리는 쪽을 두고 `max_steps_rate`를 감시**하는 것이 맞다.
|
||
|
||
- [ ] **학습 파이프라인 배선** (원래 계획서에서 누락된 항목): `ppo.py`의 rollout/eval,
|
||
`gates.py`, `league.py`가 전부 단판 `State`를 받는다. `MatchState`를 받도록 배선한다.
|
||
- 매치용 obs 래퍼: 우선 `observation(state.round, player)`로 carry-blind하게 연결
|
||
(carry/round 피처는 Phase 3에서 추가).
|
||
- `match_legal_action_mask`, `match_step`, `match_score`로 교체.
|
||
- 스캔 길이는 자유 파라미터가 되었으므로 셀프플레이 매치 길이(~160수) 기준으로 잡되,
|
||
잘려도 부트스트랩이 처리한다.
|
||
- [ ] **league 풀 재구축**: 기존 스냅샷 멤버는 obs 변경으로 전부 무효화된다 — Phase 3 이후로
|
||
미룬다 (obs가 확정되기 전에 풀을 다시 채우면 두 번 일한다).
|
||
- [x] **테스트 12개** (`tests/lost_cities_jax/test_match.py`): 라운드당 덱 드로우 = 44 +
|
||
버림패 드로우만큼 연장, `carry`가 각 라운드를 정확히 한 번씩 적립, 선공 규칙 3분기,
|
||
1라운드 코인 공정성(≈50/50), 라운드 경계에서 누적 점수 불연속 없음(PBRS 전제),
|
||
미러 매치 대칭성, 보상은 3라운드 끝에서만 지급.
|
||
|
||
### Phase 0b — 측정 자 (첫 장기 3라운드 학습 **전에** 착지)
|
||
|
||
- [ ] **매치 단위 평가**: 3딜 전부 미러링 + 자리 교대 + **동일 코인플립 비트**,
|
||
매치 승률 + Wilson CI, 평균 총 마진. `gates.py`/`league.py`의 단판 기준을 대체.
|
||
**`gates.py:884`, `gates.py:961`의 `MAX_STEPS` 길이 단판 스캔 2곳 포팅 포함** —
|
||
exploiter 학습·평가 경로 전체가 3라운드로 가야 성공 기준 3을 잴 수 있다.
|
||
- [ ] **carry 조건부 프로브**: carry ∈ {−60, −25, −1, +1, +25, +60}을 주입한 3라운드 시작
|
||
위치에서 승률·행동 변화(원정 개수, 악수 비율, 덱 레이스 비율) 측정.
|
||
- [ ] **shuffle bank 포맷 확장**: 덱과 함께 코인플립·선공 비트도 뽑도록. 안 그러면
|
||
`jax.random.PRNGKey(0)` 고정 eval(`ppo.py:1025`)의 재현성이 매치 정의와 얽힌다.
|
||
- [ ] **legacy obs 버전 보존**: 구 정책을 3라운드 매치에 투입해 베이스라인을 재려면 구
|
||
obs(454차원)로 추론해야 하는데, `checkpoint_policy_from_params`는 전역 `observation`을
|
||
호출한다 (`ppo.py:802`) → obs 개편 후 구 체크포인트는 **로드 자체가 실패**한다.
|
||
policy 로더가 체크포인트별 obs 버전을 받도록 할 것. **성공 기준 2의 분모가 여기 달렸다.**
|
||
|
||
### Phase 2 — 보상
|
||
|
||
- [ ] 종료 보상: 3라운드 끝에서만 `tanh(총_점수차 / scale)`, **scale = 30~50**
|
||
(근거는 위 "보상 스케일에 대한 해소" 절).
|
||
- [ ] 마진 shaping 부활: `Φ(s) = carry + 현재 보드 점수차`, **작은 계수**(종료 보상 스케일의
|
||
0.05~0.2). 현재의 계수 1.0 원점수 shaping은 ±1 종료 보상보다 10~30배 크다.
|
||
learner 스텝 기준으로 어닐링하되 **후반까지 정확히 0으로 내리지 않는다.**
|
||
- [ ] γ=1.0에서 `Φ(s') − Φ(s)`는 **올바른 PBRS 형태다** (γ 누락 아님 — 검토에서 확인).
|
||
|
||
### Phase 3 — 관측
|
||
|
||
- [ ] `carry`: **÷75 스칼라 + 구간 원-핫**(약 9구간). 3라운드 정책은 "1점만 더" 임계값
|
||
근처에서 급격히 꺾여야 한다. 기존 `score_diff`의 ÷780 정규화도 같이 고친다.
|
||
- [ ] `round_idx` 원-핫 + 남은 라운드 수.
|
||
- [ ] **`to_move` 비트** + "내가 이번 라운드 선공인가" + "현재 홀짝에서 마지막 덱 카드를
|
||
누가 뽑는가"(덱 시계).
|
||
- [ ] 색깔별 **살아있는 점수 3분할**: 내 `col_top` 위로 아직 나올 수 있는 점수를
|
||
(a) **내 손패**, (b) **버림패 더미**(공개돼 있고 회수 가능 — 빠뜨리기 쉬움),
|
||
(c) **미공개**(덱 ∪ 상대 은닉 손패)로 나눠 넣는다. 상대에 대해서도 동일
|
||
(상대 `col_top`은 공개). MLP가 7×60 채널에서 뽑아내기 어려운 비선형 집계이고,
|
||
모든 개시/연장/차단 판단을 좌우한다.
|
||
|
||
### Phase 4 — 학습 효율
|
||
|
||
- [ ] **전지적 critic (CTDE)**: critic에만 상대 손패 + 덱 구성을 준다. 행동과 무관한
|
||
정보이므로 정책 그래디언트를 편향시키지 않는다. 딜 운 분산을 정면으로 깎는다.
|
||
**가치 경로를 정책 트렁크에서 분리해야 한다** (현재 공유 트렁크, `ppo.py:104`) —
|
||
특권 정보가 정책 로짓으로 새면 안 된다.
|
||
→ 이 critic은 나중에 **PIMC 탐색의 리프 평가기로 그대로 재활용**된다.
|
||
- [ ] **양쪽 자리 학습**: `stop_gradient`된 상대 자리 전이(`ppo.py:425`)도 학습에 쓴다
|
||
(샘플 효율 2배). 같은 게임의 두 자리는 반상관이므로 같은 배치에 두고 advantage
|
||
정규화에 맡긴다.
|
||
- [ ] `gae_lambda` 재검토: 0.95는 150수 지평에서 너무 짧다(중반 수 직접 가중치 0.02).
|
||
전지적 critic이 있으면 유지, 없으면 0.97~0.99. **λ=1은 금지**(딜 분산).
|
||
- [ ] `entropy_coef` **스윕으로 재결정**. (주의: "0.01이 원점수 shaping 기준으로 잡혔다"는
|
||
추론은 **틀렸다** — league는 shaping을 끄고 학습했으므로 0.01은 이미 ±1 tanh 체제에서
|
||
동작해온 값이다. 다만 3라운드에서 보상 빈도가 1/150로 희석되고 작은 shaping이
|
||
추가되므로 재튜닝 자체는 타당하다. **근거 없이 10배 낮추면 과소탐색으로 직행한다.**)
|
||
|
||
### Phase 5 (나중) — 최종 강함
|
||
|
||
- [ ] **후기 fine-tune**: 학습 말미에 종료 보상 scale을 낮춰(50 → 25) 마진 왜곡을 제거하고
|
||
순수 승률 쪽으로 당긴다.
|
||
- [ ] 페어드 antithetic 딜(같은 3딜 + 자리 교대 + 같은 코인플립 비트)을 **학습에** 도입.
|
||
단순 포함이 아니라 **쌍으로 묶어** control variate로 써야 효과가 있다.
|
||
(Phase 1의 pre-sampled PRNG 설계 덕에 사실상 공짜.)
|
||
- [ ] MMD식 정규화 셀프플레이 (loss에 ~20줄) — 2인 제로섬 근사 내시 보험.
|
||
- [ ] **추론 시점 탐색 (PIMC / ISMCTS)** — 최종 강함의 가장 큰 이득. 로스트시티는 블러핑
|
||
경제가 없는 저기만성 불완전정보 게임이라 결정화 탐색이 잘 맞는다. raw net 대
|
||
net+search 맞대결로 측정.
|
||
- [ ] 네트워크 용량 A/B (512×3 → 1024×3 또는 residual) — **파이프라인 변경이 끝난 뒤에.**
|
||
|
||
## 범위 밖 (명시적 동결)
|
||
|
||
- **웹 클라이언트/ONNX는 별도 계획 전까지 레거시 단판 모델로 동결한다.** obs 개편 즉시
|
||
export 파이프라인(`scripts/export_jax_ppo_onnx.py:70`, manifest `observation_size: 454`),
|
||
TS obs 빌더, TS 단판 엔진이 전부 비호환이 된다. 이 선언이 없으면 실행 중 스코프가
|
||
웹 재작성으로 샌다.
|
||
- **Deep CFR 복귀** — 이미 BC 천장을 쳤고, 3라운드는 트리만 키운다. PPO+league를 학습
|
||
백본으로 유지한다.
|
||
- 레거시 호환을 위한 타협.
|
||
|
||
## 성공 기준
|
||
|
||
0. **(Phase 0a 게이트)** 루프 수정 후 단판 체제에서 동일 learner-액션 예산으로 기존 anchor
|
||
성적 재현 ≥ 동등. **없으면 auto-reset 버그가 3라운드 결과에 섞여 원인 분리가 불가능해진다.**
|
||
1. **carry 프로브에서 행동이 단조롭게 변한다** — carry 6개 수준에 걸쳐 원정 개수/악수 비율의
|
||
**단조 추세**(CI 포함). ("행동이 변한다"는 노이즈로도 통과 가능하므로 단조성으로 정의.)
|
||
2. **매치 승률이 구 정책(3라운드에 그대로 투입)을 이긴다** — 페어드 매치 **≥ 1만 쌍**,
|
||
매치 승률 **Wilson 하한 > 0.5** *및* 평균 총 마진 **CI 하한 > 0**.
|
||
3. 착취자(exploiter) 승률이 악화되지 않는다.
|
||
4. **(anchor 비회귀)** 휴리스틱 anchor 상대 매치 승률·라운드당 마진이 구 정책 대비 악화되지
|
||
않는다. **기준 2의 구멍을 막는 항목**: carry-blind인 구 정책만 상대로 이기는 것은
|
||
**카드 플레이가 퇴보해도 carry 착취만으로 달성 가능**하다.
|
||
|
||
**단, anchor 선택에 주의:** Phase 0a에서 실증됐듯 **약한 anchor(`heuristic_balanced`)는
|
||
무한 stalling을 허용해서 신호를 오염시킨다** — 승률이 실력이 아니라 상한 도달률을 잰다.
|
||
**`heuristic_expert`처럼 stalling을 허용하지 않는 anchor**(게임이 자연 길이로 끝남)만
|
||
실력 지표로 쓰고, 약한 anchor는 **자연 종료 게임만 필터링해서** 보거나 `max_steps_rate`를
|
||
함께 보고할 것.
|
||
|
||
---
|
||
|
||
## 실행 결과 (2026-07-15, 커밋 `9ba5a07`)
|
||
|
||
### 동작하는 것
|
||
|
||
- **셀프플레이가 stalling을 스스로 제거한다.** 매치가 146.8수(라운드 ≈49수)로 수렴하고
|
||
덱 레이스 비율 91%. Phase 0a에서 정적 상대가 225수까지 끌던 것과 대조된다.
|
||
- **미러 매치 평가가 정확하다.** 셀프플레이 duplicate 승률 0.4968, **평균 lead 정확히 0.0** —
|
||
같은 3딜 + 같은 코인 + 자리 교대가 딜 운을 완전히 상쇄한다. 미완료 0%.
|
||
- **전지적 critic이 격리돼 있다.** 특권 입력을 흔들어도 정책 로짓은 비트 단위로 동일하고,
|
||
가치만 움직인다 (테스트로 고정).
|
||
|
||
### 성공 기준 1 — **약하게만 충족. 사실상 미달.**
|
||
|
||
carry 프로브(3라운드 시작 시 carry 주입)에서:
|
||
|
||
| | scale 50 | scale 25 | scale 12 |
|
||
|---|---|---|---|
|
||
| 악수 스프레드 (carry −60 → +60) | 0.19 | 0.08 | **0.31** |
|
||
| 원정 개수 | 5.00 고정 | 4.99 고정 | **5.00 고정** |
|
||
|
||
- **악수 사용은 6개 carry 수준 전체에서 단조**로 움직인다 (뒤지면 배수 베팅 ↑). 방향은 맞다.
|
||
- 그러나 **원정 개수는 carry와 무관하게 5.00에 붙어 있다.** 굳은 습관이지 조건부 플레이가 아니다.
|
||
- 효과 크기가 작다.
|
||
|
||
### 확정된 진단
|
||
|
||
1. **shaping 가설은 틀렸다.** `potential_shaping_final=0.0`으로 완전히 꺼도 프로브는 평평했다.
|
||
2. **원인은 `terminal_scale`이다.** carry = −60에서 `tanh((margin − 60)/50)`의 인자는 현실적
|
||
마진 범위에서 **거의 선형**이고, 선형 구간에서 `E[tanh]` 최대화는 `E[margin]` 최대화와 같다
|
||
→ 도박할 이유가 없다. 위험 추구는 tanh가 **강하게 볼록한** 구간에서만 나오며 scale을 낮춰야
|
||
그 구간에 들어간다. scale 50 → 12에서 스프레드가 0.19 → 0.31로 커진 것이 이를 확인한다.
|
||
3. **프로브 설계 결함:** scale 12에서 `tanh(60/12) ≈ 1.0`이라 **±60은 포화 = 그래디언트 0**.
|
||
거기서 정책은 학습된 바가 없다. **의미 있는 측정 구간은 `|carry| ≲ 2 × terminal_scale`.**
|
||
프로브 수준을 scale에 맞춰 재설계해야 한다.
|
||
|
||
### 다음에 할 일
|
||
|
||
- [ ] **프로브 재설계**: carry 수준을 `terminal_scale`에 맞춰 잡는다 (포화 구간 측정 금지).
|
||
- [ ] **학습 중 carry 분포 확인**: 셀프플레이에서 3라운드 진입 시 carry가 실제로 얼마나
|
||
퍼지는가. 좁으면 정책이 큰 deficit을 본 적이 없다는 뜻이고, 그게 진짜 원인일 수 있다.
|
||
- [ ] **"항상 5색 개시"가 정상인지 검증**: 인간 기보에서 AI는 4.81, 인간은 4.19를 열었다.
|
||
5.00 고정은 의심스럽다. 셀프플레이 균형인지, 탐색 붕괴인지 (엔트로피 3.5 → 1.03).
|
||
- [ ] scale 스케줄(50 → 12 후기 fine-tune)이 처음부터 12로 학습하는 것보다 나은지 A/B.
|
||
|
||
---
|
||
|
||
## 판정: 보상은 **선형 총점**이다 (2026-07-15, Fable 3차 검토 + 실측)
|
||
|
||
사용자 제안("그냥 3판 총점이 크기만 하면 되는 것 아니냐")이 **맞았다. tanh 종료 보상은
|
||
오버엔지니어링이었다.**
|
||
|
||
### 근거 1 — 분해 논증 (실측)
|
||
|
||
보상이 총점에 선형이면 라운드가 독립이므로 3라운드 게임이 3개의 독립 단판으로 분해된다.
|
||
셀프플레이 2,048매치 실측: `corr(m1, m2) = 0.004`, `corr(m1+m2, m3) = 0.05`.
|
||
라운드를 잇는 유일한 고리인 **선공 어드밴티지는 +0.73 ± 0.84점** — 0과 구분 불가.
|
||
|
||
### 근거 2 — 리스크 태도는 값어치가 없다 (실측)
|
||
|
||
Fable이 리스크 태도를 직접 구현해 greedy 클론과 duplicate 6,144판 맞대결:
|
||
|
||
| 도박 정책 | 매치 승률 |
|
||
|---|---|
|
||
| 3R에서 20점 이상 뒤지면 온도 샘플링 | **0.482** (진다) |
|
||
| 40점 이상 뒤질 때만 | 0.498 (본전) |
|
||
|
||
**일부러 도박을 시켜도 지거나 본전.** 로스트시티의 분산 레버(한계 악수 ≈ Δσ +1.7에 마진
|
||
−2~3점)가 근본적으로 약해서, 분산을 사는 비용이 볼록성 이득을 먹는다.
|
||
carry 조건부 플레이의 가치 상한: **1승점 미만.**
|
||
|
||
### 근거 3 — 맞대결에서 단순한 쪽이 **이겼다**
|
||
|
||
duplicate 10,000판 (같은 3딜 + 같은 코인 + 자리 교대), 동일 컴퓨트 300 업데이트:
|
||
|
||
| A | B | A 승률 | A 평균 총점차 |
|
||
|---|---|---|---|
|
||
| **선형 총점** | tanh(총점/12) | **0.5859** (CI 0.576–0.596) | **+20.3점** |
|
||
|
||
버려도 손해가 없는 게 아니라 **버리니 더 강해졌다.** 이유는 리스크가 아니라 **신호 밀도**다:
|
||
tanh는 ~150수 매치에 포화된 ±1 하나를 주고, 선형은 매 수마다 그 수가 총점차를 움직인 만큼을
|
||
준다. γ=1에서 후자의 합이 정확히 최종 총점차로 telescoping되므로 **목적함수는 동일한데 크레딧
|
||
할당만 150배 조밀**하다.
|
||
|
||
### 근거 4 — 탐색 붕괴가 아니었다
|
||
|
||
프로브가 평평했던 이유는 탐색 붕괴가 아니다. 샘플링 프로브에서도 원정 5.00 ± 0.05,
|
||
엔트로피 1.36나트(유효 행동 3.9개)로 정상 수렴. 그리고 **critic은 carry를 완벽히 읽고 있었다**
|
||
(3R 시작 가치 −0.87 → +0.86 단조). 신호는 있었고, **정책이 그걸로 살 수 있는 물건이
|
||
없었을 뿐**이다.
|
||
|
||
### 최종 설계
|
||
|
||
```python
|
||
# match_ppo.py 롤아웃 바디
|
||
reward0 = jnp.where(active, (after - before) / cfg.reward.terminal_scale, 0.0)
|
||
```
|
||
|
||
- **성공 기준 1(carry 프로브 단조성)은 게이트에서 제거한다.** 최적 반응 자체가 이 게임에서
|
||
거의 존재하지 않는다는 것이 측정 결과다. 기준 2·4(맞대결 + anchor 비회귀)가 옳은 자다.
|
||
- `carry`는 obs에 **남긴다** (비용 0, 선공 규칙이 키로 쓰는 상태, critic이 잘 읽음).
|
||
|
||
### 목적함수와 무관하게 살아남은 것 (전부 순이득)
|
||
|
||
in-scan auto-reset + GAE 절단 부트스트랩(샘플 5.8배), 메트릭 재작업, **비대칭 CTDE critic**,
|
||
**양쪽 좌석 학습**, **duplicate 매치 평가**, pre-sampled PRNG/미러 설계, 그리고 match_obs의
|
||
단판 결함 수정분(`to_move`, 덱 시계, ÷780 → ÷75, 살아있는 점수 3분할).
|
||
|
||
**죽은 것은 tanh 종료 보상과 carry 구간 원-핫뿐이다.**
|
||
|
||
---
|
||
|
||
## 신구 대결: 진짜 3라운드 클래식에서 (2026-07-15)
|
||
|
||
**질문: "기존 학습 방법 대비 실제로 뭐가 나아졌나?"**
|
||
|
||
duplicate 매치 8,192판(같은 3딜 + 같은 코인 + 자리 교대), 진짜 3라운드 게임:
|
||
|
||
| 신(매치 스택) | 상대 | 신 승률 | 신 평균 총점차 | 상대 learner 액션 |
|
||
|---|---|---|---|---|
|
||
| 39.3M | 기존 baseline (정적 상대 학습) | 0.6077 | +19.5 | 104M |
|
||
| 39.3M | Phase 0a 게이트 (루프 수정만) | **0.5842** | +16.0 | **411M (10.5배)** |
|
||
| 39.3M | league (셀프플레이, 웹 배포판) | 0.3142 | −37.0 | 122.6M (3.1배) |
|
||
| **131M** | **league (셀프플레이, 웹 배포판)** | **0.6094** (CI 0.599–0.620) | **+22.0** | 122.6M |
|
||
|
||
**결론:**
|
||
|
||
1. **샘플 효율이 크게 올랐다.** 39.3M짜리가 **411M(10.5배)짜리를 이긴다.**
|
||
2. **동일 예산에서 기존 최강(league)을 이긴다** — 131M vs 122.6M에서 0.6094.
|
||
3. 39.3M에서 league에 졌던 것(0.3142)은 **약해서가 아니라 예산이 1/3이어서**였다.
|
||
|
||
**남은 단서:** league는 **착취자(exploiter) 구조**를 포함한 런이다. 평균 강함은 우리가 이기지만
|
||
**exploitability는 아직 재지 않았다.** "덜 착취당한다"는 증명되지 않았다.
|
||
|
||
---
|
||
|
||
## 기여도 A/B (2026-07-15)
|
||
|
||
각 조각을 하나씩 끄고 동일 컴퓨트(300 업데이트, batch 512)로 학습시킨 뒤,
|
||
full 스택과 duplicate 매치 8,192판 맞대결. **0.5 미만 = 그 조각이 기여하고 있었다.**
|
||
|
||
| 제거한 것 | full 대비 승률 | 95% CI | 평균 총점차 | 판정 |
|
||
|---|---|---|---|---|
|
||
| **양쪽 좌석 학습** | **0.3317** | [0.322, 0.342] | −28.6 | **압도적 기여** |
|
||
| 매치 관측 (carry/to_move/덱시계/살아있는점수) | 0.4751 | [0.464, 0.486] | −4.0 | 유의하게 기여 |
|
||
| **전지적 critic (CTDE)** | **0.5160** | [0.505, 0.527] | +2.6 | **오히려 해가 된다** |
|
||
|
||
### 1. 양쪽 좌석 학습 — 가장 큰 기여
|
||
|
||
끄면 승률이 0.33으로 무너진다. 같은 업데이트 수에서 learner 액션이 **정확히 절반**이 되므로
|
||
상당 부분은 샘플 수 효과다 — 그러나 **그게 요점이다.** 셀프플레이에서 상대 좌석의 수는 같은
|
||
네트워크가 둔 것인데 기존 트레이너는 `stop_gradient`로 버렸다. **공짜로 데이터가 2배**가 된다.
|
||
|
||
### 2. 매치 관측 — 작지만 실재
|
||
|
||
0.4751 (CI 상한 0.486 < 0.5). `to_move`, 덱 시계, ÷780 → ÷75 스케일 수정, 살아있는 점수
|
||
3분할이 합쳐서 약 2.5%p 값어치. carry 자체는 (분해 논증대로) 거의 기여하지 않으므로,
|
||
이 이득의 대부분은 **단판 obs의 결함 수정분**으로 보인다.
|
||
|
||
### 3. 전지적 critic — **Fable의 최우선 권고가 틀렸다**
|
||
|
||
Fable은 이것을 "가장 큰 누락 아이디어"로 꼽았다. 실측은 **반대**다: 끄면 오히려
|
||
**0.5160 (CI 하한 0.505 > 0.5)** 으로 유의하게 **더 강해진다.**
|
||
|
||
그럴듯한 이유: 특권 critic은 덱을 알기 때문에 가치를 아주 잘 맞추지만, 그 결과
|
||
`V(전체상태) ≠ E[리턴 | 마스킹된 관측]`이 되어, **어드밴티지에 정책이 통제할 수 없는 성분**이
|
||
섞인다. 액터 입장에서 그건 분산 감소가 아니라 노이즈다. 부분관측 환경에서 비대칭 critic이
|
||
해가 되는 것은 알려진 현상이다.
|
||
|
||
**조치: 전지적 critic을 기본에서 끈다.** (PIMC 탐색의 리프 평가기로 재활용한다는 계획은
|
||
별개로 유효할 수 있으나, 정책 학습용으로는 손해다.)
|
||
|
||
### 종합: 실제로 값어치 있었던 것
|
||
|
||
1. **in-scan auto-reset + GAE 절단 부트스트랩** — 샘플 5.8배, expert 0.4525 → 0.5071
|
||
2. **양쪽 좌석 학습** — 데이터 2배, 단독으로 승률 0.33 → 0.5
|
||
3. **선형 총점 보상** — tanh 대비 0.5859 (사용자 제안)
|
||
4. **매치 관측 결함 수정** — 약 2.5%p
|
||
5. ~~전지적 critic~~ — **해가 된다. 끈다.**
|
||
|
||
### 정정: 전지적 critic의 가치는 **스케일에 따라 뒤집힌다**
|
||
|
||
위 A/B는 39.3M learner 액션 규모에서 돌렸다. 실전 규모(131M)에서 동일 설정으로 둘을 직접
|
||
맞붙이면 **결론이 반대로 나온다.**
|
||
|
||
| 규모 | critic OFF의 승률 (critic ON 상대) | 판정 |
|
||
|---|---|---|
|
||
| 39.3M (A/B 규모) | **0.5160** [0.505, 0.527] | OFF가 낫다 |
|
||
| **131M (실전 규모)** | **0.4633** [0.453, 0.474] | **ON이 낫다** |
|
||
|
||
**전지적 critic은 그것을 학습시킬 데이터가 충분해질 때 값어치를 한다.** 39.3M에서는 특권
|
||
정보를 쓰는 큰 가치 트렁크를 제대로 못 맞춰서 어드밴티지에 노이즈만 얹었고, 131M에서는
|
||
제대로 맞춰서 분산 감소가 실현된다.
|
||
|
||
**교훈: 배포할 예산이 아닌 규모에서 ablation을 돌리면 결론이 뒤집힐 수 있다.**
|
||
`privileged_critic` 기본값은 **ON으로 되돌린다.**
|
||
|
||
### 최종 순위 (진짜 3라운드 클래식, duplicate 8,192판)
|
||
|
||
| 모델 | learner 액션 | vs league |
|
||
|---|---|---|
|
||
| **매치 스택 (critic ON)** | 131M | **0.6094** (+22.0점) |
|
||
| 매치 스택 (critic OFF) | 131M | 0.5526 (+10.9점) |
|
||
| league (기존 최강, 웹 배포판) | 122.6M | — |
|
||
|
||
### 기여도 재측정: 동일 규모(131M)에서
|
||
|
||
39.3M A/B는 critic에게 가장 불리한 지점이었다. 두 조각을 **같은 131M 예산**에서 다시 쟀다:
|
||
|
||
| 조각 | full의 승률 (그 조각 뺀 버전 상대) | 순이득 |
|
||
|---|---|---|
|
||
| **양쪽 좌석 학습** | 0.6707 | **+17.1 %p** |
|
||
| **전지적 critic** | 0.5367 | **+3.7 %p** |
|
||
|
||
**양쪽 좌석이 약 4.6배 크다.** 그리고 성격이 다르다:
|
||
|
||
| 규모 | 양쪽 좌석 | 전지적 critic |
|
||
|---|---|---|
|
||
| 39.3M | +16.8 %p | **−1.6 %p** |
|
||
| 131M | **+17.1 %p** | +3.7 %p |
|
||
|
||
**양쪽 좌석은 규모와 무관하게 안정적**(+17%p). **critic은 규모를 탄다.**
|
||
|
||
이유: 양쪽 좌석은 **데이터를 2배로** 만든다(65.5M → 131M, 같은 컴퓨트에서). critic은 데이터를
|
||
안 늘리고 **채점 정확도만** 올리는데, 코치가 배울 게 많아져서 데이터가 부족하면 오히려
|
||
엉터리 채점을 한다.
|
||
|
||
**Fable은 critic을 "가장 큰 누락 아이디어" 1순위로, 양쪽 좌석을 4순위로 꼽았다. 정확히 거꾸로였다.**
|
||
|
||
---
|
||
|
||
## Exploitability: 우리가 league보다 **덜 착취당한다** (2026-07-15)
|
||
|
||
**남겨뒀던 단서를 해소했다.** 평균 실력에서 이기는 것과 "파먹을 약점이 없는 것"은 다른
|
||
속성이다. 가위바위보에서 바위를 60% 내는 놈은 아무한테나 이길 수도 있지만, 그 습관을
|
||
알아챈 놈에게는 매번 진다.
|
||
|
||
**측정 방법:** 정책을 얼려놓고, **오직 그놈만 이기도록 특화된 새 정책을 처음부터 학습**시킨다
|
||
(`src/lost_cities_jax/exploit.py`). 착취자가 도달한 승률이 곧 그 정책이 못 막아낸 습관의 크기다.
|
||
|
||
**동일 착취자 예산 (250 업데이트 × batch 1024 = 32.5M learner 액션):**
|
||
|
||
| 얼려놓은 정책 | 착취자 승률 | 95% CI |
|
||
|---|---|---|
|
||
| **우리 매치 스택 (131M)** | **0.2278** | [0.219, 0.237] |
|
||
| league (웹 배포판, 122.6M) | **0.3213** | [0.311, 0.331] |
|
||
|
||
**league가 9.4%p 더 털린다.** 그리고 둘 다 0.5에서 한참 멀다 — 어느 쪽도 만만한 상대는 아니다.
|
||
|
||
### 최종 정리: 두 축 모두에서 이긴다
|
||
|
||
| 축 | 결과 |
|
||
|---|---|
|
||
| **평균 실력** (정면 대결) | 0.6094, +22.0점 ✅ |
|
||
| **약점 적음** (exploitability) | 0.2278 vs 0.3213 ✅ |
|
||
|
||
### 단서
|
||
|
||
1. **하한선이다.** 더 세거나 더 오래 학습한 착취자는 더 찾아낼 수 있다. **절대값이 아니라
|
||
동일 예산에서의 비교로만 의미가 있다.**
|
||
2. **착취자 예산이 목표의 약 1/4에 불과하다 (32.5M vs 131M).** 약한 공격자다.
|
||
따라서 **"우리는 22.8%밖에 안 털린다"고 말할 수 없다** — 그건 이 약한 공격자 기준일 뿐이다.
|
||
살아남는 주장은 **"동일 예산에서 league가 더 털린다"** 하나뿐이다.
|
||
착취자를 목표와 같은 예산(131M)으로 키워서 **순서가 유지되는지** 확인해야 한다.
|
||
3. league는 단판 정책이라 3라운드 게임에선 다소 제 물이 아니다 — 다만 carry가 무의미하다는
|
||
것이 이미 증명됐으므로 큰 불리함은 아니다.
|
||
|
||
**흥미로운 점:** 우리는 **순수 셀프플레이**이고 league는 **착취자 구조를 학습에 넣은** 런인데,
|
||
그런데도 우리가 덜 착취당한다. 착취자 구조가 exploitability를 낮춰줄 것이라는 기대가
|
||
이 게임에서는 확인되지 않았다.
|
||
|
||
### 착취자에 제대로 자금을 대고 재측정 (131M, 목표와 동일 예산)
|
||
|
||
앞선 32.5M 측정은 **공격자가 목표보다 4배 부족**했다. 예산을 목표와 맞춰 다시 쟀다.
|
||
실측 확인: 착취자 learner 액션 **130.4M** (우리 상대) / **131.1M** (league 상대).
|
||
|
||
| 얼려놓은 정책 | 착취자 승률 | 95% CI | 착취자 평균 마진 | 이전(32.5M) |
|
||
|---|---|---|---|---|
|
||
| **우리 매치 스택** (131.1M 학습) | **0.4657** | [0.455, 0.477] | **−5.5점** | 0.2278 |
|
||
| league (122.6M 학습) | **0.6295** | [0.619, 0.640] | **+28.8점** | 0.3213 |
|
||
|
||
**CI가 전혀 겹치지 않고, 격차가 오히려 벌어졌다 (0.094 → 0.164).**
|
||
|
||
**질적으로 선을 넘었다:** 제대로 자금 댄 전담 공격자는 **league를 아예 이긴다**(0.63, +28.8점).
|
||
반면 **우리는 여전히 못 뚫는다**(0.47, −5.5점).
|
||
|
||
#### 우리에게 불리한 단서 (반드시 함께 읽을 것)
|
||
|
||
- **둘 다 1000 업데이트 끝까지 정체 없이 오르고 있었다. 이 수치도 여전히 하한선이다.**
|
||
- **우리 쪽 착취자의 말단 기울기가 더 가파르다** (+0.031 vs +0.016 / 100 업데이트).
|
||
league 착취자 곡선은 꺾이기 시작했는데 우리 쪽은 아직 갈 길이 남았다.
|
||
**공격자 예산을 크게 더 키우면 격차가 좁혀질 수 있고, 뒤집힐 가능성도 배제 못 한다.**
|
||
|
||
#### 구조적 교란 (순수한 "학습법 A vs B"가 아니다)
|
||
|
||
1. **league는 단판 정책이다.** carry도 매치 점수도 모른다. 더 털리는 것의 일부는
|
||
**학습 방법이 아니라 구조적 맹점** 때문일 수 있다.
|
||
2. **둘 다 greedy(argmax)로 뒀다.** 결정론적 정책은 **정의상 최대로 착취당한다.**
|
||
양쪽을 똑같이 대우했으니 비교는 공정하나, **절대값은 부풀려져 있다.**
|
||
|
||
#### 살아남는 주장 / 못 하는 주장
|
||
|
||
- ✅ **"동일 예산의 전담 공격자 앞에서 league는 뚫리고 우리는 안 뚫린다."**
|
||
- ❌ "우리 정책은 착취 불가능하다." — 하한선일 뿐이고, 우리 쪽 곡선은 아직 오르는 중이다.
|