深度学习 | 斯坦福cs231n第一讲学习笔记 --- Image Classification

课程主页

课程视频

课程PPT

斯坦福cs231n学习笔记系列博客基于cs231n 2017年春季版,课程主要内容关于视觉识别和深度学习,主要介绍卷积神经网络。

第一讲学习笔记主要介绍图像分类问题和数据驱动方法,以及使用k近邻算法进行图像分类的一些细节。

目录

1.图像分类介绍

2.最近邻分类器(1-NN)

3.K近邻分类器

4.使用验证集进行超参数调试/模型选择

5.交叉验证

6.最近邻分类器/k近邻的优缺点

7.总结

8.实际应用kNN总结

9.拓展阅读


1.图像分类介绍

  • Motivation

在本小节中,我们将介绍图像分类问题,该问题的任务是从一组固定的类别中为输入图像分配一个标签。这是计算机视觉中的核心问题之一,尽管非常简单,但他具有各种各样的实际应用。此外,正如我们将在后续的课程中看到的那样,许多其他看似不同的计算机视觉任务(如物体/目标检测,图像分割等)都可以简化为图像分类。

  • 例子

下图中,图像分类模型以图像为输入,进行4分类,输出是对应4个标签{cat,dog,hat,mug}的概率,概率最大的那个标签即为输入图像的标签。如图所示,对于计算机来说,图像表示为一个大的3维数组。在下例中,猫图像是248像素宽,400像素高,并且具有三个颜色通道红、绿、蓝(或简称RGB).因此图像由248*400*3个数字(像素/亮度值)组成,或总共297,600个数字。每个数字是一个整数,范围从0(黑色)到255(白色).我们的任务是将这25万个数字转换为单个标签,如"Cat"。

图像分类的任务是对于一张给定的图像预测一个标签/类别(或在标签上的概率分布来表示置信度).图像是一个3维数组,包含一些取值为0-255的整数,维度width*hight*3.  3代表RGB三个颜色通道(彩色图,灰度图没有一个颜色通道)。

  • 挑战

由于视觉识别(如识别一只猫)这项任务对于人来说相对微不足道,因此从计算机视觉算法的角度考虑所涉及的挑战是值得的。正如我们在下面提出的这个挑战列表(但不仅限于此),记住图像的原始表示/原始特征(raw features)是作为亮度值的三维数组:

拍摄角度问题:物体的单个实例可以相对于相机以多种方式定向。

规模变化:视觉类通常表现出其大小的变化(现实世界中的大小,不仅仅是他们在图像中的范围).

形变:许多物体不是刚体,可以以极端的方式变型:

遮挡:我们感兴趣的对象/物体可以被遮挡或部分遮挡,有时只能看到物体的一小部分(少至几个像素)。

背景杂乱:我们感兴趣的物体可能融入背景中,使其难以识别:

类别变异:感兴趣的类别通常可以相对宽泛,比如椅子。这些对象有很多不同的类型,每个对象都有自己的外观:

良好的图像分类模型必须对所有这些变化的向量积(cross product)不变,同时保持类间变化的敏感性。

  • 数据驱动方法

我们如何编写可以将图像分类为不同类别的算法?与编写算法(如,对数字列表排序)不同,编写用于识别图像中的猫的算法或规则很难定义、很不明显。因此,我们不会尝试直接在代码中指定识别某个我们感兴趣类别的规则,而是采用和孩子一样的方法:我们为计算机提供各个类别的许多示例,然后开发学习算法,从这些示例中进行学习,找到这些示例中的规律,让计算机可以学到每个类别的视觉外观,具有图像分类的能力,从而能对图像进行分类;就像在教一个小孩子识别物体一样,我们告诉他这是猫,那是狗...,然后他会对这些物体产生印象,学习到他们的特征,下次再见到猫,他就能认出来了。这种方法被称为数据驱动方法,因为他依赖于带标记的图像训练集。下面是这种训练集的示例:

