统计学习方法与Python实现——k近邻法

Posted iwehdio

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了统计学习方法与Python实现——k近邻法相关的知识,希望对你有一定的参考价值。

统计学*方法与Python实现(二)——k*邻法

  iwehdio的博客园:https://www.cnblogs.com/iwehdio/

 

1、定义

  k*邻法假设给定一个训练数据集,其中的实例类别已定。分类时,对新的实例,根据其k个最*邻的训练实例的类别,通过多数表决的方式进行预测。k*邻法不具有显式的学*过程,而实际上是利用训练数据集对特征空间进行划分,并作为其分类的模型。k*邻法的三个基本要素是 k值的选择、距离度量和分类决策规则。

  k*邻法的模型是将特征空间划分成一些称为单元的子空间,并且每个单元内的点所属的类都被该单元的类标记所唯一确定。

  单元的划分和类标记的确定需要首先对距离进行度量。特征空间中两个实例点的距离是它们之间相似程度的反映。对于n维实数向量的特征空间Rn,两向量xi和xj之间的Lp距离定义为:

  当p=1时,称为曼哈顿距离:

  当p=2时,称为欧氏距离:

  当p=∞时,取值为各个坐标距离的最大值:

  对于k值的选择,如果选择较小的k值,学*的*似误差会减小,但估计误差会增大,对噪声敏感。k值的减小就意味着整体模型变得复杂,容易发生过拟合。如果选择较大的k值,可以减少学*的估计误差,但缺点是学*的*似误差会增大。k值的增大 就意味着整体的模型变得简单。

  在应用中,k值一般取一个较小的数值,并通过交叉验证法来确定最优的k值。

  k*邻法中的分类决策规则往往是多数表决,即由输入实例的k个*邻的训练实例中的多数类决定输入实例的类。多数表决规则等价于经验风险最小化。

 

2、构造kd树

  实现k*邻法是,主要的问题是如何对训练数据进行快速k*邻搜索。如果使用现行扫描,需要计算输入实例与每一个训练实例的距离,非常耗时。kd树是一种对k维空间中的实例 点进行存储以便对其进行快速检索的树形数据结构,可以提高搜索效率。

  kd树是二叉树,表示对k维空间的一个划分。每次划分需要选定一个坐标轴和一个切分点,以此确定一个超平面对训练实例进行一次划分,并递归直到将实例划分完全。如果切分点每次*似选为该坐标轴上的中位数,则称这样的kd树为平衡kd树,算法流程如下:

  a、构造根结点,其对应于包含整个训练实例T的k维空间。选择x1作为坐标轴,以T中所有实例的x1坐标的中位数作为切分点,切分由通过切分点并与x1轴垂直的超平面实现。由根节点生成深度为1的左右结点,左子结点对应于x1坐标小于切分点的子区域,右子结点对应于x1坐标大于切分点的子区域。落在切分超平面上的实例被保存在根结点。

  b、递归重复。对于深度为 j 的结点,选择xn作为切分的坐标轴,其中n = ( j mod k) + 1,以节点区域中所有实例的xn坐标的中位数作为切分点。其他与a步中相同。

  c、到两个子区域都没有实例点存在时停止,从而形成kd树的区域划分。

 

3、搜索kd树

  完成对kd树的构造后,对于输入的测试实例,需要对kd树进行搜索,以得到输入实例的类别。以k=1的最*邻为例。给定输入实例,搜索最*邻。首先找到包含目标点的叶结点,其对应于包含目标点的最小子区域。以此叶结点的实例作为当前最*点,则目标点的最*邻一定在以目标点为中心,并通过当前最*点的超球体内部。然后返回当前节点的父结点,如果父结点的另一子结点的子区域与超球体相交,则在此子区域内寻找与目标点更*的实例点。如果存在这样的点,将此点作为新的当前最*点。返回更上一级的父结点,继续上述过程,直到父节点的另一子结点的子区域与超球体不相交,即不存在更*的点。算法流程如下:

  a、从根结点出发,向下访问kd树,找到子区域包含输入实例的叶结点。

  b、以此叶结点作为当前最*点。

  c、递归的向上回退。如果该结点保存的实例点比当前最*点更*,则将此结点更新为当前最*点。如果以目标点为中心,通过当前最*点的的超球体与当前最*点的父结点的另一个子节点对应的子区域相交,则在此子区域中进行搜索与更新。如果不相交,则向上回退。

  d、当回退到根结点时,搜索结束。当前最*点即为最*邻点。

 

 4、kd树的构造的Python实现

  用到的数据集是sk-learn中的iris鸢尾花卉数据集,共150个数据,分为\'setosa\', \'versicolor\', \'virginica\'三类,数据包含四个特征sepal length(花萼长度)、sepal width(花萼宽度)、petal length(花瓣长度)和petal width(花瓣宽度)。

  本次先从k=1的最*邻法实现k*邻。

  首先,载入数据集并划分训练集和测试集。

from binarytree import *
import numpy as np
from sklearn.datasets import load_iris

# 从sk-learn库载入iris数据集
iris = load_iris()
# dict_keys([\'data\', \'target\', \'target_names\', \'DESCR\', \'feature_names\', \'filename\'])
# \'target_names\': array([\'setosa\', \'versicolor\', \'virginica\']

# 设定训练集和测试集大小
train_length = 105
test_length = 45

data = iris[\'data\']       # shape = (150, 4)
label = iris[\'target\']    # 0:50,0; 50:100,1; 100:150,2

train_data = np.zeros([train_length, 4])
train_label = np.zeros([train_length])
test_data = np.zeros([test_length, 4])
test_label = np.zeros([test_length])

# 划分训练集和测试集
length = train_length
for j in range(3):
    train_data[j * int(length/3):(j+1) * int(length/3)] = data[j*50:j*50 + int(length/3)]
    train_label[j * int(length/3):(j+1) * int(length/3)] = label[j*50:j*50 + int(length/3)]

length = test_length
for j in range(3):
    test_data[j * int(length/3):(j+1) * int(length/3)] = data[(j+1) * 50 - int(length/3):(j+1) * 50]
    test_label[j * int(length/3):(j+1) * int(length/3)] = label[(j+1) * 50 - int(length/3):(j+1) * 50]

train_index = np.arange(train_length).reshape([train_length, 1])
train_data = np.hstack((train_data, train_index))

  

  然后,构造kd树,二叉树由binarytree库实现。构造策略是,每次将数据四个特征中方差最大的轴作为划分轴,将该轴上特征值小于等于中位数的数据划分到左子树,大于中位数的数据划分到右子树。并记录每次划分时的轴和中位数值。

# 生成kd树
def creat_kd_tree(data, root, turn, log):

    axis = selct_axis(data[:, :-1])
    data = data[data[:, axis].argsort()]    # 按第axis列排序
    mid = data[:, axis].shape[0] // 2
    # 如果多个值在axis上的值与mid_data相同,则全部划分到左结点
    while mid < data.shape[0]-1 and data[mid + 1, axis] == data[mid, axis]:
        mid += 1
    mid_data = data[mid]
    log[int(mid_data[-1])] = (axis, mid_data[axis])
    # 存储左右子树下的结点
    data_left, data_right = [], []
    for temp in data[:mid]:
        data_left.append(temp)
    for temp in data[mid + 1:]:
        data_right.append(temp)
    # 创建新结点并递归
    node = Node(int(mid_data[-1]))
    # print(mid_data[-1], data_left, data_right)
    if turn: root.right = node
    else: root.left = node
    if data_left:
        creat_kd_tree(np.array(data_left), node, 0, log)
    if data_right:
        creat_kd_tree(np.array(data_right), node, 1, log)


# 选择方差最大的轴作为划分对象
def selct_axis(data, num=4):

    index = 0
    all_var = 0
    for i in range(num):
        axis_var = data[:, i].var()
        if all_var < axis_var:
            all_var = axis_var
            index = i
    return index


# kd树的根节点
node_init = Node(-1)
# log中保存了每个值为index的结点的超平面的轴和中位数值
log = [0 for i in range(train_length)]
creat_kd_tree(train_data, node_init, 0, log)
print(node_init.left)

 

 5、kd树的搜索的Python实现

   首先,寻找输入实例所属的子区域的叶节点,并记录路径。然后,根据记录的路径,从叶结点开始,计算以输入实例为球心,最*邻点距离为半径的超球体,与父结点的超平面有无交集。如果有交集,则遍历该父结点下的所有子结点,同时记录遍历过得结点防止重复计算。最后,返回模型中最*邻点的索引和距离。

# 寻找输入实例所属的子区域的叶节点,并记录路径
def find_leave(data, root, log):

    route = [(root.value, root)]
    while 1:
        index = root.value
        # print(index)
        if data[log[index][0]] <= log[index][1]:
            temp = root.left
        else:
            temp = root.right
        if temp is None:
            return route
        else:
            route.append((temp.value, temp))
            root = temp


# 寻找最*邻点
def find_neibor(simple, route, log):

    # 初始化最*邻点和距离
    near = route[-1][0]
    dst = np.linalg.norm((simple - train_data[near, :-1]))

    # 记录已经遍历过的结点
    save = []
    # 从后往前返回父结点
    for fa in route[:-1][::-1]:

        # 如果父结点的超平面与以输入实例为球心,最*邻点距离为半径的超球体有交集,则遍历其所有子结点
        if abs(log[fa[0]][1] - simple[log[fa[0]][0]]) < dst:
            child = []
            get_child(fa[1], child)
            for choic in child:
                if choic not in save:
                    dst0 = np.linalg.norm((simple - train_data[choic, :-1]))
                    save.append(choic)
                    if dst0 < dst:
                        dst = dst0
                        near = fa[0]
    return near, dst


# 返回父结点的所有子结点的值的列表
def get_child(root, child):

    if root is None:
        return 0
    else:
        child.append(root.value)
        get_child(root.left, child)
        get_child(root.right, child)

   

  最后,在测试集上进行测试。

# 测试准确率
def acc(ans, label):

    counter = 0
    for index, num in enumerate(ans):
        if num == label[index]: counter += 1
    return counter / len(ans)


# 训练集
for n in range(train_length):
    valid_simple = train_data[n, :-1]
    rou = find_leave(valid_simple, node_init.left, log)
    valid_point, zero_true_distance = find_neibor(valid_simple, rou, log)
    # print(rou)
    # print(point, distance)

# 测试
ans = []
for n in range(test_length):
    test_simple = test_data[n]
    # rou记录了到输入实例叶结点的路径
    rou = find_leave(test_simple, node_init.left, log)
    test_point, test_distance = find_neibor(test_simple, rou, log)
    ans.append(train_label[test_point])
    # print(rou)
    print(test_point, test_distance, train_label[test_point])

print(\'test_acc\', acc(ans, test_label))

  

  在训练集上,每个输入都可以找到自己对应距离为0的结点。在测试集中,准确率为1,部分测试结果如下。第一列为最*邻点的索引,第二列为距离,第三列为分类结果。

 

 

6、其他问题

  a、如何从k=1的最*邻法拓展到k为其他值下的k*邻法?

    可以用长度为k的排序列表来实现。首先,先以关系最*的k个父结点和兄弟结点初始化排序列表。然后,按与最*邻法相同的算法,每次用排序列表中距离最大的值进行比较(也可能出现新值的距离比原来列表中的多个值都小的情况)。最后,当距离最大的值的超球体都与父结点的超平面无交集时,返回排序列表作为最*的k个值进行投票。

  b、为什么用到超平面的距离代替超球体与其他结点的区域是否有交集?

    因为计算点到区域的距离比较复杂,用到超平面的距离来代替超球体与超区域的问题是充分的,而且易于计算。

  c、对于如手写数字集mnist类似的,数据值为0,1二值化的数据集,如何进行kd树的中位数划分?

    可以每次在结点上任选一值为0的样本,然后将值为0的分到左子树,值为1的分为右子树,但是这样做并不能提高多少搜索效率。(所以用了iris...)

 

参考:李航 《统计学*方法(第二版)》

 

iwehdio的博客园:https://www.cnblogs.com/iwehdio/

 

以上是关于统计学习方法与Python实现——k近邻法的主要内容,如果未能解决你的问题,请参考以下文章

统计学习方法 (第3章)K近邻法 学习笔记

机器学习笔记——K近邻法

李航统计学习方法——算法2——k近邻法

李航统计学习方法(第二版):k 近邻算法简介

python kd树 搜索

Python-机器学习-K近邻算法的原理与鸢尾花数据集实现详解