| """ | |
| Chess Position Evaluator (V7 HULK) - Inference Example | |
| Author: Ahmed Darwish (@engdarwish) | |
| https://huggingface.co/engdarwish/chess-position-evaluator | |
| Minimal, runnable example: download the weights, rebuild the exact | |
| SE-ResNet-20 dual-head architecture, encode a FEN position, and get | |
| a value estimate + ranked legal moves. | |
| Install: | |
| pip install torch python-chess huggingface_hub numpy | |
| """ | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import chess | |
| from huggingface_hub import hf_hub_download | |
| REPO_ID = "engdarwish/chess-position-evaluator" | |
| MOVE_SPACE = 64 * 64 * 5 | |
| PROMO_MAP = {None: 0, chess.QUEEN: 1, chess.ROOK: 2, chess.BISHOP: 3, chess.KNIGHT: 4} | |
| PROMO_MAP_INV = {v: k for k, v in PROMO_MAP.items() if k is not None} | |
| def uci_to_idx(uci_str: str) -> int: | |
| move = chess.Move.from_uci(uci_str) | |
| promo = PROMO_MAP.get(move.promotion, 0) | |
| return (move.from_square * 64 + move.to_square) * 5 + promo | |
| def idx_to_uci(idx: int) -> str: | |
| promo_type = idx % 5 | |
| rest = idx // 5 | |
| to_sq = rest % 64 | |
| from_sq = rest // 64 | |
| promo_piece = PROMO_MAP_INV.get(promo_type) | |
| return chess.Move(from_sq, to_sq, promotion=promo_piece).uci() | |
| PIECE_IDX = { | |
| chess.PAWN: 0, chess.KNIGHT: 1, chess.BISHOP: 2, | |
| chess.ROOK: 3, chess.QUEEN: 4, chess.KING: 5, | |
| } | |
| def fen_to_tensor(fen: str) -> np.ndarray: | |
| board = chess.Board(fen) | |
| t = np.zeros((18, 8, 8), dtype=np.float32) | |
| for sq in chess.SQUARES: | |
| piece = board.piece_at(sq) | |
| if piece: | |
| r = 7 - (sq >> 3) | |
| c = sq & 7 | |
| ch = PIECE_IDX[piece.piece_type] | |
| t[ch if piece.color else ch + 6, r, c] = 1.0 | |
| if board.turn == chess.WHITE: | |
| t[12] = 1.0 | |
| t[13] = float(board.has_kingside_castling_rights(chess.WHITE)) | |
| t[14] = float(board.has_queenside_castling_rights(chess.WHITE)) | |
| t[15] = float(board.has_kingside_castling_rights(chess.BLACK)) | |
| t[16] = float(board.has_queenside_castling_rights(chess.BLACK)) | |
| if board.ep_square is not None: | |
| t[17, 7 - (board.ep_square >> 3), board.ep_square & 7] = 1.0 | |
| return t | |
| class SEBlock(nn.Module): | |
| def __init__(self, channels, reduction=16): | |
| super().__init__() | |
| self.pool = nn.AdaptiveAvgPool2d(1) | |
| self.fc = nn.Sequential( | |
| nn.Linear(channels, channels // reduction, bias=False), | |
| nn.ReLU(inplace=True), | |
| nn.Linear(channels // reduction, channels, bias=False), | |
| nn.Sigmoid(), | |
| ) | |
| def forward(self, x): | |
| b, c, _, _ = x.size() | |
| y = self.pool(x).view(b, c) | |
| y = self.fc(y).view(b, c, 1, 1) | |
| return x * y | |
| class SEResBlock(nn.Module): | |
| def __init__(self, channels): | |
| super().__init__() | |
| self.conv1 = nn.Conv2d(channels, channels, 3, padding=1, bias=False) | |
| self.bn1 = nn.BatchNorm2d(channels) | |
| self.conv2 = nn.Conv2d(channels, channels, 3, padding=1, bias=False) | |
| self.bn2 = nn.BatchNorm2d(channels) | |
| self.se = SEBlock(channels) | |
| def forward(self, x): | |
| r = x | |
| out = F.relu(self.bn1(self.conv1(x)), inplace=True) | |
| out = self.bn2(self.conv2(out)) | |
| out = self.se(out) | |
| return F.relu(out + r, inplace=True) | |
| class ChessPositionEvaluator(nn.Module): | |
| """ | |
| Chess Position Evaluator - V7 HULK (SE-ResNet-20, Dual Head) | |
| Input : (B, 18, 8, 8) | |
| Value : scalar in [-1, 1] (tanh, from White's perspective) | |
| Policy : logits over 20,480 moves (includes underpromotion) | |
| """ | |
| def __init__(self, in_channels=18, num_filters=256, num_res_blocks=20, policy_size=MOVE_SPACE): | |
| super().__init__() | |
| self.input_block = nn.Sequential( | |
| nn.Conv2d(in_channels, num_filters, 3, padding=1, bias=False), | |
| nn.BatchNorm2d(num_filters), | |
| nn.ReLU(inplace=True), | |
| ) | |
| self.tower = nn.Sequential(*[SEResBlock(num_filters) for _ in range(num_res_blocks)]) | |
| self.value_head = nn.Sequential( | |
| nn.Conv2d(num_filters, 32, 1, bias=False), | |
| nn.BatchNorm2d(32), nn.ReLU(inplace=True), | |
| nn.Flatten(), | |
| nn.Linear(32 * 8 * 8, 256), nn.ReLU(inplace=True), | |
| nn.Dropout(0.3), | |
| nn.Linear(256, 1), nn.Tanh(), | |
| ) | |
| self.policy_head = nn.Sequential( | |
| nn.Conv2d(num_filters, 32, 1, bias=False), | |
| nn.BatchNorm2d(32), nn.ReLU(inplace=True), | |
| nn.Flatten(), | |
| nn.Linear(32 * 8 * 8, 1024), nn.ReLU(inplace=True), | |
| nn.Dropout(0.3), | |
| nn.Linear(1024, policy_size), | |
| ) | |
| def forward(self, x): | |
| x = self.input_block(x) | |
| x = self.tower(x) | |
| value = self.value_head(x).squeeze(-1) | |
| policy = self.policy_head(x) | |
| return value, policy | |
| def load_model(device: str = "cpu") -> ChessPositionEvaluator: | |
| weights_path = hf_hub_download(repo_id=REPO_ID, filename="model_weights.pt") | |
| model = ChessPositionEvaluator().to(device) | |
| state_dict = torch.load(weights_path, map_location=device) | |
| model.load_state_dict(state_dict) | |
| model.eval() | |
| return model | |
| def get_best_move(model: ChessPositionEvaluator, fen: str, top_k: int = 5, device: str = "cpu"): | |
| """Returns (value, [(uci_move, probability), ...]) using legal-move masking.""" | |
| board = chess.Board(fen) | |
| t = fen_to_tensor(fen) | |
| inp = torch.tensor(t, dtype=torch.float32).unsqueeze(0).to(device) | |
| value, policy_logits = model(inp) | |
| logits = policy_logits[0].float().cpu() | |
| mask = torch.full((MOVE_SPACE,), float("-inf")) | |
| legal_moves = list(board.legal_moves) | |
| for move in legal_moves: | |
| idx = uci_to_idx(move.uci()) | |
| if 0 <= idx < MOVE_SPACE: | |
| mask[idx] = 0.0 | |
| probs = torch.softmax(logits + mask, dim=0) | |
| top_indices = torch.topk(probs, min(top_k, len(legal_moves))).indices | |
| return value.item(), [(idx_to_uci(i.item()), probs[i].item()) for i in top_indices] | |
| if __name__ == "__main__": | |
| model = load_model() | |
| print(f"Parameters: {sum(p.numel() for p in model.parameters()):,}") | |
| positions = [ | |
| ("Starting position", chess.STARTING_FEN), | |
| ("After 1.e4", "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"), | |
| ] | |
| for name, fen in positions: | |
| val, moves = get_best_move(model, fen, top_k=3) | |
| print(f"\n{name}") | |
| print(f" Value: {val:.4f}") | |
| for uci, prob in moves: | |
| print(f" {uci} ({prob * 100:.1f}%)") | |