机器学习笔记(八)KNN原理详解和实践

一、KNN原理

K近邻法(k-nearest neighbors,KNN)是一种应用比较多的机器学习算法模型,其核心思想就是未知的对象总是和距离自己最近的群体类似。简单地说就是一个人如果经常和好人走的近,那么我们可以认为(大概率)他是好人,如果他经常和坏人混迹在一起,那么我们就会认为他更可能是坏人,在推荐领域,KNN 可以用来为消费行为相似的人推荐商品。现在还有一个不确定的问题,就是距离最近的群体怎么衡量呢?这也正是KNN中K的含义,就是找距离自己最近的K个样本。这里需要注意 KNN 中的 K 和 K-Means 中的 K 是不同的, K-Means 的 K 表示把样本分为 K 个簇(类别),KNN 的 K 则是为了找最近的 K 个点,不过两者之间也有一些相似点,核心思想都是距离最小化,具体可参考上一篇文章《聚类算法K-means原理和实践》。

由原理可知,KNN 算法可以认为是没有训练过程的,在预测阶段,计算未知点和所有样本的距离,然后选择距离未知点最近的 K 的样本,如果是分类任务,则根据 K 个样本的类别投票决定预测结果,如果是回归任务,则输出 K 个样本的平均值,可以发现 KNN 的输出策略和决策树是一致的,具体可参考《决策树原理和实践》介绍。之所以在这里介绍 KNN 一是为了更好地区分 KNN 和 K-Means,二是决策树的有些知识对于理解 KNN 的原理很有帮助,三是理解 KNN 的 KD 树,对于接下来要介绍的其他聚类算法会更容易理解。这里有一个细节需要注意,如果在近邻样本中,k 和 k+1 样本距离目标点的距离是一样的,但是标签不同,则取哪一个是由在训练集中的顺序决定的。

KNN原理1

1.1 度量距离

在介绍KNN具体原理之前,我们先回顾一下,机器学习领域中常用的距离度量方式,在前面的文章中,例如GBDT篇,我们也多次提到了距离度量方法,这里我们做一个总结。当然,不同的方法,对 KNN 的结果是没有影响的,因为 KNN 找的是最近的点。在统计学中,向量距离度量的方式很多,比较常用的主要有以下几种:

(1)欧式距离,也就大家常说的平方差:

                                                                    D(x,y) = \sqrt{(x_1-y_1)^2 + (x_2-y_2)^2 + ... + (x_n-y_n)^2} = \sqrt{\sum\limits_{i=1}^{n}(x_i-y_i)^2}

(2)曼哈顿距离:

                                                                    D(x,y) =|x_1-y_1| + |x_2-y_2| + ... + |x_n-y_n| =\sum\limits_{i=1}^{n}|x_i-y_i|

(3)闵可夫斯基距离(Minkowski Distance):

                                                                   D(x,y) =\sqrt[p]{(|x_1-y_1|)^p + (|x_2-y_2|)^p + ... + (|x_n-y_n|)^p} =\sqrt[p]{\sum\limits_{i=1}^{n}(|x_i-y_i|)^p}

可以看出,欧式距离是闵可夫斯基距离距离在p=2时的特例,而曼哈顿距离是p=1时的特例。

(4)余弦距离:

                                                                   \cos (\overrightarrow{A},\overrightarrow{B}) = \frac{\overrightarrow{A} \cdot \overrightarrow{B}}{\left | \overrightarrow{A} \right | \left | \overrightarrow{B} \right |} = \frac{\sum_{i=1}^{n}A_iB_i}{\sqrt{\sum_{i=1}^{n}A_{i}^{2}}\cdot \sqrt{\sum_{i=1}^{n}B_{i}^{2}}}

总体来说,欧氏距离体现数值上的绝对差异,而余弦距离体现方向上的相对差异。

(5)切比雪夫距离

                                                                   D(x,y) = max(\left | x_1-y_1 \right |,\left | x_2-y_2 \right |,...)

(6)马氏距离

已知数据样本矩阵为:X = \left [ x_1,x2,...x_n \right ]^T,特征形式表示为:X = \left [ f_1,f_2,...,f_d \right ],即样本数为n,特征数为d,数据集是 n*d 的矩阵。假设样本间的协方差矩阵为:S \in R^{d\times d},那么有:

                                                              

