用python手写KNN算法+kd树及其BBF优化(原理与实现)(下篇)

用python手写KNN算法+kd树及其BBF优化(原理与实现)(下篇)

接上一篇用python手写KNN算法+kd树及其BBF优化(原理与实现)(上篇)
我们使用training2和test2两个数据集时,运行结果如下:
运行情况
当不使用kd-tree时(将最小划分个数设为10000),运行情况如下:
运行情况
可以发现,使用kd-tree并不会减少预测用时,甚至多于不使用kd-tree时。
这似乎有点违背常理,我们使用详细print版代码(使用kd-tree)再运行一遍,就可以找到原因:
print
可以看到每次回溯都几乎遍历了全部训练样本,kd-tree并没有发挥应有的效果。
事实上,我们之前的回溯算法只能适用于样本特征维数较小的情况,当维数太大时,与前面介绍的超球面相交的超矩形就会大大增多,这意味这我们回溯的次数大增,最终丧失了二叉树查找的优势。
于是我们需要改进查找算法,减少回溯次数,并且尽可能让找出K近邻的准度不至于下降太多。这就是下面的BBF优化。
BBF优化主要有两点:
(1) 限制回溯次数,这个阈值需要手动设置
(2)选择回溯点,这里我们采取和前面不一样的办法:
1、在向叶结点查找的同时,将途经分支结点放入优先级队列,这里的优先级指待确定的测试样本点在分支结点的划分维度上与划分值的差值,即前面的|Q[Di]-Dv|,此值越小,优先级越高,稍后就优先回溯
2、到达叶结点后,取优先级队首的分支结点进行回溯,回到步骤1
BBF优化后的完整代码如下:

ps: 之所以搞一个queueBack类是为了避免限制回溯次数时设置外部变量。

import numpy as np
import time
import queue


"""que是用于回溯的分支结点优先级队列,bbfmax是最大回溯次数,为0时不再回溯"""
class queueBack():
    def __init__(self, bbfmax):
        self.que = queue.PriorityQueue()
        self.bbfmax = bbfmax
    def put(self, node):
        self.que.put(node)
    def get(self):
        if not self.que.empty():
            self.bbfmax -= 1
            return self.que.get()
    def empty(self):
        return self.que.empty()
    def getbbfmax(self):
        return self.bbfmax
    def cangoback(self):
        return (not self.que.empty()) and self.bbfmax!=0

def loadData(filePath):
    with open(filePath, 'r+') as fr:  # with语句会自动调用close()方法,且比显式调用更安全
        lines = fr.readlines()
        data = []
        for line in lines:
            items = line.strip().split(",")
            data.append([int(items[i]) for i in range(len(items))])
    return np.asarray(data)

class kdNode():
    # 分支结点
    def __init__(self, demo, value, left, right):
        # 切割维度,切割值,左子树,右子树
        self.demo = demo
        self.value = value
        self.left = left
        self.right = right
        self.dis = 1000000000 #球面与超平面的距离,保险起见初始给一个较大的值
    def setdis(self, value):
        self.dis = value
    def __lt__(self, other):  #用于优先级队列判断
        return self.dis < self.dis


class kdtree():
    # kd树
    def __init__(self, data_array, threshold):  # data_array为初始的数据集合
        row, col = data_array.shape
        k = col - 1  # k指维度,即特征向量的元素个数
        self.threshold = threshold  # 最小分支阈值,数据个数低于此值不在划分

        """寻找方差最小的维度"""
        def getMaxDimension(data):  # data即当前待划分的数据集合(数组)
            maxv = -1  # 记录当前最大方差
            maxi = -1  # 记录当前方差最大的维度
            for i in range(k):
                a = np.var(data[:, i])  # 计算维度i对应的方差
                if a > maxv:
                    maxi = i
                    maxv = a
            return maxi, maxv  # 返回最大方差对应的维度和最大方差值

        """创建一个分支结点"""
        def createNode(data):
            split_dimension, maxv = getMaxDimension(data)  # split_dimension, maxv分别指划分轴(维度)和最大方差值
            if maxv == 0:  # 考虑边界情况,最大方差为0时当前数据不必划分,直接作为叶子结点
                return data
            split_value = np.median(data[:, split_dimension])  # 取当前维度下的中位数作为划分值
            maxvalue = np.max(data[:, split_dimension])  # 当前维度下的最大元素
            minvalue = np.min(data[:, split_dimension])  # 当前维度下的最小元素
            left = []  # 保存在split_dimension下小于(或等于)split_value的点
            right = []  # 保存在split_dimension下大于(或等于)split_value的点
            for i in range(len(data)):
                if split_value < maxvalue:  # 避免0,0,0,1,2这样的分不开
                    if data[i][split_dimension] <= split_value:
                        left.append(list(data[i]))
                    else:
                        right.append(list(data[i]))
                elif split_value > minvalue:  # 避免0,1,2,2,2这样的分不开
                    if data[i][split_dimension] < split_value:
                        left.append(list(data[i]))
                    else:
                        right.append(list(data[i]))
            # 最小分支阈值,低于此值不在划分
            root = kdNode(split_dimension, split_value,
                          (createNode(np.asarray(left)) if len(left) >= threshold else np.asarray(left)),
                          (createNode(np.asarray(right)) if len(right) >= threshold else np.asarray(right)))
            # 递归建树,注意当点集中元素个数小于最小分支阈值时直接作为叶结点而不必分支
            return root

        self.root = createNode(data_array)

"""寻找vec对应的k邻近,klist为(距离,[向量])构成的列表,存放vec的k个近邻点的信息,初始为空
pque是一个queueBack实例"""
n = 0
def findn(root, vec, klist, k, pque):
    if type(root) == np.ndarray:
        if len(root) == 0:
            return
        temp = (root[:, :-1] - vec) ** 2
        for i in range(len(temp)):
            global n
            n += 1
            a = sum(temp[i])
            if len(klist) != k:
                klist.append((a, root[i]))
                klist.sort(key=lambda x: x[0])  # 按距离排序
            else:
                if a < klist[k - 1][0]:
                    klist[k - 1] = [a, root[i]]
                    klist.sort(key=lambda x: x[0])  # 按距离排序

        while pque.cangoback():
            newroot = pque.get()
            findn(newroot, vec, klist, k, pque)
    else:
        #print("2:" + str(bbfmax))
        if vec[root.demo] < root.value:
            #findn(root.left, vec, klist, k, pque, bbfmax)
            #if abs((vec[root.demo] - root.value)**2) < klist[len(klist) - 1][0]:
            if type(root.right) == np.ndarray:  # 如果是叶结点则无需入队直接遍历
                findn(root.right, vec, klist, k, pque)
            else:
                dis = abs(vec[root.right.demo] - root.right.value)
                root.right.setdis(dis)
                #print("dis: "+ str(dis))
                pque.put(root.right)
            findn(root.left, vec, klist, k, pque)
        else:
            #findn(root.right, vec, klist, k, pque, bbfmax)
            #if abs((vec[root.demo] - root.value)**2) < klist[len(klist) - 1][0]:
            if type(root.left) == np.ndarray:  # 如果是叶结点则无需入队直接遍历
                findn(root.left, vec, klist, k, pque)
            else:
                dis = abs(vec[root.left.demo] - root.left.value)
                root.left.setdis(dis)
                #print("dis: " + str(dis))
                pque.put(root.left)
            findn(root.right, vec, klist, k, pque)
""" 选出列表中出现次数最多的元素,一个需要注意的问题是像[2,2,1,1,3]这样的怎么选,因为之前已经按距离从小到大排序,所以应选2"""
def findMain(alist):
    hashtable = [0 for i in range(10)]
    for i in range(len(alist)):
        hashtable[alist[i]]+=1
    maxnum = -1
    main = -1
    for i in range(len(alist)):
        if hashtable[alist[i]]>maxnum:
            main = alist[i]
            maxnum = hashtable[alist[i]]
    return main

def forecast(root, data, k, bbfmax):
    a = []  # 作为findn方法中的klist参数
    global n
    n = 0
    que = queueBack(bbfmax)
    findn(root, data, a, k, que)
    #print("遍历了" + str(n) + "个点")
    L = len(a[0][1])  # 其实就是向量维度
    res = []
    for i in range(len(a)):
        res.append(a[i][1][L - 1])
    #return np.argmax(np.bincount(np.asarray(res))) #这样并不能正确处理[2,2,1,1,3]这样的情况
    return findMain(res)

def knn(train_list, test_list, k, bbfmax):
    tic1 = time.time()
    root = kdtree(train_list, 10).root
    print("最小划分个数: 10")
    print("k = " + str(k))
    tic2 = time.time()
    res = []
    num = 0
    for i in range(len(test_list)):
        a = forecast(root, np.asarray(test_list[i][:-1]), k, bbfmax)
        #print(test_list[i][-1])
        if a == test_list[i][-1]:
            num += 1
        res.append(str(a) + "\n")
    print("最大回溯数:"+str(bbfmax))
    print("正确率:" + str(num / len(test_list)))  # 预测准确率
    toc = time.time()
    print("总用时:" + str(1000 * (toc - tic1)) + "ms")
    print("训练用时:" + str(1000 * (tic2 - tic1)) + "ms")
    print("预测用时:" + str(1000 * (toc - tic2)) + "ms")


if __name__ == "__main__":
    train_list = loadData("training2.txt")
    test_list = loadData("test2.txt")
    knn(train_list, test_list, 3, 10)

运行结果:
最大回溯数为10:
结果
最大回溯数为50:
结果
对比前面未用BBF的结果,可以发现,使用BBF优化提高了查找速度,但降低了准确率。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值