logo头像

不忘初心 方得始终

图像分类 *K-最近邻算法*

图像分类笔记

本内容为学习斯坦福课程CS231n 2017的课后笔记记录

原文链接地址:https://blog.csdn.net/nnnnnnnnnnnny/article/details/54577123
  例子:以上图为例,图像分类模型读取该图片,并生成该图片属于集合 {cat, dog, hat, mug}中各个标签的概率。需要注意的是,对于计算机来说,图像是一个由数字组成的巨大的3维数组。在这个例子中,猫的图像大小是宽248像素,高400像素,有3个颜色通道,分别是红、绿和蓝(简称RGB)。如此,该图像就包含了248X400X3=297600个数字,每个数字都是在范围0-255之间的整型,其中0表示全黑,255表示全白。我们的任务就是把这些上百万的数字变成一个简单的标签,比如“猫”。


  图像分类的任务,就是对于一个给定的图像,预测它属于的那个分类标签(或者给出属于一系列不同标签的可能性)。图像是3维数组,数组元素是取值范围从0到255的整数。数组的尺寸是宽度x高度x3,其中这个3代表的是红、绿和蓝3个颜色通道。
困难和挑战:对于人来说,识别出一个像“猫”一样视觉概念是简单至极的,然而从计算机视觉算法的角度来看就值得深思了。我们在下面列举了计算机视觉算法在图像识别方面遇到的一些困难,要记住图像是以3维数组来表示的,数组中的元素是亮度值。
  •视角变化(Viewpoint variation):同一个物体,摄像机可以从多个角度来展现。
  •大小变化(Scale variation):物体可视的大小通常是会变化的(不仅是在图片中,在真实世界中大小也是变化的)。
  •形变(Deformation):很多东西的形状并非一成不变,会有很大变化。
  •遮挡(Occlusion):目标物体可能被挡住。有时候只有物体的一小部分(可以小到几个像素)是可见的。
  光照条件(Illumination conditions):在像素层面上,光照的影响非常大。
  背景干扰(Background clutter):物体可能混入背景之中,使之难以被辨认。
  类内差异(Intra-class variation):一类物体的个体之间的外形差异很大,比如椅子。这一类物体有许多不同的对象,每个都有自己的外形。
  面对以上所有变化及其组合,好的图像分类模型能够在维持分类结论稳定的同时,保持对类间差异足够敏感。

数据驱动方法:

  如何写一个图像分类的算法呢?这和写个排序算法可是大不一样。怎么写一个从图像中认出猫的算法?搞不清楚。因此,与其在代码中直接写明各类物体到底看起来是什么样的,倒不如说我们采取的方法和教小孩儿看图识物类似:给计算机很多数据,然后实现学习算法,让计算机学习到每个类的外形。这种方法,就是数据驱动方法。既然该方法的第一步就是收集已经做好分类标注的图片来作为训练集,那么下面就看看数据库到底长什么样:


  一个有4个视觉分类的训练集。在实际中,我们可能有上千的分类,每个分类都有成千上万的图像。

图像分类流程:

  在课程视频中已经学习过,图像分类就是输入一个元素为像素值的数组,然后给它分配一个分类标签。完整流程如下:
  •输入:输入是包含N个图像的集合,每个图像的标签是K种分类标签中的一种。这个集合称为训练集。
  •学习: 这一步的任务是使用训练集来学习每个类到底长什么样。一般该步骤叫做训练分类器或者学习一个模型。
  •评价:让分类器来预测它未曾见过的图像的分类标签,并以此来评价分类器的质量。我们会把分类器预测的标签和图像真正的分类标签对比。毫无疑问,分类器预测的分类标签和图像真正的分类标签如果一致,那就是好事,这样的情况越多越好。

Nearest Neighbor分类器

作为课程介绍的第一个方法,我们来实现一个Nearest Neighbor分类器。虽然这个分类器和卷积神经网络没有任何关系,实际中也极少使用,但通过实现它,可以让读者对于解决图像分类问题的方法有个基本的认识。
图像分类数据集:CIFAR-10。一个非常流行的图像分类数据集是CIFAR-10。这个数据集包含了60000张32X32的小图像。每张图像都有10种分类标签中的一种。这60000张图像被分为包含50000张图像的训练集和包含10000张图像的测试集。在下图中你可以看见10个类的10张随机图片。