马氏距离的定义为:

                                                                

1.2 KD树

前面我们介绍了 KNN 的原理,但是有一个问题,就是在预测的时候,KNN 不同于传统有监督算法,只要把待预测样本的特征在训练好的模型中计算一遍即可,也不同于K-Means,只需要计算待预测样本和所有簇质心的距离即可,KNN 需要计算待预测样本和所有已知样本的距离,如果样本量有千百万级、特征也达到几千个,即使利用elkan K-means优化,那么这个计算量无疑也是很大的。因此为了优化这个问题,大神们也提出了解决方法,其中之一就是基于 KD 树的近邻搜索。KD树算法包括三步,第一步是建树,第二部是搜索最近邻,最后一步是预测。

1.2.1 KD树构建

KD树建树采用的是从m个样本的n维特征中,分别计算n个特征的取值的方差,用方差最大的第k维特征 n_k 对应的中位数样本 x_{n_k} 来作为根节点。对于所有第k维特征的取值小于 x_{n_k} 的样本,我们划入左子树,对于第k维特征的取值大于 x_{n_k} 的样本,我们划入右子树,对于左子树和右子树,我们采用和刚才同样的办法来找方差最大的特征来做根节点,递归的生成KD树。

例如我们有6个二维特征样本:{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},构建kd树的具体步骤为:

  1. 分别计算两个特征的样本方差:var(x_1) = 6.97var(x_2) = 5.37,可以发现 x_1 的方差更大,因此选择 x_1 作为第一次划分特征
  2. 对 x_1 特征排序后,对应的中位数特征样本为 (7,2),即第一次划分的超平面为 x=7。因此,左子树样本集为:{(2,3), (4,7), (5,4)},右子树样本集为:{(8,1), (9,6)}
  3. 对左右子树重复过程1和2,直到所有样本到划到叶子节点。例如,上面的左子树,x_2 的方差更大,中位数划分样本为:(5,4),则对应的二级左子树为:{(2,3)},二级右子树为:{(4,7)},同样右子树对应的划分点为:(9,6),对应的二级左子树为:{(8,1)}

最终得到的KD树如下:

上面在取中位数的时候,偶数个数时,选取的都是右节点,选取左节点也是可以的。另外,也有一些资料介绍在构建KD树的过程中是依次循环遍历所有特征的,没有基于方差进行特征选择,但是用方差选择后建立的kd树搜索近邻效率更高。因此现在主流 KNN 类库中 KD 树建立都不再是轮流而是基于方差选择了了。

1.2.2 KD树最近邻搜索

在完成KD树构建后,就可以对未知点进行最近邻搜索了。对于一个目标点,我们首先在KD树里面找到包含目标点的叶子节点(或者和目标点一样的点,此时一定是最近邻点,距离为0)。以目标点为圆心,以目标点到叶子节点样本实例的距离为半径,得到一个超球体,最近邻的点一定在这个超球体内部。这里先以两个简单的实例来描述最邻近查找的基本思路。

(1)查询点(2.1,3.1)

  1. 对于点(2.1,3.1)沿着KD树搜索,首先找到叶子节点,根据前面构建的二叉树超平面 x=7和y=4,显然叶子节点为(2,3),此时搜索路径中的节点为<(7,2),(5,4),(2,3)>
  2. 以(2,3)作为当前最近邻点,计算其到查询点(2.1,3.1)的距离为0.1414。以(2.1,3.1)为圆心,以0.1414为半径画圆,如下图所示。发现该圆并不和超平面y = 4交割,因此不用进入(5,4)节点右子空间中去搜索,也意味着点(2.1,3.1)到点(5,4)的距离大于0.1414
  3. 同样,以(2.1,3.1)为圆心,以0.1414为半径画圆也不和超平面y=4相交,因此也不用进入(7,2)右子空间进行查找,所以点(2.1,3.1)的最近邻点时(2,3)

