image classification
图像分类问题就是为输入图像从一组给定的类别中为其分配一个标签的问题,这是计算机视觉领域的核心任务之一,尽管他很简单但却有很多种实际应用,许多看似不同的其他计算机视觉任务都可以简化为图像分类任务。例如下图就给出了一个图像分类模型根据给出的输入图像为其分配标签。
可能遇到的问题
虽然图像识别任务对于人来说是微不足道的,因为大脑早已具备了视觉识别的神经系统,但是这对于计算机来说却是十足的挑战,你很难像编写一个排序算法一样对给定的图像去给出他的输出类别,而且分类算法需要对图像细微的差别具有良好的鲁棒性,在实际情况中可能会遇到各种各样的问题
1.角度不同,同一个物体可能因为镜头角度不同产生多幅图像
2.遮挡,要分类的物体可能被其它东西遮挡,只能看到物体的一部分
3.背景环境,物体可能与背景环境融为一体,使他们很难辨认
4.同一类别也可能有很多种不同形式,比如颜色,外观不同
数据驱动de方法
由于这些困难我们很难以传统的方式来编写图像分类算法,所以我们提出了数据驱动的方法,我们不会尝试直接在代码中指定每个感兴趣类别的样子,而是采用与孩子一样的方法:我们将为计算机提供许多示例,每个班级,然后开发学习算法,以查看这些示例并了解每个班级的视觉外观。该方法称为数据驱动方法,因为它依赖于首先积累训练数据集标签图像。这是一个此类数据集的示例:
图像分类的步骤:
输入:我们的输入包含一组N个图像,每个图像标记有K个不同的类别之一。我们将此数据称为训练集。
学习:我们的任务是使用训练集来学习每一堂课的模样。我们将此步骤称为训练分类器或学习模型。
评估:最后,我们通过要求分类器预测从未见过的一组新图像的标签来评估分类器的质量。然后,我们将这些图像的真实标签与分类器预测的标签进行比较。直观地,我们希望很多预测与真实答案(我们称之为基本事实)相匹配。
knn 最近邻分类器
图像分类数据集示例:CIFAR-10。一种流行的玩具图像分类数据集是CIFAR-10数据集。该数据集包含60,000个高度和宽度为32像素的微小图像。每个图像都标记有10类(例如“飞机,汽车,鸟类等”)之一。将这60,000张图像划分为50,000张图像的训练集和10,000张图像的测试集。在下面的图像中,您可以从10个类别的每一个中看到10个随机示例图像:
假设现在给我们提供了50,000张图像的CIFAR-10训练集(每个标签有5,000张图像),我们希望将剩下的10,000张标签。最近的邻居分类器将获取测试图像,将其与训练图像中的每一个进行比较,并预测最近的训练图像的标签。在上方和右侧的图像中,您可以看到针对10个示例测试图像的此过程的示例结果。请注意,在10个示例中,只有大约3个检索到相同类的图像,而在其他7个示例中,并非如此。例如,在第8行中,最接近马头的训练图像是红色汽车,可能是由于黑色背景很强。结果,这种马的图像在这种情况下将被错误地标记为汽车。
我们还没有确切指定如何比较两个图像的细节,最简单的方法就是把两个图像的像素矩阵逐元素相减,然后把减完得到的矩阵元素全部相加得到 一个数字,这个数字就是L1距离
下图给出了具体实现过程
下面是代码实现
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
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) )
距离的选择:
还有许多其他方法可以计算向量之间的距离。另一个常见的选择是改为使用L2距离,它具有计算两个向量之间的欧式距离的几何解释。距离采用以下形式:
k-近邻分类器
当我们希望进行预测时,仅使用最近图像的标签效果并不好。确实,几乎总是有一种情况可以通过使用所谓的k最近邻居分类器来做得更好。这个想法非常简单:我们将找到前k个最接近的图像,而不是在训练集中找到单个最接近的图像,然后将它们投票给测试图像的标签。特别是,当k = 1时,我们恢复了最近邻分类器。直观地,较高的k值具有平滑效果,使分类器更能抵抗离群值:
我们可以看到如果k值是1,那么分类器的决策边界将会非常不平滑,可能比如在大片蓝色分类的区域里出现一小块绿色的区域,尽管这可能只是数据的噪声或者误差数据,分类器仍然会把它分类成错误类别,会出现过拟合现象,如果把k值设为5,可以看到决策边界就会比较平滑,应用到测试集上就会有更好的泛化效果,而不仅仅是在训练集上表现优秀。
Linear Classification
前面我们提到了knn最近邻分类器,他确实能帮助我们实现图像分类问题,但是他也有一些天生的缺陷,比如knn的分类精确率不高,他在训练阶段几乎没有做什么事情,只是把所有的训练数据记住了而已,但是在测试阶段他要花费大量的时间去比对测试图像与每一张训练图像的L1距离或是L2距离,这是不合适的,因为我们更希望在训练阶段花费时间,而在测试阶段来一张新图片的时候我们的分类算法能够迅速给出类别标签。
接下来我们要提出一个更强大的分类方法,尽管这只是基础,但是却是后续神经网络以及卷积神经网络的重要组成部分。这个方法主要有两个部分组成score fucntion和loss function,评分函数用于把输入图像的数据映射到各个类别的得分,损失函数则定义我们预测的类别得分与真实类别的差距,然后问题就转化为了优化loss function的问题,我们将找到使得loss function最小的score function中的权重值。最简单的score function就是我们这里要说的linear classfier
这就是一个映射函数,W表示权重矩阵,xi是我们的输入数据,b是偏置项,简而言之这个映射函数将会把我们的输入数据映射到类别得分,注意这里的xi表示单张图片维度是d*1 d表示把输入的图片展开成d维的列向量,W的维度是k*d k表示有k个类别,这样做完矩阵乘法就会得到各个类别的score了,其中这里的W我们称为权重矩阵,我们要做的就是找到使得后面定义的loss function最小的权重矩阵。
下图就是一个由输入图片经过score function映射到类别得分的例子
可以看到W权重矩阵的每一行,可以看做是某个类别的分类器,例如红色的那一行就可以看做是专门用来检测猫的分类器,这一行权重与输入的图像数据相乘就会得到cat这个类别的分数,我们可以看到这个例子的类别得分并不好,因为分类器给dog类别很高的分数,说明分类器认为这个图像更像是个狗,显示这是不对的,后续要做的就是去优化我们的权重矩阵让他在cat类别得到更高的分数。
类比图像为高维点
这有助于我们理解线性分类器到底在做什么,因为真实的图像数据都是很高维度的,比如cifar-10数据集里的图像就是3072维的高维数据,我们很难可视化这么高维度的数据,但是我们可以把数据维度压缩到二维,观察数据在空间的分布,实际上线性分类在做的就是在数据空间中把不同的类别划分出边界来
把线性分类器认为是模板匹配
可以把线性分类解释为一种模板匹配。因为W矩阵的每一行都是与输入数据做点积,所以可以认为w的每一行对应了一个类别的模板,一旦输入数据与某一个类别模板相匹配,那么他将在这个类别上获得很高的分数。但是有一个问题就是,线性分类器只允许一个类别学习一个模板,显然这很难达成目标,因为比方说数据集中的汽车有许多颜色,车头可能朝着很多方向,但是因为汽车模板只允许学习一个,那么模板就会尽量匹配数据集里的所有汽车生成一个可能多种颜色,车头朝各种方向的汽车模板,又或者可能因为数据集中关于马的分类图片有头朝左的也有头朝右的,所以马模板可能是两个头的马,这些都是由于线性分类只允许一个类别学习一个模板造成的。下图就是cifar-10在线性分类下不同类别的权重可视化之后的类别模板
到这里我们就介绍完了线性分类器的score function和从高维空间以及模板匹配的方式去理解线性分类器所做的事,后面我们还会讲到linear classification中的另一部分 loss function和优化 。
Loss Function
在之前的部分我们定义了一个score function可以从给定的像素输入值映射到类别分数,我们需要做的就是去调整权重参数使得预测的分类得分在正确的分类标签上尽可能高,例如上面图中那个猫图片的例子,实际上得到的分数就不好,分类器给了猫-98的得分,那么我们就要去定义一个损失函数loss function去表达我们对分类效果的不满,直觉上来说,如果分类器总是给图片正确类别的分数高,那么应该loss function应该尽可能小,反之loss function就会大,当然定义损失函数的方法有很多,我们这里先介绍一种非常常用的损失函数SVM 支持向量机。
svm
svm的思想就是它总是希望正确类别的分数比错误类别的高出一个边界,具体的公式如下
公式里sj表示score function得到的预测类别分数,把其中不是正确类别的分数减去正确类别的分数再加上一个边界值得到的值与0做一个max函数再把所有类别的损失值加起来就得到了svm的损失值。可以想象一下,只有当正确类别的分数比一个错误类别的分数高出至少一个边界值才能使得这一错误类别的损失为0,换言之就是当正确类别的分数比错误类别高出一个边界svm就满足了。
举个例子来说,假如我们得到的得分是s=[13,-7,11],而且13是正确类别的得分,那么svm损失就等于
regularization正则化
关于损失函数,这里还有一个问题,就是假如我们有一个W矩阵可以刚好正确的分类数据集中的所有图片,就是说在svm下,所有图片的损失值都是0,那么问题来了,就是这个权重矩阵不是唯一的。简单来说,如果找到一个W使得loss 为0,那么这个W矩阵乘1个大于1的倍数得到的W都能使得损失为1,因为svm仅需正确类别得分比错误类别高一个边界,在放大W后得到的正确类别得分将会比错误类别得分更高。
基于此问题我们提出了一个最常用的正则化惩罚就是L2正则化惩罚
根据上面的公式,我们会把权重矩阵W里的元素逐元素求平方和把他们全部累加起来得到正则化损失,很容易看到,权重矩阵值越大,得到的正则化损失就会越大,因此这一举措会防止权重矩阵过大,更倾向于得到小而全面的权重矩阵。加入了正则化损失之后,完整的svm的损失函数就由两个部分组成,一个是前面的数据损失,一个是这里的正则化损失
更具体一点
这样我们就得到了完整的svm损失,上面公式中的N表示训练图片的数量。引入正则化损失除了要惩罚大值的权重外还有其他的优点,举个例子我们得到一个x=【1,1,1,1】 的输入,并且有两个权重向量W1=【1,0,0,0】 和W2 = 【0.25,0.25,0.25,0.25】 显而易见把x乘上这两个权重向量得到的这一类别的分类得分结果都是1,但是根据L2正则化 ,显然W2是更优秀的选择,因为他的L2损失更低,直观来讲L2损失更喜欢比较小而且比较分散的权重,这样分类器将会考虑更多维度的输入,而不是仅看在权重上值比较大的输入,这会提高分类器的泛化性,并减少过拟合。
softmax
上述的svm支持向量机是两个最常见的分类器之一,下面将介绍另一个常见的分类器,那就是softmax。
softmax有与svm完全不同的损失函数,softmax在输入数据与权重W相乘之后得到的分数之后,会把分数做一个指数运算,然后把每个类别做指数运算的值除以所有类别指数运算之后求和得到的值得到这个分类的概率,softmax得到的会是每个类别的概率分布,这相比svm的分类得分会显得更直观,softmax会直接告诉你每一个类别的概率是多少,当然我们希望在正确类别上取得较高的概率,至于损失函数就是对正确类别的概率做一个负对数运算,直觉上来看,正确类别概率越高越接近1,那么负对数就越接近0,反之,负对数就越接近无穷大。
这里有一个小的实践问题需要注意一下,就是在softmax里存在一个指数运算,为了保证数值稳定性我们可以先给所有得分减去最大值之后再做指数运算,这样就不会出现指数爆炸的情况,比如得分是200,那么e的200次方数值就太大了。
f = np.array([123, 456, 789]) # example with 3 classes and each having large scores
p = np.exp(f) / np.sum(np.exp(f)) # Bad: Numeric problem, potential blowup
# instead: first shift the values of f so that the highest number is 0:
f -= np.max(f) # f becomes [-666, -333, 0]
p = np.exp(f) / np.sum(np.exp(f)) # safe to do, gives the correct answer
softmax与svm
为了更好更直观的理解svm与softmax 我们来通过一个例子来具体看实现过程
准确的说,svm分类器使用铰链损失(hinge loss),softmax分类器使用交叉商损失。
svm给出的是每个类别的得分,softmax则给出的是每个类别的概率,相对来说softmax的概率会让人更好理解,直观的给出了分类器认为它是什么类别的概率。
他们之间的主要区别还是在于损失函数上,svm的hinge loss相对来说要更宽容。只要正确类别比错误类别高出一定的界限就会给出0损失,但是softmax则对分类永远都不会满意永远都希望在正确分类上取得更好的概率得分,svm损失这种宽容的特征可以被看成是某种功能,举个例子如果我们在做汽车分类任务那我们应该将精力放在分类不同汽车和卡车的棘手问题上,而不应该受青蛙分类的影响,因为在青蛙分类上的得分已经足够低了。
优化
在我们得到了score function和loss fucntion之后就要对loss 做优化处理了,这里我们使用一种名为随机梯度下降的方法做优化。基本的思路就是求loss对于权重矩阵每个权重的偏导数,然后沿着导数的反方向去更新权重,这样就能使loss值变小。
步长的影响: 梯度告诉我们函数具有最陡峭的增长率的方向,但没有告诉我们应该沿着这个方向走多远。正如我们将在本课程的后面部分看到的那样,选择步长(也称为学习率))将成为训练神经网络中最重要(也是最令人头疼的)超参数设置之一。在蒙住眼睛的山坡下降类比中,我们感觉脚下的山坡向某个方向倾斜,但是我们应该采取的步长是不确定的。如果我们仔细地迈步,我们可以期待取得一致但很小的进步(这相当于步幅很小)。相反,我们可以选择采取较大的自信步骤来尝试更快地下降,但这可能不会奏效。如您在上面的代码示例中看到的那样,在某些时候,采取更大的措施会导致更大的损失,因为我们“过度”。
但是因为有的时候训练样本过大,更新一次权重需要计算所有训练样本的损失之后再进行效率太低了,通常会采取一小批一小批的数据进行更新,就是随机梯度下降,这样会提高更新效率,在后续课程中会继续讲到具体是怎么通过链式法则计算loss对于权重的梯度的。