原文章为scikit-learn中"用户指南"-->"监督学习的第六节:Nearest Neighbors"######
sklearn.neighbors
提供了一些在无监督和有监督学习中基于近邻的学习方法。无监督近邻是许多其他学习方法的基石,特别是在流学习和光谱聚类方面。有监督的基于近邻的学习有两个方面:对带有离散标签的数据进行分类 ,对带有连续标签的数据计算回归。
最近邻的原则上是先找出距离新点(预测点)最近的预定数量的训练样本,继而预测其所属的标签。样本数可以是用户所定义的常数(此处即为K近邻算法,KNN),或基于点的局部密度而变化的值(基于半径的近邻学习)。近邻的距离通常是可以通过任何度量方式来测量:所以一般使用标准欧几里得距离来判断点与点的距离。近邻方式同样以非泛化机器学习方式被人所知,因为它会简单的“记住”了所有的训练数据(可能是被转化成一种能够快速索引的格式,例如 Ball Tree 或 KD Tree)。
尽管看起来他的实现原理看起来很简单,但是近邻已经成功的应用在一些大规模的分类和回归问题上了,例如手写文字识别和卫星图像场景。因为它是一个无参数方法,所以通常能够很成功地应用在一个有无规则的决策边界的分类问题上。
sklearn.neighbors
能够同时接受** Numpy 数组或 scipy.sparse **矩阵作为输入。对于密集矩阵同样也支持其超大的可能距离度量。而对于稀疏矩阵则支持搜索任意的 Minkowski 度量。
这里有许多学习事务使用近邻作为他们的计算核心。一个例子是核密度估计,我们会在 密度估计 一节中来进行讨论。
1.6.1. 无监督近邻#
NearestNeighbors
实现了一个无监督近邻学习。它作为一个统一的接口应用在了三种不同的近邻算法中,分别是:BallTree
,KDTree
和 sklearn.metrics.pairwise
中一个基于事务的brute方法。可以通过关键字** algorithm 来选择需要使用哪种近邻搜索算法,可选值有: [‘auto’, 'ball_tree', 'kd_tree', 'brute'] 。在输入默认值 'auto' **时,会自动从这些算法中试图找到对训练集最合适的算法。如果想要知道知道这些算法各自的长短处的话,可以查看这篇近邻算法文章。
警告:关于近邻算法,如果两个邻居,k + 1** 和 ** k 拥有着相同的距离,但却属于不同的标签时,计算结果将根据训练数据的排序来决定。
1.6.1.1. 寻找最近邻##
对于在两组数据之间寻找最近邻的简单任务, 可以使用 sklearn.neighbors
这一无监督算法来完成:
>>> from sklearn.neighbors import NearestNeighbors
>>> import numpy as np
>>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
>>> nbrs = NearestNeighbors(n_neighbors=2, algorithm='ball_tree').fit(X)
>>> distances, indices = nbrs.kneighbors(X)
>>> indices
array([[0, 1],
[1, 0],
[2, 1],
[3, 4],
[4, 3],
[5, 4]]...)
>>> distances
array([[ 0. , 1. ],
[ 0. , 1. ],
[ 0. , 1.41421356],
[ 0. , 1. ],
[ 0. , 1. ],
[ 0. , 1.41421356]])
所以查询集跟训练集一样,因为最靠近的点就是他自身(距离为0)。
另外还可以使用它来有效地产生表示相邻点之间关系的稀疏图:
>>> nbrs.kneighbors_graph(X).toarray()
array([[ 1., 1., 0., 0., 0., 0.],
[ 1., 1., 0., 0., 0., 0.],
[ 0., 1., 1., 0., 0., 0.],
[ 0., 0., 0., 1., 1., 0.],
[ 0., 0., 0., 1., 1., 0.],
[ 0., 0., 0., 0., 1., 1.]])
因为我们的数据集是经过结构化了,所以使得数据点会顺序排列在参数空间旁边,得出K个邻居的一个近似对角矩阵(也就是上方代码块所排列的矩阵,因为自己与自己必定"相邻",所以对角线是1)。这样的稀疏图在各种情况下都是很有用的,能够在无监督学习的情况下利用各样本点的空间关系:尤其是 sklearn.manifold.Isomap
,sklearn.manifold.LocallyLinearEmbedding
和 sklearn.cluster.SpectralClustering
对这三个类来说。
1.6.1.2. KD树和Ball树类##
另外一种寻找最近邻的可选方式是使用 KDTree
或 BallTree
类。这些类是 NearestNeighbors
经过包装后生成的类。Ball树和KD树都拥有着相同的接口(即有着相同的使用方式),以下方的KD树为例:
>>> from sklearn.neighbors import KDTree
>>> import numpy as np
>>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
>>> kdt = KDTree(X, leaf_size=30, metric='euclidean')
>>> kdt.query(X, k=2, return_distance=False)
array([[0, 1],
[1, 0],
[2, 1],
[3, 4],
[4, 3],
[5, 4]]...)
可以在 KDTree
和 BallTree
的文档里找到更多关于邻居搜索的设置,包括如何规范化搜索策略,各种距离度量的规范等等。对于所有可用的矩阵列表,可用查看 DistanceMetric
类的文档。
1.6.2. 近邻分类#
基于邻居的分类是基于实例的学习或非泛化学习的一种:它不需要试图去构造一个通用的泛化模型,而是通过使用简单的储存训练样本的实例来进行分类。这种分类是遍历每个样本点,然后对其的"最近邻居"进行简单的多数表决来进行分类:查询点通过与其最相邻,最能代表它的点来分配它的所属类。
scikit-learn 实现了两种不同的近邻分类器:
-
KNeighborsClassifier
实现了对每个查询点的基于** K 近邻算法,其中 K **为通过用户输入的整数。 -
RadiusNeighborsClassifier
实现了一个基于每个训练点的固定半径** r 内的"邻居"的数量的学习,其中 r **是用户输入的浮点数。
KNeighborsClassifier
里的 **K 邻分类是上述两种技术中最常用的一个。K 的最优值是与数据高度依赖的:一般来说 K **值越大就越能够降低噪音对模型的影响,但同样也会使分类边界变得不明显。
当数据样本不均匀时,使用 RadiusNeighborsClassifier
中基于半径的邻居分类也许是一个比较好的选择。用户要指定一个固定的半径值** r **,这样位于稀疏近邻内的样本点会使用对应的邻居数量(应该是根据半径来搜索)来进行分类。但是对高维参数空间来说,因为存在所谓的"维度灾难"(过多的特征导致分类的误差增大),所以使用这一方式有可能会变使分类得低效。
基于近邻分类的实现类在默认情况下都有着一致的设置权重的方法:也就是说分配到查询点的值是只通过计算其附近邻居点的多数表决(统计)来得到的。在某些情况下,最好是通过对"邻居"添加一个权重,使得越"近"的邻居对拟合的贡献更大。所以这一操作可以通过关键字** weight 来完成。一般情况下这个参数的默认值是 uniform ,此时会使得所有"邻居"都有相同的权重;而当该参数设置为 distance **时,权重值以取反比的方式,距离查询点越进的点权重越高。当然也可以通过用户自定义的权重来对“邻居”进行计算。
示例
- 近邻分类: 一个使用近邻算法来进行分类的例子。
1.6.3. 近邻回归#
近邻回归能够用在数据标签是连续的而不是离散变量的情况下。这种标签的分配方式为其值是根据查询点的“邻居”标签的平均数来决定的。
scikit-learn实现了两种不同的近邻回归:
-
KNeighborsRegressor
实现了一个基于每个查询点的** K 近邻学习,其中 K **是通过用户输入的整数。 -
RadiusNeighborsRegressor
实现了一个基于查询点在固定半径** r 内的近邻学习,其中 r **是一个用户指定的浮点数。
基于近邻回归的实现类在默认情况下都有着一致的设置权重的方法:对查询点来说,所有样本点对他的贡献度都是一致的。在某些情况下,最好是通过对"邻居"添加一个权重,使得越"近"的邻居对拟合的贡献更大。所以这一操作可以通过关键字** weight 来完成。一般情况下这个参数的默认值是 uniform ,此时会使得所有"邻居"都有相同的权重;而当该参数设置为 distance **时,权重值以取反比的方式,距离查询点越进的点权重越高。当然也可以通过用户自定义的权重来对“邻居”进行计算。
有关多路输出的近邻回归可以在 使用多路输出估计器的脸部补全 这篇文章中找到其功能演示。在这个例子中,输入** X 是包括脸部的上半部分,输出 Y **则是脸部的下半部分。
例子
- 近邻回归: 一个使用近邻回归的例子。
- 使用多路输出估计器的脸部补全: 一个使用多路输出的近邻回归例子。
1.6.4. 近邻算法#
1.6.4.1. 暴力算法(BF)##
在机器学习中,如何快速地计算近邻一直都是一个很活跃的研究课题。最原始的近邻搜索所涉及到的就是使用BF去计算数据集中的所有点对之间的距离:对于** D 维中的 N 个样本,其规模有 O(D·N^2) 。高效的BF近邻搜索对于小数量的数据样本来说还是很具有竞争力的。但是随着样本数量 N 的赠面积,BF就会越来越快的变得不可行。在类 sklearn.neighbors
里,BF近邻搜索可以通过设置关键字 algorithm='brute' **来实现,并且使用 sklearn.metrics.pairwise
中的事务来进行计算。
1.6.4.2. K-D 树##
为了解决BF在大规模数据下的低效的计算能力,已经发明了各种基于树的数据结构。一般来说,这些结构都通过其结构来有效地编码样本的聚合距离的信息来试图减少需要计算的距离数量。其基本思想是,如果一个样本点** A 对另一个样本点 B 的距离很遥远,但是样本点 B 的又很靠近点 C ,因此我们可以知道点 A 同样距离点 C 很遥远,而这样就不需要再额外计算他们的距离。所以使用这种方式后,近邻的计算成本就减少为 O[D·N·log(N)] 或更好。这在大规模 N **的情况下比BF有着更显著的效果。
KD树(K维树的简称)数据结构则是一种应用了这种综合信息的早期方法,其使用生成二维的四叉树和三维的八叉树来匹配任意维度的数据。KD树是一种二叉树结构,其沿着数据的轴递归地分割参数空间,然后再将其分配到原来的区域内,最后再填充数据点。KD树的构建过程是非常快的:因为只会沿着数据轴来进行分区,不需要计算** D 维的距离。一旦构建完成后,查询点的近邻只需要计算 O[Log(N)] 的距离就足够了。因为KD树在低维度( D < 20,但是这里的低维度相对于BF而言是高维度的)下的近邻搜索非常快,但还是会因为 D 的增加而该搜索过程会变得不怎么有效率。造成这个原因的同样也是因为"维度灾难"的存在。在scikit-learn里,KD树近邻搜索可以通过关键字 algorithm 将其设置为 td_tree **来使用,这会使得计算过程是通过 KDTree
类来完成。
引用
- “Multidimensional binary search trees used for associative searching”, Bentley, J.L., Communications of the ACM (1975)
1.6.4.3. Ball树##
同样为了解决KD树在高维度下的低效能,ball树数据结构被发明了出来。相对于KD树的分区是沿着笛卡尔轴来进行分割,Ball树则是将数据分割成一系列的超球体。当然这会使得Ball树的开销比KD树要打,但是却获得了一种对高度数据化的数据仍然具有高效的数据结构,即使是用于处理拥有高维的数据也不例外。
Ball树的递归分割是通过定义一个质心** C (如果不知道什么是质心,可以查看该网址的Q1)和半径 r ,使得节点中的每个点都位于由 r 和 C 定义的超球体内。然后使用三角不等式**来减少用于邻居搜索的候选点的数量:
这样测试点到质心的距离就足以用来决定该测试点与其近邻距离的上下限。因为Ball树的节点就是球形的几何,所以它能够在高维上执行KD树,虽然实际性能是高度依赖于训练数据的结构。在scikit-learn,可以通过使用设置关键字** algorithm 为 ball_tree **来使用基于Ball树的近邻搜索,且它是通过 sklearn.neighbors.BallTree
类来进行计算的。不过用户也可以直接使用 BallTree
类来进行计算。
引用
- “Five balltree construction algorithms”, Omohundro, S.M., International Computer Science Institute Technical Report (1989)
1.6.4.4. 近邻算法的选择##
对给定的数据集选择一个的最佳算法是一个很复杂的过程,要考虑的因素有多个:
- 样本的数量** N (即 n_samples)和维度 D (即n_features **)
- BF查询时间的增长为** O[D·N]**
- Ball树查询时间的增长近似为** O[D·log(N)] **
- KD树查询时间以一种难以表达的方式进行改变:
- D <= 20 左右 时,近似于 O[D·log(N)]
- D > 20时,可能接近于** O[D·N] **或者是更糟。
对小数据集(N <= 30 左右),**log(N) 与 N 相当,并且此时的BF比基于树的搜索更具效率。为了应对这种情况,KDTree和BallTree都通过提供参数 叶子大小 **来解决这个问题:这个参数能够控制在处理多少样本数时切换算法到BF。这样处理的话允许每一个算法都能够在小数据集上获得BF的高效率。
- 数据的结构:数据的内在维度与其稀疏性。内在维度是指数据的流形维度(d <= D),其可以以线性或者非线性的形式来嵌入参数空间中。稀疏性是指数据在参数空间的填充程度(这将与在“稀疏”矩阵中使用的概念不同。数据上的矩阵可能不是零,但是他在** 结构 **上也是有可能“稀疏”的)
- BF的查询时间跟数据的结构无关。
- Ball树和KD树的查询时间跟数据的结构有很大的关系。一般来说,有较小的内在维度的稀疏数据能够使用很少的处理时间,因为KD树的内部是与参数轴对其的,他一般不会在对有任意结构的数据上,像Ball树那样有着太多的性能提升。
因为用于机器学习的数据集一般是高度结构化的,所以很适合使用基于树的算法来进行查询搜索。
- 查询点所需要的近邻数量** K**:
- **K **的值不会影响BF的查询时间。
- 当** K 值增加时,Ball树和KD树的查询时间会变得越来越慢。这是因为两方面的原因:第一,越大的 K 值会导致需要在参数空间搜索的部分变大。第二,对K > 1**的情况下,需要对结果遍历排序成树。
当** K 变得比 N **还要大时,修建基于查询结果所生成的树的能力会降低。在这种情况下,BF查询可能会更有效。
- 查询点的数量。Ball树和KD树同样需要一个构造阶段。但是在面对分摊查询所消耗的时间上,在构造时所消耗的时间对此相比也不多了。如果只是进行少量的查询的话,那么构造的时间就会在总体时间内占据很大的比例了。如果只需要查询少量的几个点的话(即查询点旁边的点的数量确实不怎么多),此时BF比所有基于树的算法要好。
基于这些情况,在** algorithm='auto' 的情况下,如果 K < N / 2 且 'effective_metric_' 存在于 'kd_tree' 中的 'VALID_METRICS' 列表,则会自动设置成 kd_tree 。如果 K < N / 2 且 'effective_metric_' 不存在于 'kd_tree' 中的 'VALID_METRICS' 列表,则会自动设置成 ball_tree。如果 K >= N / 2 且查询的点数量至少与训练点的数量相同,并且 leaf_size 接近默认值 30 时,则会设置为 'brute' **。
1.6.4.5 **leaf_size **的影响##
综上所述,对于小的样本数来说,BF比其他基于树的算法要更有效率。基于这个现状可以通过在Ball树或KD树的叶子节点数量来从内部切换搜索算法到BF。可以通过设置** leaf_size **参数来切换的标准。这个参数的取值会导致以下几种影响:
- 构造时间
越大的** leaf_size **会有更快的构造时间,因为这样只需要创建更少的节点。 - 查询时间
对很大或很小的** leaf_size 都会获得一个次优的查询成本。对于 leaf_size 接近1的情况,遍历所有节点所需的开销会显著的减慢查询所需的时间。对 leaf_size 接近训练集的数目时,就基本上是直接使用BF了。一个比较好的妥协方法是将其设置为 30,并且这也是 leaf_size **的默认值。 - 内存
随着** leaf_size 增加,需要用来储存树结构的内存也会下降。这一点对Ball树来说尤其重要,因为Ball树的每一个节点都是存放有一个 D 维的质心球体。BallTree
所需要的内存近似为训练集大小的 1 / leaf_size **倍。
**leaf_size **不会在BF查询里使用。
1.6.5. 最近邻质心分类器#
NearestCentroid
分类器是一个用其成员的质心来代表每一类的简单的算法的实现。但是在实际上,这个操作类似于** sklearn.KMeans **算法的标签更新阶段。同样这个算法因为不需要选择参数这一点,使得它同样也是一个良好的基准分类器。当类之间有着太大的方差就好收到非凸类的影响,因为在假设的情况下,在所有维度中的方差都是相等的。对于没有做这个假设的更复杂的方法,可以查看 线性判别法 和 二次判别分析法
。以下是使用默认的 NearestCentroid
例子:
>>> from sklearn.neighbors.nearest_centroid import NearestCentroid
>>> import numpy as np
>>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
>>> y = np.array([1, 1, 1, 2, 2, 2])
>>> clf = NearestCentroid()
>>> clf.fit(X, y)
NearestCentroid(metric='euclidean', shrink_threshold=None)
>>> print(clf.predict([[-0.8, -1]]))
[1]
1.6.5.1. 最近收缩质心##
NearestCentroid
分类器拥有一个** shrink_threshold 参数,其实现了一个最近收缩质心分类器。实际上每个质心的每个特征值被其类内的特征方差所划分。然后特征值就会根据 shrink_threshold **的值来减少。最显著的是如果一个特定的特征值越过0值,那么其值会被设置为0。实际上这操作直接消除了毫无作用的特征。因此这可以用来去除无用的噪音特征。
在线的路子,使用一个较小的收缩阈值将模型的准确度从0.81增加到0.82.
示例
- 近邻质心分类: 一个使用了拥有不同收缩阈值的近邻质分类的例子。
1.6.6. 近似最近邻#
在处理低维度** d (值近似50)的问题上,有许多高效且精确的近邻搜索算法可以使用。但是这些算法都存在当 d **增加时,相对的占用空间和查询时间都会增加。在高维空间中,这些算法跟遍历数据集中查询点旁的所有点的算法(也就是 BF了)并没有什么优越的地方。造成这一点的原因自然是“维度灾难”这一现象了。
对一些应用来说其并不需要获得有多精确的近邻,相反他只需要做出一个“合格的猜测”就足够了。当预测结果不需要太过于精确时,LSHForest
类实现了一个近似近邻搜索。近似近邻搜索是设计来试图加速在高维数据中的查询速度。这项技术在表现"邻里关系",而不是需要精确的结果(例如K近邻分类与回归)时十分有用。一些受欢迎的近似近邻搜索技术都是局部敏感哈希,即一种基于best bin first(BBF)和Balanced box-decomposition tree的搜索技术。
1.6.6.1. 局部敏感哈希树##
局部敏感哈希的Vanilla实现拥有一个在实践中难以调整的超参数,因此scikit-learn实现了一种叫 LSHForest
的变体算法,其拥有较多的合理超参数。这两种方式都是用了内部随机超平面去把样本索引成"桶",并且只计算样本的实际的余弦相似性,以此来对查询结果进行碰撞以实现一个子线性的对样本缩放(详细情况可以查看局部敏感哈希的数学描述)。
LSHForest
有两个主要的超参数:**n_estimators 和 n_candidates **。可以通过下图来证明这些参数能够用来控制查询的准确度:
根据经验来看,一个用户可以通过设置** n_estimators 为一个足够大的值(例如10到50之间),然后在查询时间来调整 n_candidates **的值以平衡准确度。
对小数据集来说,BF仍旧要比LSH树要快。但是LSH树可以通过索引的大小来拥有一个可收缩的子线性的查询时间。LSH树与BF的性能平衡点取决于数据集的维度结构,所需的精度,运行环境的差别,例如BLAS优化的可用性,CPU的性能及其缓存。
在固定的 LSHForest
参数下,随着数据集的增加,查询结果的准确度倾向于缓慢下降。上述图表中的错误条代表在不同查询情况下的标准差。
示例
- 近似最近邻的超参数: 一个使用LSH树的近似最近邻搜索的超参数的行为例子。
- 近似最近邻的可扩展性: 一个使用LSH数的近似最近邻的可伸缩性的例子。
1.6.6.2. 局部敏感哈希的数学描述##
局部敏感哈希(LSH)技术能够使用在很多领域上,其中最多的是用在高维中来执行近邻搜索。LSH背后的主要概念是使用多个(通常是简单的)哈希函数来哈希数据集中的每个数据点,将其形成一个索引列表。对于哈希碰撞的可能性,当两个对象有相似的索引时,彼此距离相近的两个节点比起其他点而言产生碰撞的概率要高得多。散列函数族对局部敏感的要求如下。
从一个域** S 到另一个范围 U 的一系列函数族 H 被称为(r, e, p1, p2)-敏感,其中 r, e > 0, p1 > p2 > 0 ,如果对任意 p, q ∈ S ,都满足以下条件(D **是距离函数):
- 如果** D(p, q) <= r ,且 P[h(p) = h(q)] >= p1**。
- 如果** D(p, q) < r(1 + e) ,且 P[h(p) = h(q)] >= p1**。
跟定义的一样,距离为** r 之内的点很容易以 p1 的概率发生碰撞。相反,距离大于 r(1 + e) 的点则是会以以 p2 的小概率发生碰撞。
假设这里有一个LSH的函数族 H**。LSH索引的建立如下:
- 从** H 中随机均匀选择出 k 个函数(h1, h2, ..., hk)。对任意的 p ∈ S ,使用标签 g(p) = (h1(p), h2(p), ..., hk(p)) 来替换掉原来的 p。如果每个 hi 输出一位数字,可以观察到每个桶都会有 k **位数字的标签。
- 然后再独立执行步骤1,ι次去构造出ι个分别具有** g1, g2, ..., gl **哈希函数的估量器。
在步骤1中使用连续的哈希函数的原因是尽可能地减少于远处节点的碰撞概率。然后碰撞的概率从** p2 降低到 p2^k,并且是对于 k 来说是可以忽略的小数。k 值的选择强烈取决于数据集的大小和结构,因此在数据集中很难以对 k 调参。对较大的 k **值来说还有一个副作用就是:它有可能会降低临近点发生碰撞的几率。为了解决这个问题,所以就在步骤2中构造了多个估量器。
因为需要对给定的数据集的** k **进行调参,所以在实践中很难应用经典的LSH算法。所以就有一种设计来缓解这个问题的LSH树的变种,其通过自动地调整哈希样本所需的位数来解决这个问题。
LSH树是由前缀树形成的,树的每个叶子节点代表着数据库中实际的数据点。使用** ι 个这样树组成树林,然后再使用从 H 中随机抽取的哈希函数来独立地构造他们。在这个实例中使用了随机投影**作为LSH的技术,其为余弦距离的近似值。哈希函数序列的长度被固定为32。此外还使用了已排序的数组和折半搜索实现了一个前缀树。
为了找出查询点** q 的 m 个临近点,需要使用树的两个遍历阶段。首先,从一个自顶向下的遍历来用折半搜索找出经过哈希函数处理过后的 q 标签的最长前缀(最大深度)叶子。然后从森林中提取出M 远大于 m的节点(候选集),然后再自底向上的遍历中同步地将找到的节点向根部移动。其中 M 是设置为 c·l,其中 c 是从树中提取出的候选集的常数数量。最后这些 M 个点各自对 q 点的相似度,然后返回前 m 个最相似的点作为 q 点的近邻。由于在查询近邻的过程中,大部分的时间都是花费在计算点之间的距离,所以这个方法比起BF搜索要快 N / M 左右,其中 N **是数据集中的数量。
引用
- “Near-Optimal Hashing Algorithms for Approximate Nearest Neighbor in High Dimensions”, Alexandr, A., Indyk, P., Foundations of Computer Science, 2006. FOCS ‘06. 47th Annual IEEE Symposium
- “LSH Forest: Self-Tuning Indexes for Similarity Search”, Bawa, M., Condie, T., Ganesan, P., WWW ‘05 Proceedings of the 14th international conference on World Wide Web Pages 651-660
(在尝试翻译这篇文档的时候难免会因为各种问题而出现错翻,如果发现的话,烦请指出,谢谢> <)