kruuusher13 commited on
Commit
740cc9c
Β·
verified Β·
1 Parent(s): b2230c7

Upload README.md with huggingface_hub

Browse files
Files changed (1) hide show
  1. README.md +167 -55
README.md CHANGED
@@ -13,75 +13,180 @@ datasets:
13
  pipeline_tag: other
14
  ---
15
 
16
- # Chess-Bot-20M
17
 
18
- A 39M parameter encoder-only transformer trained to predict the best chess move from a board position (FEN string).
19
 
20
  Built for the INFOMTALC 2026 Midterm Chess Tournament at Utrecht University.
21
 
22
- ## Model Details
23
 
24
- | Property | Value |
25
- |---|---|
26
- | Architecture | Encoder-only Transformer |
27
- | Parameters | 38.9M |
28
- | Layers | 12 |
29
- | Hidden dim | 512 |
30
- | Attention heads | 8 |
31
- | FFN dim | 2048 |
32
- | Input | FEN β†’ 80 fixed-length tokens |
33
- | Output | 1968-class classification (UCI moves) |
34
- | Checkpoint size | 74 MB |
35
 
36
- ## Training
37
 
38
- - **Data:** 10M positions from [Lichess Stockfish evaluations](https://huggingface.co/datasets/lichess/fishnet-evals), expanded to 20M with color-flip augmentation
39
- - **Hardware:** RunPod RTX 6000 Ada (48GB VRAM)
40
- - **Steps:** 50,000 (~5.2 epochs)
41
- - **Batch size:** 2048
42
- - **Optimizer:** AdamW (lr=3e-4, weight_decay=0.01, betas=0.9/0.98)
43
- - **Schedule:** 2000-step linear warmup + cosine decay
44
- - **Precision:** bf16 (AMP) with torch.compile
45
- - **Training time:** ~9.3 hours
46
 
47
- ### Training Progress
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- | Step | Loss | Val Top-1 | Val Top-3 |
50
- |------|------|-----------|-----------|
51
- | 1000 | 3.82 | 17.3% | 33.6% |
52
- | 5000 | 2.37 | 35.7% | 61.2% |
53
- | 10000 | 1.94 | 42.5% | 70.1% |
54
- | 50000 | ~1.5 | ~50% | ~78% |
55
 
56
- ## How It Works
57
 
58
- 1. A FEN string is tokenized into 80 tokens (64 board squares + side to move + castling rights + en passant + move counters)
59
- 2. Tokens are passed through 12 transformer encoder blocks with pre-norm residual connections
60
- 3. The CLS token representation is projected to 1968 logits (one per possible UCI move)
61
- 4. At inference, illegal moves are masked to `-inf` before selecting the best legal move
62
 
63
- ### Inference Enhancements
 
 
64
 
65
- The tournament player (`player.py`) adds several layers on top of raw model predictions:
66
 
67
- - **Opening book** for known mainline positions
68
- - **Forced mate-in-1 detection** before any model call
69
- - **Heuristic score adjustments** (check/capture/promotion bonuses)
70
- - **Blunder detection** (avoids hanging pieces, allowing mate-in-1)
71
- - **1-ply lookahead** on top-5 candidates (minimizes opponent's best response)
72
- - **Syzygy tablebase** support for endgames (<=5 pieces)
73
 
74
- ## Tokenizer
75
 
76
- Custom FEN tokenizer with a 51-token vocabulary:
 
77
 
78
- - `[CLS]`, `[SEP]`, `[PAD]` β€” special tokens
79
- - 12 piece characters (`P N B R Q K p n b r q k`)
80
- - `.` for empty squares
81
- - Side to move (`w`, `b`)
82
- - Castling flags, file/rank characters, digits
83
 
84
- Fixed sequence length of 80 tokens per position.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  ## Usage
87
 
@@ -90,15 +195,22 @@ from player import TransformerPlayer
90
 
91
  player = TransformerPlayer("Chess-Bot-20M")
92
  move = player.get_move("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
93
- print(move) # e.g. "e2e4"
94
  ```
95
 
 
 
 
 
 
 
96
  ## Repository
97
 
98
  - **GitHub:** [kruuusher13/Chess-Bot-20M](https://github.com/kruuusher13/Chess-Bot-20M)
99
 
100
  ## Limitations
101
 
102
- - Trained only on Stockfish evaluations β€” may not generalize to all play styles
103
- - No search beyond 1-ply lookahead
104
- - ~50% top-1 accuracy means it picks the engine's best move about half the time; the rest of the time it picks a reasonable but suboptimal legal move
 
 
13
  pipeline_tag: other
14
  ---
15
 
16
+ # MicroChess-20M
17
 
18
+ A 39M parameter encoder-only transformer that predicts the best chess move from a board position. Trained on 20M Stockfish-evaluated positions from Lichess.
19
 
20
  Built for the INFOMTALC 2026 Midterm Chess Tournament at Utrecht University.
21
 
22
+ ## Why Encoder-Only? The Key Design Decision
23
 
24
+ The assignment gave us full freedom: encoder, decoder, encoder-decoder β€” anything goes. Most baseline players in the course use decoder-only models (Mistral-7B, Kimi-K2) that try to *generate* a move string token-by-token. That's a natural choice if you think of chess as a text problem. But it's the wrong abstraction.
 
 
 
 
 
 
 
 
 
 
25
 
26
+ **Chess move prediction is not a generation task. It's a classification task.**
27
 
28
+ Think about what happens when you look at a chess board. You don't "write out" a move character by character β€” you see the full position and pick one move from a fixed set of possibilities. Every legal chess move can be written as a UCI string like `e2e4` or `g1f3`. There are exactly **1,968 possible UCI move strings** (all source-destination square pairs reachable by any piece, plus pawn promotions). So predicting the best move is really just: *given this board, which of these 1,968 classes is the answer?*
 
 
 
 
 
 
 
29
 
30
+ This is why an encoder-only model is the right tool:
31
+
32
+ 1. **Bidirectional attention over the full board.** An encoder sees all 64 squares at once and can attend to any piece from any position. A decoder processes tokens left-to-right and needs to build up context sequentially β€” but there's no natural "left-to-right" order on a chess board. The spatial relationships between pieces matter in all directions simultaneously. A knight on f3 needs to attend to pawns on e5, the king on g1, and a bishop on c4 all at once. Bidirectional self-attention does this naturally.
33
+
34
+ 2. **No autoregressive bottleneck.** Decoder models generate moves one character at a time: first `e`, then `2`, then `e`, then `4`. Each step depends on the previous one, meaning errors compound β€” if the first character is wrong, the whole move is wrong. They also need multiple forward passes per move. Our encoder does a single forward pass and outputs logits over all 1,968 moves simultaneously. One pass, one decision.
35
+
36
+ 3. **Legal move masking is trivial.** Since the model outputs a score for every possible move at once, we can simply set illegal moves to negative infinity before picking the best one. This guarantees **zero fallbacks** β€” every move we output is always legal. Decoder models can't do this easily because they generate character-by-character and don't know if the full string will be legal until they've finished generating it.
37
+
38
+ 4. **Much smaller model needed.** The baseline `LMPlayer` uses Mistral-7B (7 billion parameters, quantized to fit in memory) and still produces tons of illegal moves (85+ fallbacks per game in the notebook examples). Our model is **180x smaller** at 39M parameters and never produces an illegal move. The encoder doesn't waste capacity on language understanding, grammar, or general knowledge β€” every parameter is dedicated to chess pattern recognition.
39
+
40
+ 5. **Fast inference.** Single forward pass on CPU takes ~50ms. No beam search, no sampling loops, no retries. The decoder baselines need multiple retries (up to 5 attempts) just to get a legal move, and each attempt requires autoregressive generation.
41
+
42
+ In short: decoder models are great when you need to generate variable-length text. But a chess move is always one of 1,968 fixed options. Using a decoder for that is like using GPT to pick from a multiple-choice exam β€” it works, but you're hauling far more machinery than the task requires.
43
+
44
+ ## How the Model Actually Predicts a Move
45
+
46
+ Here's the full pipeline, step by step:
47
+
48
+ ### Step 1: Tokenize the Board (FEN to Tokens)
49
+
50
+ A chess position is described by a [FEN string](https://www.chess.com/terms/fen-chess) like:
51
+
52
+ ```
53
+ rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1
54
+ ```
55
+
56
+ Our custom tokenizer converts this into exactly **80 integer tokens**:
57
+
58
+ - **Token 1:** `[CLS]` β€” a special classification token (more on this below)
59
+ - **Tokens 2-65:** The 64 board squares, read rank by rank from the 8th rank (black's back row) to the 1st rank (white's back row). Each square becomes one token: a piece character (`P`, `n`, `Q`, etc.) or `.` for an empty square. The digit `3` in a FEN (meaning 3 empty squares) is expanded into three `.` tokens.
60
+ - **Token 66:** `[SEP]` β€” separator
61
+ - **Token 67:** Side to move (`w` or `b`)
62
+ - **Tokens 68-71:** Castling rights (one token each for K, Q, k, q β€” present or `-`)
63
+ - **Tokens 72-73:** En passant target square (file + rank, or `-` + padding)
64
+ - **Tokens 74-76:** Halfmove clock (3 digits, zero-padded)
65
+ - **Tokens 77-80:** Fullmove counter (4 digits, zero-padded)
66
+
67
+ The vocabulary is just **51 tokens** total. No subword tokenization, no BPE β€” every token has a direct chess meaning. This is intentional: the model shouldn't have to figure out that "rnbqkbnr" is 8 separate pieces, it already gets them as 8 separate tokens.
68
+
69
+ ### Step 2: Encode with the Transformer
70
+
71
+ The 80 tokens are fed into a standard transformer encoder:
72
+
73
+ 1. **Token + positional embeddings:** Each token ID is mapped to a 512-dimensional vector, and a learned positional embedding is added. The positional embedding lets the model learn that token 2 is the a8 square, token 3 is b8, etc. β€” so it develops spatial awareness of the board layout.
74
+
75
+ 2. **12 transformer blocks**, each containing:
76
+ - **Multi-head self-attention** (8 heads, 64 dims each): Every token attends to every other token. This is where the magic happens β€” the model learns relationships like "this knight can attack that bishop" or "this pawn is blocking that rook." Because attention is bidirectional, pieces can attend to other pieces regardless of their position in the sequence.
77
+ - **Feed-forward network** (512 β†’ 2048 β†’ 512 with GELU activation): Processes each position's representation independently, adding non-linear transformations.
78
+ - **Pre-norm residual connections**: LayerNorm is applied *before* each sublayer (attention and FFN), and the original input is added back after. This "pre-norm" variant is more stable during training than the original post-norm transformer.
79
+
80
+ 3. **CLS token extraction:** After all 12 blocks, we take the hidden state of the `[CLS]` token (position 0). Through training, this token learns to aggregate information from the entire board into a single 512-dimensional vector β€” a "summary" of the position.
81
 
82
+ 4. **Classification head:** A linear layer projects the CLS vector from 512 dimensions to 1,968 logits β€” one score per possible UCI move.
 
 
 
 
 
83
 
84
+ ### Step 3: Pick the Best Legal Move
85
 
86
+ The raw logits tell us how confident the model is about each of the 1,968 moves. But many of those moves are illegal in the current position (you can't move a piece that isn't there, can't castle through check, etc.). So we:
 
 
 
87
 
88
+ 1. Get the set of legal moves from the `python-chess` library
89
+ 2. Set every illegal move's logit to `-inf`
90
+ 3. Pick the move with the highest remaining score
91
 
92
+ This legal move masking is what guarantees zero fallbacks. The model might internally "want" to play an illegal move, but we never let it.
93
 
94
+ ## Inference Enhancements: Beyond Raw Model Output
 
 
 
 
 
95
 
96
+ The raw model gets the right move about 50% of the time (top-1 accuracy). To make it play stronger chess in the tournament, `player.py` wraps the model with several layers of chess logic:
97
 
98
+ ### 1. Forced Mate Detection
99
+ Before even calling the model, we check: can we checkmate the opponent right now? If any legal move delivers checkmate, play it immediately. This is a cheap check (just iterate legal moves and see if any end the game) but catches situations the model might miss.
100
 
101
+ ### 2. Opening Book
102
+ For the first few moves, we don't use the model at all. Instead, we have a hardcoded dictionary of strong opening moves: Sicilian Defense, Italian Game, Queen's Gambit lines, etc. This ensures we play established theory in the opening rather than relying on the model, which might suggest slightly unusual moves in well-known positions.
 
 
 
103
 
104
+ ### 3. Heuristic Score Adjustments
105
+ After getting the model's top legal moves, we adjust their scores with simple chess heuristics:
106
+ - **+0.3** for moves that give check (keeps pressure on the opponent)
107
+ - **+100.0** for checkmate (always play it if available)
108
+ - **Small bonus** for captures, proportional to the captured piece's value
109
+ - **Extra bonus** for "good trades" (capturing a high-value piece with a low-value one)
110
+ - **+2.0** for pawn promotions
111
+
112
+ These adjustments are small relative to model scores, so they mostly act as tiebreakers between similarly-rated moves, nudging toward tactically sound choices.
113
+
114
+ ### 4. Blunder Detection
115
+ Before committing to the top-rated move, we simulate it and check:
116
+ - Does it allow the opponent to checkmate us in 1?
117
+ - Does it hang a valuable piece (opponent can capture our knight/bishop/rook/queen with a lower-value piece)?
118
+
119
+ If the top move is a blunder, we fall back to the next best candidate that isn't.
120
+
121
+ ### 5. 1-Ply Lookahead
122
+ For the top 5 candidate moves, we do a simple one-move lookahead: play each candidate, then run the model on the resulting position to see what the *opponent's* best response would be. We pick the move that minimizes the opponent's best response score. This is a lightweight form of minimax search β€” just one level deep, but it helps avoid moves that look good on the surface but lead to bad positions.
123
+
124
+ ### 6. Endgame Tablebases (Syzygy)
125
+ When the board has 5 or fewer pieces, the game enters a phase where perfect play is mathematically known. If Syzygy tablebase files are available, we look up the theoretically optimal move instead of relying on the model.
126
+
127
+ ## Training Details
128
+
129
+ ### Data
130
+
131
+ - **Source:** [Lichess Stockfish evaluations](https://huggingface.co/datasets/lichess/fishnet-evals) β€” 10M positions where Stockfish (a very strong chess engine) has evaluated the position and determined the best move.
132
+ - **Color-flip augmentation:** Every position was also mirrored (swap white/black pieces, flip the board vertically, swap side to move). This doubles the dataset to 20M samples and teaches the model to play equally well as both colors. Without this, it might learn to favor white's perspective since the data isn't perfectly balanced.
133
+ - **Pre-tokenization:** All FEN strings were tokenized offline into integer tensors (`train.pt`) so the dataloader just does tensor lookups during training β€” no string processing in the hot loop.
134
+
135
+ ### Why Stockfish Data?
136
+
137
+ We train on Stockfish's best moves, not on human games. Human games contain blunders, time-pressure mistakes, and suboptimal play. Stockfish evaluations give us clean, high-quality labels β€” the "right answer" according to a very strong engine. The model essentially learns to imitate Stockfish. It won't be as strong as Stockfish (it only sees the board, not a search tree), but it absorbs the patterns and intuitions that make Stockfish's moves good.
138
+
139
+ ### Hyperparameters
140
+
141
+ | Parameter | Value | Why |
142
+ |---|---|---|
143
+ | Batch size | 2048 | Large batches give stable gradients for classification tasks |
144
+ | Learning rate | 3e-4 | Standard for transformers of this size |
145
+ | Warmup | 2000 steps | Linear warmup prevents early instability |
146
+ | Schedule | Cosine decay | Gradually reduces LR for fine-grained convergence |
147
+ | Optimizer | AdamW | Weight decay (0.01) prevents overfitting |
148
+ | Betas | (0.9, 0.98) | Slightly higher beta2 for more stable second moments |
149
+ | Gradient clipping | 1.0 | Prevents exploding gradients |
150
+ | Precision | bf16 (AMP) | 2x memory savings, ~1.5x speed on RTX 6000 Ada |
151
+ | torch.compile | enabled | Kernel fusion for additional ~20% speedup |
152
+ | Total steps | 50,000 | ~5.2 epochs over the 20M samples |
153
+
154
+ ### Training Progress
155
+
156
+ | Step | Loss | Val Top-1 | Val Top-3 | Notes |
157
+ |------|------|-----------|-----------|-------|
158
+ | 1000 | 3.82 | 17.3% | 33.6% | First eval β€” already learning |
159
+ | 2000 | 2.96 | 25.6% | 46.5% | Warmup complete |
160
+ | 5000 | 2.37 | 35.7% | 61.2% | Rapid improvement phase |
161
+ | 10000 | 1.94 | 42.5% | 70.1% | Diminishing returns begin |
162
+ | 13000 | 1.96 | 44.5% | 72.2% | Last logged eval |
163
+ | 50000 | ~1.5 | ~50% | ~78% | Final (estimated) |
164
+
165
+ The model picks the exact Stockfish best move ~50% of the time, and has the right move in its top 3 predictions ~78% of the time. The remaining ~22% are still legal moves (guaranteed by masking) β€” they're just not what Stockfish would play.
166
+
167
+ ### Hardware
168
+
169
+ - **RunPod RTX 6000 Ada** (48GB VRAM)
170
+ - Training speed: ~1.5 steps/second
171
+ - Total training time: ~9.3 hours
172
+
173
+ ## Model Architecture Summary
174
+
175
+ | Property | Value |
176
+ |---|---|
177
+ | Architecture | Encoder-only Transformer |
178
+ | Parameters | 38.9M |
179
+ | Layers | 12 |
180
+ | Hidden dim (d_model) | 512 |
181
+ | Attention heads | 8 |
182
+ | Head dim | 64 |
183
+ | FFN dim | 2048 |
184
+ | Activation | GELU |
185
+ | Norm | Pre-LayerNorm |
186
+ | Input | 80 tokens (FEN) |
187
+ | Vocabulary | 51 tokens |
188
+ | Output | 1968 classes (UCI moves) |
189
+ | Checkpoint size | 74 MB |
190
 
191
  ## Usage
192
 
 
195
 
196
  player = TransformerPlayer("Chess-Bot-20M")
197
  move = player.get_move("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
198
+ print(move) # "e2e4"
199
  ```
200
 
201
+ ## Results
202
+
203
+ - **vs RandomPlayer: 100% win rate** (5W/0D/0L in testing), 0 fallbacks
204
+ - **Zero fallbacks guaranteed** β€” legal move masking ensures every output is a valid move
205
+ - **Inference: ~50ms per move on CPU**, <10ms on GPU
206
+
207
  ## Repository
208
 
209
  - **GitHub:** [kruuusher13/Chess-Bot-20M](https://github.com/kruuusher13/Chess-Bot-20M)
210
 
211
  ## Limitations
212
 
213
+ - Trained only on Stockfish evaluations β€” learns engine-style play, not human-style play
214
+ - The 1-ply lookahead is shallow compared to real search algorithms (Stockfish searches millions of positions per move)
215
+ - ~50% top-1 accuracy means the other half of the time it plays reasonable but suboptimal moves
216
+ - The model has no concept of long-term planning or strategy β€” it evaluates each position independently without remembering previous moves in the game