左边:从CIFAR-10数据库来的样本图像。右边:第一列是测试图像,然后第一列的每个测试图像右边是使用Nearest Neighbor算法,根据像素差异,从训练集中选出的10张最类似的图片。
  假设现在我们有CIFAR-10的50000张图片(每种分类5000张)作为训练集,我们希望将余下的10000作为测试集并给他们打上标签。Nearest Neighbor算法将会拿着测试图片和训练集中每一张图片去比较,然后将它认为最相似的那个训练集图片的标签赋给这张测试图片。上面右边的图片就展示了这样的结果。请注意上面10个分类中,只有3个是准确的。比如第8行中,马头被分类为一个红色的跑车,原因在于红色跑车的黑色背景非常强烈,所以这匹马就被错误分类为跑车了。
  那么具体如何比较两张图片呢?在本例中,就是比较32x32x3的像素块。最简单的方法就是逐个像素比较,最后将差异值全部加起来。换句话说,就是将两张图片先转化为两个向量I1和I2,然后计算他们的 L1距离:


这里的求和是针对所有的像素。下面是整个比较流程的图例:

以图片中的一个颜色通道为例来进行说明。两张图片使用L1距离来进行比较。逐个像素求差值,然后将所有差值加起来得到一个数值。如果两张图片一模一样,那么L1距离为0,但是如果两张图片很是不同,那L1值将会非常大。

距离选择:计算向量间的距离有很多种方法,另一个常用的方法是L2距离,从几何学的角度,可以理解为它在计算两个向量间的欧式距离。L2距离的公式如下:

!

换句话说,我们依旧是在计算像素间的差值,只是先求其平方,然后把这些平方全部加起来,最后对这个和开方。
L1和L2比较:比较这两个度量方式是挺有意思的。在面对两个向量之间的差异时,L2比L1更加不能容忍这些差异。也就是说,相对于1个巨大的差异,L2距离更倾向于接受多个中等程度的差异。L1和L2都是在p-norm常用的特殊形式。

k-Nearest Neighbor分类器

你可能注意到了,为什么只用最相似的1张图片的标签来作为测试图像的标签呢?这不是很奇怪吗!是的,使用k-Nearest Neighbor分类器就能做得更好。它的思想很简单:与其只找最相近的那1个图片的标签,我们找最相似的k个图片的标签,然后让他们针对测试图片进行投票,最后把票数最高的标签作为对测试图片的预测。所以当k=1的时候,k-Nearest Neighbor分类器就是Nearest Neighbor分类器。从直观感受上就可以看到,更高的k值可以让分类的效果更平滑,使得分类器对于异常值更有抵抗力。

用于超参数调优的验证集

k-NN分类器需要设定k值,那么选择哪个k值最合适的呢?我们可以选择不同的距离函数,比如L1范数和L2范数等,那么选哪个好?还有不少选择我们甚至连考虑都没有考虑到(比如:点积)。所有这些选择,被称为超参数(hyperparameter)。在基于数据进行学习的机器学习算法设计中,超参数是很常见的。一般说来,这些超参数具体怎么设置或取值并不是显而易见的。

你可能会建议尝试不同的值,看哪个值表现最好就选哪个。好主意!我们就是这么做的,但这样做的时候要非常细心。特别注意:决不能使用测试集来进行调优。当你在设计机器学习算法的时候,应该把测试集看做非常珍贵的资源,不到最后一步,绝不使用它。如果你使用测试集来调优,而且算法看起来效果不错,那么真正的危险在于:算法实际部署后,性能可能会远低于预期。这种情况,称之为算法对测试集过拟合。从另一个角度来说,如果使用测试集来调优,实际上就是把测试集当做训练集,由测试集训练出来的算法再跑测试集,自然性能看起来会很好。这其实是过于乐观了,实际部署起来效果就会差很多。所以,最终测试的时候再使用测试集,可以很好地近似度量你所设计的分类器的泛化性能(在接下来的课程中会有很多关于泛化性能的讨论)。

测试数据集只使用一次,即在训练完成后评价最终的模型时使用。

好在我们有不用测试集调优的方法。其思路是:从训练集中取出一部分数据用来调优,我们称之为验证集(validation set)。以CIFAR-10为例,我们可以用49000个图像作为训练集,用1000个图像作为验证集。验证集其实就是作为假的测试集来调优。

程序结束后,我们会作图分析出哪个k值表现最好,然后用这个k值来跑真正的测试集,并作出对算法的评价。

把训练集分成训练集和验证集。使用验证集来对所有超参数调优。最后只在测试集上跑一次并报告结果。

