模块1:神经网络
第1部分:图像分类:数据驱动的方法,k-近邻,划分训练集 / 验证集 / 测试集
原文链接
这篇介绍性教程目的是向非机器视觉专业的人介绍图像分类问题,以及数据驱动的方法。
动机。在本节中,我们将介绍图像分类问题,这是一种从固定类别中为输入图像分配标签的任务。它是计算机视觉的核心问题之一,尽管很简单,却有着各种各样的实际应用。而且,后面将提到,许多其他看似不同的计算机视觉任务(如目标检测、分割)都可以归结为图像分类。
例子。例如下图中,图像分类模型会为一张图像分配可能的4种标签之一{猫,狗,帽子,杯子}。如图所示,记住,对于计算机而言,图像会被表示成一个大的三维数组。在这个例子中,猫图像的宽为248像素,高为400像素,有三个颜色通道:红、绿、蓝(简称为RGB)。因此,图像由248 x 400 x 3个数字组成,共297,600。每个数字都是一个从0(黑)到255(白)的整数。我们的任务是将这25万个数字变成1个标签,如“猫”。
挑战。识别视觉概念(如猫)的任务对人类来说很容易,因此需要从计算机视觉算法的角度考虑所涉及的挑战。下面我们提出了一系列(无穷无尽的)挑战,请记住图像的原始表示形式,亮度值的三维数组:
视点变化。相机可以从多个方向朝向对象。
尺度变化。视觉类通常在尺寸(现实中的尺寸和图像上的尺寸)上有所不同。
形变。许多感兴趣的物体不是刚体,可以以极端方式变形。
遮挡。感兴趣的对象可能会被遮挡。有时只有对象的一小部分(少数像素)可见。
光照条件。光照在像素级上的影响非常大。
背景混乱。感兴趣的物体可能混入背景中,使其难以识别。
类内变化。一种感兴趣的类别通常比较宽泛,如椅子。这些对象有很多不同的风格和外观。
一个好的图像分类模型必须对这些变化共同作用的结果保持性能不变,同时保持类间变化的敏感性。
数据驱动的方法。我们怎样实现一种算法,将图像分为不同的类别?与编写一个用于排序数字列表的算法不同,如何编写用于识别图像中的猫的算法并不明确。因此,我们不是直接在代码中指定每一个感兴趣的类别,而是采用与儿童一样的方法:为计算机提供每一类的大量例子,然后开发学习算法观察这些例子,并了解每类的视觉外观。这种方法被称为数据驱动的方法,因为它依赖于前期累积的带标签图像的训练数据集。以下是这种的数据集例子:
图像分类流程。正如我们所见,图像分类的任务是:获取表示单个图像的像素阵列,并为其分配标签。完整的流程如下:
我们将介绍的第一种方法是最近邻分类器。这种分类器与卷积神经网络无关,在实际中很少使用,但它可以让我们了解一种图像分类问题的基本方法。
图像分类数据集示例:CIFAR-10。CIFAR-10数据集是一个流行的小图像分类数据集。这个数据集由6万个微小的图像组成,这些图像的高和宽都是32个像素。每个图像都标有10类中的一个(例如“飞机,汽车,鸟”等)。这60,000个图像被分割成50,000个图像的训练集和10,000个图像的测试集。下图是每类10个随机示例图像:
你可能注意到,我们没有详细说明如何比较两个图像的细节,两个图像只是两个32 x 32 x 3的块。一个最简单的方法是逐个像素地比较图像,并把所有这些差异累加。换一种说法,给定两个图像,并将其表示为向量 I1,I2 ,一种合理的选择是比较它们的L1距离:
在所有像素差异上求和。下面可视化步骤:
看看我们如何在代码中实现分类器。首先,我们将CIFAR-10数据以4个数组的形式加载到内存中:训练数据/标签、测试数据/标签。在下面的代码中,Xtr(大小为50,000 x 32 x 32 x 3)保存训练集中的所有图像,相应的一维数组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) )
请注意,作为评估标准,通常使用精确度来度量预测正确的部分。请注意,我们构建的所有分类器都满足这一个通用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个类别,随机猜测有10%的准确度),但远不及人类的表现(估计约为94%),最先进的卷积神经网络可以达到95%,媲美人类的准确性(参见CIFAR-10最近的Kaggle比赛排行榜)。
距离的选择。计算矢量之间距离的方法还有很多。另一个常见的选择是L2距离,它的几何解释是两个向量之间的欧氏距离。形式是:
请注意,上式包含了对np.sqrt
的调用,但是在实际的最近邻应用程序中,我们可以忽略平方根操作,因为平方根是一个单调函数。也就是说,它缩放距离的绝对大小,但它保留了排序,所以有或没有平方根的最近邻是相同的。如果在CIFAR-10上运行这种最近邻分类器,可以获得35.4%的准确率(略低于L1距离结果)。
L1与L2。这两个指标之间的差异很有趣。特别是,当涉及到两个向量之间的差异时,L2距离比L1距离更严格。也就是说,L2距离更偏向于较大的差异。L1和L2距离(或者等价于一对图像之间差异的L1 / L2范数)是p-范数最常用的特例。
你可能已经注意到,当我们在预测时,只使用最近图像的标签会很奇怪。实际上,k-最近邻分类器可以做得更好。这个想法非常简单:不仅是从训练集中找到最接近的一个图像,而是找到前k个最接近的图像,并让它们在测试图像的标签上投票。特别是,当k=1时,恢复到最近邻分类器。直观上,较高的k值具有平滑效果,使得分类器对异常值更具抵抗性:
实际上,你几乎总是想用k-最近邻。但应该使用什么样的k值?接下来探讨这个问题。
k-最近邻分类器需要对k进行设置。但是,什么数字最适合?另外,我们看到有许多不同的距离函数可供我们使用:L1范数,L2范数,还有许多我们甚至没有考虑过的其他选择(例如点积)。这些选择被称为超参数,它们经常会在许多从数据中学习的机器学习算法中出现。人们应该选择什么样的值/设置通常并不明显。
你可能会试图建议我们尝试许多不同的值,看看哪个效果最好。这是一个好主意,确实是我们要做的事情,但一定要非常小心。尤其是,我们不能使用测试集来调整超参数。无论何时设计机器学习算法,你都应该将测试集作为一个非常宝贵的资源,理想情况下永远不会被触及,直到最后。否则,真正的危险是你可能会调整超参数使得在测试集上正常工作,但如果要应用模型,性能可能会显著降低。在实践中,我们会说你对测试集合过拟合了。另一种看待问题的方式是,如果你在测试集上调整超参数,那么你将有效地把使用测试集作为当做训练集使用,因此,当应用模型的时候,相对于实际观察到的情况,模型学习到的性能过于乐观。但是如果你只在最后使用测试集,那么它仍然是衡量分类器泛化的一个很好的指标(在后面我们将会看到更多关于泛化的讨论)。
只在最后对测试集进行一次评估。
幸运的是,调整超参数有一个正确的方法,它根本不会触及测试集。其思想是把训练集分成两部分:一个稍小的训练集,以及我们所说的验证集。以CIFAR-10为例,我们可以使用49,000个图像进行训练,并留出1,000个用于验证。该验证集本质上用作假测试集来调整超参数。
以下是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值最好。然后,我们将使用这个值,并在实际测试集上评估一次。
将你的训练集分成训练集和验证集。使用验证集来调整所有超参数。最后在测试集上运行一次并报告性能。
交叉验证。如果训练数据(验证数据也同样)很少,那么人们有时会使用更复杂的超参数调整技术(称为交叉验证)。用我们以前的例子,这个想法是:并非选取前1000个数据点作为验证集,其余的作为训练集,而是通过迭代不同的验证集并对这些性能进行平均,来得到某个k值更好、更少噪声的估计。例如,在5-折交叉验证中,我们将训练数据分成相等的5个折叠,其中4个用于训练,1个用于验证。然后,我们将迭代验证折叠,评估性能,最终将不同验证折叠的性能进行平均。
实际。在实践中,相比于交叉验证,人们倾向于单个验证划分,因为交叉验证的计算开销更多。人们一般会把训练数据的50%-90%当做训练集,剩下的作为验证集。但这取决于多个因素:例如,如果超参数的数量很多,你可能想划分更大的验证集。如果验证集中的样本数量很少(可能只有几百个),则使用交叉验证更安全。实践中,典型的折数是3-折、5-折或10-折交叉验证。
最近邻分类器的优缺点
最近邻分类器的优点和缺点值得考虑。显然,其中一个优点是实施和理解起来非常简单。此外,分类器不需要花时间来训练,所需要的是存储并且可能将训练数据索引。然而,我们在测试时支付这个计算成本,因为分类一个测试样本需要与每个训练样本比较。这是倒退的,因为在实践中,我们经常关心测试的时间效率远超训练时间。事实上,我们稍后在这个课上将要讲解的深度神经网络将这种权衡转移到另一个极端:训练成本非常昂贵,但是一旦训练结束,对新的测试样本进行分类的成本是非常低的。这种操作模式在实践中更加可取。
另外,最近邻分类器的计算复杂度是一个活跃的研究领域,并且存在若干近似最近邻(ANN)算法和库,能够加速数据集(例如FLANN)中的最近邻查找。这些算法允许在检索期间利用其空间/时间复杂度来权衡最近邻检索的正确性,并且通常依赖于涉及构建kd树或运行k-均值算法的预处理/索引阶段。
最近邻分类器可能是某些情况(尤其是在数据维度较低的情况下)的一个不错的选择,但在实际的图像分类中很少使用。一个问题是图像是高维对象(即它们通常包含许多像素),并且在高维空间上的距离可能非常不直观。下图说明了我们上面所述的基于像素的L2相似性与我们感知的相似性完全不同:
下面通过一个更加可视化的例子来说服你,使用像素差异来比较图像是不够的。使用t-SNE可视化技术来读取CIFAR-10图像,并将它们嵌入到两个维度中,以便最好地保留它们(局部)成对的距离。在这个可视化中,根据L2像素距离,附近显示的图像被认为是非常接近的:
尤其要注意,彼此相邻的图像更多地是图像的颜色分布或背景类型的作用,而不是其语义标识。例如,可以看到一只狗非常接近于一只青蛙,因为它们都在白色背景上。理想情况下,我们希望10类图像形成自己的聚类,使得同一类的图像彼此相邻,而不管无关的特征和变化(例如背景)如何。但要获得这个属性,我们将不得不超越原始像素。
总结如下:
我们介绍了图像分类问题,在这个问题中,我们给出了一组图像,这些图像都被标记为某个类别。然后要求我们预测属于这些类别的一组新的测试图像,并衡量预测的准确性。
我们引入了一个简单的分类器,称为最近邻分类器。我们看到有多个与分类器相关超参数(如k的值、距离类型),这些超参数没有明确的方法来选择。
我们看到,设置这些超参数的正确方法是将训练数据分为两部分:训练集和假测试集,我们称之为验证集。尝试不同的超参数值,并保留在验证集上性能最佳的值。
如果缺乏训练数据,我们讨论了一个叫做交叉验证的步骤,在评估哪些超参数工作得最好时,它可以帮助减少评估的噪声。
一旦找到最好的超参数,我们就固定它们,并对实际测试集进行单次评估。
我们看到最近邻在CIFAR-10上的准确率可以达到40%左右。实现起来很简单,但要求我们存储整个训练集,并且在测试图像上评估的时间开销是昂贵的。
最后我们看到,在原始像素值上使用L1或L2距离是不足的,因为相比语义内容,这些距离与图像的背景和颜色分布更相关。
在接下来的教程中,我们将着手解决这些问题,并最终得到90%准确率的解决方案,可以在学习完成后完全丢弃训练集,并在不到一毫秒的时间内评估测试图像。
如果你想在实践中应用kNN(希望不是图像,或者只是一个基准线),按如下步骤进行:
对数据进行预处理:将数据中的特征(例如图像中的一个像素)标准化为零均值和单位方差。我们将在后面的章节中更详细地介绍这一点,因为图像中的像素通常是均匀的,并且不会呈现广泛不同的分布,因此减少了对数据规范化的需求。
如果你的数据维度非常高,可以考虑使用降维技术,比如PCA( wiki ref,CS229ref,blog ref)或是 随机投影 。
将训练数据随机分成train / val分组。从经验上来讲,70-90%的数据通常分配给训练分组。这个设置取决于你有多少超参数以及你期望它们有多少影响力。如果有多个超参数需要估算,那么你应该在更大的验证集上进行更有效的估算。如果您担心验证数据的大小,最好将训练数据拆分为折叠并执行交叉验证。如果你能负担得起计算预算,那么交叉验证就越安全(折叠越多越好,但代价越高)。
在验证数据上,对多个k(越多越好)和不同距离类型(L1和L2是好的选择)训练和评估kNN分类器(对于所有折叠,如果进行交叉验证)。
如果你的kNN分类器运行时间过长,请考虑使用近似最近邻库(例如FLANN)来加速检索(以准确度为代价)。
记下给出最佳结果的超参数。有一个问题是,是否应该使用具有最好超参数的完整训练集,因为如果将验证数据添加到训练集中(因为数据的规模会更大),则最优超参数可能会发生变化。实际上,在最终的分类器中不使用验证数据,并且在估计超参数时认为它被烧掉是更好的。评估测试集上的最佳模型。报告测试集的准确率,并将结果声明为数据上kNN分类器的性能。
这里有一些(可选)链接,你可能会发现有趣的延伸阅读:
关于机器学习的一些有用的知识,特别是第六部分与课程相关,但强烈推荐通读整篇论文。
识别和学习对象类别,ICCV2005对象分类的短期课程。