原文地址:http://cs231n.github.io/classification/
############################################################################33
这是一篇入门级的文章,为了向计算机视觉专业外的学生介绍图像识别问题以及数据驱动方法。内容如下:
1.图像分类,数据驱动方法,算法流程介绍
2.最近邻分类器 / k-近邻分类器
3.验证集,交叉验证,超参数调整
4.最近邻的优缺点
5.总结
6.总结:实际应用k-近邻
7.扩展阅读
图像分类
Motivation. 本节,我们将会介绍图像分类问题,即在一组固定类别中给输入图像选择一个类别。尽管看起来简单,但它却是计算机视觉中核心问题之一,在实际生活中有大量应用。此外,就如我们稍后会看到的许多看起来不同的计算机视觉问题(比如对象检测,分割等),都可以被归纳为图像分类问题。
Example. 举个例子,如下图所示,在某个图像分类模型中,得到一幅图像,判断它属于4种类别的可能性(猫,狗,帽子,马克杯)。就像图中展示的,对于电脑而言,一个图片相当于一组3维数组。在这个例子中,这幅猫的图像宽248像素,高400像素,并且拥有三个颜色通道,红,绿和蓝(简称为RGB)。因此,这幅图像拥有248x400x3个数字,共297600个。每个数字均为整数,范围从0(黑色)到255(白色)。我们的任务是把这么多的数字转换为一个标记,比如“猫”。
Challenges. 尽管识别一个可视化的对象对于人类来说是比较简单的,但是从计算机视觉算法的角度看,这是一个值得思考的问题。我们在下面(粗略地)列出了一些挑战,注意原始图像格式是一个3维亮度值数组:
1)角度变化。相对与相机而言,一个对象可以在不同的角度下呈现
2)尺寸变化。视觉类经常表现出其大小的改变(不仅仅是它们在图像上的大小,还有它们在现实世界中的大小)
3)变形。许多对象并不是刚性体(没有变化),可以在极端的方式下变形
4)遮挡。对象可能会被遮挡。有些时候仅有对象的一小部分(极小的像素)可以被看见
5)照明条件。光照的影响在像素水平上是很大的
6)背景干扰。对象可能会融入到环境中,使得我们很难去辨识它
7)类内变化。某一类对象的差别可能比较大,比如椅子。有许多种不同类型的椅子,每种都是不同的外表
一个好的图像分类模型不仅要不受外界条件的干扰,同时对同一个类别的不同外表保持敏感。
Data-driven approach. 我们要怎么样才能写好一个算法,用来分类图像?这并不像某些算法,举个例子,排序一组数字,这并不是简单的写一个算法就能识别出猫的图像。因此,我们采取的方法并不像你教育孩子似的,尽量在代码中直接指定每一个类别的所有对象,而是给予计算机每一个类别的许多对象图像,开发学习算法去学习这些图像。这种方法被称为数据驱动方法,因此它也依赖于给予的带有标记的训练图像集。下面是这种数据集的一个例子:
The Image classification pipeline. 我们可以看到图像分类的任务就是得到一组表示一副图像的像素数组,然后给它标记。完整的算法流程可以归纳如下:
1)输入:输入包括一组N个图像,每个被标记为K个类别中的一个。我们称之为训练集。
2)学习:我们的任务是学习这个训练集中的每一类别中的每一个图像。我们称之为训练分类器或学习模型。
3)评估:最后,我们将通过预测一组新的图像来评估分类器的性能。我们将比较分类器预测的类别和图像真实类别。我们希望预测结果能够匹配上真实类别。
Neatest Neighbor Classifier
作为第一个方法,我们将会开发一个最近邻分类器。这个分类器和卷积神经网络并没有关系而且在现实生活中它几乎没有应用,但你能够启发我们关于图像分类问题的基本思想。
Example image classification dataset:CIFAR-10. 一个很受欢迎的小型的图像分类数据集是CIFAR-10数据集。这个数据集拥有60,000张小型图像,每张图像宽高均为32像素。每张图像均被标记为10个类别之一(举个例子,“飞机”,“汽车”,“鸟”等)。这60,000张图片被分为一个50,000张图像的训练集和10,000张图像的测试集。下图左侧是10个类别中随机挑选的10张图像:
假设现在我们被给予了50,000张CIFAR-10训练集图像(每个标记共5,000张图像),而我们要去标记剩下的10,000张图像。最近邻分类器将会把每一个测试图像和所有训练图像进行比较,然后根据最相似的训练图像的类别来作为这个测试图像的类别。上图右侧第一列表示10张不同类别的测试图像,右侧表示最近邻分类器预测的最相似的前10个图像。从图中可以看出,仅有3个类别的测试图像分类正确,其余7个图像分类失败。比如,第8行的测试图像类别为马头,但是它最相似的训练图像的类别是一辆红色汽车,大概是因为背景的关系。结果,本例子中“马”的图像被错误标记为“车”。
相比你已经注意到,我们并没有透露比较两幅图像的详细信息,在本例中仅知道图像大小为32x32x3。最简单的方式是求出图像对应像素的差值的绝对值,然后求和。换句话说,给予两幅图像,用向量和表示,一种比较图像的合理选择可能是L1 距离:
最后的值是求和所有的像素点。下图是可视化的过程:
下面让我们来看看如何在代码中实现。首先,加载CIFAR-10数据,共4个数组:训练集数据 / 标记和测试集数据 / 标记。代码如下,Xtr(大小为50,000 x 32 x 32 x 3)表示训练集的所有图像,以及相应的1维数组Ytr(大小为50,000)表示训练集标记(从0到9):
Xtr, Ytr, Xte, Yte = load_CIFAR10('data/cifar10/') # a magic function we provide
# flatten out all images to be one-dimensional
Xtr_rows = Xtr.reshape(Xtr.shape[0], 32 * 32 * 3) # Xtr_rows becomes 50000 x 3072
Xte_rows = Xte.reshape(Xte.shape[0], 32 * 32 * 3) # Xte_rows becomes 10000 x 3072
nn = NearestNeighbor() # create a Nearest Neighbor classifier class
nn.train(Xtr_rows, Ytr) # train the classifier on the training images and labels
Yte_predict = nn.predict(Xte_rows) # predict labels on the test images
# and now print the classification accuracy, which is the average number
# of examples that are correctly predicted (i.e. label matches)
print 'accuracy: %f' % ( np.mean(Yte_predict == Yte) )
import numpy as np
class NearestNeighbor(object):
def __init__(self):
pass
def train(self, X, y):
""" X is N x D where each row is an example. Y is 1-dimension of size N """
# the nearest neighbor classifier simply remembers all the training data
self.Xtr = X
self.ytr = y
def predict(self, X):
""" X is N x D where each row is an example we wish to predict label for """
num_test = X.shape[0]
# lets make sure that the output type matches the input type
Ypred = np.zeros(num_test, dtype = self.ytr.dtype)
# loop over all test rows
for i in xrange(num_test):
# find the nearest training image to the i'th test image
# using the L1 distance (sum of absolute value differences)
distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
min_index = np.argmin(distances) # get the index with smallest distance
Ypred[i] = self.ytr[min_index] # predict the label of the nearest example
return Ypred
当你运行以上代码时,你会发现该分类器在CIFAR-10上仅有38.6%的检测精度。虽然这已经超过了随机猜测(因为共10类,所以有10%的检测精度),但是远没有到达人类的检测精度(估计有94%)或者最先进的卷积神经网络(已到达95%,查看最近在CIFAR-10上的排行榜)。
The choice of distance. 还有许多计算向量之间距离的方式。另一种通用的方法是使用L2距离,表示求解向量间的欧式距离。格式如下:
换句话说,我们将像以往一样计算像素差值,但这并没有结束,这一次我们会平方求和,并且其平方根。python代码如下:
distances = np.sqrt(np.sum(np.square(self.Xtr - X[i,:]), axis = 1))
注意,在上面我包括了np.sart函数,但是在最近邻分类器的实际应用中我们会省略平方根操作,因为平方根是一个单调函数。即平方根能够扩展距离的绝对值,但是它保留了距离的大小,所以最近邻分类器有还是没有它都是一样的。如果在CIFAR-10上使用这种distance方式运行最近邻分类器,你会获得35.4%精度(略小于使用L1距离的结果)。
L1 vs. L2. It is interesting to consider differences between the two metrics(度量)。In particular, the L2 distance is much more unforgiving(明显的)than the L1 distance when it comes to differences between two vectors. That is, the L2 distance prefers many medium disagreements(分歧) to one big one. L1 and L2 distances (or equivalently(相等地) the L1/L2 norms of the differences between a pair of images ) are the most commonly used special cases of p-norm.
k-Nearest Neighbor Classifier
你可能可能已经注意到这是很奇怪的,当我们想要去预测一张图片的类别时仅仅使用了最相似图像的类别。事实上,使用k-近邻分类器往往可以得到更好的结果。这个想法是很简单的:我们将使用前k张最相似的图像,而不是仅仅在训练集中寻找一张最相似的图像,然后让它们对测试图像的类别进行投票决定。特别的,当k=1时,就是最近邻分类器。直观上,k(比1大)值有一个滤波的作用,能够让分类器对离群点有更好的抵抗力。(makes the classifier more resistant to outliers):
实际生活中,通常会使用k-近邻分类器。但是你应该设定k=?呢,我们接下来讨论这个问题。
Validation sets for Hyperparameter tuning
k-近邻分类器需要对k值进行设定。但哪个值最好呢?另外,我们可以选择多个不同的距离函数:L1准则,L1准则,还有很多我们甚至还没有考虑的(比如点积)。这些方法统称为超参数(hyperparameter),在机器学习算法的设计中,它们会经常出现,并且从数据中学习得到。这通常是很不明显的去选择哪个值或者设置。
你可能会建议我们应该尝试所有的不同值的算法,然后看哪个结果最好。这是一个好注意,也是我们想要做的,但是必须非常小心。特别是我们不能为了调整超参数而使用测试集。无论你什么时候设计机器学习算法,都应该牢牢谨记测试集是非常珍贵的资源,最好仅在最后使用一次。否则,一个很危险的地方就是调整好的超参数在测试集的表现中很好,但当你真正在实际部署你的模型时,会有一个明显的缩水的性能。在实践中,我们会说你过拟合(overfit)你的测试集了。另一种看待它的方式是如果你在测试集上调整超参数,你其实是把测试集当做了训练集,因此当你真正部署你的模型的时候,你会发现在测试集上达到的性能是过于乐观了。当你仅在最后一次使用测试集,测试集的结果还是能够很好的代表你的分类器的性能(我们在本节后面会有更多的讨论)。
幸运的是,有一种很好的方式来调整超参数,并且完全不会使用测试集。这个想法是把我们的训练集分为两部分:设置一个稍微小点的训练集,称为验证集。在CIFAR-10上,我们可以使用49,000个训练图片作为训练,而留下1,000个用作验证。这个验证集基本上是作为一个假的测试集来调整超参数。
以下是相关代码:
# assume we have Xtr_rows, Ytr, Xte_rows, Yte as before
# recall Xtr_rows is 50,000 x 3072 matrix
Xval_rows = Xtr_rows[:1000, :] # take first 1000 for validation
Yval = Ytr[:1000]
Xtr_rows = Xtr_rows[1000:, :] # keep last 49,000 for train
Ytr = Ytr[1000:]
# find hyperparameters that work best on the validation set
validation_accuracies = []
for k in [1, 3, 5, 10, 20, 50, 100]:
# use a particular value of k and evaluation on validation data
nn = NearestNeighbor()
nn.train(Xtr_rows, Ytr)
# here we assume a modified NearestNeighbor class that can take a k as input
Yval_predict = nn.predict(Xval_rows, k = k)
acc = np.mean(Yval_predict == Yval)
print 'accuracy: %f' % (acc,)
# keep track of what works on the validation set
validation_accuracies.append((k, acc))
Cross-validation. 如果你的训练数据很小,我们可以使用一种更复杂的技术来进行超参数调整,称为交叉验证(cross-validation)。按照先前的例子,并不是随机挑选1000个数据点作为验证集,其他作为训练集,而是通过迭代不同的验证集,然后平均这些结果,从而得到一个更好的,更少噪音干扰的k值。举个例子,在5等分交叉验证中,首先我们把训练数据等分为5份,使用其中4份作为训练集,剩下一份作为验证集。然后通过迭代,把训练集中一份作为验证集,其他作为新的训练集的方式,评估这个性能,最后平均这五次迭代产生的值。
In practice. 实际使用中,人们更倾向于使用单一的验证集而不是交叉验证,因为交叉验证计算量更大。划分时通常使用50%-90%的训练数据作为训练集,剩下的作为验证集。然而,它依赖于多个因素:例如,如果超参数的数量很多,你可能更倾向于分离出更大的验证集。如果验证集的数量很好(也许只有几百左右),使用交叉验证是更好的方式。在实际使用交叉验证的典型的等分数量是3等分,5等分或10等分。
Pros and Cons of Nearest Neighbor classifier
最近邻分类器的优缺点
考虑下最近邻分类器的优缺点。显然,一个优点就是它易于理解和应用。其次,这个分类器几乎不花时间用作训练,因为它需要的就是存储,可能还要标注训练数据。然而,我们在测试时间上付出计算成本,因为分类一个图像需要和所有训练数据进行单个比对。这个是缺点,因为在实际生活中我们经常关心测试时间上的效率而不是训练的效率。事实上,之后我们要开发的深度神经网络将训练时间和测试时间转移到了另一个极端:训练需要很久,一旦训练完成就能方便的进行测试。实际生活中,这种操作模式是更加符合需要的。
其实,最近邻分类器的计算复杂度是一个活跃的研究区域,并且几个近似最近邻(Approximate Nearest Neighbor, ANN)算法和库可以加快在一个数据集上的最近邻查找(比如FLANN)。这些算法允许在检索过程中就它的时间/空间复杂度来取舍最近邻检索的正确性,通常依赖于预处理/标记阶段,包括构建kdtree,或者运行k均值算法。
最近邻分类器在某些方面还是一个很好的选择(特别当数据是低维情况下),但是它在实际的图像分类中并不合适。一个问题就是图像都是高维对象(它们经常包含许多像素),而高维空间的距离常常是反直觉的。下面的图像说明了这一点,基于像素的L2相似性和感知相似性是存在很大不同的:
下面是一个更加可视化的图,让你知道使用像素差值来比较图像是不合适的。我们使用的可视化的技术叫做t-SNE,它把CIFAR-10的图像嵌入到两个维度中使得两两之间的距离被保留下来。在图中,相邻的图像被认为是最相近的就上面提到的L2距离而言:
特别的,注意到相邻图像的颜色分布更为相似,或者是背景类型相似而不是因为图像的类别相同。举个例子,一只狗可以被看作和一只青蛙相近因为它们都出现在一个白色的背景里。最理想的情况下,我们希望10各类的图像各自组成集群,因为相同类别的图像应该是相近的不管无关的特征和变化(比如背景)。然而,要获取这种属性,我们必须超越原始像素的使用。
Summary
1)我们介绍了图像分类的问题。使用一组已经被标记为某个类别的图像,去预测另一组未标记的测试图像的类别,并且给出预测的精度。
2)我们介绍了一个简单的分类器-最近邻分类器。发现最近邻分类器和多个超参数(比如k的值,或者distance类型)相关,但是没有明显的方式去选择它们。
3)我们发现一种设置超参数的正确的方式是去分离训练数据为两部分:一部分为训练集,另一部分为假的测试集,称为验证集。我们尝试不同的超参数进行计算,然后选取在验证集中效果最好的值。
4)如果训练集很少,我们可以使用交叉验证,它可以帮助我们在评估哪组超参数工作最好的时候减少噪声的干扰。
5)一旦最佳超参数确定后,我们在测试数据上进行一次评估。
6)可以看到最近邻分类器在CIFAR-10上可以得到大约40%的精度。它易于理解但是需要存储整个数据集,并且在测试一张图片耗费大量时间。
7)最后,我们发现在原始像素上使用L1或L2距离并不准确,因为距离公式对于颜色分布和图像背景的关联程度比它们的语义内容更加大。
后面我们会解决这些问题,并最终达到90%的检测精度,可以完全舍弃训练集一旦学习完成后,而且允许我们以小于1毫秒时间来测试一张图片。
Summary:Applying kNN in practice
如果你想要在实际生活中使用kNN分类器(希望不要在图像分类中,也许仅作为一个baseline),处理流程如下:
1)预处理数据:归一化特征(图像中的每一个像素均为一个特征)为0均值并且是单位变量。我们将在后面的章节里详细讲解,选择不再这一节讲解数据归一化是因为图像像素通常是均匀的,不会呈现出广泛的不同分布,从而减轻了数据归一化的需要。
2)如果你的数据是高维的,考虑使用一个维度缩减技术,比如PCA(wiki ref,CS229ref,blog ref)或者Random Projections。
3)随机分离训练数据为训练/验证集。一般来说,训练集包括数据的70%-90%之间。这个设置依赖于超参数的数量以及你期待它们的影响。如果要确定多个超参数,使用更大的验证集可能更有效。如果你不能确定验证集的大小,最好的方式是等分训练数据然后使用交叉验证方式。如果你能够接受计算上的耗费,使用交叉验证总是更加安全的方式(更多的等分更好,但也更加耗费计算量)。
4)为了不同的k的值的选择(越多越好)和不同的distance类型(L1和L2是很好的候选),可以在验证集上训练和测试(使用交叉验证的话使用所有等分数据)
5)如果你的kNN分类器运行需要过多时间运行,可以考虑使用一个近似最近邻库(比如FLANN)去加速检索(一些精度上的耗费)
6)注意给出最好结果的超参数。有一个问题是你是否应该使用所有训练集来训练超参数,因为最佳超参数可能会发生改变如果你把验证集数据放入训练集中(因为数据会变得更大)。实际使用中,不再最后的分类器训练中使用验证集是更好的。使用测试集来评估最好的模型,得出的这个测试集精度将会称为这个kNN分类器的性能。
Further Reading
下面是一些关于延伸阅读的链接:
A Few Useful Things to Know about Machine Learning
Recognizing and Learning Object Categories:在ICCV 2005上一个小型的关于对象类别的课程