├── .gitattributes
├── LICENSE
├── README.md
├── docker
├── Dockerfile
├── build.sh
└── run.sh
├── exps
├── parity.py
└── sudoku.py
├── images
├── forward_pass.png
├── mnist_sudoku.png
└── poster_forward.png
├── notebooks
└── Learning and Solving Sudoku via SATNet.ipynb
├── requirements.txt
├── satnet
├── __init__.py
└── models.py
├── setup.py
└── src
├── satnet.cpp
├── satnet.h
├── satnet_cpu.cpp
└── satnet_cuda.cu
/.gitattributes:
--------------------------------------------------------------------------------
1 | notebooks/*.ipynb linguist-documentation
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Po-Wei Wang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SATNet • [![PyPi][pypi-image]][pypi] [![colab][colab-image]][colab] [![License][license-image]][license]
2 |
3 | [license-image]: https://img.shields.io/badge/License-MIT-yellow.svg
4 | [license]: LICENSE
5 |
6 | [pypi-image]: https://img.shields.io/pypi/v/satnet.svg
7 | [pypi]: https://pypi.python.org/pypi/satnet
8 |
9 | [colab-image]: https://colab.research.google.com/assets/colab-badge.svg
10 | [colab]: https://colab.research.google.com/drive/1dRfepPLEE8N6BBZhXz8bbLDcPnRKaOcJ#forceEdit=true&offline=true&sandboxMode=true
11 |
12 | *Bridging deep learning and logical reasoning using a differentiable satisfiability solver.*
13 |
14 | This repository contains the source code to reproduce the experiments in the ICML 2019 paper [SATNet: Bridging deep learning and logical reasoning using a differentiable satisfiability solver](https://arxiv.org/abs/1905.12149) by [Po-Wei Wang](https://powei.tw/), [Priya L. Donti](https://priyadonti.com/), [Bryan Wilder](http://teamcore.usc.edu/people/bryanwilder/default.htm), and [J. Zico Kolter](http://zicokolter.com/).
15 |
16 |
17 | ## What is SATNet
18 |
19 | SATNet is a differentiable (smoothed) maximum satisfiability (MAXSAT) solver that can be integrated into the loop of larger deep learning systems. This (approximate) solver is based upon a fast coordinate descent approach to solving the semidefinite program (SDP) associated with the MAXSAT problem.
20 |
21 | #### How SATNet works
22 |
23 | A SATNet layer takes as input the discrete or probabilistic assignments of known MAXSAT variables, and outputs guesses for the assignments of unknown variables via a MAXSAT SDP relaxation with weights *S*. A schematic depicting the forward pass of this layer is shown below. To obtain the backward pass, we analytically differentiate through the SDP relaxation (see the paper for more details).
24 |
25 | 
26 |
27 | #### Overview of experiments
28 |
29 | We show that by integrating SATNet into end-to-end learning systems, we can learn the logical structure of challenging problems in a minimally supervised fashion. In particular, we show that we can:
30 | * Learn the **parity function** using single-bit supervision (a traditionally hard task for deep networks)
31 | * Learn how to play **9×9 Sudoku (original and permuted)** solely from examples.
32 | * Solve a **"visual Sudoku"** problem that maps images of Sudoku puzzles to their associated logical solutions. (A sample "visual Sudoku" input is shown below.)
33 |
34 |
35 |
36 |
37 |
38 | ## Installation
39 |
40 | ### Via pip
41 | ```bash
42 | pip install satnet
43 | ```
44 |
45 |
46 | ### From source
47 | ```bash
48 | git clone https://github.com/locuslab/SATNet
49 | cd SATNet && python setup.py install
50 | ```
51 |
52 | #### Package Dependencies
53 | ```
54 | conda install -c pytorch tqdm
55 | ```
56 | The package also depends on the nvcc compiler. If it doesn't exist (try nvcc from commandline), you can install it via
57 | ```
58 | conda install -c conda-forge cudatoolkit-dev
59 | ```
60 |
61 |
62 |
63 | ### Via Docker image
64 | ```bash
65 | cd docker
66 | sh ./build.sh
67 | sh ./run.sh
68 | ```
69 |
70 | ## Running experiments
71 | ### Jupyter Notebook and Google Colab
72 | [Jupyter notebook](https://github.com/locuslab/SATNet/blob/master/notebooks/Learning%20and%20Solving%20Sudoku%20via%20SATNet.ipynb)
73 | and [Google Colab](https://colab.research.google.com/drive/1dRfepPLEE8N6BBZhXz8bbLDcPnRKaOcJ#forceEdit=true&offline=true&sandboxMode=true)
74 |
75 | ### Run them manually
76 |
77 | #### Getting the datasets
78 | The [Sudoku dataset](https://powei.tw/sudoku.zip) and [Parity dataset](https://powei.tw/parity.zip) can be downloaded via
79 |
80 | ```bash
81 | wget -cq powei.tw/sudoku.zip && unzip -qq sudoku.zip
82 | wget -cq powei.tw/parity.zip && unzip -qq parity.zip
83 | ```
84 | #### Sudoku experiments (original, permuted, and visual)
85 | ```bash
86 | python exps/sudoku.py
87 | python exps/sudoku.py --perm
88 | python exps/sudoku.py --mnist --batchSz=50
89 | ```
90 |
91 | #### Parity experiments
92 | ```bash
93 | python exps/parity.py --seq=20
94 | python exps/parity.py --seq=40
95 | ```
96 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM pytorch/pytorch:1.13.0-cuda11.6-cudnn8-devel
2 | RUN pip install setproctitle
3 |
4 | ARG USER_ID
5 | ARG GROUP_ID
6 | ARG USER_NAME
7 | ARG HOME_DIR
8 |
9 | RUN addgroup --gid ${GROUP_ID} ${USER_NAME} || groupmod -n ${USER_NAME} $(getent group ${GROUP_ID})
10 | RUN apt-get -q update; apt-get -q -y install sudo vim
11 | RUN conda install -y -q jupyter matplotlib
12 | RUN adduser --quiet --disabled-password --system --no-create-home --uid ${USER_ID} --gid ${GROUP_ID} --gecos '' --shell /bin/bash ${USER_NAME}
13 | RUN usermod -d ${HOME_DIR} ${USER_NAME}
14 | RUN adduser --quiet ${USER_NAME} sudo ; echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
15 | RUN apt-get install -y git
16 |
17 | RUN mkdir -p /data
18 | WORKDIR /data
19 | USER ${USER_NAME}
20 |
--------------------------------------------------------------------------------
/docker/build.sh:
--------------------------------------------------------------------------------
1 | docker image build \
2 | --build-arg USER_ID=$(id -u ${USER}) \
3 | --build-arg GROUP_ID=$(id -g ${USER}) \
4 | --build-arg USER_NAME=$(whoami) \
5 | --build-arg HOME_DIR=$HOME \
6 | -t satnet .
7 |
--------------------------------------------------------------------------------
/docker/run.sh:
--------------------------------------------------------------------------------
1 | DATA_VOLUME="-v $(pwd)/..:/data"
2 | HOME_VOLUME="-v $HOME:$HOME"
3 | docker run --rm --runtime=nvidia -it --net=host --ipc=host ${DATA_VOLUME} ${HOME_VOLUME} --name=satnet satnet
4 |
--------------------------------------------------------------------------------
/exps/parity.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import argparse
4 |
5 | import os
6 | import sys
7 | import csv
8 | import shutil
9 |
10 | import numpy.random as npr
11 |
12 | import torch
13 | import torch.optim as optim
14 | import torch.nn.functional as F
15 | from torch.utils.data import TensorDataset, DataLoader
16 |
17 | import satnet
18 | from tqdm.auto import tqdm
19 |
20 | class CSVLogger(object):
21 | def __init__(self, fname):
22 | self.f = open(fname, 'w')
23 | self.logger = csv.writer(self.f)
24 |
25 | def log(self, fields):
26 | self.logger.writerow(fields)
27 | self.f.flush()
28 |
29 | def main():
30 | parser = argparse.ArgumentParser()
31 | parser.add_argument('--data_dir', type=str, default='parity')
32 | parser.add_argument('--testPct', type=float, default=0.1)
33 | parser.add_argument('--batchSz', type=int, default=100)
34 | parser.add_argument('--testBatchSz', type=int, default=500)
35 | parser.add_argument('--nEpoch', type=int, default=100)
36 | parser.add_argument('--lr', type=float, default=1e-1)
37 | parser.add_argument('--seq', type=int, default=20)
38 | parser.add_argument('--save', type=str)
39 | parser.add_argument('--m', type=int, default=4)
40 | parser.add_argument('--aux', type=int, default=4)
41 | parser.add_argument('--no_cuda', action='store_true')
42 | parser.add_argument('--adam', action='store_true')
43 |
44 | args = parser.parse_args()
45 |
46 | # For debugging: fix the random seed
47 | npr.seed(1)
48 | torch.manual_seed(7)
49 |
50 | args.cuda = not args.no_cuda and torch.cuda.is_available()
51 | if args.cuda:
52 | print('Using', torch.cuda.get_device_name(0))
53 | torch.backends.cudnn.deterministic = True
54 | torch.backends.cudnn.benchmark = False
55 | torch.cuda.init()
56 |
57 | save = 'parity.aux{}-m{}-lr{}-bsz{}'.format(
58 | args.aux, args.m, args.lr, args.batchSz)
59 |
60 | if args.save: save = '{}-{}'.format(args.save, save)
61 | save = os.path.join('logs', save)
62 | if os.path.isdir(save): shutil.rmtree(save)
63 | os.makedirs(save)
64 |
65 | L = args.seq
66 |
67 | with open(os.path.join(args.data_dir, str(L), 'features.pt'), 'rb') as f:
68 | X = torch.load(f).float()
69 | with open(os.path.join(args.data_dir, str(L), 'labels.pt'), 'rb') as f:
70 | Y = torch.load(f).float()
71 |
72 | if args.cuda: X, Y = X.cuda(), Y.cuda()
73 |
74 | N = X.size(0)
75 |
76 | nTrain = int(N*(1-args.testPct))
77 | nTest = N-nTrain
78 |
79 | assert(nTrain % args.batchSz == 0)
80 | assert(nTest % args.testBatchSz == 0)
81 |
82 | train_is_input = torch.IntTensor([1,1,0]).repeat(nTrain,1)
83 | test_is_input = torch.IntTensor([1,1,0]).repeat(nTest,1)
84 | if args.cuda: train_is_input, test_is_input = train_is_input.cuda(), test_is_input.cuda()
85 |
86 | train_set = TensorDataset(X[:nTrain], train_is_input, Y[:nTrain])
87 | test_set = TensorDataset(X[nTrain:], test_is_input, Y[nTrain:])
88 |
89 | model = satnet.SATNet(3, args.m, args.aux, prox_lam=1e-1)
90 | if args.cuda: model = model.cuda()
91 |
92 | if args.adam:
93 | optimizer = optim.Adam(model.parameters(), lr=args.lr)
94 | else:
95 | optimizer = optim.SGD(model.parameters(), lr=args.lr)
96 |
97 | train_logger = CSVLogger(os.path.join(save, 'train.csv'))
98 | test_logger = CSVLogger(os.path.join(save, 'test.csv'))
99 | fields = ['epoch', 'loss', 'err']
100 | train_logger.log(fields)
101 | test_logger.log(fields)
102 |
103 | test(0, model, optimizer, test_logger, test_set, args.testBatchSz)
104 | for epoch in range(1, args.nEpoch+1):
105 | train(epoch, model, optimizer, train_logger, train_set, args.batchSz)
106 | test(epoch, model, optimizer, test_logger, test_set, args.testBatchSz)
107 |
108 | def apply_seq(net, zeros, batch_data, batch_is_inputs, batch_targets):
109 | y = torch.cat([batch_data[:,:2], zeros], dim=1)
110 | y = net(y, batch_is_inputs)
111 | L = batch_data.size(1)
112 | for i in range(L-2):
113 | y = torch.cat([y[:,-1].unsqueeze(1), batch_data[:,i+2].unsqueeze(1), zeros], dim=1)
114 | y = net(((y-0.5).sign()+1)/2, batch_is_inputs)
115 | loss = F.binary_cross_entropy(y[:,-1], batch_targets[:,-1])
116 | return loss, y
117 |
118 | def run(epoch, model, optimizer, logger, dataset, batchSz, to_train):
119 | loss_final, err_final = 0, 0
120 |
121 | loader = DataLoader(dataset, batch_size=batchSz)
122 | tloader = tqdm(enumerate(loader), total=len(loader))
123 |
124 | start = torch.zeros(batchSz, 1)
125 | if next(model.parameters()).is_cuda: start = start.cuda()
126 |
127 | for i,(data,is_input, label) in tloader:
128 | if to_train: optimizer.zero_grad()
129 |
130 | loss, pred = apply_seq(model, start, data, is_input, label)
131 |
132 | if to_train:
133 | loss.backward()
134 | optimizer.step()
135 |
136 | err = computeErr(pred, label)
137 | tloader.set_description('Epoch {} {} Loss {:.4f} Err: {:.4f}'.format(
138 | epoch, ('Train' if to_train else 'Test '), loss.item(), err))
139 | loss_final += loss.item()
140 | err_final += err
141 |
142 | loss_final, err_final = loss_final/len(loader), err_final/len(loader)
143 | logger.log((epoch, loss_final, err_final))
144 |
145 | if not to_train:
146 | print('TESTING SET RESULTS: Average loss: {:.4f} Err: {:.4f}'.format(loss_final, err_final))
147 |
148 | def train(epoch, model, optimizer, logger, dataset, batchSz):
149 | run(epoch, model, optimizer, logger, dataset, batchSz, True)
150 |
151 | @torch.no_grad()
152 | def test(epoch, model, optimizer, logger, dataset, batchSz):
153 | run(epoch, model, optimizer, logger, dataset, batchSz, False)
154 |
155 | @torch.no_grad()
156 | def computeErr(pred, target):
157 | y = (pred[:,-1]-0.5)
158 | t = (target[:,-1]-0.5)
159 | correct = ((y * t).sign()+1.)/2
160 | acc = correct.sum().float()/target.size(0)
161 |
162 | return 1-float(acc)
163 |
164 | if __name__ == '__main__':
165 | main()
166 |
--------------------------------------------------------------------------------
/exps/sudoku.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | #
3 | # Partly derived from:
4 | # https://github.com/locuslab/optnet/blob/master/sudoku/train.py
5 |
6 | import argparse
7 |
8 | import os
9 | import shutil
10 | import csv
11 |
12 | import numpy as np
13 | import numpy.random as npr
14 | #import setproctitle
15 |
16 | import torch
17 | import torch.nn as nn
18 | import torch.optim as optim
19 | import torch.nn.functional as F
20 | from torch.utils.data import TensorDataset, DataLoader
21 | from tqdm.auto import tqdm
22 |
23 | import satnet
24 |
25 | class SudokuSolver(nn.Module):
26 | def __init__(self, boardSz, aux, m):
27 | super(SudokuSolver, self).__init__()
28 | n = boardSz**6
29 | self.sat = satnet.SATNet(n, m, aux)
30 |
31 | def forward(self, y_in, mask):
32 | out = self.sat(y_in, mask)
33 | return out
34 |
35 | class DigitConv(nn.Module):
36 | '''
37 | Convolutional neural network for MNIST digit recognition. From:
38 | https://github.com/pytorch/examples/blob/master/mnist/main.py
39 | '''
40 | def __init__(self):
41 | super(DigitConv, self).__init__()
42 | self.conv1 = nn.Conv2d(1, 20, 5, 1)
43 | self.conv2 = nn.Conv2d(20, 50, 5, 1)
44 | self.fc1 = nn.Linear(4*4*50, 500)
45 | self.fc2 = nn.Linear(500, 10)
46 |
47 | def forward(self, x):
48 | x = F.relu(self.conv1(x))
49 | x = F.max_pool2d(x, 2, 2)
50 | x = F.relu(self.conv2(x))
51 | x = F.max_pool2d(x, 2, 2)
52 | x = x.view(-1, 4*4*50)
53 | x = F.relu(self.fc1(x))
54 | x = self.fc2(x)
55 | return F.softmax(x, dim=1)[:,:9].contiguous()
56 |
57 | class MNISTSudokuSolver(nn.Module):
58 | def __init__(self, boardSz, aux, m):
59 | super(MNISTSudokuSolver, self).__init__()
60 | self.digit_convnet = DigitConv()
61 | self.sudoku_solver = SudokuSolver(boardSz, aux, m)
62 | self.boardSz = boardSz
63 | self.nSq = boardSz**2
64 |
65 | def forward(self, x, is_inputs):
66 | nBatch = x.shape[0]
67 | x = x.flatten(start_dim = 0, end_dim = 1)
68 | digit_guess = self.digit_convnet(x)
69 | puzzles = digit_guess.view(nBatch, self.nSq * self.nSq * self.nSq)
70 |
71 | solution = self.sudoku_solver(puzzles, is_inputs)
72 | return solution
73 |
74 | class CSVLogger(object):
75 | def __init__(self, fname):
76 | self.f = open(fname, 'w')
77 | self.logger = csv.writer(self.f)
78 |
79 | def log(self, fields):
80 | self.logger.writerow(fields)
81 | self.f.flush()
82 |
83 | class FigLogger(object):
84 | def __init__(self, fig, base_ax, title):
85 | self.colors = ['tab:red', 'tab:blue']
86 | self.labels = ['Loss (entropy)', 'Error']
87 | self.markers = ['d', '.']
88 | self.axes = [base_ax, base_ax.twinx()]
89 | base_ax.set_xlabel('Epochs')
90 | base_ax.set_title(title)
91 |
92 | for i, ax in enumerate(self.axes):
93 | ax.set_ylabel(self.labels[i], color=self.colors[i])
94 | ax.tick_params(axis='y', labelcolor=self.colors[i])
95 |
96 | self.reset()
97 | self.fig = fig
98 |
99 | def log(self, args):
100 | for i, arg in enumerate(args[-2:]):
101 | self.curves[i].append(arg)
102 | x = list(range(len(self.curves[i])))
103 | self.axes[i].plot(x, self.curves[i], self.colors[i], marker=self.markers[i])
104 | self.axes[i].set_ylim(0, 1.05)
105 |
106 | self.fig.canvas.draw()
107 |
108 | def reset(self):
109 | for ax in self.axes:
110 | for line in ax.lines:
111 | line.remove()
112 | self.curves = [[], []]
113 |
114 | def print_header(msg):
115 | print('===>', msg)
116 |
117 | def find_unperm(perm):
118 | unperm = torch.zeros_like(perm)
119 | for i in range(perm.size(0)):
120 | unperm[perm[i]] = i
121 | return unperm
122 |
123 | def main():
124 | parser = argparse.ArgumentParser()
125 | parser.add_argument('--data_dir', type=str, default='sudoku')
126 | parser.add_argument('--boardSz', type=int, default=3)
127 | parser.add_argument('--batchSz', type=int, default=40)
128 | parser.add_argument('--testBatchSz', type=int, default=40)
129 | parser.add_argument('--aux', type=int, default=300)
130 | parser.add_argument('--m', type=int, default=600)
131 | parser.add_argument('--nEpoch', type=int, default=100)
132 | parser.add_argument('--testPct', type=float, default=0.1)
133 | parser.add_argument('--lr', type=float, default=2e-3)
134 | parser.add_argument('--save', type=str)
135 | parser.add_argument('--model', type=str)
136 | parser.add_argument('--no_cuda', action='store_true')
137 | parser.add_argument('--mnist', action='store_true')
138 | parser.add_argument('--perm', action='store_true')
139 |
140 | args = parser.parse_args()
141 |
142 | # For debugging: fix the random seed
143 | npr.seed(1)
144 | torch.manual_seed(7)
145 |
146 | args.cuda = not args.no_cuda and torch.cuda.is_available()
147 | if args.cuda:
148 | print('Using', torch.cuda.get_device_name(0))
149 | torch.backends.cudnn.deterministic = True
150 | torch.backends.cudnn.benchmark = False
151 | torch.cuda.init()
152 |
153 | save = 'sudoku{}{}.boardSz{}-aux{}-m{}-lr{}-bsz{}'.format(
154 | '.perm' if args.perm else '', '.mnist' if args.mnist else '',
155 | args.boardSz, args.aux, args.m, args.lr, args.batchSz)
156 | if args.save: save = '{}-{}'.format(args.save, save)
157 | save = os.path.join('logs', save)
158 | if os.path.isdir(save): shutil.rmtree(save)
159 | os.makedirs(save)
160 |
161 | #setproctitle.setproctitle('sudoku.{}'.format(save))
162 |
163 | print_header('Loading data')
164 |
165 | with open(os.path.join(args.data_dir, 'features.pt'), 'rb') as f:
166 | X_in = torch.load(f)
167 | with open(os.path.join(args.data_dir, 'features_img.pt'), 'rb') as f:
168 | Ximg_in = torch.load(f)
169 | with open(os.path.join(args.data_dir, 'labels.pt'), 'rb') as f:
170 | Y_in = torch.load(f)
171 | with open(os.path.join(args.data_dir, 'perm.pt'), 'rb') as f:
172 | perm = torch.load(f)
173 |
174 | N = X_in.size(0)
175 | nTrain = int(N*(1.-args.testPct))
176 | nTest = N-nTrain
177 | assert(nTrain % args.batchSz == 0)
178 | assert(nTest % args.testBatchSz == 0)
179 |
180 | print_header('Forming inputs')
181 | X, Ximg, Y, is_input = process_inputs(X_in, Ximg_in, Y_in, args.boardSz)
182 | data = Ximg if args.mnist else X
183 | if args.cuda: data, is_input, Y = data.cuda(), is_input.cuda(), Y.cuda()
184 |
185 | unperm = None
186 | if args.perm and not args.mnist:
187 | print('Applying permutation')
188 | data[:,:], Y[:,:], is_input[:,:] = data[:,perm], Y[:,perm], is_input[:,perm]
189 | unperm = find_unperm(perm)
190 |
191 | train_set = TensorDataset(data[:nTrain], is_input[:nTrain], Y[:nTrain])
192 | test_set = TensorDataset(data[nTrain:], is_input[nTrain:], Y[nTrain:])
193 |
194 | print_header('Building model')
195 | if args.mnist:
196 | model = MNISTSudokuSolver(args.boardSz, args.aux, args.m)
197 | else:
198 | model = SudokuSolver(args.boardSz, args.aux, args.m)
199 |
200 | if args.cuda: model = model.cuda()
201 |
202 | if args.mnist:
203 | optimizer = optim.Adam([
204 | {'params': model.sudoku_solver.parameters(), 'lr': args.lr},
205 | {'params': model.digit_convnet.parameters(), 'lr': 1e-5},
206 | ])
207 | else:
208 | optimizer = optim.Adam(model.parameters(), lr=args.lr)
209 |
210 | if args.model:
211 | model.load_state_dict(torch.load(args.model))
212 |
213 | train_logger = CSVLogger(os.path.join(save, 'train.csv'))
214 | test_logger = CSVLogger(os.path.join(save, 'test.csv'))
215 | fields = ['epoch', 'loss', 'err']
216 | train_logger.log(fields)
217 | test_logger.log(fields)
218 |
219 | test(args.boardSz, 0, model, optimizer, test_logger, test_set, args.testBatchSz, unperm)
220 | for epoch in range(1, args.nEpoch+1):
221 | train(args.boardSz, epoch, model, optimizer, train_logger, train_set, args.batchSz, unperm)
222 | test(args.boardSz, epoch, model, optimizer, test_logger, test_set, args.testBatchSz, unperm)
223 | #torch.save(model.state_dict(), os.path.join(save, 'it'+str(epoch)+'.pth'))
224 |
225 | def process_inputs(X, Ximg, Y, boardSz):
226 | is_input = X.sum(dim=3, keepdim=True).expand_as(X).int().sign()
227 |
228 | Ximg = Ximg.flatten(start_dim=1, end_dim=2)
229 | Ximg = Ximg.unsqueeze(2).float()
230 |
231 | X = X.view(X.size(0), -1)
232 | Y = Y.view(Y.size(0), -1)
233 | is_input = is_input.view(is_input.size(0), -1)
234 |
235 | return X, Ximg, Y, is_input
236 |
237 | def run(boardSz, epoch, model, optimizer, logger, dataset, batchSz, to_train=False, unperm=None):
238 |
239 | loss_final, err_final = 0, 0
240 |
241 | loader = DataLoader(dataset, batch_size=batchSz)
242 | tloader = tqdm(enumerate(loader), total=len(loader))
243 |
244 | for i,(data,is_input,label) in tloader:
245 | if to_train: optimizer.zero_grad()
246 | preds = model(data.contiguous(), is_input.contiguous())
247 | loss = nn.functional.binary_cross_entropy(preds, label)
248 |
249 | if to_train:
250 | loss.backward()
251 | optimizer.step()
252 |
253 | err = computeErr(preds.data, boardSz, unperm)/batchSz
254 | tloader.set_description('Epoch {} {} Loss {:.4f} Err: {:.4f}'.format(epoch, ('Train' if to_train else 'Test '), loss.item(), err))
255 | loss_final += loss.item()
256 | err_final += err
257 |
258 | loss_final, err_final = loss_final/len(loader), err_final/len(loader)
259 | logger.log((epoch, loss_final, err_final))
260 |
261 | if not to_train:
262 | print('TESTING SET RESULTS: Average loss: {:.4f} Err: {:.4f}'.format(loss_final, err_final))
263 |
264 | #print('memory: {:.2f} MB, cached: {:.2f} MB'.format(torch.cuda.memory_allocated()/2.**20, torch.cuda.memory_cached()/2.**20))
265 | torch.cuda.empty_cache()
266 |
267 | def train(args, epoch, model, optimizer, logger, dataset, batchSz, unperm=None):
268 | run(args, epoch, model, optimizer, logger, dataset, batchSz, True, unperm)
269 |
270 | @torch.no_grad()
271 | def test(args, epoch, model, optimizer, logger, dataset, batchSz, unperm=None):
272 | run(args, epoch, model, optimizer, logger, dataset, batchSz, False, unperm)
273 |
274 | @torch.no_grad()
275 | def computeErr(pred_flat, n, unperm):
276 | if unperm is not None: pred_flat[:,:] = pred_flat[:,unperm]
277 |
278 | nsq = n ** 2
279 | pred = pred_flat.view(-1, nsq, nsq, nsq)
280 |
281 | batchSz = pred.size(0)
282 | s = (nsq-1)*nsq//2 # 0 + 1 + ... + n^2-1
283 | I = torch.max(pred, 3)[1].squeeze().view(batchSz, nsq, nsq)
284 |
285 | def invalidGroups(x):
286 | valid = (x.min(1)[0] == 0)
287 | valid *= (x.max(1)[0] == nsq-1)
288 | valid *= (x.sum(1) == s)
289 | return valid.bitwise_not()
290 |
291 | boardCorrect = torch.ones(batchSz).type_as(pred)
292 | for j in range(nsq):
293 | # Check the jth row and column.
294 | boardCorrect[invalidGroups(I[:,j,:])] = 0
295 | boardCorrect[invalidGroups(I[:,:,j])] = 0
296 |
297 | # Check the jth block.
298 | row, col = n*(j // n), n*(j % n)
299 | M = invalidGroups(I[:,row:row+n,col:col+n].contiguous().view(batchSz,-1))
300 | boardCorrect[M] = 0
301 |
302 | if boardCorrect.sum() == 0:
303 | return batchSz
304 |
305 | return float(batchSz-boardCorrect.sum())
306 |
307 | if __name__=='__main__':
308 | main()
309 |
--------------------------------------------------------------------------------
/images/forward_pass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/locuslab/SATNet/50897688eae47bf765c1d9ed9a7c6f5419d62a9a/images/forward_pass.png
--------------------------------------------------------------------------------
/images/mnist_sudoku.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/locuslab/SATNet/50897688eae47bf765c1d9ed9a7c6f5419d62a9a/images/mnist_sudoku.png
--------------------------------------------------------------------------------
/images/poster_forward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/locuslab/SATNet/50897688eae47bf765c1d9ed9a7c6f5419d62a9a/images/poster_forward.png
--------------------------------------------------------------------------------
/notebooks/Learning and Solving Sudoku via SATNet.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import os\n",
10 | "import shutil\n",
11 | "import argparse\n",
12 | "from collections import namedtuple\n",
13 | "\n",
14 | "import numpy as np\n",
15 | "import numpy.random as npr\n",
16 | "\n",
17 | "import torch\n",
18 | "import torch.nn as nn\n",
19 | "import torch.optim as optim\n",
20 | "import torch.nn.functional as F\n",
21 | "from torch.utils.data import TensorDataset, DataLoader\n",
22 | "\n",
23 | "%matplotlib inline\n",
24 | "import matplotlib.pyplot as plt\n",
25 | "from IPython.display import display, Markdown, Latex, clear_output"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "# Introduction to SATNet"
33 | ]
34 | },
35 | {
36 | "cell_type": "markdown",
37 | "metadata": {},
38 | "source": [
39 | "SATNet is a differentiable (smoothed) maximum satisfiability (MAXSAT) solver that can be integrated into the loop of larger deep learning systems. Our (approximate) solver is based upon a fast coordinate descent approach to solving the semidefinite program (SDP) associated with the MAXSAT problem.\n",
40 | "\n",
41 | "The code below reproduces the Sudoku experiments from our paper \"SATNet: Bridging deep learning and logical reasoning using a differentiable satisfiability solver.\" These experiments show that by integrating the SATNet solver into end-to-end learning systems, we can learn the logical structure of challenging problems in a minimally supervised fashion. In particular, this notebook shows how we can learn to:\n",
42 | "* Play **9×9 Sudoku (original and permuted)** solely from examples.\n",
43 | "* Solve a **\"visual Sudoku\"** problem that maps images of Sudoku puzzles to their associated logical solutions. \n",
44 | "\n",
45 | "For more details and discussion about these experiments, please see the [SATNet paper](https://icml.cc/Conferences/2019/Schedule?showEvent=3947)."
46 | ]
47 | },
48 | {
49 | "cell_type": "code",
50 | "execution_count": 2,
51 | "metadata": {},
52 | "outputs": [
53 | {
54 | "name": "stdout",
55 | "output_type": "stream",
56 | "text": [
57 | "SATNet document\n",
58 | " Apply a SATNet layer to complete the input probabilities.\n",
59 | "\n",
60 | " Args:\n",
61 | " n: Number of input variables.\n",
62 | " m: Rank of the clause matrix.\n",
63 | " aux: Number of auxiliary variables.\n",
64 | "\n",
65 | " max_iter: Maximum number of iterations for solving\n",
66 | " the inner optimization problem.\n",
67 | " Default: 40\n",
68 | " eps: The stopping threshold for the inner optimizaiton problem.\n",
69 | " The inner Mixing method will stop when the function decrease\n",
70 | " is less then eps times the initial function decrease.\n",
71 | " Default: 1e-4\n",
72 | " prox_lam: The diagonal increment in the backward linear system\n",
73 | " to make the backward pass more stable.\n",
74 | " Default: 1e-2\n",
75 | " weight_normalize: Set true to perform normlization for init weights.\n",
76 | " Default: True\n",
77 | "\n",
78 | " Inputs: (z, is_input)\n",
79 | " **z** of shape `(batch, n)`: \n",
80 | " Float tensor containing the probabilities (must be in [0,1]).\n",
81 | " **is_input** of shape `(batch, n)`: \n",
82 | " Int tensor indicating which **z** is a input.\n",
83 | "\n",
84 | " Outputs: z\n",
85 | " **z** of shape `(batch, n)`:\n",
86 | " The prediction probabiolities.\n",
87 | "\n",
88 | " Attributes: S\n",
89 | " **S** of shape `(n, m)`:\n",
90 | " The learnable clauses matrix containing `m` clauses \n",
91 | " for the `n` variables.\n",
92 | "\n",
93 | " Examples:\n",
94 | " >>> sat = satnet.SATNet(3, 4, aux=5)\n",
95 | " >>> z = torch.randn(2, 3)\n",
96 | " >>> is_input = torch.IntTensor([[1, 1, 0], [1,0,1]])\n",
97 | " >>> pred = sat(z, is_input)\n",
98 | " \n"
99 | ]
100 | }
101 | ],
102 | "source": [
103 | "import satnet\n",
104 | "print('SATNet document\\n', satnet.SATNet.__doc__)"
105 | ]
106 | },
107 | {
108 | "cell_type": "markdown",
109 | "metadata": {},
110 | "source": [
111 | "# Building SATNet-based Models"
112 | ]
113 | },
114 | {
115 | "cell_type": "markdown",
116 | "metadata": {},
117 | "source": [
118 | "To solve **Sudoku** and a **permuted version of Sudoku**: We construct a SATNet-based SudokuSolver layer that takes as input a logical (bit) representation of the initial Sudoku board along with a mask representing which bits must be learned (i.e. all bits in empty Sudoku cells). This input is vectorized. Given this input, the SudokuSolver layer then outputs a bit representation of the Sudoku board with guesses for the unknown bits."
119 | ]
120 | },
121 | {
122 | "cell_type": "code",
123 | "execution_count": 3,
124 | "metadata": {},
125 | "outputs": [],
126 | "source": [
127 | "class SudokuSolver(nn.Module):\n",
128 | " def __init__(self, boardSz, aux, m):\n",
129 | " super(SudokuSolver, self).__init__()\n",
130 | " n = boardSz**6\n",
131 | " self.sat = satnet.SATNet(n, m, aux)\n",
132 | "\n",
133 | " def forward(self, y_in, mask):\n",
134 | " out = self.sat(y_in, mask)\n",
135 | " del y_in, mask\n",
136 | " return out"
137 | ]
138 | },
139 | {
140 | "cell_type": "markdown",
141 | "metadata": {},
142 | "source": [
143 | "To solve **\"visual Sudoku\"**: We construct a (standard) convolutional neural network for MNIST digit recognition and train it end-to-end with our SudokuSolver layer. This architecture takes in an image representation of a Sudoku board constructed with MNIST digits. Each MNIST digit is classified by the convolutional network, and the resulting (estimated) logical representation of the initial Sudoku board is then fed as input to the SudokuSolver layer. (As described earlier, the SudokuSolver layer then outputs a bit representation of the Sudoku board with guesses for the unknown bits.)"
144 | ]
145 | },
146 | {
147 | "cell_type": "code",
148 | "execution_count": 4,
149 | "metadata": {},
150 | "outputs": [],
151 | "source": [
152 | "class DigitConv(nn.Module):\n",
153 | " '''\n",
154 | " Convolutional neural network for MNIST digit recognition. From:\n",
155 | " https://github.com/pytorch/examples/blob/master/mnist/main.py\n",
156 | " '''\n",
157 | " def __init__(self):\n",
158 | " super(DigitConv, self).__init__()\n",
159 | " self.conv1 = nn.Conv2d(1, 20, 5, 1)\n",
160 | " self.conv2 = nn.Conv2d(20, 50, 5, 1)\n",
161 | " self.fc1 = nn.Linear(4*4*50, 500)\n",
162 | " self.fc2 = nn.Linear(500, 10)\n",
163 | "\n",
164 | " def forward(self, x):\n",
165 | " x = F.relu(self.conv1(x))\n",
166 | " x = F.max_pool2d(x, 2, 2)\n",
167 | " x = F.relu(self.conv2(x))\n",
168 | " x = F.max_pool2d(x, 2, 2)\n",
169 | " x = x.view(-1, 4*4*50)\n",
170 | " x = F.relu(self.fc1(x))\n",
171 | " x = self.fc2(x)\n",
172 | " return F.softmax(x, dim=1)[:,:9].contiguous()\n",
173 | "\n",
174 | "class MNISTSudokuSolver(nn.Module):\n",
175 | " def __init__(self, boardSz, aux, m):\n",
176 | " super(MNISTSudokuSolver, self).__init__()\n",
177 | " self.digit_convnet = DigitConv()\n",
178 | " self.sudoku_solver = SudokuSolver(boardSz, aux, m)\n",
179 | " self.boardSz = boardSz\n",
180 | " self.nSq = boardSz**2\n",
181 | " \n",
182 | " def forward(self, x, is_inputs):\n",
183 | " nBatch = x.shape[0]\n",
184 | " x = x.flatten(start_dim = 0, end_dim = 1)\n",
185 | " digit_guess = self.digit_convnet(x)\n",
186 | " puzzles = digit_guess.view(nBatch, self.nSq * self.nSq * self.nSq)\n",
187 | "\n",
188 | " solution = self.sudoku_solver(puzzles, is_inputs)\n",
189 | " return solution"
190 | ]
191 | },
192 | {
193 | "cell_type": "markdown",
194 | "metadata": {},
195 | "source": [
196 | "The experimental parameters we use in the paper are below."
197 | ]
198 | },
199 | {
200 | "cell_type": "code",
201 | "execution_count": 5,
202 | "metadata": {},
203 | "outputs": [],
204 | "source": [
205 | "from exps.sudoku import train, test, FigLogger, find_unperm\n",
206 | "args_dict = {'lr': 2e-3, \n",
207 | " 'cuda': torch.cuda.is_available(), \n",
208 | " 'batchSz': 40,\n",
209 | " 'mnistBatchSz': 50,\n",
210 | " 'boardSz': 3, # for 9x9 Sudoku\n",
211 | " 'm': 600,\n",
212 | " 'aux': 300,\n",
213 | " 'nEpoch': 100\n",
214 | " }\n",
215 | "args = namedtuple('Args', args_dict.keys())(*args_dict.values())"
216 | ]
217 | },
218 | {
219 | "cell_type": "markdown",
220 | "metadata": {},
221 | "source": [
222 | "# The Sudoku Datasets"
223 | ]
224 | },
225 | {
226 | "cell_type": "markdown",
227 | "metadata": {},
228 | "source": [
229 | "We use and/or create the following datasets:\n",
230 | "* **Sudoku:** We generate 10K 9x9 Sudoku boards (9K test/1K train) using code available [here](https://github.com/Kyubyong/sudoku) and represent them via bit (one-hot) representations.\n",
231 | "* **Permuted Sudoku:** We apply a fixed permutation to the 10K Sudoku board bit representations generated for the Sudoku experiment.\n",
232 | "* **Visual Sudoku:** We construct versions of the 10K Sudoku boards generated for the Sudoku experiment in which each board cell is represented by a (randomly-selected) MNIST digit. (MNIST digits are also split into train/test sets, with train and test MNIST digits applied only to train and test Sudoku boards, respectively.)\n",
233 | "\n",
234 | "The code below reads and processes these datasets for use with the architectures constructed above. A sample Sudoku board, its associated bit representation, and its associated MNIST representation are displayed below."
235 | ]
236 | },
237 | {
238 | "cell_type": "code",
239 | "execution_count": 6,
240 | "metadata": {},
241 | "outputs": [],
242 | "source": [
243 | "def process_inputs(X, Ximg, Y, boardSz):\n",
244 | " is_input = X.sum(dim=3, keepdim=True).expand_as(X).int().sign()\n",
245 | "\n",
246 | " Ximg = Ximg.flatten(start_dim=1, end_dim=2)\n",
247 | " Ximg = Ximg.unsqueeze(2).float()\n",
248 | "\n",
249 | " X = X.view(X.size(0), -1)\n",
250 | " Y = Y.view(Y.size(0), -1)\n",
251 | " is_input = is_input.view(is_input.size(0), -1)\n",
252 | "\n",
253 | " return X, Ximg, Y, is_input\n",
254 | "\n",
255 | "with open('sudoku/features.pt', 'rb') as f:\n",
256 | " X_in = torch.load(f)\n",
257 | "with open('sudoku/features_img.pt', 'rb') as f:\n",
258 | " Ximg_in = torch.load(f)\n",
259 | "with open('sudoku/labels.pt', 'rb') as f:\n",
260 | " Y_in = torch.load(f)\n",
261 | "with open('sudoku/perm.pt', 'rb') as f:\n",
262 | " perm = torch.load(f)\n",
263 | "\n",
264 | "X, Ximg, Y, is_input = process_inputs(X_in, Ximg_in, Y_in, args.boardSz)\n",
265 | "if args.cuda: X, Ximg, is_input, Y = X.cuda(), Ximg.cuda(), is_input.cuda(), Y.cuda()\n",
266 | "\n",
267 | "N = X_in.size(0)\n",
268 | "nTrain = int(N*0.9)\n",
269 | "\n",
270 | "sudoku_train = TensorDataset(X[:nTrain], is_input[:nTrain], Y[:nTrain])\n",
271 | "sudoku_test = TensorDataset(X[nTrain:], is_input[nTrain:], Y[nTrain:])\n",
272 | "perm_train = TensorDataset(X[:nTrain,perm], is_input[:nTrain,perm], Y[:nTrain,perm])\n",
273 | "perm_test = TensorDataset(X[nTrain:,perm], is_input[nTrain:,perm], Y[nTrain:,perm])\n",
274 | "mnist_train = TensorDataset(Ximg[:nTrain], is_input[:nTrain], Y[:nTrain])\n",
275 | "mnist_test = TensorDataset(Ximg[nTrain:], is_input[nTrain:], Y[nTrain:])"
276 | ]
277 | },
278 | {
279 | "cell_type": "code",
280 | "execution_count": 7,
281 | "metadata": {},
282 | "outputs": [
283 | {
284 | "data": {
285 | "text/markdown": [
286 | "## Sudoku"
287 | ],
288 | "text/plain": [
289 | ""
290 | ]
291 | },
292 | "metadata": {},
293 | "output_type": "display_data"
294 | },
295 | {
296 | "name": "stdout",
297 | "output_type": "stream",
298 | "text": [
299 | "tensor([[6, 7, 0, 0, 0, 0, 0, 0, 5],\n",
300 | " [0, 0, 3, 0, 4, 0, 0, 8, 2],\n",
301 | " [0, 4, 0, 0, 0, 5, 1, 3, 6],\n",
302 | " [0, 0, 0, 7, 3, 0, 0, 9, 0],\n",
303 | " [3, 0, 4, 2, 0, 6, 0, 7, 0],\n",
304 | " [0, 0, 1, 0, 9, 0, 6, 0, 0],\n",
305 | " [5, 0, 9, 0, 0, 8, 0, 0, 0],\n",
306 | " [0, 0, 0, 9, 5, 0, 2, 0, 8],\n",
307 | " [0, 0, 0, 1, 2, 7, 4, 0, 0]])\n",
308 | "\n"
309 | ]
310 | },
311 | {
312 | "data": {
313 | "text/markdown": [
314 | "## One-hot encoded Boolean Sudoku"
315 | ],
316 | "text/plain": [
317 | ""
318 | ]
319 | },
320 | "metadata": {},
321 | "output_type": "display_data"
322 | },
323 | {
324 | "name": "stdout",
325 | "output_type": "stream",
326 | "text": [
327 | "tensor([0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,\n",
328 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
329 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
330 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
331 | " 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
332 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,\n",
333 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.,\n",
334 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
335 | " 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,\n",
336 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.,\n",
337 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
338 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.,\n",
339 | " 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,\n",
340 | " 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
341 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
342 | " 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,\n",
343 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
344 | " 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
345 | " 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
346 | " 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,\n",
347 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,\n",
348 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,\n",
349 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
350 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
351 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.,\n",
352 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,\n",
353 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
354 | " 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
355 | " 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
356 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.,\n",
357 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
358 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
359 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
360 | " 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0.,\n",
361 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,\n",
362 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.,\n",
363 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
364 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
365 | " 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,\n",
366 | " 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n",
367 | " 0., 0., 0., 0., 0., 0., 0., 0., 0.], device='cuda:0')\n"
368 | ]
369 | },
370 | {
371 | "data": {
372 | "text/markdown": [
373 | "## MNIST Sudoku"
374 | ],
375 | "text/plain": [
376 | ""
377 | ]
378 | },
379 | "metadata": {},
380 | "output_type": "display_data"
381 | },
382 | {
383 | "data": {
384 | "image/png": "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\n",
385 | "text/plain": [
386 | ""
387 | ]
388 | },
389 | "metadata": {
390 | "needs_background": "light"
391 | },
392 | "output_type": "display_data"
393 | }
394 | ],
395 | "source": [
396 | "def show_sudoku(raw):\n",
397 | " return (torch.argmax(raw,2)+1)*(raw.sum(2).long())\n",
398 | "\n",
399 | "def show_mnist_sudoku(raw):\n",
400 | " A = raw.numpy()\n",
401 | " digits = np.concatenate(np.concatenate(A,axis=1), axis=1).astype(np.uint8)\n",
402 | " linewidth = 2\n",
403 | " board = np.zeros((digits.shape[0]+linewidth*4, digits.shape[1]+linewidth*4), dtype=np.uint8)\n",
404 | " gridwidth = digits.shape[0]//3\n",
405 | "\n",
406 | " board[:] = 255\n",
407 | " for i in range(3):\n",
408 | " for j in range(3):\n",
409 | " xoff = linewidth+(linewidth+gridwidth)*i\n",
410 | " yoff = linewidth+(linewidth+gridwidth)*j\n",
411 | " xst = gridwidth*i\n",
412 | " yst = gridwidth*j\n",
413 | " board[xoff:xoff+gridwidth, yoff:yoff+gridwidth] = digits[xst:xst+gridwidth, yst:yst+gridwidth]\n",
414 | "\n",
415 | " #img = Image.fromarray(255-board)\n",
416 | " plt.imshow(255-board, cmap='gray')\n",
417 | "\n",
418 | "display(Markdown('## Sudoku'))\n",
419 | "print(show_sudoku(X_in[0]))\n",
420 | "print()\n",
421 | "display(Markdown('## One-hot encoded Boolean Sudoku'))\n",
422 | "print(X[0])\n",
423 | " \n",
424 | "display(Markdown('## MNIST Sudoku'))\n",
425 | "show_mnist_sudoku(Ximg_in[0])"
426 | ]
427 | },
428 | {
429 | "cell_type": "markdown",
430 | "metadata": {},
431 | "source": [
432 | "# The 9x9 Sudoku Experiment"
433 | ]
434 | },
435 | {
436 | "cell_type": "markdown",
437 | "metadata": {},
438 | "source": [
439 | "The results for our 9x9 Sudoku experiment are below. In this experiment, we:\n",
440 | "* **Input** a logical (bit) representation of the initial (unsolved) Sudoku board along with a mask representing which bits must be learned (i.e. all bits in empty Sudoku cells). This input is vectorized, which means that our SATNet model cannot exploit the locality structure of the input Sudoku grid when learning to solve puzzles.\n",
441 | "* **Output** a bit representation of the Sudoku board with guesses for the unknown bits."
442 | ]
443 | },
444 | {
445 | "cell_type": "code",
446 | "execution_count": 8,
447 | "metadata": {},
448 | "outputs": [],
449 | "source": [
450 | "%%capture\n",
451 | "sudoku_model = SudokuSolver(args.boardSz, args.aux, args.m)\n",
452 | "if args.cuda: sudoku_model = sudoku_model.cuda()\n",
453 | " \n",
454 | "optimizer = optim.Adam(sudoku_model.parameters(), lr=args.lr)\n",
455 | "\n",
456 | "fig, axes = plt.subplots(1,2, figsize=(10,4))\n",
457 | "plt.subplots_adjust(wspace=0.4)\n",
458 | "train_logger = FigLogger(fig, axes[0], 'Traininig')\n",
459 | "test_logger = FigLogger(fig, axes[1], 'Testing')"
460 | ]
461 | },
462 | {
463 | "cell_type": "code",
464 | "execution_count": 9,
465 | "metadata": {},
466 | "outputs": [
467 | {
468 | "data": {
469 | "image/png": "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\n",
470 | "text/plain": [
471 | ""
472 | ]
473 | },
474 | "metadata": {},
475 | "output_type": "display_data"
476 | }
477 | ],
478 | "source": [
479 | "test(args.boardSz, 0, sudoku_model, optimizer, test_logger, sudoku_test, args.batchSz)\n",
480 | "for epoch in range(1, args.nEpoch+1):\n",
481 | " train(args.boardSz, epoch, sudoku_model, optimizer, train_logger, sudoku_train, args.batchSz)\n",
482 | " test(args.boardSz, epoch, sudoku_model, optimizer, test_logger, sudoku_test, args.batchSz)\n",
483 | " clear_output()\n",
484 | " display(fig)"
485 | ]
486 | },
487 | {
488 | "cell_type": "markdown",
489 | "metadata": {},
490 | "source": [
491 | "# The Permuted 9x9 Sudoku Experiment"
492 | ]
493 | },
494 | {
495 | "cell_type": "markdown",
496 | "metadata": {},
497 | "source": [
498 | "The results for our permuted 9x9 Sudoku experiment are below. In this experiment, we:\n",
499 | "* **Input** the same inputs as in the original 9x9 Sudoku experiment, but with a fixed permutation applied.\n",
500 | "* **Output** a bit representation of the permuted Sudoku board with guesses for the unknown bits."
501 | ]
502 | },
503 | {
504 | "cell_type": "code",
505 | "execution_count": 10,
506 | "metadata": {},
507 | "outputs": [],
508 | "source": [
509 | "%%capture\n",
510 | "perm_model = SudokuSolver(args.boardSz, args.aux, args.m)\n",
511 | "if args.cuda: perm_model = perm_model.cuda()\n",
512 | " \n",
513 | "optimizer = optim.Adam(perm_model.parameters(), lr=args.lr)\n",
514 | "\n",
515 | "fig, axes = plt.subplots(1,2, figsize=(10,4))\n",
516 | "plt.subplots_adjust(wspace=0.4)\n",
517 | "train_logger = FigLogger(fig, axes[0], 'Traininig')\n",
518 | "test_logger = FigLogger(fig, axes[1], 'Testing')"
519 | ]
520 | },
521 | {
522 | "cell_type": "code",
523 | "execution_count": 11,
524 | "metadata": {},
525 | "outputs": [
526 | {
527 | "data": {
528 | "image/png": "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\n",
529 | "text/plain": [
530 | ""
531 | ]
532 | },
533 | "metadata": {},
534 | "output_type": "display_data"
535 | }
536 | ],
537 | "source": [
538 | "unperm = find_unperm(perm)\n",
539 | "test(args.boardSz, 0, perm_model, optimizer, test_logger, perm_test, args.batchSz, unperm)\n",
540 | "for epoch in range(1, args.nEpoch+1):\n",
541 | " train(args.boardSz, epoch, perm_model, optimizer, train_logger, perm_train, args.batchSz, unperm)\n",
542 | " test(args.boardSz, epoch, perm_model, optimizer, test_logger, perm_test, args.batchSz, unperm)\n",
543 | " clear_output()\n",
544 | " display(fig)"
545 | ]
546 | },
547 | {
548 | "cell_type": "markdown",
549 | "metadata": {},
550 | "source": [
551 | "# The End-to-End MNIST Sudoku (\"Visual Sudoku\") Experiment"
552 | ]
553 | },
554 | {
555 | "cell_type": "markdown",
556 | "metadata": {},
557 | "source": [
558 | "The results for our permuted 9x9 Sudoku experiment are below. In this experiment, we:\n",
559 | "* **Input** an image representation of the initial (unsolved) Sudoku board.\n",
560 | "* **Output** a bit representation of the Sudoku board with guesses for the unknown bits."
561 | ]
562 | },
563 | {
564 | "cell_type": "code",
565 | "execution_count": 12,
566 | "metadata": {},
567 | "outputs": [],
568 | "source": [
569 | "%%capture\n",
570 | "mnist_sudoku = MNISTSudokuSolver(args.boardSz, args.aux, args.m)\n",
571 | "if args.cuda: mnist_sudoku = mnist_sudoku.cuda()\n",
572 | " \n",
573 | "optimizer = optim.Adam([\n",
574 | " {'params': mnist_sudoku.sudoku_solver.parameters(), 'lr': args.lr},\n",
575 | " {'params': mnist_sudoku.digit_convnet.parameters(), 'lr': 1e-5},\n",
576 | " ])\n",
577 | "\n",
578 | "fig, axes = plt.subplots(1,2, figsize=(10,4))\n",
579 | "plt.subplots_adjust(wspace=0.4)\n",
580 | "train_logger = FigLogger(fig, axes[0], 'Traininig')\n",
581 | "test_logger = FigLogger(fig, axes[1], 'Testing')"
582 | ]
583 | },
584 | {
585 | "cell_type": "code",
586 | "execution_count": 13,
587 | "metadata": {},
588 | "outputs": [
589 | {
590 | "data": {
591 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoYAAAEWCAYAAAD7BFanAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzt3Xm8XHV9//HX596bhBCEhE2BIAMSPQdBRSiLWqQoFhiFVrGC0rpAUX/yc6cdK264dGplUaFqSgVRZBFbQQalaqX+VMBERU04g8Q4SgQlKIuECLnJ9/fHnLmZ3DvLmeXMWeb9fDzmcecsM/OdSe77fub7Ped7zDmHiIiIiMhE0g0QERERkXRQYSgiIiIigApDEREREQmpMBQRERERQIWhiIiIiIRUGIqIiIgIoMJQRsTMJs3sETN7clL7iohkkZktCHNuz6TbIvlnmsdQWjGzR5oWtwceAzaHy693zl0x+laJiKRHXDlpZrcCFznnvjBgE0V6NpV0AySdnHM7NO6bWQ04wzn3zXb7m9mUc256FG0TEUmDXnNSJAs0lCx9MbMPmdnVZnalmf0ROM3MjjSzW83sQTO718w+YWbzwv2nzMyZWSFc/kK4/Wtm9kczu8XM9h3CvruZWcXMHjazH5jZR8zs5tF+OiIiM4e6vMfM1prZ/WZ2hZktDrctMrOrzOwPYWbeZmZLzOw84M+AS8Lh4/PMbLsw55aGj73KzC40s5vCTPyeme3T9LpFM7srfN4Lw1w+LZlPQbJGhaEM4q+BLwI7AVcD08BbgF2B5wLHAa/v8PhXAu8BdgZ+DXxwCPt+CngQeCLwOuDV0d6KiMjQnQ28CHgesBTYBFwQbjuD+qjdXtQz8yzgcefcO4AV1HsfdwiXW3kl8C7qmXgv8AEAM3sS9Tx+G7AbcA9wyNDfmeSWCkMZxHedc191zm1xzm10zq1wzt3mnJt2zq0FlgPP7/D4a51zK51zm4ArgGcNsm/YO/lXwHvD9qwCPt/3uxMRGczrgZJz7h7n3J+oF2+vMDOjXiTuBjwlzMwVzrkNPTz3Nc65H4WZ+EW2ZuKJwArn3A3hto8BDwztHUnu6RhDGcTdzQtm5gHnUf92uj31/1+3dXj8b5vuPwrs0G7HiPs+EZic1a67gSM6PK+IyNCFxd/ewI1m1nyW5wSwC/AfwJOAa81sB+By4D3Ouc1znqy1dpm4J00Z6JzbYma/6e9dyDhSj6EMYvYp7Z8BVgH7O+d2BN4L2Ajb8ztgC/Uhm4a9R/j6IiIAuPqUH78BjnHOLW66beecu98595hz7r3OOQ84Cng5cErj4QO89L00ZaCZTVAfrhaJRIWhDNMTgIeADWbm0/n4wqELh02+AnzAzBaa2dMBHXAtIkn5NFA2s70BzGx3M3tJeP+FZnZAWLg9TP0Y7UZv4e+A/fp8zeuBw83sBDObAt4OLBnkTch4UWEow/QO6id7/JF67+HVCbThjdSHaX4HXApcSX1uMRGRUfso8E3gf8LZG74PPDvcthdwHfW8XAXcCFwTbrsA+Dsze8DMPtrLCzrn7gVOBT4B3E+99/BnKAclIk1wLbkWTv2w2Dl3etJtEREZtbDX8LfAS5xztyTdHkk/9RhKroRDMwdZ3RHAa4H/SrpdIiKjYmbHm9lOZrYd8D7qJ6f8MOFmSUborGTJmx2pT2ezB/Xh5LJz7oZkmyQiMlJHUc/BKerD1H/tnHs82SZJVmgoWUREREQADSWLiIiISChzQ8kTExNu4cKFSTdDRNp49NFHnXNOXzpHKEoubnnsMdiypb6/MlRkZLKWiZkrDBcuXMiGDb1cNUhERsnMNibdhnETJReDSy6Bj50HgF8NRtEsESF7mZiZClZERPrnn3FG0k0QkQxQYSgiIiIigApDEREREQmpMBQRERERQIWhiIiIiIRUGIqIiIgIEON0NYHnfxZ4MXCfXw0ObLHdgI8DJ1C/juNr/Grwo7jaIyLpUChVZrKhVi7OyYZCqTInG2rlYi6yQbkoIrOlLRPjnMfwMuAi4PI2248HloW3w4FPhT8HdumlFT5wZ/vttXJxGC8jIv25jISyIQUuI6H3XihV4MSP1ie5LlVa7qNsFEnEZaQoE2MbSvarwXeAP3TY5STgcr8aOL8a3AosDjx/j2G8dqeiEOoBWWgTjCISr1q5GCkbauWiq5WLtwKLC6XKULIhaUnl4kzeTUzAVPv+gEY2Kh9FRidtmZjkMYZ7AXc3La8L181hZmea2UozWzk9PT20Bij8RGIx1fh9DW9n9vj4yNmQQ/HnonORdlM+igxNpjIxyUviWYt1LRPLObccWA6waNGiaKkmIkmZds4dOsDjI2dDDsWbixGLQhEZqkxlYpI9huuAvZuWlwL3DOOJdZyMSKbFlg0ZEMt7n8lE52Dz5kGfTkRGa6SZmGSP4fXAWYHnX0X9IMqH/Gpw77CevF1x2Dw8ogJSJJWuB84qlCoz2VArF4eWDSkXWy7WykUCzwfArwYt91E+iqTSSDMxzulqrgSOBnYNPH8d8D5gHoBfDT4N3Ej91Os11E+/fm1cbWlWKxd17IxIggqlykw2FEqVbbKhVi4mlg2jkNZcFJHkpC0TzWXsmJNFixa5DRs2DPQc+lYsEh8ze9Q5tyjpdoyTqLnYrcfwGaUKD4f3lY0iw5G1TNSVT0REBICfqhgUGXtjXxhqWFlEZC7NZygynsa+MAQVhyIi7SgfRcaLCkMRERERAVQYioiIiEhoLAvD2Wfb6ew7EZE65aPIeBvLwlBERERE5hrbwvCv9k+6BSIiIiLpMraF4YVnaHhERKQVDR+LjK+xLQxFRKQ7TVcjMl5UGAKf+5yCT0SkHRWHIuNDhSHwvtaXDRURkZCKQ5HxoMJQRERERAAVhiIiIiISGuvCcKekGyAiklKzz0w+94CEGiIiIzXWheFPNCWDiEhbzcXhe+/QcYYi42CsC8NmCjwRke6UlSL5NtaF4eyAU+CJiIjIOBvrwlBEREREtlJhKCIibc0+CUWXyxPJt7EuDBV4IiLdNWejDrkRybexLgwBDlmcdAtERLJFxaFIfo19YfjlknoJRUR6peJQJJ/GvjAUERERkToVhk321zdgERERGWMqDJtMJ90AEZGU0sl6IuNBhaGIiIiIACoMRUQkIvUSiuSfCkMUdiIiIiKgwlBERPqg6WpE8kmF4SwKOxERERlXKgyZWwyqOBQREZFxNBXnkweefxzwcWASuMSvBuVZ258MfA5YHO5T8qvBjXG2SUSSVShVtsmFWrlYnrV9Ti7UysVc5EIeMrFWLs58eS6UKjpGW2QI0pSLsfUYBp4/CVwMHA8cAJwaeP4Bs3Y7B7jGrwYHA6cA/xZXe0QkeYVSZU4uFEqVlrlQKxdzlQt5yUSNsIgMV9pyMc6h5MOANX41WOtXg8eBq4CTZu3jgB3D+zsB98TYnrY0cavIyBwGrKmVi2tr5WKqcyEGmclEERmpVOVinIXhXsDdTcvrwnXN3g+cFnj+OuBG4P+2eiIzO9PMVprZyunpeK5PomJQZGimGr+v4e3Mpm2Rc6FQqnTMhQwaWibCaHJRRIaiUyZCynIxzsLQWqxzs5ZPBS7zq8FS4ATg84Hnz2mTc265c+5Q59yhU1OxHhYpIoObbvy+hrflTdsi50KtXJzJhUKpkocT5YaWiZBcLmqERaRnnTIRUpaLcYbtOmDvpuWlzO36PB24BsCvBrcA2wG7xtimSHTMjEhsesqFWrmYmlwYgsxm4mzNxaDyUmRgqcrFOAvDFcCywPP3DTx/PvWDJa+ftc+vgRcABJ7vU3+j62Nsk4gkawWwrFCq7FsoVbrmQqFUyVMupCYTg0suGfZTikj/UpWLsRWGfjWYBs4CbgIC6mfarQ48/9zA808Md3sH8PeB5/8EuBJ4jV8NZnefikhO1MrFOblQKxdXF0qVcwulyja5UChVZnKhVi5mPhdSlYk3fm3gp/jwQVvvq9dQpH9py0VzLlt5u2jRIrdhw4ZYnvvzn6/wntX1+zpuRqQ/Zvaoc25R0u0YJ1FzMfD8+p3ttsO//ccDvWarYlC5KTJX1jIxDwd0D83f/q1CTUTGwJ/+lHQLRCSlVBiKiMhQaEhZJPtUGLZRKFUUciIibbQbNlZuimSbCsMuFHIiIq3pmEKR/FFhKCIiIiKACkMRERmAroQiki8qDGdRyImI9KY5J1/wAR1+I5JlKgw7+NCBSbdARCRbfrEx6RaIyCBUGHZwzqqkWyAiIiIyOioMRURkYH9zQNItEJFhUGEoIiID++jf6XhskTxQYdiCTjgREemfLhAgkl0qDEVEJBYqDkWyR4VhF++4VMEmIiIi40GFYRdfvlPfekVERGQ8qDCMSMWhiEhnukCASPapMBQRkaE5cvet93USikj2qDAUEZGhufLtc3sJVRyKZMdUp42B528HvBj4c2BPYCOwCqj41WB1/M1LTq1c3CbMNCQiIjDeuTiIQqmiHBXJgLY9hoHnvx/4HnAkcBvwGeAaYBooB57/jcDznzGKRoqIpIFycTDqORRJv049hiv8avD+NtvODzx/d+DJw2+SiEhqKRcjmD3iIiLZ0bbH0K8GFYDA8w9ss/0+vxqsjKthIiJpo1yMTsPGItkU5eSTTwee/4PA8/9P4PmLY29RiixIugEiklZjm4u90PQ1ItnTtTD0q8HzgFcBewMrA8//YuD5x8beshS4UyEmIi2Mcy726tSDkm6BiPQi0nQ1fjW4CzgH+Efg+cAnAs+vBp7/0jgbJyKSVsrFaP75VVu/YGteQ5H061oYBp7/jMDzLwAC4BjgJX418MP7F8TcvtRQmIlIg3JxMMpTkfTqOI9h6CLg34F/8qvBxsZKvxrcE3j+ObG1TEQkvZSLIpJLUY4xPAq4GlgWeP5BgefPb9r2+Tgblzb6lisioFwUkfyKMpR8AvAL4BPUvyWvCTz/+LgbllYqDkVEudibVmcjK0tF0inKySfnA3/hV4Oj/WrwfOAv0DE0IjLelIs9alccqkAUSZcoheF9fjVY07S8FrgvpvaIiGSBcnGIVCCKpIc55zruEHj+p4B9qF8P1AEvB+6kfr1Q/GrwnzG3cRuLFi1yGzZsGOVLbhNYmqBVpDMze9Q5tyjpdsQpq7kYeP7Mfb8axNmklqIUf8pYyZusZWKUs5K3A35HfZ4ugPXAzsBLqAdi2wAMPP844OPAJHCJXw3KLfb5G+D94XP9xK8Gr+yh/SKSMYVSZZtcqJWLc3KhUKpskwu1cjFtudBXLo57Jka5hrK+iMs4SlMudu0x7Ffg+ZPAz4FjgXXACuBUvxrc0bTPMurfuI/xq8EDgefv7leDjsMx6jEUSbdO344LpUrLXKiVi3c07TOTC7Vy8YFCqbJ7rVzM/DBtXJkI2ekxbBZ16FiZK1nXrccwbbnYtccw8PylwCeB51KvUr8LvMWvBuu6PPQwYI1fDdaGz3MVcBJwR9M+fw9c7FeDB6B+Afqe38EIRPmWKyKRHAasqZWLawEKpUrbXKiViw8ApLEo7DMXc5OJw9Ao+JStIunKxSgnn1wKXA/sCewFfDVc181ewN1Ny+vCdc2eCjw18PzvBZ5/azjMIiL5FTkXCqXK9wqlyq3hEEva9JOLysQW1CMokq5cjFIY7uZXg0v9ajAd3i4DdovwOGuxbva49RSwDDgaOBW4JPD8xXOeyOxMM1tpZiunp6cjvHR89O1WpKupxu9reDuzaVtfuVAoVebkQsL6ycWhZSKkKxfjpDOWJQc6ZSKkLBejnHxyf+D5pwFXhsunAr+P8Lh1wN5Ny0uBe1rsc6tfDTYBvww8/07qb3xF807OueXAcqgfSxPhtUUkOdPOuUPbbIucC7VycRPwy0Kp0jIXEtZPLg4tEyFfuTj7cJ1Wh+8UShX1LkpWdcpESFkuRikMX0d9Zv8LqFew3w/XdbOC+uWi9gV+A5wCzD6D5ivUA/WywPN3pd5VujZa05OjgBLp2wpgWaFUiZQLhVIlrbnQTy7mNhOHIUqm6kRAyalU5WLHwjA8i+5lfjU4sdcn9qvBdOD5ZwE3UT/9+rN+NVgdeP65wEq/GlwfbntR4Pl3AJuBs/1qEKU3MnEqDkV6VysXpwulyja5UCsXVxdKlXOBlbVycSYXCqXKTC7UysXU5EK/uZj3TBw1ZbDkRdpyMcoE1zf71eDoOF68H0lMVwOtjy1UKInMlbXJXPuR1VxM03Q1UXQ7tlAZLFmQtUyMMpT8vcDzLwKuBmaSx68GP4qtVSIiEYTzf5Vr5eLZI35p5eIIaKowkd4NmotRzkp+DvB04FzgvPD2sX5eLMtmfzPVN1WR5NXKxc3AIYVSpdVZfXFSLiZMGSzS2qC5GKXH8PTGhKwNgefv18+LiYjE4MfAdYVS5Us09d7VysU4r1esXByRdr2GjXUqEEVa6jsXoxSG1wLPnrXuS8AhvbQwDzSsIZJKO1OfKuaYpnUdr+M+BMrFEWou/lpNYzN7HxHpPxfbFoaB53vUh0p2Cjz/pU2bdqR+AflUy9pB1iLSn1q5+NpRvVbWczHPdJayyFaD5GKnHsOnAS8GFgMvaVr/R+rX7Eut5qKwsTzs4lAhJJIOhVKl5XWLa+Vit+u59yOzuSgi42OQXGxbGPrV4DrgusDzj/SrwS3DaqyIyJBdCnwReHm4fFq47thhv5ByMXmdDunpdvWUdtuiLItkTN+5GGUew92ofxMu0FRI+tUgytVPhq7bfF2zewubDaPXUGEh0tmo5+wqlCq318rFZ3VbN0xZy8WGvB1iM8pjvpX30q8k5jEcJBejnHxyHfD/gG9Sn217rP3zM+FdP6nf13CySCrcXyhV+rme+yCymYu77w733Zd0K4ZGJwSKtNV3LkYpDLf3q8E/9tuyvGkUhQ0qDkUS1+/13AeRzVw8+GC46aakW5FJynrJmL5zMUpheEPg+Sf41eDG/ts3On41aDmcnIdhExHZVjjD/8tq5WLP13MfUKZyscH/+IUdD7fJolH2Gqo4lCwYNBejFIZvAf4p8PzHgccBA5xfDXbs5wVFRIalVi5uLpQqJ1H/VjxKysUUmV2stTsWPI3DzjpuXYZt0FzsevJJ2qThIGv9Iou0l8DJJx8GdmLWdYtr5eLYXLc4ai7C1mwcx1GUVoVhr4XjMDK/0+vob0r+JHTySd+52LXHMPB8A14F7OtXgw8Gnr83sIdfDX7Qf5NHJ47w0wHPIqnynPDnuU3rHNvO+D9UWc9Faa3btDULhvAa+tshI9J3LkYZSv43YEv4ZB8EHgEuBv6stzbm01VXVTjlFH3DE0lCoVSZAD5VKxevGfFLKxczKMr8hLPXNT/msXBdtx6/QUaVdByjDGrQXJyIsM/hfjV4E/AnAL8aPADM7+fF8qh0e9ItEBlftXJxC3BWAi+tXMyoWrk4c4vqmU/aer9bj1+razn32kvYeEw/jxUZNBej9BhuCjx/knoXZGNi1y39vqCIyJB9o1CqvJO5x9L8IcbXVC6OkeveOvjhQ1GvwtLusepFlB71nYtRCsNPAP8F7B54/oeBk4Fz+mxobuy/Pax5NOlWiAhb5+Z6U9M6B+wX42sqF8dIXL12KvYkRn3nYqSzkgPP94AXUJ+S4Vt+NUjsdLZez0qO88w7nZ0sMlcSZ+AlIYu5CON9VnK/RlEYjuqMaBm9rGVi22MMA8/foXHfrwZVvxpc7FeDi5rDr3mfcdPqOBIRGZ1CqfIPTfdfPmvbR+J4TeWiNDQfqziMgi3Kc+iYQ+lmGLnY6eST6wLPPy/w/KMCz5+pdAPP3y/w/NMDz78JOK7XRouIDMkpTfffNWtbXNmkXBxDrc5U7udx3bZFfd52J7hEXS+5NnAutj3G0K8GLwg8/wTg9cBzA8/fGdgE3AlUgFf71eC3vbVXRGRorM39VstDoVwcX92KtnbT1PQ6fU2vw8tRR68a67u9vg6RyryBc7HjySfhdUAzdS3QUdEk1yKJc23ut1oeGuWitNOukOq3wOr0d6bfM5U7FYitiszGfioYM2PgXIxyVrK0oeJQJFHPLJQqD1P/FrwwvE+4vF1yzRIZnlaFWcMgf39mP7ZTsdepYMyKZ33gJh7cOM3ihVPc/r6/TLo5cRo4F3N/reS4z7y7+uoK//jjrctZ+2URGbasnYGXBzoreTwk0RHRqQNk0L93o+qFbBSFDaMuDrOWiVGufCIdNBeFoLOTRURk9OIorHo5HrHXE11GObNHc1HYalm21XUoOfD8pwDr/GrwWOD5RwPPAC73q8GDcTduGILPfhb/da/rvqOISERZz0XJpm6HL3U7HjBK8fWtv/d4wb9XI+/faYh7dmGZVMfJE+Ybf3x86+joDvNjOTctN6IcY/hl4NDA8/cH/gO4HvgicEKcDRuaT14EKgxFZLiynYuSWVFOBul0Eky34qxRFA7DKArBp7yrwmYHkwa/+OfW73u7eVP88fFNM8vzpyZjb1eWRSkMt/jVYDrw/L8GLvSrwScDz/9x10elxcaNsT69TkARGUvZzkXJvLwdz97tLOlW2xtFIcBmV1+enDAe3+yYP2n8/MP172nrN9SLwhd6u/LN6v384dFoQ8lvverH3Pzz9Rz91N248JSDe31LmRWlMNwUeP6pwKuBl4Tr5sXXpOxRcSgydpSLkkmD/r0a1d+7KGdCb5517uxmB5vDlY9vdjz13Tcy2TRDyyWvOXzmeQulysykfhNWn8flKbsu4hvvOBqoF4Vfuf0egJmfh+6zhK+v/h0nHLQHrzz8yUN4l+kUpTB8LfAG4MN+Nfhl4Pn7Al+It1nZswPwSNKNEJFRUS5KZnU69q/T9DjN+7TaftRTlvCdXzzQV5tW/eY+Dtxr98j7RylOH59VOR743q9vs9zY2tjtrvUbOPa8m/nGO47m5p+v32bfm1b/dqZA/O6a+wFyWxz2NF1N4PlLgL39avDT+JrUWa/T1cDopmXoZV4okbzK2tQMg8pSLoKmq5HedBrO7fY3r9eexW4nyoyqt/JZS3di0/QWVv/2jzPrdtthPusfeXxm+c+X7crnTz880vNlLRO7FoaB598MnEi9d/F2YD3wv341eHvsrWshS4UhqDiU8ZO1EOxHVnMRVBjK8ET5mzesQm4S2Dzgc+wwf5KF8ye3KfDavp5t7UmcNHjfiU/nvdetntn+/pccwGueu2+k181aJkaZx3Anvxo8DLwUuNSvBocAL4zy5IHnHxd4/p2B568JPL/UYb+TA893gecfGq3ZIpJVhVLluEKpcmehVFlTKFXa5kKhVDm5UKq4QqmSxlzoKxeViTJuop4x3c0wisJV5x7HinOOZbcd5gP1S4EY9cJvzuuFReH28ye3OXZxyfb1Q4knJ4Y75U2acjHKMYZTgefvAfwN8O6oTxx4/iRwMXAssA5YEXj+9X41uGPWfk8A3gzcFrnVIpJJhVJlTi4USpXra+XiHbP2S3su9JyLykQZV52Kw0F7FCeAiQmY3tJ+n4VTE6w697iZ5RXnHDtnn7+66Lvcvu6hbdZNTcAzl+7ELWv/wHnfuBOAG9/857zhih/xbzf/goc2buLIp+zKIfssGeg9pC0Xo/QYngvcBPzCrwYrAs/fD7grwuMOA9b41WCtXw0eB64CTmqx3weBjwJ/itjm1BrWNyORHDsMWFMrF9fWysUs50I/uTh2mSj5luTfPAtfb225yJqPFJkKq5mpFlXNxk5VY+grZz2PZy3daZt101vgkT/Vp7Z55LHNLN5+HnssXsgLvN2596E/cd5//5xXXXIrP/xVfyfcNElVLnYtDP1q8CW/GjzDrwZvDJfX+tXgZRGeey/g7qbldeG6GYHnH0z9oO0bOj2RmZ1pZivNbOX0tC5lI5JyU43f1/B2ZtO2rrlQKFUOBvaulYsdcyFJfebi0DIRlIuSDrVycebW7+P72f7LWevXfKTehjUfKbJwVnU4e7mdr5z1PBbMGle+a/3W+UZ23j4cgrZ6YeqATdNbuHXt77s9dadMhJTlYpRL4i0FPgk8l/rn8F3gLX41WNfloa0G4GfOdAk8fwK4AHhNtzY455YDy6F+kHW3/ZPUfNZUu8sCieTctHOu3fEvHXOhUKpEzoUk9ZmLQ8tEyFYuinQy7L+RwYeOxz/na2yc3sLCqQmCDx0f+bGH77cL37nr/pnlXRfNZ92D9Q66tfdvoHxjwLFPfxIXz1vDpuktzJua4Ij9dun2tJ0yEVKWi1HK6EupX+5pT+oV7FfDdd2sA/ZuWl4K3NO0/ATgQODmwPNrwBHA9Xk82FqTX4vMiJwLhVKlRpgLKTwBpZ9cVCaK9KGfIevgQ8dTKxd7KgoBLj/9cI5ativbzZvgqGW7MjW5bZn09dW/5ZB9lnDFGUfw9hc9jSvOOGLgYwxJWS5GOflkN78aNAfeZYHnvzXC41YAy8KJX38DnAK8srHRrwYPAbs2lsPpH97pV4OVURouIpm0AlhWKFVa5kKtXNwmFwqlys3AO2vlYtpyoZ9cVCaK9GmUI2+XN81PWL4x4NPfWTuzfNzTnwTAIfssGUZB2JCqXIzSY3h/4PmnBZ4/Gd5OA7oOqPvVYBo4i/oB2gFwjV8NVgeef27g+ScO1mwRyaJauTgnF2rl4upCqXJuoVTJUi70nIvKRJHsKZ3g84aj9qOwy/a84aj9KJ3gd39Qj9KWi1EmuH4ycBFwJPUx7+8Db/arwa/jb95caZ7gulm3i3+L5FXWJnPtR1ZzETTBtcioZS0Tuw4lh0G3TcUaDplcGFejhuLsd8K/fiyxl58HbErs1UUkTpnNRRGRLqKdwz1XIpd96oV/+umJvv5dsybw1AkoIrmX+lwUEemm38JwuNeCGRMqDkVyTbkoIpnXb2GoObNERLalXBSRzGt7jGHg+X+kddAZsDC2FomIpJRyUUTyrm1h6FeDJ4yyIXnUfBWUxrKIZJdyUUTyrt+hZImoppNQREREJCNUGCZAxaGIiIikkQpDEREREQFUGIqIiIhISIXhCLQ66UTDySIiIpI2KgxHRMWhiIiIpJ0KQxEREREBVBiKiIiISEiF4QhpOFlERETSbCwKw+Ctb0u6CTNUHIqIiEhajUVhyNe/nnQLRERERFJvPAq/NstCAAARx0lEQVRDEREREelKhWECNJwsIkkLPD/pJohICqkwTIiKQxEZtdnFoIpDEZlNhaGIiIiIACoMU0e9hiISh3a9g+o1FJFmKgwT1Go4GVQcioiISDJUGCasXXEoIiIiMmoqDFOqUKqo51BEhsavBj2tF5HxpMIwBTr1Gqo4FBERkVFRYZgStXJRxxyKSKxm9w6qt1BEZhubwjDrZ96pOBSRYVAxKCKdjE1hCNkoDjWsLCIiIkkZq8IwK1QcisgoZOHLsoiMVm4Lw6xP5qppbERERGTUclsY5oFORhEREZFRmorzyQPPPw74ODAJXOJXg/Ks7W8HzgCmgfXA6/xq8Ks425Q1tXKxZSFYKFXUqyiZVChVtsmFWrlYnrV9Ti7UysVc5IIyUURaSVMuxtZjGHj+JHAxcDxwAHBq4PkHzNrtx8ChfjV4BnAt8NFhvX6eJnNVz6HkRaFUmZMLhVKlZS7UysWh50KSks7EbZz9zuZ2xfISIhJN2nIxzqHkw4A1fjVY61eDx4GrgJOad/Crwbf9avBouHgrsDTG9uSSikPJmMOANbVycW2tXGyZC7Vy8du1cjGPuZCeTPzXj22zqOJQJFGpysU4C8O9gLublteF69o5Hfhaqw1mdqaZrTSzldPT05EbkKfJXHWmsmTIVOP3Nbyd2bRtaLmQQUN97/3mooiMXKdMhJTlYpzHGFqLda7VjoHnnwYcCjy/1Xbn3HJgOcCiRYtaPkdbxx8PX8vH35V2xxvC1uJQxx1KCkw75w5tsy1yLhRKlY65kEFDy0ToPxc7zdiQ5S/PIinWKRMhZbkYZ4/hOmDvpuWlwD2zdwo8/4XAu4ET/Wrw2LAb4V9w/rCfMlHdCr9CqTJzE0mhSLlQKFVmcqFWLg49FxKSikwUkdRJVS7G2WO4AlgWeP6+wG+AU4BXNu8QeP7BwGeA4/xqcF+MbWm8HpDtIWXo3HPYTGcuSwqtAJYVSpW2uVAoVWZyoVYuxp4LI5S6TBSRVEhVLsbWY+hXg2ngLOAmIACu8avB6sDzzw08/8Rwt38FdgC+FHj+7YHnXx9Xe5rl4UDrqAWfeg4lTWrl4pxcqJWLqwulyrmFUmVOLhRKldsLpcpIciFuacnEPM3YIJIHactFc663Q/aStmjRIrdhw4aeHtOqEMxTCEYp/tRzKKNiZo865xYl3Y5x0msutvtynKdcFEmLrGVi7gvDTr2DeQzBTkWiikMZhayFYB4M6wsz5DMXRZKUtUzUJfFyptu0NhpaFhFQASgirakwzKEoZy6LyHjrNG1NHo7DFpH+5H4oGcZvOLkhagGoIWYZpqwNm+TBsHOxIc/5KDIqWctE9RjmWC9nLqsXUURERMaixxDGt9ewod/CT72J0qusfTvOgzhysWEc8lEkTlnLRPUYko95Dbvpt8BTb6LIeBuHfBSRrVQYhsYh/Abp/VOBKJI/UXsDxyEfRaRubIaSQcMmrQxa7GmoWWbL2rBJHgySiw1Ri79xy0iRQWUtE1UYtjCuwTeMHkEVipK1EMyDURaGDeOakyK9ylomjlVhCAq/Xgx76FhF43jIWgjmwTAKQ+hvyHicM1Ikiqxl4tgVhqDw65UKROlF1kIwD4ZVGEL/xxOOc0aKdJK1TFRh2KNxDr+4Tz5RwZgPWQvBPEhDYdgwzhkp0krWMnEsC0NQ+A1DnIWiisTsyloI5sEwC0MY/lnIyksZZ1nLxLEtDGE44afA21ZcxaIKxezIWgjmwbALQ4h3ihrlpoyTrGXiWBeGoG/Go6KCcXxkLQTzII7CEJKbv1A5KnmStUwc+8IQ4gs/hdtcSU6SXSsXt3l9FZXxyFoI5kFchWFDFia4Vt5KWmUtE1UYhkYZfAqw1pK+sooKxeHIWgjmQdyFYUMWCsRRU55LN1nLRBWGLaQp/MYxdJIuEDtpVTyqF3JbWQvBPBhVYdiQpoyU0Wv1d6n5/0S7v1uz9+n2mODAg2B6Gqam8Ff9bJAmJyprmajCsIOsh19eiso0F4qt5K04/OGvHuDWtb/niP124ZB9lnTdP2shmAejLgwbsp6RMkYmJmDLFqYKBWzBAjbdeScsWACPPQaLFsGGDdiSJbgHHmByzz3ZfM89zHva09h0550sPPJINt5yCzudfDIPXX89S88/n/n7PJl1b3s7Sy84nwXLlnV86axlogrDCBR+c6Wh6MxawThsUxOw5iPxFqE//NUDvOIzt7B5i2PBvAmuOOOIrsVh1kIwD5IqDBuUkTJWJiaY2m03pu+7j3l77MF+N3yVie23b7t71jJRhWEPFH7ZdfyLyzPfGAGYnNy60bn6+uZ1ZqNt4AAmgflTE2yc3tJ2n8a7mTBwwNSEscXBgXvuyFfOel7bx5W+/FOuWnF3/XUM3v6ip/Gmv9i/Y3uyFoJ5kHRh2Ew5KePEFixgh2OOYekF57ffJ2OZqMKwDwq+7GsuFL92Q2nOupY/OxWLjd8js/rNuWg/Wz2+3T7dXrtP5rbwZxMPsfOCSb738CQ7T0yz6U+b2P0JU/xs8xPYPDnJhHPMn5rkijOPVI9hCqUhF9tRXkre2cKFPPGcd7PkZS9rvT1jmajCcAAKvPHSqXD82g2l+vbmXsdRaBSQzfd7KUghUmF55L2rOPXBVbz4quUdh0zqzchWCOZBmnKxG+Wm5NHkLjvz1O99r+W2rGWiCsMhU+iNt669jp1+Qm+PGdZwd4Ti8tn3/ZyP/OjyrkMm9afIVgjmQdpzsRfKUMkaW7iQJ73nHBa/9KWtt2csE1UYxkThJnE7sfhhNk3OY97mTUy4LTw2taBrMTmxZTNuYoKpzdNsmpwCm4j0Wm++/VqO/9VtXYdMIHshmAdZycW4KG8lKTrGMAWyHoAKMEmTcw5/HT/b7SkctP4X7PT4BlY8yWfJxoeZnpzHHo+sx01M8rx7fsrxv7pt5jGdhkwgeyGYB1nPxaxSno8pnZWcLuMUgAodSZtuQyaQvRDMg3HKRUlWp79LsyetjrKPXw0I/ANaH+s8OYm/elX9dQ96Bmza1F+jQfMY9kCFYU6pqJRhizJkAtkLwTxQLoqkV9YycSrpBkg84pyAWkXnGDJjapdd2PPDH0q6JSIiEiMVhtKzNFz1JE6JFL6Tk/UzgKene36oLVmCTU2xZf36rSsbJ5xMTdWfs8uQyU4nn8zUzjvz++XL5wybPPHss3ng6qtZesH5XaeqERGRbNNQsogMVdaGTfJAuSiSXlnLxGhzVYiIiIhI7sU6lBx4/nHAx6lfzvUSvxqUZ21fAFwOHAL8HniFXw1qcbZJRJJVKFW2yYVauVietX1OLtTKxdqo2xkHZaKItJKmXIytxzDw/EngYuB44ADg1MDzD5i12+nAA3412B+4APiXuNojIskrlCpzcqFQqrTMhVq5mKtcUCaKSCtpy8U4h5IPA9b41WCtXw0eB64CTpq1z0nA58L71wIvCDx/SNf5EpEUOgxYUysX19bKxci5UChV8pALykQRaSVVuRjnUPJewN1Ny+uAw9vt41eD6cDzHwJ2Ae5v3snMzgTODBedmW2M2IYpoPfTPEcvK+0EtTUOWWknRGvrQjNb2bS83Dm3PLzfUy7UysXpQqnSMhcyaGiZCH3nYlb+r2WlnZCdtqqdwxe1rZ0yEVKWi3EWhq0q2dmnQEfZh/ADXN5i384NMFvpnDu018eNWlbaCWprHLLSThhKW4eWCxk01PfeTy5m5f9aVtoJ2Wmr2jl8Q2xrqnIxzqHkdcDeTctLgXva7RN4/hSwE/CHGNskIsnqKRcKpUqeckGZKCKtpCoX4+wxXAEsCzx/X+A3wCnAK2ftcz3wauAW4GTgf/xqkIeeARFpbQWwrFCq9JQLtXIxD7mgTBSRVlKVi7H1GPrVYBo4C7gJCIBr/GqwOvD8cwPPPzHc7T+AXQLPXwO8HSgNuRk9Dz8nJCvtBLU1DllpJwzY1lq5OCcXauXi6kKpcm6hVNkmFwqlSly5kAhlYk+y0k7ITlvVzuEbSlvTlouZu/KJiIiIiMRDVz4REREREUCFoYiIiIiEclkYmtlxZnanma0xs1Qdn2Rme5vZt80sMLPVZvaWcP3OZvYNM7sr/Lkk6bYCmNmkmf3YzG4Il/c1s9vCdl5tZvOTbiOAmS02s2vNrBp+tkem+DN9W/hvv8rMrjSz7dLyuZrZZ83sPjNb1bSu5edodZ8If89+ambPTqLNEk1ac1GZGEsblYeDt2tsszB3haGZzbm0jJnNvrRMkqaBdzjnfOAI4E1h+0rAt5xzy4BvkZ4D7t9C/WDYhn8BLgjb+QD1y/SkwceBrzvnPOCZ1Nucus/UzPYC3gwc6pw7kPp1MU8hPZ/rZcBxs9a1+xyPB5aFtzOBT42ojdKjlOeiMnH4lIeDu4xxzULnXK5uwJHATU3L7wLelXS7OrT3OuBY4E5gj3DdHsCdKWjbUur/+Y8BbqA+web9wFSrzzrBdu4I/JLwZKqm9Wn8TBuz1+9MfbqoG4C/TNPnChSAVd0+R+AzwKmt9tMtXbcs5aIyceA2Kg+H176xzMLc9RjS+tIyeyXUlo7MrAAcDNwGPNE5dy9A+HP35Fo240LgH4At4fIuwIPOucYlgNLy2e4HrAcuDYd4LjGzRaTwM3XO/Qb4GPBr4F7gIeCHpPNzbWj3OWbmd02y8W+lTBwK5WF8xiIL81gYZuJyWma2A/Bl4K3OuYeTbs9sZvZi4D7n3A+bV7fYNQ2f7RTwbOBTzrmDgQ2kYJiklfCYlJOAfYE9gUXUhyFmS8Pn2k1a/z/IXKn/t1ImDo3ycPTS+P+gb3ksDKNcWiZRZjaPegBe4Zz7z3D178xsj3D7HsB9SbUv9FzgRDOrAVdRHzq5EFhsZo0r5qTls10HrHPO3RYuX0s9GNP2mQK8EPilc269c24T8J/Ac0jn59rQ7nNM/e+azEj1v5UycaiUh/EZiyzMY2G4AlgWntU0n/qBrNcn3KYZZmbUZzAPnHPnN21qXO6G8Od1o25bM+fcu5xzS51zBeqf4f84514FfJv65XggBe0EcM79FrjbzJ4WrnoBcAcp+0xDvwaOMLPtw/8Ljbam7nNt0u5zvB74u/CMvCOAhxrDLJI6qc1FZeJwKQ9jNR5ZmPRBjnHcgBOAnwO/AN6ddHtmte151LuYfwrcHt5OoH6syreAu8KfOyfd1qY2Hw3cEN7fD/gBsAb4ErAg6faF7XoWsDL8XL8CLEnrZwp8AKgCq4DPAwvS8rkCV1I/1mcT9W/Bp7f7HKkPn1wc/p79jPqZhYl/vrq1/bdNZS4qE2Npn/Jw8HaNbRbqkngiIiIiAuRzKFlERERE+qDCUEREREQAFYYiIiIiElJhKCIiIiKACkMRERERCakwlIGY2WYzu73pNrQZ9s2sYGarhvV8IiJxUyZK1k1130Wko43OuWcl3QgRkZRQJkqmqcdQYmFmNTP7FzP7QXjbP1y/j5l9y8x+Gv58crj+iWb2X2b2k/D2nPCpJs3s381stZn9t5ktDPd/s5ndET7PVQm9TRGRSJSJkhUqDGVQC2cNm7yiadvDzrnDgIuoX1OU8P7lzrlnAFcAnwjXfwL4X+fcM6lf13N1uH4ZcLFz7unAg8DLwvUl4ODwed4Q15sTEemRMlEyTVc+kYGY2SPOuR1arK8Bxzjn1prZPOC3zrldzOx+YA/n3KZw/b3OuV3NbD2w1Dn3WNNzFIBvOOeWhcv/CMxzzn3IzL4OPEL9ck9fcc49EvNbFRHpSpkoWaceQ4mTa3O/3T6tPNZ0fzNbj4stUr825SHAD81Mx8uKSNopEyX1VBhKnF7R9POW8P73gVPC+68Cvhve/xbwRgAzmzSzHds9qZlNAHs7574N/AOwGJjzDV1EJGWUiZJ6+kYhg1poZrc3LX/dOdeYnmGBmd1G/QvIqeG6NwOfNbOzgfXAa8P1bwGWm9np1L8FvxG4t81rTgJfMLOdAAMucM49OLR3JCLSP2WiZJqOMZRYhMfTHOqcuz/ptoiIJE2ZKFmhoWQRERERAdRjKCIiIiIh9RiKiIiICKDCUERERERCKgxFREREBFBhKCIiIiIhFYYiIiIiAsD/BwJcufa2CAHSAAAAAElFTkSuQmCC\n",
592 | "text/plain": [
593 | ""
594 | ]
595 | },
596 | "metadata": {},
597 | "output_type": "display_data"
598 | }
599 | ],
600 | "source": [
601 | "test(args.boardSz, 0, mnist_sudoku, optimizer, test_logger, mnist_test, args.mnistBatchSz)\n",
602 | "for epoch in range(1, args.nEpoch+1):\n",
603 | " train(args.boardSz, epoch, mnist_sudoku, optimizer, train_logger, mnist_train, args.mnistBatchSz)\n",
604 | " test(args.boardSz, epoch, mnist_sudoku, optimizer, test_logger, mnist_test, args.mnistBatchSz)\n",
605 | " clear_output()\n",
606 | " display(fig)"
607 | ]
608 | },
609 | {
610 | "cell_type": "code",
611 | "execution_count": null,
612 | "metadata": {},
613 | "outputs": [],
614 | "source": []
615 | }
616 | ],
617 | "metadata": {
618 | "kernelspec": {
619 | "display_name": "Python 3",
620 | "language": "python",
621 | "name": "python3"
622 | },
623 | "language_info": {
624 | "codemirror_mode": {
625 | "name": "ipython",
626 | "version": 3
627 | },
628 | "file_extension": ".py",
629 | "mimetype": "text/x-python",
630 | "name": "python",
631 | "nbconvert_exporter": "python",
632 | "pygments_lexer": "ipython3",
633 | "version": "3.6.8"
634 | }
635 | },
636 | "nbformat": 4,
637 | "nbformat_minor": 2
638 | }
639 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | torch>=1.0.0
2 | tqdm
3 | requests
4 |
--------------------------------------------------------------------------------
/satnet/__init__.py:
--------------------------------------------------------------------------------
1 | from .models import SATNet
2 |
3 | __all__ = ['SATNet']
4 |
--------------------------------------------------------------------------------
/satnet/models.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 | from torch.autograd import Function
4 | import torch.optim as optim
5 |
6 | import satnet._cpp
7 | if torch.cuda.is_available(): import satnet._cuda
8 |
9 |
10 | def get_k(n):
11 | return int((2*n)**0.5+3)//4*4
12 |
13 | class MixingFunc(Function):
14 | '''Apply the Mixing method to the input probabilities.
15 |
16 | Args: see SATNet.
17 |
18 | Impl Note:
19 | The SATNet is a wrapper for the MixingFunc,
20 | handling the initialization and the wrapping of auxiliary variables.
21 | '''
22 | @staticmethod
23 | def forward(ctx, S, z, is_input, max_iter, eps, prox_lam):
24 | B, n, m, k = z.size(0), S.size(0), S.size(1), 32 #get_k(S.size(0))
25 | ctx.prox_lam = prox_lam
26 |
27 | device = 'cuda' if S.is_cuda else 'cpu'
28 | ctx.g, ctx.gnrm = torch.zeros(B,k, device=device), torch.zeros(B,n, device=device)
29 | ctx.index = torch.zeros(B,n, dtype=torch.int, device=device)
30 | ctx.is_input = torch.zeros(B,n, dtype=torch.int, device=device)
31 | ctx.V, ctx.W = torch.zeros(B,n,k, device=device).normal_(), torch.zeros(B,k,m, device=device)
32 | ctx.z = torch.zeros(B,n, device=device)
33 | ctx.niter = torch.zeros(B, dtype=torch.int, device=device)
34 |
35 | ctx.S = torch.zeros(n,m, device=device)
36 | ctx.Snrms = torch.zeros(n, device=device)
37 |
38 | ctx.z[:] = z.data
39 | ctx.S[:] = S.data
40 | ctx.is_input[:] = is_input.data
41 |
42 | perm = torch.randperm(n-1, dtype=torch.int, device=device)
43 |
44 | satnet_impl = satnet._cuda if S.is_cuda else satnet._cpp
45 | satnet_impl.init(perm, is_input, ctx.index, ctx.z, ctx.V)
46 |
47 | for b in range(B):
48 | ctx.W[b] = ctx.V[b].t().mm(ctx.S)
49 | ctx.Snrms[:] = S.norm(dim=1)**2
50 |
51 | satnet_impl.forward(max_iter, eps,
52 | ctx.index, ctx.niter, ctx.S, ctx.z,
53 | ctx.V, ctx.W, ctx.gnrm, ctx.Snrms, ctx.g)
54 |
55 | return ctx.z.clone()
56 |
57 | @staticmethod
58 | def backward(ctx, dz):
59 | B, n, m, k = dz.size(0), ctx.S.size(0), ctx.S.size(1), 32 #get_k(ctx.S.size(0))
60 |
61 | device = 'cuda' if ctx.S.is_cuda else 'cpu'
62 | ctx.dS = torch.zeros(B,n,m, device=device)
63 | ctx.U, ctx.Phi = torch.zeros(B,n,k, device=device), torch.zeros(B,k,m, device=device)
64 | ctx.dz = torch.zeros(B,n, device=device)
65 |
66 | ctx.dz[:] = dz.data
67 |
68 | satnet_impl = satnet._cuda if ctx.S.is_cuda else satnet._cpp
69 | satnet_impl.backward(ctx.prox_lam,
70 | ctx.is_input, ctx.index, ctx.niter, ctx.S, ctx.dS, ctx.z, ctx.dz,
71 | ctx.V, ctx.U, ctx.W, ctx.Phi, ctx.gnrm, ctx.Snrms, ctx.g)
72 |
73 | ctx.dS = ctx.dS.sum(dim=0)
74 |
75 | return ctx.dS, ctx.dz, None, None, None, None
76 |
77 | def insert_constants(x, pre, n_pre, app, n_app):
78 | ''' prepend and append torch tensors
79 | '''
80 | one = x.new(x.size()[0],1).fill_(1)
81 | seq = []
82 | if n_pre != 0:
83 | seq.append((pre*one).expand(-1, n_pre))
84 | seq.append(x)
85 | if n_app != 0:
86 | seq.append((app*one).expand(-1, n_app))
87 | r = torch.cat(seq, dim=1)
88 | r.requires_grad = False
89 | return r
90 |
91 | class SATNet(nn.Module):
92 | '''Apply a SATNet layer to complete the input probabilities.
93 |
94 | Args:
95 | n: Number of input variables.
96 | m: Rank of the clause matrix.
97 | aux: Number of auxiliary variables.
98 |
99 | max_iter: Maximum number of iterations for solving
100 | the inner optimization problem.
101 | Default: 40
102 | eps: The stopping threshold for the inner optimizaiton problem.
103 | The inner Mixing method will stop when the function decrease
104 | is less then eps times the initial function decrease.
105 | Default: 1e-4
106 | prox_lam: The diagonal increment in the backward linear system
107 | to make the backward pass more stable.
108 | Default: 1e-2
109 | weight_normalize: Set true to perform normlization for init weights.
110 | Default: True
111 |
112 | Inputs: (z, is_input)
113 | **z** of shape `(batch, n)`:
114 | Float tensor containing the probabilities (must be in [0,1]).
115 | **is_input** of shape `(batch, n)`:
116 | Int tensor indicating which **z** is a input.
117 |
118 | Outputs: z
119 | **z** of shape `(batch, n)`:
120 | The prediction probabiolities.
121 |
122 | Attributes: S
123 | **S** of shape `(n, m)`:
124 | The learnable clauses matrix containing `m` clauses
125 | for the `n` variables.
126 |
127 | Examples:
128 | >>> sat = satnet.SATNet(3, 4, aux=5)
129 | >>> z = torch.randn(2, 3)
130 | >>> is_input = torch.IntTensor([[1, 1, 0], [1,0,1]])
131 | >>> pred = sat(z, is_input)
132 | '''
133 |
134 | def __init__(self, n, m, aux=0, max_iter=40, eps=1e-4, prox_lam=1e-2, weight_normalize=True):
135 | super(SATNet, self).__init__()
136 |
137 | S_t = torch.FloatTensor(n+1+aux, m) # n+1 for truth vector
138 | S_t = S_t.normal_()
139 | if weight_normalize: S_t = S_t * ((.5/(n+1+aux+m))**0.5)
140 |
141 | self.S = nn.Parameter(S_t)
142 | self.aux = aux
143 | self.max_iter, self.eps, self.prox_lam = max_iter, eps, prox_lam
144 |
145 | def forward(self, z, is_input):
146 | B = z.size(0)
147 | device = 'cuda' if self.S.is_cuda else 'cpu'
148 | m = self.S.shape[1]
149 | if device == 'cpu' and m%4 != 0:
150 | raise ValueError('m is required to be a multiple of 4 on CPU for SSE acceleration. Now '+str(m))
151 | is_input = insert_constants(is_input.data, 1, 1, 0, self.aux)
152 | z = torch.cat([torch.ones(z.size(0),1,device=device), z, torch.zeros(z.size(0),self.aux,device=device)],dim=1)
153 |
154 | z = MixingFunc.apply(self.S, z, is_input, self.max_iter, self.eps, self.prox_lam)
155 |
156 | return z[:,1:self.S.size(0)-self.aux]
157 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import torch.cuda
2 |
3 | from setuptools import setup
4 | from torch.utils.cpp_extension import BuildExtension, CppExtension, CUDAExtension
5 | from torch.utils.cpp_extension import CUDA_HOME
6 |
7 | gencode = [
8 | '-gencode=arch=compute_50,code=sm_50',
9 | '-gencode=arch=compute_52,code=sm_52',
10 | '-gencode=arch=compute_60,code=sm_60',
11 | '-gencode=arch=compute_61,code=sm_61',
12 | '-gencode=arch=compute_75,code=sm_75',
13 | '-gencode=arch=compute_80,code=sm_80',
14 | '-gencode=arch=compute_86,code=sm_86',
15 | ]
16 |
17 | ext_modules = [
18 | CppExtension(
19 | name = 'satnet._cpp',
20 | include_dirs = ['./src'],
21 | sources = [
22 | 'src/satnet.cpp',
23 | 'src/satnet_cpu.cpp',
24 | ],
25 | extra_compile_args = ['-fopenmp', '-msse4.1', '-Wall', '-g']
26 | )
27 | ]
28 |
29 | if torch.cuda.is_available() and CUDA_HOME is not None:
30 | extension = CUDAExtension(
31 | name = 'satnet._cuda',
32 | include_dirs = ['./src'],
33 | sources = [
34 | 'src/satnet.cpp',
35 | 'src/satnet_cuda.cu',
36 | ],
37 | extra_compile_args = {
38 | 'cxx': ['-DMIX_USE_GPU', '-g'],
39 | 'nvcc': ['-g', '-restrict', '-maxrregcount', '32', '-lineinfo', '-Xptxas=-v']
40 | }
41 | )
42 | ext_modules.append(extension)
43 |
44 | with open("README.md", "r", encoding="utf-8") as fh:
45 | long_description = fh.read()
46 |
47 | # Python interface
48 | setup(
49 | name='satnet',
50 | version='0.1.4',
51 | install_requires=['torch>=1.3'],
52 | packages=['satnet'],
53 | ext_modules=ext_modules,
54 | cmdclass={'build_ext': BuildExtension},
55 | author='Po-Wei Wang',
56 | author_email='poweiw@cs.cmu.edu',
57 | url='https://github.com/locuslab/SATNet',
58 | zip_safe=False,
59 | description='Bridging deep learning and logical reasoning using a differentiable satisfiability solver',
60 | long_description=long_description,
61 | long_description_content_type="text/markdown",
62 | classifiers=[
63 | "License :: OSI Approved :: MIT License",
64 | ],
65 | )
66 |
--------------------------------------------------------------------------------
/src/satnet.cpp:
--------------------------------------------------------------------------------
1 | #ifdef MIX_USE_GPU
2 | #include
3 | #endif
4 | #include
5 |
6 | #ifdef MIX_USE_GPU
7 | #define DEVICE_NAME cuda
8 | #define _MIX_DEV_STR "cuda"
9 | #define _MIX_CUDA_DECL , cudaStream_t stream
10 | #define _MIX_CUDA_ARG , stream
11 | #define _MIX_CUDA_HEAD cudaStream_t stream = at::cuda::getCurrentCUDAStream();
12 | #define _MIX_CUDA_TAIL AT_CUDA_CHECK(cudaGetLastError());
13 | //AT_CUDA_CHECK(cudaStreamSynchronize(stream));
14 | #else
15 | #define DEVICE_NAME cpu
16 | #define _MIX_DEV_STR "cpu"
17 | #define _MIX_CUDA_DECL
18 | #define _MIX_CUDA_ARG
19 | #define _MIX_CUDA_HEAD
20 | #define _MIX_CUDA_TAIL
21 | #endif
22 |
23 | // name mangling for CPU and CUDA
24 | #define _MIX_CAT(x,y) x ## _ ## y
25 | #define _MIX_EVAL(x,y) _MIX_CAT(x,y)
26 | #define _MIX_FUNC(name) _MIX_EVAL(name, DEVICE_NAME)
27 |
28 | #include "satnet.h"
29 |
30 | using Tensor=torch::Tensor;
31 | float *fptr(Tensor& a) { return a.data_ptr(); }
32 | int *iptr(Tensor& a) { return a.data_ptr(); }
33 |
34 | void _MIX_FUNC(mix_init_launcher) (mix_t mix, int32_t *perm _MIX_CUDA_DECL);
35 | void _MIX_FUNC(mix_forward_launcher) (mix_t mix, int max_iter, float eps _MIX_CUDA_DECL);
36 | void _MIX_FUNC(mix_backward_launcher)(mix_t mix, float prox_lam _MIX_CUDA_DECL);
37 |
38 | void mix_init(Tensor perm,
39 | Tensor is_input, Tensor index, Tensor z, Tensor V)
40 | {
41 | _MIX_CUDA_HEAD;
42 |
43 | mix_t mix;
44 | mix.b = V.size(0); mix.n = V.size(1); mix.k = V.size(2);
45 | mix.is_input = iptr(is_input);
46 | mix.index = iptr(index);
47 | mix.z = fptr(z);
48 | mix.V = fptr(V);
49 |
50 | _MIX_FUNC(mix_init_launcher)(mix, iptr(perm) _MIX_CUDA_ARG);
51 |
52 | _MIX_CUDA_TAIL;
53 | }
54 |
55 | void mix_forward(int max_iter, float eps,
56 | Tensor index, Tensor niter, Tensor S, Tensor z, Tensor V, Tensor W, Tensor gnrm, Tensor Snrms, Tensor cache)
57 | {
58 | _MIX_CUDA_HEAD;
59 |
60 | mix_t mix;
61 | mix.b = V.size(0); mix.n = V.size(1); mix.m = S.size(1); mix.k = V.size(2);
62 | mix.index = iptr(index);
63 | mix.niter = iptr(niter);
64 | mix.S = fptr(S);
65 | mix.z = fptr(z);
66 | mix.V = fptr(V);
67 | mix.W = fptr(W);
68 | mix.gnrm = fptr(gnrm); mix.Snrms = fptr(Snrms);
69 | mix.cache = fptr(cache);
70 |
71 | _MIX_FUNC(mix_forward_launcher)(mix, max_iter, eps _MIX_CUDA_ARG);
72 |
73 | _MIX_CUDA_TAIL;
74 | }
75 |
76 | void mix_backward(float prox_lam,
77 | Tensor is_input, Tensor index, Tensor niter, Tensor S, Tensor dS, Tensor z, Tensor dz,
78 | Tensor V, Tensor U, Tensor W, Tensor Phi, Tensor gnrm, Tensor Snrms, Tensor cache)
79 | {
80 | _MIX_CUDA_HEAD;
81 |
82 | mix_t mix;
83 | mix.b = V.size(0); mix.n = V.size(1); mix.m = S.size(1); mix.k = V.size(2);
84 | mix.is_input = iptr(is_input);
85 | mix.index = iptr(index);
86 | mix.niter = iptr(niter);
87 | mix.S = fptr(S); mix.dS = fptr(dS);
88 | mix.z = fptr(z); mix.dz = fptr(dz);
89 | mix.V = fptr(V); mix.U = fptr(U);
90 | mix.W = fptr(W); mix.Phi = fptr(Phi);
91 | mix.gnrm = fptr(gnrm); mix.Snrms = fptr(Snrms);
92 | mix.cache = fptr(cache);
93 |
94 | _MIX_FUNC(mix_backward_launcher)(mix, prox_lam _MIX_CUDA_ARG);
95 |
96 | _MIX_CUDA_TAIL;
97 | }
98 |
99 | PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
100 | m.def("init" , &mix_init, "SATNet init (" _MIX_DEV_STR ")");
101 | m.def("forward" , &mix_forward, "SATNet forward (" _MIX_DEV_STR ")");
102 | m.def("backward" , &mix_backward, "SATNet backward (" _MIX_DEV_STR ")");
103 | }
104 |
--------------------------------------------------------------------------------
/src/satnet.h:
--------------------------------------------------------------------------------
1 | typedef struct mix_t {
2 | int b, n, m, k;
3 | int32_t *is_input; // b*n
4 | int32_t *index; // b*n
5 | int32_t *niter; // b
6 | float *S, *dS; // n*m
7 | float *z, *dz; // b*n
8 | float *V, *U; // b*n*k
9 | float *W, *Phi; // b*m*k
10 | float *gnrm, *Snrms;// b*n
11 | float *cache;
12 | } mix_t ;
13 |
--------------------------------------------------------------------------------
/src/satnet_cpu.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #include
12 |
13 | #include "satnet.h"
14 |
15 | #define saxpy mysaxpy
16 | #define scopy myscopy
17 | #define sscal mysscal
18 | #define sdot mysdot
19 | #define snrm2 mysnrm2
20 | #define szero myszero
21 | #define saturate mysaturate
22 |
23 | const double MEPS = 1e-24;
24 |
25 | /*
26 | * Helper functions
27 | */
28 | void saxpy(float *__restrict__ y, float a, const float *__restrict__ x, int l)
29 | {
30 | y = (float*)__builtin_assume_aligned(y, 4*sizeof(float));
31 | x = (float*)__builtin_assume_aligned(x, 4*sizeof(float));
32 | __m128 const a_ = _mm_set1_ps(a);
33 | for(int i=0; i1)*(1-x);
162 | }
163 |
164 | // consider the \min unsat problem,
165 | void mix_forward(int max_iter, float eps, int n, int m, int k, const int32_t *index, int32_t *niter, const float *S, float *z, float *V, float *W, float *gnrm, float *Snrms, float *cache)
166 | {
167 | float delta;
168 | int iter = 0;
169 | for (; iter < max_iter; iter++) {
170 | delta = mix_kernel(1, 0, m, k, index, S, NULL, V, NULL, W, gnrm, Snrms, cache);
171 | if (iter && delta < eps) break;
172 | if (iter == 0) eps = delta*eps;
173 | }
174 |
175 | *niter = iter;
176 |
177 | for (int i,i_=0; (i=index[i_]); i_++) {
178 | float zi = V[i*k];
179 | zi = saturate((zi+1)/2)*2-1;
180 | zi = saturate(1-acosf(zi)/M_PI);
181 | z[i] = zi;
182 | }
183 | }
184 |
185 | void mix_backward(float prox_lam, int n, int m, int k, int32_t *is_input, int32_t *index, int32_t *niter, const float *S, float *dS, float *z, float *dz, const float *V, float *U, float *W, float *Phi, float *gnrm, float *Snrms, float *cache)
186 | {
187 | int invalid_flag=0;
188 | for (int i,i_=0; (i=index[i_]); i_++) {
189 | float zi = z[i];
190 | float dzi = dz[i]/M_PI/sin(zi*M_PI);
191 | if (isnan(dzi) || isinf(dzi) || gnrm[i] < MEPS) invalid_flag = 1;
192 | dz[i] = dzi;
193 | }
194 | if (invalid_flag) { szero(dz, n); return; }
195 |
196 | // solve P (S'S+D_z-D_sii)xI_k P U = -dz P v0
197 | for (int iter=0; iter<*niter; iter++) {
198 | mix_kernel(0, prox_lam, m, k, index, S, dz, U, V, Phi, gnrm, Snrms, cache);
199 | }
200 |
201 | // sanity check
202 | for (int ik=0; ik
2 | #include
3 | //#include
4 | #include
5 | #include
6 |
7 | #include
8 | #include "satnet.h"
9 |
10 | const double MEPS = 1e-24;
11 | const int WARP_SIZE = 32;
12 | const int WARP_NUM = 32;
13 | const int MBUF_SIZE = 320;
14 |
15 | // Warp level dot product
16 | __device__
17 | float warpdot(const float * x, const float * z, int k)
18 | {
19 | if (k==0) return 0;
20 | int lane = threadIdx.x % WARP_SIZE;
21 |
22 | float val = 0;
23 | #pragma unroll 2
24 | for (int i=lane; iMBUF_SIZE ? MBUF_SIZE : m; // mbuf = # of m inside buffer (in smem)
90 | int mrem = m>MBUF_SIZE ? m-MBUF_SIZE : 0; // mrem = # of m outside buffer (in global mem)
91 | for (int j=lane; j>>(perm,
259 | mix.n, mix.k, mix.is_input, mix.index, mix.z,
260 | mix.V);
261 | }
262 |
263 | void mix_forward_launcher_cuda(mix_t mix, int max_iter, float eps, cudaStream_t stream)
264 | {
265 | int smem_size = (mix.m+mix.k*(1+MBUF_SIZE))*sizeof(float);
266 | mix_forward<<>>(max_iter, eps,
267 | mix.n, mix.m, mix.k, mix.index, mix.niter,
268 | mix.S, mix.z, mix.V, mix.W, mix.gnrm, mix.Snrms, mix.cache);
269 | }
270 |
271 | void mix_backward_launcher_cuda(mix_t mix, float prox_lam, cudaStream_t stream)
272 | {
273 | int smem_size = (mix.m+mix.k*(1+MBUF_SIZE))*sizeof(float);
274 | mix_backward<<>>(prox_lam,
275 | mix.n, mix.m, mix.k, mix.is_input, mix.index, mix.niter,
276 | mix.S, mix.dS, mix.z, mix.dz, mix.V, mix.U, mix.W, mix.Phi, mix.gnrm, mix.Snrms, mix.cache);
277 | }
278 |
--------------------------------------------------------------------------------