【Stanford CNN课程笔记】1. Image Classification and Nearest Neighbor Classifier
本课程笔记是基于今年斯坦福大学Feifei Li, Andrej Karpathy & Justin Johnson联合开设的Convolutional Neural Networks for Visual Recognition课程的学习笔记。目前课程还在更新中,此学习笔记也会尽量根据课程的进度来更新。
今天的话题是:图像分类和最近邻分类器。
http://blog.csdn.net/elaine_bao/article/details/50511419
1. Image Classification
1.1 问题概述
图像分类是指输入一张图片,让计算机从给定的众多类别中搜索出它的真实类别。例如,输入下图,输出它属于{猫,狗,帽子,杯子}四个类别中的哪个。
对于计算机而言,它看到的并不是图片,而是(寂寞…)一个三维矩阵。这个例子里,猫这张图片是248 pixel*400 pixel,并包含RGB三个颜色通道,也就是说这张图片的矩阵是248*400*3维的,总共2976000个值,每个值是0(黑)~255(白)之间的整数。我们的任务就是将这300万输入的值转变成一个简单的标签输出,比如我们想要的标签是“猫”。
1.2 面临的问题
对于人类来说识别物体是一件很容易的事情,然而从计算机的角度来看就不那么简单了,刚刚我们说到计算机看到的并不是一张图片,而是一大堆像素点构成的三维矩阵。另外其他面临的问题还有:
- 视角的差异:摄像机与物体的相对位置不同,拍摄出的图片会发生很大的变化,如下图。
- 尺度差异:同一类别的物体在图像上的表现大小不一。
- 形变:许多我们感兴趣的物体并不是刚体,比如下图那只猫,其柔软程度真是够了。
- 遮挡:物体可能受到其他物体的遮挡导致只有部分可见。
- 光照条件:光照的影响在pixel level的变化是非常显著的。
- 背景杂乱(background clutter):物体和背景过于接近使得它很难被识别出来。
- 同类物体的差异:“类”的概念可以很宽泛,比如椅子这个类,我们就可以给出长相很不同的椅子,这也给分类增加了难度。
1.3 数据驱动方法
所以我们究竟要怎么做才能对图片进行分类呢?就像一个小孩学习对世界的认知一样,我们首先给计算机提供每个类的很多样例,让它去学习每个类该长什么样子。这种方法称为数据驱动的方法。顾名思义,它需要收集大量的数据用于训练,我们称为训练集(training set)。下图是一个训练集的例子,假设我们有4个类别(现实中类别数目远比4大),我们为每个类别收集一些图片,在训练时我们就告诉计算机这张照片是猫,那张照片是狗,让他去学习不同类别的特点。
1.4 图像分类流程
我们已经知道图像分类是输入一张图片(其实是一个像素构成的矩阵),输出它对应的一个标签。它的整个流程是这样的:
- 训练阶段(learning):我们首先准备包含K个类别共N张图片的一个训练集。在训练阶段,我们利用这个训练集来学习每个类别所具有的特征,这一步我们称为训练分类器或学习模型。
- 评估阶段(evaluation):为了评价所训练的分类器的好坏,我们给它一些从未见过的图片(称为测试集)让它去预测类别。就像小孩子今天学了新的知识,我们要测试一下学习效果。最后我们再拿这些图片真实的标签(称为ground truth)和预测的标签进行对比,我们所希望的是预测的标签基本和ground truth一样,这样就说明我们的分类器效果是好的。
2. Nearest Neighbor Classifier
我们先来学习最简单的最近邻分类器。这种分类器和卷积神经网络没有半毛钱关系,在现实生活中也不常用,但是它会帮助我们理解整个图像分类问题。
2.1 CIFAR-10 dataset
作为例子,我们会使用到的一个图像分类数据集是CIFAR-10 dataset. 这个数据集共包含60000张32*32pixel的小图片,每个图片的标签是以下10类中的1类:{飞机,汽车,鸟,猫,鹿,狗,青蛙,马,船,卡车}。这60000张图片被分成两部分,训练集包含50000张图片,测试集包含10000张图片。下图(左)是这10个类别随机的100张图片。
我们的任务是对测试集中的10000张图片进行分类。最近邻分类器的做法是:让每张测试图片和训练集中的所有图片做对比,然后以最相似的那张训练集图片的标签作为测试图片的标签。上图(右)可以看到为10张测试图片进行最近邻查找的结果,10个例子中只有3个分类正确,其他的都错了,比如第8行一个马头的最近邻是一辆车,这种误分类的原因可能是他们都有相似的黑色背景。
2.2 相似性度量
到现在为止,我们还没有具体说明如何比较两张图片之间的相似度。我们知道,一张图片是32*32*3的矩阵,那么比较两个矩阵,一种最简单的方法是比较每个像素点之间的差异再加起来:两张图片分别表示成两个矩阵I1,I2,其相似度度量可以通过L1距离:d1(I1,I2)=∑p|Ip1−Ip2|.
意思就是说像下图一样,把测试图片和某张训练图片进行pixel by pixel的作差,得到每个像素点之间的差异,再加和得到总的两张图片之间的差异。如果两张图片是一模一样的这个结果应为0,如果长得很不一样则L1距离会很大。
我们再来看看上述过程在代码中如何实现。首先读入CIFAR-10的数据,组成4个数组:训练数据/标签,测试数据/标签。以下代码中Xtr(size:50000*32*32*3)包含了训练集中所有的图片,对应的一个一维数组Ytr(length:50000)是这些图片的标签 (从0到9):
现在我们将所有的图片拉成一维向量,然后用于训练和评估:
在评估阶段我们经常会用到精度(accuracy),表示的是预测正确的图片数目占总测试图片数的比例。
接下来我们构建的所有分类器都会具有一个train函数和一个predict函数:train(X,y)用于接收训练图片和标签并进行学习;predict(X)接收新的测试图片并预测他们的标签。比如我们刚说的最近邻分类器的构造是这样的:
import numpy as np class NearestNeighbor: 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
- 如果在CIFAR-10上跑上面的代码,会发现这个分类器实现了38.6%的精度,额已经比随机的结果(10%)要好很多,但是相比人类在该数据集上的识别率(94%)或者优秀的CNN算法(95%),那就差太多了。
我们刚才用的是L1距离来衡量图片间的相似度,我们还可以用其他的,比如说另一个常用的距离是L2距离或叫欧氏距离: d2(I1,I2)=∑p(Ip1−Ip2)2−−−−−−−−−−−−√.
计算L2距离的时候,我们同样计算pixelwise的差异,但是这次我们先做平方和,再开方,在numpy中的代码是:
distances=np.sqrt(np.sum(np.square(self.Xtr-X[i,:]),axis=1))
- 1
- 1
其实在实际中计算L2距离时是否开方并不会对分类结果产生影响,因为开方是单调函数,它不会对距离大小的相对排序产生影响。那么如果我们用L2距离在CIFAR-10上跑一遍代码,精度为35.4%,比L1距离的结果略低。
在实际中何时使用L1距离何时使用L2距离呢?其实L2距离通常比L1使用的更加广泛,不过具体情况会有不同的preference。
2.3 k - Nearest Neighbor Classifier
k-NN分类器和NN分类器的一个区别是:NN只用了最相似的那张图片来预测测试图片的标签,而k-NN是用排名前k张最相似的图片进行投票得出测试图片的最终标签。可以想象,k-NN通常会比NN的效果更好。下图是NN classifier和5-NN classifier的对比结果。数据是分布在二维平面的点,包括3个类别(分别用红,绿,蓝表示)。两个分类器中的色块分别表示分类器使用L2距离得到的decision boundaries,白色的部分则表示进行随机分类(可能原因是比如说5个最近邻投票结果投出了至少两类)。我们可以看到在NN分类器中,一些离群的数据点(outliers)会划分出突兀的小色块,比如一大片蓝色色块中出现的一小块绿色色块,这种情况很有可能是不正确的预测结果。而在k-NN中,因为有投票的机制,它的结果相对更加平滑,对outliers也更为鲁棒。
在实际中,我们总是更倾向于使用k - Nearest Neighbor Classifier而不是Nearest Neighbor Classifier。那么k值怎么选取呢?请看下回分解->
3. Validation sets, Cross-validation, hyperparameter tuning
3.1 Validation sets for Hyperparameter tuning
k-NN分类器需要设置k的值。另外,我们也需要提前设定所使用的距离函数:到底是用L1距离,L2距离,还是其他一些函数(比如点积)呢?这些选择性的设置我们称为hyperparameters,它们在许多机器学习算法中都会出现。这些参数的值到底该如何选取并不是那么显而易见的。
你可能会建议说要不我们试一下不同的参数,看看哪个效果最好呗?这确实是个好主意,并且我们确实是这么做的,只不过如何进行这一步我们需要格外小心。特别注意,我们不能使用测试集的图片来调整hyperparameters,因为如果你这么做了,很有可能你的测试集会被你调整得识别率很高,但是当你实际去用你的模型时就会发现效果变得差很多。这种情况称为模型过拟合(overfit)了test set。另一个解释是如果你用测试集来调参,那么其实你是在把测试集当训练集用了。不管你在设计什么机器学习的算法,测试集都应该被看成一个非常珍贵的资源,并且只在最最后的一步才使用它。只有将测试集真正用于最后的测试环节,我们才能最真实地知道模型泛化的效果。
幸运的是,我们用正确的方法来调整这些hyperparameters,并且不需要动测试集。想法是将我们的训练集一分为二,其中一部分做验证集(validation set)用于调参。比如说以CIFAR-10为例,50000张训练图片,我们可以用49000张做训练,剩下1000张做验证。代码可以写成这样:
# 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))
- 1# 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-NN算法分类结果好坏的影响,并从中得到表现最好的那个k,将其固定,用于测试集。
Split your training set into training set and a validation set. Use validation set to tune all hyperparameters. At the end run a single time on the test set and report performance.
3.2 Cross-validation
有的时候可能训练集的大小有限(那么验证集也会很小),人们会采取稍微复杂一点的交叉验证(cross validation)的方法进行hyperparamters的调整。比如之前的例子,我们不再随机选取1000张图片作为验证集,而是进行多次验证集的选取,测多次参数,再把最后的结果进行平均。例如在5折交叉验证(5-fold cross-validation)中,我们首先将所有训练数据等分成5份,拿其中4份做训练,1份做验证,然后进行迭代,保证每一份都作为验证集用过一次,再把5次的结果进行平均。下图就是在CIFAR-10上对k值进行5折交叉验证的结果。相当于每个k值我们会得到5个validation accuracy,然后进行平均,最终的结果是当k=7的时候,平均精度最高。因此我们在测试的时候就会固定k=7。
实际中人们一般会尽量避免进行交叉验证,因为毕竟交叉验证计算量大。如果使用的话,通常会选择50%-90%的训练数据用作训练集,剩下的作为验证集,这个比例视具体情况而定:比如说我们需要调整的hyperparameters的数目很多,那么通常会选择大一点的验证集。如果验证集包含的数据数目很少,比如几百张,那么使用交叉验证的方法调参会比较保险。常用的交叉验证是3折,5折,10折。下图是5折交叉验证的示意图。当我们训练好了模型调整好了参数以后,最后的最后再把模型用于测试集去评估模型的效果。
4. Pros and Cons of Nearest Neighbor classifier
我们有必要来总结一下最近邻分类器的优缺点。很明显的一个优点是它非常容易理解和实现。另外,这个分类器并不需要花时间进行训练,它所做的只不过是把所有的训练数据保存起来以便索引。然而在测试的时候我们需要花费大量的时间,因为对一张测试图片的分类意味着将它和每一个训练图片作比较,这是一个很大的缺陷,在实际中我们通常更关心测试的效率而不是训练的效率。而我们即将涉及到的深度神经网络则和最近邻分类器正好相反,它在训练的时候非常耗时,而一旦训练好了,测试阶段效率会很高。这个特性是我们所希望的。
顺便说一下,如何降低最近邻分类器的计算复杂度也是研究的热门,有几种近似最近邻算法(Approximate Nearest Neighbor (ANN) algorithms)能够加速在数据集中查找最近邻的效率(如FLANN),这些方法通常在预处理阶段建立kd树或使用k-means聚类等,它们也是牺牲了一定的最近邻精度来换取空间/时间复杂度的降低。
最近邻分类器在某些情况下可能会是一种好的选择,特别是数据维度比较低的时候,但是对于图像分类问题来说它基本不适合。原因之一是图像是高维物体(包含很多像素),在高维空间中进行距离运算通常不可靠。下面4张图虽然从视觉上看我们知道是同一个人,但是如果对它们计算pixel-based L2 similarities,则会发现距离很大,说明对图像计算基于像素的距离来描述图像之间的相似度是不可靠的。
另一个例子同样可以说明基于像素进行图像比较是不恰当的。我们使用 t-SNE 的可视化技术将CIFAR-10中所有图片embed到两维空间,相邻的图片则表示 L2 pixelwise distance很小。我们可以发现相邻的图片大多有相似的颜色分布或者背景相似而不是语义上的相似。点击这里查看大图,比如一只狗和一只青蛙很近因为他们都有白色的背景。理想情况下我们希望的结果是10个类别每个类分别聚成一簇,也就是说相邻的物体具有语义上的相似性,同时对背景等噪声鲁棒。
为了得到这种理想的结果,我们需要 go beyond raw pixels.
5. 总结:
- 我们介绍了图像分类的问题,首先我们会得到一个带标签的图片的集合,根据这些图片训练好分类器,然后用于预测新的图片所属的类别,并评价分类器分类的精度。
- 我们介绍了一种简单的分类器,叫做最近邻分类器( Nearest Neighbor classifier)。我们知道了这种分类器会有一些hyperpatameters(比如说k值得选取,距离函数的选择等)需要我们进行设置,但是我们没有显而易见的方法来选择它们。
- 选择hyperparameters的正确方法是将训练数据一分为二:一个作为训练集,一个作为验证集(validation set)。我们在验证集上测试不同的hyperparameter的值,然后选取表现最优的作为我们的参数来使用。
- 如果训练数据并不多,我们可以考虑交叉验证(cross-validation)的方法,可以帮助我们减少因数据过少估计参数所导致的不准确。
- 一旦最佳的hyperparameters被找到,我们就将它们的值固定,然后再在测试集上进行评估。
- 我们发现最近邻分类器在CIFAR-10上的精度大约为40%。它的实现很简单,但是需要我们将整个训练集保存下来用于测试,这也导致测试效率很低。
- 最后我们发现在像素上运用L1或L2距离来表示图片的相似度是不恰当的,因为这个距离很大程度上图片的颜色分布或者背景有关,而和语义没啥关联。
在后面的课程中,我们会解决这里提到的精度低/计算复杂度高/效率低等问题。
延伸阅读:
- A Few Useful Things to Know about Machine Learning, 第6章相关,不过整篇文章都值得一读。
- Recognizing and Learning Object Categories,ICCV 2005关于图像分类的一个short course。