机器学习笔记(八):应用CNN和RNN解决数独问题


机器学习笔记(八):应用CNN和RNN解决数独问题

这篇博客整理于我选修的《机器学习》课程的大作业。

问题描述

背景

数独(shù dú, Sudoku)是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。

数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。

数据

这个数据集包含了1百万数独的数据,你可以在这里找到它。

数据集包含2列。quizzes栏目是未解的游戏,solutions栏目各自的已解游戏。每场比赛都由81个数字组成的字符串表示。以下是从字符串转换而来的9x9数独。数字0代表未解游戏中的空白位置。

示例的数据如下所示:

数据集包含2列。quizzes栏目是未解的游戏,solutions栏目各自的已解游戏。每场比赛都由81个数字组成的字符串表示。以下是从字符串转换而来的9x9数独。数字0代表未解游戏中的空白位置。

[[0 0 4 3 0 0 2 0 9]
 [0 0 5 0 0 9 0 0 1]
 [0 7 0 0 6 0 0 4 3]
 [0 0 6 0 0 2 0 8 7]
 [1 9 0 0 0 7 4 0 0]
 [0 5 0 0 8 3 0 0 0]
 [6 0 0 0 0 0 1 0 5]
 [0 0 3 5 0 8 6 9 0]
 [0 4 2 9 1 0 3 0 0]]

整体思路

分析数独问题并确定解决方案

数独(shù dú, Sudoku)需要我们根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。

因此,数独是具有空间特征的,它有特殊的数字排列,这样的话,我们就可以尝试使用CNN来提取空间特征,进而通过CNN求解数独问题。

思考解决数独问题的其他方案

但是,在网上查找资料的时候,我了解到CNN解决数独问题的时候,是将整个数独盘作为输入,一次正向传播就直接得到完整的解决方案,这样的话就忽视了目标之间存在的归纳偏置,以及它们是以一致的方式互相作用的,所以在一些情况下CNN模型就会出现问题。

同时,有一部分文章和评论认为使用CNN或者NN解决数独是单纯的用非常多的参数去记住数独的每一个答案,虽然不知道这个观点是否正确,但我觉得就算网络的参数不多,用CNN还是可以解决数独问题的。

CNN做数独的时候是一次性得到完整的解决方案,但我们做数独的时候,实际上是一步一步进行的:我们每次填入一个数字,然后观察这个数字带来的影响。所以数独的解决可以认为是一系列互相依赖的关系推理任务。

因此对于数独问题,循环网络的效果可能比卷积网络要好。按照上面的分析,循环关系网络(RRN)会比较适合数独问题。

解决思路

根据上面的分析,我打算自己实现CNN网络解决数独问题,同时参考网上的资料尝试一下其他的方法,再把这些方法进行对比。

由于没有找到RRN处理数独问题的相关资料,所以我打算尝试一下用RNN解决数独问题,这个部分主要是参照PyTorch中的数独RNN

数据处理

下面是数据处理的相关代码。

这里我利用pandas来读取和处理数据,利用torch.utils.data来打包数据,这样处理后面就可以用DataLoader来加载数据,比较方便。

由于这里我需要同时满足CNN与RNN网络输入输出的要求(其实可以改成同样的,但是RNN部分只是看的差不多懂了,还不太会改,改了就出问题),所以我设置了一个one_hot参数,如果是RNN就使用one hot编码,如果是CNN就直接编码成一个9*9的矩阵。

import torch.utils.data as data
import torch
import pandas as pd


