chorcat commited on
Commit
da8e564
·
verified ·
1 Parent(s): 821e112

Publish encoder-v4

Browse files
README.md CHANGED
@@ -18,34 +18,34 @@ tags:
18
 
19
  A bidirectional transformer written from scratch that reads a chess **position** and says three
20
  things about it: how good it is, whether the move that led to it threw the game away, and how
21
- the game is going to end. This is the `encoder` stage of [Rukh](https://github.com/borja-glez/rukh), a
22
- course that builds a chess language model end to end: 15,054,725 parameters
23
- over 8 layers of width 384,
24
  pretrained with masked move modeling and fine-tuned on Stockfish
25
  labels.
26
 
27
- See it evaluate a live game: [https://rukh.borjaglez.com/?stage=encoder](https://rukh.borjaglez.com/?stage=encoder) · read how it was built:
28
  [https://lab.rukh.borjaglez.com](https://lab.rukh.borjaglez.com)
29
 
30
  ## Results
31
 
32
- Measured with `rukh eval encoder` on 2026-09-19, over
33
  10000 held-out positions (7453 of them with a blunder label).
34
 
35
  | Metric | Value |
36
  |---|---|
37
- | Blunder F1 (tuned, `p >= 0.06631`) | 18.0 % |
38
- | Blunder precision | 11.7 % |
39
- | Blunder recall | 39.6 % |
40
  | Blunder F1 (fixed, `p >= 0.5`) | 0.0 % |
41
- | Blunder ROC AUC | 0.740 |
42
- | Blunder average precision | 0.112 |
43
  | Blunder base rate | 3.7 % |
44
  | Blunder F1, material baseline | 8.9 % |
45
- | Margin over the baseline | +9.2 F1 points |
46
- | Value vs Stockfish cp, Pearson | 0.648 |
47
- | Value vs Stockfish cp, Spearman | 0.520 |
48
- | Result accuracy | 50.0 % |
49
 
50
  ### Acceptance bars
51
 
@@ -55,8 +55,8 @@ flattering or not.
55
 
56
  | Bar | Target | Measured | Verdict |
57
  |---|---|---|---|
58
- | Blunder F1 over the material baseline | at least +5 F1 points | +9.2 F1 points | met |
59
- | Value vs Stockfish cp, Spearman | at least 0.80 | 0.520 | **not met** |
60
 
61
  **The value bar is not met.** The head was fine-tuned for 4 000 steps on labels that cover 9.8 %
62
  of the positions they were crossed with, and the target `tanh(cp / 400)` compresses the scale
@@ -68,7 +68,7 @@ missing is labelled data, not capacity.
68
  A blunder is rare (3.7 % of the labelled rows), so **accuracy is meaningless** here: a model that always answers "no blunder" scores 96.3 % without knowing anything about chess. F1 at an arbitrary threshold is nearly as bad, because an uncalibrated sigmoid can rank the positions well and still put every probability below 0.5: that number measures the operating point, not the representation. This is why the threshold is chosen on a `tune` half and the F1 is reported on a `score` half, and why ROC AUC and average precision, which no threshold can flatter, are reported next to it.
69
 
70
  The labelled rows are cut in two **by game**, never by position: the `tune` half
71
- (3793 rows, 2455 games) chose the threshold `p >= 0.06630505621433258`
72
  by maximising F1 there, and the `score` half (3660 rows, 2368 games) is
73
  where the F1, the precision and the recall in the table are measured. No game is in both halves,
74
  and the threshold never saw the rows it is scored on. The F1 at the fixed threshold is in the
@@ -99,11 +99,11 @@ How to read these numbers:
99
 
100
  - the blunder F1 of the encoder and of the material baseline are measured on the same rows of the held-out 'val' split, which is drawn by game_id, never by position, and those rows are cut in two by game_id again: the 'tune' half chooses the threshold and the 'score' half is what gets reported
101
  - the checkpoint carries no label-count curve: run `rukh train heads --curve` and evaluate one of its checkpoints to fill that table
102
- - the encoder is +9.2 F1 points from the baseline; GOAL.md asks for at least +5
103
  - the value head is correlated against `tanh(cp / 400)`, the bounded score it is trained on, and not against raw `cp`, where a forced mate is worth ±9 99x and a few rows would decide Pearson for the whole set; the GOAL.md bar of 0.80 is read on Spearman
104
  - the baseline counts material (1/3/3/5/9) and mobility and looks one ply ahead at captures: it cannot see a positional sacrifice, and calls Fischer's 17...Be6 (Byrne-Fischer, 1956) a nine-point blunder
105
  - the two blunder detectors do not see the same thing: the baseline is given the predecessor position and the move that was played, while the encoder is given only the resulting position and has to infer that something was thrown away. That is the comparison GOAL.md asks for, but it is not a level playing field
106
- - the blunder threshold 0.06631 was chosen on the 'tune' half (3,793 rows, 2,455 games) by maximising F1 there, and the reported numbers are measured on the 'score' half (3,660 rows, 2,368 games), which no threshold ever saw; the same rows at the fixed threshold 0.5 give an F1 of 0.0000 against 0.1804 tuned
107
  - the material baseline is a hard yes/no rule: it has no threshold, so nothing was tuned on its side and it got no half to tune on. The margin therefore compares a model at its best operating point against a rule at its only one
108
  - a blunder is rare (3.7 % of the labelled rows), so **accuracy is meaningless** here: a model that always answers "no blunder" scores 96.3 % without knowing anything about chess. F1 at an arbitrary threshold is nearly as bad, because an uncalibrated sigmoid can rank the positions well and still put every probability below 0.5: that number measures the operating point, not the representation. This is why the threshold is chosen on a `tune` half and the F1 is reported on a `score` half, and why ROC AUC and average precision, which no threshold can flatter, are reported next to it
109
 
@@ -152,9 +152,9 @@ repository is that measurement, as the exporter wrote it.
152
 
153
  | File | Same blunder decision as PyTorch | Worst `value` drift |
154
  |---|---|---|
155
- | `model.onnx` (fp32) | 100.0 % | 2.57e-06 |
156
- | `model-fp16.onnx` (fp16) | 100.0 % | 0.00249 |
157
- | `model-int8.onnx` (int8) | 100.0 % | 0.135 |
158
 
159
  The bar the project set itself is 99.9 %.
160
  Every precision, int8 included, makes the same call on every position checked: quantizing this
@@ -171,9 +171,9 @@ decoder's number, where it is not true.
171
  | `block` | `200` |
172
  | `ckpt_every` | `1000` |
173
  | `compile` | `False` |
174
- | `curve` | `[0.1, 0.25, 0.5, 1.0]` |
175
  | `device` | `cuda` |
176
- | `encoder_ckpt` | `checkpoints/encoder-mmm-20260919-093554//best.pt` |
177
  | `eval_batches` | `50` |
178
  | `eval_every` | `250` |
179
  | `fraction` | `1.0` |
@@ -194,11 +194,11 @@ decoder's number, where it is not true.
194
  | `min_lr_ratio` | `0.1` |
195
  | `mode` | `last-n` |
196
  | `model` | `None` |
197
- | `num_params` | `15054725` |
198
  | `out_dir` | `checkpoints` |
199
  | `pooling` | `mean` |
200
  | `precision` | `bf16` |
201
- | `run_name` | `encoder-heads-moves` |
202
  | `seed` | `42` |
203
  | `train_labels` | `438093` |
204
  | `trainable_encoder_tensors` | `26` |
@@ -209,9 +209,10 @@ decoder's number, where it is not true.
209
  | `weights.blunder` | `1.0` |
210
  | `weights.result` | `0.5` |
211
  | `weights.value` | `1.0` |
 
212
  | `workers` | `4` |
213
 
214
- MLflow run: `be759117d1f743f996e8418842456314`.
215
 
216
  ```json
217
  {
@@ -221,26 +222,26 @@ MLflow run: `be759117d1f743f996e8418842456314`.
221
  "model_type": "rukh-position-encoder",
222
  "library_name": "rukh",
223
  "rukh_version": "0.0.1",
224
- "stage": "encoder",
225
- "step": 3000,
226
- "params": 15054725,
227
  "tokenizer": "moves",
228
  "vocab_hash": null,
229
  "data_manifest_sha": null,
230
- "git_sha": "cb84867f1c101c80cb1b64a1d1dda8d24d7c44e0",
231
  "heads": [
232
  "value",
233
  "blunder",
234
  "result"
235
  ],
236
  "pooling": "mean",
237
- "pretrained_from": "checkpoints/encoder-mmm-20260919-093554//best.pt",
238
  "input": "moves",
239
  "vocab_size": 2030,
240
  "square_vocab": 47,
241
- "n_layer": 8,
242
- "n_head": 6,
243
- "d_model": 384,
244
  "d_ff": null,
245
  "block": 200,
246
  "dropout": 0.1,
@@ -257,7 +258,7 @@ positions
257
  crossed with the Lichess Stockfish evaluations: `value` and `blunder` come from those
258
  scores, `result` from the game the position was played in.
259
 
260
- The pretraining checkpoint the heads started from: `checkpoints/encoder-mmm-20260919-093554//best.pt`.
261
 
262
 
263
  ## Limitations
 
18
 
19
  A bidirectional transformer written from scratch that reads a chess **position** and says three
20
  things about it: how good it is, whether the move that led to it threw the game away, and how
21
+ the game is going to end. This is the `encoder-v4` stage of [Rukh](https://github.com/borja-glez/rukh), a
22
+ course that builds a chess language model end to end: 38,973,957 parameters
23
+ over 12 layers of width 512,
24
  pretrained with masked move modeling and fine-tuned on Stockfish
25
  labels.
26
 
27
+ See it evaluate a live game: [https://rukh.borjaglez.com](https://rukh.borjaglez.com) · read how it was built:
28
  [https://lab.rukh.borjaglez.com](https://lab.rukh.borjaglez.com)
29
 
30
  ## Results
31
 
32
+ Measured with `rukh eval encoder` on 2026-09-20, over
33
  10000 held-out positions (7453 of them with a blunder label).
34
 
35
  | Metric | Value |
36
  |---|---|
37
+ | Blunder F1 (tuned, `p >= 0.09617`) | 18.6 % |
38
+ | Blunder precision | 13.8 % |
39
+ | Blunder recall | 28.5 % |
40
  | Blunder F1 (fixed, `p >= 0.5`) | 0.0 % |
41
+ | Blunder ROC AUC | 0.761 |
42
+ | Blunder average precision | 0.117 |
43
  | Blunder base rate | 3.7 % |
44
  | Blunder F1, material baseline | 8.9 % |
45
+ | Margin over the baseline | +9.7 F1 points |
46
+ | Value vs Stockfish cp, Pearson | 0.726 |
47
+ | Value vs Stockfish cp, Spearman | 0.666 |
48
+ | Result accuracy | 50.8 % |
49
 
50
  ### Acceptance bars
51
 
 
55
 
56
  | Bar | Target | Measured | Verdict |
57
  |---|---|---|---|
58
+ | Blunder F1 over the material baseline | at least +5 F1 points | +9.7 F1 points | met |
59
+ | Value vs Stockfish cp, Spearman | at least 0.80 | 0.666 | **not met** |
60
 
61
  **The value bar is not met.** The head was fine-tuned for 4 000 steps on labels that cover 9.8 %
62
  of the positions they were crossed with, and the target `tanh(cp / 400)` compresses the scale
 
68
  A blunder is rare (3.7 % of the labelled rows), so **accuracy is meaningless** here: a model that always answers "no blunder" scores 96.3 % without knowing anything about chess. F1 at an arbitrary threshold is nearly as bad, because an uncalibrated sigmoid can rank the positions well and still put every probability below 0.5: that number measures the operating point, not the representation. This is why the threshold is chosen on a `tune` half and the F1 is reported on a `score` half, and why ROC AUC and average precision, which no threshold can flatter, are reported next to it.
69
 
70
  The labelled rows are cut in two **by game**, never by position: the `tune` half
71
+ (3793 rows, 2455 games) chose the threshold `p >= 0.09617140889167786`
72
  by maximising F1 there, and the `score` half (3660 rows, 2368 games) is
73
  where the F1, the precision and the recall in the table are measured. No game is in both halves,
74
  and the threshold never saw the rows it is scored on. The F1 at the fixed threshold is in the
 
99
 
100
  - the blunder F1 of the encoder and of the material baseline are measured on the same rows of the held-out 'val' split, which is drawn by game_id, never by position, and those rows are cut in two by game_id again: the 'tune' half chooses the threshold and the 'score' half is what gets reported
101
  - the checkpoint carries no label-count curve: run `rukh train heads --curve` and evaluate one of its checkpoints to fill that table
102
+ - the encoder is +9.7 F1 points from the baseline; GOAL.md asks for at least +5
103
  - the value head is correlated against `tanh(cp / 400)`, the bounded score it is trained on, and not against raw `cp`, where a forced mate is worth ±9 99x and a few rows would decide Pearson for the whole set; the GOAL.md bar of 0.80 is read on Spearman
104
  - the baseline counts material (1/3/3/5/9) and mobility and looks one ply ahead at captures: it cannot see a positional sacrifice, and calls Fischer's 17...Be6 (Byrne-Fischer, 1956) a nine-point blunder
105
  - the two blunder detectors do not see the same thing: the baseline is given the predecessor position and the move that was played, while the encoder is given only the resulting position and has to infer that something was thrown away. That is the comparison GOAL.md asks for, but it is not a level playing field
106
+ - the blunder threshold 0.09617 was chosen on the 'tune' half (3,793 rows, 2,455 games) by maximising F1 there, and the reported numbers are measured on the 'score' half (3,660 rows, 2,368 games), which no threshold ever saw; the same rows at the fixed threshold 0.5 give an F1 of 0.0000 against 0.1855 tuned
107
  - the material baseline is a hard yes/no rule: it has no threshold, so nothing was tuned on its side and it got no half to tune on. The margin therefore compares a model at its best operating point against a rule at its only one
108
  - a blunder is rare (3.7 % of the labelled rows), so **accuracy is meaningless** here: a model that always answers "no blunder" scores 96.3 % without knowing anything about chess. F1 at an arbitrary threshold is nearly as bad, because an uncalibrated sigmoid can rank the positions well and still put every probability below 0.5: that number measures the operating point, not the representation. This is why the threshold is chosen on a `tune` half and the F1 is reported on a `score` half, and why ROC AUC and average precision, which no threshold can flatter, are reported next to it
109
 
 
152
 
153
  | File | Same blunder decision as PyTorch | Worst `value` drift |
154
  |---|---|---|
155
+ | `model.onnx` (fp32) | 100.0 % | 4.44e-06 |
156
+ | `model-fp16.onnx` (fp16) | 100.0 % | 0.00164 |
157
+ | `model-int8.onnx` (int8) | 100.0 % | 0.117 |
158
 
159
  The bar the project set itself is 99.9 %.
160
  Every precision, int8 included, makes the same call on every position checked: quantizing this
 
171
  | `block` | `200` |
172
  | `ckpt_every` | `1000` |
173
  | `compile` | `False` |
174
+ | `curve` | `[]` |
175
  | `device` | `cuda` |
176
+ | `encoder_ckpt` | `checkpoints/encoder-mmm-v4-20260920-111628/best.pt` |
177
  | `eval_batches` | `50` |
178
  | `eval_every` | `250` |
179
  | `fraction` | `1.0` |
 
194
  | `min_lr_ratio` | `0.1` |
195
  | `mode` | `last-n` |
196
  | `model` | `None` |
197
+ | `num_params` | `38973957` |
198
  | `out_dir` | `checkpoints` |
199
  | `pooling` | `mean` |
200
  | `precision` | `bf16` |
201
+ | `run_name` | `encoder-heads-v4` |
202
  | `seed` | `42` |
203
  | `train_labels` | `438093` |
204
  | `trainable_encoder_tensors` | `26` |
 
209
  | `weights.blunder` | `1.0` |
210
  | `weights.result` | `0.5` |
211
  | `weights.value` | `1.0` |
212
+ | `weights.value_rank` | `1.0` |
213
  | `workers` | `4` |
214
 
215
+ MLflow run: `647cdcf0d6b1411b96190312561aaf62`.
216
 
217
  ```json
218
  {
 
222
  "model_type": "rukh-position-encoder",
223
  "library_name": "rukh",
224
  "rukh_version": "0.0.1",
225
+ "stage": "encoder-v4",
226
+ "step": 4000,
227
+ "params": 38973957,
228
  "tokenizer": "moves",
229
  "vocab_hash": null,
230
  "data_manifest_sha": null,
231
+ "git_sha": "db5d48e5db36a0d24e00cbe21375167b8f9f9c3b",
232
  "heads": [
233
  "value",
234
  "blunder",
235
  "result"
236
  ],
237
  "pooling": "mean",
238
+ "pretrained_from": "checkpoints/encoder-mmm-v4-20260920-111628/best.pt",
239
  "input": "moves",
240
  "vocab_size": 2030,
241
  "square_vocab": 47,
242
+ "n_layer": 12,
243
+ "n_head": 8,
244
+ "d_model": 512,
245
  "d_ff": null,
246
  "block": 200,
247
  "dropout": 0.1,
 
258
  crossed with the Lichess Stockfish evaluations: `value` and `blunder` come from those
259
  scores, `result` from the game the position was played in.
260
 
261
+ The pretraining checkpoint the heads started from: `checkpoints/encoder-mmm-v4-20260920-111628/best.pt`.
262
 
263
 
264
  ## Limitations
config.json CHANGED
@@ -5,26 +5,26 @@
5
  "model_type": "rukh-position-encoder",
6
  "library_name": "rukh",
7
  "rukh_version": "0.0.1",
8
- "stage": "encoder",
9
- "step": 3000,
10
- "params": 15054725,
11
  "tokenizer": "moves",
12
  "vocab_hash": null,
13
  "data_manifest_sha": null,
14
- "git_sha": "cb84867f1c101c80cb1b64a1d1dda8d24d7c44e0",
15
  "heads": [
16
  "value",
17
  "blunder",
18
  "result"
19
  ],
20
  "pooling": "mean",
21
- "pretrained_from": "checkpoints/encoder-mmm-20260919-093554//best.pt",
22
  "input": "moves",
23
  "vocab_size": 2030,
24
  "square_vocab": 47,
25
- "n_layer": 8,
26
- "n_head": 6,
27
- "d_model": 384,
28
  "d_ff": null,
29
  "block": 200,
30
  "dropout": 0.1,
 
5
  "model_type": "rukh-position-encoder",
6
  "library_name": "rukh",
7
  "rukh_version": "0.0.1",
8
+ "stage": "encoder-v4",
9
+ "step": 4000,
10
+ "params": 38973957,
11
  "tokenizer": "moves",
12
  "vocab_hash": null,
13
  "data_manifest_sha": null,
14
+ "git_sha": "db5d48e5db36a0d24e00cbe21375167b8f9f9c3b",
15
  "heads": [
16
  "value",
17
  "blunder",
18
  "result"
19
  ],
20
  "pooling": "mean",
21
+ "pretrained_from": "checkpoints/encoder-mmm-v4-20260920-111628/best.pt",
22
  "input": "moves",
23
  "vocab_size": 2030,
24
  "square_vocab": 47,
25
+ "n_layer": 12,
26
+ "n_head": 8,
27
+ "d_model": 512,
28
  "d_ff": null,
29
  "block": 200,
30
  "dropout": 0.1,
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:3688e89c78f05dc44cc1fcf927cfbc16ef66fef6eb66a5cf454c0d7d1f50b54d
3
- size 60229196
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:00839193bf6a0beccf618331d4946548d1061aa79d4f4861975de2753eddce85
3
+ size 155911036
onnx/model-fp16.onnx CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:d837ddb45aa9eeaf09fc017ceb02247ba336b693e8ed26496e0fffab1704a45d
3
- size 30648768
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c418d19953f58d9c47806723f137f86a914ba3e853e5c6819f3b0845212ce26e
3
+ size 78745459
onnx/model-int8.onnx CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:96f9d9e8b65207952eedd72a6b80b25589514bc6e8602864d748dae1a38be130
3
- size 18254174
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d19e8df829d5c6ed94f81d02ba27e526444791004999a81210312820040d10bd
3
+ size 43393427
onnx/model.onnx CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:c7ae867d7e02c1bc368a59aaacd100e66a110ad14258310f01db33ae6185d79e
3
- size 60735659
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5b83eb089a88a2ccee2eb276d6e0b2b05d42a0bbebb12438ac3bf6b2c0635621
3
+ size 156660338
onnx/parity.json CHANGED
@@ -11,20 +11,20 @@
11
  "fp32": {
12
  "file": "model.onnx",
13
  "agreement": 1.0,
14
- "max_abs_value_delta": 2.566725015640259e-06,
15
- "max_abs_blunder_delta": 2.8312206268310547e-07
16
  },
17
  "fp16": {
18
  "file": "model-fp16.onnx",
19
  "agreement": 1.0,
20
- "max_abs_value_delta": 0.002493828535079956,
21
- "max_abs_blunder_delta": 0.00048488378524780273
22
  },
23
  "int8": {
24
  "file": "model-int8.onnx",
25
  "agreement": 1.0,
26
- "max_abs_value_delta": 0.13458502292633057,
27
- "max_abs_blunder_delta": 0.0055537596344947815
28
  }
29
  }
30
  }
 
11
  "fp32": {
12
  "file": "model.onnx",
13
  "agreement": 1.0,
14
+ "max_abs_value_delta": 4.4405460357666016e-06,
15
+ "max_abs_blunder_delta": 2.2351741790771484e-07
16
  },
17
  "fp16": {
18
  "file": "model-fp16.onnx",
19
  "agreement": 1.0,
20
+ "max_abs_value_delta": 0.0016387104988098145,
21
+ "max_abs_blunder_delta": 0.00014109909534454346
22
  },
23
  "int8": {
24
  "file": "model-int8.onnx",
25
  "agreement": 1.0,
26
+ "max_abs_value_delta": 0.11746431887149811,
27
+ "max_abs_blunder_delta": 0.022291138768196106
28
  }
29
  }
30
  }