超参数调优注意问题

1.选择能对你训练集给出最高准确率,表现最佳的超参数
  千万不要这么做
  例如在之前的k-最近邻分类算法中,假设k=1我们总能完美分类训练集数据,所以如果我们采用这一策略,总是选择k=1,但是正如之前案例所见的,在实践中,让k取更大的值会在训练集中分错个别数据,但是对于在训练集中,未出现过的数据分类性能更佳。
  我们关心的不是要尽可能拟合训练集,而是要让我们的分类器,我们的方法在训练集以外的未知数据上表现更好。

2.把所有的数据分成两部分,一部分是训练集,一部分是测试集,然后在训练集上用不同的超参数来训练算法,然后将训练好的分类器,用在测试集上,再选择一组在测试集上表现最好的超参数。
  错误做法
  因为同样的机器学习系统的目的,是让我们了解算法表现究竟如何,所以测试集的目的是给了我们一种预估的方法,即在没有遇到的数据上算法表现将会如何,如果采用这种用不同的超参数,训练不同算法的策略,然后选择在测试集上表现最好的超参数,那么很可能我们选择了,一组超参数,只是让我们的算法在这组测试集上表现良好,但是这组测试集的表现无法代表在全新的未见过的数据上的表现。

3.更常见的做法就是将数据,分为不同的三组,大部分数据作为训练集(train set),然后建立一个验证集(validation set),一个测试集(test set),我们通常所做的就是,在训练集上用不同的超参数来训练算法,在验证集上进行评估,然后用一组超参数,选择在验证集上表现最好的,然后当完成了这些步骤以后,你也完成了所有的调试,所有都完成了,然后把这组在验证集上表现最佳的分类器拿出来,在测试集上跑一跑,这才是你写到论文中的数据,也是你要写到报告中的数据,这个数据才告诉你,你的算法在未见的新数据上表现如何,非常非常重要的一点就是,必须分割验证集和测试集,要保证测试集收到严格的控制。

4.超参数的另一个方法就是交叉验证,这个在小数据集中更常用一些,在深度学习中不那么常用。因为:在深度学习中,当我们训练大型模型时,训练本身非常消耗计算能力,因此这些方法实际不常用。

5.训练集和验证集的区别:
  如果你想一想k-最近邻分类算法(KNN),那么训练集就是一堆贴上标签的图片,我们记下标签,要给图像分类,我们会将图片与训练集的每一个元素比较,然后将与训练点最接近点的标签作为结果,我们的算法会记住训练集中的所有样本,然后我们会把验证集中的每个元素与训练集中的每个元素比较,将它为依据判定分类器的准确率,在验证集上表现如何,这就是验证集和训练集的区别,你的算法可以看到训练集中的各个标签,但是在验证集中,你的算法不能直接看到他们的标签,我们只是用验证集中的标签,来检查我们算法的表现。

6.关于测试集能不能很好地代表真实世界中的数据?
  我们背后的统计学假设就是,你的数据都互相独立,服从同一分布,这样你数据上所有的点都来自同一个概率分布,当然在现实中不可能总是这样的情况,你可能碰到一些其他的情况,也就是你的测试集代表性,不佳,不能很好地反映真实世界。这一问题就是数据集的创建者和数据集管理者需要好好思考的。

交叉验证:

  有时候,训练集数量较小(因此验证集的数量更小),人们会使用一种被称为交叉验证的方法,这种方法更加复杂些。还是用刚才的例子,如果是交叉验证集,我们就不是取1000个图像,而是将训练集平均分成5份,其中4份用来训练,1份用来验证。然后我们循环着取其中4份来训练,其中1份来验证,最后取所有5次验证结果的平均值作为算法验证结果。

实际应用。在实际情况下,人们不是很喜欢用交叉验证,主要是因为它会耗费较多的计算资源。一般直接把训练集按照50%-90%的比例分成训练集和验证集。但这也是根据具体情况来定的:如果超参数数量多,你可能就想用更大的验证集,而验证集的数量不够,那么最好还是用交叉验证吧。至于分成几份比较好,一般都是分成3、5和10份。

常用的数据分割模式。给出训练集和测试集后,训练集一般会被均分。这里是分成5份。前面4份用来训练,黄色那份用作验证集调优。如果采取交叉验证,那就各份轮流作为验证集。最后模型训练完毕,超参数都定好了,让模型跑一次(而且只跑一次)测试集,以此测试结果评价算法。

