Lecture 2 图像分类
注意:课程需要使用大量Python编程并使用Numpy 完成矩阵运算,所以学习本lecture的同时需要学习Python 和Numpy。使用教程:点击这里
本课重点:
-
数据驱动方法
-
K-最近邻算法
-
线性分类I——评分函数
0 引言
图像分类是计算机视觉的核心任务,计算机视觉领域中很多问题(比如目标检测和分割),都可以被归结为图像分类问题。图像分类问题,就是已有固定的分类标签集合,然后对于输入的图像,从分类标签集合中找出一个分类标签,最后把分类标签分配给该输入图像。
挑战
计算机看到的图像是一个像素矩阵,而人类给出的标签只是一个单词,所以对计算机而言存在巨大的语义鸿沟。比如给计算机输入下图一张猫的图片,计算机图像分类模型会读取该图片,并计算该图片属于集合 {猫, 狗, 帽子, 杯子}中各个标签的概率。然而计算机读取的图像数据是一个由数字组成的巨大的3维数组。在下图中,猫的图像大小是宽248像素,高400像素,有3个颜色通道,分别是红、绿和蓝(简称RGB)。所以,该图像就包含了248X400X3=297600个数字,每个数字都是在范围0-255之间的整型,其中0表示全黑,255表示全白。我们的任务就是把这些数字变成一个简单的标签,比如“猫”。
图1 计算机“眼”中的图像图像分类算法要足够健壮(鲁棒,robust),要至少能够适应下述变化及组合:
- 视角变化(Viewpoint variation):同一个物体,摄像机可以从多个角度来展现。
- 大小变化(Scale variation):物体可视的大小通常是会变化的(不仅是在图片中,在真实世界中大小也是变化的)。
- 形变(Deformation):很多东西的形状并非一成不变,会有很大变化。
- 遮挡(Occlusion):目标物体可能被挡住。有时候只有物体的一小部分(可以小到几个像素)是可见的。
- 光照条件(Illumination conditions):在像素层面上,光照的影响非常大。
- 背景干扰(Background clutter):物体可能混入背景之中,使之难以被辨认。
- 类内差异(Intra-class variation):一类物体的个体之间的外形差异很大,比如椅子。这一类物体有许多不同的对象,每个都有自己的外形。
1 数据驱动方法
如果采用硬编码的方法编写一个识别猫的算法,可以输入猫的图片输出标签“猫”,可以先获取猫图像的边缘得到一些线条,然后定义规则比如三条线交叉是耳朵之类。然而这种方式的识别效果不好,并且不能识别新的物体。
所以可以采用数据驱动算法。即不具体写出识别每个物体对应的规则,而是对每一类物体标签给计算机大量的图片,采用机器学习的方法,计算机会收集所有的数据,用某种方式总结,然后会生成一个分类器模型,总结出区分不同类物体的核心知识要素,然后用训练好的模型,识别新的图像。过程如下:
- 输入:输入是包含N个图像的集合,每个图像的标签是K种分类标签中的一种。这个集合称为训练集。
- 学习:这一步的任务是使用训练集来学习每个类到底长什么样。一般该步骤叫做训练分类器或者学习一个模型。
- 评价:让分类器来预测它未曾见过的图像的分类标签,把分类器预测的标签和图像真正的分类标签 (基本事实) 对比,并以此来评价分类器的质量。
最邻近算法(Nearest Neighbor )
第一个分类器算法。训练过程只是简单的记住图像数据和标签,预测的时候和训练数据中图片比较找出最接近的输出标签。这个分类器和卷积神经网络没有任何关系,实际中也极少使用,但通过实现它,可以对解决图像分类问题的方法有个基本认识。
图像分类数据集:CIFAR-10一个非常流行的图像分类数据集。这个数据集包含10种分类标签,60000张32X32的小图像,每张图片含有一个标签。这60000张图像被分为包含50000张(每种分类5000张)图像的训练集和包含10000张图像的测试集。假设现在我们用这50000张图片作为训练集,将余下的10000作为测试集并打上标签,Nearest Neighbor算法将会拿着测试图片和训练集中每一张图片去比较,然后将它认为最相似的那个训练集图片的标签赋给这张测试图片。结果如下图所示,效果并不是特别好。
图3 左边:CIFAR-10数据库的样本图像;右边:第一列是测试图像,后面是使用Nearest Neighbor算法,根据像素差异,从训练集中选出的10张最类似的图片那么具体如何比较两张图片呢?
L1距离(曼哈顿距离)
在本例中,就是比较32x32x3的像素块。最简单的方法就是逐个像素比较,最后将差异值全部加起来。即将两张图片先转化为两个向量 和,然后计算他们的L1距离:
,其中为像素点,表示第个像素点的值。
两张图片使用L1距离来进行比较,即逐个像素求差值,然后将所有差值加起来得到一个数值。如果两张图片一模一样,那么L1距离为0,但是如果两张图片很是不同,那L1值将会非常大。下图是仅一个RGB通道的4X4图片计算L1距离。
首先,我们将CIFAR-10的数据加载到内存中,并分成4个数组:训练数据和标签,测试数据和标签。在下面的代码中,Xtr(大小是50000x32x32x3)存有训练集中所有的图像,Xte(大小是10000x3072)存有测试集中所有的图像,Ytr 是对应的长度为50000的1维数组,存有图像对应的分类标签(从0到9),Yte 对应长度为10000的1维数组:
Xtr, Ytr, Xte, Yte = load_CIFAR10('data/cifar10/') # 这个函数可以加载CIFAR10的数据
# Xtr是一个50000x32x32x3的数组,一共50000个数据,
# 每条数据都是32行32列的数组,数组每个元素都是一个三维数组,表示RGB。
# Xte是一个10000x32x32x3的数组;
# Ytr是一个长度为50000的一维数组,Yte是一个长度为10000的一维数组。
Xtr_rows = Xtr.reshape(Xtr.shape[0], 32 * 32 * 3)
# Xtr_rows是50000x3072的数组,按每个像素点排列,每个像素点有三个值。
Xte_rows = Xte.reshape(Xte.shape[0], 32 * 32 * 3)
# Xte_rows是10000x3072的数组
''' shape会返回数组的行和列数元组:(行数,列数),shape[0]表示行数,
Xtr.shape[0]会返回50000;Xtr.shape会返回(50000,32,32,3)
Xtr.reshape(50000,3072)会将Xtr 重构成50000x3072数组,等于 np.reshape(Xtr, (50000,3072))'''
现在我们得到所有的图像数据,每张图片对应一个长度为3072的行向量。接下来展示如何训练并评价一个分类器:
nn = NearestNeighbor() # 创建一个最邻近分类器对象
nn.train(Xtr_rows, Ytr) # 用训练图片数据和标签训练分类器
Yte_predict = nn.predict(Xte_rows) # 预测测试图片的标签
# 并输出预测准确率,是一个平均值
print 'accuracy: %f' % ( np.mean(Yte_predict == Yte) )
我们常常使用准确率作为评价标准,它描述了我们预测正确的得分。请注意以后我们实现的所有分类器都需要有这个接口函数(API):train(X, y) 函数。该函数使用训练集的数据和标签来进行训练。从其内部来看,类应该实现一些关于标签和标签如何被预测的模型。这里还有个predict(X) 函数,它的作用是预测输入的新数据的分类标签。下面就是使用L1距离的Nearest Neighbor分类器的实现:
import numpy as np
class NearestNeighbor(object):
def __init__(self):
pass
def train(self, X, y):
""" X 是 NxD 维的数组,每一行都是一个样本,比如一张图片,D 是样本的数据维度;
Y 是长度为 N 的一维数组。 """
# 最邻近分类器只是简单的记住所有的训练数据
self.Xtr = X
self.ytr = y
def predict(self, X):
""" X 是 NxD 维的数组,每一行都是一个希望预测其标签的样本 """
num_test = X.shape[0]
# 确保输出的标签数据类型和输入的标签格式一致,长度是测试样本数
Ypred = np.zeros(num_test, dtype = self.ytr.dtype)
# 循环所有测试样本数,即测试数组的行数
for i in range(num_test):
# 为第 i 张测试图片找到最接近的训练图片
# 使用 L1 距离 (差值的绝对值求和)
'''self.Xtr - X[i,:] 利用传播机制,求测试集第 i 张图片对应的行向量和
训练集所有图片行向量的差值,得到一个一个50000x3072的差值矩阵;
abs(self.Xtr - X[i,:] )会将矩阵所有元素求绝对值;
然后axis = 1 会对差值矩阵按行求和,最终得到一个长度为50000的一维
数组,存放第 i 张图片和训练集所有50000张图片的L1距离。'''
distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
min_index = np.argmin(distances) # 获取距离最小的训练集图片索引
Ypred[i] = self.ytr[min_index] # 预测第 i 张测试集图片的标签时与其最接近的训练集图片索引
return Ypred
这段代码的训练时间复杂度为 O(1),因为只是简单的存储数据,不管数据多大,都是一个相对固定的时间;如果训练集有N个样本,则预测时间复杂度为 O(N),因为测试图片要和训练集每张图片进行比较。所以这是一个不好的分类器,分类器预测的时候要快,训练的时候可以慢。
这段代码跑CIFAR-10,准确率能达到38.6%。这比随机猜测的10%要好,但是比人类识别的水平和卷积神经网络能达到的95%还是差很多。(Kaggle算法竞赛排行榜)
L2距离(欧式距离)
另一个常用的方法是L2距离,从几何学的角度,可以理解为它在计算两个向量间的欧式距离。L2距离的公式如下:
依旧是在计算像素间的差值,只是先求差值的平方,然后把这些平方全部加起来,最后对这个和开方。此时的代码只需改动一行:
distances = np.sqrt(np.sum(np.square(self.Xtr - X[i,:]), axis = 1))
'''np.square(self.Xtr - X[i,:]) 会对差值矩阵的每一个元素求平方'''
注意在这里使用了np.sqrt,但是在实际中可能不用。因为对不同距离的绝对值求平方根虽然改变了数值大小,但依然保持了不同距离大小的顺序。这个模型,正确率是35.4%,比刚才低了一点。
L1和L2比较
在 L1 距离更依赖于坐标轴的选定,坐标轴选择不同L1距离也会跟着变化,判定的数据归类的边界会更趋向于贴近坐标系的轴来分割所属区域,而 L2 的话相对来说于坐标系的关联度没那么大,会形成一个圆,不跟随坐标轴变化;在面对两个向量之间的差异时,L2比L1更加不能容忍这些差异。也就是说,相对于1个巨大的差异,L2距离更倾向于接受多个中等程度的差异(因为会把差值平方)。L1和L2都是在p-norm常用的特殊形式。
当图像中有特别在意的特征时可以选择L1距离;当对图像中所有元素未知时,L2距离会更自然一些。最好的方式是两种距离都尝试,然后找出最好的那一个。
2 k-最近邻分类器(k-Nearest Neighbor Classifier)
只用最相似的1张图片的标签来作为测试图像的标签很不合理,可以使用k-Nearest Neighbor分类器。它的思想是:找最相似的k个图片的标签,k中数量最多的标签作为对测试图片的预测。当k=1的时候,k-Nearest Neighbor分类器就是上面所说的最邻近分类器。从直观感受上就可以看到,更高的k值可以让分类的效果更平滑,使得分类器对于异常值更有抵抗力。
2.1 超参数(hyperparameter)调优
k-NN分类器需要设定k值,如何选择k值最合适?L1距离和L2距离选哪个比较好?还有不少选择我们甚至都没有考虑到(比如:点积)。所有这些选择,被称为超参数。在基于数据进行学习的机器学习算法设计中,超参数是很常见的。
一般说来,这些超参数只能在外部设置,不能自动学习,具体怎么设置或取值并不是显而易见的,需要尝试不同的值,看哪个值表现最好就选哪个。(特别注意:决不能使用测试集来进行调优。如果使用测试集来调优,而且算法看起来效果不错,真正的危险在于:算法实际部署后,性能可能会远低于预期。这种情况,称之为算法对测试集过拟合。从另一个角度来说,如果使用测试集来调优,实际上就是把测试集当做训练集,由测试集训练出来的算法再预测测试集,性能自然会看起来很好,但实际部署起来效果就会差很多。所以,最终测试的时候再使用测试集,可以很好地近似度量分类器的泛化性能。)
测试数据集只能使用一次,而且是在训练完成后评价最终模型时使用,不可用来调优!
方法1:设置验证集
从训练集中取出一部分数据用来调优,称之为验证集(validation set)。以CIFAR-10为例,可以用49000个图像作为训练集,用1000个图像作为验证集。验证集其实就是作为假的测试集来调优。代码如下:
# 假设 Xtr_rows, Ytr, Xte_rows, Yte 还是和之前一样
# Xtr_rows 是 50,000 x 3072 的矩阵
Xval_rows = Xtr_rows[:1000, :] # 取前 1000 个训练集样本作为验证集
Yval = Ytr[:1000]
Xtr_rows = Xtr_rows[1000:, :] # 剩下的 49,000 个作为训练集
Ytr = Ytr[1000:]
# 找出在验证集表现最好的超参数 k
validation_accuracies = []
for k in [1, 3, 5, 10, 20, 50, 100]:
# 使用一个明确的 k 值评估验证集
nn = NearestNeighbor()
nn.train(Xtr_rows, Ytr)
# 这里假设一个修正过的 NearestNeighbor 类,可以把 k 值作为参数输入
Yval_predict = nn.predict(Xval_rows, k = k)
acc = np.mean(Yval_predict == Yval)
print 'accuracy: %f' % (acc,)
# 把每个 k 值和相应的准确率保存起来
validation_accuracies.append((k, acc))
程序结束后,作图分析出哪个k值表现最好,然后用这个k值来跑真正的测试集,并作出对算法的评价。
方法2:交叉验证
训练集数量较小(因此验证集的数量更小)时,可以使用交叉验证的方法。还是用刚才的例子,如果是交叉验证集,我们就不是取1000个图像,而是将训练集平均分成5份,每份10000张图片 ,其中4份用来训练,1份用来验证。然后我们循环着取其中4份来训练,其中1份来验证,最后取所有5次验证结果的平均值作为算法验证结果。
实际情况下,深度学习不会使用交叉验证,主要是因为它会耗费较多的计算资源。一般直接把训练集按照50%-90%的比例分成训练集和验证集。但是训练集数量不多时可以使用交叉验证,一般都是分成3、5和10份。
2.2 k-Nearest Neighbor分类器的优劣
优点:
易于理解,实现简单;算法的训练不需要花时间,因为其训练过程只是将训练集数据存储起来。
缺点:
1、测试要花费大量时间,因为每个测试图像需要和所有存储的训练图像进行比较在实际应用中,关注测试效率远远高于训练效率;
2、使用像素差异来比较图像是不够的,图片间L2距离小,更多的是被背景主导而不是图片语义内容本身主导,往往背景相似图片的L2距离就会小。也就是说,在高维度数据上,基于像素的相似和基于感官上的相似非常不同。感官上不同的两张图片,可能有相同的L2距离。
3、维度灾难。k-NN 有点像训练数据把样本空间分成几块,我们需要训练数据密集的分布在样本空间里,否则测试图片的最邻近点可能实际距离会非常远,导致和最接近的训练集样本实际上完全不同。但是如果使训练数据密集分布,需要的训练集数量指数倍增加,是数据维度的平方。
2.3 实际应用k-NN
1. 预处理数据:对数据中的特征进行归一化(normalize),让其具有零均值(zero mean)和单位方差(unit variance)。本小节不讨论,是因为图像中的像素都是同质的,不会表现出较大的差异分布,不需要标准化处理。
2. 降维:如果数据是高维数据,考虑使用降维方法,比如PCA或者随机投影。
3. 将数据随机分入训练集和验证集:一般规律,70%-90% 数据作为训练集。这个比例根据算法中有多少超参数,以及这些超参数对于算法的预期影响来决定。如果需要预测的超参数很多,那么就应该使用更大的验证集来有效地估计它们;如果担心验证集数量不够,那么就尝试交叉验证方法;如果计算资源足够,使用交叉验证更好(份数越多,效果越好,也更耗费计算资源)。
4. 在验证集上调优:尝试足够多的k值,尝试L1和L2两种范数计算方式。
5. 加速分类器:如果分类器跑得太慢,尝试使用ANN库(比如FLANN来加速这个过程,其代价是降低一些准确率。
6. 对最优的超参数做记录:记录最优参数后,不要使用最优参数的算法在完整的训练集上运行并再次训练,这样做会破坏对于最优参数的估计。直接使用测试集来测试用最优参数设置好的最优模型,得到测试集数据的分类准确率,并以此作为你的k-NN分类器在该数据上的性能表现。
3 线性分类I——评分函数
3.1 线性分类概述
k-NN 模型中训练过程中没有使用任何参数,只是单纯的把训练数据存储起来(参数 k 是在预测中使用的,找出 k 个接近的图片,然后找出标签最多的,并且 k 是超参数,是人为设定的。)。与之相对的是参数模型,参数模型往往会在训练完成后得到一组参数,之后就可以完全扔掉训练数据,预测的时候只需和这组参数做某种运算,即可根据运算结果做出判断。线性分类器是参数模型里最简单的一种,但却是神经网络里很重要的基础模块。
线性分类的方法由两部分组成,一个是评分函数(score function),它是原始图像数据到类别分值的映射。另一个是损失函数(loss function),它用来量化评分函数计算的分数与真实标签之间的一致性。该方法可转化为一个最优化问题,在最优化过程中,通过更新评分函数的参数来最小化损失函数值。
评分函数:从图像到类别分值的参数化映射
评分函数将图像的像素值映射为各个分类类别的得分,得分高低代表图像属于该类别的可能性高低。上面的所有说明都比较抽象,下面以具体的例子说明。
重新回到 k-NN 使用的CIFAR-10图像分类数据集。假设我们的训练集有 N 个样本,这里 N=50000,每个样本,其中 i = 1,2,..N,这里的 D=3072;每个对应着一个标签,在[1, K]上取值,K 表示总分类数,这里 K=10。现在可以定义评分函数:,即把一个D 维的图像映射为K 个类别的分数。
直接能想到的一个模型就是参数和输入数据相乘的形式。比如:
,其中参数被称为权重,被称为偏差向量
在上面的公式中,假设每个图像数据都被拉长为一个长度为D的列向量,大小为[D x 1]。其中大小为 [K x D] 的矩阵和大小为 [K x 1] 的列向量为该函数的参数(parameters)。还是以CIFAR-10为例,就包含了第 i 个图像的所有像素信息,这些信息被拉成为一个[3072 x 1]的列向量,大小为[10x3072],的大小为[10x1]。因此,输入3072个数字(原始像素数值),函数输出10个数字(不同分类得到的分值),是一个3072维到10维的映射。
注意:
- 常常混用权重(weights)和参数(parameters)这两个术语,实际上数据和参数相乘,就相当于数据占的比重,这个权重就是参数值;
- 该方法的一个优势是训练数据是用来学习参数和的,一旦训练完成,训练数据就可以丢弃,留下学习到的参数即可。当测试图像时可以简单地把图像数据输入给函数,函数计算出的分类分值来进行分类;
- 输入数据是给定且不可改变的,但参数和是可改变的。目标就是通过改变这些参数,使得计算出来的分类分值情况和训练集中图像数据的真实类别标签相符;
- 只需一个矩阵乘法和一个矩阵加法就能对一个测试数据分类,这比k-NN中将测试图像和所有训练数据做比较的方法快多了。
3.2 理解线性分类器
理解一:W是所有分类器的组合
图8 实际计算评分函数如上图所示,将小猫的图像像素数据拉伸成一个列向量xi,这里为方便说明,假设图像只有4个像素(都是黑白像素,不考虑RGB通道),即 D=4;有3个分类(红色代表猫,绿色代表狗,蓝色代表船,颜色仅代表分类,和RGB通道没有关系),即 K=3。W 矩阵乘列向量xi,得到各个分类的分值。实际上,我们可以看到,参数矩阵 W 相当于是三个分类器的组合,W 的每一行都是一个分类器,分别对应猫、狗、船。每个分类器的参数个数等于图像数据的维度,每个像素和对应的参数相乘,就表示该像素在该分类器中应占的比重。需要注意的是,这个 W 一点也不好:猫分类的分值非常低。从上图来看,算法倒是觉得这个图像是一只狗。
抽象一点的说法,线性分类器会计算图像中3个颜色通道中所有像素的值与权重矩阵的乘积,从而得到分类分值。根据我们对权重设置的值,对于图像中的某些位置的某些颜色,函数表现出喜好或者厌恶(根据每个权重的符号而定)。举个例子,可以想象“船”分类就是被大量的蓝色所包围(对应的就是水)。那么“船”分类器在蓝色通道上的权重就有很多的正权重(它们的出现提高了“船”分类的分值),而在绿色和红色通道上的权重为负的就比较多(它们的出现降低了“船”分类的分值)。从上面的小猫示例也能看出,猫分类器对第二个位置的像素比较“厌恶”,而恰好输入的小猫图像第二个位置像素值很大,从而导致得到一个很低的分数。(当然,这个分类器是错误的。)
理解二:将线性分类器看做模板匹配
把权重W的每一行看作一个分类的模板,一张图像对应不同分类的得分,是通过使用内积(也叫点积)来比较图像和模板,然后找到和哪个模板最相似。从这个角度来看,线性分类器就是在利用学习到的模板,针对图像做模板匹配。从另一个角度来看,可以认为还是在高效地使用k-NN,不同的是我们没有使用所有的训练集的图像来比较,而是每个类别只用了一张图片(这张图片是我们学习到的,而不是训练集中的某一张),而且我们会使用(负)内积来计算向量间的距离,而不是使用L1或者L2距离。
理解三:将图像看做高维度的点
既然定义每个分类类别的分值是权重和图像的矩阵乘积,那么每个分类类别的分数就是这个空间中的一个线性函数的函数值。我们没办法可视化3072维空间中的线性函数,但假设把这些维度挤压到二维,那么就可以看看这些分类器在做什么了:
图10 二维空间示意图
其中每个图像是一个点,有3个分类器。以红色的汽车分类器为例,红线表示空间中汽车分类分数为0的点的集合,红色的箭头表示分值上升的方向。所有红线右边的点的分数值均为正,且线性升高。红线左边的点分值为负,且线性降低。
从上面可以看到,W的每一行都是一个分类类别的分类器。对于这些数字的几何解释是:如果改变其中一行的数字,会看见分类器在空间中对应的直线开始向着不同方向旋转。而偏差b,则允许分类器对应的直线平移。需要注意的是,如果没有偏差,无论权重如何,在时分类分值始终为0。这样所有分类器的线都不得不穿过原点。
3.3 偏差和权重合并
分开处理这两个参数(权重参数和偏差参数)有点笨拙,一般常用的方法是把两个参数放到同一个矩阵中,同时列向量就要增加一个维度,这个维度的数值是常量1,这就是默认的偏差维度。这样新的公式就简化成下面这样:
还是以CIFAR-10为例,那么的大小就变成[3073x1],而不是 [3072x1] 了,多出了包含常量1的1个维度;大小就是[10x3073]了,中多出来的这一列对应的就是偏差值,具体见下图:
图11 W和b合并后的示意图通过右边这样做,就只需要学习一个权重矩阵,而不用去学习两个分别装着权重和偏差的矩阵。
3.4 图像数据预处理
在上面的例子中,所有图像都是使用的原始像素值(从0到255)。在机器学习中,对于输入的特征做归一化(normalization)处理是常见的方式。而在图像分类的例子中,图像上的每个像素可以看做一个特征。在实践中,对每个特征减去平均值来中心化数据是非常重要的。在这些图片的例子中,该步骤是根据训练集中所有的图像计算出一个平均图像值,然后每个图像都减去这个平均值,这样图像的像素值就大约分布在[-127, 127]之间了。下一个常见步骤是,让所有数值分布的区间变为[-1, 1]。
3.5 线性分类器难以处理的情形
图12 线性分类器难以处理的情形这三种情形都无法找到合适的直线区分开。case1是奇偶分类,case3是有多个模型。
总结
- 图像分类中的困难与挑战;
- 数据驱动方法、最邻近算法、L1和L2距离;
- k-NN分类器、超参数调优、k-NN的优缺点与实际应用;
- 线性分类的概念、评分函数的理解、参数合并、数据预处理。