【斯坦福cs231n】2-1图像分类

图像分类

动机.在这一部分,我们将介绍图像分类问题,这是将输入图像分配到一组固定类别标签中的某一个标签的任务。尽管这个问题很简单,但它是计算机视觉的核心问题之一,并且有着各种各样的实际应用。此外,正如我们将在后面看到的,许多其它不同的计算机视觉任务(例如对象检测,分割)都可以被简化为图像识别问题。

示例.例如,在下面的图像识别的例子中,图像分类模型接收单张图片,并对四个标签{cat,dog,hat,mug}的概率赋值。如图所示,请记住,对于计算机而言,图像被表示为一个大的三维数组。在这个例子中,猫图像是248像素宽,400像素高,并且有三个颜色通道:红色,绿色,蓝色(或简称RGB)。因此,该图像由248 x 400 x 3数字组成,总共297,600个数字。其中每个数字都是一个范围在0(黑色)到255(白色)的整数。我们的任务就是把这将近30万个数字转成像{cat}这样的单一的标签。

图像分类的任务是预测给定图像的单个标签(或者类似这里的一个标签的概率分布,用于表示我们信心)。图像是包含0到255整数的3维整数,大小是宽度x高度x3。其中3表示RGB三色通道。


挑战.由于视觉识别问题(例如识别“猫”的例子)对于人类来说,是再简单不过的问题,但从计算机视觉算法的角度来重新审视这个问题,就充满了挑战。在我们下面提出的这些挑战中,请记住图像的原始表示形式为3-D阵列的亮度值:

  • 不同的观察点(Viewpoint variation)照相机可以从多个方向来拍摄同一个实例对象。
  • 大小变化(Scale variation)视觉分类通常会出现不同的尺寸变化(这里包含在现实世界中的尺寸,不仅仅是针对图像中的大小程度)。
  • 变形(Deformation)许多物体并不是刚体,并且可以以极端的方式变形。
  • 闭塞(Occlusion)我们所观察的目标对象可能处于被遮挡的状态。有的时候我们只能看到对象的一小部分(像素比较少)。
  • 光照情况(Illumination conditions)照明的效果在像素级上的影响是剧烈的。
  • 背景杂波(Background clutter)我们所观察的对象可能融入到他们的背景环境中,使其难以识别。
  • 类型内部变化(Intra-class variation)我们所关心的类别通常会比较宽泛,比如椅子。不同类型的椅子有着不同的外观,但他们都属于“椅子”这一类别。

一个好的图片识别分类模型必须在所有这些情况交叉出现的情况下,都能产出不变的结果输出,同时保持类别变化时的敏感性。


数据驱动的方法.如何编写一个将图像分类到不同类别的算法呢?与编写一个像数字排序这样的算法不同的是,如何编写一个识别猫的算法的方法并不是显式的。我们不会视图在代码中指定每个不同的类别在代码中是什么样的,我们所采取的方法就像是在教一个小孩子一样:我们首先为每个类别提供许多个实例,然后开发学习算法,通过学习算法来输入这些类别实例图像,学习到这些每个类别的视觉外观。这种方法被称为数据驱动方法,因为它依赖于事先已经被标记过的标签的图像的训练数据集:

上图是四个视觉类别的训练集示例。在实际情况中,我们可能会有数千种类别和数十万种图像。


图像分类流水线.正如我们已经看到的,图像分类中的任务是使用一个像素数组来代表一个图像,并且为其分配一个标签。完整的流水线可以表示成如下的流程:

  • 输入:我们的输入由一组N个图像组成,每一个都标有K个不同的类别。我们将这些数据称为训练集。
  • 学习:我们的任务是使用训练集来了解每一个类别的样子。我们将此步骤称为训练分类器(training a classifier),或者学习模型(learning a model)
  • 评估:最后,我们通过预测一组从未见过的新图像的标签来评估分类器的质量。然后,我们将这些图像的真实标签与分类器预测的图像进行比较。直观地说,我们希望达到的效果是大多数预测结果与真实答案(我们称之为ground truth)相匹配。

最邻近分类器(Nearest Neighbor Classifier)

