Deep CFR imitation pretraining 추가

This commit is contained in:
2026-05-07 00:12:41 +09:00
parent 8f54415832
commit 647baa7d6d
3 changed files with 167 additions and 0 deletions
@@ -14,6 +14,9 @@ from coolrl_lost_cities.games.classic.deep_cfr.evaluate import (
evaluate_strategy_network, evaluate_strategy_network,
load_strategy_policy_from_checkpoint, load_strategy_policy_from_checkpoint,
) )
from coolrl_lost_cities.games.classic.deep_cfr.imitation import (
new_pretrained_strategy_network,
)
from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer
from coolrl_lost_cities.games.classic.game import classic_config from coolrl_lost_cities.games.classic.game import classic_config
@@ -80,6 +83,23 @@ def benchmark_command(args: argparse.Namespace) -> None:
print(json.dumps(result, indent=2, sort_keys=True)) print(json.dumps(result, indent=2, sort_keys=True))
def pretrain_command(args: argparse.Namespace) -> None:
network, metrics = new_pretrained_strategy_network(
classic_config(seed=args.seed),
hidden_size=args.hidden_size,
games=args.games,
seed=args.seed,
steps=args.steps,
)
if args.output:
import torch
torch.save(
{"strategy_network": network.state_dict(), "metrics": metrics.__dict__}, args.output
)
print(json.dumps(metrics.__dict__, sort_keys=True))
def main(argv: list[str] | None = None) -> None: def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="Lost Cities classic Deep CFR tools.") parser = argparse.ArgumentParser(description="Lost Cities classic Deep CFR tools.")
subparsers = parser.add_subparsers(dest="command", required=True) subparsers = parser.add_subparsers(dest="command", required=True)
@@ -114,6 +134,14 @@ def main(argv: list[str] | None = None) -> None:
benchmark.add_argument("--compare", action="store_true") benchmark.add_argument("--compare", action="store_true")
benchmark.set_defaults(func=benchmark_command) benchmark.set_defaults(func=benchmark_command)
pretrain = subparsers.add_parser("pretrain")
pretrain.add_argument("--games", type=int, default=4)
pretrain.add_argument("--steps", type=int, default=32)
pretrain.add_argument("--hidden-size", type=int, default=64)
pretrain.add_argument("--seed", type=int, default=1)
pretrain.add_argument("--output")
pretrain.set_defaults(func=pretrain_command)
args = parser.parse_args(argv) args = parser.parse_args(argv)
args.func(args) args.func(args)
@@ -0,0 +1,108 @@
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
import torch
from torch import nn
from coolrl_lost_cities.games.classic.bots import SafeHeuristicBot
from coolrl_lost_cities.games.classic.deep_cfr.encoding import encode_info_state, input_dim
from coolrl_lost_cities.games.classic.deep_cfr.networks import DeepCFRMLP
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig, classic_config
@dataclass(frozen=True)
class ImitationMetrics:
samples: int
loss: float
def collect_safe_heuristic_samples(
config: LostCitiesConfig | None = None,
*,
games: int = 4,
seed: int = 1,
max_steps: int = 10_000,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
game_config = config or classic_config(seed=seed)
probe = GameState.new_game(game_config, seed=seed)
action_size = 2 * probe.config.hand_size + 1 + probe.config.n_colors
bot = SafeHeuristicBot()
infos: list[np.ndarray] = []
targets: list[np.ndarray] = []
masks: list[np.ndarray] = []
for game_index in range(games):
state = GameState.new_game(game_config, seed=seed + game_index)
for _ in range(max_steps):
if state.terminal:
break
info = encode_info_state(state, state.current_player)
legal = np.asarray(state.unified_legal_mask(), dtype=bool)
action = bot.act(state)
unified = state.to_unified_action(action)
target = np.zeros(action_size, dtype=np.float32)
target[unified] = 1.0
infos.append(info)
targets.append(target)
masks.append(legal)
state.apply_action(action)
if not infos:
raise RuntimeError("no imitation samples collected")
return (
np.stack(infos).astype(np.float32),
np.stack(targets).astype(np.float32),
np.stack(masks).astype(bool),
)
def pretrain_strategy_network(
strategy_network: nn.Module,
config: LostCitiesConfig | None = None,
*,
games: int = 4,
seed: int = 1,
steps: int = 32,
batch_size: int = 64,
learning_rate: float = 1.0e-3,
device: torch.device | str = "cpu",
) -> ImitationMetrics:
x_np, y_np, legal_np = collect_safe_heuristic_samples(config, games=games, seed=seed)
device = torch.device(device)
strategy_network.to(device)
strategy_network.train()
optimizer = torch.optim.Adam(strategy_network.parameters(), lr=learning_rate)
rng = np.random.default_rng(seed + 17)
last_loss = 0.0
for _ in range(max(0, steps)):
indices = rng.choice(
len(x_np), size=min(batch_size, len(x_np)), replace=len(x_np) < batch_size
)
x = torch.as_tensor(x_np[indices], dtype=torch.float32, device=device)
y = torch.as_tensor(y_np[indices], dtype=torch.float32, device=device)
legal = torch.as_tensor(legal_np[indices], dtype=torch.bool, device=device)
logits = strategy_network(x).masked_fill(~legal, torch.finfo(torch.float32).min)
log_probs = nn.functional.log_softmax(logits, dim=-1).masked_fill(~legal, 0.0)
loss = -(y * log_probs).sum(dim=-1).mean()
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
last_loss = float(loss.detach().cpu())
return ImitationMetrics(samples=len(x_np), loss=last_loss)
def new_pretrained_strategy_network(
config: LostCitiesConfig | None = None,
*,
hidden_size: int = 64,
games: int = 4,
seed: int = 1,
steps: int = 32,
) -> tuple[DeepCFRMLP, ImitationMetrics]:
game_config = config or classic_config(seed=seed)
probe = GameState.new_game(game_config, seed=seed)
network = DeepCFRMLP(
input_dim(probe), 2 * probe.config.hand_size + 1 + probe.config.n_colors, hidden_size
)
metrics = pretrain_strategy_network(network, game_config, games=games, seed=seed, steps=steps)
return network, metrics
@@ -0,0 +1,31 @@
from __future__ import annotations
from coolrl_lost_cities.games.classic.game import LostCitiesConfig
from coolrl_lost_cities.games.classic.deep_cfr.imitation import (
collect_safe_heuristic_samples,
new_pretrained_strategy_network,
)
def test_collect_safe_heuristic_samples_shapes() -> None:
x, y, legal = collect_safe_heuristic_samples(LostCitiesConfig(seed=61), games=1, seed=61)
assert len(x) == len(y) == len(legal)
assert x.ndim == 2
assert y.shape == legal.shape
assert y.sum(axis=1).min() == 1.0
def test_pretrain_strategy_network_smoke() -> None:
network, metrics = new_pretrained_strategy_network(
LostCitiesConfig(seed=67),
hidden_size=16,
games=1,
seed=67,
steps=1,
)
assert metrics.samples > 0
assert metrics.loss >= 0.0
assert network is not None