本课程笔记是基于今年斯坦福大学Feifei Li, Andrej Karpathy & Justin Johnson联合开设的Convolutional Neural Networks for Visual Recognition课程的学习笔记。目前课程还在更新中,此学习笔记也会尽量根据课程的进度来更新。
今天的话题是:图像分类和最近邻分类器。
图像分类是指输入一张图片,让计算机从给定的众多类别中搜索出它的真实类别。例如,输入下图,输出它属于{猫,狗,帽子,杯子}四个类别中的哪个。
对于计算机而言,它看到的并不是图片,而是(寂寞…)一个三维矩阵。这个例子里,猫这张图片是248 pixel*400 pixel,并包含RGB三个颜色通道,也就是说这张图片的矩阵是248*400*3维的,总共2976000个值,每个值是0(黑)~255(白)之间的整数。我们的任务就是将这300万输入的值转变成一个简单的标签输出,比如我们想要的标签是“猫”。
对于人类来说识别物体是一件很容易的事情,然而从计算机的角度来看就不那么简单了,刚刚我们说到计算机看到的并不是一张图片,而是一大堆像素点构成的三维矩阵。另外其他面临的问题还有:
所以我们究竟要怎么做才能对图片进行分类呢?就像一个小孩学习对世界的认知一样,我们首先给计算机提供每个类的很多样例,让它去学习每个类该长什么样子。这种方法称为数据驱动的方法。顾名思义,它需要收集大量的数据用于训练,我们称为训练集(training set)。下图是一个训练集的例子,假设我们有4个类别(现实中类别数目远比4大),我们为每个类别收集一些图片,在训练时我们就告诉计算机这张照片是猫,那张照片是狗,让他去学习不同类别的特点。
我们已经知道图像分类是输入一张图片(其实是一个像素构成的矩阵),输出它对应的一个标签。它的整个流程是这样的:
我们先来学习最简单的最近邻分类器。这种分类器和卷积神经网络没有半毛钱关系,在现实生活中也不常用,但是它会帮助我们理解整个图像分类问题。
作为例子,我们会使用到的一个图像分类数据集是CIFAR-10 dataset. 这个数据集共包含60000张32*32pixel的小图片,每个图片的标签是以下10类中的1类:{飞机,汽车,鸟,猫,鹿,狗,青蛙,马,船,卡车}。这60000张图片被分成两部分,训练集包含50000张图片,测试集包含10000张图片。下图(左)是这10个类别随机的100张图片。
我们的任务是对测试集中的10000张图片进行分类。最近邻分类器的做法是:让每张测试图片和训练集中的所有图片做对比,然后以最相似的那张训练集图片的标签作为测试图片的标签。上图(右)可以看到为10张测试图片进行最近邻查找的结果,10个例子中只有3个分类正确,其他的都错了,比如第8行一个马头的最近邻是一辆车,这种误分类的原因可能是他们都有相似的黑色背景。
到现在为止,我们还没有具体说明如何比较两张图片之间的相似度。我们知道,一张图片是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):
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) )
在评估阶段我们经常会用到精度(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
return Ypred
如果在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))
其实在实际中计算L2距离时是否开方并不会对分类结果产生影响,因为开方是单调函数,它不会对距离大小的相对排序产生影响。那么如果我们用L2距离在CIFAR-10上跑一遍代码,精度为35.4%,比L1距离的结果略低。
在实际中何时使用L1距离何时使用L2距离呢?其实L2距离通常比L1使用的更加广泛,不过具体情况会有不同的preference。
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值怎么选取呢?请看下回分解->
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))
这里我们就可以得出不同的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.
有的时候可能训练集的大小有限(那么验证集也会很小),人们会采取稍微复杂一点的交叉验证(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折交叉验证的示意图。当我们训练好了模型调整好了参数以后,最后的最后再把模型用于测试集去评估模型的效果。
我们有必要来总结一下最近邻分类器的优缺点。很明显的一个优点是它非常容易理解和实现。另外,这个分类器并不需要花时间进行训练,它所做的只不过是把所有的训练数据保存起来以便索引。然而在测试的时候我们需要花费大量的时间,因为对一张测试图片的分类意味着将它和每一个训练图片作比较,这是一个很大的缺陷,在实际中我们通常更关心测试的效率而不是训练的效率。而我们即将涉及到的深度神经网络则和最近邻分类器正好相反,它在训练的时候非常耗时,而一旦训练好了,测试阶段效率会很高。这个特性是我们所希望的。
顺便说一下,如何降低最近邻分类器的计算复杂度也是研究的热门,有几种近似最近邻算法(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.