kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集

kaggle是一个著名的数据科学竞赛平台,暑假里我也抽空自己独立完成了三四个getting started级别的比赛。对于MNIST数据集,想必入门计算机视觉的人应该也不会陌生。kaggle上getting started的第一个比赛就是Digit Recognizer:Learn computer vision fundamentals with the famous MNIST data。当时作为入门小白的我,使用了入门级的方法KNN完成了我的第一次机器学习(自认为KNN是最最基础的算法,对它的介绍可见我的另一篇博文machine-learning笔记:机器学习的几个常见算法及其优缺点,真的非常简单,但也极其笨拙)。而最近我又使用CNN再一次尝试了这个数据集,踩了不少坑,因此想把两次经历统统记录在这,可能会有一些不足之处,留作以后整理优化。

References

电子文献:
https://blog.csdn.net/gybinlero/article/details/79294649
https://blog.csdn.net/qq_43497702/article/details/95005248
https://blog.csdn.net/a19990412/article/details/90349429


KNN

首先导入必要的包,这里基本用不到太多:

1
2
3
4
5
6
7
import numpy as np
import csv
import operator

import matplotlib
from matplotlib import pyplot as plt
%matplotlib inline

导入训练数据:

1
2
3
4
5
6
7
8
9
10
trainSet = []
with open('train.csv','r') as trainFile:
lines=csv.reader(trainFile)
for line in lines:
trainSet.append(line)
trainSet.remove(trainSet[0])

trainSet = np.array(trainSet)
rawTrainLabel = trainSet[:, 0] #分割出训练集标签
rawTrainData = trainSet[:, 1:] #分割出训练集数据

我当时用了一种比较笨拙的办法转换数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
rawTrainData = np.mat(rawTrainData) #转化成矩阵,或许不需要
m, n = np.shape(rawTrainData)
trainData = np.zeros((m, n)) #创建初值为0的ndarray
for i in range(m):
for j in range(n):
trainData[i, j] = int(rawTrainData[i, j]) #转化并赋值

rawTrainLabel = np.mat(rawTrainLabel) #或许不需要
m, n = np.shape(rawTrainLabel)
trainLabel = np.zeros((m, n))
for i in range(m):
for j in range(n):
trainLabel[i, j] = int(rawTrainLabel[i, j])

这里我们可以查看以下数据的维度,确保没有出错。

为了方便起见,我们把所有pixel不为0的点都设置为1。

1
2
3
4
5
m, n = np.shape(trainData)
for i in range(m):
for j in range(n):
if trainData[i, j] != 0:
trainData[i, j] = 1

仿照训练集的步骤,导入测试集并做相同处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
testSet = []
with open('test.csv','r') as testFile:
lines=csv.reader(testFile)
for line in lines:
testSet.append(line)
testSet.remove(testSet[0])

testSet = np.array(testSet)
rawTestData = testSet

rawTestData = np.mat(rawTestData)
m, n = np.shape(rawTestData)
testData = np.zeros((m, n))
for i in range(m):
for j in range(n):
testData[i, j] = int(rawTestData[i, j])

m, n = np.shape(testData)
for i in range(m):
for j in range(n):
if testData[i, j] != 0:
testData[i, j] = 1

同样的,可使用testData.shape查看测试集的维度,保证它是28000*784,由此可知操作无误。
接下来,我们定义KNN的分类函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def classify(inX, dataSet, labels, k):
inX = np.mat(inX)
dataSet = np.mat(dataSet)
labels = np.mat(labels)
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = np.array(diffMat) ** 2
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances ** 0.5
sortedDistIndicies = distances.argsort()
classCount={}
for i in range(k):
voteIlabel = labels[0, sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse = True)
return sortedClassCount[0][0]

为了更好地分类,这里我们需要选择合适的k值,我选取了4000个样本作为验证机进行尝试,找到误差最小的k值并作为最终的k值输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
trainingTestSize = 4000

#分割出验证集
m, n = np.shape(trainLabel)
trainingTrainLabel = np.zeros((m, n - trainingTestSize))
for i in range(m):
for j in range(n - trainingTestSize):
trainingTrainLabel[i, j] = trainLabel[i, j]

trainingTestLabel = np.zeros((m, trainingTestSize))
for i in range(m):
for j in range(trainingTestSize):
trainingTestLabel[i, j] = trainLabel[i, n - trainingTestSize + j]

m, n = np.shape(trainData)
trainingTrainData = np.zeros((m - trainingTestSize, n))
for i in range(m - trainingTestSize):
for j in range(n):
trainingTrainData[i, j] = trainData[i, j]