(2)查询点(2,4.5)

  1. 同样先进行二叉查找,先从(7,2)查找到(5,4)节点,在进行查找时是由y = 4为分割超平面的,由于查找点为y值为4.5,因此进入右子空间查找到(4,7),形成搜索路径<(7,2),(5,4),(4,7)>
  2. 计算(4,7)与目标查找点的距离为3.202,(5,4)与查找点之间的距离为3.041,所以(5,4)为查询点的最近点
  3. 以(2,4.5)为圆心,以3.041为半径作圆,如下图所示。可见该圆和y = 4超平面交割,所以需要进入(5,4)左子空间进行查找,将(2,3)节点加入搜索路径中得<(7,2),(2,3)>
  4. (2,3)距离(2,4.5)比(5,4)要近,所以最近邻点更新为(2,3),最近距离更新为1.5。回溯到根结点(7,2),以(2,4.5)为圆心1.5为半径作圆,并不和x = 7分割超平面交割,所以不需要查找根节点的右子树,最近邻点为(2,3)

现在我们来总结一下KD树搜索最近邻节点的步骤:

  1. 对于一个目标点,我们首先在KD树里面找到包含目标点的叶子节点,并形成搜索路径
  2. 计算叶子节点和目标点的距离,以及当前叶节点的父节点和目标点的距离,距离最小的就是当前的最近邻点
  3. 以2中的最小距离为半径,目标点为圆心作圆,判断是否和父节点的分隔超平面相交,若不相交,则无需进入父节点的另一颗子树进行搜索,若相交,则需进入父节点的另一颗子树,计算目标点和另一颗子树节点的距离,并判断和已知最小距离的大小进而决定是否更新最近邻点和最近距离
  4. 根据搜索路径按照步骤3继续往上回溯,直到根节点,得到最近邻点。

1.2.3 KNN预测

基于1.2.1构建的KD树和1.2.2节介绍而KD树最近邻搜索算法,再进行KNN预测的效率无疑就高了很多。在KD树搜索最近邻的基础上,我们选择到了第一个最近邻样本,就把它置为已选。在第二轮中,我们忽略置为已选的样本,重新选择最近邻,这样跑k次,就得到了目标的K个最近邻点。一种优化方法就是,在第一轮寻找最近邻点的过程中,我们已经计算了目标点和搜索路径上所有点的距离,因此实际中不必一定执行K次。例如,在第二个例子中,如果k=3,则根据第一轮的结算结果,最近的3个点显然是{(2,3),(5,4),(4,7)}。

1.3 KNN算法之球树预测

KD树算法虽然提高了KNN搜索的效率,但是在某些时候效率并不高,比如当处理不均匀分布的数据集时。例如下图的KD树:

图中的五角星是目标点,黑色点是叶子节点,显然目标点到黑色点的距离和 y=y1 的超平面相交(虚线圆),需要搜索 y=y1 的上面子树的右边部分。但是如果黑色点再远一点,可以发现红色圆不仅仅和 y=y1 的超平面相交,还和 x=x22 超平面相交,因此,不仅仅需要搜索 y=y1 的上面子树的右边部分,还要搜索 y=y1 的上面子树的左边部分,即 y=y1 的上面子树的全部。为了优化超矩形体导致的搜索效率的问题,大神们又引入了球树,这种结构可以优化上面的这种问题。

1.3.1 球树构建

球树的构建过程如下图所示,具体过程为[2]:

  1.  先构建一个超球体,这个超球体是包含所有样本的最小球体,这里为了减少计算复杂度,构建最小球体的方法常采用计算质心为圆心,距离质心最远样本点的距离为半径的方法得到局部最优解
  2. 从球中选择一个离球的中心最远的点,然后选择第二个点离第一个点最远,将球中所有的点分配到离这两个聚类中心最近的一个上,然后计算每个聚类的中心,以及聚类能够包含它所有数据点所需的最小半径。这样我们得到了两个子超球体,和KD树里面的左右子树对应
  3. 对于这两个子超球体,递归执行步骤2最终得到了一个球树,类似于KD树的分裂

可以看出KD树和球树类似,主要区别在于球树得到的是节点样本组成的最小超球体,而KD得到的是节点样本组成的超矩形体,这个超球体要比对应的KD树的超矩形体小,这样在做最近邻搜索的时候,可以避免一些无谓的搜索

1.3.2 球树最近邻搜索

