コンテンツにスキップ

ルールエージェントのパラメータ調整

作成日: 2026-07-05最終更新: 2026-07-07

Rule-based agent の params.yaml自動チューニング する仕組み。手動で magic number をいじる代わりに、grid / Optuna TPE で探索し、勝率で自動選定する。

関連ドキュメント

先に読むと分かりやすい順:

  1. 本ドキュメント — Tuning 全体像 / 目的関数 / 使い方
  2. rule-agent-tuning-optuna.md — Optuna stage 実装詳細
  3. rule-agent-tuning-algorithm.mdTPE / MedianPruner のアルゴリズム詳細
  4. rule-agent-tuning-flow.mdどのファイルが、いつ、どういう仕組みで更新されるか
  5. rule-agent-config.md — Config loader (PCA_PARAMS_OVERRIDE_DIR env var)
  6. ../decisions/0003-tuning-tpe-vs-grid.md — なぜ TPE を採用したか (ADR)

対応実装:

対応 research doc: docs/research/2026-07-05-rule-agents-redesign.md の "パラメータチューニング機構" 節。

目的

現状の rule agent は 20k 行のコードに magic number が散在し、閾値を触るには main.py を書き換える必要がある。これは以下を許さない:

  • 差分の意味 / 過去 config / A/B 履歴の管理
  • Grid search、CMA-ES、Bayes-opt などの自動探索
  • チューニング結果の再現性

tuning runner は params.yaml だけを触って 自動的に best を探す。

全体像

flowchart TB
    SP["space.yaml<br/>(何をチューニングするか)"]:::input
    P0["params.yaml<br/>(baseline)"]:::input
    OP["opponent pool<br/>(凍結 agent 中心)"]:::input

    GRID["iter_grid(space)"]:::proc
    OVL["override_yaml(base, cfg)"]:::proc
    EVAL["evaluator<br/>(stub | selfplay)"]:::proc
    AGG["Aggregator.rank()"]:::proc

    HIST["history.csv"]:::output
    TOP["top_k.yaml"]:::output
    BEST["best.yaml"]:::output
    MAN["manifest.json"]:::output

    SP --> GRID
    P0 --> OVL
    GRID --> OVL --> EVAL
    OP --> EVAL
    EVAL --> AGG --> HIST
    AGG --> TOP
    AGG --> BEST
    AGG --> MAN

    classDef input fill:#e3f2fd,stroke:#1976d2,color:#000
    classDef proc fill:#fff3e0,stroke:#f57c00,color:#000
    classDef output fill:#e8f5e9,stroke:#388e3c,color:#000

段階的探索 (research doc の再掲)

ノイズが大きい (1 config × 200 games で勝率 ±0.03) ため、単一アルゴリズムではなく 3 段階 で進める:

Stage アルゴリズム 用途 対象 params 数
A Coarse grid 初期値決定、大域最適近似 高影響 5〜8 個
B Coordinate descent grid best からの微調整 5 個 × 5 値 = 25 config/round
C CMA-ES 全 params の同時最適化 (将来) 20〜50

現時点で Stage A (grid) のみ実装済み。Stage B/C は空きスケルトンで、将来追加する余地を残す (space.py は continuous 型もサポート済み)。

探索空間 YAML

configs/rule_agents/tuning/<agent_id>_space.yaml に置く。実例:

# configs/rule_agents/tuning/raging_bolt_space.yaml
supporter_tier.critical:
  type: choice
  values: [18000, 20000, 22000, 25000]

supporter_tier.lethal:
  type: choice
  values: [40000, 50000, 60000]

attach_energy.main_attacker_type_match_bonus:
  type: choice
  values: [3000, 5000, 7000]

items.hyper_ball_empty_bench:
  type: choice
  values: [18000, 22000, 26000]

= 4 × 3 × 3 × 3 = 108 config。各 config で 200 games × 3〜5 対戦相手 = 600〜1000 games。CPU 8 並列で 4〜6 時間。

パラメータの種類

Choice — 列挙:

some_key:
  type: choice
  values: [x, y, z]

Continuous — 連続 (grid は step で離散化、CMA-ES/Bayes は step 無視):

some_key:
  type: continuous
  low: 3000
  high: 8000
  step: 1000 # 省略時は 5-point 等分割

公開API

SearchSpace

from pca.rule_agents.tuning import SearchSpace

space = SearchSpace.load("configs/rule_agents/tuning/raging_bolt_space.yaml")
print(space.size_estimate())    # 108
for cfg in iter_grid(space):
    print(cfg)  # {'supporter_tier.critical': 18000, ...}

Overrides の適用

from pca.rule_agents.tuning import override_yaml
import yaml

base = yaml.safe_load(open("params.yaml"))
merged = override_yaml(base, {"supporter_tier.critical": 22000})
# merged is a deep copy; base is untouched

Aggregator (目的関数 + 順位付け)

from pca.rule_agents.tuning import Aggregator, EvaluationResult