trainingTestData = np.zeros((trainingTestSize, n))
for i in range(trainingTestSize):
for j in range(n):
trainingTestData[i, j] = trainData[m - trainingTestSize + i, j]

#使k值为3到9依次尝试
training = []
for x in range(3, 10):
error = 0
for y in range(trainingTestSize):
answer = (classify(trainingTestData[y], trainingTrainData, trainingTrainLabel, x))
print 'the classifier came back with: %d, %.2f%% has done, the k now is %d' % (answer, (y + (x - 3) * trainingTestSize) / float(trainingTestSize * 7) * 100, x) #方便知道进度
if answer != trainingTestLabel[0, y]:
error += 1
training.append(error)

这个过程比较长,结果会得到training的结果是[156, 173, 159, 164, 152, 155, 156]。
可以使用plt.plot(training)更直观地查看误差,呈现如下:

注意:这里的下标应该加上3才是对应的k值。

可以看图手动选择k值,但由于事先无法把握训练结束的时间,可以编写函数自动选择并使程序继续进行。

1
2
3
4
5
6
theK = 3
hasError = training[0]
for i in range(7):
if training[i] < hasError:
theK = i + 3
hasError = training[i]

在确定k值后,接下来就是代入测试集进行结果的计算了。由于KNN算法相对而言比较低级,因此就别指望效率了,跑CPU的话整个过程大概需要半天左右。

1
2
3
4
5
6
m, n = np.shape(testData)
result = []
for i in range(m):
answer = (classify(testData[i], trainData, trainLabel, theK))
result.append(answer)
print 'the classifier came back with: %d, %.2f%% has done' % (answer, i / float(m) * 100)

最后,定义一个保存结果的函数,然后saveResult(result)之后,再对csv文件进行处理(后文会提到),然后就可以submit了。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最终此方法在kaggle上获得的score为0.96314,准确率还是挺高的,主要是因为问题相对简单,放到leaderboard上,这结果的排名就要到两千左右了。


CNN

在学习了卷积神经网络和pytorch框架之后,我决定使用CNN对这个比赛再进行一次尝试。
首先还是导入相关的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import pandas as pd
import numpy as np
from math import *

%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.cm as cm

import torch.utils.data as Data

from torch.autograd import Variable

import csv

导入训练数据,可以使用train.head()查看导入的结果,便于后续的处理。

1
train = pd.read_csv("train.csv")

对数据进行处理,由于要使用的是CNN,我们必须要把数据整理成能输入的形式,即从数组变成高维张量。

1
2
3
4
5
train_labels = torch.from_numpy(np.array(train.label[:]))

image_size = train.iloc[:, 1:].shape[1]
image_width = image_height = np.ceil(np.sqrt(image_size)).astype(np.uint8)
train_data = torch.FloatTensor(np.array(train.iloc[:, 1:]).reshape((-1, 1, image_width, image_height))) / 255 #灰度压缩,进行归一化

注:reshape中的-1表示自适应,这样我们能让我们更好的变化数据的形式。

我们可以使用matplotlib查看数据处理的结果。

1
2
3
plt.imshow(train_data[1].numpy().squeeze(), cmap = 'gray')
plt.title('%i' % train_labels[1])
plt.show()

可以看到如下图片,可以与plt.title进行核对。

注:可以用squeeze()函数来降维,例如:从[[1]]—>[1]
与之相反的是便是unsqueeze(dim = 1),该函数可以使[1]—>[[1]]

以同样的方式导入并处理测试集。

1
2
test= pd.read_csv("test.csv")
test_data = torch.FloatTensor(np.array(test).reshape((-1, 1, image_width, image_height))) / 255

接下来我们定义几个超参数,这里将要使用的是小批梯度下降的优化算法,因此定义如下:

1
2
3
4
#超参数
EPOCH = 1 #整个数据集循环训练的轮数
BATCH_SIZE = 10 #每批的样本个数
LR = 0.01 #学习率

定义好超参数之后,我们使用Data对数据进行最后的处理。

1
2
3
4
5
6
7
trainData = Data.TensorDataset(train_data, train_labels) #用后会变成元组类型

train_loader = Data.DataLoader(
dataset = trainData,
batch_size = BATCH_SIZE,
shuffle = True
)