和KD树一样,球树的搜索过程,也是首先找到包含目标点的最小球体(如果目标点落在了叶子球体的缝隙处,则最小球体就是叶子球体的父球体)和搜索路径,在这个球里找出与目标点最邻近的点。这将确定出目标点距离它的最近邻点的一个上限值,然后跟KD树查找一样,检查兄弟球体。如果目标点到兄弟球体中心的距离超过兄弟球体的半径与当前的上限值之和,那么兄弟球体里不可能存在一个更近的点;否则的话,必须进一步检查位于兄弟球体以下的子树。检查完兄弟球体后,根据搜索路径,我们向父球体回溯,继续搜索最小邻近值。当回溯到根节点时,此时的最小邻近值就是最终的搜索结果。从上面的描述可以看出,KD树在搜索路径优化时使用的是两点之间的距离来判断,而球树使用的是两边之和大于第三边来判断,相对来说球树的判断更加复杂,但是却避免了更多的搜索,这是一个权衡。

1.4 KNN的扩展

有时候我们会遇到这样的问题,即样本中某系类别的样本非常的少,甚至少于K,或者是样本分布不均匀,这导致稀有类别样本在找K个最近邻的时候,会把距离较远的其他样本考虑进来,而导致预测不准确,随着K的增大而更加严重。为了解决这个问题,我们限定最近邻的一个最大距离,也就是说,我们只在一个距离范围内搜索所有的最近邻,这避免了上述问题。这个距离我们一般称为限定半径[2]。但是对于高维特征空间,由于所谓的“维度诅咒”,该方法的有效性较差。

接着我们再讨论下另一种扩展,最近质心算法。这个算法比KNN还简单。它首先把样本按输出类别归类。对于第 L 类的 C_l 个样本。它会对这 C_l 个样本的n维特征中每一维特征求平均值,最终该类别所有维度的n个平均值形成所谓的质心点。对于样本中的所有出现的类别,每个类别会最终得到一个质心点。当我们做预测时,仅仅需要比较预测样本和这些质心的距离,最小的距离质心类别即为预测的类别[2]。这个算法通常用在文本分类处理上。但是该算法不适用于样本较为分散的情况或者说非凸数据集,而且不适用于回归问题。

前面我们讨论了KNN的基本原理,即由最近的K个样本预测结果。但是随着K的增大或者K近邻的样本点距离目标点距离较远的时候,大概率会导致预测精度下降,根本原因是距离目标点越远的样本点,对预测结果的判断误差会越大。因此,KNN也衍生了另一种变种,就是为k个近邻点设置样本权重,距离样本点越远,权重越低,即权重和距离成反比。

1.5 总结

KNN算法是很基本的机器学习算法了,它非常容易学习,在维度很高的时候也有很好的分类效率,因此运用也很广泛,这里总结下KNN的优缺点。

1.5.1 优缺点

(1)优点

  • 理论成熟,简单易用,既可以用来做分类也可以用来做回归,即使没有很高的数学基础也能搞清楚它的原理
  • 训练时间复杂度低,因为是惰性的
  • 可用于非线性分类
  • 对异常点不敏感
  • 由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。但是如果目标点也处于重叠区域的话,也可能导致结果不正确
  • 该算法比较适用于样本容量比较大的类域的自动分类,而那些样本容量较小的类域采用这种算法比较容易产生误分

(2)缺点

  • 对内存要求较高,因为该算法存储了所有训练数据,KD树、球树的建立需要大量的内存,且计算量大,尤其是特征很多的时候(因为需要计算和很多样本之间的距离),导致预测过程效率较低
  • 样本不平衡的时候,对稀有类别的预测准确率低(参考1.4节提到的样本量少于K)
  • 可解释性问题,其实 KNN 的可解释性是取决于已有样本的解释程度的

1.5.2 K的选择

KNN K的选择和其他算法一样,也可以通过交叉验证的方法确定。选择较小的k值,就相当于用较小的领域中的训练实例进行预测,训练误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是泛化误差会增大,对异常值更加敏感,更容易发生过拟合;选择较大的k值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少泛化误差,但缺点是训练误差会增大。这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,尤其是回归问题的精度会下降。一个极端是k等于样本数m,则完全没有分类,此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的类,模型过于简单。

