Bestemshe ResMLP β value and policy network distilled from an exact oracle
Authors: Ansar Zeinulla & Murat Manassov Β· Nazarbayev University Oracle (tablebase): ansarzeinulla/bestemshe-tablebase (8.3 GiB, 1.26Γ10ΒΉΒΉ positions) Code: github.com/ansarzeinulla/Bestemshe
A dual-headed residual MLP trained by supervised distillation on positions labelled by the exact Bestemshe tablebase. It predicts, for the side to move:
- Value: loss / draw / win.
- Policy: logits over the 5 own pits; a move is a target if it preserves the game-theoretic value.
Files
| File | Content | Size |
|---|---|---|
model.safetensors + config.json |
weights only (recommended) | 69 MB |
latest.pt |
full training checkpoint: weights, optimizer state, args, step | 207 MB |
Both hold the same weights: outputs are identical.
Architecture
- Input: 10 pits (0β50 stones) and 2 kazans (stored as kazan/2, 0β12) through categorical embeddings of width 32.
- Trunk: linear 384 β 1024, then 8 residual blocks (LayerNorm β Linear β GELU β Linear), width 1024.
- Heads: value
1024 β 3, policy1024 β 5. - Parameters: 17,214,472.
Training
- 394,852 optimizer steps at batch 32,768, i.e. 12.9 billion training samples.
- Samples came from shards of oracle-labelled positions, regenerated throughout training (continual distillation).
- Loss: cross-entropy on the value head, binary cross-entropy on the policy head.
Results
Against the oracle (2,000 games)
- 1,000 symmetric start positions (equal kazans, equal stones), each played twice with colours swapped.
- The model moves by a 3-ply minimax search over its value head; the oracle plays a value-preserving move.
- Games are stopped at 400 plies and scored as draws. The oracle stores win/draw/loss only, so neither side is pushed to convert a win.
| Metric | Value |
|---|---|
| Value-preserving moves | 99.69% (226,232 of 226,933; 701 value-losing moves) |
| Games ending below the theoretical result | 556 of 2,000 (27.8%) = 556 of the 1,036 games the model started won or drawn (53.7%) |
| Average model moves per game | 113 |
| Random-move baseline (other starts, 100 games) | 97.48% value-preserving moves |
Deeper search helps:
- 5-ply minimax: 21 of 51 degradable games below theory;
- 5-ply with an averaged backup: 20 of 206.
A same-start comparison of all move-selection methods is being run with the updated evaluation/vs_god.py.
Value head (4,000 symmetric positions)
| Metric | Value |
|---|---|
| Accuracy (argmax WDL) | 85.7% |
| Expected calibration error (15 bins) | 0.0098 |
| Mean confidence when wrong / right | 0.673 / 0.891 |
| Draw recall | 0 (146 true draws; draw is never the argmax) |
A telling example: on the initial position the value head gives the first player win 0.89 / draw 0.05 / loss 0.06. The tablebase says it is a loss (the second player wins).
Errors concentrate where a position's latent neighbours carry other labels: 0% error in the most homogeneous 30% of positions, 80% in the most mixed decile. Script: analysis/latent_calibration.py.
During training
A cheap greedy 1-ply check on uniformly sampled positions, run at each checkpoint, rose to 97.0% value-preserving moves and 92.9% value accuracy at the final step. Full history: the eval_results_*.json files of the training run.
Usage
pip install torch safetensors huggingface_hub
import json, torch, torch.nn as nn, torch.nn.functional as F
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file
class ResBlock(nn.Module):
def __init__(self, w):
super().__init__()
self.norm, self.fc1, self.fc2 = nn.LayerNorm(w), nn.Linear(w, w), nn.Linear(w, w)
def forward(self, x):
return x + self.fc2(F.gelu(self.fc1(self.norm(x))))
class BestemsheNet(nn.Module):
def __init__(self, width=1024, blocks=8, emb=32):
super().__init__()
self.pit_emb, self.kaz_emb = nn.Embedding(51, emb), nn.Embedding(13, emb)
self.inp = nn.Linear(12 * emb, width)
self.body = nn.Sequential(*[ResBlock(width) for _ in range(blocks)])
self.value_head, self.policy_head = nn.Linear(width, 3), nn.Linear(width, 5)
def forward(self, pits, kaz):
x = torch.cat([self.pit_emb(pits).flatten(1), self.kaz_emb(kaz).flatten(1)], dim=1)
h = self.body(F.gelu(self.inp(x)))
return self.value_head(h), self.policy_head(h)
repo = "ansarzeinulla/bestemshe-resmlp"
cfg = json.load(open(hf_hub_download(repo, "config.json")))
model = BestemsheNet(cfg["width"], cfg["blocks"]).eval()
model.load_state_dict(load_file(hf_hub_download(repo, "model.safetensors")))
# side to move: pits[0:5] own, pits[5:10] opponent; kazans are even, passed as kazan // 2
pits = torch.tensor([[5] * 10]); kaz = torch.tensor([[0, 0]])
with torch.no_grad():
value, policy = model(pits, kaz)
print(dict(zip(["loss", "draw", "win"], torch.softmax(value, 1)[0].tolist())))
Citation
@misc{zeinulla2026bestemshe_model,
title = {Bestemshe ResMLP: Value and Policy Distillation from an Exact Oracle},
author = {Zeinulla, Ansar and Manassov, Murat},
year = {2026},
publisher = {Hugging Face},
howpublished = {\url{https://huggingface.co/ansarzeinulla/bestemshe-resmlp}}
}
- Downloads last month
- 6
Dataset used to train ansarzeinulla/bestemshe-resmlp
Space using ansarzeinulla/bestemshe-resmlp 1
Evaluation results
- Value-preserving move rate on Bestemshe tablebase (1,000 symmetric start positions)self-reported99.690