上面的Data.TensorDataset可以把数据进行打包,以方便我们更好的使用;而Data.DataLoade可以将我们的数据打乱并且分批。要注意的是,这里不要对测试集进行操作,否则最终输出的结果就难以再与原来的顺序匹配了。
接下来,我们定义卷积神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#build CNN
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
#一个卷积层
self.conv1 = nn.Sequential(
nn.Conv2d( #输入(1, 28, 28)
in_channels = 1, #1个通道
out_channels = 16, #输出层数
kernel_size = 5, #过滤器的大小
stride = 1, #步长
padding = 2 #填白
), #输出(16, 28, 28)
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
)
self.conv2 = nn.Sequential( #输入(16, 14, 14)
nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
)
self.out = nn.Linear(32 * 7 * 7, 10) #全连接层,将三维的数据展为2维的数据并输出

def forward(self, x): #父类已定义,不能修改名字
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
output = F.softmax(self.out(x))
return output

cnn = CNN()
optimzer = torch.optim.Adam(cnn.parameters(), lr = LR) #define optimezer
loss_func = nn.CrossEntropyLoss() #loss function使用交叉嫡误差

print(cnn) # 查看net architecture

完成以上的操作之后,就可以开始训练了,整个训练时间在CPU上只需要几分钟,这比KNN算法要优越许多。

1
2
3
4
5
6
7
8
9
10
11
12
for epoch in range(EPOCH):
for step, (x, y) in enumerate(train_loader):
b_x = Variable(x)
b_y = Variable(y)
output = cnn(b_x)
loss = loss_func(output, b_y) #cross entropy loss
#update W
optimzer.zero_grad()
loss.backward()
optimzer.step()
print('epoch%d' % (epoch + 1), '-', 'batch%d' % step, '-', 'loss%f' % loss) #查看训练过程
print('No.%depoch is over' % (epoch + 1))

代入测试集求解:

1
2
3
4
5
output = cnn(test_data[:])
#print(output)

result = torch.max(output, 1)[1].squeeze()
#print(result)

仿照KNN中的结果转存函数,定义saveResult函数。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最后使用saveResult(result.numpy())把结果存入csv文件。


改进

然而,若使用上述的CNN,得出的结果在leaderboard上会达到两千三百多名,这已经进入所有参赛者的倒数两百名之内了。为什么这个CNN的表现甚至不如我前面的KNN算法呢?我觉得主要有下面三个原因。

  1. 首先,由于CNN的参数较多,仅经过1轮epoch应该是不足够把所有参数训练到最优或者接近最优的位置的。个人认为,靠前的数据在参数相对远离最优值时参与训练而在之后不起作用,很有可能导致最后顾此失彼,因此有必要增加epoch使之前的数据多次参与参数的校正。同时,也要增大batch size使每次优化参数使用的样本更多,从而在测试集上表现更好。训练结束后,我发现我的C盘会被占用几个G,不知道是不是出错了,也有可能是参数占用的空间,必须停止kernel才能得到释放(我关闭了VScode后刷新,空间就回来了)。关于内存,这里似乎存在着一个问题,我将在后文阐述。

    注:由于VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试。

  2. 学习率过大。尽管我这里的学习率设置为0.01,但对于最后的收敛来说或许还是偏大,这就导致了最后会在最优解附近来回抖动而难以接近的问题。关于这个问题,可以到deep-learning笔记:学习率衰减与批归一化中看看我较为详细的分析与解决方法。

  3. 由于训练时间和epoch轮数相对较小,我推测模型可能会存在过拟合的问题。尤其是最后的全连接层,它的结构很容易造成过拟合。关于这个问题,也可以到machine-learning笔记:过拟合与欠拟合machine-learning笔记:机器学习中正则化的理解中看看我较为详细的分析与解决方法。

针对上述原因,我对我的CNN模型做了如下调整:

  1. 首先,增加训练量,调整超参数如下。

    1
    2
    3
    4
    #超参数
    EOPCH = 3
    BATCH_SIZE = 50
    LR = 1e-4
  2. 引入dropout随机失活,加强全连接层的鲁棒性,修改网络结构如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #build CNN
    class CNN(nn.Module):
    def __init__(self):
    super(CNN, self).__init__()
    #一个卷积层
    self.conv1 = nn.Sequential(
    nn.Conv2d( #输入(1, 28, 28)
    in_channels = 1, #1个通道
    out_channels = 16, #输出层数
    kernel_size = 5, #过滤器的大小
    stride = 1, #步长
    padding = 2 #填白
    ), #输出(16, 28, 28)
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
    )
    self.conv2 = nn.Sequential( #输入(16, 14, 14)
    nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
    )

    self.dropout = nn.Dropout(p = 0.5) #每次减少50%神经元之间的连接

    self.fc = nn.Linear(32 * 7 * 7, 1024)
    self.out = nn.Linear(1024, 10) #全连接层,将三维的数据展为2维的数据并输出

    def forward(self, x):
    x = self.conv1(x)
    x = self.conv2(x)
    x = x.view(x.size(0), -1)
    x = self.fc(x)
    x = self.dropout(x)
    output = F.softmax(self.out(x))
    return output