Nearest Neighbor分类器的优劣

现在对Nearest Neighbor分类器的优缺点进行思考。首先,Nearest Neighbor分类器易于理解,实现简单。其次,算法的训练不需要花时间,因为其训练过程只是将训练集数据存储起来。然而测试要花费大量时间计算,因为每个测试图像需要和所有存储的训练图像进行比较,这显然是一个缺点。在实际应用中,我们关注测试效率远远高于训练效率。其实,我们后续要学习的卷积神经网络在这个权衡上走到了另一个极端:虽然训练花费很多时间,但是一旦训练完成,对新的测试数据进行分类非常快。这样的模式就符合实际使用需求。

Nearest Neighbor分类器的计算复杂度研究是一个活跃的研究领域,若干Approximate Nearest Neighbor (ANN)算法和库的使用可以提升Nearest Neighbor分类器在数据上的计算速度(比如:FLANN)。这些算法可以在准确率和时空复杂度之间进行权衡,并通常依赖一个预处理/索引过程,这个过程中一般包含kd树的创建和k-means算法的运用。

Nearest Neighbor分类器在某些特定情况(比如数据维度较低)下,可能是不错的选择。但是在实际的图像分类工作中,很少使用。因为图像都是高维度数据(他们通常包含很多像素),而高维度向量之间的距离通常是反直觉的。下面的图片展示了基于像素的相似和基于感官的相似是有很大不同的:

在高维度数据上,基于像素的的距离和感官上的非常不同。上图中,右边3张图片和左边第1张原始图片的L2距离是一样的。很显然,基于像素比较的相似和感官上以及语义上的相似是不同的。

这里还有个视觉化证据,可以证明使用像素差异来比较图像是不够的。z这是一个叫做t-SNE的可视化技术,它将CIFAR-10中的图片按照二维方式排布,这样能很好展示图片之间的像素差异值。在这张图片中,排列相邻的图片L2距离就小。

上图使用t-SNE的可视化技术将CIFAR-10的图片进行了二维排列。排列相近的图片L2距离小。可以看出,图片的排列是被背景主导而不是图片语义内容本身主导。

具体说来,这些图片的排布更像是一种颜色分布函数,或者说是基于背景的,而不是图片的语义主体。比如,狗的图片可能和青蛙的图片非常接近,这是因为两张图片都是白色背景。从理想效果上来说,我们肯定是希望同类的图片能够聚集在一起,而不被背景或其他不相关因素干扰。为了达到这个目的,我们不能止步于原始像素比较,得继续前进。

小结:实际应用k-NN

如果你希望将k-NN分类器用到实处(最好别用到图像上,若是仅仅作为练手还可以接受),那么可以按照以下流程:
  1.预处理你的数据:对你数据中的特征进行归一化(normalize),让其具有零平均值(zero mean)和单位方差(unit variance)。在后面的小节我们会讨论这些细节。本小节不讨论,是因为图像中的像素都是同质的,不会表现出较大的差异分布,也就不需要标准化处理了。
  2.如果数据是高维数据,考虑使用降维方法,比如PCA(wiki ref, CS229ref, blog ref)或随机投影。
  3.将数据随机分入训练集和验证集。按照一般规律,70%-90% 数据作为训练集。这个比例根据算法中有多少超参数,以及这些超参数对于算法的预期影响来决定。如果需要预测的超参数很多,那么就应该使用更大的验证集来有效地估计它们。如果担心验证集数量不够,那么就尝试交叉验证方法。如果计算资源足够,使用交叉验证总是更加安全的(份数越多,效果越好,也更耗费计算资源)。
  4.在验证集上调优,尝试足够多的k值,尝试L1和L2两种范数计算方式。
  5.如果分类器跑得太慢 ,尝试使用Approximate Nearest Neighbor库(比如FLANN)来加速这个过程,其代价是降低一些准确率。
  6.对最优的超参数做记录。记录最优参数后,是否应该让使用最优参数的算法在完整的训练集上运行并再次训练呢?因为如果把验证集重新放回到训练集中(自然训练集的数据量就又变大了),有可能最优参数又会有所变化。在实践中,不要这样做。千万不要在最终的分类器中使用验证集数据,这样做会破坏对于最优参数的估计。直接使用测试集来测试用最优参数设置好的最优模型,得到测试集数据的分类准确率,并以此作为你的kNN分类器在该数据上的性能表现。