Deep CFR imitation pretraining 추가
This commit is contained in:
@@ -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
|
||||||
Reference in New Issue
Block a user