上图是一个标记图像训练集的示例,只有四个类别。实际上我们的训练集一般有几千种类别,每个类别都有几十万的图像。

  • 图像分类流水线

如之前看到的那样,图像分类任务采用表示单个图像的像素数组并为其指定标签。正式化流程定义如下:

输入:输入是一个包含N张图像并且每张图像对应K个类别中的一个标签,我们称之为训练集。

学习:我们的任务是使用训练集来学习每个类别长什么样。我们把这一步称为训练一个分类器或学习一个模型。

验证:最后,我们通过要求它(训练好的模型/分类器)来预测一组从未见过的新图像的标签来评估分类器的质量。然后,我们比较这些图像的真实标签和分类器预测的标签。直观的说,我们希望很多预测与真正的答案(ground truth)相匹配。

 

2.最近邻分类器(1-NN)

作为我们使用的第一个方法,我们将使用一个名为最近邻分类器的算法。该分类器和我们之后要学习的卷积神经网络无关,在实践中也很少使用,但他可以让我们了解图像分类问题的基本方法。

  • CIFAR-10图像分类数据集

一个流行的图像分类数据集是CIFAR-10,这个数据集包含60000张小图像,宽和高都是32像素。每张图像被标有10个类别中的一个(如飞机,汽车,鸟等).这60000张图像被划分为50000张训练集和10000张测试集。下图是10个类别中每个类随机取10张图像的示例:

左图是CIFAR-10的示例图像。右图:第一列是一些测试图像,他们之后的每一行代表在训练集中与他们最相近的10张图像(基于kNN算法,根据像素方面的差异).

 

假设我们现在获得了CIFAR-10训练集中的50000张图像(每张图像对应10个类别中的一个),我们希望标记测试集中的10000张图像。最近邻算法将把测试集中的每张图像与训练集中的所有图像一一进行比较,并把与它最接近的训练图像的标签作为该测试图像的标签(k近邻算法是找到与测试图像最接近的k张训练图像,对着k张训练图像进行投票,这k张图像中哪个标签出现的最多,把它作为该测试图像的标签,最近邻是k近邻的特殊情况,当k=1时)。在上面的右图中,我们可以看到10张测试图像的上述过程产生的示例结果。但是,在这10个测试图像中只有三个检索到了同一类图像(预测正确),其余都预测错了。例如第8行中,最靠近马头的训练图像是红色的汽车,可能由于强烈的黑色背景。在这种情况下,这种马的图像被错误的标记为汽车)。

我们并没有提到如何比较两张图像的细节。最简单的做法是逐一比较两张图像的每个像素并将差异相加。换句话说,给定两张图像将他们表示为矢量l_{1},l_{2}(32*32*3),一个合理的选择是使用L1 距离:

下面是使用L1距离比较连两个矢量/图像的简单示例:

上例中的图像只有一个颜色通道,实际上我们的图像是彩色图有3个颜色通道,运算过程是一样的,比较每个对应位置像素值的差异并求和。如果两个图像相同,则结果为0,但如果图像非常不同,结果会很大。

 

接下来,看一下如何编程实现这个分类器,首先加载CIFAR-10数据存储在一个四维数组中:训练集数据/标签以及测试集数据/标签。在下例中,Xtr(50000,32,32,3)包含训练集中的所有图像,并有一个对应的一维数组Ytr(50000,)表示训练集中每张图像的标签(值为0-9).

Xtr, Ytr, Xte, Yte = load_CIFAR10('data/cifar10/') 
# 把所有图像拉伸为一维。(32,32,3) -> (32*32*3)
Xtr_rows = Xtr.reshape(Xtr.shape[0], 32 * 32 * 3) # Xtr_rows  50000 x 3072
Xte_rows = Xte.reshape(Xte.shape[0], 32 * 32 * 3) # Xte_rows  10000 x 3072

现在所有的图像都被拉伸为一行,集Xtr_rows中的每一行代表一张图像,下面我们将训练和验证/测试我们分类器:

nn = NearestNeighbor() # 实例化最近邻(1-NN)分类器的对象
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),它接受新数据并用训练好的模型来预测标签,以下是一个简单的最近邻实现:

import numpy as np

class NearestNeighbor(object):
  def __init__(self):
    pass

  def train(self, X, y):
    """ X(N,D) 每行代表一个样本/图像. Y 是一个一维数组 大小为N """
    # 最近邻分类器的训练过程 就是简单记住所有训练样本
    self.Xtr = X
    self.ytr = y

  def predict(self, X):
    """ X(N,D) 每行代表一个样本/图像  我们需要给这些样本预测一个标签 """
    num_test = X.shape[0]
   
    Ypred = np.zeros(num_test, dtype = self.ytr.dtype)

    # 遍历所有测试样本
    for i in range(num_test):
      # 为第i个测试样本找到离他最近的训练样本
      # 使用L1距离 对应像素值差的绝对值再求和
      distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
      min_index = np.argmin(distances) #得到最近的训练样本的索引
      Ypred[i] = self.ytr[min_index] # 得到最近的训练样本的标签

    return Ypred

运行上述代码,在CIFAR-10上仅有38.6%的准确率,这比随机猜测好一些(因为有10个类别随机猜测的话会有10%的准确率),但是远远没有达到人类的表现(大约94%)或接近最先进的卷积神经网络的95%。(参考最近的CIFAR-10 kaggle竞赛的排行榜 leaderboard)

 

其他距离度量方式:有很多计算两个向量距离的方式,另一个常用的选择是L2 距离,它具有计算两个向量之间欧氏距离的几何解释:

和L1距离类似,他计算每个像素值的差异,但是这次不是取绝对值而是取平方,求和之后再开方。在NumPy中可以使用下面的代码:

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

注意,我们上面包含了np.sqrt的调用,但是实际的最近邻应用程序中,我们可以省略平方根操作,因为平方根是单调函数。也就是说,它缩放距离的绝对大小,但他保留了排序,因此最近的邻居有或没有开方运算都是一样的。如果使用L2 距离运行最近邻分类器,将获得35.4%的准确度(比L1 略低)

 

L1 vs. L2: 考虑两个指标之间的差异很有意思。特别是,当涉及两个矢量之间的差异时,L2通常选择多个平均的、中等大小的变量,而L1倾向于选择一个大的变量,而将其他变量趋于0(更稀疏)。这个点在后面的regulation中还会细讲,这里记住这个例子就可以了:

L2: [0.25, 0.25, 0.25, 0.25]
L1: [1, 0, 0, 0]

L1和L2距离是p范数中常用的特殊情况。

 

3.K近邻分类器

在我们进行预测时,仅仅使用最近图像的标签很奇怪。实际上,通过所谓的k近邻分类器,可以做的更好。想法非常简单:我们不是在训练集中找到最近的单个图像,而是找到最前面的k个最近的图像,并让他们在测试图像的标签上进行投票。特别的,当k=1时,为最近邻分类器。直观来讲,较高的k值具有平滑效果,使分类器对异常值更具有抵抗力:

 

