机器学习——KNN算法(手动代码,含泪)

徒手实现代码的过程,真是含泪和心酸,浪费了生命中的三天,以及工作中的划水一小时
终于滤清思路后,自己实现了KNN
都说KNN是最基础,最简单的分类器
放屁!骗纸!!!它的想法是简单的,但实现的过程何其复杂!!!
问题何其之多!!是我实现感知机、逻辑回归分类、线性回归、朴素贝叶斯中,最难实现的分类算法!!!!
给多少时间都无法将这个槽吐尽,甚至算法都没完全弄好,但只剩收尾工作了

1. KNN 分类思想

首先,KNN思想很简单,找到离目标对象最近的K个对象,以K个对象中数量最多的那个类型为预测结果。

就好像:要对一个有喉结、短头发、平胸的对象进行性别的分类,那么特征最相近的K个人,如果男性居多,则将该对象分类为男性。如果女性居多,则分类为女性。

KNN搜索特征最相近的精髓,在于建立KD树——二叉树

2. KNN 分类流程

2.1 构建KD树

构建KD树的原因是,我们不能逐一去计算某个对象与所有对象的特征距离,因为数据太多,工作量太大。

那就对所有对象的特征,进行KD树的分组,分组规则别人说的非常清晰明了,我看了之后有种恍然大悟、拍手叫好的激动
kd树的搜索过程到底是怎么进行的? - 月来客栈的文章 - 知乎

但是当我自己去实现时,遇到重重的困难

先来实现 KD树:应用二叉树的原理

# 构建一个二叉树节点
class Node_1():
    def __init__(self,X_value):
        self.left = None
        self.right = None
        self.value = []
        self.X = X_value # {"特证名":"x","划分标准":"aaaaaa"}
# 根据节点,构建一个二叉树
class Tree():
    def __init__(self):
        self.root = None
    def get_value_1(self,X_arg,node_arg=None):
        node = node_arg
        if self.root == None:
            node = Node_1(X_arg)
            self.root = node
        # 获取所有特征各自对应的方差
        vars = [X_arg[i].var() for i in X_arg.columns[0:2]]
        # 如果这些方差中,最大的那个方差为0或是无法计算,则说明数据已经分的很清楚了
        """重点:停止迭代的条件:所有特征的方差均为0 或无法计算"""
        if max(vars) == 0 or math.isnan(max(vars)):
            return
        """以方差最大的那个特征,作为分组的维度,根据该特征的中位数进行分组:
        pandas中的median中位数,与统计学稍微不同
        统计学中偶数个数据的中位数,是中间两个数据的均值
        pandas中偶数个数据的中位数,是中间两个数据的其中一个
        因此,用pandas的median计算中位数,还需要考虑一个非常坑的问题:如果最后只剩两个数据,中位数到底是偏大的那个,还是偏小的那个,这决定了我们分组到底能不能分清楚!!!"""   
        X_name = X_arg.columns[vars.index(max(vars))]
        med = X_arg[X_name].median()
        if med == X_arg[X_name].max():
            """要根据中位数,分为左组、右组:涉及到pandas的分组条件筛选【抄网上】"""
            left = X_arg.groupby(X_name).apply(lambda x: x.loc[x.loc[x[X_name] < med].index]).reset_index(drop=True)
            right = X_arg.groupby(X_name).apply(lambda x: x.loc[x.loc[x[X_name] >= med].index]).reset_index(drop=True)
        else:
            left = X_arg.groupby(X_name).apply(lambda x: x.loc[x.loc[x[X_name] <= med].index]).reset_index(drop=True)
            right = X_arg.groupby(X_name).apply(lambda x: x.loc[x.loc[x[X_name] > med].index]).reset_index(drop=True)
        """如果左右组有一个为空,说明这个节点已经分完了"""
        if left.empty or right.empty:
            return
        # print(f"当前节点以【{X_name}】为分组特征,中位数为{med},【{X_name}】方差为{X_arg[X_name].std()}")
        # print(X_arg)
        node.value=[X_name,med]
        # print(f"【————————左子树(left)————————】")
        # print(left)
        node_left = Node_1(left)
        node.left = node_left
        self.get_value_1(left, node_left)
        # print(f"【————————右子树(right)————————】")
        # print(right)
        node_right = Node_1(right)
        node.right = node_right
        self.get_value_1(right, node_right)

分的过程,很漂亮,我看了很多次。。。肉眼数它是怎么分的!!!瞎了
在这里插入图片描述

最终分组后的数据,都分在叶子节点上【全挂在树梢,中间节点不挂】

2.2 搜索最近邻&K近邻

搜索最近邻和K近邻也并不容易,用到了递归

我现在对递归,真的是轻车熟路!!!!
痛苦,总是让人印象深刻

最近邻,主要还是用到了欧式距离的计算

class Find_KD_Tree:
    K = []
    Y_hats = []
    def __init__(self,X_arg):
        self.k = 10
        self.K_list = []
        self.min_point = []
        self.X = X_arg
    def find_K(self,node_arg, X_arg):
        row, columns_1 = node_arg.X.shape
        if not node_arg.value:
            """计算叶子与当前数据的欧式距离:叶子上都是一样的数据,选择其中一个数据与当前数据进行计算即可。"""
            k_distance = ((node_arg.X.loc[0][0:columns_1 - 1] - X_arg[0:columns_1 - 1]) ** 2).sum()
            if (not self.min_point) or (k_distance < self.min_point[0]):  # 当没有最小值,或比最小值更小时,更新min_point
                self.min_point = [k_distance, node_arg]
            if len(self.K_list) < self.k:
                for i in range(row):
                    if len(self.K_list) <= self.k:
                        self.K_list.append([k_distance, node_arg.X.loc[i]])
                        self.K_list = sorted(self.K_list, key=lambda cus: cus[0], reverse=False)
                    elif k_distance < self.K_list[-1][0]:
                        self.K_list.pop()
                        self.K_list.append([k_distance, node_arg.X.loc[i]])
                        self.K_list = sorted(self.K_list, key=lambda cus: cus[0], reverse=False)
                    else:
                        break
            elif k_distance < self.K_list[-1][0]:
                for i in range(row):
                    self.K_list.pop()
                    self.K_list.append([k_distance, node_arg.X.loc[i]])
                    self.K_list = sorted(self.K_list, key=lambda cus: cus[0], reverse=False)
                    if k_distance > self.K_list[-1][0]:
                        break
        else:

            X_name = node_arg.value[0]
            X_value = node_arg.value[1]
            node_distance = (X_arg[X_name] - X_value) ** 2
            """
                1、为对象选择搜索方向:根据每个节点的划分维度,来判断对象是节点的左侧,还是右侧分
                2、判断最近邻是否有可能在节点的另外一侧:递归回溯判断每个子节点的维度距离,是否小于最近邻
                    (若小于最近邻,则最近邻也有可能在节点另一侧,进而去搜索)
                3、判断 K近邻是否有可能在节点的另外一侧:递归回溯判断每个子节点的维度距离,是否小于K列表末尾最大的距离
                    (若小于K列表里的最大距离,则在节点另一侧也有可能存在K近邻数据点,进而去搜索)
            """
            if X_arg[X_name] <= X_value:
                self.find_K(node_arg.left, X_arg)
                if node_distance <= self.min_point[0] or node_distance<self.K_list[-1][0]:
                    self.find_K(node_arg.right, X_arg)
            else:
                self.find_K(node_arg.right, X_arg)
                if node_distance <= self.min_point[0] or node_distance<self.K_list[-1][0]:
                    self.find_K(node_arg.left, X_arg)

最终将每一条数据的K近邻,存储在一个二维列表中,输出结果如下

有些K近邻的列表,不足k个(我设为10个)元素,主要还是因为回溯到上一个维度时,维度距离超过了全局最小距离,因此就不再搜索另一边的树了。
搜索最近邻和K近邻的过程,比较简单:(主要是要细讲逻辑,太复杂了。。。放弃细讲。。。)

在这里插入图片描述

2.3 预测分类

最终,只需要将上述每个对象的K近邻,通过统计,得出它们各自类型最多的那个分类,即为预测分类的结果。

相比于前边徒手建立二叉树,和徒手进行K近邻、最近邻的回溯搜索来说,这一步真是太过easy,我都已经不稀罕浪费脑细胞来详写了!!!
待回头,收拾旧心情,再来写

来写了

    def predict(self):
        dict_temp = {}
        y_hat_single = [i[1][-1] for i in self.K_list]
        Find_KD_Tree.K.append(y_hat_single)
        for i in set(y_hat_single):
            dict_temp[i] = y_hat_single.count(i)
        max_key = max(dict_temp, key=dict_temp.get)
        Find_KD_Tree.Y_hats.append(max_key)
        print(f"K近邻的分类统计为:{dict_temp},👉👉👉👉预测分类为:【{max_key}】")

在这里插入图片描述

3. KNN - 手动、sk代码、GPT代码效果对比

3.1 初版手动代码

""" 难点:构思要如何建立KD树,如何回溯找出最近邻和K近邻"""
import math
import time
import pandas as pd

datas = pd.read_excel('./datas1.xlsx')
important_features = ['推荐类型','推荐分值', '辅导老师专业度','回复速度']
datas_1 = datas[important_features]

# 明确实值Y为'推荐分值',X分别为'专业度','回复速度','用户群活跃天数'
Y = datas_1['推荐类型']
X = datas_1.drop('推荐类型',axis=1)
# X 归一化处理
data = (X-X.min())/(X.max()-X.min())
data["推荐类型"] = Y
# print(data)
"""
构建KNN二叉树
# 构造二叉树的节点、构造二叉树的内容
    ① 二叉树的节点是node:包含 4 个属性 - 值value、左left、右left、数据组
    ② 二叉树的内容是三个节点:根节点、左节点、右节点
# 迭代创建二叉树的三个关键:
    ① 停止迭代的条件:特征方差为0或Nan(无法计算方差)
    ② 如何分组迭代:选择方差最大的特征,按该特征的中位数,对数据进行分组,再对组调用构造二叉树的方法
"""



# 构建一个二叉树节点
class Node_1():
    def __init__(self,X_value):
        self.left = None
        self.right = None
        self.value = []
        self.X = X_value # {"特证名":"x","划分标准":"aaaaaa"}
# 根据节点,构建一个二叉树
class Tree():
    def __init__(self):
        self.root = None
    def get_value_1(self,X_arg,node_arg=None):
        node = node_arg
        if self.root == None:
            node = Node_1(X_arg)
            self.root = node
        vars = [X_arg[i].var() for i in X_arg.columns[0:2]]
        if max(vars) == 0 or math.isnan(max(vars)):
            return
        X_name = X_arg.columns[vars.index(max(vars))]
        med = X_arg[X_name].median()
        if med == X_arg[X_name].max():
            """要根据中位数,分为左组、右组:涉及到pandas的分组条件筛选"""
            left = X_arg.groupby(X_name).apply(lambda x: x.loc[x.loc[x[X_name] < med].index]).reset_index(drop=True)
            right = X_arg.groupby(X_name).apply(lambda x: x.loc[x.loc[x[X_name] >= med].index]).reset_index(drop=True)
        else:
            left = X_arg.groupby(X_name).apply(lambda x: x.loc[x.loc[x[X_name] <= med].index]).reset_index(drop=True)
            right = X_arg.groupby(X_name).apply(lambda x: x.loc[x.loc[x[X_name] > med].index]).reset_index(drop=True)
        if left.empty or right.empty:
            return

        node.value=[X_name,med]
        
        node_left = Node_1(left)
        node.left = node_left
        self.get_value_1(left, node_left)

        node_right = Node_1(right)
        node.right = node_right
        self.get_value_1(right, node_right)

    def pre_print(self,root):
        if root is None:
            return
        self.pre_print(root.left)
        self.pre_print(root.right)

class Find_KD_Tree:
    K = []
    Y_hats = []
    def __init__(self,X_arg):
        self.k = 10
        self.K_list = []
        self.min_point = []
        self.X = X_arg
    def find_K(self,node_arg, X_arg):
        row, columns_1 = node_arg.X.shape
        if not node_arg.value:
            """计算叶子与当前数据的欧式距离:叶子上都是一样的数据,选择其中一个数据与当前数据进行计算即可。"""
            k_distance = ((node_arg.X.loc[0][0:columns_1 - 1] - X_arg[0:columns_1 - 1]) ** 2).sum()
            if (not self.min_point) or (k_distance < self.min_point[0]):  # 当没有最小值,或比最小值更小时,更新min_point
                self.min_point = [k_distance, node_arg]
            if len(self.K_list) < self.k:
                for i in range(row):
                    if len(self.K_list) <= self.k:
                        self.K_list.append([k_distance, node_arg.X.loc[i]])
                        self.K_list = sorted(self.K_list, key=lambda cus: cus[0], reverse=False)
                    elif k_distance < self.K_list[-1][0]:
                        self.K_list.pop()
                        self.K_list.append([k_distance, node_arg.X.loc[i]])
                        self.K_list = sorted(self.K_list, key=lambda cus: cus[0], reverse=False)
                    else:
                        break
            elif k_distance < self.K_list[-1][0]:
                for i in range(row):
                    self.K_list.pop()
                    self.K_list.append([k_distance, node_arg.X.loc[i]])
                    self.K_list = sorted(self.K_list, key=lambda cus: cus[0], reverse=False)
                    if k_distance > self.K_list[-1][0]:
                        break
        else:

            X_name = node_arg.value[0]
            X_value = node_arg.value[1]
            node_distance = (X_arg[X_name] - X_value) ** 2
            """
                1、为对象选择搜索方向:根据每个节点的划分维度,来判断对象是节点的左侧,还是右侧分
                2、判断最近邻是否有可能在节点的另外一侧:递归回溯判断每个子节点的维度距离,是否小于最近邻
                    (若小于最近邻,则最近邻也有可能在节点另一侧,进而去搜索)
                3、判断 K近邻是否有可能在节点的另外一侧:递归回溯判断每个子节点的维度距离,是否小于K列表末尾最大的距离
                    (若小于K列表里的最大距离,则在节点另一侧也有可能存在K近邻数据点,进而去搜索)
            """
            if X_arg[X_name] <= X_value:
                self.find_K(node_arg.left, X_arg)
                if node_distance <= self.min_point[0] or node_distance<self.K_list[-1][0]:
                    self.find_K(node_arg.right, X_arg)
            else:
                self.find_K(node_arg.right, X_arg)
                if node_distance <= self.min_point[0] or node_distance<self.K_list[-1][0]:
                    self.find_K(node_arg.left, X_arg)
    def predict(self):
        dict_temp = {}
        y_hat_single = [i[1][-1] for i in self.K_list]
        Find_KD_Tree.K.append(y_hat_single)
        for i in set(y_hat_single):
            dict_temp[i] = y_hat_single.count(i)
        max_key = max(dict_temp, key=dict_temp.get)
        Find_KD_Tree.Y_hats.append(max_key)
        print(f"K近邻的分类统计为:{dict_temp},👉👉👉👉预测分类为:【{max_key}】")

a = Tree()
a.get_value_1(data)

def func(datas_arg):
    ob = Find_KD_Tree(datas_arg)
    ob.find_K(a.root,datas_arg)
    ob.predict()
data.apply(func,axis = 1)

compares = sum([Y[i]==Find_KD_Tree.Y_hats[i] for i in range(Y.shape[0])])
print(f"分类准确率为:{round(compares/Y.shape[0]*100,2)}%")

3.2 sklearn代码

from sklearn import neighbors
import time
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report

# 获取所需数据:'推荐分值', '专业度','回复速度','用户群活跃天数'
datas = pd.read_excel('./datas1.xlsx')
important_features = ['推荐类型','推荐分值', '专业度','回复速度','用户群活跃天数']
datas_1 = datas[important_features]

# 明确实值Y为'推荐分值',X分别为'专业度','回复速度','用户群活跃天数'
Y = datas_1['推荐类型']
X = datas_1.drop('推荐类型',axis=1)

start_time = time.time()
# 1. 建立模型
classifier = neighbors.KNeighborsClassifier(10)
classifier.fit(X,Y)

# 2. 学习模型
classifier.fit(X,Y)
Y_predict = classifier.predict(X)
result_P = classifier.predict_proba(X)
end_time = time.time()

# 3. 衡量模型
accurency = classifier.score(X,Y)
PRF = classification_report(Y,Y_predict)


# 输出模型最优状态下的参数及衡量模型的指标
print(f"KNN 耗时{end_time-start_time}秒")
print("KNN的分类【准备率】为:",accurency)
print("KNN的精确率、召回率、F1分数为:")
print(PRF)

print('【模型分类,实际分类】的对比如下:')
for index,value in enumerate(zip(Y_predict,Y)):
    print(value)
    print(result_P[index])
# print(result_P)




3.3 分类准确率对比

sklearn的准确率和手动代码的准确率对比,差别不大

在这里插入图片描述

为什么会有差别呢??
这是一个值得细思,反思的问题

3.4 手动代码KNN的实现与书中的思路稍有不同

首先,手动代码中的分类规则,我自作聪明地进行了修改。

修改点1:分组的判断方式不同
修改点2:确定分组维度的方式不同

逐一反思

修改点1:分组的判断方式不同

书中的分组判断方式,是根据中位数的位置,分为数据量相等的左、右两组。
而我不是

中位数的计算,我不是根据数据量的中间值来计算获取的,而是根据pandas的median函数,偷懒让它计算的!!!

为什么我要偷懒呢?主要还是为了避免每次分组,都要对数据重新根据维度来排序。

原想着能够减少每次分组前的排序过程,降低运算量。

但这也带来了一个问题,那就是我的分组,不是分成数据量相等的左、右两组了。
因为median函数获取到中位数时,我没有排序,而是与median中位数值比较大小,分为左、右两组

  • 比median大的,分在右组
  • 比median小的,分在左组

听起来也很合理对吧,只需要比较,还免去了排序这一步骤
但也带来了一个问题,那就是与median中位数的比较分组,无法分为数据量相等的左、右两组
在这里插入图片描述
因此,不知道是不是我的自作聪明,导致分类效果有细微的差别

修改点2:确定分组维度的方式不同

书中的分组维度,是按顺序更换的维度

而我,是根据方差大小来选择的分组维度,如果维度的方差较大,说明数据比较分散,因此可以先对这个维度进行分组。

目前修改下来,分类效果虽然略低于SKlearn的分类效果,但实际还是相差不大的。

想看看如果在没有【推荐分值】这个因素的影响下,对比一下分类效果

在这里插入图片描述

那还是手动的分类效果相对好一些的。

因此,总结来看:手动KNN的思路是没问题的。

3.5 ChatGPT的代码对比

等自己折磨完自己的脑子后,才想起GPT也是大有可为的!!!!
GPT给出的算法思路,基本是与书上相似的
且在搜索K近邻和最近邻时,是分开的
我直接合在一起了,其实还是分开会比较清晰
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值