ルールエージェントのパラメータ調整¶
作成日: 2026-07-05最終更新: 2026-07-07
Rule-based agent の params.yaml を 自動チューニング する仕組み。手動で magic
number をいじる代わりに、grid / Optuna TPE で探索し、勝率で自動選定する。
関連ドキュメント¶
先に読むと分かりやすい順:
- 本ドキュメント — Tuning 全体像 / 目的関数 / 使い方
- rule-agent-tuning-optuna.md — Optuna stage 実装詳細
- rule-agent-tuning-algorithm.md — TPE / MedianPruner のアルゴリズム詳細
- rule-agent-tuning-flow.md — どのファイルが、いつ、どういう仕組みで更新されるか
- rule-agent-config.md — Config loader (
PCA_PARAMS_OVERRIDE_DIRenv var) - ../decisions/0003-tuning-tpe-vs-grid.md — なぜ TPE を採用したか (ADR)
対応実装:
src/pca/rule_agents/tuning/space.py— 探索空間 schemasrc/pca/rule_agents/tuning/grid.py— grid 生成 + params overridesrc/pca/rule_agents/tuning/optuna_stage.py— Optuna TPE 統合src/pca/rule_agents/tuning/evaluator.py— Stub / Selfplay evaluatorsrc/pca/rule_agents/tuning/aggregate.py— 目的関数 + top-K 出力src/pca/rule_agents/tuning/__main__.py— CLI
対応 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.yamlのmatch_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
を付けると、探索終了後に:
- top-K の探索結果 + baseline (現行 params.yaml) を候補に選ぶ (重複除去)
- 全候補を
--confirm-games-per-config(default: 探索の 3 倍) ×--confirm-seed(default: seed + 100000、探索と別系列) で再評価 - 確認ランの 1 位が best.yaml になる。history.csv / top_k.yaml は探索の記録のままなので、探索順位と確認順位のズレが confirmed.csv で見える
- 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.py の
SelfplayEvaluator。
仕組み:
workdir/<agent_id>/params.yamlに base + overrides を書く (override_dir.py)workdir/rule_agents.yamlを tuning target + opponents だけに絞って生成subprocess.run([python, -m, pca.training.selfplay, ...])で games 実行、env にPCA_PARAMS_OVERRIDE_DIR=workdirを注入load_params_alongside(__file__)が env を尊重し override 版 params を採用 (config.py)- 生成された
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 サブコマンドを実行する。
テスト方針¶
tests/rule_agents/test_tuning.py— space / grid / aggregator / CLI (18 tests)tests/rule_agents/test_tuning_evaluator.py— StubEvaluator / SelfplayEvaluator (mock subprocess) / summary CSV parsertests/rule_agents/test_tuning_override_dir.py— override_dir 書き出し + cleanuptests/rule_agents/test_tuning_optuna.py—space_to_trial/run_optunaE2E (skipUnless(optuna_available))tests/rule_agents/test_config.py—PCA_PARAMS_OVERRIDE_DIR優先ロード
既知の制約¶
- Stub evaluator は勝率とは無関係: パイプライン検証のみ。実運用は Selfplay evaluator を使う
- 並列は現時点で単純: Grid stage は逐次、Optuna stage は
n_jobs=1推奨 (subprocess 並列と衝突するため)。将来multiprocessing.Poolで config 並列化 - params.yaml override の deep copy: 大きい yaml だとメモリ消費が気になる。現状 raging_bolt の params.yaml は ~150 行 = 数 KB なので問題なし
- Continuous の step 挙動: grid enumerate は
low + span * i / (n-1)5-point がデフォルト、"n" は現状 5 固定。Optuna 側はsuggest_int/floatに step をそのまま渡す - 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対応。