这是一篇介绍性的文章,旨在向人们介绍计算机视觉的外围应用:图片分类问题。现在我们介绍一种基于数据驱动的方法。内容如下:
简述图像分类,数据驱动和管道
K近邻分离器
验证集合,交叉验证和超参数调优
近邻算法的优点和缺点
总结
总结:kNN在实践中的应用
扩展阅读
图像分类 (Image Classification)
动机(Motivation),在这个章节中,我们讲介绍图像分类问题,任务是给定一个输入图片,将其指派到一个已知的混合类别中的某一个标签。图像分类是计算机视觉领域的核心问题之一,尽管它很(看上去)很简单,但是却有广泛的实践应用。而且,在之后的课程中我们将看到,许多其他的看上去不同的计算机视觉任务(例如物体识别,分割),都能够还原成图像分类。
例子,例如,在下图中,一个图像分类模型将一个图片分配给四个类别(cat,dog,hat,mug)标签的概率。如图所示,记住对于计算机而言,图片被表示成一个大的3位数字矩阵。在这个例子中,猫图宽248像素,高400像素,并且有3个颜色通过Red,Green,Blue简称RGB 。因此,这个图片包括248 x 400 x 3个数,总共197600个数字。 每一个数字都在0到255之间,其中0表示黑色,255表示白色。我们的任务是转换这个百万级别的数字到一个单独的标签,例如“猫”。
图片分类的任务是对于一个给定的图片,预测其的类别标签。图片是一个的0-255之间的3维数组,大小是宽 x 高 x 3。3表示颜色通道Red,Green,Blue。
挑战:虽然视觉识别的概念对于人类来讲是一个相对简单的问题,但是从计算视觉算法的角度来看,这儿有许多值得考虑的挑战。正如我们下面展示的一个挑战列表,我们要牢记,一个原始的图片是一个由亮度值组成的3维数组。
1、视觉点变化(Viewpoint variation):一个对像的单个实例可以根据相机的方向来定位。
2、规模变化(Scale variation):视觉上的类经常展示在他们大小上的变化。
3、变形(Deformation):许多感兴趣的对象,不是一个僵硬不变的身体,能够以多方式变形。
4、遮挡(Occlusion):我们感兴趣的对象被遮挡,有时仅仅是目标对象的一小部分能够显示。
5、光照条件(Illumination conditions):在像素层上,光照的影响是巨大的。
6、背景杂乱(Background clutter):感兴趣的目标可能混合在背景中,使得他们很难被识别。
7、类别内部的变化(Intra-class variation):我们感兴趣的类别相对广泛,例如椅子,就有许多不同的形式。
一个好的图像分类模型对于这些所有变化的交叉的变化必须是不变的,并且要在类别内部变化中保持有敏感性。
数据驱动的方法(Data-driven approach):我们怎样写一个算法分类图片到不同的类别?这个算法不像排序算法,在图片中识别猫的算法并不是那么显而易见。因此,我们不是指定定感兴趣的类别中的每一个类别的直接代码,(自己都翻译蒙了),我们采取的方法就像一个孩子的方法:我们将提供计算机每一个类中的许多例子,进而看着这些类学习每一个类的视觉显示的算法。这种算法就叫做数据驱动的算法,因为它依赖与第一次选取的带有标签的训练数据集。这种例子看起来就像这样:
一个有四个类别的训练集合,在现实中,我们可能有成千个类别,每一类有成白上千个图片。
图像分类管道(The image classification pipeline):我们已经知道在图像分类中,是使用一个像素的数组来表示单一的图像,进而分配标记给它。我们的完整的通道被定义如下:
输入(Input):输入包括N个图片的集合,每一个图片都标记为k个不同的类之一,叫做训练数据。
学习(Learning):我们的任务是使用训练数据学习每一个类别看起来像什么,这一步叫做训练一个分类器,或者学习一个模型。
评价(Evaluation):在最后,通过让它预测之前从没见过图像,评价这个模型的质量。我们将比较这些图片的真实标签和预测结果。直观地,我们希望尽可能多的预测和真实答案匹配。
最近邻分类器(Nearest Neighbor Classifier)
我们的第一个方法是,开发一个最近邻分离器。这个分离器与卷积神经网络没有任何关系,并且在实践中很少使用,但是对于图像分类问题,它使我们从基本的方法的出发得到一个idea。
例如图像分类数据集:CIFAR-10。CIFAR-10 数据集是一个著名的小图像分类数据集。这个数据集包含60000个宽和高都是32像素的小图片。每个图片被标记成10个类别中的一个。这60000个图片被分成50000个训练集和10000个测试集。下图你将看到10个类中每一个显示10个随机的图片。
Left:样例图片来自于CIFAR-10 dataset。Right:第一列表示1一些测试图片,接下来表示根据最近邻算法得到的最相近的10个图片。
假设现在你已经得到CIFAR-10的50000训练集,接下来我们希望标记剩下的10000个图片。最近邻算法讲每一个测试图片和训练数据中的图片进行比较,预测的是最接近的训练数据的标签。上图的右边,我们看到一个结果的例子。注意只有3个是同一类的检索,其他的7个并不是如此。例如在第8行中,马头在训练数据集张最接近的是红色的车,可能是由于强烈的黑色背景。结果,图片马就被错误的标记成一个车。
你也许已经注意到,我们剩下的未提及的细节是怎么比较两个图片,在这个例子中我们有两个32 x 32 x 3的块数据。一个最简单的方法就是比较所有的像素值,并加上所有像素的差值。换句话说,给你两个图片,并将他们表示成向量I_1和I_2,一个合理的选择是比较他们的L1距离。下面是一个可视化的例子:
两个图片相减,之后求和。
下面是让我们实现分类器的代码,第一步加载CIFAR-10数据到内存中得到4个数组,训练数据和标签,测试数据和标签。Xtr大小是50000 x 32 x 32 x 3训练其中的所有图片,对应的一维数据是Ytr长度50000,表示的是训练数据的标签,其在0-9之间。
# a magic function we provide
Xtr, Ytr, Xte, Yte = load_CIFAR10('data/cifar10/')
# flatten out all images to be one-dimensional
# Xtr_rows becomes 50000 x 3072
Xtr_rows = Xtr.reshape(Xtr.shape[0], 32 * 32 * 3)
# Xte_rows becomes 10000 x 3072
Xte_rows = Xte.reshape(Xte.shape[0], 32 * 32 * 3)
现在所有的图片作为一个行数据,接着我们训练和评测这个分类器。
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) )
注意,一般情况下我们使用正确率作为评价标准,它表示正确预测的得分。我们建立所有的分类器,满足一个公共的API借口,train(X,y)函数表示数据和标签的学习。进一步,这些带有标签的分类器能够从数据中进行预测,因此我们定义了predict(X)函数,可以从新的数据中预测标签。当然,我们遗漏了函数本身的东西。这里有一个简单的最近邻的框架,其使用的是L1距离进行度量。
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%的概率好了很多。但是,这个分分类器达到的正确率,相对人的94%的正确率和最先进的卷积神经网络(CNN)95%的分类器,相差太远了。
选择距离:有许多其他的计算两个向量之间的距离的方法。另一个常用的选择是L2距离,它是计算两个向量之间欧几里得距离的几何解释。公式如下:
用python numpy一行代码就可以表示上式;
distances = np.sqrt(np.sum(np.square(self.Xtr - X[i,:]), axis = 1))
注意上式中包括了np.sqrt,但是在实际的最近邻应用中,由于平方根是单调函数,我们一般不进行开方。虽然这表示了绝对大小,但是比较的顺序是不变的,因此最近邻是正确的。如果你使用带有这个距离方法的最近邻分类CIFAR-10,你可能得到35.4%的正确率。
L1 vs. L2,比较两者之间差异,特别当涉及到两个向量的差异是,L2比L1更加的敏感。
也就是说L2距离更倾向与一个大的分歧,L1和L2是p-范数中最常用的两个距离表示。
k-最近邻分类器(k-Nearest Neighbor Classifier)
你可能已经注意到,当我们希望做个预测时,只是用一个最近的标签是奇怪的。更进一步,我们使用k-最近邻时,总会做的更好。它的思想是简单的,不是在训练集中发现一个最近的图片,而是发现k的最近的图片,用这k个图片对测试图片进行投票。特别地,当k=1时就是最近邻。直观地,较高的k具有平滑的作用,使得分类器能够抵抗异常值。
这是最近邻和k=5最近邻的算法在一个二维数据和3个类别之间的不同。颜色边界表示使用L2距离度量的分类器的决策边界。白色表示未分类。可以看到5-最近邻在于这些不规则的点更加光滑。
超参数调优的验证集(Validation sets for hyperparameter tuning)
k-最近邻分类器要求设置k值,但是k取多少才最好呢?而且,我们知道有很多距离函数,包括我们用过的L1范数,L2范数,和我们提到的其他的范数。这些选择被叫做超参数,它们经常在基于数据的机器学习算法中出现。通常不清楚什么值/设置应该选择。
你可能试图建议我们尝试每一个不同的值,观测谁工作的最好。这是一个好主意,实际上也是我们要做的,但是这样做必须要小心。特别地,我们不能使用测试集来调整超参数。无论
你设计机器学习算法的什么时候,你都应该将测试数据作为做宝贵的资源,直到最后之前保证它没有被接触过。否则,如果你在测试集集上调整超参数,使得它工作很好,在你部署模型之后,你将看到一个性能的明显的下降。实际上,我们将这种情况叫做过拟合。另一种观察方式是,如果您在测试集上调整超参数,您有效地使用测试集作为训练集,因此在部署模型时 您实现的性能对于您实际观察到的效果将是过于乐观。但是如果你仅仅在最后使用一次测试集,它仍然是你的分类器的好的代表。只在最后评测测试集。
幸运的是,有个正确的方法来调整超参数,并且不碰测试集。一个好的注意就是经训练集分成两部分:选出一小部分的训练集,我们叫做验证集(validation set)。使用CIFAR-10作为一个例子,使用49000个图片作为训练集,剩下1000作为验证集。验证集基本作为一个假的测试集来优化超参数。
在CIFAR-10上的例子:
# 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))
在程序结束的时候,我们作图显示哪个k工作的最好,并将这个k值在特测试集上验证。
交叉验证(Cross-validation):在某些例子中,训练集可能很小,人们有时使用一个叫做交叉验证的更加复杂的技术。在我们的前面的工作中,我们的想法是武断的选则1000个数据点作为验证集,剩下的作为训练集。通过迭代所有不同的验证集,平均他们的性能,你将得到一个确定k值的更好的低噪声的估计。例如,我们使用5-折交叉验证,分离训练集作为5等份,使用4份作为训练集,剩下的1份作为验证集。我们将在每一份上进行验证,估计性能,最终平均这个不同份的性能。
我们可以看到k=7时有最好的表现
实践中,由于交叉验证的计算昂贵,人们一般避免交叉验证,而是选择一个单一的验证集。一般使用50%-90%作为训练集,其他作为验证集。但是这依赖于多种因素:如果有很多的超参数,就需要大的验证集。在所有例子中,验证集很小,因此使用交叉验证是更安全。在实践中最常用的交叉验证是3折交叉验证,5折交叉验证和7折交叉验证。
交叉验证的可视化表示
最近邻算法的优缺点(Pros and Cons of Nearest Neighbor classifier.)
优点:
简单、易于实现、好理解
没有训练时间,但要存储训练集和下标
缺点:
测试时,计算复杂,在实践中,一般我们更关心测试效率。
提出近似最近邻算法(Approximate Nearest Neighbor)来加速最近邻算法,kd树加速算法。
虽然最近邻有时对于低纬度数据是很好的选择,但是在实际的图片分类中它很少使用。一个原因是图片是一个高维的数据对象,需要更多的空间存储。
一般情况下,使用像素的不同来比较图片是不充分的。下图中,是根据L2像素距离显示的。
CIFAR-10上使用t-SNE技术,嵌入到二维上。相邻的图片是也是其L2距离最近的。注意到背景比类别有更大的影响。
总结
我们介绍了图像分类问题,并给出了带有单独标记的图片集合。要求预测测试集中的分类并给出预测的正确率。
简单介绍了最近邻分类器,简单介绍了超参数的调优。例如最近邻中的k值。
介绍了验证集来调整超参数的选择,如果数据缺少时,交叉验证的使用。
一旦最优的参数被发现,将它使用到测试集上进行评价。
我们看到k最近邻在CIFAR-10上只有大约40%的正确率,它简单但是要求存储所有的训练数据,并且测试昂贵。
最后说了,L1和L2距离在图片分类问题上使用是不充分地,因为背景起到了太大的作用。
在下一节我们将着手解决这些挑战,达到一个90%的正确率,而且我们将完全放弃训练集,
评价测试集时的时间低于毫秒。
扩展阅读:
A Few Useful Things to Know about Machine Learning, 特别是第六章,但是整篇文章也值得读
Recognizing and Learning Object Categories, 物体分类的简单介绍。