CyanMonkey commited on
Commit
d7289c2
·
verified ·
1 Parent(s): 4f7e449

Upload modeling_chessnet.py

Browse files
Files changed (1) hide show
  1. modeling_chessnet.py +242 -0
modeling_chessnet.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =====================================================================
2
+ # modeling_chessnet.py — "Monkey" chess policy/value network
3
+ # Hugging Face custom-code modeling file.
4
+ #
5
+ # State-dict keys match the Monkey Trainer v5 checkpoints exactly
6
+ # (stem.* / blocks.* / policy_head.* / value_head.*), so trainer
7
+ # weights load with zero key mismatch.
8
+ # =====================================================================
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from dataclasses import dataclass
14
+ from typing import List, Optional, Tuple
15
+
16
+ import numpy as np
17
+ import torch
18
+ import torch.nn as nn
19
+ import torch.nn.functional as F
20
+
21
+ import chess
22
+
23
+ from transformers import PreTrainedModel, PretrainedConfig
24
+ from transformers.utils import ModelOutput
25
+
26
+ # ---------------------------------------------------------------------
27
+ # Constants (MUST stay identical to the trainer)
28
+ # ---------------------------------------------------------------------
29
+ INPUT_PLANES = 19
30
+ BOARD_SIZE = 8
31
+ POLICY_SIZE = 20480 # 64 * 64 * 5
32
+
33
+ PROMO_TO_IDX = {None: 0, chess.KNIGHT: 1, chess.BISHOP: 2, chess.ROOK: 3, chess.QUEEN: 4}
34
+ IDX_TO_PROMO = {v: k for k, v in PROMO_TO_IDX.items()}
35
+
36
+
37
+ # ---------------------------------------------------------------------
38
+ # Config
39
+ # ---------------------------------------------------------------------
40
+ class ChessNetConfig(PretrainedConfig):
41
+ model_type = "chessnet"
42
+
43
+ def __init__(
44
+ self,
45
+ input_planes: int = INPUT_PLANES,
46
+ board_size: int = BOARD_SIZE,
47
+ num_blocks: int = 10,
48
+ num_channels: int = 128,
49
+ policy_channels: int = 32,
50
+ value_channels: int = 8,
51
+ policy_size: int = POLICY_SIZE,
52
+ **kwargs,
53
+ ):
54
+ super().__init__(**kwargs)
55
+ self.input_planes = input_planes
56
+ self.board_size = board_size
57
+ self.num_blocks = num_blocks
58
+ self.num_channels = num_channels
59
+ self.policy_channels = policy_channels
60
+ self.value_channels = value_channels
61
+ self.policy_size = policy_size
62
+
63
+
64
+ # ---------------------------------------------------------------------
65
+ # Board <-> Tensor <-> Move utilities
66
+ # ---------------------------------------------------------------------
67
+ def encode_board(board: chess.Board) -> np.ndarray:
68
+ """Encode a chess.Board into a (19, 8, 8) float32 array."""
69
+ planes = np.zeros((INPUT_PLANES, 64), dtype=np.float32)
70
+ for sq, piece in board.piece_map().items():
71
+ planes[piece.piece_type - 1 + (0 if piece.color == chess.WHITE else 6), sq] = 1.0
72
+ planes[12].fill(1.0 if board.turn == chess.WHITE else 0.0)
73
+ planes[13].fill(1.0 if board.has_kingside_castling_rights(chess.WHITE) else 0.0)
74
+ planes[14].fill(1.0 if board.has_queenside_castling_rights(chess.WHITE) else 0.0)
75
+ planes[15].fill(1.0 if board.has_kingside_castling_rights(chess.BLACK) else 0.0)
76
+ planes[16].fill(1.0 if board.has_queenside_castling_rights(chess.BLACK) else 0.0)
77
+ if board.ep_square is not None:
78
+ planes[17, board.ep_square] = 1.0
79
+ planes[18].fill(min(board.halfmove_clock / 100.0, 1.0))
80
+ return planes.reshape(INPUT_PLANES, BOARD_SIZE, BOARD_SIZE)
81
+
82
+
83
+ def tensor_to_board(tensor) -> chess.Board:
84
+ """Rebuild a chess.Board from a (19, 64) or (19, 8, 8) tensor/array."""
85
+ t = np.asarray(tensor).reshape(INPUT_PLANES, 64)
86
+ board = chess.Board.empty()
87
+ for sq in range(64):
88
+ for p in range(6):
89
+ if t[p, sq] == 1.0:
90
+ board.set_piece_at(sq, chess.Piece(p + 1, chess.WHITE))
91
+ if t[p + 6, sq] == 1.0:
92
+ board.set_piece_at(sq, chess.Piece(p + 1, chess.BLACK))
93
+ board.turn = chess.WHITE if t[12, 0] == 1.0 else chess.BLACK
94
+ rights = 0
95
+ if t[13, 0] == 1.0: rights |= chess.BB_H1
96
+ if t[14, 0] == 1.0: rights |= chess.BB_A1
97
+ if t[15, 0] == 1.0: rights |= chess.BB_H8
98
+ if t[16, 0] == 1.0: rights |= chess.BB_A8
99
+ board.castling_rights = rights
100
+ ep = int(np.argmax(t[17]))
101
+ board.ep_square = ep if t[17, ep] == 1.0 else None
102
+ return board
103
+
104
+
105
+ def get_move_index(move: chess.Move) -> int:
106
+ """(from_square * 64 + to_square) * 5 + promotion_type -> 0..20479"""
107
+ return (move.from_square * 64 + move.to_square) * 5 + PROMO_TO_IDX[move.promotion]
108
+
109
+
110
+ def index_to_move(idx: int) -> chess.Move:
111
+ """Flat index -> chess.Move (legality NOT checked)."""
112
+ from_sq = (idx // 5) // 64
113
+ to_sq = (idx // 5) % 64
114
+ return chess.Move(from_sq, to_sq, promotion=IDX_TO_PROMO[idx % 5])
115
+
116
+
117
+ # ---------------------------------------------------------------------
118
+ # Model
119
+ # ---------------------------------------------------------------------
120
+ @dataclass
121
+ class ChessNetOutput(ModelOutput):
122
+ policy_logits: torch.FloatTensor = None
123
+ value: torch.FloatTensor = None
124
+
125
+
126
+ class ResidualBlock(nn.Module):
127
+ def __init__(self, c: int):
128
+ super().__init__()
129
+ self.conv1 = nn.Conv2d(c, c, 3, padding=1, bias=False)
130
+ self.bn1 = nn.BatchNorm2d(c)
131
+ self.conv2 = nn.Conv2d(c, c, 3, padding=1, bias=False)
132
+ self.bn2 = nn.BatchNorm2d(c)
133
+
134
+ def forward(self, x):
135
+ return F.relu(self.bn2(self.conv2(F.relu(self.bn1(self.conv1(x))))) + x)
136
+
137
+
138
+ class ChessNet(PreTrainedModel):
139
+ config_class = ChessNetConfig
140
+ base_model_prefix = "chessnet"
141
+ supports_gradient_checkpointing = False
142
+ _no_split_modules = ["ResidualBlock"]
143
+
144
+ def __init__(self, config: ChessNetConfig):
145
+ super().__init__(config)
146
+ inc, nc = config.input_planes, config.num_channels
147
+ pc, vc, bs = config.policy_channels, config.value_channels, config.board_size
148
+
149
+ self.stem = nn.Sequential(
150
+ nn.Conv2d(inc, nc, 3, padding=1, bias=False),
151
+ nn.BatchNorm2d(nc),
152
+ nn.ReLU(),
153
+ )
154
+ self.blocks = nn.Sequential(*[ResidualBlock(nc) for _ in range(config.num_blocks)])
155
+ self.policy_head = nn.Sequential(
156
+ nn.Conv2d(nc, pc, 1, bias=False),
157
+ nn.BatchNorm2d(pc),
158
+ nn.ReLU(),
159
+ nn.Flatten(),
160
+ nn.Linear(pc * bs * bs, config.policy_size),
161
+ )
162
+ self.value_head = nn.Sequential(
163
+ nn.Conv2d(nc, vc, 1, bias=False),
164
+ nn.BatchNorm2d(vc),
165
+ nn.ReLU(),
166
+ nn.Flatten(),
167
+ nn.Linear(vc * bs * bs, 64),
168
+ nn.ReLU(),
169
+ nn.Linear(64, 1),
170
+ nn.Tanh(),
171
+ )
172
+ self.post_init()
173
+
174
+ def _init_weights(self, module):
175
+ pass # keep PyTorch default init (identical to trainer)
176
+
177
+ def forward(self, x, **kwargs) -> ChessNetOutput:
178
+ h = self.blocks(self.stem(x))
179
+ return ChessNetOutput(policy_logits=self.policy_head(h), value=self.value_head(h))
180
+
181
+
182
+ # ---------------------------------------------------------------------
183
+ # Player wrapper (inference / arena / Colab matches)
184
+ # ---------------------------------------------------------------------
185
+ class MonkeyPlayer:
186
+ """Convenience wrapper: legal-move masking + argmax/temperature play."""
187
+
188
+ def __init__(self, model: ChessNet, device: Optional[str] = None):
189
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
190
+ self.model = model.to(self.device).eval()
191
+
192
+ @classmethod
193
+ def from_pretrained(cls, repo_or_dir: str, token=None, device=None, **kw):
194
+ try:
195
+ model = ChessNet.from_pretrained(repo_or_dir, token=token, trust_remote_code=True, **kw)
196
+ except Exception:
197
+ # Fallback: manual load (works even if config.json lacks auto_map)
198
+ from huggingface_hub import hf_hub_download
199
+ from safetensors.torch import load_file
200
+ if os.path.isdir(repo_or_dir):
201
+ cfg_path = os.path.join(repo_or_dir, "config.json")
202
+ w_path = os.path.join(repo_or_dir, "model.safetensors")
203
+ else:
204
+ cfg_path = hf_hub_download(repo_or_dir, "config.json", token=token)
205
+ w_path = hf_hub_download(repo_or_dir, "model.safetensors", token=token)
206
+ with open(cfg_path) as f:
207
+ cfg = json.load(f)
208
+ model = ChessNet(ChessNetConfig(**cfg))
209
+ model.load_state_dict(load_file(w_path))
210
+ return cls(model, device=device)
211
+
212
+ @torch.no_grad()
213
+ def choose_move(self, board: chess.Board, temperature: float = 0.0):
214
+ """Returns (move, confidence, value)."""
215
+ legal = list(board.legal_moves)
216
+ if not legal:
217
+ return None, 0.0, 0.0
218
+ x = torch.tensor(encode_board(board), dtype=torch.float32, device=self.device).unsqueeze(0)
219
+ out = self.model(x)
220
+ logits = out.policy_logits[0].float()
221
+ idx = torch.tensor([get_move_index(m) for m in legal], device=logits.device)
222
+ masked = logits[idx]
223
+ if temperature > 0:
224
+ probs = torch.softmax(masked / temperature, dim=-1)
225
+ k = int(torch.multinomial(probs, 1).item())
226
+ else:
227
+ probs = torch.softmax(masked, dim=-1)
228
+ k = int(torch.argmax(probs).item())
229
+ return legal[k], float(probs[k]), float(out.value[0][0])
230
+
231
+ @torch.no_grad()
232
+ def evaluate(self, board: chess.Board) -> float:
233
+ x = torch.tensor(encode_board(board), dtype=torch.float32, device=self.device).unsqueeze(0)
234
+ return float(self.model(x).value[0][0])
235
+
236
+
237
+ if __name__ == "__main__":
238
+ cfg = ChessNetConfig()
239
+ m = ChessNet(cfg)
240
+ b = chess.Board()
241
+ mv, conf, val = MonkeyPlayer(m, device="cpu").choose_move(b)
242
+ print(f"params={sum(p.numel() for p in m.parameters()):,} | smoke move={mv.uci()} | ok")