本节首先介绍KNN的基本原理,包括距离公式,kd树等。然后对kNN进行python实现。接着我们阅读分析sklearn中的knn源码。比较自己的代码和人家的差距。最后用成熟的KNN代码做一些实验。
从我们的玩具代码的实验来看,影响knn精度的因素主要有两个
1. k值的选取。一般来讲k值越大,则模型越简单,泛化能力越好,但容易欠拟合;k值越小,模型越复杂,拟合效果好,但是泛化能力不够。一般的,我们可以通过交叉验证的方法选取合适的k值。
2. 距离度量方案。可以看到,在我们的数据集中,使用欧式距离,精度可以达到70%,而使用cosin距离精度只有36%。
实验数据到这里下载
在进行实验之前,我们需要先对txt数据进行一个转换,代码如下(import numpy as np):
raw_data = np.loadtxt("./knnTestData.txt")
np.save('data.npy',raw_data)
接下来,我们首先编写一个玩具代码。之所以说我们写的代码是玩具代码,原因在于我们还没有在knn类中加入kd树的部分。因此对于规模较大的数据,实验时间会很长。在这个代码中,我们设计了两种度量距离的方案,欧式距离(euclidean,默认距离)和余弦距离(cosin)。你可以分别使用两种距离方案,看一看实际的效果。从这个数据集来看,用前者效果要比后者好。
在主函数部分,你可以打印分类出错的部分,也可以稍微修改后打印所有的分类结果(这部分代码被注释掉)。我们在主函数中,主要想要探讨一下K取值从10到200之间,分类的精度变化情况。最后给出一个测试图。
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@author: XiangguoSun
@contact: sunxiangguodut@qq.com
@file: KNN.py
@time: 2017/3/28 8:40
@software: PyCharm
"""
from sklearn.model_selection import train_test_split
from collections import Counter
from numpy.linalg import norm
import numpy as np
import matplotlib.pyplot as plt
class KNN(object):
'''
self.k = kwargs.pop('k',1)
self.distance = kwargs.pop('distance','cosin')
self.train_data = kwargs.pop('train_data',None)
self.train_label = kwargs.pop('train_label',None)
self.test_data = kwargs.pop('test_data',None)
self.test_label
self.neighbor_data
self.neighbor_label
'''
def __init__(self,**kwargs):
self.k = kwargs.pop('k',1)
self.distance = kwargs.pop('distance','euclidean')
self.train_data = kwargs.pop('train_data',None)
self.train_label = kwargs.pop('train_label',None)
self.test_data = kwargs.pop('test_data',None)
if kwargs:
raise TypeError('you give an unexpected keyword''argument "{0}"'.format(list(kwargs.keys())[0]))
def get_input(self,test_data):
self.test_data = test_data
def get_neighbor(self):
if self.distance == 'cosin':
vector1 = self.train_data
vector2 = self.test_data
dominator = norm(vector1, axis=1).reshape((vector1.shape[0], 1)) * norm(vector2)
dominator = np.where(dominator == 0,float('inf'),dominator)
distance = np.dot(vector1, vector2).reshape((vector1.shape[0], 1)) / dominator
min_index = distance.reshape((distance.shape[0],)).argsort()[0:self.k]
self.neighbor_data,self.neighbor_label = train_data[min_index],train_label[min_index]
elif self.distance == 'euclidean':
vector1 = self.train_data #(100L,3)
vector2 = self.test_data # (3L,)
distance = np.sqrt(((vector1 - vector2) ** 2).sum(1))
min_index = distance.reshape((distance.shape[0],)).argsort()[0:self.k]
self.neighbor_data, self.neighbor_label = train_data[min_index], train_label[min_index]
def get_label(self):
self.get_neighbor()
self.test_label = Counter(self.neighbor_label.reshape((self.neighbor_label.shape[0],))).most_common(1)[0][0]
return self.test_label
if __name__ == '__main__':
# raw_data = np.loadtxt("./knnTestData.txt")
# np.save('data.npy',raw_data)
fulldata = np.load('data.npy')
data, label = np.split(fulldata, (3,), axis=1)
train_data, test_data, train_label, test_label = train_test_split(data, label, random_state=1, train_size=0.2)
precise = []
for k in xrange(10, 200, 5):
knn = KNN(k=k, train_data=train_data, train_label=train_label)
erro = 0
num = test_data.shape[0]
for ite in xrange(0,num):
knn.get_input(test_data[ite])
guess = knn.get_label()
if test_label[ite][0]!= guess:
#print erro," actual label: ",test_label[ite][0]," || guess label: ",guess
erro = erro + 1
#print ite, " actual label: ", test_label[ite][0], " || guess label: ", guess
print "total tests: ",num,"\n erro: ",erro
precise.append((k,(num-erro)*1.0/num*100))
print "precise:\n",(num-erro)*1.0/num*100,"%"
K = []
P = []
for it in precise:
K.append(it[0])
P.append(it[1])
print 'K= ', it[0]," precise: ",it[1]
plt.figure()
plt.plot(K, P)
plt.show()
事实上,K取125时,可以得到最高的准确率83.375%
你可以将上面的代码稍作修改,就可以得到K=125时的分类情况:
接下来,本文将要实现kd树部分。并且在原来的数据集上,比较采用kd树策略和不采用kd树策略时运行的时间差异。在做完这项工作后,我们开始分析一些KNN的优秀开源代码(事实上,在sklearn中有专用的knn包:from sklearn.neighbors import KNeighborsClassifier
;还有有封装的包可以直接返回最近邻from sklearn.neighbors import NearestNeighbors
)。