二、实践

sklearn 提供了关于KNN以及扩展KNN的多个API类,接下来我们介绍几个常用的API。

2.1 API

(1)NearestNeighbors在指定数据集中寻找k近邻

sklearn.neighbors.NearestNeighbors(*, 
n_neighbors=5,     ##knn中的k,近邻数
radius=1.0,        ##参数范围,即在目标点的指定半径内查找,也可通过交叉验证来选择
algorithm='auto',  ##查找算法,可选‘auto’, ‘ball_tree’, ‘kd_tree’, ‘brute’。‘brute’表示蛮力实现, ‘auto’则会在上面三种算法中做权衡,选择一个拟合最好的最优算法。需要注意的是,如果输入样本特征是稀疏的时候,无论我们选择哪种算法,最后scikit-learn都会去用蛮力实现‘brute’。
leaf_size=30,      ##这个值控制了使用KD树或者球树时, 停止建子树的叶子节点数量的阈值。这个值越小,则生成的KD树或者球树就越大,层数越深,建树时间越长,反之,则生成的KD树或者球树会小,层数较浅,建树时间较短。默认是30. 这个值一般依赖于样本的数量,随着样本数量的增加,这个值必须要增加。该值越小,越容易欠拟合,越大,越容易过拟合。可以通过交叉验证来选择一个适中的值。如果使用的算法是蛮力实现,则这个参数可以忽略。
metric='minkowski',  ##距离度量,默认是闵可夫斯基距离,欧式距离 “euclidean”,曼哈顿距离 “manhattan”,切比雪夫距离“chebyshev”,带权重闵可夫斯基距离 “wminkowski”,标准化欧式距离 “seuclidean”,马氏距离“mahalanobis”,也可以自定义距离度量回调函数
p=2,                 ##只用于闵可夫斯基距离和带权重闵可夫斯基距离中p值的选择,p=1为曼哈顿距离, p=2为欧式距离。默认为2,即欧氏距离
metric_params=None,  ##一般都用不上,主要是用于带权重闵可夫斯基距离的权重,以及其他一些比较复杂的距离度量的参数
n_jobs=None          ##并行数
)

关于距离的度量方法,可参考sklearn官网介绍。

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)

(2)sklearn也包装了KD树和球树的API

sklearn.neighbors.KDTree(X, leaf_size=40, metric='minkowski', **kwargs)
sklearn.neighbors.BallTree(X, leaf_size=40, metric='minkowski', **kwargs)

(3)KNN

sklearn.neighbors.KNeighborsClassifier(n_neighbors=5, *, 
weights='uniform', ##样本权重,可选‘uniform’, ‘distance’。‘uniform’表示权重一样,‘distance’表示距离越远,权重越低,参考1.4节
algorithm='auto', 
leaf_size=30, 
p=2, 
metric='minkowski', 
metric_params=None, 
n_jobs=None, 
**kwargs)

sklearn.neighbors.KNeighborsRegressor(n_neighbors=5, *, 
weights='uniform', 
algorithm='auto', 
leaf_size=30, 
p=2, 
metric='minkowski', 
metric_params=None, 
n_jobs=None, 
**kwargs)

(4)RadiusNeighborsClassifier 即1.4节介绍的第一种扩展

sklearn.neighbors.RadiusNeighborsClassifier(radius=1.0,  ##查找范围
*, weights='uniform', 
algorithm='auto', 
leaf_size=30, 
p=2, 
metric='minkowski', 
outlier_label=None, 
metric_params=None, 
n_jobs=None, **kwargs)

(5)NearestCentroid 即1.4节介绍的第二种扩展

sklearn.neighbors.NearestCentroid(metric='euclidean', *, shrink_threshold=None)

(6)KNeighborsTransformer

sklearn.neighbors.KNeighborsTransformer(*, 
mode='distance',   ##可选‘distance’, ‘connectivity’。‘distance’表示返回的是距离,‘connectivity’返回的是0/1连接矩阵
n_neighbors=5, 
algorithm='auto', 
leaf_size=30, 
metric='minkowski', 
p=2, 
metric_params=None, 
n_jobs=1)

