机器学习 实验二 K-近邻算法及应用

1. 作业信息

这个作业属于哪个课程 机器学习
这个作业要求在哪里 作业要求
学号 3180701312

2.实验目的

(1)理解K-近邻算法原理,能实现算法K近邻算法;
(2)掌握常见的距离度量方法;
(3)掌握K近邻树实现算法;
(4)针对特定应用场景及数据,能应用K近邻解决实际问题。

3.实验内容

(1)实现曼哈顿距离、欧氏距离、闵式距离算法,并测试算法正确性。
(2)实现K近邻树算法;
(3)针对iris数据集,应用sklearn的K近邻算法进行类别预测。
(4)针对iris数据集,编制程序使用K近邻树进行类别预测。

4.实验报告要求

(1)对照实验内容,撰写实验过程、算法及测试结果;
(2)代码规范化:命名规则、注释;
(3)分析核心算法的复杂度;
(4)查阅文献,讨论K近邻的优缺点;
(5)举例说明K近邻的应用场景。按实验内容撰写实验过程;

5.代码

代码一:三点之中,与点1最近的点

import math
from itertools import combinations
#当p=1时,就是曼哈顿距离;
#当p=2时,就是欧氏距离;
#当p→∞时,就是切比雪夫距离。
def L(x, y, p=2):
    # x1 = [1, 1]
    if len(x) == len(y) and len(x) > 1:
        sum = 0
        for i in range(len(x)):
            sum += math.pow(abs(x[i] - y[i]), p)
        return math.pow(sum, 1/p)
    else:
        return 0

x1 = [1, 1]
x2 = [5, 1]
x3 = [4, 4]
# x1与x2和x3的距离
for i in range(1, 5): #i取值1,2,3,4
    r = { '1-{}'.format(c):L(x1, c, p=i) for c in [x2, x3]}
    print(min(zip(r.values(), r.keys()))) #当p=i时x2和x3中离x1最近的点的距离

结果:
(4.0, '1-[5, 1]')
(4.0, '1-[5, 1]')
(3.7797631496846193, '1-[4, 4]')
(3.5676213450081633, '1-[4, 4]')

代码二:手动编写K近邻算法

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter

# 获取数据
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
data = np.array(df.iloc[:100, [0, 1, -1]]) #iloc函数:通过行号来取行数据,读取数据前100行的第0,1列和最后一列

#画出数据散点图
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0') #将数据的前50个数据绘制散点图
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1') #将数据的50-100之间的数据绘制成散点图
plt.xlabel('sepal length') #给x坐标命名
plt.ylabel('sepal width') #给y坐标命名
plt.legend()

结果:

X, y = data[:,:-1], data[:,-1] #X为data数据中除去最后一列的数据,y为data数据的最后一列(y中有两类0和1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) #从样本中随机的按比例选取train data和testdata,样本占比为0.2及20
#生成K近邻算法
class KNN:
    #初始化
    def __init__(self, X_train, y_train, n_neighbors=3, p=2): #K的值( n_neighbors) 临近点个数; p 距离度量(欧氏距离)
        self.n = n_neighbors
        self.p = p
        self.X_train = X_train
        self.y_train = y_train
    
    def predict(self, X):
        # X为测试集
        knn_list = []
        for i in range(self.n):
            dist = np.linalg.norm(X - self.X_train[i], ord=self.p) #计算测试集与(0-2)训练集的欧式距离
            knn_list.append((dist, self.y_train[i]))#在列表knn_list最后(末尾)添加一个元素(dist, self.y_train[i])
            
        for i in range(self.n, len(self.X_train)):#3-20
            max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))#找出距离最大的点
            dist = np.linalg.norm(X - self.X_train[i], ord=self.p) #计算测试集与(3-20)训练集的欧式距离
            if knn_list[max_index][0] > dist:   #距离最大的点大于新测试的点,就替换掉距离最大的点
                knn_list[max_index] = (dist, self.y_train[i])
                
        # 统计
        knn = [k[-1] for k in knn_list]
        count_pairs = Counter(knn)   #统计各个标签的个数如 蓝点:2 ;黄点:1
        max_count = sorted(count_pairs, key=lambda x:x)[-1] #升序排序,取个数最大的标签
        return max_count
    
    #用测试集测试算法的正确率
    def score(self, X_test, y_test):
        right_count = 0 
        n = 10
        for X, y in zip(X_test, y_test):
            label = self.predict(X)
            if label == y:
                right_count += 1
        return right_count / len(X_test)
