k近邻理论与实践

保留初心,砥砺前行

k-nearest neighbor, k-NN是一种可以用于多分类回归的方法。knn是一个很简单的分类方法。

k近邻法的输入是实例的特征向量,也就是特征空间中的点;
k近邻法的输出是实例的类别,可以是多个类。

接下来本文将从:

  • k近邻算法
  • k近邻法的模型
  • kd

三个方面进行阐述。


1. k近邻算法

本文的第一句话,就已经非常明确地告诉了你,k-NN是用来做什么的。机器学习无非就是做这几件事而已,k-NN一下子就可以分类和回归,是不是很牛。

然后介绍了它的输入输出,这个时候你可能就一头雾水了——等等,我连它是什么都不清楚,怎么就输入输出了呢。别急,接下来我就开始介绍k近邻算法。

k近邻算法简单、直观:给定一个训练数据集(其中的实例类别已定),对新的输入实例,在训练数据集中找到与该实例最接近的k个实例,这k个实例的多数属于某个类,就把该输入实例分为这个类。

通过以上的话已经足够理解k近邻,如果说的再具象一些:

  1. 我们需要输入一个这样的数据集
    T = {(x1,y1),(x2,y2),(x3,y3),···,(xN,yN)}
    其中xi是实例的特征向量,yi是xi这个实例对应的类别,有可能是c1,c2,c3,··· , cK
    然后我们要再输入一个需要被分类的实例的特征向量x

  2. 根据给定的某种距离度量,在 T 中找到与x最接近的k个点。

  3. k个点中根据某种分类的决策规则(例如,哪个类别多就是哪个类别)决定x的类别y

  4. 输出x的类别y

从以上的叙述可以看出, k近邻是没有显式的学习过程的。

k近邻的k = 1时,成为最近邻算法。顾名思义,相信聪明的你肯定知道是什么意思了。


2. k近邻法的模型

在前边的介绍中的第2个步骤中,可以看到“某种距离度量”;第3步中有“某种决策规则”;还有k近邻的k究竟是多少。
以上这些都是在了解了 k近邻算法后我们会产生的问题。

没错,这三个问题就是k近邻法的3个基本要素。
下面,我们就从这3个基本要素,探究k近邻法的模型。

2.1. 距离度量

根据k近邻法的思路,我们输入的x与数据集中的某个xi的相似程度,需要通过两个点的距离来反应。

一般使用的是欧氏距离,推广到更一般的情况是Lp距离。

Lp距离的定义是:

Lp(xi,xj) = (∑l=1n|xi(l) - xj(l)|p)1/p

其中xi,xj为特征空间中的两个点。上式就代表了xi和xj间的Lp距离。

p = 2时Lp距离就称为欧氏距离。
欧氏距离简单说就是两点的每一个维度的元素分别做差,再求平方和,最后开根号,是不是很容易。

2.2. k值的选择

算法名叫 k近邻,因此显而易见k值对于该算法的重要性。

先说结论: K值一般取一个较小的值,通常采用交叉验证法来选取最优的k值。

K值的选择会对k近邻法的结果产生较大的影响。

  • 当k较小时:
    优点:学习的近似误差会减小。
    缺点:预测结果会对近邻的实例点非常敏感(我个人的理解是:例如,如果k = 2, 邻近的2个点恰好的不同类的情况下, 预测结果就会出错),k值的减小就意味着整体模型变得复杂,容易发生过拟合。

  • 当k较大时:
    优点:减小学习的估计误差。
    缺点:学习的近似误差会增大,k值的增大意味着整体的模型变得简单,过于简单会完全忽略训练实例中的大量有用信息。


从上图可以看出,k = 3 与 k = 5 时的分类结果是不同的。然而k值并不会在学习过程中自动优化,而是在模型初始化是人为约定好的。因此k值对于k近邻至关重要,需要给予更多的关注。

2.3. 分类决策规则

一般是多数表决,就是说k个实例中哪个类别多,输入的x就是哪一类。


3. kd

重头戏来了,在实现 k近邻时,需要计算输入x与数据集中的每一个xi的距离,从而选择距离最小的k个。当训练集很大或维数很大时,计算会非常耗时。
因此为了提高 k近邻的效率,下面介绍kd树方法来存储训练数据,减少计算次数。

kd树是对k维空间中的实例点进行存储以便对其快速检索的二叉树数据结构,表示对k维空间的一种划分。

对构造kd树的语言描述晦涩难懂,因此直接上算法,再加上一个低维空间的例子,就可以搞懂它到底是怎么回事。

(以下算法描述只为了清晰易懂,并不是严禁的说法)

  • 构造算法:
    输入:k维空间数据集T = {x1, x2, ..., xN},
    其中xi = (xi(1), xi(2), ..., xi(k)), i = 1, 2, 3, ..., N
    输出: kd
  1. 将所有实例点视作根节点。也就是说现在的根节点(为了叙述方便,起个别名叫做节点A)包含了整个k维空间。
  2. 选择一条坐标轴x(1),l = j mod k + 1,其中j为节点的深度,k为空间的维度。
    将节点A中的所有的点的 x(1)轴的中位数找出来,给它起个名字叫做切分点(选择中位数作为切分点叫做平衡kd树)。并且通过切分点画一个与x(1)轴垂直的超平面,将节点A所表示的那个k维空间1分为2。
  3. 把那些被划分到两个子空间的实例点放在2个子节点(节点B和节点C)上(坐标x(1)小于切分点的点放在左子节点,大于的放在右子节点),那些恰巧落在切分超平面上的点,还保留在节点A中。
  4. 上述的2和3步骤,也就是选择坐标轴,选择切分点,划分超平面的动作循环执行下去... ...
    直到,某次划分的两个子区域没有实例点存在,就停止算法。至此,kd树形成。
  • 例题:
    这个例题从低维出发,便于读者想象思考,高维同理。
    使用如下2维空间数据集:
    T = {(2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2)}
    和上述算法构造一个平衡kd树。

对应上述算法的第1步


图-1 对应上述算法的第1步

将根节点1分为2,恰好落在超平面上的点保留在根节点,剩下的点按照划分进入左右子节点


图-2 将根节点1分为2,恰好落在超平面上的点保留在根节点,剩下的点按照划分进入左右子节点

继续上图的划分,在左右两个子节点的基础上继续划分,落在超平面上的点留下,分到两边的点进入下一层的左右子节点


图-3 继续上图的划分,在左右两个子节点的基础上继续划分,落在超平面上的点留下,分到两边的点进入下一层的左右子节点

最后一次划分,左右子节点已经没有实例点的存在了,算法结束


图-4 最后一次划分,左右子节点已经没有实例点的存在了,算法结束
图-5 kd树
  • 搜索算法
    输入:经过上边的构造算法构造出来的kd树,目标点x
    输出:x的最近邻点
图-6 图解搜索算法

上图是构造kd树算法的输出结果,红点代表目标点x。也就是说我们要寻找红色目标点的最近邻。
首先在kd树中找到红色园点所在的子节点,也就是(4,7)那个点所在的叶子节点。
因此把(4,7)作为最邻近。并且以红色目标点为圆心,经过(4,7)画圆。从图中可以看出的是,真正的最近邻一定在这个圆的内部。
你可能会说,从这个图一下不就看出来内部有一个点吗。等等等等,这是你从上帝视角看的图,而真实的数据结构是一棵二叉树,并不是这张图,因此从树上是无法做出这种判断的。
接下来我们就要寻找比(4,7)距离红点更近的点。
根据kd树找到(4,7)父节点的另一个子节点,如果这个子节点的区域与圆不相交,这个子节点的区域就不会存在最近邻点,这种情况下就退回(4,7)父节点的父节点,继续以上的步骤。而从图-5可以看出(4,7)的兄弟节点是(2,3),就在圆内,因此最近邻节点由(4,7)变成(2,3)。因此就可以得到红色圆点的最近邻为(2,3)。
这是低维,少量数据的情况,只是为了清楚地解释搜索算法。可以推广到高维大量数据的情况,实相同的道理。


OK,理论结束,开始实践。
经过上述的讲解,我们已经知道,k近邻算法是没有显示的学习过程的,整个分类过程就是构建kd树和搜索kd树的过程。

k近邻实验,我们使用sklearn中内置的Iris数据集。它由150个实例组成,平均分为3个种类,每个种类50个实例。每个实例分别有4个属性描述。
下面从sklearn中导入Iris数据集的下载器,使用下载器下载数据存入变量iris中。


from sklearn.datasets import load_iris

iris = load_iris()

print iris.data.shape
(150, 4)
print iris.data   #打印数据集中的数据
print iris.target  #打印实例对应的类别
[[ 5.1  3.5  1.4  0.2]
 [ 4.9  3.   1.4  0.2]
 [ 4.7  3.2  1.3  0.2]
 [ 4.6  3.1  1.5  0.2]
... ...
... ...
篇幅原因,中间省略
一共150个实例
... ...
... ...
 [ 6.8  3.2  5.9  2.3]
 [ 6.7  3.3  5.7  2.5]
 [ 6.7  3.   5.2  2.3]
 [ 6.3  2.5  5.   1.9]
 [ 6.5  3.   5.2  2. ]
 [ 6.2  3.4  5.4  2.3]
 [ 5.9  3.   5.1  1.8]]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2]

接下来对数据集进行分割,需要强调的一点是,无论什么数据集,在分割训练集和测试集的时候一定要做到随机采样,避免相同特征的数据聚在一起的情况。

from sklearn.cross_validation import train_test_split

X_train, X_test, Y_train, Y_test = train_test_split(iris.data, iris.target, test_size=0.25, random_state=33)

print 'X_train: ', X_train
print 'X_test: ', X_test
print 'Y_train: ', Y_train
print 'Y_test: ', Y_test
X_train:  [[ 5.   2.3  3.3  1. ]
 [ 4.9  3.1  1.5  0.1]
 [ 6.3  2.3  4.4  1.3]
 [ 5.8  2.6  4.   1.2]
... ...
... ...
... ...
 [ 5.6  3.   4.5  1.5]
 [ 7.7  3.   6.1  2.3]
 [ 5.4  3.4  1.7  0.2]]
X_test:  [[ 5.7  2.9  4.2  1.3]
 [ 6.7  3.1  4.4  1.4]
 [ 4.7  3.2  1.6  0.2]
 [ 6.5  2.8  4.6  1.5]
... ...
 [ 5.8  2.7  5.1  1.9]
 [ 5.7  3.   4.2  1.2]]
Y_train:  [1 0 1 1 1 0 0 1 0 2 0 0 1 2 0 1 2 2 1 1 0 0 2 0 0 2 1 1 2 2 2 2 0 0 1 1 0
 1 2 1 2 0 2 0 1 0 2 1 0 2 2 0 0 2 0 0 0 2 2 0 1 0 1 0 1 1 1 1 1 0 1 0 1 2
 0 0 0 0 2 2 0 1 1 2 1 0 0 1 1 1 0 1 1 0 2 2 2 1 2 0 1 0 0 0 2 1 2 1 2 1 2
 0]
Y_test:  [1 1 0 1 2 2 0 0 2 2 2 0 2 1 2 1 2 0 1 2 0 0 2 0 2 2 1 1 2 2 1 1 2 2 2 2 2
 1]

最后就是使用sklearn的k近邻分类器,对测试集进行分类测试:
KNeighborsClassifier函数默认k为5,经过测试,对于这个数据集,当k=7时效果最好。

# 导入数据标准化模块
from sklearn.preprocessing import StandardScaler
# 导入k近邻分类器
from sklearn.neighbors import KNeighborsClassifier
# 标准化训练集
ss = StandardScaler()
X_train = ss.fit_transform(X_train)
# 标准化测试集
X_test = ss.fit_transform(X_test)
# 将knn分类器赋给变量knn,并且参数中选择上文中讲的kd树
knn = KNeighborsClassifier(algorithm='kd_tree')
#knn = KNeighborsClassifier(n_neighbors=7, algorithm='kd_tree')
# Fit the model using X as training data and y as target values
knn.fit(X_train, Y_train)
# 输入测试集进行预测
y_predict = knn.predict(X_test)

print Y_test
print y_predict

第一行打印的是正确的结果
第二行打印的是knn模型预测的结果

[1 1 0 1 2 2 0 0 2 2 2 0 2 1 2 1 2 0 1 2 0 0 2 0 2 2 1 1 2 2 1 1 2 2 2 2 2 1]
[1 1 0 1 1 2 0 0 1 2 2 0 2 1 2 1 1 0 1 1 0 0 2 0 1 1 1 1 1 1 1 1 1 1 2 2 1 1]

错误率:e = 1/m ∑i=1mI(f(xi)≠yi)
由以上结果可以算出e = 10 / 36
模型精度1 - e


如果你也喜欢机器学习,并且也像我一样在ML之路上努力,请关注我,我会进行不定期更新,总有一些可以帮到你。

所有图片均来自网络

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容