zoukankan      html  css  js  c++  java
  • 实验二 K-近邻算法及应用

    【实验目的】

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

    【实验内容】

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

    【实验报告要求】

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

    k近邻法

    1.(k)近邻法是基本且简单的分类与回归方法。(k)近邻法的基本做法是:对给定的训练实例点和输入实例点,首先确定输入实例点的(k)个最近邻训练实例点,然后利用这(k)个训练实例点的类的多数来预测输入实例点的类。
    2.(k)近邻模型对应于基于训练数据集对特征空间的一个划分。(k)近邻法中,当训练集、距离度量、(k)值及分类决策规则确定后,其结果唯一确定。
    3.(k)近邻法三要素:距离度量、(k)值的选择和分类决策规则。常用的距离度量是欧氏距离及更一般的pL距离。(k)值小时,(k)近邻模型更复杂;(k)值大时,(k)近邻模型更简单。(k)值的选择反映了对近似误差与估计误差之间的权衡,通常由交叉验证选择最优的(k)
    常用的分类决策规则是多数表决,对应于经验风险最小化。
    4.(k)近邻法的实现需要考虑如何快速搜索k个最近邻点。kd树是一种便于对k维空间中的数据进行快速检索的数据结构。kd树是二叉树,表示对(k)维空间的一个划分,其每个结点对应于(k)维空间划分中的一个超矩形区域。利用kd树可以省去对大部分数据点的搜索, 从而减少搜索的计算量。

    距离度量

    设特征空间(x)(n)维实数向量空间 ,(x_{i}, x_{j} in mathcal{X}),(x_{i}=left(x_{i}^{(1)}, x_{i}^{(2)}, cdots, x_{i}^{(n)} ight)^{mathrm{T}}),(x_{j}=left(x_{j}^{(1)}, x_{j}^{(2)}, cdots, x_{j}^{(n)} ight)^{mathrm{T}})
    ,则:(x_i),(x_j)(L_p)距离定义为:
    (L_{p}left(x_{i}, x_{j} ight)=left(sum_{i=1}^{n}left|x_{i}^{(i)}-x_{j}^{(l)} ight|^{p} ight)^{frac{1}{p}})

    • (p= 1) 曼哈顿距离
    • (p= 2) 欧氏距离
    • (p= infty) 切比雪夫距离
    import math
    from itertools import combinations
    def L(x, y, p=2):
        # x1 = [1, 1], x2 = [5,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
    for i in range(1, 5):
        r = {'1-{}'.format(c): L(x1, c, p=i) for c in [x2, x3]}
        print(min(zip(r.values(), r.keys())))
    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
    
    # data
    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]])
    
    df
    
    plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
    plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
    plt.xlabel('sepal length')
    plt.ylabel('sepal width')
    plt.legend()
    

    data = np.array(df.iloc[:100, [0, 1, -1]])
    X, y = data[:,:-1], data[:,-1]
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
    
    class KNN:
        def __init__(self, X_train, y_train, n_neighbors=3, p=2):
            """
            parameter: n_neighbors 临近点个数
            parameter: p 距离度量
            """
            self.n = n_neighbors
            self.p = p
            self.X_train = X_train
            self.y_train = y_train
    
        def predict(self, X):
            # 取出n个点
            knn_list = []
            for i in range(self.n):
                dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
                knn_list.append((dist, self.y_train[i]))
    
            for i in range(self.n, len(self.X_train)):
                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)
                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)
    #         max_count = sorted(count_pairs, key=lambda x: x)[-1]
            max_count = sorted(count_pairs.items(), key=lambda x: x[1])[-1][0]
            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)
    test_point = [6.0, 3.0]
    print('Test Point: {}'.format(clf.predict(test_point)))
    plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
    plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
    plt.plot(test_point[0], test_point[1], 'bo', label='test_point')
    plt.xlabel('sepal length')
    plt.ylabel('sepal width')
    plt.legend()
    

    scikit-learn实例

    from sklearn.neighbors import KNeighborsClassifier
    clf_sk = KNeighborsClassifier()
    clf_sk.fit(X_train, y_train)
    clf_sk.score(X_test, y_test)
    

    kd树

    kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。

    kd树是二叉树,表示对(k)维空间的一个划分(partition)。构造kd树相当于不断地用垂直于坐标轴的超平面将(k)维空间切分,构成一系列的k维超矩形区域。kd树的每个结点对应于一个(k)维超矩形区域。

    构造kd树的方法如下:

    构造根结点,使根结点对应于(k)维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对(k)维空间进行切分,生成子结点。在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面,这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域切分为左右两个子区域
    (子结点);这时,实例被分到两个子区域。这个过程直到子区域内没有实例时终止(终止时的结点为叶结点)。在此过程中,将实例保存在相应的结点上。

    通常,依次选择坐标轴对空间切分,选择训练实例点在选定坐标轴上的中位数
    (median)为切分点,这样得到的kd树是平衡的。注意,平衡的kd树搜索时的效率未必是最优的。

    构造平衡kd树算法

    输入:(k)维空间数据集(T={x_1,x_2,…,x_N})

    其中(x_{i}=left(x_{i}^{(1)}, x_{i}^{(2)}, cdots, x_{i}^{(k)} ight)^{mathrm{T}})(i=1,2,…,N)

    输出:kd树。

    (1)开始:构造根结点,根结点对应于包含(T)(k)维空间的超矩形区域。

    选择(x^{(1)})为坐标轴,以T中所有实例的(x^{(1)})坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴(x^{(1)})垂直的超平面实现。

    由根结点生成深度为1的左、右子结点:左子结点对应坐标(x^{(1)})小于切分点的子区域, 右子结点对应于坐标(x^{(1)})大于切分点的子区域。

    将落在切分超平面上的实例点保存在根结点。

    (2)重复:对深度为(j)的结点,选择(x^{(1)})为切分的坐标轴,(l=j(modk)+1),以该结点的区域中所有实例的(x^{(1)})坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴(x^{(1)})垂直的超平面实现。

    由该结点生成深度为(j+1)的左、右子结点:左子结点对应坐标(x^{(1)})小于切分点的子区域,右子结点对应坐标(x^{(1)})大于切分点的子区域。

    将落在切分超平面上的实例点保存在该结点。

    (3)直到两个子区域没有实例存在时停止。从而形成kd树的区域划分。

    # kd-tree每个结点中主要包含的数据结构如下
    class KdNode(object):
        def __init__(self, dom_elt, split, left, right):
            self.dom_elt = dom_elt  # k维向量节点(k维空间中的一个样本点)
            self.split = split  # 整数(进行分割维度的序号)
            self.left = left  # 该结点分割超平面左子空间构成的kd-tree
            self.right = right  # 该结点分割超平面右子空间构成的kd-tree
    
    
    class KdTree(object):
        def __init__(self, data):
            k = len(data[0])  # 数据维度
    
            def CreateNode(split, data_set):  # 按第split维划分数据集exset创建KdNode
                if not data_set:  # 数据集为空
                    return None
                # key参数的值为一个函数,此函数只有一个参数且返回一个值用来进行比较
                # operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为需要获取的数据在对象中的序号
                #data_set.sort(key=itemgetter(split)) # 按要进行分割的那一维数据排序
                data_set.sort(key=lambda x: x[split])
                split_pos = len(data_set) // 2  # //为Python中的整数除法
                median = data_set[split_pos]  # 中位数分割点
                split_next = (split + 1) % k  # cycle coordinates
    
                # 递归的创建kd树
                return KdNode(
                    median,
                    split,
                    CreateNode(split_next, data_set[:split_pos]),  # 创建左子树
                    CreateNode(split_next, data_set[split_pos + 1:]))  # 创建右子树
    
            self.root = CreateNode(0, data)  # 从第0维分量开始构建kd树,返回根节点
    
    
    # KDTree的前序遍历
    def preorder(root):
        print(root.dom_elt)
        if root.left:  # 节点不为空
            preorder(root.left)
        if root.right:
            preorder(root.right)
    
    # 对构建好的kd树进行搜索,寻找与目标点最近的样本点:
    from math import sqrt
    from collections import namedtuple
    
    # 定义一个namedtuple,分别存放最近坐标点、最近距离和访问过的节点数
    result = namedtuple("Result_tuple",
                        "nearest_point  nearest_dist  nodes_visited")
    
    
    def find_nearest(tree, point):
        k = len(point)  # 数据维度
    
        def travel(kd_node, target, max_dist):
            if kd_node is None:
                return result([0] * k, float("inf"),
                              0)  # python中用float("inf")和float("-inf")表示正负无穷
    
            nodes_visited = 1
    
            s = kd_node.split  # 进行分割的维度
            pivot = kd_node.dom_elt  # 进行分割的“轴”
    
            if target[s] <= pivot[s]:  # 如果目标点第s维小于分割轴的对应值(目标离左子树更近)
                nearer_node = kd_node.left  # 下一个访问节点为左子树根节点
                further_node = kd_node.right  # 同时记录下右子树
            else:  # 目标离右子树更近
                nearer_node = kd_node.right  # 下一个访问节点为右子树根节点
                further_node = kd_node.left
    
            temp1 = travel(nearer_node, target, max_dist)  # 进行遍历找到包含目标点的区域
    
            nearest = temp1.nearest_point  # 以此叶结点作为“当前最近点”
            dist = temp1.nearest_dist  # 更新最近距离
    
            nodes_visited += temp1.nodes_visited
    
            if dist < max_dist:
                max_dist = dist  # 最近点将在以目标点为球心,max_dist为半径的超球体内
    
            temp_dist = abs(pivot[s] - target[s])  # 第s维上目标点与分割超平面的距离
            if max_dist < temp_dist:  # 判断超球体是否与超平面相交
                return result(nearest, dist, nodes_visited)  # 不相交则可以直接返回,不用继续判断
    
            #----------------------------------------------------------------------
            # 计算目标点与分割点的欧氏距离
            temp_dist = sqrt(sum((p1 - p2)**2 for p1, p2 in zip(pivot, target)))
    
            if temp_dist < dist:  # 如果“更近”
                nearest = pivot  # 更新最近点
                dist = temp_dist  # 更新最近距离
                max_dist = dist  # 更新超球体半径
    
            # 检查另一个子结点对应的区域是否有更近的点
            temp2 = travel(further_node, target, max_dist)
    
            nodes_visited += temp2.nodes_visited
            if temp2.nearest_dist < dist:  # 如果另一个子结点内存在更近距离
                nearest = temp2.nearest_point  # 更新最近点
                dist = temp2.nearest_dist  # 更新最近距离
    
            return result(nearest, dist, nodes_visited)
    
        return travel(tree.root, point, float("inf"))  # 从根节点开始递归
    
    data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
    kd = KdTree(data)
    preorder(kd.root)
    from time import clock
    from random import random
    
    # 产生一个k维随机向量,每维分量值在0~1之间
    def random_point(k):
        return [random() for _ in range(k)]
     
    # 产生n个k维随机向量 
    def random_points(k, n):
        return [random_point(k) for _ in range(n)]     
    ret = find_nearest(kd, [3,4.5])
    print (ret)
    
    N = 400000
    t0 = clock()
    kd2 = KdTree(random_points(3, N))            # 构建包含四十万个3维空间样本点的kd树
    ret2 = find_nearest(kd2, [0.1,0.5,0.8])      # 四十万个样本点中寻找离目标最近的点
    t1 = clock()
    print ("time: ",t1-t0, "s")
    print (ret2)
    

    实验结果

    time:  6.945672 s
    Result_tuple(nearest_point=[0.097329902511892, 0.49823498137918865, 0.7964108251004665], nearest_dist=0.004809042294184305, nodes_visited=42)
    

    实验小结

    1.K近邻的优缺点
    优点:
    (1)简单,易于理解,易于实现,无需估计参数。
    (2)训练时间为零。它没有显示的训练,不像其它有监督的算法会用训练集train一个模型(也就是拟合一个函数),然后验证集或测试集用该模型分类。KNN只是把样本保存起来,收到测试数据时再处理,所以KNN训练时间为零。
    (3)KNN可以处理分类问题,同时天然可以处理多分类问题,适合对稀有事件进行分类。
    (4)特别适合于多分类问题(multi-modal,对象具有多个类别标签), KNN比SVM的表现要好。
    (5)KNN还可以处理回归问题,也就是预测。
    (6)和朴素贝叶斯之类的算法比,对数据没有假设,准确度高,对异常点不敏感。
    缺点:
    (1)计算量太大,尤其是特征数非常多的时候。每一个待分类文本都要计算它到全体已知样本的距离,才能得到它的第K个最近邻点。
    (2)可理解性差,无法给出像决策树那样的规则。
    (3)是慵懒散学习方法,基本上不学习,导致预测时速度比起逻辑回归之类的算法慢。
    (4)样本不平衡的时候,对稀有类别的预测准确率低。当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。
    (5)对训练数据依赖度特别大,对训练数据的容错性太差。如果训练数据集中,有一两个数据是错误的,刚刚好又在需要分类的数值的旁边,这样就会直接导致预测的数据的不准确。
    2.K近邻应用场合
    题目描述:海伦喜欢在在线约会网站寻找适合自己的对象,但是她不是喜欢每一个人。她发现交往过三种类型的人:
      1.不喜欢的人
      2.魅力一般的人
      3.极具魅力的人
    所以需要对网站的对象归入恰当的分类。她周一到周五喜欢魅力一般的人,而周末则更喜欢极具魅力的人。所以需要根据数据来分类

  • 相关阅读:
    《增长黑客》阅读内容摘要(前三章)
    ios的安全机制
    R语言  RStudio快捷键总结
    R in action 笔记(第二部分)
    R in action 笔记(第一部分)
    R统计函数-开源
    R语言函数索引-11月
    mysql join的优化实例
    android异步消息处理机制
    android ListView与EditText共存错位
  • 原文地址:https://www.cnblogs.com/smallsung/p/14797631.html
Copyright © 2011-2022 走看看