CCS231n第一节:图像分类问题 传送门 本系列文章基于CS231n课程,记录自己的学习过程,参考视频资料为 2017年版CS231n,阅读材料为CS231n官网2022年春季课程相关材料, 本文主要介绍图片分类问题以及数据驱动的方法,同时介绍了一个简单的分类器KNN。
图片分类问题就是辨认输入的图片类别的问题,且图片的类别属于事先给定的一个类别组中。尽管这看起来很简单,但这是计算机视觉的一个核心问题,且有很广泛的实际应用。并且,有很多的计算机视觉的问题最终会化简为图片分类问题。
举例来说,假设有一个图片分类模型,它对于输入的三通道的图片会预测其属于四个标签(label)的概率(四个标签为 cat, dog,hat,mug)。下图所示的图片是一张248像素宽度,400像素高度的图片,并且有RGB三通道,那么这张图片可以用 3 * 248 * 400 个数字表示,每个数字范围从 0到255,模型的任务就是接受这些数字,然后预测出这些数字代表的标签(label)。
虽然图片识别对于人来说是一件轻松的事情,但是对于计算机来说,由于接受的是一串数字,对于同一个物体,表示这个物体的数字可能会有很大的不同,所以使用算法来实现这一任务还是有很多挑战的,具体来说:
那么我们如何设计算法去分辨不同的类别呢?我们不会去设计一个特定的算法来解决这样的问题,而是将大量带有标签的数据送给一个模型,让模型自己学习,这种方式就成为数据驱动方法,因为它依赖于一个带有标签的数据集合。
所以通常图片识别任务的流水线如下:
首先我们来介绍一下最近邻域分类器,这是一个十分简单并且不常用于分类的算法,但是通过这个算法, 我们也可以大致了解解决图片分类问题的大致方法。本次使用的数据集是 CIFAR-10,这是一个有名的公开图片数据集,由60000张 3*32*32 的图片组成,一共有10个种类,一般我们将其中的50000张作为训练集,10000张作为测试集,下图左就是10个类别的部分图片。
现在我们的训练集中就有了50000张图片,每个类别5000张,对于测试集10000张图片中的每一张图片,我们要做的是将其与训练集中的每一张图片进行比较,然后将这种图片与训练集中最相似的图片归为一类,上图右就是部分分类后的结果,可以发现,存在很多的误分类,原因在于虽然图片的种类不同,但是两种图片的颜色图案等非常类似,就容易被归为一类。
那么在最近邻域算法中,我们衡量两张图片是否相近的标准是什么呢?一种最简单的标准就是 L1距离,假设我们将两张图片分别表示为两个向量 I_1,I_2,那么L1距离的定义如下:
一个简单的计算流程演示:
对于两种图片的衡量标准还有 L2 距离,定义如下:
首先我们需要处理 CIFAR-10 数据集,将其以四个数组的形式表示,分别为 训练集数据、训练集标签、测试集数据、测试集标签,下面的代码中 Xtr
表示训练集数据,Ytr
表示训练集标签,得到数据后将其拉成一条向量,便于计算。
Xtr, Ytr, Xte, Yte = load_CIFAR10('data/cifar10/')
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
print 'accuracy: %f' % ( np.mean(Yte_predict == Yte) )
其中,NearestNeighor
类的定义如下:
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 range(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
如果要是用 L2 距离,则只需要将距离计算公式改写为 distances = np.sqrt(np.sum(np.square(self.Xtr - X[i,:]), axis = 1))
。
L1和L2距离比较: 在某一维度上,如果两个点相距较远,则L2距离比L1距离更大,若两个点相距很近,则反之,这是由于平方导致的。所以L2距离对距离差异的容忍度更差。
可以注意到,前面的最近邻域算法只关注和预测图片最相近的一张训练集中的图片,不同于最近邻域算法,KNN算法会关注与预测图片最相近的 k 张图片,如果 k=1 则KNN就是最近邻域算法,这里的 k 是超参数,由我们自己设置。具体的流程是,分别计算预测样本和训练集中所有数据的距离,取前 k 个距离最相近的,应用投票法,票数最多的就当做预测种类。直观来看,从最大1个变成最大的 k 个可以使得分类器对于异常值有更好的抵抗能力。如下图所示,最左边的是原始数据,一共有三种类别,分别用三种颜色表示,中间是NN分类器,最右边是5-NN分类器,可以看到有很多空白的部分,这表示这些地方属于模糊不清的(投票结果至少有两个种类相同)
所有需要我们自行选择的,而不是由算法自学习得到的参数都是超参数,比如 KNN 中 k 的选择以及使用 L1 还是 L2 距离,这些都属于超参数。超参数对模型好坏的印影响也十分巨大,最简单的办法就是选择所有超参数的可能组合,然后逐一验证优劣,选择最好的一组超参数。但是需要注意的是,我们不能使用测试集或者训练集来选择超参数,因为会有过拟合的风险。所有我们需要一组新的数据,我们称为验证集,一般是从训练集中划分出一部分得到。测试集只能在整个训练过程的最后使用一次。
下面是选择超参数 k 的过程:
# 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))
像 CIFAR-10 这样的数据集比较大,可以很好地划分出验证集,但是当训练集很小的时候,就不容易划分出一个验证集了,那么我们可以使用交叉验证的方法。不同于直接从训练集中拿出一部分作为验证集,交叉验证通过将训练集均分为几部分,然后每次迭代地从中选取一部分作为验证集。举例来说,假设进行5折交叉验证,即将训练集平均划分为5份,对于某个待验证的超参数,我们迭代使用其中的1份作为验证集,4份作为测试集,一共进行5次准确率的计算,将5次的结果取平均作为这个超参数的准确率。如下如所示就是一个5折交叉验证的结果,横轴为 k 的选择,竖轴为准确率,可以看到每个 k 都有5个点,表示进行了5次计算,线条表示平均值的趋势。可以看到 k=7 是最好的超参数。
不过通常来说,在实践中我们会尽力避免使用交叉验证,因为使用交叉验证会造成很多的计算,造成性能浪费。一般我们会选择训练集的 50%-90%作为训练集,剩下作为验证集,具体的划分和选择由训练集大小以及需要验证的超参数数量决定。
KNN算法的最大优点就是实现和理解起来很简单,并且分类器无需训练时间,只需要将训练集存储下来,然后在预测的时候将待预测的图片与训练集中的图片进行比较。但这就导致了,KNN的预测时间与训练集的大小正相关,且预测时间很长。这是很不好的,我们希望的是,一个模型的训练时间可以比较长,但是训练结束后,预测的时间要尽量短,这样在实际应用中才比较方便。KNN模型有时候可能是一个好的选择,尤其是在数据维度很小的情况下,但是它也有很致命的问题。前面介绍了图片识别问题的挑战,受图片的大小、背景、光线等因素,同样一个物体可能在像素表示上大相径庭。如下图的例子,四张图片都是同一个人脸,但是每个位置的像素值完全不同,这也导致采用像素点间距离之和作为判别标准的KNN模型变得十分不准确。换一个角度,除了对于同一个物体可能会分辨错误,对于不同的物体KNN也可能将其归为一类,因为KNN认为这些图片在像素上是十分相近的,但是在语义上,这些图片可能完全不同。