def create_sudoku_tensors(input_data, one_hot=False, train_split=0.7):
    """
    分割训练集和测试集
    :param input_data: 待分割的数据
    :param one_hot: 是否使用one hot编码
    :param train_split: 分割的比例
    :return: 训练集,测试集
    """
    # 数据的长度
    data_size = input_data.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

    # 给每一个数独题信息进行编码
    def encode(s):
        zeros = torch.zeros((1, 9, 9), dtype=torch.float)
        for a in range(9):
            for b in range(9):
                zeros[0, a, b] = int(s[a * 9 + b]) - 1
        return zeros

    # 得到编码内容
    if one_hot:
        quizzes_t = input_data.quizzes.apply(one_hot_encode)
        solutions_t = input_data.solutions.apply(one_hot_encode)
    else:
        quizzes_t = input_data.quizzes.apply(encode)
        solutions_t = input_data.solutions.apply(encode)
    # 将编码好的内容拼接起来
    quizzes_t = torch.cat(quizzes_t.values.tolist())
    solutions_t = torch.cat(solutions_t.values.tolist())

    # 按比例进行随机分割
    randperm = torch.randperm(data_size)
    train = randperm[:int(train_split * data_size)]
    test = randperm[int(train_split * data_size):]

    # 打包训练集和标签
    return data.TensorDataset(quizzes_t[train], solutions_t[train]), \
           data.TensorDataset(quizzes_t[test], solutions_t[test])


