Spaces:
Running
Running
| from __future__ import annotations | |
| import json | |
| import random | |
| from collections import deque | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import trackio | |
| from game import ( | |
| canonical, | |
| is_terminal, | |
| legal_actions, | |
| minimax_action, | |
| play, | |
| symmetries, | |
| winner, | |
| ) | |
| from mcts import NeuralMCTS | |
| from model import MicroZeroNet, parameter_count | |
| from safetensors.torch import save_file | |
| from torch.nn import functional as F | |
| from torch.utils.data import DataLoader, TensorDataset | |
| PROJECT_DIR = Path(__file__).resolve().parent | |
| ARTIFACT_DIR = PROJECT_DIR / "artifacts" / "microzero" | |
| def seed_everything(seed: int) -> None: | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| def self_play_game( | |
| model: MicroZeroNet, | |
| rng: np.random.Generator, | |
| simulations: int, | |
| ) -> tuple[list[tuple[np.ndarray, np.ndarray, float]], int, int]: | |
| board = np.zeros(9, dtype=np.int8) | |
| player = 1 | |
| history = [] | |
| mcts = NeuralMCTS(model, simulations=simulations) | |
| moves = 0 | |
| while not is_terminal(board): | |
| temperature = 1.0 if moves < 4 else 0.15 | |
| policy = mcts.policy( | |
| board, | |
| player, | |
| temperature=temperature, | |
| add_noise=True, | |
| rng=rng, | |
| ) | |
| history.append((canonical(board, player), policy.copy(), player)) | |
| action = int(rng.choice(9, p=policy)) | |
| board = play(board, action, player) | |
| player = -player | |
| moves += 1 | |
| result = winner(board) | |
| examples = [] | |
| for state, policy, state_player in history: | |
| outcome = float(result * state_player) | |
| for transformed_state, transformed_policy in symmetries(state, policy): | |
| examples.append((transformed_state, transformed_policy, outcome)) | |
| return examples, result, moves | |
| def train_replay( | |
| model: MicroZeroNet, | |
| replay: deque, | |
| epochs: int, | |
| ) -> dict: | |
| states = torch.tensor(np.stack([item[0] for item in replay]), dtype=torch.float32) | |
| policies = torch.tensor( | |
| np.stack([item[1] for item in replay]), | |
| dtype=torch.float32, | |
| ) | |
| outcomes = torch.tensor([item[2] for item in replay], dtype=torch.float32) | |
| loader = DataLoader( | |
| TensorDataset(states, policies, outcomes), | |
| batch_size=256, | |
| shuffle=True, | |
| ) | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=0.0015, weight_decay=0.001) | |
| model.train() | |
| policy_losses = [] | |
| value_losses = [] | |
| for _ in range(epochs): | |
| for state, target_policy, target_value in loader: | |
| logits, value = model(state) | |
| policy_loss = -( | |
| target_policy * F.log_softmax(logits, dim=1) | |
| ).sum(dim=1).mean() | |
| value_loss = F.mse_loss(value, target_value) | |
| loss = policy_loss + value_loss | |
| optimizer.zero_grad(set_to_none=True) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| optimizer.step() | |
| policy_losses.append(policy_loss.item()) | |
| value_losses.append(value_loss.item()) | |
| return { | |
| "policy_loss": float(np.mean(policy_losses)), | |
| "value_loss": float(np.mean(value_losses)), | |
| } | |
| def neural_action( | |
| model: MicroZeroNet, | |
| board: np.ndarray, | |
| player: int, | |
| simulations: int, | |
| seed: int, | |
| ) -> int: | |
| mcts = NeuralMCTS(model, simulations=simulations) | |
| policy = mcts.policy( | |
| board, | |
| player, | |
| temperature=0.0, | |
| add_noise=False, | |
| rng=np.random.default_rng(seed), | |
| ) | |
| return int(policy.argmax()) | |
| def arena( | |
| model: MicroZeroNet, | |
| opponent: str, | |
| games: int, | |
| simulations: int, | |
| seed: int, | |
| ) -> dict: | |
| rng = np.random.default_rng(seed) | |
| wins = 0 | |
| draws = 0 | |
| losses = 0 | |
| lengths = [] | |
| for game in range(games): | |
| agent_player = 1 if game % 2 == 0 else -1 | |
| board = np.zeros(9, dtype=np.int8) | |
| player = 1 | |
| moves = 0 | |
| while not is_terminal(board): | |
| if player == agent_player: | |
| action = neural_action( | |
| model, | |
| board, | |
| player, | |
| simulations, | |
| seed + game * 11 + moves, | |
| ) | |
| elif opponent == "random": | |
| action = int(rng.choice(legal_actions(board))) | |
| else: | |
| action = minimax_action(board, player) | |
| board = play(board, action, player) | |
| player = -player | |
| moves += 1 | |
| result = winner(board) | |
| if result == agent_player: | |
| wins += 1 | |
| elif result == 0: | |
| draws += 1 | |
| else: | |
| losses += 1 | |
| lengths.append(moves) | |
| return { | |
| "games": games, | |
| "wins": wins, | |
| "draws": draws, | |
| "losses": losses, | |
| "win_rate": wins / games, | |
| "non_loss_rate": (wins + draws) / games, | |
| "average_moves": float(np.mean(lengths)), | |
| } | |
| def main() -> None: | |
| seed_everything(2043) | |
| rng = np.random.default_rng(2043) | |
| model = MicroZeroNet() | |
| replay: deque = deque(maxlen=24_000) | |
| iterations = 14 | |
| games_per_iteration = 70 | |
| trackio.init( | |
| project="microzero", | |
| name="neural-mcts-self-play-v1", | |
| config={ | |
| "parameters": parameter_count(model), | |
| "iterations": iterations, | |
| "games_per_iteration": games_per_iteration, | |
| "self_play_simulations": 32, | |
| "symmetry_augmentation": 8, | |
| }, | |
| ) | |
| history = [] | |
| for iteration in range(1, iterations + 1): | |
| outcomes = [] | |
| moves = [] | |
| for _ in range(games_per_iteration): | |
| examples, result, game_moves = self_play_game( | |
| model, | |
| rng, | |
| simulations=32, | |
| ) | |
| replay.extend(examples) | |
| outcomes.append(result) | |
| moves.append(game_moves) | |
| training = train_replay(model, replay, epochs=4) | |
| record = { | |
| "iteration": iteration, | |
| "replay_examples": len(replay), | |
| "x_win_rate": float(np.mean(np.asarray(outcomes) == 1)), | |
| "o_win_rate": float(np.mean(np.asarray(outcomes) == -1)), | |
| "draw_rate": float(np.mean(np.asarray(outcomes) == 0)), | |
| "average_game_moves": float(np.mean(moves)), | |
| **training, | |
| } | |
| history.append(record) | |
| trackio.log(record) | |
| random_arena = arena( | |
| model, | |
| opponent="random", | |
| games=400, | |
| simulations=64, | |
| seed=3043, | |
| ) | |
| minimax_arena = arena( | |
| model, | |
| opponent="minimax", | |
| games=200, | |
| simulations=96, | |
| seed=4043, | |
| ) | |
| results = { | |
| "model": "MicroZero", | |
| "parameters": parameter_count(model), | |
| "self_play_iterations": iterations, | |
| "self_play_games": iterations * games_per_iteration, | |
| "final_replay_examples": len(replay), | |
| "training_history": history, | |
| "versus_random": random_arena, | |
| "versus_exact_minimax": minimax_arena, | |
| } | |
| trackio.log( | |
| { | |
| "random_win_rate": random_arena["win_rate"], | |
| "random_non_loss_rate": random_arena["non_loss_rate"], | |
| "minimax_non_loss_rate": minimax_arena["non_loss_rate"], | |
| } | |
| ) | |
| trackio.finish() | |
| ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) | |
| save_file(model.state_dict(), ARTIFACT_DIR / "model.safetensors") | |
| (ARTIFACT_DIR / "evaluation.json").write_text( | |
| json.dumps(results, indent=2), | |
| encoding="utf-8", | |
| ) | |
| print(json.dumps(results, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |