何谓K近邻算法,即K-Nearest Neighbor algorithm,简称KNN算法。单从名字来猜想,可以简单的认为:K个最近的邻居。当K=1时,算法便成了寻找最近的那个邻居。
用官方的话来说,所谓K近邻算法,即是给定一个训练数据集。对新的输入实例,在训练数据集中找到与该实例最邻近的K个实例(也就是上面所说的K个邻居)。这K个实例的多数属于某个类,就把该输入实例分类到这个类中。
如上图所示,有两类不同的样本数据,分别用蓝色的小正方形和红色的小三角形表示。而图正中间的那个绿色的圆所标示的数据则是待分类的数据。也就是说,现在,我们不知道中间那个绿色的数据是从属于哪一类(蓝色小正方形或者红色小三角形)。所以,我们解决问题是给这个绿色的圆分类。
我们常说,物以类聚,人以群分,判别一个人是一个什么样品质特征的人,常常可以从他/她身边的朋友入手,所谓观其友,而识其人。我们不是要判别上图中那个绿色的圆是属于哪一类数据么,好说,从它的邻居下手。但一次性看多少个邻居呢?
从上图中,你还能看到:
+ 如果K=3,绿色圆点的最近的3个邻居是2个红色小三角形和1个蓝色小正方形,基于统计的方法,判定绿色的这个待分类点属于红色的三角形一类。
+ 如果K=5,绿色圆点的最近的5个邻居是2个红色三角形和3个蓝色的正方形,基于统计的方法,判定绿色的这个待分类点属于蓝色的正方形一类。
于此我们看到,当无法判定当前待分类点是从属于已知分类中的哪一类时,我们可以依据统计学的理论看它所处的位置特征,衡量它周围邻居的权重,而把它归为(或分配)到权重更大的那一类。这就是K近邻算法的核心思想。
我们看到,K近邻算法的核心在于找到待测点的邻居。那么,问题就接踵而至了,如何找到邻居,邻居的判定标准是什么,用什么来度量。这一系列问题便是下面要讲的距离度量方式。
这是因为特征空间中两个实例点的距离可以反应出两个实例点之间的相似性程度。K近邻模型的特征空间一般是n维实数向量空间,使用的距离可以是欧式距离,也是可以是其它距离,既然扯到了距离,下面就来具体阐述下都有哪些距离度量的表示法,
最常见的两点之间或多点之间的距离表示法,又称之为欧几里得度量,它定义于欧几里得空间中,如点 x = (x1,…,xn) 和 y = (y1,…,yn) 之间的距离为:
代码如下:
double euclideanDistance(const vector<double>& v1, const vector<double>& v2)
{
assert(v1.size() == v2.size());
double ret = 0.0;
for (vector<double>::size_type i = 0; i != v1.size(); ++i)
{
ret += (v1[i] - v2[i]) * (v1[i] - v2[i]);
}
return sqrt(ret);
}
我们可以定义曼哈顿距离的正式意义为L1-距离或城市区块距离,也就是在欧几里得空间的固定直角坐标系上两点所形成的线段对轴产生的投影的距离总和。例如在平面上,坐标(x1, y1)的点P1与坐标(x2, y2)的点P2的曼哈顿距离为:
通俗来讲,想象你在曼哈顿要从一个十字路口开车到另外一个十字路口,驾驶距离是两点间的直线距离吗?显然不是,除非你能穿越大楼。而实际驾驶距离就是这个曼哈顿距离,此即曼哈顿距离名称的来源。同时,曼哈顿距离也称为城市街区距离(City Block Distance)。
除了上述如何定义邻居的问题之外,还有一个选择多少个邻居,即K值定义为多大的问题。不要小看了这个K值选择问题,因为它对K近邻算法的结果会产生重大影响。如下图所示,X是待分类样本,‘+’和‘-’是样本类别属性,
如果K选小了的话,结果对噪音样本点很敏感。
如果K选大了的话,可能求出来的k最近邻集合可能包含了太多隶属于其它类别的样本点,最极端的就是k取训练集的大小,此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的累,模型过于简单,忽略了训练实例中大量有用信息
在实际应用中,K值一般取一个比较小的数值,例如采用交叉验证法(一部分样本做训练集,而另一部分做测试集,即K值初始取一个比较小的数值,之后不断来调整K值的大小使得分类最优,得到的K值就是最优解。但是这个K值具有一定的局限性)来选择最优的K值。
__author__ = 'Xinyou DONG'
import tensorflow as tf
import numpy as np
def loadMNIST():
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
return mnist
def KNN(mnist):
train_x,train_y = mnist.train.next_batch(5000)
test_x,test_y = mnist.train.next_batch(200)
xtr = tf.placeholder(tf.float32, [None,784])
xte = tf.placeholder(tf.float32, [784])
distance = tf.sqrt(tf.reduce_sum(tf.pow(tf.add(xtr, tf.neg(xte)), 2), reduction_indices=1))
pred = tf.argmin(distance,0)
init = tf.initialize_all_variables()
sess = tf.Session()
sess.run(init)
right = 0
for i in range(200):
ansIndex = sess.run(pred,{xtr:train_x, xte:test_x[i,:]})
print 'prediction is ', np.argmax(train_y[ansIndex])
print 'true value is ', np.argmax(test_y[i])
if np.argmax(test_y[i]) == np.argmax(train_y[ansIndex]):
right += 1.0
accracy = right/200.0
print accracy
if __name__ == "__main__":
mnist = loadMNIST()
KNN(mnist)
正如前面所讲的卷积神经元算法中图像匹配类似,特征点的匹配和数据库查、图像检索本质上是同一个问题,都可以归结为一个通过距离函数在高维向量之间进行相似性检索的问题,如何快速而准确地找到查询点的近邻,不少人提出了很多高维空间索引结构和近似查询的算法。
一般说来,索引结构中相似性查询有两种基本的方式:
+ 一种是范围查询,范围查询时给定查询点和查询距离阈值,从数据集中查找所有与查询点距离小于阈值的数据
+ 另一种是K近邻查询,就是给定查询点及正整数K,从数据集中找到距离查询点最近的K个数据,当K=1时,它就是最近邻查询。
同样,针对特征点匹配也有两种方法:
+ 最容易的办法就是线性扫描,也就是我们常说的穷举搜索,依次计算样本集E中每个样本到输入实例点的距离,然后抽取出计算出来的最小距离的点即为最近邻点。此种办法简单直白,但当样本集或训练集很大时,它的缺点就立马暴露出来了,举个例子,在物体识别的问题中,可能有数千个甚至数万个SIFT特征点,而去一一计算这成千上万的特征点与输入实例点的距离,明显是不足取的。
+ 另外一种,就是构建数据索引,因为实际数据一般都会呈现簇状的聚类形态,因此我们想到建立数据索引,然后再进行快速匹配。索引树是一种树结构索引方法,其基本思想是对搜索空间进行层次划分。根据划分的空间是否有混叠可以分为Clipping和Overlapping两种。前者划分空间没有重叠,其代表就是KD树;后者划分空间相互有交叠,其代表为R树。
1975年,来自斯坦福大学的Jon Louis Bentley在ACM杂志上发表的一篇论文:Multidimensional Binary Search Trees Used for Associative Searching中正式提出和阐述的了如下图形式的把空间划分为多个部分的KD树。
KD树是K-dimension tree的缩写,是对数据点在k维空间(如二维(x,y),三维(x,y,z),k维(x,y,z..))中划分的一种数据结构,主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。
首先必须搞清楚的是,KD树是一种空间划分树,就是把整个空间划分为特定的几个部分,然后在特定空间的部分内进行相关搜索操作。想像一个三维空间,K-D树按照一定的划分规则把这个三维空间划分了多个空间,如下图所示:
再举一个简单直观的实例来介绍KD树构建算法。假设有6个二维数据点{(2, 3),(5, 4),(9, 6),(4, 7),(8, 1),(7, 2)},数据点位于二维空间内,如下图所示。为了能有效的找到最近邻,KD树采用分而治之的思想,即将整个空间划分为几个小部分,首先,粗黑线将空间一分为二,然后在两个子空间中,细黑直线又将整个空间划分为四部分,最后虚黑直线将这四部分进一步划分。
6个二维数据点{(2, 3),(5, 4),(9, 6),(4, 7),(8, 1),(7, 2)}构建KD树的具体步骤为:
+ 确定split域。具体步骤是,6个数据点在x,y维度上的数据方差分别为39,28.63,所以在x轴上方差更大,故split域值为x;
+ 确定Node-data=(7, 2)。具体步骤是,根据x维上的值将数据排序,6个数据的中值(所谓中值,即中间大小的值)为7,所以Node-data域位数据点(7, 2)。这样,该节点的分割超平面就是通过(7, 2)并垂直于split=x轴的直线x=7;
+ 确定左子空间和右子空间。具体步骤是:分割超平面x=7将整个空间分为两部分,x<=7的部分为左子空间,包含3个节点{(2, 3),(5, 4),(4, 7)};另一部分为右子空间,包含2个节点{(9, 6),(8, 1)};
如上算法所述,KD树的构建是一个递归过程,我们对左子空间和右子空间内的数据重复根节点的过程就可以得到一级子节点(5, 4)和(9, 6),同时将空间和数据集进一步细分,如此往复直到空间中只包含一个数据点。
与此同时,经过对上面所示的空间划分之后,我们可以看出,点(7, 2)可以为根结点,从根结点出发的两条红粗斜线指向的(5, 4)和(9, 6)则为根结点的左右子结点,而(2, 3),(4, 7)则为(5, 4)的左右孩子(通过两条细红斜线相连),最后,(8,1)为(9,6)的左孩子(通过细红斜线相连)。如此,便形成了下面这样一棵k-d树:
KD树的数据结构(注,本文以下代码分析基于Rob Hess维护的sift库),
/** a node in a k-d tree */
struct kd_node
{
int ki; /**< partition key index */
double kv; /**< partition key value */
int leaf; /**< 1 if node is a leaf, 0 otherwise */
struct feature* features; /**< features at this node */
int n; /**< number of features */
struct kd_node* kd_left; /**< left child */
struct kd_node* kd_right; /**< right child */
}
也就是说,KD树中,KD代表k-dimension,每个节点即为一个k维的点。每个非叶节点可以想象为一个分割超平面,用垂直于坐标轴的超平面将空间分为两个部分,这样递归的从根节点不停的划分,直到没有实例为止。经典的构造k-d tree的规则如下:
+ 随着树的深度增加,循环的选取坐标轴,作为分割超平面的法向量。对于3-d tree来说,根节点选取x轴,根节点的孩子选取y轴,根节点的孙子选取z轴,根节点的曾孙子选取x轴,这样循环下去。
+ 每次均为所有对应实例的中位数的实例作为切分点,切分点作为父节点,左右两侧为划分的作为左右两子树。
对于n个实例的k维数据来说,建立kd-tree的时间复杂度为O(k*n*logn)。
以下是构建k-d树的代码:
struct kd_node* kdtree_build( struct feature* features, int n )
{
struct kd_node* kd_root;
if( ! features || n <= 0 )
{
fprintf( stderr, "Warning: kdtree_build(): no features, %s, line %d\n", __FILE__, __LINE__ );
return NULL;
}
kd_root = kd_node_init( features, n ); //n--number of features,initinalize root of tree.
expand_kd_node_subtree( kd_root ); //kd tree expand
return kd_root;
}
上面的涉及初始化操作的两个函数kd_node_init,及expand_kd_node_subtree代码分别如下所示:
static struct kd_node* kd_node_init( struct feature* features, int n )
{ //n--number of features
struct kd_node* kd_node;
kd_node = (struct kd_node*)(malloc(sizeof(struct kd_node) ));
memset( kd_node, 0, sizeof( struct kd_node ) ); //0填充
kd_node->ki = -1;
kd_node->features = features;
kd_node->n = n;
return kd_node;
}
static void expand_kd_node_subtree( struct kd_node* kd_node )
{
/* base case: leaf node */
if( kd_node->n == 1 || kd_node->n == 0 )
{ //叶节点 //伪叶节点
kd_node->leaf = 1;
return;
}
assign_part_key( kd_node ); //get ki,kv
partition_features( kd_node ); //creat left and right children,特征点ki位置左树比右树模值小,kv作为分界模值
//kd_node中关键点已经排序
if( kd_node->kd_left )
expand_kd_node_subtree( kd_node->kd_left );
if( kd_node->kd_right )
expand_kd_node_subtree( kd_node->kd_right );
}
元素插入到一个K-D树的方法和二叉检索树类似。本质上,在偶数层比较x坐标值,而在奇数层比较y坐标值。当我们到达了树的底部,(也就是当一个空指针出现),我们也就找到了结点将要插入的位置。生成的K-D树的形状依赖于结点插入时的顺序。给定N个点,其中一个结点插入和检索的平均代价是
我们来考虑如下的例子,用下列的点逐步插入KD树中,
(3, 6),(17, 15),(13, 15),(6, 12),(9, 1),(2, 7),(10, 19)
删除节点(30, 40):因为节点的右子树非空并且比较域是x轴,我们需要在右子树中查询x轴的最小值节点代替当前节点。又因为点(35, 45)是叶子节点,可直接删除。如下图所示:
删除节点(70, 70):因为当前节点的比较域在y轴,且右子树为空,所以我们需要在其左子树中查找对于值最小的节点。将查找的节点(50, 30)代替(70, 70),并将左子树作为当前节点的右子树。如下图所示:
现实生活中有许多问题需要在多维数据的快速分析和快速搜索,对于这个问题最常用的方法是所谓的KD树。在KD树中进行数据的查找也是特征匹配的重要环节,其目的是检索在KD树中与查询点距离最近的数据点。在一个N维的笛卡儿空间在两个点之间的距离是由下述公式确定:
算法:KD树最邻近查找
输入: Kd, //k-d tree类型
target //查询数据点
输出: nearest, //最邻近数据点
dist //最邻近数据点和查询点间的距离
1. If KD为NULL,则设dist为infinite并返回
2. //进行二叉查找,生成搜索路径
Kd_point = &Kd; //Kd-point中保存k-d tree根节点地址
nearest = Kd_point -> Node-data; //初始化最近邻点
while(Kd_point)
push(Kd_point)到search_path中; //search_path是一个堆栈结构,存储着搜索路径节点指针
If Dist(nearest,target) > Dist(Kd_point -> Node-data,target)
nearest = Kd_point -> Node-data; //更新最近邻点
Min_dist = Dist(Kd_point,target); //更新最近邻点与查询点间的距离 ***/
s = Kd_point -> split; //确定待分割的方向
If target[s] <= Kd_point -> Node-data[s] //进行二叉查找
Kd_point = Kd_point -> left;
else
Kd_point = Kd_point ->right;
End while
3. //回溯查找
while(search_path != NULL)
back_point = 从search_path取出一个节点指针; //从search_path堆栈弹栈
s = back_point -> split; //确定分割方向
If Dist(target[s],back_point -> Node-data[s]) < Max_dist //判断还需进入的子空间
If target[s] <= back_point -> Node-data[s]
Kd_point = back_point -> right; //如果target位于左子空间,就应进入右子空间
else
Kd_point = back_point -> left; //如果target位于右子空间,就应进入左子空间
将Kd_point压入search_path堆栈;
If Dist(nearest,target) > Dist(Kd_Point -> Node-data,target)
nearest = Kd_point -> Node-data; //更新最近邻点
Min_dist = Dist(Kd_point -> Node-data,target); //更新最近邻点与查询点间的距离的
End while
将Kd_point压入search_path堆栈;”这行代码后,应该是调到步骤2再往下走二分搜索的逻辑一直到叶结点,我写了一个递归版本的二维kd tree的搜索函数你对比的看看:
void innerGetClosest(NODE* pNode, PT point, PT& res, int& nMinDis)
{
if (NULL == pNode)
return;
int nCurDis = abs(point.x - pNode->pt.x) + abs(point.y - pNode->pt.y);
if (nMinDis < 0 || nCurDis < nMinDis)
{
nMinDis = nCurDis;
res = pNode->pt;
}
if (pNode->splitX && point.x <= pNode->pt.x || !pNode->splitX && point.y <= pNode->pt.y)
innerGetClosest(pNode->pLft, point, res, nMinDis);
else
innerGetClosest(pNode->pRgt, point, res, nMinDis);
int rang = pNode->splitX ? abs(point.x - pNode->pt.x) : abs(point.y - pNode->pt.y);
if (rang > nMinDis)
return;
NODE* pGoInto = pNode->pLft;
if (pNode->splitX && point.x > pNode->pt.x || !pNode->splitX && point.y > pNode->pt.y)
pGoInto = pNode->pRgt;
innerGetClosest(pGoInto, point, res, nMinDis);
}
以两个简单的实例(例子来自图像局部不变特性特征与描述一书)来描述最邻近查找的基本思路。
加号表示要查询的点(2.1, 3.1)。通过二叉搜索,顺着搜索路径很快就能找到最邻近的近似点,也就是叶子节点(2, 3)。而找到的叶子节点并不一定就是最邻近的,最邻近肯定距离查询点更近,应该位于以查询点为圆心且通过叶子节点的圆域内。为了找到真正的最近邻,还需要进行相关的回溯操作。也就是说,算法首先沿搜索路径反向查找是否有距离查询点更近的数据点。
以查询(2.1, 3.1)为例:
一个复杂点了例子如查找点为(2, 4.5),具体步骤依次如下:
上述两次实例表明,当查询点的邻域与分割超平面两侧空间交割时,需要查找另一侧子空间,导致检索过程复杂,效率下降。
一般来讲,最临近搜索只需要检测几个叶子结点即可,如下图所示:
但是,如果当实例点的分布比较糟糕时,几乎要遍历所有的结点,如下所示:
研究表明N个节点的K维KD树搜索过程时间复杂度为: Tworst=O(kn1−1k) 。
同时,以上为了介绍方便,讨论的是二维或三维情形。但在实际的应用中,如SIFT特征矢量128维,SURF特征矢量64维,维度都比较大,直接利用KD树快速检索(维数不超过20)的性能急剧下降,几乎接近贪婪线性扫描。假设数据集的维数为D,一般来说要求数据的规模N满足