基本概念:
k近邻法是一种基本分类与回归方法,属于判别模型。k值的选择、距离度量及分类决策规则是k近邻法的三个基本要素。
k近邻法的特殊情况是k=1的情形,称为最近邻算法,对于输入的实例,将最邻近的点的类作为x的类别。
1.距离度量
特征空间中两个实例点的距离是其相似程度的反应,常用的距离是欧式距离,也可以是其他距离,比如余弦距离或者Lp距离或者Minkowski距离。
Lp距离定义为
当p=2时,称为欧式距离,即:
当p=1时,称为曼哈顿距离,即:
当p为无穷大时,是各个坐标距离的最大值,即:
2.k值的选择
当选择较小的k值时,学习的近似误差会减小,只有与输入实例相似度高的数据才会对结果有作用,但问题是估计误差会增大,容易发生过拟合,因为对近邻的数据点特别敏感,噪声对预测结果的干扰增强。选择较大值时,较远的实例也会影响预测结果,容易发生错误。一般选择一个较小的k值,通常使用交叉验证来选择合适的k值。
3.分类决策规则
k近邻中决策规则一般是多数表决,即通过k个邻近的值中占据大多数比重的值的类别,当做输入实例的类别。
使用knn实现分类代码如下:
from numpy import *
import numpy as np
import operator
# 余弦距离
def cosdist(vector1, vector2):
return dot(vector1, vector2)/(linalg.norm(vector1)*linalg.norm(vector2))
'''
testdata:测试数据
trainSet:训练集
listClasses:训练数据对应标签
K:邻居数
'''
def classify(testdata, trainSet, listClasses, k):
dataSetSize = trainSet.shape[0] # 返回样本集的行数
distances = array(zeros(dataSetSize))
for indx in range(dataSetSize):
distances[indx] = cosdist(testdata, trainSet[indx])
sortedDistIndicies = argsort(-distances) # 从大到小排序,结果为索引号
classCount = {}
for i in range(k):
voteIlabel = listClasses[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0)+1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
使用knn最简单的方法就是线性扫描,但是当训练集很大时,计算非常耗时,曾经用训练好的模型测试了3000个数据用了三天。因此如何实现快速搜索是主要问题,可以考虑使用特殊的结构存储训练数据,具体方法很多,下面实现一下kdtree,网上对kdtree的介绍很详细,因此直接上代码:
kdtree的最邻近实现(其中在进行维度划分的时候可以考虑两种方式:一种是循环维度进行划分,另一种是计算不同维度的方差,选择最大方差对应的维度进行划分,都进行了实现):
其中最邻近查找的方法中,neararray应该使用可修改的参数类型,比如list或者nparray。在学习knn的时候看了李航老师的统计学习方法,但是上面老师觉得树结构的例子的兄弟节点只有一个,兄弟节点没有子树,所以不能按照网上说的递归进行二叉查找,然后在进行回溯。因为对于兄弟节点有子树的情况,只能遍历兄弟节点,无法进入子树,所以应该全部使用递归。这算是遇到的一个大坑,因为这样实现,在进行k邻近的时候,完全没法搞定。
import numpy as np
from collections import Counter
import math
# 实现kdtree
class Kdtree_node:
def __init__(self, partitionValue=None, partitionDimention=None, left=None, right=None, isLeaf=False):
self.partitionValue = partitionValue # 分割值
self.partitionDimention = partitionDimention # 分割维度
self.left = left # 左子树
self.right = right # 右子树
self.isLeaf = isLeaf # 叶子节点标志位
# 计算方差,对常规方差公式进行化简,然后采用numpy的向量运算计算,计算速度快
def getVariance(valuelist):
length = len(valuelist)
narray = np.array(valuelist)
sum1 = narray.sum()/length
narray2 = narray*narray
sum2 = narray2.sum()/length
var = sum2 - sum1**2
return var
# 递归创建kdtree方法
def create_kdtree(root, datalist):
data_length = len(datalist)
if data_length == 0:
return
# 根据方差确定数据点的维度
if flag == 0:
varianceList = [] # 方差
for i in range(len(datalist[0])):
data_Value = [ii[i] for ii in datalist]
data_Variance = getVariance(data_Value)
varianceList.append(data_Variance)
partitionDimention = varianceList.index(max(varianceList)) # 获得多维度方差列表中最大值得索引,即为切分的维度
else:
global deepheight
# xy轴循环作为数据点的维度
partitionDimention = deepheight % len(datalist[0])
deepheight += 1
datalist.sort(key=lambda x: x[partitionDimention])
position = math.floor(data_length/2) # 向下取整,获取切分的位置
# print(datalist[position])
root = Kdtree_node(datalist[position], partitionDimention)
# 如果数据长度为1即为叶子节点
if data_length == 1:
root.isLeaf = True
return root
# print(datalist[:position])
root.left = create_kdtree(root.left, datalist[:position]) # 构建左子树
# print(datalist[position+1:])
root.right = create_kdtree(root.right, datalist[position+1:]) # 构建右子树
return root
# 计算两点之间的欧氏距离
def getdistance(alist, blist):
return np.sqrt(np.sum(np.square(np.array(alist) - np.array(blist))))
#遍历打印kdtree
def printPre(root):
print(root.partitionValue)
if root.left:
printPre(root.left)
if root.right:
printPre(root.right)
#查询最近邻的值
def findNearList(root,neararray,querypoint,mindist):
'''
:param root:父亲节点
:param neararray: 可变类型的最近点
:param querypoint: 查找点
:param min_dist: 最小距离
:return:
'''
if root is None:
return
partitionValue = root.partitionValue
if len(querypoint) != len(partitionValue):
print("error,dimention is not euqal")
return
min_distance = getdistance(querypoint, partitionValue)
if mindist[0] > min_distance:
neararray = partitionValue
mindist[0] = min_distance
# 获取当前节点的划分维度
partitionDimention = root.partitionDimention
print(partitionValue,partitionDimention,mindist[0])
if querypoint[partitionDimention] < partitionValue[partitionDimention]:
findNearList(root.left, neararray, querypoint, mindist)
else:
findNearList(root.right, neararray, querypoint, mindist)
if abs(querypoint[partitionDimention] - root.partitionValue[partitionDimention]) < mindist[0]:
if querypoint[partitionDimention] <= root.partitionValue[partitionDimention]:
findNearList(root.right, neararray, querypoint, mindist)
else:
findNearList(root.left, neararray, querypoint, mindist)
print(partitionValue, mindist)
kdtree实现k邻近:
刚开始对于最近邻参考了一下网上的例子,进行递归查找然后回溯,在k邻近的时候根本没法实现。如果最邻近方法写对了,k邻近就很简单了。维护一个有k个数据的数组或者列表,设置比较大的一个默认值,小于这个值的距离都插入,然后进行升序和降序排列。最邻近算法在比较距离的时候是和最小距离比较的,在k邻近中,与数组中最大值进行比较,小于最大值的就替换掉最大值,然后排序,递归操作。代码如下:
def findKNearList(root,neararray,querypoint,k):
'''
:param root:父亲节点
:param neararray: 可变类型的最近点
:param querypoint: 查找点
:param min_dist: 最小距离
:return:
'''
if root is None:
return
partitionValue = root.partitionValue
if len(querypoint) != len(partitionValue):
print("error,dimention is not euqal")
return
min_distance = getdistance(querypoint, partitionValue)
temparray = neararray[np.argsort(-neararray[:, 2])]
for i in range(k):
neararray[i] = temparray[i]
if neararray[0][2] >= 99999 or neararray[0][2] >min_distance:
neararray[0][2] = min_distance
neararray[0,0:2] = partitionValue
# 获取当前节点的划分维度
partitionDimention = root.partitionDimention
if querypoint[partitionDimention] < partitionValue[partitionDimention]:
findKNearList(root.left, neararray, querypoint, k)
else:
findKNearList(root.right, neararray, querypoint, k)
if abs(querypoint[partitionDimention] - root.partitionValue[partitionDimention]) < neararray[0][2]:
if querypoint[partitionDimention] <= root.partitionValue[partitionDimention]:
findKNearList(root.right, neararray, querypoint, k)
else:
findKNearList(root.left, neararray, querypoint, k)
deepheight = 0 # 树的深度,用于循环维度做切分
flag = 1 # 标志位,0为方差,1位维度循环
data = [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]]
root = None
k = 3 # 邻居值
root = create_kdtree(root, data, deepheight)
neararray = np.zeros((k,3)) # 索引0和1位置存储的是最近点,索引2存储的是最小距离
neararray[:,2] = 99999 # 默认值
findKNearList(root, neararray, [6, 1], k)
neararray = neararray[np.argsort(-neararray[:, 2])]
print(neararray)