Skip to content

Commit 6fd75de

Browse files
author
bryan
committed
Updated
1 parent a72357e commit 6fd75de

File tree

5 files changed

+136
-113
lines changed

5 files changed

+136
-113
lines changed

README.md

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Can Convolutional Neural Networks Crack Sudoku Puzzles?
22

3-
This project is motivated simply by my personal curiosity--can CNNs crack learn how to solve Sudoku? There are many computational approaches to do that. Why not neural networks?
3+
Sudoku is a popular number puzzle that requires you to fill blanks in a 9X9 grid with digits so that each column, each row, and each of the nine 3×3 subgrids contains all of the digits from 1 to 9. There have been various approaches to that, including computational ones. In this pilot project, we show convolutional neural networks have the potential to crack Sukoku puzzles without any other rule-based post-processing.
44

55
## Requirements
66
* numpy >= 1.11.1
@@ -10,68 +10,72 @@ This project is motivated simply by my personal curiosity--can CNNs crack learn
1010
Can Convolutional Neural Networks Crack Sudoku Puzzles?
1111

1212
## Background
13-
* To see what Sudoku is, check the wikipedia [here](https://en.wikipedia.org/wiki/Sudoku)
14-
* To investigate this task comprehensively, read through [McGuire et al. 2013](https://arxiv.org/pdf/1201.0749.pdf)
15-
16-
## Workflow
17-
* STEP 1. Generate [1M games of Sudoku](https://drive.google.com/open?id=0B0ZXk88koS2Ka0lVQWtBTUhWbUU). (=Y) (See `generate_sudoku.py`)<br/>
18-
* STEP 2. Make [1, 65] blanks randomly with uniform probabilities for every cell. (=X) (See `load_data` in `train.py`)<br/>
19-
* STEP 3. Build convolutional networks as follows. (See `Graph` in `train.py`)<br/>
20-
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;5 convolutional layers of 512 dimensions<br/>
21-
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1 final convolutional layer with a 1 by 1 filter.<br/>
22-
STEP 4. Train the model, feeding X and Y. Loss is calucated from the predictions for the blanks. (See `train.py`)<br/>
23-
STEP 5. Evaluate (See `test.py`).
13+
* To see what Sudoku is, check the [wikipedia](https://en.wikipedia.org/wiki/Sudoku)
14+
* To investigate this task comprehensively, read through [McGuire et al. 2013](https://arxiv.org/pdf/1201.0749.pdf).
15+
16+
## Training
17+
* STEP 1. Generate 1 million Sudoku games. (See `generate_sudoku.py`). The pre-generated games are available [here](https://www.kaggle.com/bryanpark/sudoku).
18+
* STEP 2. Construct convolutional networks as follows. (See `Graph` in `train.py`)<br/>
19+
![graph](graph.png?raw=true)
20+
* STEP 3. Train the model, feeding X (quizzes) and Y (solutions). Note that only the predictions for the position of the blanks count when computing loss. (See `train.py`)<br/>
21+
22+
## Evaluation
23+
We test the performance of the final model against 30 real Sudoku puzzles and their solutions, which vary from the easy to evil level.
2424

2525
## Results
26-
After 10 epochs, we got [this model file](https://drive.google.com/open?id=0B0ZXk88koS2KR0hETzI4dVdZV0k). Subsequently, we evaluated according to the following two methods.
26+
After 4 epochs, we got [the best model file](https://drive.google.com/open?id=0B0ZXk88koS2KV1VIT2RYUGhuOEU). We designed two test methods.
2727

2828
* Test method 1: Predict the numbers in blanks all at once.
29-
* Test method 2: Predict the numbers sequentially from the most confident one at each step.
29+
* Test method 2: Predict the numbers sequentially the most confident one at a time.
3030

3131

3232
| Level | Test1 <br/>(#correct/#blanks=acc.)| Test2 <br/>(#correct/#blanks=acc.) |
3333
| --- |--- |--- |
34-
|Easy|25/47=0.53|27/47=0.57|
35-
|Easy|26/45=0.58|29/45=0.64|
36-
|Easy|29/47=0.62|37/47=0.79|
37-
|Easy|24/45=0.53|24/45=0.53|
38-
|Easy|25/47=0.53|35/47=0.74|
39-
|Easy|23/46=0.50|30/46=0.65|
40-
|Medium|17/53=0.32|9/53=0.17|
41-
|Medium|20/55=0.36|13/55=0.24|
42-
|Medium|17/55=0.31|16/55=0.29|
43-
|Medium|25/53=0.47|39/53=0.74|
44-
|Medium|25/52=0.48|32/52=0.62|
45-
|Medium|28/56=0.50|12/56=0.21|
46-
|Hard|18/56=0.32|12/56=0.21|
47-
|Hard|19/55=0.35|14/55=0.25|
48-
|Hard|19/55=0.35|21/55=0.38|
49-
|Hard|22/57=0.39|16/57=0.28|
50-
|Hard|26/55=0.47|9/55=0.16|
51-
|Hard|25/56=0.45|36/56=0.64|
52-
|Expert|21/56=0.38|19/56=0.34|
53-
|Expert|22/55=0.40|25/55=0.45|
54-
|Expert|20/54=0.37|12/54=0.22|
55-
|Expert|25/55=0.45|25/55=0.45|
56-
|Expert|23/55=0.42|20/55=0.36|
57-
|Expert|24/54=0.44|19/54=0.35|
58-
|Evil|28/50=0.56|38/50=0.76|
59-
|Evil|20/50=0.40|26/50=0.52|
60-
|Evil|26/49=0.53|29/49=0.59|
61-
|Evil|21/53=0.40|17/53=0.32|
62-
|Evil|23/51=0.45|15/51=0.29|
63-
|Evil|26/51=0.51|16/51=0.31|
64-
Total Accuracy| 692/1568=0.44|672/1568=0.43|
34+
|Easy|43/47=0.91|**47/47=1.00**|
35+
|Easy|37/45=0.82|**45/45=1.00**|
36+
|Easy|40/47=0.85|**47/47=1.00**|
37+
|Easy|33/45=0.73|**45/45=1.00**|
38+
|Easy|37/47=0.79|**47/47=1.00**|
39+
|Easy|39/46=0.85|**46/46=1.00**|
40+
|Medium|27/53=0.51|32/53=0.60|
41+
|Medium|27/55=0.49|27/55=0.49|
42+
|Medium|32/55=0.58|36/55=0.65|
43+
|Medium|28/53=0.53|**53/53=1.00**|
44+
|Medium|27/52=0.52|33/52=0.63|
45+
|Medium|29/56=0.52|39/56=0.70|
46+
|Hard|30/56=0.54|41/56=0.73|
47+
|Hard|31/55=0.56|28/55=0.51|
48+
|Hard|33/55=0.60|**55/55=1.00**|
49+
|Hard|33/57=0.58|**57/57=1.00**|
50+
|Hard|27/55=0.49|50/55=0.91|
51+
|Hard|28/56=0.50|27/56=0.48|
52+
|Expert|32/56=0.57|22/56=0.39|
53+
|Expert|32/55=0.58|**55/55=1.00**|
54+
|Expert|37/54=0.69|**54/54=1.00**|
55+
|Expert|33/55=0.60|**55/55=1.00**|
56+
|Expert|30/55=0.55|23/55=0.42|
57+
|Expert|25/54=0.46|**54/54=1.00**|
58+
|Evil|32/50=0.64|**50/50=1.00**|
59+
|Evil|33/50=0.66|**50/50=1.00**|
60+
|Evil|34/49=0.69|**49/49=1.00**|
61+
|Evil|33/53=0.62|**53/53=1.00**|
62+
|Evil|35/51=0.69|**51/51=1.00**|
63+
|Evil|34/51=0.67|**51/51=1.00**|
64+
|Total Accuracy| 971/1568=0.62| **1322/1568=0.84**|
65+
|Success Rate| 0/30=0| **19/30=0.63**|
6566

6667
## Conclusions
6768
* I also tested fully connected layers, to no avail.
6869
* Up to some point, it seems that CNNs can learn to solve Sudoku.
69-
* For some problems, the second method was more effective than the first method. But I can't figure out more about that.
70-
* Probably reinforcement learning would be more appropriate for Sudoku solving.
70+
* For most problems, the second method was outperfrom the fist one.
71+
* Humans cannot predict all numbers simultaneously. Probably so do CNNs.
72+
73+
## Furthery Study
74+
* Reinforcement learning would be more appropriate for Sudoku solving.
7175

7276
## Notes for reproducibility
73-
* Download pre-generated Sudoku games [here](https://drive.google.com/open?id=0B0ZXk88koS2Ka0lVQWtBTUhWbUU) and extract it to `data/` folder.
74-
* Download pre-trained model file [here](https://drive.google.com/open?id=0B0ZXk88koS2KR0hETzI4dVdZV0k) and extract it to `asset/train/ckpt` folder.
77+
* Download pre-generated Sudoku games [here](https://www.kaggle.com/bryanpark/sudoku) and extract it to `data/` folder.
78+
* Download the pre-trained model file [here](https://drive.google.com/open?id=0B0ZXk88koS2KV1VIT2RYUGhuOEU) and extract it to `asset/train/ckpt` folder.
7579

7680

7781

generate_sudoku.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
1+
#!/usr/bin/python2
22
"""
33
This is adapted from https://www.ocf.berkeley.edu/~arel/sudoku/main.html.
4-
Kyubyong Park.
4+
Generates 1 million Sudoku games.
5+
Kyubyong Park. [email protected] www.github.com/kyubyong
56
"""
67

78
import random, copy
@@ -139,16 +140,14 @@ def run(n = 28, iter=100):
139140
# print "* creating the solution..."
140141
a_puzzle_solution = construct_puzzle_solution()
141142

142-
return a_puzzle_solution
143-
144143
# print "* constructing a puzzle..."
145-
# for i in range(iter):
146-
# puzzle = copy.deepcopy(a_puzzle_solution)
147-
# (result, number_of_cells) = pluck(puzzle, n)
148-
# all_results.setdefault(number_of_cells, []).append(result)
149-
# if number_of_cells <= n: break
150-
#
151-
# return all_results
144+
for i in range(iter):
145+
puzzle = copy.deepcopy(a_puzzle_solution)
146+
(result, number_of_cells) = pluck(puzzle, n)
147+
all_results.setdefault(number_of_cells, []).append(result)
148+
if number_of_cells <= n: break
149+
150+
return all_results, a_puzzle_solution
152151

153152
def best(set_of_puzzles):
154153
# Could run some evaluation function here. For now just pick
@@ -170,13 +169,18 @@ def main(num):
170169
'''
171170
Generates `num` games of Sudoku.
172171
'''
173-
Y = np.zeros((num, 9, 9), np.int32)
172+
quizzes = np.zeros((num, 9, 9), np.int32)
173+
solutions = np.zeros((num, 9, 9), np.int32)
174174
for i in range(num):
175-
game = np.array(run())
176-
Y[i] = game
175+
all_results, solution = run(n=23, iter=10)
176+
quiz = best(all_results)
177+
178+
quizzes[i] = quiz
179+
solutions[i] = solution
180+
177181
if (i+1) % 1000 == 0:
178182
print i+1
179-
np.save('data/sudoku.npy', Y)
183+
np.save('data/sudoku.npz', quizzes=quizzes, solutions=solutions)
180184

181185
if __name__ == "__main__":
182186
main(1000000)

graph.png

21.6 KB
Loading

test.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
# -*- coding: utf-8 -*-
2+
'''
3+
Test the performance of the model.
4+
'''
25
import sugartensor as tf
36
import numpy as np
47
from train import Graph
58

9+
# Test sets
10+
# 6 * Easy + 6 * Medium + 6 * Hard + 6 * Expert + 6 * Evil
11+
# From http://1sudoku.com/print/print-sudoku-free/
12+
613
problems = '''\
714
080032001
815
703080002
@@ -605,13 +612,14 @@
605612
438269517
606613
619875423'''
607614

608-
def data_process():
609-
# Convert problem and solution sets to the proper format
615+
def preprocess():
616+
'''Converts problem and solution sets to the proper format
617+
'''
610618
global problems, solutions
611619

612620
nproblems = len(problems.strip().split("\n\n"))
613621
X = np.zeros((nproblems, 9, 9), np.float32)
614-
Y = np.zeros((nproblems, 9, 9), np.float32)
622+
Y = np.zeros((nproblems, 9, 9), np.int32)
615623

616624
for i, prob in enumerate(problems.strip().split('\n\n')):
617625
for j, row in enumerate(prob.splitlines()):
@@ -631,7 +639,7 @@ def test1():
631639
'''
632640
Predicts all at once.
633641
'''
634-
X, Y = data_process()
642+
X, Y = preprocess()
635643
g = Graph(is_train=False)
636644

637645
with tf.Session() as sess:
@@ -668,7 +676,7 @@ def test2():
668676
'''
669677
Predicts sequentially.
670678
'''
671-
X, Y = data_process()
679+
X, Y = preprocess()
672680
g = Graph(is_train=False)
673681

674682
with tf.Session() as sess:

train.py

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,90 +5,97 @@
55
# set log level to debug
66
tf.sg_verbosity(10)
77

8+
class Hyperparams:
9+
batch_size = 64
10+
811
def load_data(is_train=True):
9-
Y = np.load('data/sudoku.npy') # solutions
12+
'''Loads training / validation data.
1013
11-
X = np.zeros_like(Y, dtype=np.float32)
12-
for i, y in enumerate(Y): # game-wise
13-
nblanks = np.random.randint(1, 65) # We generate a problem which varies from 1 to 65 in number of blanks.
14-
blank_indices = np.random.choice(81, nblanks)
15-
masks= np.ones((9*9))
16-
masks[blank_indices] = 0
17-
masks = masks.reshape((9, 9))
18-
19-
x = y * masks # puzzle. 0: blanks=targets.
20-
X[i] = x
14+
Args
15+
is_train: Boolean. If True, it loads training data.
16+
Otherwise, it loads validation data.
17+
18+
Returns:
19+
X: 4-D array of float. Has the shape of (# total games, 9, 9, 1) (for train)
20+
or (batch_size, 9, 9, 1) (for validation)
21+
Y: 3-D array of int. Has the shape of (# total games, 9, 9) (for train)
22+
or (batch_size, 9, 9) (for validation)
23+
'''
24+
X = np.load('data/sudoku.npz')['quizzes'].astype(np.float32)
25+
Y = np.load('data/sudoku.npz')['solutions']
2126

2227
X = np.expand_dims(X, -1)
2328

2429
if is_train:
25-
return X[:-100], Y[:-100] # training data
30+
return X[:-Hyperparams.batch_size], Y[:-Hyperparams.batch_size] # training data
2631
else:
27-
return X[-100:], Y[-100:] # validation data
32+
return X[-Hyperparams.batch_size:], Y[-Hyperparams.batch_size:] # validation data
33+
34+
def get_batch_data(is_train=True):
35+
'''Returns batch data.
2836
29-
def get_batch_data(is_train=True, batch_size=16):
30-
'''
3137
Args:
32-
is_train: Boolean. If True, load training data. Otherwise, load validation data.
38+
is_train: Boolean. If True, it returns batch training data.
39+
Otherwise, batch validation data.
40+
3341
Returns:
34-
A Tuple of X batch queues (Tensor), Y batch queues (Tensor), and number of batches (int)
42+
A Tuple of x, y, and num_batch
43+
x: A `Tensor` of float. Has the shape of (batch_size, 9, 9, 1).
44+
y: A `Tensor` of int. Has the shape of (batch_size, 9, 9).
45+
num_batch = A Python int. Number of batches.
3546
'''
36-
# Load data
3747
X, Y = load_data(is_train=is_train)
3848

3949
# Create Queues
4050
input_queues = tf.train.slice_input_producer([tf.convert_to_tensor(X),
4151
tf.convert_to_tensor(Y)])
4252

4353
# create batch queues
44-
X_batch, Y_batch = tf.train.shuffle_batch(input_queues,
45-
num_threads=8,
46-
batch_size=batch_size,
47-
capacity=batch_size*64,
48-
min_after_dequeue=batch_size*32,
49-
allow_smaller_final_batch=False)
54+
x, y = tf.train.shuffle_batch(input_queues,
55+
num_threads=8,
56+
batch_size=Hyperparams.batch_size,
57+
capacity=Hyperparams.batch_size*64,
58+
min_after_dequeue=Hyperparams.batch_size*32,
59+
allow_smaller_final_batch=False)
5060
# calc total batch count
5161
num_batch = len(X) // batch_size
5262

53-
return X_batch, Y_batch, num_batch # (16, 9, 9, 1) int32. cf. Y_batch: (16, 9, 9) int32
63+
return x, y, num_batch # (64, 9, 9, 1), (64, 9, 9), ()
5464

5565
class Graph(object):
5666
def __init__(self, is_train=True):
5767
# inputs
5868
if is_train:
59-
self.X, self.Y, self.num_batch = get_batch_data() # (16, 9, 9, 1), (16, 9, 9)
60-
self.X_val, self.Y_val, _ = get_batch_data(is_train=False)
69+
self.x, self.y, self.num_batch = get_batch_data()
70+
self.x_val, self.y_val, _ = get_batch_data(is_train=False)
6171
else:
62-
self.X = tf.placeholder(tf.float32, [None, 9, 9, 1])
72+
self.x = tf.placeholder(tf.float32, [None, 9, 9, 1])
6373

6474
with tf.sg_context(size=3, act='relu', bn=True):
65-
self.logits = self.X.sg_identity()
66-
for _ in range(5):
75+
self.logits = self.x.sg_identity()
76+
for _ in range(10):
6777
self.logits = (self.logits.sg_conv(dim=512))
68-
self.logits = self.logits.sg_conv(dim=10, size=1, act='linear', bn=False) # (16, 9, 9, 10) float32
78+
79+
self.logits = self.logits.sg_conv(dim=10, size=1, act='linear', bn=False)
6980

7081
if is_train:
71-
self.ce = self.logits.sg_ce(target=self.Y, mask=False) # (16, 9, 9) dtype=float32
72-
self.istarget = tf.equal(self.X.sg_squeeze(), tf.zeros_like(self.X.sg_squeeze())).sg_float() # zeros: 1, non-zeros: 0 (16, 9, 9) dtype=float32
73-
self.loss = self.ce * self.istarget # (16, 9, 9) dtype=float32
82+
self.ce = self.logits.sg_ce(target=self.y, mask=False)
83+
self.istarget = tf.equal(self.x.sg_squeeze(), tf.zeros_like(self.x.sg_squeeze())).sg_float()
84+
self.loss = self.ce * self.istarget
7485
self.reduced_loss = self.loss.sg_sum() / self.istarget.sg_sum()
7586
tf.sg_summary_loss(self.reduced_loss, "reduced_loss")
7687

77-
# accuracy evaluation ( for train set )
78-
self.preds = (self.logits.sg_argmax()).sg_int()
79-
self.hits = tf.equal(self.preds, self.Y).sg_float()
80-
self.acc_train = (self.hits * self.istarget).sg_sum() / self.istarget.sg_sum()
81-
8288
# accuracy evaluation ( for validation set )
83-
self.preds_ = (self.logits.sg_reuse(input=self.X_val).sg_argmax()).sg_int()
84-
self.hits_ = tf.equal(self.preds_, self.Y_val).sg_float()
85-
self.istarget_ = tf.equal(self.X_val.sg_squeeze(), tf.zeros_like(self.X_val.sg_squeeze())).sg_float()
86-
self.acc_val = (self.hits_ * self.istarget_).sg_sum() / self.istarget_.sg_sum()
89+
self.preds_ = (self.logits.sg_reuse(input=self.x_val).sg_argmax()).sg_int()
90+
self.hits_ = tf.equal(self.preds_, self.y_val).sg_float()
91+
self.istarget_ = tf.equal(self.x_val.sg_squeeze(), tf.zeros_like(self.x_val.sg_squeeze())).sg_float()
92+
self.acc = (self.hits_ * self.istarget_).sg_sum() / self.istarget_.sg_sum()
8793

8894
def main():
8995
g = Graph()
9096

91-
tf.sg_train(log_interval=10, loss=g.reduced_loss, eval_metric=[g.acc_train, g.acc_val],
97+
tf.sg_train(lr=0.0001, lr_reset=True, log_interval=10, save_interval=300,
98+
loss=g.reduced_loss, eval_metric=[g.acc],
9299
ep_size=g.num_batch, save_dir='asset/train', max_ep=10, early_stop=False)
93100

94101
if __name__ == "__main__":

0 commit comments

Comments
 (0)