我们将开发一种叫做最邻近分类器,来作为我们的第一个图片分类的实现。这种分类器与卷积网络无关,在实践中很少使用,但它可以使我们了解处理图像分类问题的基本方法。

示例图像分类数据集:CIFAR-10.CIFAR-10是一个流行的toy级别的图像分类数据集。这个数据集包含60,000个长宽都是32像素的小图片。每个图片都有一个标签(例如“飞机,汽车,鸟等”)。这60,000张图像被划分为50,000张图像的训练集和10,000张图像的测试集。在下面的图片中,您可以看到10个类别中的每个类别的10个随机示例图像:

左图:来自CIFAR-10数据集的示例图像。右图:第一列显示几个测试图像,并且在每个测试图像旁边,我们根据像素差异显示训练集中的前10个最近邻居。


假设现在我们有50,000张训练图像(50,000张图像都有其对应的标签数据),并且我们希望为剩余的10,000张图片打标签。最邻近分类器将接收一个测试图片,与每一张训练图片做对比,然后以其最接近的训练图像的标签作为其预测标签。在上面的右图中,您可以看到10个示例图片的十个用这种方式得到的最相近的结果图片。请注意,在这10个示例中,有三个图片在检索相同的类别,而其他7个示例并非如此。例如,在第八行最接近“马头图片”的训练的图像是一个“红色汽车图片”,也许是由于它们都拥有黑色背景的原因。这种结果,使得这匹马的形象在这种情况下会被误认为是汽车。

您可能已经注意到,我们并没有详细的说明我们是如何比较这两个尺寸为32 x 32 x 3的图像的细节的。一种最简单的处理方式是对像素点逐个比较,然后求差值绝对值之和。换句话说,给出两个图像,并将其表示为向量$I_1, I_2$,比较他们的一种合理的方式是求L1距离:

$$
d_1 (I_1, I_2) = \sum_p \left| I^p_1 - I^p_2 \right|
$$

下面是将这段程序可视化的过程:

一个使用L1距离来比较图片像素差异的示例(在这里例子中只有一个颜色通道)。将两个图像元素相减,然后将所有差值相加到一个数字上。如果两个图像相同,则结果将为零。但如果图像非常不同,结果会很大。


我们来看看我们如何在代码中实现分类器。首先,我们将CIFAR-10数据作为4个阵列加载到内存中:用于训练的数据/标签集合,以及用于测试的数据/标签集合。在下面的代码中,Xtr(尺寸是50,000 x 32 x 32 x 3)包含全部的用于训练的图像数据,与之对应的是一个一维数组Ytr(长度是50,000)持有训练集标签(标签是0到9的类别):

1
2
3
4
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

现在我们把所有的图像都拉伸成行了,下面的代码演示了我们如何训练并且评估一个分类器:

1
2
3
4
5
6
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) )

请注意,作为评估标准,我们通常使用准确率(accuracy)来衡量预测结果的准确性。请注意,我们将构建的所有分类器都满足这一个常见的API:它们有一个接受用来学习的数据和标签的train(X,y)函数。在内部,该类应该建立一些标签的模型,以及如何从数据中预测结果的逻辑。同时还需要有一个用来接受新数据并且预测其标签的predict(X)函数。下面是一个L1距离的最邻近分类器的一个简单实现,它满足这套模板:

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
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距离。公式如下:

$$
d_2 (I_1, I_2) = \sqrt{\sum_{p} \left( I^p_1 - I^p_2 \right)^2}
$$

换句话说,我们之前需要计算两者的像素差,而这一次,我们需要计算像素差的平方,并且把他们加起来之后开根号。在numpy中,我们可以用一行代码来实现上述的计算:

1
distances = np.sqrt(np.sum(np.square(self.Xtr - X[i,:]), axis = 1))

请注意,在上面的计算中,我引入了np.sqrt,但在实际的最邻近应用中,我们可以省略求平方根的操作,因为平方根是一个单调函数。也就是说,它对距离的绝对值的差值起到了缩放的作用,但它依然保留了排序结果,因此对于最邻近问题来说,有没有这一步开根号的运算,其结果都是相同的。如果你在CIFAR-10上运行这个L2最邻近分类器,那么你得到的准确率是35.4%(比L1的结果略低一些)。