agg = Aggregator(
    win_weight=1.0,
    unfinished_penalty=0.5,
    prize_diff_weight=0.1,
    deck_out_penalty=0.2,
)
ranked = agg.rank(results)  # list of (result, score)

目的関数

score = 1.0 * win_term
      - 0.5 * unfinished_rate
      + 0.1 * (mean_prize_diff / 6)
      - 0.2 * deck_out_loss_rate
      + worst_opponent_weight * min(per_opponent)   # optional, default 0
  • win_term: 既定では match_weight 加重の per-opponent 勝率 Σ w_i·rate_i / Σ w_i (メタで当たりやすい相手を重視)。重みは --opponents auto なら configs/rule_agents.yamlmatch_weight、明示リストなら id:weight 記法から取る。--no-match-weight-objective で従来のプール全体平均勝率に戻せる。per_opponent データが無い場合も自動で従来式に fallback
  • unfinished_rate: unfinished (時間切れ) の比率 — サステインループ / 無限ターン化を抑制
  • mean_prize_diff: 平均サイド差 / 6 で正規化
  • deck_out_loss_rate: deck-out 負けの比率 (山切れは強く減点)
  • worst_opponent 項: --worst-opponent-weight w+ w * min(per_opponent) を加算。1 マッチアップを犠牲に平均を稼ぐ過適合を抑える保険 (default 0 = 無効)

注意: per_opponent は matchup CSV の 完走ゲームのみの勝率 (win_rate_finished)。加重時の win_term は「完走勝率の加重平均 + unfinished は別ペナルティ項」という意味になる。matchup 行が無い相手は除外して再正規化される (警告 1 回)。

重みは CLI で調整可能:

--win-weight 1.0 --unfinished-penalty 0.5 \
  --prize-diff-weight 0.1 --deck-out-penalty 0.2 \
  --worst-opponent-weight 0.0

コマンドラインからの実行

探索を実行する

uv run python -m pca.rule_agents.tuning run \
  --agent raging_bolt_ogerpon \
  --stage grid \
  --space configs/rule_agents/tuning/raging_bolt_space.yaml \
  --games-per-config 200 \
  --parallel 8 \
  --output data/rule_tuning/raging_bolt/2026-07-05/ \
  --top-k 5 \
  --evaluator stub # or selfplay (実装済み)

対戦相手プール--opponents で制御する:

  • --opponents auto (default): --rule-agent-config から enabled != false かつ match_weight > 0 の agent を全て抽出、tuning 対象を除いた集合を使用。runtime の rule-pool selfplay と同じ配布
  • --opponents opp1,opp2,opp3: 明示的カンマ区切り。ミニ pool でスモークしたい / 特定 matchup にフォーカスしたいときに使う

自動 pool は例えば raging_bolt_ogerpon で 25 種以上の pool になる。1 config の games/opponent が薄まる (200 games × 25 = 8 games/opp) ので per-opponent の win rate は信頼できないが、combined win rate は runtime に近い評価になる。

出力ファイル:

ファイル 内容
history.csv 全 config の (objective / win_rate / unfinished / prize_diff / deck_out / overrides)
top_k.yaml 上位 K config の overrides と score
best.yaml 確認ラン有効時は confirm winner、無効時は探索 top-1 を base にマージした完全版
confirmed.csv (確認ラン有効時のみ) 各候補の search/confirm objective と順位
manifest.json 再現用の meta (agent_id, space, base, 重み, seed_mode, warm_start, confirmation)

