| import torch |
| import torch.nn.functional as F |
| import yaml |
| import os |
| import chess |
| import chess.engine |
| import random |
| import time |
| from model import ChessTransformer |
| from data_loader import VOCAB |
|
|
| |
| INV_VOCAB = {v: k for k, v in VOCAB.items()} |
|
|
| |
| with open('config.yaml', 'r') as f: |
| config = yaml.safe_load(f) |
|
|
| model_cfg = config['model'] |
| data_cfg = config['data'] |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| print(f"Using device: {device}") |
|
|
| |
| model = ChessTransformer( |
| vocab_size=len(VOCAB), |
| d_model=model_cfg['d_model'], |
| nhead=model_cfg['nhead'], |
| num_layers=model_cfg['num_layers'], |
| max_length=data_cfg['max_length'] |
| ).to(device) |
|
|
| |
| weights_path = 'weights/sf_rl_latest.pth' |
| if not os.path.exists(weights_path): |
| weights_path = 'rl_weights/champion_latest.pth' |
| if not os.path.exists(weights_path): |
| weights_path = config['training']['weights_path'] |
|
|
| if os.path.exists(weights_path): |
| checkpoint = torch.load(weights_path, map_location=device, weights_only=True) |
| if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint: |
| model.load_state_dict(checkpoint['model_state_dict']) |
| else: |
| model.load_state_dict(checkpoint) |
| print(f"Loaded weights from {weights_path}") |
| else: |
| print("Warning: No pre-trained weights found. Training from scratch!") |
|
|
| model.train() |
| optimizer = torch.optim.Adam(model.parameters(), lr=1e-5) |
|
|
| |
| stockfish_path = os.path.join("stockfish", "stockfish-windows-x86-64-avx2.exe") |
| if not os.path.exists(stockfish_path): |
| raise FileNotFoundError(f"Stockfish not found at {stockfish_path}") |
|
|
| engine = chess.engine.SimpleEngine.popen_uci(stockfish_path) |
| |
| engine.configure({"Hash": 256, "Threads": 2}) |
| LIMIT = chess.engine.Limit(time=0.1) |
|
|
| def encode_history(history, max_length=120): |
| seq = [VOCAB.get(tok, VOCAB.get("<unk>", 0)) for tok in history] |
| if len(seq) > max_length: |
| seq = seq[-max_length:] |
| else: |
| seq = seq + [0] * (max_length - len(seq)) |
| return torch.tensor(seq, dtype=torch.long, device=device).unsqueeze(0) |
|
|
| def evaluate_position(board): |
| info = engine.analyse(board, LIMIT) |
| score = info["score"].white() |
| |
| if score.is_mate(): |
| return 10000 if score.mate() > 0 else -10000 |
| return score.score() |
|
|
| print("\nStarting Dense Policy Gradient RL with Stockfish...") |
|
|
| epoch = 0 |
| os.makedirs("weights", exist_ok=True) |
|
|
| try: |
| while True: |
| epoch += 1 |
| board = chess.Board() |
| history = ["<bos>"] |
| |
| states = [] |
| actions = [] |
| advantages = [] |
| |
| print(f"\n--- Game {epoch} ---") |
| |
| current_eval_cp = evaluate_position(board) |
| |
| while not board.is_game_over() and len(history) < 150: |
| |
| with torch.no_grad(): |
| x = encode_history(history) |
| policy_logits, _ = model(x) |
| p_logits = policy_logits[0, -1, :] |
| |
| |
| temperature = 1.0 |
| logits = p_logits / temperature |
| legal_moves = list(board.legal_moves) |
| legal_ucis = [m.uci() for m in legal_moves] |
| |
| mask = torch.full_like(logits, float('-inf')) |
| for idx, token in INV_VOCAB.items(): |
| if token in legal_ucis: |
| mask[idx] = 0.0 |
| |
| masked_logits = logits + mask |
| probs = F.softmax(masked_logits, dim=-1) |
| |
| if torch.isnan(probs).any() or probs.sum() == 0: |
| move = random.choice(legal_moves) |
| else: |
| m_idx = torch.multinomial(probs, 1).item() |
| action_str = INV_VOCAB.get(m_idx) |
| if action_str not in legal_ucis: |
| move = random.choice(legal_moves) |
| else: |
| move = chess.Move.from_uci(action_str) |
| |
| |
| board.push(move) |
| next_eval_cp = evaluate_position(board) |
| |
| if not board.turn: |
| adv = next_eval_cp - current_eval_cp |
| else: |
| adv = current_eval_cp - next_eval_cp |
| |
| current_eval_cp = next_eval_cp |
| |
| |
| states.append(x.squeeze(0)) |
| actions.append(VOCAB.get(move.uci(), 0)) |
| advantages.append(adv) |
| |
| history.append(move.uci()) |
| |
| if len(history) % 10 == 0: |
| print(f"Move {len(history)} | Advantage: {adv} cp | Eval: {current_eval_cp}") |
| |
| |
| print(f"Game finished. Training on {len(states)} moves...") |
| |
| states_t = torch.stack(states).to(device) |
| actions_t = torch.tensor(actions, dtype=torch.long, device=device) |
| |
| adv_t = torch.tensor(advantages, dtype=torch.float32, device=device) |
| if adv_t.std() > 0: |
| adv_t = (adv_t - adv_t.mean()) / (adv_t.std() + 1e-8) |
| else: |
| adv_t = adv_t - adv_t.mean() |
| |
| optimizer.zero_grad() |
| |
| |
| policy_logits, _ = model(states_t) |
| |
| |
| p_logits = policy_logits[:, -1, :] |
| |
| |
| log_probs = F.log_softmax(p_logits, dim=-1) |
| action_log_probs = log_probs.gather(1, actions_t.unsqueeze(1)).squeeze(1) |
| |
| |
| loss = -(action_log_probs * adv_t).mean() |
| |
| loss.backward() |
| |
| |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) |
| optimizer.step() |
| |
| print(f"Loss: {loss.item():.4f} | Avg Advantage: {sum(advantages)/len(advantages):.2f} cp") |
| |
| |
| if epoch % 5 == 0: |
| torch.save(model.state_dict(), 'weights/sf_rl_latest.pth') |
| print("Saved weights to weights/sf_rl_latest.pth") |
|
|
| except KeyboardInterrupt: |
| print("Training stopped manually.") |
| finally: |
| engine.quit() |
| print("Stockfish engine closed.") |
|
|