最近邻(k=1)和k近邻(k=5)分类器之间差异的一个示例,我们使用2位数据(方便可视化,每个样本的特征向量是2维的,每个样本可以表示为2维平面上的一个点;在图像分类的例子中每个样本/图像的特征向量上千维,每张图像可以表示为千维空间中的一个点。算法都是针对向量进行计算的,2维和几千维一样运算,代码都是通用的,只是时间差异),有3个不同的类别,用不同的颜色表示。有色区域显示由使用L2距离的分类器引起的决策边界,白色区域显示模糊分类的点(即,类别投票与至少两个类别相关(在最近的k个训练样本中,出现频次最多的类别>=2)。注意在最近邻分类器情况下,异常数据点(如蓝色区域左下方有一个绿点)会产生可能不正确预测的绿色小区域,而5-NN分类器会平滑这些不规则性,可能会使模型具有更好的泛化能力,在测试集上准确率更高。

实际使用中,我们通常使用k近邻算法。但是如何选择一个最好的k值呢?

4.使用验证集进行超参数调试/模型选择

k近邻分类器需要k的设置。 但是哪个数字效果最好? 另外,我们看到我们可以使用许多不同的距离函数:L1范数,L2范数,还有许多我们甚至没有考虑的其他选择(例如点积)。 这些选择被称为超参数(控制模型参数的参数),它们经常出现在许多从数据中学习的机器学习算法的设计中。 人们应该选择哪些值/设置通常并不明显。

您可能会建议我们尝试使用许多不同的值,看看哪种方法效果最好。这是一个好主意,这确实是我们将要做的,但必须非常谨慎地完成。特别是,我们不能使用测试集来调整超参数。无论何时设计机器学习算法,您都应该将测试集视为一种非常宝贵的资源,理想情况下,直到最后一次才能触及(我们使用验证集调试超参数或进行模型选择,选择一组最好的超参数设置或一个最好的模型,最后用这个最好的模型在测试集上进行测试,得到模型最后的评定)。否则,真正的危险在于您在测试集上调整超参数使其在测试集上工作的很好,但如果您要部署/实际应用该模型,则可能会发现性能显著降低。在实践中,我们会说你在测试集上过拟合。另一种看待它的方法是,如果你在测试集上调整超参数,你就可以有效地使用测试集作为训练集,因此在实际部署/应用模型时,你在其上实现的性能对于你实际观察到的内容会过于乐观(模型泛化能力可能会很差)。但是如果你最后只使用一次测试集,那么它仍然是衡量分类器泛化的一个很好的代理(我们将在后面的课程中看到更多关于泛化的讨论)。

我们使用验证集调试超参数或进行模型选择,选择一组最好的超参数设置或一个最好的模型,最后用这个最好的模型在测试集上进行测试,得到模型最后的评定(测试集仅仅在最后进行一次验证)

幸运的是,有一种调整超参数的正确方法,它根本不会触及测试集。 我们的想法是将我们的原始训练集分成两部分:一个稍小的训练集,以及我们称之为验证集。 以CIFAR-10为例,我们可以使用49,000个训练图像进行训练,并留出1,000个进行验证。 该验证集主要用作伪测试集来调整超参数/进行模型选择。

# 假设有了之前的 Xtr_rows, Ytr, Xte_rows, Yte 
# Xtr_rows 50,000 x 3072 
Xval_rows = Xtr_rows[:1000, :] # 取前1000行/前1000个样本作为验证集
Yval = Ytr[:1000]
Xtr_rows = Xtr_rows[1000:, :] # 后490000个样本用于训练
Ytr = Ytr[1000:]

# 找到在验证集上效果最好的超参数
validation_accuracies = []
for k in [1, 3, 5, 10, 20, 50, 100]:
  
  #使用一个特定的k值在验证集上进行验证
  nn = NearestNeighbor()
  nn.train(Xtr_rows, Ytr)
  #假设现在有一个修正的k近邻分类器,有一个输入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值最佳。 然后我们将坚持使用此值并在实际测试集上进行一次评估,作为模型最后的表现。

将训练集拆分为训练集和验证集。 使用验证集调整所有超参数/进行模型选择。 最后在测试集上运行一次并报告性能。

5.交叉验证

如果您的训练数据的大小可能很小,人们有时会使用更复杂的技术进行超参数调整,称为交叉验证(在深度学习中我们一般不使用交叉验证,因为数据集很大而且训练速度比较慢)。 使用我们之前的示例,我们的想法是,不是任意选择前1000个数据点作为验证集,而是通过迭代来获得更好且噪声更小的k估计值。 验证集平衡这些性能。 例如,在5倍交叉验证中,我们将训练数据分成5等份,其中4份用于训练,1份用于验证。 然后进行迭代,每一份都做一次验证集,其余四份做训练集。对于每个超参数取值,都要训练5次,用验证集做评估,得到5个评估指标,最后平均5次的性能。

上图是超参数k的5倍交叉验证运行的示例。 对于k的每个值,我们用其中四份作为训练,剩余的一份验证。 因此,对于每个k,我们在交叉验证集上获得5个精度(精度是y轴,每个结果是一个点)。 趋势线通过每个k的5个结果的平均值绘制,误差条表示标准偏差。 请注意,在此特定情况下,交叉验证表明约k = 7的值最适合此特定数据集(对应于图中的峰值)。 如果我们使用更高倍的交叉验证(>5),我们可能会看到更平滑(即噪声较小)的曲线。

在实践中,人们更喜欢避免交叉验证而支持单个验证拆分,因为交叉验证在计算上可能是昂贵的(对于深度学习来说更是如此,数据量大且训练速度慢)。 人们倾向于使用的是把原始训练数据的50%-90%用于训练和剩余数据进行验证。 但是,这取决于多个因素:例如,如果超参数的数量很大,可能验证集占的比例会更大。 如果验证集中的样本数量很少(可能只有几百个左右),则使用交叉验证会更安全。 您在实践中可以看到的典型折叠数量是交叉验证的3倍,5倍或10倍。

常见数据拆分。 给出了训练和测试集。 训练集分为若干折叠(例如,这里为5折)。 折叠1-4为训练集。 剩余折叠(例如折叠5,此处为黄色)表示为验证折叠/验证集用于调整超参数。 交叉验证更进一步,重复选择哪个折叠(1-5)是验证折叠,其余为训练折叠/训练集。 这将被称为5倍交叉验证。 最后,一旦训练模型确定了所有最好的超参数,就会在测试数据上单次评估模型(红色)。

 

6.最近邻分类器/k近邻的优缺点

值得考虑最近邻分类器的一些优点和缺点。 显然,一个优点是实现和理解起来非常简单。 另外,分类器不需要训练,模型没有参数,只需要存储并索引训练数据即可。 但是,我们在测试时需要支付很高的计算成本,因为对测试示例进行分类需要与每个训练示例进行比较。 这是倒退的,因为在实践中我们真正关心的是测试时间效率而不是训练时间效率,而最近邻分类器测试时间效率远低于训练时间效率。 事实上,我们将在本课程后期开发的深度神经网络将这种权衡转移到另一个极端:训练非常昂贵,但是一旦完成训练,对新的测试示例进行分类是非常便宜/高效的。 这种操作模式在实践中更加令人满意。

另外,最近邻分类器的计算复杂度是研究的活跃领域,并且存在若干近似最近邻(ANN)算法和库可以加速数据集(例如FLANN)中最近邻居的查找。 这些算法允许人们在检索期间利用其空间/时间复杂度来权衡最近邻检索的正确性,并且通常依赖于涉及构建kdtree或运行k均值算法的预处理/索引阶段。

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

高维数据(尤其是图像)上的基于像素的距离可能非常不直观。 原始图像(左)和其旁边的三个其他图像,基于L2像素距离,它们都离它很远(而实际上,他们是相似的)。 显然,像素距离根本不对应于感知或语义相似性。
 

下面是另一个可视化,以说服你使用像素差异来比较图像是不够的。 我们可以使用一种名为t-SNE的可视化技术来获取CIFAR-10图像并将它们嵌入到二维中,以便最好地保留它们的(局部)成对距离。 在此可视化中,根据我们上面开发的L2像素距离,附近显示的图像被认为非常接近:

使用t-SNE对CIFAR-10图像进行2维嵌入。 基于L2像素距离,认为该图像与附近的图像接近。 注意背景的影响远远比语义类差异影响大(背景相似的图像更为接近,而不是相同类别的图像更为接近)。 单击此处查看此可视化的更大版本。

特别要注意的是,彼此相邻的图像更多地是图像的一般颜色分布,或背景的类型而不是它们的语义标识的函数。 例如,可以看到一只狗非常靠近青蛙,因为两者都碰巧在白色背景上。 理想情况下,我们希望所有10个类的图像形成自己的聚类,因此相同类的图像彼此相邻,而不管无关的特征和变化(例如背景)。 但是,要获得此属性,我们必须超越原始像素。

 

7.总结

  • 我们介绍了图像分类的问题,其中我们给出了一组图像,这些图像都用单个类别标记。 然后,我们要求为一组新的测试图像预测他们的类别,并测量预测的准确性。
  • 我们引入了一个名为Nearest Neighbor/kNN分类器的简单分类器。 我们看到与此分类器相关联的多个超参数(例如k的值,或距离度量方式),并且没有明显的选择方法。
  • 我们看到设置这些超参数的正确方法是将训练数据分成两部分:训练集和验证集。 我们尝试不同的超参数值,并保留在验证集上获得最佳性能的值。
  • 如果训练数据比较少,我们讨论了一个称为交叉验证的方法,它可以帮助减少噪声,估计哪些超参数最有效。
  • 找到最佳超参数后,我们会用它们对实际测试集执行单一评估。
  • 我们看到最近邻可以让我们在CIFAR-10上获得大约40%的准确率。 它实现起来很简单,但要求我们存储整个训练集,并且在测试阶段进行评估,计算代价非常高。
  • 最后,我们看到在原始像素值上使用L1或L2距离是不够的,因为距离与图像的背景和颜色分布的相关性比与其语义内容的相关性更强。

在接下来的课程中,我们将着手解决这些挑战并提供最终达到90%准确度的解决方案,让我们在学习完成后,可以完全丢弃训练集,并且它们将允许我们在不到一毫秒的时间内评估测试图像(训练过程花费的时间比较多,训练完成后就可以完全抛弃训练数据了,测试过程非常高效)。

8.实际应用kNN总结

如果你希望在实践中应用kNN(希望不在用在图像上,或者仅作为基线模型),请按以下步骤操作:

  • 预处理数据:规范化数据中的要素/特征(例如图像中的一个像素),使其均值为0,标准差为1。 我们将在后面的章节中更详细地介绍这一点,不选择在本节中介绍数据规范化,因为图像中的像素通常是同构的(0-255),并且没有表现出不相同的分布,从而减少了对数据规范化的需求,一般让图像中的每个像素值除以最大值255或也可以不做处理。
  • 如果数据维度非常高(输入特征非常多)可以考虑使用降维算法,如PCA(wiki refCS229refblog ref)或 Random Projections
  • 将训练数据随机分成训练集/验证集。 根据经验,70-90%的数据通常用于训练集。 此设置取决于您拥有多少超参数以及您希望它们具有多大影响力。 如果有很多超参数需要选择/调试,那么你应该在设置更大的验证集以便有效地调试他们,有更好的容错性。 如果您担心验证数据的大小,最好将训练数据拆分为folds并执行交叉验证。 如果你能负担得起计算预算,那么使用交叉验证总是更安全的,噪声更小(folds越多越好,但更昂贵)。
  • 在验证集上评估不同的超参数选择,如k值的选择(尝试的越多越好)和距离度量方式(L1和L2是良好的候选者),如果是采用交叉验证,对于每个超参数取值,在所有的folds上都进行验证。
  • 如果你的kNN分类器运行时间过长,请考虑使用近似最近邻库(例如  FLANN)来加速检索(以一定精度为代价)。
  • 记下验证集/交叉验证产生最佳结果的超参数。有一个问题是是否应该使用该最优超参数在完整训练集(训练集+验证集)再训练一遍分类器,如果把验证数据加到训练数据中,最优的超参数可能会改变(因为数据量增大了). 实际上,通过验证集得到最优超参数/最好的模型后,就不再使用验证集了,就将其销毁,然后把选出的最优的模型在测试集上进行评估,报告测试集准确度,并将结果声明为数据上kNN分类器的性能。

9.拓展阅读

 

猜你喜欢

转载自blog.csdn.net/sdu_hao/article/details/86522479
今日推荐