课时4:数据驱动方法
在上一章,提到了关于图像分类的任务,这是一个计算机视觉中真正核心的任务,同时也是本课程中关注的重点。
目标:这一节我们将介绍图像分类问题。所谓图像分类问题,就是已有固定的分类标签集合,然后对于输入的图像,从分类标签集合中找出一个分类标签,最后把分类标签分配给该输入图像。虽然看起来挺简单的,但这可是计算机视觉领域的核心问题之一,并且有着各种各样的实际应用。在后面的课程中,我们可以看到计算机视觉领域中很多看似不同的问题(比如物体检测和分割),都可以被归结为图像分类问题。
例子:以下图为例,图像分类模型读取该图片,并生成该图片属于集合 {cat, dog, hat, mug}中各个标签的概率。需要注意的是,对于计算机来说,图像是一个由数字组成的巨大的3维数组。在这个例子中,猫的图像大小是宽248像素,高400像素,有3个颜色通道,分别是红、绿和蓝(简称RGB)。如此,该图像就包含了248X400X3=297600个数字,每个数字都是在范围0-255之间的整型,其中0表示全黑,255表示全白。我们的任务就是把这些上百万的数字变成一个简单的标签,比如“猫”。
图像分类的任务,就是对于一个给定的图像,预测它属于的那个分类标签(或者给出属于一系列不同标签的可能性)。图像是3维数组,数组元素是取值范围从0到255的整数。数组的尺寸是宽度x高度x3,其中这个3代表的是红、绿和蓝3个颜色通道。
困难和挑战:对于人来说,识别出一个像“猫”一样视觉概念是简单至极的,然而从计算机视觉算法的角度来看就值得深思了。我们在下面列举了计算机视觉算法在图像识别方面遇到的一些困难,要记住图像是以3维数组来表示的,数组中的元素是亮度值。
- 视角变化:同一个物体,摄像机可以从多个角度来展现。
- 大小变化:物体可视的大小通常是会变化的(不仅是在图片中,在真实世界中大小也是变化的)。
- 形变:很多东西的形状并非一成不变,会有很大变化。
- 遮挡:目标物体可能被挡住。有时候只有物体的一小部分(可以小到几个像素)是可见的。
- 光照条件:在像素层面上,光照的影响非常大。
- 背景干扰:物体可能混入背景之中,使之难以被辨认。
- 类内差异:一类物体的个体之间的外形差异很大,比如椅子。这一类物体有许多不同的对象,每个都有自己的外形。
面对以上所有变化及其组合,好的图像分类模型能够在维持分类结论稳定的同时,保持对类间差异足够敏感。
数据驱动方法:如何写一个图像分类的算法呢?这和写个排序算法可是大不一样。怎么写一个从图像中认出猫的算法?搞不清楚。因此,与其在代码中直接写明各类物体到底看起来是什么样的,倒不如说我们采取的方法和教小孩儿看图识物类似:给计算机很多数据,然后实现学习算法,让计算机学习到每个类的外形。这种方法,就是数据驱动方法。既然该方法的第一步就是收集已经做好分类标注的图片来作为训练集,那么下面就看看数据库到底长什么样:
一个有4个视觉分类的训练集。在实际中,我们可能有上千的分类,每个分类都有成千上万的图像。
如果使用python写一个图像分类器,定义一个方法,接受图片作为输入参数,经过一系列的操作,最终返回到图片上进行标记是猫还是狗等等。
对于猫来说,它有耳朵、眼睛、鼻子、嘴巴,而通过上一章中Hubel和Wiesel的研究,我们了解到边缘对于视觉识别是十分重要的,所以尝试计算出图像的边缘,然后把边、角各种形状分类好,可以写一些规则来识别这些猫。
但是如果想识别比如卡车、其他动物等,又需要重新从头再来一遍,所以这不是一种可推演的方法,我们需要的是一种识别算法可以拓展到识别世界上各种对象,由此我们想到了一种数据驱动的方法。
图像分类流程。在课程视频中已经学习过,图像分类就是输入一个元素为像素值的数组,然后给它分配一个分类标签。完整流程如下:
- 输入:输入是包含N个图像的集合,每个图像的标签是K种分类标签中的一种。这个集合称为训练集。
- 学习:这一步的任务是使用训练集来学习每个类到底长什么样。一般该步骤叫做训练分类器或者学习一个模型。
- 评价:让分类器来预测它未曾见过的图像的分类标签,并以此来评价分类器的质量。我们会把分类器预测的标签和图像真正的分类标签对比。毫无疑问,分类器预测的分类标签和图像真正的分类标签如果一致,那就是好事,这样的情况越多越好。
所以,如果写一个方法,可以定义两个函数,一个是训练函数,用来接收图片和标签,然后输出模型;另一个数预测函数,接收一个模型,对图片种类进行预测。
这种数据驱动类的算法是比深度学习更广义的一种理念,通过这种过程,对于一个简单的分类器(最近邻分类器),在训练过程中,我们只是单纯的记录所有的训练数据;在预测过程中,拿新的图像与已训练好的训练对比,进行预测。
Nearest Neighbor分类器
图像分类思想:拿测试图片和训练集中每一张图片去比较,然后将它认为最相似的那个训练集图片的标签赋给这张测试图片。
虽然这个分类器和卷积神经网络没有任何关系,实际中也极少使用,但通过实现它,可以让读者对于解决图像分类问题的方法有个基本的认识。
图像分类数据集:CIFAR-10。一个非常流行的图像分类数据集是CIFAR-10。这个数据集包含了60000张32X32的小图像。每张图像都有10种分类标签中的一种。这60000张图像被分为包含50000张图像的训练集和包含10000张图像的测试集。在下图中你可以看见10个类的10张随机图片。
左边:从CIFAR-10数据库来的样本图像。右边:第一列是测试图像,然后第一列的每个测试图像右边是使用Nearest Neighbor算法,根据像素差异,从训练集中选出的10张最类似的图片。
假设现在我们有CIFAR-10的50000张图片(每种分类5000张)作为训练集,我们希望将余下的10000作为测试集并给他们打上标签。Nearest Neighbor算法将会拿着测试图片和训练集中每一张图片去比较,然后将它认为最相似的那个训练集图片的标签赋给这张测试图片。上面右边的图片就展示了这样的结果。请注意上面10个分类中,只有3个是准确的。比如第8行中,马头被分类为一个红色的跑车,原因在于红色跑车的黑色背景非常强烈,所以这匹马就被错误分类为跑车了。
如何比较两张图片呢?
方法1:L1距离---曼哈顿距离---
在本例中,就是比较32x32x3的像素块。最简单的方法就是逐个像素比较,最后将差异值全部加起来。换句话说,就是将两张图片先转化为两个向量I1和I2,然后计算他们的L1距离:
这里的求和是针对所有的像素。下面是整个比较流程的图例:
以图片中的一个颜色通道为例来进行说明。两张图片使用L1距离来进行比较。逐个像素求差值,然后将所有差值加起来得到一个数值。如果两张图片一模一样,那么L1距离为0,但是如果两张图片很是不同,那L1值将会非常大。
虽然这个方法有些笨,但是有些时候却有它的合理性,它给出了比较两幅图片的具体方法。
python代码实现:
import numpy as np
class KNN_L1:
def __init__(self):
pass
def train(self,X, y):
"""
Train the classifier. For k-nearest neighbors this is just
memorizing the training data.
Inputs:
- X: A numpy array of shape (num_train, D) containing the training data
consisting of num_train samples each of dimension D.
- y: A numpy array of shape (N,) containing the training labels, where
y[i] is the label for X[i].
"""
self.X_train = X
self.y_train = y
def predict(self, x):
"""
Compute the distance between each test point in x and each training point
in self.X_train using L1 distance
Inputs:
- x: A numpy array of shape (num_test, D) containing test data.
Returns:
- y_pred: A numpy array of shape (num_test, num_train) where dists[i, j]
is the Euclidean distance between the ith test point and the jth training
point.
"""
num_test = x.shape[0]
y_pred = np.zeros(num_test, dtype=self.y_train.dtype)
for i in range(num_test):
# 求和
distances = np.sum(np.abs(self.X_train-x[i, :]), axis=1)
#求最小索引
min_index = np.argmin(distances)
#排序
y_pred[i] = self.y_train[min_index]
return y_pred
方法2:L2距离--欧式距离--
import numpy as np
class KNN_L2:
def __init__(self):
pass
def train(self,X,y):
self.X_train = X
self.y_train = y
def predict(self,X,k=1,num_loops=0):
if num_loops==0:
dists=self.compute_distances_no_loops(X)
elif num_loops==1:
dists=self.compute_distances_one_loops(X)
elif num_loops==2:
dists=self.compute_distances_one_loops(X)
return self.predict_labels(dists,k=k)
#双重循环
def compute_distances_two_loops(self, X):
"""
Compute the distance between each test point in X and each training point
in self.X_train using a nested loop over both the training data and the
test data.
Inputs:
- X: A numpy array of shape (num_test, D) containing test data.
Returns:
- dists: A numpy array of shape (num_test, num_train) where dists[i, j]
is the Euclidean distance between the ith test point and the jth training
point.
"""
num_test = X.shape[0]
num_train = self.X_train.shape[0]
dists = np.zeros((num_test,num_train))
for i in range(num_test):
for j in range(num_train):
#X[i,:]是取第i维中下标为i的元素的所有数据,第i行(从0开始)
dists[i, j] = np.sqrt(np.sum((X[i, :]-self.X_train[j, :])**2))
return dists
# 一层循环
def compute_distances_one_loop(self, X):
"""
Compute the distance between each test point in X and each training point
in self.X_train using a single loop over the test data.
Input / Output: Same as compute_distances_two_loops
"""
num_test = X.shape[0]
num_train = self.X_train.shape[0]
dists = np.zeros((num_test, num_train))
for i in range(num_test):
#np.square(x) : 计算数组各元素的平方
dists[i, :] = np.sqrt(np.sum(np.square(self.X_train - X[i, :]), axis=1))
return dists
#无循环
def compute_distances_no_loops(self, X):
"""
Compute the distance between each test point in X and each training point
in self.X_train using no explicit loops.
Input / Output: Same as compute_distances_two_loops
"""
num_test = X.shape[0]
num_train = self.X_train.shape[0]
dists = np.zeros((num_test, num_train))
test_sum = np.sum(np.square(X), axis=1)
train_sum = np.sum(np.square(self.X_train), axis=1)
inner_product = np.dot(X, self.X_train.T)
dists = np.sqrt(-2 * inner_product + test_sum.reshape(-1, 1) + train_sum)
return dists
def predict_labels(self, dists, k=1):
"""
Given a matrix of distances between test points and training points,
predict a label for each test point.
Inputs:
- dists: A numpy array of shape (num_test, num_train) where dists[i, j]
gives the distance betwen the ith test point and the jth training point.
Returns:
- y: A numpy array of shape (num_test,) containing predicted labels for the
test data, where y[i] is the predicted label for the test point X[i].
"""
num_test = dists.shape[0]
y_pred = np.zeros(num_test)
for i in range(num_test):
# # A list of length k storing the labels of the k nearest neighbors to
# the ith test point.
closest_y = []
# Use the distance matrix to find the k nearest neighbors of the ith #
# testing point, and use self.y_train to find the labels of these #
# neighbors. Store these labels in closest_y. #
# Hint: Look up the function numpy.argsort.
y_indicies = np.argsort(dists[i, :], axis=0)
closest_y = self.y_train[y_indicies[: k]]
y_pred[i] = np.argmax(np.bincount(closest_y))
return y_pred
下面是最近邻分类器的python代码:
该代码非常的简明,因为使用了numpy提供的向量运算,train函数将它用于最近邻算法非常简单,只需存储训练数据即可,在测试的时候,将输入图像使用L1距离函数,将测试图片与训练实例进行比较,在训练集中找到最相似的实例。可以看出使用向量化操作,只需要1,2行python代码就能实现。
关于最近邻分类器的问题:
1.如果我们在训练集中有N个实例,训练和测试的过程可以有多快?
答:训练:O(1) 测试:O(N)
由此看来最近邻算法有点落后了,它在训练中花的时间很少,而在测试中花了大量时间;而看卷积神经网络和其他参数模型,则正好相反,它们会花很多时间在训练上,而在测试过程中则非常快。我们希望的是测试能够更快一点,而训练慢一点没有关系,它是在数据中心完成的。
那么在实际应用中,最近邻算法到底表现如何?可以看到下面的图像:
它是最近邻分类器的决策区域,训练集包含二维平面中的这些点,点的颜色代表不同的类别或不同的标签,这里有五种类型的点。对于这些点来说,将计算这些训练数据中最近的实例,然后在这些点的背景上着色,标示出它的类标签,可以发现最近邻分类器是根据相邻的点来切割空间并进行着色。
但是通过上述图片中,可以看到绿色区域中间的黄色区域(事实上该点应该是绿色的),蓝色区域中有绿色区域的一部分,这些都说明了最近邻分类器的处理是有问题的。
那么,基于以上问题,产生了K-近邻算法,它不仅是寻找最近的点,还会做一些特殊的操作,根据距离度量,找到最近的K个点,然后在这些相邻点中进行投票,票数多的近邻点预测出结果。
下面用同样的数据集分别使用K=1、K=3、K=5的最近邻分类器:
在K=3时,可以看到绿色区域中的黄色点不再会导致周围的区域被划分成黄色,因为使用了多数投票,中间的这个绿色区域都会被划分成绿色;在K=5时,可以看到蓝色和红色区域间的决策边界变得更加平滑好看。
问题:上图中白色区域代表什么?
答:白色区域表示这个区域没有获得K-最近邻的投票,可以做大胆的假设,把它划分为一个不同的类别。
所以使用最近邻分类器时,总会给K赋一个比较大的值,这会是决策边界变得更加平滑,从而得到更好的结果。
课时5 K-近邻算法
继续讨论KNN(K-最近邻算法),回到图片中来,它实际表现的并不好,用红色和绿色分别标注了图像分类的正确与否:
取决于它的近邻值,可以看到KNN的表现效果不是很好,但如果可以使用一个更大的K值,那么投票操作的结果就可能会达到很好的分类效果。
当我们使用K-最近邻算法时,确定应该如何比较相对近邻数据距离值。比如,已经讨论过的L1距离,它是像素之间绝对值的总和;另一种常见的选择是L2距离,也就是欧式距离(平方和的平方根)。
这两种方式,L1取决于你选择的坐标系统,所以如果转动坐标轴,将会改变点之间的L1距离;而改变坐标轴对L2距离无影响。
问题:什么情况下L1距离比L2距离表现的更好?
答:主要和解决的问题相关,很难说哪一种更好,因为L1有坐标依赖,其依赖于数据的坐标系统,如果你有一个向量,如出于某种原因要对员工进行分类,向量的不同元素对应员工的不同特征,如果向量中的各个元素有着实际意义,那么L1可能更加好一点。最佳的方法就是两个都尝试下,看看哪个效果更佳。
通过使用不同的距离度量,可以将K-最近邻分类器泛化到许多不同的数据类型上,而不仅仅是向量和图像。例如,假设想对文本进行分类,那么要做的就是对KNN指定一个距离函数,这个函数可以测量两段、两句话之间的距离。
因此,通过指定不同的距离度量,就可以很好地应用这个算法在基本上任何数据类型的数据上。
下面不同距离的决策边界的形状变化很大,L1中这些决策边界趋于跟随坐标轴,又是因为L1取决于我们选择的坐标系,L2对距离的排序不会受到坐标轴的影响,只是吧边界放置在最自然的地方。
可以在http://vision.stanford.edu/teaching/cs231n-demos/knn/尝试这个KNN,实际上是非常有趣的,可以很好地培养决策边界的直觉。
所以,一旦真的尝试在实践中使用这个算法,有几个选择是需要做的。比如,讨论过的选择K的不同值,选择不同的距离度量,该如何根据问题和数据来选择这些超参数,K值和距离度量称之为超参数,它们不一定能从训练数据中学到。
在实际中,大多使用k-NN分类器。但是k值如何确定呢?接下来就讨论这个问题。
错误的策略:
(1)选择能对训练集给出最高的准确率、表现最佳的超参数;
不要这么做,在机器学习中,不是要尽可能拟合训练集,而是要让分类器在训练集以外的未知数据上表现更好。如在k最近邻算法中,假设k=1,我们总能完美的分类训练集数据,在实践中,让k取更大的值,尽管会在训练集中分错个别数据,但对于训练集中未出现过的数据分类性能更佳。
(2)所有的数据分成两部分:一部分是训练集,另一部分是测试集,然后在训练集上使用不同的超参数来训练算法,将训练好的分类器用在测试集上,再选择一组在测试集上表现最好的超参数;
同样不要这么做,机器学习系统的目的是让我们了解算法表现究竟如何,所以测试集的目的是给我们一种预估方法,如果采用这种方法,只能让我们算法在这组测试集上表现良好,但它无法代表在未见过的数据上的表现。
正确的策略:
(1)所有数据分成三部分:训练集、验证集和测试集,大部分数据作为训练集,通常所做的是在训练集上用不同的超参数来训练算法,在验证集上进行评估,然后用一组超参选择在验证集上表现最好的,再把这组验证集上表现最好的分类器拿出来在测试集上运行,这才是正确的方法。
(2)交叉验证:在深度学习中不太常见。有时候,训练集数量较小(因此验证集的数量更小),这种方法更加复杂些。还是用刚才的例子,如果是交叉验证集,我们就不是取1000个图像,而是将训练集平均分成5份,其中4份用来训练,1份用来验证。然后我们循环着取其中4份来训练,其中1份来验证,最后取所有5次验证结果的平均值作为算法验证结果。
实际应用。在实际情况下,人们不是很喜欢用交叉验证,主要是因为它会耗费较多的计算资源。一般直接把训练集按照50%-90%的比例分成训练集和验证集。但这也是根据具体情况来定的:如果超参数数量多,你可能就想用更大的验证集,而验证集的数量不够,那么最好还是用交叉验证吧。至于分成几份比较好,一般都是分成3、5和10份。
问题:训练集和验证集的区别是什么?
答:算法可以看到训练集中的各个标签,但是在验证集中,只是用验证集中的标签来检查算法的表现。
问题:测试集不能很好的代表真实的数据?
答:统计学假设数据都相互独立,服从同一分布,数据中所有的点都来自同一概率分布,当然现实中不可能总是这样,肯定会遇到一些例外,也就是测试集代表性不佳,不能很好的反应真实世界,
那么经过交叉验证可能会得到这样的一张图:
横轴表示K-近邻分类器中的参数K值,纵轴表示分类器对不同K值在数据上的准确率。5份交叉验证对k值调优的例子,对每个K值,得到5个准确率结果,取其平均值,然后对不同k值的平均表现画线连接。本例中,当k=7的时算法表现最好(对应图中的准确率峰值)。如果我们将训练集分成更多份数,直线一般会更加平滑(噪音更少)。所以当训练一个机器学习的模型时,最后要画这样一张图,从中可以看出算法的表现以及各个超参数之间的关系,最终可以选出在验证集上最好的模型以及相应的超参数。
为什么只用最相似的1张图片的标签来作为测试图像的标签呢?使用k-Nearest Neighbor分类器就能做得更好。思想:与其只找最相近的那1个图片的标签,我们找最相似的k个图片的标签,然后让他们针对测试图片进行投票,最后把票数最高的标签作为对测试图片的预测。所以当k=1的时候,k-Nearest Neighbor分类器就是Nearest Neighbor分类器。从直观感受上就可以看到,更高的k值可以让分类的效果更平滑,使得分类器对于异常值更有抵抗力。
上面示例展示了Nearest Neighbor分类器和5-Nearest Neighbor分类器的区别。例子使用了2维的点来表示,分成3类(红、蓝和绿)。不同颜色区域代表的是使用L2距离的分类器的决策边界。白色的区域是分类模糊的例子(即图像与两个以上的分类标签绑定)。需要注意的是,在NN分类器中,异常的数据点(比如:在蓝色区域中的绿点)制造出一个不正确预测的孤岛。5-NN分类器将这些不规则都平滑了,使得它针对测试数据的泛化(generalization)能力更好(例子中未展示)。注意,5-NN中也存在一些灰色区域,这些区域是因为近邻标签的最高票数相同导致的(比如:2个邻居是红色,2个邻居是蓝色,还有1个是绿色)。
其实,KNN在图像分类中很少用到。
(1)它的测试时间非常慢;
(2)像欧式距离或者L1距离这样的衡量标准用在比较图像上不太合适;这种向量化的距离函数不太适合表示图像之间视觉的相似度,究竟我们是如何区分图像不同呢?
最左边是最原始的图片,右边是经过处理的图片,如遮住嘴,向下平移几个像素的距离,或者把整幅图染的偏蓝,如果计算原图和遮挡的图、平移、染色的图之间的欧几里得距离,结果是一样的,L2确实不适合表示图像之间视觉感知的差异。
问题:为什么距离都是一样的?
答:我们在处理时注意确保让他们的L2距离保持一致,
(3)维度灾难:KNN有点像把样本空间分成几块,意味着如果希望分类器有好的效果,需要训练数据密集的分布在空间中;而问题在于,想要密集的分布在样本空间中,需要指数倍的训练数据,然而不可能拿到这样高维空间中的像素。
注:这里用点表示训练数据,点的颜色代表他们的类别。
总结:借用KNN介绍了图像分类的基本思路,借助训练集的图片和相应的标记可以预测测试集中数据的分类。
课时6 线性分类
在线性分类中,将采用与K-最近邻稍有不同的方法,线性分类是参数模型中最简单的例子,以下图为例:
问题:图像中的3指的是什么?
指的是3种颜色通道,红绿蓝。因为经常和彩色图像打交道,所以这3种通道信息就是不想丢掉的好信息。
通常把输入数据设为x,权重设为w,现在写一些函数包含了输入参数x和参数w,然后就会有10个数字描述的输出,即在CIFAR-10中对应的10个类别所对应的分数。 现在,在这个参数化的方法中,我们总结对训练数据的认知并把它都用到这些参数w中,在测试的时候,不再需要实际的训练数据,只需要这些参数我,这使得模型更有效率。 在深度学习中,整个描述都是关于函数F正确的结构,可以来编写不同的函数形式用不同的、复杂的方式组合权重和数据,这些对应于不同的神经网络体系结构,将他们相乘是最简单的组合方式,这就是一个线性分类器。
线性分类器工作的例子如下:
我们把2*2的图像拉伸成一个有4个元素的列向量,在这个例子中,只限制了3类:猫,狗,船;权重矩阵w是3行4列(4个像素3个类);加上一个3元偏差向量,它提供了每个类别的数据独立偏差项;现在可以看到猫的分数是图像像素和权重矩阵之间的输入乘积加上偏置项。 线性分类器的另一个观点是回归到图像,作为点和高维空间的概念,可以想像每一张图像都是类似高维空间中的一个点,现在线性分类器尝试在线性决策边界上画一个线性分类面来划分一个类别和剩余其他类别,如下图所示:
在训练过程中,这些线条会随机地开始,然后快速变化,试图将数据正确区分开,但是从这个高维的角度考虑线性分类器,就能再次看到线性分类器中出现的问题。 假设有一个两类别的数据集,蓝色和红色,蓝色类别是图像中像素数量大于0且都是奇数;红色类别是图像中像素数量大于0且都是偶数,如果去画这些不同的决策,能看到奇数像素点的蓝色类别在平面上有两个象限,所以没有办法能够绘制一条单独的直线来划分蓝色和红色,这就是线性分类器的问题所在。
当然线性分类器还有其他难以解决的情况,比如多分类问题,如下图所示:
因此,线性分类器的确存在很多问题,但它是一个非常简单的算法,易于使用和理解。 总结:本节中讨论了线性分类器对应的函数形式(矩阵向量相乘),对应于模版匹配和为每一类别学习一个单独的模板,一旦有了这个训练矩阵,可以用他得到任何新的训练样本的得分。
思考问题:如何给数据集选择一个正确的权重?这里包括损失函数和一些其他的优化方法,将在下一章中继续讨论。