clf = KNN(X_train, y_train)#生成一个算法对象
clf.score(X_test, y_test)#将测试数据代入算法中

结果:
1.0

test_point = [6.0, 3.0]
print('Test Point: {}'.format(clf.predict(test_point))) #测试数据(6,3)应该属于哪一类

结果:
Test Point: 1.0

plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')#将数据的前50个数据绘制散点图
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')#将数据的50-100个数据绘制散点图
plt.plot(test_point[0], test_point[1], 'bo', label='test_point') #将测试数据点(3,6)绘制在图中
plt.xlabel('sepal length') #给x坐标命名
plt.ylabel('sepal width') #给y坐标命名
plt.legend()  #表示不同图形的文本标签图案

结果:

代码三:使用KNeighborsClassifier验证及引入K近邻算法对比验证

from sklearn.neighbors import KNeighborsClassifier
clf_sk = KNeighborsClassifier()
#sklearn.neighbors.KNeighborsClassifier
#n_neighbors: 临近点个数
#p: 距离度量
#algorithm: 近邻算法,可选{'auto', 'ball_tree', 'kd_tree', 'brute'}
#weights: 确定近邻的权重
clf_sk.fit(X_train, y_train)

KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=None, n_neighbors=5, p=2,
weights='uniform')

clf_sk.score(X_test, y_test) #测试精确度

结果:1.0

clf_sk.predict([[6,3]]) #测试数据[6,3]应该属于哪一类

结果:array([1.])

代码四:针对iris数据集,编制程序使用K近邻树进行类别预测。

import numpy as np
import time
import pandas as pd

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

class Node(object):
    '''结点对象'''
    def __init__(self, item=None, label=None, dim=None, parent=None, left_child=None, right_child=None):
        self.item = item   # 结点的值(样本信息)
        self.label = label  # 结点的标签
        self.dim = dim   # 结点的切分的维度(特征)
        self.parent = parent  # 父结点
        self.left_child = left_child  # 左子树
        self.right_child = right_child # 右子树


class KDTree(object):
    '''kd树'''

    def __init__(self, aList, labelList):
        self.__length = 0  # 不可修改
        self.__root = self.__create(aList,labelList)  # 根结点, 私有属性, 不可修改

    def __create(self, aList, labelList, parentNode=None):
        '''
        创建kd树
        :param aList: 需要传入一个类数组对象(行数表示样本数,列数表示特征数)
        :labellist: 样本的标签
        :parentNode: 父结点
        :return: 根结点
        '''
        dataArray = np.array(aList)
        m,n = dataArray.shape
        labelArray = np.array(labelList).reshape(m,1)
        if m == 0:  # 样本集为空
            return None
        # 求所有特征的方差,选择最大的那个特征作为切分超平面
        var_list = [np.var(dataArray[:,col]) for col in range(n)]  # 获取每一个特征的方差
        max_index = var_list.index(max(var_list))  # 获取最大方差特征的索引
        # 样本按最大方差特征进行升序排序后,取出位于中间的样本
        max_feat_ind_list = dataArray[:,max_index].argsort()
        mid_item_index = max_feat_ind_list[m // 2]
        if m == 1:  # 样本为1时,返回自身
            self.__length += 1
            return Node(dim=max_index,label=labelArray[mid_item_index], item=dataArray[mid_item_index], parent=parentNode, left_child=None, right_child=None)

        # 生成结点
        node = Node(dim=max_index, label=labelArray[mid_item_index], item=dataArray[mid_item_index], parent=parentNode, )
        # 构建有序的子树
        left_tree = dataArray[max_feat_ind_list[:m // 2]] # 左子树
        left_label = labelArray[max_feat_ind_list[:m // 2]] # 左子树标签
        left_child = self.__create(left_tree,left_label,node)
        if m == 2:  # 只有左子树,无右子树
            right_child = None
        else:
            right_tree = dataArray[max_feat_ind_list[m // 2 + 1:]] # 右子树
            right_label = labelArray[max_feat_ind_list[m // 2 + 1:]] # 右子树标签
            right_child = self.__create(right_tree,right_label,node)
            # self.__length += 1
        # 左右子树递归调用自己,返回子树根结点
        node.left_child=left_child
        node.right_child=right_child
        self.__length += 1
        return node

    @property
    def length(self):
        return self.__length

    @property
    def root(self):
        return self.__root

    def transfer_dict(self,node):
        '''
        查看kd树结构
        :node:需要传入根结点对象
        :return: 字典嵌套格式的kd树,字典的key是self.item,其余项作为key的值,类似下面格式
        {(1,2,3):{
                'label':1,
                'dim':0,
                'left_child':{(2,3,4):{
                                     'label':1,
                                     'dim':1,
                                     'left_child':None,
                                     'right_child':None
                                    },
                'right_child':{(4,5,6):{
                                        'label':1,
                                        'dim':1,
                                        'left_child':None,
                                        'right_child':None
                                        }
                }
        '''
        if node == None:
            return None
        kd_dict = {}
        kd_dict[tuple(node.item)] = {}  # 将自身值作为key
        kd_dict[tuple(node.item)]['label'] = node.label[0]
        kd_dict[tuple(node.item)]['dim'] = node.dim
        kd_dict[tuple(node.item)]['parent'] = tuple(node.parent.item) if node.parent else None
        kd_dict[tuple(node.item)]['left_child'] = self.transfer_dict(node.left_child)
        kd_dict[tuple(node.item)]['right_child'] = self.transfer_dict(node.right_child)
        return kd_dict

    def transfer_list(self,node, kdList=[]):
        '''
        将kd树转化为列表嵌套字典的嵌套字典的列表输出
        :param node: 需要传入根结点
        :return: 返回嵌套字典的列表
        '''
        if node == None:
            return None
        element_dict = {}
        element_dict['item'] = tuple(node.item)
        element_dict['label'] = node.label[0]
        element_dict['dim'] = node.dim
        element_dict['parent'] = tuple(node.parent.item) if node.parent else None
        element_dict['left_child'] = tuple(node.left_child.item) if node.left_child else None
        element_dict['right_child'] = tuple(node.right_child.item) if node.right_child else None
        kdList.append(element_dict)
        self.transfer_list(node.left_child, kdList)
        self.transfer_list(node.right_child, kdList)
        return kdList

    def _find_nearest_neighbour(self, item):
        '''
        找最近邻点
        :param item:需要预测的新样本
        :return: 距离最近的样本点
        '''
        itemArray = np.array(item)
        if self.length == 0:  # 空kd树
            return None
        # 递归找离测试点最近的那个叶结点
        node = self.__root
        if self.length == 1: # 只有一个样本
            return node
        while True:
            cur_dim = node.dim
            if item[cur_dim] == node.item[cur_dim]:
                return node
            elif item[cur_dim] < node.item[cur_dim]:  # 进入左子树
                if node.left_child == None:  # 左子树为空,返回自身
                    return node
                node = node.left_child
            else:
                if node.right_child == None:  # 右子树为空,返回自身
                    return node
                node = node.right_child

    def knn_algo(self, item, k=1):
        '''
        找到距离测试样本最近的前k个样本
        :param item: 测试样本
        :param k: knn算法参数,定义需要参考的最近点数量,一般为1-5
        :return: 返回前k个样本的最大分类标签
        '''
        if self.length <= k:
            label_dict = {}
            # 获取所有label的数量
            for element in self.transfer_list(self.root):
                if element['label'] in label_dict:
                    label_dict[element['label']] += 1
                else:
                    label_dict[element['label']] = 1
            sorted_label = sorted(label_dict.items(), key=lambda item:item[1],reverse=True)  # 给标签排序
            return sorted_label[0][0]

        item = np.array(item)
        node = self._find_nearest_neighbour(item)  # 找到最近的那个结点
        if node == None:  # 空树
            return None
        print('靠近点%s最近的叶结点为:%s'%(item, node.item))
        node_list = []
        distance = np.sqrt(sum((item-node.item)**2))  # 测试点与最近点之间的距离
        least_dis = distance
        # 返回上一个父结点,判断以测试点为圆心,distance为半径的圆是否与父结点分隔超平面相交,若相交,则说明父结点的另一个子树可能存在更近的点
        node_list.append([distance, tuple(node.item), node.label[0]])  # 需要将距离与结点一起保存起来

        # 若最近的结点不是叶结点,则说明,它还有左子树
        if node.left_child != None:
            left_child = node.left_child
            left_dis = np.sqrt(sum((item-left_child.item)**2))
            if k > len(node_list) or least_dis < least_dis:
                node_list.append([left_dis, tuple(left_child.item), left_child.label[0]])
                node_list.sort()  # 对结点列表按距离排序
                least_dis = node_list[-1][0] if k >= len(node_list) else node_list[k-1][0]
        # 回到父结点
        while True:
            if node == self.root:  # 已经回到kd树的根结点
                break
            parent = node.parent
            # 计算测试点与父结点的距离,与上面距离做比较
            par_dis = np.sqrt(sum((item-parent.item)**2))
            if k >len(node_list) or par_dis < least_dis:  # k大于结点数或者父结点距离小于结点列表中最大的距离
                node_list.append([par_dis, tuple(parent.item) , parent.label[0]])
                node_list.sort()  # 对结点列表按距离排序
                least_dis = node_list[-1][0] if k >= len(node_list) else node_list[k - 1][0]

            # 判断父结点的另一个子树与结点列表中最大的距离构成的圆是否有交集
            if k >len(node_list) or abs(item[parent.dim] - parent.item[parent.dim]) < least_dis :  # 说明父结点的另一个子树与圆有交集
                # 说明父结点的另一子树区域与圆有交集
                other_child = parent.left_child if parent.left_child != node else parent.right_child  # 找另一个子树
                # 测试点在该子结点超平面的左侧
                if other_child != None:
                    if item[parent.dim] - parent.item[parent.dim] <= 0:
                        self.left_search(item,other_child,node_list,k)
                    else:
                        self.right_search(item,other_child,node_list,k)  # 测试点在该子结点平面的右侧

            node = parent  # 否则继续返回上一层
        # 接下来取出前k个元素中最大的分类标签
        label_dict = {}
        node_list = node_list[:k]
        # 获取所有label的数量
        for element in node_list:
            if element[2] in label_dict:
                label_dict[element[2]] += 1
            else:
                label_dict[element[2]] = 1
        sorted_label = sorted(label_dict.items(), key=lambda item:item[1], reverse=True)  # 给标签排序
        return sorted_label[0][0],node_list

    def left_search(self, item, node, nodeList, k):
        '''
        按左中右顺序遍历子树结点,返回结点列表
        :param node: 子树结点
        :param item: 传入的测试样本
        :param nodeList: 结点列表
        :param k: 搜索比较的结点数量
        :return: 结点列表
        '''
        nodeList.sort()  # 对结点列表按距离排序
        least_dis = nodeList[-1][0] if k >= len(nodeList) else nodeList[k - 1][0]
        if node.left_child == None and node.right_child == None:  # 叶结点
            dis = np.sqrt(sum((item - node.item) ** 2))
            if k > len(nodeList) or dis < least_dis:
                nodeList.append([dis, tuple(node.item), node.label[0]])
            return
        self.left_search(item, node.left_child, nodeList, k)
        # 每次进行比较前都更新nodelist数据
        nodeList.sort()  # 对结点列表按距离排序
        least_dis = nodeList[-1][0] if k >= len(nodeList) else nodeList[k - 1][0]
        # 比较根结点
        dis = np.sqrt(sum((item-node.item)**2))
        if k > len(nodeList) or dis < least_dis:
            nodeList.append([dis, tuple(node.item), node.label[0]])
        # 右子树
        if k > len(nodeList) or abs(item[node.dim] - node.item[node.dim]) < least_dis: # 需要搜索右子树
            if node.right_child != None:
                self.left_search(item, node.right_child, nodeList, k)

        return nodeList

    def right_search(self,item, node, nodeList, k):
        '''
        按右根左顺序遍历子树结点
        :param item: 测试的样本点
        :param node: 子树结点
        :param nodeList: 结点列表
        :param k: 搜索比较的结点数量
        :return: 结点列表
        '''
        nodeList.sort()  # 对结点列表按距离排序
        least_dis = nodeList[-1][0] if k >= len(nodeList) else nodeList[k - 1][0]
        if node.left_child == None and node.right_child == None:  # 叶结点
            dis = np.sqrt(sum((item - node.item) ** 2))
            if k > len(nodeList) or dis < least_dis:
                nodeList.append([dis, tuple(node.item), node.label[0]])
            return
        if node.right_child != None:
            self.right_search(item, node.right_child, nodeList, k)

        nodeList.sort()  # 对结点列表按距离排序
        least_dis = nodeList[-1][0] if k >= len(nodeList) else nodeList[k - 1][0]
        # 比较根结点
        dis = np.sqrt(sum((item - node.item) ** 2))
        if k > len(nodeList) or dis < least_dis:
            nodeList.append([dis, tuple(node.item), node.label[0]])
        # 左子树
        if k > len(nodeList) or abs(item[node.dim] - node.item[node.dim]) < least_dis: # 需要搜索左子树
            self.right_search(item, node.left_child, nodeList, k)

        return nodeList


if __name__ == '__main__':
    t1 = time.time()
    # 获取数据
    iris = load_iris()
    df = pd.DataFrame(iris.data, columns=iris.feature_names)
    df['label'] = iris.target
    df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
    data = np.array(df.iloc[:100, [0, 1, -1]]) 
    X, y = data[:,:-1], data[:,-1] #X为data数据中除去最后一列的数据,y为data数据的最后一列(y中有两类0和1)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) #从样本中随机的按比例选取train data和testdata,样本占比为0.2及20
    kd_tree = KDTree(X_train,y_train)
    t2 = time.time()
    label, node_list = kd_tree.knn_algo([6,3],k=5)
    print('点%s的最接近的前k个点为:%s'%([6,3], node_list))
    print('点%s的标签:%s'%([6,3],label))
    t3 = time.time()
    print('创建树耗时:',t2-t1)
    print('搜索前k个最近邻点耗时:',t3-t2)

结果:
靠近点[6 3]最近的叶结点为:[6.1 3. ]
点[6, 3]的最接近的前k个点为:[[0.09999999999999964, (5.9, 3.0), 1.0], [0.09999999999999964, (6.1, 3.0), 1.0], [0.10000000000000009, (6.0, 2.9), 1.0], [0.22360679774997896, (5.9, 3.2), 1.0], [0.22360679774997896, (6.1, 2.8), 1.0]]
点[6, 3]的标签:1.0
创建树耗时: 0.015619754791259766
搜索前k个最近邻点耗时: 0.015622138977050781

6.小结

(1)两个n维变量a(x11,x12,…,x1n)与b(x21,x22,…,x2n)间的闵可夫斯基距离定义为:

n维空间点a(x11,x12,…,x1n)与b(x21,x22,…,x2n)间的欧氏距离(两个n维向量):

(2)K近邻算法优点
k近邻算法是一种在线技术,新数据可以直接加入数据集而不必进行重新训练,k近邻算法理论简单,容易实现,准确性高,对异常值和噪声有较高的容忍度。k近邻算法天生就支持多分类,区别与感知机、逻辑回归、SVM。
K近邻算法缺点
基本的 k近邻算法每预测一个“点”的分类都会重新进行一次全局运算,对于样本容量大的数据集计算量比较大。而且K近邻算法容易导致维度灾难,在高维空间中计算距离的时候,就会变得非常远;样本不平衡时,预测偏差比较大,k值大小的选择得依靠经验或者交叉验证得到。

posted @ 2021-05-13 16:52  咸豆腐脑棒  阅读(204)  评论(0编辑  收藏  举报