KNeighborsTransformer将X转换为k近邻的稀疏图,参见下面案例,两种方法的输出是一致的。

from sklearn.neighbors import NearestNeighbors,KNeighborsTransformer
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)
kntf = KNeighborsTransformer(n_neighbors=2, mode='connectivity').fit(X)

print(nbrs.kneighbors_graph(X).toarray())
print(kntf.transform(X).toarray())

2.2 实践

下面是一个使用KNeighborsTransformer + KNeighborsClassifier实现的分类任务,通过交叉验证比较不同的k效果。

from tempfile import TemporaryDirectory
import matplotlib.pyplot as plt

from sklearn.neighbors import KNeighborsTransformer, KNeighborsClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.datasets import load_digits
from sklearn.pipeline import Pipeline

print(__doc__)

X, y = load_digits(return_X_y=True)
n_neighbors_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

# The transformer computes the nearest neighbors graph using the maximum number
# of neighbors necessary in the grid search. The classifier model filters the
# nearest neighbors graph as required by its own n_neighbors parameter.
graph_model = KNeighborsTransformer(n_neighbors=max(n_neighbors_list),
                                    mode='distance')
classifier_model = KNeighborsClassifier(metric='precomputed')

# Note that we give `memory` a directory to cache the graph computation
# that will be used several times when tuning the hyperparameters of the
# classifier.
with TemporaryDirectory(prefix="sklearn_graph_cache_") as tmpdir:
    full_model = Pipeline(
        steps=[('graph', graph_model), ('classifier', classifier_model)],
        memory=tmpdir)

    param_grid = {'classifier__n_neighbors': n_neighbors_list}
    grid_model = GridSearchCV(full_model, param_grid)
    grid_model.fit(X, y)

# Plot the results of the grid search.
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
axes[0].errorbar(x=n_neighbors_list,
                 y=grid_model.cv_results_['mean_test_score'],
                 yerr=grid_model.cv_results_['std_test_score'])
axes[0].set(xlabel='n_neighbors', title='Classification accuracy')
axes[1].errorbar(x=n_neighbors_list, y=grid_model.cv_results_['mean_fit_time'],
                 yerr=grid_model.cv_results_['std_fit_time'], color='r')
axes[1].set(xlabel='n_neighbors', title='Fit time (with caching)')
fig.tight_layout()
plt.show()

KNeighborsTransformer已经计算好了k近邻距离稀疏矩阵,因此,KNeighborsClassifier 的 metric 参数为 'precomputed'。

Classification accuracy, Fit time (with caching)

三、近邻成分分析

近邻成分分析(Neighbourhood Component Analysis,NCA)是由Jacob Goldberger和Geoff Hinton等大神们在2005年发表的一项工作,属于度量学习(Metric Learning)和降维(Dimension Reduction)领域,其目的是对KNN的优化。其关键点可以概括为:任务是KNN Classification,样本相似度计算方法基于马氏距离(Mahalanobis Distance),参数选择方法为留一验证法(Leave One Out)。最后模型可以学习样本的低维嵌入表示(Embedding),既属于度量学习范畴,又是降维的过程[3]。

3.1 NCA原理

在介绍 NCA 之前,先回归一下KNN的内容,我们取目标点最近的k个样本点投票预测结果。前面也提到这里主要有两个问题,一是当特征维度很高时,计算会变得很复杂,而是当样本分布不均匀或者同类样本不集中时预测结果会变差。针对这两个问题有没有方法解决呢?大神们提出了两个设想:

  1. 把距离计算变简单:降低维度,就像深度学习一样,通过一个全连接网络(或者函数)进行矩阵降维。
  2. 自动学习出一个计算距离方式,使得和目标点类别相同的样本距离目标点更近。

其实这就是NCA的核心思想了。NCA 看起来和 PCA 很像,其实原理也很像,也用到了降维的思想。接下来我们详细介绍怎么量化上面的两点思想,转化为数学逻辑。首先,NCA 引入了距离度量概率的方法来量化概率,给定数据集 \left \{ (x_1,y_1),(x_2,y_2),...,(x_n,y_n) \right \},x_i\in R^d 表示第 i 个样本,y_i\in R 表示样本标签。则样本 i 的 k 近邻样本概率分布为:

                                                                   p_{i j} = \frac{\exp(-|| x_i - x_j||^2)}{\sum\limits_{k \ne i} {\exp{-(||x_i - x_k||^2)}}} , \quad p_{i i} = 0

