KNN算法详解并自主构建kd树及sklearn简单实现

k-近邻算法(KNN)

一、简介

1、定义

核心思想:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。(近朱者赤,近墨者黑)

2、分类

有监督学习、多分类算法

3、简单流程

(1)计算已知类别数据集中的样本与当前样本的距离。
(2)按顺序递增排序。
(3)选取距离最小的k个点。
(4)统计这k个样本类别出现的频率最高的类别。
(5)出现的频率最高的类别即为预测分类。

4、优缺点


简单有效
重新训练代价低
适合类域交叉样本
适合大样本自动分类(不适合类域较小的样本)

惰性学习
类别评分不是规格化
输出可解释性不强
对不均衡的样本不擅长(一般若达到4:1,就说不均衡)
计算量较大

二、距离公式

1、欧氏距离:

在m维空间中两个点之间的真实距离
欧氏距离体现数值上的绝对差异欧氏距离公式
缺点:它将样品的不同量纲之间的差别等同看待

2、曼哈顿距离(城市街区距离)

用以标明两个点在标准坐标系上的绝对轴距总和
曼哈顿距离公式

3、切比雪夫距离

各坐标数值差绝对值的最大值
切比雪夫距离公式

4、闵可夫斯基距离

闵可夫斯基距离不是一种距离,而是一组距离的定义,是对多个距离度量公式的概括性的表述。
闵可夫斯基距离公式
p=1时为曼哈顿距离
p=2时为欧氏距离
p=无穷大时为切比雪夫距离

注意:
(1)闵氏距离与特征参数的量纲(单位)有关,有不同量纲的特征参数的闵氏距离常常是无意义的。
(2)闵氏距离没有考虑特征参数间的相关性,而马哈拉诺比斯距离解决了这个问题。

5、标准化欧氏距离

解决了欧氏距离将样品的不同量纲之间的差别等同看待的缺点

Sk:当前分量标准差
eg:求(0,5)(1,6)(2,7)中(0,5)(1,6)的距离,Sk1=0,1,2标准差,Sk2=5,6,7标准差。
ps:若将1/Sk^2看作一个权重,也可称为加权欧氏距离。

6、余弦距离

余弦距离体现方向上的相对差异。将数据映射为高维度的向量,余弦值接近1,夹角趋于0,表明两个向量越相似,余弦值接近于0,夹角趋于90度,表明两个向量越不相似。

7、汉明距离

两个不同字符串,将其中一个变为另一个所需要做的最小字符替换数。
对两个字符串进行异或运算,并统计结果为1的个数,那么这个数就是汉明距离。
ps:汉明重量是字符串相对于同样长度的零字符串的汉明距离,也就是说,它是字符串中非零的元素个数:对于二进制字符串来说,就是 1 的个数,所以 11101 的汉明重量是 4。
汉明距离更多的用于信号处理,表明一个信号变成另一个信号需要的最小操作。

8、杰卡德距离

杰卡德距离(Jaccard Distance) 是用来衡量两个集合差异性的一种指标,它是杰卡德 相似系数的 补集,被定义为1减去Jaccard相似系数。而杰卡德相似系数(Jaccard similarity coefficient),也称杰卡德指数(Jaccard Index),是用来衡量两个集合相似度的一种指标。

9、马氏距离

表示点与一个分布之间的距离。它是一种有效的计算两个未知样本集的相似度的方法。与欧氏距离不同的是,它考虑到各种特性之间的联系(例如:一条关于身高的信息会带来一条关于体重的信息,因为两者是有关联的),并且是尺度无关的(scale-invariant),即独立于测量尺度。
马氏距离也可以定义为两个服从同一分布并且其协方差矩阵为Σ的随机变量之间的差异程度。
如果协方差矩阵为单位矩阵,那么马氏距离就简化为欧氏距离,如果协方差矩阵为对角阵,则其也可称为正规化的欧氏距离。

μ:均值
Σ:协方差矩阵
协方差矩阵:


为n维随机变量,称矩阵
为n维随机变量 的协方差矩阵(covariance matrix),也记为D(X)
,其中

为 X的分量Xi 和 Xj的协方差(设它们都存在)。
例如,二维随机变量 的协方差矩阵为
所以协方差矩阵为对称非负定矩阵。

性质:

二、Scikit-learn

针对Python 编程语言的免费软件机器学习库 。它具有各种分类,回归和聚类算法,包括支持向量机,随机森林,梯度提升,k均值和DBSCAN,并且旨在与Python数值科学库NumPy和SciPy联合使用。(依赖NumPy和SciPy)
. 分类、聚类、回归
. 特征工程
. 模型选择、调优
sklearn中文社区
sklearn官方文档中文版

三、k值大小选取

在实际应用中,K值一般取一个比较小的数值,例如采用交叉验证法(简单来说,就是一部分样本做训练集,一部分做测试集)来选择最优的K值。

过大:减小估计误差,受到样本均衡的问题,模型简单,增大近似误差。
过小:减小近似误差,容易受异常点的影响,模型复杂,过拟合,增大估计误差。
误差分析:

四、kd树

k近邻法最简单的实现是线性扫描(穷举搜索),即要计算输入实例与每一个训练实例的距离。计算并存储好以后,再查找K近邻。当训练集很大时,计算非常耗时。

1、简介

为了避免每次都重新计算一遍距离,算法会把距离信息保存在一棵树里,这样在计算之前从树里查询距离信息,尽量避免重新计算。其基本原理是,如果A和B距离很远,B和C距离很近,那么A和C的距离也很远。有了这个信息,就可以在合适的时候跳过距离远的点。(减少距离值的计算)

2、原理

构造kd树相当于不断地用垂直于坐标轴的超平面将K维空间切分,构成一系列的K维超矩形区域。kd树的每个结点对应于一个k维超矩形区域。利用kd树可以省去对大部分数据点的搜索,从而减少搜索的计算量。
其原理有点类似于“二分查找”:给出一组数据:[9 1 4 7 2 5 0 3 8],要查找8。如果挨个查找(线性扫描),那么将会把数据集都遍历一遍。而如果排一下序那数据集就变成了:[0 1 2 3 4 5 6 7 8 9],按前一种方式我们进行了很多没有必要的查找,现在如果我们以5为分界点,那么数据集就被划分为了左右两个“簇” [0 1 2 3 4]和[6 7 8 9]。因此,根本就没有必要进入第一个簇,可以直接进入第二个簇进行查找。把二分查找中的数据点换成k维数据点,这样的划分就变成了用超平面对k维空间的划分。空间划分就是对数据点进行分类,“挨得近”的数据点就在一个空间里面。

3、构建

(1)构建根节点,使根节点对应K维空间中包含所有实例的超矩形区域。
(2)构建子节点,使用递归的方法对K维空间进行切分,生成子节点。
(3)重复上述过程,直到子区域内没有实例。
(4)通常循环选择坐标轴对空间进行切分,选择坐标轴上的中位数为切分点,这样得出的kd树是平衡的。(左子树和右子树深度之差绝对值不超过一)
主要问题:
(1)选择向量的哪一维进行划分:最好在数据比较分散的那一维进行划分(方差较大的)
(2)如何划分:一般选择中位数划分

4、查询

从root节点开始,DFS搜索直到叶子节点,同时创建回溯队列,按顺序存储已经访问的节点。
如果搜索到叶子节点,当前的叶子节点被设为最近邻节点。
然后通过队列回溯:
如果当前点的距离比最近邻点距离近,更新最近邻节点.
然后检查以最近距离为半径的圆是否和父节点的超平面相交.
如果相交,则必须到父节点的另外一侧,用同样的DFS搜索法,开始检查最近邻节点。
如果不相交,则继续往上回溯,而父节点的另一侧子节点都被淘汰,不再考虑的范围中.
当搜索回到root节点时,搜索完成,得到最近邻节点。

DFS:
深度优先搜索算法(Depth First Search,简称DFS):一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。

5、kd树构建及查询实现

 import numpy as np
    class TreeNode:
        def __init__(self, s, d):
            self.vec = s  # 特征向量
            self.Dimension = d  # 即划分空间时的特征维度,这里选取方差最大的维度
            self.left = None  # 左子节点
            self.right = None  # 右子节点
            self.father = None  # 父节点(搜索时需要往回退)

        def __str__(self):
            return str(self.vec)  # print 一个 Node 类时会打印其特征向量
#求欧式距离
    def distance(arr1, arr2):
        res = 0
        for a, b in zip(arr1, arr2):
            res += (a - b) ** 2
        return res ** 0.5
#求出最大方差对应下标
    def myvar(data):
        data=np.array(data).T
        maxvar=0
        varindex=0
        index=0
        for i in data:
            if np.var(i)>maxvar:
                maxvar=np.var(i)
                varindex=index
            index+=1
        return varindex
#构建kd树
    def build(arr,  father):
        if len(arr) == 0:  # 样本空间为空则返回
            return None
        #确认分割维度
        l=myvar(data=arr)
        # 找x^l的中位数和对应特征向量,即arr[:][l]的中位数及arr[x][:]
        #取长度
        size = len(arr)
        # 直接对arr进行排序,因为要得到特征向量和划分子空间,由此直接对arr排序最便捷
        # 对l列进行排序
        arr.sort(key=(lambda x: x[l]))
        # 中位数的下标值
        mid = int((size - 1) / 2)
        # 创建节点
        root = TreeNode(arr[mid], l)
        root.father = father
        # 递归创建左右节点
        root.left = build(arr[0:mid] , root)  # 0:mid不包括mid,即[0,mid)
        root.right = build(arr[mid + 1:] , root)
        print(root.left, root.right,root.vec,root.Dimension)
        return root

#dfs寻找当前最近节点
    #root:kd树节点、father:父节点,stack:回溯队列、depth:深度
    def dfs(depth, root, father, stack,target):
        if root == None:
            return father
        stack.append(root)
        if target[root.Dimension]<root.vec[root.Dimension]:
            return dfs(depth + 1, root.left, root, stack,target)
        else:
            return dfs(depth + 1, root.right, root, stack,target)

    #获得回溯队列与当前最近节点
    def mykd(root,target):
        depth=0
        father=None
        mystack=[]
        dfs(depth,root,father,mystack,target)
        return mystack,root

#获得最近邻
    def mynearest(root, target):
        # 获取当前最近邻与回溯队列
        stack, nearest = mykd(root, target)
        # nearest_dis为当前最近邻离target的距离,即最小距离,也是超球体的半径
        nearest_dis = distance(nearest.vec, target)
        visited = {}  # 用来判断兄弟节点是否已经讨论过
        # 利用stack进行回溯,而非递归
        while stack[-1] != root:
            # 取出当前节点
            cur = stack[-1]
            # 将当前节点移出队列
            stack.pop()
            # 定义父亲节点father
            father = cur.father
            # 定义兄弟节点bro
            bro = father.left
            if father.left == cur:
                bro = father.right
            # 如果当前节点与target的距离小于最近距离,则更新最近结点和最近距离
            if distance(cur.vec, target) < nearest_dis:
                nearest = cur
                nearest_dis = distance(cur.vec, target)
            # 若当前节点没有递归过
            if visited.get(hash(cur)) == None:
                # 若超球体和父节点的超平面相交,相交则父节点的另一侧,即兄弟节点所在划分域可能存在更近的节点
                if father.vec[father.Dimension] - target[father.Dimension] < nearest_dis:
                    visited.update({hash(bro): 'yes'})
                    dfs(father.Dimension, bro, father, stack, target)
        return nearest_dis,nearest

    Featureset = [[1, 6, 2],
                  [2, 9, 3],
                  [5, 1, 4],
                  [9, 4, 7],
                  [4, 2, 6],
                  [6, 3, 5],
                  [7, 2, 5],
                  [9, 1, 4]]
    target = [1, 4, 5]

    # 递归构造kd树
    root = build(Featureset,  None)
    #获取最近距离及最近邻
    nearest_dis,nearest=mynearest(root,target)
    print("最近距离:", nearest_dis)
    print("最近邻:", nearest)

五、特征预处理

有时特征的数值或单位相差较大,容易影响预测结果,使得算法无法学习到其他的特征需要进行无量纲化,使其转移到统一规格(归一化,标准化)

1、归一化

对数据进行处理,使其同意映射在某一区间内(默认0~1)

鲁棒性较差,易受到异常点的影响,适合传统精确小数据。
API:sklearn.preprocessing.MinMaxScaler(feature_range=[0,1])
feature_range:范围

#实例化转换器类(归一化)
transfer=sklearn.preprocessing.MinMaxScaler(feature_range=[2,3])
#导入数据
iris_data=transfer.fit_transform(iris.data)

# 把数据转换为dataframe格式
iris_data=pd.DataFrame(data=iris_data,columns=iris.feature_names)
iris_data['Specials']=iris.target

2、标准化

把数据变换到均值为0,标准差为一的范围内


API:sklearn.preprocessing.StandardScaler()

#实例化转换器类(归一化)
transfer=sklearn.preprocessing.StandardScaler()
#导入数据
iris_data=transfer.fit_transform(iris.data)
print(iris_data)

# 把数据转换为dataframe格式
iris_data=pd.DataFrame(data=iris_data,columns=iris.feature_names)
iris_data['Specials']=iris.target

可通过

transfer.var_
transfer.mean_`

查看每一列的方差及均值

六、交叉验证与网格搜索

将训练集再分为n份,每一份都分别作为验证集,
分成几份就叫几折交叉验证。
为了使模型更加准确可信(并不能提高准确率)

超参数:需要手动输入的参数eg:KNN算法的k值
可设置多组超参数,用交叉验证来寻找最优参数组合来建立模型。
API: sklearn.model_selection.GridSearchCV(estimator,param_grid,cv)
estimator:估计器对象
param_grid:估计器对象所需参数(字典)eg:{‘n_neighbors’:[1,2,3]}
cv:几折交叉验证
n_jobs:运行的cpu个数

七、简单示例

1、

(1)导入KNN的api
from sklearn.neighbors import KNeighborsClassifier as KNN

sklearn.neighbors.KNeighborsClassifier(self, n_neighbors=5, *,
weights=‘uniform’, algorithm=‘auto’, leaf_size=30,
p=2, metric=‘minkowski’, metric_params=None, n_jobs=None,
**kwargs)
常用参数: n_neighbors=5 k-近邻算法中k值
algorithm=‘auto’ 选择搜索算法
auto:自动选择
brute:暴力检索
kd tree:kd树,20维以下效率较高
ball tree:克服kd树高位失效问题,每一个节点都是一个超球体

(2)生成简单数据
index0=[ '电影'+str(i) for i in range(1,8)]
columns0=[ chr(ord('A')+i)+'镜头' for i in range(8)]+['电影类型']
data=pd.DataFrame(data=np.random.randint(0,100,(7,9)),index=index0,columns=columns0)
data['电影类型']=['动作类','动作类','喜剧类','喜剧类','喜剧类','爱情类','动作类']
print(data)


>>>
     A镜头  B镜头  C镜头  D镜头  E镜头  F镜头  G镜头  H镜头 电影类型
电影1   99    1   95   47   15   62   45    5  动作类
电影2   94    9   40   18   84   46   84    2  动作类
电影3   20   87   77    9   34   41   56    2  喜剧类
电影4   65   77   25   51   54   81   51   97  喜剧类
电影5   26   12   19   76   27   89   87   59  喜剧类
电影6   23   21   86   70    5   50   24   10  爱情类
电影7   75   20    3   23   65   34   34   96  动作类
(3)训练模型
 #实例化API
    estimator=KNN(n_neighbors=2)
    #使用fit(x,y)方法进行训练:x为特征值,y为目标值
    estimator.fit(X=data.iloc[:,:-1].values,y=data['电影类型'])
  
(4)数据预测
  #使用模型预测分类
    data2=np.random.randint(0,100,(3,8))
    print(estimator.predict(data2))


>>>
['动作类' '喜剧类' '动作类']

2、

(1)导入数据集及查看

sklearn自带一些小数据集,可直接导出

 sklearn.datasets.load_[name]

也可下载一些sklearn较大的数据集

 sklearn.datasets.fetch_[name]

他们的返回值为sklearn.utils.Bunch,类字典型,主要包含
data:特征数据数组,numpy.ndarry二维数组
target:标签数组,numpy.ndarry一维数组
DESCR:数据描述
feature_names:特征数据名
target_names:标签名()新闻数据、手写数字、回归数据集没有
此次我们使用鸢尾花数据集

    from sklearn.datasets import load_iris
    iris=load_iris()
    

查看

def data_descr():
    print(iris.feature_names)
    print(iris.data)
    print(iris.target_names)
    print(iris.target)
    print(iris.DESCR)


data_descr()

使用sepal length (cm), sepal width (cm), petal length (cm),petal width (cm)四个特征值对鸢尾花的种类(Specials)进行预测

(2)查看数据分布

**Seaborn:**基于matplotlib的图形可视化python包。它提供了一种高度交互式界面,便于用户能够做出各种有吸引力的统计图表。
Seaborn是在matplotlib的基础上进行了更高级的API封装,从而使得作图更加容易,在大多数情况下使用seaborn能做出很具有吸引力的图,而使用matplotlib就能制作具有更多特色的图。应该把Seaborn视为matplotlib的补充,而不是替代物。同时它能高度兼容numpy与pandas数据结构以及scipy与statsmodels等统计模式。

导入所需包
ps:seaborn绘图时使用DataFrame,所以需要pandas

import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

使用seaborn.lmplot绘图
lmplot(
*,
x=None, y=None,
data=None,
hue=None, col=None, row=None, # TODO move before data once * is enforced
palette=None, col_wrap=None, height=5, aspect=1, markers=“o”,
sharex=None, sharey=None, hue_order=None, col_order=None, row_order=None,
legend=True, legend_out=None, x_estimator=None, x_bins=None,
x_ci=“ci”, scatter=True, fit_reg=True, ci=95, n_boot=1000,
units=None, seed=None, order=1, logistic=False, lowess=False,
robust=False, logx=False, x_partial=None, y_partial=None,
truncate=True, x_jitter=None, y_jitter=None, scatter_kws=None,
line_kws=None, facet_kws=None, size=None,
)
data:关联到数据集
x、y:对应坐标轴列名
fit_reg=True:是否进行线性回归
col_wrap:指定每行的列数,最多等于col参数所对应的不同类别的数量
aspect:控制图的长宽比
sharex:共享x轴刻度(默认为True)
sharey:共享y轴刻度(默认为True)
hue:用于分类
ci:控制回归的置信区间
x_jitter:给x轴随机增加噪音点
y_jitter:给y轴随机增加噪音点
order:控制进行回归的幂次

# 把数据转换为dataframe格式
iris_data=pd.DataFrame(data=iris.data,columns=iris.feature_names)
iris_data['Specials']=iris.target
def show_dataset(data,col1,col2):
    sns.lmplot(x=col1,y=col2,data=data,hue=data.columns[-1],fit_reg=False)
    plt.xlabel=col1
    plt.ylabel=col2
    plt.title('鸢尾花种类分布图')
    plt.show()
show_dataset(iris_data,iris_data.columns[1],iris_data.columns[2])

(3)数据集划分

API:

sklearn.model_selection.train_test_split()

train_test_split(*arrays,test_size=None,train_size=None,random_state=None,shuffle=True,stratify=None)

return:x_train,x_test,y_train,y_test
arrays:分割对象同样长度的列表或者numpy 的ndarray。
test_size:两种指定方法。1:指定小数。小数范围在0.0~0.1之间,它代表test集占据的比例。2:指定整数。整数的大小必须在这个数据集个数范围内,要是test_size在没有指定的场合,可以通过train_size来指定。(两个是对应关系),默认25%
train_size:和test_size相似。
random_state:随机种子

(4)完整代码
    from sklearn.datasets import load_iris
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.preprocessing import StandardScaler
    from sklearn.model_selection import train_test_split
    from sklearn.model_selection import GridSearchCV
    #导入数据集
    iris=load_iris()
    #数据集划分
    x_train,x_test,y_train,y_test=train_test_split(iris['data'],iris['target'],test_size=0.2,random_state=2)
    #标准化
    transfer=StandardScaler()
    x_train=transfer.fit_transform(x_train)
    x_test=transfer.fit_transform(x_test)
    #实例化估计器
    esitmator=KNeighborsClassifier(algorithm='auto')
    #参数调优
    param_grid={'n_neighbors':[1,2,3]}
    esitmator=GridSearchCV(estimator=esitmator,param_grid=param_grid,cv=3)
    #训练模型
    esitmator.fit(x_train,y_train)
    #模型评估
    #1、直接比较
    pre=esitmator.predict(x_test)
    print(pre)
    print(pre==y_test)
    #2、模型评分(准确率)
    esitmator.score(x_test,y_test)
    #查看参数调优结果
    print('交叉验证最好结果',esitmator.best_score_)
    print('交叉验证最好估计器',esitmator.best_estimator_)
    print('每次的结果',esitmator.cv_results_)
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值