本想直接使用torch.nn.functional中的dropout函数轻松实现随机失活正则化,但在网上看到这个函数好像有点坑,因此就不以身试坑了,还是在网络初始化中先定义dropout。

注:训练完新定义的网络之后我一直在思考dropout添加的方式与位置。在看了一些资料之后,我认为或许去掉全连接层、保持原来的层数并在softmax之前dropout可能能达到更好的效果。考虑到知乎上有知友提到做研究试验不宜在MNIST这些玩具级别的数据集上进行,因此暂时不再做没有太大意义的调整,今后有空在做改进试验。

经过上面的改进后,我再次训练网络并提交结果,在kaggle上的评分提高至0.97328,大约处在1600名左右,可以继续调整超参数(可以分割验证集寻找)和加深网络结构以取得更高的分数,但我的目的已经达到了。与之前的KNN相比,无论从时间效率还是准确率,CNN都有很大的进步,这也体现了深度学习相对于一些经典机器学习算法的优势。


出现的问题

由于这个最后的网络是我重复构建之后完成的,因此下列部分问题可能不存在于我上面的代码中,但我还是想汇总在这,以防之后继续踩相同的坑。

  1. 报错element 0 of tensors does not require grad and does not have a grad_fn

    pytorch具有自动求导机制,这就省去了我们编写反向传播的代码。每个Variable变量都有两个标志:requires_grad和volatile。出现上述问题的原因是requires_grad = False,修改或者增加(因为默认是false)成True即可。
  2. RuntimeError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

    这个好像是我在计算交叉熵时遇到的,原因是因为torch的交叉熵的输入第一个位置的输入应该是在每个label下的概率,而不是对应的label,详细分析与举例可参考文首给出的第三个链接。
  3. AttributeError: ‘tuple’ object has no attribute ‘numpy’

    为了查看数据处理效果,我在数据预处理过程中使用matplotlib绘制出处理后的图像,但是却出现了如上报错,当时的代码如下:

    1
    2
    3
    plt.imshow(trainData[1].numpy().squeeze(), cmap = 'gray')
    plt.title('%i' % train_labels[1])
    plt.show()

    查找相关资料之后,我才知道torch.utils.data会把打包的数据变成元组类型,因此我们绘图还是要使用原来train_data中的数据。

  4. 转存结果时提醒DefaultCPUAllocator: not enough memory

    由于当初在实现KNN算法转存结果时使用的函数存入csv文件后还要对文件进行空值删除处理,比较麻烦(后文会写具体如何处理),因此我想借用文章顶部给出的第二个链接中提供的方法:

    1
    2
    3
    out = pd.DataFrame(np.array(result), index = range(1, 1 + len(result)), columns = ['ImageId', 'Label'])
    #torch和pandas的类型不能直接的转换,所以需要借助numpy中间的步骤,将torch的数据转给pandas
    out.to_csv('result.csv', header = None)

    结果出现如下错误:

    我好歹也是八千多买的DELL旗舰本,8G内存,它居然说我不够让我换块新的RAM?什么情况…
    尝试许久,我怀疑是训练得到的参数占用了我的内存,那只好先把训练出的result保存下来,再导入到csv文件。
    最后我还是选择自己手动处理csv文件中的空值,应该有其它的转存csv文件的方法或者上述问题的解决措施,留待以后实践过程中发现解决,也欢迎大家不吝赐教。


excel/csv快速删除空白行

如果你使用的是我的saveResult函数或者类似,你就很有可能发现更新后的csv文件中数据之间双数行都是留空的,即一列数据之间都有空白行相隔,那么可以使用如下方法快速删除空白行。

  1. 选中对应列或者区域。
  2. 在“开始”工具栏中找到“查找与选择”功能并点击。
  3. 在下拉菜单中,点击“定位条件”选项。
  4. 在打开的定位条件窗口中,选择“空值”并确定。
  5. 待电脑为你选中所有空值后,任意右键一个被选中的空白行,在弹出的菜单中点击“删除”。
  6. 如果数据量比较大,这时候会有一个处理时间可能会比较长的提醒弹出,确认即可。
  7. 等待处理完毕。

碰到底线咯 后面没有啦

本文标题:kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集

文章作者:高深远

发布时间:2019年11月02日 - 11:24

最后更新:2020年02月07日 - 17:22

原始链接:https://gsy00517.github.io/kaggle20191102112435/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%