bobbydeveloper2014 commited on
Commit
8932d7c
·
verified ·
1 Parent(s): 2b2e6d7

Create train.py

Browse files
Files changed (1) hide show
  1. train.py +555 -0
train.py ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def train():
2
+ """
3
+ Chess AI (AlphaZero-style) — tối ưu cho Google Colab
4
+ =====================================================
5
+ Cách dùng trên Colab:
6
+ !pip install chess tinygrad
7
+ from google.colab import drive; drive.mount('/content/drive')
8
+ !python chess_ai_colab.py
9
+ """
10
+ import os
11
+ import time
12
+
13
+ # ==================== ENV ====================
14
+ os.environ["DEV"] = "CUDA"
15
+
16
+ import chess
17
+ import chess.pgn
18
+ import numpy as np
19
+ import pickle
20
+ from tinygrad import Tensor, nn
21
+ from tinygrad.nn.state import get_state_dict, load_state_dict, get_parameters
22
+ from tinygrad.nn.optim import Adam
23
+
24
+ def log(msg):
25
+ print(msg, flush=True)
26
+
27
+ # =============================================================
28
+ # CONFIG
29
+ # =============================================================
30
+ CFG = {
31
+ "mcts_sims": 16,
32
+ "buffer_maxlen": 52_000,
33
+ "min_buffer": 1,
34
+ "batch_size": 512,
35
+ "lr": 1e-4,
36
+ "train_steps": 30,
37
+ "save_every": 10,
38
+ "pgn_limit": 2000,
39
+ "res_blocks": 6,
40
+ "max_moves": 81,
41
+ "checkpoint_path": "dataset stockfish/latest.pkl",
42
+ "pgn_path": "dataset stockfish/million_games.pgn",
43
+ "log_path": "dataset stockfish/training_log.txt",
44
+ }
45
+ CFG["pgn_chunk"] = 500
46
+ CFG["pgn_offset_file"] = "dataset stockfish/pgn_offset.pkl"
47
+ CFG["channels"] = 256
48
+ TOTAL_STEPS_TARGET = CFG["train_steps"]
49
+ os.makedirs(os.path.dirname(CFG["checkpoint_path"]), exist_ok=True)
50
+
51
+ # =============================================================
52
+ # HUGGING FACE CONFIG
53
+ # =============================================================
54
+ HF_REPO = "bobbydeveloper2014/bybychessengine"
55
+ HF_TOKEN = "yourtoken"
56
+ HF_SYNC_FILES = {
57
+ "latest.pkl": CFG["checkpoint_path"],
58
+ "pgn_offset.pkl": CFG["pgn_offset_file"],
59
+ "training_log.txt": CFG["log_path"],
60
+ }
61
+
62
+ hf_api = None
63
+ try:
64
+ from huggingface_hub import HfApi, create_repo
65
+ hf_api = HfApi(token=HF_TOKEN)
66
+ create_repo(HF_REPO, token=HF_TOKEN, exist_ok=True, repo_type="model")
67
+ log(f" [HF] sẵn sàng sync với {HF_REPO}")
68
+ except Exception as e:
69
+ log(f" [HF] lỗi khởi tạo (bỏ qua): {e}")
70
+
71
+ def push_to_hf(games_played, total_steps):
72
+ if hf_api is None:
73
+ return
74
+ for remote_name, local_path in HF_SYNC_FILES.items():
75
+ if not os.path.exists(local_path):
76
+ continue
77
+ try:
78
+ hf_api.upload_file(
79
+ path_or_fileobj=local_path,
80
+ path_in_repo=remote_name,
81
+ repo_id=HF_REPO,
82
+ repo_type="model",
83
+ commit_message=f"checkpoint @ game={games_played} steps={total_steps}",
84
+ )
85
+ except Exception as e:
86
+ log(f" [HF][ERROR] upload {remote_name}: {e}")
87
+ log(f" [HF] uploaded -> games={games_played}, steps={total_steps:,}")
88
+
89
+ # =============================================================
90
+ # ENCODING
91
+ # =============================================================
92
+ MCTS_BATCH = 16
93
+ ACTION_SIZE = 4672
94
+ PROMOTION_OFFSET = 4096
95
+ PROMOTION_MAP = {chess.QUEEN: 0, chess.ROOK: 1, chess.BISHOP: 2, chess.KNIGHT: 3}
96
+ PIECE_IDX = {chess.PAWN: 0, chess.KNIGHT: 1, chess.BISHOP: 2,
97
+ chess.ROOK: 3, chess.QUEEN: 4, chess.KING: 5}
98
+ ENCODE_CACHE = {}
99
+ EVAL_CACHE = {}
100
+ CACHE_MAX = 100_000
101
+
102
+ def encode_board(board):
103
+ key = board.fen()
104
+ if key in ENCODE_CACHE:
105
+ return ENCODE_CACHE[key]
106
+ x = np.zeros((12, 8, 8), dtype=np.float32)
107
+ for sq, piece in board.piece_map().items():
108
+ r, c = divmod(sq, 8)
109
+ offset = 0 if piece.color == chess.WHITE else 6
110
+ x[PIECE_IDX[piece.piece_type] + offset][r][c] = 1.0
111
+ ENCODE_CACHE[key] = x
112
+ return x
113
+
114
+ def move_to_idx(move):
115
+ if move.promotion is not None:
116
+ return PROMOTION_OFFSET + move.to_square * 4 + PROMOTION_MAP[move.promotion]
117
+ return move.from_square * 64 + move.to_square
118
+
119
+ # =============================================================
120
+ # MODEL
121
+ # =============================================================
122
+ class ResBlock:
123
+ def __init__(self, channels):
124
+ self.c1 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
125
+ self.bn1 = nn.BatchNorm(channels)
126
+ self.c2 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
127
+ self.bn2 = nn.BatchNorm(channels)
128
+ def __call__(self, x):
129
+ r = x
130
+ x = self.bn1(self.c1(x)).relu()
131
+ x = self.bn2(self.c2(x))
132
+ return (x + r).relu()
133
+
134
+ class ChessNet:
135
+ def __init__(self, channels=256, num_blocks=6):
136
+ self.stem_conv = nn.Conv2d(12, channels, 3, padding=1, bias=False)
137
+ self.stem_bn = nn.BatchNorm(channels)
138
+ self.blocks = [ResBlock(channels) for _ in range(num_blocks)]
139
+ self.policy_conv = nn.Conv2d(channels, 32, 1, bias=False)
140
+ self.policy_bn = nn.BatchNorm(32)
141
+ self.policy_fc = nn.Linear(32 * 8 * 8, ACTION_SIZE)
142
+ self.value_conv = nn.Conv2d(channels, 8, 1, bias=False)
143
+ self.value_bn = nn.BatchNorm(8)
144
+ self.value_fc1 = nn.Linear(8 * 8 * 8, 256)
145
+ self.value_fc2 = nn.Linear(256, 1)
146
+ def __call__(self, x):
147
+ x = self.stem_bn(self.stem_conv(x)).relu()
148
+ for b in self.blocks: x = b(x)
149
+ p = self.policy_bn(self.policy_conv(x)).relu()
150
+ p = self.policy_fc(p.flatten(1))
151
+ v = self.value_bn(self.value_conv(x)).relu()
152
+ v = self.value_fc1(v.flatten(1)).relu()
153
+ v = self.value_fc2(v).tanh()
154
+ return p, v
155
+
156
+ model = ChessNet(CFG["channels"], CFG["res_blocks"])
157
+ optimizer = Adam(get_parameters(model), lr=CFG["lr"])
158
+
159
+ # =============================================================
160
+ # CHECKPOINT — weights luôn lưu numpy, tương thích Metal + CUDA
161
+ # =============================================================
162
+ def _tensor_to_numpy(v):
163
+ """Convert bất kỳ kiểu weight nào → numpy float32."""
164
+ if isinstance(v, np.ndarray):
165
+ return v.astype(np.float32)
166
+ if isinstance(v, Tensor):
167
+ try:
168
+ return v.numpy().astype(np.float32)
169
+ except Exception:
170
+ pass
171
+ if hasattr(v, 'numpy'):
172
+ try:
173
+ return np.array(v.numpy(), dtype=np.float32)
174
+ except Exception:
175
+ pass
176
+ if hasattr(v, 'data'):
177
+ try:
178
+ return np.array(v.data, dtype=np.float32)
179
+ except Exception:
180
+ pass
181
+ try:
182
+ return np.array(v, dtype=np.float32)
183
+ except Exception:
184
+ return None
185
+
186
+ def save_checkpoint(games_played):
187
+ # Luôn save numpy để macOS Metal load được
188
+ numpy_weights = {}
189
+ for k, v in get_state_dict(model).items():
190
+ arr = _tensor_to_numpy(v)
191
+ if arr is not None:
192
+ numpy_weights[k] = arr
193
+
194
+ data = {
195
+ "games": games_played,
196
+ "total_steps": total_steps,
197
+ "weights": numpy_weights,
198
+ }
199
+ with open(CFG["checkpoint_path"], "wb") as f:
200
+ pickle.dump(data, f)
201
+ log(f" [CKPT] saved → games={games_played}, steps={total_steps:,}")
202
+
203
+ def load_checkpoint():
204
+ path = CFG["checkpoint_path"]
205
+ if not os.path.exists(path):
206
+ log(" [CKPT] không tìm thấy checkpoint, bắt đầu từ đầu.")
207
+ return 0, 0
208
+
209
+ with open(path, "rb") as f:
210
+ data = pickle.load(f)
211
+
212
+ # Convert tất cả về Tensor để load vào model
213
+ clean = {}
214
+ nan_keys = []
215
+ for k, v in data["weights"].items():
216
+ arr = _tensor_to_numpy(v)
217
+ if arr is not None:
218
+ if not np.isfinite(arr).all():
219
+ nan_keys.append(k)
220
+ clean[k] = Tensor(arr)
221
+ load_state_dict(model, clean)
222
+
223
+ if nan_keys:
224
+ log(f" [CKPT][CẢNH BÁO] checkpoint chứa NaN/Inf ở {len(nan_keys)} tensor: {nan_keys[:5]}{'...' if len(nan_keys) > 5 else ''}")
225
+ log(" [CKPT] -> checkpoint có thể đã hỏng. Kiểm tra lại trước khi train tiếp.")
226
+
227
+ games = data.get("games", 0)
228
+ steps = data.get("total_steps", 0)
229
+ log(f" [CKPT] loaded → games={games}, steps={steps:,}")
230
+ return games, steps
231
+
232
+ # =============================================================
233
+ # REPLAY BUFFER
234
+ # =============================================================
235
+ class ReplayBuffer:
236
+ def __init__(self, maxlen):
237
+ self.maxlen = maxlen
238
+ self.states = np.zeros((maxlen, 12, 8, 8), dtype=np.float32)
239
+ self.policies = np.zeros((maxlen, ACTION_SIZE), dtype=np.float32)
240
+ self.values = np.zeros((maxlen, 1), dtype=np.float32)
241
+ self.ptr = 0
242
+ self.size = 0
243
+
244
+ def extend(self, samples):
245
+ for state, pi, z in samples:
246
+ self.states[self.ptr] = state
247
+ self.policies[self.ptr] = pi
248
+ self.values[self.ptr, 0] = z
249
+ self.ptr = (self.ptr + 1) % self.maxlen
250
+ self.size = min(self.size + 1, self.maxlen)
251
+
252
+ def sample_batch(self, batch_size):
253
+ idx = np.random.randint(0, self.size, batch_size)
254
+ return self.states[idx], self.policies[idx], self.values[idx]
255
+
256
+ def __len__(self):
257
+ return self.size
258
+
259
+ # =============================================================
260
+ # PGN
261
+ # =============================================================
262
+ def load_offset():
263
+ if os.path.exists(CFG["pgn_offset_file"]):
264
+ with open(CFG["pgn_offset_file"], "rb") as f:
265
+ return pickle.load(f)
266
+ return 0
267
+
268
+ def load_pgn_chunk(buf, path, max_games=500):
269
+ if not os.path.exists(path):
270
+ return 0, None
271
+ offset = load_offset()
272
+ count = 0
273
+ with open(path, "r") as f:
274
+ f.seek(offset)
275
+ while count < max_games:
276
+ game = chess.pgn.read_game(f)
277
+ if game is None: break
278
+ result = game.headers.get("Result", "1/2-1/2")
279
+ white_z = 1.0 if result == "1-0" else -1.0 if result == "0-1" else 0.0
280
+ board = game.board()
281
+ samples = []
282
+ for move in game.mainline_moves():
283
+ pi = np.zeros(ACTION_SIZE, dtype=np.float32)
284
+ pi[move_to_idx(move)] = 1.0
285
+ z = white_z if board.turn == chess.WHITE else -white_z
286
+ samples.append((encode_board(board), pi, np.float32(z)))
287
+ board.push(move)
288
+ buf.extend(samples)
289
+ count += 1
290
+ new_offset = f.tell()
291
+ with open(CFG["pgn_offset_file"], "wb") as f:
292
+ pickle.dump(new_offset, f)
293
+ log(f"[PGN] loaded chunk={count}, buffer={len(buf):,}")
294
+ return count, new_offset
295
+
296
+ def load_pgn(buf, path, limit=2000):
297
+ if not os.path.exists(path):
298
+ log(f" [PGN] không tìm thấy {path}, bỏ qua.")
299
+ return
300
+ count = 0
301
+ with open(path) as f:
302
+ while count < limit:
303
+ game = chess.pgn.read_game(f)
304
+ if game is None: break
305
+ result = game.headers.get("Result", "1/2-1/2")
306
+ white_z = 1.0 if result == "1-0" else -1.0 if result == "0-1" else 0.0
307
+ board = game.board()
308
+ samples = []
309
+ for move in game.mainline_moves():
310
+ pi = np.zeros(ACTION_SIZE, dtype=np.float32)
311
+ pi[move_to_idx(move)] = 1.0
312
+ z = white_z if board.turn == chess.WHITE else -white_z
313
+ samples.append((encode_board(board), pi, np.float32(z)))
314
+ board.push(move)
315
+ buf.extend(samples)
316
+ count += 1
317
+ log(f" [PGN] loaded {count} games, buffer={len(buf):,}")
318
+
319
+ # =============================================================
320
+ # INFERENCE
321
+ # =============================================================
322
+ def predict_batch(boards):
323
+ n = len(boards)
324
+ states = np.empty((n, 12, 8, 8), dtype=np.float32)
325
+ for i, b in enumerate(boards):
326
+ states[i] = encode_board(b)
327
+ x = Tensor(states)
328
+ p_logits, values = model(x)
329
+ p_logits = p_logits.numpy().astype(np.float64, copy=False)
330
+ values = values.numpy()
331
+ outputs = []
332
+ for i, board in enumerate(boards):
333
+ p = np.nan_to_num(p_logits[i], nan=0.0, posinf=0.0, neginf=0.0, copy=False)
334
+ mask = np.zeros(ACTION_SIZE, dtype=np.float64)
335
+ mask[[move_to_idx(m) for m in board.legal_moves]] = 1.0
336
+ p -= p.max(); np.exp(p, out=p); p *= mask
337
+ s = p.sum()
338
+ p = p / s if s > 0 else mask / mask.sum()
339
+ outputs.append((p.copy(), float(values[i, 0])))
340
+ return outputs
341
+
342
+ # =============================================================
343
+ # MCTS
344
+ # =============================================================
345
+ class MCTSNode:
346
+ __slots__ = ("board", "prior", "parent", "children", "visits", "total_value")
347
+ def __init__(self, board, prior=0.0, parent=None):
348
+ self.board = board
349
+ self.prior = prior
350
+ self.parent = parent
351
+ self.children = {}
352
+ self.visits = 0
353
+ self.total_value = 0.0
354
+ @property
355
+ def q(self):
356
+ return self.total_value / self.visits if self.visits > 0 else 0.0
357
+ def uct_score(self):
358
+ pv = self.parent.visits if self.parent else 1
359
+ return self.q + 1.5 * self.prior * (pv ** 0.5) / (1 + self.visits)
360
+
361
+ def terminal_value(board):
362
+ out = board.outcome()
363
+ if out is None or out.winner is None: return 0.0
364
+ return -1.0
365
+
366
+ def expand_node(node, policy):
367
+ for move in node.board.legal_moves:
368
+ if move not in node.children:
369
+ cb = node.board.copy(stack=False); cb.push(move)
370
+ node.children[move] = MCTSNode(cb, prior=float(policy[move_to_idx(move)]), parent=node)
371
+
372
+ def backup_path(path, value):
373
+ for n in reversed(path):
374
+ n.visits += 1; n.total_value += value; value = -value
375
+
376
+ def mcts_search(root, num_sims=64):
377
+ remaining = num_sims
378
+ while remaining > 0:
379
+ batch_size = min(MCTS_BATCH, remaining)
380
+ leaves, paths = [], []
381
+ for _ in range(batch_size):
382
+ node = root; path = [node]
383
+ while node.children and not node.board.is_game_over():
384
+ node = max(node.children.values(), key=lambda n: n.uct_score())
385
+ path.append(node)
386
+ leaves.append(node); paths.append(path)
387
+
388
+ boards_to_eval, eval_indices = [], []
389
+ values = [0.0] * batch_size
390
+ policies = [None] * batch_size
391
+ for i, node in enumerate(leaves):
392
+ if node.board.is_game_over(): values[i] = terminal_value(node.board)
393
+ else: boards_to_eval.append(node.board); eval_indices.append(i)
394
+
395
+ if boards_to_eval:
396
+ for idx, (pol, val) in zip(eval_indices, predict_batch(boards_to_eval)):
397
+ policies[idx] = pol; values[idx] = val
398
+
399
+ for i, node in enumerate(leaves):
400
+ if not node.board.is_game_over():
401
+ policy = policies[i]
402
+ if node == root:
403
+ legal = list(node.board.legal_moves)
404
+ if legal:
405
+ noise = np.random.dirichlet([0.3] * len(legal))
406
+ for j, move in enumerate(legal):
407
+ k = move_to_idx(move)
408
+ policy[k] = 0.75 * policy[k] + 0.25 * noise[j]
409
+ expand_node(node, policy)
410
+ backup_path(paths[i], values[i])
411
+ remaining -= batch_size
412
+
413
+ # =============================================================
414
+ # SELF-PLAY
415
+ # =============================================================
416
+ def play_one_game(temperature=1.0, game_id=0):
417
+ board = chess.Board()
418
+ history = []
419
+ move_num = 0
420
+ Tensor.training = False
421
+ try:
422
+ while not board.is_game_over() and move_num < CFG["max_moves"]:
423
+ root = MCTSNode(board.copy(stack=False))
424
+ mcts_search(root, CFG["mcts_sims"])
425
+ moves_list = list(root.children.keys())
426
+ if not moves_list: break
427
+ visits = np.array([root.children[m].visits for m in moves_list], dtype=np.float32)
428
+ s = visits.sum()
429
+ if s <= 0 or np.isnan(s): visits[:] = 1.0
430
+ idxs = np.array([move_to_idx(m) for m in moves_list], dtype=np.int32)
431
+ pi = np.zeros(ACTION_SIZE, dtype=np.float32); pi[idxs] = visits
432
+ s = pi.sum()
433
+ if s > 0 and not np.isnan(s): pi /= s
434
+ history.append((encode_board(board), pi, board.turn))
435
+ if move_num < 30 and temperature > 0:
436
+ probs = visits ** (1.0 / temperature)
437
+ s = probs.sum()
438
+ if s <= 0 or np.isnan(s): probs = np.ones(len(visits), dtype=np.float32)
439
+ probs /= probs.sum()
440
+ move = moves_list[np.random.choice(len(moves_list), p=probs)]
441
+ else:
442
+ move = moves_list[int(np.argmax(visits))]
443
+ board.push(move); move_num += 1
444
+ finally:
445
+ Tensor.training = True
446
+
447
+ result = board.result() if board.is_game_over() else "1/2-1/2"
448
+ white_z = 1.0 if result == "1-0" else -1.0 if result == "0-1" else 0.0
449
+ samples = [(s, p, np.float32(white_z if c == chess.WHITE else -white_z)) for s, p, c in history]
450
+ return samples, result
451
+
452
+ # =============================================================
453
+ # TRAINING STEP
454
+ # =============================================================
455
+ def train_step(buf):
456
+ if len(buf) < CFG["min_buffer"]:
457
+ return None
458
+ loss_policy = loss_value = loss = 0.0
459
+ skipped = 0
460
+ for _ in range(CFG["train_steps"]):
461
+ s, p, z = buf.sample_batch(CFG["batch_size"])
462
+ S = Tensor(np.ascontiguousarray(s))
463
+ P = Tensor(np.ascontiguousarray(p))
464
+ Z = Tensor(np.ascontiguousarray(z))
465
+ logits, v = model(S)
466
+ # clamp logits trước softmax để tránh exp() overflow -> inf
467
+ logits = logits.clip(-30, 30)
468
+ logits = logits - logits.max(axis=1).reshape(-1, 1)
469
+ exp_l = logits.exp()
470
+ prob = exp_l / exp_l.sum(axis=1).reshape(-1, 1)
471
+ loss_policy = -(P * (prob + 1e-8).log()).sum(axis=1).mean()
472
+ loss_value = (v - Z).square().mean()
473
+ loss = loss_policy + loss_value
474
+
475
+ loss_val = float(loss.item())
476
+ if not np.isfinite(loss_val):
477
+ # loss NaN/Inf -> bỏ qua step này, KHÔNG cập nhật weights
478
+ skipped += 1
479
+ log(f" [WARN] loss={loss_val} (NaN/Inf) -> skip optimizer.step()")
480
+ continue
481
+
482
+ optimizer.zero_grad(); loss.backward()
483
+ # gradient clipping: chặn gradient explosion gây NaN lan vào weights
484
+ for t in get_parameters(model):
485
+ if t.grad is not None:
486
+ t.grad = t.grad.clip(-1.0, 1.0)
487
+ optimizer.step()
488
+ return {"loss": float(loss.item()), "loss_policy": float(loss_policy.item()),
489
+ "loss_value": float(loss_value.item()), "steps": CFG["train_steps"]}
490
+
491
+ # =============================================================
492
+ # MAIN
493
+ # =============================================================
494
+ if __name__ == "__main__":
495
+ step_since_pgn = 0
496
+ print("=" * 60)
497
+ print(" Chess AI — AlphaZero-style, tối ưu Colab")
498
+ print("=" * 60)
499
+ buf = ReplayBuffer(CFG["buffer_maxlen"])
500
+ games_played, total_steps = load_checkpoint()
501
+ if games_played == 0:
502
+ load_pgn(buf, CFG["pgn_path"], CFG["pgn_limit"])
503
+ load_pgn_chunk(buf, CFG["pgn_path"], max_games=500)
504
+ print(f"\n Bắt đầu training từ game #{games_played + 1}")
505
+ print(f" Buffer: {len(buf):,} | Min train: {CFG['min_buffer']:,}\n")
506
+
507
+ while True:
508
+ t0 = time.time()
509
+ step_since_pgn += CFG["train_steps"]
510
+ if step_since_pgn >= CFG["pgn_chunk"]:
511
+ load_pgn_chunk(buf, CFG["pgn_path"], CFG["pgn_chunk"]); step_since_pgn = 0
512
+
513
+ try:
514
+ samples, result = play_one_game(temperature=1.0, game_id=games_played + 1)
515
+ except Exception as e:
516
+ import traceback
517
+ ts = time.strftime("%Y-%m-%d %H:%M:%S")
518
+ msg = f"[ERROR] self-play: {e}\n{traceback.format_exc()}"
519
+ log(f"[{ts}] {msg}")
520
+ try:
521
+ with open(CFG["log_path"], "a") as f: f.write(f"[{ts}] {msg}\n")
522
+ except Exception: pass
523
+ continue
524
+
525
+ buf.extend(samples); games_played += 1
526
+ elapsed_game = time.time() - t0
527
+ t1 = time.time()
528
+ info = train_step(buf)
529
+ elapsed_train = time.time() - t1
530
+
531
+ if info:
532
+ total_steps += 1
533
+ log(f" Steps: {total_steps}/{TOTAL_STEPS_TARGET} "
534
+ f"loss={info['loss']:.4f} p={info['loss_policy']:.4f} v={info['loss_value']:.4f} "
535
+ f"t={elapsed_train:.1f}s")
536
+
537
+ loss_str = f"{info['loss']:.4f}" if info else "warmup"
538
+ log(f"🎮 Game {games_played}: result={result} moves={len(samples)} "
539
+ f"buf={len(buf):,} loss={loss_str} t={elapsed_game:.1f}s")
540
+
541
+ ts = time.strftime("%Y-%m-%d %H:%M:%S")
542
+ line = (f"game={games_played:5d} | {result} | moves={len(samples):3d} | buf={len(buf):,} | "
543
+ f"steps={total_steps:,} | "
544
+ + (f"loss={info['loss']:.4f} (p={info['loss_policy']:.4f} v={info['loss_value']:.4f})" if info else "warmup")
545
+ + f" | game={elapsed_game:.1f}s train={elapsed_train:.1f}s")
546
+ try:
547
+ with open(CFG["log_path"], "a") as f: f.write(f"[{ts}] {line}\n")
548
+ except Exception: pass
549
+
550
+ if games_played % CFG["save_every"] == 0:
551
+ save_checkpoint(games_played)
552
+ return push_to_hf(games_played, total_steps)
553
+
554
+
555
+ train()