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 | import numpy as np |
导入训练数据:
1 | trainSet = [] |
我当时用了一种比较笨拙的办法转换数据类型:
1 | rawTrainData = np.mat(rawTrainData) #转化成矩阵,或许不需要 |
这里我们可以查看以下数据的维度,确保没有出错。
为了方便起见,我们把所有pixel不为0的点都设置为1。
1 | m, n = np.shape(trainData) |
仿照训练集的步骤,导入测试集并做相同处理:
1 | testSet = [] |
同样的,可使用testData.shape
查看测试集的维度,保证它是28000*784,由此可知操作无误。
接下来,我们定义KNN的分类函数。
1 | def classify(inX, dataSet, labels, k): |
为了更好地分类,这里我们需要选择合适的k值,我选取了4000个样本作为验证机进行尝试,找到误差最小的k值并作为最终的k值输入。
1 | trainingTestSize = 4000 |
这个过程比较长,结果会得到training的结果是[156, 173, 159, 164, 152, 155, 156]。
可以使用plt.plot(training)
更直观地查看误差,呈现如下:
注意:这里的下标应该加上3才是对应的k值。
可以看图手动选择k值,但由于事先无法把握训练结束的时间,可以编写函数自动选择并使程序继续进行。
1 | theK = 3 |
在确定k值后,接下来就是代入测试集进行结果的计算了。由于KNN算法相对而言比较低级,因此就别指望效率了,跑CPU的话整个过程大概需要半天左右。
1 | m, n = np.shape(testData) |
最后,定义一个保存结果的函数,然后saveResult(result)
之后,再对csv文件进行处理(后文会提到),然后就可以submit了。
1 | def saveResult(result): |
最终此方法在kaggle上获得的score为0.96314,准确率还是挺高的,主要是因为问题相对简单,放到leaderboard上,这结果的排名就要到两千左右了。
CNN
在学习了卷积神经网络和pytorch框架之后,我决定使用CNN对这个比赛再进行一次尝试。
首先还是导入相关的包。
1 | import torch |
导入训练数据,可以使用train.head()
查看导入的结果,便于后续的处理。
1 | train = pd.read_csv("train.csv") |
对数据进行处理,由于要使用的是CNN,我们必须要把数据整理成能输入的形式,即从数组变成高维张量。
1 | train_labels = torch.from_numpy(np.array(train.label[:])) |
注:reshape中的-1表示自适应,这样我们能让我们更好的变化数据的形式。
我们可以使用matplotlib查看数据处理的结果。
1 | plt.imshow(train_data[1].numpy().squeeze(), cmap = 'gray') |
可以看到如下图片,可以与plt.title进行核对。
注:可以用squeeze()函数来降维,例如:从
[[1]]
—>[1]
。
与之相反的是便是unsqueeze(dim = 1),该函数可以使[1]
—>[[1]]
。
以同样的方式导入并处理测试集。
1 | test= pd.read_csv("test.csv") |
接下来我们定义几个超参数,这里将要使用的是小批梯度下降的优化算法,因此定义如下:
1 | #超参数 |
定义好超参数之后,我们使用Data对数据进行最后的处理。
1 | trainData = Data.TensorDataset(train_data, train_labels) #用后会变成元组类型 |
上面的Data.TensorDataset
可以把数据进行打包,以方便我们更好的使用;而Data.DataLoade可以将我们的数据打乱并且分批。要注意的是,这里不要对测试集进行操作,否则最终输出的结果就难以再与原来的顺序匹配了。
接下来,我们定义卷积神经网络。
1 | #build CNN |
完成以上的操作之后,就可以开始训练了,整个训练时间在CPU上只需要几分钟,这比KNN算法要优越许多。
1 | for epoch in range(EPOCH): |
代入测试集求解:
1 | output = cnn(test_data[:]) |
仿照KNN中的结果转存函数,定义saveResult函数。
1 | def saveResult(result): |
最后使用saveResult(result.numpy())
把结果存入csv文件。
改进
然而,若使用上述的CNN,得出的结果在leaderboard上会达到两千三百多名,这已经进入所有参赛者的倒数两百名之内了。为什么这个CNN的表现甚至不如我前面的KNN算法呢?我觉得主要有下面三个原因。
首先,由于CNN的参数较多,仅经过1轮epoch应该是不足够把所有参数训练到最优或者接近最优的位置的。个人认为,靠前的数据在参数相对远离最优值时参与训练而在之后不起作用,很有可能导致最后顾此失彼,因此有必要增加epoch使之前的数据多次参与参数的校正。同时,也要增大batch size使每次优化参数使用的样本更多,从而在测试集上表现更好。训练结束后,我发现我的C盘会被占用几个G,不知道是不是出错了,也有可能是参数占用的空间,必须停止kernel才能得到释放(我关闭了VScode后刷新,空间就回来了)。关于内存,这里似乎存在着一个问题,我将在后文阐述。
注:由于VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试。
学习率过大。尽管我这里的学习率设置为0.01,但对于最后的收敛来说或许还是偏大,这就导致了最后会在最优解附近来回抖动而难以接近的问题。关于这个问题,可以到deep-learning笔记:学习率衰减与批归一化中看看我较为详细的分析与解决方法。
- 由于训练时间和epoch轮数相对较小,我推测模型可能会存在过拟合的问题。尤其是最后的全连接层,它的结构很容易造成过拟合。关于这个问题,也可以到machine-learning笔记:过拟合与欠拟合和machine-learning笔记:机器学习中正则化的理解中看看我较为详细的分析与解决方法。
针对上述原因,我对我的CNN模型做了如下调整:
首先,增加训练量,调整超参数如下。
1
2
3
4#超参数
EOPCH = 3
BATCH_SIZE = 50
LR = 1e-4引入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都有很大的进步,这也体现了深度学习相对于一些经典机器学习算法的优势。
出现的问题
由于这个最后的网络是我重复构建之后完成的,因此下列部分问题可能不存在于我上面的代码中,但我还是想汇总在这,以防之后继续踩相同的坑。
报错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即可。RuntimeError: Dimension out of range (expected to be in range of [-1, 0], but got 1)
这个好像是我在计算交叉熵时遇到的,原因是因为torch的交叉熵的输入第一个位置的输入应该是在每个label下的概率,而不是对应的label,详细分析与举例可参考文首给出的第三个链接。AttributeError: ‘tuple’ object has no attribute ‘numpy’
为了查看数据处理效果,我在数据预处理过程中使用matplotlib绘制出处理后的图像,但是却出现了如上报错,当时的代码如下:
1
2
3plt.imshow(trainData[1].numpy().squeeze(), cmap = 'gray')
plt.title('%i' % train_labels[1])
plt.show()查找相关资料之后,我才知道
torch.utils.data
会把打包的数据变成元组类型,因此我们绘图还是要使用原来train_data中的数据。转存结果时提醒DefaultCPUAllocator: not enough memory
由于当初在实现KNN算法转存结果时使用的函数存入csv文件后还要对文件进行空值删除处理,比较麻烦(后文会写具体如何处理),因此我想借用文章顶部给出的第二个链接中提供的方法:
1
2
3out = 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文件中数据之间双数行都是留空的,即一列数据之间都有空白行相隔,那么可以使用如下方法快速删除空白行。
- 选中对应列或者区域。
- 在“开始”工具栏中找到“查找与选择”功能并点击。
- 在下拉菜单中,点击“定位条件”选项。
- 在打开的定位条件窗口中,选择“空值”并确定。
- 待电脑为你选中所有空值后,任意右键一个被选中的空白行,在弹出的菜单中点击“删除”。
- 如果数据量比较大,这时候会有一个处理时间可能会比较长的提醒弹出,确认即可。
- 等待处理完毕。