转自:https://www.toutiao.com/i6640028992575373828/?tt_from=mobile_qq&utm_campaign=client_share×tamp=1552006941&app=news_article&utm_source=mobile_qq&iid=65267103732&utm_medium=toutiao_ios&group_id=6640028992575373828
通过简单的策略,我们能够在PyTorch中训练神经网络,可以完美地解决Kaggle数据集中的Sudoku难题。
自从Sudokus在80年代出现在现场以来,实际上已经对它们的属性进行了一些理论研究:人们已经表明他们需要至少17条线索来获得独特的解决方案。
数独作为序列问题
假设一个数独可以在约束递归神经网络的框架下解决,这似乎是合理的:输入序列是可用的线索,输出序列是在空单元格中输入的数字序列来完成这个谜题。它应该是循环的,因为还要填充数字以解决剩余的空单元格。这个问题进一步受到了限制,因为行、列和3x3单元格中不应该包含重复的整数。
我们使用的机器学习数据集是从Kaggle(https://www.kaggle.com/bryanpark/sudoku)下载的,包含一个带有一百万个数独谜题的CSV和两个列:一个带有线索,另一个带有解决方案。
应该注意的是,这个数据集包含非常简单的Sudokus,因为它们平均有大约33条线索。Hard Sudokus应该不超过25(大致)。
为了在网络中处理这些数据,我们将每个测验变换为大小(81,9)的矩阵,其中数字是one-hot编码,空单元由空向量给出。因此整个数据集成为维度(bts,81,9 )的张量, 其中bts是数据集大小或批量大小。我们进行这种转换,以便能够在数字上运行softmax,将每个单元格中的分布视为分类分布。
数独最重要的规则是,任何行、列和3x3单元格都不能包含重复的数字。为了以网络可理解的形式描述这个约束,我们建立了一个“constraint mask tensor”。这个张量的维数为(81、3、81),其中第一个索引枚举拼图的81个单元格,第二个索引枚举约束(行、列和单元格)。最后一个索引枚举约束所讨论的单元格的单元格。因此,第一个单元格的行约束由前9个单元格中的行约束描述。
数据集Python函数如下:
import torch.utils.data as data import torch import pandas as pd def create_sudoku_tensors(df, train_split=0.5): s = df.shape[0] def one_hot_encode(s): zeros = torch.zeros((1, 81, 9), dtype=torch.float) for a in range(81): zeros[0, a, int(s[a]) - 1] = 1 if int(s[a]) > 0 else 0 return zeros quizzes_t = df.quizzes.apply(one_hot_encode) solutions_t = df.solutions.apply(one_hot_encode) quizzes_t = torch.cat(quizzes_t.values.tolist()) solutions_t = torch.cat(solutions_t.values.tolist()) randperm = torch.randperm(s) train = randperm[:int(train_split * s)] test = randperm[int(train_split * s):] return data.TensorDataset(quizzes_t[train], solutions_t[train]), data.TensorDataset(quizzes_t[test], solutions_t[test]) def create_constraint_mask(): constraint_mask = torch.zeros((81, 3, 81), dtype=torch.float) # row constraints for a in range(81): r = 9 * (a // 9) for b in range(9): constraint_mask[a, 0, r + b] = 1 # column constraints for a in range(81): c = a % 9 for b in range(9): constraint_mask[a, 1, c + 9 * b] = 1 # box constraints for a in range(81): r = a // 9 c = a % 9 br = 3 * 9 * (r // 3) bc = 3 * (c // 3) for b in range(9): r = b % 3 c = 9 * (b // 3) constraint_mask[a, 2, br + bc + r + c] = 1 return constraint_mask def load_dataset(subsample=10000): dataset = pd.read_csv("sudoku.csv", sep=',') my_sample = dataset.sample(subsample) train_set, test_set = create_sudoku_tensors(my_sample) return train_set, test_set
为了在未完成的拼图中填充空单元格,我们为每个单元格评分该单元格是特定数字的概率。这是通过九个可能数字上的每个单元的softmax激活函数来完成的。在循环网络的输出序列中的每个步骤,具有最高估计概率的单元格将用softmax中的最大值填充。当我们在空单元上循环时,拼图将一次解决一个单元格。
Python代码如下:
import torch import torch.nn as nn class SudokuSolver(nn.Module): def __init__(self, constraint_mask, n=9, hidden1=100): super(SudokuSolver, self).__init__() self.constraint_mask = constraint_mask.view(1, n * n, 3, n * n, 1) self.n = n self.hidden1 = hidden1 # Feature vector is the 3 constraints self.input_size = 3 * n self.l1 = nn.Linear(self.input_size, self.hidden1, bias=False) self.a1 = nn.ReLU() self.l2 = nn.Linear(self.hidden1, n, bias=False) self.softmax = nn.Softmax(dim=1) # x is a (batch, n^2, n) tensor def forward(self, x): n = self.n bts = x.shape[0] c = self.constraint_mask min_empty = (x.sum(dim=2) == 0).sum(dim=1).max() x_pred = x.clone() for a in range(min_empty): # score empty numbers constraints = (x.view(bts, 1, 1, n * n, n) * c).sum(dim=3) # empty cells empty_mask = (x.sum(dim=2) == 0) f = constraints.reshape(bts, n * n, 3 * n) y_ = self.l2(self.a1(self.l1(f[empty_mask]))) s_ = self.softmax(y_) # Score the rows x_pred[empty_mask] = s_ s = torch.zeros_like(x_pred) s[empty_mask] = s_ # find most probable guess score, score_pos = s.max(dim=2) mmax = score.max(dim=1)[1] # fill it in nz = empty_mask.sum(dim=1).nonzero().view(-1) mmax_ = mmax[nz] ones = torch.ones(nz.shape[0]) x.index_put_((nz, mmax_, score_pos[nz, mmax_]), ones) return x_pred, x
Python代码如下:
import dataset as d import model as m import torch import torch.utils.data as data import torch.nn as nn import torch.optim as optim batch_size = 100 train_set, test_set = d.load_dataset() constraint_mask = d.create_constraint_mask() dataloader_ = data.DataLoader(train_set, batch_size=batch_size, shuffle=True) dataloader_val_ = data.DataLoader(test_set, batch_size=batch_size, shuffle=True) loss = nn.MSELoss() sudoku_solver = m.SudokuSolver(constraint_mask) optimizer = optim.Adam(sudoku_solver.parameters(), lr=0.01, weight_decay=0.000) epochs = 20 loss_train = [] loss_val = [] for e in range(epochs): for i_batch, ts_ in enumerate(dataloader_): sudoku_solver.train() optimizer.zero_grad() pred, mat = sudoku_solver(ts_[0]) ls = loss(pred, ts_[1]) ls.backward() optimizer.step() print("Epoch " + str(e) + " batch " + str(i_batch) + ": " + str(ls.item())) sudoku_solver.eval() with torch.no_grad(): n = 100 rows = torch.randperm(test_set.tensors[0].shape[0])[:n] test_pred, test_fill = sudoku_solver(test_set.tensors[0][rows]) errors = test_fill.max(dim=2)[1] != test_set.tensors[1][rows].max(dim=2)[1] loss_val.append(errors.sum().item()) print("Cells in error: " + str(errors.sum().item()))
我们就可以开始训练机器学习模型了!以下是经过10 batches (a batch size of 100 puzzles),20 batches 后和50 batches 后量网络给出的三种方案。
10 batches 后猜测。该机器学习模型非常不确定,并且会产生很多错误。
20 batches 后。我们看到网络对中间的8非常不确定。这个数字也是错误的。
在50 batches 之后,我们看到网络对其猜测更加自信,并设法解决了这个难题。
训练时验证误差(根据错误的单元格)
在大约两个epochs(100 batches with a dataset of 10k puzzles)后,模型可以自信地解决验证集中的所有谜题。
机器学习数据集中的Sudoku谜题包含很多线索,这使得它们非常简单。网络不需要很多涉及的推理,这可能就是为什么这种简单的方法运作良好的原因。当面对大约20条线索的谜题时,模型可能需要扩展更多信息。这可以通过向特征向量添加信息或通过使用循环网络结构更好地推理前面几步来完成,例如重新校正错误的猜测。