CyanMonkey commited on
Commit
8d30cae
·
verified ·
1 Parent(s): 032ba88

Create modeling_chessnet.py

Browse files
Files changed (1) hide show
  1. modeling_chessnet.py +270 -0
modeling_chessnet.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Blackwell Knight - ChessNet Modeling File
3
+ Custom chess neural network for the TobiasLogic Chess Arena.
4
+
5
+ Architecture: ResNet backbone with policy + value heads.
6
+ Board input: 19 planes of 8x8.
7
+ Move output: Flat index over 20,480 possible moves.
8
+ """
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.nn.functional as F
13
+ import numpy as np
14
+ import chess
15
+
16
+ # =====================================================================
17
+ # BOARD ENCODING
18
+ # =====================================================================
19
+
20
+ def encode_board(board):
21
+ """
22
+ Encode a chess.Board into a (19, 8, 8) float32 tensor.
23
+
24
+ Planes:
25
+ 0-5: White pieces (Pawn, Knight, Bishop, Rook, Queen, King)
26
+ 6-11: Black pieces (Pawn, Knight, Bishop, Rook, Queen, King)
27
+ 12: Side to move (1.0 = white, 0.0 = black)
28
+ 13-16: Castling rights (WK, WQ, BK, BQ)
29
+ 17: En passant target square
30
+ 18: Halfmove clock (normalized by /100)
31
+ """
32
+ planes = np.zeros((19, 64), dtype=np.float32)
33
+
34
+ for square, piece in board.piece_map().items():
35
+ idx = piece.piece_type - 1 + (0 if piece.color == chess.WHITE else 6)
36
+ planes[idx, square] = 1.0
37
+
38
+ planes[12].fill(1.0 if board.turn == chess.WHITE else 0.0)
39
+ planes[13].fill(1.0 if board.has_kingside_castling_rights(chess.WHITE) else 0.0)
40
+ planes[14].fill(1.0 if board.has_queenside_castling_rights(chess.WHITE) else 0.0)
41
+ planes[15].fill(1.0 if board.has_kingside_castling_rights(chess.BLACK) else 0.0)
42
+ planes[16].fill(1.0 if board.has_queenside_castling_rights(chess.BLACK) else 0.0)
43
+
44
+ if board.ep_square is not None:
45
+ planes[17, board.ep_square] = 1.0
46
+
47
+ planes[18].fill(min(board.halfmove_clock / 100.0, 1.0))
48
+
49
+ return planes.reshape(19, 8, 8)
50
+
51
+
52
+ # =====================================================================
53
+ # MOVE ENCODING / DECODING
54
+ # =====================================================================
55
+
56
+ POLICY_SIZE = 20480 # 64 * 64 * 5
57
+
58
+ def get_move_index(move):
59
+ """Convert a chess.Move to a flat index (0-20479)."""
60
+ promo_idx = 0
61
+ if move.promotion:
62
+ promo_idx = {
63
+ chess.KNIGHT: 1,
64
+ chess.BISHOP: 2,
65
+ chess.ROOK: 3,
66
+ chess.QUEEN: 4,
67
+ }.get(move.promotion, 0)
68
+ return (move.from_square * 64 + move.to_square) * 5 + promo_idx
69
+
70
+
71
+ def index_to_move(idx, board):
72
+ """
73
+ Convert a flat index back to a chess.Move.
74
+ If the decoded move is illegal, returns None.
75
+ """
76
+ from_sq = (idx // 5) // 64
77
+ to_sq = (idx // 5) % 64
78
+ promo_idx = idx % 5
79
+
80
+ promo = None
81
+ if promo_idx == 1:
82
+ promo = chess.KNIGHT
83
+ elif promo_idx == 2:
84
+ promo = chess.BISHOP
85
+ elif promo_idx == 3:
86
+ promo = chess.ROOK
87
+ elif promo_idx == 4:
88
+ promo = chess.QUEEN
89
+
90
+ move = chess.Move(from_sq, to_sq, promotion=promo)
91
+
92
+ # Safety: check if the move is actually legal
93
+ if move in board.legal_moves:
94
+ return move
95
+ return None
96
+
97
+
98
+ # =====================================================================
99
+ # MODEL ARCHITECTURE
100
+ # =====================================================================
101
+
102
+ class ResidualBlock(nn.Module):
103
+ def __init__(self, channels):
104
+ super().__init__()
105
+ self.conv1 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
106
+ self.bn1 = nn.BatchNorm2d(channels)
107
+ self.conv2 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
108
+ self.bn2 = nn.BatchNorm2d(channels)
109
+
110
+ def forward(self, x):
111
+ identity = x
112
+ out = F.relu(self.bn1(self.conv1(x)))
113
+ out = self.bn2(self.conv2(out))
114
+ out += identity
115
+ return F.relu(out)
116
+
117
+
118
+ class ChessNet(nn.Module):
119
+ """
120
+ Blackwell Knight ChessNet.
121
+
122
+ ~18.5M parameters.
123
+ Input: (batch, 19, 8, 8) board planes.
124
+ Output: policy_logits (batch, 20480), value (batch, 1) in [-1, 1].
125
+ """
126
+
127
+ def __init__(self, config=None):
128
+ super().__init__()
129
+
130
+ if config is None:
131
+ config = {}
132
+
133
+ in_planes = config.get("input_planes", 19)
134
+ num_blocks = config.get("num_blocks", 10)
135
+ num_channels = config.get("num_channels", 128)
136
+ policy_channels = config.get("policy_channels", 32)
137
+ value_channels = config.get("value_channels", 8)
138
+ policy_size = config.get("policy_size", POLICY_SIZE)
139
+
140
+ self.stem = nn.Sequential(
141
+ nn.Conv2d(in_planes, num_channels, 3, padding=1, bias=False),
142
+ nn.BatchNorm2d(num_channels),
143
+ nn.ReLU(),
144
+ )
145
+
146
+ self.blocks = nn.Sequential(
147
+ *[ResidualBlock(num_channels) for _ in range(num_blocks)]
148
+ )
149
+
150
+ self.policy_head = nn.Sequential(
151
+ nn.Conv2d(num_channels, policy_channels, 1, bias=False),
152
+ nn.BatchNorm2d(policy_channels),
153
+ nn.ReLU(),
154
+ nn.Flatten(),
155
+ nn.Linear(policy_channels * 8 * 8, policy_size),
156
+ )
157
+
158
+ self.value_head = nn.Sequential(
159
+ nn.Conv2d(num_channels, value_channels, 1, bias=False),
160
+ nn.BatchNorm2d(value_channels),
161
+ nn.ReLU(),
162
+ nn.Flatten(),
163
+ nn.Linear(value_channels * 8 * 8, 64),
164
+ nn.ReLU(),
165
+ nn.Linear(64, 1),
166
+ nn.Tanh(),
167
+ )
168
+
169
+ def forward(self, x):
170
+ x = self.stem(x)
171
+ x = self.blocks(x)
172
+ policy_logits = self.policy_head(x)
173
+ value = self.value_head(x)
174
+ return policy_logits, value
175
+
176
+
177
+ # =====================================================================
178
+ # INFERENCE / PLAY CHESS
179
+ # =====================================================================
180
+
181
+ class BlackwellKnightPlayer:
182
+ """
183
+ Arena-compatible player wrapper.
184
+
185
+ Usage:
186
+ player = BlackwellKnightPlayer("akshit/blackwell-knight-v1")
187
+ move = player.choose_move(board)
188
+ """
189
+
190
+ def __init__(self, model_repo, device=None, temperature=0.0):
191
+ from huggingface_hub import hf_hub_download
192
+ from safetensors.torch import load_file
193
+ import json
194
+
195
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
196
+ self.temperature = temperature
197
+
198
+ # Load config
199
+ config_path = hf_hub_download(model_repo, "config.json")
200
+ with open(config_path, "r") as f:
201
+ self.config = json.load(f)
202
+
203
+ # Build model
204
+ arch = self.config.get("model_architecture", {})
205
+ self.model = ChessNet({
206
+ "input_planes": self.config.get("board_encoding", {}).get("input_planes", 19),
207
+ "num_blocks": arch.get("num_blocks", 10),
208
+ "num_channels": arch.get("num_channels", 128),
209
+ "policy_channels": arch.get("policy_channels", 32),
210
+ "value_channels": arch.get("value_channels", 8),
211
+ "policy_size": self.config.get("move_encoding", {}).get("policy_size", POLICY_SIZE),
212
+ })
213
+
214
+ # Load weights
215
+ weights_path = hf_hub_download(model_repo, "model.safetensors")
216
+ state_dict = load_file(weights_path)
217
+ self.model.load_state_dict(state_dict)
218
+ self.model.to(self.device)
219
+ self.model.eval()
220
+
221
+ def choose_move(self, board):
222
+ """
223
+ Given a chess.Board, return the best legal move.
224
+ """
225
+ x = encode_board(board)
226
+ x_tensor = torch.tensor(x, dtype=torch.float32).unsqueeze(0).to(self.device)
227
+
228
+ with torch.no_grad():
229
+ policy_logits, value = self.model(x_tensor)
230
+
231
+ # Get legal moves
232
+ legal_moves = list(board.legal_moves)
233
+ if not legal_moves:
234
+ return None
235
+
236
+ legal_indices = [get_move_index(m) for m in legal_moves]
237
+
238
+ # Mask illegal moves
239
+ mask = torch.full((POLICY_SIZE,), float("-inf"), device=self.device)
240
+ mask[legal_indices] = 0.0
241
+
242
+ logits = policy_logits[0] + mask
243
+
244
+ if self.temperature > 0:
245
+ probs = torch.softmax(logits / self.temperature, dim=-1)
246
+ idx = torch.multinomial(probs, 1).item()
247
+ else:
248
+ idx = torch.argmax(logits).item()
249
+
250
+ move = index_to_move(idx, board)
251
+
252
+ # Fallback: if something went wrong, pick a random legal move
253
+ if move is None or move not in legal_moves:
254
+ import random
255
+ move = random.choice(legal_moves)
256
+
257
+ return move
258
+
259
+ def evaluate(self, board):
260
+ """
261
+ Return the value estimate for the current position.
262
+ Positive = good for side to move.
263
+ """
264
+ x = encode_board(board)
265
+ x_tensor = torch.tensor(x, dtype=torch.float32).unsqueeze(0).to(self.device)
266
+
267
+ with torch.no_grad():
268
+ _, value = self.model(x_tensor)
269
+
270
+ return value.item()