L1 vs L2.这两者之间存在什么样的差异呢?这是一个很有意思的问题。在特定的情况下,当涉及两个向量之间的差异时,L2距离比L1距离更糟一些。L1和L2距离(或等效地,一对图像之间的差异的L1 / L2范数)是p范数最常用的特殊情况。

KNN分类器(k - Nearest Neighbor Classifier)

您可能已经注意到,当我们想进行预测时,只使用最接近的一个图像的标签是很奇怪的。事实上,通过使用所谓的KNN分类器,人们可以做得更好。实际上这个想法很简单:不是在训练集中找到单个最接近的图像,我们将找到最接近的k个图像,并让他们对测试图像的标签进行投票。尤其是,当$k=1$的时候,我们KNN分类器实际上就是最邻近分类器。从直觉上来说,较高的k值有更平滑的效果,使得分类器更能抵抗离群值:

上图中,使用二维散点图用三种颜色(红、蓝、绿)来表示三种类别,分别展示了原始数据在最邻近分类器上和在5-NN分类器上的效果。彩色的区域显示在L2距离下生成的判定边界。白色区域显示出不能被明确分类的点(即,类别投票至少被分为两类)。请注意,在使用最邻近分类器的情况下,异常值数据点(例如蓝色区域中的绿点)会产生可能不正确的预测的区块,而5-NN分类器在这种情况下会得到相对平滑的效果,这也意味着对测试数据的泛化能力更好(未被显示)。还要注意,5-NN图像中的灰色区域是由最邻近的几个邻居颜色不同导致的(例如,有两个是红色的,两个是蓝色的,一个是绿色的)。


在实践中,当我们使用kNN分类器时,应该选择什么样的k值才合适呢?接下来我们来谈谈这个问题。

验证集,交叉验证,超参数调谐

kNN分类器需要指定一个k值,但是当k取什么值时效果最好呢?另外,我们还可以选择L1范数、L2范数,等,以及许多我们甚至没有考虑到的选择。这些选择被称为超参数(Hyperparameter),他们在许多机器学习算法的设计过程中都会出现。通常这些值应该被设置为多少,并不是十分显而易见。

你也许会试图建议我们尝试许多不同的值,然后看看哪些值的效果最好。这的确是一个好办法,这也是我们接下来要做的,但这个过程必须非常仔细地进行。特别要说吗的是,我们不能使用测试集来调整参数。当您在设计一款机器学习算法时,您应该将将测试集视为一个非常宝贵的资源,在理想情况下,除非在测试阶段,都不要去触碰它。否则,你调整的参数将会作用域测试集上,这样做非常危险,因为当你开始在真实的数据上使用该模型时,你会发现性能显著降低。在实践过程中,我们称这种现象为过拟合测试集。关于这一现象的另一种解释是,如果你在测试集上调整了参数,你实际上是在把测试集当做训练集在使用,因此,你实现的模型的性能对于实际观察到的情况来说都是过于乐观的。但与之相反的,如果我们只是在最后的测试过程中使用一次测试集,那么它仍然是测量分类器泛化的一个很好的代理(我们将在以后的课程中看到更多关于泛化的讨论)。

评估测试集在每次训练结束后只运行一次。

幸运的是,有一种不触碰测试集的调整超参数的方法。这个方法就是将我们的训练集分为两部分:其中稍微小一些的那部分训练集,我们称之为验证集(validation set)。使用CIFAR-10为例,我们使用49,000的样本作为训练集,然后使用剩下的1,000个样本作为验证集。这个验证集是用来调整超参数的一个假测试集。

在CIFAR-10中,这个例子看起来可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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值,对真正的测试集进行一次评估。

将你的训练集分割为训练集和验证集。使用验证集来调整所有的超参数。最后在测试集上仅运行一次评估操作,并上报结果。