def create_constraint_mask():
    """
    创建一个mask
    :return: 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(filepath, one_hot=False, subsample=10000):
    """
    加载数据集
    :param filepath: 数据集文件
    :param one_hot: 是否使用one hot编码
    :param subsample: 数据集总共的行数
    :return: 训练集、测试集
    """

    dataset = pd.read_csv(filepath, sep=',')
    # 返回随机 subsample 行数据
    my_sample = dataset.sample(subsample)
    # 分割出训练集和测试集
    train_set, test_set = create_sudoku_tensors(my_sample, one_hot)
    return train_set, test_set

通过下面的代码我们可以测试读取的数据格式。

# TEST
train_set, test_set = load_dataset("./src/data/sudoku_test.csv", True, 10)
for train_quiz, train_label in train_set:
    print(train_quiz.shape)
    print(train_label.shape)
    break
train_set, test_set = load_dataset("./src/data/sudoku_test.csv", False, 10)
for train_quiz, train_label in train_set:
    print(train_quiz.shape)
    print(train_label.shape)
    break

搭建CNN模型

这里主要是根据report3使用的FashionCNN来搭建的,大体框架基本一样,不过增加了padding,去除了池化层,同时也放弃了dropout层。

这个网络接收N*1*9*9的输入,最后会输出N*81*9的预测结果。

N是数据批数,81是9*9的数独盘面数字列表,9是对于某一个盘面数字,它是每一种数字的概率。(这里数字的取值范围是0-8,但是代表的是数字1-9,这是因为如果取值为1-9,而数独不需要填0,标签中也没有0,所以标签的取值范围还是0-8,这样就等于是标签对应不上,交叉熵损失函数还是会运行失败)

import torch.nn as nn


class SudokuCNN(nn.Module):
    def __init__(self):
        super(SudokuCNN, self).__init__()
        # 第一层卷积
        # 输入[1,9,9]
        self.layer1 = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=64, kernel_size=(3, 3), padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
        )

        # 9-3+2*1+1
        # 第二层卷积
        # 输入[64,9,9]
        self.layer2 = nn.Sequential(
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3, 3), padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
        )

        # 第三层卷积
        # 输入[64,9,9]
        self.layer3 = nn.Sequential(
            nn.Conv2d(in_channels=128, out_channels=128, kernel_size=(3, 3), padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
        )

        # 全连接层
        # 输入[128, 9, 9]
        self.fc1 = nn.Linear(in_features=128 * 9 * 9, out_features=1000)
        self.fc2 = nn.Linear(in_features=1000, out_features=81 * 9)
        self.fc3 = nn.Softmax(dim=2)

    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = self.layer3(out)

        # 矩阵展开为向量
        out = out.view(out.size(0), -1)

        out = self.fc1(out)
        out = self.fc2(out)

        # 向量变化为矩阵(81*9)
        out = out.view(out.shape[0], 81, 9)

        out = self.fc3(out)
        return out

训练CNN模型

下面是训练CNN的代码,采用的是Adam优化器和CrossEntropyLoss损失函数。

测试的时候发现这里的学习率要取比较小,取大了loss可能就不下降,或者仅仅是下降了一点点,然后发生震荡。

import copy
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.autograd import Variable
from torch.utils.data import Dataset

# 尝试使用GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 加载数据集
batch_size = 100
train_set, test_set = load_dataset("./src/data/sudoku.csv", False, 50000)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size)

# 加载模型
model = SudokuCNN()
model.to(device)
# 损失函数
error = nn.CrossEntropyLoss()
# 定义学习率
learning_rate = 0.0005

# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 循环次数
num_epochs = 50
# 迭代次数
count = 0
# 是否画图
display = True
# 最小迭代次数
min_epochs = 5
# 最小的损失
min_loss = float('inf')
# 最优模型
best_model = None

# Lists for visualization of loss_function and accuracy
loss_list = []
iteration_list = []
accuracy_list = []

for epoch in range(num_epochs):
    #  分批次加载数据集
    for train_quiz, train_label in train_loader:
        train_quiz, train_label = train_quiz.to(device), train_label.to(device)
        train_quiz = Variable(train_quiz.view(train_quiz.shape[0], 1, 9, 9))
        train_label = Variable(train_label.view(-1))

        # Forward pass
        outputs = model(train_quiz)
        outputs = outputs.view(-1, 9)

        loss = error(outputs, train_label.long())

        # Initializing a gradient as 0 so there is no mixing of gradient among the batches
        optimizer.zero_grad()

        # Propagating the error backward
        loss.backward()

        # Optimizing the parameters
        optimizer.step()

        count += 1

        # print('Epoch: {}, Batch: {}, Avg. Loss: {}'.format(epoch, count, loss_function.item()))

        # Testing the model
        if not (count % 50):  # It's same as "if count % 50 == 0"
            total = 0
            correct = 0

            for test_quiz, test_label in test_loader:
                test_quiz, test_label = test_quiz.to(device), test_label.to(device)
                test_quiz = Variable(test_quiz.view(test_quiz.shape[0], 1, 9, 9))
                test_label = Variable(test_label.view(-1))

                outputs = model(test_quiz)
                outputs = outputs.view(-1, 9)

                predictions = torch.max(outputs, 1)[1].to(device)
                correct += (predictions == test_label).sum()

                total += len(test_label)

            accuracy = correct * 100 / total
            loss_list.append(loss.data)
            iteration_list.append(count)
            accuracy_list.append(accuracy)

        if not (count % 100):
            print("Iteration: {}, Loss: {}, Accuracy: {}%".format(count, loss.data, accuracy))

运行输出:

Iteration: 100, Loss: 2.154716968536377, Accuracy: 17.909875869750977%
Iteration: 200, Loss: 2.1245620250701904, Accuracy: 22.032262802124023%
Iteration: 300, Loss: 2.091231107711792, Accuracy: 26.08452606201172%
Iteration: 400, Loss: 2.0490970611572266, Accuracy: 30.193002700805664%
Iteration: 500, Loss: 2.010570764541626, Accuracy: 34.447654724121094%
Iteration: 600, Loss: 1.973477840423584, Accuracy: 38.08658218383789%
Iteration: 700, Loss: 1.9365869760513306, Accuracy: 41.5409049987793%
Iteration: 800, Loss: 1.8946945667266846, Accuracy: 45.34855651855469%
Iteration: 900, Loss: 1.8635038137435913, Accuracy: 48.742549896240234%

.........................................

Iteration: 17100, Loss: 1.4262917041778564, Accuracy: 71.87982940673828%
Iteration: 17200, Loss: 1.4250632524490356, Accuracy: 71.79407501220703%
Iteration: 17300, Loss: 1.423291802406311, Accuracy: 71.8446044921875%
Iteration: 17400, Loss: 1.419376254081726, Accuracy: 71.94452667236328%
Iteration: 17500, Loss: 1.4205855131149292, Accuracy: 71.94963073730469%

实现过程中出现的问题

前面处理数据和定义模型都挺顺利的,但是训练CNN模型这里出现的问题特别特别多,整个实现过程好像也就这部分出了问题,耽误了比较长的时间。

这里主要是因为自己对交叉熵损失函数的具体实现过程不太了解,同时对pytorch处理tensor的一些常用手法不太熟练。

一开始我是用one hot编码在这里训练和测试,但是交叉熵损失函数并不支持one hot编码,并且它还会报错,报错内容大概是“multi target”相关的,这导致我一开始没有精准的定位问题所在,然后我尝试了其他的损失函数,有的可以用但可能因为我当时的数据格式处理本身有问题,最后的结果不收敛。所以后面我专门去学习了pytorch中的损失函数,并且也知道了交叉熵损失函数是不支持one hot编码的。

之后我就修改了处理数据的代码,让CNN得到9*9的数据,而RNN继续使用one hot编码。但是因为我的网络计算出来的是batch*81*9的,并且标签也是batch*81的,这样在CrossEntropyLoss函数里面还是会报错,报错内容大概是“期望 batch*9 但是得到了 batch*81”。(batch是数据批数)

这里的报错其实也很奇怪,上网查了比较久也没解决这个问题。后面我好好想了一下,*这个数独问题可以理解为一个特殊的分类问题,我们对每一个格子进行分类,判断它是什么数字类型。**这样的话,就很自然的想到要把预测结果和标签都展开,预测结果展开为[(batch\81), 9]的二维矩阵,而标签就展开为[batch*81]的向量。

通过这样的处理方式,我终于能够开始训练这个CNN网络了。(上面找问题找了四五天,不过解决其实就花了一个下午,感觉定位问题的位置很关键!)

一个样例

这里是一个样例代码,我在这个过程中犯的错误用比较简单的形式给再现了出来。

首先定义好各种变量。

import numpy as np
import torch
import torch.nn.functional as F

# 假设这是一个3分类问题,一共有4组样本

# 下面是这个模型的输出
pred_y = np.array([[0.30722019, -0.8358033, -1.24752918],
                   [0.72186664, 0.58657704, -0.25026393],
                   [0.16449865, -0.44255082, 0.68046693],
                   [-0.52082402, 1.71407838, -1.36618063]])
pred_y = torch.from_numpy(pred_y)

# 真实的标签如下所示,很明显这里就是one hot编码
true_y_one_hot = np.array([[1, 0, 0],
                   [0, 1, 0],
                   [0, 1, 0],
                   [0, 0, 1]])
true_y_one_hot = torch.from_numpy(true_y_one_hot)

# 这是采用普通编码的标签
# true_y_1是正确编码的,对于数独问题,我们认为这里的标签实际上代表1,2,2,3
true_y_1 = np.array([0, 1, 1, 2])
true_y_1 = torch.from_numpy(true_y_1)
# true_y_2是错误编码的,这最后会导致输出类型和标签对应不上
true_y_2 = np.array([1, 2, 2, 3])
true_y_2 = torch.from_numpy(true_y_2)

如果使用one hot编码,这样的运行结果如下所示:

loss = F.cross_entropy(pred_y, true_y_one_hot)
print(loss)

运行输出:

---------------------------------------------------------------------------

RuntimeError                              Traceback (most recent call last)

<ipython-input-32-64c355d4eafe> in <module>
----> 1 loss = F.cross_entropy(pred_y, true_y_one_hot)
      2 print(loss)

~/.local/lib/python3.6/site-packages/torch/nn/functional.py in cross_entropy(input, target, weight, size_average, ignore_index, reduce, reduction)
   2822     if size_average is not None or reduce is not None:
   2823         reduction = _Reduction.legacy_get_string(size_average, reduce)
-> 2824     return torch._C._nn.cross_entropy_loss(input, target, weight, _Reduction.get_enum(reduction), ignore_index)
   2825 
   2826 
   
RuntimeError: 1D target tensor expected, multi-target not supported

如果编码不正确,输出类型和标签不对应,就会发生下面这样的情况(在数独问题里面其实还要复杂一点):

loss = F.cross_entropy(pred_y, true_y_2)
print(loss)

运行输出:

---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-33-a6c07789433b> in <module>
----> 1 loss = F.cross_entropy(pred_y, true_y_2)
      2 print(loss)

~/.local/lib/python3.6/site-packages/torch/nn/functional.py in cross_entropy(input, target, weight, size_average, ignore_index, reduce, reduction)
   2822     if size_average is not None or reduce is not None:
   2823         reduction = _Reduction.legacy_get_string(size_average, reduce)
-> 2824     return torch._C._nn.cross_entropy_loss(input, target, weight, _Reduction.get_enum(reduction), ignore_index)
   2825 
   2826 

IndexError: Target 3 is out of bounds.

正常运行的话,是下面这样的情况:

loss = F.cross_entropy(pred_y, true_y_1)
print(loss)

运行输出:

tensor(1.5929, dtype=torch.float64)

测试CNN模型

通过下面的代码可以简单测试训练好的CNN模型。

for test_quiz, test_label in test_loader:
    test_quiz, test_label = test_quiz.to(device), test_label.to(device)
    test_quiz = Variable(test_quiz.view(test_quiz.shape[0], 1, 9, 9))
    test_label = Variable(test_label.view(-1))

    outputs = model(test_quiz)
    outputs = outputs.view(-1, 9)

    predictions = torch.max(outputs, 1)[1].to(device)
    correct += (predictions == test_label).sum()

    total += len(test_label)

accuracy = correct * 100 / total

print("Test Accuracy: {}%".format(accuracy))

运行输出:

Test Accuracy: 71.94963073730469%

结果可视化

我们可以把训练和测试过程中的损失与准确率画出来。

# 画出迭代中的损失
plt.figure(1)
plt.plot(iteration_list, loss_list)
plt.xlabel("No. of Iteration")
plt.ylabel("Loss")
plt.title("Iterations vs Loss")
# 画出迭代中的准确度
plt.figure(2)
plt.plot(iteration_list, accuracy_list)
plt.xlabel("No. of Iteration")
plt.ylabel("Accuracy")
plt.title("Iterations vs Accuracy")

plt.show()

运行输出:

尝试使用RNN模型

在一开始做这个项目的时候,我查了很多关于CNN解决数独问题的资料,期间偶然了解到数独用循环网络做的效果会比较好,同时又找到一篇PyTorch中的数独RNN的文章,所以我打算尝试一下RNN模型做数独问题。

在这个环节中,RNN模型是从这篇文章里面搬运过来的,而训练过程有参考这篇文章,但为了和CNN进行对比也做了不少修改。

搭建RNN模型

下面是搭建的RNN模型。

import torch
import torch.nn as nn


class SudokuRNN(nn.Module):
    def __init__(self, constraint_mask, n=9, hidden1=100):
        super(SudokuRNN, 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

训练并测试

import copy
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.autograd import Variable
from torch.utils.data import Dataset

# FIXME 这里RNN不能使用GPU,尝试着用就会报错。感觉是定义模型的时候出的问题。。。
# device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
device = torch.device("cpu")

# 加载数据集
batch_size = 100
train_set, test_set =load_dataset("./src/data/sudoku.csv", True, 10000)
# train_set, test_set = load_dataset("./data/sudoku_test.csv", True, 1000)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size)

# 加载模型
model = SudokuRNN(create_constraint_mask())
model.to(device)
# 损失函数
loss_fun = nn.MSELoss()
# 定义学习率
learning_rate = 0.01
# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=0.000)
# 循环次数(用cpu训练,不敢弄太大了,训练时间太长)
num_epochs = 1
# 迭代次数
count = 0
# 是否画图
display = True
# 最小迭代次数
min_epochs = 0
# 最小的损失
min_loss = float('inf')
# 最优模型
best_model = None

# Lists for visualization of loss_function and accuracy
loss_list = []
iteration_list = []
accuracy_list = []
accuracy = 0

for epoch in range(num_epochs):
    #  分批次加载数据集
    for train_quiz, train_label in train_loader:
        train_quiz, train_label = train_quiz.to(device), train_label.to(device)
        train_quiz = Variable(train_quiz)
        train_label = Variable(train_label)

        model.train()
        optimizer.zero_grad()

        pred, mat = model(train_quiz)

        loss = loss_fun(pred, train_label)

        loss.backward()

        # Optimizing the parameters
        optimizer.step()

        count += 1

        model.eval()
        # Testing the model
        if not (count % 2):  # It's same as "if count % 50 == 0"
            total = 0
            correct = 0

            for test_quiz, test_label in test_loader:
                test_quiz, test_label = test_quiz.to(device), test_label.to(device)
                test_quiz = Variable(test_quiz)
                test_label = Variable(test_label)

                test_pred, test_fill = model(test_quiz)

                correct += (test_fill.max(dim=2)[1] == test_label.max(dim=2)[1]).sum().item()

                total += len(test_label) * 81

            accuracy = correct / total
            loss_list.append(loss.data)
            iteration_list.append(count)
            accuracy_list.append(accuracy)

            print("ACC: ", accuracy)
            print("error in cells: ", total - correct)

        if not (count % 5):
            print("Iteration: {}, Loss: {}, Accuracy: {}".format(count, loss.item(), accuracy))

运行输出:

ACC:  0.6399711934156379
error in cells:  87487
ACC:  0.7732098765432098
error in cells:  55110
Iteration: 5, Loss: 0.05192449316382408, Accuracy: 0.7732098765432098
ACC:  0.820440329218107
error in cells:  43633
ACC:  0.837559670781893
error in cells:  39473

....................................................................

ACC:  0.9995720164609053
error in cells:  104
Iteration: 65, Loss: 0.0002745767415035516, Accuracy: 0.9995720164609053
ACC:  0.9996378600823045
error in cells:  88
ACC:  0.9998024691358025
error in cells:  48
ACC:  0.9998106995884773
error in cells:  46
Iteration: 70, Loss: 0.00030164315830916166, Accuracy: 0.9998106995884773

结果可视化

# 画出迭代中的损失
plt.figure(1)
plt.plot(iteration_list, loss_list)
plt.xlabel("No. of Iteration")
plt.ylabel("Loss")
plt.title("Iterations vs Loss")
# 画出迭代中的准确度
plt.figure(2)
plt.plot(iteration_list, accuracy_list)
plt.xlabel("No. of Iteration")
plt.ylabel("Accuracy")
plt.title("Iterations vs Accuracy")

plt.show()

对比CNN与RNN

对CNN模型进行测试

import torch
import time
from torch.autograd import Variable
from torch.utils.data import Dataset

print("===================对CNN模型进行测试===================")

model_path="./src/SudokuCNN_model_1.40394.pth"
test_size=10000

# 为公平起见,CNN也是用cpu
device = torch.device("cpu")

# 加载数据集
batch_size = 100
# 这里因为是测试,所以就取分割数据中的训练集部分做测试了(不是训练模型的训练集!)
test_set, _ = load_dataset("./src/data/sudoku.csv", False, test_size)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size)

# 加载模型
model = SudokuCNN()
model.to(device)
model.load_state_dict(torch.load(model_path))

# Testing the model
total = 0
correct = 0

# 开始计时
start = time.clock()
    
for test_quiz, test_label in test_loader:
    test_quiz, test_label = test_quiz.to(device), test_label.to(device)
    test_quiz = Variable(test_quiz.view(test_quiz.shape[0], 1, 9, 9))
    test_label = Variable(test_label.view(-1))

    outputs = model(test_quiz)
    outputs = outputs.view(-1, 9)

    predictions = torch.max(outputs, 1)[1].to(device)
    correct += (predictions == test_label).sum()

    total += len(test_label)

# 结束计时
end = time.clock()
    
accuracy = correct * 100 / total

print("SudokuCNN accuracy: {}%".format(accuracy))
print("SudokuCNN cells in error: {}".format(total - correct))
print("SudokuCNN time = %s" % str(end - start))

print("====================================================")

运行输出:

===================对CNN模型进行测试===================
SudokuCNN accuracy: 75.1141128540039%
SudokuCNN cells in error: 141103
SudokuCNN time = 8.729239000000003
====================================================

对RNN模型进行测试

print("===================对RNN模型进行测试===================")

model_path="./src/SudokuRNN_model_0.00001.pth"
test_size=10000

# 尝试使用GPU
# device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
device = torch.device("cpu")

# 加载数据集
batch_size = 100
# 这里因为是测试,所以就取分割数据中的训练集部分做测试了(不是训练模型的训练集!)
test_set, _ = load_dataset("./src/data/sudoku.csv", True, test_size)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size)

# 加载模型
model = SudokuRNN(create_constraint_mask())
model.to(device)
model.load_state_dict(torch.load(model_path))

# Testing the model
total = 0
correct = 0

# 开始计时
start = time.clock()

for test_quiz, test_label in test_loader:
    test_quiz, test_label = test_quiz.to(device), test_label.to(device)
    test_quiz = Variable(test_quiz)
    test_label = Variable(test_label)

    test_pred, test_fill = model(test_quiz)

    correct += (test_fill.max(dim=2)[1] == test_label.max(dim=2)[1]).sum().item()

    total += len(test_label) * 81

# 结束计时
end = time.clock()

accuracy = correct * 100 / total

print("SudokuRNN accuracy: {}%".format(accuracy))
print("SudokuRNN cells in error: {}".format(total - correct))
print("SudokuRNN time = %s" % str(end - start))

print("====================================================")

运行输出:

===================对RNN模型进行测试===================
SudokuRNN accuracy: 99.96490299823633%
SudokuRNN cells in error: 199
SudokuRNN time = 337.7354459999997
====================================================

比较分析

从上面的结果我们发现:

  • RNN的准确率远远超过CNN,RNN达到了99.6%的准确率,而CNN只有75%的准确率。

  • RNN的错误单元格数目也远比CNN少,后者的错误数是前者的100倍。

  • 但是RNN解决数独问题花费的时间比CNN多得多。

总结

我觉得数独问题和之前的服装分类问题是相通的:

  • 服装分类问题是一个非常经典的多分类问题,我们需要对10种服装进行分类;

  • 而数独问题可以认为是一个特殊的多分类问题,我们可以说是对每一个单元格进行分类,最后通过这些单元格的分类结果,得到数独的解决方案。

关于卷积神经网络解决数独问题是不是通过大量的参数记忆答案:

我在网上查阅资料的时候,有一部分文章和评论认为使用CNN或者NN解决数独是单纯的用非常多的参数去记住数独的每一个答案,这个观点我不知道是否正确,但是我对这个看法产生了比较大的兴趣,所以我后面有尝试修改我设计的卷积网络的参数,比如把卷积层节点调整为256或者512,再把全连接层的节点增加1000到2000个,再对网络进行训练和测试。

不过在我的测试中,我发现无论我怎么改参数(改太大会提示我GPU显存不够),这个网络的准确率最后都是75%左右,并且后面梯度下降的特别慢。所以我觉得可能卷积神经网络是通过空间特征提取,学习到了解决数独问题的函数,而不是仅仅通过大量的参数来记忆数独的答案。


文章作者: Immortalqx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Immortalqx !
评论
  目录