確認ラン (winner's curse 対策)

探索の argmax は上振れを選びやすい (50 trial の最大値は選択バイアスで楽観的)。 --confirm-top-k K を付けると、探索終了後に:

  1. top-K の探索結果 + baseline (現行 params.yaml) を候補に選ぶ (重複除去)
  2. 全候補を --confirm-games-per-config (default: 探索の 3 倍) × --confirm-seed (default: seed + 100000、探索と別系列) で再評価
  3. 確認ランの 1 位が best.yaml になる。history.csv / top_k.yaml は探索の記録のままなので、探索順位と確認順位のズレが confirmed.csv で見える
  4. winner が baseline だった場合は WARNING — 探索が現行値を超えなかったことを意味し、apply は no-op。空間を広げるか games/trials を増やす

grid / optuna 両 stage で使える (stage 非依存の後段処理)。運用推奨は --confirm-top-k 3 --confirm-games-per-config <探索の3倍>

探索結果を反映する

uv run python -m pca.rule_agents.tuning apply \
  --agent raging_bolt_ogerpon \
  --from data/rule_tuning/raging_bolt/2026-07-05/best.yaml \
  --dry-run

--dry-run を外すと agent の params.yaml が書き換わる。commit message には tuning 内容 (対戦相手プール、games/config、勝率改善) を書く。

評価器 (Evaluator)

Stub evaluator (実装済み)

overrides の SHA-256 と seed から擬似ランダムな win_rate を返す。パイプラインの smoke test 用。実際の勝率とは無関係。以下の 3 用途に有効:

  • CLI が最後まで走ることの確認
  • history.csv / top_k.yaml / best.yaml のフォーマット検証
  • Aggregator 順位付けの smoke

Selfplay evaluator (実装済み)

実装: src/pca/rule_agents/tuning/evaluator.pySelfplayEvaluator

仕組み:

  1. workdir/<agent_id>/params.yaml に base + overrides を書く (override_dir.py)
  2. workdir/rule_agents.yaml を tuning target + opponents だけに絞って生成
  3. subprocess.run([python, -m, pca.training.selfplay, ...]) で games 実行、env に PCA_PARAMS_OVERRIDE_DIR=workdir を注入
  4. load_params_alongside(__file__) が env を尊重し override 版 params を採用 (config.py)
  5. 生成された agent-summary.csv + agent-matchup-summary.csv をパースして EvaluationResult を返す

Optuna stage との連携詳細は rule-agent-tuning-optuna.md を参照。

並列化: 現状は 1 trial = 1 subprocess (逐次)。Optuna の n_jobs > 1 は CPU コアの取り合いになるため非推奨。将来的に multiprocessing.Pool ベースの grid 並列化を追加予定。

成功条件下限: 200 games × 3 対戦相手 = 600 games 以上。勝率 ±0.02 の分解能を確保。

設計判断

なぜ Stub evaluator を先に入れるか

Selfplay 統合はプロジェクトの副作用が大きく、実装コストも高い。パイプライン (space → grid → aggregate → best) 自体の検証は stub で完結できるため、 先にパイプラインを固めて後で evaluator を差し替え可能な設計 にした。

なぜ Aggregator を dataclass で分離するか

重みは実験ごとに変わりうる (unfinished を 0.3 にするか 0.5 にするかで結果が変わる)。CLI 引数から作れるようにしておくと、同じ history.csv を後から別の重み付けで再ランキングできる。

なぜ CMA-ES / Bayes-opt を optional にしたか

  • ノイズ 0.03 で Bayes-opt の GP surrogate が信用できない
  • CMA-ES は大次元で光るが最低 500 games/config 必要 → 現状の計算予算では重すぎる
  • Grid + coordinate descent が現実的、後で必要になったら足す

best.yaml の書き戻し方針

手動 apply。tuning が best.yaml を作ったからといって自動で agent の params.yaml を上書きしない。ryo さんが top_k.yaml と history.csv を見て妥当と判断した時のみ apply サブコマンドを実行する。

テスト方針

既知の制約

  1. Stub evaluator は勝率とは無関係: パイプライン検証のみ。実運用は Selfplay evaluator を使う
  2. 並列は現時点で単純: Grid stage は逐次、Optuna stage は n_jobs=1 推奨 (subprocess 並列と衝突するため)。将来 multiprocessing.Pool で config 並列化
  3. params.yaml override の deep copy: 大きい yaml だとメモリ消費が気になる。現状 raging_bolt の params.yaml は ~150 行 = 数 KB なので問題なし
  4. Continuous の step 挙動: grid enumerate は low + span * i / (n-1) 5-point がデフォルト、"n" は現状 5 固定。Optuna 側は suggest_int/float に step をそのまま渡す
  5. Optuna stage は in-memory: セッション再開不可。trials.jsonl に逐次書き出しはしているので事後分析は可能

今後の拡張

  • games 数の multi-fidelity 化: 30 → 100 → 200 games の段階評価 + Wilson 区間での早期打ち切り (Evaluator protocol の拡張が必要。tuning を数回実走して評価時間が痛くなってから)
  • 並列 config 実行: multiprocessing.Pool を grid / optuna 双方に追加
  • Hyperband / BOHB: optuna.pruners.HyperbandPruner に切り替え
  • CMA-ES sampler: optuna.samplers.CmaEsSampler に差し替え
  • Coordinate descent stage: grid best から始めて 1 パラメータずつ微調整
  • Study 永続化 (optional): SQLite / Redis 経由の再開サポート
  • Elo ベース評価: 相手プールが増えたら raw 勝率でなく Elo 差を objective に
  • 多目的化 (NSGA-II): win_rate vs unfinished の Pareto front
  • Regret-based re-ranking: history.csv を別重みで再ランクする CLI

変更履歴

  • 2026-07-09: 信頼性改善。baseline warm-start / --seed-mode fixed 既定化 / 確認ラン (--confirm-top-k, confirmed.csv, best.yaml = confirm winner) / match_weight 加重 objective + --worst-opponent-weight
  • 2026-07-05: 初版。SearchSpace / iter_grid / Aggregator / CLI run + apply。Stub evaluator のみ実装。
  • 2026-07-06: Selfplay evaluator 実装 + Optuna stage 追加 (rule-agent-tuning-optuna.md 参照)。Evaluator を Protocol として抽象化、PCA_PARAMS_OVERRIDE_DIR 対応。