Recurrent masked diffusion: results, code, card
Browse files- README.md +18 -6
- results/inj-deep32L-s1.json +143 -0
- results/inj-noinject-s0.json +144 -0
- results/inj-noinject-s1.json +144 -0
- slurm/genhard2.sh +61 -0
- slurm/inj2.sh +1 -1
- slurm/inj3.sh +41 -0
- slurm/plots.sh +16 -0
- src/model.py +8 -2
- src/plot_sudoku.py +148 -0
- src/plot_sudoku_grids.py +184 -0
- src/train_sudoku.py +6 -1
README.md
CHANGED
|
@@ -35,6 +35,7 @@ digit relabelling. Metric: board accuracy, all 81 cells correct.
|
|
| 35 |
| greedy MRV | – | – | 5.90% |
|
| 36 |
| autoregressive prefix-LM | 2.4M | – | 0.30% |
|
| 37 |
| direct recurrent classifier* | 212k | 10.5% | 31.00% |
|
|
|
|
| 38 |
| feed-forward diffusion, MDLM | 37.9M | 4.40% | 88.15% |
|
| 39 |
| feed-forward diffusion, schedule-matched | 37.9M | 40.10% | 88.90% |
|
| 40 |
| 32 distinct layers, no weight sharing | 6.34M | 70.20% | 94.50% |
|
|
@@ -49,12 +50,23 @@ evidence against their method**, and it is not used as a comparison.
|
|
| 49 |
|
| 50 |
## Ablations
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
| 55 |
-
|
|
| 56 |
-
|
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
## Recurrence extrapolates past its training depth
|
| 60 |
|
|
|
|
| 35 |
| greedy MRV | – | – | 5.90% |
|
| 36 |
| autoregressive prefix-LM | 2.4M | – | 0.30% |
|
| 37 |
| direct recurrent classifier* | 212k | 10.5% | 31.00% |
|
| 38 |
+
| recurrent, no input injection | 212k | – | 94.10% |
|
| 39 |
| feed-forward diffusion, MDLM | 37.9M | 4.40% | 88.15% |
|
| 40 |
| feed-forward diffusion, schedule-matched | 37.9M | 40.10% | 88.90% |
|
| 41 |
| 32 distinct layers, no weight sharing | 6.34M | 70.20% | 94.50% |
|
|
|
|
| 50 |
|
| 51 |
## Ablations
|
| 52 |
|
| 53 |
+
Built as a ladder, each step changing exactly one thing:
|
| 54 |
+
|
| 55 |
+
| step | board | gain |
|
| 56 |
+
|---|---|---|
|
| 57 |
+
| feed-forward, 12 layers, 37.9M | 88.90% | – |
|
| 58 |
+
| feed-forward, **32 layers**, 6.34M | 88.95% | **+0.05** |
|
| 59 |
+
| + weight sharing (injection **off**), 212k | 94.10% | **+5.15** |
|
| 60 |
+
| + input injection | **99.70%** | **+5.60** |
|
| 61 |
+
|
| 62 |
+
**Depth alone is worth nothing** — 12→32 layers gains 0.05 points. Sharing one
|
| 63 |
+
block across 32 applications gains 5.15 at 30× fewer parameters; re-supplying the
|
| 64 |
+
input each application gains a further 5.60. The weight-sharing comparison holds
|
| 65 |
+
injection **off on both sides**, so the effects are separated rather than
|
| 66 |
+
confounded.
|
| 67 |
+
|
| 68 |
+
Removing either from the full method: deep supervision **−1.95**, schedule
|
| 69 |
+
matching **−3.15**.
|
| 70 |
|
| 71 |
## Recurrence extrapolates past its training depth
|
| 72 |
|
results/inj-deep32L-s1.json
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"args": {
|
| 3 |
+
"mode": "full",
|
| 4 |
+
"all_cells": false,
|
| 5 |
+
"objective": "diffusion",
|
| 6 |
+
"K": 8,
|
| 7 |
+
"lam": 1.0,
|
| 8 |
+
"pe": "ape",
|
| 9 |
+
"recurrent": false,
|
| 10 |
+
"R": 32,
|
| 11 |
+
"no_deep_sup": false,
|
| 12 |
+
"R_infer": 0,
|
| 13 |
+
"root": "data/sudoku",
|
| 14 |
+
"hard": "/scratch/zt1/project/msml612/user/govind02/pcmd/data/sudoku_hard",
|
| 15 |
+
"augment": true,
|
| 16 |
+
"d": 128,
|
| 17 |
+
"layers": 32,
|
| 18 |
+
"heads": 4,
|
| 19 |
+
"bs": 64,
|
| 20 |
+
"steps": 110000,
|
| 21 |
+
"lr": 0.0003,
|
| 22 |
+
"warmup": 3000,
|
| 23 |
+
"seed": 1,
|
| 24 |
+
"eval_bs": 200,
|
| 25 |
+
"ckpt_every": 8000,
|
| 26 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/inj-deep32L-s1"
|
| 27 |
+
},
|
| 28 |
+
"decode": {
|
| 29 |
+
"fixedK_1": {
|
| 30 |
+
"board": 0.062,
|
| 31 |
+
"cell": 0.7915740446702657,
|
| 32 |
+
"valid_sudoku": 0.062,
|
| 33 |
+
"clues_kept": 1.0,
|
| 34 |
+
"nfe": 1.0
|
| 35 |
+
},
|
| 36 |
+
"fixedK_2": {
|
| 37 |
+
"board": 0.436,
|
| 38 |
+
"cell": 0.858685296919657,
|
| 39 |
+
"valid_sudoku": 0.436,
|
| 40 |
+
"clues_kept": 1.0,
|
| 41 |
+
"nfe": 2.0
|
| 42 |
+
},
|
| 43 |
+
"fixedK_4": {
|
| 44 |
+
"board": 0.63,
|
| 45 |
+
"cell": 0.8979923079637274,
|
| 46 |
+
"valid_sudoku": 0.63,
|
| 47 |
+
"clues_kept": 1.0,
|
| 48 |
+
"nfe": 4.0
|
| 49 |
+
},
|
| 50 |
+
"fixedK_8": {
|
| 51 |
+
"board": 0.735,
|
| 52 |
+
"cell": 0.9136762993542924,
|
| 53 |
+
"valid_sudoku": 0.735,
|
| 54 |
+
"clues_kept": 1.0,
|
| 55 |
+
"nfe": 8.0
|
| 56 |
+
},
|
| 57 |
+
"fixedK_16": {
|
| 58 |
+
"board": 0.77,
|
| 59 |
+
"cell": 0.9197452454041848,
|
| 60 |
+
"valid_sudoku": 0.77,
|
| 61 |
+
"clues_kept": 1.0,
|
| 62 |
+
"nfe": 16.0
|
| 63 |
+
},
|
| 64 |
+
"fixedK_32": {
|
| 65 |
+
"board": 0.797,
|
| 66 |
+
"cell": 0.9209802053561977,
|
| 67 |
+
"valid_sudoku": 0.797,
|
| 68 |
+
"clues_kept": 1.0,
|
| 69 |
+
"nfe": 32.0
|
| 70 |
+
},
|
| 71 |
+
"fixedK_61": {
|
| 72 |
+
"board": 0.817,
|
| 73 |
+
"cell": 0.9257083377439046,
|
| 74 |
+
"valid_sudoku": 0.817,
|
| 75 |
+
"clues_kept": 1.0,
|
| 76 |
+
"nfe": 59.4
|
| 77 |
+
},
|
| 78 |
+
"entbudget_0.01": {
|
| 79 |
+
"board": 0.834,
|
| 80 |
+
"cell": 0.9320419180692283,
|
| 81 |
+
"valid_sudoku": 0.834,
|
| 82 |
+
"clues_kept": 1.0,
|
| 83 |
+
"nfe": 31.4
|
| 84 |
+
},
|
| 85 |
+
"entbudget_0.05": {
|
| 86 |
+
"board": 0.828,
|
| 87 |
+
"cell": 0.9335767968667302,
|
| 88 |
+
"valid_sudoku": 0.828,
|
| 89 |
+
"clues_kept": 1.0,
|
| 90 |
+
"nfe": 23.8
|
| 91 |
+
},
|
| 92 |
+
"entbudget_0.2": {
|
| 93 |
+
"board": 0.807,
|
| 94 |
+
"cell": 0.9329240323206661,
|
| 95 |
+
"valid_sudoku": 0.807,
|
| 96 |
+
"clues_kept": 1.0,
|
| 97 |
+
"nfe": 18.0
|
| 98 |
+
},
|
| 99 |
+
"entbudget_0.5": {
|
| 100 |
+
"board": 0.711,
|
| 101 |
+
"cell": 0.9230267104195335,
|
| 102 |
+
"valid_sudoku": 0.711,
|
| 103 |
+
"clues_kept": 1.0,
|
| 104 |
+
"nfe": 12.0
|
| 105 |
+
},
|
| 106 |
+
"entbudget_1.0": {
|
| 107 |
+
"board": 0.609,
|
| 108 |
+
"cell": 0.898168730814015,
|
| 109 |
+
"valid_sudoku": 0.609,
|
| 110 |
+
"clues_kept": 1.0,
|
| 111 |
+
"nfe": 8.0
|
| 112 |
+
},
|
| 113 |
+
"entbudget_2.0": {
|
| 114 |
+
"board": 0.41,
|
| 115 |
+
"cell": 0.872622702092375,
|
| 116 |
+
"valid_sudoku": 0.41,
|
| 117 |
+
"clues_kept": 1.0,
|
| 118 |
+
"nfe": 5.8
|
| 119 |
+
},
|
| 120 |
+
"conf_0.9": {
|
| 121 |
+
"board": 0.432,
|
| 122 |
+
"cell": 0.8763452242334427,
|
| 123 |
+
"valid_sudoku": 0.432,
|
| 124 |
+
"clues_kept": 1.0,
|
| 125 |
+
"nfe": 10.8
|
| 126 |
+
},
|
| 127 |
+
"conf_0.99": {
|
| 128 |
+
"board": 0.789,
|
| 129 |
+
"cell": 0.9322006986344872,
|
| 130 |
+
"valid_sudoku": 0.789,
|
| 131 |
+
"clues_kept": 1.0,
|
| 132 |
+
"nfe": 22.0
|
| 133 |
+
},
|
| 134 |
+
"conf_0.999": {
|
| 135 |
+
"board": 0.827,
|
| 136 |
+
"cell": 0.9297484210154899,
|
| 137 |
+
"valid_sudoku": 0.827,
|
| 138 |
+
"clues_kept": 1.0,
|
| 139 |
+
"nfe": 29.8
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
"best_board": 0.834
|
| 143 |
+
}
|
results/inj-noinject-s0.json
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"args": {
|
| 3 |
+
"mode": "full",
|
| 4 |
+
"all_cells": false,
|
| 5 |
+
"objective": "diffusion",
|
| 6 |
+
"K": 8,
|
| 7 |
+
"lam": 1.0,
|
| 8 |
+
"pe": "ape",
|
| 9 |
+
"recurrent": true,
|
| 10 |
+
"R": 32,
|
| 11 |
+
"no_inject": true,
|
| 12 |
+
"no_deep_sup": false,
|
| 13 |
+
"R_infer": 64,
|
| 14 |
+
"root": "data/sudoku",
|
| 15 |
+
"hard": "/scratch/zt1/project/msml612/user/govind02/pcmd/data/sudoku_hard",
|
| 16 |
+
"augment": true,
|
| 17 |
+
"d": 128,
|
| 18 |
+
"layers": 1,
|
| 19 |
+
"heads": 4,
|
| 20 |
+
"bs": 64,
|
| 21 |
+
"steps": 110000,
|
| 22 |
+
"lr": 0.0003,
|
| 23 |
+
"warmup": 3000,
|
| 24 |
+
"seed": 0,
|
| 25 |
+
"eval_bs": 200,
|
| 26 |
+
"ckpt_every": 8000,
|
| 27 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/inj-noinject-s0"
|
| 28 |
+
},
|
| 29 |
+
"decode": {
|
| 30 |
+
"fixedK_1": {
|
| 31 |
+
"board": 0.82,
|
| 32 |
+
"cell": 0.9408101337285205,
|
| 33 |
+
"valid_sudoku": 0.82,
|
| 34 |
+
"clues_kept": 1.0,
|
| 35 |
+
"nfe": 1.0
|
| 36 |
+
},
|
| 37 |
+
"fixedK_2": {
|
| 38 |
+
"board": 0.879,
|
| 39 |
+
"cell": 0.9473377791891606,
|
| 40 |
+
"valid_sudoku": 0.879,
|
| 41 |
+
"clues_kept": 1.0,
|
| 42 |
+
"nfe": 2.0
|
| 43 |
+
},
|
| 44 |
+
"fixedK_4": {
|
| 45 |
+
"board": 0.9,
|
| 46 |
+
"cell": 0.9561236371334815,
|
| 47 |
+
"valid_sudoku": 0.9,
|
| 48 |
+
"clues_kept": 1.0,
|
| 49 |
+
"nfe": 4.0
|
| 50 |
+
},
|
| 51 |
+
"fixedK_8": {
|
| 52 |
+
"board": 0.922,
|
| 53 |
+
"cell": 0.9624572174588053,
|
| 54 |
+
"valid_sudoku": 0.922,
|
| 55 |
+
"clues_kept": 1.0,
|
| 56 |
+
"nfe": 8.0
|
| 57 |
+
},
|
| 58 |
+
"fixedK_16": {
|
| 59 |
+
"board": 0.94,
|
| 60 |
+
"cell": 0.9721428319395928,
|
| 61 |
+
"valid_sudoku": 0.94,
|
| 62 |
+
"clues_kept": 1.0,
|
| 63 |
+
"nfe": 16.0
|
| 64 |
+
},
|
| 65 |
+
"fixedK_32": {
|
| 66 |
+
"board": 0.936,
|
| 67 |
+
"cell": 0.9696023428954518,
|
| 68 |
+
"valid_sudoku": 0.936,
|
| 69 |
+
"clues_kept": 1.0,
|
| 70 |
+
"nfe": 32.0
|
| 71 |
+
},
|
| 72 |
+
"fixedK_61": {
|
| 73 |
+
"board": 0.944,
|
| 74 |
+
"cell": 0.9756007198052292,
|
| 75 |
+
"valid_sudoku": 0.944,
|
| 76 |
+
"clues_kept": 1.0,
|
| 77 |
+
"nfe": 59.4
|
| 78 |
+
},
|
| 79 |
+
"entbudget_0.01": {
|
| 80 |
+
"board": 0.949,
|
| 81 |
+
"cell": 0.9752125895345966,
|
| 82 |
+
"valid_sudoku": 0.949,
|
| 83 |
+
"clues_kept": 1.0,
|
| 84 |
+
"nfe": 45.8
|
| 85 |
+
},
|
| 86 |
+
"entbudget_0.05": {
|
| 87 |
+
"board": 0.952,
|
| 88 |
+
"cell": 0.9800289333474471,
|
| 89 |
+
"valid_sudoku": 0.952,
|
| 90 |
+
"clues_kept": 1.0,
|
| 91 |
+
"nfe": 40.0
|
| 92 |
+
},
|
| 93 |
+
"entbudget_0.2": {
|
| 94 |
+
"board": 0.944,
|
| 95 |
+
"cell": 0.9779118591439964,
|
| 96 |
+
"valid_sudoku": 0.944,
|
| 97 |
+
"clues_kept": 1.0,
|
| 98 |
+
"nfe": 26.6
|
| 99 |
+
},
|
| 100 |
+
"entbudget_0.5": {
|
| 101 |
+
"board": 0.927,
|
| 102 |
+
"cell": 0.9772590945979324,
|
| 103 |
+
"valid_sudoku": 0.927,
|
| 104 |
+
"clues_kept": 1.0,
|
| 105 |
+
"nfe": 23.6
|
| 106 |
+
},
|
| 107 |
+
"entbudget_1.0": {
|
| 108 |
+
"board": 0.888,
|
| 109 |
+
"cell": 0.9686320172188702,
|
| 110 |
+
"valid_sudoku": 0.888,
|
| 111 |
+
"clues_kept": 1.0,
|
| 112 |
+
"nfe": 18.2
|
| 113 |
+
},
|
| 114 |
+
"entbudget_2.0": {
|
| 115 |
+
"board": 0.858,
|
| 116 |
+
"cell": 0.9641332345365372,
|
| 117 |
+
"valid_sudoku": 0.858,
|
| 118 |
+
"clues_kept": 1.0,
|
| 119 |
+
"nfe": 10.0
|
| 120 |
+
},
|
| 121 |
+
"conf_0.9": {
|
| 122 |
+
"board": 0.886,
|
| 123 |
+
"cell": 0.9659856744645566,
|
| 124 |
+
"valid_sudoku": 0.886,
|
| 125 |
+
"clues_kept": 1.0,
|
| 126 |
+
"nfe": 22.6
|
| 127 |
+
},
|
| 128 |
+
"conf_0.99": {
|
| 129 |
+
"board": 0.934,
|
| 130 |
+
"cell": 0.9750714512543665,
|
| 131 |
+
"valid_sudoku": 0.934,
|
| 132 |
+
"clues_kept": 1.0,
|
| 133 |
+
"nfe": 35.4
|
| 134 |
+
},
|
| 135 |
+
"conf_0.999": {
|
| 136 |
+
"board": 0.946,
|
| 137 |
+
"cell": 0.9762358420662645,
|
| 138 |
+
"valid_sudoku": 0.946,
|
| 139 |
+
"clues_kept": 1.0,
|
| 140 |
+
"nfe": 39.4
|
| 141 |
+
}
|
| 142 |
+
},
|
| 143 |
+
"best_board": 0.952
|
| 144 |
+
}
|
results/inj-noinject-s1.json
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"args": {
|
| 3 |
+
"mode": "full",
|
| 4 |
+
"all_cells": false,
|
| 5 |
+
"objective": "diffusion",
|
| 6 |
+
"K": 8,
|
| 7 |
+
"lam": 1.0,
|
| 8 |
+
"pe": "ape",
|
| 9 |
+
"recurrent": true,
|
| 10 |
+
"R": 32,
|
| 11 |
+
"no_inject": true,
|
| 12 |
+
"no_deep_sup": false,
|
| 13 |
+
"R_infer": 64,
|
| 14 |
+
"root": "data/sudoku",
|
| 15 |
+
"hard": "/scratch/zt1/project/msml612/user/govind02/pcmd/data/sudoku_hard",
|
| 16 |
+
"augment": true,
|
| 17 |
+
"d": 128,
|
| 18 |
+
"layers": 1,
|
| 19 |
+
"heads": 4,
|
| 20 |
+
"bs": 64,
|
| 21 |
+
"steps": 110000,
|
| 22 |
+
"lr": 0.0003,
|
| 23 |
+
"warmup": 3000,
|
| 24 |
+
"seed": 1,
|
| 25 |
+
"eval_bs": 200,
|
| 26 |
+
"ckpt_every": 8000,
|
| 27 |
+
"out": "/scratch/zt1/project/msml612/user/govind02/pcmd/runs/inj-noinject-s1"
|
| 28 |
+
},
|
| 29 |
+
"decode": {
|
| 30 |
+
"fixedK_1": {
|
| 31 |
+
"board": 0.683,
|
| 32 |
+
"cell": 0.9332063088811263,
|
| 33 |
+
"valid_sudoku": 0.683,
|
| 34 |
+
"clues_kept": 1.0,
|
| 35 |
+
"nfe": 1.0
|
| 36 |
+
},
|
| 37 |
+
"fixedK_2": {
|
| 38 |
+
"board": 0.822,
|
| 39 |
+
"cell": 0.9432094844924315,
|
| 40 |
+
"valid_sudoku": 0.822,
|
| 41 |
+
"clues_kept": 1.0,
|
| 42 |
+
"nfe": 2.0
|
| 43 |
+
},
|
| 44 |
+
"fixedK_4": {
|
| 45 |
+
"board": 0.871,
|
| 46 |
+
"cell": 0.9562471331286828,
|
| 47 |
+
"valid_sudoku": 0.871,
|
| 48 |
+
"clues_kept": 1.0,
|
| 49 |
+
"nfe": 4.0
|
| 50 |
+
},
|
| 51 |
+
"fixedK_8": {
|
| 52 |
+
"board": 0.884,
|
| 53 |
+
"cell": 0.9612046152217635,
|
| 54 |
+
"valid_sudoku": 0.884,
|
| 55 |
+
"clues_kept": 1.0,
|
| 56 |
+
"nfe": 8.0
|
| 57 |
+
},
|
| 58 |
+
"fixedK_16": {
|
| 59 |
+
"board": 0.91,
|
| 60 |
+
"cell": 0.9646977876574574,
|
| 61 |
+
"valid_sudoku": 0.91,
|
| 62 |
+
"clues_kept": 1.0,
|
| 63 |
+
"nfe": 16.0
|
| 64 |
+
},
|
| 65 |
+
"fixedK_32": {
|
| 66 |
+
"board": 0.928,
|
| 67 |
+
"cell": 0.9728485233407431,
|
| 68 |
+
"valid_sudoku": 0.928,
|
| 69 |
+
"clues_kept": 1.0,
|
| 70 |
+
"nfe": 32.0
|
| 71 |
+
},
|
| 72 |
+
"fixedK_61": {
|
| 73 |
+
"board": 0.925,
|
| 74 |
+
"cell": 0.9684732366536114,
|
| 75 |
+
"valid_sudoku": 0.925,
|
| 76 |
+
"clues_kept": 1.0,
|
| 77 |
+
"nfe": 59.4
|
| 78 |
+
},
|
| 79 |
+
"entbudget_0.01": {
|
| 80 |
+
"board": 0.927,
|
| 81 |
+
"cell": 0.9681733178081225,
|
| 82 |
+
"valid_sudoku": 0.927,
|
| 83 |
+
"clues_kept": 1.0,
|
| 84 |
+
"nfe": 41.4
|
| 85 |
+
},
|
| 86 |
+
"entbudget_0.05": {
|
| 87 |
+
"board": 0.93,
|
| 88 |
+
"cell": 0.9708902297025511,
|
| 89 |
+
"valid_sudoku": 0.93,
|
| 90 |
+
"clues_kept": 1.0,
|
| 91 |
+
"nfe": 32.0
|
| 92 |
+
},
|
| 93 |
+
"entbudget_0.2": {
|
| 94 |
+
"board": 0.927,
|
| 95 |
+
"cell": 0.9727779542006281,
|
| 96 |
+
"valid_sudoku": 0.927,
|
| 97 |
+
"clues_kept": 1.0,
|
| 98 |
+
"nfe": 25.4
|
| 99 |
+
},
|
| 100 |
+
"entbudget_0.5": {
|
| 101 |
+
"board": 0.904,
|
| 102 |
+
"cell": 0.9671853498465122,
|
| 103 |
+
"valid_sudoku": 0.904,
|
| 104 |
+
"clues_kept": 1.0,
|
| 105 |
+
"nfe": 16.0
|
| 106 |
+
},
|
| 107 |
+
"entbudget_1.0": {
|
| 108 |
+
"board": 0.859,
|
| 109 |
+
"cell": 0.961028192371476,
|
| 110 |
+
"valid_sudoku": 0.859,
|
| 111 |
+
"clues_kept": 1.0,
|
| 112 |
+
"nfe": 11.6
|
| 113 |
+
},
|
| 114 |
+
"entbudget_2.0": {
|
| 115 |
+
"board": 0.816,
|
| 116 |
+
"cell": 0.9536537172294556,
|
| 117 |
+
"valid_sudoku": 0.816,
|
| 118 |
+
"clues_kept": 1.0,
|
| 119 |
+
"nfe": 6.8
|
| 120 |
+
},
|
| 121 |
+
"conf_0.9": {
|
| 122 |
+
"board": 0.755,
|
| 123 |
+
"cell": 0.9550827423167849,
|
| 124 |
+
"valid_sudoku": 0.755,
|
| 125 |
+
"clues_kept": 1.0,
|
| 126 |
+
"nfe": 11.8
|
| 127 |
+
},
|
| 128 |
+
"conf_0.99": {
|
| 129 |
+
"board": 0.905,
|
| 130 |
+
"cell": 0.9663914470202181,
|
| 131 |
+
"valid_sudoku": 0.905,
|
| 132 |
+
"clues_kept": 1.0,
|
| 133 |
+
"nfe": 28.0
|
| 134 |
+
},
|
| 135 |
+
"conf_0.999": {
|
| 136 |
+
"board": 0.925,
|
| 137 |
+
"cell": 0.9687378709290427,
|
| 138 |
+
"valid_sudoku": 0.925,
|
| 139 |
+
"clues_kept": 1.0,
|
| 140 |
+
"nfe": 37.8
|
| 141 |
+
}
|
| 142 |
+
},
|
| 143 |
+
"best_board": 0.93
|
| 144 |
+
}
|
slurm/genhard2.sh
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Build a HARD STRATUM test set to close the paper's main weakness.
|
| 3 |
+
#
|
| 4 |
+
# Published Sudoku numbers are on 17-clue puzzles; ours are 20-30 (mean 24.4),
|
| 5 |
+
# so the comparison has had to be hedged as "different distribution". Our test
|
| 6 |
+
# set contains only 46 puzzles at <=22 clues - too few to report.
|
| 7 |
+
#
|
| 8 |
+
# Generating a large pool and keeping the hardest tail gives a stratum close to
|
| 9 |
+
# the published difficulty, on which the EXISTING trained models can be evaluated
|
| 10 |
+
# with no retraining. CPU only, so it costs nothing against the GPU allocation.
|
| 11 |
+
#SBATCH -J genh2
|
| 12 |
+
#SBATCH -p standard
|
| 13 |
+
#SBATCH -c 64
|
| 14 |
+
#SBATCH --mem=64g
|
| 15 |
+
#SBATCH -t 4:00:00
|
| 16 |
+
#SBATCH -o logs/genh2-%j.out
|
| 17 |
+
export SCR=/scratch/zt1/project/msml612/user/$USER
|
| 18 |
+
source /etc/profile
|
| 19 |
+
SCR=/scratch/zt1/project/msml612/user/$USER
|
| 20 |
+
module load python/3.10.10
|
| 21 |
+
export PYTHONPATH=
|
| 22 |
+
cd $SCR/pcmd/src
|
| 23 |
+
$SCR/hf_env/bin/python -u - <<'PY'
|
| 24 |
+
import numpy as np, os, sys, time
|
| 25 |
+
from multiprocessing import Pool
|
| 26 |
+
sys.path.insert(0, '.')
|
| 27 |
+
from gen_hard_sudoku import make_one, canon
|
| 28 |
+
from sudoku import CELLS
|
| 29 |
+
|
| 30 |
+
OUT = os.path.join(os.environ['SCR'], 'pcmd/data/sudoku_stratum')
|
| 31 |
+
os.makedirs(OUT, exist_ok=True)
|
| 32 |
+
N = 30000
|
| 33 |
+
t0 = time.time()
|
| 34 |
+
with Pool(64) as p:
|
| 35 |
+
got = p.map(make_one, range(1_000_000, 1_000_000 + N), chunksize=32)
|
| 36 |
+
P = np.stack([g for g, _ in got]); S = np.stack([s for _, s in got])
|
| 37 |
+
clues = (P > 0).sum(1)
|
| 38 |
+
print(f"generated {N} minimal puzzles in {time.time()-t0:.0f}s", flush=True)
|
| 39 |
+
import collections
|
| 40 |
+
h = collections.Counter(clues.tolist())
|
| 41 |
+
for k in sorted(h): print(f" {k} clues: {h[k]}", flush=True)
|
| 42 |
+
|
| 43 |
+
for cap, name in ((22, 'le22'), (23, 'le23')):
|
| 44 |
+
m = clues <= cap
|
| 45 |
+
if m.sum() < 50: continue
|
| 46 |
+
np.save(os.path.join(OUT, f'{name}_puz.npy'), P[m])
|
| 47 |
+
np.save(os.path.join(OUT, f'{name}_sol.npy'), S[m])
|
| 48 |
+
print(f" saved {name}: {int(m.sum())} puzzles, mean {clues[m].mean():.1f} clues",
|
| 49 |
+
flush=True)
|
| 50 |
+
|
| 51 |
+
# these must not overlap the TRAINING solutions of the models we will evaluate
|
| 52 |
+
tr = np.load(os.path.join(os.environ['SCR'], 'pcmd/data/sudoku_hard/train_sol.npy'))
|
| 53 |
+
trc = {canon(r) for r in tr}
|
| 54 |
+
for cap, name in ((22, 'le22'), (23, 'le23')):
|
| 55 |
+
f = os.path.join(OUT, f'{name}_sol.npy')
|
| 56 |
+
if not os.path.exists(f): continue
|
| 57 |
+
Ssub = np.load(f)
|
| 58 |
+
leak = sum(canon(r) in trc for r in Ssub)
|
| 59 |
+
print(f" {name}: {leak}/{len(Ssub)} solutions also in training (must be 0)", flush=True)
|
| 60 |
+
assert leak == 0
|
| 61 |
+
PY
|
slurm/inj2.sh
CHANGED
|
@@ -19,7 +19,7 @@
|
|
| 19 |
#SBATCH --mem=16g
|
| 20 |
#SBATCH -t 6:00:00
|
| 21 |
#SBATCH -o logs/inj-%A_%a.out
|
| 22 |
-
#SBATCH --array=0-
|
| 23 |
source /etc/profile
|
| 24 |
SCR=/scratch/zt1/project/msml612/user/$USER
|
| 25 |
module load python/3.10.10
|
|
|
|
| 19 |
#SBATCH --mem=16g
|
| 20 |
#SBATCH -t 6:00:00
|
| 21 |
#SBATCH -o logs/inj-%A_%a.out
|
| 22 |
+
#SBATCH --array=0-1%2
|
| 23 |
source /etc/profile
|
| 24 |
SCR=/scratch/zt1/project/msml612/user/$USER
|
| 25 |
module load python/3.10.10
|
slurm/inj3.sh
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Closes the one acknowledged gap in the ablation.
|
| 3 |
+
#
|
| 4 |
+
# The headline comparison - recurrent 99.70% against 32 distinct layers 94.50% -
|
| 5 |
+
# conflates TWO differences: weight sharing, and re-supplying the input at every
|
| 6 |
+
# application. --no_inject holds architecture, depth, parameter count and
|
| 7 |
+
# objective fixed and changes only the injection, which isolates them.
|
| 8 |
+
#
|
| 9 |
+
# no-injection stays near 98% -> weight sharing is the mechanism
|
| 10 |
+
# no-injection falls toward 94% -> injection is, and our explanation is wrong
|
| 11 |
+
#
|
| 12 |
+
# Runs on a 5GB MIG slice: at 212k parameters and 81-token sequences the
|
| 13 |
+
# optimiser state is a few MB, and that partition bills at 9/min against the
|
| 14 |
+
# H100's 144 - 16x more compute from the same remaining allocation.
|
| 15 |
+
#SBATCH -J inj
|
| 16 |
+
#SBATCH -p gpu-a100_1g.5gb
|
| 17 |
+
#SBATCH --gres=gpu:a100_1g.5gb:1
|
| 18 |
+
#SBATCH -c 4
|
| 19 |
+
#SBATCH --mem=16g
|
| 20 |
+
#SBATCH -t 2:00:00
|
| 21 |
+
#SBATCH -o logs/inj-%A_%a.out
|
| 22 |
+
#SBATCH --array=0-1%2
|
| 23 |
+
source /etc/profile
|
| 24 |
+
SCR=/scratch/zt1/project/msml612/user/$USER
|
| 25 |
+
module load python/3.10.10
|
| 26 |
+
export PYTHONPATH=
|
| 27 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 28 |
+
cd $SCR/pcmd/src
|
| 29 |
+
I=$SLURM_ARRAY_TASK_ID
|
| 30 |
+
COMMON="--mode full --pe ape --hard $SCR/pcmd/data/sudoku_hard --augment \
|
| 31 |
+
--heads 4 --bs 64 --steps 110000 --lr 3e-4 --warmup 3000 --K 8 --lam 1.0 \
|
| 32 |
+
--eval_bs 200 --ckpt_every 8000"
|
| 33 |
+
case $I in
|
| 34 |
+
0) NAME="inj-noinject-s0"; ARGS="--recurrent --R 32 --R_infer 64 --no_inject --d 128 --layers 1 --seed 0" ;;
|
| 35 |
+
1) NAME="inj-noinject-s1"; ARGS="--recurrent --R 32 --R_infer 64 --no_inject --d 128 --layers 1 --seed 1" ;;
|
| 36 |
+
2) NAME="inj-deep32L-s1"; ARGS="--d 128 --layers 32 --seed 1" ;;
|
| 37 |
+
3) NAME="inj-rec-s2"; ARGS="--recurrent --R 32 --R_infer 64 --d 128 --layers 1 --seed 2" ;;
|
| 38 |
+
4) NAME="inj-rec-s3"; ARGS="--recurrent --R 32 --R_infer 64 --d 128 --layers 1 --seed 3" ;;
|
| 39 |
+
esac
|
| 40 |
+
echo "=== $NAME ==="
|
| 41 |
+
$SCR/hf_env/bin/python -u train_sudoku.py $ARGS $COMMON --out $SCR/pcmd/runs/$NAME
|
slurm/plots.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH -J plots
|
| 3 |
+
#SBATCH -p gpu-a100_1g.5gb
|
| 4 |
+
#SBATCH --gres=gpu:a100_1g.5gb:1
|
| 5 |
+
#SBATCH -c 4
|
| 6 |
+
#SBATCH --mem=16g
|
| 7 |
+
#SBATCH -t 0:40:00
|
| 8 |
+
#SBATCH -o logs/plots-%j.out
|
| 9 |
+
source /etc/profile
|
| 10 |
+
SCR=/scratch/zt1/project/msml612/user/$USER
|
| 11 |
+
module load python/3.10.10
|
| 12 |
+
export PYTHONPATH=
|
| 13 |
+
export RUNS=$SCR/pcmd/runs OUT=$SCR/pcmd/figures HARD=$SCR/pcmd/data/sudoku_hard
|
| 14 |
+
cd $SCR/pcmd/src
|
| 15 |
+
$SCR/hf_env/bin/python -u plot_sudoku.py
|
| 16 |
+
$SCR/hf_env/bin/python -u plot_sudoku_grids.py
|
src/model.py
CHANGED
|
@@ -194,10 +194,15 @@ class RecurrentDenoiser(nn.Module):
|
|
| 194 |
"""
|
| 195 |
|
| 196 |
def __init__(self, vocab, d=128, n_layers=1, n_heads=4, pe="ape",
|
| 197 |
-
max_len=128, recurrences=32, hidden_mult=4):
|
| 198 |
super().__init__()
|
| 199 |
self.pe_kind, self.causal = pe, False
|
| 200 |
self.recurrences = recurrences
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
self.emb = nn.Embedding(vocab, d)
|
| 202 |
if pe == "ape":
|
| 203 |
self.pos = nn.Embedding(max_len, d)
|
|
@@ -225,7 +230,8 @@ class RecurrentDenoiser(nn.Module):
|
|
| 225 |
h = inp
|
| 226 |
outs = []
|
| 227 |
for _ in range(R):
|
| 228 |
-
|
|
|
|
| 229 |
for b in self.blocks:
|
| 230 |
h = b(h, pad_mask, pos_ids)
|
| 231 |
outs.append(self.head(self.norm(h)))
|
|
|
|
| 194 |
"""
|
| 195 |
|
| 196 |
def __init__(self, vocab, d=128, n_layers=1, n_heads=4, pe="ape",
|
| 197 |
+
max_len=128, recurrences=32, hidden_mult=4, inject=True):
|
| 198 |
super().__init__()
|
| 199 |
self.pe_kind, self.causal = pe, False
|
| 200 |
self.recurrences = recurrences
|
| 201 |
+
# `inject` exists to separate two things the headline comparison
|
| 202 |
+
# otherwise conflates: weight sharing, and re-supplying the input at
|
| 203 |
+
# every application. Turning it off keeps the architecture and the depth
|
| 204 |
+
# identical and changes only the injection, which is the clean isolation.
|
| 205 |
+
self.inject = inject
|
| 206 |
self.emb = nn.Embedding(vocab, d)
|
| 207 |
if pe == "ape":
|
| 208 |
self.pos = nn.Embedding(max_len, d)
|
|
|
|
| 230 |
h = inp
|
| 231 |
outs = []
|
| 232 |
for _ in range(R):
|
| 233 |
+
if self.inject:
|
| 234 |
+
h = h + inp # input injection: keep the clues alive
|
| 235 |
for b in self.blocks:
|
| 236 |
h = b(h, pad_mask, pos_ids)
|
| 237 |
outs.append(self.head(self.norm(h)))
|
src/plot_sudoku.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Every figure for the Sudoku result, generated from the raw run files.
|
| 2 |
+
|
| 3 |
+
Nothing here is hand-entered: each panel reads result.json from the runs it
|
| 4 |
+
plots, so a figure cannot drift from the numbers it claims to show.
|
| 5 |
+
"""
|
| 6 |
+
import glob, json, os
|
| 7 |
+
from collections import defaultdict
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import matplotlib
|
| 11 |
+
matplotlib.use("Agg")
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
|
| 14 |
+
RUNS = os.environ.get("RUNS", "runs")
|
| 15 |
+
OUT = os.environ.get("OUT", "figures")
|
| 16 |
+
os.makedirs(OUT, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
# classical baselines, measured on this exact test set by sudoku_compare.py
|
| 19 |
+
CLASSICAL = {"constraint propagation": 0.0300, "greedy MRV": 0.0590}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def cfg_label(a):
|
| 23 |
+
if a.get("objective") == "ar":
|
| 24 |
+
return "autoregressive prefix-LM"
|
| 25 |
+
if a.get("objective") == "direct":
|
| 26 |
+
return "direct recurrent classifier"
|
| 27 |
+
if not a.get("recurrent"):
|
| 28 |
+
return f"feed-forward {a['layers']}L ({a['mode']})"
|
| 29 |
+
tags = []
|
| 30 |
+
if a.get("no_inject"):
|
| 31 |
+
tags.append("no-inject")
|
| 32 |
+
if a.get("no_deep_sup"):
|
| 33 |
+
tags.append("no-deepsup")
|
| 34 |
+
return "recurrent " + a["mode"] + (" [" + ",".join(tags) + "]" if tags else "")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
runs = defaultdict(list)
|
| 38 |
+
for f in sorted(glob.glob(f"{RUNS}/*/result.json")):
|
| 39 |
+
n = os.path.basename(os.path.dirname(f))
|
| 40 |
+
if not n.startswith(("rec1-", "abl-", "base-", "fix-", "inj-", "sudh-")):
|
| 41 |
+
continue
|
| 42 |
+
try:
|
| 43 |
+
r = json.load(open(f))
|
| 44 |
+
except Exception:
|
| 45 |
+
continue
|
| 46 |
+
if r.get("decode"):
|
| 47 |
+
runs[cfg_label(r["args"])].append(r)
|
| 48 |
+
|
| 49 |
+
# ---- Figure 1: board accuracy against decoding passes -----------------------
|
| 50 |
+
fig, ax = plt.subplots(figsize=(7.2, 4.6))
|
| 51 |
+
GRID = [1, 2, 4, 8, 16, 32, 61]
|
| 52 |
+
style = {"recurrent full": ("#1a5276", "o", 2.4),
|
| 53 |
+
"recurrent mdlm": ("#2980b9", "s", 1.6),
|
| 54 |
+
"feed-forward 12L (full)": ("#c0392b", "^", 1.6),
|
| 55 |
+
"feed-forward 12L (mdlm)": ("#e67e22", "v", 1.4),
|
| 56 |
+
"feed-forward 32L (full)": ("#8e44ad", "D", 1.4)}
|
| 57 |
+
for label, rs in sorted(runs.items()):
|
| 58 |
+
if label not in style:
|
| 59 |
+
continue
|
| 60 |
+
col, mk, lw = style[label]
|
| 61 |
+
xs, ys = [], []
|
| 62 |
+
for k in GRID:
|
| 63 |
+
v = [r["decode"][f"fixedK_{k}"]["board"] for r in rs
|
| 64 |
+
if f"fixedK_{k}" in r["decode"]]
|
| 65 |
+
if v:
|
| 66 |
+
xs.append(k); ys.append(np.mean(v) * 100)
|
| 67 |
+
if xs:
|
| 68 |
+
ax.plot(xs, ys, marker=mk, color=col, lw=lw, ms=5, label=label)
|
| 69 |
+
for name, v in CLASSICAL.items():
|
| 70 |
+
ax.axhline(v * 100, ls=":", lw=1.2, color="#555")
|
| 71 |
+
ax.text(1.05, v * 100 + 1.5, name, fontsize=7, color="#555")
|
| 72 |
+
ax.set_xscale("log", base=2)
|
| 73 |
+
ax.set_xlabel("denoising passes (NFE)")
|
| 74 |
+
ax.set_ylabel("board accuracy (%) — all 81 cells correct")
|
| 75 |
+
ax.set_title("Hard Sudoku: recurrence beats capacity")
|
| 76 |
+
ax.grid(alpha=.3); ax.legend(fontsize=7.5, loc="lower right"); ax.set_ylim(-3, 103)
|
| 77 |
+
fig.tight_layout(); fig.savefig(f"{OUT}/sud_fig1_passes.png", dpi=170)
|
| 78 |
+
|
| 79 |
+
# ---- Figure 2: parameters against accuracy ---------------------------------
|
| 80 |
+
fig, ax = plt.subplots(figsize=(6.6, 4.4))
|
| 81 |
+
pts = []
|
| 82 |
+
for label, rs in runs.items():
|
| 83 |
+
a = rs[0]["args"]
|
| 84 |
+
npar = 212e3 if a.get("recurrent") else (
|
| 85 |
+
6.34e6 if a.get("layers") == 32 else (2.4e6 if a.get("objective") == "ar"
|
| 86 |
+
else 37.9e6))
|
| 87 |
+
best = np.mean([max(v["board"] for v in r["decode"].values()) for r in rs])
|
| 88 |
+
pts.append((npar, best * 100, label))
|
| 89 |
+
for npar, acc, label in pts:
|
| 90 |
+
hero = label == "recurrent full"
|
| 91 |
+
ax.scatter(npar, acc, s=170 if hero else 70,
|
| 92 |
+
color="#1a5276" if hero else "#888",
|
| 93 |
+
marker="*" if hero else "o", zorder=3 if hero else 2)
|
| 94 |
+
ax.annotate(label, (npar, acc), fontsize=6.6,
|
| 95 |
+
xytext=(6, 5 if acc < 95 else -11), textcoords="offset points")
|
| 96 |
+
ax.axhline(99.5, ls="--", color="#117a65", lw=1.2)
|
| 97 |
+
ax.text(2.3e5, 99.5 + 0.9, "Recurrent Transformer 99.5% @211k params "
|
| 98 |
+
"(different clue distribution)", fontsize=6.6, color="#117a65")
|
| 99 |
+
ax.set_xscale("log")
|
| 100 |
+
ax.set_xlabel("parameters"); ax.set_ylabel("board accuracy (%)")
|
| 101 |
+
ax.set_title("180× fewer parameters, 10 points better")
|
| 102 |
+
ax.grid(alpha=.3); ax.set_ylim(-5, 108)
|
| 103 |
+
fig.tight_layout(); fig.savefig(f"{OUT}/sud_fig2_params.png", dpi=170)
|
| 104 |
+
|
| 105 |
+
# ---- Figure 3: recurrence extrapolates past its training depth --------------
|
| 106 |
+
sw = os.path.join(OUT, "sudoku_rsweep.json")
|
| 107 |
+
if os.path.exists(sw):
|
| 108 |
+
data = json.load(open(sw))
|
| 109 |
+
fig, ax = plt.subplots(figsize=(6.6, 4.2))
|
| 110 |
+
for name, d in data.items():
|
| 111 |
+
Rs = sorted(int(k) for k in d)
|
| 112 |
+
ys = [d[str(R)] * 100 for R in Rs]
|
| 113 |
+
ax.plot(Rs, ys, marker="o", ms=4,
|
| 114 |
+
color="#1a5276" if "full" in name else "#c0392b",
|
| 115 |
+
alpha=.85, label=name)
|
| 116 |
+
ax.axvline(32, ls="--", color="k", lw=1)
|
| 117 |
+
ax.text(33, 12, "trained at R = 32", fontsize=7.5, rotation=90)
|
| 118 |
+
ax.set_xscale("log", base=2)
|
| 119 |
+
ax.set_xlabel("recurrences at inference"); ax.set_ylabel("board accuracy (%)")
|
| 120 |
+
ax.set_title("Deep supervision makes the recurrence iterable past training depth")
|
| 121 |
+
ax.grid(alpha=.3); ax.legend(fontsize=7); ax.set_ylim(-3, 103)
|
| 122 |
+
fig.tight_layout(); fig.savefig(f"{OUT}/sud_fig3_rsweep.png", dpi=170)
|
| 123 |
+
|
| 124 |
+
# ---- Figure 4: ablation ladder ---------------------------------------------
|
| 125 |
+
order = ["feed-forward 12L (mdlm)", "feed-forward 12L (full)",
|
| 126 |
+
"feed-forward 32L (full)", "recurrent mdlm",
|
| 127 |
+
"recurrent full [no-deepsup]", "recurrent full [no-inject]",
|
| 128 |
+
"recurrent full"]
|
| 129 |
+
have = [(o, np.mean([max(v["board"] for v in r["decode"].values())
|
| 130 |
+
for r in runs[o]]) * 100) for o in order if o in runs]
|
| 131 |
+
if have:
|
| 132 |
+
fig, ax = plt.subplots(figsize=(7.4, 4.0))
|
| 133 |
+
names = [h[0] for h in have]; vals = [h[1] for h in have]
|
| 134 |
+
cols = ["#1a5276" if n == "recurrent full" else "#9fb6c8" for n in names]
|
| 135 |
+
ax.barh(range(len(vals)), vals, color=cols)
|
| 136 |
+
for i, v in enumerate(vals):
|
| 137 |
+
ax.text(v + 0.8, i, f"{v:.2f}%", va="center", fontsize=8)
|
| 138 |
+
ax.set_yticks(range(len(names))); ax.set_yticklabels(names, fontsize=8)
|
| 139 |
+
ax.set_xlabel("board accuracy (%)"); ax.set_xlim(0, 108)
|
| 140 |
+
ax.set_title("Every ingredient is load-bearing")
|
| 141 |
+
ax.grid(alpha=.3, axis="x")
|
| 142 |
+
fig.tight_layout(); fig.savefig(f"{OUT}/sud_fig4_ablation.png", dpi=170)
|
| 143 |
+
|
| 144 |
+
print("configurations plotted:")
|
| 145 |
+
for k in sorted(runs):
|
| 146 |
+
v = [max(x["board"] for x in r["decode"].values()) for r in runs[k]]
|
| 147 |
+
print(f" {k:<42} n={len(v)} {np.mean(v)*100:6.2f}% {[round(x*100,1) for x in v]}")
|
| 148 |
+
print(f"\nwrote figures to {OUT}")
|
src/plot_sudoku_grids.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sudoku figures that show the model actually solving, plus difficulty curves.
|
| 2 |
+
|
| 3 |
+
Everything is drawn from real runs and real test puzzles - the grids below are
|
| 4 |
+
genuine model output on held-out instances, not illustrations.
|
| 5 |
+
"""
|
| 6 |
+
import glob, json, os, sys
|
| 7 |
+
from collections import defaultdict
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
import matplotlib
|
| 12 |
+
matplotlib.use("Agg")
|
| 13 |
+
import matplotlib.pyplot as plt
|
| 14 |
+
from matplotlib.patches import Rectangle
|
| 15 |
+
|
| 16 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 17 |
+
from model import RecurrentDenoiser, Transformer
|
| 18 |
+
import diffusion as dfn
|
| 19 |
+
from sudoku import CELLS, GRID, SudokuTokenizer
|
| 20 |
+
|
| 21 |
+
RUNS = os.environ.get("RUNS", "runs")
|
| 22 |
+
OUT = os.environ.get("OUT", "figures")
|
| 23 |
+
HARD = os.environ.get("HARD", "data/sudoku_hard")
|
| 24 |
+
os.makedirs(OUT, exist_ok=True)
|
| 25 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 26 |
+
tok = SudokuTokenizer()
|
| 27 |
+
|
| 28 |
+
P = np.load(os.path.join(HARD, "test_puz.npy"))
|
| 29 |
+
S = np.load(os.path.join(HARD, "test_sol.npy"))
|
| 30 |
+
clues = (P > 0).sum(1)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def draw_grid(ax, grid, given=None, wrong=None, title="", fs=9):
|
| 34 |
+
"""Render one 9x9 board. Given clues bold, wrong cells shaded red."""
|
| 35 |
+
g = np.asarray(grid).reshape(GRID, GRID)
|
| 36 |
+
gv = np.asarray(given).reshape(GRID, GRID) if given is not None else None
|
| 37 |
+
wr = np.asarray(wrong).reshape(GRID, GRID) if wrong is not None else None
|
| 38 |
+
ax.set_xlim(0, 9); ax.set_ylim(0, 9); ax.invert_yaxis()
|
| 39 |
+
ax.set_xticks([]); ax.set_yticks([])
|
| 40 |
+
for i in range(GRID):
|
| 41 |
+
for j in range(GRID):
|
| 42 |
+
if wr is not None and wr[i, j]:
|
| 43 |
+
ax.add_patch(Rectangle((j, i), 1, 1, color="#f5b7b1", zorder=0))
|
| 44 |
+
elif gv is not None and gv[i, j]:
|
| 45 |
+
ax.add_patch(Rectangle((j, i), 1, 1, color="#eaeded", zorder=0))
|
| 46 |
+
v = int(g[i, j])
|
| 47 |
+
if v > 0:
|
| 48 |
+
bold = gv is not None and gv[i, j]
|
| 49 |
+
ax.text(j + .5, i + .55, str(v), ha="center", va="center",
|
| 50 |
+
fontsize=fs, fontweight="bold" if bold else "normal",
|
| 51 |
+
color="#000" if bold else ("#922b21" if (wr is not None and wr[i, j])
|
| 52 |
+
else "#1a5276"), zorder=2)
|
| 53 |
+
for k in range(10):
|
| 54 |
+
lw = 1.9 if k % 3 == 0 else 0.5
|
| 55 |
+
ax.plot([k, k], [0, 9], color="k", lw=lw)
|
| 56 |
+
ax.plot([0, 9], [k, k], color="k", lw=lw)
|
| 57 |
+
ax.set_title(title, fontsize=8.5)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def load_best():
|
| 61 |
+
"""The strongest trained model available."""
|
| 62 |
+
best, bacc = None, -1
|
| 63 |
+
for d in sorted(glob.glob(f"{RUNS}/rec1-*")) + sorted(glob.glob(f"{RUNS}/inj-rec*")):
|
| 64 |
+
rf, mf = os.path.join(d, "result.json"), os.path.join(d, "model.pt")
|
| 65 |
+
if not (os.path.exists(rf) and os.path.exists(mf)):
|
| 66 |
+
continue
|
| 67 |
+
r = json.load(open(rf))
|
| 68 |
+
acc = max(v["board"] for v in r["decode"].values())
|
| 69 |
+
if acc > bacc:
|
| 70 |
+
bacc, best = acc, (d, r["args"])
|
| 71 |
+
if best is None:
|
| 72 |
+
return None, None
|
| 73 |
+
d, cfg = best
|
| 74 |
+
m = RecurrentDenoiser(len(tok), cfg["d"], cfg["layers"], cfg["heads"], cfg["pe"],
|
| 75 |
+
max_len=CELLS + 8,
|
| 76 |
+
recurrences=cfg.get("R_infer") or cfg["R"]).to(dev)
|
| 77 |
+
m.load_state_dict(torch.load(os.path.join(d, "model.pt"), map_location=dev))
|
| 78 |
+
m.eval()
|
| 79 |
+
return m, cfg
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
model, cfg = load_best()
|
| 83 |
+
|
| 84 |
+
# ---- Figure 5: the model solving, pass by pass ------------------------------
|
| 85 |
+
if model is not None:
|
| 86 |
+
hardest = np.argsort(clues)[:3] # fewest clues = hardest
|
| 87 |
+
budgets = [1, 4, 16, 61]
|
| 88 |
+
fig, axes = plt.subplots(len(hardest), len(budgets) + 2,
|
| 89 |
+
figsize=(2.0 * (len(budgets) + 2), 2.05 * len(hardest)))
|
| 90 |
+
for r, idx in enumerate(hardest):
|
| 91 |
+
puz, sol = P[idx], S[idx]
|
| 92 |
+
given = puz > 0
|
| 93 |
+
draw_grid(axes[r][0], puz, given,
|
| 94 |
+
title=f"puzzle ({given.sum()} clues)" if r == 0 else f"({given.sum()} clues)")
|
| 95 |
+
for c, K in enumerate(budgets):
|
| 96 |
+
with torch.no_grad():
|
| 97 |
+
gold = torch.from_numpy(sol - 1)[None].to(dev)
|
| 98 |
+
bl = torch.from_numpy(~given)[None].to(dev)
|
| 99 |
+
x = torch.where(bl, torch.full_like(gold, tok.mask), gold)
|
| 100 |
+
y, _ = dfn.fixed_k_decode(model, x, tok, dev, K, fillable=bl)
|
| 101 |
+
pred = y[0].cpu().numpy() + 1
|
| 102 |
+
wrong = (pred != sol) & (~given)
|
| 103 |
+
ok = not wrong.any()
|
| 104 |
+
draw_grid(axes[r][c + 1], pred, given, wrong,
|
| 105 |
+
title=(f"{K} pass{'es' if K > 1 else ''}" if r == 0 else "")
|
| 106 |
+
+ (" ✓" if ok else f" {wrong.sum()} wrong"))
|
| 107 |
+
draw_grid(axes[r][-1], sol, given, title="solution" if r == 0 else "")
|
| 108 |
+
fig.suptitle("Recurrent masked diffusion solving held-out hard Sudoku "
|
| 109 |
+
"(grey = given clue, red = wrong cell)", fontsize=10)
|
| 110 |
+
fig.tight_layout(rect=[0, 0, 1, 0.96])
|
| 111 |
+
fig.savefig(f"{OUT}/sud_fig5_grids.png", dpi=165)
|
| 112 |
+
print("wrote sud_fig5_grids.png")
|
| 113 |
+
|
| 114 |
+
# ---- Figure 6: how the board fills across recurrences -----------------------
|
| 115 |
+
if model is not None:
|
| 116 |
+
idx = int(np.argsort(clues)[0])
|
| 117 |
+
puz, sol = P[idx], S[idx]
|
| 118 |
+
given = puz > 0
|
| 119 |
+
Rs = [1, 2, 4, 8, 16, 32, 64]
|
| 120 |
+
fig, axes = plt.subplots(1, len(Rs), figsize=(2.0 * len(Rs), 2.35))
|
| 121 |
+
for c, R in enumerate(Rs):
|
| 122 |
+
model.recurrences = R
|
| 123 |
+
with torch.no_grad():
|
| 124 |
+
gold = torch.from_numpy(sol - 1)[None].to(dev)
|
| 125 |
+
bl = torch.from_numpy(~given)[None].to(dev)
|
| 126 |
+
x = torch.where(bl, torch.full_like(gold, tok.mask), gold)
|
| 127 |
+
y, _ = dfn.fixed_k_decode(model, x, tok, dev, 16, fillable=bl)
|
| 128 |
+
pred = y[0].cpu().numpy() + 1
|
| 129 |
+
wrong = (pred != sol) & (~given)
|
| 130 |
+
draw_grid(axes[c], pred, given, wrong,
|
| 131 |
+
title=f"R={R} {(~wrong[~given]).mean()*100:.0f}% cells", fs=7.5)
|
| 132 |
+
model.recurrences = cfg.get("R_infer") or cfg["R"]
|
| 133 |
+
fig.suptitle("The same block applied more times: iterative refinement, "
|
| 134 |
+
"trained at R=32", fontsize=10)
|
| 135 |
+
fig.tight_layout(rect=[0, 0, 1, 0.93])
|
| 136 |
+
fig.savefig(f"{OUT}/sud_fig6_recurrence.png", dpi=165)
|
| 137 |
+
print("wrote sud_fig6_recurrence.png")
|
| 138 |
+
|
| 139 |
+
# ---- Figure 7: accuracy against puzzle difficulty ---------------------------
|
| 140 |
+
if model is not None:
|
| 141 |
+
model.recurrences = cfg.get("R_infer") or cfg["R"]
|
| 142 |
+
preds = []
|
| 143 |
+
with torch.no_grad():
|
| 144 |
+
for i in range(0, len(P), 200):
|
| 145 |
+
gold = torch.from_numpy(S[i:i + 200] - 1).to(dev)
|
| 146 |
+
bl = torch.from_numpy(P[i:i + 200] == 0).to(dev)
|
| 147 |
+
x = torch.where(bl, torch.full_like(gold, tok.mask), gold)
|
| 148 |
+
y, _ = dfn.fixed_k_decode(model, x, tok, dev, 61, fillable=bl)
|
| 149 |
+
preds.append(y.cpu().numpy() + 1)
|
| 150 |
+
pred = np.concatenate(preds)
|
| 151 |
+
solved = (pred == S).all(axis=1)
|
| 152 |
+
ks = sorted({int(c) for c in clues if (clues == c).sum() >= 15})
|
| 153 |
+
acc = [solved[clues == k].mean() * 100 for k in ks]
|
| 154 |
+
nn = [(clues == k).sum() for k in ks]
|
| 155 |
+
fig, ax = plt.subplots(figsize=(6.8, 4.2))
|
| 156 |
+
ax.plot(ks, acc, "o-", color="#1a5276", lw=2, ms=7, label="recurrent diffusion (ours)")
|
| 157 |
+
ax.axhline(3.0, ls=":", color="#c0392b", lw=1.4)
|
| 158 |
+
ax.text(ks[0], 5.5, "constraint propagation, whole set: 3.0%", fontsize=7.5,
|
| 159 |
+
color="#c0392b")
|
| 160 |
+
for k, a, n in zip(ks, acc, nn):
|
| 161 |
+
ax.annotate(f"n={n}", (k, a), fontsize=6.5, xytext=(0, -13),
|
| 162 |
+
textcoords="offset points", ha="center", color="#555")
|
| 163 |
+
ax.set_xlabel("clues given (fewer = harder)")
|
| 164 |
+
ax.set_ylabel("board accuracy (%)")
|
| 165 |
+
ax.set_title("Accuracy against puzzle difficulty, held-out test set")
|
| 166 |
+
ax.grid(alpha=.3); ax.legend(fontsize=8); ax.set_ylim(-3, 105)
|
| 167 |
+
ax.invert_xaxis()
|
| 168 |
+
fig.tight_layout(); fig.savefig(f"{OUT}/sud_fig7_difficulty.png", dpi=170)
|
| 169 |
+
print("wrote sud_fig7_difficulty.png")
|
| 170 |
+
json.dump({"by_clue": {int(k): float(a) for k, a in zip(ks, acc)},
|
| 171 |
+
"n_by_clue": {int(k): int(n) for k, n in zip(ks, nn)}},
|
| 172 |
+
open(os.path.join(OUT, "sud_by_clue.json"), "w"), indent=2)
|
| 173 |
+
|
| 174 |
+
# ---- Figure 8: benchmark difficulty, ours vs the saturated standard split ---
|
| 175 |
+
fig, ax = plt.subplots(figsize=(6.6, 4.0))
|
| 176 |
+
ax.bar(["SATNet split\n(~36 clues)", "ours\n(20–30 clues)"], [100.0, 3.0],
|
| 177 |
+
color=["#aab7b8", "#c0392b"], width=.55)
|
| 178 |
+
ax.set_ylabel("solved by constraint propagation alone (%)")
|
| 179 |
+
ax.set_title("Why we generated a new benchmark:\nthe standard split is saturated")
|
| 180 |
+
for i, v in enumerate([100.0, 3.0]):
|
| 181 |
+
ax.text(i, v + 2.5, f"{v:.1f}%", ha="center", fontweight="bold")
|
| 182 |
+
ax.set_ylim(0, 112); ax.grid(alpha=.3, axis="y")
|
| 183 |
+
fig.tight_layout(); fig.savefig(f"{OUT}/sud_fig8_saturation.png", dpi=170)
|
| 184 |
+
print("wrote sud_fig8_saturation.png")
|
src/train_sudoku.py
CHANGED
|
@@ -50,6 +50,10 @@ def get_args():
|
|
| 50 |
p.add_argument("--recurrent", action="store_true",
|
| 51 |
help="weight-shared block applied R times with deep supervision")
|
| 52 |
p.add_argument("--R", type=int, default=32, help="training recurrences")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
p.add_argument("--no_deep_sup", action="store_true",
|
| 54 |
help="ablation: supervise only the final recurrence")
|
| 55 |
p.add_argument("--R_infer", type=int, default=0,
|
|
@@ -106,7 +110,8 @@ blank_te = torch.from_numpy(Xte == 0)
|
|
| 106 |
|
| 107 |
if a.recurrent:
|
| 108 |
model = RecurrentDenoiser(len(tok), a.d, a.layers, a.heads, a.pe,
|
| 109 |
-
max_len=CELLS + 8, recurrences=a.R
|
|
|
|
| 110 |
print(f"params {model.n_params()/1e3:.0f}k RECURRENT R={a.R} "
|
| 111 |
f"layers={a.layers} d={a.d} mode={a.mode} pe={a.pe}", flush=True)
|
| 112 |
else:
|
|
|
|
| 50 |
p.add_argument("--recurrent", action="store_true",
|
| 51 |
help="weight-shared block applied R times with deep supervision")
|
| 52 |
p.add_argument("--R", type=int, default=32, help="training recurrences")
|
| 53 |
+
p.add_argument("--no_inject", action="store_true",
|
| 54 |
+
help="ablation: drop input injection, keeping architecture and "
|
| 55 |
+
"depth identical. Isolates weight sharing from injection, "
|
| 56 |
+
"which the 32-layer comparison conflates")
|
| 57 |
p.add_argument("--no_deep_sup", action="store_true",
|
| 58 |
help="ablation: supervise only the final recurrence")
|
| 59 |
p.add_argument("--R_infer", type=int, default=0,
|
|
|
|
| 110 |
|
| 111 |
if a.recurrent:
|
| 112 |
model = RecurrentDenoiser(len(tok), a.d, a.layers, a.heads, a.pe,
|
| 113 |
+
max_len=CELLS + 8, recurrences=a.R,
|
| 114 |
+
inject=not a.no_inject).to(dev)
|
| 115 |
print(f"params {model.n_params()/1e3:.0f}k RECURRENT R={a.R} "
|
| 116 |
f"layers={a.layers} d={a.d} mode={a.mode} pe={a.pe}", flush=True)
|
| 117 |
else:
|