即 p_{ij} 表示样本 i 和近邻样本 j 属于同一类的概率,这里利用 softmax 的方法把距离转化为概率,之所以加负号,因为距离越近,影响越大,即概率越大。则样本 i 被正确预测的概率为:

                                                                   p_{i}=\sum\limits_{j \in C_i}{p_{i j}}

C_i 表示和 i 属于同一类的样本集。则所有样本被正确判断的概率为:

                                                                   f = \sum_{i=1}^{n} p_{i}=\sum_{i=1}^{n} \sum\limits_{j \in C_i}{p_{i j}}

现在我们得到了一个目标函数,但是我们还有两个问题:第一,这个目标函数没有可学习的参数,如果仅仅为了选择合适的 k ,完全没必要大费周折;第二,前面提到的第一个问题,高维特征下的计算复杂问题还没有解决。我们通过引入距离度量变换矩阵A得到下面变化后的概率公式:

                                                                   p_{i j} = \frac{\exp(-||A x_i - A x_j||^2)}{\sum\limits_{k \ne i} {\exp{-(||A x_i - A x_k||^2)}}} , \quad p_{i i} = 0

其实,NCA 可以看作是学习马氏距离的平方,因为:

                                                                   || A(x_i - x_j)||^2 = (x_i - x_j)^T A^T A (x_i - x_j) = (x_i - x_j)^TM(x_i - x_j)

也就是马氏距离的表达形式,M 是一个半正定矩阵。接下来就可以根据参数 A 对 f 进行优化了,记 g_{ij} = \exp (-|| A(x_i - x_j)||^2),则:

由矩阵求导公式:

可得:

前半部分为:

后半部分为:

上面之所以用s替代k是为了不容易混淆,s可以换成任意字符,因为分母是一个定值。

所以:

则:

化简如下:

至此,我们就得到了目标函数关于A的梯度,根据目标函数和梯度,自然就可以得到转换矩阵 A,同时把训练集X转换为:X^{'} = AX,即低维张量。上式还可以进一步化简变换,这里不再讨论,详细可参考文献[3]。

3.2 实践

sklearn也提供了NCA的相关API,通过下面的案例可以发现NCA与KNN结合后的效果比单纯使用KNN的效果更好。

sklearn.neighbors.NeighborhoodComponentsAnalysis(
n_components=None,  ##线性变换的投影空间维度,转换矩阵A的大小,默认是样本的特征大小
*, 
init='auto',   ##线性变换的初始化,可选‘auto’, ‘pca’, ‘lda’, ‘identity’, ‘random’。一般默认使用auto,会根据n_components大小选择合适的初始化方法,如果n_components <= n_classes 使用 ‘lda’,如果n_components < min(n_features, n_samples),使用 ‘pca’,否则使用‘identity’
warm_start=False,   ##用于在前一次运行训练的基础上,继续迭代
max_iter=50,   ##优化目标函数的最大迭代次数
tol=1e-05,     ##优化器的损失误差
callback=None, ##优化器每次迭代后调用此函数,可以用来保存每次迭代后的转换矩阵
verbose=0, 
random_state=None)
from sklearn.neighbors import NeighborhoodComponentsAnalysis
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y,stratify=y, test_size=0.7, random_state=42)
nca = NeighborhoodComponentsAnalysis(random_state=42)
nca.fit(X_train, y_train)
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)
print(knn.score(X_test, y_test))
knn.fit(nca.transform(X_train), y_train)
print(knn.score(nca.transform(X_test), y_test))

0.9333333333333333
0.9619047619047619

可以发现,n_components和n_features一致,效果也比单独使用KNN更好,类似于深度学习中,增加了非线性表达能力。

参考资料

[1] https://www.cnblogs.com/wqbin/p/10744277.html

[2] https://www.cnblogs.com/pinard/p/6061661.html

[3] https://zhuanlan.zhihu.com/p/48371593

[4] https://blog.csdn.net/seavan811/article/details/46915551

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值