交叉验证.在你的训练集(同时也包括验证集)可能非常小的情况下,人们有时使用一个更复杂叫做交叉验证的技术来调节超参数。作用于我们之前的例子中,取代之前我们选取1000个数据点来作为验证集、剩下的部分用做测试集这种方式,我们通过迭代不同的验证集以及求得他们的平均表现这种方式,来得到一个更好的,噪音更小的k值。举个例子,在5倍交叉验证中,我们将数据分为5等份,使用其中4份来训练,用剩余的1份作为验证。然后我们迭代所有其他份数据来作为验证集,计算每一份作为验证集的最终表现,最终将每次得到的表现求和在求平均值。

参数k的五倍交叉验证运行示例。对于k的每个值,我们都在4份数据上训练,并且在第5份数据上评估。因此,对于每个k,我们在交叉验证上都会有5个评估得到的准确率(y轴是准确率,每个结果对应一个点)。趋势曲线通过每个k的结果的平均值绘制,错误条表明标准偏差。注意在这里的一个特定场景,交叉验证集建议我们选取的k=7,此时在数据集上的预测效果最好(对应于图中的峰值处)。如果我们使用超过5份的数据,我们也许会看到一个更平滑(低噪音)的曲线。


实践.在实践中,人们更倾向于避免使用交叉验证,人们更愿意接受单个的验证集分割,因为交叉验证的计算是十分昂贵的。人们倾向使用的分割方式是将训练集分割50%-90%用作训练,剩下的部分用作验证。然而,这取决于多个因素:例如如果超参数数量很大,你也许更倾向于使用一个更大的验证集分割。如果验证集样本的数量很少(可能只有几百个),那么使用交叉验证则更安全一些。正如你所见的,典型的交叉验证可能会是三等分、5等分或者10等分的交叉验证。

常见的数据分割。给定训练集和测试集。训练集被分割为几等份(例如这里的五等分)。第1-4份数据作为训练集。一份作为验证集(例如这里的黄颜色的第五份),用来调节超参数。交叉验证更进一步的操作是迭代循环这5份数据,分别作为验证集的预测结果。这被称为5倍交叉验证。一旦模型被训练,并且确定了所有的最佳的超参数,最终在测试集(红色)上单词评估干模型。


最近邻分类器的优缺点

思考最邻近分类器的优缺点是一件值得做的事情。很明显,一个优点是:它的实现很简单,理解起来也很简单。此外,这个分类器不需要训练的时间,因为用于预测结果的数据全部来自于被存储的以及可能被索引的训练数据。但是,我们需要消耗一次测试的时间,因为对测试数据进行分类,我们需要将每个训练样本进行比较。这是一种不好的方式,因为在实践过程中,我们往往很在意测试运行的时间,而不太在意训练所花费的时间。事实上,我们将在稍后的深度神经网络课程中,我们将看到另一个极端:它会在训练样本的过程中花费巨大的开销,但一旦训练结束,在一个新的测试样本上执行分类任务时的开销是非常小的。这种模式在实践中更为理想。

除此之外,关于最邻近分类器的计算复杂度问题,一直是一个活跃的研究领域,并且有一些可以加速数据集中最邻近数据的查找的近似最邻近(Approximate Nearest Neighbor(ANN))算法和库存在(例如FLANN)。这些算法允许在检索期间以其空间/时间复杂度来折衷最近相邻检索的正确性,并且通常依赖于涉及构建kdtree或运行k-means算法的预处理/索引阶段。

在某些场景中,最近邻分类器有时可能是一个很好的选择(特别是如果数据是低维数据),但很少适用于实际的图像分类场景。其中一个问题是图像是高维度对象(即它们通常包含许多像素),并且高维空间的距离是非常不直观的。下面的图像说明了我们上面开发的基于像素的L2相似度与人类感知相似性的区别:

高维数据(尤其是图像)上基于像素的距离可能非常不直观。基于L2像素距离,原始图像(左)和旁边的三个其他图像都距离它们相同。显然,像素方向的距离并不能与人类感知上的相似性相对应。


这里有更多的可视化数据来说服你,使用像素差异来比较图像是不够的。我们可以使用一种名为t-SNE的可视化技术来拍摄CIFAR-10图像,并将其嵌入到二维空间中,使其(局部)成对距离最好地保留下来。在这种可视化中,根据我们上面开发的L2像素距离,我们将其L2距离相对较小的图像聚集在一起:

使用t-SNE嵌入二维的CIFAR-10图像。该图像附近的图像被认为是基于L2像素距离接近的。注意到背景的强烈影响,而不是语义层次的差异。点击这里查看这个可视化的更大版本。


特别地,请注意,彼此相邻的图像更多是图像的平均颜色分布或背景的类型相似的图像,而不是其相似的标签类型的图像。例如,可以看到一张狗的图片和一张青蛙的照片像邻,因为两者都是在白色背景上。理想情况下,我们希望所有10个类中的图像形成自己的集群,使得同一类的图像在彼此附近,而不管不相关的特征和变化(如背景)。然而,要获得这个属性,我们将不得不超越像素级别的去考虑问题。

概要

综上所述:

  • 我们引入了图像分类的问题,其中给出了一组全部标记为单一类别的图像。然后,我们要求为这些类别预测一组新的测试图像,并测量预测的准确性。
  • 我们引入了一个称为最近邻分类器的简单分类。我们看到有与此分类器相关联的多个超参数(如k值或用于比较示例的距离类型),并没有明显的选择方式。
  • 我们看到设置这些超参数的正确方法是将训练数据分为两个:训练集和假测试集,我们称之为验证集。我们尝试不同的超参数值,并保持在验证集上达到最佳性能的值。
  • 如果缺乏培训数据是一个问题,我们讨论了一个称为交叉验证的过程,它可以帮助减少噪声,以估计哪些超参数最有效。
  • 一旦找到了最佳的超参数,我们修复它们,并对实际测试集执行单个评估
  • 我们看到最近邻居可以在CIFAR-10上获得约40%的准确性。它实现起来很简单,但要求我们存储整个训练集,并且在测试图像上进行评估是很昂贵的。
  • 最后,我们看到在原始像素值上使用L1或L2距离是不够的,因为这些距离与图像的背景和颜色分布相比,与其语义内容相比更强烈。

在接下来的课程中,我们将着手解决这些挑战,最终达成90%精度的解决方案,让我们在完成学习后完全丢弃训练集,并允许我们在不到一毫秒内评估测试图像。

总结:在实践中应用kNN

如果您希望在实践中应用kNN(希望不是在图像上),请按如下步骤进行:

  • 1.预处理数据:规范数据中的特征(例如图像中的一个像素),使其具有零均值和单位方差。我们将在后面的章节中更详细地介绍这一点,并且选择不覆盖本节中的数据规范化,因为图像中的像素通常是均匀的,并且不会展现出广泛不同的分布,从而减轻了数据规范化的需要。
  • 2.如果您的数据非常高,请考虑使用维度降低技术,如PCA(wiki refCS229ref博客引用),甚至使用随机投影
  • 3.将您的训练数据随机分成训练集/验证集。根据经验,70-90%的数据通常会分配到训练集上。此设置取决于您拥有多少超参数以及您期望他们拥有多少影响力。如果有很多超参数需要估计,那么您应该在验证集更大的一边进行有效的估计。如果您的验证数据集比较小,最好将训练数据拆分为几等分,并执行交叉验证。如果你能负担得起计算机的运算量,那么交叉验证(更多的份数越好,但是更昂贵)总是更安全。
  • 4.对于k的许多选择(比如越多越好)和不同距离类型(L1和L2都是不错的选择),对验证数集(对于所有份数,如果进行交叉验证)训练和评估kNN分类器。
  • 5.如果您的kNN分类器运行时间过长,请考虑使用近似最近邻库(FLANN)来加速检索(以某种精度为代价)。
  • 6.记下提供最佳效果的超参数。有一个问题是您应该使用最佳超参数的完整训练集,因为如果要将验证数据折叠到训练集中(因为数据的大小会更大),最佳超参数可能会改变。实际上,在最终分类器中不使用验证数据更为清晰,并且在估计超参数时认为它被刻录。评估测试集上的最佳模型。上报在测试集上的准确率,这个结果作为kNN分类器的最终表现。

进一步阅读

这里有一些(可选)链接,您可能会发现更多有趣的东西:

坚持原创技术分享,您的支持将鼓励我继续创作!