4

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

 2 years ago
source link: https://gsy00517.github.io/kaggle20191102112435/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集 | 高深远的博客

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

发表于 2019-11-02 | 更新于: 2020-02-07 | 分类于 人工智能 | 0 | 阅读次数:
字数统计: 4.3k字 | 阅读时长 ≈ 17分钟

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


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

import numpy as np
import csv
import operator

import matplotlib
from matplotlib import pyplot as plt
%matplotlib inline

导入训练数据:

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:] #分割出训练集数据

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

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。

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

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

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的分类函数。

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值输入。

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值,但由于事先无法把握训练结束的时间,可以编写函数自动选择并使程序继续进行。

theK = 3
hasError = training[0]
for i in range(7):
if training[i] < hasError:
theK = i + 3
hasError = training[i]

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

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了。

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上,这结果的排名就要到两千左右了。


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

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()查看导入的结果,便于后续的处理。

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

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

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查看数据处理的结果。

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]]

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

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

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

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

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

trainData = Data.TensorDataset(train_data, train_labels) #用后会变成元组类型

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

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

#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算法要优越许多。

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))

代入测试集求解:

output = cnn(test_data[:])
#print(output)

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

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

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. 首先,增加训练量,调整超参数如下。

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

    #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绘制出处理后的图像,但是却出现了如上报错,当时的代码如下:

    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文件后还要对文件进行空值删除处理,比较麻烦(后文会写具体如何处理),因此我想借用文章顶部给出的第二个链接中提供的方法:

    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. 等待处理完毕。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK