K近邻法(K-Nearest Neighbor,以下简称KNN)是由Cover和Hart于1968年提出来的基本分类和回归方法,KNN的基本思想是对于每一个样本,计算与其最邻近的K个样本点,然后基于某种分类规则的的方式将这K个邻近点的类别作为该样本的预测类别,因此,KNN并不具有显式的学习过程。
对于一个给定的训练数据集 T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , ⋯   , ( x N , y N ) } T=\left\{\left(x_{1}, y_{1}\right),\left(x_{2}, y_{2}\right), \cdots,\left(x_{N}, y_{N}\right)\right\} T={(x1,y1),(x2,y2),⋯,(xN,yN)},其中, x i ∈ X ⊆ R n x_{i} \in \mathcal{X} \subseteq \mathbf{R}^{n} xi∈X⊆Rn为实例的特征向量, y i ∈ Y = { c 1 , c 2 , ⋯   , c K } y_{i} \in \mathcal{Y}=\left\{c_{1}, c_{2}, \cdots, c_{K}\right\} yi∈Y={c1,c2,⋯,cK}为实例的类别, i = 1 , 2 , ⋯   , N i=1,2, \cdots, N i=1,2,⋯,N。则对于一个实例向量 x x x,KNN算法将根据指定的距离度量方式,从训练集 T T T中找出与 x x x最邻近的K个点,记涵盖这K个点的 x x x的领域为 N k ( x ) N_{k}(x) Nk(x)。
接着,根据分类决策规则,一般采用多数表决的形式,计算 N k ( x ) N_{k}(x) Nk(x)中的多数类,然后将多数类的类别作为实例 x x x的预测类别 y y y,其计算公式如下:
y = arg max c j ∑ x j ∈ N k ( x ) I ( y i = c j ) , i = 1 , 2 , ⋯   , N ; j = 1 , 2 , ⋯   , K y=\arg \max _{c_{j}} \sum_{x_{j} \in N_{k}(x)} I\left(y_{i}=c_{j}\right), \quad i=1,2, \cdots, N ; \quad j=1,2, \cdots, K y=argcjmaxxj∈Nk(x)∑I(yi=cj),i=1,2,⋯,N;j=1,2,⋯,K
其中 I I I为指示函数,即当 y i = c j y_{i}=c_{j} yi=cj时, I = 1 I=1 I=1,否则, I = 0 I=0 I=0
当K取值为1时,则此时K邻近法也称为最近邻法,此时,对于每一个实例 x x x,其预测类别将直接采用与其最邻近的点的类别。
对于KNN模型,主要有三个基本要素——距离度量、K值和分类规则。对于距离度量,常用的有欧式距离、曼哈顿距离等,用更一般的 L p L_{p} Lp距离表示如下:
L p ( x i , x j ) = ( ∑ i = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{i=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(i=1∑n∣∣∣xi(l)−xj(l)∣∣∣p)p1
其中, x i = ( x i ( 1 ) , x i ( 2 ) , ⋯   , x i ( n ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(n)}\right)^{\mathrm{T}} xi=(xi(1),xi(2),⋯,xi(n))T和 x j = ( x j ( 1 ) , x j ( 2 ) , ⋯   , x j ( n ) ) T x_{j}=\left(x_{j}^{(1)}, x_{j}^{(2)}, \cdots, x_{j}^{(n)}\right)^{\mathrm{T}} xj=(xj(1),xj(2),⋯,xj(n))T分别表示 n n n维向量空间中的两个点,当 p = 2 p=2 p=2时,则相当于欧式距离:
L 2 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ 2 ) 1 2 L_{2}\left(x_{i}, x_{j}\right)=\left(\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right|^{2}\right)^{\frac{1}{2}} L2(xi,xj)=(l=1∑n∣∣∣xi(l)−xj(l)∣∣∣2)21
当 p = 1 p=1 p=1时,则相当于曼哈顿距离:
L 1 ( x i , x j ) = ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ L_{1}\left(x_{i}, x_{j}\right)=\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right| L1(xi,xj)=l=1∑n∣∣∣xi(l)−xj(l)∣∣∣
当 p = ∞ p=\infty p=∞时,则相当于各个坐标距离的最大值:
L ∞ ( x i , x j ) = max l ∣ x i ( l ) − x j ( l ) ∣ L_{\infty}\left(x_{i}, x_{j}\right)=\max _{l}\left|x_{i}^{(l)}-x_{j}^{(l)}\right| L∞(xi,xj)=lmax∣∣∣xi(l)−xj(l)∣∣∣
当采用不同的距离度量方式时,此时对于每个实例 x x x得到的K个近邻点可能是不一样的。
对于K值的选择,当K值选的比较小时,则此时模型容易受到噪声点的影响,估计误差会比较大,当K值选的比较大时,则容易受到不相似点的影响,近似误差比较大,因此,K值的选择过大或过小对结果的影响都比较大,一般来讲,会通过交叉验证法来选择最优的K值。
对于分类规则的选择,一般采用的是多数表决规则。
根据KNN算法的原理我们可以知道,KNN是没有显示的学习过程,因此,对于每一个实例,都要对所有的训练集 T T T进行遍历,当训练集非常多,维度非常大时,此时KNN算法将非常耗费计算资源和计算时间,因此,在实际情况中,我们会采用一些更加高效的方法来实现KNN算法,其中,kd树是最常用的一种,下面具体介绍kd树的构造和搜索过程。
kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构,这里的k指的是向量空间的维度,而不是KNN中的K值。kd树二叉树,对于k维空间训练集 T = { x 1 , x 2 , ⋯   , x N } T=\left\{x_{1}, x_{2}, \cdots, x_{N}\right\} T={x1,x2,⋯,xN},其中 x i = ( x i ( 1 ) , x i ( 2 ) , ⋯   , x i ( k ) ) T , i = 1 , 2 , ⋯   , N x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(k)}\right)^{\mathrm{T}}, \quad i=1,2, \cdots, N xi=(xi(1),xi(2),⋯,xi(k))T,i=1,2,⋯,N,kd树的构建步骤如下:
当kd树勾结完成后,此时对于k维向量空间,会被划分为很多个小的超矩形区域,每个小的超矩形区域即为kd树的叶结点,对于一个新的实例 x x x,此时,先找到他所在的叶结点,然后从该叶结点出发,依次回退到父结点,不断查找与它最邻近的结点,直到回退到根结点为止,这样一来,搜索就相当于被限制在局部区域上,从而使得效率大大提高。具体的搜索步骤如下(以最近邻为例):
在kd树中找出包含目标点 x x x的叶结点:从根结点出发,递归地向下访问kd树。若目标点 x x x当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点,直到子结点为叶结点为止。
以此叶结点为“当前最近点”。
递归地向上回退,在每个结点进行以下操作:
a. 如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”
b. 当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一子结点对应的区域是否有更近的点,具体地,检查另一子结点对应的区域是否与以目标点为球心、以目标点与“当前最近点”间的距离为半径的超球体相交。如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点,接着,递归地进行最近邻搜索; 如果不相交,向上回退。
当回退到根结点时,搜索结束。最后的“当前最近点”即为 x x x的最近邻点。
python中sklearn已经有KNN模型的API接口,因此,本文不再重复造轮子,有关代码的实现可以参见本人的github地址:
未来,该项目将陆续增加机器学习中的其他模型,包括一些sklearn上没有的模型,如果涉及到一些类似的任务,比如分类任务,可以同时对比多个模型的效果。
最后,对KNN模型的优缺点总结如下: