chess-pgn-1000-1300
A 51M-parameter character-level transformer that plays chess by writing PGN movetext, trained only
on Lichess games where both players were rated 1000-1300. It follows the PGN model of
Transcendence: Generative Models Can Outperform The Experts That Train Them (Zhang et al., NeurIPS 2024):
it reads and writes games one character at a time (;1.e4 e5 2.Nf3 ...) and is never shown a board or the rules.
It is one of a pair trained with the same recipe on different rating bands: chess-pgn-1000-1300 and chess-pgn-2400plus.
Data
- Lichess standard rated games, June 2025 and part of May 2025, newest first, both players rated 1000-1300, initial clock time of at least 180 seconds.
- 10,000,000 training games (3.42B characters, 342 per game on average) and 5,000 held-out games (the newest).
- Draws are kept. Games that are the source of a Lichess puzzle in our evaluation sets are excluded (0 dropped).
- Format: one game per line of text,
;+ SAN movetext + result, e.g.;1.e4 e5 2.Nf3 Nc6 ... 1-0. Vocabulary of 33 characters.
Model
Decoder-only transformer: 16 layers, d=512, 8 heads, ReLU MLPs, learned positions, context 1,024 characters (config.json).
Training
- Next-character cross-entropy on 1,024-character windows that each start at a game's beginning; 128 windows (131,072 characters) per step.
- AdamW (betas 0.9/0.95, weight decay 0.1), learning rate 2e-4 after 2,000 warmup steps, cosine decay to 2e-5 over 200,000 steps, bf16, one NVIDIA B200.
- Held-out loss every 500 steps; early stopping after 16 evals without improvement, counted after the first epoch.
- This model: early stopping at step 138,500 (best held-out loss at step 130,500), about 5.0 passes over the training characters.
Results
Held-out loss: 0.4126 bits per character on the 5,000 held-out games.
Top move (greedy) equal to the move played, on 20,000 held-out positions (10,000 from each band's held-out games):
| 1000-1300 games | 2400+ games | |
|---|---|---|
| this model | 50.0% | 41.4% |
| mean surprise of the played move (nats) | 1.56 | 1.85 |
Illegal moves, on 3,000 held-out positions per band:
| 1000-1300 games | 2400+ games | |
|---|---|---|
| top move (greedy) | 0.33% | 0.30% |
| one sampled move, T = 1 | 1.07% | 1.33% |
Usage
import json, chess, torch
from huggingface_hub import snapshot_download
from safetensors.torch import load_file
import sys
path = snapshot_download("Arisp/chess-pgn-1000-1300")
sys.path.insert(0, path)
from pgn_model import PGNTransformer, generate_moves
cfg = json.load(open(f"{path}/config.json"))["architecture"]
model = PGNTransformer(**cfg)
model.load_state_dict(load_file(f"{path}/model.safetensors"))
model.eval()
board = chess.Board()
board.push_san("e4")
move = generate_moves(model, [board], temperature=0.0, dev="cpu")[0] # None if 5 attempts were illegal
print(board.san(move))
generate_moves writes characters until a space and parses them as SAN. At temperature 0 a retry repeats the
same characters, so for play, sample (temperature > 0) on retries.
Limitations
- Illegal moves. The model only knows the position from the move history, and occasionally writes a move that is not legal.
- Context of 1,024 characters. Past roughly move 80-100 the start of the game no longer fits; the model then no longer knows the position and its move-match accuracy halves. Training never shows characters beyond a game's first 1,024.
- It imitates players of one rating band, blunders included; it is not an engine.