k近邻算法的Python实现
0. 写在前面
这篇小教程适合对Python与NumPy有一定了解的朋友阅读,如果在阅读本文的源代码时感到吃力,请及时参照相关的教程或者文档。
1. 算法原理
k近邻算法(k Nearest Neighbor)可以简称为kNN。kNN是一个简单直观的算法,也是机器学习从业者入门首选的算法。先看一个简单的应用场景。
小例子
设有下表,命名为为表1
电影名称
打斗镜头数量
接吻镜头数量
电影类型
foo1
3
104
爱情片
foo2
2
100
爱情片
foo3
1
81
爱情片
foo4
101
10
动作片
foo5
99
5
动作片
foo6
98
2
动作片
一个朴素的愿望是,能够根据打斗镜头与接吻镜头的数量来推测一部电影是属于爱情片还是动作片。具体而言,如果有一部电影的相关信息如下,命名为表2:
电影名称
打斗镜头数量
接吻镜头数量
foo7
18
90
我们能否给出这部电影的类型?
解决方案
表1可以抽象为一个矩阵A与一个列向量x如下:
矩阵A
foo13104
foo22100
foo3181
foo410110
foo5995
foo6982
列向量x
爱情片
爱情片
爱情片
动作片
动作片
动作片
表2可以抽象为一个行向量a如下:
行向量a
foo7 18 90
显然,可以求矩阵A中每一个行向量与行向量a的欧式距离(本例中计算欧式距离时只考虑打斗镜头数量与接吻镜头数量两个分量),并按照距离由小到大排序,结果如下表,命名为表3:
电影名称
与未知电影之的距离
foo2
18.7
foo3
19.2
foo1
20.5
foo4
115.3
foo5
117.4
foo6
118.9
此时选择前k个距离最小的电影及其所属的类型,结果如下表,命名为表4:
电影名称
类型
foo2
爱情片
foo3
爱情片
foo4
爱情片
找出表4中出现次数最多的类型——“爱情片”,即kNN认为行向量a所属的类型为爱情片。
2. Python实现
代码的核心部分是如下函数,将其保存在文件中mykNN.py中。
import numpy as np
import operator as op
from collections import defaultdict
def classify(vec, dataSet, labels, k):
"""
要求dataSet为NumPy的array类型
vec: 参照行向量a
dataSet: 参照矩阵A
labels: 参照列向量x
k: kNN中选择前k小的行
"""
size = dataSet.shape[0]
assert size == len(labels) #断言,确保输入正确
tmp = (dataSet - vec)**2 #使用了NumPy的广播机制
tmp = tmp.sum(axis=1)
tmp = tmp.argsort()
tmpDict = defaultdict(int) #简化用于分组的代码
for i in range(k):
tmpDict[labels[tmp[i]]] += 1
return max(tmpDict.items(),key=op.itemgetter(1))[0]
3. 练手案例
我们使用第2小节的代码解决第1小节的问题。下面的代码文件保存为test.py,请确保test.py与mykNN.py文件位于同一个路径下。
import numpy as np
import mykNN as knn
if __name__ == "__main__":
dataSet = np.array([
[3, 104],
[2, 100],
[1, 81],
[101, 10],
[99, 5],
[98, 2]
])
labels = ["爱情片", "爱情片", "爱情片",
"动作片", "动作片", "动作片"]
k = 3
vec = [18, 90]
res = knn.classify(vec,dataSet,labels,k)
print(res)
4. 补充说明
真实的分类任务不会像我们的案例那样简单。
一般来说,第3小节中的dataSet与labels都会放在文件或者数据库中,并且未必是NumPy可以处理的数据类型。这时需要增加读文件或者读数据库并解析转换数据的一系列代码。
有时需要考虑对表格的不同字段归一化的问题。
以数据驱动的应用的开发需要关注kNN算法的正确率,这时需要增加判断正确率或者错误率的代码。