小瓜讲机器学习——分类算法(四)K近邻法算法原理及Python代码实现

4.K近邻法的算法原理

分类问题的K近邻法算法原理本质其实很简明,就是计算与预测点“距离”最近K个已知点(训练样本),通过统筹这些点的类别,决定预测点的应该分到哪一类中去。
用数学语言来定义,即如下所述。

K近邻算法:
输入:训练样本 T ( X , Z ) = { ( X ( 1 ) , Z ( 1 ) ) , ( X ( 2 ) , Z ( 2 ) ) , . . . , ( X ( n ) , Z ( n ) ) } T(X, Z)=\{(X^{(1)},Z^{(1)}),(X^{(2)},Z^{(2)}),...,(X^{(n)},Z^{(n)})\} T(X,Z)={(X(1),Z(1)),(X(2),Z(2)),...,(X(n),Z(n))}
   其中特征向量 X ( i ) = ( x 1 ( i ) , x 2 ( i ) , . . . , x l ( i ) ) X^{(i)}=(x_1^{(i)},x_2^{(i)},...,x_l^{(i)}) X(i)=(x1(i),x2(i),...,xl(i)), Z Z Z是分类标签。
   待预测点 X ^ = ( x ^ 1 , x ^ 2 , . . . , x ^ l ) \hat X=(\hat x_1,\hat x_2,...,\hat x_l) X^=(x^1,x^2,...,x^l)
过程:① 计算待预测点 X ^ \hat X X^与训练样本点的“距离”,选择最近的K个训练样本点;
   ② 统筹考虑K个训练样本点的分类类别,决定待预测点应该分属于哪一类。
输出:待预测点 X ^ \hat X X^的分类 Z ^ \hat Z Z^

综上所述K近邻法针对的多分类问题,而且最重要的一点是K近邻法不需要训练模型,几乎是直接拿到训练样本,在上面定义距离和设定K值就可以对待预测点进行分类预测了,这是K近邻法对比其他分类方法最大的不同之处(也是最大的优点)。

当然我这里用了几乎,主要是由于在K近邻法中为了减少距离计算的遍历,需要对训练样本建立树结构的索引机制,所以实际应用中还是会基于训练样本进行预先“训练”(虽然严格意义上不是训练,应该说是构建训练样本的索引数据结构)。

4.1 K近邻法的“距离”

K近邻算法中得先定义距离算法,才能开展接下来的工作。
最常见的距离是欧式距离,定义为特征向量差的2-范数(实际意义就是n维空间的长度度量),如下所示:
∣ ∣ X ( i ) − X ( j ) ∣ ∣ 2 = ∑ k = 1 l ( x k ( i ) − x k ( j ) ) 2 ||X^{(i)}-X^{(j)}||_2=\sqrt {\sum_{k=1}^l{(x_k^{(i)}-x_k^{(j)}})^2} X(i)X(j)2=k=1l(xk(i)xk(j))2
当然还有其他的距离定义,如曼哈顿距离,特征向量差的1-范数:
∣ ∣ X ( i ) − X ( j ) ∣ ∣ 1 = ∑ k = 1 l ∣ x k ( i ) − x k ( j ) ∣ ||X^{(i)}-X^{(j)}||_1=\sum_{k=1}^l{|x_k^{(i)}-x_k^{(j)}}| X(i)X(j)1=k=1lxk(i)xk(j)
还有具有更一般形式的闵科夫斯基距离,特征向量差的p-范数:
∣ ∣ X ( i ) − X ( j ) ∣ ∣ 2 = ( ∑ k = 1 l ( x k ( i ) − x k ( j ) ) p ) 1 / p ||X^{(i)}-X^{(j)}||_2=(\sum_{k=1}^l{(x_k^{(i)}-x_k^{(j)}})^p)^{1/p} X(i)X(j)2=(k=1l(xk(i)xk(j))p)1/p
当然你会发现欧式距离和曼哈顿距离就是闵科夫斯基距离在p=2和p=1的具体形式。
还有 inf ⁡ \inf inf范数,
∣ ∣ X ( i ) − X ( j ) ∣ ∣ 1 = max ⁡ k = 1 , . . . , l ∣ x k ( i ) − x k ( j ) ∣ ||X^{(i)}-X^{(j)}||_1=\max_{k=1,...,l}{|x_k^{(i)}-x_k^{(j)}}| X(i)X(j)1=k=1,...,lmaxxk(i)xk(j)
不同的距离定义对最后的分类结果有一定影响,比如

4.2 K近邻法的K值

在K近邻算法中,唯一需要使用到的参数是K值。K值也是对分类结果产生最重要影响的参数。
k的取值是一项case by case的事,没有规定应该具体取什么值,k的取值都是跟具体的分析对象和训练样本等有关。

当然也不是说k值就随便取,也有几个规律性的结果可以遵循:
① k值取越小,在训练样本上面表现的误差越小;
② k值取过小值,容易变得过拟合,训练样本中的噪声点对结果影响变得很大,对于预测不利
③ k值取过大值,容易变得欠拟合,容易损失数据特征,对预测同样不利

所以说k值得选取一般还需要调试,《统计学习方法》中建议采用交叉验证法来优化k的取值。

4.3 K近邻法的统筹方法(分类决策原则)

当在k近邻算法进行完距离计算,找到k个最近训练样本点之后需要根据这些样本的类别属性统筹,决定预测点应该分到哪个类别中。

统筹方法或者分类决策原则一般是多数表决机制,即选取k个样本点中最多的类别作为待预测点的类别。这个合理性是显而易见可以理解的。

从数学上也是可以解释的。实际上对于最近邻的k各训练样本点组成的集合 N N N,类别为 z i z_i zi的发生概率可以用先验概率估计
P ( Z = z i ) = 1 k ∑ I ( Z = z i ) P(Z=z_i)=\frac{1}{k}\sum I(Z=z_i) P(Z=zi)=k1I(Z=zi)
那么产生误分类的风险如下所示:
1 − P ( Z = z i ) = 1 − 1 k ∑ I ( Z = z i ) 1-P(Z=z_i)=1-\frac{1}{k}\sum I(Z=z_i) 1P(Z=zi)=1k1I(Z=zi)
显然当 P ( Z = z i ) P(Z=z_i) P(Z=zi)最大的时候,误分类风险最小。

4.4 kd树

k近邻算法中主要的计算消耗在于遍历计算距离选取k个最近的训练样本点这个过程。对于训练样本很大的情况,想想都是很大的计算消耗,kd树是减少遍历计算的一种索引数据结构。

kd树的本质是,建立一种二叉树形式的索引数据结构,在计算距离时优先计算叶节点样本点上的距离,向根节点方向遍历,以一定的规则判断并摒弃没有可能成为k邻近域内的样本点,仅计算一部分满足规则的样本点,与叶节点计算的距离组成集合,比较选取k个值。

显然上面的话读着很拗口,很难理解(其实我也不知道怎么简单表述)。还是以实际的过程为例。

① 构建kd树
输入:训练样本 T ( X , Z ) = { ( X ( 1 ) , Z ( 1 ) ) , ( X ( 2 ) , Z ( 2 ) ) , . . . , ( X ( n ) , Z ( n ) ) } T(X, Z)=\{(X^{(1)},Z^{(1)}),(X^{(2)},Z^{(2)}),...,(X^{(n)},Z^{(n)})\} T(X,Z)={(X(1),Z(1)),(X(2),Z(2)),...,(X(n),Z(n))}
   其中特征向量 X ( i ) = ( x 1 ( i ) , x 2 ( i ) , . . . , x l ( i ) ) X^{(i)}=(x_1^{(i)},x_2^{(i)},...,x_l^{(i)}) X(i)=(x1(i),x2(i),...,xl(i)), Z Z Z是分类标签。
过程:
(1) 对于训练样本的特征向量 X ( i ) X^{(i)} X(i)按照 x 1 x_1 x1为轴,将训练样本按序排列,取中位数(奇数个取中位数,偶数个取中间树靠左或者靠右第一个数)为根节点,在中位数左侧的数据放入左子树,将中位数右侧的数据放入右子树;
(2)在左子树(右子树同样)中按照 x 2 x_2 x2作为轴,将左子树中的训练样本按序排列(注意这里排序是按照 x 2 x_2 x2作为轴),取中位数作为根节点,将中位数左侧数据放入左子树,右侧数据放入右子树;
(3)改变排序轴,重复开展上述二叉树构建步骤,直到子树中只包含一个数据,即为叶节点(如下图所示)。
输出:kd树结构
显然这是一个递归算法的典型范例。
在这里插入图片描述

② 搜索kd树
在建立kd树结构后,计算距离就变成搜索kd树中与待遇测点距离最小的k个值得过程了。先以k近邻算法为例说明这个算法步骤。
k近邻搜索算法:
输入:已建立的kd树,待预测点特征向量;
过程:
(1) 从根节点出发,递归查找待预测点特征向量归属的叶节点,即如果待预测点在根节点左侧,就移动到左子树,如果待预测点在根节点右侧,就移动到右子树,重复此步,知道移动到叶节点为止;
(2) 计算叶节点与待预测点的距离为k近邻值之一;
(3) 退到上一级根节点,并计算该根节点距离,对比现存k近邻值列表,当满足一下两点,则更新k近邻列表:a) 当k近邻列表中有未定义值,b)当根节点距离小于k近邻列表中最小值;
(4) 计算(3)根节点中的另一个子节点,对比现存k近邻值列表,当满足一下两点,则更新k近邻列表:a) 当k近邻列表中有未定义值,b)当该子节点距离小于k近邻列表中最小值;
(5)重复(3)(4),直到最终根节点。如下图所示。
在这里插入图片描述

4.5 最近邻算法的Python代码实现

输入:训练样本如下,训练样本特征向量为前两列,第三列是类别标签,第四列是为了说明kd树结构的辅助序列号,仅仅为了说明而已。

#       x1                 x2           label    order
4.45925637575900	8.22541838354701	-1      #1
0.0432761720122110	6.30740040001402	-1      #2
6.99716180262699	9.31339338579386	-1      #3
4.75483224215432	9.26037784240288	-1      #4
0.640487340762152	2.96504627163533	-1      #5
2.56324459286653	7.83286351946551	-1      #6
5.42032358874179	8.77024851395462	-1      #7
7.09749399121559	4.84058301823207	1       #8
4.15244831176753	1.44597290703838	1       #9
9.55986996363196	1.13832040773527	1       #10
1.63276516895206	0.446783742774178	1       #11
9.38532498107474	0.913169554364942	1       #12
6.08012336783280	3.21848321902105	1       #13
5.48289786541560	0.267286639106428	1       #14

以字典结构为例,建立kd树

def kdtree(dataset, loop):
    subtree = {}

    # figure out if it is leaf node
    if dataset.shape[0] == 1:
        subtree['node'] = list(dataset.iloc[0])
        subtree['End'] = 1
        return subtree
    if dataset.empty:
        return {}
    # loop in feature list, and sort dataset
    columnslist = dataset.columns.values.tolist()
    loopi = loop % (dataset.shape[1] - 1)
    dataset1 = dataset.sort_values(by=columnslist[loopi], axis=0, ascending=True)

    # make the middle value as root node
    index = int(dataset.shape[0] / 2)
    node = list(dataset1.iloc[index])

    subtree['axis'] = columnslist[loopi]
    subtree['node'] = node

    # left sub tree
    loop += 1
    left = dataset1[dataset1[columnslist[loopi]] < node[loopi]]
    right = dataset1[dataset1[columnslist[loopi]] > node[loopi]]
    print(left)
    subtree1 = kdtree(left, loop)
    subtree2 = kdtree(right, loop)
    if len(subtree1)>0:
        subtree['leftsubtree'] = subtree1
    if len(subtree2)>0:
        subtree['rightsubtree'] = subtree2

    return subtree

建立的kd树如下所示
在这里插入图片描述
距离定义:这里采用欧式距离。

def distance(data1, data2):
    dis = 0
    for loopi in range(len(data1)):
        dis += (data1[loopi]-data2[loopi])**2
    dis = math.sqrt(dis)
    return dis

k近邻搜索kd树

def searchnode(dataset, tree, k):
    # the function is to figure out a leaf node
    # dataset is the target to predict, just feature vector.
    # searchdir is a list of the dir to reach leaf node
    axis_label = dataset.columns.values.tolist()
    axis_label.extend(['label', 'distance'])
    knode = pd.DataFrame(np.zeros((k, len(axis_label))), columns=axis_label)
    subtree = tree
    searchdir = []

    while 'End' not in subtree:
        #print(subtree)
        axis = subtree['axis']
        index = axis_label.index(axis)
        # if value(in axis)<root node or child node just leftnode, move leftsubtree 
        if dataset.iloc[0][axis] < subtree['node'][index] or ('rightsubtree' not in subtree):
            subtree = subtree['leftsubtree']
            searchdir.append('leftsubtree')
           # print(subtree)
        else:
            subtree = subtree['rightsubtree']
            searchdir.append('rightsubtree')
    # define the leaf node as k neighbour node
    leafnode = []
    leafnode[:] = subtree['node']
    dis = distance(leafnode[:len(leafnode)-1], dataset.iloc[0])
    leafnode.append(dis)
    knode.loc[0:2, axis_label] = leafnode
    # search back the kd tree
    data = list(dataset.iloc[0].values)
    knode = searchback(data, knode, tree, searchdir, k)

    return knode

def searchback(data, knode, tree, searchdir, k):
	# this function is to search back the kd tree
    subtree = tree
    tempnode = []
    axis_label = knode.columns.values.tolist()
    for loopi in searchdir[:len(searchdir)-1]:
        subtree = subtree[loopi]
    # check father node if it is closer 
    tempnode[:] = subtree['node']
    dis = distance(tempnode[:len(tempnode)-1], data)

    if dis<knode.distance.max():
        tempnode.append(dis)
        knode.loc[knode.distance.idxmax()] = tempnode

    # check sister node if it is closer  
    if searchdir[len(searchdir)-1] == 'leftsubtree' and ('rightsubtree' in subtree):
        subtree2 = subtree['rightsubtree']
        tempnode[:] = subtree2['node']
        dis2 = distance(tempnode[:len(tempnode)-1], data)

        if dis2<knode.distance.max():
            tempnode.append(dis2)
            knode.loc[knode.distance.idxmax()] = tempnode

    elif searchdir[len(searchdir)-1] == 'rightsubtree' and ('leftsubtree' in subtree):
        subtree2 = subtree['leftsubtree']
        tempnode[:] = subtree2['node']
        dis2 = distance(tempnode[:len(tempnode)-1], data)

        if dis2<knode.distance.max():
            tempnode.append(dis2)
            knode.loc[knode.distance.idxmax()] = tempnode
            
    if len(searchdir) > 1:
        searchdir2 =  searchdir[:len(searchdir)-1]
        searchback(data, knode, tree, searchdir2, k)

    return knode


统筹方法-分类决策原则

def judge(knode):
    if knode['label'].sum()>0:
        return 1
    else:
        return -1
#   return 1 if knode.label.sum()>0 else -1

待预测点分类预测

def predict(node, tree, k):
    label = []
    for loopi, data in node.iterrows():
        data = pd.DataFrame(data)
        knode = searchnode(data.T, tree, 3)
        templabel = judge(knode)
        label.append(templabel)
        print(tree)
    return label

最近邻法就没有统筹方法这一节的内容了。

附录:k近邻法Python原代码

import pandas as pd
import numpy as np
import math

def kdtree(dataset, loop):
    subtree = {}

    # figure out if it is leaf node
    if dataset.shape[0] == 1:
        subtree['node'] = list(dataset.iloc[0])
        subtree['End'] = 1
        return subtree
    if dataset.empty:
        return {}
    # loop in feature list, and sort dataset
    columnslist = dataset.columns.values.tolist()
    loopi = loop % (dataset.shape[1] - 1)
    dataset1 = dataset.sort_values(by=columnslist[loopi], axis=0, ascending=True)

    # make the middle value as root node
    index = int(dataset.shape[0] / 2)
    node = list(dataset1.iloc[index])

    subtree['axis'] = columnslist[loopi]
    subtree['node'] = node

    # left sub tree
    loop += 1
    left = dataset1[dataset1[columnslist[loopi]] < node[loopi]]
    right = dataset1[dataset1[columnslist[loopi]] > node[loopi]]
    print(left)
    subtree1 = kdtree(left, loop)
    subtree2 = kdtree(right, loop)
    if len(subtree1)>0:
        subtree['leftsubtree'] = subtree1
    if len(subtree2)>0:
        subtree['rightsubtree'] = subtree2

    return subtree


def searchnode(dataset, tree, k):
    # the function is to figure out a leaf node of target
    axis_label = dataset.columns.values.tolist()
    axis_label.extend(['label', 'distance'])
    knode = pd.DataFrame(np.zeros((k, len(axis_label))), columns=axis_label)
    subtree = tree
    searchdir = []

    while 'End' not in subtree:
        print(subtree)
        axis = subtree['axis']
        index = axis_label.index(axis)
        if dataset.iloc[0][axis] < subtree['node'][index] or ('rightsubtree' not in subtree):
            subtree = subtree['leftsubtree']
            searchdir.append('leftsubtree')
            print(subtree)
        else:
            subtree = subtree['rightsubtree']
            searchdir.append('rightsubtree')
    leafnode = []
    leafnode[:] = subtree['node']
    dis = distance(leafnode[:len(leafnode)-1], dataset.iloc[0])
    leafnode.append(dis)
    knode.loc[0:2, axis_label] = leafnode
    data = list(dataset.iloc[0].values)
    knode = searchback(data, knode, tree, searchdir, k)

    return knode


def judge(knode):
    '''if knode['label'].sum()>0:
        return 1
    else:
        return -1'''
    return 1 if knode.label.sum() > 0 else -1


def searchback(data, knode, tree, searchdir, k):

    subtree = tree
    tempnode = []
    axis_label = knode.columns.values.tolist()
    for loopi in searchdir[:len(searchdir)-1]:
        subtree = subtree[loopi]
    # father node check
    tempnode[:] = subtree['node']
    dis = distance(tempnode[:len(tempnode)-1], data)


    if dis<knode.distance.max():
        tempnode.append(dis)
        print(knode)
        knode.loc[knode.distance.idxmax()] = tempnode
        print(knode)

    # sister node check
    if searchdir[len(searchdir)-1] == 'leftsubtree' and ('rightsubtree' in subtree):
        subtree2 = subtree['rightsubtree']
        tempnode[:] = subtree2['node']
        dis2 = distance(tempnode[:len(tempnode)-1], data)

        if dis2<knode.distance.max():
            tempnode.append(dis2)
            print(knode)
            knode.loc[knode.distance.idxmax()] = tempnode
            print(knode)

    elif searchdir[len(searchdir)-1] == 'rightsubtree' and ('leftsubtree' in subtree):
        subtree2 = subtree['leftsubtree']
        tempnode[:] = subtree2['node']
        dis2 = distance(tempnode[:len(tempnode)-1], data)

        if dis2<knode.distance.max():
            tempnode.append(dis2)
            print(knode)
            knode.loc[knode.distance.idxmax()] = tempnode
            print(knode)
    if len(searchdir) > 1:
        searchdir2 =  searchdir[:len(searchdir)-1]
        searchback(data, knode, tree, searchdir2, k)

    return knode


def distance(data1, data2):
    dis = 0
    for loopi in range(len(data1)):
        dis += (data1[loopi]-data2[loopi])**2
    dis = math.sqrt(dis)
    return dis

def searchleafnode(dataset, tree, k):
    axis_label = dataset.columns.values.tolist()
    while 'End' not in subtree:
        axis = subtree['axis']
        index = axis_label.index(axis)
        if dataset.iloc[0][axis] < subtree['node'][index] or ('rightsubtree' not in subtree):
            subtree = subtree['leftsubtree']
        else:
            subtree = subtree['rightsubtree']
    return subtree['node']


def predict(node, tree, k):
    label = []
    for loopi, data in node.iterrows():
        data = pd.DataFrame(data)
        knode = searchnode(data.T, tree, 3)
        templabel = judge(knode)
        label.append(templabel)
        print(tree)
    return label

if __name__ =='__main__':

    temp = []
    with open(r'XX\knn\train_data.txt') as f:
        for loopi in f.readlines():
            line = loopi.strip().split('\t')
            temp.append([float(line[0]), float(line[1]), float(line[2])])

    data_set=pd.DataFrame(temp, columns=['x','y','label'])
    vector = data_set[['x','y']]
    tree = kdtree(data_set, 0)
    print(tree)
    # node is a target data to be predicted, dataframe structrue
    node = vector.iloc[6:8] + 1
    label = predict(node, tree, 3)
    print(label)
      
文章导引列表:
机器学习
  1. 小瓜讲机器学习——分类算法(一)logistic regression(逻辑回归)算法原理详解
  2. 小瓜讲机器学习——分类算法(二)支持向量机(SVM)算法原理详解
  3. 小瓜讲机器学习——分类算法(三)朴素贝叶斯法(naive Bayes)
  4. 小瓜讲机器学习——分类算法(四)K近邻法算法原理及Python代码实现
  5. 小瓜讲机器学习——分类算法(五)决策树算法原理及Python代码实现
  6. 小瓜讲机器学习——聚类算法(一)K-Means算法原理Python代码实现
  7. 小瓜讲机器学习——聚类算法(二)Mean Shift算法原理及Python代码实现
  8. 小瓜讲机器学习——聚类算法(三)DBSCAN算法原理及Python代码实现

数据分析
  1. 小呆学数据分析——使用pandas中的merge函数进行数据集合并
  2. 小呆学数据分析——使用pandas中的concat函数进行数据集堆叠
  3. 小呆学数据分析——pandas中的层次化索引
  4. 小呆学数据分析——使用pandas的pivot进行数据重塑
  5. 小呆学数据分析——用duplicated/drop_duplicates方法进行重复项处理
  6. 小呆学数据分析——缺失值处理(一)
  7. 小呆学数据分析——异常值判定与处理(一)
  8. 小瓜讲数据分析——数据清洗

数据可视化
  1. 小瓜讲数据分析——数据可视化工程(matplotlib库使用基础篇)
  2. 小瓜讲matplotlib高级篇——坐标轴设置(坐标轴居中、坐标轴箭头、刻度设置、标识设置)
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力的骆驼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值